@asmlift/core 0.6.0 → 0.8.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 (88) hide show
  1. package/README.md +48 -24
  2. package/package.json +1 -1
  3. package/src/backend/cfamily.ts +39 -11
  4. package/src/backend/pascal.ts +2 -2
  5. package/src/codegen-flags.ts +640 -0
  6. package/src/contracts.ts +60 -11
  7. package/src/frontend/disasm.ts +141 -11
  8. package/src/frontend/high-half.ts +149 -0
  9. package/src/frontend/mips.ts +458 -209
  10. package/src/frontend/ppc.ts +332 -67
  11. package/src/frontend/reloc-symbol.ts +109 -0
  12. package/src/frontend/splat.ts +56 -18
  13. package/src/frontend/ssa.ts +127 -30
  14. package/src/frontend/stackargs.ts +420 -0
  15. package/src/frontend/thumb.ts +209 -232
  16. package/src/ir/alias.ts +24 -0
  17. package/src/ir/core.ts +70 -3
  18. package/src/ir/opcodes.ts +52 -7
  19. package/src/ir/parse.ts +7 -1
  20. package/src/ir/simplify.ts +1 -1
  21. package/src/l3/address.ts +2 -2
  22. package/src/l3/advance.ts +373 -0
  23. package/src/l3/argbase.ts +6 -6
  24. package/src/l3/argcopy.ts +269 -0
  25. package/src/l3/ast.ts +110 -22
  26. package/src/l3/basecse.ts +50 -30
  27. package/src/l3/coalesce.ts +118 -61
  28. package/src/l3/gates.ts +75 -1
  29. package/src/l3/hoist.ts +1 -1
  30. package/src/l3/homesplit.ts +13 -13
  31. package/src/l3/initfirst.ts +3 -3
  32. package/src/l3/inlinebase.ts +16 -16
  33. package/src/l3/mentions.ts +68 -5
  34. package/src/l3/mulfirst.ts +3 -3
  35. package/src/l3/nearbase.ts +4 -4
  36. package/src/l3/offmember.ts +5 -5
  37. package/src/l3/parkfirst.ts +6 -6
  38. package/src/l3/pollguard.ts +3 -3
  39. package/src/l3/ptrfield.ts +4 -4
  40. package/src/l3/regspell.ts +8 -8
  41. package/src/l3/reindex.ts +22 -17
  42. package/src/l3/scopebase.ts +32 -29
  43. package/src/l3/sinkinit.ts +7 -7
  44. package/src/l3/slotorder.ts +3 -3
  45. package/src/l3/storage.ts +1 -1
  46. package/src/l3/tailmerge.ts +2 -2
  47. package/src/l3/tailret.ts +70 -0
  48. package/src/l3/typing.ts +3 -3
  49. package/src/l3/unmerge.ts +483 -59
  50. package/src/l3/unreduce.ts +15 -14
  51. package/src/l3/volatileptr.ts +11 -11
  52. package/src/l3/volatileval.ts +11 -11
  53. package/src/l3/volstore.ts +16 -16
  54. package/src/l3/zerosub.ts +6 -6
  55. package/src/mangle.ts +49 -0
  56. package/src/pattern/engine.ts +132 -17
  57. package/src/pipeline.ts +39 -16
  58. package/src/proto.ts +2 -2
  59. package/src/raise/const.ts +203 -3
  60. package/src/raise/divpow2.ts +2 -2
  61. package/src/raise/extscale.ts +345 -0
  62. package/src/raise/globalshape.ts +32 -12
  63. package/src/raise/gvn.ts +2 -2
  64. package/src/raise/magicdiv.ts +2 -2
  65. package/src/raise/memberarrays.ts +4 -4
  66. package/src/raise/narrowlocal.ts +18 -2
  67. package/src/raise/paramwidth.ts +133 -3
  68. package/src/raise/pre-recovery.ts +100 -25
  69. package/src/raise/retsink.ts +389 -19
  70. package/src/raise/shortcircuit.ts +595 -34
  71. package/src/raise/structs.ts +4 -4
  72. package/src/raise/tailsink.ts +141 -0
  73. package/src/rank-declare.ts +21 -13
  74. package/src/{rank-axes.ts → rank-variations.ts} +319 -189
  75. package/src/rank.ts +1176 -805
  76. package/src/structure/analysis.ts +87 -90
  77. package/src/structure/bitfields.ts +130 -30
  78. package/src/structure/globalaccess.ts +30 -4
  79. package/src/structure/namecoalesce.ts +32 -13
  80. package/src/structure/retspell.ts +95 -0
  81. package/src/structure/structure.ts +1425 -201
  82. package/src/structure/switch-recover.ts +101 -8
  83. package/src/symbols.ts +127 -6
  84. package/src/target.ts +374 -44
  85. package/src/trace.ts +28 -19
  86. package/src/variation-definitions.ts +1590 -0
  87. package/src/variation-gates.ts +92 -0
  88. package/src/variation-tokens.ts +356 -0
