-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathverified-codegen-roadmap.yaml
More file actions
4230 lines (4087 loc) · 278 KB
/
Copy pathverified-codegen-roadmap.yaml
File metadata and controls
4230 lines (4087 loc) · 278 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Verified Codegen Roadmap (ASPICE SWE.1 / SWE.2 — forward-looking program)
#
# System: Synth -- WebAssembly-to-ARM/RISC-V AOT compiler
#
# WHY THIS EXISTS
# The oracle-gated cleanup audit keeps reaching the same conclusion: the
# recurring greedy-fixes (reciprocal-mult cost-gates, the i64/temp
# register-exhaustion HARD-FAIL, and the "selector missed an op" bug class
# #223/#226/#232) are SYMPTOMS of two single-pass, hand-written components —
# the instruction selector and the register allocator. Each fix is locally
# correct and oracle-gated, but they accrete because the substrate forces them.
# A compiler-landscape research pass (Cranelift ISLE/Crocus, regalloc2/aegraph,
# CompCert/CakeML, Sail, WasmCert-Coq, equality saturation) identified the
# best-in-class structural cure. This file PLANS that cure as typed, phased,
# parallelizable requirements so the infrastructure can be built incrementally
# alongside the per-issue gale cadence WITHOUT a big-bang rewrite.
#
# All items are `proposed` — this is the backlog, not shipped work. Each carries
# a falsifiable kill-criterion (PulseEngine methodology). Behavior is frozen:
# every phase must keep the differential fixtures (control_step 0x00210A55,
# flat+inlined flight_algo 0x07FDF307, divseam 338/338) bit-identical and is
# gated by the existing oracle before merge.
#
# Format: rivet generic-yaml
artifacts:
# ---------------------------------------------------------------------------
# Program north-star
# ---------------------------------------------------------------------------
- id: VCR-001
type: system-req
title: Foundationally-verified, allocator-robust codegen core
description: >
Synth's code generator shall evolve from a hand-written single-pass
selector + single-pass allocator into principled, foundationally-verified
infrastructure, so correctness comes from construction rather than from an
accreting set of locally-correct patches. The end state: (a) instruction
selection is expressed as verified rewrite rules discharged against
synth's Rocq integer/control-flow core, not open-coded match arms;
(b) register allocation spills under pressure instead of hard-failing,
eliminating the exhaustion that forces cost-gated fallbacks; (c) the
target and source semantics the proofs rest on are the authoritative ISA
(Sail) and Wasm (WasmCert) models; (d) the differential oracle is
coverage-complete and theorem-linked. Migration is incremental and
oracle-gated: every step keeps the frozen fixtures bit-identical.
status: proposed
tags: [codegen, verification, roadmap, north-star, real-compiler]
links:
- type: derives-from
target: BR-001
- type: derives-from
target: BR-002
fields:
req-type: non-functional
priority: should
verification-criteria: >
Over the program's lifetime: zero NEW cost-gate/fallback shortcuts
added to the selector; the register-exhaustion hard-fail path is
removed; the "selector missed an op" gale bug rate trends to zero; all
frozen fixtures stay bit-identical at every step.
rationale: >
Antidote to the patch-accretion the cleanup audit repeatedly surfaces.
Sources: Cranelift ISLE + Crocus (ASPLOS'24 SMT rule verification),
regalloc2/aegraph, CompCert/CakeML (foundational lowering), Sail RISC-V
(official Rocq export), WasmCert-Coq (FM'21).
# ---------------------------------------------------------------------------
# Track A — codegen core (sequential within: allocator first, then selector)
# ---------------------------------------------------------------------------
# PROGRESS (substrate, side-by-side / unwired):
# - reg_effect + per-segment dead-store/redundant-const analysis
# (synth-synthesis/src/liveness.rs)
# - CFG-aware liveness: cfg_liveness() builds the label-form basic-block CFG
# and solves per-block live-in/live-out via the backward dataflow fixpoint
# (the interference-graph substrate). Pure analysis, zero codegen callers,
# so every frozen fixture stays bit-identical by construction. Leaf-only,
# label-form scope; declines (None) on calls/offset/computed branches.
# - Interference graph: interference_graph() derives per-instruction liveness
# from cfg_liveness (backward from each block's live_out) and builds the
# undirected "cannot share a physical register" graph — defs interfere with
# everything live-after + with co-defs (Umull rdlo/rdhi). This is the
# colouring input. Still unwired. Exposes interferes/neighbors/degree.
# - Graph colouring: color_graph(graph, k) -> Colored(map) | Spilled(set) via
# Chaitin/Briggs simplify (push degree<k) + optimistic spill + select (lowest
# free colour; no colour => actual spill). THE allocation decision the
# single-pass allocator can't make (it hard-fails instead). Verified on real
# selector output: colourable within the R0-R8 pool (k=9). Still unwired.
# - Precolouring: color_graph_precolored(graph, k, pins) pins nodes to fixed
# colours (never simplified/spilled, only constrain neighbours) — the
# mechanism for synth's reserved regs (R9/R10/R11/R12) + ABI arg pins.
# color_graph() now delegates with empty pins. Still unwired.
# - Allocation verifier (wiring plan step 1): verify_allocation(graph, assign)
# -> Ok | Err(SameColor|Unassigned). The self-check the wired allocator runs
# on its own output before emitting (independent of color_graph => real
# cross-check / defense-in-depth). Verified to accept color_graph's own
# output on real codegen. Still unwired. Wiring plan:
# docs/design/vcr-ra-allocator-wiring.md.
# - Spill-cost ranking (wiring step 2): color_graph_precolored_costed picks
# the optimistic spill candidate by Chaitin's cost/degree (cost = def+use
# occurrence count via use_def_counts) instead of degree-only; the uncosted
# API delegates with all-costs-equal, reproducing the historic choice
# exactly. allocate_function now ranks by real counts. Pure, unwired,
# fixtures bit-identical (verified by cmp + full differential).
# - Range interference + colouring (step 3a, part 1): range_interference
# builds the adjacency over VALUE RANGES (vreg ids; dies-at-birth boundary
# deliberately non-interfering, co-defs always interfere) and color_ranges
# k-colours it via the chaitin_core now generic over node id (Reg for the
# physical graph, usize for vregs — type-parameter refactor, identical
# algorithm/tie-breaks). The spurious-physical-spill elimination is now
# unit-proven in miniature (r0 reused for 3 values -> 3 independent ranges
# -> colours at true pressure 4). Pure, unwired, fixtures bit-identical.
# - Spill-on-exhaustion retry (step 3b-lite, LANDED): the i32 register-
# exhaustion hard-fail ("all allocatable registers are live on the stack",
# alloc_temp_safe) is now recoverable — the backend retries the failed
# function once with spill_on_exhaustion, which always reserves the i64
# spill area and makes alloc_temp_or_spill spill the deepest stack value
# (reusing the #171 StackVal::Spilled reload-on-pop machinery) instead of
# failing. Bit-identity is STRUCTURAL: only functions whose first
# (unmodified) pass already failed ever reach the retry. Honest bound:
# the hard-fail shrinks, doesn't vanish — I64_SPILL_SLOTS=8 pool
# exhaustion, the i64 consecutive-pair site, and the call-result
# callee-saved site keep their Err. Oracle: high_pressure_i32.wat
# (fails on pre-3b main, compiles + unicorn-vs-wasmtime PASS now) +
# 3 frozen fixtures sha256-identical.
# Remaining for VCR-RA-001 (CONSEQUENTIAL, changes emitted bytes, full
# differential gate): rename_def + apply_range_coloring (3a part 2) ->
# flag-gated re-allocation pass + measured flip -> full 3b virtual temps ->
# spill-code insertion -> wire-in + per-function flip where the
# differential proves no-regression -> delete a hard-fail site.
- id: VCR-RA-001
type: sw-req
title: SSA-based register allocator with spilling (remove the hard-fail)
description: >
Replace the single-pass allocator (fixed R0-R8 pool, hard-fail on
exhaustion at instruction_selector.rs:330/356/577/4667) with an
SSA/liveness-based allocator that spills under pressure (regalloc2-class:
live ranges, spill/reload, coalescing). This is the FIRST track item
because the exhaustion hard-fail is what forces the reciprocal-mult
cost-gate (v0.11.20) and the 2-temp UMULL path; removing it lets those
shortcuts be reverted toward the clean form. Must keep the reserved-reg
architecture (R9 globals / R11 mem-base / R12 IP-scratch) and the
bounds-mode R10 invariant.
LOCAL-PROMOTION SCOPING SPIKE (2026-06-24, #390, scripts/repro/
local_promotion_headroom.py): the structural gap to native parity is that the
SELECTOR lowers every wasm local to a frame slot (`compute_local_layout` →
LocalGet/Set/Tee emit `ldr/str [sp,#off]`), so locals never reach this
allocator. Measured cost+pressure on the frozen fixtures: flight_algo 22% of
instructions are local sp-traffic (25/109) from just 4 DISTINCT slots;
control_step 2 slots, filter_step 3, controller_step 1. Distinct slots (1–4)
are far below the allocatable pool (R0–R8 minus result/scratch ≈ 6–7), so on
these fixtures EVERY local fits in a register with NO SPILLING — the spill
path (the historically bug-prone part, #193→#226) is never exercised; the
built `color_graph`/`color_ranges` colour a ≤4-node graph trivially. CONCLUSION:
selector-side local promotion is HIGH headroom (≈22% removable on the hot fn)
+ LOW risk (no-spill at this pressure), and is the right next parity lever —
more tractable than a general allocator rewrite. The spike script asserts the
no-spill scope and fails loudly if a fixture ever exceeds the pool (then the
spill path + its risk return).
LOCAL-PROMOTION v1 (2026-06-24, #390, PR #458): the spike's lever, LANDED.
Keep eligible non-param i32 locals in callee-saved registers (r4..r8) instead
of frame slots. Behind SYNTH_LOCAL_PROMOTE (default off ⇒ frame path
bit-identical, frozen gates green). Reuses the #193 param-reservation
machinery (seed local_to_reg before the param_regs snapshot) rather than a
new allocator. Scope v1: i32 only, write-before-read only (read-before-write
= #457), dominance via control-flow depth-0, cost-gated, budget r4..r8 with
frame overflow, and LEAF-ONLY (declines functions with calls — the
post-selection range-reallocator can remap a callee-saved home to a
caller-saved reg that a bl would clobber; lift once VCR-RA-003 is shown to
reject that remap). Validated: flag-on local_promote_i32 fixture (7 live
locals → 2 overflow, dirty r4..r8/frame sentinels) clean==dirty==wasmtime;
control_step 0x00210A55 + flight_seam 0x07FDF307 preserved; flight_seam
sp-traffic 39→5, .text 902→798 B. ARM-only (RV32 untouched). The default-on
flip is a separate gated step (re-freeze + G474RE silicon), like cmp→select.
LEAF-ONLY LIFT — REALLOCATOR-SAFETY PROBE (2026-06-24, #390, fast-follow #1):
the empirical question the leaf-only gate was set on — can the post-selection
range-reallocator move a promoted local's callee-saved home to a CALLER-saved
register that an intervening `bl` would clobber? RESOLVED: NO, by construction.
reallocate_function FLUSHES its segment at every non-straight-line op
(liveness.rs:2341), and `Bl` has reg_effect→None, so a call SPLITS segments;
the pre-call segment's live-out (the cross-call value) is its last-opened
range per register ⇒ PINNED to its original register (liveness.rs:2371). The
reallocator never renames across a call. Pinned by the unit test
`reallocate_function_preserves_callee_saved_value_across_call` (constructs the
exact promotion+call shape under interior pressure; r5 stays r5, not remapped
to a caller-saved reg). This is the specific mechanism the v1 advisor flagged;
it is safe. HONEST REMAINING GAP before lifting the gate: this validates the
reallocator pass in isolation; the LIFT still needs an end-to-end WITH-CALL
differential (flag-on execution oracle over the full selector-seed +
preserve_caller_saved + realloc pipeline, like local_promote_i32) before the
gate comes off — flag-off stays bit-identical, but flag-on for call-containing
functions is new codegen wiring = a SEPARATE gated step, not an idle-tick
change. This pass landed only the frozen-safe probe + finding.
LEAF-ONLY LIFT — END-TO-END GAP CLOSED EMPIRICALLY (2026-06-24, #390,
fast-follow #1 cont.): the with-call fixture scripts/repro/
local_promote_cross_call.wat (exported `cross_call` holds an i32 local `acc`
live across a `bl` to `$helper`; acc satisfies every v1 promotion predicate
except leaf-only). With leaf-only ON it compiles byte-IDENTICAL flag-on vs
flag-off (promotion declines — the gate holds end-to-end, frozen-safe). Under
a THROWAWAY leaf-only lift (experiment reverted, NOT committed), promotion
runs on the call fn and objdump confirms the FULL pipeline is correct: acc is
promoted to r4 (`mov r4,r3`), survives the `bl func_0` by AAPCS, and is read
back from r4 AFTER the call (`adds r6,r4,r0`) — NOT remapped to a caller-saved
reg, exactly as the #459 reallocator-isolation proof predicted. So the
end-to-end with-call promotion is correctness-ready. REMAINING to lift the
gate (the byte-changing gated step, NOT this tick): make the committed lift PR
(a) remove the leaf-only early-return, (b) add a with-call EXECUTION
differential (resolve the internal BL, run flag-on vs wasmtime) as the gate,
(c) re-freeze + G474RE silicon. This pass landed the frozen-safe fixture +
finding; flag-off bit-identical, no codegen change.
STACK-FWD + FRAME-SLOT-DCE FLIP TO DEFAULT-ON (#242 feature loop): the two
paired lowering passes — `forward_stack_reloads` (a `local.set; local.get`
reload `ldr rY,[sp,#N]` whose value still lives in `rX` becomes `mov rY,rX`)
and `eliminate_dead_frame_stores` (the now-dead `str rX,[sp,#N]` whose slot is
overwritten-before-read is removed) — shipped flag-off in PR #514/#515 and are
now DEFAULT-ON. Escape hatch `SYNTH_NO_STACK_FWD=1` restores the frame-resident
bytes. Gated exactly like the cmp→select (v0.13.0) and local-promotion
(v0.14.0) flips: RESULTS re-verified bit-identical on every frozen anchor
(control_step 0x00210A55 13/13; flat AND inlined flight_algo 0x07FDF307 —
flight_seam_differential.py MATCH both) while .text shrank on the SHIPPED
--relocatable path (flight_seam 774→738, flight_seam_flat 910→878; control_step
unchanged — no spurious slot reuse; signed_div_const + all RV32 unchanged,
ARM-only). Frozen goldens re-frozen + an escape-hatch gate asserts the opt-out
restores the pre-flip bytes. SOUNDNESS: overwrite-only DCE (dead only when a
later store to the same immediate slot overwrites it with no intervening read;
reaching the function end does NOT count), sub-word sp accesses (ldrb/ldrh/…)
as blockers (advisor-caught #483-class hole, test verified failing pre-fix); 8
unit tests pin the boundaries. Instruction/memory proxy: flight_algo sp-traffic
20→7, 139→135 insns — the measured CYCLE number is gale's G474RE (post-ship
confirmation, the cmp→select silicon-gate-waiver precedent). Broad oracle:
`cargo test --workspace` green under the new default (wast/spec suite is
compile-only, so gale silicon is the broad-execution check). NOT flipped:
const-CSE (SYNTH_CONST_CSE) stays flag-off — its alias-eviction prerequisite is
open and it is inert on flat_flight. NO-REGRESSION FIX (#242, 2026-06-26): gale's
v0.17.0 burndown found const-CSE GREW a `--relocatable` `gust_mix` 90→92 B (the
post-hoc `apply_const_cse` retargeted a use, kept a constant resident longer, and
defeated a downstream immediate-fold). Fixed by running `apply_const_cse` LAST in
the pass pipeline (after every immediate-fold, so foldable consts are already
gone) plus a per-segment SIZE GUARD (commit a segment's removals/retargets only
when they do not grow its estimated bytes — e.g. a retarget flipping a 16-bit
`ldr` to 32-bit is declined). Guard decline path proven non-vacuously by two
contrasting liveness unit tests; per-function no-regression gate added to
`const_cse_differential.py`; flag-off byte-identical (frozen gate green). The
pressure/size prerequisite for the eventual default-on flip is now CLOSED;
alias-eviction remains the sole open prerequisite. (Not empirically reproduced on
gale's exact `gust_mix` — fixture requested to pin the trigger.)
status: verified
release: v0.24.0
tags: [codegen, register-allocation, ssa, regalloc2, track-a, release-v0.11.40]
links:
- type: derives-from
target: VCR-001
# Bootstrap-verification wiring: the SSA allocator is the structural
# satisfaction of the two STPA code-generation constraints that the
# single-pass allocator violates by construction (H-CODE-1).
- type: constraint-satisfies
target: SC-CODE-1
- type: constraint-satisfies
target: SC-CODE-2
fields:
req-type: functional
priority: should
verification-criteria: >
control_step + flight_algo + a high-pressure i64 module compile with no
hard-fail; all frozen fixtures bit-identical; cycles equal-or-better;
the reciprocal-mult cost-gate becomes revert-able with no regression.
- id: VCR-SEL-001
type: sw-req
title: Rocq-discharged verified instruction-selection DSL
description: >
Express lowering as a rule DSL (`lower(op) = [target seq]`) whose
well-formed rules emit Rocq proof obligations auto-closed by the existing
synth_binop_proof / synth_comparison_proof / synth_unop_proof tactic
family — "ISLE with a proof-assistant backend": foundational discharge,
not Crocus-style per-rule SMT, and unlike CompCert it is a reusable rule
DSL rather than monolithic hand-proofs. Replaces the open-coded selector
match arms incrementally (one op-class at a time, behind the oracle). This
structurally kills the "selector missed an op" bug class (#223/#226/#232):
a missing rule is a coverage gap the DSL can enumerate, not a silent
miscompile. Depends on VCR-RA-001 landing first (a verified rule that
can't be allocated is not yet correct end-to-end).
FIRST MEASUREMENT (2026-06-20, coq/Synth/Synth/VcrSelPilot.v, built green
via //coq:vcr_sel_pilot, 7 Qed / 0 Admitted): the go/abandon question is
whether the existing synth_binop_proof tactic family still auto-discharges
once a rule is lifted from fixed R0/R1 to universally-quantified registers.
Two-tier result. (A) NO-SCRATCH single-instruction i32 binops
(Add/Sub/Mul/And/Or/Xor): 6/6 auto-discharge UNCHANGED — synth_binop_proof_poly
is verbatim synth_binop_proof modulo the lowering-unfold target; one Qed per
op covers every aliasing (universal over rd/rn/rm). (B) SCRATCH-USING
multi-instruction op (Rotl = RSB scratch + ROR): generalizes too but NOT for
free — needs an explicit rs<>rn scratch-non-aliasing side condition (the
fixed-register proof's `discriminate` on R0<>R2 becomes a real hypothesis
the DSL must carry and feed the allocator) plus a per-shape proof. CONCLUSION:
DSL viable; auto-discharge "for free" is ~100% only for the no-scratch class,
and the DSL must model scratch non-aliasing — exactly why SEL depends on
RA-001. NOT YET MET: the >=70%-per-attempted-rule over the FULL integer
pilot (div/rem trap-control-flow tail unmeasured), and the bit-for-bit
selector match (this is a discharge measurement, not a wired DSL — generates
no Rust, replaces no selector arms). Also: the lemmas carry but do not USE
the wasm hypotheses (pre-existing T1 property — they prove "ARM computes the
named result", not "ARM refines the WASM op").
CONTROL-FLOW OP CLASS GATED ON VCR-ISA-001: the measurement is complete for
the straight-line classes (no-scratch binops generalize free; scratch ops
need the rs<>rn non-aliasing constraint). The TRAP-GUARDED ops (i32 div/rem)
are NOT a register-generalization question at all — their fixed-register
proofs are already Admitted (coq/STATUS.md), blocked upstream by the flat
exec_program's no-op BCondOffset (see VCR-ISA-001). So div/rem SEL coverage
depends on VCR-ISA-001's real-control-flow executor, not on this track.
MOTIVATION-1 RE-SCOPED — "selector missed an op" silent-drop is already
structurally closed (measured 2026-06-20). The original framing ("a missing
rule is a silent miscompile the DSL enumerates away") is now only partly a
DSL job, because the existing selectors no longer silent-drop:
- ARM select_default (instruction_selector.rs:1799→4189) is an EXHAUSTIVE
match over WasmOp with NO `_ =>` wildcard (the SIMD and
MemoryCopy|MemoryFill arms `return Err`). A newly-added WasmOp variant is
a COMPILE error, not a silent NOP — exhaustiveness checking IS coverage
enumeration for ARM. select_with_stack's `_ =>` (10057) delegates to
select_default, so every op is Ok-or-typed-Err: no drop path.
- RISC-V selector.rs:911 `other => Err(SelectorError::Unsupported(op))` is a
RUNTIME typed loud-skip — kills silent WRONG-CODE but, unlike ARM, is not
compile-checked, so a new op silently DIVERGES (loud-skipped on RV32,
lowered on ARM) rather than failing the build.
- Clean split of the cited #223/#226/#232 class: the REGALLOC subset (#226
alloc_temp live-value clobber) is owned by VCR-RA-001 and already landed;
the OP-GAP subset (#223/#232 "RV32 rejected ops ARM handles") is NOT a
silent drop — it is a CROSS-BACKEND PARITY gap (ARM lowers X; RV32
loud-declines X) that no oracle covers today and gale, not a gate, caught.
So VCR-SEL-001's remaining value is NOT silent-drop prevention (the compiler
+ typed-Err terminals own that). It is: (i) correctness-by-construction of the
lowering rules; (ii) collapsing the two/three-selector accretion
(select_default / select_with_stack / optimizer_bridge) into one verified
source; and (iii) cross-backend op-parity — the concrete near-term increment,
tracked as VCR-SEL-005 below.
FIRST INCREMENT SCOPE (2026-07-03, docs/design/vcr-sel-001-first-increment.md):
(1) OP FAMILIES: exactly the pilot's tier-A no-scratch single-instruction
i32 ALU class — Add/Sub/Mul/And/Or/Xor, the measured 6/6 auto-discharge
set — plus, as the increment's stretch (proves the DSL can carry side
conditions), ONE scratch-using shape (i32.rotl: RSB scratch + ROR) with
the explicit rs<>rn non-aliasing hypothesis. Excluded from increment 1:
trap-guarded div/rem (Admitted upstream, gated on VCR-ISA-001's real
control-flow executor), i64 pairs, memory, control flow, and everything
the optimized path (select_with_stack/optimizer_bridge) does — the DSL
targets select_default's arms only.
(2) COEXISTENCE: the DSL is a checked-in rule table
(declarative op -> parameterized ARM sequence) + a generator emitting
plain Rust lowering fns committed to the tree (reviewable, no build-time
magic); select_default KEEPS dispatch ownership — a migrated op's match
arm delegates to the generated rule behind SYNTH_SEL_DSL (default off),
unmigrated arms untouched. OFF ≡ baseline byte-identical by construction.
(3) ROCQ OBLIGATION PER RULE: one universally-quantified T1 theorem per
rule in the VcrSelPilot.v form — forall rd rn rm (+ scratch hypotheses
where the shape needs them), exec of the rule's emitted sequence yields
the op's I32 semantics in rd — auto-discharged by synth_binop_proof_poly;
rule<->theorem naming is 1:1 and a coverage check fails the //coq build
if a rule lacks its Qed (a rule without a theorem cannot merge).
(4) FROZEN-ANCHOR GATE for the flip: per-op mirror-pinning first (the
#511/#513 pattern — a test lowers the probe set through BOTH the
hand-written arm and the generated rule and asserts byte-equality, so the
two must-agree implementations are pinned before any flip); then
SYNTH_SEL_DSL=1 must keep control_step 0x00210A55, flat+inlined
flight_algo 0x07FDF307, and divseam bit-identical (the generated rules
reproduce the current lowering EXACTLY — increment 1 migrates structure,
never bytes); only then may the flag default flip, one op-class per
release, full differential green. Success criterion for increment 1: the
six ALU arms + rotl are served from the DSL with their seven Qed theorems,
fixtures bit-identical, and deleting a hand-written arm body becomes a
no-op — the first structural (not additive) step of the migration.
INCREMENT 1 LANDED (2026-07-07, flag-off): the checked-in rule table
(crates/synth-synthesis/src/sel_dsl/mod.rs RULES, 7 rules: tier-A six +
rotl with SideCondition::NotAlias(Rs, Rn)) + generator emitting committed
plain-Rust lowerings (sel_dsl/generated.rs, pinned rustfmt-stable by
generated_lowering_is_up_to_date); select_default's seven arms delegate
behind SYNTH_SEL_DSL (default OFF ⇒ original hand-written body,
byte-identical by construction). Rocq: coq/Synth/Synth/VcrSelRules.v —
7 Qed / 0 Admitted, 1:1 rule<->theorem naming, tier-A discharged by
synth_binop_proof_poly verbatim, rotl by the pilot's stepped proof with
the rs<>rn hypothesis. Coverage gate wired: //coq:verify_proofs is now a
test_suite = rocq_proofs + vcr_sel_rules_coverage (manifest
coq/vcr_sel_rules.manifest pinned to RULES by a cargo test; missing
theorem or non-Qed proof fails the //coq gate CI runs). Gate 1 evidence:
per-op mirror-pin
(sel_dsl_mirror_pin_generated_rules_match_handwritten_arms_242) asserts
hand-written ≡ generated ArmOp sequences for all 7 rules; frozen
byte-gate green with the flag OFF (by construction) AND with
SYNTH_SEL_DSL=1.
INCREMENT 2 LANDED (2026-07-08, flag-off,
docs/design/vcr-sel-001-increment-2.md): +14 rules = 21 total, each with
its 1:1 Qed (21/21, 0 Admitted, 0 holdout theorems). (a) i32 register
shifts shl/shr_s/shr_u + rotr — measured TIER-A (single instruction, NO
scratch; the inc-1 side-condition machinery turned out unneeded for
shifts, remains exercised by rotl alone), discharged by the same
synth_binop_proof_poly; delegated in BOTH selectors (Delegation::Both),
byte-identical. (b) The ten i32 comparisons — the CMP+SetCond shape,
modeled per the Compilation.v convention as CMP; MOV rd #0; MOVcc rd #1;
NO aliasing side conditions (CMP latches NZCV before rd is written, so
the universal quantifier admits every aliasing); 7/10 discharged by
synth_cmp_binop_proof_poly (synth_cmp_binop_proof generalized to
universally-quantified registers) + the same three manual scripts
(ne/lt_s/lt_u) as their CorrectnessI32.v ancestors, register-generalized
verbatim. DELEGATION HONESTY: comparisons delegate in
select_with_stack's reg-reg arm (the load-bearing CMP+SetCond emission,
byte-identical by construction; the #258 imm-fold peephole stays
hand-written on both flag settings, pinned) because select_default's
comparison arms are a blind bare-Cmp lowering that never materializes
the 0/1 result — production-unreachable AND unprovable as T1
result-correspondence, so they stay hand-written (the increment's
documented holdout; this choice also keeps the flip strictly
byte-invisible). Mirror-pins:
sel_dsl_mirror_pin_generated_rules_match_handwritten_arms_242 (the 11
select_default-delegated rules) +
sel_dsl_mirror_pin_select_with_stack_rules_byte_identical_242 (the 14
select_with_stack-delegated rules, with a non-vacuity window check that
the delegation actually fired) +
sel_dsl_cmp_imm_fold_path_stays_handwritten_and_byte_identical_242.
INCREMENT 3 LANDED (2026-07-08, flag-off,
docs/design/vcr-sel-001-increment-3.md): +6 rules = 27 total, each with
its 1:1 Qed (27/27, 0 Admitted, 0 holdouts among attempted rules). The
i64 register-pair family — add/sub/and/or/xor (two-instruction pair
shapes ADDS+ADC / SUBS+SBC / ANDx2 / ORRx2 / EORx2 over SIX register
variables rd_lo..rm_hi) + eqz (single I64SetCondZ). Chosen deliberately:
the #615/#632/#633/#643/#599 miscompile cluster is all hand-written i64
paths, zero bugs in DSL-covered ops. MODEL FINDING: the Rocq flat model
needed ZERO extension for pairs — ADDS/ADC carry coupling +
combine_i32/lo_of_i64/hi_of_i64 have carried the pair convention since
v0.8.0; the increment is the pilot's fixed-to-universal register lift
applied to CorrectnessI64.v's ancestors. SIDE-CONDITION DESIGN (the
#632 lesson made structural): each pair rule carries THREE explicit
aliasing hypotheses (rd_hi<>rd_lo, rd_lo<>rn_hi, rd_lo<>rm_hi — the
low-half write must not clobber the high-half operands still to be
read, and the high write must not destroy the low result), runtime
Ok-or-Err in the generated Rust; in-place rd=rn reuse (select_default's
fixed R0:R1 op= R2:R3) is deliberately admitted, so one Qed covers both
selectors. Theorems are pair-result T1: BOTH words proven
(rd_lo=lo_of_i64, rd_hi=hi_of_i64 post-conditions), discharged by
synth_i64_carry_pair_proof_poly (via the existing i64_add_via_adds_adc
/ i64_sub_via_subs_sbc, no new axiom) and
synth_i64_bitwise_pair_proof_poly (combine lemmas imported from
CorrectnessI64, not duplicated); eqz via i64_setcondz_bits_spec.
Delegation::Both — select_default fixed-register arms + the
select_with_stack allocated-pair arms (alloc_consecutive_pair's
extra_avoid satisfies the side conditions by construction). Mirror-pins:
the select_default loop now covers 17 rules; the dedicated
sel_dsl_mirror_pin_i64_pair_rules_select_with_stack_242 probes i64-typed
stacks with the RMW-vacuity window check;
i64_pair_side_conditions_are_enforced pins all three Errs per rule.
FLIP LANDED (default-on): with all 40 rules mirror-pinned
byte-identical, the SYNTH_SEL_DSL default-on flip shipped — the DSL is
now the SHIPPED lowering path for its 40 covered ops (opt-out
SYNTH_NO_SEL_DSL=1, CI-gated to the hand-written path). The flip was
byte-invisible by construction: the frozen 10/10 anchors did not move
(frozen_codegen_bytes), the mirror-pins stayed green, and the broad
default-vs-opt-out differential was bit-identical across all 88
compilable scripts/repro fixtures — on the default (optimized), the
--relocatable (select_with_stack, the path the DSL rules ship on) AND
the --no-optimize paths — including flight_seam(_flat), controller_step,
filter_axis and signed_div_const; the 2 flag-gated fixtures
(gust_mix_686, spill_on_exhaust_242) fail identically on both paths.
Status stays `implemented` (NOT `verified`): the flip
clears exactly the default-flip item; still NOT done (hence not
verified) — i64 mul/div/shifts/rotates/comparisons (I64SetCond) +
optimizer_bridge + the remaining select_with_stack families; div/rem
(VCR-ISA-001-gated).
status: implemented
tags: [codegen, selector, isle, verified-dsl, rocq, track-a, novel, release-v0.12.1]
links:
- type: derives-from
target: VCR-001
- type: constrained-by
target: VCR-RA-001
- type: constrained-by
target: VCR-ISA-001
fields:
req-type: functional
priority: should
verification-criteria: >
A pilot op-class (e.g. i32 arithmetic) is fully expressed as DSL rules
with >=70% auto-discharged by existing tactics (kill-criterion: below
70% it collapses into "CompCert with extra syntax" — abandon); the
generated lowering matches the current selector bit-for-bit on all
fixtures. CALIBRATION (Crocus, ASPLOS'24 deep-research 2026-06-10):
the SMT state of the art hit ~88% per-attempted-rule on the Wasm 1.0
integer subset but needed 136-182 manual annotations (~1000 LOC),
custom VCs for 6 rules, timeouts on wide mul/div/rem/popcnt, and its
verified rules covered only 19.8% of dynamically-invoked rules — so
measure the criterion PER-ATTEMPTED-RULE on the integer pilot, budget
for annotations, and do NOT promise full-selector coverage. A rule DSL
is an enabler, not a prerequisite: CompCert verifies selection
functions directly, and synth's existing T1 tactic suite
(synth_binop_proof family vs exec_indexed) IS that direct path — the
DSL pays off only when rule count amortizes its cost.
- id: VCR-SEL-005
type: sw-req
title: Cross-backend op-parity oracle (ARM / RISC-V / AArch64)
description: >
A test-time gate (crates/synth-backend-riscv/tests/cross_backend_op_parity.rs)
that lowers self-contained, minimally-valid WasmOp sequences on ALL THREE
shipping selectors — ARM (Thumb-2), RISC-V (RV32IMAC) and AArch64 (A64) —
and asserts integer-core PARITY against ARM as the reference: an op the ARM
selector lowers must be lowered-or-explicitly-ledgered by the RV32 selector
(`cross_backend_integer_op_parity_242`) and by the aarch64 selector
(`aarch64_integer_op_parity_851`). NOTE the backend count: this description
said "BOTH the ARM and RISC-V selectors" until v0.55 (#893) — it has spanned
three backends since v0.53 (#883). This is the concrete near-term increment
of VCR-SEL-001: the
VCR-SEL-001 pilot measurement (2026-06-20) established that "selector missed
an op" is no longer a silent miscompile (ARM select_default is an exhaustive
WasmOp match; RV32 ends dispatch with a typed Unsupported Err), so the
residual #223/#232 risk is purely cross-backend DIVERGENCE — ARM lowers op
X, RV32 loud-declines it, and the function silently compiles on one target
and is skipped on the other. Historically that gap was found only by gale on
qemu; this oracle surfaces it as a gate.
The ledger (KNOWN_DIVERGENCES) is bidirectional: a NEW divergence not in the
ledger fails the build (a fresh op-gap, owned here not on silicon), and a
ledgered divergence that has CLOSED also fails (a stale entry the fix must
delete) — so a parity claim cannot outlive the gap it documents, and a
landed RV32 lowering is forced to retire its ledger line.
FIRST MEASUREMENT (2026-06-20, first run): the oracle found FIVE
ARM-lowers / RV32-declines integer op-gaps, all the Zbb bit-manipulation
class — i32.rotl, i32.rotr, i32.clz, i32.ctz, i32.popcnt. RV32IMAC and
rv32imc (incl. ESP32-C3) do not include Zbb, so there is no single native
instruction; ARM lowers them via sequences (ROR/CLZ/RBIT, and a software
sequence for popcount which ARMv7-M also lacks natively), proving they are
sequence-lowerable on RV32 too (as Select already is). Recorded as tracked
deferrals under this requirement. NOTE the i64 bit-manip analogues
(i64.clz/i64.popcnt) came back at PARITY — not flagged. SCOPE: integer core
only; float (VFP) and SIMD (Helium) parity is a separate, large, known gap
and is intentionally NOT asserted by this oracle.
FOLLOW-UP (the work this oracle drives): implement the five RV32 sequence
lowerings, each differential-validated against wasmtime per the established
bug-loop discipline; landing each one retires its ledger entry via the
stale-entry check.
COVERAGE WIDENING (2026-06-24, frozen-safe test-only increment): the curated
set grew from ~50 to 76 probes — now NEAR-EXHAUSTIVE over the integer core:
the i32 compare set completed (gt_u/le_s/le_u/ge_s), the FULL i64 compare set
added (ne + signed+unsigned lt/gt/le/ge), i64 shr_s/rotl/rotr/ctz, all four
i64 div/rem, the sub-word i64.extend{8,16,32}_s, and i64.load/i64.store. ALL
26 new probes came back at PARITY (verified both-Ok, not vacuous both-Err) —
ZERO new divergences; the ledger stays exactly the five i32 Zbb deferrals.
Floor raised 30→65. This widens the net for the #223-class cross-backend gap
(gale would otherwise hit it on qemu) over div/rem, the full compare lattice,
and i64 memory — none of which were previously probed. ASYMMETRY FOUND:
i64.rotl/i64.rotr LOWER on RV32 (sequence-composed in the i64 path) while
i32.rotl/i32.rotr are ledgered RV32-declines — evidence the i32 Zbb rotate
deferral is closable by routing i32 rotate through the same shift+or sequence
the i64 path already uses (sharpens the FOLLOW-UP's first target; byte-changing
codegen, gated — NOT in this test-only increment).
UNIVERSE-COMPLETENESS UPGRADE (2026-07-17, #49, frozen-safe test-only): the
oracle was hardened from a CURATED integer probe list (whose vacuity trap is
that a new WasmOp variant is silently never probed) to a COMPILER-ENFORCED
universe-complete gate. Every WasmOp variant is now assigned a ParityClass by
a NO-WILDCARD match (classify()); a new op fails to COMPILE until classified
(the #615 no-wildcard tripwire). Integer-core ops carry a probe and are
asserted; float/f64/SIMD are StructurallyExcluded with a written reason —
their ARM lowering is TARGET-PARAMETERIZED (measured: f32.add DECLINES with
fpu=None, LOWERS with FPUPrecision::Single/Double; f64.add needs Double), so
asserting "ARM lowers float" at a fixed no-FPU probe would be FALSE; float/SIMD
parity stays a separately-tracked gap. Enumerating the FULL universe surfaced
SIXTEEN previously-unprobed ARM-lowers / RV32-loud-declines one-sided gaps
(each confirmed END-TO-END via `synth compile -b riscv`, "unsupported wasm op
for RV32 skeleton: …"): global.get, global.set, memory.size, memory.grow,
memory.copy, memory.fill, br_table, and the nine sub-word i64 loads/stores
(i64.load8_s/u, i64.load16_s/u, i64.load32_s/u, i64.store8/16/32 — the
full-word i64.load/i64.store DO lower on both; only the sub-word extend/
truncate variants are the gap). All sixteen are now ledgered VCR-SEL-005
RV32-selector DEFERRALS (a gated, owned fact instead of a silicon surprise) —
the KNOWN_DIVERGENCES ledger is now 5 Zbb + 16 new = 21 entries. RED-FIRST
non-vacuity is now a first-class companion test
(red_first_unledgered_one_sided_gap_is_caught): it drops the real i32.rotl
ledger entry and asserts the gate reports it as an unexpected divergence on
the REAL backends (no synthetic op, no shipping-code change). A
ledger-liveness test guards drift. The gate is now WIRED as a named CI job
(vcr-sel-005-cross-backend-op-parity-gate) so a regression is visible on its
own failing check.
FOLLOW-UP EXPANDED: the RV32 sequence-lowering backlog this oracle drives now
also covers globals, the memory intrinsics (size/grow/copy/fill), br_table,
and the sub-word i64 memory variants — each landing retires its ledger line
via the stale-entry check.
THIRD BACKEND (v0.53, #883/#851): aarch64 joined the same no-wildcard
enumeration. Before that its only breadth check was 25 hand-written probes,
so "what does aarch64 not lower?" had no mechanical answer and gap-picking
was guesswork. The aarch64 leg (`aarch64_integer_op_parity_851`, ledger
`aarch64_known_divergences`) enumerated TWENTY ARM-lowers/aarch64-declines
integer-core gaps on first run; each decline names the missing selector arm
AND the A64 instruction that would implement it, so the ledger reads as a
work plan rather than an absence. Thirteen closed in the same release
(`select` via CSEL/FCSEL, `drop`/`nop`, `i32.wrap_i64`,
`i64.extend_i32_{s,u}`, the five `extend8/16/32_s` forms, fixed-memory
`memory.size`/`memory.grow` — selector 148 -> 161 ops, gale's matrix 35 ->
45); v0.54 closed `global.get`/`global.set`. aarch64 also gets what ARM and
RV32 structurally cannot have: a probed FLOAT/SIMD surface
(`a64_extended_surface`, floor `probed >= 100`). Float is
`StructurallyExcluded` from the ARM/RV32 leg because ARM float lowering is
TARGET-parameterized (f32.add DECLINES at fpu=None, LOWERS at
Single/Double) and RV32 has no FPU — but the aarch64 backend has ONE fixed
host profile, so both directions are assertable there, and a stale "gap"
claim is caught the same way a stale divergence is.
LEDGER STATE (2026-08, keep in step with the arrays — a drifting count is
the #893 class this oracle exists to prevent): ARM<->RV32 18 entries
(5 Zbb + 16 universe-completeness − `memory.size`/`memory.grow` closed
v0.50 − `br_table` closed v0.53 #882); ARM<->aarch64 5 entries — br_table,
`local.set`+get on a PARAM local, `local.tee` on a PARAM local (two
separate entries, both the leaf-function param-homing prerequisite),
memory.copy, memory.fill — after v0.54 closed `global.get`/`global.set`.
Non-vacuity floors: `at_parity >= 65` (RV32),
`at_parity >= 60` (aarch64), `probed >= 100` (aarch64 extended surface).
Both legs carry their own red-first companion
(`red_first_unledgered_one_sided_gap_is_caught` drops the real i32.rotl
entry; `red_first_unledgered_aarch64_gap_is_caught` does the same on the
aarch64 leg) and their own ledger-liveness test.
status: implemented
tags: [codegen, selector, riscv, aarch64, parity, oracle, track-a, release-v0.11.51, release-v0.53, synth-883, synth-893]
links:
- type: derives-from
target: VCR-001
- type: traces-to
target: VCR-SEL-001
fields:
req-type: functional
priority: should
verification-criteria: >
All EIGHT tests in cross_backend_op_parity.rs are green: every WasmOp
variant is classified by a no-wildcard match (universe-complete by
construction — a new variant fails to COMPILE until classified, the #615
tripwire), and every integer-core probe is at parity with ARM on BOTH the
RV32 and the aarch64 leg OR carries a reasoned ledger entry. The
at-parity floors (>=65 RV32, >=60 aarch64 common-core ops; >=100 probed
on the aarch64 extended float/SIMD surface) guard against a construction
regression that would let "everything errors" masquerade as parity. Each
leg carries its own red-first companion proving non-vacuity on the REAL
backends — red_first_unledgered_one_sided_gap_is_caught surfaces the real
i32.rotl gap once its ledger entry is removed, and
red_first_unledgered_aarch64_gap_is_caught does the same for aarch64 — and
a ledger-liveness test per leg guards drift. Stale entries fail too, in
both directions: a divergence that has CLOSED reddens the gate until its
line is deleted, so a parity claim cannot outlive the gap it documents.
CI-wired as vcr-sel-005-cross-backend-op-parity-gate.
- id: VCR-A64-CF-001
type: sw-req
title: AArch64 multi-way dispatch (br_table) + value-carrying structured control flow
description: >
The VCR-SEL-005 third-backend enumeration (#851) is a MECHANICALLY DERIVED
complement: whatever the aarch64 selector does not lower is named, by op,
in `aarch64_known_divergences()` and in the `a64_extended_surface` gap
column. After v0.53/v0.54 closed the integer core, the floats, globals and
`call_indirect`, the TWO LARGEST remaining entries in that complement were
structural, not arithmetic:
(a) `br_table` — the multi-way branch. Declined outright ("aarch64
selector has no BrTable arm"), so any wasm switch/jump-table shape
skipped the whole function on `-b aarch64`.
(b) VALUE-CARRYING `block`/`loop`/`if` — a typed (`(result T)`) frame.
Declined because the straight-line value-stack model had no
RESULT-REGISTER RECONCILIATION: two paths reaching one `end` leave
the result in path-dependent temps. This is the #509 block-arity
class, and it is what makes `(block (result i32) … br_if …)` — the
shape every non-trivial optimizing producer emits — uncompilable.
THIS REQUIREMENT closes both, with the residual named rather than widened:
br_table lowers as a COMPARE-AND-BRANCH CHAIN, deliberately the same
construction #882 chose for RV32 so the two backends stay reviewable
against each other: `cbz w_idx, L0` for entry 0, then `cmp w_idx, #i` +
`b.eq L_i` per further entry, then an unconditional `b L_default`. The
index is compared in the W view, so the i32 UNSIGNED semantics hold
exactly: only 0..len-1 match, and every other index — including the
"negative" i32s that are huge unsigned values — falls to the default
label, per WASM core semantics. Targets may mix FORWARD (block/if end,
patched at `End`) and BACKWARD (loop header, resolved eagerly to a
negative offset) destinations in one table, since the chain dispatches on
the TARGET FRAME's kind exactly like `br`/`br_if` already do.
Value-carrying frames get a RESERVED RECONCILIATION REGISTER PAIR (one GP,
one FP — the file is not known from the arity side-table, which carries
counts only) allocated at frame entry and withheld from the temp
allocator for the frame's whole extent. Every path that reaches the
frame's label MOVES its result into that register before transferring:
`br` and `br_if` at the branch, the then-arm at `else`, the fall-through
at `end`. At `end` the frame pushes the reserved register as the frame's
value, so the result is in ONE register on every path by construction —
the reconciliation the old decline said was missing.
SOUNDNESS-CRITICAL DISTINCTION (named because getting it wrong is a silent
miscompile, not a decline): a `br` to a `loop` targets the loop HEADER and
carries the loop's PARAMETER count, NOT its result count. So a
`loop (result i32)` — arity (0,1) — must NOT reconcile on its back-edge;
its reserved register is written only by the fall-through `end`. The frame
therefore carries `label_arity` (= params for Loop, results for Block/If)
SEPARATELY from `result_arity`, and branch reconciliation is driven off
`label_arity`. Reconciling the back-edge would deposit a garbage value
into the result register on every iteration.
HONEST RESIDUALS (loud declines with machine reasons, each pinned by name
in `br_table_subshape_asymmetry_882` so the gap claim cannot outlive the
gap, and each MOVED not deleted):
* `br_table` past BR_TABLE_MAX_TARGETS (16) — the chain is O(n); a real
jump table (PC-relative dispatch + bounds check) is the follow-up.
Same threshold and same decline as RV32 #882.
* `br_table` with VALUE-CARRYING targets — a per-path result move would
have to sit on the taken edge of each compare, which the flat chain
has no room for. Conservatively refused when any targeted frame
(default included) has non-zero label arity or was entered at a
different height than the post-pop height.
* block/loop/if with PARAMETERS, or with MORE THAN ONE result
(multi-value) — the reserved pair is a single slot.
Frozen-safe by construction: a VOID frame reserves nothing and emits no
reconciliation move, so all pre-existing aarch64 control-flow output is
byte-identical.
status: implemented
tags: [codegen, selector, aarch64, control-flow, parity, track-a, issue-851]
links:
- type: derives-from
target: VCR-001
- type: traces-to
target: VCR-SEL-005
fields:
req-type: functional
priority: should
verification-criteria: >
(1) The aarch64 leg of the op-parity gate no longer ledgers `br_table`
as an ARM-lowers/aarch64-declines divergence, and the stale-entry check
proves it (the gate reddens if the line survives the lowering).
(2) `br_table_subshape_asymmetry_882` asserts the THREE aarch64
residuals by name in both directions: a 17-target table declines, a
value-carrying table declines, and the probed in-scope shape LOWERS.
(3) Execution-differential vs wasmtime under unicorn (and natively on an
arm64 host): br_table default arm, index at the table bound, index over
the bound, index 0xFFFFFFFF, a table mixing loop and block targets, and
a value-carrying loop's back-edge — plus a trap case, so the gate cannot
collapse to "everything returns a value".
(4) The pre-existing void control-flow differentials stay green with
UNCHANGED output (no spurious reconciliation move on a void frame).
# ---------------------------------------------------------------------------
# Track B — authoritative semantics (independent; parallel with Track A)
# ---------------------------------------------------------------------------
- id: VCR-ISA-001
type: sw-req
title: Re-base ARM/RISC-V semantics on Sail-generated Rocq
description: >
Replace the hand-written ARM (ArmSemantics.v) and RISC-V execution models
with definitions imported from the official Sail ISA models (the RISC-V
Sail model exports to Rocq today; ARM-ARM is machine-readable ASL),
restricted to the RV32IMAC / Thumb-2 slice synth targets, and re-prove the
integer core against them. Collapses a major TCB component: "verified
against the authoritative spec" becomes a defensible claim. Independent of
Track A — can proceed in parallel.
GATES THE TRAP-GUARD ADMITS (found via the VCR-SEL-001 pilot, 2026-06-20):
the current hand-written ArmSemantics.v exec_program is a FLAT sequential
executor with no program counter — BCondOffset is a literal no-op
(ArmSemantics.v ~715, "No-op in sequential model"). So every trap-guarded
lowering (CMP; BCondOffset; UDF; <op> — i32 div/rem, the 4 i32_div*/rem*
admits + their i64 analogues, the bulk of coq/STATUS.md's 9 admits) CANNOT
be discharged: the model can't express "skip the UDF when the divisor is
non-zero". A real-control-flow executor (PC-indexed, which a Sail-derived
model supplies natively) is the precondition. This also gates VCR-SEL-001
coverage of the control-flow op class (div/rem) — straight-line and
scratch-using ops generalize without it, control-flow ops do not.
FEASIBILITY SPIKE DONE (2026-07-08, docs/design/vcr-isa-001-spike.md):
the 2026-06-10 gate is answered. Tool: VERIFIED alive (Sail 0.20 via
nixpkgs ships --coq, bbv|stdpp styles; coq-sail maintained). Generated
ARM model IMPORT: REFUTED as a build path — sail-arm arm-v9.4-a covers
all of T32/A32 (instrs32.sail) but its Coq snapshot is a single 42 MB
armv9.v pinned to stdpp-unstable + git-pinned coq-sail-stdpp (the 2019
v8.5 snapshot needed ~40 GB RAM on Coq 8.9.1) — incompatible with the
hermetic Rocq 9 Bazel toolchain and CI. WORKING ALTERNATIVE LANDED:
per-instruction transcribe-and-bridge — coq/Synth/ARM/SailArmBridge.v
(in //coq:verify_proofs, 23 Qed / 0 Admitted) hand-transcribes
AddWithCarry + the AArch32 ADD/CMP register-form execute clauses from
sail-arm @ 1bf2e5574ba9 with file:line provenance, and proves
ArmSemantics.v's ADD/ADDS/CMP ≡ the transcription on registers AND all
four NZCV flags (the six hand-written compute_*_flag definitions equal
ASL's AddWithCarry outputs — the foundation under every CMP-based
comparison lowering). Abstraction gaps documented in-file: shift_n=0,
d<>15 (no PC — the executor gap above is NOT closed by the bridge and
stays this item's core prerequisite), no IT blocks, bits(32) as Z mod
2^32. Measured cost: flag machinery proven once (~230 lines incl.
reusable mod-case/testbit/NOT helpers + blast tactics); remaining
AddWithCarry family (SUB/SUBS/CMN/RSB) near-free, ~0.5-1 day per
remaining straight-line op class; memory ops widest gap (byte-addressed
Sail memory vs Z->I32 model). RISC-V half unmeasured — sail-riscv's
maintained Coq export may admit the import path there; re-evaluate
separately.
ROUND 2 LANDED (2026-07-08, same day): the spike's priority items (1)
and (2) + the moves stretch — SailArmBridge.v now 81 Qed / 0 Admitted /
0 new axioms covering the FULL AddWithCarry family (ADD/ADDS/CMP +
SUB/SUBS/CMN/RSB + ADC/SBC with live-C carry_in — the i64 pair-codegen
primitives), flag-free ALU (AND/ORR/EOR/MVN), all four shifts in
immediate (1..31) and register (0 < amt < 32; amt = 0 on the unsigned
view) forms via a faithful Shift_C transcription incl. shifter
carry-out, and MOV/MOVW/MOVT. Measured cost ~0.5 day for six-plus
classes (well under the spike's per-class estimate; the flag-machinery
amortization held). BONUS FINDING: the bridge surfaced two latent
hand-model divergences from the ASL (LSR/ASR #32 and register shift
amounts >= 32 — unreachable from Compilation.v since WASM masks to
0..31), documented in-file as gaps 6-7 — the exact bug class this item
exists to surface. Still open (why not implemented): PC-indexed
executor (the trap-guard admits, this item's core motivation),
MUL/UMULL/CLZ/RBIT/memory/branches, and the whole RISC-V half.
status: approved
tags: [semantics, sail, isa, trust, rocq, track-b, unblocks-admits]
links:
- type: derives-from
target: VCR-001
fields:
req-type: non-functional
priority: could
verification-criteria: >
The i32 Tier-1 theorems re-discharge against the Sail-generated Rocq
target model for the supported instruction subset; if the Sail state
monad makes the proofs intractable to port, fall back to using Sail
emulators as a differential oracle (kill-criterion / partial credit).
GATE ADDED (deep-research 2026-06-10): substrate freshness is
UNVERIFIED — no claim about 2025-26 Sail-Rocq export maturity for the
Thumb-2/RV32IMAC integer slice survived adversarial verification. A
bounded feasibility SPIKE (can the Sail RV32 Rocq export build + one
instruction's semantics be related to ArmSemantics/RiscvSemantics?)
must precede any release commitment.
GATE ANSWERED for the ARM half (spike 2026-07-08, evidence
docs/design/vcr-isa-001-spike.md + coq/Synth/ARM/SailArmBridge.v):
one instruction family bridged end-to-end with Qed proofs; criterion
for the item REVISED accordingly — re-discharge is per-instruction
transcribe-and-bridge against provenance-pinned Sail source, not
against an imported generated model. RV32 export build remains
unmeasured (the spike's ARM no-go on import does not transfer).
STATUS proposed -> approved (2026-07-08): two landed increments
(23 then 81 Qed, //coq:verify_proofs green) validate the revised
criterion's method on the i32 straight-line integer core; approved
means the approach is committed, NOT that the item is implemented —
the PC-executor refactor, memory ops, and the RISC-V half remain.
- id: VCR-WASM-001
type: sw-req
title: Anchor source semantics on WasmCert-Coq
description: >
Map synth's hand-written WasmSemantics.v stack machine onto WasmCert-Coq
(the mechanized Wasm spec, FM'21) as the authoritative source semantics,
and prove the frontend decode/validate refines it — giving the
verified-source -> verified-lowering -> verified-target triangle (with
Sail at the bottom via VCR-ISA-001). The Component Model layer stays an
open problem (no mechanized semantics yet) and is explicitly out of scope
for this item. Independent of Track A.
status: implemented
tags: [semantics, wasmcert, source, rocq, track-b]
links:
- type: derives-from
target: VCR-001
fields:
req-type: non-functional
priority: could
verification-criteria: >
synth's core-Wasm reduction relates to WasmCert-Coq's for the
integer/control-flow fragment; kill-criterion: if the semantic gap is
too large to relate without a major proof, scope to that fragment only.
GATE ADDED (deep-research 2026-06-10): WasmCert-Coq maintenance state
is UNVERIFIED (no surviving claim) — same bounded feasibility spike
required before release commitment.
BOUNDED FIRST INCREMENT LANDED (v0.45, VCR-WASM-001). The WASM-side
analogue of SailArmBridge.v (VCR-ISA-001): coq/Synth/WASM/
WasmCertReference.v transcribes WasmCert-Coq's i32.add operational rule
(= CompCert Int.add, repr(unsigned x + unsigned y)) into synth's I32
primitives with line-level provenance; WasmCertBridge.v proves (2 Qed,
real — not reflexivity) that synth's exec_wasm_instr I32Add refines
that INDEPENDENT reference. Non-vacuous: the reference is phrased only
in repr/unsigned/Z, never I32.add/exec_wasm_instr, so a wrong add
semantics fails the proof; the raw-vs-normalized (Zplus_mod) gap is
genuinely discharged. SCOPE: 1 op, establishing the pattern.
FOLLOW-UP (status stays implemented, not verified, until then): a real
external WasmCert-Coq/CompCert coq dependency into the hermetic
bazel/nix toolchain (the transcription is the bounded scaffold), plus
the full integer/control-flow fragment.
PHASE 2 (v0.47): real-dependency FEASIBILITY SPIKE result — nix-level
FEASIBLE, bazel-level DEFERRED on three named blockers. Evidence:
coqPackages.wasmcert (coq9.0-wasm-2.2.0) EXISTS in the exact nixpkgs
commit the rules_rocq_rust toolchain pins (88d3861a) and nix-builds
GREEN against the same Rocq 9.0.1 (verified 2026-07-17), propagating
coq-ext-lib 0.13.0, mathcomp-boot 2.5.0, parseque 0.3.0, flocq 4.2.1,
compcert 3.16 — the phase-1 "drags mathcomp+CompCert, no-go" wording
is obsolete. Blockers for wiring it into bazel: (1) rules_rocq_rust
has NO generic extra-coq-package hook — coqutil/hammer/smpl are
hard-coded attrs across extensions.bzl + toolchain.bzl + rocq.bzl, so
a fork change (or a large MODULE.bazel patch) is required; (2)
LICENSE: nixpkgs wasmcert 2.2.0 depends on CompCert 3.16, license
inria-compcert (meta.license.free = false) — an unfree dep in the
default CI proof toolchain needs an explicit allowUnfree opt-in and a
project policy decision; (3) upstream WasmCert-Coq 2.2.1 (master,
coq >= 9.0 & < 9.2) DROPS the coq-compcert dependency entirely, so
waiting for the next nixpkgs wasmcert bump removes blocker (2) for
free. Verdict: land the ruleset extra-package hook + take the dep when
wasmcert >= 2.2.1 reaches nixpkgs. Meanwhile phase 2 batch-extends the
transcription bridge: the full i32 arithmetic/bitwise/shift + eqz +
compare family (19 ops), each transcribed from the PINNED
coq9.0-wasm-2.2.0 sources (numerics.v / operations.v line-level
provenance, CompCert 3.16 lib/Integers.v for the underlying ops) with
a real-Qed refinement lemma per op in WasmCertBridge.v.
PHASE 3 (v0.48, #242): the extra-coq-package HOOK is LANDED — blocker
(1) is CLOSED; the dep stays PENDING on blockers (2)+(3). Delivered as
patches/rules_rocq_rust_extra_coq_pkg.patch (applied via the existing
MODULE.bazel git_override patch list): a new
`rocq.extra_coq_package(name=, attribute_path=)` bzlmod tag that emits
one nixpkgs_package per declaration (exposing `:vo` compiled artifacts +
`:sources`), letting a module pull an arbitrary coqPackages.<attr> from
the pinned nixpkgs into the hermetic Rocq toolchain WITHOUT forking the
ruleset — the generic mechanism the coqutil/hammer/smpl hard-coded attrs
lacked. The hook is INERT as shipped: NO extra_coq_package tag is
declared, zero repos are emitted, //coq:verify_proofs stays GREEN and
the 536 Qed are untouched. The REAL WasmCert-Coq dep is NOT wired,
because it is refused by CI's no-unfree-dependency policy: re-verified
2026-07-17 by nix eval against pin 88d3861a — coqPackages.wasmcert is
version 2.2.0 (meta.license MIT, free) but PROPAGATES coqPackages.
compcert 3.16 (meta.license inria-compcert, meta.license.free = false,
meta.unfree = true), and wasmcert 2.2.0's Int32 IS CompCert's
Integers.Make, so compcert cannot be dropped from the 2.2.0 build graph.
wasmcert >= 2.2.1 (which removes the CompCert dep) is NOT yet in the
pin. REMAINING WORK to retire the transcription (all mechanical once a
pin ships wasmcert >= 2.2.1): (a) bump the nixpkgs pin in
rules_rocq_rust to a rev carrying wasmcert >= 2.2.1 — removes blockers
(2)+(3); (b) uncomment the `rocq.extra_coq_package(name="rocq_wasmcert",
attribute_path="coqPackages.wasmcert")` tag + `use_repo` in MODULE.bazel;
(c) swap WasmCertBridge.v's `Require Import Synth.WASM.WasmCertReference`
for a `Require` of the real WasmCert numerics/operations modules and
delete WasmCertReference.v. Until (a) lands upstream, the transcription
stays — the "trusted transcription" caveat is NOT yet retired.
PHASE 3 TRANSCRIPTION BATCH (#242): while the real dep stays blocked, the
hand transcription is EXTENDED to the i64 integer family (22 ops:
add/sub/mul/and/or/xor/shl/shr_u/shr_s/rotl/rotr/eqz/eq/ne/lt_u/lt_s/
gt_u/gt_s/le_u/le_s/ge_u/ge_s), from the SAME pinned coq9.0-wasm-2.2.0
sources — arithmetic/bitwise/shift/compare cite the same generic Make
functor lines as i32 (read at wordsize 64, Int64 at numerics.v:1038-1039);
the two rotates newly cite CompCert 3.16 Integers.v:217-222 (rol/ror).
Each op has a real-Qed op-level refinement lemma; the 16 ops WIRED in
exec_wasm_instr (shifts + rotates + 11 comparisons (incl. eqz)) also get an
executor-level Qed. NAMED RESIDUAL (not forced): i64 add/sub/mul/and/or/xor
have op-level refinement ONLY — exec_wasm_instr returns None for those
constructors (unwired in the model), so no executor-level lemma is
claimed; wiring them is a semantics-model change, out of scope for a
transcription batch. Rocq Qed total 536 -> 585 (WasmCertBridge.v 49 -> 98);
the rotate proofs discharge a genuine n=0 boundary gap (synth re-normalizes
the [64-n] sub-shift count while the reference's is raw, reconciled via
lor-idempotence at the boundary). Still a hand transcription — the caveat
stands.
# ---------------------------------------------------------------------------
# Track C — validation methodology (can start now; feeds Tracks A and B)
# ---------------------------------------------------------------------------
- id: VCR-ORACLE-001
type: sw-req
title: Coverage-guided, theorem-linked differential oracle
description: >
Evolve the frozen-fixture differential into a coverage-complete,
proof-aware suite: (1) coverage-guided fixture generation over wasm inputs
(libFuzzer/AFL-style) instead of hand-picked vectors; (2) MC/DC-style
coverage on the selector/lowering rules; (3) prioritize fixtures on ops
NOT covered by a Tier-1 theorem, and re-frame the suite as the empirical
witness that the hand-written ARM/RISC-V model matches silicon (validating
the proof's assumptions). Can start immediately and feeds every other
track.
FIRST SLICE LANDED (2026-06-23): the frozen-fixture BYTE gate
(`crates/synth-cli/tests/frozen_codegen_bytes.rs`) — compiles each frozen
fixture flag-off and asserts a locked SHA-256 of its `.text`, on BOTH
backends: ARM (control_step / flight_seam / flight_seam_flat /
signed_div_const) and RV32 (control_step / signed_div_const — the rv32imac
skeleton's import-call gap excludes flight_seam; scoped to fixtures with a
`_riscv_differential.py`). The RISC-V backend is an INDEPENDENT codegen path
with its own miscompile history (#220/#223/#226), so the ARM gate gave it
zero protection. Promotes the
previously OUT-OF-CI "stay bit-identical" invariant (asserted only by the
`scripts/repro/*_differential.py` unicorn scripts, never run by cargo CI nor