@asmlift/core 0.9.0 → 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.
- package/README.md +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +165 -50
- package/src/backend/cpp.ts +2 -1
- package/src/backend/pascal.ts +31 -12
- package/src/contracts.ts +173 -16
- package/src/declare.ts +69 -20
- package/src/frontend/asmdata.ts +7 -1
- package/src/frontend/call-plan.ts +964 -60
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +23 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1232 -192
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2076 -215
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2711 -232
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1460 -44
- package/src/frontend/stackargs.ts +106 -57
- package/src/frontend/thumb.ts +2044 -318
- package/src/ir/alias.ts +42 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +100 -5
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +159 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +109 -9
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +80 -11
- package/src/l3/ast.ts +290 -18
- package/src/l3/basecse.ts +33 -5
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +2 -2
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +31 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +9 -4
- package/src/l3/reindex.ts +283 -96
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +4 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +541 -24
- package/src/proto-context.ts +905 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +10 -26
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +102 -19
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +694 -0
- package/src/raise/shortcircuit.ts +26 -12
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +89 -31
- package/src/rank.ts +307 -54
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +165 -2
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +2180 -1054
- package/src/structure/switch-recover.ts +244 -100
- package/src/symbols.ts +6 -0
- package/src/target.ts +765 -52
- package/src/trace.ts +26 -8
- package/src/variation-definitions.ts +126 -14
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +5 -1
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* that matter — a sub-word or register-offset access is not a slot, and one anywhere turns the
|
|
16
16
|
* whole word-slot model off at the same gate this analysis's `blocker` feeds, so no access that
|
|
17
17
|
* could alias a licensed word is ever silently missing from the events. */
|
|
18
|
-
export type StackArgsEvent<C> = StackArgsSlot | StackArgsCall<C
|
|
18
|
+
export type StackArgsEvent<C> = StackArgsSlot | StackArgsCall<C> | StackArgsEscape;
|
|
19
19
|
|
|
20
20
|
/** A whole-word access to a reserved frame slot at `off`, which must lie inside `[0, localArea)`. */
|
|
21
21
|
export interface StackArgsSlot {
|
|
@@ -23,11 +23,24 @@ export interface StackArgsSlot {
|
|
|
23
23
|
readonly off: number;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** A frame address `sp + off` LEAVING this function — in an argument register at a call, or stored
|
|
27
|
+
* to memory a device or another function reads. Its receiver may read the frame from `off` up, so
|
|
28
|
+
* a word there that this function stored and never reloads has been read, by the receiver.
|
|
29
|
+
*
|
|
30
|
+
* Emitted only under `compilerBehaviors.escapedFrameAddressNamesALocal`: that premise is what
|
|
31
|
+
* makes the words from `off` up a local's rather than an argument area's. It ends only what is
|
|
32
|
+
* pending WHERE it fires, never a word staged after it, and the field says why that is the safe
|
|
33
|
+
* reading of a whole-function layout fact (a block copy's base escapes too). */
|
|
34
|
+
export interface StackArgsEscape {
|
|
35
|
+
readonly kind: 'escape';
|
|
36
|
+
readonly off: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
/** A call, and what its callee's DECLARATION says it takes on the stack. `declared` is the block
|
|
27
40
|
* `[0, 4*(n - argRegs))` as an ascending offset list, or null when nothing declares this call —
|
|
28
41
|
* an indirect call, a callee with no prototype, or one whose arity fits in registers. The code
|
|
29
42
|
* alone cannot say where a null declaration's block ENDS, so such a call is never handed a stack
|
|
30
|
-
* word; it lifts only when `
|
|
43
|
+
* word; it lifts only when `loadedWordBound` clears every word that could open one. */
|
|
31
44
|
export interface StackArgsCall<C> {
|
|
32
45
|
readonly kind: 'call';
|
|
33
46
|
/** The frontend's own handle for this call — the key of `OutgoingArgs.blocks`. */
|
|
@@ -60,10 +73,12 @@ export interface StackArgsInput<C> {
|
|
|
60
73
|
readonly argRegs: number;
|
|
61
74
|
/** The frontend's verdict that the whole frame is one addressable object handed to a callee. */
|
|
62
75
|
readonly capturedWholeFrame: boolean;
|
|
63
|
-
/** The
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
|
|
76
|
+
/** The lowest frame word this function loads where its compiler loads no word of the outgoing
|
|
77
|
+
* area, before a call or after (`target.ts` `outgoingAreaNeverLoadedFrom`, the strong form of
|
|
78
|
+
* `localsAboveOutgoingArea`, 0 on agbcc): `loadedWordBound` over these blocks, which the frontend
|
|
79
|
+
* computes once and hands the frame-object audit too. Every word at or above it is a local's.
|
|
80
|
+
* `Infinity` where the compiler claims no such offset. */
|
|
81
|
+
readonly loadedWordBound: number;
|
|
67
82
|
}
|
|
68
83
|
|
|
69
84
|
/** What this function's calls do with the BOTTOM of its frame — the outgoing stack-argument area
|
|
@@ -82,45 +97,35 @@ export interface OutgoingArgs<C> {
|
|
|
82
97
|
}
|
|
83
98
|
|
|
84
99
|
const isCallEvent = <C>(ev: StackArgsEvent<C>): ev is StackArgsCall<C> => ev.kind === 'call';
|
|
100
|
+
const isSlotEvent = <C>(ev: StackArgsEvent<C>): ev is StackArgsSlot => ev.kind === 'store' || ev.kind === 'load';
|
|
85
101
|
|
|
86
|
-
/** THE LOWEST FRAME WORD
|
|
87
|
-
*
|
|
88
|
-
* `
|
|
89
|
-
*
|
|
90
|
-
*
|
|
102
|
+
/** THE LOWEST FRAME WORD THIS FUNCTION LOADS AT OR ABOVE `from`, or `Infinity` when it loads none
|
|
103
|
+
* or nothing states `from`. The Thumb frontend calls it once per lift and hands the number to both
|
|
104
|
+
* readers: `analyzeOutgoingArgs` and the frame-object audit (`FrameObjectAudit.loadedWords`).
|
|
105
|
+
* Under `outgoingAreaNeverLoadedFrom` the caller never reads a word of its outgoing area from that
|
|
106
|
+
* offset up — not after a call, where the callee may have assigned to it, and not before one
|
|
107
|
+
* either: the area holds only what the caller staged for a callee, every argument that contains a
|
|
108
|
+
* call is computed before any word is staged, and nothing is saved there around a nested call
|
|
109
|
+
* (agbcc-restage.s pins all three). So a word it loads is a local; and since the area is one
|
|
110
|
+
* region at the frame bottom that every call shares, every word at or above the bound is proven
|
|
111
|
+
* not to be an argument.
|
|
91
112
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
function
|
|
113
|
+
* Only a load in a live block counts: a load that cannot run proves nothing. An ESCAPE does not
|
|
114
|
+
* count: its receiver MAY read the words, and a bound is a proof, which a read that may not happen
|
|
115
|
+
* cannot supply. */
|
|
116
|
+
export function loadedWordBound<C>(
|
|
96
117
|
blocks: readonly StackArgsBlock<C>[],
|
|
97
|
-
succs: readonly (readonly number[])[],
|
|
98
118
|
live: ReadonlySet<number>,
|
|
119
|
+
from: number | undefined,
|
|
99
120
|
): number {
|
|
100
|
-
|
|
101
|
-
|
|
121
|
+
if (from === undefined) {
|
|
122
|
+
return Infinity;
|
|
123
|
+
}
|
|
102
124
|
let bound = Infinity;
|
|
103
|
-
for (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
const cur = liveOut(b);
|
|
110
|
-
for (const ev of [...blocks[b].events].reverse()) {
|
|
111
|
-
if (isCallEvent(ev)) {
|
|
112
|
-
for (const o of cur) {
|
|
113
|
-
bound = Math.min(bound, o);
|
|
114
|
-
}
|
|
115
|
-
} else if (ev.kind === 'load') {
|
|
116
|
-
cur.add(ev.off);
|
|
117
|
-
} else {
|
|
118
|
-
cur.delete(ev.off);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if ([...cur].some((o) => !liveIn[b].has(o))) {
|
|
122
|
-
liveIn[b] = cur;
|
|
123
|
-
changed = true;
|
|
125
|
+
for (const b of live) {
|
|
126
|
+
for (const ev of blocks[b].events) {
|
|
127
|
+
if (ev.kind === 'load' && ev.off >= from) {
|
|
128
|
+
bound = Math.min(bound, ev.off);
|
|
124
129
|
}
|
|
125
130
|
}
|
|
126
131
|
}
|
|
@@ -164,14 +169,14 @@ function survivorBound<C>(
|
|
|
164
169
|
// and (b) below, kept for every call no declaration covers).
|
|
165
170
|
//
|
|
166
171
|
// A THIRD WITNESS SAYS WHERE THE AREA ENDS, and it only ever clears words. Under a compiler that
|
|
167
|
-
// keeps one outgoing area below every local and never
|
|
168
|
-
// (`
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
172
|
+
// keeps one outgoing area below every local and never loads a word of it
|
|
173
|
+
// (`outgoingAreaNeverLoadedFrom`), a word this function LOADS is a local, and so is every word above it
|
|
174
|
+
// (`loadedWordBound`). Every refusal that reads the code — NOTHING EXTRA, (a) and (b) — then
|
|
175
|
+
// weighs only the words beneath the bound, so an undeclared call lifts once no word there could
|
|
176
|
+
// open an argument block, and takes no stack word. Hand-written asm that reads its own outgoing
|
|
177
|
+
// argument defeats it, and lifts with that argument dropped — a producer assumption of the same
|
|
178
|
+
// kind as CONTIGUITY below, which is why the premise is declared per compiler (`target.ts`
|
|
179
|
+
// `outgoingAreaNeverLoadedFrom`) and never inferred.
|
|
175
180
|
//
|
|
176
181
|
// THE TWO SIDES ARE CHECKED AGAINST DIFFERENT SETS, and the asymmetry is the point.
|
|
177
182
|
// * NOTHING EXTRA is checked against the MAY set (stored and unreloaded on SOME path): the
|
|
@@ -183,12 +188,12 @@ function survivorBound<C>(
|
|
|
183
188
|
// must never render as a plausible value.
|
|
184
189
|
//
|
|
185
190
|
// A SPILL LIVE ACROSS A LICENSED CALL sits in the may set and is not in the declared block — agbcc's
|
|
186
|
-
// commonest frame with an outgoing area. The declaration cannot excuse it; `
|
|
187
|
-
// word
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
// A spill with no
|
|
191
|
+
// commonest frame with an outgoing area. The declaration cannot excuse it; `loadedWordBound` can: a
|
|
192
|
+
// word this function loads is a local, so NOTHING EXTRA counts only the pending words beneath the
|
|
193
|
+
// bound. A declared block that reaches the bound contradicts it, and needs no check of its own:
|
|
194
|
+
// the block is contiguous words from zero, so it holds the bound's word, which this function loads
|
|
195
|
+
// — "this function also LOADS it" below refuses it if nothing earlier does.
|
|
196
|
+
// A spill with no load at or beneath it still refuses, and its decline names a STORE ("[sp,#k]
|
|
192
197
|
// also reaches the call unread") rather than the capability, so a gap histogram groups it under
|
|
193
198
|
// that message.
|
|
194
199
|
// The must set is an intersection over predecessors, which is exactly what a TAIL-MERGED call
|
|
@@ -213,7 +218,7 @@ export function analyzeOutgoingArgs<C>({
|
|
|
213
218
|
localArea,
|
|
214
219
|
argRegs,
|
|
215
220
|
capturedWholeFrame,
|
|
216
|
-
|
|
221
|
+
loadedWordBound: bound,
|
|
217
222
|
}: StackArgsInput<C>): OutgoingArgs<C> {
|
|
218
223
|
const refuse = (blocker: string): OutgoingArgs<C> => ({ blocker, blocks: new Map(), area: 0 });
|
|
219
224
|
// EVERY block, not the entry-reachable ones: a call in dead code stages nothing, so the
|
|
@@ -242,6 +247,9 @@ export function analyzeOutgoingArgs<C>({
|
|
|
242
247
|
// that computes it owns which escapes qualify. A declared fifth argument says [sp,#0] is a
|
|
243
248
|
// DIFFERENT callee's argument slot. Two contradictory claims about the same word, and nothing
|
|
244
249
|
// here can decide which to believe, so the honest answer is the decline it has always been.
|
|
250
|
+
// This is the flow-INSENSITIVE reading of an escape, sound only at one word; the `escape` event
|
|
251
|
+
// is the per-program-point reading of the same escapes at any frame size, and its contradiction
|
|
252
|
+
// with a declared block is the "ESCAPED ADDRESS AT OR BELOW A LICENSED WORD" refusal below.
|
|
245
253
|
if (capturedWholeFrame) {
|
|
246
254
|
for (const ev of calls) {
|
|
247
255
|
const offs = ev.declared;
|
|
@@ -272,7 +280,6 @@ export function analyzeOutgoingArgs<C>({
|
|
|
272
280
|
// code-reading refusals below — the licence's NOTHING EXTRA, (a) and (b) — each hunt for a word
|
|
273
281
|
// that could be one, so the words they weigh are cut to those beneath the bound once, where
|
|
274
282
|
// they are collected (`mayAt`, `storedAnywhere`), and none of them repeats the test.
|
|
275
|
-
const bound = localsAboveOutgoingArea ? survivorBound(asmBlocks, succs, live) : Infinity;
|
|
276
283
|
const belowBound = (s: Iterable<number>): Set<number> => new Set([...s].filter((o) => o < bound));
|
|
277
284
|
|
|
278
285
|
const everySlot: number[] = [];
|
|
@@ -332,9 +339,18 @@ export function analyzeOutgoingArgs<C>({
|
|
|
332
339
|
may.add(ev.off);
|
|
333
340
|
must.add(ev.off);
|
|
334
341
|
stored.add(ev.off);
|
|
335
|
-
} else {
|
|
342
|
+
} else if (ev.kind === 'load') {
|
|
336
343
|
may.delete(ev.off);
|
|
337
344
|
must.delete(ev.off);
|
|
345
|
+
} else {
|
|
346
|
+
// the receiver of the escaped address reads what it was handed, so those words stop
|
|
347
|
+
// being pending exactly as a reload would end them
|
|
348
|
+
for (const o of everySlot) {
|
|
349
|
+
if (o >= ev.off) {
|
|
350
|
+
may.delete(o);
|
|
351
|
+
must.delete(o);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
338
354
|
}
|
|
339
355
|
}
|
|
340
356
|
const grow = (out: Array<Set<number>>, cur: Set<number>): void => {
|
|
@@ -400,6 +416,24 @@ export function analyzeOutgoingArgs<C>({
|
|
|
400
416
|
licensed.add(o);
|
|
401
417
|
}
|
|
402
418
|
}
|
|
419
|
+
// An ESCAPED ADDRESS AT OR BELOW A LICENSED WORD — the one-word captured frame's contradiction,
|
|
420
|
+
// at any frame size. The escape names the words from its offset up as an object's, a licensed
|
|
421
|
+
// block names one of them as a callee's argument, and nothing here can decide which to believe.
|
|
422
|
+
// Under agbcc the two never meet (an address-taken local sits ABOVE the outgoing area, and at
|
|
423
|
+
// [sp,#0] only when no call takes a stack word), so a function that has both is not agbcc's
|
|
424
|
+
// layout and declines.
|
|
425
|
+
const escapes = [...live].flatMap((b) => asmBlocks[b].events.flatMap((ev) => (ev.kind === 'escape' ? [ev.off] : [])));
|
|
426
|
+
const lowestEscape = Math.min(Infinity, ...escapes);
|
|
427
|
+
for (const ev of calls) {
|
|
428
|
+
const offs = blocks.get(ev.call) ?? [];
|
|
429
|
+
const clash = offs.find((o) => o >= lowestEscape);
|
|
430
|
+
if (clash !== undefined) {
|
|
431
|
+
return refuse(
|
|
432
|
+
`callee \`${ev.callee}\` is declared with ${declaredAs(ev, offs)}, so [sp,#${clash}] is its outgoing stack argument — ` +
|
|
433
|
+
`but the frame address [sp,#${lowestEscape}] escapes this function, so the words from there up are an object's, and the two name the same word`,
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
403
437
|
// The two whole-function facts the next two refusals read: every offset live code stores, and
|
|
404
438
|
// every offset live code loads back. A reload in dead code is not evidence that anything reads
|
|
405
439
|
// the slot back, so it does not count.
|
|
@@ -407,11 +441,26 @@ export function analyzeOutgoingArgs<C>({
|
|
|
407
441
|
const stores = new Set<number>();
|
|
408
442
|
for (const b of live) {
|
|
409
443
|
for (const ev of asmBlocks[b].events) {
|
|
410
|
-
if (ev
|
|
444
|
+
if (isSlotEvent(ev)) {
|
|
411
445
|
(ev.kind === 'store' ? stores : reloaded).add(ev.off);
|
|
412
446
|
}
|
|
413
447
|
}
|
|
414
448
|
}
|
|
449
|
+
// …and a word an escaped address hands over is read by its receiver, which is a reload for (a):
|
|
450
|
+
// its never being read back HERE is explained by who reads it THERE.
|
|
451
|
+
//
|
|
452
|
+
// WHOLE-FUNCTION, unlike the pending sets above: a store after the last escape, or on a path
|
|
453
|
+
// that never escapes, is excused here too. That is sound because (a) is not what clears a word
|
|
454
|
+
// for a call. A word that is some call's argument reaches that call pending, and the licence's
|
|
455
|
+
// equality (a declared callee) or (b) (an undeclared one) refuses it there, per program point:
|
|
456
|
+
// `qh5`'s fifth `h5` word, staged after its memcpy base escapes, is refused by (b). What this
|
|
457
|
+
// widening lets through is a store that reaches no call pending with its lower slots supplied,
|
|
458
|
+
// which by CONTIGUITY (above) no call takes as an argument.
|
|
459
|
+
for (const o of everySlot) {
|
|
460
|
+
if (o >= lowestEscape) {
|
|
461
|
+
reloaded.add(o);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
415
464
|
const storedAnywhere = belowBound(stores);
|
|
416
465
|
// A LICENSED WORD THIS FUNCTION ALSO LOADS. The area belongs to the CALLEE — which may assign
|
|
417
466
|
// to a stack parameter — so after the `bl` the word holds whatever the callee left, and an
|