diff --git a/fuzz/fuzz_targets/compile_parse_tree.rs b/fuzz/fuzz_targets/compile_parse_tree.rs index a4e89c8c..9234cf95 100644 --- a/fuzz/fuzz_targets/compile_parse_tree.rs +++ b/fuzz/fuzz_targets/compile_parse_tree.rs @@ -5,6 +5,7 @@ fn do_test(data: &[u8]) { use arbitrary::Arbitrary; use simplicityhl::ast::ElementsJetHinter; + use simplicityhl::error::DiagnosticManager; use simplicityhl::{ast, named, parse, ArbitraryOfType, Arguments}; let mut u = arbitrary::Unstructured::new(data); @@ -12,11 +13,16 @@ fn do_test(data: &[u8]) { Ok(x) => x, Err(_) => return, }; - let ast_program = - match ast::Program::analyze(&parse_program, Box::new(ElementsJetHinter::new())) { - Ok(x) => x, - Err(_) => return, - }; + + let mut diags = DiagnosticManager::new(); + let ast_program = match ast::Program::analyze( + &parse_program, + Box::new(ElementsJetHinter::new()), + &mut diags, + ) { + Some(x) => x, + None => return, + }; let arguments = match Arguments::arbitrary_of_type(&mut u, ast_program.parameters()) { Ok(arguments) => arguments, Err(..) => return, diff --git a/src/ast.rs b/src/ast.rs index e3a084cb..0b6ee78d 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1,5 +1,6 @@ use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; +use std::convert::Infallible; use std::num::NonZeroUsize; use std::sync::Arc; @@ -9,7 +10,7 @@ use simplicity::jet::{Core, Elements, Jet}; use crate::debug::{CallTracker, DebugSymbols, TrackedCallName}; use crate::driver::{CRATE_STR, MAIN_STR}; -use crate::error::{Diagnostic, Error, Span, WithSpan}; +use crate::error::{Diagnostic, DiagnosticManager, Error, Span, WithSpan}; use crate::jet::{source_type, target_type, JetHL}; use crate::num::{NonZeroPow2Usize, Pow2Usize}; use crate::parse::{MatchPattern, UseDecl, Visibility}; @@ -83,7 +84,7 @@ pub enum Item { Use, Module(Vec), /// A placeholder used for error recovery during parsing. - Ignored, + Error, } /// Definition of a function. @@ -115,6 +116,8 @@ pub enum Statement { Assignment(Assignment), /// Expression that returns nothing (the unit value). Expression(Expression), + /// A placeholder for a statement that failed to parse. + Error, } /// Assignment of a value to a variable identifier. @@ -254,6 +257,10 @@ pub enum SingleExpressionInner { /// /// The enum's definition lives in the type of the expression. EnumConstruction(EnumConstruction), + /// Placeholder for subexpression that failed to parse. + /// + /// Emitted by the parser during error recovery. + Error, } /// Call of a user-defined or of a builtin function. @@ -318,6 +325,18 @@ pub enum CallName { ForWhile(CustomFunction, Pow2Usize), } +impl CallName { + /// Does this call name a function whose signature is unknowable? + fn is_error(&self) -> bool { + match self { + Self::Custom(f) | Self::Fold(f, _) | Self::ArrayFold(f, _) | Self::ForWhile(f, _) => { + f.is_error() + } + _ => false, + } + } +} + // Manually implemented because the 1.74 (MSRV) derive expands to a body that // moves out of the non-Copy `Box` field, later rustc versions are // fine. @@ -348,9 +367,29 @@ pub struct CustomFunction { params: Arc<[FunctionParam]>, body: Arc, span: Span, + /// Poison: the definition could not be found, so the signature is a + /// placeholder rather than the truth. + is_error: bool, } impl CustomFunction { + /// A function whose signature is unknowable, used when a name is declared + /// but its definition cannot be found. + fn error(span: Span) -> Self { + Self { + params: Arc::from([]), + body: Arc::new(Expression::error(ResolvedType::error(), span)), + span, + is_error: true, + } + } + + /// Is the signature unknowable? Every check against it — arity, argument + /// types, output type, foldability, loopability — is then absorbed. + pub fn is_error(&self) -> bool { + self.is_error + } + /// Access the identifiers of the parameters of the function. pub fn params(&self) -> &[FunctionParam] { &self.params @@ -595,6 +634,7 @@ impl TreeLike for ExprTree<'_> { Self::Statement(statement) => match statement { Statement::Assignment(assignment) => Tree::Unary(Self::Assignment(assignment)), Statement::Expression(expression) => Tree::Unary(Self::Expression(expression)), + Statement::Error => Tree::Nullary, }, Self::Assignment(assignment) => Tree::Unary(Self::Expression(assignment.expression())), Self::Single(single) => match single.inner() { @@ -602,7 +642,8 @@ impl TreeLike for ExprTree<'_> { | S::Witness(_) | S::Parameter(_) | S::Variable(_) - | S::Option(None) => Tree::Nullary, + | S::Option(None) + | S::Error => Tree::Nullary, S::Expression(l) | S::Either(Either::Left(l)) | S::Either(Either::Right(l)) @@ -706,6 +747,24 @@ struct ModuleScope { functions: HashMap, /// Nested inling `mod` blocks, each becoming a child scope. submodules: HashMap, + /// Names a failed `use` was meant to introduce. Kept apart from the maps + /// above so poison never takes part in a redefinition check, and consulted + /// only on a miss, so a real definition always wins. + poisoned_imports: HashSet, +} + +impl ModuleScope { + fn poison_import(&mut self, name: &SymbolName) { + self.poisoned_imports.insert(name.shallow_clone()); + } + + fn is_poisoned_import(&self, name: &str) -> bool { + let name = SymbolName::from_str_unchecked(name); + self.poisoned_imports.contains(&name) + && !self + .submodules + .contains_key(&ModuleName::from(name.shallow_clone())) + } } /// Scope for generating the abstract syntax tree. @@ -734,6 +793,8 @@ struct Scope { is_main: bool, call_tracker: CallTracker, jet_hinter: Box, + + diagnostics: Vec, } impl Default for Scope { @@ -741,12 +802,13 @@ impl Default for Scope { Self::new( // TODO: Should be passed in global configuration Box::new(ElementsJetHinter), + Vec::new(), ) } } impl Scope { - pub fn new(jet_hinter: Box) -> Self { + pub fn new(jet_hinter: Box, diagnostics: Vec) -> Self { Self { module_path: Vec::new(), root: ModuleScope::default(), @@ -757,6 +819,7 @@ impl Scope { is_main: false, call_tracker: CallTracker::default(), jet_hinter, + diagnostics, } } @@ -773,49 +836,6 @@ impl Scope { self.variables.is_empty() } - /// Enter a new block inside the current function. - pub fn enter_block(&mut self) { - self.variables.push(HashMap::new()); - } - - /// Push the scope of the main function onto the stack. - /// - /// ## Panics - /// - /// - Already inside the main function. - /// - Already inside a function body. - pub fn enter_main(&mut self) { - assert!(!self.is_main, "Already inside main function"); - assert!(self.is_outside_function(), "Already inside a function body"); - self.enter_block(); - self.is_main = true; - } - - /// Exit the current block inside the curreent function. - /// - /// ## Panics - /// - /// - No acive block to exit. - pub fn exit_block(&mut self) { - self.variables.pop().expect("No active block to exit"); - } - - /// Pop the scope of the main function from the stack. - /// - /// ## Panics - /// - /// - Not inside the main function. - /// - Unclosed nested blocks remain. - pub fn exit_main(&mut self) { - assert!(self.is_main, "Current scope is not inside main function"); - self.exit_block(); - self.is_main = false; - assert!( - self.is_outside_function(), - "Current scope is not nested in topmost scope" - ) - } - /// Enter a named module, pushing it onto the module path. /// /// ## Errors @@ -834,6 +854,23 @@ impl Scope { Ok(()) } + /// Re-enter a module that is already registered, without registering it again. + /// + /// Used to keep analyzing the items of a *redefined* module: the collision + /// was reported, but the items inside are independent of it, and discarding + /// the whole block would hide every error in it. + /// + /// ## Panics + /// + /// No module of this name exists in the current scope. + fn reenter_module(&mut self, name: ModuleName) { + assert!( + self.current_module().submodules.contains_key(&name), + "module must already exist to be re-entered" + ); + self.module_path.push(name); + } + /// Exit the current module, popping it from the module path. /// /// ## Panics @@ -888,10 +925,22 @@ impl Scope { parse::UseItems::List(elems) => elems.as_slice(), }; + // Errors from individual items, reported together at the end: one bad + // item neither aborts the declaration nor hides the ones beside it. + let mut item_errors: Vec = Vec::new(); + + // The local name each item introduces, in declaration order. `main` is + // never one: aliasing to it is rejected below. + let local_names: Vec = use_decl_items + .iter() + .filter(|(_, aliased)| !aliased.as_ref().is_some_and(|a| a.as_inner() == MAIN_STR)) + .map(|(name, aliased)| aliased.as_ref().unwrap_or(name).clone()) + .collect(); + // Phase 1: navigate to target and collect items. Immutable borrow, dropped at end of block - // Vec<(ProcessedAlias, ProcessedFunction, ProcessedModule)> - // where each is Result<(Key, (Value, Visibility)), Error> - let collected: Vec<_> = { + // Vec<(LocalName, ProcessedAlias, ProcessedFunction, ProcessedModule)> + // where each result is Result<(Key, (Value, Visibility)), Error> + let collected: Result, Error> = { // TODO: Part, that can be optimized // How many segments do the caller's path and the target's path have in common? let shared_prefix_len = self @@ -901,51 +950,92 @@ impl Scope { .take_while(|(curr, nav)| curr.as_inner() == nav.as_inner()) .count(); - let mut target_scope = &self.root; + let mut target_scope = Ok(&self.root); for (ind, segment) in path[1..].iter().enumerate() { + let Ok(scope) = target_scope else { break }; let name = ModuleName::from_str_unchecked(segment.as_inner()); - let (inner, visibility) = target_scope - .submodules - .get(&name) - .ok_or_else(|| Error::ModuleNotFound { name: name.clone() })?; + target_scope = match scope.submodules.get(&name) { + None => Err(Error::ModuleNotFound { name }), + Some((_, Visibility::Private)) if shared_prefix_len < ind => { + Err(Error::ModuleIsPrivate { name }) + } + Some((inner, _)) => Ok(inner), + }; + } - if matches!(visibility, Visibility::Private) && shared_prefix_len < ind { - return Err(Error::ModuleIsPrivate { name }); - } + target_scope.map(|target_scope| { + let mut collected = Vec::with_capacity(use_decl_items.len()); + for (name, aliased) in use_decl_items { + if aliased.as_ref().is_some_and(|a| a.as_inner() == MAIN_STR) { + // The items of one `use` are independent, so a rejected + // alias is recorded and the rest are still collected. + item_errors.push(Error::MainCannotBeAlias); + continue; + } - target_scope = inner; - } + let local_name = aliased.as_ref().unwrap_or(name); - let mut collected = Vec::with_capacity(use_decl_items.len()); - for (name, aliased) in use_decl_items { - if aliased.as_ref().is_some_and(|a| a.as_inner() == MAIN_STR) { - return Err(Error::MainCannotBeAlias); - } + let alias_res = + Self::try_collect_item(name, local_name, &target_scope.aliases, &use_vis); + let func_res = + Self::try_collect_item(name, local_name, &target_scope.functions, &use_vis); + let mod_res = Self::try_collect_item( + name, + local_name, + &target_scope.submodules, + &use_vis, + ); - let local_name = aliased.as_ref().unwrap_or(name); + collected.push((local_name.clone(), alias_res, func_res, mod_res)); + } + collected + }) + }; - let alias_res = - Self::try_collect_item(name, local_name, &target_scope.aliases, &use_vis); - let func_res = - Self::try_collect_item(name, local_name, &target_scope.functions, &use_vis); - let mod_res = - Self::try_collect_item(name, local_name, &target_scope.submodules, &use_vis); + let span = *use_decl.span(); - collected.push((alias_res, func_res, mod_res)); + // A broken path binds nothing at all, so every name the declaration + // introduces is poisoned before the one real error is returned. + let collected = match collected { + Ok(collected) => collected, + Err(path_err) => { + let current = self.current_module_mut(); + for local_name in &local_names { + current.poison_import(local_name); + } + for err in item_errors { + self.report(err.with_span(span)); + } + return Err(path_err); } - collected }; - // Phase 2: insert into current scope - let current = self.current_module_mut(); - for (alias_res, func_res, mod_res) in collected { - Self::resolve_processing_use_items_error(&[ - Self::insert_collected(alias_res, &mut current.aliases), - Self::insert_collected(func_res, &mut current.functions), - Self::insert_collected(mod_res, &mut current.submodules), - ])?; + // Phase 2: insert into current scope. + // + // The items of one `use` are independent — `b` failing says nothing + // about `a` — so each is recorded and the loop continues. Only the + // path-level failures above abort, because they leave nothing to insert. + { + let current = self.current_module_mut(); + for (local_name, alias_res, func_res, mod_res) in collected { + let results = [ + Self::insert_collected(alias_res, &mut current.aliases), + Self::insert_collected(func_res, &mut current.functions), + Self::insert_collected(mod_res, &mut current.submodules), + ]; + + if !results.iter().any(Result::is_ok) { + current.poison_import(&local_name); + } + + item_errors.extend(Self::resolve_processing_use_items_error(&results)); + } + } + + for err in item_errors { + self.report(err.with_span(span)); } Ok(()) @@ -1018,25 +1108,32 @@ impl Scope { /// /// * Returns a specific error (e.g., [`Error::PrivateItem`], [`Error::RedefinedItem`]) if one occurred. /// * Returns a fallback [`Error::UnresolvedItem`] if the item could not be found in any of the checked namespaces. - fn resolve_processing_use_items_error(results: &[Result<(), Error>]) -> Result<(), Error> { - if results.iter().any(|res| res.is_ok()) { - return Ok(()); - } - + fn resolve_processing_use_items_error(results: &[Result<(), Error>]) -> Vec { let errors: Vec<&Error> = results .iter() .filter_map(|res| res.as_ref().err()) .collect(); - if let Some(&specific_err) = errors - .iter() - .find(|e| !matches!(e, Error::UnresolvedItem { .. })) - { - return Err(specific_err.clone()); + if results.iter().any(|res| res.is_ok()) { + // The item lives in one namespace, so "not found" from the others is + // expected noise. A collision or a privacy violation is a real + // failure of *this* import, and importing into some other namespace + // must not mask it. + return errors + .into_iter() + .filter(|err| !matches!(err, Error::UnresolvedItem { .. })) + .cloned() + .collect(); } - // Fallback to the first `UnresolvedItem` error - Err(errors[0].clone()) + // Nothing was imported: name the specific reason if there is one, + // otherwise report that the item was not found. + errors + .iter() + .find(|err| !matches!(err, Error::UnresolvedItem { .. })) + .or(errors.first()) + .map(|err| vec![(*err).clone()]) + .unwrap_or_default() } /// Insert a variable into the current block. @@ -1065,20 +1162,37 @@ impl Scope { /// /// * [`Error::UndefinedAlias`]: The alias is not defined in the current scope. fn get_alias(&self, name: &AliasName) -> Result { - self.current_module() - .aliases - .get(name) - .map(|(ty, _)| ty.clone()) - .ok_or_else(|| Error::UndefinedAlias { name: name.clone() }) + let module = self.current_module(); + if let Some((ty, _)) = module.aliases.get(name) { + return Ok(ty.clone()); + } + if module.is_poisoned_import(name.as_inner()) { + return Ok(ResolvedType::error()); + } + Err(Error::UndefinedAlias { name: name.clone() }) } - /// Resolve a type with aliases to a type without aliases. - /// - /// ## Errors + /// Resolve a type with aliases, substituting a poison type for every alias + /// that is not defined and reporting each one. /// - /// * [`Error::UndefinedAlias`]: The alias is not found in the global registry. - pub fn resolve(&self, ty: &AliasedType) -> Result { - ty.resolve(|name| self.get_alias(name)) + /// Never fails, so a caller can keep analyzing: a type whose aliases are all + /// broken still yields a type, and a type with two broken aliases reports + /// both instead of aborting at the first. + pub fn resolve_or_poison(&mut self, ty: &AliasedType, span: Span) -> ResolvedType { + let mut undefined = Vec::new(); + let resolved = ty + .resolve::<_, Infallible>(|name| { + Ok(self.get_alias(name).unwrap_or_else(|_| { + undefined.push(name.clone()); + ResolvedType::error() + })) + }) + .unwrap_or_else(|never| match never {}); + + for name in undefined { + self.report(Error::UndefinedAlias { name }.with_span(span)); + } + resolved } /// Error if `name` is already defined as an alias in the current module. @@ -1092,13 +1206,20 @@ impl Scope { /// Insert a type alias into the current module scope. /// + /// An alias whose body does not resolve is still registered, at a poison + /// type: the body's error was reported, and a missing registration would + /// cascade into "undefined alias" at every use of the name. + /// /// ## Errors /// /// * [`Error::RedefinedAlias`]: The alias name is already defined in the current scope. - pub fn insert_alias(&mut self, alias: parse::TypeAlias) -> Result<(), Error> { - self.check_alias_free(alias.name())?; + pub fn insert_alias(&mut self, alias: parse::TypeAlias, span: Span) -> Result<(), Error> { + // The body is independent of the name collision, so resolve it either + // way for its own diagnostics. + let resolved = self.resolve_or_poison(alias.ty(), span); - let resolved = self.resolve(alias.ty())?; + // A redefinition keeps the existing binding, which is the good one. + self.check_alias_free(alias.name())?; self.current_module_mut() .aliases @@ -1137,6 +1258,29 @@ impl Scope { Ok(()) } + /// Register a type name at a poison type. + /// + /// Used when a declaration is rejected but its name was still written: the + /// cause was reported, and leaving the name unbound would cascade into + /// [`Error::UndefinedAlias`] at every use of the type. + /// + /// ## Errors + /// + /// * [`Error::RedefinedAlias`]: The name is already defined in the current module. + fn insert_poison_alias( + &mut self, + name: AliasName, + visibility: Visibility, + ) -> Result<(), Error> { + self.check_alias_free(&name)?; + + self.current_module_mut() + .aliases + .insert(name, (ResolvedType::error(), visibility)); + + Ok(()) + } + /// Insert a parameter into the global map. /// /// ## Errors @@ -1144,7 +1288,8 @@ impl Scope { /// * [`Error::ExpressionTypeMismatch`] A parameter of the same name has already been defined as a different type. pub fn insert_parameter(&mut self, name: WitnessName, ty: ResolvedType) -> Result<(), Error> { match self.parameters.entry(name.clone()) { - Entry::Occupied(entry) if entry.get() == &ty => Ok(()), + // Only matters if the same `param::X` appears once with a poisoned expected type and once with a real one + Entry::Occupied(entry) if entry.get().compatible(&ty) => Ok(()), Entry::Occupied(entry) => Err(Error::ExpressionTypeMismatch { expected: entry.get().clone(), found: ty, @@ -1216,17 +1361,115 @@ impl Scope { /// /// * [`Error::FunctionUndefined`]: The function is not found in the global registry. pub fn get_function(&self, name: &FunctionName) -> Result { - self.current_module() - .functions - .get(name) - .map(|(func, _)| func.clone()) - .ok_or_else(|| Error::FunctionUndefined { name: name.clone() }) + let module = self.current_module(); + if let Some((func, _)) = module.functions.get(name) { + return Ok(func.clone()); + } + if module.is_poisoned_import(name.as_inner()) { + return Ok(CustomFunction::error(Span::DUMMY)); + } + Err(Error::FunctionUndefined { name: name.clone() }) } /// Track a call expression with its span. pub fn track_call>(&mut self, span: &S, name: TrackedCallName) { self.call_tracker.track_call(*span.as_ref(), name); } + + fn report(&mut self, diag: Diagnostic) { + self.diagnostics.push(diag); + } + + fn diagnostics(&self) -> &[Diagnostic] { + &self.diagnostics + } +} + +/// RAII guard that balances a function/block scope. +/// +/// Multi-error analysis keeps going after an error, so a body can fail +/// mid-analysis. Pairing enter/exit by hand around a `?` leaks the scope on the +/// error path, and the next item's balance assertion then panics. This guard +/// runs the exit on *every* path — normal return, early `?`, or unwind — so the +/// scope is always balanced. Entering a scope is only possible through +/// [`Scope::block`] / [`Scope::main_scope`], which always arm the matching exit. +struct ScopeGuard<'a> { + scope: &'a mut Scope, + exit: fn(&mut Scope), +} + +impl Drop for ScopeGuard<'_> { + fn drop(&mut self) { + (self.exit)(self.scope); + } +} + +impl Scope { + /// Enter a nested block; the guard exits it on drop. + pub fn block(&mut self) -> ScopeGuard<'_> { + self.enter_block(); + + ScopeGuard { + scope: self, + exit: Scope::exit_block, + } + } + + /// Enter the main function's scope; the guard exits it on drop. + /// + /// ## Panics + /// - Already inside the main function. + /// - Already inside a function body. + pub fn main_scope(&mut self) -> ScopeGuard<'_> { + self.enter_main(); + ScopeGuard { + scope: self, + exit: Scope::exit_main, + } + } + + /// Enter a new block inside the current function. + fn enter_block(&mut self) { + self.variables.push(HashMap::new()); + } + + /// Push the scope of the main function onto the stack. + /// + /// ## Panics + /// + /// - Already inside the main function. + /// - Already inside a function body. + fn enter_main(&mut self) { + assert!(!self.is_main, "Already inside main function"); + assert!(self.is_outside_function(), "Already inside a function body"); + self.enter_block(); + self.is_main = true; + } + + /// Exit the current block inside the curreent function. + /// + /// ## Panics + /// + /// - No acive block to exit. + fn exit_block(&mut self) { + self.variables.pop().expect("No active block to exit"); + } + + /// Pop the scope of the main function from the stack. + /// + /// ## Panics + /// + /// - Not inside the main function. + /// - Unclosed nested blocks remain. + fn exit_main(&mut self) { + assert!(self.is_main, "Current scope is not inside main function"); + self.exit_block(); + self.is_main = false; + assert!( + self.is_outside_function(), + "Current scope is not nested in topmost scope" + ) + } } /// Part of the abstract syntax tree that can be generated from a precursor in the parse tree. @@ -1247,30 +1490,50 @@ impl Program { pub fn analyze( from: &parse::Program, jet_hinter: Box, - ) -> Result { + diagnostics: &mut DiagnosticManager, + ) -> Option { + let before = diagnostics.error_count(); let unit = ResolvedType::unit(); - let mut scope = Scope::new(jet_hinter); + let mut scope = Scope::new(jet_hinter, Vec::new()); - let items = from + let items: Vec = from .items() .iter() - .map(|s| Item::analyze(s, &unit, &mut scope)) - .collect::, Diagnostic>>()?; + .map(|item| match Item::analyze(item, &unit, &mut scope) { + Ok(item) => item, + Err(diag) => { + scope.report(diag); + Item::Error + } + }) + .collect(); + debug_assert!(scope.is_outside_function()); debug_assert!( scope.module_path.is_empty(), "Unclosed module scopes remain" ); - let (parameters, witness_types, call_tracker) = scope.destruct(); - let main = Self::extract_single_main(&items) - // If we find a duplicate of main function - .map_err(|err| err.with_span(from.into()))? - .ok_or(Error::MainRequired) - .with_span(from)?; + let main = match Self::extract_single_main(&items) { + Ok(Some(main)) => Some(main), + Ok(None) => { + scope.report(Error::MainRequired.with_span(from.into())); + None + } + Err(err) => { + scope.report(err.with_span(from.into())); + None + } + }; - Ok(Self { - main, + diagnostics.extend(scope.diagnostics().iter().cloned()); + if diagnostics.error_count() > before { + return None; + } + + let (parameters, witness_types, call_tracker) = scope.destruct(); + Some(Self { + main: main?, parameters, witness_types, call_tracker: Arc::new(call_tracker), @@ -1318,7 +1581,8 @@ impl AbstractSyntaxTree for Item { match from { parse::Item::TypeAlias(alias) => { - scope.insert_alias(alias.clone()).with_span(alias)?; + let span = *alias.as_ref(); + scope.insert_alias(alias.clone(), span).with_span(alias)?; Ok(Self::TypeAlias) } parse::Item::Function(function) => { @@ -1329,42 +1593,63 @@ impl AbstractSyntaxTree for Item { Ok(Self::Use) } parse::Item::EnumDeclaration(decl) => { - if decl.variants().is_empty() { - // A sum of zero types would be uninhabited, which - // Simplicity's type algebra cannot express. - return Err(Error::Grammar { - msg: format!("enum '{}' must have at least one variant", decl.name()), - }) - .with_span(decl); - } - - let mut seen_names = HashSet::new(); - for v in decl.variants() { - if !seen_names.insert(v.name()) { - return Err(Error::Grammar { - msg: format!( + // A sum of zero types would be uninhabited, which Simplicity's + // type algebra cannot express; duplicate variant names leave the + // enum's shape ambiguous. Either way the declaration is rejected, + // but its *name* must still be registered — see below. + let rejected = if decl.variants().is_empty() { + Some(format!( + "enum '{}' must have at least one variant", + decl.name() + )) + } else { + let mut seen_names = HashSet::new(); + decl.variants() + .iter() + .find(|v| !seen_names.insert(v.name())) + .map(|v| { + format!( "enum '{}' has duplicate variant name '{}'", decl.name(), v.name() - ), + ) }) - .with_span(decl); + }; + + if let Some(msg) = rejected { + scope.report(Diagnostic::new(Error::Grammar { msg }, decl.into())); + + // The payload types are independent of the rejection, so + // resolve them anyway to surface their own errors. + for v in decl.variants() { + for payload_ty in v.payload() { + scope.resolve_or_poison(payload_ty, v.into()); + } } + + // Register the name at a poison type instead of the enum, so + // that `let x: E = ..` does not cascade into "E is not + // defined" at every use. + scope + .insert_poison_alias(decl.name().clone(), decl.visibility().clone()) + .with_span(decl)?; + + return Ok(Self::EnumDeclaration); } - let variants = decl + let variants: Arc<[EnumVariantInfo]> = decl .variants() .iter() .map(|v| { - let payload = v + let payload: Arc<[ResolvedType]> = v .payload() .iter() - .map(|ty| scope.resolve(ty)) - .collect::, Error>>() - .with_span(v)?; - Ok(EnumVariantInfo::new(v.name().clone(), payload)) + .map(|ty| scope.resolve_or_poison(ty, v.into())) + .collect(); + EnumVariantInfo::new(v.name().clone(), payload) }) - .collect::, Diagnostic>>()?; + .collect(); + scope .insert_enum(decl.name().clone(), decl.visibility().clone(), variants) .with_span(decl)?; @@ -1372,18 +1657,32 @@ impl AbstractSyntaxTree for Item { Ok(Self::EnumDeclaration) } parse::Item::Module(module) => { - scope - .enter_module(module.name().clone(), module.visibility().clone()) - .with_span(module)?; + // A name collision is reported, not returned: the items inside + // are independent of it, so they are analyzed in the existing + // module of that name rather than discarded wholesale. A genuine + // duplicate among them then reports itself, as any item would. + if let Err(err) = + scope.enter_module(module.name().clone(), module.visibility().clone()) + { + scope.report(err.with_span(module.into())); + scope.reenter_module(module.name().clone()); + } let mut analyzed_children = Vec::new(); for item in module.items() { - analyzed_children.push(Item::analyze(item, ty, scope)?); + let analyzed = match Item::analyze(item, ty, scope) { + Ok(item) => item, + Err(diag) => { + scope.report(diag); + Item::Error + } + }; + analyzed_children.push(analyzed); } scope.exit_module(); Ok(Self::Module(analyzed_children)) } - parse::Item::Ignored => Ok(Self::Ignored), + parse::Item::Ignored => Ok(Self::Error), } } } @@ -1403,39 +1702,37 @@ impl AbstractSyntaxTree for Function { ); if from.name().as_inner() != MAIN_STR { - let params = from + let params: Arc<[FunctionParam]> = from .params() .iter() - .map(|param| { - let identifier = param.identifier().clone(); - let ty = scope.resolve(param.ty())?; - Ok(FunctionParam { - identifier, - ty, - span: *param.span(), - }) + .map(|param| FunctionParam { + identifier: param.identifier().clone(), + ty: scope.resolve_or_poison(param.ty(), *from.span()), + span: *param.span(), }) - .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); + .collect(); + let ret = match from.ret() { + Some(aliased) => scope.resolve_or_poison(aliased, from.into()), + None => ResolvedType::unit(), + }; - scope.enter_block(); - for param in params.iter() { - scope.insert_variable(param.identifier().clone(), param.ty().clone()); - } - let body = Expression::analyze(from.body(), &ret, scope).map(Arc::new)?; - scope.exit_block(); + let body = { + let guard = scope.block(); + for param in params.iter() { + guard + .scope + .insert_variable(param.identifier().clone(), param.ty().clone()); + } + + Arc::new(analyze_child(from.body(), &ret, guard.scope)) + }; debug_assert!(scope.is_outside_function()); let function = CustomFunction { params, body, span: *from.span(), + is_error: false, }; scope .insert_function(from.name().clone(), from.visibility().clone(), function) @@ -1444,23 +1741,49 @@ impl AbstractSyntaxTree for Function { return Ok(Self::Custom); } + // An invalid signature is reported, not returned. `main` was written, so + // erasing it here would invent a spurious `MainRequired`, skip the body, + // and hide a second `main`. It survives as a poisoned `Function::Main`, + // exactly as a `main` whose body fails already does. + let span: Span = from.into(); + if !from.params().is_empty() { - return Err(Error::MainNoInputs).with_span(from); + scope.report(Diagnostic::new(Error::MainNoInputs, span)); } 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); + let resolved = scope.resolve_or_poison(aliased, span); + // A poisoned return type absorbs this check: the alias was already + // reported, and whether it is unit is unknowable. + if !resolved.is_unit() && !resolved.is_error() { + scope.report(Diagnostic::new(Error::MainNoOutput, span)); } } - if matches!(from.visibility(), Visibility::Public) { - return Err(Error::MainCannotBePublic).with_span(from); + scope.report(Diagnostic::new(Error::MainCannotBePublic, span)); } - scope.enter_main(); - let body = Expression::analyze(from.body(), ty, scope)?; - scope.exit_main(); + // The rejected parameters are still declared, so bind them: a body that + // uses one must not cascade into "undefined variable". + let params: Vec<(Identifier, ResolvedType)> = from + .params() + .iter() + .map(|param| { + ( + param.identifier().clone(), + scope.resolve_or_poison(param.ty(), span), + ) + }) + .collect(); + + let guard = scope.main_scope(); + for (identifier, param_ty) in params { + guard.scope.insert_variable(identifier, param_ty); + } + + // The body is checked against unit whatever the declared return type: + // `Function::Main` always carries a unit-typed body, so a rejected + // return type must not change what the body is held to. + let body = analyze_child(from.body(), ty, guard.scope); Ok(Self::Main(body)) } } @@ -1481,6 +1804,7 @@ impl AbstractSyntaxTree for Statement { parse::Statement::Expression(expression) => { Expression::analyze(expression, ty, scope).map(Self::Expression) } + parse::Statement::Error(_) => Ok(Self::Error), } } } @@ -1488,6 +1812,7 @@ impl AbstractSyntaxTree for Statement { impl AbstractSyntaxTree for Assignment { type From = parse::Assignment; + // TODO: So, currently we do not need a `Diagnostic` for this fn analyze( from: &Self::From, ty: &ResolvedType, @@ -1498,9 +1823,16 @@ impl AbstractSyntaxTree for Assignment { // // However, the expression evaluated in the assignment does have a type, // namely the type specified in the assignment. - let ty_expr = scope.resolve(from.ty()).with_span(from)?; - let expression = Expression::analyze(from.expression(), &ty_expr, scope)?; - let typed_variables = from.pattern().is_of_type(&ty_expr).with_span(from)?; + let ty_expr = scope.resolve_or_poison(from.ty(), from.into()); + + let expression = analyze_child(from.expression(), &ty_expr, scope); + let typed_variables = match from.pattern().is_of_type(&ty_expr) { + Ok(vars) => vars, + Err(err) => { + scope.report(err.with_span(from.into())); + poison_bindings(from.pattern()) + } + }; for (identifier, ty) in typed_variables { scope.insert_variable(identifier, ty); } @@ -1528,22 +1860,82 @@ impl Expression { let mut empty_scope = Scope::for_value_parsing(); Self::analyze(from, ty, &mut empty_scope) } + + fn error(ty: ResolvedType, span: Span) -> Self { + let inner = ExpressionInner::Single(SingleExpression { + inner: SingleExpressionInner::Error, + ty: ty.clone(), + span, + }); + + Expression { inner, ty, span } + } } -/// Analyze the construction of an enum variant, e.g. `Action::Refresh(sig, 3)`. +/// Bind every identifier a pattern declares at a poison type. /// -/// Analysis is type-directed. The expected type must be an enum, and the written enum name must name it. -/// In program source that means an alias in lexical scope, the same rule -/// matches follow. In witness and argument files, which are parsed without -/// a scope ([`Scope::unscoped_enum_names`]), the enum's declared name -/// itself also matches. -fn analyze_enum_construction( - construction: &parse::EnumConstruction, - ty: &ResolvedType, - scope: &mut Scope, -) -> Result { +/// Used when the pattern does not match its annotation: the shape error was +/// already reported, and dropping the identifiers would cascade into +/// "undefined variable" at every use of them. +fn poison_bindings(pattern: &Pattern) -> HashMap { + pattern + .identifiers() + .map(|identifier| (identifier.clone(), ResolvedType::error())) + .collect() +} + +/// Analyze one child expression, substituting a poison node if it fails. +/// +/// Catches the error instead of propagating it, so the caller keeps analyzing +/// whatever comes after the child: a sibling, a registration, or an artifact. +fn analyze_child(child: &parse::Expression, ty: &ResolvedType, scope: &mut Scope) -> Expression { + match Expression::analyze(child, ty, scope) { + Ok(expr) => expr, + Err(diag) => { + scope.report(diag); + Expression::error(ty.clone(), *child.span()) + } + } +} + +/// Analyze a container's children, each against its own expected type. +/// +/// Missing expected types are padded with poison, so a count mismatch between +/// children and types never hides a child's own errors. Catches every child, so +/// it cannot fail. +fn analyze_children( + children: &[parse::Expression], + expected: &[ResolvedType], + scope: &mut Scope, +) -> Arc<[Expression]> { + children + .iter() + .enumerate() + .map(|(i, child)| { + let ty = expected.get(i).cloned().unwrap_or_else(ResolvedType::error); + analyze_child(child, &ty, scope) + }) + .collect() +} + +/// Analyze the construction of an enum variant, e.g. `Action::Refresh(sig, 3)`. +/// +/// Analysis is type-directed. The expected type must be an enum, and the written enum name must name it. +/// In program source that means an alias in lexical scope, the same rule +/// matches follow. In witness and argument files, which are parsed without +/// a scope ([`Scope::unscoped_enum_names`]), the enum's declared name +/// itself also matches. +fn analyze_enum_construction( + construction: &parse::EnumConstruction, + ty: &ResolvedType, + scope: &mut Scope, +) -> Result { let span = *construction.span(); + + // A construction we cannot analyze structurally still has arguments whose + // own errors must surface, so drain them at a poison type before bailing. let Some(info) = ty.as_enum() else { + analyze_children(construction.args(), &[], scope); return Err(Error::ExpressionUnexpectedType { ty: ty.clone() }).with_span(span); }; @@ -1558,6 +1950,7 @@ fn analyze_enum_construction( match scope.get_alias(&alias) { Ok(resolved) if &resolved == ty => true, Ok(resolved) => { + analyze_children(construction.args(), &[], scope); return Err(Error::ExpressionTypeMismatch { expected: ty.clone(), found: resolved, @@ -1570,35 +1963,41 @@ fn analyze_enum_construction( _ => false, }; if !names_expected_enum { + analyze_children(construction.args(), &[], scope); return Err(Error::Grammar { msg: format!("`{written}` does not name enum `{}`", info.name()), }) .with_span(span); } - let (variant_index, variant) = info - .variant(construction.variant()) - .ok_or_else(|| enum_variant_error(construction.variant().as_inner(), info)) - .with_span(span)?; + let Some((variant_index, variant)) = info.variant(construction.variant()) else { + analyze_children(construction.args(), &[], scope); + return Err(enum_variant_error(construction.variant().as_inner(), info)).with_span(span); + }; + + // The variant is known, so the payload types are the best expected types + // available even when the count disagrees: report the count and keep going. if construction.args().len() != variant.payload().len() { - return Err(Error::Grammar { - msg: format!( - "variant `{}` of enum `{}` carries {} payload value(s), found {}", - construction.variant(), - info.name(), - variant.payload().len(), - construction.args().len() - ), - }) - .with_span(span); + scope.report( + Error::Grammar { + msg: format!( + "variant `{}` of enum `{}` carries {} payload value(s), found {}", + construction.variant(), + info.name(), + variant.payload().len(), + construction.args().len() + ), + } + .with_span(span), + ); } - let payload = construction - .args() - .iter() - .zip(variant.payload()) - .map(|(arg, payload_ty)| Expression::analyze(arg, payload_ty, scope).map(Arc::new)) - .collect::]>, Diagnostic>>()?; + let payload: Arc<[Arc]> = + analyze_children(construction.args(), variant.payload(), scope) + .iter() + .cloned() + .map(Arc::new) + .collect(); Ok(EnumConstruction { variant_index, @@ -1686,23 +2085,35 @@ impl AbstractSyntaxTree for Expression { }) } parse::ExpressionInner::Block(statements, expression) => { - scope.enter_block(); + let guard = scope.block(); let ast_statements = statements .iter() - .map(|s| Statement::analyze(s, &ResolvedType::unit(), scope)) - .collect::, Diagnostic>>()?; + .map( + |s| match Statement::analyze(s, &ResolvedType::unit(), guard.scope) { + Ok(stmt) => stmt, + Err(diag) => { + guard.scope.report(diag); + Statement::Error + } + }, + ) + .collect(); + let ast_expression = match expression { - Some(expression) => Expression::analyze(expression, ty, scope) + Some(expression) => Expression::analyze(expression, ty, guard.scope) .map(Arc::new) .map(Some), - None if ty.is_unit() => Ok(None), + + // A poisoned expected type absorbs the missing-tail check: the + // annotation was already reported, so demanding unit here would + // cascade. + None if ty.is_unit() || ty.is_error() => Ok(None), None => Err(Error::ExpressionTypeMismatch { expected: ty.clone(), found: ResolvedType::unit(), }) .with_span(from), }?; - scope.exit_block(); Ok(Self { ty: ty.clone(), @@ -1724,36 +2135,53 @@ impl AbstractSyntaxTree for SingleExpression { ) -> Result { let inner = match from.inner() { parse::SingleExpressionInner::Boolean(bit) => { - if !ty.is_boolean() { + if ty.is_error() { + SingleExpressionInner::Error + } else if !ty.is_boolean() { return Err(Error::ExpressionTypeMismatch { expected: ty.clone(), found: ResolvedType::boolean(), }) .with_span(from); + } else { + SingleExpressionInner::Constant(Value::from(*bit)) } - SingleExpressionInner::Constant(Value::from(*bit)) } parse::SingleExpressionInner::Decimal(decimal) => { - let ty = ty - .as_integer() - .ok_or(Error::ExpressionUnexpectedType { ty: ty.clone() }) - .with_span(from)?; - UIntValue::parse_decimal(decimal, ty) - .with_span(from) - .map(Value::from) - .map(SingleExpressionInner::Constant)? + if ty.is_error() { + SingleExpressionInner::Error + } else { + let ty = ty + .as_integer() + .ok_or_else(|| Error::ExpressionUnexpectedType { ty: ty.clone() }) + .with_span(from)?; + + UIntValue::parse_decimal(decimal, ty) + .with_span(from) + .map(Value::from) + .map(SingleExpressionInner::Constant)? + } } parse::SingleExpressionInner::Binary(bits) => { - let ty = ty - .as_integer() - .ok_or(Error::ExpressionUnexpectedType { ty: ty.clone() }) - .with_span(from)?; - let value = UIntValue::parse_binary(bits, ty).with_span(from)?; - SingleExpressionInner::Constant(Value::from(value)) + if ty.is_error() { + SingleExpressionInner::Error + } else { + let ty = ty + .as_integer() + .ok_or_else(|| Error::ExpressionUnexpectedType { ty: ty.clone() }) + .with_span(from)?; + + let value = UIntValue::parse_binary(bits, ty).with_span(from)?; + SingleExpressionInner::Constant(Value::from(value)) + } } parse::SingleExpressionInner::Hexadecimal(bytes) => { - let value = Value::parse_hexadecimal(bytes, ty).with_span(from)?; - SingleExpressionInner::Constant(value) + if ty.is_error() { + SingleExpressionInner::Error + } else { + let value = Value::parse_hexadecimal(bytes, ty).with_span(from)?; + SingleExpressionInner::Constant(value) + } } parse::SingleExpressionInner::Witness(name) => { scope @@ -1770,18 +2198,22 @@ impl AbstractSyntaxTree for SingleExpression { parse::SingleExpressionInner::Variable(identifier) => { let bound_ty = scope .get_variable(identifier) - .ok_or(Error::UndefinedVariable { + .ok_or_else(|| Error::UndefinedVariable { identifier: identifier.clone(), }) .with_span(from)?; - if ty != bound_ty { + + if !ty.compatible(bound_ty) { return Err(Error::ExpressionTypeMismatch { expected: ty.clone(), found: bound_ty.clone(), }) .with_span(from); } - scope.insert_variable(identifier.clone(), ty.clone()); + + // Reading a variable does not rebind it. `insert_variable` writes + // into the innermost scope, so binding the *expected* type here + // shadows the declaration. SingleExpressionInner::Variable(identifier.clone()) } parse::SingleExpressionInner::Expression(parse) => { @@ -1790,74 +2222,98 @@ impl AbstractSyntaxTree for SingleExpression { .map(SingleExpressionInner::Expression)? } parse::SingleExpressionInner::Tuple(tuple) => { - let types = ty - .as_tuple() - .ok_or(Error::ExpressionUnexpectedType { ty: ty.clone() }) - .with_span(from)?; - if tuple.len() != types.len() { - return Err(Error::ExpressionUnexpectedType { ty: ty.clone() }).with_span(from); - } - tuple - .iter() - .zip(types.iter()) - .map(|(el_parse, el_ty)| Expression::analyze(el_parse, el_ty, scope)) - .collect::, Diagnostic>>() - .map(SingleExpressionInner::Tuple)? + // A shape or arity failure is reported, not returned: the + // elements are independent, so each must still be analyzed — + // against poison, the only expected type left. + let types: Vec = match ty.as_tuple() { + Some(types) if types.len() == tuple.len() => { + types.iter().map(|a| a.as_ref().clone()).collect() + } + _ => { + report_shape_mismatch(ty, *from.as_ref(), scope); + vec![ResolvedType::error(); tuple.len()] + } + }; + + SingleExpressionInner::Tuple(analyze_children(tuple, &types, scope)) } parse::SingleExpressionInner::Array(array) => { - let (el_ty, size) = ty - .as_array() - .ok_or(Error::ExpressionUnexpectedType { ty: ty.clone() }) - .with_span(from)?; - if array.len() != size { - return Err(Error::ExpressionUnexpectedType { ty: ty.clone() }).with_span(from); - } - array - .iter() - .map(|el_parse| Expression::analyze(el_parse, el_ty, scope)) - .collect::, Diagnostic>>() - .map(SingleExpressionInner::Array)? + // A size mismatch leaves the element type intact, so it stays + // the expected type; only a non-array annotation poisons it. + let el_ty: ResolvedType = match ty.as_array() { + Some((el_ty, size)) => { + if array.len() != size { + scope.report( + Error::ExpressionUnexpectedType { ty: ty.clone() } + .with_span(from.into()), + ); + } + el_ty.clone() + } + None => { + report_shape_mismatch(ty, *from.as_ref(), scope); + ResolvedType::error() + } + }; + + let types = vec![el_ty; array.len()]; + SingleExpressionInner::Array(analyze_children(array, &types, scope)) } parse::SingleExpressionInner::List(list) => { - let (el_ty, bound) = ty - .as_list() - .ok_or(Error::ExpressionUnexpectedType { ty: ty.clone() }) - .with_span(from)?; - if bound.get() <= list.len() { - return Err(Error::ExpressionUnexpectedType { ty: ty.clone() }).with_span(from); - } - list.iter() - .map(|e| Expression::analyze(e, el_ty, scope)) - .collect::, Diagnostic>>() - .map(SingleExpressionInner::List)? + let el_ty: ResolvedType = match ty.as_list() { + Some((el_ty, bound)) => { + if bound.get() <= list.len() { + scope.report( + Error::ExpressionUnexpectedType { ty: ty.clone() } + .with_span(from.into()), + ); + } + el_ty.clone() + } + None => { + report_shape_mismatch(ty, *from.as_ref(), scope); + ResolvedType::error() + } + }; + + let types = vec![el_ty; list.len()]; + SingleExpressionInner::List(analyze_children(list, &types, scope)) } parse::SingleExpressionInner::Either(either) => { - let (ty_l, ty_r) = ty - .as_either() - .ok_or(Error::ExpressionUnexpectedType { ty: ty.clone() }) - .with_span(from)?; - match either { - Either::Left(parse_l) => Expression::analyze(parse_l, ty_l, scope) - .map(Arc::new) - .map(Either::Left), - Either::Right(parse_r) => Expression::analyze(parse_r, ty_r, scope) - .map(Arc::new) - .map(Either::Right), - } - .map(SingleExpressionInner::Either)? + // A shape mismatch is reported, not returned: the wrapped child + // has its own errors either way, so it is analyzed against + // poison, the only expected type left. + let (ty_l, ty_r) = match ty.as_either() { + Some((ty_l, ty_r)) => (ty_l.clone(), ty_r.clone()), + None => { + report_shape_mismatch(ty, *from.as_ref(), scope); + (ResolvedType::error(), ResolvedType::error()) + } + }; + + let inner = match either { + Either::Left(parse_l) => { + Either::Left(Arc::new(analyze_child(parse_l, &ty_l, scope))) + } + Either::Right(parse_r) => { + Either::Right(Arc::new(analyze_child(parse_r, &ty_r, scope))) + } + }; + SingleExpressionInner::Either(inner) } parse::SingleExpressionInner::Option(maybe_parse) => { - let ty = ty - .as_option() - .ok_or(Error::ExpressionUnexpectedType { ty: ty.clone() }) - .with_span(from)?; - match maybe_parse { - Some(parse) => { - Some(Expression::analyze(parse, ty, scope).map(Arc::new)).transpose() + let inner_ty: ResolvedType = match ty.as_option() { + Some(inner_ty) => inner_ty.clone(), + None => { + report_shape_mismatch(ty, *from.as_ref(), scope); + ResolvedType::error() } - None => Ok(None), - } - .map(SingleExpressionInner::Option)? + }; + + let inner = maybe_parse + .as_ref() + .map(|parse| Arc::new(analyze_child(parse, &inner_ty, scope))); + SingleExpressionInner::Option(inner) } parse::SingleExpressionInner::Call(call) => { Call::analyze(call, ty, scope).map(SingleExpressionInner::Call)? @@ -1866,12 +2322,21 @@ impl AbstractSyntaxTree for SingleExpression { Match::analyze(match_, ty, scope).map(SingleExpressionInner::Match)? } parse::SingleExpressionInner::EnumConstruction(construction) => { - analyze_enum_construction(construction, ty, scope) - .map(SingleExpressionInner::EnumConstruction)? + if ty.is_error() { + // The variant is unknowable, but the payload arguments are + // independent of it: drain them at poison so their own + // errors still surface. + analyze_children(construction.args(), &[], scope); + SingleExpressionInner::Error + } else { + analyze_enum_construction(construction, ty, scope) + .map(SingleExpressionInner::EnumConstruction)? + } } parse::SingleExpressionInner::EnumMatch(enum_match) => { - EnumMatch::analyze(enum_match, ty, scope).map(SingleExpressionInner::EnumMatch)? + analyze_enum_match(enum_match, ty, scope)? } + parse::SingleExpressionInner::Error => SingleExpressionInner::Error, }; Ok(Self { @@ -1882,131 +2347,222 @@ impl AbstractSyntaxTree for SingleExpression { } } -impl AbstractSyntaxTree for EnumMatch { - type From = parse::EnumMatch; +/// Report a shape mismatch, unless the expected type is poison. +fn report_shape_mismatch(ty: &ResolvedType, span: Span, scope: &mut Scope) { + if !ty.is_error() { + scope.report(Error::ExpressionUnexpectedType { ty: ty.clone() }.with_span(span)); + } +} - fn analyze( - from: &Self::From, - ty: &ResolvedType, - scope: &mut Scope, - ) -> Result { - let arms = from.arms(); - let span = *from.span(); - debug_assert!(!arms.is_empty(), "the parser rejects empty enum matches"); +/// Surface the errors inside an enum match that cannot be analyzed structurally. +/// +/// The scrutinee and the arm bodies are independent of the variant bookkeeping +/// that failed — an unknown enum, an unknown or duplicated variant, a missing +/// one — so they are analyzed anyway. Without a known variant the declared +/// bindings cannot be trusted, so their identifiers are bound at poison, which +/// keeps the bodies from cascading into "undefined variable". +fn drain_enum_match(from: &parse::EnumMatch, ty: &ResolvedType, scope: &mut Scope) { + let span = *from.span(); + analyze_child(from.scrutinee(), &ResolvedType::error(), scope); + + for arm in from.arms() { + let guard = scope.block(); + for (pattern, declared) in arm.bindings() { + // Resolve to surface the declared type's own errors, but discard it. + guard.scope.resolve_or_poison(declared, span); + for (identifier, binding_ty) in poison_bindings(pattern) { + guard.scope.insert_variable(identifier, binding_ty); + } + } + analyze_child(arm.expression(), ty, guard.scope); + } +} + +/// Analyze an enum match into the expression node it yields. +/// +/// Returns the node rather than an [`EnumMatch`], because a match whose enum is +/// poisoned yields [`SingleExpressionInner::Error`] instead: there is no match +/// to build and nothing further to report. `Err` carries the one diagnostic the +/// caller should report; any others are reported here. +fn analyze_enum_match( + from: &parse::EnumMatch, + ty: &ResolvedType, + scope: &mut Scope, +) -> Result { + let arms = from.arms(); + let span = *from.span(); + debug_assert!(!arms.is_empty(), "the parser rejects empty enum matches"); + + let enum_name = arms[0].enum_path_string(); + let [single] = arms[0].enum_path() else { + drain_enum_match(from, ty, scope); + return Err(Error::Grammar { + msg: format!( + "`{enum_name}` does not name an enum; enums are declared at the \ + top level, so match arms name them by a single identifier" + ), + }) + .with_span(span); + }; + let alias = AliasName::from_str_unchecked(single.as_inner()); + let enum_ty = match scope.get_alias(&alias) { + Ok(enum_ty) => enum_ty, + Err(err) => { + drain_enum_match(from, ty, scope); + return Err(err).with_span(span); + } + }; + // A poisoned alias absorbs the check: whether it names an enum is + // unknowable, and its own error was already reported at the declaration. + if enum_ty.is_error() { + drain_enum_match(from, ty, scope); + return Ok(SingleExpressionInner::Error); + } - let enum_name = arms[0].enum_path_string(); - let [single] = arms[0].enum_path() else { + let info = match enum_ty.as_enum() { + Some(info) => info.clone(), + None => { + drain_enum_match(from, ty, scope); return Err(Error::Grammar { msg: format!( - "`{enum_name}` does not name an enum; enums are declared at the \ - top level, so match arms name them by a single identifier" + "`{enum_name}` is not an enum, so match arms of the form \ + `{enum_name}::Variant` cannot apply to it" ), }) .with_span(span); - }; - let alias = AliasName::from_str_unchecked(single.as_inner()); - let enum_ty = scope.get_alias(&alias).with_span(span)?; - let info = match enum_ty.as_enum() { - Some(info) => info.clone(), - None => { - return Err(Error::Grammar { - msg: format!( - "`{enum_name}` is not an enum, so match arms of the form \ - `{enum_name}::Variant` cannot apply to it" - ), - }) - .with_span(span) - } - }; + } + }; - // One slot per variant, in declaration order. - // the order of the leaves of the enum's balanced sum. - let mut arms_by_index: Vec> = - vec![None; info.variants().len()]; - for arm in arms { - if arm.enum_path() != arms[0].enum_path() { - return Err(Error::Grammar { + // One slot per variant, in declaration order. + // the order of the leaves of the enum's balanced sum. + let mut arms_by_index: Vec> = vec![None; info.variants().len()]; + // The arms are independent: a wrong enum path, an unknown variant or a + // duplicate says nothing about the arm beside it, so the loop records + // each and keeps going. + let mut arm_errors: Vec = Vec::new(); + for arm in arms { + if arm.enum_path() != arms[0].enum_path() { + arm_errors.push( + Error::Grammar { msg: format!( "all match arms must use the same enum; expected '{}', found '{}'", enum_name, arm.enum_path_string() ), - }) - .with_span(span); - } - let (index, _) = info - .variant(arm.variant()) - .ok_or_else(|| Error::Grammar { + } + .with_span(span), + ); + continue; + } + let Some((index, _)) = info.variant(arm.variant()) else { + arm_errors.push( + Error::Grammar { msg: format!( "variant '{}' is not defined in enum '{}'", arm.variant(), enum_name ), - }) - .with_span(span)?; - let slot = &mut arms_by_index[index]; - if slot.is_some() { - return Err(Error::Grammar { + } + .with_span(span), + ); + continue; + }; + let slot = &mut arms_by_index[index]; + if slot.is_some() { + arm_errors.push( + Error::Grammar { msg: format!("duplicate arm for variant '{}'", arm.variant()), - }) - .with_span(span); - } - *slot = Some(arm); + } + .with_span(span), + ); + continue; } + *slot = Some(arm); + } - // One collect: Some(arms) iff every variant is covered. - let covered: Option> = arms_by_index.iter().copied().collect(); - let Some(covered) = covered else { - let missing: Vec = arms_by_index - .iter() - .zip(info.variants()) - .filter(|(slot, _)| slot.is_none()) - .map(|(_, variant)| format!("'{}'", variant.name())) - .collect(); - return Err(Error::Grammar { - msg: format!( - "enum match on '{}' must cover all {} variants; missing: {}", - enum_name, - info.variants().len(), - missing.join(", ") - ), - }) - .with_span(span); - }; - - // Analyze the scrutinee against the nominal enum type, so that - // matching a value of a different enum (or any other type) against - // this enum's variants is a type error. - let scrutinee = Expression::analyze(from.scrutinee(), &enum_ty, scope).map(Arc::new)?; + if !arm_errors.is_empty() { + drain_enum_match(from, ty, scope); + // The caller reports the one we return, so the rest go in here. + let first = arm_errors.remove(0); + for diag in arm_errors { + scope.report(diag); + } + return Err(first); + } - let arm_asts = covered - .into_iter() + // One collect: Some(arms) iff every variant is covered. + let covered: Option> = arms_by_index.iter().copied().collect(); + let Some(covered) = covered else { + let missing: Vec = arms_by_index + .iter() .zip(info.variants()) - .map(|(arm, variant)| { - let arm_span = *arm.span(); - let pattern = analyze_enum_arm_bindings(arm, variant, scope, arm_span)?; - scope.enter_block(); - let payload_ty = variant.payload_type(); - let typed_variables = pattern.is_of_type(payload_ty).with_span(arm_span)?; - for (identifier, variable_ty) in typed_variables { - scope.insert_variable(identifier, variable_ty); + .filter(|(slot, _)| slot.is_none()) + .map(|(_, variant)| format!("'{}'", variant.name())) + .collect(); + drain_enum_match(from, ty, scope); + return Err(Error::Grammar { + msg: format!( + "enum match on '{}' must cover all {} variants; missing: {}", + enum_name, + info.variants().len(), + missing.join(", ") + ), + }) + .with_span(span); + }; + + // Analyze the scrutinee against the nominal enum type, so that + // matching a value of a different enum (or any other type) against + // this enum's variants is a type error. + // A poisoned scrutinee must not suppress the arm-body errors. + let scrutinee = Arc::new(analyze_child(from.scrutinee(), &enum_ty, scope)); + + let arm_asts = covered + .into_iter() + .zip(info.variants()) + .map(|(arm, variant)| { + let arm_span = *arm.span(); + // A failed binding poisons only this arm; later arms still run. + let (pattern, fits) = analyze_enum_arm_bindings(arm, variant, scope, arm_span); + + let guard = scope.block(); + + // When the arity is wrong the combined pattern cannot match the + // payload, and saying so again would only restate the arity + // error. Check it against poison instead of skipping the check: + // poison absorbs the *shape* mismatch while every independent + // check still runs, so a reused name or a malformed nested + // pattern is still caught. + let expected = if fits { + variant.payload_type().clone() + } else { + ResolvedType::error() + }; + let typed_variables = match pattern.is_of_type(&expected) { + Ok(variables) => variables, + Err(err) => { + guard.scope.report(err.with_span(arm_span)); + poison_bindings(&pattern) } - let body = Expression::analyze(arm.expression(), ty, scope).map(Arc::new); - scope.exit_block(); - Ok(EnumMatchArm { - pattern, - body: body?, - span: arm_span, - }) - }) - .collect::, Diagnostic>>()?; + }; + for (identifier, variable_ty) in typed_variables { + guard.scope.insert_variable(identifier, variable_ty); + } + let body = Arc::new(analyze_child(arm.expression(), ty, guard.scope)); - Ok(Self { - scrutinee, - arms: arm_asts, - span, + EnumMatchArm { + pattern, + body, + span: arm_span, + } }) - } + .collect::>(); + + Ok(SingleExpressionInner::EnumMatch(EnumMatch { + scrutinee, + arms: arm_asts, + span, + })) } /// Check an enum match arm's payload bindings against the variant's declared @@ -2015,35 +2571,53 @@ impl AbstractSyntaxTree for EnumMatch { /// Unit variants bind nothing ([`Pattern::Ignore`]); a single binding stands /// alone; multiple bindings form a tuple pattern, matching the tuple that a /// multi-payload variant carries at its leaf. +/// Reports rather than returns: an arm whose bindings do not fit the variant +/// still declares its identifiers, and every binding is checked, so one bad +/// declared type neither hides the next nor drops the arm's names. +/// +/// Also returns whether the pattern *fits* the variant's payload. When it does +/// not, the arity error was already reported here, and the caller must bind the +/// identifiers at poison rather than check the combined pattern against the +/// payload type. fn analyze_enum_arm_bindings( arm: &parse::EnumMatchArm, variant: &EnumVariantInfo, - scope: &Scope, + scope: &mut Scope, span: Span, -) -> Result { - if arm.bindings().len() != variant.payload().len() { - return Err(Error::Grammar { - msg: format!( - "variant '{}' of enum '{}' carries {} payload value(s), \ - but the arm binds {}", - arm.variant(), - arm.enum_path_string(), - variant.payload().len(), - arm.bindings().len() - ), - }) - .with_span(span); +) -> (Pattern, bool) { + let fits = arm.bindings().len() == variant.payload().len(); + if !fits { + scope.report( + Error::Grammar { + msg: format!( + "variant '{}' of enum '{}' carries {} payload value(s), \ + but the arm binds {}", + arm.variant(), + arm.enum_path_string(), + variant.payload().len(), + arm.bindings().len() + ), + } + .with_span(span), + ); } let mut patterns = Vec::with_capacity(arm.bindings().len()); - for ((pattern, declared), payload_ty) in arm.bindings().iter().zip(variant.payload()) { - let declared = scope.resolve(declared).with_span(span)?; - if &declared != payload_ty { - return Err(Error::ExpressionTypeMismatch { - expected: payload_ty.clone(), - found: declared, - }) - .with_span(span); + for (i, (pattern, declared)) in arm.bindings().iter().enumerate() { + let declared = scope.resolve_or_poison(declared, span); + + // Compare only against a payload slot that exists; a count mismatch + // was already reported above. + if let Some(payload_ty) = variant.payload().get(i) { + if !declared.compatible(payload_ty) { + scope.report( + Error::ExpressionTypeMismatch { + expected: payload_ty.clone(), + found: declared, + } + .with_span(span), + ); + } } patterns.push(pattern.clone()); } @@ -2053,7 +2627,7 @@ fn analyze_enum_arm_bindings( 1 => patterns[0].clone(), _ => Pattern::tuple(patterns), }; - Ok(pattern) + (pattern, fits) } impl AbstractSyntaxTree for Call { @@ -2082,7 +2656,7 @@ impl AbstractSyntaxTree for Call { observed_ty: &ResolvedType, expected_ty: &ResolvedType, ) -> Result<(), Error> { - if observed_ty == expected_ty { + if observed_ty.compatible(expected_ty) { Ok(()) } else { Err(Error::ExpressionTypeMismatch { @@ -2092,20 +2666,38 @@ impl AbstractSyntaxTree for Call { } } - fn analyze_arguments( - parse_args: &[parse::Expression], - args_tys: &[ResolvedType], - scope: &mut Scope, - ) -> Result, Diagnostic> { - let args = parse_args - .iter() - .zip(args_tys.iter()) - .map(|(arg_parse, arg_ty)| Expression::analyze(arg_parse, arg_ty, scope)) - .collect::, Diagnostic>>()?; - Ok(args) + /// Report a callee-level failure without returning. + /// + /// The arguments are analyzed independently of the arity and of the + /// output type, so a failure here must not stop them from surfacing + /// their own errors. + fn report_check(result: Result<(), Error>, scope: &mut Scope, span: Span) { + if let Err(err) = result { + scope.report(err.with_span(span)); + } + } + + let span = *from.as_ref(); + + let name = match CallName::analyze(from, ty, scope) { + Ok(n) => n, + Err(name_err) => { + // callee unknown: analyze args against error() to surface their errors, + // then propagate the name error (do not double-report, because container reports it) + analyze_children(from.args(), &[], scope); + return Err(name_err); + } + }; + + if name.is_error() { + let args = analyze_children(from.args(), &[], scope); + return Ok(Self { + name, + args, + span: *from.as_ref(), + }); } - let name = CallName::analyze(from, ty, scope)?; let args = match name.clone() { CallName::Jet(jet) => { let args_tys = source_type(&*jet) @@ -2114,64 +2706,76 @@ impl AbstractSyntaxTree for Call { .collect::, AliasName>>() .map_err(|alias| Error::UndefinedAlias { name: alias }) .with_span(from)?; - check_argument_types(from.args(), &args_tys).with_span(from)?; + report_check(check_argument_types(from.args(), &args_tys), scope, span); + let out_ty = target_type(&*jet) .resolve_builtin() .map_err(|alias| Error::UndefinedAlias { name: alias }) .with_span(from)?; - check_output_type(&out_ty, ty).with_span(from)?; + report_check(check_output_type(&out_ty, ty), scope, span); + scope.track_call(from, TrackedCallName::Jet); - analyze_arguments(from.args(), &args_tys, scope)? + analyze_children(from.args(), &args_tys, scope) } CallName::UnwrapLeft(right_ty) => { let args_tys = [ResolvedType::either(ty.clone(), right_ty)]; - check_argument_types(from.args(), &args_tys).with_span(from)?; - let args = analyze_arguments(from.args(), &args_tys, scope)?; + report_check(check_argument_types(from.args(), &args_tys), scope, span); + + let args = analyze_children(from.args(), &args_tys, scope); let [arg_ty] = args_tys; + scope.track_call(from, TrackedCallName::UnwrapLeft(arg_ty)); args } CallName::UnwrapRight(left_ty) => { let args_tys = [ResolvedType::either(left_ty, ty.clone())]; - check_argument_types(from.args(), &args_tys).with_span(from)?; - let args = analyze_arguments(from.args(), &args_tys, scope)?; + report_check(check_argument_types(from.args(), &args_tys), scope, span); + + let args = analyze_children(from.args(), &args_tys, scope); let [arg_ty] = args_tys; scope.track_call(from, TrackedCallName::UnwrapRight(arg_ty)); args } CallName::IsNone(some_ty) => { let args_tys = [ResolvedType::option(some_ty)]; - check_argument_types(from.args(), &args_tys).with_span(from)?; + report_check(check_argument_types(from.args(), &args_tys), scope, span); + let out_ty = ResolvedType::boolean(); - check_output_type(&out_ty, ty).with_span(from)?; - analyze_arguments(from.args(), &args_tys, scope)? + report_check(check_output_type(&out_ty, ty), scope, span); + analyze_children(from.args(), &args_tys, scope) } CallName::Unwrap => { let args_tys = [ResolvedType::option(ty.clone())]; - check_argument_types(from.args(), &args_tys).with_span(from)?; + report_check(check_argument_types(from.args(), &args_tys), scope, span); + scope.track_call(from, TrackedCallName::Unwrap); - analyze_arguments(from.args(), &args_tys, scope)? + analyze_children(from.args(), &args_tys, scope) } CallName::Assert => { let args_tys = [ResolvedType::boolean()]; - check_argument_types(from.args(), &args_tys).with_span(from)?; + report_check(check_argument_types(from.args(), &args_tys), scope, span); + let out_ty = ResolvedType::unit(); - check_output_type(&out_ty, ty).with_span(from)?; + report_check(check_output_type(&out_ty, ty), scope, span); + scope.track_call(from, TrackedCallName::Assert); - analyze_arguments(from.args(), &args_tys, scope)? + analyze_children(from.args(), &args_tys, scope) } CallName::Panic => { let args_tys = []; - check_argument_types(from.args(), &args_tys).with_span(from)?; + report_check(check_argument_types(from.args(), &args_tys), scope, span); + // panic! allows every output type because it will never return anything scope.track_call(from, TrackedCallName::Panic); - analyze_arguments(from.args(), &args_tys, scope)? + analyze_children(from.args(), &args_tys, scope) } CallName::Debug => { let args_tys = [ty.clone()]; - check_argument_types(from.args(), &args_tys).with_span(from)?; - let args = analyze_arguments(from.args(), &args_tys, scope)?; + report_check(check_argument_types(from.args(), &args_tys), scope, span); + + let args = analyze_children(from.args(), &args_tys, scope); let [arg_ty] = args_tys; + scope.track_call(from, TrackedCallName::Debug(arg_ty)); args } @@ -2180,19 +2784,26 @@ impl AbstractSyntaxTree for Call { // every enum must map to itself at its structural position // (see `cast_preserves_enum_identity`), else same-shaped // enums would convert variants by ordinal position. - if !cast_preserves_enum_identity(&source, ty) - || StructuralType::from(&source) != StructuralType::from(ty) + // A poisoned type on either side absorbs the check: the cause was + // already reported, and `StructuralType` cannot represent poison + // (converting one panics, by design — see the gate). + let comparable = !source.contains_error() && !ty.contains_error(); + if comparable + && (!cast_preserves_enum_identity(&source, ty) + || StructuralType::from(&source) != StructuralType::from(ty)) { - return Err(Error::InvalidCast { - source, - target: ty.clone(), - }) - .with_span(from); + scope.report( + Error::InvalidCast { + source: source.clone(), + target: ty.clone(), + } + .with_span(span), + ); } let args_tys = [source]; - check_argument_types(from.args(), &args_tys).with_span(from)?; - analyze_arguments(from.args(), &args_tys, scope)? + report_check(check_argument_types(from.args(), &args_tys), scope, span); + analyze_children(from.args(), &args_tys, scope) } CallName::Custom(function) => { let args_ty = function @@ -2201,10 +2812,11 @@ impl AbstractSyntaxTree for Call { .map(FunctionParam::ty) .cloned() .collect::>(); - check_argument_types(from.args(), &args_ty).with_span(from)?; + report_check(check_argument_types(from.args(), &args_ty), scope, span); + let out_ty = function.body().ty(); - check_output_type(out_ty, ty).with_span(from)?; - analyze_arguments(from.args(), &args_ty, scope)? + report_check(check_output_type(out_ty, ty), scope, span); + analyze_children(from.args(), &args_ty, scope) } CallName::Fold(function, bound) => { // A list fold has the signature: @@ -2220,11 +2832,12 @@ impl AbstractSyntaxTree for Call { .ty() .clone(); let args_ty = [list_ty, accumulator_ty]; + report_check(check_argument_types(from.args(), &args_ty), scope, span); - check_argument_types(from.args(), &args_ty).with_span(from)?; let out_ty = function.body().ty(); - check_output_type(out_ty, ty).with_span(from)?; - analyze_arguments(from.args(), &args_ty, scope)? + report_check(check_output_type(out_ty, ty), scope, span); + + analyze_children(from.args(), &args_ty, scope) } CallName::ArrayFold(function, size) => { // An array fold has the signature: @@ -2240,11 +2853,12 @@ impl AbstractSyntaxTree for Call { .ty() .clone(); let args_ty = [array_ty, accumulator_ty]; + report_check(check_argument_types(from.args(), &args_ty), scope, span); - check_argument_types(from.args(), &args_ty).with_span(from)?; let out_ty = function.body().ty(); - check_output_type(out_ty, ty).with_span(from)?; - analyze_arguments(from.args(), &args_ty, scope)? + report_check(check_output_type(out_ty, ty), scope, span); + + analyze_children(from.args(), &args_ty, scope) } CallName::ForWhile(function, _bit_width) => { // A for-while loop has the signature: @@ -2265,11 +2879,12 @@ impl AbstractSyntaxTree for Call { .ty() .clone(); let args_ty = [accumulator_ty, context_ty]; + report_check(check_argument_types(from.args(), &args_ty), scope, span); - check_argument_types(from.args(), &args_ty).with_span(from)?; let out_ty = function.body().ty(); - check_output_type(out_ty, ty).with_span(from)?; - analyze_arguments(from.args(), &args_ty, scope)? + report_check(check_output_type(out_ty, ty), scope, span); + + analyze_children(from.args(), &args_ty, scope) } }; @@ -2281,6 +2896,18 @@ impl AbstractSyntaxTree for Call { } } +/// Does the function have a foldable signature, `fn f(element: E, acc: A) -> A`? +/// +/// Compares with [`ResolvedType::compatible`] rather than `==`: while any type +/// in the signature is poisoned the answer is unknowable, so the check is +/// absorbed rather than failed, which would cascade on top of the type's own +/// error. +fn is_foldable(function: &CustomFunction) -> bool { + function.is_error() + || (function.params().len() == 2 + && function.params()[1].ty().compatible(function.body().ty())) +} + impl AbstractSyntaxTree for CallName { // Take parse::Call, so we have access to the span for pretty errors type From = parse::Call; @@ -2295,24 +2922,27 @@ impl AbstractSyntaxTree for CallName { Some(jet) if !jet.is_disabled() => Ok(Self::Jet(jet)), _ => Err(Error::JetDoesNotExist { name: name.clone() }).with_span(from), }, - parse::CallName::UnwrapLeft(right_ty) => scope - .resolve(right_ty) - .map(Self::UnwrapLeft) - .with_span(from), - parse::CallName::UnwrapRight(left_ty) => scope - .resolve(left_ty) - .map(Self::UnwrapRight) - .with_span(from), - parse::CallName::IsNone(some_ty) => { - scope.resolve(some_ty).map(Self::IsNone).with_span(from) - } + // A builtin's type argument resolves like any other type: every + // undefined alias in it is independent, so all of them are reported + // and the call proceeds against a poisoned type argument. + parse::CallName::UnwrapLeft(right_ty) => Ok(Self::UnwrapLeft( + scope.resolve_or_poison(right_ty, *from.as_ref()), + )), + parse::CallName::UnwrapRight(left_ty) => Ok(Self::UnwrapRight( + scope.resolve_or_poison(left_ty, *from.as_ref()), + )), + parse::CallName::IsNone(some_ty) => Ok(Self::IsNone( + scope.resolve_or_poison(some_ty, *from.as_ref()), + )), parse::CallName::Unwrap => Ok(Self::Unwrap), parse::CallName::Assert => Ok(Self::Assert), parse::CallName::Panic => Ok(Self::Panic), parse::CallName::Debug => Ok(Self::Debug), - parse::CallName::TypeCast(target) => { - scope.resolve(target).map(Self::TypeCast).with_span(from) - } + // Safe to poison: the cast's structural check already skips a + // poisoned side, which `StructuralType` cannot represent. + parse::CallName::TypeCast(target) => Ok(Self::TypeCast( + scope.resolve_or_poison(target, *from.as_ref()), + )), parse::CallName::Custom(name) => { scope.get_function(name).map(Self::Custom).with_span(from) } @@ -2320,26 +2950,32 @@ impl AbstractSyntaxTree for CallName { let function = scope.get_function(name).with_span(from)?; // A function that is used in a array fold has the signature: // fn f(element: E, accumulator: A) -> A - if function.params().len() != 2 || function.params()[1].ty() != function.body().ty() - { - Err(Error::FunctionNotFoldable { name: name.clone() }).with_span(from) - } else { + // + // `compatible` rather than `==`: while the signature holds a + // poisoned type, foldability is unknowable, so the check is + // absorbed instead of failed. + if is_foldable(&function) { Ok(Self::ArrayFold(function, *size)) + } else { + Err(Error::FunctionNotFoldable { name: name.clone() }).with_span(from) } } parse::CallName::Fold(name, bound) => { let function = scope.get_function(name).with_span(from)?; // A function that is used in a list fold has the signature: // fn f(element: E, accumulator: A) -> A - if function.params().len() != 2 || function.params()[1].ty() != function.body().ty() - { - Err(Error::FunctionNotFoldable { name: name.clone() }).with_span(from) - } else { + if is_foldable(&function) { Ok(Self::Fold(function, *bound)) + } else { + Err(Error::FunctionNotFoldable { name: name.clone() }).with_span(from) } } parse::CallName::ForWhile(name) => { let function = scope.get_function(name).with_span(from)?; + // A poisoned signature has no shape to check against. + if function.is_error() { + return Ok(Self::ForWhile(function, Pow2Usize::ONE)); + } // 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 @@ -2348,7 +2984,11 @@ impl AbstractSyntaxTree for CallName { return Err(Error::FunctionNotLoopable { name: name.clone() }).with_span(from); } match function.body().ty().as_either() { - Some((_, out_r)) if out_r == function.params().first().unwrap().ty() => {} + Some((_, out_r)) + if out_r.compatible(function.params().first().unwrap().ty()) => {} + // A poisoned body type absorbs the shape check: whether it + // is the required `Either` is unknowable. + _ if function.body().ty().is_error() => {} _ => { return Err(Error::FunctionNotLoopable { name: name.clone() }) .with_span(from); @@ -2357,7 +2997,9 @@ impl AbstractSyntaxTree for CallName { // Disable loops for u32 or higher since no one will want to run // 2^32 = 4294967296 ≈ 4 billion iterations. // The resulting Simplicity program will not fit into a Bitcoin block. - match function.params().get(2).unwrap().ty().as_integer() { + let counter_ty = function.params().get(2).unwrap().ty(); + + match counter_ty.as_integer() { Some( int_ty @ (UIntType::U1 | UIntType::U2 @@ -2365,6 +3007,14 @@ impl AbstractSyntaxTree for CallName { | UIntType::U8 | UIntType::U16), ) => Ok(Self::ForWhile(function, int_ty.bit_width())), + + // A poisoned counter has no knowable width, so whether the + // function loops is unknowable too: absorb the check rather + // than cascade on top of the type's own error. The width is + // a placeholder, like every other poison substitution — the + // program has an error, so the gate stops it before codegen + // can read it. + None if counter_ty.is_error() => Ok(Self::ForWhile(function, Pow2Usize::ONE)), _ => Err(Error::FunctionNotLoopable { name: name.clone() }).with_span(from), } } @@ -2380,31 +3030,73 @@ impl AbstractSyntaxTree for Match { ty: &ResolvedType, scope: &mut Scope, ) -> Result { - let scrutinee_ty = from.scrutinee_type(); - let scrutinee_ty = scope.resolve(&scrutinee_ty).with_span(from)?; - let scrutinee = - Expression::analyze(from.scrutinee(), &scrutinee_ty, scope).map(Arc::new)?; - - scope.enter_block(); - if let Some((pat_l, ty_l)) = from.left().pattern().as_typed_pattern() { - let ty_l = scope.resolve(ty_l).with_span(from.left())?; - let typed_variables = pat_l.is_of_type(&ty_l).with_span(from.left())?; + //let span = *from.as_ref(); + + // Resolve each arm's declared type exactly once, poison-tolerantly. A + // broken type in one arm must neither abort the match nor be reported + // twice — once for the scrutinee's composite type and once for the arm. + let left = from + .left() + .pattern() + .as_typed_pattern() + .map(|(pat, aliased)| (pat, scope.resolve_or_poison(aliased, *from.left().span()))); + let right = from + .right() + .pattern() + .as_typed_pattern() + .map(|(pat, aliased)| (pat, scope.resolve_or_poison(aliased, *from.right().span()))); + + // Rebuild the scrutinee's type from the arms, mirroring + // `parse::Match::scrutinee_type` but over the resolved parts. + let scrutinee_ty = match (from.left().pattern(), from.right().pattern()) { + (MatchPattern::Left(..), MatchPattern::Right(..)) => { + let (_, ty_l) = left.as_ref().expect("left arm binds a type"); + let (_, ty_r) = right.as_ref().expect("right arm binds a type"); + ResolvedType::either(ty_l.clone(), ty_r.clone()) + } + (MatchPattern::None, MatchPattern::Some(..)) => { + let (_, ty_r) = right.as_ref().expect("some arm binds a type"); + ResolvedType::option(ty_r.clone()) + } + (MatchPattern::False, MatchPattern::True) => ResolvedType::boolean(), + _ => unreachable!("Match expressions have valid left and right arms"), + }; + + // A poisoned scrutinee must not suppress the arm-body errors. + let scrutinee = Arc::new(analyze_child(from.scrutinee(), &scrutinee_ty, scope)); + + let guard = scope.block(); + if let Some((pat_l, ty_l)) = &left { + let typed_variables = match pat_l.is_of_type(ty_l) { + Ok(vars) => vars, + Err(err) => { + guard.scope.report(err.with_span(*from.left().span())); + poison_bindings(pat_l) + } + }; for (identifier, ty) in typed_variables { - scope.insert_variable(identifier, ty); + guard.scope.insert_variable(identifier, ty); } } - let ast_l = Expression::analyze(from.left().expression(), ty, scope).map(Arc::new)?; - scope.exit_block(); - scope.enter_block(); - if let Some((pat_r, ty_r)) = from.right().pattern().as_typed_pattern() { - let ty_r = scope.resolve(ty_r).with_span(from.right())?; - let typed_variables = pat_r.is_of_type(&ty_r).with_span(from.right())?; + + let ast_l = Arc::new(analyze_child(from.left().expression(), ty, guard.scope)); + drop(guard); + + let guard = scope.block(); + if let Some((pat_r, ty_r)) = &right { + let typed_variables = match pat_r.is_of_type(ty_r) { + Ok(vars) => vars, + Err(err) => { + guard.scope.report(err.with_span(*from.right().span())); + poison_bindings(pat_r) + } + }; + for (identifier, ty) in typed_variables { - scope.insert_variable(identifier, ty); + guard.scope.insert_variable(identifier, ty); } } - let ast_r = Expression::analyze(from.right().expression(), ty, scope).map(Arc::new)?; - scope.exit_block(); + let ast_r = Arc::new(analyze_child(from.right().expression(), ty, guard.scope)); Ok(Self { scrutinee, @@ -2487,7 +3179,7 @@ mod span_tests { fn analyzed_custom_function_preserves_declaration_and_parameter_spans() { let source = "fn helper(value: u8) -> u8 { value }"; let parsed = parse::Function::parse_from_str(source).expect("function parses"); - let mut scope = Scope::new(Box::new(ElementsJetHinter)); + let mut scope = Scope::new(Box::new(ElementsJetHinter), Vec::new()); Function::analyze(&parsed, &ResolvedType::unit(), &mut scope).expect("function analyzes"); let function = scope @@ -2510,9 +3202,7 @@ mod span_tests { Right(right: u8) => {}, } }"#; - let parsed = parse::Program::parse_from_str(source).expect("program parses"); - let program = - Program::analyze(&parsed, Box::new(ElementsJetHinter)).expect("program analyzes"); + let program = analyzed(source); let ExpressionInner::Block(_, Some(last)) = program.main().inner() else { panic!("main body should end in a match"); @@ -2544,9 +3234,7 @@ fn main() { Choice::Second => {}, } }"#; - let parsed = parse::Program::parse_from_str(source).expect("program parses"); - let program = - Program::analyze(&parsed, Box::new(ElementsJetHinter)).expect("program analyzes"); + let program = analyzed(source); let ExpressionInner::Block(_, Some(last)) = program.main().inner() else { panic!("main body should end in an enum match"); @@ -2570,33 +3258,83 @@ fn main() { } #[cfg(test)] -mod scope_resolution_tests { - use super::{ElementsJetHinter, Program}; +pub(super) fn analyze_multifile(files: Vec<(&str, &str)>) -> Result<(), DiagnosticManager> { use crate::driver::tests::setup_graph; - pub(super) fn analyze_multifile(files: Vec<(&str, &str)>) -> Result<(), String> { - let (graph, _ids, _dir, mut diagnostics) = setup_graph(files); + let (graph, _ids, _dir, mut diagnostics) = setup_graph(files); - let Some(driver_program) = graph.linearize_and_assemble(&mut diagnostics) else { - return Err(diagnostics.render_to_string()); - }; + let Some(driver_program) = graph.linearize_and_assemble(&mut diagnostics) else { + return Err(diagnostics); + }; - Program::analyze(&driver_program, Box::new(ElementsJetHinter)) - .map(|_| ()) - .map_err(|e| e.to_string()) + if diagnostics.has_errors() { + return Err(diagnostics); } - #[test] - fn private_type_alias_from_dependency_does_not_leak() { - let result = analyze_multifile(vec![ - ( - "main.simf", - "use lib::A::helper; fn main() { helper(); let x: Secret = 0; }", - ), - ("libs/lib/A.simf", "type Secret = u32; pub fn helper() {}"), - ]); + if Program::analyze( + &driver_program, + Box::new(ElementsJetHinter), + &mut diagnostics, + ) + .is_none() + { + return Err(diagnostics); + } - assert!( + Ok(()) +} + +/// All diagnostics for a multi-file program, empty if it compiled. +/// +/// The sink is the source of truth for whether a program is correct, so tests +/// assert on it directly rather than on a `Result`: no `expect_err` to reach +/// the diagnostics, and a passing program is simply an empty manager. +#[cfg(test)] +pub(super) fn errors_multifile(files: Vec<(&str, &str)>) -> DiagnosticManager { + analyze_multifile(files).err().unwrap_or_default() +} + +/// All diagnostics for a single-file program, empty if it compiled. +#[cfg(test)] +pub(super) fn errors(src: &str) -> DiagnosticManager { + errors_multifile(vec![("main.simf", src)]) +} + +/// The analyzed AST of a single-file program that is expected to compile. +/// +/// Goes through the in-memory parse/analyze path rather than the driver, so +/// spans stay relative to `src`: the driver flattens each file into a +/// `mod unit_N` block, which shifts every offset. +#[cfg(test)] +pub(super) fn analyzed(src: &str) -> Program { + use crate::parse::ParseFromStr; + + let parsed = parse::Program::parse_from_str(src).expect("program parses"); + let mut diagnostics = DiagnosticManager::new(); + let program = Program::analyze(&parsed, Box::new(ElementsJetHinter), &mut diagnostics); + + assert!( + !diagnostics.has_errors(), + "program must analyze:\n{diagnostics}" + ); + program.expect("analysis yields a program when it reports no errors") +} + +#[cfg(test)] +mod scope_resolution_tests { + use super::analyze_multifile; + + #[test] + fn private_type_alias_from_dependency_does_not_leak() { + let result = analyze_multifile(vec![ + ( + "main.simf", + "use lib::A::helper; fn main() { helper(); let x: Secret = 0; }", + ), + ("libs/lib/A.simf", "type Secret = u32; pub fn helper() {}"), + ]); + + assert!( result.is_err(), "private alias from another file leaked into root scope: {result:?}" ); @@ -2671,7 +3409,9 @@ mod scope_resolution_tests { ("main.simf", "use lib::B::foo; fn main() {}"), ]); - let err = result.expect_err("Private imports should not be accessible"); + let err = result + .expect_err("Private imports should not be accessible") + .to_string(); assert!(err.contains("private") || err.contains("foo")); } @@ -2695,7 +3435,9 @@ mod scope_resolution_tests { fn test_public_main_is_forbidden() { let result = analyze_multifile(vec![("main.simf", "pub fn main() {}")]); - let err = result.expect_err("Public main should be rejected"); + let err = result + .expect_err("Public main should be rejected") + .to_string(); assert!(err.contains("Main") && err.contains("public")); } @@ -2706,7 +3448,9 @@ mod scope_resolution_tests { ("main.simf", "use lib::A::bar as main; fn main() {}"), ]); - let err = result.expect_err("Aliasing to main should be rejected"); + let err = result + .expect_err("Aliasing to main should be rejected") + .to_string(); assert!(err.contains("Main") && err.contains("alias")); } @@ -2721,7 +3465,9 @@ mod scope_resolution_tests { ), ]); - let err = result.expect_err("Using the original unaliased name 'foo' should fail"); + let err = result + .expect_err("Using the original unaliased name 'foo' should fail") + .to_string(); assert!(err.contains("foo") && (err.contains("not defined") || err.contains("unresolved"))); } @@ -2748,7 +3494,9 @@ mod scope_resolution_tests { ("main.simf", "use lib::A::secret as my_secret; fn main() {}"), ]); - let err = result.expect_err("Aliasing a private item should fail"); + let err = result + .expect_err("Aliasing a private item should fail") + .to_string(); assert!(err.contains("secret") && err.contains("private")); } @@ -2777,7 +3525,9 @@ mod scope_resolution_tests { ("main.simf", "use lib::B::hidden_alias; fn main() {}"), ]); - let err = result.expect_err("Private intermediate aliases should block resolution"); + let err = result + .expect_err("Private intermediate aliases should block resolution") + .to_string(); assert!(err.contains("hidden_alias") && err.contains("private")); } @@ -2792,7 +3542,9 @@ mod scope_resolution_tests { ), ]); - let err = result.expect_err("Duplicate names in scope should fail"); + let err = result + .expect_err("Duplicate names in scope should fail") + .to_string(); assert!(err.contains("foo") && err.contains("multiple times")); } @@ -2806,7 +3558,9 @@ mod scope_resolution_tests { ), ]); - let err = result.expect_err("Alias reusing a local name should fail"); + let err = result + .expect_err("Alias reusing a local name should fail") + .to_string(); assert!(err.contains("foo") && err.contains("multiple times")); } @@ -2824,7 +3578,9 @@ mod scope_resolution_tests { ), ]); - let err = result.expect_err("Duplicate function import should fail"); + let err = result + .expect_err("Duplicate function import should fail") + .to_string(); // It shouldn't just complain about the private type `foo`; it must also // complain that `foo` was imported twice! @@ -2842,7 +3598,9 @@ mod scope_resolution_tests { ), ]); - let err = result.expect_err("Build should fail on the unresolved import"); + let err = result + .expect_err("Build should fail on the unresolved import") + .to_string(); // It should complain about `missing`, but NOT about `foo` being duplicated, // because the first import failed and never actually reserved the name `foo`. @@ -2860,8 +3618,9 @@ mod scope_resolution_tests { ), ]); - let err = - result.expect_err("Build should fail when a local definition reuses an alias name"); + let err = result + .expect_err("Build should fail when a local definition reuses an alias name") + .to_string(); assert!(err.contains("foo") && err.contains("multiple times")); } @@ -2875,15 +3634,16 @@ mod scope_resolution_tests { ), ]); - let err = - result.expect_err("Build should fail when a local definition reuses an alias name"); + let err = result + .expect_err("Build should fail when a local definition reuses an alias name") + .to_string(); assert!(err.contains("foo") && err.contains("multiple times")); } } #[cfg(test)] mod module_tests { - use crate::ast::scope_resolution_tests::analyze_multifile; + use super::analyze_multifile; #[test] fn test_public_nested_modules_are_accessible() { @@ -2919,7 +3679,9 @@ mod module_tests { ), ]); - let err = result.expect_err("Private inner module must block access"); + let err = result + .expect_err("Private inner module must block access") + .to_string(); assert!(err.contains("inner") && err.contains("private")); } @@ -2946,7 +3708,9 @@ mod module_tests { "mod inner {} mod inner {} fn main() {}", )]); - let err = result.expect_err("Duplicate mod blocks must fail"); + let err = result + .expect_err("Duplicate mod blocks must fail") + .to_string(); assert!(err.contains("inner") && err.contains("multiple times")); } @@ -3031,7 +3795,9 @@ mod module_tests { ", )]); - let err = result.expect_err("Private inline items must remain hidden from siblings"); + let err = result + .expect_err("Private inline items must remain hidden from siblings") + .to_string(); assert!(err.contains("secret_toy") && err.contains("private")); } @@ -3049,7 +3815,9 @@ mod module_tests { ", )]); - let err = result.expect_err("The root file scope must respect inline module privacy"); + let err = result + .expect_err("The root file scope must respect inline module privacy") + .to_string(); assert!(err.contains("hidden") && err.contains("private")); } @@ -3089,14 +3857,14 @@ mod enum_tests { Box::new(ElementsJetHinter::new()), ) .map(|_| ()) - .map_err(|e| e.to_string()) + .map_err(|diag| diag.to_string()) } #[test] fn enum_declaration_registers_type_alias() { let result = analyze( "enum Color { Red, Green } - fn main() { let _x: Color = witness::C; }", + fn main() { let _x: Color = witness::C; }", ); assert!( result.is_ok(), @@ -3118,13 +3886,13 @@ mod enum_tests { // unrestricted. let result = analyze( "enum Action { None, Some, Other, } - fn main() { - match witness::W { - Action::None => {}, - Action::Some => {}, - Action::Other => {}, - } - }", + fn main() { + match witness::W { + Action::None => {}, + Action::Some => {}, + Action::Other => {}, + } + }", ); assert!( result.is_ok(), @@ -3143,8 +3911,8 @@ mod enum_tests { fn enum_duplicate_name_is_error() { let result = analyze( "enum Color { Red, Green } - enum Color { Blue, Cyan } - fn main() {}", + enum Color { Blue, Cyan } + fn main() {}", ); assert!(result.is_err(), "redefined enum name should error"); } @@ -3154,9 +3922,9 @@ mod enum_tests { // FIXME: Enums may only be declared at the top level of a file. let result = analyze( "mod m { - pub enum Choice { X, Y, } - } - fn main() {}", + pub enum Choice { X, Y, } + } + fn main() {}", ); let err = result.expect_err("enum inside `mod` must be rejected"); assert!( @@ -3167,21 +3935,24 @@ mod enum_tests { #[test] fn enum_declaration_in_dependency_errors() { - use crate::ast::scope_resolution_tests::analyze_multifile; + use super::analyze_multifile; // FIXME: An enum's declared name is its identity in the ABI, so enums may only be declared in the program's own files. let result = analyze_multifile(vec![ ( "main.simf", "use lib::A::helper; - fn main() { helper(); }", + fn main() { helper(); }", ), ( "libs/lib/A.simf", "pub enum Status { On, Off, } pub fn helper() {}", ), ]); - let err = result.expect_err("enums in dependency files must be rejected"); + + let err = result + .expect_err("enums in dependency files must be rejected") + .to_string(); assert!( err.contains("dependency"), "error should say enums cannot live in dependency files: {err}" @@ -3192,15 +3963,15 @@ mod enum_tests { fn enum_payload_match_binds_payload() { let result = analyze( "enum Action { Refresh(u32, bool), Cold, } - fn main() { - match witness::W { - Action::Refresh(n: u32, b: bool) => { - assert!(jet::is_zero_32(n)); - assert!(b); - }, - Action::Cold => {}, - } - }", + fn main() { + match witness::W { + Action::Refresh(n: u32, b: bool) => { + assert!(jet::is_zero_32(n)); + assert!(b); + }, + Action::Cold => {}, + } + }", ); assert!( result.is_ok(), @@ -3212,12 +3983,12 @@ mod enum_tests { fn enum_payload_binding_type_mismatch_is_error() { let result = analyze( "enum Action { Refresh(u32), Cold, } - fn main() { - match witness::W { - Action::Refresh(n: u16) => { assert!(jet::is_zero_16(n)); }, - Action::Cold => {}, - } - }", + fn main() { + match witness::W { + Action::Refresh(n: u16) => { assert!(jet::is_zero_16(n)); }, + Action::Cold => {}, + } + }", ); assert!( result.is_err(), @@ -3229,12 +4000,12 @@ mod enum_tests { fn enum_payload_binding_arity_mismatch_is_error() { let result = analyze( "enum Action { Refresh(u32, bool), Cold, } - fn main() { - match witness::W { - Action::Refresh(n: u32) => { assert!(jet::is_zero_32(n)); }, - Action::Cold => {}, - } - }", + fn main() { + match witness::W { + Action::Refresh(n: u32) => { assert!(jet::is_zero_32(n)); }, + Action::Cold => {}, + } + }", ); assert!(result.is_err()); assert!( @@ -3248,11 +4019,11 @@ mod enum_tests { // A single-variant enum is a named unit type; its match has one arm. let result = analyze( "enum Marker { Only } - fn main() { - match witness::M { - Marker::Only => {}, - } - }", + fn main() { + match witness::M { + Marker::Only => {}, + } + }", ); assert!( result.is_ok(), @@ -3264,11 +4035,11 @@ mod enum_tests { fn enum_match_undefined_enum_is_error() { let result = analyze( "fn main() { - match witness::P { - Unknown::A => {}, - Unknown::B => {}, - } - }", + match witness::P { + Unknown::A => {}, + Unknown::B => {}, + } + }", ); assert!(result.is_err(), "undefined enum should error"); } @@ -3277,13 +4048,13 @@ mod enum_tests { fn enum_match_mixed_enum_names_is_error() { let result = analyze( "enum A { X, Y } - enum B { P, Q } - fn main() { - match witness::W { - A::X => {}, - B::Q => {}, - } - }", + enum B { P, Q } + fn main() { + match witness::W { + A::X => {}, + B::Q => {}, + } + }", ); assert!(result.is_err()); assert!(result.unwrap_err().contains("same enum")); @@ -3293,12 +4064,12 @@ mod enum_tests { fn enum_match_unknown_variant_is_error() { let result = analyze( "enum A { X, Y } - fn main() { - match witness::W { - A::X => {}, - A::Z => {}, - } - }", + fn main() { + match witness::W { + A::X => {}, + A::Z => {}, + } + }", ); assert!(result.is_err()); assert!(result.unwrap_err().contains("not defined")); @@ -3308,13 +4079,13 @@ mod enum_tests { fn enum_match_duplicate_arm_is_error() { let result = analyze( "enum A { X, Y } - fn main() { - match witness::W { - A::X => {}, - A::X => {}, - A::Y => {}, - } - }", + fn main() { + match witness::W { + A::X => {}, + A::X => {}, + A::Y => {}, + } + }", ); assert!(result.is_err()); assert!(result.unwrap_err().contains("duplicate arm")); @@ -3324,12 +4095,12 @@ mod enum_tests { fn enum_match_missing_arm_is_error() { let result = analyze( "enum A { X, Y, Z } - fn main() { - match witness::W { - A::X => {}, - A::Y => {}, - } - }", + fn main() { + match witness::W { + A::X => {}, + A::Y => {}, + } + }", ); assert!(result.is_err()); assert!(result.unwrap_err().contains("must cover all 3 variants")); @@ -3339,14 +4110,14 @@ mod enum_tests { fn enum_match_rejects_scrutinee_of_different_enum() { let result = analyze( "enum A { X, Y, } - enum B { P, Q, } - fn main() { - let v: A = witness::V; - match v { - B::P => {}, - B::Q => {}, - } - }", + enum B { P, Q, } + fn main() { + let v: A = witness::V; + match v { + B::P => {}, + B::Q => {}, + } + }", ); assert!( result.is_err(), @@ -3358,13 +4129,13 @@ mod enum_tests { fn enum_match_rejects_plain_u8_scrutinee() { let result = analyze( "enum Action { A, B, } - fn main() { - let v: u8 = witness::V; - match v { - Action::A => {}, - Action::B => {}, - } - }", + fn main() { + let v: u8 = witness::V; + match v { + Action::A => {}, + Action::B => {}, + } + }", ); assert!( result.is_err(), @@ -3378,14 +4149,14 @@ mod enum_tests { // are distinct types, so their values are not interchangeable. let result = analyze( "enum AChoice { X, Y, } - enum BChoice { X, Y, } - fn main() { - let v: AChoice = witness::V; - match v { - BChoice::X => {}, - BChoice::Y => {}, - } - }", + enum BChoice { X, Y, } + fn main() { + let v: AChoice = witness::V; + match v { + BChoice::X => {}, + BChoice::Y => {}, + } + }", ); assert!( result.is_err(), @@ -3397,12 +4168,12 @@ mod enum_tests { fn enum_match_on_non_enum_alias_is_error() { let result = analyze( "type Foo = u32; - fn main() { - match witness::W { - Foo::A => {}, - Foo::B => {}, - } - }", + fn main() { + match witness::W { + Foo::A => {}, + Foo::B => {}, + } + }", ); assert!(result.is_err()); assert!( @@ -3418,11 +4189,11 @@ mod enum_tests { // (Source::Allow -> Target::Deny), silently reversing semantics. let result = analyze( "enum Source { Allow, Deny, } - enum Target { Deny, Allow, } - fn main() { - let s: Source = Source::Allow; - let _t: Target = ::into(s); - }", + enum Target { Deny, Allow, } + fn main() { + let s: Source = Source::Allow; + let _t: Target = ::into(s); + }", ); assert!( result.is_err(), @@ -3434,10 +4205,10 @@ mod enum_tests { fn enum_cast_to_structural_sum_is_rejected() { let result = analyze( "enum Source { Allow, Deny, } - fn main() { - let s: Source = Source::Allow; - let _e: Either<(), ()> = ::into(s); - }", + fn main() { + let s: Source = Source::Allow; + let _e: Either<(), ()> = ::into(s); + }", ); assert!( result.is_err(), @@ -3446,10 +4217,10 @@ mod enum_tests { let result = analyze( "enum Source { Allow, Deny, } - fn main() { - let e: Either<(), ()> = Left(()); - let _s: Source = >::into(e); - }", + fn main() { + let e: Either<(), ()> = Left(()); + let _s: Source = >::into(e); + }", ); assert!(result.is_err(), "a structural sum must not cast to an enum"); } @@ -3460,10 +4231,10 @@ mod enum_tests { // at its position. let result = analyze( "enum E { A, B, } - fn main() { - let x: (E, (u16, u16)) = (E::A, (1, 2)); - let _y: (E, u32) = <(E, (u16, u16))>::into(x); - }", + fn main() { + let x: (E, (u16, u16)) = (E::A, (1, 2)); + let _y: (E, u32) = <(E, (u16, u16))>::into(x); + }", ); assert!( result.is_ok(), @@ -3475,10 +4246,10 @@ mod enum_tests { fn enum_cast_to_itself_is_ok() { let result = analyze( "enum Source { Allow, Deny, } - fn main() { - let s: Source = Source::Allow; - let _t: Source = ::into(s); - }", + fn main() { + let s: Source = Source::Allow; + let _t: Source = ::into(s); + }", ); assert!( result.is_ok(), @@ -3504,14 +4275,14 @@ mod enum_tests { // (built-in pattern) by the `::` that follows. let result = analyze( "enum Action { A, B, } - type Left = Action; - fn main() { - let v: Left = Action::A; - match v { - Left::A => {}, - Left::B => {}, - } - }", + type Left = Action; + fn main() { + let v: Left = Action::A; + match v { + Left::A => {}, + Left::B => {}, + } + }", ); assert!( result.is_ok(), @@ -3526,10 +4297,10 @@ mod enum_tests { // follows, like the arm parser does. let result = analyze( "enum Action { A, B, } - type None = Action; - fn main() { - let _x: None = None::A; - }", + type None = Action; + fn main() { + let _x: None = None::A; + }", ); assert!( result.is_ok(), @@ -3559,16 +4330,16 @@ mod enum_tests { // declared-name fallback applies only to witness/argument files. let result = analyze( "pub enum E { A, B, } - mod m { - use crate::E as Choice; - pub fn make() -> Choice { - E::A - } - } - use crate::m::make; - fn main() { - let _x: E = make(); - }", + mod m { + use crate::E as Choice; + pub fn make() -> Choice { + E::A + } + } + use crate::m::make; + fn main() { + let _x: E = make(); + }", ); assert!( result.is_err(), @@ -3577,16 +4348,16 @@ mod enum_tests { let result = analyze( "pub enum E { A, B, } - mod m { - use crate::E as Choice; - pub fn make() -> Choice { - Choice::A - } - } - use crate::m::make; - fn main() { - let _x: E = make(); - }", + mod m { + use crate::E as Choice; + pub fn make() -> Choice { + Choice::A + } + } + use crate::m::make; + fn main() { + let _x: E = make(); + }", ); assert!( result.is_ok(), @@ -3605,6 +4376,1030 @@ mod enum_tests { } } +#[cfg(test)] +mod multi_error_tests { + use super::*; + + use crate::parse::{self, ParseFromStr}; + use crate::types::{ResolvedType, TypeConstructible}; + + /// Analyze a syntactically valid expression against `ty` as a constant. + fn analyze_at(src: &str, ty: &ResolvedType) -> bool { + let expr = parse::Expression::parse_from_str(src).expect("valid syntax"); + Expression::analyze_const(&expr, ty).is_ok() + } + + #[test] + fn two_undefined_vars_in_separate_statements_both_report() { + let src = "fn main() { + let p: u32 = missing_a; + let q: u32 = missing_b; + }"; + + let diags = errors(src); + assert_eq!(2, diags.error_count()); + + let r = diags.to_string(); + assert!(r.contains("missing_a") && r.contains("missing_b"), "{r}"); + } + + #[test] + fn later_statements_analyzed_after_an_earlier_failure() { + // The bool/u32 mismatch after the undefined var proves analysis continued. + let src = "fn main() { + let p: u32 = missing_a; + assert!(jet::eq_32(true, 28)); + }"; + assert!(errors(src).error_count() >= 2); + } + + #[test] + fn two_broken_functions_both_report() { + let src = "fn f() -> u32 { missing_a } + fn g() -> u32 { missing_b } + fn main() { }"; + assert_eq!(2, errors(src).error_count()); + } + + #[test] + fn broken_function_does_not_hide_error_in_main() { + let src = "fn f() -> u32 { missing_a } + fn main() { let q: u32 = missing_b; }"; + assert_eq!(2, errors(src).error_count()); + } + + #[test] + fn broken_items_inside_module_all_surface() { + let src = "mod m { + pub fn f() -> u32 { missing_a } + pub fn g() -> u32 { missing_b } + } + fn main() { }"; + assert_eq!(2, errors(src).error_count()); + } + + #[test] + fn broken_items_inside_tuple_all_surface() { + let src = "fn main() { + let pair: (u32, u32) = (missing_a, missing_b); +}"; + assert_eq!(2, errors(src).error_count()); + } + + #[test] + fn cascading_error_with_function() { + let src = "fn f() -> u32 { missing } +fn main() { + let x: u32 = f(); +}"; + assert_eq!(1, errors(src).error_count()); + } + + #[test] + fn duplicate_main_detected_when_first_body_failed() { + // Even though the first main's body fails, it stays a Function::Main (poison + // body), so the duplicate is still caught. + let src = "fn main() { missing_tail } fn main() {}"; + let diags = errors(src); + assert_eq!(2, diags.error_count()); + assert!( + diags + .diagnostics() + .iter() + .any(|d| matches!(d.error(), Error::FunctionRedefined { .. })), + "duplicate main must be reported despite the first main's broken body" + ); + } + + #[test] + fn failed_main_body_does_not_trigger_main_required() { + // Broken main body must report ONLY the body error, not also "Main required": + // main is preserved with a poison body, so extract_single_main still finds it. + let src = "fn main() { missing_tail }"; + assert_eq!(1, errors(src).error_count()); + } + + #[test] + fn failed_assigment_does_not_trigger_undefined_variable() { + let src = "fn main() { + let x: u32 = missing_value; + let y: u32 = x; +}"; + assert_eq!(1, errors(src).error_count()); + } + + #[test] + fn failed_return_type_registers_poison_function() { + // `Missing` (undefined return type) + `missing_body` = 2 errors. + // `f` still registers with a poison return type, so `f()` in main does + // NOT cascade into "function f is not defined". + let src = "fn f() -> Missing { missing_body } + fn main() { let x: u32 = f(); }"; + assert_eq!(2, errors(src).error_count()); + } + + #[test] + fn bad_annotation_still_analyzes_rhs() { + // The annotation resolves to ResolvedType::error(); the RHS is still + // analyzed, so both the bad type and the undefined value are reported. + let src = "fn main() { let x: Missing = missing_value; }"; + assert_eq!(2, errors(src).error_count()); + } + + #[test] + fn undefined_function_still_reports_arg() { + // The unknown callee must not suppress the argument error: the supplied + // arg is analyzed against a poison type, so `missing_arg` still surfaces. + let src = "fn main() { missing_fn(missing_arg); }"; + assert_eq!(2, errors(src).error_count()); + } + + #[test] + fn both_match_arms_report_independent_errors() { + // An error in the left arm must not suppress the right arm. + let src = "fn main() { + let r: u32 = match false { + false => missing_a, + true => missing_b, + }; + }"; + assert_eq!(2, errors(src).error_count()); + } + + #[test] + fn undefined_scrutinee_still_analyzes_both_arms() { + // A poisoned scrutinee must not hide the arm-body errors. + let src = "fn main() { + let r: u32 = match missing_scrutinee { + false => missing_a, + true => missing_b, + }; + }"; + assert_eq!(3, errors(src).error_count()); + } + + #[test] + fn poison_expected_type_absorbs_literals_and_containers() { + // Without the is_error() guards each of these emits ExpressionUnexpectedType. + let err = ResolvedType::error(); + for src in ["5", "0x00", "(1, 2)", "[1, 2, 3]", "Some(1)"] { + assert!(analyze_at(src, &err), "poison type should absorb `{src}`"); + } + } + + #[test] + fn poison_type_absorbs_the_cast_validity_check() { + // `StructuralType` has no representation for poison and panics on it by + // design, so a cast must not compare structures when either side is + // poisoned at any depth — each of these used to abort the compiler. + for src in [ + "fn main() { let x: Missing = ::into(5); }", + "type Broken = Missing; + fn main() { let x: u32 = ::into(5); }", + "type Broken = Missing; + fn main() { let x: Broken = ::into(5); }", + ] { + // One root cause: the undefined alias, with no cast error piled on. + assert_eq!(1, errors(src).error_count(), "{src}"); + } + } + + #[test] + fn error_unifies_with_anything_but_reals_stay_structural() { + let err = ResolvedType::error(); + let u32 = ResolvedType::parse_from_str("u32").unwrap(); + let u16 = ResolvedType::parse_from_str("u16").unwrap(); + + // poison unifies both directions + assert!(err.compatible(&u32) && u32.compatible(&err) && err.compatible(&err)); + // reals stay structural + assert!(u32.compatible(&u32)); + assert!(!u32.compatible(&u16)); + + // nested: (u32, error) ~ (u32, u32), but (u32, u32) !~ (u32, u16) + let t_err = ResolvedType::tuple([u32.clone(), err]); + let t_u32 = ResolvedType::tuple([u32.clone(), u32.clone()]); + let t_u16 = ResolvedType::tuple([u32.clone(), u16]); + assert!(t_err.compatible(&t_u32)); + assert!(!t_u32.compatible(&t_u16)); + + // arity still enforced under compatibility + let t3 = ResolvedType::tuple([u32.clone(), u32.clone(), u32]); + assert!(!t_u32.compatible(&t3)); + } +} + +#[cfg(test)] +mod known_collection_gaps { + use super::*; + // The gaps fall into three families: + // + // * P1/P4 — a failed *registration* removes a name from scope (or an + // artifact from the program), so every later use cascades. + // * P2/P3 — a *container-level* check (arity, output type, shape) + // returns before its children are analyzed, so their independent + // errors never surface. + // * P5 — *binding recovery* discards the declared identifiers, so every + // later use of them cascades. + + /// Is any diagnostic of the kind matched by `pred`? + fn reports(diags: &DiagnosticManager, pred: impl Fn(&Error) -> bool) -> bool { + diags.diagnostics().iter().any(|d| pred(d.error())) + } + + /// The undefined variables reported, in report order. + fn undefined_vars(diags: &DiagnosticManager) -> Vec { + diags + .diagnostics() + .iter() + .filter_map(|d| match d.error() { + Error::UndefinedVariable { identifier } => Some(identifier.to_string()), + _ => None, + }) + .collect() + } + + /// The undefined type aliases reported, in report order. + fn undefined_aliases(diags: &DiagnosticManager) -> Vec { + diags + .diagnostics() + .iter() + .filter_map(|d| match d.error() { + Error::UndefinedAlias { name } => Some(name.to_string()), + _ => None, + }) + .collect() + } + + // P1: an invalid `main` signature must not erase the declaration + + #[test] + fn public_main_reports_only_the_visibility_error() { + let diags = errors("pub fn main() {}"); + assert!( + !reports(&diags, |e| matches!(e, Error::MainRequired)), + "main exists, it is merely public: {diags}" + ); + assert_eq!(1, diags.error_count()); + } + + #[test] + fn main_with_inputs_reports_only_the_signature_error() { + let diags = errors("fn main(x: u32) {}"); + assert!( + !reports(&diags, |e| matches!(e, Error::MainRequired)), + "{diags}" + ); + assert_eq!(1, diags.error_count()); + } + + #[test] + fn main_with_output_reports_only_the_signature_error() { + // The body is still checked against unit, so a second error is fine + // here — but it must not be "main required". + let diags = errors("fn main() -> u32 { 1 }"); + assert!( + reports(&diags, |e| matches!(e, Error::MainNoOutput)), + "{diags}" + ); + assert!( + !reports(&diags, |e| matches!(e, Error::MainRequired)), + "{diags}" + ); + } + + #[test] + fn public_main_still_analyzes_its_body() { + // A poisoned Function::Main keeps the body reachable, exactly as a + // failed body already does (see failed_main_body_does_not_trigger_main_required). + let diags = errors("pub fn main() { missing_body }"); + assert!( + reports(&diags, |e| matches!(e, Error::MainCannotBePublic)), + "{diags}" + ); + assert_eq!(["missing_body"][..], undefined_vars(&diags)[..]); + assert!( + !reports(&diags, |e| matches!(e, Error::MainRequired)), + "{diags}" + ); + } + + #[test] + fn public_main_does_not_hide_a_duplicate_main() { + let diags = errors("pub fn main() {}\nfn main() {}"); + assert!( + reports(&diags, |e| matches!(e, Error::MainCannotBePublic)), + "{diags}" + ); + assert!( + reports(&diags, |e| matches!(e, Error::FunctionRedefined { .. })), + "the second main must still be caught: {diags}" + ); + } + + // P2: a known callee's checks must not suppress argument errors + + #[test] + fn call_output_mismatch_still_reports_argument_errors() { + let diags = errors( + "fn f(x: u32) -> u32 { x } + fn main() { let y: u16 = f(missing_arg); }", + ); + assert_eq!(["missing_arg"][..], undefined_vars(&diags)[..]); + assert_eq!(2, diags.error_count()); + } + + #[test] + fn call_arity_mismatch_still_reports_every_argument_error() { + // Surplus arguments have no expected type, so they must be analyzed + // against error() — dropping them loses missing_b. + let diags = errors( + "fn f(x: u32) -> u32 { x } + fn main() { let y: u32 = f(missing_a, missing_b); }", + ); + assert_eq!(["missing_a", "missing_b"][..], undefined_vars(&diags)[..]); + assert_eq!(3, diags.error_count()); + } + + #[test] + fn jet_arity_mismatch_still_reports_argument_errors() { + let diags = errors("fn main() { assert!(jet::eq_32(missing_a)); }"); + assert_eq!(["missing_a"][..], undefined_vars(&diags)[..]); + assert_eq!(2, diags.error_count()); + } + + #[test] + fn invalid_cast_still_reports_argument_errors() { + let diags = errors("fn main() { let x: u32 = ::into(missing_a); }"); + assert_eq!(["missing_a"][..], undefined_vars(&diags)[..]); + assert_eq!(2, diags.error_count()); + } + + #[test] + fn tuple_arity_mismatch_still_reports_element_errors() { + let diags = errors("fn main() { let t: (u32, u32) = (missing_a, missing_b, missing_c); }"); + assert_eq!( + ["missing_a", "missing_b", "missing_c"][..], + undefined_vars(&diags)[..] + ); + } + + #[test] + fn tuple_against_non_tuple_type_still_reports_element_errors() { + // The elements have no expected type, so they must be analyzed + // against error() rather than skipped. + let diags = errors("fn main() { let t: u32 = (missing_a, missing_b); }"); + assert_eq!(["missing_a", "missing_b"][..], undefined_vars(&diags)[..]); + } + + #[test] + fn array_size_mismatch_still_reports_element_errors() { + let diags = errors("fn main() { let a: [u32; 2] = [missing_a, missing_b, missing_c]; }"); + assert_eq!( + ["missing_a", "missing_b", "missing_c"][..], + undefined_vars(&diags)[..] + ); + } + + #[test] + fn list_bound_mismatch_still_reports_element_errors() { + let diags = + errors("fn main() { let a: List = list![missing_a, missing_b, missing_c]; }"); + assert_eq!( + ["missing_a", "missing_b", "missing_c"][..], + undefined_vars(&diags)[..] + ); + } + + #[test] + fn enum_construction_arity_mismatch_still_reports_payload_errors() { + let diags = errors( + "enum E { V(u32) } + fn main() { let x: E = E::V(missing_a, missing_b); }", + ); + assert_eq!(["missing_a", "missing_b"][..], undefined_vars(&diags)[..]); + } + + #[test] + fn non_exhaustive_enum_match_still_reports_arm_body_errors() { + let diags = errors( + "enum E { A, B } + fn main() { let r: u32 = match witness::W { E::A => missing_a, }; }", + ); + assert_eq!(["missing_a"][..], undefined_vars(&diags)[..]); + assert_eq!(2, diags.error_count()); + } + + #[test] + fn duplicate_enum_match_arm_still_reports_arm_body_errors() { + let diags = errors( + "enum E { A, B } + fn main() { + let r: u32 = match witness::W { + E::A => missing_a, + E::A => missing_b, + E::B => missing_c, + }; + }", + ); + assert_eq!( + ["missing_a", "missing_b", "missing_c"][..], + undefined_vars(&diags)[..] + ); + } + + #[test] + fn unknown_enum_match_variant_still_reports_arm_body_errors() { + let diags = errors( + "enum E { A, B } + fn main() { + let r: u32 = match witness::W { E::A => missing_a, E::Zzz => missing_b, }; + }", + ); + assert_eq!(["missing_a", "missing_b"][..], undefined_vars(&diags)[..]); + } + + // P3: type resolution must not abort a whole match + + #[test] + fn broken_arm_types_do_not_abort_the_match() { + let diags = errors( + "fn main() { + let r: u32 = match witness::W { + Left(a: Missing) => missing_a, + Right(b: MissingTwo) => missing_b, + }; + }", + ); + assert_eq!(["Missing", "MissingTwo"][..], undefined_aliases(&diags)[..]); + assert_eq!(["missing_a", "missing_b"][..], undefined_vars(&diags)[..]); + assert_eq!(4, diags.error_count()); + } + + #[test] + fn broken_right_arm_type_does_not_hide_the_left_arm_body() { + let diags = errors( + "fn main() { + let r: u32 = match witness::W { + Left(a: u32) => missing_a, + Right(b: MissingTwo) => missing_b, + }; + }", + ); + assert_eq!(["missing_a", "missing_b"][..], undefined_vars(&diags)[..]); + assert_eq!(3, diags.error_count()); + } + + #[test] + fn arm_pattern_shape_mismatch_does_not_abort_the_match() { + let diags = errors( + "fn main() { + let r: u32 = match witness::W { + Left((a, b): u32) => missing_a, + Right(c: u32) => missing_b, + }; + }", + ); + assert_eq!(["missing_a", "missing_b"][..], undefined_vars(&diags)[..]); + } + + #[test] + fn every_broken_enum_arm_binding_type_reports() { + let diags = errors( + "enum E { V(u32, u32) } + fn main() { let r: u32 = match witness::W { E::V(x: Missing, y: MissingTwo) => 1, }; }", + ); + assert_eq!(["Missing", "MissingTwo"][..], undefined_aliases(&diags)[..]); + } + + // P4: a failed registration must still leave a poisoned name + + #[test] + fn broken_type_alias_does_not_cascade() { + let diags = errors( + "type Broken = Missing; + fn main() { let x: Broken = 5; }", + ); + assert_eq!(["Missing"][..], undefined_aliases(&diags)[..]); + assert_eq!(1, diags.error_count()); + } + + #[test] + fn broken_type_alias_does_not_cascade_per_use_site() { + let diags = errors( + "type Broken = Missing; + fn f() -> Broken { 5 } + fn main() { let x: Broken = 5; }", + ); + assert_eq!(1, diags.error_count(), "{diags}"); + } + + #[test] + fn enum_with_duplicate_variant_still_registers_its_name() { + let diags = errors( + "enum E { A, A } + fn main() { let x: E = witness::W; }", + ); + assert!(undefined_aliases(&diags).is_empty(), "{diags}"); + assert_eq!(1, diags.error_count()); + } + + #[test] + fn empty_enum_still_registers_its_name() { + let diags = errors( + "enum E { } + fn main() { let x: E = witness::W; }", + ); + assert!(undefined_aliases(&diags).is_empty(), "{diags}"); + assert_eq!(1, diags.error_count()); + } + + #[test] + fn redefined_module_still_analyzes_its_items() { + let diags = errors( + "mod m { pub fn a() -> u32 { missing_a } } + mod m { pub fn b() -> u32 { missing_b } } + fn main() {}", + ); + assert_eq!(["missing_a", "missing_b"][..], undefined_vars(&diags)[..]); + assert_eq!(3, diags.error_count()); + } + + // P5: binding recovery must retain the declared identifiers + + #[test] + fn failed_assignment_pattern_keeps_its_bindings() { + // The pattern does not fit the annotation; `a` and `b` are still + // declared, so using them must not cascade. + let diags = errors("fn main() { let (a, b): u32 = 5; assert!(jet::eq_32(a, b)); }"); + assert!(undefined_vars(&diags).is_empty(), "{diags}"); + assert_eq!(1, diags.error_count()); + } + + #[test] + fn failed_enum_arm_binding_keeps_its_bindings() { + let diags = errors( + "enum E { V(u32) } + fn main() { let r: u32 = match witness::W { E::V(x: u16) => x, }; }", + ); + assert!(undefined_vars(&diags).is_empty(), "{diags}"); + assert_eq!(1, diags.error_count()); + } + + /// The unresolved import names reported, in report order. + fn unresolved_items(diags: &DiagnosticManager) -> Vec { + diags + .diagnostics() + .iter() + .filter_map(|d| match d.error() { + Error::UnresolvedItem { name } => Some(name.clone()), + _ => None, + }) + .collect() + } + + #[test] + fn single_child_shape_mismatch_still_reports_the_child() { + // `Left`, `Right` and `Some` wrap exactly one child. The child has its + // own errors whether or not the annotation is the right shape, so the + // mismatch must be reported and the child analyzed against poison. + for src in [ + "fn main() { let x: u32 = Left(missing_child); }", + "fn main() { let x: u32 = Right(missing_child); }", + "fn main() { let x: u32 = Some(missing_child); }", + ] { + let diags = errors(src); + assert_eq!(["missing_child"][..], undefined_vars(&diags)[..], "{src}"); + assert_eq!(2, diags.error_count(), "{src}"); + } + } + + #[test] + fn poison_expected_type_still_reports_enum_payload_errors() { + // The payload is analyzed at poison, like every other container's + // children, rather than skipped wholesale. + let diags = errors( + "enum E { V(u32) } + fn main() { let x: Missing = E::V(missing_child); }", + ); + assert_eq!(["missing_child"][..], undefined_vars(&diags)[..]); + assert_eq!(2, diags.error_count()); + } + + #[test] + fn multi_item_import_reports_every_unresolved_item() { + // The items of one `use` are independent: `b` failing tells you nothing + // about `a`, so both must be reported. + let diags = errors( + "mod m {} + use crate::m::{a, b}; + fn main() {}", + ); + assert_eq!(["a", "b"][..], unresolved_items(&diags)[..]); + } + + #[test] + fn generic_call_type_reports_every_undefined_alias() { + // The type argument of a builtin resolves like any other type, so two + // broken aliases in it report twice, not once. + for src in [ + "fn main() { let x: bool = is_none::>(missing_arg); }", + "fn main() { let x: u32 = unwrap_left::>(missing_arg); }", + "fn main() { let x: u32 = <(Missing, MissingTwo)>::into(missing_arg); }", + ] { + let diags = errors(src); + assert_eq!( + ["Missing", "MissingTwo"][..], + undefined_aliases(&diags)[..], + "{src}" + ); + } + } + + #[test] + fn poisoned_fold_signature_does_not_cascade_into_not_foldable() { + // Whether the signature folds is unknowable until its types resolve, so + // a poisoned one must absorb the check rather than fail it. + for src in [ + "fn step(e: u32, acc: Missing) -> u32 { 0 } + fn main() { let r: u32 = array_fold::([1, 2], 0); }", + "fn step(e: u32, acc: u32) -> Missing { 0 } + fn main() { let r: u32 = array_fold::([1, 2], 0); }", + ] { + let diags = errors(src); + assert!( + !reports(&diags, |e| matches!(e, Error::FunctionNotFoldable { .. })), + "the undefined alias is the only root cause: {diags}" + ); + assert_eq!(["Missing"][..], undefined_aliases(&diags)[..], "{src}"); + } + } + + #[test] + fn redefined_alias_still_reports_errors_in_the_rejected_body() { + // The rejected body is independent of the name collision: resolve it for + // its own diagnostics, without replacing the existing binding. + let diags = errors( + "type A = u32; + type A = Missing; + fn main() {}", + ); + assert!(reports(&diags, |e| matches!( + e, + Error::RedefinedAlias { .. } + ))); + assert_eq!(["Missing"][..], undefined_aliases(&diags)[..]); + assert_eq!(2, diags.error_count()); + } + + #[test] + fn every_rejected_registration_still_reports_its_own_errors() { + // `insert_alias` is the only registration that resolves anything itself, + // so it is the only one whose collision check could swallow an inner + // error (see the test above). The rest take values their caller already + // analyzed and reported on, which is what keeps these cases honest — + // moving that work inside a registration would silently regress them. + for (src, inner) in [ + // enum: the payload types are resolved before `insert_enum` + ( + "enum E { A }\nenum E { B(Missing) }\nfn main() {}", + "Missing", + ), + // ...including when the collision is with a plain alias + ( + "type E = u32;\nenum E { B(Missing) }\nfn main() {}", + "Missing", + ), + // function: params, return type and body precede `insert_function` + ("fn f() {}\nfn f(x: Missing) {}\nfn main() {}", "Missing"), + ( + "fn f() {}\nfn f() -> Missing { 1 }\nfn main() {}", + "Missing", + ), + ] { + let diags = errors(src); + assert_eq!([inner][..], undefined_aliases(&diags)[..], "{src}"); + assert_eq!(2, diags.error_count(), "{src}"); + } + } + + #[test] + fn rejected_function_body_errors_still_report() { + // The body case, which has no alias to count: the duplicate must not + // swallow the undefined variable inside the rejected definition. + let diags = errors( + "fn f() {} + fn f() -> u32 { missing_body } + fn main() {}", + ); + assert!(reports(&diags, |e| matches!( + e, + Error::FunctionRedefined { .. } + ))); + assert_eq!(["missing_body"][..], undefined_vars(&diags)[..]); + } + + #[test] + fn surplus_enum_arm_bindings_do_not_cascade_into_a_shape_error() { + // Binding two names to a one-value payload is one mistake. The arity + // diagnostic says so; re-checking the combined pattern against the + // payload type only restates it. + let diags = errors( + "enum E { V(u32) } + fn main() { let r: u32 = match witness::W { E::V(x: u32, y: u32) => 1, }; }", + ); + assert!( + !reports(&diags, |e| matches!( + e, + Error::ExpressionUnexpectedType { .. } + )), + "the arity error already says it: {diags}" + ); + assert_eq!(1, diags.error_count()); + } + + /// How many diagnostics carry a [`Error::Grammar`] message? + /// + /// Grammar errors are distinguished only by their text, so counting them is + /// the robust way to assert that several independent ones surfaced. + fn grammar_count(diags: &DiagnosticManager) -> usize { + diags + .diagnostics() + .iter() + .filter(|d| matches!(d.error(), Error::Grammar { .. })) + .count() + } + + #[test] + fn aliasing_an_item_to_main_does_not_abort_its_siblings() { + // A: the items of one `use` are independent, so a rejected alias must + // not swallow the item beside it. + let diags = errors( + "mod m { pub fn a() {} } + use crate::m::{a as main, missing}; + fn main() {}", + ); + assert!( + reports(&diags, |e| matches!(e, Error::MainCannotBeAlias)), + "{diags}" + ); + assert_eq!(["missing"][..], unresolved_items(&diags)[..]); + } + + #[test] + fn every_malformed_enum_match_arm_reports() { + // A: the arm loop must not stop at the first unknown variant; `Y` is a + // separate mistake from `X`. + let diags = errors( + "enum E { A, B } + fn main() { let r: u32 = match witness::W { E::X => 1, E::Y => 2, }; }", + ); + assert_eq!( + 2, + grammar_count(&diags), + "both unknown variants must report: {diags}" + ); + } + + #[test] + fn poisoned_enum_alias_does_not_cascade_into_not_an_enum() { + // B: `E` resolves to poison, so whether it names an enum is unknowable. + // Treating poison as a concrete non-enum invents a second error. + let diags = errors( + "type E = Missing; + fn main() { let r: u32 = match witness::W { E::A => 1, }; }", + ); + assert_eq!(["Missing"][..], undefined_aliases(&diags)[..]); + assert_eq!(1, diags.error_count(), "{diags}"); + } + + #[test] + fn poisoned_loop_counter_does_not_cascade_into_not_loopable() { + // B: the counter's width is unknowable while its type is poisoned, so + // loopability is unknowable too — absorb the check rather than fail it. + let diags = errors( + "fn step(acc: u32, ctx: (), i: Missing) -> Either { Left(acc) } + fn main() { let r: Either = for_while::(0, ()); }", + ); + assert!( + !reports(&diags, |e| matches!(e, Error::FunctionNotLoopable { .. })), + "the undefined alias is the only root cause: {diags}" + ); + assert_eq!(["Missing"][..], undefined_aliases(&diags)[..]); + } + + #[test] + fn reading_a_variable_does_not_rebind_it() { + // C: reading `x` at a poisoned expected type must not replace the + // declared `x: u32`. The second read is an independent mistake and has + // to be caught against the *declaration*, not against the first read. + let diags = errors( + "fn f(x: u32) { let a: Missing = x; let b: u16 = x; } + fn main() {}", + ); + assert_eq!(["Missing"][..], undefined_aliases(&diags)[..]); + assert!( + reports(&diags, |e| matches!( + e, + Error::ExpressionTypeMismatch { .. } + )), + "the u32/u16 mismatch is independent of the bad annotation: {diags}" + ); + assert_eq!(2, diags.error_count()); + } + + #[test] + fn arity_recovery_still_validates_the_binding_pattern() { + // D: the arity error explains the count, and nothing else. Skipping the + // pattern check to suppress the shape cascade also drops the reuse of + // `x`, which is an independent mistake. + let diags = errors( + "enum E { V(u32) } + fn main() { let r: u32 = match witness::W { E::V(x: u32, x: u32) => 1, }; }", + ); + assert!( + reports(&diags, |e| matches!( + e, + Error::VariableReuseInPattern { .. } + )), + "reusing `x` is independent of the arity: {diags}" + ); + assert_eq!(2, diags.error_count()); + } + + #[test] + fn import_collision_in_one_namespace_is_not_masked_by_another() { + // E: the alias `foo` imports cleanly, the function `foo` collides with a + // local definition. Succeeding in one namespace must not report success + // for all of them. + let diags = errors( + "mod m { pub type foo = u32; pub fn foo() {} } + fn foo() {} + use crate::m::foo; + fn main() {}", + ); + assert!( + reports(&diags, |e| matches!(e, Error::RedefinedItem { .. })), + "the duplicate function import must surface: {diags}" + ); + } + + // P1, imports: a name a failed `use` was meant to introduce is poisoned, so + // the cost of a broken import is one error rather than one per use. + + #[test] + fn unresolved_module_does_not_cascade_into_every_call() { + let diags = errors( + "use crate::nope::foo; + fn main() { foo(); foo(); foo(); foo(); foo(); }", + ); + assert!( + reports(&diags, |e| matches!(e, Error::ModuleNotFound { .. })), + "{diags}" + ); + assert_eq!(1, diags.error_count(), "{diags}"); + } + + #[test] + fn unresolved_item_does_not_cascade_into_every_call() { + let diags = errors( + "mod m { pub fn f() {} } + use crate::m::missing; + fn main() { missing(); missing(); missing(); }", + ); + assert!( + reports(&diags, |e| matches!(e, Error::UnresolvedItem { .. })), + "{diags}" + ); + assert_eq!(1, diags.error_count(), "{diags}"); + } + + #[test] + fn private_item_does_not_cascade_into_every_call() { + let diags = errors( + "mod m { fn f() -> u32 { 1 } } + use crate::m::f; + fn main() { let a: u32 = f(); let b: u32 = f(); let c: u32 = f(); }", + ); + assert!( + reports(&diags, |e| matches!(e, Error::PrivateItem { .. })), + "{diags}" + ); + assert_eq!(1, diags.error_count(), "{diags}"); + } + + #[test] + fn failed_import_does_not_cascade_into_every_type_use() { + // A `use` does not say which namespace the item lives in, so the name is + // poisoned as a type as well as a function. + let diags = errors( + "mod m { pub fn f() {} } + use crate::m::Missing; + fn main() { let x: Missing = 5; let y: Missing = 6; }", + ); + assert!(undefined_aliases(&diags).is_empty(), "{diags}"); + assert_eq!(1, diags.error_count(), "{diags}"); + } + + #[test] + fn failed_import_inside_a_module_does_not_cascade() { + let diags = errors( + "mod a { pub fn f() {} } + mod b { use crate::a::nope; pub fn g() { nope(); nope(); } } + fn main() {}", + ); + assert_eq!(1, diags.error_count(), "{diags}"); + } + + #[test] + fn poisoned_import_absorbs_arity_and_output_checks() { + // The signature is unknowable, so neither the argument count nor the + // output type has an answer to check against. + let diags = errors( + "use crate::nope::foo; + fn main() { let x: u32 = foo(1, 2, 3); }", + ); + assert_eq!(1, diags.error_count(), "{diags}"); + } + + #[test] + fn poisoned_import_still_reports_its_argument_errors() { + // Absorbing the callee must not swallow the arguments' own errors. + let diags = errors( + "use crate::nope::foo; + fn main() { foo(missing_a, missing_b); }", + ); + assert_eq!(["missing_a", "missing_b"][..], undefined_vars(&diags)[..]); + assert_eq!(3, diags.error_count(), "{diags}"); + } + + #[test] + fn poisoned_import_does_not_cascade_into_not_foldable_or_not_loopable() { + for src in [ + "use crate::nope::step; + fn main() { let r: u32 = array_fold::([1, 2], 0); }", + "use crate::nope::step; + fn main() { let r: u32 = fold::(list![1, 2], 0); }", + "use crate::nope::step; + fn main() { let r: Either = for_while::(0, ()); }", + ] { + let diags = errors(src); + assert!( + !reports(&diags, |e| matches!( + e, + Error::FunctionNotFoldable { .. } | Error::FunctionNotLoopable { .. } + )), + "a poisoned signature has no shape to fail: {src}\n{diags}" + ); + assert_eq!(1, diags.error_count(), "{src}\n{diags}"); + } + } + + #[test] + fn a_failed_import_still_reports_every_bad_item() { + // Poisoning the names must not collapse the per-item collection. + let diags = errors( + "mod m { pub fn f() {} } + use crate::m::{missing_x, missing_y}; + fn main() { missing_x(); missing_y(); }", + ); + assert_eq!(2, diags.error_count(), "{diags}"); + } + + #[test] + fn a_failed_import_does_not_poison_its_healthy_siblings() { + let diags = errors( + "mod m { pub fn a() -> u32 { 1 } pub fn b() -> u32 { 2 } } + use crate::m::{a, nope, b}; + fn main() { let x: u32 = a(); let y: u32 = b(); nope(); }", + ); + assert_eq!(1, diags.error_count(), "{diags}"); + } + + #[test] + fn a_redefined_import_keeps_the_good_binding() { + // The name is already bound to a real function, so it must not be + // poisoned: a genuine mistake against that signature still reports. + let diags = errors( + "mod m { pub fn f(x: u32) -> u32 { x } } + use crate::m::f; + use crate::m::f; + fn main() { let y: u32 = f(1, 2); }", + ); + assert!( + reports(&diags, |e| matches!(e, Error::RedefinedItem { .. })), + "{diags}" + ); + assert!( + reports(&diags, |e| matches!( + e, + Error::InvalidNumberOfArguments { .. } + )), + "the real signature must still be checked: {diags}" + ); + } +} + #[cfg(feature = "fmt")] #[cfg(test)] mod literal_tests { diff --git a/src/compile/mod.rs b/src/compile/mod.rs index ed16801a..338b36bb 100644 --- a/src/compile/mod.rs +++ b/src/compile/mod.rs @@ -251,6 +251,9 @@ fn compile_blk<'brand>( let drop_iden = ProgNode::drop_(&ProgNode::iden(scope.ctx())); pair.comp(&drop_iden).with_span(expression) } + Statement::Error => unreachable!( + "poisoned statement reached codegen; compilation must be gated on zero errors" + ), } } @@ -380,6 +383,9 @@ impl SingleExpression { } SingleExpressionInner::Match(match_) => match_.compile(scope)?, SingleExpressionInner::EnumMatch(enum_match) => enum_match.compile(scope)?, + SingleExpressionInner::Error => unreachable!( + "poisoned expression reached codegen; compilation must be gated on zero errors" + ), }; scope diff --git a/src/driver/mod.rs b/src/driver/mod.rs index 50a94d48..2b640f74 100644 --- a/src/driver/mod.rs +++ b/src/driver/mod.rs @@ -37,7 +37,7 @@ use std::path::PathBuf; use std::sync::Arc; use crate::error::{Diagnostic, DiagnosticManager, Error, Span}; -use crate::parse::{self, ParseFromStrWithErrors}; +use crate::parse::{self, ParseFromContent}; use crate::resolution::{DependencyMap, ResolvedUse}; use crate::source::{CanonPath, CanonSourceFile}; use crate::unstable::UnstableFeatures; @@ -221,7 +221,7 @@ impl DependencyGraph { let mut diagnostics = DiagnosticManager::default(); let sources = SourceMap::with_source(root_source.clone()); - let Some(program) = parse::Program::parse_from_str_with_errors( + let Some(program) = parse::Program::parse_from_content( MAIN_MODULE, &root_source.content(), unstable_features, @@ -250,13 +250,7 @@ impl DependencyGraph { }; graph.discover_dependencies(&mut diagnostics, unstable_features); - - if diagnostics.has_errors() { - diagnostics.with_sources(graph.sources); - (None, diagnostics) - } else { - (Some(graph), diagnostics) - } + (Some(graph), diagnostics) } /// BFS over `use` declarations, populating `modules`, `dependencies`, @@ -421,12 +415,8 @@ impl DependencyGraph { ) -> Option { let before = diagnostics.error_count(); - let ast = parse::Program::parse_from_str_with_errors( - new_id, - content, - unstable_features, - diagnostics, - ); + let ast = + parse::Program::parse_from_content(new_id, content, unstable_features, diagnostics); if diagnostics.error_count() > before { return None; @@ -747,14 +737,9 @@ pub(crate) mod tests { // Setup: root imports from "unknown", which is not in our dependency map. // We use `setup_graph_raw` because we expect graph generation to fail and // emit an error, rather than panicking the standard test helper. - let (graph_option, _ids, _ws, diagnostics) = + let (_graph, _ids, _ws, diagnostics) = setup_graph_raw(vec![("main.simf", "use unknown::library::item;")]); - assert!( - graph_option.is_none(), - "Graph unexpectedly succeeded despite having an unknown import!" - ); - assert!( diagnostics.has_errors(), "The ErrorCollector should have logged an error about the unmapped import" diff --git a/src/driver/resolve_order.rs b/src/driver/resolve_order.rs index 32ec6c1b..aa75af32 100644 --- a/src/driver/resolve_order.rs +++ b/src/driver/resolve_order.rs @@ -42,16 +42,16 @@ fn forbid_enum_dec_in_deps( } for decl in enum_declarations(local_items) { - diagnostics.push(Diagnostic::new( + diagnostics.push( Error::Grammar { msg: format!( "enum `{}` is declared in a dependency file; \ - enums may only be declared in the program's own files", + enums may only be declared in the program's own files", decl.name() ), - }, - *decl.as_ref(), - )); + } + .with_span(decl.into()), + ); } } @@ -63,7 +63,7 @@ impl DependencyGraph { diagnostics: &mut DiagnosticManager, ) -> Option { match self.linearize() { - Ok(order) => self.assemble_program(&order, diagnostics), + Ok(order) => Some(self.assemble_program(&order, diagnostics)), Err(err) => { diagnostics.push(err); None @@ -76,7 +76,7 @@ impl DependencyGraph { &self, order: &[usize], diagnostics: &mut DiagnosticManager, - ) -> Option { + ) -> parse::Program { let mut items = Vec::with_capacity(order.len()); let target_ids: HashMap = self @@ -130,8 +130,7 @@ impl DependencyGraph { ))); } - (!diagnostics.has_errors()) - .then(|| parse::Program::new(&items, *self.modules[&MAIN_MODULE].program.as_ref())) + parse::Program::new(&items, *self.modules[&MAIN_MODULE].program.as_ref()) } /// Rewrites a single item for the flattened single-file representation. @@ -177,8 +176,13 @@ impl DependencyGraph { target_ids: &HashMap, ) -> parse::Item { let span = *use_decl.span(); - let resolved = &self.use_cache[&span]; - let target_id = target_ids[&span]; + + // A use that failed to resolve has no cache/target entry; its error was + // already reported. Skip it with as poison item instead of indexing and panicking. + let (Some(resolved), Some(&target_id)) = (self.use_cache.get(&span), target_ids.get(&span)) + else { + return parse::Item::Ignored; + }; let mut new_path = Vec::with_capacity(resolved.mod_path.len() + 2); new_path.push(Identifier::from_str_unchecked(CRATE_STR)); @@ -317,13 +321,7 @@ mod flattening_tests { ), ]); - let driver_program = graph.linearize_and_assemble(&mut diagnostics); - - assert!( - driver_program.is_none(), - "Expected the build to fail and return None, but got: {:?}", - driver_program - ); + let _ = graph.linearize_and_assemble(&mut diagnostics); assert!( diagnostics.has_errors(), diff --git a/src/error.rs b/src/error.rs index 5384ca5e..1038d5e2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -81,6 +81,13 @@ impl Span { } } + /// Check whether this function lives in the main module. + /// + /// Useful for LSP. + pub const fn in_main_module(&self) -> bool { + self.file_id == MAIN_MODULE + } + /// EOF sentinel: zero-width position at the end of `file_id`'s contents pub const fn eof(file_id: usize, source_len: usize) -> Self { // start == end is intentional diff --git a/src/lexer.rs b/src/lexer.rs index f71375a2..552c4acc 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -25,7 +25,7 @@ pub enum Token<'src> { Let, Type, Mod, - Const, + Const, // Currently unused Match, Enum, Crate, diff --git a/src/lib.rs b/src/lib.rs index eea90535..f245c772 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,7 @@ pub use simplicity::elements; use crate::debug::DebugSymbols; use crate::driver::{DependencyGraph, SourceMap, MAIN_MODULE}; use crate::error::DiagnosticManager; -use crate::parse::ParseFromStrWithErrors; +use crate::parse::ParseFromContent; use crate::resolution::DependencyMap; use crate::source::CanonSourceFile; pub use crate::types::ResolvedType; @@ -86,6 +86,10 @@ impl TemplateProgram { return Err(diagnostics); }; + if diagnostics.has_errors() { + return Err(diagnostics); + } + Ok(resolved_program.to_string()) } @@ -111,20 +115,23 @@ impl TemplateProgram { return Err(diagnostics); }; - // TODO: Add multierror to analyze - match ast::Program::analyze(&resolved_program, jet_hinter.clone_box()) { - Ok(simfony) => Ok(Self { - simfony, - file, - jet_hinter, - diagnostics, - resolved_program, - }), - Err(e) => { - diagnostics.push(e); - Err(diagnostics) - } + let Some(simfony) = + ast::Program::analyze(&resolved_program, jet_hinter.clone_box(), &mut diagnostics) + else { + return Err(diagnostics); + }; + + if diagnostics.has_errors() { + return Err(diagnostics); } + + Ok(Self { + simfony, + file, + jet_hinter, + diagnostics, + resolved_program, + }) } /// Parse the template of a SimplicityHL program. @@ -149,7 +156,7 @@ impl TemplateProgram { let mut diagnostics = DiagnosticManager::default(); let file = s.into(); - let Some(resolved_program) = parse::Program::parse_from_str_with_errors( + let Some(resolved_program) = parse::Program::parse_from_content( MAIN_MODULE, &file, unstable_features, @@ -158,19 +165,23 @@ impl TemplateProgram { return Err(diagnostics); }; - match ast::Program::analyze(&resolved_program, jet_hinter.clone_box()) { - Ok(simfony) => Ok(Self { - simfony, - file, - jet_hinter, - diagnostics, - resolved_program, - }), - Err(e) => { - diagnostics.push(e); - Err(diagnostics) - } + let Some(simfony) = + ast::Program::analyze(&resolved_program, jet_hinter.clone_box(), &mut diagnostics) + else { + return Err(diagnostics); + }; + + if diagnostics.has_errors() { + return Err(diagnostics); } + + Ok(Self { + simfony, + file, + jet_hinter, + diagnostics, + resolved_program, + }) } /// Access the parameters of the program. @@ -594,7 +605,7 @@ pub(crate) mod tests { let mut diagnostics = DiagnosticManager::new(); - let parse_program = parse::Program::parse_from_str_with_errors( + let parse_program = parse::Program::parse_from_content( MAIN_MODULE, &file, &UnstableFeatures::all(), diff --git a/src/parse.rs b/src/parse.rs index 547af341..04e113f6 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -19,8 +19,7 @@ use itertools::Itertools; use miniscript::iter::{Tree, TreeLike}; use crate::driver::{CRATE_STR, MAIN_MODULE}; -use crate::error::DiagnosticManager; -use crate::error::{Diagnostic, Error, Span}; +use crate::error::{Diagnostic, DiagnosticManager, Error, Severity, Span}; use crate::impl_eq_hash; use crate::lexer::{Token, Tokens}; use crate::num::NonZeroPow2Usize; @@ -30,7 +29,7 @@ use crate::str::{ SymbolName, WitnessName, }; use crate::types::{AliasedType, BuiltinAlias, TypeConstructible, UIntType}; -use crate::unstable::{impl_require_feature, RequireFeature, UnstableFeature, UnstableFeatures}; +use crate::unstable::{impl_require_feature, UnstableFeature, UnstableFeatures}; use crate::version::SimcDirective; #[cfg(feature = "fmt")] @@ -92,8 +91,13 @@ impl Program { } /// Parse source for formatting while retaining all comments and whitespace. + /// + /// Applies the same policy as [`ParseFromContent::parse_from_content`], but + /// lexes losslessly and keeps the token stream, which the formatter needs in + /// order to reproduce trivia. The grammar still only ever sees the semantic + /// tokens, so the two paths accept exactly the same language. #[cfg(feature = "fmt")] - pub fn parse_with_errors_for_fmt<'src>( + pub fn parse_from_content_for_fmt<'src>( file_id: usize, source: &'src str, unstable_features: &UnstableFeatures, @@ -101,35 +105,61 @@ impl Program { ) -> Option> { let before = diagnostics.error_count(); - let start = pipeline::directive_prescan(source, file_id, diagnostics)?; - - let (tokens, lex_errors) = crate::lexer::lex_lossless(file_id, source, start); - let lex_ok = pipeline::is_lex_ok(lex_errors, diagnostics)?; + // A `simc` directive is source-file syntax. An incompatible or malformed + // one is the only meaningful diagnostic; lexing starts after a valid one, + // so the lexer/grammar never see it. + let start = match SimcDirective::prescan(source, file_id) { + Ok(start) => start, + Err((err, span)) => { + diagnostics.push(Diagnostic::new(err, span)); + return None; + } + }; - let tokens = tokens?; + let (tokens, mut diags) = crate::lexer::lex_lossless(file_id, source, start); - let semantic_tokens = tokens + // A stray `simc` past the prescan point makes every other diagnostic + // noise (its `"";` remnant does not lex), so report it alone. + if diags .iter() - .filter_map(|(token, span)| match token { - FmtToken::Token(token) => Some((token.clone(), *span)), - FmtToken::Trivia(_) => None, - }) - .collect::>(); + .any(|diag| matches!(diag.error(), Error::ReservedSimcKeyword)) + { + diags.retain(|diag| matches!(diag.error(), Error::ReservedSimcKeyword)); + diagnostics.extend(diags); + return None; + } - let (program, parse_ok) = - pipeline::parse_ast(file_id, source, semantic_tokens, diagnostics); + // The grammar is defined over semantic tokens; the trivia stays behind + // in `tokens` for the formatter to reproduce. + let semantic_tokens: Option> = tokens.as_ref().map(|tokens| { + tokens + .iter() + .filter_map(|(token, span)| match token { + FmtToken::Token(token) => Some((token.clone(), *span)), + FmtToken::Trivia(_) => None, + }) + .collect() + }); - if parse_ok && lex_ok { - pipeline::post_check(unstable_features, program.as_ref(), diagnostics); + let program = parse_tokens::(file_id, source, semantic_tokens.as_ref(), &mut diags); + + let parse_clean = diags.is_empty(); + diagnostics.extend(diags); + + // Feature gating reads the tree, so it only runs on a tree the parser + // built without recovering: a poisoned node has no feature to check. + if let (Some(program), true) = (&program, parse_clean) { + unstable_features.check_program(program, diagnostics); } if diagnostics.error_count() > before { None } else { - let program = program?; + // Both are `Some` here: a missing stream or tree means `parse_tokens` + // reported, which the count check above already caught. Some(ParsedSource { - program, - tokens, + program: program?, + tokens: tokens?, prefix: Span::new(file_id, 0..start), }) } @@ -410,6 +440,8 @@ pub enum Statement { Assignment(Assignment), /// An expression that returns nothing (the unit value). Expression(Expression), + /// Recovered from a parse error; a diagnostic was already emitted. + Error(Span), } impl Statement { @@ -421,6 +453,7 @@ impl Statement { match self { Self::Assignment(assignment) => assignment.span(), Self::Expression(expression) => expression.span(), + Self::Error(span) => span, } } } @@ -429,6 +462,7 @@ impl_require_feature!(Statement { variants: Assignment(assignment), Expression(expr), + Error(_), }); /// The output of an expression is assigned to a pattern. @@ -745,10 +779,10 @@ impl Expression { } } - pub fn empty(span: Span) -> Self { + pub fn error(span: Span) -> Self { Self { inner: ExpressionInner::Single(SingleExpression { - inner: SingleExpressionInner::Tuple(Arc::new([])), + inner: SingleExpressionInner::Error, span, }), span, @@ -839,6 +873,8 @@ pub enum SingleExpressionInner { /// /// The exclusive upper bound on the list size is not known at this point List(Arc<[Expression]>), + /// Recovered from a parse error; a diagnostic was already emmited. + Error, } impl_require_feature!(SingleExpressionInner { @@ -860,6 +896,7 @@ impl_require_feature!(SingleExpressionInner { Tuple(exprs), Array(exprs), List(exprs), + Error, }); /// Match expression. @@ -1368,6 +1405,7 @@ impl TreeLike for ExprTree<'_> { Self::Statement(statement) => match statement { Statement::Assignment(assignment) => Tree::Unary(Self::Assignment(assignment)), Statement::Expression(expression) => Tree::Unary(Self::Expression(expression)), + Statement::Error(_) => Tree::Nullary, }, Self::Assignment(assignment) => Tree::Unary(Self::Expression(assignment.expression())), Self::Single(single) => match single.inner() { @@ -1378,7 +1416,8 @@ impl TreeLike for ExprTree<'_> { | S::Variable(_) | S::Witness(_) | S::Parameter(_) - | S::Option(None) => Tree::Nullary, + | S::Option(None) + | S::Error => Tree::Nullary, S::Option(Some(l)) | S::Either(Either::Left(l)) | S::Either(Either::Right(l)) @@ -1477,7 +1516,6 @@ impl fmt::Display for ExprTree<'_> { write!(f, ")")?; } }, - S::Call(..) | S::Match(..) | S::EnumMatch(..) | S::EnumConstruction(..) => {} S::Tuple(tuple) => { if data.n_children_yielded == 0 { write!(f, "(")?; @@ -1508,6 +1546,11 @@ impl fmt::Display for ExprTree<'_> { write!(f, "]")?; } } + S::Call(..) + | S::Match(..) + | S::EnumMatch(..) + | S::EnumConstruction(..) + | S::Error => {} }, Self::Call(call) => { if data.n_children_yielded == 0 { @@ -1667,18 +1710,38 @@ trait AstNode: ChumskyParse + crate::unstable::RequireFeature + std::fmt::Debug impl AstNode for T where T: ChumskyParse + crate::unstable::RequireFeature + std::fmt::Debug {} /// Copy of [`FromStr`] that internally uses the `chumsky` parser. +/// +/// # When to use this vs. [`ParseFromContent`] +/// +/// Use `parse_from_str` for a **bare fragment** — a witness value, a type +/// annotation, a single expression parsed out of a string. A fragment is not a +/// source file: it has no `simc` directive (one appearing here is a +/// reserved-keyword *error*), no `file_id`, and no feature gating. Callers only +/// need to know *whether* it parsed, so a single [`Diagnostic`] — the first +/// error — is the right, simplest surface. +/// +/// For a whole source file, use [`ParseFromContent::parse_from_content`], which +/// collects *every* error instead of stopping at the first. pub trait ParseFromStr: Sized { - /// Parse a value from the string `s`. + /// Parse fragment from the string `s`, returning the first error if any. fn parse_from_str(s: &str) -> Result; } -/// Trait for parsing with collection of errors. -pub trait ParseFromStrWithErrors: Sized { - /// Parse a value from the string `content` with Errors. - /// - /// Feature-gated syntax in the parsed AST is checked against - /// `unstable_features`; uses of disabled features are pushed to `diagnostics`. - fn parse_from_str_with_errors( +/// Parse a whole source file, collecting every diagnostic. +/// +/// # When to use this vs. [`ParseFromStr`] +/// +/// Use `parse_from_content` for a **source file** (the driver, `lib.rs`). Unlike +/// [`ParseFromStr::parse_from_str`], it: +/// * handles a leading `simc` directive via prescan, +/// * carries a `file_id` so diagnostics point at the right file, +/// * checks feature-gated syntax against `unstable_features`, and +/// * pushes *all* lex/parse/feature errors into `diagnostics` rather than +/// returning only the first — so the user sees every problem in one compile. +pub trait ParseFromContent: Sized { + /// Parse the full `content` of a file, pushing every diagnostic into + /// `diagnostics`. Returns `Some` only when no error was produced. + fn parse_from_content( file_id: usize, content: &str, unstable_features: &UnstableFeatures, @@ -1686,71 +1749,53 @@ pub trait ParseFromStrWithErrors: Sized { ) -> Option; } -mod pipeline { - use super::*; - /// Handle the `simc` directive before lexing: an incompatible or malformed - /// directive is reported as the only diagnostic (the rest is noise), and - /// lexing starts right after a valid one, so the lexer and grammar never - /// see it. - pub fn directive_prescan( - content: &str, - file_id: usize, - diagnostics: &mut DiagnosticManager, - ) -> Option { - match SimcDirective::prescan(content, file_id) { - Ok(start) => Some(start), - Err((err, span)) => { - diagnostics.push(Diagnostic::new(err, span)); - None - } - } - } +/// Lex `content` starting at byte offset `start`, then run `A`'s parser over the +/// token stream. Shared core of [`ParseFromStr`] and [`ParseFromContent`]. +/// +/// Returns the (maybe) AST together with every lex and parse diagnostic, in +/// source order. Applies no policy: does not prescan `simc`, does not run the +/// feature check, does not decide whether errors are fatal — the callers do. +fn lex_and_parse( + file_id: usize, + content: &str, + start: usize, +) -> (Option, Vec) { + let (tokens, mut diags) = crate::lexer::lex(file_id, content, start); + let ast = parse_tokens::(file_id, content, tokens.as_ref(), &mut diags); - pub fn is_lex_ok( - mut lex_errs: Vec, - diagnostics: &mut DiagnosticManager, - ) -> Option { - // A stray `simc` makes every other diagnostic noise — its `"";` remnant - // does not lex — so the reserved-keyword errors are reported alone. - if lex_errs - .iter() - .any(|e| matches!(e.error(), Error::ReservedSimcKeyword)) - { - lex_errs.retain(|e| matches!(e.error(), Error::ReservedSimcKeyword)); - diagnostics.extend(lex_errs); - None - } else { - let lex_ok = lex_errs.is_empty(); - diagnostics.extend(lex_errs); - Some(lex_ok) - } - } + (ast, diags) +} - pub fn parse_ast( - file_id: usize, - src: &str, - tokens: Tokens<'_>, - diagnostics: &mut DiagnosticManager, - ) -> (Option, bool) { - let eoi = Span::eof(file_id, src.len()); - let (ast, parse_errs) = T::parser() - .parse(tokens.as_slice().map(eoi, |(t, s)| (t, s))) - .into_output_errors(); +/// Run `A`'s parser over `tokens`, appending every parse diagnostic to `diags`. +/// +/// The half of parsing that does not depend on *how* the source was lexed. Both +/// the plain and the lossless paths reduce to a semantic token stream and then +/// run the same grammar over it, so the grammar and the empty-stream policy live +/// here once. `None` tokens mean the lexer produced no stream at all. +fn parse_tokens( + file_id: usize, + content: &str, + tokens: Option<&Tokens<'_>>, + diags: &mut Vec, +) -> Option { + let Some(tokens) = tokens else { + // Lexer failed to produce a stream; surface whatever it reported (or a + // fallback so the caller never sees an empty error set with no AST). + if diags.is_empty() { + diags.push(Diagnostic::global(Error::CannotParse { + msg: "Empty token stream without an error".to_string(), + })); + } + return None; + }; - let parse_ok = parse_errs.is_empty(); - diagnostics.extend(parse_errs); - (ast, parse_ok) - } + let eoi = Span::eof(file_id, content.len()); + let (ast, parse_errs) = A::parser() + .parse(tokens.as_slice().map(eoi, |(t, s)| (t, s))) + .into_output_errors(); + diags.extend(parse_errs); - pub fn post_check( - unstable_features: &UnstableFeatures, - program: Option<&T>, - diagnostics: &mut DiagnosticManager, - ) { - if let Some(ast) = program { - unstable_features.check_program(ast, diagnostics); - } - } + ast } /// Trait for generating parsers of themselves. @@ -1767,48 +1812,42 @@ type ParseError<'src> = extra::Err; /// This implementation only returns first encountered error. impl ParseFromStr for A { fn parse_from_str(s: &str) -> Result { - let (tokens, mut lex_errs) = crate::lexer::lex(MAIN_MODULE, s, 0); + let (ast, diags) = lex_and_parse::(MAIN_MODULE, s, 0); // The `simc` directive is source-file syntax, so fragments have no prescan // and `simc` lexes as a reserved keyword. Its `"";` remnant does not // lex either, so the first reserved-keyword error is reported alone. - if let Some(err) = lex_errs - .iter() - .find(|diag| matches!(diag.error(), Error::ReservedSimcKeyword)) - { - return Err(err.clone()); - } + let mut simc_err = None; + let mut first_err = None; - let Some(tokens) = tokens else { - return Err(lex_errs - .pop() - .unwrap_or(Diagnostic::global(Error::CannotParse { - msg: "Empty token stream without an error".to_string(), - }))); - }; + for diag in &diags { + if simc_err.is_none() && matches!(diag.error(), Error::ReservedSimcKeyword) { + simc_err = Some(diag); + } - let (ast, parse_errs) = A::parser() - .map_with(|parsed, _| parsed) - .parse( - tokens - .as_slice() - .map(Span::eof(MAIN_MODULE, s.len()), |(t, s)| (t, s)), - ) - .into_output_errors(); + if first_err.is_none() && matches!(diag.severity(), Severity::Error) { + first_err = Some(diag); + } - if parse_errs.is_empty() { - Ok(ast.ok_or(Diagnostic::global(Error::CannotParse { - msg: "Empty AST without an error.".to_string(), - }))?) - } else { - let err = parse_errs.first().unwrap().clone(); - Err(err) + if simc_err.is_some() && first_err.is_some() { + break; + } + } + + if let Some(diag) = simc_err.or(first_err) { + return Err(diag.clone()); } + + ast.ok_or_else(|| { + Diagnostic::global(Error::CannotParse { + msg: "Empty AST without an error.".to_string(), + }) + }) } } -impl ParseFromStrWithErrors for A { - fn parse_from_str_with_errors( +impl ParseFromContent for A { + fn parse_from_content( file_id: usize, content: &str, unstable_features: &UnstableFeatures, @@ -1816,22 +1855,37 @@ impl ParseFromStrWithErrors for A { ) -> Option { let before = diagnostics.error_count(); - let start = pipeline::directive_prescan(content, file_id, diagnostics)?; - - let (tokens, lex_errs) = crate::lexer::lex(file_id, content, start); + // A `simc` directive is source-file syntax. An incompatible or malformed + // one is the only meaningful diagnostic; lexing starts after a valid one, + // so the lexer/grammar never see it. + let start = match SimcDirective::prescan(content, file_id) { + Ok(start) => start, + Err((err, span)) => { + diagnostics.push(Diagnostic::new(err, span)); + return None; + } + }; - let lex_ok = pipeline::is_lex_ok(lex_errs, diagnostics)?; + let (ast, mut diags) = lex_and_parse::(file_id, content, start); - let tokens = tokens?; + // A stray `simc` past the prescan point makes every other diagnostic + // noise (its `"";` remnant does not lex), so report it alone. + if diags + .iter() + .any(|diag| matches!(diag.error(), Error::ReservedSimcKeyword)) + { + diags.retain(|diag| matches!(diag.error(), Error::ReservedSimcKeyword)); + diagnostics.extend(diags); + return None; + } - let (ast, parse_status) = pipeline::parse_ast::(file_id, content, tokens, diagnostics); + let parse_clean = diags.is_empty(); + diagnostics.extend(diags); - if lex_ok && parse_status { - let () = pipeline::post_check(unstable_features, ast.as_ref(), diagnostics); + if let (Some(ast), true) = (&ast, parse_clean) { + unstable_features.check_program(ast, diagnostics); } - // TODO: We should return parsed result if we found errors, but because analyzing in `ast` module - // is not handling poisoned tree right now, we don't return parsed result if diagnostics.error_count() > before { None } else { @@ -1919,11 +1973,12 @@ impl ChumskyParse for AliasedType { Token::DecLiteral(i) => i.clone() } .labelled("decimal number") + .map(Some) .recover_with(via_parser( none_of([Token::RAngle, Token::RBracket]) .ignored() .or(empty()) - .to(Decimal::from_str_unchecked("0")), + .to(None), )); recursive(|ty| { @@ -1933,12 +1988,7 @@ impl ChumskyParse for AliasedType { .then(ty.clone()), Token::LAngle, Token::RAngle, - |_| { - ( - AliasedType::alias(AliasName::from_str_unchecked("error")), - AliasedType::alias(AliasName::from_str_unchecked("error")), - ) - }, + |_| (AliasedType::error(), AliasedType::error()), ); let sum_type = just(Token::Ident("Either")) @@ -1951,7 +2001,7 @@ impl ChumskyParse for AliasedType { ty.clone(), Token::LAngle, Token::RAngle, - |_| AliasedType::alias(AliasName::from_str_unchecked("error")), + |_| AliasedType::error(), )) .map(AliasedType::option) .labelled("Option"); @@ -1972,55 +2022,62 @@ impl ChumskyParse for AliasedType { ty.clone() .then_ignore(parse_token_with_recovery(Token::Semi)) .then(num.clone()) - .map(|(ty, size)| { - let digits = - crate::str::underscore_parsing::strip_digit_separators(size.as_inner()); + .validate(|(ty, size), e, emit| match size { + None => AliasedType::error(), + Some(size) => { + let digits = crate::str::underscore_parsing::strip_digit_separators( + size.as_inner(), + ); - AliasedType::array(ty, usize::from_str(digits.as_ref()).unwrap_or_default()) + match usize::from_str(digits.as_ref()) { + Ok(n) => AliasedType::array(ty, n), + Err(_) => { + emit.emit( + Error::Grammar { + msg: format!("Invalid array size `{size}`"), + } + .with_span(e.span()), + ); + AliasedType::error() + } + } + } }), Token::LBracket, Token::RBracket, - |_| { - AliasedType::array( - AliasedType::alias(AliasName::from_str_unchecked("error")), - 0, - ) - }, + |_| AliasedType::error(), ) .labelled("array"); let list = just(Token::Ident("List")) .ignore_then(delimited_with_recovery( ty.then_ignore(parse_token_with_recovery(Token::Comma)) - .then(num.clone().validate(|num, e, emit| -> NonZeroPow2Usize { - let digits = crate::str::underscore_parsing::strip_digit_separators( - num.as_inner(), - ); - - match NonZeroPow2Usize::from_str(digits.as_ref()) { - Ok(number) => number, - Err(err) => { - emit.emit( - Error::Grammar { - msg: format!("Cannot parse list bound: {err}"), - } - .with_span(e.span()), - ); - // fallback to default value - NonZeroPow2Usize::TWO + .then(num.clone()) + .validate(|(ty, bound), e, emit| match bound { + None => AliasedType::error(), + Some(size) => { + let digits = crate::str::underscore_parsing::strip_digit_separators( + size.as_inner(), + ); + + match NonZeroPow2Usize::from_str(digits.as_ref()) { + Ok(b) => AliasedType::list(ty, b), + Err(err) => { + emit.emit( + Error::Grammar { + msg: format!("Cannot parse list bound: {err}"), + } + .with_span(e.span()), + ); + AliasedType::error() + } } } - })), + }), Token::LAngle, Token::RAngle, - |_| { - ( - AliasedType::alias(AliasName::from_str_unchecked("error")), - NonZeroPow2Usize::TWO, - ) - }, + |_| AliasedType::error(), )) - .map(|(ty, size)| AliasedType::list(ty, size)) .labelled("List"); choice((sum_type, option_type, tuple, array, list, atom)) @@ -2133,7 +2190,7 @@ impl ChumskyParse for Function { (Token::LParen, Token::RParen), (Token::LBracket, Token::RBracket), ], - Expression::empty, + Expression::error, ))) .labelled("function body"); @@ -2608,7 +2665,7 @@ impl ChumskyParse for Expression { (Token::LAngle, Token::RAngle), (Token::LBracket, Token::RBracket), ], - |span| Expression::empty(span).inner().clone(), + |span| Expression::error(span).inner().clone(), ); let statements = statement @@ -2787,12 +2844,7 @@ impl ChumskyParse for MatchPattern { .then(AliasedType::parser()), Token::LParen, Token::RParen, - |_| { - ( - Pattern::Ignore, - AliasedType::alias(AliasName::from_str_unchecked("error")), - ) - }, + |_| (Pattern::Ignore, AliasedType::error()), )) .map(move |(id, ty)| ctor(id, ty)) }; @@ -2917,22 +2969,6 @@ where }) } -/// A binary match with dummy arms, standing in for a malformed match so -/// parsing can continue after its error was reported. -fn placeholder_match(scrutinee: Arc, span: Span) -> SingleExpressionInner { - let fallback_arm = MatchArm { - expression: Arc::new(Expression::empty(Span::DUMMY)), - pattern: MatchPattern::False, - span: Span::DUMMY, - }; - SingleExpressionInner::Match(Match { - scrutinee, - left: fallback_arm.clone(), - right: fallback_arm, - span, - }) -} - /// Do the two patterns complement each other in canonical order /// (`Left`/`Right`, `None`/`Some`, `false`/`true`)? fn patterns_in_canonical_order(left: &MatchPattern, right: &MatchPattern) -> bool { @@ -2961,7 +2997,7 @@ fn assemble_match_arms( msg: "match expression has no arms".to_string(), } .with_span(span), - placeholder_match(scrutinee, span), + SingleExpressionInner::Error, ))); } @@ -2998,7 +3034,7 @@ fn assemble_match_arms( ), } .with_span(span), - placeholder_match(scrutinee, span), + SingleExpressionInner::Error, ))); } @@ -3009,7 +3045,7 @@ fn assemble_match_arms( msg: "binary match requires exactly 2 arms".to_string(), } .with_span(span), - placeholder_match(scrutinee, span), + SingleExpressionInner::Error, ))); }; @@ -3588,14 +3624,14 @@ mod regular_parsing { let input = "fn main() { let ab: u8 = <(u4, u4)> : :into((0b1011, 0b1101)); }"; let mut diagnostics = DiagnosticManager::new(); - let parsed_program = Program::parse_from_str_with_errors( + let _ = Program::parse_from_content( MAIN_MODULE, input, &UnstableFeatures::all(), &mut diagnostics, ); - assert!(parsed_program.is_none()); + assert!(diagnostics.has_errors()); assert!(diagnostics.to_string().contains("Expected '::', found ':'")); } @@ -3604,14 +3640,14 @@ mod regular_parsing { let input = "fn main() { let pk: Pubkey = witnes::::PK; }"; let mut diagnostics = DiagnosticManager::new(); - let parsed_program = Program::parse_from_str_with_errors( + let _ = Program::parse_from_content( MAIN_MODULE, input, &UnstableFeatures::all(), &mut diagnostics, ); - assert!(parsed_program.is_none()); + assert!(diagnostics.has_errors()); assert!( diagnostics .to_string() @@ -3620,16 +3656,15 @@ mod regular_parsing { ); } - /// Parse `input` and return whether it was rejected and the collected error text. + /// Parse `input` and return whether it was has errors and the collected error text. fn parse_with(input: &str, features: &UnstableFeatures) -> (bool, String) { let mut diagnostics = DiagnosticManager::new(); - let program = - Program::parse_from_str_with_errors(MAIN_MODULE, input, features, &mut diagnostics); + let _ = Program::parse_from_content(MAIN_MODULE, input, features, &mut diagnostics); - let rejected = program.is_none(); + let has_errors = diagnostics.has_errors(); let text = diagnostics.to_string(); - (rejected, text) + (has_errors, text) } #[test] @@ -3645,14 +3680,13 @@ mod regular_parsing { // Simplifying any part (even NUL to a space) loses the crash. let src = "fn`u({?\u{12}$0;;enum\0===lHf\u{15}"; let mut diagnostics = DiagnosticManager::new(); - let program = Program::parse_from_str_with_errors( + let _ = Program::parse_from_content( MAIN_MODULE, src, &UnstableFeatures::all(), &mut diagnostics, ); - assert!(program.is_none(), "garbage input must be rejected"); assert!(diagnostics.has_errors()); } @@ -3664,13 +3698,12 @@ mod regular_parsing { // error is reported; with it, the enum parses as its own item and // its malformation is reported as a second error. let mut diagnostics = DiagnosticManager::new(); - let program = Program::parse_from_str_with_errors( + let _ = Program::parse_from_content( MAIN_MODULE, "let invalid = 1;\nenum Action { A B }\nfn main() {}", &UnstableFeatures::all(), &mut diagnostics, ); - assert!(program.is_none(), "the invalid program must be rejected"); assert_eq!( 2, diagnostics.error_count(), @@ -3742,7 +3775,7 @@ fn main() { assert!(jet::eq_32(chosen, chosen)); } "#; - // `parse_from_str_with_errors` returns `Some` only when no errors were + // `parse_from_content` returns `Some` only when no errors were // collected, so a non-rejection already proves there were no gate errors. let (rejected, error) = parse_with(input, &UnstableFeatures::none()); assert!( @@ -3873,7 +3906,7 @@ mod fmt_parsing { features: &UnstableFeatures, diagnostics: &mut DiagnosticManager, ) -> Option> { - Program::parse_with_errors_for_fmt(MAIN_MODULE, input, features, diagnostics) + Program::parse_from_content_for_fmt(MAIN_MODULE, input, features, diagnostics) } /// Parse `input` and return whether it was rejected and the collected error text. @@ -3913,12 +3946,8 @@ mod fmt_parsing { fn assert_formatter_matches_regular_parser(input: &str, features: &UnstableFeatures) { let mut regular_diagnostics = DiagnosticManager::new(); - let regular = Program::parse_from_str_with_errors( - MAIN_MODULE, - input, - features, - &mut regular_diagnostics, - ); + let regular = + Program::parse_from_content(MAIN_MODULE, input, features, &mut regular_diagnostics); let mut formatting_diagnostics = DiagnosticManager::new(); let formatting = parse_with_diagnostics(input, features, &mut formatting_diagnostics); @@ -4092,7 +4121,7 @@ fn main() { assert!(jet::eq_32(chosen, chosen)); } "#; - // `parse_from_str_with_errors` returns `Some` only when no errors were + // `parse_from_content` returns `Some` only when no errors were // collected, so a non-rejection already proves there were no gate errors. let (rejected, error) = parse_with(input, &UnstableFeatures::none()); assert!( diff --git a/src/pattern.rs b/src/pattern.rs index caf6f608..c51d301e 100644 --- a/src/pattern.rs +++ b/src/pattern.rs @@ -41,6 +41,17 @@ impl Pattern { Self::Array(elements.into_iter().collect()) } + /// Get an iterator over every identifier the pattern declares. + /// + /// Unlike [`Self::is_of_type`] this needs no type, so it still lists the + /// declared names when the pattern does not match its annotation. + pub fn identifiers(&self) -> impl Iterator { + self.pre_order_iter().filter_map(|pattern| match pattern { + Pattern::Identifier(identifier) => Some(identifier), + Pattern::Ignore | Pattern::Tuple(..) | Pattern::Array(..) => None, + }) + } + /// Check if the pattern matches the given type. /// /// Return a map of bound variable identifiers to their assigned type. @@ -48,8 +59,10 @@ impl Pattern { &self, ty: &ResolvedType, ) -> Result, Error> { + let err = ResolvedType::error(); let mut stack = vec![(self, ty)]; let mut output = HashMap::new(); + while let Some((pattern, ty)) = stack.pop() { match (pattern, ty.as_inner()) { (Pattern::Identifier(i), _) => match output.entry(i.clone()) { @@ -69,9 +82,16 @@ impl Pattern { (Pattern::Array(pats), TypeInner::Array(ty, size)) if pats.len() == *size => { stack.extend(pats.iter().zip(std::iter::repeat(ty.as_ref()))); } + // Poison type absorbs the shape check: recurse binding each sub-pattern + // at error() instead of reporting a mismatch. + (Pattern::Tuple(pats), TypeInner::Error) + | (Pattern::Array(pats), TypeInner::Error) => { + stack.extend(pats.iter().map(|p| (p, &err))); + } _ => return Err(Error::ExpressionUnexpectedType { ty: ty.clone() }), } } + Ok(output) } } diff --git a/src/types.rs b/src/types.rs index b2d67f67..cd67324a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -31,6 +31,9 @@ pub enum TypeInner { /// Nominal enum type, represented as a balanced sum of its variants' /// payload types Enum(EnumInfo), + /// Type of a recovered subtree. Compatible with every type; never re-reported. + /// A diagnostic was already emitted for its span. + Error, } /// One variant of a nominal enum type: its name and payload types. @@ -201,6 +204,7 @@ impl TypeInner { } }, TypeInner::Enum(info) => write!(f, "{}", info.name()), + TypeInner::Error => write!(f, ""), } } } @@ -455,10 +459,36 @@ pub trait TypeDeconstructible: Sized { pub struct ResolvedType(TypeInner>); impl ResolvedType { + /// Diagnostic-facing compatibility: a poisoned type unifies with anything, + /// recursively. Use before emitting a type-mismatch error. + pub fn compatible(&self, other: &Self) -> bool { + use TypeInner::*; + + match (self.as_inner(), other.as_inner()) { + (Error, _) | (_, Error) => true, + (Either(a, b), Either(c, d)) => a.compatible(c) && b.compatible(d), + (Option(a), Option(b)) => a.compatible(b), + (Tuple(a), Tuple(b)) => { + a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| x.compatible(y)) + } + (Array(a, m), Array(b, n)) => m == n && a.compatible(b), + (List(a, m), List(b, n)) => m == n && a.compatible(b), + _ => self == other, + } + } + /// Access the inner type primitive. pub fn as_inner(&self) -> &TypeInner> { &self.0 } + + pub(crate) const fn error() -> Self { + Self(TypeInner::Error) + } + + pub(crate) fn is_error(&self) -> bool { + matches!(self.0, TypeInner::Error) + } } /// Nominal enum types. @@ -490,6 +520,16 @@ impl ResolvedType { self.post_order_iter() .any(|data| data.node.as_enum().is_some()) } + + /// Check whether the type is poisoned, at any nesting depth. + /// + /// [`Self::is_error`] only inspects the outermost constructor, but a + /// poisoned leaf anywhere makes the type unrepresentable as a + /// [`StructuralType`], so analysis must consult this before any + /// structural comparison. + pub(crate) fn contains_error(&self) -> bool { + self.post_order_iter().any(|data| data.node.is_error()) + } } impl TypeConstructible for ResolvedType { @@ -571,7 +611,9 @@ impl TypeDeconstructible for ResolvedType { impl TreeLike for &ResolvedType { fn as_node(&self) -> Tree { match &self.0 { - TypeInner::Boolean | TypeInner::UInt(..) | TypeInner::Enum(..) => Tree::Nullary, + TypeInner::Boolean | TypeInner::UInt(..) | TypeInner::Enum(..) | TypeInner::Error => { + Tree::Nullary + } TypeInner::Option(l) | TypeInner::Array(l, _) | TypeInner::List(l, _) => Tree::Unary(l), TypeInner::Either(l, r) => Tree::Binary(l, r), TypeInner::Tuple(elements) => Tree::Nary(elements.iter().map(Arc::as_ref).collect()), @@ -729,6 +771,10 @@ impl AliasedType { Self(AliasedInner::Builtin(builtin)) } + pub(crate) const fn error() -> Self { + Self(AliasedInner::Inner(TypeInner::Error)) + } + /// Resolve all aliases in the type based on the given map of `aliases` to types. pub fn resolve(&self, mut get_alias: F) -> Result where @@ -775,6 +821,7 @@ impl AliasedType { TypeInner::Enum(info) => { output.push(ResolvedType::enumeration(info.clone())); } + TypeInner::Error => output.push(ResolvedType::error()), }, } } @@ -809,6 +856,7 @@ impl_require_feature!(TypeInner> { Array(element, _), List(element, _), Enum(_), + Error, }); impl TypeConstructible for AliasedType { @@ -901,7 +949,10 @@ impl TreeLike for &AliasedType { match &self.0 { AliasedInner::Alias(_) | AliasedInner::Builtin(_) => Tree::Nullary, AliasedInner::Inner(inner) => match inner { - TypeInner::Boolean | TypeInner::UInt(..) | TypeInner::Enum(..) => Tree::Nullary, + TypeInner::Boolean + | TypeInner::UInt(..) + | TypeInner::Enum(..) + | TypeInner::Error => Tree::Nullary, TypeInner::Option(l) | TypeInner::Array(l, _) | TypeInner::List(l, _) => { Tree::Unary(l) } @@ -1205,6 +1256,9 @@ impl From<&ResolvedType> for StructuralType { TypeInner::Enum(info) => { output.push(StructuralType::balanced_sum(info.structural_variants())); } + TypeInner::Error => unreachable!( + "poisoned type reached codegen; compilation must be gated on zero errors" + ), } } debug_assert_eq!(output.len(), 1); diff --git a/src/value.rs b/src/value.rs index 2dd1ca22..70630b4c 100644 --- a/src/value.rs +++ b/src/value.rs @@ -666,6 +666,7 @@ impl Value { if s.len() % 2 != 0 || s.len() != expected_byte_len * 2 { return Err(Error::ExpressionUnexpectedType { ty: ty.clone() }); } + let bytes = Vec::::from_hex(s).expect("valid chars and valid length"); let ret = match ty.as_inner() { TypeInner::UInt(..) => { @@ -674,6 +675,7 @@ impl Value { TypeInner::Array(..) => Self::byte_array(bytes), _ => unreachable!(), }; + Ok(ret) } } @@ -714,7 +716,8 @@ impl Value { | S::Variable(..) | S::Call(..) | S::Match(..) - | S::EnumMatch(..) => return None, // not const + | S::EnumMatch(..) + | S::Error => return None, // not const S::Expression(..) => continue, // skip S::Tuple(..) => { let elements = output.split_off(output.len() - size); @@ -864,6 +867,7 @@ impl Value { } } } + TypeInner::Error => unreachable!("poisoned type in value reconstruction; values exist only for error-free programs"), } } debug_assert_eq!(output.len(), 1); @@ -874,8 +878,9 @@ impl Value { pub fn parse_from_str(s: &str, ty: &ResolvedType) -> Result { let parse_expr = parse::Expression::parse_from_str(s)?; let ast_expr = ast::Expression::analyze_const(&parse_expr, ty)?; + Self::from_const_expr(&ast_expr) - .ok_or(Error::ExpressionUnexpectedType { ty: ty.clone() }) + .ok_or_else(|| Error::ExpressionUnexpectedType { ty: ty.clone() }) .with_span(s) } } @@ -937,6 +942,7 @@ impl crate::ArbitraryOfType for Value { .collect::>>()?; Ok(Self::list(elements, ty.as_ref().clone(), *bound)) } + TypeInner::Error => unreachable!("cannot generate a value of a poisoned type"), } } } @@ -1257,7 +1263,7 @@ impl TreeLike for Destructor<'_> { Self::WrongType => return Tree::Nullary, }; match ty.as_inner() { - TypeInner::Boolean | TypeInner::UInt(..) => Tree::Nullary, + TypeInner::Boolean | TypeInner::UInt(..) | TypeInner::Error => Tree::Nullary, TypeInner::Enum(info) => match destruct::as_enum_leaf(value, info.variants().len()) { Some((index, leaf)) => { Tree::Unary(Self::new(leaf, info.variants()[index].payload_type())) @@ -1649,4 +1655,38 @@ mod tests { assert!(parsed_value.is_of_type(&ty)); } } + + #[test] + fn value_parse_from_str_demo() { + use crate::parse::ParseFromStr; // brings ResolvedType::parse_from_str into scope + use crate::types::ResolvedType; + use crate::value::Value; + + // 1. A plain integer literal, parsed at u32. + let u32_ty = ResolvedType::parse_from_str("u32").unwrap(); + let v = Value::parse_from_str("42", &u32_ty).unwrap(); + assert_eq!(v, Value::u32(42)); + + // 2. A tuple — analyze_const recurses into each element against its expected type. + let pair_ty = ResolvedType::parse_from_str("(u32, bool)").unwrap(); + let v = Value::parse_from_str("(42, true)", &pair_ty).unwrap(); + assert_eq!("(42, true)", v.to_string()); + + // 3. An Option wrapper. + let opt_ty = ResolvedType::parse_from_str("Option").unwrap(); + let v = Value::parse_from_str("Some(7)", &opt_ty).unwrap(); + assert_eq!("Some(7)", v.to_string()); + + // 4. An array. + let arr_ty = ResolvedType::parse_from_str("[u16; 3]").unwrap(); + let v = Value::parse_from_str("[1, 2, 3]", &arr_ty).unwrap(); + assert_eq!("[1, 2, 3]", v.to_string()); + + // 5. Type mismatch is rejected — `true` is not a u32. + // (This is the path where analyze_const's is_error/mismatch checks live.) + assert!(Value::parse_from_str("true", &u32_ty).is_err()); + + // 6. A non-constant expression is rejected — no scope, so `witness::A` can't resolve. + assert!(Value::parse_from_str("witness::A", &u32_ty).is_err()); + } } diff --git a/src/witness.rs b/src/witness.rs index f2aad16a..aa840354 100644 --- a/src/witness.rs +++ b/src/witness.rs @@ -191,6 +191,7 @@ impl UnresolvedValues { { let declared_types = declared_types.as_ref(); let mut map = HashMap::with_capacity(self.0.len()); + for (name, unresolved) in self.0 { let value = match unresolved { UnresolvedValue::Typed(value) => value, @@ -318,14 +319,28 @@ mod tests { let s = r#"fn main() { assert!(jet::eq_32(witness::A, witness::A)); }"#; + + let mut diagnostics = DiagnosticManager::new(); let parse_program = parse::Program::parse_from_str(s).expect("parsing works"); - match ast::Program::analyze(&parse_program, Box::new(ElementsJetHinter::new())) - .map_err(Error::from) - { - Ok(_) => panic!("Witness reuse was falsely accepted"), - Err(Error::WitnessReused { .. }) => {} - Err(error) => panic!("Unexpected error: {error}"), - } + + let ast = ast::Program::analyze( + &parse_program, + Box::new(ElementsJetHinter::new()), + &mut diagnostics, + ); + + assert!(ast.is_none(), "witness reuse was falsely accepted"); + + let found = diagnostics + .diagnostics() + .iter() + .any(|diag| matches!(diag.error(), Error::WitnessReused { .. })); + + assert!( + found, + "expected a WitnessReused diagnostic, got: {:?}", + diagnostics.to_string() + ); } #[test]