@asmlift/core 0.5.0 → 0.7.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 (94) hide show
  1. package/README.md +22 -16
  2. package/package.json +1 -1
  3. package/src/backend/c.ts +1 -0
  4. package/src/backend/cfamily.ts +270 -171
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +243 -39
  8. package/src/declare.ts +41 -4
  9. package/src/frontend/mips.ts +11 -0
  10. package/src/frontend/ppc.ts +43 -7
  11. package/src/frontend/ssa.ts +404 -29
  12. package/src/frontend/thumb.ts +2176 -686
  13. package/src/ir/alias.ts +78 -0
  14. package/src/ir/bits.ts +75 -0
  15. package/src/ir/core.ts +345 -2
  16. package/src/ir/opcodes.ts +176 -21
  17. package/src/ir/parse.ts +19 -2
  18. package/src/ir/print.ts +27 -2
  19. package/src/ir/simplify.ts +190 -3
  20. package/src/ir/struct-names.ts +42 -0
  21. package/src/ir/verify.ts +43 -49
  22. package/src/l3/address.ts +62 -0
  23. package/src/l3/advance.ts +373 -0
  24. package/src/l3/argbase.ts +6 -5
  25. package/src/l3/ast.ts +510 -59
  26. package/src/l3/basecse.ts +686 -78
  27. package/src/l3/coalesce.ts +432 -46
  28. package/src/l3/dce.ts +31 -9
  29. package/src/l3/gates.ts +96 -1
  30. package/src/l3/hoist.ts +293 -14
  31. package/src/l3/homesplit.ts +285 -0
  32. package/src/l3/initfirst.ts +301 -0
  33. package/src/l3/inlinebase.ts +193 -0
  34. package/src/l3/mentions.ts +176 -0
  35. package/src/l3/mulfirst.ts +42 -0
  36. package/src/l3/nearbase.ts +152 -0
  37. package/src/l3/offmember.ts +371 -0
  38. package/src/l3/parkfirst.ts +96 -0
  39. package/src/l3/pollguard.ts +154 -0
  40. package/src/l3/ptrfield.ts +227 -0
  41. package/src/l3/regspell.ts +114 -89
  42. package/src/l3/reindex.ts +722 -80
  43. package/src/l3/scopebase.ts +649 -220
  44. package/src/l3/sinkinit.ts +40 -0
  45. package/src/l3/slotorder.ts +123 -0
  46. package/src/l3/storage.ts +48 -0
  47. package/src/l3/symbol-refs.ts +41 -8
  48. package/src/l3/tailmerge.ts +16 -1
  49. package/src/l3/typing.ts +198 -9
  50. package/src/l3/unmerge.ts +687 -0
  51. package/src/l3/unreduce.ts +971 -0
  52. package/src/l3/volatileptr.ts +207 -0
  53. package/src/l3/volatileval.ts +130 -0
  54. package/src/l3/volstore.ts +229 -0
  55. package/src/l3/zerosub.ts +62 -0
  56. package/src/pattern/engine.ts +239 -16
  57. package/src/pipeline.ts +173 -60
  58. package/src/proto.ts +112 -14
  59. package/src/raise/arrays.ts +6 -1
  60. package/src/raise/const.ts +203 -3
  61. package/src/raise/divpow2.ts +4 -4
  62. package/src/raise/extscale.ts +342 -0
  63. package/src/raise/globalshape.ts +1058 -0
  64. package/src/raise/gvn.ts +33 -18
  65. package/src/raise/latch.ts +126 -0
  66. package/src/raise/magicdiv.ts +2 -2
  67. package/src/raise/memberarrays.ts +594 -0
  68. package/src/raise/narrow.ts +124 -0
  69. package/src/raise/narrowlocal.ts +572 -0
  70. package/src/raise/paramwidth.ts +201 -0
  71. package/src/raise/pre-recovery.ts +169 -21
  72. package/src/raise/recover.ts +56 -23
  73. package/src/raise/retsink.ts +585 -19
  74. package/src/raise/shortcircuit.ts +1050 -89
  75. package/src/raise/struct-arrays.ts +19 -2
  76. package/src/raise/structs.ts +34 -4
  77. package/src/raise/tailsink.ts +126 -0
  78. package/src/rank-declare.ts +256 -0
  79. package/src/rank-variations.ts +760 -0
  80. package/src/rank.ts +2122 -326
  81. package/src/structure/analysis.ts +1398 -150
  82. package/src/structure/bitfields.ts +432 -0
  83. package/src/structure/globalaccess.ts +300 -0
  84. package/src/structure/hazards.ts +411 -20
  85. package/src/structure/loops.ts +2 -49
  86. package/src/structure/namecoalesce.ts +454 -0
  87. package/src/structure/structure.ts +3979 -612
  88. package/src/structure/switch-recover.ts +710 -145
  89. package/src/symbols.ts +188 -6
  90. package/src/target.ts +495 -32
  91. package/src/trace.ts +112 -33
  92. package/src/variation-definitions.ts +1540 -0
  93. package/src/variation-gates.ts +89 -0
  94. package/src/variation-tokens.ts +355 -0
