// 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 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 ...]\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: \" 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: " 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; } // ---- 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, 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 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, 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; }