Skip to content

Commit a84428d

Browse files
committed
[llm] add -trace LINE[:COL] for single-sentence before/after goals
Runs every sentence before the target, prints the goal state, runs the target sentence, prints the state again. Output uses stable === BEFORE / TACTIC / AFTER === delimiters so callers can split. Exit codes: 0 ok, 1 if the target sentence failed, 2 if no sentence at or after the target. Mutually exclusive with -upto.
1 parent b09b7ae commit a84428d

4 files changed

Lines changed: 154 additions & 6 deletions

File tree

doc/llm/CLAUDE.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,36 @@ easycrypt llm [OPTIONS] FILE.ec
2323
stdout and exit with code 0. Use this to inspect the proof state at
2424
a specific point in a file.
2525

26+
- `-trace LINE` or `-trace LINE:COL` — Run every sentence strictly
27+
before the target sentence, print the focused goal, run the target
28+
sentence, print the new-or-modified goals, then a one-line summary.
29+
The target sentence is the first sentence whose start position is
30+
`>= (LINE, COL)`, same as `-upto`. Output uses stable delimiters:
31+
32+
```
33+
=== BEFORE: line L (col C) ===
34+
<focused goal only>
35+
36+
=== TACTIC (lines L1:C1 - L2:C2) ===
37+
<full source of the sentence>
38+
39+
=== AFTER: line L (col C) ===
40+
<new or modified goals, in focus order; or "(no open goals)"
41+
if the proof closed>
42+
43+
=== SUMMARY ===
44+
open goals: N1 -> N2
45+
```
46+
47+
AFTER always prints the new focused goal (its focus status counts as
48+
modified even if its text matched a sibling in BEFORE), followed by
49+
any subsequent goals that didn't appear in BEFORE. `N1` / `N2` are
50+
the total open-goal counts before and after the tactic.
51+
52+
Exit code 0 on success; 1 if the target sentence failed (BEFORE is
53+
still printed, error goes to stderr); 2 if no sentence at or after
54+
the target exists. Mutually exclusive with `-upto`.
55+
2656
- `-lastgoals` — On failure, print the goal state (as it was just
2757
before the failing command) to stdout, then print the error to
2858
stderr, and exit with code 1. Use this to understand what the

src/ec.ml

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,7 @@ let main () =
416416
(*---*) docgen : bool;
417417
(*---*) outdirp : string option;
418418
(*---*) upto : (int * int option) option;
419+
(*---*) trace_at : (int * int option) option;
419420
mutable trace : trace1 list option;
420421
}
421422

@@ -495,6 +496,7 @@ let main () =
495496
; docgen = false
496497
; outdirp = None
497498
; upto = None
499+
; trace_at = None
498500
; trace = None }
499501

500502
end
@@ -531,6 +533,7 @@ let main () =
531533
; docgen = false
532534
; outdirp = None
533535
; upto = None
536+
; trace_at = None
534537
; trace = trace0 }
535538

536539
end
@@ -560,6 +563,7 @@ let main () =
560563
; docgen = false
561564
; outdirp = None
562565
; upto = llmopts.llmo_upto
566+
; trace_at = llmopts.llmo_trace
563567
; trace = None }
564568

565569
end
@@ -605,6 +609,7 @@ let main () =
605609
; docgen = true
606610
; outdirp = docopts.doco_outdirp
607611
; upto = None
612+
; trace_at = None
608613
; trace = None }
609614
end
610615

@@ -618,7 +623,7 @@ let main () =
618623
| Some pwd -> EcCommands.addidir pwd);
619624

620625
(* Check if the .eco is up-to-date and exit if so *)
621-
(if not state.docgen && state.upto = None then
626+
(if not state.docgen && state.upto = None && state.trace_at = None then
622627
oiter
623628
(fun input -> if EcCommands.check_eco input then exit 0)
624629
state.input);
@@ -712,6 +717,38 @@ let main () =
712717
| None -> true
713718
| Some c -> sc >= c) in
714719

