alef 0.103.0

Opinionated polyglot binding generator for Rust libraries
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
use crate::core::config::{BridgeBinding, TraitBridgeConfig};
use crate::core::ir::{ApiSurface, FieldDef, FunctionDef, ParamDef, TypeDef, TypeRef};

pub fn bridge_handle_path(api: &ApiSurface, bridge: &TraitBridgeConfig, core_import: &str) -> String {
    let alias = bridge.type_alias.as_deref().unwrap_or(&bridge.trait_name);
    api.types
        .iter()
        .find(|typ| typ.name == alias && !typ.rust_path.is_empty())
        .map(|typ| typ.rust_path.replace('-', "_"))
        .or_else(|| api.excluded_type_paths.get(alias).map(|path| path.replace('-', "_")))
        .unwrap_or_else(|| format!("{core_import}::{alias}"))
}

/// Generate a backend visitor bridge wrapper name from configuration.
pub fn bridge_wrapper_name(prefix: &str, bridge: &TraitBridgeConfig) -> String {
    format!("{}{}Bridge", prefix, bridge.trait_name)
}

/// Return true when a type reference points at any configured bridge handle alias.
pub fn is_bridge_handle_type_ref(ty: &TypeRef, bridges: &[TraitBridgeConfig]) -> bool {
    bridges
        .iter()
        .filter_map(|bridge| bridge.type_alias.as_deref())
        .any(|alias| field_type_matches_alias(ty, alias))
}

/// Return true when a function name is emitted by trait-bridge codegen.
pub fn is_trait_bridge_managed_fn(func_name: &str, bridges: &[TraitBridgeConfig]) -> bool {
    bridges.iter().any(|bridge| {
        bridge.register_fn.as_deref() == Some(func_name)
            || bridge.unregister_fn.as_deref() == Some(func_name)
            || bridge.clear_fn.as_deref() == Some(func_name)
    })
}

/// Find the first function parameter that matches a trait bridge configuration
/// (by type alias before falling back to parameter name).
///
/// Bridges configured with `bind_via = "options_field"` are skipped — they live on a
/// struct field rather than directly as a parameter, and are returned by
/// [`find_bridge_field`] instead.
pub fn find_bridge_param<'a>(
    func: &FunctionDef,
    bridges: &'a [TraitBridgeConfig],
) -> Option<(usize, &'a TraitBridgeConfig)> {
    for (idx, param) in func.params.iter().enumerate() {
        let named = match &param.ty {
            TypeRef::Named(n) => Some(n.as_str()),
            TypeRef::Optional(inner) => {
                if let TypeRef::Named(n) = inner.as_ref() {
                    Some(n.as_str())
                } else {
                    None
                }
            }
            _ => None,
        };
        for bridge in bridges {
            if bridge.bind_via != BridgeBinding::FunctionParam {
                continue;
            }
            if let Some(type_name) = named
                && bridge.type_alias.as_deref() == Some(type_name)
            {
                return Some((idx, bridge));
            }
        }
        // A generic name such as `host` may be shared by unrelated traits. Search all type
        // aliases first so configuration order cannot override an explicit type match. ~keep
        for bridge in bridges {
            if bridge.bind_via == BridgeBinding::FunctionParam
                && bridge.param_name.as_deref() == Some(param.name.as_str())
            {
                return Some((idx, bridge));
            }
        }
    }
    None
}

/// Match info for a trait bridge whose handle lives as a struct field
/// (`bind_via = "options_field"`).
#[derive(Debug, Clone)]
pub struct BridgeFieldMatch<'a> {
    /// Index of the function parameter that carries the owning struct.
    pub param_index: usize,
    /// Name of the parameter (e.g., `"options"`).
    pub param_name: String,
    /// IR type name of the parameter, with any `Option<>` wrapper unwrapped.
    pub options_type: String,
    /// True if the param is `Option<TypeName>` rather than `TypeName`.
    pub param_is_optional: bool,
    /// Name of the field on `options_type` that holds the bridge handle.
    pub field_name: String,
    /// The matching field definition (carries the field's `TypeRef`).
    pub field: &'a FieldDef,
    /// The bridge configuration that produced the match.
    pub bridge: &'a TraitBridgeConfig,
}

