From ce9f0d32c33333161cf9a5c2b7446d80fa0f13b5 Mon Sep 17 00:00:00 2001 From: hachem Date: Mon, 31 Aug 2026 02:01:44 +0200 Subject: feat: add diagnostics module with source-anchored caret errors --- src/diag/diag.c | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/diag/diag.h | 12 ++++++++++ src/io/file.c | 18 ++++++++++----- src/lexer/lexer.c | 1 + src/lexer/lexer.h | 1 + src/main.c | 11 +++++++--- src/parser/parser.c | 6 ++++- src/sema/sema.c | 22 +++++++++++-------- src/sema/sema.h | 3 ++- tests/sema_test.c | 3 ++- 10 files changed, 120 insertions(+), 20 deletions(-) create mode 100644 src/diag/diag.c create mode 100644 src/diag/diag.h diff --git a/src/diag/diag.c b/src/diag/diag.c new file mode 100644 index 0000000..cc61e89 --- /dev/null +++ b/src/diag/diag.c @@ -0,0 +1,63 @@ +#include +#include +#include + +#include "diag/diag.h" + +static bool colored(void) +{ + return getenv("NO_COLOR") == NULL; +} + +void report_error(struct Source source, struct Token token, const char* message) +{ + const char* name = source.name != NULL ? source.name : ""; + const char* text = source.text; + + const char* line_start = token.start; + while (line_start > text && line_start[-1] != '\n') + line_start -= 1; + + const char* line_end = token.start; + while (*line_end != '\0' && *line_end != '\n') + line_end += 1; + + uint32_t column = (uint32_t)(token.start - line_start) + 1; + + size_t span = token.length > 0 ? token.length : 1; + if ((size_t)(line_end - token.start) < span) + span = (size_t)(line_end - token.start); + if (span == 0) + span = 1; + + const char* bold = colored() ? "\033[1m" : ""; + const char* red = colored() ? "\033[31m" : ""; + const char* blue = colored() ? "\033[34m" : ""; + const char* reset = colored() ? "\033[0m" : ""; + + int gutter = snprintf(NULL, 0, "%u", token.line); + + fprintf(stderr, "%s%serror:%s %s%s%s\n", bold, red, reset, bold, message, reset); + fprintf(stderr, "%*s %s-->%s %s:%u:%u\n", gutter, "", blue, reset, name, token.line, column); + fprintf(stderr, "%*s %s|%s\n", gutter, "", blue, reset); + fprintf(stderr, "%s%u%s %s|%s %.*s\n", blue, token.line, reset, blue, reset, + (int)(line_end - line_start), line_start); + fprintf(stderr, "%*s %s|%s ", gutter, "", blue, reset); + + for (const char* character = line_start; character < token.start; character += 1) + fputc(*character == '\t' ? '\t' : ' ', stderr); + + fprintf(stderr, "%s", red); + for (size_t i = 0; i < span; i += 1) + fputc('^', stderr); + fprintf(stderr, "%s\n", reset); +} + +void report_error_message(const char* message) +{ + const char* bold = colored() ? "\033[1m" : ""; + const char* red = colored() ? "\033[31m" : ""; + const char* reset = colored() ? "\033[0m" : ""; + + fprintf(stderr, "%s%serror:%s %s%s%s\n", bold, red, reset, bold, message, reset); +} diff --git a/src/diag/diag.h b/src/diag/diag.h new file mode 100644 index 0000000..7a99266 --- /dev/null +++ b/src/diag/diag.h @@ -0,0 +1,12 @@ +#pragma once + +#include "lexer/lexer.h" + +struct Source +{ + const char* name; + const char* text; +}; + +void report_error(struct Source source, struct Token token, const char* message); +void report_error_message(const char* message); diff --git a/src/io/file.c b/src/io/file.c index 8ce8eef..3d7083c 100644 --- a/src/io/file.c +++ b/src/io/file.c @@ -1,20 +1,28 @@ #include #include +#include "diag/diag.h" #include "io/file.h" +static void report_file_error(const char* verb, const char* path) +{ + char message[512]; + snprintf(message, sizeof(message), "%s '%s'", verb, path); + report_error_message(message); +} + bool read_file(const char* path, struct File* out) { FILE* stream = fopen(path, "rb"); if (stream == NULL) { - fprintf(stderr, "error: could not open '%s'\n", path); + report_file_error("could not open", path); return false; } if (fseek(stream, 0, SEEK_END) != 0) { - fprintf(stderr, "error: could not read '%s'\n", path); + report_file_error("could not read", path); fclose(stream); return false; } @@ -22,7 +30,7 @@ bool read_file(const char* path, struct File* out) long length = ftell(stream); if (length < 0) { - fprintf(stderr, "error: could not read '%s'\n", path); + report_file_error("could not read", path); fclose(stream); return false; } @@ -32,14 +40,14 @@ bool read_file(const char* path, struct File* out) char* data = malloc(size + 1); if (data == NULL) { - fprintf(stderr, "error: out of memory reading '%s'\n", path); + report_file_error("out of memory reading", path); fclose(stream); return false; } if (fread(data, 1, size, stream) != size) { - fprintf(stderr, "error: could not read '%s'\n", path); + report_file_error("could not read", path); free(data); fclose(stream); return false; diff --git a/src/lexer/lexer.c b/src/lexer/lexer.c index da45741..fb3fce7 100644 --- a/src/lexer/lexer.c +++ b/src/lexer/lexer.c @@ -118,6 +118,7 @@ static struct Token make_token(struct Lexer* lexer, enum TokenType type, const c struct Lexer create_lexer(const char* source) { struct Lexer lexer; + lexer.name = NULL; lexer.source = source; lexer.current = source; lexer.line = 1; diff --git a/src/lexer/lexer.h b/src/lexer/lexer.h index 441fe10..afe6e7f 100644 --- a/src/lexer/lexer.h +++ b/src/lexer/lexer.h @@ -64,6 +64,7 @@ struct Token struct Lexer { + const char* name; const char* source; const char* current; uint32_t line; diff --git a/src/main.c b/src/main.c index 90b3c82..71e8918 100644 --- a/src/main.c +++ b/src/main.c @@ -2,6 +2,7 @@ #include "io/file.h" #include "cli/args.h" +#include "diag/diag.h" #include "sema/sema.h" #include "lexer/lexer.h" #include "codegen/nasm.h" @@ -19,7 +20,7 @@ int main(int argc, char** argv) if (args.target != ASSEMBLER_NASM) { - fprintf(stderr, "error: only the nasm target is supported\n"); + report_error_message("only the nasm target is supported"); return 1; } @@ -28,6 +29,7 @@ int main(int argc, char** argv) return 1; struct Lexer lexer = create_lexer(source.data); + lexer.name = args.input_path; struct Program program; if (!parse_program(&lexer, &program)) @@ -37,7 +39,8 @@ int main(int argc, char** argv) return 1; } - if (!analyze_program(&program)) + struct Source diagnostics = { args.input_path, source.data }; + if (!analyze_program(diagnostics, &program)) { free_program(&program); free_file(&source); @@ -50,7 +53,9 @@ int main(int argc, char** argv) out = fopen(args.output_path, "w"); if (out == NULL) { - fprintf(stderr, "error: could not open '%s' for writing\n", args.output_path); + char message[256]; + snprintf(message, sizeof(message), "could not open '%s' for writing", args.output_path); + report_error_message(message); free_program(&program); free_file(&source); return 1; diff --git a/src/parser/parser.c b/src/parser/parser.c index 7ec4474..beb6b04 100644 --- a/src/parser/parser.c +++ b/src/parser/parser.c @@ -2,11 +2,13 @@ #include #include +#include "diag/diag.h" #include "parser/parser.h" struct Parser { struct Lexer* lexer; + struct Source source; struct Token current; struct Token previous; bool had_error; @@ -25,7 +27,7 @@ static bool check(struct Parser* parser, enum TokenType type) static void error_at(struct Parser* parser, struct Token token, const char* message) { - fprintf(stderr, "error: line %u: %s\n", token.line, message); + report_error(parser->source, token, message); parser->had_error = true; } @@ -509,6 +511,8 @@ bool parse_program(struct Lexer* lexer, struct Program* out) { struct Parser parser; parser.lexer = lexer; + parser.source.name = lexer->name; + parser.source.text = lexer->source; parser.had_error = false; advance_parser(&parser); diff --git a/src/sema/sema.c b/src/sema/sema.c index 55f4a93..f86159c 100644 --- a/src/sema/sema.c +++ b/src/sema/sema.c @@ -9,7 +9,7 @@ static bool names_equal(struct Token a, struct Token b) return a.length == b.length && memcmp(a.start, b.start, a.length) == 0; } -static bool check_duplicate_names(struct Program* program) +static bool check_duplicate_names(struct Source source, struct Program* program) { size_t count = program->const_count + program->data_count + program->proc_count; if (count == 0) @@ -38,8 +38,10 @@ static bool check_duplicate_names(struct Program* program) for (size_t j = 0; j < i; j += 1) if (names_equal(names[i], names[j])) { - fprintf(stderr, "error: line %u: '%.*s' is already defined\n", - names[i].line, (int)names[i].length, names[i].start); + char message[128]; + snprintf(message, sizeof(message), "'%.*s' is already defined", + (int)names[i].length, names[i].start); + report_error(source, names[i], message); ok = false; } @@ -47,7 +49,7 @@ static bool check_duplicate_names(struct Program* program) return ok; } -static bool check_entry_point(struct Program* program) +static bool check_entry_point(struct Source source, struct Program* program) { if (!program->config.has_entry) return true; @@ -57,18 +59,20 @@ static bool check_entry_point(struct Program* program) if (names_equal(program->procs[i].name, entry)) return true; - fprintf(stderr, "error: line %u: entry point '%.*s' is not defined\n", - entry.line, (int)entry.length, entry.start); + char message[128]; + snprintf(message, sizeof(message), "entry point '%.*s' is not defined", + (int)entry.length, entry.start); + report_error(source, entry, message); return false; } -bool analyze_program(struct Program* program) +bool analyze_program(struct Source source, struct Program* program) { bool ok = true; - if (!check_duplicate_names(program)) + if (!check_duplicate_names(source, program)) ok = false; - if (!check_entry_point(program)) + if (!check_entry_point(source, program)) ok = false; return ok; diff --git a/src/sema/sema.h b/src/sema/sema.h index e015f41..468d1be 100644 --- a/src/sema/sema.h +++ b/src/sema/sema.h @@ -2,6 +2,7 @@ #include +#include "diag/diag.h" #include "parser/ast.h" -bool analyze_program(struct Program* program); +bool analyze_program(struct Source source, struct Program* program); diff --git a/tests/sema_test.c b/tests/sema_test.c index b7d3805..5dbdfa7 100644 --- a/tests/sema_test.c +++ b/tests/sema_test.c @@ -14,7 +14,8 @@ static bool analyze_source(const char* source) return false; } - bool ok = analyze_program(&program); + struct Source diagnostics = { "", source }; + bool ok = analyze_program(diagnostics, &program); free_program(&program); return ok; } -- cgit v1.3