@asmlift/core 0.5.0 → 0.6.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 (86) 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 +238 -167
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +194 -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 +54 -0
  14. package/src/ir/bits.ts +75 -0
  15. package/src/ir/core.ts +337 -2
  16. package/src/ir/opcodes.ts +140 -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/argbase.ts +2 -1
  24. package/src/l3/ast.ts +464 -57
  25. package/src/l3/basecse.ts +664 -76
  26. package/src/l3/coalesce.ts +429 -43
  27. package/src/l3/dce.ts +31 -9
  28. package/src/l3/gates.ts +21 -0
  29. package/src/l3/hoist.ts +293 -14
  30. package/src/l3/homesplit.ts +285 -0
  31. package/src/l3/initfirst.ts +301 -0
  32. package/src/l3/inlinebase.ts +193 -0
  33. package/src/l3/mentions.ts +113 -0
  34. package/src/l3/mulfirst.ts +42 -0
  35. package/src/l3/nearbase.ts +152 -0
  36. package/src/l3/offmember.ts +371 -0
  37. package/src/l3/parkfirst.ts +96 -0
  38. package/src/l3/pollguard.ts +154 -0
  39. package/src/l3/ptrfield.ts +227 -0
  40. package/src/l3/regspell.ts +110 -85
  41. package/src/l3/reindex.ts +715 -78
  42. package/src/l3/scopebase.ts +644 -218
  43. package/src/l3/sinkinit.ts +40 -0
  44. package/src/l3/slotorder.ts +123 -0
  45. package/src/l3/storage.ts +48 -0
  46. package/src/l3/symbol-refs.ts +41 -8
  47. package/src/l3/tailmerge.ts +15 -0
  48. package/src/l3/typing.ts +198 -9
  49. package/src/l3/unmerge.ts +263 -0
  50. package/src/l3/unreduce.ts +971 -0
  51. package/src/l3/volatileptr.ts +207 -0
  52. package/src/l3/volatileval.ts +130 -0
  53. package/src/l3/volstore.ts +229 -0
  54. package/src/l3/zerosub.ts +62 -0
  55. package/src/pattern/engine.ts +236 -13
  56. package/src/pipeline.ts +157 -56
  57. package/src/proto.ts +112 -14
  58. package/src/raise/arrays.ts +6 -1
  59. package/src/raise/divpow2.ts +2 -2
  60. package/src/raise/globalshape.ts +1038 -0
  61. package/src/raise/gvn.ts +33 -18
  62. package/src/raise/latch.ts +126 -0
  63. package/src/raise/memberarrays.ts +594 -0
  64. package/src/raise/narrow.ts +124 -0
  65. package/src/raise/narrowlocal.ts +556 -0
  66. package/src/raise/paramwidth.ts +179 -0
  67. package/src/raise/pre-recovery.ts +97 -14
  68. package/src/raise/recover.ts +56 -23
  69. package/src/raise/retsink.ts +210 -10
  70. package/src/raise/shortcircuit.ts +474 -74
  71. package/src/raise/struct-arrays.ts +19 -2
  72. package/src/raise/structs.ts +33 -3
  73. package/src/rank-axes.ts +630 -0
  74. package/src/rank-declare.ts +256 -0
  75. package/src/rank.ts +1723 -272
  76. package/src/structure/analysis.ts +1392 -141
  77. package/src/structure/bitfields.ts +332 -0
  78. package/src/structure/globalaccess.ts +274 -0
  79. package/src/structure/hazards.ts +411 -20
  80. package/src/structure/loops.ts +2 -49
  81. package/src/structure/namecoalesce.ts +435 -0
  82. package/src/structure/structure.ts +2678 -526
  83. package/src/structure/switch-recover.ts +616 -144
  84. package/src/symbols.ts +62 -1
  85. package/src/target.ts +367 -24
  86. package/src/trace.ts +111 -32
@@ -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') {
@@ -163,7 +122,80 @@ export function renderStructDecl(name: string, fields: StructFieldDecl[]): strin
163
122
  // backend uses. The default (no hook) is byte-identical C.
164
123
  export type LeafHook = (e: Expr, rec: (e: Expr, p: number) => string) => string | null;
165
124
 
