@asmlift/core 0.4.0 → 0.6.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.
- package/README.md +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +238 -164
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +341 -22
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +24 -6
- package/src/frontend/opaque.ts +31 -18
- package/src/frontend/ppc.ts +54 -7
- package/src/frontend/ssa.ts +632 -13
- package/src/frontend/thumb.ts +2786 -286
- package/src/ir/alias.ts +129 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +156 -27
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/argbase.ts +8 -2
- package/src/l3/ast.ts +464 -49
- package/src/l3/basecse.ts +709 -88
- package/src/l3/coalesce.ts +521 -66
- package/src/l3/dce.ts +54 -19
- package/src/l3/gates.ts +88 -0
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +113 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +110 -85
- package/src/l3/reindex.ts +715 -78
- package/src/l3/scopebase.ts +649 -219
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +23 -4
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +263 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +236 -13
- package/src/pipeline.ts +206 -49
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +4 -3
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +44 -19
- package/src/raise/latch.ts +126 -0
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +556 -0
- package/src/raise/paramwidth.ts +179 -0
- package/src/raise/pre-recovery.ts +101 -16
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +215 -14
- package/src/raise/shortcircuit.ts +477 -79
- package/src/raise/struct-arrays.ts +21 -3
- package/src/raise/structs.ts +61 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1726 -251
- package/src/structure/analysis.ts +1516 -220
- package/src/structure/bitfields.ts +332 -0
- package/src/structure/globalaccess.ts +274 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +435 -0
- package/src/structure/structure.ts +2850 -533
- package/src/structure/switch-recover.ts +688 -147
- package/src/symbols.ts +62 -1
- package/src/target.ts +367 -24
- package/src/trace.ts +111 -32
package/src/pattern/engine.ts
CHANGED
|
@@ -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
|
-
| {
|
|
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
|
|
@@ -67,6 +76,21 @@ 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
96
|
/** Does this pattern apply to `target`? Every DECLARED axis must match: the ISA (so an idiom can be
|
|
@@ -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` axis: 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
|
|
311
|
-
// gets the casts. MOST patterns are `{compilers}`-gated because they
|
|
312
|
-
// and are only byte-safe where measured; the boolean-negation folds
|
|
313
|
-
// their comment — the shape is its own gate), so "gated per compiler"
|
|
314
|
-
// invariant. Ordered like the sub-bundles: the
|
|
315
|
-
//
|
|
316
|
-
// `
|
|
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
|
|
383
|
-
// bind map so a partial (then-failed) match can't
|
|
384
|
-
|
|
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;
|