Skip to content

Commit 9d46ad0

Browse files
committed
[llm] nested TREE and dotted-path FOCUS
Introduce a frame tree derived from pr_opened + parent_of: each open leaf's chain of multi-child ancestors (skipping single-child continuations) becomes its path through the tree. The same data structure backs both TREE rendering and FOCUS path lookup. TREE now shows depth-indented entries labelled with dotted paths matching what FOCUS accepts. Leading singleton frames are unwrapped: when all opens share an outermost split, the rendering starts at that split's branches, not at a redundant [1.] wrapper. FOCUS N1.N2.N3 walks the tree following each component and focuses the resolved leaf. A single integer (FOCUS k) still works (degree-1 path). The path must resolve to a leaf; selecting an internal frame yields "FOCUS: path must select a leaf goal, not a frame" and overshooting a leaf yields "FOCUS: path overshoots a leaf goal". After [split. split. split.] on [((a /\ b) /\ c) /\ d], TREE prints: [1.1.1] a = a <- focused [1.1.2] b = b [1.2] c = c [2] d = d and FOCUS 1.2 selects c, FOCUS 2 selects d, FOCUS 1.1.2 selects b. NEXT semantics unchanged. Flat proofs render unindented as before.
1 parent 799f38f commit 9d46ad0

2 files changed

Lines changed: 183 additions & 42 deletions

File tree

doc/llm/CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ These are protocol-level commands, not EasyCrypt syntax:
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) |
6363
| `GOALS ALL` | Print all subgoals |
64-
| `TREE` | List open subgoals as `[N] <one-line conclusion>`, marking the focused one |
64+
| `TREE` | List open subgoals with dotted-path labels showing nesting, marking the focused one |
6565
| `TREE ALL` | Same as `TREE`, but with full goal bodies |
66-
| `FOCUS N` | Rotate focus so subgoal `[N]` (from `TREE`) becomes the focused goal |
66+
| `FOCUS P` | Rotate focus to the leaf addressed by path `P` (`N` or `N1.N2.N3...`) |
6767
| `NEXT` | Rotate focus to the next subgoal (equivalent to `FOCUS 2`) |
6868
| `COMMIT` | Emit recorded REPL phrases as a bulleted proof body (works under `+strict_bullets`) |
6969
| `CHECKPOINT <name>` | Save current uuid under a name for later `REVERT` |

src/ecLlm.ml

Lines changed: 181 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -146,14 +146,108 @@ let run ~relocdir ~boot (llmopts : EcOptions.llm_option) =
146146
Format.pp_print_flush fmt ();
147147
Buffer.contents buf
148148

