alef 0.80.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
use crate::codegen::generators;
use crate::codegen::shared::binding_fields;
use crate::core::hash::{self, CommentStyle};
use crate::core::ir::ApiSurface;
use ahash::{AHashMap, AHashSet};

use super::async_wrappers::{
    adapter_param_python_type, adapter_return_converter, emit_adapter_wrapper, streaming_item_converter,
};
use super::converters::emit_converters;
use super::function_wrappers::emit_function_wrappers;
use super::helper_type_mapping::classify_param_type;
use crate::backends::pyo3::gen_bindings::types::collect_named_types;

/// Generate api.py — wrapper functions that convert Python types to Rust binding types.
///
/// For each function parameter whose type is a `has_default` struct (e.g. `ParseOptions`),
/// we generate a `_to_rust_{snake_name}` converter that maps the Python `@dataclass` instance
/// to the Rust binding's pyclass by passing every field as a keyword argument.
#[allow(clippy::too_many_arguments)]
pub(in crate::backends::pyo3::gen_bindings) fn gen_api_py(
    api: &ApiSurface,
    module_name: &str,
    package_name: &str,
    trait_bridges: &[crate::core::config::TraitBridgeConfig],
    dto: &crate::core::config::DtoConfig,
    capsule_types: &std::collections::HashMap<String, crate::core::config::CapsuleTypeConfig>,
    opaque_types: &std::collections::HashMap<String, String>,
    adapters: &[crate::core::config::AdapterConfig],
    reexported_types: &[String],
    exclude_functions: &AHashSet<String>,
    config: &crate::core::config::ResolvedCrateConfig,
) -> String {
    use crate::core::ir::TypeRef;

    let bridge_param_names: ahash::AHashSet<&str> =
        trait_bridges.iter().filter_map(|b| b.param_name.as_deref()).collect();

    let options_field_bridges: AHashMap<&str, (&str, &str, Option<&str>)> = trait_bridges
        .iter()
        .filter(|b| b.bind_via == crate::core::config::BridgeBinding::OptionsField)
        .filter_map(|b| {
            let options_type = b.options_type.as_deref()?;
            let param_name = b.param_name.as_deref()?;
            let field_name = b.resolved_options_field()?;
            let trait_present = api.types.iter().any(|t| t.name == b.trait_name);
            let handle_type = if trait_present {
                Some(b.trait_name.as_str())
            } else {
                b.type_alias.as_deref()
            };
            Some((options_type, (param_name, field_name, handle_type)))
        })
        .collect();

    let default_types: AHashMap<String, &crate::core::ir::TypeDef> = api
        .types
        .iter()
        .filter(|t| t.has_default && !t.name.ends_with("Update"))
        .map(|t| (t.name.clone(), t))
        .collect();

    // Types `options.py` emits as public `@dataclass` DTOs. An adapter param typed as one of
    // these crosses the Python/native boundary at a different shape than the engine call
    // actually accepts, so it needs the `_to_rust_*` converter — the same requirement plain
    // function wrappers already honor via `default_types`, scoped down to the subset that is
    // genuinely a public *input* dataclass (excludes return types; see `options_return_types`
    // and `options_publishable_return_types` below for those). ~keep
    let options_dataclass_types =
        crate::backends::pyo3::gen_bindings::types::options_dataclass_type_names(api, reexported_types);

    // Return types `options.py` publishes itself. A function returning one of these must name the
    // public type and convert into it, not name the native `#[pyclass]` behind the same word.
    let options_return_types =
        crate::backends::pyo3::gen_bindings::types::options_return_typeddict_names(api, dto, reexported_types);

    // The question a RETURN value (a plain function's, an adapter's, or a streaming adapter's
    // item) has to answer is "does `options.py` publish this name", which is
    // `options_dataclass_types` OR `options_return_types` -- not `options_dataclass_types` alone
    // and not `options_return_types` alone. `api.py`'s own import classification
    // (`options_type_names` further below) already consults the union, so a return type that is
    // a public *input* dataclass (not a return-only `TypedDict`) still gets imported from
    // `.options` and named in the `-> ReturnType` annotation. `adapter_return_converter` /
    // `streaming_item_converter` (adapters) and `emit_function_wrappers` / `function_return_converters`
    // (plain functions) must all be handed this union, not either half alone, or the annotation
    // names the public type while the body hands back the untouched native pyclass. ~keep
    let options_publishable_return_types: std::collections::HashSet<String> =
        options_dataclass_types.union(&options_return_types).cloned().collect();

    let enum_names: AHashSet<&str> = api.enums.iter().map(|e| e.name.as_str()).collect();

    // A sanitized data enum has an unresolvable variant field, so no serde-based `#[new]` is
    let data_enum_names: AHashSet<&str> = api
        .enums
        .iter()
        .filter(|e| generators::enum_has_data_variants(e) && !generators::enum_has_sanitized_fields(e))
        .map(|e| e.name.as_str())
        .collect();

    let mut needed_converters: Vec<String> = Vec::new();
    let mut visited: AHashSet<String> = AHashSet::new();

    fn collect_needed(
        type_name: &str,
        default_types: &AHashMap<String, &crate::core::ir::TypeDef>,
        needed: &mut Vec<String>,
        visited: &mut AHashSet<String>,
    ) {
        if !visited.insert(type_name.to_string()) {
            return;
        }
        if let Some(typ) = default_types.get(type_name) {
            for field in binding_fields(&typ.fields) {
                if let Some((name, _)) = classify_param_type(&field.ty)
                    && default_types.contains_key(name)
                {
                    collect_needed(name, default_types, needed, visited);
                }
            }
            needed.push(type_name.to_string());
        }
    }

    for func in &api.functions {
        for param in &func.params {
            if let Some((name, _)) = classify_param_type(&param.ty) {
                collect_needed(name, &default_types, &mut needed_converters, &mut visited);
            }
        }
    }
    // An adapter wrapper is not exempt from the same param-conversion requirement as a plain
    // function wrapper: a param typed as a public dataclass needs the `_to_rust_*` converter
    // `emit_adapter_wrapper` applies below. Only walk params that are genuinely emitted as
    // dataclasses — an `is_return_type` param would have no converter to find. ~keep
    for adapter in adapters {
        for param in &adapter.params {
            if options_dataclass_types.contains(&param.ty) {
                collect_needed(&param.ty, &default_types, &mut needed_converters, &mut visited);
            }
        }
    }

    let mut all_type_imports: AHashSet<String> = AHashSet::new();
    for type_name in &needed_converters {
        all_type_imports.insert(type_name.clone());
    }
    for func in &api.functions {
        for param in &func.params {
            collect_named_types(&param.ty, &mut all_type_imports);
        }
        collect_named_types(&func.return_type, &mut all_type_imports);
    }
    for adapter in adapters {
        if let Some(owner) = adapter.owner_type.as_deref() {
            all_type_imports.insert(owner.to_string());
        }
        if let Some(item) = adapter.item_type.as_deref() {
            all_type_imports.insert(item.to_string());
        }
        for param in &adapter.params {
            let mapped = adapter_param_python_type(&param.ty);
            if matches!(mapped, "str" | "bytes" | "None" | "int" | "float" | "bool") {
                continue;
            }
            all_type_imports.insert(param.ty.clone());
        }
        if let Some(returns) = adapter.returns.as_deref() {
            let mapped = adapter_param_python_type(returns);
            if !matches!(mapped, "str" | "bytes" | "None" | "int" | "float" | "bool") {
                all_type_imports.insert(returns.to_string());
            }
        }
    }
    for bridge in trait_bridges {
        let trait_present = api.types.iter().any(|t| t.name == bridge.trait_name);
        if trait_present {
            all_type_imports.insert(bridge.trait_name.clone());
        } else if let Some(alias) = &bridge.type_alias {
            all_type_imports.insert(alias.clone());
        }
    }

    let needs_cast = api.functions.iter().any(|f| {
        let leaf = match &f.return_type {
            crate::core::ir::TypeRef::Named(n) => Some(n.as_str()),
            crate::core::ir::TypeRef::Optional(inner) => match inner.as_ref() {
                crate::core::ir::TypeRef::Named(n) => Some(n.as_str()),
                _ => None,
            },
            _ => None,
        };
        leaf.is_some_and(|n| capsule_types.contains_key(n))
    });

    let mut out = String::with_capacity(4096);
    out.push_str(&hash::header(CommentStyle::Hash));
    out.push_str("\"\"\"Public API for conversion.\"\"\"\n\n");
    let mut typing_parts: Vec<&str> = vec!["Any", "TypeVar"];
    if needs_cast || !needed_converters.is_empty() {
        typing_parts.push("cast");
    }
    if !needed_converters.is_empty() {
        typing_parts.push("overload");
        typing_parts.push("TypedDict");
    }
    let needs_async_iterator = adapters
        .iter()
        .any(|a| matches!(a.pattern, crate::core::config::AdapterPattern::Streaming));
    if needs_async_iterator {
        typing_parts.push("AsyncIterator");
    }
    typing_parts.sort_unstable();
    if !needed_converters.is_empty() {
        out.push_str("import json\n");
    }
    out.push_str(&crate::backends::pyo3::template_env::render(
        "typing_import.jinja",
        minijinja::context! { names => typing_parts },
    ));
    out.push('\n');
    out.push_str(&crate::backends::pyo3::template_env::render(
        "import_as_module.jinja",
        minijinja::context! {
            package_name => package_name,
            module_name => module_name,
        },
    ));

    let opaque_names: AHashSet<String> = api
        .types
        .iter()
        .filter(|t| t.is_opaque)
        .map(|t| t.name.clone())
        .collect();
    let error_names: AHashSet<String> = api.errors.iter().map(|e| e.name.clone()).collect();
    let reexported_names: AHashSet<&str> = reexported_types.iter().map(|s| s.as_str()).collect();
    let options_type_names: AHashSet<String> = {
        let mut names: AHashSet<String> = api
            .types
            .iter()
            .filter(|t| t.has_default && !t.name.ends_with("Update") && !t.is_return_type)
            .map(|t| t.name.clone())
            .collect();
        names.extend(options_return_types.iter().cloned());
        names
    };
    let return_type_names: AHashSet<String> = api
        .types
        .iter()
        .filter(|t| t.is_return_type && !capsule_types.contains_key(&t.name))
        .map(|t| t.name.clone())
        .collect();
    let all_ir_type_names: AHashSet<String> = api.types.iter().map(|t| t.name.clone()).collect();
    let options_enum_names: AHashSet<String> = {
        let mut set = AHashSet::new();
        for typ in api
            .types
            .iter()
            .filter(|t| t.has_default && !t.name.ends_with("Update"))
        {
            for field in binding_fields(&typ.fields) {
                let inner_name = match &field.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,
                };
                if let Some(name) = inner_name
                    && enum_names.contains(name)
                    && !data_enum_names.contains(name)
                {
                    set.insert(name.to_string());
                }
            }
        }
        set
    };

    let all_enum_names: AHashSet<String> = api.enums.iter().map(|e| e.name.clone()).collect();
    let mut options_imports: Vec<&str> = Vec::new();
    let mut native_imports: Vec<&str> = Vec::new();
    for name in &all_type_imports {
        // Capsule types are not registered as #[pyclass] in the native module; skip them
        if capsule_types.contains_key(name) {
            continue;
        }
        let is_options = options_type_names.contains(name) || options_enum_names.contains(name);
        // binding-side #[pyclass] wrapper struct emitted in mod.rs and are exported from
        let is_opaque_wrapper = opaque_types.contains_key(name) && !capsule_types.contains_key(name);
        let is_native = !is_options
            && (opaque_names.contains(name)
                || error_names.contains(name)
                || all_ir_type_names.contains(name)
                || is_opaque_wrapper
                || (all_enum_names.contains(name) && !options_enum_names.contains(name)));
        if is_native {
            native_imports.push(name.as_str());
        } else {
            options_imports.push(name.as_str());
        }
    }

    let streaming_item_converters: std::collections::BTreeSet<String> = adapters
        .iter()
        .filter_map(|adapter| streaming_item_converter(adapter, &options_publishable_return_types))
        .collect();
    let adapter_return_converters: std::collections::BTreeSet<String> = adapters
        .iter()
        .filter_map(|adapter| adapter_return_converter(adapter, &options_publishable_return_types))
        .collect();

    // A wrapper returning a type `options.py` publishes calls that type's `_from_native_*`
    // converter, so `api.py` has to import it alongside the type itself. The publishable set is
    // the union (`options_publishable_return_types`), not `options_return_types` alone: a plain
    // function's return type is routinely a public *input* dataclass rather than a return-only
    // `TypedDict`, and the narrower set left that shape's converter uncalled and unimported --
    // the same asymmetry `adapter_return_converter`/`streaming_item_converter` already avoid. ~keep
    let function_return_converters: std::collections::BTreeSet<String> = api
        .functions
        .iter()
        .filter(|func| !exclude_functions.contains(&func.name))
        .filter_map(|func| match &func.return_type {
            crate::core::ir::TypeRef::Named(name) => Some(name),
            crate::core::ir::TypeRef::Optional(inner) => match inner.as_ref() {
                crate::core::ir::TypeRef::Named(name) => Some(name),
                _ => None,
            },
            _ => None,
        })
        .filter(|name| options_publishable_return_types.contains(*name))
        .map(|name| crate::backends::pyo3::gen_bindings::types::from_native_converter_name(name))
        .collect();

    options_imports.extend(streaming_item_converters.iter().map(String::as_str));
    options_imports.extend(adapter_return_converters.iter().map(String::as_str));
    options_imports.extend(function_return_converters.iter().map(String::as_str));
    native_imports.sort_unstable();
    options_imports.sort_unstable();
    if !native_imports.is_empty() {
        // isort: blank line between `import X as _rust` (absolute) and `from .Y import` (relative).
        out.push('\n');
        out.push_str(&crate::backends::pyo3::template_env::render(
            "import_from_module.jinja",
            minijinja::context! {
                module_name => module_name,
                imports => native_imports.join(", "),
            },
        ));
    }
    if !options_imports.is_empty() {
        out.push_str(&crate::backends::pyo3::template_env::render(
            "import_from_options.jinja",
            minijinja::context! {
                imports => options_imports.join(", "),
            },
        ));
    }
    {
        use std::collections::BTreeMap;
        let mut capsule_imports: BTreeMap<String, Vec<String>> = BTreeMap::new();
        for (rust_name, cfg) in capsule_types {
            let python_type = cfg.python_type();
            if let Some((module_path, _class_name)) = python_type.rsplit_once('.') {
                capsule_imports
                    .entry(module_path.to_string())
                    .or_default()
                    .push(rust_name.clone());
            }
        }
        if !capsule_imports.is_empty() {
            for (module_path, mut names) in capsule_imports {
                names.sort_unstable();
                out.push_str(&crate::backends::pyo3::template_env::render(
                    "import_from_absolute_module.jinja",
                    minijinja::context! {
                        module_name => module_path,
                        imports => names.join(", "),
                    },
                ));
            }
        }
    }
    out.push('\n');

    emit_converters(
        &mut out,
        &needed_converters,
        &default_types,
        &options_field_bridges,
        &enum_names,
        &data_enum_names,
        reexported_types,
        config,
        &crate::backends::pyo3::gen_bindings::types::OptionsFieldDefaults::new(api),
        &options_return_types,
    );

    emit_function_wrappers(
        &mut out,
        api,
        trait_bridges,
        capsule_types,
        exclude_functions,
        &bridge_param_names,
        &options_field_bridges,
        &default_types,
        &data_enum_names,
        &return_type_names,
        &reexported_names,
        &options_publishable_return_types,
    );

    for adapter in adapters {
        emit_adapter_wrapper(
            &mut out,
            adapter,
            &api.types,
            &options_dataclass_types,
            &options_publishable_return_types,
        );
    }

    out
}