@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.
Files changed (101) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +1 -0
  5. package/src/backend/cfamily.ts +165 -50
  6. package/src/backend/cpp.ts +2 -1
  7. package/src/backend/pascal.ts +31 -12
  8. package/src/contracts.ts +173 -16
  9. package/src/declare.ts +69 -20
  10. package/src/frontend/asmdata.ts +7 -1
  11. package/src/frontend/call-plan.ts +964 -60
  12. package/src/frontend/dead-reads.ts +628 -0
  13. package/src/frontend/device-pins.ts +23 -4
  14. package/src/frontend/emit.ts +4 -3
  15. package/src/frontend/fpu.ts +224 -17
  16. package/src/frontend/frame-objects.ts +1232 -192
  17. package/src/frontend/frontend.ts +64 -3
  18. package/src/frontend/high-half.ts +53 -4
  19. package/src/frontend/jump-table.ts +530 -0
  20. package/src/frontend/local-object.ts +158 -32
  21. package/src/frontend/mips.ts +2076 -215
  22. package/src/frontend/own-struct-return.ts +500 -0
  23. package/src/frontend/ppc.ts +2711 -232
  24. package/src/frontend/register-struct-call.ts +484 -0
  25. package/src/frontend/reloc-symbol.ts +22 -7
  26. package/src/frontend/ssa.ts +1460 -44
  27. package/src/frontend/stackargs.ts +106 -57
  28. package/src/frontend/thumb.ts +2044 -318
  29. package/src/ir/alias.ts +42 -0
  30. package/src/ir/bits.ts +681 -1
  31. package/src/ir/core.ts +100 -5
  32. package/src/ir/discipline.ts +86 -21
  33. package/src/ir/entry.ts +159 -0
  34. package/src/{structure → ir}/loops.ts +6 -2
  35. package/src/ir/opcodes.ts +109 -9
  36. package/src/ir/types.ts +86 -2
  37. package/src/ir/verify.ts +80 -11
  38. package/src/l3/ast.ts +290 -18
  39. package/src/l3/basecse.ts +33 -5
  40. package/src/l3/constinit.ts +382 -0
  41. package/src/l3/dce.ts +2 -2
  42. package/src/l3/initfirst.ts +42 -11
  43. package/src/l3/intcell.ts +2 -1
  44. package/src/l3/narrowdecl.ts +77 -36
  45. package/src/l3/pollguard.ts +8 -2
  46. package/src/l3/ptrcell.ts +31 -34
  47. package/src/l3/ptrfield.ts +123 -8
  48. package/src/l3/regspell.ts +9 -4
  49. package/src/l3/reindex.ts +283 -96
  50. package/src/l3/storage.ts +98 -1
  51. package/src/l3/symbol-refs.ts +84 -6
  52. package/src/l3/tailmerge.ts +4 -2
  53. package/src/l3/typing.ts +108 -15
  54. package/src/l3/unreduce.ts +6 -5
  55. package/src/l3/volatileptr.ts +17 -54
  56. package/src/mangle.ts +30 -0
  57. package/src/pattern/engine.ts +361 -73
  58. package/src/pipeline.ts +541 -24
  59. package/src/proto-context.ts +905 -45
  60. package/src/proto.ts +555 -40
  61. package/src/raise/aggmembers.ts +290 -0
  62. package/src/raise/arrays.ts +5 -1
  63. package/src/raise/declared-volatile.ts +207 -31
  64. package/src/raise/floathelpers.ts +254 -33
  65. package/src/raise/gvn.ts +3 -3
  66. package/src/raise/latch.ts +162 -6
  67. package/src/raise/memberarrays.ts +10 -4
  68. package/src/raise/offsetnames.ts +10 -26
  69. package/src/raise/pairparams.ts +33 -0
  70. package/src/raise/paramwidth.ts +196 -6
  71. package/src/raise/pre-recovery.ts +102 -19
  72. package/src/raise/pre-structure.ts +54 -0
  73. package/src/raise/recover.ts +12 -5
  74. package/src/raise/reroll.ts +694 -0
  75. package/src/raise/shortcircuit.ts +26 -12
  76. package/src/raise/softdiv.ts +1 -1
  77. package/src/raise/storesign.ts +153 -0
  78. package/src/raise/struct-arrays.ts +30 -11
  79. package/src/raise/structs.ts +396 -33
  80. package/src/raise/truncload.ts +4 -3
  81. package/src/raise/unthread.ts +400 -0
  82. package/src/rank-declare.ts +118 -12
  83. package/src/rank-variations.ts +89 -31
  84. package/src/rank.ts +307 -54
  85. package/src/runtime-helpers.ts +140 -22
  86. package/src/structure/analysis.ts +165 -2
  87. package/src/structure/arith-ops.ts +23 -0
  88. package/src/structure/globalaccess.ts +111 -9
  89. package/src/structure/hazards.ts +16 -64
  90. package/src/structure/local-statics.ts +145 -9
  91. package/src/structure/namecoalesce.ts +1 -1
  92. package/src/structure/pointer-spelling.ts +1106 -0
  93. package/src/structure/redundant-test.ts +1 -1
  94. package/src/structure/structure.ts +2180 -1054
  95. package/src/structure/switch-recover.ts +244 -100
  96. package/src/symbols.ts +6 -0
  97. package/src/target.ts +765 -52
  98. package/src/trace.ts +26 -8
  99. package/src/variation-definitions.ts +126 -14
  100. package/src/variation-gates.ts +2 -0
  101. 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 — where numbering
218
- // it would be VACUOUS rather than harmful, since two undefs in one function always carry
219
- // different keys. "Same key, therefore same value" is empty for a value that has none.
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 negatable-icmp == boolean-op,
294
- * true today), and l3/ast.ts `NEGATE_REL` is the SAME relation over the neutral L3 operator
295
- * vocabulary — deliberately separate, because signedness lives in the operand types there, so 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 = member.type.members.filter((m) => intWidth(elemOf(m.type)) === width * 8);
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 { member, view, index: view?.type.kind === 'array' ? (off - member.off) / width : null };
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 `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
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 none. */
20
- function doublesDeclared(op: Op): boolean {
21
- const at = op.attrs.doubles;
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
- Array.isArray(at) &&
25
- op.results.every((r) => r.type.kind !== 'float') &&
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(['add', 'sub', 'mul', 'and', 'or', 'xor', 'neg', 'not', 'sdiv', 'udiv', 'smod', 'umod']);
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 (floats.length > 0 && op.opcode !== 'ret' && !doublesDeclared(op)) {
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.