about summary refs log tree commit diff
diff options
context:
space:
mode:
authorMel <mel@rnrd.eu>2026-05-24 18:18:23 +0200
committerMel <mel@rnrd.eu>2026-05-24 18:18:23 +0200
commita76fd9b5af153773273121c7aec2a9cbeed1dfb7 (patch)
treea6c619e44df4f38f60c457b9122e207b0a3c09f0
parent8c4c219c945dfb41415e04654f488f7880e58ca2 (diff)
downloadcatskill-a76fd9b5af153773273121c7aec2a9cbeed1dfb7.tar.zst
catskill-a76fd9b5af153773273121c7aec2a9cbeed1dfb7.zip
Make diagnostics generic over passes, condensing different error handling
Signed-off-by: Mel <mel@rnrd.eu>
-rw-r--r--boot/catboot.c25
-rw-r--r--boot/common.c316
-rw-r--r--boot/ir.c44
-rw-r--r--boot/lex.c93
-rw-r--r--boot/lower.c378
-rw-r--r--boot/parse.c79
-rw-r--r--boot/tests/lower/shadowing_error.cskt4
-rw-r--r--boot/tests/lower/type_cycle_error.cskt7
-rw-r--r--boot/visit/ir.c81
9 files changed, 713 insertions, 314 deletions
diff --git a/boot/catboot.c b/boot/catboot.c
index 3919a2a..e340d48 100644
--- a/boot/catboot.c
+++ b/boot/catboot.c
@@ -130,12 +130,11 @@ debug_lower_pass(struct Source_File source_file)
     }
 
     struct Unit unit = { 0 };
-    lower_tree(&tree, source_file, &unit);
+    lower_tree(&tree, &unit);
     printer(&unit);
 
-    // diagnostics are part of the printed unit, so the caller can read
-    // them from the dump. always returning success keeps snapshot tests
-    // viable for both the green and the diagnostic paths.
+    // always succeed. errors live in the printed unit and snapshot tests
+    // pick them up from there.
     return 0;
 }
 
@@ -156,11 +155,12 @@ debug_transpile_pass(struct Source_File source_file)
     }
 
     struct Unit unit = { 0 };
-    lower_tree(&tree, source_file, &unit);
-    if (unit.had_error) {
-        log_error("lowering finished with errors\n");
-        return 1;
+    lower_tree(&tree, &unit);
+    FOR_EACH_ARRAY (struct Lower_Error, err, &unit.lower_errors) {
+        struct Diagnostic d = lower_error_to_diagnostic(err, &unit, source_file);
+        diagnostic_display(source_file, &d, stderr);
     }
+    if (unit.had_error) return 1;
 
     struct Transpile_Output output = transpile_output_from_file(stdout);
 
@@ -294,11 +294,12 @@ default_command(struct Command_Arguments* arguments)
     }
 
     struct Unit unit = { 0 };
-    lower_tree(&tree, source_file, &unit);
-    if (unit.had_error) {
-        log_error("lowering finished with errors\n");
-        return COMMAND_FAIL;
+    lower_tree(&tree, &unit);
+    FOR_EACH_ARRAY (struct Lower_Error, err, &unit.lower_errors) {
+        struct Diagnostic d = lower_error_to_diagnostic(err, &unit, source_file);
+        diagnostic_display(source_file, &d, stderr);
     }
+    if (unit.had_error) return COMMAND_FAIL;
 
     struct Transpiler transpiler;
     transpiler_new(&transpiler, transpile_output_from_string());
diff --git a/boot/common.c b/boot/common.c
index 0524b3b..85b1422 100644
--- a/boot/common.c
+++ b/boot/common.c
@@ -938,6 +938,54 @@ string_buffer_append(struct String_Buffer* buffer, struct String s)
     buffer->data[buffer->length] = '\0';
 }
 
+// pairs with `%S` in the printf-like formatters below.
+#define STR(s) (int)(s).length, (s).data
+
+void
+_format_translate_format(ascii* dst, const ascii* src, uint dst_size)
+{
+    uint si = 0, di = 0;
+    while (di + 4 < dst_size) {
+        ascii c = src[si];
+        if (c == '\0') break;
+        if (c == '%' && src[si + 1] == 'S') {
+            dst[di++] = '%';
+            dst[di++] = '.';
+            dst[di++] = '*';
+            dst[di++] = 's';
+            si += 2;
+        } else {
+            dst[di++] = c;
+            si++;
+        }
+    }
+    dst[di] = '\0';
+}
+
+#define MAX_FORMATTED_STRING_SIZE 1024
+
+struct String
+string_vformat(const ascii* format, va_list args)
+{
+    ascii translated[MAX_FORMAT_STRING_SIZE];
+    _format_translate_format(translated, format, sizeof translated);
+
+    ascii buffer[MAX_FORMATTED_STRING_SIZE];
+    int written = vsnprintf(buffer, sizeof buffer, translated, args);
+    check(written >= 0, "vsnprintf failed");
+    return string_new(buffer, (uint)written);
+}
+
+struct String
+string_format(const ascii* format, ...)
+{
+    va_list args;
+    va_start(args, format);
+    struct String result = string_vformat(format, args);
+    va_end(args);
+    return result;
+}
+
 // appends a C-style string to the string buffer.
 void
 string_buffer_append_c_str(struct String_Buffer* buffer, const ascii* c_str)
@@ -954,6 +1002,17 @@ string_buffer_append_c_str(struct String_Buffer* buffer, const ascii* c_str)
     buffer->data[buffer->length] = '\0';
 }
 
+// appends a formatted string to the buffer.
+void
+string_buffer_appendf(struct String_Buffer* buffer, const ascii* format, ...)
+{
+    va_list args;
+    va_start(args, format);
+    struct String formatted = string_vformat(format, args);
+    va_end(args);
+    string_buffer_append(buffer, formatted);
+}
+
 // converts the string buffer to an immutable string.
 // allocates a new string in the string region.
 struct String
@@ -977,6 +1036,263 @@ struct Source_File
     struct String path;
 };
 
