// 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 #include #include #include #include "../Assembler/assembly.h" #include "../Emulator/cpu.h" #define LINE_CAPACITY 1024 #define TOKEN_CAPACITY 64 #define RULE_CAPACITY 32 // Every rule this knows, named once. The count in the clean run message comes from here, // so a rule added without a name here is a rule the summary does not know about. static const char *const ruleNames[] = { "redundant-setd", "redundant-assignment", "pointer-offset", "redundant-ccf", "known-branch", "zero-load", "dead-assignment", "q-through-stack", "self-push-pop", "branch-to-next", "unreachable", "dead-suppression", "falls-into-subroutine", }; #define RULE_COUNT ((int)(sizeof(ruleNames) / sizeof(ruleNames[0]))) 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 [options] [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(" --fatal-warnings Exit non-zero if anything is reported.\n"); printf(" --machine One tab separated line per warning and nothing else:\n"); printf(" file, line, rule, message, help.\n"); printf("\n"); printf("A line whose comment says \"splitlint: \" is not reported on, and the\n"); printf("reason is required, so that a deliberate exception says what makes it one.\n"); printf("\"splitlint[rule]: \" silences one rule and leaves the line honest\n"); printf("about the others. A marker that silences nothing is itself reported.\n"); printf("\n"); printf("Rules:"); for (int i = 0; i < RULE_COUNT; i++) { printf("%s %s", i && i % 4 == 0 ? "\n " : "", ruleNames[i]); } printf("\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: " is not reported on, and the reason is // REQUIRED: a suppression with no explanation is a way to make a tool quiet rather than a // way to say something. "splitlint[rule]: " silences one rule and leaves the line // honest about every other, which matters once a line can trip more than one. // // 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. typedef struct { int line; char rule[RULE_CAPACITY]; // Empty for "every rule on this line". int used; } Suppression; static Suppression *suppressions = NULL; static int suppressedCount = 0; static int suppressedRoom = 0; static int suppressionsUsed = 0; static int machineReadable = 0; static void forgetSuppressions(void) { free(suppressions); suppressions = NULL; suppressedCount = 0; suppressedRoom = 0; } static int rememberSuppression(int line, const char *rule) { if (suppressedCount == suppressedRoom) { int room = suppressedRoom ? suppressedRoom * 2 : 16; Suppression *grown = realloc(suppressions, (size_t)room * sizeof(Suppression)); if (!grown) { return 0; } suppressions = grown; suppressedRoom = room; } suppressions[suppressedCount].line = line; suppressions[suppressedCount].used = 0; snprintf(suppressions[suppressedCount].rule, RULE_CAPACITY, "%s", rule ? rule : ""); suppressedCount++; return 1; } static int isSuppressed(int line, const char *rule) { for (int i = 0; i < suppressedCount; i++) { if (suppressions[i].line != line) { continue; } if (suppressions[i].rule[0] != '\0' && strcmp(suppressions[i].rule, rule) != 0) { continue; // Names a different rule, so this one still applies. } suppressions[i].used = 1; suppressionsUsed++; return 1; } return 0; } // A suppression that no longer suppresses anything is the thing the required reason was // meant to prevent: an exception that outlived whatever made it necessary. Saying so is // what keeps the count of them meaningful. static int reportDeadSuppressions(const char *path) { int dead = 0; for (int i = 0; i < suppressedCount; i++) { if (suppressions[i].used) { continue; } if (machineReadable) { printf("%s\t%d\t%s\t%s\t%s\n", path, suppressions[i].line, "dead-suppression", "this line no longer has a warning to suppress", "remove the splitlint comment"); } else { printf("%s:%d: style: this line no longer has a warning to suppress" " [dead-suppression]\n", path, suppressions[i].line); printf(" help: remove the splitlint comment\n"); } dead++; } return dead; } // 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. // A rule name in brackets, if there is one, is copied into `rule`. static int suppressionOn(const char *rawLine, char *rule) { rule[0] = '\0'; const char *at = strstr(rawLine, "splitlint"); if (!at) { return 0; } at += strlen("splitlint"); if (*at == '[') { const char *close = strchr(at, ']'); if (!close || close == at + 1 || (size_t)(close - at - 1) >= RULE_CAPACITY) { return -1; } memcpy(rule, at + 1, (size_t)(close - at - 1)); rule[close - at - 1] = '\0'; at = close + 1; } if (*at != ':') { return 0; // "splitlint" inside ordinary prose is not a marker. } at++; while (*at == ' ' || *at == '\t') { at++; } return (*at == '\0' || *at == '\n' || *at == '\r') ? -1 : 1; } static int warning(const char *path, const char *rule, int line, const char *message, const char *help) { if (isSuppressed(line, rule)) { return 0; } if (machineReadable) { // One line, tab separated, so nothing downstream has to read prose. This file's // own diagnostics were parsed with regular expressions three times in a day // before it had a shape anything could rely on. printf("%s\t%d\t%s\t%s\t%s\n", path, line, rule, message, help); return 1; } // The rule goes in brackets at the end, the way a compiler names the flag that // produced a warning, so that the sentence still reads as a sentence. printf("%s:%d: style: %s [%s]\n", path, line, message, rule); 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 // SRET was missing here, which is a gap in every rule that asks what reaches an // instruction: it returns from a handler exactly as RET returns from a call. || strcmp(instruction->mnemonic, "SRET") == 0 || strcmp(instruction->mnemonic, "HALT") == 0 // ---- The one trap that does not come back ---- // // A SWI is a call and almost every one of them returns, so a SWI does not end a run // of code. osExit is the exception and the exception matters: it is how a loaded // program gives the machine back, so nothing after it runs, and every program on // this machine ends with it and then writes its helper routines underneath. // // NAMED RATHER THAN NUMBERED, because that is what the source says. This is the only // name from the system that this tool knows, and it is here because without it the // rule below would report every well written program in the repository. || (strcmp(instruction->mnemonic, "SWI") == 0 && strcmp(instruction->operand, "osExit") == 0); } // ---- Which labels are entered by a CALL ---- // // Collected in a pass of its own before anything is judged, because the rule below asks a // question about a label that cannot be answered until the whole file has been read: a // subroutine is very often called from further down than it is written. #define CALL_TARGET_CAPACITY 2048 static char callTargets[CALL_TARGET_CAPACITY][TOKEN_CAPACITY]; static int callTargetCount = 0; static int isCallTarget(const char *name) { for (int i = 0; i < callTargetCount; i++) { if (strcmp(callTargets[i], name) == 0) { return 1; } } return 0; } // Reads the file once for nothing but CALL and RCAL operands. Answers 0 if there were more // distinct ones than there is room for, which is reported rather than quietly truncated: a // rule that silently forgot half its input would go quiet instead of wrong, which is worse. static int collectCallTargets(const char *path) { callTargetCount = 0; FILE *file = fopen(path, "r"); if (!file) { return 1; // The caller opens it again and reports the failure properly. } char line[LINE_CAPACITY]; int lineNumber = 0; while (fgets(line, sizeof(line), file)) { lineNumber++; SourceInstruction current; if (parseInstruction(line, lineNumber, ¤t) != SOURCE_INSTRUCTION) { continue; } if (strcmp(current.mnemonic, "CALL") != 0 && strcmp(current.mnemonic, "RCAL") != 0) { continue; } if (!current.operand[0] || isCallTarget(current.operand)) { continue; } if (callTargetCount >= CALL_TARGET_CAPACITY) { fclose(file); fprintf(stderr, "%s: error: more than %d distinct call targets\n", path, CALL_TARGET_CAPACITY); return 0; } snprintf(callTargets[callTargetCount], TOKEN_CAPACITY, "%s", current.operand); callTargetCount++; } fclose(file); return 1; } 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, "redundant-setd", 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; } // ---- 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); } } 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, "redundant-assignment", 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, "redundant-assignment", 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, "pointer-offset", 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, "pointer-offset", 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, "pointer-offset", 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, "pointer-offset", 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 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); } } 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, "redundant-ccf", 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, "known-branch", instruction->line, "BRC is always taken because carry is known set", "use BRI"); } return warning(path, "known-branch", 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, "known-branch", instruction->line, "BNC is always taken because carry is known clear", "use BRI"); } return warning(path, "known-branch", 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, "zero-load", 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, "zero-load", 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, "dead-assignment", 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, "q-through-stack", 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, "q-through-stack", 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, "self-push-pop", 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, "self-push-pop", 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; } if (!collectCallTargets(path)) { fclose(file); return -1; } char line[LINE_CAPACITY]; int lineNumber = 0; int warnings = 0; int havePrevious = 0; int fallthroughStopped = 0; int havePendingBranch = 0; // Whether anything at all has been read since the last #Program or #Data, and whether // the run of code we are in began at a label something calls. See the rule below. int sawInstruction = 0; int runHasCallTarget = 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. char suppressedRule[RULE_CAPACITY]; int marked = suppressionOn(line, suppressedRule); 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, suppressedRule)) { 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, "branch-to-next", pendingBranch.line, "branch target is the next labeled address", "remove the branch"); havePendingBranch = 0; } // ---- Walking into a subroutine instead of calling it ---- // // The code above ends without going anywhere, and the label below is one // something CALLs. So execution walks into the subroutine, reaches its RET, // and returns to whatever the Stack happens to hold - which is not a caller, // because nobody called. It goes somewhere nobody named. // // THAT IS WHAT FORMATTED A DISK. CosmOS's monitor had no branch at the end // of its command list, so an unrecognised word walked into sayPrompt; the // RET at the bottom of it went to whatever was on the Stack, and one of the // places that turned out to be was inside the filesystem's format routine. // The symptom is nowhere near the cause and changes with the Stack, which is // exactly the kind of fault worth spending a rule on. // // UNLESS THE RUN WE ARE IN WAS ITSELF CALLED. Falling out of one subroutine // into another is an ordinary tail call: the RET returns to the outer // caller, which is real and is what the author meant. So this only fires // when nothing since the last RET or branch was a call target either. if (!fallthroughStopped && sawInstruction && !runHasCallTarget && current.spelling[0] && isCallTarget(current.spelling)) { warnings += warning(path, "falls-into-subroutine", current.line, "execution walks into a subroutine nothing here called", "branch past it, or end the code above with a branch or a return"); } if (current.spelling[0] && isCallTarget(current.spelling)) { runHasCallTarget = 1; } fallthroughStopped = 0; forgetRegisters(&known); forgetPointers(pointers); carry = CARRY_UNKNOWN; } else if (sourceLine == SOURCE_BOUNDARY) { fallthroughStopped = 0; havePendingBranch = 0; sawInstruction = 0; runHasCallTarget = 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, "unreachable", 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); sawInstruction = 1; if (stopsFallthrough(¤t)) { fallthroughStopped = 1; // A new run of code starts after this, and it has not been called yet. runHasCallTarget = 0; } 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); warnings += reportDeadSuppressions(path); forgetSuppressions(); return warnings; } int main(int argc, char **argv) { int fatalWarnings = 0; int firstFile = 1; while (firstFile < argc && argv[firstFile][0] == '-' && argv[firstFile][1] == '-') { if (strcmp(argv[firstFile], "--fatal-warnings") == 0) { fatalWarnings = 1; } else if (strcmp(argv[firstFile], "--machine") == 0) { machineReadable = 1; } else if (strcmp(argv[firstFile], "--help") == 0) { usage(argv[0]); return 0; } else { fprintf(stderr, "%s: error: unknown option \"%s\"\n", argv[0], argv[firstFile]); return 2; } firstFile++; } if (argc <= firstFile || strcmp(argv[firstFile], "-h") == 0) { usage(argv[0]); return argc <= firstFile ? 1 : 0; } int warnings = 0; int files = 0; for (int i = firstFile; i < argc; i++) { int found = lintFile(argv[i]); if (found < 0) { return 2; } warnings += found; files++; } // Machine readable output is warnings and nothing else, so that a reader can take // every line the same way rather than having to know which ones are prose. if (!machineReadable) { if (warnings) { printf("%d style warning%s found.\n", warnings, warnings == 1 ? "" : "s"); } else { // SAYING SO IS THE POINT. A tool that exits silently has not told you it // found nothing, it has told you nothing at all - and the two look identical // from outside. This says what it looked at and what it looked for. printf("No style warnings: %d file%s checked against %d rule%s.\n", files, files == 1 ? "" : "s", RULE_COUNT, RULE_COUNT == 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; }