use crate::adapter::generate_tier1_adapter;
use cviz::model::{
FuncSignature, InstanceInterface, InterfaceType, TypeArena, ValueType, ValueTypeId,
};
use cviz::parse::component::parse_component;
use std::collections::{BTreeMap, HashMap, HashSet};
mod fuzz;
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(),
);
}
}
#[derive(Clone, Copy)]
pub(in crate::adapter) enum SplitKind {
Consumer,
Provider,
ConsumerSiblingTypes,
}
pub(in crate::adapter) 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),
(SplitKind::ConsumerSiblingTypes, _) => wat_consumer_cross_interface_value_types(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 order: Vec<ValueTypeId> = Vec::new();
let mut visited: HashSet<ValueTypeId> = HashSet::new();
for sig in iface.functions.values() {
for &pid in &sig.params {
collect_compound_order(pid, arena, &mut visited, &mut order);
}
for &rid in &sig.results {
collect_compound_order(rid, arena, &mut visited, &mut order);
}
}
for &vid in iface.type_exports.values() {
collect_compound_order(vid, arena, &mut visited, &mut order);
}
let mut exports_by_id: HashMap<ValueTypeId, Vec<String>> = HashMap::new();
for (export_name, &vid) in &iface.type_exports {
if matches!(
arena.lookup_val(vid),
ValueType::Resource(_) | ValueType::AsyncHandle
) {
continue;
}
exports_by_id
.entry(vid)
.or_default()
.push(export_name.clone());
}
let mut effective: HashMap<ValueTypeId, u32> = HashMap::new();
let mut next_slot: u32 = 0;
let mut body = String::new();
for &id in &order {
let decl_body = wat_compound_decl_body(id, arena, &effective);
body.push_str(&format!(" (type (;{next_slot};) {decl_body})\n"));
let decl_slot = next_slot;
next_slot += 1;
effective.insert(id, decl_slot);
if let Some(names) = exports_by_id.get(&id) {
for (i, export_name) in names.iter().enumerate() {
body.push_str(&format!(
" (export (;{next_slot};) \"{export_name}\" (type (eq {decl_slot})))\n"
));
if i == 0 {
effective.insert(id, next_slot);
}
next_slot += 1;
}
}
}
let mut export_lines = String::new();
for (fname, sig) in &iface.functions {
let params: Vec<String> = sig
.param_names
.iter()
.zip(sig.params.iter())
.map(|(pn, &pid)| format!(r#"(param "{pn}" {})"#, wat_ref(pid, arena, &effective)))
.collect();
let result = match sig.results.first() {
Some(&rid) => format!(" (result {})", wat_ref(rid, arena, &effective)),
None => String::new(),
};
let async_kw = if sig.is_async { "async " } else { "" };
let func_slot = next_slot;
body.push_str(&format!(
" (type (;{func_slot};) (func {async_kw}{}{result}))\n",
params.join(" "),
));
next_slot += 1;
export_lines.push_str(&format!(
" (export \"{fname}\" (func (type {func_slot})))\n"
));
}
body.push_str(&export_lines);
format!(
"(component\n (type $iface (instance\n{body} ))\n (import \"{target}\" (instance (type $iface)))\n)\n"
)
}
fn collect_compound_order(
id: ValueTypeId,
arena: &TypeArena,
visited: &mut HashSet<ValueTypeId>,
order: &mut Vec<ValueTypeId>,
) {
if !visited.insert(id) {
return;
}
match arena.lookup_val(id) {
ValueType::Bool
| ValueType::S8
| ValueType::U8
| ValueType::S16
| ValueType::U16
| ValueType::S32
| ValueType::U32
| ValueType::S64
| ValueType::U64
| ValueType::F32
| ValueType::F64
| ValueType::Char
| ValueType::String
| ValueType::ErrorContext
| ValueType::Resource(_)
| ValueType::AsyncHandle
| ValueType::Map(_, _) => return,
ValueType::List(inner) | ValueType::Option(inner) | ValueType::FixedSizeList(inner, _) => {
collect_compound_order(*inner, arena, visited, order);
}
ValueType::Result { ok, err } => {
if let Some(o) = ok {
collect_compound_order(*o, arena, visited, order);
}
if let Some(e) = err {
collect_compound_order(*e, arena, visited, order);
}
}
ValueType::Tuple(ids) => {
for cid in ids.clone() {
collect_compound_order(cid, arena, visited, order);
}
}
ValueType::Record(fields) => {
for (_, fid) in fields.clone() {
collect_compound_order(fid, arena, visited, order);
}
}
ValueType::Variant(cases) => {
for (_, opt) in cases.clone() {
if let Some(cid) = opt {
collect_compound_order(cid, arena, visited, order);
}
}
}
ValueType::Enum(_) | ValueType::Flags(_) => {}
}
order.push(id);
}
fn wat_ref(id: ValueTypeId, arena: &TypeArena, effective: &HashMap<ValueTypeId, u32>) -> String {
match arena.lookup_val(id) {
ValueType::Bool => "bool".into(),
ValueType::S8 => "s8".into(),
ValueType::U8 => "u8".into(),
ValueType::S16 => "s16".into(),
ValueType::U16 => "u16".into(),
ValueType::S32 => "s32".into(),
ValueType::U32 => "u32".into(),
ValueType::S64 => "s64".into(),
ValueType::U64 => "u64".into(),
ValueType::F32 => "f32".into(),
ValueType::F64 => "f64".into(),
ValueType::Char => "char".into(),
ValueType::String => "string".into(),
_ => effective
.get(&id)
.map(|n| n.to_string())
.unwrap_or_else(|| panic!("wat_ref: no effective slot for {id:?}")),
}
}
fn wat_compound_decl_body(
id: ValueTypeId,
arena: &TypeArena,
effective: &HashMap<ValueTypeId, u32>,
) -> String {
match arena.lookup_val(id) {
ValueType::List(inner) => format!("(list {})", wat_ref(*inner, arena, effective)),
ValueType::FixedSizeList(inner, n) => {
format!("(list {} {n})", wat_ref(*inner, arena, effective))
}
ValueType::Option(inner) => format!("(option {})", wat_ref(*inner, arena, effective)),
ValueType::Result { ok, err } => {
let ok_s = ok.map(|id| wat_ref(id, arena, effective));
let err_s = err.map(|id| wat_ref(id, arena, effective));
match (ok_s, err_s) {
(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_ref(*id, arena, effective))
.collect();
format!("(tuple {})", inner.join(" "))
}
ValueType::Record(fields) => {
let inner: Vec<String> = fields
.iter()
.map(|(n, fid)| format!(r#"(field "{n}" {})"#, wat_ref(*fid, arena, effective)))
.collect();
format!("(record {})", inner.join(" "))
}
ValueType::Variant(cases) => {
let inner: Vec<String> = cases
.iter()
.map(|(n, opt)| match opt {
Some(cid) => format!(r#"(case "{n}" {})"#, wat_ref(*cid, arena, effective)),
None => format!(r#"(case "{n}")"#),
})
.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(labels) => {
let items: Vec<String> = labels.iter().map(|n| format!(r#""{n}""#)).collect();
format!("(flags {})", items.join(" "))
}
other => panic!("wat_compound_decl_body: {other:?} is not a declarable compound"),
}
}
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_consumer_cross_interface_value_types(target: &str) -> String {
format!(
r#"(component
(type (;0;) (instance
(type (record (field "sku" string) (field "quantity" u32) (field "unit-price" f64)))
(export "item" (type (eq 0)))
(type (list 1))
(type (enum "BE" "US" "UK" "JP" "CA" "AU"))
(export "country" (type (eq 3)))
(type (record (field "order-id" string) (field "items" 2) (field "destination" 4)))
(export "order" (type (eq 5)))
(type (enum "EUR" "USD" "GBP" "JPY" "CAD" "AUD"))
(export "currency" (type (eq 7)))
(type (record (field "order-id" string) (field "subtotal" f64) (field "tax" f64) (field "total" f64) (field "currency" 8)))
(export "quote" (type (eq 9)))
))
(import "synth:test/types" (instance (;0;) (type 0)))
(alias export 0 "order" (type (;1;)))
(alias export 0 "quote" (type (;2;)))
(type (;3;) (instance
(alias outer 1 1 (type (;0;)))
(export "order" (type (eq 0)))
(alias outer 1 2 (type (;2;)))
(export "quote" (type (eq 2)))
(type (;4;) (func (param "order" 0) (result 2)))
(export "create-order" (func (type 4)))
))
(import "{target}" (instance (;1;) (type 3)))
)
"#
)
}
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,
tmp.path().to_str().unwrap(),
split_path,
)
.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,
}
}
const DEFAULT_HOOKS: &[&str] = &["splicer:tier1/before", "splicer:tier1/after"];
fn gen_and_validate(
target: &str,
iface: &InterfaceType,
arena: &TypeArena,
kind: SplitKind,
) -> Vec<u8> {
gen_and_validate_with(target, DEFAULT_HOOKS, iface, arena, kind)
}
fn gen_and_validate_with(
target: &str,
hooks: &[&str],
iface: &InterfaceType,
arena: &TypeArena,
kind: SplitKind,
) -> Vec<u8> {
let bytes = gen_adapter(target, hooks, iface, arena, kind);
validate_component(&bytes);
bytes
}
fn named_async_result_iface(export_name: &str, ty: ValueTypeId) -> InterfaceType {
InterfaceType::Instance(InstanceInterface {
functions: BTreeMap::from([("get".to_string(), sig(true, &[], vec![], vec![ty]))]),
type_exports: BTreeMap::from([(export_name.to_string(), ty)]),
})
}
#[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]),
)]);
gen_and_validate("test:pkg/adder@1.0.0", &iface, &arena, SplitKind::Consumer);
}
#[test]
fn test_adapter_sync_multi_func_primitives() {
let mut arena = TypeArena::default();
let s32 = arena.intern_val(ValueType::S32);
let u32_ = arena.intern_val(ValueType::U32);
let iface = make_iface(vec![
("add", sig(false, &["a", "b"], vec![s32, s32], vec![s32])),
("count", sig(false, &[], vec![], vec![u32_])),
("noop", sig(false, &["x"], vec![s32], vec![])),
]);
gen_and_validate("test:pkg/multi@1.0.0", &iface, &arena, SplitKind::Consumer);
}
#[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]))]);
gen_and_validate(
"test:pkg/messenger@1.0.0",
&iface,
&arena,
SplitKind::Consumer,
);
}
#[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]),
)]);
gen_and_validate("test:pkg/echo@1.0.0", &iface, &arena, SplitKind::Consumer);
}
#[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]))]);
gen_and_validate(
"test:pkg/messenger@1.0.0",
&iface,
&arena,
SplitKind::Consumer,
);
}
#[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![]))]);
gen_and_validate(
"test:pkg/printer@1.0.0",
&iface,
&arena,
SplitKind::Consumer,
);
}
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);
gen_and_validate(
"wasi:http/handler@0.3.0-rc-2026-01-06",
&iface,
&arena,
SplitKind::Consumer,
);
}
#[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]),
)]);
gen_and_validate("test:pkg/summer@1.0.0", &iface, &arena, SplitKind::Consumer);
}
#[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]),
)]);
gen_and_validate("test:pkg/ranger@1.0.0", &iface, &arena, SplitKind::Consumer);
}
#[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![]))]);
gen_and_validate("test:pkg/taker@1.0.0", &iface, &arena, SplitKind::Consumer);
}
#[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![]),
)]);
gen_and_validate(
"test:pkg/processor@1.0.0",
&iface,
&arena,
SplitKind::Consumer,
);
}
#[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]))]);
gen_and_validate("test:pkg/get@1.0.0", &iface, &arena, SplitKind::Consumer);
}
#[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]))]);
gen_and_validate("test:pkg/get@1.0.0", &iface, &arena, SplitKind::Consumer);
}
#[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]))]);
gen_and_validate("test:pkg/get@1.0.0", &iface, &arena, SplitKind::Consumer);
}
#[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 = named_async_result_iface("my-record", record);
gen_and_validate("test:pkg/get@1.0.0", &iface, &arena, SplitKind::Consumer);
}
#[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 = named_async_result_iface("color", en);
gen_and_validate("test:pkg/get@1.0.0", &iface, &arena, SplitKind::Consumer);
}
#[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 = named_async_result_iface("mixed", record);
gen_and_validate("test:pkg/mixed@1.0.0", &iface, &arena, SplitKind::Consumer);
}
#[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 = named_async_result_iface("mixed-v", v);
gen_and_validate(
"test:pkg/mixed-v@1.0.0",
&iface,
&arena,
SplitKind::Consumer,
);
}
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 = named_async_result_iface("fs", flags);
gen_and_validate("test:pkg/fs@1.0.0", &iface, &arena, SplitKind::Consumer);
}
#[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 = named_async_result_iface("big-v", v);
gen_and_validate("test:pkg/big-v@1.0.0", &iface, &arena, SplitKind::Consumer);
}
#[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])),
]);
gen_and_validate("test:pkg/mixed@1.0.0", &iface, &arena, SplitKind::Consumer);
}
fn build_nebula_orders_iface(arena: &mut TypeArena) -> InterfaceType {
let string_id = arena.intern_val(ValueType::String);
let u32_id = arena.intern_val(ValueType::U32);
let f64_id = arena.intern_val(ValueType::F64);
let item = arena.intern_val(ValueType::Record(vec![
("sku".into(), string_id),
("quantity".into(), u32_id),
("unit-price".into(), f64_id),
]));
let country = arena.intern_val(ValueType::Enum(vec![
"BE".into(),
"US".into(),
"UK".into(),
"JP".into(),
"CA".into(),
"AU".into(),
]));
let currency = arena.intern_val(ValueType::Enum(vec![
"EUR".into(),
"USD".into(),
"GBP".into(),
"JPY".into(),
"CAD".into(),
"AUD".into(),
]));
let list_item = arena.intern_val(ValueType::List(item));
let order = arena.intern_val(ValueType::Record(vec![
("order-id".into(), string_id),
("items".into(), list_item),
("destination".into(), country),
]));
let quote = arena.intern_val(ValueType::Record(vec![
("order-id".into(), string_id),
("subtotal".into(), f64_id),
("tax".into(), f64_id),
("total".into(), f64_id),
("currency".into(), currency),
]));
let opt_order = arena.intern_val(ValueType::Option(order));
let create_order = sig(false, &["order"], vec![order], vec![quote]);
let read_order = sig(false, &["order-id"], vec![string_id], vec![opt_order]);
let delete_order = sig(false, &["order-id"], vec![string_id], vec![]);
InterfaceType::Instance(InstanceInterface {
functions: BTreeMap::from([
("create-order".to_string(), create_order),
("read-order".to_string(), read_order),
("delete-order".to_string(), delete_order),
]),
type_exports: BTreeMap::from([
("item".to_string(), item),
("country".to_string(), country),
("currency".to_string(), currency),
("order".to_string(), order),
("quote".to_string(), quote),
]),
})
}
#[test]
fn test_adapter_nebula_orders_shape() {
let mut arena = TypeArena::default();
let iface = build_nebula_orders_iface(&mut arena);
let bytes = gen_and_validate("nebula:service/orders", &iface, &arena, SplitKind::Consumer);
for name in ["create-order", "read-order", "delete-order"] {
let needle = name.as_bytes();
let found = bytes.windows(needle.len()).any(|w| w == needle);
assert!(
found,
"generated adapter should reference `{name}` but the binary \
doesn't contain it — splicer may have dropped interface \
functions other than the first one"
);
}
}
#[test]
fn test_adapter_cross_interface_value_types() {
let wat = r#"(component
(component $inner
(import "nebula:core/types" (instance $types
(type (record (field "sku" string) (field "quantity" u32) (field "unit-price" f64)))
(export "item" (type (eq 0)))
(type (list 1))
(type (enum "BE" "US" "UK" "JP" "CA" "AU"))
(export "country" (type (eq 3)))
(type (record (field "order-id" string) (field "items" 2) (field "destination" 4)))
(export "order" (type (eq 5)))
(type (enum "EUR" "USD" "GBP" "JPY" "CAD" "AUD"))
(export "currency" (type (eq 7)))
(type (record (field "order-id" string) (field "subtotal" f64) (field "tax" f64) (field "total" f64) (field "currency" 8)))
(export "quote" (type (eq 9)))
))
(alias export $types "order" (type $order))
(alias export $types "quote" (type $quote))
(import "nebula:service/orders" (instance $svc
(alias outer 1 $order (type (;0;)))
(export "order" (type (eq 0)))
(alias outer 1 $quote (type (;2;)))
(export "quote" (type (eq 2)))
(type (;4;) (func (param "order" 0) (result 2)))
(export "create-order" (func (type 4)))
))
(alias export $svc "create-order" (func $f))
(instance $out (export "create-order" (func $f)))
(export "nebula:service/orders" (instance $out))
)
(import "nebula:core/types" (instance $host-types
(type (record (field "sku" string) (field "quantity" u32) (field "unit-price" f64)))
(export "item" (type (eq 0)))
(type (list 1))
(type (enum "BE" "US" "UK" "JP" "CA" "AU"))
(export "country" (type (eq 3)))
(type (record (field "order-id" string) (field "items" 2) (field "destination" 4)))
(export "order" (type (eq 5)))
(type (enum "EUR" "USD" "GBP" "JPY" "CAD" "AUD"))
(export "currency" (type (eq 7)))
(type (record (field "order-id" string) (field "subtotal" f64) (field "tax" f64) (field "total" f64) (field "currency" 8)))
(export "quote" (type (eq 9)))
))
(alias export $host-types "order" (type $outer-order))
(alias export $host-types "quote" (type $outer-quote))
(import "nebula:service/orders" (instance $host-svc
(alias outer 1 $outer-order (type (;0;)))
(export "order" (type (eq 0)))
(alias outer 1 $outer-quote (type (;2;)))
(export "quote" (type (eq 2)))
(type (;4;) (func (param "order" 0) (result 2)))
(export "create-order" (func (type 4)))
))
(instance $inst (instantiate $inner
(with "nebula:core/types" (instance $host-types))
(with "nebula:service/orders" (instance $host-svc))
))
(alias export $inst "nebula:service/orders" (instance $out))
(export "nebula:service/orders" (instance $out))
)"#;
let comp_bytes = wat::parse_str(wat).expect("composition WAT parses");
let graph = parse_component(&comp_bytes).expect("parse_component succeeds");
let svc_conn = graph
.nodes
.values()
.flat_map(|n| n.imports.iter())
.find(|c| c.interface_name == "nebula:service/orders")
.expect("inner component should import nebula:service/orders");
let iface = svc_conn
.interface_type
.as_ref()
.expect("interface_type populated")
.clone();
if let InterfaceType::Instance(inst) = &iface {
assert!(
inst.type_exports.contains_key("order") && inst.type_exports.contains_key("quote"),
"cviz should populate order+quote in type_exports; got {:?}",
inst.type_exports.keys().collect::<Vec<_>>()
);
}
gen_and_validate(
"nebula:service/orders",
&iface,
&graph.arena,
SplitKind::ConsumerSiblingTypes,
);
}
#[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]))]);
gen_and_validate_with(
"test:pkg/getter@1.0.0",
&["splicer:tier1/before"],
&iface,
&arena,
SplitKind::Consumer,
);
}
#[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]))]);
gen_and_validate_with(
"test:pkg/getter@1.0.0",
&["splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
}
#[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![]))]);
gen_and_validate_with(
"test:pkg/fire@1.0.0",
&[
"splicer:tier1/before",
"splicer:tier1/blocking",
"splicer:tier1/after",
],
&iface,
&arena,
SplitKind::Consumer,
);
}
#[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]),
)]);
gen_and_validate_with(
"test:pkg/adder@1.0.0",
&[],
&iface,
&arena,
SplitKind::Consumer,
);
}
#[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]),
)]);
gen_and_validate("test:pkg/adder@1.0.0", &iface, &arena, SplitKind::Provider);
}