@asmlift/core 0.8.1 → 0.10.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 (130) hide show
  1. package/README.md +36 -31
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +397 -0
  4. package/src/backend/c.ts +6 -2
  5. package/src/backend/cfamily.ts +370 -56
  6. package/src/backend/cpp.ts +116 -10
  7. package/src/backend/pascal.ts +69 -11
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +777 -84
  15. package/src/declare.ts +132 -13
  16. package/src/frontend/asmdata.ts +26 -6
  17. package/src/frontend/call-plan.ts +1590 -0
  18. package/src/frontend/dead-reads.ts +628 -0
  19. package/src/frontend/device-pins.ts +296 -0
  20. package/src/frontend/emit.ts +4 -3
  21. package/src/frontend/errors.ts +5 -0
  22. package/src/frontend/flags-edge.ts +101 -0
  23. package/src/frontend/fpu.ts +374 -0
  24. package/src/frontend/frame-objects.ts +3171 -0
  25. package/src/frontend/frontend.ts +64 -3
  26. package/src/frontend/high-half.ts +55 -5
  27. package/src/frontend/jump-table.ts +530 -0
  28. package/src/frontend/local-object.ts +616 -0
  29. package/src/frontend/mips.ts +2392 -208
  30. package/src/frontend/opaque.ts +97 -18
  31. package/src/frontend/own-struct-return.ts +500 -0
  32. package/src/frontend/ppc.ts +3137 -267
  33. package/src/frontend/register-struct-call.ts +484 -0
  34. package/src/frontend/reloc-symbol.ts +109 -13
  35. package/src/frontend/splat.ts +108 -5
  36. package/src/frontend/ssa.ts +2098 -89
  37. package/src/frontend/stackargs.ts +172 -42
  38. package/src/frontend/thumb.ts +4563 -2091
  39. package/src/ir/alias.ts +71 -0
  40. package/src/ir/bits.ts +681 -1
  41. package/src/ir/core.ts +199 -6
  42. package/src/ir/discipline.ts +313 -0
  43. package/src/ir/entry.ts +159 -0
  44. package/src/ir/float-bits.ts +47 -0
  45. package/src/{structure → ir}/loops.ts +6 -2
  46. package/src/ir/opcodes.ts +193 -121
  47. package/src/ir/parse.ts +1 -0
  48. package/src/ir/types.ts +202 -2
  49. package/src/ir/verify.ts +219 -3
  50. package/src/l3/address.ts +5 -0
  51. package/src/l3/argcopy.ts +38 -6
  52. package/src/l3/ast.ts +467 -23
  53. package/src/l3/basecse.ts +33 -5
  54. package/src/l3/coalesce.ts +21 -13
  55. package/src/l3/constinit.ts +382 -0
  56. package/src/l3/dce.ts +34 -12
  57. package/src/l3/gates.ts +9 -3
  58. package/src/l3/hoist.ts +8 -7
  59. package/src/l3/initfirst.ts +68 -19
  60. package/src/l3/intcell.ts +98 -0
  61. package/src/l3/mentions.ts +10 -4
  62. package/src/l3/narrowdecl.ts +259 -0
  63. package/src/l3/nearbase.ts +10 -5
  64. package/src/l3/parkfirst.ts +8 -5
  65. package/src/l3/pollguard.ts +11 -4
  66. package/src/l3/ptrcell.ts +354 -0
  67. package/src/l3/ptrfield.ts +123 -8
  68. package/src/l3/regspell.ts +26 -7
  69. package/src/l3/reindex.ts +293 -103
  70. package/src/l3/storage.ts +98 -1
  71. package/src/l3/symbol-refs.ts +182 -23
  72. package/src/l3/tailmerge.ts +9 -20
  73. package/src/l3/typing.ts +237 -32
  74. package/src/l3/unreduce.ts +10 -9
  75. package/src/l3/volatileptr.ts +17 -54
  76. package/src/l3/volstore.ts +96 -7
  77. package/src/mangle.ts +41 -1
  78. package/src/pattern/engine.ts +373 -82
  79. package/src/pipeline.ts +589 -27
  80. package/src/proto-context.ts +1648 -0
  81. package/src/proto.ts +1203 -49
  82. package/src/raise/aggmembers.ts +290 -0
  83. package/src/raise/arrays.ts +8 -3
  84. package/src/raise/declared-volatile.ts +307 -0
  85. package/src/raise/divpow2.ts +2 -2
  86. package/src/raise/errors.ts +17 -0
  87. package/src/raise/floathelpers.ts +464 -0
  88. package/src/raise/globalshape.ts +11 -5
  89. package/src/raise/gvn.ts +29 -8
  90. package/src/raise/latch.ts +162 -6
  91. package/src/raise/memberarrays.ts +10 -4
  92. package/src/raise/narrow.ts +29 -2
  93. package/src/raise/narrowlocal.ts +11 -6
  94. package/src/raise/offsetnames.ts +494 -0
  95. package/src/raise/pairparams.ts +114 -0
  96. package/src/raise/paramwidth.ts +203 -6
  97. package/src/raise/pre-recovery.ts +174 -17
  98. package/src/raise/pre-structure.ts +54 -0
  99. package/src/raise/recover.ts +26 -15
  100. package/src/raise/reroll.ts +694 -0
  101. package/src/raise/retsink.ts +1 -1
  102. package/src/raise/shortcircuit.ts +66 -30
  103. package/src/raise/softdiv.ts +21 -23
  104. package/src/raise/storesign.ts +153 -0
  105. package/src/raise/struct-arrays.ts +34 -14
  106. package/src/raise/structs.ts +674 -98
  107. package/src/raise/truncload.ts +367 -0
  108. package/src/raise/unthread.ts +400 -0
  109. package/src/raise/widehelpers.ts +106 -0
  110. package/src/rank-declare.ts +143 -12
  111. package/src/rank-variations.ts +117 -31
  112. package/src/rank.ts +563 -122
  113. package/src/runtime-helpers.ts +405 -0
  114. package/src/stillborn.ts +289 -0
  115. package/src/structure/analysis.ts +1449 -449
  116. package/src/structure/arith-ops.ts +23 -0
  117. package/src/structure/globalaccess.ts +111 -9
  118. package/src/structure/hazards.ts +641 -114
  119. package/src/structure/local-statics.ts +517 -0
  120. package/src/structure/namecoalesce.ts +2 -2
  121. package/src/structure/pointer-spelling.ts +1106 -0
  122. package/src/structure/redundant-test.ts +99 -0
  123. package/src/structure/structure.ts +3478 -975
  124. package/src/structure/switch-recover.ts +403 -178
  125. package/src/symbols.ts +44 -3
  126. package/src/target.ts +1387 -71
  127. package/src/trace.ts +66 -28
  128. package/src/variation-definitions.ts +243 -13
  129. package/src/variation-gates.ts +2 -0
  130. package/src/variation-tokens.ts +25 -7
