@asmlift/core 0.9.0 → 0.11.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 (117) 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 +2 -0
  5. package/src/backend/cfamily.ts +197 -61
  6. package/src/backend/cpp.ts +3 -1
  7. package/src/backend/lines.ts +10 -0
  8. package/src/backend/pascal.ts +47 -16
  9. package/src/cdecl/parse.ts +21 -0
  10. package/src/contracts.ts +246 -36
  11. package/src/declare.ts +69 -20
  12. package/src/frontend/arm-body.ts +155 -0
  13. package/src/frontend/asmdata.ts +9 -2
  14. package/src/frontend/call-plan.ts +1117 -61
  15. package/src/frontend/callee-body.ts +370 -0
  16. package/src/frontend/dead-reads.ts +628 -0
  17. package/src/frontend/device-pins.ts +33 -4
  18. package/src/frontend/emit.ts +4 -3
  19. package/src/frontend/fpu.ts +224 -17
  20. package/src/frontend/frame-objects.ts +1689 -206
  21. package/src/frontend/frontend.ts +84 -3
  22. package/src/frontend/high-half.ts +53 -4
  23. package/src/frontend/jump-table.ts +545 -0
  24. package/src/frontend/local-object.ts +158 -32
  25. package/src/frontend/mips.ts +2296 -217
  26. package/src/frontend/own-struct-return.ts +500 -0
  27. package/src/frontend/ppc.ts +2748 -245
  28. package/src/frontend/register-struct-call.ts +399 -0
  29. package/src/frontend/reloc-symbol.ts +22 -7
  30. package/src/frontend/ssa.ts +1721 -43
  31. package/src/frontend/stackargs.ts +199 -72
  32. package/src/frontend/thumb-entry.ts +340 -0
  33. package/src/frontend/thumb.ts +2400 -325
  34. package/src/ir/alias.ts +65 -2
  35. package/src/ir/bits.ts +1699 -4
  36. package/src/ir/core.ts +254 -9
  37. package/src/ir/discipline.ts +86 -21
  38. package/src/ir/entry.ts +157 -0
  39. package/src/{structure → ir}/loops.ts +6 -2
  40. package/src/ir/opcodes.ts +137 -9
  41. package/src/ir/parse.ts +1 -0
  42. package/src/ir/simplify.ts +61 -10
  43. package/src/ir/types.ts +86 -2
  44. package/src/ir/verify.ts +96 -11
  45. package/src/l3/argbase.ts +2 -0
  46. package/src/l3/ast.ts +582 -18
  47. package/src/l3/basecse.ts +68 -15
  48. package/src/l3/caseendgoto.ts +223 -0
  49. package/src/l3/coalesce.ts +42 -14
  50. package/src/l3/constinit.ts +410 -0
  51. package/src/l3/dce.ts +53 -9
  52. package/src/l3/initfirst.ts +42 -11
  53. package/src/l3/intcell.ts +2 -1
  54. package/src/l3/narrowdecl.ts +77 -36
  55. package/src/l3/pollguard.ts +8 -2
  56. package/src/l3/ptrcell.ts +33 -34
  57. package/src/l3/ptrfield.ts +123 -8
  58. package/src/l3/regspell.ts +11 -4
  59. package/src/l3/reindex.ts +295 -99
  60. package/src/l3/scopebase.ts +2 -0
  61. package/src/l3/storage.ts +98 -1
  62. package/src/l3/symbol-refs.ts +84 -6
  63. package/src/l3/tailmerge.ts +6 -2
  64. package/src/l3/typing.ts +108 -15
  65. package/src/l3/unreduce.ts +6 -5
  66. package/src/l3/volatileptr.ts +17 -54
  67. package/src/mangle.ts +30 -0
  68. package/src/pattern/engine.ts +361 -73
  69. package/src/pipeline.ts +1039 -38
  70. package/src/proto-context.ts +1039 -45
  71. package/src/proto.ts +555 -40
  72. package/src/raise/aggmembers.ts +290 -0
  73. package/src/raise/arrays.ts +5 -1
  74. package/src/raise/const.ts +25 -0
  75. package/src/raise/declared-volatile.ts +207 -31
  76. package/src/raise/divpow2.ts +192 -0
  77. package/src/raise/floathelpers.ts +254 -33
  78. package/src/raise/gvn.ts +3 -3
  79. package/src/raise/latch.ts +162 -6
  80. package/src/raise/memberarrays.ts +10 -4
  81. package/src/raise/offsetnames.ts +422 -36
  82. package/src/raise/pairparams.ts +33 -0
  83. package/src/raise/paramwidth.ts +196 -6
  84. package/src/raise/pre-recovery.ts +107 -21
  85. package/src/raise/pre-structure.ts +73 -0
  86. package/src/raise/recover.ts +12 -5
  87. package/src/raise/reroll.ts +674 -0
  88. package/src/raise/shortcircuit.ts +217 -34
  89. package/src/raise/softdiv.ts +1 -1
  90. package/src/raise/storesign.ts +153 -0
  91. package/src/raise/struct-arrays.ts +30 -11
  92. package/src/raise/structs.ts +396 -33
  93. package/src/raise/truncload.ts +4 -3
  94. package/src/raise/unpre.ts +594 -0
  95. package/src/raise/unthread.ts +400 -0
  96. package/src/rank-declare.ts +118 -12
  97. package/src/rank-variations.ts +186 -35
  98. package/src/rank.ts +536 -68
  99. package/src/runtime-helpers.ts +140 -22
  100. package/src/structure/analysis.ts +217 -8
  101. package/src/structure/arith-ops.ts +23 -0
  102. package/src/structure/cross-jumped-tail.ts +216 -0
  103. package/src/structure/globalaccess.ts +157 -10
  104. package/src/structure/hazards.ts +16 -64
  105. package/src/structure/local-statics.ts +145 -9
  106. package/src/structure/namecoalesce.ts +1 -1
  107. package/src/structure/pointer-spelling.ts +1106 -0
  108. package/src/structure/redundant-test.ts +1 -1
  109. package/src/structure/structure.ts +6192 -1213
  110. package/src/structure/switch-recover.ts +583 -120
  111. package/src/structure/unfolded-branch-constant.ts +90 -0
  112. package/src/symbols.ts +58 -15
  113. package/src/target.ts +1028 -54
  114. package/src/trace.ts +49 -17
  115. package/src/variation-definitions.ts +333 -20
  116. package/src/variation-gates.ts +4 -0
  117. package/src/variation-tokens.ts +68 -3