+// byte position within a file.
+typedef uint64 Pos;
+
+// an extent of text within a file,
+// defined by its start and end byte positions.
+struct Span
+{
+    Pos start;
+    Pos end;
+};
+
+struct Span
+span_new(Pos start, Pos end)
+{
+    return (struct Span){ .start = start, .end = end };
+}
+
+struct Span
+span_empty(void)
+{
+    return (struct Span){ 0, 0 };
+}
+
+struct Span
+span_width(Pos start, uint width)
+{
+    return (struct Span){ .start = start, .end = start + width };
+}
+
+// create a span which encompasses two spans.
+struct Span
+span_merge(struct Span a, struct Span b)
+{
+    return (struct Span){
+        .start = a.start < b.start ? a.start : b.start,
+        .end = a.end > b.end ? a.end : b.end,
+    };
+}
+
+// expand a span by a number of bytes.
+// negative number expands the span to the left,
+// positive number expands the span to the right.
+struct Span
+span_expand(struct Span span, integer by)
+{
+    uint start_expand = by < 0 ? -by : 0;
+    uint end_expand = by > 0 ? by : 0;
+    return (struct Span){
+        .start = span.start - start_expand,
+        .end = span.end + end_expand,
+    };
+}
+
+// check if two spans are equal.
+bool
+span_equals(struct Span a, struct Span b)
+{
+    return a.start == b.start && a.end == b.end;
+}
+
+// check if span equals = { 0, 0 }.
+bool
+span_is_empty(struct Span span)
+{
+    return span_equals(span, (struct Span){ 0, 0 });
+}
+
+uint
+span_length(struct Span span)
+{
+    uint length = span.end - span.start;
+    return length == 0 ? 1 : length;
+}
+
+// a cursor position placed within a text file.
+struct Cursor
+{
+    uint line;
+    uint column;
+};
+
+struct Cursor
+cursor_new(uint line, uint column)
+{
+    return (struct Cursor){ .line = line, .column = column };
+}
+
+struct Cursor
+cursor_empty(void)
+{
+    return cursor_new(0, 0);
+}
+
+enum Diagnostic_Severity
+{
+    DIAGNOSTIC_NOTE,
+    DIAGNOSTIC_WARNING,
+    DIAGNOSTIC_ERROR,
+};
+
+struct Diagnostic
+{
+    enum Diagnostic_Severity severity;
+    struct Span span;
+    // first line of the rendered diagnostic. may contain `\n` to introduce
+    // extra body lines, each indented under the headline.
+    struct String message;
+    // optional. rendered as `hint: <hint>` under the body when non-empty.
+    struct String hint;
+    // NOTE(mel): probably overkill for bootstrapping, but i kind of want to figure
+    // the error format out for future iterations, and want to see it in action,
+    // to have more thoughts about improving it later on.
+};
+
+const ascii*
+diagnostic_severity_label(enum Diagnostic_Severity severity)
+{
+    switch (severity) {
+    case DIAGNOSTIC_NOTE:
+        return "note.";
+    case DIAGNOSTIC_WARNING:
+        return "warning?";
+    case DIAGNOSTIC_ERROR:
+        return "error!";
+    default:
+        return "diagnostic";
+    }
+}
+
+const ascii*
+diagnostic_severity_color(enum Diagnostic_Severity severity)
+{
+    switch (severity) {
+    case DIAGNOSTIC_NOTE:
+        return ANSI_BLUE;
+    case DIAGNOSTIC_WARNING:
+        return ANSI_YELLOW;
+    case DIAGNOSTIC_ERROR:
+        return ANSI_RED;
+    default:
+        return ANSI_WHITE;
+    }
+}
+
+// line and column are 1-based.
+void
+source_position_from_span(struct String source, struct Span span, uint* out_line, uint* out_column)
+{
+    uint line = 1, column = 1;
+    uint upto = span.start;
+    if (upto > string_length(source)) upto = string_length(source);
+    for (uint i = 0; i < upto; ++i) {
+        if (string_at(source, i) == '\n') {
+            line++;
+            column = 1;
+        } else {
+            column++;
+        }
+    }
+    *out_line = line;
+    *out_column = column;
+}
+
+struct Span
+source_line_span(struct String source, struct Span span)
+{
+    Pos line_start = span.start, line_end = span.start;
+    while (line_start > 0 && string_at(source, line_start - 1) != '\n') line_start--;
+    while (line_end < string_length(source) && string_at(source, line_end) != '\n') line_end++;
+    return span_new(line_start, line_end);
+}
+
+// render a diagnostic to a stream.
+void
+diagnostic_display(struct Source_File source, struct Diagnostic* d, FILE* out)
+{
+    uint line, column;
+    source_position_from_span(source.source, d->span, &line, &column);
+
+    STRING_FORMAT_TO(source.path, out, ANSI_WHITE "%s:%lu:%lu:\n", line, column);
+
+    // first line of the message is the headline. anything after a `\n` is body.
+    uint headline_end = 0;
+    while (headline_end < d->message.length && d->message.data[headline_end] != '\n')
+        headline_end++;
+
+    fprintf(out, ANSI_BOLD "%s%s " ANSI_WHITE, diagnostic_severity_color(d->severity),
+            diagnostic_severity_label(d->severity));
+    fprintf(out, "%.*s", (int)headline_end, d->message.data);
+    if (d->severity == DIAGNOSTIC_ERROR) fprintf(out, " :(");
+    fprintf(out, "\n" ANSI_NO_BOLD);
+
+    if (headline_end < d->message.length) {
+        uint body_start = headline_end + 1;
+        fprintf(out, "%.*s\n", (int)(d->message.length - body_start), d->message.data + body_start);
+    }
+
+    if (!string_is_empty(d->hint)) STRING_FORMAT_TO(d->hint, out, "  hint: %S\n");
+
+    struct Span line_span = source_line_span(source.source, d->span);
+    struct String_View source_line =
+        string_substring(source.source, line_span.start, line_span.end);
+
+    fprintf(out, ANSI_WHITE "%lu| ", line);
+    STRING_FORMAT_TO(source_line, out, "%S\n");
+
+    uint line_number_length = ceil(log10(line + 1));
+    fprintf(out, "%s%*s", diagnostic_severity_color(d->severity),
+            (int)(column + 1 + line_number_length), " ");
+    uint width = span_length(d->span);
+    if (width == 0) width = 1;
+    for (uint w = 0; w < width; w++) fprintf(out, "^");
+    fprintf(out, "\n" ANSI_RESET);
+}
+
+void
+diagnostic_push_str(
+    Array(struct Diagnostic) * into, bool* out_had_error, enum Diagnostic_Severity severity,
+    struct Span span, struct String message)
+{
+    struct Diagnostic d = { .severity = severity, .span = span, .message = message };
+    array_push(into, &d);
+    if (out_had_error && severity == DIAGNOSTIC_ERROR) *out_had_error = true;
+}
+
+void
+diagnostic_push(
+    Array(struct Diagnostic) * into, bool* out_had_error, enum Diagnostic_Severity severity,
+    struct Span span, const ascii* format, ...)
+{
+    va_list args;
+    va_start(args, format);
+    struct String message = string_vformat(format, args);
+    va_end(args);
+    diagnostic_push_str(into, out_had_error, severity, span, message);
+}
+
+#define diagnostic_emit(unit, severity, span, ...) \
+    diagnostic_push(&(unit)->diagnostics, &(unit)->had_error, severity, span, __VA_ARGS__)
+
+#define diagnostic_emit_str(unit, severity, span, message) \
+    diagnostic_push_str(&(unit)->diagnostics, &(unit)->had_error, severity, span, message)
+
+void
+diagnostic_render(
+    struct Source_File source, FILE* out, enum Diagnostic_Severity severity, struct Span span,
+    const ascii* format, ...)
+{
+    va_list args;
+    va_start(args, format);
+    struct String message = string_vformat(format, args);
+    va_end(args);
+
+    struct Diagnostic d = { .severity = severity, .span = span, .message = message };
+    diagnostic_display(source, &d, out);
+}
+
 // single iteration of the CRC32 checksum algorithm
 // described in POSIX.
 // see: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/cksum.html
diff --git a/boot/ir.c b/boot/ir.c
index 4bdfc6e..6ea055e 100644
--- a/boot/ir.c
+++ b/boot/ir.c
@@ -444,19 +444,33 @@ struct Import
     struct Span span;
 };
 
-enum Diagnostic_Severity
-{
-    DIAGNOSTIC_NOTE,
-    DIAGNOSTIC_WARNING,
-    DIAGNOSTIC_ERROR,
-};
-
-// TODO: we might want to take this out to be used for other passes.
-struct Diagnostic
-{
-    enum Diagnostic_Severity severity;
+enum Lower_Error_Kind
+{
+    LOWER_ERROR_NONE,
+    LOWER_ERROR_UNDEFINED_TYPE,
+    LOWER_ERROR_DUPLICATE_TYPE,
+    LOWER_ERROR_DUPLICATE_FUNCTION,
+    LOWER_ERROR_NAME_SHADOWS,
+    LOWER_ERROR_TYPE_CYCLE,
+    LOWER_ERROR_ASSIGNMENT_AS_EXPRESSION,
+    LOWER_ERROR_RANGE_OUTSIDE_LOOP,
+    LOWER_ERROR_CONSTRUCT_SUBJECT_NOT_NAME,
+    LOWER_ERROR_TYPE_EXPRESSION_IN_BODY,
+    LOWER_ERROR_UNSUPPORTED_TOP_LEVEL,
+    LOWER_ERROR_UNKNOWN_LOOP_STYLE,
+    LOWER_ERROR_UNKNOWN_COMPOUND_ASSIGN,
+    LOWER_ERROR_UNIMPLEMENTED,
+};
+
+struct Lower_Error
+{
+    enum Lower_Error_Kind kind;
     struct Span span;
-    struct String message;
+    
+    // per-error details, meaning depends on each error kind!
+    struct String name;
+    struct String detail;
+    Array(Type_Id) cycle_chain;
 };
 
 // a single translation unit.
@@ -472,7 +486,7 @@ struct Unit
     Array(Type_Id) type_emission_order;
 
     bool had_error;
-    Array(struct Diagnostic) diagnostics;
+    Array(struct Lower_Error) lower_errors;
 };
 
 REGION(struct Type, type)