720+
(* Check if a sentence's start location is at-or-past the -trace target.
721+
Returns the actual (line, col) of the matched sentence start on hit. *)
722+
let at_trace (loc : EcLocation.t) =
723+
match state.trace_at with
724+
| None -> None
725+
| Some (line, col) ->
726+
let (sl, sc) = loc.loc_start in
727+
let hit =
728+
sl > line || (sl = line && match col with
729+
| None -> true
730+
| Some c -> sc >= c)
731+
in if hit then Some (sl, sc) else None in
732+
733+
(* Lazy read of the whole input file as bytes, used by --trace to slice
734+
the exact source text of a sentence by byte offsets. *)
735+
let input_bytes = lazy (
736+
match state.input with
737+
| None -> ""
738+
| Some path ->
739+
let ic = open_in_bin path in
740+
let n = in_channel_length ic in
741+
let b = Bytes.create n in
742+
really_input ic b 0 n;
743+
close_in ic;
744+
Bytes.unsafe_to_string b) in
745+
746+
let sentence_source (loc : EcLocation.t) =
747+
let s = Lazy.force input_bytes in
748+
let lo = max 0 loc.EcLocation.loc_bchar in
749+
let hi = min (String.length s) loc.EcLocation.loc_echar in
750+
if hi <= lo then "" else String.sub s lo (hi - lo) in
751+
715752
try
716753
if T.interactive terminal then Sys.catch_break true;
717754

@@ -781,6 +818,70 @@ let main () =
781818
(fun p ->
782819
let loc = p.EP.gl_action.EcLocation.pl_loc in
783820

821+
(* -trace: at-or-past the target, print BEFORE (focused
822+
goal only), run this one sentence, print AFTER
823+
(new-or-modified goals only), then SUMMARY, exit. *)
824+
(match at_trace loc with
825+
| None -> ()
826+
| Some (sl, sc) ->
827+
let out = Format.std_formatter in
828+
let before_goals = EcCommands.pp_all_goals () in
829+
let n1 = List.length before_goals in
830+
Format.fprintf out
831+
"=== BEFORE: line %d (col %d) ===@\n" sl sc;
832+
EcCommands.pp_current_goal_or_noproof ~all:false out;
833+
let (el, ec) = loc.EcLocation.loc_end in
834+
Format.fprintf out
835+
"@\n=== TACTIC (lines %d:%d - %d:%d) ===@\n%s@\n@\n"
836+
sl sc el ec (sentence_source loc);
837+
(try
838+
let _ : float option =
839+
EcCommands.process ~src ~timed:false ~break:false
840+
p.EP.gl_action
841+
in
842+
let after_goals = EcCommands.pp_all_goals () in
843+
let n2 = List.length after_goals in
844+
Format.fprintf out
845+
"=== AFTER: line %d (col %d) ===@\n" sl sc;
846+
let before_set =
847+
List.fold_left
848+
(fun s g -> EcMaps.Sstr.add g s)
849+
EcMaps.Sstr.empty before_goals
850+
in
851+
(* The new focused goal always counts as
852+
"modified" (its focus status changed even if
853+
its text matches an old sibling). Subsequent
854+
goals are printed only if they didn't appear
855+
in BEFORE. *)
856+
let to_print =
857+
match after_goals with
858+
| [] -> []
859+
| head :: tail ->
860+
head ::
861+
List.filter
862+
(fun g -> not (EcMaps.Sstr.mem g before_set))
863+
tail
864+
in
865+
(match to_print with
866+
| [] ->
867+
Format.fprintf out "(no open goals)@\n"
868+
| _ ->
869+
List.iteri (fun i g ->
870+
if i > 0 then Format.fprintf out "@\n";
871+
Format.fprintf out "%s@\n" g)
872+
to_print);
873+
Format.fprintf out
874+
"@\n=== SUMMARY ===@\nopen goals: %d -> %d@\n" n1 n2
875+
with e ->
876+
Format.fprintf out
877+
"=== AFTER: line %d (col %d) ===@\n<sentence failed>@\n" sl sc;
878+
EcPException.exn_printer Format.err_formatter e;
879+
Format.pp_print_newline Format.err_formatter ();
880+
T.finalize terminal;
881+
exit 1);
882+
T.finalize terminal;
883+
exit 0);
884+
784885
(* -upto: if this command starts past the target, print goals and exit *)
785886
if past_upto loc then begin
786887
T.finalize terminal;
@@ -857,6 +958,15 @@ let main () =
857958

858959
if !terminate then begin
859960
T.finalize terminal;
961+
(match state.trace_at with
962+
| Some (line, col) ->
963+
let col_s = match col with
964+
| None -> "" | Some c -> Printf.sprintf ":%d" c in
965+
Format.eprintf
966+
"trace: no sentence at or after line %d%s@."
967+
line col_s;
968+
exit 2
969+
| None -> ());
860970
if not state.eco then
861971
finalize_input state.input (EcCommands.current ());
862972
if state.docgen then

src/ecOptions.ml

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ and llm_option = {
5353
llmo_provers : prv_options;
5454
llmo_lastgoals : bool;
5555
llmo_upto : (int * int option) option;
56+
llmo_trace : (int * int option) option;
5657
}
5758

5859
and prv_options = {
@@ -374,7 +375,8 @@ let specs = {
374375
`Group "loader";
375376
`Group "provers";
376377
`Spec ("lastgoals" , `Flag , "Print last unproved goals on failure");
377-
`Spec ("upto" , `String, "Compile up to LINE or LINE:COL and print goals")]);
378+
`Spec ("upto" , `String, "Compile up to LINE or LINE:COL and print goals");
379+
`Spec ("trace" , `String, "Trace one sentence at LINE or LINE:COL (before/after goals)")]);
378380

