diff --git a/src/ast.rs b/src/ast.rs index adf58ff..e7193b5 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1,5 +1,5 @@ use std::collections::hash_map::Entry; -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use std::num::NonZeroUsize; use std::str::FromStr; use std::sync::Arc; @@ -9,6 +9,7 @@ use miniscript::iter::{Tree, TreeLike}; use simplicity::jet::Elements; use crate::debug::{CallTracker, DebugSymbols, TrackedCallName}; +use crate::driver::resolve_order::{AliasRegistry, ItemNameWithFileId}; use crate::error::{Error, RichError, Span, WithSpan}; use crate::num::{NonZeroPow2Usize, Pow2Usize}; use crate::parse::MatchPattern; @@ -19,7 +20,7 @@ use crate::types::{ }; use crate::value::{UIntValue, Value}; use crate::witness::{Parameters, WitnessTypes, WitnessValues}; -use crate::{impl_eq_hash, parse}; +use crate::{driver, impl_eq_hash, parse}; /// A program consists of the main function. /// @@ -520,18 +521,47 @@ impl TreeLike for ExprTree<'_> { /// 2. Resolving type aliases /// 3. Assigning types to each witness expression /// 4. Resolving calls to custom functions -#[derive(Clone, Debug, Eq, PartialEq, Default)] +#[derive(Clone, Debug, Eq, PartialEq)] struct Scope { + resolutions: Arc<[BTreeSet>]>, + import_aliases: AliasRegistry, + file_id: usize, + variables: Vec>, aliases: HashMap, parameters: HashMap, witnesses: HashMap, - functions: HashMap, + functions: HashMap, is_main: bool, call_tracker: CallTracker, } +impl Default for Scope { + fn default() -> Self { + Self::new(Arc::from([]), AliasRegistry::default()) + } +} + impl Scope { + pub fn new(resolutions: Arc<[BTreeSet>]>, import_aliases: AliasRegistry) -> Self { + Self { + resolutions, + import_aliases, + 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(), + } + } + + pub fn file_id(&self) -> usize { + self.file_id + } + /// Check if the current scope is topmost. pub fn is_topmost(&self) -> bool { self.variables.is_empty() @@ -542,6 +572,11 @@ impl Scope { self.variables.push(HashMap::new()); } + pub fn push_function_scope(&mut self, file_id: usize) { + self.push_scope(); + self.file_id = file_id; + } + /// Push the scope of the main function onto the stack. /// /// ## Panics @@ -564,6 +599,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 @@ -682,20 +722,66 @@ impl Scope { pub fn insert_function( &mut self, name: FunctionName, + file_id: usize, function: CustomFunction, ) -> Result<(), Error> { - match self.functions.entry(name.clone()) { - Entry::Occupied(_) => Err(Error::FunctionRedefined(name)), - Entry::Vacant(entry) => { - entry.insert(function); - Ok(()) - } + let global_id = (Arc::from(name.as_inner()), file_id); + + if self.functions.contains_key(&global_id) { + return Err(Error::FunctionRedefined(name)); } + + let _ = self.functions.insert(global_id, function); + Ok(()) } - /// Get the definition of a custom function. - pub fn get_function(&self, name: &FunctionName) -> Option<&CustomFunction> { - self.functions.get(name) + // NOTE: Why do we use this function to retrieve a `TypeAlias`? + + /// Retrieves the definition of a custom function, enforcing strict error prioritization. + /// + /// # Architecture Note + /// The order of operations here is intentional to prioritize specific compiler errors: + /// 1. Resolve the alias to find the true global coordinates. + /// 2. Check for global existence (`FunctionUndefined`) *before* checking local visibility. + /// 3. Verify if the current file's scope is actually allowed to see it (`PrivateItem`). + /// + /// # Errors + /// + /// * [`Error::FunctionUndefined`]: The function is not found in the global registry. + /// * [`Error::Internal`]: The specified `file_id` does not exist in the `files`. + /// * [`Error::PrivateItem`]: The function exists globally but is not exposed to the current file's scope. + pub fn get_function(&self, name: &FunctionName) -> Result<&CustomFunction, Error> { + // 1. Get the true global ID of the alias (or keep the current name if it is not aliased). + let initial_id = (Arc::from(name.as_inner()), self.file_id); + let global_id = self + .import_aliases + .resolved_roots() + .get(&initial_id) + .cloned() + .unwrap_or(initial_id); + + // 2. Fetch the function from the global pool. + // We do this first so we can throw FunctionUndefined before checking local visibility. + let function = self + .functions + .get(&global_id) + .ok_or_else(|| Error::FunctionUndefined(name.clone()))?; + + // TODO: Consider changing it to a better error handler with a source file. + let file_scope = self.resolutions.get(self.file_id).ok_or_else(|| { + Error::Internal(format!( + "file_id {} not found inside current Scope files", + self.file_id + )) + })?; + + // 3. Verify local scope visibility. + // We successfully found the function globally, but is this file allowed to use it? + if file_scope.contains(&Arc::from(name.as_inner())) { + Ok(function) + } else { + Err(Error::PrivateItem(name.as_inner().to_string())) + } } /// Track a call expression with its span. @@ -718,9 +804,10 @@ trait AbstractSyntaxTree: Sized { } impl Program { - pub fn analyze(from: &parse::Program) -> Result { + pub fn analyze(from: &driver::resolve_order::Program) -> Result { let unit = ResolvedType::unit(); - let mut scope = Scope::default(); + let mut scope = Scope::new(Arc::from(from.resolutions()), from.import_aliases().clone()); + let items = from .items() .iter() @@ -732,6 +819,7 @@ impl Program { Item::Function(Function::Main(expr)) => Some(expr), _ => None, }); + let main = iter.next().ok_or(Error::MainRequired).with_span(from)?; if iter.next().is_some() { return Err(Error::FunctionRedefined(FunctionName::main())).with_span(from); @@ -777,8 +865,10 @@ impl AbstractSyntaxTree for Function { fn analyze(from: &Self::From, ty: &ResolvedType, scope: &mut Scope) -> Result { assert!(ty.is_unit(), "Function definitions cannot return anything"); assert!(scope.is_topmost(), "Items live in the topmost scope only"); + let previous_file_id = scope.file_id(); if from.name().as_inner() != "main" { + let file_id = from.file_id(); let params = from .params() .iter() @@ -795,16 +885,16 @@ impl AbstractSyntaxTree for Function { .map(|aliased| scope.resolve(aliased).with_span(from)) .transpose()? .unwrap_or_else(ResolvedType::unit); - scope.push_scope(); + scope.push_function_scope(file_id); for param in params.iter() { scope.insert_variable(param.identifier().clone(), param.ty().clone()); } let body = Expression::analyze(from.body(), &ret, scope).map(Arc::new)?; - scope.pop_scope(); + scope.pop_function_scope(previous_file_id); debug_assert!(scope.is_topmost()); let function = CustomFunction { params, body }; scope - .insert_function(from.name().clone(), function) + .insert_function(from.name().clone(), file_id, function) .with_span(from)?; return Ok(Self::Custom); @@ -1325,14 +1415,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() @@ -1343,11 +1428,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() @@ -1358,11 +1439,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 diff --git a/src/driver/mod.rs b/src/driver/mod.rs index 72e036e..e168996 100644 --- a/src/driver/mod.rs +++ b/src/driver/mod.rs @@ -28,7 +28,7 @@ //! the dependency graph construction. mod linearization; -mod resolve_order; +pub(crate) mod resolve_order; use std::collections::{HashMap, HashSet, VecDeque}; use std::path::PathBuf; diff --git a/src/driver/resolve_order.rs b/src/driver/resolve_order.rs index 079011d..4abb994 100644 --- a/src/driver/resolve_order.rs +++ b/src/driver/resolve_order.rs @@ -5,7 +5,6 @@ use crate::driver::DependencyGraph; use crate::error::{Error, ErrorCollector, RichError, Span}; use crate::impl_eq_hash; use crate::parse::{self, AliasedIdentifier, Visibility}; -use crate::resolution::CanonPath; /// The final, flattened representation of a SimplicityHL program. /// @@ -16,8 +15,9 @@ pub struct Program { /// The linear sequence of compiled items (`Functions`, `TypeAliases`, etc.). items: Arc<[parse::Item]>, - /// The files that make up this program, along with their scoping rules. - files: Arc<[ResolvedFile]>, + /// The set of resolved item names available within this file's scope. + // Use BTreeSet instead of HashMap for the impl_eq_hash! macro. + resolutions: Arc<[BTreeSet>]>, import_aliases: AliasRegistry, @@ -25,12 +25,52 @@ pub struct Program { } impl Program { + pub fn from_parse( + parsed: &parse::Program, + content: Arc, + handler: &mut ErrorCollector, + ) -> Option { + let mut items: Vec = Vec::new(); + let mut resolutions: BTreeSet> = BTreeSet::new(); + + for item in parsed.items() { + if let parse::Item::Use(use_decl) = item { + handler.push( + RichError::new(Error::UnknownLibrary(use_decl.str_path()), *use_decl.span()) + .with_content(content.clone()), + ); + continue; + } + + let name = match item { + parse::Item::TypeAlias(a) => a.name().as_inner(), + parse::Item::Function(f) => f.name().as_inner(), + + // Safe to skip: `Use` items are handled earlier in the loop, and `Module` currently has no functionality. + parse::Item::Module | parse::Item::Use(_) => continue, + }; + + // Push the item as is without modifying the `file_id`, as it defaults to the correct value. + items.push(item.clone()); + resolutions.insert(Arc::from(name)); + } + + // TODO: Consider getting rid of the 'String' error here and changing it to a more appropriate error + // (e.g. 'Result') after resolving https://github.com/BlockstreamResearch/SimplicityHL/issues/270. + (!handler.has_errors()).then(|| Program { + items: items.into(), + resolutions: Arc::from([resolutions]), + import_aliases: AliasRegistry::default(), + span: *parsed.as_ref(), + }) + } + pub fn items(&self) -> &[parse::Item] { &self.items } - pub fn files(&self) -> &[ResolvedFile] { - &self.files + pub fn resolutions(&self) -> &[BTreeSet>] { + &self.resolutions } pub fn import_aliases(&self) -> &AliasRegistry { @@ -42,29 +82,7 @@ impl Program { } } -impl_eq_hash!(Program; items, files, import_aliases); - -/// Represents a single source file alongside its resolved scoping and visibility rules. -#[derive(Clone, Debug)] -pub struct ResolvedFile { - path: CanonPath, - - /// The set of resolved item names available within this file's scope. - // Use BTreeSet instead of HashMap for the impl_eq_hash! macro. - resolutions: BTreeSet>, -} - -impl ResolvedFile { - pub fn path(&self) -> &CanonPath { - &self.path - } - - pub fn resolutions(&self) -> &BTreeSet> { - &self.resolutions - } -} - -impl_eq_hash!(ResolvedFile; path, resolutions); +impl_eq_hash!(Program; items, resolutions, import_aliases); /// A registry mapping an alias [`ItemNameWithFileId`] to its target item across different files. /// @@ -159,13 +177,25 @@ impl DependencyGraph { // Handle Types & Functions let (name, vis, span) = match elem { parse::Item::TypeAlias(a) => (a.name().as_inner(), a.visibility(), *a.span()), - parse::Item::Function(f) => (f.name().as_inner(), f.visibility(), *f.span()), + parse::Item::Function(f) => { + if f.name().as_inner() == "main" + && matches!(f.visibility(), Visibility::Public) + { + handler.push( + RichError::new(Error::MainCannotBePublic, *f.span()) + .with_source(source.clone()), + ); + } + + (f.name().as_inner(), f.visibility(), *f.span()) + } // Safe to skip: `Use` items are handled earlier in the loop, and `Module` currently has no functionality. parse::Item::Module | parse::Item::Use(_) => continue, }; - let item_name = (Arc::from(name), source_id); + let shared_name = Arc::::from(name); + let item_name = (shared_name.clone(), source_id); if items_registry.contains(&item_name) { handler.push( RichError::new(Error::RedefinedItem(name.to_string()), span) @@ -174,14 +204,26 @@ impl DependencyGraph { } items_registry.insert(item_name); - items.push(elem.clone()); - resolutions[source_id].insert(Arc::from(name), vis.clone()); + let mut new_elem = elem.clone(); + match &mut new_elem { + parse::Item::TypeAlias(a) => a.set_file_id(source_id), + parse::Item::Function(f) => f.set_file_id(source_id), + _ => {} + } + items.push(new_elem); + + resolutions[source_id].insert(shared_name, vis.clone()); } } + let resolutions = resolutions + .into_iter() + .map(|map| map.into_keys().collect::>()) + .collect(); + (!handler.has_errors()).then(|| Program { items: items.into(), - files: construct_resolved_file_array(&self.paths, &resolutions), + resolutions, import_aliases, span: *self.modules[0].parsed_program.as_ref(), }) @@ -236,6 +278,9 @@ impl DependencyGraph { // 3. Determine the local name and ID up front let local_name = if let Some(alias) = alias { + if alias.as_inner() == "main" { + return Err(RichError::new(Error::MainCannotBeAlias, span)); + } Arc::from(alias.as_inner()) } else { name.clone() @@ -281,22 +326,10 @@ impl DependencyGraph { } } -fn construct_resolved_file_array( - paths: &[CanonPath], - resolutions: &[HashMap, Visibility>], -) -> Arc<[ResolvedFile]> { - let mut result = Vec::with_capacity(paths.len()); - - for i in 0..paths.len() { - let file_resolutions: BTreeSet> = resolutions[i].keys().cloned().collect(); - - result.push(ResolvedFile { - path: paths[i].clone(), - resolutions: file_resolutions, - }); +impl AsRef for Program { + fn as_ref(&self) -> &Span { + &self.span } - - result.into() } #[cfg(test)] @@ -323,7 +356,7 @@ mod resolve_order_tests { }; let root_id = ids["main"]; - let resolutions = &program.files[root_id].resolutions; + let resolutions = &program.resolutions[root_id]; resolutions .get(&Arc::from("private_fn")) @@ -359,14 +392,12 @@ mod resolve_order_tests { let id_root = ids["main"]; // Check B's scope - program.files[id_b] - .resolutions + program.resolutions[id_b] .get(&Arc::from("foo")) .expect("foo missing in B"); // Check Root's scope - program.files[id_root] - .resolutions + program.resolutions[id_root] .get(&Arc::from("foo")) .expect("foo missing in Root"); } @@ -396,6 +427,55 @@ mod resolve_order_tests { .to_string() .contains(&"Item `foo` is private".to_string())); } + + #[test] + fn test_public_main_is_forbidden() { + // Scenario: A user tries to declare the entry point as `pub fn main`. + // Expected: The compiler must reject this because `main` must be private. + + let (graph, _ids, _dir) = setup_graph(vec![("main.simf", "pub fn main() {}")]); + + let mut error_handler = ErrorCollector::new(); + let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); + + assert!( + program_option.is_none(), + "Compiler should return None when `main` is declared public" + ); + + let error_msg = error_handler.to_string(); + assert!( + error_msg.contains("main") && error_msg.contains("public"), + "Error message should mention that `main` cannot be public. Got: {}", + error_msg + ); + } + + #[test] + fn test_aliasing_to_main_is_forbidden() { + // Scenario: A user tries to bypass entry point rules by renaming an import to `main`. + // Expected: The compiler must reject this because `main` is a reserved identifier. + + let (graph, _ids, _dir) = setup_graph(vec![ + ("libs/lib/A.simf", "pub fn start() {}"), + ("main.simf", "use lib::A::start as main;"), + ]); + + let mut error_handler = ErrorCollector::new(); + let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); + + assert!( + program_option.is_none(), + "Compiler should return None when a user tries to alias an import to `main`" + ); + + let error_msg = error_handler.to_string(); + assert!( + error_msg.contains("main") && error_msg.contains("alias"), + "Error message should clearly state that `main` cannot be used as an alias. Got: {}", + error_msg + ); + } } #[cfg(test)] @@ -423,7 +503,7 @@ mod alias_tests { }; let id_root = ids["main"]; - let scope = &program.files[id_root].resolutions; + let scope = &program.resolutions[id_root]; assert!( scope.get(&Arc::from("foo")).is_none(), @@ -451,7 +531,7 @@ mod alias_tests { }; let id_root = ids["main"]; - let scope = &program.files[id_root].resolutions; + let scope = &program.resolutions[id_root]; // The original names should NOT be in scope assert!(scope.get(&Arc::from("foo")).is_none()); @@ -506,7 +586,7 @@ mod alias_tests { let id_root = ids["main"]; // Assert Main Scope - let main_scope = &program.files[id_root].resolutions; + let main_scope = &program.resolutions[id_root]; assert!(main_scope.get(&Arc::from("original")).is_none()); assert!(main_scope.get(&Arc::from("middle")).is_none()); assert!( @@ -515,7 +595,7 @@ mod alias_tests { ); // Assert B Scope (It should have the intermediate alias!) - let b_scope = &program.files[id_b].resolutions; + let b_scope = &program.resolutions[id_b]; assert!( b_scope.get(&Arc::from("middle")).is_some(), "File B must contain its own public alias" diff --git a/src/error.rs b/src/error.rs index 474f0b4..f301e7e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -500,6 +500,8 @@ pub enum Error { MainNoInputs, MainNoOutput, MainRequired, + MainCannotBePublic, + MainCannotBeAlias, FunctionRedefined(FunctionName), FunctionUndefined(FunctionName), InvalidNumberOfArguments(usize, usize), @@ -603,6 +605,14 @@ impl fmt::Display for Error { f, "Main function is required" ), + Error::MainCannotBePublic => write!( + f, + "Main function cannot be public" + ), + Error::MainCannotBeAlias => write!( + f, + "Main function cannot be alias", + ), Error::FunctionRedefined(name) => write!( f, "Function `{name}` was defined multiple times" diff --git a/src/lib.rs b/src/lib.rs index d4daa09..1ec8b06 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,7 +64,17 @@ impl TemplateProgram { let mut error_handler = ErrorCollector::new(); let parse_program = parse::Program::parse_from_str_with_errors(source, &mut error_handler); - if let Some(program) = parse_program { + let driver_program = if let Some(parse_program) = parse_program { + driver::resolve_order::Program::from_parse( + &parse_program, + file.clone(), + &mut error_handler, + ) + } else { + None + }; + + if let Some(program) = driver_program { let ast_program = ast::Program::analyze(&program).with_content(Arc::clone(&file))?; Ok(Self { simfony: ast_program, diff --git a/src/parse.rs b/src/parse.rs index 6300545..8372360 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -3,6 +3,7 @@ use std::fmt; use std::num::NonZeroUsize; +use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; @@ -108,6 +109,11 @@ impl UseDecl { self.path.iter().map(|s| s.as_inner()).collect() } + pub fn str_path(&self) -> String { + let path: PathBuf = self.path.iter().map(|s| s.as_inner()).collect(); + path.display().to_string() + } + /// Extracts the Dependency Root Path Name (the very first segment) from this path. /// /// # Errors @@ -158,6 +164,7 @@ pub enum UseItems { #[derive(Clone, Debug)] pub struct Function { + file_id: usize, // The field required for the driver visibility: Visibility, name: FunctionName, params: Arc<[FunctionParam]>, @@ -167,6 +174,14 @@ pub struct Function { } impl Function { + pub fn file_id(&self) -> usize { + self.file_id + } + + pub fn set_file_id(&mut self, file_id: usize) { + self.file_id = file_id; + } + pub fn visibility(&self) -> &Visibility { &self.visibility } @@ -326,6 +341,7 @@ pub enum CallName { #[derive(Clone, Debug)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct TypeAlias { + file_id: usize, // The field required for the driver visibility: Visibility, name: AliasName, ty: AliasedType, @@ -333,6 +349,14 @@ pub struct TypeAlias { } impl TypeAlias { + pub fn file_id(&self) -> usize { + self.file_id + } + + pub fn set_file_id(&mut self, file_id: usize) { + self.file_id = file_id; + } + pub fn visibility(&self) -> &Visibility { &self.visibility } @@ -1352,6 +1376,8 @@ impl ChumskyParse for Function { where I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, { + // NOTE: Default value. This is required by `Program::from_parse` in the driver; do not modify. + let file_id = 0usize; let visibility = just(Token::Pub) .to(Visibility::Public) .or_not() @@ -1395,7 +1421,8 @@ impl ChumskyParse for Function { .then(params) .then(ret) .then(body) - .map_with(|((((visibility, name), params), ret), body), e| Self { + .map_with(move |((((visibility, name), params), ret), body), e| Self { + file_id, visibility, name, params, @@ -1692,6 +1719,8 @@ impl ChumskyParse for TypeAlias { where I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, { + // NOTE: Default value. This is required by `Program::from_parse` in the driver; do not modify. + let file_id = 0usize; let visibility = just(Token::Pub) .to(Visibility::Public) .or_not() @@ -1725,7 +1754,8 @@ impl ChumskyParse for TypeAlias { .then(AliasedType::parser()) .then_ignore(just(Token::Semi)), ) - .map_with(|(visibility, (name, ty)), e| Self { + .map_with(move |(visibility, (name, ty)), e| Self { + file_id, visibility, name: name.0, ty, @@ -2211,6 +2241,7 @@ 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..=3)?; let visibility = Visibility::arbitrary(u)?; let name = FunctionName::arbitrary(u)?; let len = u.int_in_range(0..=3)?; @@ -2220,6 +2251,7 @@ impl crate::ArbitraryRec for Function { let ret = Option::::arbitrary(u)?; let body = Expression::arbitrary_rec(u, budget).map(Expression::into_block)?; Ok(Self { + file_id, visibility, name, params, diff --git a/src/witness.rs b/src/witness.rs index f0fbd2d..a5e1d55 100644 --- a/src/witness.rs +++ b/src/witness.rs @@ -220,17 +220,26 @@ impl crate::ArbitraryOfType for Arguments { #[cfg(test)] mod tests { use super::*; + use crate::error::ErrorCollector; use crate::parse::ParseFromStr; use crate::value::ValueConstructible; - use crate::{ast, parse, CompiledProgram, SatisfiedProgram}; + use crate::{ast, driver, parse, CompiledProgram, SatisfiedProgram}; #[test] fn witness_reuse() { let s = r#"fn main() { 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 parse_program = parse::Program::parse_from_str(s).expect("parsing works"); + + let mut error_collector = ErrorCollector::new(); + let driver_program = driver::resolve_order::Program::from_parse( + &parse_program, + Arc::from(s), + &mut error_collector, + ) + .expect("driver works"); + match ast::Program::analyze(&driver_program).map_err(Error::from) { Ok(_) => panic!("Witness reuse was falsely accepted"), Err(Error::WitnessReused(..)) => {} Err(error) => panic!("Unexpected error: {error}"),