SplitLint reports valid assembly that has a shorter direct expression: zero loads that could be RSTA or RSTB, Q moved through the stack where MVQA would do, self-cancelling push and pop pairs, assignments overwritten before use, unreachable fallthrough, one-byte pointer moves that could be INCD or DECD, a branch to the label directly below it, a SETD reloading an address the pointer already holds, and branches whose carry is known. Its model is deliberately local and conservative: every label and every directive forgets all known state, so a claim only ever lives inside a straight-line region. It knows the calling convention - CALL forgets DP3 and keeps the rest, RCAL and SWI forget everything - and it shares assembly.o with the assembler, so an added opcode cannot leave it holding a private copy of the instruction table. 260 warnings across the corpus, of which three were wrong in the way that matters: branchTest.asm and interruptFlagTest.asm exist to check that a branch whose carry is known behaves correctly, so a diagnostic saying the outcome is known is exactly right and exactly unwanted. A line whose comment says "splitlint: <reason>" is now not reported on. THE REASON IS REQUIRED and a bare marker is refused, because a suppression nobody explained outlives whatever made it necessary. Suppressed warnings are not counted, so --fatal-warnings does not fail on one, and the number of them is printed at the end so the claim is visible rather than silent. Tests/lint.sh checked a TOTAL: twenty three warnings expected, twenty three found. That number stays right while the thing behind it goes wrong - a rule that stopped firing while another fired twice would pass, and so would a rule reporting at the wrong line. It now checks which warning came out and at which line, that nothing else came out, and that the four lines meant to stay quiet did. Confirmed by breaking one rule's message and watching it name that rule: the old assertion passed the same sabotage, because the warning still fired and the count never moved. Written with the user while I was away; my part is the suppression mechanism, the harness rewrite, and the three marks in the test programs.
213 lines
7.3 KiB
Bash
Executable File
213 lines
7.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Checks that SplitLint finds what it claims to find, one rule at a time.
|
|
#
|
|
# This used to compare a TOTAL. Twenty three warnings came out and twenty three were
|
|
# expected, which is a number that stays right while the thing behind it goes wrong: a
|
|
# change that stopped one rule firing and made another fire twice would pass without a
|
|
# murmur, and so would a rule that moved to the wrong line. What is checked here now is
|
|
# WHICH warning came out and AT WHICH LINE, so a rule that stops working says which one it
|
|
# was.
|
|
#
|
|
# The fixture is written here rather than kept as a file because every line of it exists
|
|
# to trip exactly one rule, and a reader wants the pattern and the expectation side by
|
|
# side rather than in two places.
|
|
#
|
|
# Written by Anachronaut
|
|
|
|
set -u
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
cd "$ROOT" || exit 1
|
|
|
|
LINT="$ROOT/SplitLint"
|
|
[ -x "$LINT" ] || { echo "SplitLint is not built."; exit 1; }
|
|
|
|
mkdir -p Tests/build
|
|
fixture=Tests/build/lint.asm
|
|
output=Tests/build/lint.out
|
|
|
|
GREEN=$'\033[32m'; RED=$'\033[31m'; RESET=$'\033[0m'
|
|
[ -t 1 ] || { GREEN=""; RED=""; RESET=""; }
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
MISSING=()
|
|
|
|
printf '%s\n' \
|
|
'#Program' \
|
|
' INIA 0d0' \
|
|
' INIB 0x00' \
|
|
' INIA 0d1' \
|
|
' DPUA.2' \
|
|
' INIA 0x01' \
|
|
' DPDA.3' \
|
|
' PSHQ' \
|
|
' POPA' \
|
|
' PSHQ ; Comments do not break adjacent instructions.' \
|
|
' POPB' \
|
|
' PSHA' \
|
|
' POPA' \
|
|
' PSHB' \
|
|
' POPB' \
|
|
' INIA 0d5' \
|
|
' RSTA' \
|
|
' LDB.2' \
|
|
' MVQB' \
|
|
' INA ; Device reads are not removable assignments.' \
|
|
' INIA 0d8' \
|
|
' BRI reachable' \
|
|
' NOP' \
|
|
' ; A comment does not make fallthrough possible.' \
|
|
' NOP' \
|
|
'reachable:' \
|
|
' NOP' \
|
|
' BNA alreadyHere' \
|
|
' ; Blank space and comments do not move an address.' \
|
|
'alreadyHere:' \
|
|
' NOP' \
|
|
'knownValues:' \
|
|
' INIA 0d1' \
|
|
' NOP' \
|
|
' DPUA.1' \
|
|
' RSTA' \
|
|
' INIB 0d1' \
|
|
' NOP' \
|
|
' DPUW.1' \
|
|
' INIA 0d7' \
|
|
' NOP' \
|
|
' INIA 0d7' \
|
|
'pointerState:' \
|
|
' SETD.2 Thing' \
|
|
' NOP' \
|
|
' SETD.2 Thing' \
|
|
' SETD.1 Other' \
|
|
' DPUP.1 0d3' \
|
|
' DPDN.1 0d3' \
|
|
' SETD.1 Other' \
|
|
'carryState:' \
|
|
' CCF' \
|
|
' CCF' \
|
|
' INIA 0xFF' \
|
|
' INCA' \
|
|
' BRC carryTaken' \
|
|
' NOP' \
|
|
'carryTaken:' \
|
|
' CCF' \
|
|
' BRC carryClear' \
|
|
' BNC carryClear' \
|
|
' NOP' \
|
|
'carryClear:' \
|
|
' NOP' \
|
|
' "INIA 0d0; DPUA.0"' \
|
|
' ; INIB 0d0' \
|
|
> "$fixture"
|
|
|
|
"$LINT" "$fixture" > "$output" 2>&1
|
|
|
|
# ---- What every line of the fixture is for ----
|
|
#
|
|
# One row per rule: the line it should be reported at, and enough of the message to name
|
|
# the rule without pinning its exact wording.
|
|
expect() {
|
|
local line="$1" want="$2"
|
|
local got
|
|
got=$(grep -c "^${fixture}:${line}: style: ${want}" "$output")
|
|
if [ "$got" = "1" ]; then
|
|
PASS=$((PASS + 1))
|
|
printf " [%sok %s] line %-3s %s\n" "$GREEN" "$RESET" "$line" "$want"
|
|
else
|
|
FAIL=$((FAIL + 1)); MISSING+=("line $line: $want")
|
|
printf " [%sFAIL%s] line %-3s %s\n" "$RED" "$RESET" "$line" "$want"
|
|
fi
|
|
}
|
|
|
|
echo "Checking that SplitLint still finds each thing it knows about."
|
|
|
|
expect 2 "loading zero into A takes two bytes"
|
|
expect 3 "loading zero into B takes two bytes"
|
|
expect 5 "pointer offset is known to be one"
|
|
expect 6 "INIA leaves A at its known value of 1"
|
|
expect 7 "pointer offset is known to be one"
|
|
expect 8 "moving Q to A through the stack takes two instructions"
|
|
expect 10 "moving Q to B through the stack takes two instructions"
|
|
expect 12 "pushing A and immediately restoring it leaves A unchanged"
|
|
expect 14 "pushing B and immediately restoring it leaves B unchanged"
|
|
expect 16 "INIA assigns A, but RSTA replaces it immediately"
|
|
expect 18 "LDB.2 assigns B, but MVQB replaces it immediately"
|
|
expect 23 "no ordinary fallthrough reaches this instruction"
|
|
expect 25 "no ordinary fallthrough reaches this instruction"
|
|
expect 28 "branch target is the next labeled address"
|
|
expect 35 "pointer offset is known to be one"
|
|
expect 39 "word-sized pointer offset is known to be one"
|
|
expect 42 "INIA leaves A at its known value of 7"
|
|
expect 46 "DP2 is already known to hold Thing"
|
|
expect 50 "DP1 is already known to hold Other"
|
|
expect 53 "carry is already known to be clear"
|
|
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 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.
|
|
total=$(grep -c "^${fixture}:[0-9]*: style:" "$output")
|
|
if [ "$total" = "$PASS" ] && [ "$FAIL" = "0" ]; then
|
|
printf " [%sok %s] and nothing else %s warnings, no more\n" "$GREEN" "$RESET" "$total"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
printf " [%sFAIL%s] and nothing else %s warnings for %s expectations\n" \
|
|
"$RED" "$RESET" "$total" "$PASS"
|
|
FAIL=$((FAIL + 1))
|
|
grep "^${fixture}:[0-9]*: style:" "$output" | while read -r line; do
|
|
echo " $line"
|
|
done
|
|
fi
|
|
|
|
# ---- Saying a warning is deliberate ----
|
|
#
|
|
# The reason is required. A marker with nothing after it is refused rather than honoured,
|
|
# because a suppression nobody explained outlives whatever made it necessary.
|
|
printf '%s\n' '#Program' ' INIA 0d0 ; splitlint: on purpose, for the test' > Tests/build/lintSup.asm
|
|
supOut=$("$LINT" Tests/build/lintSup.asm 2>&1)
|
|
if [ -z "$(echo "$supOut" | grep 'style:')" ] && echo "$supOut" | grep -q "1 warning suppressed"; then
|
|
PASS=$((PASS + 1)); printf " [%sok %s] a reasoned suppression is honoured and counted\n" "$GREEN" "$RESET"
|
|
else
|
|
FAIL=$((FAIL + 1)); MISSING+=("a reasoned suppression")
|
|
printf " [%sFAIL%s] a reasoned suppression is honoured and counted\n" "$RED" "$RESET"
|
|
fi
|
|
|
|
printf '%s\n' '#Program' ' INIA 0d0 ; splitlint:' > Tests/build/lintBare.asm
|
|
if "$LINT" Tests/build/lintBare.asm >/dev/null 2>&1; then
|
|
FAIL=$((FAIL + 1)); MISSING+=("a bare suppression is refused")
|
|
printf " [%sFAIL%s] a suppression with no reason is refused\n" "$RED" "$RESET"
|
|
else
|
|
PASS=$((PASS + 1)); printf " [%sok %s] a suppression with no reason is refused\n" "$GREEN" "$RESET"
|
|
fi
|
|
|
|
# ---- And warnings can be made to fail a build ----
|
|
if "$LINT" --fatal-warnings "$fixture" >/dev/null 2>&1; then
|
|
FAIL=$((FAIL + 1)); MISSING+=("--fatal-warnings")
|
|
printf " [%sFAIL%s] --fatal-warnings fails on a warning\n" "$RED" "$RESET"
|
|
else
|
|
PASS=$((PASS + 1)); printf " [%sok %s] --fatal-warnings fails on a warning\n" "$GREEN" "$RESET"
|
|
fi
|
|
|
|
# A suppressed warning is not a warning, so it must not fail one either.
|
|
if "$LINT" --fatal-warnings Tests/build/lintSup.asm >/dev/null 2>&1; then
|
|
PASS=$((PASS + 1))
|
|
printf " [%sok %s] and does not fail on a suppressed one\n" "$GREEN" "$RESET"
|
|
else
|
|
FAIL=$((FAIL + 1)); MISSING+=("--fatal-warnings on a suppressed warning")
|
|
printf " [%sFAIL%s] and does not fail on a suppressed one\n" "$RED" "$RESET"
|
|
fi
|
|
|
|
echo
|
|
if [ "$FAIL" -eq 0 ]; then
|
|
echo "All $PASS SplitLint checks passed."
|
|
exit 0
|
|
fi
|
|
echo "$PASS passed, $FAIL failed: ${MISSING[*]}"
|
|
exit 1
|