use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct ValueTypeId(u32);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct InterfaceTypeId(u32);
pub const SYNTHETIC_COMPONENT: u32 = u32::MAX;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum InternedId {
Value(ValueTypeId),
Interface(InterfaceTypeId),
}
#[derive(Debug, Clone)]
pub struct ComponentNode {
pub name: String,
pub component_index: u32,
pub component_num: u32,
pub imports: Vec<InterfaceConnection>,
}
impl ComponentNode {
pub fn new(name: String, component_index: u32, component_num: u32) -> Self {
Self {
name,
component_index,
component_num,
imports: Vec::new(),
}
}
pub fn add_import(&mut self, connection: InterfaceConnection) {
self.imports.push(connection);
}
pub fn display_label(&self) -> &str {
self.name.trim_start_matches('$')
}
}
#[derive(Debug, Clone)]
pub struct InterfaceConnection {
pub interface_name: String,
pub source_instance: Option<u32>,
pub is_host_import: bool,
pub interface_type: Option<InterfaceType>,
pub fingerprint: Option<String>,
}
impl InterfaceConnection {
pub fn from_instance(
interface_name: String,
source_instance: Option<u32>,
interface_type: Option<InterfaceType>,
arena: &TypeArena,
) -> Self {
let fingerprint = interface_type.as_ref().map(|t| t.fingerprint(arena));
Self {
interface_name,
source_instance,
is_host_import: false,
interface_type,
fingerprint,
}
}
pub fn compatible_with(&self, other: &InterfaceConnection) -> bool {
compatible_fingerprints(&self.fingerprint, &other.fingerprint)
}
pub fn short_label(&self) -> String {
short_interface_name(&self.interface_name)
}
}
pub fn compatible_fingerprints(f0: &Option<String>, f1: &Option<String>) -> bool {
f0 == f1
}
#[derive(Debug, Clone)]
pub enum InterfaceType {
Instance(InstanceInterface),
Func(FuncSignature),
}
impl InterfaceType {
pub fn intern(&self, arena: &mut TypeArena) -> InterfaceTypeId {
arena.intern_interface(self)
}
pub fn fingerprint(&self, arena: &TypeArena) -> String {
let s = canonical_interface(self, arena);
let hash = Sha256::digest(s.as_bytes());
hex::encode(hash)
}
}
#[derive(Debug, Clone)]
pub struct InstanceInterface {
pub functions: BTreeMap<String, FuncSignature>,
pub type_exports: BTreeMap<String, ValueTypeId>,
}
#[derive(Debug, Clone)]
pub struct FuncSignature {
pub is_async: bool,
pub param_names: Vec<String>,
pub params: Vec<ValueTypeId>,
pub results: Vec<ValueTypeId>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ValueType {
Bool,
S8,
U8,
S16,
U16,
S32,
U32,
S64,
U64,
F32,
F64,
Char,
String,
ErrorContext,
Resource(String),
AsyncHandle,
List(ValueTypeId),
FixedSizeList(ValueTypeId, u32),
Tuple(Vec<ValueTypeId>),
Record(Vec<(String, ValueTypeId)>),
Variant(Vec<(String, Option<ValueTypeId>)>),
Enum(Vec<String>),
Option(ValueTypeId),
Result {
ok: Option<ValueTypeId>,
err: Option<ValueTypeId>,
},
Flags(Vec<String>),
Map(ValueTypeId, ValueTypeId),
}
fn canonical_interface(iface: &InterfaceType, arena: &TypeArena) -> String {
match iface {
InterfaceType::Func(f) => canonical_func(f, arena),
InterfaceType::Instance(inst) => {
let mut funcs: Vec<_> = inst.functions.iter().collect();
funcs.sort_by(|a, b| a.0.cmp(b.0));
let mut out = String::from("instance{");
for (name, func) in funcs {
out.push_str(name);
out.push(':');
out.push_str(&canonical_func(func, arena));
out.push(';');
}
out.push('}');
out
}
}
}
fn canonical_func(f: &FuncSignature, arena: &TypeArena) -> String {
let mut out = String::from("func(");
for (i, p) in f.params.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&arena.canonical_val(*p));
}
out.push_str(")->(");
for (i, r) in f.results.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&arena.canonical_val(*r));
}
out.push(')');
out
}
#[derive(Default)]
pub struct CompositionGraph {
pub nodes: BTreeMap<u32, ComponentNode>,
pub component_exports: BTreeMap<String, ExportInfo>,
pub arena: TypeArena,
}
impl CompositionGraph {
pub fn new() -> Self {
Self::default()
}
pub fn new_with(
nodes: BTreeMap<u32, ComponentNode>,
component_exports: BTreeMap<String, ExportInfo>,
arena: TypeArena,
) -> Self {
Self {
nodes,
component_exports,
arena,
}
}
pub fn add_node(&mut self, instance_index: u32, node: ComponentNode) {
self.nodes.insert(instance_index, node);
}
pub fn get_node(&self, id: u32) -> Option<&ComponentNode> {
self.nodes.get(&id)
}
pub fn add_export(
&mut self,
interface_name: String,
source_instance: u32,
interface_type: Option<InterfaceType>,
) {
let (ty, fingerprint) = match interface_type {
Some(t) => {
let id = t.intern(&mut self.arena);
let fp = t.fingerprint(&self.arena);
(Some(InternedId::Interface(id)), Some(fp))
}
None => (None, None),
};
self.component_exports.insert(
interface_name,
ExportInfo {
source_instance,
ty,
fingerprint,
},
);
}
pub fn real_nodes(&self) -> Vec<&ComponentNode> {
self.nodes
.values()
.filter(|n| n.component_index != SYNTHETIC_COMPONENT)
.collect()
}
pub fn host_interfaces(&self) -> Vec<String> {
let mut interfaces: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for node in self.real_nodes() {
for import in &node.imports {
if import.is_host_import {
interfaces.insert(import.interface_name.clone());
}
}
}
interfaces.into_iter().collect()
}
pub fn validate(&self) -> Result<(), String> {
for (
iface,
ExportInfo {
source_instance: src,
..
},
) in &self.component_exports
{
if !self.nodes.contains_key(src) {
return Err(format!(
"Export '{}' references unknown instance {}",
iface, src
));
}
}
for (id, node) in &self.nodes {
for conn in &node.imports {
if conn.is_host_import {
continue;
}
if let Some(inst) = conn.source_instance {
if !self.nodes.contains_key(&inst) {
return Err(format!(
"Instance {} imports from unknown instance {}",
id, inst
));
}
} else {
return Err(format!("Instance {} imports from unknown instance", id));
};
}
}
Ok(())
}
}
pub struct ExportInfo {
pub source_instance: u32,
pub fingerprint: Option<String>,
pub ty: Option<InternedId>,
}
use std::collections::HashMap;
#[derive(Debug, Default)]
pub struct TypeArena {
vals: Vec<ValueType>,
val_intern: HashMap<ValueType, ValueTypeId>,
interfaces: Vec<InterfaceType>,
interface_intern: HashMap<String, InterfaceTypeId>,
}
impl TypeArena {
pub fn intern_val(&mut self, ty: ValueType) -> ValueTypeId {
if let Some(id) = self.val_intern.get(&ty) {
return *id;
}
let id = ValueTypeId(self.vals.len() as u32);
self.vals.push(ty.clone());
self.val_intern.insert(ty, id);
id
}
pub fn lookup_val(&self, id: ValueTypeId) -> &ValueType {
&self.vals[id.0 as usize]
}
pub fn iter_val_ids(&self) -> impl Iterator<Item = ValueTypeId> + '_ {
(0..self.vals.len() as u32).map(ValueTypeId)
}
pub fn intern_interface(&mut self, interface: &InterfaceType) -> InterfaceTypeId {
let canonical_str = canonical_interface(interface, self);
if let Some(id) = self.interface_intern.get(&canonical_str) {
return *id;
}
let id = InterfaceTypeId(self.interfaces.len() as u32);
self.interfaces.push(interface.clone());
self.interface_intern.insert(canonical_str, id);
id
}
pub fn lookup_interface(&self, id: InterfaceTypeId) -> &InterfaceType {
&self.interfaces[id.0 as usize]
}
}
impl TypeArena {
pub fn canonical_val(&self, id: ValueTypeId) -> String {
match self.lookup_val(id) {
ValueType::Map(k, v) => {
format!("map<{},{}>", self.canonical_val(*k), self.canonical_val(*v))
}
ValueType::FixedSizeList(t, n) => format!("array{}<{}>", n, self.canonical_val(*t)),
ValueType::List(t) => format!("list<{}>", self.canonical_val(*t)),
ValueType::Option(t) => format!("option<{}>", self.canonical_val(*t)),
ValueType::Tuple(ts) => format!(
"tuple({})",
ts.iter()
.map(|t| self.canonical_val(*t))
.collect::<Vec<_>>()
.join(",")
),
ValueType::Record(fields) => format!(
"record{{{}}}",
fields
.iter()
.map(|(n, t)| format!("{}:{}", n, self.canonical_val(*t)))
.collect::<Vec<_>>()
.join(",")
),
ValueType::Variant(cases) => format!(
"variant{{{}}}",
cases
.iter()
.map(|(n, t)| match t {
Some(t) => format!("{}:{}", n, self.canonical_val(*t)),
None => n.clone(),
})
.collect::<Vec<_>>()
.join(",")
),
ValueType::Enum(names) => format!("enum{{{}}}", names.join(",")),
ValueType::Flags(names) => format!("flags{{{}}}", names.join(",")),
ValueType::Result { ok, err } => format!(
"result<{},{}>",
ok.map(|t| self.canonical_val(t)).unwrap_or("_".into()),
err.map(|t| self.canonical_val(t)).unwrap_or("_".into())
),
ValueType::Resource(_) => "resource".into(),
ValueType::AsyncHandle => "async_handle".into(),
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(),
ValueType::ErrorContext => "error-context".into(),
}
}
pub fn canonical_interface(&self, id: InterfaceTypeId) -> String {
canonical_interface(self.lookup_interface(id), self)
}
pub fn display_val(&self, id: ValueTypeId) -> String {
self.display_val_inner(id, 0)
}
fn display_val_inner(&self, id: ValueTypeId, depth: usize) -> String {
const MAX_DEPTH: usize = 3;
const MAX_ITEMS: usize = 4;
if depth > MAX_DEPTH {
return "…".to_string();
}
let next = depth + 1;
match self.lookup_val(id) {
ValueType::Map(k, v) => format!(
"map<{},{}>",
self.display_val_inner(*k, next),
self.display_val_inner(*v, next)
),
ValueType::FixedSizeList(t, n) => {
format!("array{}<{}>", n, self.display_val_inner(*t, next))
}
ValueType::List(t) => format!("list<{}>", self.display_val_inner(*t, next)),
ValueType::Option(t) => format!("option<{}>", self.display_val_inner(*t, next)),
ValueType::Tuple(ts) => {
if ts.len() > MAX_ITEMS {
format!("tuple({} items)", ts.len())
} else {
format!(
"tuple({})",
ts.iter()
.map(|t| self.display_val_inner(*t, next))
.collect::<Vec<_>>()
.join(",")
)
}
}
ValueType::Record(fields) => {
if fields.len() > MAX_ITEMS {
format!("record{{{} fields}}", fields.len())
} else {
format!(
"record{{{}}}",
fields
.iter()
.map(|(n, t)| format!("{}:{}", n, self.display_val_inner(*t, next)))
.collect::<Vec<_>>()
.join(",")
)
}
}
ValueType::Variant(cases) => {
if cases.len() > MAX_ITEMS {
format!("variant{{{} cases}}", cases.len())
} else {
format!(
"variant{{{}}}",
cases
.iter()
.map(|(n, t)| match t {
Some(t) => {
format!("{}:{}", n, self.display_val_inner(*t, next))
}
None => n.clone(),
})
.collect::<Vec<_>>()
.join(",")
)
}
}
ValueType::Enum(names) => {
if names.len() > MAX_ITEMS {
format!("enum{{{} cases}}", names.len())
} else {
format!("enum{{{}}}", names.join(","))
}
}
ValueType::Flags(names) => {
if names.len() > MAX_ITEMS {
format!("flags{{{} flags}}", names.len())
} else {
format!("flags{{{}}}", names.join(","))
}
}
ValueType::Result { ok, err } => format!(
"result<{},{}>",
ok.map(|t| self.display_val_inner(t, next))
.unwrap_or_else(|| "_".into()),
err.map(|t| self.display_val_inner(t, next))
.unwrap_or_else(|| "_".into())
),
ValueType::Resource(name) if name.is_empty() => "resource".into(),
ValueType::Resource(name) => format!("resource[{}]", name),
ValueType::AsyncHandle => "async_handle".into(),
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(),
ValueType::ErrorContext => "error-context".into(),
}
}
}
pub fn short_interface_name(full_name: &str) -> String {
if let Some(slash_pos) = full_name.rfind('/') {
let after_slash = &full_name[slash_pos + 1..];
if let Some(at_pos) = after_slash.find('@') {
return after_slash[..at_pos].to_string();
}
return after_slash.to_string();
}
full_name.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_interface_short_label() {
let conn = InterfaceConnection::from_instance(
"wasi:http/handler@0.3.0-rc-2026-01-06".to_string(),
Some(0),
None,
&TypeArena::default(),
);
assert_eq!(conn.short_label(), "handler");
let conn2 = InterfaceConnection::from_instance(
"wasi:io/streams@0.2.0".to_string(),
Some(1),
None,
&TypeArena::default(),
);
assert_eq!(conn2.short_label(), "streams");
}
#[test]
fn test_short_interface_name() {
assert_eq!(short_interface_name("wasi:http/handler@0.3.0"), "handler");
assert_eq!(short_interface_name("wasi:io/streams@0.2.0"), "streams");
assert_eq!(short_interface_name("simple"), "simple");
}
#[test]
fn test_display_val_simple_types_unchanged() {
let mut arena = TypeArena::default();
let u32_id = arena.intern_val(ValueType::U32);
let str_id = arena.intern_val(ValueType::String);
assert_eq!(arena.display_val(u32_id), "u32");
assert_eq!(arena.display_val(str_id), "string");
}
#[test]
fn test_display_val_small_variant_expanded() {
let mut arena = TypeArena::default();
let v = arena.intern_val(ValueType::Variant(vec![
("a".into(), None),
("b".into(), None),
("c".into(), None),
]));
let s = arena.display_val(v);
assert!(s.starts_with("variant{"), "got: {s}");
assert!(s.contains("a,b,c"), "got: {s}");
}
#[test]
fn test_display_val_large_variant_summarized() {
let mut arena = TypeArena::default();
let v = arena.intern_val(ValueType::Variant(
(0..5).map(|i| (format!("case-{i}"), None)).collect(),
));
let s = arena.display_val(v);
assert_eq!(s, "variant{5 cases}", "got: {s}");
}
#[test]
fn test_display_val_large_record_summarized() {
let mut arena = TypeArena::default();
let u32_id = arena.intern_val(ValueType::U32);
let v = arena.intern_val(ValueType::Record(
(0..6).map(|i| (format!("field-{i}"), u32_id)).collect(),
));
let s = arena.display_val(v);
assert_eq!(s, "record{6 fields}", "got: {s}");
}
#[test]
fn test_display_val_result_with_summarized_err() {
let mut arena = TypeArena::default();
let res_id = arena.intern_val(ValueType::Resource(String::new()));
let err_id = arena.intern_val(ValueType::Variant(
(0..10).map(|i| (format!("e{i}"), None)).collect(),
));
let result_id = arena.intern_val(ValueType::Result {
ok: Some(res_id),
err: Some(err_id),
});
let s = arena.display_val(result_id);
assert_eq!(s, "result<resource,variant{10 cases}>", "got: {s}");
}
#[test]
fn test_display_val_does_not_affect_canonical_val() {
let mut arena = TypeArena::default();
let v = arena.intern_val(ValueType::Variant(
(0..5).map(|i| (format!("case-{i}"), None)).collect(),
));
let canonical = arena.canonical_val(v);
let display = arena.display_val(v);
assert_ne!(
canonical, display,
"canonical should be full, display should be summarized"
);
assert!(
canonical.contains("case-0"),
"canonical should expand all cases"
);
assert_eq!(display, "variant{5 cases}");
}
}