@asmlift/core 0.9.0 → 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 (101) 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 +1 -0
  5. package/src/backend/cfamily.ts +165 -50
  6. package/src/backend/cpp.ts +2 -1
  7. package/src/backend/pascal.ts +31 -12
  8. package/src/contracts.ts +173 -16
  9. package/src/declare.ts +69 -20
  10. package/src/frontend/asmdata.ts +7 -1
  11. package/src/frontend/call-plan.ts +964 -60
  12. package/src/frontend/dead-reads.ts +628 -0
  13. package/src/frontend/device-pins.ts +23 -4
  14. package/src/frontend/emit.ts +4 -3
  15. package/src/frontend/fpu.ts +224 -17
  16. package/src/frontend/frame-objects.ts +1232 -192
  17. package/src/frontend/frontend.ts +64 -3
  18. package/src/frontend/high-half.ts +53 -4
  19. package/src/frontend/jump-table.ts +530 -0
  20. package/src/frontend/local-object.ts +158 -32
  21. package/src/frontend/mips.ts +2076 -215
  22. package/src/frontend/own-struct-return.ts +500 -0
  23. package/src/frontend/ppc.ts +2711 -232
  24. package/src/frontend/register-struct-call.ts +484 -0
  25. package/src/frontend/reloc-symbol.ts +22 -7
  26. package/src/frontend/ssa.ts +1460 -44
  27. package/src/frontend/stackargs.ts +106 -57
  28. package/src/frontend/thumb.ts +2044 -318
  29. package/src/ir/alias.ts +42 -0
  30. package/src/ir/bits.ts +681 -1
  31. package/src/ir/core.ts +100 -5
  32. package/src/ir/discipline.ts +86 -21
  33. package/src/ir/entry.ts +159 -0
  34. package/src/{structure → ir}/loops.ts +6 -2
  35. package/src/ir/opcodes.ts +109 -9
  36. package/src/ir/types.ts +86 -2
  37. package/src/ir/verify.ts +80 -11
  38. package/src/l3/ast.ts +290 -18
  39. package/src/l3/basecse.ts +33 -5
  40. package/src/l3/constinit.ts +382 -0
  41. package/src/l3/dce.ts +2 -2
  42. package/src/l3/initfirst.ts +42 -11
  43. package/src/l3/intcell.ts +2 -1
  44. package/src/l3/narrowdecl.ts +77 -36
  45. package/src/l3/pollguard.ts +8 -2
  46. package/src/l3/ptrcell.ts +31 -34
  47. package/src/l3/ptrfield.ts +123 -8
  48. package/src/l3/regspell.ts +9 -4
  49. package/src/l3/reindex.ts +283 -96
  50. package/src/l3/storage.ts +98 -1
  51. package/src/l3/symbol-refs.ts +84 -6
  52. package/src/l3/tailmerge.ts +4 -2
  53. package/src/l3/typing.ts +108 -15
  54. package/src/l3/unreduce.ts +6 -5
  55. package/src/l3/volatileptr.ts +17 -54
  56. package/src/mangle.ts +30 -0
  57. package/src/pattern/engine.ts +361 -73
  58. package/src/pipeline.ts +541 -24
  59. package/src/proto-context.ts +905 -45
  60. package/src/proto.ts +555 -40
  61. package/src/raise/aggmembers.ts +290 -0
  62. package/src/raise/arrays.ts +5 -1
  63. package/src/raise/declared-volatile.ts +207 -31
  64. package/src/raise/floathelpers.ts +254 -33
  65. package/src/raise/gvn.ts +3 -3
  66. package/src/raise/latch.ts +162 -6
  67. package/src/raise/memberarrays.ts +10 -4
  68. package/src/raise/offsetnames.ts +10 -26
  69. package/src/raise/pairparams.ts +33 -0
  70. package/src/raise/paramwidth.ts +196 -6
  71. package/src/raise/pre-recovery.ts +102 -19
  72. package/src/raise/pre-structure.ts +54 -0
  73. package/src/raise/recover.ts +12 -5
  74. package/src/raise/reroll.ts +694 -0
  75. package/src/raise/shortcircuit.ts +26 -12
  76. package/src/raise/softdiv.ts +1 -1
  77. package/src/raise/storesign.ts +153 -0
  78. package/src/raise/struct-arrays.ts +30 -11
  79. package/src/raise/structs.ts +396 -33
  80. package/src/raise/truncload.ts +4 -3
  81. package/src/raise/unthread.ts +400 -0
  82. package/src/rank-declare.ts +118 -12
  83. package/src/rank-variations.ts +89 -31
  84. package/src/rank.ts +307 -54
  85. package/src/runtime-helpers.ts +140 -22
  86. package/src/structure/analysis.ts +165 -2
  87. package/src/structure/arith-ops.ts +23 -0
  88. package/src/structure/globalaccess.ts +111 -9
  89. package/src/structure/hazards.ts +16 -64
  90. package/src/structure/local-statics.ts +145 -9
  91. package/src/structure/namecoalesce.ts +1 -1
  92. package/src/structure/pointer-spelling.ts +1106 -0
  93. package/src/structure/redundant-test.ts +1 -1
  94. package/src/structure/structure.ts +2180 -1054
  95. package/src/structure/switch-recover.ts +244 -100
  96. package/src/symbols.ts +6 -0
  97. package/src/target.ts +765 -52
  98. package/src/trace.ts +26 -8
  99. package/src/variation-definitions.ts +126 -14
  100. package/src/variation-gates.ts +2 -0
  101. package/src/variation-tokens.ts +5 -1
