Add SplitLint, and let it be told when something is deliberate
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.
This commit is contained in:
@@ -0,0 +1,734 @@
|
||||
// Linter.c
|
||||
// Small, deliberately conservative source linter for SplitBit assembly.
|
||||
//
|
||||
// This first version works one source file at a time and looks only at instructions
|
||||
// which are adjacent in that file. It is not an assembler and does not expand
|
||||
// includes or follow control flow. That modest boundary makes every diagnostic easy
|
||||
// to explain while the useful rules, and the interface they need, are still being
|
||||
// discovered.
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "../Assembler/assembly.h"
|
||||
#include "../Emulator/cpu.h"
|
||||
|
||||
#define LINE_CAPACITY 1024
|
||||
#define TOKEN_CAPACITY 64
|
||||
|
||||
typedef struct {
|
||||
char mnemonic[TOKEN_CAPACITY];
|
||||
char spelling[TOKEN_CAPACITY];
|
||||
char operand[TOKEN_CAPACITY];
|
||||
long literal;
|
||||
int hasLiteral;
|
||||
int line;
|
||||
} SourceInstruction;
|
||||
|
||||
typedef enum {
|
||||
SOURCE_OTHER,
|
||||
SOURCE_INSTRUCTION,
|
||||
SOURCE_LABEL,
|
||||
SOURCE_BOUNDARY
|
||||
} SourceLine;
|
||||
|
||||
typedef struct {
|
||||
int aKnown;
|
||||
int bKnown;
|
||||
uint8_t a;
|
||||
uint8_t b;
|
||||
} KnownRegisters;
|
||||
|
||||
typedef struct {
|
||||
int known;
|
||||
char base[TOKEN_CAPACITY];
|
||||
uint16_t offset;
|
||||
} KnownPointer;
|
||||
|
||||
typedef enum {
|
||||
CARRY_UNKNOWN = -1,
|
||||
CARRY_CLEAR = 0,
|
||||
CARRY_SET = 1
|
||||
} KnownCarry;
|
||||
|
||||
static void usage(const char *name) {
|
||||
printf("Usage: %s [--fatal-warnings] <sourcefile> [sourcefile ...]\n", name);
|
||||
printf("\n");
|
||||
printf("Checks SplitBit assembly source for correct but needlessly long forms.\n");
|
||||
printf("Warnings do not fail the command unless --fatal-warnings is given.\n");
|
||||
printf("\n");
|
||||
printf("A line whose comment says \"splitlint: <reason>\" is not reported on. The\n");
|
||||
printf("reason is required, so that a deliberate exception says what makes it one.\n");
|
||||
}
|
||||
|
||||
static void uppercase(char *text) {
|
||||
for (; *text; text++) {
|
||||
*text = (char)toupper((unsigned char)*text);
|
||||
}
|
||||
}
|
||||
|
||||
// A semicolon inside a string belongs to the string. Anything after one outside a
|
||||
// string is a comment and must not accidentally look like an instruction.
|
||||
static void removeComment(char *line) {
|
||||
int inString = 0;
|
||||
for (char *at = line; *at; at++) {
|
||||
if (*at == '"') {
|
||||
inString = !inString;
|
||||
} else if (*at == ';' && !inString) {
|
||||
*at = '\0';
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int literalValue(const char *text, long *value) {
|
||||
int base;
|
||||
if (text[0] != '0' || (text[1] != 'x' && text[1] != 'X'
|
||||
&& text[1] != 'd' && text[1] != 'D')) {
|
||||
return 0;
|
||||
}
|
||||
base = (text[1] == 'x' || text[1] == 'X') ? 16 : 10;
|
||||
char *end;
|
||||
long parsed = strtol(text + 2, &end, base);
|
||||
if (end == text + 2 || *end != '\0' || parsed < 0 || parsed > 255) {
|
||||
return 0;
|
||||
}
|
||||
*value = parsed;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Classifies enough of a physical source line for local analysis. Directives, strings,
|
||||
// literal data, and unknown tokens are boundaries: until the linter shares the full
|
||||
// assembler frontend, it makes no control-flow claim across something it did not parse.
|
||||
static SourceLine parseInstruction(char *line, int lineNumber, SourceInstruction *result) {
|
||||
removeComment(line);
|
||||
char *token = strtok(line, " \t\r\n");
|
||||
if (!token) {
|
||||
return SOURCE_OTHER;
|
||||
}
|
||||
if (token[0] == '#' || token[0] == '"') {
|
||||
return SOURCE_BOUNDARY;
|
||||
}
|
||||
size_t length = strlen(token);
|
||||
if (length && token[length - 1] == ':') {
|
||||
memset(result, 0, sizeof(*result));
|
||||
if (length - 1 < sizeof(result->spelling)) {
|
||||
memcpy(result->spelling, token, length - 1);
|
||||
result->spelling[length - 1] = '\0';
|
||||
}
|
||||
result->line = lineNumber;
|
||||
return SOURCE_LABEL;
|
||||
}
|
||||
|
||||
char mnemonic[TOKEN_CAPACITY];
|
||||
if (length >= sizeof(mnemonic)) {
|
||||
return SOURCE_BOUNDARY;
|
||||
}
|
||||
strcpy(mnemonic, token);
|
||||
uppercase(mnemonic);
|
||||
char *selector = strchr(mnemonic, '.');
|
||||
if (selector) {
|
||||
*selector = '\0';
|
||||
}
|
||||
if (getOpcode(mnemonic) == NOT_AN_OPCODE) {
|
||||
return SOURCE_BOUNDARY;
|
||||
}
|
||||
|
||||
memset(result, 0, sizeof(*result));
|
||||
strcpy(result->mnemonic, mnemonic);
|
||||
strncpy(result->spelling, token, sizeof(result->spelling) - 1);
|
||||
result->line = lineNumber;
|
||||
char *operand = strtok(NULL, " \t\r\n");
|
||||
if (operand) {
|
||||
strncpy(result->operand, operand, sizeof(result->operand) - 1);
|
||||
result->hasLiteral = literalValue(operand, &result->literal);
|
||||
}
|
||||
return SOURCE_INSTRUCTION;
|
||||
}
|
||||
|
||||
// ---- Saying that something is deliberate ----
|
||||
//
|
||||
// A line whose comment carries "splitlint: <reason>" is not reported on. The reason is
|
||||
// REQUIRED, and that is the whole design: a suppression with no explanation is a way to
|
||||
// make a tool quiet rather than a way to say something, and this file already has three
|
||||
// real ones - branchTest exists to check that a branch whose outcome is known behaves
|
||||
// correctly, so the diagnostic saying the outcome is known is exactly right and exactly
|
||||
// unwanted.
|
||||
//
|
||||
// Warnings are reported against a line that has already been read, sometimes an earlier
|
||||
// one than the line in hand, so what is kept is the set of lines seen so far.
|
||||
static int *suppressedLines = NULL;
|
||||
static int suppressedCount = 0;
|
||||
static int suppressedRoom = 0;
|
||||
static int suppressionsUsed = 0;
|
||||
|
||||
static void forgetSuppressions(void) {
|
||||
free(suppressedLines);
|
||||
suppressedLines = NULL;
|
||||
suppressedCount = 0;
|
||||
suppressedRoom = 0;
|
||||
}
|
||||
|
||||
static int rememberSuppression(int line) {
|
||||
if (suppressedCount == suppressedRoom) {
|
||||
int room = suppressedRoom ? suppressedRoom * 2 : 16;
|
||||
int *grown = realloc(suppressedLines, (size_t)room * sizeof(int));
|
||||
if (!grown) {
|
||||
return 0;
|
||||
}
|
||||
suppressedLines = grown;
|
||||
suppressedRoom = room;
|
||||
}
|
||||
suppressedLines[suppressedCount++] = line;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int isSuppressed(int line) {
|
||||
for (int i = 0; i < suppressedCount; i++) {
|
||||
if (suppressedLines[i] == line) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The reason has to be there and has to say something. A bare marker is refused rather
|
||||
// than honoured, because a suppression nobody explained is the one that outlives whatever
|
||||
// made it necessary.
|
||||
//
|
||||
// Returns 1 if the line is marked, 0 if it is not, and -1 if it is marked with nothing.
|
||||
static int suppressionOn(const char *rawLine) {
|
||||
const char *at = strstr(rawLine, "splitlint:");
|
||||
if (!at) {
|
||||
return 0;
|
||||
}
|
||||
at += strlen("splitlint:");
|
||||
while (*at == ' ' || *at == '\t') {
|
||||
at++;
|
||||
}
|
||||
return (*at == '\0' || *at == '\n' || *at == '\r') ? -1 : 1;
|
||||
}
|
||||
|
||||
// Returns 1 if the warning was reported and 0 if the line said it was deliberate, so that
|
||||
// a suppressed warning is not counted and --fatal-warnings does not fail on one.
|
||||
static int warning(const char *path, int line, const char *message, const char *help) {
|
||||
if (isSuppressed(line)) {
|
||||
suppressionsUsed++;
|
||||
return 0;
|
||||
}
|
||||
printf("%s:%d: style: %s\n", path, line, message);
|
||||
printf(" help: %s\n", help);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Assignments whose only architectural effect is replacing one operand register. INA,
|
||||
// INB and POP are intentionally absent: even when their result is overwritten, removing
|
||||
// them would leave a byte unread from a device or an entry unconsumed from the stack.
|
||||
static char assignedRegister(const SourceInstruction *instruction) {
|
||||
static const char *aWriters[] = { "RSTA", "INIA", "MVQA", "LDA" };
|
||||
static const char *bWriters[] = { "RSTB", "INIB", "MVQB", "LDB" };
|
||||
for (size_t i = 0; i < sizeof(aWriters) / sizeof(aWriters[0]); i++) {
|
||||
if (strcmp(instruction->mnemonic, aWriters[i]) == 0) {
|
||||
return 'A';
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < sizeof(bWriters) / sizeof(bWriters[0]); i++) {
|
||||
if (strcmp(instruction->mnemonic, bWriters[i]) == 0) {
|
||||
return 'B';
|
||||
}
|
||||
}
|
||||
return '\0';
|
||||
}
|
||||
|
||||
static int stopsFallthrough(const SourceInstruction *instruction) {
|
||||
return strcmp(instruction->mnemonic, "BRI") == 0
|
||||
|| strcmp(instruction->mnemonic, "BRD") == 0
|
||||
|| strcmp(instruction->mnemonic, "RET") == 0
|
||||
|| strcmp(instruction->mnemonic, "RRET") == 0
|
||||
|| strcmp(instruction->mnemonic, "RETI") == 0
|
||||
|| strcmp(instruction->mnemonic, "HALT") == 0;
|
||||
}
|
||||
|
||||
static int isDirectBranch(const SourceInstruction *instruction) {
|
||||
static const char *branches[] = {
|
||||
"BRI", "BRQ", "BRA", "BRB", "BRC", "BNQ", "BNA", "BNB", "BNC"
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(branches) / sizeof(branches[0]); i++) {
|
||||
if (strcmp(instruction->mnemonic, branches[i]) == 0) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void forgetRegisters(KnownRegisters *known) {
|
||||
memset(known, 0, sizeof(*known));
|
||||
}
|
||||
|
||||
static const char *selectorSuffix(const SourceInstruction *instruction) {
|
||||
const char *suffix = strchr(instruction->spelling, '.');
|
||||
return suffix ? suffix : "";
|
||||
}
|
||||
|
||||
static int selectedPointer(const SourceInstruction *instruction) {
|
||||
const char *selector = strchr(instruction->spelling, '.');
|
||||
if (!selector) {
|
||||
return 0;
|
||||
}
|
||||
char *end;
|
||||
long selected = strtol(selector + 1, &end, 10);
|
||||
if (end == selector + 1 || selected < 0 || selected >= DATA_POINTERS) {
|
||||
return 0;
|
||||
}
|
||||
return (int)selected;
|
||||
}
|
||||
|
||||
static void forgetPointers(KnownPointer pointers[DATA_POINTERS]) {
|
||||
memset(pointers, 0, sizeof(*pointers) * DATA_POINTERS);
|
||||
}
|
||||
|
||||
static int lintKnownPointers(const char *path, const SourceInstruction *instruction,
|
||||
const KnownPointer pointers[DATA_POINTERS]) {
|
||||
if (strcmp(instruction->mnemonic, "SETD") != 0 || !instruction->operand[0]) {
|
||||
return 0;
|
||||
}
|
||||
int selected = selectedPointer(instruction);
|
||||
const KnownPointer *pointer = &pointers[selected];
|
||||
if (!pointer->known || pointer->offset != 0
|
||||
|| strcmp(pointer->base, instruction->operand) != 0) {
|
||||
return 0;
|
||||
}
|
||||
char message[180];
|
||||
snprintf(message, sizeof(message), "DP%d is already known to hold %s",
|
||||
selected, instruction->operand);
|
||||
return warning(path, instruction->line, message, "remove the redundant SETD");
|
||||
}
|
||||
|
||||
static void moveKnownPointer(KnownPointer *pointer, uint16_t amount, int downward) {
|
||||
if (!pointer->known) {
|
||||
return;
|
||||
}
|
||||
pointer->offset = downward ? (uint16_t)(pointer->offset - amount)
|
||||
: (uint16_t)(pointer->offset + amount);
|
||||
}
|
||||
|
||||
static void updateKnownPointers(const SourceInstruction *instruction,
|
||||
const KnownRegisters *registers,
|
||||
KnownPointer pointers[DATA_POINTERS]) {
|
||||
int selected = selectedPointer(instruction);
|
||||
KnownPointer *pointer = &pointers[selected];
|
||||
|
||||
if (strcmp(instruction->mnemonic, "SETD") == 0) {
|
||||
if (instruction->operand[0]) {
|
||||
pointer->known = 1;
|
||||
strncpy(pointer->base, instruction->operand, sizeof(pointer->base) - 1);
|
||||
pointer->base[sizeof(pointer->base) - 1] = '\0';
|
||||
pointer->offset = 0;
|
||||
} else {
|
||||
pointer->known = 0;
|
||||
}
|
||||
} else if (strcmp(instruction->mnemonic, "INCD") == 0) {
|
||||
moveKnownPointer(pointer, 1, 0);
|
||||
} else if (strcmp(instruction->mnemonic, "DECD") == 0) {
|
||||
moveKnownPointer(pointer, 1, 1);
|
||||
} else if (strcmp(instruction->mnemonic, "DPUP") == 0 && instruction->hasLiteral) {
|
||||
moveKnownPointer(pointer, (uint16_t)instruction->literal, 0);
|
||||
} else if (strcmp(instruction->mnemonic, "DPDN") == 0 && instruction->hasLiteral) {
|
||||
moveKnownPointer(pointer, (uint16_t)instruction->literal, 1);
|
||||
} else if (strcmp(instruction->mnemonic, "DPUA") == 0 && registers->aKnown) {
|
||||
moveKnownPointer(pointer, registers->a, 0);
|
||||
} else if (strcmp(instruction->mnemonic, "DPDA") == 0 && registers->aKnown) {
|
||||
moveKnownPointer(pointer, registers->a, 1);
|
||||
} else if (strcmp(instruction->mnemonic, "DPUW") == 0
|
||||
&& registers->aKnown && registers->bKnown) {
|
||||
moveKnownPointer(pointer, ((uint16_t)registers->a << 8) | registers->b, 0);
|
||||
} else if (strcmp(instruction->mnemonic, "DPDW") == 0
|
||||
&& registers->aKnown && registers->bKnown) {
|
||||
moveKnownPointer(pointer, ((uint16_t)registers->a << 8) | registers->b, 1);
|
||||
} else if (strcmp(instruction->mnemonic, "LDD") == 0
|
||||
|| strcmp(instruction->mnemonic, "POPD") == 0
|
||||
|| strcmp(instruction->mnemonic, "MVSD") == 0) {
|
||||
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) {
|
||||
forgetPointers(pointers);
|
||||
}
|
||||
}
|
||||
|
||||
static int lintKnownRegisters(const char *path, const SourceInstruction *instruction,
|
||||
const SourceInstruction *previous,
|
||||
const KnownRegisters *known) {
|
||||
int warnings = 0;
|
||||
char message[180];
|
||||
char help[160];
|
||||
char previousAssignment = assignedRegister(previous);
|
||||
|
||||
if (known->aKnown && previousAssignment != 'A'
|
||||
&& ((strcmp(instruction->mnemonic, "RSTA") == 0 && known->a == 0)
|
||||
|| (strcmp(instruction->mnemonic, "INIA") == 0
|
||||
&& instruction->hasLiteral && known->a == instruction->literal))) {
|
||||
snprintf(message, sizeof(message), "%s leaves A at its known value of %u",
|
||||
instruction->spelling, known->a);
|
||||
warnings += warning(path, instruction->line, message, "remove the redundant assignment");
|
||||
}
|
||||
if (known->bKnown && previousAssignment != 'B'
|
||||
&& ((strcmp(instruction->mnemonic, "RSTB") == 0 && known->b == 0)
|
||||
|| (strcmp(instruction->mnemonic, "INIB") == 0
|
||||
&& instruction->hasLiteral && known->b == instruction->literal))) {
|
||||
snprintf(message, sizeof(message), "%s leaves B at its known value of %u",
|
||||
instruction->spelling, known->b);
|
||||
warnings += warning(path, instruction->line, message, "remove the redundant assignment");
|
||||
}
|
||||
|
||||
if (known->aKnown && (strcmp(instruction->mnemonic, "DPUA") == 0
|
||||
|| strcmp(instruction->mnemonic, "DPDA") == 0)) {
|
||||
if (known->a == 0) {
|
||||
warnings += warning(path, instruction->line, "pointer offset is known to be zero",
|
||||
"remove the pointer instruction");
|
||||
} else if (known->a == 1) {
|
||||
snprintf(help, sizeof(help), "use %s%s",
|
||||
strcmp(instruction->mnemonic, "DPUA") == 0 ? "INCD" : "DECD",
|
||||
selectorSuffix(instruction));
|
||||
warnings += warning(path, instruction->line, "pointer offset is known to be one", help);
|
||||
}
|
||||
}
|
||||
|
||||
if (known->aKnown && known->bKnown
|
||||
&& (strcmp(instruction->mnemonic, "DPUW") == 0
|
||||
|| strcmp(instruction->mnemonic, "DPDW") == 0)) {
|
||||
uint16_t word = ((uint16_t)known->a << 8) | known->b;
|
||||
if (word == 0) {
|
||||
warnings += warning(path, instruction->line, "word-sized pointer offset is known to be zero",
|
||||
"remove the pointer instruction");
|
||||
} else if (word == 1) {
|
||||
snprintf(help, sizeof(help), "use %s%s",
|
||||
strcmp(instruction->mnemonic, "DPUW") == 0 ? "INCD" : "DECD",
|
||||
selectorSuffix(instruction));
|
||||
warnings += warning(path, instruction->line, "word-sized pointer offset is known to be one", help);
|
||||
}
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
static void updateKnownRegisters(const SourceInstruction *instruction,
|
||||
KnownRegisters *known) {
|
||||
if (strcmp(instruction->mnemonic, "SHL") == 0
|
||||
|| strcmp(instruction->mnemonic, "SHR") == 0) {
|
||||
if (known->aKnown && known->bKnown) {
|
||||
uint16_t word = ((uint16_t)known->a << 8) | known->b;
|
||||
if (strcmp(instruction->mnemonic, "SHL") == 0) {
|
||||
word = (uint16_t)((word << 1) | (word >> 15));
|
||||
} else {
|
||||
word = (uint16_t)((word >> 1) | (word << 15));
|
||||
}
|
||||
known->a = (uint8_t)(word >> 8);
|
||||
known->b = (uint8_t)word;
|
||||
} else {
|
||||
known->aKnown = 0;
|
||||
known->bKnown = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(instruction->mnemonic, "RSTA") == 0) {
|
||||
known->aKnown = 1;
|
||||
known->a = 0;
|
||||
} else if (strcmp(instruction->mnemonic, "INIA") == 0 && instruction->hasLiteral) {
|
||||
known->aKnown = 1;
|
||||
known->a = (uint8_t)instruction->literal;
|
||||
} else if (strcmp(instruction->mnemonic, "INCA") == 0) {
|
||||
if (known->aKnown) known->a++;
|
||||
} else if (strcmp(instruction->mnemonic, "DECA") == 0) {
|
||||
if (known->aKnown) known->a--;
|
||||
} else if (strcmp(instruction->mnemonic, "MVQA") == 0
|
||||
|| strcmp(instruction->mnemonic, "LDA") == 0
|
||||
|| strcmp(instruction->mnemonic, "POPA") == 0
|
||||
|| strcmp(instruction->mnemonic, "INA") == 0) {
|
||||
known->aKnown = 0;
|
||||
}
|
||||
|
||||
if (strcmp(instruction->mnemonic, "RSTB") == 0) {
|
||||
known->bKnown = 1;
|
||||
known->b = 0;
|
||||
} else if (strcmp(instruction->mnemonic, "INIB") == 0 && instruction->hasLiteral) {
|
||||
known->bKnown = 1;
|
||||
known->b = (uint8_t)instruction->literal;
|
||||
} else if (strcmp(instruction->mnemonic, "INCB") == 0) {
|
||||
if (known->bKnown) known->b++;
|
||||
} else if (strcmp(instruction->mnemonic, "DECB") == 0) {
|
||||
if (known->bKnown) known->b--;
|
||||
} else if (strcmp(instruction->mnemonic, "MVQB") == 0
|
||||
|| strcmp(instruction->mnemonic, "LDB") == 0
|
||||
|| strcmp(instruction->mnemonic, "POPB") == 0
|
||||
|| strcmp(instruction->mnemonic, "INB") == 0) {
|
||||
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
|
||||
|| strcmp(instruction->mnemonic, "SWI") == 0) {
|
||||
forgetRegisters(known);
|
||||
}
|
||||
}
|
||||
|
||||
static int lintKnownCarry(const char *path, const SourceInstruction *instruction,
|
||||
KnownCarry carry) {
|
||||
if (carry == CARRY_UNKNOWN) {
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(instruction->mnemonic, "CCF") == 0 && carry == CARRY_CLEAR) {
|
||||
return warning(path, instruction->line, "carry is already known to be clear",
|
||||
"remove the redundant CCF");
|
||||
}
|
||||
if (strcmp(instruction->mnemonic, "BRC") == 0) {
|
||||
if (carry == CARRY_SET) {
|
||||
return warning(path, instruction->line, "BRC is always taken because carry is known set",
|
||||
"use BRI");
|
||||
}
|
||||
return warning(path, instruction->line, "BRC is never taken because carry is known clear",
|
||||
"remove the branch");
|
||||
}
|
||||
if (strcmp(instruction->mnemonic, "BNC") == 0) {
|
||||
if (carry == CARRY_CLEAR) {
|
||||
return warning(path, instruction->line, "BNC is always taken because carry is known clear",
|
||||
"use BRI");
|
||||
}
|
||||
return warning(path, instruction->line, "BNC is never taken because carry is known set",
|
||||
"remove the branch");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void updateKnownCarry(const SourceInstruction *instruction,
|
||||
const KnownRegisters *registers,
|
||||
KnownCarry *carry) {
|
||||
if (strcmp(instruction->mnemonic, "CCF") == 0) {
|
||||
*carry = CARRY_CLEAR;
|
||||
} else if (strcmp(instruction->mnemonic, "INCA") == 0) {
|
||||
*carry = registers->aKnown ? (registers->a == 0xFF ? CARRY_SET : CARRY_CLEAR)
|
||||
: CARRY_UNKNOWN;
|
||||
} else if (strcmp(instruction->mnemonic, "INCB") == 0) {
|
||||
*carry = registers->bKnown ? (registers->b == 0xFF ? CARRY_SET : CARRY_CLEAR)
|
||||
: CARRY_UNKNOWN;
|
||||
} else if (strcmp(instruction->mnemonic, "DECA") == 0) {
|
||||
*carry = registers->aKnown ? (registers->a == 0 ? CARRY_SET : CARRY_CLEAR)
|
||||
: CARRY_UNKNOWN;
|
||||
} else if (strcmp(instruction->mnemonic, "DECB") == 0) {
|
||||
*carry = registers->bKnown ? (registers->b == 0 ? CARRY_SET : CARRY_CLEAR)
|
||||
: CARRY_UNKNOWN;
|
||||
} else if (strcmp(instruction->mnemonic, "ADD") == 0) {
|
||||
if (registers->aKnown && registers->bKnown && *carry != CARRY_UNKNOWN) {
|
||||
unsigned result = registers->a + registers->b + (unsigned)*carry;
|
||||
*carry = result > 255 ? CARRY_SET : CARRY_CLEAR;
|
||||
} else {
|
||||
*carry = CARRY_UNKNOWN;
|
||||
}
|
||||
} else if (strcmp(instruction->mnemonic, "SUB") == 0) {
|
||||
if (registers->aKnown && registers->bKnown && *carry != CARRY_UNKNOWN) {
|
||||
int result = (int)registers->a - (int)registers->b - (int)*carry;
|
||||
*carry = result < 0 ? CARRY_SET : CARRY_CLEAR;
|
||||
} else {
|
||||
*carry = CARRY_UNKNOWN;
|
||||
}
|
||||
} else if (strcmp(instruction->mnemonic, "CALL") == 0
|
||||
|| strcmp(instruction->mnemonic, "RCAL") == 0
|
||||
|| strcmp(instruction->mnemonic, "SWI") == 0
|
||||
|| strcmp(instruction->mnemonic, "RET") == 0
|
||||
|| strcmp(instruction->mnemonic, "RETI") == 0) {
|
||||
*carry = CARRY_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
static int lintInstruction(const char *path, const SourceInstruction *previous,
|
||||
const SourceInstruction *current) {
|
||||
int warnings = 0;
|
||||
|
||||
if (current->hasLiteral && current->literal == 0
|
||||
&& strcmp(current->mnemonic, "INIA") == 0) {
|
||||
warnings += warning(path, current->line, "loading zero into A takes two bytes", "use RSTA");
|
||||
} else if (current->hasLiteral && current->literal == 0
|
||||
&& strcmp(current->mnemonic, "INIB") == 0) {
|
||||
warnings += warning(path, current->line, "loading zero into B takes two bytes", "use RSTB");
|
||||
}
|
||||
|
||||
char previousAssignment = assignedRegister(previous);
|
||||
if (previousAssignment && previousAssignment == assignedRegister(current)) {
|
||||
char message[180];
|
||||
snprintf(message, sizeof(message), "%s assigns %c, but %s replaces it immediately",
|
||||
previous->spelling, previousAssignment, current->spelling);
|
||||
warnings += warning(path, previous->line, message, "remove the first assignment");
|
||||
}
|
||||
|
||||
// Q already has direct moves to both operand registers. The older stack transfer
|
||||
// idiom does the same job, but costs a write, a read, and an extra instruction. This
|
||||
// is precisely the kind of sequence the linter exists to find in code written before
|
||||
// the newer instruction was available.
|
||||
if (strcmp(previous->mnemonic, "PSHQ") == 0
|
||||
&& strcmp(current->mnemonic, "POPA") == 0) {
|
||||
warnings += warning(path, previous->line, "moving Q to A through the stack takes two instructions",
|
||||
"use MVQA");
|
||||
} else if (strcmp(previous->mnemonic, "PSHQ") == 0
|
||||
&& strcmp(current->mnemonic, "POPB") == 0) {
|
||||
warnings += warning(path, previous->line, "moving Q to B through the stack takes two instructions",
|
||||
"use MVQB");
|
||||
} else if (strcmp(previous->mnemonic, "PSHA") == 0
|
||||
&& strcmp(current->mnemonic, "POPA") == 0) {
|
||||
warnings += warning(path, previous->line, "pushing A and immediately restoring it leaves A unchanged",
|
||||
"remove both instructions if the stack write is not intentional");
|
||||
} else if (strcmp(previous->mnemonic, "PSHB") == 0
|
||||
&& strcmp(current->mnemonic, "POPB") == 0) {
|
||||
warnings += warning(path, previous->line, "pushing B and immediately restoring it leaves B unchanged",
|
||||
"remove both instructions if the stack write is not intentional");
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
static int lintFile(const char *path) {
|
||||
FILE *file = fopen(path, "r");
|
||||
if (!file) {
|
||||
fprintf(stderr, "%s: error: could not open source file\n", path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char line[LINE_CAPACITY];
|
||||
int lineNumber = 0;
|
||||
int warnings = 0;
|
||||
int havePrevious = 0;
|
||||
int fallthroughStopped = 0;
|
||||
int havePendingBranch = 0;
|
||||
SourceInstruction pendingBranch;
|
||||
KnownRegisters known = {0};
|
||||
KnownPointer pointers[DATA_POINTERS] = {0};
|
||||
KnownCarry carry = CARRY_UNKNOWN;
|
||||
SourceInstruction previous;
|
||||
forgetSuppressions();
|
||||
while (fgets(line, sizeof(line), file)) {
|
||||
lineNumber++;
|
||||
// Refuse to silently analyze only the first part of an unusually long line.
|
||||
if (!strchr(line, '\n') && !feof(file)) {
|
||||
fprintf(stderr, "%s:%d: error: line is longer than %d characters\n",
|
||||
path, lineNumber, LINE_CAPACITY - 2);
|
||||
fclose(file);
|
||||
return -1;
|
||||
}
|
||||
// BEFORE THE COMMENT IS STRIPPED, because the marker lives in one. Recorded for
|
||||
// the line rather than acted on here: a warning is reported against a line that
|
||||
// has already been read, and not always the one in hand.
|
||||
int marked = suppressionOn(line);
|
||||
if (marked < 0) {
|
||||
fprintf(stderr, "%s:%d: error: splitlint: needs a reason after it\n",
|
||||
path, lineNumber);
|
||||
fclose(file);
|
||||
forgetSuppressions();
|
||||
return -1;
|
||||
}
|
||||
if (marked && !rememberSuppression(lineNumber)) {
|
||||
fprintf(stderr, "%s: error: out of memory recording a suppression\n", path);
|
||||
fclose(file);
|
||||
forgetSuppressions();
|
||||
return -1;
|
||||
}
|
||||
|
||||
SourceInstruction current;
|
||||
SourceLine sourceLine = parseInstruction(line, lineNumber, ¤t);
|
||||
if (sourceLine != SOURCE_INSTRUCTION) {
|
||||
havePrevious = 0;
|
||||
if (sourceLine == SOURCE_LABEL) {
|
||||
if (havePendingBranch && current.spelling[0]
|
||||
&& strcmp(pendingBranch.operand, current.spelling) == 0) {
|
||||
warnings += warning(path, pendingBranch.line,
|
||||
"branch target is the next labeled address",
|
||||
"remove the branch");
|
||||
havePendingBranch = 0;
|
||||
}
|
||||
fallthroughStopped = 0;
|
||||
forgetRegisters(&known);
|
||||
forgetPointers(pointers);
|
||||
carry = CARRY_UNKNOWN;
|
||||
} else if (sourceLine == SOURCE_BOUNDARY) {
|
||||
fallthroughStopped = 0;
|
||||
havePendingBranch = 0;
|
||||
forgetRegisters(&known);
|
||||
forgetPointers(pointers);
|
||||
carry = CARRY_UNKNOWN;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Reaching another instruction before the target label makes this something
|
||||
// other than a branch to the next address.
|
||||
havePendingBranch = 0;
|
||||
if (fallthroughStopped) {
|
||||
warnings += warning(path, current.line,
|
||||
"no ordinary fallthrough reaches this instruction",
|
||||
"remove it, or give an intentional indirect entry point a label");
|
||||
}
|
||||
SourceInstruction empty = {0};
|
||||
warnings += lintKnownRegisters(path, ¤t,
|
||||
havePrevious ? &previous : &empty, &known);
|
||||
warnings += lintKnownPointers(path, ¤t, pointers);
|
||||
warnings += lintKnownCarry(path, ¤t, carry);
|
||||
warnings += lintInstruction(path, havePrevious ? &previous : &empty, ¤t);
|
||||
if (stopsFallthrough(¤t)) {
|
||||
fallthroughStopped = 1;
|
||||
}
|
||||
if (isDirectBranch(¤t) && current.operand[0]) {
|
||||
pendingBranch = current;
|
||||
havePendingBranch = 1;
|
||||
}
|
||||
updateKnownPointers(¤t, &known, pointers);
|
||||
updateKnownCarry(¤t, &known, &carry);
|
||||
updateKnownRegisters(¤t, &known);
|
||||
previous = current;
|
||||
havePrevious = 1;
|
||||
}
|
||||
if (ferror(file)) {
|
||||
fprintf(stderr, "%s: error: could not read source file\n", path);
|
||||
fclose(file);
|
||||
forgetSuppressions();
|
||||
return -1;
|
||||
}
|
||||
fclose(file);
|
||||
forgetSuppressions();
|
||||
return warnings;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int fatalWarnings = 0;
|
||||
int firstFile = 1;
|
||||
if (argc > 1 && strcmp(argv[1], "--fatal-warnings") == 0) {
|
||||
fatalWarnings = 1;
|
||||
firstFile++;
|
||||
}
|
||||
if (argc <= firstFile || strcmp(argv[firstFile], "--help") == 0
|
||||
|| strcmp(argv[firstFile], "-h") == 0) {
|
||||
usage(argv[0]);
|
||||
return argc <= firstFile ? 1 : 0;
|
||||
}
|
||||
|
||||
int warnings = 0;
|
||||
for (int i = firstFile; i < argc; i++) {
|
||||
int found = lintFile(argv[i]);
|
||||
if (found < 0) {
|
||||
return 2;
|
||||
}
|
||||
warnings += found;
|
||||
}
|
||||
if (warnings) {
|
||||
printf("%d style warning%s found.\n", warnings, warnings == 1 ? "" : "s");
|
||||
}
|
||||
// Said out loud rather than left implicit. A suppression is a claim that something is
|
||||
// deliberate, and a count of them is how anybody notices the claim has spread.
|
||||
if (suppressionsUsed) {
|
||||
printf("%d warning%s suppressed by splitlint comments.\n",
|
||||
suppressionsUsed, suppressionsUsed == 1 ? "" : "s");
|
||||
}
|
||||
return fatalWarnings && warnings ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user