@asmlift/core 0.1.0 → 0.3.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.
package/README.md CHANGED
@@ -28,15 +28,10 @@ const { source, diagnostics } = decompile('my_func', asm, MIPS_IDO);
28
28
  console.log(source); // s32 my_func(s32 a0) { ... }
29
29
  ```
30
30
 
31
- Input is **text**: GBA `.s` assembly for the ARM target both agbcc compiler output and
32
- pret-style project splits (`asm/nonmatchings/`: `thumb_func_start` macros, `_08xxxxxx` labels,
33
- `.4byte` literal pools) and `objdump -d --no-show-raw-insn`
34
- output for MIPS/PPC the format follows what each target's toolchain actually produces. The
35
- frontends classify the input first (`frontend/format.ts`): text that positively matches the
36
- _other_ format declines at the boundary naming both, instead of failing confusingly mid-decode.
37
- Multi-function input is sliced to the named symbol; an absent symbol declines with the list of
38
- symbols present. (`@asmlift/cli` additionally accepts ELF **object files** and runs the right
39
- objdump for you.)
31
+ Input is **text**, following what each target's toolchain produces:
32
+
33
+ - 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`.
40
35
 
41
36
  ### `decompile(name, asm, target, opts?)`
42
37
 
@@ -84,20 +79,20 @@ injected via hooks, never copied. `verify()` runs after every IR-mutating pass;
84
79
 
85
80
  ### Modules
86
81
 
87
- | Module | What it is |
88
- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
89
- | `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) |
90
- | `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), `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) |
91
- | `pattern/engine.ts` | Idiom layer: **rewrite patterns as data** + greedy driver + DCE; `patternApplies` gates on Target capabilities |
92
- | `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 → struct-pointer → short-circuit; plus `recover.ts` (L1→L2 type recovery), `retsink.ts` (return-sinking), `errors.ts` (`RaiseUnsupportedError`) |
93
- | `structure/*.ts` | L2→L3 in four modules: `loops.ts` (natural-loop discovery), `analysis.ts` (use registry, liveness, C4 materialization), `switch-recover.ts` (Regime-A comparison-tree recovery), `structure.ts` (SSA-destruction coalescing with interference checks + emission: if/while/do-while/for/switch, break/early-return) |
94
- | `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 differ-ranked re-spelling levers `regspell.ts` + `reindex.ts`, and `typing.ts` (the rendered-expression C type the backends and contracts share) |
95
- | `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) |
96
- | `pipeline.ts` | `decompile()` + the shared tower spine + annotate-mode stubs/diagnostics |
97
- | `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 |
98
- | `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 |
99
- | `target.ts` | `TargetDescription` (ABI + capabilities + compilerBehaviors as data — no `arch ==` in shared code); toolchain paths live in `@asmlift/toolchains` |
100
- | `contracts.ts`, `proto.ts`, `mangle.ts` | Boundary contracts; prototype tables; the CodeWarrior mangler |
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) |
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 → struct-pointer → short-circuit; plus `recover.ts` (L1→L2 type recovery), `retsink.ts` (return-sinking), `errors.ts` (`RaiseUnsupportedError`) |
88
+ | `structure/*.ts` | L2→L3 in four modules: `loops.ts` (natural-loop discovery), `analysis.ts` (use registry, liveness, C4 materialization), `switch-recover.ts` (Regime-A comparison-tree recovery), `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 differ-ranked re-spelling levers `regspell.ts` + `reindex.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 data — no `arch ==` in shared code); toolchain paths live in `@asmlift/toolchains` |
95
+ | `contracts.ts`, `proto.ts`, `mangle.ts` | Boundary contracts; prototype tables; the CodeWarrior mangler |
101
96
 
102
97
  Scoring and ranking live across the package seam in [`@asmlift/cli`](../cli/README.md):