166
- function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook): string {
125
+ /** C-FAMILY LEGALIZATION (owned here, per the width-carrying `index` node contract in l3/ast.ts):
126
+ * the BASE an `index` node prints through — a deref whose base does not render as a pointer/array
127
+ * STRIDING the access width is spelled through the honest reinterpret cast at that width, the
128
+ * machine semantics of the access. Materialized as a synthetic cast node so the spelling (text,
129
+ * precedence, parens) is exactly that of a tree-level cast, and returns the base UNCHANGED (===)
130
+ * when no cast is needed, which is what the multidimensional guard tests.
131
+ *
132
+ * AND IT CARRIES THE QUALIFIER. In C the access takes the OUTER type, so a plain cast over a
133
+ * base the tree declared volatile spells an MMIO access the compiler may CSE, reorder or drop —
134
+ * `((s32 *)(volatile u16 *)0x4000208)[i]` reads once where `((volatile s32 *)0x4000208)[i]`
135
+ * reads twice (agbcc 2.9-arm-000512, `-O2 -mthumb-interwork -Wimplicit -fhex-asm
136
+ * -fprologue-bugfix`), and referring to a volatile object through a non-volatile lvalue is
137
+ * undefined behaviour (C99 6.7.3p5). */
138
+ function legalizedIndexBase(ix: Extract<Expr, { k: 'index' }>, vt: PrintEnv): Expr {
139
+ return derefStrideOk(exprCType(ix.base, vt.type), ix.width, ix.signed)
140
+ ? ix.base
141
+ : {
142
+ k: 'cast',
143
+ to: T.ptr(scalarTypeForAccess(ix.width, ix.signed)),
144
+ ...(assertsVolatile(ix.base, vt) ? { volatile: true as const } : {}),
145
+ e: ix.base,
146
+ };
147
+ }
148
+
149
+ /** The 32-bit integer cast that PINS an operand's signedness.
150
+ *
151
+ * An existing 32-bit integer cast is REPLACED rather than wrapped — `(u32)(s32)&g` and `(u32)&g`
152
+ * are the same bytes, and the arithmetic rules upstream do emit that inner cast (intifyAddr).
153
+ * The replacement CARRIES the qualifier: re-typing a `volatile` cast without it drops an
154
+ * assertion the differ cannot referee the loss of, which is why l3/initfirst.ts's
155
+ * `stripWideIntCast` refuses the same peel one pass over. */
156
+ function recast32(x: Expr, signed: boolean): Expr {
157
+ const replaced = x.k === 'cast' && x.to.kind === 'int' && x.to.width === 32 ? x : undefined;
158
+ return {
159
+ k: 'cast',
160
+ to: T.int(32, signed),
161
+ ...(replaced?.volatile === true ? { volatile: true as const } : {}),
162
+ e: replaced ? replaced.e : x,
163
+ };
164
+ }
165
+
166
+ /** C-FAMILY OPERAND-SIGNEDNESS LEGALIZATION, the same discipline as the deref cast one operator
167
+ * over. The tower keeps the signedness-carrying pairs apart (`>>>` logical / `>>` arithmetic,
168
+ * `/u` `%u` unsigned / `/` `%` signed); C spells each pair with ONE token and picks between them
169
+ * from the operand types. So the operands must be made to carry the choice, or an `shr_u`
170
+ * recompiles to `asr` where the target has `lsr` AND evaluates differently
171
+ * (`*(u8 *)&g << 30 >> 30` promotes to `int`, so a 2-bit field holding 2 comes out -1), and a
172
+ * `udiv` calls `__divsi3` where the target called `__udivsi3`.
173
+ *
174
+ * (engine.ts's zext fold covers the same hazard for widths C can NAME, by folding the whole
175
+ * shift pair to a cast op. Every other extract width — every bitfield read — lands here.)
176
+ *
177
+ * A SHIFT and a DIVIDE read their operands differently, so the pin does too. A SHIFT takes the
178
+ * type of its left operand alone, and the cast goes on unless that operand PROVABLY renders as
179
+ * the op needs (renderedIntSignedness's header carries the rule for reading `undefined`).
180
+ *
181
+ * A DIVIDE takes the usual arithmetic conversions over BOTH operands, where unsigned wins at
182
+ * equal rank, so the question is what the PAIR renders as. Once the pair renders wrong the two
183
+ * directions cost differently: unsigned takes ONE cast, which carries the whole operation, while
184
+ * signed has to pin EVERY operand short of a proof, because one unsigned side is enough to make
185
+ * the division unsigned. Verified by compiling: `((u32)a / b) / 7` calls `__udivsi3` twice,
186
+ * `(s32)((u32)a / b) / 7` calls `__udivsi3` then `__divsi3`. */
187
+ function pinnedOperands(e0: Extract<Expr, { k: 'bin' }>, wantSigned: boolean, vt: PrintEnv): [Expr, Expr] {
188
+ if (e0.op === '>>' || e0.op === '>>>') {
189
+ return [renderedIntSignedness(e0.l, vt.type) === wantSigned ? e0.l : recast32(e0.l, wantSigned), e0.r];
190
+ }
191
+ if (arithConversionSignedness(e0.l, e0.r, vt.type) === wantSigned) {
192
+ return [e0.l, e0.r];
193
+ }
194
+ const pinSigned = (x: Expr): Expr => (renderedIntSignedness(x, vt.type) === true ? x : recast32(x, true));
195
+ return wantSigned ? [pinSigned(e0.l), pinSigned(e0.r)] : [recast32(e0.l, false), e0.r];
196
+ }
197
+
198
+ function printExpr(e: Expr, parentPrec: number, vt: PrintEnv, leaf?: LeafHook): string {
167
199
  const rec = (x: Expr, p: number) => printExpr(x, p, vt, leaf);
168
200
  if (leaf) {
169
201
  const s = leaf(e, rec);
@@ -171,37 +203,6 @@ function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook):
171
203
  return s;
172
204
  }
173
205
  }
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
206
  switch (e.k) {
206
207
  case 'var':
207
208
  return e.name;
@@ -221,12 +222,13 @@ function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook):
221
222
  // `(*p)[1]`, never `*p[1]` which C groups as `*(p[1])`), `base[idx]` otherwise (POSTFIX —
