@asmlift/core 0.5.0 → 0.7.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 +270 -171
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +243 -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 +78 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +345 -2
- package/src/ir/opcodes.ts +176 -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/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -5
- package/src/l3/ast.ts +510 -59
- package/src/l3/basecse.ts +686 -78
- package/src/l3/coalesce.ts +432 -46
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +96 -1
- 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 +176 -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 +114 -89
- package/src/l3/reindex.ts +722 -80
- package/src/l3/scopebase.ts +649 -220
- 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 +16 -1
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +687 -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 +239 -16
- package/src/pipeline.ts +173 -60
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +4 -4
- package/src/raise/extscale.ts +342 -0
- package/src/raise/globalshape.ts +1058 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +572 -0
- package/src/raise/paramwidth.ts +201 -0
- package/src/raise/pre-recovery.ts +169 -21
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +585 -19
- package/src/raise/shortcircuit.ts +1050 -89
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +34 -4
- package/src/raise/tailsink.ts +126 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank-variations.ts +760 -0
- package/src/rank.ts +2122 -326
- package/src/structure/analysis.ts +1398 -150
- package/src/structure/bitfields.ts +432 -0
- package/src/structure/globalaccess.ts +300 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +454 -0
- package/src/structure/structure.ts +3979 -612
- package/src/structure/switch-recover.ts +710 -145
- package/src/symbols.ts +188 -6
- package/src/target.ts +495 -32
- package/src/trace.ts +112 -33
- package/src/variation-definitions.ts +1540 -0
- package/src/variation-gates.ts +89 -0
- package/src/variation-tokens.ts +355 -0
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') {
|
|
@@ -121,10 +80,18 @@ export function cType(t: IrType): string {
|
|
|
121
80
|
|
|
122
81
|
/** Declare a name of a given type, C declarator rules: an array puts its length AFTER the name
|
|
123
82
|
* (`u8 _pad[4]`), a pointer binds its `*` to the declarator (`void *p`), everything else is
|
|
124
|
-
* the prefix `cType name`.
|
|
83
|
+
* the prefix `cType name`. A NESTED array spells every extent after the name in declaration
|
|
84
|
+
* order (`u8 unk8[6][8]`) — one declarator, not an element type that is itself an array, which
|
|
85
|
+
* C has no syntax for and `cType` marks ill-formed as a prefix. */
|
|
125
86
|
function cDeclare(t: IrType, name: string): string {
|
|
126
87
|
if (t.kind === 'array') {
|
|
127
|
-
|
|
88
|
+
const extents: number[] = [];
|
|
89
|
+
let e: IrType = t;
|
|
90
|
+
while (e.kind === 'array') {
|
|
91
|
+
extents.push(e.count);
|
|
92
|
+
e = e.elem;
|
|
93
|
+
}
|
|
94
|
+
return `${cType(e)} ${name}${extents.map((n) => `[${n}]`).join('')}`;
|
|
128
95
|
}
|
|
129
96
|
if (t.kind === 'ptr') {
|
|
130
97
|
return `${cType(t.to)} *${name}`;
|
|
@@ -163,7 +130,92 @@ export function renderStructDecl(name: string, fields: StructFieldDecl[]): strin
|
|
|
163
130
|
// backend uses. The default (no hook) is byte-identical C.
|
|
164
131
|
export type LeafHook = (e: Expr, rec: (e: Expr, p: number) => string) => string | null;
|
|
165
132
|
|
|
166
|
-
|
|
133
|
+
/** C-FAMILY LEGALIZATION (owned here, per the width-carrying `index` node contract in l3/ast.ts):
|
|
134
|
+
* the BASE an `index` node prints through — a deref whose base does not render as a pointer/array
|
|
135
|
+
* STRIDING the access width is spelled through the honest reinterpret cast at that width, the
|
|
136
|
+
* machine semantics of the access. Materialized as a synthetic cast node so the spelling (text,
|
|
137
|
+
* precedence, parens) is exactly that of a tree-level cast, and returns the base UNCHANGED (===)
|
|
138
|
+
* when no cast is needed, which is what the multidimensional guard tests.
|
|
139
|
+
*
|
|
140
|
+
* AND IT CARRIES THE QUALIFIER. In C the access takes the OUTER type, so a plain cast over a
|
|
141
|
+
* base the tree declared volatile spells an MMIO access the compiler may CSE, reorder or drop —
|
|
142
|
+
* `((s32 *)(volatile u16 *)0x4000208)[i]` reads once where `((volatile s32 *)0x4000208)[i]`
|
|
143
|
+
* reads twice (agbcc 2.9-arm-000512, `-O2 -mthumb-interwork -Wimplicit -fhex-asm
|
|
144
|
+
* -fprologue-bugfix`), and referring to a volatile object through a non-volatile lvalue is
|
|
145
|
+
* undefined behaviour (C99 6.7.3p5). */
|
|
146
|
+
function legalizedIndexBase(ix: Extract<Expr, { k: 'index' }>, vt: PrintEnv): Expr {
|
|
147
|
+
// `baseElem` states the element type the base's own DECLARATION gives it, where the type walk
|
|
148
|
+
// cannot reconstruct one (a map-declared array MEMBER — see l3/ast.ts). It is a FALLBACK, not an
|
|
149
|
+
// override, and the order is the whole guard: the walk reads the tree in front of it, so
|
|
150
|
+
// wherever it answers at all it is the better answer and the stated one is not consulted.
|
|
151
|
+
// Inverted (`declared ?? walked`), a stale statement would stand — `derefStrideOk` cannot catch
|
|
152
|
+
// one, because it tests the STATED type against the access width and never against the base.
|
|
153
|
+
// No pass carries a statement onto a foreign base today (each refuses a `field` base by its own
|
|
154
|
+
// predicate: basecse `isHoistableBase`, scopebase/argbase `eligible`, nearbase's untouched
|
|
155
|
+
// `field` subtree, reindex's `base.k === 'var'`), but `mapExprChildren` spreads the field across
|
|
156
|
+
// an arbitrary base substitution, so that is five accidents rather than a check. This ordering
|
|
157
|
+
// is the check. Measured: zero test moves either way.
|
|
158
|
+
const declared = ix.baseElem !== undefined ? T.ptr(ix.baseElem) : undefined;
|
|
159
|
+
return derefStrideOk(exprCType(ix.base, vt.type) ?? declared, ix.width, ix.signed)
|
|
160
|
+
? ix.base
|
|
161
|
+
: {
|
|
162
|
+
k: 'cast',
|
|
163
|
+
to: T.ptr(scalarTypeForAccess(ix.width, ix.signed)),
|
|
164
|
+
...(assertsVolatile(ix.base, vt) ? { volatile: true as const } : {}),
|
|
165
|
+
e: ix.base,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The 32-bit integer cast that PINS an operand's signedness.
|
|
170
|
+
*
|
|
171
|
+
* An existing 32-bit integer cast is REPLACED rather than wrapped — `(u32)(s32)&g` and `(u32)&g`
|
|
172
|
+
* are the same bytes, and the arithmetic rules upstream do emit that inner cast (intifyAddr).
|
|
173
|
+
* The replacement CARRIES the qualifier: re-typing a `volatile` cast without it drops an
|
|
174
|
+
* assertion the differ cannot referee the loss of, which is why l3/initfirst.ts's
|
|
175
|
+
* `stripWideIntCast` refuses the same peel one pass over. */
|
|
176
|
+
function recast32(x: Expr, signed: boolean): Expr {
|
|
177
|
+
const replaced = x.k === 'cast' && x.to.kind === 'int' && x.to.width === 32 ? x : undefined;
|
|
178
|
+
return {
|
|
179
|
+
k: 'cast',
|
|
180
|
+
to: T.int(32, signed),
|
|
181
|
+
...(replaced?.volatile === true ? { volatile: true as const } : {}),
|
|
182
|
+
e: replaced ? replaced.e : x,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** C-FAMILY OPERAND-SIGNEDNESS LEGALIZATION, the same discipline as the deref cast one operator
|
|
187
|
+
* over. The tower keeps the signedness-carrying pairs apart (`>>>` logical / `>>` arithmetic,
|
|
188
|
+
* `/u` `%u` unsigned / `/` `%` signed); C spells each pair with ONE token and picks between them
|
|
189
|
+
* from the operand types. So the operands must be made to carry the choice, or an `shr_u`
|
|
190
|
+
* recompiles to `asr` where the target has `lsr` AND evaluates differently
|
|
191
|
+
* (`*(u8 *)&g << 30 >> 30` promotes to `int`, so a 2-bit field holding 2 comes out -1), and a
|
|
192
|
+
* `udiv` calls `__divsi3` where the target called `__udivsi3`.
|
|
193
|
+
*
|
|
194
|
+
* (engine.ts's zext fold covers the same hazard for widths C can NAME, by folding the whole
|
|
195
|
+
* shift pair to a cast op. Every other extract width — every bitfield read — lands here.)
|
|
196
|
+
*
|
|
197
|
+
* A SHIFT and a DIVIDE read their operands differently, so the pin does too. A SHIFT takes the
|
|
198
|
+
* type of its left operand alone, and the cast goes on unless that operand PROVABLY renders as
|
|
199
|
+
* the op needs (renderedIntSignedness's header carries the rule for reading `undefined`).
|
|
200
|
+
*
|
|
201
|
+
* A DIVIDE takes the usual arithmetic conversions over BOTH operands, where unsigned wins at
|
|
202
|
+
* equal rank, so the question is what the PAIR renders as. Once the pair renders wrong the two
|
|
203
|
+
* directions cost differently: unsigned takes ONE cast, which carries the whole operation, while
|
|
204
|
+
* signed has to pin EVERY operand short of a proof, because one unsigned side is enough to make
|
|
205
|
+
* the division unsigned. Verified by compiling: `((u32)a / b) / 7` calls `__udivsi3` twice,
|
|
206
|
+
* `(s32)((u32)a / b) / 7` calls `__udivsi3` then `__divsi3`. */
|
|
207
|
+
function pinnedOperands(e0: Extract<Expr, { k: 'bin' }>, wantSigned: boolean, vt: PrintEnv): [Expr, Expr] {
|
|
208
|
+
if (e0.op === '>>' || e0.op === '>>>') {
|
|
209
|
+
return [renderedIntSignedness(e0.l, vt.type) === wantSigned ? e0.l : recast32(e0.l, wantSigned), e0.r];
|
|
210
|
+
}
|
|
211
|
+
if (arithConversionSignedness(e0.l, e0.r, vt.type) === wantSigned) {
|
|
212
|
+
return [e0.l, e0.r];
|
|
213
|
+
}
|
|
214
|
+
const pinSigned = (x: Expr): Expr => (renderedIntSignedness(x, vt.type) === true ? x : recast32(x, true));
|
|
215
|
+
return wantSigned ? [pinSigned(e0.l), pinSigned(e0.r)] : [recast32(e0.l, false), e0.r];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function printExpr(e: Expr, parentPrec: number, vt: PrintEnv, leaf?: LeafHook): string {
|
|
167
219
|
const rec = (x: Expr, p: number) => printExpr(x, p, vt, leaf);
|
|
168
220
|
if (leaf) {
|
|
169
221
|
const s = leaf(e, rec);
|
|
@@ -171,37 +223,6 @@ function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook):
|
|
|
171
223
|
return s;
|
|
172
224
|
}
|
|
173
225
|
}
|
|
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
226
|
switch (e.k) {
|
|
206
227
|
case 'var':
|
|
207
228
|
return e.name;
|
|
@@ -221,21 +242,30 @@ function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook):
|
|
|
221
242
|
// `(*p)[1]`, never `*p[1]` which C groups as `*(p[1])`), `base[idx]` otherwise (POSTFIX —
|
|
222
243
|
// binds tighter than any prefix operator, so a cast/unary/deref base is printed at prec 1
|
|
223
244
|
// and parenthesizes itself: `((u8 *)p)[1]`). The postfix form needs no outer parentheses.
|
|
224
|
-
const base =
|
|
245
|
+
const base = legalizedIndexBase(e, vt);
|
|
225
246
|
// Leading constant subscripts (a multidimensional array global's bare spelling) keep the
|
|
226
247
|
// postfix form whatever `idx` is: `g[0][0]` is the element, `*g[0]` would be its ROW.
|
|
227
248
|
//
|
|
228
|
-
// `lead` implies the base already strides the access width
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
249
|
+
// `lead` implies the base already strides the access width, and its producers reach that by
|
|
250
|
+
// TWO different mechanisms. The two GLOBAL ones register a matching element type for the
|
|
251
|
+
// global (structure/globalaccess.ts `bareArrayLead` and `declaredSubscripts`, both through
|
|
252
|
+
// structure.ts's `noteGlobal`, so the type walk finds it); the MEMBER one
|
|
253
|
+
// (structure.ts `pointeeElement`, `gPtr->grid[0][i]`) has no global to register and states
|
|
254
|
+
// the element type on the node instead (`baseElem`). Nothing else enforced either, and the
|
|
255
|
+
// failure would be quiet-ish: legalization would wrap the base, spelling `((u16 *)g)[0][i]`,
|
|
256
|
+
// which subscripts a `u16` twice. Check it rather than assume.
|
|
257
|
+
//
|
|
258
|
+
// This throw covers rank >= 2 ONLY, because only a rank >= 2 access carries `lead`. A rank-1
|
|
259
|
+
// member (the large majority of the corpus's dims-carrying members) has no guard here: a
|
|
260
|
+
// missing or stale `baseElem` there degrades silently to the cast form the rule replaces,
|
|
261
|
+
// which is a lost spelling rather than a wrong address.
|
|
232
262
|
if (e.lead && e.lead.length > 0) {
|
|
233
263
|
if (base !== e.base) {
|
|
234
264
|
throw new Error(
|
|
235
265
|
`c backend: a multidimensional array access needs a base that strides ${e.width} bytes as spelled`,
|
|
236
266
|
);
|
|
237
267
|
}
|
|
238
|
-
return `${rec(base, 1)}${e.lead.map((l) => `[${l}]`).join('')}[${rec(e.idx, 99)}]`;
|
|
268
|
+
return `${rec(base, 1)}${e.lead.map((l) => `[${rec(l, 99)}]`).join('')}[${rec(e.idx, 99)}]`;
|
|
239
269
|
}
|
|
240
270
|
if (e.idx.k === 'const' && e.idx.value === 0) {
|
|
241
271
|
const s = `*${rec(base, 2)}`;
|
|
@@ -289,20 +319,25 @@ function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook):
|
|
|
289
319
|
// op. Under a POSTFIX parent ([]/->) the cast itself must parenthesize — `((struct S *)p)->f`,
|
|
290
320
|
// NOT `(struct S *)p->f` (which C parses as a cast OF the member access).
|
|
291
321
|
case 'cast': {
|
|
292
|
-
|
|
322
|
+
// `volatile` binds to the POINTEE (`(volatile u16 *)a`), which is where the qualifier goes
|
|
323
|
+
// on a raw-address access — the same prefix position the declaration printer uses for a
|
|
324
|
+
// pointer local's `pointeeVolatile`.
|
|
325
|
+
const s = `(${e.volatile ? 'volatile ' : ''}${cType(e.to)})${rec(e.e, 2)}`;
|
|
293
326
|
return parentPrec < 2 ? `(${s})` : s;
|
|
294
327
|
}
|
|
295
328
|
case 'bin': {
|
|
296
329
|
const p = PREC[e.op];
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
const
|
|
330
|
+
// Each signedness-carrying pair spells with ONE C token; `pinnedOperands` supplies the
|
|
331
|
+
// operand cast that says which of the pair it is.
|
|
332
|
+
const pair = C_SPELLING[e.op];
|
|
333
|
+
const [l, r] = pair ? pinnedOperands(e, pair.signed, vt) : [e.l, e.r];
|
|
334
|
+
const s = `${rec(l, p)} ${pair ? pair.token : e.op} ${rec(r, p - 1)}`;
|
|
300
335
|
return p > parentPrec ? `(${s})` : s;
|
|
301
336
|
}
|
|
302
337
|
}
|
|
303
338
|
}
|
|
304
339
|
|
|
305
|
-
function printStmt(s: Stmt, indent: string, vt:
|
|
340
|
+
function printStmt(s: Stmt, indent: string, vt: PrintEnv, leaf?: LeafHook): string[] {
|
|
306
341
|
const pe = (e: Expr, p: number) => printExpr(e, p, vt, leaf);
|
|
307
342
|
switch (s.k) {
|
|
308
343
|
case 'assign':
|
|
@@ -355,8 +390,8 @@ function printStmt(s: Stmt, indent: string, vt: VarTypes, leaf?: LeafHook): stri
|
|
|
355
390
|
return out;
|
|
356
391
|
}
|
|
357
392
|
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
|
|
393
|
+
// `for (init; cond; inc) { body }`. PRECONDITION (guaranteed by both producers — structure.ts
|
|
394
|
+
// `recognizeForLoops` and l3/reindex.ts): init/inc are each a SINGLE-LINE `assign` statement. `clause` renders one
|
|
360
395
|
// and strips its trailing `;` so it sits inside the header (`i = 0; c; i = i + 1`). A multi-line
|
|
361
396
|
// statement (an `if`/nested loop) would render mangled — but the recognizer never builds one here.
|
|
362
397
|
const clause = (st: Stmt) => printStmt(st, '', vt, leaf).join(' ').replace(/;\s*$/, '').trim();
|
|
@@ -371,36 +406,98 @@ function printStmt(s: Stmt, indent: string, vt: VarTypes, leaf?: LeafHook): stri
|
|
|
371
406
|
return [`${indent}break;`];
|
|
372
407
|
case 'continue':
|
|
373
408
|
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
|
-
|
|
409
|
+
case 'switch':
|
|
410
|
+
return printSwitchStmt(s, indent, vt, leaf);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** A C `switch`: the arm bodies, the `default:` label's position among them, and the three
|
|
415
|
+
* producer-contract refusals that stand between an L3 `switch` node and valid C. Its own function
|
|
416
|
+
* because it is the one statement kind whose printing is a program of its own; `printStmt`'s arm
|
|
417
|
+
* is one call. */
|
|
418
|
+
function printSwitchStmt(s: Extract<Stmt, { k: 'switch' }>, indent: string, vt: PrintEnv, leaf?: LeafHook): string[] {
|
|
419
|
+
const pe = (e: Expr, p: number) => printExpr(e, p, vt, leaf);
|
|
420
|
+
// A `break;` in an arm BODY is the innermost LOOP's in L3 (l3/ast.ts) and the SWITCH's in C,
|
|
421
|
+
// so printing one between `case` labels rebinds it — a changed program that reads as ordinary
|
|
422
|
+
// C. Refused HERE because the rebinding is the printer's, for every producer rather than for
|
|
423
|
+
// the two switch regimes. No recovery reaches it today (both regimes decline a loop-exiting
|
|
424
|
+
// arm first, loudly), so it is a contract on the next one.
|
|
425
|
+
//
|
|
426
|
+
// `continue;` is deliberately NOT refused: C binds it to the smallest enclosing ITERATION
|
|
427
|
+
// statement, which a `switch` is not, so it already means what L3 means. What a pass that re-spells
|
|
428
|
+
// a loop must preserve is exactly that — the three that can re-spell a loop into one whose
|
|
429
|
+
// `continue` would run a different increment (`recognizeForLoops` in structure.ts,
|
|
430
|
+
// `respellCountdown` in l3/reindex.ts, and l3/unreduce.ts) each scan switch arms for the node
|
|
431
|
+
// before firing, and a fourth must too.
|
|
432
|
+
for (const body of [...s.cases.map((c) => c.body), s.default ?? []]) {
|
|
433
|
+
if (switchBoundBreakIn(body)) {
|
|
434
|
+
throw new Error('c backend: a switch arm carries a loop-scoped `break;`, which C would bind to the switch');
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const out = [`${indent}switch (${pe(s.scrutinee, 99)}) {`];
|
|
438
|
+
const ci = indent + ' '; // case-label indent
|
|
439
|
+
const bi = indent + ' '; // case-body indent
|
|
440
|
+
// `?.length`, not just presence: a label with no statement under it is not valid C89, and an
|
|
441
|
+
// L3 pass (dce, reindex) may empty a default that arrived with statements — the structurer's
|
|
442
|
+
// own "don't attach an empty default" rule cannot see that.
|
|
443
|
+
const hasDefault = !!s.default?.length;
|
|
444
|
+
// Where the `default:` label goes, as a COUNT of case arms before it (l3/ast.ts): absent ⇒
|
|
445
|
+
// after all of them. A count past the arms matches no position at all and the label would
|
|
446
|
+
// simply not be printed — the default arm vanishing from a switch that has one — so a
|
|
447
|
+
// producer that hands one over fails loud, like the falling-arm placement below.
|
|
448
|
+
const defAt = hasDefault ? (s.defaultAt ?? s.cases.length) : -1;
|
|
449
|
+
if (hasDefault && (defAt < 0 || defAt > s.cases.length)) {
|
|
450
|
+
throw new Error(`c backend: a switch places its default at arm ${defAt} of ${s.cases.length}`);
|
|
451
|
+
}
|
|
452
|
+
const printDefault = (): void => {
|
|
453
|
+
out.push(`${ci}default:`);
|
|
454
|
+
for (const t of s.default!) {
|
|
455
|
+
out.push(...printStmt(t, bi, vt, leaf));
|
|
456
|
+
}
|
|
457
|
+
// A default that is NOT last would otherwise fall into the case below it — the mirror of the
|
|
458
|
+
// rule for cases. The last one needs no `break;` because there is nothing under it.
|
|
459
|
+
if (defAt < s.cases.length && !endsTerminated(s.default!)) {
|
|
460
|
+
out.push(`${bi}break;`);
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
s.cases.forEach((c, i) => {
|
|
464
|
+
if (i === defAt) {
|
|
465
|
+
// Moving the label in FRONT of a falling arm would divert that arm into the default —
|
|
466
|
+
// a silent control-flow change. Recovery only positions a default among closed arms, so
|
|
467
|
+
// this is a producer bug rather than an input shape: fail loud.
|
|
468
|
+
if (i > 0 && s.cases[i - 1].fallsThrough) {
|
|
469
|
+
throw new Error(
|
|
470
|
+
`c backend: a switch places its default after a case that falls through, which would divert it`,
|
|
471
|
+
);
|
|
399
472
|
}
|
|
400
|
-
|
|
401
|
-
|
|
473
|
+
printDefault();
|
|
474
|
+
}
|
|
475
|
+
for (const v of c.values) {
|
|
476
|
+
out.push(`${ci}case ${v}:`);
|
|
477
|
+
}
|
|
478
|
+
for (const t of c.body) {
|
|
479
|
+
out.push(...printStmt(t, bi, vt, leaf));
|
|
402
480
|
}
|
|
481
|
+
// A case whose body ends in `return`/`break` (a terminated arm) needs no `break;`; only an
|
|
482
|
+
// open non-fall-through arm gets one. `fallsThrough` omits it so control drops to the next case.
|
|
483
|
+
if (!c.fallsThrough && !endsTerminated(c.body)) {
|
|
484
|
+
out.push(`${bi}break;`);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
if (defAt === s.cases.length) {
|
|
488
|
+
printDefault();
|
|
403
489
|
}
|
|
490
|
+
out.push(`${indent}}`);
|
|
491
|
+
return out;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Does this arm body carry a `break` C would bind to the enclosing SWITCH rather than to the loop
|
|
495
|
+
* L3 meant? A loop opened inside the arm captures its own, so its body is not walked; a nested
|
|
496
|
+
* `switch` captures one too and is checked when it is printed. */
|
|
497
|
+
function switchBoundBreakIn(body: Stmt[]): boolean {
|
|
498
|
+
return body.some(
|
|
499
|
+
(s) => s.k === 'break' || (s.k === 'if' && (switchBoundBreakIn(s.then) || switchBoundBreakIn(s.else))),
|
|
500
|
+
);
|
|
404
501
|
}
|
|
405
502
|
|
|
406
503
|
// Does a statement list end in a control-flow terminator (so a trailing `break;` would be dead)?
|
|
@@ -415,26 +512,15 @@ function endsTerminated(body: Stmt[]): boolean {
|
|
|
415
512
|
);
|
|
416
513
|
}
|
|
417
514
|
|
|
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
515
|
// 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
|
|
516
|
+
// the C family's answer to `writesNonPointerIntoPointer` is the reinterpret cast to the DECLARED
|
|
517
|
+
// type — exactly what the machine's register move does — where Pascal, having no such cast,
|
|
518
|
+
// declines. (A rebuilding transform with per-kind semantics — its own switch, per the l3/ast.ts
|
|
428
519
|
// traversal-vocabulary exemption.)
|
|
429
520
|
function legalizePointerWrites(fn: SFn): SFn {
|
|
430
521
|
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
|
-
};
|
|
522
|
+
const castTo = (t: IrType | undefined, e: Expr): Expr =>
|
|
523
|
+
writesNonPointerIntoPointer(t, e, vt) ? { k: 'cast', to: t, e } : e;
|
|
438
524
|
const fix = (s: Stmt): Stmt => {
|
|
439
525
|
switch (s.k) {
|
|
440
526
|
case 'assign':
|
|
@@ -466,14 +552,23 @@ function legalizePointerWrites(fn: SFn): SFn {
|
|
|
466
552
|
return { ...fn, body: fn.body.map(fix) };
|
|
467
553
|
}
|
|
468
554
|
|
|
555
|
+
/** The body of a C-family function: local declarations + statements, one string per line. The
|
|
556
|
+
* SIGNATURE (return type + name + params, plus any C++ scope/`this`/mangling) is the caller's —
|
|
557
|
+
* that is the language-divergent part each backend owns. */
|
|
469
558
|
function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
|
|
470
559
|
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
|
-
|
|
560
|
+
// The legalization env: every printed var's declared type and pointee volatility, from the SAME
|
|
561
|
+
// params/locals the emitted declarations come from — so the printer judges exactly the C the
|
|
562
|
+
// reader will see.
|
|
563
|
+
const vt: PrintEnv = printEnv(fn);
|
|
474
564
|
const lines: string[] = [];
|
|
475
565
|
for (const l of fn.locals) {
|
|
476
|
-
|
|
566
|
+
// Both facts render at the PREFIX position, where C's declarator grammar reads them
|
|
567
|
+
// differently: on a scalar the qualifier binds to the object (`volatile u16 sp0`), on a
|
|
568
|
+
// pointer declarator to the pointee — the INNERMOST one for a multi-level pointer
|
|
569
|
+
// (`volatile u16 ** p`). An object-volatile POINTER (`u16 *volatile p`) has no inhabitant —
|
|
570
|
+
// no variation or recognizer produces one.
|
|
571
|
+
lines.push(` ${l.volatile || l.pointeeVolatile ? 'volatile ' : ''}${cType(l.type)} ${l.name};`);
|
|
477
572
|
}
|
|
478
573
|
for (const s of fn.body) {
|
|
479
574
|
lines.push(...printStmt(s, ' ', vt, leaf));
|
|
@@ -491,7 +586,11 @@ function structDecls(fn: SFn): string[] {
|
|
|
491
586
|
}
|
|
492
587
|
|
|
493
588
|
/** Assemble a full C-family function from a caller-supplied signature line and the shared body. */
|
|
494
|
-
export function emitCFamily(signature: string,
|
|
589
|
+
export function emitCFamily(signature: string, fn0: SFn, leaf?: LeafHook): string {
|
|
590
|
+
// The declaration list is put into the target's own frame order HERE — in the shared C-family
|
|
591
|
+
// assembler, reached only from `cBackend.emit` and `cppBackend(...).emit`, so both C-family
|
|
592
|
+
// backends order and no `.emit(` call site does (l3/slotorder.ts says why `emit` owns it).
|
|
593
|
+
const fn = orderSlotLocals(fn0);
|
|
495
594
|
const decls = structDecls(fn);
|
|
496
595
|
const preamble = decls.length ? decls.join('\n') + '\n' : '';
|
|
497
596
|
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.
|