@asmlift/core 0.8.1 → 0.9.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 +34 -30
- package/package.json +1 -1
- package/src/aggregate.ts +248 -0
- package/src/backend/c.ts +5 -2
- package/src/backend/cfamily.ts +236 -37
- package/src/backend/cpp.ts +114 -9
- package/src/backend/pascal.ts +39 -0
- 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 +615 -79
- package/src/declare.ts +75 -5
- package/src/frontend/asmdata.ts +19 -5
- package/src/frontend/call-plan.ts +686 -0
- package/src/frontend/device-pins.ts +277 -0
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +167 -0
- package/src/frontend/frame-objects.ts +2131 -0
- package/src/frontend/high-half.ts +2 -1
- package/src/frontend/local-object.ts +490 -0
- package/src/frontend/mips.ts +363 -40
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/ppc.ts +520 -129
- package/src/frontend/reloc-symbol.ts +94 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +669 -76
- package/src/frontend/stackargs.ts +120 -39
- package/src/frontend/thumb.ts +2764 -2018
- package/src/ir/alias.ts +29 -0
- package/src/ir/core.ts +101 -3
- package/src/ir/discipline.ts +248 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/ir/opcodes.ts +84 -112
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +118 -2
- package/src/ir/verify.ts +150 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +186 -14
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/dce.ts +32 -10
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +27 -9
- package/src/l3/intcell.ts +97 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +218 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +3 -2
- package/src/l3/ptrcell.ts +357 -0
- package/src/l3/regspell.ts +17 -3
- package/src/l3/reindex.ts +10 -7
- package/src/l3/symbol-refs.ts +104 -23
- package/src/l3/tailmerge.ts +6 -19
- package/src/l3/typing.ts +137 -25
- package/src/l3/unreduce.ts +4 -4
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +11 -1
- package/src/pattern/engine.ts +19 -16
- package/src/pipeline.ts +57 -12
- package/src/proto-context.ts +788 -0
- package/src/proto.ts +689 -50
- package/src/raise/arrays.ts +4 -3
- package/src/raise/declared-volatile.ts +131 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +243 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +26 -5
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +510 -0
- package/src/raise/pairparams.ts +81 -0
- package/src/raise/paramwidth.ts +7 -0
- package/src/raise/pre-recovery.ts +78 -4
- package/src/raise/recover.ts +14 -10
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +41 -19
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/struct-arrays.ts +5 -4
- package/src/raise/structs.ts +311 -98
- package/src/raise/truncload.ts +366 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +30 -5
- package/src/rank-variations.ts +36 -8
- package/src/rank.ts +267 -79
- package/src/runtime-helpers.ts +287 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1285 -448
- package/src/structure/hazards.ts +633 -58
- package/src/structure/local-statics.ts +381 -0
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +1772 -395
- package/src/structure/switch-recover.ts +159 -78
- package/src/symbols.ts +38 -3
- package/src/target.ts +662 -59
- package/src/trace.ts +44 -24
- package/src/variation-definitions.ts +119 -1
- package/src/variation-tokens.ts +20 -6
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.
|
|
@@ -109,6 +109,48 @@ export const OPCODES = {
|
|
|
109
109
|
// which recompiles to the branch diamond the source emitted.
|
|
110
110
|
logic_and: { operands: 2, results: 1 },
|
|
111
111
|
logic_or: { operands: 2, results: 1 },
|
|
112
|
+
// --- a 64-bit value and its two halves ---
|
|
113
|
+
// A 64-bit integer is ONE value of `{kind:'int'|'unknown', width:64}`, never two results and never
|
|
114
|
+
// a new `IrType` kind. The rule that decides it: for TYPES, widen the number that is already
|
|
115
|
+
// there; for OPERATIONS, add a new opcode. Existing code tests `t.kind === 'int'` and reads
|
|
116
|
+
// `t.width`, so a wider width makes a pass that is wrong about it fail LOUDLY, while a new kind
|
|
117
|
+
// would make the same pass skip SILENTLY. Existing code switches on `op.opcode`, so a new opcode
|
|
118
|
+
// makes an existing pass skip, which is the safe direction there.
|
|
119
|
+
//
|
|
120
|
+
// So the arithmetic opcodes above gain NOTHING: `add`, `sub`, `mul`, the shifts and the `icmp_*`
|
|
121
|
+
// family carry the width in their operand types. Only the three ops that have no 32-bit spelling
|
|
122
|
+
// are new — building a 64-bit value out of two halves, and reading either half back.
|
|
123
|
+
//
|
|
124
|
+
// `concat lo, hi` is TRANSIENT the way `mulh` is: it renders only in the shapes something above
|
|
125
|
+
// it recognises (a widen, a literal, an ABI pair), and one that survives to the structurer is a
|
|
126
|
+
// loud gap. `lo32`/`hi32` invert that property and are PERMANENT, because C spells them exactly —
|
|
127
|
+
// `(u32)v` and `(u32)(v >> 32)`.
|
|
128
|
+
//
|
|
129
|
+
// WHY `hi32` AND NOT `lo32(shr_u v {imm:32})`, which is the same value in two ops rather than
|
|
130
|
+
// three: spelling a projection as a 64-bit SHIFT puts a 64-bit arithmetic op into the IR at every
|
|
131
|
+
// pair site, and hands it to every pass that matches a shift — `raise/divpow2.ts` on
|
|
132
|
+
// `shr_s {imm}`, `raise/extscale.ts` on `shl`, `raise/narrow.ts` on a narrow/re-widen pair. Those
|
|
133
|
+
// passes would then be REACHED by a 64-bit operand instead of skipping it. A third opcode nothing
|
|
134
|
+
// else matches is what buys the isolation.
|
|
135
|
+
concat: { operands: 2, results: 1 }, // concat lo, hi → the 64-bit value whose low half is `lo`
|
|
136
|
+
lo32: { operands: 1, results: 1 },
|
|
137
|
+
hi32: { operands: 1, results: 1 },
|
|
138
|
+
// --- hardware floating point ---
|
|
139
|
+
// IEEE arithmetic on `{kind:'float'}` values, and on nothing else (ir/verify.ts checks both
|
|
140
|
+
// directions). NEW OPCODES rather than `add`/`sub` over a float-typed operand, by the rule the
|
|
141
|
+
// 64-bit section above states for OPERATIONS: every pass that matches `add` is an integer rewrite
|
|
142
|
+
// — a pointer walk, a reassociation, a constant fold, an idiom pattern — and a new opcode is what
|
|
143
|
+
// makes each of them skip. Pure: under the default IEEE environment none of them traps, so a
|
|
144
|
+
// dead one is reaped like any integer op.
|
|
145
|
+
fadd: { operands: 2, results: 1 },
|
|
146
|
+
fsub: { operands: 2, results: 1 },
|
|
147
|
+
fmul: { operands: 2, results: 1 },
|
|
148
|
+
fdiv: { operands: 2, results: 1 },
|
|
149
|
+
fneg: { operands: 1, results: 1 },
|
|
150
|
+
// A double LITERAL: `bits` is its IEEE pattern as sixteen hex digits (ir/float-bits.ts). Its own
|
|
151
|
+
// opcode rather than a `const` over a float type for the reason the arithmetic above has its own:
|
|
152
|
+
// every pass that matches `const` is an integer fold.
|
|
153
|
+
fconst: { operands: 0, results: 1, requiredAttrs: ['bits'] },
|
|
112
154
|
// --- memory ---
|
|
113
155
|
load: { operands: 1, results: 1, requiredAttrs: ['off', 'width', 'signed'], reads: true },
|
|
114
156
|
store: { operands: 2, results: 0, requiredAttrs: ['off', 'width'], effects: true },
|
|
@@ -119,8 +161,10 @@ export const OPCODES = {
|
|
|
119
161
|
aload: { operands: 2, results: 1, requiredAttrs: ['elemSize', 'signed'], reads: true }, // aload base, index
|
|
120
162
|
astore: { operands: 3, results: 0, requiredAttrs: ['elemSize'], effects: true }, // astore base, index, value
|
|
121
163
|
// --- call: operands are the argument values (r0..), result is the return value (r0),
|
|
122
|
-
// `target` attr is the callee symbol
|
|
123
|
-
|
|
164
|
+
// `target` attr is the callee symbol — or, with `indirect: true` and no `target`, the
|
|
165
|
+
// callee is the address in the LAST operand (ir/core.ts `calleeValue`). Exactly one of the
|
|
166
|
+
// two, which `ir/verify.ts` checks. Caller-saved clobbering is implicit. ---
|
|
167
|
+
call: { operands: 'variadic', results: 1, effects: true },
|
|
124
168
|
// The ADDRESS of a named global (agbcc `ldr rD, .Lpool` where the pool word is `.word gSym`).
|
|
125
169
|
// Pure, 0 operands. Globals come from the project headers, so they are referenced by name, never
|
|
126
170
|
// declared as locals. The structurer lowers it three ways (see scalarGlobals in structure.ts):
|
|
@@ -134,10 +178,17 @@ export const OPCODES = {
|
|
|
134
178
|
// proving every access agrees, and the structurer declares the local and renders `&name` exactly
|
|
135
179
|
// as it renders a gaddr's `&sym`. Operand-free and pure, so GVN numbers it like gaddr and a dead
|
|
136
180
|
// 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
|
-
|
|
181
|
+
// `width`/`signed`/`count` are stamped by the frontend's frame-object AUDIT — requiring them
|
|
182
|
+
// makes "the audit ran" a verifier-checkable fact instead of a convention: a frontend that emits
|
|
183
|
+
// a laddr and skips the audit fails verify loudly instead of rendering `&undefined`. The SHAPE
|
|
184
|
+
// the three of them encode is checked there too (ir/verify.ts), because the attrs are
|
|
185
|
+
// disjunctive and only `count` tells the two arms apart.
|
|
186
|
+
// The object is `count` elements of `width` bytes, so its storage spans `width * count`. A
|
|
187
|
+
// SCALAR is count 1, typed by the access width and signedness every access agreed on. Count
|
|
188
|
+
// above 1 is STORAGE the audit sized without typing — no access pins an element type, so it is
|
|
189
|
+
// width 1, unsigned, and declares as `u8 name[count]`; nothing else may mint one, because the
|
|
190
|
+
// only thing that licenses an untyped object is the frame accounting the audit solves.
|
|
191
|
+
laddr: { operands: 0, results: 1, requiredAttrs: ['off', 'width', 'signed', 'count'] },
|
|
141
192
|
// An UNDEFINED value: a read of storage that carries no INPUT — nothing was entitled to hand this
|
|
142
193
|
// function a value there, and none of its own stores reached it on this path. Deliberately NOT
|
|
143
194
|
// "storage nobody could have written": a callee-saved register holds the CALLER's value at entry,
|
|
@@ -153,7 +204,7 @@ export const OPCODES = {
|
|
|
153
204
|
// merely "owns the storage", because a frame the function owns can still be written by
|
|
154
205
|
// someone else once an address into it escapes to a callee, which fills a wider object than
|
|
155
206
|
// any in-function access reveals. Whoever mints one owes the retraction on escape
|
|
156
|
-
// (
|
|
207
|
+
// (the frame-object audit, frontend/frame-objects.ts).
|
|
157
208
|
// • a REGISTER the ABI does not pass arguments in AND this function's prologue SAVED cannot
|
|
158
209
|
// carry a value a caller handed over, and has no address for anything else to reach it by, so
|
|
159
210
|
// there is nothing to retract. The save is half of the premise, not a corroboration of it:
|
|
@@ -199,13 +250,28 @@ export const OPCODES = {
|
|
|
199
250
|
* does not. */
|
|
200
251
|
export type Opcode = keyof typeof OPCODES;
|
|
201
252
|
|
|
253
|
+
/** The width of a 64-bit IR value, named once so the three places that build one and the verifier
|
|
254
|
+
* that checks them cannot come to disagree about it. */
|
|
255
|
+
export const WIDE_BITS = 64;
|
|
256
|
+
|
|
202
257
|
/** The widths a `zext`/`sext` carries — a fact about those two opcodes, so it lives with them
|
|
203
258
|
* rather than re-declared per consumer (raise/narrow.ts pairs a narrowing op with its re-widening,
|
|
204
259
|
* raise/paramwidth.ts declares a parameter at one). Every frontend that produces the pair produces
|
|
205
260
|
* 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.
|
|
261
|
+
* `extsb`/`extsh` (frontend/ppc.ts). A third width would be a C type the backend cannot spell.
|
|
262
|
+
*
|
|
263
|
+
* 32 IS NOT ONE OF THEM, and the widening to 64 does not come here. The machine spells that widen
|
|
264
|
+
* as a PAIR — agbcc `asr rN,rM,#31` or `mov rN,#0`, PPC `srawi`, MIPS `sra` — so it is a `concat`
|
|
265
|
+
* shape (`concat(x, shr_s(x,31))` signed, `concat(x, const 0)` unsigned), which is also the only
|
|
266
|
+
* form that says HOW the half was widened. This set keeps its one job: refuse an extension with no
|
|
267
|
+
* C spelling. */
|
|
207
268
|
export const CAST_WIDTHS: ReadonlySet<number> = new Set([8, 16]);
|
|
208
269
|
|
|
270
|
+
/** The float opcodes: the only ops a `{kind:'float'}` value may be an operand or a result of,
|
|
271
|
+
* besides `ret` (ir/verify.ts). Authored beside the registry because no signature field says what
|
|
272
|
+
* an op computes ON. */
|
|
273
|
+
export const FLOAT_OPS: ReadonlySet<string> = new Set(['fadd', 'fsub', 'fmul', 'fdiv', 'fneg', 'fconst']);
|
|
274
|
+
|
|
209
275
|
/** Signature lookup by RUNTIME opcode string (Op.opcode is a plain string — IR consumers switch
|
|
210
276
|
* on it); undefined for an unregistered opcode. */
|
|
211
277
|
export function opSig(opcode: string): OpSig | undefined {
|
|
@@ -214,17 +280,16 @@ export function opSig(opcode: string): OpSig | undefined {
|
|
|
214
280
|
|
|
215
281
|
/** The comparison whose result is the logical NEGATION of each `icmp_*` — `!(a < b)` is `a >= b`.
|
|
216
282
|
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
283
|
+
* AUTHORED data seated beside the registry, not a view derived from it: nothing in `OPCODES`
|
|
284
|
+
* states which comparison opposes which. What is
|
|
219
285
|
* derived is its SYMMETRY — the five involutive pairs are expanded both ways, so `neg(neg(c)) === c`
|
|
220
286
|
* holds by construction (a hand-written map is one typo away from breaking it, and the symptom is a
|
|
221
287
|
* plainly inverted condition in the emitted C). Completeness against the icmp family is the part
|
|
222
288
|
* construction cannot give, so a test asserts it (test/pattern.test.ts) — an eleventh comparison
|
|
223
289
|
* added to `OPCODES` would otherwise degrade its consumers several different ways.
|
|
224
290
|
*
|
|
225
|
-
* It lives here
|
|
226
|
-
*
|
|
227
|
-
* cannot drift apart the way inline copies did. Two adjacent facts worth knowing: raise/
|
|
291
|
+
* It lives here so that every consumer that has to say "the opposite of this compare" reads THIS
|
|
292
|
+
* one, and they cannot drift apart the way inline copies did. Two adjacent facts worth knowing: raise/
|
|
228
293
|
* shortcircuit.ts derives its `BOOL_OPS` from these keys (asserting negatable-icmp == boolean-op,
|
|
229
294
|
* true today), and l3/ast.ts `NEGATE_REL` is the SAME relation over the neutral L3 operator
|
|
230
295
|
* vocabulary — deliberately separate, because signedness lives in the operand types there, so the
|
|
@@ -243,103 +308,17 @@ export const NEGATED_ICMP: Readonly<Record<string, Opcode>> = Object.fromEntries
|
|
|
243
308
|
]),
|
|
244
309
|
);
|
|
245
310
|
|
|
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
311
|
/** The ops whose `operands[0]` is a memory-access BASE: `load base`, `store base, value`,
|
|
285
312
|
* `aload base, index`, `astore base, index, value` (the operand roles are in the registry above).
|
|
286
313
|
* Authored rather than derived — no registry field records the operand ROLE, and the only other
|
|
287
314
|
* memory-touching opcode is `call`, whose variadic operands are arguments and not a base.
|
|
288
315
|
*
|
|
289
|
-
*
|
|
290
|
-
* is here and not next to
|
|
291
|
-
* model,
|
|
316
|
+
* Three consumers ask three different questions of it and all need the same answer, which is why
|
|
317
|
+
* it is here and not next to any: `structure/analysis.ts` uses it for the address-home variation's
|
|
318
|
+
* slot model, `raise/const.ts` to recognise a folded literal that IS an address, and
|
|
319
|
+
* `ir/discipline.ts` to tell a device access's `volatile` from a frame object's. */
|
|
292
320
|
export const MEM_BASE_OPS: ReadonlySet<string> = new Set(['load', 'store', 'aload', 'astore']);
|
|
293
321
|
|
|
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
322
|
/** Ops that MATERIALIZE a value out of nothing: no operands, no state read, no effect, no trap, no
|
|
344
323
|
* control flow — so where one sits in a block says nothing about what ran before it. Derived, so a
|
|
345
324
|
* future pure nullary opcode joins without a second edit. Consumed by raise/paramwidth.ts, whose
|
|
@@ -352,10 +331,3 @@ export const MATERIALIZING_OPS: ReadonlySet<string> = new Set(
|
|
|
352
331
|
return sig.operands === 0 && !sig.terminator && !sig.effects && !sig.reads && !sig.traps;
|
|
353
332
|
}),
|
|
354
333
|
);
|
|
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
|
-
}
|
package/src/ir/parse.ts
CHANGED
package/src/ir/types.ts
CHANGED
|
@@ -17,13 +17,36 @@ export type IrType =
|
|
|
17
17
|
// from `ptr(int)`+array-index because its access pattern is inconsistent with a homogeneous
|
|
18
18
|
// array (mixed widths / non-uniform offsets). `name` is synthetic today (`Struct0`); a later
|
|
19
19
|
// DWARF pass supplies real names. Fields are sorted by `off`.
|
|
20
|
-
|
|
20
|
+
//
|
|
21
|
+
// `declared` is a struct the project's HEADERS own — one a callee is declared to return
|
|
22
|
+
// (aggregate.ts `aggregateType`) — holding the headers' own spelling of it (`struct Blob64`,
|
|
23
|
+
// `BlobT`). No source this lift prints defines it: the declarations block does, which a candidate
|
|
24
|
+
// compiled inside the project's headers drops, since the headers define it there.
|
|
25
|
+
| { kind: 'struct'; name: string; fields: StructField[]; size?: number; declared?: string }
|
|
21
26
|
// A fixed-length array `elem[count]`. Today its sole inhabitant is struct padding (a `u8[N]`
|
|
22
27
|
// pad member seats fields at their exact offsets, raise/struct-arrays.ts) — a REAL type,
|
|
23
28
|
// not a printed string. Array-typed fields declare with the length AFTER the name in C
|
|
24
29
|
// (`u8 _pad[4]`), so the backend routes them through a declarator-aware `cDeclare`, not the
|
|
25
30
|
// prefix `cType`.
|
|
26
31
|
| { kind: 'array'; elem: IrType; count: number }
|
|
32
|
+
// Several VIEWS of one storage cell (raise/structs.ts): a base read or written at more than one
|
|
33
|
+
// width or extension over the same bytes. Every member sits at offset 0. It carries no name
|
|
34
|
+
// because it is only ever declared INLINE, as the type of the struct member that holds it, and its
|
|
35
|
+
// `size` is the one the target's compiler gives it — which is not always its widest view
|
|
36
|
+
// (`T.union` computes it from the compiler's aggregate boundary).
|
|
37
|
+
| { kind: 'union'; members: StructField[]; size: number }
|
|
38
|
+
// An IEEE floating-point value, single (32) or double (64). A KIND and not an `int` width, which is
|
|
39
|
+
// the one exception to the 64-bit rule at `ir/opcodes.ts` `concat` ("widen the number that is already
|
|
40
|
+
// there"): a float is not an integer of any width, so every pass that tests `kind === 'int'` must
|
|
41
|
+
// SKIP it rather than compute on it, and skipping is what a new kind buys. Its values live in the
|
|
42
|
+
// FPU's own register file, or in a soft-float runtime's register pair, and are computed only by
|
|
43
|
+
// the float opcodes (`fadd` …); `docs/floating-point.md` says which frontends mint one and what
|
|
44
|
+
// they still refuse. Two floats of different widths are different types (`typeEquals`), so a
|
|
45
|
+
// float op mixing them fails `ir/verify.ts`. A null width is a float whose code states no precision:
|
|
46
|
+
// a PowerPC FPR holds a double whichever the source declared, and `fneg`/`fmr` are one instruction
|
|
47
|
+
// for both, so `double dneg(double a){ return -a; }` and its `float` twin compile to one object.
|
|
48
|
+
// Either spelling reproduces it, and a declaration the caller supplies decides between them.
|
|
49
|
+
| { kind: 'float'; width: 32 | 64 | null }
|
|
27
50
|
| { kind: 'void' }; // a function that returns nothing
|
|
28
51
|
|
|
29
52
|
/** The scalar type of a memory access of `width` bytes: word ⇒ the s32 integer default;
|
|
@@ -34,6 +57,23 @@ export function scalarTypeForAccess(width: number, signed: boolean): IrType {
|
|
|
34
57
|
return width === 4 ? T.s(32) : T.int(width * 8, signed);
|
|
35
58
|
}
|
|
36
59
|
|
|
60
|
+
/** The width in bits of the integer a type carries, or null where a type carries no integer. A
|
|
61
|
+
* pointer's own width is the machine's and says nothing about what it addresses; an aggregate and
|
|
62
|
+
* `void` have no single width at all. `unknown` DOES carry one, which is the point — before type
|
|
63
|
+
* recovery every value is `unknown`, so a rule that skipped that kind would be vacuous exactly
|
|
64
|
+
* where the frontends build these.
|
|
65
|
+
*
|
|
66
|
+
* THE one copy, for the reason `scalarTypeForAccess` above is: its two consumers read the same
|
|
67
|
+
* question under OPPOSITE policies, and a per-consumer copy is how they drift apart. `ir/verify.ts`
|
|
68
|
+
* reads null as "this type does not take part in the width rule, so pass"; `runtime-helpers.ts`
|
|
69
|
+
* reads null as "this operand carries no integer width, so refuse to fold a 64-bit operation over
|
|
70
|
+
* it". Both want exactly this predicate — a pointer handed to `__muldi3` has no width either of
|
|
71
|
+
* them may claim — so an `IrType` kind added here reaches the verifier and the recogniser together
|
|
72
|
+
* or neither. */
|
|
73
|
+
export function intWidth(t: IrType): number | null {
|
|
74
|
+
return t.kind === 'int' || t.kind === 'unknown' ? t.width : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
37
77
|
export const T = {
|
|
38
78
|
unk: (width = 32): IrType => ({ kind: 'unknown', width }),
|
|
39
79
|
int: (width: number, signed: boolean): IrType => ({ kind: 'int', width, signed }),
|
|
@@ -42,9 +82,68 @@ export const T = {
|
|
|
42
82
|
ptr: (to: IrType): IrType => ({ kind: 'ptr', to }),
|
|
43
83
|
struct: (name: string, fields: StructField[], size?: number): IrType => ({ kind: 'struct', name, fields, size }),
|
|
44
84
|
array: (elem: IrType, count: number): IrType => ({ kind: 'array', elem, count }),
|
|
85
|
+
// Its size is its widest view rounded up to the compiler's aggregate boundary (target.ts).
|
|
86
|
+
union: (members: StructField[], boundary: number): IrType => {
|
|
87
|
+
const extent = Math.max(...members.map((m) => (m.type.kind === 'array' ? m.type.count : 1) * viewBytes(m.type)));
|
|
88
|
+
return { kind: 'union', members, size: Math.ceil(extent / boundary) * boundary };
|
|
89
|
+
},
|
|
90
|
+
f32: (): IrType => ({ kind: 'float', width: 32 }),
|
|
91
|
+
f64: (): IrType => ({ kind: 'float', width: 64 }),
|
|
92
|
+
fUnstated: (): IrType => ({ kind: 'float', width: null }),
|
|
45
93
|
void: (): IrType => ({ kind: 'void' }),
|
|
46
94
|
};
|
|
47
95
|
|
|
96
|
+
/** The byte width of a union view's element (an array view's element, or the scalar itself). */
|
|
97
|
+
function viewBytes(t: IrType): number {
|
|
98
|
+
const e = t.kind === 'array' ? t.elem : t;
|
|
99
|
+
if (e.kind !== 'int') {
|
|
100
|
+
throw new Error(`a union view is an integer or an array of one, not '${typeToString(t)}'`);
|
|
101
|
+
}
|
|
102
|
+
return e.width / 8;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The member `name` of an aggregate — a struct's field or a union's view — or undefined when `t`
|
|
106
|
+
* is not an aggregate or declares no such member. THE one lookup the typing walk and the deref
|
|
107
|
+
* contract share, so the two cannot disagree on which member access is well-typed. */
|
|
108
|
+
export function memberOf(t: IrType | undefined, name: string): StructField | undefined {
|
|
109
|
+
if (t?.kind === 'struct') {
|
|
110
|
+
return t.fields.find((f) => f.name === name);
|
|
111
|
+
}
|
|
112
|
+
if (t?.kind === 'union') {
|
|
113
|
+
return t.members.find((m) => m.name === name);
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The union member of a struct that holds byte `off`, and the view of it an access of `width`
|
|
119
|
+
* bytes reads — the view of that width and, for a narrow LOAD, of that extension; a store, which
|
|
120
|
+
* carries none, takes the width's only view or the unsigned one of two — with the element index
|
|
121
|
+
* inside an ARRAY view (null for a scalar one). Undefined when no union member holds `off`; `view`
|
|
122
|
+
* undefined when the member has no view for the access. */
|
|
123
|
+
export function unionViewAt(
|
|
124
|
+
st: Extract<IrType, { kind: 'struct' }>,
|
|
125
|
+
off: number,
|
|
126
|
+
width: number,
|
|
127
|
+
signed: boolean,
|
|
128
|
+
isStore: boolean,
|
|
129
|
+
): { member: StructField; view: StructField | undefined; index: number | null } | undefined {
|
|
130
|
+
const member = st.fields.find((f) => f.type.kind === 'union' && f.off <= off && off < f.off + f.type.size);
|
|
131
|
+
if (member === undefined || member.type.kind !== 'union') {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
const elemOf = (t: IrType): IrType => (t.kind === 'array' ? t.elem : t);
|
|
135
|
+
const wide = member.type.members.filter((m) => intWidth(elemOf(m.type)) === width * 8);
|
|
136
|
+
const signOf = (m: StructField): boolean | undefined => {
|
|
137
|
+
const e = elemOf(m.type);
|
|
138
|
+
return e.kind === 'int' ? e.signed : undefined;
|
|
139
|
+
};
|
|
140
|
+
const view =
|
|
141
|
+
width === 4 || (isStore && wide.length === 1)
|
|
142
|
+
? wide[0]
|
|
143
|
+
: wide.find((m) => signOf(m) === (isStore ? false : signed));
|
|
144
|
+
return { member, view, index: view?.type.kind === 'array' ? (off - member.off) / width : null };
|
|
145
|
+
}
|
|
146
|
+
|
|
48
147
|
export function typeToString(t: IrType): string {
|
|
49
148
|
switch (t.kind) {
|
|
50
149
|
case 'unknown':
|
|
@@ -57,6 +156,10 @@ export function typeToString(t: IrType): string {
|
|
|
57
156
|
return t.name;
|
|
58
157
|
case 'array':
|
|
59
158
|
return `${typeToString(t.elem)}[${t.count}]`;
|
|
159
|
+
case 'union':
|
|
160
|
+
return `union{${t.members.map((m) => `${typeToString(m.type)} ${m.name}`).join(';')}}`;
|
|
161
|
+
case 'float':
|
|
162
|
+
return `f${t.width ?? '?'}`;
|
|
60
163
|
case 'void':
|
|
61
164
|
return 'void';
|
|
62
165
|
}
|
|
@@ -67,6 +170,9 @@ export function parseType(s: string): IrType {
|
|
|
67
170
|
if (s.endsWith('*')) {
|
|
68
171
|
return T.ptr(parseType(s.slice(0, -1)));
|
|
69
172
|
}
|
|
173
|
+
if (s === 'f32' || s === 'f64' || s === 'f?') {
|
|
174
|
+
return s === 'f32' ? T.f32() : s === 'f64' ? T.f64() : T.fUnstated();
|
|
175
|
+
}
|
|
70
176
|
const m = s.match(/^(unk|s|u)(\d+)$/);
|
|
71
177
|
if (!m) {
|
|
72
178
|
throw new Error(`bad type '${s}'`);
|
|
@@ -85,12 +191,22 @@ export function typeEquals(a: IrType, b: IrType): boolean {
|
|
|
85
191
|
if (a.kind === 'int' && b.kind === 'int') {
|
|
86
192
|
return a.width === b.width && a.signed === b.signed;
|
|
87
193
|
}
|
|
88
|
-
if (a.kind === '
|
|
194
|
+
if (a.kind === 'void' && b.kind === 'void') {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
if ((a.kind === 'unknown' && b.kind === 'unknown') || (a.kind === 'float' && b.kind === 'float')) {
|
|
89
198
|
return a.width === b.width;
|
|
90
199
|
}
|
|
91
200
|
if (a.kind === 'array' && b.kind === 'array') {
|
|
92
201
|
return a.count === b.count && typeEquals(a.elem, b.elem);
|
|
93
202
|
}
|
|
203
|
+
if (a.kind === 'union' && b.kind === 'union') {
|
|
204
|
+
return (
|
|
205
|
+
a.size === b.size &&
|
|
206
|
+
a.members.length === b.members.length &&
|
|
207
|
+
a.members.every((m, i) => m.name === b.members[i].name && typeEquals(m.type, b.members[i].type))
|
|
208
|
+
);
|
|
209
|
+
}
|
|
94
210
|
// Two structs are equal when their name + field layout match (recovered structs are named
|
|
95
211
|
// by layout-discovery order, so equal name ⇒ equal layout in practice).
|
|
96
212
|
if (a.kind === 'struct' && b.kind === 'struct') {
|
package/src/ir/verify.ts
CHANGED
|
@@ -3,16 +3,75 @@
|
|
|
3
3
|
// 1. every block ends in exactly one terminator (and it is the last op)
|
|
4
4
|
// 2. operands well-formed: opcode registered, correct arity/attrs
|
|
5
5
|
// 3. SSA: each value defined once; every use is defined; def dominates use
|
|
6
|
-
// 4.
|
|
6
|
+
// 4. a float value is an operand or result of a float op or `ret` only — or a `double` operand of
|
|
7
|
+
// a call whose callee declares one there — and crosses an edge only into a block parameter of
|
|
8
|
+
// its own type; an `fconst` is a double, sixteen hex digits into an f64
|
|
9
|
+
// 5. side data: a fn that carries a write-order record carries one for EVERY block, with every
|
|
7
10
|
// ordinal inside that block's own write count (ir/core.ts `WriteOrder`)
|
|
8
|
-
import { Block, Fn, Value, dominators } from './core';
|
|
9
|
-
import {
|
|
11
|
+
import { Block, Fn, Op, Value, dominators } from './core';
|
|
12
|
+
import { isDoubleBits } from './float-bits';
|
|
13
|
+
import { FLOAT_OPS, WIDE_BITS, opSig } from './opcodes';
|
|
14
|
+
import { type IrType, T, intWidth, typeEquals, typeToString } from './types';
|
|
10
15
|
|
|
11
16
|
export class VerifyError extends Error {}
|
|
12
17
|
|
|
18
|
+
/** A call hands a float only to a parameter its callee is declared to take a `double` in — the
|
|
19
|
+
* operands the frontend lists in `doubles` — and returns none. */
|
|
20
|
+
function doublesDeclared(op: Op): boolean {
|
|
21
|
+
const at = op.attrs.doubles;
|
|
22
|
+
return (
|
|
23
|
+
op.opcode === 'call' &&
|
|
24
|
+
Array.isArray(at) &&
|
|
25
|
+
op.results.every((r) => r.type.kind !== 'float') &&
|
|
26
|
+
op.operands.every((o, i) => o.type.kind !== 'float' || (at.includes(i) && typeEquals(o.type, T.f64())))
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
13
30
|
// Opcodes admitting EITHER a 2-operand register form OR a 1-operand + `imm` attr form.
|
|
14
31
|
const TWO_OR_IMM = new Set(['sdiv', 'shl', 'shr_u', 'shr_s']);
|
|
15
32
|
|
|
33
|
+
// ── 64 DOES NOT MIX ────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
// A 64-bit value is one `Value` whose type carries width 64 (ir/opcodes.ts, `concat`), so every
|
|
35
|
+
// pass that reads `t.width` and is wrong about it is wrong HERE rather than three stages later. The
|
|
36
|
+
// rule below is a QUARANTINE and not full width agreement, and the difference is measured rather
|
|
37
|
+
// than stylistic: full agreement is already false on 32-bit IR, because `raise/paramwidth.ts`
|
|
38
|
+
// narrows a parameter to 8 or 16 bits and `add(p_u8, x_s32)` is an ordinary correct shape. What
|
|
39
|
+
// must hold is that a 64-bit operand never meets a 32-bit one in an op that computes on both.
|
|
40
|
+
//
|
|
41
|
+
// `unknown` PARTICIPATES, and that is the whole point: at L1 every value is `unknown`, so a rule
|
|
42
|
+
// that skipped it would be vacuous exactly where this is meant to be red. The shifts count only
|
|
43
|
+
// their SHIFTED operand — a 64-bit shift by a 32-bit count is what the machine does.
|
|
44
|
+
const WIDTH_UNIFORM = new Set(['add', 'sub', 'mul', 'and', 'or', 'xor', 'neg', 'not', 'sdiv', 'udiv', 'smod', 'umod']);
|
|
45
|
+
// The shifts count only their SHIFTED operand: a 64-bit value shifted by a 32-bit count is what
|
|
46
|
+
// every one of these machines does. The comparisons count only their OPERANDS: the result of a C
|
|
47
|
+
// comparison is an `int` whatever it compared, which is what `raise/recover.ts` already types it.
|
|
48
|
+
const SHIFTS = new Set(['shl', 'shr_u', 'shr_s']);
|
|
49
|
+
const COMPARES = new Set([
|
|
50
|
+
'icmp_slt',
|
|
51
|
+
'icmp_sle',
|
|
52
|
+
'icmp_sgt',
|
|
53
|
+
'icmp_sge',
|
|
54
|
+
'icmp_ult',
|
|
55
|
+
'icmp_ule',
|
|
56
|
+
'icmp_ugt',
|
|
57
|
+
'icmp_uge',
|
|
58
|
+
'icmp_eq',
|
|
59
|
+
'icmp_ne',
|
|
60
|
+
]);
|
|
61
|
+
/** Whether `t` takes part in the width rule at all: it does exactly when it carries an integer
|
|
62
|
+
* width. `ir/types.ts` owns that question — `raise/widehelpers.ts` asks it too, of the same kinds,
|
|
63
|
+
* and reads the null with the opposite polarity. */
|
|
64
|
+
const widthOf = intWidth;
|
|
65
|
+
/** The types an op's width rule quantifies over. */
|
|
66
|
+
const widthParticipants = (op: Op): IrType[] => {
|
|
67
|
+
const operands = op.operands.map((o) => o.type);
|
|
68
|
+
if (COMPARES.has(op.opcode)) {
|
|
69
|
+
return operands;
|
|
70
|
+
}
|
|
71
|
+
const results = op.results.map((r) => r.type);
|
|
72
|
+
return SHIFTS.has(op.opcode) ? [...results, ...operands.slice(0, 1)] : [...results, ...operands];
|
|
73
|
+
};
|
|
74
|
+
|
|
16
75
|
export function verify(fn: Fn): void {
|
|
17
76
|
if (fn.blocks.length === 0) {
|
|
18
77
|
throw new VerifyError(`fn '${fn.name}' has no blocks`);
|
|
@@ -104,11 +163,90 @@ export function verify(fn: Fn): void {
|
|
|
104
163
|
if (!isTerm && op.successors.length) {
|
|
105
164
|
throw new VerifyError(`non-terminator '${op.opcode}' has successors`);
|
|
106
165
|
}
|
|
166
|
+
// `laddr` carries two shapes in one set of attrs and `count` is the only thing telling
|
|
167
|
+
// them apart: one element is a SCALAR typed by the accesses the frame-object audit read
|
|
168
|
+
// off the machine, and more than one is STORAGE that audit sized without typing, whose
|
|
169
|
+
// width and signedness are therefore not an access at all. The structurer declares the
|
|
170
|
+
// second as `u8 name[count]`, so a typed element there would be a type nothing pinned —
|
|
171
|
+
// the exact answer the audit refuses to invent. (One BYTE encodes like a real `u8`
|
|
172
|
+
// scalar either way; the declaration is the same one, so nothing has to tell them apart.)
|
|
173
|
+
if (op.opcode === 'laddr') {
|
|
174
|
+
const count = op.attrs.count;
|
|
175
|
+
if (typeof count !== 'number' || !Number.isInteger(count) || count < 1) {
|
|
176
|
+
throw new VerifyError(`'laddr' count must be a positive integer, got ${String(count)}`);
|
|
177
|
+
}
|
|
178
|
+
if (count > 1 && (op.attrs.width !== 1 || op.attrs.signed !== false)) {
|
|
179
|
+
throw new VerifyError(
|
|
180
|
+
`'laddr' of ${count} elements is storage no access typed, so it must be unsigned bytes — ` +
|
|
181
|
+
`got width ${String(op.attrs.width)}, signed ${String(op.attrs.signed)}`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// A 64-bit value is BUILT and TAKEN APART by exactly these three, so their shape is
|
|
186
|
+
// checked here rather than left to whoever reads a half. `concat` is the only producer.
|
|
187
|
+
if (op.opcode === 'concat' || op.opcode === 'lo32' || op.opcode === 'hi32') {
|
|
188
|
+
const wide = op.opcode === 'concat' ? op.results[0].type : op.operands[0].type;
|
|
189
|
+
const narrow = op.opcode === 'concat' ? op.operands.map((o) => o.type) : op.results.map((r) => r.type);
|
|
190
|
+
const wideW = widthOf(wide);
|
|
191
|
+
if (wideW !== WIDE_BITS) {
|
|
192
|
+
throw new VerifyError(
|
|
193
|
+
`'${op.opcode}' ${op.opcode === 'concat' ? 'result' : 'operand'} must be an integer of ` +
|
|
194
|
+
`width ${WIDE_BITS}, got ${String(wideW ?? wide.kind)}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
for (const t of narrow) {
|
|
198
|
+
if (widthOf(t) !== 32) {
|
|
199
|
+
throw new VerifyError(
|
|
200
|
+
`'${op.opcode}' half must be an integer of width 32, got ${String(widthOf(t) ?? t.kind)}`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// …and everywhere else, 64 does not mix (see the header above this file's checks).
|
|
206
|
+
if (WIDTH_UNIFORM.has(op.opcode) || SHIFTS.has(op.opcode) || COMPARES.has(op.opcode)) {
|
|
207
|
+
const ws = widthParticipants(op)
|
|
208
|
+
.map(widthOf)
|
|
209
|
+
.filter((w): w is number => w !== null);
|
|
210
|
+
if (ws.some((w) => w === WIDE_BITS) && ws.some((w) => w !== WIDE_BITS)) {
|
|
211
|
+
throw new VerifyError(
|
|
212
|
+
`'${op.opcode}' mixes a ${WIDE_BITS}-bit operand with a narrower one (widths ${ws.join(', ')})`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// A FLOAT IS COMPUTED ON BY THE FLOAT OPS AND RETURNED, AND NOTHING ELSE TOUCHES IT. Both
|
|
217
|
+
// halves are checked, because each is a silent wrong the other cannot see: a float op over
|
|
218
|
+
// an integer is a frontend that lost track of a register file, and an integer op over a
|
|
219
|
+
// float is a pass that matched an opcode without asking what it computes on.
|
|
220
|
+
const floats = [...op.operands, ...op.results].map((v) => v.type).filter((t) => t.kind === 'float');
|
|
221
|
+
if (FLOAT_OPS.has(op.opcode)) {
|
|
222
|
+
const all = [...op.operands, ...op.results].map((v) => v.type);
|
|
223
|
+
if (floats.length !== all.length || all.some((t) => !typeEquals(t, all[0]))) {
|
|
224
|
+
throw new VerifyError(`'${op.opcode}' computes on floats only, got ${all.map(typeToString).join(', ')}`);
|
|
225
|
+
}
|
|
226
|
+
} else if (floats.length > 0 && op.opcode !== 'ret' && !doublesDeclared(op)) {
|
|
227
|
+
throw new VerifyError(`a float value reaches '${op.opcode}', which does not compute on floats`);
|
|
228
|
+
}
|
|
229
|
+
// A literal's width is its pattern's, so the pattern and the result must name one width.
|
|
230
|
+
if (op.opcode === 'fconst' && (!isDoubleBits(op.attrs.bits) || !typeEquals(op.results[0].type, T.f64()))) {
|
|
231
|
+
throw new VerifyError(
|
|
232
|
+
`'fconst' is a double literal: bits ${JSON.stringify(op.attrs.bits)} into ${typeToString(op.results[0].type)}`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
107
235
|
for (const k of sig.requiredAttrs ?? []) {
|
|
108
236
|
if (!(k in op.attrs)) {
|
|
109
237
|
throw new VerifyError(`'${op.opcode}' missing required attr '${k}'`);
|
|
110
238
|
}
|
|
111
239
|
}
|
|
240
|
+
if (op.opcode === 'call') {
|
|
241
|
+
const named = typeof op.attrs.target === 'string';
|
|
242
|
+
const indirect = op.attrs.indirect === true;
|
|
243
|
+
if (named === indirect || 'target' in op.attrs !== named || (indirect && op.operands.length === 0)) {
|
|
244
|
+
throw new VerifyError(
|
|
245
|
+
"'call' names its callee in a string 'target' or calls the value in its last operand under " +
|
|
246
|
+
"'indirect', and does exactly one of the two",
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
112
250
|
for (const u of op.operands) {
|
|
113
251
|
if (!defined.has(u)) {
|
|
114
252
|
throw new VerifyError(`use of undefined value in '${op.opcode}'`);
|
|
@@ -128,6 +266,15 @@ export function verify(fn: Fn): void {
|
|
|
128
266
|
throw new VerifyError(`use of undefined value in successor args of '${op.opcode}'`);
|
|
129
267
|
}
|
|
130
268
|
}
|
|
269
|
+
// …and a float crosses an edge only into a block parameter of its own type.
|
|
270
|
+
s.args.forEach((u, i) => {
|
|
271
|
+
const p = s.block.params[i].type;
|
|
272
|
+
if ((u.type.kind === 'float' || p.kind === 'float') && !typeEquals(u.type, p)) {
|
|
273
|
+
throw new VerifyError(
|
|
274
|
+
`successor of '${op.opcode}' passes a ${typeToString(u.type)} to a ${typeToString(p)} block parameter`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
131
278
|
}
|
|
132
279
|
},
|
|
133
280
|
() => at(b, idx),
|