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}