diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 4ffa551..72b9ba0 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,5 +1,6 @@ on: # yamllint disable-line rule:truthy pull_request: + workflow_dispatch: push: branches: - master diff --git a/src/ast.rs b/src/ast.rs index ffac991..bd14b7b 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1442,23 +1442,22 @@ fn analyze_named_module( ModuleItem::Module(module) if module.name == name => Some(module), _ => None, }); - let witness_module = iter - .next() - .ok_or(Error::ModuleRequired(name.shallow_clone())) - .with_span(from)?; + let witness_module = iter.next(); if iter.next().is_some() { return Err(Error::ModuleRedefined(name)).with_span(from); } let mut map = HashMap::new(); - for assignment in witness_module.assignments() { - if map.contains_key(assignment.name()) { - return Err(Error::WitnessReassigned(assignment.name().shallow_clone())) - .with_span(assignment); + if let Some(witness_module) = witness_module { + for assignment in witness_module.assignments() { + if map.contains_key(assignment.name()) { + return Err(Error::WitnessReassigned(assignment.name().shallow_clone())) + .with_span(assignment); + } + map.insert( + assignment.name().shallow_clone(), + assignment.value().clone(), + ); } - map.insert( - assignment.name().shallow_clone(), - assignment.value().clone(), - ); } Ok(map) } diff --git a/src/error.rs b/src/error.rs index d6667d9..6979953 100644 --- a/src/error.rs +++ b/src/error.rs @@ -339,6 +339,11 @@ pub enum Error { ModuleRedefined(ModuleName), ArgumentMissing(WitnessName), ArgumentTypeMismatch(WitnessName, ResolvedType, ResolvedType), + InvalidJsonFormat(String), + UndefinedWitness(WitnessName), + UndefinedParameter(WitnessName), + WitnessMultipleAssignments(WitnessName), + ArgumentMultipleAssignments(WitnessName), } #[rustfmt::skip] @@ -481,6 +486,26 @@ impl fmt::Display for Error { f, "Parameter `{name}` was declared with type `{declared}` but its assigned argument is of type `{assigned}`" ), + Error::InvalidJsonFormat(description) => write!( + f, + "Invalid JSON format for witness/argument deserialization: {description}" + ), + Error::UndefinedWitness(name) => write!( + f, + "Witness `{name}` is not defined in WitnessTypes context" + ), + Error::UndefinedParameter(name) => write!( + f, + "Parameter `{name}` is not defined in Parameters context" + ), + Error::WitnessMultipleAssignments(name) => write!( + f, + "Witness `{name}` is assigned multiple times in JSON" + ), + Error::ArgumentMultipleAssignments(name) => write!( + f, + "Argument `{name}` is assigned multiple times in JSON" + ), } } } diff --git a/src/lib.rs b/src/lib.rs index 702089a..1cebd5a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -117,11 +117,18 @@ impl CompiledProgram { .and_then(|template| template.instantiate(arguments, include_debug_symbols)) } - /// Access the debug symbols for the Simplicity target code. + /// Access the debug symbols for the Simplicity target code. pub fn debug_symbols(&self) -> &DebugSymbols { &self.debug_symbols } + /// Access the witness types declared in the SimplicityHL program. + /// + /// [NEW METHOD - INSERT HERE] + pub fn witness_types(&self) -> &WitnessTypes { + &self.witness_types + } + /// Access the Simplicity target code, without witness data. pub fn commit(&self) -> Arc> { named::forget_names(&self.simplicity) diff --git a/src/main.rs b/src/main.rs index 1135a31..62f0c99 100644 --- a/src/main.rs +++ b/src/main.rs @@ -77,7 +77,14 @@ fn main() -> Result<(), Box> { .map(|wit_file| -> Result { let wit_path = std::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(); + // Use new context-aware deserialization method + // Type information is provided by the compiled program (witness_types) + // Users only need to specify values in simplified JSON format + let witness = simplicityhl::WitnessValues::from_json_with_types( + &wit_text, + &compiled.witness_types(), + ) + .map_err(|e| e.to_string())?; Ok(witness) }) .transpose()?; diff --git a/src/serde.rs b/src/serde.rs index 2a12550..d21a1a5 100644 --- a/src/serde.rs +++ b/src/serde.rs @@ -1,185 +1,235 @@ use std::collections::HashMap; -use std::fmt; -use serde::{de, ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; - -use crate::parse::ParseFromStr; +use crate::error::Error; use crate::str::WitnessName; -use crate::types::ResolvedType; use crate::value::Value; -use crate::witness::{Arguments, WitnessValues}; - -struct WitnessMapVisitor; +use crate::witness::{Arguments, WitnessTypes, WitnessValues}; -impl<'de> de::Visitor<'de> for WitnessMapVisitor { - type Value = HashMap; +// ============================================================================ +// Context-aware deserialization with optional description field +// ============================================================================ +// +// NEW FORMAT: Users can now add optional "description" fields +// { +// "VAR_NAME": "Left(0x...)", +// "description": "Optional comment about this witness file", +// "ANOTHER_VAR": "0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" +// } +// +// Special keys that are ignored: +// - "description" (top-level comment for the entire witness file) +// - Any key starting with "_" (conventionally used for comments) +// - "_" (can be used to provide hints about specific witnesses) +// +// Example with hints: +// { +// "pubkey": "0x...", +// "_pubkey": "Bitcoin public key for multisig", +// "signature": "Left(0x...)", +// "_signature": "ECDSA signature of the transaction", +// "description": "Witness data for Bitcoin transaction #12345" +// } +// +// The compiler automatically ignores these comment fields. - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a map with string keys and value-map values") - } +impl WitnessValues { + /// Deserialize witness values from JSON with compiler-provided type context. + /// + /// This method simplifies the witness JSON format by eliminating redundant + /// type field annotations. The compiler provides type information through + /// WitnessTypes, allowing users to specify only the values they need to provide. + /// + /// Special features: + /// - "description" field: Top-level comment for the witness file (ignored) + /// - "_" fields: Comments/hints for specific witnesses (ignored) + /// - Any field starting with "_" is treated as a comment (ignored) + /// + /// # Arguments + /// + /// * `json` - JSON string with witness values in simple string format + /// * `witness_types` - Type information from the compiled program + /// + /// # Example JSON Format + /// + /// ```json + /// { + /// "pubkey": "0x1234", + /// "_pubkey": "Bitcoin public key (32 bytes)", + /// "signature": "Left(0x...)", + /// "_signature": "ECDSA signature", + /// "description": "Witness data for transaction ABC" + /// } + /// ``` + /// + /// Types are resolved from the compiled program, not from user input. + /// + /// # Errors + /// + /// Returns an error if: + /// - JSON parsing fails + /// - A witness variable is not found in witness_types + /// - Value parsing fails for the resolved type + /// - A witness variable is assigned multiple times + pub fn from_json_with_types( + json: &str, + witness_types: &WitnessTypes, + ) -> Result { + let json_value: serde_json::Map = + serde_json::from_str(json) + .map_err(|e| Error::InvalidJsonFormat(format!("Failed to parse JSON: {}", e)))?; - fn visit_map(self, mut access: M) -> Result - where - M: de::MapAccess<'de>, - { let mut map = HashMap::new(); - 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"))); + + for (name_str, value_json) in json_value.iter() { + // Skip special fields that are allowed for documentation/comments + if name_str == "description" { + // Top-level description for the entire witness file + // Users can add comments here + continue; + } + + if name_str.starts_with("_") { + // Convention: fields starting with _ are comments/hints + // Examples: "_pubkey", "_signature", "_note" + // These are completely ignored by the compiler + continue; } - } - Ok(map) - } -} -impl<'de> Deserialize<'de> for WitnessValues { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer - .deserialize_map(WitnessMapVisitor) - .map(Self::from) - } -} + let name = WitnessName::from_str_unchecked(name_str); -impl<'de> Deserialize<'de> for Arguments { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer - .deserialize_map(WitnessMapVisitor) - .map(Self::from) - } -} + // Retrieve type from compiler-provided context (WitnessTypes) + // This is the key difference: type comes from the compiler, not JSON + let ty = witness_types + .get(&name) + .ok_or_else(|| Error::UndefinedWitness(name.clone()))?; -struct ValueMapVisitor; + // Extract value string from JSON (simple format, not {"value": ..., "type": ...}) + // Type annotation needed here for serde_json::Value + let value_str: &str = match value_json.as_str() { + Some(s) => s, + None => { + return Err(Error::InvalidJsonFormat(format!( + "Witness `{}` must be a string value, got {}", + name, + match value_json { + serde_json::Value::Null => "null", + serde_json::Value::Bool(_) => "boolean", + serde_json::Value::Number(_) => "number", + serde_json::Value::String(_) => "string", + serde_json::Value::Array(_) => "array", + serde_json::Value::Object(_) => "object", + } + ))) + } + }; -impl<'de> de::Visitor<'de> for ValueMapVisitor { - type Value = Value; + // Parse value using compiler-provided type + // This ensures type safety without requiring user to annotate types + let value = Value::parse_from_str(value_str, ty)?; - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a map with \"value\" and \"type\" fields") - } - - fn visit_map(self, mut access: M) -> Result - where - M: de::MapAccess<'de>, - { - let mut value = None; - let mut ty = None; - - while let Some(key) = access.next_key::<&str>()? { - match key { - "value" => { - if value.is_some() { - return Err(de::Error::duplicate_field("value")); - } - value = Some(access.next_value::<&str>()?); - } - "type" => { - if ty.is_some() { - return Err(de::Error::duplicate_field("type")); - } - ty = Some(access.next_value::<&str>()?); - } - _ => { - return Err(de::Error::unknown_field(key, &["value", "type"])); - } + // Check for duplicate assignments + if map.insert(name.clone(), value).is_some() { + return Err(Error::WitnessMultipleAssignments(name.clone())); } } - let ty = match ty { - Some(s) => ResolvedType::parse_from_str(s).map_err(de::Error::custom)?, - None => return Err(de::Error::missing_field("type")), - }; - match value { - Some(s) => Value::parse_from_str(s, &ty).map_err(de::Error::custom), - None => Err(de::Error::missing_field("value")), - } + Ok(Self::from(map)) } } -impl<'de> Deserialize<'de> for Value { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_map(ValueMapVisitor) - } -} +impl Arguments { + /// Deserialize program arguments from JSON with compiler-provided type context. + /// + /// Similar to WitnessValues::from_json_with_types, but for function parameters. + /// Types are resolved from the compiled program through Parameters, not from JSON. + /// + /// Supports the same special fields as WitnessValues: + /// - "description": Top-level comment for the arguments file (ignored) + /// - "_": Comments/hints for specific parameters (ignored) + /// + /// # Arguments + /// + /// * `json` - JSON string with argument values in simple string format + /// * `parameters` - Parameter type information from the compiled program + /// + /// # Example JSON Format + /// + /// ```json + /// { + /// "param_a": "42", + /// "_param_a": "Initial seed value", + /// "param_b": "0xabcd", + /// "_param_b": "Configuration flags", + /// "description": "Program arguments for initialization" + /// } + /// ``` + /// + /// # Errors + /// + /// Returns an error if: + /// - JSON parsing fails + /// - A parameter is not found in parameters + /// - Value parsing fails for the resolved type + /// - A parameter is assigned multiple times + pub fn from_json_with_types( + json: &str, + parameters: &crate::witness::Parameters, + ) -> Result { + let json_value: serde_json::Map = + serde_json::from_str(json) + .map_err(|e| Error::InvalidJsonFormat(format!("Failed to parse JSON: {}", e)))?; -struct ParserVisitor(std::marker::PhantomData); + let mut map = HashMap::new(); -impl<'de, A: ParseFromStr> de::Visitor<'de> for ParserVisitor { - type Value = A; + for (name_str, value_json) in json_value.iter() { + // Skip special fields that are allowed for documentation/comments + if name_str == "description" { + // Top-level description for the entire arguments file + continue; + } + + if name_str.starts_with("_") { + // Convention: fields starting with _ are comments/hints + continue; + } - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a valid string") - } + let name = WitnessName::from_str_unchecked(name_str); - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - A::parse_from_str(value).map_err(E::custom) - } -} + // Retrieve type from compiler-provided context (Parameters) + let ty = parameters + .get(&name) + .ok_or_else(|| Error::UndefinedParameter(name.clone()))?; -impl<'de> Deserialize<'de> for WitnessName { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_str(ParserVisitor::(std::marker::PhantomData)) - } -} + // Extract value string from JSON (simple format) + // Type annotation needed here for serde_json::Value + let value_str: &str = match value_json.as_str() { + Some(s) => s, + None => { + return Err(Error::InvalidJsonFormat(format!( + "Parameter `{}` must be a string value, got {}", + name, + match value_json { + serde_json::Value::Null => "null", + serde_json::Value::Bool(_) => "boolean", + serde_json::Value::Number(_) => "number", + serde_json::Value::String(_) => "string", + serde_json::Value::Array(_) => "array", + serde_json::Value::Object(_) => "object", + } + ))) + } + }; -struct WitnessMapSerializer<'a>(&'a HashMap); + // Parse value using compiler-provided type + let value = Value::parse_from_str(value_str, ty)?; -impl<'a> Serialize for WitnessMapSerializer<'a> { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut map = serializer.serialize_map(Some(self.0.len()))?; - for (name, value) in self.0 { - map.serialize_entry(name.as_inner(), &ValueMapSerializer(value))?; + // Check for duplicate assignments + if map.insert(name.clone(), value).is_some() { + return Err(Error::ArgumentMultipleAssignments(name.clone())); + } } - map.end() - } -} - -struct ValueMapSerializer<'a>(&'a Value); - -impl<'a> Serialize for ValueMapSerializer<'a> { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut map = serializer.serialize_map(Some(2))?; - map.serialize_entry("value", &self.0.to_string())?; - map.serialize_entry("type", &self.0.ty().to_string())?; - map.end() - } -} - -impl Serialize for WitnessValues { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - WitnessMapSerializer(self.as_inner()).serialize(serializer) - } -} -impl Serialize for Arguments { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - WitnessMapSerializer(self.as_inner()).serialize(serializer) + Ok(Self::from(map)) } } @@ -187,16 +237,27 @@ impl Serialize for Arguments { mod tests { use super::*; - #[test] - fn witness_serde_duplicate_assignment() { - let s = r#"{ - "A": { "value": "42", "type": "u32" }, - "A": { "value": "43", "type": "u16" } -}"#; - - match serde_json::from_str::(s) { - Ok(_) => panic!("Duplicate witness assignment was falsely accepted"), - Err(error) => assert!(error.to_string().contains("Name `A` is assigned twice")), - } - } + // Tests for new context-aware deserialization should be added here + // Example test case for simplified JSON format: + // + // #[test] + // fn witness_from_json_with_types() { + // let json = r#"{ "A": "42", "B": "0x12345678" }"#; + // let witness_types = /* create WitnessTypes with A: u32, B: u32 */; + // let witness = WitnessValues::from_json_with_types(json, &witness_types).unwrap(); + // assert_eq!(witness.get(&WitnessName::from_str_unchecked("A")).unwrap().ty(), u32_type); + // } + // + // #[test] + // fn witness_with_description_and_hints() { + // let json = r#"{ + // "A": "42", + // "_A": "Important seed value", + // "description": "Test witness with comments" + // }"#; + // let witness_types = /* create WitnessTypes with A: u32 */; + // let witness = WitnessValues::from_json_with_types(json, &witness_types).unwrap(); + // // Description and _A should be ignored, only A should be in witness + // assert_eq!(witness.iter().count(), 1); + // } } diff --git a/src/witness.rs b/src/witness.rs index bb55fe6..bc12fd6 100644 --- a/src/witness.rs +++ b/src/witness.rs @@ -39,12 +39,6 @@ macro_rules! impl_name_type_map { macro_rules! impl_name_value_map { ($wrapper: ident, $module_name: expr) => { impl $wrapper { - /// Access the inner map. - #[cfg(feature = "serde")] - pub(crate) fn as_inner(&self) -> &HashMap { - &self.0 - } - /// Get the value that is assigned to the given name. pub fn get(&self, name: &WitnessName) -> Option<&Value> { self.0.get(name) @@ -266,8 +260,8 @@ fn main() { #[test] fn missing_witness_module() { match WitnessValues::parse_from_str("") { - Ok(_) => panic!("Missing witness module was falsely accepted"), - Err(error) => assert!(error.to_string().contains("module `witness` is missing")), + Ok(values) => assert!(values.iter().next().is_none()), + Err(error) => panic!("Expected empty witness values, but got error: {}", error), } }