379381
("cli", "Run EasyCrypt top-level", [
380382
`Group "loader";
@@ -562,11 +564,11 @@ let doc_options_of_values values input =
562564
{ doco_input = input;
563565
doco_outdirp = get_string "outdir" values; }
564566

565-
let parse_upto values =
566-
get_string "upto" values |> Option.map (fun s ->
567+
let parse_line_col ~name values =
568+
get_string name values |> Option.map (fun s ->
567569
let invalid () =
568570
raise (Arg.Bad (Printf.sprintf
569-
"invalid -upto format: expected LINE or LINE:COL, got %S" s)) in
571+
"invalid -%s format: expected LINE or LINE:COL, got %S" name s)) in
570572
match String.split_on_char ':' s with
571573
| [line] ->
572574
let line = try int_of_string line with Failure _ -> invalid () in
@@ -578,10 +580,15 @@ let parse_upto values =
578580
| _ -> invalid ())
579581

580582
let llm_options_of_values ini values input =
583+
let upto = parse_line_col ~name:"upto" values in
584+
let trace = parse_line_col ~name:"trace" values in
585+
if Option.is_some upto && Option.is_some trace then
586+
raise (Arg.Bad "options -upto and -trace are mutually exclusive");
581587
{ llmo_input = input;
582588
llmo_provers = prv_options_of_values ini values;
583589
llmo_lastgoals = get_flag "lastgoals" values;
584-
llmo_upto = parse_upto values; }
590+
llmo_upto = upto;
591+
llmo_trace = trace; }
585592

586593
(* -------------------------------------------------------------------- *)
587594
let parse getini argv =

src/ecOptions.mli

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ and llm_option = {
4949
llmo_provers : prv_options;
5050
llmo_lastgoals : bool;
5151
llmo_upto : (int * int option) option;
52+
llmo_trace : (int * int option) option;
5253
}
5354

5455
and prv_options = {

0 commit comments

Comments
 (0)