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:
@@ -186,8 +186,19 @@ instruction, instructions with no ordinary fallthrough path, and one-byte Data P
|
||||
changes that can use `INCD` or `DECD`, including direct branches whose target is already
|
||||
the next labeled address. It also tracks symbolic Data Pointer bases and known offsets to
|
||||
find a `SETD` that reloads an address the pointer already holds. Device input and stack
|
||||
pops are excluded from dead-assignment checks because consuming their input is itself an effect. SplitLint's control-flow
|
||||
knowledge is deliberately local: labels begin reachable regions, while an unparsed
|
||||
pops are excluded from dead-assignment checks because consuming their input is itself an effect.
|
||||
|
||||
**Nothing it knows survives a call.** `CALL` really does restore A, B and Data Pointers 0
|
||||
to 2, so a pointer set before one is genuinely still set after it - and saying so produced
|
||||
advice that was correct today and unsafe to take. 122 of the 178 redundant `SETD`s it first
|
||||
found were redundant *only* because of that restore, and removing them would have become a
|
||||
wrong-pointer bug the moment the callee was reached with `RCAL` instead, which is what
|
||||
`RCAL` was added to this machine for. Worse, the linter would have gone quiet rather than
|
||||
complained, because it forgets everything across an `RCAL`. 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.
|
||||
|
||||
SplitLint's control-flow knowledge is otherwise deliberately local: labels begin reachable regions, while an unparsed
|
||||
directive or data item ends the current claim. It does not yet expand includes or build a
|
||||
complete control-flow graph. Warnings normally leave a successful exit status, while
|
||||
`--fatal-warnings` makes any warning fail the command for use in automated checks.
|
||||
|
||||
+26
-7
@@ -352,9 +352,26 @@ 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
|
||||
// ---- 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);
|
||||
}
|
||||
|
||||
+36
-5
@@ -99,6 +99,13 @@ printf '%s\n' \
|
||||
' NOP' \
|
||||
' "INIA 0d0; DPUA.0"' \
|
||||
' ; INIB 0d0' \
|
||||
'acrossACall:' \
|
||||
' SETD.2 Thing' \
|
||||
' CALL somewhere' \
|
||||
' SETD.2 Thing' \
|
||||
' INIA 0d9' \
|
||||
' CALL somewhere' \
|
||||
' INIA 0d9' \
|
||||
> "$fixture"
|
||||
|
||||
"$LINT" "$fixture" > "$output" 2>&1
|
||||
@@ -146,14 +153,38 @@ expect 56 "BRC is always taken because carry is known set"
|
||||
expect 60 "BRC is never taken because carry is known clear"
|
||||
expect 61 "BNC is always taken because carry is known clear"
|
||||
|
||||
# ---- And a claim does not survive a call ----
|
||||
#
|
||||
# CALL really does restore A, B and DP0 to DP2, so both repeats below are genuinely
|
||||
# redundant and the linter used to say so. It no longer does, on purpose: that advice is
|
||||
# correct only while the callee is reached with CALL, and RCAL exists precisely so hot
|
||||
# helpers can stop being one. 122 of the 178 redundant SETDs in the corpus were of this
|
||||
# kind, and removing them would have become wrong the day a helper was made faster.
|
||||
#
|
||||
# THE LINE NUMBERS ARE THE REPEATS, not the label above them. Checking the label instead
|
||||
# passes for free, because a label is never warned about by anything.
|
||||
quiet=0
|
||||
for line in 70 73; do
|
||||
if grep -q "^${fixture}:${line}: style:" "$output"; then
|
||||
FAIL=$((FAIL + 1)); MISSING+=("line $line should be quiet across a CALL")
|
||||
printf " [%sFAIL%s] line %-3s no claim survives a CALL\n" "$RED" "$RESET" "$line"
|
||||
else
|
||||
PASS=$((PASS + 1)); quiet=$((quiet + 1))
|
||||
printf " [%sok %s] line %-3s no claim survives a CALL\n" "$GREEN" "$RESET" "$line"
|
||||
fi
|
||||
done
|
||||
|
||||
# ---- And nothing it does not know about ----
|
||||
#
|
||||
# The fixture carries four lines that must stay quiet: a device read whose result is
|
||||
# overwritten, a self-restoring push pair that is NOT one, a string that happens to spell
|
||||
# instructions, and a commented-out instruction. A rule that started firing on any of
|
||||
# those would be a rule reading source that is not code.
|
||||
# The fixture carries four lines that must stay quiet for a different reason: a device read
|
||||
# whose result is overwritten, a self-restoring push pair that is NOT one, a string that
|
||||
# happens to spell instructions, and a commented-out instruction. A rule that started
|
||||
# firing on any of those would be a rule reading source that is not code.
|
||||
#
|
||||
# Every pass so far is either a warning that was expected or a line that stayed quiet, so
|
||||
# the warnings that came out should be the passes less the quiet ones.
|
||||
total=$(grep -c "^${fixture}:[0-9]*: style:" "$output")
|
||||
if [ "$total" = "$PASS" ] && [ "$FAIL" = "0" ]; then
|
||||
if [ "$total" = "$((PASS - quiet))" ] && [ "$FAIL" = "0" ]; then
|
||||
printf " [%sok %s] and nothing else %s warnings, no more\n" "$GREEN" "$RESET" "$total"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user