@asmlift/core 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +36 -31
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +397 -0
  4. package/src/backend/c.ts +6 -2
  5. package/src/backend/cfamily.ts +370 -56
  6. package/src/backend/cpp.ts +116 -10
  7. package/src/backend/pascal.ts +69 -11
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +777 -84
  15. package/src/declare.ts +132 -13
  16. package/src/frontend/asmdata.ts +26 -6
  17. package/src/frontend/call-plan.ts +1590 -0
  18. package/src/frontend/dead-reads.ts +628 -0
  19. package/src/frontend/device-pins.ts +296 -0
  20. package/src/frontend/emit.ts +4 -3
  21. package/src/frontend/errors.ts +5 -0
  22. package/src/frontend/flags-edge.ts +101 -0
  23. package/src/frontend/fpu.ts +374 -0
  24. package/src/frontend/frame-objects.ts +3171 -0
  25. package/src/frontend/frontend.ts +64 -3
  26. package/src/frontend/high-half.ts +55 -5
  27. package/src/frontend/jump-table.ts +530 -0
  28. package/src/frontend/local-object.ts +616 -0
  29. package/src/frontend/mips.ts +2392 -208
  30. package/src/frontend/opaque.ts +97 -18
  31. package/src/frontend/own-struct-return.ts +500 -0
  32. package/src/frontend/ppc.ts +3137 -267
  33. package/src/frontend/register-struct-call.ts +484 -0
  34. package/src/frontend/reloc-symbol.ts +109 -13
  35. package/src/frontend/splat.ts +108 -5
  36. package/src/frontend/ssa.ts +2098 -89
  37. package/src/frontend/stackargs.ts +172 -42
  38. package/src/frontend/thumb.ts +4563 -2091
  39. package/src/ir/alias.ts +71 -0
  40. package/src/ir/bits.ts +681 -1
  41. package/src/ir/core.ts +199 -6
  42. package/src/ir/discipline.ts +313 -0
  43. package/src/ir/entry.ts +159 -0
  44. package/src/ir/float-bits.ts +47 -0
  45. package/src/{structure → ir}/loops.ts +6 -2
  46. package/src/ir/opcodes.ts +193 -121
  47. package/src/ir/parse.ts +1 -0
  48. package/src/ir/types.ts +202 -2
  49. package/src/ir/verify.ts +219 -3
  50. package/src/l3/address.ts +5 -0
  51. package/src/l3/argcopy.ts +38 -6
  52. package/src/l3/ast.ts +467 -23
  53. package/src/l3/basecse.ts +33 -5
  54. package/src/l3/coalesce.ts +21 -13
  55. package/src/l3/constinit.ts +382 -0
  56. package/src/l3/dce.ts +34 -12
  57. package/src/l3/gates.ts +9 -3
  58. package/src/l3/hoist.ts +8 -7
  59. package/src/l3/initfirst.ts +68 -19
  60. package/src/l3/intcell.ts +98 -0
  61. package/src/l3/mentions.ts +10 -4
  62. package/src/l3/narrowdecl.ts +259 -0
  63. package/src/l3/nearbase.ts +10 -5
  64. package/src/l3/parkfirst.ts +8 -5
  65. package/src/l3/pollguard.ts +11 -4
  66. package/src/l3/ptrcell.ts +354 -0
  67. package/src/l3/ptrfield.ts +123 -8
  68. package/src/l3/regspell.ts +26 -7
  69. package/src/l3/reindex.ts +293 -103
  70. package/src/l3/storage.ts +98 -1
  71. package/src/l3/symbol-refs.ts +182 -23
  72. package/src/l3/tailmerge.ts +9 -20
  73. package/src/l3/typing.ts +237 -32
  74. package/src/l3/unreduce.ts +10 -9
  75. package/src/l3/volatileptr.ts +17 -54
  76. package/src/l3/volstore.ts +96 -7
  77. package/src/mangle.ts +41 -1
  78. package/src/pattern/engine.ts +373 -82
  79. package/src/pipeline.ts +589 -27
  80. package/src/proto-context.ts +1648 -0
  81. package/src/proto.ts +1203 -49
  82. package/src/raise/aggmembers.ts +290 -0
  83. package/src/raise/arrays.ts +8 -3
  84. package/src/raise/declared-volatile.ts +307 -0
  85. package/src/raise/divpow2.ts +2 -2
  86. package/src/raise/errors.ts +17 -0
  87. package/src/raise/floathelpers.ts +464 -0
  88. package/src/raise/globalshape.ts +11 -5
  89. package/src/raise/gvn.ts +29 -8
  90. package/src/raise/latch.ts +162 -6
  91. package/src/raise/memberarrays.ts +10 -4
  92. package/src/raise/narrow.ts +29 -2
  93. package/src/raise/narrowlocal.ts +11 -6
  94. package/src/raise/offsetnames.ts +494 -0
  95. package/src/raise/pairparams.ts +114 -0
  96. package/src/raise/paramwidth.ts +203 -6
  97. package/src/raise/pre-recovery.ts +174 -17
  98. package/src/raise/pre-structure.ts +54 -0
  99. package/src/raise/recover.ts +26 -15
  100. package/src/raise/reroll.ts +694 -0
  101. package/src/raise/retsink.ts +1 -1
  102. package/src/raise/shortcircuit.ts +66 -30
  103. package/src/raise/softdiv.ts +21 -23
  104. package/src/raise/storesign.ts +153 -0
  105. package/src/raise/struct-arrays.ts +34 -14
  106. package/src/raise/structs.ts +674 -98
  107. package/src/raise/truncload.ts +367 -0
  108. package/src/raise/unthread.ts +400 -0
  109. package/src/raise/widehelpers.ts +106 -0
  110. package/src/rank-declare.ts +143 -12
  111. package/src/rank-variations.ts +117 -31
  112. package/src/rank.ts +563 -122
  113. package/src/runtime-helpers.ts +405 -0
  114. package/src/stillborn.ts +289 -0
  115. package/src/structure/analysis.ts +1449 -449
  116. package/src/structure/arith-ops.ts +23 -0
  117. package/src/structure/globalaccess.ts +111 -9
  118. package/src/structure/hazards.ts +641 -114
  119. package/src/structure/local-statics.ts +517 -0
  120. package/src/structure/namecoalesce.ts +2 -2
  121. package/src/structure/pointer-spelling.ts +1106 -0
  122. package/src/structure/redundant-test.ts +99 -0
  123. package/src/structure/structure.ts +3478 -975
  124. package/src/structure/switch-recover.ts +403 -178
  125. package/src/symbols.ts +44 -3
  126. package/src/target.ts +1387 -71
  127. package/src/trace.ts +66 -28
  128. package/src/variation-definitions.ts +243 -13
  129. package/src/variation-gates.ts +2 -0
  130. package/src/variation-tokens.ts +25 -7
