From 3eec49fc1ff456387886ea5d10d5967e68c7540b Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Sun, 22 Mar 2026 08:09:04 +0100 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=90=9B=20Fix=20REPL=20error=20attribu?= =?UTF-8?q?tion=20by=20tracking=20source=20per=20span=20(#29)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add SourceId to Span and a SourceDb registry so each span knows which source string it belongs to. Diagnostics now use codespan-reporting's multi-file support to render errors against the correct source, even when a function defined on a previous REPL line triggers a runtime error. Co-Authored-By: Claude Opus 4.6 --- compiler_tests/tests/compiler.rs | 4 +- ndc_bin/src/diagnostic.rs | 102 ++++++++++++++++++++++++------- ndc_bin/src/highlighter.rs | 7 ++- ndc_bin/src/main.rs | 12 ++-- ndc_bin/src/repl.rs | 4 +- ndc_interpreter/src/lib.rs | 25 ++++++-- ndc_lexer/src/lib.rs | 12 ++-- ndc_lexer/src/source_db.rs | 39 ++++++++++++ ndc_lexer/src/span.rs | 45 +++++++++++++- ndc_lexer/src/token.rs | 17 +++--- ndc_lsp/src/backend.rs | 4 +- ndc_vm/src/compiler.rs | 50 +++++++-------- ndc_vm/src/lib.rs | 2 +- 13 files changed, 239 insertions(+), 84 deletions(-) create mode 100644 ndc_lexer/src/source_db.rs diff --git a/compiler_tests/tests/compiler.rs b/compiler_tests/tests/compiler.rs index baf2bda7..b3a641ce 100644 --- a/compiler_tests/tests/compiler.rs +++ b/compiler_tests/tests/compiler.rs @@ -1,11 +1,11 @@ -use ndc_lexer::Lexer; +use ndc_lexer::{Lexer, SourceId}; use ndc_parser::Parser; use ndc_vm::chunk::OpCode; use ndc_vm::chunk::OpCode::*; use ndc_vm::compiler::Compiler; fn compile(input: &str) -> Vec { - let tokens = Lexer::new(input) + let tokens = Lexer::new(input, SourceId::SYNTHETIC) .collect::, _>>() .expect("lex failed"); let expressions = Parser::from_tokens(tokens).parse().expect("parse failed"); diff --git a/ndc_bin/src/diagnostic.rs b/ndc_bin/src/diagnostic.rs index 05efe767..332b8f24 100644 --- a/ndc_bin/src/diagnostic.rs +++ b/ndc_bin/src/diagnostic.rs @@ -1,22 +1,70 @@ use codespan_reporting::diagnostic::{Diagnostic, Label}; -use codespan_reporting::files::SimpleFile; +use codespan_reporting::files; use codespan_reporting::term; use codespan_reporting::term::termcolor::{ColorChoice, StandardStream}; use ndc_interpreter::InterpreterError; -use ndc_lexer::Span; +use ndc_lexer::{SourceDb, SourceId, Span}; +use std::ops::Range; -fn span_to_range(span: Span) -> std::ops::Range { +fn span_to_range(span: Span) -> Range { span.offset()..span.end() } -fn into_diagnostic(err: InterpreterError) -> Diagnostic<()> { +struct DiagnosticFiles<'a>(&'a SourceDb); + +impl<'a> files::Files<'a> for DiagnosticFiles<'a> { + type FileId = SourceId; + type Name = &'a str; + type Source = &'a str; + + fn name(&'a self, id: SourceId) -> Result<&'a str, files::Error> { + if id == SourceId::SYNTHETIC { + return Ok(""); + } + Ok(self.0.name(id)) + } + + fn source(&'a self, id: SourceId) -> Result<&'a str, files::Error> { + if id == SourceId::SYNTHETIC { + return Ok(""); + } + Ok(self.0.source(id)) + } + + fn line_index(&'a self, id: SourceId, byte_index: usize) -> Result { + let source = self.source(id)?; + Ok(files::line_starts(source) + .take_while(|&start| start <= byte_index) + .count() + .saturating_sub(1)) + } + + fn line_range(&'a self, id: SourceId, line_index: usize) -> Result, files::Error> { + let source = self.source(id)?; + let line_starts: Vec = files::line_starts(source).collect(); + let start = *line_starts + .get(line_index) + .ok_or(files::Error::LineTooLarge { + given: line_index, + max: line_starts.len().saturating_sub(1), + })?; + let end = line_starts + .get(line_index + 1) + .copied() + .unwrap_or(source.len()); + Ok(start..end) + } +} + +fn into_diagnostic(err: InterpreterError) -> Diagnostic { match err { InterpreterError::Lexer { cause } => { + let span = cause.span(); let mut d = Diagnostic::error() .with_code("lexer") .with_message(cause.to_string()) .with_labels(vec![ - Label::primary((), span_to_range(cause.span())).with_message("here"), + Label::primary(span.source_id(), span_to_range(span)).with_message("here"), ]); if let Some(help) = cause.help_text() { d = d.with_notes(vec![help.to_owned()]); @@ -24,36 +72,46 @@ fn into_diagnostic(err: InterpreterError) -> Diagnostic<()> { d } InterpreterError::Parser { cause } => { + let span = cause.span(); let mut d = Diagnostic::error() .with_code("parser") .with_message(cause.to_string()) .with_labels(vec![ - Label::primary((), span_to_range(cause.span())).with_message("here"), + Label::primary(span.source_id(), span_to_range(span)).with_message("here"), ]); if let Some(help) = cause.help_text() { d = d.with_notes(vec![help.to_owned()]); } d } - InterpreterError::Resolver { cause } => Diagnostic::error() - .with_code("resolver") - .with_message(cause.to_string()) - .with_labels(vec![ - Label::primary((), span_to_range(cause.span())).with_message("related to this"), - ]), - InterpreterError::Compiler { cause } => Diagnostic::error() - .with_code("compiler") - .with_message(cause.to_string()) - .with_labels(vec![ - Label::primary((), span_to_range(cause.span())).with_message("related to this"), - ]), + InterpreterError::Resolver { cause } => { + let span = cause.span(); + Diagnostic::error() + .with_code("resolver") + .with_message(cause.to_string()) + .with_labels(vec![ + Label::primary(span.source_id(), span_to_range(span)) + .with_message("related to this"), + ]) + } + InterpreterError::Compiler { cause } => { + let span = cause.span(); + Diagnostic::error() + .with_code("compiler") + .with_message(cause.to_string()) + .with_labels(vec![ + Label::primary(span.source_id(), span_to_range(span)) + .with_message("related to this"), + ]) + } InterpreterError::Vm(err) => { let mut d = Diagnostic::error() .with_code("vm") .with_message(&err.message); if let Some(span) = err.span { d = d.with_labels(vec![ - Label::primary((), span_to_range(span)).with_message("related to this"), + Label::primary(span.source_id(), span_to_range(span)) + .with_message("related to this"), ]); } d @@ -61,10 +119,10 @@ fn into_diagnostic(err: InterpreterError) -> Diagnostic<()> { } } -pub fn emit_error(filename: &str, source: &str, err: InterpreterError) { +pub fn emit_error(source_db: &SourceDb, err: InterpreterError) { let diagnostic = into_diagnostic(err); - let file = SimpleFile::new(filename, source); + let files = DiagnosticFiles(source_db); let writer = StandardStream::stderr(ColorChoice::Auto); let config = term::Config::default(); - let _ = term::emit(&mut writer.lock(), &config, &file, &diagnostic); + let _ = term::emit(&mut writer.lock(), &config, &files, &diagnostic); } diff --git a/ndc_bin/src/highlighter.rs b/ndc_bin/src/highlighter.rs index 3b7dbeae..110d6962 100644 --- a/ndc_bin/src/highlighter.rs +++ b/ndc_bin/src/highlighter.rs @@ -1,6 +1,6 @@ use ahash::AHashSet; use itertools::Itertools; -use ndc_lexer::{Lexer, Token, TokenLocation}; +use ndc_lexer::{Lexer, SourceId, Token, TokenLocation}; use ndc_parser::{Expression, ExpressionLocation, ForBody, ForIteration}; use yansi::{Paint, Painted}; @@ -12,7 +12,7 @@ impl AndycppHighlighter { pub fn highlight_parsed(line: &str) -> Vec> { let mut function_spans = AHashSet::new(); - let expressions = Lexer::new(line) + let expressions = Lexer::new(line, SourceId::SYNTHETIC) .collect::, _>>() .ok() .and_then(|tokens| ndc_parser::Parser::from_tokens(tokens).parse().ok()); @@ -30,7 +30,8 @@ impl AndycppHighlighter { line: &'a str, function_spans: &AHashSet, ) -> Vec> { - let Ok(tokens) = Lexer::new(line).collect::, _>>() else { + let Ok(tokens) = Lexer::new(line, SourceId::SYNTHETIC).collect::, _>>() + else { return vec![line.red()]; }; diff --git a/ndc_bin/src/main.rs b/ndc_bin/src/main.rs index 0ecce76c..23048549 100644 --- a/ndc_bin/src/main.rs +++ b/ndc_bin/src/main.rs @@ -118,24 +118,20 @@ fn main() -> anyhow::Result<()> { let mut interpreter = Interpreter::new(); interpreter.configure(ndc_stdlib::register); - if let Err(err) = interpreter.eval(&string) { - diagnostic::emit_error(&filename.expect("filename must exist"), &string, err); + let name = filename.as_deref().unwrap_or(""); + if let Err(err) = interpreter.eval_named(name, &string) { + diagnostic::emit_error(interpreter.source_db(), err); process::exit(1); } } Action::DisassembleFile(path) => { - let filename = path - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or("") - .to_string(); let string = fs::read_to_string(path)?; let mut interpreter = Interpreter::new(); interpreter.configure(ndc_stdlib::register); match interpreter.disassemble_str(&string) { Ok(output) => print!("{output}"), Err(e) => { - diagnostic::emit_error(&filename, &string, e); + diagnostic::emit_error(interpreter.source_db(), e); process::exit(1); } } diff --git a/ndc_bin/src/repl.rs b/ndc_bin/src/repl.rs index 5fb83239..bc3e43ef 100644 --- a/ndc_bin/src/repl.rs +++ b/ndc_bin/src/repl.rs @@ -41,7 +41,7 @@ pub fn run() -> anyhow::Result<()> { let _ = rl.add_history_entry(line.as_str()); // Run the line we just read through the interpreter - match interpreter.eval(line.as_str()) { + match interpreter.eval_named("", line.as_str()) { Ok(value) => { let output = value.to_string(); if !output.is_empty() { @@ -49,7 +49,7 @@ pub fn run() -> anyhow::Result<()> { } } Err(err) => { - crate::diagnostic::emit_error("", &line, err); + crate::diagnostic::emit_error(interpreter.source_db(), err); } } } diff --git a/ndc_interpreter/src/lib.rs b/ndc_interpreter/src/lib.rs index ac9f038d..70b032e9 100644 --- a/ndc_interpreter/src/lib.rs +++ b/ndc_interpreter/src/lib.rs @@ -1,6 +1,6 @@ use ndc_analyser::{Analyser, ScopeTree}; use ndc_core::FunctionRegistry; -use ndc_lexer::{Lexer, TokenLocation}; +use ndc_lexer::{Lexer, SourceDb, SourceId, TokenLocation}; use ndc_parser::ExpressionLocation; use ndc_vm::compiler::Compiler; use ndc_vm::value::CompiledFunction; @@ -13,6 +13,7 @@ pub struct Interpreter { registry: FunctionRegistry>, capturing: bool, analyser: Analyser, + source_db: SourceDb, /// Persistent REPL VM and the compiler checkpoint from the last run. /// `None` until the first `eval` call; kept alive afterwards so that /// variables declared on one line are visible on subsequent lines. @@ -38,6 +39,7 @@ impl Interpreter { registry: FunctionRegistry::default(), capturing, analyser: Analyser::from_scope_tree(ScopeTree::from_global_scope(vec![])), + source_db: SourceDb::new(), repl_state: None, } } @@ -72,15 +74,21 @@ impl Interpreter { } } + pub fn source_db(&self) -> &SourceDb { + &self.source_db + } + pub fn analyse_str( &mut self, input: &str, ) -> Result, InterpreterError> { - self.parse_and_analyse(input) + let source_id = self.source_db.add("", input); + self.parse_and_analyse(input, source_id) } pub fn compile_str(&mut self, input: &str) -> Result { - let expressions = self.parse_and_analyse(input)?; + let source_id = self.source_db.add("", input); + let expressions = self.parse_and_analyse(input, source_id)?; Ok(Compiler::compile(expressions.into_iter())?) } @@ -95,15 +103,22 @@ impl Interpreter { /// /// Statements (semicolon-terminated) produce [`Value::unit()`]. pub fn eval(&mut self, input: &str) -> Result { - let expressions = self.parse_and_analyse(input)?; + self.eval_named("", input) + } + + /// Execute source code with a custom source name for diagnostics. + pub fn eval_named(&mut self, name: &str, input: &str) -> Result { + let source_id = self.source_db.add(name, input); + let expressions = self.parse_and_analyse(input, source_id)?; self.interpret_vm(input, expressions.into_iter()) } fn parse_and_analyse( &mut self, input: &str, + source_id: SourceId, ) -> Result, InterpreterError> { - let tokens = Lexer::new(input).collect::, _>>()?; + let tokens = Lexer::new(input, source_id).collect::, _>>()?; let mut expressions = ndc_parser::Parser::from_tokens(tokens).parse()?; let checkpoint = self.analyser.checkpoint(); diff --git a/ndc_lexer/src/lib.rs b/ndc_lexer/src/lib.rs index 8b3e279f..6ddeab53 100644 --- a/ndc_lexer/src/lib.rs +++ b/ndc_lexer/src/lib.rs @@ -1,4 +1,5 @@ mod number; +mod source_db; mod span; mod string; mod token; @@ -8,7 +9,8 @@ use std::collections::VecDeque; use std::str::Chars; use string::StringLexer; -pub use span::Span; +pub use source_db::SourceDb; +pub use span::{SourceId, Span}; pub use token::{Token, TokenLocation}; pub struct Lexer<'a> { @@ -41,12 +43,13 @@ impl<'a> Lexer<'a> { } #[must_use] - pub fn new(source: &'a str) -> Self { + pub fn new(source: &'a str, source_id: SourceId) -> Self { Self { source: SourceIterator { inner: source.chars(), buffer: VecDeque::default(), offset: 0, + source_id, }, } } @@ -182,6 +185,7 @@ struct SourceIterator<'a> { inner: Chars<'a>, buffer: VecDeque, offset: usize, + source_id: SourceId, } impl SourceIterator<'_> { @@ -190,11 +194,11 @@ impl SourceIterator<'_> { } pub fn create_span(&self, start: usize) -> Span { - Span::new(start, (self.current_offset()) - start) + Span::new(self.source_id, start, self.current_offset() - start) } pub fn span(&self) -> Span { - Span::new(self.current_offset(), 1) + Span::new(self.source_id, self.current_offset(), 1) } pub fn consume(&mut self, count: usize) { diff --git a/ndc_lexer/src/source_db.rs b/ndc_lexer/src/source_db.rs new file mode 100644 index 00000000..ebc891d9 --- /dev/null +++ b/ndc_lexer/src/source_db.rs @@ -0,0 +1,39 @@ +use crate::SourceId; + +#[derive(Default)] +pub struct SourceDb { + sources: Vec<(String, String)>, +} + +impl SourceDb { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + pub fn add(&mut self, name: impl Into, source: impl Into) -> SourceId { + let id = u32::try_from(self.sources.len()).expect("too many sources"); + self.sources.push((name.into(), source.into())); + SourceId::new(id) + } + + #[must_use] + pub fn name(&self, id: SourceId) -> &str { + &self.sources[id.index()].0 + } + + #[must_use] + pub fn source(&self, id: SourceId) -> &str { + &self.sources[id.index()].1 + } + + #[must_use] + pub fn len(&self) -> usize { + self.sources.len() + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.sources.is_empty() + } +} diff --git a/ndc_lexer/src/span.rs b/ndc_lexer/src/span.rs index 32178cf0..50272c7f 100644 --- a/ndc_lexer/src/span.rs +++ b/ndc_lexer/src/span.rs @@ -1,27 +1,68 @@ use std::ops::Range; +#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash, Ord, PartialOrd)] +pub struct SourceId(u32); + +impl SourceId { + pub const SYNTHETIC: Self = Self(u32::MAX); + + #[must_use] + pub fn new(id: u32) -> Self { + Self(id) + } + + #[must_use] + pub fn index(self) -> usize { + self.0 as usize + } +} + #[derive(Clone, Copy, Eq, PartialEq, Debug)] pub struct Span { + source_id: SourceId, offset: usize, length: usize, } impl Span { #[must_use] - pub fn new(offset: usize, length: usize) -> Self { - Self { offset, length } + pub fn new(source_id: SourceId, offset: usize, length: usize) -> Self { + Self { + source_id, + offset, + length, + } + } + + #[must_use] + pub fn synthetic() -> Self { + Self { + source_id: SourceId::SYNTHETIC, + offset: 0, + length: 0, + } } #[must_use] pub fn merge(self, other: Self) -> Self { + debug_assert_eq!( + self.source_id, other.source_id, + "cannot merge spans from different sources" + ); let from = self.offset.min(other.offset); let to = (self.offset + self.length).max(other.offset + other.length); Self { + source_id: self.source_id, offset: from, length: to - from, } } + #[must_use] + pub fn source_id(&self) -> SourceId { + self.source_id + } + #[must_use] pub fn range(&self) -> Range { self.offset..self.end() diff --git a/ndc_lexer/src/token.rs b/ndc_lexer/src/token.rs index add7c55e..ed4f09c5 100644 --- a/ndc_lexer/src/token.rs +++ b/ndc_lexer/src/token.rs @@ -331,17 +331,18 @@ impl From for Token { #[cfg(test)] mod test { - use crate::Span; + use crate::{SourceId, Span}; #[test] fn test_merge() { - let a = Span::new(0, 1); - let b = Span::new(3, 1); - assert_eq!(Span::new(0, 4), a.merge(b)); + let s = SourceId::SYNTHETIC; + let a = Span::new(s, 0, 1); + let b = Span::new(s, 3, 1); + assert_eq!(Span::new(s, 0, 4), a.merge(b)); - let a = Span::new(3, 100); - let b = Span::new(5, 10); - assert_eq!(Span::new(3, 100), a.merge(b)); - assert_eq!(Span::new(3, 100), a.merge(a)); + let a = Span::new(s, 3, 100); + let b = Span::new(s, 5, 10); + assert_eq!(Span::new(s, 3, 100), a.merge(b)); + assert_eq!(Span::new(s, 3, 100), a.merge(a)); } } diff --git a/ndc_lsp/src/backend.rs b/ndc_lsp/src/backend.rs index 04d8c168..9fec6159 100644 --- a/ndc_lsp/src/backend.rs +++ b/ndc_lsp/src/backend.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use ndc_core::{FunctionRegistry, StaticType}; use ndc_interpreter::Interpreter; use ndc_interpreter::NativeFunction; -use ndc_lexer::{Lexer, Span, TokenLocation}; +use ndc_lexer::{Lexer, SourceId, Span, TokenLocation}; use ndc_parser::{Expression, ExpressionLocation, ForBody, ForIteration, Lvalue}; use std::rc::Rc; use tokio::sync::Mutex; @@ -40,7 +40,7 @@ impl Backend { } async fn validate(&self, uri: &Url, text: &str) { - let scanner = Lexer::new(text); + let scanner = Lexer::new(text, SourceId::SYNTHETIC); let tokens = scanner .collect::, ndc_lexer::Error>>() .map_err(|err| { diff --git a/ndc_vm/src/compiler.rs b/ndc_vm/src/compiler.rs index a599fc54..ecae137e 100644 --- a/ndc_vm/src/compiler.rs +++ b/ndc_vm/src/compiler.rs @@ -70,7 +70,7 @@ impl Compiler { /// Internal: clone a checkpoint (pre-Halt), write Halt, return both. fn finish(mut self) -> Result<(CompiledFunction, Self), CompileError> { let checkpoint = self.clone(); - self.chunk.write(OpCode::Halt, Span::new(0, 0)); + self.chunk.write(OpCode::Halt, Span::synthetic()); let function = CompiledFunction { name: None, static_type: StaticType::Function { @@ -125,7 +125,7 @@ impl Compiler { let needs_pop = produces_value(&stm.expression); self.compile_expr(*stm)?; if needs_pop { - self.chunk.write(OpCode::Pop, Span::new(0, 0)); + self.chunk.write(OpCode::Pop, Span::synthetic()); } } Expression::Logical { @@ -138,13 +138,13 @@ impl Compiler { match operator { LogicalOperator::And => { let end_jump = self.chunk.write(OpCode::JumpIfFalse(0), left_span); - self.chunk.write(OpCode::Pop, Span::new(0, 0)); + self.chunk.write(OpCode::Pop, Span::synthetic()); self.compile_expr(*right)?; self.chunk.patch_jump(end_jump); } LogicalOperator::Or => { let end_jump = self.chunk.write(OpCode::JumpIfTrue(0), left_span); - self.chunk.write(OpCode::Pop, Span::new(0, 0)); + self.chunk.write(OpCode::Pop, Span::synthetic()); self.compile_expr(*right)?; self.chunk.patch_jump(end_jump); } @@ -175,7 +175,7 @@ impl Compiler { self.compile_expr(*value)?; self.compile_lvalue(l_value, span)?; let idx = self.chunk.add_constant(Value::unit()); - self.chunk.write(OpCode::Constant(idx), Span::new(0, 0)); + self.chunk.write(OpCode::Constant(idx), Span::synthetic()); } Lvalue::Sequence(seq) => { self.compile_expr(*value)?; @@ -184,7 +184,7 @@ impl Compiler { self.compile_lvalue(l_value, span)?; } let idx = self.chunk.add_constant(Value::unit()); - self.chunk.write(OpCode::Constant(idx), Span::new(0, 0)); + self.chunk.write(OpCode::Constant(idx), Span::synthetic()); } }, Expression::OpAssignment { @@ -374,7 +374,7 @@ impl Compiler { self.compile_expr(v)? } else { let idx = self.chunk.add_constant(Value::unit()); - self.chunk.write(OpCode::Constant(idx), Span::new(0, 0)); + self.chunk.write(OpCode::Constant(idx), Span::synthetic()); } } if let Some(default) = default { @@ -456,7 +456,7 @@ impl Compiler { self.compile_expr(*index)?; self.chunk.write(OpCode::GetLocal(tmp_value), span); self.chunk.write(OpCode::Call(3), span); - self.chunk.write(OpCode::Pop, Span::new(0, 0)); + self.chunk.write(OpCode::Pop, Span::synthetic()); } Lvalue::Sequence(seq) => { self.chunk.write(OpCode::Unpack(seq.len()), span); @@ -528,7 +528,7 @@ impl Compiler { if statements.is_empty() { let idx = self.chunk.add_constant(Value::unit()); // Synthetic unit from empty block has no meaningful source - self.chunk.write(OpCode::Constant(idx), Span::new(0, 0)); + self.chunk.write(OpCode::Constant(idx), Span::synthetic()); } else { let last = statements.len() - 1; for (i, stmt) in statements.into_iter().enumerate() { @@ -537,7 +537,7 @@ impl Compiler { if i == last && !is_last_expr { let idx = self.chunk.add_constant(Value::unit()); // Synthetic unit when last statement doesn't produce value - self.chunk.write(OpCode::Constant(idx), Span::new(0, 0)); + self.chunk.write(OpCode::Constant(idx), Span::synthetic()); } } } @@ -555,21 +555,21 @@ impl Compiler { let condition_span = condition.span; self.compile_expr(condition)?; let conditional_jump_idx = self.chunk.write(OpCode::JumpIfFalse(0), condition_span); - self.chunk.write(OpCode::Pop, Span::new(0, 0)); + self.chunk.write(OpCode::Pop, Span::synthetic()); self.compile_expr(on_true)?; if let Some(on_false) = on_false { - let jump_to_end = self.chunk.write(OpCode::Jump(0), Span::new(0, 0)); + let jump_to_end = self.chunk.write(OpCode::Jump(0), Span::synthetic()); self.chunk.patch_jump(conditional_jump_idx); - self.chunk.write(OpCode::Pop, Span::new(0, 0)); + self.chunk.write(OpCode::Pop, Span::synthetic()); self.compile_expr(on_false)?; self.chunk.patch_jump(jump_to_end); } else { // No else branch — push unit so the if-expression always produces a value. - let jump_to_end = self.chunk.write(OpCode::Jump(0), Span::new(0, 0)); + let jump_to_end = self.chunk.write(OpCode::Jump(0), Span::synthetic()); self.chunk.patch_jump(conditional_jump_idx); - self.chunk.write(OpCode::Pop, Span::new(0, 0)); + self.chunk.write(OpCode::Pop, Span::synthetic()); let idx = self.chunk.add_constant(Value::unit()); - self.chunk.write(OpCode::Constant(idx), Span::new(0, 0)); + self.chunk.write(OpCode::Constant(idx), Span::synthetic()); self.chunk.patch_jump(jump_to_end); } @@ -586,12 +586,12 @@ impl Compiler { let loop_start = self.new_loop_context(); self.compile_expr(condition)?; let conditional_jump_idx = self.chunk.write(OpCode::JumpIfFalse(0), condition_span); - self.chunk.write(OpCode::Pop, Span::new(0, 0)); + self.chunk.write(OpCode::Pop, Span::synthetic()); self.compile_expr(loop_body)?; - self.chunk.write(OpCode::Pop, Span::new(0, 0)); - self.chunk.write_jump_back(loop_start, Span::new(0, 0)); + self.chunk.write(OpCode::Pop, Span::synthetic()); + self.chunk.write_jump_back(loop_start, Span::synthetic()); self.chunk.patch_jump(conditional_jump_idx); - self.chunk.write(OpCode::Pop, Span::new(0, 0)); + self.chunk.write(OpCode::Pop, Span::synthetic()); let break_instructions = std::mem::take(&mut self.current_loop_context_mut().unwrap().break_instructions); for instruction in break_instructions { @@ -633,7 +633,7 @@ impl Compiler { ..Default::default() }; fn_compiler.compile_expr(body)?; - fn_compiler.chunk.write(OpCode::Return, Span::new(0, 0)); + fn_compiler.chunk.write(OpCode::Return, Span::synthetic()); let compiled = CompiledFunction { name, @@ -739,7 +739,7 @@ impl Compiler { this.compile_expr(value)?; } else { let idx = this.chunk.add_constant(Value::unit()); - this.chunk.write(OpCode::Constant(idx), Span::new(0, 0)); + this.chunk.write(OpCode::Constant(idx), Span::synthetic()); } this.chunk.write(OpCode::MapInsert(tmp_map), span); Ok(()) @@ -793,16 +793,16 @@ impl Compiler { self.end_loop_context(); // Pop the iterator - self.chunk.write(OpCode::Pop, Span::new(0, 0)); + self.chunk.write(OpCode::Pop, Span::synthetic()); } ForIteration::Guard(condition) => { self.compile_expr(condition.clone())?; let skip_jump = self.chunk.write(OpCode::JumpIfFalse(0), span); - self.chunk.write(OpCode::Pop, Span::new(0, 0)); + self.chunk.write(OpCode::Pop, Span::synthetic()); self.compile_for_iterations(rest, span, compile_leaf)?; let end_jump = self.chunk.write(OpCode::Jump(0), span); self.chunk.patch_jump(skip_jump); - self.chunk.write(OpCode::Pop, Span::new(0, 0)); + self.chunk.write(OpCode::Pop, Span::synthetic()); self.chunk.patch_jump(end_jump); } } diff --git a/ndc_vm/src/lib.rs b/ndc_vm/src/lib.rs index cc61bb13..4ae2bd61 100644 --- a/ndc_vm/src/lib.rs +++ b/ndc_vm/src/lib.rs @@ -35,7 +35,7 @@ mod test { use ndc_core::StaticType; use ndc_lexer::Span; - let dummy_span = Span::new(0, 0); + let dummy_span = Span::synthetic(); let mut chunk = Chunk::default(); chunk.write(OpCode::Return, dummy_span); let function = CompiledFunction { From c62950362951a980bfac62cb6cfe50be3a4d3d4b Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Sun, 22 Mar 2026 08:58:52 +0100 Subject: [PATCH 2/3] Remove redundant span_to_range wrapper, use Span::range() directly Co-Authored-By: Claude Opus 4.6 --- ndc_bin/src/diagnostic.rs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/ndc_bin/src/diagnostic.rs b/ndc_bin/src/diagnostic.rs index 332b8f24..83b22284 100644 --- a/ndc_bin/src/diagnostic.rs +++ b/ndc_bin/src/diagnostic.rs @@ -3,13 +3,9 @@ use codespan_reporting::files; use codespan_reporting::term; use codespan_reporting::term::termcolor::{ColorChoice, StandardStream}; use ndc_interpreter::InterpreterError; -use ndc_lexer::{SourceDb, SourceId, Span}; +use ndc_lexer::{SourceDb, SourceId}; use std::ops::Range; -fn span_to_range(span: Span) -> Range { - span.offset()..span.end() -} - struct DiagnosticFiles<'a>(&'a SourceDb); impl<'a> files::Files<'a> for DiagnosticFiles<'a> { @@ -64,7 +60,7 @@ fn into_diagnostic(err: InterpreterError) -> Diagnostic { .with_code("lexer") .with_message(cause.to_string()) .with_labels(vec![ - Label::primary(span.source_id(), span_to_range(span)).with_message("here"), + Label::primary(span.source_id(), span.range()).with_message("here"), ]); if let Some(help) = cause.help_text() { d = d.with_notes(vec![help.to_owned()]); @@ -77,7 +73,7 @@ fn into_diagnostic(err: InterpreterError) -> Diagnostic { .with_code("parser") .with_message(cause.to_string()) .with_labels(vec![ - Label::primary(span.source_id(), span_to_range(span)).with_message("here"), + Label::primary(span.source_id(), span.range()).with_message("here"), ]); if let Some(help) = cause.help_text() { d = d.with_notes(vec![help.to_owned()]); @@ -90,8 +86,7 @@ fn into_diagnostic(err: InterpreterError) -> Diagnostic { .with_code("resolver") .with_message(cause.to_string()) .with_labels(vec![ - Label::primary(span.source_id(), span_to_range(span)) - .with_message("related to this"), + Label::primary(span.source_id(), span.range()).with_message("related to this"), ]) } InterpreterError::Compiler { cause } => { @@ -100,8 +95,7 @@ fn into_diagnostic(err: InterpreterError) -> Diagnostic { .with_code("compiler") .with_message(cause.to_string()) .with_labels(vec![ - Label::primary(span.source_id(), span_to_range(span)) - .with_message("related to this"), + Label::primary(span.source_id(), span.range()).with_message("related to this"), ]) } InterpreterError::Vm(err) => { @@ -110,8 +104,7 @@ fn into_diagnostic(err: InterpreterError) -> Diagnostic { .with_message(&err.message); if let Some(span) = err.span { d = d.with_labels(vec![ - Label::primary(span.source_id(), span_to_range(span)) - .with_message("related to this"), + Label::primary(span.source_id(), span.range()).with_message("related to this"), ]); } d From 658eb57352d0fd70a796cbd45d149229d4c2ea57 Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Sun, 22 Mar 2026 13:20:28 +0100 Subject: [PATCH 3/3] Give each REPL input a unique source name (, , ...) Co-Authored-By: Claude Opus 4.6 --- ndc_bin/src/repl.rs | 4 ++-- ndc_interpreter/src/lib.rs | 6 +++++- ndc_parser/src/expression.rs | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/ndc_bin/src/repl.rs b/ndc_bin/src/repl.rs index bc3e43ef..a2ce4d56 100644 --- a/ndc_bin/src/repl.rs +++ b/ndc_bin/src/repl.rs @@ -34,14 +34,14 @@ pub fn run() -> anyhow::Result<()> { let mut interpreter = Interpreter::new(); interpreter.configure(ndc_stdlib::register); - loop { + for command_nr in 1.. { match rl.readline("λ ") { Ok(line) => { // If we can't append the history we just ignore this let _ = rl.add_history_entry(line.as_str()); // Run the line we just read through the interpreter - match interpreter.eval_named("", line.as_str()) { + match interpreter.eval_named(format!(""), line.as_str()) { Ok(value) => { let output = value.to_string(); if !output.is_empty() { diff --git a/ndc_interpreter/src/lib.rs b/ndc_interpreter/src/lib.rs index 70b032e9..3fc87c2c 100644 --- a/ndc_interpreter/src/lib.rs +++ b/ndc_interpreter/src/lib.rs @@ -107,7 +107,11 @@ impl Interpreter { } /// Execute source code with a custom source name for diagnostics. - pub fn eval_named(&mut self, name: &str, input: &str) -> Result { + pub fn eval_named( + &mut self, + name: impl Into, + input: &str, + ) -> Result { let source_id = self.source_db.add(name, input); let expressions = self.parse_and_analyse(input, source_id)?; self.interpret_vm(input, expressions.into_iter()) diff --git a/ndc_parser/src/expression.rs b/ndc_parser/src/expression.rs index 4173bd36..e3f312f1 100644 --- a/ndc_parser/src/expression.rs +++ b/ndc_parser/src/expression.rs @@ -140,6 +140,7 @@ pub enum ForIteration { } #[derive(Debug, Eq, PartialEq, Clone)] +#[allow(clippy::large_enum_variant)] pub enum ForBody { Block(ExpressionLocation), List {