103
98
  `score.ts` + `objdiff.ts` (toolchain compiles → in-process pinned `objdiff-wasm`, fail-closed)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asmlift/core",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Match decompile an assembly function to C or Pascal",
@@ -47,14 +47,38 @@ export function cType(t: IrType): string {
47
47
  }
48
48
 
49
49
  /** Declare a name of a given type, C declarator rules: an array puts its length AFTER the name
50
- * (`u8 _pad[4]`), everything else is the prefix `cType name`. */
50
+ * (`u8 _pad[4]`), a pointer binds its `*` to the declarator (`void *p`), everything else is
51
+ * the prefix `cType name`. */
51
52
  function cDeclare(t: IrType, name: string): string {
52
53
  if (t.kind === 'array') {
53
54
  return `${cType(t.elem)} ${name}[${t.count}]`;
54
55
  }
56
+ if (t.kind === 'ptr') {
57
+ return `${cType(t.to)} *${name}`;
58
+ }
55
59
  return `${cType(t)} ${name}`;
56
60
  }
57
61
 
62
+ /** One field of a rendered struct declaration — the minimal input shape shared by the two
63
+ * producers of `struct N { ... };` text: the backend's recovered structs (SFn.structs, whose
64
+ * StructType fields already carry pads as real `u8[N]` members) and the cli's map-layout
65
+ * declaration synthesis (declare.ts, which seats fields at exact offsets by interleaving pad
66
+ * fields itself). `volatile` is the MMIO member idiom (`volatile u16 gain;`) — only the
67
+ * map-derived synthesis sets it today; recovered structs never do. */
68
+ export interface StructFieldDecl {
69
+ name: string;
70
+ type: IrType;
71
+ volatile?: boolean;
72
+ }
73
+
74
+ /** THE struct-declaration spelling — every `struct N { ... };` asmlift prints comes from here,
75
+ * so the backend's recovered-struct decls and the scoring layer's synthesized decls cannot
76
+ * drift apart. One line, fields in caller order (the type is self-describing: padding is the
77
+ * caller's discipline, already present as real fields). */
78
+ export function renderStructDecl(name: string, fields: StructFieldDecl[]): string {
79
+ return `struct ${name} { ${fields.map((f) => `${f.volatile ? 'volatile ' : ''}${cDeclare(f.type, f.name)};`).join(' ')} };`;
80
+ }
81
+
58
82
  // A LEAF hook lets a C-family backend override how a `var` or `index` node spells WITHOUT
59
83
  // re-implementing precedence, parenthesization, or statement structure. It returns the
60
84
  // replacement text, or null to fall through to the default C spelling — how the C++ backend
@@ -121,6 +145,10 @@ function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook):
121
145
  const baseTxt = hooked ?? `${rec(ix.base, 1)}[${rec(ix.idx, 99)}]`;
122
146
  return `${baseTxt}.${e.name}`;
123
147
  }
148
+ // explicit dot: a struct-VALUE global's field (`gSym.field`, symbol-map layout spelling)
149
+ if (e.dot) {
150
+ return `${rec(e.base, 1)}.${e.name}`;
151
+ }
124
152
  return `${rec(e.base, 1)}->${e.name}`;
125
153
  }
