mod build;
pub(crate) use build::{build_indexed, index};
use crate::abi::{AbiParam, CType};
use crate::ir::{Api, TypeRef};
use crate::pkg::Identity;
use crate::ty::{Ty, TypeDecl, TypeIndex, TypeKind};
pub const RUNTIME_SYMBOLS: &[&str] = &[
"str",
"bytes",
"reader",
"writer",
"abi_version",
"error",
"error_set",
"error_set_payload",
"error_clear",
"error_free",
"free_bytes",
"cancel_token",
"cancel_token_create",
"cancel_token_cancel",
"cancel_token_is_cancelled",
"cancel_token_destroy",
"alloc",
"debug_live",
"contract_entry",
];
pub const RESERVED_SYMBOL_FAMILIES: &[&str] = &[
"str_", "bytes_", "reader_", "writer_", "list_", "map_", "opt_",
];
pub const VALUE_CODECS: &[&str] = &["write", "read", "decode", "free"];
pub const ABI_VERSION: u32 = 4;
#[must_use]
pub fn contract_symbol(prefix: &str, module: &str) -> String {
format!("{prefix}_{module}_contract")
}
#[must_use]
pub fn contract_check_symbol(prefix: &str, module: &str) -> String {
format!("{prefix}_{module}_contract_check")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AbiFn {
pub symbol: String,
pub params: Vec<AbiParam>,
pub ret: CType,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CallShape {
Sync(AbiFn),
Async(AsyncBinding),
Iterator(IteratorBinding),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AsyncBinding {
pub launch: AbiFn,
pub callback_type: String,
pub callback_params: Vec<AbiParam>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IteratorBinding {
pub elem: Ty,
pub iter_tag: String,
pub launch: AbiFn,
pub next: AbiFn,
pub destroy_symbol: String,
}
impl IteratorBinding {
pub fn item_ctype(&self) -> &CType {
match &self.next.params[1].ty {
CType::Ptr { pointee, .. } => pointee,
other => panic!("iterator `out_item` slot is {other:?}, not a pointer"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParamBinding {
pub name: String,
pub ty: Ty,
pub doc: Option<String>,
pub abi: Vec<AbiParam>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FnBinding {
pub name: String,
pub doc: Option<String>,
pub deprecated: Option<String>,
pub cancellable: bool,
pub throws: bool,
pub has_self: bool,
pub params: Vec<ParamBinding>,
pub ret: Option<Ty>,
pub c_base: String,
pub shape: CallShape,
}
impl FnBinding {
pub fn is_async(&self) -> bool {
matches!(self.shape, CallShape::Async(_))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldBinding {
pub name: String,
pub doc: Option<String>,
pub ty: Ty,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StructBinding {
pub name: String,
pub doc: Option<String>,
pub deprecated: Option<String>,
pub fields: Vec<FieldBinding>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnumBinding {
pub name: String,
pub doc: Option<String>,
pub deprecated: Option<String>,
pub c_tag: String,
pub variants: Vec<EnumVariantBinding>,
pub rich: bool,
}
impl EnumBinding {
pub fn is_rich(&self) -> bool {
self.rich
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnumVariantBinding {
pub name: String,
pub value: i32,
pub doc: Option<String>,
pub c_const: String,
pub fields: Vec<FieldBinding>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InterfaceBinding {
pub name: String,
pub doc: Option<String>,
pub deprecated: Option<String>,
pub c_tag: String,
pub constructors: Vec<FnBinding>,
pub methods: Vec<FnBinding>,
pub statics: Vec<FnBinding>,
pub clone_symbol: String,
pub destroy_symbol: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CallbackMethodBinding {
pub name: String,
pub doc: Option<String>,
pub deprecated: Option<String>,
pub throws: bool,
pub params: Vec<ParamBinding>,
pub ret: Option<Ty>,
pub abi_params: Vec<AbiParam>,
pub abi_ret: CType,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CallbackInterfaceBinding {
pub name: String,
pub doc: Option<String>,
pub deprecated: Option<String>,
pub c_tag: String,
pub vtable_tag: String,
pub methods: Vec<CallbackMethodBinding>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ErrorCodeBinding {
pub name: String,
pub value: i32,
pub message: String,
pub doc: Option<String>,
pub c_const: String,
pub fields: Vec<FieldBinding>,
}
impl ErrorCodeBinding {
pub fn payload_tag(&self) -> String {
format!("{}_payload", self.c_const)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ErrorBinding {
pub name: String,
pub type_name: String,
pub owner_path: String,
pub c_tag: String,
pub codes: Vec<ErrorCodeBinding>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModuleBinding {
pub name: String,
pub segments: Vec<String>,
pub path: String,
pub dot_path: String,
pub doc: Option<String>,
pub errors: Option<ErrorBinding>,
pub enums: Vec<EnumBinding>,
pub structs: Vec<StructBinding>,
pub interfaces: Vec<InterfaceBinding>,
pub callback_interfaces: Vec<CallbackInterfaceBinding>,
pub functions: Vec<FnBinding>,
}
impl ModuleBinding {
pub fn callables(&self) -> impl Iterator<Item = &FnBinding> {
self.functions
.iter()
.chain(self.interfaces.iter().flat_map(|i| {
i.constructors
.iter()
.chain(i.methods.iter())
.chain(i.statics.iter())
}))
}
pub fn has_async(&self) -> bool {
self.callables().any(FnBinding::is_async)
}
pub fn has_iterators(&self) -> bool {
self.callables()
.any(|f| matches!(f.shape, CallShape::Iterator(_)))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Model {
pub identity: Identity,
pub version: String,
pub modules: Vec<ModuleBinding>,
pub types: TypeIndex,
}
impl Model {
#[doc(hidden)]
#[must_use]
pub fn assume_valid(api: &Api, identity: Identity) -> Self {
build::build(api, identity)
}
pub fn resolve(&self, ty: &TypeRef) -> Ty {
build::resolve(&self.types, ty)
}
pub fn prefix(&self) -> &str {
&self.identity.prefix
}
pub fn roots(&self) -> impl Iterator<Item = &ModuleBinding> {
self.modules.iter().filter(|m| m.segments.len() == 1)
}
pub fn children<'a>(
&'a self,
parent: &'a ModuleBinding,
) -> impl Iterator<Item = &'a ModuleBinding> + 'a {
self.modules.iter().filter(move |m| {
m.segments.len() == parent.segments.len() + 1
&& m.segments[..parent.segments.len()] == parent.segments[..]
})
}
pub fn functions(&self) -> impl Iterator<Item = (&ModuleBinding, &FnBinding)> {
self.modules
.iter()
.flat_map(|m| m.functions.iter().map(move |f| (m, f)))
}
pub fn callables(&self) -> impl Iterator<Item = (&ModuleBinding, &FnBinding)> {
self.modules
.iter()
.flat_map(|m| m.callables().map(move |f| (m, f)))
}
pub fn callback_interfaces(
&self,
) -> impl Iterator<Item = (&ModuleBinding, &CallbackInterfaceBinding)> {
self.modules
.iter()
.flat_map(|m| m.callback_interfaces.iter().map(move |c| (m, c)))
}
fn decl(&self, name: &str, kinds: &[TypeKind]) -> &TypeDecl {
match self.types.get(name) {
Some(d) if kinds.contains(&d.kind) => d,
Some(d) => panic!("type '{name}' is a {:?}, not one of {kinds:?}", d.kind),
None => panic!("type '{name}' is not declared"),
}
}
pub fn owner(&self, name: &str) -> &ModuleBinding {
let decl = self.types.get(name);
let decl = decl.unwrap_or_else(|| panic!("type '{name}' is not declared"));
&self.modules[decl.module]
}
pub fn interface(&self, name: &str) -> &InterfaceBinding {
let d = self.decl(name, &[TypeKind::Interface]);
&self.modules[d.module].interfaces[d.index]
}
pub fn callback_interface(&self, name: &str) -> &CallbackInterfaceBinding {
let d = self.decl(name, &[TypeKind::CallbackInterface]);
&self.modules[d.module].callback_interfaces[d.index]
}
pub fn enumeration(&self, name: &str) -> &EnumBinding {
let d = self.decl(name, &[TypeKind::Enum, TypeKind::RichEnum]);
&self.modules[d.module].enums[d.index]
}
pub fn error_domain(&self, module: &ModuleBinding) -> Option<&ErrorBinding> {
(1..=module.segments.len()).rev().find_map(|n| {
let scope = &module.segments[..n];
self.modules
.iter()
.find(|m| m.segments == scope)
.and_then(|m| m.errors.as_ref())
})
}
pub fn c_symbols(&self) -> Vec<(String, String)> {
let p = self.prefix();
let mut out: Vec<(String, String)> = RUNTIME_SYMBOLS
.iter()
.map(|s| (format!("{p}_{s}"), format!("the runtime symbol '{p}_{s}'")))
.collect();
for m in self.roots() {
out.push((
contract_symbol(p, &m.name),
format!("the contract table of module '{}'", m.name),
));
out.push((
contract_check_symbol(p, &m.name),
format!("the contract check of module '{}'", m.name),
));
}
let callable = |out: &mut Vec<(String, String)>, f: &FnBinding, owner: &str| {
let what = format!("'{owner}.{}'", f.name);
match &f.shape {
CallShape::Sync(abi) => out.push((abi.symbol.clone(), format!("function {what}"))),
CallShape::Async(a) => {
out.push((a.launch.symbol.clone(), format!("async function {what}")));
out.push((
a.callback_type.clone(),
format!("the completion type of {what}"),
));
}
CallShape::Iterator(it) => {
out.push((it.launch.symbol.clone(), format!("function {what}")));
out.push((it.iter_tag.clone(), format!("the iterator type of {what}")));
out.push((
it.next.symbol.clone(),
format!("the iterator step of {what}"),
));
out.push((
it.destroy_symbol.clone(),
format!("the iterator destructor of {what}"),
));
}
}
};
let codecs = |out: &mut Vec<(String, String)>, tag: &str, what: &str| {
for codec in VALUE_CODECS {
out.push((
format!("{tag}_{codec}"),
format!("the value-buffer codec '{tag}_{codec}' of {what}"),
));
}
};
for m in &self.modules {
let dot = &m.dot_path;
if let Some(e) = &m.errors {
out.push((e.c_tag.clone(), format!("error domain '{dot}.{}'", e.name)));
for c in &e.codes {
let what = format!("error code '{dot}.{}.{}'", e.name, c.name);
out.push((c.c_const.clone(), what.clone()));
if !c.fields.is_empty() {
let payload = c.payload_tag();
codecs(&mut out, &payload, &format!("the payload of {what}"));
out.push((payload, format!("the payload struct of {what}")));
}
}
}
for e in &m.enums {
out.push((e.c_tag.clone(), format!("enum '{dot}.{}'", e.name)));
if e.rich {
out.push((
format!("{}_Tag", e.c_tag),
format!("the tag type of enum '{dot}.{}'", e.name),
));
codecs(&mut out, &e.c_tag, &format!("enum '{dot}.{}'", e.name));
}
for v in &e.variants {
out.push((
v.c_const.clone(),
format!("enum variant '{dot}.{}.{}'", e.name, v.name),
));
}
}
for s in &m.structs {
let tag = format!("{p}_{}_{}", m.path, s.name);
let what = format!("record '{dot}.{}'", s.name);
codecs(&mut out, &tag, &what);
out.push((tag, what));
}
for c in &m.callback_interfaces {
out.push((
c.vtable_tag.clone(),
format!("the vtable of callback interface '{dot}.{}'", c.name),
));
}
for i in &m.interfaces {
let owner = format!("{dot}.{}", i.name);
out.push((i.c_tag.clone(), format!("interface '{owner}'")));
out.push((i.clone_symbol.clone(), format!("the clone of '{owner}'")));
out.push((
i.destroy_symbol.clone(),
format!("the destructor of '{owner}'"),
));
for f in i.constructors.iter().chain(&i.methods).chain(&i.statics) {
callable(&mut out, f, &owner);
}
}
for f in &m.functions {
callable(&mut out, f, dot);
}
}
out
}
pub fn has_async(&self) -> bool {
self.modules.iter().any(ModuleBinding::has_async)
}
pub fn has_iterators(&self) -> bool {
self.modules.iter().any(ModuleBinding::has_iterators)
}
pub fn has_callback_interfaces(&self) -> bool {
self.modules
.iter()
.any(|m| !m.callback_interfaces.is_empty())
}
pub fn has_interfaces(&self) -> bool {
self.modules.iter().any(|m| !m.interfaces.is_empty())
}
pub fn has_buffers(&self) -> bool {
let buffered = |ty: &Ty| ty.any(&Ty::is_buffered);
let signature = |params: &[ParamBinding], ret: &Option<Ty>| {
params.iter().any(|p| buffered(&p.ty)) || ret.as_ref().is_some_and(buffered)
};
self.modules.iter().any(|m| {
!m.structs.is_empty()
|| m.enums.iter().any(|e| e.rich)
|| m.errors
.as_ref()
.is_some_and(|e| e.codes.iter().any(|c| !c.fields.is_empty()))
|| m.callables().any(|f| signature(&f.params, &f.ret))
|| m.callback_interfaces
.iter()
.any(|c| c.methods.iter().any(|f| signature(&f.params, &f.ret)))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::{
CallbackInterfaceDef, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, InterfaceDef,
Module, Param, StructDef, StructField, TypeRef,
};
use crate::ty::Prim;
fn param(name: &str, ty: TypeRef) -> Param {
Param {
name: name.into(),
ty,
doc: None,
}
}
fn func(name: &str, params: Vec<Param>, returns: Option<TypeRef>) -> Function {
Function {
name: name.into(),
params,
returns,
doc: None,
throws: false,
r#async: false,
cancellable: false,
deprecated: None,
}
}
fn module(name: &str) -> Module {
Module {
name: name.into(),
doc: None,
functions: vec![],
interfaces: vec![],
callback_interfaces: vec![],
structs: vec![],
enums: vec![],
errors: None,
modules: vec![],
}
}
fn build(modules: Vec<Module>, prefix: &str) -> Model {
let api = Api {
version: crate::ir::CURRENT_SCHEMA_VERSION.into(),
modules,
};
Model::assume_valid(&api, Identity::named(prefix))
}
fn rendered(abi: &AbiFn) -> Vec<String> {
abi.params
.iter()
.map(|p| format!("{} {}", p.ty.render_c("weaveffi"), p.name))
.collect()
}
#[test]
fn sync_function_symbol_and_sig() {
let m = Module {
functions: vec![func(
"add",
vec![
param("a", TypeRef::Prim(Prim::I32)),
param("b", TypeRef::Prim(Prim::I32)),
],
Some(TypeRef::Prim(Prim::I32)),
)],
..module("math")
};
let model = build(vec![m], "weaveffi");
let f = &model.modules[0].functions[0];
assert_eq!(f.c_base, "weaveffi_math_add");
let CallShape::Sync(abi) = &f.shape else {
panic!("expected sync")
};
assert_eq!(abi.symbol, "weaveffi_math_add");
assert_eq!(abi.ret, CType::Int32);
assert_eq!(
rendered(abi),
["int32_t a", "int32_t b", "weaveffi_error* out_err"]
);
let model = build(vec![module("net")], "acme");
assert_eq!(model.prefix(), "acme");
}
#[test]
fn async_function_has_launch_and_callback() {
let m = Module {
functions: vec![Function {
cancellable: true,
r#async: true,
..func(
"fetch",
vec![param("id", TypeRef::Prim(Prim::I64))],
Some(TypeRef::Prim(Prim::String)),
)
}],
..module("net")
};
let model = build(vec![m], "weaveffi");
let CallShape::Async(a) = &model.modules[0].functions[0].shape else {
panic!("expected async")
};
assert_eq!(a.launch.symbol, "weaveffi_net_fetch");
assert_eq!(a.callback_type, "weaveffi_net_fetch_callback");
let names: Vec<&str> = a.launch.params.iter().map(|p| p.name.as_str()).collect();
assert_eq!(names, ["id", "cancel_token", "callback", "context"]);
assert_eq!(a.callback_params[0].name, "context");
assert_eq!(a.callback_params[1].name, "err");
assert_eq!(a.callback_params[2].name, "result_ptr");
assert!(model.has_async());
}
#[test]
fn iterator_function_has_next_and_destroy() {
let m = Module {
functions: vec![func(
"get_messages",
vec![],
Some(TypeRef::Iterator(Box::new(TypeRef::Prim(Prim::String)))),
)],
..module("events")
};
let model = build(vec![m], "weaveffi");
let CallShape::Iterator(it) = &model.modules[0].functions[0].shape else {
panic!("expected iterator")
};
assert_eq!(it.iter_tag, "weaveffi_events_GetMessagesIterator");
assert_eq!(it.launch.symbol, "weaveffi_events_get_messages");
assert_eq!(it.next.symbol, "weaveffi_events_GetMessagesIterator_next");
assert_eq!(
it.destroy_symbol,
"weaveffi_events_GetMessagesIterator_destroy"
);
assert_eq!(it.elem, Ty::Prim(Prim::String));
assert_eq!(it.next.ret, CType::Int32);
assert_eq!(it.next.params[1].ty.render_c("weaveffi"), "const uint8_t**");
assert_eq!(it.item_ctype().render_c("weaveffi"), "const uint8_t*");
assert!(model.has_iterators());
}
#[test]
fn user_types_resolve_to_kinds_and_buffers() {
let shared = Module {
structs: vec![StructDef {
name: "Contact".into(),
doc: None,
deprecated: Some("use Person".into()),
fields: vec![
StructField {
name: "name".into(),
ty: TypeRef::Prim(Prim::String),
doc: None,
},
StructField {
name: "status".into(),
ty: TypeRef::Named("Status".into()),
doc: None,
},
StructField {
name: "store".into(),
ty: TypeRef::Optional(Box::new(TypeRef::Named("Store".into()))),
doc: None,
},
],
}],
enums: vec![EnumDef {
name: "Status".into(),
doc: None,
deprecated: None,
variants: vec![EnumVariant {
name: "Ok".into(),
value: 0,
doc: None,
fields: vec![],
}],
}],
interfaces: vec![InterfaceDef {
name: "Store".into(),
doc: None,
deprecated: None,
constructors: vec![func("open", vec![], None)],
methods: vec![func(
"save",
vec![param("contact", TypeRef::Named("Contact".into()))],
Some(TypeRef::List(Box::new(TypeRef::Named("Contact".into())))),
)],
statics: vec![],
}],
..module("contacts")
};
let other = Module {
functions: vec![func(
"status_of",
vec![param("store", TypeRef::Named("Store".into()))],
Some(TypeRef::Named("Status".into())),
)],
..module("ops")
};
let model = build(vec![shared, other], "weaveffi");
let contacts = &model.modules[0];
let s = &contacts.structs[0];
assert_eq!(s.deprecated.as_deref(), Some("use Person"));
assert_eq!(s.fields[1].ty, Ty::Enum("Status".into()));
assert_eq!(
s.fields[2].ty,
Ty::Optional(Box::new(Ty::Interface("Store".into())))
);
assert_eq!(contacts.enums[0].c_tag, "weaveffi_contacts_Status");
assert_eq!(
contacts.enums[0].variants[0].c_const,
"weaveffi_contacts_Status_Ok"
);
let iface = &contacts.interfaces[0];
assert_eq!(iface.c_tag, "weaveffi_contacts_Store");
assert_eq!(iface.clone_symbol, "weaveffi_contacts_Store_clone");
assert_eq!(iface.destroy_symbol, "weaveffi_contacts_Store_destroy");
assert_eq!(
iface.constructors[0].ret,
Some(Ty::Interface("Store".into()))
);
let save = &iface.methods[0];
assert!(save.has_self);
assert_eq!(save.params[0].ty, Ty::Record("Contact".into()));
let CallShape::Sync(abi) = &save.shape else {
panic!("expected sync")
};
assert_eq!(
rendered(abi),
[
"const weaveffi_contacts_Store* self",
"const uint8_t* contact_ptr",
"size_t contact_len",
"size_t* out_len",
"weaveffi_error* out_err"
]
);
assert_eq!(abi.ret.render_c("weaveffi"), "const uint8_t*");
let ops = &model.modules[1];
let f = &ops.functions[0];
assert_eq!(f.params[0].ty, Ty::Interface("Store".into()));
assert_eq!(f.ret, Some(Ty::Enum("Status".into())));
let CallShape::Sync(abi) = &f.shape else {
panic!("expected sync")
};
assert_eq!(
rendered(abi),
[
"const weaveffi_contacts_Store* store",
"weaveffi_error* out_err"
]
);
assert_eq!(abi.ret.render_c("weaveffi"), "weaveffi_contacts_Status");
assert!(model.has_buffers());
assert!(model.has_interfaces());
assert_eq!(model.interface("Store").c_tag, "weaveffi_contacts_Store");
assert_eq!(model.owner("Status").path, "contacts");
assert_eq!(
model.enumeration("Status").c_tag,
"weaveffi_contacts_Status"
);
}
#[test]
fn callback_interfaces_lower_to_vtables() {
let m = Module {
callback_interfaces: vec![CallbackInterfaceDef {
name: "Listener".into(),
doc: Some("Receives messages.".into()),
deprecated: None,
methods: vec![
func(
"on_message",
vec![param("text", TypeRef::Prim(Prim::String))],
None,
),
func("should_stop", vec![], Some(TypeRef::Prim(Prim::Bool))),
],
}],
functions: vec![func(
"subscribe",
vec![param("listener", TypeRef::Named("Listener".into()))],
None,
)],
..module("events")
};
let model = build(vec![m], "weaveffi");
let mb = &model.modules[0];
let cb = &mb.callback_interfaces[0];
assert_eq!(cb.c_tag, "weaveffi_events_Listener");
assert_eq!(cb.vtable_tag, "weaveffi_events_Listener_vtable");
let on_message = &cb.methods[0];
let slots: Vec<String> = on_message
.abi_params
.iter()
.map(|p| format!("{} {}", p.ty.render_c("weaveffi"), p.name))
.collect();
assert_eq!(
slots,
[
"void* ctx",
"const uint8_t* text_ptr",
"size_t text_len",
"weaveffi_error* out_err"
]
);
assert_eq!(on_message.abi_ret, CType::Void);
assert_eq!(cb.methods[1].abi_ret, CType::Bool);
assert_eq!(model.callback_interface("Listener").c_tag, cb.c_tag);
assert!(model.has_callback_interfaces());
let f = &mb.functions[0];
assert_eq!(f.params[0].ty, Ty::CallbackInterface("Listener".into()));
let CallShape::Sync(abi) = &f.shape else {
panic!("expected sync")
};
assert_eq!(
rendered(abi),
[
"void* listener_ctx",
"const weaveffi_events_Listener_vtable* listener_vtable",
"weaveffi_error* out_err"
]
);
assert!(!model.has_buffers());
}
#[test]
fn nested_modules_flatten_pre_order_with_paths_and_docs() {
let inner = Module {
functions: vec![Function {
doc: Some("Leaf.".into()),
..func("leaf_fn", vec![], None)
}],
..module("inner")
};
let outer = Module {
doc: Some("Outer module.".into()),
functions: vec![func("outer_fn", vec![], None)],
modules: vec![inner],
..module("outer")
};
let model = build(vec![outer], "weaveffi");
let paths: Vec<&str> = model.modules.iter().map(|m| m.path.as_str()).collect();
assert_eq!(paths, ["outer", "outer_inner"]);
assert_eq!(model.modules[1].dot_path, "outer.inner");
assert_eq!(
model.modules[1].functions[0].c_base,
"weaveffi_outer_inner_leaf_fn"
);
assert_eq!(model.modules[0].doc.as_deref(), Some("Outer module."));
assert_eq!(model.modules[1].doc, None);
}
#[test]
fn error_domains_are_declared_once_and_inherited_by_lookup() {
let domain = ErrorDomain {
name: "OuterError".into(),
codes: vec![ErrorCode {
name: "Bad".into(),
code: 1,
message: "bad".into(),
doc: None,
fields: vec![],
}],
};
let outer = Module {
errors: Some(domain),
modules: vec![module("inner")],
..module("outer")
};
let model = build(vec![outer, module("other")], "weaveffi");
let [outer, inner, other] = &model.modules[..] else {
panic!("three modules");
};
assert!(outer.errors.is_some() && inner.errors.is_none());
assert_eq!(
model.error_domain(inner).map(|e| e.c_tag.as_str()),
Some("weaveffi_outer_OuterError")
);
assert_eq!(model.error_domain(outer), outer.errors.as_ref());
assert!(model.error_domain(other).is_none());
}
}