Skip to main content

weaveffi_model/
resolved.rs

1//! The resolved IR: a validated [`Api`] paired with the type index that turns
2//! its written [`TypeRef`]s into resolved [`Ty`]s.
3//!
4//! WeaveFFI keeps two distinct representations of an API:
5//!
6//! * the **IDL document**: the [`Api`] tree exactly as parsed from
7//!   YAML/JSON/TOML (or extracted from annotated Rust), in which every
8//!   user-type reference is a [`TypeRef::Named`] string; and
9//! * the **binding model** ([`crate::model::BindingModel`]): the lowered view
10//!   generators consume, in which every type is a [`Ty`] whose kind (record,
11//!   enum, interface) and owning module are known.
12//!
13//! [`ResolvedApi`] is the bridge. The only checked way to obtain one is
14//! [`validate_api`](crate::validate::validate_api), which proves every rule
15//! holds; [`ResolvedApi::resolve`] then maps a written reference to its
16//! resolved type against the index built from the document's declarations.
17//! The document itself is never mutated, so an IDL always round-trips.
18
19use std::collections::BTreeMap;
20
21use crate::ir::{Api, Module, TypeRef};
22
23use crate::model::Ty;
24use crate::pkg::Identity;
25
26/// What kind of declaration a bare type name refers to.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum TypeKind {
29    /// A `structs:` entry.
30    Record,
31    /// An `enums:` entry with no payload-carrying variant.
32    Enum,
33    /// An `enums:` entry with at least one payload-carrying variant.
34    RichEnum,
35    /// An `interfaces:` entry.
36    Interface,
37    /// A `callback_interfaces:` entry.
38    CallbackInterface,
39}
40
41/// Where a type is declared: the owning module's dot-joined path and its kind.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct TypeDecl {
44    /// Dot-joined path of the declaring module (e.g. `graphics.shapes`).
45    pub module_path: String,
46    /// The declaration kind.
47    pub kind: TypeKind,
48}
49
50/// A validated API whose type references can be resolved.
51///
52/// This is the input to [`BindingModel::build`](crate::model::BindingModel::build).
53/// The checked way to obtain one is
54/// [`validate_api`](crate::validate::validate_api). [`assume_valid`](Self::assume_valid)
55/// exists for the `#[weaveffi::module]` proc-macro, which expands one module
56/// tree in isolation, and for tests that build well-formed trees by hand.
57///
58/// `ResolvedApi` dereferences to [`Api`] for read access; there is no mutable
59/// access, so the index can never go stale. It also carries the library's
60/// [`Identity`], which the CLI attaches with [`with_identity`](Self::with_identity)
61/// and every generator reads names from.
62#[derive(Debug, Clone, PartialEq)]
63pub struct ResolvedApi {
64    api: Api,
65    types: BTreeMap<String, TypeDecl>,
66    identity: Identity,
67}
68
69impl ResolvedApi {
70    /// Attach the library's resolved identity.
71    #[must_use]
72    pub fn with_identity(mut self, identity: Identity) -> Self {
73        self.identity = identity;
74        self
75    }
76
77    /// The library's identity: its name, C symbol prefix, native library
78    /// name, and package metadata.
79    pub fn identity(&self) -> &Identity {
80        &self.identity
81    }
82
83    /// Wrap a document without running validation.
84    ///
85    /// Type names that resolve to no declaration become [`Ty::Record`]
86    /// references named exactly as written; [`unresolved`](Self::unresolved)
87    /// lists them. The proc-macro relies on this to expand one module tree
88    /// while a sibling tree declares some of the types it mentions, and
89    /// asserts at compile time that each such type really crosses as a value
90    /// buffer.
91    #[doc(hidden)]
92    pub fn assume_valid(api: Api) -> Self {
93        let mut types = BTreeMap::new();
94        for module in &api.modules {
95            index_module(module, "", &mut types);
96        }
97        Self {
98            api,
99            types,
100            identity: Identity::default(),
101        }
102    }
103
104    /// The underlying document.
105    pub fn api(&self) -> &Api {
106        &self.api
107    }
108
109    /// Look up the declaration a bare type name refers to.
110    pub fn declaration(&self, name: &str) -> Option<&TypeDecl> {
111        self.types.get(bare(name))
112    }
113
114    /// Every type name the document references that no declaration in it
115    /// provides, deduplicated and sorted. Always empty for an API that
116    /// passed validation.
117    pub fn unresolved(&self) -> Vec<String> {
118        let mut out = std::collections::BTreeSet::new();
119        let mut visit = |ty: &TypeRef| {
120            ty.walk(&mut |t| {
121                if let TypeRef::Named(n) = t {
122                    if self.declaration(n).is_none() {
123                        out.insert(n.clone());
124                    }
125                }
126            });
127        };
128        for_each_type_ref(&self.api.modules, &mut visit);
129        out.into_iter().collect()
130    }
131
132    /// The absolute, dot-joined name of the type a reference denotes
133    /// (`kv.Store`, `shapes.geo.Point`). Unknown names come back as written.
134    pub fn qualified_name(&self, name: &str) -> String {
135        match self.declaration(name) {
136            Some(decl) => format!("{}.{}", decl.module_path, bare(name)),
137            None => name.to_string(),
138        }
139    }
140
141    /// Map a written type reference to its resolved type. User types carry
142    /// their kind and absolute dot-joined name, so two references to one
143    /// declaration compare equal wherever they are written.
144    pub fn resolve(&self, ty: &TypeRef) -> Ty {
145        match ty {
146            TypeRef::I8 => Ty::I8,
147            TypeRef::I16 => Ty::I16,
148            TypeRef::I32 => Ty::I32,
149            TypeRef::I64 => Ty::I64,
150            TypeRef::U8 => Ty::U8,
151            TypeRef::U16 => Ty::U16,
152            TypeRef::U32 => Ty::U32,
153            TypeRef::U64 => Ty::U64,
154            TypeRef::F32 => Ty::F32,
155            TypeRef::F64 => Ty::F64,
156            TypeRef::Bool => Ty::Bool,
157            TypeRef::StringUtf8 => Ty::StringUtf8,
158            TypeRef::Bytes => Ty::Bytes,
159            TypeRef::Named(name) => {
160                let qualified = self.qualified_name(name);
161                match self.declaration(name).map(|d| d.kind) {
162                    Some(TypeKind::Enum) => Ty::Enum(qualified),
163                    Some(TypeKind::RichEnum) => Ty::RichEnum(qualified),
164                    Some(TypeKind::Interface) => Ty::Interface(qualified),
165                    Some(TypeKind::CallbackInterface) => Ty::CallbackInterface(qualified),
166                    Some(TypeKind::Record) | None => Ty::Record(qualified),
167                }
168            }
169            TypeRef::Optional(inner) => Ty::Optional(Box::new(self.resolve(inner))),
170            TypeRef::List(inner) => Ty::List(Box::new(self.resolve(inner))),
171            TypeRef::Map(k, v) => Ty::Map(Box::new(self.resolve(k)), Box::new(self.resolve(v))),
172            TypeRef::Iterator(inner) => Ty::Iterator(Box::new(self.resolve(inner))),
173        }
174    }
175}
176
177/// Call `f` on every type reference written anywhere in `modules`.
178pub(crate) fn for_each_type_ref(modules: &[Module], f: &mut dyn FnMut(&TypeRef)) {
179    fn fns(fs: &[crate::ir::Function], f: &mut dyn FnMut(&TypeRef)) {
180        for func in fs {
181            for p in &func.params {
182                f(&p.ty);
183            }
184            if let Some(r) = &func.returns {
185                f(r);
186            }
187        }
188    }
189    for m in modules {
190        fns(&m.functions, f);
191        for i in &m.interfaces {
192            fns(&i.constructors, f);
193            fns(&i.methods, f);
194            fns(&i.statics, f);
195        }
196        for c in &m.callback_interfaces {
197            fns(&c.methods, f);
198        }
199        for s in &m.structs {
200            for field in &s.fields {
201                f(&field.ty);
202            }
203        }
204        for e in &m.enums {
205            for v in &e.variants {
206                for field in &v.fields {
207                    f(&field.ty);
208                }
209            }
210        }
211        if let Some(d) = &m.errors {
212            for c in &d.codes {
213                for field in &c.fields {
214                    f(&field.ty);
215                }
216            }
217        }
218        for_each_type_ref(&m.modules, f);
219    }
220}
221
222fn bare(name: &str) -> &str {
223    name.rsplit('.').next().unwrap_or(name)
224}
225
226fn index_module(module: &Module, parent: &str, out: &mut BTreeMap<String, TypeDecl>) {
227    let path = if parent.is_empty() {
228        module.name.clone()
229    } else {
230        format!("{parent}.{}", module.name)
231    };
232    let mut add = |name: &str, kind: TypeKind| {
233        out.entry(name.to_string()).or_insert(TypeDecl {
234            module_path: path.clone(),
235            kind,
236        });
237    };
238    for s in &module.structs {
239        add(&s.name, TypeKind::Record);
240    }
241    for e in &module.enums {
242        let kind = if e.is_rich() {
243            TypeKind::RichEnum
244        } else {
245            TypeKind::Enum
246        };
247        add(&e.name, kind);
248    }
249    for i in &module.interfaces {
250        add(&i.name, TypeKind::Interface);
251    }
252    for c in &module.callback_interfaces {
253        add(&c.name, TypeKind::CallbackInterface);
254    }
255    for child in &module.modules {
256        index_module(child, &path, out);
257    }
258}
259
260impl std::ops::Deref for ResolvedApi {
261    type Target = Api;
262
263    fn deref(&self) -> &Api {
264        &self.api
265    }
266}
267
268impl AsRef<Api> for ResolvedApi {
269    fn as_ref(&self) -> &Api {
270        &self.api
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277    use crate::ir::{
278        CallbackInterfaceDef, EnumDef, EnumVariant, Function, InterfaceDef, StructDef, StructField,
279        CURRENT_SCHEMA_VERSION,
280    };
281
282    fn module(name: &str) -> Module {
283        Module {
284            name: name.into(),
285            doc: None,
286            functions: vec![],
287            interfaces: vec![],
288            callback_interfaces: vec![],
289            structs: vec![],
290            enums: vec![],
291            errors: None,
292            modules: vec![],
293        }
294    }
295
296    fn api() -> ResolvedApi {
297        let shared = Module {
298            enums: vec![
299                EnumDef {
300                    name: "Status".into(),
301                    doc: None,
302                    deprecated: None,
303                    variants: vec![EnumVariant {
304                        name: "Ok".into(),
305                        value: 0,
306                        doc: None,
307                        fields: vec![],
308                    }],
309                },
310                EnumDef {
311                    name: "Shape".into(),
312                    doc: None,
313                    deprecated: None,
314                    variants: vec![EnumVariant {
315                        name: "Circle".into(),
316                        value: 0,
317                        doc: None,
318                        fields: vec![StructField {
319                            name: "r".into(),
320                            ty: TypeRef::F64,
321                            doc: None,
322                        }],
323                    }],
324                },
325            ],
326            structs: vec![StructDef {
327                name: "Point".into(),
328                doc: None,
329                deprecated: None,
330                fields: vec![],
331            }],
332            modules: vec![Module {
333                interfaces: vec![InterfaceDef {
334                    name: "Store".into(),
335                    doc: None,
336                    deprecated: None,
337                    constructors: vec![],
338                    methods: vec![],
339                    statics: vec![],
340                }],
341                callback_interfaces: vec![CallbackInterfaceDef {
342                    name: "Watcher".into(),
343                    doc: None,
344                    deprecated: None,
345                    methods: vec![Function {
346                        name: "on_change".into(),
347                        params: vec![],
348                        returns: None,
349                        doc: None,
350                        throws: false,
351                        r#async: false,
352                        cancellable: false,
353                        deprecated: None,
354                    }],
355                }],
356                ..module("inner")
357            }],
358            ..module("shared")
359        };
360        ResolvedApi::assume_valid(Api {
361            version: CURRENT_SCHEMA_VERSION.into(),
362            modules: vec![shared, module("orders")],
363        })
364    }
365
366    #[test]
367    fn kinds_and_qualification() {
368        let api = api();
369        assert_eq!(
370            api.resolve(&TypeRef::Named("Status".into())),
371            Ty::Enum("shared.Status".into())
372        );
373        assert_eq!(
374            api.resolve(&TypeRef::Named("Status".into())),
375            Ty::Enum("shared.Status".into())
376        );
377        assert_eq!(
378            api.resolve(&TypeRef::Named("Shape".into())),
379            Ty::RichEnum("shared.Shape".into())
380        );
381        assert_eq!(
382            api.resolve(&TypeRef::Named("Store".into())),
383            Ty::Interface("shared.inner.Store".into())
384        );
385        assert_eq!(
386            api.resolve(&TypeRef::Named("Store".into())),
387            Ty::Interface("shared.inner.Store".into())
388        );
389        // Already-qualified spellings normalize to the owner's path.
390        assert_eq!(
391            api.resolve(&TypeRef::Named("shared.Point".into())),
392            Ty::Record("shared.Point".into())
393        );
394        assert_eq!(
395            api.resolve(&TypeRef::Named("Watcher".into())),
396            Ty::CallbackInterface("shared.inner.Watcher".into())
397        );
398        assert_eq!(
399            api.declaration("Watcher").unwrap().kind,
400            TypeKind::CallbackInterface
401        );
402    }
403
404    #[test]
405    fn composites_resolve_recursively() {
406        let api = api();
407        let ty = TypeRef::List(Box::new(TypeRef::Map(
408            Box::new(TypeRef::StringUtf8),
409            Box::new(TypeRef::Optional(Box::new(TypeRef::Named("Point".into())))),
410        )));
411        assert_eq!(
412            api.resolve(&ty),
413            Ty::List(Box::new(Ty::Map(
414                Box::new(Ty::StringUtf8),
415                Box::new(Ty::Optional(Box::new(Ty::Record("shared.Point".into()))))
416            )))
417        );
418    }
419
420    #[test]
421    fn unknown_names_fall_back_to_records() {
422        let api = api();
423        assert_eq!(
424            api.resolve(&TypeRef::Named("Elsewhere".into())),
425            Ty::Record("Elsewhere".into())
426        );
427        assert!(api.unresolved().is_empty());
428        assert!(api.declaration("Elsewhere").is_none());
429        assert_eq!(api.declaration("Point").unwrap().kind, TypeKind::Record);
430    }
431}