@asmlift/core 0.6.0 → 0.8.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 (88) hide show
  1. package/README.md +48 -24
  2. package/package.json +1 -1
  3. package/src/backend/cfamily.ts +39 -11
  4. package/src/backend/pascal.ts +2 -2
  5. package/src/codegen-flags.ts +640 -0
  6. package/src/contracts.ts +60 -11
  7. package/src/frontend/disasm.ts +141 -11
  8. package/src/frontend/high-half.ts +149 -0
  9. package/src/frontend/mips.ts +458 -209
  10. package/src/frontend/ppc.ts +332 -67
  11. package/src/frontend/reloc-symbol.ts +109 -0
  12. package/src/frontend/splat.ts +56 -18
  13. package/src/frontend/ssa.ts +127 -30
  14. package/src/frontend/stackargs.ts +420 -0
  15. package/src/frontend/thumb.ts +209 -232
  16. package/src/ir/alias.ts +24 -0
  17. package/src/ir/core.ts +70 -3
  18. package/src/ir/opcodes.ts +52 -7
  19. package/src/ir/parse.ts +7 -1
  20. package/src/ir/simplify.ts +1 -1
  21. package/src/l3/address.ts +2 -2
  22. package/src/l3/advance.ts +373 -0
  23. package/src/l3/argbase.ts +6 -6
  24. package/src/l3/argcopy.ts +269 -0
  25. package/src/l3/ast.ts +110 -22
  26. package/src/l3/basecse.ts +50 -30
  27. package/src/l3/coalesce.ts +118 -61
  28. package/src/l3/gates.ts +75 -1
  29. package/src/l3/hoist.ts +1 -1
  30. package/src/l3/homesplit.ts +13 -13
  31. package/src/l3/initfirst.ts +3 -3
  32. package/src/l3/inlinebase.ts +16 -16
  33. package/src/l3/mentions.ts +68 -5
  34. package/src/l3/mulfirst.ts +3 -3
  35. package/src/l3/nearbase.ts +4 -4
  36. package/src/l3/offmember.ts +5 -5
  37. package/src/l3/parkfirst.ts +6 -6
  38. package/src/l3/pollguard.ts +3 -3
  39. package/src/l3/ptrfield.ts +4 -4
  40. package/src/l3/regspell.ts +8 -8
  41. package/src/l3/reindex.ts +22 -17
  42. package/src/l3/scopebase.ts +32 -29
  43. package/src/l3/sinkinit.ts +7 -7
  44. package/src/l3/slotorder.ts +3 -3
  45. package/src/l3/storage.ts +1 -1
  46. package/src/l3/tailmerge.ts +2 -2
  47. package/src/l3/tailret.ts +70 -0
  48. package/src/l3/typing.ts +3 -3
  49. package/src/l3/unmerge.ts +483 -59
  50. package/src/l3/unreduce.ts +15 -14
  51. package/src/l3/volatileptr.ts +11 -11
  52. package/src/l3/volatileval.ts +11 -11
  53. package/src/l3/volstore.ts +16 -16
  54. package/src/l3/zerosub.ts +6 -6
  55. package/src/mangle.ts +49 -0
  56. package/src/pattern/engine.ts +132 -17
  57. package/src/pipeline.ts +39 -16
  58. package/src/proto.ts +2 -2
  59. package/src/raise/const.ts +203 -3
  60. package/src/raise/divpow2.ts +2 -2
  61. package/src/raise/extscale.ts +345 -0
  62. package/src/raise/globalshape.ts +32 -12
  63. package/src/raise/gvn.ts +2 -2
  64. package/src/raise/magicdiv.ts +2 -2
  65. package/src/raise/memberarrays.ts +4 -4
  66. package/src/raise/narrowlocal.ts +18 -2
  67. package/src/raise/paramwidth.ts +133 -3
  68. package/src/raise/pre-recovery.ts +100 -25
  69. package/src/raise/retsink.ts +389 -19
  70. package/src/raise/shortcircuit.ts +595 -34
  71. package/src/raise/structs.ts +4 -4
  72. package/src/raise/tailsink.ts +141 -0
  73. package/src/rank-declare.ts +21 -13
  74. package/src/{rank-axes.ts → rank-variations.ts} +319 -189
  75. package/src/rank.ts +1176 -805
  76. package/src/structure/analysis.ts +87 -90
  77. package/src/structure/bitfields.ts +130 -30
  78. package/src/structure/globalaccess.ts +30 -4
  79. package/src/structure/namecoalesce.ts +32 -13
  80. package/src/structure/retspell.ts +95 -0
  81. package/src/structure/structure.ts +1425 -201
  82. package/src/structure/switch-recover.ts +101 -8
  83. package/src/symbols.ts +127 -6
  84. package/src/target.ts +374 -44
  85. package/src/trace.ts +28 -19
  86. package/src/variation-definitions.ts +1590 -0
  87. package/src/variation-gates.ts +92 -0
  88. package/src/variation-tokens.ts +356 -0