@@ -23,7 +23,7 @@
23
23
  // base @ aload(index) -> array (variable index — untouched)
24
24
  //
25
25
  // This recovery is USUALLY BYTE-NEUTRAL — `->field_N` and `[idx]` mostly compile identically, so it
26
- // is a representation upgrade driven by access evidence rather than a scored lever. TWO
26
+ // is a representation upgrade driven by access evidence rather than a scored variation. TWO
27
27
  // MEASUREMENTS SAY "USUALLY" IS THE RIGHT WORD, and both were made on agbcc against a real target
28
28
  // object, each pair differing in ONE token:
29
29
  // • THE SPELLING. `synthetic:dmanest`'s reference compiles from `((struct Elem0 *)K)[a1].field_4`
@@ -34,9 +34,9 @@
34
34
  // `void *` rather than `s32` changes agbcc's alias set and lets a loop-invariant load leave the
35
35
  // loop: `synthetic:dmaptrsrc` matches with the pointer declaration and diffs by 35 without it
36
36
  // (its own fan: `/vol-store/unreduce/ptr-field` 0, `/vol-store/unreduce` 35).
37
- // That is what `l3/ptrfield.ts` offers as a differ-ranked lever.
37
+ // That is what `l3/ptrfield.ts` offers as a differ-ranked variation.
38
38
  // So the neutrality claim is CONDITIONAL, and nothing here says on what. Until it does, read it as
39
- // "no candidate is enumerated for this axis", not as "the differ could not referee one" — the
39
+ // "no candidate is enumerated for this question", not as "the differ could not referee one" — the
40
40
  // second reading is the one both measurements above falsify. GAPS between accessed
41
41
  // offsets (unaccessed leading/interior fields) are filled with `u8[N]` PAD fields so the declared
42
42
  // struct reproduces the observed offsets byte-for-byte and is self-describing (the same
