Skip to main content

weaveffi_model/
rust.rs

1//! Extract a WeaveFFI [`Api`] from annotated Rust source.
2//!
3//! This module is the single Rust-to-IR bridge shared by two callers:
4//!
5//! * the `#[weaveffi::module]` proc-macro, which lowers a module to its IR to
6//!   build the C ABI scaffolding at compile time; and
7//! * the `weaveffi` CLI, which reads the same annotated source to drive
8//!   `generate`/`extract`.
9//!
10//! Because both paths call into the *same* extraction, the IDL the CLI emits
11//! and the symbols the macro produces cannot drift: they are two views of one
12//! parse. Annotated Rust is therefore the single source of truth.
13//!
14//! # The annotation scheme
15//!
16//! A `#[weaveffi::module]` on an inline `mod` marks an exported namespace.
17//! Inside it:
18//!
19//! * `#[weaveffi::export]` on a `fn` exports a function. An `async fn` lowers to
20//!   an asynchronous symbol; a `fn -> Result<T, E>` is fallible (`throws` in
21//!   the IDL; the return type is `T` and errors are reported through the ABI's
22//!   `out_err`).
23//! * `#[weaveffi::interface]` on a `struct` declares an interface (opaque,
24//!   reference-counted object type). Its `impl` block's `pub fn`s become the
25//!   interface's members: an associated function returning `Self` (or
26//!   `Arc<Self>`) is a constructor, a `&self` or `self: Arc<Self>` function is
27//!   a method, and any other associated function is a static.
28//! * `#[weaveffi::record]` on a `struct` declares a by-value record.
29//! * `#[weaveffi::enumeration]` on a `#[repr(i32)]` `enum` declares a C-style
30//!   enum; an enum with data-carrying variants declares a rich enum.
31//! * `#[weaveffi::error]` on an enum declares the module's error domain.
32//! * `#[weaveffi::callback_interface]` on a `trait` declares a callback
33//!   interface: a method set the consumer implements. Producers accept one as
34//!   `Arc<dyn Trait>`.
35//!
36//! The type mapping mirrors the IDL: `String` (or `&str`) is a string, `Vec<u8>`
37//! (or `&[u8]`) is a byte buffer, every integer primitive (including `u64`) is
38//! the matching scalar, `Vec<T>`, `Option<T>`, and `HashMap`/`BTreeMap` are the
39//! list, optional, and map types, `weaveffi::Iter<T>` is an `iter<T>` return,
40//! `Arc<T>` names the interface `T` (an object reference), `Arc<dyn Trait>`
41//! names the callback interface `Trait`, and any other named path is a record,
42//! enum, or interface resolved later.
43
44use crate::ir::{
45    Api, CallbackInterfaceDef, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function,
46    InterfaceDef, Module, Param, StructDef, StructField, TypeRef, CURRENT_SCHEMA_VERSION,
47};
48use syn::spanned::Spanned;
49
50/// Match a WeaveFFI marker attribute by its final path segment.
51///
52/// Accepts both the namespaced form (`#[weaveffi::record]`) and the bare form
53/// (`#[record]`), so the macro's stripped re-emit and a hand-written attribute
54/// resolve identically.
55fn is_marker(attr: &syn::Attribute, name: &str) -> bool {
56    attr.path().segments.last().is_some_and(|s| s.ident == name)
57}
58
59/// Whether any attribute in `attrs` is the WeaveFFI marker `name`.
60pub fn has_marker(attrs: &[syn::Attribute], name: &str) -> bool {
61    attrs.iter().any(|a| is_marker(a, name))
62}
63
64fn has_repr_i32(attrs: &[syn::Attribute]) -> bool {
65    attrs.iter().any(|a| {
66        a.path().is_ident("repr") && a.parse_args::<syn::Ident>().is_ok_and(|id| id == "i32")
67    })
68}
69
70/// Parse `#[deprecated(since = "...", note = "...")]` into `(since, note)`.
71fn parse_deprecated(attrs: &[syn::Attribute]) -> (Option<String>, Option<String>) {
72    let Some(attr) = attrs.iter().find(|a| a.path().is_ident("deprecated")) else {
73        return (None, None);
74    };
75    let mut since = None;
76    let mut note = None;
77    if matches!(attr.meta, syn::Meta::Path(_)) {
78        return (None, Some("deprecated".to_string()));
79    }
80    let _ = attr.parse_nested_meta(|meta| {
81        let Some(ident) = meta.path.get_ident() else {
82            return Ok(());
83        };
84        let value = meta.value()?;
85        let lit: syn::LitStr = value.parse()?;
86        match ident.to_string().as_str() {
87            "since" => since = Some(lit.value()),
88            "note" => note = Some(lit.value()),
89            _ => {}
90        }
91        Ok(())
92    });
93    if note.is_none() && since.is_none() {
94        note = Some("deprecated".to_string());
95    }
96    (since, note)
97}
98
99fn extract_doc(attrs: &[syn::Attribute]) -> Option<String> {
100    let lines: Vec<String> = attrs
101        .iter()
102        .filter_map(|attr| {
103            let syn::Meta::NameValue(nv) = &attr.meta else {
104                return None;
105            };
106            if !nv.path.is_ident("doc") {
107                return None;
108            }
109            let syn::Expr::Lit(syn::ExprLit {
110                lit: syn::Lit::Str(s),
111                ..
112            }) = &nv.value
113            else {
114                return None;
115            };
116            let val = s.value();
117            Some(match val.strip_prefix(' ') {
118                Some(stripped) => stripped.to_string(),
119                None => val,
120            })
121        })
122        .collect();
123    (!lines.is_empty()).then(|| lines.join("\n"))
124}
125
126fn is_ident(ty: &syn::Type, name: &str) -> bool {
127    matches!(ty, syn::Type::Path(p) if p.path.is_ident(name))
128}
129
130fn single_generic_arg(seg: &syn::PathSegment) -> syn::Result<&syn::Type> {
131    let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
132        return Err(syn::Error::new(seg.span(), "expected generic arguments"));
133    };
134    if args.args.len() != 1 {
135        return Err(syn::Error::new(
136            seg.span(),
137            "expected exactly 1 generic argument",
138        ));
139    }
140    let syn::GenericArgument::Type(ty) = &args.args[0] else {
141        return Err(syn::Error::new(seg.span(), "expected a type argument"));
142    };
143    Ok(ty)
144}
145
146fn two_generic_args(seg: &syn::PathSegment) -> syn::Result<(&syn::Type, &syn::Type)> {
147    let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
148        return Err(syn::Error::new(seg.span(), "expected generic arguments"));
149    };
150    if args.args.len() != 2 {
151        return Err(syn::Error::new(
152            seg.span(),
153            "expected exactly 2 generic arguments",
154        ));
155    }
156    let syn::GenericArgument::Type(k) = &args.args[0] else {
157        return Err(syn::Error::new(seg.span(), "expected a key type argument"));
158    };
159    let syn::GenericArgument::Type(v) = &args.args[1] else {
160        return Err(syn::Error::new(
161            seg.span(),
162            "expected a value type argument",
163        ));
164    };
165    Ok((k, v))
166}
167
168fn type_path_ident(ty: &syn::Type) -> Option<String> {
169    let syn::Type::Path(p) = ty else { return None };
170    p.path.segments.last().map(|s| s.ident.to_string())
171}
172
173/// The bare name of the single trait a `dyn Trait` object names, ignoring
174/// auto-trait and lifetime bounds (`dyn Listener + Send + Sync + 'static`).
175fn trait_object_name(ty: &syn::Type) -> Option<String> {
176    let syn::Type::TraitObject(obj) = ty else {
177        return None;
178    };
179    let mut names = obj.bounds.iter().filter_map(|b| match b {
180        syn::TypeParamBound::Trait(t) => {
181            let name = t.path.segments.last()?.ident.to_string();
182            (!matches!(name.as_str(), "Send" | "Sync" | "Unpin")).then_some(name)
183        }
184        _ => None,
185    });
186    let first = names.next()?;
187    names.next().is_none().then_some(first)
188}
189
190/// Peel `Arc<T>` to its `T`, returning any other type unchanged.
191///
192/// `Arc` is how a producer spells an interface object it holds or returns
193/// (`Arc<Store>`, `Arc<Self>`) and a callback interface it accepts
194/// (`Arc<dyn Listener>`); the IR names the pointee type in both cases.
195pub fn peel_arc(ty: &syn::Type) -> &syn::Type {
196    if let syn::Type::Path(p) = ty {
197        if let Some(seg) = p.path.segments.last() {
198            if seg.ident == "Arc" {
199                if let Ok(inner) = single_generic_arg(seg) {
200                    return inner;
201                }
202            }
203        }
204    }
205    ty
206}
207
208/// Whether a parameter's type is the reserved `weaveffi::CancelToken`.
209///
210/// A `#[weaveffi::cancellable]` `async fn` accepts a `CancelToken` as its final
211/// parameter, but the token is part of the async *calling convention* (the
212/// launcher's `cancel_token` slot), not the function's logical signature, so it
213/// is filtered out of the extracted parameter list. Matching on the final path
214/// segment accepts both the bare and `weaveffi::`-qualified spellings.
215fn is_cancel_token(ty: &syn::Type) -> bool {
216    type_path_ident(ty).as_deref() == Some("CancelToken")
217}
218
219/// Map a Rust [`syn::Type`] onto the WeaveFFI [`TypeRef`] it represents.
220///
221/// This is the canonical mapping every caller shares. Notable conventions:
222///
223/// * `String` and `&str` are both the `string` type; `Vec<u8>` and `&[u8]`
224///   are both `bytes`. A reference is a producer-side calling convention (the
225///   thunk lifts an owned value and lends it), not an IDL distinction.
226/// * every integer primitive maps to its matching scalar (`u64` included).
227/// * `Vec<T>` and `&[T]` are lists, `Option<T>` is an optional, and
228///   `HashMap`/`BTreeMap` are maps.
229/// * `weaveffi::Iter<T>` is an `iter<T>` return.
230/// * `Arc<T>` and `&T` name the interface `T`; `Arc<dyn Trait>` names the
231///   callback interface `Trait`. Any other named path is a record, enum,
232///   interface, or callback interface reference resolved by the validator.
233///
234/// # Errors
235///
236/// Returns a spanned error for type syntax WeaveFFI cannot express across the
237/// FFI boundary (raw pointers, `Box<dyn Trait>`, tuples, a generic with the
238/// wrong arity, and so on).
239pub fn type_ref_from_syn(ty: &syn::Type) -> syn::Result<TypeRef> {
240    match ty {
241        syn::Type::Reference(r) => {
242            if let syn::Type::Path(p) = r.elem.as_ref() {
243                if p.path.is_ident("str") {
244                    return Ok(TypeRef::StringUtf8);
245                }
246            }
247            if let syn::Type::Slice(slice) = r.elem.as_ref() {
248                if is_ident(&slice.elem, "u8") {
249                    return Ok(TypeRef::Bytes);
250                }
251                return Ok(TypeRef::List(Box::new(type_ref_from_syn(&slice.elem)?)));
252            }
253            type_ref_from_syn(&r.elem)
254        }
255        syn::Type::Ptr(_) => Err(syn::Error::new(
256            ty.span(),
257            "weaveffi: raw pointers cannot cross the FFI boundary; declare the pointee as a \
258             #[weaveffi::interface] and pass it as `&T` or `Arc<T>`",
259        )),
260        syn::Type::TraitObject(_) => {
261            let name = trait_object_name(ty).ok_or_else(|| {
262                syn::Error::new(
263                    ty.span(),
264                    "weaveffi: a trait object must name exactly one #[weaveffi::callback_interface] \
265                     trait (plus optional `Send`/`Sync` bounds)",
266                )
267            })?;
268            Ok(TypeRef::Named(name))
269        }
270        syn::Type::Path(type_path) => {
271            let seg = type_path
272                .path
273                .segments
274                .last()
275                .ok_or_else(|| syn::Error::new(ty.span(), "empty type path"))?;
276            let ident = seg.ident.to_string();
277            match ident.as_str() {
278                "i8" => Ok(TypeRef::I8),
279                "i16" => Ok(TypeRef::I16),
280                "i32" => Ok(TypeRef::I32),
281                "i64" => Ok(TypeRef::I64),
282                "u8" => Ok(TypeRef::U8),
283                "u16" => Ok(TypeRef::U16),
284                "u32" => Ok(TypeRef::U32),
285                "f32" => Ok(TypeRef::F32),
286                "f64" => Ok(TypeRef::F64),
287                "bool" => Ok(TypeRef::Bool),
288                "String" => Ok(TypeRef::StringUtf8),
289                "u64" => Ok(TypeRef::U64),
290                // `Arc<T>` is a reference to the interface object `T`, and
291                // `Arc<dyn Trait>` a consumer-implemented callback interface;
292                // the IR names the pointee in both cases.
293                "Arc" => type_ref_from_syn(single_generic_arg(seg)?),
294                "Box" | "Rc" => Err(syn::Error::new(
295                    ty.span(),
296                    format!(
297                        "weaveffi: `{ident}` cannot cross the FFI boundary; objects and callback \
298                         interfaces are shared, so spell them as `Arc<T>` / `Arc<dyn Trait>`"
299                    ),
300                )),
301                "Vec" => {
302                    let inner = single_generic_arg(seg)?;
303                    if is_ident(inner, "u8") {
304                        return Ok(TypeRef::Bytes);
305                    }
306                    Ok(TypeRef::List(Box::new(type_ref_from_syn(inner)?)))
307                }
308                "Option" => {
309                    let inner = single_generic_arg(seg)?;
310                    Ok(TypeRef::Optional(Box::new(type_ref_from_syn(inner)?)))
311                }
312                // `weaveffi::Iter<T>` is the producer spelling of an `iter<T>`
313                // return: a lazily-pulled stream rather than a materialized list.
314                "Iter" => {
315                    let inner = single_generic_arg(seg)?;
316                    Ok(TypeRef::Iterator(Box::new(type_ref_from_syn(inner)?)))
317                }
318                "HashMap" | "BTreeMap" => {
319                    let (k, v) = two_generic_args(seg)?;
320                    Ok(TypeRef::Map(
321                        Box::new(type_ref_from_syn(k)?),
322                        Box::new(type_ref_from_syn(v)?),
323                    ))
324                }
325                other => Ok(TypeRef::Named(other.to_string())),
326            }
327        }
328        _ => Err(syn::Error::new(ty.span(), "unsupported type syntax")),
329    }
330}
331
332/// Peel `Result<T, E>` to its `T`, returning any other type unchanged. A
333/// `Result` return marks the function `throws` in the IDL; the error type
334/// itself carries no extra IR shape (it reports through the ABI's `out_err`).
335pub fn peel_result(ty: &syn::Type) -> &syn::Type {
336    if let syn::Type::Path(p) = ty {
337        if let Some(seg) = p.path.segments.last() {
338            if seg.ident == "Result" {
339                if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
340                    if let Some(syn::GenericArgument::Type(ok)) = args.args.first() {
341                        return ok;
342                    }
343                }
344            }
345        }
346    }
347    ty
348}
349
350/// Whether a function's return type is a `Result<_, _>`, which marks the
351/// function as `throws` in the IDL.
352pub fn output_is_result(output: &syn::ReturnType) -> bool {
353    let syn::ReturnType::Type(_, ty) = output else {
354        return false;
355    };
356    matches!(ty.as_ref(), syn::Type::Path(p)
357        if p.path.segments.last().is_some_and(|s| s.ident == "Result"))
358}
359
360fn is_unit(ty: &syn::Type) -> bool {
361    matches!(ty, syn::Type::Tuple(t) if t.elems.is_empty())
362}
363
364/// Map a function's return type to its IDL return [`TypeRef`], peeling
365/// `Result<T, E>` and treating `()` (and `Result<(), E>`) as no return.
366///
367/// # Errors
368///
369/// Propagates any error from [`type_ref_from_syn`] on the (peeled) return type.
370pub fn return_type_from_syn(output: &syn::ReturnType) -> syn::Result<Option<TypeRef>> {
371    match output {
372        syn::ReturnType::Default => Ok(None),
373        syn::ReturnType::Type(_, ty) => {
374            let inner = peel_result(ty);
375            if is_unit(inner) {
376                Ok(None)
377            } else {
378                Ok(Some(type_ref_from_syn(inner)?))
379            }
380        }
381    }
382}
383
384fn parse_discriminant(expr: &syn::Expr) -> syn::Result<i32> {
385    match expr {
386        syn::Expr::Lit(lit) => {
387            let syn::Lit::Int(int_lit) = &lit.lit else {
388                return Err(syn::Error::new(
389                    expr.span(),
390                    "expected an integer literal discriminant",
391                ));
392            };
393            int_lit.base10_parse::<i32>()
394        }
395        syn::Expr::Unary(unary) if matches!(unary.op, syn::UnOp::Neg(_)) => {
396            Ok(-parse_discriminant(&unary.expr)?)
397        }
398        _ => Err(syn::Error::new(
399            expr.span(),
400            "unsupported discriminant expression",
401        )),
402    }
403}
404
405fn extract_params(
406    inputs: &syn::punctuated::Punctuated<syn::FnArg, syn::Token![,]>,
407) -> syn::Result<Vec<Param>> {
408    inputs
409        .iter()
410        .filter_map(|arg| match arg {
411            syn::FnArg::Typed(pt) => Some(pt),
412            syn::FnArg::Receiver(_) => None,
413        })
414        // The cancellation token is part of the async calling convention, not a
415        // logical parameter, so it never appears in the IDL.
416        .filter(|pt| !is_cancel_token(&pt.ty))
417        .map(|pt| {
418            let param_name = match pt.pat.as_ref() {
419                syn::Pat::Ident(id) => id.ident.to_string(),
420                _ => return Err(syn::Error::new(pt.span(), "unsupported parameter pattern")),
421            };
422            if matches!(pt.ty.as_ref(), syn::Type::Reference(r) if r.mutability.is_some()) {
423                return Err(syn::Error::new(
424                    pt.ty.span(),
425                    "weaveffi: `&mut` parameters cannot cross the FFI boundary; take the value \
426                     by `&T` or by value and return the updated result",
427                ));
428            }
429            let ty = type_ref_from_syn(&pt.ty)?;
430            // The proc macro path skips `validate_api` (see the macro crate),
431            // so the one type shape lowering can't represent as a parameter
432            // must be caught here with a span instead of panicking later.
433            let mut has_iterator = false;
434            ty.walk(&mut |t| has_iterator |= matches!(t, TypeRef::Iterator(_)));
435            if has_iterator {
436                return Err(syn::Error::new(
437                    pt.ty.span(),
438                    "weaveffi: `Iter<T>` is a pull handle the consumer drives and is only \
439                     valid as a function's outermost return type, not as a parameter",
440                ));
441            }
442            Ok(Param {
443                name: param_name,
444                ty,
445                doc: extract_doc(&pt.attrs),
446            })
447        })
448        .collect()
449}
450
451/// Map one `fn` signature (free function, interface member, or callback
452/// method) to the IR [`Function`]. `returns` is supplied by the caller because
453/// constructors and `Self` returns need interface-aware handling.
454fn function_from_sig(
455    sig: &syn::Signature,
456    attrs: &[syn::Attribute],
457    returns: Option<TypeRef>,
458) -> syn::Result<Function> {
459    if let Some(ret) = &returns {
460        let nested_iterator = |t: &TypeRef| {
461            let mut found = false;
462            t.walk(&mut |inner| found |= matches!(inner, TypeRef::Iterator(_)));
463            found
464        };
465        let invalid = match ret {
466            TypeRef::Iterator(elem) => nested_iterator(elem),
467            other => nested_iterator(other),
468        };
469        if invalid {
470            let span = match &sig.output {
471                syn::ReturnType::Type(_, ty) => ty.span(),
472                other => other.span(),
473            };
474            return Err(syn::Error::new(
475                span,
476                "weaveffi: `Iter<T>` is only valid as the outermost return type; it can't be \
477                 nested in an `Option`, `Vec`, map, or another `Iter`",
478            ));
479        }
480    }
481    Ok(Function {
482        name: sig.ident.to_string(),
483        params: extract_params(&sig.inputs)?,
484        returns,
485        doc: extract_doc(attrs),
486        throws: output_is_result(&sig.output),
487        r#async: sig.asyncness.is_some(),
488        cancellable: has_marker(attrs, "cancellable"),
489        deprecated: parse_deprecated(attrs).1,
490    })
491}
492
493fn extract_function(item: &syn::ItemFn) -> syn::Result<Function> {
494    let returns = return_type_from_syn(&item.sig.output)?;
495    function_from_sig(&item.sig, &item.attrs, returns)
496}
497
498/// Whether the (peeled) return type names the interface itself (`Self`,
499/// `Arc<Self>`, or the interface's own name), which classifies an associated
500/// function as a constructor.
501fn returns_self(output: &syn::ReturnType, iface: &str) -> bool {
502    let syn::ReturnType::Type(_, ty) = output else {
503        return false;
504    };
505    match type_path_ident(peel_arc(peel_result(ty))) {
506        Some(name) => name == "Self" || name == iface,
507        None => false,
508    }
509}
510
511/// Extract one interface member from an `impl` block function.
512///
513/// The receiver decides the member kind at the call site (see
514/// [`members_from_impl`]); this helper maps the signature. A `Self` (or
515/// interface-named) return on a constructor is dropped: the IR leaves a
516/// constructor's `return` empty because the instance is implicit.
517fn extract_member(item: &syn::ImplItemFn, iface: &str, is_ctor: bool) -> syn::Result<Function> {
518    let returns = if is_ctor {
519        None
520    } else {
521        match return_type_from_syn(&item.sig.output)? {
522            // `fn hand(&self) -> Arc<Self>` style returns name the interface.
523            Some(TypeRef::Named(name)) if name == "Self" => Some(TypeRef::Named(iface.to_string())),
524            other => other,
525        }
526    };
527    function_from_sig(&item.sig, &item.attrs, returns)
528}
529
530/// Whether a method receiver is one WeaveFFI can lift: `&self` (a borrow for
531/// the call) or `self: Arc<Self>` (a retained reference).
532///
533/// `&mut self` and by-value `self` are rejected: the object is shared across
534/// the FFI boundary (and may be in use on other threads), so mutable state
535/// needs interior mutability.
536pub fn receiver_is_supported(recv: &syn::Receiver) -> bool {
537    if recv.mutability.is_some() {
538        return false;
539    }
540    if recv.reference.is_some() {
541        return true;
542    }
543    recv.colon_token.is_some() && type_path_ident(&recv.ty).as_deref() == Some("Arc")
544}
545
546/// Classify and extract every `pub fn` of an interface's `impl` block into
547/// the interface's constructors, methods, and statics.
548///
549/// * a function with a `&self` or `self: Arc<Self>` receiver is a **method**;
550/// * an associated function returning `Self` or `Arc<Self>` (or the interface
551///   type, optionally inside `Result`) is a **constructor**;
552/// * any other associated function is a **static**.
553///
554/// Non-`pub` items are private helpers and stay unexported.
555fn members_from_impl(item_impl: &syn::ItemImpl, iface: &mut InterfaceDef) -> syn::Result<()> {
556    for impl_item in &item_impl.items {
557        let syn::ImplItem::Fn(f) = impl_item else {
558            continue;
559        };
560        if !matches!(f.vis, syn::Visibility::Public(_)) {
561            continue;
562        }
563        match f.sig.receiver() {
564            Some(recv) => {
565                if !receiver_is_supported(recv) {
566                    return Err(syn::Error::new(
567                        recv.span(),
568                        "weaveffi: interface methods must take `&self` or `self: Arc<Self>`; \
569                         use interior mutability (Mutex, RwLock, atomics) for mutable state, \
570                         because the object is shared across the FFI boundary",
571                    ));
572                }
573                iface.methods.push(extract_member(f, &iface.name, false)?);
574            }
575            None if returns_self(&f.sig.output, &iface.name) => {
576                iface
577                    .constructors
578                    .push(extract_member(f, &iface.name, true)?);
579            }
580            None => {
581                iface.statics.push(extract_member(f, &iface.name, false)?);
582            }
583        }
584    }
585    Ok(())
586}
587
588/// Whether a return type is `Result<T, ForeignError>`: a callback method's
589/// way of receiving the consumer's failure as a value. The `Result` is a
590/// producer-side convention (the IDL method still returns `T` and doesn't
591/// throw), so it's peeled without marking the method `throws`.
592pub fn returns_foreign_result(output: &syn::ReturnType) -> bool {
593    let syn::ReturnType::Type(_, ty) = output else {
594        return false;
595    };
596    let syn::Type::Path(p) = ty.as_ref() else {
597        return false;
598    };
599    let Some(seg) = p.path.segments.last() else {
600        return false;
601    };
602    if seg.ident != "Result" {
603        return false;
604    }
605    let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
606        return false;
607    };
608    match args.args.iter().nth(1) {
609        Some(syn::GenericArgument::Type(err)) => {
610            type_path_ident(err).as_deref() == Some("ForeignError")
611        }
612        _ => false,
613    }
614}
615
616/// Extract a `#[weaveffi::callback_interface]` trait into a
617/// [`CallbackInterfaceDef`].
618///
619/// Every trait method is a callback method the consumer implements. Methods
620/// must take `&self`. A method may return `Result<T, weaveffi::ForeignError>`
621/// to receive the consumer's failure as a value; that maps to a plain `T`
622/// return. The validator rejects `async`, other `Result` returns, and
623/// non-direct return types, so those are mapped faithfully here and reported
624/// with the IDL-level diagnostics.
625fn extract_callback_interface(item: &syn::ItemTrait) -> syn::Result<CallbackInterfaceDef> {
626    let mut methods = Vec::new();
627    for trait_item in &item.items {
628        let syn::TraitItem::Fn(f) = trait_item else {
629            continue;
630        };
631        match f.sig.receiver() {
632            Some(recv) if recv.reference.is_some() && recv.mutability.is_none() => {}
633            Some(recv) => {
634                return Err(syn::Error::new(
635                    recv.span(),
636                    "weaveffi: callback interface methods must take `&self`",
637                ));
638            }
639            None => {
640                return Err(syn::Error::new(
641                    f.sig.span(),
642                    "weaveffi: callback interface methods must take `&self` (associated \
643                     functions can't be implemented by the consumer)",
644                ));
645            }
646        }
647        let returns = return_type_from_syn(&f.sig.output)?;
648        let mut method = function_from_sig(&f.sig, &f.attrs, returns)?;
649        if returns_foreign_result(&f.sig.output) {
650            method.throws = false;
651        }
652        methods.push(method);
653    }
654    Ok(CallbackInterfaceDef {
655        name: item.ident.to_string(),
656        doc: extract_doc(&item.attrs),
657        deprecated: parse_deprecated(&item.attrs).1,
658        methods,
659    })
660}
661
662fn extract_struct(item: &syn::ItemStruct) -> syn::Result<StructDef> {
663    let name = item.ident.to_string();
664    let fields = match &item.fields {
665        syn::Fields::Named(named) => named
666            .named
667            .iter()
668            .map(|f| {
669                let field_name = f
670                    .ident
671                    .as_ref()
672                    .ok_or_else(|| syn::Error::new(f.span(), "unnamed field in record"))?
673                    .to_string();
674                Ok(StructField {
675                    name: field_name,
676                    ty: type_ref_from_syn(&f.ty)?,
677                    doc: extract_doc(&f.attrs),
678                })
679            })
680            .collect::<syn::Result<_>>()?,
681        _ => {
682            return Err(syn::Error::new(
683                item.span(),
684                "only named fields are supported for #[weaveffi::record]",
685            ))
686        }
687    };
688    Ok(StructDef {
689        name,
690        doc: extract_doc(&item.attrs),
691        deprecated: parse_deprecated(&item.attrs).1,
692        fields,
693    })
694}
695
696/// Extract the named fields of one rich (algebraic) enum variant into the IR's
697/// [`StructField`] list, the same shape a record's fields take.
698fn extract_variant_fields(fields: &syn::FieldsNamed) -> syn::Result<Vec<StructField>> {
699    fields
700        .named
701        .iter()
702        .map(|f| {
703            let field_name = f
704                .ident
705                .as_ref()
706                .ok_or_else(|| syn::Error::new(f.span(), "unnamed field in enum variant"))?
707                .to_string();
708            Ok(StructField {
709                name: field_name,
710                ty: type_ref_from_syn(&f.ty)?,
711                doc: extract_doc(&f.attrs),
712            })
713        })
714        .collect()
715}
716
717fn extract_enum(item: &syn::ItemEnum) -> syn::Result<EnumDef> {
718    let name = item.ident.to_string();
719
720    // A *rich* (algebraic) enum has at least one variant carrying data. Rust
721    // forbids explicit discriminants on such an enum, so its tags are the
722    // declaration-order positions (0, 1, 2, …) - exactly what the IDL records.
723    // A *C-style* enum (every variant fieldless) keeps the stricter contract:
724    // it must be `#[repr(i32)]` with an explicit discriminant on each variant.
725    let is_rich = item
726        .variants
727        .iter()
728        .any(|v| !matches!(v.fields, syn::Fields::Unit));
729
730    if !is_rich && !has_repr_i32(&item.attrs) {
731        return Err(syn::Error::new(
732            item.span(),
733            format!(
734                "enum `{}` must have #[repr(i32)] to be a #[weaveffi::enumeration]",
735                item.ident
736            ),
737        ));
738    }
739
740    let mut next_value: i32 = 0;
741    let variants = item
742        .variants
743        .iter()
744        .map(|v| {
745            let value = match v.discriminant.as_ref() {
746                Some((_, expr)) => parse_discriminant(expr)?,
747                None if is_rich => next_value,
748                None => {
749                    return Err(syn::Error::new(
750                        v.span(),
751                        format!(
752                            "enum `{name}` variant `{}` must have an explicit discriminant",
753                            v.ident
754                        ),
755                    ))
756                }
757            };
758            next_value = value.wrapping_add(1);
759
760            let fields = match &v.fields {
761                syn::Fields::Unit => vec![],
762                syn::Fields::Named(named) => extract_variant_fields(named)?,
763                syn::Fields::Unnamed(_) => {
764                    return Err(syn::Error::new(
765                        v.span(),
766                        format!(
767                            "enum `{name}` variant `{}`: tuple-style variants are not supported; \
768                             use named fields",
769                            v.ident
770                        ),
771                    ))
772                }
773            };
774
775            Ok(EnumVariant {
776                name: v.ident.to_string(),
777                value,
778                doc: extract_doc(&v.attrs),
779                fields,
780            })
781        })
782        .collect::<syn::Result<_>>()?;
783    Ok(EnumDef {
784        name,
785        doc: extract_doc(&item.attrs),
786        deprecated: parse_deprecated(&item.attrs).1,
787        variants,
788    })
789}
790
791/// Extract a `#[weaveffi::error]` enum into the module's [`ErrorDomain`].
792///
793/// Every variant needs an explicit integer discriminant: the code's stable
794/// ABI value. A variant may be a unit variant or carry named fields, which
795/// become the code's structured payload (serialized in the value-buffer
796/// format alongside the `(code, message)` pair). Note that Rust requires a
797/// primitive representation such as `#[repr(i32)]` on an enum that mixes
798/// explicit discriminants with data-carrying variants. A variant's doc
799/// comment becomes the code's default message (falling back to the variant
800/// name). The enum's name is the domain name, and the macro generates a
801/// matching `ErrorReport` implementation so `Err(KvError::...)` reports its
802/// declared code and payload.
803fn extract_error_domain(item: &syn::ItemEnum) -> syn::Result<ErrorDomain> {
804    let codes = item
805        .variants
806        .iter()
807        .map(|v| {
808            let fields = match &v.fields {
809                syn::Fields::Unit => vec![],
810                syn::Fields::Named(named) => extract_variant_fields(named)?,
811                syn::Fields::Unnamed(_) => {
812                    return Err(syn::Error::new(
813                        v.span(),
814                        "weaveffi: #[weaveffi::error] payload variants must use named \
815                         fields (tuple-style variants are not supported)",
816                    ));
817                }
818            };
819            let Some((_, expr)) = v.discriminant.as_ref() else {
820                return Err(syn::Error::new(
821                    v.span(),
822                    format!(
823                        "weaveffi: error variant `{}` must have an explicit discriminant \
824                         (its stable ABI error code)",
825                        v.ident
826                    ),
827                ));
828            };
829            let doc = extract_doc(&v.attrs);
830            let message = doc
831                .as_deref()
832                .and_then(|d| d.lines().next())
833                .unwrap_or(&v.ident.to_string())
834                .to_string();
835            Ok(ErrorCode {
836                name: v.ident.to_string(),
837                code: parse_discriminant(expr)?,
838                message,
839                doc,
840                fields,
841            })
842        })
843        .collect::<syn::Result<Vec<_>>>()?;
844    Ok(ErrorDomain {
845        name: item.ident.to_string(),
846        codes,
847    })
848}
849
850/// Extract a single [`Module`] from a `#[weaveffi::module]`-annotated `mod`.
851///
852/// Only items carrying a WeaveFFI marker are exported; everything else (private
853/// helpers, `use` items, free functions without `#[weaveffi::export]`) is
854/// ignored, so a module can freely mix exported surface and implementation.
855///
856/// # Errors
857///
858/// Returns a spanned error when an annotated item cannot be mapped to the IR
859/// (an unsupported type, an enum without `#[repr(i32)]`, a callback interface
860/// method without `&self`, and so on).
861pub fn module_from_item_mod(item_mod: &syn::ItemMod) -> syn::Result<Module> {
862    let name = item_mod.ident.to_string();
863    let mut functions = Vec::new();
864    let mut interfaces: Vec<InterfaceDef> = Vec::new();
865    let mut structs = Vec::new();
866    let mut enums = Vec::new();
867    let mut callback_interfaces = Vec::new();
868    let mut modules = Vec::new();
869    let mut errors: Option<ErrorDomain> = None;
870
871    if let Some((_, items)) = &item_mod.content {
872        for item in items {
873            match item {
874                syn::Item::Trait(t) if has_marker(&t.attrs, "callback_interface") => {
875                    callback_interfaces.push(extract_callback_interface(t)?);
876                }
877                syn::Item::Fn(f) if has_marker(&f.attrs, "export") => {
878                    functions.push(extract_function(f)?);
879                }
880                syn::Item::Struct(s) if has_marker(&s.attrs, "interface") => {
881                    interfaces.push(InterfaceDef {
882                        name: s.ident.to_string(),
883                        doc: extract_doc(&s.attrs),
884                        deprecated: parse_deprecated(&s.attrs).1,
885                        constructors: vec![],
886                        methods: vec![],
887                        statics: vec![],
888                    });
889                }
890                syn::Item::Struct(s) if has_marker(&s.attrs, "record") => {
891                    structs.push(extract_struct(s)?);
892                }
893                syn::Item::Enum(e) if has_marker(&e.attrs, "error") => {
894                    if errors.is_some() {
895                        return Err(syn::Error::new(
896                            e.span(),
897                            "weaveffi: a module may declare at most one #[weaveffi::error] \
898                             domain",
899                        ));
900                    }
901                    errors = Some(extract_error_domain(e)?);
902                }
903                syn::Item::Enum(e) if has_marker(&e.attrs, "enumeration") => {
904                    enums.push(extract_enum(e)?);
905                }
906                syn::Item::Mod(m) if has_marker(&m.attrs, "module") && m.content.is_some() => {
907                    modules.push(module_from_item_mod(m)?);
908                }
909                _ => {}
910            }
911        }
912
913        // Second pass: attach `impl` block members to their interfaces. The
914        // interface struct may be declared after its impl block, so members
915        // are collected only once every interface name is known.
916        for item in items {
917            let syn::Item::Impl(item_impl) = item else {
918                continue;
919            };
920            if item_impl.trait_.is_some() {
921                continue;
922            }
923            let Some(self_name) = type_path_ident(&item_impl.self_ty) else {
924                continue;
925            };
926            if let Some(iface) = interfaces.iter_mut().find(|i| i.name == self_name) {
927                members_from_impl(item_impl, iface)?;
928            }
929        }
930    }
931
932    Ok(Module {
933        name,
934        doc: extract_doc(&item_mod.attrs),
935        functions,
936        interfaces,
937        structs,
938        enums,
939        callback_interfaces,
940        errors,
941        modules,
942    })
943}
944
945/// Extract a full [`Api`] from a parsed Rust [`syn::File`], collecting every
946/// top-level `#[weaveffi::module]`.
947///
948/// # Errors
949///
950/// Propagates any error from [`module_from_item_mod`].
951pub fn api_from_file(file: &syn::File) -> syn::Result<Api> {
952    let mut modules = Vec::new();
953    for item in &file.items {
954        if let syn::Item::Mod(item_mod) = item {
955            if has_marker(&item_mod.attrs, "module") && item_mod.content.is_some() {
956                modules.push(module_from_item_mod(item_mod)?);
957            }
958        }
959    }
960    Ok(Api {
961        version: CURRENT_SCHEMA_VERSION.to_string(),
962        modules,
963    })
964}
965
966/// Parse Rust source text and extract its [`Api`].
967///
968/// # Errors
969///
970/// Returns a (line/column aware) error when the source does not parse as Rust,
971/// or when an annotated item cannot be mapped to the IR.
972pub fn api_from_src(src: &str) -> syn::Result<Api> {
973    let file = syn::parse_file(src)?;
974    api_from_file(&file)
975}
976
977/// A convenience wrapper around [`api_from_src`] that renders any error to a
978/// plain string, for non-macro callers (e.g. the CLI).
979///
980/// # Errors
981///
982/// Returns the formatted error message when extraction fails.
983pub fn api_from_src_stringly(src: &str) -> Result<Api, String> {
984    api_from_src(src).map_err(|e| e.to_string())
985}
986
987#[cfg(test)]
988mod tests {
989    use super::*;
990
991    fn one_module(src: &str) -> Module {
992        let api = api_from_src(src).unwrap();
993        assert_eq!(api.modules.len(), 1, "expected exactly one module");
994        api.modules.into_iter().next().unwrap()
995    }
996
997    #[test]
998    fn extracts_exported_function() {
999        let m = one_module(
1000            r#"
1001            #[weaveffi::module]
1002            mod math {
1003                #[weaveffi::export]
1004                pub fn add(a: i32, b: i32) -> i32 { a + b }
1005            }
1006        "#,
1007        );
1008        assert_eq!(m.name, "math");
1009        assert_eq!(m.functions.len(), 1);
1010        let f = &m.functions[0];
1011        assert_eq!(f.name, "add");
1012        assert_eq!(f.params[0].ty, TypeRef::I32);
1013        assert_eq!(f.returns, Some(TypeRef::I32));
1014        assert!(!f.r#async);
1015    }
1016
1017    #[test]
1018    fn unmarked_module_is_ignored() {
1019        let api = api_from_src(
1020            r#"
1021            mod plain {
1022                #[weaveffi::export]
1023                pub fn add(a: i32) -> i32 { a }
1024            }
1025        "#,
1026        )
1027        .unwrap();
1028        assert!(api.modules.is_empty());
1029    }
1030
1031    #[test]
1032    fn result_return_peels_to_ok_type() {
1033        let m = one_module(
1034            r#"
1035            #[weaveffi::module]
1036            mod m {
1037                #[weaveffi::export]
1038                pub fn get(id: u64) -> Result<Contact, MyError> { todo!() }
1039            }
1040        "#,
1041        );
1042        assert_eq!(
1043            m.functions[0].returns,
1044            Some(TypeRef::Named("Contact".into()))
1045        );
1046        assert_eq!(m.functions[0].params[0].ty, TypeRef::U64);
1047    }
1048
1049    #[test]
1050    fn result_unit_return_is_none() {
1051        let m = one_module(
1052            r#"
1053            #[weaveffi::module]
1054            mod m {
1055                #[weaveffi::export]
1056                pub fn run() -> Result<(), MyError> { Ok(()) }
1057            }
1058        "#,
1059        );
1060        assert_eq!(m.functions[0].returns, None);
1061    }
1062
1063    #[test]
1064    fn async_fn_is_async() {
1065        let m = one_module(
1066            r#"
1067            #[weaveffi::module]
1068            mod m {
1069                #[weaveffi::export]
1070                pub async fn fetch(url: String) -> String { url }
1071            }
1072        "#,
1073        );
1074        assert!(m.functions[0].r#async);
1075    }
1076
1077    #[test]
1078    fn record_and_enumeration() {
1079        let m = one_module(
1080            r#"
1081            #[weaveffi::module]
1082            mod contacts {
1083                #[weaveffi::enumeration]
1084                #[repr(i32)]
1085                pub enum ContactType { Personal = 0, Work = 1 }
1086
1087                #[weaveffi::record]
1088                pub struct Contact {
1089                    pub id: i64,
1090                    pub email: Option<String>,
1091                    pub kind: ContactType,
1092                }
1093            }
1094        "#,
1095        );
1096        assert_eq!(m.enums.len(), 1);
1097        assert_eq!(m.enums[0].variants.len(), 2);
1098        assert_eq!(m.structs.len(), 1);
1099        let s = &m.structs[0];
1100        assert_eq!(
1101            s.fields[1].ty,
1102            TypeRef::Optional(Box::new(TypeRef::StringUtf8))
1103        );
1104        assert_eq!(s.fields[2].ty, TypeRef::Named("ContactType".into()));
1105    }
1106
1107    #[test]
1108    fn enumeration_requires_repr_i32() {
1109        let err = api_from_src(
1110            r#"
1111            #[weaveffi::module]
1112            mod m {
1113                #[weaveffi::enumeration]
1114                pub enum Bad { A = 0 }
1115            }
1116        "#,
1117        )
1118        .unwrap_err();
1119        assert!(err.to_string().contains("repr(i32)"));
1120    }
1121
1122    #[test]
1123    fn references_and_arcs_name_the_pointee() {
1124        let m = one_module(
1125            r#"
1126            #[weaveffi::module]
1127            mod m {
1128                #[weaveffi::export]
1129                pub fn open(name: &str, data: &[u8], tags: &[String]) -> Arc<Store> { todo!() }
1130                #[weaveffi::export]
1131                pub fn peek(store: &Store, other: Option<Arc<Store>>) -> Option<Arc<Store>> { None }
1132            }
1133        "#,
1134        );
1135        let open = &m.functions[0];
1136        assert_eq!(open.params[0].ty, TypeRef::StringUtf8);
1137        assert_eq!(open.params[1].ty, TypeRef::Bytes);
1138        assert_eq!(
1139            open.params[2].ty,
1140            TypeRef::List(Box::new(TypeRef::StringUtf8))
1141        );
1142        assert_eq!(open.returns, Some(TypeRef::Named("Store".into())));
1143        let peek = &m.functions[1];
1144        assert_eq!(peek.params[0].ty, TypeRef::Named("Store".into()));
1145        let opt_store = TypeRef::Optional(Box::new(TypeRef::Named("Store".into())));
1146        assert_eq!(peek.params[1].ty, opt_store);
1147        assert_eq!(peek.returns, Some(opt_store));
1148    }
1149
1150    #[test]
1151    fn raw_pointers_and_boxes_are_rejected() {
1152        let err = api_from_src(
1153            r#"
1154            #[weaveffi::module]
1155            mod m {
1156                #[weaveffi::export]
1157                pub fn close(token: *mut Token) {}
1158            }
1159        "#,
1160        )
1161        .unwrap_err();
1162        assert!(err.to_string().contains("raw pointers"));
1163        let err = api_from_src(
1164            r#"
1165            #[weaveffi::module]
1166            mod m {
1167                #[weaveffi::export]
1168                pub fn listen(l: Box<dyn Listener>) {}
1169            }
1170        "#,
1171        )
1172        .unwrap_err();
1173        assert!(err.to_string().contains("Arc<dyn Trait>"));
1174        let err = api_from_src(
1175            r#"
1176            #[weaveffi::module]
1177            mod m {
1178                #[weaveffi::export]
1179                pub fn bump(counter: &mut i32) {}
1180            }
1181        "#,
1182        )
1183        .unwrap_err();
1184        assert!(err.to_string().contains("&mut"));
1185    }
1186
1187    #[test]
1188    fn cancel_token_param_is_skipped() {
1189        let m = one_module(
1190            r#"
1191            #[weaveffi::module]
1192            mod m {
1193                #[weaveffi::export]
1194                #[weaveffi::cancellable]
1195                pub async fn compact(store: Arc<Store>, cancel: weaveffi::CancelToken) -> i64 { 0 }
1196            }
1197        "#,
1198        );
1199        let f = &m.functions[0];
1200        assert!(f.r#async);
1201        assert!(f.cancellable);
1202        // Only `store` survives; the `CancelToken` is dropped from the IDL.
1203        assert_eq!(f.params.len(), 1);
1204        assert_eq!(f.params[0].name, "store");
1205        assert_eq!(f.params[0].ty, TypeRef::Named("Store".into()));
1206    }
1207
1208    #[test]
1209    fn interface_members_and_receivers() {
1210        let m = one_module(
1211            r#"
1212            #[weaveffi::module]
1213            mod m {
1214                #[weaveffi::interface]
1215                pub struct Store;
1216                impl Store {
1217                    pub fn open(path: String) -> Result<Arc<Self>, StoreError> { todo!() }
1218                    pub fn new() -> Self { Store }
1219                    pub fn len(&self) -> u64 { 0 }
1220                    pub fn share(self: Arc<Self>) -> Arc<Self> { self }
1221                    pub fn default_path() -> String { String::new() }
1222                    fn private(&self) {}
1223                }
1224            }
1225        "#,
1226        );
1227        let s = &m.interfaces[0];
1228        assert_eq!(s.constructors.len(), 2);
1229        assert!(s.constructors[0].throws);
1230        assert_eq!(s.constructors[0].returns, None);
1231        assert_eq!(s.methods.len(), 2);
1232        assert_eq!(s.methods[1].returns, Some(TypeRef::Named("Store".into())));
1233        assert_eq!(s.statics.len(), 1);
1234    }
1235
1236    #[test]
1237    fn mut_self_receiver_is_rejected() {
1238        let err = api_from_src(
1239            r#"
1240            #[weaveffi::module]
1241            mod m {
1242                #[weaveffi::interface]
1243                pub struct Store;
1244                impl Store {
1245                    pub fn bump(&mut self) {}
1246                }
1247            }
1248        "#,
1249        )
1250        .unwrap_err();
1251        assert!(err.to_string().contains("&self"));
1252    }
1253
1254    #[test]
1255    fn callback_interface_trait() {
1256        let m = one_module(
1257            r#"
1258            #[weaveffi::module]
1259            mod m {
1260                /// Receives messages.
1261                #[weaveffi::callback_interface]
1262                pub trait Listener: Send + Sync {
1263                    fn on_message(&self, text: String, meta: Option<Meta>);
1264                    fn level(&self) -> i32;
1265                }
1266                #[weaveffi::export]
1267                pub fn subscribe(listener: Arc<dyn Listener>) {}
1268                #[weaveffi::export]
1269                pub fn subscribe_bounded(listener: Arc<dyn Listener + Send + Sync>) {}
1270            }
1271        "#,
1272        );
1273        assert_eq!(m.callback_interfaces.len(), 1);
1274        let cb = &m.callback_interfaces[0];
1275        assert_eq!(cb.name, "Listener");
1276        assert_eq!(cb.doc.as_deref(), Some("Receives messages."));
1277        assert_eq!(cb.methods.len(), 2);
1278        assert_eq!(cb.methods[0].params.len(), 2);
1279        assert_eq!(cb.methods[1].returns, Some(TypeRef::I32));
1280        assert_eq!(
1281            m.functions[0].params[0].ty,
1282            TypeRef::Named("Listener".into())
1283        );
1284        assert_eq!(
1285            m.functions[1].params[0].ty,
1286            TypeRef::Named("Listener".into())
1287        );
1288    }
1289
1290    #[test]
1291    fn callback_methods_may_return_foreign_results() {
1292        let m = one_module(
1293            r#"
1294            #[weaveffi::module]
1295            mod m {
1296                #[weaveffi::callback_interface]
1297                pub trait Listener: Send + Sync {
1298                    fn level(&self) -> Result<i32, weaveffi::ForeignError>;
1299                    fn ping(&self) -> Result<(), ForeignError>;
1300                    fn strict(&self) -> Result<i32, String>;
1301                }
1302            }
1303        "#,
1304        );
1305        let methods = &m.callback_interfaces[0].methods;
1306        assert_eq!(methods[0].returns, Some(TypeRef::I32));
1307        assert!(!methods[0].throws);
1308        assert_eq!(methods[1].returns, None);
1309        assert!(!methods[1].throws);
1310        assert!(methods[2].throws, "other error types still throw");
1311    }
1312
1313    #[test]
1314    fn callback_interface_methods_need_self() {
1315        let err = api_from_src(
1316            r#"
1317            #[weaveffi::module]
1318            mod m {
1319                #[weaveffi::callback_interface]
1320                pub trait Listener {
1321                    fn make() -> i32;
1322                }
1323            }
1324        "#,
1325        )
1326        .unwrap_err();
1327        assert!(err.to_string().contains("&self"));
1328    }
1329
1330    #[test]
1331    fn nested_modules() {
1332        let m = one_module(
1333            r#"
1334            #[weaveffi::module]
1335            mod outer {
1336                #[weaveffi::export]
1337                pub fn top() -> i32 { 0 }
1338                #[weaveffi::module]
1339                mod inner {
1340                    #[weaveffi::export]
1341                    pub fn deep(x: bool) -> bool { x }
1342                }
1343            }
1344        "#,
1345        );
1346        assert_eq!(m.modules.len(), 1);
1347        assert_eq!(m.modules[0].name, "inner");
1348        assert_eq!(m.modules[0].functions[0].name, "deep");
1349    }
1350}