@asmlift/core 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +36 -31
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +397 -0
  4. package/src/backend/c.ts +6 -2
  5. package/src/backend/cfamily.ts +370 -56
  6. package/src/backend/cpp.ts +116 -10
  7. package/src/backend/pascal.ts +69 -11
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +777 -84
  15. package/src/declare.ts +132 -13
  16. package/src/frontend/asmdata.ts +26 -6
  17. package/src/frontend/call-plan.ts +1590 -0
  18. package/src/frontend/dead-reads.ts +628 -0
  19. package/src/frontend/device-pins.ts +296 -0
  20. package/src/frontend/emit.ts +4 -3
  21. package/src/frontend/errors.ts +5 -0
  22. package/src/frontend/flags-edge.ts +101 -0
  23. package/src/frontend/fpu.ts +374 -0
  24. package/src/frontend/frame-objects.ts +3171 -0
  25. package/src/frontend/frontend.ts +64 -3
  26. package/src/frontend/high-half.ts +55 -5
  27. package/src/frontend/jump-table.ts +530 -0
  28. package/src/frontend/local-object.ts +616 -0
  29. package/src/frontend/mips.ts +2392 -208
  30. package/src/frontend/opaque.ts +97 -18
  31. package/src/frontend/own-struct-return.ts +500 -0
  32. package/src/frontend/ppc.ts +3137 -267
  33. package/src/frontend/register-struct-call.ts +484 -0
  34. package/src/frontend/reloc-symbol.ts +109 -13
  35. package/src/frontend/splat.ts +108 -5
  36. package/src/frontend/ssa.ts +2098 -89
  37. package/src/frontend/stackargs.ts +172 -42
  38. package/src/frontend/thumb.ts +4563 -2091
  39. package/src/ir/alias.ts +71 -0
  40. package/src/ir/bits.ts +681 -1
  41. package/src/ir/core.ts +199 -6
  42. package/src/ir/discipline.ts +313 -0
  43. package/src/ir/entry.ts +159 -0
  44. package/src/ir/float-bits.ts +47 -0
  45. package/src/{structure → ir}/loops.ts +6 -2
  46. package/src/ir/opcodes.ts +193 -121
  47. package/src/ir/parse.ts +1 -0
  48. package/src/ir/types.ts +202 -2
  49. package/src/ir/verify.ts +219 -3
  50. package/src/l3/address.ts +5 -0
  51. package/src/l3/argcopy.ts +38 -6
  52. package/src/l3/ast.ts +467 -23
  53. package/src/l3/basecse.ts +33 -5
  54. package/src/l3/coalesce.ts +21 -13
  55. package/src/l3/constinit.ts +382 -0
  56. package/src/l3/dce.ts +34 -12
  57. package/src/l3/gates.ts +9 -3
  58. package/src/l3/hoist.ts +8 -7
  59. package/src/l3/initfirst.ts +68 -19
  60. package/src/l3/intcell.ts +98 -0
  61. package/src/l3/mentions.ts +10 -4
  62. package/src/l3/narrowdecl.ts +259 -0
  63. package/src/l3/nearbase.ts +10 -5
  64. package/src/l3/parkfirst.ts +8 -5
  65. package/src/l3/pollguard.ts +11 -4
  66. package/src/l3/ptrcell.ts +354 -0
  67. package/src/l3/ptrfield.ts +123 -8
  68. package/src/l3/regspell.ts +26 -7
  69. package/src/l3/reindex.ts +293 -103
  70. package/src/l3/storage.ts +98 -1
  71. package/src/l3/symbol-refs.ts +182 -23
  72. package/src/l3/tailmerge.ts +9 -20
  73. package/src/l3/typing.ts +237 -32
  74. package/src/l3/unreduce.ts +10 -9
  75. package/src/l3/volatileptr.ts +17 -54
  76. package/src/l3/volstore.ts +96 -7
  77. package/src/mangle.ts +41 -1
  78. package/src/pattern/engine.ts +373 -82
  79. package/src/pipeline.ts +589 -27
  80. package/src/proto-context.ts +1648 -0
  81. package/src/proto.ts +1203 -49
  82. package/src/raise/aggmembers.ts +290 -0
  83. package/src/raise/arrays.ts +8 -3
  84. package/src/raise/declared-volatile.ts +307 -0
  85. package/src/raise/divpow2.ts +2 -2
  86. package/src/raise/errors.ts +17 -0
  87. package/src/raise/floathelpers.ts +464 -0
  88. package/src/raise/globalshape.ts +11 -5
  89. package/src/raise/gvn.ts +29 -8
  90. package/src/raise/latch.ts +162 -6
  91. package/src/raise/memberarrays.ts +10 -4
  92. package/src/raise/narrow.ts +29 -2
  93. package/src/raise/narrowlocal.ts +11 -6
  94. package/src/raise/offsetnames.ts +494 -0
  95. package/src/raise/pairparams.ts +114 -0
  96. package/src/raise/paramwidth.ts +203 -6
  97. package/src/raise/pre-recovery.ts +174 -17
  98. package/src/raise/pre-structure.ts +54 -0
  99. package/src/raise/recover.ts +26 -15
  100. package/src/raise/reroll.ts +694 -0
  101. package/src/raise/retsink.ts +1 -1
  102. package/src/raise/shortcircuit.ts +66 -30
  103. package/src/raise/softdiv.ts +21 -23
  104. package/src/raise/storesign.ts +153 -0
  105. package/src/raise/struct-arrays.ts +34 -14
  106. package/src/raise/structs.ts +674 -98
  107. package/src/raise/truncload.ts +367 -0
  108. package/src/raise/unthread.ts +400 -0
  109. package/src/raise/widehelpers.ts +106 -0
  110. package/src/rank-declare.ts +143 -12
  111. package/src/rank-variations.ts +117 -31
  112. package/src/rank.ts +563 -122
  113. package/src/runtime-helpers.ts +405 -0
  114. package/src/stillborn.ts +289 -0
  115. package/src/structure/analysis.ts +1449 -449
  116. package/src/structure/arith-ops.ts +23 -0
  117. package/src/structure/globalaccess.ts +111 -9
  118. package/src/structure/hazards.ts +641 -114
  119. package/src/structure/local-statics.ts +517 -0
  120. package/src/structure/namecoalesce.ts +2 -2
  121. package/src/structure/pointer-spelling.ts +1106 -0
  122. package/src/structure/redundant-test.ts +99 -0
  123. package/src/structure/structure.ts +3478 -975
  124. package/src/structure/switch-recover.ts +403 -178
  125. package/src/symbols.ts +44 -3
  126. package/src/target.ts +1387 -71
  127. package/src/trace.ts +66 -28
  128. package/src/variation-definitions.ts +243 -13
  129. package/src/variation-gates.ts +2 -0
  130. package/src/variation-tokens.ts +25 -7
