@asmlift/core 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +34 -30
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +248 -0
  4. package/src/backend/c.ts +5 -2
  5. package/src/backend/cfamily.ts +236 -37
  6. package/src/backend/cpp.ts +114 -9
  7. package/src/backend/pascal.ts +39 -0
  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 +615 -79
  15. package/src/declare.ts +75 -5
  16. package/src/frontend/asmdata.ts +19 -5
  17. package/src/frontend/call-plan.ts +686 -0
  18. package/src/frontend/device-pins.ts +277 -0
  19. package/src/frontend/errors.ts +5 -0
  20. package/src/frontend/flags-edge.ts +101 -0
  21. package/src/frontend/fpu.ts +167 -0
  22. package/src/frontend/frame-objects.ts +2131 -0
  23. package/src/frontend/high-half.ts +2 -1
  24. package/src/frontend/local-object.ts +490 -0
  25. package/src/frontend/mips.ts +363 -40
  26. package/src/frontend/opaque.ts +97 -18
  27. package/src/frontend/ppc.ts +520 -129
  28. package/src/frontend/reloc-symbol.ts +94 -13
  29. package/src/frontend/splat.ts +108 -5
  30. package/src/frontend/ssa.ts +669 -76
  31. package/src/frontend/stackargs.ts +120 -39
  32. package/src/frontend/thumb.ts +2764 -2018
  33. package/src/ir/alias.ts +29 -0
  34. package/src/ir/core.ts +101 -3
  35. package/src/ir/discipline.ts +248 -0
  36. package/src/ir/float-bits.ts +47 -0
  37. package/src/ir/opcodes.ts +84 -112
  38. package/src/ir/parse.ts +1 -0
  39. package/src/ir/types.ts +118 -2
  40. package/src/ir/verify.ts +150 -3
  41. package/src/l3/address.ts +5 -0
  42. package/src/l3/argcopy.ts +38 -6
  43. package/src/l3/ast.ts +186 -14
  44. package/src/l3/coalesce.ts +21 -13
  45. package/src/l3/dce.ts +32 -10
  46. package/src/l3/gates.ts +9 -3
  47. package/src/l3/hoist.ts +8 -7
  48. package/src/l3/initfirst.ts +27 -9
  49. package/src/l3/intcell.ts +97 -0
  50. package/src/l3/mentions.ts +10 -4
  51. package/src/l3/narrowdecl.ts +218 -0
  52. package/src/l3/nearbase.ts +10 -5
  53. package/src/l3/parkfirst.ts +8 -5
  54. package/src/l3/pollguard.ts +3 -2
  55. package/src/l3/ptrcell.ts +357 -0
  56. package/src/l3/regspell.ts +17 -3
  57. package/src/l3/reindex.ts +10 -7
  58. package/src/l3/symbol-refs.ts +104 -23
  59. package/src/l3/tailmerge.ts +6 -19
  60. package/src/l3/typing.ts +137 -25
  61. package/src/l3/unreduce.ts +4 -4
  62. package/src/l3/volstore.ts +96 -7
  63. package/src/mangle.ts +11 -1
  64. package/src/pattern/engine.ts +19 -16
  65. package/src/pipeline.ts +57 -12
  66. package/src/proto-context.ts +788 -0
  67. package/src/proto.ts +689 -50
  68. package/src/raise/arrays.ts +4 -3
  69. package/src/raise/declared-volatile.ts +131 -0
  70. package/src/raise/divpow2.ts +2 -2
  71. package/src/raise/errors.ts +17 -0
  72. package/src/raise/floathelpers.ts +243 -0
  73. package/src/raise/globalshape.ts +11 -5
  74. package/src/raise/gvn.ts +26 -5
  75. package/src/raise/narrow.ts +29 -2
  76. package/src/raise/narrowlocal.ts +11 -6
  77. package/src/raise/offsetnames.ts +510 -0
  78. package/src/raise/pairparams.ts +81 -0
  79. package/src/raise/paramwidth.ts +7 -0
  80. package/src/raise/pre-recovery.ts +78 -4
  81. package/src/raise/recover.ts +14 -10
  82. package/src/raise/retsink.ts +1 -1
  83. package/src/raise/shortcircuit.ts +41 -19
  84. package/src/raise/softdiv.ts +21 -23
  85. package/src/raise/struct-arrays.ts +5 -4
  86. package/src/raise/structs.ts +311 -98
  87. package/src/raise/truncload.ts +366 -0
  88. package/src/raise/widehelpers.ts +106 -0
  89. package/src/rank-declare.ts +30 -5
  90. package/src/rank-variations.ts +36 -8
  91. package/src/rank.ts +276 -80
  92. package/src/runtime-helpers.ts +287 -0
  93. package/src/stillborn.ts +289 -0
  94. package/src/structure/analysis.ts +1285 -448
  95. package/src/structure/hazards.ts +633 -58
  96. package/src/structure/local-statics.ts +381 -0
  97. package/src/structure/namecoalesce.ts +1 -1
  98. package/src/structure/redundant-test.ts +99 -0
  99. package/src/structure/structure.ts +1772 -395
  100. package/src/structure/switch-recover.ts +159 -78
  101. package/src/symbols.ts +38 -3
  102. package/src/target.ts +662 -59
  103. package/src/trace.ts +44 -24
  104. package/src/variation-definitions.ts +119 -1
  105. package/src/variation-tokens.ts +20 -6