package/src/ir/parse.ts CHANGED
@@ -127,6 +127,7 @@ export function parse(text: string): Fn {
127
127
  writeOrder: undefined,
128
128
  slotHomes: undefined,
129
129
  paramEvidence: undefined,
130
+ localObjects: undefined,
130
131
  };
131
132
  }
132
133
 
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 =
@@ -17,13 +22,36 @@ export type IrType =
17
22
  // from `ptr(int)`+array-index because its access pattern is inconsistent with a homogeneous
18
23
  // array (mixed widths / non-uniform offsets). `name` is synthetic today (`Struct0`); a later
19
24
  // DWARF pass supplies real names. Fields are sorted by `off`.
20
- | { kind: 'struct'; name: string; fields: StructField[]; size?: number }
25
+ //
26
+ // `declared` is a struct the project's HEADERS own — one a callee is declared to return
27
+ // (aggregate.ts `aggregateType`) — holding the headers' own spelling of it (`struct Blob64`,
28
+ // `BlobT`). No source this lift prints defines it: the declarations block does, which a candidate
29
+ // compiled inside the project's headers drops, since the headers define it there.
30
+ | { kind: 'struct'; name: string; fields: StructField[]; size?: number; declared?: string }
21
31
  // A fixed-length array `elem[count]`. Today its sole inhabitant is struct padding (a `u8[N]`
22
32
  // pad member seats fields at their exact offsets, raise/struct-arrays.ts) — a REAL type,
23
33
  // not a printed string. Array-typed fields declare with the length AFTER the name in C
24
34
  // (`u8 _pad[4]`), so the backend routes them through a declarator-aware `cDeclare`, not the
25
35
  // prefix `cType`.
26
36
  | { kind: 'array'; elem: IrType; count: number }
