Skip to content

Commit 964cb1e

Browse files
kim-emKim Morrison
authored andcommitted
feat(interval): retain bounded branch trees
Progress: progress/20260811T172657Z.md
1 parent a58d042 commit 964cb1e

6 files changed

Lines changed: 439 additions & 11 deletions

File tree

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/-
2+
Copyright (c) 2026 Lean FRO, LLC. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Authors: Kim Morrison
5+
-/
6+
7+
module
8+
9+
public import HexInterval.Experiment.BranchStart
10+
11+
@[expose] public section
12+
13+
/-!
14+
# Resource-bounded branch trees
15+
16+
This experiment retains a complete runtime tree while repeatedly running
17+
ordinary target-directed sessions and expanding accepted split plans. It is
18+
generic in the fact domain, executable packages, and external policy state.
19+
20+
The tree is search data, not proof evidence. In particular, a runtime
21+
contradiction leaf is not a proof of `False`, and a pair of target leaves is
22+
not a proof of their parent target. The proof frontend must separately replay
23+
each retained leaf and join checked child evidence through a split schema.
24+
-/
25+
26+
namespace Hex.Interval.Experiment.BranchTree
27+
28+
open Propagator PolicySession SemanticReplay TargetRun BranchStart
29+
30+
/-- Which child is being initialized. Policies may use this to fork private
31+
state differently on the two sides. -/
32+
inductive Side where
33+
| left
34+
| right
35+
deriving DecidableEq, Repr
36+
37+
/-- Replaceable pending-leaf order. -/
38+
inductive Order where
39+
| depthFirst
40+
| breadthFirst
41+
deriving DecidableEq, Repr
42+
43+
/-- Global tree resources. Child sessions retain their independent engine,
44+
policy, and payload limits. -/
45+
structure Limits where
46+
branch : BranchStart.Limits
47+
maxSteps : Nat
48+
maxSplits : Nat
49+
maxLeaves : Nat
50+
leafFuel : Nat
51+
deriving DecidableEq, Repr
52+
53+
/-- Everything needed to run and split one pending leaf. -/
54+
structure Config (Fact PolicyState : Type) : Type 1 where
55+
factDomain : FactDomain Fact
56+
packages : Array (Package Fact)
57+
sessionLimits : PolicySession.Limits
58+
controller : TargetRun.Controller Fact PolicyState
59+
splitter : BranchStart.Splitter Fact
60+
forkPolicy : PolicyState -> Side -> PolicyState
61+
order : Order
62+
limits : Limits
63+
64+
/-- Stable index into the append-only node array. -/
65+
structure TreeId where
66+
index : Nat
67+
deriving DecidableEq, Repr
68+
69+
/-- Exact input, scope, depth, and private policy state of one leaf. -/
70+
structure Leaf (Fact PolicyState : Type) where
71+
scope : Propagator.Policy.ScopeId
72+
depth : Nat
73+
input : CheckerInput Fact
74+
policyState : PolicyState
75+
76+
/-- A leaf whose policy session can still be run. -/
77+
structure Job (Fact PolicyState : Type) : Type 1 where
78+
scope : Propagator.Policy.ScopeId
79+
depth : Nat
80+
input : CheckerInput Fact
81+
policyState : PolicyState
82+
session : PolicySession.Session Fact
83+
84+
/-- Why a split result was retained as a blocked leaf. -/
85+
inductive Blocked where
86+
| splitRejected (error : BranchStart.Error)
87+
| splitLimit
88+
| leafLimit
89+
deriving DecidableEq, Repr
90+
91+
/-- Terminal runtime data for a leaf. None of these constructors is proof
92+
evidence. -/
93+
inductive LeafEnd (Fact PolicyState : Type) : Type 1
94+
| result (run : TargetRun.Result Fact PolicyState)
95+
| blocked (run : TargetRun.Result Fact PolicyState) (reason : Blocked)
96+
| startError (error : PolicySession.StartError)
97+
98+
/-- One retained runtime-tree node. A split node stores the exact validated
99+
child snapshots and both child identities, even if a child session failed to
100+
start. -/
101+
inductive Node (Fact PolicyState : Type) : Type 1
102+
| pending (job : Job Fact PolicyState)
103+
| leaf (source : Leaf Fact PolicyState) (ending : LeafEnd Fact PolicyState)
104+
| split (source : Leaf Fact PolicyState)
105+
(run : TargetRun.Result Fact PolicyState)
106+
(children : BranchStart.Children Fact) (left right : TreeId)
107+
108+
/-- Monotone retained state for one tree. `leaves` counts current leaves, so
109+
an accepted binary split increases it by exactly one. -/
110+
structure State (Fact PolicyState : Type) : Type 1 where
111+
nodes : Array (Node Fact PolicyState)
112+
frontier : List TreeId
113+
branch : BranchStart.State
114+
splits : Nat
115+
leaves : Nat
116+
steps : Nat
117+
118+
/-- Failure before a coherent root tree exists. -/
119+
inductive StartError where
120+
| leafLimit
121+
| scopeLimit
122+
| unknownTarget
123+
| session (error : PolicySession.StartError)
124+
deriving DecidableEq, Repr
125+
126+
/-- An append-only tree should only schedule valid pending nodes. -/
127+
inductive Error where
128+
| missingNode (id : TreeId)
129+
| settledNode (id : TreeId)
130+
deriving DecidableEq, Repr
131+
132+
def sourceOf (job : Job Fact PolicyState) : Leaf Fact PolicyState :=
133+
{ scope := job.scope
134+
depth := job.depth
135+
input := job.input
136+
policyState := job.policyState }
137+
138+
/-- A tree is settled exactly when it has no runnable leaf. It may still
139+
contain blocked, unfinished, or failed leaves. -/
140+
def State.settled (state : State Fact PolicyState) : Bool :=
141+
state.frontier.isEmpty
142+
143+
/-- The global processing budget may leave a nonempty frontier. -/
144+
def State.stepLimited (limits : Limits) (state : State Fact PolicyState) : Bool :=
145+
state.steps >= limits.maxSteps && !state.frontier.isEmpty
146+
147+
/-- Build a coherent root session from the exact caller input. -/
148+
def start (config : Config Fact PolicyState) (scope : Propagator.Policy.ScopeId)
149+
(input : CheckerInput Fact) (policyState : PolicyState) :
150+
Except StartError (State Fact PolicyState) := do
151+
if config.limits.maxLeaves = 0 then throw .leafLimit
152+
if config.limits.branch.maxScopes = 0 then throw .scopeLimit
153+
if (input.baseProgram.node? input.target.node).isNone then
154+
throw StartError.unknownTarget
155+
let session <-
156+
match PolicySession.Session.start config.factDomain input.baseProgram
157+
config.packages input.initialFacts config.sessionLimits scope with
158+
| .ok session => pure session
159+
| .error error => throw (.session error)
160+
let root : Job Fact PolicyState :=
161+
{ scope, depth := 0, input, policyState, session }
162+
pure
163+
{ nodes := #[.pending root]
164+
frontier := [{ index := 0 }]
165+
branch := BranchStart.State.start scope
166+
splits := 0
167+
leaves := 1
168+
steps := 0 }
169+
170+
def schedule (order : Order) (rest fresh : List TreeId) : List TreeId :=
171+
match order with
172+
| .depthFirst => fresh ++ rest
173+
| .breadthFirst => rest ++ fresh
174+
175+
def childNode (config : Config Fact PolicyState) (side : Side)
176+
(scope : Propagator.Policy.ScopeId) (depth : Nat) (input : CheckerInput Fact)
177+
(policyState : PolicyState) : Node Fact PolicyState × Bool :=
178+
let childState := config.forkPolicy policyState side
179+
let source : Leaf Fact PolicyState := { scope, depth, input, policyState := childState }
180+
match PolicySession.Session.start config.factDomain input.baseProgram
181+
config.packages input.initialFacts config.sessionLimits scope with
182+
| .ok session =>
183+
(.pending
184+
{ scope := source.scope
185+
depth := source.depth
186+
input := source.input
187+
policyState := source.policyState
188+
session }, true)
189+
| .error error => (.leaf source (.startError error), false)
190+
191+
def retainLeaf (state : State Fact PolicyState) (id : TreeId)
192+
(source : Leaf Fact PolicyState) (ending : LeafEnd Fact PolicyState)
193+
(rest : List TreeId) : State Fact PolicyState :=
194+
{ state with
195+
nodes := state.nodes.set! id.index (.leaf source ending)
196+
frontier := rest
197+
steps := state.steps + 1 }
198+
199+
/-- Process one pending leaf. Split rejection and tree-resource exhaustion
200+
are terminal leaf data; only a malformed internal frontier returns `Error`. -/
201+
def step [DecidableEq Fact] (config : Config Fact PolicyState)
202+
(state : State Fact PolicyState) : Except Error (State Fact PolicyState) := do
203+
if state.steps >= config.limits.maxSteps then return state
204+
let some id := state.frontier.head? | return state
205+
let rest := state.frontier.tail
206+
let some node := state.nodes[id.index]? | throw (.missingNode id)
207+
let .pending job := node | throw (.settledNode id)
208+
let source := sourceOf job
209+
let run := TargetRun.drive config.factDomain job.input.target.node
210+
job.input.target.fact config.controller config.limits.leafFuel
211+
job.session job.policyState
212+
let .split plan := run.stop |
213+
return retainLeaf state id source (.result run) rest
214+
if state.splits >= config.limits.maxSplits then
215+
return retainLeaf state id source (.blocked run .splitLimit) rest
216+
if state.leaves + 1 > config.limits.maxLeaves then
217+
return retainLeaf state id source (.blocked run .leafLimit) rest
218+
match BranchStart.prepare config.limits.branch state.branch job.depth
219+
run.session plan job.input.target config.splitter with
220+
| .error error =>
221+
pure (retainLeaf state id source (.blocked run (.splitRejected error)) rest)
222+
| .ok (branch, children) =>
223+
let leftId : TreeId := { index := state.nodes.size }
224+
let rightId : TreeId := { index := state.nodes.size + 1 }
225+
let (leftNode, leftPending) := childNode config .left children.leftScope
226+
children.depth children.left run.policyState
227+
let (rightNode, rightPending) := childNode config .right children.rightScope
228+
children.depth children.right run.policyState
229+
let fresh :=
230+
(if leftPending then [leftId] else []) ++
231+
(if rightPending then [rightId] else [])
232+
pure
233+
{ nodes := (state.nodes.set! id.index
234+
(.split source run children leftId rightId)).push leftNode |>.push rightNode
235+
frontier := schedule config.order rest fresh
236+
branch
237+
splits := state.splits + 1
238+
leaves := state.leaves + 1
239+
steps := state.steps + 1 }
240+
241+
/-- Run for at most the caller fuel and never beyond the retained global step
242+
budget. A nonempty frontier in the result is an honest partial tree. -/
243+
def runFrom [DecidableEq Fact] (config : Config Fact PolicyState) :
244+
Nat -> State Fact PolicyState -> Except Error (State Fact PolicyState)
245+
| 0, state => pure state
246+
| fuel + 1, state =>
247+
if state.settled || state.steps >= config.limits.maxSteps then pure state
248+
else do
249+
let state ← step config state
250+
runFrom config fuel state
251+
252+
termination_by fuel _ => fuel
253+
254+
/-- Consume the configured global processing budget. -/
255+
def run [DecidableEq Fact] (config : Config Fact PolicyState)
256+
(state : State Fact PolicyState) : Except Error (State Fact PolicyState) :=
257+
runFrom config config.limits.maxSteps state
258+
259+
end Hex.Interval.Experiment.BranchTree

