@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
@@ -0,0 +1,223 @@
1
+ // L3 respell variation `/case-end-goto`: a search loop's hit flag, read once after its loop where the
2
+ // code after the loop ends the switch arm the loop sits in, rewritten as the source's `goto` to that
3
+ // arm's end — the case-end goto (l3/ast.ts `caseEndGoto`, docs/asmlift-101.md §2.7):
4
+ //
5
+ // f = 0; for (j…) {
6
+ // for (j…) { if (hit) { s(j); goto case_end; }
7
+ // if (hit) { s(j); f = 1; break; } }
8
+ // } → k(i);
9
+ // if (f == 0) { k(i); } case_end:
10
+ // break; break;
11
+ //
12
+ // The structurer writes the flag where a two-level exit's second target lies past the code after
13
+ // the loop (structure.ts `TwoLevelExit`, `emitForeverExit`). Where that target is the end of the
14
+ // switch arm, agbcc threads the source's goto past the loop around the switch (jump.c
15
+ // `follow_jumps`), and on agbcc, KMC, gcc 2.7.2 and IDO the goto compiles to the source's bytes
16
+ // where the flag never does (`synthetic:sw_hitexit`); but inside asmlift's own candidates the flag
17
+ // can still score closer (gcc 2.7.2), so both are emitted, the differ referees, and a tie goes to
18
+ // the candidate without the goto (rank.ts `gotoCount`).
19
+ //
20
+ // The structurer is not the only writer the rewrite reaches: it reads the tree, not who built it,
21
+ // so a flag the source declared and the compiler kept is rewritten too where it has the same shape
22
+ // (IDO keeps a `u8 found` a source sets to 2 in a case arm, and lifts it as that flag). That is
23
+ // sound for the reason below, which reads only the tree, and ranked like the rest.
24
+ //
25
+ // SOUND OVER THE TREE ALONE. `f` is a local mentioned only by `f = 0;` right before the loop, by
26
+ // each hit's `f = c; break;` (c a nonzero constant, the `break` the loop's own: reached from the
27
+ // loop's body through `if`s only) and by the test right after the loop (`CASE_END_GOTO_GATES`
28
+ // `flag-elsewhere`). So after the loop `f == 0` exactly when it left by any other way than a hit,
29
+ // and `X` runs exactly then — which is what `goto L;` at each hit and `X` unguarded after the loop
30
+ // run. After `X` both spellings go on to whatever follows the `if`, and that is the arm's end with
31
+ // nothing run on the way (`target-not-arm-end`): a switch `break;` right after the `if`, which takes
32
+ // the label, or the `if` last in a list whose falling off runs nothing before leaving the switch,
33
+ // which gets a labelled `break;` appended. `L: break;` leaves the switch as falling off the arm
34
+ // does. The label sits in the loop's own list, no loop between it and its arm, and
35
+ // the hits' `break`s are reached through `if`s only, so no switch stands between them and it: the
36
+ // form `caseGotoViolation` holds every case-end goto to.
37
+ //
38
+ // A RESPELL, NOT A STRUCTURE VARIATION — the third fork of docs/level-tower.md. The rewrite reads
39
+ // nothing but the flag's tree, so asked of the structured tree it reaches every site a structure
40
+ // option inside `emitForeverExit` would. A structure variation doubles the settings every
41
+ // enumeration walks even where it changes no tree, which level-tower.md measures on this one; the
42
+ // respell costs one walk of each structured tree, and nothing more where no flag is read after its
43
+ // loop. What it gives up is composition with the other pre-respell variation (`/unmerge` runs on
44
+ // the structured tree beside it, not on its output); no row has asked for both.
45
+ //
46
+ // Outer sites first, and an inner site whose `if` the outer rewrite leaves right before the outer
47
+ // label takes that label, so two search loops in sequence into one case end share one `L:`.
48
+ import { type Expr, type Loop, type SFn, type Stmt, type SwitchCase, isLoop, mapStmtLists, stmtChildren } from './ast';
49
+ import { type Gate, firstRejection } from './gates';
50
+ import { localMentions, readsOf } from './mentions';
51
+
52
+ /** A flag read after its loop that the rewrite would replace (`CASE_END_GOTO_GATES`). */
53
+ export interface CaseEndSite {
54
+ /** what runs after the `if (f == 0)` is not the arm's end: no switch `break;` follows it, and it
55
+ * is not last in a list whose falling off leaves the switch with nothing run. Outside a switch
56
+ * arm, or in a loop inside one, neither can hold (a switch `break;` sits only in an arm with no
57
+ * loop between, `refuseMisboundBreak`), so this is also the rule that keeps such a flag */
58
+ readonly targetNotArmEnd: boolean;
59
+ /** `f` is a param or global, or mentioned anywhere but its `f = 0;`, its hits and its one test */
60
+ readonly flagElsewhere: boolean;
61
+ }
62
+
63
+ /** When a flag read after its loop is rewritten as a case-end goto. The argument is the file's. */
64
+ export const CASE_END_GOTO_GATES: readonly Gate<CaseEndSite>[] = [
65
+ {
66
+ id: 'target-not-arm-end',
67
+ why: 'the goto skips only the guarded code, so what follows it must leave the switch with nothing run',
68
+ sound: true,
69
+ guardedBy: [
70
+ 'case-end-goto.test.ts: a hit whose target is a join running code keeps its flag',
71
+ 'case-end-goto.test.ts: a flag read outside a switch arm, or in a loop inside one, keeps its flag',
72
+ ],
73
+ rejects: (c) => c.targetNotArmEnd,
74
+ },
75
+ {
76
+ id: 'flag-elsewhere',
77
+ why: 'the flag is zero after the loop exactly when no hit ran only if nothing else writes or reads it',
78
+ sound: true,
79
+ guardedBy: 'case-end-goto.test.ts: a flag read or written anywhere else keeps its flag',
80
+ rejects: (c) => c.flagElsewhere,
81
+ },
82
+ ];
83
+
84
+ /** `f == 0`, the structurer's test of a flag (structure.ts `emitForeverExit`) */
85
+ const flagTest = (e: Expr): string | null =>
86
+ e.k === 'bin' && e.op === '==' && e.l.k === 'var' && e.r.k === 'const' && e.r.value === 0 ? e.l.name : null;
87
+
88
+ /** The hits in `body`, a loop's body: each `f = c; break;` (c a nonzero constant) reached through
89
+ * `if`s only, so the `break` is that loop's own. */
90
+ function hitsIn(body: readonly Stmt[], flag: string): number {
91
+ return body.reduce((n, s, i) => {
92
+ const next = body[i + 1];
93
+ if (
94
+ s.k === 'assign' &&
95
+ s.name === flag &&
96
+ s.value.k === 'const' &&
97
+ s.value.value !== 0 &&
98
+ next?.k === 'break' &&
99
+ next.ofSwitch !== true &&
100
+ next.label === undefined
101
+ ) {
102
+ return n + 1;
103
+ }
104
+ return s.k === 'if' ? n + hitsIn(s.then, flag) + hitsIn(s.else, flag) : n;
105
+ }, 0);
106
+ }
107
+
108
+ /** `body` with each hit `hitsIn` counts written `goto label;` */
109
+ function jumpHits(body: readonly Stmt[], flag: string, label: string): Stmt[] {
110
+ return body.flatMap((s, i): Stmt[] => {
111
+ const prev = body[i - 1];
112
+ if (s.k === 'break' && prev?.k === 'assign' && prev.name === flag && s.ofSwitch !== true) {
113
+ return [];
114
+ }
115
+ if (s.k === 'assign' && s.name === flag) {
116
+ return [{ k: 'caseEndGoto', label }];
117
+ }
118
+ return s.k === 'if' ? [{ ...s, then: jumpHits(s.then, flag, label), else: jumpHits(s.else, flag, label) }] : [s];
119
+ });
120
+ }
121
+
122
+ /** `f = 0; LOOP; if (f == 0) { X }` starting at `list[i]`, the structurer's flag read after its loop */
123
+ function siteAt(
124
+ list: readonly Stmt[],
125
+ i: number,
126
+ ): { flag: string; loop: Loop; test: Extract<Stmt, { k: 'if' }> } | null {
127
+ const [init, loop, test] = [list[i], list[i + 1], list[i + 2]];
128
+ if (
129
+ init?.k !== 'assign' ||
130
+ init.value.k !== 'const' ||
131
+ init.value.value !== 0 ||
132
+ loop === undefined ||
133
+ !isLoop(loop) ||
134
+ test?.k !== 'if' ||
135
+ test.else.length > 0 ||
136
+ flagTest(test.cond) !== init.name
137
+ ) {
138
+ return null;
139
+ }
140
+ return { flag: init.name, loop, test };
141
+ }
142
+
143
+ /** Every flag read after its loop where the code after the loop ends its switch arm, rewritten as
144
+ * a case-end goto, or null where none is (the variation's decline). */
145
+ export function jumpToCaseEnds(sfn: SFn, gates: readonly Gate<CaseEndSite>[] = CASE_END_GOTO_GATES): SFn | null {
146
+ const mentions = localMentions(sfn);
147
+ const labels = new Set<string>();
148
+ const collect = (s: Stmt): void => {
149
+ if (s.k === 'switch') {
150
+ s.cases.forEach((c) => c.label !== undefined && labels.add(c.label));
151
+ } else if (s.k === 'break' && s.label !== undefined) {
152
+ labels.add(s.label);
153
+ }
154
+ stmtChildren(s).forEach(collect);
155
+ };
156
+ sfn.body.forEach(collect);
157
+ const fresh = (): string => {
158
+ let label = 'case_end';
159
+ for (let n = 2; labels.has(label); n++) {
160
+ label = `case_end_${n}`;
161
+ }
162
+ labels.add(label);
163
+ return label;
164
+ };
165
+ const dropped = new Set<string>();
166
+
167
+ /** `inArm`: the list is inside a switch arm with no loop between; `leaves`: falling off its end
168
+ * leaves that switch with nothing run on the way. */
169
+ const rewrite = (list: readonly Stmt[], inArm: boolean, leaves: boolean): Stmt[] => {
170
+ const out = [...list];
171
+ for (let i = 0; i < out.length; i++) {
172
+ const site = siteAt(out, i);
173
+ if (site !== null) {
174
+ const { flag, loop, test } = site;
175
+ const after = out[i + 3];
176
+ const end = after?.k === 'break' && after.ofSwitch === true ? after : undefined;
177
+ const m = mentions.get(flag);
178
+ const hits = hitsIn(loop.body, flag);
179
+ const refused = firstRejection(gates, {
180
+ targetNotArmEnd: end === undefined && !(after === undefined && leaves),
181
+ flagElsewhere:
182
+ m === undefined || hits === 0 || m.assigns !== hits + 1 || readsOf(m) !== 1 || m.addrTaken !== 0,
183
+ });
184
+ if (refused === null) {
185
+ const label = end?.label ?? fresh();
186
+ const jumped: Stmt = { ...loop, body: jumpHits(loop.body, flag, label) };
187
+ out.splice(i, end ? 4 : 3, descend(jumped, false, undefined, false), ...test.then, {
188
+ k: 'break',
189
+ ofSwitch: true,
190
+ label,
191
+ });
192
+ dropped.add(flag);
193
+ // `X` is visited next, in this list, so a site inside it whose `if` now sits right before
194
+ // the label takes that label
195
+ continue;
196
+ }
197
+ }
198
+ out[i] = descend(out[i], inArm, out[i + 1], i === out.length - 1 && leaves);
199
+ }
200
+ return out;
201
+ };
202
+ /** `s` with its lists rewritten; `next` follows it in its list, and `last` says it ends a list
203
+ * whose falling off leaves the switch */
204
+ const descend = (s: Stmt, inArm: boolean, next: Stmt | undefined, last: boolean): Stmt => {
205
+ if (s.k === 'switch') {
206
+ const arm = (c: SwitchCase): SwitchCase => ({ ...c, body: rewrite(c.body, true, !c.fallsThrough) });
207
+ return { ...s, cases: s.cases.map(arm), ...(s.default ? { default: rewrite(s.default, true, true) } : {}) };
208
+ }
209
+ if (isLoop(s)) {
210
+ return mapStmtLists(s, (l) => rewrite(l, false, false));
211
+ }
212
+ if (s.k === 'if') {
213
+ const leaves = inArm && ((next?.k === 'break' && next.ofSwitch === true) || (next === undefined && last));
214
+ return { ...s, then: rewrite(s.then, inArm, leaves), else: rewrite(s.else, inArm, leaves) };
215
+ }
216
+ return s;
217
+ };
218
+ const body = rewrite(sfn.body, false, false);
219
+ if (dropped.size === 0) {
220
+ return null;
221
+ }
222
+ return { ...sfn, body, locals: sfn.locals.filter((l) => !dropped.has(l.name)) };
223
+ }
@@ -12,7 +12,16 @@
12
12
  // and whether fall-through joins the two arms onto one path. `coalesceCandidates` offers both.
