@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.
Files changed (68) hide show
  1. package/package.json +1 -1
  2. package/src/backend/c.ts +1 -0
  3. package/src/backend/cfamily.ts +33 -12
  4. package/src/backend/cpp.ts +1 -0
  5. package/src/backend/lines.ts +10 -0
  6. package/src/backend/pascal.ts +17 -5
  7. package/src/cdecl/parse.ts +21 -0
  8. package/src/contracts.ts +73 -20
  9. package/src/frontend/arm-body.ts +155 -0
  10. package/src/frontend/asmdata.ts +2 -1
  11. package/src/frontend/call-plan.ts +197 -45
  12. package/src/frontend/callee-body.ts +370 -0
  13. package/src/frontend/device-pins.ts +18 -8
  14. package/src/frontend/frame-objects.ts +504 -61
  15. package/src/frontend/frontend.ts +20 -0
  16. package/src/frontend/jump-table.ts +35 -20
  17. package/src/frontend/mips.ts +226 -8
  18. package/src/frontend/ppc.ts +26 -2
  19. package/src/frontend/register-struct-call.ts +1 -86
  20. package/src/frontend/ssa.ts +263 -1
  21. package/src/frontend/stackargs.ts +95 -17
  22. package/src/frontend/thumb-entry.ts +340 -0
  23. package/src/frontend/thumb.ts +410 -61
  24. package/src/ir/alias.ts +29 -8
  25. package/src/ir/bits.ts +1081 -66
  26. package/src/ir/core.ts +154 -4
  27. package/src/ir/entry.ts +3 -5
  28. package/src/ir/opcodes.ts +28 -0
  29. package/src/ir/parse.ts +1 -0
  30. package/src/ir/simplify.ts +61 -10
  31. package/src/ir/verify.ts +16 -0
  32. package/src/l3/argbase.ts +2 -0
  33. package/src/l3/ast.ts +294 -2
  34. package/src/l3/basecse.ts +35 -10
  35. package/src/l3/caseendgoto.ts +223 -0
  36. package/src/l3/coalesce.ts +42 -14
  37. package/src/l3/constinit.ts +34 -6
  38. package/src/l3/dce.ts +52 -8
  39. package/src/l3/ptrcell.ts +2 -0
  40. package/src/l3/regspell.ts +2 -0
  41. package/src/l3/reindex.ts +13 -4
  42. package/src/l3/scopebase.ts +2 -0
  43. package/src/l3/tailmerge.ts +2 -0
  44. package/src/pipeline.ts +507 -23
  45. package/src/proto-context.ts +135 -1
  46. package/src/raise/const.ts +25 -0
  47. package/src/raise/declared-volatile.ts +1 -1
  48. package/src/raise/divpow2.ts +192 -0
  49. package/src/raise/offsetnames.ts +412 -10
  50. package/src/raise/pre-recovery.ts +8 -5
  51. package/src/raise/pre-structure.ts +24 -5
  52. package/src/raise/reroll.ts +4 -24
  53. package/src/raise/shortcircuit.ts +192 -23
  54. package/src/raise/unpre.ts +594 -0
  55. package/src/rank-variations.ts +97 -4
  56. package/src/rank.ts +258 -43
  57. package/src/structure/analysis.ts +56 -10
  58. package/src/structure/cross-jumped-tail.ts +216 -0
  59. package/src/structure/globalaccess.ts +46 -1
  60. package/src/structure/structure.ts +4221 -368
  61. package/src/structure/switch-recover.ts +345 -26
  62. package/src/structure/unfolded-branch-constant.ts +90 -0
  63. package/src/symbols.ts +52 -15
  64. package/src/target.ts +269 -8
  65. package/src/trace.ts +28 -14
  66. package/src/variation-definitions.ts +207 -6
  67. package/src/variation-gates.ts +2 -0
  68. package/src/variation-tokens.ts +63 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asmlift/core",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Match decompile an assembly function to C or Pascal",
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
@@ -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.push(...printStmt(t, indent + ' ', vt, leaf));
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.push(...printStmt(e, indent + ' ', vt, leaf));
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.push(...printStmt(t, indent + ' ', vt, leaf));
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.push(...printStmt(t, indent + ' ', vt, leaf));
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 out = [`${indent}for (${clause(s.init)}; ${pe(s.cond, 99)}; ${clause(s.inc)}) {`];
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.push(...printStmt(t, indent + ' ', vt, leaf));
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.push(...printStmt(t, bi, vt, leaf));
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.push(...printStmt(t, bi, vt, leaf));
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.push(...staticDefinition(st));
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.push(...printStmt(s, ' ', vt, leaf));
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 (
@@ -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
+ }
@@ -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.push(...block(s.then, indent + ' ', s.else.length > 0));
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.push(...block(s.else, indent + ' '));
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(', ')}:`, ...arm(c.body, indent + ' '));
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.push(...arm(s.default, indent + ' '));
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.push(...ps(fn.name, s, ' ', i === fn.body.length - 1)));
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
  },
@@ -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 `continue`. Null when no path leaves that way. Each is the
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. Built from the end; `chain[i]` is that arm's paths.
358
- const chain: PathCounts[] = new Array(arms.length);
359
- for (let i = arms.length - 1; i >= 0; i--) {
360
- const next = i + 1 < arms.length ? chain[i + 1] : dflt.paths;
361
- chain[i] = s.cases[i].fallsThrough ? seq(arms[i].paths, next) : arms[i].paths;
362
- }
363
- // No `default` is one more alternative: no arm runs. A `break` leaves the switch, so it
364
- // continues after it as a fall-off-the-end does.
365
- const inner = either(...chain, s.default ? dflt.paths : falls(new Map()));
366
- here = after(own, { through: most(inner.through, inner.brk), ret: inner.ret, brk: null, cont: inner.cont });
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, { through: most(p.through, p.brk, p.cont, new Map()), ret: p.ret, brk: null, cont: null });
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
+ }
@@ -45,7 +45,8 @@ export interface AsmSymbol {
45
45
  index: number;
46
46
  }
47
47
 
48
- const readU32 = (b: Uint8Array, off: number, big: boolean): number =>
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;