@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
@@ -20,6 +20,7 @@
20
20
  // byte form (`add(base, index)`, elemSize 1) is deferred: without types, base and index are
21
21
  // indistinguishable there.
22
22
  import { Fn, Op, Value, defOpMap, mkOp, mkValue, replaceAllUsesWith } from '../ir/core';
23
+ import { carryDiscipline } from '../ir/discipline';
23
24
 
24
25
  // If `addr` is `add(base, shl(index, k))` with `1 << k === width`, return {base, index}.
25
26
  // The `add` is commutative, so the scaled side may be either operand.
@@ -62,11 +63,11 @@ export function recognizeArrays(fn: Fn): number {
62
63
  operands: [m.base, m.index],
63
64
  results: [res],
64
65
  // listOrder rides along: an ldmia-expanded load keeps its stream-order caveat as an aload
65
- attrs: {
66
+ attrs: carryDiscipline(op, {
66
67
  elemSize: op.attrs.width as number,
67
68
  signed: op.attrs.signed as boolean,
68
69
  ...(op.attrs.listOrder === true && { listOrder: true }),
69
- },
70
+ }),
70
71
  });
71
72
  replaceAllUsesWith(fn, op.results[0], res);
72
73
  count++;
@@ -77,7 +78,7 @@ export function recognizeArrays(fn: Fn): number {
77
78
  }
78
79
  b.ops[i] = mkOp('astore', {
79
80
  operands: [m.base, m.index, op.operands[1]],
80
- attrs: { elemSize: op.attrs.width as number },
81
+ attrs: carryDiscipline(op, { elemSize: op.attrs.width as number }),
81
82
  });
82
83
  count++;
83
84
  }
