@asmlift/core 0.10.0 → 0.11.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/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +33 -12
- package/src/backend/cpp.ts +1 -0
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +17 -5
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +73 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +2 -1
- package/src/frontend/call-plan.ts +197 -45
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/device-pins.ts +18 -8
- package/src/frontend/frame-objects.ts +504 -61
- package/src/frontend/frontend.ts +20 -0
- package/src/frontend/jump-table.ts +35 -20
- package/src/frontend/mips.ts +226 -8
- package/src/frontend/ppc.ts +26 -2
- package/src/frontend/register-struct-call.ts +1 -86
- package/src/frontend/ssa.ts +263 -1
- package/src/frontend/stackargs.ts +95 -17
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +410 -61
- package/src/ir/alias.ts +29 -8
- package/src/ir/bits.ts +1081 -66
- package/src/ir/core.ts +154 -4
- package/src/ir/entry.ts +3 -5
- package/src/ir/opcodes.ts +28 -0
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/verify.ts +16 -0
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +294 -2
- package/src/l3/basecse.ts +35 -10
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +34 -6
- package/src/l3/dce.ts +52 -8
- package/src/l3/ptrcell.ts +2 -0
- package/src/l3/regspell.ts +2 -0
- package/src/l3/reindex.ts +13 -4
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/tailmerge.ts +2 -0
- package/src/pipeline.ts +507 -23
- package/src/proto-context.ts +135 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +1 -1
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/offsetnames.ts +412 -10
- package/src/raise/pre-recovery.ts +8 -5
- package/src/raise/pre-structure.ts +24 -5
- package/src/raise/reroll.ts +4 -24
- package/src/raise/shortcircuit.ts +192 -23
- package/src/raise/unpre.ts +594 -0
- package/src/rank-variations.ts +97 -4
- package/src/rank.ts +258 -43
- package/src/structure/analysis.ts +56 -10
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +46 -1
- package/src/structure/structure.ts +4221 -368
- package/src/structure/switch-recover.ts +345 -26
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +52 -15
- package/src/target.ts +269 -8
- package/src/trace.ts +28 -14
- package/src/variation-definitions.ts +207 -6
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +63 -2
|
@@ -49,6 +49,7 @@ import { type ContextGlobals, structNamed } from '../proto-context';
|
|
|
49
49
|
import { floatSidesOf, helperPrototypes, isFloatHelper, isWideHelper, lookupHelper } from '../runtime-helpers';
|
|
50
50
|
import { type SymbolMap, symbolsByName } from '../symbols';
|
|
51
51
|
import { type TargetDescription, argRegisterHoles } from '../target';
|
|
52
|
+
import { type CalleeBodyBound, asksCalleeBody } from './callee-body';
|
|
52
53
|
import { type FrontendRefusal, FrontendUnsupportedError } from './errors';
|
|
53
54
|
import { type OwnDeclaration, ownDeclarationOf } from './frontend';
|
|
54
55
|
import type { HighHalves } from './high-half';
|
|
@@ -193,6 +194,10 @@ export interface CallSite {
|
|
|
193
194
|
write(reg: string, v: Value): void;
|
|
194
195
|
/** argument word `k` past the registers, where this ABI places it (`CallLowering.stackArgs`) */
|
|
195
196
|
stackWord?(k: number): Value;
|
|
197
|
+
/** the stack words the frontend licensed for a call nothing declares, off its callee's body
|
|
198
|
+
* (frontend/stackargs.ts `StackArgsCall.bodyReads`): the call then takes every argument register
|
|
199
|
+
* and these words, and its arity is no guess. Absent or 0 where none was licensed. */
|
|
200
|
+
readonly stackWords?: number;
|
|
196
201
|
/** how this frontend builds and splits a pair (`CallLowering.pairs`) */
|
|
197
202
|
readonly pairs?: CallPairs;
|
|
198
203
|
/** what this frontend defines registers with that is no value and so no argument — a pending
|
|
@@ -260,14 +265,24 @@ export function callDeclarations(
|
|
|
260
265
|
fail: FrontendRefusal,
|
|
261
266
|
/** the function's own declaration, where one binds its body — asked as a call is lowered */
|
|
262
267
|
ownDeclaration: () => OwnDeclaration | undefined = () => ownDeclarationOf(name, prototypes, undefined),
|
|
268
|
+
/** the argument words a callee's own body reads at most, where it is in the input
|
|
269
|
+
* (frontend/callee-body.ts) — asked of a named call no declaration lays out */
|
|
270
|
+
calleeBodyBound?: CalleeBodyBound,
|
|
271
|
+
/** the callees taken at every argument register that may still hold its entry value
|
|
272
|
+
* (`LiftOptions.entryReadCalls`) */
|
|
273
|
+
entryReadCalls?: ReadonlySet<string>,
|
|
274
|
+
/** told of each call laid out at the stack words its callee's body loads (`LiftOptions.stackSized`) */
|
|
275
|
+
stackSized?: (callee: string, words: number) => void,
|
|
263
276
|
): CallDeclarations {
|
|
264
277
|
// What a call leaves holding nothing this function can name — checked against `argRegs` there.
|
|
265
278
|
const callClobbers = clobberedByCall(target);
|
|
266
279
|
// `refuseDispatchUnlockedPassThrough`'s two facts, gathered as each call is lowered: whether a
|
|
267
|
-
// dispatch's marker reached a guessed call's argument registers, and
|
|
268
|
-
//
|
|
280
|
+
// dispatch's marker reached a guessed call's argument registers, and every guessed count the
|
|
281
|
+
// bytes cannot tell from another, in the order they were lowered, each with its refusal. `unread`
|
|
282
|
+
// says the register lies past what the callee's own body reads (frontend/callee-body.ts), which
|
|
283
|
+
// excuses it: asked only as the gate would refuse, so no body is lifted where it would not.
|
|
269
284
|
let parkedInArgs = false;
|
|
270
|
-
|
|
285
|
+
const passThrough: Array<{ call: string; reg: string; why: string; unread: () => boolean }> = [];
|
|
271
286
|
// …and what a call to a PAIR-RETURNING helper leaves: the same set minus the high half, because
|
|
272
287
|
// the low half is the return register (already excluded) and `lower` writes the high one itself
|
|
273
288
|
// from the callee's own result. The two arms of one rule: where the callee hands a register back,
|
|
@@ -783,7 +798,8 @@ export function callDeclarations(
|
|
|
783
798
|
if (target.compilerBehaviors.keepsUnusedReads === true || !ssa.writtenSinceCall(set, bi)) {
|
|
784
799
|
return undefined;
|
|
785
800
|
}
|
|
786
|
-
|
|
801
|
+
// a lookup, not the frontend's read: `leftover` asks what instructions read (`readAfterEveryReachingWrite`)
|
|
802
|
+
const v = ssa.readGuessedArg(set, bi);
|
|
787
803
|
const def = ssa.irBlocks[bi].ops.find((op) => op.results.includes(v));
|
|
788
804
|
const sig = def === undefined ? undefined : (OPCODES[def.opcode as keyof typeof OPCODES] as OpSig | undefined);
|
|
789
805
|
const readsRefuse = target.compilerBehaviors.discardedReadMayFillArgRegister !== false;
|
|
@@ -914,9 +930,144 @@ export function callDeclarations(
|
|
|
914
930
|
// A DECLARATION HOLDING A SPELLING NOTHING CAN SIZE STATES NO LAYOUT (`proto.ts`
|
|
915
931
|
// `declaredCallArgs`), so `widths` is null for it and this falls to the guess below —
|
|
916
932
|
// the same answer the callee would get with no prototype at all.
|
|
917
|
-
const { widths, returns } = p;
|
|
918
933
|
// the leading arguments a variadic declaration states, where the rest is guessed
|
|
919
|
-
const floor = widths === null ? variadicFloor(callee) : 0;
|
|
934
|
+
const floor = p.widths === null ? variadicFloor(callee) : 0;
|
|
935
|
+
// WHAT THE CALLEE'S OWN BODY READS, where it is in the input and nothing lays the call out
|
|
936
|
+
// (frontend/callee-body.ts): at most `bodyWords()` argument words. It answers only a refusal
|
|
937
|
+
// about registers at or past it — the gap rule's (`fallbackArgc`'s `bound`) and the dispatch
|
|
938
|
+
// pass-through gate's — since whether those are arguments changes nothing the callee computes.
|
|
939
|
+
// It never caps the count: it bounds what the callee READS, and a source call passes more
|
|
940
|
+
// whenever a parameter goes unused or a K&R call hands extra arguments, so a register the caller
|
|
941
|
+
// set up for the call stays an argument (`tick(5)` to a body that reads none, `rd1(a, b, 7)` to
|
|
942
|
+
// one that reads r0, whose 7 also proves r0 is the caller's own a passed on). Asked lazily, as a
|
|
943
|
+
// refusal would fire, so a call no gate questions lifts no body. Only a callee whose entry
|
|
944
|
+
// states no parameter list is asked: a list nothing here can size, or a variadic one, still
|
|
945
|
+
// states parameters the call must be handed (`refuseShortOfDeclared`).
|
|
946
|
+
const stated = callee === undefined ? undefined : known(prototypes, callee);
|
|
947
|
+
const unstated = callee !== undefined && p.widths === null && asksCalleeBody(stated);
|
|
948
|
+
let bodyMemo: { words: number | undefined } | undefined;
|
|
949
|
+
const bodyWords = (): number | undefined =>
|
|
950
|
+
unstated ? (bodyMemo ??= { words: calleeBodyBound?.(callee!) }).words : undefined;
|
|
951
|
+
// whether an op lowered so far reads `v`: the call being lowered is not one yet
|
|
952
|
+
const reads = (o: Op, v: Value): boolean => o.operands.includes(v) || o.successors.some((e) => e.args.includes(v));
|
|
953
|
+
const readSoFar = (v: Value): boolean => ssa.irBlocks.some((b) => b.ops.some((o) => reads(o, v)));
|
|
954
|
+
// the function's own incoming value of argument register `r`
|
|
955
|
+
const incoming = (v: Value, r: string): boolean => ssa.paramReg.get(v) === r && ssa.irBlocks[0].params.includes(v);
|
|
956
|
+
// WHETHER WHAT ARGUMENT REGISTER `r` HOLDS WAS LEFT THERE BY A WRITE MADE FOR SOMETHING ELSE: a
|
|
957
|
+
// question about the register's last WRITE, not about its value. `mov r0, #5; strh r0, [r1];
|
|
958
|
+
// bl rnd` left the store's 5 in r0; `add r1, r5, #0; bl un` is a copy made for the call, though
|
|
959
|
+
// an earlier call read its value (`g4(a, b, c, d); un(a, b, c, d)`: agbcc and mwcc copy them back
|
|
960
|
+
// from callee-saved homes). A copy names no new value, so no value-level question tells them apart.
|
|
961
|
+
//
|
|
962
|
+
// WHAT READ COUNTS DEPENDS ON WHERE THE WRITE WAS, because a read that lowers to nothing — a copy
|
|
963
|
+
// into a callee-saved register, a store to a frame home — SAVES a value across the call, and a
|
|
964
|
+
// saved value may be handed to the call as well:
|
|
965
|
+
// - the call's own block wrote it, or nothing in this function did (the incoming value): only a
|
|
966
|
+
// lowered op's read since. `un(a); return g2(a);` and `un(); return g2(a);` compile to the
|
|
967
|
+
// same caller bytes on agbcc, IDO, gcc 2.7.2, KMC and mwcc (agbcc `add r4, r0, #0; bl un`).
|
|
968
|
+
// - another block wrote it: a lowered op's read in the call's block, or a read after EACH write
|
|
969
|
+
// that reaches the call, a save included, on ANY path out of it but the ones into the call
|
|
970
|
+
// (`SsaBuilder.readAfterEveryReachingWrite`). agbcc's case CSE moves the cases' `i << 3` from
|
|
971
|
+
// r0 to r4 at their join (`add r4, r0, #0`) before the call's join, while a value handed to
|
|
972
|
+
// the call is copied back for it in the call's block (`add r0, r4, #0; bl rnd`, measured).
|
|
973
|
+
// EACH write, because one no read follows was made for the call (`if (c) { t = d + 3; gX = t;
|
|
974
|
+
// } else t = d - 9; rnd(t);` at IDO stores one arm's a0). ANY path, because gcc 2.7.2 and KMC
|
|
975
|
+
// fill a branch's delay slot with one call's argument (`bnez v0, L; move a0, zero; jal cr`),
|
|
976
|
+
// and the 0 reaches `L: jal stat` unread. NOT the call's own block's saves: IDO saves the
|
|
977
|
+
// argument it hands a call in the jal's delay slot (`jal rnd; sw a0, 28(sp)`, measured) and
|
|
978
|
+
// computes t into v1 where rnd is handed nothing, so that save is evidence FOR the argument.
|
|
979
|
+
// An earlier call's result is no such write (sa3 Task_274_8056314: `strb r0` of one arm's
|
|
980
|
+
// value, `bl sub_8056408` in the other, then `bl Task_274_8054E38`, whose source takes nothing).
|
|
981
|
+
const leftover = (r: string): boolean => {
|
|
982
|
+
const at = ssa.lastWriteOp(r, bi);
|
|
983
|
+
if (at !== undefined) {
|
|
984
|
+
const since = ssa.irBlocks[bi].ops.slice(at);
|
|
985
|
+
return ssa.holdsValue(r, bi, (v) => mayPass(v, r) && since.some((o) => reads(o, v)));
|
|
986
|
+
}
|
|
987
|
+
if (ssa.holdsValue(r, bi, (v) => mayPass(v, r) && ssa.irBlocks[bi].ops.some((o) => reads(o, v)))) {
|
|
988
|
+
return true;
|
|
989
|
+
}
|
|
990
|
+
const callResult = (v: Value): boolean =>
|
|
991
|
+
ssa.irBlocks.some((b) => b.ops.some((o) => o.opcode === 'call' && o.results.includes(v)));
|
|
992
|
+
// the values a join's phi of `v` merges, through phis of phis, or `undefined` where an edge
|
|
993
|
+
// into one is not lowered yet
|
|
994
|
+
const merged = (v: Value, seen = new Set<Value>()): Value[] | undefined => {
|
|
995
|
+
const at = ssa.irBlocks.find((b) => b.params.includes(v));
|
|
996
|
+
if (at === undefined) {
|
|
997
|
+
return [v];
|
|
998
|
+
}
|
|
999
|
+
if (seen.has(v)) {
|
|
1000
|
+
return [];
|
|
1001
|
+
}
|
|
1002
|
+
seen.add(v);
|
|
1003
|
+
const k = at.params.indexOf(v);
|
|
1004
|
+
const edges = ssa.irBlocks.flatMap((b) => b.ops.flatMap((o) => o.successors.filter((e) => e.block === at)));
|
|
1005
|
+
const out: Value[] = [];
|
|
1006
|
+
for (const e of edges) {
|
|
1007
|
+
const sub = e.args[k] === undefined ? undefined : merged(e.args[k], seen);
|
|
1008
|
+
if (sub === undefined) {
|
|
1009
|
+
return undefined;
|
|
1010
|
+
}
|
|
1011
|
+
out.push(...sub);
|
|
1012
|
+
}
|
|
1013
|
+
return edges.length > 0 ? out : undefined;
|
|
1014
|
+
};
|
|
1015
|
+
// an earlier call's result on every path in: a join's phi of copies of one (IDO's `move a3,
|
|
1016
|
+
// v0` in a branch's delay slot meets the path that skips the store between)
|
|
1017
|
+
const resultOnEveryPath = (v: Value): boolean => {
|
|
1018
|
+
const vs = merged(v);
|
|
1019
|
+
return vs !== undefined && vs.length > 0 && vs.every(callResult);
|
|
1020
|
+
};
|
|
1021
|
+
return (
|
|
1022
|
+
ssa.readAfterEveryReachingWrite(r, bi, {
|
|
1023
|
+
// an earlier call's result where that call left it is no write this function made, for this
|
|
1024
|
+
// call or another
|
|
1025
|
+
passes: (v) => mayPass(v, r) && !(r === target.returnReg && callResult(v)),
|
|
1026
|
+
accept: (v) => mayPass(v, r) && (!incoming(v, r) || readSoFar(v)),
|
|
1027
|
+
// a path into a block not lowered yet: what its value answers, as the base question did
|
|
1028
|
+
unlowered: (v) => readSoFar(v),
|
|
1029
|
+
// a copy of an earlier call's result into another register, or a reload of one, answers
|
|
1030
|
+
// only where read after: `bl gets; add r1, r0, #0` and `jal gets; move a1, v0` were, by the
|
|
1031
|
+
// loop's loads, while IDO's `lw a3, 28(sp)` of a result saved across a call (af
|
|
1032
|
+
// mCPk_PakPrivateLoad) was not, and says nothing
|
|
1033
|
+
answersIfRead: (v) => resultOnEveryPath(v),
|
|
1034
|
+
}) ??
|
|
1035
|
+
// nothing written here left to judge: what reaches answers by its value, the base question,
|
|
1036
|
+
// a join's phi by the values it merges (af mCPk_PakPrivateLoad compares the result it keeps)
|
|
1037
|
+
ssa.holdsValue(r, bi, (v) => mayPass(v, r) && (readSoFar(v) || (merged(v) ?? []).some(readSoFar)))
|
|
1038
|
+
);
|
|
1039
|
+
};
|
|
1040
|
+
const unreadByBody = (r: string): boolean => {
|
|
1041
|
+
const words = bodyWords();
|
|
1042
|
+
return words !== undefined && target.argRegs.indexOf(r) >= words;
|
|
1043
|
+
};
|
|
1044
|
+
// A CALL THE BODY BOUND READS AT EVERY INCOMING VALUE IT MAY BE HANDED (`entryReadCalls`): up to
|
|
1045
|
+
// the highest argument register that some path into the call leaves unwritten since the entry.
|
|
1046
|
+
// A register a call destroyed on every path holds nothing of this function's arguments, so a
|
|
1047
|
+
// read of it could add nothing to what the body reads of them, and it is not taken: taken, a
|
|
1048
|
+
// callee making two calls that nothing declares would refuse its own lift, and lose its bound.
|
|
1049
|
+
const entryWords =
|
|
1050
|
+
unstated && entryReadCalls?.has(callee!) === true
|
|
1051
|
+
? target.argRegs.reduce((n, r, k) => (ssa.writtenOnEveryPath(r, bi, () => false) ? n : k + 1), 0)
|
|
1052
|
+
: undefined;
|
|
1053
|
+
// A STACK BLOCK THE CALLEE'S BODY LICENSED states the arity outright: arguments past the
|
|
1054
|
+
// registers are staged only once every register is one, so the call takes all of them and the
|
|
1055
|
+
// block, one word each — what a count of that many words in a prototype would state. That holds
|
|
1056
|
+
// as stated on an ABI that never skips an argument register (agbcc's APCS). One that does
|
|
1057
|
+
// (`evenWordAlignedArgs`: O32 leaves a3 empty before a `long long` passed at 16(sp)) licenses a
|
|
1058
|
+
// block only where the frontend saw the call write every register and the callee's lift read
|
|
1059
|
+
// each (frontend/mips.ts `licensedStackWords`, frontend/callee-body.ts `readsEveryArgReg`).
|
|
1060
|
+
const licensed = unstated && (site.stackWords ?? 0) > 0 ? target.argRegs.length + site.stackWords! : undefined;
|
|
1061
|
+
if (licensed !== undefined) {
|
|
1062
|
+
stackSized?.(callee!, site.stackWords!);
|
|
1063
|
+
}
|
|
1064
|
+
const widths =
|
|
1065
|
+
licensed !== undefined
|
|
1066
|
+
? Array.from({ length: licensed }, () => 32)
|
|
1067
|
+
: entryWords !== undefined
|
|
1068
|
+
? Array.from({ length: entryWords }, () => 32)
|
|
1069
|
+
: p.widths;
|
|
1070
|
+
const { returns } = p;
|
|
920
1071
|
// the gaps `decideGap` decided: the value that proved each, and the gap's own refusal, owed if
|
|
921
1072
|
// that proof fails once the function is lifted
|
|
922
1073
|
const decided: Array<{ value: Value; refuse: () => void }> = [];
|
|
@@ -929,6 +1080,8 @@ export function callDeclarations(
|
|
|
929
1080
|
widths === null
|
|
930
1081
|
? fallbackArgc(ssa, target.argRegs, bi, {
|
|
931
1082
|
accept: mayPass,
|
|
1083
|
+
bound: bodyWords,
|
|
1084
|
+
leftover,
|
|
932
1085
|
gap: { name, at: guess.at, fail },
|
|
933
1086
|
floor,
|
|
934
1087
|
parked: (r) =>
|
|
@@ -983,41 +1136,35 @@ export function callDeclarations(
|
|
|
983
1136
|
ssa.hasReachingDef(r, bi, (v) => site.parked?.(v, r) === true || site.scaledIndex?.(v, r) === true),
|
|
984
1137
|
);
|
|
985
1138
|
parkedInArgs ||= markedAt >= 0;
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
[
|
|
996
|
-
|
|
997
|
-
(OPCODES[op.opcode as keyof typeof OPCODES] as OpSig | undefined)?.effects ? op.operands : [],
|
|
998
|
-
),
|
|
1139
|
+
const call = typeof guess.at === 'number' ? `at 0x${guess.at.toString(16)}` : guess.at;
|
|
1140
|
+
const last = target.argRegs[argc - 1];
|
|
1141
|
+
// the blocks since the dispatch on the way to the call: a path stops at a `switch_br`'s block
|
|
1142
|
+
const since = new Set<number>([bi]);
|
|
1143
|
+
const dispatch = (at: number) => ssa.irBlocks[at].ops.some((op) => op.opcode === 'switch_br');
|
|
1144
|
+
const lastWritten = argc <= floor || ssa.writtenOnEveryPath(last, bi, dispatch, (at) => since.add(at));
|
|
1145
|
+
// what an effect those blocks made so far reads
|
|
1146
|
+
const effectReads = new Set(
|
|
1147
|
+
[...since].flatMap((at) =>
|
|
1148
|
+
ssa.irBlocks[at].ops.flatMap((op) =>
|
|
1149
|
+
(OPCODES[op.opcode as keyof typeof OPCODES] as OpSig | undefined)?.effects ? op.operands : [],
|
|
999
1150
|
),
|
|
1000
|
-
)
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
reg: leftIncoming,
|
|
1018
|
-
why: "which the count leaves out below a dispatch's register, holds its incoming value",
|
|
1019
|
-
};
|
|
1020
|
-
}
|
|
1151
|
+
),
|
|
1152
|
+
);
|
|
1153
|
+
const stored = target.argRegs
|
|
1154
|
+
.slice(floor, argc)
|
|
1155
|
+
.find((r) => ssa.holdsValue(r, bi, (v) => mayPass(v, r) && effectReads.has(v)));
|
|
1156
|
+
const leftIncoming =
|
|
1157
|
+
markedAt < 0 ? undefined : target.argRegs.slice(argc, markedAt).find((r) => ssa.holdsEntryValue(r, bi));
|
|
1158
|
+
// each the lowest register of its kind, so one past the body's bound has none below it
|
|
1159
|
+
const open = (reg: string, why: string) => passThrough.push({ call, reg, why, unread: () => unreadByBody(reg) });
|
|
1160
|
+
if (!lastWritten) {
|
|
1161
|
+
open(last, 'the last register the count takes, holds a value some path since the dispatch did not write');
|
|
1162
|
+
}
|
|
1163
|
+
if (stored !== undefined) {
|
|
1164
|
+
open(stored, 'which the count takes, holds a value an effect of its block uses');
|
|
1165
|
+
}
|
|
1166
|
+
if (leftIncoming !== undefined) {
|
|
1167
|
+
open(leftIncoming, "which the count leaves out below a dispatch's register, holds its incoming value");
|
|
1021
1168
|
}
|
|
1022
1169
|
guess.refuse?.(argc);
|
|
1023
1170
|
refuseShortOfDeclared(callee, argc);
|
|
@@ -1040,11 +1187,14 @@ export function callDeclarations(
|
|
|
1040
1187
|
// count, or past the bound a call through an argument register sets, that a value reaches.
|
|
1041
1188
|
// The gap refusal has already fired on any such register past a guess; past the bound, the
|
|
1042
1189
|
// register leaves the call silently, and the frontend says whether it knows that one to be an
|
|
1043
|
-
// argument all the same.
|
|
1190
|
+
// argument all the same. A register at or past what the callee's own body reads is no
|
|
1191
|
+
// argument of it whatever it holds, so it is not asked: read only where the count already asked
|
|
1192
|
+
// the body, the one place the bound left a register out (`fallbackArgc`'s `bound`).
|
|
1044
1193
|
if (site.leftOut !== undefined && (widths === null || bound >= 0)) {
|
|
1194
|
+
const readTo = bodyMemo?.words ?? target.argRegs.length;
|
|
1045
1195
|
site.leftOut(
|
|
1046
1196
|
target.argRegs
|
|
1047
|
-
.slice(argc)
|
|
1197
|
+
.slice(argc, Math.max(argc, readTo))
|
|
1048
1198
|
.filter((r) => r !== indirect?.reg && ssa.hasReachingDef(r, bi, (v) => mayPass(v, r))),
|
|
1049
1199
|
target.argRegs.slice(0, argc),
|
|
1050
1200
|
);
|
|
@@ -1060,6 +1210,7 @@ export function callDeclarations(
|
|
|
1060
1210
|
const guessed = (k: number): Value => {
|
|
1061
1211
|
const r = target.argRegs[k];
|
|
1062
1212
|
const v = ssa.readGuessedArg(r, bi);
|
|
1213
|
+
ssa.noteGuessedArgRead(r, bi, v);
|
|
1063
1214
|
return placeholders ? placeholders.guardRead(name, r, v) : v;
|
|
1064
1215
|
};
|
|
1065
1216
|
const args: Value[] = [];
|
|
@@ -1230,10 +1381,11 @@ export function callDeclarations(
|
|
|
1230
1381
|
lower,
|
|
1231
1382
|
heldAtUndeclaredCall,
|
|
1232
1383
|
refuseDispatchUnlockedPassThrough: (caseStateRead) => {
|
|
1233
|
-
|
|
1384
|
+
const open = parkedInArgs || caseStateRead ? passThrough.find((p) => !p.unread()) : undefined;
|
|
1385
|
+
if (open !== undefined) {
|
|
1234
1386
|
fail(
|
|
1235
|
-
`cannot lift '${name}': the call ${
|
|
1236
|
-
`${
|
|
1387
|
+
`cannot lift '${name}': the call ${open.call} has no prototype, and ${open.reg}, ` +
|
|
1388
|
+
`${open.why}, in a function that lifts only through what a jump-table dispatch left in its ` +
|
|
1237
1389
|
'registers — the arity guessed without a prototype and another compile to the same bytes, so the ' +
|
|
1238
1390
|
'argument count is not decidable',
|
|
1239
1391
|
);
|