@asmlift/core 0.3.0 → 0.4.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 +5 -3
- package/package.json +1 -1
- package/src/backend/cfamily.ts +125 -2
- package/src/backend/cpp.ts +3 -1
- package/src/backend/pascal.ts +11 -0
- package/src/contracts.ts +15 -2
- package/src/declare.ts +35 -9
- package/src/frontend/mips.ts +24 -23
- package/src/frontend/opaque.ts +39 -2
- package/src/frontend/ssa.ts +32 -53
- package/src/frontend/thumb.ts +301 -26
- package/src/ir/opcodes.ts +44 -0
- package/src/ir/simplify.ts +72 -0
- package/src/l3/argbase.ts +216 -0
- package/src/l3/ast.ts +118 -4
- package/src/l3/basecse.ts +3 -40
- package/src/l3/coalesce.ts +146 -0
- package/src/l3/dce.ts +2 -23
- package/src/l3/hoist.ts +65 -0
- package/src/l3/reindex.ts +7 -0
- package/src/l3/scopebase.ts +436 -0
- package/src/l3/tailmerge.ts +120 -0
- package/src/macros.ts +222 -13
- package/src/pattern/engine.ts +99 -6
- package/src/pipeline.ts +5 -2
- package/src/raise/divpow2.ts +226 -0
- package/src/raise/gvn.ts +141 -0
- package/src/raise/pre-recovery.ts +37 -3
- package/src/raise/recover.ts +24 -7
- package/src/raise/retsink.ts +36 -7
- package/src/raise/shortcircuit.ts +264 -22
- package/src/raise/structs.ts +12 -2
- package/src/rank.ts +172 -20
- package/src/structure/analysis.ts +42 -1
- package/src/structure/structure.ts +399 -31
- package/src/structure/switch-recover.ts +21 -3
- package/src/symbols.ts +128 -13
- package/src/target.ts +4 -2
- package/src/trace.ts +9 -0
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
// header also entered by a plain br).
|
|
39
39
|
import { Block, Fn, Op, Value, defOpMap, successorsOf } from '../ir/core';
|
|
40
40
|
import { type IrType, T, scalarTypeForAccess, typeEquals } from '../ir/types';
|
|
41
|
-
import { BinOp, Expr, SFn, Stmt, SwitchCase, exprChildren, mapExprChildren } from '../l3/ast';
|
|
41
|
+
import { BinOp, Expr, SFn, Stmt, SwitchCase, exprChildren, mapExprChildren, negateCond } from '../l3/ast';
|
|
42
42
|
import { exprCType, ptrElemBytes } from '../l3/typing';
|
|
43
43
|
import { returnType } from '../raise/recover';
|
|
44
44
|
import { collectStructs } from '../raise/structs';
|
|
@@ -46,9 +46,13 @@ import {
|
|
|
46
46
|
type DeclaredField,
|
|
47
47
|
type SymbolInfo,
|
|
48
48
|
type SymbolStructField,
|
|
49
|
+
arrayInnerExtents,
|
|
49
50
|
declaredFields,
|
|
50
51
|
isArrayField,
|
|
52
|
+
isBitfieldField,
|
|
53
|
+
isScalarCellSize,
|
|
51
54
|
pointeeFields,
|
|
55
|
+
scalarCellType,
|
|
52
56
|
} from '../symbols';
|
|
53
57
|
import { analyze } from './analysis';
|
|
54
58
|
import { makeLoopHazards, updateWriteSet } from './hazards';
|
|
@@ -108,6 +112,32 @@ function globalOf(e: Expr, width: number): { name: string; idx: Expr } | null {
|
|
|
108
112
|
return null;
|
|
109
113
|
}
|
|
110
114
|
|
|
115
|
+
// THE one gate on the BARE-NAME array-global spelling (`gSym[i]` rather than `((T *)&gSym)[i]`),
|
|
116
|
+
// shared by the constant-offset and variable-index access paths so the two cannot disagree.
|
|
117
|
+
// Returns the `index` node's `lead` fragment when the bare form is spellable, or null to fall
|
|
118
|
+
// through to the always-valid `&gSym` cast form.
|
|
119
|
+
//
|
|
120
|
+
// Two facts are required, not one. The element WIDTH must match, as it always has. And the RANK
|
|
121
|
+
// must be SPELLABLE, because one subscript reaches an element only on a rank-1 array: on `u16
|
|
122
|
+
// g[4][0x400]`, `g[i]` is a ROW. Against the project's own header that is usually a type error,
|
|
123
|
+
// but where the row address flows into an integer context it is merely a warning and the emitted C
|
|
124
|
+
// then addresses a different object than the asm did — silently.
|
|
125
|
+
//
|
|
126
|
+
// A rank > 1 pins the leading dimensions at 0 and puts the whole flat element index in the last
|
|
127
|
+
// subscript (`g[0][i]`) — the same address arithmetic, and the idiom decomp sources themselves use
|
|
128
|
+
// when the split is not observable in the asm either (`gBgTilemapBufs[0][…]` in kleod,
|
|
129
|
+
// `gNatureStatTable[nature][…]` in pokeemerald). A rank the map states but cannot spell (an unknown
|
|
130
|
+
// inner extent) gets no bare form at all; `((T *)&gSym)[i]` is byte-identical and valid under ANY
|
|
131
|
+
// declaration, which is why it is the safe fallback. See symbols.ts arrayInnerExtents for why an
|
|
132
|
+
// ABSENT rank is read as 1 rather than as unknown.
|
|
133
|
+
function bareArrayLead(si: SymbolInfo, width: number): { lead?: number[] } | null {
|
|
134
|
+
if (si.shape !== 'array' || si.elemSize !== width) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const inner = arrayInnerExtents(si);
|
|
138
|
+
return inner === null ? null : inner.length === 0 ? {} : { lead: new Array<number>(inner.length).fill(0) };
|
|
139
|
+
}
|
|
140
|
+
|
|
111
141
|
// A BYTE residual read as an ELEMENT index of `elemSize`-wide elements, or null when it is not one
|
|
112
142
|
// — the residual then addresses mid-element and no whole-element spelling can express it, so the
|
|
113
143
|
// caller falls through to the honest cast forms. THE one copy of the rule, indexing the
|
|
@@ -305,8 +335,10 @@ function pointeeAccess(
|
|
|
305
335
|
// Constant offset: the member must match EXACTLY — offset, read width, and the SPELLED type
|
|
306
336
|
// (spellsAccessType). An ARRAY member is excluded whatever its size: `u8 x[1]` would match a
|
|
307
337
|
// byte access by (offset, size) and spell `->x`, which is not an lvalue of that width at all.
|
|
338
|
+
// A BITFIELD member likewise: its `size` is the byte span its bits touch, so a 7-bit field
|
|
339
|
+
// would match a plain u16 read and spell a 7-bit lvalue for a 16-bit access.
|
|
308
340
|
const p = spellablePointee(pg.name, sym);
|
|
309
|
-
const f = p?.fields.find((m) => m.offset === total && m.size === width && !isArrayField(m));
|
|
341
|
+
const f = p?.fields.find((m) => m.offset === total && m.size === width && !isArrayField(m) && !isBitfieldField(m));
|
|
310
342
|
return p && f && spellsAccessType(f.signed, width, signed) && memberQualsAllow(f, p.const, isStore)
|
|
311
343
|
? { k: 'field', base: { k: 'var', name: pg.name }, name: f.name }
|
|
312
344
|
: null;
|
|
@@ -363,8 +395,12 @@ function memAccess(
|
|
|
363
395
|
// declaration on: a layout it declines whole is a layout with no nameable members, and a
|
|
364
396
|
// union alias it drops for the first view at that offset is a name no declaration carries.
|
|
365
397
|
// An ARRAY member is excluded for the same reason as in pointeeAccess: `u8 x[1]` would match
|
|
366
|
-
// a byte access by (offset, size) and spell `.x`, which is not an lvalue of that width.
|
|
367
|
-
|
|
398
|
+
// a byte access by (offset, size) and spell `.x`, which is not an lvalue of that width. A
|
|
399
|
+
// BITFIELD member likewise — a plain read of its bytes is not a read of its bits (the named
|
|
400
|
+
// bitfield spelling has its own recognizer, on the extract shape: see lowerDef).
|
|
401
|
+
const fld = declaredFields(si.layout)?.find(
|
|
402
|
+
(f) => f.offset === gb.byte && f.size === width && !isArrayField(f) && !isBitfieldField(f),
|
|
403
|
+
);
|
|
368
404
|
if (fld && memberQualsAllow(fld, si.const, isStore)) {
|
|
369
405
|
return { k: 'field', base: { k: 'var', name: gb.name }, name: fld.name, dot: true };
|
|
370
406
|
}
|
|
@@ -402,9 +438,10 @@ function memAccess(
|
|
|
402
438
|
// dogfood proved agbcc needs for ROM tables — with the element type registered in the env
|
|
403
439
|
// so the stride check passes and no cast is added. Element-width match only.
|
|
404
440
|
const siArr = sym?.info(g.name);
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
441
|
+
const lead = siArr === undefined ? null : bareArrayLead(siArr, width);
|
|
442
|
+
if (lead !== null) {
|
|
443
|
+
sym!.noteGlobal(g.name, T.ptr(T.int(width * 8, siArr!.elemSigned ?? false)));
|
|
444
|
+
return { k: 'index', base: { k: 'var', name: g.name }, idx, width, signed, ...lead };
|
|
408
445
|
}
|
|
409
446
|
return { k: 'index', base: { k: 'addr', name: g.name }, idx, width, signed };
|
|
410
447
|
}
|
|
@@ -447,9 +484,10 @@ function arrayAccess(
|
|
|
447
484
|
if (baseExpr.k === 'addr' && fieldOff === undefined) {
|
|
448
485
|
// ARRAY-declared global (symbol map): the bare-name spelling, same rule as memAccess.
|
|
449
486
|
const si = sym?.info(baseExpr.name);
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
487
|
+
const lead = si === undefined ? null : bareArrayLead(si, elemSize);
|
|
488
|
+
if (lead !== null) {
|
|
489
|
+
sym!.noteGlobal(baseExpr.name, T.ptr(T.int(elemSize * 8, si!.elemSigned ?? false)));
|
|
490
|
+
return { k: 'index', base: { k: 'var', name: baseExpr.name }, idx: idxExpr, width: elemSize, signed, ...lead };
|
|
453
491
|
}
|
|
454
492
|
return { k: 'index', base: baseExpr, idx: idxExpr, width: elemSize, signed };
|
|
455
493
|
}
|
|
@@ -520,12 +558,11 @@ const ARITH_TO_BIN: Record<string, BinOp> = {
|
|
|
520
558
|
and: '&',
|
|
521
559
|
xor: '^',
|
|
522
560
|
shl: '<<',
|
|
523
|
-
shr_u: '
|
|
561
|
+
shr_u: '>>>', // the LOGICAL right shift; the C backend spells it `>>` over an unsigned operand
|
|
524
562
|
shr_s: '>>',
|
|
525
563
|
logic_and: '&&',
|
|
526
564
|
logic_or: '||', // short-circuit connectives (raise/shortcircuit.ts)
|
|
527
565
|
};
|
|
528
|
-
const NEGATE: Record<string, BinOp> = { '<': '>=', '>=': '<', '>': '<=', '<=': '>', '==': '!=', '!=': '==' };
|
|
529
566
|
|
|
530
567
|
// Recovered info for a self-loop header: its exit block and the per-parameter back-edge
|
|
531
568
|
// arg it feeds (the value on the header→header edge). The back-edge arg is the "next"
|
|
@@ -577,6 +614,21 @@ export interface StructureOptions {
|
|
|
577
614
|
// body). GCC freely uses `!=`; IDO prefers `==`/`<`. A per-compiler DATA lever, not an `arch ==`
|
|
578
615
|
// branch — default true (permissive; the decline path keeps it sound either way).
|
|
579
616
|
switchAllowsNeqCase?: boolean;
|
|
617
|
+
// Anchor a constant merge copy at its const op's ORIGINAL position instead of at the CFG edge:
|
|
618
|
+
// `movs r9, #0` at entry ahead of a single-armed overwrite emits as a pre-initialization above
|
|
619
|
+
// the `if`, not as its else-arm. A differ-refereed candidate axis (rank.ts `/defsite`), never a
|
|
620
|
+
// default — see the refusal conditions where it is computed.
|
|
621
|
+
anchorConstCopies?: boolean;
|
|
622
|
+
// HARDWARE fact from TargetDescription.capabilities.endianness, threaded by structureOptionsFor:
|
|
623
|
+
// the bitfield extract recognizer solves an LSB-first equation, so it only runs on little-endian
|
|
624
|
+
// data. The provider already refuses to EMIT bitfield facts for a big-endian ELF; this is the
|
|
625
|
+
// same boundary enforced on core's side, against a hand-built map that never went through it.
|
|
626
|
+
littleEndian?: boolean;
|
|
627
|
+
// Spell `(x << a) >> b` extracts of a struct global as the map's named bitfield member. On by
|
|
628
|
+
// default; rank.ts enumerates the OFF spelling as the `/no-bitfield` axis, because the named
|
|
629
|
+
// read recompiles at the DECLARATION's access width — where that diverges from the asm's load
|
|
630
|
+
// width the honest shift spelling is the one that matches, and the differ referees.
|
|
631
|
+
spellBitfieldMembers?: boolean;
|
|
580
632
|
// How an unresolvable VALUE degrades (a live `opaque`, an unlowered transient op, a dropped def):
|
|
581
633
|
// "strict" (default) — the `"?"` sentinel, tripping assertResolved at the boundary (loud in
|
|
582
634
|
// the PROCESS);
|
|
@@ -597,6 +649,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
597
649
|
preserveDivergentBranchSense = true,
|
|
598
650
|
orderArgCopiesByComputation = true,
|
|
599
651
|
switchAllowsNeqCase = true,
|
|
652
|
+
anchorConstCopies = false,
|
|
653
|
+
littleEndian = true,
|
|
654
|
+
spellBitfieldMembers = true,
|
|
600
655
|
onGap = 'strict',
|
|
601
656
|
symbols,
|
|
602
657
|
} = opts;
|
|
@@ -606,7 +661,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
606
661
|
const dom = dominators(fn);
|
|
607
662
|
|
|
608
663
|
// ── analysis phase (structure/analysis.ts): use registry, liveness, materialization ──
|
|
609
|
-
const { useSitesOf, opIndex, opBlock, liveIn, materialize, reachFrom } = analyze(
|
|
664
|
+
const { useSitesOf, opIndex, opBlock, liveIn, materialize, reachFrom, emitPos, memWriteBetween } = analyze(
|
|
665
|
+
fn,
|
|
666
|
+
returnsVoid,
|
|
667
|
+
);
|
|
610
668
|
|
|
611
669
|
// SCALAR-vs-AGGREGATE globals: a `gaddr` symbol accessed EXCLUSIVELY at offset 0 is a scalar
|
|
612
670
|
// global → the bare name `gSym` (byte-exact, matches the source). A symbol accessed at any
|
|
@@ -895,6 +953,50 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
895
953
|
// The C static type of a rendered expression, over the declared variable types — what decides
|
|
896
954
|
// whether a memory access's base may be dereferenced as spelled (memAccess/arrayAccess).
|
|
897
955
|
const ctype = (e0: Expr): IrType | undefined => exprCType(e0, (n) => varType.get(n));
|
|
956
|
+
|
|
957
|
+
/** `&gSym` assigned to a `T *` local: the address of an AGGREGATE is not a pointer to its
|
|
958
|
+
* element. `&gArr` is `T (*)[n]`, `&gStruct` is `struct S *`, and neither is assignable to
|
|
959
|
+
* `T *` — yet the IR's `gaddr` value legitimately has type `T *`, because that is what the asm
|
|
960
|
+
* loaded. The bare spelling therefore states a type the project's own header contradicts.
|
|
961
|
+
*
|
|
962
|
+
* It survived because agbcc only WARNS ("assignment from incompatible pointer type") and
|
|
963
|
+
* computes the right address anyway. That leniency is not something to rely on: the Klonoa
|
|
964
|
+
* project's own build template treats these as fatal, so the row's emitted C does not build
|
|
965
|
+
* where its author would put it. The cast is the always-valid spelling — the same fallback
|
|
966
|
+
* `bareArrayLead` documents for the indexed form — and it is byte-identical (measured on
|
|
967
|
+
* kleod:UpdateHUDCounterDisplay: 81 with and without).
|
|
968
|
+
*
|
|
969
|
+
* The test is whether `&gSym`'s rendered type PROVABLY equals the destination's, not whether the
|
|
970
|
+
* symbol looks like an aggregate. A shape enumeration got this wrong three ways, each a real
|
|
971
|
+
* miss: `shape:'pointer'` declares a pointer cell (`void *gSym`, or `struct Tag *gSym` when the
|
|
972
|
+
* pointee has a declarable layout), so `&gSym` is a pointer-to-pointer either way; a `shape:'scalar'`
|
|
973
|
+
* whose width differs from the destination's pointee gives `s32 *` for a `u16 *` slot; and a
|
|
974
|
+
* NAME-ONLY symbol is synthesized as `extern u32 gSym;` (declare.ts), which is `u32 *` — not the
|
|
975
|
+
* `T *` the older comment here claimed. So the default is to CAST, and the cast is omitted only
|
|
976
|
+
* where the declared cell type is known and matches exactly. Byte-identical either way, so the
|
|
977
|
+
* cost of casting one time too many is a redundant `(T *)`, never a wrong address. */
|
|
978
|
+
const castAggregateAddr = (name: string, value: Expr): Expr => {
|
|
979
|
+
const t = varType.get(name);
|
|
980
|
+
if (t?.kind !== 'ptr' || value.k !== 'addr') {
|
|
981
|
+
return value;
|
|
982
|
+
}
|
|
983
|
+
// The only provably-redundant case: a NON-VOLATILE scalar cell whose DECLARED type is the
|
|
984
|
+
// destination's pointee, where `&gSym` already denotes exactly `T *`.
|
|
985
|
+
//
|
|
986
|
+
// `scalarCellType` and not `scalarTypeForAccess`: the latter answers what an ACCESS of that
|
|
987
|
+
// width reads and collapses every 4-byte access to `s32`, so it called a `u32` cell equal to an
|
|
988
|
+
// `s32 *` destination and let the incompatible assignment through. And a `volatile` cell makes
|
|
989
|
+
// `&gSym` a `volatile T *`, so omitting the cast would DISCARD the qualifier — the same class of
|
|
990
|
+
// fatal-under-a-strict-build defect this rule exists to remove.
|
|
991
|
+
const si = symCtx?.info(value.name);
|
|
992
|
+
if (si?.shape === 'scalar' && !si.volatile && isScalarCellSize(si.size)) {
|
|
993
|
+
if (typeEquals(scalarCellType(si.size, si.signed), t.to)) {
|
|
994
|
+
return value;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return { k: 'cast', to: t, e: value };
|
|
998
|
+
};
|
|
999
|
+
|
|
898
1000
|
let fresh = 0;
|
|
899
1001
|
// Materialized defs are named FIRST: the temp is the register the compiler held the
|
|
900
1002
|
// value in, so downstream coalescing (loop inits, merge params) may adopt it — subject to the
|
|
@@ -1101,6 +1203,256 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1101
1203
|
}
|
|
1102
1204
|
}
|
|
1103
1205
|
|
|
1206
|
+
// ── def-site anchoring of constant merge copies (anchorConstCopies) ──────────────────────────
|
|
1207
|
+
// An edge copy `v = K` places the constant where the EDGE is, but the asm often materialized K
|
|
1208
|
+
// earlier: `movs r9, #0` at entry ahead of a single-armed overwrite, `movs r5, #1` at the top
|
|
1209
|
+
// of an arm ahead of a nested if. Anchoring the copy at the const op's own program position
|
|
1210
|
+
// reproduces that placement — the write is emitted as a statement there (sideEffects reads
|
|
1211
|
+
// `anchoredAt`) and the edge copies it replaces are suppressed (argAssignsFor reads
|
|
1212
|
+
// `suppressedArgs`). Where the surviving arm then empties, mkIf's empty-then peephole yields
|
|
1213
|
+
// the single-armed positive `if` the source wrote.
|
|
1214
|
+
//
|
|
1215
|
+
// REFUSAL CONDITIONS — each keeps the edge placement, never producing a different write:
|
|
1216
|
+
// - the arg is not an UNNAMED `const` op (only a rematerializable constant carries
|
|
1217
|
+
// unambiguous placement evidence; a named value's position is its materialized def's);
|
|
1218
|
+
// - the merge is a loop header (loop copies have their own placement discipline);
|
|
1219
|
+
// - the const's block does not dominate every edge source passing it (the anchored write
|
|
1220
|
+
// must precede the edge on every path);
|
|
1221
|
+
// - the const's block or any edge source sits inside ANY loop. Block-level dominance does
|
|
1222
|
+
// not give per-ITERATION precedence — a path may pass the def in iteration 1 and take the
|
|
1223
|
+
// suppressed edge in iteration 2 with the variable overwritten in between, the /preinit
|
|
1224
|
+
// sticky-arm failure class (PR #13) — so in-loop shapes are declined outright;
|
|
1225
|
+
// - the merge variable names any OTHER SSA value (a shared name has readers and writers
|
|
1226
|
+
// between the def site and the edge that edge placement respects and anchoring would not);
|
|
1227
|
+
// - another anchored const of the same variable lies on a path from this one to this one's
|
|
1228
|
+
// edge (the later write would clobber this arg's value; both stay at their edges instead).
|
|
1229
|
+
const anchoredAt = new Map<Op, { name: string; arg: Value }[]>();
|
|
1230
|
+
const suppressedArgs = new Map<object, Set<number>>();
|
|
1231
|
+
if (anchorConstCopies) {
|
|
1232
|
+
const nameCount = new Map<string, number>();
|
|
1233
|
+
for (const n of varName.values()) {
|
|
1234
|
+
nameCount.set(n, (nameCount.get(n) ?? 0) + 1);
|
|
1235
|
+
}
|
|
1236
|
+
const inLoop = (b: Block): boolean => {
|
|
1237
|
+
for (const nl of forest.byHeader.values()) {
|
|
1238
|
+
if (nl.body.has(b)) {
|
|
1239
|
+
return true;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return false;
|
|
1243
|
+
};
|
|
1244
|
+
// conservative "a write in `a` may execute between one in `b` and `b`'s terminator": same
|
|
1245
|
+
// block counts (op order refined by the caller where it matters), else CFG reachability
|
|
1246
|
+
const mayFollow = (a: Block, b: Block): boolean => a === b || reachFrom(a).has(b);
|
|
1247
|
+
for (const M of fn.blocks) {
|
|
1248
|
+
if (M === entry || M.params.length === 0 || forest.byHeader.has(M)) {
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
M.params.forEach((p, i) => {
|
|
1252
|
+
const name = varName.get(p)!;
|
|
1253
|
+
if (nameCount.get(name) !== 1) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
// every in-edge record into M, grouped by the SSA value it passes for param i
|
|
1257
|
+
const groups = new Map<Value, { rec: { block: Block; args: Value[] }; src: Block }[]>();
|
|
1258
|
+
for (const pr of new Set(preds.get(M) ?? [])) {
|
|
1259
|
+
for (const s of pr.ops[pr.ops.length - 1].successors) {
|
|
1260
|
+
if (s.block === M) {
|
|
1261
|
+
const g = groups.get(s.args[i]);
|
|
1262
|
+
if (g) {
|
|
1263
|
+
g.push({ rec: s, src: pr });
|
|
1264
|
+
} else {
|
|
1265
|
+
groups.set(s.args[i], [{ rec: s, src: pr }]);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
const candidates: { arg: Value; def: Op; defBlock: Block; edges: { rec: object; src: Block }[] }[] = [];
|
|
1271
|
+
for (const [arg, edges] of groups) {
|
|
1272
|
+
const def = defs.get(arg);
|
|
1273
|
+
if (!def || def.opcode !== 'const' || varName.has(arg)) {
|
|
1274
|
+
continue;
|
|
1275
|
+
}
|
|
1276
|
+
const defBlock = opBlock.get(def)!;
|
|
1277
|
+
if (inLoop(defBlock) || edges.some(({ src }) => inLoop(src))) {
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1280
|
+
if (edges.some(({ src }) => !dom.get(src)!.has(defBlock))) {
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
candidates.push({ arg, def, defBlock, edges });
|
|
1284
|
+
}
|
|
1285
|
+
// pairwise clobber check: candidate `c` is unsafe when another candidate's write can lie
|
|
1286
|
+
// between c's def and one of c's edges (def_c → def_o → edge_c); both then keep their edges
|
|
1287
|
+
const safe = candidates.filter((c) =>
|
|
1288
|
+
candidates.every((o) => {
|
|
1289
|
+
if (o === c) {
|
|
1290
|
+
return true;
|
|
1291
|
+
}
|
|
1292
|
+
const oAfterC =
|
|
1293
|
+
c.defBlock === o.defBlock ? opIndex.get(o.def)! > opIndex.get(c.def)! : mayFollow(c.defBlock, o.defBlock);
|
|
1294
|
+
return !(oAfterC && c.edges.some(({ src }) => mayFollow(o.defBlock, src)));
|
|
1295
|
+
}),
|
|
1296
|
+
);
|
|
1297
|
+
for (const c of safe) {
|
|
1298
|
+
const at = anchoredAt.get(c.def);
|
|
1299
|
+
if (at) {
|
|
1300
|
+
at.push({ name, arg: c.arg });
|
|
1301
|
+
} else {
|
|
1302
|
+
anchoredAt.set(c.def, [{ name, arg: c.arg }]);
|
|
1303
|
+
}
|
|
1304
|
+
for (const { rec } of c.edges) {
|
|
1305
|
+
const sup = suppressedArgs.get(rec);
|
|
1306
|
+
if (sup) {
|
|
1307
|
+
sup.add(i);
|
|
1308
|
+
} else {
|
|
1309
|
+
suppressedArgs.set(rec, new Set([i]));
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// ── BITFIELD member reads (symbol map) ──────────────────────────────────────────────────────
|
|
1318
|
+
// The `(x << a) >> b` extract of a struct global's loaded bytes IS a bitfield access when the
|
|
1319
|
+
// map declares a bitfield at exactly those bits: spelled `gSym.field`, the source form, whose
|
|
1320
|
+
// declared `u32 field : n` then makes C's own integer promotion reproduce the signedness every
|
|
1321
|
+
// downstream operator compiled with (a 7-bit unsigned field promotes to signed int — sdiv
|
|
1322
|
+
// renders `/` and recompiles to __divsi3, where the raw-shift spelling stays u32).
|
|
1323
|
+
//
|
|
1324
|
+
// Semantically EXACT, never approximate: the window must lie inside the loaded bytes (so the
|
|
1325
|
+
// load's extension bits cannot reach it), the field's position, width and signedness must all
|
|
1326
|
+
// match the extract (a logical shift is an unsigned read, an arithmetic one a signed read —
|
|
1327
|
+
// a signless field never matches), and the member must be nameable at all (memberQualsAllow;
|
|
1328
|
+
// the map only carries bitfield facts for little-endian ELFs — see SymbolStructField). Any
|
|
1329
|
+
// mismatch keeps the honest shift spelling.
|
|
1330
|
+
//
|
|
1331
|
+
// Precomputed over the ops (not folded during rendering) for the load's sake: a load whose
|
|
1332
|
+
// EVERY use is a spelled extract chain must not also emit its materialized `v = *(u16 *)&g;`
|
|
1333
|
+
// temp — the compiler CSEs the repeated member reads back to one load, but the leftover temp
|
|
1334
|
+
// would be a second one. A VOLATILE container refuses the whole fold: N member reads are N
|
|
1335
|
+
// volatile accesses where the asm did one load. (Byte-level residual, differ-refereed: a load
|
|
1336
|
+
// only PARTIALLY absorbed — one extract spelled, another use kept — emits both the temp and
|
|
1337
|
+
// the named reads, one load more than the asm; semantics hold, the score decides.)
|
|
1338
|
+
//
|
|
1339
|
+
// ORDERING GATE (adversarial round, CRITICAL 1 — twice): the named spelling replaces a
|
|
1340
|
+
// REGISTER value — the bits captured at the load's program position — with a fresh memory
|
|
1341
|
+
// read at each render position. Every other memory read in this file goes through the
|
|
1342
|
+
// materialization model (analysis.ts) for exactly that hazard, so the fold clears the SAME
|
|
1343
|
+
// bar with the SAME machinery: `emitPos` resolves where each extract actually renders
|
|
1344
|
+
// (transitively through its inlining consumers — an unresolvable position refuses), and
|
|
1345
|
+
// `memWriteBetween` walks every def-avoiding load→render path for a call, an opaque, or a
|
|
1346
|
+
// store not provably to a DIFFERENT named global. Path-based on purpose: the second audit
|
|
1347
|
+
// pass broke the first fix's linear-position scan with a block laid out AFTER the render in
|
|
1348
|
+
// address order but executing between load and render on the taken path — fn.blocks order is
|
|
1349
|
+
// address order, not topological order.
|
|
1350
|
+
const bitfieldSpelling = new Map<Op, { global: string; field: string }>();
|
|
1351
|
+
const absorbedLoads = new Set<Op>();
|
|
1352
|
+
if (symCtx && littleEndian && spellBitfieldMembers) {
|
|
1353
|
+
// the (name, byte) of a load's address when it resolves through defs alone — `gaddr` or
|
|
1354
|
+
// `add(gaddr, const)`; anything else (a materialized base, a variable index) declines
|
|
1355
|
+
const loadTargets = new Map<Op, { name: string; byte: number }>();
|
|
1356
|
+
const addrOf = (v: Value, off: number): { name: string; byte: number } | null => {
|
|
1357
|
+
const d0 = defs.get(v);
|
|
1358
|
+
if (d0?.opcode === 'gaddr') {
|
|
1359
|
+
return { name: d0.attrs.sym as string, byte: off };
|
|
1360
|
+
}
|
|
1361
|
+
if (d0?.opcode === 'add' && d0.operands.length === 2) {
|
|
1362
|
+
for (const [x, y] of [
|
|
1363
|
+
[d0.operands[0], d0.operands[1]],
|
|
1364
|
+
[d0.operands[1], d0.operands[0]],
|
|
1365
|
+
] as const) {
|
|
1366
|
+
const g0 = defs.get(x);
|
|
1367
|
+
const c0 = defs.get(y);
|
|
1368
|
+
if (g0?.opcode === 'gaddr' && c0?.opcode === 'const') {
|
|
1369
|
+
return { name: g0.attrs.sym as string, byte: (c0.attrs.value as number) + off };
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
return null;
|
|
1374
|
+
};
|
|
1375
|
+
// A write for the fold's purposes: calls and opaques always; a store/astore unless its base
|
|
1376
|
+
// resolves to a global PROVABLY different from the folded one. (Name comparison suffices:
|
|
1377
|
+
// the pool promotion picks one canonical name per address, so one cell cannot appear under
|
|
1378
|
+
// two names within a function.)
|
|
1379
|
+
const mayWrite =
|
|
1380
|
+
(sym: string) =>
|
|
1381
|
+
(x: Op): boolean => {
|
|
1382
|
+
if (x.opcode === 'call' || x.opcode === 'opaque') {
|
|
1383
|
+
return true;
|
|
1384
|
+
}
|
|
1385
|
+
if (x.opcode !== 'store' && x.opcode !== 'astore') {
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1388
|
+
const t = addrOf(x.operands[0], 0);
|
|
1389
|
+
return !(t && t.name !== sym);
|
|
1390
|
+
};
|
|
1391
|
+
for (const blk of fn.blocks) {
|
|
1392
|
+
for (const op of blk.ops) {
|
|
1393
|
+
if ((op.opcode !== 'shr_u' && op.opcode !== 'shr_s') || op.operands.length !== 1) {
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
const b = op.attrs.imm as number | undefined;
|
|
1397
|
+
const inner = defs.get(op.operands[0]);
|
|
1398
|
+
if (typeof b !== 'number' || b <= 0 || b >= 32 || inner?.opcode !== 'shl' || inner.operands.length !== 1) {
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
const a = inner.attrs.imm as number | undefined;
|
|
1402
|
+
if (typeof a !== 'number' || a < 0 || b < a) {
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
const w = 32 - b; // extract width
|
|
1406
|
+
const lo = b - a; // low bit within the loaded value
|
|
1407
|
+
const load = defs.get(inner.operands[0]);
|
|
1408
|
+
if (load?.opcode !== 'load' || lo + w > (load.attrs.width as number) * 8) {
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
// a materialized shl would still emit its `v = x << a` temp reading the load — the fold
|
|
1412
|
+
// would then ADD member reads on top of it; rare, refuse
|
|
1413
|
+
if (materialize.has(inner)) {
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
const gb = addrOf(load.operands[0], load.attrs.off as number);
|
|
1417
|
+
const si = gb ? symCtx.info(gb.name) : undefined;
|
|
1418
|
+
if (!gb || si?.shape !== 'struct' || si.volatile) {
|
|
1419
|
+
continue;
|
|
1420
|
+
}
|
|
1421
|
+
// where does the member read RENDER? at the extract's own position when materialized,
|
|
1422
|
+
// else wherever each of its consumers ultimately renders (emitPos, transitively —
|
|
1423
|
+
// unresolvable refuses); every load→render path must be write-free
|
|
1424
|
+
const renders = materialize.has(op)
|
|
1425
|
+
? [{ blk: opBlock.get(op)!, idx: opIndex.get(op)! }]
|
|
1426
|
+
: [...new Set((useSitesOf.get(op.results[0]) ?? []).map((s) => s.op))].map((c) => emitPos(c));
|
|
1427
|
+
const writes = mayWrite(gb.name);
|
|
1428
|
+
if (renders.some((r) => r === null) || renders.some((r) => memWriteBetween(load, r!, writes))) {
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
const signedRead = op.opcode === 'shr_s';
|
|
1432
|
+
const fld = declaredFields(si.layout)?.find(
|
|
1433
|
+
(f) => f.bitWidth === w && f.offset * 8 + f.bitOffset! === gb.byte * 8 + lo && f.signed === signedRead,
|
|
1434
|
+
);
|
|
1435
|
+
if (fld && memberQualsAllow(fld, si.const, false)) {
|
|
1436
|
+
bitfieldSpelling.set(op, { global: gb.name, field: fld.name });
|
|
1437
|
+
loadTargets.set(load, gb);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
// a load is ABSORBED when every use is an shl whose every use is a spelled extract
|
|
1442
|
+
for (const load of loadTargets.keys()) {
|
|
1443
|
+
const shls = useSitesOf.get(load.results[0]) ?? [];
|
|
1444
|
+
const absorbed =
|
|
1445
|
+
shls.length > 0 &&
|
|
1446
|
+
shls.every(
|
|
1447
|
+
(u) =>
|
|
1448
|
+
u.op.opcode === 'shl' && (useSitesOf.get(u.op.results[0]) ?? []).every((v) => bitfieldSpelling.has(v.op)),
|
|
1449
|
+
);
|
|
1450
|
+
if (absorbed) {
|
|
1451
|
+
absorbedLoads.add(load);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1104
1456
|
// An unresolvable value: strict mode keeps the `"?"` sentinel AND records the reason — the
|
|
1105
1457
|
// decline thrown below names the actual gaps ("unmodelled instruction 'adde'"), the same
|
|
1106
1458
|
// reasons annotate mode's markers carry, instead of the anonymous `?` that assertResolved
|
|
@@ -1123,6 +1475,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1123
1475
|
if (d.opcode === 'const') {
|
|
1124
1476
|
return { k: 'const', value: d.attrs.value as number };
|
|
1125
1477
|
}
|
|
1478
|
+
// a bitfield extract recognized over the ops (see the precompute above): the member read,
|
|
1479
|
+
// not the shift pair
|
|
1480
|
+
const bf = bitfieldSpelling.get(d);
|
|
1481
|
+
if (bf) {
|
|
1482
|
+
return { k: 'field', base: { k: 'var', name: bf.global }, name: bf.field, dot: true };
|
|
1483
|
+
}
|
|
1126
1484
|
if (CMP_TO_BIN[d.opcode]) {
|
|
1127
1485
|
// A bare global address `&gSym` as a COMPARISON operand is the same unspelled escape as the
|
|
1128
1486
|
// arithmetic case below (see intifyAddr): its C type comes from the PROJECT's own
|
|
@@ -1231,6 +1589,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1231
1589
|
l = isPtrGlobal(l) ? intifyPtrGlobal(l) : l;
|
|
1232
1590
|
r = isPtrGlobal(r) ? intifyPtrGlobal(r) : r;
|
|
1233
1591
|
}
|
|
1592
|
+
// (The two right shifts stay DISTINCT ops here — `>>>` logical, `>>` arithmetic. Which token
|
|
1593
|
+
// a language spells each with, and what cast pins the choice, is a BACKEND decision; see
|
|
1594
|
+
// l3/ast.ts BinOp and backend/cfamily.ts's shift rule.)
|
|
1234
1595
|
// SCOPE: this and intifyAddr cover the ARITHMETIC escapes. A pointer global under a
|
|
1235
1596
|
// COMPARISON (`gPtr < K` — C compares unsigned whatever the asm's icmp_s* said) is the same
|
|
1236
1597
|
// class as intifyAddrCmp's `addr` rule and is deliberately left alone here: it is valid C
|
|
@@ -1242,8 +1603,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1242
1603
|
// The C rotate idiom — `x >> n | x << (32 - n)` (mirrored for rotl). Byte-exact round-trip
|
|
1243
1604
|
// on agbcc (thumb ror) and mwcc (rotlw/rotlwi), verified against both toolchains before the
|
|
1244
1605
|
// ops landed. `x` and `n` render twice — both pure by construction (SSA values; the rotate's
|
|
1245
|
-
// operands are register reads)
|
|
1246
|
-
//
|
|
1606
|
+
// operands are register reads). The right half is the LOGICAL shift `>>>` — the idiom is
|
|
1607
|
+
// wrong with an arithmetic one — stated on the node rather than left to the rotated value's
|
|
1608
|
+
// recovered unsignedness, which is a property of recovery rather than of the idiom.
|
|
1247
1609
|
//
|
|
1248
1610
|
// (The PPC mirror fold — `rotl(x, 32 - m)` ⇒ rotr(x, m) — lives in the PATTERN layer,
|
|
1249
1611
|
// engine.ts ROTL_MIRROR: it is a compiler-spelling idiom, mwcc-gated there, not a
|
|
@@ -1260,7 +1622,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1260
1622
|
n.k === 'const'
|
|
1261
1623
|
? { k: 'const', value: 32 - n.value }
|
|
1262
1624
|
: { k: 'bin', op: '-', l: { k: 'const', value: 32 }, r: n };
|
|
1263
|
-
const [near, far] = dir === 'rotr' ? (['
|
|
1625
|
+
const [near, far] = dir === 'rotr' ? (['>>>', '<<'] as const) : (['<<', '>>>'] as const);
|
|
1264
1626
|
return {
|
|
1265
1627
|
k: 'bin',
|
|
1266
1628
|
op: '|',
|
|
@@ -1382,13 +1744,17 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1382
1744
|
const target = succ.block;
|
|
1383
1745
|
const argExpr = sub ? exprWith(sub) : expr;
|
|
1384
1746
|
const copies: { name: string; value: Expr; arg: Value }[] = [];
|
|
1747
|
+
const suppressed = suppressedArgs.get(succ);
|
|
1385
1748
|
target.params.forEach((p, i) => {
|
|
1749
|
+
if (suppressed?.has(i)) {
|
|
1750
|
+
return;
|
|
1751
|
+
} // anchored at its const's def site — the write already ran before this edge
|
|
1386
1752
|
const name = varName.get(p)!;
|
|
1387
1753
|
const arg = succ.args[i];
|
|
1388
1754
|
if ((sub?.get(arg) ?? varName.get(arg)) === name) {
|
|
1389
1755
|
return;
|
|
1390
1756
|
} // identity copy — coalesced away
|
|
1391
|
-
copies.push({ name, value: argExpr(arg), arg });
|
|
1757
|
+
copies.push({ name, value: castAggregateAddr(name, argExpr(arg)), arg });
|
|
1392
1758
|
});
|
|
1393
1759
|
// Emit in the order the args are COMPUTED in `pred` — a compiler that lays the defining ops
|
|
1394
1760
|
// (and thus the copies that read them) out in that order matches with no spurious arg-swap.
|
|
@@ -1463,8 +1829,15 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1463
1829
|
});
|
|
1464
1830
|
} else if (op.opcode === 'call' && op.results.length && !useSitesOf.has(op.results[0])) {
|
|
1465
1831
|
out.push({ k: 'exprstmt', value: expr(op.results[0]) });
|
|
1466
|
-
} else if (materialize.has(op)) {
|
|
1467
|
-
|
|
1832
|
+
} else if (materialize.has(op) && !absorbedLoads.has(op)) {
|
|
1833
|
+
// (an absorbed load's every consumer spells a named bitfield read — emitting its temp
|
|
1834
|
+
// here would recompile to a second load the asm does not have)
|
|
1835
|
+
const nm = varName.get(op.results[0])!;
|
|
1836
|
+
out.push({ k: 'assign', name: nm, value: castAggregateAddr(nm, lowerDef(op, expr)) });
|
|
1837
|
+
}
|
|
1838
|
+
// a merge copy anchored at this const's original position (anchorConstCopies, above)
|
|
1839
|
+
for (const a of anchoredAt.get(op) ?? []) {
|
|
1840
|
+
out.push({ k: 'assign', name: a.name, value: expr(a.arg) });
|
|
1468
1841
|
}
|
|
1469
1842
|
}
|
|
1470
1843
|
return out;
|
|
@@ -1524,6 +1897,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1524
1897
|
isNamed: (v) => varName.has(v),
|
|
1525
1898
|
isCmpOpcode: (opcode) => !!CMP_TO_BIN[opcode],
|
|
1526
1899
|
switchAllowsNeqCase,
|
|
1900
|
+
emitsAnchoredWrite: (blk) => blk.ops.some((o) => anchoredAt.has(o)),
|
|
1527
1901
|
expr: (v) => expr(v),
|
|
1528
1902
|
structureRegion: (b, stop) => structureRegion(b, stop),
|
|
1529
1903
|
});
|
|
@@ -1725,7 +2099,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1725
2099
|
out.push(...updateCopies); // the loop update, RAW (i++, p>>=1, …)
|
|
1726
2100
|
let leaveCond = exprWith(sub)(term.operands[0]);
|
|
1727
2101
|
if (contIsTaken) {
|
|
1728
|
-
leaveCond =
|
|
2102
|
+
leaveCond = negateCond(leaveCond);
|
|
1729
2103
|
} // continue is `taken` → leave when NOT it
|
|
1730
2104
|
const exitArm = isBreak
|
|
1731
2105
|
? [...argAssigns(b, loopCtx.exit, sub), { k: 'break' } as Stmt] // break to the loop exit
|
|
@@ -1759,7 +2133,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1759
2133
|
// IDO/MIPS; agbcc/GCC canonicalise either way, so it is safe there too. A compiler that
|
|
1760
2134
|
// inverts branch canonicalization sets preserveDivergentBranchSense false and falls through
|
|
1761
2135
|
// to the positive form below.
|
|
1762
|
-
out.push({ k: 'if', cond:
|
|
2136
|
+
out.push({ k: 'if', cond: negateCond(cond), then: elseS, else: thenS });
|
|
1763
2137
|
return out;
|
|
1764
2138
|
}
|
|
1765
2139
|
out.push(mkIf(cond, thenS, elseS));
|
|
@@ -1788,7 +2162,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1788
2162
|
const term = li.header.ops[li.header.ops.length - 1];
|
|
1789
2163
|
let cond = exprWith(loopSub(li))(term.operands[0]);
|
|
1790
2164
|
if (term.successors[0].block !== li.header) {
|
|
1791
|
-
cond =
|
|
2165
|
+
cond = negateCond(cond);
|
|
1792
2166
|
} // loop-continue must be `taken`
|
|
1793
2167
|
const body = [...sideEffects(li.header), ...(updates ?? argAssigns(li.header, li.header))];
|
|
1794
2168
|
return { k: 'while', cond, body };
|
|
@@ -1802,7 +2176,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1802
2176
|
const term = wl.header.ops[wl.header.ops.length - 1];
|
|
1803
2177
|
let cond = expr(term.operands[0]);
|
|
1804
2178
|
if (term.successors[1].block === wl.bodyEntry) {
|
|
1805
|
-
cond =
|
|
2179
|
+
cond = negateCond(cond);
|
|
1806
2180
|
}
|
|
1807
2181
|
// The header→bodyEntry edge may carry non-identity phi args (a value the header COMPUTED and passes
|
|
1808
2182
|
// into the body). Those copies must open the body — dropping them reads an uninitialised local.
|
|
@@ -1864,7 +2238,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1864
2238
|
const body = [...inner, ...sideEffects(dw.latch), ...updates];
|
|
1865
2239
|
let cond = exprWith(sub)(lterm.operands[0]);
|
|
1866
2240
|
if (lterm.successors[1].block === dw.header) {
|
|
1867
|
-
cond =
|
|
2241
|
+
cond = negateCond(cond);
|
|
1868
2242
|
} // continue edge must be `taken`
|
|
1869
2243
|
const out: Stmt[] = [{ k: 'dowhile', cond, body }];
|
|
1870
2244
|
// The exit region reads latch back-edge values under `sub` (post-loop they live in the loop vars).
|
|
@@ -2047,16 +2421,10 @@ function substVar(e: Expr, from: string, to: string): Expr {
|
|
|
2047
2421
|
// empty-then peephole: `if (c) {} else { S }` → `if (!c) { S }`
|
|
2048
2422
|
function mkIf(cond: Expr, thenS: Stmt[], elseS: Stmt[]): Stmt {
|
|
2049
2423
|
if (thenS.length === 0 && elseS.length > 0) {
|
|
2050
|
-
return { k: 'if', cond:
|
|
2424
|
+
return { k: 'if', cond: negateCond(cond), then: elseS, else: [] };
|
|
2051
2425
|
}
|
|
2052
2426
|
return { k: 'if', cond, then: thenS, else: elseS };
|
|
2053
2427
|
}
|
|
2054
|
-
function negate(e: Expr): Expr {
|
|
2055
|
-
if (e.k === 'bin' && NEGATE[e.op]) {
|
|
2056
|
-
return { ...e, op: NEGATE[e.op] };
|
|
2057
|
-
}
|
|
2058
|
-
return { k: 'un', op: '!', e };
|
|
2059
|
-
}
|
|
2060
2428
|
|
|
2061
2429
|
// --- CFG utilities ---
|
|
2062
2430
|
function predecessorBlocks(fn: Fn): Map<Block, Block[]> {
|