/// A function parameter that carries an `options_field` bridge's owning struct.
///
/// Produced by [`options_field_bridge_sites`], the one predicate every emitter asks. It
/// deliberately stops short of resolving the owning struct's *field*: that needs the
/// `ApiSurface`'s `TypeDef` list, which the `.pyi` stub, the `api.py` facade and the FFI
/// wrapper emitter do not all have, and none of them needs it.
#[derive(Debug, Clone, Copy)]
pub struct OptionsFieldBridgeSite<'a> {
    /// Index of the function parameter that carries the owning struct.
    pub param_index: usize,
    /// The parameter itself.
    pub param: &'a ParamDef,
    /// IR type name of the parameter, with any `Option<>` wrapper unwrapped.
    pub options_type: &'a str,
    /// True if the param is `Option<TypeName>` rather than `TypeName`.
    pub param_is_optional: bool,
    /// The bridge configuration that produced the match.
    pub bridge: &'a TraitBridgeConfig,
}

/// A `Named(N)` / `Optional<Named(N)>` type reference reduced to `(N, was_optional)`.
fn unwrap_named(ty: &TypeRef) -> Option<(&str, bool)> {
    match ty {
        TypeRef::Named(n) => Some((n.as_str(), false)),
        TypeRef::Optional(inner) => {
            if let TypeRef::Named(n) = inner.as_ref() {
                Some((n.as_str(), true))
            } else {
                None
            }
        }
        _ => None,
    }
}

/// Whether `bridge` attaches to the generated function named `func_name`.
///
/// `exclude_functions` names the functions that must NOT gain the bridge's extra keyword —
/// a synchronous diagnostic entry point where a listener is meaningless, for instance.
pub fn bridge_covers_function(bridge: &TraitBridgeConfig, func_name: &str) -> bool {
    !bridge.exclude_functions.iter().any(|excluded| excluded == func_name)
}

/// Every parameter of `func` that carries an `options_field` bridge's owning struct.
///
/// **This is the single predicate that answers "does this function take the bridge's config".**
/// The `.pyi` stub, the `api.py` facade, the `#[pyfunction]` wrapper and the FFI wrapper each
/// used to answer it with their own copy of the same walk; four copies meant four chances to
/// ship a stub that disagrees with the binding it describes. Emitters pass an already
/// language-filtered bridge list, so the `exclude_languages` and `exclude_functions` gates are
/// applied in exactly one place too. ~keep
pub fn options_field_bridge_sites<'a, I>(func: &'a FunctionDef, bridges: I) -> Vec<OptionsFieldBridgeSite<'a>>
where
    I: IntoIterator<Item = &'a TraitBridgeConfig>,
{
    let candidates: Vec<&'a TraitBridgeConfig> = bridges
        .into_iter()
        .filter(|bridge| bridge.bind_via == BridgeBinding::OptionsField)
        .filter(|bridge| bridge_covers_function(bridge, &func.name))
        .collect();
    if candidates.is_empty() {
        return Vec::new();
    }

    let mut sites = Vec::new();
    for (param_index, param) in func.params.iter().enumerate() {
        let Some((type_name, param_is_optional)) = unwrap_named(&param.ty) else {
            continue;
        };
        for bridge in &candidates {
            if bridge.options_type.as_deref() != Some(type_name) {
                continue;
            }
            sites.push(OptionsFieldBridgeSite {
                param_index,
                param,
                options_type: type_name,
                param_is_optional,
                bridge,
            });
        }
    }
    sites
}

/// The first parameter of `func` that carries an `options_field` bridge's owning struct.
pub fn options_field_bridge_site<'a, I>(func: &'a FunctionDef, bridges: I) -> Option<OptionsFieldBridgeSite<'a>>
where
    I: IntoIterator<Item = &'a TraitBridgeConfig>,
{
    options_field_bridge_sites(func, bridges).into_iter().next()
}

