@asmlift/core 0.4.0 → 0.6.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 +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +238 -164
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +341 -22
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +24 -6
- package/src/frontend/opaque.ts +31 -18
- package/src/frontend/ppc.ts +54 -7
- package/src/frontend/ssa.ts +632 -13
- package/src/frontend/thumb.ts +2786 -286
- package/src/ir/alias.ts +129 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +156 -27
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/argbase.ts +8 -2
- package/src/l3/ast.ts +464 -49
- package/src/l3/basecse.ts +709 -88
- package/src/l3/coalesce.ts +521 -66
- package/src/l3/dce.ts +54 -19
- package/src/l3/gates.ts +88 -0
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +113 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +110 -85
- package/src/l3/reindex.ts +715 -78
- package/src/l3/scopebase.ts +649 -219
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +23 -4
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +263 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +236 -13
- package/src/pipeline.ts +206 -49
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +4 -3
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +44 -19
- package/src/raise/latch.ts +126 -0
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +556 -0
- package/src/raise/paramwidth.ts +179 -0
- package/src/raise/pre-recovery.ts +101 -16
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +215 -14
- package/src/raise/shortcircuit.ts +477 -79
- package/src/raise/struct-arrays.ts +21 -3
- package/src/raise/structs.ts +61 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1726 -251
- package/src/structure/analysis.ts +1516 -220
- package/src/structure/bitfields.ts +332 -0
- package/src/structure/globalaccess.ts +274 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +435 -0
- package/src/structure/structure.ts +2850 -533
- package/src/structure/switch-recover.ts +688 -147
- package/src/symbols.ts +62 -1
- package/src/target.ts +367 -24
- package/src/trace.ts +111 -32
package/README.md
CHANGED
|
@@ -58,8 +58,8 @@ Everything under `src/` is importable as `@asmlift/core/<path>` (e.g.
|
|
|
58
58
|
|
|
59
59
|
## Architecture
|
|
60
60
|
|
|
61
|
-
Three ISA frontends (ARMv4T/Thumb, MIPS, PowerPC),
|
|
62
|
-
CodeWarrior), three language backends over one neutral AST — all scored across the package seam
|
|
61
|
+
Three ISA frontends (ARMv4T/Thumb, MIPS, PowerPC), five compilers (agbcc, IDO, KMC GCC, GCC
|
|
62
|
+
2.7.2, CodeWarrior), three language backends over one neutral AST — all scored across the package seam
|
|
63
63
|
by [`@asmlift/cli`](../cli/README.md) with the community `objdiff` engine (in-process, pinned
|
|
64
64
|
`objdiff-wasm`; asmlift never hand-rolls a diff).
|
|
65
65
|
|
|
@@ -79,20 +79,20 @@ injected via hooks, never copied. `verify()` runs after every IR-mutating pass;
|
|
|
79
79
|
|
|
80
80
|
### Modules
|
|
81
81
|
|
|
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
|
|
89
|
-
| `l3/*.ts` | `ast.ts`: language-**neutral** structured AST, the one traversal vocabulary (`exprChildren` etc.), and the `LanguageBackend` seam. Post-structure passes `dce.ts` + `basecse.ts`, the shared hoist mechanism `hoist.ts`, the differ-ranked re-spelling levers `regspell.ts` + `reindex.ts` + `argbase.ts` + `scopebase.ts`, and `typing.ts` (the rendered-expression C type the backends and contracts share)
|
|
90
|
-
| `backend/{c,cpp,cfamily,pascal}.ts` | Three backends: C and C++ (CodeWarrior mangling via `mangle.ts`) over the shared `cfamily.ts` substrate, and Pascal (`:=`, `div`, tail-position returns; unspellable constructs throw)
|
|
91
|
-
| `pipeline.ts` | `decompile()` + the shared tower spine + annotate-mode stubs/diagnostics
|
|
92
|
-
| `trace.ts` | `decompileTraced` — the traced tower (per-stage IR dumps + pattern before/after events), browser-pure; @asmlift/cli's `report.ts` enriches it with objdiff scores/candidates, the playground's Pipeline tab renders it directly
|
|
93
|
-
| `rank.ts` | Pure candidate enumeration + `rankBy` (an injected score function ranks). @asmlift/cli's differ ranks through `rankBy`; the playground's wasm scorer consumes the same enumeration with its own async loop
|
|
94
|
-
| `target.ts` | `TargetDescription` (ABI + capabilities + compilerBehaviors as 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
|
|
82
|
+
| Module | What it is |
|
|
83
|
+
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
84
|
+
| `ir/{types,core,opcodes,print,parse,verify}.ts` | MLIR-lite substrate: CFG of blocks + typed **block-arguments**, the typed opcode registry (`Opcode`, the one `effects` table DCE and hoist guards derive from), printer + parser (round-trip for L1/scalar types — see the domain note in parse.ts), verifier (arity/attrs/terminators/SSA dominance, located errors); `core.ts` also owns the CFG facts every level reads — `successorsOf`, `predecessors`, `dominators` |
|
|
85
|
+
| `frontend/{thumb,mips,ppc}.ts` | ISA frontends: decode → CFG → L1 with **Braun-2013 block-arg SSA** (`ssa.ts`), incl. loops, calls (signature-driven arity), memory, jump tables. Shared scaffolding: `disasm.ts` (objdump parsing), `splat.ts` (Splat-dialect MIPS → objdump-shaped instrs), `format.ts` (input-format classification), `emit.ts` (per-block emitter kit + `switch_br`), `opaque.ts` (the unmodelled-op → loud-`opaque` contract), `errors.ts` (`FrontendUnsupportedError`; PPC's subclass), `registry.ts`, `asmdata.ts` (Regime-B jump-table side-table) |
|
|
86
|
+
| `pattern/engine.ts` | Idiom layer: **rewrite patterns as data** + greedy driver + DCE; `patternApplies` gates on Target capabilities |
|
|
87
|
+
| `raise/*.ts` | The pre-recovery recognizers, in ONE ordered list (`pre-recovery.ts`): const materialize → magic division (`magicdiv.ts`, Hacker's Delight inverse) → soft division → array legalize → struct-array → member-array (`memberarrays.ts`, a constant offset feeding a variable-index walk is a struct's ARRAY member) → struct-pointer → short-circuit → narrow-reads (`narrow.ts`) → narrow locals (`narrowlocal.ts`, a carrier read only through its own extension IS that width) → parameter width (`paramwidth.ts`, a parameter extended in the prologue is DECLARED at that width); plus `recover.ts` (L1→L2 type recovery), `retsink.ts` (return-sinking), `latch.ts` (empty-latch folding), `errors.ts` (`RaiseUnsupportedError`) |
|
|
88
|
+
| `structure/*.ts` | L2→L3 in six modules: `loops.ts` (natural-loop discovery, over `ir/core.ts`'s dominators), `analysis.ts` (use registry, liveness, C4 materialization), `switch-recover.ts` (Regime-A comparison-tree recovery), `hazards.ts` (the checks a loop emitter runs before committing to a loop form), `namecoalesce.ts` (copy coalescing over the interference graph — the `/merge-names` axis), `structure.ts` (SSA-destruction coalescing with interference checks + emission: if/while/do-while/for/switch, break/early-return) |
|
|
89
|
+
| `l3/*.ts` | `ast.ts`: language-**neutral** structured AST, the one traversal vocabulary (`exprChildren` etc.), and the `LanguageBackend` seam. Post-structure passes `dce.ts` + `basecse.ts`, the shared hoist mechanism `hoist.ts`, the differ-ranked re-spelling levers `regspell.ts` + `reindex.ts` + `argbase.ts` + `scopebase.ts`, and `typing.ts` (the rendered-expression C type the backends and contracts share) |
|
|
90
|
+
| `backend/{c,cpp,cfamily,pascal}.ts` | Three backends: C and C++ (CodeWarrior mangling via `mangle.ts`) over the shared `cfamily.ts` substrate, and Pascal (`:=`, `div`, tail-position returns; unspellable constructs throw) |
|
|
91
|
+
| `pipeline.ts` | `decompile()` + the shared tower spine + annotate-mode stubs/diagnostics |
|
|
92
|
+
| `trace.ts` | `decompileTraced` — the traced tower (per-stage IR dumps + pattern before/after events), browser-pure; @asmlift/cli's `report.ts` enriches it with objdiff scores/candidates, the playground's Pipeline tab renders it directly |
|
|
93
|
+
| `rank.ts` | Pure candidate enumeration + `rankBy` (an injected score function ranks). @asmlift/cli's differ ranks through `rankBy`; the playground's wasm scorer consumes the same enumeration with its own async loop |
|
|
94
|
+
| `target.ts` | `TargetDescription` (ABI + capabilities + compilerBehaviors as 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 |
|
|
96
96
|
|
|
97
97
|
Scoring and ranking live across the package seam in [`@asmlift/cli`](../cli/README.md):
|
|
98
98
|
`score.ts` + `objdiff.ts` (toolchain compiles → in-process pinned `objdiff-wasm`, fail-closed)
|
|
@@ -133,6 +133,12 @@ Landmarks (not exhaustive — suites are named for what they pin):
|
|
|
133
133
|
- `contract-invariant` / `contracts` — the loud-fail contract, mutation-proven.
|
|
134
134
|
- `structure-guard` / `structure-soundness` / `audit-regression` — the adversarial-audit repro
|
|
135
135
|
locks.
|
|
136
|
+
- `one-version` — a WORKSPACE-scope invariant, like `offline-list`: every dependency that two or
|
|
137
|
+
more workspace packages declare must be declared AND resolved at one version. It exists because
|
|
138
|
+
`packages/cli` scored with `objdiff-wasm` 3.7.3 while `apps/web` scored with 3.7.0, so the
|
|
139
|
+
playground silently showed 229 where the benchmark published 233 on a byte-identical object
|
|
140
|
+
pair. Both the package set and the dependency set are derived, so a third consumer is covered
|
|
141
|
+
without editing the test.
|
|
136
142
|
|
|
137
143
|
**`test/corpus/` is load-bearing beyond this suite.** The committed disassembly fixtures in
|
|
138
144
|
`test/corpus/` are ALSO imported (via Vite `?raw`) by the playground's example gallery —
|
package/package.json
CHANGED
package/src/backend/c.ts
CHANGED
|
@@ -12,6 +12,7 @@ export { cComment }; // re-export: the shared spelling lives in cfamily.ts
|
|
|
12
12
|
|
|
13
13
|
export const cBackend: LanguageBackend = {
|
|
14
14
|
id: 'c',
|
|
15
|
+
spellsSwitchFallthrough: true,
|
|
15
16
|
emit(fn: SFn): string {
|
|
16
17
|
const params = fn.params.map((p) => `${cType(p.type)} ${p.name}`).join(', ') || 'void';
|
|
17
18
|
return emitCFamily(`${cType(fn.retType)} ${fn.name}(${params})`, fn);
|
package/src/backend/cfamily.ts
CHANGED
|
@@ -3,95 +3,41 @@
|
|
|
3
3
|
// here, `pasType` in the Pascal backend). The empirical fact grounding the sharing: a
|
|
4
4
|
// CodeWarrior member function's BODY is byte-identical to the same C with `this` as an
|
|
5
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`).
|
|
7
|
-
//
|
|
6
|
+
// DIVERGENT surface (the mangled/scoped signature, references, `this`).
|
|
7
|
+
//
|
|
8
|
+
// THE SEAM, stated as what it means rather than as a list that rots: a C-family backend owns its
|
|
9
|
+
// SIGNATURE LINE and — for C++, whose member access spells differently — a leaf hook. Everything
|
|
10
|
+
// below the signature is this file's: declarations, statements, expressions, precedence, the
|
|
11
|
+
// legalizing casts, and the recovered-struct declaration spelling (which is why that lives here
|
|
12
|
+
// too, shared with the scoring layer's synthesized declarations so the two cannot drift).
|
|
8
13
|
import { IrType, T, scalarTypeForAccess, typeToString } from '../ir/types';
|
|
9
14
|
import { BinOp, Expr, SFn, Stmt, dotBase } from '../l3/ast';
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
* It exists for one question, and the question is byte-load-bearing: C spells both `>>>` and `>>`
|
|
23
|
-
* as `>>` and chooses between them from the left operand's type. A logical shift rendered over a
|
|
24
|
-
* signed expression recompiles to `asr` where the target has `lsr`, and evaluates to a different
|
|
25
|
-
* value. The C-family backend casts the operand whenever this returns anything but the signedness
|
|
26
|
-
* the operator needs, so `undefined` is the safe answer in every case the model does not cover — a
|
|
27
|
-
* redundant cast is codegen-identical, a missing one is a miscompile.
|
|
28
|
-
*
|
|
29
|
-
* Anything narrower than 32 bits promotes to `int` and is therefore SIGNED, whatever it was
|
|
30
|
-
* declared. Pointers, calls and markers are `undefined`.
|
|
31
|
-
*/
|
|
32
|
-
function renderedIntSignedness(e: Expr, varType: VarTypes): boolean | undefined {
|
|
33
|
-
const rec = (x: Expr): boolean | undefined => renderedIntSignedness(x, varType);
|
|
34
|
-
// an lvalue-ish leaf: its C type is a declaration / an explicit cast / a carried access width
|
|
35
|
-
const promoted = (t: IrType | undefined): boolean | undefined =>
|
|
36
|
-
t?.kind !== 'int' ? undefined : t.width < 32 ? true : t.width === 32 ? t.signed : undefined;
|
|
37
|
-
switch (e.k) {
|
|
38
|
-
case 'var':
|
|
39
|
-
case 'cast':
|
|
40
|
-
case 'index':
|
|
41
|
-
case 'field':
|
|
42
|
-
return promoted(exprCType(e, varType));
|
|
43
|
-
// A decimal literal is `int` when it fits in one; C89 gives a larger one an unsigned type,
|
|
44
|
-
// which is not the same operand — so it is left undetermined rather than assumed. INT_MIN is
|
|
45
|
-
// in that larger class despite fitting: the backend prints it as `-2147483648`, which C lexes
|
|
46
|
-
// as unary minus applied to `2147483648` — a constant too big for `int`, hence unsigned long.
|
|
47
|
-
case 'const':
|
|
48
|
-
return e.value > -2147483648 && e.value <= 2147483647 ? true : undefined;
|
|
49
|
-
// `-x` / `~x` carry the PROMOTED type of the operand; `!x` is `int`.
|
|
50
|
-
case 'un':
|
|
51
|
-
return e.op === '!' ? true : rec(e.e);
|
|
52
|
-
case 'bin': {
|
|
53
|
-
// Shifts take the type of the LEFT operand alone — the right is promoted independently.
|
|
54
|
-
if (e.op === '<<' || e.op === '>>' || e.op === '>>>') {
|
|
55
|
-
return rec(e.l);
|
|
56
|
-
}
|
|
57
|
-
// Comparisons and the logical connectives yield `int`.
|
|
58
|
-
if (['<', '<=', '>', '>=', '==', '!=', '&&', '||'].includes(e.op)) {
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
// Usual arithmetic conversions over the remaining binary operators: at equal rank, unsigned
|
|
62
|
-
// wins. Either side unknown leaves the result unknown — EXCEPT when the known side is
|
|
63
|
-
// unsigned, which already decides it.
|
|
64
|
-
//
|
|
65
|
-
// That exception is the one place this returns a DEFINITE answer from an unknown operand,
|
|
66
|
-
// and it is sound only because every integer here is rank `int`: at UNEQUAL rank C converts
|
|
67
|
-
// to the wider type first, so `unsigned int & long long` is SIGNED. Core has no 64-bit
|
|
68
|
-
// integer type at all (the decomp typedef vocabulary stops at 32 — see contracts.ts
|
|
69
|
-
// SCALAR_WIDTHS), so the unequal-rank case cannot arise. Adding one would invalidate this.
|
|
70
|
-
const l = rec(e.l);
|
|
71
|
-
const r = rec(e.r);
|
|
72
|
-
if (l === false || r === false) {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
return l === true && r === true ? true : undefined;
|
|
76
|
-
}
|
|
77
|
-
case 'call':
|
|
78
|
-
case 'marker':
|
|
79
|
-
case 'addr':
|
|
80
|
-
return undefined;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
15
|
+
import { orderSlotLocals } from '../l3/slotorder';
|
|
16
|
+
import {
|
|
17
|
+
type PrintEnv,
|
|
18
|
+
arithConversionSignedness,
|
|
19
|
+
assertsVolatile,
|
|
20
|
+
declaredTypes,
|
|
21
|
+
derefStrideOk,
|
|
22
|
+
exprCType,
|
|
23
|
+
printEnv,
|
|
24
|
+
renderedIntSignedness,
|
|
25
|
+
writesNonPointerIntoPointer,
|
|
26
|
+
} from '../l3/typing';
|
|
83
27
|
|
|
84
28
|
// C operator precedence (lower binds tighter). Used to emit MINIMAL parentheses. Shared: C++ has
|
|
85
29
|
// the same precedence for these operators.
|
|
86
30
|
const PREC: Record<BinOp, number> = {
|
|
87
31
|
'*': 3,
|
|
88
32
|
'/': 3,
|
|
33
|
+
'/u': 3, // the unsigned twins spell as C's `/` and `%` (see C_SPELLING) — same precedence
|
|
89
34
|
'%': 3,
|
|
35
|
+
'%u': 3,
|
|
90
36
|
'+': 4,
|
|
91
37
|
'-': 4,
|
|
92
38
|
'<<': 5,
|
|
93
39
|
'>>': 5,
|
|
94
|
-
'>>>': 5, // spells as C's `>>`
|
|
40
|
+
'>>>': 5, // spells as C's `>>` — same precedence
|
|
95
41
|
'<': 6,
|
|
96
42
|
'<=': 6,
|
|
97
43
|
'>': 6,
|
|
@@ -105,6 +51,19 @@ const PREC: Record<BinOp, number> = {
|
|
|
105
51
|
'||': 12,
|
|
106
52
|
};
|
|
107
53
|
|
|
54
|
+
/** The signedness-carrying operator pairs and the ONE C token each pair shares (l3/ast.ts BinOp).
|
|
55
|
+
* Membership here is what makes an operator's operands get pinned; every other operator prints
|
|
56
|
+
* its own symbol and needs no cast, because C's own rules for it are sign-blind or already
|
|
57
|
+
* agree. */
|
|
58
|
+
const C_SPELLING: Partial<Record<BinOp, { token: string; signed: boolean }>> = {
|
|
59
|
+
'>>': { token: '>>', signed: true },
|
|
60
|
+
'>>>': { token: '>>', signed: false },
|
|
61
|
+
'/': { token: '/', signed: true },
|
|
62
|
+
'/u': { token: '/', signed: false },
|
|
63
|
+
'%': { token: '%', signed: true },
|
|
64
|
+
'%u': { token: '%', signed: false },
|
|
65
|
+
};
|
|
66
|
+
|
|
108
67
|
/** Spell a recovered type in the decomp C-family typedef vocabulary (`s32`/`u32`/`u8`/`T *`). */
|
|
109
68
|
export function cType(t: IrType): string {
|
|
110
69
|
if (t.kind === 'ptr') {
|
|
@@ -163,7 +122,80 @@ export function renderStructDecl(name: string, fields: StructFieldDecl[]): strin
|
|
|
163
122
|
// backend uses. The default (no hook) is byte-identical C.
|
|
164
123
|
export type LeafHook = (e: Expr, rec: (e: Expr, p: number) => string) => string | null;
|
|
165
124
|
|
|
166
|
-
|
|
125
|
+
/** C-FAMILY LEGALIZATION (owned here, per the width-carrying `index` node contract in l3/ast.ts):
|
|
126
|
+
* the BASE an `index` node prints through — a deref whose base does not render as a pointer/array
|
|
127
|
+
* STRIDING the access width is spelled through the honest reinterpret cast at that width, the
|
|
128
|
+
* machine semantics of the access. Materialized as a synthetic cast node so the spelling (text,
|
|
129
|
+
* precedence, parens) is exactly that of a tree-level cast, and returns the base UNCHANGED (===)
|
|
130
|
+
* when no cast is needed, which is what the multidimensional guard tests.
|
|
131
|
+
*
|
|
132
|
+
* AND IT CARRIES THE QUALIFIER. In C the access takes the OUTER type, so a plain cast over a
|
|
133
|
+
* base the tree declared volatile spells an MMIO access the compiler may CSE, reorder or drop —
|
|
134
|
+
* `((s32 *)(volatile u16 *)0x4000208)[i]` reads once where `((volatile s32 *)0x4000208)[i]`
|
|
135
|
+
* reads twice (agbcc 2.9-arm-000512, `-O2 -mthumb-interwork -Wimplicit -fhex-asm
|
|
136
|
+
* -fprologue-bugfix`), and referring to a volatile object through a non-volatile lvalue is
|
|
137
|
+
* undefined behaviour (C99 6.7.3p5). */
|
|
138
|
+
function legalizedIndexBase(ix: Extract<Expr, { k: 'index' }>, vt: PrintEnv): Expr {
|
|
139
|
+
return derefStrideOk(exprCType(ix.base, vt.type), ix.width, ix.signed)
|
|
140
|
+
? ix.base
|
|
141
|
+
: {
|
|
142
|
+
k: 'cast',
|
|
143
|
+
to: T.ptr(scalarTypeForAccess(ix.width, ix.signed)),
|
|
144
|
+
...(assertsVolatile(ix.base, vt) ? { volatile: true as const } : {}),
|
|
145
|
+
e: ix.base,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The 32-bit integer cast that PINS an operand's signedness.
|
|
150
|
+
*
|
|
151
|
+
* An existing 32-bit integer cast is REPLACED rather than wrapped — `(u32)(s32)&g` and `(u32)&g`
|
|
152
|
+
* are the same bytes, and the arithmetic rules upstream do emit that inner cast (intifyAddr).
|
|
153
|
+
* The replacement CARRIES the qualifier: re-typing a `volatile` cast without it drops an
|
|
154
|
+
* assertion the differ cannot referee the loss of, which is why l3/initfirst.ts's
|
|
155
|
+
* `stripWideIntCast` refuses the same peel one pass over. */
|
|
156
|
+
function recast32(x: Expr, signed: boolean): Expr {
|
|
157
|
+
const replaced = x.k === 'cast' && x.to.kind === 'int' && x.to.width === 32 ? x : undefined;
|
|
158
|
+
return {
|
|
159
|
+
k: 'cast',
|
|
160
|
+
to: T.int(32, signed),
|
|
161
|
+
...(replaced?.volatile === true ? { volatile: true as const } : {}),
|
|
162
|
+
e: replaced ? replaced.e : x,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** C-FAMILY OPERAND-SIGNEDNESS LEGALIZATION, the same discipline as the deref cast one operator
|
|
167
|
+
* over. The tower keeps the signedness-carrying pairs apart (`>>>` logical / `>>` arithmetic,
|
|
168
|
+
* `/u` `%u` unsigned / `/` `%` signed); C spells each pair with ONE token and picks between them
|
|
169
|
+
* from the operand types. So the operands must be made to carry the choice, or an `shr_u`
|
|
170
|
+
* recompiles to `asr` where the target has `lsr` AND evaluates differently
|
|
171
|
+
* (`*(u8 *)&g << 30 >> 30` promotes to `int`, so a 2-bit field holding 2 comes out -1), and a
|
|
172
|
+
* `udiv` calls `__divsi3` where the target called `__udivsi3`.
|
|
173
|
+
*
|
|
174
|
+
* (engine.ts's zext fold covers the same hazard for widths C can NAME, by folding the whole
|
|
175
|
+
* shift pair to a cast op. Every other extract width — every bitfield read — lands here.)
|
|
176
|
+
*
|
|
177
|
+
* A SHIFT and a DIVIDE read their operands differently, so the pin does too. A SHIFT takes the
|
|
178
|
+
* type of its left operand alone, and the cast goes on unless that operand PROVABLY renders as
|
|
179
|
+
* the op needs (renderedIntSignedness's header carries the rule for reading `undefined`).
|
|
180
|
+
*
|
|
181
|
+
* A DIVIDE takes the usual arithmetic conversions over BOTH operands, where unsigned wins at
|
|
182
|
+
* equal rank, so the question is what the PAIR renders as. Once the pair renders wrong the two
|
|
183
|
+
* directions cost differently: unsigned takes ONE cast, which carries the whole operation, while
|
|
184
|
+
* signed has to pin EVERY operand short of a proof, because one unsigned side is enough to make
|
|
185
|
+
* the division unsigned. Verified by compiling: `((u32)a / b) / 7` calls `__udivsi3` twice,
|
|
186
|
+
* `(s32)((u32)a / b) / 7` calls `__udivsi3` then `__divsi3`. */
|
|
187
|
+
function pinnedOperands(e0: Extract<Expr, { k: 'bin' }>, wantSigned: boolean, vt: PrintEnv): [Expr, Expr] {
|
|
188
|
+
if (e0.op === '>>' || e0.op === '>>>') {
|
|
189
|
+
return [renderedIntSignedness(e0.l, vt.type) === wantSigned ? e0.l : recast32(e0.l, wantSigned), e0.r];
|
|
190
|
+
}
|
|
191
|
+
if (arithConversionSignedness(e0.l, e0.r, vt.type) === wantSigned) {
|
|
192
|
+
return [e0.l, e0.r];
|
|
193
|
+
}
|
|
194
|
+
const pinSigned = (x: Expr): Expr => (renderedIntSignedness(x, vt.type) === true ? x : recast32(x, true));
|
|
195
|
+
return wantSigned ? [pinSigned(e0.l), pinSigned(e0.r)] : [recast32(e0.l, false), e0.r];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function printExpr(e: Expr, parentPrec: number, vt: PrintEnv, leaf?: LeafHook): string {
|
|
167
199
|
const rec = (x: Expr, p: number) => printExpr(x, p, vt, leaf);
|
|
168
200
|
if (leaf) {
|
|
169
201
|
const s = leaf(e, rec);
|
|
@@ -171,37 +203,6 @@ function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook):
|
|
|
171
203
|
return s;
|
|
172
204
|
}
|
|
173
205
|
}
|
|
174
|
-
// C-FAMILY LEGALIZATION (owned here, per the width-carrying `index` node contract in l3/ast.ts):
|
|
175
|
-
// a deref whose base does not render as a pointer/array STRIDING the access width is spelled
|
|
176
|
-
// through the honest reinterpret cast at that width — the machine semantics of the access.
|
|
177
|
-
// Materialized as a synthetic cast node so the spelling (text, precedence, parens) is exactly
|
|
178
|
-
// that of a tree-level cast.
|
|
179
|
-
const legalized = (ix: Extract<Expr, { k: 'index' }>): Expr =>
|
|
180
|
-
derefStrideOk(exprCType(ix.base, vt), ix.width)
|
|
181
|
-
? ix.base
|
|
182
|
-
: { k: 'cast', to: T.ptr(scalarTypeForAccess(ix.width, ix.signed)), e: ix.base };
|
|
183
|
-
// C-FAMILY SHIFT LEGALIZATION, the same discipline one operator over. The tower keeps the two
|
|
184
|
-
// right shifts apart (`>>>` logical, `>>` arithmetic); C spells BOTH `>>` and picks between them
|
|
185
|
-
// from the LEFT OPERAND'S TYPE. So the operand must be made to carry the choice, or an `shr_u`
|
|
186
|
-
// recompiles to `asr` where the target has `lsr` AND evaluates differently —
|
|
187
|
-
// `*(u8 *)&g << 30 >> 30` promotes to `int`, so a 2-bit field holding 2 comes out -1.
|
|
188
|
-
//
|
|
189
|
-
// (engine.ts's zext fold covers the same hazard for widths C can NAME, by folding the whole
|
|
190
|
-
// shift pair to a cast op. Every other extract width — every bitfield read — lands here.)
|
|
191
|
-
//
|
|
192
|
-
// The cast is added unless the operand PROVABLY renders with the signedness the op needs:
|
|
193
|
-
// renderedIntSignedness answers `undefined` wherever its model does not reach, and a redundant
|
|
194
|
-
// cast is codegen-identical while a missing one is a miscompile. An existing 32-bit integer cast
|
|
195
|
-
// is REPLACED rather than wrapped — `(u32)(s32)&g` and `(u32)&g` are the same bytes, and the
|
|
196
|
-
// arithmetic rules upstream do emit that inner cast (intifyAddr).
|
|
197
|
-
const shiftOperand = (e0: Extract<Expr, { k: 'bin' }>): Expr => {
|
|
198
|
-
const wantSigned = e0.op === '>>';
|
|
199
|
-
if (renderedIntSignedness(e0.l, vt) === wantSigned) {
|
|
200
|
-
return e0.l;
|
|
201
|
-
}
|
|
202
|
-
const inner = e0.l.k === 'cast' && e0.l.to.kind === 'int' && e0.l.to.width === 32 ? e0.l.e : e0.l;
|
|
203
|
-
return { k: 'cast', to: T.int(32, wantSigned), e: inner };
|
|
204
|
-
};
|
|
205
206
|
switch (e.k) {
|
|
206
207
|
case 'var':
|
|
207
208
|
return e.name;
|
|
@@ -221,12 +222,13 @@ function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook):
|
|
|
221
222
|
// `(*p)[1]`, never `*p[1]` which C groups as `*(p[1])`), `base[idx]` otherwise (POSTFIX —
|
|
222
223
|
// binds tighter than any prefix operator, so a cast/unary/deref base is printed at prec 1
|
|
223
224
|
// and parenthesizes itself: `((u8 *)p)[1]`). The postfix form needs no outer parentheses.
|
|
224
|
-
const base =
|
|
225
|
+
const base = legalizedIndexBase(e, vt);
|
|
225
226
|
// Leading constant subscripts (a multidimensional array global's bare spelling) keep the
|
|
226
227
|
// postfix form whatever `idx` is: `g[0][0]` is the element, `*g[0]` would be its ROW.
|
|
227
228
|
//
|
|
228
|
-
// `lead` implies the base already strides the access width — its only
|
|
229
|
-
// matching element type for the global (structure.ts bareArrayLead
|
|
229
|
+
// `lead` implies the base already strides the access width — its only producers register a
|
|
230
|
+
// matching element type for the global (structure/globalaccess.ts `bareArrayLead` and
|
|
231
|
+
// `declaredSubscripts`, both through structure.ts's `noteGlobal`). Nothing
|
|
230
232
|
// else enforced that, and the failure would be quiet-ish: legalization would wrap the base,
|
|
231
233
|
// spelling `((u16 *)g)[0][i]`, which subscripts a `u16` twice. Check it rather than assume.
|
|
232
234
|
if (e.lead && e.lead.length > 0) {
|
|
@@ -235,7 +237,7 @@ function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook):
|
|
|
235
237
|
`c backend: a multidimensional array access needs a base that strides ${e.width} bytes as spelled`,
|
|
236
238
|
);
|
|
237
239
|
}
|
|
238
|
-
return `${rec(base, 1)}${e.lead.map((l) => `[${l}]`).join('')}[${rec(e.idx, 99)}]`;
|
|
240
|
+
return `${rec(base, 1)}${e.lead.map((l) => `[${rec(l, 99)}]`).join('')}[${rec(e.idx, 99)}]`;
|
|
239
241
|
}
|
|
240
242
|
if (e.idx.k === 'const' && e.idx.value === 0) {
|
|
241
243
|
const s = `*${rec(base, 2)}`;
|
|
@@ -289,20 +291,25 @@ function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook):
|
|
|
289
291
|
// op. Under a POSTFIX parent ([]/->) the cast itself must parenthesize — `((struct S *)p)->f`,
|
|
290
292
|
// NOT `(struct S *)p->f` (which C parses as a cast OF the member access).
|
|
291
293
|
case 'cast': {
|
|
292
|
-
|
|
294
|
+
// `volatile` binds to the POINTEE (`(volatile u16 *)a`), which is where the qualifier goes
|
|
295
|
+
// on a raw-address access — the same prefix position the declaration printer uses for a
|
|
296
|
+
// pointer local's `pointeeVolatile`.
|
|
297
|
+
const s = `(${e.volatile ? 'volatile ' : ''}${cType(e.to)})${rec(e.e, 2)}`;
|
|
293
298
|
return parentPrec < 2 ? `(${s})` : s;
|
|
294
299
|
}
|
|
295
300
|
case 'bin': {
|
|
296
301
|
const p = PREC[e.op];
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
const
|
|
302
|
+
// Each signedness-carrying pair spells with ONE C token; `pinnedOperands` supplies the
|
|
303
|
+
// operand cast that says which of the pair it is.
|
|
304
|
+
const pair = C_SPELLING[e.op];
|
|
305
|
+
const [l, r] = pair ? pinnedOperands(e, pair.signed, vt) : [e.l, e.r];
|
|
306
|
+
const s = `${rec(l, p)} ${pair ? pair.token : e.op} ${rec(r, p - 1)}`;
|
|
300
307
|
return p > parentPrec ? `(${s})` : s;
|
|
301
308
|
}
|
|
302
309
|
}
|
|
303
310
|
}
|
|
304
311
|
|
|
305
|
-
function printStmt(s: Stmt, indent: string, vt:
|
|
312
|
+
function printStmt(s: Stmt, indent: string, vt: PrintEnv, leaf?: LeafHook): string[] {
|
|
306
313
|
const pe = (e: Expr, p: number) => printExpr(e, p, vt, leaf);
|
|
307
314
|
switch (s.k) {
|
|
308
315
|
case 'assign':
|
|
@@ -355,8 +362,8 @@ function printStmt(s: Stmt, indent: string, vt: VarTypes, leaf?: LeafHook): stri
|
|
|
355
362
|
return out;
|
|
356
363
|
}
|
|
357
364
|
case 'for': {
|
|
358
|
-
// `for (init; cond; inc) { body }`. PRECONDITION (guaranteed by
|
|
359
|
-
// `recognizeForLoops`): init/inc are each a SINGLE-LINE `assign` statement. `clause` renders one
|
|
365
|
+
// `for (init; cond; inc) { body }`. PRECONDITION (guaranteed by both producers — structure.ts
|
|
366
|
+
// `recognizeForLoops` and l3/reindex.ts): init/inc are each a SINGLE-LINE `assign` statement. `clause` renders one
|
|
360
367
|
// and strips its trailing `;` so it sits inside the header (`i = 0; c; i = i + 1`). A multi-line
|
|
361
368
|
// statement (an `if`/nested loop) would render mangled — but the recognizer never builds one here.
|
|
362
369
|
const clause = (st: Stmt) => printStmt(st, '', vt, leaf).join(' ').replace(/;\s*$/, '').trim();
|
|
@@ -371,33 +378,98 @@ function printStmt(s: Stmt, indent: string, vt: VarTypes, leaf?: LeafHook): stri
|
|
|
371
378
|
return [`${indent}break;`];
|
|
372
379
|
case 'continue':
|
|
373
380
|
return [`${indent}continue;`];
|
|
374
|
-
case 'switch':
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
381
|
+
case 'switch':
|
|
382
|
+
return printSwitchStmt(s, indent, vt, leaf);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** A C `switch`: the arm bodies, the `default:` label's position among them, and the three
|
|
387
|
+
* producer-contract refusals that stand between an L3 `switch` node and valid C. Its own function
|
|
388
|
+
* because it is the one statement kind whose printing is a program of its own; `printStmt`'s arm
|
|
389
|
+
* is one call. */
|
|
390
|
+
function printSwitchStmt(s: Extract<Stmt, { k: 'switch' }>, indent: string, vt: PrintEnv, leaf?: LeafHook): string[] {
|
|
391
|
+
const pe = (e: Expr, p: number) => printExpr(e, p, vt, leaf);
|
|
392
|
+
// A `break;` in an arm BODY is the innermost LOOP's in L3 (l3/ast.ts) and the SWITCH's in C,
|
|
393
|
+
// so printing one between `case` labels rebinds it — a changed program that reads as ordinary
|
|
394
|
+
// C. Refused HERE because the rebinding is the printer's, for every producer rather than for
|
|
395
|
+
// the two switch regimes. No recovery reaches it today (both regimes decline a loop-exiting
|
|
396
|
+
// arm first, loudly), so it is a contract on the next one.
|
|
397
|
+
//
|
|
398
|
+
// `continue;` is deliberately NOT refused: C binds it to the smallest enclosing ITERATION
|
|
399
|
+
// statement, which a `switch` is not, so it already means what L3 means. What a loop-respelling
|
|
400
|
+
// pass must preserve is exactly that — the three that can re-spell a loop into one whose
|
|
401
|
+
// `continue` would run a different increment (`recognizeForLoops` in structure.ts,
|
|
402
|
+
// `respellCountdown` in l3/reindex.ts, and l3/unreduce.ts) each scan switch arms for the node
|
|
403
|
+
// before firing, and a fourth must too.
|
|
404
|
+
for (const body of [...s.cases.map((c) => c.body), s.default ?? []]) {
|
|
405
|
+
if (switchBoundBreakIn(body)) {
|
|
406
|
+
throw new Error('c backend: a switch arm carries a loop-scoped `break;`, which C would bind to the switch');
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const out = [`${indent}switch (${pe(s.scrutinee, 99)}) {`];
|
|
410
|
+
const ci = indent + ' '; // case-label indent
|
|
411
|
+
const bi = indent + ' '; // case-body indent
|
|
412
|
+
// `?.length`, not just presence: a label with no statement under it is not valid C89, and an
|
|
413
|
+
// L3 pass (dce, reindex) may empty a default that arrived with statements — the structurer's
|
|
414
|
+
// own "don't attach an empty default" rule cannot see that.
|
|
415
|
+
const hasDefault = !!s.default?.length;
|
|
416
|
+
// Where the `default:` label goes, as a COUNT of case arms before it (l3/ast.ts): absent ⇒
|
|
417
|
+
// after all of them. A count past the arms matches no position at all and the label would
|
|
418
|
+
// simply not be printed — the default arm vanishing from a switch that has one — so a
|
|
419
|
+
// producer that hands one over fails loud, like the falling-arm placement below.
|
|
420
|
+
const defAt = hasDefault ? (s.defaultAt ?? s.cases.length) : -1;
|
|
421
|
+
if (hasDefault && (defAt < 0 || defAt > s.cases.length)) {
|
|
422
|
+
throw new Error(`c backend: a switch places its default at arm ${defAt} of ${s.cases.length}`);
|
|
423
|
+
}
|
|
424
|
+
const printDefault = (): void => {
|
|
425
|
+
out.push(`${ci}default:`);
|
|
426
|
+
for (const t of s.default!) {
|
|
427
|
+
out.push(...printStmt(t, bi, vt, leaf));
|
|
428
|
+
}
|
|
429
|
+
// A default that is NOT last would otherwise fall into the case below it — the mirror of the
|
|
430
|
+
// rule for cases. The last one needs no `break;` because there is nothing under it.
|
|
431
|
+
if (defAt < s.cases.length && !endsTerminated(s.default!)) {
|
|
432
|
+
out.push(`${bi}break;`);
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
s.cases.forEach((c, i) => {
|
|
436
|
+
if (i === defAt) {
|
|
437
|
+
// Moving the label in FRONT of a falling arm would divert that arm into the default —
|
|
438
|
+
// a silent control-flow change. Recovery only positions a default among closed arms, so
|
|
439
|
+
// this is a producer bug rather than an input shape: fail loud.
|
|
440
|
+
if (i > 0 && s.cases[i - 1].fallsThrough) {
|
|
441
|
+
throw new Error(
|
|
442
|
+
`c backend: a switch places its default after a case that falls through, which would divert it`,
|
|
443
|
+
);
|
|
396
444
|
}
|
|
397
|
-
|
|
398
|
-
|
|
445
|
+
printDefault();
|
|
446
|
+
}
|
|
447
|
+
for (const v of c.values) {
|
|
448
|
+
out.push(`${ci}case ${v}:`);
|
|
449
|
+
}
|
|
450
|
+
for (const t of c.body) {
|
|
451
|
+
out.push(...printStmt(t, bi, vt, leaf));
|
|
399
452
|
}
|
|
453
|
+
// A case whose body ends in `return`/`break` (a terminated arm) needs no `break;`; only an
|
|
454
|
+
// open non-fall-through arm gets one. `fallsThrough` omits it so control drops to the next case.
|
|
455
|
+
if (!c.fallsThrough && !endsTerminated(c.body)) {
|
|
456
|
+
out.push(`${bi}break;`);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
if (defAt === s.cases.length) {
|
|
460
|
+
printDefault();
|
|
400
461
|
}
|
|
462
|
+
out.push(`${indent}}`);
|
|
463
|
+
return out;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Does this arm body carry a `break` C would bind to the enclosing SWITCH rather than to the loop
|
|
467
|
+
* L3 meant? A loop opened inside the arm captures its own, so its body is not walked; a nested
|
|
468
|
+
* `switch` captures one too and is checked when it is printed. */
|
|
469
|
+
function switchBoundBreakIn(body: Stmt[]): boolean {
|
|
470
|
+
return body.some(
|
|
471
|
+
(s) => s.k === 'break' || (s.k === 'if' && (switchBoundBreakIn(s.then) || switchBoundBreakIn(s.else))),
|
|
472
|
+
);
|
|
401
473
|
}
|
|
402
474
|
|
|
403
475
|
// Does a statement list end in a control-flow terminator (so a trailing `break;` would be dead)?
|
|
@@ -412,26 +484,15 @@ function endsTerminated(body: Stmt[]): boolean {
|
|
|
412
484
|
);
|
|
413
485
|
}
|
|
414
486
|
|
|
415
|
-
/** The body of a C-family function: local declarations + statements, one string per line. The
|
|
416
|
-
* SIGNATURE (return type + name + params, plus any C++ scope/`this`/mangling) is the caller's —
|
|
417
|
-
* that is the language-divergent part each backend owns. */
|
|
418
487
|
// C-FAMILY WRITE LEGALIZATION (the assign-side sibling of the deref legalization in printExpr):
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
// is the reinterpret cast to the DECLARED type, exactly what the machine's register move does.
|
|
423
|
-
// Unknowable renderings (calls) are left alone: their C type comes from prototypes outside this
|
|
424
|
-
// function. (A rebuilding transform with per-kind semantics — its own switch, per the l3/ast.ts
|
|
488
|
+
// the C family's answer to `writesNonPointerIntoPointer` is the reinterpret cast to the DECLARED
|
|
489
|
+
// type — exactly what the machine's register move does — where Pascal, having no such cast,
|
|
490
|
+
// declines. (A rebuilding transform with per-kind semantics — its own switch, per the l3/ast.ts
|
|
425
491
|
// traversal-vocabulary exemption.)
|
|
426
492
|
function legalizePointerWrites(fn: SFn): SFn {
|
|
427
493
|
const vt = declaredTypes(fn);
|
|
428
|
-
const castTo = (t: IrType | undefined, e: Expr): Expr =>
|
|
429
|
-
|
|
430
|
-
return e;
|
|
431
|
-
}
|
|
432
|
-
const ct = exprCType(e, vt);
|
|
433
|
-
return ct && ct.kind !== 'ptr' && ct.kind !== 'array' ? { k: 'cast', to: t, e } : e;
|
|
434
|
-
};
|
|
494
|
+
const castTo = (t: IrType | undefined, e: Expr): Expr =>
|
|
495
|
+
writesNonPointerIntoPointer(t, e, vt) ? { k: 'cast', to: t, e } : e;
|
|
435
496
|
const fix = (s: Stmt): Stmt => {
|
|
436
497
|
switch (s.k) {
|
|
437
498
|
case 'assign':
|
|
@@ -463,14 +524,23 @@ function legalizePointerWrites(fn: SFn): SFn {
|
|
|
463
524
|
return { ...fn, body: fn.body.map(fix) };
|
|
464
525
|
}
|
|
465
526
|
|
|
527
|
+
/** The body of a C-family function: local declarations + statements, one string per line. The
|
|
528
|
+
* SIGNATURE (return type + name + params, plus any C++ scope/`this`/mangling) is the caller's —
|
|
529
|
+
* that is the language-divergent part each backend owns. */
|
|
466
530
|
function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
|
|
467
531
|
const fn = legalizePointerWrites(fn0);
|
|
468
|
-
// The legalization env: every printed var's declared type, from the SAME
|
|
469
|
-
// emitted declarations come from — so the printer judges exactly the C the
|
|
470
|
-
|
|
532
|
+
// The legalization env: every printed var's declared type and pointee volatility, from the SAME
|
|
533
|
+
// params/locals the emitted declarations come from — so the printer judges exactly the C the
|
|
534
|
+
// reader will see.
|
|
535
|
+
const vt: PrintEnv = printEnv(fn);
|
|
471
536
|
const lines: string[] = [];
|
|
472
537
|
for (const l of fn.locals) {
|
|
473
|
-
|
|
538
|
+
// Both facts render at the PREFIX position, where C's declarator grammar reads them
|
|
539
|
+
// differently: on a scalar the qualifier binds to the object (`volatile u16 sp0`), on a
|
|
540
|
+
// pointer declarator to the pointee — the INNERMOST one for a multi-level pointer
|
|
541
|
+
// (`volatile u16 ** p`). An object-volatile POINTER (`u16 *volatile p`) has no inhabitant —
|
|
542
|
+
// no lever or recognizer produces one.
|
|
543
|
+
lines.push(` ${l.volatile || l.pointeeVolatile ? 'volatile ' : ''}${cType(l.type)} ${l.name};`);
|
|
474
544
|
}
|
|
475
545
|
for (const s of fn.body) {
|
|
476
546
|
lines.push(...printStmt(s, ' ', vt, leaf));
|
|
@@ -488,7 +558,11 @@ function structDecls(fn: SFn): string[] {
|
|
|
488
558
|
}
|
|
489
559
|
|
|
490
560
|
/** Assemble a full C-family function from a caller-supplied signature line and the shared body. */
|
|
491
|
-
export function emitCFamily(signature: string,
|
|
561
|
+
export function emitCFamily(signature: string, fn0: SFn, leaf?: LeafHook): string {
|
|
562
|
+
// The declaration list is put into the target's own frame order HERE — in the shared C-family
|
|
563
|
+
// assembler, reached only from `cBackend.emit` and `cppBackend(...).emit`, so both C-family
|
|
564
|
+
// backends order and no `.emit(` call site does (l3/slotorder.ts says why `emit` owns it).
|
|
565
|
+
const fn = orderSlotLocals(fn0);
|
|
492
566
|
const decls = structDecls(fn);
|
|
493
567
|
const preamble = decls.length ? decls.join('\n') + '\n' : '';
|
|
494
568
|
return preamble + [`${signature} {`, ...cFamilyBody(fn, leaf), '}'].join('\n') + '\n';
|