Add explicit control-flow API (If/While/For) - #300
Draft
PhilippGrulich wants to merge 6 commits into
Draft
Conversation
Contributor
There was a problem hiding this comment.
Tracing Benchmark
Details
| Benchmark suite | Current: ef68c45 | Previous: d63bd8f | Ratio |
|---|---|---|---|
exec_mlir_add |
11.0712 ns (± 0.967408) |
10.5656 ns (± 1.08602) |
1.05 |
exec_mlir_fibonacci |
17.0254 us (± 1.85862) |
13.0323 us (± 1.09201) |
1.31 |
exec_mlir_sum |
579.89 us (± 62.2236) |
544.744 us (± 29.5581) |
1.06 |
exec_cpp_add |
4.64652 ns (± 0.66486) |
4.75823 ns (± 1.02643) |
0.98 |
exec_cpp_fibonacci |
108.376 us (± 8.68802) |
96.1834 us (± 7.67007) |
1.13 |
exec_cpp_sum |
23.6076 ms (± 290.035) |
35.9165 ms (± 549.897) |
0.66 |
exec_bc_add |
45.3422 ns (± 7.3473) |
43.6174 ns (± 3.93352) |
1.04 |
exec_bc_fibonacci |
700.998 us (± 12.5978) |
842.262 us (± 14.6154) |
0.83 |
exec_bc_sum |
165.402 ms (± 2.56586) |
179.187 ms (± 955.653) |
0.92 |
exec_asmjit_add |
3.54416 ns (± 0.265601) |
3.60782 ns (± 0.497292) |
0.98 |
exec_asmjit_fibonacci |
22.2166 us (± 1.42737) |
21.5143 us (± 4.60828) |
1.03 |
exec_asmjit_sum |
5.3001 ms (± 28.8712) |
4.85738 ms (± 33.8584) |
1.09 |
exec_bc_add_noRegAlloc |
44.7281 ns (± 4.8541) |
45.4957 ns (± 7.38134) |
0.98 |
exec_bc_add_regAlloc |
44.41 ns (± 4.80615) |
43.8958 ns (± 4.00479) |
1.01 |
exec_bc_fibonacci_noRegAlloc |
708.888 us (± 39.7798) |
844.035 us (± 14.8666) |
0.84 |
exec_bc_fibonacci_regAlloc |
700.92 us (± 11.1988) |
842.01 us (± 14.1375) |
0.83 |
exec_bc_sum_noRegAlloc |
166.514 ms (± 13.4918) |
178.948 ms (± 362.678) |
0.93 |
exec_bc_sum_regAlloc |
165.185 ms (± 1.24748) |
179.176 ms (± 555.823) |
0.92 |
ir_add |
737.895 ns (± 42.0488) |
795.982 ns (± 87.5329) |
0.93 |
ir_ifThenElse |
1.51726 us (± 111.785) |
1.63138 us (± 141.868) |
0.93 |
ir_deeplyNestedIfElse |
3.28903 us (± 243.726) |
3.34702 us (± 226.142) |
0.98 |
ir_loop |
1.56014 us (± 123.349) |
1.64605 us (± 113.623) |
0.95 |
ir_ifInsideLoop |
2.71115 us (± 174.615) |
2.88247 us (± 245.938) |
0.94 |
ir_loopDirectCall |
1.74363 us (± 119.54) |
1.85863 us (± 162.199) |
0.94 |
ir_pointerLoop |
1.91153 us (± 118.01) |
2.0172 us (± 179.225) |
0.95 |
ir_staticLoop |
1.47986 us (± 82.8024) |
1.4698 us (± 172.512) |
1.01 |
ir_fibonacci |
1.68547 us (± 101.3) |
1.74373 us (± 101.681) |
0.97 |
ir_gcd |
1.43211 us (± 132.305) |
1.5363 us (± 98.0465) |
0.93 |
ir_nestedIf10 |
7.6688 us (± 697.208) |
7.53336 us (± 572.678) |
1.02 |
ir_nestedIf100 |
90.6165 us (± 4.16844) |
86.6056 us (± 6.84222) |
1.05 |
ir_chainedIf10 |
11.8133 us (± 1.04125) |
11.706 us (± 1.05245) |
1.01 |
ir_chainedIf100 |
167.555 us (± 6.06906) |
162.563 us (± 10.9684) |
1.03 |
ir_loopExplicit |
1.95978 us (± 118.748) |
||
ir_gcdExplicit |
1.33239 us (± 150.114) |
||
ir_nestedIf10Explicit |
10.2351 us (± 606.914) |
||
ir_chainedIf10Explicit |
11.3535 us (± 794.845) |
||
ir_chainedIf100Explicit |
151.702 us (± 7.85716) |
||
comp_mlir_add |
6.73455 ms (± 285.356) |
5.59561 ms (± 178.594) |
1.20 |
comp_mlir_ifThenElse |
7.52709 ms (± 211.482) |
6.2488 ms (± 412.914) |
1.20 |
comp_mlir_deeplyNestedIfElse |
6.32428 ms (± 251.624) |
5.04225 ms (± 200.911) |
1.25 |
comp_mlir_loop |
8.87658 ms (± 245.137) |
7.12491 ms (± 237.471) |
1.25 |
comp_mlir_ifInsideLoop |
31.7429 ms (± 295.249) |
28.6722 ms (± 476.575) |
1.11 |
comp_mlir_loopDirectCall |
13.9529 ms (± 343.037) |
11.808 ms (± 265.45) |
1.18 |
comp_mlir_pointerLoop |
30.6671 ms (± 409.687) |
27.8439 ms (± 674.969) |
1.10 |
comp_mlir_staticLoop |
5.97853 ms (± 188.177) |
4.99347 ms (± 156.303) |
1.20 |
comp_mlir_fibonacci |
12.9372 ms (± 257.21) |
10.3822 ms (± 196.241) |
1.25 |
comp_mlir_gcd |
11.2887 ms (± 226.464) |
9.44927 ms (± 264.935) |
1.19 |
comp_mlir_nestedIf10 |
12.5487 ms (± 260.307) |
10.4284 ms (± 206.842) |
1.20 |
comp_mlir_nestedIf100 |
27.1037 ms (± 203.028) |
24.8611 ms (± 502.505) |
1.09 |
comp_mlir_chainedIf10 |
11.7744 ms (± 206.076) |
9.5372 ms (± 230.498) |
1.23 |
comp_mlir_chainedIf100 |
23.0814 ms (± 240.101) |
20.5244 ms (± 490.216) |
1.12 |
comp_mlir_loopExplicit |
8.96576 ms (± 227.49) |
||
comp_mlir_gcdExplicit |
11.1918 ms (± 182.115) |
||
comp_mlir_nestedIf10Explicit |
12.4176 ms (± 263.98) |
||
comp_mlir_chainedIf10Explicit |
11.4817 ms (± 247.884) |
||
comp_mlir_chainedIf100Explicit |
22.5637 ms (± 241.509) |
||
comp_cpp_add |
29.8458 ms (± 502.834) |
||
comp_cpp_ifThenElse |
30.1822 ms (± 412.019) |
||
comp_cpp_deeplyNestedIfElse |
31.4409 ms (± 392.926) |
||
comp_cpp_loop |
30.6173 ms (± 414.894) |
||
comp_cpp_ifInsideLoop |
31.4386 ms (± 462.842) |
||
comp_cpp_loopDirectCall |
30.8984 ms (± 514.651) |
||
comp_cpp_pointerLoop |
30.8774 ms (± 454.299) |
||
comp_cpp_staticLoop |
30.6028 ms (± 602.088) |
||
comp_cpp_fibonacci |
30.6823 ms (± 455.166) |
||
comp_cpp_gcd |
30.9411 ms (± 404.776) |
||
comp_cpp_nestedIf10 |
33.3159 ms (± 535.384) |
||
comp_cpp_nestedIf100 |
66.8244 ms (± 302.102) |
||
comp_cpp_chainedIf10 |
35.8279 ms (± 510.534) |
||
comp_cpp_chainedIf100 |
97.8869 ms (± 1.50644) |
||
comp_cpp_loopExplicit |
31.1342 ms (± 342.155) |
||
comp_cpp_gcdExplicit |
30.5738 ms (± 313.009) |
||
comp_cpp_nestedIf10Explicit |
34.3866 ms (± 442.1) |
||
comp_cpp_chainedIf10Explicit |
35.2514 ms (± 609.732) |
||
comp_cpp_chainedIf100Explicit |
85.7684 ms (± 444.66) |
||
comp_bc_add |
13.9169 us (± 2.55371) |
||
comp_bc_ifThenElse |
15.7682 us (± 2.75612) |
||
comp_bc_deeplyNestedIfElse |
19.791 us (± 4.99383) |
||
comp_bc_loop |
15.8716 us (± 2.93568) |
||
comp_bc_ifInsideLoop |
18.3923 us (± 4.1608) |
||
comp_bc_loopDirectCall |
16.0146 us (± 3.6726) |
||
comp_bc_pointerLoop |
17.716 us (± 4.42899) |
||
comp_bc_staticLoop |
15.7268 us (± 3.1669) |
||
comp_bc_fibonacci |
16.4309 us (± 3.09246) |
||
comp_bc_gcd |
15.6309 us (± 2.96836) |
||
comp_bc_nestedIf10 |
30.6249 us (± 4.79893) |
||
comp_bc_nestedIf100 |
195.111 us (± 12.7317) |
||
comp_bc_chainedIf10 |
43.523 us (± 9.2614) |
||
comp_bc_chainedIf100 |
311.668 us (± 13.0632) |
||
comp_bc_loopExplicit |
16.8916 us (± 3.66805) |
||
comp_bc_gcdExplicit |
15.3346 us (± 2.82387) |
||
comp_bc_nestedIf10Explicit |
34.2867 us (± 5.27455) |
||
comp_bc_chainedIf10Explicit |
33.5051 us (± 4.96747) |
||
comp_bc_chainedIf100Explicit |
241.59 us (± 10.909) |
||
comp_asmjit_add |
17.5392 us (± 5.62026) |
||
comp_asmjit_ifThenElse |
26.3073 us (± 6.14298) |
||
comp_asmjit_deeplyNestedIfElse |
50.1955 us (± 16.2805) |
||
comp_asmjit_loop |
28.8873 us (± 6.86097) |
||
comp_asmjit_ifInsideLoop |
49.5814 us (± 12.4179) |
||
comp_asmjit_loopDirectCall |
32.0131 us (± 7.61984) |
||
comp_asmjit_pointerLoop |
34.8156 us (± 6.69106) |
||
comp_asmjit_staticLoop |
24.1165 us (± 6.16998) |
||
comp_asmjit_fibonacci |
31.7857 us (± 9.9986) |
||
comp_asmjit_gcd |
28.7462 us (± 7.34748) |
||
comp_asmjit_nestedIf10 |
95.2504 us (± 14.7172) |
||
comp_asmjit_nestedIf100 |
1.07993 ms (± 31.3352) |
||
comp_asmjit_chainedIf10 |
148.139 us (± 18.7611) |
||
comp_asmjit_chainedIf100 |
2.30046 ms (± 44.2273) |
||
comp_asmjit_loopExplicit |
33.6589 us (± 6.27493) |
||
comp_asmjit_gcdExplicit |
27.0775 us (± 7.78299) |
||
comp_asmjit_nestedIf10Explicit |
109.422 us (± 15.1011) |
||
comp_asmjit_chainedIf10Explicit |
122.285 us (± 15.8086) |
||
comp_asmjit_chainedIf100Explicit |
1.70584 ms (± 36.1135) |
||
exec_bc_addOne |
36.792 ns (± 5.16173) |
36.9564 ns (± 5.11982) |
1.00 |
exec_mlir_addOne |
330.969 ns (± 9.37774) |
282.96 ns (± 7.86376) |
1.17 |
exec_cpp_addOne |
4.16843 ns (± 0.552334) |
3.98915 ns (± 0.469858) |
1.04 |
exec_interpreted_addOne |
39.7826 ns (± 2.00027) |
39.6342 ns (± 2.2298) |
1.00 |
ssa_add |
167.937 ns (± 9.05868) |
189.998 ns (± 20.8108) |
0.88 |
ssa_ifThenElse |
412.325 ns (± 37.6405) |
498.381 ns (± 36.2383) |
0.83 |
ssa_deeplyNestedIfElse |
1.06388 us (± 82.9091) |
1.2078 us (± 82.3775) |
0.88 |
ssa_loop |
434.723 ns (± 35.7133) |
536.005 ns (± 34.4901) |
0.81 |
ssa_ifInsideLoop |
830.006 ns (± 89.8546) |
949.222 ns (± 72.7998) |
0.87 |
ssa_loopDirectCall |
445.222 ns (± 39.6345) |
545.93 ns (± 41.961) |
0.82 |
ssa_pointerLoop |
517.911 ns (± 39.4371) |
635.604 ns (± 42.4872) |
0.81 |
ssa_staticLoop |
378.382 ns (± 22.0845) |
511.315 ns (± 49.3922) |
0.74 |
ssa_fibonacci |
451.289 ns (± 31.8331) |
565.925 ns (± 55.8251) |
0.80 |
ssa_gcd |
411.792 ns (± 35.2522) |
498.53 ns (± 44.5426) |
0.83 |
ssa_loopExplicit |
532.876 ns (± 32.4148) |
||
ssa_gcdExplicit |
386.708 ns (± 20.9109) |
||
ssa_nestedIf10Explicit |
3.76737 us (± 341.651) |
||
ssa_chainedIf10Explicit |
4.22435 us (± 402.09) |
||
ssa_chainedIf100Explicit |
204.274 us (± 8.42827) |
||
trace_add |
2.35909 us (± 245.593) |
2.34496 us (± 232.961) |
1.01 |
completing_trace_add |
2.35162 us (± 225.474) |
2.44207 us (± 308.893) |
0.96 |
trace_ifThenElse |
8.54694 us (± 1.68798) |
9.02627 us (± 1.38021) |
0.95 |
completing_trace_ifThenElse |
4.60148 us (± 649.739) |
4.71302 us (± 618.171) |
0.98 |
trace_deeplyNestedIfElse |
25.6286 us (± 4.12392) |
26.141 us (± 3.38505) |
0.98 |
completing_trace_deeplyNestedIfElse |
12.7623 us (± 2.05046) |
13.1745 us (± 1.88355) |
0.97 |
trace_loop |
8.39181 us (± 1.59209) |
8.63063 us (± 1.1372) |
0.97 |
completing_trace_loop |
4.65878 us (± 708.294) |
4.67873 us (± 394.54) |
1.00 |
trace_ifInsideLoop |
16.355 us (± 4.03469) |
16.9711 us (± 2.80042) |
0.96 |
completing_trace_ifInsideLoop |
8.45783 us (± 1.41354) |
8.5433 us (± 1.04142) |
0.99 |
trace_loopDirectCall |
8.41977 us (± 1.68297) |
8.80748 us (± 968.435) |
0.96 |
completing_trace_loopDirectCall |
5.0492 us (± 1.11871) |
6.22222 us (± 2.08068) |
0.81 |
trace_pointerLoop |
13.9689 us (± 3.09042) |
14.2787 us (± 2.09311) |
0.98 |
completing_trace_pointerLoop |
10.3055 us (± 1.94132) |
10.4844 us (± 1.6894) |
0.98 |
trace_staticLoop |
7.42103 us (± 937.293) |
7.36281 us (± 790.484) |
1.01 |
completing_trace_staticLoop |
7.44825 us (± 786.271) |
7.34517 us (± 621.764) |
1.01 |
trace_fibonacci |
9.78045 us (± 1.57944) |
10.1657 us (± 1.81837) |
0.96 |
completing_trace_fibonacci |
6.05948 us (± 790.3) |
6.22191 us (± 551.559) |
0.97 |
trace_gcd |
7.69087 us (± 1.1662) |
8.12246 us (± 1.16253) |
0.95 |
completing_trace_gcd |
3.92763 us (± 500.51) |
4.14409 us (± 578.076) |
0.95 |
trace_nestedIf10 |
40.3017 us (± 8.38889) |
40.2189 us (± 5.18296) |
1.00 |
completing_trace_nestedIf10 |
40.918 us (± 8.514) |
38.9548 us (± 4.13451) |
1.05 |
trace_nestedIf100 |
1.53557 ms (± 36.1092) |
1.38063 ms (± 47.1346) |
1.11 |
completing_trace_nestedIf100 |
1.53453 ms (± 37.2206) |
1.39143 ms (± 27.2891) |
1.10 |
trace_chainedIf10 |
96.5139 us (± 11.5154) |
99.3271 us (± 8.40838) |
0.97 |
completing_trace_chainedIf10 |
52.5404 us (± 10.7873) |
51.5743 us (± 8.42379) |
1.02 |
trace_chainedIf100 |
4.39674 ms (± 117.672) |
4.43705 ms (± 51.9703) |
0.99 |
completing_trace_chainedIf100 |
2.43518 ms (± 51.537) |
2.31297 ms (± 41.3848) |
1.05 |
trace_loopExplicit |
7.66991 us (± 1.06937) |
||
completing_trace_loopExplicit |
7.71279 us (± 1.2076) |
||
trace_gcdExplicit |
3.51343 us (± 448.338) |
||
completing_trace_gcdExplicit |
3.59924 us (± 458.481) |
||
trace_nestedIf10Explicit |
44.184 us (± 10.2211) |
||
completing_trace_nestedIf10Explicit |
44.4474 us (± 10.9558) |
||
trace_chainedIf10Explicit |
21.0972 us (± 3.84807) |
||
e2e_tiered_bc_to_mlir |
49.3137 us (± 20.8453) |
55.2826 us (± 11.295) |
0.89 |
e2e_single_mlir |
6.71334 ms (± 273.131) |
5.51468 ms (± 173.212) |
1.22 |
tiered_compile_addOne |
49.1076 us (± 20.5234) |
54.1272 us (± 7.73487) |
0.91 |
single_compile_mlir_addOne |
3.79132 ms (± 192.456) |
3.22997 ms (± 116.608) |
1.17 |
single_compile_cpp_addOne |
29.774 ms (± 525.658) |
24.7532 ms (± 459.823) |
1.20 |
single_compile_bc_addOne |
50.6676 us (± 20.1719) |
56.0543 us (± 12.3518) |
0.90 |
tiered_compile_sumLoop |
64.931 us (± 21.858) |
77.2887 us (± 16.7111) |
0.84 |
single_compile_mlir_sumLoop |
6.00627 ms (± 185.022) |
5.19219 ms (± 79.0478) |
1.16 |
single_compile_cpp_sumLoop |
30.6289 ms (± 475.137) |
25.472 ms (± 447.056) |
1.20 |
single_compile_bc_sumLoop |
66.2084 us (± 22.8259) |
76.3645 us (± 15.2721) |
0.87 |
This comment was automatically generated by workflow using github-action-benchmark.
Add a lightweight, opt-in public API (nautilus/control_flow.hpp) that lets users state control flow explicitly via closure-style If/While/For constructs instead of native C++ if/for over val<bool>. Native control flow is traced by exception-based symbolic execution, which re-runs the whole function once per path (O(2^N) in branch count). The explicit constructs emit the CMP/JMP blocks directly in a single trace pass and never enqueue a symbolic path, so a function built entirely from them traces in exactly one iteration regardless of branch count. Loops emit a real cyclic CFG (header with a back-edge), so loop-carried values are reconciled by the existing SSA phase just like the implicit path. Implementation: - ExecutionTrace gains emitCmpNoRecord/emitJmp/createMergeBlock: tag-free block emission that never touches the global tag map or symbolic executor. - TracingInterface gains explicit-CF primitives, implemented once on the shared TraceContextBase so both exception-based and lazy tracing support them. - control_flow.hpp orchestrates the primitives in the tracing path and runs natively in the interpreter (non-tracing) path, mirroring select.hpp. Mixing explicit constructs with implicit native control flow in the same traced function is detected (a second trace iteration) and rejected with a clear error; express all of a function's control flow with explicit constructs instead. Tests cover correctness across the interpreter, every backend, and both trace modes (nested ifs, while, gcd, nested/counted loops, if-in-loop, general for), plus the mixing-rejection guard. Tracing benchmark gains explicit twins of the path-explosion kernels: chainedIf100 traces ~230x faster explicitly and is flat in branch count. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SQ9LFon2N8u2RXdLmj1Z47
The tracing benchmark's chainedIfExplicitN generated its explicit ifs with a plain host for-loop, so the body operations (result = result + 1) all sat on the same source line. Their snapshots aliased across iterations, triggering a spurious control-flow merge that corrupted the trace and crashed SSACreationPhase::getReturnBlock on an empty return set (the pre-existing multi-return bug the SSA benchmark already skips chainedIf*/nestedIf* for). Use a static_val<int32_t> loop counter so each unrolled iteration gets a distinct snapshot hash, exactly as static_iterable loops do. The explicit control-flow feature itself was never affected: the execution tests (spelled-out If/While/For) pass on every backend including MLIR and both trace modes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SQ9LFon2N8u2RXdLmj1Z47
Add explicitLoopWithNativeIf / explicitWhileWithNativeIf kernels: an explicit For/While loop whose body contains a native (implicit) if. The native branch enqueues a symbolic path, so the function re-runs and the explicit loop's emitters re-enter on the second iteration, where the mixing guard detects getIterations() > 1 and throws. Asserted via REQUIRE_THROWS across every backend and both trace modes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SQ9LFon2N8u2RXdLmj1Z47
Add larger explicit twins (chainedIf50/250/500Explicit) and the implicit chainedIf500 to a tracing-only benchmark vector, so the front-end scaling curve is chartable at 10/100/500 (implicit) and 10/50/100/250/500 (explicit). They are kept out of the shared `tests` vector so the SSA/IR/backend-compilation benchmarks don't run the 500-branch kernels. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SQ9LFon2N8u2RXdLmj1Z47
Add a "Explicit Control-flow Trace Test" case that runs the explicit If/While/For kernels through the full trace pipeline (tracing, after_ssa, ssa_verify, ir, after_constant_folding, after_empty_block_elim) under both tracing contexts, and check the dumps against committed golden files under test/data/explicit-control-flow-tests/. The goldens confirm the explicit constructs lower to canonical CFG/SSA: branches produce a ControlFlowMerge block with the merged value as a block argument, and loops produce a header with a body back-edge and loop-carried values threaded as block arguments -- identical in shape to the implicit path, and stable across both ExceptionBasedTraceContext and LazyTraceContext. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SQ9LFon2N8u2RXdLmj1Z47
PhilippGrulich
force-pushed
the
claude/nautilus-explicit-controlflow-api-41irz5
branch
from
June 18, 2026 20:49
a7b0ab8 to
d025719
Compare
…nd tests Round of improvements to the explicit control-flow API: - Break()/Continue(): early loop exit for explicit For/While. Tracing emits a jump to the loop's exit (Break) or continue target (Continue) via a per-trace loop-frame stack, then isolates trailing dead-path ops in a fresh unreachable block; the interpreter path throws a signal the loop catches, so both paths agree on what runs after a break/continue. For's continue target is its step block (C for-loop semantics: continue runs the step); While's is the header. - Value-yielding If: a constrained overload returns the taken arm's val<T> (reconciled at the merge), selected when the arms return non-void. Only the taken arm is evaluated at runtime, unlike select(). - For(begin, end, step, body): custom (ascending) stride; the unit-stride form delegates to it. For loops now emit a dedicated step block directly. - If/If-else deduplicated (no-else is If-else with an empty else arm). - Mixing guard now keys on a per-trace sawImplicitBranch flag set in traceBool (both contexts), so mixing is rejected immediately and regardless of ordering (including a native if placed before an explicit construct), with no cross-function contamination. - control_flow.hpp documents the host-loop static_val requirement (body ops on a shared source line otherwise alias and corrupt the trace). Tests: Break/Continue (While/For/nested), value-If, strided For, and the implicit-before-explicit guard ordering, all across every backend (incl. MLIR) and both trace modes; plus an explicit-vs-implicit equivalence test that compiles each explicit kernel and its native twin and asserts identical results. Trace goldens regenerated and extended (14 kernels) — Break jumps to the exit block, Continue back-edges through the step block. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SQ9LFon2N8u2RXdLmj1Z47
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds a lightweight, opt-in public API —
nautilus/control_flow.hpp— that lets users state control flow explicitly via closure-style constructs instead of native C++if/foroverval<bool>:It plugs into the existing engine/module — no engine API change; the constructs work inside any lambda passed to
engine.registerFunction(...).Why
Native control flow is traced by exception-based symbolic execution, which re-runs the whole function once per path —
O(2^N)in branch count. The explicit constructs emit theCMP/JMPblocks directly in a single trace pass and never enqueue a symbolic path, so a function built entirely from them traces in exactly one iteration regardless of branch count.Tracing-benchmark twins make the win concrete (Debug, cpp/bc/asmjit):
chainedIf10chainedIf1005.8 ms (230×)The explicit version is flat in branch count (chainedIf10Explicit ≈ chainedIf100Explicit), while the implicit one explodes.
How
ExecutionTracegainsemitCmpNoRecord/emitJmp/createMergeBlock— tag-free block emission that never touches the global tag map or the symbolic executor (so it can never triggerprocessControlFlowMerge).TracingInterfacegains explicit-CF primitives (emitExplicitCmp,openMergeBlock,switchToBlock,currentBlock,jumpTo), implemented once on the sharedTraceContextBaseso both exception-based and lazy tracing support them.control_flow.hpporchestrates the primitives in the tracing path and runs natively in the interpreter (non-tracing) path, mirroringselect.hpp. Loops emit a real cyclic CFG (header + back-edge), so loop-carried values are reconciled by the existing SSA phase exactly like the implicit path.Forlowers toWhile.Mixing
Explicit constructs and implicit native control flow must not be mixed in the same traced function: a native branch triggers symbolic re-execution that would re-enter and double-emit the explicit constructs. This is detected (a second trace iteration) and rejected with a clear
RuntimeExceptionrather than miscompiling. Express all of a function's control flow with explicit constructs — they compose freely and arbitrarily nest among themselves. (A fully transparent mix would require single-pass replay against the symbolic re-execution machinery; deferred.)Tests
ExplicitControlFlowTest.cppcovers correctness across the interpreter, every backend, and both trace modes — if-then, if-then-else, nested if, chained if, conditional-abs, counted/while/gcd/nested loops, if-in-loop, general three-clause for — plus the mixing-rejection guard. (363 assertions; full suite green with no regressions.) Because the explicit primitives throw on a second trace iteration, every passing explicit-only test is also a proof of single-pass tracing.🤖 Generated with Claude Code
https://claude.ai/code/session_01SQ9LFon2N8u2RXdLmj1Z47
Generated by Claude Code