@@ -369,6 +369,27 @@ class Parser {
369
369
  if (specifiers === null) {
370
370
  return null;
371
371
  }
372
+ // C89's implicit `int` ahead of a declarator list: `extern gN, gM;` and `static gA[4];` read the
373
+ // first name as a type word, and in valid C no type is followed by `,`, `=` or `[`. The word goes
374
+ // back to being the first declarator. A lone `extern gN;` is a statement this cannot tell from
375
+ // an unknown type declaring nothing; `proto-context.ts` reads that one against the types the
376
+ // context declares.
377
+ const { storage, type, unknownWords } = specifiers;
378
+ const next = this.t.char(this.i);
379
+ if (
380
+ !this.cxx &&
381
+ (storage.includes('extern') || storage.includes('static')) &&
382
+ type.kind === 'words' &&
383
+ type.words.length === 1 &&
384
+ !BASIC.has(type.words[0]) &&
385
+ unknownWords.length === 0 &&
386
+ (next === COMMA || next === EQUALS || next === OPEN_BRACKET) &&
387
+ this.t.text(this.i - 1) === type.words[0]
388
+ ) {
389
+ this.i--;
390
+ specifiers.type = { kind: 'words', words: ['int'] };
391
+ specifiers.spelling = 'int';
392
+ }
372
393
  const declarators: Declarator[] = [];
373
394
  if (this.t.char(this.i) === SEMICOLON) {
374
395
  this.i++;
package/src/contracts.ts CHANGED
@@ -9,8 +9,9 @@ import { placedAt } from './ir/discipline';
9
9
  import { MEM_BASE_OPS } from './ir/opcodes';
10
10
  import { type IrType, memberOf, typeToString } from './ir/types';
11
11
  import { cellAddress, qualifiedBase } from './l3/address';
12
- import { type BinOp, type Expr, type SFn, type Stmt, calleeOf } from './l3/ast';
12
+ import { type BinOp, type Expr, type SFn, type Stmt, calleeOf, caseGotoViolation, switchArmSuccessors } from './l3/ast';
13
13
  import {
14
+ assignsIn,
14
15
  exprChildren,
15
16
  fieldSpellsDot,
16
17
  gapReasonFor,
@@ -19,9 +20,11 @@ import {
19
20
  stmtChildren,
20
21
  stmtExprs,
21
22
  stmtLists,
23
+ taintOverAssigns,
22
24
  walkExprs,
23
25
  } from './l3/ast';
24
26
  import { mentionedLocals } from './l3/mentions';
27
+ import { localAddresses } from './l3/storage';
25
28
  import { declaredTypes, exprCType } from './l3/typing';
26
29
  import { castOffDeclaration } from './l3/volstore';
27
30
 
@@ -188,17 +191,23 @@ function qualifiedAccessChain(e: Expr): boolean {
188
191
  }
189
192
 
190
193
  /** The global an access's address is named by: its base through casts, address arithmetic and the
191
- * element or member a dot member selects from, down to `gSym` or `&gSym`. Null when the base is
194
+ * element or member a dot member selects from, down to `&gSym`, or to a `gSym` that spells its own
195
+ * address — an array's name, which decays to it (`addresses`), or the struct a dot member selects
196
+ * from (`dotted`). Any other bare `gSym` is the object's VALUE: `*(u16 *)gPtr` reads what the
197
+ * pointer points at, not the pointer, so the access names no object. Null too when the base is
192
198
  * anything else — another access, a local. */
193
- function accessedObject(base: Expr): string | null {
199
+ function accessedObject(base: Expr, addresses: ReadonlySet<string>, dotted = false): string | null {
194
200
  switch (base.k) {
195
201
  case 'var':
202
+ return dotted || addresses.has(base.name) ? base.name : null;
196
203
  case 'addr':
197
204
  return base.name;
198
205
  case 'cast':
199
- return accessedObject(base.e);
206
+ return accessedObject(base.e, addresses);
200
207
  case 'bin':
201
- return base.op === '+' || base.op === '-' ? (accessedObject(base.l) ?? accessedObject(base.r)) : null;
208
+ return base.op === '+' || base.op === '-'
209
+ ? (accessedObject(base.l, addresses) ?? accessedObject(base.r, addresses))
210
+ : null;
202
211
  default:
203
212
  return null;
204
213
  }
@@ -211,6 +220,9 @@ interface TreeContext {
211
220
  declared: ReadonlySet<string>;
212
221
  written: ReadonlySet<string>;
213
222
  typeOf: (e: Expr) => IrType | undefined;
223
+ /** The names whose bare spelling is the object's address: the array-declared globals the
224
+ * structurer subscripts by name (SFn.globals), and the array or struct statics it defines. */
225
+ addresses: ReadonlySet<string>;
214
226
  }
215
227
 
216
228
  /** every call under `e`, counted by target name; every qualified memory access, by the address it
@@ -231,7 +243,9 @@ function effectsInExpr(
231
243
  ): void {
232
244
  const bump = (k: string) => into.set(k, (into.get(k) ?? 0) + 1);
233
245
  const access = e.k === 'index' || e.k === 'field';
234
- const object = access && !selected ? accessedObject(dotSelected(e)) : null;
246
+ const inner = access && !selected ? dotSelectedAccess(e) : null;
247
+ const object =
248
+ inner === null ? null : accessedObject(inner.base, ctx.addresses, inner.k === 'field' && fieldSpellsDot(inner));
235
249
  const ofDeclared = object !== null && !target && ctx.declared.has(object);
236
250
  const writesDeclared = object !== null && target && ctx.written.has(object);
237
251
  if (e.k === 'call') {
@@ -248,7 +262,9 @@ function effectsInExpr(
248
262
  if (object !== null) {
249
263
  bump(`${ofDeclared && castOffDeclaration(e, ctx.typeOf) ? STRIPPED : DECLARED}${object}`);
250
264
  }
251
- } else if (!named && !target && e.k === 'var') {
265
+ } else if (!target && e.k === 'var' && !(named && (selected || ctx.addresses.has(e.name)))) {
266
+ // A bare name is a read of the object's value unless it spells the address of an access, which
267
+ // counts it: the struct a dot member selects from, or an array's decayed name.
252
268
  bump(`${DECLARED}${e.name}`);
253
269
  }
254
270
  const partOfThis = e.k === 'field' && fieldSpellsDot(e);
@@ -257,23 +273,27 @@ function effectsInExpr(
257
273
  );
258
274
  }
259
275
 
260
- /** A dot member's own base chain, past the element or member it selects from: the address of the
261
- * one access the member spells. */
262
- function dotSelected(e: Extract<Expr, { k: 'index' } | { k: 'field' }>): Expr {
276
+ type Access = Extract<Expr, { k: 'index' } | { k: 'field' }>;
277
+
278
+ /** A dot member's innermost access, past the element or member it selects from: its base is the
279
+ * address of the one access the member spells. */
280
+ function dotSelectedAccess(e: Access): Access {
263
281
  if (e.k === 'field' && fieldSpellsDot(e) && (e.base.k === 'index' || e.base.k === 'field')) {
264
- return dotSelected(e.base);
282
+ return dotSelectedAccess(e.base);
265
283
  }
266
- return e.base;
284
+ return e;
267
285
  }
268
286
 
269
287
  /** The executions on the paths through a statement list, split by how each path LEAVES it: falling off
270
- * the end, or by a `return`, `break` or `continue`. Null when no path leaves that way. Each is the
288
+ * the end, or by a `return`, `break`, `continue` or case-end `goto` (`end`, which leaves every loop
289
+ * up to its switch arm and then the switch). Null when no path leaves that way. Each is the
271
290
  * per-name maximum over the paths of that kind. */
272
291
  interface PathCounts {
273
292
  through: EffectCounts | null;
274
293
  ret: EffectCounts | null;
275
294
  brk: EffectCounts | null;
276
295
  cont: EffectCounts | null;
296
+ end: EffectCounts | null;
277
297
  }
278
298
 
279
299
  const sum = (a: EffectCounts, b: EffectCounts): EffectCounts => combine(a, b, (x, y) => x + y);
@@ -285,6 +305,7 @@ const after = (c: EffectCounts, p: PathCounts): PathCounts => ({
285
305
  ret: p.ret && sum(c, p.ret),
286
306
  brk: p.brk && sum(c, p.brk),
287
307
  cont: p.cont && sum(c, p.cont),
308
+ end: p.end && sum(c, p.end),
288
309
  });
289
310
  /** `a` then `b`: only a path that falls off the end of `a` reaches `b` */
290
311
  const seq = (a: PathCounts, b: PathCounts): PathCounts =>
@@ -295,6 +316,7 @@ const seq = (a: PathCounts, b: PathCounts): PathCounts =>
295
316
  ret: most(a.ret, b.ret && sum(a.through, b.ret)),
296
317
  brk: most(a.brk, b.brk && sum(a.through, b.brk)),
297
318
  cont: most(a.cont, b.cont && sum(a.through, b.cont)),
319
+ end: most(a.end, b.end && sum(a.through, b.end)),
298
320
  };
299
321
  /** whichever of the alternatives runs */
300
322
  const either = (...ps: PathCounts[]): PathCounts => ({
@@ -302,14 +324,18 @@ const either = (...ps: PathCounts[]): PathCounts => ({
302
324
  ret: most(...ps.map((p) => p.ret)),
303
325
  brk: most(...ps.map((p) => p.brk)),
304
326
  cont: most(...ps.map((p) => p.cont)),
327
+ end: most(...ps.map((p) => p.end)),
305
328
  });
306
- const falls = (c: EffectCounts): PathCounts => ({ through: c, ret: null, brk: null, cont: null });
329
+ const falls = (c: EffectCounts): PathCounts => ({ through: c, ret: null, brk: null, cont: null, end: null });
307
330
 
308
331
  /** `total` = every occurrence in the tree; `paths` = the most any single syntactic path executes,
309
332
  * by how it leaves `stmts`. A path that leaves by `return` runs nothing after it, so two sequenced
310
333
  * `if (…) { f(); return; }` arms are one call on any path, not two. `unreached` collects every
311
334
  * statement that follows one no path falls past: the path count skips it, so the caller refuses it
312
- * rather than let a duplicated region hide there. */
335
+ * rather than let a duplicated region hide there. A labelled `break;` no path falls into is not
336
+ * one of them while a case-end `goto` in an earlier statement of the list is still leaving
337
+ * (`paths.end`): the goto lands on it, or on a labelled `break;` further out that this one's
338
+ * `break` reaches too, so it is reached, and it holds no code a region could hide in. */
313
339
  function countEffects(
314
340
  stmts: Stmt[],
315
341
  ctx: TreeContext,
@@ -318,7 +344,7 @@ function countEffects(
318
344
  let total: EffectCounts = new Map();
319
345
  let paths = falls(new Map());
320
346
  for (const s of stmts) {
321
- if (paths.through === null) {
347
+ if (paths.through === null && !(s.k === 'break' && s.label !== undefined && paths.end !== null)) {
322
348
  unreached.push(s);
323
349
  }
324
350
  const own: EffectCounts = new Map();
@@ -336,31 +362,64 @@ function countEffects(
336
362
  total = arms.reduce((acc, a) => sum(acc, a.total), sum(total, dflt.total));
337
363
  // A fall-through arm continues into the NEXT one emitted (the last into `default`), so a
338
364
  // path through arm i runs the chain starting at i — the shape the fall-through round's
339
- // CRITICAL took. Built from the end; `chain[i]` is that arm's paths.
340
- const chain: PathCounts[] = new Array(arms.length);
341
- for (let i = arms.length - 1; i >= 0; i--) {
342
- const next = i + 1 < arms.length ? chain[i + 1] : dflt.paths;
343
- chain[i] = s.cases[i].fallsThrough ? seq(arms[i].paths, next) : arms[i].paths;
344
- }
345
- // No `default` is one more alternative: no arm runs. A `break` leaves the switch, so it
346
- // continues after it as a fall-off-the-end does.
347
- const inner = either(...chain, s.default ? dflt.paths : falls(new Map()));
348
- here = after(own, { through: most(inner.through, inner.brk), ret: inner.ret, brk: null, cont: inner.cont });
365
+ // CRITICAL took. `chain[i]` is that arm's paths. An arm that ends `goto L;` continues into
366
+ // the arm labelled L instead (`switchArmSuccessors`): the goto is its body's last top-level
367
+ // statement (`assertCaseGotosNarrow`), so the paths that fall off its end are the goto's,
368
+ // counted below as `through` (a `goto` ends nothing of its own). Memoised, since a goto may
369
+ // name an arm above it; a chain that comes back to an arm it already runs is no tree the
370
+ // structurer builds. The `default` is node `cases.length`.
371
+ const next = switchArmSuccessors(s);
372
+ const bodies = [...arms, dflt];
373
+ const chain: (PathCounts | undefined)[] = new Array(bodies.length);
374
+ const onChain = new Set<number>();
375
+ const chainAt = (i: number): PathCounts => {
376
+ const known = chain[i];
377
+ if (known !== undefined) {
378
+ return known;
379
+ }
380
+ if (onChain.has(i)) {
381
+ throw new ContractError('a switch whose arms fall or jump round in a cycle');
382
+ }
383
+ onChain.add(i);
384
+ const to = next[i];
385
+ const then = to === undefined ? undefined : chainAt(to);
386
+ onChain.delete(i);
387
+ return (chain[i] = then === undefined ? bodies[i].paths : seq(bodies[i].paths, then));
388
+ };
389
+ const dfltChain = chainAt(arms.length);
390
+ // No `default` is one more alternative: no arm runs. A `break` or a case-end `goto` leaves the
391
+ // switch, so it continues after it as a fall-off-the-end does.
392
+ const inner = either(...arms.map((_, i) => chainAt(i)), s.default ? dfltChain : falls(new Map()));
393
+ here = after(own, {
394
+ through: most(inner.through, inner.brk, inner.end),
395
+ ret: inner.ret,
396
+ brk: null,
397
+ cont: inner.cont,
398
+ end: null,
399
+ });
349
400
  } else if (stmtChildren(s).length > 0) {
350
401
  // A loop: its sequenced children (a body, a `for`'s init/inc) counted ONCE — a loop's dynamic
351
402
  // trip count is not a syntactic occurrence, and the IR side is static too. A `break` or
352
403
  // `continue` in the body lands after the loop or at its test, and the test may exit without
353
- // running the body.
404
+ // running the body. A case-end `goto` leaves the loop for its switch's end.
354
405
  const body = countEffects(stmtChildren(s), ctx, unreached);
355
406
  total = sum(total, body.total);
356
407
  const p = body.paths;
357
- here = after(own, { through: most(p.through, p.brk, p.cont, new Map()), ret: p.ret, brk: null, cont: null });
408
+ here = after(own, {
409
+ through: most(p.through, p.brk, p.cont, new Map()),
410
+ ret: p.ret,
411
+ brk: null,
412
+ cont: null,
413
+ end: p.end,
414
+ });
358
415
  } else if (s.k === 'return') {
359
- here = { through: null, ret: own, brk: null, cont: null };
416
+ here = { through: null, ret: own, brk: null, cont: null, end: null };
360
417
  } else if (s.k === 'break') {
361
- here = { through: null, ret: null, brk: own, cont: null };
418
+ here = { through: null, ret: null, brk: own, cont: null, end: null };
362
419
  } else if (s.k === 'continue') {
363
- here = { through: null, ret: null, brk: null, cont: own };
420
+ here = { through: null, ret: null, brk: null, cont: own, end: null };
421
+ } else if (s.k === 'caseEndGoto') {
422
+ here = { through: null, ret: null, brk: null, cont: null, end: own };
364
423
  } else {
365
424
  here = falls(own);
366
425
  }
@@ -369,12 +428,24 @@ function countEffects(
369
428
  return { total, paths };
370
429
  }
371
430
 
431
+ /** Every `goto` keeps one of the two forms L3 allows it (l3/ast.ts `caseGotoViolation`): the last
432
+ * statement of a switch arm's own body naming a case label of that switch, or a case-end goto
433
+ * naming a labelled `break;` that ends the arm it sits in. The effect count below reads a goto
434
+ * that way, so a tree outside it is refused before it is counted. */
435
+ export function assertCaseGotosNarrow(sfn: SFn): void {
436
+ const why = caseGotoViolation(sfn.body);
437
+ if (why !== null) {
438
+ throw new ContractError(`'${sfn.name}': ${why}`);
439
+ }
440
+ }
441
+
372
442
  /**
373
443
  * Post structuring: every call and pinned access the asm makes is emitted, and none of those or of
374
444
  * its declared volatile reads is emitted more times than the asm makes it on any one path. See the
375
445
  * note above for what this deliberately does not cover.
376
446
  */
377
447
  export function assertEffectsPreserved(fn: Fn, sfn: SFn): void {
448
+ assertCaseGotosNarrow(sfn);
378
449
  // Reachable blocks only: an unreachable block's call is legitimately never emitted.
379
450
  const seen = reachableBlocks(fn);
380
451
  const irCalls: EffectCounts = new Map();
@@ -499,6 +570,12 @@ export function assertEffectsPreserved(fn: Fn, sfn: SFn): void {
499
570
  declared: new Set([...irDeclared.keys()].map((k) => k.slice(DECLARED.length))),
500
571
  written: declaredWritten,
501
572
  typeOf: (e) => exprCType(e, vt),
573
+ addresses: new Set([
574
+ ...(sfn.globals ?? []).map((g) => g.name),
575
+ ...[...(sfn.statics ?? []), ...(sfn.images ?? [])]
576
+ .filter((st) => st.type.kind === 'array' || st.type.kind === 'struct')
577
+ .map((st) => st.name),
578
+ ]),
502
579
  };
503
580
  const { total, paths } = countEffects(sfn.body, ctx, unreached);
504
581
  if (unreached.length) {
@@ -753,10 +830,17 @@ export function assertLocalsWritten(sfn: SFn): void {
753
830
  }
754
831
  exprChildren(e).forEach(walkExpr);
755
832
  };
833
+ // A store to a member of a local struct (`ret.a = v`) writes that local.
834
+ const memberOf = (e: Expr): string | undefined =>
835
+ e.k === 'field' && e.dot === true ? (e.base.k === 'var' ? e.base.name : memberOf(e.base)) : undefined;
756
836
  const walkStmt = (st: Stmt): void => {
757
837
  if (st.k === 'assign' && suspect.has(st.name)) {
758
838
  written.add(st.name);
759
839
  }
840
+ const member = st.k === 'store' ? memberOf(st.lval) : undefined;
841
+ if (member !== undefined && suspect.has(member)) {
842
+ written.add(member);
843
+ }
760
844
  stmtExprs(st).forEach(walkExpr);
761
845
  stmtChildren(st).forEach(walkStmt);
762
846
  };
@@ -858,6 +942,131 @@ export function assertNoOrphanedLocals(before: SFn, after: SFn): void {
858
942
  }
859
943
  }
860
944
 
945
+ /** The types a tree reads or writes LOCAL STORAGE at, other than an integer — the frame bytes
946
+ * reached by address (l3/storage.ts `localAddresses`), each access typed as it is spelled
947
+ * (`exprCType`: the cast on the storage, or the element struct's field). An aggregate is the
948
+ * element a `.field` selects from, not an access, so only scalars count. */
949
+ function localStorageNonIntegers(sfn: SFn): Set<string> {
950
+ const { isAddress, accessAddress } = localAddresses(sfn);
951
+ const vt = declaredTypes(sfn);
952
+ const out = new Set<string>();
953
+ for (const e of walkExprs(sfn.body)) {
954
+ if ((e.k === 'index' || e.k === 'field') && isAddress(accessAddress(e))) {
955
+ const t = exprCType(e, vt);
956
+ if (t !== undefined && t.kind !== 'int' && t.kind !== 'struct' && t.kind !== 'array') {
957
+ out.add(typeToString(t));
958
+ }
959
+ }
960
+ }
961
+ return out;
962
+ }
963
+
964
+ /** After a respell variation: no access to LOCAL STORAGE is re-typed to a non-integer.
965
+ *
966
+ * The frame audit's licence for a runtime index wider than a byte (frontend/frame-objects.ts
967
+ * `indexedWidth`) rests on what L3 SPELLS: agbcc at -O2 keeps a read ordered after a store to the
968
+ * same bytes only while both are integers of one width, and gives a pointer (or `long`, or a
969
+ * float) an alias set of its own, so a variation that re-types the bytes' reads hands the
970
+ * compiler a stale read the asm never had — C that compiles, scores, and is wrong. `/ptr-field`'s
971
+ * `over-local-storage` gate is the refusal that names itself; this is the backstop for the next
972
+ * variation that re-types, failing at its own stage rather than three away.
973
+ *
974
+ * A DIFFERENTIAL, like `assertNoOrphanedLocals`: it judges only a non-integer type the tree it
975
+ * derives from did not already read the storage at. The default tree is the frame audit's
976
+ * answer, and a type it gives a local object (a header-typed out-parameter's pointer member, say)
977
+ * is the source's own spelling, not a variation's claim. */
978
+ export function assertLocalStorageStaysInteger(before: SFn, after: SFn): void {
979
+ const had = localStorageNonIntegers(before);
980
+ const added = [...localStorageNonIntegers(after)].filter((t) => !had.has(t));
981
+ if (added.length) {
982
+ throw new ContractError(
983
+ `a respell variation re-typed an access to local storage in '${after.name}' as ${added
984
+ .sort()
985
+ .map((t) => `'${t}'`)
986
+ .join(', ')} — not an integer, so agbcc may read it stale past an integer store to the same bytes`,
987
+ );
988
+ }
989
+ }
990
+
991
+ /** A FRAME UNION IS READ AND WRITTEN ONLY THROUGH ITS VIEWS. The frame audit declares storage
992
+ * accessed at two widths over one byte as a union of per-width views (frontend/frame-objects.ts
993
+ * `unionViews`, structure.ts `laddrType`), because agbcc gives an access through a union member
994
+ * alias set 0 and reorders nothing past it — where a cast on the same bytes is read stale past a
995
+ * store of the other width. That licence holds only while EVERY access is a member access:
996
+ * - the union's name appears only as the base of a view (`u.half`), and an element of an array
997
+ * view is subscripted, element 0 included — `*u.half` dereferences the view's decay, an
998
+ * INDIRECT_REF agbcc types by its pointee and reads stale exactly as the cast (compiled);
999
+ * - nothing is accessed through its ADDRESS: `&u`, under casts and `+`/`-`, or a local ever
1000
+ * assigned such a value, as the base of a subscript, an arrow or a deref is the cast
1001
+ * spelling again. The address itself may go anywhere a value goes — a call's argument, a
1002
+ * device's source register — since handing it on accesses nothing.
1003
+ * ABSOLUTE, not a differential: the default tree satisfies it by construction, so any tree that
1004
+ * does not was made by a variation. Checked on the default tree (pipeline.ts) and on every
1005
+ * respelled one (rank.ts, beside `assertLocalStorageStaysInteger`). */
1006
+ export function assertFrameUnionsViewed(sfn: SFn): void {
1007
+ const unions = new Map(sfn.locals.filter((l) => l.type.kind === 'union').map((l) => [l.name, l.type]));
1008
+ if (unions.size === 0) {
1009
+ return;
1010
+ }
1011
+ const bad = (why: string): never => {
1012
+ throw new ContractError(`'${sfn.name}' ${why} — agbcc may read the frame union stale through it`);
1013
+ };
1014
+ const reaches = (e: Expr, held: ReadonlySet<string>): boolean => {
1015
+ switch (e.k) {
1016
+ case 'addr':
1017
+ return unions.has(e.name);
1018
+ case 'var':
1019
+ return held.has(e.name);
1020
+ case 'cast':
1021
+ return reaches(e.e, held);
1022
+ case 'bin':
1023
+ return (e.op === '+' || e.op === '-') && (reaches(e.l, held) || reaches(e.r, held));
1024
+ default:
1025
+ return false;
1026
+ }
1027
+ };
1028
+ const held = taintOverAssigns(assignsIn(sfn.body), [], (a, t) => reaches(a.value, t));
1029
+ const isView = (e: Expr): boolean =>
1030
+ e.k === 'field' && e.dot === true && e.base.k === 'var' && unions.has(e.base.name);
1031
+ const visit = (e: Expr): void => {
1032
+ if (e.k === 'field' && isView(e)) {
1033
+ if (memberOf(unions.get((e.base as { name: string }).name), e.name) === undefined) {
1034
+ bad(`names '${e.name}', which is no view of the frame union`);
1035
+ }
1036
+ return;
1037
+ }
1038
+ if (e.k === 'index' && isView(e.base)) {
1039
+ if (e.idx.k === 'const' && e.idx.value === 0 && e.subscript !== true) {
1040
+ bad('dereferences a view of a frame union (`*u.view`) instead of subscripting it');
1041
+ }
1042
+ visit(e.base);
1043
+ visit(e.idx);
1044
+ return;
1045
+ }
1046
+ if ((e.k === 'index' || (e.k === 'field' && e.dot !== true)) && reaches(e.base, held)) {
1047
+ bad('accesses a frame union through a pointer to its bytes');
1048
+ }
1049
+ if (e.k === 'var' && unions.has(e.name)) {
1050
+ bad(`uses the frame union '${e.name}' other than through a view`);
1051
+ }
1052
+ exprChildren(e).forEach(visit);
1053
+ };
1054
+ const walk = (list: readonly Stmt[]): void => {
1055
+ for (const st of list) {
1056
+ if (st.k === 'assign' && unions.has(st.name)) {
1057
+ bad(`assigns the frame union '${st.name}' whole`);
1058
+ }
1059
+ stmtExprs(st).forEach(visit);
1060
+ // a `for`'s init and inc are statements of their own, outside both lists
1061
+ stmtLists(st).forEach(walk);
1062
+ if (st.k === 'for') {
1063
+ walk([st.init, st.inc]);
1064
+ }
1065
+ }
1066
+ };
1067
+ walk(sfn.body);
1068
+ }
1069
+
861
1070
  /** After a respell variation: every read of a MINTED local — its ADDRESS being taken included — must sit where
862
1071
  * that local's assignment has already run.
863
1072
  *
@@ -1013,8 +1222,9 @@ export function assertDerefsTyped(sfn: SFn): void {
1013
1222
  if (e.k === 'index' && !structElem.has(e) && !SCALAR_WIDTHS.has(e.width)) {
1014
1223
  bad.push(`index width ${e.width} is not a C scalar width`);
1015
1224
  }
1016
- // The emitter legalizes pointer operands away from these ops (structure.ts intify); a
1017
- // pointer surviving here is ill-typed C the compiler will reject.
1225
+ // The emitter legalizes pointer operands away from these ops (structure/pointer-spelling.ts
1226
+ // `ARITH_ROWS`, its `bitwise` row); a pointer surviving here is ill-typed C the compiler will
1227
+ // reject.
1018
1228
  if (e.k === 'bin' && NO_PTR_OPS.has(e.op)) {
1019
1229
  for (const side of [e.l, e.r]) {
1020
1230
  if (ctype(side)?.kind === 'ptr') {
@@ -1026,9 +1236,9 @@ export function assertDerefsTyped(sfn: SFn): void {
1026
1236
  // offset by sizeof(SYM), which is unknown for a header-typed global, so `&SYM + N` is byte-
1027
1237
  // inexact. Nothing emits this shape anymore: a load/store base folds byte-correctly (globalOf
1028
1238
  // turns `&SYM + N` into an `index`/`field` node whose base is a bare `addr`), and the additive
1029
- // lowering intifies every other `addr` operand to `(u32)&SYM` (structure.ts intifyAddr — the
1030
- // cast types int, so it never lands here). A bare `addr` reaching a `+`/`-` operand is therefore
1031
- // a lowering REGRESSION — flag it rather than emit wrong bytes.
1239
+ // lowering intifies every other `addr` operand to `(u32)&SYM` (structure/pointer-spelling.ts
1240
+ // ARITH_ROWS — the cast types int, so it never lands here). A bare `addr` reaching a `+`/`-`
1241
+ // operand is therefore a lowering REGRESSION — flag it rather than emit wrong bytes.
1032
1242
  if (e.k === 'bin' && (e.op === '+' || e.op === '-')) {
1033
1243
  const addrSide = e.l.k === 'addr' ? e.l : e.r.k === 'addr' ? e.r : undefined;
1034
1244
  if (addrSide) {
package/src/declare.ts CHANGED
@@ -185,7 +185,36 @@ export function renderDeclarations(refs: SymbolRef[]): string {
185
185
  const lines: string[] = [];
186
186
  const declaredTags = new Set<string>();
187
187
  const declaredTypedefs = new Set<string>();
188
- for (const { name, info, access, proto, returned, holdsPointer } of refs) {
188
+ const defineStruct = (s: { readonly name: string; readonly declared: string; readonly layout: AggregateLayout }) => {
189
+ if (!declaredTags.has(s.name)) {
190
+ declaredTags.add(s.name);
191
+ lines.push(declaredStructDecl(s.name, s.layout));
192
+ }
193
+ if (!/^(?:struct|union)\s/.test(s.declared) && !declaredTypedefs.has(s.declared)) {
194
+ declaredTypedefs.add(s.declared);
195
+ lines.push(`typedef struct ${s.name} ${s.declared};`);
196
+ }
197
+ };
198
+ for (const {
199
+ name,
200
+ info,
201
+ access,
202
+ proto,
203
+ returned,
204
+ passed,
205
+ passedAs,
206
+ holdsPointer,
207
+ implicitCall,
208
+ definesReturn,
209
+ } of refs) {
210
+ // THE FUNCTION'S OWN STRUCT RETURN, defined as a callee's is; the function itself is declared
211
+ // by its definition
212
+ if (definesReturn) {
213
+ if (returned !== undefined) {
214
+ defineStruct(returned);
215
+ }
216
+ continue;
217
+ }
189
218
  // An address-cast macro declares itself: the header's own body, verbatim. It must NOT become
190
219
  // an `extern` — that is the whole point of the fact (an extern emits a relocated pool word
191
220
  // where the macro emits the numeric one the target shows).
@@ -193,6 +222,14 @@ export function renderDeclarations(refs: SymbolRef[]): string {
193
222
  lines.push(`#define ${name} ${info.macroBody}`);
194
223
  continue;
195
224
  }
225
+ // A GLOBAL REGISTER VARIABLE declares itself in its register (SymbolInfo.globalRegister), typed
226
+ // as a name-only global is: a pointer where the body stores or meets one, else the word the
227
+ // body accesses.
228
+ if (info.globalRegister !== undefined) {
229
+ const t = holdsPointer ? 'void *' : `${intType(access?.width ?? 4, access?.signed ?? true) ?? 's32'} `;
230
+ lines.push(`register ${t}${name} __asm__("${info.globalRegister}");`);
231
+ continue;
232
+ }
196
233
  // A CALL TARGET the project declared completely — its own prototype, re-spelled from the
197
234
  // parameter and return type texts the project wrote. This is the one declaration here that
198
235
  // is load-bearing for the CALL and not only for the name: an implicit declaration is `int`,
@@ -203,23 +240,24 @@ export function renderDeclarations(refs: SymbolRef[]): string {
203
240
  // A STRUCT IT RETURNS THROUGH MEMORY is the headers' type, spelled as they spell it, so it is
204
241
  // defined here with the rest of what they own: by its tag, once, whoever else declares that
205
242
  // tag — and a typedef name is bound to it on top.
206
- if (returned?.declared !== undefined) {
207
- if (!declaredTags.has(returned.name)) {
208
- declaredTags.add(returned.name);
209
- lines.push(declaredStructDecl(returned.name, returned.layout));
210
- }
211
- if (!/^(?:struct|union)\s/.test(returned.declared) && !declaredTypedefs.has(returned.declared)) {
212
- declaredTypedefs.add(returned.declared);
213
- lines.push(`typedef struct ${returned.name} ${returned.declared};`);
214
- }
243
+ // …and so is a struct it is passed by value, the same way.
244
+ for (const s of [...(returned?.declared !== undefined ? [returned] : []), ...(passed ?? [])]) {
245
+ defineStruct(s);
215
246
  }
216
247
  lines.push(`${proto.returns} ${name}(${proto.params.length > 0 ? proto.params.join(', ') : 'void'});`);
217
248
  continue;
218
249
  }
250
+ // A CALL TARGET no prototype spells, named ahead of its first call by an initializer — C's
251
+ // implicit declaration, `int F();`, written where the initializer can see it. No parameter
252
+ // list, so the call binds to it exactly as it bound to the implicit one (see collectSymbolRefs).
253
+ if (implicitCall) {
254
+ lines.push(`int ${name}();`);
255
+ continue;
256
+ }
219
257
  if (info.kind === 'code') {
220
258
  // value-referenced code symbol ((u32)Func): any prototype makes the name visible, and
221
- // the address is arity-independent. A call target reaches this module only with a complete
222
- // prototype, which the branch above already printed — see collectSymbolRefs.
259
+ // the address is arity-independent. A call target reaches here only with a complete
260
+ // prototype or as an initializer's implicit call, which the branches above printed.
223
261
  lines.push(`void ${name}(void);`);
224
262
  continue;
225
263
  }
@@ -317,6 +355,12 @@ export function renderDeclarations(refs: SymbolRef[]): string {
317
355
  lines.push(`extern void *${info.volatile ? 'volatile ' : ''}${info.const ? 'const ' : ''}${name};`);
318
356
  break;
319
357
  }
358
+ // A global passed bare as a struct by value is that struct, which its callee's prototype
359
+ // defines (`passed`); a declaration of an incomplete tag is completed by that definition.
360
+ if (passedAs !== undefined) {
361
+ lines.push(`extern ${quals(info)}${passedAs} ${name};`);
362
+ break;
363
+ }
320
364
  const t = access ? intType(access.width, access.signed) : null;
321
365
  lines.push(`extern ${quals(info)}${t ?? 'u32'} ${name};`);
322
366
  break;
@@ -326,18 +370,23 @@ export function renderDeclarations(refs: SymbolRef[]): string {
326
370
  return lines.length ? lines.join('\n') + '\n' : '';
327
371
  }
328
372
 
329
- /** The object-like `#define`s out of a rendered declaration block.
373
+ /** THE LINES OF A RENDERED DECLARATION BLOCK THAT THE HEADERS WORLD KEEPS: its object-like
374
+ * `#define`s and its global register variables. Everything else there is owned by the injected
375
+ * headers (a duplicate typedef or struct definition is a C89 hard error), and these two are not:
376
+ *
377
+ * - an address-cast macro, because a duplicate `#define` with an identical body is legal — and a
378
+ * PREPROCESSED project context has no macros left at all, so dropping these turns a macro-named
379
+ * candidate into an `undeclared identifier` rather than a spelling choice;
380
+ * - a global register variable (`register T greg_r8 __asm__("r8");`, SymbolInfo.globalRegister),
381
+ * because no header declares the name the lift made up for it.
330
382
  *
331
- * Address-cast macro defines are the one part of a synthesized block that must survive into the
332
- * HEADERS world too. Everything else there is owned by the injected headers (a duplicate typedef
333
- * or struct definition is a C89 hard error), but a duplicate `#define` with an identical body is
334
- * legal — and a PREPROCESSED project context has no macros left at all, so dropping these turns a
335
- * macro-named candidate into an `undeclared identifier` rather than a spelling choice. */
336
- export function macroDefinesOf(declarations: string | undefined): string {
383
+ * Chosen by each line's leading keyword, which `renderDeclarations` prints for these two kinds
384
+ * only: a new kind of line it prints with either keyword rides into the headers world too. */
385
+ export function declarationsKeptWithHeaders(declarations: string | undefined): string {
337
386
  if (!declarations) {
338
387
  return '';
339
388
  }
340
- const lines = declarations.split('\n').filter((l) => l.startsWith('#define '));
389
+ const lines = declarations.split('\n').filter((l) => l.startsWith('#define ') || l.startsWith('register '));
341
390
  return lines.length ? lines.join('\n') + '\n' : '';
342
391
  }
343
392