@asmlift/core 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +36 -31
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +397 -0
  4. package/src/backend/c.ts +6 -2
  5. package/src/backend/cfamily.ts +370 -56
  6. package/src/backend/cpp.ts +116 -10
  7. package/src/backend/pascal.ts +69 -11
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +777 -84
  15. package/src/declare.ts +132 -13
  16. package/src/frontend/asmdata.ts +26 -6
  17. package/src/frontend/call-plan.ts +1590 -0
  18. package/src/frontend/dead-reads.ts +628 -0
  19. package/src/frontend/device-pins.ts +296 -0
  20. package/src/frontend/emit.ts +4 -3
  21. package/src/frontend/errors.ts +5 -0
  22. package/src/frontend/flags-edge.ts +101 -0
  23. package/src/frontend/fpu.ts +374 -0
  24. package/src/frontend/frame-objects.ts +3171 -0
  25. package/src/frontend/frontend.ts +64 -3
  26. package/src/frontend/high-half.ts +55 -5
  27. package/src/frontend/jump-table.ts +530 -0
  28. package/src/frontend/local-object.ts +616 -0
  29. package/src/frontend/mips.ts +2392 -208
  30. package/src/frontend/opaque.ts +97 -18
  31. package/src/frontend/own-struct-return.ts +500 -0
  32. package/src/frontend/ppc.ts +3137 -267
  33. package/src/frontend/register-struct-call.ts +484 -0
  34. package/src/frontend/reloc-symbol.ts +109 -13
  35. package/src/frontend/splat.ts +108 -5
  36. package/src/frontend/ssa.ts +2098 -89
  37. package/src/frontend/stackargs.ts +172 -42
  38. package/src/frontend/thumb.ts +4563 -2091
  39. package/src/ir/alias.ts +71 -0
  40. package/src/ir/bits.ts +681 -1
  41. package/src/ir/core.ts +199 -6
  42. package/src/ir/discipline.ts +313 -0
  43. package/src/ir/entry.ts +159 -0
  44. package/src/ir/float-bits.ts +47 -0
  45. package/src/{structure → ir}/loops.ts +6 -2
  46. package/src/ir/opcodes.ts +193 -121
  47. package/src/ir/parse.ts +1 -0
  48. package/src/ir/types.ts +202 -2
  49. package/src/ir/verify.ts +219 -3
  50. package/src/l3/address.ts +5 -0
  51. package/src/l3/argcopy.ts +38 -6
  52. package/src/l3/ast.ts +467 -23
  53. package/src/l3/basecse.ts +33 -5
  54. package/src/l3/coalesce.ts +21 -13
  55. package/src/l3/constinit.ts +382 -0
  56. package/src/l3/dce.ts +34 -12
  57. package/src/l3/gates.ts +9 -3
  58. package/src/l3/hoist.ts +8 -7
  59. package/src/l3/initfirst.ts +68 -19
  60. package/src/l3/intcell.ts +98 -0
  61. package/src/l3/mentions.ts +10 -4
  62. package/src/l3/narrowdecl.ts +259 -0
  63. package/src/l3/nearbase.ts +10 -5
  64. package/src/l3/parkfirst.ts +8 -5
  65. package/src/l3/pollguard.ts +11 -4
  66. package/src/l3/ptrcell.ts +354 -0
  67. package/src/l3/ptrfield.ts +123 -8
  68. package/src/l3/regspell.ts +26 -7
  69. package/src/l3/reindex.ts +293 -103
  70. package/src/l3/storage.ts +98 -1
  71. package/src/l3/symbol-refs.ts +182 -23
  72. package/src/l3/tailmerge.ts +9 -20
  73. package/src/l3/typing.ts +237 -32
  74. package/src/l3/unreduce.ts +10 -9
  75. package/src/l3/volatileptr.ts +17 -54
  76. package/src/l3/volstore.ts +96 -7
  77. package/src/mangle.ts +41 -1
  78. package/src/pattern/engine.ts +373 -82
  79. package/src/pipeline.ts +589 -27
  80. package/src/proto-context.ts +1648 -0
  81. package/src/proto.ts +1203 -49
  82. package/src/raise/aggmembers.ts +290 -0
  83. package/src/raise/arrays.ts +8 -3
  84. package/src/raise/declared-volatile.ts +307 -0
  85. package/src/raise/divpow2.ts +2 -2
  86. package/src/raise/errors.ts +17 -0
  87. package/src/raise/floathelpers.ts +464 -0
  88. package/src/raise/globalshape.ts +11 -5
  89. package/src/raise/gvn.ts +29 -8
  90. package/src/raise/latch.ts +162 -6
  91. package/src/raise/memberarrays.ts +10 -4
  92. package/src/raise/narrow.ts +29 -2
  93. package/src/raise/narrowlocal.ts +11 -6
  94. package/src/raise/offsetnames.ts +494 -0
  95. package/src/raise/pairparams.ts +114 -0
  96. package/src/raise/paramwidth.ts +203 -6
  97. package/src/raise/pre-recovery.ts +174 -17
  98. package/src/raise/pre-structure.ts +54 -0
  99. package/src/raise/recover.ts +26 -15
  100. package/src/raise/reroll.ts +694 -0
  101. package/src/raise/retsink.ts +1 -1
  102. package/src/raise/shortcircuit.ts +66 -30
  103. package/src/raise/softdiv.ts +21 -23
  104. package/src/raise/storesign.ts +153 -0
  105. package/src/raise/struct-arrays.ts +34 -14
  106. package/src/raise/structs.ts +674 -98
  107. package/src/raise/truncload.ts +367 -0
  108. package/src/raise/unthread.ts +400 -0
  109. package/src/raise/widehelpers.ts +106 -0
  110. package/src/rank-declare.ts +143 -12
  111. package/src/rank-variations.ts +117 -31
  112. package/src/rank.ts +563 -122
  113. package/src/runtime-helpers.ts +405 -0
  114. package/src/stillborn.ts +289 -0
  115. package/src/structure/analysis.ts +1449 -449
  116. package/src/structure/arith-ops.ts +23 -0
  117. package/src/structure/globalaccess.ts +111 -9
  118. package/src/structure/hazards.ts +641 -114
  119. package/src/structure/local-statics.ts +517 -0
  120. package/src/structure/namecoalesce.ts +2 -2
  121. package/src/structure/pointer-spelling.ts +1106 -0
  122. package/src/structure/redundant-test.ts +99 -0
  123. package/src/structure/structure.ts +3478 -975
  124. package/src/structure/switch-recover.ts +403 -178
  125. package/src/symbols.ts +44 -3
  126. package/src/target.ts +1387 -71
  127. package/src/trace.ts +66 -28
  128. package/src/variation-definitions.ts +243 -13
  129. package/src/variation-gates.ts +2 -0
  130. package/src/variation-tokens.ts +25 -7