@@ -5,13 +5,22 @@
5
5
  //
6
6
  // Crucially, rewrites go through replaceAllUsesWith + DCE — never in-place opcode
7
7
  // mutation of a live value.
8
- import { Fn, Op, Value, defOpMap, mkOp, mkValue, replaceAllUsesWith } from '../ir/core';
9
- import { NEGATED_ICMP, type Opcode, isDceSafe } from '../ir/opcodes';
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';
10
10
  import type { IrType } from '../ir/types';
11
11
  import { T } from '../ir/types';
12
12
 
13
13
  export type MatchNode =
14
- | { op: string; attrEquals?: Record<string, number>; bindImm?: Record<string, string>; args: MatchNode[] }
14
+ | {
15
+ op: string;
16
+ attrEquals?: Record<string, number>;
17
+ bindImm?: Record<string, string>;
18
+ /** Match this node's two operands in the WRITTEN order only, even when the opcode is in
19
+ * `COMMUTATIVE` — for an idiom where the machine's operand order is evidence about the
20
+ * source rather than an accident of the encoding (see HWMOD_PATTERNS for the one case). */
21
+ ordered?: true;
22
+ args: MatchNode[];
23
+ }
15
24
  | { bind: string } // bind this operand's VALUE to a name
16
25
  | { same: string } // this operand must equal a previously-bound value
17
26
  | { constImm: string }; // this operand must be a `const`; bind its numeric VALUE to an imm name