@@ -10,8 +10,11 @@
10
10
  // below the signature is this file's: declarations, statements, expressions, precedence, the
11
11
  // legalizing casts, and the recovered-struct declaration spelling (which is why that lives here
12
12
  // too, shared with the scoring layer's synthesized declarations so the two cannot drift).
13
- import { IrType, T, scalarTypeForAccess, typeToString } from '../ir/types';
14
- import { BinOp, Expr, SFn, Stmt, dotBase } from '../l3/ast';
13
+ import { doubleLiteral } from '../ir/float-bits';
14
+ import { IrType, T, scalarTypeForAccess, typeEquals, typeToString } from '../ir/types';
15
+ import { BinOp, Expr, InitAddress, SFn, SStatic, Stmt, dotBase } from '../l3/ast';
16
+ import { foldConstantImages } from '../l3/constinit';
17
+ import { legalizePointerCells } from '../l3/ptrcell';
15
18
  import { orderSlotLocals } from '../l3/slotorder';
16
19
  import {
17
20
  type PrintEnv,
@@ -20,14 +23,26 @@ import {
20
23
  declaredTypes,
21
24
  derefStrideOk,
22
25
  exprCType,
26
+ exprIntWidth,
23
27
  printEnv,
24
28
  renderedIntSignedness,
25
29
  writesNonPointerIntoPointer,
26
30
  } from '../l3/typing';
31
+ import { spellableType } from '../proto';
27
32
 
28
33
  // C operator precedence (lower binds tighter). Used to emit MINIMAL parentheses. Shared: C++ has
29
34
  // the same precedence for these operators.
30
35
  const PREC: Record<BinOp, number> = {
36
+ 'f*': 3,
37
+ 'f/': 3,
38
+ 'f+': 4,
39
+ 'f-': 4,
40
+ 'f<': 6,
41
+ 'f<=': 6,
42
+ 'f>': 6,
43
+ 'f>=': 6,
44
+ 'f==': 7,
45
+ 'f!=': 7,
31
46
  '*': 3,
32
47
  '/': 3,
33
48
  '/u': 3, // the unsigned twins spell as C's `/` and `%` (see C_SPELLING) — same precedence
@@ -64,17 +79,44 @@ const C_SPELLING: Partial<Record<BinOp, { token: string; signed: boolean }>> = {
64
79
  '%u': { token: '%', signed: false },
65
80
  };
66
81
 
82
+ /** The float operators (l3/ast.ts BinOp) spell as C's own tokens and take no operand pin: C's
83
+ * arithmetic over two floats is the IEEE operation the machine did, and its relations over two
84
+ * floats are the IEEE compares — false on a NaN, `!=` true. */
85
+ const FLOAT_TOKEN: Partial<Record<BinOp, string>> = {
86
+ 'f+': '+',
87
+ 'f-': '-',
88
+ 'f*': '*',
89
+ 'f/': '/',
90
+ 'f<': '<',
91
+ 'f<=': '<=',
92
+ 'f>': '>',
93
+ 'f>=': '>=',
94
+ 'f==': '==',
95
+ 'f!=': '!=',
96
+ };
97
+
67
98
  /** Spell a recovered type in the decomp C-family typedef vocabulary (`s32`/`u32`/`u8`/`T *`). */
68
99
  export function cType(t: IrType): string {
69
100
  if (t.kind === 'ptr') {
70
101
  return `${cType(t.to)} *`;
71
102
  }
72
103
  if (t.kind === 'struct') {
73
- return `struct ${t.name}`;
104
+ return t.declared ?? `struct ${t.name}`;
105
+ }
106
+ // Declared inline, where the struct member that holds it is declared (ir/types.ts).
107
+ if (t.kind === 'union') {
108
+ return `union { ${t.members.map((m) => `${cDeclare(m.type, m.name)};`).join(' ')} }`;
74
109
  }
75
110
  if (t.kind === 'array') {
76
111
  return `${cType(t.elem)}[${t.count}]`;
77
112
  } // ill-formed as a prefix; use cDeclare
113
+ // The C89 KEYWORDS, not the `f32`/`f64` typedefs a decomp project declares: the candidate prelude
114
+ // (`C_TYPEDEFS`) holds the integer family only, and a keyword needs no declaration in any
115
+ // translation unit — so no project context that already typedefs `f32` can collide with it.
116
+ // A float of no stated precision compiles the same under either keyword; `float` is its spelling.
117
+ if (t.kind === 'float') {
118
+ return t.width === 64 ? 'double' : 'float';
119
+ }
78
120
  return typeToString(t); // s32 / u32 / u8 / unk32 (treated as s32 upstream)
79
121
  }
80
122
 
@@ -83,7 +125,7 @@ export function cType(t: IrType): string {
83
125
  * the prefix `cType name`. A NESTED array spells every extent after the name in declaration
84
126
  * order (`u8 unk8[6][8]`) — one declarator, not an element type that is itself an array, which
85
127
  * C has no syntax for and `cType` marks ill-formed as a prefix. */
86
- function cDeclare(t: IrType, name: string): string {
128
+ export function cDeclare(t: IrType, name: string): string {
87
129
  if (t.kind === 'array') {
88
130
  const extents: number[] = [];
89
131
  let e: IrType = t;
@@ -113,10 +155,11 @@ export interface StructFieldDecl {
113
155
  bits?: number;
114
156
  }
115
157
 
116
- /** THE struct-declaration spelling — every `struct N { ... };` asmlift prints comes from here,
117
- * so the backend's recovered-struct decls and the scoring layer's synthesized decls cannot
118
- * drift apart. One line, fields in caller order (the type is self-describing: padding is the
119
- * caller's discipline, already present as real fields). */
158
+ /** THE struct-declaration spelling — every `struct N { ... };` asmlift recovers or synthesizes
159
+ * comes from here, so the backend's recovered-struct decls and the scoring layer's synthesized
160
+ * decls cannot drift apart. A struct the headers own is transcribed from their declaration
161
+ * instead (declare.ts `declaredStructDecl`). One line, fields in caller order (the type is
162
+ * self-describing: padding is the caller's discipline, already present as real fields). */
120
163
  export function renderStructDecl(name: string, fields: StructFieldDecl[]): string {
121
164
  const one = (f: StructFieldDecl) =>
122
165
  `${f.volatile ? 'volatile ' : ''}${cDeclare(f.type, f.name)}${f.bits !== undefined ? ` : ${f.bits}` : ''};`;
@@ -166,18 +209,23 @@ function legalizedIndexBase(ix: Extract<Expr, { k: 'index' }>, vt: PrintEnv): Ex
166
209
  };
167
210
  }
168
211
 
169
- /** The 32-bit integer cast that PINS an operand's signedness.
212
+ /** The integer cast that PINS an operand's signedness, AT THE OPERAND'S OWN RANK.
170
213
  *
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;
214
+ * The width is not a constant: `(s32)a / (s32)b` over two `s64`s does not pin a signedness, it
215
+ * truncates the operation to 32 bits and calls a different helper than the machine did. So the
216
+ * rank comes from `exprIntWidth`, which is total and answers 64 only where it can prove it.
217
+ *
218
+ * An existing integer cast AT THE SAME WIDTH is REPLACED rather than wrapped — `(u32)(s32)&g` and
219
+ * `(u32)&g` are the same bytes, and the arithmetic rules upstream do emit that inner cast
220
+ * (structure/pointer-spelling.ts ARITH_ROWS). A cast at a DIFFERENT width is a narrowing the
221
+ * source wrote and must be kept. The replacement CARRIES the qualifier: re-typing a `volatile`
222
+ * cast without it drops an assertion the differ cannot referee the loss of, which is why
223
+ * l3/initfirst.ts's `stripWideIntCast` refuses the same peel one pass over. */
224
+ function recastInt(x: Expr, signed: boolean, width: 32 | 64): Expr {
225
+ const replaced = x.k === 'cast' && x.to.kind === 'int' && x.to.width === width ? x : undefined;
178
226
  return {
179
227
  k: 'cast',
180
- to: T.int(32, signed),
228
+ to: T.int(width, signed),
181
229
  ...(replaced?.volatile === true ? { volatile: true as const } : {}),
182
230
  e: replaced ? replaced.e : x,
183
231
  };
@@ -205,14 +253,66 @@ function recast32(x: Expr, signed: boolean): Expr {
205
253
  * the division unsigned. Verified by compiling: `((u32)a / b) / 7` calls `__udivsi3` twice,
206
254
  * `(s32)((u32)a / b) / 7` calls `__udivsi3` then `__divsi3`. */
207
255
  function pinnedOperands(e0: Extract<Expr, { k: 'bin' }>, wantSigned: boolean, vt: PrintEnv): [Expr, Expr] {
256
+ // A shift reads its LEFT operand's rank; a divide reads the wider of the two, which is what the
257
+ // usual arithmetic conversions make the operation's own rank.
208
258
  if (e0.op === '>>' || e0.op === '>>>') {
209
- return [renderedIntSignedness(e0.l, vt.type) === wantSigned ? e0.l : recast32(e0.l, wantSigned), e0.r];
259
+ const w = exprIntWidth(e0.l, vt.type);
260
+ return [renderedIntSignedness(e0.l, vt.type) === wantSigned ? e0.l : recastInt(e0.l, wantSigned, w), e0.r];
210
261
  }
211
262
  if (arithConversionSignedness(e0.l, e0.r, vt.type) === wantSigned) {
212
263
  return [e0.l, e0.r];
213
264
  }
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];
265
+ const w = exprIntWidth(e0.l, vt.type) === 64 || exprIntWidth(e0.r, vt.type) === 64 ? 64 : 32;
266
+ const pinSigned = (x: Expr): Expr => (renderedIntSignedness(x, vt.type) === true ? x : recastInt(x, true, w));
267
+ return wantSigned ? [pinSigned(e0.l), pinSigned(e0.r)] : [recastInt(e0.l, false, w), e0.r];
268
+ }
269
+
270
+ /** Does passing `a` to a parameter declared `to` take a conversion C++ will not make implicitly?
271
+ * Three do, and C makes each of them implicitly, so the cast spells the conversion C would have
272
+ * made and the compiled code is C's: an object pointer to a pointer of another pointee (a conversion
273
+ * to `void *`, or one that only adds a qualifier, is implicit in C++ too), an integer to a pointer
274
+ * (the literal `0` is the one integer that converts), and a pointer to an integer. Only reached on a
275
+ * function compiled as C++ (`SFn.dialect`), or by a C front end that refuses the same three
276
+ * (`SFn.strictConversions` — mwcc). */
277
+ function argConversion(a: Expr, to: string, vt: PrintEnv): boolean {
278
+ const from = exprCType(a, vt.type);
279
+ // qualifiers never decide it: a `T * const` parameter is a pointer parameter
280
+ const bare = to
281
+ .replace(/\b(?:const|volatile)\b/g, ' ')
282
+ .replace(/\s+/g, ' ')
283
+ .trim();
284
+ if (bare.endsWith('*')) {
285
+ if (a.k === 'const' && a.value === 0) {
286
+ return false;
287
+ }
288
+ if (from?.kind !== 'ptr') {
289
+ return true;
290
+ }
291
+ return bare !== 'void *' && bare !== cType(from).replace(/\s+/g, ' ').trim();
292
+ }
293
+ return from?.kind === 'ptr';
294
+ }
295
+
296
+ /** The unqualified pointer a `const` static's address is spelled as where it reaches a place that
297
+ * converts it implicitly — an argument, an assigned or stored value, a returned one — or null for
298
+ * any other expression. The static is `const` because the target holds it in read-only data, not
299
+ * because the source's pointer types say so: mwcc rejects the implicit `const u8[4]` → `u8 *` a
300
+ * bare `g(tbl)` makes (compiled), and a pointer cast changes no instruction. */
301
+ function constEscape(e: Expr, vt: PrintEnv): IrType | null {
302
+ if (e.k === 'addr' && vt.constStatic(e.name)) {
303
+ const t = vt.type(e.name);
304
+ return t === undefined ? null : T.ptr(t.kind === 'array' ? t.elem : t);
305
+ }
306
+ if (e.k === 'bin' && (e.op === '+' || e.op === '-')) {
307
+ return constEscape(e.l, vt) ?? (e.op === '+' ? constEscape(e.r, vt) : null);
308
+ }
309
+ return null;
310
+ }
311
+
312
+ /** `e` printed where C converts it implicitly, with a `const` static's address cast (constEscape). */
313
+ function printConverted(e: Expr, vt: PrintEnv, leaf?: LeafHook): string {
314
+ const to = constEscape(e, vt);
315
+ return to === null ? printExpr(e, 99, vt, leaf) : `(${cType(to)})${printExpr(e, 2, vt, leaf)}`;
216
316
  }
217
317
 
218
318
  function printExpr(e: Expr, parentPrec: number, vt: PrintEnv, leaf?: LeafHook): string {
@@ -228,14 +328,59 @@ function printExpr(e: Expr, parentPrec: number, vt: PrintEnv, leaf?: LeafHook):
228
328
  return e.name;
229
329
  case 'const':
230
330
  return String(e.value);
331
+ // a negative literal is a prefix `-`, and parenthesizes where one would (`un` below)
332
+ case 'fconst': {
333
+ const s = doubleLiteral(e.bits);
334
+ return parentPrec < 2 && s.startsWith('-') ? `(${s})` : s;
335
+ }
231
336
  case 'addr': {
232
- // `&gSym` — the address of a named global. A prefix operator; parenthesizes under a POSTFIX
233
- // parent like the other prefix forms.
337
+ // `&gSym` — the address of a named global or of a frame-local object. A prefix operator;
338
+ // parenthesizes under a POSTFIX parent like the other prefix forms.
339
+ //
340
+ // AN ARRAY DECAYS INSTEAD, and the declared type is what says so, which is why the decision
341
+ // is here rather than on the node: `&` on `u8 sp0[16]` spells a `u8 (*)[16]` — a different
342
+ // type for the same byte, which every typed pointer parameter rejects (`passing arg 1 from
343
+ // incompatible pointer type`) — where the bare name is the `u8 *` the machine produced.
344
+ // Compiled both ways, the object is identical, so the `&` buys a diagnostic and nothing
345
+ // else. An identifier binds tighter than every parent, so the decayed form never
346
+ // parenthesizes. A name this env cannot type keeps the `&`: an unknown shape is not an
347
+ // array, and dropping the operator on a scalar would spell its value.
348
+ if (vt.type(e.name)?.kind === 'array') {
349
+ return e.name;
350
+ }
234
351
  const g = `&${e.name}`;
235
352
  return parentPrec < 2 ? `(${g})` : g;
236
353
  }
237
- case 'call':
238
- return `${e.fn}(${e.args.map((a) => rec(a, 99)).join(', ')})`;
354
+ case 'call': {
355
+ if (typeof e.fn !== 'string') {
356
+ // An unprototyped pointer to a function returning the call's own rank: C passes each
357
+ // argument as its promoted self, which is what a call with no declaration passes.
358
+ const ret = cType(T.int(e.wide64 ? 64 : 32, true));
359
+ return `((${ret} (*)())${rec(e.fn, 2)})(${e.args.map((a) => printConverted(a, vt, leaf)).join(', ')})`;
360
+ }
361
+ if (e.sret !== undefined) {
362
+ throw new Error(`the call to '${e.fn}' returns a struct through memory and is spelled only as a statement`);
363
+ }
364
+ const declared = vt.declaredArgs(e.fn);
365
+ return `${e.fn}(${e.args
366
+ .map((a, i) => {
367
+ const to = declared?.[i];
368
+ // A parameter type this cannot spell is an object pointer (target.ts `declaredArgTypes`),
369
+ // reached through `void *` from an argument whose own type is known to differ — a value
370
+ // this function types, or the address of an object it declares (a static) — never from
371
+ // `&gW` of a global only the project declares, which may already be the parameter's type.
372
+ const opaque = to !== undefined && !spellableType(to);
373
+ const typed = exprCType(a, vt.type) !== undefined || (a.k === 'addr' && vt.type(a.name) !== undefined);
374
+ return to !== undefined && (!opaque || typed) && argConversion(a, to, vt)
375
+ ? `(${opaque ? 'void *' : to})${rec(a, 2)}`
376
+ : printConverted(a, vt, leaf);
377
+ })
378
+ .join(', ')})`;
379
+ }
380
+ // `v++` / `v--`. Postfix over a bare identifier, so it binds tighter than every parent and
381
+ // never self-parenthesizes.
382
+ case 'postincr':
383
+ return `${e.name}${e.by === 1 ? '++' : '--'}`;
239
384
  case 'index': {
240
385
  // `*base` for the zero offset (a PREFIX operator, so a prefix-shaped base like a cast needs
241
386
  // no parens: `*(u8 *)p` — but the whole form must self-parenthesize under a POSTFIX parent:
@@ -267,7 +412,7 @@ function printExpr(e: Expr, parentPrec: number, vt: PrintEnv, leaf?: LeafHook):
267
412
  }
268
413
  return `${rec(base, 1)}${e.lead.map((l) => `[${rec(l, 99)}]`).join('')}[${rec(e.idx, 99)}]`;
269
414
  }
270
- if (e.idx.k === 'const' && e.idx.value === 0) {
415
+ if (e.idx.k === 'const' && e.idx.value === 0 && e.subscript !== true) {
271
416
  const s = `*${rec(base, 2)}`;
272
417
  return parentPrec < 2 ? `(${s})` : s;
273
418
  }
@@ -306,7 +451,8 @@ function printExpr(e: Expr, parentPrec: number, vt: PrintEnv, leaf?: LeafHook):
306
451
  // A prefix operator: parenthesize under a POSTFIX parent (`(-a)[1]`), and parenthesize a
307
452
  // same-op nested `-` (`-(-a)`, never `--a` — C lexes that as predecrement).
308
453
  const inner = rec(e.e, 2);
309
- const s = `${e.op}${e.op === '-' && inner.startsWith('-') ? `(${inner})` : inner}`;
454
+ const op = e.op === 'f-' ? '-' : e.op;
455
+ const s = `${op}${op === '-' && inner.startsWith('-') ? `(${inner})` : inner}`;
310
456
  return parentPrec < 2 ? `(${s})` : s;
311
457
  }
312
458
  // A gap marker spells as a call to the UNDEFINED macro ASMLIFT_ERROR("reason", args…) — the
@@ -331,25 +477,41 @@ function printExpr(e: Expr, parentPrec: number, vt: PrintEnv, leaf?: LeafHook):
331
477
  // operand cast that says which of the pair it is.
332
478
  const pair = C_SPELLING[e.op];
333
479
  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)}`;
480
+ const s = `${rec(l, p)} ${pair ? pair.token : (FLOAT_TOKEN[e.op] ?? e.op)} ${rec(r, p - 1)}`;
335
481
  return p > parentPrec ? `(${s})` : s;
336
482
  }
337
483
  }
338
484
  }
339
485
 
486
+ /** `dest = fn(args)` for a call whose `args[0]` is the address of the struct it returns: a local
487
+ * declared with that struct type, named by its address. Any other destination has no spelling
488
+ * that hands the callee the same pointer — compiled, `*(struct Blob64 *)&gDst = makeblob(gBlob);`
489
+ * is a frame temp and a `memcpy`, where `gDst = makeblob(gBlob);` hands over `&gDst` itself. */
490
+ function structReturnCall(call: Extract<Expr, { k: 'call' }>, vt: PrintEnv, leaf?: LeafHook): string {
491
+ const [dest, ...rest] = call.args;
492
+ const declared = dest?.k === 'addr' ? vt.type(dest.name) : undefined;
493
+ if (dest?.k !== 'addr' || declared === undefined || !typeEquals(declared, call.sret!)) {
494
+ throw new Error(`the call to '${call.fn}' returns a struct into something other than a local of its type`);
495
+ }
496
+ return `${dest.name} = ${printExpr({ k: 'call', fn: call.fn, args: rest }, 99, vt, leaf)}`;
497
+ }
498
+
340
499
  function printStmt(s: Stmt, indent: string, vt: PrintEnv, leaf?: LeafHook): string[] {
341
500
  const pe = (e: Expr, p: number) => printExpr(e, p, vt, leaf);
342
501
  switch (s.k) {
343
502
  case 'assign':
344
- return [`${indent}${s.name} = ${pe(s.value, 99)};`];
503
+ return [`${indent}${s.name} = ${printConverted(s.value, vt, leaf)};`];
345
504
  case 'store':
346
505
  // The lvalue is a full Expr (`index` or `field`), so the leaf hook spells a member write
347
506
  // (`this->x = …`) exactly as it spells a member read.
348
- return [`${indent}${pe(s.lval, 2)} = ${pe(s.value, 99)};`];
507
+ return [`${indent}${pe(s.lval, 2)} = ${printConverted(s.value, vt, leaf)};`];
349
508
  case 'exprstmt':
509
+ if (s.value.k === 'call' && s.value.sret !== undefined) {
510
+ return [`${indent}${structReturnCall(s.value, vt, leaf)};`];
511
+ }
350
512
  return [`${indent}${pe(s.value, 99)};`];
351
513
  case 'return':
352
- return [`${indent}return${s.value ? ' ' + pe(s.value, 99) : ''};`];
514
+ return [`${indent}return${s.value ? ' ' + printConverted(s.value, vt, leaf) : ''};`];
353
515
  case 'if': {
354
516
  const cond = pe(s.cond, 99);
355
517
  if (s.then.length === 1 && s.else.length === 0 && s.then[0].k !== 'if') {
@@ -374,6 +536,7 @@ function printStmt(s: Stmt, indent: string, vt: PrintEnv, leaf?: LeafHook): stri
374
536
  return out;
375
537
  }
376
538
  case 'while': {
539
+ refuseMisboundBreak(s.body, 'loop');
377
540
  const out = [`${indent}while (${pe(s.cond, 99)}) {`];
378
541
  for (const t of s.body) {
379
542
  out.push(...printStmt(t, indent + ' ', vt, leaf));
@@ -382,6 +545,7 @@ function printStmt(s: Stmt, indent: string, vt: PrintEnv, leaf?: LeafHook): stri
382
545
  return out;
383
546
  }
384
547
  case 'dowhile': {
548
+ refuseMisboundBreak(s.body, 'loop');
385
549
  const out = [`${indent}do {`];
386
550
  for (const t of s.body) {
387
551
  out.push(...printStmt(t, indent + ' ', vt, leaf));
@@ -394,6 +558,7 @@ function printStmt(s: Stmt, indent: string, vt: PrintEnv, leaf?: LeafHook): stri
394
558
  // `recognizeForLoops` and l3/reindex.ts): init/inc are each a SINGLE-LINE `assign` statement. `clause` renders one
395
559
  // and strips its trailing `;` so it sits inside the header (`i = 0; c; i = i + 1`). A multi-line
396
560
  // statement (an `if`/nested loop) would render mangled — but the recognizer never builds one here.
561
+ refuseMisboundBreak(s.body, 'loop');
397
562
  const clause = (st: Stmt) => printStmt(st, '', vt, leaf).join(' ').replace(/;\s*$/, '').trim();
398
563
  const out = [`${indent}for (${clause(s.init)}; ${pe(s.cond, 99)}; ${clause(s.inc)}) {`];
399
564
  for (const t of s.body) {
@@ -417,11 +582,12 @@ function printStmt(s: Stmt, indent: string, vt: PrintEnv, leaf?: LeafHook): stri
417
582
  * is one call. */
418
583
  function printSwitchStmt(s: Extract<Stmt, { k: 'switch' }>, indent: string, vt: PrintEnv, leaf?: LeafHook): string[] {
419
584
  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
585
+ // A plain `break;` in an arm BODY is the innermost LOOP's in L3 (l3/ast.ts) and the SWITCH's in
586
+ // C, so printing one between `case` labels rebinds it — a changed program that reads as ordinary
422
587
  // 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.
588
+ // the two switch regimes (`refuseMisboundBreak`, which checks a loop body the same way for a
589
+ // `break` marked `ofSwitch`). The structurer spells a loop exit inside an arm as the switch's
590
+ // own `break` or refuses it, so no recovery reaches either today: a contract on the next one.
425
591
  //
426
592
  // `continue;` is deliberately NOT refused: C binds it to the smallest enclosing ITERATION
427
593
  // statement, which a `switch` is not, so it already means what L3 means. What a pass that re-spells
@@ -430,9 +596,7 @@ function printSwitchStmt(s: Extract<Stmt, { k: 'switch' }>, indent: string, vt:
430
596
  // `respellCountdown` in l3/reindex.ts, and l3/unreduce.ts) each scan switch arms for the node
431
597
  // before firing, and a fourth must too.
432
598
  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
- }
599
+ refuseMisboundBreak(body, 'switch');
436
600
  }
437
601
  const out = [`${indent}switch (${pe(s.scrutinee, 99)}) {`];
438
602
  const ci = indent + ' '; // case-label indent
@@ -491,13 +655,26 @@ function printSwitchStmt(s: Extract<Stmt, { k: 'switch' }>, indent: string, vt:
491
655
  return out;
492
656
  }
493
657
 
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
- );
658
+ /** L3 has two `break`s (l3/ast.ts) and C one: C binds `break;` to the innermost enclosing loop OR
659
+ * switch, so a `break` prints as what L3 meant only where its own kind is that innermost
660
+ * statement. `body` is a loop body or a switch arm (`owner`); a loop or switch opened inside it
661
+ * captures its own breaks and is checked when it is printed, so it is not walked. Refused loud on
662
+ * either mismatch — a plain (loop) `break` in an arm, a switch's `break` in a loop body — since
663
+ * each prints as ordinary C that leaves a different statement. */
664
+ function refuseMisboundBreak(body: Stmt[], owner: 'loop' | 'switch'): void {
665
+ const misbound = (ss: Stmt[]): boolean =>
666
+ ss.some(
667
+ (s) =>
668
+ (s.k === 'break' && (s.ofSwitch === true) !== (owner === 'switch')) ||
669
+ (s.k === 'if' && (misbound(s.then) || misbound(s.else))),
670
+ );
671
+ if (misbound(body)) {
672
+ throw new Error(
673
+ owner === 'switch'
674
+ ? 'c backend: a switch arm carries a loop-scoped `break;`, which C would bind to the switch'
675
+ : 'c backend: a loop body carries a switch-scoped `break;`, which C would bind to the loop',
676
+ );
677
+ }
501
678
  }
502
679
 
503
680
  // Does a statement list end in a control-flow terminator (so a trailing `break;` would be dead)?
@@ -512,6 +689,22 @@ function endsTerminated(body: Stmt[]): boolean {
512
689
  );
513
690
  }
514
691
 
692
+ /** Is `e` a pointer to another pointee than the pointer slot `t` declares, as rendered? Its pointer
693
+ * type is its C type, or, for the address of a declared object (which `exprCType` leaves
694
+ * unknown), a pointer to that object or to an array's element, the way it decays. A `void *` slot
695
+ * takes every object pointer implicitly. A `void *` value converts into any pointer slot in C
696
+ * (mwcc's C takes `gp = (void *)x` into an `s32 *gp`, compiled) but not in C++ (`cxx`). An unknown
697
+ * type is no opinion. */
698
+ function otherPointee(t: IrType | undefined, e: Expr, vt: (name: string) => IrType | undefined, cxx: boolean): boolean {
699
+ if (t?.kind !== 'ptr' || t.to.kind === 'void') {
700
+ return false;
701
+ }
702
+ const of = e.k === 'addr' ? vt(e.name) : undefined;
703
+ const ct = of !== undefined ? T.ptr(of.kind === 'array' ? of.elem : of) : exprCType(e, vt);
704
+ const p = ct?.kind === 'array' ? T.ptr(ct.elem) : ct;
705
+ return p?.kind === 'ptr' && (cxx || p.to.kind !== 'void') && !typeEquals(p, t);
706
+ }
707
+
515
708
  // C-FAMILY WRITE LEGALIZATION (the assign-side sibling of the deref legalization in printExpr):
516
709
  // the C family's answer to `writesNonPointerIntoPointer` is the reinterpret cast to the DECLARED
517
710
  // type — exactly what the machine's register move does — where Pascal, having no such cast,
@@ -519,8 +712,21 @@ function endsTerminated(body: Stmt[]): boolean {
519
712
  // traversal-vocabulary exemption.)
520
713
  function legalizePointerWrites(fn: SFn): SFn {
521
714
  const vt = declaredTypes(fn);
715
+ // …and the mirror: a rendered pointer into an INTEGER slot takes the slot's type. A cast keeps
716
+ // the sum it wraps, so the bytes are the uncast write's; CodeWarrior rejects that write outright.
717
+ //
718
+ // …and, where the compiler refuses the third implicit conversion too (C++, or mwcc's C:
719
+ // `SFn.strictConversions`), a pointer of ANOTHER pointee into a pointer slot: mwcc rejects
720
+ // `return t;` from an `s32 *` function when `t` is a `const u32[2]` static (mwcc: "illegal implicit
721
+ // conversion" of an `unsigned int *` to an `int *`, compiled), and takes it cast (`otherPointee`
722
+ // says which `void *` converts implicitly).
723
+ const strict = fn.dialect === 'c++' || fn.strictConversions === true;
522
724
  const castTo = (t: IrType | undefined, e: Expr): Expr =>
523
- writesNonPointerIntoPointer(t, e, vt) ? { k: 'cast', to: t, e } : e;
725
+ writesNonPointerIntoPointer(t, e, vt) ||
726
+ (t?.kind === 'int' && exprCType(e, vt)?.kind === 'ptr') ||
727
+ (strict && otherPointee(t, e, vt, fn.dialect === 'c++'))
728
+ ? { k: 'cast', to: t!, e }
729
+ : e;
524
730
  const fix = (s: Stmt): Stmt => {
525
731
  switch (s.k) {
526
732
  case 'assign':
@@ -555,20 +761,28 @@ function legalizePointerWrites(fn: SFn): SFn {
555
761
  /** The body of a C-family function: local declarations + statements, one string per line. The
556
762
  * SIGNATURE (return type + name + params, plus any C++ scope/`this`/mangling) is the caller's —
557
763
  * that is the language-divergent part each backend owns. */
558
- function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
764
+ function cFamilyBody(fn0: SFn, leaf: LeafHook | undefined, designators: boolean): string[] {
559
765
  const fn = legalizePointerWrites(fn0);
560
766
  // The legalization env: every printed var's declared type and pointee volatility, from the SAME
561
767
  // params/locals the emitted declarations come from — so the printer judges exactly the C the
562
768
  // reader will see.
563
769
  const vt: PrintEnv = printEnv(fn);
564
770
  const lines: string[] = [];
771
+ for (const st of fn.statics ?? []) {
772
+ lines.push(...staticDefinition(st));
773
+ }
565
774
  for (const l of fn.locals) {
566
775
  // Both facts render at the PREFIX position, where C's declarator grammar reads them
567
776
  // differently: on a scalar the qualifier binds to the object (`volatile u16 sp0`), on a
568
777
  // 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};`);
778
+ // (`volatile u16 **p`). An object-volatile POINTER (`u16 *volatile p`) has no inhabitant —
779
+ // no variation or recognizer produces one. The NAME is placed by `cDeclare`, the one
780
+ // placement every declaration position in this backend uses: a local can carry an array type
781
+ // — the storage extent a frame object pins — and C puts those extents after the name.
782
+ const head = ` ${l.volatile || l.pointeeVolatile ? 'volatile ' : ''}${cDeclare(l.type, l.name)}`;
783
+ lines.push(
784
+ ...(l.init === undefined ? [`${head};`] : initialized(head, l.type, l.init, l.initString === true, designators)),
785
+ );
572
786
  }
573
787
  for (const s of fn.body) {
574
788
  lines.push(...printStmt(s, ' ', vt, leaf));
@@ -576,6 +790,96 @@ function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
576
790
  return lines;
577
791
  }
578
792
 
793
+ /** One initial number: hex from 10 up, a negative one with its sign, so a byte table reads as the
794
+ * bytes and a signed one as its values. */
795
+ const initNumber = (v: number): string =>
796
+ Math.abs(v) < 10 ? `${v}` : `${v < 0 ? '-' : ''}0x${Math.abs(v).toString(16)}`;
797
+
798
+ /** One initial element of type `elem`: a number, or the address constant a relocated one holds,
799
+ * converted to the element's integer type — `(u32)&sym`, and with an addend `(u32)((u8 *)&sym + k)`:
800
+ * the offset in bytes, as the relocation counts it. Not `(u32)&sym + k`, which agbcc folds alike but
801
+ * mwcc refuses as an illegal constant expression (compiled); through a byte pointer both write the
802
+ * pointer-typed source's word and relocation (`&garr[2]`, `&garr[-1]`, compiled on both). */
803
+ function initElement(v: number | InitAddress, elem: IrType): string {
804
+ if (typeof v === 'number') {
805
+ return initNumber(v);
806
+ }
807
+ const t = cType(elem);
808
+ return v.addend === 0
809
+ ? `(${t})&${v.symbol}`
810
+ : `(${t})((u8 *)&${v.symbol} ${v.addend < 0 ? '-' : '+'} ${initNumber(Math.abs(v.addend))})`;
811
+ }
812
+
813
+ /** A byte array's initial elements as a string literal: printable characters as themselves, every
814
+ * other byte in three-digit octal (which no following digit can extend), and a `?` after a `?`
815
+ * escaped so no trigraph forms. The trailing zeros are left to the declared size, which C fills
816
+ * with zeros. */
817
+ function stringLiteral(init: readonly number[]): string {
818
+ let end = init.length;
819
+ while (end > 0 && (init[end - 1] & 0xff) === 0) {
820
+ end--;
821
+ }
822
+ let out = '';
823
+ for (const v of init.slice(0, end)) {
824
+ const b = v & 0xff;
825
+ const c = String.fromCharCode(b);
826
+ out +=
827
+ b < 0x20 || b > 0x7e || c === '"' || c === '\\' || (c === '?' && out.endsWith('?'))
828
+ ? `\\${b.toString(8).padStart(3, '0')}`
829
+ : c;
830
+ }
831
+ return `"${out}"`;
832
+ }
833
+
834
+ /** A function-scope static's definition, as the first lines of the body. An array's elements are
835
+ * laid out eight to a line; an alignment the type does not give is an attribute, which gcc and
836
+ * mwcc both read after the declarator. */
837
+ function staticDefinition(st: SStatic): string[] {
838
+ const aligned = st.align === undefined ? '' : ` __attribute__((aligned(${st.align})))`;
839
+ const head = ` static ${st.const ? 'const ' : ''}${cDeclare(st.type, st.name)}${aligned}`;
840
+ return st.init === undefined ? [`${head};`] : initialized(head, st.type, st.init, st.string === true, false);
841
+ }
842
+
843
+ /** A declaration `head` of `type` with its initializer: a string literal, one scalar, or an
844
+ * array's elements laid out eight to a line. An element left out (null) is skipped and the next
845
+ * one spelled by designator, `[k] = v` — or, where the compiler cannot parse one (`SFn.designators`
846
+ * absent: IDO, mwcc) or the language has none (C++), spelled as the zero it is. */
847
+ function initialized(
848
+ head: string,
849
+ type: IrType,
850
+ init: readonly (number | InitAddress | null)[],
851
+ string: boolean,
852
+ designators: boolean,
853
+ ): string[] {
854
+ const whole = init.map((v) => v ?? 0);
855
+ if (string) {
856
+ // a string's elements are bytes, and no byte holds an address (structure/local-statics.ts)
857
+ const bytes = whole.filter((v): v is number => typeof v === 'number');
858
+ if (bytes.length !== whole.length) {
859
+ throw new Error(`internal: a string initializer of '${head.trim()}' holds an address`);
860
+ }
861
+ return [`${head} = ${stringLiteral(bytes)};`];
862
+ }
863
+ if (type.kind !== 'array') {
864
+ return [`${head} = ${initElement(whole[0], type)};`];
865
+ }
866
+ const items: string[] = [];
867
+ init.forEach((v, i) => {
868
+ if (v !== null || !designators) {
869
+ const e = initElement(v ?? 0, type.elem);
870
+ items.push(init[i - 1] === null && designators ? `[${i}] = ${e}` : e);
871
+ }
872
+ });
873
+ if (items.length <= 8) {
874
+ return [`${head} = { ${items.join(', ')} };`];
875
+ }
876
+ const rows: string[] = [];
877
+ for (let i = 0; i < items.length; i += 8) {
878
+ rows.push(` ${items.slice(i, i + 8).join(', ')},`);
879
+ }
880
+ return [`${head} = {`, ...rows, ' };'];
881
+ }
882
+
579
883
  /** Struct declarations this function references, one `struct N { ... };` per recovered aggregate.
580
884
  * The struct type is SELF-DESCRIBING: any padding needed to seat fields at their exact offsets is
581
885
  * already present as real `u8[N]` pad fields — both raise/struct-arrays.ts (element strides) and
@@ -585,15 +889,25 @@ function structDecls(fn: SFn): string[] {
585
889
  return (fn.structs ?? []).map((s) => renderStructDecl(s.name, s.fields));
586
890
  }
587
891
 
588
- /** Assemble a full C-family function from a caller-supplied signature line and the shared body. */
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);
892
+ /** Assemble a full C-family function from a caller-supplied signature line and the shared body.
893
+ * `designators`: the LANGUAGE spells an array element by index in an initializer (C, not C++).
894
+ * Whether the compiler's dialect parses one is the function's own fact (`SFn.designators`); an
895
+ * initializer is spelled by index only where both say yes. */
896
+ export function emitCFamily(signature: string, fn0: SFn, leaf?: LeafHook, designators = true): string {
897
+ // The declaration list is put into the target's own frame order, and a pointer cell's integer
898
+ // uses converted (l3/ptrcell.ts), HERE — in the shared C-family assembler, reached only from
899
+ // `cBackend.emit` and `cppBackend(...).emit`, so both C-family backends print them and no `.emit(`
900
+ // call site does (l3/slotorder.ts says why `emit` owns it).
901
+ // …and the compiler's anonymous constant images folded into the initializers of the locals
902
+ // their copies fill (l3/constinit.ts), which the tree keeps as copies until here.
903
+ const fn = orderSlotLocals(legalizePointerCells(foldConstantImages(fn0)));
594
904
  const decls = structDecls(fn);
595
905
  const preamble = decls.length ? decls.join('\n') + '\n' : '';
596
- return preamble + [`${signature} {`, ...cFamilyBody(fn, leaf), '}'].join('\n') + '\n';
906
+ return (
907
+ preamble +
908
+ [`${signature} {`, ...cFamilyBody(fn, leaf, designators && fn.designators === true), '}'].join('\n') +
909
+ '\n'
910
+ );
597
911
  }
598
912
 
599
913
  // One C-family comment spelling, shared by the C and C++ backends. A `*` followed by `/`