@asmlift/core 0.1.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/LICENSE +21 -0
- package/README.md +148 -0
- package/package.json +14 -0
- package/src/backend/c.ts +20 -0
- package/src/backend/cfamily.ts +352 -0
- package/src/backend/cpp.ts +145 -0
- package/src/backend/pascal.ts +279 -0
- package/src/contracts.ts +131 -0
- package/src/detect.ts +12 -0
- package/src/frontend/asmdata.ts +170 -0
- package/src/frontend/disasm.ts +102 -0
- package/src/frontend/emit.ts +57 -0
- package/src/frontend/errors.ts +14 -0
- package/src/frontend/format.ts +47 -0
- package/src/frontend/frontend.ts +22 -0
- package/src/frontend/mips.ts +875 -0
- package/src/frontend/opaque.ts +82 -0
- package/src/frontend/ppc.ts +990 -0
- package/src/frontend/registry.ts +34 -0
- package/src/frontend/ssa.ts +214 -0
- package/src/frontend/thumb.ts +1419 -0
- package/src/ir/core.ts +104 -0
- package/src/ir/opcodes.ts +143 -0
- package/src/ir/parse.ts +221 -0
- package/src/ir/print.ts +77 -0
- package/src/ir/types.ts +106 -0
- package/src/ir/verify.ts +221 -0
- package/src/l3/ast.ts +301 -0
- package/src/l3/basecse.ts +218 -0
- package/src/l3/dce.ts +256 -0
- package/src/l3/regspell.ts +331 -0
- package/src/l3/reindex.ts +447 -0
- package/src/l3/typing.ts +145 -0
- package/src/mangle.ts +135 -0
- package/src/pattern/engine.ts +392 -0
- package/src/pipeline.ts +272 -0
- package/src/proto.ts +42 -0
- package/src/raise/arrays.ts +84 -0
- package/src/raise/const.ts +52 -0
- package/src/raise/errors.ts +10 -0
- package/src/raise/magicdiv.ts +386 -0
- package/src/raise/pre-recovery.ts +71 -0
- package/src/raise/recover.ts +215 -0
- package/src/raise/retsink.ts +72 -0
- package/src/raise/shortcircuit.ts +207 -0
- package/src/raise/softdiv.ts +62 -0
- package/src/raise/struct-arrays.ts +257 -0
- package/src/raise/structs.ts +223 -0
- package/src/rank.ts +208 -0
- package/src/structure/analysis.ts +410 -0
- package/src/structure/hazards.ts +142 -0
- package/src/structure/loops.ts +169 -0
- package/src/structure/structure.ts +1726 -0
- package/src/structure/switch-recover.ts +410 -0
- package/src/target.ts +140 -0
- package/src/trace.ts +233 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bruno Macabeus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# @asmlift/core
|
|
2
|
+
|
|
3
|
+
The asmlift decompile pipeline as a pure library: one function's **assembly text in, C / C++ /
|
|
4
|
+
Pascal source out**, built to recompile **byte-identical** to the original object — the
|
|
5
|
+
generator role m2c plays in the console-decompilation workflow. Three ISA frontends
|
|
6
|
+
(ARMv4T/Thumb, MIPS, PowerPC), three language backends over one neutral AST; C and Pascal are
|
|
7
|
+
drop-in backends, C++ is a deliberately scoped per-function factory (see the `backend` option).
|
|
8
|
+
|
|
9
|
+
The package is **browser-pure by enforced contract**: zero dependencies, no Node or DOM APIs —
|
|
10
|
+
it bundles unchanged into the playground webapp (`apps/web` in the repo). The contract is gated
|
|
11
|
+
twice: `test/browser-safe.test.ts` (import scanning) and a dedicated `tsc -p packages/core`
|
|
12
|
+
project with `types: []`.
|
|
13
|
+
|
|
14
|
+
The operative invariant everywhere: **loud decline > silent miscompile**. Where the pipeline
|
|
15
|
+
cannot be byte-faithful it throws a typed error (strict mode) or emits an `ASMLIFT_ERROR`-marked
|
|
16
|
+
stub (`onGap: "annotate"`) — never plausible wrong code.
|
|
17
|
+
|
|
18
|
+
> Not yet published to npm. Inside this repo it resolves via the pnpm workspace.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { decompile } from '@asmlift/core/pipeline';
|
|
24
|
+
import { MIPS_IDO } from '@asmlift/core/target';
|
|
25
|
+
|
|
26
|
+
const asm = `...output of: mips-linux-gnu-objdump -d --no-show-raw-insn fn.o ...`;
|
|
27
|
+
const { source, diagnostics } = decompile('my_func', asm, MIPS_IDO);
|
|
28
|
+
console.log(source); // s32 my_func(s32 a0) { ... }
|
|
29
|
+
```
|
|
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.)
|
|
40
|
+
|
|
41
|
+
### `decompile(name, asm, target, opts?)`
|
|
42
|
+
|
|
43
|
+
| Option | Meaning |
|
|
44
|
+
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
45
|
+
| `backend` | `cBackend` (default) or `pascalBackend` — values from `@asmlift/core/backend/*`. C++ is `cppBackend(spec)`, a per-function factory: it takes a `CppFnSpec` (class/method name, explicit param types, class field layouts — what a project's headers supply) and covers free and non-virtual member functions with word-sized fields; virtual dispatch, references, ctors/dtors decline |
|
|
46
|
+
| `patterns` | Idiom rewrite patterns. Omitted = `DEFAULT_IDIOM_PATTERNS` (each gated per compiler); `[]` = none |
|
|
47
|
+
| `prototypes` | Callee arities + void-ness, as a real project takes them from headers — drives call-argument recovery |
|
|
48
|
+
| `asmData` | Optional `objdump -s -r -t` side-table; required to recover MIPS/PPC jump-table switches |
|
|
49
|
+
| `onGap` | `"strict"` (default): throw on any gap. `"annotate"`: emit best-effort source with `ASMLIFT_ERROR` markers; every gap is also returned in the structured `diagnostics` array (empty ⇔ gap-free) |
|
|
50
|
+
|
|
51
|
+
Targets: `ARMV4T_AGBCC`, `MIPS_IDO`, `MIPS_GCC`, `PPC_MWCC` (`@asmlift/core/target`).
|
|
52
|
+
|
|
53
|
+
### Other entry points
|
|
54
|
+
|
|
55
|
+
- `decompileTraced` (`@asmlift/core/trace`) — same tower, returns a `TraceReport`: per-stage IR
|
|
56
|
+
dumps + pattern before/after events. This is what the playground's Pipeline tab renders.
|
|
57
|
+
- `detectName` (`@asmlift/core/detect`) — best-effort symbol detection for pasted asm.
|
|
58
|
+
- Typed decline errors: `FrontendUnsupportedError`, `RaiseUnsupportedError`, `StructureError`,
|
|
59
|
+
`ContractError`, `VerifyError` — a principled decline is distinguishable from a bug.
|
|
60
|
+
|
|
61
|
+
Everything under `src/` is importable as `@asmlift/core/<path>` (e.g.
|
|
62
|
+
`@asmlift/core/pattern/engine`); `@asmlift/core` alone resolves to the pipeline.
|
|
63
|
+
|
|
64
|
+
## Architecture
|
|
65
|
+
|
|
66
|
+
Three ISA frontends (ARMv4T/Thumb, MIPS, PowerPC), four compilers (agbcc, IDO, KMC GCC,
|
|
67
|
+
CodeWarrior), three language backends over one neutral AST — all scored across the package seam
|
|
68
|
+
by [`@asmlift/cli`](../cli/README.md) with the community `objdiff` engine (in-process, pinned
|
|
69
|
+
`objdiff-wasm`; asmlift never hand-rolls a diff).
|
|
70
|
+
|
|
71
|
+
### The pipeline (`decompile()` in `pipeline.ts`)
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
asm ─▶ lift ─▶ idiom fold ─▶ pre-recovery ─▶ type recovery ─▶ retsink ─▶ structure ─▶ emit
|
|
75
|
+
(L1) (patterns) (recognizers) (L2) (L3) (C/C++/Pascal)
|
|
76
|
+
└────▶ ranked candidates ─▶ objdiff ─▶ score (@asmlift/cli rank.ts)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The stage sequence is ONE shared spine (`applyIdiomPatterns` / `raiseRecovered` /
|
|
80
|
+
`structureChecked`, exported by `pipeline.ts`) that `decompile()`, `decompileTraced` (trace.ts),
|
|
81
|
+
and @asmlift/cli's `decompileRanked`/`decompileWithReport` all run — per-caller differences are
|
|
82
|
+
injected via hooks, never copied. `verify()` runs after every IR-mutating pass;
|
|
83
|
+
`assertTypesRecovered` / `assertResolved` (contracts.ts) gate the L2/L3 boundaries.
|
|
84
|
+
|
|
85
|
+
### Modules
|
|
86
|
+
|
|
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 |
|
|
101
|
+
|
|
102
|
+
Scoring and ranking live across the package seam in [`@asmlift/cli`](../cli/README.md):
|
|
103
|
+
`score.ts` + `objdiff.ts` (toolchain compiles → in-process pinned `objdiff-wasm`, fail-closed)
|
|
104
|
+
and `rank.ts` (ranked type candidates re-ranked by the differ).
|
|
105
|
+
|
|
106
|
+
### Honest coverage gaps
|
|
107
|
+
|
|
108
|
+
Recovered today: straight-line, if/else diamonds, natural loops (`while` / `do-while` / `for`,
|
|
109
|
+
properly nested, in-body `break`/early-`return`), comparison-tree and jump-table switches,
|
|
110
|
+
direct calls, constant-offset and variable-index memory (`*p`, `p[n]`, `a[i]`, struct fields),
|
|
111
|
+
magic-number and soft division, short-circuit booleans, width casts. Still DECLINED (loud, never
|
|
112
|
+
wrong code): **local stack frames** (address-taken locals / sp-as-data / live spills),
|
|
113
|
+
**cross-block condition flags** on PPC (a `cmpw` whose branch lands in another block — the
|
|
114
|
+
capability gap behind the mwcc switch stubs), computed tail calls, PIC/`gp`/SDA global access,
|
|
115
|
+
switch fall-through, multi-latch/irreducible loops, floats, and 64-bit memory ops. Prototypes
|
|
116
|
+
(callee arities, void-ness) come from a caller-supplied map, as a real project takes them from
|
|
117
|
+
headers.
|
|
118
|
+
|
|
119
|
+
## Tests
|
|
120
|
+
|
|
121
|
+
The toolchain-free half of the test story lives in `test/`: every suite there runs with no
|
|
122
|
+
compiler installed, on any machine and in hosted CI. The toolchain-bound matching suites live in
|
|
123
|
+
[`@asmlift/cli`](../cli/CONTRIBUTION.md#tests) (Docker-gated suites skip WITH a warning — see
|
|
124
|
+
`../cli/test/matching/docker-gate.ts`). The CI gate is `pnpm run test:offline`, whose directory
|
|
125
|
+
list in the root package.json is SELF-VERIFYING — `offline-list.test.ts` derives the offline set from each
|
|
126
|
+
suite's imports and fails on drift.
|
|
127
|
+
|
|
128
|
+
Landmarks (not exhaustive — suites are named for what they pin):
|
|
129
|
+
|
|
130
|
+
- `roundtrip` / `verify` / `determinism` / `pattern` — the IR substrate.
|
|
131
|
+
- `m1`–`m5` (in `../cli/test/matching/`) — one milestone thesis each.
|
|
132
|
+
- `../cli/test/matching/regression.test.ts` — data-driven over `matching/fixtures.ts`; the guard
|
|
133
|
+
that keeps every already-matching function matching. How to add a fixture — and when a
|
|
134
|
+
matching test is the right tool at all, vs a benchmark row — is under
|
|
135
|
+
[`@asmlift/cli` › Tests](../cli/CONTRIBUTION.md#tests).
|
|
136
|
+
- `contract-invariant` / `contracts` — the loud-fail contract, mutation-proven.
|
|
137
|
+
- `structure-guard` / `structure-soundness` / `audit-regression` — the adversarial-audit repro
|
|
138
|
+
locks.
|
|
139
|
+
|
|
140
|
+
**`test/corpus/` is load-bearing beyond this suite.** The committed disassembly fixtures in
|
|
141
|
+
`test/corpus/` are ALSO imported (via Vite `?raw`) by the playground's example gallery —
|
|
142
|
+
`apps/web/src/pages/playground/examples.ts`. Renaming or pruning a corpus file breaks the `apps/web` build
|
|
143
|
+
(CI-gated on every push), so treat these files as a public fixture surface, not suite-private
|
|
144
|
+
scratch.
|
|
145
|
+
|
|
146
|
+
## More
|
|
147
|
+
|
|
148
|
+
- the root [`README.md`](../../README.md) — monorepo layout, benchmark, webapps.
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@asmlift/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Match decompile an assembly function to C or Pascal",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/pipeline.ts",
|
|
9
|
+
"./*": "./src/*.ts"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src"
|
|
13
|
+
]
|
|
14
|
+
}
|
package/src/backend/c.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// asmlift — the C language backend. Consumes the language-NEUTRAL L3 AST and owns ALL C spelling.
|
|
2
|
+
// Text is produced ONLY by a precedence-aware printer over the typed AST — never by string-
|
|
3
|
+
// concatenating over IR ops.
|
|
4
|
+
//
|
|
5
|
+
// Expression/statement/type spelling lives in backend/cfamily.ts, shared with the C++ backend;
|
|
6
|
+
// this backend owns only the C SIGNATURE line. The Pascal backend implements the same
|
|
7
|
+
// LanguageBackend interface over the same L3 with its OWN spelling.
|
|
8
|
+
import { LanguageBackend, SFn } from '../l3/ast';
|
|
9
|
+
import { cComment, cType, emitCFamily } from './cfamily';
|
|
10
|
+
|
|
11
|
+
export { cComment }; // re-export: the shared spelling lives in cfamily.ts
|
|
12
|
+
|
|
13
|
+
export const cBackend: LanguageBackend = {
|
|
14
|
+
id: 'c',
|
|
15
|
+
emit(fn: SFn): string {
|
|
16
|
+
const params = fn.params.map((p) => `${cType(p.type)} ${p.name}`).join(', ') || 'void';
|
|
17
|
+
return emitCFamily(`${cType(fn.retType)} ${fn.name}(${params})`, fn);
|
|
18
|
+
},
|
|
19
|
+
comment: cComment,
|
|
20
|
+
};
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
// asmlift — the SHARED C-family spelling. The L3 AST is language-neutral for the C-family:
|
|
2
|
+
// expression/statement nodes are neutral, and TYPE SPELLING is a backend decision (`cType`
|
|
3
|
+
// here, `pasType` in the Pascal backend). The empirical fact grounding the sharing: a
|
|
4
|
+
// CodeWarrior member function's BODY is byte-identical to the same C with `this` as an
|
|
5
|
+
// explicit pointer — so the C++ backend reuses this body spelling VERBATIM and owns only its
|
|
6
|
+
// DIVERGENT surface (the mangled/scoped signature, references, `this`). c.ts and cpp.ts
|
|
7
|
+
// consume exactly the exported seam: `emitCFamily` + `cType` + `LeafHook`.
|
|
8
|
+
import { IrType, T, scalarTypeForAccess, typeToString } from '../ir/types';
|
|
9
|
+
import { BinOp, Expr, SFn, Stmt, dotBase } from '../l3/ast';
|
|
10
|
+
import { type VarTypes, declaredTypes, derefStrideOk, exprCType } from '../l3/typing';
|
|
11
|
+
|
|
12
|
+
// C operator precedence (lower binds tighter). Used to emit MINIMAL parentheses. Shared: C++ has
|
|
13
|
+
// the same precedence for these operators.
|
|
14
|
+
const PREC: Record<BinOp, number> = {
|
|
15
|
+
'*': 3,
|
|
16
|
+
'/': 3,
|
|
17
|
+
'%': 3,
|
|
18
|
+
'+': 4,
|
|
19
|
+
'-': 4,
|
|
20
|
+
'<<': 5,
|
|
21
|
+
'>>': 5,
|
|
22
|
+
'<': 6,
|
|
23
|
+
'<=': 6,
|
|
24
|
+
'>': 6,
|
|
25
|
+
'>=': 6,
|
|
26
|
+
'==': 7,
|
|
27
|
+
'!=': 7,
|
|
28
|
+
'&': 8,
|
|
29
|
+
'^': 9,
|
|
30
|
+
'|': 10,
|
|
31
|
+
'&&': 11,
|
|
32
|
+
'||': 12,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Spell a recovered type in the decomp C-family typedef vocabulary (`s32`/`u32`/`u8`/`T *`). */
|
|
36
|
+
export function cType(t: IrType): string {
|
|
37
|
+
if (t.kind === 'ptr') {
|
|
38
|
+
return `${cType(t.to)} *`;
|
|
39
|
+
}
|
|
40
|
+
if (t.kind === 'struct') {
|
|
41
|
+
return `struct ${t.name}`;
|
|
42
|
+
}
|
|
43
|
+
if (t.kind === 'array') {
|
|
44
|
+
return `${cType(t.elem)}[${t.count}]`;
|
|
45
|
+
} // ill-formed as a prefix; use cDeclare
|
|
46
|
+
return typeToString(t); // s32 / u32 / u8 / unk32 (treated as s32 upstream)
|
|
47
|
+
}
|
|
48
|
+
|
|
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`. */
|
|
51
|
+
function cDeclare(t: IrType, name: string): string {
|
|
52
|
+
if (t.kind === 'array') {
|
|
53
|
+
return `${cType(t.elem)} ${name}[${t.count}]`;
|
|
54
|
+
}
|
|
55
|
+
return `${cType(t)} ${name}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// A LEAF hook lets a C-family backend override how a `var` or `index` node spells WITHOUT
|
|
59
|
+
// re-implementing precedence, parenthesization, or statement structure. It returns the
|
|
60
|
+
// replacement text, or null to fall through to the default C spelling — how the C++ backend
|
|
61
|
+
// renders member access (`this->x` as bare `x`, `o->x`) over the exact same printer the C
|
|
62
|
+
// backend uses. The default (no hook) is byte-identical C.
|
|
63
|
+
export type LeafHook = (e: Expr, rec: (e: Expr, p: number) => string) => string | null;
|
|
64
|
+
|
|
65
|
+
function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook): string {
|
|
66
|
+
const rec = (x: Expr, p: number) => printExpr(x, p, vt, leaf);
|
|
67
|
+
if (leaf) {
|
|
68
|
+
const s = leaf(e, rec);
|
|
69
|
+
if (s !== null) {
|
|
70
|
+
return s;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// C-FAMILY LEGALIZATION (owned here, per the width-carrying `index` node contract in l3/ast.ts):
|
|
74
|
+
// a deref whose base does not render as a pointer/array STRIDING the access width is spelled
|
|
75
|
+
// through the honest reinterpret cast at that width — the machine semantics of the access.
|
|
76
|
+
// Materialized as a synthetic cast node so the spelling (text, precedence, parens) is exactly
|
|
77
|
+
// that of a tree-level cast.
|
|
78
|
+
const legalized = (ix: Extract<Expr, { k: 'index' }>): Expr =>
|
|
79
|
+
derefStrideOk(exprCType(ix.base, vt), ix.width)
|
|
80
|
+
? ix.base
|
|
81
|
+
: { k: 'cast', to: T.ptr(scalarTypeForAccess(ix.width, ix.signed)), e: ix.base };
|
|
82
|
+
switch (e.k) {
|
|
83
|
+
case 'var':
|
|
84
|
+
return e.name;
|
|
85
|
+
case 'const':
|
|
86
|
+
return String(e.value);
|
|
87
|
+
case 'addr': {
|
|
88
|
+
// `&gSym` — the address of a named global. A prefix operator; parenthesizes under a POSTFIX
|
|
89
|
+
// parent like the other prefix forms.
|
|
90
|
+
const g = `&${e.name}`;
|
|
91
|
+
return parentPrec < 2 ? `(${g})` : g;
|
|
92
|
+
}
|
|
93
|
+
case 'call':
|
|
94
|
+
return `${e.fn}(${e.args.map((a) => rec(a, 99)).join(', ')})`;
|
|
95
|
+
case 'index': {
|
|
96
|
+
// `*base` for the zero offset (a PREFIX operator, so a prefix-shaped base like a cast needs
|
|
97
|
+
// no parens: `*(u8 *)p` — but the whole form must self-parenthesize under a POSTFIX parent:
|
|
98
|
+
// `(*p)[1]`, never `*p[1]` which C groups as `*(p[1])`), `base[idx]` otherwise (POSTFIX —
|
|
99
|
+
// binds tighter than any prefix operator, so a cast/unary/deref base is printed at prec 1
|
|
100
|
+
// and parenthesizes itself: `((u8 *)p)[1]`). The postfix form needs no outer parentheses.
|
|
101
|
+
const base = legalized(e);
|
|
102
|
+
if (e.idx.k === 'const' && e.idx.value === 0) {
|
|
103
|
+
const s = `*${rec(base, 2)}`;
|
|
104
|
+
return parentPrec < 2 ? `(${s})` : s;
|
|
105
|
+
}
|
|
106
|
+
return `${rec(base, 1)}[${rec(e.idx, 99)}]`;
|
|
107
|
+
}
|
|
108
|
+
case 'field': {
|
|
109
|
+
// `base->name`, or `base.name` when the base is itself an array element (a struct VALUE, not
|
|
110
|
+
// a pointer) — an array-of-struct access `arr[i].field` (fieldSpellsDot, the shared rule).
|
|
111
|
+
// Postfix (base at prec 1, exactly like `[]` above), no outer parens.
|
|
112
|
+
//
|
|
113
|
+
// The dot form's base index node is printed WITHOUT legalization: a struct-array element's
|
|
114
|
+
// base is legalized at the TREE level (arrayAccess casts to the recovered struct pointer;
|
|
115
|
+
// carrying the struct identity ON the node — like width — is the named follow-up), and the
|
|
116
|
+
// width-cast legalization above must not double-wrap it. The leaf hook still sees the base
|
|
117
|
+
// first (the C++ member-access rewrite).
|
|
118
|
+
const ix = dotBase(e);
|
|
119
|
+
if (ix) {
|
|
120
|
+
const hooked = leaf?.(ix, rec);
|
|
121
|
+
const baseTxt = hooked ?? `${rec(ix.base, 1)}[${rec(ix.idx, 99)}]`;
|
|
122
|
+
return `${baseTxt}.${e.name}`;
|
|
123
|
+
}
|
|
124
|
+
return `${rec(e.base, 1)}->${e.name}`;
|
|
125
|
+
}
|
|
126
|
+
case 'un': {
|
|
127
|
+
// A prefix operator: parenthesize under a POSTFIX parent (`(-a)[1]`), and parenthesize a
|
|
128
|
+
// same-op nested `-` (`-(-a)`, never `--a` — C lexes that as predecrement).
|
|
129
|
+
const inner = rec(e.e, 2);
|
|
130
|
+
const s = `${e.op}${e.op === '-' && inner.startsWith('-') ? `(${inner})` : inner}`;
|
|
131
|
+
return parentPrec < 2 ? `(${s})` : s;
|
|
132
|
+
}
|
|
133
|
+
// A gap marker spells as a call to the UNDEFINED macro ASMLIFT_ERROR("reason", args…) — the
|
|
134
|
+
// m2c M2C_ERROR discipline: the function is complete and readable, but a compile fails until
|
|
135
|
+
// the user consciously defines the macro. Postfix/call-shaped, so no outer parens needed.
|
|
136
|
+
case 'marker':
|
|
137
|
+
return `ASMLIFT_ERROR(${[JSON.stringify(e.reason), ...e.args.map((a) => rec(a, 99))].join(', ')})`;
|
|
138
|
+
// A C cast binds as a prefix operator (like unary), tighter than any binary op: the operand is
|
|
139
|
+
// printed at prec 2, so `(u8)a & 1` needs no parens but `(u8)(a & 1)` gets them from the inner
|
|
140
|
+
// op. Under a POSTFIX parent ([]/->) the cast itself must parenthesize — `((struct S *)p)->f`,
|
|
141
|
+
// NOT `(struct S *)p->f` (which C parses as a cast OF the member access).
|
|
142
|
+
case 'cast': {
|
|
143
|
+
const s = `(${cType(e.to)})${rec(e.e, 2)}`;
|
|
144
|
+
return parentPrec < 2 ? `(${s})` : s;
|
|
145
|
+
}
|
|
146
|
+
case 'bin': {
|
|
147
|
+
const p = PREC[e.op];
|
|
148
|
+
const s = `${rec(e.l, p)} ${e.op} ${rec(e.r, p - 1)}`;
|
|
149
|
+
return p > parentPrec ? `(${s})` : s;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function printStmt(s: Stmt, indent: string, vt: VarTypes, leaf?: LeafHook): string[] {
|
|
155
|
+
const pe = (e: Expr, p: number) => printExpr(e, p, vt, leaf);
|
|
156
|
+
switch (s.k) {
|
|
157
|
+
case 'assign':
|
|
158
|
+
return [`${indent}${s.name} = ${pe(s.value, 99)};`];
|
|
159
|
+
case 'store':
|
|
160
|
+
// The lvalue is a full Expr (`index` or `field`), so the leaf hook spells a member write
|
|
161
|
+
// (`this->x = …`) exactly as it spells a member read.
|
|
162
|
+
return [`${indent}${pe(s.lval, 2)} = ${pe(s.value, 99)};`];
|
|
163
|
+
case 'exprstmt':
|
|
164
|
+
return [`${indent}${pe(s.value, 99)};`];
|
|
165
|
+
case 'return':
|
|
166
|
+
return [`${indent}return${s.value ? ' ' + pe(s.value, 99) : ''};`];
|
|
167
|
+
case 'if': {
|
|
168
|
+
const cond = pe(s.cond, 99);
|
|
169
|
+
if (s.then.length === 1 && s.else.length === 0 && s.then[0].k !== 'if') {
|
|
170
|
+
// Inline `if (c) stmt;` ONLY when the statement prints as a single line — a multi-line
|
|
171
|
+
// then (a do-while/while/for/switch) taking just `[0]` here silently truncated the body.
|
|
172
|
+
const inner = printStmt(s.then[0], '', vt, leaf);
|
|
173
|
+
if (inner.length === 1) {
|
|
174
|
+
return [`${indent}if (${cond}) ${inner[0].trim()}`];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const out = [`${indent}if (${cond}) {`];
|
|
178
|
+
for (const t of s.then) {
|
|
179
|
+
out.push(...printStmt(t, indent + ' ', vt, leaf));
|
|
180
|
+
}
|
|
181
|
+
if (s.else.length) {
|
|
182
|
+
out.push(`${indent}} else {`);
|
|
183
|
+
for (const e of s.else) {
|
|
184
|
+
out.push(...printStmt(e, indent + ' ', vt, leaf));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
out.push(`${indent}}`);
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
case 'while': {
|
|
191
|
+
const out = [`${indent}while (${pe(s.cond, 99)}) {`];
|
|
192
|
+
for (const t of s.body) {
|
|
193
|
+
out.push(...printStmt(t, indent + ' ', vt, leaf));
|
|
194
|
+
}
|
|
195
|
+
out.push(`${indent}}`);
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
case 'dowhile': {
|
|
199
|
+
const out = [`${indent}do {`];
|
|
200
|
+
for (const t of s.body) {
|
|
201
|
+
out.push(...printStmt(t, indent + ' ', vt, leaf));
|
|
202
|
+
}
|
|
203
|
+
out.push(`${indent}} while (${pe(s.cond, 99)});`);
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
case 'for': {
|
|
207
|
+
// `for (init; cond; inc) { body }`. PRECONDITION (guaranteed by the sole producer, structure.ts
|
|
208
|
+
// `recognizeForLoops`): init/inc are each a SINGLE-LINE `assign` statement. `clause` renders one
|
|
209
|
+
// and strips its trailing `;` so it sits inside the header (`i = 0; c; i = i + 1`). A multi-line
|
|
210
|
+
// statement (an `if`/nested loop) would render mangled — but the recognizer never builds one here.
|
|
211
|
+
const clause = (st: Stmt) => printStmt(st, '', vt, leaf).join(' ').replace(/;\s*$/, '').trim();
|
|
212
|
+
const out = [`${indent}for (${clause(s.init)}; ${pe(s.cond, 99)}; ${clause(s.inc)}) {`];
|
|
213
|
+
for (const t of s.body) {
|
|
214
|
+
out.push(...printStmt(t, indent + ' ', vt, leaf));
|
|
215
|
+
}
|
|
216
|
+
out.push(`${indent}}`);
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
case 'break':
|
|
220
|
+
return [`${indent}break;`];
|
|
221
|
+
case 'continue':
|
|
222
|
+
return [`${indent}continue;`];
|
|
223
|
+
case 'switch': {
|
|
224
|
+
const out = [`${indent}switch (${pe(s.scrutinee, 99)}) {`];
|
|
225
|
+
const ci = indent + ' '; // case-label indent
|
|
226
|
+
const bi = indent + ' '; // case-body indent
|
|
227
|
+
for (const c of s.cases) {
|
|
228
|
+
for (const v of c.values) {
|
|
229
|
+
out.push(`${ci}case ${v}:`);
|
|
230
|
+
}
|
|
231
|
+
for (const t of c.body) {
|
|
232
|
+
out.push(...printStmt(t, bi, vt, leaf));
|
|
233
|
+
}
|
|
234
|
+
// A case whose body ends in `return`/`break` (a terminated arm) needs no `break;`; only an
|
|
235
|
+
// open non-fall-through arm gets one. `fallsThrough` omits it so control drops to the next case.
|
|
236
|
+
if (!c.fallsThrough && !endsTerminated(c.body)) {
|
|
237
|
+
out.push(`${bi}break;`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (s.default) {
|
|
241
|
+
out.push(`${ci}default:`);
|
|
242
|
+
for (const t of s.default) {
|
|
243
|
+
out.push(...printStmt(t, bi, vt, leaf));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
out.push(`${indent}}`);
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Does a statement list end in a control-flow terminator (so a trailing `break;` would be dead)?
|
|
253
|
+
function endsTerminated(body: Stmt[]): boolean {
|
|
254
|
+
const last = body[body.length - 1];
|
|
255
|
+
return (
|
|
256
|
+
!!last &&
|
|
257
|
+
(last.k === 'return' ||
|
|
258
|
+
last.k === 'break' ||
|
|
259
|
+
last.k === 'continue' ||
|
|
260
|
+
(last.k === 'if' && endsTerminated(last.then) && last.else.length > 0 && endsTerminated(last.else)))
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** The body of a C-family function: local declarations + statements, one string per line. The
|
|
265
|
+
* SIGNATURE (return type + name + params, plus any C++ scope/`this`/mangling) is the caller's —
|
|
266
|
+
* that is the language-divergent part each backend owns. */
|
|
267
|
+
// C-FAMILY WRITE LEGALIZATION (the assign-side sibling of the deref legalization in printExpr):
|
|
268
|
+
// a value whose rendered C type is definitely NON-pointer written into a pointer-declared slot
|
|
269
|
+
// (`v2 = a1 + v0` with `v2: u8 *`; `return a0 + v0` from a ptr-returning fn; `*pp = intexpr`
|
|
270
|
+
// through a pointer-element slot) is an ERROR on mwcc (gcc merely warns) — the honest spelling
|
|
271
|
+
// is the reinterpret cast to the DECLARED type, exactly what the machine's register move does.
|
|
272
|
+
// Unknowable renderings (calls) are left alone: their C type comes from prototypes outside this
|
|
273
|
+
// function. (A rebuilding transform with per-kind semantics — its own switch, per the l3/ast.ts
|
|
274
|
+
// traversal-vocabulary exemption.)
|
|
275
|
+
function legalizePointerWrites(fn: SFn): SFn {
|
|
276
|
+
const vt = declaredTypes(fn);
|
|
277
|
+
const castTo = (t: IrType | undefined, e: Expr): Expr => {
|
|
278
|
+
if (t?.kind !== 'ptr') {
|
|
279
|
+
return e;
|
|
280
|
+
}
|
|
281
|
+
const ct = exprCType(e, vt);
|
|
282
|
+
return ct && ct.kind !== 'ptr' && ct.kind !== 'array' ? { k: 'cast', to: t, e } : e;
|
|
283
|
+
};
|
|
284
|
+
const fix = (s: Stmt): Stmt => {
|
|
285
|
+
switch (s.k) {
|
|
286
|
+
case 'assign':
|
|
287
|
+
return { ...s, value: castTo(vt(s.name), s.value) };
|
|
288
|
+
case 'store': {
|
|
289
|
+
// the slot's type is the lvalue's C element type (a pointer-element slot needs the cast)
|
|
290
|
+
const slot = exprCType(s.lval, vt);
|
|
291
|
+
return { ...s, value: castTo(slot, s.value) };
|
|
292
|
+
}
|
|
293
|
+
case 'return':
|
|
294
|
+
return s.value ? { ...s, value: castTo(fn.retType, s.value) } : s;
|
|
295
|
+
case 'if':
|
|
296
|
+
return { ...s, then: s.then.map(fix), else: s.else.map(fix) };
|
|
297
|
+
case 'while':
|
|
298
|
+
case 'dowhile':
|
|
299
|
+
return { ...s, body: s.body.map(fix) };
|
|
300
|
+
case 'for':
|
|
301
|
+
return { ...s, init: fix(s.init), inc: fix(s.inc), body: s.body.map(fix) };
|
|
302
|
+
case 'switch':
|
|
303
|
+
return {
|
|
304
|
+
...s,
|
|
305
|
+
cases: s.cases.map((c) => ({ ...c, body: c.body.map(fix) })),
|
|
306
|
+
default: s.default ? s.default.map(fix) : undefined,
|
|
307
|
+
};
|
|
308
|
+
default:
|
|
309
|
+
return s;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
return { ...fn, body: fn.body.map(fix) };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
|
|
316
|
+
const fn = legalizePointerWrites(fn0);
|
|
317
|
+
// The legalization env: every printed var's declared type, from the SAME params/locals the
|
|
318
|
+
// emitted declarations come from — so the printer judges exactly the C the reader will see.
|
|
319
|
+
const vt: VarTypes = declaredTypes(fn);
|
|
320
|
+
const lines: string[] = [];
|
|
321
|
+
for (const l of fn.locals) {
|
|
322
|
+
lines.push(` ${cType(l.type)} ${l.name};`);
|
|
323
|
+
}
|
|
324
|
+
for (const s of fn.body) {
|
|
325
|
+
lines.push(...printStmt(s, ' ', vt, leaf));
|
|
326
|
+
}
|
|
327
|
+
return lines;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Struct declarations this function references, one `struct N { ... };` per recovered aggregate.
|
|
331
|
+
* The struct type is SELF-DESCRIBING: any padding needed to seat fields at their exact offsets is
|
|
332
|
+
* already present as real `u8[N]` pad fields — both raise/struct-arrays.ts (element strides) and
|
|
333
|
+
* raise/structs.ts (unaccessed leading/interior gaps) interleave them where natural C alignment
|
|
334
|
+
* does not already cover the offset. This just declares each field in order. */
|
|
335
|
+
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
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Assemble a full C-family function from a caller-supplied signature line and the shared body. */
|
|
342
|
+
export function emitCFamily(signature: string, fn: SFn, leaf?: LeafHook): string {
|
|
343
|
+
const decls = structDecls(fn);
|
|
344
|
+
const preamble = decls.length ? decls.join('\n') + '\n' : '';
|
|
345
|
+
return preamble + [`${signature} {`, ...cFamilyBody(fn, leaf), '}'].join('\n') + '\n';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// One C-family comment spelling, shared by the C and C++ backends. A `*` followed by `/`
|
|
349
|
+
// inside the text would terminate the comment early — split it.
|
|
350
|
+
export function cComment(text: string): string {
|
|
351
|
+
return `/* ${text.replace(/\*\//g, '* /')} */`;
|
|
352
|
+
}
|