#!/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; } # Recording the corpus as it stands, for when warnings have been deliberately fixed or # deliberately accepted. Same shape as run.sh's --bless, and for the same reason. if [ "${1:-}" = "--bless" ]; then "$LINT" --machine $(find Programs -name '*.asm' | sort) \ | awk -F'\t' '{print $1"\t"$3}' | sort | uniq -c \ | awk '{printf "%s\t%s\t%s\n", $2, $3, $1}' | sort > "$ROOT/Tests/lint-baseline.txt" echo "Recorded $(wc -l < "$ROOT/Tests/lint-baseline.txt" | tr -d ' ') file and rule pairs." exit 0 fi 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' \ 'acrossACall:' \ ' SETD.2 Thing' \ ' CALL somewhere' \ ' SETD.2 Thing' \ ' INIA 0d9' \ ' CALL somewhere' \ ' INIA 0d9' \ > "$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 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 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 - quiet))" ] && [ "$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 # ---- And the corpus has not grown a new warning ---- # # Sixty one warnings are left in the corpus ON PURPOSE - registers whose equal values mean # different things, idioms whose redundancy is what makes them self contained, arms of # comparison chains that get reordered. Nothing stopped a sixty second from appearing. # # THE BASELINE IS COUNTS PER FILE AND RULE RATHER THAN LINE NUMBERS. Recording lines would # churn the whole file every time something was inserted above a warning, which is the same # reason a cycle count is stripped from every recorded output here. baseline="$ROOT/Tests/lint-baseline.txt" current=Tests/build/lint-current.txt "$LINT" --machine $(find Programs -name '*.asm' | sort) \ | awk -F'\t' '{print $1"\t"$3}' | sort | uniq -c \ | awk '{printf "%s\t%s\t%s\n", $2, $3, $1}' | sort > "$current" if [ ! -f "$baseline" ]; then FAIL=$((FAIL + 1)); MISSING+=("the baseline is missing") printf " [%sFAIL%s] the corpus baseline is missing\n" "$RED" "$RESET" elif diff -q "$baseline" "$current" >/dev/null; then PASS=$((PASS + 1)) printf " [%sok %s] the corpus matches its baseline %s file and rule pairs\n" \ "$GREEN" "$RESET" "$(wc -l < "$current" | tr -d ' ')" else FAIL=$((FAIL + 1)); MISSING+=("the corpus baseline") printf " [%sFAIL%s] the corpus has moved away from its baseline\n" "$RED" "$RESET" diff "$baseline" "$current" | sed 's/^/ /' printf " To accept these, run: ./Tests/lint.sh --bless\n" fi echo if [ "$FAIL" -eq 0 ]; then echo "All $PASS SplitLint checks passed." exit 0 fi echo "$PASS passed, $FAIL failed: ${MISSING[*]}" exit 1