@asmlift/core 0.9.0 → 0.11.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 (117) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +2 -0
  5. package/src/backend/cfamily.ts +197 -61
  6. package/src/backend/cpp.ts +3 -1
  7. package/src/backend/lines.ts +10 -0
  8. package/src/backend/pascal.ts +47 -16
  9. package/src/cdecl/parse.ts +21 -0
  10. package/src/contracts.ts +246 -36
  11. package/src/declare.ts +69 -20
  12. package/src/frontend/arm-body.ts +155 -0
  13. package/src/frontend/asmdata.ts +9 -2
  14. package/src/frontend/call-plan.ts +1117 -61
  15. package/src/frontend/callee-body.ts +370 -0
  16. package/src/frontend/dead-reads.ts +628 -0
  17. package/src/frontend/device-pins.ts +33 -4
  18. package/src/frontend/emit.ts +4 -3
  19. package/src/frontend/fpu.ts +224 -17
  20. package/src/frontend/frame-objects.ts +1689 -206
  21. package/src/frontend/frontend.ts +84 -3
  22. package/src/frontend/high-half.ts +53 -4
  23. package/src/frontend/jump-table.ts +545 -0
  24. package/src/frontend/local-object.ts +158 -32
  25. package/src/frontend/mips.ts +2296 -217
  26. package/src/frontend/own-struct-return.ts +500 -0
  27. package/src/frontend/ppc.ts +2748 -245
  28. package/src/frontend/register-struct-call.ts +399 -0
  29. package/src/frontend/reloc-symbol.ts +22 -7
  30. package/src/frontend/ssa.ts +1721 -43
  31. package/src/frontend/stackargs.ts +199 -72
  32. package/src/frontend/thumb-entry.ts +340 -0
  33. package/src/frontend/thumb.ts +2400 -325
  34. package/src/ir/alias.ts +65 -2
  35. package/src/ir/bits.ts +1699 -4
  36. package/src/ir/core.ts +254 -9
  37. package/src/ir/discipline.ts +86 -21
  38. package/src/ir/entry.ts +157 -0
  39. package/src/{structure → ir}/loops.ts +6 -2
  40. package/src/ir/opcodes.ts +137 -9
  41. package/src/ir/parse.ts +1 -0
  42. package/src/ir/simplify.ts +61 -10
  43. package/src/ir/types.ts +86 -2
  44. package/src/ir/verify.ts +96 -11
  45. package/src/l3/argbase.ts +2 -0
  46. package/src/l3/ast.ts +582 -18
  47. package/src/l3/basecse.ts +68 -15
  48. package/src/l3/caseendgoto.ts +223 -0
  49. package/src/l3/coalesce.ts +42 -14
  50. package/src/l3/constinit.ts +410 -0
  51. package/src/l3/dce.ts +53 -9
  52. package/src/l3/initfirst.ts +42 -11
  53. package/src/l3/intcell.ts +2 -1
  54. package/src/l3/narrowdecl.ts +77 -36
  55. package/src/l3/pollguard.ts +8 -2
  56. package/src/l3/ptrcell.ts +33 -34
  57. package/src/l3/ptrfield.ts +123 -8
  58. package/src/l3/regspell.ts +11 -4
  59. package/src/l3/reindex.ts +295 -99
  60. package/src/l3/scopebase.ts +2 -0
  61. package/src/l3/storage.ts +98 -1
  62. package/src/l3/symbol-refs.ts +84 -6
  63. package/src/l3/tailmerge.ts +6 -2
  64. package/src/l3/typing.ts +108 -15
  65. package/src/l3/unreduce.ts +6 -5
  66. package/src/l3/volatileptr.ts +17 -54
  67. package/src/mangle.ts +30 -0
  68. package/src/pattern/engine.ts +361 -73
  69. package/src/pipeline.ts +1039 -38
  70. package/src/proto-context.ts +1039 -45
  71. package/src/proto.ts +555 -40
  72. package/src/raise/aggmembers.ts +290 -0
  73. package/src/raise/arrays.ts +5 -1
  74. package/src/raise/const.ts +25 -0
  75. package/src/raise/declared-volatile.ts +207 -31
  76. package/src/raise/divpow2.ts +192 -0
  77. package/src/raise/floathelpers.ts +254 -33
  78. package/src/raise/gvn.ts +3 -3
  79. package/src/raise/latch.ts +162 -6
  80. package/src/raise/memberarrays.ts +10 -4
  81. package/src/raise/offsetnames.ts +422 -36
  82. package/src/raise/pairparams.ts +33 -0
  83. package/src/raise/paramwidth.ts +196 -6
  84. package/src/raise/pre-recovery.ts +107 -21
  85. package/src/raise/pre-structure.ts +73 -0
  86. package/src/raise/recover.ts +12 -5
  87. package/src/raise/reroll.ts +674 -0
  88. package/src/raise/shortcircuit.ts +217 -34
  89. package/src/raise/softdiv.ts +1 -1
  90. package/src/raise/storesign.ts +153 -0
  91. package/src/raise/struct-arrays.ts +30 -11
  92. package/src/raise/structs.ts +396 -33
  93. package/src/raise/truncload.ts +4 -3
  94. package/src/raise/unpre.ts +594 -0
  95. package/src/raise/unthread.ts +400 -0
  96. package/src/rank-declare.ts +118 -12
  97. package/src/rank-variations.ts +186 -35
  98. package/src/rank.ts +536 -68
  99. package/src/runtime-helpers.ts +140 -22
  100. package/src/structure/analysis.ts +217 -8
  101. package/src/structure/arith-ops.ts +23 -0
  102. package/src/structure/cross-jumped-tail.ts +216 -0
  103. package/src/structure/globalaccess.ts +157 -10
  104. package/src/structure/hazards.ts +16 -64
  105. package/src/structure/local-statics.ts +145 -9
  106. package/src/structure/namecoalesce.ts +1 -1
  107. package/src/structure/pointer-spelling.ts +1106 -0
  108. package/src/structure/redundant-test.ts +1 -1
  109. package/src/structure/structure.ts +6192 -1213
  110. package/src/structure/switch-recover.ts +583 -120
  111. package/src/structure/unfolded-branch-constant.ts +90 -0
  112. package/src/symbols.ts +58 -15
  113. package/src/target.ts +1028 -54
  114. package/src/trace.ts +49 -17
  115. package/src/variation-definitions.ts +333 -20
  116. package/src/variation-gates.ts +4 -0
  117. package/src/variation-tokens.ts +68 -3
