Stop the linter recommending a change that a faster helper would break

SplitLint knew that CALL restores A, B and Data Pointers 0 to 2, so a
pointer set before a call is still set after it. That is true, and it made
the tool give advice that was correct today and unsafe to take.

Of the 178 redundant SETDs it found across the corpus, 122 were redundant
ONLY because of that restore - the shape is everywhere, because it is how a
helper is given its arguments:

  SETD.0 SbfsBlock
  SETD.2 SbfsFileStart
  CALL sbfsSetWord
  SETD.0 SbfsBlock        <- flagged

Removing that last line is right until sbfsSetWord is reached with RCAL,
which restores nothing - and that is not hypothetical, it is what RCAL was
added to this machine for, measured at close to halving the assembler's
memory traffic. The failure would also be silent from the linter's side: it
forgets everything across an RCAL, so it would stop reporting while the
removals stayed removed.

So a claim now ends at any call, for pointers and for registers, the way a
claim about carry already did. 257 warnings become 127, and the redundant
SETDs 178 become 54 - which is exactly the number an independent count of
"no CALL in between" had arrived at separately.

The fixture gained a SETD and an INIA repeated across a CALL, which must
stay quiet, and the harness fails with the old behaviour put back. Two
mistakes worth recording: the new expectations first pointed at the LABEL
above the repeats rather than the repeats, which passes for free because
nothing ever warns about a label; and the block landed in the middle of
another check's comment, leaving that comment describing the code below it
instead of its own.
This commit is contained in:
Anachronaut
2026-08-26 17:42:56 -04:00
parent 8f4cc5878d
commit c146d98588
3 changed files with 76 additions and 15 deletions
+27 -8
View File
@@ -352,10 +352,27 @@ static void updateKnownPointers(const SourceInstruction *instruction,
pointer->known = 0;
}
if (strcmp(instruction->mnemonic, "CALL") == 0) {
pointers[3].known = 0;
} else if (strcmp(instruction->mnemonic, "RCAL") == 0
|| strcmp(instruction->mnemonic, "SWI") == 0) {
// ---- A CALL ends every claim, and that is a deliberate loss of precision ----
//
// CALL really does save and restore A, B and Data Pointers 0 to 2, so a pointer set
// before one is genuinely still set after it, and this used to say so - forgetting
// only DP3, which CALL does not restore. It was right, and the advice it produced was
// not safe to take.
//
// 122 of the 178 redundant SETDs it found across the corpus were redundant ONLY
// because of that restore. Removing them is correct today and becomes a wrong-pointer
// bug the moment the callee is converted from CALL to RCAL - which is not a
// hypothetical, it is what RCAL was added to this machine for, and converting the hot
// helpers was measured at close to halving the assembler's memory traffic. Worse, the
// linter would go quiet rather than complain: it forgets everything across an RCAL, so
// it would simply stop reporting while the removals stayed removed.
//
// So a claim now ends at any call, the way a claim about carry already did. Fifty
// four recommendations that stay true are worth more than a hundred and seventy eight
// that are conditional on a change the project intends to make.
if (strcmp(instruction->mnemonic, "CALL") == 0
|| strcmp(instruction->mnemonic, "RCAL") == 0
|| strcmp(instruction->mnemonic, "SWI") == 0) {
forgetPointers(pointers);
}
}
@@ -469,10 +486,12 @@ static void updateKnownRegisters(const SourceInstruction *instruction,
known->bKnown = 0;
}
// A raw callee and a software interrupt may return with operand values the source
// immediately around the call cannot describe. CALL's documented convention saves
// A and B, so it deliberately does not appear here.
if (strcmp(instruction->mnemonic, "RCAL") == 0
// A call ends what is known about A and B for the same reason it ends what is known
// about a pointer: CALL's convention saves them, so the knowledge is real, but it is
// knowledge about the CALLEE rather than about the code in hand - and it stops being
// true the day that callee is reached with RCAL instead. See updateKnownPointers.
if (strcmp(instruction->mnemonic, "CALL") == 0
|| strcmp(instruction->mnemonic, "RCAL") == 0
|| strcmp(instruction->mnemonic, "SWI") == 0) {
forgetRegisters(known);
}