package/README.md CHANGED
@@ -75,8 +75,8 @@ Everything under `src/` is importable as `@asmlift/core/<path>` (e.g.
75
75
 
76
76
  Three ISA frontends (ARMv4T/Thumb, MIPS, PowerPC), four compiler families (`agbcc`, `ido`, `gcc`,
77
77
  `mwcc` — `codegen-flags.ts`'s `FlagFamily`) over seven target keys, three language backends over one neutral AST — all scored across the package seam
78
- by [`@asmlift/cli`](../cli/README.md) with the community `objdiff` engine (in-process, pinned
79
- `objdiff-wasm`; asmlift never hand-rolls a diff).
78
+ by [`@asmlift/cli`](../cli/README.md) with the community `objdiff` engine (in-process through
79
+ `@match-kit/scoring`, pinned `objdiff-wasm`; asmlift never hand-rolls a diff).
80
80
 
81
81
  ### The pipeline (`decompile()` in `pipeline.ts`)
82
82
 
@@ -94,27 +94,28 @@ injected via hooks, never copied. `verify()` runs after every IR-mutating pass;
94
94
 
95
95
  ### Modules
96
96
 
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 |
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` and its `effects`/`reads`/`traps` flags), `discipline.ts` (the access discipline: an op's placement — `call`, `helper`, `device`, `declared` — and the questions DCE, hoist and placement guards ask of an op instead of an opcode set), 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), `frame-objects.ts` (the ISA-neutral audit of every address-taken frame object, over the frame partition a frontend measured), `device-pins.ts` (the device pin that audit answers: which device-register accesses must run where the machine ran them, per `DEVICE_PIN_POLICIES` row) |
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), `declared-volatile.ts` (the `declared` stamp on an access of an object the symbol map or the declaration context declares volatile, or through a pointer the context declares to point at a volatile), `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`, `narrowdecl.ts`, six of the modules behind the registry in `variation-tokens.ts`; a module may mint more than one, as `scopebase.ts` does with `/scopebase` and `/regionbase` and `narrowdecl.ts` with `/narrow-decl` and `/narrow-read`), 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). Every driver ranks through `rankBy`: the sync driver (@asmlift/cli's `decompileRanked`, which the benchmark and `bench fan` run) scores inside it, and the two async drivers (the CLI's pooled ranker, the playground's wasm scorer) compile on their own schedule, memoize the outcomes and hand `rankBy` the memo |
109
+ | `stillborn.ts`, `compiler-diagnostics.ts` | The stillborn-fan rule, one copy for every driver: when the default candidate is rejected, each signedness half's default and one probe per variation name in that half are compiled, and only a fan whose every half's default keeps an error through every probe of its half, in every attempt, is left uncompiled — the rest reported as NOT COMPILED, never as dropped. `compiler-diagnostics.ts` is how a rejection is read: `CompilerRejection` (a compile that ran to completion and said no, as distinct from a transient), the error lines of each compiler's output with positions normalised away, the attempts of a rejection that holds several compiles (`joinAttempts`/`attemptsOf`), and `errorsFirst` for whatever bounds a diagnostic downstream |
110
+ | `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 |
111
+ | `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 |
112
+ | `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 |
113
+ | `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 |
114
+ | `target.ts` | `TargetDescription` (ABI + capabilities + compilerBehaviors as data — no `arch ==` in shared code); toolchain paths live in `@asmlift/toolchains` |
115
+ | `contracts.ts`, `proto.ts`, `mangle.ts` | Boundary contracts; prototype tables; the CodeWarrior mangler |
115
116
 
116
117
  Scoring and ranking live across the package seam in [`@asmlift/cli`](../cli/README.md):
117
- `score.ts` + `objdiff.ts` (toolchain compiles → in-process pinned `objdiff-wasm`, fail-closed)
118
+ `score.ts` (toolchain compiles → `@match-kit/scoring`, the in-process pinned objdiff engine, fail-closed)
118
119
  and `rank.ts` (ranked type candidates re-ranked by the differ).
119
120
 
120
121
  ### Honest coverage gaps
@@ -124,17 +125,21 @@ properly nested, in-body `break`/early-`return`), comparison-tree and jump-table
124
125
  direct calls, constant-offset and variable-index memory (`*p`, `p[n]`, `a[i]`, struct fields),
125
126
  magic-number and soft division, short-circuit booleans, width casts, PowerPC small-data access
126
127
  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
129
- wrong code): **local stack frames** (address-taken locals / sp-as-data; MIPS models word `sp`
130
- slots and PPC elides callee-saved save slots, so a spill/reload pair is modelled on those two —
131
- anything the narrow models cannot honour declines),
132
- **cross-block condition flags** on PPC (a `cmpw` whose branch lands in another block — the
133
- capability gap behind the mwcc switch stubs), computed tail calls, PIC and `gp`-relative access outside PowerPC's
128
+ declaration and the code agree (`stagesOutgoingArgsInFrame`; MIPS lifts a declared callee's too),
129
+ a compare read by a branch in a later block that only its own block enters (Thumb and
130
+ PowerPC, `frontend/flags-edge.ts`), and a MIPS likely branch's nullified delay slot. Still DECLINED (loud,
131
+ never wrong code): **local stack frames** beyond what the models prove (Thumb, MIPS and PPC lift an
132
+ address-taken local the frame-object audit can bound — a scalar, or an untyped buffer that fills
133
+ the frame — and decline the rest, MIPS inside the O32 local area and PPC inside mwcc's local area
134
+ that its compiler's measured layout draws; MIPS and PPC also read an incoming stack word as a
135
+ parameter, MIPS models word `sp` slots and PPC elides callee-saved save slots, so a spill/reload
136
+ pair is modelled on those two — anything the narrow models cannot honour declines),
137
+ **condition flags at a join** (a compare two or more edges bring to one branch, or one the caller
138
+ set), computed tail calls, PIC and `gp`-relative access outside PowerPC's
134
139
  small-data relocation — MIPS refuses `R_MIPS_GOT16`/`R_MIPS_CALL16`/`R_MIPS_GPREL16` alike —
135
- switch fall-through, multi-latch/irreducible loops, floats, and 64-bit memory ops. Prototypes
136
- (callee arities, void-ness) come from a caller-supplied map, as a real project takes them from
137
- headers.
140
+ switch fall-through, irreducible loops and loops that exit to more than one live merge, floats,
141
+ and 64-bit memory ops. Prototypes (callee arities, void-ness) come from a caller-supplied map, as a
142
+ real project takes them from headers.
138
143
 
139
144
  ## Tests
140
145
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asmlift/core",
3
- "version": "0.8.1",
3
+ "version": "0.10.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Match decompile an assembly function to C or Pascal",
@@ -0,0 +1,397 @@
1
+ // asmlift — a declared struct or union on a target: its size, and whether a function returning it
2
+ // by value hands it back through a hidden pointer. The members come from a declaration
3
+ // (`proto.ts` AggregateLayout); the layout rules and the return rule are the target's
4
+ // (`compilerBehaviors.aggregateBoundary`, `largestAlignment`, `aggregateReturn`).
5
+ import { type IrType, type StructField, T } from './ir/types';
6
+ import {
7
+ type AggregateLayout,
8
+ type AggregateMember,
9
+ type FnProto,
10
+ type Prototypes,
11
+ STANDARD_SIGNATURES,
12
+ declaredCallArgs,
13
+ declaredWidth,
14
+ declaresAggregateReturn,
15
+ declaresVoidReturn,
16
+ ownArgumentWords,
17
+ ownLeadingWordParams,
18
+ paramClassOf,
19
+ wordsOf,
20
+ } from './proto';
21
+ import type { TargetDescription } from './target';
22
+
23
+ /** The size and alignment of a laid-out aggregate in bytes, or undefined where the declaration or
24
+ * the target leaves either open: a member not laid out, a member whose size or placement the
25
+ * target does not state (an enum, a bitfield), or a target that states no largest alignment. */
26
+ export function aggregateSize(
27
+ layout: AggregateLayout,
28
+ target: TargetDescription,
29
+ ): { size: number; align: number } | undefined {
30
+ const placed = place(layout, target);
31
+ return placed && { size: placed.size, align: placed.align };
32
+ }
33
+
34
+ /** Each member's offset — a bitfield's is the byte its first bit is in — with the aggregate's size
35
+ * and alignment: `aggregateSize`'s walk. The cursor is in bits, since a bitfield can end
36
+ * mid-byte. */
37
+ function place(
38
+ layout: AggregateLayout,
39
+ target: TargetDescription,
40
+ ): { size: number; align: number; offsets: number[] } | undefined {
41
+ const { aggregateBoundary, largestAlignment, bitfieldPacking } = target.compilerBehaviors;
42
+ if (layout.members === undefined || aggregateBoundary === undefined || largestAlignment === undefined) {
43
+ return undefined;
44
+ }
45
+ let bits = 0;
46
+ let align = aggregateBoundary;
47
+ const offsets: number[] = [];
48
+ for (const m of layout.members) {
49
+ if (m.bits !== undefined) {
50
+ if (bitfieldPacking !== 'contiguous' || m.bits === 0 || typeof m.type !== 'string') {
51
+ return undefined;
52
+ }
53
+ if (scalarBytes(m.type, target) === undefined) {
54
+ return undefined;
55
+ }
56
+ offsets.push(layout.kind === 'struct' ? Math.floor(bits / 8) : 0);
57
+ bits = layout.kind === 'struct' ? bits + m.bits : Math.max(bits, m.bits);
58
+ continue;
59
+ }
60
+ const one = memberSize(m, target);
61
+ if (one === undefined) {
62
+ return undefined;
63
+ }
64
+ align = Math.max(align, one.align);
65
+ if (layout.kind === 'struct') {
66
+ offsets.push(roundUp(Math.ceil(bits / 8), one.align));
67
+ bits = (offsets[offsets.length - 1] + one.size) * 8;
68
+ } else {
69
+ offsets.push(0);
70
+ bits = Math.max(bits, one.size * 8);
71
+ }
72
+ }
73
+ return { size: roundUp(Math.ceil(bits / 8), align), align, offsets };
74
+ }
75
+
76
+ /** A declared struct as the IR types it: `name` at the size this target lays it out at, spelled as
77
+ * the headers spell it (`spelling`, which is what marks it theirs — ir/types.ts), with a field at
78
+ * its offset for each member the IR can type: a scalar, a pointer, or an array of either. A member
79
+ * it cannot — a nested struct or union, a bitfield, an enum, a plain `char`, whose signedness is
80
+ * the compiler's and stated nowhere — has no field, and so no read of it is typed. The struct is
81
+ * DEFINED from the declaration (declare.ts), not from these fields. Undefined for a union, whose IR
82
+ * type carries no name to declare it by, and where this target does not size it. */
83
+ export function aggregateType(
84
+ name: string,
85
+ spelling: string,
86
+ layout: AggregateLayout,
87
+ target: TargetDescription,
88
+ ): IrType | undefined {
89
+ const placed = layout.kind === 'struct' ? place(layout, target) : undefined;
90
+ if (placed === undefined) {
91
+ return undefined;
92
+ }
93
+ const fields: StructField[] = [];
94
+ for (const [i, m] of layout.members!.entries()) {
95
+ const scalar = m.bits === undefined && typeof m.type === 'string' ? scalarType(m.type) : undefined;
96
+ if (scalar !== undefined) {
97
+ const type = (m.dims ?? []).reduceRight<IrType>((elem, n) => T.array(elem, n), scalar);
98
+ fields.push({ off: placed.offsets[i], type, name: m.name });
99
+ }
100
+ }
101
+ return { kind: 'struct', name, fields, size: placed.size, declared: spelling };
102
+ }
103
+
104
+ /** The IR type of a scalar or pointer member spelling: a pointer to a scalar keeps its pointee, any
105
+ * other pointer is `void *`, which lays out the same. */
106
+ function scalarType(spelling: string): IrType | undefined {
107
+ const s = spelling
108
+ .replace(/\b(?:const|volatile)\b/g, ' ')
109
+ .trim()
110
+ .replace(/\s+/g, ' ');
111
+ if (s.endsWith('*')) {
112
+ return T.ptr(scalarType(s.slice(0, -1)) ?? T.void());
113
+ }
114
+ if (s === 'float' || s === 'double') {
115
+ return s === 'float' ? T.f32() : T.f64();
116
+ }
117
+ const bits = declaredWidth(s);
118
+ return bits === undefined || s === 'char' ? undefined : T.int(bits, !/^u\d|\bunsigned\b/.test(s));
119
+ }
120
+
121
+ function memberSize(m: AggregateMember, target: TargetDescription): { size: number; align: number } | undefined {
122
+ const count = (m.dims ?? []).reduce((n, d) => n * d, 1);
123
+ if (typeof m.type !== 'string') {
124
+ const inner = aggregateSize(m.type, target);
125
+ return inner === undefined ? undefined : { size: inner.size * count, align: inner.align };
126
+ }
127
+ const bytes = scalarBytes(m.type, target);
128
+ if (bytes === undefined) {
129
+ return undefined;
130
+ }
131
+ return { size: bytes * count, align: Math.min(bytes, target.compilerBehaviors.largestAlignment!) };
132
+ }
133
+
134
+ /** The bytes a scalar or pointer member spelled `spelling` takes in storage. Not `declaredWidth`'s
135
+ * question, which is how many argument registers a parameter takes, and which leaves the floating
136
+ * types out because on an FPU target they take none: in memory a `float` is 4 bytes and a `double`
137
+ * 8 on every target here (IEEE single and double). An enum (`enum E`) is the target's `enumBytes`,
138
+ * and unsized where it states none. */
139
+ function scalarBytes(spelling: string, target: TargetDescription): number | undefined {
140
+ const s = spelling
141
+ .replace(/\b(?:const|volatile)\b/g, ' ')
142
+ .trim()
143
+ .replace(/\s+/g, ' ');
144
+ if (/^enum [A-Za-z_]\w*$/.test(s)) {
145
+ return target.compilerBehaviors.enumBytes;
146
+ }
147
+ const bits = s === 'float' ? 32 : s === 'double' ? 64 : declaredWidth(s);
148
+ return bits === undefined ? undefined : bits / 8;
149
+ }
150
+
151
+ const roundUp = (n: number, to: number): number => Math.ceil(n / to) * to;
152
+
153
+ /** The aggregate a declaration says `p` returns by value (`declaresAggregateReturn`): its stated
154
+ * layout, or the bare kind its `returns` spells. */
155
+ export function returnedAggregate(p: FnProto): AggregateLayout {
156
+ return p.returnLayout ?? { kind: /\bunion\b/.test(p.returns ?? '') ? 'union' : 'struct' };
157
+ }
158
+
159
+ /** THE ARGUMENT WORDS A FUNCTION'S OWN DECLARATION FILLS (`proto.ts` `ownArgumentWords`), with the
160
+ * hidden pointer a struct return may put ahead of them decided by this target's rule: a struct
161
+ * `returnsInMemory` answers `false` for comes back in registers and takes no argument word, as a
162
+ * call to it does (frontend/call-plan.ts `returnedStruct`). One the rule cannot place still counts
163
+ * the word, since the bound must not fall short. */
164
+ export function ownArgumentWordsOn(
165
+ name: string,
166
+ p: FnProto | undefined,
167
+ target: TargetDescription,
168
+ ): number | undefined {
169
+ const registerReturn = declaresAggregateReturn(p) && returnsInMemory(returnedAggregate(p!), target) === false;
170
+ return ownArgumentWords(name, p, target, registerReturn);
171
+ }
172
+
173
+ /** The first argument registers that surely hold one of the function's own word parameters
174
+ * (`proto.ts` `ownLeadingWordParams`), with the hidden pointer decided by this target's rule as
175
+ * {@link ownArgumentWordsOn} decides it. */
176
+ export function ownLeadingWordParamsOn(name: string, p: FnProto | undefined, target: TargetDescription): number {
177
+ const registerReturn = declaresAggregateReturn(p) && returnsInMemory(returnedAggregate(p!), target) === false;
178
+ return ownLeadingWordParams(name, p, target, registerReturn);
179
+ }
180
+
181
+ /** Whether a function declared to return `layout` by value hands it back through memory — so its
182
+ * caller passes the address as argument 0 — or `undefined` where the target states no rule or the
183
+ * rule needs a size the layout does not give. */
184
+ export function returnsInMemory(layout: AggregateLayout, target: TargetDescription): boolean | undefined {
185
+ const rule = target.compilerBehaviors.aggregateReturn;
186
+ if (rule === undefined) {
187
+ return undefined;
188
+ }
189
+ if (rule === 'memory') {
190
+ return true;
191
+ }
192
+ if (rule === 'svr4') {
193
+ const size = aggregateSize(layout, target)?.size;
194
+ return size === undefined ? undefined : size > 8;
195
+ }
196
+ // agbcc thumb.c:1423-1493: a struct's second member that is not a bitfield puts it in memory
197
+ // whatever its size; otherwise anything over a word does
198
+ const members = layout.members;
199
+ if (layout.kind === 'struct' && members?.slice(1).some((m) => m.bits === undefined)) {
200
+ return true;
201
+ }
202
+ const size = aggregateSize(layout, target)?.size;
203
+ if (size === undefined) {
204
+ return undefined;
205
+ }
206
+ if (size > 4 || layout.kind === 'struct') {
207
+ return size > 4;
208
+ }
209
+ // a union: in memory as soon as one member would be on its own — an array member is an aggregate
210
+ // that is neither a struct nor a union, which thumb.c puts in memory (line 1491)
211
+ let answer: boolean | undefined = false;
212
+ for (const m of members!) {
213
+ const own = m.dims !== undefined ? true : typeof m.type === 'string' ? false : returnsInMemory(m.type, target);
214
+ if (own === true) {
215
+ return true;
216
+ }
217
+ if (own === undefined) {
218
+ answer = undefined;
219
+ }
220
+ }
221
+ return answer;
222
+ }
223
+
224
+ /** Whether a call to `callee` is KNOWN not to be handed a hidden struct-return pointer in
225
+ * argument 0. A callee that returns nothing has no such pointer to be given; neither has one
226
+ * whose return travels in a register. Every other answer — including silence — is `false`,
227
+ * because this is a fact a caller must be TOLD: the two frames are the same instructions in the
228
+ * same order, so there is nothing in the assembly to read it off.
229
+ *
230
+ * A RETURN WIDER THAN A REGISTER STILL TRAVELS IN REGISTERS — it travels in a PAIR, which is
231
+ * still not a hidden pointer the caller supplied — so `declaredWidth` answering 64 is the right
232
+ * answer here rather than a width that slipped through a test meant for words. Nothing reaches
233
+ * it: `STANDARD_SIGNATURES` has one entry and it returns `void *`.
234
+ *
235
+ * TWO SOURCES AND NEITHER RANKS ABOVE THE OTHER, because on this one question they cannot
236
+ * disagree: the project's own headers — `returnsVoid`, or a struct or union `target` hands back
237
+ * in a register — and the `returns` of a signature the C standard fixes, which is as known as its
238
+ * parameters. That a project may re-declare a standard function differently is real and is why
239
+ * `declaredCall` ranks the two for ARITY — but a re-declaration that changed `memcpy` into a
240
+ * struct-returning function would not be `memcpy`.
241
+ *
242
+ * `Object.hasOwn`, not `in`: `prototypes` is caller-supplied JSON and the table is an object
243
+ * literal, so `in` would answer for `toString` and every other name on `Object.prototype`. The
244
+ * ENTRY is read through `?.` for the other half of the same fact: `decompile` is a published
245
+ * entry point that runs no `validatePrototypes`, so a `null` entry out of parsed JSON reaches
246
+ * here, and a raw TypeError would leave through neither the decline channel nor anything a
247
+ * caller can act on. Every other reader of this table — `declaredCallArgs`, and `declaredCall`
248
+ * through it — answers "nothing is declared" for such an entry, and so does this. */
249
+ export function returnsWithoutHiddenPointer(
250
+ callee: string,
251
+ prototypes: Prototypes,
252
+ target: TargetDescription,
253
+ ): boolean {
254
+ // Nothing at all, a value in registers, or a spelling nobody here can size — the last of which is
255
+ // the only one that leaves the hidden pointer open. A pair is `declaredWidth` 64 and still
256
+ // travels in registers, so a width wider than a word is an answer here and not an overflow.
257
+ const travelsInRegisters = (spelling: string): boolean => {
258
+ const t = spelling.trim();
259
+ return t === 'void' || declaredWidth(t) !== undefined;
260
+ };
261
+ const own = Object.hasOwn(prototypes, callee) ? prototypes[callee] : undefined;
262
+ if (declaresVoidReturn(own)) {
263
+ return true;
264
+ }
265
+ // A STRUCT OR UNION IS THE TARGET'S TO PLACE — agbcc hands a one-word struct back in r0, mwcc up
266
+ // to eight bytes in r3/r3:r4 — so this asks the rule the call lowering reads, and the two cannot
267
+ // disagree. A rule or a size nothing states leaves the pointer open.
268
+ if (own && declaresAggregateReturn(own)) {
269
+ return returnsInMemory(returnedAggregate(own), target) === false;
270
+ }
271
+ // A PROJECT'S OWN `returns` ANSWERS THIS THROUGH THE SAME READING A STANDARD SIGNATURE'S DOES,
272
+ // and it ranks above the table for the same reason `declaredCall` ranks a re-declaration above
273
+ // one: a project that spells the return has told you about the function it is building.
274
+ if (own?.returns !== undefined) {
275
+ return travelsInRegisters(own.returns);
276
+ }
277
+ return Object.hasOwn(STANDARD_SIGNATURES, callee) && travelsInRegisters(STANDARD_SIGNATURES[callee].returns);
278
+ }
279
+
280
+ /** One struct a call passes by value in argument words (`byValueCallArgs`). */
281
+ export interface ByValueArg {
282
+ /** the parameter's index in the declared list */
283
+ readonly param: number;
284
+ /** its first argument word, among the call's widths */
285
+ readonly word: number;
286
+ /** how many argument words it fills */
287
+ readonly words: number;
288
+ /** the struct as the IR types it (`aggregateType`) */
289
+ readonly type: IrType;
290
+ }
291
+
292
+ /** Where a declaration's by-value structs travel (`byValueCallArgs`), or why that is not known. */
293
+ type ByValueLayout =
294
+ | { widths: readonly number[]; doubles: ReadonlySet<number>; byValue: readonly ByValueArg[] }
295
+ | { refused: string; param: number };
296
+
297
+ /** THE ARGUMENT WORDS OF A DECLARATION WITH A STRUCT PASSED BY VALUE, where the target passes one
298
+ * in consecutive argument words (`compilerBehaviors.byValueAggregateArgWords`) and the declaration
299
+ * lays it out (`FnProto.byValueLayouts`): each such parameter is its size in words, rounded up, of
300
+ * 32 bits each, and every other parameter is what `declaredCallArgs` makes of it. Undefined where
301
+ * any parameter is unsized that way — a union, a layout this target does not size, a spelling that
302
+ * names no tag — or placed where no rule here follows it (`byValueRefusal` names which), and where
303
+ * the declaration passes no struct by value at all, which `declaredCallArgs` answers alone. */
304
+ export function byValueCallArgs(
305
+ p: FnProto | undefined,
306
+ target: TargetDescription,
307
+ hidden = 0,
308
+ ): { widths: readonly number[]; doubles: ReadonlySet<number>; byValue: readonly ByValueArg[] } | undefined {
309
+ const laid = layOutByValue(p, target, hidden);
310
+ return laid === undefined || 'refused' in laid ? undefined : laid;
311
+ }
312
+
313
+ /** Why `byValueCallArgs` places none of a declaration that does pass a struct or union by value:
314
+ * the parameter it stops at (an index into `params`) and the reason. Undefined where it places
315
+ * them, or where the declaration passes none. */
316
+ export function byValueRefusal(
317
+ p: FnProto | undefined,
318
+ target: TargetDescription,
319
+ hidden = 0,
320
+ ): { refused: string; param: number } | undefined {
321
+ const laid = layOutByValue(p, target, hidden);
322
+ return laid !== undefined && 'refused' in laid ? laid : undefined;
323
+ }
324
+
325
+ /** `hidden` is the argument words ahead of the declared list — 1 for a call handed a hidden
326
+ * struct-return pointer in argument 0 — which the even-word rule counts and `widths` leaves out. */
327
+ function layOutByValue(p: FnProto | undefined, target: TargetDescription, hidden: number): ByValueLayout | undefined {
328
+ if (!Array.isArray(p?.params)) {
329
+ return undefined;
330
+ }
331
+ const params = p.params;
332
+ const first = params.findIndex((_, i) => paramClassOf(p, params, i) === 'by-value');
333
+ if (first < 0) {
334
+ return undefined;
335
+ }
336
+ if (!target.compilerBehaviors.byValueAggregateArgWords) {
337
+ return { refused: 'this target states no rule for which argument words carry one', param: first };
338
+ }
339
+ const widths: number[] = [];
340
+ const doubles = new Set<number>();
341
+ const byValue: ByValueArg[] = [];
342
+ for (const [i, t] of params.entries()) {
343
+ if (paramClassOf(p, params, i) === 'by-value') {
344
+ const layout =
345
+ p.byValueLayouts !== undefined && Object.hasOwn(p.byValueLayouts, i) ? p.byValueLayouts[i] : undefined;
346
+ if (layout === undefined) {
347
+ return { refused: 'the declaration does not lay it out (a context does, by defining the struct)', param: i };
348
+ }
349
+ const spelling = t
350
+ .replace(/\b(?:const|volatile)\b/g, ' ')
351
+ .trim()
352
+ .replace(/\s+/g, ' ');
353
+ const tag = /^(?:struct\s+)?([A-Za-z_]\w*)$/.exec(spelling)?.[1];
354
+ const type = tag === undefined ? undefined : aggregateType(tag, spelling, layout, target);
355
+ if (type?.kind !== 'struct' || type.size === undefined) {
356
+ return { refused: 'it is a union, or has a member this target does not size', param: i };
357
+ }
358
+ // A STRUCT OF NO BYTES (GNU C's `struct E { };`) occupies no argument word, so it would be an
359
+ // argument with no operand to name it, and the fold that names the struct has none to read.
360
+ if (type.size === 0) {
361
+ return { refused: 'it has no members, so no argument word carries it', param: i };
362
+ }
363
+ // An 8-aligned struct starts at an even argument word where the target aligns one
364
+ // (`evenWordAlignedArgs`). Placed at an even word that changes nothing; placed at an odd
365
+ // one, the skipped word would be an argument operand nothing passes, so that one refuses.
366
+ // The word is counted from argument 0, a hidden struct-return pointer included: O32's
367
+ // `struct S8 f(s32 k, struct L l)` passes `l` in a2:a3, and `struct S8 f(struct L l)` skips a1.
368
+ if (
369
+ target.evenWordAlignedArgs === true &&
370
+ (aggregateSize(layout, target)?.align ?? 0) >= 8 &&
371
+ (hidden + wordsOf(widths, target)) % 2 === 1
372
+ ) {
373
+ return {
374
+ refused:
375
+ 'it is aligned to 8 and would start at an odd argument word, which this target skips — an argument word left empty is not modelled',
376
+ param: i,
377
+ };
378
+ }
379
+ const words = Math.ceil(type.size / 4);
380
+ byValue.push({ param: i, word: widths.length, words, type });
381
+ widths.push(...Array.from({ length: words }, () => 32));
382
+ continue;
383
+ }
384
+ const one = declaredCallArgs({ params: [t], wordParams: p.wordParams?.includes(i) ? [0] : [] }, target);
385
+ if (one === undefined) {
386
+ return {
387
+ refused: `its parameter ${i + 1} '${t}' is not sized, so where the struct starts is not known`,
388
+ param: first,
389
+ };
390
+ }
391
+ if (one.doubles.size > 0) {
392
+ doubles.add(widths.length);
393
+ }
394
+ widths.push(...one.widths);
395
+ }
396
+ return { widths, doubles, byValue };
397
+ }
package/src/backend/c.ts CHANGED
@@ -6,15 +6,19 @@
6
6
  // this backend owns only the C SIGNATURE line. The Pascal backend implements the same
7
7
  // LanguageBackend interface over the same L3 with its OWN spelling.
8
8
  import { LanguageBackend, SFn } from '../l3/ast';
9
- import { cComment, cType, emitCFamily } from './cfamily';
9
+ import { cComment, cDeclare, cType, emitCFamily } from './cfamily';
10
10
 
11
11
  export { cComment }; // re-export: the shared spelling lives in cfamily.ts
12
12
 
13
13
  export const cBackend: LanguageBackend = {
14
14
  id: 'c',
15
15
  spellsSwitchFallthrough: true,
16
+ spellsSwitchBreak: true,
16
17
  emit(fn: SFn): string {
17
- const params = fn.params.map((p) => `${cType(p.type)} ${p.name}`).join(', ') || 'void';
18
+ // The SAME declarator placement the local list and the struct-field printer use, so one
19
+ // function cannot spell the `*` on the type in its signature and on the declarator in its
20
+ // body.
21
+ const params = fn.params.map((p) => cDeclare(p.type, p.name)).join(', ') || 'void';
18
22
  return emitCFamily(`${cType(fn.retType)} ${fn.name}(${params})`, fn);
19
23
  },
20
24
  comment: cComment,