From 72581d5213fcaba8f61bbeb16b71b4035ed85b3f Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Tue, 10 Feb 2026 14:42:35 +0200 Subject: [PATCH 1/7] feat: add pub and use keywords and parse them --- examples/modules.simf | 12 ++ examples/modules/main.simf | 9 ++ examples/modules/temp/get_five.simf | 3 + src/ast.rs | 68 +++++++++- src/lexer.rs | 9 ++ src/parse.rs | 187 ++++++++++++++++++++++++++-- 6 files changed, 276 insertions(+), 12 deletions(-) create mode 100644 examples/modules.simf create mode 100644 examples/modules/main.simf create mode 100644 examples/modules/temp/get_five.simf diff --git a/examples/modules.simf b/examples/modules.simf new file mode 100644 index 00000000..bf3bead8 --- /dev/null +++ b/examples/modules.simf @@ -0,0 +1,12 @@ +use example::of::my::file; +pub use another::file::*; +pub use some::list::{of, items}; + +pub fn get_five() -> u32 { + 5 +} + +fn main() { + let five: u32 = dbg!(get_five()); + assert!(jet::eq_32(five, 5)); +} \ No newline at end of file diff --git a/examples/modules/main.simf b/examples/modules/main.simf new file mode 100644 index 00000000..857e2fc7 --- /dev/null +++ b/examples/modules/main.simf @@ -0,0 +1,9 @@ +//pub use temp::get_five; + +fn get_five() -> u32 { + 7 +} + +fn main() { + let five: u32 = get_five(); +} \ No newline at end of file diff --git a/examples/modules/temp/get_five.simf b/examples/modules/temp/get_five.simf new file mode 100644 index 00000000..e5def94a --- /dev/null +++ b/examples/modules/temp/get_five.simf @@ -0,0 +1,3 @@ +pub fn get_five() -> u32 { + 5 +} \ No newline at end of file diff --git a/src/ast.rs b/src/ast.rs index 6cda4851..6eda96d5 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -11,7 +11,7 @@ use simplicity::jet::Elements; use crate::debug::{CallTracker, DebugSymbols, TrackedCallName}; use crate::error::{Error, RichError, Span, WithSpan}; use crate::num::{NonZeroPow2Usize, Pow2Usize}; -use crate::parse::MatchPattern; +use crate::parse::{MatchPattern, UseDecl}; use crate::pattern::Pattern; use crate::str::{AliasName, FunctionName, Identifier, ModuleName, WitnessName}; use crate::types::{ @@ -73,6 +73,8 @@ pub enum Item { TypeAlias, /// A function. Function(Function), + /// A use declaration + Use(UseDecl), /// A module, which is ignored. Module, } @@ -759,6 +761,12 @@ impl AbstractSyntaxTree for Item { parse::Item::Function(function) => { Function::analyze(function, ty, scope).map(Self::Function) } + parse::Item::Use(_) => { + println!("WARN: Skipping use declaration (not implemented yet)"); + Ok(Self::Module) + //todo!() + //Use::analyze(use_declaration).map(Self::Use) + } parse::Item::Module => Ok(Self::Module), } } @@ -820,6 +828,64 @@ impl AbstractSyntaxTree for Function { } } +/* +impl AbstractSyntaxTree for UseDecl { + type From = parse::UseDecl; + + 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"); + + if from.name().as_inner() != "main" { + let params = from + .params() + .iter() + .map(|param| { + let identifier = param.identifier().clone(); + let ty = scope.resolve(param.ty())?; + Ok(FunctionParam { identifier, ty }) + }) + .collect::, Error>>() + .with_span(from)?; + let ret = from + .ret() + .as_ref() + .map(|aliased| scope.resolve(aliased).with_span(from)) + .transpose()? + .unwrap_or_else(ResolvedType::unit); + scope.push_scope(); + 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(); + debug_assert!(scope.is_topmost()); + let function = CustomFunction { params, body }; + scope + .insert_function(from.name().clone(), function) + .with_span(from)?; + + return Ok(Self::Custom); + } + + if !from.params().is_empty() { + return Err(Error::MainNoInputs).with_span(from); + } + if let Some(aliased) = from.ret() { + let resolved = scope.resolve(aliased).with_span(from)?; + if !resolved.is_unit() { + return Err(Error::MainNoOutput).with_span(from); + } + } + + scope.push_main_scope(); + let body = Expression::analyze(from.body(), ty, scope)?; + scope.pop_main_scope(); + Ok(Self::Main(body)) + } +} +*/ + impl AbstractSyntaxTree for Statement { type From = parse::Statement; diff --git a/src/lexer.rs b/src/lexer.rs index 71c004b6..d2006922 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, @@ -63,6 +66,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"), @@ -134,6 +140,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, diff --git a/src/parse.rs b/src/parse.rs index f47dda5e..b2f0b7bc 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, // TODO: Maybe change to Arc<[Identifier]> for consisten + 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: 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,7 @@ 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 +1267,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 +1279,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 +1316,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 +1333,48 @@ impl ChumskyParse for Function { } } +impl ChumskyParse for UseDecl { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + // TODO: Check does it possible to parse `use a: : smth`, because we need parse only `use a::smth` + let double_colon = just(Token::Colon).then(just(Token::Colon)).labelled("::"); + + let visibility = + just(Token::Pub).to(Visibility::Public) + .or_not() + .map(|v| v.unwrap_or(Visibility::Private)); + + let path = Identifier::parser() + .then_ignore(double_colon) + .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,7 +1503,7 @@ 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 double_colon: chumsky::label::Labelled, I, extra::Full>, chumsky::primitive::Just, I, extra::Full>, Token<'_>, Token<'_>, extra::Full>, &str> = just(Token::Colon).then(just(Token::Colon)).labelled("::"); let turbofish_start = double_colon.clone().then(just(Token::LAngle)).ignored(); @@ -1461,14 +1617,21 @@ 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) + + 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(|(name, ty), e| Self { + .then_ignore(just(Token::Semi))) + .map_with(|(visibility, (name, ty)), e| Self { + visibility, name: name.0, ty, span: e.span(), @@ -1953,6 +2116,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 +2125,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 800315df56d21c139fb71c019527fb1b9b3233af Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Wed, 11 Feb 2026 14:14:59 +0200 Subject: [PATCH 2/7] feat: add the option CLI argument --lib and driver file --- Cargo.lock | 322 +++++++++++++++++++++++++++++++++++-- Cargo.toml | 5 +- examples/modules/main.simf | 7 +- src/driver.rs | 317 ++++++++++++++++++++++++++++++++++++ src/lib.rs | 38 ++++- src/main.rs | 47 +++++- src/parse.rs | 39 +++-- src/str.rs | 6 + src/tracker.rs | 4 +- src/witness.rs | 4 +- 10 files changed, 736 insertions(+), 53 deletions(-) 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 c3a8cc30..4634a8bf 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"] } @@ -152,7 +155,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/examples/modules/main.simf b/examples/modules/main.simf index 857e2fc7..ae05f4e4 100644 --- a/examples/modules/main.simf +++ b/examples/modules/main.simf @@ -1,9 +1,10 @@ -//pub use temp::get_five; +pub use temp::get_five::get_five; -fn get_five() -> u32 { +fn seven() -> u32 { 7 } fn main() { - let five: u32 = get_five(); + let five: u32 = dbg!(get_five()); + let seven: u32 = dbg!(seven()); } \ No newline at end of file diff --git a/src/driver.rs b/src/driver.rs new file mode 100644 index 00000000..58dc6db6 --- /dev/null +++ b/src/driver.rs @@ -0,0 +1,317 @@ +use std::collections::{HashMap, VecDeque}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use crate::error::ErrorCollector; +use crate::parse::{self, Item, ParseFromStrWithErrors, Program, UseDecl}; +use crate::LibConfig; + +/// Graph Node: One file = One module +#[derive(Debug, Clone)] +pub struct Module { + /// Parsed AST (your `parse::Program`) + /// Using Option to first create the node, then add the AST + pub parsed_program: Program, +} + +/// The Dependency Graph itself +pub struct ProjectGraph { + /// Arena Pattern: the data itself lies here. Vector guarantees data lives in one place. + pub modules: Vec, + + /// Fast lookup: Path -> ID + /// Solves the duplicate problem (so as not to parse a.simf twice) + pub lookup: HashMap, + + /// Adjacency list: Who depends on whom + pub dependencies: HashMap>, +} + +fn get_full_path( + library_map: &HashMap, + use_decl: &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) = library_map.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, + )) +} + +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: &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 Item::Use(use_decl) = elem { + if let Ok(path) = get_full_path(&lib_cfg.libraries, 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 + // (Assuming your parser works via a helper function) + fn parse_root(path: &Path) -> 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(); // Empty map + + 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 a9e5bc0e..459ad2ea 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,29 @@ 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, + } + } +} + /// The template of a SimplicityHL program. /// /// A template has parameterized values that need to be supplied with arguments. @@ -51,11 +71,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, @@ -115,11 +142,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)) } @@ -186,12 +214,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) } @@ -294,7 +323,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}"), }; @@ -631,6 +660,7 @@ fn main() { } "#; match SatisfiedProgram::new( + None, prog_text, Arguments::default(), WitnessValues::default(), diff --git a/src/main.rs b/src/main.rs index fdd090cc..f6690f2d 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::{Arguments, CompiledProgram}; -use std::{env, fmt}; +use simplicityhl::{Arguments, CompiledProgram, LibConfig}; +use std::{collections::HashMap, env, fmt, path::PathBuf}; #[cfg_attr(feature = "serde", derive(serde::Serialize))] /// The compilation output. @@ -41,6 +41,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") .value_name("WITNESS_FILE") @@ -69,14 +77,37 @@ fn main() -> Result<(), Box> { let include_debug_symbols = matches.get_flag("debug"); let output_json = matches.get_flag("json"); - let compiled = - match CompiledProgram::new(prog_text, Arguments::default(), include_debug_symbols) { - Ok(program) => program, - Err(e) => { - eprintln!("{}", e); + 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, + Arguments::default(), + 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 b2f0b7bc..919f751c 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -63,7 +63,7 @@ pub enum Item { #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct UseDecl { visibility: Visibility, - path: Vec, // TODO: Maybe change to Arc<[Identifier]> for consisten + path: Vec, // TODO: Maybe change to Arc<[Identifier]> for consisten items: UseItems, span: Span, } @@ -1243,7 +1243,12 @@ impl ChumskyParse for Program { let skip_until_next_item = any() .then( any() - .filter(|t| !matches!(t, Token::Pub | Token::Use | 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 @@ -1279,8 +1284,8 @@ impl ChumskyParse for Function { where I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, { - let visibility = - just(Token::Pub).to(Visibility::Public) + let visibility = just(Token::Pub) + .to(Visibility::Public) .or_not() .map(|v| v.unwrap_or(Visibility::Private)) .labelled("function visibility"); @@ -1341,8 +1346,8 @@ impl ChumskyParse for UseDecl { // TODO: Check does it possible to parse `use a: : smth`, because we need parse only `use a::smth` let double_colon = just(Token::Colon).then(just(Token::Colon)).labelled("::"); - let visibility = - just(Token::Pub).to(Visibility::Public) + let visibility = just(Token::Pub) + .to(Visibility::Public) .or_not() .map(|v| v.unwrap_or(Visibility::Private)); @@ -1503,7 +1508,7 @@ impl ChumskyParse for CallName { where I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, { - let double_colon: chumsky::label::Labelled, I, extra::Full>, chumsky::primitive::Just, I, extra::Full>, Token<'_>, Token<'_>, extra::Full>, &str> = just(Token::Colon).then(just(Token::Colon)).labelled("::"); + let double_colon = just(Token::Colon).then(just(Token::Colon)).labelled("::"); let turbofish_start = double_colon.clone().then(just(Token::LAngle)).ignored(); @@ -1617,19 +1622,21 @@ impl ChumskyParse for TypeAlias { where I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, { - let visibility = - just(Token::Pub).to(Visibility::Public) + 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())); - + visibility - .then(just(Token::Type) - .ignore_then(name) - .then_ignore(parse_token_with_recovery(Token::Eq)) - .then(AliasedType::parser()) - .then_ignore(just(Token::Semi))) + .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, diff --git a/src/str.rs b/src/str.rs index 71190b69..7869cfb8 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 031b3a71..ca91d85c 100644 --- a/src/witness.rs +++ b/src/witness.rs @@ -235,7 +235,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`", @@ -254,7 +254,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 c4a78a909c147f135b8e8609ee754e7954d68945 Mon Sep 17 00:00:00 2001 From: Sdoba16 Date: Wed, 11 Feb 2026 17:21:29 +0200 Subject: [PATCH 3/7] Added C3 linearization --- src/driver.rs | 184 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) diff --git a/src/driver.rs b/src/driver.rs index 58dc6db6..8ddb5b20 100644 --- a/src/driver.rs +++ b/src/driver.rs @@ -27,6 +27,12 @@ pub struct ProjectGraph { pub dependencies: HashMap>, } +#[derive(Debug)] +pub enum C3Error { + CycleDetected(Vec), + InconsistentLinearization { module: usize }, +} + fn get_full_path( library_map: &HashMap, use_decl: &UseDecl, @@ -118,6 +124,91 @@ 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)] @@ -180,6 +271,26 @@ 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: @@ -231,6 +342,47 @@ 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: @@ -267,6 +419,37 @@ 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: @@ -314,4 +497,5 @@ mod tests { "Root should have no resolved dependencies" ); } + } From 673e287c179cf1f5addd7f40e8761d2d7696a722 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Mon, 16 Feb 2026 17:31:10 +0200 Subject: [PATCH 4/7] feat: add build_order function, refactor and test it --- examples/modules/main.simf | 2 +- .../temp/{get_five.simf => funcs.simf} | 0 src/ast.rs | 10 +- src/driver.rs | 411 +++++++++++++++--- src/lib.rs | 29 +- src/parse.rs | 4 + src/str.rs | 19 + 7 files changed, 405 insertions(+), 70 deletions(-) rename examples/modules/temp/{get_five.simf => funcs.simf} (100%) diff --git a/examples/modules/main.simf b/examples/modules/main.simf index ae05f4e4..c5c60e9c 100644 --- a/examples/modules/main.simf +++ b/examples/modules/main.simf @@ -1,4 +1,4 @@ -pub use temp::get_five::get_five; +pub use temp::funcs::get_five; fn seven() -> u32 { 7 diff --git a/examples/modules/temp/get_five.simf b/examples/modules/temp/funcs.simf similarity index 100% rename from examples/modules/temp/get_five.simf rename to examples/modules/temp/funcs.simf diff --git a/src/ast.rs b/src/ast.rs index 6eda96d5..2a6d08bd 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -11,7 +11,7 @@ use simplicity::jet::Elements; use crate::debug::{CallTracker, DebugSymbols, TrackedCallName}; use crate::error::{Error, RichError, Span, WithSpan}; use crate::num::{NonZeroPow2Usize, Pow2Usize}; -use crate::parse::{MatchPattern, UseDecl}; +use crate::parse::{MatchPattern, UseDecl, Visibility}; use crate::pattern::Pattern; use crate::str::{AliasName, FunctionName, Identifier, ModuleName, WitnessName}; use crate::types::{ @@ -307,6 +307,7 @@ pub enum CallName { /// Definition of a custom function. #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct CustomFunction { + visibility: Visibility, params: Arc<[FunctionParam]>, body: Arc, } @@ -780,6 +781,7 @@ impl AbstractSyntaxTree for Function { assert!(scope.is_topmost(), "Items live in the topmost scope only"); if from.name().as_inner() != "main" { + let visibility = from.visibility().clone(); let params = from .params() .iter() @@ -803,7 +805,11 @@ impl AbstractSyntaxTree for Function { let body = Expression::analyze(from.body(), &ret, scope).map(Arc::new)?; scope.pop_scope(); debug_assert!(scope.is_topmost()); - let function = CustomFunction { params, body }; + let function = CustomFunction { + visibility, + params, + body, + }; scope .insert_function(from.name().clone(), function) .with_span(from)?; diff --git a/src/driver.rs b/src/driver.rs index 8ddb5b20..33a20b22 100644 --- a/src/driver.rs +++ b/src/driver.rs @@ -2,57 +2,52 @@ use std::collections::{HashMap, VecDeque}; use std::path::{Path, PathBuf}; use std::sync::Arc; -use crate::error::ErrorCollector; -use crate::parse::{self, Item, ParseFromStrWithErrors, Program, UseDecl}; +use crate::error::{ErrorCollector, Span}; +use crate::parse::{self, ParseFromStrWithErrors, Visibility}; +use crate::str::Identifier; use crate::LibConfig; /// Graph Node: One file = One module #[derive(Debug, Clone)] -pub struct Module { +struct Module { /// Parsed AST (your `parse::Program`) /// Using Option to first create the node, then add the AST - pub parsed_program: Program, + pub parsed_program: parse::Program, } /// The Dependency Graph itself pub struct ProjectGraph { /// Arena Pattern: the data itself lies here. Vector guarantees data lives in one place. - pub modules: Vec, + pub(self) modules: Vec, /// Fast lookup: Path -> ID /// Solves the duplicate problem (so as not to parse a.simf twice) + pub config: Arc, pub lookup: HashMap, + pub paths: Vec, /// Adjacency list: Who depends on whom 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), InconsistentLinearization { module: usize }, } -fn get_full_path( - library_map: &HashMap, - use_decl: &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) = library_map.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, - )) -} - 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(); @@ -66,15 +61,16 @@ fn parse_and_get_program(prog_file: &Path) -> Result { } impl ProjectGraph { - pub fn new(lib_cfg: &LibConfig, root_program: &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 @@ -86,8 +82,8 @@ impl ProjectGraph { let current_program = &modules[curr_id].parsed_program; for elem in current_program.items() { - if let Item::Use(use_decl) = elem { - if let Ok(path) = get_full_path(&lib_cfg.libraries, use_decl) { + if let parse::Item::Use(use_decl) = elem { + if let Ok(path) = config.get_full_path(use_decl) { pending_imports.push(path); } } @@ -112,6 +108,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); @@ -120,7 +117,9 @@ impl ProjectGraph { Ok(Self { modules, + config, lookup, + paths, dependencies, }) } @@ -148,9 +147,7 @@ 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(), - )); + return Err(C3Error::CycleDetected(visiting[cycle_start..].to_vec())); } visiting.push(module); @@ -167,8 +164,7 @@ impl ProjectGraph { 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 })?; result.extend(merged); @@ -177,6 +173,121 @@ impl ProjectGraph { Ok(result) } + + // TODO: @Sdoba16 to implement + // fn build_ordering(&self) {} + + 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: Change. Consider processing more than one errro 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: Resolve errors more appropriately + let mut order = self.c3_linearize().unwrap(); + order.reverse(); + // self.build_ordering(); + self.build_program(&order) + } } fn merge(mut seqs: Vec>) -> Option> { @@ -219,8 +330,7 @@ mod tests { use std::path::Path; use tempfile::TempDir; - // --- Helper to setup environment --- - + // ProjectGraph::new tests // 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); @@ -238,10 +348,192 @@ mod tests { // Helper to mock the initial root program parsing // (Assuming your parser works via a helper function) - fn parse_root(path: &Path) -> Program { + fn parse_root(path: &Path) -> parse::Program { 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" + ); + + // Optional: Verify the error message contains relevant info + // let err = result.err().unwrap(); + // assert!(err.to_string().to_lowercase().contains("private")); + } + + /* + #[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;"), + ]); + + let id_a = *ids.get("A.simf").unwrap(); + let id_root = *ids.get("main.simf").unwrap(); + let order = vec![id_a, id_root]; + + let program = graph.build_program(&order).expect("Failed to build program"); + let scope = &program.scope_items[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_simple_import() { // Setup: @@ -258,10 +550,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"); @@ -273,18 +565,16 @@ 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;"); + 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 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"); @@ -321,8 +611,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) @@ -372,17 +662,14 @@ 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"); - assert_eq!( - order, vec![0, 1, 2, 3], - ); + assert_eq!(order, vec![0, 1, 2, 3],); } - #[test] fn test_cyclic_dependency() { // Setup: @@ -406,11 +693,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 @@ -442,14 +727,13 @@ 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(_)); } - #[test] fn test_missing_file_error() { // Setup: @@ -463,8 +747,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(); @@ -487,9 +771,9 @@ mod tests { let lib_map = HashMap::new(); // Empty map 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!( @@ -497,5 +781,4 @@ mod tests { "Root should have no resolved dependencies" ); } - } diff --git a/src/lib.rs b/src/lib.rs index 459ad2ea..119eeff9 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, @@ -54,6 +55,23 @@ impl LibConfig { root_path, } } + + 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]; + + 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. @@ -77,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 919f751c..71ed2dba 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 7869cfb8..98d5aa5f 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 078257abbe79caf812875da364330c93c6bcf833 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Fri, 20 Feb 2026 14:26:09 +0200 Subject: [PATCH 5/7] feat: add tests and modules to ast, change architecture --- examples/modules/main.simf | 10 - 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 + .../{modules => single_lib}/temp/funcs.simf | 2 + examples/single_lib/temp/two.simf | 5 + src/ast.rs | 190 +++---- src/driver.rs | 513 +++++++++++++++--- src/error.rs | 11 + src/lib.rs | 193 +++++-- src/main.rs | 10 +- src/parse.rs | 29 +- src/tracker.rs | 15 +- src/witness.rs | 24 +- 15 files changed, 793 insertions(+), 240 deletions(-) delete mode 100644 examples/modules/main.simf 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 rename examples/{modules => single_lib}/temp/funcs.simf (60%) create mode 100644 examples/single_lib/temp/two.simf diff --git a/examples/modules/main.simf b/examples/modules/main.simf deleted file mode 100644 index c5c60e9c..00000000 --- a/examples/modules/main.simf +++ /dev/null @@ -1,10 +0,0 @@ -pub use temp::funcs::get_five; - -fn seven() -> u32 { - 7 -} - -fn main() { - let five: u32 = dbg!(get_five()); - let seven: u32 = dbg!(seven()); -} \ No newline at end of file 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/modules/temp/funcs.simf b/examples/single_lib/temp/funcs.simf similarity index 60% rename from examples/modules/temp/funcs.simf rename to examples/single_lib/temp/funcs.simf index e5def94a..0ff2da55 100644 --- a/examples/modules/temp/funcs.simf +++ b/examples/single_lib/temp/funcs.simf @@ -1,3 +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 2a6d08bd..19d04052 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -9,9 +9,10 @@ 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, UseDecl, Visibility}; +use crate::parse::MatchPattern; use crate::pattern::Pattern; use crate::str::{AliasName, FunctionName, Identifier, ModuleName, WitnessName}; use crate::types::{ @@ -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. /// @@ -73,8 +74,6 @@ pub enum Item { TypeAlias, /// A function. Function(Function), - /// A use declaration - Use(UseDecl), /// A module, which is ignored. Module, } @@ -307,7 +306,6 @@ pub enum CallName { /// Definition of a custom function. #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct CustomFunction { - visibility: Visibility, params: Arc<[FunctionParam]>, body: Arc, } @@ -525,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, @@ -535,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() @@ -545,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 @@ -567,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 +725,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 +780,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,103 +809,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(_) => { - println!("WARN: Skipping use declaration (not implemented yet)"); - Ok(Self::Module) - //todo!() - //Use::analyze(use_declaration).map(Self::Use) - } - parse::Item::Module => Ok(Self::Module), + driver::Item::Module => Ok(Self::Module), } } } impl AbstractSyntaxTree for Function { - type From = parse::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"); - - if from.name().as_inner() != "main" { - let visibility = from.visibility().clone(); - let params = from - .params() - .iter() - .map(|param| { - let identifier = param.identifier().clone(); - let ty = scope.resolve(param.ty())?; - Ok(FunctionParam { identifier, ty }) - }) - .collect::, Error>>() - .with_span(from)?; - let ret = from - .ret() - .as_ref() - .map(|aliased| scope.resolve(aliased).with_span(from)) - .transpose()? - .unwrap_or_else(ResolvedType::unit); - scope.push_scope(); - 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(); - debug_assert!(scope.is_topmost()); - let function = CustomFunction { - visibility, - params, - body, - }; - scope - .insert_function(from.name().clone(), function) - .with_span(from)?; - - return Ok(Self::Custom); - } - - if !from.params().is_empty() { - return Err(Error::MainNoInputs).with_span(from); - } - if let Some(aliased) = from.ret() { - let resolved = scope.resolve(aliased).with_span(from)?; - if !resolved.is_unit() { - return Err(Error::MainNoOutput).with_span(from); - } - } - - scope.push_main_scope(); - let body = Expression::analyze(from.body(), ty, scope)?; - scope.pop_main_scope(); - Ok(Self::Main(body)) - } -} - -/* -impl AbstractSyntaxTree for UseDecl { - type From = parse::UseDecl; + 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() @@ -859,12 +856,12 @@ impl AbstractSyntaxTree for UseDecl { .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 @@ -890,7 +887,6 @@ impl AbstractSyntaxTree for UseDecl { Ok(Self::Main(body)) } } -*/ impl AbstractSyntaxTree for Statement { type From = parse::Statement; @@ -1390,14 +1386,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() @@ -1408,11 +1399,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() @@ -1423,11 +1410,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 @@ -1503,6 +1486,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 33a20b22..8d1e45ea 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}; /// Graph Node: One file = One module #[derive(Debug, Clone)] @@ -22,26 +24,255 @@ pub struct ProjectGraph { /// Fast lookup: Path -> ID /// Solves the duplicate problem (so as not to parse a.simf twice) - pub config: Arc, - pub lookup: HashMap, - pub paths: Vec, + //pub config: Arc, + pub libraries: Arc, + pub lookup: HashMap, + pub paths: Arc<[SourceName]>, /// Adjacency list: Who depends on whom pub dependencies: HashMap>, } -#[derive(Clone, Debug)] +// TODO: 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, file_id); + 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 { + file_id: usize, // NOTE: Maybe don't need + 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, file_id: usize) -> Self { + Self { + file_id, + name: parsed.name().clone(), + ty: parsed.ty().clone(), + span: *parsed.as_ref(), + } + } + + /// Access the visibility of the alias. + pub fn file_id(&self) -> usize { + self.file_id + } + + /// 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; file_id, name, ty); + #[derive(Debug)] pub enum C3Error { CycleDetected(Vec), @@ -61,16 +292,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 @@ -83,7 +319,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); } } @@ -91,12 +327,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; } @@ -107,8 +344,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); @@ -117,9 +354,9 @@ impl ProjectGraph { Ok(Self { modules, - config, + libraries, lookup, - paths, + paths: paths.into(), dependencies, }) } @@ -178,14 +415,14 @@ impl ProjectGraph { // fn build_ordering(&self) {} 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!( @@ -194,7 +431,7 @@ impl ProjectGraph { )); } - scope_items[file_id].insert( + resolutions[file_id].insert( elem.clone(), Resolution { visibility: use_decl_visibility, @@ -205,26 +442,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: Change. Consider processing more than one errro at a time + // TODO: Change. 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(); @@ -232,8 +470,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() { @@ -243,7 +482,7 @@ impl ProjectGraph { for target in use_targets { ProjectGraph::process_use_item( - &mut scope_items, + &mut resolutions, file_id, ind, target, @@ -254,20 +493,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 => {} } @@ -276,7 +517,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(), }) } @@ -322,8 +564,135 @@ 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 {} [file_id: {}] = {};", + self.name(), + self.file_id(), + 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; @@ -332,7 +701,7 @@ mod tests { // ProjectGraph::new tests // 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 @@ -380,13 +749,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); } @@ -410,7 +783,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 @@ -451,7 +824,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"); @@ -464,7 +837,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"); @@ -527,7 +900,7 @@ mod tests { let order = vec![id_a, id_root]; let program = graph.build_program(&order).expect("Failed to build program"); - let scope = &program.scope_items[id_root]; + 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"); @@ -550,10 +923,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"); @@ -573,8 +947,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"); @@ -611,8 +986,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) @@ -662,8 +1038,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"); @@ -693,8 +1070,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"); @@ -727,8 +1105,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(_)); @@ -747,8 +1126,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(); @@ -771,9 +1150,9 @@ mod tests { let lib_map = HashMap::new(); // Empty map 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 c06cc90b..b09a74ea 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), @@ -494,6 +497,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" @@ -514,6 +521,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 119eeff9..e52af784 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, @@ -165,12 +175,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)) } @@ -237,13 +248,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) } @@ -342,11 +355,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}"), }; @@ -386,13 +417,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()) } @@ -495,6 +582,29 @@ 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(); + } + + // Demonstration of functionality for the user + #[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") @@ -683,7 +793,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 f6690f2d..69ec1cd7 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::{Arguments, CompiledProgram, LibConfig}; -use std::{collections::HashMap, env, fmt, path::PathBuf}; +use simplicityhl::{Arguments, CompiledProgram, LibTable, SourceName}; +use std::{env, fmt, sync::Arc}; #[cfg_attr(feature = "serde", derive(serde::Serialize))] /// The compilation output. @@ -79,7 +79,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(); @@ -95,9 +95,9 @@ fn main() -> Result<(), Box> { }) .collect(); - let config = LibConfig::new(library_map, prog_path); let compiled = match CompiledProgram::new( - Some(&config), + SourceName::Real(prog_path.to_path_buf()), + Arc::from(libraries), prog_text, Arguments::default(), include_debug_symbols, diff --git a/src/parse.rs b/src/parse.rs index 71ed2dba..a84f4f70 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 ca91d85c..552e0fcf 100644 --- a/src/witness.rs +++ b/src/witness.rs @@ -210,7 +210,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() { @@ -218,7 +219,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}"), @@ -235,7 +238,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`", @@ -254,7 +264,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 5cbe3675c13f1409570bc7707ffab7d4a96de7cb Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Mon, 23 Feb 2026 16:38:25 +0200 Subject: [PATCH 6/7] test: add simple tests for module flow --- src/lib.rs | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 138 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index e52af784..01d81a56 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -583,6 +583,14 @@ pub(crate) mod tests { } // Real test cases + #[test] + fn some_sort_of_test() { + let (test, _dir) = TestCase::temp_env("fn main() { help(); }\nfn help() {}", Vec::new()); + + test.with_witness_values(WitnessValues::default()) + .assert_run_success(); + } + #[test] fn module_simple() { let (test, _dir) = TestCase::temp_env( @@ -594,7 +602,136 @@ pub(crate) mod tests { .assert_run_success(); } - // Demonstration of functionality for the user + #[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] + 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; + use temp::level1::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; pub use temp::level3::core_val;", + ), + ( + "temp", + "temp/level1.simf", + "pub use temp::level2::CoreSmth; pub use temp::level2::core_val;", + ), + ]; + + let (test, _dir) = TestCase::temp_env(main_code, libs); + test.with_witness_values(WitnessValues::default()) + .assert_run_success(); + } + + #[test] + #[should_panic] + 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] + #[should_panic] // 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( From c5693a2c42dcf2351137b75b07c9aa80c6f3429d Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Tue, 24 Feb 2026 17:12:28 +0200 Subject: [PATCH 7/7] feat: add filename to errors --- src/ast.rs | 2 +- src/driver.rs | 382 +++++++++++++++++++++---------------------------- src/error.rs | 105 +++++++++----- src/lib.rs | 118 ++++++++++----- src/main.rs | 2 +- src/witness.rs | 4 +- 6 files changed, 324 insertions(+), 289 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index 19d04052..5da07c4f 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -745,7 +745,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 8d1e45ea..ead193b4 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}; /// Graph Node: One file = One module #[derive(Debug, Clone)] @@ -281,8 +281,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) @@ -293,11 +297,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(), }]; @@ -327,7 +332,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)); @@ -411,9 +416,6 @@ impl ProjectGraph { Ok(result) } - // TODO: @Sdoba16 to implement - // fn build_ordering(&self) {} - fn process_use_item( resolutions: &mut [FileResolutions], file_id: usize, @@ -421,10 +423,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() @@ -471,7 +474,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(); @@ -721,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'"); - let root_p = root_path.expect("main.simf must be defined in file list"); + // 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")); + + // 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) @@ -910,50 +914,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] @@ -962,202 +960,148 @@ 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"); + 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;"), + ]); - assert_eq!(graph.modules.len(), 2, "Should only have A and B"); + let a_id = ids["A"]; + let b_id = ids["B"]; - // A depends on B - assert!(graph.dependencies[&0].contains(&1)); - // B depends on A (Circular) - assert!(graph.dependencies[&1].contains(&0)); + // Check if graph correctly recorded the cycle + assert!( + graph.dependencies[&a_id].contains(&b_id), + "A should depend on B" + ); + assert!( + graph.dependencies[&b_id].contains(&a_id), + "B should depend on A" + ); } #[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;", - ); + 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;"), + ]); - let mut lib_map = HashMap::new(); - lib_map.insert("test".to_string(), temp_dir.path().join("libs/test")); + let result = graph.c3_linearize(); + assert!(matches!(result, Err(C3Error::CycleDetected(_)))); + } - 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"); + #[test] + fn test_ignores_unmapped_imports() { + // Setup: root imports from "unknown", which is not in our lib_map + let (graph, ids, _dir) = setup_graph(vec![("main.simf", "use unknown::library;")]); - let order = graph.c3_linearize().unwrap_err(); - matches!(order, C3Error::CycleDetected(_)); + assert_eq!(graph.modules.len(), 1, "Should only contain root"); + assert!(graph.dependencies[&ids["main"]].is_empty()); } + /* #[test] fn test_missing_file_error() { - // Setup: - // root -> imports missing_lib + // 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(), "root.simf", "use std::ghost;"); - // We do NOT create ghost.simf + let root_path = create_simf_file(temp_dir.path(), "main.simf", "use lib::ghost;"); + // We purposefully DO NOT create ghost.simf let mut lib_map = HashMap::new(); - lib_map.insert("std".to_string(), temp_dir.path().join("libs/std")); + 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 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(); - assert!( - err_msg.contains("does not exist"), - "Error message should mention missing file" + let source_name = SourceName::Real(Arc::from(root_path)); + let source = SourceFile::new(source_name, Arc::from("")); + let mut handler = ErrorCollector::new(source.clone()); + + let result = ProjectGraph::new( + source, + Arc::from(lib_map), + &root_program, + &mut handler ); - } - #[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(); // Empty map + assert!(result.is_none(), "Graph construction should fail"); + assert!(!handler.get().is_empty()); - 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"); - - assert_eq!(graph.modules.len(), 1, "Should only contain root"); - assert!( - graph.dependencies[&0].is_empty(), - "Root should have no resolved dependencies" - ); + // Optional: Check error message text + // let errors = handler.into_errors(); + // assert!(errors[0].to_string().contains("File not found")); } + */ } diff --git a/src/error.rs b/src/error.rs index b09a74ea..7401a8cd 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), @@ -446,6 +459,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}" @@ -632,6 +649,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; @@ -639,47 +660,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()); } @@ -697,9 +732,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 01d81a56..d1121284 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))? @@ -149,10 +209,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(), }) @@ -476,7 +536,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) @@ -583,14 +643,6 @@ pub(crate) mod tests { } // Real test cases - #[test] - fn some_sort_of_test() { - let (test, _dir) = TestCase::temp_env("fn main() { help(); }\nfn help() {}", Vec::new()); - - test.with_witness_values(WitnessValues::default()) - .assert_run_success(); - } - #[test] fn module_simple() { let (test, _dir) = TestCase::temp_env( diff --git a/src/main.rs b/src/main.rs index 69ec1cd7..9089eee9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -96,7 +96,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, Arguments::default(), diff --git a/src/witness.rs b/src/witness.rs index 552e0fcf..91197803 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; @@ -138,7 +138,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 } }