use std::collections::HashMap;
use cviz::model::{TypeArena, ValueType, ValueTypeId};
use wasm_encoder::ValType;
use wit_parser::{
abi::{FlatTypes, WasmType},
Case, Docs, Enum, EnumCase, Field, Flag, Flags, Handle, Record, Resolve, Result_, SizeAlign,
Span, Stability, Tuple, Type, TypeDef, TypeDefKind, TypeId, TypeOwner, Variant,
};
pub(crate) struct WitBridge {
pub resolve: Resolve,
pub sizes: SizeAlign,
type_map: HashMap<ValueTypeId, Type>,
resource_by_name: HashMap<String, TypeId>,
}
impl WitBridge {
pub fn from_cviz(arena: &TypeArena) -> Self {
let mut bridge = Self {
resolve: Resolve::default(),
sizes: SizeAlign::default(),
type_map: HashMap::new(),
resource_by_name: HashMap::new(),
};
for id in arena.iter_val_ids() {
bridge.translate(id, arena);
}
bridge.sizes.fill(&bridge.resolve);
bridge
}
pub fn get(&self, id: ValueTypeId) -> Type {
self.type_map[&id]
}
pub fn size_bytes(&self, id: ValueTypeId) -> u32 {
self.sizes.size(&self.get(id)).size_wasm32() as u32
}
pub fn align_bytes(&self, id: ValueTypeId) -> u32 {
self.sizes.align(&self.get(id)).align_wasm32() as u32
}
pub fn flat_types(&self, id: ValueTypeId) -> Vec<ValType> {
let ty = self.get(id);
let mut buf = [WasmType::I32; 32];
let mut flat = FlatTypes::new(&mut buf);
self.resolve.push_flat(&ty, &mut flat);
flat.to_vec().into_iter().map(wasm_to_val).collect()
}
pub fn has_strings(&self, id: ValueTypeId) -> bool {
self.any_type(self.get(id), &|ty, _| matches!(ty, Type::String))
}
pub fn has_lists(&self, id: ValueTypeId) -> bool {
self.any_type(self.get(id), &|_, kind| {
matches!(
kind,
Some(
TypeDefKind::List(_) | TypeDefKind::FixedLengthList(..) | TypeDefKind::Map(..)
)
)
})
}
fn any_type(&self, ty: Type, pred: &impl Fn(&Type, Option<&TypeDefKind>) -> bool) -> bool {
let kind = match ty {
Type::Id(id) => Some(&self.resolve.types[id].kind),
_ => None,
};
if pred(&ty, kind) {
return true;
}
let Some(kind) = kind else {
return false;
};
match kind {
TypeDefKind::Record(r) => r.fields.iter().any(|f| self.any_type(f.ty, pred)),
TypeDefKind::Tuple(t) => t.types.iter().any(|t| self.any_type(*t, pred)),
TypeDefKind::Variant(v) => v
.cases
.iter()
.any(|c| c.ty.is_some_and(|t| self.any_type(t, pred))),
TypeDefKind::Option(t) => self.any_type(*t, pred),
TypeDefKind::Result(r) => {
r.ok.is_some_and(|t| self.any_type(t, pred))
|| r.err.is_some_and(|t| self.any_type(t, pred))
}
TypeDefKind::List(t) | TypeDefKind::FixedLengthList(t, _) => self.any_type(*t, pred),
TypeDefKind::Map(k, v) => self.any_type(*k, pred) || self.any_type(*v, pred),
TypeDefKind::Type(t) => self.any_type(*t, pred),
TypeDefKind::Enum(_)
| TypeDefKind::Flags(_)
| TypeDefKind::Handle(_)
| TypeDefKind::Resource
| TypeDefKind::Future(_)
| TypeDefKind::Stream(_)
| TypeDefKind::Unknown => false,
}
}
fn translate(&mut self, id: ValueTypeId, arena: &TypeArena) -> Type {
if let Some(ty) = self.type_map.get(&id) {
return *ty;
}
let translated = match arena.lookup_val(id) {
ValueType::Bool => Type::Bool,
ValueType::S8 => Type::S8,
ValueType::U8 => Type::U8,
ValueType::S16 => Type::S16,
ValueType::U16 => Type::U16,
ValueType::S32 => Type::S32,
ValueType::U32 => Type::U32,
ValueType::S64 => Type::S64,
ValueType::U64 => Type::U64,
ValueType::F32 => Type::F32,
ValueType::F64 => Type::F64,
ValueType::Char => Type::Char,
ValueType::String => Type::String,
ValueType::ErrorContext => Type::ErrorContext,
ValueType::Resource(name) => {
let resource_id = self.resource_for_name(name);
self.alloc(TypeDefKind::Handle(Handle::Own(resource_id)))
}
ValueType::AsyncHandle => self.alloc(TypeDefKind::Future(None)),
ValueType::List(inner) => {
let inner = self.translate(*inner, arena);
self.alloc(TypeDefKind::List(inner))
}
ValueType::FixedSizeList(inner, n) => {
let inner = self.translate(*inner, arena);
self.alloc(TypeDefKind::FixedLengthList(inner, *n))
}
ValueType::Map(k, v) => {
let k = self.translate(*k, arena);
let v = self.translate(*v, arena);
self.alloc(TypeDefKind::Map(k, v))
}
ValueType::Tuple(ids) => {
let types: Vec<Type> = ids
.clone()
.iter()
.map(|i| self.translate(*i, arena))
.collect();
self.alloc(TypeDefKind::Tuple(Tuple { types }))
}
ValueType::Record(fields) => {
let fields: Vec<Field> = fields
.clone()
.iter()
.map(|(name, fid)| Field {
name: name.clone(),
ty: self.translate(*fid, arena),
docs: Docs::default(),
span: Span::default(),
})
.collect();
self.alloc(TypeDefKind::Record(Record { fields }))
}
ValueType::Variant(cases) => {
let cases: Vec<Case> = cases
.clone()
.iter()
.map(|(name, payload)| Case {
name: name.clone(),
ty: payload.map(|p| self.translate(p, arena)),
docs: Docs::default(),
span: Span::default(),
})
.collect();
self.alloc(TypeDefKind::Variant(Variant { cases }))
}
ValueType::Enum(names) => {
let cases: Vec<EnumCase> = names
.iter()
.map(|name| EnumCase {
name: name.clone(),
docs: Docs::default(),
span: Span::default(),
})
.collect();
self.alloc(TypeDefKind::Enum(Enum { cases }))
}
ValueType::Option(inner) => {
let inner = self.translate(*inner, arena);
self.alloc(TypeDefKind::Option(inner))
}
ValueType::Result { ok, err } => {
let ok = ok.map(|i| self.translate(i, arena));
let err = err.map(|i| self.translate(i, arena));
self.alloc(TypeDefKind::Result(Result_ { ok, err }))
}
ValueType::Flags(names) => {
let flags: Vec<Flag> = names
.iter()
.map(|name| Flag {
name: name.clone(),
docs: Docs::default(),
span: Span::default(),
})
.collect();
self.alloc(TypeDefKind::Flags(Flags { flags }))
}
};
self.type_map.insert(id, translated);
translated
}
fn alloc(&mut self, kind: TypeDefKind) -> Type {
let synth = format!("t{}", self.resolve.types.len());
let id = self.resolve.types.alloc(TypeDef {
name: Some(synth),
kind,
owner: TypeOwner::None,
docs: Docs::default(),
stability: Stability::default(),
span: Span::default(),
});
Type::Id(id)
}
fn resource_for_name(&mut self, name: &str) -> TypeId {
assert!(
!name.is_empty(),
"resource_for_name: got empty name — upstream (cviz / wirm) \
should never produce anonymous resources; please file a bug"
);
if let Some(id) = self.resource_by_name.get(name) {
return *id;
}
let id = self.resolve.types.alloc(TypeDef {
name: Some(name.to_string()),
kind: TypeDefKind::Resource,
owner: TypeOwner::None,
docs: Docs::default(),
stability: Stability::default(),
span: Span::default(),
});
self.resource_by_name.insert(name.to_string(), id);
id
}
}
pub(crate) fn wasm_to_val(wt: WasmType) -> ValType {
match wt {
WasmType::I32 | WasmType::Pointer | WasmType::Length => ValType::I32,
WasmType::I64 | WasmType::PointerOrI64 => ValType::I64,
WasmType::F32 => ValType::F32,
WasmType::F64 => ValType::F64,
}
}
#[cfg(test)]
mod tests {
use super::*;
use cviz::model::TypeArena;
use wit_parser::Alignment;
fn bytes(a: Alignment) -> usize {
a.align_wasm32()
}
#[test]
fn primitives_size_and_align_match() {
let mut arena = TypeArena::default();
let cases = [
(arena.intern_val(ValueType::Bool), 1, 1),
(arena.intern_val(ValueType::U8), 1, 1),
(arena.intern_val(ValueType::S16), 2, 2),
(arena.intern_val(ValueType::U32), 4, 4),
(arena.intern_val(ValueType::S64), 8, 8),
(arena.intern_val(ValueType::F32), 4, 4),
(arena.intern_val(ValueType::F64), 8, 8),
(arena.intern_val(ValueType::Char), 4, 4),
(arena.intern_val(ValueType::String), 8, 4),
];
let bridge = WitBridge::from_cviz(&arena);
for (id, expected_size, expected_align) in cases {
let ty = bridge.get(id);
assert_eq!(bridge.sizes.size(&ty).size_wasm32(), expected_size);
assert_eq!(bytes(bridge.sizes.align(&ty)), expected_align);
}
}
#[test]
fn record_with_string_and_u32_has_canonical_layout() {
let mut arena = TypeArena::default();
let string_id = arena.intern_val(ValueType::String);
let u32_id = arena.intern_val(ValueType::U32);
let record_id = arena.intern_val(ValueType::Record(vec![
("name".to_string(), string_id),
("age".to_string(), u32_id),
]));
let bridge = WitBridge::from_cviz(&arena);
let ty = bridge.get(record_id);
assert_eq!(bridge.sizes.size(&ty).size_wasm32(), 12);
assert_eq!(bytes(bridge.sizes.align(&ty)), 4);
}
#[test]
fn variant_heterogeneous_arms_use_max_payload_layout() {
let mut arena = TypeArena::default();
let u8_id = arena.intern_val(ValueType::U8);
let u64_id = arena.intern_val(ValueType::U64);
let variant_id = arena.intern_val(ValueType::Variant(vec![
("small".to_string(), Some(u8_id)),
("big".to_string(), Some(u64_id)),
]));
let bridge = WitBridge::from_cviz(&arena);
let ty = bridge.get(variant_id);
assert_eq!(bridge.sizes.size(&ty).size_wasm32(), 16);
assert_eq!(bytes(bridge.sizes.align(&ty)), 8);
}
#[test]
fn fixed_size_list_is_n_copies() {
let mut arena = TypeArena::default();
let u32_id = arena.intern_val(ValueType::U32);
let list_id = arena.intern_val(ValueType::FixedSizeList(u32_id, 4));
let bridge = WitBridge::from_cviz(&arena);
let ty = bridge.get(list_id);
assert_eq!(bridge.sizes.size(&ty).size_wasm32(), 16);
assert_eq!(bytes(bridge.sizes.align(&ty)), 4);
}
#[test]
fn resource_is_four_bytes() {
let mut arena = TypeArena::default();
let request_id = arena.intern_val(ValueType::Resource("request".to_string()));
let response_id = arena.intern_val(ValueType::Resource("response".to_string()));
let bridge = WitBridge::from_cviz(&arena);
assert_eq!(bridge.sizes.size(&bridge.get(request_id)).size_wasm32(), 4);
assert_eq!(bridge.sizes.size(&bridge.get(response_id)).size_wasm32(), 4);
}
#[test]
fn async_handle_is_four_bytes() {
let mut arena = TypeArena::default();
let id = arena.intern_val(ValueType::AsyncHandle);
let bridge = WitBridge::from_cviz(&arena);
assert_eq!(bridge.sizes.size(&bridge.get(id)).size_wasm32(), 4);
}
#[test]
fn flags_packing_follows_canonical_rules() {
let mut arena = TypeArena::default();
let five = arena.intern_val(ValueType::Flags((0..5).map(|i| format!("f{i}")).collect()));
let twenty = arena.intern_val(ValueType::Flags((0..20).map(|i| format!("f{i}")).collect()));
let forty = arena.intern_val(ValueType::Flags((0..40).map(|i| format!("f{i}")).collect()));
let bridge = WitBridge::from_cviz(&arena);
assert_eq!(bridge.sizes.size(&bridge.get(five)).size_wasm32(), 1);
assert_eq!(bridge.sizes.size(&bridge.get(twenty)).size_wasm32(), 4);
assert_eq!(bridge.sizes.size(&bridge.get(forty)).size_wasm32(), 8);
}
#[test]
fn flat_types_for_string_is_two_i32s() {
let mut arena = TypeArena::default();
let id = arena.intern_val(ValueType::String);
let bridge = WitBridge::from_cviz(&arena);
assert_eq!(bridge.flat_types(id), vec![ValType::I32, ValType::I32]);
}
#[test]
fn flat_types_for_heterogeneous_variant_joins_arms() {
let mut arena = TypeArena::default();
let u8_id = arena.intern_val(ValueType::U8);
let u64_id = arena.intern_val(ValueType::U64);
let variant_id = arena.intern_val(ValueType::Variant(vec![
("small".to_string(), Some(u8_id)),
("big".to_string(), Some(u64_id)),
]));
let bridge = WitBridge::from_cviz(&arena);
assert_eq!(
bridge.flat_types(variant_id),
vec![ValType::I32, ValType::I64]
);
}
#[test]
fn has_strings_finds_nested_string() {
let mut arena = TypeArena::default();
let string_id = arena.intern_val(ValueType::String);
let u32_id = arena.intern_val(ValueType::U32);
let nested = arena.intern_val(ValueType::Record(vec![
("s".to_string(), string_id),
("n".to_string(), u32_id),
]));
let deep = arena.intern_val(ValueType::Option(nested));
let bridge = WitBridge::from_cviz(&arena);
assert!(bridge.has_strings(deep));
assert!(!bridge.has_strings(u32_id));
}
#[test]
fn has_lists_finds_list_fixed_list_and_map() {
let mut arena = TypeArena::default();
let u8_id = arena.intern_val(ValueType::U8);
let string_id = arena.intern_val(ValueType::String);
let list_id = arena.intern_val(ValueType::List(u8_id));
let fixed_id = arena.intern_val(ValueType::FixedSizeList(u8_id, 4));
let map_id = arena.intern_val(ValueType::Map(string_id, u8_id));
let plain_record = arena.intern_val(ValueType::Record(vec![("n".to_string(), u8_id)]));
let bridge = WitBridge::from_cviz(&arena);
assert!(bridge.has_lists(list_id));
assert!(bridge.has_lists(fixed_id));
assert!(bridge.has_lists(map_id));
assert!(!bridge.has_lists(plain_record));
assert!(!bridge.has_lists(string_id));
}
}