From 67e5f050a23ca8d84e28ce230aa60f76362d556c Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Fri, 29 May 2026 14:59:31 +0300 Subject: [PATCH 01/10] fix: add `.vscode` directory to `.gitignore` --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index af36ea38..7b2e12af 100644 --- a/.gitignore +++ b/.gitignore @@ -9,10 +9,14 @@ target/ # MSVC Windows builds of rustc generate these, which store debugging information *.pdb -# IntelliJ project files +# IDE project files (for developers) +## IntelliJ .idea *.iml +## VSCode +.vscode + # mdbook HTML output dir book/book/ From 35a21430c053de3ba7e36090e43251affc34adce Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Fri, 29 May 2026 17:08:41 +0300 Subject: [PATCH 02/10] feat: add module parser --- src/ast.rs | 52 ++------- src/driver/resolve_order.rs | 6 +- src/error.rs | 6 + src/parse.rs | 214 +++++++++++++++++++++--------------- src/str.rs | 155 +++++++++++++++----------- 5 files changed, 236 insertions(+), 197 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index fca0947f..87d24f17 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -74,8 +74,10 @@ pub enum Item { TypeAlias, /// A function. Function(Function), - /// A module, which is ignored. + Use, Module, + /// A placeholder used for error recovery during parsing. + Ignored, } /// Definition of a function. @@ -984,7 +986,11 @@ impl AbstractSyntaxTree for Item { Error::UseKeywordIsNotSupported, *use_decl.span(), )), - parse::Item::Module => Ok(Self::Module), + parse::Item::Module(module) => Err(RichError::new( + Error::ModuleKeywordIsNotSupported, + *module.span(), + )), + parse::Item::Ignored => Ok(Self::Ignored), }; scope.file_id = previous_file_id; @@ -1663,48 +1669,6 @@ impl AbstractSyntaxTree for Match { } } -impl AbstractSyntaxTree for Module { - type From = parse::Module; - - fn analyze(from: &Self::From, ty: &ResolvedType, scope: &mut Scope) -> Result { - assert!(ty.is_unit(), "Modules cannot return anything"); - assert!(scope.is_topmost(), "Modules live in the topmost scope only"); - let assignments = from - .assignments() - .iter() - .map(|s| ModuleAssignment::analyze(s, ty, scope)) - .collect::, RichError>>()?; - debug_assert!(scope.is_topmost()); - - Ok(Self { - name: from.name().shallow_clone(), - span: *from.as_ref(), - assignments, - }) - } -} - -impl AbstractSyntaxTree for ModuleAssignment { - type From = parse::ModuleAssignment; - - fn analyze(from: &Self::From, ty: &ResolvedType, scope: &mut Scope) -> Result { - assert!(ty.is_unit(), "Assignments cannot return anything"); - let ty_expr = scope.resolve(from.ty()).with_span(from)?; - let expression = Expression::analyze(from.expression(), &ty_expr, scope)?; - let value = Value::from_const_expr(&expression) - .ok_or(Error::ExpressionUnexpectedType { - ty: ty_expr.clone(), - }) - .with_span(from.expression())?; - - Ok(Self { - name: from.name().clone(), - value, - span: *from.as_ref(), - }) - } -} - impl AsRef for Assignment { fn as_ref(&self) -> &Span { &self.span diff --git a/src/driver/resolve_order.rs b/src/driver/resolve_order.rs index ad5463d5..efc951bd 100644 --- a/src/driver/resolve_order.rs +++ b/src/driver/resolve_order.rs @@ -68,7 +68,8 @@ impl Program { } // Safe to skip: `Use` items are handled earlier in the loop, and `Module` currently has no functionality. - parse::Item::Module | parse::Item::Use(_) => continue, + // It will handle properly in the following commits. + parse::Item::Module(_) | parse::Item::Use(_) | parse::Item::Ignored => continue, } items.push(new_elem); } @@ -238,7 +239,8 @@ impl DependencyGraph { } // Safe to skip: `Use` items are handled earlier in the loop, and `Module` currently has no functionality. - parse::Item::Module | parse::Item::Use(_) => continue, + // It will handle properly in the following commits. + parse::Item::Module(_) | parse::Item::Use(_) | parse::Item::Ignored => continue, } items.push(new_elem); } diff --git a/src/error.rs b/src/error.rs index 83420973..6998525d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -633,7 +633,9 @@ pub enum Error { declared: ResolvedType, assigned: ResolvedType, }, + // TODO: Remove these once `use` and `mod` are supported by the AST UseKeywordIsNotSupported, + ModuleKeywordIsNotSupported, } #[rustfmt::skip] @@ -860,6 +862,10 @@ impl fmt::Display for Error { f, "The `use` keyword is not supported yet" ), + Error::ModuleKeywordIsNotSupported => write!( + f, + "The `mod` keyword is not supported yet" + ), } } } diff --git a/src/parse.rs b/src/parse.rs index 42b5dac2..62c21451 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -49,7 +49,6 @@ impl_eq_hash!(Program; items); /// An item is a component of a program. #[derive(Clone, Debug, Eq, PartialEq, Hash)] -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub enum Item { /// A type alias. TypeAlias(TypeAlias), @@ -58,8 +57,13 @@ pub enum Item { /// An import declaration (e.g., `use math::add`) that brings another /// [`Item`] into the current scope. Use(UseDecl), - /// A module, which is ignored. - Module, + /// A module containing a collection of nested [`Item`]. + Module(Module), + /// A placeholder used exclusively for error recovery during parsing. + /// + /// When the parser encounters a syntax error, it skips the malformed tokens + /// until it reaches a valid top-level keyword and inserts `Ignored` into the AST. + Ignored, } #[derive(Clone, Debug, PartialEq, Eq, Hash, Default)] @@ -621,20 +625,26 @@ impl MatchPattern { #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct Module { + file_id: usize, + visibility: Visibility, name: ModuleName, - assignments: Arc<[ModuleAssignment]>, + items: Arc<[Item]>, span: Span, } impl Module { + pub fn visibility(&self) -> &Visibility { + &self.visibility + } + /// Access the name of the module. pub fn name(&self) -> &ModuleName { &self.name } /// Access the assignments of the module. - pub fn assignments(&self) -> &[ModuleAssignment] { - &self.assignments + pub fn items(&self) -> &[Item] { + &self.items } /// Access the span of the module. @@ -643,31 +653,6 @@ impl Module { } } -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub struct ModuleAssignment { - name: WitnessName, - ty: AliasedType, - expression: Expression, - span: Span, -} - -impl ModuleAssignment { - /// Access the assigned witness name. - pub fn name(&self) -> &WitnessName { - &self.name - } - - /// Access the assigned witness type. - pub fn ty(&self) -> &AliasedType { - &self.ty - } - - /// Access the assigned witness expression. - pub fn expression(&self) -> &Expression { - &self.expression - } -} - impl fmt::Display for Program { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for item in self.items() { @@ -683,10 +668,8 @@ impl fmt::Display for Item { 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`. - Self::Module => write!(f, "mod witness {{}}"), + Self::Module(module) => write!(f, "{module}"), + Self::Ignored => Ok(()), } } } @@ -729,6 +712,12 @@ impl fmt::Display for Function { } } +impl fmt::Display for FunctionParam { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: {}", self.identifier(), self.ty()) + } +} + impl fmt::Display for UseDecl { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let _ = write!(f, "{}use ", self.visibility()); @@ -778,9 +767,19 @@ impl fmt::Display for UseItems { } } -impl fmt::Display for FunctionParam { +impl fmt::Display for Module { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}: {}", self.identifier(), self.ty()) + writeln!(f, "{}mod {} {{", self.visibility(), self.name())?; + + for item in self.items() { + let item_str = item.to_string(); + + for line in item_str.lines() { + writeln!(f, " {line}")?; + } + } + + write!(f, "}}") } } @@ -847,6 +846,8 @@ impl TreeLike for ExprTree<'_> { } } +// TODO: Fix indentation and formatting logic. The current flat iterator approach cannot +// track AST depth, causing incorrect indentation for nested `Block` and `Match` nodes. impl fmt::Display for ExprTree<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use SingleExpressionInner as S; @@ -1329,6 +1330,12 @@ impl ChumskyParse for AliasedType { } impl ChumskyParse for Program { + /// Parses a sequence of top-level [`Item`]s into a complete [`Program`]. + /// + /// If an invalid item is encountered, it will safely skip the broken tokens + /// until it finds a synchronization point. This prevents the parser from + /// failing completely, allowing it to report multiple syntax errors across the file + /// while substituting the unparseable sections with [`Item::Ignored`]. fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone where I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, @@ -1344,8 +1351,7 @@ impl ChumskyParse for Program { }) .repeated(), ) - // map to empty module - .map_with(|_, _| Item::Module); + .map_with(|_, _| Item::Ignored); Item::parser() .recover_with(via_parser(skip_until_next_item)) @@ -1363,12 +1369,16 @@ impl ChumskyParse for Item { where I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, { - 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); + recursive(|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); - choice((func_parser, use_parser, type_parser, mod_parser)) + // Lazy item here + let mod_parser = Module::parser_with_items(item).map(Item::Module); + + choice((func_parser, use_parser, type_parser, mod_parser)) + }) } } @@ -1442,21 +1452,23 @@ impl ChumskyParse for UseDecl { .or_not() .map(Option::unwrap_or_default); - // Parse the base path prefix (e.g., `dependency_root_path::file::`, `dependency_root_path::dir::file::`, or `crate::dir::file::`). - // We require at least 2 segments here because a valid import needs a minimum - // of 3 items total: the dependency root path (or `crate`), the file, and the specific item/function. let first_segment = select! { Token::Ident(ident) => Identifier::from_str_unchecked(ident), Token::Crate => Identifier::from_str_unchecked(CRATE_STR), }; + // Parse the base path prefix (e.g., `dependency_root_path::file::`, `dependency_root_path::dir::file::`, + // or `crate::dir::file::`). We require at least 2 segments here because a valid import needs a minimum + // of 3 items total: the dependency root path (or `crate`), the file, and the specific item. + // + // With the introduction of `mod` keyword and single-file flattening, 2 total segments are now + // valid: `crate::item`, where `crate` is the program root. let path = first_segment .then_ignore(just(Token::DoubleColon)) .then( Identifier::parser() .then_ignore(just(Token::DoubleColon)) .repeated() - .at_least(1) .collect::>(), ) .map(|(first, mut rest)| { @@ -2082,14 +2094,22 @@ impl Match { } } -impl ChumskyParse for Module { - fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone +impl Module { + pub fn parser_with_items<'tokens, 'src: 'tokens, I>( + item_parser: impl Parser<'tokens, I, Item, ParseError<'src>> + Clone + 'tokens, + ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone where I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, { + let file_id = MAIN_MODULE; + let visibility = just(Token::Pub) + .to(Visibility::Public) + .or_not() + .map(Option::unwrap_or_default); + let name = ModuleName::parser().map_with(|name, e| (name, e.span())); - let assignments = ModuleAssignment::parser() + let items = item_parser .repeated() .collect::>() .delimited_by(just(Token::LBrace), just(Token::RBrace)) @@ -2104,35 +2124,13 @@ impl ChumskyParse for Module { ))) .map(Arc::from); - just(Token::Mod) - .ignore_then(name) - .then(assignments) - .map_with(|(name, assignments), e| Self { + visibility + .then(just(Token::Mod).ignore_then(name).then(items)) + .map_with(move |(visibility, (name, items)), e| Self { + file_id, + visibility, name: name.0, - assignments, - span: e.span(), - }) - } -} - -impl ChumskyParse for ModuleAssignment { - fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone - where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - { - let name = WitnessName::parser(); - - just(Token::Const) - .ignore_then(name) - .then_ignore(just(Token::Colon)) - .then(AliasedType::parser()) - .then_ignore(just(Token::Eq)) - .then(Expression::parser()) - .then_ignore(just(Token::Semi)) - .map_with(|((name, ty), expression), e| Self { - name, - ty, - expression, + items, span: e.span(), }) } @@ -2192,26 +2190,37 @@ impl AsRef for Match { } } -impl AsRef for Module { +impl AsRef for UseDecl { fn as_ref(&self) -> &Span { &self.span } } -impl AsRef for ModuleAssignment { +impl AsRef for Module { fn as_ref(&self) -> &Span { &self.span } } +#[cfg(feature = "arbitrary")] +pub(crate) fn generate_arbitrary_items<'a>( + u: &mut arbitrary::Unstructured<'a>, +) -> arbitrary::Result> { + let mut items_vec = Vec::new(); + + let len = u.int_in_range(0..=2)?; + for _ in 0..len { + items_vec.push(::arbitrary(u)?); + } + + Ok(items_vec) +} + #[cfg(feature = "arbitrary")] impl<'a> arbitrary::Arbitrary<'a> for Program { fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { - let mut items_vec: Vec = Vec::new(); - let len = u.int_in_range(0..=2)?; - for _ in 0..len { - items_vec.push(Item::arbitrary(u)?); - } + let mut items_vec = generate_arbitrary_items(u)?; + // Three equally-likely modes for how `fn main()` is injected: // 0 — no explicit main (arbitrary items only) // 1 — main with arbitrary params and return type @@ -2240,6 +2249,19 @@ impl<'a> arbitrary::Arbitrary<'a> for Program { } } +#[cfg(feature = "arbitrary")] +impl<'a> arbitrary::Arbitrary<'a> for Item { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + match u.int_in_range(0..=3)? { + 0 => Ok(Item::TypeAlias(TypeAlias::arbitrary(u)?)), + 1 => Ok(Item::Function(Function::arbitrary(u)?)), + 2 => Ok(Item::Use(UseDecl::arbitrary(u)?)), + 3 => Ok(Item::Module(Module::arbitrary(u)?)), + _ => unreachable!(), + } + } +} + #[cfg(feature = "arbitrary")] impl<'a> arbitrary::Arbitrary<'a> for Function { fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { @@ -2273,6 +2295,26 @@ impl crate::ArbitraryRec for Function { } } +#[cfg(feature = "arbitrary")] +impl<'a> arbitrary::Arbitrary<'a> for Module { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + // A small range allows us to test scenarios where two + // randomly generated modules end up in the same file. + let file_id = u.int_in_range(0..=3)?; + let visibility = Visibility::arbitrary(u)?; + let name = ModuleName::arbitrary(u)?; + let items_vec = generate_arbitrary_items(u)?; + + Ok(Self { + file_id, + visibility, + name, + items: items_vec.into(), + span: Span::DUMMY, + }) + } +} + #[cfg(feature = "arbitrary")] impl crate::ArbitraryRec for Expression { fn arbitrary_rec(u: &mut arbitrary::Unstructured, budget: usize) -> arbitrary::Result { diff --git a/src/str.rs b/src/str.rs index 19245c83..c51eb1fd 100644 --- a/src/str.rs +++ b/src/str.rs @@ -96,30 +96,84 @@ impl From for FunctionName { } } +pub(crate) const FUNCTION_RESERVED: &[&str] = &[ + "unwrap_left", + "unwrap_right", + "for_while", + "is_none", + "unwrap", + "assert", + "panic", + "match", + "into", + "fold", + "dbg", +]; + +pub(crate) const ALIAS_RESERVED: &[&str] = &[ + "Either", + "Option", + "bool", + "List", + "u128", + "u256", + "u16", + "u32", + "u64", + "u1", + "u2", + "u4", + "u8", + "Ctx8", + "Pubkey", + "Message64", + "Message", + "Signature", + "Scalar", + "Fe", + "Gej", + "Ge", + "Point", + "Height", + "Time", + "Distance", + "Duration", + "Lock", + "Outpoint", + "Confidential1", + "ExplicitAsset", + "Asset1", + "ExplicitAmount", + "Amount1", + "ExplicitNonce", + "Nonce", + "TokenAmount1", +]; + +pub(crate) const MODULE_RESERVED: &[&str] = &["jet", "witness", "param"]; + +pub fn is_reserved_function_name(name: &str) -> bool { + FUNCTION_RESERVED.contains(&name) || crate::lexer::is_keyword(name) +} + +pub fn is_reserved_alias_name(name: &str) -> bool { + ALIAS_RESERVED.contains(&name) || crate::lexer::is_keyword(name) +} + +pub fn is_reserved_module_name(name: &str) -> bool { + MODULE_RESERVED.contains(&name) || crate::lexer::is_keyword(name) +} + #[cfg(feature = "arbitrary")] impl<'a> arbitrary::Arbitrary<'a> for FunctionName { fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { - const RESERVED_NAMES: [&str; 11] = [ - "unwrap_left", - "unwrap_right", - "for_while", - "is_none", - "unwrap", - "assert", - "panic", - "match", - "into", - "fold", - "dbg", - ]; - let len = u.int_in_range(1..=10)?; let mut string = String::with_capacity(len); for _ in 0..len { let offset = u.int_in_range(0..=25)?; string.push((b'a' + offset) as char) } - if RESERVED_NAMES.contains(&string.as_str()) || crate::lexer::is_keyword(string.as_str()) { + if is_reserved_function_name(string.as_str()) { string.push('_'); } @@ -184,53 +238,13 @@ impl From for AliasName { #[cfg(feature = "arbitrary")] impl<'a> arbitrary::Arbitrary<'a> for AliasName { fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { - const RESERVED_NAMES: [&str; 37] = [ - "Either", - "Option", - "bool", - "List", - "u128", - "u256", - "u16", - "u32", - "u64", - "u1", - "u2", - "u4", - "u8", - "Ctx8", - "Pubkey", - "Message64", - "Message", - "Signature", - "Scalar", - "Fe", - "Gej", - "Ge", - "Point", - "Height", - "Time", - "Distance", - "Duration", - "Lock", - "Outpoint", - "Confidential1", - "ExplicitAsset", - "Asset1", - "ExplicitAmount", - "Amount1", - "ExplicitNonce", - "Nonce", - "TokenAmount1", - ]; - let len = u.int_in_range(1..=10)?; let mut string = String::with_capacity(len); for _ in 0..len { let offset = u.int_in_range(0..=25)?; string.push((b'a' + offset) as char) } - if RESERVED_NAMES.contains(&string.as_str()) || crate::lexer::is_keyword(string.as_str()) { + if is_reserved_alias_name(string.as_str()) { string.push('_'); } @@ -305,15 +319,9 @@ impl<'a> arbitrary::Arbitrary<'a> for Hexadecimal { #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] pub struct ModuleName(Arc); -impl ModuleName { - /// Return the name of the witness module. - pub fn witness() -> Self { - Self(Arc::from("witness")) - } - - /// Return the name of the parameter module. - pub fn param() -> Self { - Self(Arc::from("param")) +impl Default for ModuleName { + fn default() -> Self { + Self(Arc::from("")) } } @@ -325,6 +333,23 @@ impl From for ModuleName { wrapped_string!(ModuleName, "module name"); +#[cfg(feature = "arbitrary")] +impl<'a> arbitrary::Arbitrary<'a> for ModuleName { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let len = u.int_in_range(1..=10)?; + let mut string = String::with_capacity(len); + for _ in 0..len { + let offset = u.int_in_range(0..=25)?; + string.push((b'a' + offset) as char) + } + if is_reserved_module_name(string.as_str()) { + string.push('_'); + } + + Ok(Self::from_str_unchecked(string.as_str())) + } +} + /// An unresolved identifier parsed from the source code. /// /// During the parsing of `use` statements, the exact kind of the imported From cf59561de84935bfa080801879f5be160ec0a703 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Fri, 29 May 2026 17:35:41 +0300 Subject: [PATCH 03/10] feat: add module errors --- src/error.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/error.rs b/src/error.rs index 6998525d..63bdab19 100644 --- a/src/error.rs +++ b/src/error.rs @@ -12,6 +12,7 @@ use chumsky::DefaultExpected; use itertools::Itertools; +use crate::driver::CRATE_STR; use crate::lexer::Token; use crate::parse::MatchPattern; use crate::source::SourceFile; @@ -559,6 +560,7 @@ pub enum Error { PrivateItem { name: String, }, + MissingCrateKeyword, MainNoInputs, MainNoOutput, MainRequired, @@ -625,6 +627,12 @@ pub enum Error { ModuleRedefined { name: ModuleName, }, + ModuleNotFound { + name: ModuleName, + }, + ModuleIsPrivate { + name: ModuleName, + }, ArgumentMissing { name: WitnessName, }, @@ -734,6 +742,10 @@ impl fmt::Display for Error { f, "Cannot cast values of type `{source}` as values of type `{target}`" ), + Error::MissingCrateKeyword => write!( + f, + "Imports must begin with the `{CRATE_STR}` keyword in single-file programs", + ), Error::MainNoInputs => write!( f, "Main function takes no input parameters" @@ -848,7 +860,15 @@ impl fmt::Display for Error { ), Error::ModuleRedefined { name } => write!( f, - "Module `{name}` is defined twice" + "Module `{name}` was defined multiple times" + ), + Error::ModuleNotFound { name } => write!( + f, + "Module `{name}` not found" + ), + Error::ModuleIsPrivate { name } => write!( + f, + "Module `{name}` is private", ), Error::ArgumentMissing { name } => write!( f, From 4de6bc7b773aa96befa9d648d778f1c8c80d1844 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Fri, 29 May 2026 19:05:44 +0300 Subject: [PATCH 04/10] feat: change resolution strategy to resolve `use` statements --- src/lib.rs | 1 + src/parse.rs | 8 +- src/resolution.rs | 186 +++++++++++++++++++++++++++++----------------- 3 files changed, 121 insertions(+), 74 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 707ce622..8ab0d1eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1430,6 +1430,7 @@ mod functional_tests { } #[test] + #[ignore = "TODO: Enable this once module resolution is complete"] #[should_panic(expected = "not found")] fn crate_file_not_found_error() { run_multidep_test( diff --git a/src/parse.rs b/src/parse.rs index 62c21451..9390df7b 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -109,12 +109,12 @@ impl UseDecl { /// /// 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() + pub fn path(&self) -> &[Identifier] { + &self.path } pub fn str_path(&self) -> String { - let path: PathBuf = self.path.iter().map(|s| s.as_inner()).collect(); + let path: PathBuf = self.path().iter().map(|iden| iden.as_inner()).collect(); path.display().to_string() } @@ -124,7 +124,7 @@ impl UseDecl { /// /// Returns a `RichError` if the use declaration path is completely empty. pub fn drp_name(&self) -> Result<&str, RichError> { - let parts = self.path(); + let parts: Vec<&str> = self.path().iter().map(|iden| iden.as_inner()).collect(); parts.first().copied().ok_or_else(|| { Error::CannotParse { msg: "Empty use path".to_string(), diff --git a/src/resolution.rs b/src/resolution.rs index eea71223..0c54bc60 100644 --- a/src/resolution.rs +++ b/src/resolution.rs @@ -2,6 +2,7 @@ use crate::driver::CRATE_STR; use crate::error::{Error, RichError, WithSpan as _}; use crate::parse::UseDecl; use crate::source::CanonPath; +use crate::str::Identifier; /// This defines how a specific dependency root path (e.g. "math") /// should be resolved to a physical path on the disk, restricted to @@ -160,6 +161,28 @@ impl DependencyMapBuilder { } } +/// Represents a fully resolved `use` declaration, split into two parts: +/// the physical file on disk and the remaining inline path within it. +/// +/// # Example +/// +/// ``` md +/// use drp_name::dir1::dir2::simf_file::first_mod::item; +/// // |______________________________| |______________| +/// // path mod_path +/// ``` +#[derive(Debug, Clone)] +pub(crate) struct ResolvedUse { + /// The resolved `.simf` file this `use` points to. + /// Represents the root `crate` directory if this is the root file. + pub(crate) path: CanonPath, + + /// Path segments after the file boundary — inline `mod` names and the final item. + /// Empty if the `use` points directly to a file-level item. + #[allow(dead_code)] + pub(crate) mod_path: Vec, +} + impl DependencyMap { /// 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. @@ -189,30 +212,35 @@ impl DependencyMap { current_file: &CanonPath, use_decl: &UseDecl, ) -> Result { - let parts = use_decl.path(); + Ok(self.resolve_path_internal(current_file, use_decl)?.path) + } + + pub(crate) fn resolve_path_internal( + &self, + current_file: &CanonPath, + use_decl: &UseDecl, + ) -> Result { let drp_name = use_decl.drp_name()?; + let span = *use_decl.span(); if drp_name == CRATE_STR { - return self.resolve_crate_path(current_file, use_decl, &parts); + return self.resolve_crate_path(current_file, use_decl); } // 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.remappings { - 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 self.resolve_external_path(remapping, current_file, use_decl, &parts); - } - } - - Err(Error::UnknownLibrary { - name: drp_name.to_string(), - }) - .with_span(*use_decl.span()) + self.remappings + .iter() + .find(|r| current_file.starts_with(&r.context_prefix) && r.drp_name == drp_name) + .ok_or_else(|| { + RichError::new( + Error::UnknownLibrary { + name: drp_name.to_string(), + }, + span, + ) + }) + .and_then(|remapping| self.resolve_external_path(remapping, current_file, use_decl)) } fn resolve_external_path( @@ -220,12 +248,12 @@ impl DependencyMap { remapping: &Remapping, current_file: &CanonPath, use_decl: &UseDecl, - parts: &[&str], - ) -> Result { + ) -> Result { let drp_name = use_decl.drp_name()?; + let parts_without_drp_name = &use_decl.path()[1..]; - let resolved = - Self::build_and_verify_path(&remapping.target, &parts[1..]).map_err(|failed_path| { + let resolved = Self::build_and_verify_path(&remapping.target, parts_without_drp_name) + .map_err(|failed_path| { RichError::new( Error::ExternalFileNotFound { lib: drp_name.to_string(), @@ -235,28 +263,19 @@ impl DependencyMap { ) })?; - if !resolved.starts_with(&remapping.target) { - return Err(RichError::new( - Error::ExternalFileNotFound { - lib: drp_name.to_string(), - filename: resolved.as_path().to_path_buf(), - }, - *use_decl.span(), - )); - } - - self.check_local_file_imported_as_external(current_file, &resolved, use_decl)?; - + self.check_local_file_imported_as_external(current_file, &resolved.path, use_decl.span())?; Ok(resolved) } /// Resolves `crate::...` imports into a physical file path. + /// + /// Attempts physical file resolution first. If that fails and the current file + /// is at the package root, it falls back to resolving inline items from the main scope. fn resolve_crate_path( &self, current_file: &CanonPath, use_decl: &UseDecl, - parts: &[&str], - ) -> Result { + ) -> Result { let root = self .get_package_root(current_file) .ok_or_else(|| Error::Internal { @@ -264,25 +283,27 @@ impl DependencyMap { }) .map_err(|e| RichError::new(e, *use_decl.span()))?; - let resolved = Self::build_and_verify_path(root, &parts[1..]).map_err(|failed_path| { - RichError::new( - Error::FileNotFound { - filename: failed_path, - }, - *use_decl.span(), - ) - })?; - - if !resolved.starts_with(root) { - return Err(RichError::new( - Error::FileNotFound { - filename: resolved.as_path().to_path_buf(), - }, - *use_decl.span(), - )); + let parts_without_drp_name = &use_decl.path()[1..]; + let failed_path = match Self::build_and_verify_path(root, parts_without_drp_name) { + Ok(resolved) => return Ok(resolved), + Err(path) => path, + }; + + // Fallback: Check if the current file sits directly inside the root directory. + let is_in_root_dir = current_file.as_path().parent() == Some(root.as_path()); + if is_in_root_dir { + return Ok(ResolvedUse { + path: current_file.clone(), + mod_path: parts_without_drp_name.to_vec(), + }); } - Ok(resolved) + Err(RichError::new( + Error::FileNotFound { + filename: failed_path, + }, + *use_decl.span(), + )) } /// Enforces that a local file is imported via `crate::` and not via an external alias. @@ -290,38 +311,63 @@ impl DependencyMap { &self, current_file: &CanonPath, resolved: &CanonPath, - use_decl: &UseDecl, + use_decl_span: &crate::error::Span, ) -> Result<(), RichError> { - let current_crate = self.get_package_root(current_file); - let resolved_crate = self.get_package_root(resolved); - - if let (Some(curr), Some(res)) = (current_crate, resolved_crate) { + if let (Some(curr), Some(res)) = ( + self.get_package_root(current_file), + self.get_package_root(resolved), + ) { if curr == res { return Err(Error::LocalFileImportedAsExternal { path: resolved.as_path().to_path_buf(), }) - .with_span(*use_decl.span()); + .with_span(*use_decl_span); } } - Ok(()) } - /// Replace `.join` method to better error handling + /// Walks `module_parts` greedily. Directories first, then the first matching `.simf` file. + /// Remaining segments after the file boundary are collected as inline `mod_path`. + /// + /// # Errors + /// + /// Returns the failed candidate path as a raw `PathBuf`, without any additional context. + /// The caller is responsible for enriching this into a [`RichError`] with the appropriate + /// span, library name, and any other diagnostic information. fn build_and_verify_path( base_target: &CanonPath, - module_parts: &[impl ToString], - ) -> Result { - let mut theoretical_path = base_target.as_path().to_path_buf(); - for part in module_parts { - theoretical_path.push(part.to_string()); - } - theoretical_path.set_extension("simf"); + module_parts: &[Identifier], + ) -> Result { + let mut path = base_target.as_path().to_path_buf(); + + let mut iter = module_parts.iter(); + + while let Some(part) = iter.next() { + let joined = path.join(part.as_inner()); + if joined.is_dir() { + path = joined; + continue; + } - match CanonPath::canonicalize(&theoretical_path) { - Ok(valid_canon_path) => Ok(valid_canon_path), - Err(_) => Err(theoretical_path), + let mut file_candidate = joined; + file_candidate.set_extension("simf"); + + // Error context is intentionally dropped here. Callers enrich it with span, lib name, etc. + let resolved = + CanonPath::canonicalize(&file_candidate).map_err(|_| file_candidate.clone())?; + + if !resolved.starts_with(base_target) { + return Err(file_candidate); + } + + return Ok(ResolvedUse { + path: resolved, + mod_path: iter.cloned().collect(), // Add only remaining elements + }); } + + Err(path) } } From e1d9806cf95e40eea71ea181bdba467e2d9f542c Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Mon, 1 Jun 2026 17:23:04 +0300 Subject: [PATCH 05/10] feat: add new strategy to resolve imports and rename some variables --- src/driver/linearization.rs | 5 ++ src/driver/mod.rs | 175 ++++++++++++++++++++++++++---------- src/driver/resolve_order.rs | 4 +- 3 files changed, 134 insertions(+), 50 deletions(-) diff --git a/src/driver/linearization.rs b/src/driver/linearization.rs index 7e6dc413..d6ef591c 100644 --- a/src/driver/linearization.rs +++ b/src/driver/linearization.rs @@ -58,6 +58,11 @@ impl DependencyGraph { .map_or(&[] as &[usize], |v| v.as_slice()); for &parent in parents { + // Ignore self-imports. Since the program is sequential and nodes are loaded + // sequentially during `AST::analyze`, forward declarations cannot be loaded at all. + if parent == module { + continue; + } self.dfs_linearize(parent, visited, visiting, order)?; } diff --git a/src/driver/mod.rs b/src/driver/mod.rs index 0f026f84..5967623e 100644 --- a/src/driver/mod.rs +++ b/src/driver/mod.rs @@ -38,7 +38,7 @@ use chumsky::container::Container; use crate::error::{Error, ErrorCollector, RichError, Span}; use crate::parse::{self, ParseFromStrWithErrors}; -use crate::resolution::DependencyMap; +use crate::resolution::{DependencyMap, ResolvedUse}; use crate::source::{CanonPath, CanonSourceFile}; pub use crate::driver::resolve_order::{FileScoped, Program, SymbolTable}; @@ -55,11 +55,11 @@ pub(crate) const MAIN_MODULE: usize = 0; /// Represents a single, isolated file in the SimplicityHL project. /// In this architecture, a file and a module are the exact same thing. #[derive(Debug, Clone)] -struct Module { +struct SourceModule { source: CanonSourceFile, /// The completely parsed program for this specific file. /// it contains all the functions, aliases, and imports defined inside the file. - parsed_program: parse::Program, + program: parse::Program, } /// An Intermediate Representation that helps transform isolated files into a global program. @@ -85,7 +85,7 @@ pub(crate) struct DependencyGraph { /// lifetimes or performance-heavy reference counting (`Rc>`). /// /// Using a flat `Vec` as a memory arena is the idiomatic Rust solution. - modules: Vec, + modules: Vec, /// The configuration environment. /// Used to resolve external library dependencies and invoke their associated functions. @@ -97,10 +97,18 @@ pub(crate) struct DependencyGraph { lookup: HashMap, /// Fast lookup: Module ID -> `CanonPath`. + /// /// A direct index mapping internal IDs back to their absolute file paths. /// This serves as the exact inverse of the `lookup` map. + /// + /// This is highly useful for error reporting and diagnostics. paths: Vec, + /// Memoized results of [`crate::resolution::DependencyMap::resolve_path`] to avoid + /// resolving the same [`parse::UseDecl`] twice — once during the driver phase + /// and once during the building phase after linearization. + use_cache: HashMap, + // TODO: Consider to optimising this with `Vec` instead of `HashMap` /// The Adjacency List: Defines the Directed acyclic Graph (DAG) of imports. /// @@ -141,24 +149,26 @@ impl DependencyGraph { handler: &mut ErrorCollector, ) -> Result, String> { let mut graph = Self { - modules: vec![Module { + modules: vec![SourceModule { source: root_source.clone(), - parsed_program: root_program.clone(), + program: root_program.clone(), }], dependency_map, lookup: HashMap::new(), paths: vec![root_source.name().clone()], + use_cache: HashMap::new(), dependencies: HashMap::new(), }; graph.lookup.insert(root_source.name().clone(), MAIN_MODULE); graph.dependencies.insert(MAIN_MODULE, Vec::new()); + let mut use_cache = HashMap::new(); let mut queue = VecDeque::new(); queue.push_back(MAIN_MODULE); // Prevent errors in the checked files from being doubled in the `load_and_parse_dependencies` function. - let mut inalid_imports = HashSet::new(); + let mut invalid_imports = HashSet::new(); while let Some(curr_id) = queue.pop_front() { let Some(current_module) = graph.modules.get(curr_id) else { @@ -171,25 +181,28 @@ impl DependencyGraph { // We need this to report errors inside THIS file. let importer_source = current_module.source.clone(); - // PHASE 1: Immutably read from the graph let valid_imports = Self::resolve_imports( - ¤t_module.parsed_program, + ¤t_module.program, &importer_source, &graph.dependency_map, + &mut use_cache, handler, ); - // PHASE 2: Mutate the graph - graph.load_and_parse_dependencies( - curr_id, - valid_imports, - &mut inalid_imports, - &importer_source, + let mut ctx = LoadContext { + invalid_imports: &mut invalid_imports, handler, - &mut queue, - ); + queue: &mut queue, + }; + let current = CurrentModule { + id: curr_id, + source: &importer_source, + }; + graph.load_and_parse_dependencies(¤t, valid_imports, &mut ctx); } + graph.use_cache = use_cache; + // TODO: Consider getting rid of the 'String' error here and changing it to a more appropriate error // (e.g. 'Result') after resolving https://github.com/BlockstreamResearch/SimplicityHL/issues/270. Ok((!handler.has_errors()).then_some(graph)) @@ -199,12 +212,12 @@ impl DependencyGraph { /// into an `parse::Program`, and combining them so the compiler can easily work with the file. /// If the file is missing or contains syntax errors, it logs the diagnostic to the /// `ErrorCollector` and safely returns `None`. - fn parse_and_get_program( + fn parse_and_get_source_module( path: &CanonPath, importer_source: &CanonSourceFile, span: Span, handler: &mut ErrorCollector, - ) -> Option { + ) -> Option { let Ok(content) = std::fs::read_to_string(path.as_path()) else { let err = RichError::new( Error::FileNotFound { @@ -227,55 +240,52 @@ impl DependencyGraph { handler.extend_with_handler(source, &error_handler); None } else { - ast.map(|parsed_program| Module { - source, - parsed_program, - }) + ast.map(|program| SourceModule { source, program }) } } - /// PHASE 1 OF GRAPH CONSTRUCTION: Resolves all imports inside a single `parse::Program`. - /// Note: This is a specialized helper function designed exclusively for the `DependencyGraph::new()` constructor. + /// PHASE 1 OF GRAPH CONSTRUCTION: Resolves all `use` declarations within a single + /// [`parse::Program`], recursively walking into inline `mod` blocks. + /// + /// Results are cached in `use_cache` to avoid redundant filesystem lookups during + /// later construction phases. + /// Note: This is a specialized helper designed exclusively for [`DependencyGraph::new()`]. fn resolve_imports( current_program: &parse::Program, importer_source: &CanonSourceFile, dependency_map: &DependencyMap, + use_cache: &mut HashMap, handler: &mut ErrorCollector, ) -> Vec<(CanonPath, Span)> { - let mut valid_imports = Vec::new(); - - for elem in current_program.items() { - let parse::Item::Use(use_decl) = elem else { - continue; - }; + let mut ctx = ImportContext { + importer_source, + dependency_map, + use_cache, + handler, + }; - match dependency_map.resolve_path(importer_source.name(), use_decl) { - Ok(path) => valid_imports.push((path, *use_decl.span())), - Err(err) => handler.push(err.with_source(importer_source.clone())), - } + let mut valid_imports = Vec::new(); + for item in current_program.items() { + ctx.process_item(item, &mut valid_imports); } - valid_imports } /// PHASE 2 OF GRAPH CONSTRUCTION: Loads, parses, and registers new dependencies. - /// Note: This is a specialized helper function designed exclusively for the `DependencyGraph::new()` constructor. + /// Note: This is a specialized helper designed exclusively for [`DependencyGraph::new()`]. fn load_and_parse_dependencies( &mut self, - curr_id: usize, + current: &CurrentModule, valid_imports: Vec<(CanonPath, Span)>, - inalid_imports: &mut HashSet, - importer_source: &CanonSourceFile, - handler: &mut ErrorCollector, - queue: &mut VecDeque, + ctx: &mut LoadContext, ) { for (path, import_span) in valid_imports { - if inalid_imports.contains(&path) { + if ctx.invalid_imports.contains(&path) { continue; } if let Some(&existing_id) = self.lookup.get(&path) { - let deps = self.dependencies.entry(curr_id).or_default(); + let deps = self.dependencies.entry(current.id).or_default(); if !deps.contains(&existing_id) { deps.push(existing_id); } @@ -283,9 +293,9 @@ impl DependencyGraph { } let Some(module) = - Self::parse_and_get_program(&path, importer_source, import_span, handler) + Self::parse_and_get_source_module(&path, current.source, import_span, ctx.handler) else { - inalid_imports.push(path); + ctx.invalid_imports.push(path); continue; }; @@ -294,11 +304,80 @@ impl DependencyGraph { self.lookup.insert(path.clone(), last_ind); self.paths.push(path.clone()); - self.dependencies.entry(curr_id).or_default().push(last_ind); + self.dependencies + .entry(current.id) + .or_default() + .push(last_ind); + ctx.queue.push_back(last_ind); + } + } +} + +/// Groups all shared state for import resolution to avoid threading a lot of parameters +/// through every recursive call. Lives only for the duration of [`resolve_imports`]. +struct ImportContext<'a> { + importer_source: &'a CanonSourceFile, + dependency_map: &'a DependencyMap, + use_cache: &'a mut HashMap, + handler: &'a mut ErrorCollector, +} - queue.push_back(last_ind); +impl<'a> ImportContext<'a> { + /// Resolves a single `use` declaration, caches the result for reuse during + /// later graph construction phases, and returns the resolved path and span. + /// Returns `None` and reports to `handler` if resolution fails. + fn resolve_single(&mut self, use_decl: &parse::UseDecl) -> Option<(CanonPath, Span)> { + match self + .dependency_map + .resolve_path_internal(self.importer_source.name(), use_decl) + { + Ok(resolved) => { + let result = (resolved.path.clone(), *use_decl.span()); + self.use_cache.insert(use_decl.clone(), resolved); + Some(result) + } + Err(err) => { + self.handler + .push(err.with_source(self.importer_source.clone())); + None + } } } + + /// Recursively walks an item, collecting resolved imports. + /// Recurses into inline `mod` blocks. + fn process_item(&mut self, item: &parse::Item, valid_imports: &mut Vec<(CanonPath, Span)>) { + match item { + parse::Item::Use(use_decl) => { + if let Some(import) = self.resolve_single(use_decl) { + valid_imports.push(import); + } + } + parse::Item::Module(module) => { + for item in module.items() { + self.process_item(item, valid_imports); + } + } + + // These items carry no import information at this stage and can be safely skipped. + parse::Item::TypeAlias(_) | parse::Item::Function(_) | parse::Item::Ignored => {} + } + } +} + +/// Shared mutable state threaded through dependency loading. +/// Lives only for the duration of [`DependencyGraph::new()`]. +struct LoadContext<'a> { + invalid_imports: &'a mut HashSet, + handler: &'a mut ErrorCollector, + queue: &'a mut VecDeque, +} + +/// The currently processed module and its source, used for error reporting +/// and dependency registration. +struct CurrentModule<'a> { + id: usize, + source: &'a CanonSourceFile, } #[cfg(test)] diff --git a/src/driver/resolve_order.rs b/src/driver/resolve_order.rs index efc951bd..e909ead4 100644 --- a/src/driver/resolve_order.rs +++ b/src/driver/resolve_order.rs @@ -178,7 +178,7 @@ impl DependencyGraph { let module = &self.modules[source_id]; let source = &module.source; - for elem in module.parsed_program.items() { + for elem in module.program.items() { // Handle Uses (Early Continue flattens the nesting) if let parse::Item::Use(use_decl) = elem { let resolve_path = @@ -250,7 +250,7 @@ impl DependencyGraph { items: items.into(), aliases: aliases.into_symbol_table(), functions: functions.into_symbol_table(), - span: *self.modules[0].parsed_program.as_ref(), + span: *self.modules[0].program.as_ref(), }) } From ab629c6b56bcca9822f47c76a8c3720ac539d6da Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Tue, 2 Jun 2026 17:06:44 +0300 Subject: [PATCH 06/10] update: driver resolution strategy, ast analyze. Add scopes --- src/ast.rs | 688 +++++++++++----------- src/driver/mod.rs | 24 +- src/driver/resolve_order.rs | 1089 +++-------------------------------- src/lib.rs | 62 +- src/parse.rs | 39 ++ src/witness.rs | 13 +- 6 files changed, 520 insertions(+), 1395 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index 87d24f17..6f071f63 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -8,19 +8,19 @@ use miniscript::iter::{Tree, TreeLike}; use simplicity::jet::{Elements, Jet}; use crate::debug::{CallTracker, DebugSymbols, TrackedCallName}; -use crate::driver::{FileScoped, SymbolTable, MAIN_MODULE, MAIN_STR}; +use crate::driver::{CRATE_STR, MAIN_STR}; use crate::error::{Error, RichError, Span, WithSpan}; use crate::jet::JetHL; use crate::num::{NonZeroPow2Usize, Pow2Usize}; -use crate::parse::MatchPattern; +use crate::parse::{MatchPattern, UseDecl, Visibility}; use crate::pattern::Pattern; -use crate::str::{AliasName, FunctionName, Identifier, ModuleName, WitnessName}; +use crate::str::{AliasName, FunctionName, Identifier, ModuleName, SymbolName, WitnessName}; use crate::types::{ AliasedType, ResolvedType, StructuralType, TypeConstructible, TypeDeconstructible, UIntType, }; use crate::value::{UIntValue, Value}; use crate::witness::{Parameters, WitnessTypes}; -use crate::{driver, impl_eq_hash, parse}; +use crate::{impl_eq_hash, parse}; /// A program consists of the main function. /// @@ -431,56 +431,6 @@ impl MatchArm { } } -/// Item when analyzing modules. -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub enum ModuleItem { - Ignored, - Module(Module), -} - -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub struct Module { - name: ModuleName, - assignments: Arc<[ModuleAssignment]>, - span: Span, -} - -impl Module { - /// Access the assignments of the module. - pub fn assignments(&self) -> &[ModuleAssignment] { - &self.assignments - } - - /// Access the span of the module. - pub fn span(&self) -> &Span { - &self.span - } -} - -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub struct ModuleAssignment { - name: WitnessName, - value: Value, - span: Span, -} - -impl ModuleAssignment { - /// Access the assigned witness name. - pub fn name(&self) -> &WitnessName { - &self.name - } - - /// Access the assigned witness value. - pub fn value(&self) -> &Value { - &self.value - } - - /// Access the span of the module. - pub fn span(&self) -> &Span { - &self.span - } -} - #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub enum ExprTree<'a> { Expression(&'a Expression), @@ -584,6 +534,15 @@ impl JetHinter for ElementsJetHinter { } } +/// A single module namespace. Handles arbitrary nesting via `submodules`. +#[derive(Clone, Debug, Eq, PartialEq, Default)] +struct ModuleScope { + aliases: HashMap, + functions: HashMap, + /// Nested inling `mod` blocks, each becoming a child scope. + submodules: HashMap, +} + /// Scope for generating the abstract syntax tree. /// /// The scope is used for: @@ -592,14 +551,17 @@ impl JetHinter for ElementsJetHinter { /// 3. Assigning types to each witness expression /// 4. Resolving calls to custom functions struct Scope { - file_id: usize, + /// Current position in the module tree. Push on `mod` enter, pop on exit. + /// Empty path means we are at the root (main file) scope. + module_path: Vec, + + /// Global scope where items from the main file that live at the root level. + root: ModuleScope, + + /// Block-level variable scopes. Push on block enter, pop on block exit. variables: Vec>, - aliases: HashMap, ResolvedType>, - aliases_table: SymbolTable, parameters: HashMap, witnesses: HashMap, - functions: HashMap, CustomFunction>, - functions_table: SymbolTable, is_main: bool, call_tracker: CallTracker, jet_hinter: Box, @@ -608,8 +570,6 @@ struct Scope { impl Default for Scope { fn default() -> Self { Self::new( - SymbolTable::::default(), - SymbolTable::::default(), // TODO: Should be passed in global configuration Box::new(ElementsJetHinter), ) @@ -617,37 +577,25 @@ impl Default for Scope { } impl Scope { - pub fn new( - aliases_table: SymbolTable, - functions_table: SymbolTable, - jet_hinter: Box, - ) -> Self { + pub fn new(jet_hinter: Box) -> Self { Self { - file_id: MAIN_MODULE, + module_path: Vec::new(), + root: ModuleScope::default(), variables: Vec::new(), - aliases: HashMap::new(), - aliases_table, parameters: HashMap::new(), witnesses: HashMap::new(), - functions: HashMap::new(), - functions_table, is_main: false, call_tracker: CallTracker::default(), jet_hinter, } } - pub fn file_id(&self) -> usize { - self.file_id - } - - /// Check if the current scope is topmost. - pub fn is_topmost(&self) -> bool { + pub fn is_outside_function(&self) -> bool { self.variables.is_empty() } - /// Push a new scope onto the stack. - pub fn push_scope(&mut self) { + /// Enter a new block inside the current function. + pub fn enter_block(&mut self) { self.variables.push(HashMap::new()); } @@ -655,45 +603,266 @@ impl Scope { /// /// ## Panics /// - /// - The current scope is already inside the main function. - /// - The current scope is not topmost. - pub fn push_main_scope(&mut self) { + /// - Already inside the main function. + /// - Already inside a function body. + pub fn enter_main(&mut self) { assert!(!self.is_main, "Already inside main function"); - assert!(self.is_topmost(), "Current scope is not topmost"); - self.push_scope(); + assert!(self.is_outside_function(), "Already inside a function body"); + self.enter_block(); self.is_main = true; } - /// Pop the current scope from the stack. + /// Exit the current block inside the curreent function. /// /// ## Panics /// - /// The stack is empty. - pub fn pop_scope(&mut self) { - self.variables.pop().expect("Stack is empty"); + /// - No acive block to exit. + pub fn exit_block(&mut self) { + self.variables.pop().expect("No active block to exit"); } /// Pop the scope of the main function from the stack. /// /// ## Panics /// - /// - The current scope is not inside the main function. - /// - The current scope is not nested in the topmost scope. - pub fn pop_main_scope(&mut self) { + /// - Not inside the main function. + /// - Unclosed nested blocks remain. + pub fn exit_main(&mut self) { assert!(self.is_main, "Current scope is not inside main function"); - self.pop_scope(); + self.exit_block(); self.is_main = false; assert!( - self.is_topmost(), + self.is_outside_function(), "Current scope is not nested in topmost scope" ) } - /// Push a variable onto the current stack. + /// Enter a named module, pushing it onto the module path. + /// + /// ## Errors + /// + /// * [`Error::ModuleRedefined`] A module with this name is already defined in the current scope. + pub fn enter_module(&mut self, name: ModuleName, visibility: Visibility) -> Result<(), Error> { + let current = self.current_module_mut(); + if current.submodules.contains_key(&name) { + return Err(Error::ModuleRedefined { name }); + } + + current + .submodules + .insert(name.clone(), (ModuleScope::default(), visibility)); + self.module_path.push(name); + Ok(()) + } + + /// Exit the current module, popping it from the module path. /// /// ## Panics /// - /// The stack is empty. + /// Not inside any module. + pub fn exit_module(&mut self) { + self.module_path.pop().expect("Not inside any module"); + } + + /// This allows us to perform read-only checks (like redefinitions) and + /// call `resolve` without taking a premature mutable borrow of `self`. + fn current_module(&self) -> &ModuleScope { + self.module_path.iter().fold(&self.root, |scope, segment| { + &scope.submodules.get(segment).expect("Module not found").0 + }) + } + + /// We use iterations and `O(N)` algorithm, because nested block are not so deep. + /// It will be strange to see 100 nested blocks, so common `.fold()` will be enough for that. + fn current_module_mut(&mut self) -> &mut ModuleScope { + self.module_path + .iter() + .fold(&mut self.root, |scope, segment| { + &mut scope + .submodules + .get_mut(segment) + .expect("Module not found") + .0 + }) + } + + // TODO: Consider to optimize it (we definitely can do it) + /// Resolves a `use` declaration by navigating the module tree, checking visibility, + /// and importing matching items into the current scope. + /// + /// ## Errors + /// + /// * [`Error::MissingCrateKeyword`] The import path does not start with the `crate` keyword. + /// * [`Error::ModuleNotFound`] A module segment in the target path does not exist. + /// * [`Error::ModuleIsPrivate`] Attempted to navigate into a private module from an unauthorized scope. + /// * [`Error::MainCannotBeAlias`] Attempted to alias an imported item to the reserved `main` identifier. + /// * May also return errors propagated from item collection and insertion, such as [`Error::PrivateItem`] or [`Error::RedefinedItem`]. + pub fn resolve_use(&mut self, use_decl: &UseDecl) -> Result<(), Error> { + let path = use_decl.path(); + if path[0].as_inner() != CRATE_STR { + return Err(Error::MissingCrateKeyword); + } + + let use_vis = use_decl.visibility().clone(); + let use_decl_items = match use_decl.items() { + parse::UseItems::Single(elem) => std::slice::from_ref(elem), + parse::UseItems::List(elems) => elems.as_slice(), + }; + + // Phase 1: navigate to target and collect items. Immutable borrow, dropped at end of block + let collected: Vec<_> = { + // TODO: Part, that can be optimized + // How many segments do the caller's path and the target's path have in common? + let shared_prefix_len = self + .module_path + .iter() + .zip(&path[1..]) + .take_while(|(curr, nav)| curr.as_inner() == nav.as_inner()) + .count(); + + let mut target_scope = &self.root; + + for (ind, segment) in path[1..].iter().enumerate() { + let name = ModuleName::from_str_unchecked(segment.as_inner()); + + let (inner, visibility) = target_scope + .submodules + .get(&name) + .ok_or_else(|| Error::ModuleNotFound { name: name.clone() })?; + + if matches!(visibility, Visibility::Private) && shared_prefix_len < ind { + return Err(Error::ModuleIsPrivate { name }); + } + + target_scope = inner; + } + + let mut collected = Vec::with_capacity(use_decl_items.len()); + for (name, aliased) in use_decl_items { + if aliased.as_ref().is_some_and(|a| a.as_inner() == MAIN_STR) { + return Err(Error::MainCannotBeAlias); + } + + let local_name = aliased.as_ref().unwrap_or(name); + + let alias_res = + Self::try_collect_item(name, local_name, &target_scope.aliases, &use_vis); + let func_res = + Self::try_collect_item(name, local_name, &target_scope.functions, &use_vis); + let mod_res = + Self::try_collect_item(name, local_name, &target_scope.submodules, &use_vis); + + collected.push((alias_res, func_res, mod_res)); + } + collected + }; + + // Phase 2: insert into current scope + let current = self.current_module_mut(); + for (alias_res, func_res, mod_res) in collected { + Self::resolve_processing_use_items_error(&[ + Self::insert_collected(alias_res, &mut current.aliases), + Self::insert_collected(func_res, &mut current.functions), + Self::insert_collected(mod_res, &mut current.submodules), + ])?; + } + + Ok(()) + } + + /// Attempts to find `name` in `target_map` and prepare it for import into another scope. + /// + /// ## Errors + /// + /// * [`Error::UnresolvedItem`] The requested `name` was not found in the `target_map`. + /// * [`Error::PrivateItem`] The requested item exists in the map, but its visibility is restricted to private. + fn try_collect_item( + name: &SymbolName, + local_name: &SymbolName, + target_map: &HashMap, + use_vis: &Visibility, + ) -> Result<(K, (V, Visibility)), Error> + where + K: Eq + std::hash::Hash + From + Clone, + V: Clone, + { + let (value, vis) = + target_map + .get(&K::from(name.clone())) + .ok_or_else(|| Error::UnresolvedItem { + name: name.to_string(), + })?; + + if matches!(vis, Visibility::Private) { + return Err(Error::PrivateItem { + name: name.to_string(), + }); + } + + Ok(( + K::from(local_name.clone()), + (value.clone(), use_vis.clone()), + )) + } + + /// Inserts a successfully collected item into the current scope's map. + /// + /// ## Errors + /// + /// * [`Error::RedefinedItem`] An item with the same name is already defined in the target scope. + /// * Propagates any upstream resolution error passed into the `res` argument. + fn insert_collected( + res: Result<(K, (V, Visibility)), Error>, + map: &mut HashMap, + ) -> Result<(), Error> + where + K: Eq + std::hash::Hash + std::fmt::Display, + { + res.and_then(|(k, v)| match map.entry(k) { + Entry::Occupied(entry) => Err(Error::RedefinedItem { + name: entry.key().to_string(), + }), + Entry::Vacant(entry) => { + entry.insert(v); + Ok(()) + } + }) + } + + // TODO: Consider to use better error handling + /// Evaluates the results of attempting to collect an item from multiple namespaces + /// (aliases, functions, submodules) and resolves the final error state. + /// + /// ## Errors + /// + /// * Returns a specific error (e.g., [`Error::PrivateItem`], [`Error::RedefinedItem`]) if one occurred. + /// * Returns a fallback [`Error::UnresolvedItem`] if the item could not be found in any of the checked namespaces. + fn resolve_processing_use_items_error(results: &[Result<(), Error>]) -> Result<(), Error> { + if results.iter().any(|res| res.is_ok()) { + return Ok(()); + } + + let errors: Vec<&Error> = results + .iter() + .filter_map(|res| res.as_ref().err()) + .collect(); + + if let Some(&specific_err) = errors + .iter() + .find(|e| !matches!(e, Error::UnresolvedItem { .. })) + { + return Err(specific_err.clone()); + } + + // Fallback to the first `UnresolvedItem` error + Err(errors[0].clone()) + } + + /// Insert a variable into the current block. + /// + /// ## Panics + /// + /// - No active block. pub fn insert_variable(&mut self, identifier: Identifier, ty: ResolvedType) { self.variables .last_mut() @@ -709,50 +878,17 @@ impl Scope { .find_map(|scope| scope.get(identifier)) } - /// Retrieves the definition of a type alias, enforcing strict error prioritization. + /// Retrieves the resolved type of a type alias in the current module scope. /// - /// # Errors - /// * [`Error::UndefinedAlias`]: The alias is not found in the global registry. - /// * [`Error::Internal`]: The specified `file_id` does not exist in the `files`. - /// * [`Error::PrivateItem`]: The alias exists globally but is not exposed to the current file's scope. + /// ## Errors + /// + /// * [`Error::UndefinedAlias`]: The alias is not defined in the current scope. fn get_alias(&self, name: &AliasName) -> Result { - // 1. Get the true global ID of the alias. - let initial_id = (name.clone(), self.file_id); - let global_id = self - .aliases_table - .imports() - .resolved_roots() - .get(&initial_id) - .cloned() - .unwrap_or(initial_id); - - // 2. Fetch the alias from the global pool. - let resolved_type = self + self.current_module() .aliases - .get(&global_id) - .ok_or_else(|| Error::UndefinedAlias { name: name.clone() })?; - - // 3. Fetch the file scope for visibility checking. - let file_scope = self - .aliases_table - .local_scopes() - .get(self.file_id) - .ok_or_else(|| Error::Internal { - msg: format!( - "file_id {} not found inside current Scope aliases", - self.file_id - ), - })?; - - // 4. Verify local scope visibility. - if file_scope.contains(name) { - // We clone here because types usually need to be owned in AST resolution - Ok(resolved_type.clone()) - } else { - Err(Error::PrivateItem { - name: name.as_inner().to_string(), - }) - } + .get(name) + .map(|(ty, _)| ty.clone()) + .ok_or_else(|| Error::UndefinedAlias { name: name.clone() }) } /// Resolve a type with aliases to a type without aliases. @@ -760,24 +896,25 @@ impl Scope { /// ## Errors /// /// * [`Error::UndefinedAlias`]: The alias is not found in the global registry. - /// * [`Error::Internal`]: The specified `file_id` does not exist in the `files`. - /// * [`Error::PrivateItem`]: The alias exists globally but is not exposed to the current file's scope. pub fn resolve(&self, ty: &AliasedType) -> Result { ty.resolve(|name| self.get_alias(name)) } - /// Push a type alias into the global map. + /// Insert a type alias into the current module scope. /// /// ## Errors /// - /// There are any undefined aliases. - pub fn insert_alias(&mut self, name: AliasName, ty: AliasedType) -> Result<(), Error> { - let plug = (name.clone(), self.file_id); - if self.aliases.contains_key(&plug) { + /// * [`Error::RedefinedAlias`]: The alias name is already defined in the current scope. + pub fn insert_alias(&mut self, alias: parse::TypeAlias) -> Result<(), Error> { + let name = alias.name().clone(); + if self.current_module().aliases.contains_key(&name) { return Err(Error::RedefinedAlias { name }); } - let _ = self.aliases.insert(plug, self.resolve(&ty)?); + let resolved = self.resolve(alias.ty())?; + self.current_module_mut() + .aliases + .insert(name, (resolved, alias.visibility().clone())); Ok(()) } @@ -785,7 +922,7 @@ impl Scope { /// /// ## Errors /// - /// A parameter of the same name has already been defined as a different type. + /// * [`Error::ExpressionTypeMismatch`] A parameter of the same name has already been defined as a different type. pub fn insert_parameter(&mut self, name: WitnessName, ty: ResolvedType) -> Result<(), Error> { match self.parameters.entry(name.clone()) { Entry::Occupied(entry) if entry.get() == &ty => Ok(()), @@ -804,8 +941,8 @@ impl Scope { /// /// ## Errors /// - /// - The current scope is not inside the main function. - /// - A witness with the same name has already been defined. + /// * [`Error::WitnessOutsideMain`] The current scope is not inside the main function. + /// * [`Error::WitnessReused`] A witness with the same name has already been defined. pub fn insert_witness(&mut self, name: WitnessName, ty: ResolvedType) -> Result<(), Error> { if !self.is_main { return Err(Error::WitnessOutsideMain); @@ -837,74 +974,34 @@ impl Scope { /// /// ## Errors /// - /// The function has already been defined. + /// * [`Error::FunctionRedefined`] The function has already been defined. pub fn insert_function( &mut self, name: FunctionName, + visibility: Visibility, function: CustomFunction, ) -> Result<(), Error> { - let func_name_id = (name.clone(), self.file_id); - - if self.functions.contains_key(&func_name_id) { + if self.current_module().functions.contains_key(&name) { return Err(Error::FunctionRedefined { name }); } - let _ = self.functions.insert(func_name_id, function); + self.current_module_mut() + .functions + .insert(name, (function, visibility)); Ok(()) } /// Retrieves the definition of a custom function, enforcing strict error prioritization. /// - /// # Architecture Note - /// The order of operations here is intentional to prioritize specific compiler errors: - /// 1. Resolve the alias to find the true global coordinates. - /// 2. Check for global existence (`FunctionUndefined`) *before* checking local visibility. - /// 3. Verify if the current file's scope is actually allowed to see it (`PrivateItem`). - /// - /// # Errors + /// ## Errors /// /// * [`Error::FunctionUndefined`]: The function is not found in the global registry. - /// * [`Error::Internal`]: The specified `file_id` does not exist in the `files`. - /// * [`Error::PrivateItem`]: The function exists globally but is not exposed to the current file's scope. - pub fn get_function(&self, name: &FunctionName) -> Result<&CustomFunction, Error> { - // 1. Get the true global ID of the alias (or keep the current name if it is not aliased). - let initial_id = (name.clone(), self.file_id); - let global_id = self - .functions_table - .imports() - .resolved_roots() - .get(&initial_id) - .cloned() - .unwrap_or(initial_id); - - // 2. Fetch the function from the global pool. - // We do this first so we can throw FunctionUndefined before checking local visibility. - let function = self + pub fn get_function(&self, name: &FunctionName) -> Result { + self.current_module() .functions - .get(&global_id) - .ok_or_else(|| Error::FunctionUndefined { name: name.clone() })?; - - // TODO: Consider changing it to a better error handler with a source file. - let file_scope = self - .functions_table - .local_scopes() - .get(self.file_id) - .ok_or_else(|| Error::Internal { - msg: format!( - "file_id {} not found inside current Scope files", - self.file_id - ), - })?; - - // 3. Verify local scope visibility. - // We successfully found the function globally, but is this file allowed to use it? - if file_scope.contains(name) { - Ok(function) - } else { - Err(Error::PrivateItem { - name: name.as_inner().to_string(), - }) - } + .get(name) + .map(|(func, _)| func.clone()) + .ok_or_else(|| Error::FunctionUndefined { name: name.clone() }) } /// Track a call expression with its span. @@ -928,18 +1025,23 @@ trait AbstractSyntaxTree: Sized { impl Program { pub fn analyze( - from: &driver::Program, + from: &parse::Program, jet_hinter: Box, ) -> Result { let unit = ResolvedType::unit(); - let mut scope = Scope::new(from.aliases().clone(), from.functions().clone(), jet_hinter); + let mut scope = Scope::new(jet_hinter); let items = from .items() .iter() .map(|s| Item::analyze(s, &unit, &mut scope)) .collect::, RichError>>()?; - debug_assert!(scope.is_topmost()); + debug_assert!(scope.is_outside_function()); + debug_assert!( + scope.module_path.is_empty(), + "Unclosed module scopes remain" + ); + let (parameters, witness_types, call_tracker) = scope.destruct(); let mut iter = items.into_iter().filter_map(|item| match item { Item::Function(Function::Main(expr)) => Some(expr), @@ -967,34 +1069,35 @@ impl AbstractSyntaxTree for Item { fn analyze(from: &Self::From, ty: &ResolvedType, scope: &mut Scope) -> Result { assert!(ty.is_unit(), "Items cannot return anything"); - assert!(scope.is_topmost(), "Items live in the topmost scope only"); - let previous_file_id = scope.file_id(); + assert!( + scope.is_outside_function(), + "Variables live only inside the function" + ); - let res = match from { + match from { parse::Item::TypeAlias(alias) => { - scope.file_id = alias.file_id(); - scope - .insert_alias(alias.name().clone(), alias.ty().clone()) - .with_span(alias)?; + scope.insert_alias(alias.clone()).with_span(alias)?; Ok(Self::TypeAlias) } parse::Item::Function(function) => { - scope.file_id = function.file_id(); Function::analyze(function, ty, scope).map(Self::Function) } - parse::Item::Use(use_decl) => Err(RichError::new( - Error::UseKeywordIsNotSupported, - *use_decl.span(), - )), - parse::Item::Module(module) => Err(RichError::new( - Error::ModuleKeywordIsNotSupported, - *module.span(), - )), + parse::Item::Use(use_decl) => { + scope.resolve_use(use_decl).with_span(use_decl)?; + Ok(Self::Use) + } + parse::Item::Module(module) => { + scope + .enter_module(module.name().clone(), module.visibility().clone()) + .with_span(module)?; + for item in module.items() { + Item::analyze(item, ty, scope)?; + } + scope.exit_module(); + Ok(Self::Module) + } parse::Item::Ignored => Ok(Self::Ignored), - }; - - scope.file_id = previous_file_id; - res + } } } @@ -1003,7 +1106,10 @@ impl AbstractSyntaxTree for Function { fn analyze(from: &Self::From, ty: &ResolvedType, scope: &mut Scope) -> Result { assert!(ty.is_unit(), "Function definitions cannot return anything"); - assert!(scope.is_topmost(), "Items live in the topmost scope only"); + assert!( + scope.is_outside_function(), + "Variables live only inside the function" + ); if from.name().as_inner() != MAIN_STR { let params = from @@ -1023,17 +1129,17 @@ impl AbstractSyntaxTree for Function { .transpose()? .unwrap_or_else(ResolvedType::unit); - scope.push_scope(); + scope.enter_block(); for param in params.iter() { scope.insert_variable(param.identifier().clone(), param.ty().clone()); } let body = Expression::analyze(from.body(), &ret, scope).map(Arc::new)?; - scope.pop_scope(); + scope.exit_block(); - debug_assert!(scope.is_topmost()); + debug_assert!(scope.is_outside_function()); let function = CustomFunction { params, body }; scope - .insert_function(from.name().clone(), function) + .insert_function(from.name().clone(), from.visibility().clone(), function) .with_span(from)?; return Ok(Self::Custom); @@ -1049,13 +1155,17 @@ impl AbstractSyntaxTree for Function { } } - if scope.file_id() != MAIN_MODULE { + if !scope.module_path.is_empty() { return Err(Error::MainOutOfEntryFile).with_span(from); } - scope.push_main_scope(); + if matches!(from.visibility(), Visibility::Public) { + return Err(Error::MainCannotBePublic).with_span(from); + } + + scope.enter_main(); let body = Expression::analyze(from.body(), ty, scope)?; - scope.pop_main_scope(); + scope.exit_main(); Ok(Self::Main(body)) } } @@ -1129,7 +1239,7 @@ impl AbstractSyntaxTree for Expression { }) } parse::ExpressionInner::Block(statements, expression) => { - scope.push_scope(); + scope.enter_block(); let ast_statements = statements .iter() .map(|s| Statement::analyze(s, &ResolvedType::unit(), scope)) @@ -1145,7 +1255,7 @@ impl AbstractSyntaxTree for Expression { }) .with_span(from), }?; - scope.pop_scope(); + scope.exit_block(); Ok(Self { ty: ty.clone(), @@ -1563,13 +1673,11 @@ impl AbstractSyntaxTree for CallName { parse::CallName::TypeCast(target) => { scope.resolve(target).map(Self::TypeCast).with_span(from) } - parse::CallName::Custom(name) => scope - .get_function(name) - .cloned() - .map(Self::Custom) - .with_span(from), + parse::CallName::Custom(name) => { + scope.get_function(name).map(Self::Custom).with_span(from) + } parse::CallName::ArrayFold(name, size) => { - let function = scope.get_function(name).cloned().with_span(from)?; + let function = scope.get_function(name).with_span(from)?; // A function that is used in a array fold has the signature: // fn f(element: E, accumulator: A) -> A if function.params().len() != 2 || function.params()[1].ty() != function.body().ty() @@ -1580,7 +1688,7 @@ impl AbstractSyntaxTree for CallName { } } parse::CallName::Fold(name, bound) => { - let function = scope.get_function(name).cloned().with_span(from)?; + let function = scope.get_function(name).with_span(from)?; // A function that is used in a list fold has the signature: // fn f(element: E, accumulator: A) -> A if function.params().len() != 2 || function.params()[1].ty() != function.body().ty() @@ -1591,7 +1699,7 @@ impl AbstractSyntaxTree for CallName { } } parse::CallName::ForWhile(name) => { - let function = scope.get_function(name).cloned().with_span(from)?; + let function = scope.get_function(name).with_span(from)?; // A function that is used in a for-while loop has the signature: // fn f(accumulator: A, readonly_context: C, counter: u{N}) -> Either // where @@ -1633,7 +1741,7 @@ impl AbstractSyntaxTree for Match { let scrutinee = Expression::analyze(from.scrutinee(), &scrutinee_ty, scope).map(Arc::new)?; - scope.push_scope(); + scope.enter_block(); if let Some((pat_l, ty_l)) = from.left().pattern().as_typed_pattern() { let ty_l = scope.resolve(ty_l).with_span(from)?; let typed_variables = pat_l.is_of_type(&ty_l).with_span(from)?; @@ -1642,8 +1750,8 @@ impl AbstractSyntaxTree for Match { } } let ast_l = Expression::analyze(from.left().expression(), ty, scope).map(Arc::new)?; - scope.pop_scope(); - scope.push_scope(); + scope.exit_block(); + scope.enter_block(); if let Some((pat_r, ty_r)) = from.right().pattern().as_typed_pattern() { let ty_r = scope.resolve(ty_r).with_span(from)?; let typed_variables = pat_r.is_of_type(&ty_r).with_span(from)?; @@ -1652,7 +1760,7 @@ impl AbstractSyntaxTree for Match { } } let ast_r = Expression::analyze(from.right().expression(), ty, scope).map(Arc::new)?; - scope.pop_scope(); + scope.exit_block(); Ok(Self { scrutinee, @@ -1698,95 +1806,3 @@ impl AsRef for Match { &self.span } } - -impl AsRef for Module { - fn as_ref(&self) -> &Span { - &self.span - } -} - -impl AsRef for ModuleAssignment { - fn as_ref(&self) -> &Span { - &self.span - } -} - -#[cfg(test)] -mod alias_scope_regression_tests { - use super::{ElementsJetHinter, Program}; - use crate::driver::tests::setup_graph; - use crate::error::ErrorCollector; - - fn analyze_multifile(files: Vec<(&str, &str)>) -> Result<(), String> { - let (graph, _ids, _dir) = setup_graph(files); - - let mut error_handler = ErrorCollector::new(); - let driver_program = graph - .linearize_and_build(&mut error_handler) - .unwrap() - .expect("driver build should succeed"); - - Program::analyze(&driver_program, Box::new(ElementsJetHinter)) - .map(|_| ()) - .map_err(|e| e.to_string()) - } - - #[test] - fn private_type_alias_from_dependency_does_not_leak() { - let result = analyze_multifile(vec![ - ( - "main.simf", - "use lib::A::helper; fn main() { helper(); let x: Secret = 0; }", - ), - ("libs/lib/A.simf", "type Secret = u32; pub fn helper() {}"), - ]); - - assert!( - result.is_err(), - "private alias from another file leaked into root scope: {result:?}" - ); - } - - #[test] - fn same_alias_name_in_different_modules_does_not_conflict_if_only_one_is_imported() { - let result = analyze_multifile(vec![ - ( - "main.simf", - "use lib::A::Word; use lib::B::id; fn main() { let x: Word = 0; assert!(jet::is_zero_32(id(x))); }", - ), - ("libs/lib/A.simf", "pub type Word = u32;"), - ("libs/lib/B.simf", "pub type Word = u16; pub fn id(x: u32) -> u32 { x }"), - ]); - - assert!( - result.is_ok(), - "unimported alias from another module should not collide: {result:?}" - ); - } - - #[test] - fn dependency_main_does_not_satisfy_missing_root_main() { - let result = analyze_multifile(vec![ - ("main.simf", "use lib::A::helper;"), - ("libs/lib/A.simf", "fn main() {} pub fn helper() {}"), - ]); - - assert!( - result.is_err(), - "Main function must be inside an entry file: {result:?}" - ) - } - - #[test] - fn main_must_be_defined_once_per_project() { - let result = analyze_multifile(vec![ - ("main.simf", "use lib::A::helper; fn main() { helper(); }"), - ("libs/lib/A.simf", "fn main() {} pub fn helper() {}"), - ]); - - assert!( - result.is_err(), - "Main function must be inside an entry file: {result:?}" - ); - } -} diff --git a/src/driver/mod.rs b/src/driver/mod.rs index 5967623e..c9103725 100644 --- a/src/driver/mod.rs +++ b/src/driver/mod.rs @@ -13,36 +13,30 @@ //! to build a Directed Acyclic Graph (DAG) of the project's dependencies. Because //! the final AST requires a flat array of items, the driver applies a deterministic //! linearization strategy to this DAG. This safely flattens the multi-file project -//! into a single, logically ordered sequence, strictly enforcing visibility rules -//! and preventing duplicate imports. +//! into a single, logically ordered sequence. //! //! ## Project Structure & Entry Point //! -//! SimplicityHL does not define a "project root" directory. Instead, the compiler -//! relies on a single entry point: the file passed as the first positional argument. -//! This file must contain the `main` function, which serves as the program's -//! starting point. +//! SimplicityHL programs begin execution at a single entry point file, +//! which the driver registers internally as the root [`MAIN_MODULE`]. +//! This is typically the file passed as the root to the [`DependencyGraph::new`] function. //! //! External libraries are explicitly linked using the `--dep` flag. The driver //! resolves and parses these external files relative to the entry point during //! the dependency graph construction. mod linearization; -pub(crate) mod resolve_order; +mod resolve_order; use std::collections::{HashMap, HashSet, VecDeque}; use std::path::PathBuf; use std::sync::Arc; -use chumsky::container::Container; - use crate::error::{Error, ErrorCollector, RichError, Span}; use crate::parse::{self, ParseFromStrWithErrors}; use crate::resolution::{DependencyMap, ResolvedUse}; use crate::source::{CanonPath, CanonSourceFile}; -pub use crate::driver::resolve_order::{FileScoped, Program, SymbolTable}; - /// The reserved identifier for the program's entry point. pub(crate) const MAIN_STR: &str = "main"; @@ -249,7 +243,7 @@ impl DependencyGraph { /// /// Results are cached in `use_cache` to avoid redundant filesystem lookups during /// later construction phases. - /// Note: This is a specialized helper designed exclusively for [`DependencyGraph::new()`]. + /// Note: This is a specialized helper designed exclusively for [`DependencyGraph::new`]. fn resolve_imports( current_program: &parse::Program, importer_source: &CanonSourceFile, @@ -272,7 +266,7 @@ impl DependencyGraph { } /// PHASE 2 OF GRAPH CONSTRUCTION: Loads, parses, and registers new dependencies. - /// Note: This is a specialized helper designed exclusively for [`DependencyGraph::new()`]. + /// Note: This is a specialized helper designed exclusively for [`DependencyGraph::new`]. fn load_and_parse_dependencies( &mut self, current: &CurrentModule, @@ -295,7 +289,7 @@ impl DependencyGraph { let Some(module) = Self::parse_and_get_source_module(&path, current.source, import_span, ctx.handler) else { - ctx.invalid_imports.push(path); + ctx.invalid_imports.insert(path); continue; }; @@ -366,7 +360,7 @@ impl<'a> ImportContext<'a> { } /// Shared mutable state threaded through dependency loading. -/// Lives only for the duration of [`DependencyGraph::new()`]. +/// Lives only for the duration of [`DependencyGraph::new`]. struct LoadContext<'a> { invalid_imports: &'a mut HashSet, handler: &'a mut ErrorCollector, diff --git a/src/driver/resolve_order.rs b/src/driver/resolve_order.rs index e909ead4..69cfbff0 100644 --- a/src/driver/resolve_order.rs +++ b/src/driver/resolve_order.rs @@ -1,158 +1,7 @@ -use std::collections::{BTreeMap, BTreeSet, HashMap}; -use std::sync::Arc; - -use crate::driver::{DependencyGraph, MAIN_MODULE, MAIN_STR}; -use crate::error::{Error, ErrorCollector, RichError, Span}; -use crate::impl_eq_hash; -use crate::parse::{self, AliasedSymbolName, Function, TypeAlias, Visibility}; -use crate::str::{AliasName, FunctionName, SymbolName}; - -/// The final, flattened representation of a SimplicityHL program. -/// -/// This struct holds the fully resolved sequence of items, paths, and scope -/// resolutions, ready to be passed to the next stage of the compiler. -#[derive(Clone, Debug)] -pub struct Program { - /// The linear sequence of compiled items (`Functions`, `TypeAliases`, etc.). - items: Arc<[parse::Item]>, - - /// Contains all resolved aliases for the local scopes and the global import registry. - aliases: SymbolTable, - - /// Contains all resolved functions for the local scopes and the global import registry. - functions: SymbolTable, - - span: Span, -} - -impl Program { - pub fn from_parse( - parsed: &parse::Program, - content: Arc, - handler: &mut ErrorCollector, - ) -> Option { - let module_count = 1; - - let mut items: Vec = Vec::new(); - - let mut aliases = NamespaceTracker::::new(module_count); - let mut functions = NamespaceTracker::::new(module_count); - - for item in parsed.items() { - if let parse::Item::Use(use_decl) = item { - handler.push( - RichError::new( - Error::UnknownLibrary { - name: use_decl.str_path(), - }, - *use_decl.span(), - ) - .with_content(content.clone()), - ); - continue; - } - - let mut new_elem = item.clone(); - match &mut new_elem { - parse::Item::TypeAlias(type_alias) => { - if let Err(err) = register_type_alias(type_alias, &mut aliases, MAIN_MODULE) { - handler.push(err.with_content(content.clone())); - continue; - } - } - parse::Item::Function(function) => { - if let Err(err) = register_function(function, &mut functions, MAIN_MODULE) { - handler.push(err.with_content(content.clone())); - continue; - } - } - - // Safe to skip: `Use` items are handled earlier in the loop, and `Module` currently has no functionality. - // It will handle properly in the following commits. - parse::Item::Module(_) | parse::Item::Use(_) | parse::Item::Ignored => continue, - } - items.push(new_elem); - } - - // TODO: Consider getting rid of the 'String' error here and changing it to a more appropriate error - // (e.g. 'Result') after resolving https://github.com/BlockstreamResearch/SimplicityHL/issues/270. - (!handler.has_errors()).then(|| Program { - items: items.into(), - aliases: aliases.into_symbol_table(), - functions: functions.into_symbol_table(), - span: *parsed.as_ref(), - }) - } - - pub fn items(&self) -> &[parse::Item] { - &self.items - } - - pub fn aliases(&self) -> &SymbolTable { - &self.aliases - } - - pub fn functions(&self) -> &SymbolTable { - &self.functions - } - - pub fn span(&self) -> &Span { - &self.span - } -} - -impl_eq_hash!(Program; items, aliases, functions); - -/// Holds all scoping and import data for a specific namespace (e.g., Functions or Aliases). -#[derive(Clone, Debug)] -pub struct SymbolTable { - /// The items available in each file's local scope. - /// The index of the array corresponds to the file ID. - local_scopes: Arc<[BTreeSet]>, - - /// The cross-file import mappings and cached roots. - imports: ImportRegistry, -} - -impl SymbolTable { - pub fn local_scopes(&self) -> &[BTreeSet] { - &self.local_scopes - } - - pub fn imports(&self) -> &ImportRegistry { - &self.imports - } -} - -impl_eq_hash!(SymbolTable; local_scopes, imports); - -/// Represents an item name alongside its originating file ID. -pub type FileScoped = (T, usize); - -/// A registry mapping an alias [`FileScoped`] to its target item across different files. -/// -/// We use a type alias here to provide a convenient abstraction for the `AST::analyze` -/// phase, making it easier to modify the underlying structure in the future if needed. -pub type ImportMap = BTreeMap, FileScoped>; - -/// Manages the resolution of import aliases across the entire program. -#[derive(Clone, Debug)] -pub struct ImportRegistry { - direct_targets: ImportMap, - resolved_roots: ImportMap, -} - -impl ImportRegistry { - pub fn direct_targets(&self) -> &ImportMap { - &self.direct_targets - } - - pub fn resolved_roots(&self) -> &ImportMap { - &self.resolved_roots - } -} - -impl_eq_hash!(ImportRegistry; direct_targets, resolved_roots); +use crate::driver::{DependencyGraph, CRATE_STR, MAIN_MODULE}; +use crate::error::ErrorCollector; +use crate::parse::{self, Visibility}; +use crate::str::{Identifier, ModuleName}; /// This is a core component of the [`DependencyGraph`]. impl DependencyGraph { @@ -160,882 +9,110 @@ impl DependencyGraph { pub fn linearize_and_build( &self, handler: &mut ErrorCollector, - ) -> Result, String> { + ) -> Result, String> { match self.linearize() { Ok(order) => Ok(self.build_program(&order, handler)), Err(err) => Err(err.to_string()), } } - /// Constructs the unified AST for the entire program. - fn build_program(&self, order: &[usize], handler: &mut ErrorCollector) -> Option { - let mut items: Vec = Vec::new(); + fn get_module_name(source_id: usize) -> Identifier { + Identifier::from_str_unchecked(format!("unit_{}", source_id).as_str()) + } - let mut aliases = NamespaceTracker::::new(self.modules.len()); - let mut functions = NamespaceTracker::::new(self.modules.len()); + /// Constructs the unified array of items for the entire multi-program. + fn build_program( + &self, + order: &[usize], + handler: &mut ErrorCollector, + ) -> Option { + let mut items = Vec::with_capacity(order.len()); for &source_id in order { let module = &self.modules[source_id]; - let source = &module.source; - for elem in module.program.items() { - // Handle Uses (Early Continue flattens the nesting) - if let parse::Item::Use(use_decl) = elem { - let resolve_path = - match self.dependency_map.resolve_path(source.name(), use_decl) { - Ok(path) => path, - Err(err) => { - handler.push(err.with_source(source.clone())); - continue; - } - }; + let local_items: Vec = module + .program + .items() + .iter() + .filter_map(|item| self.rewrite_item(source_id, item)) + .collect(); - let ind = self.lookup[&resolve_path]; - let use_decl_items = match use_decl.items() { - parse::UseItems::Single(elem) => std::slice::from_ref(elem), - parse::UseItems::List(elems) => elems.as_slice(), - }; - - for aliased_item in use_decl_items { - let alias_err = Self::process_use_item( - &mut aliases, - source_id, - ind, - aliased_item, - use_decl, - ); - - let function_err = Self::process_use_item( - &mut functions, - source_id, - ind, - aliased_item, - use_decl, - ); - - if let Err(err) = - Self::resolve_processing_use_items_error(alias_err, function_err) - { - handler.push(err.with_source(source.clone())); - } - } - continue; - } - - // Handle Types & Functions by inserting them into their STRICT namespaces - let mut new_elem = elem.clone(); - match &mut new_elem { - parse::Item::TypeAlias(type_alias) => { - if let Err(err) = register_type_alias(type_alias, &mut aliases, source_id) { - handler.push(err.with_source(source.clone())); - continue; - } - } - parse::Item::Function(function) => { - if let Err(err) = register_function(function, &mut functions, source_id) { - handler.push(err.with_source(source.clone())); - continue; - } - } - - // Safe to skip: `Use` items are handled earlier in the loop, and `Module` currently has no functionality. - // It will handle properly in the following commits. - parse::Item::Module(_) | parse::Item::Use(_) | parse::Item::Ignored => continue, - } - items.push(new_elem); - } - } - - (!handler.has_errors()).then(|| Program { - items: items.into(), - aliases: aliases.into_symbol_table(), - functions: functions.into_symbol_table(), - span: *self.modules[0].program.as_ref(), - }) - } - - /// Attempts to pick the most helpful error when an import fails in both namespaces. - /// - /// Since SimplicityHL supports separated namespaces, a single `use` statement - /// may successfully load a `Function`, a `TypeAlias`, or both simultaneously. - fn resolve_processing_use_items_error( - alias: Result<(), RichError>, - function: Result<(), RichError>, - ) -> Result<(), RichError> { - match (alias, function) { - (Ok(()), _) | (_, Ok(())) => Ok(()), - - (Err(err_alias), Err(err_func)) => { - let alias_is_missing = matches!(err_alias.error(), Error::UnresolvedItem { .. }); - let func_is_missing = matches!(err_func.error(), Error::UnresolvedItem { .. }); - - if !alias_is_missing || func_is_missing { - // If it's missing everywhere, OR if the function is missing - // but the alias has a specific error (like PrivateItem). - Err(err_alias) - } else { - Err(err_func) - } - } - } - } - - /// Processes a single imported item (or alias) and registers it within a specific namespace. - /// - /// This function verifies that the requested item exists in the source module and has the appropriate public - /// visibility. If validation passes and no local naming collisions are found, the item is registered - /// in the destination module's local scope and the global import registry. - /// - /// # Arguments - /// - /// * `namespace` - The generic tracker (e.g., for Functions or Aliases) that holds - /// the local file scopes, the global import registry, and the memoization set to prevent collisions. - /// * `source_id` - The `usize` identifier of the destination module where the item is being imported *to*. - /// * `ind` - The unique identifier of the source module being imported *from*. - /// * `aliased_symbol_name` - The specific identifier (and potential alias) being imported from the source. - /// * `use_decl` - The node of the `use` statement. This dictates the visibility of the new import - /// (e.g., `pub use` re-exports the item publicly). - /// - /// # Returns - /// - /// Returns `Ok(())` on success. Returns `Err(RichError)` if: - /// * [`Error::UnresolvedItem`]: The target name does not exist in the source module (`ind`). - /// * [`Error::PrivateItem`]: The target exists, but its visibility is explicitly `Private`. - /// * [`Error::MainCannotBeAlias`]: The `main` cannot be alias. - /// * [`Error::DuplicateAlias`]: The local name (or alias) has already been used in another import statement. - /// * [`Error::RedefinedItem`]: The local name conflicts with an existing item already defined in this module. - fn process_use_item( - namespace: &mut NamespaceTracker, - source_id: usize, - ind: usize, - (name, alias): &AliasedSymbolName, - use_decl: &parse::UseDecl, - ) -> Result<(), RichError> - where - T: From + std::fmt::Display + Clone + Eq + std::hash::Hash + std::cmp::Ord, - { - // NOTE: The order of errors is important! - let span = *use_decl.span(); - - // 1. Convert the unresolved SymbolName into our strict type T - let target_name: T = name.clone().into(); - let orig_id = (target_name.clone(), ind); - - // 2. Verify Existence using T - let visibility: &Visibility = - namespace.resolutions[ind] - .get(&target_name) - .ok_or_else(|| { - RichError::new( - Error::UnresolvedItem { - name: name.to_string(), - }, - span, - ) - })?; - - // 3. Verify Visibility - if matches!(visibility, parse::Visibility::Private) { - return Err(RichError::new( - Error::PrivateItem { - name: name.to_string(), - }, - span, - )); - } - - // 4. Determine the local name and ID up front - // We figure out the raw symbol first, so we can use it for error messages - let local_symbol = alias.as_ref().unwrap_or(name); - - // Then convert that raw symbol to T - let local_name: T = if let Some(alias_sym) = alias { - let t_alias: T = alias_sym.clone().into(); - - if t_alias.to_string() == MAIN_STR { - return Err(RichError::new(Error::MainCannotBeAlias, span)); + if source_id == MAIN_MODULE { + items.extend(local_items); + continue; } - t_alias - } else { - name.clone().into() - }; - - let local_id = (local_name.clone(), source_id); - - // 5. Check for collisions using `namespace` fields - if namespace.registry.direct_targets.contains_key(&local_id) { - return Err(RichError::new( - Error::DuplicateAlias { - name: local_symbol.to_string(), - }, - span, - )); - } - if namespace.memo.contains(&local_id) { - return Err(RichError::new( - Error::RedefinedItem { - name: local_symbol.to_string(), - }, - span, - )); + let name = ModuleName::from_str_unchecked(Self::get_module_name(source_id).as_inner()); + items.push(parse::Item::Module(parse::Module::new( + source_id, + Visibility::Private, + name, + &local_items, + ))); } - namespace.memo.insert(local_id.clone()); - - // 6. Update the registers - namespace - .registry - .direct_targets - .insert(local_id.clone(), orig_id.clone()); - - // 7. Find the true root - let true_root = namespace - .registry - .resolved_roots - .get(&orig_id) - .cloned() - .unwrap_or_else(|| orig_id.clone()); - - namespace - .registry - .resolved_roots - .insert(local_id, true_root); - // 8. Register the item in the local module's namespace - namespace.resolutions[source_id].insert(local_name, use_decl.visibility().clone()); - Ok(()) - } -} - -// Architectural Note: -// The two functions may seem duplicated. To prevent this, the best approach -// would be to add a `NamedItem` trait. However, doing so would require -// duplicating getter methods for both `Function` and `TypeAlias`. -// As a result, it is better to leave it as is. -fn register_type_alias( - item: &mut TypeAlias, - tracker: &mut NamespaceTracker, - source_id: usize, -) -> Result<(), RichError> { - item.set_file_id(source_id); - - let name = item.name(); - let local_id = (name.clone(), source_id); - - if tracker.memo.contains(&local_id) { - return Err(RichError::new( - Error::RedefinedAlias { name: name.clone() }, - *item.span(), - )); - } - - tracker.memo.insert(local_id); - tracker.resolutions[source_id].insert(name.clone(), item.visibility().clone()); - Ok(()) -} - -fn register_function( - item: &mut Function, - tracker: &mut NamespaceTracker, - source_id: usize, -) -> Result<(), RichError> { - item.set_file_id(source_id); - - let name = item.name(); - let local_id = (name.clone(), source_id); - - if name.as_inner() == MAIN_STR && matches!(item.visibility(), Visibility::Public) { - return Err(RichError::new(Error::MainCannotBePublic, *item.span())); - } - - if tracker.memo.contains(&local_id) { - return Err(RichError::new( - Error::FunctionRedefined { name: name.clone() }, - *item.span(), - )); - } - - tracker.memo.insert(local_id); - tracker.resolutions[source_id].insert(name.clone(), item.visibility().clone()); - Ok(()) -} - -/// Helper struct, that tracks the resolution state, imports, and memoization for a single namespace. -#[derive(Clone, Debug)] -struct NamespaceTracker { - /// Local resolutions per file. - resolutions: Vec>, - - /// Global registry for `use` imports and aliasing. - registry: ImportRegistry, - - /// Tracks processed items to prevent infinite loops or redefinitions. - memo: BTreeSet>, -} - -impl NamespaceTracker { - pub fn new(module_count: usize) -> Self { - Self { - resolutions: vec![HashMap::new(); module_count], - registry: ImportRegistry::::default(), - memo: BTreeSet::new(), - } + (!handler.has_errors()) + .then(|| parse::Program::new(&items, *self.modules[MAIN_MODULE].program.as_ref())) } - pub fn into_symbol_table(self) -> SymbolTable { - SymbolTable { - local_scopes: self - .resolutions - .into_iter() - .map(|map| map.into_keys().collect::>()) - .collect::>() - .into(), - imports: self.registry, - } - } -} - -impl Default for ImportRegistry { - fn default() -> Self { - Self { - direct_targets: BTreeMap::new(), - resolved_roots: BTreeMap::new(), - } - } -} - -impl Default for SymbolTable { - fn default() -> Self { - Self { - local_scopes: Arc::from([]), - imports: ImportRegistry::::default(), - } - } -} - -impl AsRef for Program { - fn as_ref(&self) -> &Span { - &self.span - } -} - -#[cfg(test)] -mod resolve_order_tests { - use crate::driver::tests::setup_graph; - - use super::*; - - #[test] - fn test_local_definitions_visibility() { - // main.simf defines a private function and a public function. - // Expected: Both should appear in the scope with correct visibility. - - let (graph, ids, _dir) = setup_graph(vec![( - "main.simf", - "fn private_fn() {} pub fn public_fn() {}", - )]); - - let mut error_handler = ErrorCollector::new(); - let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); - - let Some(program) = program_option else { - panic!("{}", error_handler); - }; - - let root_id = ids["main"]; - let resolutions = &program.functions.local_scopes[root_id]; - - resolutions - .get(&FunctionName::from_str_unchecked("private_fn")) - .expect("private_fn missing"); - - resolutions - .get(&FunctionName::from_str_unchecked("public_fn")) - .expect("public_fn missing"); - } - - #[test] - fn test_pub_use_propagation() { - // Scenario: Re-exporting. - // 1. A.simf defines `pub fn foo`. - // 2. B.simf imports it and re-exports it via `pub use`. - // 3. main.simf imports it from B. - // Expected: B's scope must contain `foo` marked as Public. - - let (graph, ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub fn foo() {}"), - ("libs/lib/B.simf", "pub use crate::A::foo;"), - ("main.simf", "use lib::B::foo;"), - ]); - - let mut error_handler = ErrorCollector::new(); - let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); - - let Some(program) = program_option else { - panic!("{}", error_handler); - }; - - let id_b = ids["B"]; - let id_root = ids["main"]; - - // Check B's scope - program.functions.local_scopes[id_b] - .get(&FunctionName::from_str_unchecked("foo")) - .expect("foo missing in B"); - - // Check Root's scope - program.functions.local_scopes[id_root] - .get(&FunctionName::from_str_unchecked("foo")) - .expect("foo missing in Root"); - } - - #[test] - fn test_private_import_encapsulation_error() { - // Scenario: Access violation. - // 1. A.simf defines `pub fn foo`. - // 2. B.simf imports it via `use` (Private import). - // 3. main.simf tries to import `foo` from B. - // Expected: Error, because B did not re-export foo. - - let (graph, _ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub fn foo() {}"), - ("libs/lib/B.simf", "use crate::A::foo;"), // <--- Private binding! - ("main.simf", "use lib::B::foo;"), // <--- Should fail - ]); - - let mut error_handler = ErrorCollector::new(); - let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); - - assert!( - program_option.is_none(), - "Build should fail and return None when importing a private binding" - ); - - assert!(error_handler - .to_string() - .contains(&"Item `foo` is private".to_string())); - } - - #[test] - fn test_separated_type_aliases_and_functions() { - let (graph, ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub type bar = u32; pub fn bar() {}"), - ("main.simf", "use lib::A::bar;"), - ]); - - let mut error_handler = ErrorCollector::new(); - let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); - - let Some(program) = program_option else { - panic!("{}", error_handler); - }; - - let root_id = ids["main"]; - - // Check B's scope - program.functions.local_scopes[root_id] - .get(&FunctionName::from_str_unchecked("bar")) - .expect("Function bar missing in main"); - - // Check Root's scope - program.aliases.local_scopes[root_id] - .get(&AliasName::from_str_unchecked("bar")) - .expect("Type alias missing in main"); - } - - #[test] - fn test_private_alias_error_does_not_mask_duplicate_function_import() { - // Scenario: - // main.simf: load function `foo` from A.simf. - // Then try to load both `fn foo` and `type foo`. - // However, we have already loade `fn foo` and `type foo` is private, so an error occurs. - let (graph, _ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub fn foo() {}"), - ("libs/lib/B.simf", "pub fn foo() {} type foo = u32;"), - ("main.simf", "use lib::A::foo; use lib::B::foo;"), - ]); - - let mut error_handler = ErrorCollector::new(); - let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); - let _errors = error_handler.to_string(); - - assert!( - program_option.is_none(), - "build should fail when a second import reuses the function name `foo`" - ); - } - - #[test] - fn test_public_main_is_forbidden() { - // Scenario: A user tries to declare the entry point as `pub fn main`. - // Expected: The compiler must reject this because `main` must be private. - - let (graph, _ids, _dir) = setup_graph(vec![("main.simf", "pub fn main() {}")]); - - let mut error_handler = ErrorCollector::new(); - let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); - - assert!( - program_option.is_none(), - "Compiler should return None when `main` is declared public" - ); - - let error_msg = error_handler.to_string(); - assert!( - error_msg.contains("main") && error_msg.contains("public"), - "Error message should mention that `main` cannot be public. Got: {}", - error_msg - ); - } - - #[test] - fn test_aliasing_to_main_is_forbidden() { - // Scenario: A user tries to bypass entry point rules by renaming an import to `main`. - // Expected: The compiler must reject this because `main` is a reserved identifier. - - let (graph, _ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub type bar = u32;"), - ("main.simf", "use lib::A::bar as main;"), - ]); - - let mut error_handler = ErrorCollector::new(); - let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); - - assert!( - program_option.is_none(), - "Compiler should return None when a user tries to alias an import to `main`" - ); - - let error_msg = error_handler.to_string(); - assert!( - error_msg.contains("main") && error_msg.contains("alias"), - "Error message should clearly state that `main` cannot be used as an alias. Got: {}", - error_msg - ); - } -} - -#[cfg(test)] -mod alias_tests { - use super::*; - use crate::driver::tests::setup_graph; - - #[test] - fn test_renaming_with_use() { - // Scenario: Renaming imports. - // main.simf: use lib::A::foo as bar; - // Expected: Scope should contain "bar", but not "foo". - - let (graph, ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub fn foo() {}"), - ("main.simf", "use lib::A::foo as bar;"), - ]); - - let mut error_handler = ErrorCollector::new(); - let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); - - let Some(program) = program_option else { - panic!("{}", error_handler); - }; - - let id_root = ids["main"]; - let scope = &program.functions.local_scopes[id_root]; - - assert!( - scope - .get(&FunctionName::from_str_unchecked("foo")) - .is_none(), - "Original name 'foo' should not be in scope" - ); - assert!( - scope - .get(&FunctionName::from_str_unchecked("bar")) - .is_some(), - "Alias 'bar' should be in scope" - ); - } - - #[test] - fn test_multiple_aliases_in_list() { - // Scenario: Renaming multiple imports inside brackets. - let (graph, ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub fn foo() {} pub fn baz() {}"), - ("main.simf", "use lib::A::{foo as bar, baz as qux};"), - ]); - - let mut error_handler = ErrorCollector::new(); - let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); - - let Some(program) = program_option else { - panic!("{}", error_handler); - }; - - let id_root = ids["main"]; - let scope = &program.functions.local_scopes[id_root]; - - // The original names should NOT be in scope - assert!(scope - .get(&FunctionName::from_str_unchecked("foo")) - .is_none()); - assert!(scope - .get(&FunctionName::from_str_unchecked("baz")) - .is_none()); - - // The aliases MUST be in scope - assert!(scope - .get(&FunctionName::from_str_unchecked("bar")) - .is_some()); - assert!(scope - .get(&FunctionName::from_str_unchecked("qux")) - .is_some()); - } - - #[test] - fn test_alias_private_item_fails() { - // Scenario: Attempting to alias a private item should fail. - let (graph, _ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "fn secret() {}"), // Note: Missing `pub` - ("main.simf", "use lib::A::secret as my_secret;"), - ]); - - let mut error_handler = ErrorCollector::new(); - let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); - - assert!( - program_option.is_none(), - "Compiler should emit an error and return None when aliasing a private item" - ); - - assert!( - error_handler - .to_string() - .contains("Item `secret` is private"), - "Error should mention the private item restriction" - ); - } - - #[test] - fn test_deep_reexport_with_aliases() { - // Scenario: Chaining aliases across multiple files. - let (graph, ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub fn original() {}"), - ("libs/lib/B.simf", "pub use crate::A::original as middle;"), - ("main.simf", "use lib::B::middle as final_name;"), - ]); - - let mut error_handler = ErrorCollector::new(); - let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); - - let Some(program) = program_option else { - panic!("{}", error_handler); - }; - - let id_b = ids["B"]; - let id_root = ids["main"]; - - // Assert Main Scope - let main_scope = &program.functions.local_scopes[id_root]; - assert!(main_scope - .get(&FunctionName::from_str_unchecked("original")) - .is_none()); - assert!(main_scope - .get(&FunctionName::from_str_unchecked("middle")) - .is_none()); - assert!( - main_scope - .get(&FunctionName::from_str_unchecked("final_name")) - .is_some(), - "Main must see the final alias" - ); - - // Assert B Scope (It should have the intermediate alias!) - let b_scope = &program.functions.local_scopes[id_b]; - assert!( - b_scope - .get(&FunctionName::from_str_unchecked("middle")) - .is_some(), - "File B must contain its own public alias" - ); - } - - #[test] - fn test_deep_reexport_private_link_fails() { - // Scenario: Main tries to import an alias from B, but B's alias is private! - let (graph, _ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub fn target() {}"), - // Note: Missing `pub` keyword here! This makes `hidden_alias` private to B. - ("libs/lib/B.simf", "use crate::A::target as hidden_alias;"), - ("main.simf", "use lib::B::hidden_alias;"), - ]); - - let mut error_handler = ErrorCollector::new(); - let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); - - assert!( - program_option.is_none(), - "Compiler must return None when trying to import a private alias from an intermediate module" - ); - - assert!( - error_handler - .to_string() - .contains("Item `hidden_alias` is private"), - "Error should correctly identify the private intermediate alias" - ); - } - - #[test] - fn test_alias_cycle_detection() { - // Scenario: A malicious or confused user creates an infinite alias/import loop. - let (graph, _ids, _dir) = setup_graph(vec![ - // A imports from B, B imports from A. This creates a file-level cycle! - ("libs/lib/A.simf", "pub use crate::B::pong as ping;"), - ("libs/lib/B.simf", "pub use crate::A::ping as pong;"), - ("main.simf", "use lib::A::ping;"), - ]); - - let mut error_handler = ErrorCollector::new(); - - // Because A and B depend on each other, `linearize()` should catch the cycle - // and return an Err(...) directly, rather than causing a Stack Overflow. - let result = graph.linearize_and_build(&mut error_handler); - - match result { - Err(e) => { - println!("{e}"); - assert!( - e.contains("Cycle") || e.contains("Circular"), - "DFS Linearizer must catch infinite alias cycles" - ); + /// Rewrites a single item for the flattened single-file representation. + fn rewrite_item(&self, source_id: usize, item: &parse::Item) -> Option { + match item { + parse::Item::TypeAlias(alias) => { + let mut alias = alias.clone(); + alias.set_file_id(source_id); + Some(parse::Item::TypeAlias(alias)) } - Ok(None) => { - assert!( - error_handler.has_errors(), - "If linearization passes, the builder must catch the cycle" - ); + parse::Item::Function(function) => { + let mut function = function.clone(); + function.set_file_id(source_id); + Some(parse::Item::Function(function)) } - Ok(Some(_)) => { - panic!("Expected compilation to fail due to a dependency cycle, but it succeeded!") + parse::Item::Use(use_decl) => Some(self.rewrite_use(use_decl)), + parse::Item::Module(module) => { + let items: Vec = module + .items() + .iter() + .filter_map(|inner_item| self.rewrite_item(source_id, inner_item)) + .collect(); + + Some(parse::Item::Module(parse::Module::new( + source_id, + module.visibility().clone(), + module.name().clone(), + &items, + ))) } + parse::Item::Ignored => None, } } - #[test] - fn test_plain_import_and_alias_to_same_name_is_rejected() { - let (graph, _ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub fn foo() {}"), - ("libs/lib/B.simf", "pub fn foo() {}"), - ("main.simf", "use lib::A::foo; use lib::B::foo as foo;"), - ]); - - let mut error_handler = ErrorCollector::new(); - let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); - - assert!( - program_option.is_none(), - "build should fail when two imports bind the same local name" - ); - assert!( - error_handler - .to_string() - .contains("The alias `foo` was defined multiple times"), - "expected a duplicate-alias diagnostic" - ); - } - - #[test] - fn test_failed_alias_import_does_not_poison_following_imports() { - let (graph, _ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub fn nope() {}"), - ("libs/lib/B.simf", "pub fn bar() {}"), - ( - "main.simf", - "use lib::A::missing as foo; use lib::B::bar as foo;", - ), - ]); - - let mut error_handler = ErrorCollector::new(); - let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); - let errors = error_handler.to_string(); - - assert!( - program_option.is_none(), - "build should fail on the unresolved import" - ); - assert!(errors.contains("Item `missing` could not be found")); - assert!( - !errors.contains("The alias `foo` was defined multiple times"), - "a failed import must not reserve the alias name" - ); - } - - #[test] - fn test_alias_cannot_reuse_local_definition_name() { - let (graph, _ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub fn bar() {}"), - ("main.simf", "pub fn foo() {} use lib::A::bar as foo;"), - ]); - - let mut error_handler = ErrorCollector::new(); - let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); - - dbg!(&error_handler.to_string()); - - assert!( - program_option.is_none(), - "build should fail when an alias reuses a local definition name" - ); - assert!( - error_handler - .to_string() - .contains("Item `foo` was defined multiple times"), - "expected a redefined-item diagnostic" - ); - } - - #[test] - fn test_local_function_cannot_reuse_alias_name() { - let (graph, _ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub fn bar() {}"), - ("main.simf", "use lib::A::bar as foo; pub fn foo() {}"), - ]); - - let mut error_handler = ErrorCollector::new(); - let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); - - assert!( - program_option.is_none(), - "build should fail when a local definition reuses an alias name" - ); - - assert!( - error_handler - .to_string() - .contains("Function `foo` was defined multiple times"), - "expected a redefined-item diagnostic" - ); - } - - #[test] - fn test_local_type_alias_cannot_reuse_alias_name() { - let (graph, _ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub type bar = u32;"), - ("main.simf", "use lib::A::bar as foo; type foo = u64;"), - ]); - - let mut error_handler = ErrorCollector::new(); - let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); + /// Rewrites a `use` declaration by replacing the drp alias with the canonical + /// `file_N` module name, prepending it to the remaining `mod_path` from the cache. + /// If the target is the `MAIN_MODULE`, the `file_N` segment is safely omitted. + /// + /// ## Example + /// + /// `use base_math::simple_op::hash` into `use file_2::hash` + /// `use crate::inline_mod::item` into `use crate::inline_mod::item` + fn rewrite_use(&self, use_decl: &parse::UseDecl) -> parse::Item { + let resolved = &self.use_cache[use_decl]; + let target_id = self.lookup[&resolved.path]; - assert!( - program_option.is_none(), - "build should fail when a local definition reuses an alias name" - ); + let mut new_path = Vec::with_capacity(resolved.mod_path.len() + 2); + new_path.push(Identifier::from_str_unchecked(CRATE_STR)); - assert!( - error_handler - .to_string() - .contains("Type alias `foo` was defined multiple times"), - "expected a redefined-item diagnostic" - ); + if target_id != MAIN_MODULE { + new_path.push(Self::get_module_name(target_id)); + } + new_path.extend(resolved.mod_path.iter().cloned()); + + parse::Item::Use(parse::UseDecl::new( + use_decl.visibility().clone(), + &new_path, + use_decl.items().clone(), + *use_decl.span(), + )) } } diff --git a/src/lib.rs b/src/lib.rs index 8ab0d1eb..c3a8c776 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,6 +59,22 @@ pub struct TemplateProgram { } impl TemplateProgram { + /// Parses and flattens a multi-file program into a single enriched [`parse::Program`] + /// with all imports resolved and each file wrapped in a `unit_N` module. + /// + /// ## Errors + /// + /// The string is not a valid SimplicityHL program. + pub fn flatten( + source: CanonSourceFile, + dependency_map: &DependencyMap, + ) -> Result { + let mut error_handler = ErrorCollector::new(); + Self::dependency_helper(source, dependency_map, &mut error_handler)? + .ok_or_else(|| error_handler.to_string()) + .map(|p| p.to_string()) + } + /// Parse the template of a SimplicityHL program. /// /// ## Errors @@ -71,25 +87,10 @@ impl TemplateProgram { ) -> Result { let mut error_handler = ErrorCollector::new(); - // 1. Parse root file - let parsed_program = - parse::Program::parse_from_str_with_errors(source.clone(), &mut error_handler) + let driver_program = + Self::dependency_helper(source.clone(), dependency_map, &mut error_handler)? .ok_or_else(|| error_handler.to_string())?; - // 2. Create the driver program - let graph = DependencyGraph::new( - source.clone(), - Arc::from(dependency_map.clone()), - &parsed_program, - &mut error_handler, - )? - .ok_or_else(|| error_handler.to_string())?; - - let driver_program: driver::Program = graph - .linearize_and_build(&mut error_handler)? - .ok_or_else(|| error_handler.to_string())?; - - // 3. AST Analysis let ast_program = ast::Program::analyze(&driver_program, jet_hinter.clone_box()) .with_source(source.clone())?; Ok(Self { @@ -112,14 +113,9 @@ impl TemplateProgram { let source = SourceFile::anonymous(file.clone()); let mut error_handler = ErrorCollector::new(); let parse_program = parse::Program::parse_from_str_with_errors(source, &mut error_handler); + dbg!(&file); - let driver_program = if let Some(parse_program) = parse_program { - driver::Program::from_parse(&parse_program, file.clone(), &mut error_handler) - } else { - None - }; - - if let Some(program) = driver_program { + if let Some(program) = parse_program { let ast_program = ast::Program::analyze(&program, jet_hinter.clone_box()) .with_content(Arc::clone(&file))?; Ok(Self { @@ -128,10 +124,23 @@ impl TemplateProgram { jet_hinter, }) } else { - Err(ErrorCollector::to_string(&error_handler))? + Err(error_handler.to_string())? } } + fn dependency_helper( + source: CanonSourceFile, + dependency_map: &DependencyMap, + handler: &mut ErrorCollector, + ) -> Result, String> { + let program = parse::Program::parse_from_str_with_errors(source.clone(), handler) + .ok_or_else(|| handler.to_string())?; + let graph = + DependencyGraph::new(source, Arc::from(dependency_map.clone()), &program, handler)? + .ok_or_else(|| handler.to_string())?; + graph.linearize_and_build(handler) + } + /// Access the parameters of the program. pub fn parameters(&self) -> &Parameters { self.simfony.parameters() @@ -1390,7 +1399,7 @@ mod functional_tests { } #[test] - #[should_panic(expected = "The alias `add` was defined multiple times")] + #[should_panic(expected = "Item `add` was defined multiple times")] fn name_collision_error() { run_dependency_test( format!("{}/name-collision", ERROR_TESTS_DIR).as_str(), @@ -1430,7 +1439,6 @@ mod functional_tests { } #[test] - #[ignore = "TODO: Enable this once module resolution is complete"] #[should_panic(expected = "not found")] fn crate_file_not_found_error() { run_multidep_test( diff --git a/src/parse.rs b/src/parse.rs index 9390df7b..7a269532 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -39,6 +39,14 @@ pub struct Program { } impl Program { + // Need for driver usage + pub(crate) fn new(items: &[Item], span: Span) -> Self { + Self { + items: Arc::from(items), + span, + } + } + /// Access the items of the program. pub fn items(&self) -> &[Item] { &self.items @@ -101,6 +109,21 @@ pub struct UseDecl { } impl UseDecl { + /// The driver uses this to ensure imports conform to the flattened program structure. + pub(crate) fn new( + visibility: Visibility, + path: &[Identifier], + items: UseItems, + span: Span, + ) -> Self { + Self { + visibility, + path: Vec::from(path), + items, + span, + } + } + pub fn visibility(&self) -> &Visibility { &self.visibility } @@ -633,6 +656,22 @@ pub struct Module { } impl Module { + /// Needed by the driver to wrap a single file into a module. + pub(crate) fn new( + file_id: usize, + visibility: Visibility, + name: ModuleName, + items: &[Item], + ) -> Module { + Self { + file_id, + visibility, + name, + items: Arc::from(items), + span: Span::new(0, 0), + } + } + pub fn visibility(&self) -> &Visibility { &self.visibility } diff --git a/src/witness.rs b/src/witness.rs index 3fd878d4..ae8b0581 100644 --- a/src/witness.rs +++ b/src/witness.rs @@ -214,10 +214,9 @@ impl crate::ArbitraryOfType for Arguments { mod tests { use super::*; use crate::ast::ElementsJetHinter; - use crate::error::ErrorCollector; use crate::parse::ParseFromStr; use crate::value::ValueConstructible; - use crate::{ast, driver, parse, CompiledProgram, SatisfiedProgram}; + use crate::{ast, parse, CompiledProgram, SatisfiedProgram}; #[test] fn witness_reuse() { @@ -225,15 +224,7 @@ mod tests { assert!(jet::eq_32(witness::A, witness::A)); }"#; let parse_program = parse::Program::parse_from_str(s).expect("parsing works"); - - let mut error_collector = ErrorCollector::new(); - let driver_program = driver::resolve_order::Program::from_parse( - &parse_program, - Arc::from(s), - &mut error_collector, - ) - .expect("driver works"); - match ast::Program::analyze(&driver_program, Box::new(ElementsJetHinter::new())) + match ast::Program::analyze(&parse_program, Box::new(ElementsJetHinter::new())) .map_err(Error::from) { Ok(_) => panic!("Witness reuse was falsely accepted"), From 21979a57268127b3b691a49788625c895317282e Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Tue, 2 Jun 2026 17:09:04 +0300 Subject: [PATCH 07/10] feat: add new tests for `driver/resolve_order.rs` file --- src/driver/resolve_order.rs | 215 ++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) diff --git a/src/driver/resolve_order.rs b/src/driver/resolve_order.rs index 69cfbff0..04ef86a5 100644 --- a/src/driver/resolve_order.rs +++ b/src/driver/resolve_order.rs @@ -116,3 +116,218 @@ impl DependencyGraph { )) } } + +#[cfg(test)] +mod flattening_tests { + use crate::driver::tests::setup_graph; + use crate::driver::CRATE_STR; + use crate::error::ErrorCollector; + use crate::parse::{self, Visibility}; + + use std::collections::HashMap; + + // Helper to get the built program + fn build_flattened_program( + files: Vec<(&str, &str)>, + ) -> (parse::Program, HashMap) { + let (graph, ids, _dir) = setup_graph(files); + let mut error_handler = ErrorCollector::new(); + + let program = graph + .linearize_and_build(&mut error_handler) + .expect("Linearize should not fail in this test") + .expect("Build should succeed and return Some(Program)"); + + (program, ids) + } + + #[test] + fn test_main_module_is_not_wrapped() { + // Scenario: The entry file should have its items injected directly into + // the root of the AST, NOT wrapped in a `mod file_0` block. + let (program, _) = build_flattened_program(vec![( + "main.simf", + "pub fn root_func() {} type root_type = u32;", + )]); + + let items = program.items(); + + // Ensure there are no Module wrappers at the root level + let has_modules = items + .iter() + .any(|item| matches!(item, parse::Item::Module(_))); + assert!( + !has_modules, + "Main module items should not be wrapped in a mod block" + ); + + // Ensure the items are directly present + let has_func = items.iter().any( + |item| matches!(item, parse::Item::Function(f) if f.name().as_inner() == "root_func"), + ); + assert!( + has_func, + "root_func must be injected directly into the root" + ); + } + + #[test] + fn test_dependency_is_wrapped_in_file_module() { + // Scenario: A dependency file MUST be wrapped in a `mod file_N` block, + // and its visibility must be Private to prevent leaking. + let (program, ids) = build_flattened_program(vec![ + ("libs/lib/A.simf", "pub fn dep_func() {}"), + ("main.simf", "use lib::A::dep_func;"), + ]); + + let file_a_id = ids["A"]; + let expected_mod_name = format!("unit_{}", file_a_id); + + let wrapped_module = program + .items() + .iter() + .find_map(|item| { + if let parse::Item::Module(m) = item { + if m.name().as_inner() == expected_mod_name.as_str() { + return Some(m); + } + } + None + }) + .expect("Dependency should be wrapped in a file_N module"); + + assert!( + matches!(wrapped_module.visibility(), Visibility::Private), + "The file wrapper module must be strictly private" + ); + + let has_dep_func = wrapped_module.items().iter().any( + |item| matches!(item, parse::Item::Function(f) if f.name().as_inner() == "dep_func"), + ); + assert!( + has_dep_func, + "The file_N module must contain the dependency's items" + ); + } + + #[test] + fn test_use_paths_are_rewritten_to_canonical_files() { + // Scenario: When main.simf says `use lib::A::foo`, the AST flattener + // must rewrite this path to `use crate::file_N::foo`. + let (program, ids) = build_flattened_program(vec![ + ("libs/lib/A.simf", "pub fn foo() {}"), + ("main.simf", "use lib::A::foo;"), + ]); + + let file_a_id = ids["A"]; + let expected_file_segment = format!("unit_{}", file_a_id); + + let use_decl = program + .items() + .iter() + .find_map(|item| { + if let parse::Item::Use(u) = item { + Some(u) + } else { + None + } + }) + .expect("Main module should contain a use declaration"); + + // Get the segments of the rewritten path + let path = use_decl.path(); + + assert!( + path.len() >= 2, + "Rewritten path must have at least 2 segments" + ); + assert_eq!( + path[0].as_inner(), + CRATE_STR, + "Path must start with `crate`" + ); + assert_eq!( + path[1].as_inner(), + expected_file_segment.as_str(), + "Path must route through the canonical `file_N`" + ); + } +} + +#[cfg(test)] +mod dependency_map_tests { + use crate::driver::tests::setup_graph; + use crate::error::ErrorCollector; + + // Helper to run the driver and return the error collector so we can inspect it. + fn run_driver(files: Vec<(&str, &str)>) -> ErrorCollector { + let (graph, _ids, _dir) = setup_graph(files); + let mut error_handler = ErrorCollector::new(); + let _ = graph.linearize_and_build(&mut error_handler).unwrap(); + error_handler + } + + #[test] + fn test_crate_path_resolves_to_physical_file() { + // Scenario: `crate::utils::math` should map to the physical `utils/math.simf` file. + let errors = run_driver(vec![ + ("utils/math.simf", "pub fn add() {}"), + ("main.simf", "use crate::utils::math::add; fn main() {}"), + ]); + + assert!( + !errors.has_errors(), + "Driver should successfully find the physical file 'utils/math.simf'. Errors: {errors}" + ); + } + + #[test] + fn test_crate_path_fallback_to_inline_module() { + // Scenario: `brother.simf` does NOT exist. `crate::brother` must fallback + // to `main.simf` and treat `brother` as an inline mod_path. + let errors = run_driver(vec![( + "main.simf", + " + mod brother { pub fn toy() {} } + use crate::brother::toy; + fn main() {} + ", + )]); + + assert!(!errors.has_errors(), "Driver must fallback to main.simf for inline modules without throwing FileNotFound. Errors: {errors}"); + } + + #[test] + fn test_crate_path_deeply_nested_inline_fallback() { + // Scenario: A physical file exists (`utils.simf`), but the REST of the path is inline modules! + let errors = run_driver(vec![ + ( + "utils.simf", + "pub mod deeply { pub mod nested { pub fn func() {} } }", + ), + ( + "main.simf", + "use crate::utils::deeply::nested::func; fn main() {}", + ), + ]); + + assert!( + !errors.has_errors(), + "Driver must split the path at the file boundary correctly. Errors: {errors}" + ); + } + + #[test] + fn test_external_dependency_resolution() { + // Scenario: Resolving `use lib::A::foo` across the remapping boundary. + let errors = run_driver(vec![ + ("libs/lib/A.simf", "pub fn foo() {}"), + ("main.simf", "use lib::A::foo; fn main() {}"), + ]); + + assert!( + !errors.has_errors(), + "External dependency resolution via drp_name failed. Errors: {errors}" + ); + } +} From e7e6eb4d14ef574341de409365b5c2f9f1600886 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Tue, 2 Jun 2026 17:11:13 +0300 Subject: [PATCH 08/10] feat: add module, alias and scope tests for `ast.rs` --- src/ast.rs | 524 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 524 insertions(+) diff --git a/src/ast.rs b/src/ast.rs index 6f071f63..ff7b3b30 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1806,3 +1806,527 @@ impl AsRef for Match { &self.span } } + +#[cfg(test)] +mod scope_resolution_tests { + use super::{ElementsJetHinter, Program}; + use crate::driver::tests::setup_graph; + use crate::error::ErrorCollector; + + pub(super) fn analyze_multifile(files: Vec<(&str, &str)>) -> Result<(), String> { + let (graph, _ids, _dir) = setup_graph(files); + + let mut error_handler = ErrorCollector::new(); + let driver_program = graph + .linearize_and_build(&mut error_handler) + .unwrap() + .expect("driver build should succeed"); + + Program::analyze(&driver_program, Box::new(ElementsJetHinter)) + .map(|_| ()) + .map_err(|e| e.to_string()) + } + + #[test] + fn private_type_alias_from_dependency_does_not_leak() { + let result = analyze_multifile(vec![ + ( + "main.simf", + "use lib::A::helper; fn main() { helper(); let x: Secret = 0; }", + ), + ("libs/lib/A.simf", "type Secret = u32; pub fn helper() {}"), + ]); + + assert!( + result.is_err(), + "private alias from another file leaked into root scope: {result:?}" + ); + } + + #[test] + fn same_alias_name_in_different_modules_does_not_conflict_if_only_one_is_imported() { + let result = analyze_multifile(vec![ + ( + "main.simf", + "use lib::A::Word; use lib::B::id; fn main() { let x: Word = 0; assert!(jet::is_zero_32(id(x))); }", + ), + ("libs/lib/A.simf", "pub type Word = u32;"), + ("libs/lib/B.simf", "pub type Word = u16; pub fn id(x: u32) -> u32 { x }"), + ]); + + assert!( + result.is_ok(), + "unimported alias from another module should not collide: {result:?}" + ); + } + + #[test] + fn dependency_main_does_not_satisfy_missing_root_main() { + let result = analyze_multifile(vec![ + ("main.simf", "use lib::A::helper;"), + ("libs/lib/A.simf", "fn main() {} pub fn helper() {}"), + ]); + + assert!( + result.is_err(), + "Main function must be inside an entry file: {result:?}" + ) + } + + #[test] + fn main_must_be_defined_once_per_project() { + let result = analyze_multifile(vec![ + ("main.simf", "use lib::A::helper; fn main() { helper(); }"), + ("libs/lib/A.simf", "fn main() {} pub fn helper() {}"), + ]); + + assert!( + result.is_err(), + "Main function must be inside an entry file: {result:?}" + ); + } + + #[test] + fn test_local_definitions_visibility() { + // main.simf defines a private function and a public function. + // Expected: Both should be usable locally in main. + let result = analyze_multifile(vec![( + "main.simf", + "fn private_fn() {} pub fn public_fn() {} fn main() { private_fn(); public_fn(); }", + )]); + + assert!( + result.is_ok(), + "Local definitions should be visible: {result:?}" + ); + } + + #[test] + fn test_pub_use_propagation() { + // Scenario: Re-exporting. + let result = analyze_multifile(vec![ + ("libs/lib/A.simf", "pub fn foo() {}"), + ("libs/lib/B.simf", "pub use crate::A::foo;"), + ("main.simf", "use lib::B::foo; fn main() { foo(); }"), + ]); + + assert!( + result.is_ok(), + "Public re-exports must be visible: {result:?}" + ); + } + + #[test] + fn test_private_import_encapsulation_error() { + // Scenario: A private import cannot be re-exported. + let result = analyze_multifile(vec![ + ("libs/lib/A.simf", "pub fn foo() {}"), + ("libs/lib/B.simf", "use crate::A::foo;"), // <--- Private binding! + ("main.simf", "use lib::B::foo; fn main() {}"), + ]); + + let err = result.expect_err("Private imports should not be accessible"); + assert!(err.contains("private") || err.contains("foo")); + } + + #[test] + fn test_separated_type_aliases_and_functions() { + let result = analyze_multifile(vec![ + ("libs/lib/A.simf", "pub type bar = u32; pub fn bar() {}"), + ( + "main.simf", + "use lib::A::bar; fn main() { bar(); let x: bar = 0; }", + ), + ]); + + assert!( + result.is_ok(), + "AST should support separate namespaces for types and functions: {result:?}" + ); + } + + #[test] + fn test_public_main_is_forbidden() { + let result = analyze_multifile(vec![("main.simf", "pub fn main() {}")]); + + let err = result.expect_err("Public main should be rejected"); + assert!(err.contains("Main") && err.contains("public")); + } + + #[test] + fn test_aliasing_to_main_is_forbidden() { + let result = analyze_multifile(vec![ + ("libs/lib/A.simf", "pub type bar = u32;"), + ("main.simf", "use lib::A::bar as main; fn main() {}"), + ]); + + let err = result.expect_err("Aliasing to main should be rejected"); + assert!(err.contains("Main") && err.contains("alias")); + } + + #[test] + fn test_renaming_with_use() { + // Expected: "bar" is usable, "foo" is not. + let result = analyze_multifile(vec![ + ("libs/lib/A.simf", "pub fn foo() {}"), + ( + "main.simf", + "use lib::A::foo as bar; fn main() { bar(); foo(); }", + ), + ]); + + let err = result.expect_err("Using the original unaliased name 'foo' should fail"); + assert!(err.contains("foo") && (err.contains("not defined") || err.contains("unresolved"))); + } + + #[test] + fn test_multiple_aliases_in_list() { + let result = analyze_multifile(vec![ + ("libs/lib/A.simf", "pub fn foo() {} pub fn baz() {}"), + ( + "main.simf", + "use lib::A::{foo as bar, baz as qux}; fn main() { bar(); qux(); }", + ), + ]); + + assert!( + result.is_ok(), + "List aliases should be resolvable: {result:?}" + ); + } + + #[test] + fn test_alias_private_item_fails() { + let result = analyze_multifile(vec![ + ("libs/lib/A.simf", "fn secret() {}"), + ("main.simf", "use lib::A::secret as my_secret; fn main() {}"), + ]); + + let err = result.expect_err("Aliasing a private item should fail"); + assert!(err.contains("secret") && err.contains("private")); + } + + #[test] + fn test_deep_reexport_with_aliases() { + let result = analyze_multifile(vec![ + ("libs/lib/A.simf", "pub fn original() {}"), + ("libs/lib/B.simf", "pub use crate::A::original as middle;"), + ( + "main.simf", + "use lib::B::middle as final_name; fn main() { final_name(); }", + ), + ]); + + assert!( + result.is_ok(), + "Deep alias re-exports should work: {result:?}" + ); + } + + #[test] + fn test_deep_reexport_private_link_fails() { + let result = analyze_multifile(vec![ + ("libs/lib/A.simf", "pub fn target() {}"), + ("libs/lib/B.simf", "use crate::A::target as hidden_alias;"), + ("main.simf", "use lib::B::hidden_alias; fn main() {}"), + ]); + + let err = result.expect_err("Private intermediate aliases should block resolution"); + assert!(err.contains("hidden_alias") && err.contains("private")); + } + + #[test] + fn test_plain_import_and_alias_to_same_name_is_rejected() { + let result = analyze_multifile(vec![ + ("libs/lib/A.simf", "pub fn foo() {}"), + ("libs/lib/B.simf", "pub fn foo() {}"), + ( + "main.simf", + "use lib::A::foo; use lib::B::foo as foo; fn main() {}", + ), + ]); + + let err = result.expect_err("Duplicate names in scope should fail"); + assert!(err.contains("foo") && err.contains("multiple times")); + } + + #[test] + fn test_alias_cannot_reuse_local_definition_name() { + let result = analyze_multifile(vec![ + ("libs/lib/A.simf", "pub fn bar() {}"), + ( + "main.simf", + "pub fn foo() {} use lib::A::bar as foo; fn main() {}", + ), + ]); + + let err = result.expect_err("Alias reusing a local name should fail"); + assert!(err.contains("foo") && err.contains("multiple times")); + } + + #[test] + #[ignore = "Pending better error handler:private item errors currently mask duplicate imports"] + fn test_private_alias_error_does_not_mask_duplicate_function_import() { + // Scenario: Loading a private item fails, but we must STILL catch if a + // secondary import tries to bind to the same name. + let result = analyze_multifile(vec![ + ("libs/lib/A.simf", "pub fn foo() {}"), + ("libs/lib/B.simf", "pub fn foo() {} type foo = u32;"), + ( + "main.simf", + "use lib::A::foo; use lib::B::foo; fn main() {}", + ), + ]); + + let err = result.expect_err("Duplicate function import should fail"); + + // It shouldn't just complain about the private type `foo`; it must also + // complain that `foo` was imported twice! + assert!(err.contains("foo") && err.contains("multiple times")); + } + + #[test] + fn test_failed_alias_import_does_not_poison_following_imports() { + let result = analyze_multifile(vec![ + ("libs/lib/A.simf", "pub fn nope() {}"), + ("libs/lib/B.simf", "pub fn bar() {}"), + ( + "main.simf", + "use lib::A::missing as foo; use lib::B::bar as foo; fn main() {}", + ), + ]); + + let err = result.expect_err("Build should fail on the unresolved import"); + + // It should complain about `missing`, but NOT about `foo` being duplicated, + // because the first import failed and never actually reserved the name `foo`. + assert!(err.contains("missing") || err.contains("not found")); + assert!(!err.contains("multiple times")); + } + + #[test] + fn test_local_function_cannot_reuse_alias_name() { + let result = analyze_multifile(vec![ + ("libs/lib/A.simf", "pub fn bar() {}"), + ( + "main.simf", + "use lib::A::bar as foo; pub fn foo() {} fn main() {}", + ), + ]); + + let err = + result.expect_err("Build should fail when a local definition reuses an alias name"); + assert!(err.contains("foo") && err.contains("multiple times")); + } + + #[test] + fn test_local_type_alias_cannot_reuse_alias_name() { + let result = analyze_multifile(vec![ + ("libs/lib/A.simf", "pub type bar = u32;"), + ( + "main.simf", + "use lib::A::bar as foo; type foo = u64; fn main() {}", + ), + ]); + + let err = + result.expect_err("Build should fail when a local definition reuses an alias name"); + assert!(err.contains("foo") && err.contains("multiple times")); + } +} + +#[cfg(test)] +mod module_tests { + use crate::ast::scope_resolution_tests::analyze_multifile; + + #[test] + fn test_public_nested_modules_are_accessible() { + let result = analyze_multifile(vec![ + ( + "libs/lib/A.simf", + "pub mod outer { pub mod inner { pub fn target() {} } }", + ), + ( + "main.simf", + "use lib::A::outer::inner::target; fn main() {}", + ), + ]); + + assert!( + result.is_ok(), + "Deeply nested public modules should be accessible: {result:?}" + ); + } + + #[test] + fn test_private_inner_module_blocks_external_access() { + let result = analyze_multifile(vec![ + // `outer` is public, but `inner` is private + // Even though `target` is public, the private wall at `inner` blocks it. + ( + "libs/lib/A.simf", + "pub mod outer { mod inner { pub fn target() {} } }", + ), + ( + "main.simf", + "use lib::A::outer::inner::target; fn main() {}", + ), + ]); + + let err = result.expect_err("Private inner module must block access"); + assert!(err.contains("inner") && err.contains("private")); + } + + #[test] + #[ignore = "Not implemented now"] + fn test_importing_a_whole_module_allows_path_traversal() { + // Scenario: Instead of importing the function, the user imports the module itself, + // and then uses the module name as a prefix. + let result = analyze_multifile(vec![ + ("libs/lib/A.simf", "pub mod math { pub fn add() {} }"), + ("main.simf", "use lib::A::math; fn main() { math::add(); }"), + ]); + + assert!( + result.is_ok(), + "Importing a module should bring its namespace into scope: {result:?}" + ); + } + + #[test] + fn test_duplicate_module_blocks_are_rejected() { + let result = analyze_multifile(vec![( + "main.simf", + "mod inner {} mod inner {} fn main() {}", + )]); + + let err = result.expect_err("Duplicate mod blocks must fail"); + assert!(err.contains("inner") && err.contains("multiple times")); + } + + #[test] + fn test_sibling_modules_can_access_each_others_public_items() { + // In Rust, sibling modules share the same parent, so they are allowed to see + // each other (even if they are private to the outside world). + let result = analyze_multifile(vec![( + "main.simf", + " + mod brother { pub fn toy() {} } + mod sister { use crate::brother::toy; } + fn main() {} + ", + )]); + + assert!( + result.is_ok(), + "Sibling modules should be able to import from each other: {result:?}" + ); + } + + #[test] + fn test_inline_module_can_import_global_item() { + // Scenario: A nested module needs to access a function defined at the very top of the file. + // This proves `crate::` correctly points to the un-wrapped MAIN_MODULE root. + let result = analyze_multifile(vec![( + "main.simf", + " + pub fn global_func() {} + mod inner { + use crate::global_func; + pub fn call_it() { global_func(); } + } + fn main() {} + ", + )]); + + assert!( + result.is_ok(), + "Nested modules must be able to import global items: {result:?}" + ); + } + + #[test] + fn test_deeply_nested_inline_modules() { + // Scenario: Traversing multiple inline module boundaries. + let result = analyze_multifile(vec![( + "main.simf", + " + mod level1 { + pub mod level2 { + pub fn treasure() {} + } + } + mod explorer { + use crate::level1::level2::treasure; + } + fn main() {} + ", + )]); + + assert!( + result.is_ok(), + "Deeply nested inline modules must resolve correctly: {result:?}" + ); + } + + #[test] + fn test_inline_module_privacy_is_enforced_between_siblings() { + // Scenario: Sibling modules can see each other, but they CANNOT see each other's PRIVATE items. + let result = analyze_multifile(vec![( + "main.simf", + " + mod brother { + fn secret_toy() {} // Missing 'pub' + } + mod sister { + use crate::brother::secret_toy; + } + fn main() {} + ", + )]); + + let err = result.expect_err("Private inline items must remain hidden from siblings"); + assert!(err.contains("secret_toy") && err.contains("private")); + } + + #[test] + fn test_main_scope_cannot_access_private_inline_items() { + // Scenario: The root of the file tries to import a private item from its own child module. + let result = analyze_multifile(vec![( + "main.simf", + " + mod child { + fn hidden() {} + } + use crate::child::hidden; + fn main() {} + ", + )]); + + let err = result.expect_err("The root file scope must respect inline module privacy"); + assert!(err.contains("hidden") && err.contains("private")); + } + + #[test] + fn test_inline_module_alias_import() { + // Scenario: Importing an item from a sibling inline module and renaming it locally. + let result = analyze_multifile(vec![( + "main.simf", + " + mod supplier { + pub fn raw_material() {} + } + mod factory { + use crate::supplier::raw_material as finished_product; + pub fn produce() { finished_product(); } + } + fn main() {} + ", + )]); + + assert!( + result.is_ok(), + "Inline imports must support aliasing: {result:?}" + ); + } +} From d21477b82f4c37bd53da249207b9f384c2137f5d Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Tue, 2 Jun 2026 17:23:43 +0300 Subject: [PATCH 09/10] feat: add examples and flattening tests. Fix bugs in driver --- examples/modules.simf | 26 ++ examples/multiple_deps/flattened.simf | 34 +++ examples/simple_multidep/flattened.simf | 24 ++ examples/single_dep/flattened.simf | 29 ++ .../module-name-collision/lib/module.simf | 1 + .../module-name-collision/main.simf | 3 + .../use-statement-collision/A.simf | 1 + .../use-statement-collision/control.simf | 5 + .../use-statement-collision/flattened.simf | 20 ++ .../use-statement-collision/libs/lib/A.simf | 1 + .../use-statement-collision/libs/lib/B.simf | 3 + .../use-statement-collision/main.simf | 7 + fuzz/fuzz_targets/compile_parse_tree.rs | 17 +- fuzz/fuzz_targets/display_parse_tree.rs | 8 +- src/ast.rs | 72 ++--- src/driver/mod.rs | 68 +++-- src/driver/resolve_order.rs | 109 ++++---- src/lib.rs | 250 +++++++++++++++--- src/parse.rs | 46 ++-- 19 files changed, 546 insertions(+), 178 deletions(-) create mode 100644 examples/modules.simf create mode 100644 examples/multiple_deps/flattened.simf create mode 100644 examples/simple_multidep/flattened.simf create mode 100644 examples/single_dep/flattened.simf create mode 100644 functional-tests/valid-test-cases/module-name-collision/lib/module.simf create mode 100644 functional-tests/valid-test-cases/module-name-collision/main.simf create mode 100644 functional-tests/valid-test-cases/use-statement-collision/A.simf create mode 100644 functional-tests/valid-test-cases/use-statement-collision/control.simf create mode 100644 functional-tests/valid-test-cases/use-statement-collision/flattened.simf create mode 100644 functional-tests/valid-test-cases/use-statement-collision/libs/lib/A.simf create mode 100644 functional-tests/valid-test-cases/use-statement-collision/libs/lib/B.simf create mode 100644 functional-tests/valid-test-cases/use-statement-collision/main.simf diff --git a/examples/modules.simf b/examples/modules.simf new file mode 100644 index 00000000..70979db9 --- /dev/null +++ b/examples/modules.simf @@ -0,0 +1,26 @@ +mod math { + pub mod ops { + pub fn double(x: u32) -> u32 { + let (_, res): (bool, u32) = jet::add_32(x, x); + res + } + } +} + +mod business_logic { + use crate::math::ops::double; + + pub fn calculate_fee(base_price: u32, tax: u32) -> u32 { + let (_, res): (bool, u32) = jet::add_32(double(base_price), tax); + res + } +} + +use crate::business_logic::calculate_fee; +fn main() { + let price: u32 = 15; + let tax: u32 = 5; + + let total: u32 = calculate_fee(price, tax); + assert!(jet::eq_32(total, 35)); +} diff --git a/examples/multiple_deps/flattened.simf b/examples/multiple_deps/flattened.simf new file mode 100644 index 00000000..cd934f4d --- /dev/null +++ b/examples/multiple_deps/flattened.simf @@ -0,0 +1,34 @@ +mod unit_2 { + pub fn hash(x: u32, y: u32) -> u32 { + jet::xor_32(x, y) + } +} + +mod unit_1 { + use crate::unit_2::hash as temp_hash; + pub fn get_root(tx1: u32, tx2: u32) -> u32 { + temp_hash(tx1, tx2) + } + pub fn hash(tx1: u32, tx2: u32) -> u32 { + jet::and_32(tx1, tx2) + } +} + +// main unit +mod unit_0 { + use crate::unit_1::{get_root, hash as and_hash}; + use crate::unit_2::hash as or_hash; + + pub fn get_block_value_hash(prev_hash: u32, tx1: u32, tx2: u32) -> u32 { + let root: u32 = get_root(tx1, tx2); + or_hash(prev_hash, root) + } + + fn main() { + let block_val_hash: u32 = get_block_value_hash(5, 10, 20); + assert!(jet::eq_32(block_val_hash, 27)); + let first_value: u32 = 15; + let second_value: u32 = 22; + assert!(jet::eq_32(and_hash(first_value, second_value), 6)); + } +} \ No newline at end of file diff --git a/examples/simple_multidep/flattened.simf b/examples/simple_multidep/flattened.simf new file mode 100644 index 00000000..8f5e77f2 --- /dev/null +++ b/examples/simple_multidep/flattened.simf @@ -0,0 +1,24 @@ +mod unit_1 { + pub fn add(a: u32, b: u32) -> u32 { + let (_, res): (bool, u32) = jet::add_32(a, b); + res + } +} + +mod unit_2 { + pub fn sha256(data: u32) -> u256 { + let ctx: Ctx8 = jet::sha_256_ctx_8_init(); + let ctx: Ctx8 = jet::sha_256_ctx_8_add_4(ctx, data); + jet::sha_256_ctx_8_finalize(ctx) + } +} + +mod unit_0 { + use crate::unit_1::add; + use crate::unit_2::sha256; + + fn main() { + let sum: u32 = add(2, 3); + let hash: u256 = sha256(sum); + } +} \ No newline at end of file diff --git a/examples/single_dep/flattened.simf b/examples/single_dep/flattened.simf new file mode 100644 index 00000000..ef182924 --- /dev/null +++ b/examples/single_dep/flattened.simf @@ -0,0 +1,29 @@ +mod unit_2 { + pub type Smth = u32; + + pub fn get_five() -> u32 { + 5 + } +} + +mod unit_1 { + pub use crate::unit_2::Smth; + + pub fn two() -> Smth { + 2 + } +} + +mod unit_0 { + pub use crate::unit_1::two as smth; + use crate::unit_2::{get_five, Smth}; + + fn seven() -> u32 { + 7 + } + + fn main() { + let (_, temp): (bool, u32) = jet::add_32(smth(), get_five()); + assert!(jet::eq_32(temp, seven())); + } +} diff --git a/functional-tests/valid-test-cases/module-name-collision/lib/module.simf b/functional-tests/valid-test-cases/module-name-collision/lib/module.simf new file mode 100644 index 00000000..d5cfec25 --- /dev/null +++ b/functional-tests/valid-test-cases/module-name-collision/lib/module.simf @@ -0,0 +1 @@ +pub fn add() {} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/module-name-collision/main.simf b/functional-tests/valid-test-cases/module-name-collision/main.simf new file mode 100644 index 00000000..75102521 --- /dev/null +++ b/functional-tests/valid-test-cases/module-name-collision/main.simf @@ -0,0 +1,3 @@ +use lib::module::add; +mod unit_1 {} +fn main() {} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/use-statement-collision/A.simf b/functional-tests/valid-test-cases/use-statement-collision/A.simf new file mode 100644 index 00000000..2af1be6a --- /dev/null +++ b/functional-tests/valid-test-cases/use-statement-collision/A.simf @@ -0,0 +1 @@ +pub fn foo() -> u32 { 7 } \ No newline at end of file diff --git a/functional-tests/valid-test-cases/use-statement-collision/control.simf b/functional-tests/valid-test-cases/use-statement-collision/control.simf new file mode 100644 index 00000000..8360a3cb --- /dev/null +++ b/functional-tests/valid-test-cases/use-statement-collision/control.simf @@ -0,0 +1,5 @@ +use crate::A::foo; + +fn main() { + assert!(jet::eq_32(foo(), 7)); +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/use-statement-collision/flattened.simf b/functional-tests/valid-test-cases/use-statement-collision/flattened.simf new file mode 100644 index 00000000..6c67b3cb --- /dev/null +++ b/functional-tests/valid-test-cases/use-statement-collision/flattened.simf @@ -0,0 +1,20 @@ +mod unit_1 { + pub fn foo() -> u32 { 7 } +} + +mod unit_3 { + pub fn foo() -> u32 { 8 } +} + +mod unit_2 { + use crate::unit_3::foo; + pub fn bar() {} +} + +mod unit_0 { + use crate::unit_1::foo; + use crate::unit_2::bar; + fn main() { + assert!(jet::eq_32(foo(), 7)); + } +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/use-statement-collision/libs/lib/A.simf b/functional-tests/valid-test-cases/use-statement-collision/libs/lib/A.simf new file mode 100644 index 00000000..8456c23a --- /dev/null +++ b/functional-tests/valid-test-cases/use-statement-collision/libs/lib/A.simf @@ -0,0 +1 @@ +pub fn foo() -> u32 { 8 } \ No newline at end of file diff --git a/functional-tests/valid-test-cases/use-statement-collision/libs/lib/B.simf b/functional-tests/valid-test-cases/use-statement-collision/libs/lib/B.simf new file mode 100644 index 00000000..041fe2a7 --- /dev/null +++ b/functional-tests/valid-test-cases/use-statement-collision/libs/lib/B.simf @@ -0,0 +1,3 @@ +use crate::A::foo; + +pub fn bar() {} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/use-statement-collision/main.simf b/functional-tests/valid-test-cases/use-statement-collision/main.simf new file mode 100644 index 00000000..2293b510 --- /dev/null +++ b/functional-tests/valid-test-cases/use-statement-collision/main.simf @@ -0,0 +1,7 @@ +use crate::A::foo; + +use lib::B::bar; + +fn main() { + assert!(jet::eq_32(foo(), 7)); +} \ 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 8352424c..5b95a7d4 100644 --- a/fuzz/fuzz_targets/compile_parse_tree.rs +++ b/fuzz/fuzz_targets/compile_parse_tree.rs @@ -4,28 +4,17 @@ fn do_test(data: &[u8]) { use arbitrary::Arbitrary; use simplicityhl::ast::ElementsJetHinter; - use std::sync::Arc; - use simplicityhl::error::{ErrorCollector, WithContent}; - use simplicityhl::{ast, driver, named, parse, ArbitraryOfType, Arguments}; + use simplicityhl::error::WithContent; + use simplicityhl::{ast, named, parse, ArbitraryOfType, Arguments}; let mut u = arbitrary::Unstructured::new(data); let parse_program = match parse::Program::arbitrary(&mut u) { Ok(x) => x, Err(_) => return, }; - - let mut error_handler = ErrorCollector::new(); - let driver_program = if let Some(program) = - driver::Program::from_parse(&parse_program, Arc::from(""), &mut error_handler) - { - program - } else { - return; - }; - let ast_program = - match ast::Program::analyze(&driver_program, Box::new(ElementsJetHinter::new())) { + match ast::Program::analyze(&parse_program, Box::new(ElementsJetHinter::new())) { Ok(x) => x, Err(_) => return, }; diff --git a/fuzz/fuzz_targets/display_parse_tree.rs b/fuzz/fuzz_targets/display_parse_tree.rs index 799ff64f..66b00786 100644 --- a/fuzz/fuzz_targets/display_parse_tree.rs +++ b/fuzz/fuzz_targets/display_parse_tree.rs @@ -18,7 +18,12 @@ fn main() {} #[cfg(fuzzing)] libfuzzer_sys::fuzz_target!(|data: simplicityhl::parse::Program| { - do_test(data); + // TODO: Adapt to a multifile program (detailed in https://github.com/BlockstreamResearch/SimplicityHL/issues/350) + // Temporarily disabled to prevent panics during file_id initialization. + + // do_test(data); + + let _ = data; }); #[cfg(test)] @@ -26,6 +31,7 @@ mod test { use simplicityhl::parse::{ParseFromStr, Program}; #[test] + #[ignore] fn test() { let program_test = r#"fn main() { assert!(jet::eq_32(witness::A, witness::A)); diff --git a/src/ast.rs b/src/ast.rs index ff7b3b30..4d269829 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -75,7 +75,7 @@ pub enum Item { /// A function. Function(Function), Use, - Module, + Module(Vec), /// A placeholder used for error recovery during parsing. Ignored, } @@ -699,7 +699,7 @@ impl Scope { /// * May also return errors propagated from item collection and insertion, such as [`Error::PrivateItem`] or [`Error::RedefinedItem`]. pub fn resolve_use(&mut self, use_decl: &UseDecl) -> Result<(), Error> { let path = use_decl.path(); - if path[0].as_inner() != CRATE_STR { + if path.first().map(|id| id.as_inner()) != Some(CRATE_STR) { return Err(Error::MissingCrateKeyword); } @@ -710,6 +710,8 @@ impl Scope { }; // Phase 1: navigate to target and collect items. Immutable borrow, dropped at end of block + // Vec<(ProcessedAlias, ProcessedFunction, ProcessedModule)> + // where each is Result<(Key, (Value, Visibility)), Error> let collected: Vec<_> = { // TODO: Part, that can be optimized // How many segments do the caller's path and the target's path have in common? @@ -1043,18 +1045,12 @@ impl Program { ); let (parameters, witness_types, call_tracker) = scope.destruct(); - let mut iter = items.into_iter().filter_map(|item| match item { - Item::Function(Function::Main(expr)) => Some(expr), - _ => None, - }); - - let main = iter.next().ok_or(Error::MainRequired).with_span(from)?; - if iter.next().is_some() { - return Err(Error::FunctionRedefined { - name: FunctionName::main(), - }) - .with_span(from); - } + let main = Self::extract_single_main(&items) + // If we find a duplicate of main function + .map_err(|err| err.with_span(from.into()))? + .ok_or(Error::MainRequired) + .with_span(from)?; + Ok(Self { main, parameters, @@ -1062,6 +1058,31 @@ impl Program { call_tracker: Arc::new(call_tracker), }) } + + // Put this helper function inside your impl block or just above your logic + fn extract_single_main(items: &[Item]) -> Result, Error> { + let mut main_expr = None; + + for item in items { + let extracted = match item { + Item::Function(Function::Main(expr)) => Some(expr.clone()), + Item::Module(items) => Self::extract_single_main(items)?, + _ => None, + }; + + let Some(expr) = extracted else { + continue; + }; + + if main_expr.replace(expr).is_some() { + return Err(Error::FunctionRedefined { + name: FunctionName::main(), + }); + } + } + + Ok(main_expr) + } } impl AbstractSyntaxTree for Item { @@ -1090,11 +1111,13 @@ impl AbstractSyntaxTree for Item { scope .enter_module(module.name().clone(), module.visibility().clone()) .with_span(module)?; + + let mut analyzed_children = Vec::new(); for item in module.items() { - Item::analyze(item, ty, scope)?; + analyzed_children.push(Item::analyze(item, ty, scope)?); } scope.exit_module(); - Ok(Self::Module) + Ok(Self::Module(analyzed_children)) } parse::Item::Ignored => Ok(Self::Ignored), } @@ -1155,10 +1178,6 @@ impl AbstractSyntaxTree for Function { } } - if !scope.module_path.is_empty() { - return Err(Error::MainOutOfEntryFile).with_span(from); - } - if matches!(from.visibility(), Visibility::Public) { return Err(Error::MainCannotBePublic).with_span(from); } @@ -1860,19 +1879,6 @@ mod scope_resolution_tests { ); } - #[test] - fn dependency_main_does_not_satisfy_missing_root_main() { - let result = analyze_multifile(vec![ - ("main.simf", "use lib::A::helper;"), - ("libs/lib/A.simf", "fn main() {} pub fn helper() {}"), - ]); - - assert!( - result.is_err(), - "Main function must be inside an entry file: {result:?}" - ) - } - #[test] fn main_must_be_defined_once_per_project() { let result = analyze_multifile(vec![ diff --git a/src/driver/mod.rs b/src/driver/mod.rs index c9103725..79e75c2a 100644 --- a/src/driver/mod.rs +++ b/src/driver/mod.rs @@ -165,7 +165,7 @@ impl DependencyGraph { let mut invalid_imports = HashSet::new(); while let Some(curr_id) = queue.pop_front() { - let Some(current_module) = graph.modules.get(curr_id) else { + let Some(current_source_module) = graph.modules.get(curr_id) else { return Err(format!( "Internal Driver Error: Module ID {} is in the queue but missing from the graph.modules.", curr_id @@ -173,11 +173,15 @@ impl DependencyGraph { }; // We need this to report errors inside THIS file. - let importer_source = current_module.source.clone(); + let importer_source = current_source_module.source.clone(); + let current = CurrentModule { + id: curr_id, + source: importer_source, + }; let valid_imports = Self::resolve_imports( - ¤t_module.program, - &importer_source, + ¤t_source_module.program, + ¤t, &graph.dependency_map, &mut use_cache, handler, @@ -188,10 +192,6 @@ impl DependencyGraph { handler, queue: &mut queue, }; - let current = CurrentModule { - id: curr_id, - source: &importer_source, - }; graph.load_and_parse_dependencies(¤t, valid_imports, &mut ctx); } @@ -246,13 +246,13 @@ impl DependencyGraph { /// Note: This is a specialized helper designed exclusively for [`DependencyGraph::new`]. fn resolve_imports( current_program: &parse::Program, - importer_source: &CanonSourceFile, + current_module: &CurrentModule, dependency_map: &DependencyMap, use_cache: &mut HashMap, handler: &mut ErrorCollector, ) -> Vec<(CanonPath, Span)> { let mut ctx = ImportContext { - importer_source, + current: current_module.clone(), dependency_map, use_cache, handler, @@ -287,9 +287,10 @@ impl DependencyGraph { } let Some(module) = - Self::parse_and_get_source_module(&path, current.source, import_span, ctx.handler) + Self::parse_and_get_source_module(&path, ¤t.source, import_span, ctx.handler) else { - ctx.invalid_imports.insert(path); + // Safe to ignore output: previous `.contains` check prevents collisions. + let _ = ctx.invalid_imports.insert(path); continue; }; @@ -310,7 +311,7 @@ impl DependencyGraph { /// Groups all shared state for import resolution to avoid threading a lot of parameters /// through every recursive call. Lives only for the duration of [`resolve_imports`]. struct ImportContext<'a> { - importer_source: &'a CanonSourceFile, + current: CurrentModule, dependency_map: &'a DependencyMap, use_cache: &'a mut HashMap, handler: &'a mut ErrorCollector, @@ -321,21 +322,39 @@ impl<'a> ImportContext<'a> { /// later graph construction phases, and returns the resolved path and span. /// Returns `None` and reports to `handler` if resolution fails. fn resolve_single(&mut self, use_decl: &parse::UseDecl) -> Option<(CanonPath, Span)> { - match self + let resolved = match self .dependency_map - .resolve_path_internal(self.importer_source.name(), use_decl) + .resolve_path_internal(self.current.source.name(), use_decl) { - Ok(resolved) => { - let result = (resolved.path.clone(), *use_decl.span()); - self.use_cache.insert(use_decl.clone(), resolved); - Some(result) - } + Ok(res) => res, Err(err) => { self.handler - .push(err.with_source(self.importer_source.clone())); - None + .push(err.with_source(self.current.source.clone())); + return None; } + }; + + // We assign a file ID to prevent collisions between identical `use` statements across different files. + // For instance, `use crate::A::foo;` in the local workspace and in an external dependency + // might resolve to completely different implementations of the `foo` function. + let mut use_decl = use_decl.clone(); + let span = *use_decl.span(); + use_decl.set_file_id(self.current.id); + + let result: (CanonPath, Span) = (resolved.path.clone(), span); + + // Since we found an error, when we can reevalute the result, we do not want to break it again + // So, add error to prevent similar cases in the future + if let Some(old_value) = self.use_cache.insert(use_decl, resolved) { + let msg = format!( + "Reevaluated an existing use_decl. Old value was: {:?}", + old_value + ); + let err = RichError::new(Error::Internal { msg }, span) + .with_source(self.current.source.clone()); + self.handler.push(err); } + Some(result) } /// Recursively walks an item, collecting resolved imports. @@ -369,9 +388,10 @@ struct LoadContext<'a> { /// The currently processed module and its source, used for error reporting /// and dependency registration. -struct CurrentModule<'a> { +#[derive(Debug, Clone)] +struct CurrentModule { id: usize, - source: &'a CanonSourceFile, + source: CanonSourceFile, } #[cfg(test)] diff --git a/src/driver/resolve_order.rs b/src/driver/resolve_order.rs index 04ef86a5..865c71c3 100644 --- a/src/driver/resolve_order.rs +++ b/src/driver/resolve_order.rs @@ -1,5 +1,5 @@ -use crate::driver::{DependencyGraph, CRATE_STR, MAIN_MODULE}; -use crate::error::ErrorCollector; +use crate::driver::{DependencyGraph, CRATE_STR, MAIN_MODULE, MAIN_STR}; +use crate::error::{Error, ErrorCollector, RichError}; use crate::parse::{self, Visibility}; use crate::str::{Identifier, ModuleName}; @@ -39,8 +39,15 @@ impl DependencyGraph { .collect(); if source_id == MAIN_MODULE { - items.extend(local_items); - continue; + let has_main = local_items.iter().any(|item| { + matches!(item, parse::Item::Function(f) if f.name().as_inner() == MAIN_STR) + }); + + if !has_main { + handler.push(RichError::parsing_error( + &Error::MainOutOfEntryFile.to_string(), + )); + } } let name = ModuleName::from_str_unchecked(Self::get_module_name(source_id).as_inner()); @@ -69,7 +76,7 @@ impl DependencyGraph { function.set_file_id(source_id); Some(parse::Item::Function(function)) } - parse::Item::Use(use_decl) => Some(self.rewrite_use(use_decl)), + parse::Item::Use(use_decl) => Some(self.rewrite_use(source_id, use_decl)), parse::Item::Module(module) => { let items: Vec = module .items() @@ -96,24 +103,20 @@ impl DependencyGraph { /// /// `use base_math::simple_op::hash` into `use file_2::hash` /// `use crate::inline_mod::item` into `use crate::inline_mod::item` - fn rewrite_use(&self, use_decl: &parse::UseDecl) -> parse::Item { - let resolved = &self.use_cache[use_decl]; + fn rewrite_use(&self, source_id: usize, use_decl: &parse::UseDecl) -> parse::Item { + let mut use_decl = use_decl.clone(); + use_decl.set_file_id(source_id); + let resolved = &self.use_cache[&use_decl]; let target_id = self.lookup[&resolved.path]; let mut new_path = Vec::with_capacity(resolved.mod_path.len() + 2); new_path.push(Identifier::from_str_unchecked(CRATE_STR)); - if target_id != MAIN_MODULE { - new_path.push(Self::get_module_name(target_id)); - } + new_path.push(Self::get_module_name(target_id)); new_path.extend(resolved.mod_path.iter().cloned()); - parse::Item::Use(parse::UseDecl::new( - use_decl.visibility().clone(), - &new_path, - use_decl.items().clone(), - *use_decl.span(), - )) + use_decl.set_path(&new_path); + parse::Item::Use(use_decl) } } @@ -141,43 +144,13 @@ mod flattening_tests { (program, ids) } - #[test] - fn test_main_module_is_not_wrapped() { - // Scenario: The entry file should have its items injected directly into - // the root of the AST, NOT wrapped in a `mod file_0` block. - let (program, _) = build_flattened_program(vec![( - "main.simf", - "pub fn root_func() {} type root_type = u32;", - )]); - - let items = program.items(); - - // Ensure there are no Module wrappers at the root level - let has_modules = items - .iter() - .any(|item| matches!(item, parse::Item::Module(_))); - assert!( - !has_modules, - "Main module items should not be wrapped in a mod block" - ); - - // Ensure the items are directly present - let has_func = items.iter().any( - |item| matches!(item, parse::Item::Function(f) if f.name().as_inner() == "root_func"), - ); - assert!( - has_func, - "root_func must be injected directly into the root" - ); - } - #[test] fn test_dependency_is_wrapped_in_file_module() { // Scenario: A dependency file MUST be wrapped in a `mod file_N` block, // and its visibility must be Private to prevent leaking. let (program, ids) = build_flattened_program(vec![ ("libs/lib/A.simf", "pub fn dep_func() {}"), - ("main.simf", "use lib::A::dep_func;"), + ("main.simf", "use lib::A::dep_func; fn main() {}"), ]); let file_a_id = ids["A"]; @@ -216,17 +189,26 @@ mod flattening_tests { // must rewrite this path to `use crate::file_N::foo`. let (program, ids) = build_flattened_program(vec![ ("libs/lib/A.simf", "pub fn foo() {}"), - ("main.simf", "use lib::A::foo;"), + ("main.simf", "use lib::A::foo; fn main() {}"), ]); let file_a_id = ids["A"]; let expected_file_segment = format!("unit_{}", file_a_id); + // Flatten the modules and search their inner contents let use_decl = program .items() .iter() - .find_map(|item| { - if let parse::Item::Use(u) = item { + .filter_map(|item| { + if let parse::Item::Module(module) = item { + Some(module.items()) // Get the slice of inner items + } else { + None + } + }) + .flatten() // Unpack all the inner slices into a single stream + .find_map(|inner_item| { + if let parse::Item::Use(u) = inner_item { Some(u) } else { None @@ -249,7 +231,32 @@ mod flattening_tests { assert_eq!( path[1].as_inner(), expected_file_segment.as_str(), - "Path must route through the canonical `file_N`" + "Path must route through the canonical `unit_N`" + ); + } + + #[test] + fn dependency_main_does_not_satisfy_missing_root_main() { + let (graph, _ids, _dir) = setup_graph(vec![ + ("main.simf", "use lib::A::helper;"), + ( + "libs/lib/A.simf", + "fn main() { assert!(false); } pub fn helper() {}", + ), + ]); + + let mut error_handler = ErrorCollector::new(); + let driver_program = graph.linearize_and_build(&mut error_handler); + + assert!( + matches!(driver_program, Ok(None)), + "Expected the build to fail and return Ok(None), but got: {:?}", + driver_program + ); + + assert!( + error_handler.has_errors(), + "a dependency `fn main` must not satisfy a missing entrypoint `fn main`" ); } } diff --git a/src/lib.rs b/src/lib.rs index c3a8c776..9170b17a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,7 +113,6 @@ impl TemplateProgram { let source = SourceFile::anonymous(file.clone()); let mut error_handler = ErrorCollector::new(); let parse_program = parse::Program::parse_from_str_with_errors(source, &mut error_handler); - dbg!(&file); if let Some(program) = parse_program { let ast_program = ast::Program::analyze(&program, jet_hinter.clone_box()) @@ -435,6 +434,54 @@ pub(crate) mod tests { use crate::*; + const FLATTENED: &str = "flattened.simf"; + pub(crate) const MAIN: &str = "main.simf"; + + pub(crate) fn flatten_template_file( + prog_path: &Path, + dependency_map: &DependencyMap, + ) -> String { + let program_text = std::fs::read_to_string(prog_path).unwrap(); + let source = CanonSourceFile::new( + CanonPath::canonicalize(prog_path).unwrap(), + Arc::from(program_text), + ); + + match TemplateProgram::flatten(source, dependency_map) { + Ok(single_file) => single_file, + Err(error) => panic!("{error}"), + } + } + + pub(crate) fn format_program_file(prog_path: &Path) -> String { + let file = Arc::::from(std::fs::read_to_string(prog_path).unwrap()); + let source = SourceFile::anonymous(file.clone()); + + let mut error_handler = ErrorCollector::new(); + let parse_program = + parse::Program::parse_from_str_with_errors(source, &mut error_handler).unwrap(); + parse_program.to_string() + } + + pub(crate) fn build_dependency_map(prog_path: P, dependencies: I) -> DependencyMap + where + P: AsRef, + I: IntoIterator, + K: Into, + { + let parent = prog_path.as_ref().parent().unwrap(); + let canon_root = canon(parent); + let mut builder = DependencyMapBuilder::new(canon_root); + + for (context, alias, target) in dependencies { + let context = canon(context.as_ref()); + let target = canon(target.as_ref()); + + builder = builder.add_dependency(context, alias.into(), target); + } + builder.build().unwrap() + } + pub(crate) struct TestCase { program: T, lock_time: elements::LockTime, @@ -451,7 +498,7 @@ pub(crate) mod tests { pub fn template_deps(prog_path: &Path, dependency_map: &DependencyMap) -> Self { let program_text = std::fs::read_to_string(prog_path).unwrap(); let source = CanonSourceFile::new( - crate::source::CanonPath::canonicalize(prog_path).unwrap(), + CanonPath::canonicalize(prog_path).unwrap(), Arc::from(program_text), ); @@ -526,26 +573,11 @@ pub(crate) mod tests { .with_arguments(Arguments::default()) } - pub fn program_file_with_deps(prog_path: P, dependencies: I) -> Self - where - P: AsRef, - I: IntoIterator, - K: Into, - { - let parent = prog_path.as_ref().parent().unwrap(); - let canon_root = canon(parent); - let mut builder = DependencyMapBuilder::new(canon_root); - - for (context, alias, target) in dependencies { - let context = canon(context.as_ref()); - let target = canon(target.as_ref()); - - builder = builder.add_dependency(context, alias.into(), target); - } - - let dependency_map = builder.build().unwrap(); - - TestCase::::template_deps(prog_path.as_ref(), &dependency_map) + pub fn program_file_with_deps( + prog_path: impl AsRef, + dependency_map: &DependencyMap, + ) -> Self { + TestCase::::template_deps(prog_path.as_ref(), dependency_map) .with_arguments(Arguments::default()) } @@ -655,20 +687,35 @@ pub(crate) mod tests { pub(crate) fn run_dependency_test(root_path: &str, lib_alias: &str) { let root_path = PathBuf::from(root_path); let lib_path = root_path.join(lib_alias); - let main_path = root_path.join("main.simf"); + let main_path = root_path.join(MAIN); + + let dependency_map = build_dependency_map(&main_path, [(&root_path, lib_alias, &lib_path)]); - TestCase::program_file_with_deps(&main_path, [(&root_path, lib_alias, &lib_path)]) + TestCase::program_file_with_deps(&main_path, &dependency_map) .with_witness_values(WitnessValues::default()) .assert_run_success(); } - /// THE ADVANCED HELPER + pub(crate) fn flatten_dependency_test(root_path: &str, lib_alias: &str) { + let root_path = PathBuf::from(root_path); + let lib_path = root_path.join(lib_alias); + let main_path = root_path.join(MAIN); + let flattened_path = root_path.join(FLATTENED); + + let dependency_map = build_dependency_map(&main_path, [(&root_path, lib_alias, &lib_path)]); + + let expected = format_program_file(&flattened_path); + let actual = flatten_template_file(&main_path, &dependency_map); + + assert_eq!(expected, actual); + } + /// A helper function to run standard library dependency tests. /// `deps` expects an array of tuples: `(context_folder, alias, target_folder)`. /// Use `"."` for the `context_folder` if the context is the root test directory. pub(crate) fn run_multidep_test(root_path: &str, deps: &[(&str, &str, &str)]) { let root_path = PathBuf::from(root_path); - let main_path = root_path.join("main.simf"); + let main_path = root_path.join(MAIN); // Convert the string slices into proper PathBufs dynamically let mapped_deps: Vec<(PathBuf, &str, PathBuf)> = deps @@ -687,12 +734,46 @@ pub(crate) mod tests { .collect(); let ref_deps = mapped_deps.iter().map(|(c, a, t)| (c, *a, t)); - - TestCase::program_file_with_deps(&main_path, ref_deps) + let dependency_map = build_dependency_map(&main_path, ref_deps); + TestCase::program_file_with_deps(&main_path, &dependency_map) .with_witness_values(WitnessValues::default()) .assert_run_success(); } + /// THE ADVANCED HELPER + /// A helper function to run standard library dependency tests. + /// `deps` expects an array of tuples: `(context_folder, alias, target_folder)`. + /// Use `"."` for the `context_folder` if the context is the root test directory. + pub(crate) fn flatten_multidep_test(root_path: &str, deps: &[(&str, &str, &str)]) { + let root_path = PathBuf::from(root_path); + let main_path = root_path.join(MAIN); + let flattened_path = root_path.join(FLATTENED); + + // Convert the string slices into proper PathBufs dynamically + let mapped_deps: Vec<(PathBuf, &str, PathBuf)> = deps + .iter() + .map(|(ctx, alias, target)| { + let ctx_path = if *ctx == "." { + root_path.clone() + } else { + root_path.join(ctx) + }; + + let target_path = root_path.join(target); + + (ctx_path, *alias, target_path) + }) + .collect(); + + let ref_deps = mapped_deps.iter().map(|(c, a, t)| (c, *a, t)); + let dependency_map = build_dependency_map(&main_path, ref_deps); + + let expected = format_program_file(&flattened_path); + let actual = flatten_template_file(&main_path, &dependency_map); + + assert_eq!(expected, actual); + } + /// Run with `simc` command: /// /// ``` @@ -704,6 +785,11 @@ pub(crate) mod tests { run_dependency_test("./examples/single_dep", "temp"); } + #[test] + fn flatten_single_dep() { + flatten_dependency_test("./examples/single_dep", "temp"); + } + /// Run with `simc` command: /// /// ``` @@ -719,6 +805,14 @@ pub(crate) mod tests { ); } + #[test] + fn flatten_simple_multidep() { + flatten_multidep_test( + "./examples/simple_multidep", + &[(".", "math", "math"), (".", "crypto", "crypto")], + ); + } + /// Run with `simc` command: /// /// ``` @@ -739,6 +833,18 @@ pub(crate) mod tests { ); } + #[test] + fn flatten_multiple_deps() { + flatten_multidep_test( + "./examples/multiple_deps", + &[ + (".", "merkle", "merkle"), + (".", "base_math", "math"), + ("merkle", "math", "math"), + ], + ); + } + /// Run with `simc` command: /// /// ``` @@ -754,7 +860,7 @@ pub(crate) mod tests { let ws = TempWorkspace::new("crate_success"); let root = ws.create_dir("workspace"); ws.create_file( - "workspace/main.simf", + format!("workspace/{MAIN}").as_str(), "use crate::utils::add;\nfn main() { assert!(jet::eq_32(add(2, 2), 4)); }", ); ws.create_file( @@ -762,7 +868,7 @@ pub(crate) mod tests { "pub fn add(a: u32, b: u32) -> u32 { let (_, sum): (bool, u32) = jet::add_32(a, b); sum }", ); - let main_path = root.join("main.simf"); + let main_path = root.join(MAIN); let canon_root = CanonPath::canonicalize(&root).unwrap(); let dependency_map = DependencyMapBuilder::new(canon_root).build().unwrap(); @@ -790,6 +896,13 @@ pub(crate) mod tests { .assert_run_success(); } + #[test] + fn modules() { + TestCase::program_file("./examples/modules.simf") + .with_witness_values(WitnessValues::default()) + .assert_run_success(); + } + #[test] fn ctv() { TestCase::program_file("./examples/ctv.simf") @@ -1187,6 +1300,7 @@ fn main() { mod error_tests { use std::path::Path; + use super::tests::MAIN; use super::*; use crate::ast::ElementsJetHinter; @@ -1217,7 +1331,7 @@ mod error_tests { let root_dir = ws.create_dir("workspace"); let lib_dir = ws.create_dir("workspace/lib"); let main_path = ws.create_file( - "workspace/main.simf", + format!("workspace/{MAIN}").as_str(), "use lib::bad::f;\nfn main() { f(); }\n", ); let bad_path = ws.create_file( @@ -1247,7 +1361,7 @@ mod error_tests { let root_dir = ws.create_dir("workspace"); let lib_dir = ws.create_dir("workspace/lib"); let main_path = ws.create_file( - "workspace/main.simf", + format!("workspace/{MAIN}").as_str(), "use lib::nested::two;\nfn main() { assert!(jet::eq_32(two(), 2)); }\n", ); ws.create_file( @@ -1271,7 +1385,7 @@ mod error_tests { let root_dir = ws.create_dir("workspace"); let lib_dir = ws.create_dir("workspace/lib"); let main_path = ws.create_file( - "workspace/main.simf", + format!("workspace/{MAIN}").as_str(), "use lib::missing::Thing;\nfn main() {}\n", ); let dependencies = dependency_map(&root_dir, "lib", &lib_dir); @@ -1292,17 +1406,30 @@ mod error_tests { #[cfg(test)] mod functional_tests { - use crate::tests::{run_dependency_test, run_multidep_test}; + use crate::ast::ElementsJetHinter; + use crate::resolution::{DependencyMap, DependencyMapBuilder}; + use crate::source::{CanonPath, CanonSourceFile}; + use crate::tests::{flatten_multidep_test, run_dependency_test, run_multidep_test}; + use crate::{Arguments, CompiledProgram}; + use std::path::{Path, PathBuf}; + use std::sync::Arc; const VALID_TESTS_DIR: &str = "./functional-tests/valid-test-cases"; const ERROR_TESTS_DIR: &str = "./functional-tests/error-test-cases"; - // Real test cases #[test] fn module_simple() { run_dependency_test(format!("{}/module-simple", VALID_TESTS_DIR).as_str(), "lib"); } + #[test] + fn module_name_simple() { + run_dependency_test( + format!("{}/module-name-collision", VALID_TESTS_DIR).as_str(), + "lib", + ); + } + #[test] fn diamond_dependency_resolution() { run_dependency_test( @@ -1457,4 +1584,57 @@ mod functional_tests { &[(".", "ext", ".")], ); } + + fn compile_with_deps(path: &Path, dependency_map: &DependencyMap) -> CompiledProgram { + let program_text = std::fs::read_to_string(path) + .unwrap_or_else(|_| panic!("Failed to read source file: {}", path.display())); + + let source = CanonSourceFile::new( + CanonPath::canonicalize(path).expect("Failed to canonicalize path"), + Arc::from(program_text), + ); + + CompiledProgram::new_with_dep( + source, + dependency_map, + Arguments::default(), + false, + Box::new(ElementsJetHinter::new()), + ) + .expect("Failed to compile program in test") + } + + #[test] + fn identical_crate_uses_in_different_package_roots_do_not_poison_resolution_cache() { + let base_dir = PathBuf::from(format!("{}/use-statement-collision", VALID_TESTS_DIR)); + let lib_dir = base_dir.join("libs/lib"); + + let poisoned_main = base_dir.join("main.simf"); + let control_main = base_dir.join("control.simf"); + + let root_canon = CanonPath::canonicalize(&base_dir).unwrap(); + let lib_canon = CanonPath::canonicalize(&lib_dir).unwrap(); + + // 3. Set up the dependency maps + let dependency_map = DependencyMapBuilder::new(root_canon.clone()) + .add_dependency(root_canon.clone(), "lib".to_string(), lib_canon) + .build() + .unwrap(); + + let no_dependency_map = DependencyMapBuilder::new(root_canon).build().unwrap(); + + // Compile both programs reading directly from the file system + let poisoned = compile_with_deps(&poisoned_main, &dependency_map); + let control = compile_with_deps(&control_main, &no_dependency_map); + + // Compare the CMR outputs + assert_eq!( + poisoned.commit().cmr(), + control.commit().cmr(), + "Resolving an identical `use crate::...` inside a dependency must not change \ + what `crate::...` means in the entry package" + ); + + flatten_multidep_test(&base_dir.to_string_lossy(), &[(".", "lib", "libs/lib")]); + } } diff --git a/src/parse.rs b/src/parse.rs index 7a269532..0f4a035b 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -94,6 +94,8 @@ pub enum Visibility { /// ``` #[derive(Clone, Debug)] pub struct UseDecl { + file_id: usize, + /// The visibility of the import (e.g., `pub use` vs `use`). visibility: Visibility, @@ -110,18 +112,16 @@ pub struct UseDecl { impl UseDecl { /// The driver uses this to ensure imports conform to the flattened program structure. - pub(crate) fn new( - visibility: Visibility, - path: &[Identifier], - items: UseItems, - span: Span, - ) -> Self { - Self { - visibility, - path: Vec::from(path), - items, - span, - } + pub(crate) fn set_file_id(&mut self, file_id: usize) { + self.file_id = file_id; + } + + pub(crate) fn set_path(&mut self, path: &[Identifier]) { + self.path = Vec::from(path) + } + + pub fn file_id(&self) -> usize { + self.file_id } pub fn visibility(&self) -> &Visibility { @@ -165,11 +165,15 @@ impl UseDecl { } } -impl_eq_hash!(UseDecl; visibility, path, items); +// `file_id` and `span` are required because `UseDecl` hashing is context-dependent. +// For instance, identical `use crate::...` paths differ between binary and library roots. +// Tested by: `functional_tests::identical_crate_uses_in_different_package_roots_do_not_poison_resolution_cache`. +impl_eq_hash!(UseDecl; file_id, visibility, path, items, span); #[cfg(feature = "arbitrary")] impl<'a> arbitrary::Arbitrary<'a> for UseDecl { fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let file_id = u.int_in_range(0..=3)?; let visibility = Visibility::arbitrary(u)?; let path_len = u.int_in_range(2..=4)?; let path = (0..path_len) @@ -178,6 +182,7 @@ impl<'a> arbitrary::Arbitrary<'a> for UseDecl { let items = UseItems::arbitrary(u)?; Ok(Self { + file_id, visibility, path, items, @@ -226,7 +231,7 @@ impl Function { self.file_id } - pub fn set_file_id(&mut self, file_id: usize) { + pub(crate) fn set_file_id(&mut self, file_id: usize) { self.file_id = file_id; } @@ -401,7 +406,7 @@ impl TypeAlias { self.file_id } - pub fn set_file_id(&mut self, file_id: usize) { + pub(crate) fn set_file_id(&mut self, file_id: usize) { self.file_id = file_id; } @@ -1469,7 +1474,7 @@ impl ChumskyParse for Function { .then(params) .then(ret) .then(body) - .map_with(move |((((visibility, name), params), ret), body), e| Self { + .map_with(|((((visibility, name), params), ret), body), e| Self { file_id: MAIN_MODULE, visibility, name, @@ -1535,6 +1540,7 @@ impl ChumskyParse for UseDecl { .then(items) .then_ignore(just(Token::Semi)) .map_with(|((visibility, path), items), e| Self { + file_id: MAIN_MODULE, visibility, path, items, @@ -1822,7 +1828,7 @@ impl ChumskyParse for TypeAlias { .then(AliasedType::parser()) .then_ignore(just(Token::Semi)), ) - .map_with(move |(visibility, (name, ty)), e| Self { + .map_with(|(visibility, (name, ty)), e| Self { file_id: MAIN_MODULE, visibility, name: name.0, @@ -2140,7 +2146,6 @@ impl Module { where I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, { - let file_id = MAIN_MODULE; let visibility = just(Token::Pub) .to(Visibility::Public) .or_not() @@ -2165,8 +2170,8 @@ impl Module { visibility .then(just(Token::Mod).ignore_then(name).then(items)) - .map_with(move |(visibility, (name, items)), e| Self { - file_id, + .map_with(|(visibility, (name, items)), e| Self { + file_id: MAIN_MODULE, visibility, name: name.0, items, @@ -2558,6 +2563,7 @@ mod test { /// Creates a dummy `UseDecl` specifically for testing `DependencyMap` resolution. pub fn dummy_path(path: Vec) -> Self { Self { + file_id: MAIN_MODULE, visibility: Visibility::default(), path, items: UseItems::List(Vec::new()), From 4359a845206a8c7f11d4bc03d6dc9674f135e060 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Thu, 4 Jun 2026 18:28:21 +0300 Subject: [PATCH 10/10] docs: clarify imports and flattening have no effect on CMR --- doc/architecture.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/doc/architecture.md b/doc/architecture.md index b730a3b1..0213c086 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -1,5 +1,40 @@ # Architecture Notes +## The effect of imports and flattening on the CMR + +Imports and the flattening phase have **no impact** on the CMR. The underlying order of calculation is strictly determined by the `main` function. + +For example, consider the following code: + +```rust +fn a() -> u32 { 5 } +fn b() -> u32 { 6 } + +fn main() { + let a: u32 = a(); + let b: u32 = b(); + let (_, c): (bool, u32) = jet::add_32(a, b); + assert!(jet::eq_32(c, 11)); +} +``` + +It does not matter whether `fn a()` or `fn b()` is declared first; the driver can reorder these declarations as it sees fit without affecting the CMR. + +The flattening phase behaves in the exact same way. While it wraps the file's contents into a `mod unit_N { ... }` block, it does not alter the execution order inside `main`. The CMR will change if and only if we explicitly modify the execution order (e.g., swapping the evaluation of `let a` and `let b`) within the `main` function itself. + +Below are three scenarios where resolution errors corrupt the CMR: + +1. Incorrect Aliasing or Function Substitution +If the resolution phase maps an alias to the wrong function, the compiler silently substitutes one implementation for another. Since the program logic has changed, the resulting CMR will be entirely different. + +2. Entry Point Fallback (main hijacking) +The CMR is rooted at the main function of the entry file. If the compiler does not enforce this, it may traverse the dependency graph and pick up a main from a dependency instead. This completely changes the execution graph. + +3. Resolution Cache Poisoning (use path collisions) +When different package roots share structurally identical import paths (e.g., both a binary and a library declare `use crate::A::foo`), an improperly isolated resolution cache may link one context's import to the other's physical file. (See `functional_tests::identical_crate_uses_in_different_package_roots_do_not_poison_resolution_cache`.) + +*Note: The scenarios listed above reflect bugs discovered during current testing. The list is ***not exhaustive*** and may be expanded as further testing uncovers additional edge cases.* + ## Crate and Module Paths The `crate` keyword is used to construct absolute paths where the path root is the current package's root directory. This provides an explicit and readable way to distinguish local imports from external library imports.