/// Find the first function parameter whose IR type carries a bridge field
/// (`bind_via = "options_field"`), resolving the owning struct's field definition.
///
/// Layered on [`options_field_bridge_sites`] so it cannot disagree with the other emitters
/// about *which functions* carry a bridge; it adds only the stricter requirement that the
/// owning struct and its bridge field are actually present in `types`. When a site matches but
/// its field does not resolve, that is logged rather than passed over silently — it is exactly
/// the shape where the Rust wrapper and the host-language stub drift apart. ~keep
///
/// Bridges configured with `bind_via = "function_param"` are skipped — those go
/// through [`find_bridge_param`] instead.
pub fn find_bridge_field<'a>(
    func: &'a FunctionDef,
    types: &'a [TypeDef],
    bridges: &'a [TraitBridgeConfig],
) -> Option<BridgeFieldMatch<'a>> {
    for site in options_field_bridge_sites(func, bridges) {
        let Some(type_def) = types.iter().find(|t| t.name == site.options_type) else {
            tracing::debug!(
                function = %func.name,
                options_type = %site.options_type,
                "options-field bridge skipped: the owning struct is absent from the API surface"
            );
            continue;
        };
        let field_name = site.bridge.resolved_options_field();
        let matched = type_def.fields.iter().find(|field| {
            field_name.is_some_and(|n| field.name == n)
                || site
                    .bridge
                    .type_alias
                    .as_deref()
                    .is_some_and(|alias| field_type_matches_alias(&field.ty, alias))
        });
        let Some(field) = matched else {
            tracing::warn!(
                function = %func.name,
                options_type = %site.options_type,
                trait_name = %site.bridge.trait_name,
                "options-field bridge skipped: the configured field is absent from the owning struct"
            );
            continue;
        };
        return Some(BridgeFieldMatch {
            param_index: site.param_index,
            param_name: site.param.name.clone(),
            options_type: site.options_type.to_string(),
            param_is_optional: site.param_is_optional,
            field_name: field.name.clone(),
            field,
            bridge: site.bridge,
        });
    }
    None
}

/// Names of Clone opaque types represented by read-only native Arc wrappers.
pub fn native_marshalled_opaque_params(api: &ApiSurface) -> std::collections::HashSet<String> {
    // Read-only Arc wrappers can carry Clone handles without a serde round trip.
    // Mutex-backed, excluded, lifetime-bearing and trait wrappers need different conversions.
    api.types
        .iter()
        .filter(|t| {
            t.is_opaque
                && t.is_clone
                && !t.is_trait
                && !t.binding_excluded
                && !t.has_lifetime_params
                && !crate::codegen::generators::type_needs_mutex(t)
        })
        .map(|t| t.name.clone())
        .collect()
}

/// Decide whether a trait-callback parameter type should be marshalled to the host as the
/// binding's NATIVE object (constructed through the binding's Rust→host conversion) rather
/// than as a serialized string.
///
/// This is the single, backend-agnostic classification rule for trait-bridge callback params.
/// Backends consult it (typically by seeding a per-generator struct-param allowlist with
/// [`native_marshalled_struct_params`]) to choose between handing the host a native object vs.
/// the prior string representation. Keeping the rule here — not in any one backend — lets every
/// backend share the same notion of "this param is a struct the host should receive as a native
/// value".
///
/// A type qualifies only when it is a **known serde struct**:
/// - present in `api.types` (so the binding actually emits a representation for it),
/// - a struct, i.e. NOT a trait (`!is_trait`) and NOT opaque/handle (`!is_opaque`),
/// - derives serde (`has_serde`), so the binding's value conversion is well-defined,
/// - not excluded from the binding surface (`!binding_excluded`).
///
/// Enums (which live in `api.enums`, never `api.types`), opaque/handle types, excluded types,
/// and unknown `Named` types all fail this test and are left to their prior representation.
pub fn is_native_marshalled_struct(type_name: &str, api: &ApiSurface) -> bool {
    api.types
        .iter()
        .any(|t| t.name == type_name && !t.is_trait && !t.is_opaque && t.has_serde && !t.binding_excluded)
}

