@asmlift/core 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +22 -16
  2. package/package.json +1 -1
  3. package/src/backend/c.ts +1 -0
  4. package/src/backend/cfamily.ts +270 -171
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +243 -39
  8. package/src/declare.ts +41 -4
  9. package/src/frontend/mips.ts +11 -0
  10. package/src/frontend/ppc.ts +43 -7
  11. package/src/frontend/ssa.ts +404 -29
  12. package/src/frontend/thumb.ts +2176 -686
  13. package/src/ir/alias.ts +78 -0
  14. package/src/ir/bits.ts +75 -0
  15. package/src/ir/core.ts +345 -2
  16. package/src/ir/opcodes.ts +176 -21
  17. package/src/ir/parse.ts +19 -2
  18. package/src/ir/print.ts +27 -2
  19. package/src/ir/simplify.ts +190 -3
  20. package/src/ir/struct-names.ts +42 -0
  21. package/src/ir/verify.ts +43 -49
  22. package/src/l3/address.ts +62 -0
  23. package/src/l3/advance.ts +373 -0
  24. package/src/l3/argbase.ts +6 -5
  25. package/src/l3/ast.ts +510 -59
  26. package/src/l3/basecse.ts +686 -78
  27. package/src/l3/coalesce.ts +432 -46
  28. package/src/l3/dce.ts +31 -9
  29. package/src/l3/gates.ts +96 -1
  30. package/src/l3/hoist.ts +293 -14
  31. package/src/l3/homesplit.ts +285 -0
  32. package/src/l3/initfirst.ts +301 -0
  33. package/src/l3/inlinebase.ts +193 -0
  34. package/src/l3/mentions.ts +176 -0
  35. package/src/l3/mulfirst.ts +42 -0
  36. package/src/l3/nearbase.ts +152 -0
  37. package/src/l3/offmember.ts +371 -0
  38. package/src/l3/parkfirst.ts +96 -0
  39. package/src/l3/pollguard.ts +154 -0
  40. package/src/l3/ptrfield.ts +227 -0
  41. package/src/l3/regspell.ts +114 -89
  42. package/src/l3/reindex.ts +722 -80
  43. package/src/l3/scopebase.ts +649 -220
  44. package/src/l3/sinkinit.ts +40 -0
  45. package/src/l3/slotorder.ts +123 -0
  46. package/src/l3/storage.ts +48 -0
  47. package/src/l3/symbol-refs.ts +41 -8
  48. package/src/l3/tailmerge.ts +16 -1
  49. package/src/l3/typing.ts +198 -9
  50. package/src/l3/unmerge.ts +687 -0
  51. package/src/l3/unreduce.ts +971 -0
  52. package/src/l3/volatileptr.ts +207 -0
  53. package/src/l3/volatileval.ts +130 -0
  54. package/src/l3/volstore.ts +229 -0
  55. package/src/l3/zerosub.ts +62 -0
  56. package/src/pattern/engine.ts +239 -16
  57. package/src/pipeline.ts +173 -60
  58. package/src/proto.ts +112 -14
  59. package/src/raise/arrays.ts +6 -1
  60. package/src/raise/const.ts +203 -3
  61. package/src/raise/divpow2.ts +4 -4
  62. package/src/raise/extscale.ts +342 -0
  63. package/src/raise/globalshape.ts +1058 -0
  64. package/src/raise/gvn.ts +33 -18
  65. package/src/raise/latch.ts +126 -0
  66. package/src/raise/magicdiv.ts +2 -2
  67. package/src/raise/memberarrays.ts +594 -0
  68. package/src/raise/narrow.ts +124 -0
  69. package/src/raise/narrowlocal.ts +572 -0
  70. package/src/raise/paramwidth.ts +201 -0
  71. package/src/raise/pre-recovery.ts +169 -21
  72. package/src/raise/recover.ts +56 -23
  73. package/src/raise/retsink.ts +585 -19
  74. package/src/raise/shortcircuit.ts +1050 -89
  75. package/src/raise/struct-arrays.ts +19 -2
  76. package/src/raise/structs.ts +34 -4
  77. package/src/raise/tailsink.ts +126 -0
  78. package/src/rank-declare.ts +256 -0
  79. package/src/rank-variations.ts +760 -0
  80. package/src/rank.ts +2122 -326
  81. package/src/structure/analysis.ts +1398 -150
  82. package/src/structure/bitfields.ts +432 -0
  83. package/src/structure/globalaccess.ts +300 -0
  84. package/src/structure/hazards.ts +411 -20
  85. package/src/structure/loops.ts +2 -49
  86. package/src/structure/namecoalesce.ts +454 -0
  87. package/src/structure/structure.ts +3979 -612
  88. package/src/structure/switch-recover.ts +710 -145
  89. package/src/symbols.ts +188 -6
  90. package/src/target.ts +495 -32
  91. package/src/trace.ts +112 -33
  92. package/src/variation-definitions.ts +1540 -0
  93. package/src/variation-gates.ts +89 -0
  94. package/src/variation-tokens.ts +355 -0
@@ -3,95 +3,41 @@
3
3
  // here, `pasType` in the Pascal backend). The empirical fact grounding the sharing: a
4
4
  // CodeWarrior member function's BODY is byte-identical to the same C with `this` as an
5
5
  // explicit pointer — so the C++ backend reuses this body spelling VERBATIM and owns only its