@@ -0,0 +1,289 @@
1
+ // asmlift — THE STILLBORN FAN: when to stop compiling a fan that cannot compile.
2
+ //
3
+ // A fan is a product of variations over ONE function body, and every variation only re-spells
4
+ // statements. When the body holds a statement the compiler refuses for a reason no variation
5
+ // reaches — a call with more arguments than the project's header declares, a struct-typed global
6
+ // read as a scalar — every candidate is refused for it, and the fan's size is the number of times
7
+ // the same sentence gets printed: 30,240 on `kleod:PauseMenuScreenHandler`.
8
+ //
9
+ // THE RULE. The DEFAULT candidate (first in enumeration order) is compiled first. Only if the
10
+ // compiler REJECTS it is anything else asked: a PROBE per variation name — the candidate with the
11
+ // fewest variations that carries it, per signedness half (HALF BY HALF) — is compiled, and the fan
12
+ // is stillborn only when the default holds an error that SURVIVES every probe: a message the probe
13
+ // was rejected with too, exactly as many times as the default was (compiler-diagnostics.ts
14
+ // `errorMessages`, positions normalised away). Then the remaining candidates are NOT COMPILED, and
15
+ // are reported as exactly that.
16
+ //
17
+ // WHAT IT BETS ON: that a statement no probe re-spells is re-spelled by no product of the probes'
18
+ // variations either, so its error is in every candidate. That is a bet, not a theorem — a product
19
+ // is not the sum of its variations (THE RESIDUALS, below) — and the rule never compiles a product
20
+ // to check it.
21
+ //
22
+ // One survivor is enough; the probes may reach the default's other errors. In
23
+ // `ac-decomp:aINS_destruct:mwcc_242_81`'s vendored-context attempt `/setup-args` cures the call to
24
+ // `mPlib_Get_item_net_catch_label`, both `illegal implicit conversion` errors stay, and the fan of
25
+ // 12 stops after 8 compiles.
26
+ //
27
+ // EXACTLY as many times: a probe that prints the survivor more often has re-spelled a statement
28
+ // that raises it (`/reread-globals` re-reading a global whose type is refused), and a probe that
29
+ // prints it fewer times has cured one. Either one reached it, so neither vouches that a product
30
+ // leaves it alone.
31
+ //
32
+ // PER ATTEMPT. A rejection may hold several compiles (compiler-diagnostics.ts `attemptsOf`: the
33
+ // benchmark's real tier tries every candidate in a ladder of contexts and two dialects), and a
34
+ // candidate compiles when ANY attempt does. So a survivor is asked of EACH attempt, and one attempt
35
+ // without a survivor keeps the fan alive: an attempt no candidate can compile in — a context that
36
+ // cannot open its own headers — prints the same sentences for every candidate, and those survive
37
+ // every probe while another attempt's product compiles. Every probe must have been tried in the
38
+ // same attempts, under the same labels, as the default.
39
+ //
40
+ // HALF BY HALF. Every fan is enumerated at BOTH signednesses (variation-tokens.ts, kind
41
+ // `signedness`), and a signedness re-types the whole body, so the survivor is worded once per half:
42
+ // aINS_destruct's `illegal implicit conversion` to `struct game_s *` names `unsigned long` in one
43
+ // half and `long` in the other, and asked over the whole fan nothing in its vendored-context
44
+ // attempt survives the `signed` probe. So each half is asked as a fan of its own — its first
45
+ // candidate is its default, its probes are the smallest carrier of each variation name INSIDE it —
46
+ // and the fan is stillborn only when every half is. A half is found by the registry's kind, never
47
+ // by a name, and a fan with no signedness in its names is one half. A stopped fan pays both halves'
48
+ // probes.
49
+ //
50
+ // KNOWN GAP: a variation that re-words the survivor in PART of the fan is not split on, so its
51
+ // probe leaves that attempt without a survivor and the fan is ranked whole —
52
+ // `kleod:WorldMapScreenUnlockNewWorld:agbcc`'s only vendored-context error, `incompatible types in
53
+ // assignment`, reads `invalid operands to binary &` under `/derived-home` and `/setup-args`.
54
+ //
55
+ // Anything else ranks the whole fan. A probe that compiles or is withheld: the fan is alive. An
56
+ // attempt with no survivor: a product of variations may cure what no single one does. An attempt
57
+ // whose errors are not read — none recognised, or the compiler stopped reporting — or an attempt
58
+ // list that differs from the default's: an unread sentence equals nothing. A throw that is not a
59
+ // `CompilerRejection`: a timeout or a killed compiler says nothing about the candidate.
60
+ //
61
+ // THE RESIDUALS, which this rule does NOT close: a product that cures the survivor while every
62
+ // probe keeps it. One survivor being enough, a stop may rest its whole bet on a single message in
63
+ // the one attempt a candidate could compile in. Two shapes are known.
64
+ //
65
+ // JOINT CURE: ONE error MESSAGE that needs TWO variations together. `a & b` is `invalid operands
66
+ // to binary &` while either operand is a struct; if one variation re-types `a` and another re-types
67
+ // `b`, each probe leaves the message where it was, and only their product compiles. The same shape
68
+ // arises without any re-typing wherever the compiler prints ONE message for a statement holding TWO
69
+ // defects: mwcc and IDO report `y = g(1, 2) + h(3, 4)` as a single message, and agbcc, kmc and IDO
70
+ // report an undeclared name once however many times it is used. Two DIFFERENT messages each cured
71
+ // by its own variation are not this case: each probe removes its message, and neither survives.
72
+ // Signedness re-types operands and can cure half a statement — against a declared
73
+ // `CARDGetSectorSize(long, unsigned long *)`, `signed` turns a call's `(unsigned long, long *)` into
74
+ // `(long, long *)`, one message either way — and HALF BY HALF is what closes it there: a half holds
75
+ // its signedness fixed. Any OTHER variation that re-types an operand or cures half a statement
76
+ // must revisit this rule.
77
+ //
78
+ // INTERACTION: a variation whose reach depends on another. A respell runs over whatever tree the
79
+ // structure variations built, so a structure variation × respell product re-spells statements
80
+ // neither of its probes touched: `/unmerge` copies a join's call into both arms only on the
81
+ // `/flip-join` tree, so their product can print a message more often than the default and either
82
+ // probe do. A product that moves a survivor UP keeps it an error; one that moved it to zero, leaving
83
+ // no other error, would be a candidate that compiles and that the stop never compiles.
84
+ //
85
+ // ONE COPY. Probe selection and the verdict are pure functions over indices, so the sync driver
86
+ // (rank.ts `rankBy`), the pooled CLI driver and the webapp's async loop all sequence their own
87
+ // compiles and ask the same question. Every driver finishes the probe phase before compiling
88
+ // anything else, which is what keeps the verdict independent of a scheduler.
89
+ import { CompilerRejection, attemptsOf, verdictMessages } from './compiler-diagnostics';
90
+ import { VARIATION_TOKENS } from './variation-tokens';
91
+
92
+ /** What compiling one candidate came to, as far as this rule reads it. `compiled` covers scored
93
+ * AND withheld: either way the compiler accepted the text. */
94
+ export type ProbeOutcome = 'compiled' | { thrown: unknown };
95
+
96
+ /** The signedness names, read off the registry by kind. */
97
+ const SIGNEDNESS = new Set<string>(VARIATION_TOKENS.filter((t) => t.variationKind === 'signedness').map((t) => t.name));
98
+
99
+ /** The fan's signedness HALVES: candidates grouped by the signedness their names carry, each in
100
+ * enumeration order, the halves in the order of their first candidate. Index 0 opens the first. */
101
+ function halvesOf(candidates: readonly { variations: readonly string[] }[]): number[][] {
102
+ const halves = new Map<string, number[]>();
103
+ candidates.forEach((c, i) => {
104
+ const key = c.variations.filter((v) => SIGNEDNESS.has(v)).join('/');
105
+ const half = halves.get(key);
106
+ if (half === undefined) {
107
+ halves.set(key, [i]);
108
+ } else {
109
+ half.push(i);
110
+ }
111
+ });
112
+ return [...halves.values()];
113
+ }
114
+
115
+ /** A half's probes: for every variation name in the half, the index of the candidate IN THE HALF
116
+ * with the FEWEST variations that carries it, enumeration order breaking a tie. Ascending, and
117
+ * without the half's own first candidate, which is its default.
118
+ *
119
+ * A NAME is the whole part as the candidate carries it, subject included: `argcopy-a0@1.0` and
120
+ * `argcopy-a0@2.0` are two probes, because each re-spells a different statement and the rule asks
121
+ * whether ANY re-spelling reaches the failing one. Collapsing them to the registered name would
122
+ * probe one region and answer for both. */
123
+ function probesOf(candidates: readonly { variations: readonly string[] }[], half: readonly number[]): number[] {
124
+ const smallest = new Map<string, number>();
125
+ for (const i of half) {
126
+ for (const v of candidates[i].variations) {
127
+ const held = smallest.get(v);
128
+ if (held === undefined || candidates[i].variations.length < candidates[held].variations.length) {
129
+ smallest.set(v, i);
130
+ }
131
+ }
132
+ }
133
+ return [...new Set(smallest.values())].filter((i) => i !== half[0]).sort((a, b) => a - b);
134
+ }
135
+
136
+ /** The candidates to compile once the default was rejected: every half's default and probes
137
+ * (`halvesOf`, `probesOf`). Ascending, and without the default itself. */
138
+ export function probeIndices(candidates: readonly { variations: readonly string[] }[]): number[] {
139
+ const probes = new Set<number>();
140
+ for (const half of halvesOf(candidates)) {
141
+ for (const i of [half[0], ...probesOf(candidates, half)]) {
142
+ probes.add(i);
143
+ }
144
+ }
145
+ probes.delete(0);
146
+ return [...probes].sort((a, b) => a - b);
147
+ }
148
+
149
+ /** A fan declared stillborn. */
150
+ export interface Stillborn {
151
+ /** each half's default's errors that survived every probe of that half, as the compiler worded
152
+ * them in that half: a message named at the most times one attempt printed it */
153
+ messages: string[];
154
+ /** indices that WERE compiled — the default and the probes, ascending */
155
+ compiled: number[];
156
+ /** indices that were not, ascending: the rest of the fan */
157
+ notCompiled: number[];
158
+ }
159
+
160
+ /** A rejection read attempt by attempt: each attempt's label and the multiset of its error
161
+ * messages. Null for anything the rule cannot read as a verdict — a compile, a throw that is no
162
+ * `CompilerRejection`, or any attempt whose errors are unread. */
163
+ interface Reading {
164
+ labels: string[];
165
+ errors: Map<string, number>[];
166
+ }
167
+
168
+ /** Whether a throw is the compiler REFUSING the text, read as the rule reads a probe: a
169
+ * `CompilerRejection` whose every attempt prints an error the rule reads. Any other throw — a
170
+ * killed compiler, a timeout — says nothing about the candidate, and neither does a rejection
171
+ * that prints no such error: a Docker CLI that cannot reach its daemon exits 1, as a refusing
172
+ * compiler does. Whatever checks the rule's bet by compiling a stopped fan's rest (the
173
+ * benchmark's `bench fan --whole`) counts a refusal through this test, never a second one. */
174
+ export const readableRefusal = (thrown: unknown): boolean => readingOf({ thrown }) !== null;
175
+
176
+ function readingOf(outcome: ProbeOutcome): Reading | null {
177
+ if (outcome === 'compiled' || !(outcome.thrown instanceof CompilerRejection)) {
178
+ return null;
179
+ }
180
+ const attempts = attemptsOf(outcome.thrown.diagnostic);
181
+ const errors: Map<string, number>[] = [];
182
+ for (const attempt of attempts) {
183
+ const messages = verdictMessages(attempt.diagnostic);
184
+ if (messages === null) {
185
+ return null;
186
+ }
187
+ const tally = new Map<string, number>();
188
+ for (const m of messages) {
189
+ tally.set(m, (tally.get(m) ?? 0) + 1);
190
+ }
191
+ errors.push(tally);
192
+ }
193
+ return { labels: attempts.map((a) => a.label), errors };
194
+ }
195
+
196
+ /** The default's errors that survive `probes`, per attempt — null as soon as one attempt has none
197
+ * left, or a probe cannot be read against the default. Asks `outcomeOf` for the probes in order
198
+ * and for none past the first that ends it. */
199
+ function survivors(
200
+ anchor: number,
201
+ probes: readonly number[],
202
+ outcomeOf: (index: number) => ProbeOutcome,
203
+ ): Map<string, number>[] | null {
204
+ const anchored = readingOf(outcomeOf(anchor));
205
+ if (anchored === null) {
206
+ return null;
207
+ }
208
+ const left = anchored.errors.map((tally) => new Map(tally));
209
+ for (const i of probes) {
210
+ const probe = readingOf(outcomeOf(i));
211
+ if (
212
+ probe === null ||
213
+ probe.labels.length !== anchored.labels.length ||
214
+ probe.labels.some((label, k) => label !== anchored.labels[k])
215
+ ) {
216
+ return null;
217
+ }
218
+ for (const [k, tally] of left.entries()) {
219
+ for (const [message, times] of tally) {
220
+ if (probe.errors[k].get(message) !== times) {
221
+ tally.delete(message);
222
+ }
223
+ }
224
+ if (tally.size === 0) {
225
+ return null;
226
+ }
227
+ }
228
+ }
229
+ return left;
230
+ }
231
+
232
+ /** The verdict over a fan whose default and probes have all been compiled — null for "rank the
233
+ * whole fan", which is every case but one (the module header states the rule).
234
+ *
235
+ * `outcomeOf` is asked for index 0 and then, only while the answer can still be "stillborn", for
236
+ * the probes of its half in order, then for each further half's default and probes — so a
237
+ * synchronous driver may compile on demand inside it and pays for no probe past the first that
238
+ * ends it. It must answer whenever asked: a driver that skipped a
239
+ * probe has not run the rule, and "stillborn" over the probes it happened to compile would be a
240
+ * different, weaker rule. */
241
+ export function stillbornVerdict(
242
+ candidates: readonly { variations: readonly string[] }[],
243
+ outcomeOf: (index: number) => ProbeOutcome | undefined,
244
+ ): Stillborn | null {
245
+ if (candidates.length === 0) {
246
+ return null;
247
+ }
248
+ const asked = (i: number): ProbeOutcome => {
249
+ const outcome = outcomeOf(i);
250
+ if (outcome === undefined) {
251
+ throw new Error(`internal: the stillborn verdict was asked before candidate ${i} was compiled`);
252
+ }
253
+ return outcome;
254
+ };
255
+ const perHalf: Map<string, number>[][] = [];
256
+ for (const half of halvesOf(candidates)) {
257
+ const left = survivors(half[0], probesOf(candidates, half), asked);
258
+ if (left === null) {
259
+ return null;
260
+ }
261
+ perHalf.push(left);
262
+ }
263
+ const compiled = [0, ...probeIndices(candidates)];
264
+ const tried = new Set(compiled);
265
+ return {
266
+ messages: namedOnce(perHalf.flat()),
267
+ compiled,
268
+ notCompiled: candidates.map((_, i) => i).filter((i) => !tried.has(i)),
269
+ };
270
+ }
271
+
272
+ /** Every half's and attempt's survivors as one list: a message in the order it first appears, as
273
+ * many times as the attempt that printed it most. */
274
+ function namedOnce(perAttempt: readonly Map<string, number>[]): string[] {
275
+ const most = new Map<string, number>();
276
+ for (const tally of perAttempt) {
277
+ for (const [message, times] of tally) {
278
+ most.set(message, Math.max(most.get(message) ?? 0, times));
279
+ }
280
+ }
281
+ return [...most].flatMap(([message, times]) => Array.from({ length: times }, () => message));
282
+ }
283
+
284
+ /** Whether a driver that has compiled ONLY the default should now compile the probes — i.e.
285
+ * whether the default was rejected in a way the rule can read. A driver may skip this and compile
286
+ * the probes regardless; asking first is what keeps an ordinary fan's compile order untouched. */
287
+ export function defaultIsReadableRejection(outcome: ProbeOutcome): boolean {
288
+ return readingOf(outcome) !== null;
289
+ }