@@ -64,7 +64,7 @@ interface Access {
64
64
  // is a DIFFERENT operation: this pass is ALIGNMENT-AWARE (no explicit pad when C's own inter-field
65
65
  // padding already lands the field) and carries NO trailing pad / struct `size` (a recovered struct
66
66
  // here is only ever a `struct S *` pointee accessed by named field — never an array element or a
67
- // by-value param, so sizeof is never taken). If the two are ever unified, PARAMETERIZE those axes
67
+ // by-value param, so sizeof is never taken). If the two are ever unified, PARAMETERIZE those dimensions
68
68
  // — a naive merge would break the natural-alignment golden or silently mislay a struct that later
69
69
  // becomes an element / ABI value.
70
70
  const sizeAlign = (width: number): number => width;
@@ -0,0 +1,141 @@
1
+ // asmlift — sink a shared STORE tail back into the paths that branch to it.
2
+ //
3
+ // agbcc's gcse.c PRE finds a trailing store's base partially redundant, INSERTS it at the end of
4
+ // every predecessor of the join, and the arms then cross-jump into one `store; ret`:
5
+ //
6
+ // if (c) { for (…) …; } else { if (x) { gQ.cur = fnA; return; } }
7
+ // gQ.cur = fnB;
8
+ //
9
+ // lifts as ONE tail block `^t(v): store gQ.cur, v; ret` that the `fnA` arm and the join the two
10
+ // sides reach both branch to. The structurer can spell that tail once, after the `if`, only if the
11
+ // `fnA` path returns before it — so this duplicates the tail into every path that branches to it.
12
+ // WHICH copy is the tail the source wrote once is not decided here: it is the `ret` reachable from
13
+ // both successors of the `if`, which `structure()`'s `followEarlyReturns` finds and spells after
14
+ // the `if`, with every other copy an early `return;` (`synthetic:gcsetail`). Under `-fno-gcse` the
15
+ // tail/duplicated pairs compile byte-identical, and there is no loop gate: a loop before the join
16
+ // is a sample, not a law.
17
+ //
18
+ // A block that supplies the tail's arguments is a SOURCE: a PURE FORWARDER (`^f(v): br ^t(v)`,
19
+ // reached only by `br`) is seen through to its own predecessors, so the arms that branch into the
20
+ // tail through a shared base reload are sources like any other (`synthetic:gcsefwd`).
21
+ //
22
+ // A LIFT VARIATION, NEVER A DEFAULT: `synthetic:gcsepre` and `synthetic:gcsepredup` compile to
23
+ // different objects (an r4/r5 swap) and lift to byte-identical IR, the first written with the
24
+ // shared tail and the second with the default duplicated into each arm — no IR rule tells them
25
+ // apart, so rank.ts enumerates this beside the unsunk lift and the differ referees.
26
+ //
27
+ // SOUND BY CONSTRUCTION: tail duplication. Each copy runs on exactly the paths that ran the tail,
28
+ // immediately before the same `ret`. A value the tail reads is one of two things. It is a block
29
+ // parameter of the tail OR OF A FORWARDER on the way, and the copy takes the argument that path's
30
+ // edge into that block carried. Or it is defined elsewhere, dominates the tail, and so dominates
31
+ // every source of it. The forwarder's own parameter is not a corner: a tail whose only predecessor
32
+ // is a forwarder loses its parameter to raise's `simplifyTrivialPhis` and reads the forwarder's
33
+ // directly — a jump pad `.L4: b .L6` in front of the store does exactly that — and the forwarder is
34
+ // swept once every source holds its copy. A copy replaces a `br`, the source's only edge. A
35
+ // CONDITIONAL edge cannot carry one — splicing over it would drop the branch's other successor — so
36
+ // the tail stays for the sources that reach it that way; that restriction is the rewrite's own
37
+ // precondition, not a gate.
38
+ //
39
+ // NO GATE: which copy stays shared is the follow's question, and whether a sunk function is worth
40
+ // a candidate is rank.ts's, asked with the follow's own predicate (`hasDivergentSharedRet`) rather
41
+ // than a copy of it here.
42
+ import {
43
+ type Block,
44
+ type Fn,
45
+ type Value,
46
+ fallThroughOf,
47
+ mkOp,
48
+ predecessors,
49
+ reachableBlocks,
50
+ terminator,
51
+ } from '../ir/core';
52
+ import { simplifyTrivialPhis } from '../ir/simplify';
53
+
54
+ /** One edge that supplies the tail its arguments. `resolve` sends a value the tail reads to the
55
+ * value it has at the end of `from`: a parameter of the tail or of any forwarder between becomes
56
+ * the argument this path's edge into that block carried, and anything else is left alone. */
57
+ interface Source {
58
+ readonly from: Block;
59
+ readonly resolve: (v: Value) => Value;
60
+ }
61
+
62
+ /** A block whose ops are one or more `store`s and then a void `ret`. */
63
+ function isStoreTail(b: Block): boolean {
64
+ const t = b.ops[b.ops.length - 1];
65
+ return (
66
+ b.ops.length >= 2 &&
67
+ t.opcode === 'ret' &&
68
+ t.operands.length === 0 &&
69
+ b.ops.slice(0, -1).every((o) => o.opcode === 'store')
70
+ );
71
+ }
72
+
73
+ /** Copy every store tail that two or more sources reach into each source that reaches it by a
74
+ * `br`; returns whether anything changed. */
75
+ export function sinkStoreTails(fn: Fn): boolean {
76
+ let changed = false;
77
+ for (const tail of [...fn.blocks]) {
78
+ if (tail === fn.blocks[0] || !fn.blocks.includes(tail) || !isStoreTail(tail)) {
79
+ continue;
80
+ }
81
+ const preds = predecessors(fn);
82
+ // The sources, seen through pure forwarders. `resolve` sends a value the tail reads to the value
83
+ // it has on entry to the block being walked; each edge out of a predecessor composes one more
84
+ // step onto it — `to`'s own parameters, not only the tail's, because the tail may read a
85
+ // forwarder's parameter directly.
86
+ const forwarders = new Set<Block>();
87
+ const sources: Source[] = [];
88
+ const conditional: Block[] = [];
89
+ const walk = (to: Block, resolve: (v: Value) => Value) => {
90
+ for (const p of preds.get(to) ?? []) {
91
+ const t = terminator(p)!;
92
+ if (t.opcode !== 'br') {
93
+ conditional.push(p);
94
+ continue;
95
+ }
96
+ const edge = t.successors[0].args;
97
+ const through = (v: Value): Value => {
98
+ const w = resolve(v);
99
+ const i = to.params.indexOf(w);
100
+ return i >= 0 ? edge[i] : w;
101
+ };
102
+ const isForwarder =
103
+ p !== fn.blocks[0] &&
104
+ p.ops.length === 1 &&
105
+ !forwarders.has(p) &&
106
+ (preds.get(p) ?? []).every((q) => terminator(q)?.opcode === 'br');
107
+ if (isForwarder) {
108
+ forwarders.add(p);
109
+ walk(p, through);
110
+ } else {
111
+ sources.push({ from: p, resolve: through });
112
+ }
113
+ }
114
+ };
115
+ walk(tail, (v) => v);
116
+ if (sources.length === 0 || sources.length + conditional.length < 2) {
117
+ continue;
118
+ }
119
+ const body = tail.ops.slice(0, -1);
120
+ for (const src of sources) {
121
+ const copies = body.map((o) => mkOp('store', { operands: o.operands.map(src.resolve), attrs: { ...o.attrs } }));
122
+ // Carry the replaced edge's fall-through fact onto the duplicated `ret`, as `raise/retsink.ts`
123
+ // does. Only for a source that branches straight to the tail: seen THROUGH a forwarder the path
124
+ // is several edges and no one of them answers on its own, so the copy is left unmarked and
125
+ // read as a branch — the side that can never delete a return the object needs.
126
+ const t = src.from.ops[src.from.ops.length - 1];
127
+ const attrs = t.successors[0]?.block === tail ? fallThroughOf(t) : {};
128
+ src.from.ops.splice(src.from.ops.length - 1, 1, ...copies, mkOp('ret', { attrs }));
129
+ }
130
+ // By REACHABILITY, not predecessor count: the tail (unless a conditional edge keeps it) and
131
+ // every forwarder on the way are left unreachable, however long the chain, and a forwarder's
132
+ // in-edge from another orphan still counts as a predecessor.
133
+ const live = reachableBlocks(fn);
134
+ fn.blocks = fn.blocks.filter((b) => live.has(b));
135
+ changed = true;
136
+ }
137
+ if (changed) {
138
+ simplifyTrivialPhis(fn);
139
+ }
140
+ return changed;
141
+ }
@@ -5,9 +5,9 @@
5
5
  // (`bareGlobalAccessFacts`), which names exist at all (`bareGlobalSymbols`), and which of them a
6
6
  // declaration must REFUSE to claim (`makeRefCollector`, via `RefusedDeclarationReason`).
7
7
  //
8
- // It knows nothing about axes, levers or ranking: the enumeration driver hands it a dictionary and
8
+ // It knows nothing about variations or ranking: the enumeration driver hands it a dictionary and
9
9
  // asks each emitted tree for its references. A SIBLING MODULE, never a `rank/` directory — see the
10
- // same note on rank-axes.ts.
10
+ // same note on rank-variations.ts.
11
11
  import { type Fn, type Value, defOpMap } from './ir/core';
12
12
  import type { SFn } from './l3/ast';
13
13
  import { type SymbolRef, collectSymbolRefs } from './l3/symbol-refs';
@@ -118,23 +118,21 @@ const DECL_RESERVED = new Set<string>([
118
118
 
119
119
  /** The emitter's own NAME GRAMMAR for storage it invents: parameters `a0, a1, …` (structure.ts
120
120
  * names them positionally, so no rename can move one) and coalesced/temp locals `v0…`/`t0…`
121
- * (structure.ts's `localNames` accepts exactly `/^[vt]\d+$/`). A pool or map symbol with one of
122
- * these names cannot be declared beside the C that spells it — see the refusal in `refsOf`, which
123
- * is the one that kills the spelling rather than the line.
121
+ * (structure.ts's `localNames` accepts exactly `/^[vt]\d+$/`). A global the target names with one
122
+ * of these names cannot be declared beside the C that spells it — see the refusal in `refsOf`,
123
+ * which is the one that kills the spelling rather than the line.
124
124
  *
125
125
  * Checked as a grammar IN ADDITION to the tree's own bound names, because the collision that
126
126
  * matters is the one the tree cannot show: `localNames` DROPS a local whose name a written
127
127
  * global already claims, so where the global is stored `tree.locals` is silent about it. The
128
- * price is refusing a real global that happens to be named `v3` in a function that never mints
129
- * one measured at zero: over the benchmark corpus, in each row's own symbol world, no candidate
130
- * references such a name, and no vendored symbol map on that sweep's checkouts contained one. The
131
- * map's own name total is deliberately not quoted — it is a property of the checkouts the sweep
132
- * ran over rather than of this repo, so nothing here can re-derive it. */
128
+ * price is refusing a global the target names `v3` in a function that never mints one. A map
129
+ * global the target does NOT name pays nothing: Pikmin's map holds `v0` and `v1`, the only
130
+ * grammar names in any vendored map, and no Pikmin row references either. */
133
131
  const EMITTER_NAME = /^[avt]\d+$/;
134
132
 
135
133
  /** Why a name the candidate's tree references got NO declaration. Reported rather than silently
136
134
  * applied, because an undeclared name and a REFUSED one produce the same `'x' undeclared` from
137
- * the compiler and only the second one is asmlift's own decision. Same argument as `onLeverError`
135
+ * the compiler and only the second one is asmlift's own decision. Same argument as `onEnumerationError`
138
136
  * one screen down: a refusal nobody can see is indistinguishable from a capability that was
139
137
  * never there.
140
138
  *
@@ -201,10 +199,13 @@ export function makeRefCollector(ctx: {
201
199
  accessFacts: ReadonlyMap<string, { width: number; signed: boolean }>;
202
200
  /** the project map alone — a name it does NOT know makes the ref a `synthesized` hypothesis */
203
201
  mapSymbols: ReadonlyMap<string, SymbolInfo> | undefined;
202
+ /** the globals the TARGET names — the pool/reloc names and the shapes the asm evidences, before
203
+ * the map is unioned in */
204
+ targetNames: ReadonlySet<string>;
204
205
  /** reports a refusal at most once per (name, reason); the caller owns the dedup */
205
206
  refuse: (name: string, reason: RefusedDeclarationReason) => void;
206
207
  }): (tree: SFn) => { symbolRefs?: SymbolRef[] } {
207
- const { declSymbols, accessFacts, mapSymbols, refuse } = ctx;
208
+ const { declSymbols, accessFacts, mapSymbols, targetNames, refuse } = ctx;
208
209
  return (tree: SFn): { symbolRefs?: SymbolRef[] } => {
209
210
  // The names THIS tree binds. Computed per tree because the emitter mints local names per
210
211
  // spelling — but the test below is NOT `bound` alone, and the difference is a wrong answer.
@@ -223,9 +224,16 @@ export function makeRefCollector(ctx: {
223
224
  // `tree.locals`: every use of the emitter's local binds the extern instead, and the
224
225
  // loop pointer it was holding becomes a store to that global once per iteration.
225
226
  // Both compile, both are wrong, and a compiling wrong answer is the one outcome this
226
- // project trades nothing for — so the spelling dies here and `fanOut`'s catch reports it.
227
+ // project trades nothing for — so the spelling dies here and `respellTree`'s catch reports it.
227
228
  // If every spelling of every tree dies, the row declines LOUDLY naming the collision.
228
229
  if (bound.has(r.name) || EMITTER_NAME.test(r.name)) {
230
+ // …but only a global the TARGET names can collide. A name that only the MAP supplies is a
231
+ // project global the function never touches, and the tree's `v0` is its own local: Pikmin
232
+ // holds `.sdata` statics named `v0` and `v1`, and refusing them killed every spelling of
233
+ // every function whose emitted C mints those locals.
234
+ if (!targetNames.has(r.name)) {
235
+ return [];
236
+ }
229
237
  refuse(r.name, 'emitter-name');
230
238
  throw new Error(
231
239
  `cannot spell '${tree.name}': the target names a global '${r.name}', which is a name the ` +