From 2c1817f340acca11b6c1ffc4f906b07cb44113a2 Mon Sep 17 00:00:00 2001 From: 2kai2kai2 Date: Wed, 1 Jul 2026 16:37:58 -0700 Subject: [PATCH 1/2] Zero-cost type family conversions By ensuring that the type family members have recursively identical memory layouts, we can enable zero-cost conversions via `::std::mem::transmute`. Caveats: - Concrete family members have a smaller total size, so the root node of their type tree must be converted. However, this is O(1) since all child nodes are `RuntimeTy` or `RealizedTy`, meaning we do not have to walk the children. - Downcasts still require a walk to verify - We rely on some de-facto sound but not formally guaranteed memory layouts from rust: primarily, that `Vec` and tuples should have a consistent memory layout based on element memory size. At a later date, we can switch to guaranteed-safe versions (but in practice this is fine) --- baml_language/crates/baml_type/src/family.rs | 49 ++++++ .../crates/baml_type/src/runtime_ty.rs | 18 +-- .../crates/baml_type_macros/src/convert.rs | 153 ++++++++++++++---- .../crates/baml_type_macros/src/emit.rs | 40 ++++- 4 files changed, 216 insertions(+), 44 deletions(-) diff --git a/baml_language/crates/baml_type/src/family.rs b/baml_language/crates/baml_type/src/family.rs index e79d56de323..ac1125622eb 100644 --- a/baml_language/crates/baml_type/src/family.rs +++ b/baml_language/crates/baml_type/src/family.rs @@ -462,4 +462,53 @@ mod tests { 25 ); } + + /// The leading byte of a `#[repr(C, u8)]` value is its discriminant. Reading + /// it directly lets us assert a logical variant carries the same *in-memory* + /// tag in every member — the premise the zero-cost `transmute` upcasts rest + /// on. (Distinct from the Borsh *wire* tag above, which is by declaration + /// index and so shifts when a member drops earlier variants.) + fn in_memory_tag(v: &T) -> u8 { + // SAFETY: every family member is `#[repr(C, u8)]`, so its first byte is + // the `u8` discriminant. + unsafe { *(v as *const T as *const u8) } + } + + #[test] + fn in_memory_discriminants_are_consistent_across_members() { + // `BuiltinUnknown` is master variant #27; `RealizedTy` drops the two + // `typevar` variants before it, yet its in-memory tag stays 27 (its + // Borsh index, checked above, is 25 — the two numbers are deliberately + // decoupled). + assert_eq!(in_memory_tag(&Ty::BuiltinUnknown { attr: a() }), 27); + assert_eq!(in_memory_tag(&RuntimeTy::BuiltinUnknown { attr: a() }), 27); + assert_eq!(in_memory_tag(&RealizedTy::BuiltinUnknown { attr: a() }), 27); + // `Never` (#28) is shared and tag-stable across the deep members. + assert_eq!(in_memory_tag(&Ty::Never { attr: a() }), 28); + assert_eq!(in_memory_tag(&RealizedTy::Never { attr: a() }), 28); + // A leaf concrete variant present in every member, shallow ones included. + assert_eq!(in_memory_tag(&Ty::Int { attr: a() }), 0); + assert_eq!(in_memory_tag(&ConcreteTy::Int { attr: a() }), 0); + assert_eq!(in_memory_tag(&ConcreteRealizedTy::Int { attr: a() }), 0); + } + + /// The borrowed upcast (`RuntimeTy::as_ty`, `RealizedTy::as_runtime_ty`, …) + /// reinterprets in place and yields a `&Super` structurally equal to the + /// owned widening — proving the `transmute` produces the right *value*, not + /// merely a same-sized one, at every nesting depth of `deep_concrete()`. + #[test] + fn borrowed_upcast_matches_owned_widening() { + let t = deep_concrete(); + let rt = RuntimeTy::try_from(&t).unwrap(); + let rz = RealizedTy::try_from(&t).unwrap(); + + // Reinterpreting the narrower value yields the wider value by reference. + assert_eq!(rt.as_ty(), &t); + assert_eq!(rz.as_ty(), &t); + assert_eq!(rz.as_runtime_ty(), &rt); + + // And it agrees with the owned `From` (also a transmute) on equal input. + assert_eq!(rt.as_ty(), &Ty::from(rt.clone())); + assert_eq!(rz.as_runtime_ty(), &RuntimeTy::from(rz.clone())); + } } diff --git a/baml_language/crates/baml_type/src/runtime_ty.rs b/baml_language/crates/baml_type/src/runtime_ty.rs index a8c9c4411a1..bd6f0b190cc 100644 --- a/baml_language/crates/baml_type/src/runtime_ty.rs +++ b/baml_language/crates/baml_type/src/runtime_ty.rs @@ -216,35 +216,35 @@ impl RuntimeTy { // --- Rendering / subtyping --- // - // These reuse `Ty`'s implementation via the infallible upcast so the - // structural logic lives in exactly one place. The value remains a - // statically runtime-safe `RuntimeTy`; the upcast is purely to share the - // algorithm. None of these are on a VM-hot path. + // These reuse `Ty`'s implementation so the structural logic lives in exactly + // one place. The upcast is [`RuntimeTy::as_ty`] — a zero-cost borrow, not a + // clone — so sharing the algorithm costs nothing; the value remains a + // statically runtime-safe `RuntimeTy`. /// User-facing rendering — see [`Ty::render_user_facing`]. pub fn render_user_facing(&self) -> String { - Ty::from(self).render_user_facing() + self.as_ty().render_user_facing() } /// Canonical structural rendering — see [`Ty::render_canonical`]. pub fn render_canonical(&self) -> String { - Ty::from(self).render_canonical() + self.as_ty().render_canonical() } /// Render with a custom strategy — see [`Ty::render_with`]. pub fn render_with(&self, s: &dyn crate::TyRenderStrategy) -> String { - Ty::from(self).render_with(s) + self.as_ty().render_with(s) } /// Structural subtyping — see [`Ty::is_subtype_of`]. pub fn is_subtype_of(&self, other: &RuntimeTy) -> bool { - Ty::from(self).is_subtype_of(&Ty::from(other)) + self.as_ty().is_subtype_of(other.as_ty()) } } impl std::fmt::Display for RuntimeTy { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(&Ty::from(self), f) + std::fmt::Display::fmt(self.as_ty(), f) } } diff --git a/baml_language/crates/baml_type_macros/src/convert.rs b/baml_language/crates/baml_type_macros/src/convert.rs index b0d389c960e..5c0eee211f6 100644 --- a/baml_language/crates/baml_type_macros/src/convert.rs +++ b/baml_language/crates/baml_type_macros/src/convert.rs @@ -42,12 +42,26 @@ pub(crate) fn emit_conversions(family: &Family) -> TokenStream { } for (sub, sup) in comparable_pairs(family) { - out.extend(gen_widen(family, sub, sup)); + // A pair of *deep* members that this family builds equal-size (every + // deep member includes the largest-payload axis) can widen by a pure + // `transmute` — no walk, no allocation. Shallow-involved pairs differ in + // size (a shallow member omits the wider variants), so they keep the + // structural rebuild. The equal-size premise is enforced, not assumed: + // every emitted transmute carries a co-located `const` size+align assert + // that fails compilation if a future member breaks it. + if family.members[sub].deep && family.members[sup].deep { + out.extend(gen_widen_transmute(family, sub, sup)); + out.extend(gen_as_upcast(family, sub, sup)); + } else { + out.extend(gen_widen(family, sub, sup)); + } + // Narrowing always walks to validate; it stays a structural rebuild. out.extend(gen_narrow(family, sub, sup)); } // Satellite conversions, for the comparable pairs among deep members (the - // only members that own satellites). + // only members that own satellites). Deep satellites are equal-size too, so + // their widenings transmute as well. for (sub, sup) in comparable_pairs(family) { if family.members[sub].deep && family.members[sup].deep { for sat in &family.satellites { @@ -63,6 +77,102 @@ pub(crate) fn emit_conversions(family: &Family) -> TokenStream { out } +/// A `const`-block guard, co-located with every unsafe upcast, that fails +/// compilation unless `a` and `b` share size *and* alignment. Size equality is +/// what `transmute` of owned values already checks, but the reference upcast +/// (`&a -> &b`) is pointer-sized regardless, so without this a size mismatch +/// there would be silent UB; alignment is never checked by `transmute` at all. +fn layout_assert(a: &Ident, b: &Ident) -> TokenStream { + quote! { + const { + ::core::assert!( + ::core::mem::size_of::<#a>() == ::core::mem::size_of::<#b>() + && ::core::mem::align_of::<#a>() == ::core::mem::align_of::<#b>(), + "ty_family: family members must share size and alignment for a \ + zero-cost transmute upcast", + ); + }; + } +} + +/// `RuntimeTy` → `as_runtime_ty`, `Ty` → `as_ty`: the borrowed-upcast accessor +/// name for widening *to* `sup`. +fn as_method_name(sup: &Ident) -> Ident { + let s = sup.to_string(); + let mut snake = String::with_capacity(s.len() + 4); + for (i, ch) in s.char_indices() { + if ch.is_ascii_uppercase() { + if i != 0 { + snake.push('_'); + } + snake.push(ch.to_ascii_lowercase()); + } else { + snake.push(ch); + } + } + format_ident!("as_{}", snake) +} + +/// Widen `sub` → `sup` (both deep, equal-size) by `transmute`. The owned form is +/// a genuine zero-cost move of the whole value; the by-reference form widens the +/// borrow and clones through it (so it composes with the recursion but is not +/// itself zero-cost — callers wanting that reach for [`as_method_name`]). +fn gen_widen_transmute(family: &Family, sub: usize, sup: usize) -> TokenStream { + let sub_name = &family.members[sub].name; + let sup_name = &family.members[sup].name; + let assert = layout_assert(sub_name, sup_name); + quote! { + impl ::core::convert::From<#sub_name> for #sup_name { + fn from(value: #sub_name) -> Self { + #assert + // SAFETY: `#sub_name` ≤ `#sup_name` in the family order, so every + // variant of `#sub_name` exists in `#sup_name` at the same + // `#[repr(C, u8)]` discriminant. Both are equal size + align (the + // `const` assert above), and the recursion travels through + // `Box`/`Vec`, so nested `#sub_name` children are likewise valid + // `#sup_name` when read back. The move transfers ownership of the + // heap allocations unchanged; dealloc uses the identical layout. + unsafe { ::core::mem::transmute::<#sub_name, #sup_name>(value) } + } + } + impl ::core::convert::From<&#sub_name> for #sup_name { + fn from(value: &#sub_name) -> Self { + <#sup_name as ::core::convert::From<#sub_name>>::from(value.clone()) + } + } + } +} + +/// The zero-cost borrowed upcast `sub::as_(&self) -> &sup`, for a deep, +/// equal-size pair. Used on hot paths that only need to *view* a narrower type +/// through the wider type's algorithms (rendering, subtyping) without cloning. +fn gen_as_upcast(family: &Family, sub: usize, sup: usize) -> TokenStream { + let sub_name = &family.members[sub].name; + let sup_name = &family.members[sup].name; + let method = as_method_name(sup_name); + let assert = layout_assert(sub_name, sup_name); + let doc = format!( + " Reinterpret this `{sub_name}` as a `{sup_name}` without copying.\n\n\ + Every `{sub_name}` is a valid `{sup_name}` and the two share an\n\ + identical layout, so this widening is a pure borrow — no allocation,\n\ + no walk. Prefer it over `{sup_name}::from(&self)` when a shared\n\ + reference suffices." + ); + quote! { + impl #sub_name { + #[doc = #doc] + #[must_use] + pub fn #method(&self) -> &#sup_name { + #assert + // SAFETY: as in the owned `From` above, but at the reference + // level — a shared borrow reinterpreted in place. Sound only + // because the two types are equal size + align (asserted). + unsafe { ::core::mem::transmute::<&#sub_name, &#sup_name>(self) } + } + } + } +} + // ── Product order ──────────────────────────────────────────────────────────── fn subset(a: &[usize], b: &[usize]) -> bool { @@ -432,16 +542,25 @@ fn gen_sat(family: &Family, sub: usize, sup: usize, sat: &SatFields) -> TokenStr let sub_sat = satellite_name_for(&family.members[sub].name, &sat.name); let sup_sat = satellite_name_for(&family.members[sup].name, &sat.name); let err = format_ident!("Not{}", &family.members[sub].name); - let widen_ref = sat_widen_body(family, sub, sup, sat, Own::Ref); - let widen_owned = sat_widen_body(family, sub, sup, sat, Own::Owned); + let assert = layout_assert(&sub_sat, &sup_sat); let narrow_ref = sat_narrow_body(family, sub, sup, sat, Own::Ref); let narrow_owned = sat_narrow_body(family, sub, sup, sat, Own::Owned); quote! { impl ::core::convert::From<&#sub_sat> for #sup_sat { - fn from(value: &#sub_sat) -> Self { #widen_ref } + fn from(value: &#sub_sat) -> Self { + <#sup_sat as ::core::convert::From<#sub_sat>>::from(value.clone()) + } } impl ::core::convert::From<#sub_sat> for #sup_sat { - fn from(value: #sub_sat) -> Self { #widen_owned } + fn from(value: #sub_sat) -> Self { + #assert + // SAFETY: a satellite differs across the family only in its + // recursive field's member type (`ty: Ty` vs `ty: RuntimeTy`, + // …), which are equal-layout; `#[repr(C)]` pins the field order + // identically, and the pair is equal size + align (asserted). So + // the bytes of `#sub_sat` are a valid `#sup_sat`. + unsafe { ::core::mem::transmute::<#sub_sat, #sup_sat>(value) } + } } impl ::core::convert::TryFrom<&#sup_sat> for #sub_sat { type Error = #err; @@ -455,28 +574,6 @@ fn gen_sat(family: &Family, sub: usize, sup: usize, sat: &SatFields) -> TokenStr } // Satellites live on deep members, so each side's child is the member itself. -fn sat_widen_body( - family: &Family, - sub: usize, - sup: usize, - sat: &SatFields, - own: Own, -) -> TokenStream { - let sup_sat = satellite_name_for(&family.members[sup].name, &sat.name); - let cx = Cx { - family, - sub_child: sub, - sup_child: sup, - own, - }; - let fields = sat.fields.iter().map(|f| { - let fid = f.ident.as_ref().unwrap(); - let e = field_widen(&cx, &f.ty, value_field(own, fid)); - quote!( #fid: #e ) - }); - quote! { #sup_sat { #(#fields),* } } -} - fn sat_narrow_body( family: &Family, sub: usize, diff --git a/baml_language/crates/baml_type_macros/src/emit.rs b/baml_language/crates/baml_type_macros/src/emit.rs index 39a91ec5a9e..42b1691ce59 100644 --- a/baml_language/crates/baml_type_macros/src/emit.rs +++ b/baml_language/crates/baml_type_macros/src/emit.rs @@ -10,7 +10,7 @@ use std::collections::HashMap; -use proc_macro2::{Group, Ident, TokenStream, TokenTree}; +use proc_macro2::{Group, Ident, Literal, TokenStream, TokenTree}; use quote::{ToTokens, format_ident, quote}; use syn::{Attribute, Fields}; @@ -39,12 +39,25 @@ fn gen_member_enum(family: &Family, member: &Member) -> TokenStream { let child = &family.members[member.child]; let map = replacements(family, &child.name); - let variants = member_variants(family, member).map(|v| { - let attrs = &v.attrs; - let ident = &v.ident; - let fields = replace_idents(v.fields.to_token_stream(), &map); - quote! { #(#attrs)* #ident #fields } - }); + // Each variant carries an explicit discriminant equal to its index in the + // *master* declaration order — not its position within this member. Members + // that omit variants therefore leave gaps in their discriminant sequence + // rather than renumbering the tail, so a given logical variant has the same + // `#[repr(C, u8)]` tag in every member that includes it. That cross-member + // tag identity is exactly what lets an upcast reinterpret one member's bytes + // as another's without a walk (see `convert::gen_widen_transmute`). + let variants = family + .variants + .iter() + .enumerate() + .filter(|(_, v)| member.includes.contains(&v.axis)) + .map(|(idx, v)| { + let attrs = &v.attrs; + let ident = &v.ident; + let fields = replace_idents(v.fields.to_token_stream(), &map); + let disc = Literal::u8_unsuffixed(idx as u8); + quote! { #(#attrs)* #ident #fields = #disc } + }); let docs = member_docs(family, member); let derives = nondoc_attrs(&family.master_attrs); @@ -52,6 +65,14 @@ fn gen_member_enum(family: &Family, member: &Member) -> TokenStream { quote! { #(#docs)* #(#derives)* + // `repr(C, u8)`: a layout-stable `{ u8 tag, C-union payload }` shared + // identically across the family, so members with matching size/align are + // mutually transmutable. `use_discriminant = false`: Borsh keeps tagging + // by declaration index (compact, gap-free), decoupled from the in-memory + // discriminant — so the explicit tags above leave the wire format + // unchanged. + #[repr(C, u8)] + #[borsh(use_discriminant = false)] pub enum #name { #(#variants),* } @@ -155,6 +176,11 @@ fn gen_satellite(family: &Family, member: &Member, sat: &Satellite) -> TokenStre quote! { #[doc = #doc] #(#derives)* + // `repr(C)`: a fixed field order shared across the family so a satellite + // is layout-identical to its siblings (they differ only in the recursive + // field's member type), letting the enum-level transmute reinterpret a + // `Vec` field as `Vec` soundly. + #[repr(C)] pub struct #sat_name { #fields } From 7bdf4e22b6f54dfd38c7a288daeb1acd929ed5f0 Mon Sep 17 00:00:00 2001 From: 2kai2kai2 Date: Wed, 1 Jul 2026 16:59:51 -0700 Subject: [PATCH 2/2] No reallocation on type family narrowing Instead of walking and copying for narrowing, we instead walk to verify (no reallocations) then transumte in-place if the narrowing is legal. --- baml_language/crates/baml_type/src/family.rs | 62 ++- .../crates/baml_type_macros/src/convert.rs | 414 ++++++++++++++++-- 2 files changed, 446 insertions(+), 30 deletions(-) diff --git a/baml_language/crates/baml_type/src/family.rs b/baml_language/crates/baml_type/src/family.rs index ac1125622eb..32df449a78c 100644 --- a/baml_language/crates/baml_type/src/family.rs +++ b/baml_language/crates/baml_type/src/family.rs @@ -300,7 +300,7 @@ mod tests { use crate::{ ConcreteRealizedTy, ConcreteTy, FunctionParamTy, MediaKind, Name, NotRealizedTy, - RealizedTy, RuntimeTy, Ty, TyAttr, TypeName, + NotRuntimeTy, RealizedTy, RuntimeTy, Ty, TyAttr, TypeName, }; fn a() -> TyAttr { @@ -511,4 +511,64 @@ mod tests { assert_eq!(rt.as_ty(), &Ty::from(rt.clone())); assert_eq!(rz.as_runtime_ty(), &RuntimeTy::from(rz.clone())); } + + /// Narrowing a representable value: the borrow-to-borrow `TryFrom<&Super> + /// for &Sub` validates in place and reinterprets the borrow without copying; + /// the owned `TryFrom` validates then moves the tree. Both agree with the + /// narrower value at every depth. + #[test] + fn downcast_validates_then_reinterprets() { + let t = deep_concrete(); + let rt = RuntimeTy::try_from(&t).unwrap(); + let rz = RealizedTy::try_from(&t).unwrap(); + + // Borrow-to-borrow narrowing yields `Ok(&narrower)` at every depth. + assert_eq!(<&RuntimeTy>::try_from(&t), Ok(&rt)); + assert_eq!(<&RealizedTy>::try_from(&t), Ok(&rz)); + assert_eq!(<&RealizedTy>::try_from(&rt), Ok(&rz)); + + // Owned `TryFrom` (validate + move-transmute, no rebuild) agrees. + assert_eq!(RuntimeTy::try_from(t.clone()).unwrap(), rt); + assert_eq!(RealizedTy::try_from(t.clone()).unwrap(), rz); + assert_eq!(RealizedTy::try_from(rt.clone()).unwrap(), rz); + } + + /// The validation walk is complete: an unrepresentable variant nested at any + /// depth makes the narrowing fail — with the offending variant named, since + /// `TryFrom` carries an error — rather than transmute into an invalid + /// discriminant. + #[test] + fn downcast_rejects_unrepresentable_at_depth() { + // `TypeVar` under a `List`: a valid `RuntimeTy` (keeps `typevar`), not a + // valid `RealizedTy` (drops it) — the error names the culprit. + let tv = with_typevar(); + let tv_rt = <&RuntimeTy>::try_from(&tv).unwrap(); + assert_eq!( + <&RealizedTy>::try_from(&tv), + Err(NotRealizedTy { variant: "TypeVar" }) + ); + assert_eq!( + <&RealizedTy>::try_from(tv_rt), + Err(NotRealizedTy { variant: "TypeVar" }) + ); + + // A `tir`-only `Unknown` buried in a map value: not even a `RuntimeTy`. + let bad = Ty::Map { + key: Box::new(Ty::String { attr: a() }), + value: Box::new(Ty::List(Box::new(Ty::Unknown { attr: a() }), a())), + attr: a(), + }; + assert_eq!( + <&RuntimeTy>::try_from(&bad), + Err(NotRuntimeTy { variant: "Unknown" }) + ); + assert_eq!( + <&RealizedTy>::try_from(&bad), + Err(NotRealizedTy { variant: "Unknown" }) + ); + assert_eq!( + RuntimeTy::try_from(bad), + Err(NotRuntimeTy { variant: "Unknown" }) + ); + } } diff --git a/baml_language/crates/baml_type_macros/src/convert.rs b/baml_language/crates/baml_type_macros/src/convert.rs index 5c0eee211f6..9094a177d00 100644 --- a/baml_language/crates/baml_type_macros/src/convert.rs +++ b/baml_language/crates/baml_type_macros/src/convert.rs @@ -9,11 +9,32 @@ //! `includes(child(Sub)) ⊆ includes(child(Super))` — a product order over the //! top-level variant set and the (deep) child type. //! -//! Both an owned and a by-reference form are generated for each direction. The -//! **owned** form is a genuine move: when the two sides share a child type -//! (`Sub` and `Super` differ only at the top level) every field is moved -//! wholesale — zero allocation — and otherwise only the recursive spine is -//! rebuilt while leaf payloads are moved. The **by-reference** form clones. +//! **Deep, equal-size pairs** additionally get zero-cost borrow conversions and +//! convert by `transmute`: widening is `Sub::as_(&self) -> &Super` and a +//! pure-move `From`; narrowing adds `TryFrom<&Super> for &Sub` (validate once, +//! reinterpret the borrow) alongside the owned/clone `TryFrom`s. See +//! [`gen_widen_transmute`], [`gen_as_upcast`], and [`gen_narrow_transmute`]. +//! +//! **Shallow-involved pairs** (different-sized members) keep the structural +//! walk. Its owned form is a genuine move — when the two sides share a child +//! type every field is moved wholesale (zero allocation), otherwise only the +//! recursive spine is rebuilt while leaf payloads are moved; the by-reference +//! form clones. +//! +//! ## Layout reliance (the residual `unsafe` assumption) +//! +//! The `transmute` conversions are sound only because a member and its sibling +//! are laid out identically wherever they differ. `#[repr(C, u8)]` (enums) and +//! `#[repr(C)]` (satellites) pin the tag and field order, and the per-site +//! `const` assert pins total size + align. Beyond that, reinterpreting a nested +//! position — `Vec -> Vec`, `(Name, Ty) -> (Name, RuntimeTy)` — +//! rests on a *de-facto*, not language-guaranteed, property: `Vec` is a +//! `(ptr, cap, len)` triple whose layout is independent of `T`, and a +//! `#[repr(Rust)]` tuple's layout is a deterministic function of its fields' +//! sizes + aligns (equal here, since `Ty` and its members are equal size + +//! align). This holds on current `rustc` but is not a stability guarantee; the +//! unit tests plus a `cargo +nightly miri test -p baml_type` run in CI are what +//! guard it against a future layout change. use std::collections::HashSet; @@ -23,7 +44,7 @@ use syn::{Fields, GenericArgument, Index, PathArguments, Type}; use crate::{ emit::{member_variants, satellite_name_for}, - parse::{Family, MVariant}, + parse::{Family, MVariant, Satellite}, }; pub(crate) fn emit_conversions(family: &Family) -> TokenStream { @@ -43,20 +64,23 @@ pub(crate) fn emit_conversions(family: &Family) -> TokenStream { for (sub, sup) in comparable_pairs(family) { // A pair of *deep* members that this family builds equal-size (every - // deep member includes the largest-payload axis) can widen by a pure - // `transmute` — no walk, no allocation. Shallow-involved pairs differ in - // size (a shallow member omits the wider variants), so they keep the - // structural rebuild. The equal-size premise is enforced, not assumed: - // every emitted transmute carries a co-located `const` size+align assert - // that fails compilation if a future member breaks it. + // deep member includes the largest-payload axis) converts by `transmute` + // in both directions: widening is a pure move (every narrower value is a + // valid wider one), and narrowing walks *once* to validate then reuses + // the same bytes — no rebuild, no reallocation. Shallow-involved pairs + // differ in size (a shallow member omits the wider variants), so they + // keep the structural rebuild. The equal-size premise is enforced, not + // assumed: every emitted transmute carries a co-located `const` + // size+align assert that fails compilation if a future member breaks it. if family.members[sub].deep && family.members[sup].deep { out.extend(gen_widen_transmute(family, sub, sup)); out.extend(gen_as_upcast(family, sub, sup)); + out.extend(gen_narrow_transmute(family, sub, sup)); } else { out.extend(gen_widen(family, sub, sup)); + // Narrowing to a shallow member walks and rebuilds (sizes differ). + out.extend(gen_narrow(family, sub, sup)); } - // Narrowing always walks to validate; it stays a structural rebuild. - out.extend(gen_narrow(family, sub, sup)); } // Satellite conversions, for the comparable pairs among deep members (the @@ -77,11 +101,14 @@ pub(crate) fn emit_conversions(family: &Family) -> TokenStream { out } -/// A `const`-block guard, co-located with every unsafe upcast, that fails -/// compilation unless `a` and `b` share size *and* alignment. Size equality is -/// what `transmute` of owned values already checks, but the reference upcast -/// (`&a -> &b`) is pointer-sized regardless, so without this a size mismatch -/// there would be silent UB; alignment is never checked by `transmute` at all. +/// A `const`-block guard, co-located with every unsafe `transmute` (widening, +/// narrowing, and satellite), that fails compilation unless `a` and `b` share +/// size *and* alignment. Size equality is what `transmute` of owned values +/// already checks, but a *reference* transmute (`&a -> &b`) is pointer-sized +/// regardless, so without this a size mismatch there would be silent UB; +/// alignment is never checked by `transmute` at all. It guards *total* size and +/// align only — per-field layout identity additionally rests on the de-facto +/// reliance documented at the module level. fn layout_assert(a: &Ident, b: &Ident) -> TokenStream { quote! { const { @@ -89,16 +116,15 @@ fn layout_assert(a: &Ident, b: &Ident) -> TokenStream { ::core::mem::size_of::<#a>() == ::core::mem::size_of::<#b>() && ::core::mem::align_of::<#a>() == ::core::mem::align_of::<#b>(), "ty_family: family members must share size and alignment for a \ - zero-cost transmute upcast", + zero-cost transmute conversion", ); }; } } -/// `RuntimeTy` → `as_runtime_ty`, `Ty` → `as_ty`: the borrowed-upcast accessor -/// name for widening *to* `sup`. -fn as_method_name(sup: &Ident) -> Ident { - let s = sup.to_string(); +/// `RuntimeTy` → `runtime_ty`. +fn snake_ident(id: &Ident) -> String { + let s = id.to_string(); let mut snake = String::with_capacity(s.len() + 4); for (i, ch) in s.char_indices() { if ch.is_ascii_uppercase() { @@ -110,13 +136,21 @@ fn as_method_name(sup: &Ident) -> Ident { snake.push(ch); } } - format_ident!("as_{}", snake) + snake +} + +/// `RuntimeTy` → `as_runtime_ty`: the borrowed-*upcast* accessor for widening +/// `&sub -> &sup`. (Narrowing is the fallible `TryFrom<&sup> for &sub`, not an +/// inherent method.) +fn as_method_name(target: &Ident) -> Ident { + format_ident!("as_{}", snake_ident(target)) } /// Widen `sub` → `sup` (both deep, equal-size) by `transmute`. The owned form is /// a genuine zero-cost move of the whole value; the by-reference form widens the /// borrow and clones through it (so it composes with the recursion but is not -/// itself zero-cost — callers wanting that reach for [`as_method_name`]). +/// itself zero-cost — for a zero-cost borrow, the generated `as_()` +/// accessor is emitted separately by `gen_as_upcast`). fn gen_widen_transmute(family: &Family, sub: usize, sup: usize) -> TokenStream { let sub_name = &family.members[sub].name; let sup_name = &family.members[sup].name; @@ -129,9 +163,12 @@ fn gen_widen_transmute(family: &Family, sub: usize, sup: usize) -> TokenStream { // variant of `#sub_name` exists in `#sup_name` at the same // `#[repr(C, u8)]` discriminant. Both are equal size + align (the // `const` assert above), and the recursion travels through - // `Box`/`Vec`, so nested `#sub_name` children are likewise valid - // `#sup_name` when read back. The move transfers ownership of the - // heap allocations unchanged; dealloc uses the identical layout. + // `Box`/`Vec`/tuple positions whose in-memory layout depends only + // on the element's (here equal) size + align, so nested + // `#sub_name` children are likewise valid `#sup_name` when read + // back — a de-facto layout guarantee, not a language-level one + // (see the module docs). The move transfers ownership of the heap + // allocations unchanged; dealloc uses the identical layout. unsafe { ::core::mem::transmute::<#sub_name, #sup_name>(value) } } } @@ -173,6 +210,325 @@ fn gen_as_upcast(family: &Family, sub: usize, sup: usize) -> TokenStream { } } +// ── Narrowing by validate-then-transmute ───────────────────────────────────── +// +// For a deep, equal-size pair, narrowing `sup -> sub` is a single read-only walk +// that proves every node (at every depth) is representable in `sub`, followed by +// a `transmute` that reuses the already-allocated tree in place. This replaces +// the structural rebuild's per-node `Box::new`/`collect` allocations with zero. +// The walk itself is unavoidable — a single illegal variant nested anywhere +// would make the `transmute` UB — so completeness is the correctness obligation: +// [`gen_validators`] descends through every `Box`/`Vec`/`Option`/tuple/satellite +// position. + +/// Emit the three `TryFrom` narrowings for a deep, equal-size pair, all backed +/// by a single validation walk: owned (`Sup -> Sub`, validate then move), by-ref +/// to owned (`&Sup -> Sub`, validate then clone), and the zero-copy +/// borrow-to-borrow (`&Sup -> &Sub`, validate then reinterpret the reference in +/// place). The last recovers the `Not` reason on failure — the reason a +/// plain `Option`-returning accessor cannot. +fn gen_narrow_transmute(family: &Family, sub: usize, sup: usize) -> TokenStream { + let sub_name = &family.members[sub].name; + let sup_name = &family.members[sup].name; + let err = format_ident!("Not{}", sub_name); + let vfn = enum_validator_name(family, sub, sup); + let validators = gen_validators(family, sub, sup); + let assert = layout_assert(sub_name, sup_name); + let ref_doc = format!( + " Narrow `&{sup_name}` to `&{sub_name}` without copying, if every nested\n\ + variant is representable in `{sub_name}`.\n\n\ + Walks once to validate, then reinterprets the borrow in place — no\n\ + allocation. The borrow-preserving analogue of the owned\n\ + `{sub_name}::try_from`; `Err` carries the offending variant." + ); + quote! { + #validators + + impl ::core::convert::TryFrom<#sup_name> for #sub_name { + type Error = #err; + fn try_from(value: #sup_name) -> ::core::result::Result { + #assert + #vfn(&value)?; + // SAFETY: the walk above proved every node is a valid `#sub_name` + // variant, and the two types are equal size + align (asserted), + // so the owned tree is bit-valid as `#sub_name`. The move reuses + // its heap allocations unchanged. + ::core::result::Result::Ok(unsafe { + ::core::mem::transmute::<#sup_name, #sub_name>(value) + }) + } + } + + impl ::core::convert::TryFrom<&#sup_name> for #sub_name { + type Error = #err; + fn try_from(value: &#sup_name) -> ::core::result::Result { + #assert + #vfn(value)?; + // A borrow can't be moved out, so clone the validated tree and + // reinterpret the owned copy. + ::core::result::Result::Ok(unsafe { + ::core::mem::transmute::<#sup_name, #sub_name>(value.clone()) + }) + } + } + + #[doc = #ref_doc] + impl<'a> ::core::convert::TryFrom<&'a #sup_name> for &'a #sub_name { + type Error = #err; + fn try_from(value: &'a #sup_name) -> ::core::result::Result { + #assert + #vfn(value)?; + // SAFETY: the walk proved every node is a valid `#sub_name`, and + // the two types are equal size + align (asserted). A shared borrow + // reinterpreted in place, lifetime preserved. + ::core::result::Result::Ok(unsafe { + ::core::mem::transmute::<&#sup_name, &#sub_name>(value) + }) + } + } + } +} + +/// The module-private validation functions for a pair: one for the enum and one +/// per satellite. Each returns `Ok(())` iff its argument is representable in +/// `sub` at every depth; they recurse into one another to cover the whole tree. +fn gen_validators(family: &Family, sub: usize, sup: usize) -> TokenStream { + let sup_name = &family.members[sup].name; + let err = format_ident!("Not{}", &family.members[sub].name); + let vfn = enum_validator_name(family, sub, sup); + let body = validate_body(family, sub, sup); + let reachable = reachable_satellites(family, sub); + let sats = family + .satellites + .iter() + .filter(|sat| reachable.contains(&sat.name.to_string())) + .map(|sat| gen_sat_validator(family, sub, sup, sat)); + quote! { + fn #vfn(value: &#sup_name) -> ::core::result::Result<(), #err> { + #body + } + #(#sats)* + } +} + +fn validate_body(family: &Family, sub: usize, sup: usize) -> TokenStream { + let sup_name = &family.members[sup].name; + let sub_includes = &family.members[sub].includes; + let arms = member_variants(family, &family.members[sup]).map(|v| { + validate_arm( + family, + sub, + sup, + sup_name, + v, + sub_includes.contains(&v.axis), + ) + }); + quote! { match value { #(#arms),* } } +} + +/// One match arm of an enum validator. A variant absent from `sub` fails by +/// name; a present one binds just its recursion-bearing fields and checks each. +fn validate_arm( + family: &Family, + sub: usize, + sup: usize, + sup_name: &Ident, + v: &MVariant, + in_sub: bool, +) -> TokenStream { + let id = &v.ident; + if !in_sub { + let name = id.to_string(); + let err = format_ident!("Not{}", &family.members[sub].name); + let skip = match &v.fields { + Fields::Unit => quote!(), + Fields::Unnamed(_) => quote!((..)), + Fields::Named(_) => quote!({ .. }), + }; + return quote! { + #sup_name::#id #skip => ::core::result::Result::Err(#err { variant: #name }) + }; + } + match &v.fields { + Fields::Unit => quote! { #sup_name::#id => ::core::result::Result::Ok(()) }, + Fields::Named(n) => { + let mut binds = Vec::new(); + let mut checks = Vec::new(); + for f in &n.named { + if contains_recursion(family, &f.ty) { + let fid = f.ident.as_ref().unwrap(); + binds.push(quote!(#fid)); + checks.push(validate_expr(family, sub, sup, &f.ty, quote!(#fid))); + } + } + quote! { + #sup_name::#id { #(#binds,)* .. } => { #(#checks?;)* ::core::result::Result::Ok(()) } + } + } + Fields::Unnamed(u) => { + let mut pats = Vec::new(); + let mut checks = Vec::new(); + for (i, f) in u.unnamed.iter().enumerate() { + if contains_recursion(family, &f.ty) { + let b = format_ident!("f{}", i); + checks.push(validate_expr(family, sub, sup, &f.ty, quote!(#b))); + pats.push(quote!(#b)); + } else { + pats.push(quote!(_)); + } + } + quote! { + #sup_name::#id ( #(#pats),* ) => { #(#checks?;)* ::core::result::Result::Ok(()) } + } + } + } +} + +/// A `Result<(), NotSub>` expression checking that the value behind `binding` (a +/// reference to a value of master-type `ty`) is representable in `sub`. Leaves +/// are trivially `Ok`; recursion-bearing shapes descend, terminating in a call +/// to the relevant validator. +fn validate_expr( + family: &Family, + sub: usize, + sup: usize, + ty: &Type, + binding: TokenStream, +) -> TokenStream { + if !contains_recursion(family, ty) { + return quote! { ::core::result::Result::Ok(()) }; + } + if let Some(id) = single_ident(ty) { + if *id == family.master_ident { + let f = enum_validator_name(family, sub, sup); + return quote! { #f(#binding) }; + } + if let Some(sat) = family.satellites.iter().find(|s| s.name == *id) { + let f = sat_validator_name(family, sub, sup, sat); + return quote! { #f(#binding) }; + } + } + if let Some(inner) = wrapper_arg(ty, "Box") { + return validate_expr(family, sub, sup, inner, quote!(&**#binding)); + } + if let Some(inner) = wrapper_arg(ty, "Vec") { + let e = validate_expr(family, sub, sup, inner, quote!(__v)); + return quote! { #binding.iter().try_for_each(|__v| #e) }; + } + if let Some(inner) = wrapper_arg(ty, "Option") { + let e = validate_expr(family, sub, sup, inner, quote!(__v)); + return quote! { + match #binding { + ::core::option::Option::Some(__v) => #e, + ::core::option::Option::None => ::core::result::Result::Ok(()), + } + }; + } + if let Type::Tuple(t) = ty { + let checks = t + .elems + .iter() + .enumerate() + .filter(|(_, et)| contains_recursion(family, et)) + .map(|(i, et)| { + let idx = Index::from(i); + validate_expr(family, sub, sup, et, quote!(&#binding.#idx)) + }); + return quote! { { #(#checks?;)* ::core::result::Result::Ok(()) } }; + } + unsupported(ty) +} + +/// A satellite's validator: check every recursion-bearing field, then `Ok`. The +/// field binding is parenthesized so a `Vec`/`Box` field composes as +/// `(&value.f).iter()`, not `&(value.f.iter())`. +fn gen_sat_validator(family: &Family, sub: usize, sup: usize, sat: &Satellite) -> TokenStream { + let sup_sat = satellite_name_for(&family.members[sup].name, &sat.name); + let err = format_ident!("Not{}", &family.members[sub].name); + let vfn = sat_validator_name(family, sub, sup, sat); + let checks = sat + .fields + .iter() + .filter(|f| contains_recursion(family, &f.ty)) + .map(|f| { + let fid = f.ident.as_ref().unwrap(); + validate_expr(family, sub, sup, &f.ty, quote!((&value.#fid))) + }); + quote! { + fn #vfn(value: &#sup_sat) -> ::core::result::Result<(), #err> { + #(#checks?;)* + ::core::result::Result::Ok(()) + } + } +} + +/// Satellite names transitively reachable when validating a value as `sub`: only +/// variants present in `sub` are walked, so a satellite reached only through an +/// excluded variant (e.g. `Interface`, reachable solely via the `typevar` +/// `AssociatedTypeProjection`) needs no validator. Emitting one anyway would be +/// dead code — this prunes it. +fn reachable_satellites(family: &Family, sub: usize) -> HashSet { + let sat_names: HashSet = family + .satellites + .iter() + .map(|s| s.name.to_string()) + .collect(); + let collect = |ty: &dyn quote::ToTokens, out: &mut Vec| { + fn walk(ts: TokenStream, names: &HashSet, out: &mut Vec) { + for tt in ts { + match tt { + TokenTree::Ident(id) if names.contains(&id.to_string()) => { + out.push(id.to_string()); + } + TokenTree::Group(g) => walk(g.stream(), names, out), + _ => {} + } + } + } + walk(quote!(#ty), &sat_names, out); + }; + + let mut work: Vec = Vec::new(); + for v in &family.variants { + if family.members[sub].includes.contains(&v.axis) { + collect(&v.fields, &mut work); + } + } + let mut reached = HashSet::new(); + while let Some(name) = work.pop() { + if reached.insert(name.clone()) + && let Some(sat) = family.satellites.iter().find(|s| s.name == name) + { + for f in &sat.fields { + collect(&f.ty, &mut work); + } + } + } + reached +} + +/// `Ty` narrowing to `RuntimeTy` → `__valid_ty_as_runtime_ty`. Module-private and +/// unique per ordered pair. +fn enum_validator_name(family: &Family, sub: usize, sup: usize) -> Ident { + format_ident!( + "__valid_{}_as_{}", + snake_ident(&family.members[sup].name), + snake_ident(&family.members[sub].name), + ) +} + +/// The satellite analogue, e.g. `__valid_function_param_ty_as_runtime_function_param_ty`. +fn sat_validator_name(family: &Family, sub: usize, sup: usize, sat: &Satellite) -> Ident { + let sup_sat = satellite_name_for(&family.members[sup].name, &sat.name); + let sub_sat = satellite_name_for(&family.members[sub].name, &sat.name); + format_ident!( + "__valid_{}_as_{}", + snake_ident(&sup_sat), + snake_ident(&sub_sat) + ) +} + // ── Product order ──────────────────────────────────────────────────────────── fn subset(a: &[usize], b: &[usize]) -> bool {