@@ -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, 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,7 @@ import {
27
28
  renderedIntSignedness,
28
29
  writesNonPointerIntoPointer,
29
30
  } from '../l3/typing';
31
+ import { spellableType } from '../proto';
30
32
 
31
33
  // C operator precedence (lower binds tighter). Used to emit MINIMAL parentheses. Shared: C++ has
32
34
  // the same precedence for these operators.
@@ -35,6 +37,12 @@ const PREC: Record<BinOp, number> = {
35
37
  'f/': 3,
36
38
  'f+': 4,
37
39
  'f-': 4,
40
+ 'f<': 6,
41
+ 'f<=': 6,
42
+ 'f>': 6,
43
+ 'f>=': 6,
44
+ 'f==': 7,
45
+ 'f!=': 7,
38
46
  '*': 3,
39
47
  '/': 3,
40
48
  '/u': 3, // the unsigned twins spell as C's `/` and `%` (see C_SPELLING) — same precedence
@@ -72,8 +80,20 @@ const C_SPELLING: Partial<Record<BinOp, { token: string; signed: boolean }>> = {
72
80
  };
73
81
 
74
82
  /** 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/': '/' };
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
+ };
77
97
 
78
98
  /** Spell a recovered type in the decomp C-family typedef vocabulary (`s32`/`u32`/`u8`/`T *`). */
79
99
  export function cType(t: IrType): string {
@@ -197,10 +217,10 @@ function legalizedIndexBase(ix: Extract<Expr, { k: 'index' }>, vt: PrintEnv): Ex
197
217
  *
198
218
  * An existing integer cast AT THE SAME WIDTH is REPLACED rather than wrapped — `(u32)(s32)&g` and
199
219
  * `(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. */
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. */
204
224
  function recastInt(x: Expr, signed: boolean, width: 32 | 64): Expr {
205
225
  const replaced = x.k === 'cast' && x.to.kind === 'int' && x.to.width === width ? x : undefined;
206
226
  return {
@@ -252,7 +272,8 @@ function pinnedOperands(e0: Extract<Expr, { k: 'bin' }>, wantSigned: boolean, vt
252
272
  * made and the compiled code is C's: an object pointer to a pointer of another pointee (a conversion
253
273
  * to `void *`, or one that only adds a qualifier, is implicit in C++ too), an integer to a pointer
254
274
  * (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`). */
275
+ * function compiled as C++ (`SFn.dialect`), or by a C front end that refuses the same three
276
+ * (`SFn.strictConversions` — mwcc). */
256
277
  function argConversion(a: Expr, to: string, vt: PrintEnv): boolean {
257
278
  const from = exprCType(a, vt.type);
258
279
  // qualifiers never decide it: a `T * const` parameter is a pointer parameter
@@ -344,7 +365,15 @@ function printExpr(e: Expr, parentPrec: number, vt: PrintEnv, leaf?: LeafHook):
344
365
  return `${e.fn}(${e.args
345
366
  .map((a, i) => {
346
367
  const to = declared?.[i];
347
- return to !== undefined && argConversion(a, to, vt) ? `(${to})${rec(a, 2)}` : printConverted(a, vt, leaf);
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);
348
377
  })
349
378
  .join(', ')})`;
350
379
  }
@@ -383,7 +412,7 @@ function printExpr(e: Expr, parentPrec: number, vt: PrintEnv, leaf?: LeafHook):
383
412
  }
