docs: add source location error messages design document
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
# Bux Source Location Tracking in Error Messages — Design Document
|
||||
|
||||
> **Date:** 2026-06-10
|
||||
> **Status:** Design Approved
|
||||
> **Scope:** MVP Rust-style error messages with code snippets for parser and sema diagnostics
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
The Bux self-hosted compiler already tracks source locations (lexer tokens have line/column, AST nodes have line/column, parser and sema have diagnostic structs with line/column). However, the CLI prints errors as plain strings or raw `line X col Y` text, without code snippets or consistent formatting.
|
||||
|
||||
This feature unifies and improves error display to match the Rust-style format:
|
||||
```
|
||||
error: type mismatch
|
||||
--> src/Main.bux:42:15
|
||||
|
|
||||
42 | let x: int = "hello";
|
||||
| ^
|
||||
```
|
||||
|
||||
### Goals
|
||||
- Unified Rust-style error formatting for all parser and sema diagnostics
|
||||
- Code snippets showing the exact line with an underline
|
||||
- Consistent severity prefixes (`error:`, `warning:`, `note:`)
|
||||
|
||||
### Non-Goals (for MVP)
|
||||
- Source maps (mapping merged-source line numbers back to original files)
|
||||
- Multi-line error spans
|
||||
- Multi-character underlines (`^^^^` for entire tokens)
|
||||
- Colorized output
|
||||
- Error codes (e.g., `E0001`)
|
||||
- HIR lower / C backend diagnostics (these phases do not currently emit errors)
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Parser/Sema Diagnostics │
|
||||
│ ParserDiag { line, col, message } │
|
||||
│ SemaDiag { line, col, message } │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Unified Diagnostic Formatter │
|
||||
│ (inline in cli.bux or lib/Diagnostic) │
|
||||
│ • Reads source line from file │
|
||||
│ • Formats snippet with underline │
|
||||
│ • Prints Rust-style error block │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ CLI Output │
|
||||
│ error: type mismatch │
|
||||
│ --> src/Main.bux:42:15 │
|
||||
│ | │
|
||||
│ 42 | let x: int = "hello"; │
|
||||
│ | ^ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Components
|
||||
|
||||
1. **Source Line Loader** — reads a specific line from a source file
|
||||
2. **Snippet Renderer** — formats `line_num | code` + underline
|
||||
3. **Diagnostic Printer** — assembles the full Rust-style error block
|
||||
|
||||
**Note:** Bux compiles a merged source file (all `.bux` files merged before compilation). For MVP, snippets show lines from the merged source. Source map tracking (which original file a line came from) is future work.
|
||||
|
||||
---
|
||||
|
||||
## 3. Data Structures
|
||||
|
||||
```bux
|
||||
struct Diagnostic {
|
||||
message: String;
|
||||
line: uint32; /* 1-based */
|
||||
column: uint32; /* 1-based */
|
||||
severity: int; /* 0=error, 1=warning, 2=note */
|
||||
}
|
||||
```
|
||||
|
||||
**Severity values:**
|
||||
- `0` — `error:`
|
||||
- `1` — `warning:`
|
||||
- `2` — `note:`
|
||||
|
||||
For MVP, parser and sema diagnostics are always severity `0` (error). Warning/note severity is reserved for future use.
|
||||
|
||||
---
|
||||
|
||||
## 4. API Design
|
||||
|
||||
```bux
|
||||
/* Print a single diagnostic in Rust-style format */
|
||||
func Diagnostic_Print(diag: *Diagnostic, sourcePath: String);
|
||||
|
||||
/* Helper: read line N from a file (1-based) */
|
||||
func Diagnostic_GetLine(path: String, lineNum: uint32) -> String;
|
||||
```
|
||||
|
||||
### Output Format
|
||||
|
||||
```
|
||||
error: <message>
|
||||
--> <path>:<line>:<col>
|
||||
|
|
||||
<ln> | <source_line>
|
||||
| <spaces>^
|
||||
```
|
||||
|
||||
Where:
|
||||
- `<ln>` is the line number, right-aligned
|
||||
- `<spaces>` is `column - 1` spaces to position the `^`
|
||||
- If `source_line` is empty (file not found), only the header is printed
|
||||
|
||||
---
|
||||
|
||||
## 5. Snippet Rendering Algorithm
|
||||
|
||||
### `Diagnostic_GetLine(path, lineNum)`
|
||||
|
||||
1. Open the file at `path`
|
||||
2. Read character by character, counting newlines
|
||||
3. When reaching line `lineNum`, accumulate characters until `\n` or EOF
|
||||
4. Return the accumulated string (without trailing `\n`)
|
||||
5. If file cannot be opened or `lineNum` exceeds file length, return `""`
|
||||
|
||||
### `Diagnostic_Print(diag, sourcePath)`
|
||||
|
||||
```
|
||||
Print severity prefix + message
|
||||
Print " --> " + sourcePath + ":" + line + ":" + column + "\n"
|
||||
Print " |\n"
|
||||
Print line number + " | " + source line + "\n"
|
||||
Print " | " + (column-1 spaces) + "^\n"
|
||||
```
|
||||
|
||||
**Underline:** Single `^` at the column position. Multi-character spans are future work.
|
||||
|
||||
---
|
||||
|
||||
## 6. Integration Points
|
||||
|
||||
### Parser Diagnostics
|
||||
|
||||
**Current code** (`src/cli.bux`, lexer error printing):
|
||||
```bux
|
||||
PrintLine(lex.diags[i].message);
|
||||
```
|
||||
|
||||
**New code:**
|
||||
```bux
|
||||
let diag: Diagnostic = Diagnostic {
|
||||
message: lex.diags[i].message,
|
||||
line: lex.diags[i].line,
|
||||
column: lex.diags[i].column,
|
||||
severity: 0,
|
||||
};
|
||||
Diagnostic_Print(&diag, sourcePath);
|
||||
```
|
||||
|
||||
### Sema Diagnostics
|
||||
|
||||
**Current code** (`src/cli.bux`, `Cli_Compile` path):
|
||||
```bux
|
||||
PrintLine(sema.diags[i].message);
|
||||
```
|
||||
|
||||
**Current code** (`src/cli.bux`, `Cli_BuildProject` path):
|
||||
```bux
|
||||
Print(" line "); PrintInt(sema.diags[i].line as int64);
|
||||
Print(" col "); PrintInt(sema.diags[i].column as int64);
|
||||
Print(": "); PrintLine(sema.diags[i].message);
|
||||
```
|
||||
|
||||
**New code (both paths):**
|
||||
```bux
|
||||
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);
|
||||
```
|
||||
|
||||
### Unified Helper
|
||||
|
||||
Add to `src/cli.bux`:
|
||||
```bux
|
||||
func Cli_ReportSemaErrors(sema: *Sema, sourcePath: String) {
|
||||
let i: int = 0;
|
||||
while i < Sema_DiagCount(sema) {
|
||||
let d: SemaDiag = sema.diags[i];
|
||||
let diag: Diagnostic = Diagnostic {
|
||||
message: d.message, line: d.line,
|
||||
column: d.column, severity: 0,
|
||||
};
|
||||
Diagnostic_Print(&diag, sourcePath);
|
||||
i = i + 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/cli.bux` | Replace all manual error printing with `Diagnostic_Print`. Add `Diagnostic` struct and helper functions. |
|
||||
|
||||
**No changes needed to:**
|
||||
- `src/lexer.bux` — already emits `LexerDiag` with line/col
|
||||
- `src/parser.bux` — already emits `ParserDiag` with line/col
|
||||
- `src/sema.bux` — already emits `SemaDiag` with line/col via `Sema_EmitError`
|
||||
- `src/ast.bux` — already stores line/col on every node
|
||||
- `src/hir.bux` — already stores line/col on HIR nodes
|
||||
|
||||
---
|
||||
|
||||
## 8. Testing Strategy
|
||||
|
||||
1. **Type error test:** Create a `.bux` file with `let x: int = "hello";` → build → verify Rust-style output with snippet
|
||||
2. **Syntax error test:** Create a `.bux` file with missing `}` → build → verify parser error shows snippet
|
||||
3. **Selfhost loop:** Verify compiler C output remains identical (CLI changes do not affect codegen)
|
||||
|
||||
---
|
||||
|
||||
## 9. Future Work
|
||||
|
||||
- **Source maps:** Track which original file each merged line came from, so errors show original paths
|
||||
- **Multi-character underlines:** Underline entire token/span, not just a single `^`
|
||||
- **Colorized output:** ANSI color codes for error/warning/note
|
||||
- **Error codes:** Assign stable codes like `E0001` for each error type
|
||||
- **Notes and help text:** Attach explanatory notes to errors (Rust's `help:` and `note:` blocks)
|
||||
Reference in New Issue
Block a user