@@ -53,37 +53,24 @@
53
53
  // lifts alike. What no arm of the fan spells is the third form, the hidden statement merged
54
54
  // while KEEPING the record's order.
55
55
  //
56
- // SCOPE. Only `assign`/`store`/`exprstmt` merge, compared structurally through `exprEquals`.
56
+ // SCOPE. Only `assign`/`store`/`exprstmt` merge, compared structurally through `stmtEquals`.
57
57
  // Control flow (`break`/`continue`/`return`) is excluded: moving one out of an arm changes which
58
- // statements the arm can still reach. Nested `if`/loop/`switch` statements are excluded because
59
- // comparing them needs a full `Stmt` congruence, and there is no second inhabitant for one — the
60
- // `Expr`-level comparison is the part that already exists, is tested, and is all this needs.
58
+ // statements the arm can still reach. Nested `if`/loop/`switch` statements are excluded: merging
59
+ // one needs a congruence over loops and `switch` too, which `stmtEquals` does not have, and there
60
+ // is no second inhabitant for one.
61
61
  //
62
62
  // An `ASMLIFT_ERROR` marker ending both arms merges like anything else. The gap stays loud (the
63
63
  // artifact still refuses to compile) but `collectMarkers` then reports it once rather than twice,
64
64
  // which is accurate — it is one gap that ran on both paths.
65
65
  import type { SFn, Stmt } from './ast';
66
- import { exprEquals } from './ast';
66
+ import { stmtEquals } from './ast';
67
67
 
68
68
  /** Statements this pass may move. Deliberately narrow — see SCOPE. */
69
69
  type Mergeable = Extract<Stmt, { k: 'assign' } | { k: 'store' } | { k: 'exprstmt' }>;
70
70
  const isMergeable = (s: Stmt): s is Mergeable => s.k === 'assign' || s.k === 'store' || s.k === 'exprstmt';
71
71
 
72
72
  /** Do these two statements write the same thing from the same expression? */
73
- function sameStmt(a: Stmt, b: Stmt): boolean {
74
- if (!isMergeable(a) || !isMergeable(b) || a.k !== b.k) {
75
- return false;
76
- }
77
- if (a.k === 'assign' && b.k === 'assign') {
78
- return a.name === b.name && exprEquals(a.value, b.value);
79
- }
80
- if (a.k === 'store' && b.k === 'store') {
81
- return exprEquals(a.lval, b.lval) && exprEquals(a.value, b.value);
82
- }
83
- const av = (a as Extract<Stmt, { k: 'exprstmt' }>).value;
84
- const bv = (b as Extract<Stmt, { k: 'exprstmt' }>).value;
85
- return exprEquals(av, bv);
86
- }
73
+ const sameStmt = (a: Stmt, b: Stmt): boolean => isMergeable(a) && isMergeable(b) && stmtEquals(a, b);
87
74
 
88
75
  /** Rewrite one statement, then the list it lives in. */
89
76
  function rewrite(s: Stmt): Stmt[] {
package/src/l3/typing.ts CHANGED
@@ -17,8 +17,8 @@
17
17
  // Callers choose their conservative direction: the emission guard treats `undefined` as "not
18
18
  // provably a pointer" (adds a cast — valid C either way); the deref contract treats `undefined`
19
19
  // as "not provably wrong" (no error).
20
- import { IrType, T, scalarTypeForAccess } from '../ir/types';
21
- import { type Expr, type SFn, exprChildren } from './ast';
20
+ import { IrType, T, memberOf, scalarTypeForAccess } from '../ir/types';
21
+ import { type BinOp, type Expr, type SFn, exprChildren } from './ast';
22
22
 
23
23
  /** The declared type of a printed variable — the env `exprCType` judges rendered C against.
24
24
  * THE one copy of the SFn→env derivation — printers, contracts and L3 respell variations alike: each
@@ -31,6 +31,9 @@ export function declaredTypes(fn: SFn): VarTypes {
31
31
  for (const g of fn.globals ?? []) {
32
32
  m.set(g.name, g.type);
33
33
  }
34
+ for (const st of fn.statics ?? []) {
35
+ m.set(st.name, st.type);
36
+ }
34
37
  for (const p of fn.params) {
35
38
  m.set(p.name, p.type);
36
39
  }
@@ -55,6 +58,11 @@ export function ptrElemBytes(to: IrType): number {
55
58
  export interface PrintEnv {
56
59
  readonly type: VarTypes;
57
60
  readonly volatilePointee: (name: string) => boolean;
61
+ /** a callee's declared parameter types, on a function compiled as C++ (`SFn.declaredArgs`,
62
+ * `SFn.dialect`) */
63
+ readonly declaredArgs: (callee: string) => readonly (string | undefined)[] | undefined;
64
+ /** a function-scope static this function defines `const` (`SFn.statics`) */
65
+ readonly constStatic: (name: string) => boolean;
58
66
  }