@@ -12,7 +12,8 @@
12
12
  // too, shared with the scoring layer's synthesized declarations so the two cannot drift).
13
13
  import { doubleLiteral } from '../ir/float-bits';
14
14
  import { IrType, T, scalarTypeForAccess, typeEquals, typeToString } from '../ir/types';
15
- import { BinOp, Expr, SFn, SStatic, Stmt, dotBase } from '../l3/ast';
15
+ import { BinOp, Expr, InitAddress, SFn, SStatic, Stmt, caseGotoViolation, dotBase } from '../l3/ast';
16
+ import { foldConstantImages } from '../l3/constinit';
16
17
  import { legalizePointerCells } from '../l3/ptrcell';
17
18
  import { orderSlotLocals } from '../l3/slotorder';
18
19
  import {
@@ -27,6 +28,8 @@ import {
27
28
  renderedIntSignedness,
28
29
  writesNonPointerIntoPointer,
29
30
  } from '../l3/typing';
31
+ import { spellableType } from '../proto';
32
+ import { append } from './lines';
30
33
 
31
34
  // C operator precedence (lower binds tighter). Used to emit MINIMAL parentheses. Shared: C++ has
32
35
  // the same precedence for these operators.
@@ -35,6 +38,12 @@ const PREC: Record<BinOp, number> = {
35
38
  'f/': 3,
36
39
  'f+': 4,
37
40
  'f-': 4,
41
+ 'f<': 6,
42
+ 'f<=': 6,
43
+ 'f>': 6,
44
+ 'f>=': 6,
45
+ 'f==': 7,
46
+ 'f!=': 7,
38
47
  '*': 3,
39
48
  '/': 3,
40
49
  '/u': 3, // the unsigned twins spell as C's `/` and `%` (see C_SPELLING) — same precedence
@@ -72,8 +81,20 @@ const C_SPELLING: Partial<Record<BinOp, { token: string; signed: boolean }>> = {
72
81
  };
73
82
 
74
83
  /** 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/': '/' };
84
+ * arithmetic over two floats is the IEEE operation the machine did, and its relations over two
85
+ * floats are the IEEE compares — false on a NaN, `!=` true. */
86
+ const FLOAT_TOKEN: Partial<Record<BinOp, string>> = {
87
+ 'f+': '+',
88
+ 'f-': '-',
89
+ 'f*': '*',
90
+ 'f/': '/',
91
+ 'f<': '<',
92
+ 'f<=': '<=',
93
+ 'f>': '>',
94
+ 'f>=': '>=',
95
+ 'f==': '==',
96
+ 'f!=': '!=',
97
+ };
77
98
 
78
99
  /** Spell a recovered type in the decomp C-family typedef vocabulary (`s32`/`u32`/`u8`/`T *`). */
79
100
  export function cType(t: IrType): string {
@@ -197,10 +218,10 @@ function legalizedIndexBase(ix: Extract<Expr, { k: 'index' }>, vt: PrintEnv): Ex
197
218
  *
198
219
  * An existing integer cast AT THE SAME WIDTH is REPLACED rather than wrapped — `(u32)(s32)&g` and
199
220
  * `(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. */
221
+ * (structure/pointer-spelling.ts ARITH_ROWS). A cast at a DIFFERENT width is a narrowing the
222
+ * source wrote and must be kept. The replacement CARRIES the qualifier: re-typing a `volatile`
223
+ * cast without it drops an assertion the differ cannot referee the loss of, which is why
224
+ * l3/initfirst.ts's `stripWideIntCast` refuses the same peel one pass over. */
204
225
  function recastInt(x: Expr, signed: boolean, width: 32 | 64): Expr {
205
226
  const replaced = x.k === 'cast' && x.to.kind === 'int' && x.to.width === width ? x : undefined;
206
227
  return {
@@ -252,7 +273,8 @@ function pinnedOperands(e0: Extract<Expr, { k: 'bin' }>, wantSigned: boolean, vt
252
273
  * made and the compiled code is C's: an object pointer to a pointer of another pointee (a conversion
253
274
  * to `void *`, or one that only adds a qualifier, is implicit in C++ too), an integer to a pointer
254
275
  * (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`). */
276
+ * function compiled as C++ (`SFn.dialect`), or by a C front end that refuses the same three
277
+ * (`SFn.strictConversions` — mwcc). */
256
278
  function argConversion(a: Expr, to: string, vt: PrintEnv): boolean {
257
279
  const from = exprCType(a, vt.type);
258
280
  // qualifiers never decide it: a `T * const` parameter is a pointer parameter
@@ -344,7 +366,15 @@ function printExpr(e: Expr, parentPrec: number, vt: PrintEnv, leaf?: LeafHook):
344
366
  return `${e.fn}(${e.args
345
367
  .map((a, i) => {
346
368
  const to = declared?.[i];
347
- return to !== undefined && argConversion(a, to, vt) ? `(${to})${rec(a, 2)}` : printConverted(a, vt, leaf);
369
+ // A parameter type this cannot spell is an object pointer (target.ts `declaredArgTypes`),
370
+ // reached through `void *` from an argument whose own type is known to differ — a value
371
+ // this function types, or the address of an object it declares (a static) — never from
372
+ // `&gW` of a global only the project declares, which may already be the parameter's type.
373
+ const opaque = to !== undefined && !spellableType(to);
374
+ const typed = exprCType(a, vt.type) !== undefined || (a.k === 'addr' && vt.type(a.name) !== undefined);
375
+ return to !== undefined && (!opaque || typed) && argConversion(a, to, vt)
376
+ ? `(${opaque ? 'void *' : to})${rec(a, 2)}`
377
+ : printConverted(a, vt, leaf);
348
378
  })
349
379
  .join(', ')})`;
350
380
  }
@@ -383,7 +413,7 @@ function printExpr(e: Expr, parentPrec: number, vt: PrintEnv, leaf?: LeafHook):
383
413
  }
384
414
  return `${rec(base, 1)}${e.lead.map((l) => `[${rec(l, 99)}]`).join('')}[${rec(e.idx, 99)}]`;
385
415
  }
