@asmlift/core 0.7.0 → 0.8.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 +48 -24
- package/package.json +1 -1
- package/src/backend/pascal.ts +2 -2
- package/src/codegen-flags.ts +640 -0
- package/src/frontend/disasm.ts +141 -11
- package/src/frontend/high-half.ts +149 -0
- package/src/frontend/mips.ts +458 -209
- package/src/frontend/ppc.ts +332 -67
- package/src/frontend/reloc-symbol.ts +109 -0
- package/src/frontend/splat.ts +56 -18
- package/src/frontend/ssa.ts +126 -29
- package/src/frontend/stackargs.ts +420 -0
- package/src/frontend/thumb.ts +207 -230
- package/src/ir/core.ts +62 -3
- package/src/ir/opcodes.ts +9 -0
- package/src/ir/parse.ts +7 -1
- package/src/l3/advance.ts +2 -2
- package/src/l3/argbase.ts +2 -2
- package/src/l3/argcopy.ts +269 -0
- package/src/l3/ast.ts +45 -1
- package/src/l3/basecse.ts +2 -2
- package/src/l3/coalesce.ts +109 -52
- package/src/l3/scopebase.ts +4 -4
- package/src/l3/tailret.ts +70 -0
- package/src/l3/unmerge.ts +2 -2
- package/src/l3/unreduce.ts +2 -1
- package/src/mangle.ts +49 -0
- package/src/pattern/engine.ts +128 -13
- package/src/pipeline.ts +22 -11
- package/src/raise/extscale.ts +5 -2
- package/src/raise/paramwidth.ts +111 -3
- package/src/raise/pre-recovery.ts +11 -1
- package/src/raise/retsink.ts +8 -4
- package/src/raise/tailsink.ts +17 -2
- package/src/rank-declare.ts +17 -9
- package/src/rank.ts +45 -19
- package/src/structure/retspell.ts +95 -0
- package/src/structure/structure.ts +12 -3
- package/src/structure/switch-recover.ts +1 -1
- package/src/target.ts +224 -14
- package/src/trace.ts +27 -18
- package/src/variation-definitions.ts +52 -2
- package/src/variation-gates.ts +3 -0
- package/src/variation-tokens.ts +1 -0
package/src/frontend/thumb.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { Block, Fn, Op, Successor, Value, mkOp, mkValue } from '../ir/core';
|
|
22
22
|
import type { Opcode } from '../ir/opcodes';
|
|
23
23
|
import { T } from '../ir/types';
|
|
24
|
-
import { type Prototypes, protoArity } from '../proto';
|
|
24
|
+
import { type FnProto, type Prototypes, declaredWidth, protoArity } from '../proto';
|
|
25
25
|
import { RUNTIME_HELPERS } from '../raise/softdiv';
|
|
26
26
|
import { type SymbolMap, lookupInterior, lookupSymbol } from '../symbols';
|
|
27
27
|
import type { TargetDescription } from '../target';
|
|
@@ -32,6 +32,7 @@ import { assertInputFormat } from './format';
|
|
|
32
32
|
import type { Frontend } from './frontend';
|
|
33
33
|
import { opaqueDest } from './opaque';
|
|
34
34
|
import { abiSortEntryParams, fallbackArgc, makeSsaBuilder, slotKeyOffset, stackSlotKey } from './ssa';
|
|
35
|
+
import { type OutgoingArgs, type StackArgsEvent, analyzeOutgoingArgs } from './stackargs';
|
|
35
36
|
|
|
36
37
|
interface Instr {
|
|
37
38
|
/** the CANONICAL spelling — legacy names are normalised (see LEGACY_MNEMONICS) so that every
|
|
@@ -755,6 +756,28 @@ interface Resolved {
|
|
|
755
756
|
witness: LayoutFact[];
|
|
756
757
|
}
|
|
757
758
|
|
|
759
|
+
/** A GNU-as listing without its debug and stabs sections: every line from a `.section .debug_*` (or a
|
|
760
|
+
* `.stab*` section) up to the next section switch is dropped, and every other line is kept verbatim,
|
|
761
|
+
* so a listing with no debug section comes back unchanged. What agbcc `-g` adds to a `.s` beside
|
|
762
|
+
* the marker labels in its code (`corpus/agbcc-debug{,-g}.s`). */
|
|
763
|
+
export function withoutDebugSections(asm: string): string {
|
|
764
|
+
let inDebugSection = false;
|
|
765
|
+
const kept: string[] = [];
|
|
766
|
+
for (const rawLine of asm.split('\n')) {
|
|
767
|
+
const sectionSwitch = rawLine
|
|
768
|
+
.split('@')[0]
|
|
769
|
+
.trim()
|
|
770
|
+
.match(/^\.(?:section\s+([^\s,]+)|text\b|data\b|bss\b)/);
|
|
771
|
+
if (sectionSwitch) {
|
|
772
|
+
inDebugSection = sectionSwitch[1] !== undefined && /^\.(debug|stab)/.test(sectionSwitch[1]);
|
|
773
|
+
}
|
|
774
|
+
if (!inDebugSection) {
|
|
775
|
+
kept.push(rawLine);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return kept.join('\n');
|
|
779
|
+
}
|
|
780
|
+
|
|
758
781
|
/** The canonical serialisation a witness is compared BY, and the prose it is reported AS. Keeping
|
|
759
782
|
* them apart is the point: rewording `sayFact` changes a message, never a decision. */
|
|
760
783
|
const factKey = (f: LayoutFact) => JSON.stringify(f);
|
|
@@ -806,13 +829,36 @@ function decode(
|
|
|
806
829
|
let dataLabel: string | null = null;
|
|
807
830
|
let pendingFn = false;
|
|
808
831
|
let pendingArm = false;
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
832
|
+
// DEBUG OUTPUT IS NEITHER CODE NOR DATA THE CODE READS. agbcc `-g` leaves `.text` byte-identical
|
|
833
|
+
// (`corpus/agbcc-debug{,-g}.s`), so the lift reads exactly the function it reads without `-g`, and
|
|
834
|
+
// two things are dropped:
|
|
835
|
+
// • every debug or stabs section (`withoutDebugSections`): the debug rows, their labels, and the
|
|
836
|
+
// data a trailing `.Letext0` label would otherwise head;
|
|
837
|
+
// • the marker labels `-g` plants in the code stream (`.LFB1`, `.LM3`, `.LBB2`, `.LBE2`, `.LFE1`,
|
|
838
|
+
// `.Letext0`) that no code line names. A label starts a block, and a block split at a
|
|
839
|
+
// lexical-scope marker restructures the loop around it: kept, it lifts the pair's `gcd` loop as
|
|
840
|
+
// an `if` around a `do` instead of a `while`.
|
|
841
|
+
const codeLines: string[] = [];
|
|
842
|
+
for (const rawLine of withoutDebugSections(asm).split('\n')) {
|
|
843
|
+
const line = rawLine.split('@')[0].trim();
|
|
844
|
+
if (line) {
|
|
845
|
+
codeLines.push(line);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
const DEBUG_MARKER = /(?<![\w.$])\.L(?:FB|FE|M|BB|BE|etext)\d+\b/g;
|
|
849
|
+
const namedByCode = new Set<string>();
|
|
850
|
+
for (const line of codeLines) {
|
|
851
|
+
for (const m of line.replace(/^[A-Za-z_.$][\w.$]*:\s*/, '').matchAll(DEBUG_MARKER)) {
|
|
852
|
+
namedByCode.add(m[0]);
|
|
813
853
|
}
|
|
854
|
+
}
|
|
855
|
+
const isDebugMarker = (label: string) => /^\.L(?:FB|FE|M|BB|BE|etext)\d+$/.test(label) && !namedByCode.has(label);
|
|
856
|
+
for (let rest of codeLines) {
|
|
814
857
|
// A label may share the line with what follows it (pret pools: `_08x: .4byte 0x…`) — peel it.
|
|
815
858
|
const lm = rest.match(/^([A-Za-z_.$][\w.$]*):\s*(.*)$/);
|
|
859
|
+
if (lm && isDebugMarker(lm[1]) && lm[2] === '') {
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
816
862
|
if (lm) {
|
|
817
863
|
const lab = lm[1];
|
|
818
864
|
if (pendingFn || pendingArm) {
|
|
@@ -2808,16 +2854,16 @@ export function lift(
|
|
|
2808
2854
|
// local the compiler was entitled to put in place with no prologue at all.
|
|
2809
2855
|
const ssa = makeSsaBuilder(name, asmBlocks.length, preds, () => ({
|
|
2810
2856
|
ownedLocals: { from: 0, to: localArea },
|
|
2811
|
-
// THE SAME RANGE, AND NOT THE SAME CLAIM. `ownedLocals` answers "is a def-less read here
|
|
2812
|
-
// uninitialised local?"
|
|
2813
|
-
// `
|
|
2814
|
-
//
|
|
2815
|
-
//
|
|
2816
|
-
//
|
|
2817
|
-
//
|
|
2818
|
-
//
|
|
2819
|
-
//
|
|
2820
|
-
declaredLocals: { from:
|
|
2857
|
+
// NOT THE SAME RANGE, AND NOT THE SAME CLAIM. `ownedLocals` answers "is a def-less read here
|
|
2858
|
+
// an uninitialised local?" — and the outgoing stack-argument area IS owned, so it starts at 0.
|
|
2859
|
+
// `declaredLocals` answers "is a spill here a DECLARATION RANK?", which `ir/core.ts`
|
|
2860
|
+
// `SlotHomes` and `l3/slotorder.ts` consume, and an argument slot's offset is an ABI POSITION,
|
|
2861
|
+
// not an `expand_decl` rank. Under agbcc's ACCUMULATE_OUTGOING_ARGS that area sits at the
|
|
2862
|
+
// BOTTOM of `localArea`, so the declared range starts where the largest licensed block ends
|
|
2863
|
+
// (`frontend/stackargs.ts`). A refusal there licenses nothing and reports `area` 0, so the two
|
|
2864
|
+
// ranges coincide exactly when no argument word was proved, and the narrowing can only ever
|
|
2865
|
+
// skip offsets a callee's declaration and this function's own stores agreed on.
|
|
2866
|
+
declaredLocals: { from: outgoingArgs.area, to: localArea },
|
|
2821
2867
|
...(target.nonArgRegs
|
|
2822
2868
|
? {
|
|
2823
2869
|
uninitRegs: target.nonArgRegs.filter((r) => scratchRegs.has(r) || savedRegs.has(r)),
|
|
@@ -2982,7 +3028,7 @@ export function lift(
|
|
|
2982
3028
|
// which the sp declines append, so a refused function names the capability actually missing
|
|
2983
3029
|
// instead of the generic "local stack frames". The gap histogram is the improvement loop's
|
|
2984
3030
|
// work-list; a misattributed refusal sends that loop to build the wrong thing.
|
|
2985
|
-
const slotModelBlocker = (): string | null => {
|
|
3031
|
+
const slotModelBlocker = (outgoing: OutgoingArgs<Instr>): string | null => {
|
|
2986
3032
|
for (const ab of asmBlocks) {
|
|
2987
3033
|
for (const ins of ab.instrs) {
|
|
2988
3034
|
const acc = spMemAccess(ins);
|
|
@@ -3042,213 +3088,12 @@ export function lift(
|
|
|
3042
3088
|
}
|
|
3043
3089
|
}
|
|
3044
3090
|
}
|
|
3045
|
-
// OUTGOING ARGUMENTS
|
|
3046
|
-
//
|
|
3047
|
-
//
|
|
3048
|
-
//
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
// does not mean "private": the outgoing area belongs to the callee, which may even assign to a
|
|
3052
|
-
// stack parameter.
|
|
3053
|
-
//
|
|
3054
|
-
// How big is that area? A declared arity bounds it from BELOW — `4 * max(0, arity - 4)` — and
|
|
3055
|
-
// that is all the facts available here can support. It is used in exactly one direction: to
|
|
3056
|
-
// REFUSE. A callee declared with five parameters proves this frame has an outgoing area, and
|
|
3057
|
-
// consuming those stores as call operands is the dual capability, unbuilt, so the model
|
|
3058
|
-
// declines. A callee declared with four proves NOTHING, because a declaration is a lower bound
|
|
3059
|
-
// on the words a call actually pushes:
|
|
3060
|
-
//
|
|
3061
|
-
// * a parameter may occupy more than one word (`double`, `long long`, a struct by value),
|
|
3062
|
-
// * a variadic callee's list is a prefix — `sprintf` truthfully declares two and is handed six,
|
|
3063
|
-
// * a large struct return adds a hidden pointer argument that appears in no parameter list.
|
|
3064
|
-
//
|
|
3065
|
-
// None of those is recorded by `FnProto` or `SymbolSignature`, so no arity here can license an
|
|
3066
|
-
// ACCEPTANCE. An earlier cut treated `arity <= 4` as proof of an empty area and had all three
|
|
3067
|
-
// holes: supplying a TRUE fact (`{ sprintf: { params: 2 } }`) turned a correct decline into
|
|
3068
|
-
// `return sprintf(a0, a1)` with both stack arguments deleted, where supplying nothing declined.
|
|
3069
|
-
// A fact must only ever move a function toward refusal — never toward an acceptance the facts
|
|
3070
|
-
// do not entail. Refusing on a lower bound is monotone in exactly that way: a true arity larger
|
|
3071
|
-
// than declared can only make the area bigger, and the answer is already "decline".
|
|
3072
|
-
//
|
|
3073
|
-
// Measured, this costs nothing it was buying: forcing the old acceptance path off changed 0
|
|
3074
|
-
// lift/decline verdicts across 2686 corpus functions (sa3's vendored map carries no signatures
|
|
3075
|
-
// at all), so the path that carried those holes was never load-bearing. That sweep was run over
|
|
3076
|
-
// the sa3/klonoa CHECKOUTS, which this repo does not vendor — it is not the benchmark's 404
|
|
3077
|
-
// agbcc rows, and it has not been re-run since it was taken.
|
|
3078
|
-
for (const ab of asmBlocks) {
|
|
3079
|
-
for (const ins of ab.instrs) {
|
|
3080
|
-
if (ins.mnemonic !== 'bl' && ins.mnemonic !== 'blx') {
|
|
3081
|
-
continue;
|
|
3082
|
-
}
|
|
3083
|
-
const c = ins.ops[0] ?? '';
|
|
3084
|
-
const arity = protoArity(prototypes[c]) ?? protoArity(RUNTIME_HELPERS[c]);
|
|
3085
|
-
if (arity !== undefined && arity > target.argRegs.length) {
|
|
3086
|
-
return `callee \`${c}\` is declared with ${arity} arguments, so this frame has an outgoing stack-argument area — consuming stack call arguments is not implemented`;
|
|
3087
|
-
}
|
|
3088
|
-
}
|
|
3089
|
-
}
|
|
3090
|
-
// Nothing above could prove the area empty, so fall back to reading the CODE. Two conditions,
|
|
3091
|
-
// covering different escapes:
|
|
3092
|
-
// (a) every slot store must be reloaded somewhere reachable. An outgoing argument is read by
|
|
3093
|
-
// the CALLEE, never by the caller, so a store never read back is the signature of one.
|
|
3094
|
-
// (b) no slot store may reach a `bl` unread ALONG A PATH.
|
|
3095
|
-
//
|
|
3096
|
-
// Neither is sound alone and the pair is not either, so keep two things straight. (a)'s real
|
|
3097
|
-
// theorem is not "the callee reads it, the caller does not" — it is that agbcc's
|
|
3098
|
-
// ACCUMULATE_OUTGOING_ARGS puts the outgoing area at the BOTTOM of localArea, disjoint from the
|
|
3099
|
-
// locals, so no local load can land on an argument offset. That disjointness is what a
|
|
3100
|
-
// tail-merged call site breaks, and agbcc DOES tail-merge: `Task_BonusFlower_Spawn` (sa3
|
|
3101
|
-
// bonus_game_enemies) stores argument 5 in both predecessors with the `bl` in the join.
|
|
3102
|
-
//
|
|
3103
|
-
// A SECOND THING NOW DEPENDS ON THIS DECLINE, and it is not in this file. `SlotHomes`
|
|
3104
|
-
// (ir/core.ts) reads a `[sp,#k]` spill as a DECLARATION RANK and `l3/slotorder.ts` orders the
|
|
3105
|
-
// declaration list by it. The partition that admits an offset is `LiveInModel.declaredLocals`,
|
|
3106
|
-
// which this frontend sets to `[0, localArea)` — a range that CONTAINS the outgoing area. The
|
|
3107
|
-
// only thing keeping an argument slot from becoming a declaration rank is that this decline
|
|
3108
|
-
// removes such functions from the population first. So lifting or narrowing this guard is not
|
|
3109
|
-
// a local change: it must come with a narrowed `declaredLocals`, or the ordering starts
|
|
3110
|
-
// ranking argument positions with no diagnostic. The class is populated — of 2,001 lifted real
|
|
3111
|
-
// agbcc functions, 12 carry an L1 slot home AND call, and 11 of those home offset 0. "Real
|
|
3112
|
-
// agbcc functions" there means a CHECKOUT sweep this repo does not vendor, not benchmark rows
|
|
3113
|
-
// (the benchmark has 126 real agbcc rows in total), and it has not been re-run since.
|
|
3114
|
-
//
|
|
3115
|
-
// (b) is a forward may-analysis over the CFG, and it has been wrong twice in the other
|
|
3116
|
-
// direction. Scanning per block let a LABEL decide accept versus refuse; scanning the flat
|
|
3117
|
-
// listing let BLOCK ORDER decide, because a load in one arm of a branch cleared a store that
|
|
3118
|
-
// reaches the call through the other arm — swap the arms in the listing, same CFG and same
|
|
3119
|
-
// semantics, and the verdict flipped. Only the path-sensitive form is stable under layout.
|
|
3120
|
-
//
|
|
3121
|
-
// Only for a function that CALLS. With no call there is no outgoing area to mistake a local
|
|
3122
|
-
// for, and a never-reloaded store there is an ordinary dead local — which PR #30 modelled and
|
|
3123
|
-
// which must keep working.
|
|
3124
|
-
//
|
|
3125
|
-
// …and not when the WHOLE FRAME is an object whose address a callee holds
|
|
3126
|
-
// (`capturedObjectIsTheWholeFrame` — a one-word frame, passed by a bare `mov rD, sp`). Both
|
|
3127
|
-
// conditions hunt for an argument block; every argument block starts at [sp,#0] (that is what
|
|
3128
|
-
// `prefixStored` encodes); and a one-word frame that is entirely an addressable local has no
|
|
3129
|
-
// room for one. So there is no argument block to find and both conditions can only fire as
|
|
3130
|
-
// FALSE ALARMS — which is what they did: the three address-taken rows in the synthetic tier
|
|
3131
|
-
// declined at (a) on a store that is never reloaded for the ordinary reason, that the CALLEE
|
|
3132
|
-
// reads it through the pointer.
|
|
3133
|
-
//
|
|
3134
|
-
// This is the only acceptance in this function, so keep straight what licenses it. NOT arity: a
|
|
3135
|
-
// callee declared with four arguments proves nothing (see above), and the layout gate is
|
|
3136
|
-
// deliberately independent of the declarations. The arity refusal still runs FIRST and still
|
|
3137
|
-
// wins — in a one-word frame a declared fifth argument and an addressable local at offset 0 are
|
|
3138
|
-
// contradictory claims about the same word, so the honest answer there is the decline, not a
|
|
3139
|
-
// guess about which one to believe.
|
|
3140
|
-
if (
|
|
3141
|
-
!capturedObjectIsTheWholeFrame &&
|
|
3142
|
-
asmBlocks.some((ab) => ab.instrs.some((i) => i.mnemonic === 'bl' || i.mnemonic === 'blx'))
|
|
3143
|
-
) {
|
|
3144
|
-
const slotAcc = (ins: Instr) => {
|
|
3145
|
-
const a = spMemAccess(ins);
|
|
3146
|
-
return a && !a.regOff && a.width === 4 && a.off % 4 === 0 && a.off >= 0 && a.off + 4 <= localArea
|
|
3147
|
-
? a.off
|
|
3148
|
-
: null;
|
|
3149
|
-
};
|
|
3150
|
-
const isStore = (ins: Instr) => /^str/.test(ins.mnemonic);
|
|
3151
|
-
// Entry-REACHABLE blocks only: a reload in dead code is not evidence that live code reads the
|
|
3152
|
-
// slot back, and counting it lets an argument store satisfy (a) on the strength of an
|
|
3153
|
-
// instruction that never executes.
|
|
3154
|
-
const live = entryReachable;
|
|
3155
|
-
const reloaded = new Set<number>();
|
|
3156
|
-
for (const b of live) {
|
|
3157
|
-
for (const ins of asmBlocks[b].instrs) {
|
|
3158
|
-
const off = slotAcc(ins);
|
|
3159
|
-
if (off !== null && !isStore(ins)) {
|
|
3160
|
-
reloaded.add(off);
|
|
3161
|
-
}
|
|
3162
|
-
}
|
|
3163
|
-
}
|
|
3164
|
-
// CONTIGUITY. AAPCS lays the outgoing stack arguments at [sp,#0] upward, one word each, so an
|
|
3165
|
-
// argument block is CONTIGUOUS FROM ZERO: a store at [sp,#4] can be argument 6 of a call only
|
|
3166
|
-
// if argument 5 at [sp,#0] is also supplied on a path to that same call. A pending store
|
|
3167
|
-
// whose lower slots are nowhere supplied is therefore provably not an argument block, and
|
|
3168
|
-
// refusing it is a false alarm — the exact false alarm that blocked the commonest real shape,
|
|
3169
|
-
// a value spilled at [sp,#4] and kept live across calls (kleod's ProcessInputAndUpdateEntities
|
|
3170
|
-
// stores its `sp4` local and calls m4aSongNumStart 80 lines later, with offset 0 never stored
|
|
3171
|
-
// in the whole function).
|
|
3172
|
-
//
|
|
3173
|
-
// The calibration in this: a conforming caller stores EVERY argument slot of a call it makes,
|
|
3174
|
-
// so "slot 0 unsupplied" rules out "slot 4 is an argument". Hand-written asm could skip
|
|
3175
|
-
// storing an argument the callee never reads; agbcc cannot (no interprocedural dead-argument
|
|
3176
|
-
// elimination). That is the same producer assumption the reload conditions above already
|
|
3177
|
-
// make, stated once here.
|
|
3178
|
-
const prefixStored = (k: number, st: Set<number>): boolean => {
|
|
3179
|
-
for (let j = 0; j < k; j += 4) {
|
|
3180
|
-
if (!st.has(j)) {
|
|
3181
|
-
return false;
|
|
3182
|
-
}
|
|
3183
|
-
}
|
|
3184
|
-
return true;
|
|
3185
|
-
};
|
|
3186
|
-
// (a), contiguity-filtered: a store never reloaded ANYWHERE is an argument's signature only
|
|
3187
|
-
// if its lower slots are supplied somewhere too; otherwise it is an ordinary dead local.
|
|
3188
|
-
const storedAnywhere = new Set<number>();
|
|
3189
|
-
for (const b of live) {
|
|
3190
|
-
for (const ins of asmBlocks[b].instrs) {
|
|
3191
|
-
const off = slotAcc(ins);
|
|
3192
|
-
if (off !== null && isStore(ins)) {
|
|
3193
|
-
storedAnywhere.add(off);
|
|
3194
|
-
}
|
|
3195
|
-
}
|
|
3196
|
-
}
|
|
3197
|
-
for (const off of storedAnywhere) {
|
|
3198
|
-
if (!reloaded.has(off) && prefixStored(off, storedAnywhere)) {
|
|
3199
|
-
return `the store to [sp,#${off}] is never reloaded and its lower slots are supplied — it may be an outgoing stack argument of one of this function's calls`; // (a)
|
|
3200
|
-
}
|
|
3201
|
-
}
|
|
3202
|
-
// (b): `pendingOut[b]` = offsets stored and not yet reloaded on SOME path through b;
|
|
3203
|
-
// `storedOut[b]` = offsets stored on SOME path through b (a reload does not remove the value
|
|
3204
|
-
// from memory, so it does not remove the offset from this set — the callee would still read
|
|
3205
|
-
// what the store put there).
|
|
3206
|
-
const pendingOut: Array<Set<number>> = asmBlocks.map(() => new Set<number>());
|
|
3207
|
-
const storedOut: Array<Set<number>> = asmBlocks.map(() => new Set<number>());
|
|
3208
|
-
for (let changed = true; changed;) {
|
|
3209
|
-
changed = false;
|
|
3210
|
-
for (let b = 0; b < asmBlocks.length; b++) {
|
|
3211
|
-
if (!live.has(b)) {
|
|
3212
|
-
continue;
|
|
3213
|
-
}
|
|
3214
|
-
const pend = new Set<number>();
|
|
3215
|
-
const st = new Set<number>();
|
|
3216
|
-
for (const q of preds[b]) {
|
|
3217
|
-
for (const off of pendingOut[q]) {
|
|
3218
|
-
pend.add(off);
|
|
3219
|
-
}
|
|
3220
|
-
for (const off of storedOut[q]) {
|
|
3221
|
-
st.add(off);
|
|
3222
|
-
}
|
|
3223
|
-
}
|
|
3224
|
-
for (const ins of asmBlocks[b].instrs) {
|
|
3225
|
-
const off = slotAcc(ins);
|
|
3226
|
-
if (off !== null) {
|
|
3227
|
-
if (isStore(ins)) {
|
|
3228
|
-
pend.add(off);
|
|
3229
|
-
st.add(off);
|
|
3230
|
-
} else {
|
|
3231
|
-
pend.delete(off);
|
|
3232
|
-
}
|
|
3233
|
-
} else if (ins.mnemonic === 'bl' || ins.mnemonic === 'blx') {
|
|
3234
|
-
for (const k of pend) {
|
|
3235
|
-
if (prefixStored(k, st)) {
|
|
3236
|
-
// (b) — a plausible argument block reaches this call unread
|
|
3237
|
-
return `the store to [sp,#${k}] reaches \`bl ${ins.ops[0] ?? '?'}\` unread with its lower slots supplied — it may be that call's outgoing stack argument`;
|
|
3238
|
-
}
|
|
3239
|
-
}
|
|
3240
|
-
}
|
|
3241
|
-
}
|
|
3242
|
-
const grow = (out: Array<Set<number>>, cur: Set<number>): void => {
|
|
3243
|
-
if (cur.size !== out[b].size || [...cur].some((o) => !out[b].has(o))) {
|
|
3244
|
-
out[b] = cur;
|
|
3245
|
-
changed = true;
|
|
3246
|
-
}
|
|
3247
|
-
};
|
|
3248
|
-
grow(pendingOut, pend);
|
|
3249
|
-
grow(storedOut, st);
|
|
3250
|
-
}
|
|
3251
|
-
}
|
|
3091
|
+
// OUTGOING ARGUMENTS — one analysis, and its refusals are this guard's. `analyzeOutgoingArgs`
|
|
3092
|
+
// decides per call whether the words staged at the bottom of the frame are a callee's
|
|
3093
|
+
// arguments; everything it cannot license refuses here, so a `[sp,#k]` access in such a
|
|
3094
|
+
// function still declines loud.
|
|
3095
|
+
if (outgoing.blocker !== null) {
|
|
3096
|
+
return outgoing.blocker;
|
|
3252
3097
|
}
|
|
3253
3098
|
// A `pop`/`ldm` off sp READS frame memory, and push/pop are transparent to dataflow, so a pop
|
|
3254
3099
|
// taken while the local area is still reserved reads a slot this model has retargeted into SSA
|
|
@@ -3469,7 +3314,123 @@ export function lift(
|
|
|
3469
3314
|
// widened to `localArea >= 4` (measured). No answer to the frame size moves it.
|
|
3470
3315
|
const capturedObjectIsTheWholeFrame = target.compiler === 'agbcc' && frameBasePassedToCallee && localArea === 4;
|
|
3471
3316
|
|
|
3472
|
-
|
|
3317
|
+
// THE OUTGOING STACK-ARGUMENT AREA. `frontend/stackargs.ts` holds the licence and every refusal;
|
|
3318
|
+
// what belongs HERE is the decoding it deliberately does not do — which accesses are whole frame
|
|
3319
|
+
// slots, which instructions are calls, and what each callee's DECLARATION asks for. `blx rN`
|
|
3320
|
+
// names a REGISTER in the operand slot, so it matches no prototype and its block is null, which
|
|
3321
|
+
// is correct: nothing here knows what an indirect call takes.
|
|
3322
|
+
//
|
|
3323
|
+
// A WHOLE WORD OF THE RESERVED LOCAL AREA is the same bytes `isOwnFrameWordSlot` models, minus
|
|
3324
|
+
// `slotsOk` — which is the answer this analysis is being asked to help compute.
|
|
3325
|
+
const slotAcc = (ins: Instr): number | null => {
|
|
3326
|
+
const a = spMemAccess(ins);
|
|
3327
|
+
return a && !a.regOff && a.width === 4 && a.off % 4 === 0 && a.off >= 0 && a.off + 4 <= localArea ? a.off : null;
|
|
3328
|
+
};
|
|
3329
|
+
// WHAT ONE CALLEE'S DECLARATION SAYS — the ONE place that reads it. The analysis below and the
|
|
3330
|
+
// `bl` lowering both come through here, so the arity that LICENSED a block and the arity that
|
|
3331
|
+
// CONSUMES it cannot drift apart; a disagreement between two spellings of this lookup would read
|
|
3332
|
+
// `r4` as argument 5 or throw a slot-model error naming the wrong thing.
|
|
3333
|
+
//
|
|
3334
|
+
// WHERE THE BLOCK IS is `compilerBehaviors.stagesOutgoingArgsInFrame`, not an assumption: the
|
|
3335
|
+
// area sits at the bottom of the frame this function reserved because agbcc's thumb.h defines
|
|
3336
|
+
// ACCUMULATE_OUTGOING_ARGS. A compiler that does not claim it stages nothing here, and every
|
|
3337
|
+
// call keeps the refusal it had before the licence existed.
|
|
3338
|
+
//
|
|
3339
|
+
// THE BLOCK IS WORDS AND THE ARITY IS PARAMETERS, which are the same number only while every
|
|
3340
|
+
// parameter occupies exactly one word: AAPCS lays arguments 5..n at [sp,#0] upward, one word
|
|
3341
|
+
// each, and the lowering maps parameter k to word k - |argRegs|. A `double`, a `long long` or a
|
|
3342
|
+
// by-value struct breaks both halves at once — it adds words AND moves every later argument's
|
|
3343
|
+
// home — so the premise is checked here rather than assumed from a distant module.
|
|
3344
|
+
//
|
|
3345
|
+
// WHAT THE CHECK CAN SEE. Only the TYPED form of `params` carries spellings, and `declaredWidth`
|
|
3346
|
+
// answers for every type asmlift can spell; a width it cannot read is the only evidence that a
|
|
3347
|
+
// parameter may be wider than a word, so such a declaration sizes no block and refuses
|
|
3348
|
+
// (`unsizableDeclaration`). That is not merely a message: with a wide parameter the two witnesses
|
|
3349
|
+
// can AGREE by coincidence — `void fd(s32, s32, s32, s32, double)` staged as two words matches a
|
|
3350
|
+
// six-parameter list whose fifth entry is `double`, and consuming it would hand the callee six
|
|
3351
|
+
// arguments. The COUNT form (`{ params: 5 }`) carries no spellings at all: it is the user's word
|
|
3352
|
+
// for how many WORDS the call takes, and a count that lies is garbage in — `validatePrototypes`
|
|
3353
|
+
// can no more check it than it can check `returnsVoid`.
|
|
3354
|
+
//
|
|
3355
|
+
// The machine-derived side upholds the premise at its source: `prototypesFromSymbols` drops a
|
|
3356
|
+
// whole entry rather than spell a parameter that is not 1, 2 or 4 bytes (test/proto.test.ts).
|
|
3357
|
+
const wideParam = (p: FnProto | undefined): string | null =>
|
|
3358
|
+
(Array.isArray(p?.params) ? p.params : []).find((t) => {
|
|
3359
|
+
const w = declaredWidth(t);
|
|
3360
|
+
return w === undefined || w > 32;
|
|
3361
|
+
}) ?? null;
|
|
3362
|
+
// `block` is null for the two cases that license nothing: an arity that fits in registers (there
|
|
3363
|
+
// IS no outgoing block) and one this frontend cannot lay out (`wide` names the parameter, and
|
|
3364
|
+
// `unsizableDeclaration` below turns it into the refusal). The analysis is told null for both —
|
|
3365
|
+
// it may license neither — and only the second is an error to report.
|
|
3366
|
+
const declaredCall = (
|
|
3367
|
+
callee: string,
|
|
3368
|
+
): { arity: number; block: readonly number[] | null; wide: string | null } | null => {
|
|
3369
|
+
const own = prototypes[callee];
|
|
3370
|
+
const proto = protoArity(own) !== undefined ? own : RUNTIME_HELPERS[callee];
|
|
3371
|
+
const arity = protoArity(proto);
|
|
3372
|
+
if (arity === undefined) {
|
|
3373
|
+
return null;
|
|
3374
|
+
}
|
|
3375
|
+
const words = arity - target.argRegs.length;
|
|
3376
|
+
if (words <= 0 || target.compilerBehaviors.stagesOutgoingArgsInFrame !== true) {
|
|
3377
|
+
// No outgoing block exists to lay out: it all fits in registers, or this compiler does not
|
|
3378
|
+
// claim to stage arguments inside the caller's own frame at all.
|
|
3379
|
+
return { arity, block: null, wide: null };
|
|
3380
|
+
}
|
|
3381
|
+
const wide = wideParam(proto);
|
|
3382
|
+
return {
|
|
3383
|
+
arity,
|
|
3384
|
+
block: wide === null ? Array.from({ length: words }, (_, i) => 4 * i) : null,
|
|
3385
|
+
wide,
|
|
3386
|
+
};
|
|
3387
|
+
};
|
|
3388
|
+
const outgoingArgs = analyzeOutgoingArgs<Instr>({
|
|
3389
|
+
blocks: asmBlocks.map((ab) => ({
|
|
3390
|
+
events: ab.instrs.flatMap((ins): StackArgsEvent<Instr>[] => {
|
|
3391
|
+
const off = slotAcc(ins);
|
|
3392
|
+
if (off !== null) {
|
|
3393
|
+
return [{ kind: /^str/.test(ins.mnemonic) ? 'store' : 'load', off }];
|
|
3394
|
+
}
|
|
3395
|
+
if (ins.mnemonic === 'bl' || ins.mnemonic === 'blx') {
|
|
3396
|
+
const callee = ins.ops[0] ?? '?';
|
|
3397
|
+
return [{ kind: 'call', call: ins, callee, declared: declaredCall(callee)?.block ?? null }];
|
|
3398
|
+
}
|
|
3399
|
+
return [];
|
|
3400
|
+
}),
|
|
3401
|
+
})),
|
|
3402
|
+
preds,
|
|
3403
|
+
live: entryReachable,
|
|
3404
|
+
localArea,
|
|
3405
|
+
argRegs: target.argRegs.length,
|
|
3406
|
+
capturedWholeFrame: capturedObjectIsTheWholeFrame,
|
|
3407
|
+
});
|
|
3408
|
+
|
|
3409
|
+
// A DECLARATION THIS FRONTEND CANNOT LAY OUT refuses for the whole function, ahead of every other
|
|
3410
|
+
// slot-model refusal, because it is the most specific thing that was seen. The analysis is never
|
|
3411
|
+
// told a block for such a call, so it can license nothing either way; what this adds is the
|
|
3412
|
+
// message, which names the parameter rather than a staged word that happened to disagree.
|
|
3413
|
+
const unsizableDeclaration = ((): string | null => {
|
|
3414
|
+
for (const ab of asmBlocks) {
|
|
3415
|
+
for (const ins of ab.instrs) {
|
|
3416
|
+
if (ins.mnemonic !== 'bl' && ins.mnemonic !== 'blx') {
|
|
3417
|
+
continue;
|
|
3418
|
+
}
|
|
3419
|
+
const callee = ins.ops[0] ?? '?';
|
|
3420
|
+
const declared = declaredCall(callee);
|
|
3421
|
+
if (declared !== null && declared.wide !== null) {
|
|
3422
|
+
return (
|
|
3423
|
+
`callee \`${callee}\` is declared with ${declared.arity} arguments and its parameter type \`${declared.wide}\` ` +
|
|
3424
|
+
'is one asmlift cannot size — a parameter wider than one word moves every later argument home, ' +
|
|
3425
|
+
"so this frame's outgoing stack-argument block cannot be laid out"
|
|
3426
|
+
);
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
return null;
|
|
3431
|
+
})();
|
|
3432
|
+
|
|
3433
|
+
const slotsOffReason = unsizableDeclaration ?? slotModelBlocker(outgoingArgs);
|
|
3473
3434
|
const slotsOk = slotsOffReason === null;
|
|
3474
3435
|
// Every offset the body actually keys as an SSA slot — the frame-object audit checks the
|
|
3475
3436
|
// address-taken object cannot overlap one (two models for one byte is a silent disagreement).
|
|
@@ -4156,11 +4117,27 @@ export function lift(
|
|
|
4156
4117
|
const targetSym = a;
|
|
4157
4118
|
// Caller-supplied prototype wins; otherwise a known runtime helper (`__divsi3` &c.)
|
|
4158
4119
|
// supplies its arity so its arguments are recovered; only then fall back to guessing.
|
|
4159
|
-
const declared =
|
|
4160
|
-
const argc = declared ?? fallbackArgcHere(bi);
|
|
4120
|
+
const declared = declaredCall(targetSym);
|
|
4121
|
+
const argc = declared?.arity ?? fallbackArgcHere(bi);
|
|
4122
|
+
// ARGUMENTS BEYOND THE REGISTERS come out of this frame's outgoing area, at [sp,#0]
|
|
4123
|
+
// upward — the block `analyzeOutgoingArgs` licensed for THIS call, and only that block.
|
|
4124
|
+
// `fallbackArgcHere` never exceeds `argRegs.length`, so an unlicensed stack argument can
|
|
4125
|
+
// only come from a declaration, and the analysis has already refused that function;
|
|
4126
|
+
// reaching here with no block means the slot model is off for another reason, and the
|
|
4127
|
+
// decline names it rather than reading `r4` as if it were argument 5.
|
|
4128
|
+
const stackArgs = slotsOk ? outgoingArgs.blocks.get(ins) : undefined;
|
|
4161
4129
|
const args: Value[] = [];
|
|
4162
4130
|
for (let k = 0; k < argc; k++) {
|
|
4163
|
-
|
|
4131
|
+
if (k < target.argRegs.length) {
|
|
4132
|
+
args.push(readVar(`r${k}`, bi));
|
|
4133
|
+
continue;
|
|
4134
|
+
}
|
|
4135
|
+
const off = stackArgs?.[k - target.argRegs.length];
|
|
4136
|
+
if (off === undefined) {
|
|
4137
|
+
throw spAsDataError();
|
|
4138
|
+
}
|
|
4139
|
+
usedSlotOffsets.add(off);
|
|
4140
|
+
args.push(readVar(slotKey(off), bi));
|
|
4164
4141
|
}
|
|
4165
4142
|
const res = mkValue(T.unk(32));
|
|
4166
4143
|
const callOp = mkOp('call', { operands: args, results: [res], attrs: { target: targetSym } });
|
|
@@ -4168,7 +4145,7 @@ export function lift(
|
|
|
4168
4145
|
// A GUESSED arity is revisited in `finish()`: only once the whole function is lifted is it
|
|
4169
4146
|
// known whether every path to here passes through another call, which would have clobbered
|
|
4170
4147
|
// the argument registers this guess just read.
|
|
4171
|
-
if (declared ===
|
|
4148
|
+
if (declared === null) {
|
|
4172
4149
|
ssa.recordGuessedCall(callOp, bi, target);
|
|
4173
4150
|
}
|
|
4174
4151
|
writeData('r0', bi, res); // the callee defines r0 …
|
|
@@ -4195,7 +4172,7 @@ export function lift(
|
|
|
4195
4172
|
} else if (!last) {
|
|
4196
4173
|
// an EMPTY block is only ever the synthetic entry preheader (decoded blocks are non-empty):
|
|
4197
4174
|
// fall through to the real entry, whose loop-header phis take their entry operand from here.
|
|
4198
|
-
irb.ops.push(mkOp('br', { successors: [succ(fallLabel(bi))] }));
|
|
4175
|
+
irb.ops.push(mkOp('br', { attrs: { fallthrough: true }, successors: [succ(fallLabel(bi))] }));
|
|
4199
4176
|
} else if (kind === 'return') {
|
|
4200
4177
|
// bx lr / pop {…,pc} / mov pc,lr
|
|
4201
4178
|
//
|
|
@@ -4227,7 +4204,7 @@ export function lift(
|
|
|
4227
4204
|
irb.ops.push(mkOp('cond_br', { operands: [cond], successors: [succ(last.ops[0]), succ(fallLabel(bi))] }));
|
|
4228
4205
|
} else {
|
|
4229
4206
|
// fallthrough (last instruction is a call / data op, no control transfer)
|
|
4230
|
-
irb.ops.push(mkOp('br', { successors: [succ(fallLabel(bi))] }));
|
|
4207
|
+
irb.ops.push(mkOp('br', { attrs: { fallthrough: true }, successors: [succ(fallLabel(bi))] }));
|
|
4231
4208
|
}
|
|
4232
4209
|
};
|
|
4233
4210
|
|
package/src/ir/core.ts
CHANGED
|
@@ -53,8 +53,55 @@ export interface Fn {
|
|
|
53
53
|
* so at its own definition), and a fact the structurer reads but the score probe's clone drops
|
|
54
54
|
* makes that probe's delta a fact about a program asmlift does not emit. */
|
|
55
55
|
slotHomes: SlotHomes | undefined;
|
|
56
|
+
/** L1 SIDE DATA (see {@link ParamEvidence}); set by the SSA builder, `undefined` on parsed IR.
|
|
57
|
+
* REQUIRED-but-possibly-undefined for exactly the reason `writeOrder` and `slotHomes` are. */
|
|
58
|
+
paramEvidence: ParamEvidence | undefined;
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
/** What the machine's own object shows about each ENTRY PARAMETER, beyond the value graph — two
|
|
62
|
+
* observations the lift destroys, recorded so raise/paramwidth.ts can read them. Every entry
|
|
63
|
+
* parameter of a lifted function has an entry; a `Fn` with no map is one nobody measured.
|
|
64
|
+
*
|
|
65
|
+
* BOTH ARE OBSERVATIONS, NOT VERDICTS. What a compiler's object shows for a narrow DECLARED
|
|
66
|
+
* parameter is a fact about a TARGET, and this builder holds none; `target.ts`
|
|
67
|
+
* `compilerBehaviors.narrowParamWitness` decides what the pair means, and the disassemblies for
|
|
68
|
+
* each value live there.
|
|
69
|
+
*
|
|
70
|
+
* `deadHome` — THE STORE DOES NOT REACH THE IR. Both slot-modelling frontends spell a word
|
|
71
|
+
* sp-relative store as a write to the SSA key `sp@k` instead of a `store` op (`stackSlotKey`), so
|
|
72
|
+
* a slot nothing reloads has no reader, no op and no value: it simply is not there by L1. A
|
|
73
|
+
* parameter stored to two slots, one of them reloaded, still counts — the dead store happened —
|
|
74
|
+
* and the reader's own gates decide what a parameter with that much traffic may become.
|
|
75
|
+
*
|
|
76
|
+
* It asks NO FRAME PARTITION, and that is the difference from {@link SlotHomes}. A slot home is a
|
|
77
|
+
* DECLARATION RANK, so `sp@40` had to be classified as this function's local before it could be
|
|
78
|
+
* stamped, and MIPS declares no partition and so stamps none. "Stored here and never read back"
|
|
79
|
+
* needs no such classification: it is a statement about the store, true at any offset.
|
|
80
|
+
*
|
|
81
|
+
* `selfRedefined` — THE REGISTER IDENTITY DOES NOT REACH THE IR EITHER. SSA renames, so nothing
|
|
82
|
+
* downstream can tell a value the machine put back in the argument's OWN register from one it put
|
|
83
|
+
* in a scratch. This records the first: the first write to the parameter's register, in the entry
|
|
84
|
+
* block, is a value that parameter itself feeds. First and not any — a later write is a reuse of a
|
|
85
|
+
* register the parameter is done with.
|
|
86
|
+
*
|
|
87
|
+
* ENTRY PARAMETERS ONLY, for both. A dead spill of an ordinary value is a dead spill, and an
|
|
88
|
+
* ordinary register's self-update is arithmetic; it is the incoming ARGUMENT that carries a
|
|
89
|
+
* declaration.
|
|
90
|
+
*
|
|
91
|
+
* AND SO NEITHER FOLLOWS `replaceAllUsesWith`, where a {@link SlotHomes} entry does. A frame
|
|
92
|
+
* coordinate belongs to whichever value the structurer will name, so it travels with the uses;
|
|
93
|
+
* these are facts about the ARGUMENT REGISTER the machine was handed, and no other value can come
|
|
94
|
+
* to have been that argument. The score probe's clone re-keys the map (`cli/src/report.ts`)
|
|
95
|
+
* because a clone mints new `Value` objects for the same parameters; nothing else moves it. */
|
|
96
|
+
export interface ParamObservation {
|
|
97
|
+
/** the machine stored this parameter to a stack slot no load reads back (the ABI argument home) */
|
|
98
|
+
deadHome: boolean;
|
|
99
|
+
/** the first entry-block write to this parameter's own register is a value the parameter feeds */
|
|
100
|
+
selfRedefined: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type ParamEvidence = ReadonlyMap<Value, ParamObservation>;
|
|
104
|
+
|
|
58
105
|
/** Which `[sp,#k]` the machine homed a value at — the frame coordinate, carried L1 → L3.
|
|
59
106
|
*
|
|
60
107
|
* The coordinate exists only in the frontends: they record a word sp-relative slot's value in SSA
|
|
@@ -69,9 +116,10 @@ export interface Fn {
|
|
|
69
116
|
*
|
|
70
117
|
* DECLARES, NOT OWNS, AND THE DIFFERENCE IS AN AGBCC FACT WITH A LIVE DEPENDENCY: `ownedLocals`,
|
|
71
118
|
* the partition a def-less READ asks, admits agbcc's outgoing stack-argument area, and an offset
|
|
72
|
-
* there is an ABI position rather than an `expand_decl` rank. Under Thumb the
|
|
73
|
-
*
|
|
74
|
-
* (frontend/ssa.ts) for the
|
|
119
|
+
* there is an ABI position rather than an `expand_decl` rank. Under Thumb the declared range
|
|
120
|
+
* therefore starts above that area, where the largest outgoing argument block the frontend could
|
|
121
|
+
* LICENSE ends — see `declaredLocals` (frontend/ssa.ts) for what the licence proves and for the
|
|
122
|
+
* frames it refuses outright.
|
|
75
123
|
*
|
|
76
124
|
* ONE consumer reads it for its content — the structurer, which turns it into
|
|
77
125
|
* `SFn.locals[i].slots`; everything else only carries it (`replaceAllUsesWith`, the report's
|
|
@@ -194,6 +242,17 @@ export function terminator(b: Block): Op | undefined {
|
|
|
194
242
|
return b.ops[b.ops.length - 1];
|
|
195
243
|
}
|
|
196
244
|
|
|
245
|
+
/** The layout fall-through stamp alone (`opcodes.ts` declares it on `br` and on `ret`). A pass that
|
|
246
|
+
* moves a `ret` ONTO an edge takes the edge's own stamp with it and nothing else: the rest of a
|
|
247
|
+
* terminator's attrs describe the branch, not the arrival.
|
|
248
|
+
*
|
|
249
|
+
* A GUARD rather than a fix — `fallthrough` is the only attr anything sets on a `br`, so copying
|
|
250
|
+
* the whole bag would behave identically — spelled so that the next attr a frontend invents cannot
|
|
251
|
+
* ride onto an edge it says nothing about. */
|
|
252
|
+
export function fallThroughOf(term: Op): Op['attrs'] {
|
|
253
|
+
return term.attrs.fallthrough === true ? { fallthrough: true } : {};
|
|
254
|
+
}
|
|
255
|
+
|
|
197
256
|
/** The successor blocks of `b`, read off its terminator. */
|
|
198
257
|
export function successorsOf(b: Block): Block[] {
|
|
199
258
|
return terminator(b)?.successors.map((s) => s.block) ?? [];
|
package/src/ir/opcodes.ts
CHANGED
|
@@ -177,7 +177,16 @@ export const OPCODES = {
|
|
|
177
177
|
// no more reapable than a dead `call`.
|
|
178
178
|
opaque: { operands: 'variadic', results: 1, effects: true },
|
|
179
179
|
// --- terminators ---
|
|
180
|
+
// `fallthrough: true` — OPTIONAL, the same fact as on `br`: a `ret` SUNK onto one edge
|
|
181
|
+
// (`raise/retsink.ts`, `raise/tailsink.ts`) replaces that edge's `br` and takes its stamp, so
|
|
182
|
+
// `structure/retspell.ts` can still read how the machine arrived. A `ret` a frontend built carries
|
|
183
|
+
// nothing, and is read by its block's in-edges instead.
|
|
180
184
|
ret: { operands: 'variadic', results: 0, terminator: true, successors: 0 },
|
|
185
|
+
// `fallthrough: true` — OPTIONAL, set by a frontend when the block carried NO control-transfer
|
|
186
|
+
// instruction and this `br` stands for the machine simply running into the next block. Its
|
|
187
|
+
// absence therefore means a real branch instruction. The one reader is `structure/retspell.ts`,
|
|
188
|
+
// which needs to tell a `b <label>` (a transfer the source asked for) from layout; a pass that
|
|
189
|
+
// builds a fresh `br` leaves it off and is read as a branch, which is the conservative answer.
|
|
181
190
|
br: { operands: 0, results: 0, terminator: true, successors: 1 },
|
|
182
191
|
cond_br: { operands: 1, results: 0, terminator: true, successors: 2 },
|
|
183
192
|
// Many-way switch dispatch (Regime B, jump table). The single operand is the scrutinee;
|
package/src/ir/parse.ts
CHANGED
|
@@ -121,7 +121,13 @@ export function parse(text: string): Fn {
|
|
|
121
121
|
|
|
122
122
|
// No write order: the text form is the value graph, and the record is a measurement of the
|
|
123
123
|
// MACHINE. A parsed fn's edges are UNMEASURED, never written-nowhere (ir/core.ts `WriteOrder`).
|
|
124
|
-
return {
|
|
124
|
+
return {
|
|
125
|
+
name,
|
|
126
|
+
blocks: rawBlocks.map((r) => r.block),
|
|
127
|
+
writeOrder: undefined,
|
|
128
|
+
slotHomes: undefined,
|
|
129
|
+
paramEvidence: undefined,
|
|
130
|
+
};
|
|
125
131
|
}
|
|
126
132
|
|
|
127
133
|
/** Drop the two annotations `print(fn, { writeOrder: true })` appends, and NOTHING else.
|