@asmlift/core 0.4.0 → 0.6.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 (87) hide show
  1. package/README.md +22 -16
  2. package/package.json +1 -1
  3. package/src/backend/c.ts +1 -0
  4. package/src/backend/cfamily.ts +238 -164
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +341 -22
  8. package/src/declare.ts +41 -4
  9. package/src/frontend/mips.ts +24 -6
  10. package/src/frontend/opaque.ts +31 -18
  11. package/src/frontend/ppc.ts +54 -7
  12. package/src/frontend/ssa.ts +632 -13
  13. package/src/frontend/thumb.ts +2786 -286
  14. package/src/ir/alias.ts +129 -0
  15. package/src/ir/bits.ts +75 -0
  16. package/src/ir/core.ts +337 -2
  17. package/src/ir/opcodes.ts +156 -27
  18. package/src/ir/parse.ts +19 -2
  19. package/src/ir/print.ts +27 -2
  20. package/src/ir/simplify.ts +190 -3
  21. package/src/ir/struct-names.ts +42 -0
  22. package/src/ir/verify.ts +43 -49
  23. package/src/l3/address.ts +62 -0
  24. package/src/l3/argbase.ts +8 -2
  25. package/src/l3/ast.ts +464 -49
  26. package/src/l3/basecse.ts +709 -88
  27. package/src/l3/coalesce.ts +521 -66
  28. package/src/l3/dce.ts +54 -19
  29. package/src/l3/gates.ts +88 -0
  30. package/src/l3/hoist.ts +293 -14
  31. package/src/l3/homesplit.ts +285 -0
  32. package/src/l3/initfirst.ts +301 -0
  33. package/src/l3/inlinebase.ts +193 -0
  34. package/src/l3/mentions.ts +113 -0
  35. package/src/l3/mulfirst.ts +42 -0
  36. package/src/l3/nearbase.ts +152 -0
  37. package/src/l3/offmember.ts +371 -0
  38. package/src/l3/parkfirst.ts +96 -0
  39. package/src/l3/pollguard.ts +154 -0
  40. package/src/l3/ptrfield.ts +227 -0
  41. package/src/l3/regspell.ts +110 -85
  42. package/src/l3/reindex.ts +715 -78
  43. package/src/l3/scopebase.ts +649 -219
  44. package/src/l3/sinkinit.ts +40 -0
  45. package/src/l3/slotorder.ts +123 -0
  46. package/src/l3/storage.ts +48 -0
  47. package/src/l3/symbol-refs.ts +41 -8
  48. package/src/l3/tailmerge.ts +23 -4
  49. package/src/l3/typing.ts +198 -9
  50. package/src/l3/unmerge.ts +263 -0
  51. package/src/l3/unreduce.ts +971 -0
  52. package/src/l3/volatileptr.ts +207 -0
  53. package/src/l3/volatileval.ts +130 -0
  54. package/src/l3/volstore.ts +229 -0
  55. package/src/l3/zerosub.ts +62 -0
  56. package/src/pattern/engine.ts +236 -13
  57. package/src/pipeline.ts +206 -49
  58. package/src/proto.ts +112 -14
  59. package/src/raise/arrays.ts +6 -1
  60. package/src/raise/divpow2.ts +4 -3
  61. package/src/raise/globalshape.ts +1038 -0
  62. package/src/raise/gvn.ts +44 -19
  63. package/src/raise/latch.ts +126 -0
  64. package/src/raise/memberarrays.ts +594 -0
  65. package/src/raise/narrow.ts +124 -0
  66. package/src/raise/narrowlocal.ts +556 -0
  67. package/src/raise/paramwidth.ts +179 -0
  68. package/src/raise/pre-recovery.ts +101 -16
  69. package/src/raise/recover.ts +56 -23
  70. package/src/raise/retsink.ts +215 -14
  71. package/src/raise/shortcircuit.ts +477 -79
  72. package/src/raise/struct-arrays.ts +21 -3
  73. package/src/raise/structs.ts +61 -3
  74. package/src/rank-axes.ts +630 -0
  75. package/src/rank-declare.ts +256 -0
  76. package/src/rank.ts +1726 -251
  77. package/src/structure/analysis.ts +1516 -220
  78. package/src/structure/bitfields.ts +332 -0
  79. package/src/structure/globalaccess.ts +274 -0
  80. package/src/structure/hazards.ts +411 -20
  81. package/src/structure/loops.ts +2 -49
  82. package/src/structure/namecoalesce.ts +435 -0
  83. package/src/structure/structure.ts +2850 -533
  84. package/src/structure/switch-recover.ts +688 -147
  85. package/src/symbols.ts +62 -1
  86. package/src/target.ts +367 -24
  87. package/src/trace.ts +111 -32
