Skip to content

Commit 1dec301

Browse files
committed
[llm] add LOAD ... -trace for before/after sentence inspection
Add a -trace flag to the LOAD REPL command. When set, LOAD compiles the prefix exactly as today (using the existing LINE[:COL] argument, or up to EOF if omitted), but defers the last sentence and runs it under goal capture, then returns a response body with four delimited blocks: === BEFORE: line L (col C) === <focused goal before the sentence> === TACTIC (lines L:C - L':C') === <exact source text of the sentence> === AFTER: line L (col C) === <new focused goal + any new sibling goals> === SUMMARY === open goals: N1 -> N2 Adapted from PR #1018 (-trace LINE[:COL] for batch mode): same delimiters and the same new-or-modified-head filtering for AFTER. The position is taken from LOAD's existing LINE[:COL] argument; the tag is the regular [loaded:file:LINE]. If the deferred sentence is outside a proof context, or there is no sentence to trace, the reply uses the ERROR envelope with a clear message. If the sentence fails, the BEFORE/TACTIC blocks are still delivered, AFTER carries a <sentence failed> marker, and the formatted exception is appended. Expose EcCommands.in_proof so the REPL can check the pre-execution proof status without inspecting scope internals.
1 parent 3a6b7b0 commit 1dec301

4 files changed

Lines changed: 186 additions & 31 deletions

File tree

doc/llm/CLAUDE.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ These are protocol-level commands, not EasyCrypt syntax:
5656

5757
| Command | Description |
5858
|---------|-------------|
59-
| `LOAD "file.ec" [LINE[:COL]] [-nosmt]` | Reset state, compile file (optionally skip SMT) |
59+
| `LOAD "file.ec" [LINE[:COL]] [-nosmt] [-trace]` | Reset state, compile file (optionally skip SMT or trace last sentence) |
6060
| `UNDO` | Undo the last proof step |
6161
| `REVERT <uuid-or-name>` | Revert to a specific state (by uuid or checkpoint name) |
6262
| `GOALS` | Print the current goal (first subgoal only, with remaining count) |
@@ -117,6 +117,27 @@ compilation (safe when the prefix was already verified):
117117
LOAD "myfile.ec" 436 -nosmt
118118
```
119119

120+
Add `-trace` to a LOAD to inspect the proof state around the last
121+
loaded sentence. The reply body contains four delimited blocks:
122+
123+
```
124+
LOAD "myfile.ec" 42 -trace
125+
126+
=== BEFORE: line 42 (col 0) ===
127+
<focused goal before the sentence>
128+
=== TACTIC (lines 42:0 - 42:10) ===
129+
<exact source text of the sentence>
130+
=== AFTER: line 42 (col 0) ===
131+
<new focused goal, plus any new sibling goals>
132+
=== SUMMARY ===
133+
open goals: N1 -> N2
134+
```
135+
136+
The position comes from the existing `LINE[:COL]` argument; omit it to
137+
trace the file's last sentence. On tactic failure the reply uses the
138+
`ERROR` envelope and still includes the BEFORE/TACTIC blocks plus an
139+
`<sentence failed>` marker in the AFTER block.
140+
120141
**2. Try tactics, using REVERT to restart:**
121142

122143
The uuid returned by LOAD is a revertible state. Use `REVERT` to

src/ec.ml

Lines changed: 159 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -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 ===@\nopen 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 ->

src/ecCommands.ml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1127,8 +1127,11 @@ let pp_current_goal ?(all = false) stream =
11271127
end
11281128

11291129
(* -------------------------------------------------------------------- *)
1130+
let in_proof () =
1131+
Option.is_some (S.xgoal (current ()))
1132+
11301133
let pp_current_goal_or_noproof ?(all = false) stream =
1131-
if Option.is_some (S.xgoal (current ())) then
1134+
if in_proof () then
11321135
pp_current_goal ~all stream
11331136
else
11341137
Format.fprintf stream "No active proof.@\n%!"

src/ecCommands.mli

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ val pp_current_goal : ?all:bool -> Format.formatter -> unit
6464
val pp_current_goal_or_noproof : ?all:bool -> Format.formatter -> unit
6565
val pp_maybe_current_goal : Format.formatter -> unit
6666
val pp_all_goals : unit -> string list
67+
val in_proof : unit -> bool
6768

6869
(* -------------------------------------------------------------------- *)
6970
val pragma_verbose : bool -> unit

0 commit comments

Comments
 (0)