‎HexInterval/SPEC/hex-interval.md‎

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1630,21 +1630,44 @@ runtime contradiction separately but deliberately refuses to treat that flag
16301630
as proof closure; the frontend must first resolve an established bottom fact
16311631
and apply the refutation schema described above.
16321632

1633-
The remaining tree manager should retain internal nodes recording validated
1634-
plans and checked child facts, and leaves retaining either a target proof, a
1635-
checked contradiction, or an explicit unfinished result. It may emit a theorem
1636-
only when every coverage child is closed. For best-bound mode, unfinished
1637-
leaves contribute their inherited parent fact to the global hull; they never
1638-
inherit a tighter sibling fact. Live-leaf and total branch-decision limits
1639-
remain to be added beside the delivered split-depth and total-created-scope
1640-
limits and the per-session engine and payload limits.
1633+
The first generic runtime tree manager now retains internal nodes recording
1634+
validated plans and exact child inputs, and leaves recording target,
1635+
contradiction, saturation, resource failure, split refusal, session-start
1636+
failure, or any other precise `TargetRun` result. It stores the append-only
1637+
node array and a separate pending frontier, so a global step limit returns an
1638+
honest partial tree rather than relabelling unexplored leaves as closed.
1639+
Independent limits bound processed leaves, accepted splits, current leaf
1640+
count, split depth, total created scopes, and each leaf run's policy fuel in
1641+
addition to the per-session engine and payload limits. Split- and leaf-limit
1642+
exhaustion is retained at the exact parent leaf. A child session which cannot
1643+
start is likewise retained beside its sibling instead of silently deleting
1644+
the branch.
1645+
1646+
The manager is function- and representation-independent. Its configuration
1647+
supplies the fact domain, runtime packages, controller, splitter, child-policy
1648+
fork, and pending order. The initial orders are depth-first and breadth-first;
1649+
their frontier transformation is separately tested so later best-first or
1650+
hybrid queues do not affect branch validation. The exponential conformance
1651+
tree selects the authenticated root split, starts both exact scoped children,
1652+
runs the arbitrary exponential propagator in each, and retains two target
1653+
leaves. Separate guards show that one global step leaves both children pending,
1654+
and that zero split or one-leaf budgets retain an explicitly blocked root.
1655+
1656+
This runtime tree contains no proof evidence. The existing two-child proof
1657+
canary separately demonstrates how exact retained child runs can be replayed
1658+
and joined, but the manager does not yet emit that term. The proof-tree layer
1659+
must emit a theorem only when every coverage child is closed by replay or a
1660+
checked refutation. For best-bound mode, unfinished leaves must contribute
1661+
their inherited parent fact to the global hull; they never inherit a tighter
1662+
sibling fact.
16411663

16421664
Several operational choices deliberately remain experimental:
16431665

16441666
- restart a child session from a checked snapshot, or add a sealed session-fork
16451667
operation which preserves reusable work and immutable payload sharing;
1646-
- depth-first execution for small proof memory, best-first execution for early
1647-
target closure, or a bounded hybrid frontier;
1668+
- use the delivered depth-first or breadth-first list frontier, or replace it
1669+
with best-first execution or a bounded hybrid without changing retained
1670+
nodes;
16481671
- store branch-local program suffixes directly, or hash-cons identical
16491672
instantiations above the scope layer;
16501673
- retain `Dyadic` in real-domain executable plans while keeping the proof
@@ -1670,7 +1693,7 @@ makes emission fail. The package remains a compact conformance fixture while
16701693
we decide which parts belong in the Mathlib-free runtime and Mathlib semantic
16711694
companion. Remaining acceptance tests include a nested split, a child-local
16721695
instantiation, a sibling-reference attack, a non-interior repeated split, and
1673-
fuel exhaustion with no theorem emitted.
1696+
proof emission refusing the delivered fuel-exhausted partial tree.
16741697

16751698
### Generic proof frontend
16761699

0 commit comments

Comments
 (0)