@asmlift/core 0.8.1 → 0.10.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 (130) hide show
  1. package/README.md +36 -31
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +397 -0
  4. package/src/backend/c.ts +6 -2
  5. package/src/backend/cfamily.ts +370 -56
  6. package/src/backend/cpp.ts +116 -10
  7. package/src/backend/pascal.ts +69 -11
  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 +777 -84
  15. package/src/declare.ts +132 -13
  16. package/src/frontend/asmdata.ts +26 -6
  17. package/src/frontend/call-plan.ts +1590 -0
  18. package/src/frontend/dead-reads.ts +628 -0
  19. package/src/frontend/device-pins.ts +296 -0
  20. package/src/frontend/emit.ts +4 -3
  21. package/src/frontend/errors.ts +5 -0
  22. package/src/frontend/flags-edge.ts +101 -0
  23. package/src/frontend/fpu.ts +374 -0
  24. package/src/frontend/frame-objects.ts +3171 -0
  25. package/src/frontend/frontend.ts +64 -3
  26. package/src/frontend/high-half.ts +55 -5
  27. package/src/frontend/jump-table.ts +530 -0
  28. package/src/frontend/local-object.ts +616 -0
  29. package/src/frontend/mips.ts +2392 -208
  30. package/src/frontend/opaque.ts +97 -18
  31. package/src/frontend/own-struct-return.ts +500 -0
  32. package/src/frontend/ppc.ts +3137 -267
  33. package/src/frontend/register-struct-call.ts +484 -0
  34. package/src/frontend/reloc-symbol.ts +109 -13
  35. package/src/frontend/splat.ts +108 -5
  36. package/src/frontend/ssa.ts +2098 -89
  37. package/src/frontend/stackargs.ts +172 -42
  38. package/src/frontend/thumb.ts +4563 -2091
  39. package/src/ir/alias.ts +71 -0
  40. package/src/ir/bits.ts +681 -1
  41. package/src/ir/core.ts +199 -6
  42. package/src/ir/discipline.ts +313 -0
  43. package/src/ir/entry.ts +159 -0
  44. package/src/ir/float-bits.ts +47 -0
  45. package/src/{structure → ir}/loops.ts +6 -2
  46. package/src/ir/opcodes.ts +193 -121
  47. package/src/ir/parse.ts +1 -0
  48. package/src/ir/types.ts +202 -2
  49. package/src/ir/verify.ts +219 -3
  50. package/src/l3/address.ts +5 -0
  51. package/src/l3/argcopy.ts +38 -6
  52. package/src/l3/ast.ts +467 -23
  53. package/src/l3/basecse.ts +33 -5
  54. package/src/l3/coalesce.ts +21 -13
  55. package/src/l3/constinit.ts +382 -0
  56. package/src/l3/dce.ts +34 -12
  57. package/src/l3/gates.ts +9 -3
  58. package/src/l3/hoist.ts +8 -7
  59. package/src/l3/initfirst.ts +68 -19
  60. package/src/l3/intcell.ts +98 -0
  61. package/src/l3/mentions.ts +10 -4
  62. package/src/l3/narrowdecl.ts +259 -0
  63. package/src/l3/nearbase.ts +10 -5
  64. package/src/l3/parkfirst.ts +8 -5
  65. package/src/l3/pollguard.ts +11 -4
  66. package/src/l3/ptrcell.ts +354 -0
  67. package/src/l3/ptrfield.ts +123 -8
  68. package/src/l3/regspell.ts +26 -7
  69. package/src/l3/reindex.ts +293 -103
  70. package/src/l3/storage.ts +98 -1
  71. package/src/l3/symbol-refs.ts +182 -23
  72. package/src/l3/tailmerge.ts +9 -20
  73. package/src/l3/typing.ts +237 -32
  74. package/src/l3/unreduce.ts +10 -9
  75. package/src/l3/volatileptr.ts +17 -54
  76. package/src/l3/volstore.ts +96 -7
  77. package/src/mangle.ts +41 -1
  78. package/src/pattern/engine.ts +373 -82
  79. package/src/pipeline.ts +589 -27
  80. package/src/proto-context.ts +1648 -0
  81. package/src/proto.ts +1203 -49
  82. package/src/raise/aggmembers.ts +290 -0
  83. package/src/raise/arrays.ts +8 -3
  84. package/src/raise/declared-volatile.ts +307 -0
  85. package/src/raise/divpow2.ts +2 -2
  86. package/src/raise/errors.ts +17 -0
  87. package/src/raise/floathelpers.ts +464 -0
  88. package/src/raise/globalshape.ts +11 -5
  89. package/src/raise/gvn.ts +29 -8
  90. package/src/raise/latch.ts +162 -6
  91. package/src/raise/memberarrays.ts +10 -4
  92. package/src/raise/narrow.ts +29 -2
  93. package/src/raise/narrowlocal.ts +11 -6
  94. package/src/raise/offsetnames.ts +494 -0
  95. package/src/raise/pairparams.ts +114 -0
  96. package/src/raise/paramwidth.ts +203 -6
  97. package/src/raise/pre-recovery.ts +174 -17
  98. package/src/raise/pre-structure.ts +54 -0
  99. package/src/raise/recover.ts +26 -15
  100. package/src/raise/reroll.ts +694 -0
  101. package/src/raise/retsink.ts +1 -1
  102. package/src/raise/shortcircuit.ts +66 -30
  103. package/src/raise/softdiv.ts +21 -23
  104. package/src/raise/storesign.ts +153 -0
  105. package/src/raise/struct-arrays.ts +34 -14
  106. package/src/raise/structs.ts +674 -98
  107. package/src/raise/truncload.ts +367 -0
  108. package/src/raise/unthread.ts +400 -0
  109. package/src/raise/widehelpers.ts +106 -0
  110. package/src/rank-declare.ts +143 -12
  111. package/src/rank-variations.ts +117 -31
  112. package/src/rank.ts +563 -122
  113. package/src/runtime-helpers.ts +405 -0
  114. package/src/stillborn.ts +289 -0
  115. package/src/structure/analysis.ts +1449 -449
  116. package/src/structure/arith-ops.ts +23 -0
  117. package/src/structure/globalaccess.ts +111 -9
  118. package/src/structure/hazards.ts +641 -114
  119. package/src/structure/local-statics.ts +517 -0
  120. package/src/structure/namecoalesce.ts +2 -2
  121. package/src/structure/pointer-spelling.ts +1106 -0
  122. package/src/structure/redundant-test.ts +99 -0
  123. package/src/structure/structure.ts +3478 -975
  124. package/src/structure/switch-recover.ts +403 -178
  125. package/src/symbols.ts +44 -3
  126. package/src/target.ts +1387 -71
  127. package/src/trace.ts +66 -28
  128. package/src/variation-definitions.ts +243 -13
  129. package/src/variation-gates.ts +2 -0
  130. package/src/variation-tokens.ts +25 -7