@@ -0,0 +1,256 @@
1
+ // asmlift — the DECLARATION half of candidate enumeration, split out of rank.ts. A candidate's
2
+ // source names globals its own asm named, and outside the project's headers those names need
3
+ // declarations or every candidate fails to compile. This module answers three questions about
4
+ // them and nothing else: what the tree's own IR says about a bare global's ACCESS
5
+ // (`bareGlobalAccessFacts`), which names exist at all (`bareGlobalSymbols`), and which of them a
6
+ // declaration must REFUSE to claim (`makeRefCollector`, via `RefusedDeclarationReason`).
7
+ //
8
+ // It knows nothing about axes, levers or ranking: the enumeration driver hands it a dictionary and
9
+ // asks each emitted tree for its references. A SIBLING MODULE, never a `rank/` directory — see the
10
+ // same note on rank-axes.ts.
11
+ import { type Fn, type Value, defOpMap } from './ir/core';
12
+ 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';
16
+
17
+ /** Bare-global ACCESS FACTS for name-only map symbols — the width/signedness authority the
18
+ * declaration synthesis (declare.ts) uses when the map has no shape. The map knows only the
19
+ * NAME (symtab-only projects: marioparty3); the candidate's own IR knows exactly how the cell
20
+ * is accessed, and the bare `gSym = v` / `x = gSym` spelling compiles to those bytes only
21
+ * under a decl of that exact width (`extern u16 g;` is `sh` where a guessed u32 is `sw`).
22
+ * Mirrors structure()'s scalar-global rule: a fact is recorded only for a symbol accessed
23
+ * EXCLUSIVELY at offset 0 with ONE width and ONE load signedness — anything else (interior
24
+ * offsets, address arithmetic, width or sign conflicts) records nothing, because those
25
+ * spellings go through `&gSym` casts where every object decl is address-identical. */
26
+ export function bareGlobalAccessFacts(fn: Fn): Map<string, { width: number; signed: boolean }> {
27
+ const defs = defOpMap(fn);
28
+ const symOf = (v: Value): string | null => {
29
+ const d = defs.get(v);
30
+ return d?.opcode === 'gaddr' && d.attrs.code !== true ? (d.attrs.sym as string) : null;
31
+ };
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)!;
34
+ for (const b of fn.blocks) {
35
+ for (const op of b.ops) {
36
+ if (op.opcode === 'load' || op.opcode === 'store') {
37
+ const s = symOf(op.operands[0]);
38
+ if (s) {
39
+ const a = get(s);
40
+ if ((op.attrs.off as number) !== 0) {
41
+ a.interior = true;
42
+ } else {
43
+ a.widths.add(op.attrs.width as number);
44
+ if (op.opcode === 'load') {
45
+ a.signs.add(((op.attrs.signed as boolean) ?? false) && (op.attrs.width as number) < 4);
46
+ }
47
+ }
48
+ }
49
+ } else if (op.opcode === 'aload' || op.opcode === 'astore') {
50
+ const s = symOf(op.operands[0]);
51
+ if (s) {
52
+ get(s).interior = true;
53
+ }
54
+ } else {
55
+ // any other use of the address (arithmetic, a call arg, a comparison) is interior/escape
56
+ for (const o of op.operands) {
57
+ const s = symOf(o);
58
+ if (s) {
59
+ get(s).interior = true;
60
+ }
61
+ }
62
+ }
63
+ }
64
+ }
65
+ const out = new Map<string, { width: number; signed: boolean }>();
66
+ 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) });
69
+ }
70
+ }
71
+ return out;
72
+ }
73
+
74
+ /** Names a declaration must never claim, because `extern u32 <name>;` is not a declaration of
75
+ * `<name>` at all for them. Four groups, and only the first two are guessed:
76
+ *
77
+ * 1. the prelude's own typedef names, derived FROM `C_TYPEDEFS` rather than re-listed (a
78
+ * prelude that grows a name grows this set) — `extern u32 u16;` redefines the type the
79
+ * declaration is written in;
80
+ * 2. the C89 keywords;
81
+ * 3. the gnu89 keywords gcc-2.9 REJECTS in this position, and the two library objects it
82
+ * refuses to have redeclared. MEASURED against the pinned agbcc — 72 plausible pool names
83
+ * compiled as `extern u32 <name>;` at file scope, 18 exited non-zero: `syntax error before
84
+ * 'asm'` for the keyword class, ``'exit' redeclared as different kind of symbol`` for the
85
+ * two built-ins;
86
+ * 4. the gnu89 declaration SPECIFIERS that PARSE and thereby declare nothing — `inline`,
87
+ * `__const`, `__volatile__`, … are `warning: useless keyword or type name in empty
88
+ * declaration`, exit 0, and the name is still undeclared. Emitting the line would be a
89
+ * declaration that is not one.
90
+ *
91
+ * WHAT REFUSING BUYS is less than a plain `'<name>' undeclared`, and the difference is the
92
+ * reason the refusal is REPORTED rather than trusted to the compiler. A hard error in the block
93
+ * kills that candidate's whole TU (its own — every candidate compiles alone) for a name it
94
+ * merely mentioned, so refusing is right. But for a KEYWORD the body spells the same token
95
+ * anyway and the candidate still fails: the refusal only moves the diagnostic. And for the two
96
+ * BUILT-INS nothing fails — agbcc reads `&exit` as the address of its own builtin, exit 0 with
97
+ * `warning: built-in function 'exit' used without declaration`, where the declaration would have
98
+ * been exit 1. There the refusal trades a candidate that cannot build for one that builds
99
+ * against the wrong object, which is the better half of a bad choice only because a target
100
+ * naming a global `exit` has no honest spelling either way. */
101
+ const DECL_RESERVED = new Set<string>([
102
+ ...[...C_TYPEDEFS.matchAll(/(\w+)\s*;/g)].map((m) => m[1]),
103
+ ...(
104
+ 'auto break case char const continue default do double else enum extern float for goto if int long ' +
105
+ 'register return short signed sizeof static struct switch typedef union unsigned void volatile while'
106
+ ).split(' '),
107
+ // group 3 — measured hard errors (agbcc, `extern u32 <name>;` at file scope)
108
+ ...(
109
+ 'asm __asm __asm__ typeof __typeof __typeof__ __attribute __attribute__ __extension__ __label__ ' +
110
+ '__alignof __alignof__ __real__ __imag__ __func__ __FUNCTION__ exit abort'
111
+ ).split(' '),
112
+ // group 4 — measured "useless keyword ... in empty declaration": parses, declares nothing
113
+ ...(
114
+ 'inline __inline __inline__ __const __const__ __signed __signed__ __volatile __volatile__ ' +
115
+ '__restrict __restrict__ __complex__'
116
+ ).split(' '),
117
+ ]);
118
+
119
+ /** The emitter's own NAME GRAMMAR for storage it invents: parameters `a0, a1, …` (structure.ts
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.
124
+ *
125
+ * Checked as a grammar IN ADDITION to the tree's own bound names, because the collision that
126
+ * matters is the one the tree cannot show: `localNames` DROPS a local whose name a written
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. */
133
+ const EMITTER_NAME = /^[avt]\d+$/;
134
+
135
+ /** Why a name the candidate's tree references got NO declaration. Reported rather than silently
136
+ * 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`
138
+ * one screen down: a refusal nobody can see is indistinguishable from a capability that was
139
+ * never there.
140
+ *
141
+ * ALL FIVE ARE DECIDED AT ONE POINT (`refsOf`), over the names the collector actually returns
142
+ * and AFTER the map/pool union — so the report and the rendered block are one list read two
143
+ * ways. A test applied where a name ENTERS can be undone by the other half of the union, and
144
+ * then the report contradicts the block beside it. */
145
+ export type RefusedDeclarationReason =
146
+ | 'not-an-identifier' // a relocation name like `$L1` / `.rodata.str1`
147
+ | 'reserved' // a name `extern u32 <name>;` cannot declare (DECL_RESERVED)
148
+ | 'call-target' // the name is some call's target: `void F(void);` hard-errors over args
149
+ | 'self-name' // the function's own name — its definition already declares it
150
+ | 'emitter-name'; // a name the emitted C uses for its OWN locals and parameters
151
+
152
+ /** The globals a candidate names because its own asm named them, as name-only `SymbolInfo`s —
153
+ * half of the declaration-synthesis dictionary (the symbol map, where there is one, is the other).
154
+ *
155
+ * asmlift does not need a map to EMIT a symbol name: the Thumb frontend reads it out of the
156
+ * `.s` file's own literal pool (`.word gBgTilemapBufs` → `gaddr`, thumb.ts's pool grammar) and
157
+ * the MIPS frontend out of an object relocation, and structure() spells such a `gaddr` as
158
+ * `&gSym`. So the invariant "a candidate's source only names symbols the map knows" is FALSE,
159
+ * and a consumer that compiles candidates OUTSIDE the project's own headers (the playground)
160
+ * needs these declarations or every candidate fails with "`gSym' undeclared".
161
+ *
162
+ * `kind: 'data'` unconditionally: `code: true` is set only where a symbol MAP said so
163
+ * (frontend/thumb.ts), so map-less the IR cannot tell a function pointer from a data address —
164
+ * and it does not need to. structure() spells a `code`-less `gaddr` as `&Name`, and `&Name`
165
+ * under `extern u32 Name;` is the relocated address whatever Name really is.
166
+ *
167
+ * NOTHING IS REFUSED HERE, deliberately: a name this walk drops is a name the union above it
168
+ * could put straight back. Every refusal is decided once, over the collector's output, in
169
+ * `refsOf` (see `RefusedDeclarationReason`).
170
+ *
171
+ * A declaration built from this half is a HYPOTHESIS, and where `bareGlobalAccessFacts` gives it
172
+ * a width that width came out of the asm the candidate is scored against. The marker is
173
+ * `SymbolRef.synthesized`; the argument, and its price against the vendored maps, is declare.ts's
174
+ * module note. */
175
+ export function bareGlobalSymbols(fn: Fn): Map<string, SymbolInfo> {
176
+ const out = new Map<string, SymbolInfo>();
177
+ for (const b of fn.blocks) {
178
+ for (const op of b.ops) {
179
+ if (op.opcode === 'gaddr' && typeof op.attrs.sym === 'string') {
180
+ out.set(op.attrs.sym, { name: op.attrs.sym, kind: 'data' });
181
+ }
182
+ }
183
+ }
184
+ return out;
185
+ }
186
+
187
+ /** The reference collector one enumeration uses, over ONE dictionary. A factory rather than a
188
+ * free function because all four of its inputs are per-enumeration constants that must not vary
189
+ * per tree — naming them here is what keeps a caller from passing a different dictionary to two
190
+ * spellings of the same row.
191
+ *
192
+ * ALL FIVE REFUSALS ARE DECIDED AT THIS ONE POINT, over the names the collector actually returns
193
+ * and AFTER the map/pool union — so the report and the rendered declaration block are one list
194
+ * read two ways. A test applied where a name ENTERS can be undone by the other half of the union,
195
+ * and then the report contradicts the block beside it. */
196
+ export function makeRefCollector(ctx: {
197
+ /** the union the declarations are synthesized from: pool/reloc names, the shapes the asm
198
+ * evidences for them, and the project map — in increasing authority */
199
+ declSymbols: Map<string, SymbolInfo>;
200
+ /** the IR-derived width/signedness authority for a name-only symbol's declaration */
201
+ accessFacts: ReadonlyMap<string, { width: number; signed: boolean }>;
202
+ /** the project map alone — a name it does NOT know makes the ref a `synthesized` hypothesis */
203
+ mapSymbols: ReadonlyMap<string, SymbolInfo> | undefined;
204
+ /** reports a refusal at most once per (name, reason); the caller owns the dedup */
205
+ refuse: (name: string, reason: RefusedDeclarationReason) => void;
206
+ }): (tree: SFn) => { symbolRefs?: SymbolRef[] } {
207
+ const { declSymbols, accessFacts, mapSymbols, refuse } = ctx;
208
+ return (tree: SFn): { symbolRefs?: SymbolRef[] } => {
209
+ // The names THIS tree binds. Computed per tree because the emitter mints local names per
210
+ // spelling — but the test below is NOT `bound` alone, and the difference is a wrong answer.
211
+ const bound = new Set<string>([...tree.params.map((p) => p.name), ...tree.locals.map((l) => l.name)]);
212
+ const refs = collectSymbolRefs(tree.body, declSymbols, tree.name, refuse).flatMap((r) => {
213
+ // THE ONE REFUSAL THAT IS NOT A REFUSAL — a name the emitted C uses for its OWN storage
214
+ // kills the SPELLING, because no declaration makes that candidate right and no declaration
215
+ // makes it fail either. Two shapes, and the second is why the test is the emitter's whole
216
+ // NAME GRAMMAR rather than this tree's bound set:
217
+ // READ — the tree binds `v0` and also spells `&v0` for the pool global. The local
218
+ // shadows the extern, so the candidate takes a stack address where the asm takes a
219
+ // relocated one. Withholding the declaration does not stop it compiling: its SIBLING
220
+ // names still get theirs, and the TU builds.
221
+ // WRITE — structure.ts drops a local whose name a WRITTEN global already claims
222
+ // (`localNames`, filtered by `globalNames`), so the collision is INVISIBLE in
223
+ // `tree.locals`: every use of the emitter's local binds the extern instead, and the
224
+ // loop pointer it was holding becomes a store to that global once per iteration.
225
+ // 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
+ // If every spelling of every tree dies, the row declines LOUDLY naming the collision.
228
+ if (bound.has(r.name) || EMITTER_NAME.test(r.name)) {
229
+ refuse(r.name, 'emitter-name');
230
+ throw new Error(
231
+ `cannot spell '${tree.name}': the target names a global '${r.name}', which is a name the ` +
232
+ `emitted C uses for its own locals and parameters — no declaration can bind it`,
233
+ );
234
+ }
235
+ // Applied to the UNION, not to the pool half on its way in: a map can supply `$LC0` or
236
+ // `abort` as readily as a relocation can, and `extern u32 abort;` is the same hard error
237
+ // whichever half it came from.
238
+ if (!/^[A-Za-z_]\w*$/.test(r.name)) {
239
+ refuse(r.name, 'not-an-identifier');
240
+ return [];
241
+ }
242
+ if (DECL_RESERVED.has(r.name)) {
243
+ refuse(r.name, 'reserved');
244
+ return [];
245
+ }
246
+ // name-only symbols carry the IR-derived access facts — the width authority
247
+ // for their synthesized declaration (shaped symbols keep the map's truth)
248
+ const access = r.info.shape === undefined ? accessFacts.get(r.name) : undefined;
249
+ // A ref no MAP accounts for is a hypothesis read out of the target asm, and it is marked
250
+ // as one all the way to the consumer (SymbolRef.synthesized).
251
+ const synthesized = mapSymbols?.has(r.name) ? {} : { synthesized: true as const };
252
+ return [{ ...r, ...(access ? { access } : {}), ...synthesized }];
253
+ });
254
+ return refs.length ? { symbolRefs: refs } : {};
255
+ };
256
+ }