384
413
  return `${rec(base, 1)}${e.lead.map((l) => `[${rec(l, 99)}]`).join('')}[${rec(e.idx, 99)}]`;
385
414
  }
386
- if (e.idx.k === 'const' && e.idx.value === 0) {
415
+ if (e.idx.k === 'const' && e.idx.value === 0 && e.subscript !== true) {
387
416
  const s = `*${rec(base, 2)}`;
388
417
  return parentPrec < 2 ? `(${s})` : s;
389
418
  }
@@ -507,6 +536,7 @@ function printStmt(s: Stmt, indent: string, vt: PrintEnv, leaf?: LeafHook): stri
507
536
  return out;
508
537
  }
509
538
  case 'while': {
539
+ refuseMisboundBreak(s.body, 'loop');
510
540
  const out = [`${indent}while (${pe(s.cond, 99)}) {`];
511
541
  for (const t of s.body) {
512
542
  out.push(...printStmt(t, indent + ' ', vt, leaf));
@@ -515,6 +545,7 @@ function printStmt(s: Stmt, indent: string, vt: PrintEnv, leaf?: LeafHook): stri
515
545
  return out;
516
546
  }
517
547
  case 'dowhile': {
548
+ refuseMisboundBreak(s.body, 'loop');
518
549
  const out = [`${indent}do {`];
519
550
  for (const t of s.body) {
520
551
  out.push(...printStmt(t, indent + ' ', vt, leaf));
@@ -527,6 +558,7 @@ function printStmt(s: Stmt, indent: string, vt: PrintEnv, leaf?: LeafHook): stri
527
558
  // `recognizeForLoops` and l3/reindex.ts): init/inc are each a SINGLE-LINE `assign` statement. `clause` renders one
528
559
  // and strips its trailing `;` so it sits inside the header (`i = 0; c; i = i + 1`). A multi-line
529
560
  // statement (an `if`/nested loop) would render mangled — but the recognizer never builds one here.
561
+ refuseMisboundBreak(s.body, 'loop');
530
562
  const clause = (st: Stmt) => printStmt(st, '', vt, leaf).join(' ').replace(/;\s*$/, '').trim();
531
563
  const out = [`${indent}for (${clause(s.init)}; ${pe(s.cond, 99)}; ${clause(s.inc)}) {`];
532
564
  for (const t of s.body) {
@@ -550,11 +582,12 @@ function printStmt(s: Stmt, indent: string, vt: PrintEnv, leaf?: LeafHook): stri
550
582
  * is one call. */
551
583
  function printSwitchStmt(s: Extract<Stmt, { k: 'switch' }>, indent: string, vt: PrintEnv, leaf?: LeafHook): string[] {
552
584
  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
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
555
587
  // 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.
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.
558
591
  //
559
592
  // `continue;` is deliberately NOT refused: C binds it to the smallest enclosing ITERATION
560
593
  // statement, which a `switch` is not, so it already means what L3 means. What a pass that re-spells
@@ -563,9 +596,7 @@ function printSwitchStmt(s: Extract<Stmt, { k: 'switch' }>, indent: string, vt:
563
596
  // `respellCountdown` in l3/reindex.ts, and l3/unreduce.ts) each scan switch arms for the node
564
597
  // before firing, and a fourth must too.
565
598
  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
- }
599
+ refuseMisboundBreak(body, 'switch');
569
600
  }
570
601
  const out = [`${indent}switch (${pe(s.scrutinee, 99)}) {`];
571
602
  const ci = indent + ' '; // case-label indent
@@ -624,13 +655,26 @@ function printSwitchStmt(s: Extract<Stmt, { k: 'switch' }>, indent: string, vt:
624
655
  return out;
625
656
  }
626
657
 
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
- );
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
+ }
634
678
  }
635
679
 
636
680
  // Does a statement list end in a control-flow terminator (so a trailing `break;` would be dead)?
@@ -645,6 +689,22 @@ function endsTerminated(body: Stmt[]): boolean {
645
689
  );
646
690
  }
