feat(error): add typed errors + source-span diagnostics

This commit is contained in:
2026-08-28 19:58:29 -04:00
parent 911061eab7
commit d02b5aabcd
7 changed files with 529 additions and 0 deletions
+138
View File
@@ -0,0 +1,138 @@
#include "error.h"
#include <stdlib.h>
#include <string.h>
/* C23 only: ISO/IEC 9899:2024. */
static const char *const category_names[] = {
[ST_ERR_KDL_PARSE] = "kdl-parse",
[ST_ERR_KDL_SCHEMA] = "kdl-schema",
[ST_ERR_IO] = "io",
[ST_ERR_USAGE] = "usage",
[ST_ERR_INTERNAL] = "internal",
};
struct st_error *
st_error_new(enum st_error_category category, const char *message)
{
struct st_error *e;
size_t len;
if (message == NULL) {
message = "";
}
e = calloc(1, sizeof(*e));
if (e == NULL) {
return NULL;
}
len = strlen(message);
e->message = malloc(len + 1);
if (e->message == NULL) {
free(e);
return NULL;
}
memcpy(e->message, message, len + 1);
e->category = category;
return e;
}
struct st_error *
st_error_kdl_parse(const char *message)
{
return st_error_new(ST_ERR_KDL_PARSE, message);
}
struct st_error *
st_error_kdl_schema(const char *message)
{
return st_error_new(ST_ERR_KDL_SCHEMA, message);
}
struct st_error *
st_error_io(const char *message)
{
return st_error_new(ST_ERR_IO, message);
}
struct st_error *
st_error_usage(const char *message)
{
return st_error_new(ST_ERR_USAGE, message);
}
struct st_error *
st_error_internal(const char *message)
{
return st_error_new(ST_ERR_INTERNAL, message);
}
struct st_error *
st_error_at(struct st_error *e, const struct st_span *span)
{
if (e != NULL) {
e->span = span;
}
return e;
}
void
st_error_free(struct st_error *e)
{
if (e == NULL) {
return;
}
free(e->message);
free(e);
}
enum st_error_category
st_error_category_of(const struct st_error *e)
{
return e != NULL ? e->category : ST_ERR_INTERNAL;
}
const char *
st_error_message(const struct st_error *e)
{
return e != NULL ? e->message : "";
}
const char *
st_error_category_name(enum st_error_category category)
{
if ((size_t)category >= sizeof(category_names) / sizeof(category_names[0])) {
return "unknown";
}
return category_names[category];
}
void
st_error_print(FILE *out, const struct st_error *e)
{
const char *cat;
char *prefixed;
size_t len;
int rc;
if (out == NULL || e == NULL) {
return;
}
cat = st_error_category_name(e->category);
len = strlen(cat) + strlen(e->message) + 4; /* "[", "]", " ", NUL */
prefixed = malloc(len);
if (prefixed == NULL) {
fprintf(out, "%s\n", e->message);
return;
}
rc = snprintf(prefixed, len, "[%s] %s", cat, e->message);
if (rc < 0) {
prefixed[0] = '\0';
}
if (e->span != NULL) {
st_span_print(out, e->span, prefixed);
} else {
fprintf(out, "%s\n", prefixed);
}
free(prefixed);
}
+55
View File
@@ -0,0 +1,55 @@
#ifndef ST_ERROR_H
#define ST_ERROR_H
#include <stdio.h>
#include "span.h"
/*
* Typed error for all library code paths. Library code RETURNS these;
* it never calls exit()/abort() on user input (only main.c exits).
*/
enum st_error_category {
ST_ERR_KDL_PARSE = 0,
ST_ERR_KDL_SCHEMA,
ST_ERR_IO,
ST_ERR_USAGE,
ST_ERR_INTERNAL,
};
struct st_error {
enum st_error_category category;
char *message; /* owned copy; never NULL after construction */
const struct st_span *span; /* borrowed, may be NULL */
};
/* Constructors. All duplicate `message`; NULL is treated as "". They
* return NULL only on allocation failure. */
struct st_error *st_error_new(enum st_error_category category,
const char *message);
struct st_error *st_error_kdl_parse(const char *message);
struct st_error *st_error_kdl_schema(const char *message);
struct st_error *st_error_io(const char *message);
struct st_error *st_error_usage(const char *message);
struct st_error *st_error_internal(const char *message);
/* Attaches a borrowed span and returns `e` for chaining:
* err = st_error_at(st_error_kdl_parse("bad token"), &span); */
struct st_error *st_error_at(struct st_error *e, const struct st_span *span);
/* Frees the error (message + struct). NULL is a safe no-op. */
void st_error_free(struct st_error *e);
enum st_error_category st_error_category_of(const struct st_error *e);
const char *st_error_message(const struct st_error *e);
/* Stable display name, e.g. "kdl-parse". Unknown categories yield
* "unknown" rather than indexing out of range. */
const char *st_error_category_name(enum st_error_category category);
/* Prints the diagnostic. With a span: the canonical two-line form with
* the category-prefixed message ("file:line:col: [cat] <msg>\n" +
* caret line). Without a span: "[cat] <msg>\n". NULL is tolerated. */
void st_error_print(FILE *out, const struct st_error *e);
#endif
+33
View File
@@ -0,0 +1,33 @@
#include "span.h"
/* C23 only: ISO/IEC 9899:2024. */
#define ST_SPAN_CARET_PAD_CAP 60
void
st_span_print(FILE *out, const struct st_span *span, const char *message)
{
size_t pad;
if (out == NULL || span == NULL || span->file == NULL ||
message == NULL) {
return;
}
fprintf(out, "%s:%zu:%zu: %s\n", span->file, span->line, span->col,
message);
/*
* Caret line: 4-space indent, "..." elision, caret. The caret sits
* three cells past the reported column (the elision marker occupies
* those cells), so col 5 renders exactly " ...^". Columns below
* 5 collapse onto the elision; columns above 5 push the caret right,
* capped to keep the line bounded.
*/
fputs(" ...", out);
pad = span->col > 5 ? span->col - 5 : 0;
if (pad > ST_SPAN_CARET_PAD_CAP) {
pad = ST_SPAN_CARET_PAD_CAP;
}
while (pad-- > 0) {
fputc(' ', out);
}
fputs("^\n", out);
}
+33
View File
@@ -0,0 +1,33 @@
#ifndef ST_SPAN_H
#define ST_SPAN_H
#include <stddef.h>
#include <stdio.h>
/*
* A source span: 1-based line/column into a source file.
* `file` is borrowed (never freed); typically points into stable
* storage such as a file-name arena or a string literal.
*/
struct st_span {
const char *file;
size_t line;
size_t col;
};
/*
* Renders the canonical two-line diagnostic:
*
* <file>:<line>:<col>: <message>
* ...^
*
* The caret line is a fixed 4-space indent followed by a "..."
* elision marker and the caret. For the canonical span (col 5)
* this is exactly " ...^"; the caret shifts right for larger
* columns and is capped so hostile spans cannot force unbounded
* output. NULL arguments are tolerated and render nothing.
*/
void st_span_print(FILE *out, const struct st_span *span,
const char *message);
#endif