@@ -655,10 +669,6 @@ ir_make_construct(Type_Id type_id, Array(struct Construct_Field) fields, struct
     return expression_new(EXPRESSION_CONSTRUCT, v, span);
 }
 
-// type-ref constructors.
-// `bare` is the no-modifier form; `pointer` wraps one `&` modifier; `with_mods`
-// takes a fully-formed mods array (caller owns it).
-
 struct Type_Ref
 type_ref_bare(Type_Id type_id)
 {
diff --git a/boot/lex.c b/boot/lex.c
index 594905f..18f8690 100644
--- a/boot/lex.c
+++ b/boot/lex.c
@@ -13,99 +13,6 @@
 
 #define MAX_CHAR_BUFFER_SIZE 256
 
-// byte position within a file.
-typedef uint64 Pos;
-
-// an extent of text within a file,
-// defined by its start and end byte positions.
-struct Span
-{
-    Pos start;
-    Pos end;
-};
-
-struct Span
-span_new(Pos start, Pos end)
-{
-    return (struct Span){ .start = start, .end = end };
-}
-
-struct Span
-span_empty(void)
-{
-    return (struct Span){ 0, 0 };
-}
-
-struct Span
-span_width(Pos start, uint width)
-{
-    return (struct Span){ .start = start, .end = start + width };
-}
-
-// create a span which encompasses two spans.
-struct Span
-span_merge(struct Span a, struct Span b)
-{
-    return (struct Span){
-        .start = a.start < b.start ? a.start : b.start,
-        .end = a.end > b.end ? a.end : b.end,
-    };
-}
-
-// expand a span by a number of bytes.
-// negative number expands the span to the left,
-// positive number expands the span to the right.
-struct Span
-span_expand(struct Span span, integer by)
-{
-    uint start_expand = by < 0 ? -by : 0;
-    uint end_expand = by > 0 ? by : 0;
-    return (struct Span){
-        .start = span.start - start_expand,
-        .end = span.end + end_expand,
-    };
-}
-
-// check if two spans are equal.
-bool
-span_equals(struct Span a, struct Span b)
-{
-    return a.start == b.start && a.end == b.end;
-}
-
-// check if span equals = { 0, 0 }.
-bool
-span_is_empty(struct Span span)
-{
-    return span_equals(span, (struct Span){ 0, 0 });
-}
-
-uint
-span_length(struct Span span)
-{
-    uint length = span.end - span.start;
-    return length == 0 ? 1 : length;
-}
-
-// a cursor position placed within a text file.
-struct Cursor
-{
-    uint line;
-    uint column;
-};
-
-struct Cursor
-cursor_new(uint line, uint column)
-{
-    return (struct Cursor){ .line = line, .column = column };
-}
-
-struct Cursor
-cursor_empty(void)
-{
-    return cursor_new(0, 0);
-}
-
 // what kind of token is it?
 enum Token_Kind
 {
diff --git a/boot/lower.c b/boot/lower.c
index 01762a5..65eb18c 100644
--- a/boot/lower.c
+++ b/boot/lower.c
@@ -73,10 +73,6 @@ struct Lower_Context
     // the translation unit being created
     struct Unit* unit;
 
-    // text source of the unit in translation
-    // used for attaching diagnostics to correct lines
-    struct Source_File source;
-
     // monotonic counter of synthesized types
     uint synthetic_type_counter;
 
@@ -89,21 +85,89 @@ struct Lower_Context
 };
 
 void
-lower_emit_error(struct Unit* unit, struct Span span, struct String message)
+lower_push_error(struct Unit* unit, struct Lower_Error err)
 {
-    struct Diagnostic d = {
-        .severity = DIAGNOSTIC_ERROR,
-        .span = span,
-        .message = message,
-    };
-    array_push(&unit->diagnostics, &d);
+    array_push(&unit->lower_errors, &err);
     unit->had_error = true;
 }
 
+// render the cycle's chain to a nice human-y message.
 void
-lower_emit_error_c(struct Unit* unit, struct Span span, const ascii* message)
+lower_format_cycle_chain(
+    struct String_Buffer* buf, struct Unit* unit, struct Source_File source, Array(Type_Id) * chain)
+{
+    uint n = array_length(chain);
+    for (uint k = 0; k < n; ++k) {
+        Type_Id current_id = *array_at(Type_Id, chain, k);
+        Type_Id next_id = *array_at(Type_Id, chain, (k + 1) % n);
+        struct Type* current = *array_at(struct Type*, &unit->types.entries, current_id);
+        struct Type* next = *array_at(struct Type*, &unit->types.entries, next_id);
+        uint line, column;
+        source_position_from_span(source.source, current->span, &line, &column);
+
+        if (k > 0) string_buffer_appendf(buf, "\n");
+        string_buffer_appendf(
+            buf, "  %S (line %lu) depends on %S", STR(current->name), line, STR(next->name));
+    }
+}
+
+// turn a structured lower error into a displayable diagnostic.
+struct Diagnostic
+lower_error_to_diagnostic(struct Lower_Error* err, struct Unit* unit, struct Source_File source)
 {
-    lower_emit_error(unit, span, string_from_c_string(message));
+    struct Diagnostic d = { .severity = DIAGNOSTIC_ERROR, .span = err->span };
+    switch (err->kind) {
+    case LOWER_ERROR_UNDEFINED_TYPE:
+        d.message = string_format("undefined type '%S'", STR(err->name));
+        return d;
+    case LOWER_ERROR_DUPLICATE_TYPE:
+        d.message = string_format("duplicate type '%S'", STR(err->name));
+        return d;
+    case LOWER_ERROR_DUPLICATE_FUNCTION:
+        d.message = string_format("duplicate function '%S'", STR(err->name));
+        return d;
+    case LOWER_ERROR_NAME_SHADOWS:
+        d.message = string_format("name '%S' shadows an existing binding", STR(err->name));
+        return d;
+    case LOWER_ERROR_TYPE_CYCLE: {
+        struct String_Buffer buf = string_buffer_new(512);
+        string_buffer_appendf(&buf, "type cycle detected\n");
+        lower_format_cycle_chain(&buf, unit, source, &err->cycle_chain);
+        d.message = string_buffer_to_string(&buf);
+        d.hint = string_from_static_c_string(
+            "break the cycle with a reference (use `&T` instead of `T`)");
+        return d;
+    }
+    case LOWER_ERROR_ASSIGNMENT_AS_EXPRESSION:
+        d.message = string_from_static_c_string("assignment cannot appear as an expression");
+        return d;
+    case LOWER_ERROR_RANGE_OUTSIDE_LOOP:
+        d.message =
+            string_from_static_c_string("range expressions are only valid in loop initializers");
+        return d;
+    case LOWER_ERROR_CONSTRUCT_SUBJECT_NOT_NAME:
+        d.message = string_from_static_c_string("construction subject must be a type name");
+        return d;
+    case LOWER_ERROR_TYPE_EXPRESSION_IN_BODY:
+        d.message =
+            string_from_static_c_string("type expression not allowed inside a function body");
+        return d;
+    case LOWER_ERROR_UNSUPPORTED_TOP_LEVEL:
+        d.message = string_from_static_c_string("unsupported top-level statement");
+        return d;
+    case LOWER_ERROR_UNKNOWN_LOOP_STYLE:
+        d.message = string_from_static_c_string("unknown loop style");
+        return d;
+    case LOWER_ERROR_UNKNOWN_COMPOUND_ASSIGN:
+        d.message = string_from_static_c_string("unknown compound assignment operator");
+        return d;
+    case LOWER_ERROR_UNIMPLEMENTED:
+        d.message = string_format("unimplemented: %S", STR(err->detail));
+        return d;
+    default:
+        d.message = string_from_static_c_string("unknown lower error");
+        return d;
+    }
 }
 
 bool
@@ -419,11 +483,13 @@ lower_intern_type_ref(struct Lower_Context* ctx, struct Tree_Type* tree_type)
         if (lower_type_lookup_by_name(ctx->unit, current->value.name.name, &id)) {
             ref.type_id = id;
         } else {
-            lower_emit_error(
-                ctx->unit, current->span,
-                string_concatenate(
-                    ARG_ASCII, "undefined type '", ARG_STRING, current->value.name.name, ARG_ASCII,
-                    "'", ARG_END));
+            lower_push_error(
+                ctx->unit,
+                (struct Lower_Error){
+                    .kind = LOWER_ERROR_UNDEFINED_TYPE,
+                    .span = current->span,
+                    .name = current->value.name.name,
+                });
         }
         return ref;
     }
