@asmlift/core 0.10.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/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +33 -12
- package/src/backend/cpp.ts +1 -0
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +17 -5
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +73 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +2 -1
- package/src/frontend/call-plan.ts +197 -45
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/device-pins.ts +18 -8
- package/src/frontend/frame-objects.ts +504 -61
- package/src/frontend/frontend.ts +20 -0
- package/src/frontend/jump-table.ts +35 -20
- package/src/frontend/mips.ts +226 -8
- package/src/frontend/ppc.ts +26 -2
- package/src/frontend/register-struct-call.ts +1 -86
- package/src/frontend/ssa.ts +263 -1
- package/src/frontend/stackargs.ts +95 -17
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +410 -61
- package/src/ir/alias.ts +29 -8
- package/src/ir/bits.ts +1081 -66
- package/src/ir/core.ts +154 -4
- package/src/ir/entry.ts +3 -5
- package/src/ir/opcodes.ts +28 -0
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/verify.ts +16 -0
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +294 -2
- package/src/l3/basecse.ts +35 -10
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +34 -6
- package/src/l3/dce.ts +52 -8
- package/src/l3/ptrcell.ts +2 -0
- package/src/l3/regspell.ts +2 -0
- package/src/l3/reindex.ts +13 -4
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/tailmerge.ts +2 -0
- package/src/pipeline.ts +507 -23
- package/src/proto-context.ts +135 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +1 -1
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/offsetnames.ts +412 -10
- package/src/raise/pre-recovery.ts +8 -5
- package/src/raise/pre-structure.ts +24 -5
- package/src/raise/reroll.ts +4 -24
- package/src/raise/shortcircuit.ts +192 -23
- package/src/raise/unpre.ts +594 -0
- package/src/rank-variations.ts +97 -4
- package/src/rank.ts +258 -43
- package/src/structure/analysis.ts +56 -10
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +46 -1
- package/src/structure/structure.ts +4221 -368
- package/src/structure/switch-recover.ts +345 -26
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +52 -15
- package/src/target.ts +269 -8
- package/src/trace.ts +28 -14
- package/src/variation-definitions.ts +207 -6
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +63 -2
package/package.json
CHANGED
package/src/backend/c.ts
CHANGED
|
@@ -14,6 +14,7 @@ export const cBackend: LanguageBackend = {
|
|
|
14
14
|
id: 'c',
|
|
15
15
|
spellsSwitchFallthrough: true,
|
|
16
16
|
spellsSwitchBreak: true,
|
|
17
|
+
spellsEarlyReturn: true,
|
|
17
18
|
emit(fn: SFn): string {
|
|
18
19
|
// The SAME declarator placement the local list and the struct-field printer use, so one
|
|
19
20
|
// function cannot spell the `*` on the type in its signature and on the declarator in its
|
package/src/backend/cfamily.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// too, shared with the scoring layer's synthesized declarations so the two cannot drift).
|
|
13
13
|
import { doubleLiteral } from '../ir/float-bits';
|
|
14
14
|
import { IrType, T, scalarTypeForAccess, typeEquals, typeToString } from '../ir/types';
|
|
15
|
-
import { BinOp, Expr, InitAddress, SFn, SStatic, Stmt, dotBase } from '../l3/ast';
|
|
15
|
+
import { BinOp, Expr, InitAddress, SFn, SStatic, Stmt, caseGotoViolation, dotBase } from '../l3/ast';
|
|
16
16
|
import { foldConstantImages } from '../l3/constinit';
|
|
17
17
|
import { legalizePointerCells } from '../l3/ptrcell';
|
|
18
18
|
import { orderSlotLocals } from '../l3/slotorder';
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
writesNonPointerIntoPointer,
|
|
30
30
|
} from '../l3/typing';
|
|
31
31
|
import { spellableType } from '../proto';
|
|
32
|
+
import { append } from './lines';
|
|
32
33
|
|
|
33
34
|
// C operator precedence (lower binds tighter). Used to emit MINIMAL parentheses. Shared: C++ has
|
|
34
35
|
// the same precedence for these operators.
|
|
@@ -524,12 +525,12 @@ function printStmt(s: Stmt, indent: string, vt: PrintEnv, leaf?: LeafHook): stri
|
|
|
524
525
|
}
|
|
525
526
|
const out = [`${indent}if (${cond}) {`];
|
|
526
527
|
for (const t of s.then) {
|
|
527
|
-
out
|
|
528
|
+
append(out, printStmt(t, indent + ' ', vt, leaf));
|
|
528
529
|
}
|
|
529
530
|
if (s.else.length) {
|
|
530
531
|
out.push(`${indent}} else {`);
|
|
531
532
|
for (const e of s.else) {
|
|
532
|
-
out
|
|
533
|
+
append(out, printStmt(e, indent + ' ', vt, leaf));
|
|
533
534
|
}
|
|
534
535
|
}
|
|
535
536
|
out.push(`${indent}}`);
|
|
@@ -539,7 +540,7 @@ function printStmt(s: Stmt, indent: string, vt: PrintEnv, leaf?: LeafHook): stri
|
|
|
539
540
|
refuseMisboundBreak(s.body, 'loop');
|
|
540
541
|
const out = [`${indent}while (${pe(s.cond, 99)}) {`];
|
|
541
542
|
for (const t of s.body) {
|
|
542
|
-
out
|
|
543
|
+
append(out, printStmt(t, indent + ' ', vt, leaf));
|
|
543
544
|
}
|
|
544
545
|
out.push(`${indent}}`);
|
|
545
546
|
return out;
|
|
@@ -548,7 +549,7 @@ function printStmt(s: Stmt, indent: string, vt: PrintEnv, leaf?: LeafHook): stri
|
|
|
548
549
|
refuseMisboundBreak(s.body, 'loop');
|
|
549
550
|
const out = [`${indent}do {`];
|
|
550
551
|
for (const t of s.body) {
|
|
551
|
-
out
|
|
552
|
+
append(out, printStmt(t, indent + ' ', vt, leaf));
|
|
552
553
|
}
|
|
553
554
|
out.push(`${indent}} while (${pe(s.cond, 99)});`);
|
|
554
555
|
return out;
|
|
@@ -559,18 +560,27 @@ function printStmt(s: Stmt, indent: string, vt: PrintEnv, leaf?: LeafHook): stri
|
|
|
559
560
|
// and strips its trailing `;` so it sits inside the header (`i = 0; c; i = i + 1`). A multi-line
|
|
560
561
|
// statement (an `if`/nested loop) would render mangled — but the recognizer never builds one here.
|
|
561
562
|
refuseMisboundBreak(s.body, 'loop');
|
|
563
|
+
// An identity init (`i = i`, structure.ts `forPlanAt`: the name already holds the entry value)
|
|
564
|
+
// is the empty clause.
|
|
562
565
|
const clause = (st: Stmt) => printStmt(st, '', vt, leaf).join(' ').replace(/;\s*$/, '').trim();
|
|
563
|
-
const
|
|
566
|
+
const identity = s.init.k === 'assign' && s.init.value.k === 'var' && s.init.value.name === s.init.name;
|
|
567
|
+
const out = [`${indent}for (${identity ? '' : clause(s.init)}; ${pe(s.cond, 99)}; ${clause(s.inc)}) {`];
|
|
564
568
|
for (const t of s.body) {
|
|
565
|
-
out
|
|
569
|
+
append(out, printStmt(t, indent + ' ', vt, leaf));
|
|
566
570
|
}
|
|
567
571
|
out.push(`${indent}}`);
|
|
568
572
|
return out;
|
|
569
573
|
}
|
|
574
|
+
// A labelled `break` is the end of a switch arm a case-end `goto` lands on (l3/ast.ts).
|
|
570
575
|
case 'break':
|
|
571
|
-
return [`${indent}break;`];
|
|
576
|
+
return s.label === undefined ? [`${indent}break;`] : [`${indent}${s.label}:`, `${indent}break;`];
|
|
572
577
|
case 'continue':
|
|
573
578
|
return [`${indent}continue;`];
|
|
579
|
+
// Where it may sit is the tree's contract (l3/ast.ts `caseGotoViolation`), which `emitCFamily`
|
|
580
|
+
// asks of the whole function before printing any of it.
|
|
581
|
+
case 'goto':
|
|
582
|
+
case 'caseEndGoto':
|
|
583
|
+
return [`${indent}goto ${s.label};`];
|
|
574
584
|
case 'switch':
|
|
575
585
|
return printSwitchStmt(s, indent, vt, leaf);
|
|
576
586
|
}
|
|
@@ -616,7 +626,7 @@ function printSwitchStmt(s: Extract<Stmt, { k: 'switch' }>, indent: string, vt:
|
|
|
616
626
|
const printDefault = (): void => {
|
|
617
627
|
out.push(`${ci}default:`);
|
|
618
628
|
for (const t of s.default!) {
|
|
619
|
-
out
|
|
629
|
+
append(out, printStmt(t, bi, vt, leaf));
|
|
620
630
|
}
|
|
621
631
|
// A default that is NOT last would otherwise fall into the case below it — the mirror of the
|
|
622
632
|
// rule for cases. The last one needs no `break;` because there is nothing under it.
|
|
@@ -639,8 +649,13 @@ function printSwitchStmt(s: Extract<Stmt, { k: 'switch' }>, indent: string, vt:
|
|
|
639
649
|
for (const v of c.values) {
|
|
640
650
|
out.push(`${ci}case ${v}:`);
|
|
641
651
|
}
|
|
652
|
+
// The label a `goto` of this switch lands on, after the arm's case labels: entering it there
|
|
653
|
+
// runs exactly what falling into the arm runs.
|
|
654
|
+
if (c.label !== undefined) {
|
|
655
|
+
out.push(`${ci}${c.label}:`);
|
|
656
|
+
}
|
|
642
657
|
for (const t of c.body) {
|
|
643
|
-
out
|
|
658
|
+
append(out, printStmt(t, bi, vt, leaf));
|
|
644
659
|
}
|
|
645
660
|
// A case whose body ends in `return`/`break` (a terminated arm) needs no `break;`; only an
|
|
646
661
|
// open non-fall-through arm gets one. `fallsThrough` omits it so control drops to the next case.
|
|
@@ -685,6 +700,8 @@ function endsTerminated(body: Stmt[]): boolean {
|
|
|
685
700
|
(last.k === 'return' ||
|
|
686
701
|
last.k === 'break' ||
|
|
687
702
|
last.k === 'continue' ||
|
|
703
|
+
last.k === 'goto' ||
|
|
704
|
+
last.k === 'caseEndGoto' ||
|
|
688
705
|
(last.k === 'if' && endsTerminated(last.then) && last.else.length > 0 && endsTerminated(last.else)))
|
|
689
706
|
);
|
|
690
707
|
}
|
|
@@ -769,7 +786,7 @@ function cFamilyBody(fn0: SFn, leaf: LeafHook | undefined, designators: boolean)
|
|
|
769
786
|
const vt: PrintEnv = printEnv(fn);
|
|
770
787
|
const lines: string[] = [];
|
|
771
788
|
for (const st of fn.statics ?? []) {
|
|
772
|
-
lines
|
|
789
|
+
append(lines, staticDefinition(st));
|
|
773
790
|
}
|
|
774
791
|
for (const l of fn.locals) {
|
|
775
792
|
// Both facts render at the PREFIX position, where C's declarator grammar reads them
|
|
@@ -785,7 +802,7 @@ function cFamilyBody(fn0: SFn, leaf: LeafHook | undefined, designators: boolean)
|
|
|
785
802
|
);
|
|
786
803
|
}
|
|
787
804
|
for (const s of fn.body) {
|
|
788
|
-
lines
|
|
805
|
+
append(lines, printStmt(s, ' ', vt, leaf));
|
|
789
806
|
}
|
|
790
807
|
return lines;
|
|
791
808
|
}
|
|
@@ -901,6 +918,10 @@ export function emitCFamily(signature: string, fn0: SFn, leaf?: LeafHook, design
|
|
|
901
918
|
// …and the compiler's anonymous constant images folded into the initializers of the locals
|
|
902
919
|
// their copies fill (l3/constinit.ts), which the tree keeps as copies until here.
|
|
903
920
|
const fn = orderSlotLocals(legalizePointerCells(foldConstantImages(fn0)));
|
|
921
|
+
const stray = caseGotoViolation(fn.body);
|
|
922
|
+
if (stray !== null) {
|
|
923
|
+
throw new Error(`c backend: ${stray}`);
|
|
924
|
+
}
|
|
904
925
|
const decls = structDecls(fn);
|
|
905
926
|
const preamble = decls.length ? decls.join('\n') + '\n' : '';
|
|
906
927
|
return (
|
package/src/backend/cpp.ts
CHANGED
|
@@ -139,6 +139,7 @@ export function cppBackend(spec: CppFnSpec, floatSlots: FloatSlots | undefined):
|
|
|
139
139
|
id: 'cpp',
|
|
140
140
|
spellsSwitchFallthrough: true,
|
|
141
141
|
spellsSwitchBreak: true,
|
|
142
|
+
spellsEarlyReturn: true,
|
|
142
143
|
emit(fn: SFn): string {
|
|
143
144
|
// Map each lifted param var → its C++ meaning: `this` (bare member access) or a named param
|
|
144
145
|
// (a pointer-to-class param uses `->`). A pointer-to-known-class param is a member receiver.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** `out.push(...lines)` without the spread: a spread passes every line as an ARGUMENT, and V8 throws
|
|
2
|
+
* "Maximum call stack size exceeded" past about 120k of them (120,000 print, 130,000 throw, node
|
|
3
|
+
* 24). A structuring with a long arm reaches that at a nesting depth of a few dozen, so the throw
|
|
4
|
+
* reads as deep recursion when it is only size. Every printer appends a child's lines with this,
|
|
5
|
+
* so the limit binds no backend (the C family and Pascal alike). */
|
|
6
|
+
export function append(out: string[], lines: readonly string[]): void {
|
|
7
|
+
for (const line of lines) {
|
|
8
|
+
out.push(line);
|
|
9
|
+
}
|
|
10
|
+
}
|
package/src/backend/pascal.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { IrType, typeToString } from '../ir/types';
|
|
|
12
12
|
import { BinOp, Expr, LanguageBackend, SFn, Stmt, isLoop } from '../l3/ast';
|
|
13
13
|
import { orderSlotLocals } from '../l3/slotorder';
|
|
14
14
|
import { type VarTypes, declaredTypes, derefStrideOk, exprCType, writesNonPointerIntoPointer } from '../l3/typing';
|
|
15
|
+
import { append } from './lines';
|
|
15
16
|
|
|
16
17
|
// Infix operators IDO Pascal spells directly.
|
|
17
18
|
const OP: Partial<Record<BinOp, string>> = {
|
|
@@ -215,10 +216,10 @@ function makePrinter(vt: VarTypes) {
|
|
|
215
216
|
: [`${ind}begin`, ...list(stmts, ind + ' ', tail), `${ind}end`];
|
|
216
217
|
// Pascal: no `;` before `else`; the branch statements already carry their own.
|
|
217
218
|
const out = [`${indent}if ${cond} then`];
|
|
218
|
-
out
|
|
219
|
+
append(out, block(s.then, indent + ' ', s.else.length > 0));
|
|
219
220
|
if (s.else.length) {
|
|
220
221
|
out.push(`${indent}else`);
|
|
221
|
-
out
|
|
222
|
+
append(out, block(s.else, indent + ' '));
|
|
222
223
|
}
|
|
223
224
|
return out;
|
|
224
225
|
}
|
|
@@ -255,6 +256,13 @@ function makePrinter(vt: VarTypes) {
|
|
|
255
256
|
throw new Error('pascal backend: `break` has no IDO Pascal spelling');
|
|
256
257
|
case 'continue':
|
|
257
258
|
throw new Error('pascal backend: `continue` has no IDO Pascal spelling');
|
|
259
|
+
// A jump into another arm of a `case-of` has no Pascal spelling: an arm is a statement, and a
|
|
260
|
+
// label may not be entered from outside the statement that holds it.
|
|
261
|
+
case 'goto':
|
|
262
|
+
throw new Error('pascal backend: a `goto` into a case arm has no IDO Pascal spelling');
|
|
263
|
+
// A jump to an arm's end lands on a `break`, which Pascal has none of either.
|
|
264
|
+
case 'caseEndGoto':
|
|
265
|
+
throw new Error('pascal backend: a `goto` to the end of a case arm has no IDO Pascal spelling');
|
|
258
266
|
case 'switch': {
|
|
259
267
|
// IDO/SGI Pascal `case E of L: S; … otherwise S end`. There is NO fall-through in Pascal case-of,
|
|
260
268
|
// so a `fallsThrough` arm has no faithful spelling → loud-fail (mirrors the `field`/`cast`/`%`
|
|
@@ -284,14 +292,15 @@ function makePrinter(vt: VarTypes) {
|
|
|
284
292
|
if (c.fallsThrough) {
|
|
285
293
|
throw new Error('pascal backend: switch fall-through has no faithful IDO Pascal case-of spelling');
|
|
286
294
|
}
|
|
287
|
-
out.push(`${indent} ${c.values.join(', ')}
|
|
295
|
+
out.push(`${indent} ${c.values.join(', ')}:`);
|
|
296
|
+
append(out, arm(c.body, indent + ' '));
|
|
288
297
|
}
|
|
289
298
|
// A C `switch` with no `default` runs on past a value it does not list; IDO Pascal's
|
|
290
299
|
// `case` raises a run-time `caseerror` there instead. So the clause is always printed, an
|
|
291
300
|
// empty one when the source had no default.
|
|
292
301
|
out.push(`${indent} otherwise`);
|
|
293
302
|
if (s.default?.length) {
|
|
294
|
-
out
|
|
303
|
+
append(out, arm(s.default, indent + ' '));
|
|
295
304
|
}
|
|
296
305
|
out.push(`${indent}end;`);
|
|
297
306
|
return out;
|
|
@@ -311,6 +320,9 @@ export const pascalBackend: LanguageBackend = {
|
|
|
311
320
|
// Nor has Pascal a `break`, so recovery must not mint a switch's mid-arm one either: a closed arm
|
|
312
321
|
// that leaves early spells as nested `if`s that copy the arm's tail into each path.
|
|
313
322
|
spellsSwitchBreak: false,
|
|
323
|
+
// Nor an early `return` (the `return` case below loud-fails one not in tail position), so
|
|
324
|
+
// structuring reads the return-tail copies rather than the join past early returns.
|
|
325
|
+
spellsEarlyReturn: false,
|
|
314
326
|
emit(fn0: SFn): string {
|
|
315
327
|
// The declaration list is put into the target's own frame order HERE, as the C family does it
|
|
316
328
|
// in its shared assembler — owned by `emit`, never by a `.emit(` call site (l3/slotorder.ts).
|
|
@@ -351,7 +363,7 @@ export const pascalBackend: LanguageBackend = {
|
|
|
351
363
|
lines.push('var', ...fn.locals.map((l) => ` ${l.name}: ${pasType(l.type)};`));
|
|
352
364
|
}
|
|
353
365
|
lines.push('begin');
|
|
354
|
-
fn.body.forEach((s, i) => lines
|
|
366
|
+
fn.body.forEach((s, i) => append(lines, ps(fn.name, s, ' ', i === fn.body.length - 1)));
|
|
355
367
|
lines.push('end;');
|
|
356
368
|
return lines.join('\n') + '\n';
|
|
357
369
|
},
|
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,7 +9,7 @@ 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
14
|
assignsIn,
|
|
15
15
|
exprChildren,
|
|
@@ -285,13 +285,15 @@ function dotSelectedAccess(e: Access): Access {
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
/** The executions on the paths through a statement list, split by how each path LEAVES it: falling off
|
|
288
|
-
* 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
|
|
289
290
|
* per-name maximum over the paths of that kind. */
|
|
290
291
|
interface PathCounts {
|
|
291
292
|
through: EffectCounts | null;
|
|
292
293
|
ret: EffectCounts | null;
|
|
293
294
|
brk: EffectCounts | null;
|
|
294
295
|
cont: EffectCounts | null;
|
|
296
|
+
end: EffectCounts | null;
|
|
295
297
|
}
|
|
296
298
|
|
|
297
299
|
const sum = (a: EffectCounts, b: EffectCounts): EffectCounts => combine(a, b, (x, y) => x + y);
|
|
@@ -303,6 +305,7 @@ const after = (c: EffectCounts, p: PathCounts): PathCounts => ({
|
|
|
303
305
|
ret: p.ret && sum(c, p.ret),
|
|
304
306
|
brk: p.brk && sum(c, p.brk),
|
|
305
307
|
cont: p.cont && sum(c, p.cont),
|
|
308
|
+
end: p.end && sum(c, p.end),
|
|
306
309
|
});
|
|
307
310
|
/** `a` then `b`: only a path that falls off the end of `a` reaches `b` */
|
|
308
311
|
const seq = (a: PathCounts, b: PathCounts): PathCounts =>
|
|
@@ -313,6 +316,7 @@ const seq = (a: PathCounts, b: PathCounts): PathCounts =>
|
|
|
313
316
|
ret: most(a.ret, b.ret && sum(a.through, b.ret)),
|
|
314
317
|
brk: most(a.brk, b.brk && sum(a.through, b.brk)),
|
|
315
318
|
cont: most(a.cont, b.cont && sum(a.through, b.cont)),
|
|
319
|
+
end: most(a.end, b.end && sum(a.through, b.end)),
|
|
316
320
|
};
|
|
317
321
|
/** whichever of the alternatives runs */
|
|
318
322
|
const either = (...ps: PathCounts[]): PathCounts => ({
|
|
@@ -320,14 +324,18 @@ const either = (...ps: PathCounts[]): PathCounts => ({
|
|
|
320
324
|
ret: most(...ps.map((p) => p.ret)),
|
|
321
325
|
brk: most(...ps.map((p) => p.brk)),
|
|
322
326
|
cont: most(...ps.map((p) => p.cont)),
|
|
327
|
+
end: most(...ps.map((p) => p.end)),
|
|
323
328
|
});
|
|
324
|
-
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 });
|
|
325
330
|
|
|
326
331
|
/** `total` = every occurrence in the tree; `paths` = the most any single syntactic path executes,
|
|
327
332
|
* by how it leaves `stmts`. A path that leaves by `return` runs nothing after it, so two sequenced
|
|
328
333
|
* `if (…) { f(); return; }` arms are one call on any path, not two. `unreached` collects every
|
|
329
334
|
* statement that follows one no path falls past: the path count skips it, so the caller refuses it
|
|
330
|
-
* 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. */
|
|
331
339
|
function countEffects(
|
|
332
340
|
stmts: Stmt[],
|
|
333
341
|
ctx: TreeContext,
|
|
@@ -336,7 +344,7 @@ function countEffects(
|
|
|
336
344
|
let total: EffectCounts = new Map();
|
|
337
345
|
let paths = falls(new Map());
|
|
338
346
|
for (const s of stmts) {
|
|
339
|
-
if (paths.through === null) {
|
|
347
|
+
if (paths.through === null && !(s.k === 'break' && s.label !== undefined && paths.end !== null)) {
|
|
340
348
|
unreached.push(s);
|
|
341
349
|
}
|
|
342
350
|
const own: EffectCounts = new Map();
|
|
@@ -354,31 +362,64 @@ function countEffects(
|
|
|
354
362
|
total = arms.reduce((acc, a) => sum(acc, a.total), sum(total, dflt.total));
|
|
355
363
|
// A fall-through arm continues into the NEXT one emitted (the last into `default`), so a
|
|
356
364
|
// path through arm i runs the chain starting at i — the shape the fall-through round's
|
|
357
|
-
// CRITICAL took.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const
|
|
366
|
-
|
|
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
|
+
});
|
|
367
400
|
} else if (stmtChildren(s).length > 0) {
|
|
368
401
|
// A loop: its sequenced children (a body, a `for`'s init/inc) counted ONCE — a loop's dynamic
|
|
369
402
|
// trip count is not a syntactic occurrence, and the IR side is static too. A `break` or
|
|
370
403
|
// `continue` in the body lands after the loop or at its test, and the test may exit without
|
|
371
|
-
// running the body.
|
|
404
|
+
// running the body. A case-end `goto` leaves the loop for its switch's end.
|
|
372
405
|
const body = countEffects(stmtChildren(s), ctx, unreached);
|
|
373
406
|
total = sum(total, body.total);
|
|
374
407
|
const p = body.paths;
|
|
375
|
-
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
|
+
});
|
|
376
415
|
} else if (s.k === 'return') {
|
|
377
|
-
here = { through: null, ret: own, brk: null, cont: null };
|
|
416
|
+
here = { through: null, ret: own, brk: null, cont: null, end: null };
|
|
378
417
|
} else if (s.k === 'break') {
|
|
379
|
-
here = { through: null, ret: null, brk: own, cont: null };
|
|
418
|
+
here = { through: null, ret: null, brk: own, cont: null, end: null };
|
|
380
419
|
} else if (s.k === 'continue') {
|
|
381
|
-
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 };
|
|
382
423
|
} else {
|
|
383
424
|
here = falls(own);
|
|
384
425
|
}
|
|
@@ -387,12 +428,24 @@ function countEffects(
|
|
|
387
428
|
return { total, paths };
|
|
388
429
|
}
|
|
389
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
|
+
|
|
390
442
|
/**
|
|
391
443
|
* Post structuring: every call and pinned access the asm makes is emitted, and none of those or of
|
|
392
444
|
* its declared volatile reads is emitted more times than the asm makes it on any one path. See the
|
|
393
445
|
* note above for what this deliberately does not cover.
|
|
394
446
|
*/
|
|
395
447
|
export function assertEffectsPreserved(fn: Fn, sfn: SFn): void {
|
|
448
|
+
assertCaseGotosNarrow(sfn);
|
|
396
449
|
// Reachable blocks only: an unreachable block's call is legitimately never emitted.
|
|
397
450
|
const seen = reachableBlocks(fn);
|
|
398
451
|
const irCalls: EffectCounts = new Map();
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// asmlift — what a straight-line ARM (A32) body reads of its argument registers, and the Thumb
|
|
2
|
+
// interworking veneer that reaches one.
|
|
3
|
+
//
|
|
4
|
+
// A GBA callee written in ARM is reached from Thumb through a veneer (`bx pc; nop` then ARM `b sym`),
|
|
5
|
+
// and the Thumb frontend lifts no ARM, so the callee-body bound (frontend/callee-body.ts) has nothing
|
|
6
|
+
// to lift. Where the body is straight-line, though, what it reads needs no lift: one pass over its
|
|
7
|
+
// instruction words, in order, says which argument register it reads before writing it.
|
|
8
|
+
//
|
|
9
|
+
// THE SCAN ANSWERS ONLY WHERE EVERY PATH IS THE ONE IT READ. It reads instruction words from the
|
|
10
|
+
// entry to the first `bx lr`, and answers nothing (undefined) on:
|
|
11
|
+
// - any instruction whose condition is not AL: a conditional one makes the body two paths;
|
|
12
|
+
// - any branch, call or other write of pc but that final `bx lr` (`b`, `bl`, `bx` of another
|
|
13
|
+
// register, `mov pc, …`, `ldr pc, …`, `ldm`/`stm`): the body is no longer the words that follow;
|
|
14
|
+
// - any write of lr, which makes the final `bx lr` go somewhere else;
|
|
15
|
+
// - any use of sp: a stack argument read is a fifth word, and a frame is no straight-line scratch;
|
|
16
|
+
// - any instruction it does not model (coprocessor, swi, status register moves, swap, long
|
|
17
|
+
// multiply, the halfword forms ARMv4T leaves undefined), and running out of words before `bx lr`.
|
|
18
|
+
// What it answers is the number of argument words read: one past the highest of r0-r3 that some
|
|
19
|
+
// instruction reads before any instruction writes it, and the final `bx lr` reads r0 where nothing
|
|
20
|
+
// wrote it, since r0 is the return register and holds the caller's first argument there (`void
|
|
21
|
+
// *id(void *p) { return p; }` is a lone `bx lr`), unless the callee is declared to return nothing.
|
|
22
|
+
// That is an UPPER bound on what the body reads of its arguments, as the callee-body bound is.
|
|
23
|
+
// KNOWN GAP: a return in r0:r1 that nothing declares counts r0 only.
|
|
24
|
+
|
|
25
|
+
/** The registers the scan counts argument words in, in order: the A32 procedure call standard's. */
|
|
26
|
+
export const ARM_ARGUMENT_REGISTERS: readonly string[] = ['r0', 'r1', 'r2', 'r3'];
|
|
27
|
+
|
|
28
|
+
/** Whether the scan's count is a count of `target`'s argument words: where the target passes its
|
|
29
|
+
* arguments in the registers it counts, so the caller's argument word k is the register the scan
|
|
30
|
+
* reads as k. Read off the ABI's `argRegs`, the datum the callers of the bound index it by. */
|
|
31
|
+
export function scansArgumentsOf(target: { readonly argRegs: readonly string[] }): boolean {
|
|
32
|
+
return (
|
|
33
|
+
target.argRegs.length === ARM_ARGUMENT_REGISTERS.length &&
|
|
34
|
+
target.argRegs.every((r, k) => r === ARM_ARGUMENT_REGISTERS[k])
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The argument words a straight-line ARM body reads at most, or undefined (see the header). `words`
|
|
39
|
+
* are its instruction words from the entry, in order; `returnsValue` is false only where the
|
|
40
|
+
* callee is declared to return nothing, so that its `bx lr` reads no r0. */
|
|
41
|
+
export function armArgumentWords(words: readonly number[], returnsValue = true): number | undefined {
|
|
42
|
+
const written = new Set<number>();
|
|
43
|
+
let reads = 0;
|
|
44
|
+
for (const w of words) {
|
|
45
|
+
if (w >>> 28 !== 0xe) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
if (w >>> 0 === 0xe12fff1e) {
|
|
49
|
+
return returnsValue && !written.has(0) ? Math.max(reads, 1) : reads; // bx lr, which returns r0
|
|
50
|
+
}
|
|
51
|
+
const effect = decode(w >>> 0);
|
|
52
|
+
if (effect === undefined) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const regs = [...effect.reads, ...effect.writes];
|
|
56
|
+
if (regs.includes(SP) || effect.writes.includes(LR) || effect.writes.includes(PC)) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
for (const r of effect.reads) {
|
|
60
|
+
if (r < 4 && !written.has(r)) {
|
|
61
|
+
reads = Math.max(reads, r + 1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
effect.writes.forEach((r) => written.add(r));
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const SP = 13;
|
|
70
|
+
const LR = 14;
|
|
71
|
+
const PC = 15;
|
|
72
|
+
|
|
73
|
+
interface Effect {
|
|
74
|
+
reads: number[];
|
|
75
|
+
writes: number[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const reg = (w: number, at: number): number => (w >>> at) & 0xf;
|
|
79
|
+
|
|
80
|
+
/** The registers one unconditional ARMv4T instruction reads and writes, or undefined for one this
|
|
81
|
+
* scan does not model. */
|
|
82
|
+
function decode(w: number): Effect | undefined {
|
|
83
|
+
// MUL / MLA: 0000 00AS dddd nnnn ssss 1001 mmmm
|
|
84
|
+
if ((w & 0x0fc000f0) === 0x00000090) {
|
|
85
|
+
const accumulate = (w >>> 21) & 1;
|
|
86
|
+
const reads = [reg(w, 0), reg(w, 8), ...(accumulate ? [reg(w, 12)] : [])];
|
|
87
|
+
return { reads, writes: [reg(w, 16)] };
|
|
88
|
+
}
|
|
89
|
+
// LDRH / STRH / LDRSB / LDRSH: 000P UIWL nnnn dddd oooo 1SH1 mmmm, SH ≠ 00
|
|
90
|
+
if ((w & 0x0e000090) === 0x00000090) {
|
|
91
|
+
const sh = (w >>> 5) & 3;
|
|
92
|
+
const load = (w >>> 20) & 1;
|
|
93
|
+
if (sh === 0 || (!load && sh !== 1)) {
|
|
94
|
+
return undefined; // swap, or a store form ARMv4T leaves undefined
|
|
95
|
+
}
|
|
96
|
+
const immediate = (w >>> 22) & 1;
|
|
97
|
+
return transfer(w, load === 1, immediate === 1 ? [] : [reg(w, 0)]);
|
|
98
|
+
}
|
|
99
|
+
// data processing: 00I oooo S nnnn dddd operand2
|
|
100
|
+
if ((w & 0x0c000000) === 0) {
|
|
101
|
+
const op = (w >>> 21) & 0xf;
|
|
102
|
+
const setsFlags = (w >>> 20) & 1;
|
|
103
|
+
const compare = op >= 8 && op <= 11;
|
|
104
|
+
if (compare && !setsFlags) {
|
|
105
|
+
return undefined; // MRS, MSR, BX and the rest of that space
|
|
106
|
+
}
|
|
107
|
+
const reads: number[] = [];
|
|
108
|
+
if (op !== 13 && op !== 15) {
|
|
109
|
+
reads.push(reg(w, 16)); // every op but MOV and MVN reads Rn
|
|
110
|
+
}
|
|
111
|
+
if (((w >>> 25) & 1) === 0) {
|
|
112
|
+
reads.push(reg(w, 0));
|
|
113
|
+
if ((w >>> 4) & 1) {
|
|
114
|
+
reads.push(reg(w, 8)); // shift by a register
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { reads, writes: compare ? [] : [reg(w, 12)] };
|
|
118
|
+
}
|
|
119
|
+
// LDR / STR / LDRB / STRB: 01I PUBW L nnnn dddd offset
|
|
120
|
+
if ((w & 0x0c000000) === 0x04000000) {
|
|
121
|
+
const registerOffset = (w >>> 25) & 1;
|
|
122
|
+
if (registerOffset && (w >>> 4) & 1) {
|
|
123
|
+
return undefined; // the undefined-instruction space
|
|
124
|
+
}
|
|
125
|
+
return transfer(w, ((w >>> 20) & 1) === 1, registerOffset ? [reg(w, 0)] : []);
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A single load or store: it reads its base and `offset`, a store its source too; a load writes
|
|
131
|
+
* its destination, and a post-indexed or write-back form its base. */
|
|
132
|
+
function transfer(w: number, load: boolean, offset: number[]): Effect | undefined {
|
|
133
|
+
const base = reg(w, 16);
|
|
134
|
+
const data = reg(w, 12);
|
|
135
|
+
const writeBack = ((w >>> 24) & 1) === 0 || ((w >>> 21) & 1) === 1;
|
|
136
|
+
if (data === PC || (writeBack && base === PC)) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
reads: [base, ...offset, ...(load ? [] : [data])],
|
|
141
|
+
writes: [...(load ? [data] : []), ...(writeBack ? [base] : [])],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Where a Thumb interworking veneer at a word-aligned `entry` branches to: its two halfwords are
|
|
146
|
+
* `bx pc` (0x4778) and `nop` (`mov r8, r8`, 0x46c0), which enter ARM state at `entry + 4`, and the
|
|
147
|
+
* ARM word there is an unconditional `b`. Undefined for anything else: the veneer is then some
|
|
148
|
+
* other code, and nothing here reads it. */
|
|
149
|
+
export function thumbToArmVeneerTarget(entry: number, h0: number, h1: number, armWord: number): number | undefined {
|
|
150
|
+
if (entry % 4 !== 0 || h0 !== 0x4778 || h1 !== 0x46c0 || (armWord & 0xff000000) >>> 0 !== 0xea000000) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
const offset = ((armWord & 0x00ffffff) << 8) >> 6; // sign-extended, in bytes
|
|
154
|
+
return (entry + 4 + 8 + offset) >>> 0;
|
|
155
|
+
}
|
package/src/frontend/asmdata.ts
CHANGED
|
@@ -45,7 +45,8 @@ export interface AsmSymbol {
|
|
|
45
45
|
index: number;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
/** The word at byte `off` of `b`, in the object's byte order. */
|
|
49
|
+
export const readU32 = (b: Uint8Array, off: number, big: boolean): number =>
|
|
49
50
|
big
|
|
50
51
|
? ((b[off] << 24) | (b[off + 1] << 16) | (b[off + 2] << 8) | b[off + 3]) >>> 0
|
|
51
52
|
: ((b[off + 3] << 24) | (b[off + 2] << 16) | (b[off + 1] << 8) | b[off]) >>> 0;
|