Skip to content

Commit 5739e84

Browse files
timfennisclaude
andauthored
🐛 Fix REPL error attribution by tracking source per span (#123)
## Summary Fixes #29. - Adds `SourceId` newtype and `SourceDb` registry to `ndc_lexer` so every `Span` carries a `source_id` identifying which source string its byte offsets refer to. `Span` remains `Copy`. - `Interpreter` owns a `SourceDb` and registers each source (REPL line, file, etc.) before lexing, threading the `SourceId` through the lexer. - Diagnostics now use `codespan-reporting`'s multi-file `Files` trait, looking up the correct source via the span's `source_id` instead of relying on the caller to pass the right source string. - REPL errors that originate in functions defined on previous lines now render against the correct source text. ## Test plan - [x] All 316 existing tests pass - [x] `cargo clippy` clean - [x] Manual REPL test: `f := fn x -> x / 0` then `f(5)` — error should highlight `x / 0` in the function definition 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
1 parent 323a994 commit 5739e84

14 files changed

Lines changed: 239 additions & 86 deletions

File tree

compiler_tests/tests/compiler.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
use ndc_lexer::Lexer;
1+
use ndc_lexer::{Lexer, SourceId};
22
use ndc_parser::Parser;
33
use ndc_vm::chunk::OpCode;
44
use ndc_vm::chunk::OpCode::*;
55
use ndc_vm::compiler::Compiler;
66

77
fn compile(input: &str) -> Vec<OpCode> {
8-
let tokens = Lexer::new(input)
8+
let tokens = Lexer::new(input, SourceId::SYNTHETIC)
99
.collect::<Result<Vec<_>, _>>()
1010
.expect("lex failed");
1111
let expressions = Parser::from_tokens(tokens).parse().expect("parse failed");

ndc_bin/src/diagnostic.rs

Lines changed: 74 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,121 @@
11
use codespan_reporting::diagnostic::{Diagnostic, Label};
2-
use codespan_reporting::files::SimpleFile;
2+
use codespan_reporting::files;
33
use codespan_reporting::term;
44
use codespan_reporting::term::termcolor::{ColorChoice, StandardStream};
55
use ndc_interpreter::InterpreterError;
6-
use ndc_lexer::Span;
6+
use ndc_lexer::{SourceDb, SourceId};
7+
use std::ops::Range;
78

8-
fn span_to_range(span: Span) -> std::ops::Range<usize> {
9-
span.offset()..span.end()
9+
struct DiagnosticFiles<'a>(&'a SourceDb);
10+
11+
impl<'a> files::Files<'a> for DiagnosticFiles<'a> {
12+
type FileId = SourceId;
13+
type Name = &'a str;
14+
type Source = &'a str;
15+
16+
fn name(&'a self, id: SourceId) -> Result<&'a str, files::Error> {
17+
if id == SourceId::SYNTHETIC {
18+
return Ok("<synthetic>");
19+
}
20+
Ok(self.0.name(id))
21+
}
22+
23+
fn source(&'a self, id: SourceId) -> Result<&'a str, files::Error> {
24+
if id == SourceId::SYNTHETIC {
25+
return Ok("");
26+
}
27+
Ok(self.0.source(id))
28+
}
29+
30+
fn line_index(&'a self, id: SourceId, byte_index: usize) -> Result<usize, files::Error> {
31+
let source = self.source(id)?;
32+
Ok(files::line_starts(source)
33+
.take_while(|&start| start <= byte_index)
34+
.count()
35+
.saturating_sub(1))
36+
}
37+
38+
fn line_range(&'a self, id: SourceId, line_index: usize) -> Result<Range<usize>, files::Error> {
39+
let source = self.source(id)?;
40+
let line_starts: Vec<usize> = files::line_starts(source).collect();
41+
let start = *line_starts
42+
.get(line_index)
43+
.ok_or(files::Error::LineTooLarge {
44+
given: line_index,
45+
max: line_starts.len().saturating_sub(1),
46+
})?;
47+
let end = line_starts
48+
.get(line_index + 1)
49+
.copied()
50+
.unwrap_or(source.len());
51+
Ok(start..end)
52+
}
1053
}
1154

12-
fn into_diagnostic(err: InterpreterError) -> Diagnostic<()> {
55+
fn into_diagnostic(err: InterpreterError) -> Diagnostic<SourceId> {
1356
match err {
1457
InterpreterError::Lexer { cause } => {
58+
let span = cause.span();
1559
let mut d = Diagnostic::error()
1660
.with_code("lexer")
1761
.with_message(cause.to_string())
1862
.with_labels(vec![
19-
Label::primary((), span_to_range(cause.span())).with_message("here"),
63+
Label::primary(span.source_id(), span.range()).with_message("here"),
2064
]);
2165
if let Some(help) = cause.help_text() {
2266
d = d.with_notes(vec![help.to_owned()]);
2367
}
2468
d
2569
}
2670
InterpreterError::Parser { cause } => {
71+
let span = cause.span();
2772
let mut d = Diagnostic::error()
2873
.with_code("parser")
2974
.with_message(cause.to_string())
3075
.with_labels(vec![
31-
Label::primary((), span_to_range(cause.span())).with_message("here"),
76+
Label::primary(span.source_id(), span.range()).with_message("here"),
3277
]);
3378
if let Some(help) = cause.help_text() {
3479
d = d.with_notes(vec![help.to_owned()]);
3580
}
3681
d
3782
}
38-
InterpreterError::Resolver { cause } => Diagnostic::error()
39-
.with_code("resolver")
40-
.with_message(cause.to_string())
41-
.with_labels(vec![
42-
Label::primary((), span_to_range(cause.span())).with_message("related to this"),
43-
]),
44-
InterpreterError::Compiler { cause } => Diagnostic::error()
45-
.with_code("compiler")
46-
.with_message(cause.to_string())
47-
.with_labels(vec![
48-
Label::primary((), span_to_range(cause.span())).with_message("related to this"),
49-
]),
83+
InterpreterError::Resolver { cause } => {
84+
let span = cause.span();
85+
Diagnostic::error()
86+
.with_code("resolver")
87+
.with_message(cause.to_string())
88+
.with_labels(vec![
89+
Label::primary(span.source_id(), span.range()).with_message("related to this"),
90+
])
91+
}
92+
InterpreterError::Compiler { cause } => {
93+
let span = cause.span();
94+
Diagnostic::error()
95+
.with_code("compiler")
96+
.with_message(cause.to_string())
97+
.with_labels(vec![
98+
Label::primary(span.source_id(), span.range()).with_message("related to this"),
99+
])
100+
}
50101
InterpreterError::Vm(err) => {
51102
let mut d = Diagnostic::error()
52103
.with_code("vm")
53104
.with_message(&err.message);
54105
if let Some(span) = err.span {
55106
d = d.with_labels(vec![
56-
Label::primary((), span_to_range(span)).with_message("related to this"),
107+
Label::primary(span.source_id(), span.range()).with_message("related to this"),
57108
]);
58109
}
59110
d
60111
}
61112
}
62113
}
63114

64-
pub fn emit_error(filename: &str, source: &str, err: InterpreterError) {
115+
pub fn emit_error(source_db: &SourceDb, err: InterpreterError) {
65116
let diagnostic = into_diagnostic(err);
66-
let file = SimpleFile::new(filename, source);
117+
let files = DiagnosticFiles(source_db);
67118
let writer = StandardStream::stderr(ColorChoice::Auto);
68119
let config = term::Config::default();
69-
let _ = term::emit(&mut writer.lock(), &config, &file, &diagnostic);
120+
let _ = term::emit(&mut writer.lock(), &config, &files, &diagnostic);
70121
}

ndc_bin/src/highlighter.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use ahash::AHashSet;
22
use itertools::Itertools;
3-
use ndc_lexer::{Lexer, Token, TokenLocation};
3+
use ndc_lexer::{Lexer, SourceId, Token, TokenLocation};
44
use ndc_parser::{Expression, ExpressionLocation, ForBody, ForIteration};
55
use yansi::{Paint, Painted};
66

@@ -12,7 +12,7 @@ impl AndycppHighlighter {
1212
pub fn highlight_parsed(line: &str) -> Vec<Painted<&str>> {
1313
let mut function_spans = AHashSet::new();
1414

15-
let expressions = Lexer::new(line)
15+
let expressions = Lexer::new(line, SourceId::SYNTHETIC)
1616
.collect::<Result<Vec<TokenLocation>, _>>()
1717
.ok()
1818
.and_then(|tokens| ndc_parser::Parser::from_tokens(tokens).parse().ok());
@@ -30,7 +30,8 @@ impl AndycppHighlighter {
3030
line: &'a str,
3131
function_spans: &AHashSet<usize>,
3232
) -> Vec<Painted<&'a str>> {
33-
let Ok(tokens) = Lexer::new(line).collect::<Result<Vec<_>, _>>() else {
33+
let Ok(tokens) = Lexer::new(line, SourceId::SYNTHETIC).collect::<Result<Vec<_>, _>>()
34+
else {
3435
return vec![line.red()];
3536
};
3637

ndc_bin/src/main.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,24 +118,20 @@ fn main() -> anyhow::Result<()> {
118118

119119
let mut interpreter = Interpreter::new();
120120
interpreter.configure(ndc_stdlib::register);
121-
if let Err(err) = interpreter.eval(&string) {
122-
diagnostic::emit_error(&filename.expect("filename must exist"), &string, err);
121+
let name = filename.as_deref().unwrap_or("<input>");
122+
if let Err(err) = interpreter.eval_named(name, &string) {
123+
diagnostic::emit_error(interpreter.source_db(), err);
123124
process::exit(1);
124125
}
125126
}
126127
Action::DisassembleFile(path) => {
127-
let filename = path
128-
.file_name()
129-
.and_then(|name| name.to_str())
130-
.unwrap_or("<input>")
131-
.to_string();
132128
let string = fs::read_to_string(path)?;
133129
let mut interpreter = Interpreter::new();
134130
interpreter.configure(ndc_stdlib::register);
135131
match interpreter.disassemble_str(&string) {
136132
Ok(output) => print!("{output}"),
137133
Err(e) => {
138-
diagnostic::emit_error(&filename, &string, e);
134+
diagnostic::emit_error(interpreter.source_db(), e);
139135
process::exit(1);
140136
}
141137
}

ndc_bin/src/repl.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,22 @@ pub fn run() -> anyhow::Result<()> {
3434

3535
let mut interpreter = Interpreter::new();
3636
interpreter.configure(ndc_stdlib::register);
37-
loop {
37+
for command_nr in 1.. {
3838
match rl.readline("λ ") {
3939
Ok(line) => {
4040
// If we can't append the history we just ignore this
4141
let _ = rl.add_history_entry(line.as_str());
4242

4343
// Run the line we just read through the interpreter
44-
match interpreter.eval(line.as_str()) {
44+
match interpreter.eval_named(format!("<repl:{command_nr}>"), line.as_str()) {
4545
Ok(value) => {
4646
let output = value.to_string();
4747
if !output.is_empty() {
4848
println!("{output}")
4949
}
5050
}
5151
Err(err) => {
52-
crate::diagnostic::emit_error("<repl>", &line, err);
52+
crate::diagnostic::emit_error(interpreter.source_db(), err);
5353
}
5454
}
5555
}

ndc_interpreter/src/lib.rs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use ndc_analyser::{Analyser, ScopeTree};
22
use ndc_core::FunctionRegistry;
3-
use ndc_lexer::{Lexer, TokenLocation};
3+
use ndc_lexer::{Lexer, SourceDb, SourceId, TokenLocation};
44
use ndc_parser::ExpressionLocation;
55
use ndc_vm::compiler::Compiler;
66
use ndc_vm::value::CompiledFunction;
@@ -13,6 +13,7 @@ pub struct Interpreter {
1313
registry: FunctionRegistry<Rc<NativeFunction>>,
1414
capturing: bool,
1515
analyser: Analyser,
16+
source_db: SourceDb,
1617
/// Persistent REPL VM and the compiler checkpoint from the last run.
1718
/// `None` until the first `eval` call; kept alive afterwards so that
1819
/// variables declared on one line are visible on subsequent lines.
@@ -38,6 +39,7 @@ impl Interpreter {
3839
registry: FunctionRegistry::default(),
3940
capturing,
4041
analyser: Analyser::from_scope_tree(ScopeTree::from_global_scope(vec![])),
42+
source_db: SourceDb::new(),
4143
repl_state: None,
4244
}
4345
}
@@ -72,15 +74,21 @@ impl Interpreter {
7274
}
7375
}
7476

77+
pub fn source_db(&self) -> &SourceDb {
78+
&self.source_db
79+
}
80+
7581
pub fn analyse_str(
7682
&mut self,
7783
input: &str,
7884
) -> Result<Vec<ExpressionLocation>, InterpreterError> {
79-
self.parse_and_analyse(input)
85+
let source_id = self.source_db.add("<input>", input);
86+
self.parse_and_analyse(input, source_id)
8087
}
8188

8289
pub fn compile_str(&mut self, input: &str) -> Result<CompiledFunction, InterpreterError> {
83-
let expressions = self.parse_and_analyse(input)?;
90+
let source_id = self.source_db.add("<input>", input);
91+
let expressions = self.parse_and_analyse(input, source_id)?;
8492
Ok(Compiler::compile(expressions.into_iter())?)
8593
}
8694

@@ -95,15 +103,26 @@ impl Interpreter {
95103
///
96104
/// Statements (semicolon-terminated) produce [`Value::unit()`].
97105
pub fn eval(&mut self, input: &str) -> Result<Value, InterpreterError> {
98-
let expressions = self.parse_and_analyse(input)?;
106+
self.eval_named("<input>", input)
107+
}
108+
109+
/// Execute source code with a custom source name for diagnostics.
110+
pub fn eval_named(
111+
&mut self,
112+
name: impl Into<String>,
113+
input: &str,
114+
) -> Result<Value, InterpreterError> {
115+
let source_id = self.source_db.add(name, input);
116+
let expressions = self.parse_and_analyse(input, source_id)?;
99117
self.interpret_vm(input, expressions.into_iter())
100118
}
101119

102120
fn parse_and_analyse(
103121
&mut self,
104122
input: &str,
123+
source_id: SourceId,
105124
) -> Result<Vec<ExpressionLocation>, InterpreterError> {
106-
let tokens = Lexer::new(input).collect::<Result<Vec<TokenLocation>, _>>()?;
125+
let tokens = Lexer::new(input, source_id).collect::<Result<Vec<TokenLocation>, _>>()?;
107126
let mut expressions = ndc_parser::Parser::from_tokens(tokens).parse()?;
108127

109128
let checkpoint = self.analyser.checkpoint();

ndc_lexer/src/lib.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod number;
2+
mod source_db;
23
mod span;
34
mod string;
45
mod token;
@@ -8,7 +9,8 @@ use std::collections::VecDeque;
89
use std::str::Chars;
910
use string::StringLexer;
1011

11-
pub use span::Span;
12+
pub use source_db::SourceDb;
13+
pub use span::{SourceId, Span};
1214
pub use token::{Token, TokenLocation};
1315

1416
pub struct Lexer<'a> {
@@ -41,12 +43,13 @@ impl<'a> Lexer<'a> {
4143
}
4244

4345
#[must_use]
44-
pub fn new(source: &'a str) -> Self {
46+
pub fn new(source: &'a str, source_id: SourceId) -> Self {
4547
Self {
4648
source: SourceIterator {
4749
inner: source.chars(),
4850
buffer: VecDeque::default(),
4951
offset: 0,
52+
source_id,
5053
},
5154
}
5255
}
@@ -182,6 +185,7 @@ struct SourceIterator<'a> {
182185
inner: Chars<'a>,
183186
buffer: VecDeque<char>,
184187
offset: usize,
188+
source_id: SourceId,
185189
}
186190

187191
impl SourceIterator<'_> {
@@ -190,11 +194,11 @@ impl SourceIterator<'_> {
190194
}
191195

192196
pub fn create_span(&self, start: usize) -> Span {
193-
Span::new(start, (self.current_offset()) - start)
197+
Span::new(self.source_id, start, self.current_offset() - start)
194198
}
195199

196200
pub fn span(&self) -> Span {
197-
Span::new(self.current_offset(), 1)
201+
Span::new(self.source_id, self.current_offset(), 1)
198202
}
199203

200204
pub fn consume(&mut self, count: usize) {

0 commit comments

Comments
 (0)