@asmlift/core 0.8.1 → 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 +267 -79
  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
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;
package/src/l3/ast.ts CHANGED
@@ -6,8 +6,28 @@ import { type IrType, typeEquals } from '../ir/types';
6
6
  export type Expr =
7
7
  | { k: 'var'; name: string }
8
8
  | { k: 'const'; value: number }
9
+ // A `double` literal, carried as its IEEE bit pattern (ir/float-bits.ts) — a kind apart from the
10
+ // integer `const`, so no integer fold, range test or signedness reading ever takes it for one.
11
+ | { k: 'fconst'; bits: string }
9
12
  | { k: 'bin'; op: BinOp; l: Expr; r: Expr }
10
- | { k: 'un'; op: '-' | '~' | '!'; e: Expr }
13
+ | { k: 'un'; op: '-' | '~' | '!' | 'f-'; e: Expr }
14
+ // A post-increment `name++` (`by` 1) or post-decrement `name--` (`by` -1): the value the local
15
+ // held BEFORE the update, with the update as a side effect. The only expression form here that
16
+ // WRITES anything, so `exprHasEffect` answers for it alongside `call` and `marker` — that is what
17
+ // every pass asking "may I move or duplicate this" already consults. It also NAMES a local, so
18
+ // `mentionedName` answers for it beside `var` and `addr`, and the two walks that classify their
19
+ // own leaves — `l3/mentions.ts` and `contracts.ts`'s `assertLocalsWritten` — count it on both
20
+ // sides, because `v++` reads the name and writes it.
21
+ //
22
+ // The target is a NAME rather than an Expr because its one producer has one: structure.ts's
23
+ // `emitDoWhile`, folding a loop update into a bottom test that reads the variable at its
24
+ // pre-update value (`while (v0 != 0 && v1++ <= 9)`). A general lvalue has no inhabitant.
25
+ //
26
+ // WHERE IT MAY STAND IS THE PRODUCER'S OBLIGATION, not a property of the node: C89 leaves the
27
+ // program undefined when the same object is also read elsewhere between two sequence points, and
28
+ // short-circuit operators decide whether the update happens at all. `PREUPDATE_COND_GATES`
29
+ // (structure/hazards.ts) holds both rules for the one producer.
30
+ | { k: 'postincr'; name: string; by: 1 | -1 }
11
31
  // A C-style value cast `(T)e`. Tree-level producers: a width-narrowing cast `(u8)e` (the
12
32
  // recovered form of a byte/half extend idiom — zext/sext IR ops), the STRUCT-pointer cast
13
33
  // (structure.ts memAccess/arrayAccess struct paths — see the note on `field` below), and the
@@ -22,12 +42,42 @@ export type Expr =
22
42
  // cv-qualifier, deliberately: volatility is a SPELLING, carried at the declaration or the
23
43
  // cast, the same split SFn.locals makes for `volatile`/`pointeeVolatile`.
24
44
  | { k: 'cast'; to: IrType; e: Expr; volatile?: true }
25
- | { k: 'call'; fn: string; args: Expr[] }
45
+ // A call. `wide64` is the INTEGER RANK the callee's return renders with, and it is carried on the
46
+ // node because nothing downstream can derive it: a callee's return type comes from a prototype
47
+ // outside the emitted function, so `exprCType` answers `undefined` for a call and every reader of
48
+ // the rendered rank would otherwise read C's implicit `int`.
49
+ //
50
+ // IT IS TRUE OF THE C THAT IS COMPILED BECAUSE THE SOURCES AGREE, and that agreement is a
51
+ // property of one lookup rather than of two modules happening to match. The width is set where
52
+ // the frontend was TOLD it — a compiler's own runtime-helper signature, whose callee the
53
+ // candidate never declares because the helper table IS the declaration, or a project's
54
+ // `FnProto.returns`, which `proto.ts` `spellableProto` admits only when the same entry can be
55
+ // PRINTED as the callee's own prototype. `prototypesFromSymbols` resolves that table once, so
56
+ // the frontend that stamps this and the collector that prints the declaration read one answer.
57
+ // The filters in `rank-declare.ts` can still withhold a printed declaration (a name the emitted
58
+ // C uses for its own storage, a non-identifier, a reserved name) — every one of them kills the
59
+ // SPELLING or the whole row rather than leaving this stamp standing over an implicitly-`int`
60
+ // callee.
61
+ //
62
+ // `sret` is the type of a struct the callee returns BY VALUE through memory. `args[0]` is then the
63
+ // hidden pointer the frontend read from the first argument register — the address of the struct
64
+ // the call fills — and the rest are the declared arguments. The only C spelling is a statement,
65
+ // `dest = fn(rest)`, so such a call stands alone.
66
+ //
67
+ // `fn` is the callee's NAME, or for an indirect call the EXPRESSION whose value is the address
68
+ // called (ir/core.ts `calleeValue`) — a child of the node like its arguments, listed first in
69
+ // `exprChildren`. That is no evaluation order: C leaves it unspecified, and agbcc loads the callee
70
+ // after the arguments.
71
+ | { k: 'call'; fn: string | Expr; args: Expr[]; wide64?: true; sret?: IrType }
26
72
  // The ADDRESS of a named global, `&gSym` (agbcc pool `.word gSym`, frontend `gaddr` op). A