package/README.md CHANGED
@@ -15,7 +15,8 @@ The operative invariant everywhere: **loud decline > silent miscompile**. Where
15
15
  cannot be byte-faithful it throws a typed error (strict mode) or emits an `ASMLIFT_ERROR`-marked
16
16
  stub (`onGap: "annotate"`) — never plausible wrong code.
17
17
 
18
- > Not yet published to npm. Inside this repo it resolves via the pnpm workspace.
18
+ > Published as [`@asmlift/core`](https://www.npmjs.com/package/@asmlift/core); inside this repo it
19
+ > resolves through the pnpm workspace.
19
20
 
20
21
  ## Usage
21
22
 
@@ -31,7 +32,10 @@ console.log(source); // s32 my_func(s32 a0) { ... }
31
32
  Input is **text**, following what each target's toolchain produces:
32
33
 
33
34
  - The ARM target reads GBA `.s`, produced by agbcc and pret-style project splits.
34
- - The MIPS/PPC targets read `objdump -d --no-show-raw-insn` output and Splat-disassembled `.s`.
35
+ - The MIPS targets read `objdump -d --no-show-raw-insn` output and Splat-disassembled `.s`.
36
+ - The PPC target reads `powerpc-eabi-objdump -d -r -M gekko --no-show-raw-insn` output. `-r` carries
37
+ the relocation lines a `bl`'s callee name lives in, and `-M gekko` names CodeWarrior's machine:
38
+ the generic dialect decodes the GameCube's paired-single opcodes as POWER VSX.
35
39
 
36
40
  ### `decompile(name, asm, target, opts?)`
37
41
 
@@ -41,9 +45,20 @@ Input is **text**, following what each target's toolchain produces:
41
45
  | `patterns` | Idiom rewrite patterns. Omitted = `DEFAULT_IDIOM_PATTERNS` (self-selects per target: most are compiler-gated, the boolean-negation folds are universal); `[]` = none |
42
46
  | `prototypes` | Callee arities + void-ness, as a real project takes them from headers — drives call-argument recovery |
43
47
  | `asmData` | Optional `objdump -s -r -t` side-table; required to recover MIPS/PPC jump-table switches |
48
+ | `symbols` | The project's address→symbol map: names, declaration shapes and signatures for the addresses the assembly only numbers. What each channel buys and how to build the ELF behind it: [`@asmlift/cli`](../cli/README.md#the-symbol-map-elf) |
44
49
  | `onGap` | `"strict"` (default): throw on any gap. `"annotate"`: emit best-effort source with `ASMLIFT_ERROR` markers; every gap is also returned in the structured `diagnostics` array (empty ⇔ gap-free) |
45
50
 
46
- Targets: `ARMV4T_AGBCC`, `MIPS_IDO`, `MIPS_GCC`, `PPC_MWCC` (`@asmlift/core/target`).
51
+ `DecompileResult.assumedSymbols` carries the declarations the returned source is correct only
52
+ BESIDE — array shapes derived from the assembly rather than read from `symbols`. A caller that
53
+ publishes the source alone publishes a spelling whose meaning it has not stated, so render them
54
+ with it.
55
+
56
+ Targets: `ARMV4T_AGBCC`, `MIPS_IDO`, `MIPS_GCC`, `PPC_MWCC` (`@asmlift/core/target`). A toolchain
57
+ id plus the flags its build compiles the function with resolves to one through
58
+ `targetFor(toolchain, cflags)`, which returns that description alongside the `CodegenProfile` the
59
+ flags describe — optimisation level, debug info, and the words the flag table does not name.
60
+ `TOOLCHAIN_TARGETS` is the registry it reads, and `canonicalFlagsOf` gives a toolchain's default
61
+ set where it has one.
47
62
 
48
63
  ### Other entry points
49
64
 
@@ -58,8 +73,8 @@ Everything under `src/` is importable as `@asmlift/core/<path>` (e.g.
58
73
 
59
74
  ## Architecture
60
75
 
61
- Three ISA frontends (ARMv4T/Thumb, MIPS, PowerPC), five compilers (agbcc, IDO, KMC GCC, GCC
62
- 2.7.2, CodeWarrior), three language backends over one neutral AST — all scored across the package seam
76
+ Three ISA frontends (ARMv4T/Thumb, MIPS, PowerPC), four compiler families (`agbcc`, `ido`, `gcc`,
77
+ `mwcc` — `codegen-flags.ts`'s `FlagFamily`) over seven target keys, three language backends over one neutral AST — all scored across the package seam
63
78
  by [`@asmlift/cli`](../cli/README.md) with the community `objdiff` engine (in-process, pinned
64
79
  `objdiff-wasm`; asmlift never hand-rolls a diff).
65
80
 
@@ -79,20 +94,24 @@ injected via hooks, never copied. `verify()` runs after every IR-mutating pass;
79
94
 
80
95
  ### Modules
81
96
 
82
- | Module | What it is |
83
- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
84
- | `ir/{types,core,opcodes,print,parse,verify}.ts` | MLIR-lite substrate: CFG of blocks + typed **block-arguments**, the typed opcode registry (`Opcode`, the one `effects` table DCE and hoist guards derive from), printer + parser (round-trip for L1/scalar types — see the domain note in parse.ts), verifier (arity/attrs/terminators/SSA dominance, located errors); `core.ts` also owns the CFG facts every level reads — `successorsOf`, `predecessors`, `dominators` |
85
- | `frontend/{thumb,mips,ppc}.ts` | ISA frontends: decode → CFG → L1 with **Braun-2013 block-arg SSA** (`ssa.ts`), incl. loops, calls (signature-driven arity), memory, jump tables. Shared scaffolding: `disasm.ts` (objdump parsing), `splat.ts` (Splat-dialect MIPS → objdump-shaped instrs), `format.ts` (input-format classification), `emit.ts` (per-block emitter kit + `switch_br`), `opaque.ts` (the unmodelled-op → loud-`opaque` contract), `errors.ts` (`FrontendUnsupportedError`; PPC's subclass), `registry.ts`, `asmdata.ts` (Regime-B jump-table side-table) |
86
- | `pattern/engine.ts` | Idiom layer: **rewrite patterns as data** + greedy driver + DCE; `patternApplies` gates on Target capabilities |
87
- | `raise/*.ts` | The pre-recovery recognizers, in ONE ordered list (`pre-recovery.ts`): const materialize → magic division (`magicdiv.ts`, Hacker's Delight inverse) → soft division → array legalize → struct-array → member-array (`memberarrays.ts`, a constant offset feeding a variable-index walk is a struct's ARRAY member) → struct-pointer → short-circuit → narrow-reads (`narrow.ts`) → narrow locals (`narrowlocal.ts`, a carrier read only through its own extension IS that width) → parameter width (`paramwidth.ts`, a parameter extended in the prologue is DECLARED at that width); plus `recover.ts` (L1→L2 type recovery), `retsink.ts` (return-sinking), `latch.ts` (empty-latch folding), `errors.ts` (`RaiseUnsupportedError`) |
88
- | `structure/*.ts` | L2→L3 in six modules: `loops.ts` (natural-loop discovery, over `ir/core.ts`'s dominators), `analysis.ts` (use registry, liveness, C4 materialization), `switch-recover.ts` (Regime-A comparison-tree recovery), `hazards.ts` (the checks a loop emitter runs before committing to a loop form), `namecoalesce.ts` (copy coalescing over the interference graph — the `/merge-names` axis), `structure.ts` (SSA-destruction coalescing with interference checks + emission: if/while/do-while/for/switch, break/early-return) |
89
- | `l3/*.ts` | `ast.ts`: language-**neutral** structured AST, the one traversal vocabulary (`exprChildren` etc.), and the `LanguageBackend` seam. Post-structure passes `dce.ts` + `basecse.ts`, the shared hoist mechanism `hoist.ts`, the differ-ranked re-spelling levers `regspell.ts` + `reindex.ts` + `argbase.ts` + `scopebase.ts`, and `typing.ts` (the rendered-expression C type the backends and contracts share) |
90
- | `backend/{c,cpp,cfamily,pascal}.ts` | Three backends: C and C++ (CodeWarrior mangling via `mangle.ts`) over the shared `cfamily.ts` substrate, and Pascal (`:=`, `div`, tail-position returns; unspellable constructs throw) |
91
- | `pipeline.ts` | `decompile()` + the shared tower spine + annotate-mode stubs/diagnostics |
92
- | `trace.ts` | `decompileTraced` — the traced tower (per-stage IR dumps + pattern before/after events), browser-pure; @asmlift/cli's `report.ts` enriches it with objdiff scores/candidates, the playground's Pipeline tab renders it directly |
93
- | `rank.ts` | Pure candidate enumeration + `rankBy` (an injected score function ranks). @asmlift/cli's differ ranks through `rankBy`; the playground's wasm scorer consumes the same enumeration with its own async loop |
94
- | `target.ts` | `TargetDescription` (ABI + capabilities + compilerBehaviors as datano `arch ==` in shared code); toolchain paths live in `@asmlift/toolchains` |
95
- | `contracts.ts`, `proto.ts`, `mangle.ts` | Boundary contracts; prototype tables; the CodeWarrior mangler |
97
+ | Module | What it is |
98
+ | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
99
+ | `ir/*.ts` | MLIR-lite substrate (`types`, `core`, `opcodes`, `print`, `parse`, `verify`, plus `alias.ts`'s alias predicate, `simplify.ts`, `bits.ts` and `struct-names.ts`): CFG of blocks + typed **block-arguments**, the typed opcode registry (`Opcode`, the one `effects` table DCE and hoist guards derive from), printer + parser (round-trip for L1/scalar types — see the domain note in parse.ts), verifier (arity/attrs/terminators/SSA dominance, located errors); `core.ts` also owns the CFG facts every level reads — `successorsOf`, `predecessors`, `dominators` |
100
+ | `frontend/{thumb,mips,ppc}.ts` | ISA frontends: decode → CFG → L1 with **Braun-2013 block-arg SSA** (`ssa.ts`), incl. loops, calls (signature-driven arity), memory, jump tables. Shared scaffolding: `disasm.ts` (objdump parsing), `splat.ts` (Splat-dialect MIPS → objdump-shaped instrs), `format.ts` (input-format classification), `emit.ts` (per-block emitter kit + `switch_br`), `opaque.ts` (the unmodelled-op → loud-`opaque` contract), `errors.ts` (`FrontendUnsupportedError`; PPC's subclass), `registry.ts`, `asmdata.ts` (Regime-B jump-table side-table), `high-half.ts` (a relocated address's high half is a link-time placeholder, never a value), `reloc-symbol.ts` (the per-KIND policy deciding which linker names C can spell), `stackargs.ts` (the outgoing stack-argument fixpoint, over digested slot events rather than instructions) |
101
+ | `pattern/engine.ts` | Idiom layer: **rewrite patterns as data** + greedy driver + DCE; `patternApplies` gates on Target capabilities |
102
+ | `raise/*.ts` | The pre-recovery recognizers, in ONE ordered list `PRE_RECOVERY_PASSES` in `pre-recovery.ts` is the source of truth for the order, and reads: address-numbering (`gvn.ts`) → const-materialize → magic-division (`magicdiv.ts`, Hacker's Delight inverse) → pow2-division (`divpow2.ts`) → soft-division → scaled-extension (`extscale.ts`) → array-legalize → struct-array → member-array (`memberarrays.ts`, a constant offset feeding a variable-index walk is a struct's ARRAY member) → struct-pointer → short-circuit → branch-short-circuit → narrow-reads (`narrow.ts`) → narrow-local (`narrowlocal.ts`, a carrier read only through its own extension IS that width) → parameter-width (`paramwidth.ts`, a parameter extended in the prologue is DECLARED at that width) → scaled-extension-restore; plus `recover.ts` (L1→L2 type recovery), `retsink.ts` + `tailsink.ts` (return- and tail-sinking), `latch.ts` (empty-latch folding), `globalshape.ts` (which derived shapes the source is only correct beside), `errors.ts` (`RaiseUnsupportedError`) |
103
+ | `structure/*.ts` | L2→L3: `loops.ts` (natural-loop discovery, over `ir/core.ts`'s dominators), `analysis.ts` (use registry, liveness, C4 materialization), `switch-recover.ts` (Regime-A comparison-tree recovery), `hazards.ts` (the checks a loop emitter runs before committing to a loop form), `namecoalesce.ts` (copy coalescing over the interference graph — the `/merge-names` variation), `structure.ts` (SSA-destruction coalescing with interference checks + emission: if/while/do-while/for/switch, break/early-return), `retspell.ts` (which `return;` the source wrote, read off the epilogue's in-edges), `bitfields.ts` + `globalaccess.ts` (the map-driven bitfield and global-access spellings, both refusal machines) |
104
+ | `l3/*.ts` | `ast.ts`: language-**neutral** structured AST, the one traversal vocabulary (`exprChildren` etc.), and the `LanguageBackend` seam. Post-structure passes `dce.ts` + `basecse.ts`, the shared hoist mechanism `hoist.ts`, the differ-ranked respell variations (`regspell.ts`, `reindex.ts`, `argbase.ts`, `scopebase.ts`, `argcopy.ts` five of the 31 the registry holds; a module may mint more than one, as `scopebase.ts` does with `/scopebase` and `/regionbase`), the spelling pass `tailret.ts` (drop a void `return;` `structure/retspell.ts` marked unspelled), and `typing.ts` (the rendered-expression C type the backends and contracts share) |
105
+ | `backend/{c,cpp,cfamily,pascal}.ts` | Three backends: C and C++ (CodeWarrior mangling via `mangle.ts`) over the shared `cfamily.ts` substrate, and Pascal (`:=`, `div`, tail-position returns; unspellable constructs throw) |
106
+ | `pipeline.ts` | `decompile()` + the shared tower spine + annotate-mode stubs/diagnostics |
107
+ | `trace.ts` | `decompileTraced` — the traced tower (per-stage IR dumps + pattern before/after events), browser-pure; @asmlift/cli's `report.ts` enriches it with objdiff scores/candidates, the playground's Pipeline tab renders it directly |
108
+ | `rank.ts` | Pure candidate enumeration + `rankBy` (an injected score function ranks). @asmlift/cli's differ ranks through `rankBy`; the playground's wasm scorer consumes the same enumeration with its own async loop |
109
+ | `rank-variations.ts`, `rank-declare.ts` | The two halves `rank.ts` walks rather than hand-codes: the enumeration TABLES (declaration order is published behaviour it breaks a score tie), and the declarations a candidate needs to compile outside the project's headers, including which names one must REFUSE to claim |
110
+ | `variation-{tokens,definitions,gates}.ts` | The variation REGISTRY as data: every variation a candidate's name can carry, what each one MEANS for a reader, and the admission tables a definition names. Keyed by the registry in both directions, so a variation with no definition and a definition naming no variation are each a type error |
111
+ | `codegen-flags.ts` | Compiler flags parsed into the codegen profile a function was compiled at — each option that can change what the compiler emits, as a named slot. One flag set reaches both the compile and the decompile |
112
+ | `symbols.ts`, `declare.ts`, `macros.ts` | The address→symbol map seam (core consumes the VALUE; the providers that read files live in `@asmlift/cli`), the declaration block a self-declaring candidate carries, and the address-cast macro spelling — a `#define` emits a NUMERIC pool word where an `extern` emits a relocated one, so the two are not interchangeable in the bytes |
113
+ | `target.ts` | `TargetDescription` (ABI + capabilities + compilerBehaviors as data — no `arch ==` in shared code); toolchain paths live in `@asmlift/toolchains` |
114
+ | `contracts.ts`, `proto.ts`, `mangle.ts` | Boundary contracts; prototype tables; the CodeWarrior mangler |
96
115
 
97
116
  Scoring and ranking live across the package seam in [`@asmlift/cli`](../cli/README.md):
98
117
  `score.ts` + `objdiff.ts` (toolchain compiles → in-process pinned `objdiff-wasm`, fail-closed)
@@ -103,12 +122,16 @@ and `rank.ts` (ranked type candidates re-ranked by the differ).
103
122
  Recovered today: straight-line, if/else diamonds, natural loops (`while` / `do-while` / `for`,
104
123
  properly nested, in-body `break`/early-`return`), comparison-tree and jump-table switches,
105
124
  direct calls, constant-offset and variable-index memory (`*p`, `p[n]`, `a[i]`, struct fields),
106
- magic-number and soft division, short-circuit booleans, width casts. Still DECLINED (loud, never
125
+ magic-number and soft division, short-circuit booleans, width casts, PowerPC small-data access
126
+ through its `R_PPC_EMB_SDA21` relocation, a call's outgoing stack arguments on agbcc, where the
127
+ declaration and the code agree (`stagesOutgoingArgsInFrame`; MIPS still declines the same shape),
128
+ and a MIPS likely branch's nullified delay slot. Still DECLINED (loud, never
107
129
  wrong code): **local stack frames** (address-taken locals / sp-as-data; MIPS models word `sp`
108
130
  slots and PPC elides callee-saved save slots, so a spill/reload pair is modelled on those two —
109
131
  anything the narrow models cannot honour declines),
110
132
  **cross-block condition flags** on PPC (a `cmpw` whose branch lands in another block — the
111
- capability gap behind the mwcc switch stubs), computed tail calls, PIC/`gp`/SDA global access,
133
+ capability gap behind the mwcc switch stubs), computed tail calls, PIC and `gp`-relative access outside PowerPC's
134
+ small-data relocation — MIPS refuses `R_MIPS_GOT16`/`R_MIPS_CALL16`/`R_MIPS_GPREL16` alike —
112
135
  switch fall-through, multi-latch/irreducible loops, floats, and 64-bit memory ops. Prototypes
113
136
  (callee arities, void-ness) come from a caller-supplied map, as a real project takes them from
114
137
  headers.
@@ -130,9 +153,10 @@ Landmarks (not exhaustive — suites are named for what they pin):
130
153
  that keeps every already-matching function matching. How to add a fixture — and when a
131
154
  matching test is the right tool at all, vs a benchmark row — is under
132
155
  [`@asmlift/cli` › Tests](../cli/CONTRIBUTION.md#tests).
133
- - `contract-invariant` / `contracts` — the loud-fail contract, mutation-proven.
134
- - `structure-guard` / `structure-soundness` / `audit-regression` — the adversarial-audit repro
135
- locks.
156
+ - `contract-invariant`, and `contracts` (in `../cli/test/matching/`) — the loud-fail contract,
157
+ mutation-proven.
158
+ - `structure-guard`, and `structure-soundness` / `audit-regression` (both in
159
+ `../cli/test/matching/`) — the adversarial-audit repro locks.
136
160
  - `one-version` — a WORKSPACE-scope invariant, like `offline-list`: every dependency that two or
137
161
  more workspace packages declare must be declared AND resolved at one version. It exists because
138
162
  `packages/cli` scored with `objdiff-wasm` 3.7.3 while `apps/web` scored with 3.7.0, so the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asmlift/core",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Match decompile an assembly function to C or Pascal",
@@ -80,10 +80,18 @@ export function cType(t: IrType): string {
80
80
 
81
81
  /** Declare a name of a given type, C declarator rules: an array puts its length AFTER the name
82
82
  * (`u8 _pad[4]`), a pointer binds its `*` to the declarator (`void *p`), everything else is
83
- * the prefix `cType name`. */
83
+ * the prefix `cType name`. A NESTED array spells every extent after the name in declaration
84
+ * order (`u8 unk8[6][8]`) — one declarator, not an element type that is itself an array, which
85
+ * C has no syntax for and `cType` marks ill-formed as a prefix. */
84
86
  function cDeclare(t: IrType, name: string): string {
85
87
  if (t.kind === 'array') {
86
- return `${cType(t.elem)} ${name}[${t.count}]`;
88
+ const extents: number[] = [];
89
+ let e: IrType = t;
90
+ while (e.kind === 'array') {
91
+ extents.push(e.count);
92
+ e = e.elem;
93
+ }
94
+ return `${cType(e)} ${name}${extents.map((n) => `[${n}]`).join('')}`;
87
95
  }
88
96
  if (t.kind === 'ptr') {
89
97
  return `${cType(t.to)} *${name}`;
@@ -136,7 +144,19 @@ export type LeafHook = (e: Expr, rec: (e: Expr, p: number) => string) => string
136
144
  * -fprologue-bugfix`), and referring to a volatile object through a non-volatile lvalue is
137
145
  * undefined behaviour (C99 6.7.3p5). */
138
146
  function legalizedIndexBase(ix: Extract<Expr, { k: 'index' }>, vt: PrintEnv): Expr {
139
- return derefStrideOk(exprCType(ix.base, vt.type), ix.width, ix.signed)
147
+ // `baseElem` states the element type the base's own DECLARATION gives it, where the type walk
148
+ // cannot reconstruct one (a map-declared array MEMBER — see l3/ast.ts). It is a FALLBACK, not an
149
+ // override, and the order is the whole guard: the walk reads the tree in front of it, so
150
+ // wherever it answers at all it is the better answer and the stated one is not consulted.
151
+ // Inverted (`declared ?? walked`), a stale statement would stand — `derefStrideOk` cannot catch
152
+ // one, because it tests the STATED type against the access width and never against the base.
153
+ // No pass carries a statement onto a foreign base today (each refuses a `field` base by its own
154
+ // predicate: basecse `isHoistableBase`, scopebase/argbase `eligible`, nearbase's untouched
155
+ // `field` subtree, reindex's `base.k === 'var'`), but `mapExprChildren` spreads the field across
156
+ // an arbitrary base substitution, so that is five accidents rather than a check. This ordering
157
+ // is the check. Measured: zero test moves either way.
158
+ const declared = ix.baseElem !== undefined ? T.ptr(ix.baseElem) : undefined;
159
+ return derefStrideOk(exprCType(ix.base, vt.type) ?? declared, ix.width, ix.signed)
140
160
  ? ix.base
141
161
  : {
142
162
  k: 'cast',
@@ -226,11 +246,19 @@ function printExpr(e: Expr, parentPrec: number, vt: PrintEnv, leaf?: LeafHook):
226
246
  // Leading constant subscripts (a multidimensional array global's bare spelling) keep the
227
247
  // postfix form whatever `idx` is: `g[0][0]` is the element, `*g[0]` would be its ROW.
228
248
  //
229
- // `lead` implies the base already strides the access width its only producers register a
230
- // matching element type for the global (structure/globalaccess.ts `bareArrayLead` and
231
- // `declaredSubscripts`, both through structure.ts's `noteGlobal`). Nothing
232
- // else enforced that, and the failure would be quiet-ish: legalization would wrap the base,
233
- // spelling `((u16 *)g)[0][i]`, which subscripts a `u16` twice. Check it rather than assume.
249
+ // `lead` implies the base already strides the access width, and its producers reach that by
250
+ // TWO different mechanisms. The two GLOBAL ones register a matching element type for the
251
+ // global (structure/globalaccess.ts `bareArrayLead` and `declaredSubscripts`, both through
252
+ // structure.ts's `noteGlobal`, so the type walk finds it); the MEMBER one
253
+ // (structure.ts `pointeeElement`, `gPtr->grid[0][i]`) has no global to register and states
254
+ // the element type on the node instead (`baseElem`). Nothing else enforced either, and the
255
+ // failure would be quiet-ish: legalization would wrap the base, spelling `((u16 *)g)[0][i]`,
256
+ // which subscripts a `u16` twice. Check it rather than assume.
257
+ //
258
+ // This throw covers rank >= 2 ONLY, because only a rank >= 2 access carries `lead`. A rank-1
259
+ // member (the large majority of the corpus's dims-carrying members) has no guard here: a
260
+ // missing or stale `baseElem` there degrades silently to the cast form the rule replaces,
261
+ // which is a lost spelling rather than a wrong address.
234
262
  if (e.lead && e.lead.length > 0) {
235
263
  if (base !== e.base) {
236
264
  throw new Error(
@@ -396,8 +424,8 @@ function printSwitchStmt(s: Extract<Stmt, { k: 'switch' }>, indent: string, vt:
396
424
  // arm first, loudly), so it is a contract on the next one.
397
425
  //
398
426
  // `continue;` is deliberately NOT refused: C binds it to the smallest enclosing ITERATION
399
- // statement, which a `switch` is not, so it already means what L3 means. What a loop-respelling
400
- // pass must preserve is exactly that — the three that can re-spell a loop into one whose
427
+ // statement, which a `switch` is not, so it already means what L3 means. What a pass that re-spells
428
+ // a loop must preserve is exactly that — the three that can re-spell a loop into one whose
401
429
  // `continue` would run a different increment (`recognizeForLoops` in structure.ts,
402
430
  // `respellCountdown` in l3/reindex.ts, and l3/unreduce.ts) each scan switch arms for the node
403
431
  // before firing, and a fourth must too.
@@ -539,7 +567,7 @@ function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
539
567
  // differently: on a scalar the qualifier binds to the object (`volatile u16 sp0`), on a
540
568
  // pointer declarator to the pointee — the INNERMOST one for a multi-level pointer
541
569
  // (`volatile u16 ** p`). An object-volatile POINTER (`u16 *volatile p`) has no inhabitant —
542
- // no lever or recognizer produces one.
570
+ // no variation or recognizer produces one.
543
571
  lines.push(` ${l.volatile || l.pointeeVolatile ? 'volatile ' : ''}${cType(l.type)} ${l.name};`);
544
572
  }
545
573
  for (const s of fn.body) {
@@ -9,7 +9,7 @@
9
9
  // (`rshift` is arithmetic on a signed Integer → `sra`.) This is the concrete difference from
10
10
  // Turbo/Delphi/FreePascal.
11
11
  import { IrType, typeToString } from '../ir/types';
12
- import { BinOp, Expr, LanguageBackend, SFn, Stmt } from '../l3/ast';
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
15
 
@@ -240,7 +240,7 @@ function makePrinter(vt: VarTypes) {
240
240
  (st) =>
241
241
  st.k === 'return' ||
242
242
  (st.k === 'if' && (hasReturn(st.then) || hasReturn(st.else))) ||
243
- ((st.k === 'while' || st.k === 'dowhile' || st.k === 'for') && hasReturn(st.body)) ||
243
+ (isLoop(st) && hasReturn(st.body)) ||
244
244
  (st.k === 'switch' && (st.cases.some((c) => hasReturn(c.body)) || hasReturn(st.default ?? []))),
245
245
  );
246
246
  if (s.cases.some((c) => hasReturn(c.body)) || hasReturn(s.default ?? [])) {