@asmlift/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/package.json +14 -0
- package/src/backend/c.ts +20 -0
- package/src/backend/cfamily.ts +352 -0
- package/src/backend/cpp.ts +145 -0
- package/src/backend/pascal.ts +279 -0
- package/src/contracts.ts +131 -0
- package/src/detect.ts +12 -0
- package/src/frontend/asmdata.ts +170 -0
- package/src/frontend/disasm.ts +102 -0
- package/src/frontend/emit.ts +57 -0
- package/src/frontend/errors.ts +14 -0
- package/src/frontend/format.ts +47 -0
- package/src/frontend/frontend.ts +22 -0
- package/src/frontend/mips.ts +875 -0
- package/src/frontend/opaque.ts +82 -0
- package/src/frontend/ppc.ts +990 -0
- package/src/frontend/registry.ts +34 -0
- package/src/frontend/ssa.ts +214 -0
- package/src/frontend/thumb.ts +1419 -0
- package/src/ir/core.ts +104 -0
- package/src/ir/opcodes.ts +143 -0
- package/src/ir/parse.ts +221 -0
- package/src/ir/print.ts +77 -0
- package/src/ir/types.ts +106 -0
- package/src/ir/verify.ts +221 -0
- package/src/l3/ast.ts +301 -0
- package/src/l3/basecse.ts +218 -0
- package/src/l3/dce.ts +256 -0
- package/src/l3/regspell.ts +331 -0
- package/src/l3/reindex.ts +447 -0
- package/src/l3/typing.ts +145 -0
- package/src/mangle.ts +135 -0
- package/src/pattern/engine.ts +392 -0
- package/src/pipeline.ts +272 -0
- package/src/proto.ts +42 -0
- package/src/raise/arrays.ts +84 -0
- package/src/raise/const.ts +52 -0
- package/src/raise/errors.ts +10 -0
- package/src/raise/magicdiv.ts +386 -0
- package/src/raise/pre-recovery.ts +71 -0
- package/src/raise/recover.ts +215 -0
- package/src/raise/retsink.ts +72 -0
- package/src/raise/shortcircuit.ts +207 -0
- package/src/raise/softdiv.ts +62 -0
- package/src/raise/struct-arrays.ts +257 -0
- package/src/raise/structs.ts +223 -0
- package/src/rank.ts +208 -0
- package/src/structure/analysis.ts +410 -0
- package/src/structure/hazards.ts +142 -0
- package/src/structure/loops.ts +169 -0
- package/src/structure/structure.ts +1726 -0
- package/src/structure/switch-recover.ts +410 -0
- package/src/target.ts +140 -0
- package/src/trace.ts +233 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
// asmlift L3 — the walk→index RE-SPELLING, a differ-ranked representation lever.
|
|
2
|
+
//
|
|
3
|
+
// A compiler strength-reduces a source-level `arr[i]` loop into a pointer WALK (`*p; p += 1`),
|
|
4
|
+
// so asmlift's faithful lift of the machine form emits the walk — but recompiling the walk
|
|
5
|
+
// rarely reproduces the bytes the INDEXED source produced (different induction variable,
|
|
6
|
+
// different regalloc). Which representation the source used is genuinely ambiguous from asm —
|
|
7
|
+
// exactly the class of ambiguity asmlift resolves by CANDIDATES, not guesses (rank.ts: "types
|
|
8
|
+
// are differ-ranked levers"). This module produces the indexed re-spelling of a structured
|
|
9
|
+
// function; enumerateCandidates emits BOTH and the objdiff score referees.
|
|
10
|
+
//
|
|
11
|
+
// v1 SCOPE (decline over approximate): a loop is re-spelled only when ALL hold —
|
|
12
|
+
// • it is a `for`/`while` whose pointer induction var `p` (declared `T *`) steps by exactly
|
|
13
|
+
// ONE element (`p = p + 1`) as the loop's `inc` (for) / LAST body statement;
|
|
14
|
+
// • `p`'s init `p = <base>` is the `for` init or the statement immediately preceding the
|
|
15
|
+
// `while`/`dowhile`, with `<base>` a plain var that is never written inside the loop;
|
|
16
|
+
// • every other use of `p` in the loop is a deref base (`p[k]`) or the loop condition
|
|
17
|
+
// comparing `p` against `<base> + N` (the inlined bound shape — `p < base + n`);
|
|
18
|
+
// • `p` is not read after the loop (its post-loop value would be base + iterations).
|
|
19
|
+
// The rewrite: `i = 0` (a fresh s32 local) replaces the init, `p[k]` → `base[i + k]` (`base[i]`
|
|
20
|
+
// for k 0), the bound → `i <op> N`, the step → `i = i + 1`. Everything else declines — the
|
|
21
|
+
// function keeps only its walk spelling, and no candidate is emitted.
|
|
22
|
+
//
|
|
23
|
+
// MEASURED GAP (2026-07-17, benchmark survey): this v1 template fires on ZERO current nonmatch
|
|
24
|
+
// rows — the real agbcc shape for `for(i=0;i<n;i++) a[i]` is a GUARDED COUNTDOWN do-while with
|
|
25
|
+
// TWIN induction vars (`if (0 >= n) {...} else { p = a; k = n; do { ...*p...; p += 1; k -= 1 }
|
|
26
|
+
// while (k != 0) }`, e.g. synthetic:countpos at diff 5). Re-deriving the counted `for` from that
|
|
27
|
+
// form needs guard-branch merging + induction-variable unification — the v2 recognizer, a
|
|
28
|
+
// candidate for the capability-ROI queue. The MECHANISM (candidates + boundary contracts +
|
|
29
|
+
// differ referee) is what this module establishes; the recognizer set grows against measured
|
|
30
|
+
// shapes.
|
|
31
|
+
import { IrType, T } from '../ir/types';
|
|
32
|
+
import { Expr, SFn, Stmt, mapExprChildren, stmtExprs } from './ast';
|
|
33
|
+
|
|
34
|
+
interface WalkLoop {
|
|
35
|
+
p: string; // the pointer induction var
|
|
36
|
+
base: string; // the var `p` was initialised from
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Total mentions of `name` across a statement list (reads, writes, everywhere). */
|
|
40
|
+
function countMentions(stmts: Stmt[], name: string): number {
|
|
41
|
+
let n = 0;
|
|
42
|
+
const inExpr = (e: Expr): void => {
|
|
43
|
+
if (e.k === 'var' && e.name === name) {
|
|
44
|
+
n++;
|
|
45
|
+
}
|
|
46
|
+
mapExprChildren(e, (c) => {
|
|
47
|
+
inExpr(c);
|
|
48
|
+
return c;
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
const inStmt = (s: Stmt): void => {
|
|
52
|
+
if (s.k === 'assign' && s.name === name) {
|
|
53
|
+
n++;
|
|
54
|
+
}
|
|
55
|
+
stmtExprs(s).forEach(inExpr);
|
|
56
|
+
const kids: Stmt[] =
|
|
57
|
+
s.k === 'if'
|
|
58
|
+
? [...s.then, ...s.else]
|
|
59
|
+
: s.k === 'while' || s.k === 'dowhile'
|
|
60
|
+
? s.body
|
|
61
|
+
: s.k === 'for'
|
|
62
|
+
? [s.init, s.inc, ...s.body]
|
|
63
|
+
: s.k === 'switch'
|
|
64
|
+
? [...s.cases.flatMap((c) => c.body), ...(s.default ?? [])]
|
|
65
|
+
: [];
|
|
66
|
+
kids.forEach(inStmt);
|
|
67
|
+
};
|
|
68
|
+
stmts.forEach(inStmt);
|
|
69
|
+
return n;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The walk is sound to re-spell only when `base` and `p` are pointers of the SAME element size
|
|
73
|
+
* and every rewritten deref reads exactly that size — a stride disagreement makes the walk and
|
|
74
|
+
* the indexed form read DIFFERENT addresses (adversarially learned: `*(u8 *)p` over an `s32 *`
|
|
75
|
+
* walk strides 4; `((u8 *)base)[i]` strides 1). */
|
|
76
|
+
function strideAgrees(pT: IrType | undefined, baseT: IrType | undefined, derefWidths: number[]): boolean {
|
|
77
|
+
if (pT?.kind !== 'ptr' || baseT?.kind !== 'ptr') {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const es = pT.to.kind === 'int' ? pT.to.width / 8 : pT.to.kind === 'ptr' ? 4 : 0;
|
|
81
|
+
const bs = baseT.to.kind === 'int' ? baseT.to.width / 8 : baseT.to.kind === 'ptr' ? 4 : 0;
|
|
82
|
+
return es > 0 && es === bs && derefWidths.every((w) => w === es);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Every deref width of `p` in a statement list (for the stride check). */
|
|
86
|
+
function derefWidths(stmts: Stmt[], p: string): number[] {
|
|
87
|
+
const out: number[] = [];
|
|
88
|
+
const inExpr = (e: Expr): void => {
|
|
89
|
+
if (e.k === 'index' && e.base.k === 'var' && e.base.name === p) {
|
|
90
|
+
out.push(e.width);
|
|
91
|
+
}
|
|
92
|
+
mapExprChildren(e, (c) => {
|
|
93
|
+
inExpr(c);
|
|
94
|
+
return c;
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
const inStmt = (s: Stmt): void => {
|
|
98
|
+
stmtExprs(s).forEach(inExpr);
|
|
99
|
+
const kids: Stmt[] =
|
|
100
|
+
s.k === 'if'
|
|
101
|
+
? [...s.then, ...s.else]
|
|
102
|
+
: s.k === 'while' || s.k === 'dowhile'
|
|
103
|
+
? s.body
|
|
104
|
+
: s.k === 'for'
|
|
105
|
+
? [s.init, s.inc, ...s.body]
|
|
106
|
+
: s.k === 'switch'
|
|
107
|
+
? [...s.cases.flatMap((c) => c.body), ...(s.default ?? [])]
|
|
108
|
+
: [];
|
|
109
|
+
kids.forEach(inStmt);
|
|
110
|
+
};
|
|
111
|
+
stmts.forEach(inStmt);
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Does `e` mention var `name` anywhere? */
|
|
116
|
+
function mentionsVar(e: Expr, name: string): boolean {
|
|
117
|
+
if (e.k === 'var') {
|
|
118
|
+
return e.name === name;
|
|
119
|
+
}
|
|
120
|
+
let found = false;
|
|
121
|
+
mapExprChildren(e, (c) => {
|
|
122
|
+
found = found || mentionsVar(c, name);
|
|
123
|
+
return c;
|
|
124
|
+
});
|
|
125
|
+
return found;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function stmtMentions(s: Stmt, name: string): boolean {
|
|
129
|
+
if (s.k === 'assign' && s.name === name) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
const kids: Stmt[] =
|
|
133
|
+
s.k === 'if'
|
|
134
|
+
? [...s.then, ...s.else]
|
|
135
|
+
: s.k === 'while' || s.k === 'dowhile'
|
|
136
|
+
? s.body
|
|
137
|
+
: s.k === 'for'
|
|
138
|
+
? [s.init, s.inc, ...s.body]
|
|
139
|
+
: s.k === 'switch'
|
|
140
|
+
? [...s.cases.flatMap((c) => c.body), ...(s.default ?? [])]
|
|
141
|
+
: [];
|
|
142
|
+
return stmtExprs(s).some((e) => mentionsVar(e, name)) || kids.some((k) => stmtMentions(k, name));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** `assign(p, p + 1)` on a pointer-typed `p`? */
|
|
146
|
+
function isUnitStep(s: Stmt, ptrVars: Map<string, IrType>): string | null {
|
|
147
|
+
if (s.k !== 'assign' || !ptrVars.has(s.name)) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const v = s.value;
|
|
151
|
+
const ok =
|
|
152
|
+
v.k === 'bin' && v.op === '+' && v.l.k === 'var' && v.l.name === s.name && v.r.k === 'const' && v.r.value === 1;
|
|
153
|
+
return ok ? s.name : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Rewrite every deref of `p` into an indexed access off `base`, and every OTHER mention of `p`
|
|
157
|
+
* fails the walk (returns null): `p[k]` → `base[i + k]` (`base[i]` for k 0). */
|
|
158
|
+
function reindexExpr(e: Expr, walk: WalkLoop, iv: string): Expr | null {
|
|
159
|
+
if (e.k === 'var' && e.name === walk.p) {
|
|
160
|
+
return null; // a bare `p` outside a deref/condition — post-v1 shape, decline
|
|
161
|
+
}
|
|
162
|
+
if (e.k === 'index' && e.base.k === 'var' && e.base.name === walk.p) {
|
|
163
|
+
if (mentionsVar(e.idx, walk.p)) {
|
|
164
|
+
return null; // a p-dependent element offset — beyond the v1 shape
|
|
165
|
+
}
|
|
166
|
+
const idx: Expr =
|
|
167
|
+
e.idx.k === 'const' && e.idx.value === 0
|
|
168
|
+
? { k: 'var', name: iv }
|
|
169
|
+
: { k: 'bin', op: '+', l: { k: 'var', name: iv }, r: e.idx };
|
|
170
|
+
return { k: 'index', base: { k: 'var', name: walk.base }, idx, width: e.width, signed: e.signed };
|
|
171
|
+
}
|
|
172
|
+
let failed = false;
|
|
173
|
+
const out = mapExprChildren(e, (c) => {
|
|
174
|
+
const r = reindexExpr(c, walk, iv);
|
|
175
|
+
if (r === null) {
|
|
176
|
+
failed = true;
|
|
177
|
+
return c;
|
|
178
|
+
}
|
|
179
|
+
return r;
|
|
180
|
+
});
|
|
181
|
+
return failed ? null : out;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The loop bound `p <op> base + N` → `i <op> N`; `p <op> E` with any other E declines. */
|
|
185
|
+
function reindexCond(cond: Expr, walk: WalkLoop, iv: string): Expr | null {
|
|
186
|
+
if (cond.k !== 'bin' || !['<', '<=', '>', '>=', '==', '!='].includes(cond.op)) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const [pSide, bound, swap] =
|
|
190
|
+
cond.l.k === 'var' && cond.l.name === walk.p
|
|
191
|
+
? [cond.l, cond.r, false]
|
|
192
|
+
: cond.r.k === 'var' && cond.r.name === walk.p
|
|
193
|
+
? [cond.r, cond.l, true]
|
|
194
|
+
: [null, null, false];
|
|
195
|
+
if (!pSide || !bound) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
// bound must be the inlined `base + N` (N any p-free expr)
|
|
199
|
+
if (bound.k !== 'bin' || bound.op !== '+') {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
const n =
|
|
203
|
+
bound.l.k === 'var' && bound.l.name === walk.base
|
|
204
|
+
? bound.r
|
|
205
|
+
: bound.r.k === 'var' && bound.r.name === walk.base
|
|
206
|
+
? bound.l
|
|
207
|
+
: null;
|
|
208
|
+
if (!n || mentionsVar(n, walk.p)) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
const i: Expr = { k: 'var', name: iv };
|
|
212
|
+
return swap ? { k: 'bin', op: cond.op, l: n, r: i } : { k: 'bin', op: cond.op, l: i, r: n };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function reindexStmts(stmts: Stmt[], walk: WalkLoop, iv: string): Stmt[] | null {
|
|
216
|
+
const out: Stmt[] = [];
|
|
217
|
+
for (const s of stmts) {
|
|
218
|
+
const r = reindexStmt(s, walk, iv);
|
|
219
|
+
if (r === null) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
out.push(r);
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function reindexStmt(s: Stmt, walk: WalkLoop, iv: string): Stmt | null {
|
|
228
|
+
const rx = (e: Expr) => reindexExpr(e, walk, iv);
|
|
229
|
+
switch (s.k) {
|
|
230
|
+
case 'assign': {
|
|
231
|
+
if (s.name === walk.p || s.name === walk.base) {
|
|
232
|
+
return null;
|
|
233
|
+
} // writes beyond the recognized init/step: decline
|
|
234
|
+
const v = rx(s.value);
|
|
235
|
+
return v ? { ...s, value: v } : null;
|
|
236
|
+
}
|
|
237
|
+
case 'store': {
|
|
238
|
+
const lval = rx(s.lval);
|
|
239
|
+
const value = rx(s.value);
|
|
240
|
+
return lval && value ? { ...s, lval, value } : null;
|
|
241
|
+
}
|
|
242
|
+
case 'exprstmt': {
|
|
243
|
+
const v = rx(s.value);
|
|
244
|
+
return v ? { ...s, value: v } : null;
|
|
245
|
+
}
|
|
246
|
+
case 'return': {
|
|
247
|
+
if (!s.value) {
|
|
248
|
+
return s;
|
|
249
|
+
}
|
|
250
|
+
const v = rx(s.value);
|
|
251
|
+
return v ? { ...s, value: v } : null;
|
|
252
|
+
}
|
|
253
|
+
case 'if': {
|
|
254
|
+
const cond = rx(s.cond);
|
|
255
|
+
const then = reindexStmts(s.then, walk, iv);
|
|
256
|
+
const els = reindexStmts(s.else, walk, iv);
|
|
257
|
+
return cond && then && els ? { ...s, cond, then, else: els } : null;
|
|
258
|
+
}
|
|
259
|
+
// nested loops that MENTION the walk vars decline (their own ivs are out of v1 scope);
|
|
260
|
+
// p-free nested loops pass through untouched.
|
|
261
|
+
case 'while':
|
|
262
|
+
case 'dowhile':
|
|
263
|
+
return stmtMentions(s, walk.p) ? null : s;
|
|
264
|
+
case 'for':
|
|
265
|
+
return stmtMentions(s, walk.p) ? null : s;
|
|
266
|
+
case 'switch': {
|
|
267
|
+
const scrutinee = rx(s.scrutinee);
|
|
268
|
+
if (!scrutinee) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
const cases = s.cases.map((c) => ({ ...c, body: reindexStmts(c.body, walk, iv) }));
|
|
272
|
+
if (cases.some((c) => c.body === null)) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
const dflt = s.default ? reindexStmts(s.default, walk, iv) : undefined;
|
|
276
|
+
if (s.default && dflt === null) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
...s,
|
|
281
|
+
scrutinee,
|
|
282
|
+
cases: cases as { values: number[]; body: Stmt[]; fallsThrough: boolean }[],
|
|
283
|
+
default: dflt ?? undefined,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
case 'break':
|
|
287
|
+
case 'continue':
|
|
288
|
+
return s;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Try the walk→index re-spelling on one function. Returns the transformed COPY when at least
|
|
293
|
+
* one loop re-spelled, or null (no candidate) when nothing fired — callers emit the extra
|
|
294
|
+
* candidate only on non-null. Pure: never mutates the input SFn. */
|
|
295
|
+
export function reindexWalks(sfn: SFn): SFn | null {
|
|
296
|
+
const ptrVars = new Map<string, IrType>();
|
|
297
|
+
for (const v of [...sfn.params, ...sfn.locals]) {
|
|
298
|
+
if (v.type.kind === 'ptr') {
|
|
299
|
+
ptrVars.set(v.name, v.type);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (ptrVars.size === 0) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
let fired = 0;
|
|
307
|
+
let ivCount = 0;
|
|
308
|
+
const locals = [...sfn.locals];
|
|
309
|
+
|
|
310
|
+
// SOUNDNESS GATE (adversarially learned; every rule REPRODUCED as a wrong-bytes or crash
|
|
311
|
+
// escape without it):
|
|
312
|
+
// • p !== base (a self-walk's bound chases the stepped var — divergent trip counts);
|
|
313
|
+
// • base and p must be pointers of the SAME element size, and every deref of p must read
|
|
314
|
+
// exactly that size — otherwise the walk (strides p's pointee) and the indexed form
|
|
315
|
+
// (strides base's) read different addresses;
|
|
316
|
+
// • p must not be mentioned ANYWHERE in the function outside the init + the loop — the
|
|
317
|
+
// suffix-only check missed reads after an ENCLOSING construct, leaving the deleted init's
|
|
318
|
+
// var read uninitialized. Counted globally: total mentions == init + loop mentions.
|
|
319
|
+
const soundWalk = (walk: WalkLoop, initMentions: number, loop: Stmt): boolean =>
|
|
320
|
+
walk.p !== walk.base &&
|
|
321
|
+
strideAgrees(ptrVars.get(walk.p), ptrVars.get(walk.base) ?? paramType(walk.base), derefWidths([loop], walk.p)) &&
|
|
322
|
+
countMentions(sfn.body, walk.p) === initMentions + countMentions([loop], walk.p);
|
|
323
|
+
const paramType = (n: string): IrType | undefined => sfn.params.find((x) => x.name === n)?.type;
|
|
324
|
+
|
|
325
|
+
// walk a statement LIST so the `while` shape can see its preceding init statement
|
|
326
|
+
const walkList = (stmts: Stmt[]): Stmt[] => {
|
|
327
|
+
const out: Stmt[] = [];
|
|
328
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
329
|
+
const s = stmts[i];
|
|
330
|
+
// `for (p = base; p < base + n; p = p + 1)` — the self-contained shape
|
|
331
|
+
if (s.k === 'for') {
|
|
332
|
+
const p = isUnitStep(s.inc, ptrVars);
|
|
333
|
+
const init = s.init;
|
|
334
|
+
if (
|
|
335
|
+
p &&
|
|
336
|
+
init.k === 'assign' &&
|
|
337
|
+
init.name === p &&
|
|
338
|
+
init.value.k === 'var' &&
|
|
339
|
+
// init contributes 2 mentions (the write to p and the inc's read... the for init is
|
|
340
|
+
// part of the loop stmt itself, so count the whole `for` node) — see soundWalk
|
|
341
|
+
soundWalk({ p, base: init.value.name }, 0, s)
|
|
342
|
+
) {
|
|
343
|
+
const walk: WalkLoop = { p, base: init.value.name };
|
|
344
|
+
const iv = freshIv();
|
|
345
|
+
const cond = reindexCond(s.cond, walk, iv);
|
|
346
|
+
const body = cond ? reindexStmts(s.body, walk, iv) : null;
|
|
347
|
+
if (cond && body) {
|
|
348
|
+
out.push({
|
|
349
|
+
k: 'for',
|
|
350
|
+
init: { k: 'assign', name: iv, value: { k: 'const', value: 0 } },
|
|
351
|
+
cond,
|
|
352
|
+
inc: {
|
|
353
|
+
k: 'assign',
|
|
354
|
+
name: iv,
|
|
355
|
+
value: { k: 'bin', op: '+', l: { k: 'var', name: iv }, r: { k: 'const', value: 1 } },
|
|
356
|
+
},
|
|
357
|
+
body,
|
|
358
|
+
});
|
|
359
|
+
fired++;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
retireIv();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// `p = base; while (p < base + n) { …; p = p + 1; }`
|
|
366
|
+
if (s.k === 'while' && i > 0) {
|
|
367
|
+
const prev = out[out.length - 1];
|
|
368
|
+
const last = s.body[s.body.length - 1];
|
|
369
|
+
const p = last ? isUnitStep(last, ptrVars) : null;
|
|
370
|
+
if (
|
|
371
|
+
p &&
|
|
372
|
+
prev?.k === 'assign' &&
|
|
373
|
+
prev.name === p &&
|
|
374
|
+
prev.value.k === 'var' &&
|
|
375
|
+
soundWalk({ p, base: prev.value.name }, 1, s)
|
|
376
|
+
) {
|
|
377
|
+
const walk: WalkLoop = { p, base: prev.value.name };
|
|
378
|
+
const iv = freshIv();
|
|
379
|
+
const cond = reindexCond(s.cond, walk, iv);
|
|
380
|
+
const body = cond ? reindexStmts(s.body.slice(0, -1), walk, iv) : null;
|
|
381
|
+
if (cond && body) {
|
|
382
|
+
out[out.length - 1] = { k: 'assign', name: iv, value: { k: 'const', value: 0 } };
|
|
383
|
+
out.push({
|
|
384
|
+
k: 'while',
|
|
385
|
+
cond,
|
|
386
|
+
body: [
|
|
387
|
+
...body,
|
|
388
|
+
{
|
|
389
|
+
k: 'assign',
|
|
390
|
+
name: iv,
|
|
391
|
+
value: { k: 'bin', op: '+', l: { k: 'var', name: iv }, r: { k: 'const', value: 1 } },
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
});
|
|
395
|
+
fired++;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
retireIv();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// recurse into compound statements without re-spelling them
|
|
402
|
+
out.push(recurse(s));
|
|
403
|
+
}
|
|
404
|
+
return out;
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const recurse = (s: Stmt): Stmt => {
|
|
408
|
+
switch (s.k) {
|
|
409
|
+
case 'if':
|
|
410
|
+
return { ...s, then: walkList(s.then), else: walkList(s.else) };
|
|
411
|
+
case 'while':
|
|
412
|
+
case 'dowhile':
|
|
413
|
+
return { ...s, body: walkList(s.body) };
|
|
414
|
+
case 'for':
|
|
415
|
+
return { ...s, body: walkList(s.body) };
|
|
416
|
+
case 'switch':
|
|
417
|
+
return {
|
|
418
|
+
...s,
|
|
419
|
+
cases: s.cases.map((c) => ({ ...c, body: walkList(c.body) })),
|
|
420
|
+
default: s.default ? walkList(s.default) : undefined,
|
|
421
|
+
};
|
|
422
|
+
default:
|
|
423
|
+
return s;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
function freshIv(): string {
|
|
428
|
+
// collide-checked: pipeline naming is a*/v*/t*, but future naming (DWARF) may import
|
|
429
|
+
// real source names — never conflate with an existing i<N>.
|
|
430
|
+
let name = `i${ivCount++}`;
|
|
431
|
+
while (sfn.params.some((x) => x.name === name) || locals.some((x) => x.name === name)) {
|
|
432
|
+
name = `i${ivCount++}`;
|
|
433
|
+
}
|
|
434
|
+
locals.push({ name, type: T.s(32) });
|
|
435
|
+
return name;
|
|
436
|
+
}
|
|
437
|
+
function retireIv(): void {
|
|
438
|
+
locals.pop();
|
|
439
|
+
ivCount--;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const body = walkList(sfn.body);
|
|
443
|
+
if (!fired) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
return { ...sfn, locals, body };
|
|
447
|
+
}
|
package/src/l3/typing.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// asmlift L3 — the C-facing static type of a RENDERED expression.
|
|
2
|
+
//
|
|
3
|
+
// The IR carries recovered types on VALUES, but structuring renders a value as an EXPRESSION
|
|
4
|
+
// (a declared var, an inlined arithmetic tree, a literal), and the C type of that expression is
|
|
5
|
+
// what the compiler will actually see — which can disagree with the value's recovered type
|
|
6
|
+
// (`recoverTypes` may type an `add` result as a pointer while both its operands render as
|
|
7
|
+
// declared-`s32` vars, so the C type of `a0 + a1` is `int`, not `T *`). Every memory access the
|
|
8
|
+
// structurer emits derefs a RENDERED base, so its C-validity is decided by THIS type, not the
|
|
9
|
+
// value's. `exprCType` computes it bottom-up from the declared variable types.
|
|
10
|
+
//
|
|
11
|
+
// Contract: POINTER-NESS-accurate, not signedness-accurate. Callers use this to decide whether an
|
|
12
|
+
// expression is a C pointer (and of what pointee) — integer results are uniformly reported `s32`
|
|
13
|
+
// with NO promotion/unsignedness modeling, so this must never be consulted for signedness or
|
|
14
|
+
// width of integer arithmetic. Returns `undefined` when the type is not statically knowable here
|
|
15
|
+
// (a call — its C type comes from a prototype outside the emitted function; a gap marker; a var
|
|
16
|
+
// missing from the environment; an ill-typed shape like `ptr + ptr` or a deref of a non-pointer).
|
|
17
|
+
// Callers choose their conservative direction: the emission guard treats `undefined` as "not
|
|
18
|
+
// provably a pointer" (adds a cast — valid C either way); the deref contract treats `undefined`
|
|
19
|
+
// as "not provably wrong" (no error).
|
|
20
|
+
import { IrType, T, scalarTypeForAccess } from '../ir/types';
|
|
21
|
+
import type { Expr, SFn } from './ast';
|
|
22
|
+
|
|
23
|
+
/** The declared type of a printed variable — the env `exprCType` judges rendered C against.
|
|
24
|
+
* THE one copy of the SFn→env derivation (C printer, Pascal printer, deref contract): each
|
|
25
|
+
* consumer judging against anything but the declarations it emits would let them disagree. */
|
|
26
|
+
export type VarTypes = (name: string) => IrType | undefined;
|
|
27
|
+
|
|
28
|
+
export function declaredTypes(fn: SFn): VarTypes {
|
|
29
|
+
const m = new Map<string, IrType>();
|
|
30
|
+
for (const p of fn.params) {
|
|
31
|
+
m.set(p.name, p.type);
|
|
32
|
+
}
|
|
33
|
+
for (const l of fn.locals) {
|
|
34
|
+
m.set(l.name, l.type);
|
|
35
|
+
}
|
|
36
|
+
return (n) => m.get(n);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Byte size of a pointer's element, for C pointer-arithmetic scaling. A scalar (`int`) or pointer
|
|
40
|
+
// pointee has an unambiguous size; a struct/array/void pointee returns 0 = "do not scale" (the
|
|
41
|
+
// stride is the aggregate size or unknown — left raw rather than guessed). Lives here (not in
|
|
42
|
+
// ir/types.ts) because element scaling is a C-semantics fact, not an IR fact.
|
|
43
|
+
export function ptrElemBytes(to: IrType): number {
|
|
44
|
+
return to.kind === 'int' ? to.width / 8 : to.kind === 'ptr' ? 4 : 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** May a `width`-byte access dereference a base of rendered C type `rt` AS SPELLED — i.e. is `rt`
|
|
48
|
+
* a pointer/array whose element size equals the access width? THE one copy of the stride rule:
|
|
49
|
+
* the C-family printer decides cast insertion from it, the Pascal backend decides declining from
|
|
50
|
+
* it, and exprCType types the access result from it. `false` for a non-pointer, an unknowable
|
|
51
|
+
* base (undefined), or a pointer of the WRONG stride — a wrong-stride deref would make C read
|
|
52
|
+
* the wrong width and scale the index by the wrong element size. */
|
|
53
|
+
export function derefStrideOk(rt: IrType | undefined, width: number): boolean {
|
|
54
|
+
if (rt?.kind === 'ptr') {
|
|
55
|
+
return rt.to.kind !== 'struct' && ptrElemBytes(rt.to) === width;
|
|
56
|
+
}
|
|
57
|
+
if (rt?.kind === 'array') {
|
|
58
|
+
return rt.elem.kind === 'int' && rt.elem.width === width * 8;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function exprCType(e: Expr, varType: (name: string) => IrType | undefined): IrType | undefined {
|
|
64
|
+
const rec = (x: Expr): IrType | undefined => exprCType(x, varType);
|
|
65
|
+
switch (e.k) {
|
|
66
|
+
case 'var':
|
|
67
|
+
return varType(e.name);
|
|
68
|
+
// An integer literal spells as a plain C `int` — NEVER a pointer, whatever the value's
|
|
69
|
+
// recovered type was. This is the exact gap the emission guard exists to bridge.
|
|
70
|
+
case 'const':
|
|
71
|
+
return T.s(32);
|
|
72
|
+
case 'cast':
|
|
73
|
+
return e.to;
|
|
74
|
+
// `-`/`~` yield the promoted integer; `!` yields int. None yields a pointer.
|
|
75
|
+
case 'un':
|
|
76
|
+
return T.s(32);
|
|
77
|
+
case 'bin': {
|
|
78
|
+
if (e.op === '+' || e.op === '-') {
|
|
79
|
+
const l = rec(e.l);
|
|
80
|
+
const r = rec(e.r);
|
|
81
|
+
// C pointer arithmetic: ptr ± int is that pointer type; int + ptr commutes; ptr - ptr is
|
|
82
|
+
// an integer; ptr + ptr is not C at all (unknowable — the emitter legalizes it away).
|
|
83
|
+
// Anything else is the usual arithmetic int.
|
|
84
|
+
const lp = l?.kind === 'ptr';
|
|
85
|
+
const rp = r?.kind === 'ptr';
|
|
86
|
+
if (lp && rp) {
|
|
87
|
+
return e.op === '-' ? T.s(32) : undefined;
|
|
88
|
+
}
|
|
89
|
+
if (lp) {
|
|
90
|
+
return l;
|
|
91
|
+
}
|
|
92
|
+
if (rp && e.op === '+') {
|
|
93
|
+
return r;
|
|
94
|
+
}
|
|
95
|
+
return T.s(32);
|
|
96
|
+
}
|
|
97
|
+
// comparisons/logic yield int; *,/,%,&,|,^,<<,>> yield the arithmetic int.
|
|
98
|
+
return T.s(32);
|
|
99
|
+
}
|
|
100
|
+
// A callee's C return type comes from a prototype OUTSIDE the emitted function (the
|
|
101
|
+
// project ctx / C89 implicit int) — not statically knowable here.
|
|
102
|
+
case 'call':
|
|
103
|
+
return undefined;
|
|
104
|
+
case 'index': {
|
|
105
|
+
// `base[idx]` / `*base`: the element type of the base's pointer/array type when the base
|
|
106
|
+
// strides the access width AS RENDERED — otherwise the backend legalizes with a reinterpret
|
|
107
|
+
// cast at the access width, so the access reads exactly the node's scalar type. TOTAL: an
|
|
108
|
+
// index node always has a C type, because the carried width always yields a legal spelling.
|
|
109
|
+
//
|
|
110
|
+
// A STRUCT pointee is the dot-form exception: `arr[i]` on a `struct S *` base is a struct
|
|
111
|
+
// VALUE (the array element under a `.field` access; its width is the struct STRIDE, its
|
|
112
|
+
// legalization the tree-level struct cast) — falling through to the scalar default here
|
|
113
|
+
// would type it `s96`-style garbage and make the field contract reject valid trees. The
|
|
114
|
+
// node width must AGREE with the element size (when the struct declares one): a mismatch
|
|
115
|
+
// means the stride channel is corrupt, so it types scalar and the field contract flags it.
|
|
116
|
+
const bt = rec(e.base);
|
|
117
|
+
if (bt?.kind === 'ptr' && bt.to.kind === 'struct' && (bt.to.size === undefined || bt.to.size === e.width)) {
|
|
118
|
+
return bt.to;
|
|
119
|
+
}
|
|
120
|
+
if (bt?.kind === 'ptr' && derefStrideOk(bt, e.width)) {
|
|
121
|
+
return bt.to;
|
|
122
|
+
}
|
|
123
|
+
if (bt?.kind === 'array' && derefStrideOk(bt, e.width)) {
|
|
124
|
+
return bt.elem;
|
|
125
|
+
}
|
|
126
|
+
return scalarTypeForAccess(e.width, e.signed);
|
|
127
|
+
}
|
|
128
|
+
case 'field': {
|
|
129
|
+
// `base->name` (base: ptr-to-struct) or `base.name` (base: struct value, an array element).
|
|
130
|
+
const bt = rec(e.base);
|
|
131
|
+
const st = bt?.kind === 'ptr' ? bt.to : bt;
|
|
132
|
+
if (st?.kind !== 'struct') {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
return st.fields.find((f) => f.name === e.name)?.type;
|
|
136
|
+
}
|
|
137
|
+
case 'marker':
|
|
138
|
+
return undefined;
|
|
139
|
+
// `&gSym` is a pointer, but the global's type comes from the project headers — not knowable
|
|
140
|
+
// here. Callers treat undefined conservatively (the deref of an addr is simplified away in
|
|
141
|
+
// structure.ts before it reaches a legalization decision).
|
|
142
|
+
case 'addr':
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
}
|