Error Messages and Diagnostics in Home
On this page 55
This document describes the comprehensive error message and diagnostic system implemented for the Home programming language.
Overview
Home provides rich, user-friendly error messages inspired by Rust's diagnostic system, with the following features:
- Colored output: Visual hierarchy with colors for severity levels
- Source context: Show the exact location of errors with code snippets
- Primary and secondary labels: Highlight relevant code locations
- Helpful suggestions: "Did you mean?" suggestions for typos
- Error recovery: Continue parsing to find multiple errors at once
- Builder pattern: Fluent API for constructing custom diagnostics
- Common diagnostics: Pre-built error messages for frequent cases
Architecture
The error message system consists of three main components:
1. Rich Diagnostics
Located in packages/diagnostics/src/diagnostics.zig, this provides the core diagnostic types and rendering system.
const diag = try CommonDiagnostics.typeMismatch(
allocator,
location,
"Int",
"String"
);
try diag.display("example.home", source_code, std.io.getStdErr().writer());
2. Error Recovery
Located in packages/parser/src/error_recovery.zig, this provides strategies for recovering from parse errors to continue finding more errors.
var recovery = ErrorRecovery.init(.Moderate, 100);
recovery.synchronizeToStatement(tokens, ¤t);
3. Diagnostic Builder
Provides a fluent API for constructing custom diagnostics:
var builder = DiagnosticBuilder.init(allocator, .Error, "E0001", "custom error");
_ = builder.withPrimaryLabel(location, "here's the problem");
_ = try builder.withNote("additional context");
_ = builder.withHelp("try this instead");
const diag = try builder.build();
Key Components
RichDiagnostic
The main diagnostic type that contains all information about an error:
pub const RichDiagnostic = struct {
severity: Severity, // Error, Warning, Info
error_code: []const u8, // e.g., "T0001", "V0001"
title: []const u8, // Brief description
primary_label: Label, // Main error location
secondary_labels: []const Label, // Related locations
notes: []const []const u8, // Additional information
help: ?[]const u8, // Suggestion for fixing
suggestion: ?Suggestion, // Code suggestion with replacement
};
Labels
Labels point to specific locations in source code:
pub const Label = struct {
location: ast.SourceLocation, // Line and column
message: []const u8, // Description
style: LabelStyle, // Primary or Secondary
};
Output format:
5 | let x: Int = "hello"
| ^^^ expected Int, found String
Suggestions
Code suggestions provide automatic fixes:
pub const Suggestion = struct {
location: ast.SourceLocation,
message: []const u8,
replacement: []const u8, // Suggested code
};
Output format:
help: try using type conversion
|
5 | let x: Int = "hello".parse()
| ~~~~~~~~~~~~~~~
Severity Levels
pub const Severity = enum {
Error, // Red - compilation failure
Warning, // Yellow - potential issue
Info, // Blue - informational
};
Common Diagnostics
Pre-built diagnostics for frequent error cases:
Type Mismatch (T0001)
let x: Int = "hello"
Diagnostic:
error[T0001]: type mismatch: expected Int, found String
--> example.home:1:14
|
1 | let x: Int = "hello"
| ^^^ expected Int, but found String
|
help: consider using type conversion or checking your types
Undefined Variable (V0001)
let result = variabel + 10
Diagnostic:
error[V0001]: undefined variable 'variabel'
--> example.home:1:14
|
1 | let result = variabel + 10
| ^^^^ not found in this scope
|
help: did you mean 'variable'?
Cannot Mutate (M0001)
let count = 0
count = count + 1
Diagnostic:
error[M0001]: cannot mutate immutable variable 'count'
--> example.home:2:1
|
1 | let count = 0
| ----- variable defined here as immutable
2 | count = count + 1
| ^ cannot mutate
|
help: consider making this variable mutable: let mut count = 0
Argument Count Mismatch (F0001)
fn add(x, y) = x + y
let result = add(1, 2, 3)
Diagnostic:
error[F0001]: function 'add' expects 2 arguments, but 3 were provided
--> example.home:2:14
|
2 | let result = add(1, 2, 3)
| ^^^^ expected 2 arguments, found 3
|
help: remove the extra arguments
Missing Return (R0001)
fn calculate(): Int {
let x = 42
// missing return
}
Diagnostic:
error[R0001]: function 'calculate' must return a value of type Int
--> example.home:3:1
|
3 | }
| ^ expected return statement
|
note: function signature declares return type Int
help: add a return statement: return <expr>
Non-Exhaustive Match (P0001)
match option {
Some(x) => x
}
Diagnostic:
error[P0001]: non-exhaustive pattern match
--> example.home:1:1
|
1 | match option {
| ^^^^ missing patterns
|
note: missing pattern: Some(T)
note: missing pattern: None
help: add missing patterns or use a wildcard (_)
Division by Zero (A0001)
let x = 10 / 0
Diagnostic:
error[A0001]: attempt to divide by zero
--> example.home:1:12
|
1 | let x = 10 / 0
| ^ division by zero
|
note: this is a compile-time error for constant expressions
Index Out of Bounds (A0002)
let arr = [1, 2, 3]
let x = arr[5]
Diagnostic:
error[A0002]: index 5 is out of bounds for array of length 3
--> example.home:2:9
|
2 | let x = arr[5]
| ^^ index out of bounds
|
help: valid indices are 0..3
Unreachable Code (W0001)
fn example() {
return 42
let x = 10 // unreachable
}
Diagnostic:
warning[W0001]: unreachable code
--> example.home:3:5
|
3 | let x = 10
| ^^^^ unreachable statement
|
note: any code after return statement will never be executed
help: remove the unreachable code
Unused Variable (W0002)
fn example() {
let temp = 42
// temp is never used
}
Diagnostic:
warning[W0002]: unused variable 'temp'
--> example.home:2:9
|
2 | let temp = 42
| ^^ unused variable
|
help: if intentional, prefix with underscore: _temp
Cannot Infer Type (T0002)
let x = []
Diagnostic:
error[T0002]: cannot infer type in this context
--> example.home:1:9
|
1 | let x = []
| ^^ type must be known at this point
|
note: context: empty array literal
help: provide an explicit type annotation: let x: [Int] = []
Error Recovery
The parser uses multiple strategies to recover from errors and continue parsing:
Recovery Modes
pub const RecoveryMode = enum {
/// Stop at first error in statement
Minimal,
/// Skip to next statement boundary
Moderate,
/// Try to recover within expressions
Aggressive,
};
Synchronization Points
Recovery synchronizes to these boundaries:
- Statement: Semicolon or statement-starting keywords (
let,fn,if, etc.) - Expression: Comma, semicolon, or closing delimiters
- Declaration: Declaration keywords (
fn,struct,enum, etc.) - Block: Matching closing brace
- Function: Next function declaration
- Struct/Enum: Next type declaration
Panic Mode Recovery
Skip tokens until reaching a synchronization point:
panicModeRecover(tokens, ¤t, .Statement);
Phrase-Level Recovery
Suggest common corrections:
// User typed '=' in comparison
pub const common_substitutions = [_]Substitution{
.{
.wrong = .Equal,
.correct = .EqualEqual,
.message = "use '==' for comparison, '=' is for assignment"
},
};
Error Suggestions
Fuzzy Name Matching
Uses Levenshtein distance to suggest similar names:
const suggestion = ErrorSuggestions.suggestSimilarName(
allocator,
"variabel",
&.{"variable", "value", "variance"}
);
// Returns "variable" (distance of 1)
Algorithm: Dynamic programming implementation of Levenshtein distance
- Maximum distance: 3 edits
- O(n×m) time complexity
- Stack allocation for strings < 100 chars
Keyword Suggestions
Suggest complete keywords from partial input:
const suggestion = ErrorSuggestions.suggestKeyword("ret");
// Returns "return"
Closing Delimiter Suggestions
Suggest matching closing delimiters:
const closing = ErrorSuggestions.suggestClosingDelimiter(.LeftParen);
// Returns .RightParen
Minimal Edit Suggestions
Suggest the smallest edit to fix syntax:
pub const Edit = union(enum) {
Insert: Token, // Insert a missing token
Delete: usize, // Delete an extra token
Replace: struct { // Replace with correct token
index: usize,
new_token: Token,
},
};
Output Format
Error Message Structure
error[CODE]: title
--> file.home:line:column
|
line | source code
| ^^^ primary label
| ----- secondary label
|
note: additional information
help: suggestion for fixing
Color Scheme
- Error: Bold red header, red underlines
- Warning: Bold yellow header, yellow underlines
- Info: Bold blue header, blue underlines
- Primary labels: Use severity color
- Secondary labels: Cyan color
- Line numbers: Dim blue
- Source code: Default terminal color
- Suggestions: Green color
Multi-Label Display
When multiple labels exist:
error[E0001]: conflicting definitions
--> example.home:10:5
|
10 | fn process() { }
| ^^^ first definition here
20 | fn process() { }
| ^^^ second definition here
|
help: rename one of the functions
Usage Examples
Creating Custom Diagnostics
const allocator = std.heap.page_allocator;
// Using the builder pattern
var builder = DiagnosticBuilder.init(
allocator,
.Error,
"CUSTOM01",
"something went wrong"
);
const location = ast.SourceLocation{ .line = 10, .column = 5 };
_ = builder.withPrimaryLabel(location, "this is the problem");
const related_loc = ast.SourceLocation{ .line = 5, .column = 8 };
_ = try builder.withSecondaryLabel(related_loc, "related to this");
_ = try builder.withNote("This happened because...");
_ = builder.withHelp("Try doing this instead");
const diag = try builder.build();
try diag.display("file.home", source_code, writer);
Using Common Diagnostics
// Type mismatch
const diag = try CommonDiagnostics.typeMismatch(
allocator,
location,
"Int",
"String"
);
try diag.display(file_path, source, writer);
// Undefined variable with suggestion
const diag = try CommonDiagnostics.undefinedVariable(
allocator,
location,
"variabel",
"variable" // Similar name
);
try diag.display(file_path, source, writer);
Error Recovery in Parser
// When an error occurs during parsing
fn parseStatement(self: _Parser) !_ast.Stmt {
if (self.match(.Let)) {
return self.parseLetDeclaration() catch |err| {
// Report the error
try self.reportError("invalid let declaration");
// Recover to next statement
var recovery = ErrorRecovery.init(.Moderate, 100);
recovery.synchronizeToStatement(self.tokens, &self.current);
// Return error to skip this statement
return err;
};
}
// ... other cases
}
Collecting Multiple Errors
var errors = std.ArrayList(RichDiagnostic).init(allocator);
defer errors.deinit();
// Parser continues after errors
while (parser.current < parser.tokens.len) {
parser.parseStatement() catch |err| {
if (parser.last_diagnostic) |diag| {
try errors.append(diag);
}
continue; // Keep parsing
};
}
// Display all collected errors
for (errors.items) |diag| {
try diag.display(file_path, source, writer);
}
Error Code Prefixes
Error codes follow a systematic naming scheme:
- T0xxx: Type system errors (mismatch, inference, conversion)
- V0xxx: Variable errors (undefined, redeclaration, shadowing)
- M0xxx: Mutability errors (immutable assignment, move after use)
- F0xxx: Function errors (argument count, return type, signature)
- R0xxx: Return statement errors (missing, unreachable)
- P0xxx: Pattern matching errors (non-exhaustive, invalid pattern)
- A0xxx: Arithmetic errors (division by zero, overflow, bounds)
- W0xxx: Warnings (unreachable code, unused variables)
- I0xxx: Informational messages
- S0xxx: Syntax errors (unexpected token, missing delimiter)
- L0xxx: Lifetime and borrow errors
Integration with Parser
The diagnostic system integrates with the parser at multiple points:
- Lexical errors: Invalid tokens, unterminated strings
- Syntax errors: Unexpected tokens, missing delimiters
- Semantic errors: Type mismatches, undefined variables
- Warning generation: Unused variables, unreachable code
Parser Error Reporting
fn expect(self: *Parser, token_type: TokenType, message: []const u8) !Token {
if (self.check(token_type)) {
return self.advance();
}
// Create diagnostic
var builder = DiagnosticBuilder.init(
self.allocator,
.Error,
"S0001",
message
);
const location = ast.SourceLocation.fromToken(self.peek());
_ = builder.withPrimaryLabel(location, "unexpected token");
if (self.current > 0) {
const prev_loc = ast.SourceLocation.fromToken(self.previous());
_ = try builder.withSecondaryLabel(prev_loc, "after this");
}
const diag = try builder.build();
self.last_diagnostic = diag;
return error.ParseError;
}
Performance Considerations
- Allocation: Diagnostics allocate for labels, notes, and messages
- String formatting: Uses Zig's
std.fmtfor efficient formatting - Color codes: Minimal overhead, just ANSI escape sequences
- Recovery: O(n) token skipping in worst case
- Levenshtein distance: O(n×m) but limited to strings < 100 chars
Testing
The diagnostic system has comprehensive tests in packages/diagnostics/tests/rich_diagnostics_test.zig:
Common Diagnostic Tests
- Type mismatch error (T0001)
- Undefined variable with suggestion (V0001)
- Cannot mutate immutable (M0001)
- Argument count mismatch - too many (F0001)
- Argument count mismatch - too few (F0001)
- Missing return statement (R0001)
- Non-exhaustive match (P0001)
- Division by zero (A0001)
- Index out of bounds (A0002)
- Unreachable code warning (W0001)
- Unused variable warning (W0002)
- Cannot infer type (T0002)
Builder Tests
- Custom error with builder
- Diagnostic with suggestion
- Multiple secondary labels
- Multiple notes
- Error without primary label (should fail)
Error Recovery Tests
Located in packages/parser/src/error_recovery.zig tests:
- Synchronization to statement boundary
- Synchronization to expression boundary
- Synchronization to declaration boundary
- Block depth tracking
- Missing token recovery
- Delimiter matching
Run tests with:
./pantry/.bin/zig build test
Future Enhancements
Potential improvements to the diagnostic system:
- IDE integration: LSP diagnostics for real-time feedback
- Fix-it hints: Automatic code fixes with apply button
- Diagnostic groups: Related errors grouped together
- Error explanations: Detailed explanations with examples
- Localization: Error messages in multiple languages
- Telemetry: Track common errors to improve messages
- Interactive mode: Ask questions to clarify intent
- Diff display: Show before/after for suggestions
- Call stack traces: For runtime errors
- Batch mode: Suppress repeated similar errors
Best Practices
For Compiler Developers
- Use common diagnostics when possible for consistency
- Provide context with secondary labels
- Suggest fixes with help messages when known
- Recover gracefully to find multiple errors
- Use appropriate severity (Error vs Warning vs Info)
- Include error codes for documentation lookup
- Test error messages to ensure clarity
For Language Users
- Read the full diagnostic including notes and help
- Check secondary labels for related context
- Look up error codes in documentation for details
- Use suggestions as starting points for fixes
- Report unclear messages to improve the system
References
Files
- Core:
packages/diagnostics/src/diagnostics.zig(includes RichDiagnostic, DiagnosticBuilder, CommonDiagnostics) - Recovery:
packages/parser/src/error_recovery.zig - Tests:
packages/diagnostics/tests/rich_diagnostics_test.zig