Skip to main content

weaveffi_core/
plan.rs

1//! The **marshalling plan**: the language-neutral calling contracts every
2//! backend renders, stated once.
3//!
4//! The [`crate::model`] layer answers *which symbols exist and what their C
5//! signatures are*. This module answers the questions one level up, the ones
6//! the eleven generators used to answer independently (and inconsistently):
7//!
8//! * **Errors** ([`ErrorStrategy`]): when a call reports through `out_err`,
9//!   is that a typed domain error the caller can catch, or a producer bug the
10//!   wrapper must trap on?
11//! * **Ownership** ([`ReturnFree`], [`ElemFree`]): after copying a returned
12//!   value into a native one, exactly which runtime release call does the
13//!   wrapper owe, if any?
14//! * **Iterators** ([`IteratorProtocol`]): the pull contract of `iter<T>`,
15//!   including the requirement that wrappers stay **lazy** (one producer
16//!   `next` per consumer step, never a hidden drain into a list).
17//! * **Async** ([`AsyncProtocol`]): the completion-callback contract,
18//!   including the rule that result buffers are borrowed for the callback's
19//!   duration and must be copied before it returns.
20//!
21//! A backend that renders these plans in its own syntax cannot drift from the
22//! others on semantics; only the spelling differs.
23
24use weaveffi_ir::ir::TypeRef;
25
26use crate::abi::lower::split_qualified;
27use crate::model::{AsyncBinding, FnBinding, IteratorBinding};
28
29/// How a callable's `out_err` slot is interpreted by idiomatic wrappers.
30///
31/// Every synchronous C ABI entry point carries a trailing `out_err`, and every
32/// async completion callback carries an `err` slot, regardless of `throws`.
33/// What differs is the *meaning* of a non-zero code, and every backend must
34/// agree on it.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum ErrorStrategy {
37    /// The function declares `throws: true`: a non-zero code is a typed
38    /// domain error. The wrapper maps the code onto the module's error
39    /// domain (an exception subclass, a Swift `Error` enum case, a Go
40    /// `error` value, ...) and surfaces it through the target's normal
41    /// error channel so callers can catch and match on it.
42    Throws,
43    /// The function does not throw: the only way `out_err` reports failure
44    /// is a producer bug (most commonly a caught panic, code `-2`). The
45    /// wrapper surfaces it through the target's *programming-error* idiom
46    /// (a Python `WeaveFFIError`, a Go `panic`, a Swift `fatalError`, a C#
47    /// exception). It must never be silently ignored, and it must never be
48    /// dressed up as a typed domain error.
49    Trap,
50}
51
52impl FnBinding {
53    /// The error strategy of this callable: [`ErrorStrategy::Throws`] when the
54    /// IDL declares `throws: true`, otherwise [`ErrorStrategy::Trap`].
55    pub fn error_strategy(&self) -> ErrorStrategy {
56        if self.throws {
57            ErrorStrategy::Throws
58        } else {
59            ErrorStrategy::Trap
60        }
61    }
62}
63
64/// The release call a consumer wrapper owes for one *element* slot it copied
65/// out of an array, a map buffer, or an iterator `next` slot.
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub enum ElemFree {
68    /// By-value element (scalar, bool, C-style enum, handle): nothing to free.
69    None,
70    /// A `const char*` element: release with `{runtime}_free_string`.
71    String,
72    /// An opaque object pointer element (record or rich enum): the consumer
73    /// receives ownership and releases it with the type's `_destroy` symbol.
74    Object {
75        /// The `{prefix}_{module}_{Name}_destroy` symbol to call.
76        destroy_symbol: String,
77    },
78}
79
80/// The release call(s) a consumer wrapper owes after copying a *returned*
81/// value into a native one.
82///
83/// This is the single statement of the ownership contract the producer runtime
84/// implements (`weaveffi-abi`'s `lower_*` helpers): strings via
85/// `{runtime}_free_string`, buffers via `{runtime}_free_bytes`, opaque objects
86/// via their `_destroy` symbol. A backend renders these as its disposal calls
87/// (or wraps the object and defers the release to its finalizer idiom).
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum ReturnFree {
90    /// By-value return: nothing to free.
91    None,
92    /// `const char*`: copy, then `{runtime}_free_string(ptr)`.
93    String,
94    /// `const uint8_t* + out_len`: copy, then
95    /// `{runtime}_free_bytes(ptr, len)`.
96    Bytes,
97    /// A boxed optional scalar (`T*`, null = none): dereference, then
98    /// `{runtime}_free_bytes(ptr, sizeof(T))`.
99    BoxedScalar,
100    /// An array return (`T* + out_len`): free each element per `elem`, then
101    /// release the array itself with
102    /// `{runtime}_free_bytes(ptr, len * sizeof(T))`.
103    Array {
104        /// The per-element release owed before freeing the array buffer.
105        elem: ElemFree,
106    },
107    /// A map return (parallel `out_keys`/`out_values`/`out_len` buffers):
108    /// free each key and value per the element plans, then release both
109    /// parallel arrays with `{runtime}_free_bytes`.
110    MapBuffers {
111        /// The per-key release owed.
112        key: ElemFree,
113        /// The per-value release owed.
114        value: ElemFree,
115    },
116    /// An owned opaque object (record, rich enum, or interface return): the
117    /// caller owns the reference and eventually calls `destroy_symbol`.
118    /// Wrappers adopt the pointer into their disposal idiom (RAII, `__del__`,
119    /// finalizers, `close()`), rather than freeing eagerly.
120    OwnedObject {
121        /// The `{prefix}_{module}_{Name}_destroy` symbol to call.
122        destroy_symbol: String,
123    },
124}
125
126/// The per-element release owed for one array/map/iterator element of type
127/// `ty`, declared inside `module` under `prefix`.
128///
129/// Optionals of pointer elements share the inner element's plan (a null slot
130/// simply skips the release).
131pub fn elem_free(ty: &TypeRef, module: &str, prefix: &str) -> ElemFree {
132    match ty {
133        TypeRef::StringUtf8 | TypeRef::BorrowedStr => ElemFree::String,
134        TypeRef::Record(name) | TypeRef::RichEnum(name) => ElemFree::Object {
135            destroy_symbol: destroy_symbol(name, module, prefix),
136        },
137        TypeRef::Optional(inner) => elem_free(inner, module, prefix),
138        _ => ElemFree::None,
139    }
140}
141
142/// The release plan for a value of type `ty` *returned* from a callable
143/// declared inside `module` under `prefix`. `None` (a void return) owes
144/// nothing.
145pub fn return_free(ty: Option<&TypeRef>, module: &str, prefix: &str) -> ReturnFree {
146    let Some(ty) = ty else {
147        return ReturnFree::None;
148    };
149    match ty {
150        TypeRef::StringUtf8 | TypeRef::BorrowedStr => ReturnFree::String,
151        TypeRef::Bytes | TypeRef::BorrowedBytes => ReturnFree::Bytes,
152        TypeRef::Record(name) | TypeRef::RichEnum(name) | TypeRef::Interface(name) => {
153            ReturnFree::OwnedObject {
154                destroy_symbol: destroy_symbol(name, module, prefix),
155            }
156        }
157        TypeRef::Optional(inner) => match inner.as_ref() {
158            // Optional pointer returns reuse the inner plan; null = none.
159            t if crate::codegen::common::is_c_pointer_type(t) => {
160                return_free(Some(t), module, prefix)
161            }
162            // Optional scalar returns are boxed by the producer.
163            _ => ReturnFree::BoxedScalar,
164        },
165        TypeRef::List(inner) => ReturnFree::Array {
166            elem: elem_free(inner, module, prefix),
167        },
168        TypeRef::Map(k, v) => ReturnFree::MapBuffers {
169            key: elem_free(k, module, prefix),
170            value: elem_free(v, module, prefix),
171        },
172        // The iterator handle's lifecycle is the iterator protocol's own
173        // destroy symbol (see `IteratorProtocol`), not a buffer release.
174        TypeRef::Iterator(_) => ReturnFree::None,
175        _ => ReturnFree::None,
176    }
177}
178
179/// The `{prefix}_{module}_{Name}_destroy` symbol for a (possibly
180/// dot-qualified) object type name referenced from `current_module`.
181fn destroy_symbol(name: &str, current_module: &str, prefix: &str) -> String {
182    let (module, name) = split_qualified(name, current_module);
183    format!("{prefix}_{module}_{name}_destroy")
184}
185
186/// The `iter<T>` pull contract every backend renders.
187///
188/// The producer returns an opaque iterator handle; the consumer then calls
189/// `next` once per element and `destroy` exactly once when done. The binding
190/// contract has three clauses every wrapper must satisfy:
191///
192/// 1. **Laziness.** The wrapper exposes the target's native lazy iteration
193///    idiom (a Python iterator, a Ruby `Enumerator`, a Go `iter.Seq2`, a C#
194///    `IEnumerable`, a Dart `Iterable`, a JS iterable, a Swift `Sequence`, a
195///    Kotlin `Iterator`) and issues **one producer `next` call per consumer
196///    step**. Draining the producer into a hidden list defeats the point of
197///    `iter<T>` (constant-memory streaming) and is a contract violation.
198/// 2. **Element ownership.** Each `next` writes an element the consumer now
199///    owns; after copying it, the wrapper owes [`elem_free`](Self::elem_free).
200/// 3. **Handle lifecycle.** `destroy` is called exactly once: eagerly on
201///    exhaustion, and from the wrapper's disposal idiom (RAII destructor,
202///    finalizer, `close()`, generator cleanup) when iteration is abandoned
203///    early.
204///
205/// Each `next` call also carries `out_err` and follows the owning function's
206/// [`ErrorStrategy`].
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub struct IteratorProtocol<'a> {
209    /// The lowered iterator surface: launcher, `next`, and destroy symbols.
210    pub binding: &'a IteratorBinding,
211    /// The release owed for each element copied out of a `next` slot.
212    pub elem_free: ElemFree,
213    /// How `out_err` reports from the launcher and each `next` call are
214    /// interpreted.
215    pub error: ErrorStrategy,
216}
217
218impl IteratorBinding {
219    /// Build the full pull contract for this iterator, resolving the
220    /// per-element release plan against the declaring `module` and `prefix`.
221    pub fn protocol<'a>(
222        &'a self,
223        f: &FnBinding,
224        module: &str,
225        prefix: &str,
226    ) -> IteratorProtocol<'a> {
227        IteratorProtocol {
228            binding: self,
229            elem_free: elem_free(&self.elem, module, prefix),
230            error: f.error_strategy(),
231        }
232    }
233}
234
235/// The async completion contract every backend renders.
236///
237/// The launcher returns immediately; the producer later invokes the completion
238/// callback exactly once, from an arbitrary producer thread. The contract has
239/// three clauses:
240///
241/// 1. **Single completion.** The callback fires exactly once per launch; the
242///    wrapper resolves its native future idiom (a Python `asyncio` future, a
243///    JS `Promise`, a Swift continuation, a C# `TaskCompletionSource`, a Go
244///    channel) exactly once and then releases the registration.
245/// 2. **Borrowed results.** Result buffers passed to the callback (strings,
246///    bytes, arrays) are owned by the producer and valid **only for the
247///    callback's duration**; the wrapper must deep-copy them before the
248///    callback returns and must not free them. Owned-object results
249///    (records, rich enums, interfaces) are the exception: the callback
250///    receives ownership and adopts the pointer.
251/// 3. **Foreign-thread delivery.** The callback runs on a producer thread,
252///    so the wrapper must hop back to its native scheduler before touching
253///    consumer state (`call_soon_threadsafe`, a threadsafe function, a
254///    dispatched continuation) rather than resolving inline where the
255///    target's runtime forbids it.
256///
257/// The callback's `err` slot follows the owning function's [`ErrorStrategy`].
258/// The error struct itself is producer-owned and borrowed for the callback's
259/// duration: the wrapper copies the code and message inside the callback and
260/// the producer releases the message afterward. A wrapper may also call
261/// `error_clear` itself; the clear is idempotent (it nulls the message
262/// pointer), so the producer's own release stays safe.
263#[derive(Debug, Clone, PartialEq, Eq)]
264pub struct AsyncProtocol<'a> {
265    /// The lowered async surface: launcher and callback typedef.
266    pub binding: &'a AsyncBinding,
267    /// Whether the launcher carries a `cancel_token` slot before
268    /// `callback`/`context`.
269    pub cancellable: bool,
270    /// The release owed for an *owned-object* result adopted by the callback;
271    /// [`ReturnFree::None`] for borrowed (copy-only) results.
272    pub result_adopt: ReturnFree,
273    /// How the callback's `err` slot is interpreted.
274    pub error: ErrorStrategy,
275}
276
277impl AsyncBinding {
278    /// Build the full completion contract for this async function, resolving
279    /// the result-adoption plan against the declaring `module` and `prefix`.
280    ///
281    /// A direct or optional object result (record, rich enum, or interface,
282    /// where an optional's null slot simply means none) is adopted by the
283    /// callback; every other result shape is borrowed and copied.
284    pub fn protocol<'a>(&'a self, f: &FnBinding, module: &str, prefix: &str) -> AsyncProtocol<'a> {
285        fn adoptable(ty: &TypeRef) -> Option<&TypeRef> {
286            match ty {
287                TypeRef::Record(_) | TypeRef::RichEnum(_) | TypeRef::Interface(_) => Some(ty),
288                TypeRef::Optional(inner) => adoptable(inner),
289                _ => None,
290            }
291        }
292        let result_adopt = match f.ret.as_ref().and_then(|ty| adoptable(ty)) {
293            Some(ty) => return_free(Some(ty), module, prefix),
294            None => ReturnFree::None,
295        };
296        AsyncProtocol {
297            binding: self,
298            cancellable: f.cancellable,
299            result_adopt,
300            error: f.error_strategy(),
301        }
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    #[test]
310    fn strings_and_bytes_have_runtime_frees() {
311        assert_eq!(
312            return_free(Some(&TypeRef::StringUtf8), "m", "weaveffi"),
313            ReturnFree::String
314        );
315        assert_eq!(
316            return_free(Some(&TypeRef::Bytes), "m", "weaveffi"),
317            ReturnFree::Bytes
318        );
319        assert_eq!(return_free(None, "m", "weaveffi"), ReturnFree::None);
320    }
321
322    #[test]
323    fn object_returns_are_adopted_with_destroy_symbols() {
324        assert_eq!(
325            return_free(
326                Some(&TypeRef::Record("Contact".into())),
327                "contacts",
328                "weaveffi"
329            ),
330            ReturnFree::OwnedObject {
331                destroy_symbol: "weaveffi_contacts_Contact_destroy".into()
332            }
333        );
334        // Cross-module references resolve to the owner's symbol path.
335        assert_eq!(
336            return_free(
337                Some(&TypeRef::Interface("kv.Store".into())),
338                "kv_stats",
339                "weaveffi"
340            ),
341            ReturnFree::OwnedObject {
342                destroy_symbol: "weaveffi_kv_Store_destroy".into()
343            }
344        );
345    }
346
347    #[test]
348    fn optional_returns_split_boxed_scalar_from_pointer() {
349        assert_eq!(
350            return_free(
351                Some(&TypeRef::Optional(Box::new(TypeRef::I64))),
352                "m",
353                "weaveffi"
354            ),
355            ReturnFree::BoxedScalar
356        );
357        assert_eq!(
358            return_free(
359                Some(&TypeRef::Optional(Box::new(TypeRef::StringUtf8))),
360                "m",
361                "weaveffi"
362            ),
363            ReturnFree::String
364        );
365    }
366
367    #[test]
368    fn array_and_map_returns_carry_element_plans() {
369        assert_eq!(
370            return_free(
371                Some(&TypeRef::List(Box::new(TypeRef::StringUtf8))),
372                "m",
373                "weaveffi"
374            ),
375            ReturnFree::Array {
376                elem: ElemFree::String
377            }
378        );
379        assert_eq!(
380            return_free(
381                Some(&TypeRef::Map(
382                    Box::new(TypeRef::StringUtf8),
383                    Box::new(TypeRef::I32)
384                )),
385                "m",
386                "weaveffi"
387            ),
388            ReturnFree::MapBuffers {
389                key: ElemFree::String,
390                value: ElemFree::None
391            }
392        );
393    }
394
395    #[test]
396    fn list_of_records_frees_each_object() {
397        assert_eq!(
398            return_free(
399                Some(&TypeRef::List(Box::new(TypeRef::Record("Entry".into())))),
400                "kv",
401                "weaveffi"
402            ),
403            ReturnFree::Array {
404                elem: ElemFree::Object {
405                    destroy_symbol: "weaveffi_kv_Entry_destroy".into()
406                }
407            }
408        );
409    }
410}