@asmlift/core 0.5.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 (86) 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 -167
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +194 -39
  8. package/src/declare.ts +41 -4
  9. package/src/frontend/mips.ts +11 -0
  10. package/src/frontend/ppc.ts +43 -7
  11. package/src/frontend/ssa.ts +404 -29
  12. package/src/frontend/thumb.ts +2176 -686
  13. package/src/ir/alias.ts +54 -0
  14. package/src/ir/bits.ts +75 -0
  15. package/src/ir/core.ts +337 -2
  16. package/src/ir/opcodes.ts +140 -21
  17. package/src/ir/parse.ts +19 -2
  18. package/src/ir/print.ts +27 -2
  19. package/src/ir/simplify.ts +190 -3
  20. package/src/ir/struct-names.ts +42 -0
  21. package/src/ir/verify.ts +43 -49
  22. package/src/l3/address.ts +62 -0
  23. package/src/l3/argbase.ts +2 -1
  24. package/src/l3/ast.ts +464 -57
  25. package/src/l3/basecse.ts +664 -76
  26. package/src/l3/coalesce.ts +429 -43
  27. package/src/l3/dce.ts +31 -9
  28. package/src/l3/gates.ts +21 -0
  29. package/src/l3/hoist.ts +293 -14
  30. package/src/l3/homesplit.ts +285 -0
  31. package/src/l3/initfirst.ts +301 -0
  32. package/src/l3/inlinebase.ts +193 -0
  33. package/src/l3/mentions.ts +113 -0
  34. package/src/l3/mulfirst.ts +42 -0
  35. package/src/l3/nearbase.ts +152 -0
  36. package/src/l3/offmember.ts +371 -0
  37. package/src/l3/parkfirst.ts +96 -0
  38. package/src/l3/pollguard.ts +154 -0
  39. package/src/l3/ptrfield.ts +227 -0
  40. package/src/l3/regspell.ts +110 -85
  41. package/src/l3/reindex.ts +715 -78
  42. package/src/l3/scopebase.ts +644 -218
  43. package/src/l3/sinkinit.ts +40 -0
  44. package/src/l3/slotorder.ts +123 -0
  45. package/src/l3/storage.ts +48 -0
  46. package/src/l3/symbol-refs.ts +41 -8
  47. package/src/l3/tailmerge.ts +15 -0
  48. package/src/l3/typing.ts +198 -9
  49. package/src/l3/unmerge.ts +263 -0
  50. package/src/l3/unreduce.ts +971 -0
  51. package/src/l3/volatileptr.ts +207 -0
  52. package/src/l3/volatileval.ts +130 -0
  53. package/src/l3/volstore.ts +229 -0
  54. package/src/l3/zerosub.ts +62 -0
  55. package/src/pattern/engine.ts +236 -13
  56. package/src/pipeline.ts +157 -56
  57. package/src/proto.ts +112 -14
  58. package/src/raise/arrays.ts +6 -1
  59. package/src/raise/divpow2.ts +2 -2
  60. package/src/raise/globalshape.ts +1038 -0
  61. package/src/raise/gvn.ts +33 -18
  62. package/src/raise/latch.ts +126 -0
  63. package/src/raise/memberarrays.ts +594 -0
  64. package/src/raise/narrow.ts +124 -0
  65. package/src/raise/narrowlocal.ts +556 -0
  66. package/src/raise/paramwidth.ts +179 -0
  67. package/src/raise/pre-recovery.ts +97 -14
  68. package/src/raise/recover.ts +56 -23
  69. package/src/raise/retsink.ts +210 -10
  70. package/src/raise/shortcircuit.ts +474 -74
  71. package/src/raise/struct-arrays.ts +19 -2
  72. package/src/raise/structs.ts +33 -3
  73. package/src/rank-axes.ts +630 -0
  74. package/src/rank-declare.ts +256 -0
  75. package/src/rank.ts +1723 -272
  76. package/src/structure/analysis.ts +1392 -141
  77. package/src/structure/bitfields.ts +332 -0
  78. package/src/structure/globalaccess.ts +274 -0
  79. package/src/structure/hazards.ts +411 -20
  80. package/src/structure/loops.ts +2 -49
  81. package/src/structure/namecoalesce.ts +435 -0
  82. package/src/structure/structure.ts +2678 -526
  83. package/src/structure/switch-recover.ts +616 -144
  84. package/src/symbols.ts +62 -1
  85. package/src/target.ts +367 -24
  86. package/src/trace.ts +111 -32
