Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 162 additions & 2 deletions crates/pack-guest-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1740,7 +1740,22 @@ fn parse_func_sigs_into(
sigs: &mut Vec<metadata::FuncSig>,
types: &[wit_parser::TypeDef],
) -> Result<(), String> {
// Typedefs declared inside this block (e.g. `record foo { ... }`) shadow
// and extend the outer `types` slice for ref resolution within the block.
let mut local_types: Vec<wit_parser::TypeDef> = types.to_vec();

while !parser.peek_is_symbol('}') && !parser.is_eof() {
// Try a typedef first — records/variants/etc declared inside an
// interface block are scoped to that block and resolve refs structurally.
if let Some(td) = wit_parser::try_parse_typedef_public(parser)
.map_err(|e| format!("type definition error: {}", e))?
{
local_types.push(td);
parser.accept_symbol(',');
parser.accept_symbol(';');
continue;
}

let (iface, name) = parse_function_path(parser, interface)?;
parser.expect_symbol(':').map_err(|e| e.to_string())?;
parser.accept_ident("func");
Expand All @@ -1750,13 +1765,13 @@ fn parse_func_sigs_into(
let params: Vec<(String, metadata::TypeDesc)> = func
.params
.iter()
.map(|(n, t)| (n.clone(), metadata::wit_type_to_type_desc(t, types)))
.map(|(n, t)| (n.clone(), metadata::wit_type_to_type_desc(t, &local_types)))
.collect();

let results: Vec<metadata::TypeDesc> = func
.results
.iter()
.map(|t| metadata::wit_type_to_type_desc(t, types))
.map(|t| metadata::wit_type_to_type_desc(t, &local_types))
.collect();

sigs.push(metadata::FuncSig {
Expand All @@ -1771,3 +1786,148 @@ fn parse_func_sigs_into(
}
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use packr_abi::{
hash_function, hash_interface, hash_record, hash_result, Binding, HASH_STRING,
};

/// Parser accepts `record name { ... }` inside `imports { iface { ... } }`,
/// scoping the type to that interface block, and refs in subsequent function
/// signatures resolve to its structural hash. This is the original
/// agentry-actor blocker from the user report.
#[test]
fn parses_record_inside_imports_interface_block() {
let src = r#"
imports {
theater:simple/podman {
record container-spec {
image: string,
name: string,
}

run: func(spec: container-spec) -> result<string, string>
}
}
"#;

// Should parse without error (this is the bug from agentry).
let bytes = parse_and_encode_metadata(src).expect("parse");
assert!(!bytes.is_empty());

// Decoded metadata should carry an import-hash for the interface that
// matches the structural hash computed by hand.
let record_hash = hash_record(&[("image", HASH_STRING), ("name", HASH_STRING)]);
let func_hash = hash_function(&[record_hash], &[hash_result(&HASH_STRING, &HASH_STRING)]);
let expected_iface_hash = hash_interface(
"theater:simple/podman",
&[],
&[Binding {
name: "run",
hash: func_hash,
}],
);

// Re-derive via the public path used by the metadata encoder.
let mut sigs = Vec::new();
let tokens = wit_parser::tokenize(src).expect("tokenize");
let mut parser = wit_parser::make_parser(tokens);
parser.accept_ident("imports");
parser.expect_symbol('{').expect("imports {");
parse_import_sigs(&mut parser, &mut sigs, &[]).expect("import sigs");

let iface_hashes = metadata::compute_interface_hashes(&sigs);
assert_eq!(iface_hashes.len(), 1);
assert_eq!(iface_hashes[0].name, "theater:simple/podman");
assert_eq!(iface_hashes[0].hash, expected_iface_hash);
}

/// Records declared inside one interface block do not leak into a sibling
/// interface block's resolution scope.
#[test]
fn record_scope_does_not_leak_between_sibling_interfaces() {
let src = r#"
imports {
ns:pkg/a {
record shape {
a: string,
}
do-a: func(s: shape) -> string
}
ns:pkg/b {
// No `shape` typedef here — `shape` should remain unresolved.
do-b: func(s: shape) -> string
}
}
"#;

let mut sigs = Vec::new();
let tokens = wit_parser::tokenize(src).expect("tokenize");
let mut parser = wit_parser::make_parser(tokens);
parser.accept_ident("imports");
parser.expect_symbol('{').expect("imports {");
parse_import_sigs(&mut parser, &mut sigs, &[]).expect("import sigs");

let iface_hashes = metadata::compute_interface_hashes(&sigs);
let h_a = iface_hashes.iter().find(|h| h.name == "ns:pkg/a").unwrap();
let h_b = iface_hashes.iter().find(|h| h.name == "ns:pkg/b").unwrap();

// `a` resolves `shape` structurally; `b` falls back to opaque (HASH_SELF_REF
// via TypeDesc::Value). The hashes must differ.
assert_ne!(h_a.hash, h_b.hash);
}

/// Top-level typedefs remain visible inside imports/exports interface blocks.
#[test]
fn top_level_typedef_visible_inside_interface_block() {
let src = r#"
record point {
x: s32,
y: s32,
}

imports {
ns:pkg/geo {
move: func(p: point) -> point
}
}
"#;
let bytes = parse_and_encode_metadata(src).expect("parse");
assert!(!bytes.is_empty());
}

/// `result<_, E>` ok-arm hashes as Bool (and `_` in err-arm as String) by
/// convention — see the comment in `pack/src/parser/pact.rs::parse_result`.
/// The actor side must match this exactly so hashes converge.
#[test]
fn result_underscore_uses_bool_string_convention() {
let src = r#"
imports {
ns:pkg/api {
do-nothing: func() -> result<_, string>
}
}
"#;
let mut sigs = Vec::new();
let tokens = wit_parser::tokenize(src).expect("tokenize");
let mut parser = wit_parser::make_parser(tokens);
parser.accept_ident("imports");
parser.expect_symbol('{').expect("imports {");
parse_import_sigs(&mut parser, &mut sigs, &[]).expect("import sigs");

use packr_abi::HASH_BOOL;
let func_hash = hash_function(&[], &[hash_result(&HASH_BOOL, &HASH_STRING)]);
let expected = hash_interface(
"ns:pkg/api",
&[],
&[Binding {
name: "do-nothing",
hash: func_hash,
}],
);
let iface_hashes = metadata::compute_interface_hashes(&sigs);
assert_eq!(iface_hashes[0].hash, expected);
}
}
3 changes: 3 additions & 0 deletions crates/pack-guest-macros/src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,9 @@ pub fn wit_type_to_type_desc(
TypeDesc::Option(Box::new(wit_type_to_type_desc(inner, types)))
}
crate::wit_parser::Type::Result { ok, err } => TypeDesc::Result {
// `_` maps to Bool for ok / String for err to match the handler-side
// pact parser's convention (see `parse_result` in pack/src/parser/pact.rs).
// Semantically odd, but the two sides must agree on the hash.
ok: Box::new(
ok.as_ref()
.map_or(TypeDesc::Bool, |t| wit_type_to_type_desc(t, types)),
Expand Down
95 changes: 44 additions & 51 deletions src/interface_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@

use crate::metadata::TypeHash;
use crate::parser::{MetadataValue, PactExport, PactInterface};
use crate::types::Type;
use sha2::Digest;
use crate::types::{Type, TypeDef};

// ============================================================================
// PackType Trait - Maps Rust types to Pack types
Expand Down Expand Up @@ -194,55 +193,30 @@ pub struct FuncSignature {
}

impl FuncSignature {
/// Compute the hash of this function signature.
/// Compute the hash of this function signature without resolving named refs.
///
/// Equivalent to `hash_in(&[])`. Suitable for `InterfaceImpl::func`-built
/// signatures where types come from `PackType` (no `Type::Ref`s appear).
pub fn hash(&self) -> TypeHash {
// Convert Type to TypeHash for each param/result
let param_hashes: Vec<_> = self.params.iter().map(type_to_hash).collect();
let result_hashes: Vec<_> = self.results.iter().map(type_to_hash).collect();

crate::metadata::hash_function(&param_hashes, &result_hashes)
self.hash_in(&[])
}
}

/// Convert a Pack Type to a TypeHash.
fn type_to_hash(ty: &Type) -> TypeHash {
use crate::metadata::*;

match ty {
Type::Bool => HASH_BOOL,
Type::U8 => HASH_U8,
Type::U16 => HASH_U16,
Type::U32 => HASH_U32,
Type::U64 => HASH_U64,
Type::S8 => HASH_S8,
Type::S16 => HASH_S16,
Type::S32 => HASH_S32,
Type::S64 => HASH_S64,
Type::F32 => HASH_F32,
Type::F64 => HASH_F64,
Type::Char => HASH_CHAR,
Type::String => HASH_STRING,
Type::Unit => hash_tuple(&[]), // Unit is empty tuple
Type::List(inner) => hash_list(&type_to_hash(inner)),
Type::Option(inner) => hash_option(&type_to_hash(inner)),
Type::Result { ok, err } => hash_result(&type_to_hash(ok), &type_to_hash(err)),
Type::Tuple(items) => {
let hashes: Vec<_> = items.iter().map(type_to_hash).collect();
hash_tuple(&hashes)
}
// Type::Ref references a named type - for host functions using PackType,
// we won't encounter these since PackType maps Rust types to inline types.
// If we do encounter a Ref, use the path name to compute a placeholder hash.
Type::Ref(path) => {
// Named type reference - hash based on the path string
let path_str = path.to_string();
let mut hasher = sha2::Sha256::new();
hasher.update(b"ref:");
hasher.update(path_str.as_bytes());
TypeHash::from_bytes(hasher.finalize().into())
}
// Dynamic value type - use HASH_SELF_REF for consistency with pack-guest-macros
Type::Value => HASH_SELF_REF,
/// Compute the hash, resolving named refs against `types`.
///
/// Used by `from_pact`-built interfaces where function signatures may
/// reference records/variants declared at the pact-interface level.
pub fn hash_in(&self, types: &[TypeDef]) -> TypeHash {
let param_hashes: Vec<_> = self
.params
.iter()
.map(|t| crate::metadata::hash_type_in(t, types))
.collect();
let result_hashes: Vec<_> = self
.results
.iter()
.map(|t| crate::metadata::hash_type_in(t, types))
.collect();
crate::metadata::hash_function(&param_hashes, &result_hashes)
}
}

Expand All @@ -258,6 +232,12 @@ fn type_to_hash(ty: &Type) -> TypeHash {
pub struct InterfaceImpl {
/// The interface name (e.g., "theater:simple/runtime")
pub name: String,
/// Type definitions in scope for ref resolution when hashing.
///
/// Populated by `from_pact` from `pact.types`. Manually-constructed
/// interfaces (via `new` + `func`) leave this empty — their signatures
/// come from `PackType` and don't contain `Type::Ref`s.
pub types: Vec<TypeDef>,
/// Function signatures (extracted from Rust types)
pub functions: Vec<FuncSignature>,
}
Expand All @@ -267,6 +247,7 @@ impl InterfaceImpl {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
types: Vec::new(),
functions: Vec::new(),
}
}
Expand Down Expand Up @@ -306,7 +287,7 @@ impl InterfaceImpl {
.iter()
.map(|f| Binding {
name: &f.name,
hash: f.hash(),
hash: f.hash_in(&self.types),
})
.collect();
bindings.sort_by(|a, b| a.name.cmp(b.name));
Expand Down Expand Up @@ -334,7 +315,7 @@ impl InterfaceImpl {
let func = self.functions.iter().find(|f| f.name == *name)?;
bindings.push(Binding {
name: &func.name,
hash: func.hash(),
hash: func.hash_in(&self.types),
});
}

Expand All @@ -355,7 +336,7 @@ impl InterfaceImpl {
self.functions
.iter()
.find(|f| f.name == name)
.map(|f| f.hash())
.map(|f| f.hash_in(&self.types))
}

/// Get the interface name.
Expand Down Expand Up @@ -401,6 +382,18 @@ impl InterfaceImpl {

let mut interface = InterfaceImpl::new(full_name);

// Capture interface-level typedefs so refs in function signatures
// (e.g. `func run(spec: container-spec) -> ...`) resolve structurally
// when the interface hash is computed.
interface.types = pact.types.clone();
// Type-style exports (`PactExport::Type`) also contribute to the
// resolution scope alongside the inline `record/variant/...` defs.
for export in &pact.exports {
if let PactExport::Type(td) = export {
interface.types.push(td.clone());
}
}

// Extract function signatures from exports
for export in &pact.exports {
if let PactExport::Function(func) = export {
Expand Down
11 changes: 6 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,12 @@ pub use interface_impl::{FuncSignature, HostFunc, InterfaceImpl, PackParams, Pac
pub use metadata::{
compute_interface_hash, compute_interface_hashes, decode_metadata, decode_metadata_with_hashes,
encode_metadata, encode_metadata_with_hashes, hash_function, hash_function_from_sig,
hash_interface, hash_list, hash_option, hash_record, hash_result, hash_tuple, hash_type,
hash_variant, validate_value_in_type_space, Binding, CaseDesc, FieldDesc, FunctionSignature,
InterfaceHash, MetadataError, MetadataWithHashes, PackageMetadata, ParamSignature, TypeDesc,
TypeHash, TypeValidationError, HASH_BOOL, HASH_CHAR, HASH_F32, HASH_F64, HASH_FLAGS, HASH_S16,
HASH_S32, HASH_S64, HASH_S8, HASH_STRING, HASH_U16, HASH_U32, HASH_U64, HASH_U8,
hash_function_from_sig_in, hash_interface, hash_list, hash_option, hash_record, hash_result,
hash_tuple, hash_type, hash_type_in, hash_variant, validate_value_in_type_space, Binding,
CaseDesc, FieldDesc, FunctionSignature, InterfaceHash, MetadataError, MetadataWithHashes,
PackageMetadata, ParamSignature, TypeDesc, TypeHash, TypeValidationError, HASH_BOOL, HASH_CHAR,
HASH_F32, HASH_F64, HASH_FLAGS, HASH_S16, HASH_S32, HASH_S64, HASH_S8, HASH_STRING, HASH_U16,
HASH_U32, HASH_U64, HASH_U8,
};
pub use parser::{
parse_pact, parse_pact_dir, parse_pact_dir_with_registry, parse_pact_file, Interface,
Expand Down
Loading
Loading