27
73
  // DEREF of it collapses to the bare global: memAccess/arrayAccess spell `*(&gSym)` as `gSym`
28
74
  // and `(&gSym)[i]` as `gSym[i]` (a global name decays to a pointer). Only a genuinely
29
75
  // address-TAKEN global (passed by address, `&gSym` as a call arg) prints the `&` form. The
30
76
  // global's type comes from the project headers, so it is never declared as a local.
77
+ //
78
+ // An ARRAY's address is spelled by the bare name, and that is a fact of C's declarator grammar
79
+ // rather than of the address — so the C printer decides it from the name's declared type, and
80
+ // this node carries nothing about it (backend/cfamily.ts, `case 'addr'`).
31
81
  | { k: 'addr'; name: string }
32
82
  // A memory access `base[idx]` (printed `*base` when idx is the constant 0), CARRYING the
33
83
  // access's element width (bytes) and signedness. `idx` counts elements of `width` bytes.
@@ -184,7 +234,19 @@ export type Expr =
184
234
  // which side a compare was spelled from genuinely underdetermines — a signed spelling that
185
235
  // byte-matched was proved non-negative by the compiler — so it is refereed as a variation, while a
186
236
  // division helper is a pure function of the expression's C type with no such proof available.
237
+ //
238
+ // THE FLOAT OPERATORS (`f+` `f-` `f*` `f/`) are the third split, and they are earned by what an
239
+ // integer operator MEANS to the passes that read one rather than by a spelling: C writes both with
240
+ // the same token, but `/` here is the SIGNED divide the C-family backend pins with `(s32)` casts,
241
+ // and `+`/`-` are the pointer walks, reassociations and constant folds of a dozen L3 passes. A float
242
+ // under any of those is a wrong program, so it gets operators none of them match — the same reason
243
+ // the IR has `fadd` rather than a float-typed `add`. The unary minus splits the same way (`f-` on
244
+ // `un`).
187
245
  export type BinOp =
246
+ | 'f+'
247
+ | 'f-'
248
+ | 'f*'
249
+ | 'f/'
188
250
  | '+'
189
251
  | '-'
190
252
  | '*'
@@ -342,6 +404,10 @@ export interface SFn {
342
404
  * legalization env (exprCType) but NEVER declared by a backend: the project's own headers
343
405
  * declare them, exactly like every other global name asmlift emits. */
344
406
  globals?: { name: string; type: IrType }[];
407
+ /** The function-scope statics this function DEFINES (structure/local-statics.ts), declared at
408
+ * the top of its body ahead of the locals. Unlike `globals`, the backend declares these: no
409
+ * header does, because the source defined them here. */
410
+ statics?: SStatic[];
345
411
  retType: IrType;
346
412
  body: Stmt[];
347
413
  /** Struct types this function's fields reference, declared above it by the backend. Empty
@@ -359,6 +425,30 @@ export interface SFn {
359
425
  * `TargetDescription.compilerBehaviors`. ABSENT means the direction is unknown for this target
360
426
  * and `l3/slotorder.ts` is the identity — never "ascending by default". */
361
427
  slotOrder?: 'ascending' | 'descending';
428
+ /** Each declared callee's parameter types (an entry the printer cannot spell is `undefined`):
429
+ * what an argument converts to. Read by the C-family printer where `dialect` says C++, which
430
+ * refuses three conversions C makes implicitly (backend/cfamily.ts `argConversion`), and by the
431
+ * pointer-cell conversions in every dialect (l3/ptrcell.ts). */
432
+ declaredArgs?: Readonly<Record<string, readonly (string | undefined)[]>>;
433
+ /** Each declared callee's return type as the project spells it, never printed: whether a call a
434
+ * pointer cell is compared with returns a pointer (l3/ptrcell.ts). */
435
+ declaredReturns?: Readonly<Record<string, string>>;
436
+ /** The emitted C is compiled AS C++. An emission policy like `slotOrder`. */
437
+ dialect?: 'c++';
438
+ }
439
+
440
+ /** One function-scope static's definition: `static [const] T name[count] = { init };`. `init` is
441
+ * the initial elements in order, absent for a static with no initializer (the target's bss).
442
+ * `align` is an alignment in bytes wider than the type's own, which the definition must state. */
443
+ export interface SStatic {
444
+ name: string;
445
+ type: IrType;
446
+ const?: true;
447
+ align?: number;
448
+ init?: number[];
449
+ /** the initializer is spelled as a string literal — a byte array the compiler aligns as a
450
+ * string (structure/local-statics.ts) */
451
+ string?: true;
362
452
  }
