Parallel reduction in nox — confluent budget threading
Abstract
Nox is a confluent rewrite system. That's what distinguishes it from Nock,
where the reference interpreter is rigorously sequential and the budget
thread f → f1 → f2 is baked into the specification. We want nox to be
parallel by default — confluence demands it, the prover needs it, the
hardware affords it. The blocker is that "budget" is a linear resource:
two evaluators of the same (object, formula, budget) triple must agree on
the exact Result variant and the exact remaining budget,
otherwise consensus breaks.
This proposal:
- Formalizes the tension between confluence and sequential budget threading.
- Shows why every "obvious" parallelization breaks in the case of sub-expressions with asymmetric cost under a tight budget.
- Surveys four resolution schemes and proves which preserve sequential-equivalence.
- Proposes a single semantics — cost-bounded partitioning with refund-at-join — and a two-mode implementation (Layer 1 strict, Layer 2 permissive).
- Identifies the open theorems and the implementation work.
This is a specification-level proposal. The current sequential interpreter remains correct; this proposal layers a parallel-safe semantics on top with an equivalence theorem.
1. The core tension
1.1 Confluence (have)
Nox's Layer 1 patterns form an orthogonal term rewriting system: each pattern tag uniquely determines a rewrite rule, left-hand sides are linear, no two patterns overlap. By Huet-Lévy (1980), orthogonal TRSs are confluent — independent of evaluation strategy or termination.
Pattern 16 (call) intentionally breaks confluence at the level of witness choice (multiple witnesses may satisfy the same check). But once a witness is fixed, the rewrite system remains orthogonal.
Pattern 17 (look) is deterministic — confluence is preserved.
Layer 3 (jets) preserves confluence by observational equivalence to Layer 1 expansions.
So: the rewrite system is confluent. Two evaluators on the same input WILL reach the same normal form, in finite time, if budget permits.
1.2 Budget (need)
Budget is a linear resource. Each reduction step decrements it. The sequential semantics threads it through:
reduce(o, [op [a b]], f):
c = cost(op)
if f < c then Halt(f)
(va, f1) = reduce(o, a, f - c) -- f → f1
(vb, f2) = reduce(o, b, f1) -- f1 → f2
return (op(va, vb), f2)
For consensus, two evaluators must agree on:
- The
Resultvariant (Ok/Halt/Error(kind)) - The result
Noun(whenOk) - The remaining budget (when
OkorHalt) - The error kind (when
Error)
The Noun agreement is given by confluence. The budget agreement requires that every evaluator threads f → f1 → f2 → ... in the same way. Sequential threading enforces this trivially. Parallel threading does not.
1.3 The conflict
Confluence says: it doesn't matter whether you reduce a before b or
in parallel — the result is the same. But sequential budget threading
DOES depend on order: b runs with f - c - cost(a), not f - c. If
b is run in parallel with f - c directly, it gets MORE budget than
sequential gives it.
When budgets are loose (much greater than total cost), this doesn't matter. When budgets are tight (close to total cost), parallel and sequential can disagree on whether the program halted, and on the remaining budget value.
2. Why naive parallelism breaks — three counter-examples
Let c = cost(op) = 1 throughout; we focus on the budget threading for
sub-formulas. Let cost(formula) denote actual incurred cost (sequential).
2.1 Asymmetric branches, loose split
f = 102
formula = [op [a b]] with cost(a) = 100, cost(b) = 1
sequential: a runs with 101 → succeeds, f1 = 1. b runs with 1 → succeeds, f2 = 0.
Result: Ok(val, 0).
equal-split: a gets 50, halts (101 > 50, but cost(a)=100 > 50 already aborts).
Result: Halt(50). DIVERGES from sequential.
2.2 Tight budget, race to halt
f = 50, c = 1, so f - c = 49
cost(a) = 30, cost(b) = 26
sequential: a runs with 49, succeeds, returns 19. b runs with 19, halts
because some inner step needs 25 > 19. Result: Halt(x_b_seq).
parallel-full-budget: a runs with 49, succeeds (used 30, returns 19).
b runs with 49 (parallel sees full budget!), succeeds
(used 26, returns 23). Join: total work = 1+30+26=57 > 50.
Result: Halt(?). VALUE OF HALT DIFFERS from sequential.
This is the deep problem. The Halt variant agrees (both halt), but the remaining-budget payload may differ.
2.3 Halt cascade with order dependence
f = 20, formula = [op [a b]]
cost(a) = 21 (a halts immediately, returns Halt(20))
cost(b) = 5 (b would succeed if it ran)
sequential: a halts at f-c=19. Halt(?) propagates. b NEVER RUNS.
Result: Halt(value-of-a-halt).
parallel: a halts, b succeeds. At join: a halted → propagate. Halt(value-of-a-halt).
Result matches if join takes a's halt deterministically. ✓
Case 2.3 is salvageable with a deterministic "first branch's halt wins" join rule. Cases 2.1 and 2.2 require deeper changes.
3. The space of solutions
Five conceptually distinct schemes. Each fixes a different aspect of the tension.
Scheme S1 — Full-budget speculative, sum at join
Each branch runs with the parent's f - c budget as soft cap. At join,
sum the actually-incurred costs; halt if sum > f - c.
parallel_reduce(o, [op [a b]], f):
if f < c then Halt(f)
let (va, used_a) = parallel_reduce(o, a, f - c) ∥ (vb, used_b) = parallel_reduce(o, b, f - c)
let total = c + used_a + used_b
if total > f: Halt(f mod total) -- or some defined halt-value
else: Ok((va, vb), f - total)
Soundness gap: case 2.2 (tight budget). Sequential gives b budget
f1 = f - c - used_a. Parallel gives b budget f - c. If b contains
a step costing > f1 but ≤ f - c, parallel succeeds where sequential
halts. They disagree.
Fix attempt: make b's execution sensitive to the eventual join.
Impossible without sequential dependency.
Verdict: ❌ Not sequential-equivalent in general.
Scheme S2 — Static equal partition
Pre-partition: each branch gets ⌊(f-c)/2⌋. Run independently. Join with
sum of remaining.
Soundness gap: case 2.1. Asymmetric costs.
Verdict: ❌ Not sequential-equivalent.
Scheme S3 — Reformulate "sequential" to match partitioning
Redefine sequential semantics so that it ALSO partitions the budget. Then parallel and "sequential" agree by construction (they ARE the same semantics).
reduce(o, [op [a b]], f):
if f < c then Halt(f)
let fa = (f - c) / 2
let fb = (f - c) - fa
let (va, ra) = reduce(o, a, fa)
let (vb, rb) = reduce(o, b, fb)
... combine ...
This is a new language. Programs that worked under classical sequential threading may halt under the new semantics if costs are uneven.
Verdict: ✅ Self-consistent. ❌ Different language from Nock-style sequential. Lose ability to "borrow" budget from one branch to another.
Scheme S4 — Cost-bound annotated formulas
Every formula t has a statically computable upper bound bound(t) ≥ actual_cost(t). Partition by bound, refund unused at join.
parallel_reduce(o, [op [a b]], f):
if f < c then Halt(f)
if bound([op [a b]]) > f then Halt(f) -- eager bound check
let fa = bound(a), fb = bound(b)
let (va, ra) = parallel_reduce(o, a, fa) ∥ (vb, rb) = parallel_reduce(o, b, fb)
let used = (fa - ra) + (fb - rb) + c
return (op(va, vb), f - used)
This works if bound() is exact for every formula. For nox patterns
0, 1, 3, 5-15, 17 the bound is structural:
bound(atom) = 0bound([0 _]) = 1(axis)bound([1 _]) = 1(quote)bound([3 [a b]]) = 1 + bound(a) + bound(b)bound([5..7,9..14 [a b]]) = COST_op + bound(a) + bound(b)bound([8 a]) = 64 + bound(a)bound([15 a]) = 25 + bound(a)bound([17 [n k]]) = 1 + bound(n) + bound(k)
For pattern 4 (branch), bound is the max over arms (only one is
chosen): bound([4 [t [y n]]]) = 1 + bound(t) + max(bound(y), bound(n)).
For pattern 2 (compose) and pattern 16 (call), the bound is dynamic:
[2 [x y]]reducesxandy, thenreduce(rx, ry). The third reduce depends onry's runtime value. No static bound.[16 [tag check]]injects a prover-supplied witness, whose cost is unknown to the formula author.
Verdict: ✅ Sequential-equivalent for Layer 1 patterns. ⚠️ Compose and call need dynamic cost handling.
Scheme S5 — Cost is part of the witness
Extend Scheme S4 by requiring compose results and call witnesses to declare their cost. The reducer verifies the declaration.
For compose [2 [x y]]:
rx = reduce(o, x), ry = reduce(o, y)
ry must be of form [bound formula_body] — cost declaration in first slot
reduce(rx, formula_body) with budget bound
For call [16 [tag check]]:
witness = provider(tag, o)
witness must be of form [witness_bound payload]
-- cost is declared by the prover; verifier checks during proof gen
Verdict: ✅ Sequential-equivalent everywhere. ❌ Imposes new formula structure conventions (potential incompatibility, schema burden).
4. The proposal — S4 with explicit dynamic-cost frontier
We adopt Scheme S4 as the parallel semantics for Layer 1 patterns, and explicitly mark pattern 2 (compose) and pattern 16 (call) as having a sequential continuation phase.
4.1 Definition: bound(t) : Formula → ℕ
bound(atom) = 0
bound([0 a]) = 1
bound([1 c]) = 1
bound([2 [x y]]) = 1 + bound(x) + bound(y) + DYNAMIC
bound([3 [a b]]) = 1 + bound(a) + bound(b)
bound([4 [t [y n]]]) = 1 + bound(t) + max(bound(y), bound(n))
bound([5 [a b]]) = 1 + bound(a) + bound(b) -- add
bound([6 [a b]]) = 1 + bound(a) + bound(b) -- sub
bound([7 [a b]]) = 1 + bound(a) + bound(b) -- mul
bound([8 a]) = 64 + bound(a) -- inv
bound([9 [a b]]) = 1 + bound(a) + bound(b) -- eq
bound([10 [a b]]) = 64 + bound(a) + bound(b) -- lt
bound([11 [a b]]) = 32 + bound(a) + bound(b) -- xor
bound([12 [a b]]) = 32 + bound(a) + bound(b) -- and
bound([13 a]) = 32 + bound(a) -- not
bound([14 [a n]]) = 32 + bound(a) + bound(n) -- shl
bound([15 a]) = 25 + bound(a) -- hash
bound([16 [tag check]]) = 1 + bound(tag) + DYNAMIC
bound([17 [n k]]) = 1 + bound(n) + bound(k)
DYNAMIC is a sentinel meaning "not statically bounded". The frontier is
crossed at compose and call.
4.2 Parallel reduction rule (binary structural patterns)
For op ∈ {3, 5, 6, 7, 9, 10, 11, 12, 14, 17} (binary, both arms always
evaluated, no dynamic continuation):
parallel_reduce(o, [op [a b]], f):
c = cost(op)
if f < c then Halt(f)
if bound(a) + bound(b) + c > f then
-- not enough budget. fall back to sequential to determine the precise halt point.
sequential_reduce(o, [op [a b]], f)
else
let (va, ra) = parallel_reduce(o, a, bound(a)) ∥ (vb, rb) = parallel_reduce(o, b, bound(b))
let used = (bound(a) - ra) + (bound(b) - rb) + c
Ok(op_apply(va, vb), f - used)
The fallback to sequential when bound > budget preserves the sequential
halt semantics for tight budgets. When bound ≤ budget, parallel is
sound: each branch has enough budget to complete (since actual_cost ≤ bound), so no branch can halt internally; the result is identical to
sequential by confluence + arithmetic of refund.
4.3 Unary parallel patterns (8, 13, 15)
parallel_reduce(o, [op a], f):
c = cost(op)
if f < c then Halt(f)
if bound(a) + c > f then sequential_reduce(o, [op a], f)
else
let (va, ra) = parallel_reduce(o, a, bound(a))
Ok(op_apply(va), f - ((bound(a) - ra) + c))
Single sub-formula; no parallelism within this rule, but a itself may
contain parallel sub-formulas.
4.4 Branch (4) — lazy with arm-bound
parallel_reduce(o, [4 [t [y n]]], f):
c = 1
bound_total = 1 + bound(t) + max(bound(y), bound(n))
if f < c then Halt(f)
if bound_total > f then sequential_reduce(o, [4 [t [y n]]], f)
else
let (vt, rt) = parallel_reduce(o, t, bound(t))
let chosen = if vt == 0 then y else n
let (vc, rc) = parallel_reduce(o, chosen, bound(chosen))
Ok(vc, f - ((bound(t) - rt) + (bound(chosen) - rc) + 1))
The two reductions are sequential (must know vt to pick chosen).
Slack from the unchosen arm is refunded: max(bound(y), bound(n)) - bound(chosen) ≥ 0 extra budget returns to the caller.
4.5 Compose (2) — partial parallelism
parallel_reduce(o, [2 [x y]], f):
if f < 1 then Halt(f)
if bound(x) + bound(y) + 1 > f then sequential_reduce(o, [2 [x y]], f)
else
let (rx, rrx) = parallel_reduce(o, x, bound(x)) ∥ (ry, rry) = parallel_reduce(o, y, bound(y))
let f_after_init = f - 1 - (bound(x) - rrx) - (bound(y) - rry)
-- third reduce is sequential, its cost is dynamic
sequential_reduce(rx, ry, f_after_init)
The first two sub-reductions parallelize via static bounds. The third runs sequentially with whatever budget remains.
4.6 Call (16) — sequential after witness arrival
parallel_reduce(o, [16 [tag check]], f):
if f < 1 then Halt(f)
let (vtag, rt) = parallel_reduce(o, tag, bound(tag)) -- bounded, parallel-internal
let witness = provider(vtag, o)
if witness is None then Halt(remaining_budget)
let witness_object = cell(witness, o)
let f_after_witness = f - 1 - (bound(tag) - rt)
sequential_reduce(witness_object, check, f_after_witness)
Tag evaluation parallelizes internally; everything after the provider hand-off is sequential because the witness is dynamic.
4.7 Atomic patterns (0, 1, 17)
bound = 1 + bound(args). No internal parallelism (axis is a tree walk;
quote returns a literal; look does one provider call). Sequential
implementation, no semantic difference.
5. The sequential-equivalence theorem
Theorem (parallel-sequential equivalence). For every formula t,
object o, and budget f:
parallel_reduce(o, t, f).result_kind == sequential_reduce(o, t, f).result_kind
parallel_reduce(o, t, f).data == sequential_reduce(o, t, f).data when result_kind = Ok
parallel_reduce(o, t, f).remaining == sequential_reduce(o, t, f).remaining when result_kind ∈ {Ok, Halt}
parallel_reduce(o, t, f).error_kind == sequential_reduce(o, t, f).error_kind when result_kind = Error
Proof sketch (by structural induction on t):
Base cases (atoms, quote): cost is trivial, no sub-formulas. sequential and parallel are identical.
Inductive cases (Layer 1 binary patterns):
- If
bound(t) > f: both fall back to sequential. Trivially equal. - If
bound(t) ≤ f: by definitionactual_cost(a) ≤ bound(a)for both sub-formulas. Each sub-reduction in parallel has enough budget (bound(a) ≥ actual_cost(a)), so neither halts internally — both returnOk. The combined result isop_apply(va, vb). Sequential with f ≥ bound(t) ≥ actual_cost(t) also returnsOk. By confluence,va,vb, and henceop_applyare identical. - Remaining-budget computation:
- Sequential:
f → f - c → f - c - actual_cost(a) → f - c - actual_cost(a) - actual_cost(b). - Parallel:
f - c - (bound(a) - ra) - (bound(b) - rb)wherera = bound(a) - actual_cost(a)and similarly forrb. Sobound(a) - ra = actual_cost(a). Same value.
- Sequential:
Branch (4): by IH on chosen arm. The unchosen arm is never reduced in
either semantics. Bound provides upper estimate for the chosen arm; only
chosen arm's actual_cost is consumed.
Compose (2), call (16): parallel parts are bounded and use the binary case; the dynamic continuation is invoked sequentially. By IH the parallel parts produce identical results; the continuation runs in identical sequential semantics; equivalence follows.
∎
The theorem requires bound() to be a true upper bound on actual cost.
Conservatively, this is satisfied by the formula above, since pattern 4
takes max-of-arms (loose) and all other patterns compute exact structural
sums (tight).
6. Implementation strategy
6.1 Two API surfaces
// existing — never changes
;
// new — parallel-safe, identical Result
;
reduce_parallel is observationally identical to reduce. Callers can
swap freely. The library guarantees Result-equivalence by the theorem
above.
6.2 The Order sharing problem
Order<N> is mutated during reduction (atoms/cells allocated for
results). Parallel threads would race on it. Three options:
(a) Per-thread scratch + merge. Each thread has its own scratch
Order. At join, allocations from each scratch are merged into a single
canonical Order via the hash-cons table (identical data bytes → same
slot). Merge is O(scratch size × hash cost).
(b) Atomic Order. Order's allocator becomes a lock-free hash table.
Read paths are uncontended; write paths take a CAS. Hash-consing already
provides the unique-allocation invariant; we just need to make
alloc_raw atomic.
(c) Pre-allocate. Before parallel phase, pre-compute the set of allocations each branch will need. Allocate in parent; pass OrderIds into branches. Requires static analysis of allocation patterns — likely intractable for non-trivial formulas.
Recommendation: (b) for the rust impl; (a) for distributed evaluation where threads can't share memory.
6.3 Trace row ordering
The parallel evaluator produces rows in non-deterministic order. For witness-hash determinism, rows must be emitted in a canonical order.
Solution: each row carries a structural index — its position in the
reduce tree (e.g., a path string "2.0.1" meaning "left child of left
child of pattern 2 from root"). At end of parallel evaluation, the
collected rows are sorted by structural index before tracer emission.
This is one additional pass over the trace; ~O(N) work where N is row count. Acceptable cost for parallelism.
6.4 Bound computation cost
bound(t) is itself a tree walk over the formula. For a one-shot
parallel reduction, this is O(formula size) — comparable to the
reduction itself. For repeated reductions of the same formula, the
bound can be cached at the OrderId level via the Order's hash-cons
machinery (one more entry per data: cached_bound: u64).
The cache invariant: bound(data) is a pure function of data structure,
so hash-consed identical sub-data share the same bound. Cache hit rate
should be high for any recursive program.
7. Open theorems and validation
7.1 Theorems to formalize
T1 (sequential-equivalence): stated in §5. Should be machine-checked (Coq/Lean) for confidence given the consensus implications.
T2 (bound monotonicity): bound(t) is monotone under sub-data
substitution. Used in the proof of T1's compose case.
T3 (parallel commutativity): for binary patterns with both arms parallelizable, swapping the parallel execution order produces identical witness traces (modulo the canonical sort). Used to argue implementation-independence.
7.2 Counter-examples to construct
For each rejected scheme (S1, S2, S3), the spec MUST carry one concrete counter-example showing where it diverges from sequential. This is the "do not reinvent" record for future readers.
7.3 Test plan
- Round-trip tests: for 1000 random
(o, t, f)triples,reduce(o, t, f) == reduce_parallel(o, t, f). - Stress tests: deeply nested compose chains (where dynamic cost dominates); tight-budget scenarios at the bound boundary.
- Adversarial: formulas designed to exhibit the worst-case bound vs actual-cost gap (bound = 10^6 but actual = 10).
8. Migration path
8.1 Phase 0 (this proposal accepted)
- Spec: add §parallel reduction to
reduction.md, replacing the current "open spec gap" note at line 129. - Code: no changes. Sequential remains canonical.
8.2 Phase 1 (bound function)
- Add
bound: Formula → ℕtodata/order.rsas a cached field onNounEntry. - Compute bound at data construction. Pattern 2 and pattern 16 sub-data bounds carry a "DYNAMIC" sentinel.
- Specs:
data/order.mddocuments the bound field.
8.3 Phase 2 (parallel-safe Order)
- Refactor
Order::alloc_rawto atomic-cas allocation. - Tests: stress allocator with N threads doing concurrent atom/cell allocation. Verify hash-cons invariants hold.
8.4 Phase 3 (parallel reduce)
- Add
reduce_parallel()API. - Implement structural-index-sorted trace.
- Round-trip tests against
reduce(). - Document the equivalence theorem in
rs/lib.rsdoc comment.
8.5 Phase 4 (formal verification)
- Port the theorem to Lean. Machine-check T1 sequential-equivalence.
- Cross-check with zheng's witness generation: same witness hash on serial and parallel runs.
9. Implications beyond nox
9.1 zheng witness generation
zheng generates one constraint system per pattern row. Order-of-rows doesn't affect the constraint system if rows are sorted canonically. So zheng can consume parallel traces without modification, once row ordering is canonical. Without canonical ordering, zheng would produce different proofs for the same program — which is unacceptable.
9.2 Distributed evaluation
The parallel semantics extends naturally to distributed: a coordinator splits sub-formulas across worker nodes, each worker reduces locally, results are combined at the coordinator. Bound-based partitioning gives the coordinator a static work estimate for scheduling.
9.3 The Nock comparison
Nock evaluators are sequential. Parallel Nock is folklore — every implementation has its own ad-hoc rules, none are consensus-safe. Nox formalizes parallel reduction at the spec level. This is the substantive difference from Nock: not just "the patterns are different" but "the semantics is parallel by construction".
9.4 Related work
- Lévy parallel reduction (1980, lambda calculus): optimal reduction via labeled redexes. Establishes that sharing of identical sub-terms enables parallelism without cost duplication. Nox's hash-consing provides exactly this sharing.
- Lamping's algorithm (1990, interaction nets): a concrete impl of Lévy's optimal reduction. Out of scope for nox (more complex than needed) but technically applicable.
- Linear logic (Girard, 1987): budget as a linear resource maps to TENSOR (parallel partition) and WITH (shared, additive). Our scheme S4 is essentially TENSOR for binary patterns and WITH for branch.
- Resource semantics in Coq (FunLog, Iris): formal frameworks for proving resource-bounded programs. Could host the T1 machine-check.
- Cairo, RISC-Zero, Plonky2: all use fixed-cost-per-step sequential execution. None aspire to confluent parallelism. Nox occupies a unique position.
10. Decision request
This proposal asks for sign-off on:
- Adopting Scheme S4 (cost-bound partitioning with refund-at-join, sequential fallback for over-budget) as the parallel semantics.
- Pattern 2 and pattern 16 carry an explicit DYNAMIC marker in their bound. They are not parallelizable past their first phase.
- Phasing: spec change (§reduction.md) lands now; rust impl follows in 3 phases (bound caching, atomic Order, parallel reduce).
- Formal verification track: T1 is goal-stated; machine-checked proof is non-blocking for v1 release but blocks v2.
Open questions to discuss:
- Should
boundbe carried inOrder::NounEntry(always-on cache) or computed on-demand? Cache costs ~16 bytes per data (one u64 + status bit), gives O(1) bound lookup at the price of permanent memory. - For the
Haltpayload in the over-budget fallback case: do we want the sequential halt's precise remaining-budget value (requires running sequentially in fallback) or a simplerHalt(0)(loses precision but parallelizable)? - Cross-regime considerations: does the bound function need to be
algebra-aware (different costs per regime per
vm.mdtable)? If so, bound becomesbound(t, regime)and the cache becomes regime-keyed.
Appendix A: rejected lemmas
For the historical record, here are sequence-equivalence claims that LOOK plausible but FAIL:
False claim 1: "Sum of sub-formula costs equals total cost". False when sub-formulas share state via compose (third reduce sees both).
False claim 2: "Confluence implies budget order-independence". False; confluence is about normal-form uniqueness, not about resource consumption path.
False claim 3: "If f ≥ actual_cost(t), sequential and any
parallel strategy produce identical Result". True only if parallel
gives each branch ≥ actual_cost of that branch. False if parallel uses
fixed partition smaller than actual cost.
False claim 4: "Pattern 4 (branch) bound is bound(t) + bound(y) + bound(n)". False because only one arm is reduced; sum overestimates.
Correct bound uses max-of-arms.
Appendix B: bound function as Rust
Appendix C: example traces under both semantics
For program [5 [[1 3] [1 5]]] with budget 100 (cost 3):
Sequential:
row 0: tag=5, budget_in=100, budget_out=97
row 1: tag=1, budget_in=99, budget_out=98 (left quote)
row 2: tag=1, budget_in=98, budget_out=97 (right quote)
Parallel (with canonical sort by structural index (), (1.0), (1.1)):
row 0: tag=5, budget_in=100, budget_out=97, struct_idx=()
row 1: tag=1, budget_in=99, budget_out=98, struct_idx=(1.0)
row 2: tag=1, budget_in=98, budget_out=97, struct_idx=(1.1)
Identical after sort. The budget_in/budget_out values reflect the
partitioned-then-refunded view: each branch's budget_in is its
bound(branch); budget_out is bound(branch) - actual_cost(branch).
For atomic quotes, both are 1 → 0, so the visible row contents may
differ from sequential's threading view. But the parent's final
remaining (97) and the result OrderId are identical.
This is the subtle point: observable result identical; per-row budget values differ on intermediate rows. The witness hash includes per-row budget, so the witness IS different. For consensus we need EITHER:
- (option α) only the parent budget matters; sub-row budgets are ignored in the witness hash; OR
- (option β) the witness format is the parallel-canonical one; all evaluators (including sequential interpreters) produce the parallel budget values to match.
This is the dual-mode budget bookkeeping decision. Recommend option β: all reductions, sequential or parallel, follow the partitioned-budget model. Sequential becomes a single-threaded execution of the same semantics. Then witnesses are always identical regardless of execution strategy.
This means adopting the parallel semantics as THE semantics, and sequential becomes just a particular implementation choice. The "sequential-equivalence theorem" then degrades to a statement about implementations: any implementation that follows the spec produces the same witness, regardless of whether it ran one thread or many.
This is the cleanest design and what we recommend.