@asmlift/core 0.8.0 → 0.9.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 (105) hide show
  1. package/README.md +34 -30
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +248 -0
  4. package/src/backend/c.ts +5 -2
  5. package/src/backend/cfamily.ts +236 -37
  6. package/src/backend/cpp.ts +114 -9
  7. package/src/backend/pascal.ts +39 -0
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +615 -79
  15. package/src/declare.ts +75 -5
  16. package/src/frontend/asmdata.ts +19 -5
  17. package/src/frontend/call-plan.ts +686 -0
  18. package/src/frontend/device-pins.ts +277 -0
  19. package/src/frontend/errors.ts +5 -0
  20. package/src/frontend/flags-edge.ts +101 -0
  21. package/src/frontend/fpu.ts +167 -0
  22. package/src/frontend/frame-objects.ts +2131 -0
  23. package/src/frontend/high-half.ts +2 -1
  24. package/src/frontend/local-object.ts +490 -0
  25. package/src/frontend/mips.ts +363 -40
  26. package/src/frontend/opaque.ts +97 -18
  27. package/src/frontend/ppc.ts +520 -129
  28. package/src/frontend/reloc-symbol.ts +94 -13
  29. package/src/frontend/splat.ts +108 -5
  30. package/src/frontend/ssa.ts +669 -76
  31. package/src/frontend/stackargs.ts +120 -39
  32. package/src/frontend/thumb.ts +2764 -2018
  33. package/src/ir/alias.ts +29 -0
  34. package/src/ir/core.ts +101 -3
  35. package/src/ir/discipline.ts +248 -0
  36. package/src/ir/float-bits.ts +47 -0
  37. package/src/ir/opcodes.ts +84 -112
  38. package/src/ir/parse.ts +1 -0
  39. package/src/ir/types.ts +118 -2
  40. package/src/ir/verify.ts +150 -3
  41. package/src/l3/address.ts +5 -0
  42. package/src/l3/argcopy.ts +38 -6
  43. package/src/l3/ast.ts +186 -14
  44. package/src/l3/coalesce.ts +21 -13
  45. package/src/l3/dce.ts +32 -10
  46. package/src/l3/gates.ts +9 -3
  47. package/src/l3/hoist.ts +8 -7
  48. package/src/l3/initfirst.ts +27 -9
  49. package/src/l3/intcell.ts +97 -0
  50. package/src/l3/mentions.ts +10 -4
  51. package/src/l3/narrowdecl.ts +218 -0
  52. package/src/l3/nearbase.ts +10 -5
  53. package/src/l3/parkfirst.ts +8 -5
  54. package/src/l3/pollguard.ts +3 -2
  55. package/src/l3/ptrcell.ts +357 -0
  56. package/src/l3/regspell.ts +17 -3
  57. package/src/l3/reindex.ts +10 -7
  58. package/src/l3/symbol-refs.ts +104 -23
  59. package/src/l3/tailmerge.ts +6 -19
  60. package/src/l3/typing.ts +137 -25
  61. package/src/l3/unreduce.ts +4 -4
  62. package/src/l3/volstore.ts +96 -7
  63. package/src/mangle.ts +11 -1
  64. package/src/pattern/engine.ts +19 -16
  65. package/src/pipeline.ts +57 -12
  66. package/src/proto-context.ts +788 -0
  67. package/src/proto.ts +689 -50
  68. package/src/raise/arrays.ts +4 -3
  69. package/src/raise/declared-volatile.ts +131 -0
  70. package/src/raise/divpow2.ts +2 -2
  71. package/src/raise/errors.ts +17 -0
  72. package/src/raise/floathelpers.ts +243 -0
  73. package/src/raise/globalshape.ts +11 -5
  74. package/src/raise/gvn.ts +26 -5
  75. package/src/raise/narrow.ts +29 -2
  76. package/src/raise/narrowlocal.ts +11 -6
  77. package/src/raise/offsetnames.ts +510 -0
  78. package/src/raise/pairparams.ts +81 -0
  79. package/src/raise/paramwidth.ts +7 -0
  80. package/src/raise/pre-recovery.ts +78 -4
  81. package/src/raise/recover.ts +14 -10
  82. package/src/raise/retsink.ts +1 -1
  83. package/src/raise/shortcircuit.ts +41 -19
  84. package/src/raise/softdiv.ts +21 -23
  85. package/src/raise/struct-arrays.ts +5 -4
  86. package/src/raise/structs.ts +311 -98
  87. package/src/raise/truncload.ts +366 -0
  88. package/src/raise/widehelpers.ts +106 -0
  89. package/src/rank-declare.ts +30 -5
  90. package/src/rank-variations.ts +36 -8
  91. package/src/rank.ts +276 -80
  92. package/src/runtime-helpers.ts +287 -0
  93. package/src/stillborn.ts +289 -0
  94. package/src/structure/analysis.ts +1285 -448
  95. package/src/structure/hazards.ts +633 -58
  96. package/src/structure/local-statics.ts +381 -0
  97. package/src/structure/namecoalesce.ts +1 -1
  98. package/src/structure/redundant-test.ts +99 -0
  99. package/src/structure/structure.ts +1772 -395
  100. package/src/structure/switch-recover.ts +159 -78
  101. package/src/symbols.ts +38 -3
  102. package/src/target.ts +662 -59
  103. package/src/trace.ts +44 -24
  104. package/src/variation-definitions.ts +119 -1
  105. package/src/variation-tokens.ts +20 -6
