@asmlift/core 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +165 -50
- package/src/backend/cpp.ts +2 -1
- package/src/backend/pascal.ts +31 -12
- package/src/contracts.ts +173 -16
- package/src/declare.ts +69 -20
- package/src/frontend/asmdata.ts +7 -1
- package/src/frontend/call-plan.ts +964 -60
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +23 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1232 -192
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2076 -215
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2711 -232
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1460 -44
- package/src/frontend/stackargs.ts +106 -57
- package/src/frontend/thumb.ts +2044 -318
- package/src/ir/alias.ts +42 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +100 -5
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +159 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +109 -9
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +80 -11
- package/src/l3/ast.ts +290 -18
- package/src/l3/basecse.ts +33 -5
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +2 -2
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +31 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +9 -4
- package/src/l3/reindex.ts +283 -96
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +4 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +541 -24
- package/src/proto-context.ts +905 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +10 -26
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +102 -19
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +694 -0
- package/src/raise/shortcircuit.ts +26 -12
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +89 -31
- package/src/rank.ts +307 -54
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +165 -2
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +2180 -1054
- package/src/structure/switch-recover.ts +244 -100
- package/src/symbols.ts +6 -0
- package/src/target.ts +765 -52
- package/src/trace.ts +26 -8
- package/src/variation-definitions.ts +126 -14
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +5 -1
package/src/ir/opcodes.ts
CHANGED
|
@@ -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 },
|
|
@@ -151,6 +158,32 @@ export const OPCODES = {
|
|
|
151
158
|
// opcode rather than a `const` over a float type for the reason the arithmetic above has its own:
|
|
152
159
|
// every pass that matches `const` is an integer fold.
|
|
153
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 },
|
|
154
187
|
// --- memory ---
|
|
155
188
|
load: { operands: 1, results: 1, requiredAttrs: ['off', 'width', 'signed'], reads: true },
|
|
156
189
|
store: { operands: 2, results: 0, requiredAttrs: ['off', 'width'], effects: true },
|
|
@@ -214,13 +247,16 @@ export const OPCODES = {
|
|
|
214
247
|
// live-in, and a live-in of the entry block is a PARAMETER — right for an argument register, a
|
|
215
248
|
// fabricated argument for anything else.
|
|
216
249
|
//
|
|
217
|
-
// Operand-free and pure like `laddr`, and out of raise/gvn.ts's NUMBERABLE set
|
|
218
|
-
// it
|
|
219
|
-
//
|
|
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.
|
|
220
253
|
//
|
|
221
254
|
// `key` names the storage (`sp@0`, `r4`); the structurer reads it to name the local
|
|
222
255
|
// (`uninit_sp0`) and emits NO assignment — that absence is the recovery, and an edge argument
|
|
223
|
-
// 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.
|
|
224
260
|
undef: { operands: 0, results: 1, requiredAttrs: ['key'] },
|
|
225
261
|
// --- black-box escape hatch (keeps lifting total) ---
|
|
226
262
|
// `effects: true`: an instruction asmlift could not model may do anything — write memory, trap,
|
|
@@ -243,6 +279,11 @@ export const OPCODES = {
|
|
|
243
279
|
// Many-way switch dispatch (Regime B, jump table). The single operand is the scrutinee;
|
|
244
280
|
// successors are the N case blocks followed by the default block (the LAST successor);
|
|
245
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.
|
|
246
287
|
switch_br: { operands: 1, results: 0, terminator: true, successors: 'variadic', requiredAttrs: ['cases'] },
|
|
247
288
|
} as const satisfies Record<string, OpSig>;
|
|
248
289
|
|
|
@@ -272,6 +313,65 @@ export const CAST_WIDTHS: ReadonlySet<number> = new Set([8, 16]);
|
|
|
272
313
|
* an op computes ON. */
|
|
273
314
|
export const FLOAT_OPS: ReadonlySet<string> = new Set(['fadd', 'fsub', 'fmul', 'fdiv', 'fneg', 'fconst']);
|
|
274
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
|
+
|
|
275
375
|
/** Signature lookup by RUNTIME opcode string (Op.opcode is a plain string — IR consumers switch
|
|
276
376
|
* on it); undefined for an unregistered opcode. */
|
|
277
377
|
export function opSig(opcode: string): OpSig | undefined {
|
|
@@ -290,10 +390,10 @@ export function opSig(opcode: string): OpSig | undefined {
|
|
|
290
390
|
*
|
|
291
391
|
* It lives here so that every consumer that has to say "the opposite of this compare" reads THIS
|
|
292
392
|
* one, and they cannot drift apart the way inline copies did. Two adjacent facts worth knowing: raise/
|
|
293
|
-
* shortcircuit.ts derives its `BOOL_OPS` from these keys (asserting
|
|
294
|
-
* true today), and l3/ast.ts `NEGATE_REL` is the SAME relation
|
|
295
|
-
* vocabulary — deliberately separate, because signedness lives in the
|
|
296
|
-
* two tables are not candidates for further consolidation. */
|
|
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. */
|
|
297
397
|
const ICMP_NEGATION_PAIRS: readonly (readonly [Opcode, Opcode])[] = [
|
|
298
398
|
['icmp_eq', 'icmp_ne'],
|
|
299
399
|
['icmp_slt', 'icmp_sge'],
|
package/src/ir/types.ts
CHANGED
|
@@ -7,6 +7,11 @@ export interface StructField {
|
|
|
7
7
|
off: number;
|
|
8
8
|
type: IrType;
|
|
9
9
|
name: string;
|
|
10
|
+
/** A cell the function also reads or writes at another width or extension, declared at its
|
|
11
|
+
* widest view because the union of its views would not fit the compiler's layout
|
|
12
|
+
* (raise/structs.ts `buildUnionStruct`). An access of any other type is a cast of the base
|
|
13
|
+
* (structure.ts `memAccess`). */
|
|
14
|
+
castViews?: true;
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
export type IrType =
|
|
@@ -93,6 +98,31 @@ export const T = {
|
|
|
93
98
|
void: (): IrType => ({ kind: 'void' }),
|
|
94
99
|
};
|
|
95
100
|
|
|
101
|
+
/** The member name of each view a union holds, by access width. A width with no entry has no view
|
|
102
|
+
* (the deref contract admits no other scalar width either), and its overlap still declines. A
|
|
103
|
+
* narrow width loaded with both extensions has a view per extension, `u`/`s` prefixed. */
|
|
104
|
+
export const UNION_VIEW_NAMES: Readonly<Record<number, string>> = { 1: 'byte', 2: 'half', 4: 'word' };
|
|
105
|
+
|
|
106
|
+
/** The extensions a union declares a view of `width` bytes in, from the accesses of that width:
|
|
107
|
+
* THE one rule raise/structs.ts `buildUnionStruct` and the frame audit (frontend/frame-objects.ts
|
|
108
|
+
* `unionViews`) both declare by, so a struct member's union and a frame object's cannot disagree
|
|
109
|
+
* on their views. A word has one view whatever its loads say (`scalarTypeForAccess`); a narrower
|
|
110
|
+
* width a view per extension it is LOADED with, unsigned first; a width only stored the signed
|
|
111
|
+
* view where a stamped store says signed (raise/storesign.ts) and the unsigned one otherwise. */
|
|
112
|
+
export function unionViewSigns(
|
|
113
|
+
width: number,
|
|
114
|
+
accesses: readonly { readonly isLoad: boolean; readonly signed: boolean; readonly stamped: boolean }[],
|
|
115
|
+
): boolean[] {
|
|
116
|
+
if (width === 4) {
|
|
117
|
+
return [true];
|
|
118
|
+
}
|
|
119
|
+
const loaded = new Set(accesses.filter((a) => a.isLoad).map((a) => a.signed));
|
|
120
|
+
if (loaded.size === 0) {
|
|
121
|
+
return [accesses.some((a) => a.stamped && a.signed)];
|
|
122
|
+
}
|
|
123
|
+
return [false, true].filter((sg) => loaded.has(sg));
|
|
124
|
+
}
|
|
125
|
+
|
|
96
126
|
/** The byte width of a union view's element (an array view's element, or the scalar itself). */
|
|
97
127
|
function viewBytes(t: IrType): number {
|
|
98
128
|
const e = t.kind === 'array' ? t.elem : t;
|
|
@@ -102,6 +132,11 @@ function viewBytes(t: IrType): number {
|
|
|
102
132
|
return e.width / 8;
|
|
103
133
|
}
|
|
104
134
|
|
|
135
|
+
/** A struct as its declaration spells it (`declared`), else by its tag; any other type as printed. */
|
|
136
|
+
export function structSpelling(t: IrType): string {
|
|
137
|
+
return t.kind === 'struct' ? (t.declared ?? `struct ${t.name}`) : typeToString(t);
|
|
138
|
+
}
|
|
139
|
+
|
|
105
140
|
/** The member `name` of an aggregate — a struct's field or a union's view — or undefined when `t`
|
|
106
141
|
* is not an aggregate or declares no such member. THE one lookup the typing walk and the deref
|
|
107
142
|
* contract share, so the two cannot disagree on which member access is well-typed. */
|
|
@@ -131,8 +166,57 @@ export function unionViewAt(
|
|
|
131
166
|
if (member === undefined || member.type.kind !== 'union') {
|
|
132
167
|
return undefined;
|
|
133
168
|
}
|
|
169
|
+
return { member, ...unionView(member.type, off - member.off, width, signed, isStore) };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Does a member declared at signedness `declared` read as EXACTLY the type the cast spelling this
|
|
173
|
+
* replaces would have produced — `scalarTypeForAccess(width, signed)`? That is the whole
|
|
174
|
+
* byte-exactness argument for naming a member instead of casting: same address, same read width,
|
|
175
|
+
* same C type, so every operator downstream compiles identically. A 4-byte access renders s32
|
|
176
|
+
* whatever the load said (the ISA has one word load), so only a SIGNED member may take its place;
|
|
177
|
+
* narrower accesses carry their own signedness and must match it. An undeclared signedness (the
|
|
178
|
+
* member is not a base type — a nested struct, an enum, a pointer) is never assumed to match.
|
|
179
|
+
* THE one copy of "an access names the member": structure.ts's member spellings and `isCastView`
|
|
180
|
+
* below all ask it, so they cannot drift apart. */
|
|
181
|
+
export function spellsAccessType(declared: boolean | undefined, width: number, signed: boolean): boolean {
|
|
182
|
+
if (declared === undefined) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
return typeEquals(T.int(width * 8, declared), scalarTypeForAccess(width, signed));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Does an access of `width` bytes at byte `off`, inside a `castViews` field of the struct, read it
|
|
189
|
+
* at a view other than the field's own type — another width or offset, or a load whose type the
|
|
190
|
+
* field does not spell (`spellsAccessType`)? A store carries no extension (it truncates to the
|
|
191
|
+
* field's bytes whatever their type), so one of the field's width at its offset is the field. */
|
|
192
|
+
export function isCastView(
|
|
193
|
+
st: Extract<IrType, { kind: 'struct' }>,
|
|
194
|
+
off: number,
|
|
195
|
+
width: number,
|
|
196
|
+
signed: boolean,
|
|
197
|
+
isStore: boolean,
|
|
198
|
+
): boolean {
|
|
199
|
+
const f = st.fields.find(
|
|
200
|
+
(m) => m.castViews === true && m.off <= off && off + width <= m.off + (intWidth(m.type) ?? 0) / 8,
|
|
201
|
+
);
|
|
202
|
+
if (f === undefined || f.type.kind !== 'int') {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
return off !== f.off || f.type.width !== width * 8 || (!isStore && !spellsAccessType(f.type.signed, width, signed));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The view of a union an access of `width` bytes at byte `at` of it reads — the rule
|
|
209
|
+
* `unionViewAt` states, asked of a union that is not a struct's member (a frame object declared
|
|
210
|
+
* as one, structure.ts `laddrType`). */
|
|
211
|
+
export function unionView(
|
|
212
|
+
u: Extract<IrType, { kind: 'union' }>,
|
|
213
|
+
at: number,
|
|
214
|
+
width: number,
|
|
215
|
+
signed: boolean,
|
|
216
|
+
isStore: boolean,
|
|
217
|
+
): { view: StructField | undefined; index: number | null } {
|
|
134
218
|
const elemOf = (t: IrType): IrType => (t.kind === 'array' ? t.elem : t);
|
|
135
|
-
const wide =
|
|
219
|
+
const wide = u.members.filter((m) => intWidth(elemOf(m.type)) === width * 8);
|
|
136
220
|
const signOf = (m: StructField): boolean | undefined => {
|
|
137
221
|
const e = elemOf(m.type);
|
|
138
222
|
return e.kind === 'int' ? e.signed : undefined;
|
|
@@ -141,7 +225,7 @@ export function unionViewAt(
|
|
|
141
225
|
width === 4 || (isStore && wide.length === 1)
|
|
142
226
|
? wide[0]
|
|
143
227
|
: wide.find((m) => signOf(m) === (isStore ? false : signed));
|
|
144
|
-
return {
|
|
228
|
+
return { view, index: view?.type.kind === 'array' ? at / width : null };
|
|
145
229
|
}
|
|
146
230
|
|
|
147
231
|
export function typeToString(t: IrType): string {
|
package/src/ir/verify.ts
CHANGED
|
@@ -3,30 +3,57 @@
|
|
|
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. a float value is an operand or result of a float op or
|
|
7
|
-
// a call whose callee declares one there
|
|
8
|
-
//
|
|
6
|
+
// 4. a float value is an operand or result of a float op, a conversion or a compare, or `ret` only
|
|
7
|
+
// — or a `double` operand of a call whose callee declares one there, or the result of a call
|
|
8
|
+
// whose callee is declared to return a `float` — and crosses an edge only
|
|
9
|
+
// into a block parameter of its own type; an `fconst` is a double, sixteen hex digits into an f64
|
|
9
10
|
// 5. side data: a fn that carries a write-order record carries one for EVERY block, with every
|
|
10
11
|
// ordinal inside that block's own write count (ir/core.ts `WriteOrder`)
|
|
11
12
|
import { Block, Fn, Op, Value, dominators } from './core';
|
|
12
13
|
import { isDoubleBits } from './float-bits';
|
|
13
|
-
import { FLOAT_OPS, WIDE_BITS, opSig } from './opcodes';
|
|
14
|
+
import { FLOAT_COMPUTING, FLOAT_OPS, FLOAT_RESIZE, FLOAT_TO_INT, INT_TO_FLOAT, WIDE_BITS, opSig } from './opcodes';
|
|
14
15
|
import { type IrType, T, intWidth, typeEquals, typeToString } from './types';
|
|
15
16
|
|
|
16
17
|
export class VerifyError extends Error {}
|
|
17
18
|
|
|
18
19
|
/** 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
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
* operands the frontend lists in `doubles` — and returns one only where its callee is declared to
|
|
21
|
+
* return a `float` (`floatReturn`), as that single. */
|
|
22
|
+
function floatsDeclared(op: Op): boolean {
|
|
23
|
+
const at = Array.isArray(op.attrs.doubles) ? op.attrs.doubles : [];
|
|
22
24
|
return (
|
|
23
25
|
op.opcode === 'call' &&
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
op.results.every(
|
|
27
|
+
(r) => r.type.kind !== 'float' || (op.attrs.floatReturn === 'float' && typeEquals(r.type, T.f32())),
|
|
28
|
+
) &&
|
|
26
29
|
op.operands.every((o, i) => o.type.kind !== 'float' || (at.includes(i) && typeEquals(o.type, T.f64())))
|
|
27
30
|
);
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
/** The ops where a float meets an integer or the other precision, each held to the one shape it
|
|
34
|
+
* has: a conversion takes a float of either stated precision or none and gives an integer, or the
|
|
35
|
+
* reverse; a resize takes its own source width to its own destination width; a compare takes two
|
|
36
|
+
* floats of one type and gives a 32-bit integer. The integer side may be narrower than a word once
|
|
37
|
+
* recovery has typed it (`(float)x` of a `short` parameter), which converts the value it promotes
|
|
38
|
+
* to; the C spelling casts through `s32`/`s64`. */
|
|
39
|
+
function verifyFloatCrossing(op: Op): void {
|
|
40
|
+
const ins = op.operands.map((o) => o.type);
|
|
41
|
+
const out = op.results[0].type;
|
|
42
|
+
const isInt = (t: IrType) => intWidth(t) !== null;
|
|
43
|
+
const resize = FLOAT_RESIZE[op.opcode];
|
|
44
|
+
const ok = FLOAT_TO_INT.has(op.opcode)
|
|
45
|
+
? ins[0].kind === 'float' && isInt(out)
|
|
46
|
+
: INT_TO_FLOAT.has(op.opcode)
|
|
47
|
+
? isInt(ins[0]) && out.kind === 'float'
|
|
48
|
+
: resize !== undefined
|
|
49
|
+
? typeEquals(ins[0], resize.from === 64 ? T.f64() : T.f32()) &&
|
|
50
|
+
typeEquals(out, resize.to === 64 ? T.f64() : T.f32())
|
|
51
|
+
: ins[0].kind === 'float' && typeEquals(ins[0], ins[1]) && intWidth(out) === 32;
|
|
52
|
+
if (!ok) {
|
|
53
|
+
throw new VerifyError(`'${op.opcode}' does not take ${ins.map(typeToString).join(', ')} to ${typeToString(out)}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
30
57
|
// Opcodes admitting EITHER a 2-operand register form OR a 1-operand + `imm` attr form.
|
|
31
58
|
const TWO_OR_IMM = new Set(['sdiv', 'shl', 'shr_u', 'shr_s']);
|
|
32
59
|
|
|
@@ -41,7 +68,21 @@ const TWO_OR_IMM = new Set(['sdiv', 'shl', 'shr_u', 'shr_s']);
|
|
|
41
68
|
// `unknown` PARTICIPATES, and that is the whole point: at L1 every value is `unknown`, so a rule
|
|
42
69
|
// that skipped it would be vacuous exactly where this is meant to be red. The shifts count only
|
|
43
70
|
// their SHIFTED operand — a 64-bit shift by a 32-bit count is what the machine does.
|
|
44
|
-
const WIDTH_UNIFORM = new Set([
|
|
71
|
+
const WIDTH_UNIFORM = new Set([
|
|
72
|
+
'add',
|
|
73
|
+
'sub',
|
|
74
|
+
'mul',
|
|
75
|
+
'and',
|
|
76
|
+
'or',
|
|
77
|
+
'xor',
|
|
78
|
+
'neg',
|
|
79
|
+
'neg_s',
|
|
80
|
+
'not',
|
|
81
|
+
'sdiv',
|
|
82
|
+
'udiv',
|
|
83
|
+
'smod',
|
|
84
|
+
'umod',
|
|
85
|
+
]);
|
|
45
86
|
// The shifts count only their SHIFTED operand: a 64-bit value shifted by a 32-bit count is what
|
|
46
87
|
// every one of these machines does. The comparisons count only their OPERANDS: the result of a C
|
|
47
88
|
// comparison is an `int` whatever it compared, which is what `raise/recover.ts` already types it.
|
|
@@ -181,6 +222,32 @@ export function verify(fn: Fn): void {
|
|
|
181
222
|
`got width ${String(op.attrs.width)}, signed ${String(op.attrs.signed)}`,
|
|
182
223
|
);
|
|
183
224
|
}
|
|
225
|
+
// …and a PART of the one object (frame-objects.ts `provenParts`) is such storage too
|
|
226
|
+
if (op.attrs.part !== undefined && op.attrs.part !== true) {
|
|
227
|
+
throw new VerifyError(`'laddr' part must be true, got ${String(op.attrs.part)}`);
|
|
228
|
+
}
|
|
229
|
+
if (op.attrs.part === true && count < 2) {
|
|
230
|
+
throw new VerifyError(`'laddr' part must be storage of two or more bytes, got ${count}`);
|
|
231
|
+
}
|
|
232
|
+
// …and storage accessed at two widths over one byte names the VIEWS a union of it
|
|
233
|
+
// declares (frame-objects.ts `unionViews`): one per element width in bytes, negative
|
|
234
|
+
// for a signed narrow one, each tiling the storage — so at least two, distinct, of a
|
|
235
|
+
// width a view is named for, and a word never signed-marked (it has one view).
|
|
236
|
+
const views = op.attrs.views;
|
|
237
|
+
if (views !== undefined) {
|
|
238
|
+
const ok =
|
|
239
|
+
Array.isArray(views) &&
|
|
240
|
+
count > 1 &&
|
|
241
|
+
views.length >= 2 &&
|
|
242
|
+
new Set(views).size === views.length &&
|
|
243
|
+
views.every((v) => [1, 2, 4, -1, -2].includes(v) && count % Math.abs(v) === 0);
|
|
244
|
+
if (!ok) {
|
|
245
|
+
throw new VerifyError(
|
|
246
|
+
`'laddr' views must be two or more distinct element widths (1, 2, 4; negative for a signed ` +
|
|
247
|
+
`narrow view) tiling its ${String(count)} bytes, got ${JSON.stringify(views)}`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
184
251
|
}
|
|
185
252
|
// A 64-bit value is BUILT and TAKEN APART by exactly these three, so their shape is
|
|
186
253
|
// checked here rather than left to whoever reads a half. `concat` is the only producer.
|
|
@@ -223,7 +290,9 @@ export function verify(fn: Fn): void {
|
|
|
223
290
|
if (floats.length !== all.length || all.some((t) => !typeEquals(t, all[0]))) {
|
|
224
291
|
throw new VerifyError(`'${op.opcode}' computes on floats only, got ${all.map(typeToString).join(', ')}`);
|
|
225
292
|
}
|
|
226
|
-
} else if (
|
|
293
|
+
} else if (FLOAT_COMPUTING.has(op.opcode)) {
|
|
294
|
+
verifyFloatCrossing(op);
|
|
295
|
+
} else if (floats.length > 0 && op.opcode !== 'ret' && !floatsDeclared(op)) {
|
|
227
296
|
throw new VerifyError(`a float value reaches '${op.opcode}', which does not compute on floats`);
|
|
228
297
|
}
|
|
229
298
|
// A literal's width is its pattern's, so the pattern and the result must name one width.
|