6
- // DIVERGENT surface (the mangled/scoped signature, references, `this`). c.ts and cpp.ts
7
- // consume exactly the exported seam: `emitCFamily` + `cType` + `LeafHook`.
6
+ // DIVERGENT surface (the mangled/scoped signature, references, `this`).
7
+ //
8
+ // THE SEAM, stated as what it means rather than as a list that rots: a C-family backend owns its
9
+ // SIGNATURE LINE and — for C++, whose member access spells differently — a leaf hook. Everything
10
+ // below the signature is this file's: declarations, statements, expressions, precedence, the
11
+ // legalizing casts, and the recovered-struct declaration spelling (which is why that lives here
12
+ // too, shared with the scoring layer's synthesized declarations so the two cannot drift).
8
13
  import { IrType, T, scalarTypeForAccess, typeToString } from '../ir/types';
9
14
  import { BinOp, Expr, SFn, Stmt, dotBase } from '../l3/ast';
10
- import { type VarTypes, declaredTypes, derefStrideOk, exprCType } from '../l3/typing';
11
-
12
- /**
13
- * The C SIGNEDNESS a rendered integer expression actually has — `true`/`false`, or `undefined`
14
- * when it is not determinable here. The deliberate complement to l3/typing's `exprCType`, which is
15
- * pointer-ness-accurate and reports every integer as `s32` by contract; this models the two C
16
- * rules that contract omits, integer PROMOTION and the usual arithmetic CONVERSIONS.
17
- *
18
- * It lives HERE, in the C-family backend, because it is a model of C's own rules with no meaning
19
- * for another language — the same reason the cast it feeds is synthesized here rather than in the
20
- * tower. `exprCType` stays in l3/ because Pascal consults it too.
21
- *
22
- * It exists for one question, and the question is byte-load-bearing: C spells both `>>>` and `>>`
23
- * as `>>` and chooses between them from the left operand's type. A logical shift rendered over a
24
- * signed expression recompiles to `asr` where the target has `lsr`, and evaluates to a different
25
- * value. The C-family backend casts the operand whenever this returns anything but the signedness
26
- * the operator needs, so `undefined` is the safe answer in every case the model does not cover — a
27
- * redundant cast is codegen-identical, a missing one is a miscompile.
28
- *
29
- * Anything narrower than 32 bits promotes to `int` and is therefore SIGNED, whatever it was
30
- * declared. Pointers, calls and markers are `undefined`.
31
- */
32
- function renderedIntSignedness(e: Expr, varType: VarTypes): boolean | undefined {
33
- const rec = (x: Expr): boolean | undefined => renderedIntSignedness(x, varType);
34
- // an lvalue-ish leaf: its C type is a declaration / an explicit cast / a carried access width
35
- const promoted = (t: IrType | undefined): boolean | undefined =>
36
- t?.kind !== 'int' ? undefined : t.width < 32 ? true : t.width === 32 ? t.signed : undefined;
37
- switch (e.k) {
38
- case 'var':
39
- case 'cast':
40
- case 'index':
41
- case 'field':
42
- return promoted(exprCType(e, varType));
43
- // A decimal literal is `int` when it fits in one; C89 gives a larger one an unsigned type,
44
- // which is not the same operand — so it is left undetermined rather than assumed. INT_MIN is
45
- // in that larger class despite fitting: the backend prints it as `-2147483648`, which C lexes
46
- // as unary minus applied to `2147483648` — a constant too big for `int`, hence unsigned long.
47
- case 'const':
48
- return e.value > -2147483648 && e.value <= 2147483647 ? true : undefined;
49
- // `-x` / `~x` carry the PROMOTED type of the operand; `!x` is `int`.
50
- case 'un':
51
- return e.op === '!' ? true : rec(e.e);
52
- case 'bin': {
53
- // Shifts take the type of the LEFT operand alone — the right is promoted independently.
54
- if (e.op === '<<' || e.op === '>>' || e.op === '>>>') {
55
- return rec(e.l);
56
- }
57
- // Comparisons and the logical connectives yield `int`.
58
- if (['<', '<=', '>', '>=', '==', '!=', '&&', '||'].includes(e.op)) {
59
- return true;
60
- }
61
- // Usual arithmetic conversions over the remaining binary operators: at equal rank, unsigned
62
- // wins. Either side unknown leaves the result unknown — EXCEPT when the known side is
63
- // unsigned, which already decides it.
64
- //
65
- // That exception is the one place this returns a DEFINITE answer from an unknown operand,
66
- // and it is sound only because every integer here is rank `int`: at UNEQUAL rank C converts
67
- // to the wider type first, so `unsigned int & long long` is SIGNED. Core has no 64-bit
68
- // integer type at all (the decomp typedef vocabulary stops at 32 — see contracts.ts
69
- // SCALAR_WIDTHS), so the unequal-rank case cannot arise. Adding one would invalidate this.
70
- const l = rec(e.l);
71
- const r = rec(e.r);
72
- if (l === false || r === false) {
73
- return false;
74
- }
75
- return l === true && r === true ? true : undefined;
76
- }
77
- case 'call':
78
- case 'marker':
79
- case 'addr':
80
- return undefined;
81
- }
82
- }
15
+ import { orderSlotLocals } from '../l3/slotorder';
16
+ import {
17
+ type PrintEnv,
18
+ arithConversionSignedness,
19
+ assertsVolatile,
20
+ declaredTypes,
21
+ derefStrideOk,
22
+ exprCType,
23
+ printEnv,
24
+ renderedIntSignedness,
25
+ writesNonPointerIntoPointer,
26
+ } from '../l3/typing';
83
27
 