@@ -0,0 +1,131 @@
1
+ // asmlift — the decider for ir/discipline.ts's `declared` placement: a memory access of an object
2
+ // the project's symbol map declares volatile.
3
+ //
4
+ // The stamp is a function of the IR, the map the function is lifted and structured under, and the
5
+ // compiler, and nothing else: an access whose base reaches a global the map qualifies, at the byte
6
+ // the access names (`declaresVolatile`) — and, for a read, one the compiler could have made of a
7
+ // `volatile` (target.ts `readCouldBeVolatile`, which the device pin asks too). It is put as the
8
+ // lift is made (pipeline.ts `liftStamped`), before the first pass that asks whether a read may be
9
+ // deleted, folded or moved, and re-derived by every pass that changes which object an access names
10
+ // and by structuring. Structuring the same function under another map — the `/raw-globals` setting
11
+ // has none — re-runs it, and an op the new map does not qualify loses the stamp, so a structuring
12
+ // never answers for a map it was not given. The base is read as the structurer will read it: a join
13
+ // whose every edge passes one address is that address (`addressThroughJoins`). A base that reaches
14
+ // no name is a pointer parameter, a raw address or a genuine join: no declaration is in evidence,
15
+ // and the access stays unplaced unless the lift pinned it (`device`).
16
+ //
17
+ // A read structuring stamps that the earlier stamps left plain was plain to every raising pass, and a
18
+ // short-circuit fold may have lifted it out of the arm it ran in. structure.ts declines on such a
19
+ // read in a `&&`/`||`'s guarded operand rather than place it.
20
+ import { globalBaseOf, globalCellOf } from '../ir/alias';
21
+ import { type Fn, type Op, type Value, defOpMap } from '../ir/core';
22
+ import { DECLARED_VOLATILE } from '../ir/discipline';
23
+ import { MEM_BASE_OPS } from '../ir/opcodes';
24
+ import { type SymbolInfo, type SymbolMap, declaresVolatile, symbolsByName } from '../symbols';
25
+ import { type TargetDescription, readCouldBeVolatile } from '../target';
26
+ import { valueNumberOf } from './gvn';
27
+
28
+ /** The compiler fact the stamp reads. */
29
+ export type StampBehaviors = Pick<TargetDescription['compilerBehaviors'], 'volatileReadsExtendInRegister'>;
30
+
31
+ /** May this memory access be stamped: a store, or a read the compiler could have made of a
32
+ * `volatile`? */
33
+ const stampable = (op: Op, behaviors: StampBehaviors): boolean =>
34
+ MEM_BASE_OPS.has(op.opcode) &&
35
+ (op.opcode === 'store' || op.opcode === 'astore' || readCouldBeVolatile(behaviors, op));
36
+
37
+ /** The address a value stands for once `addrnum` (raise/pre-recovery.ts) has run: a block parameter
38
+ * every in-edge of which, a back edge's self-reference aside, passes one value after numbering
39
+ * (raise/gvn.ts `valueNumberOf`) is that value, as `simplifyTrivialPhis` collapses it; any other
40
+ * value stands for itself. Each arm of an `if` loading `&gVolReg` into the register the join reads
41
+ * is such a parameter, and the stamp is put before that pass, so it reads the base the structurer
42
+ * will. */
43
+ function addressThroughJoins(fn: Fn): (v: Value) => Value {
44
+ const incoming = new Map<Value, Value[]>();
45
+ for (const b of fn.blocks) {
46
+ for (const op of b.ops) {
47
+ for (const s of op.successors) {
48
+ if (s.block === fn.blocks[0]) {
49
+ continue;
50
+ }
51
+ s.block.params.forEach((param, i) => {
52
+ if (i < s.args.length) {
53
+ (incoming.get(param) ?? incoming.set(param, []).get(param)!).push(s.args[i]);
54
+ }
55
+ });
56
+ }
57
+ }
58
+ }
59
+ const numberOf = valueNumberOf(fn);
60
+ const through = (v: Value, onWalk: Set<Value>): Value | null => {
61
+ const ins = incoming.get(v);
62
+ if (ins === undefined) {
63
+ return v;
64
+ }
65
+ if (onWalk.has(v)) {
66
+ return null;
67
+ }
68
+ onWalk.add(v);
69
+ const each = ins.filter((a) => a !== v).map((a) => through(a, onWalk));
70
+ onWalk.delete(v);
71
+ const first = each[0];
72
+ return first !== undefined && first !== null && each.every((a) => a !== null && numberOf(a) === numberOf(first))
73
+ ? first
74
+ : null;
75
+ };
76
+ return (v) => through(v, new Set()) ?? v;
77
+ }
78
+
79
+ /** Stamp every memory access of `fn` to an object `symbols` declares volatile — a read only where a
80
+ * compiler behaving as `behaviors` says could have made it of one — and clear the stamp from every
81
+ * other op. Returns the ops it stamped that were not stamped before. */
82
+ export function stampDeclaredVolatile(
83
+ fn: Fn,
84
+ symbols: ReadonlyMap<string, SymbolInfo> | undefined,
85
+ behaviors: StampBehaviors,
86
+ ): Op[] {
87
+ const added: Op[] = [];
88
+ const defs = symbols ? defOpMap(fn) : undefined;
89
+ const addressOf = symbols ? addressThroughJoins(fn) : undefined;
90
+ for (const b of fn.blocks) {
91
+ for (const op of b.ops) {
92
+ const addr = defs && stampable(op, behaviors) ? addressOf!(op.operands[0]) : undefined;
93
+ const base = addr !== undefined ? globalBaseOf(defs!, addr) : null;
94
+ // A subscript reaches the object while naming no cell, so only a `load`/`store` asks by byte,
95
+ // and an access that names no byte is placed only where every byte it could reach is qualified.
96
+ const cell =
97
+ base !== null && (op.opcode === 'load' || op.opcode === 'store')
98
+ ? globalCellOf(defs!, addr!, op.attrs.off as number)
99
+ : null;
100
+ const declared = base !== null && declaresVolatile(symbols!.get(base), cell === null ? null : cell.byte);
101
+ // A fresh record either way: a rebuilder may hand two ops one `attrs` object.
102
+ if (declared && op.attrs[DECLARED_VOLATILE] !== true) {
103
+ op.attrs = { ...op.attrs, [DECLARED_VOLATILE]: true };
104
+ added.push(op);
105
+ } else if (!declared && DECLARED_VOLATILE in op.attrs) {
106
+ const { [DECLARED_VOLATILE]: _, ...rest } = op.attrs;
107
+ op.attrs = rest;
108
+ }
109
+ }
110
+ }
111
+ return added;
112
+ }
113
+
114
+ /** {@link stampDeclaredVolatile} under an address-keyed project map, through the name-keyed view
115
+ * structuring is handed for the same map (symbols.ts `symbolsByName`), narrowed to the names the
116
+ * function's own `gaddr`s carry. */
117
+ export function stampDeclaredVolatileUnder(fn: Fn, map: SymbolMap | undefined, behaviors: StampBehaviors): void {
118
+ if (map === undefined) {
119
+ stampDeclaredVolatile(fn, undefined, behaviors);
120
+ return;
121
+ }
122
+ const names = new Set<string>();
123
+ for (const b of fn.blocks) {
124
+ for (const op of b.ops) {
125
+ if (op.opcode === 'gaddr') {
126
+ names.add(op.attrs.sym as string);
127
+ }
128
+ }
129
+ }
130
+ stampDeclaredVolatile(fn, symbolsByName(map, names), behaviors);
131
+ }
@@ -41,7 +41,7 @@
41
41
  // the biased arm must be the negative one — the consistency check m2c's `49b5d87` also adds),
42
42
  // because every one of those is a way for a superficially similar diamond to mean something else.