/// Collect the names of all trait-callback parameter types across `trait_def`'s methods that
/// qualify for native-object marshalling per [`is_native_marshalled_struct`].
///
/// The result seeds each backend generator's struct-param allowlist; generators look a param's
/// `Named` type up in that set to decide its marshalling, so the positive allowlist is computed
/// once, in shared code, and reused identically by every backend.
pub fn native_marshalled_struct_params(trait_def: &TypeDef, api: &ApiSurface) -> std::collections::HashSet<String> {
    fn leaf_named(ty: &TypeRef) -> Option<&str> {
        match ty {
            TypeRef::Named(n) => Some(n.as_str()),
            TypeRef::Optional(inner) | TypeRef::Vec(inner) => leaf_named(inner),
            _ => None,
        }
    }

    let mut out = std::collections::HashSet::new();
    for method in &trait_def.methods {
        for param in &method.params {
            if let Some(name) = leaf_named(&param.ty)
                && is_native_marshalled_struct(name, api)
            {
                out.insert(name.to_string());
            }
        }
    }
    out
}

/// Collect the names of native-marshalled structs that appear as a callback's RETURN type.
///
/// Restricted to a bare `Named` return: the native fast-path extracts the binding's native object
/// and converts it via `From<core::T>`, which is well-defined only for a direct struct return.
/// `Optional`/`Vec`/other return shapes keep the existing mapping/JSON path. Return-side
/// counterpart to [`native_marshalled_struct_params`] — a host may return the binding's native
/// result object instead of an untyped mapping, and the bridge accepts it (falling back to the
/// mapping path otherwise).
pub fn native_marshalled_struct_returns(trait_def: &TypeDef, api: &ApiSurface) -> std::collections::HashSet<String> {
    let mut out = std::collections::HashSet::new();
    for method in &trait_def.methods {
        if let TypeRef::Named(name) = &method.return_type
            && is_native_marshalled_struct(name, api)
        {
            out.insert(name.clone());
        }
    }
    out
}

/// Look up the trait `TypeDef` a bridge wraps, by the bridge's configured `trait_name`.
///
/// Trait definitions live in `api.types` with `is_trait == true`. Backends use this to recover the
/// trait's methods (params + return types) when emitting a typed, host-implementable surface for a
/// plugin bridge — e.g. an Elixir behaviour or a Python Protocol. Returns `None` when the trait is
/// not present in `api.types` (e.g. fully excluded from the binding surface).
pub fn find_trait_def<'a>(bridge: &TraitBridgeConfig, api: &'a ApiSurface) -> Option<&'a TypeDef> {
    api.types
        .iter()
        .find(|typ| typ.is_trait && typ.name == bridge.trait_name)
}

/// Whether `bridge` is emitted for the target that answers to `target_spellings`.
///
/// `exclude_languages` may name a backend by its language (`"node"`, `"r"`, `"elixir"`) or by the
/// backend itself (`"napi"`, `"extendr"`, `"rustler"`), so a target that answers to more than one
/// spelling is excluded when *any* of its spellings is listed. ~keep
pub fn bridge_targets_language(bridge: &TraitBridgeConfig, target_spellings: &[&str]) -> bool {
    target_spellings.iter().all(|spelling| bridge.is_active_for(spelling))
}

/// The `register_fn` for which a registration function is actually generated.
///
/// `register_fn` alone is not enough: every backend's `gen_registration_fn` returns an empty
/// string without `registry_getter` (the FFI backend panics instead), because the generated body
/// has no registry to insert into. A site that wires the name into a module manifest —
/// `extendr_module!`, a NAMESPACE file, a reported registration surface — must ask this rather
/// than read `register_fn` directly, or it names a symbol no emitter wrote. ~keep
pub fn bridge_register_symbol(bridge: &TraitBridgeConfig) -> Option<&str> {
    bridge
        .register_fn
        .as_deref()
        .filter(|_| bridge.registry_getter.is_some())
}

