@@ -14,6 +14,7 @@ use simplicity::elements;
1414
1515use crate :: lexer:: Token ;
1616use crate :: parse:: MatchPattern ;
17+ use crate :: resolution:: SourceFile ;
1718use crate :: str:: { AliasName , FunctionName , Identifier , JetName , ModuleName , WitnessName } ;
1819use 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
154169impl 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 ) ]
359401pub 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+
367412impl 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 {
681730mod 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;
685734let 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 |
6977461 | 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 |
7117602 | 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 |
7257741 | 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