59
67
 
60
68
  export function printEnv(fn: SFn): PrintEnv {
@@ -63,7 +71,16 @@ export function printEnv(fn: SFn): PrintEnv {
63
71
  const vol = new Set(
64
72
  fn.locals.filter((l) => l.type.kind === 'ptr' && (l.pointeeVolatile || l.volatile)).map((l) => l.name),
65
73
  );
66
- return { type: declaredTypes(fn), volatilePointee: (n) => vol.has(n) };
74
+ const constStatics = new Set((fn.statics ?? []).filter((s) => s.const).map((s) => s.name));
75
+ return {
76
+ type: declaredTypes(fn),
77
+ volatilePointee: (n) => vol.has(n),
78
+ declaredArgs: (callee) =>
79
+ fn.dialect === 'c++' && fn.declaredArgs && Object.hasOwn(fn.declaredArgs, callee)
80
+ ? fn.declaredArgs[callee]
81
+ : undefined,
82
+ constStatic: (n) => constStatics.has(n),
83
+ };
67
84
  }
68
85
 
69
86
  /** Does anything under `e` assert a VOLATILE POINTEE? The two spellings that carry the qualifier
@@ -103,15 +120,81 @@ export function derefStrideOk(rt: IrType | undefined, width: number, signed: boo
103
120
  return false;
104
121
  }
105
122
 
106
- /** The USUAL ARITHMETIC CONVERSIONS over two rendered operands: at equal rank, unsigned wins.
107
- * Either side unknown leaves the result unknown — EXCEPT when the known side is unsigned, which
108
- * already decides it.
123
+ /** The INTEGER RANK a rendered expression carries, as a width in bits: 64 only where it can be
124
+ * PROVEN, 32 everywhere else. The complement of `renderedIntSignedness` — that one models the
125
+ * signedness `exprCType` reports uniformly as `s32`, this one models the width it reports the same
126
+ * way — and the two together are what the usual arithmetic conversions need.
127
+ *
128
+ * THE COMPLEMENT PARTS COMPANY WITH `exprCType` AT ONE NODE, AND IT IS `call`. There `exprCType`
129
+ * is UNINFORMED by design and answers `undefined`, while this reads the `wide64` stamp the
130
+ * structurer put on the node and can answer 64. A reader who reaches for `exprCType` on a call
131
+ * will be told the information does not exist; it does, and it is here.
132
+ *
133
+ * ITS SOUNDNESS IS AN ENUMERATION OF THE PRODUCERS, so here is the enumeration. A 64-bit value
134
+ * reaches a rendered expression in exactly four ways:
135
+ * 1. a `var`/`postincr` whose DECLARED type is 64 bits wide — read off `varType`;
136
+ * 2. a `cast` to a 64-bit type — read off the node;
137
+ * 3. a `call` the structurer STAMPED `wide64` — read off the node, and it is the one case where
138
+ * the rank cannot be derived from anything visible here: a callee's return type comes from a
139
+ * prototype outside the emitted function, which is why `exprCType` answers `undefined` for a
140
+ * call and why the stamp exists (l3/ast.ts carries what it means);
141
+ * 4. an arithmetic node over one of those — the recursion below.
142
+ * It does NOT arrive through MEMORY, because `contracts.ts` `SCALAR_WIDTHS` is {1,2,4} and there
143
+ * is no 64-bit global; nor through a `const`, because a 64-bit literal only folds inside C `int`
144
+ * range, where 32 is the right answer for the literal's own type.
145
+ *
146
+ * AN UNSTAMPED CALL IS 32 AND THAT IS THE SAFE ANSWER, not a claim: it is what C makes of a callee
147
+ * nobody declared, and every consumer of this rank takes a cast on a 32 it doubts and takes none on
148
+ * a 64 — so the direction that would be wrong silently is the one this never guesses.
149
+ *
150
+ * A SHIFT takes the rank of its left operand alone; every other ARITHMETIC node takes the wider of
151
+ * the two. That is C, and it is also why this is not simply `exprCType(e).width`.
152
+ *
153
+ * THE NODES THAT ARE NOT ARITHMETIC ONES are the same list `renderedIntSignedness` enumerates, and
154
+ * they yield `int` however wide their operands: a comparison, a logical connective, and `!`. `-`
155
+ * and `~` are not in it, because each carries the promoted type of its operand. */
156
+ export function exprIntWidth(e: Expr, varType: VarTypes): 32 | 64 {
157
+ const wide = (t: IrType | undefined): boolean => t?.kind === 'int' && t.width === 64;
158
+ switch (e.k) {
159
+ case 'var':
160
+ case 'postincr':
161
+ case 'cast':
162
+ case 'index':
163
+ case 'field':
164
+ return wide(exprCType(e, varType)) ? 64 : 32;
165
+ case 'call':
166
+ return e.wide64 === true ? 64 : 32;
167
+ case 'un':
168
+ return e.op === '!' ? 32 : exprIntWidth(e.e, varType);
169
+ case 'bin':
170
+ if (INT_RESULT_BINOPS.has(e.op)) {
171
+ return 32;
172
+ }
173
+ if (e.op === '<<' || e.op === '>>' || e.op === '>>>') {
174
+ return exprIntWidth(e.l, varType);
175
+ }
176
+ return exprIntWidth(e.l, varType) === 64 || exprIntWidth(e.r, varType) === 64 ? 64 : 32;
177
+ default:
178
+ return 32;
179
+ }
180
+ }
181
+
182
+ /** The float operators (l3/ast.ts BinOp): their result is the float their operands are, never an
183
+ * integer, so none of the integer conversion rules below applies to one. */
184
+ export const FLOAT_BINOPS: ReadonlySet<BinOp> = new Set<BinOp>(['f+', 'f-', 'f*', 'f/']);
185
+
186
+ /** The binary operators whose RESULT is a C `int` whatever their operands are — read by both
187
+ * halves of the conversion model, which is the point of naming them once. */
188
+ const INT_RESULT_BINOPS = new Set<BinOp>(['<', '<=', '>', '>=', '==', '!=', '&&', '||']);
189
+
190
+ /** The USUAL ARITHMETIC CONVERSIONS over two rendered operands.
109
191
  *
110
- * That exception is the one place this returns a DEFINITE answer from an unknown operand, and it
111
- * is sound only because every integer here is rank `int`: at UNEQUAL rank C converts to the wider
112
- * type first, so `unsigned int & long long` is SIGNED. Core has no 64-bit integer type at all
113
- * (the decomp typedef vocabulary stops at 32 — see contracts.ts SCALAR_WIDTHS), so the
114
- * unequal-rank case cannot arise. Adding one would invalidate this.
192
+ * AT EQUAL RANK, unsigned wins, and either side unknown leaves the result unknown — EXCEPT when
193
+ * the known side is unsigned, which already decides it. AT UNEQUAL RANK C converts to the wider
194
+ * type first, so the WIDER side's signedness decides outright: `unsigned int / long long` is
195
+ * SIGNED, and taking the equal-rank shortcut there would spell an unsigned divide over a signed
196
+ * one. `exprIntWidth` is what tells the two cases apart, and it is TOTAL, so the rank is always
197
+ * known and only the signedness can be undetermined.
115
198
  *
116
199
  * Exported because two consumers ask it about an operator whose own rendering they are deciding,
117
200
  * so they cannot ask `renderedIntSignedness` about the node: the C-family backend's operand pin,
@@ -119,6 +202,11 @@ export function derefStrideOk(rt: IrType | undefined, width: number, signed: boo
119
202
  export function arithConversionSignedness(l: Expr, r: Expr, varType: VarTypes): boolean | undefined {
120
203
  const ls = renderedIntSignedness(l, varType);
121
204
  const rs = renderedIntSignedness(r, varType);
205
+ const lw = exprIntWidth(l, varType);
206
+ const rw = exprIntWidth(r, varType);
207
+ if (lw !== rw) {
208
+ return lw > rw ? ls : rs;
209
+ }
122
210
  if (ls === false || rs === false) {
123
211
  return false;
124
212
  }
@@ -158,10 +246,16 @@ export function arithConversionSignedness(l: Expr, r: Expr, varType: VarTypes):
158
246
  export function renderedIntSignedness(e: Expr, varType: VarTypes): boolean | undefined {
159
247
  const rec = (x: Expr): boolean | undefined => renderedIntSignedness(x, varType);
160
248
  // an lvalue-ish leaf: its C type is a declaration / an explicit cast / a carried access width
249
+ // Anything narrower than `int` promotes and comes out SIGNED whatever it was declared. `int` and
250
+ // `long long` keep their own signedness, because no promotion applies to either. A width that is
251
+ // NEITHER is not a C type at all — an `index` node carries a struct STRIDE as its width — and
252
+ // stays undetermined, which is what every consumer already takes a cast on.
161
253
  const promoted = (t: IrType | undefined): boolean | undefined =>
162
- t?.kind !== 'int' ? undefined : t.width < 32 ? true : t.width === 32 ? t.signed : undefined;
254
+ t?.kind !== 'int' ? undefined : t.width < 32 ? true : t.width === 32 || t.width === 64 ? t.signed : undefined;
163
255
  switch (e.k) {
164
256
  case 'var':
257
+ // `v++` renders the value the local held, so it promotes exactly as the bare name does.
258
+ case 'postincr':
165
259
  case 'cast':
166
260
  case 'index':
167
261
  case 'field':
@@ -174,10 +268,16 @@ export function renderedIntSignedness(e: Expr, varType: VarTypes): boolean | und
174
268
  // folded to a shift — where `a / (s32)-2147483648` calls `__divsi3`.
175
269
  case 'const':
176
270
  return e.value > -2147483648 && e.value <= 2147483647;
177
- // `-x` / `~x` carry the PROMOTED type of the operand; `!x` is `int`.
271
+ case 'fconst':
272
+ return undefined;
273
+ // `-x` / `~x` carry the PROMOTED type of the operand; `!x` is `int`. A float has no integer
274
+ // signedness at all.
178
275
  case 'un':
179
- return e.op === '!' ? true : rec(e.e);
276
+ return e.op === 'f-' ? undefined : e.op === '!' ? true : rec(e.e);
180
277
  case 'bin': {
278
+ if (FLOAT_BINOPS.has(e.op)) {
279
+ return undefined;
280
+ }
181
281
  // The SIGNEDNESS-CARRYING pairs — the ops the tower keeps apart because C spells each pair
182
282
  // with one token and picks between them from the operand types (l3/ast.ts BinOp). The
183
283
  // C-family backend PINS their operands, so what gets printed renders as the op says
@@ -194,8 +294,8 @@ export function renderedIntSignedness(e: Expr, varType: VarTypes): boolean | und
194
294
  if (e.op === '<<') {
195
295
  return rec(e.l);
196
296
  }
197
- // Comparisons and the logical connectives yield `int`.
198
- if (['<', '<=', '>', '>=', '==', '!=', '&&', '||'].includes(e.op)) {
297
+ // Comparisons and the logical connectives yield `int`, which is signed.
298
+ if (INT_RESULT_BINOPS.has(e.op)) {
199
299
  return true;
200
300
  }
201
301
  return arithConversionSignedness(e.l, e.r, varType);
@@ -257,17 +357,25 @@ export function exprCType(e: Expr, varType: (name: string) => IrType | undefined
257
357
  const rec = (x: Expr): IrType | undefined => exprCType(x, varType);
258
358
  switch (e.k) {
259
359
  case 'var':
360
+ case 'postincr':
260
361
  return varType(e.name);
261
362
  // An integer literal spells as a plain C `int` — NEVER a pointer, whatever the value's
262
363
  // recovered type was. This is the exact gap the emission guard exists to bridge.
263
364
  case 'const':
264
365
  return T.s(32);
366
+ case 'fconst':
367
+ return T.f64();
265
368
  case 'cast':
266
369
  return e.to;
267
- // `-`/`~` yield the promoted integer; `!` yields int. None yields a pointer.
370
+ // `-`/`~` yield the promoted integer; `!` yields int. None yields a pointer. A float negation
371
+ // yields the float it negates.
268
372
  case 'un':
269
- return T.s(32);
373
+ return e.op === 'f-' ? rec(e.e) : T.s(32);
270
374
  case 'bin': {
375
+ // Both operands are one float type (ir/verify.ts holds the IR op to that), so the left names it.
376
+ if (FLOAT_BINOPS.has(e.op)) {
377
+ return rec(e.l);
378
+ }
271
379
  if (e.op === '+' || e.op === '-') {
272
380
  const l = rec(e.l);
273
381
  const r = rec(e.r);
@@ -291,7 +399,14 @@ export function exprCType(e: Expr, varType: (name: string) => IrType | undefined
291
399
  return T.s(32);
292
400
  }
293
401
  // A callee's C return type comes from a prototype OUTSIDE the emitted function (the
294
- // project ctx / C89 implicit int) — not statically knowable here.
402
+ // project ctx / C89 implicit int) — not derivable from the tree here.
403
+ //
404
+ // THE NODE MAY STILL CARRY ITS RANK, and that is a different question from its TYPE:
405
+ // `exprIntWidth` reads the `wide64` stamp and answers 64 for a call whose callee the frontend
406
+ // was told returns a pair. Nothing is read off it here, because the stamp is a width and this
407
+ // owes its callers a full `IrType` — signedness included, which no source states. Widening
408
+ // this to answer `s64` from the stamp would be a signedness claim nobody made, over the one
409
+ // node every rendered function has; the rank-only reader is the honest half.
295
410
  case 'call':
296
411
  return undefined;
297
412
  case 'index': {
@@ -319,13 +434,10 @@ export function exprCType(e: Expr, varType: (name: string) => IrType | undefined
319
434
  return scalarTypeForAccess(e.width, e.signed);
320
435
  }
321
436
  case 'field': {
322
- // `base->name` (base: ptr-to-struct) or `base.name` (base: struct value, an array element).
437
+ // `base->name` (base: ptr-to-struct) or `base.name` (base: struct value, an array element,
438
+ // or a union member's view).
323
439
  const bt = rec(e.base);
324
- const st = bt?.kind === 'ptr' ? bt.to : bt;
325
- if (st?.kind !== 'struct') {
326
- return undefined;
327
- }
328
- return st.fields.find((f) => f.name === e.name)?.type;
440
+ return memberOf(bt?.kind === 'ptr' ? bt.to : bt, e.name)?.type;
329
441
  }
330
442
  case 'marker':
331
443
  return undefined;
@@ -440,10 +440,10 @@ function arithScale(t: IrType | undefined): number | null {
440
440
  * refusal.
441
441
  *
442
442
  * MEASURED, so this is a stated zero and not an assumption: instrumented at the deletion site, it
443
- * fires on none of the three agbcc rows that spill AND lift — `spillorder`, `dma_fill_uninit`,
444
- * `uninit_spill` — so the clause costs no candidate today. (`spill10` spills too but declines in the
445
- * Thumb frontend, so it never reaches this pass and its zero says nothing.) It is here for the day
446
- * one does. */
443
+ * fires on none of the agbcc rows that spill AND lift — `spillorder`, `dma_fill_uninit`,
444
+ * `uninit_spill`, `spill10`, `spillarg` — so the clause costs no candidate today. (The last two
445
+ * bring no local to these gates at all, so their zero says nothing.) It is here for the day one
446
+ * does. */
447
447
  function declarationPins(l: SFn['locals'][number]): boolean {
448
448
  return (
449
449
  l.volatile !== undefined ||
@@ -92,9 +92,10 @@
92
92
  // both configurations and with any window. No row demands the read spelling, and a variation with no
93
93
  // inhabitant is what "earn the level" forbids.
94
94
  import { type IrType, T, scalarTypeForAccess } from '../ir/types';
95
- import { cellAddress, inRange } from './address';
96
- import { type Expr, type SFn, type Stmt, stmtChildren } from './ast';
95
+ import { cellAddress, inRange, qualifiedBase } from './address';
96
+ import { type Expr, type SFn, type Stmt, dotBase, stmtChildren } from './ast';
97
97
  import { type Gate, firstRejection } from './gates';
98
+ import { derefStrideOk } from './typing';
98
99
 
99
100
  /** One STORE lvalue as the gates read it. */
100
101
  interface AccessCtx {
@@ -141,9 +142,6 @@ export const VOL_STORE_GATES: readonly Gate<AccessCtx>[] = [
141
142
  },
142
143
  ];
143
144
 
144
- /** Does this base already assert volatility — a `volatile` cast at any depth of the cast chain? */
145
- const qualifiedBase = (e: Expr): boolean => e.k === 'cast' && (e.volatile === true || qualifiedBase(e.e));
146
-
147
145
  /** The pointee the deref cast carries: the access's own scalar type. */
148
146
  const pointee = (ix: Extract<Expr, { k: 'index' }>): IrType => scalarTypeForAccess(ix.width, ix.signed);
149
147
 
@@ -169,8 +167,17 @@ function qualify(lval: Expr, window: readonly [number, number] | undefined): Exp
169
167
  if (firstRejection(VOL_STORE_GATES, ctx) !== null) {
170
168
  return lval;
171
169
  }
172
- // An existing scalar pointer cast takes the qualifier in place; a bare const gets one minted,
173
- // exactly the node backend/cfamily.ts's own deref legalization would have synthesized.
170
+ return qualifiedAccess(lval);
171
+ }
172
+
173
+ /** An indexed access with `volatile` on the pointee of its deref cast. An existing scalar pointer
174
+ * cast takes the qualifier in place; a bare base gets one minted, exactly the node
175
+ * backend/cfamily.ts's own deref legalization would have synthesized. Also how
176
+ * {@link qualifiedMemoryAccess} spells an indexed access the lift marked `volatile`. */
177
+ export function qualifiedAccess(lval: Extract<Expr, { k: 'index' }>): Extract<Expr, { k: 'index' }> {
178
+ if (qualifiedBase(lval.base)) {
179
+ return lval;
180
+ }
174
181
  const base: Expr =
175
182
  lval.base.k === 'cast' && lval.base.to.kind === 'ptr'
176
183
  ? { ...lval.base, volatile: true }
@@ -178,6 +185,88 @@ function qualify(lval: Expr, window: readonly [number, number] | undefined): Exp
178
185
  return { ...lval, base };
179
186
  }
180
187
 
188
+ /** A memory access the lift marked `volatile` (frontend/device-pins.ts), spelled through a
189
+ * `volatile` pointee: an indexed access by {@link qualifiedAccess}; a recovered struct member
190
+ * through the struct pointer it is reached by — `((volatile struct S *)0x40000B0)[ch].field_0`,
191
+ * `((volatile struct S *)p)->field_0` — whether that pointer is already a cast or a variable
192
+ * `typeOf` types as one; and a union view through the member it views. A named global — `gSym`,
193
+ * `gSym.field` — is returned unchanged, since its declaration owns its qualifiers. Null when the
194
+ * pointer the access is reached by has no type to cast it to, for the caller to refuse rather
195
+ * than drop the qualifier. */
196
+ export function qualifiedMemoryAccess(e: Expr, typeOf: (x: Expr) => IrType | undefined): Expr | null {
197
+ if (e.k === 'index') {
198
+ return qualifiedAccess(e);
199
+ }
200
+ if (e.k !== 'field') {
201
+ return e;
202
+ }
203
+ const element = dotBase(e);
204
+ if (element !== undefined) {
205
+ const base = qualifiedPointer(element.base, typeOf);
206
+ return base === null ? null : { ...e, base: { ...element, base } };
207
+ }
208
+ if (e.dot === true) {
209
+ if (e.base.k !== 'field') {
210
+ return e;
211
+ }
212
+ const member = qualifiedMemoryAccess(e.base, typeOf);
213
+ return member === null ? null : { ...e, base: member };
214
+ }
215
+ const base = qualifiedPointer(e.base, typeOf);
216
+ return base === null ? null : { ...e, base };
217
+ }
218
+
219
+ /** Does this access reach the object it names through a pointer cast — one the tree spells,
220
+ * `((struct T *)&gSym)->f`, or one the C-family printer puts on a base that does not stride the
221
+ * access as spelled (backend/cfamily.ts `legalizedIndexBase`), `((u16 *)&gSym)[3]` — rather than
222
+ * through the object's own name, element or member? The pointee of such a cast carries none of the
223
+ * object's declared qualifiers unless it says so itself. `typeOf` is the rendered C type the
224
+ * printer judges a base by. */
225
+ export function castOffDeclaration(e: Expr, typeOf: (x: Expr) => IrType | undefined): boolean {
226
+ if (e.k === 'field') {
227
+ // A dot member over an element prints that element's base as spelled.
228
+ const element = dotBase(e);
229
+ if (element !== undefined) {
230
+ return addressCast(element.base, typeOf);
231
+ }
232
+ return e.dot === true ? castOffDeclaration(e.base, typeOf) : addressCast(e.base, typeOf);
233
+ }
234
+ if (e.k !== 'index') {
235
+ return false;
236
+ }
237
+ if (addressCast(e.base, typeOf)) {
238
+ return true;
239
+ }
240
+ if (e.lead !== undefined && e.lead.length > 0) {
241
+ return false; // a multidimensional global's subscript prints its base as spelled
242
+ }
243
+ return !derefStrideOk(
244
+ typeOf(e.base) ?? (e.baseElem !== undefined ? T.ptr(e.baseElem) : undefined),
245
+ e.width,
246
+ e.signed,
247
+ );
248
+ }
249
+
250
+ /** Is there a cast between this address and the object it is taken from: one on it, under its
251
+ * arithmetic, or on the access whose element or member it is? */
252
+ const addressCast = (p: Expr, typeOf: (x: Expr) => IrType | undefined): boolean =>
253
+ p.k === 'cast' ||
254
+ (p.k === 'bin' && (addressCast(p.l, typeOf) || addressCast(p.r, typeOf))) ||
255
+ ((p.k === 'index' || p.k === 'field') && castOffDeclaration(p, typeOf));
256
+
257
+ /** A pointer expression carrying `volatile` on its pointee: a pointer cast takes it in place, and
258
+ * any other pointer is cast to its own type with it. */
259
+ function qualifiedPointer(p: Expr, typeOf: (x: Expr) => IrType | undefined): Expr | null {
260
+ if (qualifiedBase(p)) {
261
+ return p;
262
+ }
263
+ if (p.k === 'cast' && p.to.kind === 'ptr') {
264
+ return { ...p, volatile: true };
265
+ }
266
+ const t = typeOf(p);
267
+ return t?.kind === 'ptr' ? { k: 'cast', to: t, volatile: true, e: p } : null;
268
+ }
269
+
181
270
  /** How many stores this tree would qualify — the enumeration gate, so a function with no device
182
271
  * store costs one walk and no candidate.
183
272
  *
package/src/mangle.ts CHANGED
@@ -178,7 +178,17 @@ export function demangledName(sym: string): string | null {
178
178
  return null;
179
179
  }
180
180
 
181
- /** Spell a CppType as C++ source (`Vec *`, `unsigned int`). */
181
+ /** Spell a CppType as C++ source where no declarator is involved — a return type, a cast
182
+ * (`Vec *`, `unsigned int`). */
182
183
  export function spellType(t: CppType): string {
183
184
  return t.base + (t.ptr ? ' ' + '*'.repeat(t.ptr) : '');
184
185
  }
186
+
187
+ /** Declare a name of a CppType, C declarator rules: a pointer binds its `*` to the declarator
188
+ * (`Vec *p`), everything else is the prefix `spellType name`. The C-family backend's `cDeclare`
189
+ * says the same of an `IrType`; C++ has its own type vocabulary (`CppType`, from the mangled
190
+ * name) and so needs its own declarator, or one function spells its pointers two ways — the type
191
+ * prefix in the parameter list and the declarator form in the body. */
192
+ export function declareCpp(t: CppType, name: string): string {
193
+ return t.ptr > 0 ? `${t.base} ${'*'.repeat(t.ptr)}${name}` : `${t.base} ${name}`;
194
+ }
@@ -6,7 +6,8 @@
6
6
  // Crucially, rewrites go through replaceAllUsesWith + DCE — never in-place opcode
7
7
  // mutation of a live value.
8
8
  import { Block, Fn, Op, Value, defOpMap, mkOp, mkValue, replaceAllUsesWith } from '../ir/core';
9
- import { EFFECTFUL_OPS, NEGATED_ICMP, ORDER_SENSITIVE_OPS, type Opcode, isDceSafe } from '../ir/opcodes';
9
+ import { deletableWhenDead, effectful, orderSensitive } from '../ir/discipline';
10
+ import { NEGATED_ICMP, type Opcode } from '../ir/opcodes';
10
11
  import type { IrType } from '../ir/types';
11
12
  import { T } from '../ir/types';
12
13
 
@@ -589,17 +590,18 @@ function validatePattern(pat: RewritePattern): void {
589
590
  * just its def, and the question has to be asked over the cones: one pure `+ 1` between an effect
590
591
  * and the fold's operand is the difference between `f() % g()` and `(f() + 1) % g()`, and the
591
592
  * second one reorders exactly as the first does. Both cones become operands of ONE expression,
592
- * where C leaves their order unspecified. asmlift's inline-at-use model (structure/analysis.ts)
593
- * exempts exactly this case — "a sibling effect inlined into the SAME statement is not a reorder,
594
- * the recompiling compiler orders unsequenced operands of one expression exactly as it originally
595
- * chose to". That premise holds only when the expression asmlift re-spells is the one the source
596
- * wrote. A fold INVENTS an expression, so it must check.
593
+ * where C leaves their order unspecified. asmlift's inline-at-use model (structure/analysis.ts,
594
+ * the barrier scan) keeps two operands of one statement inline only in the order every corpus
595
+ * compiler gives back — a call before the reads beside it — but it judges the program the fold
596
+ * leaves, after the idiom's own ops are gone. The fold INVENTS the expression, so it checks the
597
+ * cones it joins itself.
597
598
  *
598
- * A cone's members are weighed by ORDER_SENSITIVE_OPS, not EFFECTFUL_OPS: a memory read answers
599
+ * A cone's members are weighed by `orderSensitive`, not `effectful`: a memory read answers
599
600
  * whichever stores ran before it, so hoisting a `load` over a `call` — one asmlift may itself be
600
601
  * passing the loaded pointer to — changes the answer as surely as swapping two calls. Two READS
601
- * are the exception and commute, the same fact the structurer states as "a load never bars a
602
- * load", so a hazard needs an EFFECT on at least one side; `*p % *q` is admitted.
602
+ * are the exception and commute, the same fact the structurer states as "a plain load never bars
603
+ * a load", so a hazard needs an EFFECT on at least one side; `*p % *q` is admitted. Two `volatile`
604
+ * device reads do not commute, and the structurer names the first of them itself.
603
605
  *
604
606
  * Refuses only what it must — the alternative spelling (the idiom written out) names the operands
605
607
  * and states the order, so a refusal is a loud, correct, slightly-worse-scoring answer. */
@@ -654,20 +656,18 @@ function reordersUnsequenced(
654
656
  return cone;
655
657
  };
656
658
  const [lc, rc] = names.map(coneOf);
657
- const sensitive = (c: Set<Op>) => [...c].filter((o) => ORDER_SENSITIVE_OPS.has(o.opcode));
659
+ const sensitive = (c: Set<Op>) => [...c].filter(orderSensitive);
658
660
  for (const l of sensitive(lc)) {
659
661
  for (const r of sensitive(rc)) {
660
662
  // One op standing in BOTH cones is one evaluation and cannot be sequenced against itself; two
661
663
  // READS commute; and a right-cone op the machine ALREADY runs first loses nothing.
662
- if (l === r || at.get(l)! >= at.get(r)! || !(EFFECTFUL_OPS.has(l.opcode) || EFFECTFUL_OPS.has(r.opcode))) {
664
+ if (l === r || at.get(l)! >= at.get(r)! || !(effectful(l) || effectful(r))) {
663
665
  continue;
664
666
  }
665
667
  // The left one runs first, so an inlined `A op B` would swap them — UNLESS a sibling effect
666
668
  // stands between, which the inline-at-use model refuses to cross, forcing a named temp at the
667
669
  // def's own position. A cone member is no sibling: it is inlined into this very expression.
668
- if (
669
- !blk.ops.slice(at.get(l)! + 1, at.get(r)!).some((o) => EFFECTFUL_OPS.has(o.opcode) && !lc.has(o) && !rc.has(o))
670
- ) {
670
+ if (!blk.ops.slice(at.get(l)! + 1, at.get(r)!).some((o) => effectful(o) && !lc.has(o) && !rc.has(o))) {
671
671
  return true;
672
672
  }
673
673
  }
@@ -794,7 +794,8 @@ export function applyPattern(fn: Fn, pat: RewritePattern, target: PatternTarget)
794
794
  }
795
795
 
796
796
  /** Remove effect-free ops whose single result is unused, to a fixed point. Deletability is
797
- * derived from the ONE effect table in ir/opcodes.ts. */
797
+ * ir/discipline.ts `deletableWhenDead`, which keeps a read the lift pinned `volatile`: the machine
798
+ * made it, so the recompile has to. */
798
799
  export function dce(fn: Fn): void {
799
800
  let changed = true;
800
801
  while (changed) {
@@ -813,7 +814,9 @@ export function dce(fn: Fn): void {
813
814
  }
814
815
  }
815
816
  for (const b of fn.blocks) {
816
- const kept = b.ops.filter((op) => !(isDceSafe(op.opcode) && op.results.length === 1 && !used.has(op.results[0])));
817
+ const kept = b.ops.filter(
818
+ (op) => !(deletableWhenDead(op) && op.results.length === 1 && !used.has(op.results[0])),
819
+ );
817
820
  if (kept.length !== b.ops.length) {
818
821
  b.ops = kept;
819
822
  changed = true;