Skip to main content

weaveffi_core/
capabilities.rs

1//! Per-target **feature capability declarations** and the loud-failure
2//! contract that replaces silent feature skipping.
3//!
4//! Historically a backend that did not implement an IDL feature simply
5//! omitted it from its output: Go and Ruby dropped `async` functions, nine
6//! of eleven wrappers skipped callbacks and listeners, and nothing told the
7//! user. That class of silent degradation is banned: every generator now
8//! declares a [`TargetCapabilities`] and the orchestrator refuses to run a
9//! generator against an API that uses a feature the target does not support,
10//! listing each offending declaration by path.
11//!
12//! A backend that gains a feature flips the corresponding flag and the gate
13//! opens; a backend that loses one (or a new feature lands in the IR before
14//! every backend implements it) fails generation with an actionable error
15//! instead of producing incomplete bindings.
16
17use std::collections::BTreeMap;
18use std::fmt;
19
20use weaveffi_ir::ir::{Api, Module, TypeRef};
21
22/// An IDL feature whose support varies (or could vary) per target.
23///
24/// Core types (scalars, strings, bytes, structs, enums, optionals, lists,
25/// maps, handles) are mandatory for every backend and are not gated.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
27pub enum Feature {
28    /// `async: true` functions (callback-completed launchers).
29    AsyncFunctions,
30    /// Module-level callback typedefs (`callbacks:`).
31    Callbacks,
32    /// Listener register/unregister pairs (`listeners:`).
33    Listeners,
34    /// `iter<T>` returns (opaque iterator handle + `next`/`destroy`).
35    Iterators,
36}
37
38impl Feature {
39    /// Every gated feature, for exhaustive iteration in checks and tests.
40    pub const ALL: [Feature; 4] = [
41        Feature::AsyncFunctions,
42        Feature::Callbacks,
43        Feature::Listeners,
44        Feature::Iterators,
45    ];
46
47    /// The IDL-facing name used in error messages.
48    pub fn idl_name(&self) -> &'static str {
49        match self {
50            Feature::AsyncFunctions => "async functions",
51            Feature::Callbacks => "callbacks",
52            Feature::Listeners => "listeners",
53            Feature::Iterators => "iterator returns (iter<T>)",
54        }
55    }
56}
57
58impl fmt::Display for Feature {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        f.write_str(self.idl_name())
61    }
62}
63
64/// The feature set a generator implements. Declared by every backend via
65/// [`LanguageBackend::capabilities`](crate::backend::LanguageBackend::capabilities)
66/// / [`Generator::capabilities`](crate::codegen::Generator::capabilities).
67///
68/// There is intentionally no `Default` impl: a backend must state what it
69/// supports explicitly so a new gated feature cannot be claimed by omission.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct TargetCapabilities {
72    /// Whether the target generates `async: true` functions.
73    pub async_functions: bool,
74    /// Whether the target generates module-level callback typedefs.
75    pub callbacks: bool,
76    /// Whether the target generates listener register/unregister pairs.
77    pub listeners: bool,
78    /// Whether the target generates `iter<T>` returns.
79    pub iterators: bool,
80}
81
82impl TargetCapabilities {
83    /// Full support for every gated feature. Every shipped WeaveFFI backend
84    /// declares this; partial sets exist for backends under development.
85    pub const fn full() -> Self {
86        Self {
87            async_functions: true,
88            callbacks: true,
89            listeners: true,
90            iterators: true,
91        }
92    }
93
94    /// Whether this set includes `feature`.
95    pub const fn supports(&self, feature: Feature) -> bool {
96        match feature {
97            Feature::AsyncFunctions => self.async_functions,
98            Feature::Callbacks => self.callbacks,
99            Feature::Listeners => self.listeners,
100            Feature::Iterators => self.iterators,
101        }
102    }
103}
104
105/// Every gated feature `api` uses, mapped to the locations (dotted IDL paths)
106/// that use it. Deterministic ordering for stable error output.
107pub fn used_features(api: &Api) -> BTreeMap<Feature, Vec<String>> {
108    let mut used: BTreeMap<Feature, Vec<String>> = BTreeMap::new();
109    for module in &api.modules {
110        collect_module(module, "", &mut used);
111    }
112    used
113}
114
115fn collect_module(module: &Module, parent: &str, used: &mut BTreeMap<Feature, Vec<String>>) {
116    let path = if parent.is_empty() {
117        module.name.clone()
118    } else {
119        format!("{parent}.{}", module.name)
120    };
121    for cb in &module.callbacks {
122        used.entry(Feature::Callbacks)
123            .or_default()
124            .push(format!("{path}.{}", cb.name));
125    }
126    for l in &module.listeners {
127        used.entry(Feature::Listeners)
128            .or_default()
129            .push(format!("{path}.{}", l.name));
130    }
131    for f in &module.functions {
132        let loc = format!("{path}.{}", f.name);
133        if f.r#async {
134            used.entry(Feature::AsyncFunctions)
135                .or_default()
136                .push(loc.clone());
137        }
138        if matches!(f.returns, Some(TypeRef::Iterator(_))) {
139            used.entry(Feature::Iterators).or_default().push(loc);
140        }
141    }
142    for child in &module.modules {
143        collect_module(child, &path, used);
144    }
145}
146
147/// A target was asked to generate bindings for an API that uses features it
148/// does not support. Carries every violation so the user sees the complete
149/// picture in one failure.
150#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
151pub struct UnsupportedFeatures {
152    /// The `--target` token of the failing generator.
153    pub target: String,
154    /// Each unsupported feature with the IDL paths that use it.
155    pub violations: Vec<(Feature, Vec<String>)>,
156}
157
158impl fmt::Display for UnsupportedFeatures {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        writeln!(
161            f,
162            "target '{}' does not support every feature this IDL uses:",
163            self.target
164        )?;
165        for (feature, locations) in &self.violations {
166            writeln!(f, "  - {feature} (used by: {})", locations.join(", "))?;
167        }
168        write!(
169            f,
170            "remove the unsupported declarations, drop '{}' from --target, or set \
171             `generators.{}.allow_unsupported: true` in the IDL to generate the supported \
172             surface anyway (unsupported entry points become explicit throwing stubs)",
173            self.target, self.target
174        )
175    }
176}
177
178/// Check `api` against one target's declared capabilities. `Ok(())` when the
179/// target supports every feature the API uses.
180///
181/// # Errors
182///
183/// Returns [`UnsupportedFeatures`] when `api` uses one or more gated features
184/// that `caps` does not declare support for. The error carries every offending
185/// feature paired with the IDL paths that use it, so the caller can report all
186/// violations at once.
187pub fn check(
188    api: &Api,
189    target: &str,
190    caps: &TargetCapabilities,
191) -> Result<(), UnsupportedFeatures> {
192    let violations: Vec<(Feature, Vec<String>)> = used_features(api)
193        .into_iter()
194        .filter(|(feature, _)| !caps.supports(*feature))
195        .collect();
196    if violations.is_empty() {
197        Ok(())
198    } else {
199        Err(UnsupportedFeatures {
200            target: target.to_string(),
201            violations,
202        })
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use weaveffi_ir::ir::{CallbackDef, Function, ListenerDef, Param};
210
211    fn func(name: &str, is_async: bool, returns: Option<TypeRef>) -> Function {
212        Function {
213            name: name.into(),
214            params: vec![Param {
215                name: "x".into(),
216                ty: TypeRef::I32,
217                mutable: false,
218                doc: None,
219            }],
220            returns,
221            doc: None,
222            r#async: is_async,
223            cancellable: false,
224            deprecated: None,
225            since: None,
226        }
227    }
228
229    fn module(name: &str) -> Module {
230        Module {
231            name: name.into(),
232            functions: vec![],
233            structs: vec![],
234            enums: vec![],
235            callbacks: vec![],
236            listeners: vec![],
237            errors: None,
238            modules: vec![],
239        }
240    }
241
242    fn api(modules: Vec<Module>) -> Api {
243        Api {
244            version: "0.4.0".into(),
245            modules,
246            generators: None,
247            package: None,
248        }
249    }
250
251    fn events_api() -> Api {
252        api(vec![Module {
253            callbacks: vec![CallbackDef {
254                name: "OnMessage".into(),
255                params: vec![],
256                doc: None,
257            }],
258            listeners: vec![ListenerDef {
259                name: "message_listener".into(),
260                event_callback: "OnMessage".into(),
261                doc: None,
262            }],
263            functions: vec![
264                func("send", false, None),
265                func("fetch", true, Some(TypeRef::StringUtf8)),
266                func(
267                    "all",
268                    false,
269                    Some(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))),
270                ),
271            ],
272            ..module("events")
273        }])
274    }
275
276    #[test]
277    fn full_capabilities_pass_everything() {
278        assert!(check(&events_api(), "c", &TargetCapabilities::full()).is_ok());
279    }
280
281    #[test]
282    fn plain_api_uses_no_gated_features() {
283        let plain = api(vec![Module {
284            functions: vec![func("add", false, Some(TypeRef::I32))],
285            ..module("math")
286        }]);
287        assert!(used_features(&plain).is_empty());
288    }
289
290    #[test]
291    fn used_features_collects_locations() {
292        let used = used_features(&events_api());
293        assert_eq!(
294            used[&Feature::Callbacks],
295            vec!["events.OnMessage".to_string()]
296        );
297        assert_eq!(
298            used[&Feature::Listeners],
299            vec!["events.message_listener".to_string()]
300        );
301        assert_eq!(
302            used[&Feature::AsyncFunctions],
303            vec!["events.fetch".to_string()]
304        );
305        assert_eq!(used[&Feature::Iterators], vec!["events.all".to_string()]);
306    }
307
308    #[test]
309    fn nested_modules_use_dotted_paths() {
310        let nested = api(vec![Module {
311            modules: vec![Module {
312                functions: vec![func("fetch", true, None)],
313                ..module("inner")
314            }],
315            ..module("outer")
316        }]);
317        let used = used_features(&nested);
318        assert_eq!(
319            used[&Feature::AsyncFunctions],
320            vec!["outer.inner.fetch".to_string()]
321        );
322    }
323
324    #[test]
325    fn missing_capability_is_reported_with_locations() {
326        let caps = TargetCapabilities {
327            async_functions: false,
328            listeners: false,
329            ..TargetCapabilities::full()
330        };
331        let err = check(&events_api(), "go", &caps).unwrap_err();
332        assert_eq!(err.target, "go");
333        assert_eq!(err.violations.len(), 2);
334        let msg = err.to_string();
335        assert!(msg.contains("target 'go' does not support"), "{msg}");
336        assert!(
337            msg.contains("async functions (used by: events.fetch)"),
338            "{msg}"
339        );
340        assert!(
341            msg.contains("listeners (used by: events.message_listener)"),
342            "{msg}"
343        );
344    }
345}