386
- if (e.idx.k === 'const' && e.idx.value === 0) {
416
+ if (e.idx.k === 'const' && e.idx.value === 0 && e.subscript !== true) {
387
417
  const s = `*${rec(base, 2)}`;
388
418
  return parentPrec < 2 ? `(${s})` : s;
389
419
  }
@@ -495,29 +525,31 @@ function printStmt(s: Stmt, indent: string, vt: PrintEnv, leaf?: LeafHook): stri
495
525
  }
496
526
  const out = [`${indent}if (${cond}) {`];
497
527
  for (const t of s.then) {
498
- out.push(...printStmt(t, indent + ' ', vt, leaf));
528
+ append(out, printStmt(t, indent + ' ', vt, leaf));
499
529
  }
500
530
  if (s.else.length) {
501
531
  out.push(`${indent}} else {`);
502
532
  for (const e of s.else) {
503
- out.push(...printStmt(e, indent + ' ', vt, leaf));
533
+ append(out, printStmt(e, indent + ' ', vt, leaf));
504
534
  }
505
535
  }
506
536
  out.push(`${indent}}`);
507
537
  return out;
508
538
  }
509
539
  case 'while': {
540
+ refuseMisboundBreak(s.body, 'loop');
510
541
  const out = [`${indent}while (${pe(s.cond, 99)}) {`];
511
542
  for (const t of s.body) {
512
- out.push(...printStmt(t, indent + ' ', vt, leaf));
543
+ append(out, printStmt(t, indent + ' ', vt, leaf));
513
544
  }
514
545
  out.push(`${indent}}`);
515
546
  return out;
516
547
  }
517
548
  case 'dowhile': {
549
+ refuseMisboundBreak(s.body, 'loop');
518
550
  const out = [`${indent}do {`];
519
551
  for (const t of s.body) {
520
- out.push(...printStmt(t, indent + ' ', vt, leaf));
552
+ append(out, printStmt(t, indent + ' ', vt, leaf));
521
553
  }
522
554
  out.push(`${indent}} while (${pe(s.cond, 99)});`);
523
555
  return out;
@@ -527,18 +559,28 @@ function printStmt(s: Stmt, indent: string, vt: PrintEnv, leaf?: LeafHook): stri
527
559
  // `recognizeForLoops` and l3/reindex.ts): init/inc are each a SINGLE-LINE `assign` statement. `clause` renders one
528
560
  // and strips its trailing `;` so it sits inside the header (`i = 0; c; i = i + 1`). A multi-line
529
561
  // statement (an `if`/nested loop) would render mangled — but the recognizer never builds one here.
562
+ refuseMisboundBreak(s.body, 'loop');
563
+ // An identity init (`i = i`, structure.ts `forPlanAt`: the name already holds the entry value)
564
+ // is the empty clause.
530
565
  const clause = (st: Stmt) => printStmt(st, '', vt, leaf).join(' ').replace(/;\s*$/, '').trim();
531
- const out = [`${indent}for (${clause(s.init)}; ${pe(s.cond, 99)}; ${clause(s.inc)}) {`];
566
+ const identity = s.init.k === 'assign' && s.init.value.k === 'var' && s.init.value.name === s.init.name;
567
+ const out = [`${indent}for (${identity ? '' : clause(s.init)}; ${pe(s.cond, 99)}; ${clause(s.inc)}) {`];
532
568
  for (const t of s.body) {
533
- out.push(...printStmt(t, indent + ' ', vt, leaf));
569
+ append(out, printStmt(t, indent + ' ', vt, leaf));
534
570
  }
535
571
  out.push(`${indent}}`);
536
572
  return out;
537
573
  }
574
+ // A labelled `break` is the end of a switch arm a case-end `goto` lands on (l3/ast.ts).
538
575
  case 'break':
539
- return [`${indent}break;`];
576
+ return s.label === undefined ? [`${indent}break;`] : [`${indent}${s.label}:`, `${indent}break;`];
540
577
  case 'continue':
541
578
  return [`${indent}continue;`];
579
+ // Where it may sit is the tree's contract (l3/ast.ts `caseGotoViolation`), which `emitCFamily`
580
+ // asks of the whole function before printing any of it.
581
+ case 'goto':
582
+ case 'caseEndGoto':
583
+ return [`${indent}goto ${s.label};`];
542
584
  case 'switch':
543
585
  return printSwitchStmt(s, indent, vt, leaf);
544
586
  }
@@ -550,11 +592,12 @@ function printStmt(s: Stmt, indent: string, vt: PrintEnv, leaf?: LeafHook): stri
550
592
  * is one call. */