/// The trait `TypeDef` a bridge wraps, when the target named by `target_spellings` emits that
/// bridge at all.
///
/// Combines the two gates a bridge emitter applies: the bridge must not be excluded for the target
/// (see [`bridge_targets_language`]) and its trait must resolve in the `ApiSurface`.
///
/// Backends that emit the Rust bridge and the host-language wrapper from separate passes ask
/// through this one lookup, so the passes cannot answer the question differently and leave a
/// wrapper calling a symbol no pass generated. `Backend::trait_bridge_registration_surface` asks
/// through it too, so the reference docs describe the API that was actually emitted. ~keep
pub fn active_bridge_trait_def<'a>(
    bridge: &TraitBridgeConfig,
    api: &'a ApiSurface,
    target_spellings: &[&str],
) -> Option<&'a TypeDef> {
    // `target` is reserved syntax in the `tracing` macros, so the field carries the backend's
    // spelling under a different key. ~keep
    let bridge_target = target_spellings.first().copied().unwrap_or("");
    if !bridge_targets_language(bridge, target_spellings) {
        tracing::debug!(
            trait_name = %bridge.trait_name,
            bridge_target,
            "trait bridge skipped: exclude_languages names this target"
        );
        return None;
    }
    let trait_def = find_trait_def(bridge, api);
    if trait_def.is_none() {
        tracing::warn!(
            trait_name = %bridge.trait_name,
            bridge_target,
            "trait bridge skipped: the configured trait is absent from the API surface"
        );
    }
    trait_def
}

/// True if `field_ty` references a `Named` type whose name equals `alias`,
/// allowing for `Option<>` and `Vec<>` wrappers.
fn field_type_matches_alias(field_ty: &TypeRef, alias: &str) -> bool {
    match field_ty {
        TypeRef::Named(n) => n == alias,
        TypeRef::Optional(inner) | TypeRef::Vec(inner) => field_type_matches_alias(inner, alias),
        _ => false,
    }
}

/// True when a `TypeRef` references a trait type (directly or through
/// `Option`/`Vec`/`Map` wrappers). Trait-typed values cannot cross a foreign
/// bridge — the host cannot construct or return a Rust trait object — so
/// methods whose signature references a trait are excluded from
/// defaulted-method forwarding and keep the Rust default unconditionally.
pub fn type_references_trait(ty: &TypeRef, api: &ApiSurface) -> bool {
    match ty {
        TypeRef::Named(name) => {
            api.types.iter().any(|t| t.is_trait && &t.name == name) || api.excluded_trait_names.contains(name)
        }
        TypeRef::Optional(inner) | TypeRef::Vec(inner) => type_references_trait(inner, api),
        TypeRef::Map(k, v) => type_references_trait(k, api) || type_references_trait(v, api),
        _ => false,
    }
}

/// True when any param or the return type of `method` references a trait type.
pub fn method_signature_references_trait(method: &crate::core::ir::MethodDef, api: &ApiSurface) -> bool {
    type_references_trait(&method.return_type, api) || method.params.iter().any(|p| type_references_trait(&p.ty, api))
}

/// The names of the Rust-defaulted own methods of `trait_type` whose signatures a
/// foreign bridge can marshal — the standard input to a backend's
/// defaulted-method forwarding opt-in (`gen_method_presence_check`).
pub fn forwardable_defaulted_method_names(trait_type: &TypeDef, api: &ApiSurface) -> std::collections::HashSet<String> {
    trait_type
        .methods
        .iter()
        .filter(|m| m.trait_source.is_none() && m.has_default_impl && !method_signature_references_trait(m, api))
        .map(|m| m.name.clone())
        .collect()
}