From 6805a10c8fa4c7dfaa3603c8dabb7bf0c20bd5bd Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Fri, 17 Jul 2026 18:28:54 +0300 Subject: [PATCH 01/16] core: unstable: add enums variant and make recurse optional --- src/unstable.rs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/unstable.rs b/src/unstable.rs index 90c2b448..779b2a27 100644 --- a/src/unstable.rs +++ b/src/unstable.rs @@ -30,6 +30,9 @@ pub enum UnstableFeature { /// Import-system syntax: `use` imports, `mod` modules, `as` import /// aliases, and `crate::` paths. Enable with `simc -Z imports`. Imports, + /// Enum syntax: `enum` declarations and `EnumName::Variant` match + /// expressions. Enable with `simc -Z enums`. + Enums, } impl UnstableFeature { @@ -43,7 +46,7 @@ impl UnstableFeature { /// Append the new variant here, bump the count in /// `all_contains_every_variant` ( in the `tests` module below), then /// gate the syntax via [`RequireFeature`]. - pub const ALL: &'static [Self] = &[Self::Imports]; + pub const ALL: &'static [Self] = &[Self::Imports, Self::Enums]; /// Human-readable description shown next to the feature's name under /// `simc --help`. @@ -52,6 +55,9 @@ impl UnstableFeature { Self::Imports => { "Module system syntax: 'use' imports, 'mod' modules, 'as' aliases, 'crate::' paths" } + Self::Enums => { + "Enum syntax: 'enum' declarations and 'EnumName::Variant' match expressions" + } } } @@ -213,6 +219,7 @@ impl fmt::Display for UnstableFeature { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Imports => write!(f, "imports"), + Self::Enums => write!(f, "enums"), } } } @@ -225,6 +232,7 @@ impl FromStr for UnstableFeature { fn from_str(s: &str) -> Result { match s { "imports" => Ok(UnstableFeature::Imports), + "enums" => Ok(UnstableFeature::Enums), _ => Err(format!("Unknown unstable feature: '{s}'")), } } @@ -265,6 +273,14 @@ impl FromStr for UnstableFeature { /// recurse: items; /// } /// } +/// +/// // Struct that gates a feature but has no children to recurse into. +/// // The `recurse` clause is optional and may be omitted entirely. +/// impl_require_feature! { +/// EnumDeclaration { +/// requires: UnstableFeature::Enums, span: span; +/// } +/// } /// ``` macro_rules! impl_require_feature { (@recurse $out:ident; _) => {}; @@ -297,7 +313,7 @@ macro_rules! impl_require_feature { ( $ty:ty { $(requires: $feature:expr, span: $span:ident;)? - recurse: $($recurse:tt),* $(,)?; + $(recurse: $($recurse:tt),* $(,)?;)? } ) => { impl $crate::unstable::RequireFeature for $ty { @@ -313,12 +329,12 @@ macro_rules! impl_require_feature { self.$span, )); )? - $( + $($( $crate::unstable::RequireFeature::feature_requirements( &self.$recurse, out, ); - )* + )*)? } } }; @@ -339,7 +355,7 @@ mod tests { // the variant to `ALL`. Pins that `ALL` stays complete. assert_eq!( all_features.len(), - 1, + 2, "update this count when adding a feature" ); From 377168d44031b86d91f65dccbd2cbcc5d925c6ae Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Fri, 17 Jul 2026 18:33:36 +0300 Subject: [PATCH 02/16] core: witness: relax check for vare witness values --- src/witness.rs | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/witness.rs b/src/witness.rs index 78dc7075..9977be96 100644 --- a/src/witness.rs +++ b/src/witness.rs @@ -161,9 +161,10 @@ impl UnresolvedValues { /// Resolve each value against the type that the program declares for its name. /// + /// Bare value strings whose name the program does not declare are skipped. + /// /// ## Errors /// - /// - A bare value string is given for a name that the program does not declare. /// - A bare value string does not parse at the declared type. /// /// Self-typed entries are passed through unchanged. @@ -179,9 +180,10 @@ impl UnresolvedValues { let value = match unresolved { UnresolvedValue::Typed(value) => value, UnresolvedValue::Untyped(s) => { - let ty = declared_types.get(&name).ok_or_else(|| { - format!("`{name}` is not declared by the program, so its value `{s}` cannot be assigned a type") - })?; + let Some(ty) = declared_types.get(&name) else { + continue; + }; + Value::parse_from_str(&s, ty) .map_err(|error| format!("`{name}` is declared as `{ty}`: {error}"))? } @@ -373,14 +375,17 @@ fn main() { Some(&Value::u16(7)) ); - let unknown = UnresolvedValues::from_map(HashMap::from([( - WitnessName::from_str_unchecked("TYPO"), + // Entries the program does not declare are skipped (consistent with `WitnessValues::is_consistent`) + let extra = UnresolvedValues::from_map(HashMap::from([( + WitnessName::from_str_unchecked("UNUSED"), UnresolvedValue::Untyped("1".to_string()), )])); - let err = unknown - .resolve::(&witness_types) - .unwrap_err(); - assert!(err.contains("TYPO"), "error should name the entry: {err}"); + let resolved: WitnessValues = extra.resolve(&witness_types).unwrap(); + assert_eq!( + resolved.get(&WitnessName::from_str_unchecked("UNUSED")), + None, + "undeclared bare entries are ignored" + ); let bad = UnresolvedValues::from_map(HashMap::from([( WitnessName::from_str_unchecked("A"), From 045c7ee6a4aad79d57af0e8e3545668ef1509548 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Fri, 17 Jul 2026 18:35:02 +0300 Subject: [PATCH 03/16] core: lexer: register enum token --- src/lexer.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/lexer.rs b/src/lexer.rs index acb8cba3..862195e1 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -23,6 +23,7 @@ pub enum Token<'src> { Mod, Const, Match, + Enum, Crate, /// Reserved for the compiler version directive, which the version prescan /// consumes before lexing; [`lex`] reports any occurrence as an error and drops @@ -86,6 +87,7 @@ impl<'src> fmt::Display for Token<'src> { Token::Mod => write!(f, "mod"), Token::Const => write!(f, "const"), Token::Match => write!(f, "match"), + Token::Enum => write!(f, "enum"), Token::Crate => write!(f, "{}", CRATE_STR), Token::Simc => write!(f, "{}", SIMC_STR), @@ -202,6 +204,7 @@ pub fn lexer<'src>( "mod" => Token::Mod, "const" => Token::Const, "match" => Token::Match, + "enum" => Token::Enum, CRATE_STR => Token::Crate, SIMC_STR => Token::Simc, "true" => Token::Bool(true), @@ -306,8 +309,8 @@ pub fn lex( /// A list of all reserved keywords. pub const KEYWORDS: &[&str] = &[ - "pub", "use", "as", "fn", "let", "type", "mod", "const", "match", CRATE_STR, SIMC_STR, "true", - "false", + "pub", "use", "as", "fn", "let", "type", "mod", "const", "match", "enum", CRATE_STR, SIMC_STR, + "true", "false", ]; /// Checks whether a given string is a keyword. @@ -435,6 +438,24 @@ mod tests { assert_eq!(tokens, Some(vec![Token::Ident("simcfoo")])); } + #[test] + fn test_enum_token() { + let (tokens, errors) = lex("enum Path { Inherit, ColdSpend }"); + assert!(errors.is_empty()); + assert_eq!( + tokens, + Some(vec![ + Token::Enum, + Token::Ident("Path"), + Token::LBrace, + Token::Ident("Inherit"), + Token::Comma, + Token::Ident("ColdSpend"), + Token::RBrace, + ]) + ); + } + #[test] fn lexer_test() { use chumsky::prelude::*; From 4bf73b4b183c591c96dfe089af8939391b6a7351 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Fri, 17 Jul 2026 18:53:38 +0300 Subject: [PATCH 04/16] core: parser: define variant, declaration, match, natch arm construction for enum --- src/parse.rs | 282 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) diff --git a/src/parse.rs b/src/parse.rs index 56188414..de21faf9 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -460,6 +460,128 @@ impl_eq_hash!(TypeAlias; name, ty); impl_require_feature!(TypeAlias { recurse: ty; }); +/// A single variant in an enum declaration. +/// +/// A variant's position among the declared variants determines its leaf +/// in the enum's balanced sum, i.e. its wire encoding. +/// A variant may carry payload types (`Refresh(Signature, u8)`). +/// A variant without payload is a unit variant. +#[derive(Clone, Debug)] +pub struct EnumVariant { + name: Identifier, + payload: Arc<[AliasedType]>, + span: Span, +} + +#[cfg(feature = "arbitrary")] +impl<'a> arbitrary::Arbitrary<'a> for EnumVariant { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let name = Identifier::arbitrary(u)?; + let len = u.int_in_range(0..=3)?; + let payload = (0..len) + .map(|_| AliasedType::arbitrary(u)) + .collect::>>()?; + Ok(Self { + name, + payload, + span: Span::DUMMY, + }) + } +} + +impl EnumVariant { + pub fn name(&self) -> &Identifier { + &self.name + } + + /// Access the payload types of the variant. Empty for unit variants. + pub fn payload(&self) -> &[AliasedType] { + &self.payload + } +} + +impl_eq_hash!(EnumVariant; name, payload); + +impl AsRef for EnumVariant { + fn as_ref(&self) -> &Span { + &self.span + } +} + +/// An enum declaration. +#[derive(Clone, Debug)] +pub struct EnumDeclaration { + visibility: Visibility, + name: AliasName, + variants: Arc<[EnumVariant]>, + span: Span, +} + +impl EnumDeclaration { + pub fn visibility(&self) -> &Visibility { + &self.visibility + } + + pub fn name(&self) -> &AliasName { + &self.name + } + + pub fn variants(&self) -> &[EnumVariant] { + &self.variants + } +} + +impl_require_feature!(EnumVariant { recurse: payload; }); + +impl_require_feature!(EnumDeclaration { + requires: UnstableFeature::Enums, span: span; + recurse: variants; +}); + +impl_eq_hash!(EnumDeclaration; name, variants); + +impl AsRef for EnumDeclaration { + fn as_ref(&self) -> &Span { + &self.span + } +} + +#[cfg(feature = "arbitrary")] +impl<'a> arbitrary::Arbitrary<'a> for EnumDeclaration { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let visibility = Visibility::arbitrary(u)?; + let name = AliasName::arbitrary(u)?; + let len = u.int_in_range(2..=8)?; + let variants = (0..len) + .map(|_| EnumVariant::arbitrary(u)) + .collect::>>()?; + Ok(Self { + visibility, + name, + variants, + span: Span::DUMMY, + }) + } +} + +impl fmt::Display for EnumDeclaration { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}enum {} {{", self.visibility(), self.name())?; + for variant in self.variants() { + write!(f, " {}", variant.name())?; + if let Some((first, rest)) = variant.payload().split_first() { + write!(f, "({first}")?; + for ty in rest { + write!(f, ", {ty}")?; + } + write!(f, ")")?; + } + write!(f, ",")?; + } + write!(f, " }}") + } +} + /// An expression is something that returns a value. #[derive(Clone, Debug)] pub struct Expression { @@ -648,6 +770,166 @@ impl_eq_hash!(Match; scrutinee, left, right); impl_require_feature!(Match {recurse: scrutinee, left, right; }); +/// Match expression over a named enum type. +#[derive(Clone, Debug)] +pub struct EnumMatch { + scrutinee: Arc, + arms: Arc<[EnumMatchArm]>, + span: Span, +} + +impl EnumMatch { + /// Access the expression that is matched. + pub fn scrutinee(&self) -> &Expression { + &self.scrutinee + } + + /// Access the match arms. + pub fn arms(&self) -> &[EnumMatchArm] { + &self.arms + } + + /// Access the span of the match statement. + pub fn span(&self) -> &Span { + &self.span + } +} + +impl_require_feature!(EnumMatch { + requires: UnstableFeature::Enums, span: span; + recurse: scrutinee, arms; +}); + +impl_eq_hash!(EnumMatch; scrutinee, arms); + +impl AsRef for EnumMatch { + fn as_ref(&self) -> &Span { + &self.span + } +} + +/// Arm of an enum match expression: `Enum::Variant(bindings..) => expression`. +/// +/// The arm type carries the classification the parser proved. +/// Every arm of an [`EnumMatch`] names an enum variant, so no other pattern kind is representable here. +#[derive(Clone, Debug)] +pub struct EnumMatchArm { + enum_path: Arc<[Identifier]>, + variant: Identifier, + bindings: Arc<[(Pattern, AliasedType)]>, + expression: Arc, +} + +impl EnumMatchArm { + /// Access the written enum path of the arm, e.g. `["m", "Choice"]`. + pub fn enum_path(&self) -> &[Identifier] { + &self.enum_path + } + + /// The written enum path as one string, e.g. `m::Choice`. + pub fn enum_path_string(&self) -> String { + self.enum_path + .iter() + .map(Identifier::as_inner) + .collect::>() + .join("::") + } + + /// Access the name of the matched variant. + pub fn variant(&self) -> &Identifier { + &self.variant + } + + /// Access the payload bindings of the arm. Empty for unit variants. + pub fn bindings(&self) -> &[(Pattern, AliasedType)] { + &self.bindings + } + + /// Access the expression that is executed in the match arm. + pub fn expression(&self) -> &Expression { + &self.expression + } +} + +impl_eq_hash!(EnumMatchArm; enum_path, variant, bindings, expression); + +impl_require_feature!(EnumMatchArm { recurse: expression; }); + +/// Displays the arm's head (`Enum::Variant(bindings..)`), without the body. +impl fmt::Display for EnumMatchArm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}::{}", self.enum_path_string(), self.variant)?; + if self.bindings.is_empty() { + return Ok(()); + } + f.write_str("(")?; + for (i, (pattern, ty)) in self.bindings.iter().enumerate() { + if 0 < i { + f.write_str(", ")?; + } + write!(f, "{pattern}: {ty}")?; + } + f.write_str(")") + } +} + +/// Construction of an enum variant: `Action::Refresh(sig, 3)`. +/// +/// The written enum name is either an alias in scope (`MyChoice`) or the +/// enum's declared name itself (`Action`), which needs no scope. +/// Unit variants take no argument list. +#[derive(Clone, Debug)] +pub struct EnumConstruction { + enum_path: Arc<[Identifier]>, + variant: Identifier, + args: Arc<[Expression]>, + span: Span, +} + +impl EnumConstruction { + /// Access the written enum path, e.g. `["m", "Action"]`. + pub fn enum_path(&self) -> &[Identifier] { + &self.enum_path + } + + /// Access the name of the constructed variant. + pub fn variant(&self) -> &Identifier { + &self.variant + } + + /// Access the payload arguments. Empty for unit variants. + pub fn args(&self) -> &[Expression] { + &self.args + } + + /// Access the span of the construction expression. + pub fn span(&self) -> &Span { + &self.span + } + + /// The written enum path as one string, e.g. `m::Action`. + pub fn enum_path_string(&self) -> String { + self.enum_path + .iter() + .map(Identifier::as_inner) + .collect::>() + .join("::") + } +} + +impl_eq_hash!(EnumConstruction; enum_path, variant, args); + +impl_require_feature!(EnumConstruction { + requires: UnstableFeature::Enums, span: span; + recurse: args; +}); + +impl AsRef for EnumConstruction { + fn as_ref(&self) -> &Span { + &self.span + } +} + /// Arm of a match expression. #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct MatchArm { From ec19c5bfb61ac59a405bef59edff4aa5b8d5b978 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Fri, 17 Jul 2026 19:02:13 +0300 Subject: [PATCH 05/16] core: parser: add enum declaration to items and parse it --- src/ast.rs | 1 + src/driver/mod.rs | 5 +- src/driver/resolve_order.rs | 4 +- src/parse.rs | 151 +++++++++++++++++++++++++++++++++++- 4 files changed, 158 insertions(+), 3 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index 7e27b20a..f2e93418 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1126,6 +1126,7 @@ impl AbstractSyntaxTree for Item { scope.resolve_use(use_decl).with_span(use_decl)?; Ok(Self::Use) } + parse::Item::EnumDeclaration(_decl) => todo!(), parse::Item::Module(module) => { scope .enter_module(module.name().clone(), module.visibility().clone()) diff --git a/src/driver/mod.rs b/src/driver/mod.rs index 0b1b6a30..50a94d48 100644 --- a/src/driver/mod.rs +++ b/src/driver/mod.rs @@ -458,7 +458,10 @@ impl<'a> ImportContext<'a> { } // These items carry no import information at this stage and can be safely skipped. - parse::Item::TypeAlias(_) | parse::Item::Function(_) | parse::Item::Ignored => {} + parse::Item::TypeAlias(_) + | parse::Item::Function(_) + | parse::Item::EnumDeclaration(_) + | parse::Item::Ignored => {} } } diff --git a/src/driver/resolve_order.rs b/src/driver/resolve_order.rs index 4df4de43..b23a6698 100644 --- a/src/driver/resolve_order.rs +++ b/src/driver/resolve_order.rs @@ -98,7 +98,9 @@ impl DependencyGraph { &items, ))) } - parse::Item::TypeAlias(_) | parse::Item::Function(_) => Some(item.clone()), + parse::Item::TypeAlias(_) + | parse::Item::Function(_) + | parse::Item::EnumDeclaration(_) => Some(item.clone()), parse::Item::Ignored => None, } } diff --git a/src/parse.rs b/src/parse.rs index de21faf9..2013af14 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -68,6 +68,8 @@ pub enum Item { /// An import declaration (e.g., `use math::add`) that brings another /// [`Item`] into the current scope. Use(UseDecl), + /// An enum declaration. + EnumDeclaration(EnumDeclaration), /// A module containing a collection of nested [`Item`]. Module(Module), /// A placeholder used exclusively for error recovery during parsing. @@ -82,6 +84,7 @@ impl_require_feature!(Item { TypeAlias(alias), Function(function), Use(use_decl), + EnumDeclaration(decl), Module(module), Ignored, }); @@ -1065,6 +1068,7 @@ impl fmt::Display for Item { Self::TypeAlias(alias) => write!(f, "{alias}"), Self::Function(function) => write!(f, "{function}"), Self::Use(use_declaration) => write!(f, "{use_declaration}"), + Self::EnumDeclaration(decl) => write!(f, "{decl}"), Self::Module(module) => write!(f, "{module}"), Self::Ignored => Ok(()), } @@ -1815,11 +1819,18 @@ impl ChumskyParse for Item { let func_parser = Function::parser().map(Item::Function); let type_parser = TypeAlias::parser().map(Item::TypeAlias); let use_parser = UseDecl::parser().map(Item::Use); + let enum_parser = EnumDeclaration::parser().map(Item::EnumDeclaration); // Lazy item here let mod_parser = Module::parser_with_items(item).map(Item::Module); - choice((func_parser, use_parser, type_parser, mod_parser)) + choice(( + func_parser, + use_parser, + type_parser, + enum_parser, + mod_parser, + )) }) } } @@ -2233,6 +2244,71 @@ impl ChumskyParse for TypeAlias { } } +/// Identifiers of the built-in binary match patterns. +/// An enum may not use them as its name, because e.g. `Left::A` in a match arm would parse as the built-in `Left(..)` pattern. +pub(crate) const RESERVED_PATTERN_NAMES: [&str; 4] = ["Left", "Right", "Some", "None"]; + +impl ChumskyParse for EnumDeclaration { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let visibility = just(Token::Pub) + .to(Visibility::Public) + .or_not() + .map(Option::unwrap_or_default); + + let name = AliasName::parser().try_map(|name, span| { + if RESERVED_PATTERN_NAMES.contains(&name.as_inner()) { + return Err(Diagnostic::new( + Error::Grammar { + msg: format!( + "enum name '{name}' is reserved for the built-in match pattern `{name}`" + ), + }, + span, + )); + } + Ok(name) + }); + + let payload = AliasedType::parser() + .separated_by(just(Token::Comma)) + .allow_trailing() + .at_least(1) + .collect::>() + .delimited_by(just(Token::LParen), just(Token::RParen)) + .or_not() + .map(|payload| Arc::from(payload.unwrap_or_default())); + + let variant = Identifier::parser() + .then(payload) + .map_with(|(name, payload), e| EnumVariant { + name, + payload, + span: e.span(), + }); + + let variants = variant + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)) + .map(Arc::from); + + visibility + .then_ignore(just(Token::Enum)) + .then(name) + .then(variants) + .map_with(|((visibility, name), variants), e| Self { + visibility, + name, + variants, + span: e.span(), + }) + } +} + impl ChumskyParse for Expression { fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone where @@ -2571,6 +2647,27 @@ impl Module { items, span: e.span(), }) + .validate(|module, _, emit| { + // TODO: Enums may only be declared at the top level of a file (done so to reduce scope of the PR). + // The bare name is the enum's identity in the ABI, and a module path would obscure it. + // Direct children suffice. Nested modules validate their own items. + for item in module.items.iter() { + if let Item::EnumDeclaration(decl) = item { + emit.emit( + Error::Grammar { + msg: format!( + "enum `{}` is declared inside `mod {}`; enums may \ + only be declared at the top level of a file", + decl.name(), + module.name + ), + } + .with_span(decl.into()), + ); + } + } + module + }) } } @@ -3113,4 +3210,56 @@ fn main() { assert_eq!(program.to_string(), format!("{input}\n")); } } + + fn parse_item(input: &str) -> Item { + let program = parse::Program::parse_from_str(input).expect("parsing should succeed"); + program.items().first().expect("expected one item").clone() + } + + #[test] + fn test_enum_declaration_basic() { + let item = parse_item("enum Path { Inherit, ColdSpend, RefreshSpend, }"); + let Item::EnumDeclaration(decl) = item else { + panic!("expected EnumDeclaration, got {item:?}"); + }; + assert_eq!(decl.name().as_inner(), "Path"); + assert_eq!(decl.variants().len(), 3); + assert_eq!(decl.variants()[0].name().as_inner(), "Inherit"); + assert_eq!(decl.variants()[2].name().as_inner(), "RefreshSpend"); + } + + #[test] + fn test_enum_declaration_pub() { + let item = parse_item("pub enum Color { Red, Green, Blue, }"); + let Item::EnumDeclaration(decl) = item else { + panic!("expected EnumDeclaration"); + }; + assert_eq!(decl.visibility(), &Visibility::Public); + assert_eq!(decl.name().as_inner(), "Color"); + } + + #[test] + fn test_enum_declaration_display_round_trip() { + let input = "enum Path { Inherit, ColdSpend, RefreshSpend, }"; + let item = parse_item(input); + let Item::EnumDeclaration(decl) = item else { + panic!("expected EnumDeclaration"); + }; + assert_eq!( + decl.to_string(), + "enum Path { Inherit, ColdSpend, RefreshSpend, }" + ); + } + + #[test] + fn test_enum_declaration_reserved_name() { + for reserved in RESERVED_PATTERN_NAMES { + let result = Program::parse_from_str(&format!("enum {reserved} {{ A, B, }}")); + let error = result.expect_err(&format!("enum name {reserved} is reserved")); + assert!( + error.to_string().contains("reserved"), + "error should say the name is reserved: {error}" + ); + } + } } From d9a462260352d08524f7c4178e7c1ee5cb51bfee Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Fri, 17 Jul 2026 19:13:05 +0300 Subject: [PATCH 06/16] core: driver: add checks that would temporarily disallow using enums in deps --- src/driver/resolve_order.rs | 52 +++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/driver/resolve_order.rs b/src/driver/resolve_order.rs index b23a6698..45648c4c 100644 --- a/src/driver/resolve_order.rs +++ b/src/driver/resolve_order.rs @@ -5,6 +5,56 @@ use crate::error::{Diagnostic, DiagnosticManager, Error, Span}; use crate::parse::{self, Visibility}; use crate::str::{Identifier, ModuleName}; +/// All enum declarations among `items`, recursing into `mod` blocks. +fn enum_declarations(items: &[parse::Item]) -> Vec<&parse::EnumDeclaration> { + let mut found = Vec::new(); + for item in items { + match item { + parse::Item::EnumDeclaration(decl) => found.push(decl), + parse::Item::Module(module) => found.extend(enum_declarations(module.items())), + _ => {} + } + } + found +} + +// TODO: allow enums in deps when mentioned problems are resolved +/// Enums by design are nominative, therefore to reason about same named enums in different modules +/// we have to have a stable ABI with the suport of "qualified name". +/// Currently, there is no support of "qualified name" concpet, therefore at the time of creating +/// enums, it is forbidden to decler them in dependencies. +/// +/// If we used current ABI we would face following problems: +/// 1. Adding or removing an unrelated dependency renumbers the files, so the same enum's ABI +/// name changes between builds even though no source changed. +/// The whole point of "identity is the qualified name" is that serialized forms can identify an enum across builds. +/// 2. Unwritable witness files. A user filling in a witness would have +/// to write `unit_2::Action::Cold` (a name that appears nowhere in their source and that they can't predict). +/// 3. Meaningless nominal distinctness. `a::Action` vs `b::Action` being distinct types only makes +/// sense if a and b are the user's module names, not compiler-generated counters. +fn forbid_enum_dec_in_deps( + source_id: usize, + local_items: &[parse::Item], + diagnostics: &mut DiagnosticManager, +) { + if source_id == MAIN_MODULE { + return; + } + + for decl in enum_declarations(local_items) { + diagnostics.push(Diagnostic::new( + Error::Grammar { + msg: format!( + "enum `{}` is declared in a dependency file; \ + enums may only be declared in the program's own files", + decl.name() + ), + }, + *decl.as_ref(), + )); + } +} + /// This is a core component of the [`DependencyGraph`]. impl DependencyGraph { /// Resolves the dependency graph and constructs the final AST program. @@ -63,6 +113,8 @@ impl DependencyGraph { } } + forbid_enum_dec_in_deps(source_id, &local_items, diagnostics); + let name = ModuleName::from_str_unchecked(Self::get_module_name(source_id).as_inner()); items.push(parse::Item::Module(parse::Module::new( source_id, From fcb68354fe48550b16dfc41b7b467079ee57d120 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Fri, 17 Jul 2026 19:15:54 +0300 Subject: [PATCH 07/16] core: ast: add match, match arm, and construction definitions --- src/ast.rs | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/src/ast.rs b/src/ast.rs index f2e93418..f221e1be 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -412,6 +412,95 @@ impl Match { impl_eq_hash!(Match; scrutinee, left, right); +/// Match expression over an enum's variants. +#[derive(Clone, Debug)] +pub struct EnumMatch { + scrutinee: Arc, + /// Arms in variant order (declaration order). + /// + /// The order matches the leaf order of the enum's balanced sum. + arms: Arc<[EnumMatchArm]>, + span: Span, +} + +impl EnumMatch { + /// Access the expression whose output is dispatched on in the match statement. + pub fn scrutinee(&self) -> &Expression { + &self.scrutinee + } + + /// Access the arms in variant order (declaration order). + pub fn arms(&self) -> &[EnumMatchArm] { + &self.arms + } + + /// Access the span of the match statement. + pub fn span(&self) -> &Span { + &self.span + } +} + +impl_eq_hash!(EnumMatch; scrutinee, arms); + +/// Arm of an [`EnumMatch`] expression, ordered by variant. +#[derive(Clone, Debug)] +pub struct EnumMatchArm { + /// Pattern binding the variant's payload. [`Pattern::Ignore`] for unit + /// variants. + pattern: Pattern, + body: Arc, +} + +impl EnumMatchArm { + /// Access the pattern that binds the variant's payload. + pub fn pattern(&self) -> &Pattern { + &self.pattern + } + + /// Access the expression that is executed in the match arm. + pub fn body(&self) -> &Expression { + &self.body + } +} + +impl_eq_hash!(EnumMatchArm; pattern, body); + +/// Construction of an enum variant: the variant's position and its payload +/// expressions. The enum's definition lives in the type of the enclosing +/// [`SingleExpression`]. +#[derive(Clone, Debug)] +pub struct EnumConstruction { + variant_index: usize, + payload: Arc<[Arc]>, + span: Span, +} + +impl EnumConstruction { + /// Access the constructed variant's position among the declared variants. + pub fn variant_index(&self) -> usize { + self.variant_index + } + + /// Access the payload expressions. Empty for unit variants. + pub fn payload(&self) -> &[Arc] { + &self.payload + } +} + +impl_eq_hash!(EnumConstruction; variant_index, payload); + +impl AsRef for EnumConstruction { + fn as_ref(&self) -> &Span { + &self.span + } +} + +impl AsRef for EnumMatch { + fn as_ref(&self) -> &Span { + &self.span + } +} + /// Arm of a [`Match`] expression. #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct MatchArm { From 47bbe0bb1bb152f52d0dbf4b158b518b81cfc528 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Sat, 18 Jul 2026 11:51:14 +0300 Subject: [PATCH 08/16] core: parser: aparse match arm and construction --- src/ast.rs | 2 + src/error.rs | 4 +- src/parse.rs | 468 ++++++++++++++++++++++++++++++++++++++------------- 3 files changed, 355 insertions(+), 119 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index f221e1be..5921f7c2 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1562,6 +1562,8 @@ impl AbstractSyntaxTree for SingleExpression { parse::SingleExpressionInner::Match(match_) => { Match::analyze(match_, ty, scope).map(SingleExpressionInner::Match)? } + parse::SingleExpressionInner::EnumConstruction(_construction) => todo!(), + parse::SingleExpressionInner::EnumMatch(_enum_match) => todo!(), }; Ok(Self { diff --git a/src/error.rs b/src/error.rs index 82d6a202..8b25e483 100644 --- a/src/error.rs +++ b/src/error.rs @@ -728,8 +728,8 @@ pub enum Error { found: Option, }, IncompatibleMatchArms { - first: MatchPattern, - second: MatchPattern, + first: Box, + second: Box, }, // TODO: Remove CompileError once SimplicityHL has a type system // The SimplicityHL compiler should never produce ill-typed Simplicity code diff --git a/src/parse.rs b/src/parse.rs index 2013af14..b092a698 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -15,6 +15,7 @@ use chumsky::prelude::{ use chumsky::{extra, select, IterParser, Parser}; use either::Either; +use itertools::Itertools; use miniscript::iter::{Tree, TreeLike}; use crate::driver::{CRATE_STR, MAIN_MODULE}; @@ -697,6 +698,10 @@ pub enum SingleExpressionInner { Expression(Arc), /// Match expression over a sum type Match(Match), + /// Match expression over an enum's variants + EnumMatch(EnumMatch), + /// Construction of an enum variant + EnumConstruction(EnumConstruction), /// Tuple wrapper expression Tuple(Arc<[Expression]>), /// Array wrapper expression @@ -721,6 +726,8 @@ impl_require_feature!(SingleExpressionInner { Call(call), Expression(expr), Match(match_), + EnumMatch(enum_match), + EnumConstruction(construction), Tuple(exprs), Array(exprs), List(exprs), @@ -1193,6 +1200,8 @@ pub enum ExprTree<'a> { Single(&'a SingleExpression), Call(&'a Call), Match(&'a Match), + EnumMatch(&'a EnumMatch), + EnumConstruction(&'a EnumConstruction), } impl TreeLike for ExprTree<'_> { @@ -1233,16 +1242,33 @@ impl TreeLike for ExprTree<'_> { | S::Expression(l) => Tree::Unary(Self::Expression(l)), S::Call(call) => Tree::Unary(Self::Call(call)), S::Match(match_) => Tree::Unary(Self::Match(match_)), + S::EnumMatch(enum_match) => Tree::Unary(Self::EnumMatch(enum_match)), + S::EnumConstruction(construction) => { + Tree::Unary(Self::EnumConstruction(construction)) + } S::Tuple(elements) | S::Array(elements) | S::List(elements) => { Tree::Nary(elements.iter().map(Self::Expression).collect()) } }, Self::Call(call) => Tree::Nary(call.args().iter().map(Self::Expression).collect()), + Self::EnumConstruction(construction) => { + Tree::Nary(construction.args().iter().map(Self::Expression).collect()) + } Self::Match(match_) => Tree::Nary(Arc::new([ Self::Expression(match_.scrutinee()), Self::Expression(match_.left().expression()), Self::Expression(match_.right().expression()), ])), + Self::EnumMatch(enum_match) => Tree::Nary( + std::iter::once(Self::Expression(enum_match.scrutinee())) + .chain( + enum_match + .arms() + .iter() + .map(|arm| Self::Expression(arm.expression())), + ) + .collect(), + ), } } } @@ -1308,7 +1334,7 @@ impl fmt::Display for ExprTree<'_> { write!(f, ")")?; } }, - S::Call(..) | S::Match(..) => {} + S::Call(..) | S::Match(..) | S::EnumMatch(..) | S::EnumConstruction(..) => {} S::Tuple(tuple) => { if data.n_children_yielded == 0 { write!(f, "(")?; @@ -1350,6 +1376,26 @@ impl fmt::Display for ExprTree<'_> { write!(f, ")")?; } } + Self::EnumConstruction(construction) => { + match data.n_children_yielded { + 0 => { + write!( + f, + "{}::{}", + construction.enum_path_string(), + construction.variant() + )?; + if !construction.args().is_empty() { + write!(f, "(")?; + } + } + _ if !data.is_complete => write!(f, ", ")?, + _ => {} + } + if data.is_complete && !construction.args().is_empty() { + write!(f, ")")?; + } + } Self::Match(match_) => match data.n_children_yielded { 0 => write!(f, "match ")?, 1 => write!(f, "{{\n{} => ", match_.left().pattern())?, @@ -1359,6 +1405,14 @@ impl fmt::Display for ExprTree<'_> { write!(f, ",\n}}")?; } }, + Self::EnumMatch(enum_match) => match data.n_children_yielded { + 0 => write!(f, "match ")?, + 1 => write!(f, "{{\n{} => ", enum_match.arms()[0])?, + n if n <= enum_match.arms().len() => { + write!(f, ",\n{} => ", enum_match.arms()[n - 1])?; + } + _ => write!(f, ",\n}}")?, + }, } } @@ -1422,6 +1476,12 @@ impl fmt::Display for Match { } } +impl fmt::Display for EnumMatch { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", ExprTree::EnumMatch(self)) + } +} + impl fmt::Display for MatchPattern { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -2428,9 +2488,34 @@ impl SingleExpression { Token::Param(s) => SingleExpressionInner::Parameter(WitnessName::from_str_unchecked(s)), }; + // Enum variant construction: `Path::To::Enum::Variant(args..)`. + // At least one `::` distinguishes the path from variables and calls. + // The built-in wrappers (Left, Some, ...) require `(` directly after + // their name, so they never reach this alternative. + let enum_construction = Identifier::parser() + .separated_by(just(Token::DoubleColon)) + .at_least(2) + .collect::>() + .then( + comma_separated + .clone() + .delimited_by(just(Token::LParen), just(Token::RParen)) + .or_not(), + ) + .map_with(|(mut segments, args), e| { + let variant = segments.pop().expect("the parser requires two segments"); + EnumConstruction { + enum_path: Arc::from(segments), + variant, + args: Arc::from(args.unwrap_or_default()), + span: e.span(), + } + }) + .map(SingleExpressionInner::EnumConstruction); + let call = Call::parser(expr.clone()).map(SingleExpressionInner::Call); - let match_expr = Match::parser(expr.clone()).map(SingleExpressionInner::Match); + let match_expr = match_expr_parser(expr.clone()); let variable = Identifier::parser().map(SingleExpressionInner::Variable); @@ -2441,8 +2526,20 @@ impl SingleExpression { .map(|es| SingleExpressionInner::Expression(Arc::from(es))); choice(( - left, right, some, none, boolean, match_expr, expression, list, array, tuple, call, - literal, variable, + left, + right, + some, + none, + boolean, + match_expr, + expression, + list, + array, + tuple, + enum_construction, + call, + literal, + variable, )) .map_with(|inner, e| Self { inner, @@ -2485,129 +2582,261 @@ impl ChumskyParse for MatchPattern { } } -impl MatchArm { - fn parser<'tokens, 'src: 'tokens, I, E>( - expr: E, - ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone - where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, - { - MatchPattern::parser() - .then_ignore(just(Token::FatArrow)) - .then(expr.map(Arc::new)) - .then(just(Token::Comma).or_not()) - .validate(|((pattern, expression), comma), e, emitter| { - let is_block = matches!(expression.as_ref().inner, ExpressionInner::Block(_, _)); - - if !is_block && comma.is_none() { - emitter.emit( - Error::Grammar { - msg: "Missing ',' after a match arm that isn't block expression" - .to_string(), - } - .with_span(e.span()), - ); - } +/// Head of an enum match arm, before the arm body is attached: `EnumName::Variant` with optional payload bindings. +#[derive(Clone)] +struct EnumArmHead { + enum_path: Arc<[Identifier]>, + variant: Identifier, + bindings: Arc<[(Pattern, AliasedType)]>, +} - Self { - pattern, - expression, - } - }) +impl EnumArmHead { + fn into_arm(self, expression: Arc) -> EnumMatchArm { + EnumMatchArm { + enum_path: self.enum_path, + variant: self.variant, + bindings: self.bindings, + expression, + } } } -impl Match { - fn parser<'tokens, 'src: 'tokens, I, E>( - expr: E, - ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone - where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, - { - let scrutinee = expr.clone().map(Arc::new); +/// One parsed match arm. An enum variant head or a built-in pattern, +/// plus the arm body. The head classification decides below whether the +/// whole match becomes an [`EnumMatch`] or a binary [`Match`]. +type ParsedMatchArm = (Either, Arc); - let arm_recovery = any() - .filter(|t| !matches!(t, Token::Comma | Token::RBrace)) - .ignored() - .or(nested_delimiters( - Token::LBrace, - Token::RBrace, - [ - (Token::LParen, Token::RParen), - (Token::LBracket, Token::RBracket), - ], - |_| (), - ) - .ignored()) - .repeated() - .map_with(|(), _| None); +/// Parser for the head of an enum match arm: `EnumName::Variant` with +/// optional payload bindings `(pattern: Type, ...)`. +/// +/// Built-in pattern names are excluded so `choice()` works without backtracking. +/// For a reserved name the `select!` guard fails without consuming the token. +fn enum_arm_head_parser<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, EnumArmHead, ParseError<'src>> + Clone +where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, +{ + let bindings = Pattern::parser() + .then_ignore(just(Token::Colon)) + .then(AliasedType::parser()) + .separated_by(just(Token::Comma)) + .allow_trailing() + .at_least(1) + .collect::>() + .delimited_by(just(Token::LParen), just(Token::RParen)) + .or_not() + .map(|bindings| Arc::from(bindings.unwrap_or_default())); + + select! { Token::Ident(name) if !RESERVED_PATTERN_NAMES.contains(&name) => Identifier::from_str_unchecked(name) } + .then_ignore(just(Token::DoubleColon)) + .then( + Identifier::parser() + .separated_by(just(Token::DoubleColon)) + .at_least(1) + .collect::>(), + ) + .then(bindings) + .map(|((first, mut rest), bindings)| { + let variant = rest.pop().expect("the parser requires one segment"); + let mut enum_path = vec![first]; + enum_path.extend(rest); + EnumArmHead { + enum_path: Arc::from(enum_path), + variant, + bindings, + } + }) +} - let arm_parser = MatchArm::parser(expr.clone()) - .map(Some) - .recover_with(via_parser(arm_recovery.clone())); +/// Parser for one match arm. An arm head, `=>`, the arm body, and the +/// comma that is required unless the body is a block expression. +fn match_arm_parser<'tokens, 'src: 'tokens, I, E>( + expr: E, +) -> impl Parser<'tokens, I, ParsedMatchArm, ParseError<'src>> + Clone +where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, +{ + let arm_head = choice(( + enum_arm_head_parser().map(Either::Left), + MatchPattern::parser().map(Either::Right), + )); + + arm_head + .then_ignore(just(Token::FatArrow)) + .then(expr.map(Arc::new)) + .then(just(Token::Comma).or_not()) + .validate(|((head, expression), comma), e, emitter| { + let is_block = matches!(expression.as_ref().inner, ExpressionInner::Block(_, _)); + if !is_block && comma.is_none() { + emitter.emit( + Error::Grammar { + msg: "Missing ',' after a match arm that isn't block expression" + .to_string(), + } + .with_span(e.span()), + ); + } + (head, expression) + }) +} - let arms = delimited_with_recovery( - arm_parser.clone().then(arm_parser.clone()), - Token::LBrace, - Token::RBrace, - |_| (None, None), - ); +/// 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 { + matches!( + (left, right), + (MatchPattern::Left(..), MatchPattern::Right(..)) + | (MatchPattern::None, MatchPattern::Some(..)) + | (MatchPattern::False, MatchPattern::True) + ) +} + +/// Assemble parsed arms into an [`EnumMatch`] or a binary [`Match`] node. +/// +/// Malformed arms return the error to report together with a fallback +/// node, so the caller emits exactly one error per malformed match. +/// Running bad arms through the pattern pairing would emit a second, +/// bogus "incompatible match arms" error for the same span. +fn assemble_match_arms( + scrutinee: Arc, + arms: Vec, + span: Span, +) -> Result> { + if arms.is_empty() { + return Err(Box::new(( + Error::Grammar { + msg: "match expression has no arms".to_string(), + } + .with_span(span), + placeholder_match(scrutinee, span), + ))); + } + + // Split arms by the classification the arm heads carry. Any number of + // enum arms (including one) routes to the enum match, whose analysis + // reports missing variants. The binary arm-count error below would be misleading there. + let (enum_arms, builtin_arms): (Vec, Vec) = arms + .into_iter() + .partition_map(|(head, expression)| match head { + Either::Left(head) => Either::Left(head.into_arm(expression)), + Either::Right(pattern) => Either::Right(MatchArm { + pattern, + expression, + }), + }); - just(Token::Match) - .ignore_then(scrutinee) - .then(arms) - .validate(|(scrutinee, arms), e, emit| match arms { - (Some(first), Some(second)) => { - let (left, right) = match (&first.pattern, &second.pattern) { - (MatchPattern::Left(..), MatchPattern::Right(..)) => (first, second), - (MatchPattern::Right(..), MatchPattern::Left(..)) => (second, first), - - (MatchPattern::None, MatchPattern::Some(..)) => (first, second), - (MatchPattern::Some(..), MatchPattern::None) => (second, first), - - (MatchPattern::False, MatchPattern::True) => (first, second), - (MatchPattern::True, MatchPattern::False) => (second, first), - - (p1, p2) => { - emit.emit( - Error::IncompatibleMatchArms { - first: p1.clone(), - second: p2.clone(), - } - .with_span(e.span()), - ); - (first, second) - } - }; + if builtin_arms.is_empty() { + return Ok(SingleExpressionInner::EnumMatch(EnumMatch { + scrutinee, + arms: Arc::from(enum_arms), + span, + })); + } + + if let Some(enum_arm) = enum_arms.first() { + // Mixed arms: name the clash instead of an arm-count error. + return Err(Box::new(( + Error::Grammar { + msg: format!( + "match arms mix the enum variant pattern `{}` \ + with the built-in pattern `{}`", + enum_arm, builtin_arms[0].pattern + ), + } + .with_span(span), + placeholder_match(scrutinee, span), + ))); + } - Self { - scrutinee, - left, - right, - span: e.span(), - } - } - _ => { - let match_arm_fallback = MatchArm { - expression: Arc::new(Expression::empty(Span::DUMMY)), - pattern: MatchPattern::False, - }; + // Binary match: exactly 2 built-in arms, in either order. + let Ok([first, second]) = <[MatchArm; 2]>::try_from(builtin_arms) else { + return Err(Box::new(( + Error::Grammar { + msg: "binary match requires exactly 2 arms".to_string(), + } + .with_span(span), + placeholder_match(scrutinee, span), + ))); + }; - let (left, right) = ( - arms.0.unwrap_or(match_arm_fallback.clone()), - arms.1.unwrap_or(match_arm_fallback.clone()), - ); - Self { - scrutinee, - left, - right, - span: e.span(), - } + let (left, right) = if patterns_in_canonical_order(&first.pattern, &second.pattern) { + (first, second) + } else if patterns_in_canonical_order(&second.pattern, &first.pattern) { + (second, first) + } else { + let error = Error::IncompatibleMatchArms { + first: Box::new(first.pattern.clone()), + second: Box::new(second.pattern.clone()), + } + .with_span(span); + // The arms still form a match; keep them so analysis can continue. + let node = SingleExpressionInner::Match(Match { + scrutinee, + left: first, + right: second, + span, + }); + return Err(Box::new((error, node))); + }; + + Ok(SingleExpressionInner::Match(Match { + scrutinee, + left, + right, + span, + })) +} + +/// Parser for `match` expressions. +/// +/// Handles both binary matches (exactly 2 arms: Left/Right, None/Some, +/// false/true) and enum matches (arms of the form `EnumName::Variant`). +/// Dispatches to [`Match`] or [`EnumMatch`] based on the patterns found. +fn match_expr_parser<'tokens, 'src: 'tokens, I, E>( + expr: E, +) -> impl Parser<'tokens, I, SingleExpressionInner, ParseError<'src>> + Clone +where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, +{ + let arms = delimited_with_recovery( + match_arm_parser(expr.clone()) + .repeated() + .collect::>(), + Token::LBrace, + Token::RBrace, + |_| vec![], + ); + + just(Token::Match) + .ignore_then(expr.map(Arc::new)) + .then(arms) + .validate(|(scrutinee, arms), e, emit| { + match assemble_match_arms(scrutinee, arms, e.span()) { + Ok(node) => node, + Err(boxed) => { + let (error, fallback) = *boxed; + emit.emit(error); + fallback } - }) - } + } + }) } impl Module { @@ -3104,7 +3333,12 @@ mod test { ); assert!(parsed_program.is_none()); - assert!(&diagnostics.to_string().contains("Expected ';', found '::'")); + assert!( + diagnostics + .to_string() + .contains("Expected identifier, found ::"), + "the second :: should be reported as the error site" + ); } /// Parse `input` and return whether it was rejected and the collected error text. From 6061667bf89502c4fd320c48fb2d3bac6482f4d1 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Sat, 18 Jul 2026 12:14:39 +0300 Subject: [PATCH 09/16] core: types: add variant info and base implementation in resolved type for enum --- src/types.rs | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 175 insertions(+), 1 deletion(-) diff --git a/src/types.rs b/src/types.rs index 84a599e6..a0173412 100644 --- a/src/types.rs +++ b/src/types.rs @@ -7,7 +7,7 @@ use simplicity::types::{CompleteBound, Final}; use crate::array::{BTreeSlice, Partition}; use crate::num::{NonZeroPow2Usize, Pow2Usize}; -use crate::str::AliasName; +use crate::str::{AliasName, Identifier}; use crate::unstable::impl_require_feature; /// Primitives of the SimplicityHL type system, excluding type aliases. @@ -30,6 +30,121 @@ pub enum TypeInner { List(A, NonZeroPow2Usize), } +/// One variant of a nominal enum type: its name and payload types. +/// +/// A variant with no payload types is a unit variant; a variant with +/// payloads carries a tuple of values of those types. +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub struct EnumVariantInfo { + name: Identifier, + payload: Arc<[ResolvedType]>, + /// The SimplicityHL type of the variant's contents: unit for unit + /// variants, the payload type itself for single payloads, a tuple + /// otherwise. Precomputed so it can be borrowed during destructuring. + payload_ty: ResolvedType, +} + +impl EnumVariantInfo { + // TODO(enum-stack): drop the allow once the ast commit uses the constructor. + #[allow(dead_code)] + pub(crate) fn new(name: Identifier, payload: Arc<[ResolvedType]>) -> Self { + let payload_ty = match payload.len() { + 0 => ResolvedType::unit(), + 1 => payload[0].clone(), + _ => ResolvedType::tuple(payload.iter().cloned()), + }; + Self { + name, + payload, + payload_ty, + } + } + + /// Access the name of the variant. + pub const fn name(&self) -> &Identifier { + &self.name + } + + /// Access the payload types of the variant, in declaration order. + /// Empty for unit variants. + pub fn payload(&self) -> &[ResolvedType] { + &self.payload + } + + /// The SimplicityHL type of the variant's contents, as one type. + pub fn payload_type(&self) -> &ResolvedType { + &self.payload_ty + } + + /// The structural type of the variant's contents: the leaf this + /// variant occupies in the enum's balanced sum. + pub(crate) fn structural_payload(&self) -> StructuralType { + StructuralType::from(&self.payload_ty) + } +} + +/// Definition of a nominal enum type: its name and variants in +/// declaration order. +/// +/// An enum with `n` variants is represented as a balanced sum of its `n` +/// variant payload types (see [`BTreeSlice`] for the tree shape), so a value +/// of the type is exactly one of the `n` variants: an undeclared variant is +/// unrepresentable. A variant's position among the declared variants +/// determines its leaf in the sum; there is no separate discriminant. +/// +/// Identity is the declared name: enums may only be declared at the top +/// level of the program's own files, so the name is unique program-wide and +/// serialized forms (such as the ABI) can identify an enum by it. +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub struct EnumInfo { + name: Arc, + variants: Arc<[EnumVariantInfo]>, +} + +impl EnumInfo { + /// Create an enum definition with the given `name` and `variants`. + /// + /// `variants` must not be empty: a sum of zero types would be + /// uninhabited, which Simplicity's type algebra cannot express. + /// A single-variant enum is a named wrapper of its payload. + // TODO(enum-stack): drop the allow once the ast commit uses the constructor. + #[allow(dead_code)] + pub(crate) fn new(name: Arc, variants: Arc<[EnumVariantInfo]>) -> Self { + debug_assert!(!variants.is_empty()); + Self { name, variants } + } + + /// Access the declared name of the enum. + pub fn name(&self) -> &str { + &self.name + } + + /// Access the variants of the enum in declaration order. + pub fn variants(&self) -> &[EnumVariantInfo] { + &self.variants + } + + /// Get the variant with the given `name` and its position among the + /// declared variants. + /// + /// The position determines the variant's leaf in the balanced sum. + pub fn variant(&self, name: &Identifier) -> Option<(usize, &EnumVariantInfo)> { + self.variants + .iter() + .enumerate() + .find(|(_, v)| v.name() == name) + } + + /// The structural payload types of all variants, in declaration order: + /// the leaves of the enum's balanced sum. + pub(crate) fn structural_variants(&self) -> Vec { + self.variants + .iter() + .map(EnumVariantInfo::structural_payload) + .collect() + } +} + impl TypeInner { /// Helper method for displaying type primitives based on the number of yielded children. /// @@ -346,6 +461,34 @@ impl ResolvedType { } } +/// Nominal enum types. +/// +/// These methods are inherent rather than part of [`TypeConstructible`] and [`TypeDeconstructible`]. +/// Those traits model the structural type algebra that every type universe (aliased, resolved, structural) +/// shares, while a nominal enum exists only at the resolved level. +/// +/// At the structural level its identity is erased into a balanced sum, and at the source level enums +/// enter types by name only. +/// Keeping the constructor off the shared traits also means that only [`crate::ast`]'s scope +/// (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 { + todo!() + } + + /// Access the enum definition if this is an enum type. + pub const fn as_enum(&self) -> Option<&EnumInfo> { + todo!() + } + + /// 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()) + } +} + impl TypeConstructible for ResolvedType { fn either(left: Self, right: Self) -> Self { Self(TypeInner::Either(Arc::new(left), Arc::new(right))) @@ -1119,6 +1262,7 @@ impl StructuralType { #[cfg(test)] mod tests { use super::*; + use crate::str::Identifier; #[test] fn display_type() { @@ -1140,4 +1284,34 @@ mod tests { let either = ResolvedType::either(ResolvedType::unit(), ResolvedType::u32()); assert_eq!("Either<(), u32>", &either.to_string()); } + + #[test] + fn enum_variant_info_payload_types() { + let unit = EnumVariantInfo::new(Identifier::from_str_unchecked("Unit"), Arc::from([])); + assert_eq!(&ResolvedType::unit(), unit.payload_type()); + + let single = EnumVariantInfo::new( + Identifier::from_str_unchecked("Single"), + Arc::from([ResolvedType::boolean()]), + ); + assert_eq!(&ResolvedType::boolean(), single.payload_type()); + + let pair = EnumVariantInfo::new( + Identifier::from_str_unchecked("Pair"), + Arc::from([ResolvedType::boolean(), ResolvedType::boolean()]), + ); + assert_eq!( + &ResolvedType::tuple([ResolvedType::boolean(), ResolvedType::boolean()]), + pair.payload_type() + ); + + let info = EnumInfo::new(Arc::from("Test"), Arc::from([unit, single, pair])); + assert_eq!("Test", info.name()); + assert_eq!(3, info.structural_variants().len()); + let (index, variant) = info + .variant(&Identifier::from_str_unchecked("Pair")) + .expect("Pair is a declared variant"); + assert_eq!(2, index); + assert_eq!("Pair", variant.name().as_inner()); + } } From f5f797cd34008a2e8d3473f1e34abde8eba473c3 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Sat, 18 Jul 2026 12:24:28 +0300 Subject: [PATCH 10/16] core: types: add enum variant to the type inner --- src/types.rs | 41 ++++++++++++++++++++++++++++++++++++----- src/value.rs | 3 +++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/types.rs b/src/types.rs index a0173412..04eb6690 100644 --- a/src/types.rs +++ b/src/types.rs @@ -28,6 +28,9 @@ pub enum TypeInner { Array(A, usize), /// List of the same type List(A, NonZeroPow2Usize), + /// Nominal enum type, represented as a balanced sum of its variants' + /// payload types + Enum(EnumInfo), } /// One variant of a nominal enum type: its name and payload types. @@ -201,6 +204,7 @@ impl TypeInner { write!(f, ", {bound}>") } }, + TypeInner::Enum(info) => write!(f, "{}", info.name()), } } } @@ -473,13 +477,16 @@ 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 { - todo!() + pub const fn enumeration(info: EnumInfo) -> Self { + Self(TypeInner::Enum(info)) } /// Access the enum definition if this is an enum type. pub const fn as_enum(&self) -> Option<&EnumInfo> { - todo!() + match &self.0 { + TypeInner::Enum(info) => Some(info), + _ => None, + } } /// Check whether the type mentions an enum, at any nesting depth. @@ -568,7 +575,7 @@ impl TypeDeconstructible for ResolvedType { impl TreeLike for &ResolvedType { fn as_node(&self) -> Tree { match &self.0 { - TypeInner::Boolean | TypeInner::UInt(..) => Tree::Nullary, + TypeInner::Boolean | TypeInner::UInt(..) | TypeInner::Enum(..) => 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()), @@ -764,6 +771,10 @@ impl AliasedType { let element = output.pop().unwrap(); output.push(ResolvedType::list(element, *bound)); } + // There is no syntax for writing an enum type inline (enums enter aliased types only by name) + TypeInner::Enum(info) => { + output.push(ResolvedType::enumeration(info.clone())); + } }, } } @@ -797,6 +808,7 @@ impl_require_feature!(TypeInner> { Tuple(elements), Array(element, _), List(element, _), + Enum(_), }); impl TypeConstructible for AliasedType { @@ -889,7 +901,7 @@ impl TreeLike for &AliasedType { match &self.0 { AliasedInner::Alias(_) | AliasedInner::Builtin(_) => Tree::Nullary, AliasedInner::Inner(inner) => match inner { - TypeInner::Boolean | TypeInner::UInt(..) => Tree::Nullary, + TypeInner::Boolean | TypeInner::UInt(..) | TypeInner::Enum(..) => Tree::Nullary, TypeInner::Option(l) | TypeInner::Array(l, _) | TypeInner::List(l, _) => { Tree::Unary(l) } @@ -1190,6 +1202,9 @@ impl From<&ResolvedType> for StructuralType { let element = output.pop().unwrap(); output.push(StructuralType::list(element, *bound)); } + TypeInner::Enum(info) => { + output.push(StructuralType::balanced_sum(info.structural_variants())); + } } } debug_assert_eq!(output.len(), 1); @@ -1250,6 +1265,22 @@ impl TypeConstructible for StructuralType { } impl StructuralType { + /// The balanced sum of the given leaf types. + /// The structural type of an enum whose variants have these payload types. + /// The tree shape is the one of [`BTreeSlice`], values ([`StructuralValue::enum_injection`]) + /// and the match lowering navigate the same shape. + /// + /// ## Panics + /// + /// `leaves` is empty: a sum of zero types would be uninhabited. + /// + /// [`StructuralValue::enum_injection`]: crate::value::StructuralValue + pub(crate) fn balanced_sum(leaves: Vec) -> Self { + BTreeSlice::from_slice(&leaves) + .fold(Self::either) + .expect("at least one leaf") + } + /// Convert into an unfinalized type that can be used in Simplicity's unification algorithm. pub fn to_unfinalized<'brand>( &self, diff --git a/src/value.rs b/src/value.rs index db319dfc..96e53741 100644 --- a/src/value.rs +++ b/src/value.rs @@ -748,6 +748,7 @@ impl Value { let integer = destruct::as_integer(value, *ty)?; output.push(Self::from(integer)); } + TypeInner::Enum(_info) => todo!(), TypeInner::Tuple(..) => { let elements = output.split_off(output.len() - size); debug_assert_eq!(elements.len(), size); @@ -808,6 +809,7 @@ impl crate::ArbitraryOfType for Value { match ty.as_inner() { TypeInner::Boolean => bool::arbitrary(u).map(Self::from), TypeInner::UInt(ty_int) => UIntValue::arbitrary_of_type(u, ty_int).map(Self::from), + TypeInner::Enum(_info) => todo!(), TypeInner::Either(ty_l, ty_r) => match u.int_in_range(0..=1)? { 0 => Self::arbitrary_of_type(u, ty_l) .map(|val_l| Self::left(val_l, ty_r.as_ref().clone())), @@ -1123,6 +1125,7 @@ impl TreeLike for Destructor<'_> { }; match ty.as_inner() { TypeInner::Boolean | TypeInner::UInt(..) => Tree::Nullary, + TypeInner::Enum(_info) => todo!(), TypeInner::Either(ty_l, ty_r) => match destruct::as_either(value) { Some(Either::Left(val_l)) => Tree::Unary(Self::new(val_l, ty_l)), Some(Either::Right(val_r)) => Tree::Unary(Self::new(val_r, ty_r)), From 727ecfa08203785e8cbbd463806b396d2a40e6ce Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Sat, 18 Jul 2026 12:34:31 +0300 Subject: [PATCH 11/16] core: value: add enum variant to the value inner --- src/array.rs | 21 +++++++-- src/value.rs | 123 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 136 insertions(+), 8 deletions(-) diff --git a/src/array.rs b/src/array.rs index 227af3bd..d95a1a28 100644 --- a/src/array.rs +++ b/src/array.rs @@ -57,15 +57,28 @@ impl BTreeSlice<'_, A> { } } +/// Index at which [`BTreeSlice`] splits a slice of length `n` into a left +/// and a right subtree. +/// +/// Exposed so that consumers which need to *navigate* the balanced tree. +/// +/// ## Panics +/// +/// `n` must be at least 2. +pub fn btree_split_index(n: usize) -> usize { + debug_assert!(2 <= n); + let next_pow2 = n.next_power_of_two(); + debug_assert!(0 < next_pow2 / 2); + debug_assert!(0 < n - next_pow2 / 2); + n - next_pow2 / 2 +} + impl TreeLike for BTreeSlice<'_, A> { fn as_node(&self) -> Tree { match self.0.len() { 0 | 1 => Tree::Nullary, n => { - let next_pow2 = n.next_power_of_two(); - debug_assert!(0 < next_pow2 / 2); - debug_assert!(0 < n - next_pow2 / 2); - let half = n - next_pow2 / 2; + let half = btree_split_index(n); let left = BTreeSlice::from_slice(&self.0[..half]); let right = BTreeSlice::from_slice(&self.0[half..]); Tree::Binary(left, right) diff --git a/src/value.rs b/src/value.rs index 96e53741..ded0f7c1 100644 --- a/src/value.rs +++ b/src/value.rs @@ -390,6 +390,10 @@ pub enum ValueInner { /// Each element must have the same type. // FIXME: Prevent construction of invalid lists (that run out of bounds) List(Arc<[Value]>, NonZeroPow2Usize), + /// Enum variant, as its position among the declared variants and its payload values (empty for unit variants). + /// + /// The enum definition lives in the type of the value. + Enum(usize, Arc<[Value]>), } /// A SimplicityHL value. @@ -410,7 +414,8 @@ impl TreeLike for &Value { | ValueInner::Option(Some(l)) => Tree::Unary(l), ValueInner::Tuple(elements) | ValueInner::Array(elements) - | ValueInner::List(elements, _) => Tree::Nary(elements.iter().collect()), + | ValueInner::List(elements, _) + | ValueInner::Enum(_, elements) => Tree::Nary(elements.iter().collect()), } } } @@ -452,6 +457,22 @@ impl fmt::Display for Value { write!(f, "{integer}")? } } + ValueInner::Enum(index, payload) => { + match data.n_children_yielded { + 0 => { + let info = data.node.ty().as_enum().expect("value is type-checked"); + write!(f, "{}::{}", info.name(), info.variants()[*index].name())?; + if !payload.is_empty() { + f.write_str("(")?; + } + } + _ if !data.is_complete => f.write_str(", ")?, + _ => {} + } + if data.is_complete && !payload.is_empty() { + f.write_str(")")?; + } + } ValueInner::Tuple(tuple) => { if data.n_children_yielded == 0 { write!(f, "(")?; @@ -748,7 +769,26 @@ impl Value { let integer = destruct::as_integer(value, *ty)?; output.push(Self::from(integer)); } - TypeInner::Enum(_info) => todo!(), + TypeInner::Enum(info) => { + // The destructor yields one child: the payload value at + // the variant's leaf, reconstructed at the payload type. + debug_assert_eq!(size, 1); + let payload_value = output.pop().unwrap(); + let (index, _) = destruct::as_enum_leaf(value, info.variants().len())?; + let arity = info.variants()[index].payload().len(); + let payload: Arc<[Self]> = match arity { + 0 => Arc::from([]), + 1 => Arc::from([payload_value]), + _ => match payload_value.inner { + ValueInner::Tuple(elements) => elements, + _ => return None, + }, + }; + output.push(Self { + inner: ValueInner::Enum(index, payload), + ty: ty.clone(), + }); + } TypeInner::Tuple(..) => { let elements = output.split_off(output.len() - size); debug_assert_eq!(elements.len(), size); @@ -809,7 +849,18 @@ impl crate::ArbitraryOfType for Value { match ty.as_inner() { TypeInner::Boolean => bool::arbitrary(u).map(Self::from), TypeInner::UInt(ty_int) => UIntValue::arbitrary_of_type(u, ty_int).map(Self::from), - TypeInner::Enum(_info) => todo!(), + TypeInner::Enum(info) => { + let index = u.int_in_range(0..=info.variants().len() - 1)?; + let payload = info.variants()[index] + .payload() + .iter() + .map(|payload_ty| Self::arbitrary_of_type(u, payload_ty)) + .collect::>>()?; + Ok(Self { + inner: ValueInner::Enum(index, payload), + ty: ty.clone(), + }) + } TypeInner::Either(ty_l, ty_r) => match u.int_in_range(0..=1)? { 0 => Self::arbitrary_of_type(u, ty_l) .map(|val_l| Self::left(val_l, ty_r.as_ref().clone())), @@ -1030,6 +1081,20 @@ impl From<&Value> for StructuralValue { } ValueInner::Boolean(bit) => output.push(Self::from(*bit)), ValueInner::UInt(integer) => output.push(Self::from(*integer)), + ValueInner::Enum(index, payload) => { + let info = data.node.ty().as_enum().expect("value is type-checked"); + let elements = output.split_off(output.len() - payload.len()); + let leaf = match elements.len() { + 0 => Self::unit(), + 1 => elements.into_iter().next().unwrap(), + _ => Self::tuple(elements), + }; + output.push(Self::enum_injection( + &info.structural_variants(), + *index, + leaf, + )); + } ValueInner::Tuple(_) => { let size = data.node.n_children(); let elements = output.split_off(output.len() - size); @@ -1063,6 +1128,30 @@ impl StructuralValue { self.as_ref().is_of_type(ty.as_ref()) } + /// The value of variant `index` of an enum whose variants have the given structural `leaves`. + /// The path to leaf `index` in the balanced sum of the leaf types, carrying `payload` at the leaf. + /// + /// The tree shape is the one of [`StructuralType::balanced_sum`], matching the structural type of the enum. + fn enum_injection(leaves: &[StructuralType], index: usize, payload: Self) -> Self { + debug_assert!(index < leaves.len()); + if leaves.len() == 1 { + return payload; + } + + let half = crate::array::btree_split_index(leaves.len()); + if index < half { + return Self::left( + Self::enum_injection(&leaves[..half], index, payload), + StructuralType::balanced_sum(leaves[half..].to_vec()), + ); + } + + Self::right( + StructuralType::balanced_sum(leaves[..half].to_vec()), + Self::enum_injection(&leaves[half..], index - half, payload), + ) + } + fn destruct<'a>(&'a self, ty: &'a ResolvedType) -> Destructor<'a> { Destructor::Ok { value: self.0.as_ref(), @@ -1125,7 +1214,12 @@ impl TreeLike for Destructor<'_> { }; match ty.as_inner() { TypeInner::Boolean | TypeInner::UInt(..) => Tree::Nullary, - TypeInner::Enum(_info) => todo!(), + 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())) + } + None => Tree::Unary(Self::WrongType), + }, TypeInner::Either(ty_l, ty_r) => match destruct::as_either(value) { Some(Either::Left(val_l)) => Tree::Unary(Self::new(val_l, ty_l)), Some(Either::Right(val_r)) => Tree::Unary(Self::new(val_r, ty_r)), @@ -1175,6 +1269,27 @@ mod destruct { use super::*; use simplicity::ValueRef; + /// Decode which leaf of a balanced sum of `n` types the value inhabits, + /// returning the leaf index and the value at the leaf. + /// + /// The tree shape is the one of [`BTreeSlice`](BTreeSlice), matching [`StructuralValue::enum_injection`]. + /// The leaf value is not type-checked here. + /// Callers destructure it at the variant's payload type. + pub fn as_enum_leaf(value: ValueRef, n: usize) -> Option<(usize, ValueRef)> { + debug_assert!(1 <= n); + if n == 1 { + return Some((0, value)); + } + + let half = crate::array::btree_split_index(n); + if let Some(val_l) = value.as_left() { + return as_enum_leaf(val_l, half); + } + + let val_r = value.as_right()?; + as_enum_leaf(val_r, n - half).map(|(index, leaf)| (index + half, leaf)) + } + pub fn as_bit(value: ValueRef) -> Option { match value.as_left() { Some(unit) if unit.is_unit() => Some(false), From 239bb37b344c28d977affcc3ccfa260fc3c46553 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Sat, 18 Jul 2026 12:38:45 +0300 Subject: [PATCH 12/16] core: value: add ability to build enum variant --- src/value.rs | 191 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 189 insertions(+), 2 deletions(-) diff --git a/src/value.rs b/src/value.rs index ded0f7c1..06f1123a 100644 --- a/src/value.rs +++ b/src/value.rs @@ -11,7 +11,7 @@ use crate::array::{BTreeSlice, Combiner, Partition, Unfolder}; use crate::error::{Diagnostic, Error, WithSpan}; use crate::num::{NonZeroPow2Usize, Pow2Usize, U256}; use crate::parse::ParseFromStr; -use crate::str::{Binary, Decimal, Hexadecimal}; +use crate::str::{Binary, Decimal, Hexadecimal, Identifier}; use crate::types::{ ResolvedType, StructuralType, TypeConstructible, TypeDeconstructible, TypeInner, UIntType, }; @@ -748,6 +748,33 @@ impl Value { output.pop() } + /// Create the value of the enum type's variant with the given `name`, + /// carrying the given `payload` values. + /// + /// Returns `None` if `ty` is not an enum type, has no variant `name`, + /// or `payload` does not match the variant's payload types in arity or type. + pub fn enum_variant(ty: &ResolvedType, name: &Identifier, payload: Vec) -> Option { + let info = ty.as_enum()?; + let (index, variant) = info.variant(name)?; + + if payload.len() != variant.payload().len() { + return None; + } + + if payload + .iter() + .zip(variant.payload()) + .any(|(value, declared)| value.ty() != declared) + { + return None; + } + + Some(Self { + inner: ValueInner::Enum(index, Arc::from(payload)), + ty: ty.clone(), + }) + } + /// Reconstruct the given structural value according to the given type. /// /// Return `None` if reconstructing fails. @@ -1355,7 +1382,167 @@ mod destruct { mod tests { use super::*; use crate::parse; - use crate::types::{StructuralType, TypeConstructible}; + use crate::types::{EnumInfo, EnumVariantInfo, StructuralType, TypeConstructible}; + + /// An enum type with `n` unit variants named `V0`, `V1`, ... + fn test_enum(n: usize) -> ResolvedType { + let variants: Arc<[EnumVariantInfo]> = (0..n) + .map(|i| { + EnumVariantInfo::new( + Identifier::from_str_unchecked(&format!("V{i}")), + Arc::from([]), + ) + }) + .collect(); + + ResolvedType::enumeration(EnumInfo::new(Arc::from("Test"), variants)) + } + + /// An enum with one unit variant, one single-payload variant and one + /// two-payload variant. + fn test_payload_enum() -> ResolvedType { + let variants: Arc<[EnumVariantInfo]> = Arc::from([ + EnumVariantInfo::new(Identifier::from_str_unchecked("Unit"), Arc::from([])), + EnumVariantInfo::new( + Identifier::from_str_unchecked("Single"), + Arc::from([ResolvedType::u8()]), + ), + EnumVariantInfo::new( + Identifier::from_str_unchecked("Pair"), + Arc::from([ResolvedType::u8(), ResolvedType::boolean()]), + ), + ]); + + ResolvedType::enumeration(EnumInfo::new(Arc::from("Test"), variants)) + } + + #[test] + fn enum_value_structural_round_trip() { + for n in [1, 2, 3, 4, 5, 7, 8, 256] { + let ty = test_enum(n); + let structural_ty = StructuralType::from(&ty); + + for index in 0..n { + let name = Identifier::from_str_unchecked(&format!("V{index}")); + let value = Value::enum_variant(&ty, &name, vec![]).expect("declared variant"); + + let structural = StructuralValue::from(&value); + assert!( + structural.is_of_type(&structural_ty), + "variant {index} of {n} inhabits the structural type" + ); + + let back = Value::reconstruct(&structural, &ty) + .expect("injection reconstructs at the enum type"); + assert_eq!(value, back, "variant {index} of {n} round-trips"); + } + } + } + + #[test] + fn enum_payload_value_structural_round_trip() { + let ty = test_payload_enum(); + let structural_ty = StructuralType::from(&ty); + + let values = [ + Value::enum_variant(&ty, &Identifier::from_str_unchecked("Unit"), vec![]), + Value::enum_variant( + &ty, + &Identifier::from_str_unchecked("Single"), + vec![Value::u8(42)], + ), + Value::enum_variant( + &ty, + &Identifier::from_str_unchecked("Pair"), + vec![Value::u8(7), Value::from(true)], + ), + ]; + + for value in values { + let value = value.expect("declared variant with matching payload"); + + let structural = StructuralValue::from(&value); + assert!( + structural.is_of_type(&structural_ty), + "payload variant inhabits the structural type: {value}" + ); + + let back = Value::reconstruct(&structural, &ty) + .expect("injection reconstructs at the enum type"); + assert_eq!(value, back, "payload variant round-trips"); + } + } + + #[test] + fn enum_value_display_and_constructor() { + let ty = test_enum(3); + let value = Value::enum_variant(&ty, &Identifier::from_str_unchecked("V1"), vec![]) + .expect("V1 is a variant"); + + assert_eq!("Test::V1", &value.to_string()); + assert!(value.is_of_type(&ty)); + assert!( + Value::enum_variant(&ty, &Identifier::from_str_unchecked("V9"), vec![]).is_none(), + "unknown variant name yields no value" + ); + + let ty = test_payload_enum(); + let single = Value::enum_variant( + &ty, + &Identifier::from_str_unchecked("Single"), + vec![Value::u8(42)], + ) + .expect("payload matches"); + assert_eq!("Test::Single(42)", &single.to_string()); + + let pair = Value::enum_variant( + &ty, + &Identifier::from_str_unchecked("Pair"), + vec![Value::u8(7), Value::from(true)], + ) + .expect("payload matches"); + assert_eq!("Test::Pair(7, true)", &pair.to_string()); + assert!( + Value::enum_variant(&ty, &Identifier::from_str_unchecked("Single"), vec![]).is_none(), + "wrong payload arity yields no value" + ); + assert!( + Value::enum_variant( + &ty, + &Identifier::from_str_unchecked("Single"), + vec![Value::from(true)] + ) + .is_none(), + "wrong payload type yields no value" + ); + } + + #[test] + fn enum_structural_type_is_balanced_sum() { + // Three unit variants: right-associative balanced tree (1, (1, 1)). + let ty = test_enum(3); + let structural = StructuralType::from(&ty); + let expected = StructuralType::either( + StructuralType::unit(), + StructuralType::either(StructuralType::unit(), StructuralType::unit()), + ); + assert_eq!(expected, structural); + + // Payload variants put their payload type at the leaf. + let ty = test_payload_enum(); + let structural = StructuralType::from(&ty); + let expected = StructuralType::either( + StructuralType::unit(), + StructuralType::either( + StructuralType::from(&ResolvedType::u8()), + StructuralType::tuple([ + StructuralType::from(&ResolvedType::u8()), + StructuralType::boolean(), + ]), + ), + ); + assert_eq!(expected, structural); + } #[test] fn display_value() { From 731235f43a1d249f27a7afb2ca56db96c749705a Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Sat, 18 Jul 2026 12:42:55 +0300 Subject: [PATCH 13/16] core: ast: handle enum declaration, match, and variant --- src/ast.rs | 746 ++++++++++++++++++++++++++++++++++++++++++++- src/compile/mod.rs | 2 + src/types.rs | 4 - src/value.rs | 14 +- 4 files changed, 749 insertions(+), 17 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index 5921f7c2..71eec55b 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1,5 +1,5 @@ use std::collections::hash_map::Entry; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::num::NonZeroUsize; use std::sync::Arc; @@ -16,7 +16,8 @@ use crate::parse::{MatchPattern, UseDecl, Visibility}; use crate::pattern::Pattern; use crate::str::{AliasName, FunctionName, Identifier, ModuleName, SymbolName, WitnessName}; use crate::types::{ - AliasedType, ResolvedType, StructuralType, TypeConstructible, TypeDeconstructible, UIntType, + AliasedType, EnumInfo, EnumVariantInfo, ResolvedType, StructuralType, TypeConstructible, + TypeDeconstructible, UIntType, }; use crate::value::{UIntValue, Value}; use crate::witness::{Parameters, WitnessTypes}; @@ -72,6 +73,11 @@ pub enum Item { /// /// A stub because the alias was resolved during the creation of the AST. TypeAlias, + /// An enum declaration. + /// + /// A stub because the declaration was resolved into scope during the + /// creation of the AST. + EnumDeclaration, /// A function. Function(Function), Use, @@ -242,6 +248,12 @@ pub enum SingleExpressionInner { Call(Call), /// Match expression. Match(Match), + /// Match expression over an enum's variants. + EnumMatch(EnumMatch), + /// Construction of an enum variant. + /// + /// The enum's definition lives in the type of the expression. + EnumConstruction(EnumConstruction), } /// Call of a user-defined or of a builtin function. @@ -529,6 +541,7 @@ pub enum ExprTree<'a> { Single(&'a SingleExpression), Call(&'a Call), Match(&'a Match), + EnumMatch(&'a EnumMatch), } impl TreeLike for ExprTree<'_> { @@ -569,6 +582,14 @@ impl TreeLike for ExprTree<'_> { } S::Call(call) => Tree::Unary(Self::Call(call)), S::Match(match_) => Tree::Unary(Self::Match(match_)), + S::EnumMatch(enum_match) => Tree::Unary(Self::EnumMatch(enum_match)), + S::EnumConstruction(construction) => Tree::Nary( + construction + .payload() + .iter() + .map(|arg| Self::Expression(arg)) + .collect(), + ), }, Self::Call(call) => Tree::Nary(call.args().iter().map(Self::Expression).collect()), Self::Match(match_) => Tree::Nary(Arc::new([ @@ -576,6 +597,16 @@ impl TreeLike for ExprTree<'_> { Self::Expression(match_.left().expression()), Self::Expression(match_.right().expression()), ])), + Self::EnumMatch(enum_match) => Tree::Nary( + std::iter::once(Self::Expression(enum_match.scrutinee())) + .chain( + enum_match + .arms() + .iter() + .map(|arm| Self::Expression(arm.body())), + ) + .collect(), + ), } } } @@ -1006,21 +1037,59 @@ impl Scope { ty.resolve(|name| self.get_alias(name)) } + /// 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) { + return Err(Error::RedefinedAlias { name: name.clone() }); + } + + Ok(()) + } + /// Insert a type alias into the current module scope. /// /// ## Errors /// /// * [`Error::RedefinedAlias`]: The alias name is already defined in the current scope. pub fn insert_alias(&mut self, alias: parse::TypeAlias) -> Result<(), Error> { - let name = alias.name().clone(); - if self.current_module().aliases.contains_key(&name) { - return Err(Error::RedefinedAlias { name }); - } + self.check_alias_free(alias.name())?; let resolved = self.resolve(alias.ty())?; + + self.current_module_mut() + .aliases + .insert(alias.name().clone(), (resolved, alias.visibility().clone())); + + Ok(()) + } + + /// Insert an enum declaration into the current module. + /// + /// An enum is a type alias for a nominal enum type, so its name resolves as a type + /// and its identity travels wherever the alias is imported. + /// + /// Enums may only be declared at the top level of the program's own files + /// (the parser rejects declarations inside `mod` blocks, the driver rejects them in dependency files), + /// so the bare name is unique program-wide and identifies the enum in the ABI. + /// + /// ## Errors + /// + /// * [`Error::RedefinedAlias`]: The name is already defined in the current module. + pub fn insert_enum( + &mut self, + name: AliasName, + visibility: Visibility, + variants: Arc<[EnumVariantInfo]>, + ) -> Result<(), Error> { + self.check_alias_free(&name)?; + + let info = EnumInfo::new(Arc::from(name.as_inner()), variants); + let resolved = ResolvedType::enumeration(info); + self.current_module_mut() .aliases - .insert(name, (resolved, alias.visibility().clone())); + .insert(name, (resolved, visibility)); + Ok(()) } @@ -1215,7 +1284,49 @@ impl AbstractSyntaxTree for Item { scope.resolve_use(use_decl).with_span(use_decl)?; Ok(Self::Use) } - parse::Item::EnumDeclaration(_decl) => todo!(), + 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!( + "enum '{}' has duplicate variant name '{}'", + decl.name(), + v.name() + ), + }) + .with_span(decl); + } + } + + let variants = decl + .variants() + .iter() + .map(|v| { + let payload = v + .payload() + .iter() + .map(|ty| scope.resolve(ty)) + .collect::, Error>>() + .with_span(v)?; + Ok(EnumVariantInfo::new(v.name().clone(), payload)) + }) + .collect::, Diagnostic>>()?; + scope + .insert_enum(decl.name().clone(), decl.visibility().clone(), variants) + .with_span(decl)?; + + Ok(Self::EnumDeclaration) + } parse::Item::Module(module) => { scope .enter_module(module.name().clone(), module.visibility().clone()) @@ -1365,6 +1476,97 @@ impl Expression { } } +/// Analyze the construction of an enum variant, e.g. `Action::Refresh(sig, 3)`. +/// +/// Analysis is type-directed. The expected type must be an enum, and the written enum name must name it. +/// Either as an alias in scope or as the enum's declared name itself. +/// The latter needs no scope at all, which is what lets witness and argument files use the same syntax. +fn analyze_enum_construction( + construction: &parse::EnumConstruction, + ty: &ResolvedType, + scope: &mut Scope, +) -> Result { + let span = *construction.span(); + let Some(info) = ty.as_enum() else { + return Err(Error::ExpressionUnexpectedType { ty: ty.clone() }).with_span(span); + }; + + // The written name must be the expected enum's. + // Enums are declared at the top level, so only a single identifier can name one. + // An alias in scope must resolve to the expected type. + // Without a scope (witness and argument files) the declared name itself matches. + let written = construction.enum_path_string(); + let names_expected_enum = match construction.enum_path() { + [single] => { + let alias = AliasName::from_str_unchecked(single.as_inner()); + match scope.get_alias(&alias) { + Ok(resolved) if &resolved == ty => true, + Ok(resolved) => { + return Err(Error::ExpressionTypeMismatch { + expected: ty.clone(), + found: resolved, + }) + .with_span(span); + } + Err(_) => written == info.name(), + } + } + _ => false, + }; + if !names_expected_enum { + 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)?; + 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); + } + + let payload = construction + .args() + .iter() + .zip(variant.payload()) + .map(|(arg, payload_ty)| Expression::analyze(arg, payload_ty, scope).map(Arc::new)) + .collect::]>, Diagnostic>>()?; + + Ok(EnumConstruction { + variant_index, + payload, + span, + }) +} + +/// The given string does not name a variant of the enum. +fn enum_variant_error(found: &str, info: &EnumInfo) -> Error { + let variants = info + .variants() + .iter() + .map(|variant| variant.name().to_string()) + .collect::>() + .join(", "); + Error::Grammar { + msg: format!( + "`{found}` is not a variant of enum `{}`; expected one of: {variants}", + info.name() + ), + } +} + impl AbstractSyntaxTree for Expression { type From = parse::Expression; @@ -1562,8 +1764,13 @@ impl AbstractSyntaxTree for SingleExpression { parse::SingleExpressionInner::Match(match_) => { Match::analyze(match_, ty, scope).map(SingleExpressionInner::Match)? } - parse::SingleExpressionInner::EnumConstruction(_construction) => todo!(), - parse::SingleExpressionInner::EnumMatch(_enum_match) => todo!(), + parse::SingleExpressionInner::EnumConstruction(construction) => { + analyze_enum_construction(construction, ty, scope) + .map(SingleExpressionInner::EnumConstruction)? + } + parse::SingleExpressionInner::EnumMatch(enum_match) => { + EnumMatch::analyze(enum_match, ty, scope).map(SingleExpressionInner::EnumMatch)? + } }; Ok(Self { @@ -1574,6 +1781,178 @@ impl AbstractSyntaxTree for SingleExpression { } } +impl AbstractSyntaxTree for EnumMatch { + type From = parse::EnumMatch; + + fn analyze( + from: &Self::From, + ty: &ResolvedType, + scope: &mut Scope, + ) -> Result { + let arms = from.arms(); + let span = *from.span(); + debug_assert!(!arms.is_empty(), "the parser rejects empty enum matches"); + + let enum_name = arms[0].enum_path_string(); + let [single] = arms[0].enum_path() else { + return Err(Error::Grammar { + msg: format!( + "`{enum_name}` does not name an enum; enums are declared at the \ + top level, so match arms name them by a single identifier" + ), + }) + .with_span(span); + }; + let alias = AliasName::from_str_unchecked(single.as_inner()); + let enum_ty = scope.get_alias(&alias).with_span(span)?; + let info = match enum_ty.as_enum() { + Some(info) => info.clone(), + None => { + return Err(Error::Grammar { + msg: format!( + "`{enum_name}` is not an enum, so match arms of the form \ + `{enum_name}::Variant` cannot apply to it" + ), + }) + .with_span(span) + } + }; + + // One slot per variant, in declaration order. + // the order of the leaves of the enum's balanced sum. + let mut arms_by_index: Vec> = + vec![None; info.variants().len()]; + for arm in arms { + if arm.enum_path() != arms[0].enum_path() { + return Err(Error::Grammar { + msg: format!( + "all match arms must use the same enum; expected '{}', found '{}'", + enum_name, + arm.enum_path_string() + ), + }) + .with_span(span); + } + let (index, _) = info + .variant(arm.variant()) + .ok_or_else(|| Error::Grammar { + msg: format!( + "variant '{}' is not defined in enum '{}'", + arm.variant(), + enum_name + ), + }) + .with_span(span)?; + let slot = &mut arms_by_index[index]; + if slot.is_some() { + return Err(Error::Grammar { + msg: format!("duplicate arm for variant '{}'", arm.variant()), + }) + .with_span(span); + } + *slot = Some(arm); + } + + // One collect: Some(arms) iff every variant is covered. + let covered: Option> = arms_by_index.iter().copied().collect(); + let Some(covered) = covered else { + let missing: Vec = arms_by_index + .iter() + .zip(info.variants()) + .filter(|(slot, _)| slot.is_none()) + .map(|(_, variant)| format!("'{}'", variant.name())) + .collect(); + return Err(Error::Grammar { + msg: format!( + "enum match on '{}' must cover all {} variants; missing: {}", + enum_name, + info.variants().len(), + missing.join(", ") + ), + }) + .with_span(span); + }; + + // Analyze the scrutinee against the nominal enum type, so that + // matching a value of a different enum (or any other type) against + // this enum's variants is a type error. + let scrutinee = Expression::analyze(from.scrutinee(), &enum_ty, scope).map(Arc::new)?; + + 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(); + let payload_ty = variant.payload_type(); + let typed_variables = pattern.is_of_type(payload_ty).with_span(span)?; + for (identifier, variable_ty) in typed_variables { + 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?, + }) + }) + .collect::, Diagnostic>>()?; + + Ok(Self { + scrutinee, + arms: arm_asts, + span, + }) + } +} + +/// Check an enum match arm's payload bindings against the variant's declared +/// payload types and combine them into one pattern for the variant's leaf. +/// +/// 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. +fn analyze_enum_arm_bindings( + arm: &parse::EnumMatchArm, + variant: &EnumVariantInfo, + scope: &Scope, + span: Span, +) -> Result { + if arm.bindings().len() != variant.payload().len() { + return Err(Error::Grammar { + msg: format!( + "variant '{}' of enum '{}' carries {} payload value(s), \ + but the arm binds {}", + arm.variant(), + arm.enum_path_string(), + variant.payload().len(), + arm.bindings().len() + ), + }) + .with_span(span); + } + + 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); + } + patterns.push(pattern.clone()); + } + + let pattern = match patterns.len() { + 0 => Pattern::Ignore, + 1 => patterns[0].clone(), + _ => Pattern::tuple(patterns), + }; + Ok(pattern) +} + impl AbstractSyntaxTree for Call { type From = parse::Call; @@ -1972,7 +2351,7 @@ mod scope_resolution_tests { let (graph, _ids, _dir, mut diagnostics) = setup_graph(files); let Some(driver_program) = graph.linearize_and_assemble(&mut diagnostics) else { - panic!("{}", &diagnostics); + return Err(diagnostics.render_to_string()); }; Program::analyze(&driver_program, Box::new(ElementsJetHinter)) @@ -2470,3 +2849,348 @@ mod module_tests { ); } } + +#[cfg(test)] +mod enum_tests { + use crate::ast::ElementsJetHinter; + use crate::{TemplateProgram, UnstableFeatures}; + + fn analyze(src: &str) -> Result<(), String> { + TemplateProgram::new_with_unstable( + src, + &UnstableFeatures::all(), + Box::new(ElementsJetHinter::new()), + ) + .map(|_| ()) + .map_err(|e| e.to_string()) + } + + #[test] + fn enum_declaration_registers_type_alias() { + let result = analyze( + "enum Color { Red, Green } + fn main() { let _x: Color = witness::C; }", + ); + assert!( + result.is_ok(), + "enum name should resolve as a type: {result:?}" + ); + } + + #[test] + fn enum_duplicate_variant_name_is_error() { + let result = analyze("enum Color { Red, Red }\nfn main() {}"); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("duplicate variant name")); + } + + #[test] + fn enum_variant_named_after_builtin_pattern_is_ok() { + // The written `Enum::Variant` form keeps `Action::None` distinct + // from the built-in option literal, so variant names are + // unrestricted. + let result = analyze( + "enum Action { None, Some, Other, } + fn main() { + match witness::W { + Action::None => {}, + Action::Some => {}, + Action::Other => {}, + } + }", + ); + assert!( + result.is_ok(), + "builtin-named variants should work: {result:?}" + ); + } + + #[test] + fn enum_empty_is_error() { + let result = analyze("enum Color { }\nfn main() {}"); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("at least one variant")); + } + + #[test] + fn enum_duplicate_name_is_error() { + let result = analyze( + "enum Color { Red, Green } + enum Color { Blue, Cyan } + fn main() {}", + ); + assert!(result.is_err(), "redefined enum name should error"); + } + + #[test] + fn enum_declaration_inside_module_errors() { + // 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() {}", + ); + let err = result.expect_err("enum inside `mod` must be rejected"); + assert!( + err.contains("top level"), + "error should say enums are top-level only: {err}" + ); + } + + #[test] + fn enum_declaration_in_dependency_errors() { + use crate::ast::scope_resolution_tests::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(); }", + ), + ( + "libs/lib/A.simf", + "pub enum Status { On, Off, } pub fn helper() {}", + ), + ]); + let err = result.expect_err("enums in dependency files must be rejected"); + assert!( + err.contains("dependency"), + "error should say enums cannot live in dependency files: {err}" + ); + } + + #[test] + 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 => {}, + } + }", + ); + assert!( + result.is_ok(), + "payload bindings should analyze: {result:?}" + ); + } + + #[test] + 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 => {}, + } + }", + ); + assert!( + result.is_err(), + "binding type must equal the declared payload type" + ); + } + + #[test] + 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 => {}, + } + }", + ); + assert!(result.is_err()); + assert!( + result.unwrap_err().contains("payload value"), + "error should describe the arity mismatch" + ); + } + + #[test] + fn enum_single_variant_matches() { + // 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 => {}, + } + }", + ); + assert!( + result.is_ok(), + "single-variant enum should work: {result:?}" + ); + } + + #[test] + fn enum_match_undefined_enum_is_error() { + let result = analyze( + "fn main() { + match witness::P { + Unknown::A => {}, + Unknown::B => {}, + } + }", + ); + assert!(result.is_err(), "undefined enum should error"); + } + + #[test] + 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 => {}, + } + }", + ); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("same enum")); + } + + #[test] + fn enum_match_unknown_variant_is_error() { + let result = analyze( + "enum A { X, Y } + fn main() { + match witness::W { + A::X => {}, + A::Z => {}, + } + }", + ); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("not defined")); + } + + #[test] + 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 => {}, + } + }", + ); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("duplicate arm")); + } + + #[test] + fn enum_match_missing_arm_is_error() { + let result = analyze( + "enum A { X, Y, Z } + fn main() { + match witness::W { + A::X => {}, + A::Y => {}, + } + }", + ); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("must cover all 3 variants")); + } + + #[test] + 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 => {}, + } + }", + ); + assert!( + result.is_err(), + "matching a value of enum A against B's variants must be a type error" + ); + } + + #[test] + 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 => {}, + } + }", + ); + assert!( + result.is_err(), + "matching a u8 against enum variants must be a type error" + ); + } + + #[test] + fn enum_match_rejects_same_shaped_enum() { + // Identity is the declaration site: two enums with the same variants + // 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 => {}, + } + }", + ); + assert!( + result.is_err(), + "structurally identical enums must not be interchangeable" + ); + } + + #[test] + fn enum_match_on_non_enum_alias_is_error() { + let result = analyze( + "type Foo = u32; + fn main() { + match witness::W { + Foo::A => {}, + Foo::B => {}, + } + }", + ); + assert!(result.is_err()); + assert!( + result.unwrap_err().contains("not an enum"), + "a defined non-enum alias should not report an undefined alias" + ); + } + + #[test] + fn enum_requires_unstable_feature() { + let result = TemplateProgram::new_with_unstable( + "enum Color { Red, Green }\nfn main() {}", + &UnstableFeatures::none(), + Box::new(ElementsJetHinter::new()), + ); + assert!(result.is_err(), "enum syntax is gated behind -Z enums"); + } +} diff --git a/src/compile/mod.rs b/src/compile/mod.rs index b79194f4..56b3693c 100644 --- a/src/compile/mod.rs +++ b/src/compile/mod.rs @@ -360,7 +360,9 @@ impl SingleExpression { inner.compile(scope).map(PairBuilder::injr)? } SingleExpressionInner::Call(call) => call.compile(scope)?, + SingleExpressionInner::EnumConstruction(_construction) => todo!(), SingleExpressionInner::Match(match_) => match_.compile(scope)?, + SingleExpressionInner::EnumMatch(_enum_match) => todo!(), }; scope diff --git a/src/types.rs b/src/types.rs index 04eb6690..4d61a1a0 100644 --- a/src/types.rs +++ b/src/types.rs @@ -48,8 +48,6 @@ pub struct EnumVariantInfo { } impl EnumVariantInfo { - // TODO(enum-stack): drop the allow once the ast commit uses the constructor. - #[allow(dead_code)] pub(crate) fn new(name: Identifier, payload: Arc<[ResolvedType]>) -> Self { let payload_ty = match payload.len() { 0 => ResolvedType::unit(), @@ -110,8 +108,6 @@ impl EnumInfo { /// `variants` must not be empty: a sum of zero types would be /// uninhabited, which Simplicity's type algebra cannot express. /// A single-variant enum is a named wrapper of its payload. - // TODO(enum-stack): drop the allow once the ast commit uses the constructor. - #[allow(dead_code)] pub(crate) fn new(name: Arc, variants: Arc<[EnumVariantInfo]>) -> Self { debug_assert!(!variants.is_empty()); Self { name, variants } diff --git a/src/value.rs b/src/value.rs index 06f1123a..49ebbc5a 100644 --- a/src/value.rs +++ b/src/value.rs @@ -696,7 +696,8 @@ impl Value { | ExprTree::Statement(..) | ExprTree::Assignment(..) | ExprTree::Call(..) - | ExprTree::Match(..) => return None, // not const + | ExprTree::Match(..) + | ExprTree::EnumMatch(..) => return None, // not const }; let size = data.node.n_children(); match single.inner() { @@ -705,7 +706,8 @@ impl Value { | S::Parameter(..) | S::Variable(..) | S::Call(..) - | S::Match(..) => return None, // not const + | S::Match(..) + | S::EnumMatch(..) => return None, // not const S::Expression(..) => continue, // skip S::Tuple(..) => { let elements = output.split_off(output.len() - size); @@ -742,6 +744,14 @@ impl Value { let inner = output.pop().unwrap(); output.push(Self::some(inner)); } + S::EnumConstruction(construction) => { + let payload = output.split_off(output.len() - size); + debug_assert_eq!(payload.len(), construction.payload().len()); + + let info = single.ty().as_enum().expect("value is type-checked"); + let name = info.variants()[construction.variant_index()].name().clone(); + output.push(Self::enum_variant(single.ty(), &name, payload)?); + } } } debug_assert_eq!(output.len(), 1); From 88aa49240d31b7248072f437cda0868540221953 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Sat, 18 Jul 2026 12:50:34 +0300 Subject: [PATCH 14/16] core: compile: handle enum compilation via balance sum prop --- src/compile/mod.rs | 79 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/src/compile/mod.rs b/src/compile/mod.rs index 56b3693c..ed16801a 100644 --- a/src/compile/mod.rs +++ b/src/compile/mod.rs @@ -11,8 +11,8 @@ use simplicity::{types, Cmr, FailEntropy}; use self::builtins::array_fold; use crate::array::{BTreeSlice, Partition}; use crate::ast::{ - Call, CallName, Expression, ExpressionInner, JetHinter, Match, Program, SingleExpression, - SingleExpressionInner, Statement, + Call, CallName, EnumMatch, Expression, ExpressionInner, JetHinter, Match, Program, + SingleExpression, SingleExpressionInner, Statement, }; use crate::debug::CallTracker; use crate::error::{Diagnostic, Error, Span, WithSpan}; @@ -360,9 +360,26 @@ impl SingleExpression { inner.compile(scope).map(PairBuilder::injr)? } SingleExpressionInner::Call(call) => call.compile(scope)?, - SingleExpressionInner::EnumConstruction(_construction) => todo!(), + SingleExpressionInner::EnumConstruction(construction) => { + let info = self + .ty() + .as_enum() + .expect("construction is type-checked at enum type"); + // Payload product: + // unit for unit variants, the expression itself for single payloads, + // a balanced pair tree for tuples (the same shape as the variant's structural payload type) + let compiled = construction + .payload() + .iter() + .map(|arg| arg.compile(scope)) + .collect::>, Diagnostic>>()?; + let payload = BTreeSlice::from_slice(&compiled) + .fold(PairBuilder::pair) + .unwrap_or_else(|| PairBuilder::unit(scope.ctx())); + inject_variant(construction.variant_index(), info.variants().len(), payload) + } SingleExpressionInner::Match(match_) => match_.compile(scope)?, - SingleExpressionInner::EnumMatch(_enum_match) => todo!(), + SingleExpressionInner::EnumMatch(enum_match) => enum_match.compile(scope)?, }; scope @@ -688,3 +705,57 @@ impl Match { input.comp(&output).with_span(self) } } + +/// Wrap a compiled payload in the injections that place it at leaf `index` of a balanced sum of `n` variants. +/// +/// The sibling types are pinned by type inference where the value is consumed. +fn inject_variant(index: usize, n: usize, payload: PairBuilder) -> PairBuilder { + debug_assert!(index < n); + if n == 1 { + return payload; + } + let half = crate::array::btree_split_index(n); + if index < half { + return inject_variant(index, half, payload).injl(); + } + inject_variant(index - half, n - half, payload).injr() +} + +/// Fold compiled match arms into a tree of `case` nodes that dispatches on a balanced sum. +/// +/// The tree shape is the one of [`BTreeSlice`], matching the structural type of the enum being matched. +/// Each `case` peels one level of the sum, so every leaf arm sees its (unit) variant payload on top of the environment. +fn case_tree<'brand>( + arms: &[PairBuilder>], +) -> Result, types::Error> { + let leaves: Vec> = + arms.iter().map(|arm| Ok(arm.as_ref().clone())).collect(); + BTreeSlice::from_slice(&leaves) + .fold(|left, right| ProgNode::case(&left?, &right?)) + .expect("enum matches have at least one arm") +} + +impl EnumMatch { + fn compile<'brand>( + &self, + scope: &mut Scope<'brand>, + ) -> Result>, Diagnostic> { + // Compile each arm with its payload pattern on top of the environment. + // `case` replaces the matched sum with the arm's payload, so every leaf sees (payload, environment) + // regardless of its depth. + // Unit variants bind nothing (`Pattern::Ignore`). + let mut arm_nodes = Vec::with_capacity(self.arms().len()); + for arm in self.arms() { + scope.push_scope(); + scope.insert(arm.pattern().clone()); + let body = arm.body().compile(scope)?; + scope.pop_scope(); + arm_nodes.push(body); + } + + let dispatch = case_tree(&arm_nodes).with_span(self)?; + let scrutinee = self.scrutinee().compile(scope)?; + let input = scrutinee.pair(PairBuilder::iden(scope.ctx())); + input.comp(&dispatch).with_span(self) + } +} From db5f5df9d86b49f5da0ffcf08fc840a2b575e944 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Sat, 18 Jul 2026 13:25:00 +0300 Subject: [PATCH 15/16] core: serde: adjust (de)serialization for enums --- src/serde.rs | 180 ++++++++++++++++++++++++++++++++++++++++++++++++- src/witness.rs | 103 ++++++++++++++++++++++++++-- 2 files changed, 277 insertions(+), 6 deletions(-) diff --git a/src/serde.rs b/src/serde.rs index 8f54f272..99bdc8f5 100644 --- a/src/serde.rs +++ b/src/serde.rs @@ -81,6 +81,8 @@ impl<'de> Deserialize<'de> for UnresolvedValue { where D: Deserializer<'de>, { + // deserialize_any tells a bare string and a { value, type } map apart. + // This requires a self-describing format (JSON). deserializer.deserialize_any(UnresolvedValueVisitor) } } @@ -101,6 +103,8 @@ impl Serialize for ResolvedType { where S: Serializer, { + // Enum mentions serialize as the enum's declared name, which is opaque. + // Variants and wire encoding live in the program's declarations. serializer.serialize_str(&self.to_string()) } } @@ -176,6 +180,16 @@ impl<'de> de::Visitor<'de> for ValueMapVisitor { formatter.write_str("a map with \"value\" and \"type\" fields") } + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + Err(de::Error::custom(format!( + "cannot deserialize the bare value `{value}` without the program's declared types; \ + deserialize into `UnresolvedValues` and resolve it against the program" + ))) + } + fn visit_map(self, mut access: M) -> Result where M: de::MapAccess<'de>, @@ -219,7 +233,9 @@ impl<'de> Deserialize<'de> for Value { where D: Deserializer<'de>, { - deserializer.deserialize_map(ValueMapVisitor) + // deserialize_any lets a bare string (the form enum values serialize as) reach visit_str's directed error. + // Self-describing formats only, as in `UnresolvedValue::deserialize`. + deserializer.deserialize_any(ValueMapVisitor) } } @@ -258,6 +274,17 @@ impl<'a> Serialize for WitnessMapSerializer<'a> { { let mut map = serializer.serialize_map(Some(self.0.len()))?; for (name, value) in self.0 { + // Enum values serialize as their bare value string. + // The { value, type } form cannot express them, since its type + // string is parsed without the program's declarations. + // Such values round-trip through `UnresolvedValues`, not through `Deserialize`. + // + // TODO: Consider serializing every value as a bare string and retiring the { value, type } form. + // That drops "witness file readable without the program" entirely. + if value.ty().contains_enum() { + map.serialize_entry(name.as_inner(), &value.to_string())?; + continue; + } map.serialize_entry(name.as_inner(), &ValueMapSerializer(value))?; } map.end() @@ -312,4 +339,155 @@ mod tests { Err(error) => assert!(error.to_string().contains("Name `A` is assigned twice")), } } + + fn unit_enum(name: &str, variants: &[&str]) -> ResolvedType { + use crate::str::Identifier; + use crate::types::{EnumInfo, EnumVariantInfo}; + use std::sync::Arc; + + let variants: Arc<[EnumVariantInfo]> = variants + .iter() + .map(|name| EnumVariantInfo::new(Identifier::from_str_unchecked(name), Arc::from([]))) + .collect(); + ResolvedType::enumeration(EnumInfo::new(Arc::from(name), variants)) + } + + #[test] + fn abi_enum_type_serializes_as_name() { + use crate::str::WitnessName; + use crate::types::TypeConstructible; + + let action_ty = unit_enum("Action", &["Inherit", "ColdSpend"]); + let witness_types = WitnessTypes::from(HashMap::from([ + (WitnessName::from_str_unchecked("ACTION"), action_ty.clone()), + ( + WitnessName::from_str_unchecked("MAYBE"), + ResolvedType::option(action_ty.clone()), + ), + ( + WitnessName::from_str_unchecked("PAIR"), + ResolvedType::tuple([action_ty, unit_enum("Reaction", &["Fast", "Slow"])]), + ), + ( + WitnessName::from_str_unchecked("PLAIN"), + crate::parse::ParseFromStr::parse_from_str("u32").unwrap(), + ), + ])); + + let json = serde_json::to_value(&witness_types).unwrap(); + assert_eq!(json["ACTION"], "Action"); + assert_eq!(json["MAYBE"], "Option"); + assert_eq!(json["PAIR"], "(Action, Reaction)"); + assert_eq!(json["PLAIN"], "u32"); + } + + #[test] + fn enum_witness_value_serializes_as_variant_name() { + use crate::str::{Identifier, WitnessName}; + + let action_ty = unit_enum("Action", &["Inherit", "ColdSpend"]); + let value = Value::enum_variant( + &action_ty, + &Identifier::from_str_unchecked("ColdSpend"), + vec![], + ) + .unwrap(); + let witness = WitnessValues::from(HashMap::from([( + WitnessName::from_str_unchecked("ACTION"), + value, + )])); + + // Serializes in the bare form that UnresolvedValues can resolve back. + let json = serde_json::to_value(&witness).unwrap(); + assert_eq!(json["ACTION"], "Action::ColdSpend"); + + // The bare form round-trips through UnresolvedValues + resolve. + let text = serde_json::to_string(&witness).unwrap(); + let unresolved: UnresolvedValues = serde_json::from_str(&text).unwrap(); + let witness_types = WitnessTypes::from(HashMap::from([( + WitnessName::from_str_unchecked("ACTION"), + action_ty, + )])); + let round_tripped: WitnessValues = unresolved.resolve(&witness_types).unwrap(); + assert_eq!(witness, round_tripped); + + // Deserializing it as WitnessValues directly cannot work without the + // program's types; the error says where to go instead. + let err = serde_json::from_str::(&text).unwrap_err(); + assert!( + err.to_string().contains("UnresolvedValues"), + "error should point at the resolution path: {err}" + ); + } + + #[test] + fn payload_enum_witness_value_round_trips() { + use crate::str::{Identifier, WitnessName}; + use crate::types::{EnumInfo, EnumVariantInfo}; + use crate::value::ValueConstructible; + use std::sync::Arc; + + let u32_ty: ResolvedType = ParseFromStr::parse_from_str("u32").unwrap(); + let variants: Arc<[EnumVariantInfo]> = Arc::from([ + EnumVariantInfo::new(Identifier::from_str_unchecked("Cold"), Arc::from([])), + EnumVariantInfo::new( + Identifier::from_str_unchecked("Refresh"), + Arc::from([u32_ty.clone()]), + ), + ]); + let action_ty = ResolvedType::enumeration(EnumInfo::new(Arc::from("Action"), variants)); + let value = Value::enum_variant( + &action_ty, + &Identifier::from_str_unchecked("Refresh"), + vec![Value::u32(42)], + ) + .unwrap(); + let witness = WitnessValues::from(HashMap::from([( + WitnessName::from_str_unchecked("ACTION"), + value, + )])); + + // Payload variants serialize as their display form and round-trip through UnresolvedValues + resolve. + let json = serde_json::to_value(&witness).unwrap(); + assert_eq!(json["ACTION"], "Action::Refresh(42)"); + + let text = serde_json::to_string(&witness).unwrap(); + let unresolved: UnresolvedValues = serde_json::from_str(&text).unwrap(); + let witness_types = WitnessTypes::from(HashMap::from([( + WitnessName::from_str_unchecked("ACTION"), + action_ty, + )])); + let round_tripped: WitnessValues = unresolved.resolve(&witness_types).unwrap(); + assert_eq!(witness, round_tripped); + } + + #[test] + fn nested_enum_witness_value_serializes_as_bare_string() { + use crate::str::{Identifier, WitnessName}; + use crate::types::TypeConstructible; + use crate::value::ValueConstructible; + + let action_ty = unit_enum("Action", &["Hot", "Cold"]); + let option_ty = ResolvedType::option(action_ty.clone()); + let cold = Value::enum_variant(&action_ty, &Identifier::from_str_unchecked("Cold"), vec![]) + .unwrap(); + let witness = WitnessValues::from(HashMap::from([( + WitnessName::from_str_unchecked("MAYBE"), + Value::some(cold), + )])); + + // A nested enum value must not fall into the { value, type } form, + // whose type string cannot be parsed without the program. + let json = serde_json::to_value(&witness).unwrap(); + assert_eq!(json["MAYBE"], "Some(Action::Cold)"); + + let text = serde_json::to_string(&witness).unwrap(); + let unresolved: UnresolvedValues = serde_json::from_str(&text).unwrap(); + let witness_types = WitnessTypes::from(HashMap::from([( + WitnessName::from_str_unchecked("MAYBE"), + option_ty, + )])); + let round_tripped: WitnessValues = unresolved.resolve(&witness_types).unwrap(); + assert_eq!(witness, round_tripped); + } } diff --git a/src/witness.rs b/src/witness.rs index 9977be96..bf3bc047 100644 --- a/src/witness.rs +++ b/src/witness.rs @@ -161,14 +161,14 @@ impl UnresolvedValues { /// Resolve each value against the type that the program declares for its name. /// - /// Bare value strings whose name the program does not declare are skipped. + /// Bare value strings are parsed at the declared type. + /// Names the program does not declare are skipped. + /// Self-typed entries pass through unchanged and are type-checked later, + /// when the program is instantiated/satisfied. /// /// ## Errors /// - /// - A bare value string does not parse at the declared type. - /// - /// Self-typed entries are passed through unchanged. - /// They are type-checked against the program later, when the program is instantiated/satisfied. + /// A bare value string does not parse at the declared type. pub fn resolve(self, declared_types: &M) -> Result where T: From>, @@ -278,6 +278,10 @@ mod tests { use super::*; use crate::ast::ElementsJetHinter; use crate::parse::ParseFromStr; + #[cfg(feature = "serde")] + use crate::str::Identifier; + #[cfg(feature = "serde")] + use crate::types::{EnumInfo, EnumVariantInfo, TypeConstructible}; use crate::value::ValueConstructible; use crate::{ast, parse, CompiledProgram, SatisfiedProgram}; @@ -427,6 +431,95 @@ fn main() { assert!(serde_json::from_str::(dup).is_err()); } + #[test] + #[cfg(feature = "serde")] + fn enum_witness_resolves_by_variant_name() { + let variants: Arc<[EnumVariantInfo]> = ["Inherit", "ColdSpend", "HotSpend"] + .into_iter() + .map(|name| EnumVariantInfo::new(Identifier::from_str_unchecked(name), Arc::from([]))) + .collect(); + let action_ty = ResolvedType::enumeration(EnumInfo::new(Arc::from("Action"), variants)); + let witness_types = WitnessTypes::from(HashMap::from([( + WitnessName::from_str_unchecked("ACTION"), + action_ty.clone(), + )])); + + let resolve_one = |input: &str| -> Result { + let unresolved = UnresolvedValues::from_map(HashMap::from([( + WitnessName::from_str_unchecked("ACTION"), + UnresolvedValue::Untyped(input.to_string()), + )])); + let resolved: WitnessValues = unresolved.resolve(&witness_types)?; + Ok(resolved + .get(&WitnessName::from_str_unchecked("ACTION")) + .unwrap() + .clone()) + }; + + let by_name = resolve_one("Action::ColdSpend").expect("written variant resolves"); + assert!(by_name.is_of_type(&action_ty)); + + // The bare form is no longer a value: variants are written with + // their enum's name, the same syntax as in source code. + assert!(resolve_one("ColdSpend").is_err()); + + let err = resolve_one("Action::Withdraw").unwrap_err(); + assert!( + err.contains("Withdraw") && err.contains("ColdSpend"), + "error names the bad value and the variants: {err}" + ); + + assert!(resolve_one("2").is_err()); + } + + #[test] + #[cfg(feature = "serde")] + fn enum_witness_resolves_inside_composite_types() { + let variants: Arc<[EnumVariantInfo]> = ["Hot", "Cold"] + .into_iter() + .map(|name| EnumVariantInfo::new(Identifier::from_str_unchecked(name), Arc::from([]))) + .collect(); + let action_ty = ResolvedType::enumeration(EnumInfo::new(Arc::from("Action"), variants)); + let option_ty = ResolvedType::option(action_ty.clone()); + let tuple_ty = ResolvedType::tuple([ + action_ty.clone(), + ResolvedType::parse_from_str("u32").unwrap(), + ]); + let witness_types = WitnessTypes::from(HashMap::from([ + (WitnessName::from_str_unchecked("MAYBE"), option_ty), + (WitnessName::from_str_unchecked("PAIR"), tuple_ty), + ])); + + let unresolved = UnresolvedValues::from_map(HashMap::from([ + ( + WitnessName::from_str_unchecked("MAYBE"), + UnresolvedValue::Untyped("Some(Action::Cold)".to_string()), + ), + ( + WitnessName::from_str_unchecked("PAIR"), + UnresolvedValue::Untyped("(Action::Hot, 42)".to_string()), + ), + ])); + let resolved: WitnessValues = unresolved + .resolve(&witness_types) + .expect("variants resolve inside options and tuples"); + let maybe = resolved + .get(&WitnessName::from_str_unchecked("MAYBE")) + .unwrap(); + assert_eq!("Some(Action::Cold)", &maybe.to_string()); + let pair = resolved + .get(&WitnessName::from_str_unchecked("PAIR")) + .unwrap(); + assert_eq!("(Action::Hot, 42)", &pair.to_string()); + + // A bare variant name in source code stays undefined: only files parse enum values. + let err = ast::Expression::analyze_const( + &parse::Expression::parse_from_str("Cold").unwrap(), + &action_ty, + ); + assert!(err.is_err(), "bare variants are not source syntax"); + } + #[test] fn witness_to_string() { let witness = WitnessValues::from(HashMap::from([ From a72da029dd5d736284765d5d626b39daad39d045 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Sat, 18 Jul 2026 13:25:47 +0300 Subject: [PATCH 16/16] examples: migrate last_will to use enums, adjust test harness --- examples/last_will.inherit.wit | 5 +- examples/last_will.simf | 18 ++- src/lib.rs | 205 ++++++++++++++++++++++++++++++++- test-data/last_will.json | 2 +- 4 files changed, 216 insertions(+), 14 deletions(-) diff --git a/examples/last_will.inherit.wit b/examples/last_will.inherit.wit index 16752030..2e1e1725 100644 --- a/examples/last_will.inherit.wit +++ b/examples/last_will.inherit.wit @@ -1,6 +1,3 @@ { - "INHERIT_OR_NOT": { - "value": "Left(0x755201bb62b0a8b8d18fd12fc02951ea3998ba42bfc6664daaf8a0d2298cad43cdc21358c7c82f37654275dc2fea8c858adbe97bac92828b498a5a237004db6f)", - "type": "Either>" - } + "ACTION": "Action::Inherit(0x755201bb62b0a8b8d18fd12fc02951ea3998ba42bfc6664daaf8a0d2298cad43cdc21358c7c82f37654275dc2fea8c858adbe97bac92828b498a5a237004db6f)" } diff --git a/examples/last_will.simf b/examples/last_will.simf index 9790a1cf..d791e155 100644 --- a/examples/last_will.simf +++ b/examples/last_will.simf @@ -4,6 +4,8 @@ * The inheritor can spend the coins if the owner doesn't move the them for 180 * days. The owner has to repeat the covenant when he moves the coins with his * hot key. The owner can break out of the covenant with his cold key. + * + * Requires -Z enums to run this example. */ fn checksig(pk: Pubkey, sig: Signature) { let msg: u256 = jet::sig_all_hash(); @@ -40,12 +42,16 @@ fn refresh_spend(hot_sig: Signature) { recursive_covenant(); } +enum Action { + Inherit(Signature), + ColdSpend(Signature), + HotSpend(Signature), +} + fn main() { - match witness::INHERIT_OR_NOT { - Left(inheritor_sig: Signature) => inherit_spend(inheritor_sig), - Right(cold_or_hot: Either) => match cold_or_hot { - Left(cold_sig: Signature) => cold_spend(cold_sig), - Right(hot_sig: Signature) => refresh_spend(hot_sig), - }, + match witness::ACTION { + Action::Inherit(sig: Signature) => inherit_spend(sig), + Action::ColdSpend(sig: Signature) => cold_spend(sig), + Action::HotSpend(sig: Signature) => refresh_spend(sig), } } diff --git a/src/lib.rs b/src/lib.rs index a7516e84..a0b2386a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -726,6 +726,17 @@ pub(crate) mod tests { .with_arguments(Arguments::default()) } + pub fn program_text_with_unstable( + program_text: Cow, + unstable_features: UnstableFeatures, + ) -> Self { + TestCase::::template_text_with_unstable( + program_text, + unstable_features, + ) + .with_arguments(Arguments::default()) + } + pub fn program_file_with_deps_and_unstable( prog_path: impl AsRef, dependency_map: &DependencyMap, @@ -1148,7 +1159,7 @@ pub(crate) mod tests { #[test] #[cfg(feature = "serde")] fn last_will_inherit() { - TestCase::program_file("./examples/last_will.simf") + TestCase::program_file_with_unstable("./examples/last_will.simf", UnstableFeatures::all()) .with_sequence(25920) .print_sighash_all() .with_witness_file("./examples/last_will.inherit.wit") @@ -1378,6 +1389,10 @@ fn main() { } fn regression_test(name: &str) { + regression_test_with_features(name, crate::UnstableFeatures::none()); + } + + fn regression_test_with_features(name: &str, features: crate::UnstableFeatures) { let program = serde_json::from_str::( std::fs::read_to_string(format!("./test-data/{}.json", name)) .unwrap() @@ -1385,7 +1400,8 @@ fn main() { ) .unwrap(); - let test_case = TestCase::program_file(format!("./examples/{}.simf", name)); + let test_case = + TestCase::program_file_with_unstable(format!("./examples/{}.simf", name), features); match program.witness { Some(wit) => { let (new_program, new_witness) = test_case @@ -1453,7 +1469,7 @@ fn main() { #[test] fn last_will_regression() { - regression_test("last_will"); + regression_test_with_features("last_will", crate::UnstableFeatures::all()); } #[test] @@ -1560,6 +1576,189 @@ fn main() { err ); } + + #[test] + fn enum_construction_compiles_and_runs() { + let src = "enum Action { Refresh(u32, bool), Cold, } + fn pick() -> Action { + Action::Refresh(7, true) + } + fn main() { + let a: Action = pick(); + match a { + Action::Refresh(n: u32, b: bool) => { + assert!(jet::eq_32(n, 7)); + assert!(b); + }, + Action::Cold => assert!(false), + } + }"; + + TestCase::program_text_with_unstable(Cow::Borrowed(src), UnstableFeatures::all()) + .with_witness_values(WitnessValues::default()) + .assert_run_success(); + } + + #[test] + fn enum_unit_construction_compiles_and_runs() { + let src = "enum Action { Hot, Cold, } + fn main() { + let a: Action = Action::Cold; + match a { + Action::Hot => assert!(false), + Action::Cold => {}, + } + }"; + + TestCase::program_text_with_unstable(Cow::Borrowed(src), UnstableFeatures::all()) + .with_witness_values(WitnessValues::default()) + .assert_run_success(); + } + + #[test] + #[cfg(feature = "serde")] + fn enum_match_witness_file_variant_name() { + use crate::str::WitnessName; + + // The witness file names the variant; resolution constructs the + // enum value at the declared type. + let src = "enum Action { Hot, Cold, } + fn main() { + let x: u32 = match witness::ACT { + Action::Hot => 1, + Action::Cold => 2, + }; + assert!(jet::eq_32(x, 2)); + }"; + let compiled = CompiledProgram::new_with_unstable( + src, + &UnstableFeatures::all(), + Arguments::default(), + false, + Box::new(ElementsJetHinter::new()), + ) + .unwrap(); + let unresolved: UnresolvedValues = + serde_json::from_str(r#"{ "ACT": "Action::Cold" }"#).unwrap(); + let witness: WitnessValues = unresolved.resolve(compiled.witness_types()).unwrap(); + assert!(witness + .get(&WitnessName::from_str_unchecked("ACT")) + .is_some()); + TestCase::program_text_with_unstable(Cow::Borrowed(src), UnstableFeatures::all()) + .with_witness_values(witness) + .assert_run_success(); + } + + #[test] + fn strict_satisfy_rejects_missing_witness() { + use crate::str::{Identifier, WitnessName}; + use crate::value::ValueConstructible; + use std::collections::HashMap; + + let src = r#" +enum Branch { A, B } +fn main() { + match witness::SELECTOR { + Branch::A => assert!(jet::is_zero_32(witness::A)), + Branch::B => assert!(jet::is_zero_32(witness::B)), + } +} +"#; + let compiled = CompiledProgram::new_with_unstable( + src, + &UnstableFeatures::all(), + Arguments::default(), + false, + Box::new(ElementsJetHinter::new()), + ) + .unwrap(); + let selector_ty = compiled + .witness_types() + .get(&WitnessName::from_str_unchecked("SELECTOR")) + .unwrap() + .clone(); + + // Only SELECTOR and A are provided; B is omitted. The strict entry + // points must reject the omitted witness rather than zero-filling it. + let mut map: HashMap = HashMap::new(); + map.insert( + WitnessName::from_str_unchecked("SELECTOR"), + Value::enum_variant(&selector_ty, &Identifier::from_str_unchecked("A"), vec![]) + .unwrap(), + ); + map.insert(WitnessName::from_str_unchecked("A"), Value::u32(0)); + + let err = compiled + .satisfy(WitnessValues::from(map.clone())) + .expect_err("satisfy must reject a missing witness"); + assert!( + err.contains('B'), + "error should mention the missing witness B, got: {err}" + ); + } + + #[test] + fn enum_match_dispatches_every_variant() { + use crate::str::{Identifier, WitnessName}; + use std::collections::HashMap; + + // Three and five variants cover leaves at unequal depths + // of the balanced sum. + for (variants, arms) in [ + ("A,", vec!["A"]), + ("A, B, C,", vec!["A", "B", "C"]), + ("A, B, C, D, E,", vec!["A", "B", "C", "D", "E"]), + ] { + let arm_lines: String = arms + .iter() + .enumerate() + .map(|(i, name)| format!("Action::{} => {},\n", name, (i + 1) * 10)) + .collect(); + let src = format!( + "enum Action {{ {variants} }} + fn main() {{ + let selected: u32 = match witness::ACT {{ + {arm_lines} + }}; + assert!(jet::eq_32(selected, witness::EXPECTED)); + }}" + ); + + let compiled = CompiledProgram::new_with_unstable( + src.as_str(), + &UnstableFeatures::all(), + Arguments::default(), + false, + Box::new(ElementsJetHinter::new()), + ) + .unwrap(); + let action_ty = compiled + .witness_types() + .get(&WitnessName::from_str_unchecked("ACT")) + .expect("ACT is declared") + .clone(); + + for (i, name) in arms.iter().enumerate() { + let action = + Value::enum_variant(&action_ty, &Identifier::from_str_unchecked(name), vec![]) + .expect("declared variant"); + let expected = u32::try_from((i + 1) * 10).unwrap(); + let map = HashMap::from([ + (WitnessName::from_str_unchecked("ACT"), action), + ( + WitnessName::from_str_unchecked("EXPECTED"), + crate::value::ValueConstructible::u32(expected), + ), + ]); + TestCase::program_text_with_unstable( + Cow::Owned(src.clone()), + UnstableFeatures::all(), + ) + .with_witness_values(WitnessValues::from(map)) + .assert_run_success(); + } + } + } } #[cfg(test)] diff --git a/test-data/last_will.json b/test-data/last_will.json index 3dace1a8..3764d1b4 100644 --- a/test-data/last_will.json +++ b/test-data/last_will.json @@ -1,4 +1,4 @@ { - "program": "5wnQKEGJsWVABAmKSEGCrynMGLpUF69BbvwQFoAuY+y1ngQJfqSPabfWRZ9K3F2jdRYYBitLzfMz987l3WKtAxSudDhYOBTf5tlucUbKz5QK2LfAvMA1kChBh+DHCpJAk4cziqISK6EzABFXCwYvClhPYGFQusJfripGQssOAVt34AhgGJAoSQbgJxuBig/FJwqFobGHNddy8HoTqejIHGcv8bcleUZT57KmW1Vp7LXaMUR4qMQ4YBiE3n41BAOBgcOJFAGQOJwuLAGkHHAHHpBiBQbkHacYEf5RB7X1tMEVAbpXAfNhcd45LjO88p6usCblccJ7lByDCchhcRcOA4GJxgBwcGIGlafkwigGSWMMQSTRPhfidUim1MchFg2+ZsIYB8RO84Db5ByMCcj0kCT4YnM/BVazZBMsdgY/lS0WYcYfsNRJVmhtHQmf/PVrNEOe4wYBisgAAAAIGkKhambcTmCIv9QHGkTTAYXN78l9PRKwkaP7L+QgG2hgCZadW734oMAxC4AwcwLvfahR91ofRxdIEhoraXiTCljMruIwlAG9G26fy7ABhgGL4cEObxOhhI4BnviN4uejwVYdGCizvg8pDe+f7r9U2pQklHLAwDhITlckiyAAAAAAUKvhqObQQ6CxT1VyVCCLZUfrJhqhp/qbNkpewATHlLgTDwBZJgwDELSQkUM6IZzkLPP/t8aZ/NfTm5pw5IQ0J/duRiaFMIlp35sJi5uVAYBgObvJWcC0jz5LzKXn0/Nn/OQnPezJTiq+w46I+xAB5sdEwwDWQKEkH1m5aCgWDNug5tpVnanSODCL5dAHG0YZYXL7/GhLKIDD2ZYRxW4obTlfDAMQtRvoQT9zeJ/JSg1zVnOQ+dSDqBncb+M9zPuT55FUoWyEHDxeBVkAAAAAg4AFwB8NhzSoNzoMc1Ae/7Z55CGHj0gOG/ZVBjb5kbDqY2kAJh07WGAYhcGISKEGB4nAwHMDLqyIAl/t8mPC4rRGEfM1lVH7CAxNfptKDrOKJGwAHAYBrC4iNL6lcZ9HTU3CRlRyCoGVZzuEYuSi/jp604WRZrD9L3pYeQ4FCcTgcuhcLAcvAcWAchQuRgDmABzNhOZwDmDFyvAcwoOVQHLcHleBy6B5fgc34A==", + "program": "5vHQKEGJsWVABAmKSEGCrynMGLpUF69BbvwQFoAuY+y1ngQJfqSPabfWRZ9K3F2jdRYYBitLzfMz987l3WKtAxSudDhYOBTf5tlucUbKz5QK2LfAvMA1kChBh+DHCpJAk4cziqISK6EzABFXCwYvClhPYGFQusJfripGQssOAVt34AhgGJAoSQbgJxuBig/FJwqFobGHNddy8HoTqejIHGcv8bcleUZT57KmW1Vp7LXaMUR4qMQ4YBiE3n41BAOBgcOJFAGQOJwuLAGkHHAHHq04wI/yiD2vraYIqA3SuA+bC47xyXGd55T1dYE3K44T3KDj4JyBFw3hIG4TisDgQMQOQy0/JhFAMksYYgkmifC/E6pFNqY5CLBt8zYQwD4id5wG3yDkOE5FJIEnwxOZ+Cq1myCZY7Ax/Klosw4w/YaiSrNDaOhM/+erWaIc9xgwDFZAAAAAQNIVC1M24nMERf6gONImmAwub35L6eiVhI0f2X8hANtDAEy06t3vxQYBiFwBg5gXe+1Cj7rQ+ji6QJDRW0vEmFLGZXcRhKAN6Nt0/l2ADDAMXw4Ic3idDCRwDPfEbxc9Hgqw6MFFnfB5SG98/3X6ptShJKOWBgHCQnKlJFkAAAAAAoVfDUc2gh0FinqrkqEEWyo/WTDVDT/U2bJS9gAmPKXAmHgCyTBgGIWkhIoZ0QznIWef/b40z+a+nNzThyQhoT+7cjE0KYRLTvzYTFzcowwDAc3eSs4FpHnyXmUvPp+bP+chOe9mSnFV9hx0R9iADzY6JhgGsgUJIPrNyvFAsGbdBzbSrO1OkcGEXy6AONowywuX3+NCWUQGHsywjitxQ2nK+GAYhajfQgn7m8T+SlBrmrOch86kHUDO438Z7mfcnzyKpQtkIOHi8CrIAAAABBwALgD4bDmlQbnQY5qA9/2zzyEMPHpAcN+yqDG3zI2HUxtIATDp2sMAxC4MQkUIMDxOBgOYGXVkQBL/b5MeFxWiMI+ZrKqP2EBia/TaUHWcUSNgAOAwDWFxEaX1K4z6OmpuEjKjkFQMqzncIxclF/HT1pwsizWH6XvSw8hwKE4nA5ai4WA5bg4sA5ChcjAHLsHMsE5lwOXwuV4DmAByqA5tAeV4PLkDm5A=", "witness": null }