Problem
The From<Binding> for Core (binding→core) impl alef generates for a public-field config type is a fully exhaustive struct literal, with no ..Default::default() trailer:
impl From<EmbeddingConfig> for xberg::EmbeddingConfig {
fn from(val: EmbeddingConfig) -> Self {
Self {
model: val.model.into(),
normalize: val.normalize,
batch_size: val.batch_size,
show_download_progress: val.show_download_progress,
cache_dir: val.cache_dir.map(Into::into),
acceleration: val.acceleration.map(Into::into),
max_embed_duration_secs: val.max_embed_duration_secs,
}
}
}
Because every field is named and there is no spread, the moment a field is added to the core struct — a purely additive, backward-compatible change — this impl stops compiling with error[E0063]: missing field <name> in initializer of <Core>, until the bindings are regenerated. The generated binding cannot survive an additive core change without a regen.
This is inconsistent across backends. napi sets optionalize_defaults, so its binding fields are optional and construction routes through a Default-seeded builder that is forward-compatible:
impl From<JsEmbeddingConfig> for xberg::EmbeddingConfig {
fn from(val: JsEmbeddingConfig) -> Self {
let mut __result = xberg::EmbeddingConfig::default();
if let Some(__v) = val.model { __result.model = __v.into(); }
// ... a new core field simply keeps its default; still compiles ...
__result
}
}
Every other backend that mirrors a core config type — pyo3, magnus (ruby), php, extendr (R), rustler (elixir), wasm, and the dart and swift mirror-crate generators — emits the exhaustive literal and is not forward-compatible.
alef already knows how to emit the trailer: src/codegen/conversions/binding_to_core/render.rs appends ..Default::default() when a field is skipped at generation time (skipped_binding_excluded, ~L238–270) or when the binding strips cfg-gated fields. But when the binding mirrors every current core field — the common case — the trailer is omitted, which is precisely the case that is not forward-compatible. The private-field path (construction.rs) already seeds from Default and is immune.
Concretely in kreuzberg: adding EmbeddingConfig.max_sequence_length: Option<usize> (defaulting to None, a source-only change with no regen) turns the Python, PHP, dart, elixir, and ruby build/E2E CI jobs red on missing field max_sequence_length, even though the change is additive and every binding compiles again after a regen. Any additive field on any core config struct reproduces it.
Solution
When the core type implements Default, always append the ..Default::default() trailer to the public-field binding→core Self { } literal — not only when a field was skipped at generation time. The currently-mapped fields are still assigned explicitly (so no behavior change for existing fields); a field added to the core after generation falls to its core default instead of breaking compilation, matching napi/wasm-style forward-compatibility without changing any backend's binding ergonomics or field optionality.
The existing "don't bypass a bespoke Core::default()" concern that motivated the conditional trailer is unaffected: the trailer only fills fields the impl does not set, and every field the binding knows about is still assigned explicitly above it.
Cross-binding scope (verified against generated output and src/backends/*/gen_bindings/mod.rs conversion configs): the fix belongs in the shared binding_to_core/render.rs public-field path (pyo3/extendr/rustler/wasm route through it) and must be mirrored in the backends that emit their own struct conversion — php (gen_bindings/helpers/struct_conversion.rs), magnus, and the dart/swift mirror-crate generators. napi already sets optionalize_defaults and is immune; the private-field construction.rs path already seeds from Default and is immune.
Present in alef v0.34.0 (main).
Exact condition on main (765002aa1288, v0.34.0), src/codegen/conversions/binding_to_core/render.rs:324:
let emit_trailer = typ.has_stripped_cfg_fields || skipped_binding_excluded;
For a fully-mirrored has_default config type (all fields present, none binding_excluded, no cfg-stripping) both operands are false, so no ..Default::default() trailer is rendered and the Self { } literal is exhaustive. napi avoids this only via optionalize_defaults => builder_mode (the Default-seeded __result builder). The fix is to also force emit_trailer when typ.has_default (and mirror it in the php/magnus/dart/swift struct-conversion generators).
Problem
The
From<Binding> for Core(binding→core) impl alef generates for a public-field config type is a fully exhaustive struct literal, with no..Default::default()trailer:Because every field is named and there is no spread, the moment a field is added to the core struct — a purely additive, backward-compatible change — this impl stops compiling with
error[E0063]: missing field <name> in initializer of <Core>, until the bindings are regenerated. The generated binding cannot survive an additive core change without a regen.This is inconsistent across backends. napi sets
optionalize_defaults, so its binding fields are optional and construction routes through aDefault-seeded builder that is forward-compatible:Every other backend that mirrors a core config type — pyo3, magnus (ruby), php, extendr (R), rustler (elixir), wasm, and the dart and swift mirror-crate generators — emits the exhaustive literal and is not forward-compatible.
alef already knows how to emit the trailer:
src/codegen/conversions/binding_to_core/render.rsappends..Default::default()when a field is skipped at generation time (skipped_binding_excluded, ~L238–270) or when the binding strips cfg-gated fields. But when the binding mirrors every current core field — the common case — the trailer is omitted, which is precisely the case that is not forward-compatible. The private-field path (construction.rs) already seeds fromDefaultand is immune.Concretely in kreuzberg: adding
EmbeddingConfig.max_sequence_length: Option<usize>(defaulting toNone, a source-only change with no regen) turns the Python, PHP, dart, elixir, and ruby build/E2E CI jobs red onmissing field max_sequence_length, even though the change is additive and every binding compiles again after a regen. Any additive field on any core config struct reproduces it.Solution
When the core type implements
Default, always append the..Default::default()trailer to the public-field binding→coreSelf { }literal — not only when a field was skipped at generation time. The currently-mapped fields are still assigned explicitly (so no behavior change for existing fields); a field added to the core after generation falls to its core default instead of breaking compilation, matching napi/wasm-style forward-compatibility without changing any backend's binding ergonomics or field optionality.The existing "don't bypass a bespoke
Core::default()" concern that motivated the conditional trailer is unaffected: the trailer only fills fields the impl does not set, and every field the binding knows about is still assigned explicitly above it.Cross-binding scope (verified against generated output and
src/backends/*/gen_bindings/mod.rsconversion configs): the fix belongs in the sharedbinding_to_core/render.rspublic-field path (pyo3/extendr/rustler/wasm route through it) and must be mirrored in the backends that emit their own struct conversion —php(gen_bindings/helpers/struct_conversion.rs),magnus, and thedart/swiftmirror-crate generators.napialready setsoptionalize_defaultsand is immune; the private-fieldconstruction.rspath already seeds fromDefaultand is immune.Present in alef v0.34.0 (
main).Exact condition on
main(765002aa1288, v0.34.0),src/codegen/conversions/binding_to_core/render.rs:324:For a fully-mirrored
has_defaultconfig type (all fields present, nonebinding_excluded, no cfg-stripping) both operands are false, so no..Default::default()trailer is rendered and theSelf { }literal is exhaustive. napi avoids this only viaoptionalize_defaults => builder_mode(theDefault-seeded__resultbuilder). The fix is to also forceemit_trailerwhentyp.has_default(and mirror it in the php/magnus/dart/swift struct-conversion generators).