@asmlift/core 0.9.0 → 0.11.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 +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +2 -0
- package/src/backend/cfamily.ts +197 -61
- package/src/backend/cpp.ts +3 -1
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +47 -16
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +246 -36
- package/src/declare.ts +69 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +9 -2
- package/src/frontend/call-plan.ts +1117 -61
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +33 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1689 -206
- package/src/frontend/frontend.ts +84 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +545 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2296 -217
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2748 -245
- package/src/frontend/register-struct-call.ts +399 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1721 -43
- package/src/frontend/stackargs.ts +199 -72
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +2400 -325
- package/src/ir/alias.ts +65 -2
- package/src/ir/bits.ts +1699 -4
- package/src/ir/core.ts +254 -9
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +157 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +137 -9
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +96 -11
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +582 -18
- package/src/l3/basecse.ts +68 -15
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +410 -0
- package/src/l3/dce.ts +53 -9
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +33 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +11 -4
- package/src/l3/reindex.ts +295 -99
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +6 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +1039 -38
- package/src/proto-context.ts +1039 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +422 -36
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +107 -21
- package/src/raise/pre-structure.ts +73 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +674 -0
- package/src/raise/shortcircuit.ts +217 -34
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unpre.ts +594 -0
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +186 -35
- package/src/rank.ts +536 -68
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +217 -8
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +157 -10
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +6192 -1213
- package/src/structure/switch-recover.ts +583 -120
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +58 -15
- package/src/target.ts +1028 -54
- package/src/trace.ts +49 -17
- package/src/variation-definitions.ts +333 -20
- package/src/variation-gates.ts +4 -0
- package/src/variation-tokens.ts +68 -3
package/src/cdecl/parse.ts
CHANGED
|
@@ -369,6 +369,27 @@ class Parser {
|
|
|
369
369
|
if (specifiers === null) {
|
|
370
370
|
return null;
|
|
371
371
|
}
|
|
372
|
+
// C89's implicit `int` ahead of a declarator list: `extern gN, gM;` and `static gA[4];` read the
|
|
373
|
+
// first name as a type word, and in valid C no type is followed by `,`, `=` or `[`. The word goes
|
|
374
|
+
// back to being the first declarator. A lone `extern gN;` is a statement this cannot tell from
|
|
375
|
+
// an unknown type declaring nothing; `proto-context.ts` reads that one against the types the
|
|
376
|
+
// context declares.
|
|
377
|
+
const { storage, type, unknownWords } = specifiers;
|
|
378
|
+
const next = this.t.char(this.i);
|
|
379
|
+
if (
|
|
380
|
+
!this.cxx &&
|
|
381
|
+
(storage.includes('extern') || storage.includes('static')) &&
|
|
382
|
+
type.kind === 'words' &&
|
|
383
|
+
type.words.length === 1 &&
|
|
384
|
+
!BASIC.has(type.words[0]) &&
|
|
385
|
+
unknownWords.length === 0 &&
|
|
386
|
+
(next === COMMA || next === EQUALS || next === OPEN_BRACKET) &&
|
|
387
|
+
this.t.text(this.i - 1) === type.words[0]
|
|
388
|
+
) {
|
|
389
|
+
this.i--;
|
|
390
|
+
specifiers.type = { kind: 'words', words: ['int'] };
|
|
391
|
+
specifiers.spelling = 'int';
|
|
392
|
+
}
|
|
372
393
|
const declarators: Declarator[] = [];
|
|
373
394
|
if (this.t.char(this.i) === SEMICOLON) {
|
|
374
395
|
this.i++;
|
package/src/contracts.ts
CHANGED
|
@@ -9,8 +9,9 @@ import { placedAt } from './ir/discipline';
|
|
|
9
9
|
import { MEM_BASE_OPS } from './ir/opcodes';
|
|
10
10
|
import { type IrType, memberOf, typeToString } from './ir/types';
|
|
11
11
|
import { cellAddress, qualifiedBase } from './l3/address';
|
|
12
|
-
import { type BinOp, type Expr, type SFn, type Stmt, calleeOf } from './l3/ast';
|
|
12
|
+
import { type BinOp, type Expr, type SFn, type Stmt, calleeOf, caseGotoViolation, switchArmSuccessors } from './l3/ast';
|
|
13
13
|
import {
|
|
14
|
+
assignsIn,
|
|
14
15
|
exprChildren,
|
|
15
16
|
fieldSpellsDot,
|
|
16
17
|
gapReasonFor,
|
|
@@ -19,9 +20,11 @@ import {
|
|
|
19
20
|
stmtChildren,
|
|
20
21
|
stmtExprs,
|
|
21
22
|
stmtLists,
|
|
23
|
+
taintOverAssigns,
|
|
22
24
|
walkExprs,
|
|
23
25
|
} from './l3/ast';
|
|
24
26
|
import { mentionedLocals } from './l3/mentions';
|
|
27
|
+
import { localAddresses } from './l3/storage';
|
|
25
28
|
import { declaredTypes, exprCType } from './l3/typing';
|
|
26
29
|
import { castOffDeclaration } from './l3/volstore';
|
|
27
30
|
|
|
@@ -188,17 +191,23 @@ function qualifiedAccessChain(e: Expr): boolean {
|
|
|
188
191
|
}
|
|
189
192
|
|
|
190
193
|
/** The global an access's address is named by: its base through casts, address arithmetic and the
|
|
191
|
-
* element or member a dot member selects from, down to
|
|
194
|
+
* element or member a dot member selects from, down to `&gSym`, or to a `gSym` that spells its own
|
|
195
|
+
* address — an array's name, which decays to it (`addresses`), or the struct a dot member selects
|
|
196
|
+
* from (`dotted`). Any other bare `gSym` is the object's VALUE: `*(u16 *)gPtr` reads what the
|
|
197
|
+
* pointer points at, not the pointer, so the access names no object. Null too when the base is
|
|
192
198
|
* anything else — another access, a local. */
|
|
193
|
-
function accessedObject(base: Expr): string | null {
|
|
199
|
+
function accessedObject(base: Expr, addresses: ReadonlySet<string>, dotted = false): string | null {
|
|
194
200
|
switch (base.k) {
|
|
195
201
|
case 'var':
|
|
202
|
+
return dotted || addresses.has(base.name) ? base.name : null;
|
|
196
203
|
case 'addr':
|
|
197
204
|
return base.name;
|
|
198
205
|
case 'cast':
|
|
199
|
-
return accessedObject(base.e);
|
|
206
|
+
return accessedObject(base.e, addresses);
|
|
200
207
|
case 'bin':
|
|
201
|
-
return base.op === '+' || base.op === '-'
|
|
208
|
+
return base.op === '+' || base.op === '-'
|
|
209
|
+
? (accessedObject(base.l, addresses) ?? accessedObject(base.r, addresses))
|
|
210
|
+
: null;
|
|
202
211
|
default:
|
|
203
212
|
return null;
|
|
204
213
|
}
|
|
@@ -211,6 +220,9 @@ interface TreeContext {
|
|
|
211
220
|
declared: ReadonlySet<string>;
|
|
212
221
|
written: ReadonlySet<string>;
|
|
213
222
|
typeOf: (e: Expr) => IrType | undefined;
|
|
223
|
+
/** The names whose bare spelling is the object's address: the array-declared globals the
|
|
224
|
+
* structurer subscripts by name (SFn.globals), and the array or struct statics it defines. */
|
|
225
|
+
addresses: ReadonlySet<string>;
|
|
214
226
|
}
|
|
215
227
|
|
|
216
228
|
/** every call under `e`, counted by target name; every qualified memory access, by the address it
|
|
@@ -231,7 +243,9 @@ function effectsInExpr(
|
|
|
231
243
|
): void {
|
|
232
244
|
const bump = (k: string) => into.set(k, (into.get(k) ?? 0) + 1);
|
|
233
245
|
const access = e.k === 'index' || e.k === 'field';
|
|
234
|
-
const
|
|
246
|
+
const inner = access && !selected ? dotSelectedAccess(e) : null;
|
|
247
|
+
const object =
|
|
248
|
+
inner === null ? null : accessedObject(inner.base, ctx.addresses, inner.k === 'field' && fieldSpellsDot(inner));
|
|
235
249
|
const ofDeclared = object !== null && !target && ctx.declared.has(object);
|
|
236
250
|
const writesDeclared = object !== null && target && ctx.written.has(object);
|
|
237
251
|
if (e.k === 'call') {
|
|
@@ -248,7 +262,9 @@ function effectsInExpr(
|
|
|
248
262
|
if (object !== null) {
|
|
249
263
|
bump(`${ofDeclared && castOffDeclaration(e, ctx.typeOf) ? STRIPPED : DECLARED}${object}`);
|
|
250
264
|
}
|
|
251
|
-
} else if (!
|
|
265
|
+
} else if (!target && e.k === 'var' && !(named && (selected || ctx.addresses.has(e.name)))) {
|
|
266
|
+
// A bare name is a read of the object's value unless it spells the address of an access, which
|
|
267
|
+
// counts it: the struct a dot member selects from, or an array's decayed name.
|
|
252
268
|
bump(`${DECLARED}${e.name}`);
|
|
253
269
|
}
|
|
254
270
|
const partOfThis = e.k === 'field' && fieldSpellsDot(e);
|
|
@@ -257,23 +273,27 @@ function effectsInExpr(
|
|
|
257
273
|
);
|
|
258
274
|
}
|
|
259
275
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
276
|
+
type Access = Extract<Expr, { k: 'index' } | { k: 'field' }>;
|
|
277
|
+
|
|
278
|
+
/** A dot member's innermost access, past the element or member it selects from: its base is the
|
|
279
|
+
* address of the one access the member spells. */
|
|
280
|
+
function dotSelectedAccess(e: Access): Access {
|
|
263
281
|
if (e.k === 'field' && fieldSpellsDot(e) && (e.base.k === 'index' || e.base.k === 'field')) {
|
|
264
|
-
return
|
|
282
|
+
return dotSelectedAccess(e.base);
|
|
265
283
|
}
|
|
266
|
-
return e
|
|
284
|
+
return e;
|
|
267
285
|
}
|
|
268
286
|
|
|
269
287
|
/** The executions on the paths through a statement list, split by how each path LEAVES it: falling off
|
|
270
|
-
* the end, or by a `return`, `break` or `
|
|
288
|
+
* the end, or by a `return`, `break`, `continue` or case-end `goto` (`end`, which leaves every loop
|
|
289
|
+
* up to its switch arm and then the switch). Null when no path leaves that way. Each is the
|
|
271
290
|
* per-name maximum over the paths of that kind. */
|
|
272
291
|
interface PathCounts {
|
|
273
292
|
through: EffectCounts | null;
|
|
274
293
|
ret: EffectCounts | null;
|
|
275
294
|
brk: EffectCounts | null;
|
|
276
295
|
cont: EffectCounts | null;
|
|
296
|
+
end: EffectCounts | null;
|
|
277
297
|
}
|
|
278
298
|
|
|
279
299
|
const sum = (a: EffectCounts, b: EffectCounts): EffectCounts => combine(a, b, (x, y) => x + y);
|
|
@@ -285,6 +305,7 @@ const after = (c: EffectCounts, p: PathCounts): PathCounts => ({
|
|
|
285
305
|
ret: p.ret && sum(c, p.ret),
|
|
286
306
|
brk: p.brk && sum(c, p.brk),
|
|
287
307
|
cont: p.cont && sum(c, p.cont),
|
|
308
|
+
end: p.end && sum(c, p.end),
|
|
288
309
|
});
|
|
289
310
|
/** `a` then `b`: only a path that falls off the end of `a` reaches `b` */
|
|
290
311
|
const seq = (a: PathCounts, b: PathCounts): PathCounts =>
|
|
@@ -295,6 +316,7 @@ const seq = (a: PathCounts, b: PathCounts): PathCounts =>
|
|
|
295
316
|
ret: most(a.ret, b.ret && sum(a.through, b.ret)),
|
|
296
317
|
brk: most(a.brk, b.brk && sum(a.through, b.brk)),
|
|
297
318
|
cont: most(a.cont, b.cont && sum(a.through, b.cont)),
|
|
319
|
+
end: most(a.end, b.end && sum(a.through, b.end)),
|
|
298
320
|
};
|
|
299
321
|
/** whichever of the alternatives runs */
|
|
300
322
|
const either = (...ps: PathCounts[]): PathCounts => ({
|
|
@@ -302,14 +324,18 @@ const either = (...ps: PathCounts[]): PathCounts => ({
|
|
|
302
324
|
ret: most(...ps.map((p) => p.ret)),
|
|
303
325
|
brk: most(...ps.map((p) => p.brk)),
|
|
304
326
|
cont: most(...ps.map((p) => p.cont)),
|
|
327
|
+
end: most(...ps.map((p) => p.end)),
|
|
305
328
|
});
|
|
306
|
-
const falls = (c: EffectCounts): PathCounts => ({ through: c, ret: null, brk: null, cont: null });
|
|
329
|
+
const falls = (c: EffectCounts): PathCounts => ({ through: c, ret: null, brk: null, cont: null, end: null });
|
|
307
330
|
|
|
308
331
|
/** `total` = every occurrence in the tree; `paths` = the most any single syntactic path executes,
|
|
309
332
|
* by how it leaves `stmts`. A path that leaves by `return` runs nothing after it, so two sequenced
|
|
310
333
|
* `if (…) { f(); return; }` arms are one call on any path, not two. `unreached` collects every
|
|
311
334
|
* statement that follows one no path falls past: the path count skips it, so the caller refuses it
|
|
312
|
-
* rather than let a duplicated region hide there.
|
|
335
|
+
* rather than let a duplicated region hide there. A labelled `break;` no path falls into is not
|
|
336
|
+
* one of them while a case-end `goto` in an earlier statement of the list is still leaving
|
|
337
|
+
* (`paths.end`): the goto lands on it, or on a labelled `break;` further out that this one's
|
|
338
|
+
* `break` reaches too, so it is reached, and it holds no code a region could hide in. */
|
|
313
339
|
function countEffects(
|
|
314
340
|
stmts: Stmt[],
|
|
315
341
|
ctx: TreeContext,
|
|
@@ -318,7 +344,7 @@ function countEffects(
|
|
|
318
344
|
let total: EffectCounts = new Map();
|
|
319
345
|
let paths = falls(new Map());
|
|
320
346
|
for (const s of stmts) {
|
|
321
|
-
if (paths.through === null) {
|
|
347
|
+
if (paths.through === null && !(s.k === 'break' && s.label !== undefined && paths.end !== null)) {
|
|
322
348
|
unreached.push(s);
|
|
323
349
|
}
|
|
324
350
|
const own: EffectCounts = new Map();
|
|
@@ -336,31 +362,64 @@ function countEffects(
|
|
|
336
362
|
total = arms.reduce((acc, a) => sum(acc, a.total), sum(total, dflt.total));
|
|
337
363
|
// A fall-through arm continues into the NEXT one emitted (the last into `default`), so a
|
|
338
364
|
// path through arm i runs the chain starting at i — the shape the fall-through round's
|
|
339
|
-
// CRITICAL took.
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
|
|
365
|
+
// CRITICAL took. `chain[i]` is that arm's paths. An arm that ends `goto L;` continues into
|
|
366
|
+
// the arm labelled L instead (`switchArmSuccessors`): the goto is its body's last top-level
|
|
367
|
+
// statement (`assertCaseGotosNarrow`), so the paths that fall off its end are the goto's,
|
|
368
|
+
// counted below as `through` (a `goto` ends nothing of its own). Memoised, since a goto may
|
|
369
|
+
// name an arm above it; a chain that comes back to an arm it already runs is no tree the
|
|
370
|
+
// structurer builds. The `default` is node `cases.length`.
|
|
371
|
+
const next = switchArmSuccessors(s);
|
|
372
|
+
const bodies = [...arms, dflt];
|
|
373
|
+
const chain: (PathCounts | undefined)[] = new Array(bodies.length);
|
|
374
|
+
const onChain = new Set<number>();
|
|
375
|
+
const chainAt = (i: number): PathCounts => {
|
|
376
|
+
const known = chain[i];
|
|
377
|
+
if (known !== undefined) {
|
|
378
|
+
return known;
|
|
379
|
+
}
|
|
380
|
+
if (onChain.has(i)) {
|
|
381
|
+
throw new ContractError('a switch whose arms fall or jump round in a cycle');
|
|
382
|
+
}
|
|
383
|
+
onChain.add(i);
|
|
384
|
+
const to = next[i];
|
|
385
|
+
const then = to === undefined ? undefined : chainAt(to);
|
|
386
|
+
onChain.delete(i);
|
|
387
|
+
return (chain[i] = then === undefined ? bodies[i].paths : seq(bodies[i].paths, then));
|
|
388
|
+
};
|
|
389
|
+
const dfltChain = chainAt(arms.length);
|
|
390
|
+
// No `default` is one more alternative: no arm runs. A `break` or a case-end `goto` leaves the
|
|
391
|
+
// switch, so it continues after it as a fall-off-the-end does.
|
|
392
|
+
const inner = either(...arms.map((_, i) => chainAt(i)), s.default ? dfltChain : falls(new Map()));
|
|
393
|
+
here = after(own, {
|
|
394
|
+
through: most(inner.through, inner.brk, inner.end),
|
|
395
|
+
ret: inner.ret,
|
|
396
|
+
brk: null,
|
|
397
|
+
cont: inner.cont,
|
|
398
|
+
end: null,
|
|
399
|
+
});
|
|
349
400
|
} else if (stmtChildren(s).length > 0) {
|
|
350
401
|
// A loop: its sequenced children (a body, a `for`'s init/inc) counted ONCE — a loop's dynamic
|
|
351
402
|
// trip count is not a syntactic occurrence, and the IR side is static too. A `break` or
|
|
352
403
|
// `continue` in the body lands after the loop or at its test, and the test may exit without
|
|
353
|
-
// running the body.
|
|
404
|
+
// running the body. A case-end `goto` leaves the loop for its switch's end.
|
|
354
405
|
const body = countEffects(stmtChildren(s), ctx, unreached);
|
|
355
406
|
total = sum(total, body.total);
|
|
356
407
|
const p = body.paths;
|
|
357
|
-
here = after(own, {
|
|
408
|
+
here = after(own, {
|
|
409
|
+
through: most(p.through, p.brk, p.cont, new Map()),
|
|
410
|
+
ret: p.ret,
|
|
411
|
+
brk: null,
|
|
412
|
+
cont: null,
|
|
413
|
+
end: p.end,
|
|
414
|
+
});
|
|
358
415
|
} else if (s.k === 'return') {
|
|
359
|
-
here = { through: null, ret: own, brk: null, cont: null };
|
|
416
|
+
here = { through: null, ret: own, brk: null, cont: null, end: null };
|
|
360
417
|
} else if (s.k === 'break') {
|
|
361
|
-
here = { through: null, ret: null, brk: own, cont: null };
|
|
418
|
+
here = { through: null, ret: null, brk: own, cont: null, end: null };
|
|
362
419
|
} else if (s.k === 'continue') {
|
|
363
|
-
here = { through: null, ret: null, brk: null, cont: own };
|
|
420
|
+
here = { through: null, ret: null, brk: null, cont: own, end: null };
|
|
421
|
+
} else if (s.k === 'caseEndGoto') {
|
|
422
|
+
here = { through: null, ret: null, brk: null, cont: null, end: own };
|
|
364
423
|
} else {
|
|
365
424
|
here = falls(own);
|
|
366
425
|
}
|
|
@@ -369,12 +428,24 @@ function countEffects(
|
|
|
369
428
|
return { total, paths };
|
|
370
429
|
}
|
|
371
430
|
|
|
431
|
+
/** Every `goto` keeps one of the two forms L3 allows it (l3/ast.ts `caseGotoViolation`): the last
|
|
432
|
+
* statement of a switch arm's own body naming a case label of that switch, or a case-end goto
|
|
433
|
+
* naming a labelled `break;` that ends the arm it sits in. The effect count below reads a goto
|
|
434
|
+
* that way, so a tree outside it is refused before it is counted. */
|
|
435
|
+
export function assertCaseGotosNarrow(sfn: SFn): void {
|
|
436
|
+
const why = caseGotoViolation(sfn.body);
|
|
437
|
+
if (why !== null) {
|
|
438
|
+
throw new ContractError(`'${sfn.name}': ${why}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
372
442
|
/**
|
|
373
443
|
* Post structuring: every call and pinned access the asm makes is emitted, and none of those or of
|
|
374
444
|
* its declared volatile reads is emitted more times than the asm makes it on any one path. See the
|
|
375
445
|
* note above for what this deliberately does not cover.
|
|
376
446
|
*/
|
|
377
447
|
export function assertEffectsPreserved(fn: Fn, sfn: SFn): void {
|
|
448
|
+
assertCaseGotosNarrow(sfn);
|
|
378
449
|
// Reachable blocks only: an unreachable block's call is legitimately never emitted.
|
|
379
450
|
const seen = reachableBlocks(fn);
|
|
380
451
|
const irCalls: EffectCounts = new Map();
|
|
@@ -499,6 +570,12 @@ export function assertEffectsPreserved(fn: Fn, sfn: SFn): void {
|
|
|
499
570
|
declared: new Set([...irDeclared.keys()].map((k) => k.slice(DECLARED.length))),
|
|
500
571
|
written: declaredWritten,
|
|
501
572
|
typeOf: (e) => exprCType(e, vt),
|
|
573
|
+
addresses: new Set([
|
|
574
|
+
...(sfn.globals ?? []).map((g) => g.name),
|
|
575
|
+
...[...(sfn.statics ?? []), ...(sfn.images ?? [])]
|
|
576
|
+
.filter((st) => st.type.kind === 'array' || st.type.kind === 'struct')
|
|
577
|
+
.map((st) => st.name),
|
|
578
|
+
]),
|
|
502
579
|
};
|
|
503
580
|
const { total, paths } = countEffects(sfn.body, ctx, unreached);
|
|
504
581
|
if (unreached.length) {
|
|
@@ -753,10 +830,17 @@ export function assertLocalsWritten(sfn: SFn): void {
|
|
|
753
830
|
}
|
|
754
831
|
exprChildren(e).forEach(walkExpr);
|
|
755
832
|
};
|
|
833
|
+
// A store to a member of a local struct (`ret.a = v`) writes that local.
|
|
834
|
+
const memberOf = (e: Expr): string | undefined =>
|
|
835
|
+
e.k === 'field' && e.dot === true ? (e.base.k === 'var' ? e.base.name : memberOf(e.base)) : undefined;
|
|
756
836
|
const walkStmt = (st: Stmt): void => {
|
|
757
837
|
if (st.k === 'assign' && suspect.has(st.name)) {
|
|
758
838
|
written.add(st.name);
|
|
759
839
|
}
|
|
840
|
+
const member = st.k === 'store' ? memberOf(st.lval) : undefined;
|
|
841
|
+
if (member !== undefined && suspect.has(member)) {
|
|
842
|
+
written.add(member);
|
|
843
|
+
}
|
|
760
844
|
stmtExprs(st).forEach(walkExpr);
|
|
761
845
|
stmtChildren(st).forEach(walkStmt);
|
|
762
846
|
};
|
|
@@ -858,6 +942,131 @@ export function assertNoOrphanedLocals(before: SFn, after: SFn): void {
|
|
|
858
942
|
}
|
|
859
943
|
}
|
|
860
944
|
|
|
945
|
+
/** The types a tree reads or writes LOCAL STORAGE at, other than an integer — the frame bytes
|
|
946
|
+
* reached by address (l3/storage.ts `localAddresses`), each access typed as it is spelled
|
|
947
|
+
* (`exprCType`: the cast on the storage, or the element struct's field). An aggregate is the
|
|
948
|
+
* element a `.field` selects from, not an access, so only scalars count. */
|
|
949
|
+
function localStorageNonIntegers(sfn: SFn): Set<string> {
|
|
950
|
+
const { isAddress, accessAddress } = localAddresses(sfn);
|
|
951
|
+
const vt = declaredTypes(sfn);
|
|
952
|
+
const out = new Set<string>();
|
|
953
|
+
for (const e of walkExprs(sfn.body)) {
|
|
954
|
+
if ((e.k === 'index' || e.k === 'field') && isAddress(accessAddress(e))) {
|
|
955
|
+
const t = exprCType(e, vt);
|
|
956
|
+
if (t !== undefined && t.kind !== 'int' && t.kind !== 'struct' && t.kind !== 'array') {
|
|
957
|
+
out.add(typeToString(t));
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return out;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/** After a respell variation: no access to LOCAL STORAGE is re-typed to a non-integer.
|
|
965
|
+
*
|
|
966
|
+
* The frame audit's licence for a runtime index wider than a byte (frontend/frame-objects.ts
|
|
967
|
+
* `indexedWidth`) rests on what L3 SPELLS: agbcc at -O2 keeps a read ordered after a store to the
|
|
968
|
+
* same bytes only while both are integers of one width, and gives a pointer (or `long`, or a
|
|
969
|
+
* float) an alias set of its own, so a variation that re-types the bytes' reads hands the
|
|
970
|
+
* compiler a stale read the asm never had — C that compiles, scores, and is wrong. `/ptr-field`'s
|
|
971
|
+
* `over-local-storage` gate is the refusal that names itself; this is the backstop for the next
|
|
972
|
+
* variation that re-types, failing at its own stage rather than three away.
|
|
973
|
+
*
|
|
974
|
+
* A DIFFERENTIAL, like `assertNoOrphanedLocals`: it judges only a non-integer type the tree it
|
|
975
|
+
* derives from did not already read the storage at. The default tree is the frame audit's
|
|
976
|
+
* answer, and a type it gives a local object (a header-typed out-parameter's pointer member, say)
|
|
977
|
+
* is the source's own spelling, not a variation's claim. */
|
|
978
|
+
export function assertLocalStorageStaysInteger(before: SFn, after: SFn): void {
|
|
979
|
+
const had = localStorageNonIntegers(before);
|
|
980
|
+
const added = [...localStorageNonIntegers(after)].filter((t) => !had.has(t));
|
|
981
|
+
if (added.length) {
|
|
982
|
+
throw new ContractError(
|
|
983
|
+
`a respell variation re-typed an access to local storage in '${after.name}' as ${added
|
|
984
|
+
.sort()
|
|
985
|
+
.map((t) => `'${t}'`)
|
|
986
|
+
.join(', ')} — not an integer, so agbcc may read it stale past an integer store to the same bytes`,
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/** A FRAME UNION IS READ AND WRITTEN ONLY THROUGH ITS VIEWS. The frame audit declares storage
|
|
992
|
+
* accessed at two widths over one byte as a union of per-width views (frontend/frame-objects.ts
|
|
993
|
+
* `unionViews`, structure.ts `laddrType`), because agbcc gives an access through a union member
|
|
994
|
+
* alias set 0 and reorders nothing past it — where a cast on the same bytes is read stale past a
|
|
995
|
+
* store of the other width. That licence holds only while EVERY access is a member access:
|
|
996
|
+
* - the union's name appears only as the base of a view (`u.half`), and an element of an array
|
|
997
|
+
* view is subscripted, element 0 included — `*u.half` dereferences the view's decay, an
|
|
998
|
+
* INDIRECT_REF agbcc types by its pointee and reads stale exactly as the cast (compiled);
|
|
999
|
+
* - nothing is accessed through its ADDRESS: `&u`, under casts and `+`/`-`, or a local ever
|
|
1000
|
+
* assigned such a value, as the base of a subscript, an arrow or a deref is the cast
|
|
1001
|
+
* spelling again. The address itself may go anywhere a value goes — a call's argument, a
|
|
1002
|
+
* device's source register — since handing it on accesses nothing.
|
|
1003
|
+
* ABSOLUTE, not a differential: the default tree satisfies it by construction, so any tree that
|
|
1004
|
+
* does not was made by a variation. Checked on the default tree (pipeline.ts) and on every
|
|
1005
|
+
* respelled one (rank.ts, beside `assertLocalStorageStaysInteger`). */
|
|
1006
|
+
export function assertFrameUnionsViewed(sfn: SFn): void {
|
|
1007
|
+
const unions = new Map(sfn.locals.filter((l) => l.type.kind === 'union').map((l) => [l.name, l.type]));
|
|
1008
|
+
if (unions.size === 0) {
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
const bad = (why: string): never => {
|
|
1012
|
+
throw new ContractError(`'${sfn.name}' ${why} — agbcc may read the frame union stale through it`);
|
|
1013
|
+
};
|
|
1014
|
+
const reaches = (e: Expr, held: ReadonlySet<string>): boolean => {
|
|
1015
|
+
switch (e.k) {
|
|
1016
|
+
case 'addr':
|
|
1017
|
+
return unions.has(e.name);
|
|
1018
|
+
case 'var':
|
|
1019
|
+
return held.has(e.name);
|
|
1020
|
+
case 'cast':
|
|
1021
|
+
return reaches(e.e, held);
|
|
1022
|
+
case 'bin':
|
|
1023
|
+
return (e.op === '+' || e.op === '-') && (reaches(e.l, held) || reaches(e.r, held));
|
|
1024
|
+
default:
|
|
1025
|
+
return false;
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
const held = taintOverAssigns(assignsIn(sfn.body), [], (a, t) => reaches(a.value, t));
|
|
1029
|
+
const isView = (e: Expr): boolean =>
|
|
1030
|
+
e.k === 'field' && e.dot === true && e.base.k === 'var' && unions.has(e.base.name);
|
|
1031
|
+
const visit = (e: Expr): void => {
|
|
1032
|
+
if (e.k === 'field' && isView(e)) {
|
|
1033
|
+
if (memberOf(unions.get((e.base as { name: string }).name), e.name) === undefined) {
|
|
1034
|
+
bad(`names '${e.name}', which is no view of the frame union`);
|
|
1035
|
+
}
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
if (e.k === 'index' && isView(e.base)) {
|
|
1039
|
+
if (e.idx.k === 'const' && e.idx.value === 0 && e.subscript !== true) {
|
|
1040
|
+
bad('dereferences a view of a frame union (`*u.view`) instead of subscripting it');
|
|
1041
|
+
}
|
|
1042
|
+
visit(e.base);
|
|
1043
|
+
visit(e.idx);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
if ((e.k === 'index' || (e.k === 'field' && e.dot !== true)) && reaches(e.base, held)) {
|
|
1047
|
+
bad('accesses a frame union through a pointer to its bytes');
|
|
1048
|
+
}
|
|
1049
|
+
if (e.k === 'var' && unions.has(e.name)) {
|
|
1050
|
+
bad(`uses the frame union '${e.name}' other than through a view`);
|
|
1051
|
+
}
|
|
1052
|
+
exprChildren(e).forEach(visit);
|
|
1053
|
+
};
|
|
1054
|
+
const walk = (list: readonly Stmt[]): void => {
|
|
1055
|
+
for (const st of list) {
|
|
1056
|
+
if (st.k === 'assign' && unions.has(st.name)) {
|
|
1057
|
+
bad(`assigns the frame union '${st.name}' whole`);
|
|
1058
|
+
}
|
|
1059
|
+
stmtExprs(st).forEach(visit);
|
|
1060
|
+
// a `for`'s init and inc are statements of their own, outside both lists
|
|
1061
|
+
stmtLists(st).forEach(walk);
|
|
1062
|
+
if (st.k === 'for') {
|
|
1063
|
+
walk([st.init, st.inc]);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
walk(sfn.body);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
861
1070
|
/** After a respell variation: every read of a MINTED local — its ADDRESS being taken included — must sit where
|
|
862
1071
|
* that local's assignment has already run.
|
|
863
1072
|
*
|
|
@@ -1013,8 +1222,9 @@ export function assertDerefsTyped(sfn: SFn): void {
|
|
|
1013
1222
|
if (e.k === 'index' && !structElem.has(e) && !SCALAR_WIDTHS.has(e.width)) {
|
|
1014
1223
|
bad.push(`index width ${e.width} is not a C scalar width`);
|
|
1015
1224
|
}
|
|
1016
|
-
// The emitter legalizes pointer operands away from these ops (structure.ts
|
|
1017
|
-
// pointer surviving here is ill-typed C the compiler will
|
|
1225
|
+
// The emitter legalizes pointer operands away from these ops (structure/pointer-spelling.ts
|
|
1226
|
+
// `ARITH_ROWS`, its `bitwise` row); a pointer surviving here is ill-typed C the compiler will
|
|
1227
|
+
// reject.
|
|
1018
1228
|
if (e.k === 'bin' && NO_PTR_OPS.has(e.op)) {
|
|
1019
1229
|
for (const side of [e.l, e.r]) {
|
|
1020
1230
|
if (ctype(side)?.kind === 'ptr') {
|
|
@@ -1026,9 +1236,9 @@ export function assertDerefsTyped(sfn: SFn): void {
|
|
|
1026
1236
|
// offset by sizeof(SYM), which is unknown for a header-typed global, so `&SYM + N` is byte-
|
|
1027
1237
|
// inexact. Nothing emits this shape anymore: a load/store base folds byte-correctly (globalOf
|
|
1028
1238
|
// turns `&SYM + N` into an `index`/`field` node whose base is a bare `addr`), and the additive
|
|
1029
|
-
// lowering intifies every other `addr` operand to `(u32)&SYM` (structure.ts
|
|
1030
|
-
// cast types int, so it never lands here). A bare `addr` reaching a `+`/`-`
|
|
1031
|
-
// a lowering REGRESSION — flag it rather than emit wrong bytes.
|
|
1239
|
+
// lowering intifies every other `addr` operand to `(u32)&SYM` (structure/pointer-spelling.ts
|
|
1240
|
+
// ARITH_ROWS — the cast types int, so it never lands here). A bare `addr` reaching a `+`/`-`
|
|
1241
|
+
// operand is therefore a lowering REGRESSION — flag it rather than emit wrong bytes.
|
|
1032
1242
|
if (e.k === 'bin' && (e.op === '+' || e.op === '-')) {
|
|
1033
1243
|
const addrSide = e.l.k === 'addr' ? e.l : e.r.k === 'addr' ? e.r : undefined;
|
|
1034
1244
|
if (addrSide) {
|
package/src/declare.ts
CHANGED
|
@@ -185,7 +185,36 @@ export function renderDeclarations(refs: SymbolRef[]): string {
|
|
|
185
185
|
const lines: string[] = [];
|
|
186
186
|
const declaredTags = new Set<string>();
|
|
187
187
|
const declaredTypedefs = new Set<string>();
|
|
188
|
-
|
|
188
|
+
const defineStruct = (s: { readonly name: string; readonly declared: string; readonly layout: AggregateLayout }) => {
|
|
189
|
+
if (!declaredTags.has(s.name)) {
|
|
190
|
+
declaredTags.add(s.name);
|
|
191
|
+
lines.push(declaredStructDecl(s.name, s.layout));
|
|
192
|
+
}
|
|
193
|
+
if (!/^(?:struct|union)\s/.test(s.declared) && !declaredTypedefs.has(s.declared)) {
|
|
194
|
+
declaredTypedefs.add(s.declared);
|
|
195
|
+
lines.push(`typedef struct ${s.name} ${s.declared};`);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
for (const {
|
|
199
|
+
name,
|
|
200
|
+
info,
|
|
201
|
+
access,
|
|
202
|
+
proto,
|
|
203
|
+
returned,
|
|
204
|
+
passed,
|
|
205
|
+
passedAs,
|
|
206
|
+
holdsPointer,
|
|
207
|
+
implicitCall,
|
|
208
|
+
definesReturn,
|
|
209
|
+
} of refs) {
|
|
210
|
+
// THE FUNCTION'S OWN STRUCT RETURN, defined as a callee's is; the function itself is declared
|
|
211
|
+
// by its definition
|
|
212
|
+
if (definesReturn) {
|
|
213
|
+
if (returned !== undefined) {
|
|
214
|
+
defineStruct(returned);
|
|
215
|
+
}
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
189
218
|
// An address-cast macro declares itself: the header's own body, verbatim. It must NOT become
|
|
190
219
|
// an `extern` — that is the whole point of the fact (an extern emits a relocated pool word
|
|
191
220
|
// where the macro emits the numeric one the target shows).
|
|
@@ -193,6 +222,14 @@ export function renderDeclarations(refs: SymbolRef[]): string {
|
|
|
193
222
|
lines.push(`#define ${name} ${info.macroBody}`);
|
|
194
223
|
continue;
|
|
195
224
|
}
|
|
225
|
+
// A GLOBAL REGISTER VARIABLE declares itself in its register (SymbolInfo.globalRegister), typed
|
|
226
|
+
// as a name-only global is: a pointer where the body stores or meets one, else the word the
|
|
227
|
+
// body accesses.
|
|
228
|
+
if (info.globalRegister !== undefined) {
|
|
229
|
+
const t = holdsPointer ? 'void *' : `${intType(access?.width ?? 4, access?.signed ?? true) ?? 's32'} `;
|
|
230
|
+
lines.push(`register ${t}${name} __asm__("${info.globalRegister}");`);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
196
233
|
// A CALL TARGET the project declared completely — its own prototype, re-spelled from the
|
|
197
234
|
// parameter and return type texts the project wrote. This is the one declaration here that
|
|
198
235
|
// is load-bearing for the CALL and not only for the name: an implicit declaration is `int`,
|
|
@@ -203,23 +240,24 @@ export function renderDeclarations(refs: SymbolRef[]): string {
|
|
|
203
240
|
// A STRUCT IT RETURNS THROUGH MEMORY is the headers' type, spelled as they spell it, so it is
|
|
204
241
|
// defined here with the rest of what they own: by its tag, once, whoever else declares that
|
|
205
242
|
// tag — and a typedef name is bound to it on top.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
lines.push(declaredStructDecl(returned.name, returned.layout));
|
|
210
|
-
}
|
|
211
|
-
if (!/^(?:struct|union)\s/.test(returned.declared) && !declaredTypedefs.has(returned.declared)) {
|
|
212
|
-
declaredTypedefs.add(returned.declared);
|
|
213
|
-
lines.push(`typedef struct ${returned.name} ${returned.declared};`);
|
|
214
|
-
}
|
|
243
|
+
// …and so is a struct it is passed by value, the same way.
|
|
244
|
+
for (const s of [...(returned?.declared !== undefined ? [returned] : []), ...(passed ?? [])]) {
|
|
245
|
+
defineStruct(s);
|
|
215
246
|
}
|
|
216
247
|
lines.push(`${proto.returns} ${name}(${proto.params.length > 0 ? proto.params.join(', ') : 'void'});`);
|
|
217
248
|
continue;
|
|
218
249
|
}
|
|
250
|
+
// A CALL TARGET no prototype spells, named ahead of its first call by an initializer — C's
|
|
251
|
+
// implicit declaration, `int F();`, written where the initializer can see it. No parameter
|
|
252
|
+
// list, so the call binds to it exactly as it bound to the implicit one (see collectSymbolRefs).
|
|
253
|
+
if (implicitCall) {
|
|
254
|
+
lines.push(`int ${name}();`);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
219
257
|
if (info.kind === 'code') {
|
|
220
258
|
// value-referenced code symbol ((u32)Func): any prototype makes the name visible, and
|
|
221
|
-
// the address is arity-independent. A call target reaches
|
|
222
|
-
// prototype, which the
|
|
259
|
+
// the address is arity-independent. A call target reaches here only with a complete
|
|
260
|
+
// prototype or as an initializer's implicit call, which the branches above printed.
|
|
223
261
|
lines.push(`void ${name}(void);`);
|
|
224
262
|
continue;
|
|
225
263
|
}
|
|
@@ -317,6 +355,12 @@ export function renderDeclarations(refs: SymbolRef[]): string {
|
|
|
317
355
|
lines.push(`extern void *${info.volatile ? 'volatile ' : ''}${info.const ? 'const ' : ''}${name};`);
|
|
318
356
|
break;
|
|
319
357
|
}
|
|
358
|
+
// A global passed bare as a struct by value is that struct, which its callee's prototype
|
|
359
|
+
// defines (`passed`); a declaration of an incomplete tag is completed by that definition.
|
|
360
|
+
if (passedAs !== undefined) {
|
|
361
|
+
lines.push(`extern ${quals(info)}${passedAs} ${name};`);
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
320
364
|
const t = access ? intType(access.width, access.signed) : null;
|
|
321
365
|
lines.push(`extern ${quals(info)}${t ?? 'u32'} ${name};`);
|
|
322
366
|
break;
|
|
@@ -326,18 +370,23 @@ export function renderDeclarations(refs: SymbolRef[]): string {
|
|
|
326
370
|
return lines.length ? lines.join('\n') + '\n' : '';
|
|
327
371
|
}
|
|
328
372
|
|
|
329
|
-
/**
|
|
373
|
+
/** THE LINES OF A RENDERED DECLARATION BLOCK THAT THE HEADERS WORLD KEEPS: its object-like
|
|
374
|
+
* `#define`s and its global register variables. Everything else there is owned by the injected
|
|
375
|
+
* headers (a duplicate typedef or struct definition is a C89 hard error), and these two are not:
|
|
376
|
+
*
|
|
377
|
+
* - an address-cast macro, because a duplicate `#define` with an identical body is legal — and a
|
|
378
|
+
* PREPROCESSED project context has no macros left at all, so dropping these turns a macro-named
|
|
379
|
+
* candidate into an `undeclared identifier` rather than a spelling choice;
|
|
380
|
+
* - a global register variable (`register T greg_r8 __asm__("r8");`, SymbolInfo.globalRegister),
|
|
381
|
+
* because no header declares the name the lift made up for it.
|
|
330
382
|
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
|
|
334
|
-
* legal — and a PREPROCESSED project context has no macros left at all, so dropping these turns a
|
|
335
|
-
* macro-named candidate into an `undeclared identifier` rather than a spelling choice. */
|
|
336
|
-
export function macroDefinesOf(declarations: string | undefined): string {
|
|
383
|
+
* Chosen by each line's leading keyword, which `renderDeclarations` prints for these two kinds
|
|
384
|
+
* only: a new kind of line it prints with either keyword rides into the headers world too. */
|
|
385
|
+
export function declarationsKeptWithHeaders(declarations: string | undefined): string {
|
|
337
386
|
if (!declarations) {
|
|
338
387
|
return '';
|
|
339
388
|
}
|
|
340
|
-
const lines = declarations.split('\n').filter((l) => l.startsWith('#define '));
|
|
389
|
+
const lines = declarations.split('\n').filter((l) => l.startsWith('#define ') || l.startsWith('register '));
|
|
341
390
|
return lines.length ? lines.join('\n') + '\n' : '';
|
|
342
391
|
}
|
|
343
392
|
|