@@ -438,10 +504,22 @@ lower_intern_type_ref(struct Lower_Context* ctx, struct Tree_Type* tree_type)
         return ref;
     }
     case TREE_TYPE_MAP:
-        lower_emit_error_c(ctx->unit, current->span, "unimplemented: map types");
+        lower_push_error(
+            ctx->unit,
+            (struct Lower_Error){
+                .kind = LOWER_ERROR_UNIMPLEMENTED,
+                .span = current->span,
+                .detail = string_from_static_c_string("map types"),
+            });
         return ref;
     default:
-        lower_emit_error_c(ctx->unit, current->span, "unimplemented: this type form");
+        lower_push_error(
+            ctx->unit,
+            (struct Lower_Error){
+                .kind = LOWER_ERROR_UNIMPLEMENTED,
+                .span = current->span,
+                .detail = string_from_static_c_string("this type form"),
+            });
         return ref;
     }
 }
@@ -494,10 +572,10 @@ lower_register_function_shell(struct Unit* unit, struct String name, struct Span
 {
     Function_Id existing;
     if (lower_function_lookup_by_name(unit, name, &existing)) {
-        lower_emit_error(
-            unit, span,
-            string_concatenate(
-                ARG_ASCII, "duplicate function '", ARG_STRING, name, ARG_ASCII, "'", ARG_END));
+        lower_push_error(
+            unit,
+            (struct Lower_Error){
+                .kind = LOWER_ERROR_DUPLICATE_FUNCTION, .span = span, .name = name });
         return false;
     }
 
@@ -517,10 +595,9 @@ lower_register_type_shell(struct Unit* unit, struct String name, struct Span spa
 {
     Type_Id existing;
     if (lower_type_lookup_by_name(unit, name, &existing)) {
-        lower_emit_error(
-            unit, span,
-            string_concatenate(
-                ARG_ASCII, "duplicate type '", ARG_STRING, name, ARG_ASCII, "'", ARG_END));
+        lower_push_error(
+            unit,
+            (struct Lower_Error){ .kind = LOWER_ERROR_DUPLICATE_TYPE, .span = span, .name = name });
         return false;
     }
 
@@ -570,11 +647,13 @@ lower_fill_type_body(struct Lower_Context* ctx, struct Type* type, struct Tree_T
             // aliases always need their target's full definition.
             array_push(&type->depends_on, &target_id);
         } else {
-            lower_emit_error(
-                ctx->unit, tree_type->span,
-                string_concatenate(
-                    ARG_ASCII, "undefined type '", ARG_STRING, tree_type->value.name.name,
-                    ARG_ASCII, "'", ARG_END));
+            lower_push_error(
+                ctx->unit,
+                (struct Lower_Error){
+                    .kind = LOWER_ERROR_UNDEFINED_TYPE,
+                    .span = tree_type->span,
+                    .name = tree_type->value.name.name,
+                });
         }
         break;
     }
@@ -628,7 +707,13 @@ lower_fill_type_body(struct Lower_Context* ctx, struct Type* type, struct Tree_T
         break;
     }
     case TREE_TYPE_CLASS:
-        lower_emit_error_c(ctx->unit, tree_type->span, "unimplemented: class types");
+        lower_push_error(
+            ctx->unit,
+            (struct Lower_Error){
+                .kind = LOWER_ERROR_UNIMPLEMENTED,
+                .span = tree_type->span,
+                .detail = string_from_static_c_string("class types"),
+            });
         break;
     default: {
         // this is a type alias assigning a name to a structural type.
@@ -698,11 +783,19 @@ lower_pass_1_fill_bodies(struct Lower_Context* ctx, struct Tree* tree)
         }
 
         if (stmt->kind == TREE_STATEMENT_PRAGMA) {
-            lower_emit_error_c(ctx->unit, stmt->span, "unimplemented: top-level pragmas");
+            lower_push_error(
+                ctx->unit,
+                (struct Lower_Error){
+                    .kind = LOWER_ERROR_UNIMPLEMENTED,
+                    .span = stmt->span,
+                    .detail = string_from_static_c_string("top-level pragmas"),
+                });
             continue;
         }
 
-        lower_emit_error_c(ctx->unit, stmt->span, "unsupported top-level statement");
+        lower_push_error(
+            ctx->unit,
+            (struct Lower_Error){ .kind = LOWER_ERROR_UNSUPPORTED_TOP_LEVEL, .span = stmt->span });
     }
 }
 
@@ -778,12 +871,19 @@ lower_expression_binary_operation(struct Lower_Context* ctx, struct Tree_Express
     enum Binary_Operation op = tree_expr->value.binary_operator.operation;
     // TODO: maybe we want to support assignment expressions some day. today they aren't.
     if (op >= BINARY_ASSIGN) {
-        lower_emit_error_c(ctx->unit, tree_expr->span, "assignment cannot appear as an expression");
+        lower_push_error(
+            ctx->unit,
+            (struct Lower_Error){
+                .kind = LOWER_ERROR_ASSIGNMENT_AS_EXPRESSION,
+                .span = tree_expr->span,
+            });
         return nil;
     }
     if (op == BINARY_RANGE) {
-        lower_emit_error_c(
-            ctx->unit, tree_expr->span, "range expressions are only valid in loop initializers");
+        lower_push_error(
+            ctx->unit,
+            (struct Lower_Error){
+                .kind = LOWER_ERROR_RANGE_OUTSIDE_LOOP, .span = tree_expr->span });
         return nil;
     }
     return ir_make_binary(
@@ -800,7 +900,13 @@ lower_expression_call(struct Lower_Context* ctx, struct Tree_Expression* tree_ex
     // table to reorder, we only handle positional arguments today.
     FOR_EACH_ARRAY (struct String, name, &group->argument_names) {
         if (name->length > 0) {
-            lower_emit_error_c(ctx->unit, tree_expr->span, "unimplemented: named call arguments");
+            lower_push_error(
+                ctx->unit,
+                (struct Lower_Error){
+                    .kind = LOWER_ERROR_UNIMPLEMENTED,
+                    .span = tree_expr->span,
+                    .detail = string_from_static_c_string("named call arguments"),
+                });
             return nil;
         }
     }
@@ -819,16 +925,23 @@ lower_expression_construct(struct Lower_Context* ctx, struct Tree_Expression* tr
 {
     struct Tree_Expression* subject = tree_expr->value.construct.subject;
     if (!subject || subject->kind != TREE_EXPRESSION_NAME) {
-        lower_emit_error_c(ctx->unit, tree_expr->span, "construction subject must be a type name");
+        lower_push_error(
+            ctx->unit,
+            (struct Lower_Error){
+                .kind = LOWER_ERROR_CONSTRUCT_SUBJECT_NOT_NAME,
+                .span = tree_expr->span,
+            });
         return nil;
     }
     Type_Id type_id;
     if (!lower_type_lookup_by_name(ctx->unit, subject->value.name.name, &type_id)) {
-        lower_emit_error(
-            ctx->unit, subject->span,
-            string_concatenate(
-                ARG_ASCII, "undefined type '", ARG_STRING, subject->value.name.name, ARG_ASCII, "'",
-                ARG_END));
+        lower_push_error(
+            ctx->unit,
+            (struct Lower_Error){
+                .kind = LOWER_ERROR_UNDEFINED_TYPE,
+                .span = subject->span,
+                .name = subject->value.name.name,
+            });
         return nil;
     }
 
@@ -849,13 +962,15 @@ lower_expression_construct(struct Lower_Context* ctx, struct Tree_Expression* tr
     return ir_make_construct(type_id, fields, tree_expr->span);
 }
 
-// type-as-expression is only valid as the right-side of a top-level binding;
+// type-as-expression is only valid as the right-side of a top-level binding,
 // anywhere else it is an error.
 struct Expression*
 lower_expression_type(struct Lower_Context* ctx, struct Tree_Expression* tree_expr)
 {
-    lower_emit_error_c(
-        ctx->unit, tree_expr->span, "type expression not allowed inside a function body");
+    lower_push_error(
+        ctx->unit,
+        (struct Lower_Error){
+            .kind = LOWER_ERROR_TYPE_EXPRESSION_IN_BODY, .span = tree_expr->span });
     return nil;
 }
 
@@ -918,7 +1033,13 @@ lower_expression(struct Lower_Context* ctx, struct Tree_Expression* tree_expr)
     case TREE_EXPRESSION_MUST:
         // TODO: implement these
     default:
-        lower_emit_error_c(ctx->unit, tree_expr->span, "unimplemented: this expression kind");
+        lower_push_error(
+            ctx->unit,
+            (struct Lower_Error){
+                .kind = LOWER_ERROR_UNIMPLEMENTED,
+                .span = tree_expr->span,
+                .detail = string_from_static_c_string("this expression kind"),
+            });
         return nil;
     }
 }
@@ -997,17 +1118,25 @@ lower_statement_declaration(struct Lower_Context* ctx, struct Tree_Statement* tr
     // TODO: multi-name decls like `var a, b int = ...` need to split into
     // N sibling declarations or introduce a temporary for the initializer.
     if (array_length(&decl->names) != 1) {
-        lower_emit_error_c(ctx->unit, tree_stmt->span, "unimplemented: multi-name declarations");
+        lower_push_error(
+            ctx->unit,
+            (struct Lower_Error){
+                .kind = LOWER_ERROR_UNIMPLEMENTED,
+                .span = tree_stmt->span,
+                .detail = string_from_static_c_string("multi-name declarations"),
+            });
         return nil;
     }
 
     struct String name = *array_at(struct String, &decl->names, 0);
     if (lower_is_shadowing(ctx, name)) {
-        lower_emit_error(
-            ctx->unit, tree_stmt->span,
-            string_concatenate(
-                ARG_ASCII, "name '", ARG_STRING, name, ARG_ASCII, "' shadows an existing binding",
-                ARG_END));
+        lower_push_error(
+            ctx->unit,
+            (struct Lower_Error){
+                .kind = LOWER_ERROR_NAME_SHADOWS,
+                .span = tree_stmt->span,
+                .name = name,
+            });
         return nil;
     }
     struct Type_Ref type = lower_intern_type_ref(ctx, decl->type);
@@ -1039,16 +1168,26 @@ lower_statement_expression(struct Lower_Context* ctx, struct Tree_Statement* tre
             if (!lhs_tree || lhs_tree->kind != TREE_EXPRESSION_NAME) {
                 // TODO: implement non-trivial lhs assignments like function
                 // calls for example. not too common but sometimes necessary.
-                lower_emit_error_c(
-                    ctx->unit, tree_stmt->span,
-                    "unimplemented: compound assignment with non-trivial lvalue");
+                lower_push_error(
+                    ctx->unit,
+                    (struct Lower_Error){
+                        .kind = LOWER_ERROR_UNIMPLEMENTED,
+                        .span = tree_stmt->span,
+                        .detail = string_from_static_c_string(
+                            "compound assignment with "
+                            "non-trivial lvalue"),
+                    });
                 return nil;
             }
 
             enum Binary_Operation simple_op = binary_operation_strip_assign(op);
             if (simple_op == BINARY_NONE) {
-                lower_emit_error_c(
-                    ctx->unit, tree_stmt->span, "unknown compound assignment operator");
+                lower_push_error(
+                    ctx->unit,
+                    (struct Lower_Error){
+                        .kind = LOWER_ERROR_UNKNOWN_COMPOUND_ASSIGN,
+                        .span = tree_stmt->span,
+                    });
                 return nil;
             }
 
@@ -1064,9 +1203,15 @@ lower_statement_expression(struct Lower_Context* ctx, struct Tree_Statement* tre
         struct Tree_Expression* subject = incdec->subject;
         if (!subject || subject->kind != TREE_EXPRESSION_NAME) {
             // TODO: handle this too, like compound assignments
-            lower_emit_error_c(
-                ctx->unit, tree_stmt->span,
-                "unimplemented: increment/decrement on non-trivial lvalue");
+            lower_push_error(
+                ctx->unit,
+                (struct Lower_Error){
+                    .kind = LOWER_ERROR_UNIMPLEMENTED,
+                    .span = tree_stmt->span,
+                    .detail = string_from_static_c_string(
+                        "increment/decrement on non-trivial "
+                        "lvalue"),
+                });
             return nil;
         }
 
@@ -1140,9 +1285,15 @@ lower_statement_loop(struct Lower_Context* ctx, struct Tree_Statement* tree_stmt
         if (loop->style == TREE_STATEMENT_LOOP_STYLE_FOR_EACH) {
             if (!decl->initializer || decl->initializer->kind != TREE_EXPRESSION_BINARY_OPERATION
                 || decl->initializer->value.binary_operator.operation != BINARY_RANGE) {
-                lower_emit_error_c(
-                    ctx->unit, tree_stmt->span,
-                    "unimplemented: for-each over non-range collections");
+                lower_push_error(
+                    ctx->unit,
+                    (struct Lower_Error){
+                        .kind = LOWER_ERROR_UNIMPLEMENTED,
+                        .span = tree_stmt->span,
+                        .detail = string_from_static_c_string(
+                            "for-each over non-range "
+                            "collections"),
+                    });
                 return nil;
             }
             init = decl->initializer->value.binary_operator.left_operand;
@@ -1152,8 +1303,13 @@ lower_statement_loop(struct Lower_Context* ctx, struct Tree_Statement* tree_stmt
         }
 
         if (array_length(&decl->names) != 1) {
-            lower_emit_error_c(
-                ctx->unit, tree_stmt->span, "unimplemented: for-loop with multi-name declaration");
+            lower_push_error(
+                ctx->unit,
+                (struct Lower_Error){
+                    .kind = LOWER_ERROR_UNIMPLEMENTED,
+                    .span = tree_stmt->span,
+                    .detail = string_from_static_c_string("for-loop with multi-name declaration"),
+                });
             return nil;
         }
         struct String name = *array_at(struct String, &decl->names, 0);
@@ -1163,11 +1319,13 @@ lower_statement_loop(struct Lower_Context* ctx, struct Tree_Statement* tree_stmt
         lower_push_scope(ctx, SCOPE_TYPE_BLOCK);
 
         if (lower_is_shadowing(ctx, name)) {
-            lower_emit_error(
-                ctx->unit, tree_stmt->span,
-                string_concatenate(
-                    ARG_ASCII, "name '", ARG_STRING, name, ARG_ASCII,
-                    "' shadows an existing binding", ARG_END));
+            lower_push_error(
+                ctx->unit,
+                (struct Lower_Error){
+                    .kind = LOWER_ERROR_NAME_SHADOWS,
+                    .span = tree_stmt->span,
+                    .name = name,
+                });
             lower_pop_scope(ctx, SCOPE_TYPE_BLOCK);
             return nil;
         }
@@ -1226,7 +1384,10 @@ lower_statement_loop(struct Lower_Context* ctx, struct Tree_Statement* tree_stmt
         return ir_make_block_statement(outer, tree_stmt->span);
     }
     default:
-        lower_emit_error_c(ctx->unit, tree_stmt->span, "unknown loop style");
+        lower_push_error(
+            ctx->unit,
+            (struct Lower_Error){
+                .kind = LOWER_ERROR_UNKNOWN_LOOP_STYLE, .span = tree_stmt->span });
         return nil;
     }
 }
@@ -1267,7 +1428,13 @@ lower_statement(struct Lower_Context* ctx, struct Tree_Statement* tree_stmt)
     case TREE_STATEMENT_CONTINUE:
         return lower_statement_continue(ctx, tree_stmt);
     default:
-        lower_emit_error_c(ctx->unit, tree_stmt->span, "unimplemented: this statement kind");
+        lower_push_error(
+            ctx->unit,
+            (struct Lower_Error){
+                .kind = LOWER_ERROR_UNIMPLEMENTED,
+                .span = tree_stmt->span,
+                .detail = string_from_static_c_string("this statement kind"),
+            });
         return nil;
     }
 }
@@ -1317,50 +1484,32 @@ lower_pass_2(struct Lower_Context* ctx)
 uint
 lower_span_to_line(struct String source, struct Span span)
 {
-    uint line = 1;
-    uint upto = span.start;
-    if (upto > string_length(source)) upto = string_length(source);
-    for (uint i = 0; i < upto; ++i) {
-        if (string_at(source, i) == '\n') line++;
-    }
+    uint line, column;
+    source_position_from_span(source, span, &line, &column);
     return line;
 }
 
-// builds a nice & friendly cycle detection message and emits it as a diagnostic.
-// `chain` is the path we walked while looking for the cycle, and
-// `cycle_start` is the chain index at which the cycle closes.
+// `chain` is the walk we took while looking for the cycle, `cycle_start` is
+// the index in it at which the cycle closes back on itself.
 void
-lower_report_type_cycle(
-    struct Unit* unit, struct Source_File source, struct _Array* chain, uint cycle_start)
+lower_report_type_cycle(struct Unit* unit, struct _Array* chain, uint cycle_start)
 {
-    struct String_Buffer buf = string_buffer_new(512);
-    string_buffer_append_c_str(&buf, "type cycle detected:\n");
-
     uint chain_len = array_length(chain);
+    Array(Type_Id) cycle_copy = array_new(Type_Id, chain_len - cycle_start);
     for (uint k = cycle_start; k < chain_len; ++k) {
-        Type_Id current_id = *array_at(Type_Id, chain, k);
-        Type_Id next_id;
-        if (k + 1 < chain_len)
-            next_id = *array_at(Type_Id, chain, k + 1);
-        else
-            next_id = *array_at(Type_Id, chain, cycle_start);
-
-        struct Type* current = *array_at(struct Type*, &unit->types.entries, current_id);
-        struct Type* next = *array_at(struct Type*, &unit->types.entries, next_id);
-        uint line = lower_span_to_line(source.source, current->span);
-
-        ascii line_buf[256];
-        snprintf(
-            line_buf, sizeof line_buf, "  %s (line %lu) depends on %s\n",
-            string_c_str(current->name), line, string_c_str(next->name));
-        string_buffer_append_c_str(&buf, line_buf);
+        Type_Id id = *array_at(Type_Id, chain, k);
+        array_push(&cycle_copy, &id);
     }
-    string_buffer_append_c_str(
-        &buf, "hint: break the cycle with a reference (use `&T` instead of `T`). :)");
 
     Type_Id origin_id = *array_at(Type_Id, chain, cycle_start);
     struct Type* origin = *array_at(struct Type*, &unit->types.entries, origin_id);
-    lower_emit_error(unit, origin->span, string_buffer_to_string(&buf));
+    lower_push_error(
+        unit,
+        (struct Lower_Error){
+            .kind = LOWER_ERROR_TYPE_CYCLE,
+            .span = origin->span,
+            .cycle_chain = cycle_copy,
+        });
 }
 
 // topologically sorting the unit's type dependency graph.
@@ -1370,7 +1519,7 @@ lower_report_type_cycle(
 // are always first to emit when their vertex in-degree is 0.
 // cycles reported as diagnostic, partial emission order will still be completed.
 void
-lower_topological_sort_dependency_graph(struct Unit* unit, struct Source_File source)
+lower_topological_sort_dependency_graph(struct Unit* unit)
 {
     const uint done_sentinel = (uint)-1; // marks vertecies that have been processed
 
@@ -1461,7 +1610,7 @@ lower_topological_sort_dependency_graph(struct Unit* unit, struct Source_File so
             }
         }
         if (found_at < chain_len) {
-            lower_report_type_cycle(unit, source, &chain, found_at);
+            lower_report_type_cycle(unit, &chain, found_at);
             return;
         }
 
@@ -1471,7 +1620,7 @@ lower_topological_sort_dependency_graph(struct Unit* unit, struct Source_File so
 }
 
 void
-lower_tree(struct Tree* tree, struct Source_File source, struct Unit* unit)
+lower_tree(struct Tree* tree, struct Unit* unit)
 {
     unit->types.entries = array_new(struct Type*, 256);
     unit->types.by_hash = array_new(struct Type_Hash_To_Id, 256);
@@ -1483,7 +1632,7 @@ lower_tree(struct Tree* tree, struct Source_File source, struct Unit* unit)
     unit->imports = array_new(struct Import, 32);
     unit->type_emission_order = array_new(Type_Id, 256);
     unit->had_error = false;
-    unit->diagnostics = array_new(struct Diagnostic, 64);
+    unit->lower_errors = array_new(struct Lower_Error, 64);
 
     unit->types.primitive_int_id = lower_seed_primitive(unit, "int");
     unit->types.primitive_uint_id = lower_seed_primitive(unit, "uint");
@@ -1496,7 +1645,6 @@ lower_tree(struct Tree* tree, struct Source_File source, struct Unit* unit)
 
     struct Lower_Context ctx = {
         .unit = unit,
-        .source = source,
         .synthetic_type_counter = 0,
         .synthetic_counter = 0,
         .scope_stack = array_new(struct Scope, 32),
@@ -1510,5 +1658,5 @@ lower_tree(struct Tree* tree, struct Source_File source, struct Unit* unit)
         lower_pass_1(&ctx, tree);
         lower_pass_2(&ctx);
     }
-    lower_topological_sort_dependency_graph(unit, source);
+    lower_topological_sort_dependency_graph(unit);
 }
diff --git a/boot/parse.c b/boot/parse.c
index db5e029..2b5d859 100644
--- a/boot/parse.c
+++ b/boot/parse.c
@@ -103,56 +103,22 @@ parser_error_is_none(const struct Parser_Error* error)
     return error->kind == PARSER_ERROR_NONE;
 }
 
-struct Span
-token_span_to_line_span(struct Span span, struct String source)
-{
-    Pos line_start = span.start, line_end = span.start;
-
-    // go backwards from the start of the token's span to find line start.
-    while (line_start > 0 && string_at(source, line_start - 1) != '\n') line_start--;
-    // go forwards from the end of the token's span to find line end.
-    while (line_end < string_length(source) && string_at(source, line_end) != '\n') line_end++;
-
-    return span_new(line_start, line_end);
-}
-
-// print out nice, human-readable error message
-// pointing to the location of the error in the source file.
-// TODO: bring out the infrastructure for displaying errors in the same format
-// outside of the parser, so that it can be used in other places.
 void
 parser_error_display(const struct Parser_Error* error, struct Source_File source_file)
 {
     if (parser_error_is_none(error)) return;
 
-    uint line = error->cause.location.line;
-    uint column = error->cause.location.column;
-
-    struct Token cause = error->cause;
-
-    STRING_FORMAT_TO(source_file.path, stderr, ANSI_WHITE "%s:%lu:%lu:\n", line, column);
-    fprintf(stderr, ANSI_BOLD ANSI_RED "error: " ANSI_WHITE);
+    struct String_View lexeme = token_lexeme(&error->cause, source_file.source);
     if (error->subkind != PARSER_ERROR_NONE) {
-        fprintf(stderr, "%s: %s", parser_error_kind_to_string(error->kind),
-                parser_error_kind_to_string(error->subkind));
+        diagnostic_render(
+            source_file, stderr, DIAGNOSTIC_ERROR, error->cause.span, "%s: %s, got '%S'",
+            parser_error_kind_to_string(error->kind), parser_error_kind_to_string(error->subkind),
+            STR(lexeme));
     } else {
-        fprintf(stderr, "%s", parser_error_kind_to_string(error->kind));
+        diagnostic_render(
+            source_file, stderr, DIAGNOSTIC_ERROR, error->cause.span, "%s, got '%S'",
+            parser_error_kind_to_string(error->kind), STR(lexeme));
     }
-    struct String_View cause_lexeme = token_lexeme(&cause, source_file.source);
-    STRING_FORMAT_TO(cause_lexeme, stderr, ", got '%S' :(\n");
-
-    struct Span line_span = token_span_to_line_span(cause.span, source_file.source);
-
-    struct String_View source_line =
-        string_substring(source_file.source, line_span.start, line_span.end);
-
-    fprintf(stderr, ANSI_WHITE ANSI_NO_BOLD "%lu| ", line);
-    STRING_FORMAT_TO(source_line, stderr, "%S\n");
-
-    uint line_number_length = ceil(log10(line + 1));
-    fprintf(stderr, ANSI_RED "%*s", (int)(column + 1 + line_number_length), " ");
-    for (uint w = 0; w < span_length(cause.span); w++) { fprintf(stderr, "^"); }
-    fprintf(stderr, "\n" ANSI_RESET);
 }
 
 // a parser context descibes the current surrounding structure
@@ -405,7 +371,8 @@ parser_block_node(struct Parser* p, struct Parser_Error* error)
     struct Tree_Statement* current = nil;
 
     while (!parser_probe(p, TOKEN_CURLY_CLOSE)) {
-        struct Tree_Statement* statement = CHECK_RETURN(parser_statement(p, error), struct Tree_Block);
+        struct Tree_Statement* statement =
+            CHECK_RETURN(parser_statement(p, error), struct Tree_Block);
 
         // statement ending token isn't required when the block ends on the same line,
         // as in e.g.: `if (true) { print("yes") }`
@@ -956,7 +923,8 @@ parser_expression_primary_boolean(struct Parser* p, struct Parser_Error* error)
           "expected boolean literal");
     bool literal = token.kind == TOKEN_WORD_TRUE;
     union Tree_Expression_Value expr_value = { .bool_literal = { literal } };
-    return tree_expression_new(TREE_EXPRESSION_BOOLEAN_LITERAL, expr_value, token.span, token.location);
+    return tree_expression_new(
+        TREE_EXPRESSION_BOOLEAN_LITERAL, expr_value, token.span, token.location);
 }
 
 struct Tree_Expression*
@@ -1008,7 +976,8 @@ parser_expression_type(struct Parser* p, struct Parser_Error* error)
 
     struct Span span = span_merge(start_token.span, type->span);
     return tree_expression_new(
-        TREE_EXPRESSION_TYPE, (union Tree_Expression_Value){ .type = { type } }, span, start_token.location);
+        TREE_EXPRESSION_TYPE, (union Tree_Expression_Value){ .type = { type } }, span,
+        start_token.location);
 }
 
 struct Tree_Expression*
@@ -1198,8 +1167,8 @@ parser_expression_postfix(struct Parser* p, struct Parser_Error* error)
 
             struct Span span = span_merge(expression->span, token.span);
             union Tree_Expression_Value value = { .increment_decrement = inc_dec };
-            expression =
-                tree_expression_new(TREE_EXPRESSION_INCREMENT_DECREMENT, value, span, token.location);
+            expression = tree_expression_new(
+                TREE_EXPRESSION_INCREMENT_DECREMENT, value, span, token.location);
             continue;
         }
 
@@ -1234,7 +1203,8 @@ parser_expression_unary_operation(struct Parser* p, struct Parser_Error* error)
 
         struct Span span = span_merge(token.span, inc_dec.subject->span);
         union Tree_Expression_Value value = { .increment_decrement = inc_dec };
-        return tree_expression_new(TREE_EXPRESSION_INCREMENT_DECREMENT, value, span, token.location);
+        return tree_expression_new(
+            TREE_EXPRESSION_INCREMENT_DECREMENT, value, span, token.location);
     }
 
     return parser_expression_postfix(p, error);
@@ -1357,7 +1327,8 @@ parser_statement_conditional(struct Parser* p, struct Parser_Error* error)
     CONTEXT_END(in_statement_clause);
 
     struct Tree_Block then_block = CHECK(parser_block_node(p, error));
-    conditional.conditions[conditional.condition_count++] = (struct Tree_Statement_Conditional_Branch){
+    conditional
+        .conditions[conditional.condition_count++] = (struct Tree_Statement_Conditional_Branch){
         .when = if_condition,
         .then = then_block,
     };
@@ -1397,8 +1368,8 @@ parser_statement_conditional(struct Parser* p, struct Parser_Error* error)
     }
 
     return tree_statement_new(
-        TREE_STATEMENT_CONDITIONAL, (union Tree_Statement_Value){ .conditional = conditional }, span,
-        if_token.location);
+        TREE_STATEMENT_CONDITIONAL, (union Tree_Statement_Value){ .conditional = conditional },
+        span, if_token.location);
 }
 
 struct Tree_Statement*
