@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.
Files changed (68) hide show
  1. package/package.json +1 -1
  2. package/src/backend/c.ts +1 -0
  3. package/src/backend/cfamily.ts +33 -12
  4. package/src/backend/cpp.ts +1 -0
  5. package/src/backend/lines.ts +10 -0
  6. package/src/backend/pascal.ts +17 -5
  7. package/src/cdecl/parse.ts +21 -0
  8. package/src/contracts.ts +73 -20
  9. package/src/frontend/arm-body.ts +155 -0
  10. package/src/frontend/asmdata.ts +2 -1
  11. package/src/frontend/call-plan.ts +197 -45
  12. package/src/frontend/callee-body.ts +370 -0
  13. package/src/frontend/device-pins.ts +18 -8
  14. package/src/frontend/frame-objects.ts +504 -61
  15. package/src/frontend/frontend.ts +20 -0
  16. package/src/frontend/jump-table.ts +35 -20
  17. package/src/frontend/mips.ts +226 -8
  18. package/src/frontend/ppc.ts +26 -2
  19. package/src/frontend/register-struct-call.ts +1 -86
  20. package/src/frontend/ssa.ts +263 -1
  21. package/src/frontend/stackargs.ts +95 -17
  22. package/src/frontend/thumb-entry.ts +340 -0
  23. package/src/frontend/thumb.ts +410 -61
  24. package/src/ir/alias.ts +29 -8
  25. package/src/ir/bits.ts +1081 -66
  26. package/src/ir/core.ts +154 -4
  27. package/src/ir/entry.ts +3 -5
  28. package/src/ir/opcodes.ts +28 -0
  29. package/src/ir/parse.ts +1 -0
  30. package/src/ir/simplify.ts +61 -10
  31. package/src/ir/verify.ts +16 -0
  32. package/src/l3/argbase.ts +2 -0
  33. package/src/l3/ast.ts +294 -2
  34. package/src/l3/basecse.ts +35 -10
  35. package/src/l3/caseendgoto.ts +223 -0
  36. package/src/l3/coalesce.ts +42 -14
  37. package/src/l3/constinit.ts +34 -6
  38. package/src/l3/dce.ts +52 -8
  39. package/src/l3/ptrcell.ts +2 -0
  40. package/src/l3/regspell.ts +2 -0
  41. package/src/l3/reindex.ts +13 -4
  42. package/src/l3/scopebase.ts +2 -0
  43. package/src/l3/tailmerge.ts +2 -0
  44. package/src/pipeline.ts +507 -23
  45. package/src/proto-context.ts +135 -1
  46. package/src/raise/const.ts +25 -0
  47. package/src/raise/declared-volatile.ts +1 -1
  48. package/src/raise/divpow2.ts +192 -0
  49. package/src/raise/offsetnames.ts +412 -10
  50. package/src/raise/pre-recovery.ts +8 -5
  51. package/src/raise/pre-structure.ts +24 -5
  52. package/src/raise/reroll.ts +4 -24
  53. package/src/raise/shortcircuit.ts +192 -23
  54. package/src/raise/unpre.ts +594 -0
  55. package/src/rank-variations.ts +97 -4
  56. package/src/rank.ts +258 -43
  57. package/src/structure/analysis.ts +56 -10
  58. package/src/structure/cross-jumped-tail.ts +216 -0
  59. package/src/structure/globalaccess.ts +46 -1
  60. package/src/structure/structure.ts +4221 -368
  61. package/src/structure/switch-recover.ts +345 -26
  62. package/src/structure/unfolded-branch-constant.ts +90 -0
  63. package/src/symbols.ts +52 -15
  64. package/src/target.ts +269 -8
  65. package/src/trace.ts +28 -14
  66. package/src/variation-definitions.ts +207 -6
  67. package/src/variation-gates.ts +2 -0
  68. 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 the first guessed count
268
- // the bytes cannot tell from another, as its refusal
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
- let passThrough: { call: string; reg: string; why: string } | undefined;
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
- const v = ssa.readVar(set, bi);
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
- if (passThrough === undefined) {
987
- const call = typeof guess.at === 'number' ? `at 0x${guess.at.toString(16)}` : guess.at;
988
- const last = target.argRegs[argc - 1];
989
- // the blocks since the dispatch on the way to the call: a path stops at a `switch_br`'s block
990
- const since = new Set<number>([bi]);
991
- const dispatch = (at: number) => ssa.irBlocks[at].ops.some((op) => op.opcode === 'switch_br');
992
- const lastWritten = argc <= floor || ssa.writtenOnEveryPath(last, bi, dispatch, (at) => since.add(at));
993
- // what an effect those blocks made so far reads
994
- const effectReads = new Set(
995
- [...since].flatMap((at) =>
996
- ssa.irBlocks[at].ops.flatMap((op) =>
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
- const stored = target.argRegs
1002
- .slice(floor, argc)
1003
- .find((r) => ssa.holdsValue(r, bi, (v) => mayPass(v, r) && effectReads.has(v)));
1004
- const leftIncoming =
1005
- markedAt < 0 ? undefined : target.argRegs.slice(argc, markedAt).find((r) => ssa.holdsEntryValue(r, bi));
1006
- if (!lastWritten) {
1007
- passThrough = {
1008
- call,
1009
- reg: last,
1010
- why: 'the last register the count takes, holds a value some path since the dispatch did not write',
1011
- };
1012
- } else if (stored !== undefined) {
1013
- passThrough = { call, reg: stored, why: 'which the count takes, holds a value an effect of its block uses' };
1014
- } else if (leftIncoming !== undefined) {
1015
- passThrough = {
1016
- call,
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
- if ((parkedInArgs || caseStateRead) && passThrough !== undefined) {
1384
+ const open = parkedInArgs || caseStateRead ? passThrough.find((p) => !p.unread()) : undefined;
1385
+ if (open !== undefined) {
1234
1386
  fail(
1235
- `cannot lift '${name}': the call ${passThrough.call} has no prototype, and ${passThrough.reg}, ` +
1236
- `${passThrough.why}, in a function that lifts only through what a jump-table dispatch left in its ` +
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
  );