@@ -58,7 +67,7 @@ export interface RewritePattern {
58
67
  // `applies` is DATA, consumed generically by patternApplies — NOT an `arch ==` branch.
59
68
  // `isa` pins the ISA; `compilers` pins which COMPILERS emit this idiom (the same shift-sequence
60
69
  // for `/2` is produced by agbcc AND gcc, so a compiler LIST, not a single arch, is the honest
61
- // predicate); `capabilities` is a hardware predicate. An absent axis means "don't constrain on it".
70
+ // predicate); `capabilities` is a hardware predicate. An absent field means "don't constrain on it".
62
71
  applies: { isa?: string; compilers?: string[]; capabilities?: Partial<{ hwDivide: boolean; hwFloat: boolean }> };
63
72
  match: MatchNode; // rooted at the op result to replace
64
73
  // NOTE: a RELATIONAL guard (a `where` clause constraining the bound immediates, e.g. "two shift
@@ -67,12 +76,27 @@ export interface RewritePattern {
67
76
  // COMPUTED-attr half of the envelope (ImmExpr, above) IS built — earned by the
68
77
  // multiply-by-constant idioms.
69
78
  replaceWith: { op: string; args: ReplaceArg[]; attrs?: Record<string, number | ImmExpr>; resultType?: IrType };
79
+ /** Two REPLACEMENT operand names, in the order the replacement RENDERS them, whose defs C leaves
80
+ * UNSEQUENCED against each other. A fold that collapses a multi-op idiom into one op drops its
81
+ * operands from several uses to one, which lets the structurer inline both at that one use — and
82
+ * then the RECOMPILING COMPILER, not asmlift, picks which of the two runs first. Naming this pair
83
+ * makes the driver refuse a fold that would CHANGE the machine's order. What counts as "the
84
+ * operand" and as "the order" is subtler than it reads, and `reordersUnsequenced` below owns both.
85
+ *
86
+ * The `RightFirst` half is a compiler fact about the operator the replacement spells, verified by
87
+ * compiling in both directions rather than assumed: mwcc lowers `f() % g()` as `bl g; bl f` and
88
+ * `g() % f()` as `bl f; bl g`. It holds per OPERATOR, not per target — the same compiler runs
89
+ * `-`'s LEFT operand first (`f() - g()` is `bl f; bl g`) — which is why it lives on the pattern
90
+ * rather than on TargetDescription, and stays a field name rather than an order enum while there
91
+ * is one inhabitant. The direction was measured on ONE compiler, so `validatePattern` pins the
92
+ * declaring pattern's compiler gate to that set; widening it fails loud. */
93
+ unsequencedRightFirst?: [string, string];
70
94
  }
71
95
 
72
- /** Does this pattern apply to `target`? Every DECLARED axis must match: the ISA (so an idiom can be
96
+ /** Does this pattern apply to `target`? Every DECLARED field must match: the ISA (so an idiom can be
73
97
  * pinned to one frontend), the compiler set (so an idiom fires only for the compilers that emit
74
98
  * it — the reason MIPS+IDO and MIPS+GCC are distinguishable despite one frontend), and every
75
- * declared capability. An omitted axis is unconstrained. */
99
+ * declared capability. An omitted field is unconstrained. */
76
100
  export function patternApplies(
77
101
  p: RewritePattern,
78
102
  target: { id: string; compiler: string; capabilities: { hwDivide: boolean; hwFloat: boolean } },
@@ -142,6 +166,52 @@ export const SDIV_POW2_2: RewritePattern = {
142
166
  replaceWith: { op: 'sdiv', args: ['X'], attrs: { imm: 2 }, resultType: T.s() },
143
167
  };
144
168
 
169
+ // ── the synthesized remainder (PPC) ─────────────────────────────────────────────────────
170
+ // PowerPC divides in hardware but has no remainder instruction, so `a % b` is lowered as
171
+ // `divw rQ,a,b; mullw rP,rQ,b; subf rD,rP,a` — the operator is GONE from the machine code, spelled
172
+ // out as its own definition. Folding the triple back to one `smod`/`umod` gives recovery and the
173
+ // structurer the operator the source wrote, and re-emitting `%` reproduces the triple byte-exact.
174
+ //
175
+ // This is NOT the `capabilities.hwDivide` field: MIPS also divides in hardware and needs no fold at
176
+ // all, because `div` leaves the remainder in `hi` and the frontend reads it straight out. The
177
+ // narrower fact is a hardware divide that yields the QUOTIENT ONLY; `isa: 'ppc'` STANDS IN for it
178
+ // until a second ISA earns the capability, and `compilers` carries the measured half, that mwcc's
179
+ // lowering is exactly this triple in exactly this order. Neither clause is independently
180
+ // falsifiable today — PPC_MWCC is the only target either one selects.
181
+ //
182
+ // `ordered: true` on the multiply says quotient-first is the PRECONDITION for re-emitting `%` — NOT
183
+ // that the order identifies what the source wrote, which is false: `int q = a / b; return a - q *
184
+ // b;` is a decomposition that compiles quotient-first too. So over-firing is byte-neutral (verified
185
+ // by compiling both), and the flag buys the refusal direction: matching the swapped `mullw rP,b,rQ`
186
+ // of `a - a / b * b` would respell an already byte-exact decomposition into a miss.
187
+ //
188
+ // A CONSTANT divisor is out of reach and deliberately left so — see the recognizer table on
189
+ // `smod` in ir/opcodes.ts, which owns why. It costs nothing: `mulli` has no register operand
190
+ // order to lose, and both spellings assemble to the same bytes.
191
+ function hwModPattern(div: 'sdiv' | 'udiv', mod: 'smod' | 'umod'): RewritePattern {
192
+ return {
193
+ id: `hwmod-${mod}`,
194
+ applies: { isa: 'ppc', compilers: ['mwcc'] },
195
+ match: {
196
+ op: 'sub',
197
+ args: [
198
+ { bind: 'A' },
199
+ {
200
+ op: 'mul',
201
+ ordered: true,
202
+ args: [{ op: div, args: [{ same: 'A' }, { bind: 'B' }] }, { same: 'B' }],
203
+ },
204
+ ],
205
+ },
206
+ replaceWith: { op: mod, args: ['A', 'B'] },
207
+ unsequencedRightFirst: ['A', 'B'],
208
+ };
209
+ }
210
+
211
+ /** `a - a / b * b` → `a % b`, signed and unsigned. The `same` bindings are load-bearing: a
212
+ * different dividend or divisor is a subtraction, not a remainder. */
213
+ export const HWMOD_PATTERNS: RewritePattern[] = [hwModPattern('sdiv', 'smod'), hwModPattern('udiv', 'umod')];
214
+
145
215
  // ── multiply-by-constant idioms (DIVMUL) ────────────────────────────────────────────────
146
216
  // A compiler strength-reduces `x * C` for a small constant C into shifts + one add/sub, because a
147
217
  // shift-add chain is cheaper than a general multiply. The reduction is COMPILER-driven and shared
@@ -307,15 +377,16 @@ export const NOT_CMP_PATTERNS: RewritePattern[] = [
307
377
 
308
378
  // The DEFAULT idiom bundle `decompile()` applies when the caller passes no `patterns`. It is
309
379
  // EVERY idiom asmlift owns; the list self-selects per target through patternApplies — agbcc/gcc get
310
- // sdiv-pow2, agbcc/ido/gcc get the mul-const folds, mwcc gets cntlzw-eq0 + rotl-mirror, and agbcc
311
- // gets the casts. MOST patterns are `{compilers}`-gated because they trade one spelling for another
312
- // and are only byte-safe where measured; the boolean-negation folds are deliberately UNGATED (see
313
- // their comment — the shape is its own gate), so "gated per compiler" is the common case, not the
314
- // invariant. Ordered like the sub-bundles: the
315
- // division idiom, then the multiplies (base folds before the composite tail). Passing an explicit
316
- // `patterns` (including `[]`) overrides this — `[]` runs the naive lift with no idiom folding.
380
+ // sdiv-pow2, agbcc/ido/gcc get the mul-const folds, mwcc gets cntlzw-eq0 + rotl-mirror + the PPC
381
+ // remainder fold, and agbcc gets the casts. MOST patterns are `{compilers}`-gated because they
382
+ // trade one spelling for another and are only byte-safe where measured; the boolean-negation folds
383
+ // are deliberately UNGATED (see their comment — the shape is its own gate), so "gated per compiler"
384
+ // is the common case, not the invariant. Ordered like the sub-bundles: the division idioms, then
385
+ // the multiplies (base folds before the composite tail). Passing an explicit `patterns` (including
386
+ // `[]`) overrides this — `[]` runs the naive lift with no idiom folding.
317
387
  export const DEFAULT_IDIOM_PATTERNS: RewritePattern[] = [
318
388
  SDIV_POW2_2,
389
+ ...HWMOD_PATTERNS,
319
390
  CNTLZW_EQ0,
320
391
  // AFTER cntlzw-eq0, which is what turns mwcc's `clz(x) >> 5` into the `icmp_eq` this fold then
321
392
  // negates — `!(x == 0)` composes only in that order (each pattern runs to fixpoint in turn).
@@ -331,6 +402,7 @@ export const DEFAULT_IDIOM_PATTERNS: RewritePattern[] = [
331
402
  // same test, and which one a frontend builds is an accident of how the branch was decoded — the
332
403
  // zero-test folds must match either. The ORDERED comparisons are deliberately absent: swapping the
333
404
  // operands of `a < b` is `b > a`, a different opcode, which this mechanism cannot express.
405
+ // Membership here is about the OPCODE; a single pattern node opts back out with `ordered: true`.
334
406
  const COMMUTATIVE = new Set(['add', 'mul', 'and', 'or', 'xor', 'icmp_eq', 'icmp_ne']);
335
407
 
336
408
  interface Binds {
@@ -379,9 +451,10 @@ function tryMatch(node: MatchNode, v: Value, defs: Map<Value, Op>, b: Binds): bo
379
451
  if (d.operands.length !== node.args.length) {
380
452
  return false;
381
453
  }
382
- // A commutative binary op matches its two args in EITHER order. Each order is tried on a cloned
383
- // bind map so a partial (then-failed) match can't leak bindings; the first full match commits.
384
- if (COMMUTATIVE.has(d.opcode) && node.args.length === 2) {
454
+ // A commutative binary op matches its two args in EITHER order, unless the pattern declared this
455
+ // node `ordered`. Each order is tried on a cloned bind map so a partial (then-failed) match can't
456
+ // leak bindings; the first full match commits.
457
+ if (COMMUTATIVE.has(d.opcode) && node.args.length === 2 && !node.ordered) {
385
458
  for (const [i, j] of [
386
459
  [0, 1],
387
460
  [1, 0],
@@ -402,8 +475,155 @@ function tryMatch(node: MatchNode, v: Value, defs: Map<Value, Op>, b: Binds): bo
402
475
  return node.args.every((a, i) => tryMatch(a, d.operands[i], defs, b));
403
476
  }
404
477
 
478
+ /** Walk a pattern's match DAG once and reject a declaration that could not have an effect, or one
479
+ * whose compiler gate outruns the evidence behind it. Same diagnosability-first rule as the
480
+ * unbound-`replaceWith` throw below: `ordered` is consulted ONLY inside `tryMatch`'s
481
+ * commutative-swap branch, so on a non-commutative or non-binary node it would be silently inert,
482
+ * and a pattern author (or a generator emitting patterns as data) would get no error and no effect.
483
+ *
484
+ * Everything checked here is a property of the pattern OBJECT, so the answer is memoized against it
485
+ * rather than recomputed per lift — patterns may still be built at runtime, which is why this is a
486
+ * WeakSet and not module-scope validation of the DEFAULT list. Only a PASSING run is recorded: a
487
+ * pattern that threw must throw again on the next lift, or the first caller (annotate mode, which
488
+ * swallows the throw into a stub) would silently license the malformed pattern for the whole run. */
489
+ const UNSEQUENCED_RIGHT_FIRST_MEASURED_ON = ['mwcc'];
490
+ const validated = new WeakSet<RewritePattern>();
491
+
492
+ function validatePattern(pat: RewritePattern): void {
493
+ if (validated.has(pat)) {
494
+ return;
495
+ }
496
+ const walk = (n: MatchNode): void => {
497
+ if (!('op' in n)) {
498
+ return;
499
+ }
500
+ if (n.ordered && !(COMMUTATIVE.has(n.op) && n.args.length === 2)) {
501
+ throw new Error(
502
+ `pattern '${pat.id}' declares 'ordered' on a '${n.op}' node with ${n.args.length} operand(s), ` +
503
+ `where the written order is already the only reading — the flag would be inert`,
504
+ );
505
+ }
506
+ n.args.forEach(walk);
507
+ };
508
+ walk(pat.match);
509
+ if (pat.unsequencedRightFirst) {
510
+ for (const name of pat.unsequencedRightFirst) {
511
+ if (!pat.replaceWith.args.includes(name)) {
512
+ throw new Error(
513
+ `pattern '${pat.id}' names '${name}' in 'unsequencedRightFirst', which is not a replaceWith operand`,
514
+ );
515
+ }
516
+ }
517
+ const on = pat.applies.compilers ?? [];
518
+ const measured = UNSEQUENCED_RIGHT_FIRST_MEASURED_ON;
519
+ if (on.length !== measured.length || !measured.every((c) => on.includes(c))) {
520
+ throw new Error(
521
+ `pattern '${pat.id}' declares 'unsequencedRightFirst' but applies to compilers [${on.join(', ')}]; ` +
522
+ `the operand direction is only measured for [${measured.join(', ')}] — measure the new one and widen the set`,
523
+ );
524
+ }
525
+ }
526
+ validated.add(pat);
527
+ }
528
+
529
+ /** Would this fold DE-SEQUENCE the two named operands — leave the recompiling compiler, rather than
530
+ * the machine code, deciding which of two observable effects runs first?
531
+ *
532
+ * The fold collapses a several-op idiom into one, which drops each named operand from two uses to
533
+ * one; the structurer then inlines a single-use def at its one use, TRANSITIVELY THROUGH PURE
534
+ * SINGLE-USE OPS. So what lands at an operand position is that operand's whole inlinable CONE, not
535
+ * just its def, and the question has to be asked over the cones: one pure `+ 1` between an effect
536
+ * and the fold's operand is the difference between `f() % g()` and `(f() + 1) % g()`, and the
537
+ * second one reorders exactly as the first does. Both cones become operands of ONE expression,
538
+ * where C leaves their order unspecified. asmlift's inline-at-use model (structure/analysis.ts)
539
+ * exempts exactly this case — "a sibling effect inlined into the SAME statement is not a reorder,
540
+ * the recompiling compiler orders unsequenced operands of one expression exactly as it originally
541
+ * chose to". That premise holds only when the expression asmlift re-spells is the one the source
542
+ * wrote. A fold INVENTS an expression, so it must check.
543
+ *
544
+ * A cone's members are weighed by ORDER_SENSITIVE_OPS, not EFFECTFUL_OPS: a memory read answers
545
+ * whichever stores ran before it, so hoisting a `load` over a `call` — one asmlift may itself be
546
+ * passing the loaded pointer to — changes the answer as surely as swapping two calls. Two READS
547
+ * are the exception and commute, the same fact the structurer states as "a load never bars a
548
+ * load", so a hazard needs an EFFECT on at least one side; `*p % *q` is admitted.
549
+ *
550
+ * Refuses only what it must — the alternative spelling (the idiom written out) names the operands
551
+ * and states the order, so a refusal is a loud, correct, slightly-worse-scoring answer. */
552
+ function reordersUnsequenced(
553
+ fn: Fn,
554
+ root: Op,
555
+ names: [string, string],
556
+ binds: Binds,
557
+ defs: Map<Value, Op>,
558
+ pid: string,
559
+ ): boolean {
560
+ // Both cones are read within the ROOT's block, on two different grounds. A cross-block EFFECT the
561
+ // structurer materializes unconditionally — its execution would otherwise become path-dependent —
562
+ // so the C names it and pins the order there. A cross-block READ it does not; what pins that one
563
+ // is the structurer's own def→render write scan, where a call counts as a write, so a read cannot
564
+ // reach this expression across the very call it would be racing.
565
+ const blk: Block | undefined = fn.blocks.find((b) => b.ops.includes(root));
566
+ if (!blk) {
567
+ return false;
568
+ }
569
+ const at = new Map<Op, number>(blk.ops.map((o, i) => [o, i]));
570
+ const uses = new Map<Value, number>();
571
+ for (const b of fn.blocks) {
572
+ for (const o of b.ops) {
573
+ for (const v of [...o.operands, ...o.successors.flatMap((x) => x.args)]) {
574
+ uses.set(v, (uses.get(v) ?? 0) + 1);
575
+ }
576
+ }
577
+ }
578
+ /** What the structurer may pull in at one operand position: the operand's own def — which this
579
+ * fold is about to drop to a single use — then each operand def that is ALREADY single-use, a
580
+ * multi-use value being named and so staying a statement of its own. */
581
+ const coneOf = (name: string): Set<Op> => {
582
+ const v = binds.values.get(name);
583
+ if (!v) {
584
+ throw new Error(`pattern '${pid}' names unbound value '${name}' in 'unsequencedRightFirst'`);
585
+ }
586
+ const cone = new Set<Op>();
587
+ const stack: (Op | undefined)[] = [defs.get(v)];
588
+ while (stack.length) {
589
+ const op = stack.pop();
590
+ if (!op || cone.has(op) || !at.has(op)) {
591
+ continue;
592
+ }
593
+ cone.add(op);
594
+ for (const o of op.operands) {
595
+ if (uses.get(o) === 1) {
596
+ stack.push(defs.get(o));
597
+ }
598
+ }
599
+ }
600
+ return cone;
601
+ };
602
+ const [lc, rc] = names.map(coneOf);
603
+ const sensitive = (c: Set<Op>) => [...c].filter((o) => ORDER_SENSITIVE_OPS.has(o.opcode));
604
+ for (const l of sensitive(lc)) {
605
+ for (const r of sensitive(rc)) {
606
+ // One op standing in BOTH cones is one evaluation and cannot be sequenced against itself; two
607
+ // READS commute; and a right-cone op the machine ALREADY runs first loses nothing.
608
+ if (l === r || at.get(l)! >= at.get(r)! || !(EFFECTFUL_OPS.has(l.opcode) || EFFECTFUL_OPS.has(r.opcode))) {
609
+ continue;
610
+ }
611
+ // The left one runs first, so an inlined `A op B` would swap them — UNLESS a sibling effect
612
+ // stands between, which the inline-at-use model refuses to cross, forcing a named temp at the
613
+ // def's own position. A cone member is no sibling: it is inlined into this very expression.
614
+ if (
615
+ !blk.ops.slice(at.get(l)! + 1, at.get(r)!).some((o) => EFFECTFUL_OPS.has(o.opcode) && !lc.has(o) && !rc.has(o))
616
+ ) {
617
+ return true;
618
+ }
619
+ }
620
+ }
621
+ return false;
622
+ }
623
+
405
624
  /** Apply one pattern greedily to a fixed point. Returns the number of rewrites. */
406
625
  export function applyPattern(fn: Fn, pat: RewritePattern): number {
626
+ validatePattern(pat);
407
627
  let count = 0,
408
628
  changed = true;
409
629
  while (changed) {
@@ -419,6 +639,9 @@ export function applyPattern(fn: Fn, pat: RewritePattern): number {
419
639
  if (!tryMatch(pat.match, op.results[0], defs, binds)) {
420
640
  continue;
421
641
  }
642
+ if (pat.unsequencedRightFirst && reordersUnsequenced(fn, op, pat.unsequencedRightFirst, binds, defs, pat.id)) {
643
+ continue;
644
+ }
422
645
  // Materialize any synthesized-constant replacement operands as their own `const` ops,
423
646
  // spliced in before the rewrite; bound-value operands resolve from the value binds.
424
647
  const rw = pat.replaceWith;