diff --git a/CHANGELOG.md b/CHANGELOG.md index d7feb300..43969856 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# Unreleased + +* Replaced the deprecated `WithFile` trait with `WithContent` and `WithSource` to cleanly separate single-file execution from multi-file environments. Additionally, replaced the `file()` method on `RichError` with `source()`. [#266](https://github.com/BlockstreamResearch/SimplicityHL/pull/266) + # 0.5.0-rc.0 - 2026-03-14 * 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) diff --git a/doc/architecture.md b/doc/architecture.md new file mode 100644 index 00000000..74dcf0f9 --- /dev/null +++ b/doc/architecture.md @@ -0,0 +1,7 @@ +# Architecture Note: Omitted Keywords +The `crate` and `super` keywords were not added to the compiler because they +are unnecessary at this stage. Typically, they are used to resolve relative +paths during import parsing. However, in our architecture, the prefix before +the first `::` in a `use` statement is always an dependency root path. Since all +dependency root paths are unique and strictly bound to specific paths, the resolver +can always unambiguously resolve the path without needing relative pointers. \ No newline at end of file diff --git a/fuzz/fuzz_targets/compile_parse_tree.rs b/fuzz/fuzz_targets/compile_parse_tree.rs index 18613500..d24c37e3 100644 --- a/fuzz/fuzz_targets/compile_parse_tree.rs +++ b/fuzz/fuzz_targets/compile_parse_tree.rs @@ -4,7 +4,7 @@ fn do_test(data: &[u8]) { use arbitrary::Arbitrary; - use simplicityhl::error::WithFile; + use simplicityhl::error::WithContent; use simplicityhl::{ast, named, parse, ArbitraryOfType, Arguments}; let mut u = arbitrary::Unstructured::new(data); @@ -22,7 +22,7 @@ fn do_test(data: &[u8]) { }; let simplicity_named_construct = ast_program .compile(arguments, false) - .with_file("") + .with_content("") .expect("AST should compile with given arguments"); let _simplicity_commit = named::forget_names(&simplicity_named_construct); } diff --git a/src/ast.rs b/src/ast.rs index 20dd2a70..adf58ff7 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -762,6 +762,10 @@ impl AbstractSyntaxTree for Item { parse::Item::Function(function) => { Function::analyze(function, ty, scope).map(Self::Function) } + parse::Item::Use(use_decl) => Err(RichError::new( + Error::CannotCompile("The `use` keyword is not supported yet.".to_string()), + *use_decl.span(), + )), parse::Item::Module => Ok(Self::Module), } } diff --git a/src/error.rs b/src/error.rs index b4d5a4b7..e32276d7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,6 @@ use std::fmt; use std::ops::Range; +use std::path::PathBuf; use std::sync::Arc; use chumsky::error::Error as ChumskyError; @@ -14,6 +15,7 @@ use simplicity::elements; use crate::lexer::Token; use crate::parse::MatchPattern; +use crate::resolution::SourceFile; use crate::str::{AliasName, FunctionName, Identifier, JetName, ModuleName, WitnessName}; use crate::types::{ResolvedType, UIntType}; @@ -118,16 +120,30 @@ impl> WithSpan for Result { } /// Helper trait to update `Result` with the affected source file. -pub trait WithFile { +pub trait WithContent { /// Update the result with the affected source file. /// /// Enable pretty errors. - fn with_file>>(self, file: F) -> Result; + fn with_content>>(self, content: C) -> Result; } -impl WithFile for Result { - fn with_file>>(self, file: F) -> Result { - self.map_err(|e| e.with_file(file.into())) +impl WithContent for Result { + fn with_content>>(self, content: C) -> Result { + self.map_err(|e| e.with_content(content.into())) + } +} + +/// Helper trait to update `Result` with the affected source file. +pub trait WithSource { + /// Update the result with the affected source file. + /// + /// Enable pretty errors. + fn with_source>(self, source: S) -> Result; +} + +impl WithSource for Result { + fn with_source>(self, source: S) -> Result { + self.map_err(|e| e.with_source(source.into())) } } @@ -137,33 +153,50 @@ impl WithFile for Result { #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct RichError { /// The error that occurred. - error: Error, + /// + /// Wrapped in a `Box` to keep the `RichError` struct small on the stack, + /// ensuring cheap moves when returning errors inside a `Result`. + error: Box, + /// Area that the error spans inside the file. span: Span, - /// File in which the error occurred. + + /// File context in which the error occurred. /// /// Required to print pretty errors. - file: Option>, + source: Option, } impl RichError { /// Create a new error with context. pub fn new(error: Error, span: Span) -> RichError { RichError { - error, + error: Box::new(error), span, - file: None, + source: None, + } + } + + /// Adds raw source code content to the error context. + /// + /// Use this when the error occurs in an environment without a backing physical file + /// (e.g., raw string input for single-file program) to enable basic error formatting. + pub fn with_content(self, program_content: Arc) -> Self { + Self { + error: self.error, + span: self.span, + source: Some(SourceFile::anonymous(program_content)), } } /// Add the source file where the error occurred. /// /// Enable pretty errors. - pub fn with_file(self, file: Arc) -> Self { + pub fn with_source(self, source: SourceFile) -> Self { Self { error: self.error, span: self.span, - file: Some(file), + source: Some(source), } } @@ -171,14 +204,14 @@ impl RichError { /// a problem on the parsing side. pub fn parsing_error(reason: &str) -> Self { Self { - error: Error::CannotParse(reason.to_string()), + error: Box::new(Error::CannotParse(reason.to_string())), span: Span::new(0, 0), - file: None, + source: None, } } - pub fn file(&self) -> &Option> { - &self.file + pub fn source(&self) -> &Option { + &self.source } pub fn error(&self) -> &Error { @@ -210,47 +243,61 @@ impl fmt::Display for RichError { (line, col + 1) } - match self.file { - Some(ref file) if !file.is_empty() => { - let (start_line, start_col) = get_line_col(file, self.span.start); - let (end_line, end_col) = get_line_col(file, self.span.end); + let Some(source) = &self.source else { + return write!(f, "{}", self.error); + }; - let start_line_index = start_line - 1; + let content = source.content(); - let n_spanned_lines = end_line - start_line_index; - let line_num_width = end_line.to_string().len(); + if content.is_empty() { + return write!(f, "{}", self.error); + } - writeln!(f, "{:width$} |", " ", width = line_num_width)?; + let (start_line, start_col) = get_line_col(&content, self.span.start); + let (end_line, end_col) = get_line_col(&content, self.span.end); - let mut lines = file - .split(|c: char| c.is_newline()) - .skip(start_line_index) - .peekable(); + let start_line_index = start_line - 1; - let start_line_len = lines - .peek() - .map_or(0, |l| l.chars().map(char::len_utf16).sum()); + let n_spanned_lines = end_line - start_line_index; + let line_num_width = end_line.to_string().len(); - for (relative_line_index, line_str) in lines.take(n_spanned_lines).enumerate() { - let line_num = start_line_index + relative_line_index + 1; - writeln!(f, "{line_num:line_num_width$} | {line_str}")?; - } + if let Some(name) = source.name() { + writeln!( + f, + "{:>width$}--> {}:{}:{}", + "", + name.display(), + start_line, + start_col, + width = line_num_width + )?; + } - let is_multiline = end_line > start_line; + writeln!(f, "{:width$} |", " ", width = line_num_width)?; - let (underline_start, underline_length) = match is_multiline { - true => (0, start_line_len), - false => (start_col, (end_col - start_col).max(1)), - }; - write!(f, "{:width$} |", " ", width = line_num_width)?; - write!(f, "{:width$}", " ", width = underline_start)?; - write!(f, "{:^ { - write!(f, "{}", self.error) - } + let mut lines = content + .split(|c: char| c.is_newline()) + .skip(start_line_index) + .peekable(); + let start_line_len = lines + .peek() + .map_or(0, |l| l.chars().map(char::len_utf16).sum()); + + for (relative_line_index, line_str) in lines.take(n_spanned_lines).enumerate() { + let line_num = start_line_index + relative_line_index + 1; + writeln!(f, "{line_num:line_num_width$} | {line_str}")?; } + + let is_multiline = end_line > start_line; + + let (underline_start, underline_length) = match is_multiline { + true => (0, start_line_len), + false => (start_col, (end_col - start_col).max(1)), + }; + write!(f, "{:width$} |", " ", width = line_num_width)?; + write!(f, "{:width$}", " ", width = underline_start)?; + write!(f, "{:^ for Error { fn from(error: RichError) -> Self { - error.error + *error.error } } @@ -274,7 +321,7 @@ where I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, { fn merge(self, other: Self) -> Self { - match (&self.error, &other.error) { + match (self.error.as_ref(), other.error.as_ref()) { (Error::Grammar(_), Error::Grammar(_)) => other, (Error::Grammar(_), _) => other, (_, Error::Grammar(_)) => self, @@ -310,13 +357,13 @@ where let found_string = found.map(|t| t.to_string()); Self { - error: Error::Syntax { + error: Box::new(Error::Syntax { expected: expected_tokens, label: None, found: found_string, - }, + }), span, - file: None, + source: None, } } } @@ -337,20 +384,20 @@ where let found_string = found.map(|t| t.to_string()); Self { - error: Error::Syntax { + error: Box::new(Error::Syntax { expected: expected_strings, label: None, found: found_string, - }, + }), span, - file: None, + source: None, } } fn label_with(&mut self, label: &'tokens str) { if let Error::Syntax { label: ref mut l, .. - } = &mut self.error + } = self.error.as_mut() { *l = Some(label.to_string()); } @@ -359,26 +406,33 @@ where #[derive(Debug, Clone, Hash)] pub struct ErrorCollector { - /// File in which the error occurred. - file: Arc, - /// Collected errors. errors: Vec, } +impl Default for ErrorCollector { + fn default() -> Self { + Self::new() + } +} + impl ErrorCollector { - pub fn new(file: Arc) -> Self { - Self { - file, - errors: Vec::new(), - } + pub fn new() -> Self { + Self { errors: Vec::new() } } - /// Extend existing errors with slice of new errors. - pub fn update(&mut self, errors: impl IntoIterator) { + /// Exten existing errors with specific `RichError`. + /// We assume that `RichError` contains `SourceFile`. + pub fn push(&mut self, error: RichError) { + self.errors.push(error); + } + + /// Appends new errors, tagging them with the provided source context. + /// Automatically handles both single-file and multi-file environments. + pub fn extend(&mut self, source: SourceFile, errors: impl IntoIterator) { let new_errors = errors .into_iter() - .map(|err| err.with_file(Arc::clone(&self.file))); + .map(|err| err.with_source(source.clone())); self.errors.extend(new_errors); } @@ -387,8 +441,8 @@ impl ErrorCollector { &self.errors } - pub fn is_empty(&self) -> bool { - self.get().is_empty() + pub fn has_errors(&self) -> bool { + !self.errors.is_empty() } } @@ -406,6 +460,8 @@ impl fmt::Display for ErrorCollector { /// Records _what_ happened but not where. #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum Error { + Internal(String), + UnknownLibrary(String), ArraySizeNonZero(usize), ListBoundPow2(usize), BitStringPow2(usize), @@ -423,6 +479,15 @@ pub enum Error { CannotCompile(String), JetDoesNotExist(JetName), InvalidCast(ResolvedType, ResolvedType), + FileNotFound(PathBuf), + UnresolvedItem { + name: String, + target_file: PathBuf, + }, + PrivateItem { + name: String, + target_file: PathBuf, + }, MainNoInputs, MainNoOutput, MainRequired, @@ -439,6 +504,10 @@ pub enum Error { RedefinedAlias(AliasName), RedefinedAliasAsBuiltin(AliasName), UndefinedAlias(AliasName), + DuplicateAlias { + name: String, + target_file: PathBuf, + }, VariableReuseInPattern(Identifier), WitnessReused(WitnessName), WitnessTypeMismatch(WitnessName, ResolvedType, ResolvedType), @@ -453,6 +522,14 @@ pub enum Error { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Error::Internal(err) => write!( + f, + "INTERNAL ERROR: {err}" + ), + Error::UnknownLibrary(name) => write!( + f, + "Unknown module or library '{name}'" + ), Error::ArraySizeNonZero(size) => write!( f, "Expected a non-negative integer as array size, found {size}" @@ -473,6 +550,10 @@ impl fmt::Display for Error { f, "Grammar error: {description}" ), + Error::FileNotFound(path) => write!( + f, + "File `{}` not found", path.to_string_lossy() + ), Error::Syntax { expected, label, found } => { let found_text = found.clone().unwrap_or("end of input".to_string()); match (label, expected.len()) { @@ -524,6 +605,16 @@ impl fmt::Display for Error { f, "Function `{name}` was called but not defined" ), + Error::UnresolvedItem { name, target_file } => write!( + f, + "Item `{}` could not be fouhnd in the file `{}`", + name, target_file.to_string_lossy() + ), + Error::PrivateItem { name, target_file } => write!( + f, + "Item `{}` is private in module `{}`", + name, target_file.to_string_lossy() + ), Error::InvalidNumberOfArguments(expected, found) => write!( f, "Expected {expected} arguments, found {found} arguments" @@ -568,6 +659,11 @@ impl fmt::Display for Error { f, "Type alias `{identifier}` is not defined" ), + Error::DuplicateAlias { name, target_file } => write!( + f, + "The alias `{}` was defined multiple times for `{}`", + name, target_file.to_string_lossy() + ), Error::VariableReuseInPattern(identifier) => write!( f, "Variable `{identifier}` is used twice in the pattern" @@ -641,7 +737,7 @@ impl From for Error { mod tests { use super::*; - const FILE: &str = r#"let a1: List = None; + const CONTENT: &str = r#"let a1: List = None; let x: u32 = Left( Right(0) );"#; @@ -651,7 +747,7 @@ let x: u32 = Left( fn display_single_line() { let error = Error::ListBoundPow2(5) .with_span(Span::new(13, 19)) - .with_file(Arc::from(FILE)); + .with_content(Arc::from(CONTENT)); let expected = r#" | 1 | let a1: List = None; @@ -664,8 +760,8 @@ let x: u32 = Left( let error = Error::CannotParse( "Expected value of type `u32`, got `Either, _>`".to_string(), ) - .with_span(Span::new(41, FILE.len())) - .with_file(Arc::from(FILE)); + .with_span(Span::new(41, CONTENT.len())) + .with_content(Arc::from(CONTENT)); let expected = r#" | 2 | let x: u32 = Left( @@ -678,8 +774,8 @@ let x: u32 = Left( #[test] fn display_entire_file() { let error = Error::CannotParse("This span covers the entire file".to_string()) - .with_span(Span::from(FILE)) - .with_file(Arc::from(FILE)); + .with_span(Span::from(CONTENT)) + .with_content(Arc::from(CONTENT)); let expected = r#" | 1 | let a1: List = None; @@ -706,7 +802,7 @@ let x: u32 = Left( fn display_empty_file() { let error = Error::CannotParse("This error has an empty file".to_string()) .with_span(Span::from(EMPTY_FILE)) - .with_file(Arc::from(EMPTY_FILE)); + .with_content(Arc::from(EMPTY_FILE)); let expected = "Cannot parse: This error has an empty file"; assert_eq!(&expected, &error.to_string()); } @@ -716,7 +812,7 @@ let x: u32 = Left( let file = "/*😀*/ let a: u8 = 65536;"; let error = Error::CannotParse("number too large to fit in target type".to_string()) .with_span(Span::new(21, 26)) - .with_file(Arc::from(file)); + .with_content(Arc::from(file)); let expected = r#" | @@ -735,7 +831,7 @@ let x: u32 = Left( );"#; let error = Error::CannotParse("This span covers the entire file".to_string()) .with_span(Span::from(file)) - .with_file(Arc::from(file)); + .with_content(Arc::from(file)); let expected = r#" | @@ -754,7 +850,7 @@ let x: u32 = Left( let file = "let a: u8 = 65536;\u{2028}let b: u8 = 0;"; let error = Error::CannotParse("number too large to fit in target type".to_string()) .with_span(Span::new(12, 17)) - .with_file(Arc::from(file)); + .with_content(Arc::from(file)); let expected = r#" | @@ -769,7 +865,7 @@ let x: u32 = Left( let file = "fn main()"; let error = Error::Grammar("Error span at (0,0)".to_string()) .with_span(Span::new(0, 0)) - .with_file(Arc::from(file)); + .with_content(Arc::from(file)); let expected = r#" | @@ -783,7 +879,7 @@ let x: u32 = Left( let file = "fn main(){\n let a:\n"; let error = Error::CannotParse("eof".to_string()) .with_span(Span::new(file.len(), file.len())) - .with_file(Arc::from(file)); + .with_content(Arc::from(file)); let expected = r#" | @@ -792,4 +888,60 @@ let x: u32 = Left( assert_eq!(&expected[1..], &error.to_string()); } + + // --- Tests with filename --- + #[test] + fn display_single_line_with_file() { + let source = SourceFile::new(std::path::Path::new("src/main.simf"), Arc::from(CONTENT)); + let error = Error::ListBoundPow2(5) + .with_span(Span::new(13, 19)) + .with_source(source); + + let expected = r#" + --> src/main.simf:1:14 + | +1 | let a1: List = None; + | ^^^^^^ Expected a power of two greater than one (2, 4, 8, 16, 32, ...) as list bound, found 5"#; + assert_eq!(&expected[1..], &error.to_string()); + } + + #[test] + fn display_multi_line_with_file() { + let source = SourceFile::new(std::path::Path::new("lib/parser.simf"), Arc::from(CONTENT)); + let error = Error::CannotParse( + "Expected value of type `u32`, got `Either, _>`".to_string(), + ) + .with_span(Span::new(41, CONTENT.len())) + .with_source(source); + + let expected = r#" + --> lib/parser.simf:2:13 + | +2 | let x: u32 = Left( +3 | Right(0) +4 | ); + | ^^^^^^^^^^^^^^^^^^ Cannot parse: Expected value of type `u32`, got `Either, _>`"#; + assert_eq!(&expected[1..], &error.to_string()); + } + + #[test] + fn display_entire_file_with_file() { + let source = SourceFile::new( + std::path::Path::new("tests/integration.simf"), + Arc::from(CONTENT), + ); + let error = Error::CannotParse("This span covers the entire file".to_string()) + .with_span(Span::from(CONTENT)) + .with_source(source); + + let expected = r#" + --> tests/integration.simf:1:1 + | +1 | let a1: List = None; +2 | let x: u32 = Left( +3 | Right(0) +4 | ); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot parse: This span covers the entire file"#; + assert_eq!(&expected[1..], &error.to_string()); + } } diff --git a/src/lexer.rs b/src/lexer.rs index f4326320..914d3bc6 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -11,6 +11,8 @@ pub type Tokens<'src> = Vec<(Token<'src>, crate::error::Span)>; #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum Token<'src> { // Keywords + Pub, + Use, Fn, Let, Type, @@ -66,6 +68,8 @@ pub enum Token<'src> { impl<'src> fmt::Display for Token<'src> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Token::Pub => write!(f, "pub"), + Token::Use => write!(f, "use"), Token::Fn => write!(f, "fn"), Token::Let => write!(f, "let"), Token::Type => write!(f, "type"), @@ -138,6 +142,8 @@ pub fn lexer<'src>( choice((just("assert!"), just("panic!"), just("dbg!"), just("list!"))).map(Token::Macro); let keyword = text::ident().map(|s| match s { + "pub" => Token::Pub, + "use" => Token::Use, "fn" => Token::Fn, "let" => Token::Let, "type" => Token::Type, @@ -247,7 +253,7 @@ pub fn lex<'src>(input: &'src str) -> (Option>, Vec bool { matches!( s, - "fn" | "let" | "type" | "mod" | "const" | "match" | "true" | "false" + "pub" | "use" | "fn" | "let" | "type" | "mod" | "const" | "match" | "true" | "false" ) } diff --git a/src/lib.rs b/src/lib.rs index 6e9bca76..f551ba12 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,9 +14,12 @@ pub mod named; pub mod num; pub mod parse; pub mod pattern; +pub mod resolution; #[cfg(feature = "serde")] mod serde; pub mod str; +#[cfg(test)] +pub mod test_utils; pub mod tracker; pub mod types; pub mod value; @@ -32,7 +35,7 @@ pub extern crate simplicity; pub use simplicity::elements; use crate::debug::DebugSymbols; -use crate::error::{ErrorCollector, WithFile}; +use crate::error::{ErrorCollector, WithContent}; use crate::parse::ParseFromStrWithErrors; pub use crate::types::ResolvedType; pub use crate::value::Value; @@ -55,10 +58,10 @@ impl TemplateProgram { /// The string is not a valid SimplicityHL program. pub fn new>>(s: Str) -> Result { let file = s.into(); - let mut error_handler = ErrorCollector::new(Arc::clone(&file)); + let mut error_handler = ErrorCollector::new(); let parse_program = parse::Program::parse_from_str_with_errors(&file, &mut error_handler); if let Some(program) = parse_program { - let ast_program = ast::Program::analyze(&program).with_file(Arc::clone(&file))?; + let ast_program = ast::Program::analyze(&program).with_content(Arc::clone(&file))?; Ok(Self { simfony: ast_program, file, @@ -96,7 +99,7 @@ impl TemplateProgram { let commit = self .simfony .compile(arguments, include_debug_symbols) - .with_file(Arc::clone(&self.file))?; + .with_content(Arc::clone(&self.file))?; Ok(CompiledProgram { debug_symbols: self.simfony.debug_symbols(self.file.as_ref()), diff --git a/src/parse.rs b/src/parse.rs index a6eebd18..5b906122 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -22,6 +22,7 @@ use crate::impl_eq_hash; use crate::lexer::Token; use crate::num::NonZeroPow2Usize; use crate::pattern::Pattern; +use crate::resolution::SourceFile; use crate::str::{ AliasName, Binary, Decimal, FunctionName, Hexadecimal, Identifier, JetName, ModuleName, WitnessName, @@ -52,13 +53,109 @@ pub enum Item { TypeAlias(TypeAlias), /// A function. Function(Function), + /// An import declaration (e.g., `use math::add`) that brings another + /// [`Item`] into the current scope. + Use(UseDecl), /// A module, which is ignored. Module, } -/// Definition of a function. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub enum Visibility { + Public, + #[default] + Private, +} + +/// Represents an import declaration in the Abstract Syntax Tree. +/// +/// This structure defines how items from other modules or files are brought into the +/// current scope. Note that in this architecture, the first identifier in the path +/// is always treated as an dependency root path name bound to a specific physical path. +/// +/// # Example +/// ```text +/// pub use std::collections::{HashMap, HashSet}; +/// ``` +#[derive(Clone, Debug)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct UseDecl { + /// The visibility of the import (e.g., `pub use` vs `use`). + visibility: Visibility, + + /// The base path to the target file or module. + /// + /// The first element is always the registered dependency root path name for + /// the import path. Subsequent elements represent nested modules or directories. + path: Vec, + + /// The specific item or list of items being imported from the resolved path. + items: UseItems, + span: Span, +} + +impl UseDecl { + pub fn visibility(&self) -> &Visibility { + &self.visibility + } + + /// Returns the full logical module path as a vector of string slices. + /// + /// This includes the Dependency Root Path Name (the first segment) + /// followed by all subsequent sub-module segments. + pub fn path(&self) -> Vec<&str> { + self.path.iter().map(|s| s.as_inner()).collect() + } + + /// Extracts the Dependency Root Path Name (the very first segment) from this path. + /// + /// # Errors + /// + /// Returns a `RichError` if the use declaration path is completely empty. + pub fn drp_name(&self) -> Result<&str, RichError> { + let parts = self.path(); + parts + .first() + .copied() + .ok_or_else(|| Error::CannotParse("Empty use path".to_string()).with_span(self.span)) + } + + pub fn items(&self) -> &UseItems { + &self.items + } + + pub fn span(&self) -> &Span { + &self.span + } +} + +impl_eq_hash!(UseDecl; visibility, path, drp_name, items); + +/// Specified the items being brought into scope at the end of a `use` declaration +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub enum UseItems { + /// A single item import. + /// + /// # Example + /// ```text + /// use core::math::add; + /// ``` + Single(Identifier), + + /// A multiple item import grouped in a list. + /// + /// # Example + /// ```text + /// use core::math::{add, subtract}; + /// ``` + List(Vec), +} + #[derive(Clone, Debug)] pub struct Function { + visibility: Visibility, name: FunctionName, params: Arc<[FunctionParam]>, ret: Option, @@ -67,6 +164,10 @@ pub struct Function { } impl Function { + pub fn visibility(&self) -> &Visibility { + &self.visibility + } + /// Access the name of the function. pub fn name(&self) -> &FunctionName { &self.name @@ -95,7 +196,7 @@ impl Function { } } -impl_eq_hash!(Function; name, params, ret, body); +impl_eq_hash!(Function; visibility, name, params, ret, body); /// Parameter of a function. #[derive(Clone, Debug, Eq, PartialEq, Hash)] @@ -222,12 +323,17 @@ pub enum CallName { #[derive(Clone, Debug)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct TypeAlias { + visibility: Visibility, name: AliasName, ty: AliasedType, span: Span, } impl TypeAlias { + pub fn visibility(&self) -> &Visibility { + &self.visibility + } + /// Access the name of the alias. pub fn name(&self) -> &AliasName { &self.name @@ -556,6 +662,7 @@ impl fmt::Display for Item { match self { Self::TypeAlias(alias) => write!(f, "{alias}"), Self::Function(function) => write!(f, "{function}"), + Self::Use(use_declaration) => write!(f, "{use_declaration}"), // The parse tree contains no information about the contents of modules. // We print a random empty module `mod witness {}` here // so that `from_string(to_string(x)) = x` holds for all trees `x`. @@ -564,15 +671,30 @@ impl fmt::Display for Item { } } +impl fmt::Display for Visibility { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Public => write!(f, "pub "), + Self::Private => write!(f, ""), + } + } +} + impl fmt::Display for TypeAlias { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "type {} = {};", self.name(), self.ty()) + write!( + f, + "{}type {} = {};", + self.visibility(), + self.name(), + self.ty() + ) } } impl fmt::Display for Function { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "fn {}(", self.name())?; + write!(f, "{}fn {}(", self.visibility(), self.name())?; for (i, param) in self.params().iter().enumerate() { if 0 < i { write!(f, ", ")?; @@ -587,6 +709,43 @@ impl fmt::Display for Function { } } +impl fmt::Display for UseDecl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let _ = write!(f, "{}use ", self.visibility()); + + for (i, segment) in self.path.iter().enumerate() { + if i > 0 { + write!(f, "::")?; + } + write!(f, "{}", segment)?; + } + + if !self.path.is_empty() { + write!(f, "::")?; + } + + write!(f, "{};", self.items) + } +} + +impl fmt::Display for UseItems { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + UseItems::Single(ident) => write!(f, "{}", ident), + UseItems::List(idents) => { + let _ = write!(f, "{{"); + for (i, ident) in idents.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", ident)?; + } + write!(f, "}}") + } + } + } +} + impl fmt::Display for FunctionParam { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}: {}", self.identifier(), self.ty()) @@ -921,7 +1080,8 @@ impl ParseFromStrWithErrors for A { fn parse_from_str_with_errors(s: &str, handler: &mut ErrorCollector) -> Option { let (tokens, lex_errs) = crate::lexer::lex(s); - handler.update(lex_errs); + let source = SourceFile::anonymous(Arc::from(s)); + handler.extend(source.clone(), lex_errs); let tokens = tokens?; let (ast, parse_errs) = A::parser() @@ -933,14 +1093,14 @@ impl ParseFromStrWithErrors for A { ) .into_output_errors(); - handler.update(parse_errs); + handler.extend(source, parse_errs); // TODO: We should return parsed result if we found errors, but because analyzing in `ast` module // is not handling poisoned tree right now, we don't return parsed result - if handler.get().is_empty() { - ast - } else { + if handler.has_errors() { None + } else { + ast } } } @@ -1129,7 +1289,12 @@ impl ChumskyParse for Program { let skip_until_next_item = any() .then( any() - .filter(|t| !matches!(t, Token::Fn | Token::Type | Token::Mod)) + .filter(|t| { + !matches!( + t, + Token::Pub | Token::Use | Token::Fn | Token::Type | Token::Mod + ) + }) .repeated(), ) // map to empty module @@ -1153,9 +1318,10 @@ impl ChumskyParse for Item { { let func_parser = Function::parser().map(Item::Function); let type_parser = TypeAlias::parser().map(Item::TypeAlias); + let use_parser = UseDecl::parser().map(Item::Use); let mod_parser = Module::parser().map(|_| Item::Module); - choice((func_parser, type_parser, mod_parser)) + choice((func_parser, use_parser, type_parser, mod_parser)) } } @@ -1164,6 +1330,12 @@ impl ChumskyParse for Function { where I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, { + let visibility = just(Token::Pub) + .to(Visibility::Public) + .or_not() + .map(Option::unwrap_or_default) + .labelled("function visibility"); + let params = delimited_with_recovery( FunctionParam::parser() .separated_by(just(Token::Comma)) @@ -1195,12 +1367,14 @@ impl ChumskyParse for Function { ))) .labelled("function body"); - just(Token::Fn) - .ignore_then(FunctionName::parser()) + visibility + .then_ignore(just(Token::Fn)) + .then(FunctionName::parser()) .then(params) .then(ret) .then(body) - .map_with(|(((name, params), ret), body), e| Self { + .map_with(|((((visibility, name), params), ret), body), e| Self { + visibility, name, params, ret, @@ -1210,6 +1384,48 @@ impl ChumskyParse for Function { } } +impl ChumskyParse for UseDecl { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let visibility = just(Token::Pub) + .to(Visibility::Public) + .or_not() + .map(Option::unwrap_or_default); + + // Parse the base path prefix (e.g., `dependency_root_path::file::` or `dependency_root_path::dir::file::`). + // We require at least 2 segments here because a valid import needs a minimum + // of 3 items total: the dependency_root_path, the file, and the specific item/function. + let path = Identifier::parser() + .then_ignore(just(Token::DoubleColon)) + .repeated() + .at_least(2) + .collect::>(); + + let list = Identifier::parser() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect() + .delimited_by(just(Token::LBrace), just(Token::RBrace)) + .map(UseItems::List); + let single = Identifier::parser().map(UseItems::Single); + let items = choice((list, single)); + + visibility + .then_ignore(just(Token::Use)) + .then(path) + .then(items) + .then_ignore(just(Token::Semi)) + .map_with(|((visibility, path), items), e| Self { + visibility, + path, + items, + span: e.span(), + }) + } +} + impl ChumskyParse for FunctionParam { fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone where @@ -1450,6 +1666,11 @@ impl ChumskyParse for TypeAlias { where I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, { + let visibility = just(Token::Pub) + .to(Visibility::Public) + .or_not() + .map(Option::unwrap_or_default); + let name = AliasName::parser() .validate(|name, e, emit| { let ident = name.as_inner(); @@ -1470,12 +1691,16 @@ impl ChumskyParse for TypeAlias { }) .map_with(|name, e| (name, e.span())); - just(Token::Type) - .ignore_then(name) - .then_ignore(parse_token_with_recovery(Token::Eq)) - .then(AliasedType::parser()) - .then_ignore(just(Token::Semi)) - .map_with(|(name, ty), e| Self { + visibility + .then( + just(Token::Type) + .ignore_then(name) + .then_ignore(parse_token_with_recovery(Token::Eq)) + .then(AliasedType::parser()) + .then_ignore(just(Token::Semi)), + ) + .map_with(|(visibility, (name, ty)), e| Self { + visibility, name: name.0, ty, span: e.span(), @@ -1960,6 +2185,7 @@ impl crate::ArbitraryRec for Function { fn arbitrary_rec(u: &mut arbitrary::Unstructured, budget: usize) -> arbitrary::Result { use arbitrary::Arbitrary; + let visibility = Visibility::arbitrary(u)?; let name = FunctionName::arbitrary(u)?; let len = u.int_in_range(0..=3)?; let params = (0..len) @@ -1968,6 +2194,7 @@ impl crate::ArbitraryRec for Function { let ret = Option::::arbitrary(u)?; let body = Expression::arbitrary_rec(u, budget).map(Expression::into_block)?; Ok(Self { + visibility, name, params, ret, @@ -2175,6 +2402,18 @@ impl crate::ArbitraryRec for Match { mod test { use super::*; + impl UseDecl { + /// Creates a dummy `UseDecl` specifically for testing `DependencyMap` resolution. + pub fn dummy_path(path: Vec) -> Self { + Self { + visibility: Visibility::default(), + path, + items: UseItems::List(Vec::new()), + span: Span::new(0, 0), + } + } + } + #[test] fn test_reject_redefined_builtin_type() { let ty = TypeAlias::parse_from_str("type Ctx8 = u32") @@ -2189,7 +2428,7 @@ mod test { #[test] fn test_double_colon() { let input = "fn main() { let ab: u8 = <(u4, u4)> : :into((0b1011, 0b1101)); }"; - let mut error_handler = ErrorCollector::new(Arc::from(input)); + let mut error_handler = ErrorCollector::new(); let parse_program = Program::parse_from_str_with_errors(input, &mut error_handler); assert!(parse_program.is_none()); @@ -2199,7 +2438,7 @@ mod test { #[test] fn test_double_double_colon() { let input = "fn main() { let pk: Pubkey = witnes::::PK; }"; - let mut error_handler = ErrorCollector::new(Arc::from(input)); + let mut error_handler = ErrorCollector::new(); let parse_program = Program::parse_from_str_with_errors(input, &mut error_handler); assert!(parse_program.is_none()); diff --git a/src/resolution.rs b/src/resolution.rs new file mode 100644 index 00000000..ee310a5c --- /dev/null +++ b/src/resolution.rs @@ -0,0 +1,332 @@ +use std::io; +use std::path::Path; +use std::sync::Arc; + +use crate::error::{Error, RichError, WithSpan as _}; +use crate::parse::UseDecl; + +/// Powers error reporting by mapping compiler diagnostics to the specific file. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct SourceFile { + /// The name or path of the source file (e.g., "./simf/main.simf"). + name: Option>, + /// The actual text content of the source file. + content: Arc, +} + +impl From<(&Path, &str)> for SourceFile { + fn from((name, content): (&Path, &str)) -> Self { + Self { + name: Some(Arc::from(name)), + content: Arc::from(content), + } + } +} + +impl SourceFile { + /// Creates a standard `SourceFile` from a file path and its content. + pub fn new(name: &Path, content: Arc) -> Self { + Self { + name: Some(Arc::from(name)), + content, + } + } + + /// Creates an anonymous `SourceFile` without a file path (e.g., for a single-file programs) + pub fn anonymous(content: Arc) -> Self { + Self { + name: None, + content, + } + } + + pub fn name(&self) -> &Option> { + &self.name + } + + pub fn content(&self) -> Arc { + self.content.clone() + } +} + +/// A guaranteed, fully coanonicalized absolute path. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct CanonPath(Arc); + +impl CanonPath { + /// Safely resolves an absolute path via the OS and wraps it in a `CanonPath`. + /// + /// # Errors + /// + /// Returns a `String` containing the OS error if the path does not exist or + /// cannot be accessed. The caller is expected to map this into a more specific + /// compiler diagnostic (e.g., `RichError`). + pub fn canonicalize(path: &Path) -> Result { + // We use `map_err` here to intercept the generic OS error and enrich + // it with the specific path that failed + let canon_path = std::fs::canonicalize(path).map_err(|err| { + format!( + "Failed to find library target path '{}' :{}", + path.display(), + err + ) + })?; + + Ok(Self(Arc::from(canon_path.as_path()))) + } + + /// Appends a logical module path to this physical root directory and verifies it. + /// It automatically appends the `.simf` extension to the final path *before* asking + /// the OS to verify its existence. + pub fn join(&self, parts: &[&str]) -> Result { + let mut new_path = self.0.to_path_buf(); + + for part in parts { + new_path.push(part); + } + + Self::canonicalize(&new_path.with_extension("simf")) + } + + /// Check if the current file is executing inside the context's directory tree. + /// This prevents a file in `/project_a/` from using a dependency meant for `/project_b/` + pub fn starts_with(&self, path: &CanonPath) -> bool { + self.as_path().starts_with(path.as_path()) + } + + pub fn as_path(&self) -> &Path { + &self.0 + } +} + +/// This defines how a specific dependency root path (e.g. "math") +/// should be resolved to a physical path on the disk, restricted to +/// files executing within the `context_prefix`. +#[derive(Debug, Clone)] +pub struct Remapping { + /// The base directory that owns this dependency mapping. + pub context_prefix: CanonPath, + /// The dependency root path name used in the `use` statement (e.g., "math"). + pub drp_name: String, + /// The physical path this dependency root path points to. + pub target: CanonPath, +} + +/// A router for resolving dependencies across multi-file workspaces. +/// +/// Mappings are strictly sorted by the longest `context_prefix` match. +/// This mathematical guarantee ensures that if multiple nested directories +/// define the same dependency root path, the most specific (deepest) context wins. +#[derive(Debug, Default)] +pub struct DependencyMap { + inner: Vec, +} + +impl DependencyMap { + pub fn new() -> Self { + Self::default() + } + + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + /// Re-sort the vector in descending order so the longest context paths are always at the front. + /// This mathematically guarantees that the first match we find is the most specific. + fn sort_mappings(&mut self) { + self.inner.sort_by(|a, b| { + let len_a = a.context_prefix.as_path().as_os_str().len(); + let len_b = b.context_prefix.as_path().as_os_str().len(); + len_b.cmp(&len_a) + }); + } + + /// Add a dependency mapped to a specific calling file's path prefix. + /// Re-sorts the vector internally to guarantee the Longest Prefix Match. + /// + /// # Arguments + /// + /// * `context` - The physical root directory where this dependency rule applies + /// (e.g., `/workspace/frontend`). + /// * `drp_name` - The Dependency Root Path Name. This is the logical alias the + /// programmer types in their source code (e.g., the `"math"` in `use math::vector;`). + /// * `target` - The physical directory where the compiler should actually + /// look for the code (e.g., `/libs/frontend_math`). + pub fn insert( + &mut self, + context: CanonPath, + drp_name: String, + target: CanonPath, + ) -> io::Result<()> { + self.inner.push(Remapping { + context_prefix: context, + drp_name, + target, + }); + + self.sort_mappings(); + + Ok(()) + } + + /// Resolve `use dependency_root_path_name::...` into a physical file path by finding the + /// most specific library context that owns the current file. + pub fn resolve_path( + &self, + current_file: CanonPath, + use_decl: &UseDecl, + ) -> Result { + let parts = use_decl.path(); + let drp_name = use_decl.drp_name()?; + + // Because the vector is sorted by longest prefix, + // the VERY FIRST match we find is guaranteed to be the correct one. + for remapping in &self.inner { + if !current_file.starts_with(&remapping.context_prefix) { + continue; + } + + // Check if the alias matches what the user typed + if remapping.drp_name == drp_name { + return remapping.target.join(&parts[1..]).map_err(|err| { + RichError::new( + Error::Internal(format!("Dependency resolution failed: {}", err)), + *use_decl.span(), + ) + }); + } + } + + Err(Error::UnknownLibrary(drp_name.to_string())).with_span(*use_decl.span()) + } +} + +#[cfg(test)] +mod tests { + use crate::str::Identifier; + use crate::test_utils::TempWorkspace; + + use super::*; + + /// Helper to easily construct a `UseDecl` for path resolution tests. + fn create_dummy_use_decl(path_segments: &[&str]) -> UseDecl { + let path: Vec = path_segments + .iter() + .map(|&s| Identifier::dummy(s)) + .collect(); + + UseDecl::dummy_path(path) + } + + fn canon(p: &Path) -> CanonPath { + CanonPath::canonicalize(p).unwrap() + } + + /// When a user registers the same library dependency root path multiple times + /// for different folders, the compiler must always check the longest folder path first. + #[test] + fn test_sorting_longest_prefix() { + let ws = TempWorkspace::new("sorting"); + + let workspace_dir = canon(&ws.create_dir("workspace")); + let nested_dir = canon(&ws.create_dir("workspace/project_a/nested")); + let project_a_dir = canon(&ws.create_dir("workspace/project_a")); + + let target_v1 = canon(&ws.create_dir("lib/math_v1")); + let target_v3 = canon(&ws.create_dir("lib/math_v3")); + let target_v2 = canon(&ws.create_dir("lib/math_v2")); + + let mut map = DependencyMap::new(); + map.insert(workspace_dir.clone(), "math".to_string(), target_v1) + .unwrap(); + map.insert(nested_dir.clone(), "math".to_string(), target_v3) + .unwrap(); + map.insert(project_a_dir.clone(), "math".to_string(), target_v2) + .unwrap(); + + // The longest prefixes should bubble to the top + assert_eq!(map.inner[0].context_prefix, nested_dir); + assert_eq!(map.inner[1].context_prefix, project_a_dir); + assert_eq!(map.inner[2].context_prefix, workspace_dir); + } + + /// Projects should not be able to "steal" or accidentally access dependencies + /// that do not belong to them. + #[test] + fn test_context_isolation() { + let ws = TempWorkspace::new("isolation"); + + let project_a = canon(&ws.create_dir("project_a")); + let target_utils = canon(&ws.create_dir("libs/utils_a")); + let current_file = canon(&ws.create_file("project_b/main.simf", "")); + + let mut map = DependencyMap::new(); + map.insert(project_a, "utils".to_string(), target_utils) + .unwrap(); + + let use_decl = create_dummy_use_decl(&["utils"]); + let result = map.resolve_path(current_file, &use_decl); + + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err().error(), + Error::UnknownLibrary(..) + )); + } + + /// It proves that a highly specific path definition will "override" or "shadow" + /// a broader path definition. + #[test] + fn test_resolve_longest_prefix_match() { + let ws = TempWorkspace::new("resolve_prefix"); + + // 1. Setup Global Context + let global_context = canon(&ws.create_dir("workspace")); + let global_target = canon(&ws.create_dir("libs/global_math")); + let global_expected = canon(&ws.create_file("libs/global_math/vector.simf", "")); + + // 2. Setup Frontend Context + let frontend_context = canon(&ws.create_dir("workspace/frontend")); + let frontend_target = canon(&ws.create_dir("libs/frontend_math")); + let frontend_expected = canon(&ws.create_file("libs/frontend_math/vector.simf", "")); + + let mut map = DependencyMap::new(); + map.insert(global_context, "math".to_string(), global_target) + .unwrap(); + map.insert(frontend_context, "math".to_string(), frontend_target) + .unwrap(); + + let use_decl = create_dummy_use_decl(&["math", "vector"]); + + // 3. Test Frontend Override + let frontend_file = canon(&ws.create_file("workspace/frontend/src/main.simf", "")); + let resolved_frontend = map.resolve_path(frontend_file, &use_decl).unwrap(); + assert_eq!(resolved_frontend, frontend_expected); + + // 4. Test Global Fallback + let backend_file = canon(&ws.create_file("workspace/backend/src/main.simf", "")); + let resolved_backend = map.resolve_path(backend_file, &use_decl).unwrap(); + assert_eq!(resolved_backend, global_expected); + } + + /// it proves that `start_with()` and `resolve_path()` logic correctly handles files + /// that are buried deep inside a project's subdirectories. + #[test] + fn test_resolve_relative_current_file_against_canonical_context() { + let ws = TempWorkspace::new("relative_current"); + + let context = canon(&ws.create_dir("workspace/frontend")); + let target = canon(&ws.create_dir("libs/frontend_math")); + let expected = canon(&ws.create_file("libs/frontend_math/vector.simf", "")); + + let current_file = canon(&ws.create_file("workspace/frontend/src/main.simf", "")); + + let mut map = DependencyMap::new(); + map.insert(context, "math".to_string(), target).unwrap(); + + let use_decl = create_dummy_use_decl(&["math", "vector"]); + let result = map.resolve_path(current_file, &use_decl).unwrap(); + + assert_eq!(result, expected); + } +} diff --git a/src/str.rs b/src/str.rs index c06c33dd..8249f0f5 100644 --- a/src/str.rs +++ b/src/str.rs @@ -293,3 +293,14 @@ impl ModuleName { } wrapped_string!(ModuleName, "module name"); + +#[cfg(test)] +mod tests { + use super::*; + + impl Identifier { + pub fn dummy(name: &str) -> Self { + Self(std::sync::Arc::from(name)) + } + } +} diff --git a/src/test_utils.rs b/src/test_utils.rs new file mode 100644 index 00000000..512d0ab9 --- /dev/null +++ b/src/test_utils.rs @@ -0,0 +1,55 @@ +use std::fs; +use std::path::PathBuf; +use std::time; + +/// A self-cleaning temporary workspace for unit tests. +/// Completely replaces the need for the external `tempfile` crate. +pub struct TempWorkspace { + root: PathBuf, +} + +impl TempWorkspace { + /// Generates a mathematically unique temporary directory on the OS + /// and physically creates it. + pub fn new(test_name: &str) -> Self { + let unique = time::SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .unwrap() + .as_nanos(); + let cwd = std::env::current_dir().unwrap(); + let root = cwd.join("target").join(format!( + "test-{}-{}-{}", + test_name, + std::process::id(), + unique + )); + fs::create_dir_all(&root).unwrap(); + Self { root } + } + + /// Helper to physically create a nested directory inside the workspace. + pub fn create_dir(&self, rel_path: &str) -> PathBuf { + let path = self.root.join(rel_path); + fs::create_dir_all(&path).unwrap(); + path + } + + /// Helper to physically create a file (and any necessary parent directories) + /// with string contents. + pub fn create_file(&self, rel_path: &str, contents: &str) -> PathBuf { + let path = self.root.join(rel_path); + + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).unwrap(); + } + + fs::write(&path, contents).unwrap(); + path + } +} + +impl Drop for TempWorkspace { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.root); + } +} diff --git a/src/witness.rs b/src/witness.rs index 6d6ffefc..f0fbd2d1 100644 --- a/src/witness.rs +++ b/src/witness.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::fmt; use std::sync::Arc; -use crate::error::{Error, RichError, WithFile, WithSpan}; +use crate::error::{Error, RichError, WithContent, WithSpan}; use crate::parse; use crate::parse::ParseFromStr; use crate::str::WitnessName; @@ -144,7 +144,7 @@ impl ParseFromStr for ResolvedType { .resolve_builtin() .map_err(Error::UndefinedAlias) .with_span(s) - .with_file(s) + .with_content(s) } }