use super::*;
use cviz::model::{
FuncSignature, InstanceInterface, InterfaceType, TypeArena, ValueType, ValueTypeId,
};
use std::collections::{BTreeMap, HashMap};
fn validate_component(bytes: &[u8]) {
let mut validator = wasmparser::Validator::new_with_features(wasmparser::WasmFeatures::all());
if let Err(e) = validator.validate_all(bytes) {
let dbg_path = std::env::temp_dir().join("splicer_failing_adapter.wasm");
let _ = std::fs::write(&dbg_path, bytes);
panic!(
"generated adapter should be a valid component: {e}\n\
(raw bytes written to {}, use `wasm-tools print` to inspect)",
dbg_path.display(),
);
}
}
fn wat_type(id: ValueTypeId, arena: &TypeArena) -> String {
match arena.lookup_val(id) {
ValueType::S32 => "s32".into(),
ValueType::U32 => "u32".into(),
ValueType::S64 => "s64".into(),
ValueType::U64 => "u64".into(),
ValueType::S8 => "s8".into(),
ValueType::U8 => "u8".into(),
ValueType::S16 => "s16".into(),
ValueType::U16 => "u16".into(),
ValueType::F32 => "f32".into(),
ValueType::F64 => "f64".into(),
ValueType::Bool => "bool".into(),
ValueType::Char => "char".into(),
ValueType::String => "string".into(),
ValueType::List(inner) => format!("(list {})", wat_type(*inner, arena)),
ValueType::FixedSizeList(inner, n) => {
format!("(list {} {n})", wat_type(*inner, arena))
}
ValueType::Option(inner) => format!("(option {})", wat_type(*inner, arena)),
ValueType::Result { ok, err } => {
let ok_str = ok.map(|id| wat_type(id, arena));
let err_str = err.map(|id| wat_type(id, arena));
match (ok_str, err_str) {
(Some(o), Some(e)) => format!("(result {o} (error {e}))"),
(Some(o), None) => format!("(result {o})"),
(None, Some(e)) => format!("(result (error {e}))"),
(None, None) => "(result)".into(),
}
}
ValueType::Tuple(ids) => {
let inner: Vec<String> = ids.iter().map(|id| wat_type(*id, arena)).collect();
format!("(tuple {})", inner.join(" "))
}
ValueType::Record(fields) => {
let inner: Vec<String> = fields
.iter()
.map(|(name, id)| format!(r#"(field "{name}" {})"#, wat_type(*id, arena)))
.collect();
format!("(record {})", inner.join(" "))
}
ValueType::Variant(cases) => {
let inner: Vec<String> = cases
.iter()
.map(|(name, opt_id)| match opt_id {
Some(id) => format!(r#"(case "{name}" {})"#, wat_type(*id, arena)),
None => format!(r#"(case "{name}")"#),
})
.collect();
format!("(variant {})", inner.join(" "))
}
ValueType::Enum(tags) => {
let inner: Vec<String> = tags.iter().map(|t| format!(r#""{t}""#)).collect();
format!("(enum {})", inner.join(" "))
}
ValueType::Flags(names) => {
let inner: Vec<String> = names.iter().map(|n| format!(r#""{n}""#)).collect();
format!("(flags {})", inner.join(" "))
}
other => panic!(
"wat_type: synth split helper only supports primitive + string + \
list + compound (option/result/tuple/record/variant/enum/flags) \
types, got {other:?}. For resources, use a dedicated WAT template."
),
}
}
#[derive(Clone, Copy)]
enum SplitKind {
Consumer,
Provider,
}
fn synth_split(
target: &str,
iface: &InterfaceType,
arena: &TypeArena,
kind: SplitKind,
) -> tempfile::NamedTempFile {
let iface_inst = match iface {
InterfaceType::Instance(i) => i,
_ => panic!("synth_split: bare function interfaces not supported"),
};
let has_resources = iface_inst
.type_exports
.values()
.any(|&vid| matches!(arena.lookup_val(vid), ValueType::Resource(_)));
let wat = match (kind, has_resources) {
(SplitKind::Consumer, false) => wat_consumer_primitive_only(target, iface_inst, arena),
(SplitKind::Consumer, true) => wat_consumer_http_handler_shape(target),
(SplitKind::Provider, false) => wat_provider_primitive_only(target, iface_inst, arena),
(SplitKind::Provider, true) => wat_provider_http_handler_shape(target),
};
let bytes = wat::parse_str(&wat).unwrap_or_else(|e| {
panic!("synth split WAT failed to parse: {e}\n\n--- WAT ---\n{wat}\n--- end ---")
});
let mut tmp = tempfile::NamedTempFile::new().expect("make tempfile");
std::io::Write::write_all(&mut tmp, &bytes).expect("write synth split");
tmp
}
fn wat_consumer_primitive_only(
target: &str,
iface: &InstanceInterface,
arena: &TypeArena,
) -> String {
let mut compounds: HashMap<ValueTypeId, u32> = HashMap::new();
let mut body = String::new();
let mut type_idx: u32 = 0;
for (export_name, &vid) in &iface.type_exports {
if matches!(
arena.lookup_val(vid),
ValueType::Resource(_) | ValueType::AsyncHandle
) {
continue;
}
let body_str = wat_compound_body(vid, arena, &compounds);
body.push_str(&format!(" (type (;{type_idx};) {body_str})\n"));
let decl_idx = type_idx;
type_idx += 1;
body.push_str(&format!(
" (export (;{type_idx};) \"{export_name}\" (type (eq {decl_idx})))\n"
));
compounds.insert(vid, type_idx);
type_idx += 1;
}
let mut export_lines = String::new();
for (name, sig) in &iface.functions {
let params: Vec<String> = sig
.param_names
.iter()
.zip(sig.params.iter())
.map(|(pname, &pid)| {
format!(
r#"(param "{pname}" {})"#,
wat_type_ctx(pid, arena, &compounds)
)
})
.collect();
let result = match sig.results.first() {
Some(&rid) => format!(" (result {})", wat_type_ctx(rid, arena, &compounds)),
None => String::new(),
};
let async_kw = if sig.is_async { "async " } else { "" };
let func_ty_id = format!("$fn_{}", name.replace('-', "_"));
body.push_str(&format!(
" (type {func_ty_id} (func {async_kw}{}{result}))\n",
params.join(" "),
));
export_lines.push_str(&format!(
" (export \"{name}\" (func (type {func_ty_id})))\n"
));
}
body.push_str(&export_lines);
format!(
"(component\n (type $iface (instance\n{body} ))\n (import \"{target}\" (instance (type $iface)))\n)\n"
)
}
fn wat_compound_body(
id: ValueTypeId,
arena: &TypeArena,
compounds: &HashMap<ValueTypeId, u32>,
) -> String {
match arena.lookup_val(id) {
ValueType::Record(fields) => {
let inner: Vec<String> = fields
.iter()
.map(|(name, fid)| {
format!(
r#"(field "{name}" {})"#,
wat_type_ctx(*fid, arena, compounds)
)
})
.collect();
format!("(record {})", inner.join(" "))
}
ValueType::Variant(cases) => {
let inner: Vec<String> = cases
.iter()
.map(|(name, opt)| match opt {
Some(cid) => {
format!(
r#"(case "{name}" {})"#,
wat_type_ctx(*cid, arena, compounds)
)
}
None => format!(r#"(case "{name}")"#),
})
.collect();
format!("(variant {})", inner.join(" "))
}
ValueType::Enum(tags) => {
let items: Vec<String> = tags.iter().map(|t| format!(r#""{t}""#)).collect();
format!("(enum {})", items.join(" "))
}
ValueType::Flags(names) => {
let items: Vec<String> = names.iter().map(|n| format!(r#""{n}""#)).collect();
format!("(flags {})", items.join(" "))
}
other => panic!(
"wat_compound_body: {other:?} should not be in the compounds pre-declaration list"
),
}
}
fn wat_type_ctx(
id: ValueTypeId,
arena: &TypeArena,
compounds: &HashMap<ValueTypeId, u32>,
) -> String {
if let Some(idx) = compounds.get(&id) {
return idx.to_string();
}
match arena.lookup_val(id) {
ValueType::List(inner) => format!("(list {})", wat_type_ctx(*inner, arena, compounds)),
ValueType::FixedSizeList(inner, n) => {
format!("(list {} {n})", wat_type_ctx(*inner, arena, compounds))
}
ValueType::Option(inner) => {
format!("(option {})", wat_type_ctx(*inner, arena, compounds))
}
ValueType::Result { ok, err } => {
let ok_str = ok.map(|id| wat_type_ctx(id, arena, compounds));
let err_str = err.map(|id| wat_type_ctx(id, arena, compounds));
match (ok_str, err_str) {
(Some(o), Some(e)) => format!("(result {o} (error {e}))"),
(Some(o), None) => format!("(result {o})"),
(None, Some(e)) => format!("(result (error {e}))"),
(None, None) => "(result)".into(),
}
}
ValueType::Tuple(ids) => {
let inner: Vec<String> = ids
.iter()
.map(|id| wat_type_ctx(*id, arena, compounds))
.collect();
format!("(tuple {})", inner.join(" "))
}
_ => wat_type(id, arena),
}
}
fn wat_consumer_http_handler_shape(target: &str) -> String {
format!(
r#"(component
(type (;0;) (instance
(export "request" (type (sub resource)))
(export "response" (type (sub resource)))
(type (option string))
(type (option u16))
(type (record (field "rcode" 2) (field "info-code" 3)))
(export "DNS-error-payload" (type (eq 4)))
(type (variant
(case "DNS-timeout")
(case "DNS-error" 5)
(case "connection-refused")
(case "internal-error" 2)))
(export "error-code" (type (eq 6)))
))
(import "synth:test/types" (instance (;0;) (type 0)))
(alias export 0 "request" (type (;1;)))
(alias export 0 "response" (type (;2;)))
(alias export 0 "error-code" (type (;3;)))
(type (;4;) (instance
(alias outer 1 1 (type (;0;)))
(export "request" (type (eq 0)))
(alias outer 1 2 (type (;2;)))
(export "response" (type (eq 2)))
(alias outer 1 3 (type (;4;)))
(export "error-code" (type (eq 4)))
(type (;6;) (own 1))
(type (;7;) (own 3))
(type (;8;) (result 7 (error 5)))
(type (;9;) (func async (param "request" 6) (result 8)))
(export "handle" (func (type 9)))
))
(import "{target}" (instance (;1;) (type 4)))
)
"#
)
}
fn wat_provider_primitive_only(
target: &str,
iface: &InstanceInterface,
arena: &TypeArena,
) -> String {
assert_eq!(
iface.functions.len(),
1,
"provider primitive helper: single-function ifaces only"
);
let (name, sig) = iface.functions.iter().next().unwrap();
assert!(!sig.is_async, "provider primitive helper: sync funcs only");
assert_eq!(
sig.params.len(),
2,
"provider primitive helper: 2-param funcs only"
);
assert!(
sig.params
.iter()
.all(|&p| matches!(arena.lookup_val(p), ValueType::S32)),
"provider primitive helper: s32 params only"
);
assert_eq!(sig.results.len(), 1);
assert!(
matches!(arena.lookup_val(sig.results[0]), ValueType::S32),
"provider primitive helper: s32 result only"
);
let pa = &sig.param_names[0];
let pb = &sig.param_names[1];
format!(
r#"(component
(core module (;0;)
(func (export "{name}") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add))
(core instance (;0;) (instantiate 0))
(alias core export 0 "{name}" (core func (;0;)))
(type (;0;) (func (param "{pa}" s32) (param "{pb}" s32) (result s32)))
(func (;0;) (type 0) (canon lift (core func 0)))
(instance (;0;) (export "{name}" (func 0)))
(export "{target}" (instance 0))
)
"#
)
}
fn wat_provider_http_handler_shape(target: &str) -> String {
format!(
r#"(component
(type (;0;) (instance
(export "request" (type (sub resource)))
(export "response" (type (sub resource)))
(type (option string))
(type (option u16))
(type (record (field "rcode" 2) (field "info-code" 3)))
(export "DNS-error-payload" (type (eq 4)))
(type (variant
(case "DNS-timeout")
(case "DNS-error" 5)
(case "connection-refused")
(case "internal-error" 2)))
(export "error-code" (type (eq 6)))
))
(import "synth:test/types" (instance (;0;) (type 0)))
(alias export 0 "request" (type (;1;)))
(alias export 0 "response" (type (;2;)))
(alias export 0 "error-code" (type (;3;)))
(type (;4;) (instance
(alias outer 1 1 (type (;0;)))
(export "request" (type (eq 0)))
(alias outer 1 2 (type (;2;)))
(export "response" (type (eq 2)))
(alias outer 1 3 (type (;4;)))
(export "error-code" (type (eq 4)))
(type (;6;) (own 1))
(type (;7;) (own 3))
(type (;8;) (result 7 (error 5)))
(type (;9;) (func async (param "request" 6) (result 8)))
(export "handle" (func (type 9)))
))
(import "impl:test/handler" (instance (;1;) (type 4)))
(export "{target}" (instance 1))
)
"#
)
}
fn gen_adapter(
target: &str,
hooks: &[&str],
iface: &InterfaceType,
arena: &TypeArena,
kind: SplitKind,
) -> Vec<u8> {
let tmp = tempfile::tempdir().unwrap();
let hook_strings: Vec<String> = hooks.iter().map(|s| s.to_string()).collect();
let split = synth_split(target, iface, arena, kind);
let split_path = split.path().to_str().expect("tempfile path utf-8");
let path = generate_tier1_adapter(
"test-mdl",
target,
&hook_strings,
Some(iface),
tmp.path().to_str().unwrap(),
split_path,
arena,
)
.expect("adapter generation should succeed");
std::fs::read(&path).expect("should read generated adapter file")
}
fn make_iface(funcs: Vec<(&str, FuncSignature)>) -> InterfaceType {
InterfaceType::Instance(InstanceInterface {
functions: funcs.into_iter().map(|(n, s)| (n.to_string(), s)).collect(),
type_exports: BTreeMap::new(),
})
}
fn sig(
is_async: bool,
names: &[&str],
params: Vec<ValueTypeId>,
results: Vec<ValueTypeId>,
) -> FuncSignature {
FuncSignature {
is_async,
param_names: names.iter().map(|s| s.to_string()).collect(),
params,
results,
}
}
#[test]
fn test_adapter_sync_primitives() {
let mut arena = TypeArena::default();
let s32 = arena.intern_val(ValueType::S32);
let iface = make_iface(vec![(
"add",
sig(false, &["a", "b"], vec![s32, s32], vec![s32]),
)]);
let bytes = gen_adapter(
"test:pkg/adder@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_sync_string_return() {
let mut arena = TypeArena::default();
let string = arena.intern_val(ValueType::String);
let iface = make_iface(vec![("get-msg", sig(false, &[], vec![], vec![string]))]);
let bytes = gen_adapter(
"test:pkg/messenger@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_sync_string_roundtrip() {
let mut arena = TypeArena::default();
let string = arena.intern_val(ValueType::String);
let iface = make_iface(vec![(
"echo",
sig(false, &["input"], vec![string], vec![string]),
)]);
let bytes = gen_adapter(
"test:pkg/echo@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_async_string_return() {
let mut arena = TypeArena::default();
let string = arena.intern_val(ValueType::String);
let iface = make_iface(vec![("get-msg", sig(true, &[], vec![], vec![string]))]);
let bytes = gen_adapter(
"test:pkg/messenger@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_async_void_string() {
let mut arena = TypeArena::default();
let string = arena.intern_val(ValueType::String);
let iface = make_iface(vec![("print", sig(true, &["msg"], vec![string], vec![]))]);
let bytes = gen_adapter(
"test:pkg/printer@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
fn build_http_handler_iface(arena: &mut TypeArena) -> InterfaceType {
let string_id = arena.intern_val(ValueType::String);
let opt_string = arena.intern_val(ValueType::Option(string_id));
let u16_id = arena.intern_val(ValueType::U16);
let opt_u16 = arena.intern_val(ValueType::Option(u16_id));
let dns_error_payload = arena.intern_val(ValueType::Record(vec![
("rcode".into(), opt_string),
("info-code".into(), opt_u16),
]));
let error_code = arena.intern_val(ValueType::Variant(vec![
("DNS-timeout".into(), None),
("DNS-error".into(), Some(dns_error_payload)),
("connection-refused".into(), None),
("internal-error".into(), Some(opt_string)),
]));
let request = arena.intern_val(ValueType::Resource("request".into()));
let response = arena.intern_val(ValueType::Resource("response".into()));
let result_ty = arena.intern_val(ValueType::Result {
ok: Some(response),
err: Some(error_code),
});
let func = sig(true, &["request"], vec![request], vec![result_ty]);
InterfaceType::Instance(InstanceInterface {
functions: BTreeMap::from([("handle".to_string(), func)]),
type_exports: BTreeMap::from([
("request".to_string(), request),
("response".to_string(), response),
("error-code".to_string(), error_code),
]),
})
}
#[test]
fn test_adapter_resource_handler() {
let mut arena = TypeArena::default();
let iface = build_http_handler_iface(&mut arena);
let bytes = gen_adapter(
"wasi:http/handler@0.3.0-rc-2026-01-06",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_list_param_sync() {
let mut arena = TypeArena::default();
let u32_id = arena.intern_val(ValueType::U32);
let list_u32 = arena.intern_val(ValueType::List(u32_id));
let iface = make_iface(vec![(
"sum",
sig(false, &["xs"], vec![list_u32], vec![u32_id]),
)]);
let bytes = gen_adapter(
"test:pkg/summer@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_list_result_sync() {
let mut arena = TypeArena::default();
let u32_id = arena.intern_val(ValueType::U32);
let list_u32 = arena.intern_val(ValueType::List(u32_id));
let iface = make_iface(vec![(
"range",
sig(false, &["n"], vec![u32_id], vec![list_u32]),
)]);
let bytes = gen_adapter(
"test:pkg/ranger@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_fixed_size_list_param_sync() {
let mut arena = TypeArena::default();
let u32_id = arena.intern_val(ValueType::U32);
let fsl = arena.intern_val(ValueType::FixedSizeList(u32_id, 4));
let iface = make_iface(vec![("take", sig(false, &["buf"], vec![fsl], vec![]))]);
let bytes = gen_adapter(
"test:pkg/taker@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_list_param_async() {
let mut arena = TypeArena::default();
let u32_id = arena.intern_val(ValueType::U32);
let list_u32 = arena.intern_val(ValueType::List(u32_id));
let iface = make_iface(vec![(
"process",
sig(true, &["xs"], vec![list_u32], vec![]),
)]);
let bytes = gen_adapter(
"test:pkg/processor@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_option_u8_async_result() {
let mut arena = TypeArena::default();
let u8_id = arena.intern_val(ValueType::U8);
let opt_u8 = arena.intern_val(ValueType::Option(u8_id));
let iface = make_iface(vec![("get", sig(true, &[], vec![], vec![opt_u8]))]);
let bytes = gen_adapter(
"test:pkg/get@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_option_u16_async_result() {
let mut arena = TypeArena::default();
let u16_id = arena.intern_val(ValueType::U16);
let opt_u16 = arena.intern_val(ValueType::Option(u16_id));
let iface = make_iface(vec![("get", sig(true, &[], vec![], vec![opt_u16]))]);
let bytes = gen_adapter(
"test:pkg/get@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_result_u8_u8_async_result() {
let mut arena = TypeArena::default();
let u8_id = arena.intern_val(ValueType::U8);
let result = arena.intern_val(ValueType::Result {
ok: Some(u8_id),
err: Some(u8_id),
});
let iface = make_iface(vec![("get", sig(true, &[], vec![], vec![result]))]);
let bytes = gen_adapter(
"test:pkg/get@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_record_with_subword_fields_async_result() {
let mut arena = TypeArena::default();
let bool_id = arena.intern_val(ValueType::Bool);
let u32_id = arena.intern_val(ValueType::U32);
let u16_id = arena.intern_val(ValueType::U16);
let record = arena.intern_val(ValueType::Record(vec![
("flag".into(), bool_id),
("count".into(), u32_id),
("tag".into(), u16_id),
]));
let iface = InterfaceType::Instance(InstanceInterface {
functions: BTreeMap::from([("get".to_string(), sig(true, &[], vec![], vec![record]))]),
type_exports: BTreeMap::from([("my-record".to_string(), record)]),
});
let bytes = gen_adapter(
"test:pkg/get@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_enum_async_result() {
let mut arena = TypeArena::default();
let en = arena.intern_val(ValueType::Enum(vec![
"red".into(),
"green".into(),
"blue".into(),
]));
let iface = InterfaceType::Instance(InstanceInterface {
functions: BTreeMap::from([("get".to_string(), sig(true, &[], vec![], vec![en]))]),
type_exports: BTreeMap::from([("color".to_string(), en)]),
});
let bytes = gen_adapter(
"test:pkg/get@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_mixed_alignment_record_async_result() {
let mut arena = TypeArena::default();
let u8_id = arena.intern_val(ValueType::U8);
let u32_id = arena.intern_val(ValueType::U32);
let u16_id = arena.intern_val(ValueType::U16);
let u64_id = arena.intern_val(ValueType::U64);
let record = arena.intern_val(ValueType::Record(vec![
("a".into(), u8_id),
("b".into(), u32_id),
("c".into(), u16_id),
("d".into(), u64_id),
]));
let iface = InterfaceType::Instance(InstanceInterface {
functions: BTreeMap::from([("get".to_string(), sig(true, &[], vec![], vec![record]))]),
type_exports: BTreeMap::from([("mixed".to_string(), record)]),
});
let bytes = gen_adapter(
"test:pkg/mixed@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_heterogeneous_numeric_variant_async_result() {
let mut arena = TypeArena::default();
let u8_id = arena.intern_val(ValueType::U8);
let u64_id = arena.intern_val(ValueType::U64);
let f64_id = arena.intern_val(ValueType::F64);
let v = arena.intern_val(ValueType::Variant(vec![
("x".into(), Some(u8_id)),
("y".into(), Some(u64_id)),
("z".into(), Some(f64_id)),
]));
let iface = InterfaceType::Instance(InstanceInterface {
functions: BTreeMap::from([("get".to_string(), sig(true, &[], vec![], vec![v]))]),
type_exports: BTreeMap::from([("mixed-v".to_string(), v)]),
});
let bytes = gen_adapter(
"test:pkg/mixed-v@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_too_many_flat_params_fails_cleanly() {
let mut arena = TypeArena::default();
let u32_id = arena.intern_val(ValueType::U32);
let param_names: Vec<String> = (0..17).map(|i| format!("p{i}")).collect();
let iface = make_iface(vec![(
"many",
FuncSignature {
is_async: false,
param_names,
params: vec![u32_id; 17],
results: vec![],
},
)]);
let tmp = tempfile::tempdir().unwrap();
let split = synth_split("test:pkg/many@1.0.0", &iface, &arena, SplitKind::Consumer);
let err = generate_tier1_adapter(
"test-mdl",
"test:pkg/many@1.0.0",
&[],
Some(&iface),
tmp.path().to_str().unwrap(),
split.path().to_str().unwrap(),
&arena,
)
.expect_err("generation should fail when a function's flat arity exceeds MAX_FLAT_PARAMS");
let msg = err.to_string();
assert!(
msg.contains("flat") || msg.contains("16"),
"error should mention the flat-param limit, got: {msg}"
);
}
fn gen_flags_adapter(n: usize) {
let mut arena = TypeArena::default();
let names: Vec<String> = (0..n).map(|i| format!("f{i}")).collect();
let flags = arena.intern_val(ValueType::Flags(names));
let iface = InterfaceType::Instance(InstanceInterface {
functions: BTreeMap::from([("get".to_string(), sig(true, &[], vec![], vec![flags]))]),
type_exports: BTreeMap::from([("fs".to_string(), flags)]),
});
let bytes = gen_adapter(
"test:pkg/fs@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_flags_1_label_async_result() {
gen_flags_adapter(1);
}
#[test]
fn test_adapter_flags_8_labels_async_result() {
gen_flags_adapter(8);
}
#[test]
fn test_adapter_flags_16_labels_async_result() {
gen_flags_adapter(16);
}
#[test]
fn test_adapter_flags_32_labels_async_result() {
gen_flags_adapter(32);
}
#[test]
fn test_adapter_variant_over_256_cases_async_result() {
let mut arena = TypeArena::default();
let cases: Vec<(String, Option<ValueTypeId>)> =
(0..300).map(|i| (format!("c{i:03}"), None)).collect();
let v = arena.intern_val(ValueType::Variant(cases));
let iface = InterfaceType::Instance(InstanceInterface {
functions: BTreeMap::from([("get".to_string(), sig(true, &[], vec![], vec![v]))]),
type_exports: BTreeMap::from([("big-v".to_string(), v)]),
});
let bytes = gen_adapter(
"test:pkg/big-v@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_multi_func() {
let mut arena = TypeArena::default();
let s32 = arena.intern_val(ValueType::S32);
let string = arena.intern_val(ValueType::String);
let iface = make_iface(vec![
("add", sig(false, &["a", "b"], vec![s32, s32], vec![s32])),
("print", sig(true, &["msg"], vec![string], vec![])),
("get-value", sig(false, &[], vec![], vec![s32])),
]);
let bytes = gen_adapter(
"test:pkg/mixed@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_before_only() {
let mut arena = TypeArena::default();
let s32 = arena.intern_val(ValueType::S32);
let iface = make_iface(vec![("get", sig(false, &[], vec![], vec![s32]))]);
let bytes = gen_adapter(
"test:pkg/getter@1.0.0",
&["splicer:tier1/before"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_after_only() {
let mut arena = TypeArena::default();
let s32 = arena.intern_val(ValueType::S32);
let iface = make_iface(vec![("get", sig(true, &[], vec![], vec![s32]))]);
let bytes = gen_adapter(
"test:pkg/getter@1.0.0",
&["splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_blocking() {
let mut arena = TypeArena::default();
let string = arena.intern_val(ValueType::String);
let iface = make_iface(vec![("fire", sig(true, &["msg"], vec![string], vec![]))]);
let bytes = gen_adapter(
"test:pkg/fire@1.0.0",
&[
"splicer:tier1/before",
"splicer:tier1/blocking",
"splicer:tier1/after",
],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_no_hooks() {
let mut arena = TypeArena::default();
let s32 = arena.intern_val(ValueType::S32);
let iface = make_iface(vec![(
"add",
sig(false, &["a", "b"], vec![s32, s32], vec![s32]),
)]);
let bytes = gen_adapter(
"test:pkg/adder@1.0.0",
&[],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_provider_split_primitive() {
let mut arena = TypeArena::default();
let s32 = arena.intern_val(ValueType::S32);
let iface = make_iface(vec![(
"add",
sig(false, &["a", "b"], vec![s32, s32], vec![s32]),
)]);
let bytes = gen_adapter(
"test:pkg/adder@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Provider,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_provider_split_resource_handler() {
let mut arena = TypeArena::default();
let iface = build_http_handler_iface(&mut arena);
let bytes = gen_adapter(
"wasi:http/handler@0.3.0-rc-2026-01-06",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Provider,
);
validate_component(&bytes);
}