From b0af213b3d47580a319138383b7b4ef9a32e5aac Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Tue, 6 Oct 2026 15:44:01 +0300 Subject: [PATCH 1/2] feat: add TypeFlags struct to ResolvedType --- src/parse.rs | 3 +- src/types/resolved.rs | 109 ++++++++++++++++++++++++++++++++++-------- src/version.rs | 13 ++--- 3 files changed, 98 insertions(+), 27 deletions(-) diff --git a/src/parse.rs b/src/parse.rs index a7d80342..6c14cc9b 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1716,7 +1716,8 @@ mod pipeline { ) -> Option { match SimcDirective::prescan(content, file_id) { Ok(start) => Some(start), - Err((err, span)) => { + Err(box_error) => { + let (err, span) = *box_error; diagnostics.push(Diagnostic::new(err, span)); None } diff --git a/src/types/resolved.rs b/src/types/resolved.rs index a8b9bcab..50323a67 100644 --- a/src/types/resolved.rs +++ b/src/types/resolved.rs @@ -10,12 +10,62 @@ use crate::num::NonZeroPow2Usize; /// SimplicityHL type without type aliases. #[derive(PartialEq, Eq, Hash, Clone)] -pub struct ResolvedType(TypeInner>); +pub struct ResolvedType { + inner: TypeInner>, + flags: TypeFlags, +} + +/// Facts about a type, computed once from its parts when the type is built. +/// +/// We need these cached flags because searching a [`TypeInner::Never`], including inside +/// enum payloads, can take exponential time on types built from shared aliases. +#[derive(PartialEq, Eq, Hash, Clone, Copy, Default)] +struct TypeFlags { + has_enum: bool, + has_never: bool, + has_never_in_enum: bool, +} + +impl TypeFlags { + const fn union(self, other: Self) -> Self { + Self { + has_enum: self.has_enum || other.has_enum, + has_never: self.has_never || other.has_never, + has_never_in_enum: self.has_never_in_enum || other.has_never_in_enum, + } + } +} impl ResolvedType { + fn new(inner: TypeInner>) -> Self { + let flags = match &inner { + TypeInner::Boolean | TypeInner::UInt(_) => TypeFlags::default(), + TypeInner::Never => Self::never().flags, + TypeInner::Enum(info) => TypeFlags { + has_enum: true, + has_never: false, + has_never_in_enum: info + .variants() + .iter() + .any(|variant| !variant.payload_type().has_structural_type()), + }, + TypeInner::Option(inner) | TypeInner::Array(inner, _) | TypeInner::List(inner, _) => { + inner.flags + } + TypeInner::Either(left, right) => left.flags.union(right.flags), + TypeInner::Tuple(elements) => elements + .iter() + .fold(TypeFlags::default(), |flags, element| { + flags.union(element.flags) + }), + }; + + Self { inner, flags } + } + /// Access the inner type primitive. pub fn as_inner(&self) -> &TypeInner> { - &self.0 + &self.inner } } @@ -31,22 +81,21 @@ impl ResolvedType { /// (which owns the uniqueness of declaration ids) can mint enum types. impl ResolvedType { /// Create a nominal enum type from the given definition. - pub const fn enumeration(info: EnumInfo) -> Self { - Self(TypeInner::Enum(info)) + pub fn enumeration(info: EnumInfo) -> Self { + Self::new(TypeInner::Enum(info)) } /// Access the enum definition if this is an enum type. pub const fn as_enum(&self) -> Option<&EnumInfo> { - match &self.0 { + match &self.inner { TypeInner::Enum(info) => Some(info), _ => None, } } /// Check whether the type mentions an enum, at any nesting depth. - pub fn contains_enum(&self) -> bool { - self.post_order_iter() - .any(|data| data.node.as_enum().is_some()) + pub const fn contains_enum(&self) -> bool { + self.flags.has_enum } } @@ -54,40 +103,60 @@ impl ResolvedType { impl ResolvedType { /// Create the uninhabited type. pub const fn never() -> Self { - Self(TypeInner::Never) + Self { + inner: TypeInner::Never, + flags: TypeFlags { + has_enum: false, + has_never: true, + has_never_in_enum: false, + }, + } } /// Check whether this is the uninhabited type. pub const fn is_never(&self) -> bool { - matches!(self.0, TypeInner::Never) + matches!(self.inner, TypeInner::Never) + } + + /// Check whether the type mentions the uninhabited type, except inside enum payloads, + /// because an enum is identified by its name. + pub const fn contains_never(&self) -> bool { + self.flags.has_never + } + + /// Check whether the type can be lowered to a structural type. + /// + /// Unlike [`Self::contains_never`], this looks inside enum payloads. + pub const fn has_structural_type(&self) -> bool { + !self.flags.has_never && !self.flags.has_never_in_enum } } impl TypeConstructible for ResolvedType { fn either(left: Self, right: Self) -> Self { - Self(TypeInner::Either(Arc::new(left), Arc::new(right))) + Self::new(TypeInner::Either(Arc::new(left), Arc::new(right))) } fn option(inner: Self) -> Self { - Self(TypeInner::Option(Arc::new(inner))) + Self::new(TypeInner::Option(Arc::new(inner))) } fn boolean() -> Self { - Self(TypeInner::Boolean) + Self::new(TypeInner::Boolean) } fn tuple>(elements: I) -> Self { - Self(TypeInner::Tuple( + Self::new(TypeInner::Tuple( elements.into_iter().map(Arc::new).collect(), )) } fn array(element: Self, size: usize) -> Self { - Self(TypeInner::Array(Arc::new(element), size)) + Self::new(TypeInner::Array(Arc::new(element), size)) } fn list(element: Self, bound: NonZeroPow2Usize) -> Self { - Self(TypeInner::List(Arc::new(element), bound)) + Self::new(TypeInner::List(Arc::new(element), bound)) } } @@ -141,7 +210,7 @@ impl TypeDeconstructible for ResolvedType { impl TreeLike for &ResolvedType { fn as_node(&self) -> Tree { - match &self.0 { + match &self.inner { TypeInner::Boolean | TypeInner::UInt(..) | TypeInner::Enum(..) | TypeInner::Never => { Tree::Nullary } @@ -161,7 +230,7 @@ impl fmt::Debug for ResolvedType { impl fmt::Display for ResolvedType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for data in self.verbose_pre_order_iter() { - data.node.0.display(f, data.n_children_yielded)?; + data.node.inner.display(f, data.n_children_yielded)?; } Ok(()) } @@ -169,7 +238,7 @@ impl fmt::Display for ResolvedType { impl From for ResolvedType { fn from(value: UIntType) -> Self { - Self(TypeInner::UInt(value)) + Self::new(TypeInner::UInt(value)) } } @@ -230,7 +299,7 @@ impl From<&ResolvedType> for StructuralType { fn from(value: &ResolvedType) -> Self { let mut output = vec![]; for data in value.post_order_iter() { - match &data.node.0 { + match &data.node.inner { TypeInner::Either(_, _) => { let right = output.pop().unwrap(); let left = output.pop().unwrap(); diff --git a/src/version.rs b/src/version.rs index 7dd7bdf6..9e552058 100644 --- a/src/version.rs +++ b/src/version.rs @@ -58,12 +58,13 @@ impl SimcDirective { /// special cases, `simc` stays plainly reserved — without copying or modifying /// the source. On error the caller should not lex: any further diagnostic is /// noise. - pub(crate) fn prescan(content: &str, file_id: usize) -> Result { + pub(crate) fn prescan(content: &str, file_id: usize) -> Result> { match Self::scan(content) { DirectiveScan::Absent => Ok(0), - DirectiveScan::Malformed { span } => { - Err((Error::MalformedSimcDirective, Span::new(file_id, span))) - } + DirectiveScan::Malformed { span } => Err(Box::new(( + Error::MalformedSimcDirective, + Span::new(file_id, span), + ))), DirectiveScan::Found { range, span } => { Self::validate(range, Span::new(file_id, span.clone()))?; Ok(span.end) @@ -135,7 +136,7 @@ impl SimcDirective { } /// Validate a directive's version-requirement string against the running compiler. - fn validate(required: &str, span: Span) -> Result<(), (Error, Span)> { + fn validate(required: &str, span: Span) -> Result<(), Box<(Error, Span)>> { let required = required.trim(); let req = VersionRequirement::parse(required) .map_err(|e| (Error::InvalidSimcVersionSyntax { err: e }, span))?; @@ -145,7 +146,7 @@ impl SimcDirective { required: required.to_string(), current: Self::current_version().to_string(), }; - return Err((err, span)); + return Err(Box::new((err, span))); } Ok(()) } From 81da4323fbe4221d381878409c6fb9174fc56ce1 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Tue, 29 Sep 2026 17:18:40 +0300 Subject: [PATCH 2/2] feat: keep analyzing items after an undefined type --- src/ast.rs | 428 +++++++++++++++++++++++++++++++++++------- src/pattern.rs | 3 + src/types/resolved.rs | 65 ++++++- 3 files changed, 427 insertions(+), 69 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index ff27ba44..88c38ad2 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; @@ -18,7 +19,7 @@ use crate::pattern::Pattern; use crate::str::{AliasName, FunctionName, Identifier, ModuleName, SymbolName}; use crate::types::{ AliasedType, EnumInfo, EnumVariantInfo, ResolvedType, StructuralType, TypeConstructible, - TypeDeconstructible, TypeInner, UIntType, + TypeDeconstructible, UIntType, }; use crate::value::{UIntValue, Value}; use crate::witness::{Parameters, WitnessTypes}; @@ -867,7 +868,11 @@ impl Scope { /// A failure that was already reported is not recorded again. fn report(&mut self, failure: impl Into) { if let Failure::New(diagnostic) = failure.into() { - self.diagnostics.push(diagnostic); + // Like rustc, an error about a type with `!` is left until the error + // that created `!` is fixed. + if !mentions_never(diagnostic.error()) { + self.diagnostics.push(diagnostic); + } } } @@ -1327,6 +1332,28 @@ impl Scope { ty.resolve(|name| self.get_alias(name)) } + /// Resolve a type, reporting every undefined alias and replacing it with `!`. + /// + /// This is the only place where analysis creates `!`, so every `!` has a reported error. + fn resolve_or_never(&mut self, ty: &AliasedType, span: impl Into) -> ResolvedType { + let mut errors = Vec::new(); + let resolved = ty + .resolve(|name| { + Ok::<_, Infallible>(self.get_alias(name).unwrap_or_else(|error| { + errors.push(error); + ResolvedType::never() + })) + }) + .unwrap_or_else(|never| match never {}); + + let span = span.into(); + for error in errors { + self.report(error.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) { @@ -1344,7 +1371,7 @@ impl Scope { pub fn insert_alias(&mut self, alias: parse::TypeAlias) -> Result<(), Error> { self.check_alias_free(alias.name())?; - let resolved = self.resolve(alias.ty())?; + let resolved = self.resolve_or_never(alias.ty(), &alias); self.current_module_mut() .aliases @@ -1393,8 +1420,14 @@ impl Scope { name: TemplateProgramWitness, ty: ResolvedType, ) -> Result<(), Error> { + // A use with a broken type says nothing reliable about the parameter, + // so the stored type never contains `!` and never changes once stored. + if ty.contains_never() { + return Ok(()); + } + match self.parameters.entry(name.clone()) { - Entry::Occupied(entry) if entry.get() == &ty => Ok(()), + Entry::Occupied(entry) if entry.get().same_as(&ty) => Ok(()), Entry::Occupied(entry) => Err(Error::ExpressionTypeMismatch { expected: entry.get().clone(), found: ty, @@ -1506,6 +1539,20 @@ impl From for Failure { } } +/// Check whether an error mentions a type that contains `!`. +fn mentions_never(error: &Error) -> bool { + match error { + Error::ExpressionUnexpectedType { ty } | Error::RawHashUnsupportedType { ty } => { + ty.contains_never() + } + Error::ExpressionTypeMismatch { expected, found } => { + expected.contains_never() || found.contains_never() + } + Error::InvalidCast { source, target } => source.contains_never() || target.contains_never(), + _ => false, + } +} + /// Part of the abstract syntax tree that can be generated from a precursor in the parse tree. trait AbstractSyntaxTree: Sized { /// Component of the parse tree. @@ -1654,14 +1701,18 @@ impl AbstractSyntaxTree for Item { .map_err(Failure::from) })?; - let variants = scope - .analyze_all(decl.variants(), |v, scope| { - let payload = scope.analyze_all(v.payload(), |ty, scope| { - Ok(scope.resolve(ty).with_span(v)?) - })?; - Ok(EnumVariantInfo::new(v.name().clone(), Arc::from(payload))) + let variants = decl + .variants() + .iter() + .map(|v| { + let payload = v + .payload() + .iter() + .map(|ty| scope.resolve_or_never(ty, v)) + .collect::>(); + EnumVariantInfo::new(v.name().clone(), payload) }) - .map(Arc::from)?; + .collect::>(); scope .insert_enum(decl.name().clone(), decl.visibility().clone(), variants) .with_span(decl)?; @@ -1691,26 +1742,23 @@ impl AbstractSyntaxTree for Function { ); if from.name() != MAIN_STR { + // A broken type in the signature becomes `!`, so the function is still + // registered and its calls do not report it as undefined. let params = from .params() .iter() - .map(|param| { - let identifier = param.identifier().clone(); - let ty = scope.resolve(param.ty())?; - Ok(FunctionParam { - identifier, - ty, - span: *param.span(), - }) + .map(|param| FunctionParam { + identifier: param.identifier().clone(), + ty: scope.resolve_or_never(param.ty(), from), + span: *param.span(), }) - .collect::, Error>>() - .with_span(from)?; + .collect::>(); let ret = from .ret() .as_ref() - .map(|aliased| scope.resolve(aliased).with_span(from)) - .transpose()? - .unwrap_or_else(ResolvedType::unit); + .map_or_else(ResolvedType::unit, |aliased| { + scope.resolve_or_never(aliased, from) + }); let body = scope.in_function(|scope| { for param in params.iter() { @@ -1744,8 +1792,8 @@ impl AbstractSyntaxTree for Function { } if let Some(aliased) = from.ret() { - let resolved = scope.resolve(aliased).with_span(from)?; - if !resolved.is_unit() { + let resolved = scope.resolve_or_never(aliased, from); + if !resolved.compatible(&ResolvedType::unit()) { return Err(Error::MainNoOutput).with_span(from)?; } } @@ -1805,6 +1853,14 @@ impl Expression { /// The returned expression might not be evaluable at compile time. /// The details depend on the current state of the SimplicityHL compiler. pub fn analyze_const(from: &parse::Expression, ty: &ResolvedType) -> Result { + // Without this check, a `!` from the caller could make the `expect` below panic, + // or make analysis drop errors and accept a wrong value. + if !ty.has_structural_type() { + return Err( + Error::ExpressionUnexpectedType { ty: ty.clone() }.with_span(*from.as_ref()) + ); + } + // Value files carry no scope, so enum constructions may name the // enum by its declared name here — and only here. let mut empty_scope = Scope::for_value_parsing(); @@ -1814,6 +1870,8 @@ impl Expression { // Value parsing has no diagnostic manager: return the first error analysis found. match empty_scope.diagnostics.into_iter().next() { Some(error) => Err(error), + // This cannot panic: a failed analysis always leaves an error, + // because types with `!` were rejected above. None => Ok(expression.expect("every failure is reported to the scope")), } } @@ -1844,8 +1902,16 @@ fn analyze_enum_construction( let names_expected_enum = match construction.enum_path() { [single] => { let alias = AliasName::from_ident(single); + match scope.get_alias(&alias) { - Ok(resolved) if &resolved == ty => true, + // The written enum is broken, so its variants and payloads are unknown. + Ok(resolved) if resolved.is_never() => { + let _ = scope.analyze_all(construction.args(), |arg, scope| { + Expression::analyze(arg, &resolved, scope) + }); + return Err(Failure::Reported); + } + Ok(resolved) if resolved.compatible(ty) => true, Ok(resolved) => Err(Error::ExpressionTypeMismatch { expected: ty.clone(), found: resolved, @@ -1914,29 +1980,13 @@ fn analyze_enum_construction( /// conservative either way, since their partition layout complicates /// position alignment. fn cast_preserves_enum_identity(source: &ResolvedType, target: &ResolvedType) -> bool { - match (source.as_inner(), target.as_inner()) { - (TypeInner::Enum(src), TypeInner::Enum(dst)) => src == dst, - (TypeInner::Enum(_), _) | (_, TypeInner::Enum(_)) => false, - (TypeInner::Option(src), TypeInner::Option(dst)) => cast_preserves_enum_identity(src, dst), - (TypeInner::Either(src_l, src_r), TypeInner::Either(dst_l, dst_r)) => { - cast_preserves_enum_identity(src_l, dst_l) && cast_preserves_enum_identity(src_r, dst_r) - } - (TypeInner::Tuple(src), TypeInner::Tuple(dst)) if src.len() == dst.len() => src - .iter() - .zip(dst.iter()) - .all(|(src_el, dst_el)| cast_preserves_enum_identity(src_el, dst_el)), - (TypeInner::Array(src, src_len), TypeInner::Array(dst, dst_len)) if src_len == dst_len => { - cast_preserves_enum_identity(src, dst) - } - (TypeInner::List(src, src_bound), TypeInner::List(dst, dst_bound)) - if src_bound == dst_bound => - { - cast_preserves_enum_identity(src, dst) - } + source.matches_with(target, |src, dst| match (src.as_enum(), dst.as_enum()) { + (Some(src), Some(dst)) => src == dst, // Differently shaped subtrees may convert freely as long as no // enum is involved on either side. - _ => !source.contains_enum() && !target.contains_enum(), - } + (None, None) => !src.contains_enum() && !dst.contains_enum(), + _ => false, + }) } /// The given string does not name a variant of the enum. @@ -1979,7 +2029,7 @@ impl AbstractSyntaxTree for Expression { Some(expression) => Expression::analyze(expression, ty, scope) .map(Arc::new) .map(Some), - None if ty.is_unit() => Ok(None), + None if ty.compatible(&ResolvedType::unit()) => Ok(None), None => Err(Error::ExpressionTypeMismatch { expected: ty.clone(), found: ResolvedType::unit(), @@ -2000,10 +2050,38 @@ impl AbstractSyntaxTree for Expression { } } +/// The sub-expressions of an expression that builds a value, or `None` for any other expression. +fn value_parts(from: &parse::SingleExpression) -> Option> { + use parse::SingleExpressionInner as Inner; + + match from.inner() { + Inner::Boolean(_) | Inner::Decimal(_) | Inner::Binary(_) | Inner::Hexadecimal(_) => { + Some(Vec::new()) + } + Inner::Tuple(elements) | Inner::Array(elements) | Inner::List(elements) => { + Some(elements.iter().collect()) + } + Inner::Either(Either::Left(inner) | Either::Right(inner)) => Some(vec![inner.as_ref()]), + Inner::Option(inner) => Some(inner.iter().map(Arc::as_ref).collect()), + Inner::EnumConstruction(construction) => Some(construction.args().iter().collect()), + _ => None, + } +} + impl AbstractSyntaxTree for SingleExpression { type From = parse::SingleExpression; fn analyze(from: &Self::From, ty: &ResolvedType, scope: &mut Scope) -> Result { + if ty.is_never() { + if let Some(parts) = value_parts(from) { + // No value has type `!`, so the value cannot be built, but its parts + // can still have errors of their own. + let _ = + scope.analyze_all(parts, |part, scope| Expression::analyze(part, ty, scope)); + return Err(Failure::Reported); + } + } + let inner = match from.inner() { parse::SingleExpressionInner::Boolean(bit) => { if !ty.is_boolean() { @@ -2056,14 +2134,16 @@ impl AbstractSyntaxTree for SingleExpression { identifier: identifier.clone(), }) .with_span(from)?; - if ty != bound_ty { + + if !ty.compatible(bound_ty) { Err(Error::ExpressionTypeMismatch { expected: ty.clone(), found: bound_ty.clone(), }) .with_span(from)?; } - scope.insert_variable(identifier.clone(), ty.clone()); + scope.insert_variable(identifier.clone(), bound_ty.clone()); + SingleExpressionInner::Variable(identifier.clone()) } parse::SingleExpressionInner::Expression(parse) => { @@ -2203,6 +2283,7 @@ impl AbstractSyntaxTree for EnumMatch { let enum_ty = scope.get_alias(&alias).with_span(span)?; let info = match enum_ty.as_enum() { Some(info) => info.clone(), + None if enum_ty.is_never() => return Err(Failure::Reported), None => Err(Error::Grammar { msg: format!( "`{enum_name}` is not an enum, so match arms of the form \ @@ -2337,13 +2418,15 @@ fn analyze_enum_arm_bindings( 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 { + + if !declared.compatible(payload_ty) { return Err(Error::ExpressionTypeMismatch { expected: payload_ty.clone(), found: declared, }) .with_span(span); } + patterns.push(pattern.clone()); } @@ -2377,7 +2460,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 { @@ -2480,8 +2563,12 @@ 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) + if !source.contains_never() + && !ty.contains_never() + && (!cast_preserves_enum_identity(&source, ty) + || (source.has_structural_type() + && ty.has_structural_type() + && StructuralType::from(&source) != StructuralType::from(ty))) { scope.report( Error::InvalidCast { @@ -2601,8 +2688,8 @@ impl AbstractSyntaxTree for Call { impl CallName { // Take parse::Call, so we have access to the span for pretty errors - fn analyze(from: &parse::Call, scope: &mut Scope) -> Result { - match from.name() { + fn analyze(from: &parse::Call, scope: &mut Scope) -> Result { + let name = match from.name() { parse::CallName::Jet(name) => match scope.jet_hinter.parse_jet(name.as_inner()) { Some(jet) if !jet.is_disabled() => Ok(Self::Jet(jet)), _ => Err(Error::JetDoesNotExist { name: name.clone() }).with_span(from), @@ -2659,7 +2746,9 @@ impl CallName { let function = scope.get_function(name).with_span(from)?; // A function that is used in a array fold has the signature: // fn f(element: E, accumulator: A) -> A - if function.params().len() != 2 || function.params()[1].ty() != function.ret() { + if function.params().len() != 2 + || !function.params()[1].ty().compatible(function.ret()) + { Err(Error::FunctionNotFoldable { name: name.clone() }).with_span(from) } else { Ok(Self::ArrayFold(function, *size)) @@ -2669,7 +2758,9 @@ impl CallName { let function = scope.get_function(name).with_span(from)?; // A function that is used in a list fold has the signature: // fn f(element: E, accumulator: A) -> A - if function.params().len() != 2 || function.params()[1].ty() != function.ret() { + if function.params().len() != 2 + || !function.params()[1].ty().compatible(function.ret()) + { Err(Error::FunctionNotFoldable { name: name.clone() }).with_span(from) } else { Ok(Self::Fold(function, *bound)) @@ -2682,19 +2773,25 @@ impl CallName { // where // N is a power of two if function.params().len() != 3 { - return Err(Error::FunctionNotLoopable { name: name.clone() }).with_span(from); + return Err(Error::FunctionNotLoopable { name: name.clone() }) + .with_span(from)?; } - match function.ret().as_either() { - Some((_, out_r)) if out_r == function.params().first().unwrap().ty() => {} - _ => { - return Err(Error::FunctionNotLoopable { name: name.clone() }) - .with_span(from); - } + let accumulator_ty = function.params().first().unwrap().ty(); + let returns_accumulator = function.ret().is_never() + || matches!( + function.ret().as_either(), + Some((_, out_r)) if out_r.compatible(accumulator_ty) + ); + if !returns_accumulator { + return Err(Error::FunctionNotLoopable { name: name.clone() }) + .with_span(from)?; } + // Disable loops for u32 or higher since no one will want to run // 2^32 = 4294967296 ≈ 4 billion iterations. // The resulting Simplicity program will not fit into a Bitcoin block. - match function.params().get(2).unwrap().ty().as_integer() { + let counter_ty = function.params().get(2).unwrap().ty(); + match counter_ty.as_integer() { Some( int_ty @ (UIntType::U1 | UIntType::U2 @@ -2702,10 +2799,13 @@ impl CallName { | UIntType::U8 | UIntType::U16), ) => Ok(Self::ForWhile(function, int_ty.bit_width())), + // The loop needs the width of the counter, which is unknown. + None if counter_ty.is_never() => return Err(Failure::Reported), _ => Err(Error::FunctionNotLoopable { name: name.clone() }).with_span(from), } } - } + }; + name.map_err(Failure::from) } } @@ -3171,6 +3271,198 @@ mod multi_error_tests { ], ); } + + #[test] + fn undefined_types_are_reported_once() { + assert_errors( + "type Alias = Missing; + fn f(a: Alias) -> Alias { a } + fn g(a: Undefined) -> Unknown { a } + fn h() -> u32 { y } + + fn main() { let x: u32 = f(1); }", + &[ + "Type alias `Missing` is not defined", + "Type alias `Undefined` is not defined", + "Type alias `Unknown` is not defined", + "Variable `y` is not defined", + ], + ); + } + + #[test] + fn broken_parameter_does_not_hide_other_errors() { + assert_errors( + "fn f(a: Missing, b: u32) -> u32 { b } + fn main() { + let t: bool = true; + let y: u32 = f((1, x), t); + }", + &[ + "Type alias `Missing` is not defined", + "Variable `x` is not defined", + "Expected expression of type `u32`, found type `bool`", + ], + ); + } + + #[test] + fn broken_enum_payload_does_not_hide_the_enum() { + assert_errors( + "enum E { A(Missing), B } + fn main() { + let e: E = E::B; + let n: u32 = match e { + E::A(a: u32) => a, + E::B => x, + }; + }", + &[ + "Type alias `Missing` is not defined", + "Variable `x` is not defined", + ], + ); + } + + #[test] + fn parameter_ignores_uses_of_broken_types() { + assert_errors( + "type Alias = Missing; + fn main() { + let a: Alias = param::X; + let b: u32 = param::X; + let c: bool = param::X; + }", + &[ + "Type alias `Missing` is not defined", + "Expected expression of type `u32`, found type `bool`", + ], + ); + } + + #[test] + fn cast_of_enum_with_broken_payload() { + assert_errors( + "enum E { A(Missing), B } + enum F { A(u8), B } + fn different(e: E) -> F { ::into(e) } + fn same(e: E) -> Option { >::into(Some(e)) } + + fn main() {}", + &[ + "Type alias `Missing` is not defined", + "Cannot cast values of type `E` as values of type `F`", + ], + ); + } + + #[test] + fn constant_of_broken_type_is_rejected() { + use crate::parse::{self, ParseFromStr}; + use crate::types::{ResolvedType, TypeConstructible}; + + let never = ResolvedType::never(); + let option_never = ResolvedType::option(never.clone()); + + for (source, ty) in [ + ("5", never), + ("5", option_never.clone()), + ("[None, None]", ResolvedType::array(option_never, 3)), + ] { + let parsed = parse::Expression::parse_from_str(source).expect("value parses"); + assert!(super::Expression::analyze_const(&parsed, &ty).is_err()); + } + } + + #[test] + fn construction_through_broken_enum_alias() { + assert_errors( + "type Bad = Missing; + enum Color { Red, Green } + + fn unknown_variant() -> Color { Bad::Blue } + fn payload_count() -> Color { Bad::Red(x) } + fn mixed_arms(c: Color) { + match c { + Color::Red => {}, + Bad::Green => {} + } + } + + fn main() {}", + &[ + "Type alias `Missing` is not defined", + "Variable `x` is not defined", + "Grammar error: all match arms must use the same enum; expected 'Color', found 'Bad'", + ], + ); + } + + #[test] + fn errors_mentioning_broken_types_are_not_reported() { + assert_errors( + "type Alias = Missing; + fn f(a: (Alias, u32)) {} + fn g() { f(1) } + fn main() { let x: Option = 5; }", + &["Type alias `Missing` is not defined"], + ); + } + + /// Aliases like `type A1 = (A0, A0);` up to `A{depth}`, whose resolved types share their parts. + fn shared_aliases(name: &str, leaf: &str, depth: usize) -> String { + let mut source = format!("type {name}0 = {leaf};\n"); + for i in 1..=depth { + source += &format!("type {name}{i} = ({name}{0}, {name}{0});\n", i - 1); + } + source + } + + #[test] + fn shared_types_are_fast() { + let source = shared_aliases("A", "u8", 64) + + &shared_aliases("B", "Missing", 64) + + &shared_aliases("C", "u8", 64) + + "enum E { V(Unknown) }\n" + + &shared_aliases("G", "E", 64) + + &shared_aliases("H", "E", 64) + + "fn cast(a: A64) -> B64 { ::into(a) } + fn enum_cast(g: G64) -> H64 { ::into(g) } + + fn main() { + let b: B64 = param::X; + let a: A64 = param::X; + let c: C64 = param::X; + }"; + + assert_errors( + &source, + &[ + "Type alias `Missing` is not defined", + "Type alias `Unknown` is not defined", + ], + ); + } + + #[test] + fn builtins_accept_broken_types() { + assert_errors( + "type Alias = Missing; + fn cast() -> u32 { ::into(x) } + fn add(element: Alias, acc: u32) -> u32 { acc } + fn fold(array: [Alias; 2]) -> u32 { array_fold::(array, 0) } + fn step(acc: u32, context: (), counter: Alias) -> Either { Right(acc) } + fn looped() -> Either { for_while::(0, ()) } + fn hash(tuple: (Alias,)) -> u256 { raw_hash::<(Alias,)>(tuple) } + + fn main() { let z: u32 = y; }", + &[ + "Type alias `Missing` is not defined", + "Variable `x` is not defined", + "Variable `y` is not defined", + ], + ); + } } #[cfg(test)] diff --git a/src/pattern.rs b/src/pattern.rs index 31b7a309..2df500be 100644 --- a/src/pattern.rs +++ b/src/pattern.rs @@ -64,6 +64,9 @@ impl Pattern { } }, Pattern::Ignore => {} + Pattern::Tuple(pats) | Pattern::Array(pats) if ty.is_never() => { + stack.extend(pats.iter().zip(std::iter::repeat(ty))); + } Pattern::Tuple(pats) => { if let Some(types) = ty.as_tuple() { stack.extend(pats.iter().zip(types.iter().map(Arc::as_ref))); diff --git a/src/types/resolved.rs b/src/types/resolved.rs index 50323a67..95b3c556 100644 --- a/src/types/resolved.rs +++ b/src/types/resolved.rs @@ -1,4 +1,5 @@ use core::fmt; +use std::collections::HashSet; use std::sync::Arc; use miniscript::iter::{Tree, TreeLike}; @@ -126,10 +127,72 @@ impl ResolvedType { /// Check whether the type can be lowered to a structural type. /// - /// Unlike [`Self::contains_never`], this looks inside enum payloads. + /// Use this before [`StructuralType::from`], which panics on `!`. Unlike + /// [`Self::contains_never`], this looks inside enum payloads. pub const fn has_structural_type(&self) -> bool { !self.flags.has_never && !self.flags.has_never_in_enum } + + /// Check whether the types are equal, where a type that mentions `!` is equal to every type. + pub(crate) fn compatible(&self, other: &Self) -> bool { + self.contains_never() || other.contains_never() || self.same_as(other) + } + + /// Check whether the types are equal, like `==` but without walking shared parts as trees. + /// + /// Use this instead of `==`, which takes exponential time on equal types + /// built from different aliases + pub(crate) fn same_as(&self, other: &Self) -> bool { + self.matches_with(other, |one, two| { + matches!( + one.inner, + TypeInner::Boolean | TypeInner::UInt(_) | TypeInner::Enum(_) | TypeInner::Never + ) && one == two + }) + } + + /// Check whether the types match, using `leaves_match` for their leaves. + /// + /// Shared by [`Self::same_as`] and the enum check of casts. + pub(crate) fn matches_with( + &self, + other: &Self, + mut leaves_match: impl FnMut(&Self, &Self) -> bool, + ) -> bool { + let mut seen = HashSet::new(); + let mut stack = vec![(self, other)]; + + while let Some((one, two)) = stack.pop() { + if std::ptr::eq(one, two) + || !seen.insert((std::ptr::from_ref(one), std::ptr::from_ref(two))) + { + continue; + } + + match (&one.inner, &two.inner) { + (TypeInner::Either(l1, r1), TypeInner::Either(l2, r2)) => { + stack.extend([(l1.as_ref(), l2.as_ref()), (r1.as_ref(), r2.as_ref())]); + } + (TypeInner::Option(i1), TypeInner::Option(i2)) => { + stack.push((i1.as_ref(), i2.as_ref())) + } + (TypeInner::Tuple(e1), TypeInner::Tuple(e2)) if Arc::ptr_eq(e1, e2) => {} + (TypeInner::Tuple(e1), TypeInner::Tuple(e2)) if e1.len() == e2.len() => { + stack.extend(e1.iter().map(Arc::as_ref).zip(e2.iter().map(Arc::as_ref))); + } + (TypeInner::Array(i1, n1), TypeInner::Array(i2, n2)) if n1 == n2 => { + stack.push((i1.as_ref(), i2.as_ref())); + } + (TypeInner::List(i1, b1), TypeInner::List(i2, b2)) if b1 == b2 => { + stack.push((i1.as_ref(), i2.as_ref())); + } + _ if leaves_match(one, two) => {} + _ => return false, + } + } + + true + } } impl TypeConstructible for ResolvedType {