From 4f94dcbb61647e43ef51d6f237b4324d9c90083d Mon Sep 17 00:00:00 2001 From: colin Date: Sat, 9 May 2026 11:32:04 -0400 Subject: [PATCH] static value types via KnownValueType + single-param unwrap Two related fixes for typed actor state in Theater. KnownValueType trait: provides ValueType at the type level, so empty containers (Vec, Option, BTreeSet, BTreeMap, [T; N], Result) carry correct elem_type / inner_type info even when the value is empty. Previously From> inferred elem_type from the items and defaulted to S32 for empty Vecs, so an empty list field encoded as list and theater's host-side type validator rejected it. GraphValue derive emits KnownValueType impls for structs (Record), tuple structs (Tuple), unit structs (Unit), and variants (Variant). Pack's host-side PackType collapses to a blanket impl over Into + KnownValueType, removing two parallel trait hierarchies. Export macro single-param unwrap: a guest function with one typed parameter (e.g. fn list(state: RouterState) -> ...) was decoding the incoming value directly via try_into(). But theater always wraps inputs as Tuple([...args]), so the wasm side received Tuple([state]) and failed with 'failed to convert parameter'. The state-mode and multi-param branches already unwrapped the wrapping tuple; the single-param branch now does too. --- crates/pack-abi/src/lib.rs | 2 +- crates/pack-abi/src/value.rs | 219 +++++++++++++++++++++------- crates/pack-derive/src/lib.rs | 29 ++++ crates/pack-guest-macros/src/lib.rs | 23 ++- crates/pack-guest/src/lib.rs | 2 +- src/abi/value.rs | 126 +++------------- 6 files changed, 227 insertions(+), 174 deletions(-) diff --git a/crates/pack-abi/src/lib.rs b/crates/pack-abi/src/lib.rs index 6a7aa21..490a7ec 100644 --- a/crates/pack-abi/src/lib.rs +++ b/crates/pack-abi/src/lib.rs @@ -61,7 +61,7 @@ pub use hash::{ HASH_U8, }; pub use parse::{parse_value, ParseError}; -pub use value::{FromValue, Value, ValueType}; +pub use value::{FromValue, KnownValueType, Value, ValueType}; // Re-export derive macro when feature is enabled #[cfg(feature = "derive")] diff --git a/crates/pack-abi/src/value.rs b/crates/pack-abi/src/value.rs index 1a7bea2..370f021 100644 --- a/crates/pack-abi/src/value.rs +++ b/crates/pack-abi/src/value.rs @@ -402,26 +402,147 @@ impl From<&str> for Value { } } -impl> From> for Value { +// ============================================================================ +// KnownValueType — compile-time ValueType for a Rust type. +// ============================================================================ +// +// The runtime `infer_type` only works on a concrete value; an empty +// `Vec` or `None: Option` carries no information. To encode an +// empty `Vec` correctly (as `list`, not the default +// `list`), we need T's ValueType at the type level. Types implement +// `KnownValueType` to declare it — primitives statically, records via +// the `#[derive(GraphValue)]` macro. + +/// Compile-time `ValueType` for a Rust type. Used by `From>` and +/// `From>` so that empty containers still encode with the +/// correct element/inner type. +pub trait KnownValueType { + fn known_value_type() -> ValueType; +} + +macro_rules! known_primitive { + ($t:ty, $vt:expr) => { + impl KnownValueType for $t { + fn known_value_type() -> ValueType { + $vt + } + } + }; +} +known_primitive!(bool, ValueType::Bool); +known_primitive!(u8, ValueType::U8); +known_primitive!(u16, ValueType::U16); +known_primitive!(u32, ValueType::U32); +known_primitive!(u64, ValueType::U64); +known_primitive!(i8, ValueType::S8); +known_primitive!(i16, ValueType::S16); +known_primitive!(i32, ValueType::S32); +known_primitive!(i64, ValueType::S64); +known_primitive!(f32, ValueType::F32); +known_primitive!(f64, ValueType::F64); +known_primitive!(char, ValueType::Char); +known_primitive!(String, ValueType::String); + +impl KnownValueType for Vec { + fn known_value_type() -> ValueType { + ValueType::List(Box::new(T::known_value_type())) + } +} + +impl KnownValueType for Option { + fn known_value_type() -> ValueType { + ValueType::Option(Box::new(T::known_value_type())) + } +} + +impl KnownValueType for core::result::Result { + fn known_value_type() -> ValueType { + ValueType::Result { + ok: Box::new(T::known_value_type()), + err: Box::new(E::known_value_type()), + } + } +} + +impl KnownValueType for Box { + fn known_value_type() -> ValueType { + T::known_value_type() + } +} + +impl KnownValueType for () { + fn known_value_type() -> ValueType { + ValueType::Tuple(Vec::new()) + } +} + +/// `Value` is dynamic — its actual ValueType is only known at runtime +/// (`Value::infer_type`). The compile-time fallback is `String`, matching +/// the previous `PackType for Value` behavior. Prefer concrete types when +/// you need accurate static typing. +impl KnownValueType for Value { + fn known_value_type() -> ValueType { + ValueType::String + } +} + +impl KnownValueType for (A,) { + fn known_value_type() -> ValueType { + ValueType::Tuple(alloc::vec![A::known_value_type()]) + } +} + +impl KnownValueType for (A, B) { + fn known_value_type() -> ValueType { + ValueType::Tuple(alloc::vec![A::known_value_type(), B::known_value_type()]) + } +} + +impl KnownValueType for (A, B, C) { + fn known_value_type() -> ValueType { + ValueType::Tuple(alloc::vec![ + A::known_value_type(), + B::known_value_type(), + C::known_value_type() + ]) + } +} + +impl KnownValueType + for (A, B, C, D) +{ + fn known_value_type() -> ValueType { + ValueType::Tuple(alloc::vec![ + A::known_value_type(), + B::known_value_type(), + C::known_value_type(), + D::known_value_type() + ]) + } +} + +// ============================================================================ +// From implementations for collections — use KnownValueType for the +// element/inner type so empty values still carry correct type info. +// ============================================================================ + +impl + KnownValueType> From> for Value { fn from(v: Vec) -> Self { let items: Vec = v.into_iter().map(Into::into).collect(); - // Infer elem_type from first item, default to S32 - let elem_type = items - .first() - .map(|v| v.infer_type()) - .unwrap_or(ValueType::S32); - Value::List { elem_type, items } + Value::List { + elem_type: T::known_value_type(), + items, + } } } -impl, const N: usize> From<[T; N]> for Value { +impl + KnownValueType, const N: usize> From<[T; N]> for Value { fn from(v: [T; N]) -> Self { let items: Vec = v.into_iter().map(Into::into).collect(); - let elem_type = items - .first() - .map(|v| v.infer_type()) - .unwrap_or(ValueType::S32); - Value::List { elem_type, items } + Value::List { + elem_type: T::known_value_type(), + items, + } } } @@ -452,17 +573,12 @@ impl, const N: usize> TryFrom } } -impl> From> for Value { +impl + KnownValueType> From> for Value { fn from(v: Option) -> Self { - let (inner_type, value) = match v { - Some(x) => { - let val: Value = x.into(); - let ty = val.infer_type(); - (ty, Some(Box::new(val))) - } - None => (ValueType::S32, None), // Default type for None - }; - Value::Option { inner_type, value } + Value::Option { + inner_type: T::known_value_type(), + value: v.map(|x| Box::new(x.into())), + } } } @@ -663,14 +779,13 @@ impl> TryFrom for Vec { } } -impl + Ord> From> for Value { +impl + KnownValueType + Ord> From> for Value { fn from(v: BTreeSet) -> Self { let items: Vec = v.into_iter().map(Into::into).collect(); - let elem_type = items - .first() - .map(|v| v.infer_type()) - .unwrap_or(ValueType::S32); - Value::List { elem_type, items } + Value::List { + elem_type: T::known_value_type(), + items, + } } } @@ -690,17 +805,18 @@ impl + Ord> TryFrom for BTreeS } } -impl + Ord, V: Into> From> for Value { +impl + KnownValueType + Ord, V: Into + KnownValueType> From> + for Value +{ fn from(v: BTreeMap) -> Self { let items: Vec = v .into_iter() .map(|(k, v)| Value::Tuple(Vec::from([k.into(), v.into()]))) .collect(); - let elem_type = items - .first() - .map(|v| v.infer_type()) - .unwrap_or(ValueType::S32); - Value::List { elem_type, items } + Value::List { + elem_type: ValueType::Tuple(alloc::vec![K::known_value_type(), V::known_value_type()]), + items, + } } } @@ -949,27 +1065,20 @@ impl< // Result conversions (now using Value::Result directly) // ============================================================================ -impl, E: Into> From> for Value { +impl + KnownValueType, E: Into + KnownValueType> + From> for Value +{ fn from(r: core::result::Result) -> Self { - match r { - Ok(v) => { - let val: Value = v.into(); - let ok_type = val.infer_type(); - Value::Result { - ok_type, - err_type: ValueType::String, // Default error type - value: Ok(Box::new(val)), - } - } - Err(e) => { - let val: Value = e.into(); - let err_type = val.infer_type(); - Value::Result { - ok_type: ValueType::S32, // Default ok type - err_type, - value: Err(Box::new(val)), - } - } + let ok_type = T::known_value_type(); + let err_type = E::known_value_type(); + let value = match r { + Ok(v) => Ok(Box::new(v.into())), + Err(e) => Err(Box::new(e.into())), + }; + Value::Result { + ok_type, + err_type, + value, } } } diff --git a/crates/pack-derive/src/lib.rs b/crates/pack-derive/src/lib.rs index 7fb514c..f452c52 100644 --- a/crates/pack-derive/src/lib.rs +++ b/crates/pack-derive/src/lib.rs @@ -203,6 +203,14 @@ fn derive_struct( } } } + + impl #impl_generics #krate::KnownValueType for #name #ty_generics #where_clause { + fn known_value_type() -> #krate::ValueType { + #krate::ValueType::Record( + #krate::__private::String::from(#type_name_str) + ) + } + } } } Fields::Unnamed(fields) => { @@ -219,6 +227,7 @@ fn derive_struct( }).collect(); let field_count = fields.unnamed.len(); + let field_types: Vec<_> = fields.unnamed.iter().map(|f| &f.ty).collect(); quote! { impl #impl_generics #krate::__private::From<#name #ty_generics> for #krate::Value #where_clause { @@ -251,6 +260,14 @@ fn derive_struct( } } } + + impl #impl_generics #krate::KnownValueType for #name #ty_generics #where_clause { + fn known_value_type() -> #krate::ValueType { + #krate::ValueType::Tuple(#krate::__private::vec![ + #(<#field_types as #krate::KnownValueType>::known_value_type()),* + ]) + } + } } } Fields::Unit => { @@ -282,6 +299,12 @@ fn derive_struct( } } } + + impl #impl_generics #krate::KnownValueType for #name #ty_generics #where_clause { + fn known_value_type() -> #krate::ValueType { + #krate::ValueType::Tuple(#krate::__private::vec![]) + } + } } } } @@ -500,6 +523,12 @@ fn derive_enum( } } } + + impl #impl_generics #krate::KnownValueType for #name #ty_generics #where_clause { + fn known_value_type() -> #krate::ValueType { + #krate::ValueType::Variant(#krate::__private::String::from(#type_name_str)) + } + } } } diff --git a/crates/pack-guest-macros/src/lib.rs b/crates/pack-guest-macros/src/lib.rs index 1693676..811b925 100644 --- a/crates/pack-guest-macros/src/lib.rs +++ b/crates/pack-guest-macros/src/lib.rs @@ -406,20 +406,27 @@ pub fn export(attr: TokenStream, item: TokenStream) -> TokenStream { Ok(output.into()) } } else if param_names.len() == 1 { - // Single typed parameter - extract from value directly + // Single typed parameter. Theater always wraps inputs in a Tuple, so + // a 1-param function receives Tuple([arg]) — unwrap before converting. + // Fall back to a direct try_into() for callers that pass the value + // unwrapped. let param_name = ¶m_names[0]; let param_type = ¶m_types[0]; quote! { - // Extract single typed parameter - let #param_name: #param_type = match value.try_into() { - Ok(v) => v, - Err(_) => return Err("failed to convert parameter"), + let #param_name: #param_type = match value { + packr_guest::Value::Tuple(mut items) if items.len() == 1 => { + match items.remove(0).try_into() { + Ok(v) => v, + Err(_) => return Err("failed to convert parameter"), + } + } + other => match other.try_into() { + Ok(v) => v, + Err(_) => return Err("failed to convert parameter"), + }, }; - // Call user's function let output = #inner_fn_name(#param_name); - - // Convert output to Value Ok(output.into()) } } else { diff --git a/crates/pack-guest/src/lib.rs b/crates/pack-guest/src/lib.rs index 6233ea1..567b02f 100644 --- a/crates/pack-guest/src/lib.rs +++ b/crates/pack-guest/src/lib.rs @@ -34,7 +34,7 @@ pub extern crate alloc; pub use packr_guest_macros::{export, import, import_from, pack_types, wit, world}; // Re-export useful types from pack-abi -pub use packr_abi::{decode, encode, ConversionError, FromValue, Value, ValueType}; +pub use packr_abi::{decode, encode, ConversionError, FromValue, KnownValueType, Value, ValueType}; // Re-export derive macro #[cfg(feature = "derive")] diff --git a/src/abi/value.rs b/src/abi/value.rs index 9f6267f..6dddc14 100644 --- a/src/abi/value.rs +++ b/src/abi/value.rs @@ -1,117 +1,25 @@ -//! Pack type trait for compile-time type information +//! Pack type trait for compile-time type information. +//! +//! `PackType` is the host-side trait used by `func_typed_result` and +//! similar wrappers to obtain a `ValueType` at compile time. It's a thin +//! wrapper over `KnownValueType` (which lives in `packr-abi` and is also +//! used by guests). Anything that's `Into + KnownValueType` is +//! automatically a `PackType`. use super::{Value, ValueType}; +pub use packr_abi::KnownValueType; -/// Trait for types that can provide their ValueType at compile time. +/// Trait for types that can provide their `ValueType` at compile time. /// -/// This is used by `func_typed_result` and `func_async_result` to determine -/// the correct type tags for Result encoding, even when we only have a value -/// for one variant (Ok or Err). -pub trait PackType: Into { - /// Returns the ValueType for this type. - fn value_type() -> ValueType; -} - -// Primitive type implementations for PackType -impl PackType for bool { - fn value_type() -> ValueType { - ValueType::Bool - } -} - -impl PackType for u8 { - fn value_type() -> ValueType { - ValueType::U8 - } -} - -impl PackType for u16 { - fn value_type() -> ValueType { - ValueType::U16 - } -} - -impl PackType for u32 { +/// This is used by `func_typed_result` and `func_async_result` to +/// determine the correct type tags for `Result` encoding, even when we +/// only have a value for one variant (Ok or Err). +pub trait PackType: Into + KnownValueType { + /// Returns the `ValueType` for this type. Defaults to + /// `KnownValueType::known_value_type`. fn value_type() -> ValueType { - ValueType::U32 + ::known_value_type() } } -impl PackType for u64 { - fn value_type() -> ValueType { - ValueType::U64 - } -} - -impl PackType for i8 { - fn value_type() -> ValueType { - ValueType::S8 - } -} - -impl PackType for i16 { - fn value_type() -> ValueType { - ValueType::S16 - } -} - -impl PackType for i32 { - fn value_type() -> ValueType { - ValueType::S32 - } -} - -impl PackType for i64 { - fn value_type() -> ValueType { - ValueType::S64 - } -} - -impl PackType for f32 { - fn value_type() -> ValueType { - ValueType::F32 - } -} - -impl PackType for f64 { - fn value_type() -> ValueType { - ValueType::F64 - } -} - -impl PackType for char { - fn value_type() -> ValueType { - ValueType::Char - } -} - -impl PackType for String { - fn value_type() -> ValueType { - ValueType::String - } -} - -// Vec for list types -impl PackType for Vec { - fn value_type() -> ValueType { - ValueType::List(Box::new(T::value_type())) - } -} - -// Option for option types -impl PackType for Option { - fn value_type() -> ValueType { - ValueType::Option(Box::new(T::value_type())) - } -} - -// Value itself - infers type at runtime (fallback for dynamic typing) -// Note: This defaults to String when we can't know the type statically. -// For accurate type encoding, use concrete types instead of Value. -impl PackType for Value { - fn value_type() -> ValueType { - // When using Value directly, we can't know the type statically. - // Default to String as a fallback - this matches the previous behavior. - ValueType::String - } -} +impl + KnownValueType> PackType for T {}