|
| 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 | +import HexInterval.Experiment.BranchTree |
| 8 | +import HexInterval.Experiment.ExpSign |
| 9 | + |
| 10 | +/-! |
| 11 | +# Nested branch scheduling conformance |
| 12 | +
|
| 13 | +This Mathlib-free canary builds a live two-level split tree. The left child |
| 14 | +splits a second source while the right child closes immediately, so the child |
| 15 | +policy fork is genuinely side-sensitive and depth-first and breadth-first |
| 16 | +frontiers differ on live pending work. |
| 17 | +-/ |
| 18 | + |
| 19 | +namespace Hex.Interval.NestedBranchConformance |
| 20 | + |
| 21 | +open Experiment |
| 22 | +open Propagator PolicySession SemanticReplay TargetRun BranchStart BranchTree |
| 23 | +open ExpSign |
| 24 | + |
| 25 | +private def program : Program := |
| 26 | + { operations |
| 27 | + nodes := #[sourceInstruction, sourceInstruction, expInstruction] } |
| 28 | + |
| 29 | +private def target : NodeFact Bound := |
| 30 | + { node := node 2, fact := .nonnegative } |
| 31 | + |
| 32 | +private def input : CheckerInput Bound := |
| 33 | + { baseProgram := program |
| 34 | + initialFacts := #[.all, .all, .all] |
| 35 | + target } |
| 36 | + |
| 37 | +private inductive Route where |
| 38 | + | root |
| 39 | + | splitLeft |
| 40 | + | close |
| 41 | + deriving DecidableEq, Repr |
| 42 | + |
| 43 | +private def splitAt (wanted : NodeId) (view : Policy.View Bound) : |
| 44 | + Option Policy.OfferView := |
| 45 | + view.offers.toList.find? fun offer => |
| 46 | + match offer.key with |
| 47 | + | .invoke invocation => |
| 48 | + invocation.rule == splitRuleKey && invocation.anchor == wanted |
| 49 | + | .split _ seen _ _ => seen.node == wanted |
| 50 | + | _ => false |
| 51 | + |
| 52 | +private def expAt (wanted : NodeId) (view : Policy.View Bound) : |
| 53 | + Option Policy.OfferView := |
| 54 | + view.offers.toList.find? fun offer => |
| 55 | + match offer.key with |
| 56 | + | .invoke invocation => |
| 57 | + invocation.rule == expRuleKey && invocation.anchor == wanted |
| 58 | + | _ => false |
| 59 | + |
| 60 | +private def controller : Controller Bound Route := |
| 61 | + { update := fun state _ => state |
| 62 | + choose := fun state view => |
| 63 | + let selected := |
| 64 | + match state with |
| 65 | + | .root => splitAt (node 0) view |
| 66 | + | .splitLeft => splitAt (node 1) view |
| 67 | + | .close => expAt (node 2) view |
| 68 | + match selected with |
| 69 | + | some offer => .select offer state |
| 70 | + | none => .stop state } |
| 71 | + |
| 72 | +private def fork : Route -> Side -> Route |
| 73 | + | .root, .left => .splitLeft |
| 74 | + | .root, .right => .close |
| 75 | + | .splitLeft, _ => .close |
| 76 | + | .close, _ => .close |
| 77 | + |
| 78 | +private def splitter : Splitter Bound := |
| 79 | + { split := fun graph splitNode instruction parent point => |
| 80 | + if graph.node? splitNode == some instruction && |
| 81 | + instruction.domain == real && parent == .all && point == 0 then |
| 82 | + some (.nonnegative, .negative) |
| 83 | + else |
| 84 | + none } |
| 85 | + |
| 86 | +private def resources : BranchTree.Limits := |
| 87 | + { branch := { maxDepth := 3, maxScopes := 5 } |
| 88 | + maxSteps := 5 |
| 89 | + maxSplits := 2 |
| 90 | + maxLeaves := 3 |
| 91 | + leafFuel := limits.policy.maxDecisions } |
| 92 | + |
| 93 | +private def config (order : Order) : Config Bound Route := |
| 94 | + { factDomain |
| 95 | + packages := splitPackages |
| 96 | + sessionLimits := limits |
| 97 | + controller |
| 98 | + splitter |
| 99 | + forkPolicy := fork |
| 100 | + order |
| 101 | + limits := resources } |
| 102 | + |
| 103 | +private def run? (order : Order) (fuel : Nat) : Option (State Bound Route) := do |
| 104 | + let .ok state := BranchTree.start (config order) { index := 0 } input .root |
| 105 | + | none |
| 106 | + let .ok state := BranchTree.runFrom (config order) fuel state | none |
| 107 | + some state |
| 108 | + |
| 109 | +private def leafReached? (entry : Node Bound Route) : Bool := |
| 110 | + match entry with |
| 111 | + | .leaf source (.result result) => |
| 112 | + source.input.target == target && |
| 113 | + match result.stop with |
| 114 | + | .target reached => reached.seen.node == target.node && |
| 115 | + reached.fact == .nonnegative |
| 116 | + | _ => false |
| 117 | + | _ => false |
| 118 | + |
| 119 | +#guard fork .root .left == .splitLeft |
| 120 | +#guard fork .root .right == .close |
| 121 | + |
| 122 | +#guard |
| 123 | + (run? .depthFirst 2).any fun state => |
| 124 | + state.steps == 2 && state.splits == 2 && state.leaves == 3 && |
| 125 | + state.frontier == [{ index := 3 }, { index := 4 }, { index := 2 }] |
| 126 | + |
| 127 | +#guard |
| 128 | + (run? .breadthFirst 2).any fun state => |
| 129 | + state.steps == 2 && state.splits == 2 && state.leaves == 3 && |
| 130 | + state.frontier == [{ index := 2 }, { index := 3 }, { index := 4 }] |
| 131 | + |
| 132 | +#guard |
| 133 | + (run? .depthFirst 5).any fun state => |
| 134 | + state.settled && state.steps == 5 && state.splits == 2 && |
| 135 | + state.leaves == 3 && state.nodes.size == 5 && |
| 136 | + match state.nodes[0]?, state.nodes[1]?, state.nodes[2]?, |
| 137 | + state.nodes[3]?, state.nodes[4]? with |
| 138 | + | some (BranchTree.Node.split root _ rootChildren left right), |
| 139 | + some (BranchTree.Node.split nested _ nestedChildren nestedLeft nestedRight), |
| 140 | + some rightLeaf, some nestedLeftLeaf, some nestedRightLeaf => |
| 141 | + root.policyState == .root && left.index == 1 && right.index == 2 && |
| 142 | + rootChildren.leftScope.index == 1 && |
| 143 | + rootChildren.rightScope.index == 2 && |
| 144 | + rootChildren.left.initialFacts == #[.nonnegative, .all, .all] && |
| 145 | + rootChildren.right.initialFacts == #[.negative, .all, .all] && |
| 146 | + nested.policyState == .splitLeft && |
| 147 | + nestedLeft.index == 3 && nestedRight.index == 4 && |
| 148 | + nestedChildren.leftScope.index == 3 && |
| 149 | + nestedChildren.rightScope.index == 4 && |
| 150 | + nestedChildren.left.initialFacts == |
| 151 | + #[.nonnegative, .nonnegative, .all] && |
| 152 | + nestedChildren.right.initialFacts == |
| 153 | + #[.nonnegative, .negative, .all] && |
| 154 | + leafReached? rightLeaf && leafReached? nestedLeftLeaf && |
| 155 | + leafReached? nestedRightLeaf |
| 156 | + | _, _, _, _, _ => false |
| 157 | + |
| 158 | +#guard |
| 159 | + (run? .breadthFirst 5).any fun state => |
| 160 | + state.settled && state.steps == 5 && state.splits == 2 && |
| 161 | + state.leaves == 3 && state.nodes.size == 5 && |
| 162 | + state.nodes.toList.countP leafReached? == 3 |
| 163 | + |
| 164 | +end Hex.Interval.NestedBranchConformance |
0 commit comments