@asmlift/core 0.5.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 -167
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +194 -39
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +11 -0
- package/src/frontend/ppc.ts +43 -7
- package/src/frontend/ssa.ts +404 -29
- package/src/frontend/thumb.ts +2176 -686
- package/src/ir/alias.ts +54 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +140 -21
- 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 +2 -1
- package/src/l3/ast.ts +464 -57
- package/src/l3/basecse.ts +664 -76
- package/src/l3/coalesce.ts +429 -43
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +21 -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 +644 -218
- 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 +15 -0
- 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 +157 -56
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +33 -18
- 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 +97 -14
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +210 -10
- package/src/raise/shortcircuit.ts +474 -74
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +33 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1723 -272
- package/src/structure/analysis.ts +1392 -141
- 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 +2678 -526
- package/src/structure/switch-recover.ts +616 -144
- package/src/symbols.ts +62 -1
- package/src/target.ts +367 -24
- package/src/trace.ts +111 -32
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,36 +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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
+
);
|
|
399
444
|
}
|
|
400
|
-
|
|
401
|
-
|
|
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));
|
|
402
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();
|
|
403
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
|
+
);
|
|
404
473
|
}
|
|
405
474
|
|
|
406
475
|
// Does a statement list end in a control-flow terminator (so a trailing `break;` would be dead)?
|
|
@@ -415,26 +484,15 @@ function endsTerminated(body: Stmt[]): boolean {
|
|
|
415
484
|
);
|
|
416
485
|
}
|
|
417
486
|
|
|
418
|
-
/** The body of a C-family function: local declarations + statements, one string per line. The
|
|
419
|
-
* SIGNATURE (return type + name + params, plus any C++ scope/`this`/mangling) is the caller's —
|
|
420
|
-
* that is the language-divergent part each backend owns. */
|
|
421
487
|
// C-FAMILY WRITE LEGALIZATION (the assign-side sibling of the deref legalization in printExpr):
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
// is the reinterpret cast to the DECLARED type, exactly what the machine's register move does.
|
|
426
|
-
// Unknowable renderings (calls) are left alone: their C type comes from prototypes outside this
|
|
427
|
-
// 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
|
|
428
491
|
// traversal-vocabulary exemption.)
|
|
429
492
|
function legalizePointerWrites(fn: SFn): SFn {
|
|
430
493
|
const vt = declaredTypes(fn);
|
|
431
|
-
const castTo = (t: IrType | undefined, e: Expr): Expr =>
|
|
432
|
-
|
|
433
|
-
return e;
|
|
434
|
-
}
|
|
435
|
-
const ct = exprCType(e, vt);
|
|
436
|
-
return ct && ct.kind !== 'ptr' && ct.kind !== 'array' ? { k: 'cast', to: t, e } : e;
|
|
437
|
-
};
|
|
494
|
+
const castTo = (t: IrType | undefined, e: Expr): Expr =>
|
|
495
|
+
writesNonPointerIntoPointer(t, e, vt) ? { k: 'cast', to: t, e } : e;
|
|
438
496
|
const fix = (s: Stmt): Stmt => {
|
|
439
497
|
switch (s.k) {
|
|
440
498
|
case 'assign':
|
|
@@ -466,14 +524,23 @@ function legalizePointerWrites(fn: SFn): SFn {
|
|
|
466
524
|
return { ...fn, body: fn.body.map(fix) };
|
|
467
525
|
}
|
|
468
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. */
|
|
469
530
|
function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
|
|
470
531
|
const fn = legalizePointerWrites(fn0);
|
|
471
|
-
// The legalization env: every printed var's declared type, from the SAME
|
|
472
|
-
// emitted declarations come from — so the printer judges exactly the C the
|
|
473
|
-
|
|
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);
|
|
474
536
|
const lines: string[] = [];
|
|
475
537
|
for (const l of fn.locals) {
|
|
476
|
-
|
|
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};`);
|
|
477
544
|
}
|
|
478
545
|
for (const s of fn.body) {
|
|
479
546
|
lines.push(...printStmt(s, ' ', vt, leaf));
|
|
@@ -491,7 +558,11 @@ function structDecls(fn: SFn): string[] {
|
|
|
491
558
|
}
|
|
492
559
|
|
|
493
560
|
/** Assemble a full C-family function from a caller-supplied signature line and the shared body. */
|
|
494
|
-
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);
|
|
495
566
|
const decls = structDecls(fn);
|
|
496
567
|
const preamble = decls.length ? decls.join('\n') + '\n' : '';
|
|
497
568
|
return preamble + [`${signature} {`, ...cFamilyBody(fn, leaf), '}'].join('\n') + '\n';
|
package/src/backend/cpp.ts
CHANGED
|
@@ -38,6 +38,7 @@ export function cppSymbol(spec: CppFnSpec): string {
|
|
|
38
38
|
export function cppBackend(spec: CppFnSpec): LanguageBackend {
|
|
39
39
|
return {
|
|
40
40
|
id: 'cpp',
|
|
41
|
+
spellsSwitchFallthrough: true,
|
|
41
42
|
emit(fn: SFn): string {
|
|
42
43
|
// Map each lifted param var → its C++ meaning: `this` (bare member access) or a named param
|
|
43
44
|
// (a pointer-to-class param uses `->`). A pointer-to-known-class param is a member receiver.
|
package/src/backend/pascal.ts
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
// Turbo/Delphi/FreePascal.
|
|
11
11
|
import { IrType, typeToString } from '../ir/types';
|
|
12
12
|
import { BinOp, Expr, LanguageBackend, SFn, Stmt } from '../l3/ast';
|
|
13
|
-
import {
|
|
13
|
+
import { orderSlotLocals } from '../l3/slotorder';
|
|
14
|
+
import { type VarTypes, declaredTypes, derefStrideOk, exprCType, writesNonPointerIntoPointer } from '../l3/typing';
|
|
14
15
|
|
|
15
16
|
// Infix operators IDO Pascal spells directly.
|
|
16
17
|
const OP: Partial<Record<BinOp, string>> = {
|
|
@@ -18,6 +19,10 @@ const OP: Partial<Record<BinOp, string>> = {
|
|
|
18
19
|
// match C's truncated `%` (sign of the DIVIDEND) — verified: `a mod 3` mis-scores against the
|
|
19
20
|
// IDO C `a % 3` codegen. There is no faithful IDO-Pascal spelling of a signed C remainder, so the
|
|
20
21
|
// backend fails LOUD on `%` (below) rather than emit a silently-wrong `mod`. `/`→`div` DOES match.
|
|
22
|
+
//
|
|
23
|
+
// And no `/u`/`%u` either, for the same reason `>>>` is absent from BIT_FN below: `div` over this
|
|
24
|
+
// backend's signed `Integer` is the SIGNED division, so lending it to the unsigned twin would
|
|
25
|
+
// emit `div` where the machine did `divu`. They reach the loud decline instead.
|
|
21
26
|
'+': '+',
|
|
22
27
|
'-': '-',
|
|
23
28
|
'*': '*',
|
|
@@ -95,7 +100,7 @@ function makePrinter(vt: VarTypes) {
|
|
|
95
100
|
throw new Error(`pascal backend: a multidimensional array access has no IDO Pascal spelling yet`);
|
|
96
101
|
}
|
|
97
102
|
const bt = exprCType(e.base, vt);
|
|
98
|
-
if ((bt !== undefined && !derefStrideOk(bt, e.width)) || (bt === undefined && e.width !== 4)) {
|
|
103
|
+
if ((bt !== undefined && !derefStrideOk(bt, e.width, e.signed)) || (bt === undefined && e.width !== 4)) {
|
|
99
104
|
throw new Error(
|
|
100
105
|
`pascal backend: a ${e.width}-byte access through a base of type '${bt ? typeToString(bt) : '<unknowable>'}' has no faithful spelling (no reinterpret cast)`,
|
|
101
106
|
);
|
|
@@ -111,7 +116,10 @@ function makePrinter(vt: VarTypes) {
|
|
|
111
116
|
// Casts have no faithful IDO-Pascal spelling yet — fail LOUD rather than emit silently-wrong
|
|
112
117
|
// source. Tree-level producers reaching here: the width-narrowing idiom casts (agbcc-gated,
|
|
113
118
|
// so never on this path today), structure.ts's STRUCT-pointer casts (unreachable too — the
|
|
114
|
-
// `field` case above throws first),
|
|
119
|
+
// `field` case above throws first), intify's `(s32)ptr` legalization (any target), and the
|
|
120
|
+
// byte-pointer walk of a pointer offset by a runtime value (any target, and reachable with no
|
|
121
|
+
// `field` in the tree — it declines two functions that used to emit here, whose Pascal was
|
|
122
|
+
// silently walking ELEMENTS where the asm walked bytes).
|
|
115
123
|
// Scalar deref casts never appear in the tree — the index case above owns that judgment.
|
|
116
124
|
case 'cast':
|
|
117
125
|
throw new Error(`pascal backend: cast has no IDO Pascal spelling yet`);
|
|
@@ -148,15 +156,13 @@ function makePrinter(vt: VarTypes) {
|
|
|
148
156
|
stmts.flatMap((x, i) => ps(fnName, x, ind, tl && i === stmts.length - 1));
|
|
149
157
|
switch (s.k) {
|
|
150
158
|
case 'assign': {
|
|
151
|
-
// The write-side sibling of the index case's deref discipline
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const ct = exprCType(s.value, vt);
|
|
157
|
-
if (dt?.kind === 'ptr' && ct && ct.kind !== 'ptr' && ct.kind !== 'array') {
|
|
159
|
+
// The write-side sibling of the index case's deref discipline. The C family answers the
|
|
160
|
+
// same question (l3/typing.ts writesNonPointerIntoPointer) with a reinterpret cast;
|
|
161
|
+
// Pascal has none, so it declines LOUD here instead of failing three stages later in upas.
|
|
162
|
+
if (writesNonPointerIntoPointer(vt(s.name), s.value, vt)) {
|
|
163
|
+
const ct = exprCType(s.value, vt);
|
|
158
164
|
throw new Error(
|
|
159
|
-
`pascal backend: assigning a ${typeToString(ct)} value into pointer var '${s.name}' has no faithful spelling (no reinterpret cast)`,
|
|
165
|
+
`pascal backend: assigning a ${ct ? typeToString(ct) : '<unknowable>'} value into pointer var '${s.name}' has no faithful spelling (no reinterpret cast)`,
|
|
160
166
|
);
|
|
161
167
|
}
|
|
162
168
|
return [`${indent}${s.name} := ${pe(s.value)};`];
|
|
@@ -263,7 +269,15 @@ function makePrinter(vt: VarTypes) {
|
|
|
263
269
|
|
|
264
270
|
export const pascalBackend: LanguageBackend = {
|
|
265
271
|
id: 'pascal',
|
|
266
|
-
|
|
272
|
+
// `case-of` has no fall-through, and this file's `switch` printing loud-fails a `fallsThrough`
|
|
273
|
+
// arm. Declared so RECOVERY never mints one for this backend: a comparison-tree switch also
|
|
274
|
+
// spells as plain if-nesting, which Pascal prints, so the choice is between a decompiled
|
|
275
|
+
// function and a stub.
|
|
276
|
+
spellsSwitchFallthrough: false,
|
|
277
|
+
emit(fn0: SFn): string {
|
|
278
|
+
// The declaration list is put into the target's own frame order HERE, as the C family does it
|
|
279
|
+
// in its shared assembler — owned by `emit`, never by a `.emit(` call site (l3/slotorder.ts).
|
|
280
|
+
const fn = orderSlotLocals(fn0);
|
|
267
281
|
// Same env discipline as the C family (cfamily.ts cFamilyBody): the printer judges derefs
|
|
268
282
|
// against the exact declarations it emits.
|
|
269
283
|
const ps = makePrinter(declaredTypes(fn));
|