@@ -1475,7 +1446,8 @@ parser_statement_block(struct Parser* p, struct Parser_Error* error)
 {
     struct Tree_Block block = CHECK(parser_block_node(p, error));
     return tree_statement_new(
-        TREE_STATEMENT_BLOCK, (union Tree_Statement_Value){ .block = { block } }, block.span, block.location);
+        TREE_STATEMENT_BLOCK, (union Tree_Statement_Value){ .block = { block } }, block.span,
+        block.location);
 }
 
 struct Tree_Statement*
@@ -1496,7 +1468,8 @@ parser_statement_break(struct Parser* p, struct Parser_Error* error)
 {
     struct Token break_token = CHECK(parser_need(p, TOKEN_WORD_BREAK, error));
     struct Span span = break_token.span;
-    return tree_statement_new(TREE_STATEMENT_BREAK, (union Tree_Statement_Value){ 0 }, span, break_token.location);
+    return tree_statement_new(
+        TREE_STATEMENT_BREAK, (union Tree_Statement_Value){ 0 }, span, break_token.location);
 }
 
 struct Tree_Statement*
diff --git a/boot/tests/lower/shadowing_error.cskt b/boot/tests/lower/shadowing_error.cskt
index b22dddf..34eacc4 100644
--- a/boot/tests/lower/shadowing_error.cskt
+++ b/boot/tests/lower/shadowing_error.cskt
@@ -25,5 +25,5 @@ main = fun () {
 			(declaration x (ref int) (initializer (expr 1)))
 		)))
 	(emission_order 0 1 2 3 4 5 6 7)