43
43
  import { Block, Fn, Op, Value, defOpMap, mkOp, mkValue, predecessors, replaceAllUsesWith } from '../ir/core';
44
- import { EFFECTFUL_OPS } from '../ir/opcodes';
44
+ import { effectful } from '../ir/discipline';
45
45
  import { T } from '../ir/types';
46
46
 
47
47
  /** `shr_s v {imm=k}` → k, else null. */
@@ -110,7 +110,7 @@ export function recognizeDivPow2(fn: Fn): boolean {
110
110
  continue;
111
111
  }
112
112
  const bp = preds.get(bias) ?? [];
113
- if (bp.length !== 1 || bias.ops.some((op) => EFFECTFUL_OPS.has(op.opcode))) {
113
+ if (bp.length !== 1 || bias.ops.some(effectful)) {
114
114
  continue;
115
115
  }
116
116
  const h = bp[0];
@@ -8,3 +8,20 @@ export class RaiseUnsupportedError extends Error {
8
8
  this.name = 'RaiseUnsupportedError';
9
9
  }
10
10
  }
11
+
12
+ /** The one raise refusal a caller ACTS ON rather than only reporting: raise/structs.ts cannot
13
+ * reproduce a layout with a plain struct because two accesses' byte ranges COLLIDE. For a base
14
+ * whose address is declared outside the function that is not a reason to decline, since each
15
+ * access is spellable on its own; for any other base the overlap is declared as a union member
16
+ * where the union can be laid out. A CLASS rather than a substring test on the message, because
17
+ * the two facts a caller needs (which refusal this is, and what to tell the reader) would
18
+ * otherwise be the same string, and the guard would then fall open the day the message is
19
+ * reworded. Every other raise/structs.ts refusal (a packed layout, a union it cannot lay out)
20
+ * stays a plain RaiseUnsupportedError and declines: a subclass is minted for a caller that acts
21
+ * on it, not for every refusal that wants a name. */
22
+ export class StructOverlapError extends RaiseUnsupportedError {
23
+ constructor(message: string) {
24
+ super(message);
25
+ this.name = 'StructOverlapError';
26
+ }
27
+ }
@@ -0,0 +1,243 @@
1
+ // asmlift — the soft-float boundary (L1 recognition): where integer words become floats.
2
+ //
3
+ // Three things cross it. A runtime helper whose op is a float op (`isFloatHelper`) — the peer of
4
+ // `raise/widehelpers.ts`, gated by the target's runtime table rather than a hardware capability.
5
+ // A parameter an ordinary callee declares `double` (the frontend's `doubles` on the call). And a
6
+ // double literal, two constant words, where the target states their order
7
+ // (`TargetDescription.doubleArgWords`) and its compiler reads a literal back
8
+ // (`compilerBehaviors.roundTripsDoubleLiterals`). What sets all three apart from the integer
9
+ // helpers is the value's KIND: the words are bits in integer registers, and the pass re-types them
10
+ // as floats, which only data flow may justify — see the function below.
11
+ import { Fn, Op, Value, mkOp, mkValue, replaceAllUsesWith } from '../ir/core';
12
+ import { doubleBits, doubleOf } from '../ir/float-bits';
13
+ import { T } from '../ir/types';
14
+ import { arrivesAsDeclared, helperOp, isFloatHelper, lookupHelper } from '../runtime-helpers';
15
+ import type { TargetDescription } from '../target';
16
+ import { RaiseUnsupportedError } from './errors';
17
+ import { isArgumentPair } from './pairparams';
18
+
19
+ /** Rewrite the float-arithmetic helper calls to the float ops, in place, each value at the width its
20
+ * helper's signature states, and hand a parameter the callee declares `double` (the frontend's
21
+ * `doubles` on the call) a double. Returns whether anything changed.
22
+ *
23
+ * A SOFT-FLOAT VALUE IS BITS IN INTEGER REGISTERS, AND ONLY DATA FLOW MAY READ THEM AS A FLOAT. A
24
+ * single travels in one register; a double in the pair a long long travels in, which the frontend
25
+ * builds the way it builds a long long's, `concat(lo = r0, hi = r1)` — but agbcc stores a double
26
+ * high word first (thumb.h:335 `FLOAT_WORDS_BIG_ENDIAN`): r0 holds the sign and exponent. Moving
27
+ * the value whole keeps it right, so a float may come only from where the ABI hands one over
28
+ * whole — this function's argument slot, or two consecutive ones for a double, or another of these
29
+ * helpers' results — and may go only into another of them, the return, or a parameter a callee
30
+ * declares `double`. Anything else refuses, and the call stays for `refuseUnmodelledHelpers` to
31
+ * gap:
32
+ * - an operand built from a load or two unrelated words, whose halves the int64 naming would put
33
+ * in the wrong order. Two CONSTANT words are the exception: they are a literal, read in the
34
+ * target's word order into an `fconst` (`a + 1.5` stages `0x3ff80000` in r2, its high word);
35
+ * - an argument the function also reads on its own, as a word or as one half of a pair;
36
+ * - a result read as a word or a half (`*(int *)&c` is a double's HIGH word, in r0), passed to
37
+ * any other call, or carried across an edge.
38
+ * A call refused takes every call its result feeds or is fed by with it, so what folds is a closed
39
+ * set of floats and verify's float rule holds by construction. */
40
+ export function recognizeFloatHelpers(fn: Fn, target: TargetDescription): boolean {
41
+ const table = target.runtimeHelpers ?? {};
42
+ let sites: Op[] = [];
43
+ const consumers = new Map<Op, readonly number[]>();
44
+ const def = new Map<Value, Op>();
45
+ const users = new Map<Value, Array<Op | null>>(); // null: an edge argument
46
+ const use = (v: Value, by: Op | null) => users.set(v, [...(users.get(v) ?? []), by]);
47
+ const helperOf = (op: Op) => (op.opcode === 'call' ? lookupHelper(table, String(op.attrs.target)) : undefined);
48
+ for (const block of fn.blocks) {
49
+ for (const op of block.ops) {
50
+ op.results.forEach((r) => def.set(r, op));
51
+ op.operands.forEach((o) => use(o, op));
52
+ op.successors.forEach((s) => s.args.forEach((a) => use(a, null)));
53
+ const helper = helperOf(op);
54
+ if (
55
+ helper &&
56
+ isFloatHelper(helper) &&
57
+ arrivesAsDeclared(
58
+ helper,
59
+ op.operands.map((o) => o.type),
60
+ op.results.map((r) => r.type),
61
+ )
62
+ ) {
63
+ sites.push(op);
64
+ } else if (!helper && op.opcode === 'call' && Array.isArray(op.attrs.doubles)) {
65
+ consumers.set(op, op.attrs.doubles as number[]);
66
+ }
67
+ }
68
+ }
69
+ if (!sites.length && !consumers.size) {
70
+ return false;
71
+ }
72
+ // A projection nothing reads is not a read of a half: `dce` runs ahead of this pass only after a
73
+ // fold that changed the IR, so the frontend's split of the result back into its registers may stand.
74
+ const deadHalf = (u: Op | null) =>
75
+ u !== null && (u.opcode === 'lo32' || u.opcode === 'hi32') && !users.has(u.results[0]);
76
+ // A USE THAT TAKES THE VALUE AS A FLOAT: a helper the fold keeps, or a parameter the callee declares
77
+ // `double` (the frontend's `doubles`) — and only that parameter, so the same value handed to the
78
+ // same call as a word is a word read.
79
+ const floatUse = (u: Op | null, v: Value, calls: ReadonlySet<Op>): boolean =>
80
+ u !== null &&
81
+ (calls.has(u) || (consumers.has(u) && u.operands.every((o, i) => o !== v || consumers.get(u)!.includes(i))));
82
+ const floatsOnly = (v: Value, calls: ReadonlySet<Op>) => (users.get(v) ?? []).every((u) => floatUse(u, v, calls));
83
+ const entry = fn.blocks[0].params;
84
+ // A `concat` of the same two slots in the same order as `d`: the frontend builds one afresh at each
85
+ // call it hands the pair to, so `g(x); g(x)` reads one parameter through two of them.
86
+ const samePair = (u: Op | null, d: Op): u is Op =>
87
+ u?.opcode === 'concat' && u.operands[0] === d.operands[0] && u.operands[1] === d.operands[1];
88
+ // The argument slot itself, or the pair of them `isArgumentPair` names, read by nothing but
89
+ // `concat`s of that pair, each taken as a float; `arrivesAsDeclared` has already held each operand
90
+ // to its parameter's width.
91
+ const argument = (v: Value, calls: ReadonlySet<Op>): boolean => {
92
+ const d = def.get(v);
93
+ if (d === undefined) {
94
+ return entry.includes(v) && floatsOnly(v, calls);
95
+ }
96
+ return (
97
+ isArgumentPair(entry, d, (p) => users.get(p)!.filter((u) => !samePair(u, d)).length + 1) &&
98
+ users.get(d.operands[0])!.every((u) => samePair(u, d) && floatsOnly(u.results[0], calls))
99
+ );
100
+ };
101
+ const fromCall = (v: Value, calls: ReadonlySet<Op>) => {
102
+ const d = def.get(v);
103
+ return d !== undefined && calls.has(d);
104
+ };
105
+ // A CONSTANT WORD, as a compiler may build one in two instructions: agbcc moves a word that is a
106
+ // shifted byte as `mov`+`lsl` (thumb.md:66, `thumb_shiftable_const` at thumb.c:61) and one in
107
+ // -255..-1 as `mov`+`neg` (thumb.md:88), so the high word of 2.0 reaches r3 as `0x80 << 23`. Its
108
+ // value, or undefined where `v` is no constant.
109
+ const wordOf = (v: Value): number | undefined => {
110
+ const d = def.get(v);
111
+ if (d?.opcode === 'const') {
112
+ return Number(d.attrs.value);
113
+ }
114
+ const shift = d?.opcode === 'shl' ? d.attrs.imm : undefined;
115
+ const w =
116
+ d?.operands.length === 1 && (d.opcode === 'neg' || typeof shift === 'number') ? wordOf(d.operands[0]) : undefined;
117
+ return w === undefined ? undefined : d!.opcode === 'neg' ? -w | 0 : (w << (shift as number)) | 0;
118
+ };
119
+ // A LITERAL IS TWO CONSTANT WORDS, HIGH WORD FIRST on a target whose double crosses a call in
120
+ // general words (`TargetDescription.doubleArgWords`) — the order that makes the long long naming
121
+ // of the same pair another number. Its bits, or undefined where the pair is no literal, the target
122
+ // states no such double, its compiler is not known to read the literal back
123
+ // (`compilerBehaviors.roundTripsDoubleLiterals`), or the double is not finite — which no C literal
124
+ // spells.
125
+ const literalBits = (v: Value): string | undefined => {
126
+ const d = def.get(v);
127
+ const [first, second] = (d?.opcode === 'concat' ? d.operands : []).map(wordOf);
128
+ if (
129
+ target.doubleArgWords === undefined ||
130
+ target.compilerBehaviors.roundTripsDoubleLiterals !== true ||
131
+ first === undefined ||
132
+ second === undefined
133
+ ) {
134
+ return undefined;
135
+ }
136
+ const bits = doubleBits(first, second);
137
+ return Number.isFinite(doubleOf(bits)) ? bits : undefined;
138
+ };
139
+ const literal = (v: Value, calls: ReadonlySet<Op>) => literalBits(v) !== undefined && floatsOnly(v, calls);
140
+ const source = (v: Value, calls: ReadonlySet<Op>) => argument(v, calls) || fromCall(v, calls) || literal(v, calls);
141
+ for (;;) {
142
+ const calls = new Set(sites);
143
+ const kept = sites.filter(
144
+ (op) =>
145
+ op.operands.every((o) => source(o, calls)) &&
146
+ (users.get(op.results[0]) ?? []).every(
147
+ (u) => deadHalf(u) || floatUse(u, op.results[0], calls) || u?.opcode === 'ret',
148
+ ),
149
+ );
150
+ if (kept.length === sites.length) {
151
+ break;
152
+ }
153
+ sites = kept;
154
+ }
155
+ const calls = new Set(sites);
156
+ // A DECLARED DOUBLE HAS NO FALLBACK. A helper the fold refuses stays a call for
157
+ // `refuseUnmodelledHelpers` to gap, but an ordinary callee is no helper, and its two words handed
158
+ // on as integers are the wrong number — so a double parameter nothing here can hand a double
159
+ // refuses the function. It counts arguments as the source does: an `sret` call's operand 0, the
160
+ // hidden pointer, is not one.
161
+ for (const [op, at] of consumers) {
162
+ for (const i of at) {
163
+ if (!source(op.operands[i], calls)) {
164
+ const arg = i + (op.attrs.sret === true ? 0 : 1);
165
+ throw new RaiseUnsupportedError(
166
+ `cannot lift '${fn.name}': argument ${arg} of the call to '${String(op.attrs.target)}' ` +
167
+ 'is a `double` its callee declares, and its two words are not a double this function was ' +
168
+ 'handed, a runtime helper returned or a finite literal staged, moved whole',
169
+ );
170
+ }
171
+ }
172
+ }
173
+ if (!sites.length && !consumers.size) {
174
+ return false;
175
+ }
176
+ const floatOf = new Map<Value, Value>();
177
+ const float = (bits: number) => mkValue(bits > 32 ? T.f64() : T.f32());
178
+ // the `fconst` each literal's `concat` becomes, and the ops computing the constant words only it read
179
+ const literals = new Map<Op, Op>();
180
+ const spent = new Set<Op>();
181
+ const spend = (w: Value, by: Op) => {
182
+ const d = def.get(w)!;
183
+ if (users.get(w)!.every((u) => u === by)) {
184
+ spent.add(d);
185
+ d.operands.forEach((o) => spend(o, d));
186
+ }
187
+ };
188
+ // One parameter in its first slot's place, which is where the ABI put the value.
189
+ const retype = (o: Value, bits: number) => {
190
+ if (floatOf.has(o) || fromCall(o, calls)) {
191
+ return;
192
+ }
193
+ const d = def.get(o);
194
+ const pattern = literalBits(o);
195
+ if (pattern !== undefined) {
196
+ const whole = float(64);
197
+ literals.set(d!, mkOp('fconst', { results: [whole], attrs: { bits: pattern } }));
198
+ d!.operands.forEach((w) => spend(w, d!));
199
+ floatOf.set(o, whole);
200
+ return;
201
+ }
202
+ const slots = d === undefined ? [o] : d.operands;
203
+ const whole = float(bits);
204
+ entry.splice(entry.indexOf(slots[0]), slots.length, whole);
205
+ for (const u of d === undefined ? [] : users.get(slots[0])!) {
206
+ if (samePair(u, d!)) {
207
+ floatOf.set(u.results[0], whole);
208
+ }
209
+ }
210
+ floatOf.set(o, whole);
211
+ };
212
+ for (const op of sites) {
213
+ const helper = helperOf(op)!;
214
+ op.operands.forEach((o, i) => retype(o, helper.params[i]));
215
+ floatOf.set(op.results[0], float(helper.returns));
216
+ }
217
+ for (const [op, at] of consumers) {
218
+ at.forEach((i) => retype(op.operands[i], 64));
219
+ }
220
+ for (const block of fn.blocks) {
221
+ block.ops = block.ops.flatMap((op) => {
222
+ if (calls.has(op)) {
223
+ return [
224
+ helperOp(
225
+ helperOf(op)!.op!,
226
+ String(op.attrs.target),
227
+ op.operands.map((o) => floatOf.get(o)!),
228
+ floatOf.get(op.results[0])!,
229
+ ),
230
+ ];
231
+ }
232
+ if (literals.has(op)) {
233
+ return [literals.get(op)!];
234
+ }
235
+ const retired = op.results.length === 1 && floatOf.has(op.results[0]);
236
+ return retired || spent.has(op) || (deadHalf(op) && floatOf.has(op.operands[0])) ? [] : [op];
237
+ });
238
+ }
239
+ for (const [was, now] of floatOf) {
240
+ replaceAllUsesWith(fn, was, now);
241
+ }
242
+ return true;
243
+ }
@@ -505,11 +505,12 @@ export const SHAPE_GATES: readonly Gate<ShapeEvidence>[] = [
505
505
  id: 'index-materialized-first',
506
506
  why: 'a scaling of the index precedes the pool load IN ITS OWN BLOCK — the pointer path, which is the cast spelling',
507
507
  sound: true,
508
- // Its SECOND reaching fixture is the compiler-shaped one: `…and the same function cast-spelled
509
- // still refuses, on the access that CAN be compared` runs the real agbcc output for a function
510
- // that subscripts the name once outside a loop and once inside it. It is not named in
511
- // `guardedBy` because that field is matched against a single test title (gate-contract.ts).
512
- guardedBy: 'global-array-shape.test.ts: one index-first access refuses a symbol the others license',
508
+ // Its SECOND reaching fixture is the compiler-shaped one: the real agbcc output for a function
509
+ // that subscripts the name once outside a loop and once inside it.
510
+ guardedBy: [
511
+ 'global-array-shape.test.ts: one index-first access refuses a symbol the others license',
512
+ 'global-array-shape.test.ts: …and the same function cast-spelled still refuses, on the access that CAN be compared',
513
+ ],
513
514
  rejects: anIndexFirstAccess,
514
515
  },
