From 3016ca28889c7767c604cc7fbd309dd78998d7d1 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Tue, 10 Feb 2026 14:42:35 +0200 Subject: [PATCH 01/13] feat: add pub and use keywords and parse them --- Cargo.toml | 4 +- src/ast.rs | 1 + src/lexer.rs | 15 ++++ src/parse.rs | 199 ++++++++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 201 insertions(+), 18 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c3a8cc30..d627bc61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,7 +68,7 @@ copy_iterator = "warn" default_trait_access = "warn" doc_link_with_quotes = "warn" doc_markdown = "warn" -empty_enum = "warn" +empty_enums = "warn" enum_glob_use = "allow" expl_impl_clone_on_copy = "warn" explicit_deref_methods = "warn" @@ -152,7 +152,7 @@ struct_field_names = "warn" too_many_lines = "allow" transmute_ptr_to_ptr = "warn" trivially_copy_pass_by_ref = "warn" -unchecked_duration_subtraction = "warn" +unchecked_time_subtraction = "warn" unicode_not_nfc = "warn" unnecessary_box_returns = "warn" unnecessary_join = "warn" diff --git a/src/ast.rs b/src/ast.rs index 3c59f2f7..94ed5c38 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -762,6 +762,7 @@ impl AbstractSyntaxTree for Item { parse::Item::Function(function) => { Function::analyze(function, ty, scope).map(Self::Function) } + parse::Item::Use(_) => todo!(), parse::Item::Module => Ok(Self::Module), } } diff --git a/src/lexer.rs b/src/lexer.rs index 71c004b6..0631b9b1 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -11,6 +11,9 @@ pub type Tokens<'src> = Vec<(Token<'src>, crate::error::Span)>; #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum Token<'src> { // Keywords + Pub, + Use, + As, Fn, Let, Type, @@ -20,6 +23,10 @@ pub enum Token<'src> { // Control symbols Arrow, + /// Represents a contiguous `::` token. + /// This prevents the lexer from allowing spaces between colons (e.g., `use a: :b`), + /// ensuring we strictly parse valid paths. + DoubleColon, Colon, Semi, Comma, @@ -63,6 +70,9 @@ pub enum Token<'src> { impl<'src> fmt::Display for Token<'src> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Token::Pub => write!(f, "pub"), + Token::Use => write!(f, "use"), + Token::As => write!(f, "as"), Token::Fn => write!(f, "fn"), Token::Let => write!(f, "let"), Token::Type => write!(f, "type"), @@ -71,6 +81,7 @@ impl<'src> fmt::Display for Token<'src> { Token::Match => write!(f, "match"), Token::Arrow => write!(f, "->"), + Token::DoubleColon => write!(f, "::"), Token::Colon => write!(f, ":"), Token::Semi => write!(f, ";"), Token::Comma => write!(f, ","), @@ -134,6 +145,9 @@ pub fn lexer<'src>( choice((just("assert!"), just("panic!"), just("dbg!"), just("list!"))).map(Token::Macro); let keyword = text::ident().map(|s| match s { + "pub" => Token::Pub, + "use" => Token::Use, + "as" => Token::As, "fn" => Token::Fn, "let" => Token::Let, "type" => Token::Type, @@ -162,6 +176,7 @@ pub fn lexer<'src>( just("->").to(Token::Arrow), just("=>").to(Token::FatArrow), just("=").to(Token::Eq), + just("::").to(Token::DoubleColon), // NOTE: It must be before ":", otherwise it does not work just(":").to(Token::Colon), just(";").to(Token::Semi), just(",").to(Token::Comma), diff --git a/src/parse.rs b/src/parse.rs index c42c6f3d..63750ae7 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -52,13 +52,58 @@ pub enum Item { TypeAlias(TypeAlias), /// A function. Function(Function), + /// Use keyword to load other items + Use(UseDecl), /// A module, which is ignored. Module, } +/// Definition of a declaration +#[derive(Clone, Debug)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct UseDecl { + visibility: Visibility, + path: Vec, + items: UseItems, + span: Span, +} + +impl UseDecl { + /// Access the visibility of the use declaration. + pub fn visibility(&self) -> &Visibility { + &self.visibility + } + + /// Access the visibility of the function. + pub fn path(&self) -> &Vec { + &self.path + } + + /// Access the visibility of the function. + pub fn items(&self) -> &UseItems { + &self.items + } + + /// Access the span of the use declaration. + pub fn span(&self) -> &Span { + &self.span + } +} + +impl_eq_hash!(UseDecl; visibility, path, items); + +// TODO: @LesterEvSe, Add aliases +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub enum UseItems { + Single(Identifier), + List(Vec), +} + /// Definition of a function. #[derive(Clone, Debug)] pub struct Function { + visibility: Visibility, name: FunctionName, params: Arc<[FunctionParam]>, ret: Option, @@ -66,7 +111,19 @@ pub struct Function { span: Span, } +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub enum Visibility { + Public, + Private, +} + impl Function { + /// Access the visibility of the function. + pub fn visibility(&self) -> &Visibility { + &self.visibility + } + /// Access the name of the function. pub fn name(&self) -> &FunctionName { &self.name @@ -95,7 +152,7 @@ impl Function { } } -impl_eq_hash!(Function; name, params, ret, body); +impl_eq_hash!(Function; visibility, name, params, ret, body); /// Parameter of a function. #[derive(Clone, Debug, Eq, PartialEq, Hash)] @@ -222,12 +279,18 @@ pub enum CallName { #[derive(Clone, Debug)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct TypeAlias { + visibility: Visibility, name: AliasName, ty: AliasedType, span: Span, } impl TypeAlias { + /// Access the visibility of the alias. + pub fn visibility(&self) -> &Visibility { + &self.visibility + } + /// Access the name of the alias. pub fn name(&self) -> &AliasName { &self.name @@ -556,6 +619,7 @@ impl fmt::Display for Item { match self { Self::TypeAlias(alias) => write!(f, "{alias}"), Self::Function(function) => write!(f, "{function}"), + Self::Use(use_declaration) => write!(f, "{use_declaration}"), // The parse tree contains no information about the contents of modules. // We print a random empty module `mod witness {}` here // so that `from_string(to_string(x)) = x` holds for all trees `x`. @@ -587,6 +651,47 @@ impl fmt::Display for Function { } } +impl fmt::Display for UseDecl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Visibility::Public = self.visibility { + write!(f, "pub ")?; + } + + let _ = write!(f, "use "); + + for (i, segment) in self.path.iter().enumerate() { + if i > 0 { + write!(f, "::")?; + } + write!(f, "{}", segment)?; + } + + if !self.path.is_empty() { + write!(f, "::")?; + } + + write!(f, "{};", self.items) + } +} + +impl fmt::Display for UseItems { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + UseItems::Single(ident) => write!(f, "{}", ident), + UseItems::List(idents) => { + let _ = write!(f, "{{"); + for (i, ident) in idents.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", ident)?; + } + write!(f, "}}") + } + } + } +} + impl fmt::Display for FunctionParam { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}: {}", self.identifier(), self.ty()) @@ -1138,7 +1243,12 @@ impl ChumskyParse for Program { let skip_until_next_item = any() .then( any() - .filter(|t| !matches!(t, Token::Fn | Token::Type | Token::Mod)) + .filter(|t| { + !matches!( + t, + Token::Pub | Token::Use | Token::Fn | Token::Type | Token::Mod + ) + }) .repeated(), ) // map to empty module @@ -1162,9 +1272,10 @@ impl ChumskyParse for Item { { let func_parser = Function::parser().map(Item::Function); let type_parser = TypeAlias::parser().map(Item::TypeAlias); + let use_parser = UseDecl::parser().map(Item::Use); let mod_parser = Module::parser().map(|_| Item::Module); - choice((func_parser, type_parser, mod_parser)) + choice((func_parser, use_parser, type_parser, mod_parser)) } } @@ -1173,6 +1284,12 @@ impl ChumskyParse for Function { where I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, { + let visibility = just(Token::Pub) + .to(Visibility::Public) + .or_not() + .map(|v| v.unwrap_or(Visibility::Private)) + .labelled("function visibility"); + let params = delimited_with_recovery( FunctionParam::parser() .separated_by(just(Token::Comma)) @@ -1204,12 +1321,14 @@ impl ChumskyParse for Function { ))) .labelled("function body"); - just(Token::Fn) - .ignore_then(FunctionName::parser()) + visibility + .then_ignore(just(Token::Fn)) + .then(FunctionName::parser()) .then(params) .then(ret) .then(body) - .map_with(|(((name, params), ret), body), e| Self { + .map_with(|((((visibility, name), params), ret), body), e| Self { + visibility, name, params, ret, @@ -1219,6 +1338,45 @@ impl ChumskyParse for Function { } } +impl ChumskyParse for UseDecl { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let visibility = just(Token::Pub) + .to(Visibility::Public) + .or_not() + .map(|v| v.unwrap_or(Visibility::Private)); + + let path = Identifier::parser() + .then_ignore(just(Token::DoubleColon)) + .repeated() + .at_least(1) + .collect::>(); + + let list = Identifier::parser() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect() + .delimited_by(just(Token::LBrace), just(Token::RBrace)) + .map(UseItems::List); + let single = Identifier::parser().map(UseItems::Single); + let items = choice((list, single)); + + visibility + .then_ignore(just(Token::Use)) + .then(path) + .then(items) + .then_ignore(just(Token::Semi)) + .map_with(|((visibility, path), items), e| Self { + visibility, + path, + items, + span: e.span(), + }) + } +} + impl ChumskyParse for FunctionParam { fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone where @@ -1347,16 +1505,14 @@ impl ChumskyParse for CallName { where I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, { - let double_colon = just(Token::Colon).then(just(Token::Colon)).labelled("::"); - - let turbofish_start = double_colon.clone().then(just(Token::LAngle)).ignored(); + let turbofish_start = just(Token::DoubleColon).then(just(Token::LAngle)).ignored(); let generics_close = just(Token::RAngle); let type_cast = just(Token::LAngle) .ignore_then(AliasedType::parser()) .then_ignore(generics_close.clone()) - .then_ignore(just(Token::Colon).then(just(Token::Colon))) + .then_ignore(just(Token::DoubleColon)) .then_ignore(just(Token::Ident("into"))) .map(CallName::TypeCast); @@ -1461,14 +1617,23 @@ impl ChumskyParse for TypeAlias { where I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, { + let visibility = just(Token::Pub) + .to(Visibility::Public) + .or_not() + .map(|v| v.unwrap_or(Visibility::Private)); + let name = AliasName::parser().map_with(|name, e| (name, e.span())); - just(Token::Type) - .ignore_then(name) - .then_ignore(parse_token_with_recovery(Token::Eq)) - .then(AliasedType::parser()) - .then_ignore(just(Token::Semi)) - .map_with(|(name, ty), e| Self { + visibility + .then( + just(Token::Type) + .ignore_then(name) + .then_ignore(parse_token_with_recovery(Token::Eq)) + .then(AliasedType::parser()) + .then_ignore(just(Token::Semi)), + ) + .map_with(|(visibility, (name, ty)), e| Self { + visibility, name: name.0, ty, span: e.span(), @@ -1953,6 +2118,7 @@ impl crate::ArbitraryRec for Function { fn arbitrary_rec(u: &mut arbitrary::Unstructured, budget: usize) -> arbitrary::Result { use arbitrary::Arbitrary; + let visibility = Visibility::arbitrary(u)?; let name = FunctionName::arbitrary(u)?; let len = u.int_in_range(0..=3)?; let params = (0..len) @@ -1961,6 +2127,7 @@ impl crate::ArbitraryRec for Function { let ret = Option::::arbitrary(u)?; let body = Expression::arbitrary_rec(u, budget).map(Expression::into_block)?; Ok(Self { + visibility, name, params, ret, From 1310a571f922aee4eebcd2e675807295c6509116 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Wed, 11 Feb 2026 14:14:59 +0200 Subject: [PATCH 02/13] feat: add the option CLI argument --lib and driver file --- Cargo.lock | 322 +++++++++++++++++- Cargo.toml | 3 + examples/simple_multilib/crypto/hashes.simf | 5 + examples/simple_multilib/main.simf | 7 + examples/simple_multilib/math/arithmetic.simf | 4 + src/driver.rs | 304 +++++++++++++++++ src/lib.rs | 55 ++- src/main.rs | 46 ++- src/str.rs | 6 + src/tracker.rs | 4 +- src/witness.rs | 4 +- 11 files changed, 726 insertions(+), 34 deletions(-) create mode 100644 examples/simple_multilib/crypto/hashes.simf create mode 100644 examples/simple_multilib/main.simf create mode 100644 examples/simple_multilib/math/arithmetic.simf create mode 100644 src/driver.rs diff --git a/Cargo.lock b/Cargo.lock index dbc6e55a..8e622e6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + [[package]] name = "ar_archive_writer" version = "0.2.0" @@ -173,6 +179,12 @@ dependencies = [ "hex-conservative", ] +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + [[package]] name = "bumpalo" version = "3.16.0" @@ -209,7 +221,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acc17a6284abccac6e50db35c1cee87f605474a72939b959a3a67d9371800efd" dependencies = [ - "hashbrown", + "hashbrown 0.15.5", "regex-automata", "serde", "stacker", @@ -291,6 +303,22 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -316,6 +344,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "ghost-cell" version = "0.2.6" @@ -333,6 +374,18 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hex-conservative" version = "0.2.1" @@ -348,6 +401,24 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -387,6 +458,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.180" @@ -403,6 +480,12 @@ dependencies = [ "cc", ] +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "log" version = "0.4.22" @@ -446,11 +529,21 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.114", +] + [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -467,13 +560,19 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" @@ -501,7 +600,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.10", ] [[package]] @@ -533,6 +632,19 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "ryu" version = "1.0.15" @@ -590,24 +702,40 @@ dependencies = [ "secp256k1-sys", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" -version = "1.0.188" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.114", ] [[package]] @@ -637,7 +765,7 @@ dependencies = [ "bitcoin_hashes", "byteorder", "elements", - "getrandom", + "getrandom 0.2.10", "ghost-cell", "hex-conservative", "miniscript", @@ -664,12 +792,13 @@ dependencies = [ "chumsky", "clap", "either", - "getrandom", + "getrandom 0.2.10", "itertools", "miniscript", "serde", "serde_json", "simplicity-lang", + "tempfile", ] [[package]] @@ -716,15 +845,28 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.31" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +dependencies = [ + "fastrand", + "getrandom 0.4.1", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "unicode-ident" version = "1.0.11" @@ -737,6 +879,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "utf8parse" version = "0.2.2" @@ -749,6 +897,24 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.92" @@ -770,7 +936,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.114", "wasm-bindgen-shared", ] @@ -792,7 +958,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.114", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -803,6 +969,40 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -875,3 +1075,91 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.114", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.114", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] diff --git a/Cargo.toml b/Cargo.toml index d627bc61..d9d0f162 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,9 @@ arbitrary = { version = "1", optional = true, features = ["derive"] } clap = "4.5.37" chumsky = "0.11.2" +[dev-dependencies] +tempfile = "3" + [target.wasm32-unknown-unknown.dependencies] getrandom = { version = "0.2", features = ["js"] } diff --git a/examples/simple_multilib/crypto/hashes.simf b/examples/simple_multilib/crypto/hashes.simf new file mode 100644 index 00000000..0e463d89 --- /dev/null +++ b/examples/simple_multilib/crypto/hashes.simf @@ -0,0 +1,5 @@ +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) +} diff --git a/examples/simple_multilib/main.simf b/examples/simple_multilib/main.simf new file mode 100644 index 00000000..81031d4c --- /dev/null +++ b/examples/simple_multilib/main.simf @@ -0,0 +1,7 @@ +use math::arithmetic::add; +use crypto::hashes::sha256; + +fn main() { + let sum: u32 = add(2, 3); + let hash: u256 = sha256(sum); +} \ No newline at end of file diff --git a/examples/simple_multilib/math/arithmetic.simf b/examples/simple_multilib/math/arithmetic.simf new file mode 100644 index 00000000..2f348e0c --- /dev/null +++ b/examples/simple_multilib/math/arithmetic.simf @@ -0,0 +1,4 @@ +pub fn add(a: u32, b: u32) -> u32 { + let (_, res): (bool, u32) = jet::add_32(a, b); + res +} \ No newline at end of file diff --git a/src/driver.rs b/src/driver.rs new file mode 100644 index 00000000..8f8e3a3a --- /dev/null +++ b/src/driver.rs @@ -0,0 +1,304 @@ +use std::collections::{HashMap, VecDeque}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use crate::error::ErrorCollector; +use crate::parse::{self, ParseFromStrWithErrors}; +use crate::LibConfig; + +/// 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)] +pub struct Module { + /// The completely parsed program for this specific file. + /// it contains all the functions, aliases, and imports defined inside the file. + pub parsed_program: parse::Program, +} + +/// The Dependency Graph itself. +pub struct ProjectGraph { + /// Arena Pattern: the data itself lives here. + /// A flat vector guarantees that module data is stored contiguously in memory. + pub modules: Vec, + + /// Fast lookup: File Path -> Module ID. + /// A reverse index mapping absolute file paths to their internal IDs. + /// This solves the duplication problem, ensuring each file is only parsed once. + pub lookup: HashMap, + + /// The Adjacency List: Defines the Directed acyclic Graph (DAG) of imports. + /// + /// The Key (`usize`) is the ID of a "Parent" module (the file doing the importing). + /// The Value (`Vec`) is a list of IDs of the "Child" modules it relies on. + /// + /// Example: If `main.simf` (ID: 0) has `use lib::math;` (ID: 1) and `use lib::io;` (ID: 2), + /// this map will contain: `{ 0: [1, 2] }`. + pub dependencies: HashMap>, +} + +fn parse_and_get_program(prog_file: &Path) -> Result { + let prog_text = std::fs::read_to_string(prog_file).map_err(|e| e.to_string())?; + let file = prog_text.into(); + let mut error_handler = crate::error::ErrorCollector::new(Arc::clone(&file)); + + if let Some(program) = parse::Program::parse_from_str_with_errors(&file, &mut error_handler) { + Ok(program) + } else { + Err(ErrorCollector::to_string(&error_handler))? + } +} + +impl ProjectGraph { + pub fn new(lib_cfg: &LibConfig, root_program: &parse::Program) -> Result { + let mut modules: Vec = vec![Module { + parsed_program: root_program.clone(), + }]; + let mut lookup: HashMap = HashMap::new(); + let mut dependencies: HashMap> = HashMap::new(); + + let root_id = 0; + lookup.insert(lib_cfg.root_path.clone(), root_id); + dependencies.insert(root_id, Vec::new()); + + // Implementation of the standard BFS algorithm with memoization and queue + let mut queue = VecDeque::new(); + queue.push_back(root_id); + + while let Some(curr_id) = queue.pop_front() { + let mut pending_imports: Vec = Vec::new(); + let current_program = &modules[curr_id].parsed_program; + + for elem in current_program.items() { + if let parse::Item::Use(use_decl) = elem { + if let Ok(path) = lib_cfg.get_full_path(use_decl) { + pending_imports.push(path); + } + } + } + + for path in pending_imports { + let full_path = path.with_extension("simf"); + + if !full_path.is_file() { + return Err(format!("File in {:?}, does not exist", full_path)); + } + + if let Some(&existing_id) = lookup.get(&path) { + dependencies.entry(curr_id).or_default().push(existing_id); + continue; + } + + let last_ind = modules.len(); + let program = parse_and_get_program(&full_path)?; + + modules.push(Module { + parsed_program: program, + }); + lookup.insert(path.clone(), last_ind); + dependencies.entry(curr_id).or_default().push(last_ind); + + queue.push_back(last_ind); + } + } + + Ok(Self { + modules, + lookup, + dependencies, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::{self, File}; + use std::io::Write; + use std::path::Path; + use tempfile::TempDir; + + // --- Helper to setup environment --- + // Creates a file with specific content in the temp directory + fn create_simf_file(dir: &Path, rel_path: &str, content: &str) -> PathBuf { + let full_path = dir.join(rel_path); + + // Ensure parent directories exist + if let Some(parent) = full_path.parent() { + fs::create_dir_all(parent).unwrap(); + } + + let mut file = File::create(&full_path).expect("Failed to create file"); + file.write_all(content.as_bytes()) + .expect("Failed to write content"); + full_path + } + + // Helper to mock the initial root program parsing + fn parse_root(path: &Path) -> parse::Program { + parse_and_get_program(path).expect("Root parsing failed") + } + + #[test] + fn test_simple_import() { + // Setup: + // root.simf -> "use std::math;" + // libs/std/math.simf -> "" + + let temp_dir = TempDir::new().unwrap(); + let root_path = create_simf_file(temp_dir.path(), "root.simf", "use std::math::some_func;"); + create_simf_file(temp_dir.path(), "libs/std/math.simf", ""); + + // Setup Library Map + let mut lib_map = HashMap::new(); + lib_map.insert("std".to_string(), temp_dir.path().join("libs/std")); + + // Parse Root + let root_program = parse_root(&root_path); + let config = LibConfig::new(lib_map, &root_path); + + // Run Logic + let graph = ProjectGraph::new(&config, &root_program).expect("Graph build failed"); + + // Assertions + assert_eq!(graph.modules.len(), 2, "Should have Root and Math module"); + assert!( + graph.dependencies[&0].contains(&1), + "Root should depend on Math" + ); + } + + #[test] + fn test_diamond_dependency_deduplication() { + // Setup: + // root -> imports A, B + // A -> imports Common + // B -> imports Common + // Expected: Common loaded ONLY ONCE. + + let temp_dir = TempDir::new().unwrap(); + let root_path = create_simf_file( + temp_dir.path(), + "root.simf", + "use lib::A::foo; use lib::B::bar;", + ); + create_simf_file( + temp_dir.path(), + "libs/lib/A.simf", + "use lib::Common::dummy1;", + ); + create_simf_file( + temp_dir.path(), + "libs/lib/B.simf", + "use lib::Common::dummy2;", + ); + create_simf_file(temp_dir.path(), "libs/lib/Common.simf", ""); // Empty leaf + + let mut lib_map = HashMap::new(); + lib_map.insert("lib".to_string(), temp_dir.path().join("libs/lib")); + + let root_program = parse_root(&root_path); + let config = LibConfig::new(lib_map, &root_path); + let graph = ProjectGraph::new(&config, &root_program).expect("Graph build failed"); + + // Assertions + // Structure: Root(0), A(1), B(2), Common(3) + assert_eq!( + graph.modules.len(), + 4, + "Should resolve exactly 4 unique modules" + ); + + // Check A -> Common + let a_id = 1; + let common_id = 3; + assert!(graph.dependencies[&a_id].contains(&common_id)); + + // Check B -> Common (Should point to SAME ID) + let b_id = 2; + assert!(graph.dependencies[&b_id].contains(&common_id)); + } + + #[test] + fn test_cyclic_dependency() { + // Setup: + // A -> imports B + // B -> imports A + // Expected: Should finish without infinite loop + + let temp_dir = TempDir::new().unwrap(); + let a_path = create_simf_file( + temp_dir.path(), + "libs/test/A.simf", + "use test::B::some_test;", + ); + create_simf_file( + temp_dir.path(), + "libs/test/B.simf", + "use test::A::another_test;", + ); + + let mut lib_map = HashMap::new(); + lib_map.insert("test".to_string(), temp_dir.path().join("libs/test")); + + let root_program = parse_root(&a_path); + let config = LibConfig::new(lib_map, &a_path); + let graph = ProjectGraph::new(&config, &root_program).expect("Graph build failed"); + + println!("Graph dependencies: {:?}", graph.dependencies); + println!("lookup: {:?}", graph.lookup); + assert_eq!(graph.modules.len(), 2, "Should only have A and B"); + + // A depends on B + assert!(graph.dependencies[&0].contains(&1)); + // B depends on A (Circular) + assert!(graph.dependencies[&1].contains(&0)); + } + + #[test] + fn test_missing_file_error() { + // Setup: + // root -> imports missing_lib + + let temp_dir = TempDir::new().unwrap(); + let root_path = create_simf_file(temp_dir.path(), "root.simf", "use std::ghost;"); + // We do NOT create ghost.simf + + let mut lib_map = HashMap::new(); + lib_map.insert("std".to_string(), temp_dir.path().join("libs/std")); + + let root_program = parse_root(&root_path); + let config = LibConfig::new(lib_map, &root_path); + let result = ProjectGraph::new(&config, &root_program); + + assert!(result.is_err(), "Should fail for missing file"); + let err_msg = result.err().unwrap(); + assert!( + err_msg.contains("does not exist"), + "Error message should mention missing file" + ); + } + + #[test] + fn test_ignores_unmapped_imports() { + // Setup: + // root -> "use unknown::library;" + // "unknown" is NOT in library_map. + // Expected: It should simply skip this import (based on `if let Ok(path)` logic) + + let temp_dir = TempDir::new().unwrap(); + let root_path = create_simf_file(temp_dir.path(), "root.simf", "use unknown::library;"); + + let lib_map = HashMap::new(); + + let root_program = parse_root(&root_path); + let config = LibConfig::new(lib_map, &root_path); + let graph = + ProjectGraph::new(&config, &root_program).expect("Should succeed but ignore import"); + + assert_eq!(graph.modules.len(), 1, "Should only contain root"); + assert!( + graph.dependencies[&0].is_empty(), + "Root should have no resolved dependencies" + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index b4a1032a..8d3d9667 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub mod array; pub mod ast; pub mod compile; pub mod debug; +pub mod driver; pub mod dummy_env; pub mod error; pub mod jet; @@ -20,6 +21,8 @@ pub mod types; pub mod value; mod witness; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; use std::sync::Arc; use simplicity::jet::elements::ElementsEnv; @@ -30,12 +33,46 @@ pub extern crate simplicity; pub use simplicity::elements; use crate::debug::DebugSymbols; +use crate::driver::ProjectGraph; use crate::error::{ErrorCollector, WithFile}; use crate::parse::ParseFromStrWithErrors; pub use crate::types::ResolvedType; pub use crate::value::Value; pub use crate::witness::{Arguments, Parameters, WitnessTypes, WitnessValues}; +pub struct LibConfig { + pub libraries: HashMap, + pub root_path: PathBuf, +} + +impl LibConfig { + pub fn new(libraries: HashMap, raw_root_path: &Path) -> Self { + let root_path = raw_root_path.with_extension(""); + + Self { + libraries, + root_path, + } + } + + pub fn get_full_path(&self, use_decl: &parse::UseDecl) -> Result { + let parts: Vec<&str> = use_decl.path().iter().map(|s| s.as_ref()).collect(); + let first_segment = parts[0]; + + if let Some(lib_root) = self.libraries.get(first_segment) { + let mut full_path = lib_root.clone(); + full_path.extend(&parts[1..]); + + return Ok(full_path); + } + + Err(format!( + "Unknown module or library '{}'. Did you forget to pass --lib {}=...?", + first_segment, first_segment, + )) + } +} + /// The template of a SimplicityHL program. /// /// A template has parameterized values that need to be supplied with arguments. @@ -51,11 +88,18 @@ impl TemplateProgram { /// ## Errors /// /// The string is not a valid SimplicityHL program. - pub fn new>>(s: Str) -> Result { + pub fn new>>(lib_cfg: Option<&LibConfig>, s: Str) -> Result { let file = s.into(); let mut error_handler = ErrorCollector::new(Arc::clone(&file)); let parse_program = parse::Program::parse_from_str_with_errors(&file, &mut error_handler); + if let Some(program) = parse_program { + let _ = if let Some(lib_cfg) = lib_cfg { + Some(ProjectGraph::new(lib_cfg, &program)?) + } else { + None + }; + let ast_program = ast::Program::analyze(&program).with_file(Arc::clone(&file))?; Ok(Self { simfony: ast_program, @@ -129,11 +173,12 @@ impl CompiledProgram { /// - [`TemplateProgram::new`] /// - [`TemplateProgram::instantiate`] pub fn new>>( + lib_cfg: Option<&LibConfig>, s: Str, arguments: Arguments, include_debug_symbols: bool, ) -> Result { - TemplateProgram::new(s) + TemplateProgram::new(lib_cfg, s) .and_then(|template| template.instantiate(arguments, include_debug_symbols)) } @@ -213,12 +258,13 @@ impl SatisfiedProgram { /// - [`TemplateProgram::instantiate`] /// - [`CompiledProgram::satisfy`] pub fn new>>( + lib_cfg: Option<&LibConfig>, s: Str, arguments: Arguments, witness_values: WitnessValues, include_debug_symbols: bool, ) -> Result { - let compiled = CompiledProgram::new(s, arguments, include_debug_symbols)?; + let compiled = CompiledProgram::new(lib_cfg, s, arguments, include_debug_symbols)?; compiled.satisfy(witness_values) } @@ -321,7 +367,7 @@ pub(crate) mod tests { } pub fn template_text(program_text: Cow) -> Self { - let program = match TemplateProgram::new(program_text.as_ref()) { + let program = match TemplateProgram::new(None, program_text.as_ref()) { Ok(x) => x, Err(error) => panic!("{error}"), }; @@ -658,6 +704,7 @@ fn main() { } "#; match SatisfiedProgram::new( + None, prog_text, Arguments::default(), WitnessValues::default(), diff --git a/src/main.rs b/src/main.rs index 0be2b086..dbbeaa1b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,8 @@ use base64::display::Base64Display; use base64::engine::general_purpose::STANDARD; use clap::{Arg, ArgAction, Command}; -use simplicityhl::{AbiMeta, CompiledProgram}; -use std::{env, fmt}; +use simplicityhl::{AbiMeta, CompiledProgram, LibConfig}; +use std::{collections::HashMap, env, fmt, path::PathBuf}; #[cfg_attr(feature = "serde", derive(serde::Serialize))] /// The compilation output. @@ -46,6 +46,14 @@ fn main() -> Result<(), Box> { .action(ArgAction::Set) .help("SimplicityHL program file to build"), ) + .arg( + Arg::new("library") + .long("lib") + .short('L') + .value_name("ALIAS=PATH") + .action(ArgAction::Append) + .help("Link a library with an alias (e.g., --lib math=./libs/math)"), + ) .arg( Arg::new("wit_file") .long("wit") @@ -110,13 +118,33 @@ fn main() -> Result<(), Box> { simplicityhl::Arguments::default() }; - let compiled = match CompiledProgram::new(prog_text, args_opt, include_debug_symbols) { - Ok(program) => program, - Err(e) => { - eprintln!("{}", e); - std::process::exit(1); - } - }; + let lib_args = matches.get_many::("library").unwrap_or_default(); + + let library_map: HashMap = lib_args + .map(|arg| { + let parts: Vec<&str> = arg.splitn(2, '=').collect(); + + if parts.len() != 2 { + eprintln!( + "Error: Library argument must be in format ALIAS=PATH, got '{}'", + arg + ); + std::process::exit(1); + } + + (parts[0].to_string(), std::path::PathBuf::from(parts[1])) + }) + .collect(); + + let config = LibConfig::new(library_map, prog_path); + let compiled = + match CompiledProgram::new(Some(&config), prog_text, args_opt, include_debug_symbols) { + Ok(program) => program, + Err(e) => { + eprintln!("{}", e); + std::process::exit(1); + } + }; #[cfg(feature = "serde")] let witness_opt = matches diff --git a/src/str.rs b/src/str.rs index a113a703..711e4471 100644 --- a/src/str.rs +++ b/src/str.rs @@ -115,6 +115,12 @@ impl<'a> arbitrary::Arbitrary<'a> for FunctionName { #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] pub struct Identifier(Arc); +impl AsRef for Identifier { + fn as_ref(&self) -> &str { + &self.0 + } +} + wrapped_string!(Identifier, "variable identifier"); impl_arbitrary_lowercase_alpha!(Identifier); diff --git a/src/tracker.rs b/src/tracker.rs index 4a6f693a..82f15a49 100644 --- a/src/tracker.rs +++ b/src/tracker.rs @@ -472,7 +472,7 @@ mod tests { #[test] fn test_debug_and_jet_tracing() { - let program = TemplateProgram::new(TEST_PROGRAM).unwrap(); + let program = TemplateProgram::new(None, TEST_PROGRAM).unwrap(); let program = program.instantiate(Arguments::default(), true).unwrap(); let satisfied = program.satisfy(WitnessValues::default()).unwrap(); @@ -541,7 +541,7 @@ mod tests { fn test_arith_jet_trace_regression() { let env = create_test_env(); - let program = TemplateProgram::new(TEST_ARITHMETIC_JETS).unwrap(); + let program = TemplateProgram::new(None, TEST_ARITHMETIC_JETS).unwrap(); let program = program.instantiate(Arguments::default(), true).unwrap(); let satisfied = program.satisfy(WitnessValues::default()).unwrap(); diff --git a/src/witness.rs b/src/witness.rs index 6d6ffefc..8cd614c8 100644 --- a/src/witness.rs +++ b/src/witness.rs @@ -247,7 +247,7 @@ mod tests { WitnessName::from_str_unchecked("A"), Value::u16(42), )])); - match SatisfiedProgram::new(s, Arguments::default(), witness, false) { + match SatisfiedProgram::new(None, s, Arguments::default(), witness, false) { Ok(_) => panic!("Ill-typed witness assignment was falsely accepted"), Err(error) => assert_eq!( "Witness `A` was declared with type `u32` but its assigned value is of type `u16`", @@ -266,7 +266,7 @@ fn main() { assert!(jet::is_zero_32(f())); }"#; - match CompiledProgram::new(s, Arguments::default(), false) { + match CompiledProgram::new(None, s, Arguments::default(), false) { Ok(_) => panic!("Witness outside main was falsely accepted"), Err(error) => { assert!(error From e94e196046716287775fa5451f54f8d92f510fec Mon Sep 17 00:00:00 2001 From: Sdoba16 Date: Wed, 11 Feb 2026 17:21:29 +0200 Subject: [PATCH 03/13] Added C3 linearization --- src/driver.rs | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/src/driver.rs b/src/driver.rs index 8f8e3a3a..83a56a60 100644 --- a/src/driver.rs +++ b/src/driver.rs @@ -36,6 +36,12 @@ pub struct ProjectGraph { pub dependencies: HashMap>, } +#[derive(Debug)] +pub enum C3Error { + CycleDetected(Vec), + InconsistentLinearization { module: usize }, +} + fn parse_and_get_program(prog_file: &Path) -> Result { let prog_text = std::fs::read_to_string(prog_file).map_err(|e| e.to_string())?; let file = prog_text.into(); @@ -107,6 +113,88 @@ impl ProjectGraph { dependencies, }) } + + pub fn c3_linearize(&self) -> Result, C3Error> { + self.linearize_module(0) + } + + fn linearize_module(&self, root: usize) -> Result, C3Error> { + let mut memo = HashMap::>::new(); + let mut visiting = Vec::::new(); + + self.linearize_rec(root, &mut memo, &mut visiting) + } + + fn linearize_rec( + &self, + module: usize, + memo: &mut HashMap>, + visiting: &mut Vec, + ) -> Result, C3Error> { + if let Some(result) = memo.get(&module) { + return Ok(result.clone()); + } + + if visiting.contains(&module) { + let cycle_start = visiting.iter().position(|m| *m == module).unwrap(); + return Err(C3Error::CycleDetected(visiting[cycle_start..].to_vec())); + } + + visiting.push(module); + + let parents = self.dependencies.get(&module).cloned().unwrap_or_default(); + + let mut seqs: Vec> = Vec::new(); + + for parent in &parents { + let lin = self.linearize_rec(*parent, memo, visiting)?; + seqs.push(lin); + } + + seqs.push(parents.clone()); + + let mut result = vec![module]; + let merged = merge(seqs).ok_or(C3Error::InconsistentLinearization { module })?; + + result.extend(merged); + + visiting.pop(); + memo.insert(module, result.clone()); + + Ok(result) + } +} + +fn merge(mut seqs: Vec>) -> Option> { + let mut result = Vec::new(); + + loop { + seqs.retain(|s| !s.is_empty()); + if seqs.is_empty() { + return Some(result); + } + + let mut candidate = None; + + 'outer: for seq in &seqs { + let head = seq[0]; + + if seqs.iter().all(|s| !s[1..].contains(&head)) { + candidate = Some(head); + break 'outer; + } + } + + let head = candidate?; + + result.push(head); + + for seq in &mut seqs { + if seq.first() == Some(&head) { + seq.remove(0); + } + } + } } #[cfg(test)] @@ -167,6 +255,24 @@ mod tests { ); } + #[test] + fn test_c3_simple_import() { + let temp_dir = TempDir::new().unwrap(); + let root_path = create_simf_file(temp_dir.path(), "root.simf", "use std::math::some_func;"); + create_simf_file(temp_dir.path(), "libs/std/math.simf", ""); + + let mut lib_map = HashMap::new(); + lib_map.insert("std".to_string(), temp_dir.path().join("libs/std")); + + let root_program = parse_root(&root_path); + let config = LibConfig::new(lib_map, &root_path); + let graph = ProjectGraph::new(&config, &root_program).expect("Graph build failed"); + + let order = graph.c3_linearize().expect("C3 failed"); + + assert_eq!(order, vec![0, 1]); + } + #[test] fn test_diamond_dependency_deduplication() { // Setup: @@ -218,6 +324,44 @@ mod tests { assert!(graph.dependencies[&b_id].contains(&common_id)); } + #[test] + fn test_c3_diamond_dependency_deduplication() { + // Setup: + // root -> imports A, B + // A -> imports Common + // B -> imports Common + // Expected: Common loaded ONLY ONCE. + + let temp_dir = TempDir::new().unwrap(); + let root_path = create_simf_file( + temp_dir.path(), + "root.simf", + "use lib::A::foo; use lib::B::bar;", + ); + create_simf_file( + temp_dir.path(), + "libs/lib/A.simf", + "use lib::Common::dummy1;", + ); + create_simf_file( + temp_dir.path(), + "libs/lib/B.simf", + "use lib::Common::dummy2;", + ); + create_simf_file(temp_dir.path(), "libs/lib/Common.simf", ""); // Empty leaf + + let mut lib_map = HashMap::new(); + lib_map.insert("lib".to_string(), temp_dir.path().join("libs/lib")); + + let root_program = parse_root(&root_path); + let config = LibConfig::new(lib_map, &root_path); + let graph = ProjectGraph::new(&config, &root_program).expect("Graph build failed"); + + let order = graph.c3_linearize().expect("C3 failed"); + + assert_eq!(order, vec![0, 1, 2, 3],); + } + #[test] fn test_cyclic_dependency() { // Setup: @@ -254,6 +398,36 @@ mod tests { assert!(graph.dependencies[&1].contains(&0)); } + #[test] + fn test_c3_cyclic_dependency() { + // Setup: + // A -> imports B + // B -> imports A + // Expected: Should finish without infinite loop + + let temp_dir = TempDir::new().unwrap(); + let a_path = create_simf_file( + temp_dir.path(), + "libs/test/A.simf", + "use test::B::some_test;", + ); + create_simf_file( + temp_dir.path(), + "libs/test/B.simf", + "use test::A::another_test;", + ); + + let mut lib_map = HashMap::new(); + lib_map.insert("test".to_string(), temp_dir.path().join("libs/test")); + + let root_program = parse_root(&a_path); + let config = LibConfig::new(lib_map, &a_path); + let graph = ProjectGraph::new(&config, &root_program).expect("Graph build failed"); + + let order = graph.c3_linearize().unwrap_err(); + matches!(order, C3Error::CycleDetected(_)); + } + #[test] fn test_missing_file_error() { // Setup: From 7a0acd7f1b4f8f39cfb255acfc37641385502ab4 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Mon, 16 Feb 2026 17:31:10 +0200 Subject: [PATCH 04/13] feat: add build_order function, refactor and test it --- src/driver.rs | 337 ++++++++++++++++++++++++++++++++++++++++++++++---- src/lib.rs | 14 ++- src/parse.rs | 4 + src/str.rs | 19 +++ 4 files changed, 346 insertions(+), 28 deletions(-) diff --git a/src/driver.rs b/src/driver.rs index 83a56a60..edac8d86 100644 --- a/src/driver.rs +++ b/src/driver.rs @@ -2,8 +2,9 @@ use std::collections::{HashMap, VecDeque}; use std::path::{Path, PathBuf}; use std::sync::Arc; -use crate::error::ErrorCollector; -use crate::parse::{self, ParseFromStrWithErrors}; +use crate::error::{ErrorCollector, Span}; +use crate::parse::{self, ParseFromStrWithErrors, Visibility}; +use crate::str::Identifier; use crate::LibConfig; /// Represents a single, isolated file in the SimplicityHL project. @@ -19,13 +20,22 @@ pub struct Module { pub struct ProjectGraph { /// Arena Pattern: the data itself lives here. /// A flat vector guarantees that module data is stored contiguously in memory. - pub modules: Vec, + pub(self) modules: Vec, + + /// The configuration environment. + /// Used to resolve xternal library dependencies and invoke their associated functions. + pub config: Arc, /// Fast lookup: File Path -> Module ID. /// A reverse index mapping absolute file paths to their internal IDs. /// This solves the duplication problem, ensuring each file is only parsed once. pub lookup: HashMap, + /// Fast lookup: Module ID -> File Path. + /// A direct index mapping internal IDs back to their absolute file paths. + /// This serves as the exact inverse of the `lookup` map. + pub paths: Vec, + /// The Adjacency List: Defines the Directed acyclic Graph (DAG) of imports. /// /// The Key (`usize`) is the ID of a "Parent" module (the file doing the importing). @@ -36,6 +46,18 @@ pub struct ProjectGraph { pub dependencies: HashMap>, } +#[derive(Clone, Debug)] +pub struct Resolution { + pub visibility: Visibility, +} + +pub struct Program { + //pub graph: ProjectGraph, + pub items: Arc<[parse::Item]>, + pub scope_items: Vec>, + pub span: Span, +} + #[derive(Debug)] pub enum C3Error { CycleDetected(Vec), @@ -55,15 +77,16 @@ fn parse_and_get_program(prog_file: &Path) -> Result { } impl ProjectGraph { - pub fn new(lib_cfg: &LibConfig, root_program: &parse::Program) -> Result { + pub fn new(config: Arc, root_program: &parse::Program) -> Result { let mut modules: Vec = vec![Module { parsed_program: root_program.clone(), }]; let mut lookup: HashMap = HashMap::new(); + let mut paths: Vec = vec![config.root_path.clone()]; let mut dependencies: HashMap> = HashMap::new(); let root_id = 0; - lookup.insert(lib_cfg.root_path.clone(), root_id); + lookup.insert(config.root_path.clone(), root_id); dependencies.insert(root_id, Vec::new()); // Implementation of the standard BFS algorithm with memoization and queue @@ -76,7 +99,7 @@ impl ProjectGraph { for elem in current_program.items() { if let parse::Item::Use(use_decl) = elem { - if let Ok(path) = lib_cfg.get_full_path(use_decl) { + if let Ok(path) = config.get_full_path(use_decl) { pending_imports.push(path); } } @@ -101,6 +124,7 @@ impl ProjectGraph { parsed_program: program, }); lookup.insert(path.clone(), last_ind); + paths.push(path.clone()); dependencies.entry(curr_id).or_default().push(last_ind); queue.push_back(last_ind); @@ -109,7 +133,9 @@ impl ProjectGraph { Ok(Self { modules, + config, lookup, + paths, dependencies, }) } @@ -163,6 +189,117 @@ impl ProjectGraph { Ok(result) } + + fn process_use_item( + scope_items: &mut [HashMap], + file_id: usize, + ind: usize, + elem: &Identifier, + use_decl_visibility: Visibility, + ) -> Result<(), String> { + if matches!( + scope_items[ind][elem].visibility, + parse::Visibility::Private + ) { + return Err(format!( + "Function {} is private and cannot be used.", + elem.as_inner() + )); + } + + scope_items[file_id].insert( + elem.clone(), + Resolution { + visibility: use_decl_visibility, + }, + ); + + Ok(()) + } + + fn register_def( + items: &mut Vec, + scope: &mut HashMap, + item: &parse::Item, + name: Identifier, + vis: &parse::Visibility, + ) { + items.push(item.clone()); + scope.insert( + name, + Resolution { + visibility: vis.clone(), + }, + ); + } + + // TODO: @LesterEvSe, consider processing more than one error at a time + fn build_program(&self, order: &Vec) -> Result { + let mut items: Vec = Vec::new(); + let mut scope_items: Vec> = + vec![HashMap::new(); order.len()]; + + for &file_id in order { + let program_items = self.modules[file_id].parsed_program.items(); + + for elem in program_items { + match elem { + parse::Item::Use(use_decl) => { + let full_path = self.config.get_full_path(use_decl)?; + let ind = self.lookup[&full_path]; + let visibility = use_decl.visibility(); + + let use_targets = match use_decl.items() { + parse::UseItems::Single(elem) => std::slice::from_ref(elem), + parse::UseItems::List(elems) => elems.as_slice(), + }; + + for target in use_targets { + ProjectGraph::process_use_item( + &mut scope_items, + file_id, + ind, + target, + visibility.clone(), + )?; + } + } + parse::Item::TypeAlias(alias) => { + Self::register_def( + &mut items, + &mut scope_items[file_id], + elem, + alias.name().clone().into(), + alias.visibility(), + ); + } + parse::Item::Function(function) => { + Self::register_def( + &mut items, + &mut scope_items[file_id], + elem, + function.name().clone().into(), + function.visibility(), + ); + } + parse::Item::Module => {} + } + } + } + + Ok(Program { + items: items.into(), + scope_items, + span: *self.modules[0].parsed_program.as_ref(), + }) + } + + pub fn resolve_complication_order(&self) -> Result { + // TODO: @LesterEvSe, resolve errors more appropriately + let mut order = self.c3_linearize().unwrap(); + order.reverse(); + self.build_program(&order) + } } fn merge(mut seqs: Vec>) -> Option> { @@ -226,6 +363,160 @@ mod tests { parse_and_get_program(path).expect("Root parsing failed") } + /// Initializes a graph environment for testing. + /// Returns: + /// 1. The constructed `ProjectGraph`. + /// 2. A `HashMap` mapping filenames (e.g., "A.simf") to their `FileID` (usize). + /// 3. The `TempDir` (to keep files alive during the test). + fn setup_graph(files: Vec<(&str, &str)>) -> (ProjectGraph, HashMap, TempDir) { + let temp_dir = TempDir::new().unwrap(); + let mut lib_map = HashMap::new(); + + // Define the standard library path structure + let lib_path = temp_dir.path().join("libs/lib"); + lib_map.insert("lib".to_string(), lib_path); + + let mut root_path = None; + + // Create all requested files + for (name, content) in files { + if name == "main.simf" { + root_path = Some(create_simf_file(temp_dir.path(), name, content)); + } else { + // Names should be passed like "libs/lib/A.simf" + create_simf_file(temp_dir.path(), name, content); + } + } + + let root_p = root_path.expect("main.simf must be defined in file list"); + let root_program = parse_root(&root_p); + + let config = Arc::from(LibConfig::new(lib_map, &root_p)); + let graph = ProjectGraph::new(config, &root_program).expect("Failed to build graph"); + + // Create a lookup map for tests: "A.simf" -> FileID + let mut file_ids = HashMap::new(); + for (path, id) in &graph.lookup { + let file_name = path.file_name().unwrap().to_string_lossy().to_string(); + file_ids.insert(file_name, *id); + } + + (graph, file_ids, temp_dir) + } + + #[test] + fn test_local_definitions_visibility() { + // Scenario: + // 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 root_id = *ids.get("main").unwrap(); + let order = vec![root_id]; // Only one file + + let program = graph + .build_program(&order) + .expect("Failed to build program"); + let scope = &program.scope_items[root_id]; + + // Check private function + let private_res = scope + .get(&Identifier::from("private_fn")) + .expect("private_fn missing"); + assert_eq!(private_res.visibility, Visibility::Private); + + // Check public function + let public_res = scope + .get(&Identifier::from("public_fn")) + .expect("public_fn missing"); + assert_eq!(public_res.visibility, Visibility::Public); + } + + #[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 lib::A::foo;"), + ("main.simf", "use lib::B::foo;"), + ]); + + let id_a = *ids.get("A").unwrap(); + let id_b = *ids.get("B").unwrap(); + let id_root = *ids.get("main").unwrap(); + + // Manual topological order: A -> B -> Root + let order = vec![id_a, id_b, id_root]; + + let program = graph + .build_program(&order) + .expect("Failed to build program"); + + // Check B's scope + let scope_b = &program.scope_items[id_b]; + let foo_in_b = scope_b + .get(&Identifier::from("foo")) + .expect("foo missing in B"); + + // This is the critical check: Did `pub use` make it Public in B? + assert_eq!( + foo_in_b.visibility, + Visibility::Public, + "B should re-export foo as Public" + ); + + // Check Root's scope + let scope_root = &program.scope_items[id_root]; + let foo_in_root = scope_root + .get(&Identifier::from("foo")) + .expect("foo missing in Root"); + + // Root imported it via `use` (not pub use), so it should be Private in Root + assert_eq!( + foo_in_root.visibility, + Visibility::Private, + "Root should have foo as Private" + ); + } + + #[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 lib::A::foo;"), // <--- Private binding! + ("main.simf", "use lib::B::foo;"), // <--- Should fail + ]); + + let id_a = *ids.get("A").unwrap(); + let id_b = *ids.get("B").unwrap(); + let id_root = *ids.get("main").unwrap(); + + // Order: A -> B -> Root + let order = vec![id_a, id_b, id_root]; + + let result = graph.build_program(&order); + + assert!( + result.is_err(), + "Build should fail when importing a private binding" + ); + } + #[test] fn test_simple_import() { // Setup: @@ -242,10 +533,10 @@ mod tests { // Parse Root let root_program = parse_root(&root_path); - let config = LibConfig::new(lib_map, &root_path); + let config = Arc::from(LibConfig::new(lib_map, &root_path)); // Run Logic - let graph = ProjectGraph::new(&config, &root_program).expect("Graph build failed"); + let graph = ProjectGraph::new(config, &root_program).expect("Graph build failed"); // Assertions assert_eq!(graph.modules.len(), 2, "Should have Root and Math module"); @@ -265,8 +556,8 @@ mod tests { lib_map.insert("std".to_string(), temp_dir.path().join("libs/std")); let root_program = parse_root(&root_path); - let config = LibConfig::new(lib_map, &root_path); - let graph = ProjectGraph::new(&config, &root_program).expect("Graph build failed"); + let config = Arc::from(LibConfig::new(lib_map, &root_path)); + let graph = ProjectGraph::new(config, &root_program).expect("Graph build failed"); let order = graph.c3_linearize().expect("C3 failed"); @@ -303,8 +594,8 @@ mod tests { lib_map.insert("lib".to_string(), temp_dir.path().join("libs/lib")); let root_program = parse_root(&root_path); - let config = LibConfig::new(lib_map, &root_path); - let graph = ProjectGraph::new(&config, &root_program).expect("Graph build failed"); + let config = Arc::from(LibConfig::new(lib_map, &root_path)); + let graph = ProjectGraph::new(config, &root_program).expect("Graph build failed"); // Assertions // Structure: Root(0), A(1), B(2), Common(3) @@ -354,8 +645,8 @@ mod tests { lib_map.insert("lib".to_string(), temp_dir.path().join("libs/lib")); let root_program = parse_root(&root_path); - let config = LibConfig::new(lib_map, &root_path); - let graph = ProjectGraph::new(&config, &root_program).expect("Graph build failed"); + let config = Arc::from(LibConfig::new(lib_map, &root_path)); + let graph = ProjectGraph::new(config, &root_program).expect("Graph build failed"); let order = graph.c3_linearize().expect("C3 failed"); @@ -385,11 +676,9 @@ mod tests { lib_map.insert("test".to_string(), temp_dir.path().join("libs/test")); let root_program = parse_root(&a_path); - let config = LibConfig::new(lib_map, &a_path); - let graph = ProjectGraph::new(&config, &root_program).expect("Graph build failed"); + let config = Arc::from(LibConfig::new(lib_map, &a_path)); + let graph = ProjectGraph::new(config, &root_program).expect("Graph build failed"); - println!("Graph dependencies: {:?}", graph.dependencies); - println!("lookup: {:?}", graph.lookup); assert_eq!(graph.modules.len(), 2, "Should only have A and B"); // A depends on B @@ -421,8 +710,8 @@ mod tests { lib_map.insert("test".to_string(), temp_dir.path().join("libs/test")); let root_program = parse_root(&a_path); - let config = LibConfig::new(lib_map, &a_path); - let graph = ProjectGraph::new(&config, &root_program).expect("Graph build failed"); + let config = Arc::from(LibConfig::new(lib_map, &a_path)); + let graph = ProjectGraph::new(config, &root_program).expect("Graph build failed"); let order = graph.c3_linearize().unwrap_err(); matches!(order, C3Error::CycleDetected(_)); @@ -441,8 +730,8 @@ mod tests { lib_map.insert("std".to_string(), temp_dir.path().join("libs/std")); let root_program = parse_root(&root_path); - let config = LibConfig::new(lib_map, &root_path); - let result = ProjectGraph::new(&config, &root_program); + let config = Arc::from(LibConfig::new(lib_map, &root_path)); + let result = ProjectGraph::new(config, &root_program); assert!(result.is_err(), "Should fail for missing file"); let err_msg = result.err().unwrap(); @@ -465,9 +754,9 @@ mod tests { let lib_map = HashMap::new(); let root_program = parse_root(&root_path); - let config = LibConfig::new(lib_map, &root_path); + let config = Arc::from(LibConfig::new(lib_map, &root_path)); let graph = - ProjectGraph::new(&config, &root_program).expect("Should succeed but ignore import"); + ProjectGraph::new(config, &root_program).expect("Should succeed but ignore import"); assert_eq!(graph.modules.len(), 1, "Should only contain root"); assert!( diff --git a/src/lib.rs b/src/lib.rs index 8d3d9667..6be6d9e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,11 +35,12 @@ pub use simplicity::elements; use crate::debug::DebugSymbols; use crate::driver::ProjectGraph; use crate::error::{ErrorCollector, WithFile}; -use crate::parse::ParseFromStrWithErrors; +use crate::parse::{ParseFromStrWithErrors, UseDecl}; pub use crate::types::ResolvedType; pub use crate::value::Value; pub use crate::witness::{Arguments, Parameters, WitnessTypes, WitnessValues}; +#[derive(Debug, Clone)] pub struct LibConfig { pub libraries: HashMap, pub root_path: PathBuf, @@ -55,7 +56,7 @@ impl LibConfig { } } - pub fn get_full_path(&self, use_decl: &parse::UseDecl) -> Result { + pub fn get_full_path(&self, use_decl: &UseDecl) -> Result { let parts: Vec<&str> = use_decl.path().iter().map(|s| s.as_ref()).collect(); let first_segment = parts[0]; @@ -94,8 +95,13 @@ impl TemplateProgram { let parse_program = parse::Program::parse_from_str_with_errors(&file, &mut error_handler); if let Some(program) = parse_program { - let _ = if let Some(lib_cfg) = lib_cfg { - Some(ProjectGraph::new(lib_cfg, &program)?) + // TODO: Consider a proper resolution strategy later. + let _: Option = if let Some(cfg) = lib_cfg { + let config_arc = Arc::new(cfg.clone()); + let graph = ProjectGraph::new(config_arc, &program)?; + + // TODO: Perhaps add an `error_handler` here, too. + Some(graph.resolve_complication_order()?) } else { None }; diff --git a/src/parse.rs b/src/parse.rs index 63750ae7..1d84edf8 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -79,6 +79,10 @@ impl UseDecl { &self.path } + pub fn path_buf(&self) -> std::path::PathBuf { + self.path().iter().map(|s| s.as_ref()).collect() + } + /// Access the visibility of the function. pub fn items(&self) -> &UseItems { &self.items diff --git a/src/str.rs b/src/str.rs index 711e4471..ddc280ec 100644 --- a/src/str.rs +++ b/src/str.rs @@ -115,6 +115,25 @@ impl<'a> arbitrary::Arbitrary<'a> for FunctionName { #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] pub struct Identifier(Arc); +impl From for Identifier { + fn from(alias: AliasName) -> Self { + // We move the inner Arc, so this is cheap + Self(alias.0) + } +} + +impl From for Identifier { + fn from(func: FunctionName) -> Self { + Self(func.0) + } +} + +impl From<&str> for Identifier { + fn from(s: &str) -> Self { + Self(Arc::from(s)) + } +} + impl AsRef for Identifier { fn as_ref(&self) -> &str { &self.0 From bc4e6d7b91a4dfd9aef8a2fd198fb98b3a6d9fa0 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Fri, 20 Feb 2026 14:26:09 +0200 Subject: [PATCH 05/13] feat: update AST architecture and test module flow - Refactor AST to include tests and modules - Add simple tests for module flow --- examples/multiple_libs/main.simf | 12 + examples/multiple_libs/math/simple_op.simf | 3 + examples/multiple_libs/merkle/build_root.simf | 5 + examples/single_lib/main.simf | 11 + examples/single_lib/temp/funcs.simf | 5 + examples/single_lib/temp/two.simf | 5 + src/ast.rs | 117 ++-- src/driver.rs | 499 +++++++++++++++--- src/error.rs | 11 + src/lib.rs | 322 +++++++++-- src/main.rs | 28 +- src/parse.rs | 29 +- src/tracker.rs | 15 +- src/witness.rs | 24 +- 14 files changed, 921 insertions(+), 165 deletions(-) create mode 100644 examples/multiple_libs/main.simf create mode 100644 examples/multiple_libs/math/simple_op.simf create mode 100644 examples/multiple_libs/merkle/build_root.simf create mode 100644 examples/single_lib/main.simf create mode 100644 examples/single_lib/temp/funcs.simf create mode 100644 examples/single_lib/temp/two.simf diff --git a/examples/multiple_libs/main.simf b/examples/multiple_libs/main.simf new file mode 100644 index 00000000..8bfee917 --- /dev/null +++ b/examples/multiple_libs/main.simf @@ -0,0 +1,12 @@ +use merkle::build_root::get_root; +use math::simple_op::hash; + +pub fn get_block_value_hash(prev_hash: u32, tx1: u32, tx2: u32) -> u32 { + let root: u32 = get_root(tx1, tx2); + hash(prev_hash, root); +} + +fn main() { + let block_val_hash: u32 = get_block_value(5, 10, 20); + assert!(jet::eq_32(block_val_hash, 27)); +} \ No newline at end of file diff --git a/examples/multiple_libs/math/simple_op.simf b/examples/multiple_libs/math/simple_op.simf new file mode 100644 index 00000000..b152a361 --- /dev/null +++ b/examples/multiple_libs/math/simple_op.simf @@ -0,0 +1,3 @@ +pub fn hash(x: u32, y: u32) -> u32 { + jet::xor_32(x, y) +} \ No newline at end of file diff --git a/examples/multiple_libs/merkle/build_root.simf b/examples/multiple_libs/merkle/build_root.simf new file mode 100644 index 00000000..18242832 --- /dev/null +++ b/examples/multiple_libs/merkle/build_root.simf @@ -0,0 +1,5 @@ +use math::simple_op::hash; + +pub fn get_root(tx1: u32, tx2: u32) -> u32 { + hash(tx1, tx2) +} \ No newline at end of file diff --git a/examples/single_lib/main.simf b/examples/single_lib/main.simf new file mode 100644 index 00000000..19b2b047 --- /dev/null +++ b/examples/single_lib/main.simf @@ -0,0 +1,11 @@ +pub use temp::two::two; +use temp::funcs::{get_five, Smth}; + +fn seven() -> u32 { + 7 +} + +fn main() { + let (_, temp): (bool, u32) = jet::add_32(two(), get_five()); + assert!(jet::eq_32(temp, seven())); +} \ No newline at end of file diff --git a/examples/single_lib/temp/funcs.simf b/examples/single_lib/temp/funcs.simf new file mode 100644 index 00000000..0ff2da55 --- /dev/null +++ b/examples/single_lib/temp/funcs.simf @@ -0,0 +1,5 @@ +pub type Smth = u32; + +pub fn get_five() -> u32 { + 5 +} \ No newline at end of file diff --git a/examples/single_lib/temp/two.simf b/examples/single_lib/temp/two.simf new file mode 100644 index 00000000..aa5cbb53 --- /dev/null +++ b/examples/single_lib/temp/two.simf @@ -0,0 +1,5 @@ +pub use temp::funcs::Smth; + +pub fn two() -> Smth { + 2 +} \ No newline at end of file diff --git a/src/ast.rs b/src/ast.rs index 94ed5c38..7e2a6d85 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -9,6 +9,7 @@ use miniscript::iter::{Tree, TreeLike}; use simplicity::jet::Elements; use crate::debug::{CallTracker, DebugSymbols, TrackedCallName}; +use crate::driver::ProgramResolutions; use crate::error::{Error, RichError, Span, WithSpan}; use crate::num::{NonZeroPow2Usize, Pow2Usize}; use crate::parse::MatchPattern; @@ -19,7 +20,7 @@ use crate::types::{ }; use crate::value::{UIntValue, Value}; use crate::witness::{Parameters, WitnessTypes, WitnessValues}; -use crate::{impl_eq_hash, parse}; +use crate::{driver, impl_eq_hash, parse, SourceName}; /// A program consists of the main function. /// @@ -522,6 +523,10 @@ impl TreeLike for ExprTree<'_> { /// 4. Resolving calls to custom functions #[derive(Clone, Debug, Eq, PartialEq, Default)] struct Scope { + resolutions: ProgramResolutions, + paths: Arc<[SourceName]>, + file_id: usize, // ID of the file from which the function is called. + variables: Vec>, aliases: HashMap, parameters: HashMap, @@ -532,6 +537,26 @@ struct Scope { } impl Scope { + pub fn new(resolutions: ProgramResolutions, paths: Arc<[SourceName]>) -> Self { + Self { + resolutions, + paths, + file_id: 0, + variables: Vec::new(), + aliases: HashMap::new(), + parameters: HashMap::new(), + witnesses: HashMap::new(), + functions: HashMap::new(), + is_main: false, + call_tracker: CallTracker::default(), + } + } + + /// Access to current function file id. + pub fn file_id(&self) -> usize { + self.file_id + } + /// Check if the current scope is topmost. pub fn is_topmost(&self) -> bool { self.variables.is_empty() @@ -542,6 +567,11 @@ impl Scope { self.variables.push(HashMap::new()); } + pub fn push_function_scope(&mut self, file_id: usize) { + self.push_scope(); + self.file_id = file_id; + } + /// Push the scope of the main function onto the stack. /// /// ## Panics @@ -564,6 +594,11 @@ impl Scope { self.variables.pop().expect("Stack is empty"); } + pub fn pop_function_scope(&mut self, previous_file_id: usize) { + self.pop_scope(); + self.file_id = previous_file_id; + } + /// Pop the scope of the main function from the stack. /// /// ## Panics @@ -693,9 +728,39 @@ impl Scope { } } - /// Get the definition of a custom function. - pub fn get_function(&self, name: &FunctionName) -> Option<&CustomFunction> { - self.functions.get(name) + /// Get the definition of a custom function with visibility and existence checks. + /// + /// # Errors + /// + /// - `Error::FileNotFound`: The specified `file_id` does not exist in the resolutions. + /// - `Error::FunctionUndefined`: The function is not found in the file's scope OR not defined globally. + /// - `Error::FunctionIsPrivate`: The function exists but is private (and thus not accessible). + pub fn get_function(&self, name: &FunctionName) -> Result<&CustomFunction, Error> { + // The order of the errors is important! + let function = self + .functions + .get(name) + .ok_or_else(|| Error::FunctionUndefined(name.clone()))?; + + let source_name = self.paths[self.file_id].clone(); + + let file_scope = match source_name { + SourceName::Real(path) => self + .resolutions + .get(self.file_id) + .ok_or(Error::FileNotFound(path))?, // TODO: File or pub type + SourceName::Virtual(_) => { + return Ok(function); + } + }; + + let identifier: Identifier = name.clone().into(); + + if file_scope.contains_key(&identifier) { + Ok(function) + } else { + Err(Error::FunctionIsPrivate(name.clone())) + } } /// Track a call expression with its span. @@ -718,9 +783,10 @@ trait AbstractSyntaxTree: Sized { } impl Program { - pub fn analyze(from: &parse::Program) -> Result { + // TODO: Add visibility check inside program + pub fn analyze(from: &driver::Program) -> Result { let unit = ResolvedType::unit(); - let mut scope = Scope::default(); + let mut scope = Scope::new(Arc::from(from.resolutions()), Arc::from(from.paths())); let items = from .items() .iter() @@ -746,36 +812,37 @@ impl Program { } impl AbstractSyntaxTree for Item { - type From = parse::Item; + type From = driver::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"); match from { - parse::Item::TypeAlias(alias) => { + driver::Item::TypeAlias(alias) => { scope .insert_alias(alias.name().clone(), alias.ty().clone()) .with_span(alias)?; Ok(Self::TypeAlias) } - parse::Item::Function(function) => { + driver::Item::Function(function) => { Function::analyze(function, ty, scope).map(Self::Function) } - parse::Item::Use(_) => todo!(), - parse::Item::Module => Ok(Self::Module), + driver::Item::Module => Ok(Self::Module), } } } impl AbstractSyntaxTree for Function { - type From = parse::Function; + type From = driver::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"); + let previous_file_id = scope.file_id(); if from.name().as_inner() != "main" { + let file_id = from.file_id(); let params = from .params() .iter() @@ -792,12 +859,12 @@ impl AbstractSyntaxTree for Function { .map(|aliased| scope.resolve(aliased).with_span(from)) .transpose()? .unwrap_or_else(ResolvedType::unit); - scope.push_scope(); + scope.push_function_scope(file_id); 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.pop_function_scope(previous_file_id); debug_assert!(scope.is_topmost()); let function = CustomFunction { params, body }; scope @@ -1322,14 +1389,9 @@ impl AbstractSyntaxTree for CallName { .get_function(name) .cloned() .map(Self::Custom) - .ok_or(Error::FunctionUndefined(name.clone())) .with_span(from), parse::CallName::ArrayFold(name, size) => { - let function = scope - .get_function(name) - .cloned() - .ok_or(Error::FunctionUndefined(name.clone())) - .with_span(from)?; + let function = scope.get_function(name).cloned().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() @@ -1340,11 +1402,7 @@ impl AbstractSyntaxTree for CallName { } } parse::CallName::Fold(name, bound) => { - let function = scope - .get_function(name) - .cloned() - .ok_or(Error::FunctionUndefined(name.clone())) - .with_span(from)?; + let function = scope.get_function(name).cloned().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() @@ -1355,11 +1413,7 @@ impl AbstractSyntaxTree for CallName { } } parse::CallName::ForWhile(name) => { - let function = scope - .get_function(name) - .cloned() - .ok_or(Error::FunctionUndefined(name.clone())) - .with_span(from)?; + let function = scope.get_function(name).cloned().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 @@ -1435,6 +1489,9 @@ fn analyze_named_module( from: &parse::ModuleProgram, ) -> Result, RichError> { let unit = ResolvedType::unit(); + + // IMPORTANT! If modules allow imports, then we need to consider + // passing the resolution conetxt by calling `Scope::new(resolutions)` let mut scope = Scope::default(); let items = from .items() diff --git a/src/driver.rs b/src/driver.rs index edac8d86..a31ed548 100644 --- a/src/driver.rs +++ b/src/driver.rs @@ -1,11 +1,13 @@ -use std::collections::{HashMap, VecDeque}; +use std::collections::{BTreeMap, HashMap, VecDeque}; +use std::fmt; use std::path::{Path, PathBuf}; use std::sync::Arc; use crate::error::{ErrorCollector, Span}; use crate::parse::{self, ParseFromStrWithErrors, Visibility}; -use crate::str::Identifier; -use crate::LibConfig; +use crate::str::{AliasName, FunctionName, Identifier}; +use crate::types::AliasedType; +use crate::{get_full_path, impl_eq_hash, LibTable, SourceName}; /// Represents a single, isolated file in the SimplicityHL project. /// In this architecture, a file and a module are the exact same thing. @@ -24,17 +26,17 @@ pub struct ProjectGraph { /// The configuration environment. /// Used to resolve xternal library dependencies and invoke their associated functions. - pub config: Arc, + pub libraries: Arc, - /// Fast lookup: File Path -> Module ID. + /// Fast lookup: `SourceName` -> Module ID. /// A reverse index mapping absolute file paths to their internal IDs. /// This solves the duplication problem, ensuring each file is only parsed once. - pub lookup: HashMap, + pub lookup: HashMap, - /// Fast lookup: Module ID -> File Path. + /// Fast lookup: Module ID -> `SourceName`. /// A direct index mapping internal IDs back to their absolute file paths. /// This serves as the exact inverse of the `lookup` map. - pub paths: Vec, + pub paths: Arc<[SourceName]>, /// The Adjacency List: Defines the Directed acyclic Graph (DAG) of imports. /// @@ -46,18 +48,239 @@ pub struct ProjectGraph { pub dependencies: HashMap>, } -#[derive(Clone, Debug)] +// TODO: @LesterEvSe, Consider to change BTreeMap to BTreeSet here +pub type FileResolutions = BTreeMap; + +pub type ProgramResolutions = Arc<[FileResolutions]>; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Resolution { pub visibility: Visibility, } +#[derive(Clone, Debug)] pub struct Program { - //pub graph: ProjectGraph, - pub items: Arc<[parse::Item]>, - pub scope_items: Vec>, - pub span: Span, + items: Arc<[Item]>, + paths: Arc<[SourceName]>, + + // Use BTreeMap instead of HashMap for the impl_eq_hash! macro. + resolutions: ProgramResolutions, + span: Span, +} + +impl Program { + pub fn from_parse(parsed: &parse::Program, root_path: SourceName) -> Result { + let root_path = root_path.without_extension(); + + let mut items: Vec = Vec::new(); + let mut resolutions: Vec = vec![BTreeMap::new()]; + + let main_file_id = 0usize; + let mut errors: Vec = Vec::new(); + + for item in parsed.items() { + match item { + parse::Item::Use(_) => { + errors.push("Unsuitable Use type".to_string()); + } + parse::Item::TypeAlias(alias) => { + let res = ProjectGraph::register_def( + &mut items, + &mut resolutions, + main_file_id, + item, + alias.name().clone().into(), + &parse::Visibility::Public, + ); + + if let Err(e) = res { + errors.push(e); + } + } + parse::Item::Function(function) => { + let res = ProjectGraph::register_def( + &mut items, + &mut resolutions, + main_file_id, + item, + function.name().clone().into(), + &parse::Visibility::Public, + ); + + if let Err(e) = res { + errors.push(e); + } + } + parse::Item::Module => {} + } + } + + if !errors.is_empty() { + return Err(errors.join("\n")); + } + + Ok(Program { + items: items.into(), + paths: Arc::from([root_path]), + resolutions: resolutions.into(), + span: *parsed.as_ref(), + }) + } + + /// Access the items of the program. + pub fn items(&self) -> &[Item] { + &self.items + } + + /// Access the paths of the program + pub fn paths(&self) -> &[SourceName] { + &self.paths + } + + /// Access the scope items of the program. + pub fn resolutions(&self) -> &[FileResolutions] { + &self.resolutions + } +} + +impl_eq_hash!(Program; items, paths, resolutions); + +/// An item is a component of a driver Program +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub enum Item { + /// A type alias. + TypeAlias(TypeAlias), + /// A function. + Function(Function), + /// A module, which is ignored. + Module, +} + +impl Item { + pub fn from_parse(parsed: &parse::Item, file_id: usize) -> Result { + match parsed { + parse::Item::TypeAlias(alias) => { + let driver_alias = TypeAlias::from_parse(alias); + Ok(Item::TypeAlias(driver_alias)) + } + parse::Item::Function(func) => { + let driver_func = Function::from_parse(func, file_id); + Ok(Item::Function(driver_func)) + } + parse::Item::Module => Ok(Item::Module), + + // Cannot convert Use to driver::Item + parse::Item::Use(_) => Err("Unsuitable Use type".to_string()), + } + } +} + +/// Definition of a function. +#[derive(Clone, Debug)] +pub struct Function { + file_id: usize, + name: FunctionName, + params: Arc<[parse::FunctionParam]>, + ret: Option, + body: parse::Expression, + span: Span, +} + +impl Function { + /// Converts a parser function to a driver function. + /// + /// We explicitly pass `file_id` here because the `parse::Function` + /// doesn't know which file it came from. + pub fn from_parse(parsed: &parse::Function, file_id: usize) -> Self { + Self { + file_id, + name: parsed.name().clone(), + params: Arc::from(parsed.params()), + ret: parsed.ret().cloned(), + body: parsed.body().clone(), + span: *parsed.as_ref(), + } + } + + /// Access the file id of the function. + pub fn file_id(&self) -> usize { + self.file_id + } + + /// Access the name of the function. + pub fn name(&self) -> &FunctionName { + &self.name + } + + /// Access the parameters of the function. + pub fn params(&self) -> &[parse::FunctionParam] { + &self.params + } + + /// Access the return type of the function. + /// + /// An empty return type means that the function returns the unit value. + pub fn ret(&self) -> Option<&AliasedType> { + self.ret.as_ref() + } + + /// Access the body of the function. + pub fn body(&self) -> &parse::Expression { + &self.body + } + + /// Access the span of the function. + pub fn span(&self) -> &Span { + &self.span + } +} + +impl_eq_hash!(Function; file_id, name, params, ret, body); + +// A type alias. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct TypeAlias { + name: AliasName, + ty: AliasedType, + span: Span, } +impl TypeAlias { + /// Converts a parser function to a driver function. + /// + /// We explicitly pass `file_id` here because the `parse::Function` + /// doesn't know which file it came from. + pub fn from_parse(parsed: &parse::TypeAlias) -> Self { + Self { + name: parsed.name().clone(), + ty: parsed.ty().clone(), + span: *parsed.as_ref(), + } + } + + /// Access the name of the alias. + pub fn name(&self) -> &AliasName { + &self.name + } + + /// Access the type that the alias resolves to. + /// + /// During the parsing stage, the resolved type may include aliases. + /// The compiler will later check if all contained aliases have been declared before. + pub fn ty(&self) -> &AliasedType { + &self.ty + } + + /// Access the span of the alias. + pub fn span(&self) -> &Span { + &self.span + } +} + +impl_eq_hash!(TypeAlias; name, ty); + #[derive(Debug)] pub enum C3Error { CycleDetected(Vec), @@ -77,16 +300,21 @@ fn parse_and_get_program(prog_file: &Path) -> Result { } impl ProjectGraph { - pub fn new(config: Arc, root_program: &parse::Program) -> Result { + pub fn new( + source_name: SourceName, + libraries: Arc, + root_program: &parse::Program, + ) -> Result { + let source_name = source_name.without_extension(); let mut modules: Vec = vec![Module { parsed_program: root_program.clone(), }]; - let mut lookup: HashMap = HashMap::new(); - let mut paths: Vec = vec![config.root_path.clone()]; + let mut lookup: HashMap = HashMap::new(); + let mut paths: Vec = vec![source_name.clone()]; let mut dependencies: HashMap> = HashMap::new(); let root_id = 0; - lookup.insert(config.root_path.clone(), root_id); + lookup.insert(source_name, root_id); dependencies.insert(root_id, Vec::new()); // Implementation of the standard BFS algorithm with memoization and queue @@ -99,7 +327,7 @@ impl ProjectGraph { for elem in current_program.items() { if let parse::Item::Use(use_decl) = elem { - if let Ok(path) = config.get_full_path(use_decl) { + if let Ok(path) = get_full_path(&libraries, use_decl) { pending_imports.push(path); } } @@ -107,12 +335,13 @@ impl ProjectGraph { for path in pending_imports { let full_path = path.with_extension("simf"); + let source_path = SourceName::Real(path); if !full_path.is_file() { return Err(format!("File in {:?}, does not exist", full_path)); } - if let Some(&existing_id) = lookup.get(&path) { + if let Some(&existing_id) = lookup.get(&source_path) { dependencies.entry(curr_id).or_default().push(existing_id); continue; } @@ -123,8 +352,8 @@ impl ProjectGraph { modules.push(Module { parsed_program: program, }); - lookup.insert(path.clone(), last_ind); - paths.push(path.clone()); + lookup.insert(source_path.clone(), last_ind); + paths.push(source_path.clone()); dependencies.entry(curr_id).or_default().push(last_ind); queue.push_back(last_ind); @@ -133,9 +362,9 @@ impl ProjectGraph { Ok(Self { modules, - config, + libraries, lookup, - paths, + paths: paths.into(), dependencies, }) } @@ -191,14 +420,14 @@ impl ProjectGraph { } fn process_use_item( - scope_items: &mut [HashMap], + resolutions: &mut [FileResolutions], file_id: usize, ind: usize, elem: &Identifier, use_decl_visibility: Visibility, ) -> Result<(), String> { if matches!( - scope_items[ind][elem].visibility, + resolutions[ind][elem].visibility, parse::Visibility::Private ) { return Err(format!( @@ -207,7 +436,7 @@ impl ProjectGraph { )); } - scope_items[file_id].insert( + resolutions[file_id].insert( elem.clone(), Resolution { visibility: use_decl_visibility, @@ -218,26 +447,27 @@ impl ProjectGraph { } fn register_def( - items: &mut Vec, - scope: &mut HashMap, + items: &mut Vec, + resolutions: &mut [FileResolutions], + file_id: usize, item: &parse::Item, name: Identifier, vis: &parse::Visibility, - ) { - items.push(item.clone()); - scope.insert( + ) -> Result<(), String> { + items.push(Item::from_parse(item, file_id)?); + resolutions[file_id].insert( name, Resolution { visibility: vis.clone(), }, ); + Ok(()) } // TODO: @LesterEvSe, consider processing more than one error at a time fn build_program(&self, order: &Vec) -> Result { - let mut items: Vec = Vec::new(); - let mut scope_items: Vec> = - vec![HashMap::new(); order.len()]; + let mut items: Vec = Vec::new(); + let mut resolutions: Vec = vec![BTreeMap::new(); order.len()]; for &file_id in order { let program_items = self.modules[file_id].parsed_program.items(); @@ -245,8 +475,9 @@ impl ProjectGraph { for elem in program_items { match elem { parse::Item::Use(use_decl) => { - let full_path = self.config.get_full_path(use_decl)?; - let ind = self.lookup[&full_path]; + let full_path = get_full_path(&self.libraries, use_decl)?; + let source_full_path = SourceName::Real(full_path); + let ind = self.lookup[&source_full_path]; let visibility = use_decl.visibility(); let use_targets = match use_decl.items() { @@ -256,7 +487,7 @@ impl ProjectGraph { for target in use_targets { ProjectGraph::process_use_item( - &mut scope_items, + &mut resolutions, file_id, ind, target, @@ -267,20 +498,22 @@ impl ProjectGraph { parse::Item::TypeAlias(alias) => { Self::register_def( &mut items, - &mut scope_items[file_id], + &mut resolutions, + file_id, elem, alias.name().clone().into(), alias.visibility(), - ); + )?; } parse::Item::Function(function) => { Self::register_def( &mut items, - &mut scope_items[file_id], + &mut resolutions, + file_id, elem, function.name().clone().into(), function.visibility(), - ); + )?; } parse::Item::Module => {} } @@ -289,7 +522,8 @@ impl ProjectGraph { Ok(Program { items: items.into(), - scope_items, + paths: self.paths.clone(), + resolutions: resolutions.into(), span: *self.modules[0].parsed_program.as_ref(), }) } @@ -334,8 +568,129 @@ fn merge(mut seqs: Vec>) -> Option> { } } +impl fmt::Display for Program { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // 1. Print the actual program code first + for item in self.items.iter() { + writeln!(f, "{item}")?; + } + + // 2. Open the Resolution Table block + writeln!(f, "\n/* --- RESOLUTION TABLE ---")?; + + // 3. Logic: Empty vs Populated + if self.resolutions.is_empty() { + writeln!(f, " EMPTY")?; + } else { + for (file_id, scope) in self.resolutions.iter().enumerate() { + if scope.is_empty() { + writeln!(f, " File ID {}: (No resolutions)", file_id)?; + continue; + } + + writeln!(f, " File ID {}:", file_id)?; + + for (ident, resolution) in scope { + writeln!(f, " {}: {:?}", ident, resolution.visibility)?; + } + } + } + + // 4. Close the block (This runs for both empty and non-empty cases) + writeln!(f, "*/")?; + + Ok(()) + } +} + +impl fmt::Display for Item { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::TypeAlias(alias) => write!(f, "{alias}"), + Self::Function(function) => write!(f, "{function}"), + // 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 {{}}"), + } + } +} + +impl fmt::Display for TypeAlias { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "type {} = {};", self.name(), self.ty()) + } +} + +impl fmt::Display for Function { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "fn {} [file_id: {}] (", self.name(), self.file_id())?; + for (i, param) in self.params().iter().enumerate() { + if 0 < i { + write!(f, ", ")?; + } + write!(f, "{param}")?; + } + write!(f, ")")?; + if let Some(ty) = self.ret() { + write!(f, " -> {ty}")?; + } + write!(f, " {}", self.body()) + } +} + +impl AsRef for Program { + fn as_ref(&self) -> &Span { + &self.span + } +} + +impl AsRef for Function { + fn as_ref(&self) -> &Span { + &self.span + } +} + +impl AsRef for TypeAlias { + fn as_ref(&self) -> &Span { + &self.span + } +} + +#[cfg(feature = "arbitrary")] +impl<'a> arbitrary::Arbitrary<'a> for Function { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + ::arbitrary_rec(u, 3) + } +} + +#[cfg(feature = "arbitrary")] +impl crate::ArbitraryRec for Function { + fn arbitrary_rec(u: &mut arbitrary::Unstructured, budget: usize) -> arbitrary::Result { + use arbitrary::Arbitrary; + + let file_id = u.int_in_range(0..=5)?; + let name = FunctionName::arbitrary(u)?; + let len = u.int_in_range(0..=3)?; + let params = (0..len) + .map(|_| parse::FunctionParam::arbitrary(u)) + .collect::>>()?; + let ret = Option::::arbitrary(u)?; + let body = + parse::Expression::arbitrary_rec(u, budget).map(parse::Expression::into_block)?; + Ok(Self { + file_id, + name, + params, + ret, + body, + span: Span::DUMMY, + }) + } +} + #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; use std::fs::{self, File}; use std::io::Write; @@ -344,7 +699,7 @@ mod tests { // --- Helper to setup environment --- // Creates a file with specific content in the temp directory - fn create_simf_file(dir: &Path, rel_path: &str, content: &str) -> PathBuf { + pub(crate) fn create_simf_file(dir: &Path, rel_path: &str, content: &str) -> PathBuf { let full_path = dir.join(rel_path); // Ensure parent directories exist @@ -391,13 +746,17 @@ mod tests { let root_p = root_path.expect("main.simf must be defined in file list"); let root_program = parse_root(&root_p); - let config = Arc::from(LibConfig::new(lib_map, &root_p)); - let graph = ProjectGraph::new(config, &root_program).expect("Failed to build graph"); + let source_name = SourceName::Real(root_p); + let graph = ProjectGraph::new(source_name, Arc::from(lib_map), &root_program) + .expect("Failed to build graph"); // Create a lookup map for tests: "A.simf" -> FileID let mut file_ids = HashMap::new(); for (path, id) in &graph.lookup { - let file_name = path.file_name().unwrap().to_string_lossy().to_string(); + let file_name = match path { + SourceName::Real(path) => path.file_name().unwrap().to_string_lossy().to_string(), + SourceName::Virtual(name) => name.clone(), + }; file_ids.insert(file_name, *id); } @@ -421,7 +780,7 @@ mod tests { let program = graph .build_program(&order) .expect("Failed to build program"); - let scope = &program.scope_items[root_id]; + let scope = &program.resolutions[root_id]; // Check private function let private_res = scope @@ -462,7 +821,7 @@ mod tests { .expect("Failed to build program"); // Check B's scope - let scope_b = &program.scope_items[id_b]; + let scope_b = &program.resolutions[id_b]; let foo_in_b = scope_b .get(&Identifier::from("foo")) .expect("foo missing in B"); @@ -475,7 +834,7 @@ mod tests { ); // Check Root's scope - let scope_root = &program.scope_items[id_root]; + let scope_root = &program.resolutions[id_root]; let foo_in_root = scope_root .get(&Identifier::from("foo")) .expect("foo missing in Root"); @@ -533,10 +892,11 @@ mod tests { // Parse Root let root_program = parse_root(&root_path); - let config = Arc::from(LibConfig::new(lib_map, &root_path)); + let source_name = SourceName::Real(root_path); // Run Logic - let graph = ProjectGraph::new(config, &root_program).expect("Graph build failed"); + let graph = ProjectGraph::new(source_name, Arc::from(lib_map), &root_program) + .expect("Graph build failed"); // Assertions assert_eq!(graph.modules.len(), 2, "Should have Root and Math module"); @@ -556,8 +916,9 @@ mod tests { lib_map.insert("std".to_string(), temp_dir.path().join("libs/std")); let root_program = parse_root(&root_path); - let config = Arc::from(LibConfig::new(lib_map, &root_path)); - let graph = ProjectGraph::new(config, &root_program).expect("Graph build failed"); + let source_name = SourceName::Real(root_path); + let graph = ProjectGraph::new(source_name, Arc::from(lib_map), &root_program) + .expect("Graph build failed"); let order = graph.c3_linearize().expect("C3 failed"); @@ -594,8 +955,9 @@ mod tests { lib_map.insert("lib".to_string(), temp_dir.path().join("libs/lib")); let root_program = parse_root(&root_path); - let config = Arc::from(LibConfig::new(lib_map, &root_path)); - let graph = ProjectGraph::new(config, &root_program).expect("Graph build failed"); + let source_name = SourceName::Real(root_path); + let graph = ProjectGraph::new(source_name, Arc::from(lib_map), &root_program) + .expect("Graph build failed"); // Assertions // Structure: Root(0), A(1), B(2), Common(3) @@ -645,8 +1007,9 @@ mod tests { lib_map.insert("lib".to_string(), temp_dir.path().join("libs/lib")); let root_program = parse_root(&root_path); - let config = Arc::from(LibConfig::new(lib_map, &root_path)); - let graph = ProjectGraph::new(config, &root_program).expect("Graph build failed"); + let source_name = SourceName::Real(root_path); + let graph = ProjectGraph::new(source_name, Arc::from(lib_map), &root_program) + .expect("Graph build failed"); let order = graph.c3_linearize().expect("C3 failed"); @@ -676,8 +1039,9 @@ mod tests { lib_map.insert("test".to_string(), temp_dir.path().join("libs/test")); let root_program = parse_root(&a_path); - let config = Arc::from(LibConfig::new(lib_map, &a_path)); - let graph = ProjectGraph::new(config, &root_program).expect("Graph build failed"); + let source_name = SourceName::Real(a_path); + let graph = ProjectGraph::new(source_name, Arc::from(lib_map), &root_program) + .expect("Graph build failed"); assert_eq!(graph.modules.len(), 2, "Should only have A and B"); @@ -710,8 +1074,9 @@ mod tests { lib_map.insert("test".to_string(), temp_dir.path().join("libs/test")); let root_program = parse_root(&a_path); - let config = Arc::from(LibConfig::new(lib_map, &a_path)); - let graph = ProjectGraph::new(config, &root_program).expect("Graph build failed"); + let source_name = SourceName::Real(a_path); + let graph = ProjectGraph::new(source_name, Arc::from(lib_map), &root_program) + .expect("Graph build failed"); let order = graph.c3_linearize().unwrap_err(); matches!(order, C3Error::CycleDetected(_)); @@ -730,8 +1095,8 @@ mod tests { lib_map.insert("std".to_string(), temp_dir.path().join("libs/std")); let root_program = parse_root(&root_path); - let config = Arc::from(LibConfig::new(lib_map, &root_path)); - let result = ProjectGraph::new(config, &root_program); + let source_name = SourceName::Real(root_path); + let result = ProjectGraph::new(source_name, Arc::from(lib_map), &root_program); assert!(result.is_err(), "Should fail for missing file"); let err_msg = result.err().unwrap(); @@ -754,9 +1119,9 @@ mod tests { let lib_map = HashMap::new(); let root_program = parse_root(&root_path); - let config = Arc::from(LibConfig::new(lib_map, &root_path)); - let graph = - ProjectGraph::new(config, &root_program).expect("Should succeed but ignore import"); + let source_name = SourceName::Real(root_path); + let graph = ProjectGraph::new(source_name, Arc::from(lib_map), &root_program) + .expect("Should succeed but ignore import"); assert_eq!(graph.modules.len(), 1, "Should only contain root"); assert!( diff --git a/src/error.rs b/src/error.rs index 1ded6fdd..cc50eb22 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,6 @@ use std::fmt; use std::ops::Range; +use std::path::PathBuf; use std::sync::Arc; use chumsky::error::Error as ChumskyError; @@ -415,11 +416,13 @@ pub enum Error { CannotCompile(String), JetDoesNotExist(JetName), InvalidCast(ResolvedType, ResolvedType), + FileNotFound(PathBuf), MainNoInputs, MainNoOutput, MainRequired, FunctionRedefined(FunctionName), FunctionUndefined(FunctionName), + FunctionIsPrivate(FunctionName), InvalidNumberOfArguments(usize, usize), FunctionNotFoldable(FunctionName), FunctionNotLoopable(FunctionName), @@ -495,6 +498,10 @@ impl fmt::Display for Error { f, "Cannot cast values of type `{source}` as values of type `{target}`" ), + Error::FileNotFound(path) => write!( + f, + "File `{}` not found", path.to_string_lossy() + ), Error::MainNoInputs => write!( f, "Main function takes no input parameters" @@ -515,6 +522,10 @@ impl fmt::Display for Error { f, "Function `{name}` was called but not defined" ), + Error::FunctionIsPrivate(name) => write!( + f, + "Function `{name}` is private" + ), Error::InvalidNumberOfArguments(expected, found) => write!( f, "Expected {expected} arguments, found {found} arguments" diff --git a/src/lib.rs b/src/lib.rs index 6be6d9e5..c5f1104e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,7 +22,7 @@ pub mod value; mod witness; use std::collections::HashMap; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::sync::Arc; use simplicity::jet::elements::ElementsEnv; @@ -40,38 +40,44 @@ pub use crate::types::ResolvedType; pub use crate::value::Value; pub use crate::witness::{Arguments, Parameters, WitnessTypes, WitnessValues}; -#[derive(Debug, Clone)] -pub struct LibConfig { - pub libraries: HashMap, - pub root_path: PathBuf, +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum SourceName { + Real(PathBuf), + Virtual(String), } -impl LibConfig { - pub fn new(libraries: HashMap, raw_root_path: &Path) -> Self { - let root_path = raw_root_path.with_extension(""); - - Self { - libraries, - root_path, +impl SourceName { + pub fn without_extension(&self) -> SourceName { + match self { + SourceName::Real(path) => SourceName::Real(path.with_extension("")), + SourceName::Virtual(name) => SourceName::Virtual(name.clone()), } } +} - pub fn get_full_path(&self, use_decl: &UseDecl) -> Result { - let parts: Vec<&str> = use_decl.path().iter().map(|s| s.as_ref()).collect(); - let first_segment = parts[0]; +impl Default for SourceName { + fn default() -> Self { + SourceName::Virtual("".to_string()) + } +} - if let Some(lib_root) = self.libraries.get(first_segment) { - let mut full_path = lib_root.clone(); - full_path.extend(&parts[1..]); +pub type LibTable = HashMap; - return Ok(full_path); - } +pub fn get_full_path(libraries: &LibTable, use_decl: &UseDecl) -> Result { + let parts: Vec<&str> = use_decl.path().iter().map(|s| s.as_ref()).collect(); + let first_segment = parts[0]; - Err(format!( - "Unknown module or library '{}'. Did you forget to pass --lib {}=...?", - first_segment, first_segment, - )) + if let Some(lib_root) = libraries.get(first_segment) { + let mut full_path = lib_root.clone(); + full_path.extend(&parts[1..]); + + return Ok(full_path); } + + Err(format!( + "Unknown module or library '{}'. Did you forget to pass --lib {}=...?", + first_segment, first_segment, + )) } /// The template of a SimplicityHL program. @@ -89,24 +95,28 @@ impl TemplateProgram { /// ## Errors /// /// The string is not a valid SimplicityHL program. - pub fn new>>(lib_cfg: Option<&LibConfig>, s: Str) -> Result { + pub fn new>>( + source_name: SourceName, + libraries: Arc, + s: Str, + ) -> Result { let file = s.into(); let mut error_handler = ErrorCollector::new(Arc::clone(&file)); let parse_program = parse::Program::parse_from_str_with_errors(&file, &mut error_handler); if let Some(program) = parse_program { // TODO: Consider a proper resolution strategy later. - let _: Option = if let Some(cfg) = lib_cfg { - let config_arc = Arc::new(cfg.clone()); - let graph = ProjectGraph::new(config_arc, &program)?; + let driver_program: driver::Program = if libraries.is_empty() { + driver::Program::from_parse(&program, source_name)? + } else { + let graph = ProjectGraph::new(source_name, libraries, &program)?; // TODO: Perhaps add an `error_handler` here, too. - Some(graph.resolve_complication_order()?) - } else { - None + graph.resolve_complication_order()? }; - let ast_program = ast::Program::analyze(&program).with_file(Arc::clone(&file))?; + let ast_program = + ast::Program::analyze(&driver_program).with_file(Arc::clone(&file))?; Ok(Self { simfony: ast_program, file, @@ -179,12 +189,13 @@ impl CompiledProgram { /// - [`TemplateProgram::new`] /// - [`TemplateProgram::instantiate`] pub fn new>>( - lib_cfg: Option<&LibConfig>, + source_name: SourceName, + libraries: Arc, s: Str, arguments: Arguments, include_debug_symbols: bool, ) -> Result { - TemplateProgram::new(lib_cfg, s) + TemplateProgram::new(source_name, libraries, s) .and_then(|template| template.instantiate(arguments, include_debug_symbols)) } @@ -264,13 +275,15 @@ impl SatisfiedProgram { /// - [`TemplateProgram::instantiate`] /// - [`CompiledProgram::satisfy`] pub fn new>>( - lib_cfg: Option<&LibConfig>, + source_name: SourceName, + libraries: Arc, s: Str, arguments: Arguments, witness_values: WitnessValues, include_debug_symbols: bool, ) -> Result { - let compiled = CompiledProgram::new(lib_cfg, s, arguments, include_debug_symbols)?; + let compiled = + CompiledProgram::new(source_name, libraries, s, arguments, include_debug_symbols)?; compiled.satisfy(witness_values) } @@ -369,11 +382,29 @@ pub(crate) mod tests { impl TestCase { pub fn template_file>(program_file_path: P) -> Self { let program_text = std::fs::read_to_string(program_file_path).unwrap(); - Self::template_text(Cow::Owned(program_text)) + Self::template_text( + SourceName::default(), + Arc::from(HashMap::new()), + Cow::Owned(program_text), + ) } - pub fn template_text(program_text: Cow) -> Self { - let program = match TemplateProgram::new(None, program_text.as_ref()) { + pub fn template_lib( + source_name: SourceName, + libraries: Arc, + program_file: &Path, + ) -> Self { + let program_text = std::fs::read_to_string(program_file).unwrap(); + Self::template_text(source_name, libraries, Cow::Owned(program_text)) + } + + pub fn template_text( + source_name: SourceName, + libraries: Arc, + program_text: Cow, + ) -> Self { + let program = match TemplateProgram::new(source_name, libraries, program_text.as_ref()) + { Ok(x) => x, Err(error) => panic!("{error}"), }; @@ -413,13 +444,69 @@ pub(crate) mod tests { } impl TestCase { + pub fn temp_env( + main_content: &str, + libs: Vec<(&str, &str, &str)>, + ) -> (Self, tempfile::TempDir) { + let temp_dir = tempfile::TempDir::new().unwrap(); + let main_path = + driver::tests::create_simf_file(temp_dir.path(), "main.simf", main_content); + let mut lib_paths = Vec::new(); + + for (lib_name, rel_path, content) in libs { + driver::tests::create_simf_file(temp_dir.path(), rel_path, content); + + let lib_root = temp_dir + .path() + .join(rel_path) + .parent() + .unwrap() + .to_path_buf(); + lib_paths.push((lib_name.to_string(), lib_root)); + } + + let libs_refs: Vec<(&str, &std::path::Path)> = lib_paths + .iter() + .map(|(k, v)| (k.as_str(), v.as_path())) + .collect(); + + let test_case = Self::program_file_with_libs(&main_path, libs_refs); + (test_case, temp_dir) + } + pub fn program_file>(program_file_path: P) -> Self { TestCase::::template_file(program_file_path) .with_arguments(Arguments::default()) } pub fn program_text(program_text: Cow) -> Self { - TestCase::::template_text(program_text) + TestCase::::template_text( + SourceName::default(), + Arc::from(HashMap::new()), + program_text, + ) + .with_arguments(Arguments::default()) + } + + pub fn program_file_with_libs(program_file_path: P, libs: I) -> Self + where + P: AsRef, + I: IntoIterator, // Magic trait: accepts anything we can iterate over + K: Into, + V: AsRef, + { + let path_ref = program_file_path.as_ref(); + + let mut libraries = HashMap::new(); + for (k, v) in libs { + libraries.insert(k.into(), v.as_ref().to_path_buf()); + } + + let source_name = + SourceName::Real(path_ref.parent().unwrap_or(Path::new("")).to_path_buf()); + + // 3. Delegate to your existing template_lib method + TestCase::::template_lib(source_name, Arc::from(libraries), path_ref) .with_arguments(Arguments::default()) } @@ -522,6 +609,158 @@ pub(crate) mod tests { } } + // Real test cases + #[test] + fn module_simple() { + let (test, _dir) = TestCase::temp_env( + "use temp::math::add; fn main() {}", + vec![("temp", "temp/math.simf", "pub fn add() {}")], + ); + + test.with_witness_values(WitnessValues::default()) + .assert_run_success(); + } + + #[test] + fn diamond_dependency_resolution() { + let main_code = r#" + use temp::left::get_left; + use temp::right::get_right; + + fn main() { + let a: BaseType = get_left(); + let b: BaseType = get_right(); + let (_, c): (bool, BaseType) = jet::add_32(a, b); + assert!(jet::eq_32(c, 3)); + } + "#; + + let libs = vec![ + ("temp", "temp/base.simf", "pub type BaseType = u32;"), + ( + "temp", + "temp/left.simf", + "pub use temp::base::BaseType; pub fn get_left() -> BaseType { 1 }", + ), + ( + "temp", + "temp/right.simf", + "pub use temp::base::BaseType; pub fn get_right() -> BaseType { 2 }", + ), + ]; + + let (test, _dir) = TestCase::temp_env(main_code, libs); + test.with_witness_values(WitnessValues::default()) + .assert_run_success(); + } + + #[test] + #[should_panic(expected = "CycleDetected")] + fn cyclic_dependency_error() { + let main_code = "use temp::module_a::TypeA; fn main() {}"; + + let libs = vec![ + ( + "temp", + "temp/module_a.simf", + "pub use temp::module_b::TypeB; pub type TypeA = u32;", + ), + ( + "temp", + "temp/module_b.simf", + "pub use temp::module_a::TypeA; pub type TypeB = u32;", + ), + ]; + + let (test, _dir) = TestCase::temp_env(main_code, libs); + test.with_witness_values(WitnessValues::default()) + .assert_run_success(); + } + + #[test] + fn deep_reexport_chain() { + let main_code = r#" + use temp::level1::{CoreSmth, core_val}; + + fn main() { + let val: CoreSmth = core_val(); + assert!(jet::eq_32(val, 42)); + } + "#; + + let libs = vec![ + ( + "temp", + "temp/level3.simf", + "pub type CoreSmth = u32; pub fn core_val() -> CoreSmth { 42 }", + ), + ( + "temp", + "temp/level2.simf", + "pub use temp::level3::{CoreSmth, core_val};", + ), + ( + "temp", + "temp/level1.simf", + "pub use temp::level2::{CoreSmth, core_val};", + ), + ]; + + let (test, _dir) = TestCase::temp_env(main_code, libs); + test.with_witness_values(WitnessValues::default()) + .assert_run_success(); + } + + #[test] + #[should_panic(expected = "Function SecretType is private and cannot be used")] + fn private_type_visibility_error() { + let main_code = r#" + use temp::hidden::SecretType; + fn main() {} + "#; + + let libs = vec![( + "temp", + "temp/hidden.simf", + "type SecretType = u32; pub fn ok() {}", + )]; + + let (test, _dir) = TestCase::temp_env(main_code, libs); + test.with_witness_values(WitnessValues::default()) + .assert_run_success(); + } + + #[test] + #[ignore] + #[should_panic(expected = "was defined multiple times")] // TODO: Or not? Fix it later, after receiving a response from the devs. + fn name_collision_error() { + let main_code = r#" + use temp::mod_a::Value; + use temp::mod_b::Value; + + fn main() {} + "#; + + let libs = vec![ + ("temp", "temp/mod_a.simf", "pub type Value = u32;"), + ("temp", "temp/mod_b.simf", "pub type Value = u32;"), + ]; + + let (test, _dir) = TestCase::temp_env(main_code, libs); + test.with_witness_values(WitnessValues::default()) + .assert_run_success(); + } + + #[test] + fn single_lib() { + TestCase::program_file_with_libs( + "./examples/single_lib/main.simf", + [("temp", "./examples/single_lib/temp")], + ) + .with_witness_values(WitnessValues::default()) + .assert_run_success(); + } + #[test] fn cat() { TestCase::program_file("./examples/cat.simf") @@ -710,7 +949,8 @@ fn main() { } "#; match SatisfiedProgram::new( - None, + SourceName::default(), + Arc::from(HashMap::new()), prog_text, Arguments::default(), WitnessValues::default(), diff --git a/src/main.rs b/src/main.rs index dbbeaa1b..1b4bb8bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,8 @@ use base64::display::Base64Display; use base64::engine::general_purpose::STANDARD; use clap::{Arg, ArgAction, Command}; -use simplicityhl::{AbiMeta, CompiledProgram, LibConfig}; -use std::{collections::HashMap, env, fmt, path::PathBuf}; +use simplicityhl::{AbiMeta, CompiledProgram, LibTable, SourceName}; +use std::{env, fmt, sync::Arc}; #[cfg_attr(feature = "serde", derive(serde::Serialize))] /// The compilation output. @@ -120,7 +120,7 @@ fn main() -> Result<(), Box> { let lib_args = matches.get_many::("library").unwrap_or_default(); - let library_map: HashMap = lib_args + let libraries: LibTable = lib_args .map(|arg| { let parts: Vec<&str> = arg.splitn(2, '=').collect(); @@ -136,15 +136,19 @@ fn main() -> Result<(), Box> { }) .collect(); - let config = LibConfig::new(library_map, prog_path); - let compiled = - match CompiledProgram::new(Some(&config), prog_text, args_opt, include_debug_symbols) { - Ok(program) => program, - Err(e) => { - eprintln!("{}", e); - std::process::exit(1); - } - }; + let compiled = match CompiledProgram::new( + SourceName::Real(prog_path.to_path_buf()), + Arc::from(libraries), + prog_text, + args_opt, + include_debug_symbols, + ) { + Ok(program) => program, + Err(e) => { + eprintln!("{}", e); + std::process::exit(1); + } + }; #[cfg(feature = "serde")] let witness_opt = matches diff --git a/src/parse.rs b/src/parse.rs index 1d84edf8..d228c720 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -314,7 +314,7 @@ impl TypeAlias { } } -impl_eq_hash!(TypeAlias; name, ty); +impl_eq_hash!(TypeAlias; visibility, name, ty); /// An expression is something that returns a value. #[derive(Clone, Debug)] @@ -336,7 +336,7 @@ impl Expression { /// Convert the expression into a block expression. #[cfg(feature = "arbitrary")] - fn into_block(self) -> Self { + pub(crate) fn into_block(self) -> Self { match self.inner { ExpressionInner::Single(_) => Expression { span: self.span, @@ -632,15 +632,30 @@ impl fmt::Display for Item { } } +impl fmt::Display for Visibility { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Private => write!(f, ""), + Self::Public => write!(f, "pub "), + } + } +} + impl fmt::Display for TypeAlias { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "type {} = {};", self.name(), self.ty()) + write!( + f, + "{}type {} = {};", + self.visibility(), + self.name(), + self.ty() + ) } } impl fmt::Display for Function { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "fn {}(", self.name())?; + write!(f, "{}fn {}(", self.visibility(), self.name())?; for (i, param) in self.params().iter().enumerate() { if 0 < i { write!(f, ", ")?; @@ -657,11 +672,7 @@ impl fmt::Display for Function { impl fmt::Display for UseDecl { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Visibility::Public = self.visibility { - write!(f, "pub ")?; - } - - let _ = write!(f, "use "); + let _ = write!(f, "{}use ", self.visibility()); for (i, segment) in self.path.iter().enumerate() { if i > 0 { diff --git a/src/tracker.rs b/src/tracker.rs index 82f15a49..9791b5f5 100644 --- a/src/tracker.rs +++ b/src/tracker.rs @@ -399,6 +399,7 @@ mod tests { use crate::elements::hashes::Hash; use crate::elements::pset::Input; use crate::elements::{AssetId, OutPoint, Script, Txid}; + use crate::SourceName; use crate::{Arguments, TemplateProgram, WitnessValues}; use super::*; @@ -472,7 +473,12 @@ mod tests { #[test] fn test_debug_and_jet_tracing() { - let program = TemplateProgram::new(None, TEST_PROGRAM).unwrap(); + let program = TemplateProgram::new( + SourceName::default(), + Arc::from(HashMap::new()), + TEST_PROGRAM, + ) + .unwrap(); let program = program.instantiate(Arguments::default(), true).unwrap(); let satisfied = program.satisfy(WitnessValues::default()).unwrap(); @@ -541,7 +547,12 @@ mod tests { fn test_arith_jet_trace_regression() { let env = create_test_env(); - let program = TemplateProgram::new(None, TEST_ARITHMETIC_JETS).unwrap(); + let program = TemplateProgram::new( + SourceName::default(), + Arc::from(HashMap::new()), + TEST_ARITHMETIC_JETS, + ) + .unwrap(); let program = program.instantiate(Arguments::default(), true).unwrap(); let satisfied = program.satisfy(WitnessValues::default()).unwrap(); diff --git a/src/witness.rs b/src/witness.rs index 8cd614c8..55cbd8bd 100644 --- a/src/witness.rs +++ b/src/witness.rs @@ -222,7 +222,8 @@ mod tests { use super::*; use crate::parse::ParseFromStr; use crate::value::ValueConstructible; - use crate::{ast, parse, CompiledProgram, SatisfiedProgram}; + use crate::SourceName; + use crate::{ast, driver, parse, CompiledProgram, SatisfiedProgram}; #[test] fn witness_reuse() { @@ -230,7 +231,9 @@ mod tests { assert!(jet::eq_32(witness::A, witness::A)); }"#; let program = parse::Program::parse_from_str(s).expect("parsing works"); - match ast::Program::analyze(&program).map_err(Error::from) { + let driver_program = + driver::Program::from_parse(&program, SourceName::default()).expect("driver works"); + match ast::Program::analyze(&driver_program).map_err(Error::from) { Ok(_) => panic!("Witness reuse was falsely accepted"), Err(Error::WitnessReused(..)) => {} Err(error) => panic!("Unexpected error: {error}"), @@ -247,7 +250,14 @@ mod tests { WitnessName::from_str_unchecked("A"), Value::u16(42), )])); - match SatisfiedProgram::new(None, s, Arguments::default(), witness, false) { + match SatisfiedProgram::new( + SourceName::default(), + Arc::from(HashMap::new()), + s, + Arguments::default(), + witness, + false, + ) { Ok(_) => panic!("Ill-typed witness assignment was falsely accepted"), Err(error) => assert_eq!( "Witness `A` was declared with type `u32` but its assigned value is of type `u16`", @@ -266,7 +276,13 @@ fn main() { assert!(jet::is_zero_32(f())); }"#; - match CompiledProgram::new(None, s, Arguments::default(), false) { + match CompiledProgram::new( + SourceName::default(), + Arc::from(HashMap::new()), + s, + Arguments::default(), + false, + ) { Ok(_) => panic!("Witness outside main was falsely accepted"), Err(error) => { assert!(error From b24b0565fc05b062bced71d8454a60b88c3ea22e Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Tue, 24 Feb 2026 17:12:28 +0200 Subject: [PATCH 06/13] feat: add filename to errors --- examples/single_lib/main.simf | 2 +- .../temp/{two.simf => constants/utils.simf} | 0 src/ast.rs | 2 +- src/driver.rs | 359 +++++++----------- src/error.rs | 105 +++-- src/lib.rs | 110 ++++-- src/main.rs | 2 +- src/witness.rs | 4 +- 8 files changed, 298 insertions(+), 286 deletions(-) rename examples/single_lib/temp/{two.simf => constants/utils.simf} (100%) diff --git a/examples/single_lib/main.simf b/examples/single_lib/main.simf index 19b2b047..5f18ba68 100644 --- a/examples/single_lib/main.simf +++ b/examples/single_lib/main.simf @@ -1,4 +1,4 @@ -pub use temp::two::two; +pub use temp::constants::utils::two; use temp::funcs::{get_five, Smth}; fn seven() -> u32 { diff --git a/examples/single_lib/temp/two.simf b/examples/single_lib/temp/constants/utils.simf similarity index 100% rename from examples/single_lib/temp/two.simf rename to examples/single_lib/temp/constants/utils.simf diff --git a/src/ast.rs b/src/ast.rs index 7e2a6d85..89d74655 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -748,7 +748,7 @@ impl Scope { SourceName::Real(path) => self .resolutions .get(self.file_id) - .ok_or(Error::FileNotFound(path))?, // TODO: File or pub type + .ok_or(Error::FileNotFound(path.to_path_buf()))?, // TODO: File or pub type SourceName::Virtual(_) => { return Ok(function); } diff --git a/src/driver.rs b/src/driver.rs index a31ed548..64f70543 100644 --- a/src/driver.rs +++ b/src/driver.rs @@ -7,7 +7,7 @@ use crate::error::{ErrorCollector, Span}; use crate::parse::{self, ParseFromStrWithErrors, Visibility}; use crate::str::{AliasName, FunctionName, Identifier}; use crate::types::AliasedType; -use crate::{get_full_path, impl_eq_hash, LibTable, SourceName}; +use crate::{get_full_path, impl_eq_hash, LibTable, SourceFile, SourceName}; /// Represents a single, isolated file in the SimplicityHL project. /// In this architecture, a file and a module are the exact same thing. @@ -289,8 +289,12 @@ pub enum C3Error { fn parse_and_get_program(prog_file: &Path) -> Result { let prog_text = std::fs::read_to_string(prog_file).map_err(|e| e.to_string())?; - let file = prog_text.into(); - let mut error_handler = crate::error::ErrorCollector::new(Arc::clone(&file)); + let file: Arc = prog_text.into(); + let source = SourceFile::new( + SourceName::Real(Arc::from(prog_file.with_extension(""))), + file.clone(), + ); + let mut error_handler = crate::error::ErrorCollector::new(source); if let Some(program) = parse::Program::parse_from_str_with_errors(&file, &mut error_handler) { Ok(program) @@ -301,11 +305,12 @@ fn parse_and_get_program(prog_file: &Path) -> Result { impl ProjectGraph { pub fn new( - source_name: SourceName, + source: SourceFile, libraries: Arc, root_program: &parse::Program, + _handler: &mut ErrorCollector, ) -> Result { - let source_name = source_name.without_extension(); + let source_name = source.name().without_extension(); let mut modules: Vec = vec![Module { parsed_program: root_program.clone(), }]; @@ -335,7 +340,7 @@ impl ProjectGraph { for path in pending_imports { let full_path = path.with_extension("simf"); - let source_path = SourceName::Real(path); + let source_path = SourceName::Real(Arc::from(path)); if !full_path.is_file() { return Err(format!("File in {:?}, does not exist", full_path)); @@ -426,10 +431,11 @@ impl ProjectGraph { elem: &Identifier, use_decl_visibility: Visibility, ) -> Result<(), String> { - if matches!( - resolutions[ind][elem].visibility, - parse::Visibility::Private - ) { + let resolution = resolutions[ind] + .get(elem) + .ok_or_else(|| format!("Try using the unknown item `{}`", elem.as_inner()))?; + + if matches!(resolution.visibility, parse::Visibility::Private) { return Err(format!( "Function {} is private and cannot be used.", elem.as_inner() @@ -476,7 +482,7 @@ impl ProjectGraph { match elem { parse::Item::Use(use_decl) => { let full_path = get_full_path(&self.libraries, use_decl)?; - let source_full_path = SourceName::Real(full_path); + let source_full_path = SourceName::Real(Arc::from(full_path)); let ind = self.lookup[&source_full_path]; let visibility = use_decl.visibility(); @@ -718,46 +724,47 @@ pub(crate) mod tests { parse_and_get_program(path).expect("Root parsing failed") } - /// Initializes a graph environment for testing. - /// Returns: - /// 1. The constructed `ProjectGraph`. - /// 2. A `HashMap` mapping filenames (e.g., "A.simf") to their `FileID` (usize). - /// 3. The `TempDir` (to keep files alive during the test). + /// Sets up a graph with "lib" mapped to "libs/lib". + /// Files format: vec![("main.simf", "content"), ("libs/lib/A.simf", "content")] fn setup_graph(files: Vec<(&str, &str)>) -> (ProjectGraph, HashMap, TempDir) { let temp_dir = TempDir::new().unwrap(); - let mut lib_map = HashMap::new(); - - // Define the standard library path structure - let lib_path = temp_dir.path().join("libs/lib"); - lib_map.insert("lib".to_string(), lib_path); + // 1. Create Files let mut root_path = None; - - // Create all requested files for (name, content) in files { + let path = create_simf_file(temp_dir.path(), name, content); if name == "main.simf" { - root_path = Some(create_simf_file(temp_dir.path(), name, content)); - } else { - // Names should be passed like "libs/lib/A.simf" - create_simf_file(temp_dir.path(), name, content); + root_path = Some(path); } } + let root_p = root_path.expect("Tests must define 'main.simf'"); + + // 2. Setup Libraries (Hardcoded "lib" -> "libs/lib" for simplicity in tests) + let mut lib_map = HashMap::new(); + lib_map.insert("lib".to_string(), temp_dir.path().join("libs/lib")); - let root_p = root_path.expect("main.simf must be defined in file list"); + // 3. Parse & Build let root_program = parse_root(&root_p); + let source = SourceFile::new( + SourceName::Real(Arc::from(root_p)), + Arc::from(""), // TODO: @LesterEvSe, consider to change it + ); + + let mut handler = ErrorCollector::new(source.clone()); - let source_name = SourceName::Real(root_p); - let graph = ProjectGraph::new(source_name, Arc::from(lib_map), &root_program) - .expect("Failed to build graph"); + let graph = ProjectGraph::new(source, Arc::from(lib_map), &root_program, &mut handler) + .expect( + "setup_graph expects a valid graph construction. Use manual setup for error tests.", + ); - // Create a lookup map for tests: "A.simf" -> FileID + // 4. Create Lookup (File Name -> ID) for easier asserting let mut file_ids = HashMap::new(); - for (path, id) in &graph.lookup { - let file_name = match path { + for (source_name, id) in &graph.lookup { + let simple_name = match source_name { SourceName::Real(path) => path.file_name().unwrap().to_string_lossy().to_string(), - SourceName::Virtual(name) => name.clone(), + SourceName::Virtual(name) => name.to_string(), }; - file_ids.insert(file_name, *id); + file_ids.insert(simple_name, *id); } (graph, file_ids, temp_dir) @@ -879,50 +886,44 @@ pub(crate) mod tests { #[test] fn test_simple_import() { // Setup: - // root.simf -> "use std::math;" - // libs/std/math.simf -> "" + // main.simf -> "use lib::math;" + // libs/lib/math.simf -> "" + // Note: Changed "std" to "lib" to match setup_graph default config - let temp_dir = TempDir::new().unwrap(); - let root_path = create_simf_file(temp_dir.path(), "root.simf", "use std::math::some_func;"); - create_simf_file(temp_dir.path(), "libs/std/math.simf", ""); - - // Setup Library Map - let mut lib_map = HashMap::new(); - lib_map.insert("std".to_string(), temp_dir.path().join("libs/std")); + let (graph, ids, _dir) = setup_graph(vec![ + ("main.simf", "use lib::math::some_func;"), + ("libs/lib/math.simf", ""), + ]); - // Parse Root - let root_program = parse_root(&root_path); - let source_name = SourceName::Real(root_path); + assert_eq!(graph.modules.len(), 2, "Should have Root and Math module"); - // Run Logic - let graph = ProjectGraph::new(source_name, Arc::from(lib_map), &root_program) - .expect("Graph build failed"); + // Check dependency: Root depends on Math + let root_id = ids["main"]; + let math_id = ids["math"]; - // Assertions - assert_eq!(graph.modules.len(), 2, "Should have Root and Math module"); assert!( - graph.dependencies[&0].contains(&1), - "Root should depend on Math" + graph.dependencies[&root_id].contains(&math_id), + "Root (main.simf) should depend on Math (math.simf)" ); } #[test] fn test_c3_simple_import() { - let temp_dir = TempDir::new().unwrap(); - let root_path = create_simf_file(temp_dir.path(), "root.simf", "use std::math::some_func;"); - create_simf_file(temp_dir.path(), "libs/std/math.simf", ""); - - let mut lib_map = HashMap::new(); - lib_map.insert("std".to_string(), temp_dir.path().join("libs/std")); - - let root_program = parse_root(&root_path); - let source_name = SourceName::Real(root_path); - let graph = ProjectGraph::new(source_name, Arc::from(lib_map), &root_program) - .expect("Graph build failed"); + // Setup similar to above + let (graph, ids, _dir) = setup_graph(vec![ + ("main.simf", "use lib::math::some_func;"), + ("libs/lib/math.simf", ""), + ]); let order = graph.c3_linearize().expect("C3 failed"); - assert_eq!(order, vec![0, 1]); + let root_id = ids["main"]; + let math_id = ids["math"]; + + // Assuming linearization order: Dependent (Root) -> Dependency (Math) + // Or vice-versa based on your specific C3 impl. + // Based on your previous test `vec![0, 1]`, it seems like [Root, Math]. + assert_eq!(order, vec![root_id, math_id]); } #[test] @@ -931,202 +932,114 @@ pub(crate) mod tests { // root -> imports A, B // A -> imports Common // B -> imports Common - // Expected: Common loaded ONLY ONCE. - - let temp_dir = TempDir::new().unwrap(); - let root_path = create_simf_file( - temp_dir.path(), - "root.simf", - "use lib::A::foo; use lib::B::bar;", - ); - create_simf_file( - temp_dir.path(), - "libs/lib/A.simf", - "use lib::Common::dummy1;", - ); - create_simf_file( - temp_dir.path(), - "libs/lib/B.simf", - "use lib::Common::dummy2;", - ); - create_simf_file(temp_dir.path(), "libs/lib/Common.simf", ""); // Empty leaf + // Expected: Common loaded ONLY ONCE (total 4 modules). - let mut lib_map = HashMap::new(); - lib_map.insert("lib".to_string(), temp_dir.path().join("libs/lib")); - - let root_program = parse_root(&root_path); - let source_name = SourceName::Real(root_path); - let graph = ProjectGraph::new(source_name, Arc::from(lib_map), &root_program) - .expect("Graph build failed"); + let (graph, ids, _dir) = setup_graph(vec![ + ("main.simf", "use lib::A::foo; use lib::B::bar;"), + ("libs/lib/A.simf", "use lib::Common::dummy1;"), + ("libs/lib/B.simf", "use lib::Common::dummy2;"), + ("libs/lib/Common.simf", ""), + ]); - // Assertions - // Structure: Root(0), A(1), B(2), Common(3) + // 1. Check strict deduplication (Unique modules count) assert_eq!( graph.modules.len(), 4, - "Should resolve exactly 4 unique modules" + "Should resolve exactly 4 unique modules (Main, A, B, Common)" ); + // 2. Verify Graph Topology via IDs + let a_id = ids["A"]; + let b_id = ids["B"]; + let common_id = ids["Common"]; + // Check A -> Common - let a_id = 1; - let common_id = 3; - assert!(graph.dependencies[&a_id].contains(&common_id)); + assert!( + graph.dependencies[&a_id].contains(&common_id), + "A should depend on Common" + ); - // Check B -> Common (Should point to SAME ID) - let b_id = 2; - assert!(graph.dependencies[&b_id].contains(&common_id)); + // Check B -> Common (Crucial: Must be the SAME common_id) + assert!( + graph.dependencies[&b_id].contains(&common_id), + "B should depend on Common" + ); } #[test] fn test_c3_diamond_dependency_deduplication() { // Setup: - // root -> imports A, B + // root (main) -> imports A, B // A -> imports Common // B -> imports Common // Expected: Common loaded ONLY ONCE. - let temp_dir = TempDir::new().unwrap(); - let root_path = create_simf_file( - temp_dir.path(), - "root.simf", - "use lib::A::foo; use lib::B::bar;", - ); - create_simf_file( - temp_dir.path(), - "libs/lib/A.simf", - "use lib::Common::dummy1;", - ); - create_simf_file( - temp_dir.path(), - "libs/lib/B.simf", - "use lib::Common::dummy2;", - ); - create_simf_file(temp_dir.path(), "libs/lib/Common.simf", ""); // Empty leaf - - let mut lib_map = HashMap::new(); - lib_map.insert("lib".to_string(), temp_dir.path().join("libs/lib")); - - let root_program = parse_root(&root_path); - let source_name = SourceName::Real(root_path); - let graph = ProjectGraph::new(source_name, Arc::from(lib_map), &root_program) - .expect("Graph build failed"); + let (graph, ids, _dir) = setup_graph(vec![ + ("main.simf", "use lib::A::foo; use lib::B::bar;"), + ("libs/lib/A.simf", "use lib::Common::dummy1;"), + ("libs/lib/B.simf", "use lib::Common::dummy2;"), + ("libs/lib/Common.simf", ""), + ]); let order = graph.c3_linearize().expect("C3 failed"); - assert_eq!(order, vec![0, 1, 2, 3],); + // Verify order using IDs from the helper map + let main_id = ids["main"]; + let a_id = ids["A"]; + let b_id = ids["B"]; + let common_id = ids["Common"]; + + // Common must be first (or early), Main last. + // Exact topological sort might vary for A and B, but Common must be before them. + assert_eq!(order, vec![main_id, a_id, b_id, common_id]); // Or [common, a, b, main] } #[test] - fn test_cyclic_dependency() { - // Setup: + fn test_cyclic_dependency_graph_structure() { + // Setup: A <-> B cycle + // main -> imports A // A -> imports B // B -> imports A - // Expected: Should finish without infinite loop - - let temp_dir = TempDir::new().unwrap(); - let a_path = create_simf_file( - temp_dir.path(), - "libs/test/A.simf", - "use test::B::some_test;", - ); - create_simf_file( - temp_dir.path(), - "libs/test/B.simf", - "use test::A::another_test;", - ); - let mut lib_map = HashMap::new(); - lib_map.insert("test".to_string(), temp_dir.path().join("libs/test")); - - let root_program = parse_root(&a_path); - let source_name = SourceName::Real(a_path); - let graph = ProjectGraph::new(source_name, Arc::from(lib_map), &root_program) - .expect("Graph build failed"); - - assert_eq!(graph.modules.len(), 2, "Should only have A and B"); + let (graph, ids, _dir) = setup_graph(vec![ + ("main.simf", "use lib::A::entry;"), + ("libs/lib/A.simf", "use lib::B::func;"), + ("libs/lib/B.simf", "use lib::A::func;"), + ]); - // A depends on B - assert!(graph.dependencies[&0].contains(&1)); - // B depends on A (Circular) - assert!(graph.dependencies[&1].contains(&0)); - } + let a_id = ids["A"]; + let b_id = ids["B"]; - #[test] - fn test_c3_cyclic_dependency() { - // Setup: - // A -> imports B - // B -> imports A - // Expected: Should finish without infinite loop - - let temp_dir = TempDir::new().unwrap(); - let a_path = create_simf_file( - temp_dir.path(), - "libs/test/A.simf", - "use test::B::some_test;", + // Check if graph correctly recorded the cycle + assert!( + graph.dependencies[&a_id].contains(&b_id), + "A should depend on B" ); - create_simf_file( - temp_dir.path(), - "libs/test/B.simf", - "use test::A::another_test;", + assert!( + graph.dependencies[&b_id].contains(&a_id), + "B should depend on A" ); - - let mut lib_map = HashMap::new(); - lib_map.insert("test".to_string(), temp_dir.path().join("libs/test")); - - let root_program = parse_root(&a_path); - let source_name = SourceName::Real(a_path); - let graph = ProjectGraph::new(source_name, Arc::from(lib_map), &root_program) - .expect("Graph build failed"); - - let order = graph.c3_linearize().unwrap_err(); - matches!(order, C3Error::CycleDetected(_)); } #[test] - fn test_missing_file_error() { - // Setup: - // root -> imports missing_lib - - let temp_dir = TempDir::new().unwrap(); - let root_path = create_simf_file(temp_dir.path(), "root.simf", "use std::ghost;"); - // We do NOT create ghost.simf - - let mut lib_map = HashMap::new(); - lib_map.insert("std".to_string(), temp_dir.path().join("libs/std")); - - let root_program = parse_root(&root_path); - let source_name = SourceName::Real(root_path); - let result = ProjectGraph::new(source_name, Arc::from(lib_map), &root_program); + fn test_c3_detects_cycle() { + // Uses the same logic as above but verifies linearization fails + let (graph, _, _dir) = setup_graph(vec![ + ("main.simf", "use lib::A::entry;"), + ("libs/lib/A.simf", "use lib::B::func;"), + ("libs/lib/B.simf", "use lib::A::func;"), + ]); - assert!(result.is_err(), "Should fail for missing file"); - let err_msg = result.err().unwrap(); - assert!( - err_msg.contains("does not exist"), - "Error message should mention missing file" - ); + let result = graph.c3_linearize(); + assert!(matches!(result, Err(C3Error::CycleDetected(_)))); } #[test] fn test_ignores_unmapped_imports() { - // Setup: - // root -> "use unknown::library;" - // "unknown" is NOT in library_map. - // Expected: It should simply skip this import (based on `if let Ok(path)` logic) - - let temp_dir = TempDir::new().unwrap(); - let root_path = create_simf_file(temp_dir.path(), "root.simf", "use unknown::library;"); - - let lib_map = HashMap::new(); - - let root_program = parse_root(&root_path); - let source_name = SourceName::Real(root_path); - let graph = ProjectGraph::new(source_name, Arc::from(lib_map), &root_program) - .expect("Should succeed but ignore import"); + // Setup: root imports from "unknown", which is not in our lib_map + let (graph, ids, _dir) = setup_graph(vec![("main.simf", "use unknown::library;")]); assert_eq!(graph.modules.len(), 1, "Should only contain root"); - assert!( - graph.dependencies[&0].is_empty(), - "Root should have no resolved dependencies" - ); + assert!(graph.dependencies[&ids["main"]].is_empty()); } } diff --git a/src/error.rs b/src/error.rs index cc50eb22..c536f3a7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,7 +1,6 @@ use std::fmt; use std::ops::Range; use std::path::PathBuf; -use std::sync::Arc; use chumsky::error::Error as ChumskyError; use chumsky::input::ValueInput; @@ -16,6 +15,7 @@ use crate::lexer::Token; use crate::parse::MatchPattern; use crate::str::{AliasName, FunctionName, Identifier, JetName, ModuleName, WitnessName}; use crate::types::{ResolvedType, UIntType}; +use crate::SourceFile; /// Area that an object spans inside a file. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] @@ -118,16 +118,16 @@ impl> WithSpan for Result { } /// Helper trait to update `Result` with the affected source file. -pub trait WithFile { +pub trait WithSource { /// Update the result with the affected source file. /// /// Enable pretty errors. - fn with_file>>(self, file: F) -> Result; + fn with_source>(self, source: S) -> Result; } -impl WithFile for Result { - fn with_file>>(self, file: F) -> Result { - self.map_err(|e| e.with_file(file.into())) +impl WithSource for Result { + fn with_source>(self, source: S) -> Result { + self.map_err(|e| e.with_source(source.into())) } } @@ -140,10 +140,10 @@ pub struct RichError { error: Error, /// Area that the error spans inside the file. span: Span, - /// File in which the error occurred. + /// File context in which the error occurred. /// /// Required to print pretty errors. - file: Option>, + source: Option, } impl RichError { @@ -152,18 +152,18 @@ impl RichError { RichError { error, span, - file: None, + source: None, } } /// Add the source file where the error occurred. /// /// Enable pretty errors. - pub fn with_file(self, file: Arc) -> Self { + pub fn with_source(self, source: SourceFile) -> Self { Self { error: self.error, span: self.span, - file: Some(file), + source: Some(source), } } @@ -173,12 +173,12 @@ impl RichError { Self { error: Error::CannotParse(reason.to_string()), span: Span::new(0, 0), - file: None, + source: None, } } - pub fn file(&self) -> &Option> { - &self.file + pub fn source(&self) -> &Option { + &self.source } pub fn error(&self) -> &Error { @@ -209,8 +209,10 @@ impl fmt::Display for RichError { (line, col) } - match self.file { - Some(ref file) if !file.is_empty() => { + match self.source { + Some(ref source) if !source.content.is_empty() => { + let file = &source.content(); + let (start_line, start_col) = get_line_col(file, self.span.start); let (end_line, end_col) = get_line_col(file, self.span.end); @@ -219,6 +221,16 @@ impl fmt::Display for RichError { let n_spanned_lines = end_line - start_line_index; let line_num_width = end_line.to_string().len(); + writeln!( + f, + "{:>width$}--> {}:{}:{}", + "", + source.name, + start_line, + start_col, + width = line_num_width + )?; + writeln!(f, "{:width$} |", " ", width = line_num_width)?; let mut lines = file.lines().skip(start_line_index).peekable(); @@ -309,7 +321,7 @@ where found: found_string, }, span, - file: None, + source: None, } } } @@ -336,7 +348,7 @@ where found: found_string, }, span, - file: None, + source: None, } } @@ -352,17 +364,17 @@ where #[derive(Debug, Clone, Hash)] pub struct ErrorCollector { - /// File in which the error occurred. - file: Arc, + /// File context in which the error occurred. + source: SourceFile, /// Collected errors. errors: Vec, } impl ErrorCollector { - pub fn new(file: Arc) -> Self { + pub fn new(source: SourceFile) -> Self { Self { - file, + source: source.clone(), errors: Vec::new(), } } @@ -371,7 +383,7 @@ impl ErrorCollector { pub fn update(&mut self, errors: impl IntoIterator) { let new_errors = errors .into_iter() - .map(|err| err.with_file(Arc::clone(&self.file))); + .map(|err| err.with_source(self.source.clone())); self.errors.extend(new_errors); } @@ -399,6 +411,7 @@ impl fmt::Display for ErrorCollector { /// Records _what_ happened but not where. #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum Error { + UnknownLibrary(String), ArraySizeNonZero(usize), ListBoundPow2(usize), BitStringPow2(usize), @@ -447,6 +460,10 @@ pub enum Error { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Error::UnknownLibrary(name) => write!( + f, + "Unknown module or library '{name}'" + ), Error::ArraySizeNonZero(size) => write!( f, "Expected a non-negative integer as array size, found {size}" @@ -637,6 +654,10 @@ impl From for Error { #[cfg(test)] mod tests { + use std::sync::Arc; + + use crate::SourceName; + use super::*; const FILE: &str = r#"let a1: List = None; @@ -644,47 +665,61 @@ let x: u32 = Left( Right(0) );"#; const EMPTY_FILE: &str = ""; + const FILENAME: &str = ""; #[test] fn display_single_line() { + let source = SourceFile::new(SourceName::Virtual(Arc::from(FILENAME)), Arc::from(FILE)); + let error = Error::ListBoundPow2(5) .with_span(Span::new(13, 19)) - .with_file(Arc::from(FILE)); - let expected = r#" + .with_source(source); + let expected = format!( + r#" + --> {FILENAME}:1:14 | 1 | let a1: List = None; - | ^^^^^^ Expected a power of two greater than one (2, 4, 8, 16, 32, ...) as list bound, found 5"#; + | ^^^^^^ Expected a power of two greater than one (2, 4, 8, 16, 32, ...) as list bound, found 5"# + ); assert_eq!(&expected[1..], &error.to_string()); } #[test] fn display_multi_line() { + let source = SourceFile::new(SourceName::Virtual(Arc::from(FILENAME)), Arc::from(FILE)); let error = Error::CannotParse( "Expected value of type `u32`, got `Either, _>`".to_string(), ) .with_span(Span::new(41, FILE.len())) - .with_file(Arc::from(FILE)); - let expected = r#" + .with_source(source); + let expected = format!( + r#" + --> {FILENAME}:2:14 | 2 | let x: u32 = Left( 3 | Right(0) 4 | ); - | ^^^^^^^^^^^^^^^^^^ Cannot parse: Expected value of type `u32`, got `Either, _>`"#; + | ^^^^^^^^^^^^^^^^^^ Cannot parse: Expected value of type `u32`, got `Either, _>`"# + ); assert_eq!(&expected[1..], &error.to_string()); } #[test] fn display_entire_file() { + let source = SourceFile::new(SourceName::Virtual(Arc::from(FILENAME)), Arc::from(FILE)); let error = Error::CannotParse("This span covers the entire file".to_string()) .with_span(Span::from(FILE)) - .with_file(Arc::from(FILE)); - let expected = r#" + .with_source(source); + let expected = format!( + r#" + --> {FILENAME}:1:1 | 1 | let a1: List = None; 2 | let x: u32 = Left( 3 | Right(0) 4 | ); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot parse: This span covers the entire file"#; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot parse: This span covers the entire file"# + ); assert_eq!(&expected[1..], &error.to_string()); } @@ -702,9 +737,13 @@ let x: u32 = Left( #[test] fn display_empty_file() { + let source = SourceFile::new( + SourceName::Virtual(Arc::from(FILENAME)), + Arc::from(EMPTY_FILE), + ); let error = Error::CannotParse("This error has an empty file".to_string()) .with_span(Span::from(EMPTY_FILE)) - .with_file(Arc::from(EMPTY_FILE)); + .with_source(source); let expected = "Cannot parse: This error has an empty file"; assert_eq!(&expected, &error.to_string()); } diff --git a/src/lib.rs b/src/lib.rs index c5f1104e..0a3fad35 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,7 +22,7 @@ pub mod value; mod witness; use std::collections::HashMap; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use simplicity::jet::elements::ElementsEnv; @@ -34,7 +34,7 @@ pub use simplicity::elements; use crate::debug::DebugSymbols; use crate::driver::ProjectGraph; -use crate::error::{ErrorCollector, WithFile}; +use crate::error::{Error, ErrorCollector, RichError, WithSource, WithSpan}; use crate::parse::{ParseFromStrWithErrors, UseDecl}; pub use crate::types::ResolvedType; pub use crate::value::Value; @@ -42,14 +42,14 @@ pub use crate::witness::{Arguments, Parameters, WitnessTypes, WitnessValues}; #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub enum SourceName { - Real(PathBuf), - Virtual(String), + Real(Arc), + Virtual(Arc), } impl SourceName { pub fn without_extension(&self) -> SourceName { match self { - SourceName::Real(path) => SourceName::Real(path.with_extension("")), + SourceName::Real(path) => SourceName::Real(Arc::from(path.with_extension(""))), SourceName::Virtual(name) => SourceName::Virtual(name.clone()), } } @@ -57,27 +57,76 @@ impl SourceName { impl Default for SourceName { fn default() -> Self { - SourceName::Virtual("".to_string()) + SourceName::Virtual(Arc::from("")) + } +} + +use std::fmt; + +impl fmt::Display for SourceName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SourceName::Real(path) => write!(f, "{}", path.display()), + SourceName::Virtual(name) => write!(f, "{}", name), + } + } +} + +/// Represents a source file containing code. +/// +/// Groups the file's name and its content together to guarantee +/// they are always synchronized when present. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct SourceFile { + /// The name or path of the source file (e.g., "main.simf"). + name: SourceName, + /// The actual text content of the source file. + content: Arc, +} + +impl From<(SourceName, &str)> for SourceFile { + fn from((name, content): (SourceName, &str)) -> Self { + Self { + name, + content: Arc::from(content), + } + } +} + +impl SourceFile { + pub fn new(name: SourceName, content: Arc) -> Self { + Self { name, content } + } + + pub fn name(&self) -> SourceName { + self.name.clone() + } + + pub fn content(&self) -> Arc { + self.content.clone() } } pub type LibTable = HashMap; -pub fn get_full_path(libraries: &LibTable, use_decl: &UseDecl) -> Result { +pub fn get_full_path(libraries: &LibTable, use_decl: &UseDecl) -> Result { let parts: Vec<&str> = use_decl.path().iter().map(|s| s.as_ref()).collect(); - let first_segment = parts[0]; + + let first_segment = match parts.first() { + Some(s) => *s, + None => { + return Err(Error::CannotParse("Empty use path".to_string())) + .with_span(*use_decl.span()) + } + }; if let Some(lib_root) = libraries.get(first_segment) { let mut full_path = lib_root.clone(); full_path.extend(&parts[1..]); - return Ok(full_path); } - Err(format!( - "Unknown module or library '{}'. Did you forget to pass --lib {}=...?", - first_segment, first_segment, - )) + Err(Error::UnknownLibrary(first_segment.to_string())).with_span(*use_decl.span()) } /// The template of a SimplicityHL program. @@ -86,7 +135,7 @@ pub fn get_full_path(libraries: &LibTable, use_decl: &UseDecl) -> Result, + source: SourceFile, } impl TemplateProgram { @@ -100,26 +149,37 @@ impl TemplateProgram { libraries: Arc, s: Str, ) -> Result { + let source_name = source_name.without_extension(); + + // TODO: @LesterEvSe fix all bugs related to error handling let file = s.into(); - let mut error_handler = ErrorCollector::new(Arc::clone(&file)); + let source = SourceFile::new(source_name.clone(), file.clone()); + + let mut error_handler = ErrorCollector::new(source.clone()); let parse_program = parse::Program::parse_from_str_with_errors(&file, &mut error_handler); if let Some(program) = parse_program { - // TODO: Consider a proper resolution strategy later. + // TODO: @LesterEvSe Consider a proper resolution strategy later. + // Consider to add `source.clone()` to Program::from_parse function let driver_program: driver::Program = if libraries.is_empty() { driver::Program::from_parse(&program, source_name)? } else { - let graph = ProjectGraph::new(source_name, libraries, &program)?; - - // TODO: Perhaps add an `error_handler` here, too. + let graph = + ProjectGraph::new(source.clone(), libraries, &program, &mut error_handler)?; graph.resolve_complication_order()? + + // if let Some(graph) = graph { + // // TODO: @LesterEvSe Perhaps add an `error_handler` here, too. + // graph.resolve_complication_order()? + // } else { + // Err(ErrorCollector::to_string(&error_handler))? + // } }; - let ast_program = - ast::Program::analyze(&driver_program).with_file(Arc::clone(&file))?; + let ast_program = ast::Program::analyze(&driver_program).with_source(source.clone())?; Ok(Self { simfony: ast_program, - file, + source, }) } else { Err(ErrorCollector::to_string(&error_handler))? @@ -154,10 +214,10 @@ impl TemplateProgram { let commit = self .simfony .compile(arguments, include_debug_symbols) - .with_file(Arc::clone(&self.file))?; + .with_source(self.source.clone())?; Ok(CompiledProgram { - debug_symbols: self.simfony.debug_symbols(self.file.as_ref()), + debug_symbols: self.simfony.debug_symbols(self.source.content.as_ref()), simplicity: commit, witness_types: self.simfony.witness_types().shallow_clone(), parameter_types: self.simfony.parameters().shallow_clone(), @@ -503,7 +563,7 @@ pub(crate) mod tests { } let source_name = - SourceName::Real(path_ref.parent().unwrap_or(Path::new("")).to_path_buf()); + SourceName::Real(Arc::from(path_ref.parent().unwrap_or(Path::new("")))); // 3. Delegate to your existing template_lib method TestCase::::template_lib(source_name, Arc::from(libraries), path_ref) diff --git a/src/main.rs b/src/main.rs index 1b4bb8bc..42bc0b64 100644 --- a/src/main.rs +++ b/src/main.rs @@ -137,7 +137,7 @@ fn main() -> Result<(), Box> { .collect(); let compiled = match CompiledProgram::new( - SourceName::Real(prog_path.to_path_buf()), + SourceName::Real(Arc::from(prog_path)), Arc::from(libraries), prog_text, args_opt, diff --git a/src/witness.rs b/src/witness.rs index 55cbd8bd..6d0a7dc2 100644 --- a/src/witness.rs +++ b/src/witness.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::fmt; use std::sync::Arc; -use crate::error::{Error, RichError, WithFile, WithSpan}; +use crate::error::{Error, RichError, WithSpan}; use crate::parse; use crate::parse::ParseFromStr; use crate::str::WitnessName; @@ -144,7 +144,7 @@ impl ParseFromStr for ResolvedType { .resolve_builtin() .map_err(Error::UndefinedAlias) .with_span(s) - .with_file(s) + // .with_source(s) // TODO: @LesterEvSe think about this deletion } } From b808b99c13d50d9321cc2d8b753b5f1245b3f3ab Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Thu, 26 Feb 2026 12:52:27 +0200 Subject: [PATCH 07/13] feat: add error handler to driver.rs, but it has not been tested yet --- src/ast.rs | 5 +- src/driver.rs | 363 +++++++++++++++++++++++++++++++++---------------- src/error.rs | 72 ++++++---- src/lib.rs | 58 ++++---- src/parse.rs | 24 ++-- src/witness.rs | 10 +- 6 files changed, 346 insertions(+), 186 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index 89d74655..661327dd 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -728,13 +728,14 @@ impl Scope { } } + // TODO: @LesterEvSe, Consider why we use this function to get type. /// Get the definition of a custom function with visibility and existence checks. /// /// # Errors /// /// - `Error::FileNotFound`: The specified `file_id` does not exist in the resolutions. /// - `Error::FunctionUndefined`: The function is not found in the file's scope OR not defined globally. - /// - `Error::FunctionIsPrivate`: The function exists but is private (and thus not accessible). + /// - `Error::PrivateItem`: The function or type exists but is private. pub fn get_function(&self, name: &FunctionName) -> Result<&CustomFunction, Error> { // The order of the errors is important! let function = self @@ -759,7 +760,7 @@ impl Scope { if file_scope.contains_key(&identifier) { Ok(function) } else { - Err(Error::FunctionIsPrivate(name.clone())) + Err(Error::PrivateItem(name.as_inner().to_string())) } } diff --git a/src/driver.rs b/src/driver.rs index 64f70543..bca12fd2 100644 --- a/src/driver.rs +++ b/src/driver.rs @@ -3,7 +3,7 @@ use std::fmt; use std::path::{Path, PathBuf}; use std::sync::Arc; -use crate::error::{ErrorCollector, Span}; +use crate::error::{Error, ErrorCollector, RichError, Span}; use crate::parse::{self, ParseFromStrWithErrors, Visibility}; use crate::str::{AliasName, FunctionName, Identifier}; use crate::types::AliasedType; @@ -13,6 +13,7 @@ use crate::{get_full_path, impl_eq_hash, LibTable, SourceFile, SourceName}; /// In this architecture, a file and a module are the exact same thing. #[derive(Debug, Clone)] pub struct Module { + pub source: SourceFile, /// The completely parsed program for this specific file. /// it contains all the functions, aliases, and imports defined inside the file. pub parsed_program: parse::Program, @@ -69,62 +70,67 @@ pub struct Program { } impl Program { - pub fn from_parse(parsed: &parse::Program, root_path: SourceName) -> Result { - let root_path = root_path.without_extension(); + pub fn from_parse( + parsed: &parse::Program, + source: SourceFile, + handler: &mut ErrorCollector, + ) -> Option { + let root_path = source.name().without_extension(); let mut items: Vec = Vec::new(); let mut resolutions: Vec = vec![BTreeMap::new()]; let main_file_id = 0usize; - let mut errors: Vec = Vec::new(); + let mut errors: Vec = Vec::new(); for item in parsed.items() { match item { - parse::Item::Use(_) => { - errors.push("Unsuitable Use type".to_string()); + parse::Item::Use(use_decl) => { + let bug_report = RichError::new( + Error::UnknownLibrary(use_decl.path_buf().to_string_lossy().to_string()), + *use_decl.span(), + ); + handler.push(bug_report); } parse::Item::TypeAlias(alias) => { - let res = ProjectGraph::register_def( + if let Some(err) = ProjectGraph::register_def( &mut items, &mut resolutions, main_file_id, item, alias.name().clone().into(), &parse::Visibility::Public, - ); - - if let Err(e) = res { - errors.push(e); + ) { + errors.push(err) } } parse::Item::Function(function) => { - let res = ProjectGraph::register_def( + if let Some(err) = ProjectGraph::register_def( &mut items, &mut resolutions, main_file_id, item, function.name().clone().into(), &parse::Visibility::Public, - ); - - if let Err(e) = res { - errors.push(e); + ) { + errors.push(err); } } parse::Item::Module => {} } } + handler.update_with_source_enrichment(source, errors); - if !errors.is_empty() { - return Err(errors.join("\n")); + if handler.has_errors() { + None + } else { + Some(Program { + items: items.into(), + paths: Arc::from([root_path]), + resolutions: resolutions.into(), + span: *parsed.as_ref(), + }) } - - Ok(Program { - items: items.into(), - paths: Arc::from([root_path]), - resolutions: resolutions.into(), - span: *parsed.as_ref(), - }) } /// Access the items of the program. @@ -158,7 +164,7 @@ pub enum Item { } impl Item { - pub fn from_parse(parsed: &parse::Item, file_id: usize) -> Result { + pub fn from_parse(parsed: &parse::Item, file_id: usize) -> Result { match parsed { parse::Item::TypeAlias(alias) => { let driver_alias = TypeAlias::from_parse(alias); @@ -169,9 +175,12 @@ impl Item { Ok(Item::Function(driver_func)) } parse::Item::Module => Ok(Item::Module), - - // Cannot convert Use to driver::Item - parse::Item::Use(_) => Err("Unsuitable Use type".to_string()), + parse::Item::Use(use_decl) => { + Err(RichError::new( + Error::Internal("Encountered 'Use' item during driver generation. Imports should be resolved by ProjectGraph.".to_string()), + *use_decl.span(), + )) + }, } } } @@ -287,39 +296,51 @@ pub enum C3Error { InconsistentLinearization { module: usize }, } -fn parse_and_get_program(prog_file: &Path) -> Result { - let prog_text = std::fs::read_to_string(prog_file).map_err(|e| e.to_string())?; - let file: Arc = prog_text.into(); - let source = SourceFile::new( - SourceName::Real(Arc::from(prog_file.with_extension(""))), - file.clone(), - ); - let mut error_handler = crate::error::ErrorCollector::new(source); - - if let Some(program) = parse::Program::parse_from_str_with_errors(&file, &mut error_handler) { - Ok(program) - } else { - Err(ErrorCollector::to_string(&error_handler))? +impl ProjectGraph { + fn parse_and_get_program( + full_path: &Path, + importer_source: SourceFile, + span: Span, + handler: &mut ErrorCollector, + ) -> Option { + let dep_key = SourceName::Real(Arc::from(full_path.with_extension(""))); + let Ok(content) = std::fs::read_to_string(full_path) else { + let err = RichError::new(Error::FileNotFound(PathBuf::from(full_path)), span) + .with_source(importer_source.clone()); + + handler.push(err); + return None; + }; + + let dep_source_file = SourceFile::new(dep_key.clone(), Arc::from(content.clone())); + + parse::Program::parse_from_str_with_errors(&content, dep_source_file.clone(), handler).map( + |parsed_program| Module { + source: dep_source_file, + parsed_program, + }, + ) } -} -impl ProjectGraph { pub fn new( - source: SourceFile, + root_source: SourceFile, libraries: Arc, root_program: &parse::Program, - _handler: &mut ErrorCollector, - ) -> Result { - let source_name = source.name().without_extension(); + handler: &mut ErrorCollector, + ) -> Option { + let root_name_no_ext = root_source.name().without_extension(); + let mut modules: Vec = vec![Module { + source: root_source, parsed_program: root_program.clone(), }]; + let mut lookup: HashMap = HashMap::new(); - let mut paths: Vec = vec![source_name.clone()]; + let mut paths: Vec = vec![root_name_no_ext.clone()]; let mut dependencies: HashMap> = HashMap::new(); let root_id = 0; - lookup.insert(source_name, root_id); + lookup.insert(root_name_no_ext, root_id); dependencies.insert(root_id, Vec::new()); // Implementation of the standard BFS algorithm with memoization and queue @@ -327,51 +348,68 @@ impl ProjectGraph { queue.push_back(root_id); while let Some(curr_id) = queue.pop_front() { - let mut pending_imports: Vec = Vec::new(); + // We need this to report errors inside THIS file. + let importer_source = modules[curr_id].source.clone(); let current_program = &modules[curr_id].parsed_program; + // Lists to separate valid logic from errors + let mut valid_imports: Vec<(PathBuf, Span)> = Vec::new(); + let mut resolution_errors: Vec = Vec::new(); + + // PHASE 1: Resolve Imports for elem in current_program.items() { if let parse::Item::Use(use_decl) = elem { - if let Ok(path) = get_full_path(&libraries, use_decl) { - pending_imports.push(path); + match get_full_path(&libraries, use_decl) { + Ok(path) => valid_imports.push((path, *use_decl.span())), + Err(err) => { + resolution_errors.push(err.with_source(importer_source.clone())) + } } } } - for path in pending_imports { + // Phase 2: Load and Parse Dependencies + for (path, import_span) in valid_imports { let full_path = path.with_extension("simf"); - let source_path = SourceName::Real(Arc::from(path)); - - if !full_path.is_file() { - return Err(format!("File in {:?}, does not exist", full_path)); - } + let dep_source_name = SourceName::Real(Arc::from(full_path.as_path())); + let dep_key = dep_source_name.without_extension(); - if let Some(&existing_id) = lookup.get(&source_path) { + if let Some(&existing_id) = lookup.get(&dep_key) { dependencies.entry(curr_id).or_default().push(existing_id); continue; } + let Some(module) = ProjectGraph::parse_and_get_program( + &full_path, + importer_source.clone(), + import_span, + handler, + ) else { + continue; + }; + let last_ind = modules.len(); - let program = parse_and_get_program(&full_path)?; + modules.push(module); - modules.push(Module { - parsed_program: program, - }); - lookup.insert(source_path.clone(), last_ind); - paths.push(source_path.clone()); + lookup.insert(dep_key.clone(), last_ind); + paths.push(dep_key); dependencies.entry(curr_id).or_default().push(last_ind); queue.push_back(last_ind); } } - Ok(Self { - modules, - libraries, - lookup, - paths: paths.into(), - dependencies, - }) + if handler.has_errors() { + None + } else { + Some(Self { + modules, + libraries, + lookup, + paths: paths.into(), + dependencies, + }) + } } pub fn c3_linearize(&self) -> Result, C3Error> { @@ -429,27 +467,30 @@ impl ProjectGraph { file_id: usize, ind: usize, elem: &Identifier, - use_decl_visibility: Visibility, - ) -> Result<(), String> { - let resolution = resolutions[ind] - .get(elem) - .ok_or_else(|| format!("Try using the unknown item `{}`", elem.as_inner()))?; + use_decl: &parse::UseDecl, + ) -> Option { + let Some(resolution) = resolutions[ind].get(elem) else { + return Some(RichError::new( + Error::UnresolvedItem(elem.as_inner().to_string()), + *use_decl.span(), + )); + }; if matches!(resolution.visibility, parse::Visibility::Private) { - return Err(format!( - "Function {} is private and cannot be used.", - elem.as_inner() + return Some(RichError::new( + Error::PrivateItem(elem.as_inner().to_string()), + *use_decl.span(), )); } resolutions[file_id].insert( elem.clone(), Resolution { - visibility: use_decl_visibility, + visibility: use_decl.visibility().clone(), }, ); - Ok(()) + None } fn register_def( @@ -459,32 +500,44 @@ impl ProjectGraph { item: &parse::Item, name: Identifier, vis: &parse::Visibility, - ) -> Result<(), String> { - items.push(Item::from_parse(item, file_id)?); + ) -> Option { + let item = match Item::from_parse(item, file_id) { + Ok(item) => item, + Err(err) => return Some(err), + }; + + items.push(item); resolutions[file_id].insert( name, Resolution { visibility: vis.clone(), }, ); - Ok(()) + + None } - // TODO: @LesterEvSe, consider processing more than one error at a time - fn build_program(&self, order: &Vec) -> Result { + fn build_program(&self, order: &Vec, handler: &mut ErrorCollector) -> Option { let mut items: Vec = Vec::new(); let mut resolutions: Vec = vec![BTreeMap::new(); order.len()]; for &file_id in order { + let importer_source = self.modules[file_id].source.clone(); let program_items = self.modules[file_id].parsed_program.items(); for elem in program_items { + let mut errors: Vec = Vec::new(); match elem { parse::Item::Use(use_decl) => { - let full_path = get_full_path(&self.libraries, use_decl)?; + let full_path = match get_full_path(&self.libraries, use_decl) { + Ok(path) => path, + Err(err) => { + handler.push(err.with_source(importer_source.clone())); + continue; + } + }; let source_full_path = SourceName::Real(Arc::from(full_path)); let ind = self.lookup[&source_full_path]; - let visibility = use_decl.visibility(); let use_targets = match use_decl.items() { parse::UseItems::Single(elem) => std::slice::from_ref(elem), @@ -492,53 +545,64 @@ impl ProjectGraph { }; for target in use_targets { - ProjectGraph::process_use_item( + if let Some(err) = ProjectGraph::process_use_item( &mut resolutions, file_id, ind, target, - visibility.clone(), - )?; + use_decl, + ) { + errors.push(err) + } } } parse::Item::TypeAlias(alias) => { - Self::register_def( + if let Some(err) = Self::register_def( &mut items, &mut resolutions, file_id, elem, alias.name().clone().into(), alias.visibility(), - )?; + ) { + errors.push(err) + } } parse::Item::Function(function) => { - Self::register_def( + if let Some(err) = Self::register_def( &mut items, &mut resolutions, file_id, elem, function.name().clone().into(), function.visibility(), - )?; + ) { + errors.push(err) + } } parse::Item::Module => {} } + handler.update_with_source_enrichment(importer_source.clone(), errors); } } - Ok(Program { - items: items.into(), - paths: self.paths.clone(), - resolutions: resolutions.into(), - span: *self.modules[0].parsed_program.as_ref(), - }) + if handler.has_errors() { + None + } else { + Some(Program { + items: items.into(), + paths: self.paths.clone(), + resolutions: resolutions.into(), + span: *self.modules[0].parsed_program.as_ref(), + }) + } } - pub fn resolve_complication_order(&self) -> Result { + pub fn resolve_complication_order(&self, handler: &mut ErrorCollector) -> Option { // TODO: @LesterEvSe, resolve errors more appropriately let mut order = self.c3_linearize().unwrap(); order.reverse(); - self.build_program(&order) + self.build_program(&order, handler) } } @@ -720,8 +784,33 @@ pub(crate) mod tests { } // Helper to mock the initial root program parsing - fn parse_root(path: &Path) -> parse::Program { - parse_and_get_program(path).expect("Root parsing failed") + // (Assuming your parser works via a helper function) + fn parse_root(path: &Path) -> (parse::Program, SourceFile) { + // 1. Read file + let content = std::fs::read_to_string(path).expect("Failed to read root file for parsing"); + + // 2. Create SourceFile (needed for the new parser signature) + // Note: We use the full path here; the logic inside `new` handles extension removal if needed + let source = SourceFile::new( + SourceName::Real(Arc::from(path)), + Arc::from(content.clone()), + ); + + // 3. Create a temporary handler just for this parse + let mut handler = ErrorCollector::new(); + + // 4. Parse + let program = + parse::Program::parse_from_str_with_errors(&content, source.clone(), &mut handler); + + // 5. Check results + assert!( + !handler.has_errors(), + "Test Setup Failed: Root file syntax error: {}", + ErrorCollector::to_string(&handler) + ); + + (program.expect("Root parsing failed internally"), source) } /// Sets up a graph with "lib" mapped to "libs/lib". @@ -744,13 +833,9 @@ pub(crate) mod tests { lib_map.insert("lib".to_string(), temp_dir.path().join("libs/lib")); // 3. Parse & Build - let root_program = parse_root(&root_p); - let source = SourceFile::new( - SourceName::Real(Arc::from(root_p)), - Arc::from(""), // TODO: @LesterEvSe, consider to change it - ); + let (root_program, source) = parse_root(&root_p); - let mut handler = ErrorCollector::new(source.clone()); + let mut handler = ErrorCollector::new(); let graph = ProjectGraph::new(source, Arc::from(lib_map), &root_program, &mut handler) .expect( @@ -784,8 +869,9 @@ pub(crate) mod tests { let root_id = *ids.get("main").unwrap(); let order = vec![root_id]; // Only one file + let mut error_handler = ErrorCollector::new(); let program = graph - .build_program(&order) + .build_program(&order, &mut error_handler) .expect("Failed to build program"); let scope = &program.resolutions[root_id]; @@ -823,8 +909,9 @@ pub(crate) mod tests { // Manual topological order: A -> B -> Root let order = vec![id_a, id_b, id_root]; + let mut error_handler = ErrorCollector::new(); let program = graph - .build_program(&order) + .build_program(&order, &mut error_handler) .expect("Failed to build program"); // Check B's scope @@ -875,12 +962,25 @@ pub(crate) mod tests { // Order: A -> B -> Root let order = vec![id_a, id_b, id_root]; - let result = graph.build_program(&order); + let mut error_handler = ErrorCollector::new(); + let result = graph.build_program(&order, &mut error_handler); assert!( - result.is_err(), + result.is_none(), "Build should fail when importing a private binding" ); + + assert!( + error_handler.has_errors(), + "Error handler should contain errors" + ); + + let err_msg = ErrorCollector::to_string(&error_handler); + assert!( + err_msg.contains("private"), + "Error message should mention 'private', but got: \n{}", + err_msg + ); } #[test] @@ -1042,4 +1142,33 @@ pub(crate) mod tests { assert_eq!(graph.modules.len(), 1, "Should only contain root"); assert!(graph.dependencies[&ids["main"]].is_empty()); } + + #[test] + fn test_missing_file_error() { + // MANUAL SETUP REQUIRED + // We cannot use `setup_graph` here because we expect `ProjectGraph::new` to fail/return None. + + let temp_dir = TempDir::new().unwrap(); + let root_path = create_simf_file(temp_dir.path(), "main.simf", "use lib::ghost::Phantom;"); + // We purposefully DO NOT create ghost.simf + + let mut lib_map = HashMap::new(); + lib_map.insert("lib".to_string(), temp_dir.path().join("libs/lib")); + + let (root_program, root_source) = parse_root(&root_path); + let mut handler = ErrorCollector::new(); + + let result = + ProjectGraph::new(root_source, Arc::from(lib_map), &root_program, &mut handler); + + assert!(result.is_none(), "Graph construction should fail"); + assert!(handler.has_errors()); + + let error_msg = ErrorCollector::to_string(&handler); + assert!( + error_msg.contains("File not found") || error_msg.contains("ghost.simf"), + "Error message should mention 'ghost.simf' or 'File not found'. Got: {}", + error_msg + ); + } } diff --git a/src/error.rs b/src/error.rs index c536f3a7..ef53e400 100644 --- a/src/error.rs +++ b/src/error.rs @@ -137,7 +137,7 @@ impl WithSource for Result { #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct RichError { /// The error that occurred. - error: Error, + error: Box, /// Area that the error spans inside the file. span: Span, /// File context in which the error occurred. @@ -150,7 +150,7 @@ impl RichError { /// Create a new error with context. pub fn new(error: Error, span: Span) -> RichError { RichError { - error, + error: Box::new(error), span, source: None, } @@ -171,7 +171,7 @@ impl RichError { /// a problem on the parsing side. pub fn parsing_error(reason: &str) -> Self { Self { - error: Error::CannotParse(reason.to_string()), + error: Box::new(Error::CannotParse(reason.to_string())), span: Span::new(0, 0), source: None, } @@ -263,7 +263,7 @@ impl std::error::Error for RichError {} impl From for Error { fn from(error: RichError) -> Self { - error.error + *error.error } } @@ -279,7 +279,7 @@ where I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, { fn merge(self, other: Self) -> Self { - match (&self.error, &other.error) { + match (&*self.error, &*other.error) { (Error::Grammar(_), Error::Grammar(_)) => other, (Error::Grammar(_), _) => other, (_, Error::Grammar(_)) => self, @@ -315,11 +315,11 @@ where let found_string = found.map(|t| t.to_string()); Self { - error: Error::Syntax { + error: Box::new(Error::Syntax { expected: expected_tokens, label: None, found: found_string, - }, + }), span, source: None, } @@ -342,11 +342,11 @@ where let found_string = found.map(|t| t.to_string()); Self { - error: Error::Syntax { + error: Box::new(Error::Syntax { expected: expected_strings, label: None, found: found_string, - }, + }), span, source: None, } @@ -355,7 +355,7 @@ where fn label_with(&mut self, label: &'tokens str) { if let Error::Syntax { label: ref mut l, .. - } = &mut self.error + } = &mut *self.error { *l = Some(label.to_string()); } @@ -364,26 +364,36 @@ where #[derive(Debug, Clone, Hash)] pub struct ErrorCollector { - /// File context in which the error occurred. - source: SourceFile, - /// Collected errors. errors: Vec, } +impl Default for ErrorCollector { + fn default() -> Self { + Self::new() + } +} + impl ErrorCollector { - pub fn new(source: SourceFile) -> Self { - Self { - source: source.clone(), - errors: Vec::new(), - } + pub fn new() -> Self { + Self { errors: Vec::new() } } - /// Extend existing errors with slice of new errors. - pub fn update(&mut self, errors: impl IntoIterator) { + /// Exend existing errors with concrete `RichError`. + /// We assume that `RichError` contains `SourceFile`. + pub fn push(&mut self, error: RichError) { + self.errors.push(error); + } + + /// Extend existing errors with slice of new errors and enrich them with source. + pub fn update_with_source_enrichment( + &mut self, + source: SourceFile, + errors: impl IntoIterator, + ) { let new_errors = errors .into_iter() - .map(|err| err.with_source(self.source.clone())); + .map(|err| err.with_source(source.clone())); self.errors.extend(new_errors); } @@ -392,8 +402,8 @@ impl ErrorCollector { &self.errors } - pub fn is_empty(&self) -> bool { - self.get().is_empty() + pub fn has_errors(&self) -> bool { + !self.errors.is_empty() } } @@ -411,6 +421,7 @@ impl fmt::Display for ErrorCollector { /// Records _what_ happened but not where. #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum Error { + Internal(String), UnknownLibrary(String), ArraySizeNonZero(usize), ListBoundPow2(usize), @@ -430,12 +441,13 @@ pub enum Error { JetDoesNotExist(JetName), InvalidCast(ResolvedType, ResolvedType), FileNotFound(PathBuf), + UnresolvedItem(String), + PrivateItem(String), MainNoInputs, MainNoOutput, MainRequired, FunctionRedefined(FunctionName), FunctionUndefined(FunctionName), - FunctionIsPrivate(FunctionName), InvalidNumberOfArguments(usize, usize), FunctionNotFoldable(FunctionName), FunctionNotLoopable(FunctionName), @@ -460,6 +472,10 @@ pub enum Error { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Error::Internal(err) => write!( + f, + "INTERNAL ERROR: {err}" + ), Error::UnknownLibrary(name) => write!( f, "Unknown module or library '{name}'" @@ -539,9 +555,13 @@ impl fmt::Display for Error { f, "Function `{name}` was called but not defined" ), - Error::FunctionIsPrivate(name) => write!( + Error::UnresolvedItem(name) => write!( + f, + "Unknown item `{name}`" + ), + Error::PrivateItem(name) => write!( f, - "Function `{name}` is private" + "Item `{name}` is private" ), Error::InvalidNumberOfArguments(expected, found) => write!( f, diff --git a/src/lib.rs b/src/lib.rs index 0a3fad35..187b1e4b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -150,40 +150,38 @@ impl TemplateProgram { s: Str, ) -> Result { let source_name = source_name.without_extension(); - - // TODO: @LesterEvSe fix all bugs related to error handling let file = s.into(); let source = SourceFile::new(source_name.clone(), file.clone()); - let mut error_handler = ErrorCollector::new(source.clone()); - let parse_program = parse::Program::parse_from_str_with_errors(&file, &mut error_handler); - - if let Some(program) = parse_program { - // TODO: @LesterEvSe Consider a proper resolution strategy later. - // Consider to add `source.clone()` to Program::from_parse function - let driver_program: driver::Program = if libraries.is_empty() { - driver::Program::from_parse(&program, source_name)? - } else { - let graph = - ProjectGraph::new(source.clone(), libraries, &program, &mut error_handler)?; - graph.resolve_complication_order()? - - // if let Some(graph) = graph { - // // TODO: @LesterEvSe Perhaps add an `error_handler` here, too. - // graph.resolve_complication_order()? - // } else { - // Err(ErrorCollector::to_string(&error_handler))? - // } - }; + // Create Global error_handler + let mut error_handler = ErrorCollector::new(); + + // 1. Parse root file + let parsed_program = + parse::Program::parse_from_str_with_errors(&file, source.clone(), &mut error_handler) + .ok_or_else(|| error_handler.to_string())?; - let ast_program = ast::Program::analyze(&driver_program).with_source(source.clone())?; - Ok(Self { - simfony: ast_program, - source, - }) + // 2. Create the driver program + let driver_program: driver::Program = if libraries.is_empty() { + driver::Program::from_parse(&parsed_program, source.clone(), &mut error_handler) + .ok_or_else(|| error_handler.to_string())? } else { - Err(ErrorCollector::to_string(&error_handler))? - } + ProjectGraph::new( + source.clone(), + libraries, + &parsed_program, + &mut error_handler, + ) + .and_then(|graph| graph.resolve_complication_order(&mut error_handler)) + .ok_or_else(|| error_handler.to_string())? + }; + + // 3. AST Analysis + let ast_program = ast::Program::analyze(&driver_program).with_source(source.clone())?; + Ok(Self { + simfony: ast_program, + source, + }) } /// Access the parameters of the program. @@ -772,7 +770,7 @@ pub(crate) mod tests { } #[test] - #[should_panic(expected = "Function SecretType is private and cannot be used")] + #[should_panic(expected = "Item `SecretType` is private")] fn private_type_visibility_error() { let main_code = r#" use temp::hidden::SecretType; diff --git a/src/parse.rs b/src/parse.rs index d228c720..8a78b5e2 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -18,7 +18,6 @@ use miniscript::iter::{Tree, TreeLike}; use crate::error::ErrorCollector; use crate::error::{Error, RichError, Span}; -use crate::impl_eq_hash; use crate::lexer::Token; use crate::num::NonZeroPow2Usize; use crate::pattern::Pattern; @@ -27,6 +26,7 @@ use crate::str::{ WitnessName, }; use crate::types::{AliasedType, BuiltinAlias, TypeConstructible}; +use crate::{impl_eq_hash, SourceFile}; /// A program is a sequence of items. #[derive(Clone, Debug)] @@ -994,7 +994,11 @@ pub trait ParseFromStr: Sized { /// Trait for parsing with collection of errors. pub trait ParseFromStrWithErrors: Sized { /// Parse a value from the string `s` with Errors. - fn parse_from_str_with_errors(s: &str, handler: &mut ErrorCollector) -> Option; + fn parse_from_str_with_errors( + s: &str, + source: SourceFile, + handler: &mut ErrorCollector, + ) -> Option; } /// Trait for generating parsers of themselves. @@ -1038,10 +1042,14 @@ impl ParseFromStr for A { } impl ParseFromStrWithErrors for A { - fn parse_from_str_with_errors(s: &str, handler: &mut ErrorCollector) -> Option { + fn parse_from_str_with_errors( + s: &str, + source: SourceFile, + handler: &mut ErrorCollector, + ) -> Option { let (tokens, lex_errs) = crate::lexer::lex(s); - handler.update(lex_errs); + handler.update_with_source_enrichment(source.clone(), lex_errs); let tokens = tokens?; let (ast, parse_errs) = A::parser() @@ -1053,14 +1061,14 @@ impl ParseFromStrWithErrors for A { ) .into_output_errors(); - handler.update(parse_errs); + handler.update_with_source_enrichment(source.clone(), parse_errs); // TODO: We should return parsed result if we found errors, but because analyzing in `ast` module // is not handling poisoned tree right now, we don't return parsed result - if handler.get().is_empty() { - ast - } else { + if handler.has_errors() { None + } else { + ast } } } diff --git a/src/witness.rs b/src/witness.rs index 6d0a7dc2..6cf2ca3a 100644 --- a/src/witness.rs +++ b/src/witness.rs @@ -220,19 +220,23 @@ impl crate::ArbitraryOfType for Arguments { #[cfg(test)] mod tests { use super::*; + use crate::error::ErrorCollector; use crate::parse::ParseFromStr; use crate::value::ValueConstructible; - use crate::SourceName; use crate::{ast, driver, parse, CompiledProgram, SatisfiedProgram}; + use crate::{SourceFile, SourceName}; #[test] fn witness_reuse() { let s = r#"fn main() { assert!(jet::eq_32(witness::A, witness::A)); }"#; + let source = SourceFile::new(SourceName::default(), Arc::from(s)); + let mut error_handler = ErrorCollector::new(); + let program = parse::Program::parse_from_str(s).expect("parsing works"); - let driver_program = - driver::Program::from_parse(&program, SourceName::default()).expect("driver works"); + let driver_program = driver::Program::from_parse(&program, source, &mut error_handler) + .expect("driver works"); match ast::Program::analyze(&driver_program).map_err(Error::from) { Ok(_) => panic!("Witness reuse was falsely accepted"), Err(Error::WitnessReused(..)) => {} From 7cc8b588f89c30bbf2e9cffaa27c894d308cd086 Mon Sep 17 00:00:00 2001 From: Sdoba16 Date: Thu, 26 Feb 2026 13:06:09 +0200 Subject: [PATCH 08/13] Fix for linearization --- src/driver.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/driver.rs b/src/driver.rs index bca12fd2..6b4613b2 100644 --- a/src/driver.rs +++ b/src/driver.rs @@ -375,7 +375,10 @@ impl ProjectGraph { let dep_key = dep_source_name.without_extension(); if let Some(&existing_id) = lookup.get(&dep_key) { - dependencies.entry(curr_id).or_default().push(existing_id); + let deps = dependencies.entry(curr_id).or_default(); + if !deps.contains(&existing_id) { + deps.push(existing_id); + } continue; } From 2b5b7ad6f1f85c6dc8840abd6bd08015d20332d0 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Mon, 2 Mar 2026 14:53:38 +0200 Subject: [PATCH 09/13] feat: add a new test and check the keyword in the library aliases --- src/driver.rs | 45 +++++++++++++++++++++++++++++++++++---------- src/error.rs | 2 +- src/lexer.rs | 6 ++++++ src/lib.rs | 47 +++++++++++++++++++++++++++++++++++++++++++---- src/main.rs | 10 +++++++++- 5 files changed, 94 insertions(+), 16 deletions(-) diff --git a/src/driver.rs b/src/driver.rs index 6b4613b2..e98e84a4 100644 --- a/src/driver.rs +++ b/src/driver.rs @@ -292,8 +292,21 @@ impl_eq_hash!(TypeAlias; name, ty); #[derive(Debug)] pub enum C3Error { - CycleDetected(Vec), - InconsistentLinearization { module: usize }, + CycleDetected(Vec), + InconsistentLinearization { module: String }, +} + +impl fmt::Display for C3Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + C3Error::CycleDetected(cycle) => { + write!(f, "Circular dependency detected: {:?}", cycle.join(" -> ")) + } + C3Error::InconsistentLinearization { module } => { + write!(f, "Inconsistent resolution order for module '{:?}'", module) + } + } + } } impl ProjectGraph { @@ -438,7 +451,11 @@ impl ProjectGraph { if visiting.contains(&module) { let cycle_start = visiting.iter().position(|m| *m == module).unwrap(); - return Err(C3Error::CycleDetected(visiting[cycle_start..].to_vec())); + let cycle_names: Vec = visiting[cycle_start..] + .iter() + .map(|&id| self.modules[id].source.name().to_string()) + .collect(); + return Err(C3Error::CycleDetected(cycle_names)); } visiting.push(module); @@ -448,14 +465,16 @@ impl ProjectGraph { let mut seqs: Vec> = Vec::new(); for parent in &parents { - let lin = self.linearize_rec(*parent, memo, visiting)?; - seqs.push(lin); + let line = self.linearize_rec(*parent, memo, visiting)?; + seqs.push(line); } seqs.push(parents.clone()); let mut result = vec![module]; - let merged = merge(seqs).ok_or(C3Error::InconsistentLinearization { module })?; + let merged = merge(seqs).ok_or(C3Error::InconsistentLinearization { + module: self.modules[module].source.name().to_string(), + })?; result.extend(merged); @@ -601,11 +620,17 @@ impl ProjectGraph { } } - pub fn resolve_complication_order(&self, handler: &mut ErrorCollector) -> Option { - // TODO: @LesterEvSe, resolve errors more appropriately - let mut order = self.c3_linearize().unwrap(); + pub fn resolve_complication_order( + &self, + handler: &mut ErrorCollector, + ) -> Result, String> { + let mut order = match self.c3_linearize() { + Ok(order) => order, + Err(err) => return Err(err.to_string()), + }; order.reverse(); - self.build_program(&order, handler) + + Ok(self.build_program(&order, handler)) } } diff --git a/src/error.rs b/src/error.rs index ef53e400..d5a2db6e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -379,7 +379,7 @@ impl ErrorCollector { Self { errors: Vec::new() } } - /// Exend existing errors with concrete `RichError`. + /// Extend existing errors with concrete `RichError`. /// We assume that `RichError` contains `SourceFile`. pub fn push(&mut self, error: RichError) { self.errors.push(error); diff --git a/src/lexer.rs b/src/lexer.rs index 0631b9b1..108c9c99 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -8,6 +8,12 @@ use crate::str::{Binary, Decimal, Hexadecimal}; pub type Spanned = (T, SimpleSpan); pub type Tokens<'src> = Vec<(Token<'src>, crate::error::Span)>; +// Define your SSoT here +pub const RESERVED_TOKENS: &[&str] = &[ + "pub", "use", "as", "fn", "let", "type", "mod", "const", "match", "true", "false", "jet", + "witness", "param", +]; + #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum Token<'src> { // Keywords diff --git a/src/lib.rs b/src/lib.rs index 187b1e4b..c90d09be 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,7 @@ pub use simplicity::elements; use crate::debug::DebugSymbols; use crate::driver::ProjectGraph; use crate::error::{Error, ErrorCollector, RichError, WithSource, WithSpan}; +use crate::lexer::RESERVED_TOKENS; use crate::parse::{ParseFromStrWithErrors, UseDecl}; pub use crate::types::ResolvedType; pub use crate::value::Value; @@ -129,6 +130,19 @@ pub fn get_full_path(libraries: &LibTable, use_decl: &UseDecl) -> Result Result<(), String> { + for k in libraries.keys() { + if RESERVED_TOKENS.contains(&k.as_str()) { + return Err(format!( + "Error: The identifier `{}` is a reserved keyword for intrinsic operations and cannot be utilized as a library name.", + k + )); + } + } + Ok(()) +} + /// The template of a SimplicityHL program. /// /// A template has parameterized values that need to be supplied with arguments. @@ -149,6 +163,7 @@ impl TemplateProgram { libraries: Arc, s: Str, ) -> Result { + is_reserved_tokens_in_aliases(&libraries)?; let source_name = source_name.without_extension(); let file = s.into(); let source = SourceFile::new(source_name.clone(), file.clone()); @@ -166,14 +181,17 @@ impl TemplateProgram { driver::Program::from_parse(&parsed_program, source.clone(), &mut error_handler) .ok_or_else(|| error_handler.to_string())? } else { - ProjectGraph::new( + let graph = ProjectGraph::new( source.clone(), libraries, &parsed_program, &mut error_handler, ) - .and_then(|graph| graph.resolve_complication_order(&mut error_handler)) - .ok_or_else(|| error_handler.to_string())? + .ok_or_else(|| error_handler.to_string())?; + + graph + .resolve_complication_order(&mut error_handler)? + .ok_or_else(|| error_handler.to_string())? }; // 3. AST Analysis @@ -713,7 +731,7 @@ pub(crate) mod tests { } #[test] - #[should_panic(expected = "CycleDetected")] + #[should_panic(expected = "Circular dependency detected:")] fn cyclic_dependency_error() { let main_code = "use temp::module_a::TypeA; fn main() {}"; @@ -788,6 +806,27 @@ pub(crate) mod tests { .assert_run_success(); } + #[test] + #[should_panic( + expected = "Error: The identifier `jet` is a reserved keyword for intrinsic operations and cannot be utilized as a library name." + )] + fn using_jet_as_module() { + let main_code = r#" + use jet::eq_32::SecretType; + fn main() {} + "#; + + let libs = vec![( + "jet", + "temp/eq_32.simf", + "type SecretType = u32; pub fn ok() {}", + )]; + + let (test, _dir) = TestCase::temp_env(main_code, libs); + test.with_witness_values(WitnessValues::default()) + .assert_run_success(); + } + #[test] #[ignore] #[should_panic(expected = "was defined multiple times")] // TODO: Or not? Fix it later, after receiving a response from the devs. diff --git a/src/main.rs b/src/main.rs index 42bc0b64..cf339556 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use base64::display::Base64Display; use base64::engine::general_purpose::STANDARD; use clap::{Arg, ArgAction, Command}; -use simplicityhl::{AbiMeta, CompiledProgram, LibTable, SourceName}; +use simplicityhl::{lexer::RESERVED_TOKENS, AbiMeta, CompiledProgram, LibTable, SourceName}; use std::{env, fmt, sync::Arc}; #[cfg_attr(feature = "serde", derive(serde::Serialize))] @@ -132,6 +132,14 @@ fn main() -> Result<(), Box> { std::process::exit(1); } + if RESERVED_TOKENS.contains(&parts[0]) { + eprintln!( + "Error: The identifier `{}` is a reserved keyword for intrinsic operations and cannot be utilized as a library name.", + parts[0] + ); + std::process::exit(1); + } + (parts[0].to_string(), std::path::PathBuf::from(parts[1])) }) .collect(); From 8e5adb85d23926f3fcef0e69179bb837434bba97 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Fri, 6 Mar 2026 16:37:22 +0200 Subject: [PATCH 10/13] feat: add and test import aliases --- examples/single_lib/main.simf | 4 +- src/ast.rs | 102 ++++++++--- src/driver.rs | 307 +++++++++++++++++++++++++++++++++- src/error.rs | 11 +- src/lexer.rs | 4 + src/parse.rs | 38 ++++- 6 files changed, 421 insertions(+), 45 deletions(-) diff --git a/examples/single_lib/main.simf b/examples/single_lib/main.simf index 5f18ba68..7897ab8a 100644 --- a/examples/single_lib/main.simf +++ b/examples/single_lib/main.simf @@ -1,4 +1,4 @@ -pub use temp::constants::utils::two; +pub use temp::constants::utils::two as smth; use temp::funcs::{get_five, Smth}; fn seven() -> u32 { @@ -6,6 +6,6 @@ fn seven() -> u32 { } fn main() { - let (_, temp): (bool, u32) = jet::add_32(two(), get_five()); + let (_, temp): (bool, u32) = jet::add_32(smth(), get_five()); assert!(jet::eq_32(temp, seven())); } \ No newline at end of file diff --git a/src/ast.rs b/src/ast.rs index 661327dd..b78a2a91 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -9,7 +9,7 @@ use miniscript::iter::{Tree, TreeLike}; use simplicity::jet::Elements; use crate::debug::{CallTracker, DebugSymbols, TrackedCallName}; -use crate::driver::ProgramResolutions; +use crate::driver::{AliasRegistry, IdentifierWithFileID, ProgramResolutions}; use crate::error::{Error, RichError, Span, WithSpan}; use crate::num::{NonZeroPow2Usize, Pow2Usize}; use crate::parse::MatchPattern; @@ -521,26 +521,50 @@ impl TreeLike for ExprTree<'_> { /// 2. Resolving type aliases /// 3. Assigning types to each witness expression /// 4. Resolving calls to custom functions -#[derive(Clone, Debug, Eq, PartialEq, Default)] +#[derive(Clone, Debug, Eq, PartialEq)] struct Scope { resolutions: ProgramResolutions, paths: Arc<[SourceName]>, + import_aliases: AliasRegistry, file_id: usize, // ID of the file from which the function is called. variables: Vec>, aliases: HashMap, parameters: HashMap, witnesses: HashMap, - functions: HashMap, + functions: HashMap, is_main: bool, call_tracker: CallTracker, } +impl Default for Scope { + fn default() -> Self { + Self { + resolutions: Arc::from([]), + paths: Arc::from([]), + import_aliases: AliasRegistry::default(), + file_id: 0, + variables: Vec::new(), + aliases: HashMap::new(), + parameters: HashMap::new(), + witnesses: HashMap::new(), + functions: HashMap::new(), + is_main: false, + call_tracker: CallTracker::default(), + } + } +} + impl Scope { - pub fn new(resolutions: ProgramResolutions, paths: Arc<[SourceName]>) -> Self { + pub fn new( + resolutions: ProgramResolutions, + paths: Arc<[SourceName]>, + import_aliases: AliasRegistry, + ) -> Self { Self { resolutions, paths, + import_aliases, file_id: 0, variables: Vec::new(), aliases: HashMap::new(), @@ -717,44 +741,75 @@ impl Scope { pub fn insert_function( &mut self, name: FunctionName, + file_id: usize, function: CustomFunction, ) -> Result<(), Error> { - match self.functions.entry(name.clone()) { - Entry::Occupied(_) => Err(Error::FunctionRedefined(name)), - Entry::Vacant(entry) => { - entry.insert(function); - Ok(()) - } + let global_id = (name.clone().into(), file_id); + + if self.functions.contains_key(&global_id) { + return Err(Error::FunctionRedefined(name)); } + + let _ = self.functions.insert(global_id, function); + Ok(()) + + // match self.functions.entry(global_id) { + // Entry::Occupied(_) => Err(Error::FunctionRedefined(name)), + // Entry::Vacant(entry) => { + // entry.insert(function); + // Ok(()) + // } + // } } - // TODO: @LesterEvSe, Consider why we use this function to get type. - /// Get the definition of a custom function with visibility and existence checks. + // TODO: @LesterEvSe, Consider why we use this function to get a type. + + /// 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 /// - /// - `Error::FileNotFound`: The specified `file_id` does not exist in the resolutions. - /// - `Error::FunctionUndefined`: The function is not found in the file's scope OR not defined globally. - /// - `Error::PrivateItem`: The function or type exists but is private. + /// * [`Error::FunctionUndefined`]: The function is not found in the global registry. + /// * [`Error::FileNotFound`]: The specified `file_id` does not exist in the resolutions table. + /// * [`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> { - // The order of the errors is important! + // 1. Get the true global ID of the alias (or keep the current name if it is not aliased). + // Note: The order of the errors is important! We must know the true identify first. + let initial_id = (name.clone().into(), self.file_id); + let global_id = self + .import_aliases + .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 .functions - .get(name) + .get(&global_id) .ok_or_else(|| Error::FunctionUndefined(name.clone()))?; - let source_name = self.paths[self.file_id].clone(); + let source_name = &self.paths[self.file_id]; let file_scope = match source_name { SourceName::Real(path) => self .resolutions .get(self.file_id) - .ok_or(Error::FileNotFound(path.to_path_buf()))?, // TODO: File or pub type + .ok_or_else(|| Error::FileNotFound(path.to_path_buf()))?, SourceName::Virtual(_) => { + // Virtual sources (e.g., injected code or REPL/Tests) bypass local visibility checks. return Ok(function); } }; + // 3. Verify lcoal scope visibility. + // We successfully found the function globally, but is this file allowed to use it? let identifier: Identifier = name.clone().into(); if file_scope.contains_key(&identifier) { @@ -784,10 +839,13 @@ trait AbstractSyntaxTree: Sized { } impl Program { - // TODO: Add visibility check inside program pub fn analyze(from: &driver::Program) -> Result { let unit = ResolvedType::unit(); - let mut scope = Scope::new(Arc::from(from.resolutions()), Arc::from(from.paths())); + let mut scope = Scope::new( + Arc::from(from.resolutions()), + Arc::from(from.paths()), + from.import_aliases().clone(), + ); let items = from .items() .iter() @@ -869,7 +927,7 @@ impl AbstractSyntaxTree for Function { debug_assert!(scope.is_topmost()); let function = CustomFunction { params, body }; scope - .insert_function(from.name().clone(), function) + .insert_function(from.name().clone(), file_id, function) .with_span(from)?; return Ok(Self::Custom); diff --git a/src/driver.rs b/src/driver.rs index e98e84a4..6b2d2efb 100644 --- a/src/driver.rs +++ b/src/driver.rs @@ -4,7 +4,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use crate::error::{Error, ErrorCollector, RichError, Span}; -use crate::parse::{self, ParseFromStrWithErrors, Visibility}; +use crate::parse::{self, AliasedIdentifier, ParseFromStrWithErrors, Visibility}; use crate::str::{AliasName, FunctionName, Identifier}; use crate::types::AliasedType; use crate::{get_full_path, impl_eq_hash, LibTable, SourceFile, SourceName}; @@ -19,6 +19,8 @@ pub struct Module { pub parsed_program: parse::Program, } +pub type IdentifierWithFileID = (Identifier, usize); + /// The Dependency Graph itself. pub struct ProjectGraph { /// Arena Pattern: the data itself lives here. @@ -54,6 +56,35 @@ pub type FileResolutions = BTreeMap; pub type ProgramResolutions = Arc<[FileResolutions]>; +/// A standard mapping from one unique identifier to another +pub type AliasMap = BTreeMap; + +/// Manages the resolution of import aliases across the entire program. +#[derive(Clone, Debug, Default)] +pub struct AliasRegistry { + /// Maps an alias to its immediate target. + /// (e.g., `use B as C;` stores C -> B) + pub(self) direct_targets: AliasMap, + + /// Caches the final, original definition of an alias to avoid walking the chain. + /// (e.g., If C -> B and B -> A, this stores C -> A) + pub(self) resolved_roots: AliasMap, +} + +impl AliasRegistry { + /// Access the direct targets of the `AliasRegistry` + pub fn direct_targets(&self) -> &AliasMap { + &self.direct_targets + } + + /// Access the resolved roots of the `AliasRegistry` + pub fn resolved_roots(&self) -> &AliasMap { + &self.resolved_roots + } +} + +impl_eq_hash!(AliasRegistry; direct_targets, resolved_roots); + #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Resolution { pub visibility: Visibility, @@ -64,7 +95,8 @@ pub struct Program { items: Arc<[Item]>, paths: Arc<[SourceName]>, - // Use BTreeMap instead of HashMap for the impl_eq_hash! macro. + import_aliases: AliasRegistry, + resolutions: ProgramResolutions, span: Span, } @@ -127,29 +159,35 @@ impl Program { Some(Program { items: items.into(), paths: Arc::from([root_path]), + import_aliases: AliasRegistry::default(), resolutions: resolutions.into(), span: *parsed.as_ref(), }) } } - /// Access the items of the program. + /// Access the items of the Program. pub fn items(&self) -> &[Item] { &self.items } - /// Access the paths of the program + /// Access the paths of the Program. pub fn paths(&self) -> &[SourceName] { &self.paths } - /// Access the scope items of the program. + /// Access the import aliases of the Program. + pub fn import_aliases(&self) -> &AliasRegistry { + &self.import_aliases + } + + /// Access the scope items of the Program. pub fn resolutions(&self) -> &[FileResolutions] { &self.resolutions } } -impl_eq_hash!(Program; items, paths, resolutions); +impl_eq_hash!(Program; items, paths, import_aliases, resolutions); /// An item is a component of a driver Program #[derive(Clone, Debug, Eq, PartialEq, Hash)] @@ -484,20 +522,82 @@ impl ProjectGraph { Ok(result) } + /// Processes a single imported item (or alias) during the module resolution phase. + /// + /// This function verifies that the requested item exists in the source module and + /// that it has the appropriate public visibility to be imported. If validation passes, + /// the item is registered in the importing module's resolution table and global alias registry. + /// + /// # Arguments + /// + /// * `import_aliases` - The global registry tracking alias chains and their canonical roots. + /// * `resolutions` - The global, mutable array mapping each `file_id` to its localized `FileResolutions` table. + /// * `file_id` - The unique identifier of the module that is *performing* the import (the destination). + /// * `ind` - The unique identifier of the source module being imported *from*. + /// * `aliased_identifier` - 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 `None` on success. Returns `Some(RichError)` if: + /// * [`Error::DuplicateAlias`]: The target `alias` (or imported name) has already been used in the current module. + /// * [`Error::UnresolvedItem`]: The target `elem` does not exist in the source module (`ind`). + /// * [`Error::PrivateItem`]: The target exists, but its visibility is explicitly `Private`, fn process_use_item( + import_aliases: &mut AliasRegistry, resolutions: &mut [FileResolutions], file_id: usize, ind: usize, - elem: &Identifier, + (elem, alias): &AliasedIdentifier, use_decl: &parse::UseDecl, ) -> Option { + let orig_id = (elem.clone(), ind); + + // 1. Determine the local name and ID up front + let local_name = alias.as_ref().unwrap_or(elem); + let local_id = (local_name.clone(), file_id); + + // 2. Check for collisions + if import_aliases.direct_targets.contains_key(&local_id) { + return Some(RichError::new( + Error::DuplicateAlias(local_name.clone()), + *use_decl.span(), + )); + } + + // 3. Find the true root using a single lookup! + // If `orig_id` exists in resolved_roots, it means it's an alias and we get its true root. + // If it returns None, it means `orig_id` is the original item, so it IS the root. + let true_root = import_aliases + .resolved_roots + .get(&orig_id) + .cloned() + .unwrap_or_else(|| orig_id.clone()); + + // 4. Update the registries + if alias.is_some() { + // Only update the chain if the user explicitly used the `as` keyword + import_aliases + .direct_targets + .insert(local_id.clone(), orig_id); + } + + // Always cache the final root for instant O(1) lookups later + import_aliases.resolved_roots.insert(local_id, true_root); + + // 5. Bind the result to the `identifier` variable + let identifier = local_name.clone(); + + // 6. Verify Existence: Does the item exist in the source file? let Some(resolution) = resolutions[ind].get(elem) else { return Some(RichError::new( - Error::UnresolvedItem(elem.as_inner().to_string()), + Error::UnresolvedItem(elem.clone()), *use_decl.span(), )); }; + // 7. Verify Visibility: Are we allowed to see it? if matches!(resolution.visibility, parse::Visibility::Private) { return Some(RichError::new( Error::PrivateItem(elem.as_inner().to_string()), @@ -505,8 +605,9 @@ impl ProjectGraph { )); } + // 8. Register the item in the local module's namespace resolutions[file_id].insert( - elem.clone(), + identifier, Resolution { visibility: use_decl.visibility().clone(), }, @@ -542,6 +643,7 @@ impl ProjectGraph { fn build_program(&self, order: &Vec, handler: &mut ErrorCollector) -> Option { let mut items: Vec = Vec::new(); let mut resolutions: Vec = vec![BTreeMap::new(); order.len()]; + let mut import_aliases = AliasRegistry::default(); for &file_id in order { let importer_source = self.modules[file_id].source.clone(); @@ -568,6 +670,7 @@ impl ProjectGraph { for target in use_targets { if let Some(err) = ProjectGraph::process_use_item( + &mut import_aliases, &mut resolutions, file_id, ind, @@ -614,6 +717,7 @@ impl ProjectGraph { Some(Program { items: items.into(), paths: self.paths.clone(), + import_aliases, resolutions: resolutions.into(), span: *self.modules[0].parsed_program.as_ref(), }) @@ -1199,4 +1303,189 @@ pub(crate) mod tests { error_msg ); } + + // Tests for aliases + // TODO: @LesterEvSe, @Sdoba16 add more tests for alias + #[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 id_a = *ids.get("A").unwrap(); + let id_root = *ids.get("main").unwrap(); + let order = vec![id_a, id_root]; + + let mut error_handler = ErrorCollector::new(); + let program = graph + .build_program(&order, &mut error_handler) + .expect("Failed to build program"); + let scope = &program.resolutions[id_root]; + + assert!( + scope.get(&Identifier::from("foo")).is_none(), + "Original name 'foo' should not be in scope" + ); + assert!( + scope.get(&Identifier::from("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 id_a = *ids.get("A").unwrap(); + let id_root = *ids.get("main").unwrap(); + let order = vec![id_a, id_root]; + + let mut error_handler = ErrorCollector::new(); + let program = graph + .build_program(&order, &mut error_handler) + .expect("Failed to build program"); + let scope = &program.resolutions[id_root]; + + // The original names should NOT be in scope + assert!(scope.get(&Identifier::from("foo")).is_none()); + assert!(scope.get(&Identifier::from("baz")).is_none()); + + // The aliases MUST be in scope + assert!(scope.get(&Identifier::from("bar")).is_some()); + assert!(scope.get(&Identifier::from("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 id_a = *ids.get("A").unwrap(); + let id_root = *ids.get("main").unwrap(); + let order = vec![id_a, id_root]; + + let mut error_handler = ErrorCollector::new(); + // This should NOT panic, but it should populate the error handler + graph.build_program(&order, &mut error_handler); + + assert!( + error_handler.has_errors(), + "Compiler should emit an error when aliasing a private item" + ); + + let error_msg = ErrorCollector::to_string(&error_handler); + assert!( + error_msg.contains("PrivateItem") || error_msg.contains("secret"), + "Error should mention the private item restriction" + ); + } + + #[test] + fn test_deep_reexport_with_aliases() { + // Scenario: Chaining aliases across multiple files. + // A.simf: pub fn original() {} + // B.simf: pub use lib::A::original as middle; + // main.simf: use lib::B::middle as final; + + let (graph, ids, _dir) = setup_graph(vec![ + ("libs/lib/A.simf", "pub fn original() {}"), + ("libs/lib/B.simf", "pub use lib::A::original as middle;"), + ("main.simf", "use lib::B::middle as final_name;"), + ]); + + let id_a = *ids.get("A").unwrap(); + let id_b = *ids.get("B").unwrap(); + let id_root = *ids.get("main").unwrap(); + // Crucial: The compiler must process A, then B, then Main! + let order = vec![id_a, id_b, id_root]; + + let mut error_handler = ErrorCollector::new(); + let program = graph + .build_program(&order, &mut error_handler) + .expect("Failed to build program"); + + // Assert Main Scope + let main_scope = &program.resolutions[id_root]; + assert!(main_scope.get(&Identifier::from("original")).is_none()); + assert!(main_scope.get(&Identifier::from("middle")).is_none()); + assert!( + main_scope.get(&Identifier::from("final_name")).is_some(), + "Main must see the final alias" + ); + + // Assert B Scope (It should have the intermediate alias!) + let b_scope = &program.resolutions[id_b]; + assert!( + b_scope.get(&Identifier::from("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 lib::A::target as hidden_alias;"), + ("main.simf", "use lib::B::hidden_alias;"), + ]); + + let id_a = *ids.get("A").unwrap(); + let id_b = *ids.get("B").unwrap(); + let id_root = *ids.get("main").unwrap(); + let order = vec![id_a, id_b, id_root]; + + let mut error_handler = ErrorCollector::new(); + graph.build_program(&order, &mut error_handler); + + assert!( + error_handler.has_errors(), + "Compiler must emit an error when trying to import a private alias from an intermediate module" + ); + + let error_msg = ErrorCollector::to_string(&error_handler); + assert!( + error_msg.contains("PrivateItem") || error_msg.contains("hidden_alias"), + "Error should correctly identify the private intermediate alias" + ); + } + + #[test] + fn test_alias_cycle_detection() { + // Scenario: A malicious or confused user creates an infinite alias loop. + let (graph, ids, _dir) = setup_graph(vec![ + // A imports from B, B imports from A. + ("libs/lib/A.simf", "pub use lib::B::pong as ping;"), + ("libs/lib/B.simf", "pub use lib::A::ping as pong;"), + ("main.simf", "use lib::A::ping;"), + ]); + + let id_a = *ids.get("A").unwrap(); + let id_b = *ids.get("B").unwrap(); + let id_root = *ids.get("main").unwrap(); + let order = vec![id_a, id_b, id_root]; + + let mut error_handler = ErrorCollector::new(); + graph.build_program(&order, &mut error_handler); + + // Driver should catch this and emit an UnresolvedItem or Cycle error, + // rather than causing a Stack Overflow! + assert!( + error_handler.has_errors(), + "Compiler must catch infinite alias cycles" + ); + } } diff --git a/src/error.rs b/src/error.rs index d5a2db6e..05b78d6c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -441,7 +441,7 @@ pub enum Error { JetDoesNotExist(JetName), InvalidCast(ResolvedType, ResolvedType), FileNotFound(PathBuf), - UnresolvedItem(String), + UnresolvedItem(Identifier), PrivateItem(String), MainNoInputs, MainNoOutput, @@ -458,6 +458,7 @@ pub enum Error { UndefinedVariable(Identifier), RedefinedAlias(AliasName), UndefinedAlias(AliasName), + DuplicateAlias(Identifier), VariableReuseInPattern(Identifier), WitnessReused(WitnessName), WitnessTypeMismatch(WitnessName, ResolvedType, ResolvedType), @@ -555,9 +556,9 @@ impl fmt::Display for Error { f, "Function `{name}` was called but not defined" ), - Error::UnresolvedItem(name) => write!( + Error::UnresolvedItem(identifier) => write!( f, - "Unknown item `{name}`" + "Unknown item `{identifier}`" ), Error::PrivateItem(name) => write!( f, @@ -603,6 +604,10 @@ impl fmt::Display for Error { f, "Type alias `{identifier}` is not defined" ), + Error::DuplicateAlias(identifier) => write!( + f, + "The alias `{identifier}` was defined multiple times" + ), Error::VariableReuseInPattern(identifier) => write!( f, "Variable `{identifier}` is used twice in the pattern" diff --git a/src/lexer.rs b/src/lexer.rs index 108c9c99..94a6fe78 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -9,6 +9,10 @@ pub type Spanned = (T, SimpleSpan); pub type Tokens<'src> = Vec<(Token<'src>, crate::error::Span)>; // Define your SSoT here +// TODO: @LesterEvSe or @Sdoba16 - Migrate to raw identifiers (`r#ident`) for forward compatibility. +// As the language grows, adding new keywords (e.g., `mut`) will break legacy code +// that uses those names for variables. Supporting `r#` escapes prevents this collision. +// BLOCKER: The Imports PR must not be merged until this feature is implemented pub const RESERVED_TOKENS: &[&str] = &[ "pub", "use", "as", "fn", "let", "type", "mod", "const", "match", "true", "false", "jet", "witness", "param", diff --git a/src/parse.rs b/src/parse.rs index 8a78b5e2..d1d70ec7 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -96,12 +96,13 @@ impl UseDecl { impl_eq_hash!(UseDecl; visibility, path, items); -// TODO: @LesterEvSe, Add aliases +pub type AliasedIdentifier = (Identifier, Option); + #[derive(Clone, Debug, PartialEq, Eq, Hash)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub enum UseItems { - Single(Identifier), - List(Vec), + Single(AliasedIdentifier), + List(Vec), } /// Definition of a function. @@ -692,16 +693,29 @@ impl fmt::Display for UseDecl { impl fmt::Display for UseItems { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - UseItems::Single(ident) => write!(f, "{}", ident), - UseItems::List(idents) => { + UseItems::Single((ident, alias)) => { + write!(f, "{};", ident)?; + + if let Some(alias) = alias { + write!(f, " as {}", alias)?; + } + + write!(f, ";") + } + UseItems::List(aliased_idents) => { let _ = write!(f, "{{"); - for (i, ident) in idents.iter().enumerate() { + for (i, (ident, alias)) in aliased_idents.iter().enumerate() { if i > 0 { write!(f, ", ")?; } + write!(f, "{}", ident)?; + + if let Some(alias) = alias { + write!(f, " as {}", alias)? + } } - write!(f, "}}") + write!(f, "}};") } } } @@ -1377,13 +1391,19 @@ impl ChumskyParse for UseDecl { .at_least(1) .collect::>(); - let list = Identifier::parser() + let aliased_item = Identifier::parser().then( + just(Token::As).ignore_then(Identifier::parser()).or_not(), // Returns None if 'as' missing' + ); + + let list = aliased_item + .clone() .separated_by(just(Token::Comma)) .allow_trailing() .collect() .delimited_by(just(Token::LBrace), just(Token::RBrace)) .map(UseItems::List); - let single = Identifier::parser().map(UseItems::Single); + + let single = aliased_item.map(UseItems::Single); let items = choice((list, single)); visibility From f9db632b44cefaffa42ab6006db957b3a90a4398 Mon Sep 17 00:00:00 2001 From: Sdoba16 Date: Tue, 10 Mar 2026 09:53:12 +0200 Subject: [PATCH 11/13] Add test, bug fix, refactoring --- Cargo.lock | 526 ++++++------------ .../error-test-cases/cross-wire/lib/b.simf | 2 + .../error-test-cases/cross-wire/lib/c.simf | 2 + .../error-test-cases/cross-wire/lib/x.simf | 1 + .../error-test-cases/cross-wire/lib/y.simf | 1 + .../error-test-cases/cross-wire/main.simf | 4 + .../cyclic-dependency/lib/module_a.simf | 2 + .../cyclic-dependency/lib/module_b.simf | 2 + .../cyclic-dependency/main.simf | 3 + .../error-test-cases/file-not-found/main.simf | 5 + .../error-test-cases/global/lib/module.simf | 2 + .../error-test-cases/global/main.simf | 5 + .../error-test-cases/lib-not-found/main.simf | 5 + .../private-visibility/lib/hidden.simf | 1 + .../private-visibility/main.simf | 3 + .../deep-reexport-chain/lib/level1.simf | 2 + .../deep-reexport-chain/lib/level2.simf | 2 + .../deep-reexport-chain/lib/level3.simf | 2 + .../deep-reexport-chain/main.simf | 7 + .../lib/base.simf | 1 + .../lib/left.simf | 2 + .../lib/right.simf | 2 + .../diamond-dependency-resolution/main.simf | 9 + .../interleaved-waterfall/auth/verify.simf | 6 + .../interleaved-waterfall/db/store.simf | 2 + .../interleaved-waterfall/main.simf | 5 + .../interleaved-waterfall/orch/handler.simf | 8 + .../interleaved-waterfall/types/def.simf | 1 + .../leaky-signature/lib/internal.simf | 5 + .../leaky-signature/main.simf | 4 + .../module-simple/lib/module.simf | 1 + .../valid-test-cases/module-simple/main.simf | 2 + .../multi-lib-facade/api/api.simf | 3 + .../multi-lib-facade/crypto/crypto.simf | 5 + .../multi-lib-facade/main.simf | 8 + .../multi-lib-facade/math/math.simf | 4 + .../reexport-diamond/lib/core.simf | 2 + .../reexport-diamond/lib/route_a.simf | 2 + .../reexport-diamond/lib/route_b.simf | 5 + .../reexport-diamond/main.simf | 9 + src/ast.rs | 8 +- src/driver.rs | 68 ++- src/error.rs | 23 +- src/lib.rs | 241 ++++---- 44 files changed, 529 insertions(+), 474 deletions(-) create mode 100644 functional-tests/error-test-cases/cross-wire/lib/b.simf create mode 100644 functional-tests/error-test-cases/cross-wire/lib/c.simf create mode 100644 functional-tests/error-test-cases/cross-wire/lib/x.simf create mode 100644 functional-tests/error-test-cases/cross-wire/lib/y.simf create mode 100644 functional-tests/error-test-cases/cross-wire/main.simf create mode 100644 functional-tests/error-test-cases/cyclic-dependency/lib/module_a.simf create mode 100644 functional-tests/error-test-cases/cyclic-dependency/lib/module_b.simf create mode 100644 functional-tests/error-test-cases/cyclic-dependency/main.simf create mode 100644 functional-tests/error-test-cases/file-not-found/main.simf create mode 100644 functional-tests/error-test-cases/global/lib/module.simf create mode 100644 functional-tests/error-test-cases/global/main.simf create mode 100644 functional-tests/error-test-cases/lib-not-found/main.simf create mode 100644 functional-tests/error-test-cases/private-visibility/lib/hidden.simf create mode 100644 functional-tests/error-test-cases/private-visibility/main.simf create mode 100644 functional-tests/valid-test-cases/deep-reexport-chain/lib/level1.simf create mode 100644 functional-tests/valid-test-cases/deep-reexport-chain/lib/level2.simf create mode 100644 functional-tests/valid-test-cases/deep-reexport-chain/lib/level3.simf create mode 100644 functional-tests/valid-test-cases/deep-reexport-chain/main.simf create mode 100644 functional-tests/valid-test-cases/diamond-dependency-resolution/lib/base.simf create mode 100644 functional-tests/valid-test-cases/diamond-dependency-resolution/lib/left.simf create mode 100644 functional-tests/valid-test-cases/diamond-dependency-resolution/lib/right.simf create mode 100644 functional-tests/valid-test-cases/diamond-dependency-resolution/main.simf create mode 100644 functional-tests/valid-test-cases/interleaved-waterfall/auth/verify.simf create mode 100644 functional-tests/valid-test-cases/interleaved-waterfall/db/store.simf create mode 100644 functional-tests/valid-test-cases/interleaved-waterfall/main.simf create mode 100644 functional-tests/valid-test-cases/interleaved-waterfall/orch/handler.simf create mode 100644 functional-tests/valid-test-cases/interleaved-waterfall/types/def.simf create mode 100644 functional-tests/valid-test-cases/leaky-signature/lib/internal.simf create mode 100644 functional-tests/valid-test-cases/leaky-signature/main.simf create mode 100644 functional-tests/valid-test-cases/module-simple/lib/module.simf create mode 100644 functional-tests/valid-test-cases/module-simple/main.simf create mode 100644 functional-tests/valid-test-cases/multi-lib-facade/api/api.simf create mode 100644 functional-tests/valid-test-cases/multi-lib-facade/crypto/crypto.simf create mode 100644 functional-tests/valid-test-cases/multi-lib-facade/main.simf create mode 100644 functional-tests/valid-test-cases/multi-lib-facade/math/math.simf create mode 100644 functional-tests/valid-test-cases/reexport-diamond/lib/core.simf create mode 100644 functional-tests/valid-test-cases/reexport-diamond/lib/route_a.simf create mode 100644 functional-tests/valid-test-cases/reexport-diamond/lib/route_b.simf create mode 100644 functional-tests/valid-test-cases/reexport-diamond/main.simf diff --git a/Cargo.lock b/Cargo.lock index 8e622e6d..83a2fd98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "aho-corasick" -version = "1.0.5" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -19,9 +19,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -34,59 +34,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "once_cell", - "windows-sys", -] - -[[package]] -name = "anyhow" -version = "1.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" - -[[package]] -name = "ar_archive_writer" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" -dependencies = [ - "object", + "once_cell_polyfill", + "windows-sys 0.61.2", ] [[package]] name = "arbitrary" -version = "1.1.3" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a7924531f38b1970ff630f03eb20a2fde69db5c590c93b0f3482e95dcc5fd60" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] @@ -109,9 +94,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.3" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" @@ -121,15 +106,15 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bech32" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" [[package]] name = "bitcoin" -version = "0.32.3" +version = "0.32.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0032b0e8ead7074cda7fc4f034409607e3f03a6f71d66ade8a307f79b4d99e73" +checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66" dependencies = [ "base58ck", "bech32", @@ -150,9 +135,9 @@ checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" [[package]] name = "bitcoin-io" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "340e09e8399c7bd8912f495af6aa58bea0c9214773417ffaa8f6460f93aaee56" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" [[package]] name = "bitcoin-private" @@ -171,9 +156,9 @@ dependencies = [ [[package]] name = "bitcoin_hashes" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" dependencies = [ "bitcoin-io", "hex-conservative", @@ -181,27 +166,27 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.2.55" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "jobserver", @@ -211,9 +196,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chumsky" @@ -221,8 +206,8 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acc17a6284abccac6e50db35c1cee87f605474a72939b959a3a67d9371800efd" dependencies = [ - "hashbrown 0.15.5", - "regex-automata", + "hashbrown", + "regex-automata 0.3.9", "serde", "stacker", "unicode-ident", @@ -231,18 +216,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.37" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.37" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -252,9 +237,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "codegen" @@ -265,32 +250,32 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "derive_arbitrary" -version = "1.1.3" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9a577516173adb681466d517d39bd468293bc2c2a16439375ef0f35bba45f3d" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "elements" -version = "0.25.0" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739a0201c8b2d1e35e6509872ddb8250dd37b38d2a462b9cea05988bf9630196" +checksum = "81b2569d3495bfdfce36c504fd4d78752ff4a7699f8a33e6f3ee523bddf9f6ad" dependencies = [ "bech32", "bitcoin", @@ -310,7 +295,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -333,9 +318,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -346,15 +331,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", "wasip2", - "wasip3", ] [[package]] @@ -374,23 +358,11 @@ dependencies = [ "foldhash", ] -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - [[package]] name = "hex-conservative" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" dependencies = [ "arrayvec", ] @@ -401,29 +373,11 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -436,45 +390,41 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ + "once_cell", "wasm-bindgen", ] -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" -version = "0.2.180" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libfuzzer-sys" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" dependencies = [ "arbitrary", "cc", @@ -482,41 +432,26 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "log" -version = "0.4.22" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "memchr" -version = "2.6.3" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "miniscript" -version = "12.3.1" +version = "12.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82911d2fb527bb9aacd2446d2f517aff3f8e3846ace1b3c24258b61ea3cce2bc" +checksum = "487906208f38448e186e3deb02f2b8ef046a9078b0de00bdb28bf4fb9b76951c" dependencies = [ "bech32", "bitcoin", ] -[[package]] -name = "object" -version = "0.32.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -524,19 +459,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "ppv-lite86" -version = "0.2.17" +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "prettyplease" -version = "0.2.37" +name = "ppv-lite86" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "proc-macro2", - "syn 2.0.114", + "zerocopy", ] [[package]] @@ -550,19 +484,18 @@ dependencies = [ [[package]] name = "psm" -version = "0.1.28" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" +checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" dependencies = [ - "ar_archive_writer", "cc", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -600,30 +533,41 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.17", ] [[package]] name = "regex" -version = "1.9.5" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.14", + "regex-syntax 0.8.10", +] + +[[package]] +name = "regex-automata" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-syntax 0.7.5", ] [[package]] name = "regex-automata" -version = "0.3.8" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.10", ] [[package]] @@ -632,24 +576,30 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] -name = "ryu" -version = "1.0.15" +name = "rustversion" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "santiago" @@ -702,12 +652,6 @@ dependencies = [ "secp256k1-sys", ] -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - [[package]] name = "serde" version = "1.0.228" @@ -735,18 +679,20 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", - "ryu", + "memchr", "serde", + "serde_core", + "zmij", ] [[package]] @@ -765,7 +711,7 @@ dependencies = [ "bitcoin_hashes", "byteorder", "elements", - "getrandom 0.2.10", + "getrandom 0.2.17", "ghost-cell", "hex-conservative", "miniscript", @@ -775,9 +721,9 @@ dependencies = [ [[package]] name = "simplicity-sys" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875630d128f19818161cefe0a3d910b6aae921d8246711db574a689cb2c11747" +checksum = "e3401ee7331f183a5458c0f5a4b3d5d00bde0fd12e2e03728c537df34efae289" dependencies = [ "bitcoin_hashes", "cc", @@ -788,11 +734,11 @@ name = "simplicityhl" version = "0.4.1" dependencies = [ "arbitrary", - "base64 0.21.3", + "base64 0.21.7", "chumsky", "clap", "either", - "getrandom 0.2.10", + "getrandom 0.2.17", "itertools", "miniscript", "serde", @@ -815,15 +761,15 @@ dependencies = [ [[package]] name = "stacker" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" +checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" dependencies = [ "cc", "cfg-if", "libc", "psm", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -834,20 +780,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -856,22 +791,22 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.25.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" @@ -879,12 +814,6 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "utf8parse" version = "0.2.2" @@ -893,9 +822,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" @@ -906,45 +835,24 @@ dependencies = [ "wit-bindgen", ] -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", -] - [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" -dependencies = [ - "bumpalo", - "log", "once_cell", - "proc-macro2", - "quote", - "syn 2.0.114", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -952,64 +860,48 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.114", - "wasm-bindgen-backend", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" - -[[package]] -name = "wasm-encoder" -version = "0.244.0" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ - "leb128fmt", - "wasmparser", + "unicode-ident", ] [[package]] -name = "wasm-metadata" -version = "0.244.0" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "wasmparser" -version = "0.244.0" +name = "windows-sys" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", + "windows-targets", ] [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-targets", + "windows-link", ] [[package]] @@ -1081,85 +973,29 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] [[package]] -name = "wit-bindgen-rust" -version = "0.51.0" +name = "zerocopy" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn 2.0.114", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", + "zerocopy-derive", ] [[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" +name = "zerocopy-derive" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" dependencies = [ - "anyhow", - "prettyplease", "proc-macro2", "quote", - "syn 2.0.114", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", + "syn", ] [[package]] -name = "wit-parser" -version = "0.244.0" +name = "zmij" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/functional-tests/error-test-cases/cross-wire/lib/b.simf b/functional-tests/error-test-cases/cross-wire/lib/b.simf new file mode 100644 index 00000000..be3150c7 --- /dev/null +++ b/functional-tests/error-test-cases/cross-wire/lib/b.simf @@ -0,0 +1,2 @@ +use lib::x::TypeX; +use lib::y::TypeY; \ No newline at end of file diff --git a/functional-tests/error-test-cases/cross-wire/lib/c.simf b/functional-tests/error-test-cases/cross-wire/lib/c.simf new file mode 100644 index 00000000..6efaf9a9 --- /dev/null +++ b/functional-tests/error-test-cases/cross-wire/lib/c.simf @@ -0,0 +1,2 @@ +use lib::y::TypeY; +use lib::x::TypeX; \ No newline at end of file diff --git a/functional-tests/error-test-cases/cross-wire/lib/x.simf b/functional-tests/error-test-cases/cross-wire/lib/x.simf new file mode 100644 index 00000000..8678e1df --- /dev/null +++ b/functional-tests/error-test-cases/cross-wire/lib/x.simf @@ -0,0 +1 @@ +pub type TypeX = u32; \ No newline at end of file diff --git a/functional-tests/error-test-cases/cross-wire/lib/y.simf b/functional-tests/error-test-cases/cross-wire/lib/y.simf new file mode 100644 index 00000000..5c4c8091 --- /dev/null +++ b/functional-tests/error-test-cases/cross-wire/lib/y.simf @@ -0,0 +1 @@ +pub type TypeY = u64; \ No newline at end of file diff --git a/functional-tests/error-test-cases/cross-wire/main.simf b/functional-tests/error-test-cases/cross-wire/main.simf new file mode 100644 index 00000000..afddec8c --- /dev/null +++ b/functional-tests/error-test-cases/cross-wire/main.simf @@ -0,0 +1,4 @@ +use lib::b::TypeX; +use lib::c::TypeY; + +fn main() {} \ No newline at end of file diff --git a/functional-tests/error-test-cases/cyclic-dependency/lib/module_a.simf b/functional-tests/error-test-cases/cyclic-dependency/lib/module_a.simf new file mode 100644 index 00000000..1d07a968 --- /dev/null +++ b/functional-tests/error-test-cases/cyclic-dependency/lib/module_a.simf @@ -0,0 +1,2 @@ +pub use lib::module_b::TypeB; +pub type TypeA = u32; diff --git a/functional-tests/error-test-cases/cyclic-dependency/lib/module_b.simf b/functional-tests/error-test-cases/cyclic-dependency/lib/module_b.simf new file mode 100644 index 00000000..2b340054 --- /dev/null +++ b/functional-tests/error-test-cases/cyclic-dependency/lib/module_b.simf @@ -0,0 +1,2 @@ +pub use lib::module_a::TypeA; +pub type TypeB = u32; diff --git a/functional-tests/error-test-cases/cyclic-dependency/main.simf b/functional-tests/error-test-cases/cyclic-dependency/main.simf new file mode 100644 index 00000000..338a4030 --- /dev/null +++ b/functional-tests/error-test-cases/cyclic-dependency/main.simf @@ -0,0 +1,3 @@ +use lib::module_a::TypeA; + +fn main() {} diff --git a/functional-tests/error-test-cases/file-not-found/main.simf b/functional-tests/error-test-cases/file-not-found/main.simf new file mode 100644 index 00000000..2d016d20 --- /dev/null +++ b/functional-tests/error-test-cases/file-not-found/main.simf @@ -0,0 +1,5 @@ +use lib::module::AssetId; + +fn main() { + let my_asset: AssetId = 5; +} \ No newline at end of file diff --git a/functional-tests/error-test-cases/global/lib/module.simf b/functional-tests/error-test-cases/global/lib/module.simf new file mode 100644 index 00000000..d87a0da8 --- /dev/null +++ b/functional-tests/error-test-cases/global/lib/module.simf @@ -0,0 +1,2 @@ +pub type AssetId = u32; +pub fn get_id() -> AssetId { 1 } \ No newline at end of file diff --git a/functional-tests/error-test-cases/global/main.simf b/functional-tests/error-test-cases/global/main.simf new file mode 100644 index 00000000..f0fb986c --- /dev/null +++ b/functional-tests/error-test-cases/global/main.simf @@ -0,0 +1,5 @@ +use lib::module::*; + +fn main() { + let my_asset: AssetId = 5; +} \ No newline at end of file diff --git a/functional-tests/error-test-cases/lib-not-found/main.simf b/functional-tests/error-test-cases/lib-not-found/main.simf new file mode 100644 index 00000000..2d016d20 --- /dev/null +++ b/functional-tests/error-test-cases/lib-not-found/main.simf @@ -0,0 +1,5 @@ +use lib::module::AssetId; + +fn main() { + let my_asset: AssetId = 5; +} \ No newline at end of file diff --git a/functional-tests/error-test-cases/private-visibility/lib/hidden.simf b/functional-tests/error-test-cases/private-visibility/lib/hidden.simf new file mode 100644 index 00000000..70da82d4 --- /dev/null +++ b/functional-tests/error-test-cases/private-visibility/lib/hidden.simf @@ -0,0 +1 @@ +type SecretType = u32; pub fn ok() {} \ No newline at end of file diff --git a/functional-tests/error-test-cases/private-visibility/main.simf b/functional-tests/error-test-cases/private-visibility/main.simf new file mode 100644 index 00000000..e1eb9dd8 --- /dev/null +++ b/functional-tests/error-test-cases/private-visibility/main.simf @@ -0,0 +1,3 @@ +use lib::hidden::SecretType; + +fn main() {} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/deep-reexport-chain/lib/level1.simf b/functional-tests/valid-test-cases/deep-reexport-chain/lib/level1.simf new file mode 100644 index 00000000..60a6f1d4 --- /dev/null +++ b/functional-tests/valid-test-cases/deep-reexport-chain/lib/level1.simf @@ -0,0 +1,2 @@ +pub use lib::level2::CoreSmth; +pub use lib::level2::core_val; \ No newline at end of file diff --git a/functional-tests/valid-test-cases/deep-reexport-chain/lib/level2.simf b/functional-tests/valid-test-cases/deep-reexport-chain/lib/level2.simf new file mode 100644 index 00000000..7a32e9a0 --- /dev/null +++ b/functional-tests/valid-test-cases/deep-reexport-chain/lib/level2.simf @@ -0,0 +1,2 @@ +pub use lib::level3::CoreSmth; +pub use lib::level3::core_val; \ No newline at end of file diff --git a/functional-tests/valid-test-cases/deep-reexport-chain/lib/level3.simf b/functional-tests/valid-test-cases/deep-reexport-chain/lib/level3.simf new file mode 100644 index 00000000..6f0b9983 --- /dev/null +++ b/functional-tests/valid-test-cases/deep-reexport-chain/lib/level3.simf @@ -0,0 +1,2 @@ +pub type CoreSmth = u32; +pub fn core_val() -> CoreSmth { 42 } \ No newline at end of file diff --git a/functional-tests/valid-test-cases/deep-reexport-chain/main.simf b/functional-tests/valid-test-cases/deep-reexport-chain/main.simf new file mode 100644 index 00000000..bd15f456 --- /dev/null +++ b/functional-tests/valid-test-cases/deep-reexport-chain/main.simf @@ -0,0 +1,7 @@ +use lib::level1::CoreSmth; +use lib::level1::core_val; + +fn main() { + let val: CoreSmth = core_val(); + assert!(jet::eq_32(val, 42)); +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/base.simf b/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/base.simf new file mode 100644 index 00000000..ad05850a --- /dev/null +++ b/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/base.simf @@ -0,0 +1 @@ +pub type BaseType = u32; \ No newline at end of file diff --git a/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/left.simf b/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/left.simf new file mode 100644 index 00000000..b1bd0652 --- /dev/null +++ b/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/left.simf @@ -0,0 +1,2 @@ +pub use lib::base::BaseType; +pub fn get_left() -> BaseType { 1 } \ No newline at end of file diff --git a/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/right.simf b/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/right.simf new file mode 100644 index 00000000..629deab8 --- /dev/null +++ b/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/right.simf @@ -0,0 +1,2 @@ +pub use lib::base::BaseType; +pub fn get_right() -> BaseType { 2 } \ No newline at end of file diff --git a/functional-tests/valid-test-cases/diamond-dependency-resolution/main.simf b/functional-tests/valid-test-cases/diamond-dependency-resolution/main.simf new file mode 100644 index 00000000..cef4de5c --- /dev/null +++ b/functional-tests/valid-test-cases/diamond-dependency-resolution/main.simf @@ -0,0 +1,9 @@ +use lib::left::get_left; +use lib::right::get_right; + +fn main() { + let a: BaseType = get_left(); + let b: BaseType = get_right(); + let (_, c): (bool, BaseType) = jet::add_32(a, b); + assert!(jet::eq_32(c, 3)); +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/interleaved-waterfall/auth/verify.simf b/functional-tests/valid-test-cases/interleaved-waterfall/auth/verify.simf new file mode 100644 index 00000000..5b33d6b9 --- /dev/null +++ b/functional-tests/valid-test-cases/interleaved-waterfall/auth/verify.simf @@ -0,0 +1,6 @@ +use types::def::UserId; +use db::store::get_record; + +pub fn is_valid(id: UserId) -> UserId { + get_record(id) +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/interleaved-waterfall/db/store.simf b/functional-tests/valid-test-cases/interleaved-waterfall/db/store.simf new file mode 100644 index 00000000..20a6cc61 --- /dev/null +++ b/functional-tests/valid-test-cases/interleaved-waterfall/db/store.simf @@ -0,0 +1,2 @@ +use types::def::UserId; +pub fn get_record(id: UserId) -> UserId { id } \ No newline at end of file diff --git a/functional-tests/valid-test-cases/interleaved-waterfall/main.simf b/functional-tests/valid-test-cases/interleaved-waterfall/main.simf new file mode 100644 index 00000000..0abb7218 --- /dev/null +++ b/functional-tests/valid-test-cases/interleaved-waterfall/main.simf @@ -0,0 +1,5 @@ +use orch::handler::run_system; + +fn main() { + assert!(jet::eq_32(run_system(5), 5)); +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/interleaved-waterfall/orch/handler.simf b/functional-tests/valid-test-cases/interleaved-waterfall/orch/handler.simf new file mode 100644 index 00000000..86d627d6 --- /dev/null +++ b/functional-tests/valid-test-cases/interleaved-waterfall/orch/handler.simf @@ -0,0 +1,8 @@ +use auth::verify::is_valid; +use db::store::get_record; +use types::def::UserId; + +pub fn run_system(id: UserId) -> UserId { + let checked: UserId = is_valid(id); + get_record(checked) +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/interleaved-waterfall/types/def.simf b/functional-tests/valid-test-cases/interleaved-waterfall/types/def.simf new file mode 100644 index 00000000..a8fd5650 --- /dev/null +++ b/functional-tests/valid-test-cases/interleaved-waterfall/types/def.simf @@ -0,0 +1 @@ +pub type UserId = u32; \ No newline at end of file diff --git a/functional-tests/valid-test-cases/leaky-signature/lib/internal.simf b/functional-tests/valid-test-cases/leaky-signature/lib/internal.simf new file mode 100644 index 00000000..2f11c01e --- /dev/null +++ b/functional-tests/valid-test-cases/leaky-signature/lib/internal.simf @@ -0,0 +1,5 @@ +type SecretKey = u64; + +pub fn unlock(key: SecretKey) -> u64 { + jet::max_64(key, 0) +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/leaky-signature/main.simf b/functional-tests/valid-test-cases/leaky-signature/main.simf new file mode 100644 index 00000000..77592ac0 --- /dev/null +++ b/functional-tests/valid-test-cases/leaky-signature/main.simf @@ -0,0 +1,4 @@ +use lib::internal::unlock; + +fn main() { +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/module-simple/lib/module.simf b/functional-tests/valid-test-cases/module-simple/lib/module.simf new file mode 100644 index 00000000..d5cfec25 --- /dev/null +++ b/functional-tests/valid-test-cases/module-simple/lib/module.simf @@ -0,0 +1 @@ +pub fn add() {} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/module-simple/main.simf b/functional-tests/valid-test-cases/module-simple/main.simf new file mode 100644 index 00000000..bb2705df --- /dev/null +++ b/functional-tests/valid-test-cases/module-simple/main.simf @@ -0,0 +1,2 @@ +use lib::module::add; +fn main() {} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/multi-lib-facade/api/api.simf b/functional-tests/valid-test-cases/multi-lib-facade/api/api.simf new file mode 100644 index 00000000..d80ca18a --- /dev/null +++ b/functional-tests/valid-test-cases/multi-lib-facade/api/api.simf @@ -0,0 +1,3 @@ +pub use crypto::crypto::mock_hash; +pub use math::math::MathInt; +pub use math::math::add_two; \ No newline at end of file diff --git a/functional-tests/valid-test-cases/multi-lib-facade/crypto/crypto.simf b/functional-tests/valid-test-cases/multi-lib-facade/crypto/crypto.simf new file mode 100644 index 00000000..51111aca --- /dev/null +++ b/functional-tests/valid-test-cases/multi-lib-facade/crypto/crypto.simf @@ -0,0 +1,5 @@ +use math::math::MathInt; + +pub fn mock_hash(x: MathInt) -> (bool, MathInt) { + jet::add_32(x, 5) +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/multi-lib-facade/main.simf b/functional-tests/valid-test-cases/multi-lib-facade/main.simf new file mode 100644 index 00000000..44ff5c3c --- /dev/null +++ b/functional-tests/valid-test-cases/multi-lib-facade/main.simf @@ -0,0 +1,8 @@ +use api::api::{add_two, mock_hash, MathInt}; + +fn main() { + let val: MathInt = 10; + let (_, step1): (bool, MathInt) = add_two(val); + let (_, step2): (bool, MathInt) = mock_hash(step1); + assert!(jet::eq_32(step2, 17)); +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/multi-lib-facade/math/math.simf b/functional-tests/valid-test-cases/multi-lib-facade/math/math.simf new file mode 100644 index 00000000..d8d16499 --- /dev/null +++ b/functional-tests/valid-test-cases/multi-lib-facade/math/math.simf @@ -0,0 +1,4 @@ +pub type MathInt = u32; +pub fn add_two(x: MathInt) -> (bool, MathInt) { + jet::add_32(x, 2) +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/reexport-diamond/lib/core.simf b/functional-tests/valid-test-cases/reexport-diamond/lib/core.simf new file mode 100644 index 00000000..71526bed --- /dev/null +++ b/functional-tests/valid-test-cases/reexport-diamond/lib/core.simf @@ -0,0 +1,2 @@ +pub type Coin = u64; +pub fn mint(val: u64) -> Coin { val } \ No newline at end of file diff --git a/functional-tests/valid-test-cases/reexport-diamond/lib/route_a.simf b/functional-tests/valid-test-cases/reexport-diamond/lib/route_a.simf new file mode 100644 index 00000000..7571e9e3 --- /dev/null +++ b/functional-tests/valid-test-cases/reexport-diamond/lib/route_a.simf @@ -0,0 +1,2 @@ +pub use lib::core::Coin; +pub use lib::core::mint; \ No newline at end of file diff --git a/functional-tests/valid-test-cases/reexport-diamond/lib/route_b.simf b/functional-tests/valid-test-cases/reexport-diamond/lib/route_b.simf new file mode 100644 index 00000000..c7f4b74b --- /dev/null +++ b/functional-tests/valid-test-cases/reexport-diamond/lib/route_b.simf @@ -0,0 +1,5 @@ +pub use lib::core::Coin; + +pub fn burn(c: Coin) -> (bool, Coin) { + jet::subtract_64(c, 1) +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/reexport-diamond/main.simf b/functional-tests/valid-test-cases/reexport-diamond/main.simf new file mode 100644 index 00000000..799b96e6 --- /dev/null +++ b/functional-tests/valid-test-cases/reexport-diamond/main.simf @@ -0,0 +1,9 @@ +use lib::route_a::Coin; +use lib::route_a::mint; +use lib::route_b::burn; + +fn main() { + let my_coin: Coin = mint(10); + let (_, remaining): (bool, Coin) = burn(my_coin); + assert!(jet::eq_64(remaining, 9)); +} \ No newline at end of file diff --git a/src/ast.rs b/src/ast.rs index b78a2a91..6b0ade46 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1543,14 +1543,18 @@ impl AbstractSyntaxTree for Match { } } +/// Analyze a parsed module program to extract assignments for a specific module. +/// +/// This function searches the parsed program for a module matching the given `name`. +/// If found, it evaluates the module's assignments and returns them as a map of +/// witness names to their constant values. If the module is not present, an empty +/// map is returned. fn analyze_named_module( name: ModuleName, from: &parse::ModuleProgram, ) -> Result, RichError> { let unit = ResolvedType::unit(); - // IMPORTANT! If modules allow imports, then we need to consider - // passing the resolution conetxt by calling `Scope::new(resolutions)` let mut scope = Scope::default(); let items = from .items() diff --git a/src/driver.rs b/src/driver.rs index 6b2d2efb..8bd2186a 100644 --- a/src/driver.rs +++ b/src/driver.rs @@ -331,7 +331,17 @@ impl_eq_hash!(TypeAlias; name, ty); #[derive(Debug)] pub enum C3Error { CycleDetected(Vec), - InconsistentLinearization { module: String }, + /// Error for inconsistent MRO. + /// This can happen if the dependency graph has a shape that makes the + /// order of parent classes ambiguous. + /// Example: A depends on B and C, and B also depends on C. + /// The linearization of A is A + merge(linearization(B), linearization(C), [B, C]). + /// If B appears before C in one parent's linearization but C appears before B + /// in another's, the merge will fail. + InconsistentLinearization { + module: String, + conflicts: Vec>, + }, } impl fmt::Display for C3Error { @@ -340,8 +350,22 @@ impl fmt::Display for C3Error { C3Error::CycleDetected(cycle) => { write!(f, "Circular dependency detected: {:?}", cycle.join(" -> ")) } - C3Error::InconsistentLinearization { module } => { - write!(f, "Inconsistent resolution order for module '{:?}'", module) + C3Error::InconsistentLinearization { module, conflicts } => { + writeln!(f, "Inconsistent resolution order for module '{}'", module)?; + writeln!( + f, + "The compiler could not resolve the following conflicting import constraints:" + )?; + + // Loop through the matrix and print each conflicting sequence + for conflict in conflicts { + writeln!(f, " [{}]", conflict.join(", "))?; + } + + write!( + f, + "Try reordering your `use` statements to avoid cross-wiring." + ) } } } @@ -507,12 +531,26 @@ impl ProjectGraph { seqs.push(line); } - seqs.push(parents.clone()); - let mut result = vec![module]; - let merged = merge(seqs).ok_or(C3Error::InconsistentLinearization { - module: self.modules[module].source.name().to_string(), - })?; + let merged = match merge(seqs) { + Ok(m) => m, + Err(conflicts) => { + // Map the failing usize sequences into readable module names + let conflict_names: Vec> = conflicts + .into_iter() + .map(|seq| { + seq.into_iter() + .map(|id| self.modules[id].source.name().to_string()) + .collect() + }) + .collect(); + + return Err(C3Error::InconsistentLinearization { + module: self.modules[module].source.name().to_string(), + conflicts: conflict_names, + }); + } + }; result.extend(merged); @@ -738,13 +776,18 @@ impl ProjectGraph { } } -fn merge(mut seqs: Vec>) -> Option> { +/// C3 Merge Algorithm +/// +/// Merges a list of sequences (parent linearizations) into a single sequence. +/// The algorithm ensures that the local precedence order of each sequence is preserved. +// Change the return type to Result +fn merge(mut seqs: Vec>) -> Result, Vec>> { let mut result = Vec::new(); loop { seqs.retain(|s| !s.is_empty()); if seqs.is_empty() { - return Some(result); + return Ok(result); } let mut candidate = None; @@ -758,7 +801,9 @@ fn merge(mut seqs: Vec>) -> Option> { } } - let head = candidate?; + let Some(head) = candidate else { + return Err(seqs); + }; result.push(head); @@ -916,7 +961,6 @@ pub(crate) mod tests { } // Helper to mock the initial root program parsing - // (Assuming your parser works via a helper function) fn parse_root(path: &Path) -> (parse::Program, SourceFile) { // 1. Read file let content = std::fs::read_to_string(path).expect("Failed to read root file for parsing"); diff --git a/src/error.rs b/src/error.rs index 05b78d6c..c901d4f6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -443,6 +443,7 @@ pub enum Error { FileNotFound(PathBuf), UnresolvedItem(Identifier), PrivateItem(String), + NameCollision(String), MainNoInputs, MainNoOutput, MainRequired, @@ -564,6 +565,10 @@ impl fmt::Display for Error { f, "Item `{name}` is private" ), + Error::NameCollision(name) => write!( + f, + "The name `{name}` is defined multiple times in this scope" + ), Error::InvalidNumberOfArguments(expected, found) => write!( f, "Expected {expected} arguments, found {found} arguments" @@ -639,7 +644,7 @@ impl fmt::Display for Error { Error::ArgumentTypeMismatch(name, declared, assigned) => write!( f, "Parameter `{name}` was declared with type `{declared}` but its assigned argument is of type `{assigned}`" - ), + ) } } } @@ -772,4 +777,20 @@ let x: u32 = Left( let expected = "Cannot parse: This error has an empty file"; assert_eq!(&expected, &error.to_string()); } + + #[test] + fn display_private_item() { + let source = SourceFile::new(SourceName::Virtual(Arc::from(FILENAME)), Arc::from(FILE)); + let error = Error::PrivateItem("SecretType".to_string()) + .with_span(Span::new(8, 20)) + .with_source(source); + let expected = format!( + r#" + --> {FILENAME}:1:9 + | +1 | let a1: List = None; + | ^^^^^^^^^^^^ Item `SecretType` is private"# + ); + assert_eq!(&expected[1..], &error.to_string()); + } } diff --git a/src/lib.rs b/src/lib.rs index c90d09be..972361a6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -685,125 +685,69 @@ pub(crate) mod tests { } } + 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() { - let (test, _dir) = TestCase::temp_env( - "use temp::math::add; fn main() {}", - vec![("temp", "temp/math.simf", "pub fn add() {}")], - ); - - test.with_witness_values(WitnessValues::default()) - .assert_run_success(); + TestCase::program_file_with_libs( + format!("{}/module-simple/main.simf", VALID_TESTS_DIR), + [("lib", format!("{}/module-simple/lib", VALID_TESTS_DIR))], + ) + .with_witness_values(WitnessValues::default()) + .assert_run_success(); } #[test] fn diamond_dependency_resolution() { - let main_code = r#" - use temp::left::get_left; - use temp::right::get_right; - - fn main() { - let a: BaseType = get_left(); - let b: BaseType = get_right(); - let (_, c): (bool, BaseType) = jet::add_32(a, b); - assert!(jet::eq_32(c, 3)); - } - "#; - - let libs = vec![ - ("temp", "temp/base.simf", "pub type BaseType = u32;"), - ( - "temp", - "temp/left.simf", - "pub use temp::base::BaseType; pub fn get_left() -> BaseType { 1 }", - ), - ( - "temp", - "temp/right.simf", - "pub use temp::base::BaseType; pub fn get_right() -> BaseType { 2 }", + TestCase::program_file_with_libs( + format!( + "{}/diamond-dependency-resolution/main.simf", + VALID_TESTS_DIR ), - ]; - - let (test, _dir) = TestCase::temp_env(main_code, libs); - test.with_witness_values(WitnessValues::default()) - .assert_run_success(); + [( + "lib", + format!("{}/diamond-dependency-resolution/lib", VALID_TESTS_DIR), + )], + ) + .with_witness_values(WitnessValues::default()) + .assert_run_success(); } #[test] #[should_panic(expected = "Circular dependency detected:")] fn cyclic_dependency_error() { - let main_code = "use temp::module_a::TypeA; fn main() {}"; - - let libs = vec![ - ( - "temp", - "temp/module_a.simf", - "pub use temp::module_b::TypeB; pub type TypeA = u32;", - ), - ( - "temp", - "temp/module_b.simf", - "pub use temp::module_a::TypeA; pub type TypeB = u32;", - ), - ]; - - let (test, _dir) = TestCase::temp_env(main_code, libs); - test.with_witness_values(WitnessValues::default()) - .assert_run_success(); + TestCase::program_file_with_libs( + format!("{}/cyclic-dependency/main.simf", ERROR_TESTS_DIR), + [("lib", format!("{}/cyclic-dependency/lib", ERROR_TESTS_DIR))], + ) + .with_witness_values(WitnessValues::default()) + .assert_run_success(); } #[test] fn deep_reexport_chain() { - let main_code = r#" - use temp::level1::{CoreSmth, core_val}; - - fn main() { - let val: CoreSmth = core_val(); - assert!(jet::eq_32(val, 42)); - } - "#; - - let libs = vec![ - ( - "temp", - "temp/level3.simf", - "pub type CoreSmth = u32; pub fn core_val() -> CoreSmth { 42 }", - ), - ( - "temp", - "temp/level2.simf", - "pub use temp::level3::{CoreSmth, core_val};", - ), - ( - "temp", - "temp/level1.simf", - "pub use temp::level2::{CoreSmth, core_val};", - ), - ]; - - let (test, _dir) = TestCase::temp_env(main_code, libs); - test.with_witness_values(WitnessValues::default()) - .assert_run_success(); + TestCase::program_file_with_libs( + format!("{}/deep-reexport-chain/main.simf", VALID_TESTS_DIR), + [( + "lib", + format!("{}/deep-reexport-chain/lib", VALID_TESTS_DIR), + )], + ) + .with_witness_values(WitnessValues::default()) + .assert_run_success(); } #[test] #[should_panic(expected = "Item `SecretType` is private")] fn private_type_visibility_error() { - let main_code = r#" - use temp::hidden::SecretType; - fn main() {} - "#; - - let libs = vec![( - "temp", - "temp/hidden.simf", - "type SecretType = u32; pub fn ok() {}", - )]; - - let (test, _dir) = TestCase::temp_env(main_code, libs); - test.with_witness_values(WitnessValues::default()) - .assert_run_success(); + TestCase::program_file_with_libs( + format!("{}/private-visibility/main.simf", ERROR_TESTS_DIR), + [("lib", format!("{}/private-visibility/lib", ERROR_TESTS_DIR))], + ) + .with_witness_values(WitnessValues::default()) + .assert_run_success(); } #[test] @@ -828,31 +772,100 @@ pub(crate) mod tests { } #[test] - #[ignore] - #[should_panic(expected = "was defined multiple times")] // TODO: Or not? Fix it later, after receiving a response from the devs. - fn name_collision_error() { - let main_code = r#" - use temp::mod_a::Value; - use temp::mod_b::Value; + fn single_lib() { + TestCase::program_file_with_libs( + "./examples/single_lib/main.simf", + [("temp", "./examples/single_lib/temp")], + ) + .with_witness_values(WitnessValues::default()) + .assert_run_success(); + } - fn main() {} - "#; + #[test] + #[should_panic( + expected = "Cannot parse: found '*' expected '/', jet, witness, param, 'a', 'p', 'd', 'l', identifier, '0', something else, '-', '=', ':', ';', ',', '(', ')', '[', ']', '{', '}', '<', or '>'" + )] + fn global_import_error() { + TestCase::program_file_with_libs( + format!("{}/global/main.simf", ERROR_TESTS_DIR), + [("lib", format!("{}/global/lib", ERROR_TESTS_DIR))], + ) + .with_witness_values(WitnessValues::default()) + .assert_run_success(); + } + + #[test] + fn file_not_found_error() { + use std::panic; + let result = panic::catch_unwind(|| { + TestCase::program_file_with_libs( + format!("{}/file-not-found/main.simf", ERROR_TESTS_DIR), + [("lib", &format!("{}/file-not-found/lib", ERROR_TESTS_DIR))], + ) + .with_witness_values(WitnessValues::default()) + .assert_run_success(); + }); + + let panic_msg = result + .err() + .and_then(|b| b.downcast_ref::().cloned()) + .unwrap_or_default(); + let expected = format!( + "File `{}/file-not-found/lib/module.simf` not found", + ERROR_TESTS_DIR + ); - let libs = vec![ - ("temp", "temp/mod_a.simf", "pub type Value = u32;"), - ("temp", "temp/mod_b.simf", "pub type Value = u32;"), - ]; + assert!(panic_msg.contains(&expected)); + } - let (test, _dir) = TestCase::temp_env(main_code, libs); - test.with_witness_values(WitnessValues::default()) + #[test] + fn lib_not_found_error() { + use std::panic; + + let result = panic::catch_unwind(|| { + TestCase::program_file_with_libs( + format!("{}/lib-not-found/main.simf", ERROR_TESTS_DIR), + [("lib", &format!("{}/lib-not-found/lib", ERROR_TESTS_DIR))], + ) + .with_witness_values(WitnessValues::default()) .assert_run_success(); + }); + + let panic_msg = result + .err() + .and_then(|b| b.downcast_ref::().cloned()) + .unwrap_or_default(); + let expected = format!( + "File `{}/lib-not-found/lib/module.simf` not found", + ERROR_TESTS_DIR + ); + + assert!(panic_msg.contains(&expected)); } #[test] - fn single_lib() { + fn multi_lib_facade_resolution() { TestCase::program_file_with_libs( - "./examples/single_lib/main.simf", - [("temp", "./examples/single_lib/temp")], + format!("{}/multi-lib-facade/main.simf", VALID_TESTS_DIR), + [ + ("math", format!("{}/multi-lib-facade/math", VALID_TESTS_DIR)), + ( + "crypto", + format!("{}/multi-lib-facade/crypto", VALID_TESTS_DIR), + ), + ("api", format!("{}/multi-lib-facade/api", VALID_TESTS_DIR)), + ], + ) + .with_witness_values(WitnessValues::default()) + .assert_run_success(); + } + + #[test] + #[should_panic(expected = "Inconsistent resolution order")] + fn cross_wire_linearization_error() { + TestCase::program_file_with_libs( + format!("{}/cross-wire/main.simf", ERROR_TESTS_DIR), + [("lib", format!("{}/cross-wire/lib", ERROR_TESTS_DIR))], ) .with_witness_values(WitnessValues::default()) .assert_run_success(); From 4a07c04cf07171db351510e06372258c900d5a91 Mon Sep 17 00:00:00 2001 From: Sdoba16 Date: Thu, 12 Mar 2026 14:50:44 +0200 Subject: [PATCH 12/13] Error test cases, multifile error case --- .../name-collision/lib/groups.simf | 3 + .../name-collision/lib/math.simf | 4 + .../error-test-cases/name-collision/main.simf | 9 + src/driver.rs | 154 ++++++++++++++++++ src/error.rs | 73 +++++++-- 5 files changed, 229 insertions(+), 14 deletions(-) create mode 100644 functional-tests/error-test-cases/name-collision/lib/groups.simf create mode 100644 functional-tests/error-test-cases/name-collision/lib/math.simf create mode 100644 functional-tests/error-test-cases/name-collision/main.simf diff --git a/functional-tests/error-test-cases/name-collision/lib/groups.simf b/functional-tests/error-test-cases/name-collision/lib/groups.simf new file mode 100644 index 00000000..7ef2eff1 --- /dev/null +++ b/functional-tests/error-test-cases/name-collision/lib/groups.simf @@ -0,0 +1,3 @@ +pub fn add(a: u32, b: u32) -> (bool, u32) { + jet::add_32(a, b) +} \ No newline at end of file diff --git a/functional-tests/error-test-cases/name-collision/lib/math.simf b/functional-tests/error-test-cases/name-collision/lib/math.simf new file mode 100644 index 00000000..b9a83fd9 --- /dev/null +++ b/functional-tests/error-test-cases/name-collision/lib/math.simf @@ -0,0 +1,4 @@ +pub fn add(a: u32, b: u32) -> (bool, u32) { + let (_, c): (bool, u32) = jet::add_32(a, b); + jet::add_32(c, 1) +} \ No newline at end of file diff --git a/functional-tests/error-test-cases/name-collision/main.simf b/functional-tests/error-test-cases/name-collision/main.simf new file mode 100644 index 00000000..59b5c813 --- /dev/null +++ b/functional-tests/error-test-cases/name-collision/main.simf @@ -0,0 +1,9 @@ +use lib::groups::add; +use lib::math::add; + +fn main() { + let x: u32 = 5; + let y: u32 = 10; + let (_, result): (bool, u32) = add(x, y); + assert!(jet::eq_32(result, 16)); +} \ No newline at end of file diff --git a/src/driver.rs b/src/driver.rs index 8bd2186a..f2a7fe97 100644 --- a/src/driver.rs +++ b/src/driver.rs @@ -1532,4 +1532,158 @@ pub(crate) mod tests { "Compiler must catch infinite alias cycles" ); } + + // --- C3 Error Display Tests --- + + #[test] + fn display_c3_cycle_detected() { + let cycle = vec![ + "main.simf".to_string(), + "libs/lib/A.simf".to_string(), + "libs/lib/B.simf".to_string(), + "main.simf".to_string(), + ]; + + let error = C3Error::CycleDetected(cycle); + + let expected = r#"Circular dependency detected: "main.simf -> libs/lib/A.simf -> libs/lib/B.simf -> main.simf""#; + + assert_eq!(error.to_string(), expected); + } + + #[test] + fn display_c3_inconsistent_linearization() { + let conflicts = vec![ + vec!["lib/x".to_string(), "lib/y".to_string()], + vec!["lib/y".to_string(), "lib/x".to_string()], + ]; + + let error = C3Error::InconsistentLinearization { + module: "main".to_string(), + conflicts, + }; + + let expected = r#"Inconsistent resolution order for module 'main' +The compiler could not resolve the following conflicting import constraints: + [lib/x, lib/y] + [lib/y, lib/x] +Try reordering your `use` statements to avoid cross-wiring."# + .to_string(); + + assert_eq!(error.to_string(), expected); + } + + // --- Dependent File Error Display Tests --- + + #[test] + #[ignore = "TODO(Error_Formatting): The compiler currently strips the .simf extension from file paths during graph construction. This test expects the extension to be preserved."] + fn test_display_error_in_imported_dependency() { + let temp_dir = TempDir::new().unwrap(); + let root_path = create_simf_file(temp_dir.path(), "main.simf", "use lib::math::add;"); + + create_simf_file( + temp_dir.path(), + "libs/lib/math.simf", + "pub fn add(a: u32 b: u32) {}", + ); + + let mut lib_map = HashMap::new(); + lib_map.insert("lib".to_string(), temp_dir.path().join("libs/lib")); + + let (root_program, root_source) = parse_root(&root_path); + let mut handler = ErrorCollector::new(); + + let result = + ProjectGraph::new(root_source, Arc::from(lib_map), &root_program, &mut handler); + + assert!( + result.is_none(), + "Graph construction should fail due to syntax error in dependency" + ); + assert!( + handler.has_errors(), + "Handler should contain the imported module's error" + ); + + let err_msg = ErrorCollector::to_string(&handler); + + assert!( + err_msg.contains("math.simf:1"), + "Error should correctly display the file name math.simf and line number. Got:\n{}", + err_msg + ); + assert!( + err_msg.contains("pub fn add(a: u32 b: u32) {}"), + "Error should print the snippet from the imported file. Got:\n{}", + err_msg + ); + } + + #[test] + #[ignore = "TODO(Error_Formatting): The compiler currently strips the .simf extension from file paths during graph construction. This test expects the extension to be preserved."] + fn test_display_unresolved_item_in_dependency() { + let (graph, ids, _dir) = setup_graph(vec![ + ("libs/lib/B.simf", "pub fn real() {}"), + ("libs/lib/A.simf", "use lib::B::ghost;\npub fn foo() {}"), + ("main.simf", "use lib::A::foo;"), + ]); + + let id_a = *ids.get("A").unwrap(); + let id_b = *ids.get("B").unwrap(); + let id_root = *ids.get("main").unwrap(); + let order = vec![id_b, id_a, id_root]; + + let mut handler = ErrorCollector::new(); + let _ = graph.build_program(&order, &mut handler); + + let err_msg = ErrorCollector::to_string(&handler); + + assert!( + err_msg.contains("A.simf:1"), + "Error should point to A.simf where the bad import happened. Got:\n{}", + err_msg + ); + assert!( + err_msg.contains("use lib::B::ghost;"), + "Error should print the snippet from A.simf" + ); + assert!( + err_msg.contains("Unknown item `ghost`"), + "Error should correctly identify the missing item" + ); + } + + #[test] + #[ignore = "TODO(Error_Formatting): The compiler currently strips the .simf extension from file paths during graph construction. This test expects the extension to be preserved."] + fn test_display_private_item_access_in_dependency() { + let (graph, ids, _dir) = setup_graph(vec![ + ("libs/lib/B.simf", "fn secret() {}"), + ("libs/lib/A.simf", "use lib::B::secret;\npub fn foo() {}"), + ("main.simf", "use lib::A::foo;"), + ]); + + let id_a = *ids.get("A").unwrap(); + let id_b = *ids.get("B").unwrap(); + let id_root = *ids.get("main").unwrap(); + let order = vec![id_b, id_a, id_root]; + + let mut handler = ErrorCollector::new(); + let _ = graph.build_program(&order, &mut handler); + + let err_msg = ErrorCollector::to_string(&handler); + + assert!( + err_msg.contains("A.simf:1"), + "Error should point to A.simf where the privacy violation happened. Got:\n{}", + err_msg + ); + assert!( + err_msg.contains("use lib::B::secret;"), + "Error should print the snippet from A.simf" + ); + assert!( + err_msg.contains("Item `secret` is private"), + "Error should correctly identify the privacy violation" + ); + } } diff --git a/src/error.rs b/src/error.rs index c901d4f6..ba9c52d9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -443,7 +443,6 @@ pub enum Error { FileNotFound(PathBuf), UnresolvedItem(Identifier), PrivateItem(String), - NameCollision(String), MainNoInputs, MainNoOutput, MainRequired, @@ -565,10 +564,6 @@ impl fmt::Display for Error { f, "Item `{name}` is private" ), - Error::NameCollision(name) => write!( - f, - "The name `{name}` is defined multiple times in this scope" - ), Error::InvalidNumberOfArguments(expected, found) => write!( f, "Expected {expected} arguments, found {found} arguments" @@ -779,18 +774,68 @@ let x: u32 = Left( } #[test] - fn display_private_item() { - let source = SourceFile::new(SourceName::Virtual(Arc::from(FILENAME)), Arc::from(FILE)); - let error = Error::PrivateItem("SecretType".to_string()) - .with_span(Span::new(8, 20)) + fn display_error_inside_imported_module() { + let module_name = "libs/math.simf"; + let module_content = "pub fn add(a: u32, b: u32) -> u64 {\n a + b\n}"; + let source = SourceFile::new( + SourceName::Virtual(Arc::from(module_name)), + Arc::from(module_content), + ); + + let error = Error::InvalidNumberOfArguments(2, 1) + .with_span(Span::new(40, 45)) .with_source(source); - let expected = format!( - r#" - --> {FILENAME}:1:9 + + let expected = r#" + --> libs/math.simf:2:6 | -1 | let a1: List = None; - | ^^^^^^^^^^^^ Item `SecretType` is private"# +2 | a + b + | ^^^^^ Expected 2 arguments, found 1 arguments"# + .to_string(); + assert_eq!(&expected[1..], &error.to_string()); + } + + #[test] + fn display_unresolved_import_in_main_file() { + let main_name = "main.simf"; + let main_content = "use libs::math::multiply;\n\nfn main() {}"; + let source = SourceFile::new( + SourceName::Virtual(Arc::from(main_name)), + Arc::from(main_content), ); + + let error = Error::UnresolvedItem("multiply".into()) + .with_span(Span::new(16, 24)) + .with_source(source); + + let expected = r#" + --> main.simf:1:17 + | +1 | use libs::math::multiply; + | ^^^^^^^^ Unknown item `multiply`"# + .to_string(); + assert_eq!(&expected[1..], &error.to_string()); + } + + #[test] + fn display_private_item_import_across_modules() { + let main_name = "src/auth_test.simf"; + let main_content = "use libs::auth::SecretToken;\n\nfn main() {}"; + let source = SourceFile::new( + SourceName::Virtual(Arc::from(main_name)), + Arc::from(main_content), + ); + + let error = Error::PrivateItem("SecretToken".into()) + .with_span(Span::new(16, 27)) + .with_source(source); + + let expected = r#" + --> src/auth_test.simf:1:17 + | +1 | use libs::auth::SecretToken; + | ^^^^^^^^^^^ Item `SecretToken` is private"# + .to_string(); assert_eq!(&expected[1..], &error.to_string()); } } From 6c34920eddb5151b457cf8cc347c2ca11ab1d6b3 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Thu, 12 Mar 2026 15:35:22 +0200 Subject: [PATCH 13/13] feat: implemented loading one lib from another and tested its functionality --- examples/multiple_libs/main.simf | 12 +- examples/multiple_libs/merkle/build_root.simf | 8 +- examples/temp/libs/lib/math.simf | 1 + examples/temp/main.simf | 1 + .../keyword-as-lib/jet/fn/let.simf | 3 + .../valid-test-cases/keyword-as-lib/main.simf | 3 + src/driver.rs | 202 ++++--- src/error.rs | 5 + src/lib.rs | 542 +++++++++++------- src/main.rs | 81 +-- src/tracker.rs | 6 +- src/witness.rs | 6 +- 12 files changed, 534 insertions(+), 336 deletions(-) create mode 100644 examples/temp/libs/lib/math.simf create mode 100644 examples/temp/main.simf create mode 100644 functional-tests/valid-test-cases/keyword-as-lib/jet/fn/let.simf create mode 100644 functional-tests/valid-test-cases/keyword-as-lib/main.simf diff --git a/examples/multiple_libs/main.simf b/examples/multiple_libs/main.simf index 8bfee917..cead0852 100644 --- a/examples/multiple_libs/main.simf +++ b/examples/multiple_libs/main.simf @@ -1,12 +1,16 @@ -use merkle::build_root::get_root; -use math::simple_op::hash; +use merkle::build_root::{get_root, hash as and_hash}; +use base_math::simple_op::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); - hash(prev_hash, root); + or_hash(prev_hash, root) } fn main() { - let block_val_hash: u32 = get_block_value(5, 10, 20); + 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/multiple_libs/merkle/build_root.simf b/examples/multiple_libs/merkle/build_root.simf index 18242832..f41c37e4 100644 --- a/examples/multiple_libs/merkle/build_root.simf +++ b/examples/multiple_libs/merkle/build_root.simf @@ -1,5 +1,9 @@ -use math::simple_op::hash; +use math::simple_op::hash as temp_hash; pub fn get_root(tx1: u32, tx2: u32) -> u32 { - hash(tx1, tx2) + temp_hash(tx1, tx2) +} + +pub fn hash(tx1: u32, tx2: u32) -> u32 { + jet::and_32(tx1, tx2) } \ No newline at end of file diff --git a/examples/temp/libs/lib/math.simf b/examples/temp/libs/lib/math.simf new file mode 100644 index 00000000..288e6d61 --- /dev/null +++ b/examples/temp/libs/lib/math.simf @@ -0,0 +1 @@ +fn add(a: u32, b: u32) {} \ No newline at end of file diff --git a/examples/temp/main.simf b/examples/temp/main.simf new file mode 100644 index 00000000..55817e4a --- /dev/null +++ b/examples/temp/main.simf @@ -0,0 +1 @@ +use lib::math::add; \ No newline at end of file diff --git a/functional-tests/valid-test-cases/keyword-as-lib/jet/fn/let.simf b/functional-tests/valid-test-cases/keyword-as-lib/jet/fn/let.simf new file mode 100644 index 00000000..1ffbb5fa --- /dev/null +++ b/functional-tests/valid-test-cases/keyword-as-lib/jet/fn/let.simf @@ -0,0 +1,3 @@ +pub fn get_some_value() -> u64 { + 1 +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/keyword-as-lib/main.simf b/functional-tests/valid-test-cases/keyword-as-lib/main.simf new file mode 100644 index 00000000..80143ab6 --- /dev/null +++ b/functional-tests/valid-test-cases/keyword-as-lib/main.simf @@ -0,0 +1,3 @@ +use r#jet::r#fn::r#let; + +pub fn main() {} \ No newline at end of file diff --git a/src/driver.rs b/src/driver.rs index f2a7fe97..13bd0fc4 100644 --- a/src/driver.rs +++ b/src/driver.rs @@ -7,7 +7,7 @@ use crate::error::{Error, ErrorCollector, RichError, Span}; use crate::parse::{self, AliasedIdentifier, ParseFromStrWithErrors, Visibility}; use crate::str::{AliasName, FunctionName, Identifier}; use crate::types::AliasedType; -use crate::{get_full_path, impl_eq_hash, LibTable, SourceFile, SourceName}; +use crate::{impl_eq_hash, DependencyMap, SourceFile, SourceName}; /// Represents a single, isolated file in the SimplicityHL project. /// In this architecture, a file and a module are the exact same thing. @@ -28,8 +28,8 @@ pub struct ProjectGraph { pub(self) modules: Vec, /// The configuration environment. - /// Used to resolve xternal library dependencies and invoke their associated functions. - pub libraries: Arc, + /// Used to resolve external library dependencies and invoke their associated functions. + pub dependency_map: Arc, /// Fast lookup: `SourceName` -> Module ID. /// A reverse index mapping absolute file paths to their internal IDs. @@ -399,7 +399,7 @@ impl ProjectGraph { pub fn new( root_source: SourceFile, - libraries: Arc, + dependency_map: Arc, root_program: &parse::Program, handler: &mut ErrorCollector, ) -> Option { @@ -434,11 +434,10 @@ impl ProjectGraph { // PHASE 1: Resolve Imports for elem in current_program.items() { if let parse::Item::Use(use_decl) = elem { - match get_full_path(&libraries, use_decl) { + match resolve_single_import(importer_source.clone(), use_decl, &dependency_map) + { Ok(path) => valid_imports.push((path, *use_decl.span())), - Err(err) => { - resolution_errors.push(err.with_source(importer_source.clone())) - } + Err(err) => resolution_errors.push(err), } } } @@ -482,7 +481,7 @@ impl ProjectGraph { } else { Some(Self { modules, - libraries, + dependency_map, lookup, paths: paths.into(), dependencies, @@ -691,6 +690,19 @@ impl ProjectGraph { let mut errors: Vec = Vec::new(); match elem { parse::Item::Use(use_decl) => { + let full_path = match resolve_single_import( + importer_source.clone(), + use_decl, + &self.dependency_map, + ) { + Ok(path) => path, + Err(err) => { + handler.push(err); + continue; + } + }; + + /* let full_path = match get_full_path(&self.libraries, use_decl) { Ok(path) => path, Err(err) => { @@ -698,6 +710,7 @@ impl ProjectGraph { continue; } }; + */ let source_full_path = SourceName::Real(Arc::from(full_path)); let ind = self.lookup[&source_full_path]; @@ -776,6 +789,33 @@ impl ProjectGraph { } } +/// Resolves a single `use` declaration into a physical file path. +fn resolve_single_import( + importer_source: SourceFile, + use_decl: &parse::UseDecl, + dependency_map: &DependencyMap, +) -> Result { + // TODO: @LesterEvSe or someone else, reconsider this architectural approach. + // Consider removing this `match` statement, or dropping `SourceName` from `paths` and `lookup`. + let curr_path = match importer_source.name() { + SourceName::Real(path) => path, + SourceName::Virtual(name) => { + // Notice we use `return Err(...)` here instead of `continue` + return Err(RichError::new( + Error::Resolution(format!( + "Virtual source '{name}' cannot be used to resolve library imports" + )), + *use_decl.span(), + )); + } + }; + + match dependency_map.resolve_path(&curr_path, use_decl) { + Ok(path) => Ok(path), + Err(err) => Err(err.with_source(importer_source.clone())), + } +} + /// C3 Merge Algorithm /// /// Merges a list of sequences (parent linearizations) into a single sequence. @@ -989,9 +1029,30 @@ pub(crate) mod tests { (program.expect("Root parsing failed internally"), source) } - /// Sets up a graph with "lib" mapped to "libs/lib". - /// Files format: vec![("main.simf", "content"), ("libs/lib/A.simf", "content")] - fn setup_graph(files: Vec<(&str, &str)>) -> (ProjectGraph, HashMap, TempDir) { + /// Bootstraps a mock file system and attempts to construct a `ProjectGraph`. + /// + /// This is the low-level, non-panicking test helper. It is designed specifically for + /// "negative tests" where you expect the graph construction to fail (e.g., due to syntax + /// errors in an imported dependency). + /// + /// The mock environment automatically maps the alias `"lib"` to the `"libs/lib"` directory. + /// + /// # Arguments + /// * `files` - A vector of tuples containing `(file_path, file_content)`. + /// **Note:** One of the files *must* be named exactly `"main.simf"`. + /// + /// # Returns + /// A tuple containing: + /// 1. `Option` - `Some` if construction succeeded, `None` if compilation failed. + /// 2. `ErrorCollector` - Contains all diagnostics emitted during parsing and resolution. + /// 3. `TempDir` - The temporary directory. It must be kept alive until the test completes. + /// + /// # Panics + /// Panics if the `files` vector does not contain a `"main.simf"` entry, or if writing + /// the mock files to the OS filesystem fails. + fn setup_graph_raw( + files: Vec<(&str, &str)>, + ) -> (Option, ErrorCollector, TempDir) { let temp_dir = TempDir::new().unwrap(); // 1. Create Files @@ -1005,20 +1066,55 @@ pub(crate) mod tests { let root_p = root_path.expect("Tests must define 'main.simf'"); // 2. Setup Libraries (Hardcoded "lib" -> "libs/lib" for simplicity in tests) - let mut lib_map = HashMap::new(); - lib_map.insert("lib".to_string(), temp_dir.path().join("libs/lib")); + let mut dependency_map = DependencyMap::new(); + dependency_map.test_insert_without_canonicalize( + temp_dir.path(), // The root of mock project + "lib".to_string(), + &temp_dir.path().join("libs/lib"), + ); // 3. Parse & Build let (root_program, source) = parse_root(&root_p); - let mut handler = ErrorCollector::new(); - let graph = ProjectGraph::new(source, Arc::from(lib_map), &root_program, &mut handler) - .expect( - "setup_graph expects a valid graph construction. Use manual setup for error tests.", - ); + let result = ProjectGraph::new( + source, + Arc::from(dependency_map), + &root_program, + &mut handler, + ); + + // Return the raw result and the handler so the test can inspect the errors + (result, handler, temp_dir) + } - // 4. Create Lookup (File Name -> ID) for easier asserting + /// Bootstraps a mock file system and constructs a valid `ProjectGraph`. + /// + /// This is the standard test helper for "happy path" scenarios. It wraps [`setup_graph_raw`] + /// and mathematically guarantees that the graph construction succeeds. It also generates a + /// convenient filename-to-ID lookup map to make asserting on specific files easier. + /// + /// # Arguments + /// * `files` - A vector of tuples containing `(file_path, file_content)`. + /// **Note:** One of the files *must* be named exactly `"main.simf"`. + /// + /// # Returns + /// A tuple containing: + /// 1. `ProjectGraph` - The fully constructed, valid dependency graph. + /// 2. `HashMap` - A mapping of simple filenames (e.g., `"math.simf"`) to their node IDs. + /// 3. `TempDir` - The temporary directory. It must be kept alive until the test completes. + /// + /// # Panics + /// Panics if the compiler encounters any errors during parsing or resolution, + /// or if `"main.simf"` is missing. For testing compiler errors, use [`setup_graph_raw`] instead. + fn setup_graph(files: Vec<(&str, &str)>) -> (ProjectGraph, HashMap, TempDir) { + let (graph_result, _handler, temp_dir) = setup_graph_raw(files); + + let graph = graph_result.expect( + "setup_graph expects a valid graph construction. Use manual setup for error tests.", + ); + + // Create Lookup (File Name -> ID) for easier asserting let mut file_ids = HashMap::new(); for (source_name, id) in &graph.lookup { let simple_name = match source_name { @@ -1319,37 +1415,7 @@ pub(crate) mod tests { assert!(graph.dependencies[&ids["main"]].is_empty()); } - #[test] - fn test_missing_file_error() { - // MANUAL SETUP REQUIRED - // We cannot use `setup_graph` here because we expect `ProjectGraph::new` to fail/return None. - - let temp_dir = TempDir::new().unwrap(); - let root_path = create_simf_file(temp_dir.path(), "main.simf", "use lib::ghost::Phantom;"); - // We purposefully DO NOT create ghost.simf - - let mut lib_map = HashMap::new(); - lib_map.insert("lib".to_string(), temp_dir.path().join("libs/lib")); - - let (root_program, root_source) = parse_root(&root_path); - let mut handler = ErrorCollector::new(); - - let result = - ProjectGraph::new(root_source, Arc::from(lib_map), &root_program, &mut handler); - - assert!(result.is_none(), "Graph construction should fail"); - assert!(handler.has_errors()); - - let error_msg = ErrorCollector::to_string(&handler); - assert!( - error_msg.contains("File not found") || error_msg.contains("ghost.simf"), - "Error message should mention 'ghost.simf' or 'File not found'. Got: {}", - error_msg - ); - } - // Tests for aliases - // TODO: @LesterEvSe, @Sdoba16 add more tests for alias #[test] fn test_renaming_with_use() { // Scenario: Renaming imports. @@ -1574,27 +1640,29 @@ Try reordering your `use` statements to avoid cross-wiring."# } // --- Dependent File Error Display Tests --- - #[test] - #[ignore = "TODO(Error_Formatting): The compiler currently strips the .simf extension from file paths during graph construction. This test expects the extension to be preserved."] - fn test_display_error_in_imported_dependency() { - let temp_dir = TempDir::new().unwrap(); - let root_path = create_simf_file(temp_dir.path(), "main.simf", "use lib::math::add;"); - - create_simf_file( - temp_dir.path(), - "libs/lib/math.simf", - "pub fn add(a: u32 b: u32) {}", - ); + fn test_missing_file_error() { + let (result, handler, _dir) = + setup_graph_raw(vec![("main.simf", "use lib::ghost::Phantom;")]); - let mut lib_map = HashMap::new(); - lib_map.insert("lib".to_string(), temp_dir.path().join("libs/lib")); + assert!(result.is_none(), "Graph construction should fail"); + assert!(handler.has_errors()); - let (root_program, root_source) = parse_root(&root_path); - let mut handler = ErrorCollector::new(); + let error_msg = handler.to_string(); + assert!( + error_msg.contains("File not found") || error_msg.contains("ghost.simf"), + "Error message should mention 'ghost.simf' or 'File not found'. Got: {}", + error_msg + ); + } - let result = - ProjectGraph::new(root_source, Arc::from(lib_map), &root_program, &mut handler); + #[test] + #[ignore = "TODO(Error_Formatting): The compiler currently strips the .simf extension from file paths during graph construction. This test expects the extension to be preserved."] + fn test_display_error_in_imported_dependency() { + let (result, handler, _dir) = setup_graph_raw(vec![ + ("main.simf", "use lib::math::add;"), + ("libs/lib/math.simf", "pub fn add(a: u32 b: u32) {}"), // NOTE: The comma is missing on purpose. + ]); assert!( result.is_none(), diff --git a/src/error.rs b/src/error.rs index ba9c52d9..97a01529 100644 --- a/src/error.rs +++ b/src/error.rs @@ -427,6 +427,7 @@ pub enum Error { ListBoundPow2(usize), BitStringPow2(usize), CannotParse(String), + Resolution(String), Grammar(String), Syntax { expected: Vec, @@ -497,6 +498,10 @@ impl fmt::Display for Error { f, "Cannot parse: {description}" ), + Error::Resolution(description) => write!( + f, + "Resolution error: {description}" + ), Error::Grammar(description) => write!( f, "Grammar error: {description}" diff --git a/src/lib.rs b/src/lib.rs index 972361a6..ab426c1a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,7 @@ pub mod types; pub mod value; mod witness; -use std::collections::HashMap; +use std::io; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -35,7 +35,6 @@ pub use simplicity::elements; use crate::debug::DebugSymbols; use crate::driver::ProjectGraph; use crate::error::{Error, ErrorCollector, RichError, WithSource, WithSpan}; -use crate::lexer::RESERVED_TOKENS; use crate::parse::{ParseFromStrWithErrors, UseDecl}; pub use crate::types::ResolvedType; pub use crate::value::Value; @@ -108,39 +107,129 @@ impl SourceFile { } } -pub type LibTable = HashMap; +/// A single dependency rule. +#[derive(Debug, Clone)] +pub struct Remapping { + /// The base directory/file that owns this dependency + pub context_prefix: PathBuf, + /// The name used in the `use` statement (e.g., "math") + pub alias: String, + /// The physical path this alias points to + pub target: PathBuf, +} -pub fn get_full_path(libraries: &LibTable, use_decl: &UseDecl) -> Result { - let parts: Vec<&str> = use_decl.path().iter().map(|s| s.as_ref()).collect(); +/// A list of dependencies, strictly sorted by longest prefix match. +#[derive(Debug, Default)] +pub struct DependencyMap { + inner: Vec, +} - let first_segment = match parts.first() { - Some(s) => *s, - None => { - return Err(Error::CannotParse("Empty use path".to_string())) - .with_span(*use_decl.span()) - } - }; +impl DependencyMap { + pub fn new() -> Self { + Self::default() + } - if let Some(lib_root) = libraries.get(first_segment) { - let mut full_path = lib_root.clone(); - full_path.extend(&parts[1..]); - return Ok(full_path); + pub fn is_empty(&self) -> bool { + self.inner.is_empty() } - Err(Error::UnknownLibrary(first_segment.to_string())).with_span(*use_decl.span()) -} + /// Inserts a dependency remapping without interacting with the physical file system. + /// + /// **Warning:** This method completely bypasses OS path canonicalization (`std::fs::canonicalize`). + /// It is designed strictly for unit testing and virtual file environments where the + /// provided paths might not actually exist on the hard drive. + /// + /// Like the standard `insert` method, it internally re-sorts the remapping vector + /// to mathematically guarantee that Longest Prefix Matching is used during path resolution. + pub fn test_insert_without_canonicalize(&mut self, context: &Path, alias: String, path: &Path) { + self.inner.push(Remapping { + context_prefix: context.to_path_buf(), + alias, + target: path.to_path_buf(), + }); -/// If something went wrong, then function was failed -fn is_reserved_tokens_in_aliases(libraries: &LibTable) -> Result<(), String> { - for k in libraries.keys() { - if RESERVED_TOKENS.contains(&k.as_str()) { - return Err(format!( - "Error: The identifier `{}` is a reserved keyword for intrinsic operations and cannot be utilized as a library name.", - k - )); + self.inner.sort_by(|a, b| { + let len_a = a.context_prefix.as_os_str().len(); + let len_b = b.context_prefix.as_os_str().len(); + len_b.cmp(&len_a) + }); + } + + /// Add a dependency mapped to a specific calling file's path prefix. + /// Re-sorts the vector internally to guarantee the Longest Prefix Match. + pub fn insert(&mut self, context: &Path, alias: String, path: &Path) -> io::Result<()> { + let canon_context = std::fs::canonicalize(context).map_err(|err| { + io::Error::new( + err.kind(), + format!( + "Failed to find context directory '{}': {}", + context.display(), + err + ), + ) + })?; + + let canon_path = std::fs::canonicalize(path).map_err(|err| { + io::Error::new( + err.kind(), + format!( + "Failed to find library target path '{}': {}", + path.display(), + err + ), + ) + })?; + + self.inner.push(Remapping { + context_prefix: canon_context, + alias, + target: canon_path, + }); + + // Re-sort the vector so the longest context paths are always at the front! + // This mathematically guarantees that the first match we find is the most specific. + self.inner.sort_by(|a, b| { + let len_a = a.context_prefix.as_os_str().len(); + let len_b = b.context_prefix.as_os_str().len(); + len_b.cmp(&len_a) // Descending order + }); + + Ok(()) + } + + /// Resolve `use alias::...` into a physical file path by finding the + /// most specific library context that owns the current file. + pub fn resolve_path( + &self, + current_file: &Path, + use_decl: &UseDecl, + ) -> Result { + // Safely extract the first segment (the alias) + let parts: Vec<&str> = use_decl.path().iter().map(|s| s.as_ref()).collect(); + let first_segment = parts.first().copied().ok_or_else(|| { + Error::CannotParse("Empty use path".to_string()).with_span(*use_decl.span()) + })?; + + // Because the vector is sorted by longest prefix, + // the VERY FIRST match we find is guaranteed to be the correct one! + for remapping in &self.inner { + // Check if the current file is inside the context's directory tree + if !current_file.starts_with(&remapping.context_prefix) { + continue; + } + + // Check if the alias matches what the user typed + if remapping.alias == first_segment { + let mut resolved_path = remapping.target.clone(); + resolved_path.extend(&parts[1..]); + return Ok(resolved_path); + } } + + println!("Got an error"); + + Err(Error::UnknownLibrary(first_segment.to_string())).with_span(*use_decl.span()) } - Ok(()) } /// The template of a SimplicityHL program. @@ -160,10 +249,9 @@ impl TemplateProgram { /// The string is not a valid SimplicityHL program. pub fn new>>( source_name: SourceName, - libraries: Arc, + dependency_map: Arc, s: Str, ) -> Result { - is_reserved_tokens_in_aliases(&libraries)?; let source_name = source_name.without_extension(); let file = s.into(); let source = SourceFile::new(source_name.clone(), file.clone()); @@ -177,13 +265,13 @@ impl TemplateProgram { .ok_or_else(|| error_handler.to_string())?; // 2. Create the driver program - let driver_program: driver::Program = if libraries.is_empty() { + let driver_program: driver::Program = if dependency_map.is_empty() { driver::Program::from_parse(&parsed_program, source.clone(), &mut error_handler) .ok_or_else(|| error_handler.to_string())? } else { let graph = ProjectGraph::new( source.clone(), - libraries, + dependency_map, &parsed_program, &mut error_handler, ) @@ -266,12 +354,12 @@ impl CompiledProgram { /// - [`TemplateProgram::instantiate`] pub fn new>>( source_name: SourceName, - libraries: Arc, + dependency_map: Arc, s: Str, arguments: Arguments, include_debug_symbols: bool, ) -> Result { - TemplateProgram::new(source_name, libraries, s) + TemplateProgram::new(source_name, dependency_map, s) .and_then(|template| template.instantiate(arguments, include_debug_symbols)) } @@ -352,14 +440,19 @@ impl SatisfiedProgram { /// - [`CompiledProgram::satisfy`] pub fn new>>( source_name: SourceName, - libraries: Arc, + dependency_map: Arc, s: Str, arguments: Arguments, witness_values: WitnessValues, include_debug_symbols: bool, ) -> Result { - let compiled = - CompiledProgram::new(source_name, libraries, s, arguments, include_debug_symbols)?; + let compiled = CompiledProgram::new( + source_name, + dependency_map, + s, + arguments, + include_debug_symbols, + )?; compiled.satisfy(witness_values) } @@ -460,30 +553,30 @@ pub(crate) mod tests { let program_text = std::fs::read_to_string(program_file_path).unwrap(); Self::template_text( SourceName::default(), - Arc::from(HashMap::new()), + Arc::from(DependencyMap::new()), Cow::Owned(program_text), ) } pub fn template_lib( source_name: SourceName, - libraries: Arc, + dependency_map: Arc, program_file: &Path, ) -> Self { let program_text = std::fs::read_to_string(program_file).unwrap(); - Self::template_text(source_name, libraries, Cow::Owned(program_text)) + Self::template_text(source_name, dependency_map, Cow::Owned(program_text)) } pub fn template_text( source_name: SourceName, - libraries: Arc, + dependency_map: Arc, program_text: Cow, ) -> Self { - let program = match TemplateProgram::new(source_name, libraries, program_text.as_ref()) - { - Ok(x) => x, - Err(error) => panic!("{error}"), - }; + let program = + match TemplateProgram::new(source_name, dependency_map, program_text.as_ref()) { + Ok(x) => x, + Err(error) => panic!("{error}"), + }; Self { program, lock_time: elements::LockTime::ZERO, @@ -520,36 +613,6 @@ pub(crate) mod tests { } impl TestCase { - pub fn temp_env( - main_content: &str, - libs: Vec<(&str, &str, &str)>, - ) -> (Self, tempfile::TempDir) { - let temp_dir = tempfile::TempDir::new().unwrap(); - let main_path = - driver::tests::create_simf_file(temp_dir.path(), "main.simf", main_content); - let mut lib_paths = Vec::new(); - - for (lib_name, rel_path, content) in libs { - driver::tests::create_simf_file(temp_dir.path(), rel_path, content); - - let lib_root = temp_dir - .path() - .join(rel_path) - .parent() - .unwrap() - .to_path_buf(); - lib_paths.push((lib_name.to_string(), lib_root)); - } - - let libs_refs: Vec<(&str, &std::path::Path)> = lib_paths - .iter() - .map(|(k, v)| (k.as_str(), v.as_path())) - .collect(); - - let test_case = Self::program_file_with_libs(&main_path, libs_refs); - (test_case, temp_dir) - } - pub fn program_file>(program_file_path: P) -> Self { TestCase::::template_file(program_file_path) .with_arguments(Arguments::default()) @@ -558,32 +621,43 @@ pub(crate) mod tests { pub fn program_text(program_text: Cow) -> Self { TestCase::::template_text( SourceName::default(), - Arc::from(HashMap::new()), + Arc::from(DependencyMap::new()), program_text, ) .with_arguments(Arguments::default()) } - pub fn program_file_with_libs(program_file_path: P, libs: I) -> Self + pub fn program_file_with_libs(program_file: P, libs: I) -> Self where P: AsRef, - I: IntoIterator, // Magic trait: accepts anything we can iterate over + I: IntoIterator, K: Into, - V: AsRef, { - let path_ref = program_file_path.as_ref(); - - let mut libraries = HashMap::new(); - for (k, v) in libs { - libraries.insert(k.into(), v.as_ref().to_path_buf()); + let path_ref = + std::fs::canonicalize(program_file).expect("Failed to canonicalize program path"); + let path_ref = path_ref.as_path(); + + let mut dependency_map = DependencyMap::new(); + for (context, alias, target) in libs { + let context = + std::fs::canonicalize(context).expect("Failed to canonicalize program path"); + let target = + std::fs::canonicalize(target).expect("Failed to canonicalize program path"); + dependency_map + .insert(context.as_ref(), alias.into(), target.as_ref()) + .unwrap(); } - let source_name = - SourceName::Real(Arc::from(path_ref.parent().unwrap_or(Path::new("")))); + let source_name = SourceName::Real(Arc::from( + path_ref.parent().unwrap_or_else(|| Path::new(".")), + )); - // 3. Delegate to your existing template_lib method - TestCase::::template_lib(source_name, Arc::from(libraries), path_ref) - .with_arguments(Arguments::default()) + TestCase::::template_lib( + source_name, + Arc::from(dependency_map), + path_ref, + ) + .with_arguments(Arguments::default()) } #[cfg(feature = "serde")] @@ -685,100 +759,154 @@ pub(crate) mod tests { } } + /// THE DEFAULT HELPER + /// Automatically sets up the standard `lib` self-referencing dependency. + 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"); + + TestCase::program_file_with_libs( + &main_path, + [ + (&root_path, lib_alias, &lib_path), + (&lib_path, lib_alias, &lib_path), + ], + ) + .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. + fn run_multi_lib_test(root_path: &str, deps: &[(&str, &str, &str)]) { + let root_path = PathBuf::from(root_path); + let main_path = root_path.join("main.simf"); + + // 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)); + + TestCase::program_file_with_libs(&main_path, ref_deps) + .with_witness_values(WitnessValues::default()) + .assert_run_success(); + } + 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() { - TestCase::program_file_with_libs( - format!("{}/module-simple/main.simf", VALID_TESTS_DIR), - [("lib", format!("{}/module-simple/lib", VALID_TESTS_DIR))], - ) - .with_witness_values(WitnessValues::default()) - .assert_run_success(); + run_dependency_test(format!("{}/module-simple", VALID_TESTS_DIR).as_str(), "lib"); } #[test] fn diamond_dependency_resolution() { - TestCase::program_file_with_libs( - format!( - "{}/diamond-dependency-resolution/main.simf", - VALID_TESTS_DIR - ), - [( - "lib", - format!("{}/diamond-dependency-resolution/lib", VALID_TESTS_DIR), - )], - ) - .with_witness_values(WitnessValues::default()) - .assert_run_success(); + run_dependency_test( + format!("{}/diamond-dependency-resolution", VALID_TESTS_DIR).as_str(), + "lib", + ); } #[test] - #[should_panic(expected = "Circular dependency detected:")] - fn cyclic_dependency_error() { - TestCase::program_file_with_libs( - format!("{}/cyclic-dependency/main.simf", ERROR_TESTS_DIR), - [("lib", format!("{}/cyclic-dependency/lib", ERROR_TESTS_DIR))], - ) - .with_witness_values(WitnessValues::default()) - .assert_run_success(); + fn deep_reexport_chain() { + run_dependency_test( + format!("{}/deep-reexport-chain", VALID_TESTS_DIR).as_str(), + "lib", + ); } #[test] - fn deep_reexport_chain() { - TestCase::program_file_with_libs( - format!("{}/deep-reexport-chain/main.simf", VALID_TESTS_DIR), - [( - "lib", - format!("{}/deep-reexport-chain/lib", VALID_TESTS_DIR), - )], - ) - .with_witness_values(WitnessValues::default()) - .assert_run_success(); + fn leaky_signature() { + run_dependency_test( + format!("{}/leaky-signature", VALID_TESTS_DIR).as_str(), + "lib", + ); } #[test] - #[should_panic(expected = "Item `SecretType` is private")] - fn private_type_visibility_error() { - TestCase::program_file_with_libs( - format!("{}/private-visibility/main.simf", ERROR_TESTS_DIR), - [("lib", format!("{}/private-visibility/lib", ERROR_TESTS_DIR))], - ) - .with_witness_values(WitnessValues::default()) - .assert_run_success(); + fn reexport_diamond() { + run_dependency_test( + format!("{}/reexport-diamond", VALID_TESTS_DIR).as_str(), + "lib", + ); } + // TODO: @Sdoba16, un-ignore this test once teh parser supports r#. + // Please also add a negative test to ensure parsing fails without r#. #[test] - #[should_panic( - expected = "Error: The identifier `jet` is a reserved keyword for intrinsic operations and cannot be utilized as a library name." - )] + #[ignore] + // Move this validation into a separate test. + // #[should_panic( + // expected = "Error: The identifier `jet` is a reserved keyword for intrinsic operations and cannot be utilized as a library name." + // )] fn using_jet_as_module() { - let main_code = r#" - use jet::eq_32::SecretType; - fn main() {} - "#; - - let libs = vec![( + run_dependency_test( + format!("{}/keyword-as-lib", VALID_TESTS_DIR).as_str(), "jet", - "temp/eq_32.simf", - "type SecretType = u32; pub fn ok() {}", - )]; + ); + } - let (test, _dir) = TestCase::temp_env(main_code, libs); - test.with_witness_values(WitnessValues::default()) - .assert_run_success(); + #[test] + fn multi_lib_facade_resolution() { + run_multi_lib_test( + format!("{}/multi-lib-facade", VALID_TESTS_DIR).as_str(), + &[ + (".", "api", "api"), + ("crypto", "math", "math"), + ("api", "crypto", "crypto"), + ("api", "math", "math"), + ], + ); } #[test] - fn single_lib() { - TestCase::program_file_with_libs( - "./examples/single_lib/main.simf", - [("temp", "./examples/single_lib/temp")], - ) - .with_witness_values(WitnessValues::default()) - .assert_run_success(); + fn interleaved_waterfall() { + run_multi_lib_test( + format!("{}/interleaved-waterfall", VALID_TESTS_DIR).as_str(), + &[ + (".", "orch", "orch"), + ("orch", "db", "db"), + ("orch", "auth", "auth"), + ("orch", "types", "types"), + ("db", "types", "types"), + ("auth", "types", "types"), + ("auth", "db", "db"), + ], + ); + } + + // Error tests + #[test] + #[should_panic(expected = "Inconsistent resolution order")] + fn cross_wire_linearization_error() { + run_dependency_test(format!("{}/cross-wire", ERROR_TESTS_DIR).as_str(), "lib"); + } + + #[test] + #[should_panic(expected = "Item `SecretType` is private")] + fn private_type_visibility_error() { + run_dependency_test( + format!("{}/private-visibility", ERROR_TESTS_DIR).as_str(), + "lib", + ); } #[test] @@ -786,89 +914,57 @@ pub(crate) mod tests { expected = "Cannot parse: found '*' expected '/', jet, witness, param, 'a', 'p', 'd', 'l', identifier, '0', something else, '-', '=', ':', ';', ',', '(', ')', '[', ']', '{', '}', '<', or '>'" )] fn global_import_error() { - TestCase::program_file_with_libs( - format!("{}/global/main.simf", ERROR_TESTS_DIR), - [("lib", format!("{}/global/lib", ERROR_TESTS_DIR))], - ) - .with_witness_values(WitnessValues::default()) - .assert_run_success(); + run_dependency_test(format!("{}/global", ERROR_TESTS_DIR).as_str(), "lib"); } #[test] - fn file_not_found_error() { - use std::panic; - let result = panic::catch_unwind(|| { - TestCase::program_file_with_libs( - format!("{}/file-not-found/main.simf", ERROR_TESTS_DIR), - [("lib", &format!("{}/file-not-found/lib", ERROR_TESTS_DIR))], - ) - .with_witness_values(WitnessValues::default()) - .assert_run_success(); - }); - - let panic_msg = result - .err() - .and_then(|b| b.downcast_ref::().cloned()) - .unwrap_or_default(); - let expected = format!( - "File `{}/file-not-found/lib/module.simf` not found", - ERROR_TESTS_DIR + #[should_panic(expected = "Circular dependency detected:")] + fn cyclic_dependency_error() { + run_dependency_test( + format!("{}/cyclic-dependency", ERROR_TESTS_DIR).as_str(), + "lib", ); + } - assert!(panic_msg.contains(&expected)); + #[test] + #[should_panic(expected = "No such file or directory")] + fn file_not_found_error() { + run_dependency_test( + format!("{}/file-not-found", ERROR_TESTS_DIR).as_str(), + "lib", + ); } #[test] + #[should_panic(expected = "No such file or directory")] fn lib_not_found_error() { - use std::panic; - - let result = panic::catch_unwind(|| { - TestCase::program_file_with_libs( - format!("{}/lib-not-found/main.simf", ERROR_TESTS_DIR), - [("lib", &format!("{}/lib-not-found/lib", ERROR_TESTS_DIR))], - ) - .with_witness_values(WitnessValues::default()) - .assert_run_success(); - }); - - let panic_msg = result - .err() - .and_then(|b| b.downcast_ref::().cloned()) - .unwrap_or_default(); - let expected = format!( - "File `{}/lib-not-found/lib/module.simf` not found", - ERROR_TESTS_DIR - ); + run_dependency_test(format!("{}/lib-not-found", ERROR_TESTS_DIR).as_str(), "lib"); + } - assert!(panic_msg.contains(&expected)); + // Examples + #[test] + fn single_lib() { + run_dependency_test("./examples/single_lib", "temp"); } #[test] - fn multi_lib_facade_resolution() { - TestCase::program_file_with_libs( - format!("{}/multi-lib-facade/main.simf", VALID_TESTS_DIR), - [ - ("math", format!("{}/multi-lib-facade/math", VALID_TESTS_DIR)), - ( - "crypto", - format!("{}/multi-lib-facade/crypto", VALID_TESTS_DIR), - ), - ("api", format!("{}/multi-lib-facade/api", VALID_TESTS_DIR)), - ], - ) - .with_witness_values(WitnessValues::default()) - .assert_run_success(); + fn simple_multilib() { + run_multi_lib_test( + "./examples/simple_multilib", + &[(".", "math", "math"), (".", "crypto", "crypto")], + ); } #[test] - #[should_panic(expected = "Inconsistent resolution order")] - fn cross_wire_linearization_error() { - TestCase::program_file_with_libs( - format!("{}/cross-wire/main.simf", ERROR_TESTS_DIR), - [("lib", format!("{}/cross-wire/lib", ERROR_TESTS_DIR))], - ) - .with_witness_values(WitnessValues::default()) - .assert_run_success(); + fn multiple_libs() { + run_multi_lib_test( + "./examples/multiple_libs", + &[ + (".", "merkle", "merkle"), + (".", "base_math", "math"), + ("merkle", "math", "math"), + ], + ); } #[test] @@ -1060,7 +1156,7 @@ fn main() { "#; match SatisfiedProgram::new( SourceName::default(), - Arc::from(HashMap::new()), + Arc::from(DependencyMap::new()), prog_text, Arguments::default(), WitnessValues::default(), diff --git a/src/main.rs b/src/main.rs index cf339556..3165c436 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use base64::display::Base64Display; use base64::engine::general_purpose::STANDARD; use clap::{Arg, ArgAction, Command}; -use simplicityhl::{lexer::RESERVED_TOKENS, AbiMeta, CompiledProgram, LibTable, SourceName}; +use simplicityhl::{AbiMeta, CompiledProgram, DependencyMap, SourceName}; use std::{env, fmt, sync::Arc}; #[cfg_attr(feature = "serde", derive(serde::Serialize))] @@ -47,12 +47,11 @@ fn main() -> Result<(), Box> { .help("SimplicityHL program file to build"), ) .arg( - Arg::new("library") - .long("lib") - .short('L') - .value_name("ALIAS=PATH") + Arg::new("dependencies") + .long("dep") + .value_name("[CONTEXT:]ALIAS=PATH") .action(ArgAction::Append) - .help("Link a library with an alias (e.g., --lib math=./libs/math)"), + .help("Link a dependency, optionally scoped to a specific module (e.g., --dep ./libs/merkle:math=./libs/math)"), ) .arg( Arg::new("wit_file") @@ -93,8 +92,11 @@ fn main() -> Result<(), Box> { let matches = command.get_matches(); let prog_file = matches.get_one::("prog_file").unwrap(); - let prog_path = std::path::Path::new(prog_file); - let prog_text = std::fs::read_to_string(prog_path).map_err(|e| e.to_string())?; + let main_path = std::fs::canonicalize(prog_file).unwrap_or_else(|_| { + panic!("Failed to find the program file: '{}'", prog_file); + }); + let main_path = main_path.as_path(); + let prog_text = std::fs::read_to_string(main_path).map_err(|e| e.to_string())?; let include_debug_symbols = matches.get_flag("debug"); let output_json = matches.get_flag("json"); let abi_param = matches.get_flag("abi"); @@ -118,34 +120,45 @@ fn main() -> Result<(), Box> { simplicityhl::Arguments::default() }; - let lib_args = matches.get_many::("library").unwrap_or_default(); - - let libraries: LibTable = lib_args - .map(|arg| { - let parts: Vec<&str> = arg.splitn(2, '=').collect(); - - if parts.len() != 2 { - eprintln!( - "Error: Library argument must be in format ALIAS=PATH, got '{}'", - arg - ); - std::process::exit(1); - } - - if RESERVED_TOKENS.contains(&parts[0]) { - eprintln!( - "Error: The identifier `{}` is a reserved keyword for intrinsic operations and cannot be utilized as a library name.", - parts[0] - ); - std::process::exit(1); - } - - (parts[0].to_string(), std::path::PathBuf::from(parts[1])) - }) - .collect(); + let lib_args = matches + .get_many::("dependencies") + .unwrap_or_default(); + + let mut libraries = DependencyMap::new(); + + for arg in lib_args { + // Split by '=' to separate the target path + let (left_side, path_str) = arg.split_once('=').unwrap_or_else(|| { + eprintln!( + "Error: Library argument must be in format [CONTEXT:]ALIAS=PATH, got '{arg}'" + ); + std::process::exit(1); + }); + + // Split by ':' to check for a specific context + let (context_path, alias) = if let Some((ctx_str, alias_str)) = left_side.split_once(':') { + // Specific context provided (e.g., merkle:base_math=...) + // We convert it to PathBuf so it shares the same type as main_path + (std::path::Path::new(ctx_str), alias_str) + } else { + // No context provided (e.g., math=...). Bind it to the main file! + (main_path, left_side) + }; + + // Insert and handle the potential io::Error! + // We convert path_str to PathBuf so it maches the tyep of context_path + if let Err(e) = libraries.insert( + context_path, + alias.to_string(), + std::path::Path::new(path_str), + ) { + eprintln!("Error: {e}"); + std::process::exit(1); + } + } let compiled = match CompiledProgram::new( - SourceName::Real(Arc::from(prog_path)), + SourceName::Real(Arc::from(main_path)), Arc::from(libraries), prog_text, args_opt, diff --git a/src/tracker.rs b/src/tracker.rs index 9791b5f5..562f95a6 100644 --- a/src/tracker.rs +++ b/src/tracker.rs @@ -399,8 +399,8 @@ mod tests { use crate::elements::hashes::Hash; use crate::elements::pset::Input; use crate::elements::{AssetId, OutPoint, Script, Txid}; - use crate::SourceName; use crate::{Arguments, TemplateProgram, WitnessValues}; + use crate::{DependencyMap, SourceName}; use super::*; @@ -475,7 +475,7 @@ mod tests { fn test_debug_and_jet_tracing() { let program = TemplateProgram::new( SourceName::default(), - Arc::from(HashMap::new()), + Arc::from(DependencyMap::new()), TEST_PROGRAM, ) .unwrap(); @@ -549,7 +549,7 @@ mod tests { let program = TemplateProgram::new( SourceName::default(), - Arc::from(HashMap::new()), + Arc::from(DependencyMap::new()), TEST_ARITHMETIC_JETS, ) .unwrap(); diff --git a/src/witness.rs b/src/witness.rs index 6cf2ca3a..4ad7beb6 100644 --- a/src/witness.rs +++ b/src/witness.rs @@ -223,7 +223,7 @@ mod tests { use crate::error::ErrorCollector; use crate::parse::ParseFromStr; use crate::value::ValueConstructible; - use crate::{ast, driver, parse, CompiledProgram, SatisfiedProgram}; + use crate::{ast, driver, parse, CompiledProgram, DependencyMap, SatisfiedProgram}; use crate::{SourceFile, SourceName}; #[test] @@ -256,7 +256,7 @@ mod tests { )])); match SatisfiedProgram::new( SourceName::default(), - Arc::from(HashMap::new()), + Arc::from(DependencyMap::new()), s, Arguments::default(), witness, @@ -282,7 +282,7 @@ fn main() { match CompiledProgram::new( SourceName::default(), - Arc::from(HashMap::new()), + Arc::from(DependencyMap::new()), s, Arguments::default(), false,