-	(diagnostics
-		(error "name 'x' shadows an existing binding")))
+	(lower_errors
+		(name-shadows x)))
\ No newline at end of file
diff --git a/boot/tests/lower/type_cycle_error.cskt b/boot/tests/lower/type_cycle_error.cskt
index 29b791e..c6bfb6c 100644
--- a/boot/tests/lower/type_cycle_error.cskt
+++ b/boot/tests/lower/type_cycle_error.cskt
@@ -22,8 +22,5 @@ B = type { a A }
 		(type 8 A structure (field b (ref B)) (depends_on 9))
 		(type 9 B structure (field a (ref A)) (depends_on 8)))
 	(emission_order 0 1 2 3 4 5 6 7)
-	(diagnostics
-		(error "type cycle detected:
-  A (line 1) depends on B
-  B (line 2) depends on A
-hint: break the cycle with a reference (use `&T` instead of `T`). :)")))
+	(lower_errors
+		(type-cycle (chain A B))))
\ No newline at end of file
diff --git a/boot/visit/ir.c b/boot/visit/ir.c
index 1920f3b..5317276 100644
--- a/boot/visit/ir.c
+++ b/boot/visit/ir.c
@@ -66,7 +66,7 @@ struct Visit_Table
     void (*visit_block)(struct Visit* visitor, struct Block* block);
     void (*visit_type_ref)(struct Visit* visitor, struct Type_Ref* ref);
     void (*visit_import)(struct Visit* visitor, struct Import* import);
-    void (*visit_diagnostic)(struct Visit* visitor, struct Diagnostic* diagnostic);
+    void (*visit_lower_error)(struct Visit* visitor, struct Lower_Error* error);
 };
 
 void
@@ -82,8 +82,7 @@ walk_unit(struct Visit* visit, struct Unit* unit)
     FOR_EACH_ARRAY (struct Function*, function, &unit->functions.entries)
         VISIT(visit_function, *function);
     FOR_EACH_ARRAY (struct Import, import, &unit->imports) VISIT(visit_import, import);
-    FOR_EACH_ARRAY (struct Diagnostic, diagnostic, &unit->diagnostics)
-        VISIT(visit_diagnostic, diagnostic);
+    FOR_EACH_ARRAY (struct Lower_Error, error, &unit->lower_errors) VISIT(visit_lower_error, error);
 }
 
 void
@@ -362,7 +361,7 @@ walk_block(struct Visit* visit, struct Block* block)
 
 WALK_LEAF_FUNCTION(walk_type_ref, struct Type_Ref*);
 WALK_LEAF_FUNCTION(walk_import, struct Import*);
-WALK_LEAF_FUNCTION(walk_diagnostic, struct Diagnostic*);
+WALK_LEAF_FUNCTION(walk_lower_error, struct Lower_Error*);
 
 struct Visit_Table walk_functions = {
     .visit_unit = walk_unit,
@@ -407,7 +406,7 @@ struct Visit_Table walk_functions = {
     .visit_block = walk_block,
     .visit_type_ref = walk_type_ref,
     .visit_import = walk_import,
-    .visit_diagnostic = walk_diagnostic,
+    .visit_lower_error = walk_lower_error,
 };
 
 void
@@ -515,15 +514,15 @@ printer_visit_unit(struct Visit* visit, struct Unit* unit)
         PRINT(")");
     }
 
-    if (array_length(&unit->diagnostics) > 0) {
+    if (array_length(&unit->lower_errors) > 0) {
         PRINT("\n");
         printer_indent(p);
-        PRINT("(diagnostics");
+        PRINT("(lower_errors");
         p->indentation_level++;
-        FOR_EACH_ARRAY (struct Diagnostic, diagnostic, &unit->diagnostics) {
+        FOR_EACH_ARRAY (struct Lower_Error, error, &unit->lower_errors) {
             PRINT("\n");
             printer_indent(p);
-            VISIT(visit_diagnostic, diagnostic);
+            VISIT(visit_lower_error, error);
         }
         p->indentation_level--;
         PRINT(")");
@@ -947,15 +946,63 @@ printer_visit_import(struct Visit* visit, struct Import* import)
 }
 
 void
-printer_visit_diagnostic(struct Visit* visit, struct Diagnostic* diagnostic)
+printer_visit_lower_error(struct Visit* visit, struct Lower_Error* error)
 {
     PRINTER_PREAMBLE
-    const ascii* sev = "note";
-    if (diagnostic->severity == DIAGNOSTIC_WARNING)
-        sev = "warning";
-    else if (diagnostic->severity == DIAGNOSTIC_ERROR)
-        sev = "error";
-    PRINT("(%s \"%s\")", sev, string_c_str(diagnostic->message));
+
+    switch (error->kind) {
+    case LOWER_ERROR_UNDEFINED_TYPE:
+        PRINT("(undefined-type %s)", string_c_str(error->name));
+        return;
+    case LOWER_ERROR_DUPLICATE_TYPE:
+        PRINT("(duplicate-type %s)", string_c_str(error->name));
+        return;
+    case LOWER_ERROR_DUPLICATE_FUNCTION:
+        PRINT("(duplicate-function %s)", string_c_str(error->name));
+        return;
+    case LOWER_ERROR_NAME_SHADOWS:
+        PRINT("(name-shadows %s)", string_c_str(error->name));
+        return;
+    case LOWER_ERROR_TYPE_CYCLE: {
+        PRINT("(type-cycle (chain");
+        FOR_EACH_ARRAY (Type_Id, id, &error->cycle_chain) {
+            struct Type* type = printer_type_at(p, *id);
+            if (type)
+                PRINT(" %s", string_c_str(type->name));
+            else
+                PRINT(" #%lu", *id);
+        }
+        PRINT("))");
+        return;
+    }
+    case LOWER_ERROR_ASSIGNMENT_AS_EXPRESSION:
+        PRINT("(assignment-as-expression)");
+        return;
+    case LOWER_ERROR_RANGE_OUTSIDE_LOOP:
+        PRINT("(range-outside-loop)");
+        return;
+    case LOWER_ERROR_CONSTRUCT_SUBJECT_NOT_NAME:
+        PRINT("(construct-subject-not-name)");
+        return;
+    case LOWER_ERROR_TYPE_EXPRESSION_IN_BODY:
+        PRINT("(type-expression-in-body)");
+        return;
+    case LOWER_ERROR_UNSUPPORTED_TOP_LEVEL:
+        PRINT("(unsupported-top-level)");
+        return;
+    case LOWER_ERROR_UNKNOWN_LOOP_STYLE:
+        PRINT("(unknown-loop-style)");
+        return;
+    case LOWER_ERROR_UNKNOWN_COMPOUND_ASSIGN:
+        PRINT("(unknown-compound-assign)");
+        return;
+    case LOWER_ERROR_UNIMPLEMENTED:
+        PRINT("(unimplemented \"%s\")", string_c_str(error->detail));
+        return;
+    default:
+        PRINT("(unknown-error)");
+        return;
+    }
 }
 
 struct Visit_Table printer_visit_functions = {
@@ -1001,7 +1048,7 @@ struct Visit_Table printer_visit_functions = {
     .visit_block = printer_visit_block,
     .visit_type_ref = printer_visit_type_ref,
     .visit_import = printer_visit_import,
-    .visit_diagnostic = printer_visit_diagnostic,
+    .visit_lower_error = printer_visit_lower_error,
 };
 
 void