@@ -5,9 +5,12 @@
5
5
  // late-bound callbacks into the emission phase, so case bodies reuse the ordinary structuring
6
6
  // machinery (loops/ifs inside cases, the onStack guard).
7
7
  import { Block, Fn, Op, Value, forwardingTarget, isBodyless, successorsOf } from '../ir/core';
8
- import { ORDER_SENSITIVE_OPS } from '../ir/opcodes';
8
+ import { orderSensitive } from '../ir/discipline';
9
9
  import { Expr, Stmt, SwitchCase } from '../l3/ast';
10
10
 
11
+ /** Which side of a relational test a compiler's dispatch lands a pinned case body on. */
12
+ export type SwitchBoundCase = 'taken' | 'either';
13
+
11
14
  export interface SwitchRecoverDeps {
12
15
  fn: Fn;
13
16
  defs: Map<Value, Op>;
@@ -19,8 +22,9 @@ export interface SwitchRecoverDeps {
19
22
  /** is this opcode an integer comparison? */
20
23
  isCmpOpcode: (opcode: string) => boolean;
21
24
  switchAllowsNeqCase: boolean;
22
- /** read a relational test whose BRANCH admits exactly one scrutinee value as that case */
23
- switchAllowsBoundCase: boolean;
25
+ /** read a relational test as a case where the values that can reach it leave exactly one on
26
+ * this side of it — its branch, or either side — or never (null) */
27
+ switchBoundCase: SwitchBoundCase | null;
24
28
  /** emit the case arms in the ASSEMBLY's block-layout order rather than by ascending case value */
25
29
  switchArmsFollowLayout: boolean;
26
30
  /** DECLINE a recovered tree whose own layout INTERLEAVES a test with a case body. A source
@@ -128,66 +132,126 @@ export interface TestInfo {
128
132
  xOnLeft: boolean;
129
133
  }
130
134
 
131
- // Evaluate a test predicate for a CONCRETE scrutinee value — used to SIMULATE the decision tree and
132
- // verify recovered case values (below). Returns true iff the `taken` (successors[0]) edge is followed.
133
- // Signed/unsigned per the icmp opcode (PRE3, done concretely rather than via interval lattices).
134
- function evalCmp(opcode: string, xOnLeft: boolean, xv: number, k: number): boolean {
135
- const uns = opcode.startsWith('icmp_u');
136
- const [xn, kn] = uns ? [xv >>> 0, k >>> 0] : [xv | 0, k | 0];
137
- const [l, r] = xOnLeft ? [xn, kn] : [kn, xn]; // put the scrutinee where it textually appears
138
- switch (opcode) {
139
- case 'icmp_eq':
140
- return l === r;
141
- case 'icmp_ne':
142
- return l !== r;
143
- case 'icmp_slt':
144
- case 'icmp_ult':
145
- return l < r;
146
- case 'icmp_sle':
147
- case 'icmp_ule':
148
- return l <= r;
149
- case 'icmp_sgt':
150
- case 'icmp_ugt':
151
- return l > r;
152
- case 'icmp_sge':
153
- case 'icmp_uge':
154
- return l >= r;
155
- default:
156
- return false;
135
+ // WHICH SCRUTINEE VALUES CAN REACH A TEST, AND WHICH ONE A SIDE OF IT PINS. A comparison dispatch
136
+ // pins its last value with a RELATIONAL test once the tests above it have narrowed the scrutinee to
137
+ // one value on that side: mwcc's `switch (x) { case 0: … case 1: … default: … }` is `cmpwi r0,1;
138
+ // beq- case1; bge- default; cmpwi r0,0; bge- case0; b default`, where `x >= 0` is `case 0` only
139
+ // because `x != 1` and `x < 1` came first. agbcc's `emit_case_nodes` does the same on the branch it
140
+ // takes once `node_is_bounded` holds, and its `x <u 1` for `case 0` of an unsigned switch is the
141
+ // instance that needs nothing from the tests above: one value at an end of the 32-bit domain. Read
142
+ // as navigation instead, the pinned arm's body becomes a second default candidate and the whole
143
+ // tree declines. Which SIDE of a test may pin a case is the compiler's (`switchBoundCase`,
144
+ // target.ts).
145
+ //
146
+ // EXACT, not a bound. A range is the set of 32-bit register values, as disjoint signed intervals in
147
+ // ascending order, and every test the walk narrows it by — `==`, `!=`, and a relational test under
148
+ // either signedness — is an exact set over that register. An unsigned interval straddling 2^31 is
149
+ // two signed ones (`fromUnsigned`). So a side that comes out a singleton names the only value that
150
+ // CAN take it, and a side that comes out empty is one no value takes. Whether the value DOES reach
151
+ // the body is still PRE3's to say, simulating the original tree case by case.
152
+ //
153
+ // THE DOMAIN IS THE REGISTER's, not the scrutinee's recovered type, so a narrower type has nearer
154
+ // ends this misses. That costs a case and never invents one.
155
+ type Ranges = readonly (readonly [number, number])[];
156
+
157
+ const INT_MIN = -0x80000000;
158
+ const INT_MAX = 0x7fffffff;
159
+ const ALL: Ranges = [[INT_MIN, INT_MAX]];
160
+
161
+ /** The same opcode with its operands swapped: `k < x` is `x > k`. */
162
+ const MIRRORED: Record<string, string> = {
163
+ icmp_slt: 'icmp_sgt',
164
+ icmp_sgt: 'icmp_slt',
165
+ icmp_sle: 'icmp_sge',
166
+ icmp_sge: 'icmp_sle',
167
+ icmp_ult: 'icmp_ugt',
168
+ icmp_ugt: 'icmp_ult',
169
+ icmp_ule: 'icmp_uge',
170
+ icmp_uge: 'icmp_ule',
171
+ };
172
+
173
+ /** `[lo, hi]` of the UNSIGNED order as signed intervals: one, or two where it straddles 2^31. */
174
+ function fromUnsigned(lo: number, hi: number): Ranges {
175
+ if (lo > hi) {
176
+ return [];
157
177
  }
178
+ return hi <= INT_MAX || lo > INT_MAX
179
+ ? [[lo | 0, hi | 0]]
180
+ : [
181
+ [INT_MIN, hi | 0],
182
+ [lo, INT_MAX],
183
+ ];
158
184
  }
159
185
 
160
- // Which single scrutinee value does this relational test's BRANCH admit, if exactly one? A
161
- // relational side is a HALF-LINE in the compare's own ordering, so it can hold one value only at
162
- // a domain endpoint — which is why testing the two endpoints and their neighbours decides it,
163
- // with no interval lattice. `x < 1` over an unsigned scrutinee admits `{0}` and is agbcc's
164
- // spelling of `case 0` in a balanced search: `emit_case_nodes` tests the subtree BOUND, not the
165
- // value, whenever the remaining range has collapsed to one. Read as navigation instead, that
166
- // arm's body becomes a second default candidate and the whole tree declines.
167
- //
168
- // THE BRANCH, never the fall-through. Every jump in `emit_case_nodes` that lands on a case body
169
- // is its test's BRANCH — for a single-valued node, LT to `node->left->code_label` and GT to
170
- // `node->right->code_label`, each guarded by `node_is_bounded` on that side — while the
171
- // fall-through always continues into more dispatch, so a fall-side reading has no producer in
172
- // this dispatch — and none turns up in 3176 generated agbcc dispatches.
173
- //
174
- // TWO PREMISES ABOUT THE DOMAIN. It is the 32-bit REGISTER's, not the scrutinee's recovered
175
- // type, so a narrower type has a nearer endpoint this misses — which costs a case and never
176
- // invents one. And it is the WHOLE of that domain, so an ancestor that already excluded the
177
- // value makes the reading wrong; PRE3 is what catches that, simulating the original tree for
178
- // every recovered case value and declining unless it lands on the recorded body, exactly as it
179
- // does for the `eq` cases. Null when the branch admits none, several, or the whole domain.
180
- function singletonTaken(ti: TestInfo): number | null {
181
- const [min, max] = ti.opcode.startsWith('icmp_u') ? [0, -1] : [-0x80000000, 0x7fffffff];
182
- for (const [v, next] of [
183
- [min, min + 1],
184
- [max, max - 1],
185
- ]) {
186
- if (evalCmp(ti.opcode, ti.xOnLeft, v, ti.k) && !evalCmp(ti.opcode, ti.xOnLeft, next, ti.k)) {
187
- return v;
186
+ function complement(r: Ranges): Ranges {
187
+ const out: [number, number][] = [];
188
+ let next = INT_MIN;
189
+ for (const [lo, hi] of r) {
190
+ if (lo > next) {
191
+ out.push([next, lo - 1]);
192
+ }
193
+ next = hi + 1;
194
+ }
195
+ if (next <= INT_MAX) {
196
+ out.push([next, INT_MAX]);
197
+ }
198
+ return out;
199
+ }
200
+
201
+ function intersect(a: Ranges, b: Ranges): Ranges {
202
+ const out: [number, number][] = [];
203
+ for (const [alo, ahi] of a) {
204
+ for (const [blo, bhi] of b) {
205
+ const lo = Math.max(alo, blo);
206
+ const hi = Math.min(ahi, bhi);
207
+ if (lo <= hi) {
208
+ out.push([lo, hi]);
209
+ }
188
210
  }
189
211
  }
190
- return null;
212
+ return out.sort((x, y) => x[0] - y[0]);
213
+ }
214
+
215
+ /** The register values that take the test's BRANCH. */
216
+ function takenBy(ti: TestInfo): Ranges {
217
+ const op = ti.xOnLeft ? ti.opcode : (MIRRORED[ti.opcode] ?? ti.opcode);
218
+ if (op === 'icmp_eq' || op === 'icmp_ne') {
219
+ const k = ti.k | 0;
220
+ return op === 'icmp_eq' ? [[k, k]] : complement([[k, k]]);
221
+ }
222
+ const uns = op.startsWith('icmp_u');
223
+ const [min, max, k] = uns ? [0, 0xffffffff, ti.k >>> 0] : [INT_MIN, INT_MAX, ti.k | 0];
224
+ const rel = op.slice('icmp_s'.length);
225
+ const [lo, hi] =
226
+ rel === 'lt'
227
+ ? [min, k - 1]
228
+ : rel === 'le'
229
+ ? [min, k]
230
+ : rel === 'gt'
231
+ ? [k + 1, max]
232
+ : rel === 'ge'
233
+ ? [k, max]
234
+ : [min, max];
235
+ return uns ? fromUnsigned(lo, hi) : lo > hi ? [] : [[lo, hi]];
236
+ }
237
+
238
+ /** What of `path` follows the test's `taken` edge, or its fall-through. */
239
+ function narrow(path: Ranges, ti: TestInfo, taken: boolean): Ranges {
240
+ const t = takenBy(ti);
241
+ return intersect(path, taken ? t : complement(t));
242
+ }
243
+
244
+ /** Whether register value `v` is in `r`. PRE3's concrete simulation reads each test through this
245
+ * and {@link takenBy}, so the tree it walks and the ranges a bound case is read off are one
246
+ * definition of the icmp family, not two. */
247
+ function contains(r: Ranges, v: number): boolean {
248
+ const x = v | 0;
249
+ return r.some(([lo, hi]) => lo <= x && x <= hi);
250
+ }
251
+
252
+ /** The one value in `r`, if it holds exactly one. */
253
+ function singleton(r: Ranges): number | null {
254
+ return r.length === 1 && r[0][0] === r[0][1] ? r[0][0] : null;
191
255
  }
192
256
 
193
257
  /** Re-thread `order` so every FALLING arm sits directly above the arm it falls into. Each
@@ -251,7 +315,7 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
251
315
  isNamed,
252
316
  isCmpOpcode,
253
317
  switchAllowsNeqCase,
254
- switchAllowsBoundCase,
318
+ switchBoundCase,
255
319
  switchArmsFollowLayout,
256
320
  switchRequiresFrontLoadedTests,
257
321
  spellSwitchFallthrough,
@@ -265,9 +329,10 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
265
329
  // A block's index in `fn.blocks` as its position in the ASSEMBLY — the warrant for every reading
266
330
  // of layout below, and true PER FRONTEND rather than of the IR:
267
331
  // - thumb.ts and mips.ts build the list by scanning the instruction stream in address order;
268
- // - ppc.ts does not. It APPENDS a synthetic return block (`synthReturn`) at the end of the list
269
- // for every conditional-return branch, wherever in the stream that branch sits, so its list
270
- // is not address order at all;
332
+ // - ppc.ts does too, and then APPENDS a synthetic return block (`synthReturn`) at the end of
333
+ // the list for every conditional-return branch, wherever in the stream that branch sits. So
334
+ // its real blocks keep address order among themselves, and each synthetic one — a bare
335
+ // return, never a test — sorts after all of them;
271
336
  // - raising only ever REMOVES blocks from the list (raise/{divpow2,latch,retsink,shortcircuit}
272
337
  // .ts all `filter`), never inserts or reorders, so the frontend's order is what survives.
273
338
  // TWO READERS, both target-gated, and they need DIFFERENT strengths of the compiler half:
@@ -278,9 +343,14 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
278
343
  // `MIPS_GCC` declares `switchRequiresFrontLoadedTests` and NOT `switchArmsFollowLayout` for
279
344
  // exactly that reason — it has a scheduler and fills delay slots (target.ts).
280
345
  // Both are therefore claims about a target's FRONTEND as much as about its compiler, and a target
281
- // opts in on both halves — which is why PPC_MWCC, whose frontend fails the frontend half outright,
282
- // declares neither. Anything added below that reads `layoutIndex` inherits the frontend half and
283
- // owes a statement of which strength of the compiler half it needs.
346
+ // opts in on both halves. PPC_MWCC's frontend holds the weaker half only: a synthetic return body
347
+ // sorting last can make PRE5 keep a `switch` it should decline, never decline one it should keep,
348
+ // so mwcc declares `switchRequiresFrontLoadedTests` and could not declare the placing reading.
349
+ // That is the frontend's limit and not the compiler's: mwcc lays its case bodies in source order
350
+ // (the compiled pairs at target.ts `switchArmsFollowLayout`), so a synthetic block placed at its
351
+ // branch's address would give PPC both halves.
352
+ // Anything added below that reads `layoutIndex` inherits the frontend half and owes a statement
353
+ // of which strength of the compiler half it needs.
284
354
  const blockIndex = new Map(fn.blocks.map((blk, i) => [blk, i] as const));
285
355
  const layoutIndex = (blk: Block): number => blockIndex.get(blk) ?? -1;
286
356
 
@@ -432,7 +502,7 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
432
502
  //
433
503
  // PRE4 (purity) is the second. A collapsed test block's ops re-render at whichever use inlines
434
504
  // them, at a point the switch decides — so the question is motion, not deletion, and
435
- // `ORDER_SENSITIVE_OPS` is the set that asks it. NOT the trapping divides: a use is dominated by
505
+ // `orderSensitive` is the question that asks it. NOT the trapping divides: a use is dominated by
436
506
  // its def, so the re-rendered op runs on a subset of the paths it already ran on — nothing is
437
507
  // speculated. `emitsOwnStatement` covers what motion cannot save: a statement belonging to the
438
508
  // block rather than to a use. The root is exempt from all of it — its ops are already emitted as
@@ -441,8 +511,7 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
441
511
  // A block that tests the scrutinee and is NOT collapsible is still dispatch, so the walk must
442
512
  // read it as dispatch and decline, never re-read it as a case body — that would spell an arm
443
513
  // whose guard the dispatch has already decided.
444
- const collapsible = (blk: Block): boolean =>
445
- !blk.ops.some((op) => ORDER_SENSITIVE_OPS.has(op.opcode)) && !emitsOwnStatement(blk);
514
+ const collapsible = (blk: Block): boolean => !blk.ops.some(orderSensitive) && !emitsOwnStatement(blk);
446
515
  const testInfo = (blk: Block): TestInfo | null => {
447
516
  const term = blk.ops[blk.ops.length - 1];
448
517
  if (term.opcode !== 'cond_br') {
@@ -588,12 +657,15 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
588
657
  }
589
658
  guard.add(cur);
590
659
  const term = cur.ops[cur.ops.length - 1];
591
- const taken = evalCmp(ti.opcode, ti.xOnLeft, xv, ti.k);
660
+ const taken = contains(takenBy(ti), xv);
592
661
  cur = forwardingTarget(term.successors[taken ? 0 : 1].block);
593
662
  }
594
663
  };
595
664
  const seen = new Set<Block>();
596
665
  const work: Block[] = [b];
666
+ // The values that can reach each test block, written as the walk pushes it. Only a bound case
667
+ // reads it; the tree is a tree (a second visit declines below), so each block has one entry.
668
+ const pathOf = new Map<Block, Ranges>([[b, ALL]]);
597
669
  while (work.length) {
598
670
  const blk = work.pop()!;
599
671
  if (seen.has(blk)) {
@@ -628,10 +700,13 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
628
700
  cases.set(k, child);
629
701
  return true;
630
702
  };
703
+ const path = pathOf.get(blk)!;
631
704
  /** Read `child` as a NAVIGATION edge: more dispatch to walk, or a non-test leaf, which is a
632
- * default candidate. Never declines — the leaf's own reading is settled below. */
633
- const asNav = (child: Block): boolean => {
705
+ * default candidate. Never declines — the leaf's own reading is settled below. `onTaken`
706
+ * says which of `blk`'s edges it is, which decides what the range narrows to. */
707
+ const asNav = (child: Block, onTaken: boolean): boolean => {
634
708
  if (isTestOn(child)) {
709
+ pathOf.set(child, narrow(path, ti, onTaken));
635
710
  work.push(child);
636
711
  return true;
637
712
  }
@@ -642,7 +717,7 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
642
717
  if (!asCase(taken, ti.k)) {
643
718
  return null;
644
719
  } // x==k → taken is case k
645
- if (!asNav(fall)) {
720
+ if (!asNav(fall, false)) {
646
721
  return null;
647
722
  }
648
723
  } else if (ti.cls === 'ne') {
@@ -652,12 +727,12 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
652
727
  if (!asCase(fall, ti.k)) {
653
728
  return null;
654
729
  } // x!=k → the EQUAL side (fall) is case k
655
- if (!asNav(taken)) {
730
+ if (!asNav(taken, true)) {
656
731
  return null;
657
732
  }
658
733
  } else {
659
- // relational → navigation, except where the BRANCH has collapsed to a single value and
660
- // lands on a BODY, on a compiler that declared the spelling. Two more refusals:
734
+ // relational → navigation, except where a side the compiler declared (`switchBoundCase`)
735
+ // admits a single value and lands on a BODY. Two more refusals:
661
736
  // - a bound test at the ROOT. `emit_case_nodes` emits a single-valued node's own
662
737
  // `do_jump_if_equal` before either descent test, so a bound test always sits under
663
738
  // another test of the same tree; one that OPENS the region did not come from this
@@ -665,11 +740,13 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
665
740
  // - a singleton branch onto another TEST of the scrutinee, which is the search
666
741
  // descending to pin the value. It is dispatch, so the walk reads it as dispatch —
667
742
  // recovering it, or declining at PRE4 if it is not collapsible.
668
- const k = switchAllowsBoundCase && blk !== b && !isTestOn(taken) ? singletonTaken(ti) : null;
669
- if (!(k === null ? asNav(taken) : asCase(taken, k))) {
743
+ const sides = blk !== b ? switchBoundCase : null;
744
+ const kTaken = sides && !isTestOn(taken) ? singleton(narrow(path, ti, true)) : null;
745
+ const kFall = sides === 'either' && !isTestOn(fall) ? singleton(narrow(path, ti, false)) : null;
746
+ if (!(kTaken === null ? asNav(taken, true) : asCase(taken, kTaken))) {
670
747
  return null;
671
748
  }
672
- if (!asNav(fall)) {
749
+ if (!(kFall === null ? asNav(fall, false) : asCase(fall, kFall))) {
673
750
  return null;
674
751
  }
675
752
  }
@@ -1060,3 +1137,7 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
1060
1137
  };
1061
1138
  return { recognizeSwitch, analyzeArmExit, layoutIndex, defaultLayoutPos, chainArms };
1062
1139
  }
1140
+
1141
+ /** Internal surface for this module's own tests, and for nothing else: PRE3 reads each test
1142
+ * through {@link takenBy}, so the ranges need a check that does not share their definition. */
1143
+ export const __testing = { takenBy, narrow, contains, ALL };
package/src/symbols.ts CHANGED
@@ -268,6 +268,27 @@ export function isBitfieldField(f: SymbolStructField): boolean {
268
268
  return f.bitWidth !== undefined;
269
269
  }
270
270
 
271
+ /** Does this declaration make an access at `byte` of the object volatile? The object's own
272
+ * qualifier answers for every byte. A qualified MEMBER answers only for the bytes it spans — the
273
+ * `vu16 field;` idiom puts one in a struct whose other members are ordinary cells — and one of
274
+ * unknown extent spans whatever follows it. `null` is an access whose offset is not pinned, which
275
+ * may reach any member, so only a layout whose every member is qualified answers it. A member this
276
+ * module cannot read (`SymbolMap` is caller-supplied) qualifies nothing. */
277
+ export function declaresVolatile(info: SymbolInfo | undefined, byte: number | null): boolean {
278
+ if (info?.volatile === true) {
279
+ return true;
280
+ }
281
+ const layout: unknown = info?.layout;
282
+ if (!Array.isArray(layout) || layout.length === 0) {
283
+ return false;
284
+ }
285
+ const qualified = (f: unknown): f is SymbolStructField => wellFormedField(f) && f.volatile === true;
286
+ if (byte === null) {
287
+ return layout.every(qualified);
288
+ }
289
+ return layout.some((f) => qualified(f) && byte >= f.offset && (f.size === null || byte < f.offset + f.size));
290
+ }
291
+
271
292
  /** Does the map declare a bitfield member anywhere, in a symbol's own layout or its pointee's? */
272
293
  export function declaresBitfields(symbols: SymbolMap): boolean {
273
294
  return [...symbols.values()].some((infos) =>
@@ -494,6 +515,16 @@ export function isScalarCellSize(size: number | undefined): size is 1 | 2 | 4 {
494
515
  return size === 1 || size === 2 || size === 4;
495
516
  }
496
517
 
518
+ /** THE SIGNEDNESS AN ACCESS OF THIS WIDTH STATES. A sub-word load says in the instruction whether
519
+ * it sign- or zero-fills; a whole-word one performs no extension at all, so whatever `signed`
520
+ * its attrs carry says nothing about the cell. Every comparison between an access and a
521
+ * declaration normalizes both sides through here — `rank-declare.ts`'s `bareGlobalAccessFacts`
522
+ * for a bare name over a whole function, `raise/offsetnames.ts` for the accesses made through one
523
+ * address — or a 4-byte load's incidental attr decides a declaration. */
524
+ export function accessSignedness(width: number, signed: boolean | undefined): boolean {
525
+ return signed === true && width < 4;
526
+ }
527
+
497
528
  /** THE DECLARED type of a 1/2/4-byte scalar cell — what `extern T gSym;` synthesis writes, and
498
529
  * therefore what `&gSym` actually points to.
499
530
  *
@@ -625,8 +656,9 @@ function factsOf(info: SymbolInfo): string {
625
656
  );
626
657
  }
627
658
 
628
- /** NAME-keyed view over every symbol in the map — what the structurer consumes (it sees gaddr
629
- * symbol names, not addresses). Aliases at one address each appear under their own name.
659
+ /** NAME-keyed view over every symbol in the map, or over the names in `only` — what the structurer
660
+ * consumes (it sees gaddr symbol names, not addresses). Aliases at one address each appear under
661
+ * their own name.
630
662
  *
631
663
  * One name can sit at SEVERAL addresses in a real project (file-static `sMenu` in two
632
664
  * translation units, a `.symtab` full of same-named locals). Where those entries agree on their
@@ -639,11 +671,14 @@ function factsOf(info: SymbolInfo): string {
639
671
  * spelling question into a compile failure); only the shape facts, which are what could be wrong,
640
672
  * are withheld — the honest cast spellings take over. `kind` is kept: it never disagrees in the
641
673
  * vendored maps, and it is settled address-side by `lookupSymbol` before a name is ever used. */
642
- export function symbolsByName(map: SymbolMap): Map<string, SymbolInfo> {
674
+ export function symbolsByName(map: SymbolMap, only?: ReadonlySet<string>): Map<string, SymbolInfo> {
643
675
  const byName = new Map<string, SymbolInfo>();
644
676
  const conflicted = new Set<string>();
645
677
  for (const infos of map.values()) {
646
678
  for (const info of infos) {
679
+ if (only !== undefined && !only.has(info.name)) {
680
+ continue;
681
+ }
647
682
  const prev = byName.get(info.name);
648
683
  if (prev === undefined) {
649
684
  byName.set(info.name, info);