647
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
+
648
708
  // C-FAMILY WRITE LEGALIZATION (the assign-side sibling of the deref legalization in printExpr):
649
709
  // the C family's answer to `writesNonPointerIntoPointer` is the reinterpret cast to the DECLARED
650
710
  // type — exactly what the machine's register move does — where Pascal, having no such cast,
@@ -654,9 +714,18 @@ function legalizePointerWrites(fn: SFn): SFn {
654
714
  const vt = declaredTypes(fn);
655
715
  // …and the mirror: a rendered pointer into an INTEGER slot takes the slot's type. A cast keeps
656
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;
657
724
  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 }
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 }
660
729
  : e;
661
730
  const fix = (s: Stmt): Stmt => {
662
731
  switch (s.k) {
@@ -692,7 +761,7 @@ function legalizePointerWrites(fn: SFn): SFn {
692
761
  /** The body of a C-family function: local declarations + statements, one string per line. The
693
762
  * SIGNATURE (return type + name + params, plus any C++ scope/`this`/mangling) is the caller's —
694
763
  * that is the language-divergent part each backend owns. */
695
- function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
764
+ function cFamilyBody(fn0: SFn, leaf: LeafHook | undefined, designators: boolean): string[] {
696
765
  const fn = legalizePointerWrites(fn0);
697
766
  // The legalization env: every printed var's declared type and pointee volatility, from the SAME
698
767
  // params/locals the emitted declarations come from — so the printer judges exactly the C the
@@ -710,7 +779,10 @@ function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
710
779
  // no variation or recognizer produces one. The NAME is placed by `cDeclare`, the one
711
780
  // placement every declaration position in this backend uses: a local can carry an array type
712
781
  // — 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)};`);
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
+ );
714
786
  }
715
787
  for (const s of fn.body) {
716
788
  lines.push(...printStmt(s, ' ', vt, leaf));
@@ -718,11 +790,26 @@ function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
718
790
  return lines;
719
791
  }
720
792
 
721
- /** One initial element: hex from 10 up, a negative one with its sign, so a byte table reads as the
793
+ /** One initial number: hex from 10 up, a negative one with its sign, so a byte table reads as the
722
794
  * bytes and a signed one as its values. */
723
- const initElement = (v: number): string =>
795
+ const initNumber = (v: number): string =>
724
796
  Math.abs(v) < 10 ? `${v}` : `${v < 0 ? '-' : ''}0x${Math.abs(v).toString(16)}`;
725
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
+
726
813
  /** A byte array's initial elements as a string literal: printable characters as themselves, every
727
814
  * other byte in three-digit octal (which no following digit can extend), and a `?` after a `?`
728
815
  * escaped so no trigraph forms. The trailing zeros are left to the declared size, which C fills
@@ -750,26 +837,45 @@ function stringLiteral(init: readonly number[]): string {
750
837
  function staticDefinition(st: SStatic): string[] {
751
838
  const aligned = st.align === undefined ? '' : ` __attribute__((aligned(${st.align})))`;
752
839
  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)};`];
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)};`];
758
862
  }
759
- if (st.type.kind !== 'array') {
760
- return [`${head} = ${initElement(st.init[0])};`];
863
+ if (type.kind !== 'array') {
864
+ return [`${head} = ${initElement(whole[0], type)};`];
761
865
  }
762
- if (st.init.length <= 8) {
763
- return [`${head} = { ${st.init.map(initElement).join(', ')} };`];
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(', ')} };`];
764
875
  }
765
876
  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
- );
877
+ for (let i = 0; i < items.length; i += 8) {
878
+ rows.push(` ${items.slice(i, i + 8).join(', ')},`);
773
879
  }
774
880
  return [`${head} = {`, ...rows, ' };'];
775
881
  }
@@ -783,16 +889,25 @@ function structDecls(fn: SFn): string[] {
783
889
  return (fn.structs ?? []).map((s) => renderStructDecl(s.name, s.fields));
784
890
  }
785
891
 
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 {
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 {
788
897
  // The declaration list is put into the target's own frame order, and a pointer cell's integer
789
898
  // uses converted (l3/ptrcell.ts), HERE — in the shared C-family assembler, reached only from
790
899
  // `cBackend.emit` and `cppBackend(...).emit`, so both C-family backends print them and no `.emit(`
791
900
  // call site does (l3/slotorder.ts says why `emit` owns it).
792
- const fn = orderSlotLocals(legalizePointerCells(fn0));
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)));
793
904
  const decls = structDecls(fn);
794
905
  const preamble = decls.length ? decls.join('\n') + '\n' : '';
795
- 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
+ );
796
911
  }
797
912
 
798
913
  // One C-family comment spelling, shared by the C and C++ backends. A `*` followed by `/`
@@ -138,6 +138,7 @@ export function cppBackend(spec: CppFnSpec, floatSlots: FloatSlots | undefined):
138
138
  return {
139
139
  id: 'cpp',
140
140
  spellsSwitchFallthrough: true,
141
+ spellsSwitchBreak: true,
141
142
  emit(fn: SFn): string {
142
143
  // Map each lifted param var → its C++ meaning: `this` (bare member access) or a named param
143
144
  // (a pointer-to-class param uses `->`). A pointer-to-known-class param is a member receiver.
@@ -223,7 +224,7 @@ export function cppBackend(spec: CppFnSpec, floatSlots: FloatSlots | undefined):
223
224
  const paramList = spec.params.map((p) => declareCpp(p.type, p.name)).join(', ');
224
225
  const decls = classDecls(spec, paramList);
225
226
  const signature = `${spellType(spec.retType)} ${spec.cls ? spec.cls + '::' : ''}${spec.method}(${paramList})`;
226
- return (decls ? decls + '\n' : '') + emitCFamily(signature, fn, leaf);
227
+ return (decls ? decls + '\n' : '') + emitCFamily(signature, fn, leaf, false);
227
228
  },
228
229
  comment: cComment, // C++ shares C's block-comment spelling
229
230
  };
@@ -140,10 +140,10 @@ function makePrinter(vt: VarTypes) {
140
140
  // Casts have no faithful IDO-Pascal spelling yet — fail LOUD rather than emit silently-wrong
141
141
  // source. Tree-level producers reaching here: the width-narrowing idiom casts (agbcc-gated,
142
142
  // 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).
143
+ // `field` case above throws first), the arithmetic table's `(s32)ptr` legalization (any
144
+ // target), and the byte-pointer walk of a pointer offset by a runtime value (any target,
145
+ // and reachable with no `field` in the tree, where a Pascal spelling would walk ELEMENTS
146
+ // where the asm walked bytes).
147
147
  // Scalar deref casts never appear in the tree — the index case above owns that judgment.
148
148
  case 'cast':
149
149
  throw new Error(`pascal backend: cast has no IDO Pascal spelling yet`);
@@ -202,13 +202,20 @@ function makePrinter(vt: VarTypes) {
202
202
  return s.value ? [`${indent}${fnName} := ${pe(s.value)};`] : [];
203
203
  case 'if': {
204
204
  const cond = pe(s.cond);
205
- const block = (stmts: Stmt[], ind: string) =>
206
- stmts.length === 1
205
+ // A branch goes bare only where it is ONE Pascal statement that cannot capture what follows.
206
+ // A `for` prints as two (its init, then a `while`), so it always takes begin/end. And with an
207
+ // `else` to come, a then-branch that is itself an `if` or a loop (whose body may end in one)
208
+ // would take that `else` as its own innermost `if`'s — Pascal's dangling else, which C's
209
+ // braces had ruled out: `if (a) { if (b) X; } else Y;` printed bare runs Y when b fails.
210
+ const bare = (stmts: Stmt[], open: boolean) =>
211
+ stmts.length === 1 && stmts[0].k !== 'for' && !(open && (stmts[0].k === 'if' || isLoop(stmts[0])));
212
+ const block = (stmts: Stmt[], ind: string, open = false) =>
213
+ bare(stmts, open)
207
214
  ? ps(fnName, stmts[0], ind, tail)
208
215
  : [`${ind}begin`, ...list(stmts, ind + ' ', tail), `${ind}end`];
209
216
  // Pascal: no `;` before `else`; the branch statements already carry their own.
210
217
  const out = [`${indent}if ${cond} then`];
211
- out.push(...block(s.then, indent + ' '));
218
+ out.push(...block(s.then, indent + ' ', s.else.length > 0));
212
219
  if (s.else.length) {
213
220
  out.push(`${indent}else`);
214
221
  out.push(...block(s.else, indent + ' '));
@@ -218,7 +225,7 @@ function makePrinter(vt: VarTypes) {
218
225
  case 'while': {
219
226
  // loop bodies are NEVER tail position — control returns to the test
220
227
  const body =
221
- s.body.length === 1
228
+ s.body.length === 1 && s.body[0].k !== 'for'
222
229
  ? ps(fnName, s.body[0], indent + ' ')
223
230
  : [`${indent}begin`, ...s.body.flatMap((x) => ps(fnName, x, indent + ' ')), `${indent}end`];
224
231
  return [`${indent}while ${pe(s.cond)} do`, ...body];
@@ -253,7 +260,7 @@ function makePrinter(vt: VarTypes) {
253
260
  // so a `fallsThrough` arm has no faithful spelling → loud-fail (mirrors the `field`/`cast`/`%`
254
261
  // throws above). Each arm's body is a single statement or a begin/end block.
255
262
  const arm = (body: Stmt[], ind: string) =>
256
- body.length === 1
263
+ body.length === 1 && body[0].k !== 'for'
257
264
  ? ps(fnName, body[0], ind + ' ')
258
265
  : [`${ind} begin`, ...body.flatMap((x) => ps(fnName, x, ind + ' ')), `${ind} end`];
259
266
  // Pascal has no early `return`: a `return v` inside a case renders `fnName := v` and then FALLS
@@ -279,8 +286,11 @@ function makePrinter(vt: VarTypes) {
279
286
  }
280
287
  out.push(`${indent} ${c.values.join(', ')}:`, ...arm(c.body, indent + ' '));
281
288
  }
282
- if (s.default) {
283
- out.push(`${indent} otherwise`);
289
+ // A C `switch` with no `default` runs on past a value it does not list; IDO Pascal's
290
+ // `case` raises a run-time `caseerror` there instead. So the clause is always printed, an
291
+ // empty one when the source had no default.
292
+ out.push(`${indent} otherwise`);
293
+ if (s.default?.length) {
284
294
  out.push(...arm(s.default, indent + ' '));
285
295
  }
286
296
  out.push(`${indent}end;`);
@@ -298,17 +308,26 @@ export const pascalBackend: LanguageBackend = {
298
308
  // spells as plain if-nesting, which Pascal prints, so the choice is between a decompiled
299
309
  // function and a stub.
300
310
  spellsSwitchFallthrough: false,
311
+ // Nor has Pascal a `break`, so recovery must not mint a switch's mid-arm one either: a closed arm
312
+ // that leaves early spells as nested `if`s that copy the arm's tail into each path.
313
+ spellsSwitchBreak: false,
301
314
  emit(fn0: SFn): string {
302
315
  // The declaration list is put into the target's own frame order HERE, as the C family does it
303
316
  // in its shared assembler — owned by `emit`, never by a `.emit(` call site (l3/slotorder.ts).
304
317
  const fn = orderSlotLocals(fn0);
305
318
  // 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.
319
+ // spelling of one (a typed constant, or a unit-level var) has not been measured — loud. So is
320
+ // the compiler's anonymous constant a local's initializer is copied from (l3/constinit.ts).
307
321
  if (fn.statics?.length) {
308
322
  throw new Error(
309
323
  `pascal backend: the function-scope static '${fn.statics[0].name}' has no IDO Pascal spelling yet`,
310
324
  );
311
325
  }
326
+ if (fn.images?.length) {
327
+ throw new Error(
328
+ `pascal backend: the compiler's anonymous constant '${fn.images[0].name}' a local is initialized from has no IDO Pascal spelling yet`,
329
+ );
330
+ }
312
331
  // Same env discipline as the C family (cfamily.ts cFamilyBody): the printer judges derefs
313
332
  // against the exact declarations it emits.
314
333
  const ps = makePrinter(declaredTypes(fn));