37
+ // Several VIEWS of one storage cell (raise/structs.ts): a base read or written at more than one
38
+ // width or extension over the same bytes. Every member sits at offset 0. It carries no name
39
+ // because it is only ever declared INLINE, as the type of the struct member that holds it, and its
40
+ // `size` is the one the target's compiler gives it — which is not always its widest view
41
+ // (`T.union` computes it from the compiler's aggregate boundary).
42
+ | { kind: 'union'; members: StructField[]; size: number }
43
+ // An IEEE floating-point value, single (32) or double (64). A KIND and not an `int` width, which is
44
+ // the one exception to the 64-bit rule at `ir/opcodes.ts` `concat` ("widen the number that is already
45
+ // there"): a float is not an integer of any width, so every pass that tests `kind === 'int'` must
46
+ // SKIP it rather than compute on it, and skipping is what a new kind buys. Its values live in the
47
+ // FPU's own register file, or in a soft-float runtime's register pair, and are computed only by
48
+ // the float opcodes (`fadd` …); `docs/floating-point.md` says which frontends mint one and what
49
+ // they still refuse. Two floats of different widths are different types (`typeEquals`), so a
50
+ // float op mixing them fails `ir/verify.ts`. A null width is a float whose code states no precision:
51
+ // a PowerPC FPR holds a double whichever the source declared, and `fneg`/`fmr` are one instruction
52
+ // for both, so `double dneg(double a){ return -a; }` and its `float` twin compile to one object.
53
+ // Either spelling reproduces it, and a declaration the caller supplies decides between them.
54
+ | { kind: 'float'; width: 32 | 64 | null }
27
55
  | { kind: 'void' }; // a function that returns nothing
28
56
 
