diff --git a/src/ast.rs b/src/ast.rs index ee31f60a..44c4c46d 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1196,9 +1196,11 @@ impl Scope { /// 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. + /// Identity is the declaration site, not the written name: `file_id` is the + /// source file the declaration came from, and it participates in the type's + /// equality. Two files may therefore each declare an `Action` without the + /// two becoming one type. `name` stays a display string, which is what + /// diagnostics print and what witness and argument files write. /// /// ## Errors /// @@ -1208,10 +1210,16 @@ impl Scope { name: AliasName, visibility: Visibility, variants: Arc<[EnumVariantInfo]>, + span: Span, ) -> Result<(), Error> { self.check_alias_free(&name)?; - let info = EnumInfo::new(Arc::clone(name.as_inner()), variants); + let info = EnumInfo::new( + Arc::clone(name.as_inner()), + variants, + span, + Arc::from(self.module_path.clone()), + ); let resolved = ResolvedType::enumeration(info); self.current_module_mut() @@ -1458,7 +1466,12 @@ impl AbstractSyntaxTree for Item { }) .collect::, Diagnostic>>()?; scope - .insert_enum(decl.name().clone(), decl.visibility().clone(), variants) + .insert_enum( + decl.name().clone(), + decl.visibility().clone(), + variants, + *decl.span(), + ) .with_span(decl)?; Ok(Self::EnumDeclaration) @@ -3270,45 +3283,6 @@ mod enum_tests { 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( diff --git a/src/driver/resolve_order.rs b/src/driver/resolve_order.rs index 31228798..70c6b8de 100644 --- a/src/driver/resolve_order.rs +++ b/src/driver/resolve_order.rs @@ -5,56 +5,6 @@ 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. @@ -113,14 +63,6 @@ impl DependencyGraph { } } - forbid_enum_dec_in_deps(source_id, &local_items, diagnostics); - - // TODO(enums): the flattened output wraps every file — the - // entry file included — in a generated module, but enum - // declarations are only valid at the top level of a file, so - // flattening an enum program produces source that no longer - // re-parses (`TemplateAst::flatten`). Splice the entry - // file's items at the root instead of wrapping them. let name = ModuleName::from_ident(&Self::get_module_name(source_id)); items.push(parse::Item::Module(parse::Module::new( source_id, diff --git a/src/lib.rs b/src/lib.rs index 2e6ac72f..97df3bc8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,8 @@ //! Library for parsing and compiling SimplicityHL +use std::collections::HashMap; +use std::fmt; + pub mod array; pub mod ast; pub mod compile; @@ -400,12 +403,78 @@ impl CompiledProgram { /// encoding and the CMR while leaving the ABI text identical). Such /// consumers need the program source or another artifact carrying the enum /// schema. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Eq, PartialEq)] pub struct AbiMeta { pub witness_types: WitnessTypes, pub param_types: Parameters, } +impl fmt::Debug for AbiMeta { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let witness_types: HashMap<_, _> = self + .witness_types + .iter() + .map(|(name, ty)| (name.clone(), ty.abi_description())) + .collect(); + let param_types: HashMap<_, _> = self + .param_types + .iter() + .map(|(name, ty)| (name.clone(), ty.abi_description())) + .collect(); + f.debug_struct("AbiMeta") + .field("witness_types", &witness_types) + .field("param_types", ¶m_types) + .finish() + } +} + +impl AbiMeta { + /// Same as `abi_description`, except an enum type is prefixed with the + /// absolute path of the file. It acts like unique identifier for nominative type + pub fn describe( + &self, + sources: Option<&SourceMap>, + ) -> (HashMap, HashMap) { + let describe_one = |ty: &types::ResolvedType| -> String { + let base = ty.abi_description(); + let (Some(info), Some(sources)) = (ty.as_enum(), sources) else { + return base; + }; + let Some(path) = sources.path(info.span().file_id) else { + return base; + }; + + let modules = info + .module_path() + .get(1..) // drop the driver-assigned `unit_N` segment + .unwrap_or(&[]) + .iter() + .map(|m| m.as_str()) + .collect::>() + .join("::"); + + let file = path.as_path().display(); + if modules.is_empty() { + format!("{file}: {base}") + } else { + format!("{file}::{modules}: {base}") + } + }; + + let witness_types = self + .witness_types + .iter() + .map(|(name, ty)| (name.as_str().to_string(), describe_one(ty))) + .collect(); + let param_types = self + .param_types + .iter() + .map(|(name, ty)| (name.as_str().to_string(), describe_one(ty))) + .collect(); + (witness_types, param_types) + } +} + /// A SimplicityHL program, compiled to Simplicity and satisfied with witness data. #[derive(Clone, Debug, PartialEq, Eq)] pub struct SatisfiedProgram { @@ -1602,24 +1671,178 @@ fn main() { #[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), - } - }"; + let ws = TempWorkspace::new("crate_success"); + let root = ws.create_dir("workspace"); + ws.create_file( + format!("workspace/{MAIN}").as_str(), + "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(); + let main_path = root.join(MAIN); + let canon_root = CanonPath::canonicalize(&root).unwrap(); + let dependency_map = build_map(&canon_root, &[]).unwrap(); + + TestCase::::template_deps_with_unstable( + &main_path, + &dependency_map, + UnstableFeatures::all(), + ) + .with_arguments(Arguments::default()) + .with_witness_values(WitnessValues::default()) + .assert_run_success(); + } + + #[test] + fn enum_same_name_in_two_modules_are_distinct_types() { + let ws = TempWorkspace::new("enum_nominal_identity"); + let root = ws.create_dir("workspace"); + let main_path = ws.create_file( + format!("workspace/{MAIN}").as_str(), + "mod door { + pub enum Action { Open, Close, } + pub fn describe(a: Action) -> u32 { + match a { Action::Open => 1, Action::Close => 2, } + } + } + mod wallet { + pub enum Action { Open, Close, } + pub fn spend(a: Action) -> u32 { + match a { Action::Open => 100, Action::Close => 200, } + } + } + use crate::door::Action as DoorAction; + use crate::wallet::spend; + fn main() { + let d: DoorAction = DoorAction::Open; + assert!(jet::eq_32(spend(d), 100)); + }", + ); + + let canon_root = CanonPath::canonicalize(&root).unwrap(); + let dependencies = build_map(&canon_root, &[]).unwrap(); + let source = CanonSourceFile::new( + CanonPath::canonicalize(&main_path).unwrap(), + Arc::from(std::fs::read_to_string(&main_path).unwrap()), + ); + + let err = TemplateAst::new_with_dep( + source, + &dependencies, + &UnstableFeatures::all(), + Box::new(ElementsJetHinter::new()), + ) + .expect_err("`door::Action` must not satisfy a `wallet::Action` parameter"); + + // TODO: both types display as `Action`, so the message cannot be acted on. + // Print the declaring module path and point at both declaration sites. + assert!( + err.to_string() + .contains("Expected expression of type `Action`, found type `Action`"), + "expected a nominal type mismatch, got:\n{err}" + ); + } + + #[test] + fn abi_description_bare_vs_qualified_for_two_same_shaped_enums() { + // Two enums, same name and same variant shapes, declared in + // different modules: distinct nominal types (see + // `enum_same_name_in_two_modules_are_distinct_types`), each used at + // its own type here, so this compiles. + let ws = TempWorkspace::new("abi_same_shaped_enums"); + let root = ws.create_dir("workspace"); + let main_path = ws.create_file( + format!("workspace/{MAIN}").as_str(), + "mod door { + pub enum Action { Open, Close(u32), } + pub fn describe(a: Action) -> u32 { + match a { Action::Open => 1, Action::Close(n: u32) => n, } + } + } + mod wallet { + pub enum Action { Open, Close(u32), } + pub fn spend(a: Action) -> u32 { + match a { Action::Open => 100, Action::Close(n: u32) => n, } + } + } + use crate::door::{Action as DoorAction, describe}; + use crate::wallet::{Action as WalletAction, spend}; + fn main() { + let dact: DoorAction = witness::DOOR_ACTION; + let wact: WalletAction = witness::WALLET_ACTION; + assert!(jet::eq_32(describe(dact), 1)); + assert!(jet::eq_32(spend(wact), 100)); + }", + ); + + let canon_root = CanonPath::canonicalize(&root).unwrap(); + let dependencies = build_map(&canon_root, &[]).unwrap(); + let source = CanonSourceFile::new( + CanonPath::canonicalize(&main_path).unwrap(), + Arc::from(std::fs::read_to_string(&main_path).unwrap()), + ); + + let template = TemplateAst::new_with_dep( + source, + &dependencies, + &UnstableFeatures::all(), + Box::new(ElementsJetHinter::new()), + ) + .expect("two distinct enums used at their own type must compile"); + + let abi = template.generate_abi_meta().unwrap(); + let door = abi + .witness_types + .get(&TemplateProgramWitness::witness_from_str("DOOR_ACTION")) + .expect("DOOR_ACTION must be recorded"); + let wallet = abi + .witness_types + .get(&TemplateProgramWitness::witness_from_str("WALLET_ACTION")) + .expect("WALLET_ACTION must be recorded"); + + // The bare form is intentionally shape-only: same name, same + // variants, so identical text either way. + assert_eq!(door.abi_description(), wallet.abi_description()); + assert_eq!(door.abi_description(), "Action { Open, Close(u32) }"); + + // Qualified by declaration site, the two must render distinctly. + let sources = template + .source_map() + .expect("a driver-compiled program has a source map"); + let (witness_types, _) = abi.describe(Some(sources)); + let door_q = witness_types.get("DOOR_ACTION").expect("recorded above"); + let wallet_q = witness_types.get("WALLET_ACTION").expect("recorded above"); + + dbg!(door_q, wallet_q); + + assert_ne!( + door_q, wallet_q, + "distinct declarations must render distinctly once qualified" + ); + assert!( + door_q.ends_with("::door: Action { Open, Close(u32) }"), + "got {door_q}" + ); + assert!( + wallet_q.ends_with("::wallet: Action { Open, Close(u32) }"), + "got {wallet_q}" + ); + assert!( + !door_q.contains("unit_0") && !wallet_q.contains("unit_0"), + "the driver-assigned segment must not leak into the qualified form: {door_q} / {wallet_q}" + ); } #[test] diff --git a/src/main.rs b/src/main.rs index 1dcd9e1a..1a852434 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,13 +6,24 @@ use simplicityhl::ast::ElementsJetHinter; use simplicityhl::error::should_color; use simplicityhl::version::SimcDirective; use simplicityhl::{ - resolution::DependencyMapBuilder, source::CanonPath, source::CanonSourceFile, AbiMeta, - TemplateAst, + resolution::DependencyMapBuilder, source::CanonPath, source::CanonSourceFile, TemplateAst, }; use simplicityhl::{UnstableFeature, UnstableFeatures}; +use std::collections::HashMap; use std::path::Path; use std::{env, fmt, io}; +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(not(feature = "serde"), allow(dead_code))] +#[derive(Debug)] +/// ABI metadata rendered for display: each witness/parameter type as text, +/// with enum types qualified by their real declaration path. +struct AbiOutput { + witness_types: HashMap, + #[cfg_attr(feature = "serde", serde(rename = "parameter_types"))] + param_types: HashMap, +} + #[cfg_attr(feature = "serde", derive(serde::Serialize))] /// The compilation output. struct Output { @@ -21,7 +32,7 @@ struct Output { /// Simplicity witness result, base64 encoded, if the .wit file was provided. witness: Option, /// Simplicity program ABI metadata to the program which the user provides. - abi_meta: Option, + abi_meta: Option, /// Commitment Merkle Root (CMR) of the program, hex encoded. cmr: String, /// Version of the compiler that produced this output. Different compiler @@ -293,7 +304,12 @@ fn main() -> Result<(), Box> { }; let abi_opt = if abi_param { - Some(compiled.generate_abi_meta()?) + let abi = compiled.generate_abi_meta()?; + let (witness_types, param_types) = abi.describe(template.source_map()); + Some(AbiOutput { + witness_types, + param_types, + }) } else { None }; diff --git a/src/parse.rs b/src/parse.rs index dc175d02..af54df39 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -3114,27 +3114,6 @@ 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 - }) } } diff --git a/src/serde.rs b/src/serde.rs index 5e1d1643..b9e7b899 100644 --- a/src/serde.rs +++ b/src/serde.rs @@ -128,9 +128,20 @@ impl Serialize for AbiMeta { { use ::serde::ser::SerializeStruct; + let witness_types: HashMap<_, _> = self + .witness_types + .iter() + .map(|(name, ty)| (name.as_str(), ty.abi_description())) + .collect(); + let param_types: HashMap<_, _> = self + .param_types + .iter() + .map(|(name, ty)| (name.as_str(), ty.abi_description())) + .collect(); + let mut state = serializer.serialize_struct("AbiMeta", 2)?; - state.serialize_field("witness_types", &self.witness_types)?; - state.serialize_field("parameter_types", &self.param_types)?; + state.serialize_field("witness_types", &witness_types)?; + state.serialize_field("parameter_types", ¶m_types)?; state.end() } } @@ -330,6 +341,7 @@ impl Serialize for Arguments { #[cfg(test)] mod tests { use super::*; + use crate::error::Span; use crate::str::Identifier; #[test] @@ -345,7 +357,7 @@ mod tests { } } - fn unit_enum(name: &str, variants: &[&str]) -> ResolvedType { + fn unit_enum(name: &str, variants: &[&str], span: Span) -> ResolvedType { use crate::types::{EnumInfo, EnumVariantInfo}; use std::sync::Arc; @@ -353,14 +365,19 @@ mod tests { .iter() .map(|name| EnumVariantInfo::new(Identifier::from_str_unchecked(name), Arc::from([]))) .collect(); - ResolvedType::enumeration(EnumInfo::new(Arc::from(name), variants)) + ResolvedType::enumeration(EnumInfo::new( + Arc::from(name), + variants, + span, + Arc::from([]), + )) } #[test] fn abi_enum_type_serializes_as_name() { use crate::types::TypeConstructible; - let action_ty = unit_enum("Action", &["Inherit", "ColdSpend"]); + let action_ty = unit_enum("Action", &["Inherit", "ColdSpend"], Span::DUMMY); let witness_types = WitnessTypes::from(HashMap::from([ ( TemplateProgramWitness::witness_from_str("ACTION"), @@ -372,7 +389,10 @@ mod tests { ), ( TemplateProgramWitness::witness_from_str("PAIR"), - ResolvedType::tuple([action_ty, unit_enum("Reaction", &["Fast", "Slow"])]), + ResolvedType::tuple([ + action_ty, + unit_enum("Reaction", &["Fast", "Slow"], Span::DUMMY), + ]), ), ( TemplateProgramWitness::witness_from_str("PLAIN"), @@ -389,7 +409,7 @@ mod tests { #[test] fn enum_witness_value_serializes_as_variant_name() { - let action_ty = unit_enum("Action", &["Inherit", "ColdSpend"]); + let action_ty = unit_enum("Action", &["Inherit", "ColdSpend"], Span::DUMMY); let value = Value::enum_variant( &action_ty, &Identifier::from_str_unchecked("ColdSpend"), @@ -438,7 +458,12 @@ mod tests { Arc::from([u32_ty.clone()]), ), ]); - let action_ty = ResolvedType::enumeration(EnumInfo::new(Arc::from("Action"), variants)); + let action_ty = ResolvedType::enumeration(EnumInfo::new( + Arc::from("Action"), + variants, + Span::DUMMY, + Arc::from([]), + )); let value = Value::enum_variant( &action_ty, &Identifier::from_str_unchecked("Refresh"), @@ -469,7 +494,7 @@ mod tests { use crate::types::TypeConstructible; use crate::value::ValueConstructible; - let action_ty = unit_enum("Action", &["Hot", "Cold"]); + let action_ty = unit_enum("Action", &["Hot", "Cold"], Span::DUMMY); let option_ty = ResolvedType::option(action_ty.clone()); let cold = Value::enum_variant(&action_ty, &Identifier::from_str_unchecked("Cold"), vec![]) .unwrap(); diff --git a/src/types/aliased.rs b/src/types/aliased.rs index e4765a03..97f08119 100644 --- a/src/types/aliased.rs +++ b/src/types/aliased.rs @@ -127,7 +127,7 @@ impl AliasedType { } // 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())); + output.push(ResolvedType(TypeInner::Enum(Arc::clone(info)))); } }, } diff --git a/src/types/inner.rs b/src/types/inner.rs index 7248a01d..b0d5ce57 100644 --- a/src/types/inner.rs +++ b/src/types/inner.rs @@ -2,8 +2,9 @@ use core::fmt; use core::str::FromStr; use std::sync::Arc; +use crate::error::Span; use crate::num::{NonZeroPow2Usize, Pow2Usize}; -use crate::str::Identifier; +use crate::str::{Identifier, ModuleName}; use super::{ResolvedType, StructuralType, TypeConstructible as _}; @@ -27,7 +28,7 @@ pub enum TypeInner { List(A, NonZeroPow2Usize), /// Nominal enum type, represented as a balanced sum of its variants' /// payload types - Enum(EnumInfo), + Enum(Arc), } impl TypeInner { @@ -227,13 +228,15 @@ impl FromStr for UIntType { /// 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. +/// Identity is the declaration site (`span`), not the written name: two +/// files may each declare an `Action` without the two becoming one type. #[derive(Debug, PartialEq, Eq, Hash, Clone)] pub struct EnumInfo { name: Arc, variants: Arc<[EnumVariantInfo]>, + span: Span, + /// Need it to retrieve actual path to unique `EnumInfo`. + module_path: Arc<[ModuleName]>, } impl EnumInfo { @@ -242,9 +245,19 @@ 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. - pub(crate) fn new(name: Arc, variants: Arc<[EnumVariantInfo]>) -> Self { + pub(crate) fn new( + name: Arc, + variants: Arc<[EnumVariantInfo]>, + span: Span, + module_path: Arc<[ModuleName]>, + ) -> Self { debug_assert!(!variants.is_empty()); - Self { name, variants } + Self { + name, + variants, + span, + module_path, + } } /// Access the declared name of the enum. @@ -252,6 +265,15 @@ impl EnumInfo { &self.name } + /// The module chain the declaration sits in, root first. + pub fn module_path(&self) -> &[ModuleName] { + &self.module_path + } + + pub fn span(&self) -> &Span { + &self.span + } + /// Access the variants of the enum in declaration order. pub fn variants(&self) -> &[EnumVariantInfo] { &self.variants diff --git a/src/types/mod.rs b/src/types/mod.rs index 0b6aef9a..de9f8176 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -107,7 +107,7 @@ pub trait TypeDeconstructible: Sized { #[cfg(test)] mod tests { use super::*; - use crate::str::Identifier; + use crate::{error::Span, str::Identifier}; #[test] fn display_type() { @@ -150,7 +150,12 @@ mod tests { pair.payload_type() ); - let info = EnumInfo::new(Arc::from("Test"), Arc::from([unit, single, pair])); + let info = EnumInfo::new( + Arc::from("Test"), + Arc::from([unit, single, pair]), + Span::DUMMY, + Arc::from([]), + ); assert_eq!("Test", info.name()); assert_eq!(3, info.structural_variants().len()); let (index, variant) = info diff --git a/src/types/resolved.rs b/src/types/resolved.rs index c7a2f1af..1372e02b 100644 --- a/src/types/resolved.rs +++ b/src/types/resolved.rs @@ -10,7 +10,7 @@ use crate::num::NonZeroPow2Usize; /// SimplicityHL type without type aliases. #[derive(PartialEq, Eq, Hash, Clone)] -pub struct ResolvedType(TypeInner>); +pub struct ResolvedType(pub(super) TypeInner>); impl ResolvedType { /// Access the inner type primitive. @@ -31,12 +31,12 @@ impl ResolvedType { /// (which owns the uniqueness of declaration ids) can mint enum types. impl ResolvedType { /// Create a nominal enum type from the given definition. - pub const fn enumeration(info: EnumInfo) -> Self { - Self(TypeInner::Enum(info)) + pub fn enumeration(info: EnumInfo) -> Self { + Self(TypeInner::Enum(Arc::new(info))) } /// Access the enum definition if this is an enum type. - pub const fn as_enum(&self) -> Option<&EnumInfo> { + pub fn as_enum(&self) -> Option<&EnumInfo> { match &self.0 { TypeInner::Enum(info) => Some(info), _ => None, @@ -48,6 +48,34 @@ impl ResolvedType { self.post_order_iter() .any(|data| data.node.as_enum().is_some()) } + + /// Full description for the ABI: an enum expands into its variants and + /// their payload types; every other type is unchanged from [`Display`]. + pub(crate) fn abi_description(&self) -> String { + let Some(info) = self.as_enum() else { + return self.to_string(); + }; + + let variants = info + .variants() + .iter() + .map(|v| { + if v.payload().is_empty() { + v.name().to_string() + } else { + let payload = v + .payload() + .iter() + .map(ToString::to_string) + .collect::>() + .join(", "); + format!("{}({})", v.name(), payload) + } + }) + .collect::>() + .join(", "); + format!("{} {{ {} }}", info.name(), variants) + } } impl TypeConstructible for ResolvedType { diff --git a/src/value.rs b/src/value.rs index 2dd1ca22..a7f5a5f4 100644 --- a/src/value.rs +++ b/src/value.rs @@ -1398,6 +1398,7 @@ mod destruct { #[cfg(test)] mod tests { use super::*; + use crate::error::Span; use crate::parse; use crate::types::{EnumInfo, EnumVariantInfo, StructuralType, TypeConstructible}; @@ -1412,7 +1413,12 @@ mod tests { }) .collect(); - ResolvedType::enumeration(EnumInfo::new(Arc::from("Test"), variants)) + ResolvedType::enumeration(EnumInfo::new( + Arc::from("Test"), + variants, + Span::DUMMY, + Arc::from([]), + )) } /// An enum with one unit variant, one single-payload variant and one @@ -1430,7 +1436,12 @@ mod tests { ), ]); - ResolvedType::enumeration(EnumInfo::new(Arc::from("Test"), variants)) + ResolvedType::enumeration(EnumInfo::new( + Arc::from("Test"), + variants, + Span::DUMMY, + Arc::from([]), + )) } #[test] diff --git a/src/witness.rs b/src/witness.rs index c8c5dffa..02a2f9ac 100644 --- a/src/witness.rs +++ b/src/witness.rs @@ -357,6 +357,8 @@ impl crate::ArbitraryOfType for Arguments { mod tests { use super::*; use crate::ast::ElementsJetHinter; + #[cfg(feature = "serde")] + use crate::error::Span; use crate::parse::ParseFromStr; #[cfg(feature = "serde")] use crate::str::Identifier; @@ -521,7 +523,12 @@ fn main() { .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 action_ty = ResolvedType::enumeration(EnumInfo::new( + Arc::from("Action"), + variants, + Span::DUMMY, + Arc::from([]), + )); let witness_types = WitnessTypes::from(HashMap::from([( TemplateProgramWitness::witness_from_str("ACTION"), action_ty.clone(), @@ -562,7 +569,12 @@ fn main() { .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 action_ty = ResolvedType::enumeration(EnumInfo::new( + Arc::from("Action"), + variants, + Span::DUMMY, + Arc::from([]), + )); let option_ty = ResolvedType::option(action_ty.clone()); let tuple_ty = ResolvedType::tuple([ action_ty.clone(),