@@ -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