@@ -0,0 +1,263 @@
1
+ // L3 re-spelling lever: duplicate a join statement back into the arms the compiler merged it out
2
+ // of — the dual of l3/tailmerge.ts, and a LEVER where that one is unconditional.
3
+ //
4
+ // agbcc cross-jumps a store the source wrote in both arms into the join block, so the lifted CFG
5
+ // carries the address and the value on merge parameters and SSA destruction mints a temp per
6
+ // parameter:
7
+ //
8
+ // if (c) { … v16 = (u16 *)A1; v17 = B1; } else { … v16 = (u16 *)A2; v17 = B2; }
9
+ // *v16 = v17;
10
+ //
11
+ // The source that produced those bytes wrote `*(u16 *)A1 = B1;` inside one arm and
12
+ // `*(u16 *)A2 = B2;` inside the other — two whole statements, no temps. Substituting each arm's
13
+ // own definitions into the join statement and duplicating it back recovers that spelling.
14
+ //
15
+ // A LEVER, NOT A DEFAULT, and the reason is the tower's: the asm UNDERDETERMINES this. A source
16
+ // that really did write the temps and one store compiles to the same bytes, because the merge is
17
+ // the compiler's own. Both spellings are emitted and the differ referees.
18
+ //
19
+ // SOUND BY ITS DUAL'S ARGUMENT, READ BACKWARDS. The join runs on every path out of the `if`,
20
+ // immediately after that arm's own tail, with nothing between; a copy at the end of each arm runs
21
+ // it exactly once per path, in the same order relative to everything else. What tailmerge needs
22
+ // (the two statements are identical) this does not — the copies legitimately differ, because each
23
+ // arm substitutes its own definitions.
24
+ //
25
+ // REFUSES (leaving the merged spelling, which is what the structurer produced) when:
26
+ // - the `if` has an empty arm, or the statement after it is not an `assign`/`store`/`exprstmt`
27
+ // (tailmerge's scope, for its reason: control flow duplicated into an arm changes what the arm
28
+ // still reaches);
29
+ // - the join statement reads no local the arms define — there is no merge to undo;
30
+ // - a local it reads is neither a merge temp (assigned EXACTLY ONCE IN EACH ARM, read only by
31
+ // the join statement, never address-taken — the counts are function-wide, so a second reader
32
+ // anywhere refuses) nor untouched by both arms (a name the arms DO write and this cannot
33
+ // substitute would read a different value at the arm's end);
34
+ // - anything but an EFFECT-FREE assignment TO A DECLARED LOCAL stands between the first
35
+ // definition and the arm's end: the substituted values are evaluated where the copy lands, so
36
+ // an intervening store or call could answer a load inside one of them differently. All three
37
+ // halves of that are tested. The second is why the statement KIND is not enough — `q = Foo();`
38
+ // is an `assign` whose value is a call, and a load moved past it is a load answered after the
39
+ // call instead of before it. The third is why the assignment's TARGET is not either: an
40
+ // `assign` names a variable, and structure.ts spells a write to a scalar GLOBAL as one, so
41
+ // `gBlendValue = v;` is an `assign` with an effect-free value that writes MEMORY. Not a
42
+ // corner of the corpus. Measured over a 957-row artifact (#140): 22 winning sources emit a
43
+ // statement-level assignment to a name they declare nowhere — 71 occurrences, 7 of them in
44
+ // `kleod:ProcessInputAndUpdateEntities` alone.
45
+ // RE-DERIVE THIS RATHER THAN QUOTING IT: one pass over the artifact does it — collect each
46
+ // winning source's declared locals and parameters, then count its statement-level `name = `
47
+ // lines whose name is not among them. `exprHasEffect` answers "a call, or a marker" and cannot
48
+ // see one, the same way it could not see a qualifier;
49
+ // - an intervening assignment writes a name one of those values reads — same reason, one level
50
+ // more precise;
51
+ // - a definition's value reads another of the merge names (the substitutions would need an order
52
+ // between them that the join statement does not fix);
53
+ // - a definition's value carries an EFFECT — a call or a gap marker. One statement's operands
54
+ // have no evaluation order in C, so folding two effectful expressions into it would let the
55
+ // backend choose an order the asm did not;
56
+ // - a definition's value performs a VOLATILE access. `exprHasEffect` above answers "a call, or a
57
+ // marker" and says nothing about a qualifier, so it is not the test for this: the refusal is
58
+ // asked of the qualifier's own model (`exprReadsVolatile`), which knows all three spellings —
59
+ // the cast, the pointee-volatile pointer local, and the volatile local object.
60
+ //
61
+ // AND THE SCOPE OF THE VOLATILE ONE IS WHAT MOVES, which is exactly one thing. A kept statement
62
+ // holds its position, and the join runs where it already ran (immediately after that arm), so the
63
+ // only access whose point in the sequence changes is a DEFINITION's value — moved down to the
64
+ // arm's end, past every kept statement. A plain read may make that trip, because the kept
65
+ // statements from the first definition on are effect-free assignments to DECLARED LOCALS and so
66
+ // none of them writes memory that could answer it differently — which is what the three gates
67
+ // above establish, the local-target one included. An observable read may not make the trip: a
68
+ // volatile access is one the source pinned so it would not be duplicated or moved, and its ORDER
69
+ // against the other device accesses beside it is observable — which is why THAT gate is stated on
70
+ // the moved value and needs no clause for the statements that stay put.
71
+ //
72
+ // WHAT THE LOCAL-TARGET GATE DOES NOT CLOSE, stated rather than implied: ALIASING. A moved value
73
+ // reading `*p` and a kept assignment to an address-taken local can name the same object under two
74
+ // spellings, and the name-keyed refusal below (an intervening assignment writes a name one of
75
+ // those values READS) cannot see it. That is one question further out than this pass models —
76
+ // every other lever here defers it to the same name-keyed model — and the sweep behind this note
77
+ // found no inhabitant: 0 arms this pass ACCEPTS hold a kept assignment to an address-taken local
78
+ // after the first definition, the same sweep that found 0 holding one to a global. That sweep's
79
+ // population was the agbcc rows whose BASE TREE the rig could build, which is not the corpus's
80
+ // agbcc row count, so re-run it before quoting a count off it.
81
+ import {
82
+ type Expr,
83
+ type SFn,
84
+ type Stmt,
85
+ exprHasEffect,
86
+ exprReadsVolatile,
87
+ mapExprChildren,
88
+ mapStmtExprs,
89
+ stmtChildren,
90
+ walkExprs,
91
+ } from './ast';
92
+ import { localMentions, readsOf } from './mentions';
93
+
94
+ /** every statement under `body`, itself included */
95
+ function* walkStmts(body: Stmt[]): Generator<Stmt> {
96
+ for (const s of body) {
97
+ yield s;
98
+ yield* walkStmts(stmtChildren(s));
99
+ }
100
+ }
101
+
102
+ /** The statements this pass moves — see the scope refusal above. */
103
+ type Joinable = Extract<Stmt, { k: 'assign' } | { k: 'store' } | { k: 'exprstmt' }>;
104
+ const isJoinable = (s: Stmt): s is Joinable => s.k === 'assign' || s.k === 'store' || s.k === 'exprstmt';
105
+
106
+ /** The locals `s` READS. An `assign`'s target is a write and `stmtExprs` does not carry it, which
107
+ * is what makes this the set of names substitution has to supply. */
108
+ function namesRead(s: Stmt, locals: ReadonlySet<string>): Set<string> {
109
+ const out = new Set<string>();
110
+ for (const e of walkExprs([s])) {
111
+ if (e.k === 'var' && locals.has(e.name)) {
112
+ out.add(e.name);
113
+ }
114
+ }
115
+ return out;
116
+ }
117
+
118
+ /** Every local `e` reads. */
119
+ function readsIn(e: Expr): Set<string> {
120
+ const out = new Set<string>();
121
+ for (const x of walkExprs([{ k: 'exprstmt', value: e }])) {
122
+ if (x.k === 'var') {
123
+ out.add(x.name);
124
+ }
125
+ }
126
+ return out;
127
+ }
128
+
129
+ /** The arm's definitions of `names` and the statements that survive beside them, or null when the
130
+ * substituted copy would not evaluate to the same values at the arm's end. */
131
+ function armDefs(
132
+ arm: Stmt[],
133
+ names: ReadonlySet<string>,
134
+ declared: ReadonlySet<string>,
135
+ ): { defs: Map<string, Expr>; keep: Stmt[] } | null {
136
+ const at = new Map<string, number>();
137
+ arm.forEach((s, i) => {
138
+ if (s.k === 'assign' && names.has(s.name)) {
139
+ at.set(s.name, i);
140
+ }
141
+ });
142
+ if (at.size !== names.size) {
143
+ return null;
144
+ }
145
+ const first = Math.min(...at.values());
146
+ // From the first definition on, nothing but EFFECT-FREE assignments TO A DECLARED LOCAL: a store
147
+ // or a call there would run BEFORE a value this moves to the arm's end, and could answer a load
148
+ // inside it differently. Neither the KIND nor the VALUE alone says that. Not the kind — an
149
+ // `assign` whose value is a call is an intervening call — so the effect test is applied to the
150
+ // kept statements too, not only to the definition values below. And not the value either: an
151
+ // `assign` names a VARIABLE, and structure.ts spells a write to a scalar global as one, so
152
+ // `gBlendValue = v;` passes both tests and writes memory. `declared` is the tree's own locals
153
+ // and params, so the target has to be an object no moved read can reach except by the name the
154
+ // refusal below already keys on.
155
+ if (arm.slice(first).some((s) => s.k !== 'assign' || !declared.has(s.name) || exprHasEffect(s.value))) {
156
+ return null;
157
+ }
158
+ const defs = new Map([...at].map(([n, i]) => [n, (arm[i] as Extract<Stmt, { k: 'assign' }>).value] as const));
159
+ const read = new Set([...defs.values()].flatMap((v) => [...readsIn(v)]));
160
+ const keep: Stmt[] = [];
161
+ for (const [i, s] of arm.entries()) {
162
+ if (i >= first && s.k === 'assign' && names.has(s.name)) {
163
+ continue; // the definition itself, consumed by the substitution
164
+ }
165
+ if (i > first && s.k === 'assign' && read.has(s.name)) {
166
+ return null; // it would change a value this moves past it
167
+ }
168
+ keep.push(s);
169
+ }
170
+ return { defs, keep };
171
+ }
172
+
173
+ /** `s` with every mention of a defined name replaced by that name's value. */
174
+ function substitute(s: Stmt, defs: ReadonlyMap<string, Expr>): Stmt {
175
+ const rec = (e: Expr): Expr => (e.k === 'var' ? (defs.get(e.name) ?? e) : mapExprChildren(e, rec));
176
+ return mapStmtExprs(s, rec);
177
+ }
178
+
179
+ /** The tree with every eligible join statement pushed back into its arms, or null when no site
180
+ * qualified — the lever declines rather than re-emitting the primary spelling. */
181
+ export function unmergeJoins(sfn: SFn): SFn | null {
182
+ const mentions = localMentions(sfn);
183
+ const localNames = new Set(sfn.locals.map((l) => l.name));
184
+ // Locals AND params — both name an automatic object, and an assignment to either is the write
185
+ // `armDefs` may keep. Separate from `localNames` above, which answers a different question
186
+ // (which of the join's reads a substitution could supply) and is deliberately locals-only.
187
+ const declaredNames = new Set([...localNames, ...sfn.params.map((p) => p.name)]);
188
+ const consumed = new Set<string>();
189
+
190
+ const unmergeAt = (iff: Extract<Stmt, { k: 'if' }>, join: Joinable): Stmt | null => {
191
+ if (iff.then.length === 0 || iff.else.length === 0) {
192
+ return null;
193
+ }
194
+ const read = namesRead(join, localNames);
195
+ const written = new Set(
196
+ [...iff.then, ...iff.else].flatMap((s) => [...walkStmts([s])].filter((x) => x.k === 'assign').map((x) => x.name)),
197
+ );
198
+ const merge = new Set<string>();
199
+ for (const n of read) {
200
+ const m = mentions.get(n);
201
+ if (m && m.assigns === 2 && readsOf(m) === 1 && m.addrTaken === 0) {
202
+ merge.add(n);
203
+ } else if (written.has(n)) {
204
+ return null; // the arms write it and this cannot substitute it
205
+ }
206
+ }
207
+ if (merge.size === 0) {
208
+ return null;
209
+ }
210
+ const then = armDefs(iff.then, merge, declaredNames);
211
+ const els = armDefs(iff.else, merge, declaredNames);
212
+ if (then === null || els === null) {
213
+ return null;
214
+ }
215
+ for (const v of [...then.defs.values(), ...els.defs.values()]) {
216
+ if (exprHasEffect(v) || exprReadsVolatile(v, sfn) || [...readsIn(v)].some((n) => merge.has(n))) {
217
+ return null;
218
+ }
219
+ }
220
+ merge.forEach((n) => consumed.add(n));
221
+ return {
222
+ ...iff,
223
+ then: [...then.keep, substitute(join, then.defs)],
224
+ else: [...els.keep, substitute(join, els.defs)],
225
+ };
226
+ };
227
+
228
+ const list = (xs: Stmt[]): Stmt[] => {
229
+ const out: Stmt[] = [];
230
+ for (let i = 0; i < xs.length; i++) {
231
+ const s = rewrite(xs[i]);
232
+ const join = xs[i + 1];
233
+ const done = s.k === 'if' && join !== undefined && isJoinable(join) ? unmergeAt(s, join) : null;
234
+ out.push(done ?? s);
235
+ if (done) {
236
+ i++; // the join statement now lives in both arms
237
+ }
238
+ }
239
+ return out;
240
+ };
241
+
242
+ const rewrite = (s: Stmt): Stmt => {
243
+ switch (s.k) {
244
+ case 'if':
245
+ return { ...s, then: list(s.then), else: list(s.else) };
246
+ case 'while':
247
+ case 'dowhile':
248
+ case 'for':
249
+ return { ...s, body: list(s.body) };
250
+ case 'switch':
251
+ return {
252
+ ...s,
253
+ cases: s.cases.map((c) => ({ ...c, body: list(c.body) })),
254
+ ...(s.default ? { default: list(s.default) } : {}),
255
+ };
256
+ default:
257
+ return s;
258
+ }
259
+ };
260
+
261
+ const body = list(sfn.body);
262
+ return consumed.size === 0 ? null : { ...sfn, body, locals: sfn.locals.filter((l) => !consumed.has(l.name)) };
263
+ }