551
593
  function printSwitchStmt(s: Extract<Stmt, { k: 'switch' }>, indent: string, vt: PrintEnv, leaf?: LeafHook): string[] {
552
594
  const pe = (e: Expr, p: number) => printExpr(e, p, vt, leaf);
553
- // A `break;` in an arm BODY is the innermost LOOP's in L3 (l3/ast.ts) and the SWITCH's in C,
554
- // so printing one between `case` labels rebinds it — a changed program that reads as ordinary
595
+ // A plain `break;` in an arm BODY is the innermost LOOP's in L3 (l3/ast.ts) and the SWITCH's in
596
+ // C, so printing one between `case` labels rebinds it — a changed program that reads as ordinary
555
597
  // C. Refused HERE because the rebinding is the printer's, for every producer rather than for
556
- // the two switch regimes. No recovery reaches it today (both regimes decline a loop-exiting
557
- // arm first, loudly), so it is a contract on the next one.
598
+ // the two switch regimes (`refuseMisboundBreak`, which checks a loop body the same way for a
599
+ // `break` marked `ofSwitch`). The structurer spells a loop exit inside an arm as the switch's
600
+ // own `break` or refuses it, so no recovery reaches either today: a contract on the next one.
558
601
  //
559
602
  // `continue;` is deliberately NOT refused: C binds it to the smallest enclosing ITERATION
560
603
  // statement, which a `switch` is not, so it already means what L3 means. What a pass that re-spells
@@ -563,9 +606,7 @@ function printSwitchStmt(s: Extract<Stmt, { k: 'switch' }>, indent: string, vt:
563
606
  // `respellCountdown` in l3/reindex.ts, and l3/unreduce.ts) each scan switch arms for the node
564
607
  // before firing, and a fourth must too.
565
608
  for (const body of [...s.cases.map((c) => c.body), s.default ?? []]) {
566
- if (switchBoundBreakIn(body)) {
567
- throw new Error('c backend: a switch arm carries a loop-scoped `break;`, which C would bind to the switch');
568
- }
609
+ refuseMisboundBreak(body, 'switch');
569
610
  }
570
611
  const out = [`${indent}switch (${pe(s.scrutinee, 99)}) {`];
571
612
  const ci = indent + ' '; // case-label indent
@@ -585,7 +626,7 @@ function printSwitchStmt(s: Extract<Stmt, { k: 'switch' }>, indent: string, vt:
585
626
  const printDefault = (): void => {
586
627
  out.push(`${ci}default:`);
587
628
  for (const t of s.default!) {
588
- out.push(...printStmt(t, bi, vt, leaf));
629
+ append(out, printStmt(t, bi, vt, leaf));
589
630
  }
590
631
  // A default that is NOT last would otherwise fall into the case below it — the mirror of the
591
632
  // rule for cases. The last one needs no `break;` because there is nothing under it.
@@ -608,8 +649,13 @@ function printSwitchStmt(s: Extract<Stmt, { k: 'switch' }>, indent: string, vt:
608
649
  for (const v of c.values) {
609
650
  out.push(`${ci}case ${v}:`);
610
651
  }
652
+ // The label a `goto` of this switch lands on, after the arm's case labels: entering it there
653
+ // runs exactly what falling into the arm runs.
654
+ if (c.label !== undefined) {
655
+ out.push(`${ci}${c.label}:`);
656
+ }
611
657
  for (const t of c.body) {
612
- out.push(...printStmt(t, bi, vt, leaf));
658
+ append(out, printStmt(t, bi, vt, leaf));
613
659
  }
614
660
  // A case whose body ends in `return`/`break` (a terminated arm) needs no `break;`; only an
615
661
  // open non-fall-through arm gets one. `fallsThrough` omits it so control drops to the next case.
@@ -624,13 +670,26 @@ function printSwitchStmt(s: Extract<Stmt, { k: 'switch' }>, indent: string, vt:
624
670
  return out;
625
671
  }
626
672
 
627
- /** Does this arm body carry a `break` C would bind to the enclosing SWITCH rather than to the loop
628
- * L3 meant? A loop opened inside the arm captures its own, so its body is not walked; a nested
629
- * `switch` captures one too and is checked when it is printed. */
630
- function switchBoundBreakIn(body: Stmt[]): boolean {
631
- return body.some(
632
- (s) => s.k === 'break' || (s.k === 'if' && (switchBoundBreakIn(s.then) || switchBoundBreakIn(s.else))),
633
- );
673
+ /** L3 has two `break`s (l3/ast.ts) and C one: C binds `break;` to the innermost enclosing loop OR
674
+ * switch, so a `break` prints as what L3 meant only where its own kind is that innermost
675
+ * statement. `body` is a loop body or a switch arm (`owner`); a loop or switch opened inside it
676
+ * captures its own breaks and is checked when it is printed, so it is not walked. Refused loud on
677
+ * either mismatch — a plain (loop) `break` in an arm, a switch's `break` in a loop body — since
678
+ * each prints as ordinary C that leaves a different statement. */
679
+ function refuseMisboundBreak(body: Stmt[], owner: 'loop' | 'switch'): void {
680
+ const misbound = (ss: Stmt[]): boolean =>
681
+ ss.some(
682
+ (s) =>
683
+ (s.k === 'break' && (s.ofSwitch === true) !== (owner === 'switch')) ||
684
+ (s.k === 'if' && (misbound(s.then) || misbound(s.else))),
685
+ );
686
+ if (misbound(body)) {
687
+ throw new Error(
688
+ owner === 'switch'
689
+ ? 'c backend: a switch arm carries a loop-scoped `break;`, which C would bind to the switch'
690
+ : 'c backend: a loop body carries a switch-scoped `break;`, which C would bind to the loop',
691
+ );
692
+ }
634
693
  }
635
694
 
636
695
  // Does a statement list end in a control-flow terminator (so a trailing `break;` would be dead)?
@@ -641,10 +700,28 @@ function endsTerminated(body: Stmt[]): boolean {
641
700
  (last.k === 'return' ||
642
701
  last.k === 'break' ||
643
702
  last.k === 'continue' ||
703
+ last.k === 'goto' ||
704
+ last.k === 'caseEndGoto' ||
644
705
  (last.k === 'if' && endsTerminated(last.then) && last.else.length > 0 && endsTerminated(last.else)))
645
706
  );
646
707
  }
647
708
 
709
+ /** Is `e` a pointer to another pointee than the pointer slot `t` declares, as rendered? Its pointer
710
+ * type is its C type, or, for the address of a declared object (which `exprCType` leaves
711
+ * unknown), a pointer to that object or to an array's element, the way it decays. A `void *` slot
712
+ * takes every object pointer implicitly. A `void *` value converts into any pointer slot in C
713
+ * (mwcc's C takes `gp = (void *)x` into an `s32 *gp`, compiled) but not in C++ (`cxx`). An unknown
714
+ * type is no opinion. */
715
+ function otherPointee(t: IrType | undefined, e: Expr, vt: (name: string) => IrType | undefined, cxx: boolean): boolean {
716
+ if (t?.kind !== 'ptr' || t.to.kind === 'void') {
717
+ return false;
718
+ }
719
+ const of = e.k === 'addr' ? vt(e.name) : undefined;
720
+ const ct = of !== undefined ? T.ptr(of.kind === 'array' ? of.elem : of) : exprCType(e, vt);
721
+ const p = ct?.kind === 'array' ? T.ptr(ct.elem) : ct;
722
+ return p?.kind === 'ptr' && (cxx || p.to.kind !== 'void') && !typeEquals(p, t);
723
+ }
724
+
648
725
  // C-FAMILY WRITE LEGALIZATION (the assign-side sibling of the deref legalization in printExpr):
649
726
  // the C family's answer to `writesNonPointerIntoPointer` is the reinterpret cast to the DECLARED
650
727
  // type — exactly what the machine's register move does — where Pascal, having no such cast,
@@ -654,9 +731,18 @@ function legalizePointerWrites(fn: SFn): SFn {
654
731
  const vt = declaredTypes(fn);
655
732
  // …and the mirror: a rendered pointer into an INTEGER slot takes the slot's type. A cast keeps
656
733
  // the sum it wraps, so the bytes are the uncast write's; CodeWarrior rejects that write outright.
734
+ //
735
+ // …and, where the compiler refuses the third implicit conversion too (C++, or mwcc's C:
736
+ // `SFn.strictConversions`), a pointer of ANOTHER pointee into a pointer slot: mwcc rejects
737
+ // `return t;` from an `s32 *` function when `t` is a `const u32[2]` static (mwcc: "illegal implicit
738
+ // conversion" of an `unsigned int *` to an `int *`, compiled), and takes it cast (`otherPointee`
739
+ // says which `void *` converts implicitly).
740
+ const strict = fn.dialect === 'c++' || fn.strictConversions === true;
657
741
  const castTo = (t: IrType | undefined, e: Expr): Expr =>
658
- writesNonPointerIntoPointer(t, e, vt) || (t?.kind === 'int' && exprCType(e, vt)?.kind === 'ptr')
659
- ? { k: 'cast', to: t, e }
742
+ writesNonPointerIntoPointer(t, e, vt) ||
743
+ (t?.kind === 'int' && exprCType(e, vt)?.kind === 'ptr') ||
744
+ (strict && otherPointee(t, e, vt, fn.dialect === 'c++'))
745
+ ? { k: 'cast', to: t!, e }
660
746
  : e;
661
747
  const fix = (s: Stmt): Stmt => {
662
748
  switch (s.k) {
@@ -692,7 +778,7 @@ function legalizePointerWrites(fn: SFn): SFn {
692
778
  /** The body of a C-family function: local declarations + statements, one string per line. The
693
779
  * SIGNATURE (return type + name + params, plus any C++ scope/`this`/mangling) is the caller's —
694
780
  * that is the language-divergent part each backend owns. */
695
- function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
781
+ function cFamilyBody(fn0: SFn, leaf: LeafHook | undefined, designators: boolean): string[] {
696
782
  const fn = legalizePointerWrites(fn0);
697
783
  // The legalization env: every printed var's declared type and pointee volatility, from the SAME
698
784
  // params/locals the emitted declarations come from — so the printer judges exactly the C the
@@ -700,7 +786,7 @@ function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
700
786
  const vt: PrintEnv = printEnv(fn);
701
787
  const lines: string[] = [];
702
788
  for (const st of fn.statics ?? []) {
703
- lines.push(...staticDefinition(st));
789
+ append(lines, staticDefinition(st));
704
790
  }
705
791
  for (const l of fn.locals) {
706
792
  // Both facts render at the PREFIX position, where C's declarator grammar reads them
@@ -710,19 +796,37 @@ function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
710
796
  // no variation or recognizer produces one. The NAME is placed by `cDeclare`, the one
711
797
  // placement every declaration position in this backend uses: a local can carry an array type
712
798
  // — 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)};`);
799
+ const head = ` ${l.volatile || l.pointeeVolatile ? 'volatile ' : ''}${cDeclare(l.type, l.name)}`;
800
+ lines.push(
801
+ ...(l.init === undefined ? [`${head};`] : initialized(head, l.type, l.init, l.initString === true, designators)),
802
+ );
714
803
  }
715
804
  for (const s of fn.body) {
716
- lines.push(...printStmt(s, ' ', vt, leaf));
805
+ append(lines, printStmt(s, ' ', vt, leaf));
717
806
  }
718
807
  return lines;
719
808
  }
720
809
 
721
- /** One initial element: hex from 10 up, a negative one with its sign, so a byte table reads as the
810
+ /** One initial number: hex from 10 up, a negative one with its sign, so a byte table reads as the
722
811
  * bytes and a signed one as its values. */
723
- const initElement = (v: number): string =>
812
+ const initNumber = (v: number): string =>
724
813
  Math.abs(v) < 10 ? `${v}` : `${v < 0 ? '-' : ''}0x${Math.abs(v).toString(16)}`;
725
814
 
815
+ /** One initial element of type `elem`: a number, or the address constant a relocated one holds,
816
+ * converted to the element's integer type — `(u32)&sym`, and with an addend `(u32)((u8 *)&sym + k)`:
817
+ * the offset in bytes, as the relocation counts it. Not `(u32)&sym + k`, which agbcc folds alike but
818
+ * mwcc refuses as an illegal constant expression (compiled); through a byte pointer both write the
819
+ * pointer-typed source's word and relocation (`&garr[2]`, `&garr[-1]`, compiled on both). */
820
+ function initElement(v: number | InitAddress, elem: IrType): string {
821
+ if (typeof v === 'number') {
822
+ return initNumber(v);
823
+ }
824
+ const t = cType(elem);
825
+ return v.addend === 0
826
+ ? `(${t})&${v.symbol}`
827
+ : `(${t})((u8 *)&${v.symbol} ${v.addend < 0 ? '-' : '+'} ${initNumber(Math.abs(v.addend))})`;
828
+ }
829
+
726
830
  /** A byte array's initial elements as a string literal: printable characters as themselves, every
727
831
  * other byte in three-digit octal (which no following digit can extend), and a `?` after a `?`
728
832
  * escaped so no trigraph forms. The trailing zeros are left to the declared size, which C fills
@@ -750,26 +854,45 @@ function stringLiteral(init: readonly number[]): string {
750
854
  function staticDefinition(st: SStatic): string[] {
751
855
  const aligned = st.align === undefined ? '' : ` __attribute__((aligned(${st.align})))`;
752
856
  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)};`];
857
+ return st.init === undefined ? [`${head};`] : initialized(head, st.type, st.init, st.string === true, false);
858
+ }
859
+
860
+ /** A declaration `head` of `type` with its initializer: a string literal, one scalar, or an
861
+ * array's elements laid out eight to a line. An element left out (null) is skipped and the next
862
+ * one spelled by designator, `[k] = v` — or, where the compiler cannot parse one (`SFn.designators`
863
+ * absent: IDO, mwcc) or the language has none (C++), spelled as the zero it is. */
864
+ function initialized(
865
+ head: string,
866
+ type: IrType,
867
+ init: readonly (number | InitAddress | null)[],
868
+ string: boolean,
869
+ designators: boolean,
870
+ ): string[] {
871
+ const whole = init.map((v) => v ?? 0);
872
+ if (string) {
873
+ // a string's elements are bytes, and no byte holds an address (structure/local-statics.ts)
874
+ const bytes = whole.filter((v): v is number => typeof v === 'number');
875
+ if (bytes.length !== whole.length) {
876
+ throw new Error(`internal: a string initializer of '${head.trim()}' holds an address`);
877
+ }
878
+ return [`${head} = ${stringLiteral(bytes)};`];
758
879
  }
759
- if (st.type.kind !== 'array') {
760
- return [`${head} = ${initElement(st.init[0])};`];
880
+ if (type.kind !== 'array') {
881
+ return [`${head} = ${initElement(whole[0], type)};`];
761
882
  }
762
- if (st.init.length <= 8) {
763
- return [`${head} = { ${st.init.map(initElement).join(', ')} };`];
883
+ const items: string[] = [];
884
+ init.forEach((v, i) => {
885
+ if (v !== null || !designators) {
886
+ const e = initElement(v ?? 0, type.elem);
887
+ items.push(init[i - 1] === null && designators ? `[${i}] = ${e}` : e);
888
+ }
889
+ });
890
+ if (items.length <= 8) {
891
+ return [`${head} = { ${items.join(', ')} };`];
764
892
  }
765
893
  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
- );
894
+ for (let i = 0; i < items.length; i += 8) {
895
+ rows.push(` ${items.slice(i, i + 8).join(', ')},`);
773
896
  }
774
897
  return [`${head} = {`, ...rows, ' };'];
775
898
  }
@@ -783,16 +906,29 @@ function structDecls(fn: SFn): string[] {
783
906
  return (fn.structs ?? []).map((s) => renderStructDecl(s.name, s.fields));
784
907
  }
785
908
 
786
- /** Assemble a full C-family function from a caller-supplied signature line and the shared body. */
787
- export function emitCFamily(signature: string, fn0: SFn, leaf?: LeafHook): string {
909
+ /** Assemble a full C-family function from a caller-supplied signature line and the shared body.
910
+ * `designators`: the LANGUAGE spells an array element by index in an initializer (C, not C++).
911
+ * Whether the compiler's dialect parses one is the function's own fact (`SFn.designators`); an
912
+ * initializer is spelled by index only where both say yes. */
913
+ export function emitCFamily(signature: string, fn0: SFn, leaf?: LeafHook, designators = true): string {
788
914
  // The declaration list is put into the target's own frame order, and a pointer cell's integer
789
915
  // uses converted (l3/ptrcell.ts), HERE — in the shared C-family assembler, reached only from
790
916
  // `cBackend.emit` and `cppBackend(...).emit`, so both C-family backends print them and no `.emit(`
791
917
  // call site does (l3/slotorder.ts says why `emit` owns it).
792
- const fn = orderSlotLocals(legalizePointerCells(fn0));
918
+ // …and the compiler's anonymous constant images folded into the initializers of the locals
919
+ // their copies fill (l3/constinit.ts), which the tree keeps as copies until here.
920
+ const fn = orderSlotLocals(legalizePointerCells(foldConstantImages(fn0)));
921
+ const stray = caseGotoViolation(fn.body);
922
+ if (stray !== null) {
923
+ throw new Error(`c backend: ${stray}`);
924
+ }
793
925
  const decls = structDecls(fn);
794
926
  const preamble = decls.length ? decls.join('\n') + '\n' : '';
795
- return preamble + [`${signature} {`, ...cFamilyBody(fn, leaf), '}'].join('\n') + '\n';
927
+ return (
928
+ preamble +
929
+ [`${signature} {`, ...cFamilyBody(fn, leaf, designators && fn.designators === true), '}'].join('\n') +
930
+ '\n'
931
+ );
796
932
  }
797
933
 
798
934
  // One C-family comment spelling, shared by the C and C++ backends. A `*` followed by `/`
@@ -138,6 +138,8 @@ export function cppBackend(spec: CppFnSpec, floatSlots: FloatSlots | undefined):
138
138
  return {
139
139
  id: 'cpp',
140
140
  spellsSwitchFallthrough: true,
141
+ spellsSwitchBreak: true,
142
+ spellsEarlyReturn: true,
141
143
  emit(fn: SFn): string {
142
144
  // Map each lifted param var → its C++ meaning: `this` (bare member access) or a named param
143
145
  // (a pointer-to-class param uses `->`). A pointer-to-known-class param is a member receiver.
@@ -223,7 +225,7 @@ export function cppBackend(spec: CppFnSpec, floatSlots: FloatSlots | undefined):
223
225
  const paramList = spec.params.map((p) => declareCpp(p.type, p.name)).join(', ');
224
226
  const decls = classDecls(spec, paramList);
225
227
  const signature = `${spellType(spec.retType)} ${spec.cls ? spec.cls + '::' : ''}${spec.method}(${paramList})`;
226
- return (decls ? decls + '\n' : '') + emitCFamily(signature, fn, leaf);
228
+ return (decls ? decls + '\n' : '') + emitCFamily(signature, fn, leaf, false);
227
229
  },
228
230
  comment: cComment, // C++ shares C's block-comment spelling
229
231
  };
@@ -0,0 +1,10 @@
1
+ /** `out.push(...lines)` without the spread: a spread passes every line as an ARGUMENT, and V8 throws
2
+ * "Maximum call stack size exceeded" past about 120k of them (120,000 print, 130,000 throw, node
3
+ * 24). A structuring with a long arm reaches that at a nesting depth of a few dozen, so the throw
4
+ * reads as deep recursion when it is only size. Every printer appends a child's lines with this,
5
+ * so the limit binds no backend (the C family and Pascal alike). */
6
+ export function append(out: string[], lines: readonly string[]): void {
7
+ for (const line of lines) {
8
+ out.push(line);
9
+ }
10
+ }
@@ -12,6 +12,7 @@ import { IrType, typeToString } from '../ir/types';
12
12
  import { BinOp, Expr, LanguageBackend, SFn, Stmt, isLoop } from '../l3/ast';
13
13
  import { orderSlotLocals } from '../l3/slotorder';
14
14
  import { type VarTypes, declaredTypes, derefStrideOk, exprCType, writesNonPointerIntoPointer } from '../l3/typing';
15
+ import { append } from './lines';
15
16
 
16
17
  // Infix operators IDO Pascal spells directly.
17
18
  const OP: Partial<Record<BinOp, string>> = {
@@ -140,10 +141,10 @@ function makePrinter(vt: VarTypes) {
140
141
  // Casts have no faithful IDO-Pascal spelling yet — fail LOUD rather than emit silently-wrong
141
142
  // source. Tree-level producers reaching here: the width-narrowing idiom casts (agbcc-gated,
142
143
  // so never on this path today), structure.ts's STRUCT-pointer casts (unreachable too — the
143
- // `field` case above throws first), intify's `(s32)ptr` legalization (any target), and the
144
- // byte-pointer walk of a pointer offset by a runtime value (any target, and reachable with no
145
- // `field` in the tree — it declines two functions that used to emit here, whose Pascal was
146
- // silently walking ELEMENTS where the asm walked bytes).
144
+ // `field` case above throws first), the arithmetic table's `(s32)ptr` legalization (any
145
+ // target), and the byte-pointer walk of a pointer offset by a runtime value (any target,
146
+ // and reachable with no `field` in the tree, where a Pascal spelling would walk ELEMENTS
147
+ // where the asm walked bytes).
147
148
  // Scalar deref casts never appear in the tree — the index case above owns that judgment.
148
149
  case 'cast':
149
150
  throw new Error(`pascal backend: cast has no IDO Pascal spelling yet`);
@@ -202,23 +203,30 @@ function makePrinter(vt: VarTypes) {
202
203
  return s.value ? [`${indent}${fnName} := ${pe(s.value)};`] : [];
203
204
  case 'if': {
204
205
  const cond = pe(s.cond);
205
- const block = (stmts: Stmt[], ind: string) =>
206
- stmts.length === 1
206
+ // A branch goes bare only where it is ONE Pascal statement that cannot capture what follows.
207
+ // A `for` prints as two (its init, then a `while`), so it always takes begin/end. And with an
208
+ // `else` to come, a then-branch that is itself an `if` or a loop (whose body may end in one)
209
+ // would take that `else` as its own innermost `if`'s — Pascal's dangling else, which C's
210
+ // braces had ruled out: `if (a) { if (b) X; } else Y;` printed bare runs Y when b fails.
211
+ const bare = (stmts: Stmt[], open: boolean) =>
212
+ stmts.length === 1 && stmts[0].k !== 'for' && !(open && (stmts[0].k === 'if' || isLoop(stmts[0])));
213
+ const block = (stmts: Stmt[], ind: string, open = false) =>
214
+ bare(stmts, open)
207
215
  ? ps(fnName, stmts[0], ind, tail)
208
216
  : [`${ind}begin`, ...list(stmts, ind + ' ', tail), `${ind}end`];
209
217
  // Pascal: no `;` before `else`; the branch statements already carry their own.
210
218
  const out = [`${indent}if ${cond} then`];
211
- out.push(...block(s.then, indent + ' '));
219
+ append(out, block(s.then, indent + ' ', s.else.length > 0));
212
220
  if (s.else.length) {
213
221
  out.push(`${indent}else`);
214
- out.push(...block(s.else, indent + ' '));
222
+ append(out, block(s.else, indent + ' '));
215
223
  }
216
224
  return out;
217
225
  }
218
226
  case 'while': {
219
227
  // loop bodies are NEVER tail position — control returns to the test
220
228
  const body =
221
- s.body.length === 1
229
+ s.body.length === 1 && s.body[0].k !== 'for'
222
230
  ? ps(fnName, s.body[0], indent + ' ')
223
231
  : [`${indent}begin`, ...s.body.flatMap((x) => ps(fnName, x, indent + ' ')), `${indent}end`];
224
232
  return [`${indent}while ${pe(s.cond)} do`, ...body];
@@ -248,12 +256,19 @@ function makePrinter(vt: VarTypes) {
248
256
  throw new Error('pascal backend: `break` has no IDO Pascal spelling');
249
257
  case 'continue':
250
258
  throw new Error('pascal backend: `continue` has no IDO Pascal spelling');
259
+ // A jump into another arm of a `case-of` has no Pascal spelling: an arm is a statement, and a
260
+ // label may not be entered from outside the statement that holds it.
261
+ case 'goto':
262
+ throw new Error('pascal backend: a `goto` into a case arm has no IDO Pascal spelling');
263
+ // A jump to an arm's end lands on a `break`, which Pascal has none of either.
264
+ case 'caseEndGoto':
265
+ throw new Error('pascal backend: a `goto` to the end of a case arm has no IDO Pascal spelling');
251
266
  case 'switch': {
252
267
  // IDO/SGI Pascal `case E of L: S; … otherwise S end`. There is NO fall-through in Pascal case-of,
253
268
  // so a `fallsThrough` arm has no faithful spelling → loud-fail (mirrors the `field`/`cast`/`%`
254
269
  // throws above). Each arm's body is a single statement or a begin/end block.
255
270
  const arm = (body: Stmt[], ind: string) =>
256
- body.length === 1
271
+ body.length === 1 && body[0].k !== 'for'
257
272
  ? ps(fnName, body[0], ind + ' ')
258
273
  : [`${ind} begin`, ...body.flatMap((x) => ps(fnName, x, ind + ' ')), `${ind} end`];
259
274
  // Pascal has no early `return`: a `return v` inside a case renders `fnName := v` and then FALLS
@@ -277,11 +292,15 @@ function makePrinter(vt: VarTypes) {
277
292
  if (c.fallsThrough) {
278
293
  throw new Error('pascal backend: switch fall-through has no faithful IDO Pascal case-of spelling');
279
294
  }
280
- out.push(`${indent} ${c.values.join(', ')}:`, ...arm(c.body, indent + ' '));
295
+ out.push(`${indent} ${c.values.join(', ')}:`);
296
+ append(out, arm(c.body, indent + ' '));
281
297
  }
282
- if (s.default) {
283
- out.push(`${indent} otherwise`);
284
- out.push(...arm(s.default, indent + ' '));
298
+ // A C `switch` with no `default` runs on past a value it does not list; IDO Pascal's
299
+ // `case` raises a run-time `caseerror` there instead. So the clause is always printed, an
300
+ // empty one when the source had no default.
301
+ out.push(`${indent} otherwise`);
302
+ if (s.default?.length) {
303
+ append(out, arm(s.default, indent + ' '));
285
304
  }
286
305
  out.push(`${indent}end;`);
287
306
  return out;
@@ -298,17 +317,29 @@ export const pascalBackend: LanguageBackend = {
298
317
  // spells as plain if-nesting, which Pascal prints, so the choice is between a decompiled
299
318
  // function and a stub.
300
319
  spellsSwitchFallthrough: false,
320
+ // Nor has Pascal a `break`, so recovery must not mint a switch's mid-arm one either: a closed arm
321
+ // that leaves early spells as nested `if`s that copy the arm's tail into each path.
322
+ spellsSwitchBreak: false,
323
+ // Nor an early `return` (the `return` case below loud-fails one not in tail position), so
324
+ // structuring reads the return-tail copies rather than the join past early returns.
325
+ spellsEarlyReturn: false,
301
326
  emit(fn0: SFn): string {
302
327
  // The declaration list is put into the target's own frame order HERE, as the C family does it
303
328
  // in its shared assembler — owned by `emit`, never by a `.emit(` call site (l3/slotorder.ts).
304
329
  const fn = orderSlotLocals(fn0);
305
330
  // A function-scope static is a definition the MIPS frontends never recover, and IDO Pascal's
306
- // spelling of one (a typed constant, or a unit-level var) has not been measured — loud.
331
+ // spelling of one (a typed constant, or a unit-level var) has not been measured — loud. So is
332
+ // the compiler's anonymous constant a local's initializer is copied from (l3/constinit.ts).
307
333
  if (fn.statics?.length) {
308
334
  throw new Error(
309
335
  `pascal backend: the function-scope static '${fn.statics[0].name}' has no IDO Pascal spelling yet`,
310
336
  );
311
337
  }
338
+ if (fn.images?.length) {
339
+ throw new Error(
340
+ `pascal backend: the compiler's anonymous constant '${fn.images[0].name}' a local is initialized from has no IDO Pascal spelling yet`,
341
+ );
342
+ }
312
343
  // Same env discipline as the C family (cfamily.ts cFamilyBody): the printer judges derefs
313
344
  // against the exact declarations it emits.
314
345
  const ps = makePrinter(declaredTypes(fn));
@@ -332,7 +363,7 @@ export const pascalBackend: LanguageBackend = {
332
363
  lines.push('var', ...fn.locals.map((l) => ` ${l.name}: ${pasType(l.type)};`));
333
364
  }
334
365
  lines.push('begin');
335
- fn.body.forEach((s, i) => lines.push(...ps(fn.name, s, ' ', i === fn.body.length - 1)));
366
+ fn.body.forEach((s, i) => append(lines, ps(fn.name, s, ' ', i === fn.body.length - 1)));
336
367
  lines.push('end;');
337
368
  return lines.join('\n') + '\n';
338
369
  },