126
154
  case 'un': {
@@ -333,9 +361,7 @@ function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
333
361
  * raise/structs.ts (unaccessed leading/interior gaps) interleave them where natural C alignment
334
362
  * does not already cover the offset. This just declares each field in order. */
335
363
  function structDecls(fn: SFn): string[] {
336
- return (fn.structs ?? []).map(
337
- (s) => `struct ${s.name} { ${s.fields.map((f) => cDeclare(f.type, f.name) + ';').join(' ')} };`,
338
- );
364
+ return (fn.structs ?? []).map((s) => renderStructDecl(s.name, s.fields));
339
365
  }
340
366
 
341
367
  /** Assemble a full C-family function from a caller-supplied signature line and the shared body. */
package/src/contracts.ts CHANGED
@@ -73,6 +73,8 @@ export function assertDerefsTyped(sfn: SFn): void {
73
73
  const bad: string[] = [];
74
74
  // Ops C rejects outright on a pointer operand (the additive ops and &&/|| are legal C).
75
75
  const NO_PTR_OPS = new Set(['&', '|', '^', '<<', '>>', '*', '/', '%']);
76
+ // The comparison operators — where a bare `&SYM` operand is SIGN-ambiguous, not ill-formed.
77
+ const CMP_OPS = new Set(['<', '<=', '>', '>=', '==', '!=']);
76
78
  // 1/2/4 only: the decomp typedef vocabulary (C_TYPEDEFS) has no 64-bit scalar, so a width-8
77
79
  // access would print as the nonexistent `(s64 *)` — exactly the three-stages-later failure
78
80
  // this rule pre-empts. (If f64 loads ever land they are floats, not a scalar width here.)
@@ -97,6 +99,34 @@ export function assertDerefsTyped(sfn: SFn): void {
97
99
  }
98
100
  }
99
101
  }
102
+ // A bare global ADDRESS `&SYM` under `+`/`-` is an ESCAPING interior pointer: C scales the byte
103
+ // offset by sizeof(SYM), which is unknown for a header-typed global, so `&SYM + N` is byte-
104
+ // inexact. Nothing emits this shape anymore: a load/store base folds byte-correctly (globalOf
105
+ // turns `&SYM + N` into an `index`/`field` node whose base is a bare `addr`), and the additive
106
+ // lowering intifies every other `addr` operand to `(u32)&SYM` (structure.ts intifyAddr — the
107
+ // cast types int, so it never lands here). A bare `addr` reaching a `+`/`-` operand is therefore
108
+ // a lowering REGRESSION — flag it rather than emit wrong bytes.
109
+ if (e.k === 'bin' && (e.op === '+' || e.op === '-')) {
110
+ const addrSide = e.l.k === 'addr' ? e.l : e.r.k === 'addr' ? e.r : undefined;
111
+ if (addrSide) {
112
+ bad.push(`interior pointer arithmetic on the global address '&${addrSide.name}'`);
113
+ }
114
+ }
115
+ // A bare global address `&SYM` as a COMPARISON operand is the same unspelled escape under a
116
+ // different operator — and worse than ill-formed: the compare's SIGNEDNESS is spelled by the
117
+ // operand TYPES (the structurer maps icmp_ult and icmp_slt to the same '<'), and `&SYM`'s C
118
+ // type is the project's own declaration, unknowable here — so the emitted compare can flip
119
+ // signedness against the asm's, silently. The cmp lowering intifies it signedness-aware
120
+ // (`(u32)`/`(s32)&SYM` — structure.ts intifyAddrCmp; the cast types int, so it never lands
121
+ // here). A bare `addr` reaching a comparison operand is therefore a lowering REGRESSION —
122
+ // flag it rather than emit sign-ambiguous C.
123
+ if (e.k === 'bin' && CMP_OPS.has(e.op)) {
124
+ for (const side of [e.l, e.r]) {
125
+ if (side.k === 'addr') {
126
+ bad.push(`bare global address '&${side.name}' as a comparison operand`);
127
+ }
128
+ }
129
+ }
100
130
  // `!p` is legal C (pointer truthiness); `-p`/`~p` are not.
