Skip to content

Commit 816b746

Browse files
committed
feat: add SourceFile to the RichError and change the ErrorCollector
1 parent 0bb3a67 commit 816b746

6 files changed

Lines changed: 197 additions & 80 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# 0.5.0-rc.0 - 2026-03-14
22

3+
* Replaced the deprecated `WithFile` trait with `WithContent` and `WithSource` to cleanly separate single-file execution from multi-file environments. [#266](https://github.com/BlockstreamResearch/SimplicityHL/pull/266)
34
* Migrate from the `pest` parser to a new `chumsky`-based parser, improving parser recovery and enabling multiple parse errors to be reported in one pass [#185](https://github.com/BlockstreamResearch/SimplicityHL/pull/185)
45
* `simc` now accepts `--args <file>` for parameterized contracts, and witness input is supplied explicitly via `--wit <file>` [#200](https://github.com/BlockstreamResearch/SimplicityHL/pull/200)
56
* Expose contract ABI metadata for tooling via `simc --abi`, and add library accessors for parameter and witness types [#201](https://github.com/BlockstreamResearch/SimplicityHL/pull/201), [#219](https://github.com/BlockstreamResearch/SimplicityHL/pull/219)

src/error.rs

Lines changed: 167 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use simplicity::elements;
1414

1515
use crate::lexer::Token;
1616
use crate::parse::MatchPattern;
17+
use crate::resolution::SourceFile;
1718
use crate::str::{AliasName, FunctionName, Identifier, JetName, ModuleName, WitnessName};
1819
use crate::types::{ResolvedType, UIntType};
1920

@@ -118,16 +119,30 @@ impl<T, E: Into<Error>> WithSpan<T> for Result<T, E> {
118119
}
119120

120121
/// Helper trait to update `Result<A, RichError>` with the affected source file.
121-
pub trait WithFile<T> {
122+
pub trait WithContent<T> {
122123
/// Update the result with the affected source file.
123124
///
124125
/// Enable pretty errors.
125-
fn with_file<F: Into<Arc<str>>>(self, file: F) -> Result<T, RichError>;
126+
fn with_content<C: Into<Arc<str>>>(self, content: C) -> Result<T, RichError>;
126127
}
127128

128-
impl<T> WithFile<T> for Result<T, RichError> {
129-
fn with_file<F: Into<Arc<str>>>(self, file: F) -> Result<T, RichError> {
130-
self.map_err(|e| e.with_file(file.into()))
129+
impl<T> WithContent<T> for Result<T, RichError> {
130+
fn with_content<C: Into<Arc<str>>>(self, content: C) -> Result<T, RichError> {
131+
self.map_err(|e| e.with_content(content.into()))
132+
}
133+
}
134+
135+
/// Helper trait to update `Result<A, RichError>` with the affected source file.
136+
pub trait WithSource<T> {
137+
/// Update the result with the affected source file.
138+
///
139+
/// Enable pretty errors.
140+
fn with_source<S: Into<SourceFile>>(self, source: S) -> Result<T, RichError>;
141+
}
142+
143+
impl<T> WithSource<T> for Result<T, RichError> {
144+
fn with_source<S: Into<SourceFile>>(self, source: S) -> Result<T, RichError> {
145+
self.map_err(|e| e.with_source(source.into()))
131146
}
132147
}
133148

@@ -145,10 +160,10 @@ pub struct RichError {
145160
/// Area that the error spans inside the file.
146161
span: Span,
147162

148-
/// File in which the error occurred.
163+
/// File context in which the error occurred.
149164
///
150165
/// Required to print pretty errors.
151-
file: Option<Arc<str>>,
166+
source: Option<SourceFile>,
152167
}
153168

154169
impl RichError {
@@ -157,18 +172,30 @@ impl RichError {
157172
RichError {
158173
error: Box::new(error),
159174
span,
160-
file: None,
175+
source: None,
176+
}
177+
}
178+
179+
/// Adds raw source code content to the error context.
180+
///
181+
/// Use this when the error occurs in an environment without a backing physical file
182+
/// (e.g., raw string input for single-file program) to enable basic error formatting.
183+
pub fn with_content(self, program_content: Arc<str>) -> Self {
184+
Self {
185+
error: self.error,
186+
span: self.span,
187+
source: Some(SourceFile::anonymous(program_content)),
161188
}
162189
}
163190

164191
/// Add the source file where the error occurred.
165192
///
166193
/// Enable pretty errors.
167-
pub fn with_file(self, file: Arc<str>) -> Self {
194+
pub fn with_source(self, source: SourceFile) -> Self {
168195
Self {
169196
error: self.error,
170197
span: self.span,
171-
file: Some(file),
198+
source: Some(source),
172199
}
173200
}
174201

@@ -178,12 +205,12 @@ impl RichError {
178205
Self {
179206
error: Box::new(Error::CannotParse(reason.to_string())),
180207
span: Span::new(0, 0),
181-
file: None,
208+
source: None,
182209
}
183210
}
184211

185-
pub fn file(&self) -> &Option<Arc<str>> {
186-
&self.file
212+
pub fn source(&self) -> &Option<SourceFile> {
213+
&self.source
187214
}
188215

189216
pub fn error(&self) -> &Error {
@@ -214,41 +241,56 @@ impl fmt::Display for RichError {
214241
(line, col)
215242
}
216243

217-
match self.file {
218-
Some(ref file) if !file.is_empty() => {
219-
let (start_line, start_col) = get_line_col(file, self.span.start);
220-
let (end_line, end_col) = get_line_col(file, self.span.end);
244+
let Some(source) = &self.source else {
245+
return write!(f, "{}", self.error);
246+
};
221247

222-
let start_line_index = start_line - 1;
248+
let content = source.content();
223249

224-
let n_spanned_lines = end_line - start_line_index;
225-
let line_num_width = end_line.to_string().len();
250+
if content.is_empty() {
251+
return write!(f, "{}", self.error);
252+
}
226253

227-
writeln!(f, "{:width$} |", " ", width = line_num_width)?;
254+
let (start_line, start_col) = get_line_col(&content, self.span.start);
255+
let (end_line, end_col) = get_line_col(&content, self.span.end);
228256

229-
let mut lines = file.lines().skip(start_line_index).peekable();
230-
let start_line_len = lines.peek().map_or(0, |l| l.len());
257+
let start_line_index = start_line - 1;
231258

232-
for (relative_line_index, line_str) in lines.take(n_spanned_lines).enumerate() {
233-
let line_num = start_line_index + relative_line_index + 1;
234-
writeln!(f, "{line_num:line_num_width$} | {line_str}")?;
235-
}
259+
let n_spanned_lines = end_line - start_line_index;
260+
let line_num_width = end_line.to_string().len();
236261

237-
let is_multiline = end_line > start_line;
262+
if let Some(name) = source.name() {
263+
writeln!(
264+
f,
265+
"{:>width$}--> {}:{}:{}",
266+
"",
267+
name.display(),
268+
start_line,
269+
start_col,
270+
width = line_num_width
271+
)?;
272+
}
238273

239-
let (underline_start, underline_length) = match is_multiline {
240-
true => (0, start_line_len),
241-
false => (start_col, end_col - start_col),
242-
};
243-
write!(f, "{:width$} |", " ", width = line_num_width)?;
244-
write!(f, "{:width$}", " ", width = underline_start)?;
245-
write!(f, "{:^<width$} ", "", width = underline_length)?;
246-
write!(f, "{}", self.error)
247-
}
248-
_ => {
249-
write!(f, "{}", self.error)
250-
}
274+
writeln!(f, "{:width$} |", " ", width = line_num_width)?;
275+
276+
let mut lines = content.lines().skip(start_line_index).peekable();
277+
let start_line_len = lines.peek().map_or(0, |l| l.len());
278+
279+
for (relative_line_index, line_str) in lines.take(n_spanned_lines).enumerate() {
280+
let line_num = start_line_index + relative_line_index + 1;
281+
writeln!(f, "{line_num:line_num_width$} | {line_str}")?;
251282
}
283+
284+
let is_multiline = end_line > start_line;
285+
286+
let (underline_start, underline_length) = match is_multiline {
287+
true => (0, start_line_len),
288+
false => (start_col, end_col - start_col),
289+
};
290+
write!(f, "{:width$} |", " ", width = line_num_width)?;
291+
write!(f, "{:width$}", " ", width = underline_start)?;
292+
write!(f, "{:^<width$} ", "", width = underline_length)?;
293+
write!(f, "{}", self.error)
252294
}
253295
}
254296

@@ -314,7 +356,7 @@ where
314356
found: found_string,
315357
}),
316358
span,
317-
file: None,
359+
source: None,
318360
}
319361
}
320362
}
@@ -341,7 +383,7 @@ where
341383
found: found_string,
342384
}),
343385
span,
344-
file: None,
386+
source: None,
345387
}
346388
}
347389

@@ -357,26 +399,33 @@ where
357399

358400
#[derive(Debug, Clone, Hash)]
359401
pub struct ErrorCollector {
360-
/// File in which the error occurred.
361-
file: Arc<str>,
362-
363402
/// Collected errors.
364403
errors: Vec<RichError>,
365404
}
366405

406+
impl Default for ErrorCollector {
407+
fn default() -> Self {
408+
Self::new()
409+
}
410+
}
411+
367412
impl ErrorCollector {
368-
pub fn new(file: Arc<str>) -> Self {
369-
Self {
370-
file,
371-
errors: Vec::new(),
372-
}
413+
pub fn new() -> Self {
414+
Self { errors: Vec::new() }
373415
}
374416

375-
/// Extend existing errors with slice of new errors.
376-
pub fn update(&mut self, errors: impl IntoIterator<Item = RichError>) {
417+
/// Exten existing errors with specific `RichError`.
418+
/// We assume that `RichError` contains `SourceFile`.
419+
pub fn push(&mut self, error: RichError) {
420+
self.errors.push(error);
421+
}
422+
423+
/// Appends new errors, tagging them with the provided source context.
424+
/// Automatically handles both single-file and multi-file environments.
425+
pub fn extend(&mut self, source: SourceFile, errors: impl IntoIterator<Item = RichError>) {
377426
let new_errors = errors
378427
.into_iter()
379-
.map(|err| err.with_file(Arc::clone(&self.file)));
428+
.map(|err| err.with_source(source.clone()));
380429

381430
self.errors.extend(new_errors);
382431
}
@@ -385,8 +434,8 @@ impl ErrorCollector {
385434
&self.errors
386435
}
387436

388-
pub fn is_empty(&self) -> bool {
389-
self.get().is_empty()
437+
pub fn has_errors(&self) -> bool {
438+
!self.errors.is_empty()
390439
}
391440
}
392441

@@ -681,7 +730,7 @@ impl From<simplicity::types::Error> for Error {
681730
mod tests {
682731
use super::*;
683732

684-
const FILE: &str = r#"let a1: List<u32, 5> = None;
733+
const CONTENT: &str = r#"let a1: List<u32, 5> = None;
685734
let x: u32 = Left(
686735
Right(0)
687736
);"#;
@@ -691,7 +740,7 @@ let x: u32 = Left(
691740
fn display_single_line() {
692741
let error = Error::ListBoundPow2(5)
693742
.with_span(Span::new(13, 19))
694-
.with_file(Arc::from(FILE));
743+
.with_content(Arc::from(CONTENT));
695744
let expected = r#"
696745
|
697746
1 | let a1: List<u32, 5> = None;
@@ -704,8 +753,8 @@ let x: u32 = Left(
704753
let error = Error::CannotParse(
705754
"Expected value of type `u32`, got `Either<Either<_, u32>, _>`".to_string(),
706755
)
707-
.with_span(Span::new(41, FILE.len()))
708-
.with_file(Arc::from(FILE));
756+
.with_span(Span::new(41, CONTENT.len()))
757+
.with_content(Arc::from(CONTENT));
709758
let expected = r#"
710759
|
711760
2 | let x: u32 = Left(
@@ -718,8 +767,8 @@ let x: u32 = Left(
718767
#[test]
719768
fn display_entire_file() {
720769
let error = Error::CannotParse("This span covers the entire file".to_string())
721-
.with_span(Span::from(FILE))
722-
.with_file(Arc::from(FILE));
770+
.with_span(Span::from(CONTENT))
771+
.with_content(Arc::from(CONTENT));
723772
let expected = r#"
724773
|
725774
1 | let a1: List<u32, 5> = None;
@@ -746,8 +795,64 @@ let x: u32 = Left(
746795
fn display_empty_file() {
747796
let error = Error::CannotParse("This error has an empty file".to_string())
748797
.with_span(Span::from(EMPTY_FILE))
749-
.with_file(Arc::from(EMPTY_FILE));
798+
.with_content(Arc::from(EMPTY_FILE));
750799
let expected = "Cannot parse: This error has an empty file";
751800
assert_eq!(&expected, &error.to_string());
752801
}
802+
803+
// --- Tests with filename ---
804+
#[test]
805+
fn display_single_line_with_file() {
806+
let source = SourceFile::new(std::path::Path::new("src/main.simf"), Arc::from(CONTENT));
807+
let error = Error::ListBoundPow2(5)
808+
.with_span(Span::new(13, 19))
809+
.with_source(source);
810+
811+
let expected = r#"
812+
--> src/main.simf:1:14
813+
|
814+
1 | let a1: List<u32, 5> = None;
815+
| ^^^^^^ Expected a power of two greater than one (2, 4, 8, 16, 32, ...) as list bound, found 5"#;
816+
assert_eq!(&expected[1..], &error.to_string());
817+
}
818+
819+
#[test]
820+
fn display_multi_line_with_file() {
821+
let source = SourceFile::new(std::path::Path::new("lib/parser.simf"), Arc::from(CONTENT));
822+
let error = Error::CannotParse(
823+
"Expected value of type `u32`, got `Either<Either<_, u32>, _>`".to_string(),
824+
)
825+
.with_span(Span::new(41, CONTENT.len()))
826+
.with_source(source);
827+
828+
let expected = r#"
829+
--> lib/parser.simf:2:14
830+
|
831+
2 | let x: u32 = Left(
832+
3 | Right(0)
833+
4 | );
834+
| ^^^^^^^^^^^^^^^^^^ Cannot parse: Expected value of type `u32`, got `Either<Either<_, u32>, _>`"#;
835+
assert_eq!(&expected[1..], &error.to_string());
836+
}
837+
838+
#[test]
839+
fn display_entire_file_with_file() {
840+
let source = SourceFile::new(
841+
std::path::Path::new("tests/integration.simf"),
842+
Arc::from(CONTENT),
843+
);
844+
let error = Error::CannotParse("This span covers the entire file".to_string())
845+
.with_span(Span::from(CONTENT))
846+
.with_source(source);
847+
848+
let expected = r#"
849+
--> tests/integration.simf:1:1
850+
|
851+
1 | let a1: List<u32, 5> = None;
852+
2 | let x: u32 = Left(
853+
3 | Right(0)
854+
4 | );
855+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot parse: This span covers the entire file"#;
856+
assert_eq!(&expected[1..], &error.to_string());
857+
}
753858
}

0 commit comments

Comments
 (0)