@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.
- package/README.md +36 -31
- package/package.json +1 -1
- package/src/aggregate.ts +397 -0
- package/src/backend/c.ts +6 -2
- package/src/backend/cfamily.ts +370 -56
- package/src/backend/cpp.ts +116 -10
- package/src/backend/pascal.ts +69 -11
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +777 -84
- package/src/declare.ts +132 -13
- package/src/frontend/asmdata.ts +26 -6
- package/src/frontend/call-plan.ts +1590 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +296 -0
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +374 -0
- package/src/frontend/frame-objects.ts +3171 -0
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +55 -5
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +616 -0
- package/src/frontend/mips.ts +2392 -208
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +3137 -267
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +109 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +2098 -89
- package/src/frontend/stackargs.ts +172 -42
- package/src/frontend/thumb.ts +4563 -2091
- package/src/ir/alias.ts +71 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +199 -6
- package/src/ir/discipline.ts +313 -0
- package/src/ir/entry.ts +159 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +193 -121
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +202 -2
- package/src/ir/verify.ts +219 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +467 -23
- package/src/l3/basecse.ts +33 -5
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +34 -12
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +68 -19
- package/src/l3/intcell.ts +98 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +259 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +11 -4
- package/src/l3/ptrcell.ts +354 -0
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +26 -7
- package/src/l3/reindex.ts +293 -103
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +182 -23
- package/src/l3/tailmerge.ts +9 -20
- package/src/l3/typing.ts +237 -32
- package/src/l3/unreduce.ts +10 -9
- package/src/l3/volatileptr.ts +17 -54
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +41 -1
- package/src/pattern/engine.ts +373 -82
- package/src/pipeline.ts +589 -27
- package/src/proto-context.ts +1648 -0
- package/src/proto.ts +1203 -49
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +8 -3
- package/src/raise/declared-volatile.ts +307 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +464 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +29 -8
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +494 -0
- package/src/raise/pairparams.ts +114 -0
- package/src/raise/paramwidth.ts +203 -6
- package/src/raise/pre-recovery.ts +174 -17
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +26 -15
- package/src/raise/reroll.ts +694 -0
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +66 -30
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +34 -14
- package/src/raise/structs.ts +674 -98
- package/src/raise/truncload.ts +367 -0
- package/src/raise/unthread.ts +400 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +143 -12
- package/src/rank-variations.ts +117 -31
- package/src/rank.ts +563 -122
- package/src/runtime-helpers.ts +405 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1449 -449
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +641 -114
- package/src/structure/local-statics.ts +517 -0
- package/src/structure/namecoalesce.ts +2 -2
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +3478 -975
- package/src/structure/switch-recover.ts +403 -178
- package/src/symbols.ts +44 -3
- package/src/target.ts +1387 -71
- package/src/trace.ts +66 -28
- package/src/variation-definitions.ts +243 -13
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +25 -7
package/src/ir/entry.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// asmlift — does a test placed ahead of a loop compute the loop's own test on the loop's entry values?
|
|
2
|
+
//
|
|
3
|
+
// Three questions ask exactly that, at two levels, and all read the answer from this one predicate:
|
|
4
|
+
// • structure's GUARD FUSION (`structure.ts`, through `hazards.ts`): the `if` a rotated loop is
|
|
5
|
+
// wrapped in is deleted when it is the loop's test at the first iteration, and the loop re-tests;
|
|
6
|
+
// • raise's RE-ROLL (`raise/reroll.ts`): a loop test the compiler peeled into the loop's entry is
|
|
7
|
+
// deleted when it is the latch's test at the values the entry hands the loop;
|
|
8
|
+
// • the re-roll's TOP-TEST RESTORE (`entryGuard`, same file): the guard a restored top test
|
|
9
|
+
// makes redundant is deleted when it is that test at the entry values, with the same exit args.
|
|
10
|
+
//
|
|
11
|
+
// The reading is STRUCTURAL, never semantic: two values are the same when they are one value, or
|
|
12
|
+
// when their defining ops have one opcode, one set of attributes and operands that are the same in
|
|
13
|
+
// turn. A false negative costs a loud decline, the direction to be wrong in. `entry` models the
|
|
14
|
+
// first iteration: on the loop side (`a`), a value it maps is read as the value it maps to, and
|
|
15
|
+
// below that substitution the walk is in the entry's world, where the map no longer applies — a
|
|
16
|
+
// loop-invariant back-edge argument read inside the entry's own expression is itself there, not the
|
|
17
|
+
// param it feeds.
|
|
18
|
+
//
|
|
19
|
+
// `normalize` adds the equalities the re-roll needs and guard fusion does not take: an `add` or
|
|
20
|
+
// `or` of a constant 0 on EITHER side, commutative operands in either order, and a load off
|
|
21
|
+
// `base + c` as the same load off `base` at its offset plus `c` (`str[0]` against `str[i]` at
|
|
22
|
+
// `i = 0`). Each is an identity of the integer operators, so turning it on for guard fusion is a
|
|
23
|
+
// spelling decision, not a soundness one.
|
|
24
|
+
import { Op, Value } from './core';
|
|
25
|
+
import { spelledWhenDead } from './discipline';
|
|
26
|
+
import { NEGATED_ICMP } from './opcodes';
|
|
27
|
+
|
|
28
|
+
export interface EntryReading {
|
|
29
|
+
/** the value each loop-side value holds on the loop's first iteration */
|
|
30
|
+
readonly entry: ReadonlyMap<Value, Value>;
|
|
31
|
+
/** values whose defining op may be read through; everything else compares by identity. Default:
|
|
32
|
+
* every value. An operand-free op that reads no memory (a constant, a symbol address) is always
|
|
33
|
+
* read through: it is its attributes wherever it sits. */
|
|
34
|
+
readonly through?: (v: Value) => boolean;
|
|
35
|
+
/** the identities above, beyond `x + 0` / `x - 0` / `x | 0` with the zero on the right */
|
|
36
|
+
readonly normalize?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Opcodes whose two operands may be read in either order, under `normalize`. */
|
|
40
|
+
const COMMUTES = new Set(['add', 'mul', 'and', 'or', 'xor', 'icmp_eq', 'icmp_ne']);
|
|
41
|
+
|
|
42
|
+
/** Is `a` (loop side), read on the loop's entry values, the same value as `b`? With `negated`, is it
|
|
43
|
+
* `b`'s logical opposite — the usual case where a guard spells the loop's test the other way round. */
|
|
44
|
+
export function sameAtEntry(
|
|
45
|
+
defs: ReadonlyMap<Value, Op>,
|
|
46
|
+
a: Value,
|
|
47
|
+
b: Value,
|
|
48
|
+
reading: EntryReading,
|
|
49
|
+
negated = false,
|
|
50
|
+
): boolean {
|
|
51
|
+
const { entry, through = () => true, normalize = false } = reading;
|
|
52
|
+
const readable = (v: Value): boolean => {
|
|
53
|
+
const d = defs.get(v);
|
|
54
|
+
return d !== undefined && (through(v) || (d.operands.length === 0 && !spelledWhenDead(d)));
|
|
55
|
+
};
|
|
56
|
+
const constOf = (v: Value): number | null => {
|
|
57
|
+
const d = defs.get(v);
|
|
58
|
+
return d?.opcode === 'const' && typeof d.attrs?.value === 'number' ? d.attrs.value : null;
|
|
59
|
+
};
|
|
60
|
+
/** A constant `v` reads as: under `normalize`, as its entry substitution reads. */
|
|
61
|
+
const constAt = (v: Value, atEntry: boolean): number | null => constOf(normalize ? view(v, atEntry).x : v);
|
|
62
|
+
/** `x0` as the comparison reads it: through the entry map while on the loop side (`!atEntry`), and
|
|
63
|
+
* with `x + 0` / `x - 0` / `x | 0` read as `x` — substituting a loop variable by its init constant
|
|
64
|
+
* turns ordinary index arithmetic into exactly these. */
|
|
65
|
+
function view(x0: Value, atEntry0: boolean): { x: Value; atEntry: boolean } {
|
|
66
|
+
let x = x0;
|
|
67
|
+
let atEntry = atEntry0;
|
|
68
|
+
for (;;) {
|
|
69
|
+
const mapped = atEntry ? undefined : entry.get(x);
|
|
70
|
+
if (mapped !== undefined) {
|
|
71
|
+
x = mapped;
|
|
72
|
+
atEntry = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const d = readable(x) ? defs.get(x) : undefined;
|
|
76
|
+
if (!d || d.operands.length !== 2 || !['add', 'sub', 'or'].includes(d.opcode)) {
|
|
77
|
+
return { x, atEntry };
|
|
78
|
+
}
|
|
79
|
+
if (constAt(d.operands[1], atEntry) === 0) {
|
|
80
|
+
x = d.operands[0];
|
|
81
|
+
} else if (normalize && d.opcode !== 'sub' && constAt(d.operands[0], atEntry) === 0) {
|
|
82
|
+
x = d.operands[1];
|
|
83
|
+
} else {
|
|
84
|
+
return { x, atEntry };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** A load's address as a base and a constant offset, the `+ c` of an `add` folded in. */
|
|
89
|
+
const address = (d: Op, atEntry0: boolean): { base: Value; off: number; atEntry: boolean } => {
|
|
90
|
+
const off = Number(d.attrs.off ?? 0);
|
|
91
|
+
const { x: base, atEntry } = view(d.operands[0], atEntry0);
|
|
92
|
+
const ad = readable(base) ? defs.get(base) : undefined;
|
|
93
|
+
if (ad?.opcode === 'add' && ad.operands.length === 2) {
|
|
94
|
+
for (const k of [0, 1]) {
|
|
95
|
+
const c = constAt(ad.operands[k], atEntry);
|
|
96
|
+
if (c !== null) {
|
|
97
|
+
return { base: ad.operands[1 - k], off: off + c, atEntry };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { base, off, atEntry };
|
|
102
|
+
};
|
|
103
|
+
const attrsOf = (d: Op, drop: string | null) =>
|
|
104
|
+
JSON.stringify(Object.fromEntries(Object.entries(d.attrs ?? {}).filter(([k]) => k !== drop)));
|
|
105
|
+
// memoised per (x, y, side): a value its own consumer reads twice would otherwise double the work
|
|
106
|
+
// at every level
|
|
107
|
+
const memo = new Map<string, boolean>();
|
|
108
|
+
const ids = new Map<Value, number>();
|
|
109
|
+
const id = (v: Value) => ids.get(v) ?? (ids.set(v, ids.size), ids.size - 1);
|
|
110
|
+
/** `da` (read with `atEntry`) and `db` (the entry's own) compute the same value. */
|
|
111
|
+
const sameOp = (da: Op, db: Op, opcodeOk: boolean, atEntry: boolean): boolean => {
|
|
112
|
+
if (!opcodeOk || da.operands.length !== db.operands.length) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (normalize && da.opcode === 'load' && db.opcode === 'load') {
|
|
116
|
+
const x = address(da, atEntry);
|
|
117
|
+
const y = address(db, true);
|
|
118
|
+
return x.off === y.off && attrsOf(da, 'off') === attrsOf(db, 'off') && same(x.base, x.atEntry, y.base);
|
|
119
|
+
}
|
|
120
|
+
if (attrsOf(da, null) !== attrsOf(db, null)) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
if (da.operands.every((o, i) => same(o, atEntry, db.operands[i]))) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
return (
|
|
127
|
+
normalize &&
|
|
128
|
+
COMMUTES.has(da.opcode) &&
|
|
129
|
+
da.operands.length === 2 &&
|
|
130
|
+
same(da.operands[0], atEntry, db.operands[1]) &&
|
|
131
|
+
same(da.operands[1], atEntry, db.operands[0])
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
function same(x0: Value, atEntry0: boolean, y0: Value): boolean {
|
|
135
|
+
const { x, atEntry } = view(x0, atEntry0);
|
|
136
|
+
const { x: y } = view(y0, true);
|
|
137
|
+
if (x === y) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
const key = `${id(x)},${id(y)},${atEntry ? 1 : 0}`;
|
|
141
|
+
const hit = memo.get(key);
|
|
142
|
+
if (hit !== undefined) {
|
|
143
|
+
return hit;
|
|
144
|
+
}
|
|
145
|
+
const da = readable(x) ? defs.get(x) : undefined;
|
|
146
|
+
const db = readable(y) ? defs.get(y) : undefined;
|
|
147
|
+
const r = !!da && !!db && sameOp(da, db, da.opcode === db.opcode, atEntry);
|
|
148
|
+
memo.set(key, r);
|
|
149
|
+
return r;
|
|
150
|
+
}
|
|
151
|
+
if (!negated) {
|
|
152
|
+
return same(a, false, b);
|
|
153
|
+
}
|
|
154
|
+
const { x, atEntry } = view(a, false);
|
|
155
|
+
const { x: y } = view(b, true);
|
|
156
|
+
const da = readable(x) ? defs.get(x) : undefined;
|
|
157
|
+
const db = readable(y) ? defs.get(y) : undefined;
|
|
158
|
+
return !!da && !!db && sameOp(da, db, NEGATED_ICMP[da.opcode] === db.opcode, atEntry);
|
|
159
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// asmlift IR — a double literal: the bit pattern the IR carries it as, and the C that spells it.
|
|
2
|
+
//
|
|
3
|
+
// THE IR CARRIES THE BITS, NOT A NUMBER, because a number has one value that is two doubles:
|
|
4
|
+
// `-0` prints and serializes as `0`, so a literal keyed or dumped by its number would be the
|
|
5
|
+
// other zero. Sixteen hex digits name exactly one double, NaN payloads included.
|
|
6
|
+
|
|
7
|
+
const view = new DataView(new ArrayBuffer(8));
|
|
8
|
+
|
|
9
|
+
/** The bit pattern, as sixteen hex digits, of the double whose high word (sign and exponent) is
|
|
10
|
+
* `hi` and low word is `lo`. */
|
|
11
|
+
export function doubleBits(hi: number, lo: number): string {
|
|
12
|
+
return (hi >>> 0).toString(16).padStart(8, '0') + (lo >>> 0).toString(16).padStart(8, '0');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Whether `bits` is a double's bit pattern: sixteen hex digits, as `doubleBits` spells one. A
|
|
16
|
+
* single's eight would name another number read as a double, so nothing here takes one. */
|
|
17
|
+
export const isDoubleBits = (bits: unknown): bits is string => typeof bits === 'string' && /^[0-9a-f]{16}$/.test(bits);
|
|
18
|
+
|
|
19
|
+
/** The double a bit pattern names. */
|
|
20
|
+
export function doubleOf(bits: string): number {
|
|
21
|
+
if (!isDoubleBits(bits)) {
|
|
22
|
+
throw new Error(`'${bits}' is not a double's bit pattern`);
|
|
23
|
+
}
|
|
24
|
+
view.setUint32(0, parseInt(bits.slice(0, 8), 16));
|
|
25
|
+
view.setUint32(4, parseInt(bits.slice(8, 16), 16));
|
|
26
|
+
return view.getFloat64(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The C spelling of a finite double: the shortest decimal that reads back as the same double
|
|
30
|
+
* (ECMAScript's Number::toString, whose digits are the fewest that do), with a `.0` where it has
|
|
31
|
+
* neither a point nor an exponent, so C reads a `double` and not an `int`, and `-0.0` for the
|
|
32
|
+
* negative zero.
|
|
33
|
+
*
|
|
34
|
+
* THAT IT READS BACK IS A PREMISE ABOUT THE COMPILER, and the one producer of a literal
|
|
35
|
+
* (`raise/floathelpers.ts`) runs only where the target states it
|
|
36
|
+
* (`compilerBehaviors.roundTripsDoubleLiterals`, which cites agbcc's source for it). */
|
|
37
|
+
export function doubleLiteral(bits: string): string {
|
|
38
|
+
const x = doubleOf(bits);
|
|
39
|
+
if (!Number.isFinite(x)) {
|
|
40
|
+
throw new Error(`the double ${bits} is not finite and has no C literal`);
|
|
41
|
+
}
|
|
42
|
+
if (Object.is(x, -0)) {
|
|
43
|
+
return '-0.0';
|
|
44
|
+
}
|
|
45
|
+
const s = String(x);
|
|
46
|
+
return /[.e]/.test(s) ? s : `${s}.0`;
|
|
47
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
// asmlift — natural-loop discovery: the pure CFG analysis the structurer consumes
|
|
1
|
+
// asmlift — natural-loop discovery: the pure CFG analysis the structurer consumes, and the raise
|
|
2
|
+
// passes that rewrite a loop's edges before it (`raise/latch.ts`, `raise/reroll.ts`).
|
|
3
|
+
//
|
|
4
|
+
// A substrate fact like dominance, so it lives here and not under `structure/`: that keeps the
|
|
5
|
+
// dependency between the levels one way — structure imports raise, never the reverse.
|
|
2
6
|
//
|
|
3
7
|
// Produces a DEFINITE header set computed structurally from back-edges — a block is a loop header
|
|
4
8
|
// iff it is a real back-edge target (`h ∈ dom(t)` for an edge `t→h`), never inferred from `cond_br`
|
|
@@ -8,7 +12,7 @@
|
|
|
8
12
|
//
|
|
9
13
|
// Dominance itself is a substrate fact and lives in `ir/core.ts`; `analyzeLoops` takes the map as
|
|
10
14
|
// a parameter, so a caller passes `dominators(fn)` from there.
|
|
11
|
-
import { Block, Fn, predecessors, successorsOf } from '
|
|
15
|
+
import { Block, Fn, predecessors, successorsOf } from './core';
|
|
12
16
|
|
|
13
17
|
/** One natural loop, keyed by its header. */
|
|
14
18
|
export interface NaturalLoop {
|
package/src/ir/opcodes.ts
CHANGED
|
@@ -13,8 +13,8 @@ export interface OpSig {
|
|
|
13
13
|
successors?: number | 'variadic';
|
|
14
14
|
requiredAttrs?: readonly string[];
|
|
15
15
|
/** observable side effect (memory write / call): never deleted when dead, never hoisted into
|
|
16
|
-
* an unconditional position. THE one effect vocabulary —
|
|
17
|
-
*
|
|
16
|
+
* an unconditional position. THE one effect vocabulary — every question ir/discipline.ts asks of an
|
|
17
|
+
* op derives from this flag and the two below. */
|
|
18
18
|
effects?: boolean;
|
|
19
19
|
/** reads memory. Deletable when dead — nothing observes a load nobody reads — but NOT movable:
|
|
20
20
|
* a load answers whichever stores ran before it, so crossing one changes the value it yields.
|
|
@@ -38,7 +38,14 @@ export const OPCODES = {
|
|
|
38
38
|
// Effect-free (no `effects` flag) so DCE reaps a dead one.
|
|
39
39
|
mulh: { operands: 2, results: 1 },
|
|
40
40
|
mulhu: { operands: 2, results: 1 },
|
|
41
|
-
neg: { operands: 1, results: 1 },
|
|
41
|
+
neg: { operands: 1, results: 1 }, // the wrapping negation, 0 - x as a 32-bit word
|
|
42
|
+
// The MATHEMATICAL negation: what Thumb's `cmn rA, rB` compares rA against when a signed branch
|
|
43
|
+
// reads it (frontend/thumb.ts `lowerTerminator`). It differs from `neg` at exactly one operand,
|
|
44
|
+
// 0x80000000, where its value (+2^31) is no 32-bit word at all. A separate opcode rather than an
|
|
45
|
+
// attr on `neg` because every word evaluator in core folds `neg` as the wrapping one, and an
|
|
46
|
+
// attr each of them must remember to read fails OPEN: one that does not know `neg_s` falls to
|
|
47
|
+
// its default arm and declines. Only the structurer spells it, as `-(s32)x`.
|
|
48
|
+
neg_s: { operands: 1, results: 1 },
|
|
42
49
|
not: { operands: 1, results: 1 }, // bitwise complement (`mvn`) → ~x
|
|
43
50
|
or: { operands: 2, results: 1 },
|
|
44
51
|
and: { operands: 2, results: 1 },
|
|
@@ -109,6 +116,74 @@ export const OPCODES = {
|
|
|
109
116
|
// which recompiles to the branch diamond the source emitted.
|
|
110
117
|
logic_and: { operands: 2, results: 1 },
|
|
111
118
|
logic_or: { operands: 2, results: 1 },
|
|
119
|
+
// --- a 64-bit value and its two halves ---
|
|
120
|
+
// A 64-bit integer is ONE value of `{kind:'int'|'unknown', width:64}`, never two results and never
|
|
121
|
+
// a new `IrType` kind. The rule that decides it: for TYPES, widen the number that is already
|
|
122
|
+
// there; for OPERATIONS, add a new opcode. Existing code tests `t.kind === 'int'` and reads
|
|
123
|
+
// `t.width`, so a wider width makes a pass that is wrong about it fail LOUDLY, while a new kind
|
|
124
|
+
// would make the same pass skip SILENTLY. Existing code switches on `op.opcode`, so a new opcode
|
|
125
|
+
// makes an existing pass skip, which is the safe direction there.
|
|
126
|
+
//
|
|
127
|
+
// So the arithmetic opcodes above gain NOTHING: `add`, `sub`, `mul`, the shifts and the `icmp_*`
|
|
128
|
+
// family carry the width in their operand types. Only the three ops that have no 32-bit spelling
|
|
129
|
+
// are new — building a 64-bit value out of two halves, and reading either half back.
|
|
130
|
+
//
|
|
131
|
+
// `concat lo, hi` is TRANSIENT the way `mulh` is: it renders only in the shapes something above
|
|
132
|
+
// it recognises (a widen, a literal, an ABI pair), and one that survives to the structurer is a
|
|
133
|
+
// loud gap. `lo32`/`hi32` invert that property and are PERMANENT, because C spells them exactly —
|
|
134
|
+
// `(u32)v` and `(u32)(v >> 32)`.
|
|
135
|
+
//
|
|
136
|
+
// WHY `hi32` AND NOT `lo32(shr_u v {imm:32})`, which is the same value in two ops rather than
|
|
137
|
+
// three: spelling a projection as a 64-bit SHIFT puts a 64-bit arithmetic op into the IR at every
|
|
138
|
+
// pair site, and hands it to every pass that matches a shift — `raise/divpow2.ts` on
|
|
139
|
+
// `shr_s {imm}`, `raise/extscale.ts` on `shl`, `raise/narrow.ts` on a narrow/re-widen pair. Those
|
|
140
|
+
// passes would then be REACHED by a 64-bit operand instead of skipping it. A third opcode nothing
|
|
141
|
+
// else matches is what buys the isolation.
|
|
142
|
+
concat: { operands: 2, results: 1 }, // concat lo, hi → the 64-bit value whose low half is `lo`
|
|
143
|
+
lo32: { operands: 1, results: 1 },
|
|
144
|
+
hi32: { operands: 1, results: 1 },
|
|
145
|
+
// --- hardware floating point ---
|
|
146
|
+
// IEEE arithmetic on `{kind:'float'}` values, and on nothing else (ir/verify.ts checks both
|
|
147
|
+
// directions). NEW OPCODES rather than `add`/`sub` over a float-typed operand, by the rule the
|
|
148
|
+
// 64-bit section above states for OPERATIONS: every pass that matches `add` is an integer rewrite
|
|
149
|
+
// — a pointer walk, a reassociation, a constant fold, an idiom pattern — and a new opcode is what
|
|
150
|
+
// makes each of them skip. Pure: under the default IEEE environment none of them traps, so a
|
|
151
|
+
// dead one is reaped like any integer op.
|
|
152
|
+
fadd: { operands: 2, results: 1 },
|
|
153
|
+
fsub: { operands: 2, results: 1 },
|
|
154
|
+
fmul: { operands: 2, results: 1 },
|
|
155
|
+
fdiv: { operands: 2, results: 1 },
|
|
156
|
+
fneg: { operands: 1, results: 1 },
|
|
157
|
+
// A double LITERAL: `bits` is its IEEE pattern as sixteen hex digits (ir/float-bits.ts). Its own
|
|
158
|
+
// opcode rather than a `const` over a float type for the reason the arithmetic above has its own:
|
|
159
|
+
// every pass that matches `const` is an integer fold.
|
|
160
|
+
fconst: { operands: 0, results: 1, requiredAttrs: ['bits'] },
|
|
161
|
+
// The CROSSINGS between a float and an integer, C's conversions: `fptosi`/`fptoui` truncate toward
|
|
162
|
+
// zero into a signed or unsigned integer of the result's width (32 or 64), `sitofp` converts a
|
|
163
|
+
// signed one, `fpext`/`fptrunc` move between the precisions. The only ops a float and an integer
|
|
164
|
+
// meet in (ir/verify.ts). No `uitofp`: no compiler here converts an unsigned integer in one step
|
|
165
|
+
// (agbcc open-codes it around `__floatsisf`), so it would be an opcode with no producer.
|
|
166
|
+
fptosi: { operands: 1, results: 1 },
|
|
167
|
+
fptoui: { operands: 1, results: 1 },
|
|
168
|
+
sitofp: { operands: 1, results: 1 },
|
|
169
|
+
fpext: { operands: 1, results: 1 },
|
|
170
|
+
fptrunc: { operands: 1, results: 1 },
|
|
171
|
+
// The float COMPARES, a 0/1 integer out of two floats of one type. Ten and not six, because
|
|
172
|
+
// NaN makes C's relations a partial order: `!(a > b)` is true on a NaN and `a <= b` is not, and
|
|
173
|
+
// every compiler here compiles the two differently. So each ordered relation (`o`, false on a
|
|
174
|
+
// NaN) has its logical negation as an unordered opcode (`u`, true on a NaN): `fcmp_ule` IS
|
|
175
|
+
// `!(a > b)`. C's `!=` is `fcmp_une`. They stay out of `NEGATED_ICMP`, whose integer negation
|
|
176
|
+
// (`<` ↔ `>=`) would be the wrong program here; `NEGATED_FCMP` below is theirs.
|
|
177
|
+
fcmp_oeq: { operands: 2, results: 1 },
|
|
178
|
+
fcmp_une: { operands: 2, results: 1 },
|
|
179
|
+
fcmp_olt: { operands: 2, results: 1 },
|
|
180
|
+
fcmp_uge: { operands: 2, results: 1 },
|
|
181
|
+
fcmp_ole: { operands: 2, results: 1 },
|
|
182
|
+
fcmp_ugt: { operands: 2, results: 1 },
|
|
183
|
+
fcmp_ogt: { operands: 2, results: 1 },
|
|
184
|
+
fcmp_ule: { operands: 2, results: 1 },
|
|
185
|
+
fcmp_oge: { operands: 2, results: 1 },
|
|
186
|
+
fcmp_ult: { operands: 2, results: 1 },
|
|
112
187
|
// --- memory ---
|
|
113
188
|
load: { operands: 1, results: 1, requiredAttrs: ['off', 'width', 'signed'], reads: true },
|
|
114
189
|
store: { operands: 2, results: 0, requiredAttrs: ['off', 'width'], effects: true },
|
|
@@ -119,8 +194,10 @@ export const OPCODES = {
|
|
|
119
194
|
aload: { operands: 2, results: 1, requiredAttrs: ['elemSize', 'signed'], reads: true }, // aload base, index
|
|
120
195
|
astore: { operands: 3, results: 0, requiredAttrs: ['elemSize'], effects: true }, // astore base, index, value
|
|
121
196
|
// --- call: operands are the argument values (r0..), result is the return value (r0),
|
|
122
|
-
// `target` attr is the callee symbol
|
|
123
|
-
|
|
197
|
+
// `target` attr is the callee symbol — or, with `indirect: true` and no `target`, the
|
|
198
|
+
// callee is the address in the LAST operand (ir/core.ts `calleeValue`). Exactly one of the
|
|
199
|
+
// two, which `ir/verify.ts` checks. Caller-saved clobbering is implicit. ---
|
|
200
|
+
call: { operands: 'variadic', results: 1, effects: true },
|
|
124
201
|
// The ADDRESS of a named global (agbcc `ldr rD, .Lpool` where the pool word is `.word gSym`).
|
|
125
202
|
// Pure, 0 operands. Globals come from the project headers, so they are referenced by name, never
|
|
126
203
|
// declared as locals. The structurer lowers it three ways (see scalarGlobals in structure.ts):
|
|
@@ -134,10 +211,17 @@ export const OPCODES = {
|
|
|
134
211
|
// proving every access agrees, and the structurer declares the local and renders `&name` exactly
|
|
135
212
|
// as it renders a gaddr's `&sym`. Operand-free and pure, so GVN numbers it like gaddr and a dead
|
|
136
213
|
// one is reaped.
|
|
137
|
-
// `width`/`signed` are stamped by the frontend's frame-object AUDIT — requiring them
|
|
138
|
-
// "the audit ran" a verifier-checkable fact instead of a convention: a frontend that emits
|
|
139
|
-
// laddr and skips the audit fails verify loudly instead of rendering `&undefined`.
|
|
140
|
-
|
|
214
|
+
// `width`/`signed`/`count` are stamped by the frontend's frame-object AUDIT — requiring them
|
|
215
|
+
// makes "the audit ran" a verifier-checkable fact instead of a convention: a frontend that emits
|
|
216
|
+
// a laddr and skips the audit fails verify loudly instead of rendering `&undefined`. The SHAPE
|
|
217
|
+
// the three of them encode is checked there too (ir/verify.ts), because the attrs are
|
|
218
|
+
// disjunctive and only `count` tells the two arms apart.
|
|
219
|
+
// The object is `count` elements of `width` bytes, so its storage spans `width * count`. A
|
|
220
|
+
// SCALAR is count 1, typed by the access width and signedness every access agreed on. Count
|
|
221
|
+
// above 1 is STORAGE the audit sized without typing — no access pins an element type, so it is
|
|
222
|
+
// width 1, unsigned, and declares as `u8 name[count]`; nothing else may mint one, because the
|
|
223
|
+
// only thing that licenses an untyped object is the frame accounting the audit solves.
|
|
224
|
+
laddr: { operands: 0, results: 1, requiredAttrs: ['off', 'width', 'signed', 'count'] },
|
|
141
225
|
// An UNDEFINED value: a read of storage that carries no INPUT — nothing was entitled to hand this
|
|
142
226
|
// function a value there, and none of its own stores reached it on this path. Deliberately NOT
|
|
143
227
|
// "storage nobody could have written": a callee-saved register holds the CALLER's value at entry,
|
|
@@ -153,7 +237,7 @@ export const OPCODES = {
|
|
|
153
237
|
// merely "owns the storage", because a frame the function owns can still be written by
|
|
154
238
|
// someone else once an address into it escapes to a callee, which fills a wider object than
|
|
155
239
|
// any in-function access reveals. Whoever mints one owes the retraction on escape
|
|
156
|
-
// (
|
|
240
|
+
// (the frame-object audit, frontend/frame-objects.ts).
|
|
157
241
|
// • a REGISTER the ABI does not pass arguments in AND this function's prologue SAVED cannot
|
|
158
242
|
// carry a value a caller handed over, and has no address for anything else to reach it by, so
|
|
159
243
|
// there is nothing to retract. The save is half of the premise, not a corroboration of it:
|
|
@@ -163,13 +247,16 @@ export const OPCODES = {
|
|
|
163
247
|
// live-in, and a live-in of the entry block is a PARAMETER — right for an argument register, a
|
|
164
248
|
// fabricated argument for anything else.
|
|
165
249
|
//
|
|
166
|
-
// Operand-free and pure like `laddr`, and out of raise/gvn.ts's NUMBERABLE set
|
|
167
|
-
// it
|
|
168
|
-
//
|
|
250
|
+
// Operand-free and pure like `laddr`, and out of raise/gvn.ts's NUMBERABLE set, which must keep
|
|
251
|
+
// it out: two undefs of one key are a storage's content and a `copied` move of it, and numbering
|
|
252
|
+
// them one value would erase the move.
|
|
169
253
|
//
|
|
170
254
|
// `key` names the storage (`sp@0`, `r4`); the structurer reads it to name the local
|
|
171
255
|
// (`uninit_sp0`) and emits NO assignment — that absence is the recovery, and an edge argument
|
|
172
|
-
// that is one emits no copy either (structure.ts undefCarriesNothing).
|
|
256
|
+
// that is one emits no copy either (structure.ts undefCarriesNothing). OPTIONAL `copied: true`:
|
|
257
|
+
// the value another storage's content arrives at a join keyed elsewhere as (`if (c) x = y;` of
|
|
258
|
+
// two uninitialised locals, frontend/ssa.ts `markCopiedUndefs`) — the machine moved it, so that
|
|
259
|
+
// edge copy IS emitted, under the name of the undef its key names.
|
|
173
260
|
undef: { operands: 0, results: 1, requiredAttrs: ['key'] },
|
|
174
261
|
// --- black-box escape hatch (keeps lifting total) ---
|
|
175
262
|
// `effects: true`: an instruction asmlift could not model may do anything — write memory, trap,
|
|
@@ -192,6 +279,11 @@ export const OPCODES = {
|
|
|
192
279
|
// Many-way switch dispatch (Regime B, jump table). The single operand is the scrutinee;
|
|
193
280
|
// successors are the N case blocks followed by the default block (the LAST successor);
|
|
194
281
|
// `cases` is the index-aligned list of the first N successors' case values.
|
|
282
|
+
// `exhaustive: true` — OPTIONAL, set by a frontend whose dispatch block is entered only past a
|
|
283
|
+
// bounds test that already sent every value outside `cases` elsewhere, so the default edge is
|
|
284
|
+
// never taken. It still names a block, because the op needs a last successor; the structurer
|
|
285
|
+
// reads the attr to emit no `default:` arm for it. A pass that rebuilds the op leaves it off and
|
|
286
|
+
// gets the default arm back, which is the conservative answer.
|
|
195
287
|
switch_br: { operands: 1, results: 0, terminator: true, successors: 'variadic', requiredAttrs: ['cases'] },
|
|
196
288
|
} as const satisfies Record<string, OpSig>;
|
|
197
289
|
|
|
@@ -199,13 +291,87 @@ export const OPCODES = {
|
|
|
199
291
|
* does not. */
|
|
200
292
|
export type Opcode = keyof typeof OPCODES;
|
|
201
293
|
|
|
294
|
+
/** The width of a 64-bit IR value, named once so the three places that build one and the verifier
|
|
295
|
+
* that checks them cannot come to disagree about it. */
|
|
296
|
+
export const WIDE_BITS = 64;
|
|
297
|
+
|
|
202
298
|
/** The widths a `zext`/`sext` carries — a fact about those two opcodes, so it lives with them
|
|
203
299
|
* rather than re-declared per consumer (raise/narrow.ts pairs a narrowing op with its re-widening,
|
|
204
300
|
* raise/paramwidth.ts declares a parameter at one). Every frontend that produces the pair produces
|
|
205
301
|
* one of these: agbcc's gated shift-pair fold (pattern/engine.ts CAST_PATTERNS) and PPC's
|
|
206
|
-
* `extsb`/`extsh` (frontend/ppc.ts). A third width would be a C type the backend cannot spell.
|
|
302
|
+
* `extsb`/`extsh` (frontend/ppc.ts). A third width would be a C type the backend cannot spell.
|
|
303
|
+
*
|
|
304
|
+
* 32 IS NOT ONE OF THEM, and the widening to 64 does not come here. The machine spells that widen
|
|
305
|
+
* as a PAIR — agbcc `asr rN,rM,#31` or `mov rN,#0`, PPC `srawi`, MIPS `sra` — so it is a `concat`
|
|
306
|
+
* shape (`concat(x, shr_s(x,31))` signed, `concat(x, const 0)` unsigned), which is also the only
|
|
307
|
+
* form that says HOW the half was widened. This set keeps its one job: refuse an extension with no
|
|
308
|
+
* C spelling. */
|
|
207
309
|
export const CAST_WIDTHS: ReadonlySet<number> = new Set([8, 16]);
|
|
208
310
|
|
|
311
|
+
/** The float opcodes: the only ops a `{kind:'float'}` value may be an operand or a result of,
|
|
312
|
+
* besides `ret` (ir/verify.ts). Authored beside the registry because no signature field says what
|
|
313
|
+
* an op computes ON. */
|
|
314
|
+
export const FLOAT_OPS: ReadonlySet<string> = new Set(['fadd', 'fsub', 'fmul', 'fdiv', 'fneg', 'fconst']);
|
|
315
|
+
|
|
316
|
+
/** The conversions out of a float into an integer, and into a float out of an integer. */
|
|
317
|
+
export const FLOAT_TO_INT: ReadonlySet<string> = new Set(['fptosi', 'fptoui']);
|
|
318
|
+
export const INT_TO_FLOAT: ReadonlySet<string> = new Set(['sitofp']);
|
|
319
|
+
/** The conversions between the two precisions, each named by the width it converts from and to. */
|
|
320
|
+
export const FLOAT_RESIZE: Readonly<Record<string, { from: 32 | 64; to: 32 | 64 }>> = {
|
|
321
|
+
fpext: { from: 32, to: 64 },
|
|
322
|
+
fptrunc: { from: 64, to: 32 },
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
/** The logical negation of each float compare, NaN included (the registry's note on `fcmp_*`):
|
|
326
|
+
* expanded both ways from the pairs, as `NEGATED_ICMP` is. */
|
|
327
|
+
const FCMP_NEGATION_PAIRS: readonly (readonly [Opcode, Opcode])[] = [
|
|
328
|
+
['fcmp_oeq', 'fcmp_une'],
|
|
329
|
+
['fcmp_olt', 'fcmp_uge'],
|
|
330
|
+
['fcmp_ole', 'fcmp_ugt'],
|
|
331
|
+
['fcmp_ogt', 'fcmp_ule'],
|
|
332
|
+
['fcmp_oge', 'fcmp_ult'],
|
|
333
|
+
];
|
|
334
|
+
export const NEGATED_FCMP: Readonly<Record<string, Opcode>> = Object.fromEntries(
|
|
335
|
+
FCMP_NEGATION_PAIRS.flatMap(([a, b]) => [
|
|
336
|
+
[a, b],
|
|
337
|
+
[b, a],
|
|
338
|
+
]),
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
/** What each float compare answers when its first operand is LESS than, EQUAL to or GREATER than
|
|
342
|
+
* its second, and when the two are UNORDERED (either is a NaN): the definition of the ten, from
|
|
343
|
+
* which a compare a runtime computes some other way is read (raise/floathelpers.ts). */
|
|
344
|
+
export const FCMP_TRUTH: Readonly<Record<string, readonly [lt: boolean, eq: boolean, gt: boolean, un: boolean]>> = {
|
|
345
|
+
fcmp_olt: [true, false, false, false],
|
|
346
|
+
fcmp_ole: [true, true, false, false],
|
|
347
|
+
fcmp_ogt: [false, false, true, false],
|
|
348
|
+
fcmp_oge: [false, true, true, false],
|
|
349
|
+
fcmp_oeq: [false, true, false, false],
|
|
350
|
+
fcmp_une: [true, false, true, true],
|
|
351
|
+
fcmp_uge: [false, true, true, true],
|
|
352
|
+
fcmp_ugt: [false, false, true, true],
|
|
353
|
+
fcmp_ule: [true, true, false, true],
|
|
354
|
+
fcmp_ult: [true, false, false, true],
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
/** The float compare whose truth table (`FCMP_TRUTH`'s order: less, equal, greater, unordered) is
|
|
358
|
+
* `truth`, or undefined where none is — always true, say. The one reader of the table backwards,
|
|
359
|
+
* for every place that computes a compare's answers some other way (agbcc's helper tests,
|
|
360
|
+
* raise/floathelpers.ts; PowerPC's `cror` of two bits, frontend/ppc.ts). */
|
|
361
|
+
export function fcmpOfTruth(truth: readonly boolean[]): Opcode | undefined {
|
|
362
|
+
return Object.keys(FCMP_TRUTH).find((k) => FCMP_TRUTH[k].every((t, i) => t === truth[i])) as Opcode | undefined;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Every op that computes on a float: the arithmetic, the conversions and the compares. A runtime
|
|
366
|
+
* helper whose op is one of these is a float helper (runtime-helpers.ts `isFloatHelper`). */
|
|
367
|
+
export const FLOAT_COMPUTING: ReadonlySet<string> = new Set([
|
|
368
|
+
...FLOAT_OPS,
|
|
369
|
+
...FLOAT_TO_INT,
|
|
370
|
+
...INT_TO_FLOAT,
|
|
371
|
+
...Object.keys(FLOAT_RESIZE),
|
|
372
|
+
...Object.keys(NEGATED_FCMP),
|
|
373
|
+
]);
|
|
374
|
+
|
|
209
375
|
/** Signature lookup by RUNTIME opcode string (Op.opcode is a plain string — IR consumers switch
|
|
210
376
|
* on it); undefined for an unregistered opcode. */
|
|
211
377
|
export function opSig(opcode: string): OpSig | undefined {
|
|
@@ -214,21 +380,20 @@ export function opSig(opcode: string): OpSig | undefined {
|
|
|
214
380
|
|
|
215
381
|
/** The comparison whose result is the logical NEGATION of each `icmp_*` — `!(a < b)` is `a >= b`.
|
|
216
382
|
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
383
|
+
* AUTHORED data seated beside the registry, not a view derived from it: nothing in `OPCODES`
|
|
384
|
+
* states which comparison opposes which. What is
|
|
219
385
|
* derived is its SYMMETRY — the five involutive pairs are expanded both ways, so `neg(neg(c)) === c`
|
|
220
386
|
* holds by construction (a hand-written map is one typo away from breaking it, and the symptom is a
|
|
221
387
|
* plainly inverted condition in the emitted C). Completeness against the icmp family is the part
|
|
222
388
|
* construction cannot give, so a test asserts it (test/pattern.test.ts) — an eleventh comparison
|
|
223
389
|
* added to `OPCODES` would otherwise degrade its consumers several different ways.
|
|
224
390
|
*
|
|
225
|
-
* It lives here
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
* two tables are not candidates for further consolidation. */
|
|
391
|
+
* It lives here so that every consumer that has to say "the opposite of this compare" reads THIS
|
|
392
|
+
* one, and they cannot drift apart the way inline copies did. Two adjacent facts worth knowing: raise/
|
|
393
|
+
* shortcircuit.ts derives its `BOOL_OPS` from these keys and `NEGATED_FCMP`'s (asserting
|
|
394
|
+
* negatable compare == boolean-op, true today), and l3/ast.ts `NEGATE_REL` is the SAME relation
|
|
395
|
+
* over the neutral L3 operator vocabulary — deliberately separate, because signedness lives in the
|
|
396
|
+
* operand types there, so the two tables are not candidates for further consolidation. */
|
|
232
397
|
const ICMP_NEGATION_PAIRS: readonly (readonly [Opcode, Opcode])[] = [
|
|
233
398
|
['icmp_eq', 'icmp_ne'],
|
|
234
399
|
['icmp_slt', 'icmp_sge'],
|
|
@@ -243,103 +408,17 @@ export const NEGATED_ICMP: Readonly<Record<string, Opcode>> = Object.fromEntries
|
|
|
243
408
|
]),
|
|
244
409
|
);
|
|
245
410
|
|
|
246
|
-
/** Ops with an observable side effect: the flag on the signature, derived rather than re-listed.
|
|
247
|
-
* `isDceSafe` asks the same question of the FLAG through `opSig` rather than of this set, so the
|
|
248
|
-
* two cannot disagree. The SET's own consumers are `HOIST_UNSAFE_OPS` below, structure.ts's
|
|
249
|
-
* block-purity tests (may this block be folded into a loop header, is this exit owned),
|
|
250
|
-
* analysis.ts's memory-write barrier, divpow2's bias block (which is DELETED rather than moved),
|
|
251
|
-
* and the idiom layer's de-sequencing guard (pattern/engine.ts). Derived rather than re-listed
|
|
252
|
-
* because three of those consumers each carried a hand-written copy of this membership, which is
|
|
253
|
-
* how the models drifted apart. */
|
|
254
|
-
export const EFFECTFUL_OPS: ReadonlySet<string> = new Set(
|
|
255
|
-
(Object.keys(OPCODES) as Opcode[]).filter((k) => (OPCODES[k] as OpSig).effects),
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
/** Ops that may not be SPECULATED — run on a path that did not run them before. Identical to
|
|
259
|
-
* `EFFECTFUL_OPS`, and kept as its own name because its call sites ask the speculation question
|
|
260
|
-
* rather than the deletion one.
|
|
261
|
-
*
|
|
262
|
-
* A memory read is deliberately absent, and that is the one entry worth arguing: its only consumer
|
|
263
|
-
* is raise/shortcircuit.ts, which hoists an arm's body into the block above, and the structurer
|
|
264
|
-
* inlines an unnamed value back into the `&&`/`||` right-hand side, where C's own short-circuit
|
|
265
|
-
* re-guards it. Adding the two reads
|
|
266
|
-
* here cost three byte-matches (kleod:UpdateHUDCounterDisplay, retired 2026-09-13 with its source,
|
|
267
|
-
* plus synthetic:breakloop and synthetic:strcmp1), so the argument was load-bearing rather than
|
|
268
|
-
* merely plausible. Not re-measured since the kleod row's retirement.
|
|
269
|
-
*
|
|
270
|
-
* KNOWN GAP: the trapping divides are absent too, and there the re-guard argument does NOT carry
|
|
271
|
-
* — a hoisted `sdiv` that the structurer NAMES becomes an unconditional statement. Left as it is
|
|
272
|
-
* because closing it is a separate change with its own measurement; `REEVAL_UNSAFE_OPS` does
|
|
273
|
-
* refuse them, so the pre-update sink is not exposed to it.
|
|
274
|
-
*
|
|
275
|
-
* AND THE EXEMPTION IS NOT TRANSFERABLE, which is worth saying beside it: a second consumer once
|
|
276
|
-
* read this set to answer "would gcc have SPECULATED this arm above a compare", where nothing
|
|
277
|
-
* re-guards anything, and admitted single-load arms `gcc/jump.c:483`'s `! may_trap_p` refuses.
|
|
278
|
-
* Pointing that consumer back at this set costs `synthetic:mergeldcast:agbcc` its byte-match
|
|
279
|
-
* (MATCH -> diff:6), measured. That consumer is `raise/narrowlocal.ts` and it reads
|
|
280
|
-
* `REEVAL_UNSAFE_OPS` instead. Reach for this set only when the argument above — a C-level
|
|
281
|
-
* re-guard at the new point — actually holds at your call site. */
|
|
282
|
-
export const HOIST_UNSAFE_OPS: ReadonlySet<string> = EFFECTFUL_OPS;
|
|
283
|
-
|
|
284
411
|
/** The ops whose `operands[0]` is a memory-access BASE: `load base`, `store base, value`,
|
|
285
412
|
* `aload base, index`, `astore base, index, value` (the operand roles are in the registry above).
|
|
286
413
|
* Authored rather than derived — no registry field records the operand ROLE, and the only other
|
|
287
414
|
* memory-touching opcode is `call`, whose variadic operands are arguments and not a base.
|
|
288
415
|
*
|
|
289
|
-
*
|
|
290
|
-
* is here and not next to
|
|
291
|
-
* model,
|
|
416
|
+
* Three consumers ask three different questions of it and all need the same answer, which is why
|
|
417
|
+
* it is here and not next to any: `structure/analysis.ts` uses it for the address-home variation's
|
|
418
|
+
* slot model, `raise/const.ts` to recognise a folded literal that IS an address, and
|
|
419
|
+
* `ir/discipline.ts` to tell a device access's `volatile` from a frame object's. */
|
|
292
420
|
export const MEM_BASE_OPS: ReadonlySet<string> = new Set(['load', 'store', 'aload', 'astore']);
|
|
293
421
|
|
|
294
|
-
/** Ops whose answer depends on WHERE they run: an effect (its order against other effects is
|
|
295
|
-
* observable) or a memory read (it answers whichever stores ran before it). The question a pass
|
|
296
|
-
* asks before moving a computation to another point on the SAME path. `SPELLED_WHEN_DEAD_OPS`
|
|
297
|
-
* below asks a DIFFERENT question and derives, today, the same set. */
|
|
298
|
-
export const ORDER_SENSITIVE_OPS: ReadonlySet<string> = new Set(
|
|
299
|
-
(Object.keys(OPCODES) as Opcode[]).filter((k) => {
|
|
300
|
-
const sig = OPCODES[k] as OpSig;
|
|
301
|
-
return sig.effects || sig.reads;
|
|
302
|
-
}),
|
|
303
|
-
);
|
|
304
|
-
|
|
305
|
-
/** Ops the structurer must still SPELL when nothing consumes their result — an effect, or a memory
|
|
306
|
-
* READ. Consumer: structure.ts's `sideEffects` walk.
|
|
307
|
-
*
|
|
308
|
-
* The read half is the entry worth arguing, because `reads` documents the OPPOSITE about C — a
|
|
309
|
-
* load nobody reads is deletable, nothing observes it. That is the C claim. The COMPILER claim
|
|
310
|
-
* points the other way: an optimizing compiler deletes every dead read it is ALLOWED to delete, so
|
|
311
|
-
* one still in the target is evidence the source's access was `volatile`. Membership here only
|
|
312
|
-
* says the structurer may not drop the op silently; whether a `volatile` can actually reach the
|
|
313
|
-
* access is a second, ADDRESS-level question the call site asks separately
|
|
314
|
-
* (structure.ts `volatileQualifiable`), and a read it answers no to is dropped as before.
|
|
315
|
-
*
|
|
316
|
-
* DERIVED FROM THE REGISTRY, not aliased to `ORDER_SENSITIVE_OPS`, even though the two are
|
|
317
|
-
* extensionally identical today and `HOIST_UNSAFE_OPS` above does alias `EFFECTFUL_OPS`. An alias
|
|
318
|
-
* makes two DIFFERENT questions incapable of ever differing, so the day one of them acquires an
|
|
319
|
-
* opcode the other should not have, the edit lands on both silently. The identity is pinned by a
|
|
320
|
-
* test instead (test/pattern.test.ts), where a future divergence surfaces as a decision to make
|
|
321
|
-
* rather than a coupling nobody sees. */
|
|
322
|
-
export const SPELLED_WHEN_DEAD_OPS: ReadonlySet<string> = new Set(
|
|
323
|
-
(Object.keys(OPCODES) as Opcode[]).filter((k) => {
|
|
324
|
-
const sig = OPCODES[k] as OpSig;
|
|
325
|
-
return sig.effects || sig.reads;
|
|
326
|
-
}),
|
|
327
|
-
);
|
|
328
|
-
|
|
329
|
-
/** Ops that may not be RE-EVALUATED at another program point — order-sensitive, or trapping. The
|
|
330
|
-
* trap half is what separates this from `ORDER_SENSITIVE_OPS`: it only matters when the new point
|
|
331
|
-
* can be reached on a path the old one was not, so a consumer that merely re-orders on one path
|
|
332
|
-
* wants the smaller set. Both are needed because the two consumers differ on exactly the divides:
|
|
333
|
-
* a collapsed switch re-renders a test block's ops AT THEIR USES, and every use is dominated by
|
|
334
|
-
* the def, so it evaluates them on a SUBSET of the original paths — a narrowing, never a
|
|
335
|
-
* speculation. */
|
|
336
|
-
export const REEVAL_UNSAFE_OPS: ReadonlySet<string> = new Set(
|
|
337
|
-
(Object.keys(OPCODES) as Opcode[]).filter((k) => {
|
|
338
|
-
const sig = OPCODES[k] as OpSig;
|
|
339
|
-
return sig.effects || sig.reads || sig.traps;
|
|
340
|
-
}),
|
|
341
|
-
);
|
|
342
|
-
|
|
343
422
|
/** Ops that MATERIALIZE a value out of nothing: no operands, no state read, no effect, no trap, no
|
|
344
423
|
* control flow — so where one sits in a block says nothing about what ran before it. Derived, so a
|
|
345
424
|
* future pure nullary opcode joins without a second edit. Consumed by raise/paramwidth.ts, whose
|
|
@@ -352,10 +431,3 @@ export const MATERIALIZING_OPS: ReadonlySet<string> = new Set(
|
|
|
352
431
|
return sig.operands === 0 && !sig.terminator && !sig.effects && !sig.reads && !sig.traps;
|
|
353
432
|
}),
|
|
354
433
|
);
|
|
355
|
-
|
|
356
|
-
/** May a dead result of this opcode be deleted? Registered, no observable effects, not control
|
|
357
|
-
* flow. `opaque` is excluded via its `effects` flag — see the note on its signature. */
|
|
358
|
-
export function isDceSafe(opcode: string): boolean {
|
|
359
|
-
const sig = opSig(opcode);
|
|
360
|
-
return !!sig && !sig.effects && !sig.terminator;
|
|
361
|
-
}
|