515
516
  {
@@ -955,6 +956,11 @@ export function inferGlobalArrays(
955
956
  }
956
957
  const pos = positions(fn);
957
958
  for (const [sym, accs] of accessesBySymbol(fn, target, gates.address)) {
959
+ // a function-scope static's shape is its definition's (structure/local-statics.ts), not an
960
+ // inference, and it is declared in the body rather than assumed
961
+ if (fn.localObjects?.has(sym)) {
962
+ continue;
963
+ }
958
964
  const si = Array.isArray(accs) ? shapeOf(accs, pos, gates.shape) : null;
959
965
  if (si !== null) {
960
966
  out.set(sym, si);
package/src/raise/gvn.ts CHANGED
@@ -93,11 +93,8 @@ function keyOf(op: Op): string {
93
93
  return `${op.opcode}(${attrs})`;
94
94
  }
95
95
 
96
- /**
97
- * Collapse operand-free pure definitions that share a key down to one apiece, defined in the entry
98
- * block. Returns the number of definitions removed (0 when nothing changed).
99
- */
100
- export function numberPureValues(fn: Fn): number {
96
+ /** The blocks numbering looks in: those the entry reaches through any op's successors. */
97
+ function numberedBlocks(fn: Fn): Set<Block> {
101
98
  // REACHABLE blocks only. The entry dominates everything REACHABLE — it does not dominate an
102
99
  // unreachable block, and verify()'s dominator fixpoint models that faithfully (a block with no
103
100
  // predecessors converges to dom = {itself}). Numbering a group with a member in such a block
@@ -120,6 +117,15 @@ export function numberPureValues(fn: Fn): number {
120
117
  if (fn.blocks[0]) {
121
118
  walk(fn.blocks[0]);
122
119
  }
120
+ return reachable;
121
+ }
122
+
123
+ /**
124
+ * Collapse operand-free pure definitions that share a key down to one apiece, defined in the entry
125
+ * block. Returns the number of definitions removed (0 when nothing changed).
126
+ */
127
+ export function numberPureValues(fn: Fn): number {
128
+ const reachable = numberedBlocks(fn);
123
129
  const groups = new Map<string, { op: Op; block: number }[]>();
124
130
  fn.blocks.forEach((b, bi) => {
125
131
  if (!reachable.has(b)) {
@@ -164,3 +170,18 @@ export function numberPureValues(fn: Fn): number {
164
170
  entry.ops.unshift(...hoisted);
165
171
  return removed;
166
172
  }
173
+
174
+ /** The class {@link numberPureValues} puts a value in: the key of a numberable definition in a block
175
+ * it numbers, or the value itself. Two values of one class are one value once it has run. */
176
+ export function valueNumberOf(fn: Fn): (v: Value) => Value | string {
177
+ const reachable = numberedBlocks(fn);
178
+ const key = new Map<Value, string>();
179
+ for (const b of reachable) {
180
+ for (const op of b.ops) {
181
+ if (isNumberable(op.opcode) && op.results.length === 1) {
182
+ key.set(op.results[0], keyOf(op));
183
+ }
184
+ }
185
+ }
186
+ return (v) => key.get(v) ?? v;
187
+ }
@@ -23,6 +23,16 @@
23
23
  // A signed and an unsigned right shift of the same value by the same amount differ only in the bits
24
24
  // the shift brings in, so `%134` is `sext(%133, 32 - 16)` — the same rewrite, and the reason this
25
25
  // pass matches the pair rather than either extension on its own.
26
+ //
27
+ // And a third, where the variable is carried SHIFTED and never extended at all: an `s8` walked down
28
+ // a list, `i = buf[i].next`, is kept as `next << 24` and the test reads it back with `asr #24`.
29
+ // The cast idiom folds that `asr` of the `lsl` into a `sext` of the load, which is the same bits
30
+ // and no longer reads the carried value:
31
+ //
32
+ // %41 = shl %40 {imm=24} what `i` holds next iteration, shifted
33
+ // %42 = sext %40 {8} the loop test
34
+ //
35
+ // `sext(v, 32 - k) === shr_s(shl(v, k), k)`, so `%42` is re-rooted as `shr_s %41 {k}`.
26
36
  import { Fn, Op, Value, defOpMap } from '../ir/core';
27
37
  import { CAST_WIDTHS, Opcode } from '../ir/opcodes';
28
38
 
@@ -41,8 +51,8 @@ const domainOf = (op: Op, low: Opcode, high: Opcode): { key: string; width: numb
41
51
  return null;
42
52
  };
43
53
 
44
- /** Re-root a sign extension on the co-existing zero extension of the same bits. Returns the number
45
- * of rewrites. */
54
+ /** Re-root a sign extension on the co-existing zero extension of the same bits, or on the carried
55
+ * left shift that holds them. Returns the number of rewrites. */
46
56
  export function rerootNarrowReads(fn: Fn): number {
47
57
  // LOOP-CARRIED edge arguments only. The rewrite is sound for ANY co-existing pair — it only
48
58
  // re-associates two extensions of the same bits — so this gate is about what it BUYS, not about
@@ -87,6 +97,8 @@ export function rerootNarrowReads(fn: Fn): number {
87
97
  // else this could be a use before a def. That is the shape the idiom emits — one increment,
88
98
  // both extensions of it, in the block that computes it.
89
99
  const unsigned = new Map<Value, Map<string, Value>>();
100
+ // …and the carried left shifts, by operand and then by amount, under the same two rules
101
+ const shifted = new Map<Value, Map<number, Value>>();
90
102
  for (const op of b.ops) {
91
103
  const u = domainOf(op, 'zext', 'shr_u');
92
104
  if (u !== null && carried.has(op.results[0])) {
@@ -95,6 +107,21 @@ export function rerootNarrowReads(fn: Fn): number {
95
107
  unsigned.set(op.operands[0], byDomain);
96
108
  continue;
97
109
  }
110
+ if (op.opcode === 'shl' && typeof op.attrs.imm === 'number' && carried.has(op.results[0])) {
111
+ const byAmount = shifted.get(op.operands[0]) ?? new Map<number, Value>();
112
+ byAmount.set(op.attrs.imm, op.results[0]);
113
+ shifted.set(op.operands[0], byAmount);
114
+ continue;
115
+ }
116
+ const k = 32 - (op.attrs.width as number);
117
+ const sh = op.opcode === 'sext' && CAST_WIDTHS.has(32 - k) ? shifted.get(op.operands[0])?.get(k) : undefined;
118
+ if (sh !== undefined) {
119
+ op.opcode = 'shr_s';
120
+ op.operands = [sh];
121
+ op.attrs = { imm: k };
122
+ rewritten++;
123
+ continue;
124
+ }
98
125
  const sgn = domainOf(op, 'sext', 'shr_s');
99
126
  if (sgn === null) {
100
127
  continue;
@@ -58,6 +58,12 @@
58
58
  // u16 v; … *out = v; zext16 4 0 MATCH
59
59
  // s32 v; … *out = (u16)v; zext16 0 MATCH 4
60
60
  //
61
+ // A THIRD SPELLING of rows three and four is the `/escape-home` variation's
62
+ // (structure/analysis.ts `escapingExtensions`): keep `s32 v` and give the truncation a NAME where
63
+ // it happened, `t = (u16)v`. It is not an alternative to this pass — it judges an extension's
64
+ // result where this one judges a block PARAMETER — and it is a ranked candidate where this is a
65
+ // decision, so neither can take the other's input.
66
+ //
61
67
  // Rows one and two are DECIDABLE by the carrier's own readers: a `zext_w` read by a `sext_w` is the
62
68
  // write-back truncation followed by the declaration's own sign extension, and no cast on a wide
63
69
  // local writes that pair. Rows three and four are the same IR in this pass's READER vocabulary —
@@ -131,7 +137,8 @@
131
137
  // admits), and `runPreRecovery` reads the shape ahead of the pass that manufactures it —
132
138
  // `mergeShapes`.
133
139
  import { type Block, type Fn, type Op, type Value, defOpMap, predecessors, replaceAllUsesWith } from '../ir/core';
134
- import { CAST_WIDTHS, REEVAL_UNSAFE_OPS } from '../ir/opcodes';
140
+ import { reevalUnsafe } from '../ir/discipline';
141
+ import { CAST_WIDTHS } from '../ir/opcodes';
135
142
  import { T } from '../ir/types';
136
143
  import { type Gate, firstRejection } from '../l3/gates';
137
144
 
@@ -350,8 +357,8 @@ function mergeArms(preds: Map<Block, Block[]>, fn: Fn, blk: Block): [Block, Bloc
350
357
  * • a MEMORY READ is one op and is NOT one speculatable SET. `gcc/rtlanal.c:1770-1771` sends a
351
358
  * MEM to `rtx_addr_can_trap_p` and `:144-147` says an address held in a plain pseudo CAN trap,
352
359
  * so `jump.c` never hoists a load and the diamond survives under BOTH spellings, carrying no
353
- * information. `REEVAL_UNSAFE_OPS` answers this (effects, reads, or traps — `may_trap_p`
354
- * refuses the trapping divides too, at `rtlanal.c:1774-1784`); `HOIST_UNSAFE_OPS` does NOT,
360
+ * information. `reevalUnsafe` answers this (effects, reads, or traps — `may_trap_p`
361
+ * refuses the trapping divides too, at `rtlanal.c:1774-1784`); `speculationUnsafe` does NOT,
355
362
  * because it omits reads for `raise/shortcircuit.ts`, whose arm C's own `&&` re-guards, and
356
363
  * that exemption does not transfer to speculation above a compare.
357
364
  * • a CONSTANT is not free. `v = a + 0x12345` is a literal-pool `ldr` plus the `add` — two
@@ -382,9 +389,7 @@ function mergeArms(preds: Map<Block, Block[]>, fn: Fn, blk: Block): [Block, Bloc
382
389
  * clause in that table is `sound: false`), so the shared conservative predicate is the right one
383
390
  * until a row asks for the cost model neither pass has. */
384
391
  export function armIsOneSet(b: Block): boolean {
385
- return (
386
- !b.ops.some((op) => REEVAL_UNSAFE_OPS.has(op.opcode)) && b.ops.filter((op) => op.results.length > 0).length === 1
387
- );
392
+ return !b.ops.some(reevalUnsafe) && b.ops.filter((op) => op.results.length > 0).length === 1;
388
393
  }
389
394
 
390
395
  /** The join shape of every block, read ONCE off the IR it is handed.