@asmlift/core 0.5.0 → 0.7.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 +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +270 -171
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +243 -39
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +11 -0
- package/src/frontend/ppc.ts +43 -7
- package/src/frontend/ssa.ts +404 -29
- package/src/frontend/thumb.ts +2176 -686
- package/src/ir/alias.ts +78 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +345 -2
- package/src/ir/opcodes.ts +176 -21
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -5
- package/src/l3/ast.ts +510 -59
- package/src/l3/basecse.ts +686 -78
- package/src/l3/coalesce.ts +432 -46
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +96 -1
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +176 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +114 -89
- package/src/l3/reindex.ts +722 -80
- package/src/l3/scopebase.ts +649 -220
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +16 -1
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +687 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +239 -16
- package/src/pipeline.ts +173 -60
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +4 -4
- package/src/raise/extscale.ts +342 -0
- package/src/raise/globalshape.ts +1058 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +572 -0
- package/src/raise/paramwidth.ts +201 -0
- package/src/raise/pre-recovery.ts +169 -21
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +585 -19
- package/src/raise/shortcircuit.ts +1050 -89
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +34 -4
- package/src/raise/tailsink.ts +126 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank-variations.ts +760 -0
- package/src/rank.ts +2122 -326
- package/src/structure/analysis.ts +1398 -150
- package/src/structure/bitfields.ts +432 -0
- package/src/structure/globalaccess.ts +300 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +454 -0
- package/src/structure/structure.ts +3979 -612
- package/src/structure/switch-recover.ts +710 -145
- package/src/symbols.ts +188 -6
- package/src/target.ts +495 -32
- package/src/trace.ts +112 -33
- package/src/variation-definitions.ts +1540 -0
- package/src/variation-gates.ts +89 -0
- package/src/variation-tokens.ts +355 -0
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
// UNAMBIGUOUS here (the scaled operand is the index, read from the machine code) — the unscaled
|
|
19
19
|
// `add(x, y)` byte form stays out of scope (genuinely ambiguous without types).
|
|
20
20
|
import { Fn, Op, Value, defOpMap, mkOp } from '../ir/core';
|
|
21
|
+
import { nextStructIndex } from '../ir/struct-names';
|
|
21
22
|
import { IrType, StructField, T, scalarTypeForAccess } from '../ir/types';
|
|
23
|
+
import { collectStructs } from './structs';
|
|
22
24
|
|
|
23
25
|
interface Scaled {
|
|
24
26
|
base: Value;
|
|
@@ -94,6 +96,15 @@ function withPadding(dataFields: StructField[], stride: number): StructField[] {
|
|
|
94
96
|
export function recognizeStructArrays(fn: Fn): number {
|
|
95
97
|
const defs = defOpMap(fn);
|
|
96
98
|
let count = 0;
|
|
99
|
+
// The NAME index is not the recognition count: `count` is this function's return value — how
|
|
100
|
+
// many groups it recognized — and seeding it would report the wrong number. The index starts
|
|
101
|
+
// past every `Elem<N>` the graph already mentions; over the corpus that scan returns 0 every
|
|
102
|
+
// time, because this pass runs once per function and nothing else mints the prefix, and
|
|
103
|
+
// ir/struct-names.ts states why the computed 0 is kept over the assumed one.
|
|
104
|
+
let name = nextStructIndex(
|
|
105
|
+
[...collectStructs(fn)].map((s) => s.name),
|
|
106
|
+
'Elem',
|
|
107
|
+
);
|
|
97
108
|
|
|
98
109
|
// group candidate element pointers by base, tracking each add's own index and stride
|
|
99
110
|
const byBase = new Map<Value, { add: Op; index: Value; stride: number }[]>();
|
|
@@ -222,7 +233,7 @@ export function recognizeStructArrays(fn: Fn): number {
|
|
|
222
233
|
type: scalarTypeForAccess(width, loadSigned ?? width === 4),
|
|
223
234
|
name: `field_${off}`,
|
|
224
235
|
}));
|
|
225
|
-
const elemStruct = T.struct(`Elem${
|
|
236
|
+
const elemStruct = T.struct(`Elem${name++}`, withPadding(dataFields, stride), stride);
|
|
226
237
|
base.type = T.ptr(elemStruct);
|
|
227
238
|
|
|
228
239
|
// Rewrite each field load/store into an aload/astore carrying base, ITS elem's index,
|
|
@@ -242,7 +253,13 @@ export function recognizeStructArrays(fn: Fn): number {
|
|
|
242
253
|
bb.ops[i] = mkOp('aload', {
|
|
243
254
|
operands: [base, index],
|
|
244
255
|
results: [op.results[0]],
|
|
245
|
-
|
|
256
|
+
// listOrder rides along: an ldmia-expanded load keeps its stream-order caveat as an aload
|
|
257
|
+
attrs: {
|
|
258
|
+
elemSize: stride,
|
|
259
|
+
signed: op.attrs.signed as boolean,
|
|
260
|
+
fieldOff: op.attrs.off as number,
|
|
261
|
+
...(op.attrs.listOrder === true && { listOrder: true }),
|
|
262
|
+
},
|
|
246
263
|
});
|
|
247
264
|
} else if (op.opcode === 'store') {
|
|
248
265
|
bb.ops[i] = mkOp('astore', {
|
package/src/raise/structs.ts
CHANGED
|
@@ -22,8 +22,22 @@
|
|
|
22
22
|
// base @ {off8 w4} -> array (single aligned access — no struct evidence)
|
|
23
23
|
// base @ aload(index) -> array (variable index — untouched)
|
|
24
24
|
//
|
|
25
|
-
// This recovery is BYTE-NEUTRAL — `->field_N` and `[idx]` compile identically, so it
|
|
26
|
-
// representation upgrade driven by access evidence
|
|
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 variation. TWO
|
|
27
|
+
// MEASUREMENTS SAY "USUALLY" IS THE RIGHT WORD, and both were made on agbcc against a real target
|
|
28
|
+
// object, each pair differing in ONE token:
|
|
29
|
+
// • THE SPELLING. `synthetic:dmanest`'s reference compiles from `((struct Elem0 *)K)[a1].field_4`
|
|
30
|
+
// to a byte-exact match and from `((s32 *)((a1 << 3) + K))[1]` to a 2-point diff — a
|
|
31
|
+
// COMPONENT_REF keeps the offset in the load displacement, an index folds it into the pool
|
|
32
|
+
// literal. The row's own dataset entry carries the reproduction.
|
|
33
|
+
// • THE FIELD TYPE, which this file assigns from the ACCESS WIDTH alone. A word field declared
|
|
34
|
+
// `void *` rather than `s32` changes agbcc's alias set and lets a loop-invariant load leave the
|
|
35
|
+
// loop: `synthetic:dmaptrsrc` matches with the pointer declaration and diffs by 35 without it
|
|
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 variation.
|
|
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 question", not as "the differ could not referee one" — the
|
|
40
|
+
// second reading is the one both measurements above falsify. GAPS between accessed
|
|
27
41
|
// offsets (unaccessed leading/interior fields) are filled with `u8[N]` PAD fields so the declared
|
|
28
42
|
// struct reproduces the observed offsets byte-for-byte and is self-describing (the same
|
|
29
43
|
// discipline raise/struct-arrays.ts withPadding uses). Each accessed field must still be
|
|
@@ -31,6 +45,7 @@
|
|
|
31
45
|
// at an offset natural C alignment could not place it at) is rejected LOUD, as is an
|
|
32
46
|
// overlap/union.
|
|
33
47
|
import { Fn, Op, Value } from '../ir/core';
|
|
48
|
+
import { nextStructIndex } from '../ir/struct-names';
|
|
34
49
|
import { IrType, StructField, T, scalarTypeForAccess } from '../ir/types';
|
|
35
50
|
import type { StructType } from '../l3/ast';
|
|
36
51
|
import { RaiseUnsupportedError } from './errors';
|
|
@@ -49,7 +64,7 @@ interface Access {
|
|
|
49
64
|
// is a DIFFERENT operation: this pass is ALIGNMENT-AWARE (no explicit pad when C's own inter-field
|
|
50
65
|
// padding already lands the field) and carries NO trailing pad / struct `size` (a recovered struct
|
|
51
66
|
// here is only ever a `struct S *` pointee accessed by named field — never an array element or a
|
|
52
|
-
// by-value param, so sizeof is never taken). If the two are ever unified, PARAMETERIZE those
|
|
67
|
+
// by-value param, so sizeof is never taken). If the two are ever unified, PARAMETERIZE those dimensions
|
|
53
68
|
// — a naive merge would break the natural-alignment golden or silently mislay a struct that later
|
|
54
69
|
// becomes an element / ABI value.
|
|
55
70
|
const sizeAlign = (width: number): number => width;
|
|
@@ -189,6 +204,10 @@ export function recognizeStructs(fn: Fn): number {
|
|
|
189
204
|
}
|
|
190
205
|
}
|
|
191
206
|
|
|
207
|
+
// The NAME counter is seeded from the names already in the graph, not from this pass's own
|
|
208
|
+
// success count: raise/memberarrays.ts runs first and mints `Struct<N>` types of its own, and two
|
|
209
|
+
// different layouts under one name would leave `collectStructs` declaring only one of them.
|
|
210
|
+
let name = firstFreeStructIndex(fn);
|
|
192
211
|
let count = 0;
|
|
193
212
|
for (const base of order) {
|
|
194
213
|
if (arrayBases.has(base)) {
|
|
@@ -203,7 +222,7 @@ export function recognizeStructs(fn: Fn): number {
|
|
|
203
222
|
continue;
|
|
204
223
|
} // uniform stride / single aligned access → array
|
|
205
224
|
try {
|
|
206
|
-
base.type = T.ptr(buildStruct(`Struct${
|
|
225
|
+
base.type = T.ptr(buildStruct(`Struct${name}`, accesses));
|
|
207
226
|
} catch (e) {
|
|
208
227
|
// A NAMED global whose accesses synthesis cannot reconcile is not a reason to decline the
|
|
209
228
|
// function: its declaration belongs to the project's own headers, and its constant-offset
|
|
@@ -219,11 +238,22 @@ export function recognizeStructs(fn: Fn): number {
|
|
|
219
238
|
}
|
|
220
239
|
throw e;
|
|
221
240
|
}
|
|
241
|
+
name++;
|
|
222
242
|
count++;
|
|
223
243
|
}
|
|
224
244
|
return count;
|
|
225
245
|
}
|
|
226
246
|
|
|
247
|
+
/** The next `Struct<N>` index past every one this function's graph already uses — the shared name
|
|
248
|
+
* allocator for the two passes that synthesize a struct pointee. The scan itself is
|
|
249
|
+
* `ir/struct-names.ts`, which the three struct minters share; this names the prefix. */
|
|
250
|
+
export function firstFreeStructIndex(fn: Fn): number {
|
|
251
|
+
return nextStructIndex(
|
|
252
|
+
[...collectStructs(fn)].map((s) => s.name),
|
|
253
|
+
'Struct',
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
227
257
|
/** The distinct struct types this function's L2 GRAPH mentions (unwrapping struct pointers on every
|
|
228
258
|
* value), deduped by name and sorted, for the backend to declare above the function.
|
|
229
259
|
*
|
|
@@ -0,0 +1,126 @@
|
|
|
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 { type Block, type Fn, type Value, mkOp, predecessors, reachableBlocks, terminator } from '../ir/core';
|
|
43
|
+
import { simplifyTrivialPhis } from '../ir/simplify';
|
|
44
|
+
|
|
45
|
+
/** One edge that supplies the tail its arguments. `resolve` sends a value the tail reads to the
|
|
46
|
+
* value it has at the end of `from`: a parameter of the tail or of any forwarder between becomes
|
|
47
|
+
* the argument this path's edge into that block carried, and anything else is left alone. */
|
|
48
|
+
interface Source {
|
|
49
|
+
readonly from: Block;
|
|
50
|
+
readonly resolve: (v: Value) => Value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A block whose ops are one or more `store`s and then a void `ret`. */
|
|
54
|
+
function isStoreTail(b: Block): boolean {
|
|
55
|
+
const t = b.ops[b.ops.length - 1];
|
|
56
|
+
return (
|
|
57
|
+
b.ops.length >= 2 &&
|
|
58
|
+
t.opcode === 'ret' &&
|
|
59
|
+
t.operands.length === 0 &&
|
|
60
|
+
b.ops.slice(0, -1).every((o) => o.opcode === 'store')
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Copy every store tail that two or more sources reach into each source that reaches it by a
|
|
65
|
+
* `br`; returns whether anything changed. */
|
|
66
|
+
export function sinkStoreTails(fn: Fn): boolean {
|
|
67
|
+
let changed = false;
|
|
68
|
+
for (const tail of [...fn.blocks]) {
|
|
69
|
+
if (tail === fn.blocks[0] || !fn.blocks.includes(tail) || !isStoreTail(tail)) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const preds = predecessors(fn);
|
|
73
|
+
// The sources, seen through pure forwarders. `resolve` sends a value the tail reads to the value
|
|
74
|
+
// it has on entry to the block being walked; each edge out of a predecessor composes one more
|
|
75
|
+
// step onto it — `to`'s own parameters, not only the tail's, because the tail may read a
|
|
76
|
+
// forwarder's parameter directly.
|
|
77
|
+
const forwarders = new Set<Block>();
|
|
78
|
+
const sources: Source[] = [];
|
|
79
|
+
const conditional: Block[] = [];
|
|
80
|
+
const walk = (to: Block, resolve: (v: Value) => Value) => {
|
|
81
|
+
for (const p of preds.get(to) ?? []) {
|
|
82
|
+
const t = terminator(p)!;
|
|
83
|
+
if (t.opcode !== 'br') {
|
|
84
|
+
conditional.push(p);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const edge = t.successors[0].args;
|
|
88
|
+
const through = (v: Value): Value => {
|
|
89
|
+
const w = resolve(v);
|
|
90
|
+
const i = to.params.indexOf(w);
|
|
91
|
+
return i >= 0 ? edge[i] : w;
|
|
92
|
+
};
|
|
93
|
+
const isForwarder =
|
|
94
|
+
p !== fn.blocks[0] &&
|
|
95
|
+
p.ops.length === 1 &&
|
|
96
|
+
!forwarders.has(p) &&
|
|
97
|
+
(preds.get(p) ?? []).every((q) => terminator(q)?.opcode === 'br');
|
|
98
|
+
if (isForwarder) {
|
|
99
|
+
forwarders.add(p);
|
|
100
|
+
walk(p, through);
|
|
101
|
+
} else {
|
|
102
|
+
sources.push({ from: p, resolve: through });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
walk(tail, (v) => v);
|
|
107
|
+
if (sources.length === 0 || sources.length + conditional.length < 2) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const body = tail.ops.slice(0, -1);
|
|
111
|
+
for (const src of sources) {
|
|
112
|
+
const copies = body.map((o) => mkOp('store', { operands: o.operands.map(src.resolve), attrs: { ...o.attrs } }));
|
|
113
|
+
src.from.ops.splice(src.from.ops.length - 1, 1, ...copies, mkOp('ret'));
|
|
114
|
+
}
|
|
115
|
+
// By REACHABILITY, not predecessor count: the tail (unless a conditional edge keeps it) and
|
|
116
|
+
// every forwarder on the way are left unreachable, however long the chain, and a forwarder's
|
|
117
|
+
// in-edge from another orphan still counts as a predecessor.
|
|
118
|
+
const live = reachableBlocks(fn);
|
|
119
|
+
fn.blocks = fn.blocks.filter((b) => live.has(b));
|
|
120
|
+
changed = true;
|
|
121
|
+
}
|
|
122
|
+
if (changed) {
|
|
123
|
+
simplifyTrivialPhis(fn);
|
|
124
|
+
}
|
|
125
|
+
return changed;
|
|
126
|
+
}
|
|
@@ -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 variations 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-variations.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 `onEnumerationError`
|
|
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 `respellTree`'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
|
+
}
|