84
28
  // C operator precedence (lower binds tighter). Used to emit MINIMAL parentheses. Shared: C++ has
85
29
  // the same precedence for these operators.
86
30
  const PREC: Record<BinOp, number> = {
87
31
  '*': 3,
88
32
  '/': 3,
33
+ '/u': 3, // the unsigned twins spell as C's `/` and `%` (see C_SPELLING) — same precedence
89
34
  '%': 3,
35
+ '%u': 3,
90
36
  '+': 4,
91
37
  '-': 4,
92
38
  '<<': 5,
93
39
  '>>': 5,
94
- '>>>': 5, // spells as C's `>>` (see printExpr's shift rule) — same precedence
40
+ '>>>': 5, // spells as C's `>>` — same precedence
95
41
  '<': 6,
96
42
  '<=': 6,
97
43
  '>': 6,
@@ -105,6 +51,19 @@ const PREC: Record<BinOp, number> = {
105
51
  '||': 12,
106
52
  };
107
53
 
54
+ /** The signedness-carrying operator pairs and the ONE C token each pair shares (l3/ast.ts BinOp).
55
+ * Membership here is what makes an operator's operands get pinned; every other operator prints
56
+ * its own symbol and needs no cast, because C's own rules for it are sign-blind or already
57
+ * agree. */
58
+ const C_SPELLING: Partial<Record<BinOp, { token: string; signed: boolean }>> = {
59
+ '>>': { token: '>>', signed: true },
60
+ '>>>': { token: '>>', signed: false },
61
+ '/': { token: '/', signed: true },
62
+ '/u': { token: '/', signed: false },
63
+ '%': { token: '%', signed: true },
64
+ '%u': { token: '%', signed: false },
65
+ };
66
+
108
67
  /** Spell a recovered type in the decomp C-family typedef vocabulary (`s32`/`u32`/`u8`/`T *`). */
109
68
  export function cType(t: IrType): string {
110
69
  if (t.kind === 'ptr') {
@@ -121,10 +80,18 @@ export function cType(t: IrType): string {
121
80
 
122
81
  /** Declare a name of a given type, C declarator rules: an array puts its length AFTER the name
123
82
  * (`u8 _pad[4]`), a pointer binds its `*` to the declarator (`void *p`), everything else is
124
- * the prefix `cType name`. */
83
+ * the prefix `cType name`. A NESTED array spells every extent after the name in declaration
84
+ * order (`u8 unk8[6][8]`) — one declarator, not an element type that is itself an array, which
85
+ * C has no syntax for and `cType` marks ill-formed as a prefix. */
125
86
  function cDeclare(t: IrType, name: string): string {
126
87
  if (t.kind === 'array') {
127
- return `${cType(t.elem)} ${name}[${t.count}]`;
88
+ const extents: number[] = [];
89
+ let e: IrType = t;
90
+ while (e.kind === 'array') {
91
+ extents.push(e.count);
92
+ e = e.elem;
93
+ }
94
+ return `${cType(e)} ${name}${extents.map((n) => `[${n}]`).join('')}`;
128
95
  }
129
96
  if (t.kind === 'ptr') {
130
97
  return `${cType(t.to)} *${name}`;
@@ -163,7 +130,92 @@ export function renderStructDecl(name: string, fields: StructFieldDecl[]): strin
163
130
  // backend uses. The default (no hook) is byte-identical C.
164
131
  export type LeafHook = (e: Expr, rec: (e: Expr, p: number) => string) => string | null;
165
132
 
166
- function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook): string {
133
+ /** C-FAMILY LEGALIZATION (owned here, per the width-carrying `index` node contract in l3/ast.ts):
134
+ * the BASE an `index` node prints through — a deref whose base does not render as a pointer/array
135
+ * STRIDING the access width is spelled through the honest reinterpret cast at that width, the
136
+ * machine semantics of the access. Materialized as a synthetic cast node so the spelling (text,
137
+ * precedence, parens) is exactly that of a tree-level cast, and returns the base UNCHANGED (===)
138
+ * when no cast is needed, which is what the multidimensional guard tests.
139
+ *
140
+ * AND IT CARRIES THE QUALIFIER. In C the access takes the OUTER type, so a plain cast over a
141
+ * base the tree declared volatile spells an MMIO access the compiler may CSE, reorder or drop —
142
+ * `((s32 *)(volatile u16 *)0x4000208)[i]` reads once where `((volatile s32 *)0x4000208)[i]`
143
+ * reads twice (agbcc 2.9-arm-000512, `-O2 -mthumb-interwork -Wimplicit -fhex-asm
144
+ * -fprologue-bugfix`), and referring to a volatile object through a non-volatile lvalue is
145
+ * undefined behaviour (C99 6.7.3p5). */
146
+ function legalizedIndexBase(ix: Extract<Expr, { k: 'index' }>, vt: PrintEnv): Expr {
147
+ // `baseElem` states the element type the base's own DECLARATION gives it, where the type walk
148
+ // cannot reconstruct one (a map-declared array MEMBER — see l3/ast.ts). It is a FALLBACK, not an
149
+ // override, and the order is the whole guard: the walk reads the tree in front of it, so
150
+ // wherever it answers at all it is the better answer and the stated one is not consulted.
151
+ // Inverted (`declared ?? walked`), a stale statement would stand — `derefStrideOk` cannot catch
152
+ // one, because it tests the STATED type against the access width and never against the base.
153
+ // No pass carries a statement onto a foreign base today (each refuses a `field` base by its own
154
+ // predicate: basecse `isHoistableBase`, scopebase/argbase `eligible`, nearbase's untouched
155
+ // `field` subtree, reindex's `base.k === 'var'`), but `mapExprChildren` spreads the field across
156
+ // an arbitrary base substitution, so that is five accidents rather than a check. This ordering
157
+ // is the check. Measured: zero test moves either way.
158
+ const declared = ix.baseElem !== undefined ? T.ptr(ix.baseElem) : undefined;
159
+ return derefStrideOk(exprCType(ix.base, vt.type) ?? declared, ix.width, ix.signed)
160
+ ? ix.base
161
+ : {
162
+ k: 'cast',
163
+ to: T.ptr(scalarTypeForAccess(ix.width, ix.signed)),
164
+ ...(assertsVolatile(ix.base, vt) ? { volatile: true as const } : {}),
165
+ e: ix.base,
166
+ };
167
+ }
168
+
169
+ /** The 32-bit integer cast that PINS an operand's signedness.
170
+ *
171
+ * An existing 32-bit integer cast is REPLACED rather than wrapped — `(u32)(s32)&g` and `(u32)&g`
172
+ * are the same bytes, and the arithmetic rules upstream do emit that inner cast (intifyAddr).
173
+ * The replacement CARRIES the qualifier: re-typing a `volatile` cast without it drops an
174
+ * assertion the differ cannot referee the loss of, which is why l3/initfirst.ts's
175
+ * `stripWideIntCast` refuses the same peel one pass over. */
176
+ function recast32(x: Expr, signed: boolean): Expr {
177
+ const replaced = x.k === 'cast' && x.to.kind === 'int' && x.to.width === 32 ? x : undefined;
178
+ return {
179
+ k: 'cast',
180
+ to: T.int(32, signed),
181
+ ...(replaced?.volatile === true ? { volatile: true as const } : {}),
182
+ e: replaced ? replaced.e : x,
183
+ };
184
+ }
185
+
186
+ /** C-FAMILY OPERAND-SIGNEDNESS LEGALIZATION, the same discipline as the deref cast one operator
187
+ * over. The tower keeps the signedness-carrying pairs apart (`>>>` logical / `>>` arithmetic,
188
+ * `/u` `%u` unsigned / `/` `%` signed); C spells each pair with ONE token and picks between them
189
+ * from the operand types. So the operands must be made to carry the choice, or an `shr_u`
190
+ * recompiles to `asr` where the target has `lsr` AND evaluates differently
191
+ * (`*(u8 *)&g << 30 >> 30` promotes to `int`, so a 2-bit field holding 2 comes out -1), and a
192
+ * `udiv` calls `__divsi3` where the target called `__udivsi3`.
193
+ *
194
+ * (engine.ts's zext fold covers the same hazard for widths C can NAME, by folding the whole
195
+ * shift pair to a cast op. Every other extract width — every bitfield read — lands here.)
196
+ *
197
+ * A SHIFT and a DIVIDE read their operands differently, so the pin does too. A SHIFT takes the
198
+ * type of its left operand alone, and the cast goes on unless that operand PROVABLY renders as
199
+ * the op needs (renderedIntSignedness's header carries the rule for reading `undefined`).
200
+ *
201
+ * A DIVIDE takes the usual arithmetic conversions over BOTH operands, where unsigned wins at
202
+ * equal rank, so the question is what the PAIR renders as. Once the pair renders wrong the two
203
+ * directions cost differently: unsigned takes ONE cast, which carries the whole operation, while
204
+ * signed has to pin EVERY operand short of a proof, because one unsigned side is enough to make
205
+ * the division unsigned. Verified by compiling: `((u32)a / b) / 7` calls `__udivsi3` twice,
206
+ * `(s32)((u32)a / b) / 7` calls `__udivsi3` then `__divsi3`. */
207
+ function pinnedOperands(e0: Extract<Expr, { k: 'bin' }>, wantSigned: boolean, vt: PrintEnv): [Expr, Expr] {
208
+ if (e0.op === '>>' || e0.op === '>>>') {
209
+ return [renderedIntSignedness(e0.l, vt.type) === wantSigned ? e0.l : recast32(e0.l, wantSigned), e0.r];
210
+ }
211
+ if (arithConversionSignedness(e0.l, e0.r, vt.type) === wantSigned) {
212
+ return [e0.l, e0.r];
213
+ }
214
+ const pinSigned = (x: Expr): Expr => (renderedIntSignedness(x, vt.type) === true ? x : recast32(x, true));
215
+ return wantSigned ? [pinSigned(e0.l), pinSigned(e0.r)] : [recast32(e0.l, false), e0.r];
216
+ }
217
+
218
+ function printExpr(e: Expr, parentPrec: number, vt: PrintEnv, leaf?: LeafHook): string {
167
219
  const rec = (x: Expr, p: number) => printExpr(x, p, vt, leaf);
168
220
  if (leaf) {
169
221
  const s = leaf(e, rec);
@@ -171,37 +223,6 @@ function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook):
171
223
  return s;
172
224
  }
173
225
  }
174
- // C-FAMILY LEGALIZATION (owned here, per the width-carrying `index` node contract in l3/ast.ts):
175
- // a deref whose base does not render as a pointer/array STRIDING the access width is spelled
176
- // through the honest reinterpret cast at that width — the machine semantics of the access.
177
- // Materialized as a synthetic cast node so the spelling (text, precedence, parens) is exactly
178
- // that of a tree-level cast.
179
- const legalized = (ix: Extract<Expr, { k: 'index' }>): Expr =>
180
- derefStrideOk(exprCType(ix.base, vt), ix.width)
181
- ? ix.base
182
- : { k: 'cast', to: T.ptr(scalarTypeForAccess(ix.width, ix.signed)), e: ix.base };
183
- // C-FAMILY SHIFT LEGALIZATION, the same discipline one operator over. The tower keeps the two
184
- // right shifts apart (`>>>` logical, `>>` arithmetic); C spells BOTH `>>` and picks between them
185
- // from the LEFT OPERAND'S TYPE. So the operand must be made to carry the choice, or an `shr_u`
186
- // recompiles to `asr` where the target has `lsr` AND evaluates differently —
187
- // `*(u8 *)&g << 30 >> 30` promotes to `int`, so a 2-bit field holding 2 comes out -1.
188
- //
189
- // (engine.ts's zext fold covers the same hazard for widths C can NAME, by folding the whole
190
- // shift pair to a cast op. Every other extract width — every bitfield read — lands here.)
191
- //
192
- // The cast is added unless the operand PROVABLY renders with the signedness the op needs:
193
- // renderedIntSignedness answers `undefined` wherever its model does not reach, and a redundant
194
- // cast is codegen-identical while a missing one is a miscompile. An existing 32-bit integer cast
195
- // is REPLACED rather than wrapped — `(u32)(s32)&g` and `(u32)&g` are the same bytes, and the
196
- // arithmetic rules upstream do emit that inner cast (intifyAddr).
197
- const shiftOperand = (e0: Extract<Expr, { k: 'bin' }>): Expr => {
198
- const wantSigned = e0.op === '>>';
199
- if (renderedIntSignedness(e0.l, vt) === wantSigned) {
200
- return e0.l;
201
- }
202
- const inner = e0.l.k === 'cast' && e0.l.to.kind === 'int' && e0.l.to.width === 32 ? e0.l.e : e0.l;
203
- return { k: 'cast', to: T.int(32, wantSigned), e: inner };
204
- };
205
226
  switch (e.k) {
206
227
  case 'var':
207
228
  return e.name;
@@ -221,21 +242,30 @@ function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook):
221
242
  // `(*p)[1]`, never `*p[1]` which C groups as `*(p[1])`), `base[idx]` otherwise (POSTFIX —
222
243
  // binds tighter than any prefix operator, so a cast/unary/deref base is printed at prec 1
223
244
  // and parenthesizes itself: `((u8 *)p)[1]`). The postfix form needs no outer parentheses.
224
- const base = legalized(e);
245
+ const base = legalizedIndexBase(e, vt);
225
246
  // Leading constant subscripts (a multidimensional array global's bare spelling) keep the
226
247
  // postfix form whatever `idx` is: `g[0][0]` is the element, `*g[0]` would be its ROW.
227
248
  //
228
- // `lead` implies the base already strides the access width its only producer registers a
229
- // matching element type for the global (structure.ts bareArrayLead + noteGlobal). Nothing
230
- // else enforced that, and the failure would be quiet-ish: legalization would wrap the base,
231
- // spelling `((u16 *)g)[0][i]`, which subscripts a `u16` twice. Check it rather than assume.
249
+ // `lead` implies the base already strides the access width, and its producers reach that by
250
+ // TWO different mechanisms. The two GLOBAL ones register a matching element type for the
251
+ // global (structure/globalaccess.ts `bareArrayLead` and `declaredSubscripts`, both through
252
+ // structure.ts's `noteGlobal`, so the type walk finds it); the MEMBER one
253
+ // (structure.ts `pointeeElement`, `gPtr->grid[0][i]`) has no global to register and states
254
+ // the element type on the node instead (`baseElem`). Nothing else enforced either, and the
255
+ // failure would be quiet-ish: legalization would wrap the base, spelling `((u16 *)g)[0][i]`,
256
+ // which subscripts a `u16` twice. Check it rather than assume.
257
+ //
258
+ // This throw covers rank >= 2 ONLY, because only a rank >= 2 access carries `lead`. A rank-1
259
+ // member (the large majority of the corpus's dims-carrying members) has no guard here: a
260
+ // missing or stale `baseElem` there degrades silently to the cast form the rule replaces,
261
+ // which is a lost spelling rather than a wrong address.
232
262
  if (e.lead && e.lead.length > 0) {
233
263
  if (base !== e.base) {
234
264
  throw new Error(
235
265
  `c backend: a multidimensional array access needs a base that strides ${e.width} bytes as spelled`,
236
266
  );
237
267
  }
238
- return `${rec(base, 1)}${e.lead.map((l) => `[${l}]`).join('')}[${rec(e.idx, 99)}]`;
268
+ return `${rec(base, 1)}${e.lead.map((l) => `[${rec(l, 99)}]`).join('')}[${rec(e.idx, 99)}]`;
239
269
  }
240
270
  if (e.idx.k === 'const' && e.idx.value === 0) {
241
271
  const s = `*${rec(base, 2)}`;
@@ -289,20 +319,25 @@ function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook):
289
319
  // op. Under a POSTFIX parent ([]/->) the cast itself must parenthesize — `((struct S *)p)->f`,
290
320
  // NOT `(struct S *)p->f` (which C parses as a cast OF the member access).
291
321
  case 'cast': {
292
- const s = `(${cType(e.to)})${rec(e.e, 2)}`;
322
+ // `volatile` binds to the POINTEE (`(volatile u16 *)a`), which is where the qualifier goes
323
+ // on a raw-address access — the same prefix position the declaration printer uses for a
324
+ // pointer local's `pointeeVolatile`.
325
+ const s = `(${e.volatile ? 'volatile ' : ''}${cType(e.to)})${rec(e.e, 2)}`;
293
326
  return parentPrec < 2 ? `(${s})` : s;
294
327
  }
295
328
  case 'bin': {
296
329
  const p = PREC[e.op];
297
- // Both right shifts spell C's `>>`; `shiftOperand` supplies the operand cast that says which.
298
- const shift = e.op === '>>' || e.op === '>>>';
299
- const s = `${rec(shift ? shiftOperand(e) : e.l, p)} ${shift ? '>>' : e.op} ${rec(e.r, p - 1)}`;
330
+ // Each signedness-carrying pair spells with ONE C token; `pinnedOperands` supplies the
331
+ // operand cast that says which of the pair it is.
332
+ const pair = C_SPELLING[e.op];
333
+ const [l, r] = pair ? pinnedOperands(e, pair.signed, vt) : [e.l, e.r];
334
+ const s = `${rec(l, p)} ${pair ? pair.token : e.op} ${rec(r, p - 1)}`;
300
335
  return p > parentPrec ? `(${s})` : s;
301
336
  }
302
337
  }
303
338
  }
304
339
 
305
- function printStmt(s: Stmt, indent: string, vt: VarTypes, leaf?: LeafHook): string[] {
340
+ function printStmt(s: Stmt, indent: string, vt: PrintEnv, leaf?: LeafHook): string[] {
306
341
  const pe = (e: Expr, p: number) => printExpr(e, p, vt, leaf);
307
342
  switch (s.k) {
308
343
  case 'assign':
@@ -355,8 +390,8 @@ function printStmt(s: Stmt, indent: string, vt: VarTypes, leaf?: LeafHook): stri
355
390
  return out;
356
391
  }
357
392
  case 'for': {
358
- // `for (init; cond; inc) { body }`. PRECONDITION (guaranteed by the sole producer, structure.ts
359
- // `recognizeForLoops`): init/inc are each a SINGLE-LINE `assign` statement. `clause` renders one
393
+ // `for (init; cond; inc) { body }`. PRECONDITION (guaranteed by both producers structure.ts
394
+ // `recognizeForLoops` and l3/reindex.ts): init/inc are each a SINGLE-LINE `assign` statement. `clause` renders one
360
395
  // and strips its trailing `;` so it sits inside the header (`i = 0; c; i = i + 1`). A multi-line
361
396
  // statement (an `if`/nested loop) would render mangled — but the recognizer never builds one here.
362
397
  const clause = (st: Stmt) => printStmt(st, '', vt, leaf).join(' ').replace(/;\s*$/, '').trim();
@@ -371,36 +406,98 @@ function printStmt(s: Stmt, indent: string, vt: VarTypes, leaf?: LeafHook): stri
371
406
  return [`${indent}break;`];
372
407
  case 'continue':
373
408
  return [`${indent}continue;`];
374
- case 'switch': {
375
- const out = [`${indent}switch (${pe(s.scrutinee, 99)}) {`];
376
- const ci = indent + ' '; // case-label indent
377
- const bi = indent + ' '; // case-body indent
378
- for (const c of s.cases) {
379
- for (const v of c.values) {
380
- out.push(`${ci}case ${v}:`);
381
- }
382
- for (const t of c.body) {
383
- out.push(...printStmt(t, bi, vt, leaf));
384
- }
385
- // A case whose body ends in `return`/`break` (a terminated arm) needs no `break;`; only an
386
- // open non-fall-through arm gets one. `fallsThrough` omits it so control drops to the next case.
387
- if (!c.fallsThrough && !endsTerminated(c.body)) {
388
- out.push(`${bi}break;`);
389
- }
390
- }
391
- // `?.length`, not just presence: a label with no statement under it is not valid C89, and an
392
- // L3 pass (dce, reindex) may empty a default that arrived with statements the structurer's
393
- // own "don't attach an empty default" rule cannot see that.
394
- if (s.default?.length) {
395
- out.push(`${ci}default:`);
396
- for (const t of s.default) {
397
- out.push(...printStmt(t, bi, vt, leaf));
398
- }
409
+ case 'switch':
410
+ return printSwitchStmt(s, indent, vt, leaf);
411
+ }
412
+ }
413
+
414
+ /** A C `switch`: the arm bodies, the `default:` label's position among them, and the three
415
+ * producer-contract refusals that stand between an L3 `switch` node and valid C. Its own function
416
+ * because it is the one statement kind whose printing is a program of its own; `printStmt`'s arm
417
+ * is one call. */
418
+ function printSwitchStmt(s: Extract<Stmt, { k: 'switch' }>, indent: string, vt: PrintEnv, leaf?: LeafHook): string[] {
419
+ const pe = (e: Expr, p: number) => printExpr(e, p, vt, leaf);
420
+ // A `break;` in an arm BODY is the innermost LOOP's in L3 (l3/ast.ts) and the SWITCH's in C,
421
+ // so printing one between `case` labels rebinds it a changed program that reads as ordinary
422
+ // C. Refused HERE because the rebinding is the printer's, for every producer rather than for
423
+ // the two switch regimes. No recovery reaches it today (both regimes decline a loop-exiting
424
+ // arm first, loudly), so it is a contract on the next one.
425
+ //
426
+ // `continue;` is deliberately NOT refused: C binds it to the smallest enclosing ITERATION
427
+ // statement, which a `switch` is not, so it already means what L3 means. What a pass that re-spells
428
+ // a loop must preserve is exactly that the three that can re-spell a loop into one whose
429
+ // `continue` would run a different increment (`recognizeForLoops` in structure.ts,
430
+ // `respellCountdown` in l3/reindex.ts, and l3/unreduce.ts) each scan switch arms for the node
431
+ // before firing, and a fourth must too.
432
+ for (const body of [...s.cases.map((c) => c.body), s.default ?? []]) {
433
+ if (switchBoundBreakIn(body)) {
434
+ throw new Error('c backend: a switch arm carries a loop-scoped `break;`, which C would bind to the switch');
435
+ }
436
+ }
437
+ const out = [`${indent}switch (${pe(s.scrutinee, 99)}) {`];
438
+ const ci = indent + ' '; // case-label indent
439
+ const bi = indent + ' '; // case-body indent
440
+ // `?.length`, not just presence: a label with no statement under it is not valid C89, and an
441
+ // L3 pass (dce, reindex) may empty a default that arrived with statements — the structurer's
442
+ // own "don't attach an empty default" rule cannot see that.
443
+ const hasDefault = !!s.default?.length;
444
+ // Where the `default:` label goes, as a COUNT of case arms before it (l3/ast.ts): absent ⇒
445
+ // after all of them. A count past the arms matches no position at all and the label would
446
+ // simply not be printed — the default arm vanishing from a switch that has one — so a
447
+ // producer that hands one over fails loud, like the falling-arm placement below.
448
+ const defAt = hasDefault ? (s.defaultAt ?? s.cases.length) : -1;
449
+ if (hasDefault && (defAt < 0 || defAt > s.cases.length)) {
450
+ throw new Error(`c backend: a switch places its default at arm ${defAt} of ${s.cases.length}`);
451
+ }
452
+ const printDefault = (): void => {
453
+ out.push(`${ci}default:`);
454
+ for (const t of s.default!) {
455
+ out.push(...printStmt(t, bi, vt, leaf));
456
+ }
457
+ // A default that is NOT last would otherwise fall into the case below it — the mirror of the
458
+ // rule for cases. The last one needs no `break;` because there is nothing under it.
459
+ if (defAt < s.cases.length && !endsTerminated(s.default!)) {
460
+ out.push(`${bi}break;`);
461
+ }
462
+ };
463
+ s.cases.forEach((c, i) => {
464
+ if (i === defAt) {
465
+ // Moving the label in FRONT of a falling arm would divert that arm into the default —
466
+ // a silent control-flow change. Recovery only positions a default among closed arms, so
467
+ // this is a producer bug rather than an input shape: fail loud.
468
+ if (i > 0 && s.cases[i - 1].fallsThrough) {
469
+ throw new Error(
470
+ `c backend: a switch places its default after a case that falls through, which would divert it`,
471
+ );
399
472
  }
400
- out.push(`${indent}}`);
401
- return out;
473
+ printDefault();
474
+ }
475
+ for (const v of c.values) {
476
+ out.push(`${ci}case ${v}:`);
477
+ }
478
+ for (const t of c.body) {
479
+ out.push(...printStmt(t, bi, vt, leaf));
402
480
  }
481
+ // A case whose body ends in `return`/`break` (a terminated arm) needs no `break;`; only an
482
+ // open non-fall-through arm gets one. `fallsThrough` omits it so control drops to the next case.
483
+ if (!c.fallsThrough && !endsTerminated(c.body)) {
484
+ out.push(`${bi}break;`);
485
+ }
486
+ });
487
+ if (defAt === s.cases.length) {
488
+ printDefault();
403
489
  }
490
+ out.push(`${indent}}`);
491
+ return out;
492
+ }
493
+
494
+ /** Does this arm body carry a `break` C would bind to the enclosing SWITCH rather than to the loop
495
+ * L3 meant? A loop opened inside the arm captures its own, so its body is not walked; a nested
496
+ * `switch` captures one too and is checked when it is printed. */
497
+ function switchBoundBreakIn(body: Stmt[]): boolean {
498
+ return body.some(
499
+ (s) => s.k === 'break' || (s.k === 'if' && (switchBoundBreakIn(s.then) || switchBoundBreakIn(s.else))),
500
+ );
404
501
  }
405
502
 
406
503
  // Does a statement list end in a control-flow terminator (so a trailing `break;` would be dead)?
@@ -415,26 +512,15 @@ function endsTerminated(body: Stmt[]): boolean {
415
512
  );
416
513
  }
417
514
 
418
- /** The body of a C-family function: local declarations + statements, one string per line. The
419
- * SIGNATURE (return type + name + params, plus any C++ scope/`this`/mangling) is the caller's —
420
- * that is the language-divergent part each backend owns. */
421
515
  // C-FAMILY WRITE LEGALIZATION (the assign-side sibling of the deref legalization in printExpr):
422
- // a value whose rendered C type is definitely NON-pointer written into a pointer-declared slot
423
- // (`v2 = a1 + v0` with `v2: u8 *`; `return a0 + v0` from a ptr-returning fn; `*pp = intexpr`
424
- // through a pointer-element slot) is an ERROR on mwcc (gcc merely warns) the honest spelling
425
- // is the reinterpret cast to the DECLARED type, exactly what the machine's register move does.
426
- // Unknowable renderings (calls) are left alone: their C type comes from prototypes outside this
427
- // function. (A rebuilding transform with per-kind semantics — its own switch, per the l3/ast.ts
516
+ // the C family's answer to `writesNonPointerIntoPointer` is the reinterpret cast to the DECLARED
517
+ // type exactly what the machine's register move does where Pascal, having no such cast,
518
+ // declines. (A rebuilding transform with per-kind semantics its own switch, per the l3/ast.ts
428
519
  // traversal-vocabulary exemption.)
429
520
  function legalizePointerWrites(fn: SFn): SFn {
430
521
  const vt = declaredTypes(fn);
431
- const castTo = (t: IrType | undefined, e: Expr): Expr => {
432
- if (t?.kind !== 'ptr') {
433
- return e;
434
- }
435
- const ct = exprCType(e, vt);
436
- return ct && ct.kind !== 'ptr' && ct.kind !== 'array' ? { k: 'cast', to: t, e } : e;
437
- };
522
+ const castTo = (t: IrType | undefined, e: Expr): Expr =>
523
+ writesNonPointerIntoPointer(t, e, vt) ? { k: 'cast', to: t, e } : e;
438
524
  const fix = (s: Stmt): Stmt => {
439
525
  switch (s.k) {
440
526
  case 'assign':
@@ -466,14 +552,23 @@ function legalizePointerWrites(fn: SFn): SFn {
466
552
  return { ...fn, body: fn.body.map(fix) };
467
553
  }
468
554
 
555
+ /** The body of a C-family function: local declarations + statements, one string per line. The
556
+ * SIGNATURE (return type + name + params, plus any C++ scope/`this`/mangling) is the caller's —
557
+ * that is the language-divergent part each backend owns. */
469
558
  function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
470
559
  const fn = legalizePointerWrites(fn0);
471
- // The legalization env: every printed var's declared type, from the SAME params/locals the
472
- // emitted declarations come from — so the printer judges exactly the C the reader will see.
473
- const vt: VarTypes = declaredTypes(fn);
560
+ // The legalization env: every printed var's declared type and pointee volatility, from the SAME
561
+ // params/locals the emitted declarations come from — so the printer judges exactly the C the
562
+ // reader will see.
563
+ const vt: PrintEnv = printEnv(fn);
474
564
  const lines: string[] = [];
475
565
  for (const l of fn.locals) {
476
- lines.push(` ${l.volatile ? 'volatile ' : ''}${cType(l.type)} ${l.name};`);
566
+ // Both facts render at the PREFIX position, where C's declarator grammar reads them
567
+ // differently: on a scalar the qualifier binds to the object (`volatile u16 sp0`), on a
568
+ // pointer declarator to the pointee — the INNERMOST one for a multi-level pointer
569
+ // (`volatile u16 ** p`). An object-volatile POINTER (`u16 *volatile p`) has no inhabitant —
570
+ // no variation or recognizer produces one.
571
+ lines.push(` ${l.volatile || l.pointeeVolatile ? 'volatile ' : ''}${cType(l.type)} ${l.name};`);
477
572
  }
478
573
  for (const s of fn.body) {
479
574
  lines.push(...printStmt(s, ' ', vt, leaf));
@@ -491,7 +586,11 @@ function structDecls(fn: SFn): string[] {
491
586
  }
492
587
 
493
588
  /** Assemble a full C-family function from a caller-supplied signature line and the shared body. */
494
- export function emitCFamily(signature: string, fn: SFn, leaf?: LeafHook): string {
589
+ export function emitCFamily(signature: string, fn0: SFn, leaf?: LeafHook): string {
590
+ // The declaration list is put into the target's own frame order HERE — in the shared C-family
591
+ // assembler, reached only from `cBackend.emit` and `cppBackend(...).emit`, so both C-family
592
+ // backends order and no `.emit(` call site does (l3/slotorder.ts says why `emit` owns it).
593
+ const fn = orderSlotLocals(fn0);
495
594
  const decls = structDecls(fn);
496
595
  const preamble = decls.length ? decls.join('\n') + '\n' : '';
497
596
  return preamble + [`${signature} {`, ...cFamilyBody(fn, leaf), '}'].join('\n') + '\n';
@@ -38,6 +38,7 @@ export function cppSymbol(spec: CppFnSpec): string {
38
38
  export function cppBackend(spec: CppFnSpec): LanguageBackend {
39
39
  return {
40
40
  id: 'cpp',
41
+ spellsSwitchFallthrough: true,
41
42
  emit(fn: SFn): string {
42
43
  // Map each lifted param var → its C++ meaning: `this` (bare member access) or a named param
43
44
  // (a pointer-to-class param uses `->`). A pointer-to-known-class param is a member receiver.