Skip to content

Tag-scoped alive-vars contribution for branch snapshots#286

Draft
PhilippGrulich wants to merge 5 commits into
mainfrom
claude/nautilus-trace-memoization-GlwPE
Draft

Tag-scoped alive-vars contribution for branch snapshots#286
PhilippGrulich wants to merge 5 commits into
mainfrom
claude/nautilus-trace-memoization-GlwPE

Conversation

@PhilippGrulich

Copy link
Copy Markdown
Member

Summary

pathExplosion_baseline_threeCallsNoBranch chains three inlined calls to a 3-return leaf with no post-call branches. Its docstring says "Expected: 3 RETURNs ... fan in at the call boundary instead of forking the trace", but the actual trace produces 27 RETURNs (3³). The cause is in recordSnapshot:

return {state->tagRecorder.createTag(),
        hashStaticVector(staticVars) ^ aliveVars.hash()};

aliveVars.hash() mixes the concrete ValueRef ids of every alive variable into the snapshot. When three inlined arms of call 1 each produce a different a-id, the same source-location CMP inside call 2 sees three different snapshots and SymbolicExecutionContext::tagMap explores each as its own path. The Tag itself already encodes call-site (via __builtin_return_address trie), so the alive-vars term only needs to disambiguate two snapshots at the same Tag — across different Tags it's pure noise.

This change makes the alive-vars term scope-relative at branch points only:

  • ScopeFrameStack mirrors the prefix of the current Tag's parent chain. Each frame records aliveVars.hash() at the moment the chain first reached that depth.
  • A new recordBranchSnapshot() (used only by traceBool) replaces the alive-vars term with the XOR delta against frames.back().aliveHashOnEntry. Value-creating ops (CONST/ASSIGN/EQ/binary) keep the original formula so their CFM machinery's ValueRef-alignment invariant is undisturbed.
  • ExecutionTrace::checkBranchTag is a branch-aware checkTag that, when a CFM fires, emits an ASSIGN matchedOperand currentOperand in the predecessor so every incoming edge of the merge block defines the CMP operand.

Gated behind engine.callsiteScopedBranchSnapshots (default false). With the option on, baseline_threeCallsNoBranch's raw trace drops from 27 RETURNs to 3 (B5/B13/B14), with explicit ControlFlowMerge blocks fanning calls 1/2/3 into the next call's body — exactly what the fixture's docstring promised.

Not in this change

Non-CMP path-local ValueRefs (e.g. the val<int32_t> a declared at the call site, which gets a fresh ValueRef per inlined arm) still trip the SSA verifier's "Wrong number of arguments" check when the option is on. That requires either deeper alignment work in processControlFlowMerge or relaxing the SSA verifier — both intentionally deferred. The plumbing here makes the follow-up tractable. This change is the focused TraceContext-level lever the broader sub-trace memoization direction needs as a foundation.

Test plan

  • ctest --test-dir build/nautilus with default options: 198/198 pass, no regressions
  • Manual run with the option flipped on: pathExplosion_baseline_threeCallsNoBranch trace shows 3 RETURNs, 17 blocks total, three ControlFlowMerge blocks fanning in the call-1 arms; calls 2 and 3 likewise — matches the fixture docstring's expectation
  • Loop tests and postCallBranch_* tests unchanged (option off path)
  • Follow-up: align non-CMP path-local ValueRefs at CFM time, then flip the option default and regenerate the path-explosion reference traces

https://claude.ai/code/session_01UeMJ7hcqhyqwYGiRewSsyk


Generated by Claude Code

claude added 3 commits May 14, 2026 17:11
The pathExplosion_baseline_threeCallsNoBranch fixture chains three inlined
calls to a 3-return leaf with no post-call branches.  Its docstring
documents the intended outcome ("Expected: 3 RETURNs ... fan in at the call
boundary instead of forking the trace"), but the actual raw trace produces
27 RETURNs because aliveVars.hash() in recordSnapshot mixes upstream-path
ValueRef ids into the snapshot of in-callee CMPs, fanning every internal
branch out per upstream arm.

Introduce a ScopeFrameStack that mirrors the prefix of the current Tag's
parent chain and a recordBranchSnapshot() (used only by traceBool) that
computes the alive-vars term as an XOR delta against the innermost scope
frame's entry hash.  Pair it with checkBranchTag in ExecutionTrace, which
inserts an ASSIGN matchedOperand currentOperand in the predecessor before
firing the CFM so the merged CMP's operand is defined on every incoming
edge.

Gated behind engine.callsiteScopedBranchSnapshots (default off) so that
existing test reference traces are untouched.  With the option on,
baseline_threeCallsNoBranch's raw trace drops from 27 RETURNs to 3 (3
RETURN-bearing blocks fanning in via explicit ControlFlowMerge blocks);
non-CMP path-local ValueRefs still need follow-up SSA alignment work
before the option can become default.

