@asmlift/core 0.8.0 → 0.9.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 (105) hide show
  1. package/README.md +34 -30
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +248 -0
  4. package/src/backend/c.ts +5 -2
  5. package/src/backend/cfamily.ts +236 -37
  6. package/src/backend/cpp.ts +114 -9
  7. package/src/backend/pascal.ts +39 -0
  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 +615 -79
  15. package/src/declare.ts +75 -5
  16. package/src/frontend/asmdata.ts +19 -5
  17. package/src/frontend/call-plan.ts +686 -0
  18. package/src/frontend/device-pins.ts +277 -0
  19. package/src/frontend/errors.ts +5 -0
  20. package/src/frontend/flags-edge.ts +101 -0
  21. package/src/frontend/fpu.ts +167 -0
  22. package/src/frontend/frame-objects.ts +2131 -0
  23. package/src/frontend/high-half.ts +2 -1
  24. package/src/frontend/local-object.ts +490 -0
  25. package/src/frontend/mips.ts +363 -40
  26. package/src/frontend/opaque.ts +97 -18
  27. package/src/frontend/ppc.ts +520 -129
  28. package/src/frontend/reloc-symbol.ts +94 -13
  29. package/src/frontend/splat.ts +108 -5
  30. package/src/frontend/ssa.ts +669 -76
  31. package/src/frontend/stackargs.ts +120 -39
  32. package/src/frontend/thumb.ts +2764 -2018
  33. package/src/ir/alias.ts +29 -0
  34. package/src/ir/core.ts +101 -3
  35. package/src/ir/discipline.ts +248 -0
  36. package/src/ir/float-bits.ts +47 -0
  37. package/src/ir/opcodes.ts +84 -112
  38. package/src/ir/parse.ts +1 -0
  39. package/src/ir/types.ts +118 -2
  40. package/src/ir/verify.ts +150 -3
  41. package/src/l3/address.ts +5 -0
  42. package/src/l3/argcopy.ts +38 -6
  43. package/src/l3/ast.ts +186 -14
  44. package/src/l3/coalesce.ts +21 -13
  45. package/src/l3/dce.ts +32 -10
  46. package/src/l3/gates.ts +9 -3
  47. package/src/l3/hoist.ts +8 -7
  48. package/src/l3/initfirst.ts +27 -9
  49. package/src/l3/intcell.ts +97 -0
  50. package/src/l3/mentions.ts +10 -4
  51. package/src/l3/narrowdecl.ts +218 -0
  52. package/src/l3/nearbase.ts +10 -5
  53. package/src/l3/parkfirst.ts +8 -5
  54. package/src/l3/pollguard.ts +3 -2
  55. package/src/l3/ptrcell.ts +357 -0
  56. package/src/l3/regspell.ts +17 -3
  57. package/src/l3/reindex.ts +10 -7
  58. package/src/l3/symbol-refs.ts +104 -23
  59. package/src/l3/tailmerge.ts +6 -19
  60. package/src/l3/typing.ts +137 -25
  61. package/src/l3/unreduce.ts +4 -4
  62. package/src/l3/volstore.ts +96 -7
  63. package/src/mangle.ts +11 -1
  64. package/src/pattern/engine.ts +19 -16
  65. package/src/pipeline.ts +57 -12
  66. package/src/proto-context.ts +788 -0
  67. package/src/proto.ts +689 -50
  68. package/src/raise/arrays.ts +4 -3
  69. package/src/raise/declared-volatile.ts +131 -0
  70. package/src/raise/divpow2.ts +2 -2
  71. package/src/raise/errors.ts +17 -0
  72. package/src/raise/floathelpers.ts +243 -0
  73. package/src/raise/globalshape.ts +11 -5
  74. package/src/raise/gvn.ts +26 -5
  75. package/src/raise/narrow.ts +29 -2
  76. package/src/raise/narrowlocal.ts +11 -6
  77. package/src/raise/offsetnames.ts +510 -0
  78. package/src/raise/pairparams.ts +81 -0
  79. package/src/raise/paramwidth.ts +7 -0
  80. package/src/raise/pre-recovery.ts +78 -4
  81. package/src/raise/recover.ts +14 -10
  82. package/src/raise/retsink.ts +1 -1
  83. package/src/raise/shortcircuit.ts +41 -19
  84. package/src/raise/softdiv.ts +21 -23
  85. package/src/raise/struct-arrays.ts +5 -4
  86. package/src/raise/structs.ts +311 -98
  87. package/src/raise/truncload.ts +366 -0
  88. package/src/raise/widehelpers.ts +106 -0
  89. package/src/rank-declare.ts +30 -5
  90. package/src/rank-variations.ts +36 -8
  91. package/src/rank.ts +276 -80
  92. package/src/runtime-helpers.ts +287 -0
  93. package/src/stillborn.ts +289 -0
  94. package/src/structure/analysis.ts +1285 -448
  95. package/src/structure/hazards.ts +633 -58
  96. package/src/structure/local-statics.ts +381 -0
  97. package/src/structure/namecoalesce.ts +1 -1
  98. package/src/structure/redundant-test.ts +99 -0
  99. package/src/structure/structure.ts +1772 -395
  100. package/src/structure/switch-recover.ts +159 -78
  101. package/src/symbols.ts +38 -3
  102. package/src/target.ts +662 -59
  103. package/src/trace.ts +44 -24
  104. package/src/variation-definitions.ts +119 -1
  105. package/src/variation-tokens.ts +20 -6
