From 31fbb6f2ba801113d98223d596727fa62d7276ce Mon Sep 17 00:00:00 2001 From: Brian Hardock Date: Thu, 10 Sep 2026 11:42:28 -0600 Subject: [PATCH] Semver spread matching Signed-off-by: Brian Hardock --- LANGUAGE.md | 7 ++++ crates/wac-parser/src/resolution.rs | 41 +++++++++++++++---- .../tests/encoding/spread-semver.wac | 8 ++++ .../tests/encoding/spread-semver.wac.result | 39 ++++++++++++++++++ .../tests/encoding/spread-semver/foo/plug.wat | 9 ++++ .../encoding/spread-semver/foo/socket.wat | 9 ++++ 6 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 crates/wac-parser/tests/encoding/spread-semver.wac create mode 100644 crates/wac-parser/tests/encoding/spread-semver.wac.result create mode 100644 crates/wac-parser/tests/encoding/spread-semver/foo/plug.wat create mode 100644 crates/wac-parser/tests/encoding/spread-semver/foo/socket.wat diff --git a/LANGUAGE.md b/LANGUAGE.md index b243f4f..0de7356 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -305,6 +305,13 @@ arguments of `a:b` first. Any unsatisfied arguments will then be satisfied by the matching exports of instance `c`, followed by any matching the exports of instance `d`. +An export matches an instantiation argument if the names are identical or if +the names are _semver-compatible_. For example, an export named +`a:b/c@0.2.0` will satisfy an instantiation argument named `a:b/c@0.2.1`, as +both are on the `0.2` track; it will not satisfy `a:b/c@0.3.0`. An exact name +match is always preferred over a semver-compatible one. The type of the export +must still be compatible with the type of the instantiation argument. + The above behavior differs from JavaScript's spread argument syntax, which is the inspiration for this syntax, because component instantiation arguments are _named_ and not _positional_. diff --git a/crates/wac-parser/src/resolution.rs b/crates/wac-parser/src/resolution.rs index c0506a3..cc6f9f2 100644 --- a/crates/wac-parser/src/resolution.rs +++ b/crates/wac-parser/src/resolution.rs @@ -10,10 +10,10 @@ use std::{ }; use wac_graph::{ types::{ - BorrowedPackageKey, DefinedType, Enum, ExternKind, Flags, FuncKind, FuncType, FuncTypeId, - Interface, InterfaceId, ItemKind, Package, PackageKey, PrimitiveType, Record, Resource, - ResourceAlias, ResourceId, SubtypeChecker, Type, UsedType, ValueType, Variant, World, - WorldId, + are_semver_compatible, BorrowedPackageKey, DefinedType, Enum, ExternKind, Flags, FuncKind, + FuncType, FuncTypeId, Interface, InterfaceId, ItemKind, Package, PackageKey, PrimitiveType, + Record, Resource, ResourceAlias, ResourceId, SubtypeChecker, Type, UsedType, ValueType, + Variant, World, WorldId, }, CompositionGraph, DefineTypeError, EncodeError, EncodeOptions, ExportError, ImportError, InstantiationArgumentError, NodeId, NodeKind, PackageId, Processor, @@ -2461,10 +2461,35 @@ impl<'a> AstResolver<'a> { continue; } - // Alias a matching export of the instance - if let Some(aliased) = - self.alias_export(state, item, name, id.span, InstanceOperation::Spread)? - { + // Prefer an exact export name match, then fall back to a semver-compatible one + let export_name = { + let exports = match item.kind(&state.graph) { + ItemKind::Instance(id) => &state.graph.types()[id].exports, + _ => unreachable!("item was checked to be an instance"), + }; + + if exports.contains_key(name) { + Some(name.clone()) + } else { + exports + .keys() + .find(|export| are_semver_compatible(export, name)) + .cloned() + } + }; + + let Some(export_name) = export_name else { + continue; + }; + + // Alias the matching export of the instance + if let Some(aliased) = self.alias_export( + state, + item, + &export_name, + id.span, + InstanceOperation::Spread, + )? { spread = true; arguments.insert(name.clone(), (aliased, id.span)); } diff --git a/crates/wac-parser/tests/encoding/spread-semver.wac b/crates/wac-parser/tests/encoding/spread-semver.wac new file mode 100644 index 0000000..2c2b704 --- /dev/null +++ b/crates/wac-parser/tests/encoding/spread-semver.wac @@ -0,0 +1,8 @@ +package test:comp; + +// The plug exports `test:dep/iface@0.2.0`, but the socket imports +// `test:dep/iface@0.2.1`; the spread should match them via semver compatibility. +let plug = new foo:plug { }; +let socket = new foo:socket { ...plug }; + +export socket...; diff --git a/crates/wac-parser/tests/encoding/spread-semver.wac.result b/crates/wac-parser/tests/encoding/spread-semver.wac.result new file mode 100644 index 0000000..b4d4b4f --- /dev/null +++ b/crates/wac-parser/tests/encoding/spread-semver.wac.result @@ -0,0 +1,39 @@ +(component + (type (;0;) + (component + (type (;0;) + (instance + (type (;0;) (func)) + (export (;0;) "foo" (func (type 0))) + ) + ) + (export (;0;) "test:dep/iface@0.2.0" (instance (type 0))) + ) + ) + (import "unlocked-dep=" (component (;0;) (type 0))) + (instance $plug (;0;) (instantiate 0)) + (alias export $plug "test:dep/iface@0.2.0" (instance (;1;))) + (type (;1;) + (component + (type (;0;) + (instance + (type (;0;) (func)) + (export (;0;) "foo" (func (type 0))) + ) + ) + (import "test:dep/iface@0.2.1" (instance (;0;) (type 0))) + (type (;1;) (func)) + (export (;0;) "bar" (func (type 1))) + ) + ) + (import "unlocked-dep=" (component (;1;) (type 1))) + (instance $socket (;2;) (instantiate 1 + (with "test:dep/iface@0.2.1" (instance 1)) + ) + ) + (alias export $socket "bar" (func (;0;))) + (export (;1;) "bar" (func 0)) + (@producers + (processed-by "wac-parser" "0.11.0") + ) +) diff --git a/crates/wac-parser/tests/encoding/spread-semver/foo/plug.wat b/crates/wac-parser/tests/encoding/spread-semver/foo/plug.wat new file mode 100644 index 0000000..eec870b --- /dev/null +++ b/crates/wac-parser/tests/encoding/spread-semver/foo/plug.wat @@ -0,0 +1,9 @@ +(component + (core module $m + (func (export "foo")) + ) + (core instance $i (instantiate $m)) + (func $foo (canon lift (core func $i "foo"))) + (instance $iface (export "foo" (func $foo))) + (export "test:dep/iface@0.2.0" (instance $iface)) +) diff --git a/crates/wac-parser/tests/encoding/spread-semver/foo/socket.wat b/crates/wac-parser/tests/encoding/spread-semver/foo/socket.wat new file mode 100644 index 0000000..0b1f126 --- /dev/null +++ b/crates/wac-parser/tests/encoding/spread-semver/foo/socket.wat @@ -0,0 +1,9 @@ +(component + (import "test:dep/iface@0.2.1" + (instance $i + (export "foo" (func)) + ) + ) + (alias export $i "foo" (func $foo)) + (export "bar" (func $foo)) +)