198/198 tests pass with default options.
Activates the new branch-snapshot scoping introduced in the previous
commit.  baseline_threeCallsNoBranch's raw trace now drops from 27
RETURNs to 3 as the fixture's docstring expected, and the trace fans
in at the call boundary via explicit ControlFlowMerge blocks.

197/198 tests still pass.  The remaining failure is "Path Explosion
Trace Test" — the four chained-inline fixtures (baseline_threeCallsNoBranch,
postCallBranch_1..3) trip the SSA verifier's "Wrong number of arguments"
check because non-CMP path-local ValueRefs (the per-arm fresh val<int32_t>
a/b/c declared at the user's `val<T> a = peLeaf(v)` call site) remain
unaligned at CFM time.  alignCmpOperandInCurrentBlock handles the CMP's
operand specifically; downstream uses (e.g. `if (a < 10)` in the next
inlined arm) still need block-arg threading.

The follow-up is to extend the CFM alignment to every divergent ValueRef
referenced by moved ops, not just the CMP operand.  Once that lands the
path-explosion reference traces can be regenerated and the test will turn
green.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracing Benchmark

Details
Benchmark suite Current: 4b475c7 Previous: f9cd28f Ratio
e2e_tiered_bc_to_mlir 54.113 us (± 7.45545) 56.9167 us (± 7.69512) 0.95
e2e_single_mlir 5.54937 ms (± 426.035) 5.52315 ms (± 41.2737) 1.00
trace_add 4.07248 us (± 319.595) 2.27731 us (± 155.69) 1.79
completing_trace_add 2.28061 us (± 207.745) 2.32405 us (± 143.855) 0.98
trace_ifThenElse 8.66975 us (± 824.97) 8.63915 us (± 776.354) 1.00
completing_trace_ifThenElse 4.73609 us (± 443.187) 4.54071 us (± 401.003) 1.04
trace_deeplyNestedIfElse 26.1046 us (± 3.00469) 25.5445 us (± 1.57172) 1.02
completing_trace_deeplyNestedIfElse 12.8054 us (± 1.11765) 12.559 us (± 987.646) 1.02
trace_loop 8.61724 us (± 810.206) 8.80429 us (± 1.55155) 0.98
completing_trace_loop 4.74316 us (± 414.017) 4.79104 us (± 376.911) 0.99
trace_ifInsideLoop 17.0259 us (± 2.74998) 16.6688 us (± 1.56827) 1.02
completing_trace_ifInsideLoop 8.62858 us (± 1.43292) 8.40114 us (± 807.253) 1.03
trace_loopDirectCall 8.79643 us (± 918.847) 8.6864 us (± 697.439) 1.01
completing_trace_loopDirectCall 4.79646 us (± 550.874) 4.86476 us (± 354.791) 0.99
trace_pointerLoop 14.1543 us (± 1.47552) 14.0902 us (± 1.18648) 1.00
completing_trace_pointerLoop 10.3877 us (± 1.5768) 10.1206 us (± 990.611) 1.03
trace_staticLoop 7.18533 us (± 578.196) 7.21602 us (± 496.139) 1.00
completing_trace_staticLoop 7.18445 us (± 473.472) 7.43532 us (± 991.516) 0.97
trace_fibonacci 10.075 us (± 1.57224) 10.0814 us (± 861.346) 1.00
completing_trace_fibonacci 6.14659 us (± 568.389) 6.00031 us (± 426.095) 1.02
trace_gcd 7.9762 us (± 1.34593) 7.79585 us (± 648.305) 1.02
completing_trace_gcd 4.18252 us (± 711.519) 3.87126 us (± 272.452) 1.08
trace_nestedIf10 38.6581 us (± 6.04038) 38.0283 us (± 3.64798) 1.02
completing_trace_nestedIf10 40.641 us (± 5.80709) 37.3711 us (± 3.21573) 1.09
trace_nestedIf100 1.36724 ms (± 19.8987) 1.35476 ms (± 22.6997) 1.01
completing_trace_nestedIf100 1.37781 ms (± 27.1255) 1.35268 ms (± 26.016) 1.02
trace_chainedIf10 99.6056 us (± 7.86015) 97.7583 us (± 4.73759) 1.02
completing_trace_chainedIf10 50.3516 us (± 4.81571) 48.3258 us (± 3.59147) 1.04
trace_chainedIf100 4.44425 ms (± 29.0219) 4.43042 ms (± 29.6138) 1.00
completing_trace_chainedIf100 2.31749 ms (± 28.8255) 2.24075 ms (± 31.9951) 1.03
ssa_add 180.113 ns (± 13.2104) 178.473 ns (± 9.97434) 1.01
ssa_ifThenElse 500.445 ns (± 37.6302) 459.53 ns (± 43.4269) 1.09
ssa_deeplyNestedIfElse 1.21541 us (± 94.7708) 1.16567 us (± 124.837) 1.04
ssa_loop 518.587 ns (± 23.8382) 487.861 ns (± 38.2642) 1.06
ssa_ifInsideLoop 950.194 ns (± 76.9495) 895.76 ns (± 72.8648) 1.06
ssa_loopDirectCall 530.224 ns (± 30.9166) 491.118 ns (± 45.7827) 1.08
ssa_pointerLoop 628.654 ns (± 48.6706) 583.073 ns (± 41.8959) 1.08
ssa_staticLoop 473.788 ns (± 31.3643) 422.539 ns (± 47.7647) 1.12
ssa_fibonacci 555.651 ns (± 40.7217) 503.737 ns (± 39.1986) 1.10
ssa_gcd 491.254 ns (± 36.7624) 445.234 ns (± 36.8343) 1.10
exec_bc_addOne 35.5674 ns (± 3.29859) 36.0837 ns (± 6.3975) 0.99
exec_mlir_addOne 275.292 ns (± 6.39703) 258.154 ns (± 2.33301) 1.07
exec_cpp_addOne 3.95238 ns (± 0.496238) 3.54853 ns (± 0.249363) 1.11
exec_interpreted_addOne 36.8899 ns (± 2.5494) 37.1985 ns (± 2.33453) 0.99
ir_add 770.203 ns (± 54.753) 757.379 ns (± 38.5584) 1.02
ir_ifThenElse 1.54204 us (± 175.729) 1.58461 us (± 136.165) 0.97
ir_deeplyNestedIfElse 3.51585 us (± 509.286) 3.40826 us (± 363.019) 1.03
ir_loop 1.646 us (± 157.725) 1.63694 us (± 149.086) 1.01
ir_ifInsideLoop 2.76985 us (± 156.51) 2.83439 us (± 293.378) 0.98
ir_loopDirectCall 1.78745 us (± 142.551) 1.80584 us (± 177.074) 0.99
ir_pointerLoop 1.95884 us (± 146.518) 1.97161 us (± 153.779) 0.99
ir_staticLoop 1.47381 us (± 136.776) 1.44956 us (± 119.501) 1.02
ir_fibonacci 1.7415 us (± 171.986) 1.72022 us (± 143.401) 1.01
ir_gcd 1.58738 us (± 423.617) 1.4549 us (± 101.976) 1.09
ir_nestedIf10 7.83673 us (± 761.876) 7.70943 us (± 540.522) 1.02
ir_nestedIf100 91.0254 us (± 4.9978) 93.6887 us (± 16.2569) 0.97
ir_chainedIf10 11.8546 us (± 1.25154) 11.594 us (± 826.837) 1.02
ir_chainedIf100 171.393 us (± 9.67101) 169.138 us (± 8.3263) 1.01
tiered_compile_addOne 53.3527 us (± 6.9883) 55.1658 us (± 10.7264) 0.97
single_compile_mlir_addOne 3.18018 ms (± 90.6188) 3.25749 ms (± 57.5499) 0.98
single_compile_cpp_addOne 24.3344 ms (± 407.264) 24.4391 ms (± 405.29) 1.00
single_compile_bc_addOne 54.6204 us (± 9.01589) 60.6703 us (± 11.2707) 0.90
tiered_compile_sumLoop 75.6064 us (± 14.2942) 79.6202 us (± 15.7788) 0.95
single_compile_mlir_sumLoop 5.13648 ms (± 115.892) 5.35632 ms (± 118.42) 0.96
single_compile_cpp_sumLoop 25.4223 ms (± 470.206) 26.0899 ms (± 453.281) 0.97
single_compile_bc_sumLoop 76.4716 us (± 9.07028) 78.6934 us (± 10.1775) 0.97
comp_mlir_add 5.42246 ms (± 76.7166) 5.55479 ms (± 56.4997) 0.98
comp_mlir_ifThenElse 5.95749 ms (± 132.164) 6.14317 ms (± 38.4483) 0.97
comp_mlir_deeplyNestedIfElse 4.91528 ms (± 112.895) 5.06889 ms (± 99.0918) 0.97
comp_mlir_loop 6.92542 ms (± 140.224) 7.14743 ms (± 31.0585) 0.97
comp_mlir_ifInsideLoop 27.9622 ms (± 300.156) 28.6125 ms (± 100.629) 0.98
comp_mlir_loopDirectCall 11.5615 ms (± 494.564) 11.792 ms (± 44.0495) 0.98
comp_mlir_pointerLoop 27.1203 ms (± 284.425) 27.7179 ms (± 76.6326) 0.98
comp_mlir_staticLoop 4.89959 ms (± 121.533) 5.03112 ms (± 38.6111) 0.97
comp_mlir_fibonacci 10.1802 ms (± 111.61) 10.4618 ms (± 76.5332) 0.97
comp_mlir_gcd 9.22702 ms (± 446.562) 9.43388 ms (± 42.8866) 0.98
comp_mlir_nestedIf10 10.2166 ms (± 109.988) 10.5281 ms (± 45.0755) 0.97
comp_mlir_nestedIf100 24.8654 ms (± 361.677) 25.0116 ms (± 102.345) 0.99
comp_mlir_chainedIf10 9.30746 ms (± 112.527) 9.56929 ms (± 46.3491) 0.97
comp_mlir_chainedIf100 19.9102 ms (± 197.675) 20.2061 ms (± 81.9884) 0.99
comp_cpp_add 24.4327 ms (± 474.887) 24.3267 ms (± 189.47) 1.00
comp_cpp_ifThenElse 24.9472 ms (± 397.586) 24.9638 ms (± 283.6) 1.00
comp_cpp_deeplyNestedIfElse 26.05 ms (± 462.863) 26.0088 ms (± 233.598) 1.00
comp_cpp_loop 25.0272 ms (± 343.336) 25.0851 ms (± 291.998) 1.00
comp_cpp_ifInsideLoop 25.9377 ms (± 366.204) 26.2072 ms (± 775.414) 0.99
comp_cpp_loopDirectCall 25.3427 ms (± 302.822) 25.3831 ms (± 531.136) 1.00
comp_cpp_pointerLoop 25.4718 ms (± 352.492) 25.4858 ms (± 203.254) 1.00
comp_cpp_staticLoop 25.1073 ms (± 612.747) 24.7845 ms (± 176.103) 1.01
comp_cpp_fibonacci 25.8078 ms (± 585.253) 25.2385 ms (± 205.167) 1.02
comp_cpp_gcd 25.3428 ms (± 859.604) 25.0233 ms (± 436.288) 1.01
comp_cpp_nestedIf10 28.3586 ms (± 521.779) 28.0721 ms (± 363.361) 1.01
comp_cpp_nestedIf100 61.9037 ms (± 684.947) 61.7276 ms (± 707.638) 1.00
comp_cpp_chainedIf10 30.5341 ms (± 355.383) 30.7822 ms (± 574.356) 0.99
comp_cpp_chainedIf100 91.6268 ms (± 838.253) 91.9107 ms (± 685.096) 1.00
comp_bc_add 15.3072 us (± 2.95827) 14.7773 us (± 1.91246) 1.04
comp_bc_ifThenElse 18.7253 us (± 3.23789) 19.2429 us (± 3.53172) 0.97
comp_bc_deeplyNestedIfElse 22.8498 us (± 3.51048) 23.0339 us (± 3.44593) 0.99
comp_bc_loop 18.7883 us (± 3.6606) 18.9128 us (± 2.70016) 0.99
comp_bc_ifInsideLoop 21.6726 us (± 2.4675) 21.5194 us (± 2.28647) 1.01
comp_bc_loopDirectCall 19.5765 us (± 2.93716) 19.2878 us (± 2.03113) 1.01
comp_bc_pointerLoop 20.5971 us (± 3.25702) 20.5351 us (± 2.66896) 1.00
comp_bc_staticLoop 17.0099 us (± 2.2341) 17.2132 us (± 2.1155) 0.99
comp_bc_fibonacci 19.1791 us (± 3.03381) 19.1709 us (± 2.93634) 1.00
comp_bc_gcd 18.7806 us (± 4.32212) 18.5754 us (± 2.5724) 1.01
comp_bc_nestedIf10 36.7799 us (± 4.18545) 35.6002 us (± 4.60432) 1.03
comp_bc_nestedIf100 201.53 us (± 12.2262) 198.41 us (± 10.198) 1.02
comp_bc_chainedIf10 52.0644 us (± 7.77225) 52.6073 us (± 9.11991) 0.99
comp_bc_chainedIf100 310.096 us (± 12.0225) 309.294 us (± 26.888) 1.00
comp_asmjit_add 21.0908 us (± 3.95538) 22.1735 us (± 4.82406) 0.95
comp_asmjit_ifThenElse 32.2842 us (± 3.72629) 33.4071 us (± 4.26649) 0.97
comp_asmjit_deeplyNestedIfElse 56.9345 us (± 9.51601) 56.4176 us (± 5.25514) 1.01
comp_asmjit_loop 34.5205 us (± 3.80188) 35.8631 us (± 4.35405) 0.96
comp_asmjit_ifInsideLoop 56.7593 us (± 10.4411) 57.5183 us (± 8.7641) 0.99
comp_asmjit_loopDirectCall 44.8799 us (± 7.51965) 47.2559 us (± 9.00425) 0.95
comp_asmjit_pointerLoop 49.0256 us (± 10.1432) 48.2785 us (± 6.56048) 1.02
comp_asmjit_staticLoop 28.1698 us (± 4.46578) 28.6917 us (± 4.31543) 0.98
comp_asmjit_fibonacci 42.8934 us (± 7.32126) 43.5157 us (± 6.92307) 0.99
comp_asmjit_gcd 34.4511 us (± 3.82449) 35.3711 us (± 4.19816) 0.97
comp_asmjit_nestedIf10 101.806 us (± 10.5981) 103.255 us (± 10.7623) 0.99
comp_asmjit_nestedIf100 1026.1100000000001 us (± 18006) 1057.97 us (± 54427.5) 0.97
comp_asmjit_chainedIf10 164.071 us (± 35.4405) 153.183 us (± 14.6589) 1.07
comp_asmjit_chainedIf100 2.20319 ms (± 223.684) 2.16653 ms (± 46.7063) 1.02
exec_mlir_add 11.0716 ns (± 0.985272) 10.6651 ns (± 0.946586) 1.04
exec_mlir_fibonacci 13.9709 us (± 1.54464) 13.4703 us (± 2.01788) 1.04
exec_mlir_sum 536.426 us (± 31.34) 527.422 us (± 21.2529) 1.02
exec_cpp_add 4.76141 ns (± 0.856455) 4.69868 ns (± 0.745035) 1.01
exec_cpp_fibonacci 96.2419 us (± 9.84059) 96.6361 us (± 9.26507) 1.00
exec_cpp_sum 35.894 ms (± 84.8136) 35.9426 ms (± 115.287) 1.00
exec_bc_add 43.6175 ns (± 6.92386) 44.8635 ns (± 8.015) 0.97
exec_bc_fibonacci 750.915 us (± 9.98364) 859.028 us (± 13.0138) 0.87
exec_bc_sum 163.07 ms (± 1.24625) 186.067 ms (± 1.36597) 0.88
exec_asmjit_add 3.48912 ns (± 0.389821) 3.48273 ns (± 0.424618) 1.00
exec_asmjit_fibonacci 20.4898 us (± 1.65734) 20.8625 us (± 3.3694) 0.98
exec_asmjit_sum 4.8532 ms (± 40.8937) 4.87965 ms (± 312.598) 0.99
exec_bc_add_noRegAlloc 43.3481 ns (± 5.30779) 44.3118 ns (± 3.89068) 0.98
exec_bc_add_regAlloc 43.404 ns (± 3.28639) 44.599 ns (± 5.15383) 0.97
exec_bc_fibonacci_noRegAlloc 754.168 us (± 25.2842) 852.767 us (± 11.3762) 0.88
exec_bc_fibonacci_regAlloc 749.164 us (± 9.1131) 875.208 us (± 25.409) 0.86
exec_bc_sum_noRegAlloc 162.976 ms (± 329.211) 185.974 ms (± 222.438) 0.88
exec_bc_sum_regAlloc 162.88 ms (± 108.795) 185.982 ms (± 165.251) 0.88

This comment was automatically generated by workflow using github-action-benchmark.

claude added 2 commits May 14, 2026 18:06
Following commits enabled engine.callsiteScopedBranchSnapshots and reverted
it because the SSA pass leaked path-local ValueRefs (the val<T> r/a/b/c
defined in only one of the merged arms) up to the root, failing the
argument-count check.

Generalise alignCmpOperandInCurrentBlock from "align the matched CMP's
operand" to "walk the data-flow DAG of the matched CMP's operand and emit
ASSIGN aliases for every ValueRef pair encountered."  At each step we look
up the producer of the reference-path operand and the producer of the
current-path operand (transitively walking the predecessor chain when the
local prefix is empty, as cascade CFMs leave behind), and when the producers
agree on op type we recurse into their inputs.  Visited pairs are
memoised on the (refRef, curRef) tuple so the walk terminates.

This identifies user-visible val<T> variables even when they don't appear
as ASSIGN targets in the trace (peLeaf's `return 1` inlines into a bare
CONST without an intervening ASSIGN), and supplies SSA with enough
predecessor-local definitions to thread block arguments cleanly.

Result with the option on by default:

  pathExplosion_baseline_threeCallsNoBranch:  27 -> 3 RETURNs (9x)
  pathExplosion_postCallBranch_1:              6 -> 2 RETURNs (3x)
  pathExplosion_postCallBranch_2:           ~18 -> 2 RETURNs (9x)
  pathExplosion_postCallBranch_3:           ~54 -> 2 RETURNs (27x)

198/198 tests pass.  Path-explosion reference traces regenerated to reflect
the collapsed structure.

https://claude.ai/code/session_01UeMJ7hcqhyqwYGiRewSsyk
…CUIT_BOOL

The DAG-walk alignment introduced in 71cc889 has a soundness gap: it emits
ASSIGN aliases for ValueRef pairs whose producer ops match by op-kind
without checking that their operand subtrees compute the same value.  Under
SHORT_CIRCUIT_BOOL the `(a && b) || (c && d)` branch tree has two CMPs at
the same source line (e.g. `value > 15` from B2's `!a` arm and from B4's
`a && !b` arm) that my snapshot collapse merges, but the operand DAGs of
the two paths reach distinct CONST literals (B1's CONST 10 for `value < 10`
vs B2's CONST 5 for `value > 5`) and the alignment emits an ASSIGN aliasing
them.  The compiled code then reads the wrong literal on one of the paths
and `multipleConditions(0)` returns 1 instead of 0.

Reverts default for engine.callsiteScopedBranchSnapshots back to false and
restores the path-explosion reference traces to their option-off shape.
The alignment implementation in ExecutionTrace.cpp is preserved as the
opt-in foundation; a follow-up needs to gate ASSIGN emission on full
structural-equivalence of producer subtrees (CONST literal match, recursive
input check) before this can flip on by default.

198/198 tests pass with default off.

https://claude.ai/code/session_01UeMJ7hcqhyqwYGiRewSsyk
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants