Files
bux-lang/docs/superpowers/plans/2026-06-10-source-location-errors.md
T

13 KiB

Source Location Error Messages Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add Rust-style error formatting with code snippets to all parser and sema diagnostic output in the Bux CLI.

Architecture: A Diagnostic struct and Diagnostic_Print helper are added to src/cli.bux. Lexer and sema error loops in CLI are updated to use the unified formatter. Parser diagnostics are printed inline by Parser_Parse using the same format. No changes to lexer, parser, or sema internals — they already emit line/column/message.

Tech Stack: Bux (selfhost compiler), stdlib file I/O


File Structure

File Responsibility
src/cli.bux Add Diagnostic struct, Diagnostic_GetLine, Diagnostic_Print. Replace all manual error printing loops.
src/parser.bux Add inline diagnostic printing at end of Parser_Parse before returning.
_test_error_snippet/src/Main.bux Test program with intentional type error to verify output format.
_test_error_snippet/bux.toml Test manifest.

Task 1: Add Diagnostic Formatter to src/cli.bux

Files:

  • Modify: src/cli.bux (add structs + helpers near the top, before first function)

Context: The file currently has no unified diagnostic type. We add Diagnostic, Diagnostic_GetLine, and Diagnostic_Print as new top-level items.

  • Step 1: Add Diagnostic struct and helpers

Insert after the imports / module declaration, before any function in src/cli.bux:

struct Diagnostic {
    message: String;
    line: uint32;
    column: uint32;
    severity: int;
}

/* Read a single line from a file (1-based). Returns "" on error or EOF. */
func Diagnostic_GetLine(path: String, lineNum: uint32) -> String {
    let fd: int = Open(path);
    if fd < 0 { return ""; }
    
    var buf: String = "";
    var currentLine: uint32 = 1;
    var c: int = ReadChar(fd);
    
    /* Skip lines until we reach lineNum */
    while currentLine < lineNum && c >= 0 {
        if c == 10 { /* '\n' */
            currentLine = currentLine + 1;
        }
        c = ReadChar(fd);
    }
    
    /* Collect characters until newline or EOF */
    while c >= 0 && c != 10 {
        buf = String_Concat(buf, String_FromChar(c as uint8));
        c = ReadChar(fd);
    }
    
    Close(fd);
    return buf;
}

/* Print a diagnostic in Rust-style format:
 *   error: <message>
 *     --> <path>:<line>:<col>
 *      |
 *   42 | <source_line>
 *      | <spaces>^
 */
func Diagnostic_Print(diag: *Diagnostic, sourcePath: String) {
    /* Severity prefix */
    if diag.severity == 0 {
        Print("error: ");
    } else if diag.severity == 1 {
        Print("warning: ");
    } else {
        Print("note: ");
    }
    PrintLine(diag.message);
    
    /* Location header */
    Print("  --> ");
    Print(sourcePath);
    Print(":");
    PrintInt(diag.line as int64);
    Print(":");
    PrintInt(diag.column as int64);
    PrintLine("");
    
    /* Source snippet */
    let lineText: String = Diagnostic_GetLine(sourcePath, diag.line);
    if !String_Eq(lineText, "") {
        let lineNumStr: String = String_FromInt(diag.line as int64);
        
        Print("   |");
        PrintLine("");
        Print(" ");
        Print(lineNumStr);
        Print(" | ");
        PrintLine(lineText);
        
        /* Underline */
        Print("   | ");
        var i: uint32 = 0;
        while i < diag.column - 1 && i < 120 {
            Print(" ");
            i = i + 1;
        }
        PrintLine("^");
    }
}
  • Step 2: Verify no syntax errors

Run: cd /home/ziko/z-git/bux/bux && ./build/buxc build Expected: Should compile the test project or at least not crash immediately. Since we're modifying the compiler source, we need to use the bootstrap compiler:

Run: cd /home/ziko/z-git/bux/bux && make buxc Expected: Bootstrap compiler builds successfully (Nim compiler compiles bootstrap).

  • Step 3: Commit
git add src/cli.bux
git commit -m "feat(diag): add Diagnostic struct and Rust-style formatter"

Task 2: Update Lexer Error Printing in CLI

Files:

  • Modify: src/cli.bux lines 49-54

Context: Current lexer error loop (in Cli_Compile or similar):

var i: int = 0;
while i < Lexer_DiagCount(lex) {
    Print("  ");
    PrintLine(lex.diags[i].message);
    i = i + 1;
}
  • Step 1: Replace lexer error loop with Diagnostic_Print

Replace the lexer error loop at lines 49-54:

var i: int = 0;
while i < Lexer_DiagCount(lex) {
    let diag: Diagnostic = Diagnostic {
        message: lex.diags[i].message,
        line: lex.diags[i].line,
        column: lex.diags[i].column,
        severity: 0,
    };
    Diagnostic_Print(&diag, sourceName);
    i = i + 1;
}

Note: sourceName is a variable available in the function context. If it's named differently (e.g., path, filePath), use the correct variable name.

  • Step 2: Verify compilation

Run: make buxc Expected: Success.

  • Step 3: Commit
git add src/cli.bux
git commit -m "feat(diag): format lexer errors with Diagnostic_Print"

Task 3: Update Sema Error Printing in Cli_Check

Files:

  • Modify: src/cli.bux lines 239-253

Context: Current code in Cli_Check:

if Sema_HasError(sema) {
    PrintLine("Sema errors found");
    var i: int = 0;
    while i < Sema_DiagCount(sema) {
        Print("  line ");
        PrintInt(sema.diags[i].line as int64);
        Print(" col ");
        PrintInt(sema.diags[i].column as int64);
        Print(": ");
        PrintLine(sema.diags[i].message);
        i = i + 1;
    }
    return 1;
}
  • Step 1: Replace with Diagnostic_Print

Replace lines 239-253:

if Sema_HasError(sema) {
    var i: int = 0;
    while i < Sema_DiagCount(sema) {
        let diag: Diagnostic = Diagnostic {
            message: sema.diags[i].message,
            line: sema.diags[i].line,
            column: sema.diags[i].column,
            severity: 0,
        };
        Diagnostic_Print(&diag, sourceName);
        i = i + 1;
    }
    return 1;
}
  • Step 2: Commit
git add src/cli.bux
git commit -m "feat(diag): format sema errors in Cli_Check with Diagnostic_Print"

Task 4: Update Sema Error Printing in Cli_BuildProject

Files:

  • Modify: src/cli.bux lines 991-1005

Context: Current code in Cli_BuildProject:

if Sema_HasError(sema) {
    PrintLine("Sema errors found");
    var i: int = 0;
    while i < Sema_DiagCount(sema) {
        Print("  line ");
        PrintInt(sema.diags[i].line as int64);
        Print(" col ");
        PrintInt(sema.diags[i].column as int64);
        Print(": ");
        PrintLine(sema.diags[i].message);
        i = i + 1;
    }
    return 1;
}

Note: The source path in Cli_BuildProject is the merged source file path. Check what variable holds the path to the merged source being compiled. It might be a temp file path. For MVP, use whatever path variable is available (e.g., mergedPath, cFile, or similar).

  • Step 1: Find the source path variable in Cli_BuildProject

Look at the context around line 991 to find what variable contains the source file path being compiled. Common candidates: sourceName, cFile, mergedPath, projectDir.

  • Step 2: Replace sema error loop

Replace the loop with:

if Sema_HasError(sema) {
    var i: int = 0;
    while i < Sema_DiagCount(sema) {
        let diag: Diagnostic = Diagnostic {
            message: sema.diags[i].message,
            line: sema.diags[i].line,
            column: sema.diags[i].column,
            severity: 0,
        };
        Diagnostic_Print(&diag, sourcePath);  /* use correct variable here */
        i = i + 1;
    }
    return 1;
}
  • Step 3: Commit
git add src/cli.bux
git commit -m "feat(diag): format sema errors in Cli_BuildProject with Diagnostic_Print"

Task 5: Update Sema Error Printing in Cli_Compile / Cli_CompileSource

Files:

  • Modify: src/cli.bux lines 78-88 and/or 295-300

Context: There may be additional sema error printing in Cli_Compile or Cli_CompileSource. Check lines 78-88 and 295-300 for similar patterns and update them to use Diagnostic_Print.

At line 78-88 (Cli_Compile or similar):

if Sema_HasError(sema) {
    PrintLine("Sema errors:");
    var i: int = 0;
    while i < Sema_DiagCount(sema) {
        Print("  ");
        PrintLine(sema.diags[i].message);
        i = i + 1;
    }
    return "";
}
  • Step 1: Replace with Diagnostic_Print
if Sema_HasError(sema) {
    var i: int = 0;
    while i < Sema_DiagCount(sema) {
        let diag: Diagnostic = Diagnostic {
            message: sema.diags[i].message,
            line: sema.diags[i].line,
            column: sema.diags[i].column,
            severity: 0,
        };
        Diagnostic_Print(&diag, sourceName);
        i = i + 1;
    }
    return "";
}
  • Step 2: Commit
git add src/cli.bux
git commit -m "feat(diag): format sema errors in Cli_Compile with Diagnostic_Print"

Task 6: Add Parser Diagnostic Printing

Files:

  • Modify: src/parser.bux lines 1774-1777 (end of Parser_Parse)

Context: Parser_Parse collects diagnostics in p.diags but never prints them. The CLI only checks mod == null which rarely/never happens. We add diagnostic printing at the end of Parser_Parse before returning.

  • Step 1: Add parser diagnostic printing

Before return mod; at line 1776, add:

    /* Print parser diagnostics */
    if p.diagCount > 0 {
        var di: int = 0;
        while di < p.diagCount {
            let d: ParserDiag = p.diags[di];
            Print("error: ");
            PrintLine(d.message);
            Print("  --> <input>:");
            PrintInt(d.line as int64);
            Print(":");
            PrintInt(d.column as int64);
            PrintLine("");
            Print("   |");
            PrintLine("");
            Print(" ");
            PrintInt(d.line as int64);
            Print(" | <source unavailable>");
            PrintLine("");
            Print("   | ");
            var sp: uint32 = 0;
            while sp < d.column - 1 && sp < 120 {
                Print(" ");
                sp = sp + 1;
            }
            PrintLine("^");
            di = di + 1;
        }
    }

Note: Parser diagnostics show <source unavailable> for the source line because Parser_Parse doesn't receive the source file path. In a future iteration, we can pass the path to Parser_Parse.

  • Step 2: Verify compilation

Run: make buxc Expected: Success.

  • Step 3: Commit
git add src/parser.bux
git commit -m "feat(diag): print parser diagnostics in Parser_Parse"

Task 7: Create Test Program

Files:

  • Create: _test_error_snippet/bux.toml

  • Create: _test_error_snippet/src/Main.bux

  • Step 1: Create test manifest

_test_error_snippet/bux.toml:

[package]
name = "error_snippet_test"
version = "0.1.0"
pkgType = "bin"
  • Step 2: Create test program with intentional type error

_test_error_snippet/src/Main.bux:

func Main() -> int {
    let x: int = "hello";
    return 0;
}
  • Step 3: Build and verify output

Run:

cd /home/ziko/z-git/bux/bux/_test_error_snippet && ../../build/buxc build 2>&1

Expected output should contain Rust-style formatting:

error: type mismatch
  --> .../Main.bux:2:9
   |
 2 |     let x: int = "hello";
   |         ^

(The exact message text depends on what sema emits for this error.)

  • Step 4: Commit
git add -f _test_error_snippet/
git commit -m "test: add error snippet formatting test"

Task 8: Selfhost Bootstrap Loop Verification

Files:

  • No changes — verification only

  • Step 1: Run selfhost loop

Run:

cd /home/ziko/z-git/bux/bux && make selfhost-loop

Expected: C output is IDENTICAL on both iterations.

  • Step 2: If loop fails, debug

Common issues:

  • C output differs → check if src/cli.bux changes affect codegen (they shouldn't — only Print statements changed)

  • Bootstrap compiler fails → syntax error in new Bux code

  • Step 3: Commit fixes if needed


Task 9: Push to Git

  • Step 1: Push
git push origin main

Spec Coverage Check

Spec Section Implementing Task
Diagnostic struct Task 1
Diagnostic_GetLine Task 1
Diagnostic_Print Task 1
Lexer error formatting Task 2
Sema error in Cli_Check Task 3
Sema error in Cli_BuildProject Task 4
Sema error in Cli_Compile Task 5
Parser diagnostic printing Task 6
Testing Task 7
Selfhost loop Task 8

Placeholder Scan

  • No TBD, TODO, or "implement later".
  • All code is complete and copy-paste ready.
  • All file paths are exact.
  • All commands have expected output.