@@ -9,10 +9,14 @@
9
9
  // asks each emitted tree for its references. A SIBLING MODULE, never a `rank/` directory — see the
10
10
  // same note on rank-variations.ts.
11
11
  import { type Fn, type Value, defOpMap } from './ir/core';
12
+ import { qualified } from './ir/discipline';
13
+ import { MEM_BASE_OPS } from './ir/opcodes';
12
14
  import type { SFn } from './l3/ast';
13
- import { type SymbolRef, collectSymbolRefs } from './l3/symbol-refs';
14
- import type { SymbolInfo } from './symbols';
15
- import { C_TYPEDEFS } from './target';
15
+ import { type SymbolRef, collectSymbolRefs, ownReturnDefinition } from './l3/symbol-refs';
16
+ import type { FnProto, Prototypes } from './proto';
17
+ import { storeSigned, storedConstantSign } from './raise/storesign';
18
+ import { type SymbolInfo, accessSignedness } from './symbols';
19
+ import { C_TYPEDEFS, type TargetDescription, globalRegisterSymbol } from './target';
16
20
 
17
21
  /** Bare-global ACCESS FACTS for name-only map symbols — the width/signedness authority the
18
22
  * declaration synthesis (declare.ts) uses when the map has no shape. The map knows only the
@@ -22,15 +26,35 @@ import { C_TYPEDEFS } from './target';
22
26
  * Mirrors structure()'s scalar-global rule: a fact is recorded only for a symbol accessed
23
27
  * EXCLUSIVELY at offset 0 with ONE width and ONE load signedness — anything else (interior
24
28
  * offsets, address arithmetic, width or sign conflicts) records nothing, because those
25
- * spellings go through `&gSym` casts where every object decl is address-identical. */
29
+ * spellings go through `&gSym` casts where every object decl is address-identical.
30
+ *
31
+ * A symbol never LOADED takes its sign from its stamped stores (raise/storesign.ts); an unstamped
32
+ * store says nothing, so a cell only ever stored unstamped keeps the unsigned default. A load
33
+ * still wins, as in raise/structs.ts. Stores whose stamps disagree keep the width with the
34
+ * unsigned default: dropping the fact would declare `extern u32` under a bare `gSym = v` that
35
+ * then compiles to `sw` where the target has `sh`. The bare spelling has no cast to carry the
36
+ * stamp, so this declaration is the only place the stored constant's view can be written.
37
+ *
38
+ * An UNSTAMPED store of a constant zero-extended from its narrow sign bit vetoes a signed
39
+ * declaration. storesign.ts leaves such a store unstamped when a wider use shares the constant,
40
+ * and the structurer spells it as the literal (`gS = 65535; gW = 65535;`). Under `extern s16 gS`,
41
+ * that literal converts to -1, and the shared register splits in two. The veto only ever chooses
42
+ * the unsigned default, which is what the declaration was before stamps existed. */
26
43
  export function bareGlobalAccessFacts(fn: Fn): Map<string, { width: number; signed: boolean }> {
27
44
  const defs = defOpMap(fn);
28
45
  const symOf = (v: Value): string | null => {
29
46
  const d = defs.get(v);
30
47
  return d?.opcode === 'gaddr' && d.attrs.code !== true ? (d.attrs.sym as string) : null;
31
48
  };
32
- const acc = new Map<string, { widths: Set<number>; signs: Set<boolean>; interior: boolean }>();
33
- const get = (s: string) => acc.get(s) ?? acc.set(s, { widths: new Set(), signs: new Set(), interior: false }).get(s)!;
49
+ const acc = new Map<
50
+ string,
51
+ { widths: Set<number>; signs: Set<boolean>; stored: Set<boolean>; unsignedLiteral: boolean; interior: boolean }
52
+ >();
53
+ const get = (s: string) =>
54
+ acc.get(s) ??
55
+ acc
56
+ .set(s, { widths: new Set(), signs: new Set(), stored: new Set(), unsignedLiteral: false, interior: false })
57
+ .get(s)!;
34
58
  for (const b of fn.blocks) {
35
59
  for (const op of b.ops) {
36
60
  if (op.opcode === 'load' || op.opcode === 'store') {
@@ -42,7 +66,17 @@ export function bareGlobalAccessFacts(fn: Fn): Map<string, { width: number; sign
42
66
  } else {
43
67
  a.widths.add(op.attrs.width as number);
44
68
  if (op.opcode === 'load') {
45
- a.signs.add(((op.attrs.signed as boolean) ?? false) && (op.attrs.width as number) < 4);
69
+ a.signs.add(accessSignedness(op.attrs.width as number, op.attrs.signed as boolean | undefined));
70
+ } else if (op.attrs.signed !== undefined) {
71
+ a.stored.add(storeSigned(op, op.attrs.width as number));
72
+ } else {
73
+ const d = defs.get(op.operands[1]);
74
+ if (
75
+ d?.opcode === 'const' &&
76
+ storedConstantSign(d.attrs.value as number, op.attrs.width as number) === false
77
+ ) {
78
+ a.unsignedLiteral = true;
79
+ }
46
80
  }
47
81
  }
48
82
  }
@@ -64,9 +98,15 @@ export function bareGlobalAccessFacts(fn: Fn): Map<string, { width: number; sign
64
98
  }
65
99
  const out = new Map<string, { width: number; signed: boolean }>();
66
100
  for (const [s, a] of acc) {
67
- if (!a.interior && a.widths.size === 1 && a.signs.size <= 1) {
68
- out.set(s, { width: [...a.widths][0], signed: a.signs.has(true) });
101
+ if (a.interior || a.widths.size !== 1 || a.signs.size > 1) {
102
+ continue;
69
103
  }
104
+ // Loads decide when there are any. Stores whose stamps disagree pick no view, nor does a signed
105
+ // stamp beside an unstamped zero-extended literal: the declaration keeps the width and takes
106
+ // the unsigned default it had before stamps existed.
107
+ const signed =
108
+ a.signs.size === 1 ? a.signs.has(true) : a.stored.size === 1 && a.stored.has(true) && !a.unsignedLiteral;
109
+ out.set(s, { width: [...a.widths][0], signed });
70
110
  }
71
111
  return out;
72
112
  }
@@ -172,16 +212,73 @@ export type RefusedDeclarationReason =
172
212
  * module note. */
173
213
  export function bareGlobalSymbols(fn: Fn): Map<string, SymbolInfo> {
174
214
  const out = new Map<string, SymbolInfo>();
215
+ const defs = defOpMap(fn);
216
+ // A name the lift accessed QUALIFIED at its own address (ir/discipline.ts `qualified`, whichever
217
+ // placement put it there) is declared `volatile`: the structurer spells that access by the bare
218
+ // name, whose declaration owns its qualifiers (l3/volstore.ts `qualifiedMemoryAccess`), so a plain
219
+ // declaration would drop the qualifier the access carries — `gR;` under `extern u32 gR;` compiles
220
+ // to no read at all. A `declared` placement says the project's own header qualifies the name, and
221
+ // the declaration synthesized beside it says so too.
222
+ const qualifiedNames = new Set<string>();
223
+ for (const b of fn.blocks) {
224
+ for (const op of b.ops) {
225
+ const d = MEM_BASE_OPS.has(op.opcode) ? defs.get(op.operands[0]) : undefined;
226
+ if (d?.opcode === 'gaddr' && op.attrs.off === 0 && qualified(op)) {
227
+ qualifiedNames.add(d.attrs.sym as string);
228
+ }
229
+ }
230
+ }
175
231
  for (const b of fn.blocks) {
176
232
  for (const op of b.ops) {
177
233
  if (op.opcode === 'gaddr' && typeof op.attrs.sym === 'string') {
178
- out.set(op.attrs.sym, { name: op.attrs.sym, kind: 'data' });
234
+ const sym = op.attrs.sym;
235
+ out.set(sym, {
236
+ name: sym,
237
+ kind: 'data',
238
+ ...(qualifiedNames.has(sym) ? { volatile: true } : {}),
239
+ ...(typeof op.attrs.globalRegister === 'string' ? { globalRegister: op.attrs.globalRegister } : {}),
240
+ });
179
241
  }
180
242
  }
181
243
  }
182
244
  return out;
183
245
  }
184
246
 
247
+ /** The globals a function-scope static's initializer holds the address of (ir/core.ts
248
+ * `LocalObject.relocs`), as name-only symbols for the same synthesis: the candidate spells each in
249
+ * the static's definition (`(u32)&Name`), and only the static's relocations name it. Kept apart
250
+ * from {@link bareGlobalSymbols}, whose names the variation gates read as the globals the BODY
251
+ * addresses: an address in a table no access ever subscripts says nothing of how one is spelled. */
252
+ export function initAddressSymbols(fn: Fn): Map<string, SymbolInfo> {
253
+ const out = new Map<string, SymbolInfo>();
254
+ for (const obj of fn.localObjects?.values() ?? []) {
255
+ for (const r of obj.relocs ?? []) {
256
+ out.set(r.symbol, { name: r.symbol, kind: 'data' });
257
+ }
258
+ }
259
+ return out;
260
+ }
261
+
262
+ /** The global register variables a compiler that declares them only as a contiguous run
263
+ * (`globalRegisterVariables.contiguous`, mwcc 2.4: "gaps between assigned global register variables
264
+ * are not permitted") needs declared below the highest one `refs` names, and they do not. Each is a
265
+ * word nothing reads, declared only so the one the body uses is legal. */
266
+ function globalRegisterFillers(
267
+ refs: readonly SymbolRef[],
268
+ target: Pick<TargetDescription, 'globalRegisterVariables'>,
269
+ ): SymbolRef[] {
270
+ const run = target.globalRegisterVariables?.contiguous ?? [];
271
+ const named = new Set(refs.flatMap((r) => (r.info.globalRegister === undefined ? [] : [r.info.globalRegister])));
272
+ const top = Math.max(-1, ...[...named].map((r) => run.indexOf(r)));
273
+ return run
274
+ .slice(0, top + 1)
275
+ .filter((r) => !named.has(r))
276
+ .map((r) => {
277
+ const name = globalRegisterSymbol(r);
278
+ return { name, info: { name, kind: 'data', globalRegister: r }, synthesized: true };
279
+ });
280
+ }
281
+
185
282
  /** The reference collector one enumeration uses, over ONE dictionary. A factory rather than a
186
283
  * free function because all four of its inputs are per-enumeration constants that must not vary
187
284
  * per tree — naming them here is what keeps a caller from passing a different dictionary to two
@@ -202,15 +299,39 @@ export function makeRefCollector(ctx: {
202
299
  /** the globals the TARGET names — the pool/reloc names and the shapes the asm evidences, before
203
300
  * the map is unioned in */
204
301
  targetNames: ReadonlySet<string>;
302
+ /** the project's prototype table — what lets a CALL TARGET be declared rather than refused */
303
+ prototypes: Prototypes;
304
+ /** the target the lift ran for, whose ABI says which declared types a printed prototype carries */
305
+ target: Pick<TargetDescription, 'doubleArgWords' | 'globalRegisterVariables'>;
205
306
  /** reports a refusal at most once per (name, reason); the caller owns the dedup */
206
307
  refuse: (name: string, reason: RefusedDeclarationReason) => void;
308
+ /** the function's own declaration — stated, or the context's own entry — whose struct return a
309
+ * tree that returns it needs defined (`ownReturnDefinition`) */
310
+ own?: FnProto;
207
311
  }): (tree: SFn) => { symbolRefs?: SymbolRef[] } {
208
- const { declSymbols, accessFacts, mapSymbols, targetNames, refuse } = ctx;
312
+ const { declSymbols, accessFacts, mapSymbols, targetNames, prototypes, target, refuse, own } = ctx;
209
313
  return (tree: SFn): { symbolRefs?: SymbolRef[] } => {
210
314
  // The names THIS tree binds. Computed per tree because the emitter mints local names per
211
315
  // spelling — but the test below is NOT `bound` alone, and the difference is a wrong answer.
212
316
  const bound = new Set<string>([...tree.params.map((p) => p.name), ...tree.locals.map((l) => l.name)]);
213
- const refs = collectSymbolRefs(tree.body, declSymbols, tree.name, refuse).flatMap((r) => {
317
+ // A function-scope static is declared in the body beside the locals, so it shares their
318
+ // namespace: one the emitter's own names can reach dies with the spelling, as below.
319
+ const statics = new Set((tree.statics ?? []).map((st) => st.name));
320
+ // …and the compiler's anonymous images, which reach the C only as a local's initializer
321
+ const images = new Set((tree.images ?? []).map((im) => im.name));
322
+ for (const st of statics) {
323
+ if (bound.has(st) || EMITTER_NAME.test(st)) {
324
+ refuse(st, 'emitter-name');
325
+ throw new Error(
326
+ `cannot spell '${tree.name}': the target names a function-scope static '${st}', which is a ` +
327
+ `name the emitted C uses for its own locals and parameters — no declaration can bind it`,
328
+ );
329
+ }
330
+ }
331
+ const refs = collectSymbolRefs(tree, declSymbols, prototypes, target, refuse).flatMap((r) => {
332
+ if (statics.has(r.name) || images.has(r.name)) {
333
+ return []; // defined in the body — the map's global of the same name is another object
334
+ }
214
335
  // THE ONE REFUSAL THAT IS NOT A REFUSAL — a name the emitted C uses for its OWN storage
215
336
  // kills the SPELLING, because no declaration makes that candidate right and no declaration
216
337
  // makes it fail either. Two shapes, and the second is why the test is the emitter's whole
@@ -257,8 +378,18 @@ export function makeRefCollector(ctx: {
257
378
  // A ref no MAP accounts for is a hypothesis read out of the target asm, and it is marked
258
379
  // as one all the way to the consumer (SymbolRef.synthesized).
259
380
  const synthesized = mapSymbols?.has(r.name) ? {} : { synthesized: true as const };
381
+ // A global the asm reads narrower than a word holds no pointer, whatever it meets bare.
382
+ if (r.holdsPointer && access !== undefined && access.width !== 4) {
383
+ const { holdsPointer: _, ...integer } = r;
384
+ return [{ ...integer, access, ...synthesized }];
385
+ }
260
386
  return [{ ...r, ...(access ? { access } : {}), ...synthesized }];
261
387
  });
388
+ refs.push(...globalRegisterFillers(refs, target));
389
+ const returned = ownReturnDefinition(tree, own);
390
+ if (returned !== undefined) {
391
+ refs.push(returned);
392
+ }
262
393
  return refs.length ? { symbolRefs: refs } : {};
263
394
  };
264
395
  }
@@ -27,6 +27,7 @@ import {
27
27
  import type { Gate } from './l3/gates';
28
28
  import type { HoistPlacement } from './l3/hoist';
29
29
  import { initFirstGuards } from './l3/initfirst';
30
+ import { narrowDeclarations, narrowLoadDeclarations, narrowReadDeclarations } from './l3/narrowdecl';
30
31
  import { pollGuards, pollReads } from './l3/pollguard';
31
32
  import { unmergeJoins } from './l3/unmerge';
32
33
  // TYPE-ONLY, and deliberately: the structure-variation table types its `options` as
@@ -35,6 +36,7 @@ import { unmergeJoins } from './l3/unmerge';
35
36
  import type { structureChecked } from './pipeline';
36
37
  import {
37
38
  hasDerivedReadHome,
39
+ hasEscapingExtension,
38
40
  hasHomeableSharedAddress,
39
41
  hasLoopSharedPureValue,
40
42
  hasMergeFeedHome,
@@ -53,11 +55,12 @@ import type { VariationName } from './variation-tokens';
53
55
  * that setting's own lifted fn (a map-lifted shared lift spells const bases as gaddr, which would
54
56
  * blind the /raw-globals siblings — the /addr-home lesson). `strip` opts the variation into the
55
57
  * dropped-sibling closure: a candidate that applies it is skipped when its default sibling (the
56
- * same setting without it) failed the boundary contracts. Two variations are EXEMPT from structure()'s assertDefaultAccepts invariant:
57
- * `/reread-globals` only relaxes inlining barriers and `/uns-cmp` only changes spelling and
58
- * declarations — neither adds materialization or merging, so neither can unlock a function the
59
- * default declines (reread also skips the strip closure). Both exemptions are stated here
60
- * rather than left implicit in a missing `||` arm or trigger term. */
58
+ * same setting without it) failed the boundary contracts. Which of these entries `structure()`'s
59
+ * `assertDefaultAccepts` guard does NOT reset — the ones that cannot unlock a function the default
60
+ * declines, and so carry a written argument in place of the guard — is stated ONCE, beside that
61
+ * guard in structure.ts, and derived from the guard's own reset list in
62
+ * test/variation-offers.test.ts — never restated here, where a second copy drifts from the guard
63
+ * without anything going red. */
61
64
  export interface StructureVariation {
62
65
  flag:
63
66
  | 'reread'
@@ -67,7 +70,8 @@ export interface StructureVariation {
67
70
  | 'exprHome'
68
71
  | 'derivedHome'
69
72
  | 'mergeHome'
70
- | 'unsCmp'
73
+ | 'escapeHome'
74
+ | 'signedCmp'
71
75
  | 'freshMerge'
72
76
  | 'copyDefPos'
73
77
  | 'siteSense';
@@ -203,17 +207,40 @@ export const STRUCTURE_VARIATIONS: readonly StructureVariation[] = [
203
207
  perLiftGate: hasMergeFeedHome,
204
208
  strip: true,
205
209
  },
206
- // `/uns-cmp` — spell unsigned compares unsigned (structure.ts unsignedCompareSpelling): an
207
- // icmp_u* operand takes a (u32) cast where the rendered operands do not guarantee the
208
- // unsignedness, and a mixed-claimant declaration reconciles to u32 when nothing under the
209
- // name needs signed. Which side the source spelled is genuinely ambiguous: a signed spelling
210
- // that byte-matched was PROVED non-negative by the compiler (only then does it emit the
211
- // unsigned branch from a signed compare), and emission's provable set is smaller than the
212
- // compiler's. Gated on the function having an unsigned compare at all.
210
+ // `/escape-home` — the escaping-extension-home variation (structure/analysis.ts AnalyzeOptions
211
+ // homeEscapingExtensions): a `zext`/`sext` with 2+ consumers, none of them in its own block,
212
+ // materializes into a local — the register the asm narrowed into once and every later block read
213
+ // — where the default re-evaluates the truncation at each consumer (`(u16)v` per use). The
214
+ // MIRROR of `/expr-home`, which wants a def outside a loop and a consumer inside; neither scope
215
+ // reaches the other's shape. Gated per symbol-map setting like its four siblings.
216
+ //
217
+ // A VARIATION, not a default, and `pokeemerald:AcroBikeHandleInputTurning:agbcc` is the row that
218
+ // decides it: it is a MATCH inside the scope, so forced on the spelling would be REPLACED across
219
+ // the fan rather than added to it. As a variation `compareScored` rides the un-homed sibling
220
+ // beside it and the match cannot be lost — the same argument `/merge-home` records above.
213
221
  {
214
- flag: 'unsCmp',
215
- name: 'uns-cmp',
216
- options: (on) => ({ unsignedCompareSpelling: on }),
222
+ flag: 'escapeHome',
223
+ name: 'escape-home',
224
+ options: (on) => ({ homeEscapingExtensions: on }),
225
+ perLiftGate: hasEscapingExtension,
226
+ strip: true,
227
+ },
228
+ // `/signed-cmp` — spell an unsigned compare SIGNED (structure.ts unsignedCompareSpelling OFF): no
229
+ // `(u32)` cast at an icmp_u* and no u32 reconciliation of a mixed-claimant declaration. The
230
+ // default is the one spelling value-faithful without a proof; this one is a real source where the
231
+ // compiler PROVED both sides non-negative (only then does it emit the unsigned branch from a
232
+ // signed compare), which emission cannot always repeat. Where the proof does not hold it is a
233
+ // different program, so rank.ts judges every emitted tree (`signedOverUnsignedCompare`) and makes
234
+ // a signed rendering `Candidate.matchOnly`, whichever setting or respell spelled it. A tie or a
235
+ // better nonmatch score is no evidence for it (`synthetic:selloop:gcc2.7.2kmc`).
236
+ // KNOWN GAP, in the safe direction: the judgment reads the tree's declarations, and globals are
237
+ // not among them, so a compare that renders unsigned only through a `void *` global the backend
238
+ // declares (ptrcell.test.ts KR1, `g >= gLimit`) is still withheld on a nonmatch.
239
+ // Gated on the function having an unsigned compare at all.
240
+ {
241
+ flag: 'signedCmp',
242
+ name: 'signed-cmp',
243
+ options: (on) => ({ unsignedCompareSpelling: !on }),
217
244
  sharedGate: (sharedLift) => sharedLift.blocks.some((b) => b.ops.some((op) => op.opcode.startsWith('icmp_u'))),
218
245
  strip: true,
219
246
  },
@@ -351,18 +378,31 @@ export const STRUCTURE_VARIATIONS: readonly StructureVariation[] = [
351
378
  },
352
379
  ];
353
380
 
354
- /** The STACKED variations (sanctioned in the POLICY note at rank.ts's respell site): each entry is
355
- * a statement-order/shape respell variation orthogonal to every other respell variation, derived
356
- * onto every source. Each fires alone, plus
357
- * all of them together in table order — not the full subset lattice; the pairs question is
358
- * settled by applyStacked' skip-on-decline below, and a row demanding a true EXCLUSION pair —
359
- * all three fire, the match needs exactly two — is what would earn the lattice. */
360
- export const STACKED_VARIATIONS: { name: VariationName; apply: (sfn: SFn) => SFn | null }[] = [
381
+ /** The statement-order/shape members. */
382
+ const STACKED_SHAPES: { name: VariationName; apply: (sfn: SFn) => SFn | null }[] = [
361
383
  { name: 'initfirst', apply: initFirstGuards },
362
384
  { name: 'pollguard', apply: pollGuards },
363
385
  { name: 'pollread', apply: pollReads },
364
386
  ];
365
387
 
388
+ /** The declaration-width members: one dimension, the width each kind of local is declared at
389
+ * (l3/narrowdecl.ts). A function holding a cast-written local,
390
+ * load-written locals and call-result locals may need any combination of the three kinds narrow
391
+ * while all three fire (the matching suite compiles all eight), so every subset is offered. */
392
+ const STACKED_WIDTHS: { name: VariationName; apply: (sfn: SFn) => SFn | null }[] = [
393
+ { name: 'narrow-decl', apply: narrowDeclarations },
394
+ { name: 'narrow-load', apply: narrowLoadDeclarations },
395
+ { name: 'narrow-read', apply: narrowReadDeclarations },
396
+ ];
397
+
398
+ /** The STACKED variations (sanctioned in the POLICY note at rank.ts's respell site): each entry is
399
+ * a statement-order/shape or declaration-width respell variation orthogonal to every respell
400
+ * variation outside this table, derived onto every source. Two members need not commute
401
+ * (`/narrow-decl` and `/narrow-read` compete for one cast, `/narrow-load` and `/narrow-read` for
402
+ * one local), and table order then decides which one takes it in a candidate holding both. Which
403
+ * of them a candidate applies is STACKED_SUBSETS. */
404
+ export const STACKED_VARIATIONS = [...STACKED_SHAPES, ...STACKED_WIDTHS];
405
+
366
406
  /** The PRE-RESPELL variations (sanctioned in the POLICY note at rank.ts's respell site): a tree
367
407
  * rewrite applied BEFORE the respell set, so the whole set derives from its output instead of
368
408
  * composing onto it. Same record type as STACKED_VARIATIONS above, and deliberately so — the only difference is
@@ -430,17 +470,47 @@ export const STACKED_VARIATIONS: { name: VariationName; apply: (sfn: SFn) => SFn
430
470
  * method are in `apps/benchmark/dataset/synthetic.ts`'s `/unmerge` block. */
431
471
  export const PRE_RESPELL_VARIATIONS: typeof STACKED_VARIATIONS = [{ name: 'unmerge', apply: unmergeJoins }];
432
472
 
433
- export const STACKED_SUBSETS: (typeof STACKED_VARIATIONS)[number][][] = [
434
- ...STACKED_VARIATIONS.map((x) => [x]),
435
- ...(STACKED_VARIATIONS.length > 1 ? [STACKED_VARIATIONS] : []),
436
- ];
473
+ /** Every subset of `xs`, the empty one first, each in `xs`' order. */
474
+ const subsets = <X>(xs: readonly X[]): X[][] =>
475
+ xs.reduce<X[][]>((acc, x) => [...acc, ...acc.map((s) => [...s, x])], [[]]);
476
+
477
+ /** The stacked members a candidate applies: the table's two dimensions crossed, a shape choice
478
+ * (none, each shape alone, or every shape) by a width choice (any subset of the widths, none
479
+ * included), the shapes first. A product, because a function may need a shape AND a strict subset
480
+ * of the widths that fire: `initfirst` beside a narrow cast-written local and wide load-written
481
+ * ones (the matching suite compiles that pair both ways). The shapes are not a lattice of their own:
482
+ * applyStacked's skip-on-decline makes "every shape" each pair of them wherever the third declines,
483
+ * and a row demanding a strict subset of the shapes while all fire is what would earn one. Fewest
484
+ * members first, so where everything else ties the smaller set is enumerated first and wins. */
485
+ export const STACKED_SUBSETS: (typeof STACKED_VARIATIONS)[number][][] = subsets(STACKED_WIDTHS)
486
+ .flatMap((widths) =>
487
+ [[], ...STACKED_SHAPES.map((x) => [x]), ...(STACKED_SHAPES.length > 1 ? [STACKED_SHAPES] : [])].map((shapes) => [
488
+ ...shapes,
489
+ ...widths,
490
+ ]),
491
+ )
492
+ .filter((subset) => subset.length > 0)
493
+ .sort((a, b) => a.length - b.length);
494
+
495
+ /** A stacked member that threw, named by the members that fired before it and then itself: the
496
+ * step that threw, which every subset reaching it shares. Reported under that name rather than
497
+ * the whole subset's, so a consumer deduping on the step prints one broken member once and not
498
+ * once per subset holding it. */
499
+ export class StackedMemberThrew extends Error {
500
+ constructor(
501
+ readonly variations: readonly VariationName[],
502
+ cause: unknown,
503
+ ) {
504
+ super(cause instanceof Error ? cause.message : String(cause), { cause });
505
+ }
506
+ }
437
507
 
438
508
  /** The subset applied in table order, SKIP-ON-DECLINE: a member that declines contributes
439
509
  * nothing rather than killing the combination — the all-shapes candidate is "everything that
440
510
  * fires", so a pair is reachable whenever the third declines. The variations are the members that
441
- * actually FIRED, so they never name a variation that declined; a fired-set that
442
- * duplicates a smaller subset emits identical source and the dedup collapses it. Null when
443
- * nothing fired. */
511
+ * actually FIRED, so they never name a variation that declined; a fired set that
512
+ * duplicates a smaller subset's derives the same tree, which the caller emits once
513
+ * (`firstFired`). Null when nothing fired. A member that throws throws StackedMemberThrew. */
444
514
  export const applyStacked = (
445
515
  subset: readonly (typeof STACKED_VARIATIONS)[number][],
446
516
  from: SFn,
@@ -448,7 +518,12 @@ export const applyStacked = (
448
518
  let cur = from;
449
519
  const fired: VariationName[] = [];
450
520
  for (const sp of subset) {
451
- const r = sp.apply(cur);
521
+ let r: SFn | null;
522
+ try {
523
+ r = sp.apply(cur);
524
+ } catch (e) {
525
+ throw new StackedMemberThrew([...fired, sp.name], e);
526
+ }
452
527
  if (r) {
453
528
  cur = r;
454
529
  fired.push(sp.name);
@@ -457,6 +532,17 @@ export const applyStacked = (
457
532
  return fired.length > 0 ? { out: cur, variations: fired } : null;
458
533
  };
459
534
 
535
+ /** Whether `fired` is a fired set not yet in `seen`, recording it. Two subsets that fire the same
536
+ * members derive the same tree, so a caller emits only the first. */
537
+ export const firstFired = (seen: Set<string>, fired: readonly VariationName[]): boolean => {
538
+ const key = fired.join('/');
539
+ if (seen.has(key)) {
540
+ return false;
541
+ }
542
+ seen.add(key);
543
+ return true;
544
+ };
545
+
460
546
  /** The locals a variation added — a NAME diff rather than a positional slice, so a pass that ever
461
547
  * reorders locals cannot silently empty the set. It is what scopes `/volatile` to the pointers
462
548
  * the variation itself created (volatilePtrLocals' `only`), leaving the tree's own locals alone. */