222
223
  // binds tighter than any prefix operator, so a cast/unary/deref base is printed at prec 1
223
224
  // and parenthesizes itself: `((u8 *)p)[1]`). The postfix form needs no outer parentheses.
224
- const base = legalized(e);
225
+ const base = legalizedIndexBase(e, vt);
225
226
  // Leading constant subscripts (a multidimensional array global's bare spelling) keep the
226
227
  // postfix form whatever `idx` is: `g[0][0]` is the element, `*g[0]` would be its ROW.
227
228
  //
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
229
+ // `lead` implies the base already strides the access width — its only producers register a
230
+ // matching element type for the global (structure/globalaccess.ts `bareArrayLead` and
231
+ // `declaredSubscripts`, both through structure.ts's `noteGlobal`). Nothing
230
232
  // else enforced that, and the failure would be quiet-ish: legalization would wrap the base,
231
233
  // spelling `((u16 *)g)[0][i]`, which subscripts a `u16` twice. Check it rather than assume.
232
234
  if (e.lead && e.lead.length > 0) {
@@ -235,7 +237,7 @@ function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook):
235
237
  `c backend: a multidimensional array access needs a base that strides ${e.width} bytes as spelled`,
236
238
  );
237
239
  }
238
- return `${rec(base, 1)}${e.lead.map((l) => `[${l}]`).join('')}[${rec(e.idx, 99)}]`;
240
+ return `${rec(base, 1)}${e.lead.map((l) => `[${rec(l, 99)}]`).join('')}[${rec(e.idx, 99)}]`;
239
241
  }
240
242
  if (e.idx.k === 'const' && e.idx.value === 0) {
241
243
  const s = `*${rec(base, 2)}`;
@@ -289,20 +291,25 @@ function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook):
289
291
  // op. Under a POSTFIX parent ([]/->) the cast itself must parenthesize — `((struct S *)p)->f`,
290
292
  // NOT `(struct S *)p->f` (which C parses as a cast OF the member access).
291
293
  case 'cast': {
292
- const s = `(${cType(e.to)})${rec(e.e, 2)}`;
294
+ // `volatile` binds to the POINTEE (`(volatile u16 *)a`), which is where the qualifier goes
295
+ // on a raw-address access — the same prefix position the declaration printer uses for a
296
+ // pointer local's `pointeeVolatile`.
297
+ const s = `(${e.volatile ? 'volatile ' : ''}${cType(e.to)})${rec(e.e, 2)}`;
293
298
  return parentPrec < 2 ? `(${s})` : s;
294
299
  }