149-
(* Render the focus-tree of open subgoals. [all=false] gives a
150-
one-line digest per goal; [all=true] gives the full goal body. *)
151-
let tree_to_string ?(all=false) () =
152-
let entries = EcCommands.pp_tree ~all () in
153-
match entries with
154-
| [] -> "No active proof.\n"
155-
| _ ->
156-
let buf = Buffer.create 256 in
149+
(* Inline focus annotation ([focus: 1/N]) appended to reply tags
150+
whenever the active proof has >=2 open subgoals. *)
151+
let focus_tag () =
152+
match EcCommands.pp_tree () with
153+
| _ :: _ :: _ as entries ->
154+
Printf.sprintf " [focus: 1/%d]" (List.length entries)
155+
| _ -> ""
156+
end in
157+
158+
(* ------------------------------------------------------------------ *)
159+
(* Frame tree: group currently-open goals by their shared multi-child
160+
ancestors. Used by [Tree] (rendering) and [Focus] (path lookup).
161+
The tree is a *derivation*: it depends only on [pr_opened] and
162+
[parent_of], no recorded transcript. *)
163+
let module FrameTree = struct
164+
(* Internal nodes are split-point frames; leaves carry a handle
165+
(the open goal), its index in [pr_opened] (1-based, used by
166+
[EcCoreGoal.rotate_focus]), and its rendered text. *)
167+
type node =
168+
| Frame of node list (* >=2 child branches *)
169+
| Leaf of
170+
{ idx : int (* 1-based in pr_opened *)
171+
; focused : bool (* idx = 1 *)
172+
; text : string } (* one-line conclusion *)
173+
174+
(* Multi-child ancestors of [h], outermost first (= root-most
175+
split first, deepest split last). This ordering means leaves
176+
sharing the same OUTER frame will agree on the chain's first
177+
element, which is what [group] partitions on. *)
178+
let split_chain h =
179+
let rec walk h acc =
180+
match EcCommands.parent_of h with
181+
| None -> acc
182+
| Some p ->
183+
match EcCommands.children_of p with
184+
| [_] -> walk p acc
185+
| _ -> walk p (p :: acc)
186+
in
187+
(* [walk] prepends each ancestor as we go up; the result has
188+
outermost at the FRONT (we add it last). No reverse needed. *)
189+
walk h []
190+
191+
(* Build the tree by grouping leaves with a common ancestor prefix.
192+
[leaves] is a list of (chain, leaf) in [pr_opened] order. The
193+
grouping is done recursively on the head of each chain. *)
194+
let rec group (leaves : (EcCoreGoal.handle list * node) list) : node list =
195+
let rec runs acc = function
196+
| [] -> List.rev acc
197+
| (chain, leaf) :: rest ->
198+
match chain with
199+
| [] -> runs (`Bare leaf :: acc) rest
200+
| hd :: tl ->
201+
let same_head, others =
202+
List.partition_map (fun (c, l) ->
203+
match c with
204+
| h :: tail when EcCoreGoal.eq_handle h hd ->
205+
Left (tail, l)
206+
| _ -> Right (c, l))
207+
rest
208+
in
209+
runs (`Group ((tl, leaf) :: same_head) :: acc) others
210+
in
211+
List.map
212+
(function
213+
| `Bare leaf -> leaf
214+
| `Group children -> Frame (group children))
215+
(runs [] leaves)
216+
217+
(* Strip leading singleton frames so the top-level forest's
218+
indices match what the user thinks of as "top-level subgoals
219+
of the current frame." When all open leaves descend from a
220+
single outermost split, the top-level forest has one Frame
221+
containing the actual user-visible siblings; unwrap it. *)
222+
let rec unwrap forest =
223+
match forest with
224+
| [Frame children] -> unwrap children
225+
| _ -> forest
226+
227+
let build () =
228+
let handles = EcCommands.open_handles () in
229+
let texts = EcCommands.pp_tree () in
230+
if handles = [] then []
231+
else
232+
let leaves =
233+
List.mapi (fun i (h, (_, focused, text)) ->
234+
let leaf = Leaf { idx = i + 1; focused; text } in
235+
(split_chain h, leaf))
236+
(List.combine handles texts)
237+
in
238+
unwrap (group leaves)
239+
240+
(* Render the tree with dotted-path labels matching what FOCUS
241+
accepts. [all] requests full goal bodies (we re-query via
242+
[pp_tree ~all:true] keyed by leaf index). *)
243+
let render ?(all=false) () =
244+
let forest = build () in
245+
if forest = [] then "No active proof.\n"
246+
else
247+
let texts_all =
248+
if all then Some (EcCommands.pp_tree ~all:true ())
249+
else None
250+
in
157251
let one_line s =
158252
let s =
159253
match String.index_opt s '\n' with
@@ -165,26 +259,61 @@ let run ~relocdir ~boot (llmopts : EcOptions.llm_option) =
165259
then String.sub s 0 (limit - 1) ^ ""
166260
else s
167261
in
168-
List.iter (fun (i, focused, text) ->
169-
let marker = if focused then " <- focused" else "" in
170-
if all then
171-
Buffer.add_string buf
172-
(Printf.sprintf "[%d]%s\n%s\n" i marker text)
173-
else
174-
Buffer.add_string buf
175-
(Printf.sprintf "[%d] %s%s\n" i (one_line text) marker)
176-
) entries;
262+
let buf = Buffer.create 256 in
263+
let rec emit ~depth ~path = function
264+
| Leaf { idx; focused; text } ->
265+
let label = String.concat "." (List.rev_map string_of_int path) in
266+
let marker = if focused then " <- focused" else "" in
267+
for _ = 1 to depth do Buffer.add_string buf " " done;
268+
(match texts_all with
269+
| None ->
270+
Buffer.add_string buf
271+
(Printf.sprintf "[%s] %s%s\n"
272+
label (one_line text) marker)
273+
| Some entries ->
274+
let (_, _, full) =
275+
List.nth entries (idx - 1)
276+
in
277+
Buffer.add_string buf
278+
(Printf.sprintf "[%s]%s\n%s\n" label marker full))
279+
| Frame children ->
280+
List.iteri (fun i child ->
281+
emit ~depth:(depth + 1) ~path:((i + 1) :: path) child)
282+
children
283+
in
284+
List.iteri (fun i node ->
285+
emit ~depth:0 ~path:[i + 1] node)
286+
forest;
177287
Buffer.contents buf
178288

