Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 110 additions & 1 deletion baml_language/crates/baml_type/src/family.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -462,4 +462,113 @@ 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<T>(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()));
}

/// 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" })
);
}
}
18 changes: 9 additions & 9 deletions baml_language/crates/baml_type/src/runtime_ty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
Loading
Loading