@asmlift/core 0.5.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 -167
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +194 -39
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +11 -0
- package/src/frontend/ppc.ts +43 -7
- package/src/frontend/ssa.ts +404 -29
- package/src/frontend/thumb.ts +2176 -686
- package/src/ir/alias.ts +54 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +140 -21
- 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 +2 -1
- package/src/l3/ast.ts +464 -57
- package/src/l3/basecse.ts +664 -76
- package/src/l3/coalesce.ts +429 -43
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +21 -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 +644 -218
- 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 +15 -0
- 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 +157 -56
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +33 -18
- 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 +97 -14
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +210 -10
- package/src/raise/shortcircuit.ts +474 -74
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +33 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1723 -272
- package/src/structure/analysis.ts +1392 -141
- 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 +2678 -526
- package/src/structure/switch-recover.ts +616 -144
- 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;
|
package/src/pipeline.ts
CHANGED
|
@@ -5,28 +5,32 @@ import {
|
|
|
5
5
|
ContractError,
|
|
6
6
|
assertDerefsTyped,
|
|
7
7
|
assertEffectsPreserved,
|
|
8
|
+
assertLocalsWritten,
|
|
8
9
|
assertResolved,
|
|
9
10
|
assertTypesRecovered,
|
|
10
11
|
} from './contracts';
|
|
11
12
|
import type { AsmData } from './frontend/asmdata';
|
|
12
13
|
import { FrontendUnsupportedError } from './frontend/errors';
|
|
13
14
|
import { frontendFor } from './frontend/registry';
|
|
14
|
-
import { type
|
|
15
|
+
import { type Fn, reachableBlocks } from './ir/core';
|
|
15
16
|
import { print } from './ir/print';
|
|
17
|
+
import { firstTrivialPhi } from './ir/simplify';
|
|
16
18
|
import { T } from './ir/types';
|
|
17
19
|
import { VerifyError, verify } from './ir/verify';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
+
import { LanguageBackend, SFn, gapReasonFor, walkExprs } from './l3/ast';
|
|
21
|
+
import { BASECSE_GATES, hoistBaseLocals } from './l3/basecse';
|
|
20
22
|
import { eliminateDeadStores } from './l3/dce';
|
|
21
23
|
import { mergeCommonTails } from './l3/tailmerge';
|
|
22
24
|
import { DEFAULT_IDIOM_PATTERNS, RewritePattern, applyPattern, dce, patternApplies } from './pattern/engine';
|
|
23
|
-
import { type Prototypes, prototypesFromSymbols } from './proto';
|
|
25
|
+
import { type FnProto, type Prototypes, prototypesFromSymbols } from './proto';
|
|
24
26
|
import { RaiseUnsupportedError } from './raise/errors';
|
|
25
|
-
import {
|
|
27
|
+
import { assumedShapes, inferGlobalArrays, orderLicensedGlobals } from './raise/globalshape';
|
|
28
|
+
import { foldEmptyLatches } from './raise/latch';
|
|
29
|
+
import { type PreRecoveryOptions, type PreRecoveryPass, runPreRecovery } from './raise/pre-recovery';
|
|
26
30
|
import { recoverTypes } from './raise/recover';
|
|
27
31
|
import { sinkReturns } from './raise/retsink';
|
|
28
32
|
import { StructureError, structure } from './structure/structure';
|
|
29
|
-
import { type SymbolMap, symbolsByName } from './symbols';
|
|
33
|
+
import { type SymbolInfo, type SymbolMap, symbolsByName } from './symbols';
|
|
30
34
|
import { type TargetDescription, structureOptionsFor } from './target';
|
|
31
35
|
|
|
32
36
|
/** How a gap (a construct asmlift cannot faithfully model) degrades:
|
|
@@ -74,10 +78,36 @@ export interface DecompileResult {
|
|
|
74
78
|
sfn: SFn;
|
|
75
79
|
ir: { raw: string; folded: string; recovered: string }; // IR dumps: post-lift, post-idiom, post-recovery
|
|
76
80
|
patternHits: number;
|
|
77
|
-
/** structured gap list — ALWAYS present
|
|
78
|
-
*
|
|
79
|
-
*
|
|
81
|
+
/** structured gap list — ALWAYS present. Non-empty ⇔ the source contains ASMLIFT_ERROR markers /
|
|
82
|
+
* a stub and will NOT compile until the user acts (the loud-in-artifact contract).
|
|
83
|
+
*
|
|
84
|
+
* EMPTY IS THE ABSENCE OF A GAP, NOT A PROMISE THAT THE C COMPILES. A candidate can also fail
|
|
85
|
+
* on a symbol the caller never declared, which is a CONTEXT question the benchmark answers by
|
|
86
|
+
* escalating to the vendored preprocessed context. Most such names survive K&R implicit
|
|
87
|
+
* declaration; a function ADDRESS does not, and klonoa's `UpdateStageSelectScreen` reports 0
|
|
88
|
+
* gaps while emitting `((s32 *)50345232)[1] = &HandlePauseMenuInput;`, which agbcc rejects
|
|
89
|
+
* where the plain call above it passes with a warning. Declaring an address-taken unknown
|
|
90
|
+
* callee would close it — an emitter change, moving source bytes on every row that has one. */
|
|
80
91
|
diagnostics: Diagnostic[];
|
|
92
|
+
/** THE SHAPES THIS SOURCE'S SPELLING ASSUMES, which no symbol map supplied (raise/globalshape.ts).
|
|
93
|
+
*
|
|
94
|
+
* Everything else a backend emits is byte-correct under ANY declaration of the names it spells
|
|
95
|
+
* — that is exactly why `((T *)&gSym)[i]` is the fallback (structure/globalaccess.ts). A bare
|
|
96
|
+
* `gSym[i]` is not: it means what the DECLARATION of `gSym` says it means, and where that
|
|
97
|
+
* declaration was derived from the assembly rather than read from the project's map, the
|
|
98
|
+
* emitted source is right about the target's bytes only beside the declaration derived with it.
|
|
99
|
+
* The element SIGNEDNESS is the sharp case, and it is an assumption rather than a reading:
|
|
100
|
+
* compiled through the benchmark's own agbcc command, `(u16)gS[i]` over `extern const s16 gS[]`
|
|
101
|
+
* and `gS[i]` over `extern const u16 gS[]` are the SAME OBJECT, so the assembly cannot say which
|
|
102
|
+
* the source wrote — asmlift picks the one its own declaration block states.
|
|
103
|
+
*
|
|
104
|
+
* So this travels with the source on every path that can emit it: the scoring layer renders it
|
|
105
|
+
* (declare.ts, and main.ts's `[declared]` block), and a caller that shows the source alone must
|
|
106
|
+
* show these too, or it is publishing a spelling whose meaning it has not stated. Empty on every
|
|
107
|
+
* run that assumed nothing — which includes every derived shape the structurer did not spell
|
|
108
|
+
* bare, and every name the caller's own map described (raise/globalshape.ts `assumedShapes`
|
|
109
|
+
* computes that narrowing and names the corpus row behind each half). */
|
|
110
|
+
assumedSymbols: SymbolInfo[];
|
|
81
111
|
}
|
|
82
112
|
|
|
83
113
|
export function decompile(
|
|
@@ -115,39 +145,66 @@ function runTower(
|
|
|
115
145
|
// (1) lift: ISA frontend (resolved by target) → L1 with block-argument SSA
|
|
116
146
|
const fn = frontendFor(target).lift(name, asm, target, prototypes, opts.asmData, opts.symbols);
|
|
117
147
|
verify(fn);
|
|
118
|
-
|
|
148
|
+
// Every dump carries the write-order record (ir/print.ts `PrintOptions`): it decides the
|
|
149
|
+
// edge-copy order and the raising folds mutate it, so two dumps compare whole program states.
|
|
150
|
+
const raw = print(fn, { writeOrder: true });
|
|
151
|
+
// (1.5) the ARRAY SHAPES this function's own assembly evidences, for globals the project map
|
|
152
|
+
// does not describe. Read HERE, off the lifted fn, because the fact it needs — whether the base
|
|
153
|
+
// was materialized before the index was scaled — is destroyed by the raising tower below
|
|
154
|
+
// (raise/globalshape.ts's module note). Empty unless the target opts in.
|
|
155
|
+
const inferredSymbols = inferGlobalArrays(fn, target);
|
|
156
|
+
// …and the ORDER half of the same reading, which reaches names the shape derivation refuses (a
|
|
157
|
+
// struct element among them). Read off the same lifted fn, for the same reason.
|
|
158
|
+
const orderLicensed = orderLicensedGlobals(fn, target);
|
|
119
159
|
|
|
120
160
|
// (2) idiom fold: apply serializable patterns on the IR (the AI-improvement surface),
|
|
121
161
|
// gated generically by the Target's capabilities (not an `arch ==` branch).
|
|
122
162
|
const patternHits = applyIdiomPatterns(fn, target, opts.patterns);
|
|
123
|
-
const folded = print(fn);
|
|
163
|
+
const folded = print(fn, { writeOrder: true });
|
|
124
164
|
|
|
125
165
|
// (2.35–3.5) pre-recovery recognizers → type recovery → return-sinking, the ONE shared spine
|
|
126
166
|
// (`raiseRecovered`) that trace.ts and the cli's rank.ts/report.ts also run.
|
|
127
|
-
raiseRecovered(fn, target);
|
|
128
|
-
const recovered = print(fn);
|
|
167
|
+
raiseRecovered(fn, target, {}, prototypes[name]);
|
|
168
|
+
const recovered = print(fn, { writeOrder: true });
|
|
129
169
|
|
|
130
170
|
// (4) structure: IR → neutral AST; boundary contract: no unresolved value leaked (strict), or
|
|
131
171
|
// every unresolved value spelled as a loud ASMLIFT_ERROR marker (annotate).
|
|
172
|
+
const mapSymbols = opts.symbols ? symbolsByName(opts.symbols) : undefined;
|
|
132
173
|
const sfn = structureChecked(fn, {
|
|
133
174
|
...structureOptionsFor(target, prototypes[name]?.returnsVoid ?? false),
|
|
175
|
+
// What the EMITTED LANGUAGE can say is a structuring input wherever two recoveries of one
|
|
176
|
+
// shape are behaviourally identical and only one of them is printable (switch fall-through
|
|
177
|
+
// vs plain if-nesting): recovery must not mint a tree this backend would refuse.
|
|
178
|
+
spellSwitchFallthrough: backend.spellsSwitchFallthrough,
|
|
134
179
|
onGap,
|
|
135
|
-
...(
|
|
180
|
+
...(mapSymbols ? { symbols: mapSymbols } : {}),
|
|
181
|
+
...(inferredSymbols.size ? { inferredSymbols } : {}),
|
|
182
|
+
...(orderLicensed.size ? { orderLicensedGlobals: orderLicensed } : {}),
|
|
136
183
|
});
|
|
137
184
|
|
|
138
185
|
// (5) lower + print: neutral AST → target language
|
|
139
186
|
const source = backend.emit(sfn);
|
|
140
187
|
|
|
141
|
-
return {
|
|
188
|
+
return {
|
|
189
|
+
source,
|
|
190
|
+
sfn,
|
|
191
|
+
ir: { raw, folded, recovered },
|
|
192
|
+
patternHits,
|
|
193
|
+
diagnostics: collectMarkers(sfn),
|
|
194
|
+
// WHAT THE SOURCE RESTS ON, not what the derivation found: a shape the structurer did not
|
|
195
|
+
// spell bare, and a name the caller's own map described, are both obligations this reader
|
|
196
|
+
// does not have (raise/globalshape.ts `assumedShapes`).
|
|
197
|
+
assumedSymbols: assumedShapes(inferredSymbols, sfn, mapSymbols),
|
|
198
|
+
};
|
|
142
199
|
}
|
|
143
200
|
|
|
144
201
|
// ── the shared raising tower ────────────────────────────────────────────────────────────────
|
|
145
|
-
// decompile(), decompileTraced (trace.ts), and the cli's
|
|
146
|
-
// decompileWithReport + its score probe (report.ts) all raise a lifted fn through the SAME
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
// observe unverified IR.
|
|
202
|
+
// decompile(), decompileTraced (trace.ts), rank.ts's decompileRanked and the cli's
|
|
203
|
+
// decompileWithReport + its score probe (report.ts) all raise a lifted fn through the SAME stage
|
|
204
|
+
// sequence. Two things vary per caller and nothing else does: the optional HOOKS — rank pins its
|
|
205
|
+
// signedness candidate at `beforeRecover`, decompileTraced pushes a trace entry after each stage —
|
|
206
|
+
// and the `pre` options bag, which reaches the pre-recovery passes themselves. Every hook fires
|
|
207
|
+
// AFTER the stage's verify, so a hook can never observe unverified IR.
|
|
151
208
|
|
|
152
209
|
/** Stage 2 — idiom fold: filter the pattern set by target capabilities, apply, dce + verify.
|
|
153
210
|
* Returns total hits. `patterns` defaults to DEFAULT_IDIOM_PATTERNS exactly like decompile(). */
|
|
@@ -173,17 +230,47 @@ export interface RaiseHooks {
|
|
|
173
230
|
afterRecover?: () => void;
|
|
174
231
|
/** after return-sinking, only when it changed the fn (fires after its verify) */
|
|
175
232
|
afterRetsink?: () => void;
|
|
233
|
+
/** after empty-latch folding, only when it removed a block (fires after its verify) */
|
|
234
|
+
afterLatchFold?: () => void;
|
|
176
235
|
}
|
|
177
236
|
|
|
178
237
|
/** Stages 2.35–3.5 — pre-recovery recognizers (the shared ordered list in raise/pre-recovery.ts)
|
|
179
238
|
* → type recovery (boundary contract: no `unknown` survives) → return-sinking (tail-duplicate a
|
|
180
|
-
* return-only merge so short-circuits emit early returns)
|
|
181
|
-
* changed the IR.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
239
|
+
* return-only merge so short-circuits emit early returns) → empty-latch folding (splice out a
|
|
240
|
+
* back-edge block SSA construction emptied). `verify` after every pass that changed the IR.
|
|
241
|
+
*
|
|
242
|
+
* The two CFG passes look ordered and are not: running latch folding FIRST instead changes
|
|
243
|
+
* nothing across a 3337-function agbcc corpus. Worth saying, because folding an empty block ahead
|
|
244
|
+
* of return-sinking does take away `br` predecessors it needs — the dominance gate is what makes
|
|
245
|
+
* that unreachable, since the blocks retsink wants are never back-edge sources. It goes last
|
|
246
|
+
* because that is where the CFG stops moving.
|
|
247
|
+
*
|
|
248
|
+
* The tail is three separate parameters rather than an options bag on purpose: this is the
|
|
249
|
+
* published package root export, so every caller outside this repo is pinned to the positions.
|
|
250
|
+
*
|
|
251
|
+
* @param hooks per-stage observers; see {@link RaiseHooks}. Each fires after that stage's verify.
|
|
252
|
+
* @param self this function's own prototype, where the caller has one — what the pre-recovery
|
|
253
|
+
* passes read to type the parameters they are narrowing.
|
|
254
|
+
* @param pre per-caller PRE-RECOVERY options. One shipped user: rank.ts's `/connective`
|
|
255
|
+
* candidate, which passes `{ shortCircuit: { foldTreeOwned: true } }` to take the
|
|
256
|
+
* fold the comparison-tree refusal owns (raise/shortcircuit.ts). */
|
|
257
|
+
export function raiseRecovered(
|
|
258
|
+
fn: Fn,
|
|
259
|
+
target: TargetDescription,
|
|
260
|
+
hooks: RaiseHooks = {},
|
|
261
|
+
self?: FnProto,
|
|
262
|
+
pre: PreRecoveryOptions = {},
|
|
263
|
+
): void {
|
|
264
|
+
runPreRecovery(
|
|
265
|
+
fn,
|
|
266
|
+
target,
|
|
267
|
+
(pass, result) => {
|
|
268
|
+
verify(fn);
|
|
269
|
+
hooks.afterPass?.(pass, result);
|
|
270
|
+
},
|
|
271
|
+
self,
|
|
272
|
+
pre,
|
|
273
|
+
);
|
|
187
274
|
hooks.beforeRecover?.();
|
|
188
275
|
recoverTypes(fn);
|
|
189
276
|
verify(fn);
|
|
@@ -193,6 +280,26 @@ export function raiseRecovered(fn: Fn, target: TargetDescription, hooks: RaiseHo
|
|
|
193
280
|
verify(fn);
|
|
194
281
|
hooks.afterRetsink?.();
|
|
195
282
|
}
|
|
283
|
+
if (foldEmptyLatches(fn)) {
|
|
284
|
+
verify(fn);
|
|
285
|
+
hooks.afterLatchFold?.();
|
|
286
|
+
}
|
|
287
|
+
// THE BOUNDARY POSTCONDITION. Above this line passes move the CFG; below it nothing does, and the
|
|
288
|
+
// structurer reads a block parameter as a JOIN — a name it must give a local of its own. A param
|
|
289
|
+
// whose every in-edge carries one value is not a join, and leaving one standing is how a
|
|
290
|
+
// CFG-motion pass does its damage three stages away rather than where it happened: retsink's own
|
|
291
|
+
// stranded merge was destroyed into `v0 = 0; return v0;` and read by Regime-A switch recovery as a
|
|
292
|
+
// SECOND `default` candidate, declining every fall-through tree. This names it at retsink.
|
|
293
|
+
// Not an `ir/verify.ts` rule: a trivial phi is well-formed IR, and both SSA construction and the
|
|
294
|
+
// `addrnum` pass mint one and clear it inside their own scope (see `firstTrivialPhi`).
|
|
295
|
+
const stranded = firstTrivialPhi(fn);
|
|
296
|
+
if (stranded) {
|
|
297
|
+
throw new Error(
|
|
298
|
+
`internal: raising left a trivial phi — block #${fn.blocks.indexOf(stranded.block)} takes ` +
|
|
299
|
+
`'${stranded.param}', whose every in-edge carries one value. A pass that retires an in-edge ` +
|
|
300
|
+
`must run simplifyTrivialPhis after it.`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
196
303
|
}
|
|
197
304
|
|
|
198
305
|
/** Run `body`; if it declines, name the unmodelled instructions the function carries.
|
|
@@ -214,15 +321,7 @@ function attributeOpaques<T>(fn: Fn, body: () => T): T {
|
|
|
214
321
|
if (!(e instanceof StructureError) || !fn.blocks[0]) {
|
|
215
322
|
throw e;
|
|
216
323
|
}
|
|
217
|
-
const seen =
|
|
218
|
-
for (const stack = [fn.blocks[0]]; stack.length;) {
|
|
219
|
-
for (const s of successorsOf(stack.pop()!)) {
|
|
220
|
-
if (!seen.has(s)) {
|
|
221
|
-
seen.add(s);
|
|
222
|
-
stack.push(s);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
324
|
+
const seen = reachableBlocks(fn);
|
|
226
325
|
const names = new Set<string>();
|
|
227
326
|
for (const b of seen) {
|
|
228
327
|
for (const op of b.ops) {
|
|
@@ -244,20 +343,26 @@ function attributeOpaques<T>(fn: Fn, body: () => T): T {
|
|
|
244
343
|
/** Stage 4 — structure + its boundary contracts, always as a pair. */
|
|
245
344
|
export function structureChecked(fn: Fn, opts: Parameters<typeof structure>[1]): SFn {
|
|
246
345
|
const raw = attributeOpaques(fn, () => structure(fn, opts));
|
|
247
|
-
//
|
|
346
|
+
// The boundary contracts run on the pre-DCE tree: the readability pass must never be able to
|
|
248
347
|
// hide a structuring defect by dropping the dead statement that carries it. assertResolved
|
|
249
348
|
// catches an unresolved `?` value; assertDerefsTyped catches an ill-typed deref (e.g. a pointer
|
|
250
|
-
// under a rejected operator) — even one sitting in dead code structure emitted
|
|
251
|
-
//
|
|
349
|
+
// under a rejected operator) — even one sitting in dead code structure emitted;
|
|
350
|
+
// assertLocalsWritten catches a materialized def whose assignment no position emitted. DCE then
|
|
351
|
+
// only removes statements/flips branches over an already-validated tree.
|
|
252
352
|
assertResolved(raw);
|
|
253
353
|
assertDerefsTyped(raw);
|
|
354
|
+
assertLocalsWritten(raw);
|
|
254
355
|
assertEffectsPreserved(fn, raw);
|
|
255
356
|
// Then the readability/quality rewrites: merge a statement common to every arm of an if,
|
|
256
|
-
// drop dead stores (whose empty-then peephole flips the arm the merge empties), then hoist
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
|
|
357
|
+
// drop dead stores (whose empty-then peephole flips the arm the merge empties), then hoist each
|
|
358
|
+
// leaf base the DEFAULT gate table admits into a typed local pointer. The hoist moves the deref
|
|
359
|
+
// cast from each `index` node onto the local's initializer, so re-validate deref typing on the
|
|
360
|
+
// rewritten tree.
|
|
361
|
+
// Both arguments SPELLED, defaults or not: this is the one call to this pass that is committed
|
|
362
|
+
// rather than offered, so it is the one whose gate table and whose placement can cost a MATCH
|
|
363
|
+
// instead of a candidate (docs/level-tower.md). A committed policy that reads as "whatever the
|
|
364
|
+
// default is" is the policy nobody reviews.
|
|
365
|
+
const sfn = hoistBaseLocals(eliminateDeadStores(mergeCommonTails(raw)), BASECSE_GATES, 'head');
|
|
261
366
|
assertDerefsTyped(sfn);
|
|
262
367
|
// Re-checked after the readability rewrites for the same reason deref typing is: a pass that
|
|
263
368
|
// merges arms or drops statements must not be able to lose or duplicate a call.
|
|
@@ -317,26 +422,22 @@ export function stubResult(name: string, asm: string, backend: LanguageBackend,
|
|
|
317
422
|
ir: { raw: '', folded: '', recovered: '' },
|
|
318
423
|
patternHits: 0,
|
|
319
424
|
diagnostics: [{ stage, reason: msg }],
|
|
425
|
+
// A stub spells no global, so it assumes nothing about one.
|
|
426
|
+
assumedSymbols: [],
|
|
320
427
|
};
|
|
321
428
|
}
|
|
322
429
|
|
|
323
430
|
/** Every ASMLIFT_ERROR marker in the emitted AST, as a structured diagnostic (one per marker,
|
|
324
431
|
* document order). The harness/self-improve loop reads THIS; the source text is for humans. */
|
|
325
432
|
function collectMarkers(sfn: SFn): Diagnostic[] {
|
|
326
|
-
// On the shared
|
|
327
|
-
//
|
|
328
|
-
//
|
|
433
|
+
// On the shared `walkExprs` traversal (l3/ast.ts). Order is exprs-then-nested-statements per
|
|
434
|
+
// statement — deterministic and near-document-order (a `for`'s cond is visited before its init;
|
|
435
|
+
// see the note on stmtChildren).
|
|
329
436
|
const out: Diagnostic[] = [];
|
|
330
|
-
const
|
|
437
|
+
for (const e of walkExprs(sfn.body)) {
|
|
331
438
|
if (e.k === 'marker') {
|
|
332
439
|
out.push({ stage: 'structure', reason: e.reason });
|
|
333
440
|
}
|
|
334
|
-
|
|
335
|
-
};
|
|
336
|
-
const ws = (s: Stmt): void => {
|
|
337
|
-
stmtExprs(s).forEach(we);
|
|
338
|
-
stmtChildren(s).forEach(ws);
|
|
339
|
-
};
|
|
340
|
-
sfn.body.forEach(ws);
|
|
441
|
+
}
|
|
341
442
|
return out;
|
|
342
443
|
}
|