diff --git a/examples/p2pk.args b/examples/p2pk.args index aa3e1490..45de4fab 100644 --- a/examples/p2pk.args +++ b/examples/p2pk.args @@ -1,6 +1,3 @@ { - "ALICE_PUBLIC_KEY": { - "value": "0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", - "type": "Pubkey" - } + "ALICE_PUBLIC_KEY": "0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" } diff --git a/examples/p2pk.wit b/examples/p2pk.wit index 3c960d63..31da7c8a 100644 --- a/examples/p2pk.wit +++ b/examples/p2pk.wit @@ -1,6 +1,3 @@ { - "ALICE_SIGNATURE": { - "value": "0xf74b3ca574647f8595624b129324afa2f38b598a9c1c7cfc5f08a9c036ec5acd3c0fbb9ed3dae5ca23a0a65a34b5d6cccdd6ba248985d6041f7b21262b17af6f", - "type": "Signature" - } + "ALICE_SIGNATURE": "0xf74b3ca574647f8595624b129324afa2f38b598a9c1c7cfc5f08a9c036ec5acd3c0fbb9ed3dae5ca23a0a65a34b5d6cccdd6ba248985d6041f7b21262b17af6f" } diff --git a/src/driver/mod.rs b/src/driver/mod.rs index f877d799..0b1b6a30 100644 --- a/src/driver/mod.rs +++ b/src/driver/mod.rs @@ -20,7 +20,7 @@ //! //! SimplicityHL programs begin execution at a single entry point file, //! which the driver registers internally as the root [`MAIN_MODULE`]. -//! This is typically the file passed as the root to the [`DependencyGraph::new`] function. +//! This is typically the file passed as the root to the [`DependencyGraph::build_program`] function. //! //! External libraries are explicitly linked using the `--dep` flag. The driver //! resolves and parses these external files relative to the entry point during @@ -319,7 +319,7 @@ impl DependencyGraph { /// /// Results are cached in `use_cache` to avoid redundant filesystem lookups during /// later construction phases. - /// Note: This is a specialized helper designed exclusively for [`DependencyGraph::new`]. + /// Note: This is a specialized helper designed exclusively for `DependencyGraph::build_graph`. fn resolve_imports( current_program: &parse::Program, current_module: &CurrentModule, @@ -342,7 +342,7 @@ impl DependencyGraph { } /// PHASE 2 OF GRAPH CONSTRUCTION: Loads, parses, and registers new dependencies. - /// Note: This is a specialized helper designed exclusively for [`DependencyGraph::new`]. + /// Note: This is a specialized helper designed exclusively for `DependencyGraph::build_graph`. fn load_and_parse_dependencies( &mut self, current: &CurrentModule, @@ -497,7 +497,7 @@ impl<'a> ImportContext<'a> { } /// Shared mutable state threaded through dependency loading. -/// Lives only for the duration of [`DependencyGraph::new`]. +/// Lives only for the duration of `DependencyGraph::discover_dependencies`. struct LoadContext<'a> { invalid_imports: &'a mut HashSet, diagnostics: &'a mut DiagnosticManager, diff --git a/src/lib.rs b/src/lib.rs index 6726bb85..a7516e84 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,8 @@ use crate::source::CanonSourceFile; pub use crate::types::ResolvedType; pub use crate::unstable::{UnstableFeature, UnstableFeatures}; pub use crate::value::Value; +#[cfg(feature = "serde")] +pub use crate::witness::UnresolvedValues; pub use crate::witness::{Arguments, Parameters, WitnessTypes, WitnessValues}; /// The template of a SimplicityHL program. @@ -323,6 +325,11 @@ impl CompiledProgram { version::SimcDirective::current_version() } + /// Access the witness types declared by the program. + pub fn witness_types(&self) -> &WitnessTypes { + &self.witness_types + } + /// Satisfy the SimplicityHL program with the given `witness_values`. /// /// ## Errors @@ -672,7 +679,11 @@ pub(crate) mod tests { arguments_file_path: P, ) -> TestCase { let arguments_text = std::fs::read_to_string(arguments_file_path).unwrap(); - let arguments = match serde_json::from_str::(&arguments_text) { + let unresolved = match serde_json::from_str::(&arguments_text) { + Ok(x) => x, + Err(error) => panic!("{error}"), + }; + let arguments = match unresolved.resolve(self.program.parameters()) { Ok(x) => x, Err(error) => panic!("{error}"), }; @@ -734,7 +745,11 @@ pub(crate) mod tests { witness_file_path: P, ) -> TestCase { let witness_text = std::fs::read_to_string(witness_file_path).unwrap(); - let witness_values = match serde_json::from_str::(&witness_text) { + let unresolved = match serde_json::from_str::(&witness_text) { + Ok(x) => x, + Err(error) => panic!("{error}"), + }; + let witness_values = match unresolved.resolve(self.program.witness_types()) { Ok(x) => x, Err(error) => panic!("{error}"), }; diff --git a/src/main.rs b/src/main.rs index 0b1e0a74..f0a2a377 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use simplicityhl::ast::ElementsJetHinter; use simplicityhl::version::SimcDirective; use simplicityhl::{ resolution::DependencyMapBuilder, source::CanonPath, source::CanonSourceFile, AbiMeta, - CompiledProgram, + TemplateProgram, }; use simplicityhl::{UnstableFeature, UnstableFeatures}; use std::path::Path; @@ -132,24 +132,25 @@ fn main() -> Result<(), Box> { UnstableFeatures::new(features.copied()) }); + // Argument values are resolved against the program's parameter types, + // so the file is parsed here and resolved once the template is built. #[cfg(feature = "serde")] - let args_opt: simplicityhl::Arguments = match matches.get_one::("args_file") { - None => simplicityhl::Arguments::default(), - Some(args_file) => { - let args_path = Path::new(&args_file); - let args_text = std::fs::read_to_string(args_path).map_err(|e| e.to_string())?; - serde_json::from_str::(&args_text)? - } - }; + let unresolved_args: Option = + match matches.get_one::("args_file") { + None => None, + Some(args_file) => { + let args_path = Path::new(&args_file); + let args_text = std::fs::read_to_string(args_path).map_err(|e| e.to_string())?; + Some(serde_json::from_str(&args_text)?) + } + }; #[cfg(not(feature = "serde"))] - let args_opt: simplicityhl::Arguments = if matches.contains_id("args_file") { + if matches.contains_id("args_file") { return Err( "Program was compiled without the 'serde' feature and cannot process .args files." .into(), ); - } else { - simplicityhl::Arguments::default() - }; + } let dep_args = matches .get_many::("dependencies") @@ -195,14 +196,30 @@ fn main() -> Result<(), Box> { }; let source = CanonSourceFile::new(main_path.clone(), std::sync::Arc::from(main_text)); - let compiled = match CompiledProgram::new_with_dep( + let template = match TemplateProgram::new_with_dep( source, &dependencies, &unstable_features, - args_opt, - include_debug_symbols, Box::new(ElementsJetHinter::new()), ) { + Ok(program) => program, + Err(e) => { + // `Display` is message-only; render for source snippets with + // file and line pointers, as the single-file path does. + eprintln!("{}", e.render_to_string()); + std::process::exit(1); + } + }; + + #[cfg(feature = "serde")] + let args_opt: simplicityhl::Arguments = match unresolved_args { + None => simplicityhl::Arguments::default(), + Some(unresolved) => unresolved.resolve(template.parameters())?, + }; + #[cfg(not(feature = "serde"))] + let args_opt = simplicityhl::Arguments::default(); + + let compiled = match template.instantiate(args_opt, include_debug_symbols) { Ok(program) => program, Err(e) => { eprintln!("{}", e); @@ -216,8 +233,9 @@ fn main() -> Result<(), Box> { .map(|wit_file| -> Result { let wit_path = Path::new(wit_file); let wit_text = std::fs::read_to_string(wit_path).map_err(|e| e.to_string())?; - let witness = serde_json::from_str::(&wit_text).unwrap(); - Ok(witness) + let unresolved = serde_json::from_str::(&wit_text) + .map_err(|e| e.to_string())?; + unresolved.resolve(compiled.witness_types()) }) .transpose()?; #[cfg(not(feature = "serde"))] diff --git a/src/serde.rs b/src/serde.rs index 18c6ef0d..8f54f272 100644 --- a/src/serde.rs +++ b/src/serde.rs @@ -5,17 +5,24 @@ use crate::parse::ParseFromStr; use crate::str::WitnessName; use crate::types::ResolvedType; use crate::value::Value; -use crate::witness::{Arguments, WitnessValues}; +use crate::witness::{Arguments, UnresolvedValue, UnresolvedValues, WitnessValues}; use crate::{AbiMeta, Parameters, WitnessTypes}; use serde::{de, ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; -struct WitnessMapVisitor; +/// Visitor for a map from witness names to values of type `V`, rejecting duplicate names. +struct NamedMapVisitor(std::marker::PhantomData); -impl<'de> de::Visitor<'de> for WitnessMapVisitor { - type Value = HashMap; +impl NamedMapVisitor { + const fn new() -> Self { + Self(std::marker::PhantomData) + } +} + +impl<'de, V: Deserialize<'de>> de::Visitor<'de> for NamedMapVisitor { + type Value = HashMap; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a map with string keys and value-map values") + formatter.write_str("a map with string keys") } fn visit_map(self, mut access: M) -> Result @@ -23,7 +30,7 @@ impl<'de> de::Visitor<'de> for WitnessMapVisitor { M: de::MapAccess<'de>, { let mut map = HashMap::new(); - while let Some((key, value)) = access.next_entry::()? { + while let Some((key, value)) = access.next_entry::()? { if map.insert(key.shallow_clone(), value).is_some() { return Err(de::Error::custom(format!("Name `{key}` is assigned twice"))); } @@ -38,11 +45,57 @@ impl<'de> Deserialize<'de> for WitnessValues { D: Deserializer<'de>, { deserializer - .deserialize_map(WitnessMapVisitor) + .deserialize_map(NamedMapVisitor::::new()) .map(Self::from) } } +struct UnresolvedValueVisitor; + +impl<'de> de::Visitor<'de> for UnresolvedValueVisitor { + type Value = UnresolvedValue; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a value string or a map with \"value\" and \"type\" fields") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + Ok(UnresolvedValue::Untyped(value.to_owned())) + } + + fn visit_map(self, access: M) -> Result + where + M: de::MapAccess<'de>, + { + ValueMapVisitor + .visit_map(access) + .map(UnresolvedValue::Typed) + } +} + +impl<'de> Deserialize<'de> for UnresolvedValue { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(UnresolvedValueVisitor) + } +} + +impl<'de> Deserialize<'de> for UnresolvedValues { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer + .deserialize_map(NamedMapVisitor::::new()) + .map(Self::from_map) + } +} + impl Serialize for ResolvedType { fn serialize(&self, serializer: S) -> Result where @@ -109,7 +162,7 @@ impl<'de> Deserialize<'de> for Arguments { D: Deserializer<'de>, { deserializer - .deserialize_map(WitnessMapVisitor) + .deserialize_map(NamedMapVisitor::::new()) .map(Self::from) } } diff --git a/src/witness.rs b/src/witness.rs index 7fb0bf3a..78dc7075 100644 --- a/src/witness.rs +++ b/src/witness.rs @@ -130,6 +130,68 @@ impl WitnessValues { } } +/// A value from a witness or argument file whose type may come from the program. +#[cfg(feature = "serde")] +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) enum UnresolvedValue { + /// A bare value string (`"NAME": "42"`), parsed against the type + /// that the program declares for `NAME`. + Untyped(String), + /// A self-typed entry (`"NAME": { "value": "42", "type": "u32" }`), + /// parsed against the type written in the file. + Typed(Value), +} + +/// Witness or argument values parsed from a file, before their types are resolved +/// against the program. +/// +/// See docs Untyped and Typed variants of `UnresolvedValue` enum to understand how entries are resolved. +/// +/// Call [`UnresolvedValues::resolve`] with the program's declared types to obtain +/// [`WitnessValues`] or [`Arguments`]. +#[cfg(feature = "serde")] +#[derive(Clone, Debug, Eq, PartialEq, Default)] +pub struct UnresolvedValues(HashMap); + +#[cfg(feature = "serde")] +impl UnresolvedValues { + pub(crate) fn from_map(map: HashMap) -> Self { + Self(map) + } + + /// Resolve each value against the type that the program declares for its name. + /// + /// ## 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. + /// They are type-checked against the program later, when the program is instantiated/satisfied. + pub fn resolve(self, declared_types: &M) -> Result + where + T: From>, + M: AsRef>, + { + let declared_types = declared_types.as_ref(); + let mut map = HashMap::with_capacity(self.0.len()); + for (name, unresolved) in self.0 { + let value = match unresolved { + UnresolvedValue::Typed(value) => value, + 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") + })?; + Value::parse_from_str(&s, ty) + .map_err(|error| format!("`{name}` is declared as `{ty}`: {error}"))? + } + }; + map.insert(name, value); + } + Ok(T::from(map)) + } +} + impl ParseFromStr for ResolvedType { fn parse_from_str(s: &str) -> Result { let aliased = AliasedType::parse_from_str(s)?; @@ -281,6 +343,85 @@ fn main() { } } + #[test] + #[cfg(feature = "serde")] + fn unresolved_values_resolve_against_declared_types() { + let u32_ty = ResolvedType::parse_from_str("u32").unwrap(); + let sig_ty = ResolvedType::parse_from_str("Signature").unwrap(); + let witness_types = WitnessTypes::from(HashMap::from([ + (WitnessName::from_str_unchecked("A"), u32_ty.clone()), + (WitnessName::from_str_unchecked("SIG"), sig_ty), + ])); + + let unresolved = UnresolvedValues::from_map(HashMap::from([ + ( + WitnessName::from_str_unchecked("A"), + UnresolvedValue::Untyped("42".to_string()), + ), + ( + WitnessName::from_str_unchecked("B"), + UnresolvedValue::Typed(Value::u16(7)), + ), + ])); + let resolved: WitnessValues = unresolved.resolve(&witness_types).unwrap(); + assert_eq!( + resolved.get(&WitnessName::from_str_unchecked("A")), + Some(&Value::u32(42)) + ); + assert_eq!( + resolved.get(&WitnessName::from_str_unchecked("B")), + Some(&Value::u16(7)) + ); + + let unknown = UnresolvedValues::from_map(HashMap::from([( + WitnessName::from_str_unchecked("TYPO"), + UnresolvedValue::Untyped("1".to_string()), + )])); + let err = unknown + .resolve::(&witness_types) + .unwrap_err(); + assert!(err.contains("TYPO"), "error should name the entry: {err}"); + + let bad = UnresolvedValues::from_map(HashMap::from([( + WitnessName::from_str_unchecked("A"), + UnresolvedValue::Untyped("not-a-number".to_string()), + )])); + let err = bad.resolve::(&witness_types).unwrap_err(); + assert!( + err.contains('A') && err.contains("u32"), + "error should name the witness and its declared type: {err}" + ); + } + + #[test] + #[cfg(feature = "serde")] + fn unresolved_values_parse_from_json() { + // Bare strings and legacy value/type maps may be mixed in one file. + let s = r#"{ + "A": "42", + "B": { "value": "7", "type": "u16" } +}"#; + let unresolved: UnresolvedValues = serde_json::from_str(s).unwrap(); + let u32_ty = ResolvedType::parse_from_str("u32").unwrap(); + let witness_types = WitnessTypes::from(HashMap::from([( + WitnessName::from_str_unchecked("A"), + u32_ty, + )])); + let resolved: WitnessValues = unresolved.resolve(&witness_types).unwrap(); + assert_eq!( + resolved.get(&WitnessName::from_str_unchecked("A")), + Some(&Value::u32(42)) + ); + assert_eq!( + resolved.get(&WitnessName::from_str_unchecked("B")), + Some(&Value::u16(7)) + ); + + // Duplicate names are rejected at parse time, as for WitnessValues. + let dup = r#"{ "A": "1", "A": "2" }"#; + assert!(serde_json::from_str::(dup).is_err()); + } + #[test] fn witness_to_string() { let witness = WitnessValues::from(HashMap::from([