29
57
  /** The scalar type of a memory access of `width` bytes: word ⇒ the s32 integer default;
@@ -34,6 +62,23 @@ export function scalarTypeForAccess(width: number, signed: boolean): IrType {
34
62
  return width === 4 ? T.s(32) : T.int(width * 8, signed);
35
63
  }
36
64
 
65
+ /** The width in bits of the integer a type carries, or null where a type carries no integer. A
66
+ * pointer's own width is the machine's and says nothing about what it addresses; an aggregate and
67
+ * `void` have no single width at all. `unknown` DOES carry one, which is the point — before type
68
+ * recovery every value is `unknown`, so a rule that skipped that kind would be vacuous exactly
69
+ * where the frontends build these.
70
+ *
71
+ * THE one copy, for the reason `scalarTypeForAccess` above is: its two consumers read the same
72
+ * question under OPPOSITE policies, and a per-consumer copy is how they drift apart. `ir/verify.ts`
73
+ * reads null as "this type does not take part in the width rule, so pass"; `runtime-helpers.ts`
74
+ * reads null as "this operand carries no integer width, so refuse to fold a 64-bit operation over
75
+ * it". Both want exactly this predicate — a pointer handed to `__muldi3` has no width either of
76
+ * them may claim — so an `IrType` kind added here reaches the verifier and the recogniser together
77
+ * or neither. */
78
+ export function intWidth(t: IrType): number | null {
79
+ return t.kind === 'int' || t.kind === 'unknown' ? t.width : null;
80
+ }
81
+
37
82
  export const T = {
38
83
  unk: (width = 32): IrType => ({ kind: 'unknown', width }),
39
84
  int: (width: number, signed: boolean): IrType => ({ kind: 'int', width, signed }),
@@ -42,9 +87,147 @@ export const T = {
42
87
  ptr: (to: IrType): IrType => ({ kind: 'ptr', to }),
43
88
  struct: (name: string, fields: StructField[], size?: number): IrType => ({ kind: 'struct', name, fields, size }),
44
89
  array: (elem: IrType, count: number): IrType => ({ kind: 'array', elem, count }),
90
+ // Its size is its widest view rounded up to the compiler's aggregate boundary (target.ts).
91
+ union: (members: StructField[], boundary: number): IrType => {
92
+ const extent = Math.max(...members.map((m) => (m.type.kind === 'array' ? m.type.count : 1) * viewBytes(m.type)));
93
+ return { kind: 'union', members, size: Math.ceil(extent / boundary) * boundary };
94
+ },
95
+ f32: (): IrType => ({ kind: 'float', width: 32 }),
96
+ f64: (): IrType => ({ kind: 'float', width: 64 }),
97
+ fUnstated: (): IrType => ({ kind: 'float', width: null }),
45
98
  void: (): IrType => ({ kind: 'void' }),
46
99
  };
47
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
+
126
+ /** The byte width of a union view's element (an array view's element, or the scalar itself). */
127
+ function viewBytes(t: IrType): number {
128
+ const e = t.kind === 'array' ? t.elem : t;
129
+ if (e.kind !== 'int') {
130
+ throw new Error(`a union view is an integer or an array of one, not '${typeToString(t)}'`);
131
+ }
132
+ return e.width / 8;
133
+ }
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
+
140
+ /** The member `name` of an aggregate — a struct's field or a union's view — or undefined when `t`
141
+ * is not an aggregate or declares no such member. THE one lookup the typing walk and the deref
142
+ * contract share, so the two cannot disagree on which member access is well-typed. */
143
+ export function memberOf(t: IrType | undefined, name: string): StructField | undefined {
144
+ if (t?.kind === 'struct') {
145
+ return t.fields.find((f) => f.name === name);
146
+ }
147
+ if (t?.kind === 'union') {
148
+ return t.members.find((m) => m.name === name);
149
+ }
150
+ return undefined;
151
+ }
152
+
153
+ /** The union member of a struct that holds byte `off`, and the view of it an access of `width`
154
+ * bytes reads — the view of that width and, for a narrow LOAD, of that extension; a store, which
155
+ * carries none, takes the width's only view or the unsigned one of two — with the element index
156
+ * inside an ARRAY view (null for a scalar one). Undefined when no union member holds `off`; `view`
157
+ * undefined when the member has no view for the access. */
158
+ export function unionViewAt(
159
+ st: Extract<IrType, { kind: 'struct' }>,
160
+ off: number,
161
+ width: number,
162
+ signed: boolean,
163
+ isStore: boolean,
164
+ ): { member: StructField; view: StructField | undefined; index: number | null } | undefined {
165
+ const member = st.fields.find((f) => f.type.kind === 'union' && f.off <= off && off < f.off + f.type.size);
166
+ if (member === undefined || member.type.kind !== 'union') {
167
+ return undefined;
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 } {
218
+ const elemOf = (t: IrType): IrType => (t.kind === 'array' ? t.elem : t);
219
+ const wide = u.members.filter((m) => intWidth(elemOf(m.type)) === width * 8);
220
+ const signOf = (m: StructField): boolean | undefined => {
221
+ const e = elemOf(m.type);
222
+ return e.kind === 'int' ? e.signed : undefined;
223
+ };
224
+ const view =
225
+ width === 4 || (isStore && wide.length === 1)
226
+ ? wide[0]
227
+ : wide.find((m) => signOf(m) === (isStore ? false : signed));
228
+ return { view, index: view?.type.kind === 'array' ? at / width : null };
229
+ }
230
+
48
231
  export function typeToString(t: IrType): string {
49
232
  switch (t.kind) {
50
233
  case 'unknown':
@@ -57,6 +240,10 @@ export function typeToString(t: IrType): string {
57
240
  return t.name;
58
241
  case 'array':
59
242
  return `${typeToString(t.elem)}[${t.count}]`;
243
+ case 'union':
244
+ return `union{${t.members.map((m) => `${typeToString(m.type)} ${m.name}`).join(';')}}`;
245
+ case 'float':
246
+ return `f${t.width ?? '?'}`;
60
247
  case 'void':
61
248
  return 'void';
62
249
  }
@@ -67,6 +254,9 @@ export function parseType(s: string): IrType {
67
254
  if (s.endsWith('*')) {
68
255
  return T.ptr(parseType(s.slice(0, -1)));
69
256
  }
257
+ if (s === 'f32' || s === 'f64' || s === 'f?') {
258
+ return s === 'f32' ? T.f32() : s === 'f64' ? T.f64() : T.fUnstated();
259
+ }
70
260
  const m = s.match(/^(unk|s|u)(\d+)$/);
71
261
  if (!m) {
72
262
  throw new Error(`bad type '${s}'`);
@@ -85,12 +275,22 @@ export function typeEquals(a: IrType, b: IrType): boolean {
85
275
  if (a.kind === 'int' && b.kind === 'int') {
86
276
  return a.width === b.width && a.signed === b.signed;
87
277
  }
88
- if (a.kind === 'unknown' && b.kind === 'unknown') {
278
+ if (a.kind === 'void' && b.kind === 'void') {
279
+ return true;
280
+ }
281
+ if ((a.kind === 'unknown' && b.kind === 'unknown') || (a.kind === 'float' && b.kind === 'float')) {
89
282
  return a.width === b.width;
90
283
  }
91
284
  if (a.kind === 'array' && b.kind === 'array') {
92
285
  return a.count === b.count && typeEquals(a.elem, b.elem);
93
286
  }
287
+ if (a.kind === 'union' && b.kind === 'union') {
288
+ return (
289
+ a.size === b.size &&
290
+ a.members.length === b.members.length &&
291
+ a.members.every((m, i) => m.name === b.members[i].name && typeEquals(m.type, b.members[i].type))
292
+ );
293
+ }
94
294
  // Two structs are equal when their name + field layout match (recovered structs are named
95
295
  // by layout-discovery order, so equal name ⇒ equal layout in practice).
96
296
  if (a.kind === 'struct' && b.kind === 'struct') {
package/src/ir/verify.ts CHANGED
@@ -3,16 +3,116 @@
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. side data: a fn that carries a write-order record carries one for EVERY block, with every
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
10
+ // 5. side data: a fn that carries a write-order record carries one for EVERY block, with every
7
11
  // ordinal inside that block's own write count (ir/core.ts `WriteOrder`)
8
- import { Block, Fn, Value, dominators } from './core';
9
- import { opSig } from './opcodes';
12
+ import { Block, Fn, Op, Value, dominators } from './core';
13
+ import { isDoubleBits } from './float-bits';
14
+ import { FLOAT_COMPUTING, FLOAT_OPS, FLOAT_RESIZE, FLOAT_TO_INT, INT_TO_FLOAT, WIDE_BITS, opSig } from './opcodes';
15
+ import { type IrType, T, intWidth, typeEquals, typeToString } from './types';
10
16
 
11
17
  export class VerifyError extends Error {}
12
18
 
19
+ /** A call hands a float only to a parameter its callee is declared to take a `double` in — the
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 : [];
24
+ return (
25
+ op.opcode === 'call' &&
26
+ op.results.every(
27
+ (r) => r.type.kind !== 'float' || (op.attrs.floatReturn === 'float' && typeEquals(r.type, T.f32())),
28
+ ) &&
29
+ op.operands.every((o, i) => o.type.kind !== 'float' || (at.includes(i) && typeEquals(o.type, T.f64())))
30
+ );
31
+ }
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
+
13
57
  // Opcodes admitting EITHER a 2-operand register form OR a 1-operand + `imm` attr form.
14
58
  const TWO_OR_IMM = new Set(['sdiv', 'shl', 'shr_u', 'shr_s']);
15
59
 
60
+ // ── 64 DOES NOT MIX ────────────────────────────────────────────────────────────────────────────
61
+ // A 64-bit value is one `Value` whose type carries width 64 (ir/opcodes.ts, `concat`), so every
62
+ // pass that reads `t.width` and is wrong about it is wrong HERE rather than three stages later. The
63
+ // rule below is a QUARANTINE and not full width agreement, and the difference is measured rather
64
+ // than stylistic: full agreement is already false on 32-bit IR, because `raise/paramwidth.ts`
65
+ // narrows a parameter to 8 or 16 bits and `add(p_u8, x_s32)` is an ordinary correct shape. What
66
+ // must hold is that a 64-bit operand never meets a 32-bit one in an op that computes on both.
67
+ //
68
+ // `unknown` PARTICIPATES, and that is the whole point: at L1 every value is `unknown`, so a rule
69
+ // that skipped it would be vacuous exactly where this is meant to be red. The shifts count only
70
+ // their SHIFTED operand — a 64-bit shift by a 32-bit count is what the machine does.
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
+ ]);
86
+ // The shifts count only their SHIFTED operand: a 64-bit value shifted by a 32-bit count is what
87
+ // every one of these machines does. The comparisons count only their OPERANDS: the result of a C
88
+ // comparison is an `int` whatever it compared, which is what `raise/recover.ts` already types it.
89
+ const SHIFTS = new Set(['shl', 'shr_u', 'shr_s']);
90
+ const COMPARES = new Set([
91
+ 'icmp_slt',
92
+ 'icmp_sle',
93
+ 'icmp_sgt',
94
+ 'icmp_sge',
95
+ 'icmp_ult',
96
+ 'icmp_ule',
97
+ 'icmp_ugt',
98
+ 'icmp_uge',
99
+ 'icmp_eq',
100
+ 'icmp_ne',
101
+ ]);
102
+ /** Whether `t` takes part in the width rule at all: it does exactly when it carries an integer
103
+ * width. `ir/types.ts` owns that question — `raise/widehelpers.ts` asks it too, of the same kinds,
104
+ * and reads the null with the opposite polarity. */
105
+ const widthOf = intWidth;
106
+ /** The types an op's width rule quantifies over. */
107
+ const widthParticipants = (op: Op): IrType[] => {
108
+ const operands = op.operands.map((o) => o.type);
109
+ if (COMPARES.has(op.opcode)) {
110
+ return operands;
111
+ }
112
+ const results = op.results.map((r) => r.type);
113
+ return SHIFTS.has(op.opcode) ? [...results, ...operands.slice(0, 1)] : [...results, ...operands];
114
+ };
115
+
16
116
  export function verify(fn: Fn): void {
17
117
  if (fn.blocks.length === 0) {
18
118
  throw new VerifyError(`fn '${fn.name}' has no blocks`);
@@ -104,11 +204,118 @@ export function verify(fn: Fn): void {
104
204
  if (!isTerm && op.successors.length) {
105
205
  throw new VerifyError(`non-terminator '${op.opcode}' has successors`);
106
206
  }
207
+ // `laddr` carries two shapes in one set of attrs and `count` is the only thing telling
208
+ // them apart: one element is a SCALAR typed by the accesses the frame-object audit read
209
+ // off the machine, and more than one is STORAGE that audit sized without typing, whose
210
+ // width and signedness are therefore not an access at all. The structurer declares the
211
+ // second as `u8 name[count]`, so a typed element there would be a type nothing pinned —
212
+ // the exact answer the audit refuses to invent. (One BYTE encodes like a real `u8`
213
+ // scalar either way; the declaration is the same one, so nothing has to tell them apart.)
214
+ if (op.opcode === 'laddr') {
215
+ const count = op.attrs.count;
216
+ if (typeof count !== 'number' || !Number.isInteger(count) || count < 1) {
217
+ throw new VerifyError(`'laddr' count must be a positive integer, got ${String(count)}`);
218
+ }
219
+ if (count > 1 && (op.attrs.width !== 1 || op.attrs.signed !== false)) {
220
+ throw new VerifyError(
221
+ `'laddr' of ${count} elements is storage no access typed, so it must be unsigned bytes — ` +
222
+ `got width ${String(op.attrs.width)}, signed ${String(op.attrs.signed)}`,
223
+ );
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
+ }
251
+ }
252
+ // A 64-bit value is BUILT and TAKEN APART by exactly these three, so their shape is
253
+ // checked here rather than left to whoever reads a half. `concat` is the only producer.
254
+ if (op.opcode === 'concat' || op.opcode === 'lo32' || op.opcode === 'hi32') {
255
+ const wide = op.opcode === 'concat' ? op.results[0].type : op.operands[0].type;
256
+ const narrow = op.opcode === 'concat' ? op.operands.map((o) => o.type) : op.results.map((r) => r.type);
257
+ const wideW = widthOf(wide);
258
+ if (wideW !== WIDE_BITS) {
259
+ throw new VerifyError(
260
+ `'${op.opcode}' ${op.opcode === 'concat' ? 'result' : 'operand'} must be an integer of ` +
261
+ `width ${WIDE_BITS}, got ${String(wideW ?? wide.kind)}`,
262
+ );
263
+ }
264
+ for (const t of narrow) {
265
+ if (widthOf(t) !== 32) {
266
+ throw new VerifyError(
267
+ `'${op.opcode}' half must be an integer of width 32, got ${String(widthOf(t) ?? t.kind)}`,
268
+ );
269
+ }
270
+ }
271
+ }
272
+ // …and everywhere else, 64 does not mix (see the header above this file's checks).
273
+ if (WIDTH_UNIFORM.has(op.opcode) || SHIFTS.has(op.opcode) || COMPARES.has(op.opcode)) {
274
+ const ws = widthParticipants(op)
275
+ .map(widthOf)
276
+ .filter((w): w is number => w !== null);
277
+ if (ws.some((w) => w === WIDE_BITS) && ws.some((w) => w !== WIDE_BITS)) {
278
+ throw new VerifyError(
279
+ `'${op.opcode}' mixes a ${WIDE_BITS}-bit operand with a narrower one (widths ${ws.join(', ')})`,
280
+ );
281
+ }
282
+ }
283
+ // A FLOAT IS COMPUTED ON BY THE FLOAT OPS AND RETURNED, AND NOTHING ELSE TOUCHES IT. Both
284
+ // halves are checked, because each is a silent wrong the other cannot see: a float op over
285
+ // an integer is a frontend that lost track of a register file, and an integer op over a
286
+ // float is a pass that matched an opcode without asking what it computes on.
287
+ const floats = [...op.operands, ...op.results].map((v) => v.type).filter((t) => t.kind === 'float');
288
+ if (FLOAT_OPS.has(op.opcode)) {
289
+ const all = [...op.operands, ...op.results].map((v) => v.type);
290
+ if (floats.length !== all.length || all.some((t) => !typeEquals(t, all[0]))) {
291
+ throw new VerifyError(`'${op.opcode}' computes on floats only, got ${all.map(typeToString).join(', ')}`);
292
+ }
293
+ } else if (FLOAT_COMPUTING.has(op.opcode)) {
294
+ verifyFloatCrossing(op);
295
+ } else if (floats.length > 0 && op.opcode !== 'ret' && !floatsDeclared(op)) {
296
+ throw new VerifyError(`a float value reaches '${op.opcode}', which does not compute on floats`);
297
+ }
298
+ // A literal's width is its pattern's, so the pattern and the result must name one width.
299
+ if (op.opcode === 'fconst' && (!isDoubleBits(op.attrs.bits) || !typeEquals(op.results[0].type, T.f64()))) {
300
+ throw new VerifyError(
301
+ `'fconst' is a double literal: bits ${JSON.stringify(op.attrs.bits)} into ${typeToString(op.results[0].type)}`,
302
+ );
303
+ }
107
304
  for (const k of sig.requiredAttrs ?? []) {
108
305
  if (!(k in op.attrs)) {
109
306
  throw new VerifyError(`'${op.opcode}' missing required attr '${k}'`);
110
307
  }
111
308
  }
309
+ if (op.opcode === 'call') {
310
+ const named = typeof op.attrs.target === 'string';
311
+ const indirect = op.attrs.indirect === true;
312
+ if (named === indirect || 'target' in op.attrs !== named || (indirect && op.operands.length === 0)) {
313
+ throw new VerifyError(
314
+ "'call' names its callee in a string 'target' or calls the value in its last operand under " +
315
+ "'indirect', and does exactly one of the two",
316
+ );
317
+ }
318
+ }
112
319
  for (const u of op.operands) {
113
320
  if (!defined.has(u)) {
114
321
  throw new VerifyError(`use of undefined value in '${op.opcode}'`);
@@ -128,6 +335,15 @@ export function verify(fn: Fn): void {
128
335
  throw new VerifyError(`use of undefined value in successor args of '${op.opcode}'`);
129
336
  }
130
337
  }
338
+ // …and a float crosses an edge only into a block parameter of its own type.
339
+ s.args.forEach((u, i) => {
340
+ const p = s.block.params[i].type;
341
+ if ((u.type.kind === 'float' || p.kind === 'float') && !typeEquals(u.type, p)) {
342
+ throw new VerifyError(
343
+ `successor of '${op.opcode}' passes a ${typeToString(u.type)} to a ${typeToString(p)} block parameter`,
344
+ );
345
+ }
346
+ });
131
347
  }
132
348
  },
133
349
  () => at(b, idx),
package/src/l3/address.ts CHANGED
@@ -33,6 +33,11 @@ export const baseConst = (e: Expr): number | null =>
33
33
  ? baseConst(e.e)
34
34
  : null;
35
35
 
36
+ /** Does this base already assert volatility — a `volatile` cast at any depth of the cast chain? A
37
+ * respell variation that replaces such a base drops the qualifier with it, and every access
38
+ * through it becomes a plain one agbcc may delete or hoist. */
39
+ export const qualifiedBase = (e: Expr): boolean => e.k === 'cast' && (e.volatile === true || qualifiedBase(e.e));
40
+
36
41
  /** the numeric address an expression IS, through any number of pointer casts */
37
42
  export const addrConst = (e: Expr): number | null =>
38
43
  e.k === 'const' ? e.value : e.k === 'cast' && e.to.kind === 'ptr' ? addrConst(e.e) : null;
package/src/l3/argcopy.ts CHANGED
@@ -101,9 +101,12 @@ export interface ArgCopyRegionCtx {
101
101
  readonly reads: number;
102
102
  /** a loop encloses the region — its own body, or any list nested below one */
103
103
  readonly underLoop: boolean;
104
+ /** the region is ONE basic block: no statement opens a nested list, and no expression holds a
105
+ * short-circuit connective */
106
+ readonly straightLine: boolean;
104
107
  }
105
108
 
106
- /** Which regions are worth offering. NEITHER is sound — each refuses a spelling that is correct
109
+ /** Which regions are worth offering. NONE is sound — each refuses a spelling that is correct
107
110
  * but models nothing, and what they buy is the candidate count: every one of these is a COMPILE,
108
111
  * and this variation multiplies with `/coalesce`. */
109
112
  export const ARGCOPY_REGION_GATES: readonly Gate<ArgCopyRegionCtx>[] = [
@@ -117,13 +120,37 @@ export const ARGCOPY_REGION_GATES: readonly Gate<ArgCopyRegionCtx>[] = [
117
120
  id: 'loop-region',
118
121
  why: 'a copy anywhere inside a loop re-runs every iteration, so it belongs to a region that holds the loop instead — and no region does when the loop is at function top level, where this variation offers nothing at all',
119
122
  sound: false,
120
- // the second clause has its own test, which `guardedBy` cannot also name (one title per gate):
121
- // `argcopy.test.ts: a TOP-LEVEL loop is offered NOTHING`
122
- guardedBy: 'argcopy.test.ts: an arm NESTED inside a loop body is refused too',
123
+ guardedBy: [
124
+ 'argcopy.test.ts: an arm NESTED inside a loop body is refused too',
125
+ 'argcopy.test.ts: a TOP-LEVEL loop is offered NOTHING',
126
+ ],
123
127
  rejects: (c) => c.underLoop,
124
128
  },
129
+ {
130
+ id: 'straight-line',
131
+ why: 'a copy whose every read sits in one basic block is forward-propagated away by every compiler in the matrix, so the copy is erased — a `&&` or `||` in the region is an edge the copy can live across, and such a region is offered',
132
+ sound: false,
133
+ guardedBy: 'argcopy.test.ts: a STRAIGHT-LINE region is refused',
134
+ rejects: (c) => c.straightLine,
135
+ },
125
136
  ];
126
137
 
138
+ /** Whether `list` is one basic block. A nested list is a branch or a loop; a short-circuit
139
+ * connective is a branch too — `p[3] = p[0] > v && p[4] < v` is two blocks, and agbcc keeps a
140
+ * copy live across that edge (the synthetic `leafand` row matches only through it). `walkExprs` for the
141
+ * reason `countReads` gives: it is the walk `repoint` rewrites by. */
142
+ function isStraightLine(list: Stmt[]): boolean {
143
+ if (list.some((st) => stmtLists(st).length > 0)) {
144
+ return false;
145
+ }
146
+ for (const e of walkExprs(list)) {
147
+ if (e.k === 'bin' && (e.op === '&&' || e.op === '||')) {
148
+ return false;
149
+ }
150
+ }
151
+ return true;
152
+ }
153
+
127
154
  /** How many `var n` leaves the region holds, nested statements included — the region rules'
128
155
  * yardstick for whether a copy has a range to shorten.
129
156
  *
@@ -176,7 +203,8 @@ function regions(body: Stmt[]): { at: number[]; list: Stmt[]; underLoop: boolean
176
203
  * for > do-while > if > store). This runs once per region per parameter per candidate, and each
177
204
  * candidate is a compile. */
178
205
  function repoint(list: Stmt[], n: string, to: string): Stmt[] {
179
- const inExpr = (e: Expr): Expr => (e.k === 'var' && e.name === n ? { ...e, name: to } : mapExprChildren(e, inExpr));
206
+ const inExpr = (e: Expr): Expr =>
207
+ (e.k === 'var' || e.k === 'postincr') && e.name === n ? { ...e, name: to } : mapExprChildren(e, inExpr);
180
208
  return list.map((s) => mapStmtExprs(s, inExpr));
181
209
  }
182
210
 
@@ -238,7 +266,11 @@ export function argCopyUnder(
238
266
  if (reads === 0) {
239
267
  continue;
240
268
  }
241
- const regionRefused = firstRejection(regionGates, { reads, underLoop: r.underLoop });
269
+ const regionRefused = firstRejection(regionGates, {
270
+ reads,
271
+ underLoop: r.underLoop,
272
+ straightLine: isStraightLine(r.list),
273
+ });
242
274
  if (regionRefused !== null) {
243
275
  refusals.set(regionRefused, (refusals.get(regionRefused) ?? 0) + 1);
244
276
  continue;