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 8d4a0c93..980bbf1a 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. @@ -565,6 +572,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() { @@ -572,7 +580,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)) @@ -704,6 +713,8 @@ struct Scope { is_main: bool, call_tracker: CallTracker, jet_hinter: Box, + + diagnostics: Vec, } impl Default for Scope { @@ -711,12 +722,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(), @@ -727,6 +739,7 @@ impl Scope { is_main: false, call_tracker: CallTracker::default(), jet_hinter, + diagnostics, } } @@ -743,49 +756,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 @@ -804,6 +774,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 @@ -1051,6 +1038,29 @@ impl Scope { ty.resolve(|name| self.get_alias(name)) } + /// Resolve a type with aliases, substituting a poison type for every alias + /// that is not defined and reporting each one. + /// + /// 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. fn check_alias_free(&self, name: &AliasName) -> Result<(), Error> { if self.current_module().aliases.contains_key(name) { @@ -1062,13 +1072,18 @@ 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> { + pub fn insert_alias(&mut self, alias: parse::TypeAlias, span: Span) -> Result<(), Error> { + // A redefinition keeps the existing binding, which is the good one. self.check_alias_free(alias.name())?; - let resolved = self.resolve(alias.ty())?; + let resolved = self.resolve_or_poison(alias.ty(), span); self.current_module_mut() .aliases @@ -1107,6 +1122,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 @@ -1114,7 +1152,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, @@ -1197,6 +1236,101 @@ impl Scope { 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. @@ -1217,30 +1351,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), @@ -1288,7 +1442,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) => { @@ -1299,42 +1454,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)?; @@ -1342,18 +1518,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), } } } @@ -1373,29 +1563,29 @@ 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 }) + .map(|param| FunctionParam { + identifier: param.identifier().clone(), + ty: scope.resolve_or_poison(param.ty(), *from.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 }; @@ -1406,23 +1596,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)); + } + + // 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); } - scope.enter_main(); - let body = Expression::analyze(from.body(), ty, scope)?; - scope.exit_main(); + // 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)) } } @@ -1443,6 +1659,7 @@ impl AbstractSyntaxTree for Statement { parse::Statement::Expression(expression) => { Expression::analyze(expression, ty, scope).map(Self::Expression) } + parse::Statement::Error(_) => Ok(Self::Error), } } } @@ -1450,6 +1667,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, @@ -1460,9 +1678,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); } @@ -1490,6 +1715,62 @@ 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 } + } +} + +/// Bind every identifier a pattern declares at a poison type. +/// +/// 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)`. @@ -1505,7 +1786,11 @@ fn analyze_enum_construction( 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); }; @@ -1520,6 +1805,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, @@ -1532,35 +1818,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, @@ -1648,23 +1940,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(), @@ -1686,36 +1990,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 @@ -1732,17 +2053,19 @@ 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()); SingleExpressionInner::Variable(identifier.clone()) } @@ -1752,70 +2075,113 @@ 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() + } + _ => { + if !ty.is_error() { + scope.report( + Error::ExpressionUnexpectedType { ty: ty.clone() } + .with_span(from.into()), + ); + } + 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 => { + if !ty.is_error() { + scope.report( + Error::ExpressionUnexpectedType { ty: ty.clone() } + .with_span(from.into()), + ); + } + 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 => { + if !ty.is_error() { + scope.report( + Error::ExpressionUnexpectedType { ty: ty.clone() } + .with_span(from.into()), + ); + } + 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)?; + let (ty_l, ty_r) = if ty.is_error() { + (ResolvedType::error(), ResolvedType::error()) + } else { + let (ty_l, ty_r) = ty + .as_either() + .ok_or(Error::ExpressionUnexpectedType { ty: ty.clone() }) + .with_span(from)?; + + (ty_l.clone(), ty_r.clone()) + }; + match either { - Either::Left(parse_l) => Expression::analyze(parse_l, ty_l, scope) + 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) + Either::Right(parse_r) => Expression::analyze(parse_r, &ty_r, scope) .map(Arc::new) .map(Either::Right), } .map(SingleExpressionInner::Either)? } parse::SingleExpressionInner::Option(maybe_parse) => { - let ty = ty - .as_option() - .ok_or(Error::ExpressionUnexpectedType { ty: ty.clone() }) - .with_span(from)?; + let inner_ty: ResolvedType = if ty.is_error() { + ResolvedType::error() + } else { + ty.as_option() + .ok_or(Error::ExpressionUnexpectedType { ty: ty.clone() }) + .with_span(from)? + .clone() + }; + match maybe_parse { Some(parse) => { - Some(Expression::analyze(parse, ty, scope).map(Arc::new)).transpose() + Some(Expression::analyze(parse, &inner_ty, scope).map(Arc::new)).transpose() } None => Ok(None), } @@ -1828,12 +2194,17 @@ 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() { + 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)? } + parse::SingleExpressionInner::Error => SingleExpressionInner::Error, }; Ok(Self { @@ -1844,7 +2215,31 @@ impl AbstractSyntaxTree for SingleExpression { } } -impl AbstractSyntaxTree for EnumMatch { +/// 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); + } +} + +impl AbstractSyntaxTree for EnumMatch { type From = parse::EnumMatch; fn analyze( @@ -1858,6 +2253,7 @@ impl AbstractSyntaxTree for EnumMatch { 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 \ @@ -1867,17 +2263,24 @@ impl AbstractSyntaxTree for EnumMatch { .with_span(span); }; let alias = AliasName::from_str_unchecked(single.as_inner()); - let enum_ty = scope.get_alias(&alias).with_span(span)?; + 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); + } + }; 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}` is not an enum, so match arms of the form \ `{enum_name}::Variant` cannot apply to it" ), }) - .with_span(span) + .with_span(span); } }; @@ -1887,6 +2290,7 @@ impl AbstractSyntaxTree for EnumMatch { vec![None; info.variants().len()]; for arm in arms { if arm.enum_path() != arms[0].enum_path() { + drain_enum_match(from, ty, scope); return Err(Error::Grammar { msg: format!( "all match arms must use the same enum; expected '{}', found '{}'", @@ -1896,18 +2300,20 @@ impl AbstractSyntaxTree for EnumMatch { }) .with_span(span); } - let (index, _) = info - .variant(arm.variant()) - .ok_or_else(|| Error::Grammar { + let Some((index, _)) = info.variant(arm.variant()) else { + drain_enum_match(from, ty, scope); + return Err(Error::Grammar { msg: format!( "variant '{}' is not defined in enum '{}'", arm.variant(), enum_name ), }) - .with_span(span)?; + .with_span(span); + }; let slot = &mut arms_by_index[index]; if slot.is_some() { + drain_enum_match(from, ty, scope); return Err(Error::Grammar { msg: format!("duplicate arm for variant '{}'", arm.variant()), }) @@ -1925,6 +2331,7 @@ impl AbstractSyntaxTree for EnumMatch { .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: {}", @@ -1939,27 +2346,33 @@ impl AbstractSyntaxTree for EnumMatch { // 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)?; + // 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 pattern = analyze_enum_arm_bindings(arm, variant, scope, span)?; - scope.enter_block(); + // A failed binding poisons only this arm; later arms still run. + let pattern = analyze_enum_arm_bindings(arm, variant, scope, span); + + let guard = scope.block(); let payload_ty = variant.payload_type(); - let typed_variables = pattern.is_of_type(payload_ty).with_span(span)?; + let typed_variables = match pattern.is_of_type(payload_ty) { + Ok(variant) => variant, + Err(err) => { + guard.scope.report(err.with_span(span)); + poison_bindings(&pattern) + } + }; for (identifier, variable_ty) in typed_variables { - scope.insert_variable(identifier, variable_ty); + guard.scope.insert_variable(identifier, variable_ty); } - let body = Expression::analyze(arm.expression(), ty, scope).map(Arc::new); - scope.exit_block(); - Ok(EnumMatchArm { - pattern, - body: body?, - }) + let body = Arc::new(analyze_child(arm.expression(), ty, guard.scope)); + + EnumMatchArm { pattern, body } }) - .collect::, Diagnostic>>()?; + .collect::>(); Ok(Self { scrutinee, @@ -1975,45 +2388,56 @@ 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. fn analyze_enum_arm_bindings( arm: &parse::EnumMatchArm, variant: &EnumVariantInfo, - scope: &Scope, + scope: &mut Scope, span: Span, -) -> Result { +) -> Pattern { 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); + 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()); } - let pattern = match patterns.len() { + match patterns.len() { 0 => Pattern::Ignore, 1 => patterns[0].clone(), _ => Pattern::tuple(patterns), - }; - Ok(pattern) + } } impl AbstractSyntaxTree for Call { @@ -2042,7 +2466,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 { @@ -2052,20 +2476,29 @@ 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 name = CallName::analyze(from, ty, scope)?; + 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); + } + }; + let args = match name.clone() { CallName::Jet(jet) => { let args_tys = source_type(&*jet) @@ -2074,64 +2507,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 } @@ -2140,19 +2585,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 @@ -2161,10 +2613,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: @@ -2180,11 +2633,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: @@ -2200,11 +2654,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: @@ -2225,11 +2680,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) } }; @@ -2340,31 +2796,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)?; - let typed_variables = pat_l.is_of_type(&ty_l).with_span(from)?; + 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, span))); + let right = from + .right() + .pattern() + .as_typed_pattern() + .map(|(pat, aliased)| (pat, scope.resolve_or_poison(aliased, 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(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)?; - let typed_variables = pat_r.is_of_type(&ty_r).with_span(from)?; + + 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(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, @@ -2412,22 +2910,36 @@ impl AsRef for Match { } #[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); + } + + if Program::analyze( + &driver_program, + Box::new(ElementsJetHinter), + &mut diagnostics, + ) + .is_none() + { + return Err(diagnostics); } + Ok(()) +} + +#[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![ @@ -2513,7 +3025,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")); } @@ -2537,7 +3051,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")); } @@ -2548,7 +3064,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")); } @@ -2563,7 +3081,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"))); } @@ -2590,7 +3110,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")); } @@ -2619,7 +3141,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")); } @@ -2634,7 +3158,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")); } @@ -2648,7 +3174,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")); } @@ -2666,7 +3194,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! @@ -2684,7 +3214,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`. @@ -2702,8 +3234,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")); } @@ -2717,15 +3250,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() { @@ -2761,7 +3295,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")); } @@ -2788,7 +3324,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")); } @@ -2873,7 +3411,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")); } @@ -2891,7 +3431,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")); } @@ -2931,14 +3473,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(), @@ -2960,13 +3502,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(), @@ -2985,8 +3527,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"); } @@ -2996,9 +3538,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!( @@ -3009,21 +3551,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}" @@ -3034,15 +3579,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(), @@ -3054,12 +3599,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(), @@ -3071,12 +3616,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!( @@ -3090,11 +3635,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(), @@ -3106,11 +3651,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"); } @@ -3119,13 +3664,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")); @@ -3135,12 +3680,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")); @@ -3150,13 +3695,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")); @@ -3166,12 +3711,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")); @@ -3181,14 +3726,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(), @@ -3200,13 +3745,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(), @@ -3220,14 +3765,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(), @@ -3239,12 +3784,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!( @@ -3260,11 +3805,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(), @@ -3276,10 +3821,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(), @@ -3288,10 +3833,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"); } @@ -3302,10 +3847,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(), @@ -3317,10 +3862,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(), @@ -3346,14 +3891,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(), @@ -3368,10 +3913,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(), @@ -3401,16 +3946,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(), @@ -3419,16 +3964,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(), @@ -3446,3 +3991,594 @@ mod enum_tests { assert!(result.is_err(), "enum syntax is gated behind -Z enums"); } } + +#[cfg(test)] +pub(crate) mod multi_error_tests { + use super::*; + + use crate::error::DiagnosticManager; + use crate::parse::{self, ParseFromStr}; + use crate::types::{ResolvedType, TypeConstructible}; + + /// All diagnostics for a single-file program (empty if it compiled). + pub(crate) fn errors(src: &str) -> DiagnosticManager { + match analyze_multifile(vec![("main.simf", src)]) { + Ok(()) => DiagnosticManager::new(), + Err(diags) => diags, + } + } + + /// 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::*; + use crate::ast::multi_error_tests::errors; + // Known collection gaps. + // + // Every test below states the behaviour the strategy in + // `doc/error-handling-strategy.md` prescribes, and every one of them + // fails today: an early return happens *before* something that must + // still run, so a diagnostic is lost or a spurious one is invented. + // They are `#[ignore]`d so the suite stays green; run them with + // `cargo test --lib -- --ignored` to see the outstanding set, and drop + // the attribute as each is fixed. + // + // 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()); + } +} 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 862195e1..8fb16f30 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -21,7 +21,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 a627adeb..02690315 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -305,12 +305,15 @@ 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_require_feature!(Statement { variants: Assignment(assignment), Expression(expr), + Error(_), }); /// The output of an expression is assigned to a pattern. @@ -617,10 +620,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, @@ -711,6 +714,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 { @@ -732,6 +737,7 @@ impl_require_feature!(SingleExpressionInner { Tuple(exprs), Array(exprs), List(exprs), + Error, }); /// Match expression. @@ -1226,6 +1232,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() { @@ -1236,7 +1243,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)) @@ -1335,7 +1343,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, "(")?; @@ -1366,6 +1373,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 { @@ -1521,18 +1533,38 @@ impl_parse_wrapped_string!(AliasName, "alias name"); impl_parse_wrapped_string!(ModuleName, "module name"); /// 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, @@ -1540,6 +1572,39 @@ pub trait ParseFromStrWithErrors: Sized { ) -> Option; } +/// 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 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, diags); + }; + + 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); + + (ast, diags) +} + /// Trait for generating parsers of themselves. /// /// Replacement for previous `PestParse` trait. @@ -1554,50 +1619,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(), crate::error::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, @@ -1605,10 +1662,9 @@ impl ParseF ) -> Option { let before = diagnostics.error_count(); - // 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. + // 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)) => { @@ -1617,34 +1673,26 @@ impl ParseF } }; - let (tokens, mut lex_errs) = crate::lexer::lex(file_id, content, start); - // A stray `simc` makes every other diagnostic noise — its `"";` remnant - // does not lex — so the reserved-keyword errors are reported alone. - if lex_errs + let (ast, mut diags) = lex_and_parse::(file_id, content, start); + + // 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(|e| matches!(e.error(), Error::ReservedSimcKeyword)) + .any(|diag| matches!(diag.error(), Error::ReservedSimcKeyword)) { - lex_errs.retain(|e| matches!(e.error(), Error::ReservedSimcKeyword)); - diagnostics.extend(lex_errs); + diags.retain(|diag| matches!(diag.error(), Error::ReservedSimcKeyword)); + diagnostics.extend(diags); return None; } - let lex_ok = lex_errs.is_empty(); - diagnostics.extend(lex_errs); - let tokens = tokens?; - - 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(); - let parse_ok = parse_errs.is_empty(); - diagnostics.extend(parse_errs); - - if let (Some(ast), true) = (&ast, lex_ok && parse_ok) { + + let parse_clean = diags.is_empty(); + diagnostics.extend(diags); + + 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 { @@ -1732,11 +1780,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| { @@ -1746,12 +1795,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")) @@ -1764,7 +1808,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"); @@ -1785,26 +1829,35 @@ impl ChumskyParse for AliasedType { ty.clone() .then_ignore(parse_token_with_recovery(Token::Semi)) .then(num.clone()) - .map(|(ty, size)| { - AliasedType::array(ty, usize::from_str(size.as_inner()).unwrap_or_default()) + .validate(|(ty, size), e, emit| match size { + None => AliasedType::error(), + Some(size) => match usize::from_str(size.as_inner()) { + 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 { - match NonZeroPow2Usize::from_str(num.as_inner()) { - Ok(number) => number, + .then(num.clone()) + .validate(|(ty, bound), e, emit| match bound { + None => AliasedType::error(), + Some(size) => match NonZeroPow2Usize::from_str(size.as_inner()) { + Ok(b) => AliasedType::list(ty, b), Err(err) => { emit.emit( Error::Grammar { @@ -1812,21 +1865,14 @@ impl ChumskyParse for AliasedType { } .with_span(e.span()), ); - // fallback to default value - NonZeroPow2Usize::TWO + 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)) @@ -1939,7 +1985,7 @@ impl ChumskyParse for Function { (Token::LParen, Token::RParen), (Token::LBracket, Token::RBracket), ], - Expression::empty, + Expression::error, ))) .labelled("function body"); @@ -2404,7 +2450,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 @@ -2583,12 +2629,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)) }; @@ -2713,21 +2754,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, - }; - 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 { @@ -2756,7 +2782,7 @@ fn assemble_match_arms( msg: "match expression has no arms".to_string(), } .with_span(span), - placeholder_match(scrutinee, span), + SingleExpressionInner::Error, ))); } @@ -2792,7 +2818,7 @@ fn assemble_match_arms( ), } .with_span(span), - placeholder_match(scrutinee, span), + SingleExpressionInner::Error, ))); } @@ -2803,7 +2829,7 @@ fn assemble_match_arms( msg: "binary match requires exactly 2 arms".to_string(), } .with_span(span), - placeholder_match(scrutinee, span), + SingleExpressionInner::Error, ))); }; @@ -3358,14 +3384,14 @@ mod test { 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 ':'")); } @@ -3374,14 +3400,14 @@ mod test { 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() @@ -3390,16 +3416,15 @@ mod test { ); } - /// 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] @@ -3415,14 +3440,13 @@ mod test { // 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()); } @@ -3434,13 +3458,12 @@ mod test { // 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(), 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 fefa298a..3832373e 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 49ebbc5a..0a4afe4d 100644 --- a/src/value.rs +++ b/src/value.rs @@ -655,10 +655,12 @@ impl Value { TypeInner::Array(inner, len) if inner.as_integer() == Some(UIntType::U8) => *len, _ => return Err(Error::ExpressionUnexpectedType { ty: ty.clone() }), }; + let s = hexadecimal.as_inner(); 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(..) => { @@ -667,6 +669,7 @@ impl Value { TypeInner::Array(..) => Self::byte_array(bytes), _ => unreachable!(), }; + Ok(ret) } } @@ -707,7 +710,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); @@ -857,6 +861,7 @@ impl Value { } } } + TypeInner::Error => unreachable!("poisoned type in value reconstruction; values exist only for error-free programs"), } } debug_assert_eq!(output.len(), 1); @@ -867,8 +872,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) } } @@ -930,6 +936,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"), } } } @@ -1250,7 +1257,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())) @@ -1642,4 +1649,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]