179-
(* Inline focus annotation ([focus: 1/N]) appended to reply tags
180-
whenever the active proof has >=2 open subgoals. *)
181-
let focus_tag () =
182-
match EcCommands.pp_tree () with
183-
| _ :: _ :: _ as entries ->
184-
Printf.sprintf " [focus: 1/%d]" (List.length entries)
185-
| _ -> ""
289+
(* Resolve a dotted path against the tree. Returns [Ok idx] where
290+
[idx] is the 1-based position in [pr_opened] of the selected
291+
leaf, or [Error msg]. *)
292+
let resolve_path (path : int list) : (int, string) result =
293+
let forest = build () in
294+
let rec walk ~components nodes =
295+
match components with
296+
| [] -> Error "FOCUS: path must select a leaf goal"
297+
| k :: rest ->
298+
if k < 1 || k > List.length nodes then
299+
Error (Printf.sprintf
300+
"FOCUS: index %d out of range (1..%d)"
301+
k (List.length nodes))
302+
else
303+
match List.nth nodes (k - 1), rest with
304+
| Leaf { idx; _ }, [] -> Ok idx
305+
| Leaf _, _ ->
306+
Error "FOCUS: path overshoots a leaf goal"
307+
| Frame _, [] ->
308+
Error "FOCUS: path must select a leaf goal, \
309+
not a frame"
310+
| Frame kids, _ -> walk ~components:rest kids
311+
in
312+
if forest = [] then Error "FOCUS: no active proof"
313+
else walk ~components:path forest
186314
end in
187315

316+
188317
(* ------------------------------------------------------------------ *)
189318
(* OK/ERROR/<END> wire envelope. *)
190319
let module Wire = struct
@@ -708,7 +837,7 @@ let run ~relocdir ~boot (llmopts : EcOptions.llm_option) =
708837
| Goals of [`One | `All]
709838
| Tree of [`One | `All]
710839
| Commit
711-
| Focus of int
840+
| Focus of int list (* dotted path; [k] = "FOCUS k" *)
712841
| Next
713842
| Checkpoint of string
714843
| Revert of string (* uuid-or-name; Dispatch resolves *)
@@ -740,10 +869,17 @@ let run ~relocdir ~boot (llmopts : EcOptions.llm_option) =
740869
let parse_focus arg =
741870
if arg = "" then
742871
raise (Parse_error "FOCUS: missing argument");
743-
try Focus (int_of_string arg)
744-
with Failure _ ->
872+
let parts = String.split_on_char '.' arg in
873+
let path =
874+
try List.map int_of_string parts
875+
with Failure _ ->
876+
raise (Parse_error
877+
(Printf.sprintf "FOCUS: not a path of integers: %s" arg))
878+
in
879+
if List.exists (fun k -> k < 1) path then
745880
raise (Parse_error
746-
(Printf.sprintf "FOCUS: not an integer: %s" arg))
881+
(Printf.sprintf "FOCUS: path indices must be >= 1: %s" arg));
882+
Focus path
747883

748884
let parse_checkpoint name =
749885
if name = "" then
@@ -834,19 +970,24 @@ let run ~relocdir ~boot (llmopts : EcOptions.llm_option) =
834970
Wire.reply_error "nothing to undo"
835971

836972
let do_focus_request request =
837-
(* [request] is the user's intent normalized; [`Next] is "second
838-
sibling unless only one open". *)
973+
(* [request] is the user's intent normalized:
974+
- [`Next] = rotate to the second open goal (or stay if <=1)
975+
- [`Path p] = resolve dotted path [p] against the frame tree
976+
and focus the matching leaf. *)
839977
Buffer.clear notices;
840-
let entries = EcCommands.pp_tree () in
841-
let n = List.length entries in
842-
let target =
978+
let resolved =
843979
match request with
844-
| `Next -> if n <= 1 then 1 else 2
845-
| `At k -> k
980+
| `Next ->
981+
let n = List.length (EcCommands.open_handles ()) in
982+
Ok (if n <= 1 then 1 else 2)
983+
| `Path path -> FrameTree.resolve_path path
846984
in
847-
match EcCommands.focus_goal target with
848-
| Ok _ -> Wire.reply_ok_goals ()
985+
match resolved with
849986
| Error msg -> Wire.reply_error msg
987+
| Ok target ->
988+
match EcCommands.focus_goal target with
989+
| Ok _ -> Wire.reply_ok_goals ()
990+
| Error msg -> Wire.reply_error msg
850991

851992
let do_checkpoint name =
852993
Buffer.clear notices;
@@ -912,14 +1053,14 @@ let run ~relocdir ~boot (llmopts : EcOptions.llm_option) =
9121053
Wire.reply_ok (Goals.goals_to_string ~all:true ())
9131054
| Tree `One ->
9141055
Buffer.clear notices;
915-
Wire.reply_ok (Goals.tree_to_string ())
1056+
Wire.reply_ok (FrameTree.render ())
9161057
| Tree `All ->
9171058
Buffer.clear notices;
918-
Wire.reply_ok (Goals.tree_to_string ~all:true ())
1059+
Wire.reply_ok (FrameTree.render ~all:true ())
9191060
| Commit ->
9201061
Buffer.clear notices;
9211062
Wire.reply_ok (Commit.proof_text ())
922-
| Focus k -> do_focus_request (`At k)
1063+
| Focus path -> do_focus_request (`Path path)
9231064
| Next -> do_focus_request `Next
9241065
| Checkpoint n -> do_checkpoint n
9251066
| Revert s -> do_revert s

0 commit comments

Comments
 (0)