13
13
  import { typeToString } from '../ir/types';
14
14
  import type { Expr, SFn, Stmt } from './ast';
15
- import { exprChildren, isLoop, mapExprChildren, mentionedName, stmtChildren, stmtExprs } from './ast';
15
+ import {
16
+ exprChildren,
17
+ isLoop,
18
+ mapExprChildren,
19
+ mentionedName,
20
+ stmtChildren,
21
+ stmtExprs,
22
+ switchArmSuccessors,
23
+ switchJumpsBack,
24
+ } from './ast';
16
25
  import { type Gate, firstRejection } from './gates';
17
26
 
18
27
  function namesIn(e: Expr, out: Set<string>): void {
@@ -122,8 +131,12 @@ function spans(body: Stmt[]): Map<string, Span> {
122
131
  };
123
132
  /** the loop set a while/do-while's own condition and children run under — its own. A `for` is
124
133
  * routed separately: `stmtChildren` hands back its init too, and the init is the one child its
125
- * loop does not re-run. */
126
- const under = (s: Stmt, loops: readonly Stmt[]): readonly Stmt[] => (isLoop(s) ? [...loops, s] : loops);
134
+ * loop does not re-run. A switch whose case-label `goto` lands on an arm laid out above it
135
+ * (`switchJumpsBack`) counts too: that jump is a back edge with no loop statement, and the arm
136
+ * it lands on runs after the one that jumps, so a local the lower arm writes can clobber one the
137
+ * upper arm still reads. */
138
+ const under = (s: Stmt, loops: readonly Stmt[]): readonly Stmt[] =>
139
+ isLoop(s) || (s.k === 'switch' && switchJumpsBack(s)) ? [...loops, s] : loops;
127
140
  const walk = (list: Stmt[], loops: readonly Stmt[]): void => {
128
141
  for (const s of list) {
129
142
  // A `for`'s INIT runs ONCE, ahead of the condition and outside the loop; its cond, inc and
@@ -208,9 +221,11 @@ export interface MergePair {
208
221
  * earlier one — and a back edge returns only to the head of its OWN loop, whose body is one
209
222
  * contiguous preorder range. So the reordering can reach a PAIR only when some loop holds a
210
223
  * mention of both locals: there the survivor's write can be followed, on the next iteration, by
211
- * the absorbed local's read. Where no loop holds both — two sibling loops, or one local living
212
- * before the loop the other lives in — no back edge connects the two ranges and preorder IS
213
- * execution order. The ancestor chain is what the span records, not the innermost loop: an outer
224
+ * the absorbed local's read. The one other back edge L3 holds is a case-label `goto` aimed at an
225
+ * arm laid out above it, which returns into its own switch's range the same way, so `spans`
226
+ * records such a switch as a loop and this rule covers it. Where no loop holds both — two sibling
227
+ * loops, or one local living before the loop the other lives in — no back edge connects the two
228
+ * ranges and preorder IS execution order. The ancestor chain is what the span records, not the innermost loop: an outer
214
229
  * loop re-runs an inner one's statements, so it can reorder a pair that no inner loop shares.
215
230
  *
216
231
  * `first-is-write` AND `const-fed` ARE INDEPENDENT, and neither masks the other: a survivor whose
@@ -332,8 +347,9 @@ export interface ArmPair {
332
347
  b: string;
333
348
  /** the confining branch has a loop ancestor, so it can run more than once */
334
349
  ifInLoop: boolean;
335
- /** control can run from one of the two arms INTO the other — `switch` fall-through, the one
336
- * way two arms of a branch land on a single path. Always false for an `if`. */
350
+ /** control can run from one of the two arms INTO the other — `switch` fall-through or a
351
+ * case-label `goto`, the two ways two arms of a branch land on a single path. Always false for
352
+ * an `if`. */
337
353
  armsJoined: boolean;
338
354
  sameType: boolean;
339
355
  /** either local is object-volatile or carries a pointee-volatile qualifier (see MergePair) */
@@ -498,9 +514,10 @@ function mentionIndex(): {
498
514
  * `switch` are one rule here rather than two walkers.
499
515
  *
500
516
  * An `if` has exactly two arms and no way to reach one from the other. A `switch`'s arms are its
501
- * `case` bodies, and FALL-THROUGH is the one way two of them land on a single path: arm `i` runs
502
- * into arm `j` exactly when every arm from `i` up to `j` falls through, so the reach is the
503
- * TRANSITIVE chain and not merely the adjacent pair.
517
+ * `case` bodies, and two of them land on a single path where one runs on into the other: by
518
+ * FALL-THROUGH, or by the case-label `goto` that ends an arm (l3/ast.ts), which lands where a
519
+ * fall-through into its target would. The reach is the TRANSITIVE chain of both and not merely
520
+ * the adjacent pair.
504
521
  *
505
522
  * ITS REACH, MEASURED, because a soundness argument with no corpus witness should say so. Counted
506
523
  * 2026-09-19 with a throwaway counter in this walk, over `pnpm bench sweep --fan --map-modes
@@ -522,10 +539,21 @@ const branchArms = (st: Stmt): { arms: Stmt[][]; joined: (i: number, j: number)
522
539
  return { arms: [st.then, st.else], joined: () => false };
523
540
  }
524
541
  if (st.k === 'switch') {
525
- const { cases } = st;
542
+ // Arm `i` runs on into the arms `switchArmSuccessors` chains it to, by falling through or by the
543
+ // `goto` that ends it (l3/ast.ts) — two arms on one path either way — so `reach[i]` is that
544
+ // chain, the `default` included as a node (a case falling into it and jumping out of it puts
545
+ // both on the path), though never offered as an arm itself.
546
+ const next = switchArmSuccessors(st);
547
+ const reach = next.map((_, i) => {
548
+ const seen = new Set<number>();
549
+ for (let at = next[i]; at !== undefined && !seen.has(at); at = next[at]) {
550
+ seen.add(at);
551
+ }
552
+ return seen;
553
+ });
526
554
  return {
527
- arms: cases.map((c) => c.body),
528
- joined: (i, j) => cases.slice(Math.min(i, j), Math.max(i, j)).every((c) => c.fallsThrough),
555
+ arms: st.cases.map((c) => c.body),
556
+ joined: (i, j) => reach[i].has(j) || reach[j].has(i),
529
557
  };
530
558
  }
531
559
  return null;
@@ -338,17 +338,45 @@ export function constantImageRefusal(sfn: SFn): { image: string; refused: string
338
338
  * signedness keeps its cast, which is the element type it spells. */
339
339
  function asElement(e: Expr, by: ReadonlyMap<string, Fold>): Expr {
340
340
  const inner = mapExprChildren(e, (c) => asElement(c, by));
341
- if (inner.k !== 'index' || (inner.lead?.length ?? 0) > 0 || inner.baseElem !== undefined) {
341
+ // …and a read of the element's BYTES at the other signedness, converted to the element's own
342
+ // type, IS the element: `(s8)((u8 *)a)[i]` is `a[i]` on `s8 a[16]`. agbcc has no `ldrsb` with an
343
+ // immediate offset, so a signed byte read through the frame base is `ldrb` and `lsl #24; asr #24`,
344
+ // which the lift reads as the unsigned byte sign-extended; C extends the element itself the same
345
+ // way. Only a conversion to exactly the element type: a wider or a narrower one is another value.
346
+ // A DECLARED array's byte (a map global's, a proven frame part's) is read signed before this, by
347
+ // the structurer (structure.ts `declaredExtendedRead`); the type this pass gives a local exists
348
+ // only here, so for those the identity is applied here.
349
+ if (inner.k === 'cast' && inner.to.kind === 'int' && inner.volatile !== true && inner.e.k === 'index') {
350
+ const read = inner.e;
351
+ const elem = elementOf(read, by);
352
+ if (elem !== undefined && elem.width === inner.to.width && elem.signed === inner.to.signed) {
353
+ return asElement({ ...read, signed: elem.signed }, by);
354
+ }
355
+ }
356
+ if (inner.k !== 'index') {
342
357
  return inner;
343
358
  }
359
+ const elem = elementOf(inner, by);
360
+ if (elem === undefined || elem.signed !== inner.signed) {
361
+ return inner;
362
+ }
363
+ return { ...inner, base: { k: 'var', name: wholeObject(inner.base)! }, subscript: true };
364
+ }
365
+
366
+ /** The element type of the folded local `read` reads one element of — at the element's width,
367
+ * whatever its signedness — or undefined for any other access. */
368
+ function elementOf(
369
+ read: Extract<Expr, { k: 'index' }>,
370
+ by: ReadonlyMap<string, Fold>,
371
+ ): Extract<IrType, { kind: 'int' }> | undefined {
372
+ if ((read.lead?.length ?? 0) > 0 || read.baseElem !== undefined) {
373
+ return undefined;
374
+ }
344
375
  // the object the base names whole, as the fold's own census of element accesses reads it (`uses`)
345
- const name = wholeObject(inner.base);
376
+ const name = wholeObject(read.base);
346
377
  const fold = name === null ? undefined : by.get(name);
347
378
  const elem = fold?.type.kind === 'array' ? fold.type.elem : undefined;
348
- if (elem?.kind !== 'int' || elem.width !== inner.width * 8 || elem.signed !== inner.signed) {
349
- return inner;
350
- }
351
- return { ...inner, base: { k: 'var', name: name! }, subscript: true };
379
+ return elem?.kind === 'int' && elem.width === read.width * 8 ? elem : undefined;
352
380
  }
353
381
 
354
382
  /** `sfn` with every anonymous image folded into the initializer of each local its copies fill,
package/src/l3/dce.ts CHANGED
@@ -25,7 +25,7 @@
25
25
  // unresolved `?` value trips the contract first and never reaches DCE; `mustKeep` treating `?` as
26
26
  // keep is defense-in-depth for any future caller that skips that check.
27
27
  import type { Expr, SFn, Stmt } from './ast';
28
- import { exprChildren, keepsHelperRel, negateCond, stmtChildren, stmtExprs } from './ast';
28
+ import { caseEndGotoLeaves, exprChildren, keepsHelperRel, negateCond, stmtChildren, stmtExprs } from './ast';
29
29
 
30
30
  /** Accumulate every LOCAL-eligible `var` name read anywhere in `e` (recurses all sub-exprs). An
31
31
  * `addr` name counts too: taking a local's address makes every store to it observable through the
@@ -106,6 +106,24 @@ function allAddrNamesInto(stmts: Stmt[], out: Set<string>): void {
106
106
  interface JumpLive {
107
107
  brk: ReadonlySet<string>;
108
108
  cont?: ReadonlySet<string>; // absent in a switch no loop encloses
109
+ /** live at a case label of the innermost switch: what its `goto` (l3/ast.ts) lands on — absent
110
+ * where no switch encloses the statement, or a loop stands between it and the switch */
111
+ caseLabel?: ReadonlySet<string>;
112
+ /** live at the end of the innermost switch arm around the statement, loops between included: what
113
+ * a case-end `goto` lands on — absent where no switch encloses it */
114
+ caseEnd?: ReadonlySet<string>;
115
+ }
116
+
117
+ /** The conservative set live throughout a loop: what is live after it and every read in it, and,
118
+ * where a case-end `goto` in its body leaves it, what is live at the arm's end that goto lands on —
119
+ * a later iteration may take it. */
120
+ function loopLiveOf(s: Stmt & { body: Stmt[] }, live: ReadonlySet<string>, jumps: JumpLive | null): Set<string> {
121
+ const loopLive = new Set(live);
122
+ allReadsInto([s], loopLive);
123
+ if (jumps?.caseEnd && caseEndGotoLeaves(s.body)) {
124
+ jumps.caseEnd.forEach((n) => loopLive.add(n));
125
+ }
126
+ return loopLive;
109
127
  }
110
128
 
111
129
  /** Backward live-variable walk over one block. `liveOut` is the set of locals live on exit;
@@ -165,6 +183,26 @@ function dceBlock(
165
183
  rev.push(s);
166
184
  break;
167
185
  }
186
+ // The jump lands on the `break;` that ends its arm (l3/ast.ts, contracts.ts
187
+ // `assertCaseGotosNarrow`), whose every read the switch below already counts live.
188
+ case 'caseEndGoto': {
189
+ if (!jumps?.caseEnd) {
190
+ throw new Error('dce: a case-end goto outside a switch arm');
191
+ }
192
+ setLive(new Set(jumps.caseEnd));
193
+ rev.push(s);
194
+ break;
195
+ }
196
+ case 'goto': {
197
+ // The jump lands on a case label of the switch it sits in (l3/ast.ts, contracts.ts
198
+ // `assertCaseGotosNarrow`), whose every read the switch below already counts live.
199
+ if (!jumps?.caseLabel) {
200
+ throw new Error('dce: a goto outside the switch whose case it names');
201
+ }
202
+ setLive(new Set(jumps.caseLabel));
203
+ rev.push(s);
204
+ break;
205
+ }
168
206
  case 'break':
169
207
  case 'continue': {
170
208
  // A mid-body `if (c) { v = x; break; }` carries the loop's exit value in `v`, while the
@@ -207,9 +245,12 @@ function dceBlock(
207
245
  case 'dowhile': {
208
246
  // Conservative: any local read anywhere in the loop is live throughout it, so a
209
247
  // loop-carried store is never cut. Body DCE removes only what is dead on EVERY path.
210
- const loopLive = new Set(live);
211
- allReadsInto([s], loopLive);
212
- const b = dceBlock(s.body, loopLive, locals, volatiles, { brk: loopLive, cont: loopLive });
248
+ const loopLive = loopLiveOf(s, live, jumps);
249
+ const b = dceBlock(s.body, loopLive, locals, volatiles, {
250
+ brk: loopLive,
251
+ cont: loopLive,
252
+ caseEnd: jumps?.caseEnd,
253
+ });
213
254
  const nlive = new Set(loopLive);
214
255
  for (const r of b.liveIn) {
215
256
  nlive.add(r);
@@ -219,9 +260,12 @@ function dceBlock(
219
260
  break;
220
261
  }
221
262
  case 'for': {
222
- const loopLive = new Set(live);
223
- allReadsInto([s], loopLive);
224
- const b = dceBlock(s.body, loopLive, locals, volatiles, { brk: loopLive, cont: loopLive });
263
+ const loopLive = loopLiveOf(s, live, jumps);
264
+ const b = dceBlock(s.body, loopLive, locals, volatiles, {
265
+ brk: loopLive,
266
+ cont: loopLive,
267
+ caseEnd: jumps?.caseEnd,
268
+ });
225
269
  const nlive = new Set(loopLive);
226
270
  for (const r of b.liveIn) {
227
271
  nlive.add(r);
@@ -235,7 +279,7 @@ function dceBlock(
235
279
  // read anywhere in the switch as live throughout — no case-body store is ever cut.
236
280
  const swLive = new Set(live);
237
281
  allReadsInto([s], swLive);
238
- const inSwitch: JumpLive = { brk: swLive, cont: jumps?.cont };
282
+ const inSwitch: JumpLive = { brk: swLive, cont: jumps?.cont, caseLabel: swLive, caseEnd: swLive };
239
283
  const cases = s.cases.map((c) => ({ ...c, body: dceBlock(c.body, swLive, locals, volatiles, inSwitch).out }));
240
284
  const def = s.default ? dceBlock(s.default, swLive, locals, volatiles, inSwitch).out : s.default;
241
285
  const nlive = new Set(swLive);
package/src/l3/ptrcell.ts CHANGED
@@ -227,6 +227,8 @@ function mapIntegerSlots(sfn: SFn, cells: ReadonlySet<string>, rules: SlotRules)
227
227
  };
228
228
  case 'break':
229
229
  case 'continue':
230
+ case 'goto':
231
+ case 'caseEndGoto':
230
232
  return s;
231
233
  }
232
234
  };
@@ -101,6 +101,8 @@ function renameInStmt(s: Stmt, from: string, to: string): Stmt {
101
101
  };
102
102
  case 'break':
103
103
  case 'continue':
104
+ case 'goto':
105
+ case 'caseEndGoto':
104
106
  return s;
105
107
  }
106
108
  }
package/src/l3/reindex.ts CHANGED
@@ -93,6 +93,8 @@ import {
93
93
  Expr,
94
94
  SFn,
95
95
  Stmt,
96
+ SwitchCase,
97
+ caseEndGotoLeaves,
96
98
  mapExprChildren,
97
99
  mapStmtExprs,
98
100
  mentionedName,
@@ -303,8 +305,13 @@ export const COUNTDOWN_GATES: readonly Gate<CountdownCtx>[] = [
303
305
 
304
306
  /** A `break`/`continue` that would target the ENCLOSING loop: found at if/switch depth, but not
305
307
  * inside a nested loop (whose own exits target itself). A switch's own `break` (`ofSwitch`)
306
- * leaves only its switch, and the loop's tail still runs after it, so it is not one. */
308
+ * leaves only its switch, and the loop's tail still runs after it, so it is not one. A case-end
309
+ * `goto` whose label lies outside `stmts` leaves every loop it sits in, at any depth. */
307
310
  function hasLoopExit(stmts: Stmt[]): boolean {
311
+ return caseEndGotoLeaves(stmts) || hasShallowLoopExit(stmts);
312
+ }
313
+
314
+ function hasShallowLoopExit(stmts: Stmt[]): boolean {
308
315
  return stmts.some((s) => {
309
316
  switch (s.k) {
310
317
  case 'break':
@@ -312,9 +319,9 @@ function hasLoopExit(stmts: Stmt[]): boolean {
312
319
  case 'continue':
313
320
  return true;
314
321
  case 'if':
315
- return hasLoopExit(s.then) || hasLoopExit(s.else);
322
+ return hasShallowLoopExit(s.then) || hasShallowLoopExit(s.else);
316
323
  case 'switch':
317
- return s.cases.some((c) => hasLoopExit(c.body)) || hasLoopExit(s.default ?? []);
324
+ return s.cases.some((c) => hasShallowLoopExit(c.body)) || hasShallowLoopExit(s.default ?? []);
318
325
  default:
319
326
  return false;
320
327
  }
@@ -502,12 +509,14 @@ function reindexStmt(s: Stmt, walk: WalkLoop, iv: string): Stmt | null {
502
509
  return {
503
510
  ...s,
504
511
  scrutinee,
505
- cases: cases as { values: number[]; body: Stmt[]; fallsThrough: boolean }[],
512
+ cases: cases as SwitchCase[],
506
513
  default: dflt ?? undefined,
507
514
  };
508
515
  }
509
516
  case 'break':
510
517
  case 'continue':
518
+ case 'goto':
519
+ case 'caseEndGoto':
511
520
  return s;
512
521
  }
513
522
  }
@@ -855,6 +855,8 @@ export function applyScopedBasePlan(sfn: SFn, { entries: plan, repoint, compound
855
855
  };
856
856
  case 'break':
857
857
  case 'continue':
858
+ case 'goto':
859
+ case 'caseEndGoto':
858
860
  return s;
859
861
  }
860
862
  };
@@ -118,6 +118,8 @@ function rewrite(s: Stmt): Stmt[] {
118
118
  case 'return':
119
119
  case 'break':
120
120
  case 'continue':
121
+ case 'goto':
122
+ case 'caseEndGoto':
121
123
  return [s];
122
124
  }
123
125
  }