@@ -604,6 +604,8 @@ let main () =
604604 Buffer. clear notices;
605605 let args = String. strip args in
606606 let last_src = ref " " in
607+ let trace_prefix = ref " " in
608+ let exception Trace_failed of exn in
607609
608610 try
609611 (* Parse quoted or unquoted filename *)
@@ -626,27 +628,32 @@ let main () =
626628 | f :: rest -> (f, String. concat " " rest)
627629 in
628630
629- (* Parse optional LINE[:COL] and flags (-nosmt) *)
630- let upto, nosmt =
631- if rest = " " then (None , false )
632- else
633- let words = String. split_on_char ' ' rest in
634- let words = List. filter (fun s -> s <> " " ) words in
635- let nosmt = List. mem " -nosmt" words in
636- let words = List. filter (fun s -> s <> " -nosmt" ) words in
637- let upto = match words with
638- | [] -> None
639- | [w] ->
640- begin match String. split_on_char ':' w with
641- | [line] ->
642- Some (int_of_string line, None )
643- | [line; col] ->
644- Some (int_of_string line, Some (int_of_string col))
645- | _ -> failwith " LOAD: invalid LINE[:COL] format"
646- end
647- | _ -> failwith " LOAD: unexpected arguments"
648- in
649- (upto, nosmt)
631+ (* Parse optional LINE[:COL] and flags (-nosmt, -trace) *)
632+ let upto, nosmt, trace =
633+ let words =
634+ String. split_on_char ' ' rest
635+ |> List. filter (fun s -> s <> " " )
636+ in
637+ let nosmt = List. mem " -nosmt" words in
638+ let trace = List. mem " -trace" words in
639+ let words =
640+ List. filter
641+ (fun s -> s <> " -nosmt" && s <> " -trace" )
642+ words
643+ in
644+ let upto = match words with
645+ | [] -> None
646+ | [w] ->
647+ begin match String. split_on_char ':' w with
648+ | [line] ->
649+ Some (int_of_string line, None )
650+ | [line; col] ->
651+ Some (int_of_string line, Some (int_of_string col))
652+ | _ -> failwith " LOAD: invalid LINE[:COL] format"
653+ end
654+ | _ -> failwith " LOAD: unexpected arguments"
655+ in
656+ (upto, nosmt, trace)
650657 in
651658
652659 (* Validate file extension *)
@@ -677,27 +684,65 @@ let main () =
677684
678685 let last_loc = ref None in
679686
687+ (* For -trace: lazy whole-file bytes, used to slice the exact
688+ source text of a sentence by byte offsets. *)
689+ let input_bytes = lazy (
690+ let ic = open_in_bin filename in
691+ let n = in_channel_length ic in
692+ let b = Bytes. create n in
693+ really_input ic b 0 n;
694+ close_in ic;
695+ Bytes. unsafe_to_string b)
696+ in
697+ let sentence_source (loc : EcLocation.t ) =
698+ let s = Lazy. force input_bytes in
699+ let lo = max 0 loc.EcLocation. loc_bchar in
700+ let hi = min (String. length s) loc.EcLocation. loc_echar in
701+ if hi < = lo then " " else String. sub s lo (hi - lo)
702+ in
703+
704+ (* For -trace: defer execution of the last sentence within the
705+ prefix so we can capture goals before and after it. *)
706+ let pending : (string * EP.global) option ref = ref None in
707+ let flush_pending () =
708+ match ! pending with
709+ | None -> ()
710+ | Some (src , p ) ->
711+ last_src := src;
712+ process_action ~src p;
713+ last_loc := Some p.EP. gl_action.EcLocation. pl_loc;
714+ pending := None
715+ in
716+ let step src p =
717+ let loc = p.EP. gl_action.EcLocation. pl_loc in
718+ if past_upto loc then raise Exit ;
719+ if trace then begin
720+ flush_pending () ;
721+ pending := Some (src, p)
722+ end else begin
723+ last_src := src;
724+ process_action ~src p;
725+ last_loc := Some loc
726+ end
727+ in
728+
680729 (* In -nosmt mode, admit all SMT calls during prefix loading *)
681730 if nosmt then EcCommands. pragma_check `WeakCheck ;
682731
683732 begin try while true do
684733 let (src, prog) = EcIo. xparse reader in
685734 let src = String. strip src in
686- last_src := src;
687735 match EcLocation. unloc prog with
688736 | EP. P_Prog (commands , locterm ) ->
689- List. iter (fun p ->
690- let loc = p.EP. gl_action.EcLocation. pl_loc in
691- if past_upto loc then raise Exit ;
692- process_action ~src p;
693- last_loc := Some loc
694- ) commands;
737+ List. iter (step src) commands;
695738 if locterm then raise Exit
696739 | EP. P_Undo i ->
740+ last_src := src;
697741 EcCommands. undo i
698742 | EP. P_Exit ->
699743 raise Exit
700744 | EP. P_DocComment doc ->
745+ last_src := src;
701746 EcCommands. doc_comment doc
702747 done with
703748 | Exit | End_of_file -> ()
@@ -712,20 +757,105 @@ let main () =
712757 (* Restore full SMT checking for interactive tactics *)
713758 if nosmt then EcCommands. pragma_check `Check ;
714759
760+ (* If -trace is set, the last in-prefix sentence is still
761+ pending. Run it with goal capture before and after, and
762+ build the BEFORE/TACTIC/AFTER/SUMMARY response body. *)
763+ let body =
764+ if not trace then
765+ goals_to_string ()
766+ else
767+ let pre_state =
768+ match ! pending with
769+ | None -> `Nothing
770+ | Some _ when not (EcCommands. in_proof () ) -> `NotInProof
771+ | Some (src , p ) -> `Ready (src, p)
772+ in
773+ match pre_state with
774+ | `Nothing -> failwith " trace: nothing to trace"
775+ | `NotInProof ->
776+ failwith
777+ " trace: target sentence is not in a proof context"
778+ | `Ready (src , p ) ->
779+ let loc = p.EP. gl_action.EcLocation. pl_loc in
780+ let (sl, sc) = loc.EcLocation. loc_start in
781+ let (el, ec) = loc.EcLocation. loc_end in
782+ let before_goals = EcCommands. pp_all_goals () in
783+ let n1 = List. length before_goals in
784+ let buf = Buffer. create 1024 in
785+ let fmt = Format. formatter_of_buffer buf in
786+ Format. fprintf fmt
787+ " === BEFORE: line %d (col %d) ===@\n " sl sc;
788+ EcCommands. pp_current_goal_or_noproof ~all: false fmt;
789+ Format. fprintf fmt
790+ " @\n === TACTIC (lines %d:%d - %d:%d) ===@\n %s@\n @\n "
791+ sl sc el ec (sentence_source loc);
792+ last_src := src;
793+ begin
794+ try
795+ process_action ~src p;
796+ last_loc := Some loc;
797+ pending := None ;
798+ let after_goals = EcCommands. pp_all_goals () in
799+ let n2 = List. length after_goals in
800+ Format. fprintf fmt
801+ " === AFTER: line %d (col %d) ===@\n " sl sc;
802+ let before_set =
803+ List. fold_left
804+ (fun s g -> EcMaps.Sstr. add g s)
805+ EcMaps.Sstr. empty before_goals
806+ in
807+ (* The new focused goal always counts as "modified"
808+ (its focus status changed even if its text matches
809+ an old sibling); the rest are printed only if
810+ they didn't appear in BEFORE. *)
811+ let to_print =
812+ match after_goals with
813+ | [] -> []
814+ | head :: tl ->
815+ head ::
816+ List. filter
817+ (fun g -> not (EcMaps.Sstr. mem g before_set))
818+ tl
819+ in
820+ begin match to_print with
821+ | [] -> Format. fprintf fmt " (no open goals)@\n "
822+ | _ ->
823+ List. iteri (fun i g ->
824+ if i > 0 then Format. fprintf fmt " @\n " ;
825+ Format. fprintf fmt " %s@\n " g)
826+ to_print
827+ end ;
828+ Format. fprintf fmt
829+ " @\n === SUMMARY ===@\n open goals: %d -> %d@\n " n1 n2;
830+ Format. pp_print_flush fmt () ;
831+ Buffer. contents buf
832+ with e ->
833+ Format. fprintf fmt
834+ " === AFTER: line %d (col %d) ===@\n <sentence failed>@\n "
835+ sl sc;
836+ Format. pp_print_flush fmt () ;
837+ trace_prefix := Buffer. contents buf;
838+ raise (Trace_failed e)
839+ end
840+ in
841+
715842 let tag =
716843 match ! last_loc with
717844 | None -> " "
718845 | Some loc ->
719846 let (el, _) = loc.EcLocation. loc_end in
720847 Printf. sprintf " [loaded:%s:%d]" filename el
721848 in
722- reply_ok ~tag (goals_to_string () )
849+ reply_ok ~tag body
723850
724851 with
725852 | EcCommands. Restart ->
726853 do_initialize () ;
727854 Hashtbl. clear checkpoints;
728855 reply_ok " Session restarted"
856+ | Trace_failed e ->
857+ let msg = format_error ~src: ! last_src e in
858+ reply_error (! trace_prefix ^ msg)
729859 | Failure s ->
730860 reply_error s
731861 | e ->
0 commit comments