363
453
 
364
454
  /** A struct declaration surfaced to the backend (name + field list). Mirrors the IR struct
@@ -427,6 +517,8 @@ export function exprEquals(a: Expr, b: Expr): boolean {
427
517
  return a.name === (b as typeof a).name;
428
518
  case 'const':
429
519
  return a.value === (b as typeof a).value;
520
+ case 'fconst':
521
+ return a.bits === (b as typeof a).bits;
430
522
  case 'bin': {
431
523
  const bb = b as typeof a;
432
524
  return a.op === bb.op && exprEquals(a.l, bb.l) && exprEquals(a.r, bb.r);
@@ -435,6 +527,10 @@ export function exprEquals(a: Expr, b: Expr): boolean {
435
527
  const bb = b as typeof a;
436
528
  return a.op === bb.op && exprEquals(a.e, bb.e);
437
529
  }
530
+ case 'postincr': {
531
+ const bb = b as typeof a;
532
+ return a.name === bb.name && a.by === bb.by;
533
+ }
438
534
  case 'cast': {
439
535
  const bb = b as typeof a;
440
536
  // `volatile` is part of the SPELLING, compared for the same reason `lead` and `dot` are: a
@@ -448,7 +544,25 @@ export function exprEquals(a: Expr, b: Expr): boolean {
448
544
  }
449
545
  case 'call': {
450
546
  const bb = b as typeof a;
451
- return a.fn === bb.fn && a.args.length === bb.args.length && a.args.every((x, i) => exprEquals(x, bb.args[i]));
547
+ // `wide64` is part of the node's IDENTITY, compared for the same reason `volatile` is one
548
+ // cast up: it decides the rank every operand pin is taken at, so two calls that differ in it
549
+ // are two different expressions and a dedup that kept one would respell the other's
550
+ // arithmetic.
551
+ //
552
+ // NO PIPELINE INPUT REACHES THIS TERM TODAY, and saying so is cheaper than a witness that
553
+ // cannot exist: the stamp is a pure function of the callee name (`structure.ts` reads the
554
+ // call result's IR width, which the frontend set from the callee's declared return), so two
555
+ // `call` nodes sharing `fn` and `args` always share `wide64` and deleting the term leaves
556
+ // the whole core suite green. What pins it is a direct test of THIS function's contract in
557
+ // test/int64-repr.test.ts, over two nodes built by hand — which is the honest bar, because
558
+ // the claim being made is about `exprEquals` and not about a path through the pipeline.
559
+ return (
560
+ (typeof a.fn === 'string' || typeof bb.fn === 'string' ? a.fn === bb.fn : exprEquals(a.fn, bb.fn)) &&
561
+ (a.wide64 ?? false) === (bb.wide64 ?? false) &&
562
+ (a.sret === undefined ? bb.sret === undefined : bb.sret !== undefined && typeEquals(a.sret, bb.sret)) &&
563
+ a.args.length === bb.args.length &&
564
+ a.args.every((x, i) => exprEquals(x, bb.args[i]))
565
+ );
452
566
  }
453
567
  case 'index': {
454
568
  const bb = b as typeof a;
@@ -493,12 +607,49 @@ export function exprEquals(a: Expr, b: Expr): boolean {
493
607
  }
494
608
  }
495
609
 
496
- /** THE spelling of an unmodelled instruction's gap reason, in one place: `structure.ts` writes it
497
- * into the marker, `contracts.ts` matches on it to prove the gap was not dropped, and the benchmark
610
+ /** Do two statements print the same C? The leaf statements compare through {@link exprEquals},
611
+ * and an `if` arm by arm; every other kind answers no, which can only keep apart two statements a
612
+ * caller might have merged. */
613
+ export function stmtEquals(a: Stmt, b: Stmt): boolean {
614
+ switch (a.k) {
615
+ case 'assign':
616
+ return b.k === 'assign' && a.name === b.name && exprEquals(a.value, b.value);
617
+ case 'store':
618
+ return b.k === 'store' && exprEquals(a.lval, b.lval) && exprEquals(a.value, b.value);
619
+ case 'exprstmt':
620
+ return b.k === 'exprstmt' && exprEquals(a.value, b.value);
621
+ case 'return':
622
+ return (
623
+ b.k === 'return' &&
624
+ (a.unspelled ?? false) === (b.unspelled ?? false) &&
625
+ (a.value === undefined ? b.value === undefined : b.value !== undefined && exprEquals(a.value, b.value))
626
+ );
627
+ case 'if':
628
+ return b.k === 'if' && exprEquals(a.cond, b.cond) && stmtsEqual(a.then, b.then) && stmtsEqual(a.else, b.else);
629
+ default:
630
+ return false;
631
+ }
632
+ }
633
+
634
+ /** {@link stmtEquals} over two lists, in order. */
635
+ export function stmtsEqual(a: readonly Stmt[], b: readonly Stmt[]): boolean {
636
+ return a.length === b.length && a.every((s, i) => stmtEquals(s, b[i]));
637
+ }
638
+
639
+ /** THE spelling of an `opaque`'s gap reason, in one place: `structure.ts` writes it into the
640
+ * marker, `contracts.ts` matches on it to prove the gap was not dropped, and the benchmark
498
641
  * classifies declines by it. Two spellings make that contract silently vacuous — enforced-looking
499
- * and never firing. `?` when a frontend stamps no mnemonic. */
500
- export function gapReasonFor(mnemonic: unknown): string {
501
- return `unmodelled instruction '${typeof mnemonic === 'string' ? mnemonic : '?'}'`;
642
+ * and never firing, since it is an EQUALITY between what the IR carries and what the output shows.
643
+ *
644
+ * TWO PRODUCERS, TWO SENTENCES, and they name different capabilities: an INSTRUCTION no frontend
645
+ * decodes (`adc`), and a call into the compiler's own RUNTIME that no recogniser folded
646
+ * (`__div2i`). The attrs decide which, so a producer chooses by what it stamps rather than by
647
+ * spelling a string. `?` when a frontend stamps neither. */
648
+ export function gapReasonFor(attrs: { mnemonic?: unknown; helper?: unknown }): string {
649
+ if (typeof attrs.helper === 'string') {
650
+ return `no model for the runtime helper '${attrs.helper}'`;
651
+ }
652
+ return `unmodelled instruction '${typeof attrs.mnemonic === 'string' ? attrs.mnemonic : '?'}'`;
502
653
  }
503
654
 
504
655
  // ── the ONE traversal vocabulary ───────────────────────────────────────────────────────────────
@@ -513,7 +664,9 @@ export function exprChildren(e: Expr): Expr[] {
513
664
  switch (e.k) {
514
665
  case 'var':
515
666
  case 'const':
667
+ case 'fconst':
516
668
  case 'addr':
669
+ case 'postincr':
517
670
  return [];
518
671
  case 'bin':
519
672
  return [e.l, e.r];
@@ -521,7 +674,7 @@ export function exprChildren(e: Expr): Expr[] {
521
674
  case 'cast':
522
675
  return [e.e];
523
676
  case 'call':
524
- return e.args;
677
+ return typeof e.fn === 'string' ? e.args : [e.fn, ...e.args];
525
678
  case 'index':
526
679
  return [e.base, ...(e.lead ?? []), e.idx];
527
680
  case 'field':
@@ -536,7 +689,9 @@ export function mapExprChildren(e: Expr, f: (c: Expr) => Expr): Expr {
536
689
  switch (e.k) {
537
690
  case 'var':
538
691
  case 'const':
692
+ case 'fconst':
539
693
  case 'addr':
694
+ case 'postincr':
540
695
  return e;
541
696
  case 'bin':
542
697
  return { ...e, l: f(e.l), r: f(e.r) };
@@ -544,7 +699,7 @@ export function mapExprChildren(e: Expr, f: (c: Expr) => Expr): Expr {
544
699
  case 'cast':
545
700
  return { ...e, e: f(e.e) };
546
701
  case 'call':
547
- return { ...e, args: e.args.map(f) };
702
+ return { ...e, ...(typeof e.fn === 'string' ? {} : { fn: f(e.fn) }), args: e.args.map(f) };
548
703
  case 'index':
549
704
  return { ...e, base: f(e.base), ...(e.lead ? { lead: e.lead.map(f) } : {}), idx: f(e.idx) };
550
705
  case 'field':
@@ -654,7 +809,7 @@ export function rematerializableAddress(e: Expr): boolean {
654
809
  case 'un':
655
810
  break;
656
811
  default:
657
- ok = false; // var, addr, index, field, call, marker
812
+ ok = false; // var, addr, postincr, index, field, call, marker
658
813
  return;
659
814
  }
660
815
  for (const c of exprChildren(x)) {
@@ -665,10 +820,27 @@ export function rematerializableAddress(e: Expr): boolean {
665
820
  return ok && nonZero;
666
821
  }
667
822
 
668
- /** Whether the tree contains a node with an EFFECT no re-ordering may move: a call, or a marker
669
- * standing in for an unmodelled instruction (annotate mode). */
823
+ /** The local or global a node NAMES, or undefined for one that names none — the whole Expr
824
+ * vocabulary a "which names does this tree mention" walk has to know about, in one place, because
825
+ * a collector that misses a kind reports a SHORT live range and a short range is a clobber where a
826
+ * long one is only a missed merge.
827
+ *
828
+ * `call`'s `fn` is not one of them: it names a function, and the walks that care about that ask
829
+ * for it beside this (l3/hoist.ts). A walk that must tell the kinds APART — `&v` is a write where
830
+ * a bare `v` is a read — keeps its own arms; this answers the name, not what is done to it. */
831
+ export function mentionedName(e: Expr): string | undefined {
832
+ return e.k === 'var' || e.k === 'addr' || e.k === 'postincr' ? e.name : undefined;
833
+ }
834
+
835
+ /** The callee a call names, or undefined for an indirect one. */
836
+ export function calleeOf(e: Extract<Expr, { k: 'call' }>): string | undefined {
837
+ return typeof e.fn === 'string' ? e.fn : undefined;
838
+ }
839
+
840
+ /** Whether the tree contains a node with an EFFECT no re-ordering may move: a call, a marker
841
+ * standing in for an unmodelled instruction (annotate mode), or a post-increment's write. */
670
842
  export function exprHasEffect(e: Expr): boolean {
671
- return e.k === 'call' || e.k === 'marker' || exprChildren(e).some(exprHasEffect);
843
+ return e.k === 'call' || e.k === 'marker' || e.k === 'postincr' || exprChildren(e).some(exprHasEffect);
672
844
  }
673
845
 
674
846
  /** Whether the tree performs an access to a `volatile` object — the OTHER thing no re-ordering may
@@ -12,23 +12,24 @@
12
12
  // and whether fall-through joins the two arms onto one path. `coalesceCandidates` offers both.
13
13
  import { typeToString } from '../ir/types';
14
14
  import type { Expr, SFn, Stmt } from './ast';
15
- import { exprChildren, isLoop, mapExprChildren, stmtChildren, stmtExprs } from './ast';
15
+ import { exprChildren, isLoop, mapExprChildren, mentionedName, stmtChildren, stmtExprs } from './ast';
16
16
  import { type Gate, firstRejection } from './gates';
17
17
 
18
18
  function namesIn(e: Expr, out: Set<string>): void {
19
- // `addr` names a GLOBAL (`&gSym`) or a LOCAL — the structurer renders an `laddr` frame object
20
- // as `&sp0`, an addr node over a name that IS in `sfn.locals`. Both are collected, because a
21
- // name mentioned only through `&` still has a live range: a span that ignored its `addr`
22
- // mentions would be SHORT, and a short span is a clobber where a long one is only a missed
23
- // merge. Collecting a global name costs nothing — it is not in `sfn.locals`, so no pair is
24
- // ever built for it.
19
+ // EVERY mention, through `mentionedName`'s vocabulary rather than this file's own: a span that
20
+ // misses one is SHORT, and a short span is a clobber where a long one is only a missed merge.
21
+ // `&sp0` is a LOCAL's mention — the structurer renders an `laddr` frame object as an addr node
22
+ // over a name that IS in `sfn.locals` — and `n++` in a bottom test is a counter's only mention
23
+ // inside its own loop. A global name comes along for the ride and costs nothing — it is not in
24
+ // `sfn.locals`, so no pair is ever built for it.
25
25
  //
26
- // `rename` DISAGREES WITH THIS, and knowingly: it rewrites only `var` leaves, so a local
27
- // absorbed while mentioned through `&` leaves that mention standing against a declaration the
28
- // merge deleted. Reconciling the two changes which candidates compile — a measured change, not a
26
+ // `rename` DISAGREES WITH THIS, and knowingly: it rewrites `var` and `postincr` leaves, so a
27
+ // local absorbed while mentioned through `&` leaves that mention standing against a declaration
28
+ // the merge deleted. Reconciling the two changes which candidates compile — a measured change, not a
29
29
  // cleanup — so today's behaviour is pinned exactly in coalesce.test.ts ('rename') rather than
30
30
  // repaired here.
31
- if (e.k === 'var' || e.k === 'addr') out.add(e.name);
31
+ const n = mentionedName(e);
32
+ if (n !== undefined) out.add(n);
32
33
  for (const c of exprChildren(e)) namesIn(c, out);
33
34
  }
34
35
 
@@ -62,7 +63,14 @@ export interface Span {
62
63
  * arithmetic over the variable is its own history. The read must be a `var`: `mentions` counts an
63
64
  * `addr` too, and `&a` is not a read of `a`. */
64
65
  const readsVarArithmetically = (e: Expr, n: string): boolean => {
65
- if (e.k === 'index' || e.k === 'field' || e.k === 'call' || e.k === 'marker' || e.k === 'addr') {
66
+ if (
67
+ e.k === 'index' ||
68
+ e.k === 'field' ||
69
+ e.k === 'call' ||
70
+ e.k === 'marker' ||
71
+ e.k === 'postincr' ||
72
+ e.k === 'addr'
73
+ ) {
66
74
  return false;
67
75
  }
68
76
  return (e.k === 'var' && e.name === n) || exprChildren(e).some((c) => readsVarArithmetically(c, n));
@@ -155,7 +163,7 @@ function spans(body: Stmt[]): Map<string, Span> {
155
163
  }
156
164
  function rename(body: Stmt[], from: string, to: string): Stmt[] {
157
165
  const inExpr = (e: Expr): Expr =>
158
- e.k === 'var' && e.name === from ? { ...e, name: to } : mapExprChildren(e, inExpr);
166
+ (e.k === 'var' || e.k === 'postincr') && e.name === from ? { ...e, name: to } : mapExprChildren(e, inExpr);
159
167
  const inStmt = (s: Stmt): Stmt => {
160
168
  const r = { ...s } as Record<string, unknown>;
161
169
  if (s.k === 'assign' && s.name === from) r.name = to;
package/src/l3/dce.ts CHANGED
@@ -29,9 +29,11 @@ import { exprChildren, negateCond, stmtChildren, stmtExprs } from './ast';
29
29
 
30
30
  /** Accumulate every LOCAL-eligible `var` name read anywhere in `e` (recurses all sub-exprs). An
31
31
  * `addr` name counts too: taking a local's address makes every store to it observable through the
32
- * escaped pointer, so an address-taken local is never dead here. */
32
+ * escaped pointer, so an address-taken local is never dead here. A `postincr` reads its name
33
+ * before it writes it, so it pins the store that reaches it — `n++` in a loop test is the only
34
+ * reader of the `n = 0` above the loop. */
33
35
  function readsInto(e: Expr, out: Set<string>): void {
34
- if (e.k === 'var' || e.k === 'addr') {
36
+ if (e.k === 'var' || e.k === 'addr' || e.k === 'postincr') {
35
37
  out.add(e.name);
36
38
  }
37
39
  for (const c of exprChildren(e)) {
@@ -48,6 +50,7 @@ function reads(e: Expr): Set<string> {
48
50
  /** True if `e` carries any reason NOT to speculatively delete its assignment/condition:
49
51
  * - `call` — may write memory/globals (a side effect);
50
52
  * - `marker` — the annotate-mode ASMLIFT_ERROR gap signal, which must survive so the gap stays loud;
53
+ * - `postincr` — its write is the loop update; deleting the assignment would delete the update;
51
54
  * - the strict-mode `?` unresolved sentinel (`{k:'var', name:'?'}`) — dropping it would let a
52
55
  * value asmlift could NOT lift slip past `assertResolved`, silently downgrading a loud gap;
53
56
  * - a memory load (`index`/`field`) — a deref's volatility is unknowable here, so a possibly-effectful read
@@ -56,7 +59,7 @@ function reads(e: Expr): Set<string> {
56
59
  * observable access the machine performed; deleting the dead assignment would delete the read.
57
60
  * A dead assignment whose value contains any of these is kept. */
58
61
  function mustKeep(e: Expr, volatiles: ReadonlySet<string>): boolean {
59
- if (e.k === 'call' || e.k === 'marker' || e.k === 'index' || e.k === 'field') {
62
+ if (e.k === 'call' || e.k === 'marker' || e.k === 'postincr' || e.k === 'index' || e.k === 'field') {
60
63
  return true;
61
64
  }
62
65
  if (e.k === 'var' && (e.name === '?' || volatiles.has(e.name))) {
@@ -95,6 +98,16 @@ function allAddrNamesInto(stmts: Stmt[], out: Set<string>): void {
95
98
  }
96
99
  }
97
100
 
101
+ /** Where a `break` and a `continue` at this depth land, as the set live there. A jump does not fall
102
+ * through to the statement after it: the live set at a jump is its TARGET's, not what the rest of
103
+ * the enclosing block reads. `break` targets the innermost loop or switch, `continue` the innermost
104
+ * loop; each is that statement's conservative in-scope set (everything read anywhere in it plus
105
+ * what is live after it), which also covers the loop's own next test and update. */
106
+ interface JumpLive {
107
+ brk: ReadonlySet<string>;
108
+ cont?: ReadonlySet<string>; // absent in a switch no loop encloses
109
+ }
110
+
98
111
  /** Backward live-variable walk over one block. `liveOut` is the set of locals live on exit;
99
112
  * returns the rewritten block and the set live on entry. */
100
113
  function dceBlock(
@@ -102,6 +115,7 @@ function dceBlock(
102
115
  liveOut: ReadonlySet<string>,
103
116
  locals: ReadonlySet<string>,
104
117
  volatiles: ReadonlySet<string>,
118
+ jumps: JumpLive | null,
105
119
  ): { out: Stmt[]; liveIn: Set<string> } {
106
120
  const live = new Set(liveOut);
107
121
  const rev: Stmt[] = [];
@@ -153,12 +167,19 @@ function dceBlock(
153
167
  }
154
168
  case 'break':
155
169
  case 'continue': {
170
+ // A mid-body `if (c) { v = x; break; }` carries the loop's exit value in `v`, while the
171
+ // statements after the `if` overwrite `v` for the next iteration. Leaving `live` as the
172
+ // fall-through set would judge the carried copy dead and drop it.
173
+ const target = s.k === 'break' ? jumps?.brk : jumps?.cont;
174
+ if (target) {
175
+ setLive(new Set(target));
176
+ }
156
177
  rev.push(s);
157
178
  break;
158
179
  }
159
180
  case 'if': {
160
- const t = dceBlock(s.then, live, locals, volatiles);
161
- const e = dceBlock(s.else, live, locals, volatiles);
181
+ const t = dceBlock(s.then, live, locals, volatiles, jumps);
182
+ const e = dceBlock(s.else, live, locals, volatiles, jumps);
162
183
  const nlive = new Set<string>();
163
184
  for (const r of reads(s.cond)) {
164
185
  nlive.add(r);
@@ -188,7 +209,7 @@ function dceBlock(
188
209
  // loop-carried store is never cut. Body DCE removes only what is dead on EVERY path.
189
210
  const loopLive = new Set(live);
190
211
  allReadsInto([s], loopLive);
191
- const b = dceBlock(s.body, loopLive, locals, volatiles);
212
+ const b = dceBlock(s.body, loopLive, locals, volatiles, { brk: loopLive, cont: loopLive });
192
213
  const nlive = new Set(loopLive);
193
214
  for (const r of b.liveIn) {
194
215
  nlive.add(r);
@@ -200,7 +221,7 @@ function dceBlock(
200
221
  case 'for': {
201
222
  const loopLive = new Set(live);
202
223
  allReadsInto([s], loopLive);
203
- const b = dceBlock(s.body, loopLive, locals, volatiles);
224
+ const b = dceBlock(s.body, loopLive, locals, volatiles, { brk: loopLive, cont: loopLive });
204
225
  const nlive = new Set(loopLive);
205
226
  for (const r of b.liveIn) {
206
227
  nlive.add(r);
@@ -214,8 +235,9 @@ function dceBlock(
214
235
  // read anywhere in the switch as live throughout — no case-body store is ever cut.
215
236
  const swLive = new Set(live);
216
237
  allReadsInto([s], swLive);
217
- const cases = s.cases.map((c) => ({ ...c, body: dceBlock(c.body, swLive, locals, volatiles).out }));
218
- const def = s.default ? dceBlock(s.default, swLive, locals, volatiles).out : s.default;
238
+ const inSwitch: JumpLive = { brk: swLive, cont: jumps?.cont };
239
+ const cases = s.cases.map((c) => ({ ...c, body: dceBlock(c.body, swLive, locals, volatiles, inSwitch).out }));
240
+ const def = s.default ? dceBlock(s.default, swLive, locals, volatiles, inSwitch).out : s.default;
219
241
  const nlive = new Set(swLive);
220
242
  for (const r of reads(s.scrutinee)) {
221
243
  nlive.add(r);
@@ -263,7 +285,7 @@ export function eliminateDeadStores(sfn: SFn): SFn {
263
285
  allAddrNamesInto(sfn.body, addressTaken);
264
286
  const volatiles = new Set(sfn.locals.filter((l) => l.volatile).map((l) => l.name));
265
287
  const locals = new Set(sfn.locals.filter((l) => !l.volatile && !addressTaken.has(l.name)).map((l) => l.name));
266
- const body = dceBlock(sfn.body, new Set<string>(), locals, volatiles).out;
288
+ const body = dceBlock(sfn.body, new Set<string>(), locals, volatiles, null).out;
267
289
  const used = new Set<string>();
268
290
  referencedNames(body, used);
269
291
  return { ...sfn, body, locals: sfn.locals.filter((l) => used.has(l.name) || l.volatile) };
package/src/l3/gates.ts CHANGED
@@ -19,8 +19,9 @@ export interface Gate<Ctx> {
19
19
  /** Remove it and some candidate is WRONG, not merely worse. Everything else is a codegen
20
20
  * heuristic the differ still referees. This flag is what makes `guardedBy` mandatory. */
21
21
  readonly sound: boolean;
22
- /** the test that fails when this gate is removed — required for a sound gate */
23
- readonly guardedBy?: string;
22
+ /** the test that fails when this gate is removed — required for a sound gate; a rule with two
23
+ * halves names one test per half */
24
+ readonly guardedBy?: string | readonly string[];
24
25
  /** true ⇒ REJECT this candidate */
25
26
  readonly rejects: (c: Ctx) => boolean;
26
27
  }
@@ -67,6 +68,11 @@ export function ablateHeuristic<Ctx>(gates: readonly Gate<Ctx>[], id: string): r
67
68
  return without(gates, id);
68
69
  }
69
70
 
71
+ /** Every test a gate names as its guard, one per half of the rule. */
72
+ export function guardsOf<Ctx>(g: Gate<Ctx>): readonly string[] {
73
+ return typeof g.guardedBy === 'string' ? [g.guardedBy] : (g.guardedBy ?? []);
74
+ }
75
+
70
76
  /** Structural defects in a gate table — the part checkable without running the pass. Returns
71
77
  * findings rather than throwing, so core stays free of a test-framework import. */
72
78
  export function gateTableDefects<Ctx>(gates: readonly Gate<Ctx>[]): string[] {
@@ -84,7 +90,7 @@ export function gateTableDefects<Ctx>(gates: readonly Gate<Ctx>[]): string[] {
84
90
  out.push(`gate '${g.id}' has no usable \`why\``);
85
91
  }
86
92
  // the one rule that costs something to declare
87
- if (g.sound && !g.guardedBy?.trim()) {
93
+ if (g.sound && !guardsOf(g).some((t) => t.trim())) {
88
94
  out.push(`gate '${g.id}' is marked sound but names no guard`);
89
95
  }
90
96
  }