101
131
  if (e.k === 'un' && e.op !== '!' && ctype(e.e)?.kind === 'ptr') {
102
132
  bad.push(`pointer operand under unary '${e.op}'`);
package/src/declare.ts ADDED
@@ -0,0 +1,225 @@
1
+ // asmlift — declaration SYNTHESIS for self-declaring candidates
2
+ // (research/self-declaring-candidates-2026-07-26.md).
3
+ //
4
+ // A scored candidate that names map-derived symbols must compile WITHOUT the project's headers:
5
+ // this module renders the declaration block for exactly the symbols the candidate's tree
6
+ // references in a value context (Candidate.symbolRefs — derived from the candidate's final
7
+ // tree at enumeration, l3/symbol-refs.ts). It is a SCORING-LAYER
8
+ // concern only — backends never print declarations (a project user compiles asmlift output
9
+ // against their own headers, where a second declaration would collide).
10
+ //
11
+ // LIVES IN CORE (browser-pure, no Node imports) because BOTH scorers prepend it: the cli's
12
+ // Node/objdiff path (which re-exports this module unchanged) and the webapp's wasm scorer
13
+ // (score-wasm.ts) — one renderer, so the two scoring worlds cannot drift.
14
+ //
15
+ // Fidelity rules (each empirically verified against agbcc — see the research doc):
16
+ // • struct decls are rebuilt from the map layout with explicit `u8 pad[]` gap fields — the
17
+ // padded synthesis is byte-identical to the real header declaration;
18
+ // • member/scalar SIGNEDNESS drives u8/s8/u16/s16/u32/s32 — an s8 field read is
19
+ // ldrb+lsl+asr where u8 is ldrb alone, so a guessed signedness is a wrong-bytes decl;
20
+ // • `volatile` is load-bearing (a non-volatile MMIO decl lets the compiler fold/reorder
21
+ // accesses), `const` is the ROM-table spelling;
22
+ // • code symbols get `void Name(void);` ONLY when value-referenced — call targets are never
23
+ // in `symbolRefs` (core excludes them: prototyping a called symbol is C89 poison);
24
+ // • nothing guesses, with TWO documented exceptions: a SHAPED symbol without the facts to
25
+ // declare faithfully is SKIPPED — the candidate then fails to compile LOUDLY and is
26
+ // dropped by rankBy. Exception one is the 4-byte signless-non-pointer cell (see
27
+ // ENUM_IS_SIGNED): spelled s32 on the C89 enum=int rule. For a true enum that is the header
28
+ // truth; the residual mis-spell class (a 4-byte nested-struct member word-read via a dot
29
+ // field) can only LOSE score — the target bytes derive from the truth decls, so a
30
+ // divergent compile can never false-match. Exception two is the NAME-ONLY data symbol
31
+ // (`extern u32 name;` — see the default case): required to reproduce symtab-only map
32
+ // rows outside project headers, justified by the same only-loses-score argument.
33
+ import { type StructFieldDecl, renderStructDecl } from './backend/cfamily';
34
+ import { T } from './ir/types';
35
+ import type { SymbolRef } from './l3/symbol-refs';
36
+ import {
37
+ ENUM_IS_SIGNED,
38
+ type SymbolInfo,
39
+ type SymbolStructField,
40
+ declaredFields,
41
+ pointeeFields,
42
+ symbolFieldType,
43
+ } from './symbols';
44
+
45
+ /** The u8/s8/u16/s16/u32/s32 spelling for a 1/2/4-byte cell, or null (no faithful narrow type). */
46
+ function intType(size: number, signed: boolean): string | null {
47
+ const base = size === 1 ? '8' : size === 2 ? '16' : size === 4 ? '32' : null;
48
+ return base === null ? null : `${signed ? 's' : 'u'}${base}`;
49
+ }
50
+
51
+ /** `volatile const ` qualifier prefix (either may be absent). */
52
+ function quals(info: SymbolInfo): string {
53
+ return `${info.volatile ? 'volatile ' : ''}${info.const ? 'const ' : ''}`;
54
+ }
55
+
56
+ /** One struct field's type, seated at its exact offset by the caller's pad discipline — THE shared
57
+ * map-field typing (symbols.ts `symbolFieldType`, which core's own legalization env also reads,
58
+ * so the declaration and the type the emitter reasoned against cannot drift). Member volatility
59
+ * is kept on the field decl here (`vu16 field;` — dropping it lets the compiler fold repeated
60
+ * reads), being a decl-only fact. */
61
+ const fieldType = symbolFieldType;
62
+
63
+ /** The padded `struct Tag { ... };` declaration for a layout: fields seated at exact offsets,
64
+ * gaps as explicit u8 pad arrays, rendered by THE shared struct renderer (core
65
+ * backend/cfamily.ts renderStructDecl — the same spelling the backend's recovered-struct
66
+ * decls use, so the two cannot drift). Returns null when the layout cannot be reproduced
67
+ * faithfully (an unsized member). */
68
+ function structDecl(tag: string, layout: SymbolStructField[] | undefined, size: number | undefined): string | null {
69
+ // THE shared spellability predicate (symbols.ts): which members exist, and whether the layout
70
+ // can be reproduced at all. Core's access rules gate on the SAME call, so a member this
71
+ // declaration omits — an unsizable layout declined whole, a union alias dropped for its first
72
+ // view — is a member no emitted expression can name.
73
+ const members = declaredFields(layout);
74
+ if (members === null) {
75
+ return null;
76
+ }
77
+ const fields: StructFieldDecl[] = [];
78
+ let cursor = 0;
79
+ let pad = 0;
80
+ for (const m of members) {
81
+ if (m.offset > cursor) {
82
+ // asmlift_-prefixed so a REAL member named pad_N (a decomp-header idiom) never collides
83
+ fields.push({ name: `asmlift_pad_${pad++}`, type: T.array(T.u(8), m.offset - cursor) });
84
+ }
85
+ fields.push({
86
+ name: m.name,
87
+ type: fieldType(m),
88
+ ...(m.volatile ? { volatile: true } : {}),
89
+ });
90
+ cursor = m.offset + m.size;
91
+ }
92
+ if (size !== undefined && size > cursor) {
93
+ // tail padding to the declared size
94
+ fields.push({ name: `asmlift_pad_${pad}`, type: T.array(T.u(8), size - cursor) });
95
+ }
96
+ return renderStructDecl(tag, fields);
97
+ }
98
+
99
+ /**
100
+ * Render the declaration block for a candidate's recorded symbol references. Deterministic
101
+ * (refs arrive name-sorted from core; struct decls dedupe by tag). The block is prepended by
102
+ * the candidate compiler AFTER the typedef prelude — it spells types as u8/s16/… — and only in
103
+ * the self-declared world (the probe in compile-command.ts arbitrates; in the headers world
104
+ * both prelude and declarations are dropped, headers own everything).
105
+ */
106
+ export function renderDeclarations(refs: SymbolRef[]): string {
107
+ const lines: string[] = [];
108
+ const declaredTags = new Set<string>();
109
+ for (const { name, info, access } of refs) {
110
+ // An address-cast macro declares itself: the header's own body, verbatim. It must NOT become
111
+ // an `extern` — that is the whole point of the fact (an extern emits a relocated pool word
112
+ // where the macro emits the numeric one the target shows).
113
+ if (info.macroBody !== undefined) {
114
+ lines.push(`#define ${name} ${info.macroBody}`);
115
+ continue;
116
+ }
117
+ if (info.kind === 'code') {
118
+ // value-referenced code symbol ((u32)Func): any prototype makes the name visible, and
119
+ // the address is arity-independent. Call targets never reach this module (core excludes
120
+ // them from symbolRefs — see collectSymbolRefs).
121
+ lines.push(`void ${name}(void);`);
122
+ continue;
123
+ }
124
+ switch (info.shape) {
125
+ case 'scalar': {
126
+ // Signedness default: absent + 4 bytes is the enum idiom (int ⇒ s32); absent + narrow
127
+ // has no honest spelling — skip (loud, see module note).
128
+ const t =
129
+ info.size !== undefined
130
+ ? intType(info.size, info.signed ?? (info.size === 4 ? ENUM_IS_SIGNED : false))
131
+ : null;
132
+ if (t !== null && (info.signed !== undefined || info.size === 4)) {
133
+ lines.push(`extern ${quals(info)}${t} ${name};`);
134
+ }
135
+ break;
136
+ }
137
+ case 'array': {
138
+ // Element type mirrors core's bare `gSym[i]` env typing exactly (elemSigned ?? false).
139
+ // A non-1/2/4 element width is never bare-indexed by core (only &gSym cast forms), so
140
+ // an unsized u8[] decl is codegen-identical for every spelling core emits.
141
+ const elem = info.elemSize !== undefined ? intType(info.elemSize, info.elemSigned ?? false) : null;
142
+ lines.push(`extern ${quals(info)}${elem ?? 'u8'} ${name}[];`);
143
+ break;
144
+ }
145
+ case 'struct': {
146
+ // With a layout: the padded struct decl + a typed extern (the `gSym.field` spelling
147
+ // compiles against it). Without one, every core spelling is &gSym-based (a struct
148
+ // global never spells bare), so an unsized u8[] extern is codegen-identical.
149
+ const tag = info.structName ?? `Asmlift_${name}`;
150
+ const decl = structDecl(tag, info.layout, info.size);
151
+ if (decl !== null) {
152
+ if (!declaredTags.has(tag)) {
153
+ declaredTags.add(tag);
154
+ lines.push(decl);
155
+ }
156
+ lines.push(`extern ${quals(info)}struct ${tag} ${name};`);
157
+ } else {
158
+ lines.push(`extern ${quals(info)}u8 ${name}[];`);
159
+ }
160
+ break;
161
+ }
162
+ case 'pointer': {
163
+ // With a POINTEE layout the emitter may spell an interior as `gPtr->member`, which only
164
+ // compiles against a pointer to that struct — so the pointee is declared here (the same
165
+ // padded synthesis a struct global gets) and the extern is typed. The declared pointee
166
+ // never changes bytes: the cell is 4 bytes whatever it addresses, and core's own lowering
167
+ // makes every arithmetic stride EXPLICIT (`(u8 *)gPtr + K` / `(u32)gPtr`), so no emitted
168
+ // expression is scaled by this type.
169
+ // Without one, pointee fidelity is unnecessary — load/store/compare of the cell are
170
+ // identical for any object-pointer type, and the output then never derefs through the
171
+ // decl's pointee.
172
+ // THE shared gate (symbols.ts pointeeFields): the typed extern is emitted on exactly
173
+ // the condition under which core may spell `gPtr->member`, so the two cannot disagree.
174
+ const tag = info.pointee?.structName;
175
+ const decl =
176
+ pointeeFields(info.pointee) !== null ? structDecl(tag!, info.pointee!.layout, info.pointee!.size) : null;
177
+ if (decl !== null && !declaredTags.has(tag!)) {
178
+ declaredTags.add(tag!);
179
+ lines.push(decl);
180
+ }
181
+ // The POINTEE's own qualifiers bind to the pointed-at type (`volatile struct S *g`); the
182
+ // cell's bind to the VARIABLE (`struct S *volatile g`). They are independent declarations
183
+ // of two different objects, and the synthesis reproduces each on its own side of the `*`.
184
+ const pointeeQuals = `${info.pointee?.volatile ? 'volatile ' : ''}${info.pointee?.const ? 'const ' : ''}`;
185
+ const pointeeType = decl !== null ? `${pointeeQuals}struct ${tag} *` : `${pointeeQuals}void *`;
186
+ lines.push(`extern ${pointeeType}${info.volatile ? 'volatile ' : ''}${info.const ? 'const ' : ''}${name};`);
187
+ break;
188
+ }
189
+ default: {
190
+ // Name-only (no sidecar shape) — the second documented exception (see the module note;
191
+ // the first is the 4-byte signless enum cell). Skipping here was the original rule, but
192
+ // it made every named-spelling row of a symtab-only map project (marioparty3: names
193
+ // with no DWARF shapes) unreproducible in the self-declared world — the benchmark
194
+ // compiled those candidates inside the project headers, which declare the symbol.
195
+ // The width authority is the candidate's OWN IR (ref.access, rank.ts
196
+ // bareGlobalAccessFacts): a bare `name = v` / `x = name` compiles to the access the
197
+ // tree performed only under a decl of that exact width (`extern u16 g;` is `sh` where
198
+ // a guessed u32 is `sw`). Without a bare off-0 access fact, every core spelling goes
199
+ // through `&name` casts, where any object decl is address-identical — u32 is the
200
+ // fallback cell. A divergent decl can only LOSE score — the target bytes derive from
201
+ // the truth decls, so a mis-declared compile can never false-match (same argument as
202
+ // enumIsSigned).
203
+ const t = access ? intType(access.width, access.signed) : null;
204
+ lines.push(`extern ${quals(info)}${t ?? 'u32'} ${name};`);
205
+ break;
206
+ }
207
+ }
208
+ }
209
+ return lines.length ? lines.join('\n') + '\n' : '';
210
+ }
211
+
212
+ /** The object-like `#define`s out of a rendered declaration block.
213
+ *
214
+ * Address-cast macro defines are the one part of a synthesized block that must survive into the
215
+ * HEADERS world too. Everything else there is owned by the injected headers (a duplicate typedef
216
+ * or struct definition is a C89 hard error), but a duplicate `#define` with an identical body is
217
+ * legal — and a PREPROCESSED project context has no macros left at all, so dropping these turns a
218
+ * macro-named candidate into an `undeclared identifier` rather than a spelling choice. */
219
+ export function macroDefinesOf(declarations: string | undefined): string {
220
+ if (!declarations) {
221
+ return '';
222
+ }
223
+ const lines = declarations.split('\n').filter((l) => l.startsWith('#define '));
224
+ return lines.length ? lines.join('\n') + '\n' : '';
225
+ }
package/src/detect.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  // asmlift — small pure helpers over raw asm TEXT (no parsing): shared by the CLI and the
2
2
  // web playground, which both need a function name before they can call `decompile`.
3
3
 
4
- /** Best-effort function-name detection: the objdump symbol header, else the `.globl` name,
5
- * else the first label. Returns undefined when the asm names nothing (caller asks the user). */
4
+ /** Best-effort function-name detection: the objdump symbol header, else Splat's `glabel` marker,
5
+ * else the `.globl` name, else the first label. Returns undefined when the asm names nothing
6
+ * (caller asks the user). Returns the FIRST function in multi-function input, matching how the
7
+ * objdump header case auto-selects. */
6
8
  export function detectName(asm: string): string | undefined {
7
9
  return (
8
10
  asm.match(/^[0-9a-f]+ <([\w.$]+)>:/m)?.[1] ??
11
+ asm.match(/^\s*glabel\s+([\w.$]+)/m)?.[1] ??
9
12
  asm.match(/^\s*\.globl\s+([\w.$]+)/m)?.[1] ??
10
13
  asm.match(/^([A-Za-z_]\w*):/m)?.[1]
11
14
  );
@@ -9,12 +9,14 @@
9
9
  // hand-written instructions) stays "unknown" and flows through to the frontend — the
10
10
  // decode-level loud-fail nets still own that case.
11
11
  import { FrontendUnsupportedError } from './errors';
12
+ import { isSplatMips } from './splat';
12
13
 
13
- export type AsmTextFormat = 'objdump' | 'gnu-as';
14
+ export type AsmTextFormat = 'objdump' | 'gnu-as' | 'splat';
14
15
 
15
16
  const FORMAT_LABEL: Record<AsmTextFormat, string> = {
16
17
  objdump: 'objdump disassembly (`objdump -d --no-show-raw-insn` output)',
17
18
  'gnu-as': 'GNU-as assembly text (compiler-emitted `.s`)',
19
+ splat: 'Splat-disassembled MIPS (`glabel` + `/* rom vram bytes */` `.s`)',
18
20
  };
19
21
 
20
22
  // objdump output: `ADDR <sym>:` section headers, address-prefixed instruction lines, or the
@@ -23,8 +25,13 @@ const OBJDUMP_SIGNAL = /^[0-9a-f]{2,} <[^>]+>:|^\s+[0-9a-f]+:\t|file format /im;
23
25
  const GNU_AS_SIGNAL =
24
26
  /^\s*\.(text|code|align|globl|global|thumb_func|section|syntax|arch|cpu|set|ent|type|size|file)\b/im;
25
27
 
26
- /** Classify assembly TEXT by positive signals; "unknown" when neither (or both) match. */
28
+ /** Classify assembly TEXT by positive signals; "unknown" when neither (or both) match. Splat is
29
+ * checked FIRST: its files also carry GNU-as directives (`.section`/`.align`), so a bare gnu-as
30
+ * match would mislabel them — the Splat-specific `glabel`/instruction-comment signal is decisive. */
27
31
  export function classifyAsmText(text: string): AsmTextFormat | 'unknown' {
32
+ if (isSplatMips(text)) {
33
+ return 'splat';
34
+ }
28
35
  const objdump = OBJDUMP_SIGNAL.test(text);
29
36
  const gnuAs = GNU_AS_SIGNAL.test(text);
30
37
  if (objdump === gnuAs) {
@@ -42,6 +49,7 @@ export function assertInputFormat(frontendId: string, expected: AsmTextFormat, a
42
49
  }
43
50
  throw new FrontendUnsupportedError(
44
51
  `cannot lift: input looks like ${FORMAT_LABEL[got]}, but the '${frontendId}' frontend reads ` +
45
- `${FORMAT_LABEL[expected]} — MIPS/PPC targets take objdump output; the ARM/agbcc target takes agbcc .s text`,
52
+ `${FORMAT_LABEL[expected]} — the ARM/agbcc target takes agbcc .s text; MIPS/PPC take objdump output ` +
53
+ `(the MIPS frontend also reads Splat .s)`,
46
54
  );
47
55
  }
@@ -4,6 +4,7 @@
4
4
  // for target→frontend dispatch.
5
5
  import type { Fn } from '../ir/core';
6
6
  import type { Prototypes } from '../proto';
7
+ import type { SymbolMap } from '../symbols';
7
8
  import type { TargetDescription } from '../target';
8
9
  import type { AsmData } from './asmdata';
9
10
  import type { AsmTextFormat } from './format';
@@ -17,6 +18,15 @@ export interface Frontend {
17
18
  /** decode one function's assembly into an L1 Fn. `prototypes` supplies callee arities
18
19
  * (and any other header facts the frontend needs); an empty map is valid. `asmData` is the
19
20
  * OPTIONAL Regime-B side-table (data-section jump tables + relocations); absent ⇒ a
20
- * dense-switch dispatch declines/loud-fails. */
21
- lift(name: string, asm: string, target: TargetDescription, prototypes: Prototypes, asmData?: AsmData): Fn;
21
+ * dense-switch dispatch declines/loud-fails. `symbols` is the OPTIONAL address→symbol map
22
+ * (symbols.ts); today only the Thumb frontend consumes it (numeric-pool promotion) — the
23
+ * MIPS/PPC objdump dialect already carries symbol names in the asm text. */
24
+ lift(
25
+ name: string,
26
+ asm: string,
27
+ target: TargetDescription,
28
+ prototypes: Prototypes,
29
+ asmData?: AsmData,
30
+ symbols?: SymbolMap,
31
+ ): Fn;
22
32
  }