@asmlift/core 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +34 -30
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +248 -0
  4. package/src/backend/c.ts +5 -2
  5. package/src/backend/cfamily.ts +236 -37
  6. package/src/backend/cpp.ts +114 -9
  7. package/src/backend/pascal.ts +39 -0
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +615 -79
  15. package/src/declare.ts +75 -5
  16. package/src/frontend/asmdata.ts +19 -5
  17. package/src/frontend/call-plan.ts +686 -0
  18. package/src/frontend/device-pins.ts +277 -0
  19. package/src/frontend/errors.ts +5 -0
  20. package/src/frontend/flags-edge.ts +101 -0
  21. package/src/frontend/fpu.ts +167 -0
  22. package/src/frontend/frame-objects.ts +2131 -0
  23. package/src/frontend/high-half.ts +2 -1
  24. package/src/frontend/local-object.ts +490 -0
  25. package/src/frontend/mips.ts +363 -40
  26. package/src/frontend/opaque.ts +97 -18
  27. package/src/frontend/ppc.ts +520 -129
  28. package/src/frontend/reloc-symbol.ts +94 -13
  29. package/src/frontend/splat.ts +108 -5
  30. package/src/frontend/ssa.ts +669 -76
  31. package/src/frontend/stackargs.ts +120 -39
  32. package/src/frontend/thumb.ts +2764 -2018
  33. package/src/ir/alias.ts +29 -0
  34. package/src/ir/core.ts +101 -3
  35. package/src/ir/discipline.ts +248 -0
  36. package/src/ir/float-bits.ts +47 -0
  37. package/src/ir/opcodes.ts +84 -112
  38. package/src/ir/parse.ts +1 -0
  39. package/src/ir/types.ts +118 -2
  40. package/src/ir/verify.ts +150 -3
  41. package/src/l3/address.ts +5 -0
  42. package/src/l3/argcopy.ts +38 -6
  43. package/src/l3/ast.ts +186 -14
  44. package/src/l3/coalesce.ts +21 -13
  45. package/src/l3/dce.ts +32 -10
  46. package/src/l3/gates.ts +9 -3
  47. package/src/l3/hoist.ts +8 -7
  48. package/src/l3/initfirst.ts +27 -9
  49. package/src/l3/intcell.ts +97 -0
  50. package/src/l3/mentions.ts +10 -4
  51. package/src/l3/narrowdecl.ts +218 -0
  52. package/src/l3/nearbase.ts +10 -5
  53. package/src/l3/parkfirst.ts +8 -5
  54. package/src/l3/pollguard.ts +3 -2
  55. package/src/l3/ptrcell.ts +357 -0
  56. package/src/l3/regspell.ts +17 -3
  57. package/src/l3/reindex.ts +10 -7
  58. package/src/l3/symbol-refs.ts +104 -23
  59. package/src/l3/tailmerge.ts +6 -19
  60. package/src/l3/typing.ts +137 -25
  61. package/src/l3/unreduce.ts +4 -4
  62. package/src/l3/volstore.ts +96 -7
  63. package/src/mangle.ts +11 -1
  64. package/src/pattern/engine.ts +19 -16
  65. package/src/pipeline.ts +57 -12
  66. package/src/proto-context.ts +788 -0
  67. package/src/proto.ts +689 -50
  68. package/src/raise/arrays.ts +4 -3
  69. package/src/raise/declared-volatile.ts +131 -0
  70. package/src/raise/divpow2.ts +2 -2
  71. package/src/raise/errors.ts +17 -0
  72. package/src/raise/floathelpers.ts +243 -0
  73. package/src/raise/globalshape.ts +11 -5
  74. package/src/raise/gvn.ts +26 -5
  75. package/src/raise/narrow.ts +29 -2
  76. package/src/raise/narrowlocal.ts +11 -6
  77. package/src/raise/offsetnames.ts +510 -0
  78. package/src/raise/pairparams.ts +81 -0
  79. package/src/raise/paramwidth.ts +7 -0
  80. package/src/raise/pre-recovery.ts +78 -4
  81. package/src/raise/recover.ts +14 -10
  82. package/src/raise/retsink.ts +1 -1
  83. package/src/raise/shortcircuit.ts +41 -19
  84. package/src/raise/softdiv.ts +21 -23
  85. package/src/raise/struct-arrays.ts +5 -4
  86. package/src/raise/structs.ts +311 -98
  87. package/src/raise/truncload.ts +366 -0
  88. package/src/raise/widehelpers.ts +106 -0
  89. package/src/rank-declare.ts +30 -5
  90. package/src/rank-variations.ts +36 -8
  91. package/src/rank.ts +267 -79
  92. package/src/runtime-helpers.ts +287 -0
  93. package/src/stillborn.ts +289 -0
  94. package/src/structure/analysis.ts +1285 -448
  95. package/src/structure/hazards.ts +633 -58
  96. package/src/structure/local-statics.ts +381 -0
  97. package/src/structure/namecoalesce.ts +1 -1
  98. package/src/structure/redundant-test.ts +99 -0
  99. package/src/structure/structure.ts +1772 -395
  100. package/src/structure/switch-recover.ts +159 -78
  101. package/src/symbols.ts +38 -3
  102. package/src/target.ts +662 -59
  103. package/src/trace.ts +44 -24
  104. package/src/variation-definitions.ts +119 -1
  105. package/src/variation-tokens.ts +20 -6
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 a read of a map-declared volatile object), `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
@@ -125,16 +126,19 @@ direct calls, constant-offset and variable-index memory (`*p`, `p[n]`, `a[i]`, s
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
128
  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
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 lifts an
132
+ address-taken local the frame-object audit can bound — a scalar, or an untyped buffer that fills
133
+ the frame — and declines the rest; MIPS models word `sp` slots and PPC elides callee-saved save
134
+ slots, so a spill/reload pair is modelled on those two — anything the narrow models cannot honour
135
+ declines),
136
+ **condition flags at a join** (a compare two or more edges bring to one branch, or one the caller
137
+ set), computed tail calls, PIC and `gp`-relative access outside PowerPC's
134
138
  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.
139
+ switch fall-through, irreducible loops and loops that exit to more than one live merge, floats,
140
+ and 64-bit memory ops. Prototypes (callee arities, void-ness) come from a caller-supplied map, as a
141
+ real project takes them from headers.
138
142
 
139
143
  ## Tests
140
144
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asmlift/core",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Match decompile an assembly function to C or Pascal",
@@ -0,0 +1,248 @@
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
+ declaredWidth,
13
+ declaresAggregateReturn,
14
+ declaresVoidReturn,
15
+ } from './proto';
16
+ import type { TargetDescription } from './target';
17
+
18
+ /** The size and alignment of a laid-out aggregate in bytes, or undefined where the declaration or
19
+ * the target leaves either open: a member not laid out, a member whose size or placement the
20
+ * target does not state (an enum, a bitfield), or a target that states no largest alignment. */
21
+ export function aggregateSize(
22
+ layout: AggregateLayout,
23
+ target: TargetDescription,
24
+ ): { size: number; align: number } | undefined {
25
+ const placed = place(layout, target);
26
+ return placed && { size: placed.size, align: placed.align };
27
+ }
28
+
29
+ /** Each member's offset — a bitfield's is the byte its first bit is in — with the aggregate's size
30
+ * and alignment: `aggregateSize`'s walk. The cursor is in bits, since a bitfield can end
31
+ * mid-byte. */
32
+ function place(
33
+ layout: AggregateLayout,
34
+ target: TargetDescription,
35
+ ): { size: number; align: number; offsets: number[] } | undefined {
36
+ const { aggregateBoundary, largestAlignment, bitfieldPacking } = target.compilerBehaviors;
37
+ if (layout.members === undefined || aggregateBoundary === undefined || largestAlignment === undefined) {
38
+ return undefined;
39
+ }
40
+ let bits = 0;
41
+ let align = aggregateBoundary;
42
+ const offsets: number[] = [];
43
+ for (const m of layout.members) {
44
+ if (m.bits !== undefined) {
45
+ if (bitfieldPacking !== 'contiguous' || m.bits === 0 || typeof m.type !== 'string') {
46
+ return undefined;
47
+ }
48
+ if (scalarBytes(m.type, target) === undefined) {
49
+ return undefined;
50
+ }
51
+ offsets.push(layout.kind === 'struct' ? Math.floor(bits / 8) : 0);
52
+ bits = layout.kind === 'struct' ? bits + m.bits : Math.max(bits, m.bits);
53
+ continue;
54
+ }
55
+ const one = memberSize(m, target);
56
+ if (one === undefined) {
57
+ return undefined;
58
+ }
59
+ align = Math.max(align, one.align);
60
+ if (layout.kind === 'struct') {
61
+ offsets.push(roundUp(Math.ceil(bits / 8), one.align));
62
+ bits = (offsets[offsets.length - 1] + one.size) * 8;
63
+ } else {
64
+ offsets.push(0);
65
+ bits = Math.max(bits, one.size * 8);
66
+ }
67
+ }
68
+ return { size: roundUp(Math.ceil(bits / 8), align), align, offsets };
69
+ }
70
+
71
+ /** A declared struct as the IR types it: `name` at the size this target lays it out at, spelled as
72
+ * the headers spell it (`spelling`, which is what marks it theirs — ir/types.ts), with a field at
73
+ * its offset for each member the IR can type: a scalar, a pointer, or an array of either. A member
74
+ * it cannot — a nested struct or union, a bitfield, an enum, a plain `char`, whose signedness is
75
+ * the compiler's and stated nowhere — has no field, and so no read of it is typed. The struct is
76
+ * DEFINED from the declaration (declare.ts), not from these fields. Undefined for a union, whose IR
77
+ * type carries no name to declare it by, and where this target does not size it. */
78
+ export function aggregateType(
79
+ name: string,
80
+ spelling: string,
81
+ layout: AggregateLayout,
82
+ target: TargetDescription,
83
+ ): IrType | undefined {
84
+ const placed = layout.kind === 'struct' ? place(layout, target) : undefined;
85
+ if (placed === undefined) {
86
+ return undefined;
87
+ }
88
+ const fields: StructField[] = [];
89
+ for (const [i, m] of layout.members!.entries()) {
90
+ const scalar = m.bits === undefined && typeof m.type === 'string' ? scalarType(m.type) : undefined;
91
+ if (scalar !== undefined) {
92
+ const type = (m.dims ?? []).reduceRight<IrType>((elem, n) => T.array(elem, n), scalar);
93
+ fields.push({ off: placed.offsets[i], type, name: m.name });
94
+ }
95
+ }
96
+ return { kind: 'struct', name, fields, size: placed.size, declared: spelling };
97
+ }
98
+
99
+ /** The IR type of a scalar or pointer member spelling: a pointer to a scalar keeps its pointee, any
100
+ * other pointer is `void *`, which lays out the same. */
101
+ function scalarType(spelling: string): IrType | undefined {
102
+ const s = spelling
103
+ .replace(/\b(?:const|volatile)\b/g, ' ')
104
+ .trim()
105
+ .replace(/\s+/g, ' ');
106
+ if (s.endsWith('*')) {
107
+ return T.ptr(scalarType(s.slice(0, -1)) ?? T.void());
108
+ }
109
+ if (s === 'float' || s === 'double') {
110
+ return s === 'float' ? T.f32() : T.f64();
111
+ }
112
+ const bits = declaredWidth(s);
113
+ return bits === undefined || s === 'char' ? undefined : T.int(bits, !/^u\d|\bunsigned\b/.test(s));
114
+ }
115
+
116
+ function memberSize(m: AggregateMember, target: TargetDescription): { size: number; align: number } | undefined {
117
+ const count = (m.dims ?? []).reduce((n, d) => n * d, 1);
118
+ if (typeof m.type !== 'string') {
119
+ const inner = aggregateSize(m.type, target);
120
+ return inner === undefined ? undefined : { size: inner.size * count, align: inner.align };
121
+ }
122
+ const bytes = scalarBytes(m.type, target);
123
+ if (bytes === undefined) {
124
+ return undefined;
125
+ }
126
+ return { size: bytes * count, align: Math.min(bytes, target.compilerBehaviors.largestAlignment!) };
127
+ }
128
+
129
+ /** The bytes a scalar or pointer member spelled `spelling` takes in storage. Not `declaredWidth`'s
130
+ * question, which is how many argument registers a parameter takes, and which leaves the floating
131
+ * types out because on an FPU target they take none: in memory a `float` is 4 bytes and a `double`
132
+ * 8 on every target here (IEEE single and double). An enum (`enum E`) is the target's `enumBytes`,
133
+ * and unsized where it states none. */
134
+ function scalarBytes(spelling: string, target: TargetDescription): number | undefined {
135
+ const s = spelling
136
+ .replace(/\b(?:const|volatile)\b/g, ' ')
137
+ .trim()
138
+ .replace(/\s+/g, ' ');
139
+ if (/^enum [A-Za-z_]\w*$/.test(s)) {
140
+ return target.compilerBehaviors.enumBytes;
141
+ }
142
+ const bits = s === 'float' ? 32 : s === 'double' ? 64 : declaredWidth(s);
143
+ return bits === undefined ? undefined : bits / 8;
144
+ }
145
+
146
+ const roundUp = (n: number, to: number): number => Math.ceil(n / to) * to;
147
+
148
+ /** The aggregate a declaration says `p` returns by value (`declaresAggregateReturn`): its stated
149
+ * layout, or the bare kind its `returns` spells. */
150
+ export function returnedAggregate(p: FnProto): AggregateLayout {
151
+ return p.returnLayout ?? { kind: /\bunion\b/.test(p.returns ?? '') ? 'union' : 'struct' };
152
+ }
153
+
154
+ /** Whether a function declared to return `layout` by value hands it back through memory — so its
155
+ * caller passes the address as argument 0 — or `undefined` where the target states no rule or the
156
+ * rule needs a size the layout does not give. */
157
+ export function returnsInMemory(layout: AggregateLayout, target: TargetDescription): boolean | undefined {
158
+ const rule = target.compilerBehaviors.aggregateReturn;
159
+ if (rule === undefined) {
160
+ return undefined;
161
+ }
162
+ if (rule === 'svr4') {
163
+ const size = aggregateSize(layout, target)?.size;
164
+ return size === undefined ? undefined : size > 8;
165
+ }
166
+ // agbcc thumb.c:1423-1493: a struct's second member that is not a bitfield puts it in memory
167
+ // whatever its size; otherwise anything over a word does
168
+ const members = layout.members;
169
+ if (layout.kind === 'struct' && members?.slice(1).some((m) => m.bits === undefined)) {
170
+ return true;
171
+ }
172
+ const size = aggregateSize(layout, target)?.size;
173
+ if (size === undefined) {
174
+ return undefined;
175
+ }
176
+ if (size > 4 || layout.kind === 'struct') {
177
+ return size > 4;
178
+ }
179
+ // a union: in memory as soon as one member would be on its own — an array member is an aggregate
180
+ // that is neither a struct nor a union, which thumb.c puts in memory (line 1491)
181
+ let answer: boolean | undefined = false;
182
+ for (const m of members!) {
183
+ const own = m.dims !== undefined ? true : typeof m.type === 'string' ? false : returnsInMemory(m.type, target);
184
+ if (own === true) {
185
+ return true;
186
+ }
187
+ if (own === undefined) {
188
+ answer = undefined;
189
+ }
190
+ }
191
+ return answer;
192
+ }
193
+
194
+ /** Whether a call to `callee` is KNOWN not to be handed a hidden struct-return pointer in
195
+ * argument 0. A callee that returns nothing has no such pointer to be given; neither has one
196
+ * whose return travels in a register. Every other answer — including silence — is `false`,
197
+ * because this is a fact a caller must be TOLD: the two frames are the same instructions in the
198
+ * same order, so there is nothing in the assembly to read it off.
199
+ *
200
+ * A RETURN WIDER THAN A REGISTER STILL TRAVELS IN REGISTERS — it travels in a PAIR, which is
201
+ * still not a hidden pointer the caller supplied — so `declaredWidth` answering 64 is the right
202
+ * answer here rather than a width that slipped through a test meant for words. Nothing reaches
203
+ * it: `STANDARD_SIGNATURES` has one entry and it returns `void *`.
204
+ *
205
+ * TWO SOURCES AND NEITHER RANKS ABOVE THE OTHER, because on this one question they cannot
206
+ * disagree: the project's own headers — `returnsVoid`, or a struct or union `target` hands back
207
+ * in a register — and the `returns` of a signature the C standard fixes, which is as known as its
208
+ * parameters. That a project may re-declare a standard function differently is real and is why
209
+ * `declaredCall` ranks the two for ARITY — but a re-declaration that changed `memcpy` into a
210
+ * struct-returning function would not be `memcpy`.
211
+ *
212
+ * `Object.hasOwn`, not `in`: `prototypes` is caller-supplied JSON and the table is an object
213
+ * literal, so `in` would answer for `toString` and every other name on `Object.prototype`. The
214
+ * ENTRY is read through `?.` for the other half of the same fact: `decompile` is a published
215
+ * entry point that runs no `validatePrototypes`, so a `null` entry out of parsed JSON reaches
216
+ * here, and a raw TypeError would leave through neither the decline channel nor anything a
217
+ * caller can act on. Every other reader of this table — `declaredCallArgs`, and `declaredCall`
218
+ * through it — answers "nothing is declared" for such an entry, and so does this. */
219
+ export function returnsWithoutHiddenPointer(
220
+ callee: string,
221
+ prototypes: Prototypes,
222
+ target: TargetDescription,
223
+ ): boolean {
224
+ // Nothing at all, a value in registers, or a spelling nobody here can size — the last of which is
225
+ // the only one that leaves the hidden pointer open. A pair is `declaredWidth` 64 and still
226
+ // travels in registers, so a width wider than a word is an answer here and not an overflow.
227
+ const travelsInRegisters = (spelling: string): boolean => {
228
+ const t = spelling.trim();
229
+ return t === 'void' || declaredWidth(t) !== undefined;
230
+ };
231
+ const own = Object.hasOwn(prototypes, callee) ? prototypes[callee] : undefined;
232
+ if (declaresVoidReturn(own)) {
233
+ return true;
234
+ }
235
+ // A STRUCT OR UNION IS THE TARGET'S TO PLACE — agbcc hands a one-word struct back in r0, mwcc up
236
+ // to eight bytes in r3/r3:r4 — so this asks the rule the call lowering reads, and the two cannot
237
+ // disagree. A rule or a size nothing states leaves the pointer open.
238
+ if (own && declaresAggregateReturn(own)) {
239
+ return returnsInMemory(returnedAggregate(own), target) === false;
240
+ }
241
+ // A PROJECT'S OWN `returns` ANSWERS THIS THROUGH THE SAME READING A STANDARD SIGNATURE'S DOES,
242
+ // and it ranks above the table for the same reason `declaredCall` ranks a re-declaration above
243
+ // one: a project that spells the return has told you about the function it is building.
244
+ if (own?.returns !== undefined) {
245
+ return travelsInRegisters(own.returns);
246
+ }
247
+ return Object.hasOwn(STANDARD_SIGNATURES, callee) && travelsInRegisters(STANDARD_SIGNATURES[callee].returns);
248
+ }
package/src/backend/c.ts CHANGED
@@ -6,7 +6,7 @@
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
 
@@ -14,7 +14,10 @@ export const cBackend: LanguageBackend = {
14
14
  id: 'c',
15
15
  spellsSwitchFallthrough: true,
16
16
  emit(fn: SFn): string {
17
- const params = fn.params.map((p) => `${cType(p.type)} ${p.name}`).join(', ') || 'void';
17
+ // The SAME declarator placement the local list and the struct-field printer use, so one
18
+ // function cannot spell the `*` on the type in its signature and on the declarator in its
19
+ // body.
20
+ const params = fn.params.map((p) => cDeclare(p.type, p.name)).join(', ') || 'void';
18
21
  return emitCFamily(`${cType(fn.retType)} ${fn.name}(${params})`, fn);
19
22
  },
20
23
  comment: cComment,