295
300
  case 'bin': {
296
301
  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)}`;
302
+ // Each signedness-carrying pair spells with ONE C token; `pinnedOperands` supplies the
303
+ // operand cast that says which of the pair it is.
304
+ const pair = C_SPELLING[e.op];
305
+ const [l, r] = pair ? pinnedOperands(e, pair.signed, vt) : [e.l, e.r];
306
+ const s = `${rec(l, p)} ${pair ? pair.token : e.op} ${rec(r, p - 1)}`;
300
307
  return p > parentPrec ? `(${s})` : s;
301
308
  }
302
309
  }
303
310
  }
304
311
 
305
- function printStmt(s: Stmt, indent: string, vt: VarTypes, leaf?: LeafHook): string[] {
312
+ function printStmt(s: Stmt, indent: string, vt: PrintEnv, leaf?: LeafHook): string[] {
306
313
  const pe = (e: Expr, p: number) => printExpr(e, p, vt, leaf);
307
314
  switch (s.k) {
308
315
  case 'assign':
@@ -355,8 +362,8 @@ function printStmt(s: Stmt, indent: string, vt: VarTypes, leaf?: LeafHook): stri
355
362
  return out;
356
363
  }
357
364
  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
365
+ // `for (init; cond; inc) { body }`. PRECONDITION (guaranteed by both producers structure.ts
366
+ // `recognizeForLoops` and l3/reindex.ts): init/inc are each a SINGLE-LINE `assign` statement. `clause` renders one
360
367
  // and strips its trailing `;` so it sits inside the header (`i = 0; c; i = i + 1`). A multi-line
361
368
  // statement (an `if`/nested loop) would render mangled — but the recognizer never builds one here.
362
369
  const clause = (st: Stmt) => printStmt(st, '', vt, leaf).join(' ').replace(/;\s*$/, '').trim();
@@ -371,36 +378,98 @@ function printStmt(s: Stmt, indent: string, vt: VarTypes, leaf?: LeafHook): stri
371
378
  return [`${indent}break;`];
372
379
  case 'continue':
373
380
  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
- }
381
+ case 'switch':
382
+ return printSwitchStmt(s, indent, vt, leaf);
383
+ }
384
+ }
385
+
386
+ /** A C `switch`: the arm bodies, the `default:` label's position among them, and the three
387
+ * producer-contract refusals that stand between an L3 `switch` node and valid C. Its own function
388
+ * because it is the one statement kind whose printing is a program of its own; `printStmt`'s arm
389
+ * is one call. */
390
+ function printSwitchStmt(s: Extract<Stmt, { k: 'switch' }>, indent: string, vt: PrintEnv, leaf?: LeafHook): string[] {
391
+ const pe = (e: Expr, p: number) => printExpr(e, p, vt, leaf);
392
+ // A `break;` in an arm BODY is the innermost LOOP's in L3 (l3/ast.ts) and the SWITCH's in C,
393
+ // so printing one between `case` labels rebinds it a changed program that reads as ordinary
394
+ // C. Refused HERE because the rebinding is the printer's, for every producer rather than for
395
+ // the two switch regimes. No recovery reaches it today (both regimes decline a loop-exiting
396
+ // arm first, loudly), so it is a contract on the next one.
397
+ //
398
+ // `continue;` is deliberately NOT refused: C binds it to the smallest enclosing ITERATION
399
+ // statement, which a `switch` is not, so it already means what L3 means. What a loop-respelling
400
+ // pass must preserve is exactly that the three that can re-spell a loop into one whose
401
+ // `continue` would run a different increment (`recognizeForLoops` in structure.ts,
402
+ // `respellCountdown` in l3/reindex.ts, and l3/unreduce.ts) each scan switch arms for the node
403
+ // before firing, and a fourth must too.
404
+ for (const body of [...s.cases.map((c) => c.body), s.default ?? []]) {
405
+ if (switchBoundBreakIn(body)) {
406
+ throw new Error('c backend: a switch arm carries a loop-scoped `break;`, which C would bind to the switch');
407
+ }
408
+ }
409
+ const out = [`${indent}switch (${pe(s.scrutinee, 99)}) {`];
410
+ const ci = indent + ' '; // case-label indent
411
+ const bi = indent + ' '; // case-body indent
412
+ // `?.length`, not just presence: a label with no statement under it is not valid C89, and an
413
+ // L3 pass (dce, reindex) may empty a default that arrived with statements — the structurer's
414
+ // own "don't attach an empty default" rule cannot see that.
415
+ const hasDefault = !!s.default?.length;
416
+ // Where the `default:` label goes, as a COUNT of case arms before it (l3/ast.ts): absent ⇒
417
+ // after all of them. A count past the arms matches no position at all and the label would
418
+ // simply not be printed — the default arm vanishing from a switch that has one — so a
419
+ // producer that hands one over fails loud, like the falling-arm placement below.
420
+ const defAt = hasDefault ? (s.defaultAt ?? s.cases.length) : -1;
421
+ if (hasDefault && (defAt < 0 || defAt > s.cases.length)) {
422
+ throw new Error(`c backend: a switch places its default at arm ${defAt} of ${s.cases.length}`);
423
+ }
424
+ const printDefault = (): void => {
425
+ out.push(`${ci}default:`);
426
+ for (const t of s.default!) {
427
+ out.push(...printStmt(t, bi, vt, leaf));
428
+ }
429
+ // A default that is NOT last would otherwise fall into the case below it — the mirror of the
430
+ // rule for cases. The last one needs no `break;` because there is nothing under it.
431
+ if (defAt < s.cases.length && !endsTerminated(s.default!)) {
432
+ out.push(`${bi}break;`);
433
+ }
434
+ };
435
+ s.cases.forEach((c, i) => {
436
+ if (i === defAt) {
437
+ // Moving the label in FRONT of a falling arm would divert that arm into the default —
438
+ // a silent control-flow change. Recovery only positions a default among closed arms, so
439
+ // this is a producer bug rather than an input shape: fail loud.
440
+ if (i > 0 && s.cases[i - 1].fallsThrough) {
441
+ throw new Error(
442
+ `c backend: a switch places its default after a case that falls through, which would divert it`,
443
+ );
399
444
  }
400
- out.push(`${indent}}`);
401
- return out;
445
+ printDefault();
446
+ }
447
+ for (const v of c.values) {
448
+ out.push(`${ci}case ${v}:`);
449
+ }
450
+ for (const t of c.body) {
451
+ out.push(...printStmt(t, bi, vt, leaf));
402
452
  }
453
+ // A case whose body ends in `return`/`break` (a terminated arm) needs no `break;`; only an
454
+ // open non-fall-through arm gets one. `fallsThrough` omits it so control drops to the next case.
455
+ if (!c.fallsThrough && !endsTerminated(c.body)) {
456
+ out.push(`${bi}break;`);
457
+ }
458
+ });
459
+ if (defAt === s.cases.length) {
460
+ printDefault();
403
461
  }
462
+ out.push(`${indent}}`);
463
+ return out;
464
+ }
465
+
466
+ /** Does this arm body carry a `break` C would bind to the enclosing SWITCH rather than to the loop
467
+ * L3 meant? A loop opened inside the arm captures its own, so its body is not walked; a nested
468
+ * `switch` captures one too and is checked when it is printed. */
469
+ function switchBoundBreakIn(body: Stmt[]): boolean {
470
+ return body.some(
471
+ (s) => s.k === 'break' || (s.k === 'if' && (switchBoundBreakIn(s.then) || switchBoundBreakIn(s.else))),
472
+ );
404
473
  }
405
474
 
406
475
  // Does a statement list end in a control-flow terminator (so a trailing `break;` would be dead)?
@@ -415,26 +484,15 @@ function endsTerminated(body: Stmt[]): boolean {
415
484
  );
416
485
  }
417
486
 
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
487
  // 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
488
+ // the C family's answer to `writesNonPointerIntoPointer` is the reinterpret cast to the DECLARED
489
+ // type exactly what the machine's register move does where Pascal, having no such cast,
490
+ // declines. (A rebuilding transform with per-kind semantics its own switch, per the l3/ast.ts
428
491
  // traversal-vocabulary exemption.)
429
492
  function legalizePointerWrites(fn: SFn): SFn {
430
493
  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
- };
494
+ const castTo = (t: IrType | undefined, e: Expr): Expr =>
495
+ writesNonPointerIntoPointer(t, e, vt) ? { k: 'cast', to: t, e } : e;
438
496
  const fix = (s: Stmt): Stmt => {
439
497
  switch (s.k) {
440
498
  case 'assign':
@@ -466,14 +524,23 @@ function legalizePointerWrites(fn: SFn): SFn {
466
524
  return { ...fn, body: fn.body.map(fix) };
467
525
  }
468
526
 
527
+ /** The body of a C-family function: local declarations + statements, one string per line. The
528
+ * SIGNATURE (return type + name + params, plus any C++ scope/`this`/mangling) is the caller's —
529
+ * that is the language-divergent part each backend owns. */
469
530
  function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
470
531
  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);
532
+ // The legalization env: every printed var's declared type and pointee volatility, from the SAME
533
+ // params/locals the emitted declarations come from — so the printer judges exactly the C the
534
+ // reader will see.
535
+ const vt: PrintEnv = printEnv(fn);
474
536
  const lines: string[] = [];
475
537
  for (const l of fn.locals) {
476
- lines.push(` ${l.volatile ? 'volatile ' : ''}${cType(l.type)} ${l.name};`);
538
+ // Both facts render at the PREFIX position, where C's declarator grammar reads them
539
+ // differently: on a scalar the qualifier binds to the object (`volatile u16 sp0`), on a
540
+ // pointer declarator to the pointee — the INNERMOST one for a multi-level pointer
541
+ // (`volatile u16 ** p`). An object-volatile POINTER (`u16 *volatile p`) has no inhabitant —
542
+ // no lever or recognizer produces one.
543
+ lines.push(` ${l.volatile || l.pointeeVolatile ? 'volatile ' : ''}${cType(l.type)} ${l.name};`);
477
544
  }
478
545
  for (const s of fn.body) {
479
546
  lines.push(...printStmt(s, ' ', vt, leaf));
@@ -491,7 +558,11 @@ function structDecls(fn: SFn): string[] {
491
558
  }
492
559
 
493
560
  /** 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 {
561
+ export function emitCFamily(signature: string, fn0: SFn, leaf?: LeafHook): string {
562
+ // The declaration list is put into the target's own frame order HERE — in the shared C-family
563
+ // assembler, reached only from `cBackend.emit` and `cppBackend(...).emit`, so both C-family
564
+ // backends order and no `.emit(` call site does (l3/slotorder.ts says why `emit` owns it).
565
+ const fn = orderSlotLocals(fn0);
495
566
  const decls = structDecls(fn);
496
567
  const preamble = decls.length ? decls.join('\n') + '\n' : '';
497
568
  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.
@@ -10,7 +10,8 @@
10
10
  // Turbo/Delphi/FreePascal.
11
11
  import { IrType, typeToString } from '../ir/types';
12
12
  import { BinOp, Expr, LanguageBackend, SFn, Stmt } from '../l3/ast';
13
- import { type VarTypes, declaredTypes, derefStrideOk, exprCType } from '../l3/typing';
13
+ import { orderSlotLocals } from '../l3/slotorder';
14
+ import { type VarTypes, declaredTypes, derefStrideOk, exprCType, writesNonPointerIntoPointer } from '../l3/typing';
14
15
 
15
16
  // Infix operators IDO Pascal spells directly.
16
17
  const OP: Partial<Record<BinOp, string>> = {
@@ -18,6 +19,10 @@ const OP: Partial<Record<BinOp, string>> = {
18
19
  // match C's truncated `%` (sign of the DIVIDEND) — verified: `a mod 3` mis-scores against the
19
20
  // IDO C `a % 3` codegen. There is no faithful IDO-Pascal spelling of a signed C remainder, so the
20
21
  // backend fails LOUD on `%` (below) rather than emit a silently-wrong `mod`. `/`→`div` DOES match.
22
+ //
23
+ // And no `/u`/`%u` either, for the same reason `>>>` is absent from BIT_FN below: `div` over this
24
+ // backend's signed `Integer` is the SIGNED division, so lending it to the unsigned twin would
25
+ // emit `div` where the machine did `divu`. They reach the loud decline instead.
21
26
  '+': '+',
22
27
  '-': '-',
23
28
  '*': '*',
@@ -95,7 +100,7 @@ function makePrinter(vt: VarTypes) {
95
100
  throw new Error(`pascal backend: a multidimensional array access has no IDO Pascal spelling yet`);
96
101
  }
97
102
  const bt = exprCType(e.base, vt);
98
- if ((bt !== undefined && !derefStrideOk(bt, e.width)) || (bt === undefined && e.width !== 4)) {
103
+ if ((bt !== undefined && !derefStrideOk(bt, e.width, e.signed)) || (bt === undefined && e.width !== 4)) {
99
104
  throw new Error(
100
105
  `pascal backend: a ${e.width}-byte access through a base of type '${bt ? typeToString(bt) : '<unknowable>'}' has no faithful spelling (no reinterpret cast)`,
101
106
  );
@@ -111,7 +116,10 @@ function makePrinter(vt: VarTypes) {
111
116
  // Casts have no faithful IDO-Pascal spelling yet — fail LOUD rather than emit silently-wrong
112
117
  // source. Tree-level producers reaching here: the width-narrowing idiom casts (agbcc-gated,
113
118
  // so never on this path today), structure.ts's STRUCT-pointer casts (unreachable too — the
114
- // `field` case above throws first), and intify's `(s32)ptr` legalization (any target).
119
+ // `field` case above throws first), intify's `(s32)ptr` legalization (any target), and the
120
+ // byte-pointer walk of a pointer offset by a runtime value (any target, and reachable with no
121
+ // `field` in the tree — it declines two functions that used to emit here, whose Pascal was
122
+ // silently walking ELEMENTS where the asm walked bytes).
115
123
  // Scalar deref casts never appear in the tree — the index case above owns that judgment.
116
124
  case 'cast':
117
125
  throw new Error(`pascal backend: cast has no IDO Pascal spelling yet`);
@@ -148,15 +156,13 @@ function makePrinter(vt: VarTypes) {
148
156
  stmts.flatMap((x, i) => ps(fnName, x, ind, tl && i === stmts.length - 1));
149
157
  switch (s.k) {
150
158
  case 'assign': {
151
- // The write-side sibling of the index case's deref discipline: Pascal has no reinterpret
152
- // cast, so a definitely-non-pointer value assigned into a pointer-declared var (the shape
153
- // the C family legalizes with `(u8 *)…`, cfamily.ts legalizePointerWrites) declines LOUD
154
- // here instead of failing three stages later in upas.
155
- const dt = vt(s.name);
156
- const ct = exprCType(s.value, vt);
157
- if (dt?.kind === 'ptr' && ct && ct.kind !== 'ptr' && ct.kind !== 'array') {
159
+ // The write-side sibling of the index case's deref discipline. The C family answers the
160
+ // same question (l3/typing.ts writesNonPointerIntoPointer) with a reinterpret cast;
161
+ // Pascal has none, so it declines LOUD here instead of failing three stages later in upas.
162
+ if (writesNonPointerIntoPointer(vt(s.name), s.value, vt)) {
163
+ const ct = exprCType(s.value, vt);
158
164
  throw new Error(
159
- `pascal backend: assigning a ${typeToString(ct)} value into pointer var '${s.name}' has no faithful spelling (no reinterpret cast)`,
165
+ `pascal backend: assigning a ${ct ? typeToString(ct) : '<unknowable>'} value into pointer var '${s.name}' has no faithful spelling (no reinterpret cast)`,
160
166
  );
161
167
  }
162
168
  return [`${indent}${s.name} := ${pe(s.value)};`];
@@ -263,7 +269,15 @@ function makePrinter(vt: VarTypes) {
263
269
 
264
270
  export const pascalBackend: LanguageBackend = {
265
271
  id: 'pascal',
266
- emit(fn: SFn): string {
272
+ // `case-of` has no fall-through, and this file's `switch` printing loud-fails a `fallsThrough`
273
+ // arm. Declared so RECOVERY never mints one for this backend: a comparison-tree switch also
274
+ // spells as plain if-nesting, which Pascal prints, so the choice is between a decompiled
275
+ // function and a stub.
276
+ spellsSwitchFallthrough: false,
277
+ emit(fn0: SFn): string {
278
+ // The declaration list is put into the target's own frame order HERE, as the C family does it
279
+ // in its shared assembler — owned by `emit`, never by a `.emit(` call site (l3/slotorder.ts).
280
+ const fn = orderSlotLocals(fn0);
267
281
  // Same env discipline as the C family (cfamily.ts cFamilyBody): the printer judges derefs
268
282
  // against the exact declarations it emits.
269
283
  const ps = makePrinter(declaredTypes(fn));