@@ -10,8 +10,10 @@
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, SFn, SStatic, Stmt, dotBase } from '../l3/ast';
16
+ import { legalizePointerCells } from '../l3/ptrcell';
15
17
  import { orderSlotLocals } from '../l3/slotorder';
16
18
  import {
17
19
  type PrintEnv,
@@ -20,6 +22,7 @@ import {
20
22
  declaredTypes,
21
23
  derefStrideOk,
22
24
  exprCType,
25
+ exprIntWidth,
23
26
  printEnv,
24
27
  renderedIntSignedness,
25
28
  writesNonPointerIntoPointer,
@@ -28,6 +31,10 @@ import {
28
31
  // C operator precedence (lower binds tighter). Used to emit MINIMAL parentheses. Shared: C++ has
29
32
  // the same precedence for these operators.
30
33
  const PREC: Record<BinOp, number> = {
34
+ 'f*': 3,
35
+ 'f/': 3,
36
+ 'f+': 4,
37
+ 'f-': 4,
31
38
  '*': 3,
32
39
  '/': 3,
33
40
  '/u': 3, // the unsigned twins spell as C's `/` and `%` (see C_SPELLING) — same precedence
@@ -64,17 +71,32 @@ const C_SPELLING: Partial<Record<BinOp, { token: string; signed: boolean }>> = {
64
71
  '%u': { token: '%', signed: false },
65
72
  };
66
73
 
74
+ /** The float operators (l3/ast.ts BinOp) spell as C's own tokens and take no operand pin: C's
75
+ * arithmetic over two floats is the IEEE operation the machine did. */
76
+ const FLOAT_TOKEN: Partial<Record<BinOp, string>> = { 'f+': '+', 'f-': '-', 'f*': '*', 'f/': '/' };
77
+
67
78
  /** Spell a recovered type in the decomp C-family typedef vocabulary (`s32`/`u32`/`u8`/`T *`). */
68
79
  export function cType(t: IrType): string {
69
80
  if (t.kind === 'ptr') {
70
81
  return `${cType(t.to)} *`;
71
82
  }
72
83
  if (t.kind === 'struct') {
73
- return `struct ${t.name}`;
84
+ return t.declared ?? `struct ${t.name}`;
85
+ }
86
+ // Declared inline, where the struct member that holds it is declared (ir/types.ts).
87
+ if (t.kind === 'union') {
88
+ return `union { ${t.members.map((m) => `${cDeclare(m.type, m.name)};`).join(' ')} }`;
74
89
  }
75
90
  if (t.kind === 'array') {
76
91
  return `${cType(t.elem)}[${t.count}]`;
77
92
  } // ill-formed as a prefix; use cDeclare
93
+ // The C89 KEYWORDS, not the `f32`/`f64` typedefs a decomp project declares: the candidate prelude
94
+ // (`C_TYPEDEFS`) holds the integer family only, and a keyword needs no declaration in any
95
+ // translation unit — so no project context that already typedefs `f32` can collide with it.
96
+ // A float of no stated precision compiles the same under either keyword; `float` is its spelling.
97
+ if (t.kind === 'float') {
98
+ return t.width === 64 ? 'double' : 'float';
99
+ }
78
100
  return typeToString(t); // s32 / u32 / u8 / unk32 (treated as s32 upstream)
79
101
  }
80
102
 
@@ -83,7 +105,7 @@ export function cType(t: IrType): string {
83
105
  * the prefix `cType name`. A NESTED array spells every extent after the name in declaration
84
106
  * order (`u8 unk8[6][8]`) — one declarator, not an element type that is itself an array, which
85
107
  * C has no syntax for and `cType` marks ill-formed as a prefix. */
86
- function cDeclare(t: IrType, name: string): string {
108
+ export function cDeclare(t: IrType, name: string): string {
87
109
  if (t.kind === 'array') {
88
110
  const extents: number[] = [];
89
111
  let e: IrType = t;
@@ -113,10 +135,11 @@ export interface StructFieldDecl {
113
135
  bits?: number;
114
136
  }
115
137
 
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). */
138
+ /** THE struct-declaration spelling — every `struct N { ... };` asmlift recovers or synthesizes
139
+ * comes from here, so the backend's recovered-struct decls and the scoring layer's synthesized
140
+ * decls cannot drift apart. A struct the headers own is transcribed from their declaration
141
+ * instead (declare.ts `declaredStructDecl`). One line, fields in caller order (the type is
142
+ * self-describing: padding is the caller's discipline, already present as real fields). */
120
143
  export function renderStructDecl(name: string, fields: StructFieldDecl[]): string {
121
144
  const one = (f: StructFieldDecl) =>
122
145
  `${f.volatile ? 'volatile ' : ''}${cDeclare(f.type, f.name)}${f.bits !== undefined ? ` : ${f.bits}` : ''};`;
@@ -166,18 +189,23 @@ function legalizedIndexBase(ix: Extract<Expr, { k: 'index' }>, vt: PrintEnv): Ex
166
189
  };
167
190
  }
168
191
 
169
- /** The 32-bit integer cast that PINS an operand's signedness.
192
+ /** The integer cast that PINS an operand's signedness, AT THE OPERAND'S OWN RANK.
170
193
  *
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;
194
+ * The width is not a constant: `(s32)a / (s32)b` over two `s64`s does not pin a signedness, it
195
+ * truncates the operation to 32 bits and calls a different helper than the machine did. So the
196
+ * rank comes from `exprIntWidth`, which is total and answers 64 only where it can prove it.
197
+ *
198
+ * An existing integer cast AT THE SAME WIDTH is REPLACED rather than wrapped — `(u32)(s32)&g` and
199
+ * `(u32)&g` are the same bytes, and the arithmetic rules upstream do emit that inner cast
200
+ * (intifyAddr). A cast at a DIFFERENT width is a narrowing the source wrote and must be kept. The
201
+ * replacement CARRIES the qualifier: re-typing a `volatile` cast without it drops an assertion the
202
+ * differ cannot referee the loss of, which is why l3/initfirst.ts's `stripWideIntCast` refuses the
203
+ * same peel one pass over. */
204
+ function recastInt(x: Expr, signed: boolean, width: 32 | 64): Expr {
205
+ const replaced = x.k === 'cast' && x.to.kind === 'int' && x.to.width === width ? x : undefined;
178
206
  return {
179
207
  k: 'cast',
180
- to: T.int(32, signed),
208
+ to: T.int(width, signed),
181
209
  ...(replaced?.volatile === true ? { volatile: true as const } : {}),
182
210
  e: replaced ? replaced.e : x,
183
211
  };
@@ -205,14 +233,65 @@ function recast32(x: Expr, signed: boolean): Expr {
205
233
  * the division unsigned. Verified by compiling: `((u32)a / b) / 7` calls `__udivsi3` twice,
206
234
  * `(s32)((u32)a / b) / 7` calls `__udivsi3` then `__divsi3`. */
207
235
  function pinnedOperands(e0: Extract<Expr, { k: 'bin' }>, wantSigned: boolean, vt: PrintEnv): [Expr, Expr] {
236
+ // A shift reads its LEFT operand's rank; a divide reads the wider of the two, which is what the
237
+ // usual arithmetic conversions make the operation's own rank.
208
238
  if (e0.op === '>>' || e0.op === '>>>') {
209
- return [renderedIntSignedness(e0.l, vt.type) === wantSigned ? e0.l : recast32(e0.l, wantSigned), e0.r];
239
+ const w = exprIntWidth(e0.l, vt.type);
240
+ return [renderedIntSignedness(e0.l, vt.type) === wantSigned ? e0.l : recastInt(e0.l, wantSigned, w), e0.r];
210
241
  }
211
242
  if (arithConversionSignedness(e0.l, e0.r, vt.type) === wantSigned) {
212
243
  return [e0.l, e0.r];
213
244
  }
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];
245
+ const w = exprIntWidth(e0.l, vt.type) === 64 || exprIntWidth(e0.r, vt.type) === 64 ? 64 : 32;
246
+ const pinSigned = (x: Expr): Expr => (renderedIntSignedness(x, vt.type) === true ? x : recastInt(x, true, w));
247
+ return wantSigned ? [pinSigned(e0.l), pinSigned(e0.r)] : [recastInt(e0.l, false, w), e0.r];
248
+ }
249
+
250
+ /** Does passing `a` to a parameter declared `to` take a conversion C++ will not make implicitly?
251
+ * Three do, and C makes each of them implicitly, so the cast spells the conversion C would have
252
+ * made and the compiled code is C's: an object pointer to a pointer of another pointee (a conversion
253
+ * to `void *`, or one that only adds a qualifier, is implicit in C++ too), an integer to a pointer
254
+ * (the literal `0` is the one integer that converts), and a pointer to an integer. Only reached on a
255
+ * function compiled as C++ (`SFn.dialect`). */
256
+ function argConversion(a: Expr, to: string, vt: PrintEnv): boolean {
257
+ const from = exprCType(a, vt.type);
258
+ // qualifiers never decide it: a `T * const` parameter is a pointer parameter
259
+ const bare = to
260
+ .replace(/\b(?:const|volatile)\b/g, ' ')
261
+ .replace(/\s+/g, ' ')
262
+ .trim();
263
+ if (bare.endsWith('*')) {
264
+ if (a.k === 'const' && a.value === 0) {
265
+ return false;
266
+ }
267
+ if (from?.kind !== 'ptr') {
268
+ return true;
269
+ }
270
+ return bare !== 'void *' && bare !== cType(from).replace(/\s+/g, ' ').trim();
271
+ }
272
+ return from?.kind === 'ptr';
273
+ }
274
+
275
+ /** The unqualified pointer a `const` static's address is spelled as where it reaches a place that
276
+ * converts it implicitly — an argument, an assigned or stored value, a returned one — or null for
277
+ * any other expression. The static is `const` because the target holds it in read-only data, not
278
+ * because the source's pointer types say so: mwcc rejects the implicit `const u8[4]` → `u8 *` a
279
+ * bare `g(tbl)` makes (compiled), and a pointer cast changes no instruction. */
280
+ function constEscape(e: Expr, vt: PrintEnv): IrType | null {
281
+ if (e.k === 'addr' && vt.constStatic(e.name)) {
282
+ const t = vt.type(e.name);
283
+ return t === undefined ? null : T.ptr(t.kind === 'array' ? t.elem : t);
284
+ }
285
+ if (e.k === 'bin' && (e.op === '+' || e.op === '-')) {
286
+ return constEscape(e.l, vt) ?? (e.op === '+' ? constEscape(e.r, vt) : null);
287
+ }
288
+ return null;
289
+ }
290
+
291
+ /** `e` printed where C converts it implicitly, with a `const` static's address cast (constEscape). */
292
+ function printConverted(e: Expr, vt: PrintEnv, leaf?: LeafHook): string {
293
+ const to = constEscape(e, vt);
294
+ return to === null ? printExpr(e, 99, vt, leaf) : `(${cType(to)})${printExpr(e, 2, vt, leaf)}`;
216
295
  }
217
296
 
218
297
  function printExpr(e: Expr, parentPrec: number, vt: PrintEnv, leaf?: LeafHook): string {
@@ -228,14 +307,51 @@ function printExpr(e: Expr, parentPrec: number, vt: PrintEnv, leaf?: LeafHook):
228
307
  return e.name;
229
308
  case 'const':
230
309
  return String(e.value);
310
+ // a negative literal is a prefix `-`, and parenthesizes where one would (`un` below)
311
+ case 'fconst': {
312
+ const s = doubleLiteral(e.bits);
313
+ return parentPrec < 2 && s.startsWith('-') ? `(${s})` : s;
314
+ }
231
315
  case 'addr': {
232
- // `&gSym` — the address of a named global. A prefix operator; parenthesizes under a POSTFIX
233
- // parent like the other prefix forms.
316
+ // `&gSym` — the address of a named global or of a frame-local object. A prefix operator;
317
+ // parenthesizes under a POSTFIX parent like the other prefix forms.
318
+ //
319
+ // AN ARRAY DECAYS INSTEAD, and the declared type is what says so, which is why the decision
320
+ // is here rather than on the node: `&` on `u8 sp0[16]` spells a `u8 (*)[16]` — a different
321
+ // type for the same byte, which every typed pointer parameter rejects (`passing arg 1 from
322
+ // incompatible pointer type`) — where the bare name is the `u8 *` the machine produced.
323
+ // Compiled both ways, the object is identical, so the `&` buys a diagnostic and nothing
324
+ // else. An identifier binds tighter than every parent, so the decayed form never
325
+ // parenthesizes. A name this env cannot type keeps the `&`: an unknown shape is not an
326
+ // array, and dropping the operator on a scalar would spell its value.
327
+ if (vt.type(e.name)?.kind === 'array') {
328
+ return e.name;
329
+ }
234
330
  const g = `&${e.name}`;
235
331
  return parentPrec < 2 ? `(${g})` : g;
236
332
  }
237
- case 'call':
238
- return `${e.fn}(${e.args.map((a) => rec(a, 99)).join(', ')})`;
333
+ case 'call': {
334
+ if (typeof e.fn !== 'string') {
335
+ // An unprototyped pointer to a function returning the call's own rank: C passes each
336
+ // argument as its promoted self, which is what a call with no declaration passes.
337
+ const ret = cType(T.int(e.wide64 ? 64 : 32, true));
338
+ return `((${ret} (*)())${rec(e.fn, 2)})(${e.args.map((a) => printConverted(a, vt, leaf)).join(', ')})`;
339
+ }
340
+ if (e.sret !== undefined) {
341
+ throw new Error(`the call to '${e.fn}' returns a struct through memory and is spelled only as a statement`);
342
+ }
343
+ const declared = vt.declaredArgs(e.fn);
344
+ return `${e.fn}(${e.args
345
+ .map((a, i) => {
346
+ const to = declared?.[i];
347
+ return to !== undefined && argConversion(a, to, vt) ? `(${to})${rec(a, 2)}` : printConverted(a, vt, leaf);
348
+ })
349
+ .join(', ')})`;
350
+ }
351
+ // `v++` / `v--`. Postfix over a bare identifier, so it binds tighter than every parent and
352
+ // never self-parenthesizes.
353
+ case 'postincr':
354
+ return `${e.name}${e.by === 1 ? '++' : '--'}`;
239
355
  case 'index': {
240
356
  // `*base` for the zero offset (a PREFIX operator, so a prefix-shaped base like a cast needs
241
357
  // no parens: `*(u8 *)p` — but the whole form must self-parenthesize under a POSTFIX parent:
@@ -306,7 +422,8 @@ function printExpr(e: Expr, parentPrec: number, vt: PrintEnv, leaf?: LeafHook):
306
422
  // A prefix operator: parenthesize under a POSTFIX parent (`(-a)[1]`), and parenthesize a
307
423
  // same-op nested `-` (`-(-a)`, never `--a` — C lexes that as predecrement).
308
424
  const inner = rec(e.e, 2);
309
- const s = `${e.op}${e.op === '-' && inner.startsWith('-') ? `(${inner})` : inner}`;
425
+ const op = e.op === 'f-' ? '-' : e.op;
426
+ const s = `${op}${op === '-' && inner.startsWith('-') ? `(${inner})` : inner}`;
310
427
  return parentPrec < 2 ? `(${s})` : s;
311
428
  }
312
429
  // A gap marker spells as a call to the UNDEFINED macro ASMLIFT_ERROR("reason", args…) — the
@@ -331,25 +448,41 @@ function printExpr(e: Expr, parentPrec: number, vt: PrintEnv, leaf?: LeafHook):
331
448
  // operand cast that says which of the pair it is.
332
449
  const pair = C_SPELLING[e.op];
333
450
  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)}`;
451
+ const s = `${rec(l, p)} ${pair ? pair.token : (FLOAT_TOKEN[e.op] ?? e.op)} ${rec(r, p - 1)}`;
335
452
  return p > parentPrec ? `(${s})` : s;
336
453
  }
337
454
  }
338
455
  }
339
456
 
457
+ /** `dest = fn(args)` for a call whose `args[0]` is the address of the struct it returns: a local
458
+ * declared with that struct type, named by its address. Any other destination has no spelling
459
+ * that hands the callee the same pointer — compiled, `*(struct Blob64 *)&gDst = makeblob(gBlob);`
460
+ * is a frame temp and a `memcpy`, where `gDst = makeblob(gBlob);` hands over `&gDst` itself. */
461
+ function structReturnCall(call: Extract<Expr, { k: 'call' }>, vt: PrintEnv, leaf?: LeafHook): string {
462
+ const [dest, ...rest] = call.args;
463
+ const declared = dest?.k === 'addr' ? vt.type(dest.name) : undefined;
464
+ if (dest?.k !== 'addr' || declared === undefined || !typeEquals(declared, call.sret!)) {
465
+ throw new Error(`the call to '${call.fn}' returns a struct into something other than a local of its type`);
466
+ }
467
+ return `${dest.name} = ${printExpr({ k: 'call', fn: call.fn, args: rest }, 99, vt, leaf)}`;
468
+ }
469
+
340
470
  function printStmt(s: Stmt, indent: string, vt: PrintEnv, leaf?: LeafHook): string[] {
341
471
  const pe = (e: Expr, p: number) => printExpr(e, p, vt, leaf);
342
472
  switch (s.k) {
343
473
  case 'assign':
344
- return [`${indent}${s.name} = ${pe(s.value, 99)};`];
474
+ return [`${indent}${s.name} = ${printConverted(s.value, vt, leaf)};`];
345
475
  case 'store':
346
476
  // The lvalue is a full Expr (`index` or `field`), so the leaf hook spells a member write
347
477
  // (`this->x = …`) exactly as it spells a member read.
348
- return [`${indent}${pe(s.lval, 2)} = ${pe(s.value, 99)};`];
478
+ return [`${indent}${pe(s.lval, 2)} = ${printConverted(s.value, vt, leaf)};`];
349
479
  case 'exprstmt':
480
+ if (s.value.k === 'call' && s.value.sret !== undefined) {
481
+ return [`${indent}${structReturnCall(s.value, vt, leaf)};`];
482
+ }
350
483
  return [`${indent}${pe(s.value, 99)};`];
351
484
  case 'return':
352
- return [`${indent}return${s.value ? ' ' + pe(s.value, 99) : ''};`];
485
+ return [`${indent}return${s.value ? ' ' + printConverted(s.value, vt, leaf) : ''};`];
353
486
  case 'if': {
354
487
  const cond = pe(s.cond, 99);
355
488
  if (s.then.length === 1 && s.else.length === 0 && s.then[0].k !== 'if') {
@@ -519,8 +652,12 @@ function endsTerminated(body: Stmt[]): boolean {
519
652
  // traversal-vocabulary exemption.)
520
653
  function legalizePointerWrites(fn: SFn): SFn {
521
654
  const vt = declaredTypes(fn);
655
+ // …and the mirror: a rendered pointer into an INTEGER slot takes the slot's type. A cast keeps
656
+ // the sum it wraps, so the bytes are the uncast write's; CodeWarrior rejects that write outright.
522
657
  const castTo = (t: IrType | undefined, e: Expr): Expr =>
523
- writesNonPointerIntoPointer(t, e, vt) ? { k: 'cast', to: t, e } : e;
658
+ writesNonPointerIntoPointer(t, e, vt) || (t?.kind === 'int' && exprCType(e, vt)?.kind === 'ptr')
659
+ ? { k: 'cast', to: t, e }
660
+ : e;
524
661
  const fix = (s: Stmt): Stmt => {
525
662
  switch (s.k) {
526
663
  case 'assign':
@@ -562,13 +699,18 @@ function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
562
699
  // reader will see.
563
700
  const vt: PrintEnv = printEnv(fn);
564
701
  const lines: string[] = [];
702
+ for (const st of fn.statics ?? []) {
703
+ lines.push(...staticDefinition(st));
704
+ }
565
705
  for (const l of fn.locals) {
566
706
  // Both facts render at the PREFIX position, where C's declarator grammar reads them
567
707
  // differently: on a scalar the qualifier binds to the object (`volatile u16 sp0`), on a
568
708
  // 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};`);
709
+ // (`volatile u16 **p`). An object-volatile POINTER (`u16 *volatile p`) has no inhabitant —
710
+ // no variation or recognizer produces one. The NAME is placed by `cDeclare`, the one
711
+ // placement every declaration position in this backend uses: a local can carry an array type
712
+ // — the storage extent a frame object pins — and C puts those extents after the name.
713
+ lines.push(` ${l.volatile || l.pointeeVolatile ? 'volatile ' : ''}${cDeclare(l.type, l.name)};`);
572
714
  }
573
715
  for (const s of fn.body) {
574
716
  lines.push(...printStmt(s, ' ', vt, leaf));
@@ -576,6 +718,62 @@ function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
576
718
  return lines;
577
719
  }
578
720
 
721
+ /** One initial element: hex from 10 up, a negative one with its sign, so a byte table reads as the
722
+ * bytes and a signed one as its values. */
723
+ const initElement = (v: number): string =>
724
+ Math.abs(v) < 10 ? `${v}` : `${v < 0 ? '-' : ''}0x${Math.abs(v).toString(16)}`;
725
+
726
+ /** A byte array's initial elements as a string literal: printable characters as themselves, every
727
+ * other byte in three-digit octal (which no following digit can extend), and a `?` after a `?`
728
+ * escaped so no trigraph forms. The trailing zeros are left to the declared size, which C fills
729
+ * with zeros. */
730
+ function stringLiteral(init: readonly number[]): string {
731
+ let end = init.length;
732
+ while (end > 0 && (init[end - 1] & 0xff) === 0) {
733
+ end--;
734
+ }
735
+ let out = '';
736
+ for (const v of init.slice(0, end)) {
737
+ const b = v & 0xff;
738
+ const c = String.fromCharCode(b);
739
+ out +=
740
+ b < 0x20 || b > 0x7e || c === '"' || c === '\\' || (c === '?' && out.endsWith('?'))
741
+ ? `\\${b.toString(8).padStart(3, '0')}`
742
+ : c;
743
+ }
744
+ return `"${out}"`;
745
+ }
746
+
747
+ /** A function-scope static's definition, as the first lines of the body. An array's elements are
748
+ * laid out eight to a line; an alignment the type does not give is an attribute, which gcc and
749
+ * mwcc both read after the declarator. */
750
+ function staticDefinition(st: SStatic): string[] {
751
+ const aligned = st.align === undefined ? '' : ` __attribute__((aligned(${st.align})))`;
752
+ const head = ` static ${st.const ? 'const ' : ''}${cDeclare(st.type, st.name)}${aligned}`;
753
+ if (st.init === undefined) {
754
+ return [`${head};`];
755
+ }
756
+ if (st.string) {
757
+ return [`${head} = ${stringLiteral(st.init)};`];
758
+ }
759
+ if (st.type.kind !== 'array') {
760
+ return [`${head} = ${initElement(st.init[0])};`];
761
+ }
762
+ if (st.init.length <= 8) {
763
+ return [`${head} = { ${st.init.map(initElement).join(', ')} };`];
764
+ }
765
+ const rows: string[] = [];
766
+ for (let i = 0; i < st.init.length; i += 8) {
767
+ rows.push(
768
+ ` ${st.init
769
+ .slice(i, i + 8)
770
+ .map(initElement)
771
+ .join(', ')},`,
772
+ );
773
+ }
774
+ return [`${head} = {`, ...rows, ' };'];
775
+ }
776
+
579
777
  /** Struct declarations this function references, one `struct N { ... };` per recovered aggregate.
580
778
  * The struct type is SELF-DESCRIBING: any padding needed to seat fields at their exact offsets is
581
779
  * already present as real `u8[N]` pad fields — both raise/struct-arrays.ts (element strides) and
@@ -587,10 +785,11 @@ function structDecls(fn: SFn): string[] {
587
785
 
588
786
  /** Assemble a full C-family function from a caller-supplied signature line and the shared body. */
589
787
  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);
788
+ // The declaration list is put into the target's own frame order, and a pointer cell's integer
789
+ // uses converted (l3/ptrcell.ts), HERE — in the shared C-family assembler, reached only from
790
+ // `cBackend.emit` and `cppBackend(...).emit`, so both C-family backends print them and no `.emit(`
791
+ // call site does (l3/slotorder.ts says why `emit` owns it).
792
+ const fn = orderSlotLocals(legalizePointerCells(fn0));
594
793
  const decls = structDecls(fn);
595
794
  const preamble = decls.length ? decls.join('\n') + '\n' : '';
596
795
  return preamble + [`${signature} {`, ...cFamilyBody(fn, leaf), '}'].join('\n') + '\n';
@@ -13,10 +13,14 @@
13
13
  // Scope: free functions and non-virtual member functions with scalar/pointer params and named
14
14
  // field access. Virtual dispatch, references, and constructors/destructors are deliberately not
15
15
  // built ahead of an inhabitant.
16
- import { Expr, LanguageBackend, SFn } from '../l3/ast';
17
- import { type CppType, mangle, spellType } from '../mangle';
16
+ import { Expr, LanguageBackend, SFn, walkExprs } from '../l3/ast';
17
+ import { type CppType, declareCpp, mangle, spellType } from '../mangle';
18
+ import type { TargetDescription } from '../target';
18
19
  import { LeafHook, cComment, emitCFamily } from './cfamily';
19
20
 
21
+ /** How the target's argument slots hold floats (`TargetDescription.fpu`). */
22
+ type FloatSlots = NonNullable<TargetDescription['fpu']>['slots'];
23
+
20
24
  export interface CppClass {
21
25
  fields: { name: string; type: CppType }[];
22
26
  } // field i at word offset i
@@ -33,9 +37,104 @@ export function cppSymbol(spec: CppFnSpec): string {
33
37
  return mangle({ name: spec.method, cls: spec.cls, params: spec.params.map((p) => p.type) });
34
38
  }
35
39
 
36
- /** Build a C++ backend for one function, parameterized by its recovered C++ signature. The lifted
37
- * SFn params are positional: for a member function SFn.params[0] is `this`, the rest are `params`. */
38
- export function cppBackend(spec: CppFnSpec): LanguageBackend {
40
+ /** The lifted parameter each EXPLICIT spec parameter is (its SFn name), for a member function after
41
+ * `this` (SFn.params[0]). How depends on the target's float slot model (`TargetDescription.fpu`),
42
+ * because the lifted order is the ABI sort's (frontend/fpu.ts `fpuArgSlots`):
43
+ * - `'separate'` (PowerPC EABI): by register FILE, then by position within it. The files count
44
+ * independently and every float argument sorts after the integers, so `float g(float x, int n)`
45
+ * lifts as `(s32 a0, float a1)`. A float the body never reads leaves no hole, so the spec may
46
+ * have MORE floats than the lift (`int m(int n, float x)` lifts as `(s32 a0)`), never fewer.
47
+ * - `'leading'` (MIPS o32), and a target with no float file: by POSITION. The lifted order is the
48
+ * slot order, which is the source order, and an unread leading float is minted as an integer
49
+ * hole — so binding by file would hand `int k(float x, int n)`'s `n` the hole `a0`.
50
+ * NULL when the two contradict: a lifted float the spec has no float for, or has one of the other
51
+ * precision for. No binding of the rest is then trustworthy. The precision is part of the
52
+ * contradiction because the body is spelled over the spec's types: a lifted `double` bound to a
53
+ * `float a, float b` prints `a + b`, which is a single-precision add where the machine did a
54
+ * double one.
55
+ *
56
+ * THE PRECISION IS AN OPERATION'S, so only a parameter the body reads has one to contradict. A
57
+ * frontend types every float register of a function at the function's one precision
58
+ * (frontend/fpu.ts `fpPrecision`), and that includes an argument slot the body never reads, which
59
+ * it mints only to hold the later arguments' places: `float f1(double a, float b){ return b + b; }`
60
+ * is `fadds f1,f2,f2`, and its `a` is a single-precision hole that nothing states the width of. So
61
+ * is a lifted float of no stated precision (`T.fUnstated`) anywhere. Either binds a spec float of
62
+ * both widths, and the spec's is the one to print.
63
+ *
64
+ * EXCEPT WHERE THE HOLE'S WIDTH PLACED THE PARAMETERS AFTER IT. Under `'leading'` a float takes one
65
+ * integer slot for a single and two for a double, counted at the function's precision
66
+ * (`fpuArgSlots`), so an unread hole of the other width than the spec's moves every integer after
67
+ * it: `float m2(double a, float b, int *p, int *q){ *p = 0; return b + b; }` (gcc2.7.2kmc) is
68
+ * `sw zero,0(a3)` with `a` laid out as ONE slot, and binding by position would name `a3` `q`. So
69
+ * there an unread float followed by an integer parameter keeps its precision.
70
+ *
71
+ * A spec `double` bound to a lifted NON-float is the same misplacement. A double takes two integer
72
+ * slots under o32 and on a target with no float file (agbcc passes one in r0:r1), so a body that
73
+ * reads no FPU register mints it as two 32-bit holes: `void n1(double d, int *p, int *q){ *p = 0; }`
74
+ * (gcc2.7.2kmc) is `sw zero,0(a2)` and lifts as `(s32 a0, s32 a1, s32 *a2)`, where binding by
75
+ * position would name `a2` `q`. Only a lifted 64-bit parameter holds both slots, so a spec double
76
+ * over a narrower one refuses when a lifted parameter follows it, and when the body reads it — a
77
+ * read half would print as the whole double (agbcc `int u1(int a, double d)` returning the word
78
+ * `((int *)&d)[0]` is `add r0,r1,#0`). A lifted parameter after it is not a refusal when the double
79
+ * is the spec's last and the body reads none of the slots from it on, which is IDO homing a
80
+ * trailing double it never uses (`int o1(int a, double d){ return a; }` spills `a2`/`a3`): nothing
81
+ * then binds past it. */
82
+ export function bindSpecParams(
83
+ spec: Pick<CppFnSpec, 'cls' | 'params'>,
84
+ lifted: Pick<SFn, 'params' | 'body'>,
85
+ floatSlots: FloatSlots | undefined,
86
+ ): (string | undefined)[] | null {
87
+ const floatBits = (t: CppType) => (t.ptr !== 0 ? null : t.base === 'float' ? 32 : t.base === 'double' ? 64 : null);
88
+ const isFloat = (t: CppType) => floatBits(t) !== null;
89
+ const read = new Set<string>();
90
+ for (const e of walkExprs(lifted.body)) {
91
+ if (e.k === 'var') {
92
+ read.add(e.name);
93
+ }
94
+ }
95
+ const clashes = (p: SFn['params'][number], t: CppType | undefined, laysOut: boolean) =>
96
+ p.type.kind === 'float' &&
97
+ (t === undefined ||
98
+ floatBits(t) === null ||
99
+ (p.type.width !== null && (laysOut || read.has(p.name)) && floatBits(t) !== p.type.width));
100
+ const explicit = lifted.params.slice(spec.cls ? 1 : 0);
101
+ const lastAndUnread = (i: number) =>
102
+ i === spec.params.length - 1 && explicit.slice(i).every((p) => !read.has(p.name));
103
+ if (floatSlots !== 'separate') {
104
+ const clash = explicit.some((p, i) => {
105
+ const t = spec.params[i]?.type;
106
+ const later = explicit.slice(i + 1);
107
+ const oneSlot = p.type.kind !== 'float' && !('width' in p.type && p.type.width === 64);
108
+ return (
109
+ clashes(
110
+ p,
111
+ t,
112
+ later.some((q) => q.type.kind !== 'float'),
113
+ ) ||
114
+ (t !== undefined &&
115
+ floatBits(t) === 64 &&
116
+ oneSlot &&
117
+ (later.length > 0 || read.has(p.name)) &&
118
+ !lastAndUnread(i))
119
+ );
120
+ });
121
+ return clash ? null : spec.params.map((_, i) => explicit[i]?.name);
122
+ }
123
+ const floats = explicit.filter((p) => p.type.kind === 'float');
124
+ const others = explicit.filter((p) => p.type.kind !== 'float');
125
+ const specFloats = spec.params.filter((p) => isFloat(p.type));
126
+ if (specFloats.length < floats.length || floats.some((p, i) => clashes(p, specFloats[i].type, false))) {
127
+ return null;
128
+ }
129
+ let f = 0;
130
+ let o = 0;
131
+ return spec.params.map((p) => (isFloat(p.type) ? floats[f++] : others[o++])?.name);
132
+ }
133
+
134
+ /** Build a C++ backend for one function, parameterized by its recovered C++ signature and the float
135
+ * slot model of the target it was lifted for. For a member function SFn.params[0] is `this`; the
136
+ * rest bind to `params` by `bindSpecParams`. */
137
+ export function cppBackend(spec: CppFnSpec, floatSlots: FloatSlots | undefined): LanguageBackend {
39
138
  return {
40
139
  id: 'cpp',
41
140
  spellsSwitchFallthrough: true,
@@ -43,15 +142,21 @@ export function cppBackend(spec: CppFnSpec): LanguageBackend {
43
142
  // Map each lifted param var → its C++ meaning: `this` (bare member access) or a named param
44
143
  // (a pointer-to-class param uses `->`). A pointer-to-known-class param is a member receiver.
45
144
  const thisVar = spec.cls ? fn.params[0]?.name : undefined;
46
- const explicitStart = spec.cls ? 1 : 0;
47
145
  const rename = new Map<string, string>(); // lifted var → C++ name
48
146
  const recv = new Map<string, { cls: string; via: 'this' | string }>(); // var → member receiver
49
147
  if (thisVar) {
50
148
  rename.set(thisVar, 'this');
51
149
  recv.set(thisVar, { cls: spec.cls!, via: 'this' });
52
150
  }
151
+ const bound = bindSpecParams(spec, fn, floatSlots);
152
+ if (!bound) {
153
+ throw new Error(
154
+ `cpp backend: the spec's floating-point parameters do not match the lifted function's — ` +
155
+ `supply a signature whose float and integer parameters are the ones it takes`,
156
+ );
157
+ }
53
158
  spec.params.forEach((p, i) => {
54
- const v = fn.params[explicitStart + i]?.name;
159
+ const v = bound[i];
55
160
  if (!v) {
56
161
  return;
57
162
  }
@@ -115,7 +220,7 @@ export function cppBackend(spec: CppFnSpec): LanguageBackend {
115
220
  return null;
116
221
  };
117
222
 
118
- const paramList = spec.params.map((p) => `${spellType(p.type)} ${p.name}`).join(', ');
223
+ const paramList = spec.params.map((p) => declareCpp(p.type, p.name)).join(', ');
119
224
  const decls = classDecls(spec, paramList);
120
225
  const signature = `${spellType(spec.retType)} ${spec.cls ? spec.cls + '::' : ''}${spec.method}(${paramList})`;
121
226
  return (decls ? decls + '\n' : '') + emitCFamily(signature, fn, leaf);
@@ -140,7 +245,7 @@ function typeWidth(t: CppType): number {
140
245
  function classDecls(spec: CppFnSpec, paramList: string): string {
141
246
  const out: string[] = [];
142
247
  for (const [cname, cdef] of Object.entries(spec.classes ?? {})) {
143
- const fields = cdef.fields.map((f) => `${spellType(f.type)} ${f.name};`).join(' ');
248
+ const fields = cdef.fields.map((f) => `${declareCpp(f.type, f.name)};`).join(' ');
144
249
  const method = cname === spec.cls ? ` ${spellType(spec.retType)} ${spec.method}(${paramList});` : '';
145
250
  out.push(`struct ${cname} { ${fields}${method} };`);
146
251
  }