From 5304e457fe6dda3f8a5de0c9b0d3176b32664795 Mon Sep 17 00:00:00 2001 From: Antonio Sarosi Date: Thu, 6 Aug 2026 06:06:08 +0200 Subject: [PATCH 1/2] runtime: mint type identity (BEP-066 s1 stack, PR 4) --- .../ns_type_reflection/type_reflection.baml | 34 ++-- .../baml_tests/tests/type_value_equality.rs | 80 +++++--- .../crates/baml_type/src/normalize.rs | 107 +++++++++++ .../crates/bex_engine/src/conversion.rs | 21 ++- baml_language/crates/bex_engine/src/lib.rs | 58 +++++- baml_language/crates/bex_heap/src/accessor.rs | 10 +- baml_language/crates/bex_heap/src/gc.rs | 14 +- baml_language/crates/bex_heap/src/heap.rs | 38 +++- baml_language/crates/bex_heap/src/tlab.rs | 14 +- .../crates/bex_vm/src/package_baml/ops.rs | 17 +- .../crates/bex_vm/src/package_baml/reflect.rs | 10 +- .../crates/bex_vm/src/package_baml/root.rs | 6 +- .../bex_vm/src/package_baml/type_class.rs | 12 +- baml_language/crates/bex_vm/src/vm.rs | 70 +++++-- .../crates/bex_vm/tests/load_type.rs | 37 ++-- .../bex_vm/tests/method_class_type_args.rs | 8 +- .../crates/bex_vm_types/src/types.rs | 2 + .../crates/bex_vm_types/src/types/object.rs | 72 ++++++- .../bex_vm_types/src/types/type_value.rs | 176 ++++++++++++++++++ 19 files changed, 652 insertions(+), 134 deletions(-) create mode 100644 baml_language/crates/bex_vm_types/src/types/type_value.rs diff --git a/baml_language/crates/baml_tests/baml_src/ns_type_reflection/type_reflection.baml b/baml_language/crates/baml_tests/baml_src/ns_type_reflection/type_reflection.baml index 553d4e925c3..ba56f7205aa 100644 --- a/baml_language/crates/baml_tests/baml_src/ns_type_reflection/type_reflection.baml +++ b/baml_language/crates/baml_tests/baml_src/ns_type_reflection/type_reflection.baml @@ -1,4 +1,5 @@ -// Tests for `Object::Type` structural equality via `baml.llm.get_return_type`. +// Tests for BEP-066 minted `Object::Type` identity via +// `baml.llm.get_return_type`. // // Converted from crates/baml_tests/tests/type_reflection.rs. // @@ -73,23 +74,14 @@ test "deep_equals_type_values_different" { assert.is_true(baml.deep_equals(baml.deep_equals(a, b), false)) } -// ─── CHARACTERIZATION: `==` vs `baml.deep_equals` on permuted unions ──────── +// ─── BEP-066 PR 4: unified type equality on permuted unions ───────────────── // -// These tests PIN A KNOWN INCONSISTENCY — they are not an endorsement of it. -// BAML currently has divergent equality implementations for `type` values: -// -// * `==` lowers through the `baml.ops.equals_equals` driver, which compares -// type values with `vm.equivalent(..)` — CANONICAL equivalence, so union -// member order does not matter (bex_vm/src/package_baml/ops.rs). -// * `baml.deep_equals` compares with the derived `PartialEq` on `RealizedTy` -// — SYNTACTIC equality, so union member order DOES matter -// (bex_vm/src/package_baml/root.rs). -// -// Both behave identically in test-block and function contexts (the historical -// context divergence was #3782's lowering bug, now fixed). The follow-up -// s1-mint-identity PR replaces all equality sites with a single mint-identity -// comparison; when it lands these pins must be updated together with it. -// Full diagnosis: thoughts/antonio/s1-vm-bug-diagnosis.md. +// PR 1 deliberately pinned the old divergence: `==` canonicalized while +// `baml.deep_equals` compared the `RealizedTy` payload syntactically. BEP-066 +// slice-1 PR 4 flips those pins: all three VM equality sites compare the same +// mint. Static mints digest the canonical form, so union order is irrelevant +// to both operations. Function and test-block contexts remain paired as the +// regression net for #3782. function union_order_eq_fn() -> bool { // Same comparison as "union_order_eq_canonical_in_test_block", function context. @@ -116,13 +108,13 @@ function union_order_deep_equals_fn() -> bool { } test "union_order_deep_equals_syntactic_in_function" { - // `baml.deep_equals` : permuted unions compare UNEQUAL (syntactic path), - // disagreeing with `==` above. Known bug, resolved by s1-mint-identity. - assert.is_true(baml.deep_equals(union_order_deep_equals_fn(), false)) + // Flipped in BEP-066 slice-1 PR 4: deep_equals now uses the same static + // canonical mint as `==`. + assert.is_true(baml.deep_equals(union_order_deep_equals_fn(), true)) } test "union_order_deep_equals_syntactic_in_test_block" { let a = type.of(); let b = type.of(); - assert.is_true(baml.deep_equals(baml.deep_equals(a, b), false)) + assert.is_true(baml.deep_equals(baml.deep_equals(a, b), true)) } diff --git a/baml_language/crates/baml_tests/tests/type_value_equality.rs b/baml_language/crates/baml_tests/tests/type_value_equality.rs index d9fbbca80bf..b2248c97b1f 100644 --- a/baml_language/crates/baml_tests/tests/type_value_equality.rs +++ b/baml_language/crates/baml_tests/tests/type_value_equality.rs @@ -1,22 +1,11 @@ -//! Function-context pins for equality on `type` values. +//! Function-context pins for BEP-066 minted equality on `type` values. //! -//! CHARACTERIZATION of a known inconsistency (do not read these as the -//! intended semantics): BAML currently has divergent equality -//! implementations for `type` values — -//! -//! * `==` lowers through the `baml.ops.equals_equals` driver and compares -//! type values with `vm.equivalent(..)` (canonical equivalence — union -//! member order is irrelevant): `bex_vm/src/package_baml/ops.rs`. -//! * `baml.deep_equals` uses the derived `PartialEq` on `RealizedTy` -//! (syntactic equality — union member order matters): -//! `bex_vm/src/package_baml/root.rs`. -//! -//! The follow-up s1-mint-identity PR replaces both with a single -//! mint-identity comparison; update these pins together with it. -//! Diagnosis: thoughts/antonio/s1-vm-bug-diagnosis.md. The matching -//! test-block-context pins live in -//! `baml_src/ns_type_reflection/type_reflection.baml` — both contexts must -//! agree (the historical context divergence was the #3782 lowering bug). +//! PR 1 characterized a known inconsistency: `==` canonicalized `RealizedTy` +//! while `baml.deep_equals` compared it syntactically. BEP-066 slice-1 PR 4 +//! deliberately flips the deep-equality pin. Every equality path now compares +//! the mint, and equivalent static spellings receive the same canonical digest. +//! Matching test-block pins live in +//! `baml_src/ns_type_reflection/type_reflection.baml`. use baml_tests::baml_test; use bex_engine::BexExternalValue; @@ -37,7 +26,7 @@ async fn permuted_union_double_equals_is_canonical() { } #[tokio::test] -async fn permuted_union_deep_equals_is_syntactic() { +async fn permuted_union_deep_equals_uses_the_canonical_mint() { let output = baml_test!( r#" function main() -> bool { @@ -47,7 +36,54 @@ async fn permuted_union_deep_equals_is_syntactic() { } "# ); - // Syntactic comparison: member order matters, disagreeing with `==` - // above. Known bug — resolved by s1-mint-identity. - assert_eq!(output.result, Ok(BexExternalValue::Bool(false))); + // Flipped in BEP-066 slice-1 PR 4: deep_equals agrees with `==` because + // both compare the same canonical static mint. + assert_eq!(output.result, Ok(BexExternalValue::Bool(true))); +} + +#[tokio::test] +async fn static_declaration_identity_survives_re_evaluation_and_a_helper_boundary() { + let output = baml_test!( + r#" + class Foo { value int } + + function foo_type() -> type { + type.of() + } + + function main() -> bool { + type.of() == type.of() + && type.of() == foo_type() + && foo_type() == foo_type() + } + "# + ); + assert_eq!(output.result, Ok(BexExternalValue::Bool(true))); +} + +#[tokio::test] +async fn of_value_reuses_the_static_class_identity() { + let output = baml_test!( + r#" + class Foo { value int } + + function main() -> bool { + let foo = Foo { value: 1 }; + type.of_value(foo) == type.of() + } + "# + ); + assert_eq!(output.result, Ok(BexExternalValue::Bool(true))); +} + +#[tokio::test] +async fn optional_and_explicit_null_union_share_a_static_identity() { + let output = baml_test!( + r#" + function main() -> bool { + type.of() == type.of() + } + "# + ); + assert_eq!(output.result, Ok(BexExternalValue::Bool(true))); } diff --git a/baml_language/crates/baml_type/src/normalize.rs b/baml_language/crates/baml_type/src/normalize.rs index 945c8e45254..bcd1c6e76d5 100644 --- a/baml_language/crates/baml_type/src/normalize.rs +++ b/baml_language/crates/baml_type/src/normalize.rs @@ -472,6 +472,113 @@ pub fn is_subtype(sub: &Ty, sup: &Ty, ctx: &C) -> bool { ctx.is_subtype(sub, sup) } +/// A deterministic 64-bit digest of `ty`'s **canonical form** under `ctx` — +/// the identity basis for statically-spelled runtime `type` values (BEP-066 +/// `MintId::Static`). +/// +/// Exact basis: canonicalize `ty` (the same walk [`TypeContext::equivalent`] +/// performs on each operand — unique representative of the equirecursive +/// equivalence class, so μ-recursion, union ordering, attr erasure, and every +/// context fact are already folded in), serialize its derived `Hash` token +/// stream with every numeric token in big-endian fixed width (`usize`/`isize` +/// widened to 64 bits), then feed those bytes into fixed-seed FNV-1a-64 +/// (offset basis `0xcbf29ce484222325`, prime `0x100000001b3`). Consequences: +/// +/// * `equivalent(a, b, ctx)` ⟹ `canonical_digest(a, ctx) == +/// canonical_digest(b, ctx)` — equivalent spellings (`string?` vs +/// `string | null`, permuted unions, renamed recursive aliases) share a +/// digest. The converse holds up to 64-bit collision odds, which the mint +/// design accepts (a collision over-equates two *static* types). +/// * The digest hashes only value data (names as strings, structure, de +/// Bruijn indices — canonical `NormalTy` equality is α-invariant and its +/// display metadata is hash-transparent). No pointers, no interner state: +/// two processes running the same build over the same program facts produce +/// identical digests. +/// * The digest is **not** an on-wire format (BEP-066 H-4: identity never +/// crosses the boundary). It may change across compiler versions — nothing +/// may persist it; a decoded type value re-derives its mint. +/// * Determinism requires only that `ctx` answers from immutable program +/// facts, as the VM's context does; digests minted under *different* fact +/// sets (e.g. a fact-free boundary context) agree exactly when +/// canonicalization never consults a fact that differs. +pub fn canonical_digest(ty: &Ty, ctx: &C) -> u64 { + /// FNV-1a, 64-bit. Local on purpose: the digest contract above is this + /// exact algorithm; routing through a swappable `Hasher` dependency would + /// invite silently changing the basis. + struct Fnv1a(u64); + + impl std::hash::Hasher for Fnv1a { + fn finish(&self) -> u64 { + self.0 + } + + fn write(&mut self, bytes: &[u8]) { + for byte in bytes { + self.0 ^= u64::from(*byte); + self.0 = self.0.wrapping_mul(0x100_0000_01b3); + } + } + + // `Hasher`'s default integer methods use native-endian bytes, which + // would make the digest architecture-dependent. Override the complete + // numeric surface so the derived `Hash` walk becomes a canonical byte + // serialization. Lengths and enum discriminants written as pointer- + // sized integers are widened, making 32- and 64-bit processes agree. + fn write_u8(&mut self, value: u8) { + self.write(&value.to_be_bytes()); + } + + fn write_u16(&mut self, value: u16) { + self.write(&value.to_be_bytes()); + } + + fn write_u32(&mut self, value: u32) { + self.write(&value.to_be_bytes()); + } + + fn write_u64(&mut self, value: u64) { + self.write(&value.to_be_bytes()); + } + + fn write_u128(&mut self, value: u128) { + self.write(&value.to_be_bytes()); + } + + fn write_usize(&mut self, value: usize) { + self.write_u64(value as u64); + } + + fn write_i8(&mut self, value: i8) { + self.write(&value.to_be_bytes()); + } + + fn write_i16(&mut self, value: i16) { + self.write(&value.to_be_bytes()); + } + + fn write_i32(&mut self, value: i32) { + self.write(&value.to_be_bytes()); + } + + fn write_i64(&mut self, value: i64) { + self.write(&value.to_be_bytes()); + } + + fn write_i128(&mut self, value: i128) { + self.write(&value.to_be_bytes()); + } + + fn write_isize(&mut self, value: isize) { + self.write_i64(value as i64); + } + } + + let canonical = NormalTy::canonical(ty, ctx); + let mut hasher = Fnv1a(0xcbf2_9ce4_8422_2325); + std::hash::Hash::hash(&canonical, &mut hasher); + std::hash::Hasher::finish(&hasher) +} + /// True only when `a` and `b` provably canonicalize to different forms, judged /// from their outermost constructor alone — a cheap reject for [`TypeContext:: /// equivalent`] that skips the two canonicalization walks on the common diff --git a/baml_language/crates/bex_engine/src/conversion.rs b/baml_language/crates/bex_engine/src/conversion.rs index 946aba787df..4295b7c0b9c 100644 --- a/baml_language/crates/bex_engine/src/conversion.rs +++ b/baml_language/crates/bex_engine/src/conversion.rs @@ -5,7 +5,7 @@ //! representation (`BexValue`, `BexExternalValue`). use ::bex_heap::{BexValue, HeapPermit, PermitProof, TlabHolder}; -use ::bex_vm_types::{HeapPtr, Object, ObjectType, RootHaver, Value, ValueKind}; +use ::bex_vm_types::{HeapPtr, Object, ObjectType, Value, ValueKind}; use baml_type::{Literal, Ty}; use bex_external_types::{ BexExternalAdt, BexExternalValue, HostValueKind, RuntimeTy, UnionMetadata, @@ -13,7 +13,7 @@ use bex_external_types::{ }; use bex_vm::BexVm; -use crate::{BexEngine, EngineError}; +use crate::{BexEngine, EngineError, thread::BexThread}; /// Narrow a host-supplied [`RuntimeTy`] to the [`baml_type::RealizedTy`] the VM /// heap stores for a value's type (an array's element type, a map's key/value @@ -376,8 +376,9 @@ impl BexEngine { }), Object::Bigint(bi) => Ok(BexExternalValue::Bigint((**bi).clone())), Object::Collector(c) => Ok(BexExternalValue::Adt(BexExternalAdt::Collector(c.clone()))), - Object::Type(ty) => Ok(BexExternalValue::Adt(BexExternalAdt::Type( - (**ty).clone().into(), + // Identity never crosses the host boundary (BEP-066 H-4). + Object::Type(type_value) => Ok(BexExternalValue::Adt(BexExternalAdt::Type( + type_value.ty.clone().into(), ))), Object::Uint8Array(bytes) => Ok(BexExternalValue::Uint8Array(bytes.to_vec())), Object::RustData(arc) => Ok(bex_external_types::try_convert_rust_data(arc) @@ -713,9 +714,9 @@ impl BexEngine { /// external input — from `--json-args`, language bindings, or buggy /// sys ops — surfaces as a graceful error instead of crashing the /// process. - pub(crate) fn convert_external_to_vm_value( + pub(crate) fn convert_external_to_vm_value( &self, - holder: &mut impl HeapPermit, + holder: &mut impl HeapPermit, external: BexExternalValue, ) -> Result { // Default: no declared-type context. Inbound `HostValue` arguments @@ -731,9 +732,9 @@ impl BexEngine { /// context for their payload subtree; unannotated children inherit their /// list, map, or class-field type from the parent. This also lets nested /// `BexExternalValue::HostValue` values bind to an [`Object::HostClosure`]. - pub(crate) fn convert_external_to_vm_value_with_ty( + pub(crate) fn convert_external_to_vm_value_with_ty( &self, - holder: &mut impl HeapPermit, + holder: &mut impl HeapPermit, external: BexExternalValue, expected_ty: Option<&RuntimeTy>, ) -> Result { @@ -977,7 +978,9 @@ impl BexEngine { } BexExternalValue::Adt(BexExternalAdt::Type(ty)) => { let ty = realize_host_ty(ty)?; - Value::object(holder.holder_mut().tlab_mut().alloc_type(ty)) + // The wire carries the definition only (H-4); derive a fresh + // static identity with the receiving VM's complete fact context. + Value::object(holder.holder_mut().vm.alloc_static_type(ty)) } BexExternalValue::Adt(BexExternalAdt::PromptAst(_)) => { return Err(EngineError::CannotConvert { diff --git a/baml_language/crates/bex_engine/src/lib.rs b/baml_language/crates/bex_engine/src/lib.rs index ba6c6c0a752..97ed0e98af5 100644 --- a/baml_language/crates/bex_engine/src/lib.rs +++ b/baml_language/crates/bex_engine/src/lib.rs @@ -978,7 +978,7 @@ fn truncate_preview(mut value: String, max_chars: usize) -> String { } /// Extract an owned `RuntimeTy` from a `SysOp::BamlHostCallHostValue` type-arg operand -/// (an `Object::Type(Box)`). +/// (an `Object::Type(Box)`). /// /// The VM packs the sys-op args as `[handle, args_array, ret_ty, throws_ty]` /// (see `bex_vm::vm`'s `CallIndirect`-`HostClosure` path): `ret_ty` is @@ -1012,7 +1012,7 @@ fn host_call_type_arg( // pointer would dangle. The caller clones the `RuntimeTy` out before awaiting. match unsafe { ptr.get() } { // `Object::Type` stores a realized type; widen it to the boundary `RuntimeTy`. - Object::Type(ty) => Ok((**ty).clone().into()), + Object::Type(type_value) => Ok(type_value.ty.clone().into()), _ => Err(bad_slot()), } } @@ -5784,3 +5784,57 @@ mod concurrent_tests { // ``` } } + +#[cfg(test)] +mod mint_identity_tests { + use std::sync::Arc; + + use baml_project::testing::compile_source; + use bex_vm_types::{Object, types::MintId}; + use sys_native::SysOpsExt; + use tokio_util::sync::CancellationToken; + + use super::BexEngine; + + fn engine() -> Arc { + let program = compile_source("function main() -> null { null }"); + Arc::new( + BexEngine::new(program, Arc::new(sys_native::SysOps::native()), Vec::new()) + .expect("engine construction should succeed"), + ) + } + + async fn mint_in_engine(engine: &Arc, ty: baml_type::RealizedTy) -> MintId { + let mut thread = engine + .new_root_thread(CancellationToken::new(), false) + .await; + let ptr = thread.vm.alloc_static_type(ty); + let Object::Type(type_value) = thread.vm.get_object(ptr) else { + panic!("alloc_static_type must allocate Object::Type") + }; + type_value.mint() + } + + #[tokio::test] + async fn static_digest_is_canonical_and_deterministic_across_engines() { + let left = baml_type::RealizedTy::Union( + vec![ + baml_type::RealizedTy::int(), + baml_type::RealizedTy::string(), + ], + baml_type::TyAttr::default(), + ); + let right = baml_type::RealizedTy::Union( + vec![ + baml_type::RealizedTy::string(), + baml_type::RealizedTy::int(), + ], + baml_type::TyAttr::default(), + ); + + let first = mint_in_engine(&engine(), left).await; + let second = mint_in_engine(&engine(), right).await; + assert_eq!(first, second); + assert!(matches!(first, MintId::Static(_))); + } +} diff --git a/baml_language/crates/bex_heap/src/accessor.rs b/baml_language/crates/bex_heap/src/accessor.rs index c0f3f0b2baf..661bcc42353 100644 --- a/baml_language/crates/bex_heap/src/accessor.rs +++ b/baml_language/crates/bex_heap/src/accessor.rs @@ -317,14 +317,15 @@ impl<'a> BexValue<'a> { ) -> Result { fn from_ptr(ptr: &HeapPtr) -> Result { let obj = unsafe { ptr.get() }; - let Object::Type(ty) = obj else { + let Object::Type(tv) = obj else { return Err(AccessError::TypeMismatch { expected: "type", actual: obj.to_string(), }); }; // `Object::Type` stores a realized type; widen it into `RuntimeTy`. - Ok((**ty).clone().into()) + // The mint stays behind (BEP-066 H-4: identity never crosses). + Ok(tv.ty.clone().into()) } match self { @@ -609,8 +610,9 @@ fn convert_object( }) } Object::Collector(c) => Ok(BexExternalValue::Adt(BexExternalAdt::Collector(c.clone()))), - Object::Type(ty) => Ok(BexExternalValue::Adt(BexExternalAdt::Type( - (**ty).clone().into(), + // Only the described type crosses the boundary (BEP-066 H-4). + Object::Type(tv) => Ok(BexExternalValue::Adt(BexExternalAdt::Type( + tv.ty.clone().into(), ))), Object::Bigint(bi) => Ok(BexExternalValue::Bigint((**bi).clone())), Object::Uint8Array(bytes) => Ok(BexExternalValue::Uint8Array(bytes.to_vec())), diff --git a/baml_language/crates/bex_heap/src/gc.rs b/baml_language/crates/bex_heap/src/gc.rs index eaef4b294b3..5c5892434d1 100644 --- a/baml_language/crates/bex_heap/src/gc.rs +++ b/baml_language/crates/bex_heap/src/gc.rs @@ -2387,15 +2387,19 @@ mod tests { fn test_gc_leaf_type_preserved() { let heap = BexHeap::new(vec![]); let mut tlab = Tlab::new(Arc::clone(&heap)); - let ptr = tlab.alloc(Object::Type(Box::new(baml_type::RealizedTy::Int { - attr: baml_type::TyAttr::default(), - }))); + let minted = bex_vm_types::types::TypeValue::from_parts( + baml_type::RealizedTy::int(), + bex_vm_types::types::MintId::Static(0xC0FFEE), + ); + let ptr = tlab.alloc(Object::Type(Box::new(minted.clone()))); let (_, new_roots, _) = unsafe { heap.collect_garbage(&[ptr]) }; - let Object::Type(ty) = (unsafe { new_roots[0].get() }) else { + let Object::Type(tv) = (unsafe { new_roots[0].get() }) else { panic!("not type") }; - assert!(matches!(**ty, baml_type::RealizedTy::Int { .. })); + assert!(matches!(tv.ty, baml_type::RealizedTy::Int { .. })); + // The mint is inline data, so a GC copy preserves identity (I-4). + assert_eq!(**tv, minted); } #[test] diff --git a/baml_language/crates/bex_heap/src/heap.rs b/baml_language/crates/bex_heap/src/heap.rs index 51fdbda1bf6..fbf6f4d8978 100644 --- a/baml_language/crates/bex_heap/src/heap.rs +++ b/baml_language/crates/bex_heap/src/heap.rs @@ -17,7 +17,7 @@ use std::{ collections::HashMap, sync::{ Arc, Mutex, RwLock, - atomic::{AtomicUsize, Ordering}, + atomic::{AtomicU64, AtomicUsize, Ordering}, }, }; @@ -181,6 +181,16 @@ pub struct BexHeap { /// Next handle key to allocate. next_handle_key: AtomicUsize, + /// Next `MintId::Runtime` counter value (BEP-066 I-1): one per structured + /// type construction, engine-wide. Lives on the heap — next to the handle + /// counter — because the heap is the one object every allocation path + /// already shares, including spawned VMs (each `Tlab` holds an + /// `Arc`), so two threads can never mint the same runtime + /// identity. Monotonic and never reused; no producer exists until the + /// slice-2 constructors, but the allocator lands with the identity + /// semantics (`bex_vm_types::types::MintId`). + next_runtime_mint: AtomicU64, + /// BEP-042: instances whose `cleanup` finalizer must run after the current /// collection. Populated during a collection (`copy_collection` / /// `copy_collection_minor`) when a dead-but-not-yet-cleaned instance of a @@ -347,6 +357,7 @@ impl BexHeap { gen2_cards: UnsafeCell::new(CardTable::new()), handles: RwLock::new(HashMap::new()), next_handle_key: AtomicUsize::new(0), + next_runtime_mint: AtomicU64::new(0), pending_finalizers: Mutex::new(Vec::new()), pending_unhandled_spawn_errors: Mutex::new(Vec::new()), has_finalizable_classes, @@ -1085,6 +1096,16 @@ impl BexHeap { Handle::new(handle_key, Arc::clone(self) as Arc) } + /// Mint a fresh `MintId::Runtime` identity (BEP-066 I-1): the next value + /// of the engine-wide monotonic counter. Every VM sharing this heap — + /// including spawned children — draws from the same counter, so two + /// constructor evaluations can never mint the same identity. `Relaxed` + /// suffices: uniqueness needs only the atomicity of `fetch_add`, no + /// ordering with other memory. + pub fn mint_runtime_id(&self) -> bex_vm_types::types::MintId { + bex_vm_types::types::MintId::Runtime(self.next_runtime_mint.fetch_add(1, Ordering::Relaxed)) + } + /// Collect all handle roots for garbage collection. /// /// Returns a Vec of HeapPtr values for all live handles. @@ -1214,6 +1235,21 @@ mod tests { assert!(stats.total_objects >= 51); // Expanded for TLAB } + #[test] + fn runtime_mints_are_heap_wide_and_disjoint_from_static_mints() { + use bex_vm_types::types::MintId; + + let heap = BexHeap::new(vec![]); + let shared = Arc::clone(&heap); + + let first = heap.mint_runtime_id(); + let second = shared.mint_runtime_id(); + assert_eq!(first, MintId::Runtime(0)); + assert_eq!(second, MintId::Runtime(1)); + assert_ne!(first, second); + assert_ne!(first, MintId::Static(0)); + } + // Note: Handle tests removed as they require HeapPtr creation which depends // on runtime allocation. Will be updated when full integration is complete. } diff --git a/baml_language/crates/bex_heap/src/tlab.rs b/baml_language/crates/bex_heap/src/tlab.rs index 9f774784a03..9d79bad862a 100644 --- a/baml_language/crates/bex_heap/src/tlab.rs +++ b/baml_language/crates/bex_heap/src/tlab.rs @@ -243,9 +243,15 @@ impl Tlab { } /// Allocate a type descriptor object on the heap. + /// + /// Takes an assembled [`bex_vm_types::types::TypeValue`] — a type plus + /// its minted identity — so no allocation site can produce a mintless + /// type object. Static materialization inside the VM should go through + /// `BexVm::alloc_static_type`, which derives (and memoizes) the digest + /// with the VM as the fact context. #[inline] - pub fn alloc_type(&mut self, ty: baml_type::RealizedTy) -> HeapPtr { - self.alloc(Object::Type(Box::new(ty))) + pub fn alloc_type(&mut self, tv: bex_vm_types::types::TypeValue) -> HeapPtr { + self.alloc(Object::Type(Box::new(tv))) } /// Allocate a future object on the heap. @@ -390,8 +396,8 @@ pub trait TlabHolder { self.tlab_mut().alloc_collector(collector) } - fn alloc_type(&mut self, ty: baml_type::RealizedTy) -> HeapPtr { - self.tlab_mut().alloc_type(ty) + fn alloc_type(&mut self, tv: bex_vm_types::types::TypeValue) -> HeapPtr { + self.tlab_mut().alloc_type(tv) } fn alloc_future(&mut self, future: bex_vm_types::Future) -> HeapPtr { diff --git a/baml_language/crates/bex_vm/src/package_baml/ops.rs b/baml_language/crates/bex_vm/src/package_baml/ops.rs index 4645e420a81..b3f324f309b 100644 --- a/baml_language/crates/bex_vm/src/package_baml/ops.rs +++ b/baml_language/crates/bex_vm/src/package_baml/ops.rs @@ -19,7 +19,7 @@ use std::{ sync::Arc, }; -use baml_type::{Name, RealizedTy, TyAttr, TypeName, normalize::TypeContext}; +use baml_type::{Name, RealizedTy, TyAttr, TypeName}; use bex_str::BexStr; use bex_vm_types::{ HeapPtr, ValueKind, @@ -560,17 +560,10 @@ impl EqualsDriver { (Object::Collector(x), Object::Collector(y)) => step(Arc::ptr_eq(&x.0, &y.0)), (Object::Collector(_), _) => Cmp::NotEqual, - // Two `type` values are equal when they denote the same type. Compare - // through the *full* program context (`vm` as the `TypeContext`), not derived - // `==` nor the resolver's fact-opaque equivalence: this is user-facing type - // equality, so it must see nominal facts. Union member order is non-canonical - // (`type_of` ≡ `type_of`), and an - // interface-membership union absorbs (`type_of` ≡ `type_of` - // when `Sq` implements `Shape`). Using `vm` here is safe — this is a *client* - // of the resolver, not on its re-entrant path, so the membership lookup this - // may trigger bottoms out in the resolver's fact-opaque internals without - // looping. - (Object::Type(x), Object::Type(y)) => step(vm.equivalent(x.as_ty(), y.as_ty())), + // BEP-066: all `type` equality paths compare the minted identity. + // Canonically equivalent static spellings received the same digest + // when materialized; runtime constructions receive unique counters. + (Object::Type(x), Object::Type(y)) => step(x.mint() == y.mint()), (Object::Type(_), _) => Cmp::NotEqual, // `Sentinel` (heap_debug builds only) is an internal freed/uninit diff --git a/baml_language/crates/bex_vm/src/package_baml/reflect.rs b/baml_language/crates/bex_vm/src/package_baml/reflect.rs index 810e2697f7b..3003a72fc91 100644 --- a/baml_language/crates/bex_vm/src/package_baml/reflect.rs +++ b/baml_language/crates/bex_vm/src/package_baml/reflect.rs @@ -83,7 +83,7 @@ fn alloc_arg( Some(n) => Value::object(vm.alloc_string(n.as_str())), None => Value::object(vm.alloc_string(format!("$arg{position}"))), }; - let ty = Value::object(vm.tlab.alloc_type(ty)); + let ty = Value::object(vm.alloc_static_type(ty)); copy::reflect::Arg { name, r#type: ty }.to_value(vm) } @@ -124,8 +124,8 @@ fn signature_impl(vm: &mut BexVm, f_val: Value) -> Result } let args = Value::object(vm.tlab.alloc_array(ty_arg(), positional)); let opts = Value::object(vm.tlab.alloc_map(RealizedTy::string(), ty_arg(), opts)); - let returns = Value::object(vm.tlab.alloc_type(sig.ret.clone())); - let errors = Value::object(vm.tlab.alloc_type(sig.throws)); + let returns = Value::object(vm.alloc_static_type(sig.ret.clone())); + let errors = Value::object(vm.alloc_static_type(sig.throws)); let docstring = opt_string(vm, sig.docstring.as_ref()); let name = opt_string(vm, sig.name.as_ref()); Ok(copy::reflect::Signature { @@ -147,8 +147,8 @@ fn raise_invalid_argument( got: RealizedTy, ) -> NativeCallResult { let argument = Value::object(vm.alloc_string(argument)); - let expected = Value::object(vm.tlab.alloc_type(expected)); - let got = Value::object(vm.tlab.alloc_type(got)); + let expected = Value::object(vm.alloc_static_type(expected)); + let got = Value::object(vm.alloc_static_type(got)); let err = copy::reflect::InvalidArgumentError { argument, expected, diff --git a/baml_language/crates/bex_vm/src/package_baml/root.rs b/baml_language/crates/bex_vm/src/package_baml/root.rs index 9a94b1af469..c0c77c60a1e 100644 --- a/baml_language/crates/bex_vm/src/package_baml/root.rs +++ b/baml_language/crates/bex_vm/src/package_baml/root.rs @@ -905,6 +905,8 @@ fn deep_copy_value_recursive( Object::Future(_) => unreachable!("Future short-circuited above"), Object::UnscheduledFuture(f) => vm.tlab.alloc(Object::UnscheduledFuture(f)), Object::Collector(c) => vm.tlab.alloc(Object::Collector(c)), + // A deep copy denotes the same type value: clone the complete + // `TypeValue`, including its mint (BEP-066 I-1/I-4). Object::Type(ty) => vm.tlab.alloc(Object::Type(ty)), // Closures, bound methods, and cells are shallow-copied: the captured // state is shared by design (mutation semantics). @@ -1007,7 +1009,9 @@ fn deep_equals_recursive( a_var.enm == b_var.enm && a_var.index == b_var.index } - (Object::Type(a_ty), Object::Type(b_ty)) => a_ty == b_ty, + // BEP-066: deep equality agrees with `==` by comparing the + // stable mint, never the type payload or moving heap pointer. + (Object::Type(a_ty), Object::Type(b_ty)) => a_ty.mint() == b_ty.mint(), (Object::Enum(a_enum), Object::Enum(b_enum)) => { a_enum.name == b_enum.name diff --git a/baml_language/crates/bex_vm/src/package_baml/type_class.rs b/baml_language/crates/bex_vm/src/package_baml/type_class.rs index 0a6b922f96e..99a71c6462c 100644 --- a/baml_language/crates/bex_vm/src/package_baml/type_class.rs +++ b/baml_language/crates/bex_vm/src/package_baml/type_class.rs @@ -17,7 +17,7 @@ impl BamlNamespaceType for PackageBamlImpl { let ty = vm .value_concrete_ty(*v) .map_or_else(baml_type::RealizedTy::unknown, baml_type::RealizedTy::from); - Ok(Value::object(vm.tlab.alloc_type(ty))) + Ok(Value::object(vm.alloc_static_type(ty))) } } @@ -35,7 +35,7 @@ impl BamlClassTypeValue for PackageBamlImpl { return bex_str::BexStr::from(""); }; match vm.get_object(ptr) { - Object::Type(ty) => bex_str::BexStr::from(ty.to_string()), + Object::Type(type_value) => bex_str::BexStr::from(type_value.ty.to_string()), _ => bex_str::BexStr::from(""), } } @@ -111,7 +111,7 @@ impl BamlClassTypeValue for PackageBamlImpl { .collect(); entries .into_iter() - .map(|(ty, _, _)| Value::object(vm.tlab.alloc(Object::Type(Box::new(ty))))) + .map(|(ty, _, _)| Value::object(vm.alloc_static_type(ty))) .collect() } } @@ -120,7 +120,7 @@ impl BamlClassTypeValue for PackageBamlImpl { /// primitive, container, …), or `None` if `value` isn't a `type`. fn type_value_ty(vm: &BexVm, value: Value) -> Option { match vm.get_object(value.as_object_ptr()?) { - Object::Type(ty) => Some(ty.as_ref().clone()), + Object::Type(type_value) => Some(type_value.ty.clone()), _ => None, } } @@ -138,10 +138,10 @@ type RealizedTypeInstantiation = ( /// interface instantiations. fn ty_name_args_and_assoc(vm: &BexVm, value: Value) -> Option { let ptr = value.as_object_ptr()?; - let Object::Type(ty) = vm.get_object(ptr) else { + let Object::Type(type_value) = vm.get_object(ptr) else { return None; }; - match ty.as_ref() { + match &type_value.ty { baml_type::RealizedTy::Class(name, args, _) => { Some((name.clone(), args.clone(), Vec::new())) } diff --git a/baml_language/crates/bex_vm/src/vm.rs b/baml_language/crates/bex_vm/src/vm.rs index 560b3d1ba18..5d894f830f4 100644 --- a/baml_language/crates/bex_vm/src/vm.rs +++ b/baml_language/crates/bex_vm/src/vm.rs @@ -86,8 +86,8 @@ use bex_vm_types::{ StackIndex, UnaryOp, Value, Variant, VmGlobals, bytecode::{self, Instruction}, types::{ - BoundMethod, Closure, ConstValue, Function, FunctionOrigin, FunctionType, Instance, Type, - UnscheduledFuture, + BoundMethod, Closure, ConstValue, Function, FunctionOrigin, FunctionType, Instance, MintId, + Type, TypeValue, UnscheduledFuture, }, }; use indexmap::IndexMap; @@ -272,9 +272,9 @@ impl Frame { #[cfg(test)] pub(crate) mod tests { - use std::sync::Arc; #[cfg(not(target_arch = "wasm32"))] use std::sync::atomic::AtomicBool; + use std::{collections::HashMap, sync::Arc}; use bex_heap::{BexHeap, CollectionLevel, Tlab}; use bex_vm_types::{ @@ -330,6 +330,7 @@ pub(crate) mod tests { id_overrides: Vec::new(), argv: Arc::from([]), pending_call_type_args: Vec::new(), + static_mint_cache: HashMap::new(), packages: Arc::new(crate::package_load::PackageIndex::default()), } } @@ -752,6 +753,16 @@ pub struct BexVm { /// re-enter the VM (via `YieldToCall`) therefore see their own type-args /// even if the inner callback uses different ones. pending_call_type_args: Vec, + + /// Memo for `MintId::Static` digests (BEP-066), keyed by the *spelled* + /// `RealizedTy`. `LoadType` runs on every generic call, and the digest is + /// a canonicalization walk (`baml_type::normalize::canonical_digest` with + /// this VM as the fact context) — this cache skips re-walking repeated + /// spellings. Pure memoization: the digest is a deterministic function of + /// the spelling under this VM's immutable program facts, so a per-VM cache + /// (spawned VMs start empty) can never produce a divergent mint. Distinct + /// spellings of equivalent types get separate entries with equal digests. + static_mint_cache: HashMap, } /// VM execution state. @@ -1260,6 +1271,7 @@ impl BexVm { id_overrides: Vec::new(), argv, pending_call_type_args: Vec::new(), + static_mint_cache: HashMap::new(), packages, } } @@ -1277,6 +1289,28 @@ impl BexVm { &self.pending_call_type_args } + /// Materialize a statically described runtime `type` value. + /// + /// The mint is the deterministic digest of the type's canonical form with + /// this VM as the complete program-fact context. Digests are memoized by + /// spelling because `LoadType` can materialize the same template many times; + /// equivalent spellings may occupy separate cache entries, but derive the + /// same digest. All VM-side static type producers route through this method + /// so an `Object::Type` cannot be allocated without its identity. + pub fn alloc_static_type(&mut self, ty: baml_type::RealizedTy) -> HeapPtr { + let type_value = if let Some(&digest) = self.static_mint_cache.get(&ty) { + TypeValue::from_parts(ty, MintId::Static(digest)) + } else { + let type_value = TypeValue::static_new(ty, self); + let MintId::Static(digest) = type_value.mint() else { + unreachable!("TypeValue::static_new always creates a static mint") + }; + self.static_mint_cache.insert(type_value.ty.clone(), digest); + type_value + }; + self.tlab.alloc_type(type_value) + } + fn take_type_args( &mut self, start: usize, @@ -1290,10 +1324,10 @@ impl BexVm { for slot in start..end { let value = self.stack[StackIndex::from_raw(slot)]; let ptr = self.as_object_ptr(value, ObjectType::Type)?; - let Object::Type(ty) = self.get_object(ptr) else { + let Object::Type(type_value) = self.get_object(ptr) else { unreachable!("as_object_ptr guarantees Type variant"); }; - type_args.push(*ty.clone()); + type_args.push(type_value.ty.clone()); } drop( self.stack @@ -1759,7 +1793,7 @@ impl BexVm { let value = self.stack.ensure_pop(); let ptr = self.as_object_ptr(value, ObjectType::Type)?; match self.get_object(ptr) { - Object::Type(ty) => Ok(*ty.clone()), + Object::Type(type_value) => Ok(type_value.ty.clone()), other => Err(VmInternalError::TypeError { expected: ObjectType::Type.into(), got: ObjectType::of(other).into(), @@ -2522,7 +2556,7 @@ impl BexVm { match callable_kind { FunctionKind::Native(_) => { for ty in type_args { - let ty_ptr = self.tlab.alloc(Object::Type(Box::new(ty))); + let ty_ptr = self.alloc_static_type(ty); self.stack.push(Value::object(ty_ptr)); } self.stack.extend(args.iter().copied()); @@ -3418,7 +3452,7 @@ impl BexVm { ) -> Result<(baml_type::TypeName, Vec), VmError> { let iface_ptr = self.as_object_ptr(iface_value, ObjectType::Type)?; match self.get_object(iface_ptr) { - Object::Type(ty) => match ty.as_ref() { + Object::Type(type_value) => match &type_value.ty { baml_type::RealizedTy::Interface(qtn, args, _assoc, _attr) => { Ok((qtn.clone(), args.clone())) } @@ -4322,8 +4356,8 @@ impl BexVm { /// `baml.host.call_host_value` in `sys_ops/.../io_generated.rs`): /// args\[0\] = `handle` (`Object::HostClosure` → `BexExternalValue::HostValue`) /// args\[1\] = `args_pack` (`Object::Array` of `[positional: Object::Array, optional: Object::Map]`) - /// args\[2\] = `ret_ty` (`Object::Type`) — `type_arg_0` (`T`) - /// args\[3\] = `throws_ty` (`Object::Type`) — `type_arg_1` (`E`) + /// args\[2\] = `ret_ty` (`Object::Type`) — `type_arg_0` (`T`) + /// args\[3\] = `throws_ty` (`Object::Type`) — `type_arg_1` (`E`) /// /// TODO: `throws_ty` is packed here but the engine doesn't yet read it /// — a future phase will validate the host's thrown value against `E` @@ -4395,8 +4429,8 @@ impl BexVm { baml_type::RealizedTy::unknown(), vec![Value::object(positional_ptr), Value::object(optional_ptr)], ); - let ret_ty_ptr = self.tlab.alloc(Object::Type(Box::new(ret_ty))); - let throws_ty_ptr = self.tlab.alloc(Object::Type(Box::new(throws_ty))); + let ret_ty_ptr = self.alloc_static_type(ret_ty); + let throws_ty_ptr = self.alloc_static_type(throws_ty); // PR4b: host-closure calls ride the sys-op pair too. No Function // object backs them, so function_id 0 (unassigned). self.prof_enter_sysop(0, call_site, VmCaptureMask::disabled()); @@ -5346,8 +5380,10 @@ impl BexVm { } }), (Object::Type(lt), Object::Type(rt)) => Value::bool(match op { - CmpOp::Eq => lt == rt, - CmpOp::NotEq => lt != rt, + // BEP-066: compare the stable identity token. Static + // canonicalization and runtime freshness happen at minting. + CmpOp::Eq => lt.mint() == rt.mint(), + CmpOp::NotEq => lt.mint() != rt.mint(), _ => { return Err(VmInternalError::CannotApplyCmpOp { left: bex_vm_types::types::Type::Object(ObjectType::Type), @@ -6345,7 +6381,7 @@ impl BexVm { // `Converter` + `Converter`); non-generic // interfaces carry none and resolve by name + `Self`. // Associated types are outputs, not part of the key. - Object::Type(ty) => match ty.as_ref() { + Object::Type(type_value) => match &type_value.ty { baml_type::RealizedTy::Interface(qtn, args, _assoc, _attr) => { (qtn.clone(), args.clone()) } @@ -6983,7 +7019,7 @@ impl BexVm { } }; - let value = Value::object(self.alloc_type(ty)); + let value = Value::object(self.alloc_static_type(ty)); self.stack.push(value); } @@ -7027,7 +7063,7 @@ impl BexVm { let (iface_qtn, iface_args) = { let iface_ptr = self.as_object_ptr(iface_value, ObjectType::Type)?; match self.get_object(iface_ptr) { - Object::Type(ty) => match ty.as_ref() { + Object::Type(type_value) => match &type_value.ty { baml_type::RealizedTy::Interface(qtn, args, _assoc, _attr) => { (qtn.clone(), args.clone()) } diff --git a/baml_language/crates/bex_vm/tests/load_type.rs b/baml_language/crates/bex_vm/tests/load_type.rs index 755046e9cd0..6d4c0c0bb78 100644 --- a/baml_language/crates/bex_vm/tests/load_type.rs +++ b/baml_language/crates/bex_vm/tests/load_type.rs @@ -18,7 +18,7 @@ use bex_vm::{BexVm, VmExecState}; use bex_vm_types::{ ConstValue, FunctionCaptureProps, GlobalIndex, Instruction, Object, ObjectIndex, Value, bytecode::Bytecode, - types::{Function, FunctionKind, FunctionOrigin, Program}, + types::{Function, FunctionKind, FunctionOrigin, MintId, Program}, }; /// Minimal valid BAML source used as the base for all tests. @@ -121,7 +121,8 @@ fn run_with_bytecode_keep_vm( // ─── 3.1 & 3.5 ── LoadType with a fully-concrete template ─────────────────── /// `LoadType(k)` where `k` is a `ConstValue::Type(TyTemplate::from(int))` -/// should push an `Object::Type` whose inner `RuntimeTy` is `RuntimeTy::int()`. +/// should push an `Object::Type` whose `TypeValue` carries `RealizedTy::int()` +/// and a deterministic static mint. #[test] fn load_type_concrete_int() { let template = TyTemplate::from(baml_type::RealizedTy::int()); @@ -135,11 +136,14 @@ fn load_type_concrete_int() { panic!("expected Object, got {result:?}"); }; match vm.get_object(ptr) { - Object::Type(ty) => assert_eq!( - **ty, - RealizedTy::int(), - "LoadType(int) should materialise RealizedTy::int" - ), + Object::Type(type_value) => { + assert_eq!( + type_value.ty, + RealizedTy::int(), + "LoadType(int) should materialise RealizedTy::int" + ); + assert!(matches!(type_value.mint(), MintId::Static(_))); + } other => panic!("expected Object::Type, got {other:?}"), } } @@ -172,11 +176,11 @@ fn load_type_concrete_string_different_from_int() { }; let int_ty = match vm_int.get_object(p_int) { - Object::Type(ty) => (**ty).clone(), + Object::Type(type_value) => type_value.ty.clone(), other => panic!("expected Object::Type for int, got {other:?}"), }; let str_ty = match vm_str.get_object(p_str) { - Object::Type(ty) => (**ty).clone(), + Object::Type(type_value) => type_value.ty.clone(), other => panic!("expected Object::Type for string, got {other:?}"), }; @@ -241,9 +245,9 @@ fn load_type_type_arg_ref_substitutes_from_frame() { }; let obj = vm.get_object(ptr); match obj { - Object::Type(ty) => { + Object::Type(type_value) => { assert_eq!( - **ty, + type_value.ty, RealizedTy::string(), "TypeArgRef(0) should resolve to string" ); @@ -255,7 +259,8 @@ fn load_type_type_arg_ref_substitutes_from_frame() { // ─── 3.5 ── Composite template Array(TypeArgRef(0)) ───────────────────────── /// `TyTemplate::Array(TypeArgRef(0))` with `frame.type_args[0] = RuntimeTy::int()` -/// should produce `Object::Type(RuntimeTy::list(int))`. +/// should produce an `Object::Type` whose `TypeValue` carries +/// `RealizedTy::list(int)`. #[test] fn load_type_array_of_type_arg_ref() { let template = TyTemplate::list(TyTemplate::TypeArgRef(0)); @@ -301,9 +306,9 @@ fn load_type_array_of_type_arg_ref() { }; let obj = vm.get_object(ptr); match obj { - Object::Type(ty) => { + Object::Type(type_value) => { assert_eq!( - **ty, + type_value.ty, RealizedTy::list(RealizedTy::int()), "Array(TypeArgRef(0)) with int → int[]" ); @@ -386,9 +391,9 @@ fn call_ntypeargs_threads_type_arg_into_callee() { }; let obj = vm.get_object(ptr); match obj { - Object::Type(ty) => { + Object::Type(type_value) => { assert_eq!( - **ty, + type_value.ty, RealizedTy::string(), "inner function should receive RealizedTy::string() via type arg" ); diff --git a/baml_language/crates/bex_vm/tests/method_class_type_args.rs b/baml_language/crates/bex_vm/tests/method_class_type_args.rs index bbc66cf5a8b..b6d3b6bcb2a 100644 --- a/baml_language/crates/bex_vm/tests/method_class_type_args.rs +++ b/baml_language/crates/bex_vm/tests/method_class_type_args.rs @@ -249,9 +249,9 @@ fn method_frame_type_args_seeded_with_class_type_args() { panic!("expected Object, got {result:?}"); }; match vm.get_object(ptr) { - Object::Type(ty) => { + Object::Type(type_value) => { assert_eq!( - **ty, + type_value.ty, RealizedTy::int(), "TypeArgRef(0) with class_type_args=[int] should yield int" ); @@ -299,9 +299,9 @@ fn method_frame_type_args_seeded_string() { panic!("expected Object") }; match vm.get_object(ptr) { - Object::Type(ty) => { + Object::Type(type_value) => { assert_eq!( - **ty, + type_value.ty, RealizedTy::string(), "TypeArgRef(0) with class_type_args=[string] should yield string" ); diff --git a/baml_language/crates/bex_vm_types/src/types.rs b/baml_language/crates/bex_vm_types/src/types.rs index c8abb21f303..8308f5d56fa 100644 --- a/baml_language/crates/bex_vm_types/src/types.rs +++ b/baml_language/crates/bex_vm_types/src/types.rs @@ -15,6 +15,7 @@ mod future; mod interface; mod object; mod package; +mod type_value; mod value; use std::collections::HashMap; @@ -32,6 +33,7 @@ pub use interface::*; pub use object::*; pub use package::*; pub use tokio_util::sync::CancellationToken; +pub use type_value::*; pub use value::*; use crate::{heap_ptr::HeapPtr, indexable::ObjectPool}; diff --git a/baml_language/crates/bex_vm_types/src/types/object.rs b/baml_language/crates/bex_vm_types/src/types/object.rs index b880eae236e..1f41755987e 100644 --- a/baml_language/crates/bex_vm_types/src/types/object.rs +++ b/baml_language/crates/bex_vm_types/src/types/object.rs @@ -121,8 +121,13 @@ pub enum Object { /// Collector object (opaque handle to `bex_events::Collector`). Collector(CollectorRef), - /// A type descriptor value — wraps a `baml_type::RealizedTy`. - Type(Box), + /// A type descriptor value — wraps a [`crate::types::TypeValue`]: the + /// described `baml_type::RealizedTy` plus the minted identity that + /// `==`/hash compare (BEP-066). The mint is inline plain data, so a GC + /// copy or `baml.deep_copy` preserves identity; the wire form + /// (`ObjectWire::Type`) carries only the type — identity never crosses a + /// boundary (H-4) and is re-derived on decode. + Type(Box), #[cfg(feature = "heap_debug")] Sentinel(crate::types::SentinelKind), @@ -205,6 +210,12 @@ enum ObjectWire { // enum's size. Borsh treats `Box` transparently, so the wire form is // unchanged. UnscheduledFuture(Box), + /// Carries only the described type — never the mint (BEP-066 H-4: + /// identity does not cross a serialization boundary). Decode re-derives a + /// `Static` mint. No compiled `Program` bakes an `Object::Type` into its + /// object pool today (`ConstValue::Type` templates materialize through + /// the VM's `LoadType`), so this round trip is exercised only by + /// unit/link tooling. Type(Box), } @@ -238,7 +249,8 @@ impl BorshSerialize for Object { Self::Float(v) => ObjectWire::Float(*v), Self::Future(v) => ObjectWire::Future(v.clone()), Self::UnscheduledFuture(v) => ObjectWire::UnscheduledFuture(v.clone()), - Self::Type(v) => ObjectWire::Type(v.clone()), + // The mint stays behind (H-4) — only the described type crosses. + Self::Type(v) => ObjectWire::Type(Box::new(v.ty.clone())), Self::RustData(_) => { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, @@ -305,7 +317,24 @@ impl BorshDeserialize for Object { ObjectWire::Float(v) => Self::Float(v), ObjectWire::Future(v) => Self::Future(v), ObjectWire::UnscheduledFuture(v) => Self::UnscheduledFuture(v), - ObjectWire::Type(v) => Self::Type(v), + ObjectWire::Type(v) => { + // Re-derive the static mint (H-4: identity never rides the + // wire). Borsh decode has no fact source to consult, so the + // digest is fact-free here; that matches the VM's digest for + // every type whose canonical form needs no program fact + // (classes, enums, primitives, containers, plain unions). + // No compiled `Program` pools an `Object::Type` today — see + // the `ObjectWire::Type` doc — so a fact-dependent payload + // (recursive alias, absorbing union) cannot reach this path + // from real programs. + #[expect( + deprecated, + reason = "borsh decode is a boundary with no fact context to supply; \ + see the fact-free digest note above" + )] + let ctx = baml_type::normalize::NoFacts; + Self::Type(Box::new(crate::types::TypeValue::static_new(*v, &ctx))) + } }) } } @@ -349,7 +378,7 @@ impl std::fmt::Display for Object { ), Object::RustData(_) => write!(f, ""), Object::Collector(_) => write!(f, ""), - Object::Type(ty) => write!(f, ""), + Object::Type(tv) => write!(f, "", tv.ty), Object::Future(future) => write!(f, "{}", future.read()), Object::UnscheduledFuture(_) => write!(f, ""), Object::Float(v) => write!(f, "{v}"), @@ -463,3 +492,36 @@ impl std::fmt::Display for ObjectType { } } } + +#[cfg(test)] +mod type_wire_tests { + use borsh::BorshDeserialize; + + use super::*; + use crate::types::{MintId, TypeValue}; + + #[test] + fn type_wire_omits_identity_and_keeps_the_existing_payload_format() { + let ty = baml_type::RealizedTy::list(baml_type::RealizedTy::string()); + let object = Object::Type(Box::new(TypeValue::from_parts( + ty.clone(), + MintId::Runtime(91), + ))); + + let encoded_object = borsh::to_vec(&object).expect("type object serializes"); + let encoded_legacy_shape = + borsh::to_vec(&ObjectWire::Type(Box::new(ty.clone()))).expect("wire proxy serializes"); + assert_eq!( + encoded_object, encoded_legacy_shape, + "adding a mint must not change the ObjectWire::Type bytes" + ); + + let decoded = Object::try_from_slice(&encoded_object).expect("type object deserializes"); + let Object::Type(type_value) = decoded else { + panic!("decoded object should be a type") + }; + assert_eq!(type_value.ty, ty); + assert!(matches!(type_value.mint(), MintId::Static(_))); + assert_ne!(type_value.mint(), MintId::Runtime(91)); + } +} diff --git a/baml_language/crates/bex_vm_types/src/types/type_value.rs b/baml_language/crates/bex_vm_types/src/types/type_value.rs new file mode 100644 index 00000000000..efae9d6eada --- /dev/null +++ b/baml_language/crates/bex_vm_types/src/types/type_value.rs @@ -0,0 +1,176 @@ +//! The payload of a runtime `type` value: a described type plus its minted +//! identity (BEP-066 slice 1). +//! +//! Equality and hashing on a [`TypeValue`] are **exactly the mint** — never +//! the heap pointer (the GC is copying, so a pointer can never be an identity +//! token — I-4) and never a fresh structural walk of `ty` (the three +//! historically divergent equality paths this replaces). The mint is plain +//! data carried inline in the object, so GC copies and `baml.deep_copy` +//! preserve identity by construction (I-1: a copy *is* the same type value). + +use baml_type::{RealizedTy, normalize::TypeContext}; + +/// A minted identity token for a runtime `type` value. +/// +/// The enum discriminant participates in derived equality, so a `Static` and +/// a `Runtime` mint never compare equal — even on a raw `u64` collision — and +/// a constructed type can never alias a static declaration (I-1). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum MintId { + /// A static spelling (`type.of()`, a reflected signature, a wire-named + /// type): the deterministic canonical-form digest computed by + /// `baml_type::normalize::canonical_digest`. Equivalent static spellings + /// (`string?` vs `string | null`, permuted unions) share a digest, so + /// every reference to a static declaration is the same value with no + /// intern table (I-2), and re-materializations (a second `type.of()`, + /// a sys-op round trip) rebuild the same identity. + Static(u64), + /// One per constructor evaluation (I-1): allocated from the monotonic + /// engine-wide counter on `BexHeap` (`mint_runtime_id`), shared by + /// spawned VMs. No producer exists yet in slice 1 — the structured + /// constructors land in slice 2 — but the variant is part of the + /// equality/hash contract now so the semantics cannot drift. + Runtime(u64), +} + +/// What an `Object::Type` wraps: the described type and its identity. +/// +/// `==`/`Hash` are mint-only (see [`MintId`]); `ty` is carried data that the +/// VM reads for rendering, parsing, dispatch, and reflection. Two values with +/// different `ty` payloads and equal mints cannot arise from the constructors +/// below: a `Static` mint is a function of `ty`'s canonical form, and a +/// `Runtime` mint is globally unique. +#[derive(Debug, Clone)] +pub struct TypeValue { + /// The type this value denotes. + pub ty: RealizedTy, + mint: MintId, +} + +impl TypeValue { + /// Mint a type value for a **statically spelled** type: the mint is the + /// canonical-form digest of `ty` under `ctx`. + /// + /// `ctx` decides which facts the canonical form folds in (alias + /// expansion, union absorption); every site materializing types for the + /// same program must supply the same fact source — inside the VM that is + /// the VM itself (use `BexVm::alloc_static_type`, which also memoizes the + /// walk). + pub fn static_new(ty: RealizedTy, ctx: &C) -> Self { + let digest = baml_type::normalize::canonical_digest(ty.as_ty(), ctx); + Self { + ty, + mint: MintId::Static(digest), + } + } + + /// Assemble a type value from an already-derived mint. + /// + /// For the memoized static-digest path (`BexVm::alloc_static_type`), the + /// slice-2 runtime constructors (a counter mint from + /// `BexHeap::mint_runtime_id`), and tests pinning mint semantics. The + /// caller owns the invariant that `mint` was produced for `ty` — a + /// mismatched pair breaks type-value equality program-wide. + pub fn from_parts(ty: RealizedTy, mint: MintId) -> Self { + Self { ty, mint } + } + + /// This value's identity token. + pub fn mint(&self) -> MintId { + self.mint + } +} + +/// Identity comparison (I-1/I-2): the mint, nothing else. `ty` is deliberately +/// excluded — see the struct doc. +impl PartialEq for TypeValue { + fn eq(&self, other: &Self) -> bool { + self.mint == other.mint + } +} + +impl Eq for TypeValue {} + +/// Identity hashing (I-4): consistent with `==` by hashing exactly what `==` +/// compares. +impl std::hash::Hash for TypeValue { + fn hash(&self, state: &mut H) { + self.mint.hash(state); + } +} + +#[cfg(test)] +mod tests { + use std::hash::{BuildHasher, RandomState}; + + use super::*; + + /// Runtime mints are counter-identities: distinct counters are distinct + /// types even for byte-identical `ty` payloads (I-1), and the variant + /// discriminant keeps `Runtime` disjoint from `Static` even when the raw + /// `u64`s collide. + #[test] + fn mint_distinctness() { + assert_ne!(MintId::Runtime(0), MintId::Runtime(1)); + assert_ne!(MintId::Runtime(7), MintId::Static(7)); + assert_eq!(MintId::Runtime(3), MintId::Runtime(3)); + assert_eq!(MintId::Static(3), MintId::Static(3)); + + let a = TypeValue::from_parts(RealizedTy::int(), MintId::Runtime(0)); + let b = TypeValue::from_parts(RealizedTy::int(), MintId::Runtime(1)); + let c = TypeValue::from_parts(RealizedTy::int(), MintId::Static(0)); + assert_ne!(a, b, "distinct runtime mints are distinct identities"); + assert_ne!(a, c, "a runtime mint never equals a static mint"); + assert_eq!(a, a.clone(), "a copy preserves identity (I-1)"); + } + + /// `Hash` must be consistent with `==` (I-4): equal values hash equal, + /// and the `ty` payload is excluded from both. + #[test] + fn hash_consistent_with_eq() { + let a = TypeValue::from_parts(RealizedTy::int(), MintId::Static(42)); + let b = TypeValue::from_parts(RealizedTy::string(), MintId::Static(42)); + assert_eq!(a, b, "mint-equal values are equal regardless of payload"); + // Same-RandomState comparison isn't available through `hash_one` + // twice (each `RandomState::new()` is keyed); use one state for both. + let state = RandomState::new(); + assert_eq!(state.hash_one(&a), state.hash_one(&b)); + // Unequal values need not hash differently, but hashing a different + // mint directly must remain supported by the same implementation. + let c = TypeValue::from_parts(RealizedTy::int(), MintId::Static(43)); + assert_ne!(a, c); + let _ = state.hash_one(&c); + } + + /// The static digest is a pure function of the canonical form: equivalent + /// spellings share a mint, distinct types do not, and derivation is + /// deterministic across independent derivations (two "processes" worth of + /// state in one test — nothing session-local feeds the digest). + #[test] + fn static_digest_deterministic_and_canonical() { + #[expect( + deprecated, + reason = "unit test of the fact-free digest itself; the VM paths under test \ + elsewhere supply the real fact context" + )] + let ctx = baml_type::normalize::NoFacts; + + let optional = RealizedTy::Union( + vec![RealizedTy::string(), RealizedTy::null()], + baml_type::TyAttr::default(), + ); + let reversed = RealizedTy::Union( + vec![RealizedTy::null(), RealizedTy::string()], + baml_type::TyAttr::default(), + ); + let a = TypeValue::static_new(optional.clone(), &ctx); + let b = TypeValue::static_new(reversed, &ctx); + assert_eq!(a, b, "permuted union spellings share a static mint"); + + let again = TypeValue::static_new(optional, &ctx); + assert_eq!(a.mint(), again.mint(), "derivation is deterministic"); + + let other = TypeValue::static_new(RealizedTy::int(), &ctx); + assert_ne!(a, other, "distinct types get distinct static mints"); + } +} From 4ee767d416c71ea902cd5d93204802b029006986 Mon Sep 17 00:00:00 2001 From: Antonio Sarosi Date: Fri, 7 Aug 2026 07:24:54 +0200 Subject: [PATCH 2/2] =?UTF-8?q?feat(reflect):=20sealed=20type-kind=20read-?= =?UTF-8?q?back=20(BEP-066=20s1,=20PR=205=20=E2=80=94=20capstone)=20(#4334?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **BEP-066 slice-1 stack, PR 5 of 5 — the capstone.** Chained on #4331. With this green, the slice-1 stack is complete: the reflection read API (K/V/N rule families) is fully live. ## What - **Grammar carve-out**: `class`/`enum`/`interface`/`function` legal as path segments after `.` across type parsing, expression paths, patterns, map entries, generic lookahead, AST lowering, and formatting — bare keywords still rejected. - **The nine sealed kind classes** (`reflect.class.Type` … `reflect.function.Type`) + the closed `baml.reflect.TypeKind` union alias; identity-preserving `kind()` (K-5), all nine nullable `as_*()` (K-6 — never throw), `as_type()`. - **The one type-system fact**: kind class `<: type` sealed edge in shared normalization; `Object::Type` reports its precise kind class while keeping the physical TYPE tag; `implement … for type` preserved by teaching impl resolution to follow the sealed edge (`to_string()` verified on all nine kinds). - **Read-back (C-15)**: fields/values/member_types/element_type/params natives for all nine kinds; generic class fields substituted before read-back; function.Type has ordered params + return, no throws (K-11). New `docstring` + string-valued `other` columns on Class/Field/Enum/Variant; emit preserves aliases, descriptions, docstrings, and custom annotations (found + fixed + pinned a pre-existing hoist bug where the custom-attribute path consumed `stream.*`). - Kind classes non-constructible with a dedicated diagnostic. ## Oracle coverage Exhaustive nine-arm `TypeKind` match + missing-arm diagnostics · every `as_*` positive/null path · mint identity through `kind().as_type()` (I-2 × K-5) · non-throwing accessor contracts · full metadata read-back · conformance queries · recursive type walking · `of_value` precision. ## Gates Full corpus + parser + LSP (reviewed) + all-features baml_cli + tir + project green; fmt/clippy/rustdoc clean; snapshot accepts grouped in the commit (new type_kinds corpus + describe-listing growth). Implemented by a Codex (gpt-5.6-sol) worker under stack-manager review. **Both BEP-066 foundation stacks are now complete at 5/5.** ## Summary by CodeRabbit * **New Features** * Added comprehensive runtime reflection for classes, enums, unions, literals, arrays, maps, interfaces, primitives, and functions. * Type values now expose their kind and provide kind-specific views and nested type information. * Added access to fields, parameters, enum values, metadata, aliases, documentation, and custom annotations. * Added validation preventing direct construction of reflection-kind values. * Expanded support for keyword-based names in qualified paths. * **Bug Fixes** * Improved type alias handling, metadata propagation, and reflection type matching. --- .../baml/ns_reflect/ns_array/array.baml | 9 + .../baml/ns_reflect/ns_class/class.baml | 17 + .../baml/ns_reflect/ns_enum/enum.baml | 16 + .../baml/ns_reflect/ns_function/function.baml | 18 + .../ns_reflect/ns_interface/interface.baml | 11 + .../baml/ns_reflect/ns_literal/literal.baml | 7 + .../baml_std/baml/ns_reflect/ns_map/map.baml | 11 + .../ns_reflect/ns_primitive/primitive.baml | 7 + .../baml/ns_reflect/ns_union/union.baml | 9 + .../baml_std/baml/ns_reflect/reflect.baml | 16 + .../baml_std/baml/type_class.baml | 26 ++ .../crates/baml_builtins2/src/lib.rs | 9 + .../baml_builtins2_codegen/src/codegen.rs | 112 ++++--- ...tests__render_builtin_package_listing.snap | 24 +- .../baml_compiler2_ast/src/disambiguate.rs | 7 + .../crates/baml_compiler2_ast/src/lib.rs | 17 + .../baml_compiler2_ast/src/lower_cst.rs | 2 +- .../baml_compiler2_ast/src/lower_expr_body.rs | 3 + .../crates/baml_compiler2_emit/src/emit.rs | 10 + .../crates/baml_compiler2_emit/src/lib.rs | 147 +++++--- .../crates/baml_compiler2_mir/src/lower.rs | 18 +- .../crates/baml_compiler2_tir/src/builder.rs | 22 +- .../baml_compiler2_tir/src/infer_context.rs | 9 + .../crates/baml_compiler_parser/src/parser.rs | 120 ++++++- .../crates/baml_compiler_syntax/src/ast.rs | 30 +- .../crates/baml_fmt/src/ast/expressions.rs | 9 +- .../crates/baml_fmt/src/ast/types.rs | 20 +- .../crates/baml_lsp2_actions/src/check.rs | 1 + .../ns_reflect_type_of/reflect_type_of.baml | 6 +- .../projects/compiles/type_kinds/main.baml | 42 +++ .../diagnostic_errors/type_kinds/main.baml | 16 + .../snapshots/baml_src/reflect_type_of.snap | 3 +- ...ests__compiles____baml_std____03_ppir.snap | 128 ++++++- ...sts__compiles____baml_std____04_5_mir.snap | 63 +++- ...tests__compiles____baml_std____04_tir.snap | 97 +++++- ...s__compiles____baml_std____06_codegen.snap | 94 +++++- ..._tests__compiles__type_kinds__03_ppir.snap | 22 ++ ...tests__compiles__type_kinds__04_5_mir.snap | 314 ++++++++++++++++++ ...l_tests__compiles__type_kinds__04_tir.snap | 46 +++ ..._compiles__type_kinds__05_diagnostics.snap | 5 + ...sts__compiles__type_kinds__06_codegen.snap | 183 ++++++++++ ...piles__type_kinds__10_formatter__main.snap | 45 +++ ...iagnostic_errors__type_kinds__03_ppir.snap | 10 + ...diagnostic_errors__type_kinds__04_tir.snap | 32 ++ ...ic_errors__type_kinds__05_diagnostics.snap | 27 ++ ...rrors__type_kinds__10_formatter__main.snap | 19 ++ ...__phase5__snapshot_baml_package_items.snap | 27 +- ...ode_format__bytecode_display_expanded.snap | 221 ++++++------ ...bytecode_display_expanded_unoptimized.snap | 221 ++++++------ .../crates/baml_tests/tests/type_kinds.rs | 261 +++++++++++++++ baml_language/crates/baml_type/src/lib.rs | 1 + .../crates/baml_type/src/normalize.rs | 11 + .../crates/baml_type/src/type_kind.rs | 99 ++++++ .../crates/bex_engine/src/conversion.rs | 6 + baml_language/crates/bex_heap/src/gc.rs | 20 ++ baml_language/crates/bex_heap/src/tlab.rs | 14 + .../crates/bex_vm/src/package_baml/mod.rs | 1 + .../crates/bex_vm/src/package_baml/resolve.rs | 10 + .../bex_vm/src/package_baml/type_class.rs | 47 ++- .../bex_vm/src/package_baml/type_kinds.rs | 268 +++++++++++++++ baml_language/crates/bex_vm/src/vm.rs | 10 +- .../bex_vm/tests/method_class_type_args.rs | 4 + baml_language/crates/bex_vm_types/src/link.rs | 2 + .../crates/bex_vm_types/src/types/class.rs | 7 + .../crates/bex_vm_types/src/types/enums.rs | 7 + .../rust/sdkgen_python_pydantic2/src/lib.rs | 63 ++++ .../sdkgen_python_pydantic2/src/routing.rs | 58 +++- 67 files changed, 2839 insertions(+), 378 deletions(-) create mode 100644 baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_array/array.baml create mode 100644 baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_class/class.baml create mode 100644 baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_enum/enum.baml create mode 100644 baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_function/function.baml create mode 100644 baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_interface/interface.baml create mode 100644 baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_literal/literal.baml create mode 100644 baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_map/map.baml create mode 100644 baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_primitive/primitive.baml create mode 100644 baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_union/union.baml create mode 100644 baml_language/crates/baml_tests/projects/compiles/type_kinds/main.baml create mode 100644 baml_language/crates/baml_tests/projects/diagnostic_errors/type_kinds/main.baml create mode 100644 baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__03_ppir.snap create mode 100644 baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__04_5_mir.snap create mode 100644 baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__04_tir.snap create mode 100644 baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__05_diagnostics.snap create mode 100644 baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__06_codegen.snap create mode 100644 baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__10_formatter__main.snap create mode 100644 baml_language/crates/baml_tests/snapshots/diagnostic_errors/type_kinds/baml_tests__diagnostic_errors__type_kinds__03_ppir.snap create mode 100644 baml_language/crates/baml_tests/snapshots/diagnostic_errors/type_kinds/baml_tests__diagnostic_errors__type_kinds__04_tir.snap create mode 100644 baml_language/crates/baml_tests/snapshots/diagnostic_errors/type_kinds/baml_tests__diagnostic_errors__type_kinds__05_diagnostics.snap create mode 100644 baml_language/crates/baml_tests/snapshots/diagnostic_errors/type_kinds/baml_tests__diagnostic_errors__type_kinds__10_formatter__main.snap create mode 100644 baml_language/crates/baml_tests/tests/type_kinds.rs create mode 100644 baml_language/crates/baml_type/src/type_kind.rs create mode 100644 baml_language/crates/bex_vm/src/package_baml/type_kinds.rs diff --git a/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_array/array.baml b/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_array/array.baml new file mode 100644 index 00000000000..57e407ca32c --- /dev/null +++ b/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_array/array.baml @@ -0,0 +1,9 @@ +/// An array-kind view of a `type` value. Users never construct this class. +class Type { + implements baml.reflect.TypeView { + //baml:vm + function as_type(self) -> type throws never { $rust_function } + } + //baml:mut_vm + function element_type(self) -> type throws never { $rust_function } +} diff --git a/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_class/class.baml b/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_class/class.baml new file mode 100644 index 00000000000..5a025801dd1 --- /dev/null +++ b/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_class/class.baml @@ -0,0 +1,17 @@ +class Field { + name string + type type? + meta baml.reflect.Meta +} + +/// A class-kind view of a `type` value. Users never construct this class. +class Type { + implements baml.reflect.TypeView { + //baml:vm + function as_type(self) -> type throws never { $rust_function } + } + //baml:mut_vm + function fields(self) -> Field[] throws never { $rust_function } + //baml:mut_vm + function meta(self) -> baml.reflect.Meta throws never { $rust_function } +} diff --git a/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_enum/enum.baml b/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_enum/enum.baml new file mode 100644 index 00000000000..d01696c383e --- /dev/null +++ b/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_enum/enum.baml @@ -0,0 +1,16 @@ +class Value { + name string + meta baml.reflect.Meta +} + +/// An enum-kind view of a `type` value. Users never construct this class. +class Type { + implements baml.reflect.TypeView { + //baml:vm + function as_type(self) -> type throws never { $rust_function } + } + //baml:mut_vm + function values(self) -> Value[] throws never { $rust_function } + //baml:mut_vm + function meta(self) -> baml.reflect.Meta throws never { $rust_function } +} diff --git a/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_function/function.baml b/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_function/function.baml new file mode 100644 index 00000000000..27857e4df14 --- /dev/null +++ b/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_function/function.baml @@ -0,0 +1,18 @@ +class Parameter { + name string? + type type + optional bool +} + +/// A function-kind view of a `type` value. Throws are intentionally not part +/// of the slice-1 reflection surface. +class Type { + implements baml.reflect.TypeView { + //baml:vm + function as_type(self) -> type throws never { $rust_function } + } + //baml:mut_vm + function params(self) -> Parameter[] throws never { $rust_function } + //baml:mut_vm + function return_type(self) -> type throws never { $rust_function } +} diff --git a/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_interface/interface.baml b/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_interface/interface.baml new file mode 100644 index 00000000000..61f51042905 --- /dev/null +++ b/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_interface/interface.baml @@ -0,0 +1,11 @@ +/// An interface-kind view of a `type` value. Users never construct this class. +class Type { + implements baml.reflect.TypeView { + //baml:vm + function as_type(self) -> type throws never { $rust_function } + } + //baml:vm + function implemented_by(self, other: type) -> bool throws never { $rust_function } + //baml:mut_vm + function implementors(self) -> type[] throws never { $rust_function } +} diff --git a/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_literal/literal.baml b/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_literal/literal.baml new file mode 100644 index 00000000000..30ec922d958 --- /dev/null +++ b/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_literal/literal.baml @@ -0,0 +1,7 @@ +/// A literal-kind view of a `type` value. Users never construct this class. +class Type { + implements baml.reflect.TypeView { + //baml:vm + function as_type(self) -> type throws never { $rust_function } + } +} diff --git a/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_map/map.baml b/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_map/map.baml new file mode 100644 index 00000000000..b523b1706b7 --- /dev/null +++ b/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_map/map.baml @@ -0,0 +1,11 @@ +/// A map-kind view of a `type` value. Users never construct this class. +class Type { + implements baml.reflect.TypeView { + //baml:vm + function as_type(self) -> type throws never { $rust_function } + } + //baml:mut_vm + function key_type(self) -> type throws never { $rust_function } + //baml:mut_vm + function value_type(self) -> type throws never { $rust_function } +} diff --git a/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_primitive/primitive.baml b/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_primitive/primitive.baml new file mode 100644 index 00000000000..30343807b73 --- /dev/null +++ b/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_primitive/primitive.baml @@ -0,0 +1,7 @@ +/// A primitive-kind view of a `type` value. Users never construct this class. +class Type { + implements baml.reflect.TypeView { + //baml:vm + function as_type(self) -> type throws never { $rust_function } + } +} diff --git a/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_union/union.baml b/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_union/union.baml new file mode 100644 index 00000000000..37bcd704b8d --- /dev/null +++ b/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/ns_union/union.baml @@ -0,0 +1,9 @@ +/// A union-kind view of a `type` value. Users never construct this class. +class Type { + implements baml.reflect.TypeView { + //baml:vm + function as_type(self) -> type throws never { $rust_function } + } + //baml:mut_vm + function member_types(self) -> type[] throws never { $rust_function } +} diff --git a/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/reflect.baml b/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/reflect.baml index fd5bae679fb..9d424ca3803 100644 --- a/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/reflect.baml +++ b/baml_language/crates/baml_builtins2/baml_std/baml/ns_reflect/reflect.baml @@ -10,6 +10,22 @@ class Arg { type type } +/// Metadata baked into reflected schema definitions. +class Meta { + alias string? + description string? + docstring string? + other map +} + +/// Common identity-preserving surface shared by every reflection kind view. +interface TypeView { + function as_type(self) -> type throws never +} + +/// The closed set of reflection views for a runtime `type` value. +type TypeKind = baml.reflect.class.Type | baml.reflect.enum.Type | baml.reflect.union.Type | baml.reflect.literal.Type | baml.reflect.array.Type | baml.reflect.map.Type | baml.reflect.interface.Type | baml.reflect.primitive.Type | baml.reflect.function.Type + /// The runtime signature of a function value, reconstructed from the value /// itself (BEP-062). Positional (required) parameters appear in `args` in /// declaration order; named/optional parameters appear in `opts`, keyed by diff --git a/baml_language/crates/baml_builtins2/baml_std/baml/type_class.baml b/baml_language/crates/baml_builtins2/baml_std/baml/type_class.baml index c17f91cbdb8..ca65e62e5c6 100644 --- a/baml_language/crates/baml_builtins2/baml_std/baml/type_class.baml +++ b/baml_language/crates/baml_builtins2/baml_std/baml/type_class.baml @@ -7,6 +7,32 @@ /// Users never construct `Type` directly; they receive `type` values via /// `type.of()` and call methods on them. class TypeValue { + /// Returns the precise kind view of this type value. The view preserves + /// the receiver's mint identity. + //baml:vm + function kind(self) -> baml.reflect.TypeKind throws never { + $rust_function + } + + //baml:vm + function as_class(self) -> baml.reflect.class.Type? throws never { $rust_function } + //baml:vm + function as_enum(self) -> baml.reflect.enum.Type? throws never { $rust_function } + //baml:vm + function as_union(self) -> baml.reflect.union.Type? throws never { $rust_function } + //baml:vm + function as_literal(self) -> baml.reflect.literal.Type? throws never { $rust_function } + //baml:vm + function as_array(self) -> baml.reflect.array.Type? throws never { $rust_function } + //baml:vm + function as_map(self) -> baml.reflect.map.Type? throws never { $rust_function } + //baml:vm + function as_interface(self) -> baml.reflect.interface.Type? throws never { $rust_function } + //baml:vm + function as_primitive(self) -> baml.reflect.primitive.Type? throws never { $rust_function } + //baml:vm + function as_function(self) -> baml.reflect.function.Type? throws never { $rust_function } + /// Returns the type name as a human-readable string. implements baml.ToString { function to_string(self) -> string throws never { diff --git a/baml_language/crates/baml_builtins2/src/lib.rs b/baml_language/crates/baml_builtins2/src/lib.rs index fcbf21ed03c..8050a76a42e 100644 --- a/baml_language/crates/baml_builtins2/src/lib.rs +++ b/baml_language/crates/baml_builtins2/src/lib.rs @@ -140,6 +140,15 @@ pub const ALL: &[BuiltinFile] = &[ builtin!("baml", "ns_random/random.baml"), // `baml.reflect` (BEP-066 I-9: `reflect` is a keyword shorthand for it). builtin!("baml", "ns_reflect/reflect.baml"), + builtin!("baml", "ns_reflect/ns_class/class.baml"), + builtin!("baml", "ns_reflect/ns_enum/enum.baml"), + builtin!("baml", "ns_reflect/ns_union/union.baml"), + builtin!("baml", "ns_reflect/ns_literal/literal.baml"), + builtin!("baml", "ns_reflect/ns_array/array.baml"), + builtin!("baml", "ns_reflect/ns_map/map.baml"), + builtin!("baml", "ns_reflect/ns_interface/interface.baml"), + builtin!("baml", "ns_reflect/ns_primitive/primitive.baml"), + builtin!("baml", "ns_reflect/ns_function/function.baml"), // `baml.type` (BEP-066 K-13: `type.of` / `type.of_value` resolve here). builtin!("baml", "ns_type/type.baml"), // --- boundary package --- diff --git a/baml_language/crates/baml_builtins2_codegen/src/codegen.rs b/baml_language/crates/baml_builtins2_codegen/src/codegen.rs index ca5c3b57564..3500ac2e410 100644 --- a/baml_language/crates/baml_builtins2_codegen/src/codegen.rs +++ b/baml_language/crates/baml_builtins2_codegen/src/codegen.rs @@ -215,7 +215,8 @@ fn emit_view_namespace_contents(out: &mut String, node: &ClassNamespaceNode, dep // Emit sub-namespace modules for (ns_name, sub_node) in &node.sub_namespaces { - writeln!(out, "{indent}pub mod {ns_name} {{").unwrap(); + let rust_ns_name = rust_field_ident(ns_name); + writeln!(out, "{indent}pub mod {rust_ns_name} {{").unwrap(); write!(out, "{indent} use super::super::*;\n\n").unwrap(); emit_view_namespace_contents(out, sub_node, depth + 1); writeln!(out, "{indent}}}\n").unwrap(); @@ -437,7 +438,11 @@ fn emit_view_struct(out: &mut String, class_name: &str, def: &NativeClassDef, de } // Generic, Named, Media, Null — fallback to a copied Value. _ => { - writeln!(out, "{inner}pub fn {field_name}(&self) -> Value {{").unwrap(); + writeln!( + out, + "{inner}pub fn {field_name}(&self) -> bex_vm_types::Value {{" + ) + .unwrap(); writeln!(out, "{inner2}self.instance.load_field({})", field.index).unwrap(); writeln!(out, "{inner}}}\n").unwrap(); } @@ -486,7 +491,7 @@ fn view_optional_type_and_expr( ), ), _ => ( - "Option".to_string(), + "Option".to_string(), format!("self.instance.load_field({field_index})"), ), } @@ -514,7 +519,8 @@ fn emit_copy_namespace_contents(out: &mut String, node: &ClassNamespaceNode, dep } for (ns_name, sub_node) in &node.sub_namespaces { - writeln!(out, "{indent}pub mod {ns_name} {{").unwrap(); + let rust_ns_name = rust_field_ident(ns_name); + writeln!(out, "{indent}pub mod {rust_ns_name} {{").unwrap(); writeln!(out, "{indent} use super::super::*;").unwrap(); writeln!(out, "{indent} use std::sync::Arc;").unwrap(); write!(out, "{indent} use std::any::Any;\n\n").unwrap(); @@ -547,7 +553,7 @@ fn emit_copy_struct(out: &mut String, class_name: &str, def: &NativeClassDef, de writeln!(out, "{indent}impl {class_name} {{").unwrap(); writeln!( out, - "{inner}pub fn to_value(self, vm: &mut BexVm) -> Value {{" + "{inner}pub fn to_value(self, vm: &mut BexVm) -> bex_vm_types::Value {{" ) .unwrap(); writeln!(out, "{inner2}let class_ptr = vm.resolve_class({fqn:?});").unwrap(); @@ -565,7 +571,7 @@ fn emit_copy_struct(out: &mut String, class_name: &str, def: &NativeClassDef, de // Build the fields vec write!( out, - "{inner2}Value::object(vm.alloc_instance(class_ptr, vec![" + "{inner2}bex_vm_types::Value::object(vm.alloc_instance(class_ptr, vec![" ) .unwrap(); for (i, field) in def.fields.iter().enumerate() { @@ -596,23 +602,25 @@ fn copy_field_type(ty: &BamlType) -> String { | BamlType::Optional(_) | BamlType::Generic(_) | BamlType::Named(_) - | BamlType::Media(_) => "Value".to_string(), + | BamlType::Media(_) => "bex_vm_types::Value".to_string(), } } /// Generate the expression to convert a copy struct field to a Value. fn copy_field_to_value(field_name: &str, ty: &BamlType) -> String { match ty { - BamlType::RustType => format!("Value::object(vm.alloc_rust_data(self.{field_name}))"), + BamlType::RustType => { + format!("bex_vm_types::Value::object(vm.alloc_rust_data(self.{field_name}))") + } // `to_value` has no error channel (`fn to_value(self, vm) -> Value`), // so an out-of-i63 native i64 reaches this path only when caller-side // Rust constructed a struct field that violates the i63 BAML // contract. Fail loudly in *both* debug and release rather than // truncating silently via `Value::int`'s `debug_assert`. BamlType::Int => format!( - "Value::try_int(self.{field_name}).unwrap_or_else(|| panic!(\ + "bex_vm_types::Value::try_int(self.{field_name}).unwrap_or_else(|| panic!(\ \"`{field_name}: int` is outside BAML int range [{{}}, {{}}], got {{}}\", \ - Value::INT_MIN, Value::INT_MAX, self.{field_name}))" + bex_vm_types::Value::INT_MIN, bex_vm_types::Value::INT_MAX, self.{field_name}))" ), // Bigints are always heap-allocated, and allocation is fallible (the // value may exceed `MAX_BIGINT_BITS`). `to_value` has no error channel, @@ -624,9 +632,11 @@ fn copy_field_to_value(field_name: &str, ty: &BamlType) -> String { "vm.try_alloc_bigint(self.{field_name}).unwrap_or_else(|p| panic!(\ \"failed to allocate bigint field `{field_name}`: {{p}}\"))" ), - BamlType::Float => format!("Value::object(vm.alloc_float(self.{field_name}))"), - BamlType::Bool => format!("Value::bool(self.{field_name})"), - BamlType::Null => "Value::NULL".to_string(), + BamlType::Float => { + format!("bex_vm_types::Value::object(vm.alloc_float(self.{field_name}))") + } + BamlType::Bool => format!("bex_vm_types::Value::bool(self.{field_name})"), + BamlType::Null => "bex_vm_types::Value::NULL".to_string(), // String, List, Map, Optional, Generic, Named, Media — already a Value _ => format!("self.{field_name}"), } @@ -648,6 +658,10 @@ fn to_pascal_case(s: &str) -> String { } } +fn namespace_pascal_case(path: &str) -> String { + path.split('.').map(to_pascal_case).collect() +} + /// Replace characters that are illegal in Rust identifiers with `_`. Synthetic /// class names for `implement Interface for Type` blocks (e.g. `Equals$for$int`, /// `Equals$for$map`) contain `$`, `<`, `>`, `[`, `]`, `,`, and spaces; the @@ -670,7 +684,7 @@ fn class_trait_name(namespace_prefix: &str, class_name: &str) -> String { if namespace_prefix.is_empty() { format!("BamlClass{class_name}") } else { - let ns_pascal = to_pascal_case(namespace_prefix); + let ns_pascal = namespace_pascal_case(namespace_prefix); format!("BamlClass{ns_pascal}{class_name}") } } @@ -680,7 +694,10 @@ fn class_dispatch_name(namespace_prefix: &str, class_name: &str) -> String { if namespace_prefix.is_empty() { format!("__dispatch_{class_lower}") } else { - format!("__dispatch_{namespace_prefix}_{class_lower}") + format!( + "__dispatch_{}_{class_lower}", + sanitize_ident(&namespace_prefix.replace('.', "_")) + ) } } @@ -691,12 +708,12 @@ fn package_trait_name(package: &str) -> String { } fn namespace_trait_name(name: &str) -> String { - let pascal = to_pascal_case(name); + let pascal = namespace_pascal_case(name); format!("BamlNamespace{pascal}") } fn namespace_dispatch_name(name: &str) -> String { - format!("__dispatch_{name}") + format!("__dispatch_{}", sanitize_ident(&name.replace('.', "_"))) } // ============================================================================ @@ -755,8 +772,13 @@ fn emit_subtree_traits(out: &mut String, node: &NamespaceNode, namespace_prefix: } for (ns_name, sub_node) in &node.sub_namespaces { - emit_subtree_traits(out, sub_node, ns_name); - emit_namespace_trait(out, ns_name, sub_node); + let child_prefix = if namespace_prefix.is_empty() { + ns_name.clone() + } else { + format!("{namespace_prefix}.{ns_name}") + }; + emit_subtree_traits(out, sub_node, &child_prefix); + emit_namespace_trait(out, &child_prefix, sub_node); } } @@ -809,16 +831,18 @@ fn emit_class_trait( // Namespace trait emission // ============================================================================ -fn emit_namespace_trait(out: &mut String, ns_name: &str, node: &NamespaceNode) { - let trait_name = namespace_trait_name(ns_name); - let dispatch_name = namespace_dispatch_name(ns_name); +fn emit_namespace_trait(out: &mut String, namespace_prefix: &str, node: &NamespaceNode) { + let trait_name = namespace_trait_name(namespace_prefix); + let dispatch_name = namespace_dispatch_name(namespace_prefix); let mut supertraits: Vec = Vec::new(); for class_name in node.classes.keys() { - supertraits.push(class_trait_name(ns_name, class_name)); + supertraits.push(class_trait_name(namespace_prefix, class_name)); } for sub_ns in node.sub_namespaces.keys() { - supertraits.push(namespace_trait_name(sub_ns)); + supertraits.push(namespace_trait_name(&format!( + "{namespace_prefix}.{sub_ns}" + ))); } if supertraits.is_empty() { @@ -851,7 +875,7 @@ fn emit_namespace_trait(out: &mut String, ns_name: &str, node: &NamespaceNode) { out.push_str(" match rest.split_once('.') {\n"); for class_name in node.classes.keys() { - let child_dispatch = class_dispatch_name(ns_name, class_name); + let child_dispatch = class_dispatch_name(namespace_prefix, class_name); writeln!( out, " Some(({class_name:?}, method)) => Self::{child_dispatch}(method)," @@ -860,7 +884,7 @@ fn emit_namespace_trait(out: &mut String, ns_name: &str, node: &NamespaceNode) { } for sub_ns in node.sub_namespaces.keys() { - let child_dispatch = namespace_dispatch_name(sub_ns); + let child_dispatch = namespace_dispatch_name(&format!("{namespace_prefix}.{sub_ns}")); writeln!( out, " Some(({sub_ns:?}, rest)) => Self::{child_dispatch}(rest),", @@ -1133,7 +1157,11 @@ fn clean_param_list(b: &NativeBuiltin) -> String { )); } for p in &b.params { - parts.push(format!("{}: {}", p.name, baml_type_to_input(&p.ty, false))); + parts.push(format!( + "{}: {}", + rust_field_ident(&p.name), + baml_type_to_input(&p.ty, false) + )); } parts.join(", ") @@ -1376,7 +1404,7 @@ fn emit_arg_extractions_indented( let arg_idx = i; emit_single_extraction_indented( out, - &p.name, + &rust_field_ident(&p.name).to_string(), arg_idx, &p.ty, indent, @@ -1389,7 +1417,7 @@ fn emit_arg_extractions_indented( let arg_idx = i + 1; emit_single_extraction_indented( out, - &p.name, + &rust_field_ident(&p.name).to_string(), arg_idx, &p.ty, indent, @@ -1420,7 +1448,7 @@ fn emit_arg_extractions_indented( let arg_idx = i + 1; emit_single_extraction_indented( out, - &p.name, + &rust_field_ident(&p.name).to_string(), arg_idx, &p.ty, indent, @@ -1433,7 +1461,7 @@ fn emit_arg_extractions_indented( for (i, p) in b.params.iter().enumerate() { emit_single_extraction_indented( out, - &p.name, + &rust_field_ident(&p.name).to_string(), i, &p.ty, indent, @@ -1694,7 +1722,11 @@ fn call_arg_list(b: &NativeBuiltin, needs_owned: bool, arraymap_needs_owned: boo BamlType::List(_) | BamlType::Map(_, _) | BamlType::Uint8Array => arraymap_is_ref, _ => is_ref, }; - args.push(call_arg_for_type(&p.name, &p.ty, p_is_ref)); + args.push(call_arg_for_type( + &rust_field_ident(&p.name).to_string(), + &p.ty, + p_is_ref, + )); } args.join(", ") @@ -2077,7 +2109,7 @@ fn baml_type_to_output(ty: &BamlType) -> String { // ============================================================================ fn receiver_param_name(recv: &Receiver) -> String { - recv.class_name.to_lowercase() + rust_field_ident(&recv.class_name.to_lowercase()).to_string() } /// Path to the generated `view::` struct for an instance-backed receiver, e.g. @@ -2086,11 +2118,13 @@ fn receiver_view_path(recv: &Receiver) -> String { if recv.namespace.is_empty() { format!("view::{}", recv.class_name) } else { - format!( - "view::{}::{}", - recv.namespace.replace('.', "::"), - recv.class_name - ) + let namespace = recv + .namespace + .split('.') + .map(|segment| rust_field_ident(segment).to_string()) + .collect::>() + .join("::"); + format!("view::{}::{}", namespace, recv.class_name) } } @@ -2545,7 +2579,7 @@ mod tests { ); // to_value method assert!( - output.contains("fn to_value(self, vm: &mut BexVm) -> Value"), + output.contains("fn to_value(self, vm: &mut BexVm) -> bex_vm_types::Value"), "copy struct should have to_value method:\n{output}" ); } diff --git a/baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_package_listing.snap b/baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_package_listing.snap index 8058b47f446..53cf0b6fc75 100644 --- a/baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_package_listing.snap +++ b/baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_package_listing.snap @@ -1,6 +1,5 @@ --- source: crates/baml_cli/src/describe_command_tests.rs -assertion_line: 417 expression: output --- class baml.Bigint /baml/bigint.baml:30 @@ -272,10 +271,25 @@ class baml.random.SystemRandom /baml/ns_random/rando class baml.random.Xoshiro256PlusPlus /baml/ns_random/random.baml:68 class baml.random.ChaCha20 /baml/ns_random/random.baml:114 class baml.reflect.Arg /baml/ns_reflect/reflect.baml:2 -class baml.reflect.Signature /baml/ns_reflect/reflect.baml:17 -class baml.reflect.InvalidArgumentError /baml/ns_reflect/reflect.baml:39 -function baml.reflect.signature /baml/ns_reflect/reflect.baml:52 -function baml.reflect.call_any /baml/ns_reflect/reflect.baml:67 +class baml.reflect.Meta /baml/ns_reflect/reflect.baml:14 +interface baml.reflect.TypeView /baml/ns_reflect/reflect.baml:22 +type baml.reflect.TypeKind /baml/ns_reflect/reflect.baml:27 +class baml.reflect.Signature /baml/ns_reflect/reflect.baml:33 +class baml.reflect.InvalidArgumentError /baml/ns_reflect/reflect.baml:55 +function baml.reflect.signature /baml/ns_reflect/reflect.baml:68 +function baml.reflect.call_any /baml/ns_reflect/reflect.baml:83 +class baml.reflect.array.Type /baml/ns_reflect/ns_array/array.baml:2 +class baml.reflect.class.Field /baml/ns_reflect/ns_class/class.baml:1 +class baml.reflect.class.Type /baml/ns_reflect/ns_class/class.baml:8 +class baml.reflect.enum.Value /baml/ns_reflect/ns_enum/enum.baml:1 +class baml.reflect.enum.Type /baml/ns_reflect/ns_enum/enum.baml:7 +class baml.reflect.function.Parameter /baml/ns_reflect/ns_function/function.baml:1 +class baml.reflect.function.Type /baml/ns_reflect/ns_function/function.baml:9 +class baml.reflect.interface.Type /baml/ns_reflect/ns_interface/interface.baml:2 +class baml.reflect.literal.Type /baml/ns_reflect/ns_literal/literal.baml:2 +class baml.reflect.map.Type /baml/ns_reflect/ns_map/map.baml:2 +class baml.reflect.primitive.Type /baml/ns_reflect/ns_primitive/primitive.baml:2 +class baml.reflect.union.Type /baml/ns_reflect/ns_union/union.baml:2 function baml.sap.parse /baml/ns_sap/sap.baml:5 function baml.sap.parse_type /baml/ns_sap/sap.baml:12 function baml.schema.json_schema /baml/ns_schema/schema.baml:8 diff --git a/baml_language/crates/baml_compiler2_ast/src/disambiguate.rs b/baml_language/crates/baml_compiler2_ast/src/disambiguate.rs index 778e832c604..f1ceccd2ee2 100644 --- a/baml_language/crates/baml_compiler2_ast/src/disambiguate.rs +++ b/baml_language/crates/baml_compiler2_ast/src/disambiguate.rs @@ -20,6 +20,13 @@ pub fn is_field_attr(name: &str) -> bool { FIELD_ATTR_NAMES.contains(&name) } +/// Whether a direct outer attribute on a class field belongs to field metadata. +/// Known type transforms stay on the type; unknown names are user schema +/// annotations and are hoisted for reflection read-back. +pub(crate) fn should_hoist_field_attr(name: &str) -> bool { + is_field_attr(name) || !name.starts_with("stream.") +} + /// Post-lowering validation: report field attrs that appear in nested type /// positions (inside parens, on union members, inside generics). /// These were not hoisted during lowering because they weren't at the diff --git a/baml_language/crates/baml_compiler2_ast/src/lib.rs b/baml_language/crates/baml_compiler2_ast/src/lib.rs index 624de52e2d0..df75f71c70c 100644 --- a/baml_language/crates/baml_compiler2_ast/src/lib.rs +++ b/baml_language/crates/baml_compiler2_ast/src/lib.rs @@ -2297,6 +2297,23 @@ class C { assert_eq!(te.attrs()[0].name.as_str(), "stream.done"); } + #[test] + fn custom_schema_attr_is_hoisted_but_stream_attr_stays_on_type() { + let source = r#" +class C { + f string @custom("read-back") @stream.done +} +"#; + let (items, diags) = parse_lower_validate(source); + assert!(diags.is_empty(), "expected no diagnostics, got {diags:?}"); + let class = first_class(items); + let field = &class.fields[0]; + assert_eq!(field.attributes.len(), 1); + assert_eq!(field.attributes[0].name.as_str(), "custom"); + assert_eq!(field.type_expr.attrs().len(), 1); + assert_eq!(field.type_expr.attrs()[0].name.as_str(), "stream.done"); + } + #[test] fn union_trailing_field_attr_hoisted_to_field() { // A | B | C @alias("x") → @alias hoisted to FieldDef, Union has no attrs. diff --git a/baml_language/crates/baml_compiler2_ast/src/lower_cst.rs b/baml_language/crates/baml_compiler2_ast/src/lower_cst.rs index 57ad1e8f67d..84e84ffebff 100644 --- a/baml_language/crates/baml_compiler2_ast/src/lower_cst.rs +++ b/baml_language/crates/baml_compiler2_ast/src/lower_cst.rs @@ -1116,7 +1116,7 @@ fn lower_class( let all_outer_attrs = std::mem::take(expr.attrs_mut()); let (hoist, keep): (Vec<_>, Vec<_>) = all_outer_attrs.into_iter().partition(|a| { - crate::disambiguate::is_field_attr(a.name.as_str()) + crate::disambiguate::should_hoist_field_attr(a.name.as_str()) && direct_attr_spans.contains(&a.span) }); *expr.attrs_mut() = keep; diff --git a/baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs b/baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs index d6ca77441cc..7ac64d5ff38 100644 --- a/baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs +++ b/baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs @@ -49,6 +49,9 @@ fn is_ident_token(kind: SyntaxKind) -> bool { | SyntaxKind::KW_CLIENT | SyntaxKind::KW_SPAWN | SyntaxKind::KW_AWAIT + | SyntaxKind::KW_CLASS + | SyntaxKind::KW_ENUM + | SyntaxKind::KW_FUNCTION | SyntaxKind::KW_IMPLEMENTS | SyntaxKind::KW_IMPLEMENT | SyntaxKind::KW_INTERFACE diff --git a/baml_language/crates/baml_compiler2_emit/src/emit.rs b/baml_language/crates/baml_compiler2_emit/src/emit.rs index d7ae136f709..9d35ea85563 100644 --- a/baml_language/crates/baml_compiler2_emit/src/emit.rs +++ b/baml_language/crates/baml_compiler2_emit/src/emit.rs @@ -3389,6 +3389,15 @@ impl PullSink for StackifyCodegen<'_, '_> { // so the VM compares each arg invariantly; empty args → // class-pointer identity. TyTemplate::Class(tn, type_args_templates, _) => { + // A reflected `type` value is physically `Object::Type` but its + // reconstructed concrete type is one of the nine sealed kind + // classes. Kind tests must therefore use the structural value + // matcher; class-object pointer identity only applies to normal + // user instances. + if baml_type::type_kind::is_type_kind_class(tn) { + emit_structural(self, ty_template); + return Ok(()); + } let class_name_str = tn.display_name(); let Some(class_obj_idx) = self.class_object_index_for_type_name(tn) else { emit_false(self); @@ -3635,6 +3644,7 @@ fn realized_type_tag(ty: &RealizedTy) -> Option { RealizedTy::List(..) => Some(baml_type::typetag::LIST), RealizedTy::Map { .. } => Some(baml_type::typetag::MAP), RealizedTy::Function { .. } => Some(baml_type::typetag::FUNCTION), + RealizedTy::Type { .. } => Some(baml_type::typetag::TYPE), RealizedTy::Uint8Array { .. } => Some(baml_type::typetag::UINT8ARRAY), RealizedTy::Literal(lit, _, _) => Some(match lit { baml_base::Literal::Int(_) => baml_type::typetag::INT, diff --git a/baml_language/crates/baml_compiler2_emit/src/lib.rs b/baml_language/crates/baml_compiler2_emit/src/lib.rs index 2051d0daa38..c6c04698c1f 100644 --- a/baml_language/crates/baml_compiler2_emit/src/lib.rs +++ b/baml_language/crates/baml_compiler2_emit/src/lib.rs @@ -1177,35 +1177,62 @@ impl std::fmt::Display for LoweringError { impl std::error::Error for LoweringError {} -/// Extract `@description`, `@alias`, `@skip` from span-free HIR attributes. -/// -/// Returns `(description, alias, skip)`. Invalid attribute usage is diagnosed -/// at HIR validation time; by this point, malformed attrs are simply skipped. +#[derive(Debug, Default, PartialEq, Eq)] +struct SchemaAttrs { + description: Option, + alias: Option, + docstring: Option, + other: indexmap::IndexMap, + skip: bool, +} + +/// Extract schema metadata from span-free HIR attributes and docstrings. fn extract_schema_attrs( attrs: &[baml_compiler2_hir::item_tree::Attribute], -) -> (Option, Option, bool) { - let mut description = None; - let mut alias = None; - let mut skip = false; + docstring: Option<&str>, +) -> SchemaAttrs { + let mut result = SchemaAttrs { + docstring: docstring.map(str::to_owned), + ..SchemaAttrs::default() + }; for attr in attrs { match attr.name.as_str() { "description" | "alias" if attr.args.len() == 1 => { let raw = attr.args[0].value.as_str(); let value = parse_string_attr_value(raw); if attr.name.as_str() == "description" { - description = value; + result.description = value; } else { - alias = value; + result.alias = value; } } "description" | "alias" => {} "skip" => { - skip = true; + result.skip = true; + } + _ => { + let value = match attr.args.as_slice() { + [] => "true".to_string(), + [arg] if arg.key.is_none() => { + parse_string_attr_value(&arg.value).unwrap_or_else(|| arg.value.clone()) + } + args => args + .iter() + .map(|arg| { + let value = parse_string_attr_value(&arg.value) + .unwrap_or_else(|| arg.value.clone()); + arg.key + .as_ref() + .map_or(value.clone(), |key| format!("{}={value}", key.as_str())) + }) + .collect::>() + .join(", "), + }; + result.other.insert(attr.name.to_string(), value); } - _ => {} } } - (description, alias, skip) + result } pub use bex_vm_types::Program as ProgramAlias; @@ -1216,6 +1243,7 @@ type MergedFieldEntry = ( String, baml_compiler2_hir::type_ref::TypeRefId, Vec, + Option, Vec, Vec, ); @@ -1238,6 +1266,7 @@ fn collect_class_fields_with_implements( name, field.type_ref, field.attributes.clone(), + field.docstring.clone(), class .generic_params .iter() @@ -2967,7 +2996,8 @@ fn emit_file_group( // validator enforces/link-checks them before emit. let merged_fields = collect_class_fields_with_implements(&pkg_info.namespace_path, class); - for (idx, (name, type_ref, attrs, _gen_params, ns)) in merged_fields.iter().enumerate() + for (idx, (name, type_ref, attrs, docstring, _gen_params, ns)) in + merged_fields.iter().enumerate() { field_indices.insert(name.clone(), idx); let (field_type, field_template) = { @@ -3005,18 +3035,20 @@ fn emit_file_group( (resolved_ty, template) } }; - let (field_desc, field_alias, field_skip) = extract_schema_attrs(attrs.as_slice()); + let meta = extract_schema_attrs(attrs.as_slice(), docstring.as_deref()); fields.push(ClassField { name: name.clone(), field_type, field_template, - description: field_desc, - alias: field_alias, - skip: field_skip, + description: meta.description, + alias: meta.alias, + docstring: meta.docstring, + other: meta.other, + skip: meta.skip, }); } - let (class_desc, class_alias, _class_skip) = extract_schema_attrs(&class.attributes); + let class_meta = extract_schema_attrs(&class.attributes, class.docstring.as_deref()); let type_tag = bex_vm_types::type_tags::class_type_tag(&fq_name); if let Some(previous) = class_type_tags.insert(type_tag, fq_name.clone()) @@ -3076,8 +3108,10 @@ fn emit_file_group( let class_obj_idx = program.add_object(Object::Class(Box::new(Class { name: fq_to_type_name(&fq_name), fields, - description: class_desc, - alias: class_alias, + description: class_meta.description, + alias: class_meta.alias, + docstring: class_meta.docstring, + other: class_meta.other, type_tag, ty_attr: TyAttr::default(), has_cleanup, @@ -3129,7 +3163,7 @@ fn emit_file_group( let rebuild_indices = || { let merged = collect_class_fields_with_implements(&pkg_info.namespace_path, class); let mut m = HashMap::new(); - for (idx, (name, _, _, _, _)) in merged.iter().enumerate() { + for (idx, (name, _, _, _, _, _)) in merged.iter().enumerate() { m.insert(name.clone(), idx); } m @@ -3159,23 +3193,27 @@ fn emit_file_group( let mut variant_map = HashMap::new(); let mut variants = Vec::new(); for (idx, variant) in enm.variants.iter().enumerate() { - let (var_desc, var_alias, var_skip) = extract_schema_attrs(&variant.attributes); + let meta = extract_schema_attrs(&variant.attributes, variant.docstring.as_deref()); variant_map.insert(variant.name.to_string(), idx); variants.push(EnumVariant { name: variant.name.to_string(), - description: var_desc, - alias: var_alias, - skip: var_skip, + description: meta.description, + alias: meta.alias, + docstring: meta.docstring, + other: meta.other, + skip: meta.skip, }); } - let (enum_desc, enum_alias, _enum_skip) = extract_schema_attrs(&enm.attributes); + let enum_meta = extract_schema_attrs(&enm.attributes, enm.docstring.as_deref()); let enum_obj_idx = program.add_object(Object::Enum(Box::new(Enum { name: fq_to_type_name(&fq_name), variants, - description: enum_desc, - alias: enum_alias, + description: enum_meta.description, + alias: enum_meta.alias, + docstring: enum_meta.docstring, + other: enum_meta.other, ty_attr: TyAttr::default(), }))); enum_object_indices.insert(fq_name.clone(), enum_obj_idx); @@ -5777,44 +5815,47 @@ mod tests { mk_attr("description", &[r#""A field""#]), mk_attr("alias", &[r#""myField""#]), ]; - let (desc, alias, skip) = extract_schema_attrs(&attrs); - assert_eq!(desc, Some("A field".to_string())); - assert_eq!(alias, Some("myField".to_string())); - assert!(!skip); + let meta = extract_schema_attrs(&attrs, Some("docs")); + assert_eq!(meta.description, Some("A field".to_string())); + assert_eq!(meta.alias, Some("myField".to_string())); + assert_eq!(meta.docstring, Some("docs".to_string())); + assert!(!meta.skip); } #[test] fn extract_skip() { let attrs = vec![mk_attr("skip", &[])]; - let (desc, alias, skip) = extract_schema_attrs(&attrs); - assert_eq!(desc, None); - assert_eq!(alias, None); - assert!(skip); + let meta = extract_schema_attrs(&attrs, None); + assert_eq!(meta.description, None); + assert_eq!(meta.alias, None); + assert!(meta.skip); } #[test] - fn extract_unknown_attrs_ignored() { + fn extract_custom_attrs_into_other() { let attrs = vec![ mk_attr("stream.done", &["true"]), mk_attr("internal.opaque", &[]), mk_attr("description", &[r#""kept""#]), ]; - let (desc, _, _) = extract_schema_attrs(&attrs); - assert_eq!(desc, Some("kept".to_string())); + let meta = extract_schema_attrs(&attrs, None); + assert_eq!(meta.description, Some("kept".to_string())); + assert_eq!(meta.other["stream.done"], "true"); + assert_eq!(meta.other["internal.opaque"], "true"); } #[test] fn extract_non_string_arg_ignored() { let attrs = vec![mk_attr("description", &["42"])]; - let (desc, _, _) = extract_schema_attrs(&attrs); - assert_eq!(desc, None); + let meta = extract_schema_attrs(&attrs, None); + assert_eq!(meta.description, None); } #[test] fn extract_wrong_arg_count_ignored() { let attrs = vec![mk_attr("description", &[])]; // 0 args - let (desc, _, _) = extract_schema_attrs(&attrs); - assert_eq!(desc, None); + let meta = extract_schema_attrs(&attrs, None); + assert_eq!(meta.description, None); } #[test] @@ -5823,30 +5864,28 @@ mod tests { mk_attr("description", &[r#""first""#]), mk_attr("description", &[r#""second""#]), ]; - let (desc, _, _) = extract_schema_attrs(&attrs); - assert_eq!(desc, Some("second".to_string())); + let meta = extract_schema_attrs(&attrs, None); + assert_eq!(meta.description, Some("second".to_string())); } #[test] fn extract_raw_string_attr() { // Simulates @description(#"raw desc"#) let attrs = vec![mk_attr("description", &["#\"raw desc\"#"])]; - let (desc, _, _) = extract_schema_attrs(&attrs); - assert_eq!(desc, Some("raw desc".to_string())); + let meta = extract_schema_attrs(&attrs, None); + assert_eq!(meta.description, Some("raw desc".to_string())); } #[test] fn extract_regular_string_attr_decodes_escapes() { let attrs = vec![mk_attr("description", &[r#""a\nb\tc\\d\"e""#])]; - let (desc, _, _) = extract_schema_attrs(&attrs); - assert_eq!(desc, Some("a\nb\tc\\d\"e".to_string())); + let meta = extract_schema_attrs(&attrs, None); + assert_eq!(meta.description, Some("a\nb\tc\\d\"e".to_string())); } #[test] fn extract_no_attrs() { - let (desc, alias, skip) = extract_schema_attrs(&[]); - assert_eq!(desc, None); - assert_eq!(alias, None); - assert!(!skip); + let meta = extract_schema_attrs(&[], None); + assert_eq!(meta, SchemaAttrs::default()); } } diff --git a/baml_language/crates/baml_compiler2_mir/src/lower.rs b/baml_language/crates/baml_compiler2_mir/src/lower.rs index 0ccec3d70e2..5594718631f 100644 --- a/baml_language/crates/baml_compiler2_mir/src/lower.rs +++ b/baml_language/crates/baml_compiler2_mir/src/lower.rs @@ -7875,7 +7875,7 @@ impl<'db> LoweringContext<'db> { // type slot, same field-chain lowering. else if let Some(members) = self .tir_path_segment_type((self.current_metadata_scope, callee, prefix_idx)) - .and_then(Self::tir_union_members) + .and_then(|ty| self.tir_union_members(ty)) { let receiver_segments = &segments[..segments.len() - 1]; let recv_local = self.lower_path_receiver_to_local( @@ -10336,7 +10336,7 @@ impl<'db> LoweringContext<'db> { ) -> bool { let Some(members) = self .tir_expr_type(self.expr_metadata_key(base)) - .and_then(Self::tir_union_members) + .and_then(|ty| self.tir_union_members(ty)) else { return false; }; @@ -10375,7 +10375,7 @@ impl<'db> LoweringContext<'db> { ) -> bool { let Some(members) = self .tir_expr_type(self.expr_metadata_key(base)) - .and_then(Self::tir_union_members) + .and_then(|ty| self.tir_union_members(ty)) else { return false; }; @@ -12605,9 +12605,14 @@ impl LoweringContext<'_> { /// layers — `(Dog | Named)?` after a null check still dispatches the /// field/method on the underlying union. Returns `None` when `ty` isn't a /// (optionally-wrapped) union. - fn tir_union_members(ty: &Tir2Ty) -> Option> { + fn tir_union_members(&self, ty: &Tir2Ty) -> Option> { match ty { Tir2Ty::Union(members, _) => Some(members.clone()), + Tir2Ty::TypeAlias(qtn, _) if !self.resolved_aliases.recursive.contains(qtn) => self + .resolved_aliases + .aliases + .get(qtn) + .and_then(|target| self.tir_union_members(target)), _ => None, } } @@ -12813,6 +12818,11 @@ impl LoweringContext<'_> { RuntimeTy::Function { .. } => Some(baml_type::typetag::FUNCTION), RuntimeTy::Future(..) => Some(baml_type::typetag::FUTURE), RuntimeTy::Type { .. } => Some(baml_type::typetag::TYPE), + // Reflection-kind classes describe the reconstructed type of an + // `Object::Type`; the physical value deliberately keeps the shared + // TYPE tag. They therefore require the structural matcher and may + // never enter class-tag switch dispatch. + RuntimeTy::Class(tn, _, _) if baml_type::type_kind::is_type_kind_class(tn) => None, RuntimeTy::Class(tn, _, _) => self.class_type_tags.get(tn).copied(), _ => None, } diff --git a/baml_language/crates/baml_compiler2_tir/src/builder.rs b/baml_language/crates/baml_compiler2_tir/src/builder.rs index b06ca9d3328..399e36a3a73 100644 --- a/baml_language/crates/baml_compiler2_tir/src/builder.rs +++ b/baml_language/crates/baml_compiler2_tir/src/builder.rs @@ -28,7 +28,7 @@ use baml_compiler2_hir::{ }; // The trait must be in scope so the builder (which implements it) can call the // defaulted type-algebra methods on itself — `self.is_subtype(a, b)`. -use baml_type::normalize::TypeContext; +use baml_type::{normalize::TypeContext, type_kind::is_type_kind_class}; use rustc_hash::{FxHashMap, FxHashSet}; use text_size::TextRange; @@ -6113,6 +6113,16 @@ impl<'db> TypeInferenceBuilder<'db> { } ty => ty, }; + if let Ty::Class(class_name, _, _) = &ty + && is_type_kind_class(class_name) + { + self.context.report_simple( + TirTypeError::CannotConstructReflectionKind { + class_name: class_name.clone(), + }, + expr_id, + ); + } self.validate_type_generic_bounds(expr_id, &ty); // Class spread is nominal and invariant: the source must be the same // resolved class with compatible generic arguments. Besides preventing @@ -6261,6 +6271,16 @@ impl<'db> TypeInferenceBuilder<'db> { ) { return inferred; } + if let Ty::Class(class_name, _, _) = expected + && is_type_kind_class(class_name) + { + self.context.report_simple( + TirTypeError::CannotConstructReflectionKind { + class_name: class_name.clone(), + }, + expr_id, + ); + } self.validate_type_generic_bounds(expr_id, expected); if let Ty::Class(class_name, type_args, _) = expected { for spread in spreads { diff --git a/baml_language/crates/baml_compiler2_tir/src/infer_context.rs b/baml_language/crates/baml_compiler2_tir/src/infer_context.rs index 21b9df34ee3..b6fe2eff6e7 100644 --- a/baml_language/crates/baml_compiler2_tir/src/infer_context.rs +++ b/baml_language/crates/baml_compiler2_tir/src/infer_context.rs @@ -131,6 +131,10 @@ pub enum TirTypeError { field_name: Name, suggestions: Vec, }, + /// Runtime reflection-kind classes are sealed VM views, not user data. + CannotConstructReflectionKind { + class_name: crate::ty::QualifiedTypeName, + }, /// Unreachable code after a diverging statement (return/break/continue). DeadCode { after: StmtId, @@ -891,6 +895,11 @@ impl fmt::Display for TirTypeError { ) } } + TirTypeError::CannotConstructReflectionKind { class_name } => write!( + f, + "reflection kind `{}` cannot be constructed; obtain it from a type value", + class_name.render_user_facing() + ), TirTypeError::DeadCode { unreachable_count, .. } => { diff --git a/baml_language/crates/baml_compiler_parser/src/parser.rs b/baml_language/crates/baml_compiler_parser/src/parser.rs index cd319a95b84..f700d5fcfb8 100644 --- a/baml_language/crates/baml_compiler_parser/src/parser.rs +++ b/baml_language/crates/baml_compiler_parser/src/parser.rs @@ -508,11 +508,13 @@ impl<'a> Parser<'a> { /// True when the current token can serve as a member name after `.`. /// - /// `interface`/`implements`/`extends` are keywords for declarations but - /// remain valid as member names — e.g. `dog_t.implements(animal_t)` on the - /// reflection `type` value. + /// Declaration keywords remain valid as member names — e.g. + /// `dog_t.implements(animal_t)` on the reflection `type` value. fn at_member_name(&self) -> bool { self.at(TokenKind::Word) + || self.at(TokenKind::Class) + || self.at(TokenKind::Enum) + || self.at(TokenKind::Function) || self.at(TokenKind::Implements) || self.at(TokenKind::Implement) || self.at(TokenKind::Extends) @@ -3108,6 +3110,10 @@ impl<'a> Parser<'a> { if self.at(TokenKind::Word) || self.at(TokenKind::Spawn) || self.at(TokenKind::Await) + || self.at(TokenKind::Class) + || self.at(TokenKind::Enum) + || self.at(TokenKind::Interface) + || self.at(TokenKind::Function) { self.bump(); // next segment } else { @@ -5290,7 +5296,18 @@ impl<'a> Parser<'a> { match self.tokens[idx].kind { TokenKind::Dot => { let next = self.skip_trivia_and_comments_from(idx + 1); - if next < self.tokens.len() && self.tokens[next].kind == TokenKind::Word { + if next < self.tokens.len() + && matches!( + self.tokens[next].kind, + TokenKind::Word + | TokenKind::Spawn + | TokenKind::Await + | TokenKind::Class + | TokenKind::Enum + | TokenKind::Interface + | TokenKind::Function + ) + { idx = self.skip_trivia_and_comments_from(next + 1); } else { return false; @@ -5365,7 +5382,11 @@ impl<'a> Parser<'a> { // args (`foo>(x)`), mirroring // the type-path parser's segment set. | TokenKind::Spawn - | TokenKind::Await => {} + | TokenKind::Await + | TokenKind::Class + | TokenKind::Enum + | TokenKind::Interface + | TokenKind::Function => {} _ => return None, } i = self.skip_trivia_and_comments_from(i + 1); @@ -5457,7 +5478,20 @@ impl<'a> Parser<'a> { return; } self.bump(); // first WORD - while self.at(TokenKind::Dot) && self.peek(1).map(|t| t.kind) == Some(TokenKind::Word) { + while self.at(TokenKind::Dot) + && self.peek(1).is_some_and(|t| { + matches!( + t.kind, + TokenKind::Word + | TokenKind::Spawn + | TokenKind::Await + | TokenKind::Class + | TokenKind::Enum + | TokenKind::Interface + | TokenKind::Function + ) + }) + { self.bump(); // . self.bump(); // WORD } @@ -6856,7 +6890,11 @@ impl<'a> Parser<'a> { // args (`foo>(x)`), mirroring // the type-path parser's segment set. | TokenKind::Spawn - | TokenKind::Await => {} + | TokenKind::Await + | TokenKind::Class + | TokenKind::Enum + | TokenKind::Interface + | TokenKind::Function => {} // Anything else — operators, braces, EOF-ish tokens — can't // appear in a type, so this `<` is a comparison. _ => return false, @@ -7331,7 +7369,14 @@ impl<'a> Parser<'a> { let segment = |k: TokenKind| { matches!( k, - TokenKind::Word | TokenKind::Client | TokenKind::Spawn | TokenKind::Await + TokenKind::Word + | TokenKind::Client + | TokenKind::Spawn + | TokenKind::Await + | TokenKind::Class + | TokenKind::Enum + | TokenKind::Interface + | TokenKind::Function ) }; @@ -7376,7 +7421,21 @@ impl<'a> Parser<'a> { while p.at(TokenKind::Dot) { shorthand_candidate = false; p.bump(); - if !p.expect(TokenKind::Word) { + if p.current().is_some_and(|t| { + matches!( + t.kind, + TokenKind::Word + | TokenKind::Spawn + | TokenKind::Await + | TokenKind::Class + | TokenKind::Enum + | TokenKind::Interface + | TokenKind::Function + ) + }) { + p.bump(); + } else { + p.error_unexpected_token("map key segment after '.'".to_string()); return; } } @@ -12622,4 +12681,47 @@ function iterate(items: int[]) -> int { "the final brace must remain the loop body" ); } + + #[test] + fn reflection_keyword_segments_parse_in_types_and_expressions() { + let source = r#" +class KeywordKinds { + class_kind reflect.class.Type + enum_kind reflect.enum.Type + interface_kind reflect.interface.Type + function_kind reflect.function.Type +} + +function class_kind_value() -> reflect.class.Type { + reflect.class.Type +} + +function enum_kind_value() -> reflect.enum.Type { + reflect.enum.Type +} + +function interface_kind_value() -> reflect.interface.Type { + reflect.interface.Type +} + +function function_kind_value() -> reflect.function.Type { + reflect.function.Type +} +"#; + let (_, errors) = parse_source(source); + assert_no_errors(&errors); + } + + #[test] + fn reflection_segment_keywords_remain_invalid_as_bare_expressions() { + for keyword in ["class", "enum", "interface", "function"] { + let source = + format!("function main() -> int {{\n let value = {keyword};\n 1\n}}"); + let (_, errors) = parse_source(&source); + assert!( + !errors.is_empty(), + "reserved keyword {keyword:?} unexpectedly parsed as a bare identifier" + ); + } + } } diff --git a/baml_language/crates/baml_compiler_syntax/src/ast.rs b/baml_language/crates/baml_compiler_syntax/src/ast.rs index 404c04e80bf..f73a4fc83a4 100644 --- a/baml_language/crates/baml_compiler_syntax/src/ast.rs +++ b/baml_language/crates/baml_compiler_syntax/src/ast.rs @@ -6,7 +6,8 @@ use crate::{SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken}; /// Extract a dotted name from a token sequence (e.g., `baml.http.Request` → `"baml.http.Request"`). /// -/// Finds the first WORD token, then consumes alternating DOT + WORD pairs. +/// Finds the first WORD token, then consumes alternating DOT + identifier +/// segments. Declaration keywords are identifiers only after a dot. fn extract_dotted_name<'a>(tokens: impl Iterator) -> Option { let mut parts = Vec::new(); let mut iter = tokens.filter(|t| !t.kind().is_trivia()); @@ -21,9 +22,8 @@ fn extract_dotted_name<'a>(tokens: impl Iterator) -> Opt }; parts.push(first.text().to_string()); - // Consume alternating DOT + WORD. `spawn`/`await` are reserved keywords - // but valid as namespace segments after a `.` (e.g. `baml.spawn.SpawnParams` - // in a type annotation), mirroring the parser's segment set. + // Consume alternating DOT + identifier segments, mirroring the parser's + // qualified-name carve-out. while let Some(t) = iter.next() { if t.kind() != SyntaxKind::DOT { break; @@ -31,7 +31,13 @@ fn extract_dotted_name<'a>(tokens: impl Iterator) -> Opt let Some(word) = iter.next() else { break }; if !matches!( word.kind(), - SyntaxKind::WORD | SyntaxKind::KW_SPAWN | SyntaxKind::KW_AWAIT + SyntaxKind::WORD + | SyntaxKind::KW_SPAWN + | SyntaxKind::KW_AWAIT + | SyntaxKind::KW_CLASS + | SyntaxKind::KW_ENUM + | SyntaxKind::KW_INTERFACE + | SyntaxKind::KW_FUNCTION ) { break; } @@ -251,7 +257,19 @@ impl UnionMemberParts { let name: Vec<_> = self .tokens .iter() - .take_while(|t| matches!(t.kind(), SyntaxKind::WORD | SyntaxKind::DOT)) + .take_while(|t| { + matches!( + t.kind(), + SyntaxKind::WORD + | SyntaxKind::DOT + | SyntaxKind::KW_SPAWN + | SyntaxKind::KW_AWAIT + | SyntaxKind::KW_CLASS + | SyntaxKind::KW_ENUM + | SyntaxKind::KW_INTERFACE + | SyntaxKind::KW_FUNCTION + ) + }) .collect(); if let (Some(first), Some(last)) = (name.first(), name.last()) { return Some(rowan::TextRange::new( diff --git a/baml_language/crates/baml_fmt/src/ast/expressions.rs b/baml_language/crates/baml_fmt/src/ast/expressions.rs index 188f6c0c363..5e1dbabe474 100644 --- a/baml_language/crates/baml_fmt/src/ast/expressions.rs +++ b/baml_language/crates/baml_fmt/src/ast/expressions.rs @@ -509,7 +509,14 @@ pub struct PathExpr { fn is_path_segment_kind(kind: SyntaxKind) -> bool { matches!( kind, - SyntaxKind::WORD | SyntaxKind::KW_CLIENT | SyntaxKind::KW_SPAWN | SyntaxKind::KW_AWAIT + SyntaxKind::WORD + | SyntaxKind::KW_CLIENT + | SyntaxKind::KW_SPAWN + | SyntaxKind::KW_AWAIT + | SyntaxKind::KW_CLASS + | SyntaxKind::KW_ENUM + | SyntaxKind::KW_INTERFACE + | SyntaxKind::KW_FUNCTION ) } diff --git a/baml_language/crates/baml_fmt/src/ast/types.rs b/baml_language/crates/baml_fmt/src/ast/types.rs index cb28941d260..6894b767ed0 100644 --- a/baml_language/crates/baml_fmt/src/ast/types.rs +++ b/baml_language/crates/baml_fmt/src/ast/types.rs @@ -583,7 +583,25 @@ impl UnionTypeMember { let mut rest = Vec::new(); while let Some(dot) = it.next_if_kind(SyntaxKind::DOT) { let dot = t::Dot::from_cst(dot)?; - let word: t::Word = it.expect_parse()?; + let segment = it.expect_next("type path segment after `.`")?; + let token = StrongAstError::assert_is_token(segment)?; + if !matches!( + token.kind(), + SyntaxKind::WORD + | SyntaxKind::KW_SPAWN + | SyntaxKind::KW_AWAIT + | SyntaxKind::KW_CLASS + | SyntaxKind::KW_ENUM + | SyntaxKind::KW_INTERFACE + | SyntaxKind::KW_FUNCTION + ) { + return Err(StrongAstError::UnexpectedKindDesc { + expected_desc: "type path segment".into(), + found: token.kind(), + at: token.text_range(), + }); + } + let word = t::Word::new_from_span(token.text_range()); rest.push((dot, word)); } Ok(UnionTypeMember::Path(PathType { first, rest })) diff --git a/baml_language/crates/baml_lsp2_actions/src/check.rs b/baml_language/crates/baml_lsp2_actions/src/check.rs index 08676fd0bb4..dc4bbf5d17a 100644 --- a/baml_language/crates/baml_lsp2_actions/src/check.rs +++ b/baml_language/crates/baml_lsp2_actions/src/check.rs @@ -1951,6 +1951,7 @@ fn tir_type_error_to_diagnostic_id( DiagnosticId::NoSuchField } TirTypeError::UnknownClassPropertyShorthand { .. } => DiagnosticId::NoSuchField, + TirTypeError::CannotConstructReflectionKind { .. } => DiagnosticId::TypeMismatch, TirTypeError::UnresolvedName { .. } | TirTypeError::UnresolvedPropertyShorthand { .. } // The removed `reflect.type_of` spelling (BEP-066 I-9) is a diff --git a/baml_language/crates/baml_tests/baml_src/ns_reflect_type_of/reflect_type_of.baml b/baml_language/crates/baml_tests/baml_src/ns_reflect_type_of/reflect_type_of.baml index 615d983e673..20e42378a5c 100644 --- a/baml_language/crates/baml_tests/baml_src/ns_reflect_type_of/reflect_type_of.baml +++ b/baml_language/crates/baml_tests/baml_src/ns_reflect_type_of/reflect_type_of.baml @@ -202,9 +202,9 @@ test "of_value_array_reports_element_type" { assert.is_true(type.of_value(xs) == type.of()) } -test "of_value_of_a_type_value_is_type" { - // A `type` value's concrete type is the `type` primitive itself. - assert.is_true(type.of_value(type.of()) == type.of()) +test "of_value_of_a_type_value_is_precise_kind" { + // A reflected class type value has the sealed class-kind view as its concrete type. + assert.is_true(type.of_value(type.of()) == type.of()) } test "of_value_widened_unknown_slot" { diff --git a/baml_language/crates/baml_tests/projects/compiles/type_kinds/main.baml b/baml_language/crates/baml_tests/projects/compiles/type_kinds/main.baml new file mode 100644 index 00000000000..9b67adcf273 --- /dev/null +++ b/baml_language/crates/baml_tests/projects/compiles/type_kinds/main.baml @@ -0,0 +1,42 @@ +class Foo { + value int +} + +enum Color { + Red + Blue +} + +interface Marker {} + +type Callback = (value: int) -> bool throws never + +function classify(t: type) -> string { + match (t.kind()) { + baml.reflect.class.Type => "class", + baml.reflect.enum.Type => "enum", + baml.reflect.union.Type => "union", + baml.reflect.literal.Type => "literal", + baml.reflect.array.Type => "array", + baml.reflect.map.Type => "map", + baml.reflect.interface.Type => "interface", + baml.reflect.primitive.Type => "primitive", + baml.reflect.function.Type => "function" + } +} + +function read_class(view: baml.reflect.class.Type) -> type throws never { + view.as_type() +} + +function exercise_all_kinds() -> bool { + classify(type.of()) == "class" + && classify(type.of()) == "enum" + && classify(type.of()) == "union" + && classify(type.of<"fixed">()) == "literal" + && classify(type.of()) == "array" + && classify(type.of>()) == "map" + && classify(type.of()) == "interface" + && classify(type.of()) == "primitive" + && classify(type.of()) == "function" +} diff --git a/baml_language/crates/baml_tests/projects/diagnostic_errors/type_kinds/main.baml b/baml_language/crates/baml_tests/projects/diagnostic_errors/type_kinds/main.baml new file mode 100644 index 00000000000..1bd770d48cd --- /dev/null +++ b/baml_language/crates/baml_tests/projects/diagnostic_errors/type_kinds/main.baml @@ -0,0 +1,16 @@ +function non_exhaustive(t: type) -> string { + match (t.kind()) { + baml.reflect.class.Type => "class", + baml.reflect.enum.Type => "enum", + baml.reflect.union.Type => "union", + baml.reflect.literal.Type => "literal", + baml.reflect.array.Type => "array", + baml.reflect.map.Type => "map", + baml.reflect.interface.Type => "interface", + baml.reflect.primitive.Type => "primitive" + } +} + +function kinds_are_not_constructible() -> baml.reflect.class.Type { + baml.reflect.class.Type {} +} diff --git a/baml_language/crates/baml_tests/snapshots/baml_src/reflect_type_of.snap b/baml_language/crates/baml_tests/snapshots/baml_src/reflect_type_of.snap index 867e7395378..2e12fa135cd 100644 --- a/baml_language/crates/baml_tests/snapshots/baml_src/reflect_type_of.snap +++ b/baml_language/crates/baml_tests/snapshots/baml_src/reflect_type_of.snap @@ -1,6 +1,5 @@ --- source: crates/baml_tests/tests/baml_src.rs -assertion_line: 149 --- function reflect_type_of.$init_test_ns_reflect_type_of_reflect_type_of(registry: testing.TestCollector) -> null { load_var registry @@ -194,7 +193,7 @@ function reflect_type_of.$init_test_ns_reflect_type_of_reflect_type_of(registry: pop 1 load_var registry load_const "root.reflect_type_of" - load_const "of_value_of_a_type_value_is_type" + load_const "of_value_of_a_type_value_is_precise_kind" make_closure ., 0 load_const null call testing.TestCollector.register_test_at diff --git a/baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____03_ppir.snap b/baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____03_ppir.snap index 6d0143dab3e..52a6c43db36 100644 --- a/baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____03_ppir.snap +++ b/baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____03_ppir.snap @@ -1,6 +1,5 @@ --- source: crates/baml_tests/src/generated_tests.rs -assertion_line: 2875 --- === PPIR === @@ -1926,6 +1925,109 @@ function baml.random.random_int(self: ?) -> int [builtin] function baml.random.random_int(self: ?) -> int [builtin] function baml.random.random_int(self: ?) -> int [builtin] +--- /baml/ns_reflect/ns_array/array.baml --- +class baml.reflect.array.Type { +} +class baml.reflect.array.Type$stream { +} +function baml.reflect.array.as_type(self: ?) -> type [builtin] +function baml.reflect.array.element_type(self: ?) -> type [builtin] + +--- /baml/ns_reflect/ns_class/class.baml --- +class baml.reflect.class.Field { + name: string + type: type? + meta: baml.reflect.Meta +} +class baml.reflect.class.Field$stream { + name: string | null + type: unknown | null + meta: baml.reflect.Meta$stream | null +} +class baml.reflect.class.Type { +} +class baml.reflect.class.Type$stream { +} +function baml.reflect.class.as_type(self: ?) -> type [builtin] +function baml.reflect.class.fields(self: ?) -> baml.reflect.class.Field[] [builtin] +function baml.reflect.class.meta(self: ?) -> baml.reflect.Meta [builtin] + +--- /baml/ns_reflect/ns_enum/enum.baml --- +class baml.reflect.enum.Type { +} +class baml.reflect.enum.Type$stream { +} +class baml.reflect.enum.Value { + name: string + meta: baml.reflect.Meta +} +class baml.reflect.enum.Value$stream { + name: string | null + meta: baml.reflect.Meta$stream | null +} +function baml.reflect.enum.as_type(self: ?) -> type [builtin] +function baml.reflect.enum.meta(self: ?) -> baml.reflect.Meta [builtin] +function baml.reflect.enum.values(self: ?) -> baml.reflect.enum.Value[] [builtin] + +--- /baml/ns_reflect/ns_function/function.baml --- +class baml.reflect.function.Parameter { + name: string? + type: type + optional: bool +} +class baml.reflect.function.Parameter$stream { + name: string | null + type: unknown + optional: bool | null +} +class baml.reflect.function.Type { +} +class baml.reflect.function.Type$stream { +} +function baml.reflect.function.as_type(self: ?) -> type [builtin] +function baml.reflect.function.params(self: ?) -> baml.reflect.function.Parameter[] [builtin] +function baml.reflect.function.return_type(self: ?) -> type [builtin] + +--- /baml/ns_reflect/ns_interface/interface.baml --- +class baml.reflect.interface.Type { +} +class baml.reflect.interface.Type$stream { +} +function baml.reflect.interface.as_type(self: ?) -> type [builtin] +function baml.reflect.interface.implemented_by(self: ?, other: type) -> bool [builtin] +function baml.reflect.interface.implementors(self: ?) -> type[] [builtin] + +--- /baml/ns_reflect/ns_literal/literal.baml --- +class baml.reflect.literal.Type { +} +class baml.reflect.literal.Type$stream { +} +function baml.reflect.literal.as_type(self: ?) -> type [builtin] + +--- /baml/ns_reflect/ns_map/map.baml --- +class baml.reflect.map.Type { +} +class baml.reflect.map.Type$stream { +} +function baml.reflect.map.as_type(self: ?) -> type [builtin] +function baml.reflect.map.key_type(self: ?) -> type [builtin] +function baml.reflect.map.value_type(self: ?) -> type [builtin] + +--- /baml/ns_reflect/ns_primitive/primitive.baml --- +class baml.reflect.primitive.Type { +} +class baml.reflect.primitive.Type$stream { +} +function baml.reflect.primitive.as_type(self: ?) -> type [builtin] + +--- /baml/ns_reflect/ns_union/union.baml --- +class baml.reflect.union.Type { +} +class baml.reflect.union.Type$stream { +} +function baml.reflect.union.as_type(self: ?) -> type [builtin] +function baml.reflect.union.member_types(self: ?) -> type[] [builtin] + --- /baml/ns_reflect/reflect.baml --- class baml.reflect.Arg { name: string @@ -1945,6 +2047,18 @@ class baml.reflect.InvalidArgumentError$stream { expected: unknown got: unknown } +class baml.reflect.Meta { + alias: string? + description: string? + docstring: string? + other: map +} +class baml.reflect.Meta$stream { + alias: string | null + description: string | null + docstring: string | null + other: map +} class baml.reflect.Signature { name: string? args: baml.reflect.Arg[] @@ -1961,6 +2075,8 @@ class baml.reflect.Signature$stream { errors: unknown docstring: string | null } +type baml.reflect.TypeKind = baml.reflect.class.Type | baml.reflect.enum.Type | baml.reflect.union.Type | baml.reflect.literal.Type | baml.reflect.array.Type | baml.reflect.map.Type | baml.reflect.interface.Type | baml.reflect.primitive.Type | baml.reflect.function.Type +type baml.reflect.TypeKind$stream = baml.reflect.class.Type$stream | baml.reflect.enum.Type$stream | baml.reflect.union.Type$stream | baml.reflect.literal.Type$stream | baml.reflect.array.Type$stream | baml.reflect.map.Type$stream | baml.reflect.interface.Type$stream | baml.reflect.primitive.Type$stream | baml.reflect.function.Type$stream function baml.reflect.call_any(f: baml.AnyFunction, args: map) -> R [builtin] function baml.reflect.signature(f: baml.AnyFunction) -> baml.reflect.Signature [builtin] @@ -2532,9 +2648,19 @@ class baml.TypeValue { class baml.TypeValue$stream { } function baml._to_string_impl(self: ?) -> string [builtin] +function baml.as_array(self: ?) -> baml.reflect.array.Type? [builtin] +function baml.as_class(self: ?) -> baml.reflect.class.Type? [builtin] +function baml.as_enum(self: ?) -> baml.reflect.enum.Type? [builtin] +function baml.as_function(self: ?) -> baml.reflect.function.Type? [builtin] +function baml.as_interface(self: ?) -> baml.reflect.interface.Type? [builtin] +function baml.as_literal(self: ?) -> baml.reflect.literal.Type? [builtin] +function baml.as_map(self: ?) -> baml.reflect.map.Type? [builtin] +function baml.as_primitive(self: ?) -> baml.reflect.primitive.Type? [builtin] +function baml.as_union(self: ?) -> baml.reflect.union.Type? [builtin] function baml.implemented_by(self: ?, other: type) -> bool [builtin] function baml.implementors(self: ?) -> type[] [builtin] function baml.implements(self: ?, other: type) -> bool [builtin] +function baml.kind(self: ?) -> baml.reflect.TypeKind [builtin] function baml.to_string(self: ?) -> string [expr] { { } self._to_string_impl() } diff --git a/baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_5_mir.snap b/baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_5_mir.snap index 69dfbb7b25b..d5d8f400f92 100644 --- a/baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_5_mir.snap +++ b/baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_5_mir.snap @@ -1,6 +1,5 @@ --- source: crates/baml_tests/src/generated_tests.rs -assertion_line: 2966 --- === MIR2 === @@ -9017,6 +9016,48 @@ fn baml.random.ChaCha20.Rng.random = builtin(vm) fn baml.random.ChaCha20.Rng.random_int = builtin(vm) +fn baml.reflect.array.Type.baml.reflect.TypeView.as_type = builtin(vm) + +fn baml.reflect.array.Type.element_type = builtin(vm) + +fn baml.reflect.class.Type.baml.reflect.TypeView.as_type = builtin(vm) + +fn baml.reflect.class.Type.fields = builtin(vm) + +fn baml.reflect.class.Type.meta = builtin(vm) + +fn baml.reflect.enum.Type.baml.reflect.TypeView.as_type = builtin(vm) + +fn baml.reflect.enum.Type.values = builtin(vm) + +fn baml.reflect.enum.Type.meta = builtin(vm) + +fn baml.reflect.function.Type.baml.reflect.TypeView.as_type = builtin(vm) + +fn baml.reflect.function.Type.params = builtin(vm) + +fn baml.reflect.function.Type.return_type = builtin(vm) + +fn baml.reflect.interface.Type.baml.reflect.TypeView.as_type = builtin(vm) + +fn baml.reflect.interface.Type.implemented_by = builtin(vm) + +fn baml.reflect.interface.Type.implementors = builtin(vm) + +fn baml.reflect.literal.Type.baml.reflect.TypeView.as_type = builtin(vm) + +fn baml.reflect.map.Type.baml.reflect.TypeView.as_type = builtin(vm) + +fn baml.reflect.map.Type.key_type = builtin(vm) + +fn baml.reflect.map.Type.value_type = builtin(vm) + +fn baml.reflect.primitive.Type.baml.reflect.TypeView.as_type = builtin(vm) + +fn baml.reflect.union.Type.baml.reflect.TypeView.as_type = builtin(vm) + +fn baml.reflect.union.Type.member_types = builtin(vm) + fn baml.reflect.signature = builtin(vm) fn baml.reflect.call_any = builtin(vm) @@ -12301,6 +12342,26 @@ fn baml.String.to_code_points = builtin(vm) fn baml.String.from_code_points = builtin(vm) +fn baml.TypeValue.kind = builtin(vm) + +fn baml.TypeValue.as_class = builtin(vm) + +fn baml.TypeValue.as_enum = builtin(vm) + +fn baml.TypeValue.as_union = builtin(vm) + +fn baml.TypeValue.as_literal = builtin(vm) + +fn baml.TypeValue.as_array = builtin(vm) + +fn baml.TypeValue.as_map = builtin(vm) + +fn baml.TypeValue.as_interface = builtin(vm) + +fn baml.TypeValue.as_primitive = builtin(vm) + +fn baml.TypeValue.as_function = builtin(vm) + fn baml.TypeValue.baml.ToString.to_string(self: baml.TypeValue) -> string { // Locals: let _0: string // _0 // return diff --git a/baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_tir.snap b/baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_tir.snap index 14cc250678e..0f1336cd4a6 100644 --- a/baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_tir.snap +++ b/baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_tir.snap @@ -1,6 +1,5 @@ --- source: crates/baml_tests/src/generated_tests.rs -assertion_line: 2915 --- === TIR2 === @@ -2989,11 +2988,100 @@ class baml.random.ChaCha20$stream { _state: $rust_type } +--- /baml/ns_reflect/ns_array/array.baml --- +class baml.reflect.array.Type { +} +class baml.reflect.array.Type$stream { +} + +--- /baml/ns_reflect/ns_class/class.baml --- +class baml.reflect.class.Field { + name: string + type: type | null + meta: baml.reflect.Meta +} +class baml.reflect.class.Type { +} +class baml.reflect.class.Field$stream { + name: string | null + type: unknown | null + meta: baml.reflect.Meta$stream | null +} +class baml.reflect.class.Type$stream { +} + +--- /baml/ns_reflect/ns_enum/enum.baml --- +class baml.reflect.enum.Value { + name: string + meta: baml.reflect.Meta +} +class baml.reflect.enum.Type { +} +class baml.reflect.enum.Value$stream { + name: string | null + meta: baml.reflect.Meta$stream | null +} +class baml.reflect.enum.Type$stream { +} + +--- /baml/ns_reflect/ns_function/function.baml --- +class baml.reflect.function.Parameter { + name: string | null + type: type + optional: bool +} +class baml.reflect.function.Type { +} +class baml.reflect.function.Parameter$stream { + name: string | null + type: unknown + optional: bool | null +} +class baml.reflect.function.Type$stream { +} + +--- /baml/ns_reflect/ns_interface/interface.baml --- +class baml.reflect.interface.Type { +} +class baml.reflect.interface.Type$stream { +} + +--- /baml/ns_reflect/ns_literal/literal.baml --- +class baml.reflect.literal.Type { +} +class baml.reflect.literal.Type$stream { +} + +--- /baml/ns_reflect/ns_map/map.baml --- +class baml.reflect.map.Type { +} +class baml.reflect.map.Type$stream { +} + +--- /baml/ns_reflect/ns_primitive/primitive.baml --- +class baml.reflect.primitive.Type { +} +class baml.reflect.primitive.Type$stream { +} + +--- /baml/ns_reflect/ns_union/union.baml --- +class baml.reflect.union.Type { +} +class baml.reflect.union.Type$stream { +} + --- /baml/ns_reflect/reflect.baml --- class baml.reflect.Arg { name: string type: type } +class baml.reflect.Meta { + alias: string | null + description: string | null + docstring: string | null + other: map +} +type baml.reflect.TypeKind = baml.reflect.class.Type | baml.reflect.enum.Type | baml.reflect.union.Type | baml.reflect.literal.Type | baml.reflect.array.Type | baml.reflect.map.Type | baml.reflect.interface.Type | baml.reflect.primitive.Type | baml.reflect.function.Type class baml.reflect.Signature { name: string | null args: baml.reflect.Arg[] @@ -3011,6 +3099,13 @@ class baml.reflect.Arg$stream { name: string | null type: unknown } +class baml.reflect.Meta$stream { + alias: string | null + description: string | null + docstring: string | null + other: map +} +type baml.reflect.TypeKind$stream = baml.reflect.class.Type$stream | baml.reflect.enum.Type$stream | baml.reflect.union.Type$stream | baml.reflect.literal.Type$stream | baml.reflect.array.Type$stream | baml.reflect.map.Type$stream | baml.reflect.interface.Type$stream | baml.reflect.primitive.Type$stream | baml.reflect.function.Type$stream class baml.reflect.Signature$stream { name: string | null args: baml.reflect.Arg$stream[] diff --git a/baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____06_codegen.snap b/baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____06_codegen.snap index 83d5c0d9830..98c7affa9c3 100644 --- a/baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____06_codegen.snap +++ b/baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____06_codegen.snap @@ -1,6 +1,5 @@ --- source: crates/baml_tests/src/generated_tests.rs -assertion_line: 3105 --- function baml.Array.at(self: baml.Array<#0>, index: int) -> #0 | null { } @@ -918,6 +917,33 @@ function baml.ToString.to_string(self: baml.ToString) -> string { function baml.TypeValue._to_string_impl(self: baml.TypeValue) -> string { } +function baml.TypeValue.as_array(self: baml.TypeValue) -> baml.reflect.array.Type | null { +} + +function baml.TypeValue.as_class(self: baml.TypeValue) -> baml.reflect.class.Type | null { +} + +function baml.TypeValue.as_enum(self: baml.TypeValue) -> baml.reflect.enum.Type | null { +} + +function baml.TypeValue.as_function(self: baml.TypeValue) -> baml.reflect.function.Type | null { +} + +function baml.TypeValue.as_interface(self: baml.TypeValue) -> baml.reflect.interface.Type | null { +} + +function baml.TypeValue.as_literal(self: baml.TypeValue) -> baml.reflect.literal.Type | null { +} + +function baml.TypeValue.as_map(self: baml.TypeValue) -> baml.reflect.map.Type | null { +} + +function baml.TypeValue.as_primitive(self: baml.TypeValue) -> baml.reflect.primitive.Type | null { +} + +function baml.TypeValue.as_union(self: baml.TypeValue) -> baml.reflect.union.Type | null { +} + function baml.TypeValue.baml.ToString.to_string(self: baml.TypeValue) -> string { load_var self call baml.TypeValue._to_string_impl @@ -933,6 +959,9 @@ function baml.TypeValue.implementors(self: baml.TypeValue) -> type[] { function baml.TypeValue.implements(self: baml.TypeValue, other: type) -> bool { } +function baml.TypeValue.kind(self: baml.TypeValue) -> baml.reflect.class.Type | baml.reflect.enum.Type | baml.reflect.union.Type | baml.reflect.literal.Type | baml.reflect.array.Type | baml.reflect.map.Type | baml.reflect.interface.Type | baml.reflect.primitive.Type | baml.reflect.function.Type { +} + function baml.Uint8Array._to_string_impl(self: baml.Uint8Array) -> string { } @@ -6295,12 +6324,75 @@ function baml.random.Xoshiro256PlusPlus.new(seed: uint8array) -> baml.random.Xos return } +function baml.reflect.array.Type.baml.reflect.TypeView.as_type(self: baml.reflect.array.Type) -> type { +} + +function baml.reflect.array.Type.element_type(self: baml.reflect.array.Type) -> type { +} + function baml.reflect.call_any(f: baml.AnyFunction, args: map) -> #0 { } +function baml.reflect.class.Type.baml.reflect.TypeView.as_type(self: baml.reflect.class.Type) -> type { +} + +function baml.reflect.class.Type.fields(self: baml.reflect.class.Type) -> baml.reflect.class.Field[] { +} + +function baml.reflect.class.Type.meta(self: baml.reflect.class.Type) -> baml.reflect.Meta { +} + +function baml.reflect.enum.Type.baml.reflect.TypeView.as_type(self: baml.reflect.enum.Type) -> type { +} + +function baml.reflect.enum.Type.meta(self: baml.reflect.enum.Type) -> baml.reflect.Meta { +} + +function baml.reflect.enum.Type.values(self: baml.reflect.enum.Type) -> baml.reflect.enum.Value[] { +} + +function baml.reflect.function.Type.baml.reflect.TypeView.as_type(self: baml.reflect.function.Type) -> type { +} + +function baml.reflect.function.Type.params(self: baml.reflect.function.Type) -> baml.reflect.function.Parameter[] { +} + +function baml.reflect.function.Type.return_type(self: baml.reflect.function.Type) -> type { +} + +function baml.reflect.interface.Type.baml.reflect.TypeView.as_type(self: baml.reflect.interface.Type) -> type { +} + +function baml.reflect.interface.Type.implemented_by(self: baml.reflect.interface.Type, other: type) -> bool { +} + +function baml.reflect.interface.Type.implementors(self: baml.reflect.interface.Type) -> type[] { +} + +function baml.reflect.literal.Type.baml.reflect.TypeView.as_type(self: baml.reflect.literal.Type) -> type { +} + +function baml.reflect.map.Type.baml.reflect.TypeView.as_type(self: baml.reflect.map.Type) -> type { +} + +function baml.reflect.map.Type.key_type(self: baml.reflect.map.Type) -> type { +} + +function baml.reflect.map.Type.value_type(self: baml.reflect.map.Type) -> type { +} + +function baml.reflect.primitive.Type.baml.reflect.TypeView.as_type(self: baml.reflect.primitive.Type) -> type { +} + function baml.reflect.signature(f: baml.AnyFunction) -> baml.reflect.Signature { } +function baml.reflect.union.Type.baml.reflect.TypeView.as_type(self: baml.reflect.union.Type) -> type { +} + +function baml.reflect.union.Type.member_types(self: baml.reflect.union.Type) -> type[] { +} + function baml.sap.parse(text: string) -> #0 { load_type #0 load_type #0 diff --git a/baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__03_ppir.snap b/baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__03_ppir.snap new file mode 100644 index 00000000000..c2bf226a953 --- /dev/null +++ b/baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__03_ppir.snap @@ -0,0 +1,22 @@ +--- +source: crates/baml_tests/src/generated_tests.rs +--- +=== PPIR === +class user.Foo { + value: int +} +class user.Foo$stream { + value: int | null +} +enum user.Color {Red, Blue} +type user.Callback = (value: int) -> bool throws never +type user.Callback$stream = unknown +function user.classify(t: type) -> string [expr] { + { } match (t.kind()) { baml.reflect.class.Type => "class", baml.reflect.enum.Type => "enum", baml.reflect.union.Type => "union", baml.reflect.literal.Type => "literal", baml.reflect.array.Type => "array", baml.reflect.map.Type => "map", baml.reflect.interface.Type => "interface", baml.reflect.primitive.Type => "primitive", baml.reflect.function.Type => "function" } +} +function user.exercise_all_kinds() -> bool [expr] { + { } classify(type.of()) Eq "class" And classify(type.of()) Eq "enum" And classify(type.of()) Eq "union" And classify(type.of()) Eq "literal" And classify(type.of()) Eq "array" And classify(type.of()) Eq "map" And classify(type.of()) Eq "interface" And classify(type.of()) Eq "primitive" And classify(type.of()) Eq "function" +} +function user.read_class(view: baml.reflect.class.Type) -> type [expr] { + { } view.as_type() +} diff --git a/baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__04_5_mir.snap b/baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__04_5_mir.snap new file mode 100644 index 00000000000..8741d1a0891 --- /dev/null +++ b/baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__04_5_mir.snap @@ -0,0 +1,314 @@ +--- +source: crates/baml_tests/src/generated_tests.rs +--- +=== MIR2 === +fn user.classify(t: type) -> string { + // Locals: + let _0: string // _0 // return + let _1: type // t // param + let _2: baml.reflect.array.Type | baml.reflect.class.Type | baml.reflect.enum.Type | baml.reflect.function.Type | baml.reflect.interface.Type | baml.reflect.literal.Type | baml.reflect.map.Type | baml.reflect.primitive.Type | baml.reflect.union.Type + let _3: bool + let _4: bool + let _5: bool + let _6: bool + let _7: bool + let _8: bool + let _9: bool + let _10: bool + + bb0: { + _2 = call const fn baml.TypeValue.kind(copy _1) -> [bb1]; + } + + bb1: { + _3 = is_type(copy _2, baml.reflect.class.Type); + branch copy _3 -> [bb17, bb2]; + } + + bb2: { + _4 = is_type(copy _2, baml.reflect.enum.Type); + branch copy _4 -> [bb16, bb3]; + } + + bb3: { + _5 = is_type(copy _2, baml.reflect.union.Type); + branch copy _5 -> [bb15, bb4]; + } + + bb4: { + _6 = is_type(copy _2, baml.reflect.literal.Type); + branch copy _6 -> [bb14, bb5]; + } + + bb5: { + _7 = is_type(copy _2, baml.reflect.array.Type); + branch copy _7 -> [bb13, bb6]; + } + + bb6: { + _8 = is_type(copy _2, baml.reflect.map.Type); + branch copy _8 -> [bb12, bb7]; + } + + bb7: { + _9 = is_type(copy _2, baml.reflect.interface.Type); + branch copy _9 -> [bb11, bb8]; + } + + bb8: { + _10 = is_type(copy _2, baml.reflect.primitive.Type); + branch copy _10 -> [bb10, bb9]; + } + + bb9: { + _0 = const "function"; + goto -> bb18; + } + + bb10: { + _0 = const "primitive"; + goto -> bb18; + } + + bb11: { + _0 = const "interface"; + goto -> bb18; + } + + bb12: { + _0 = const "map"; + goto -> bb18; + } + + bb13: { + _0 = const "array"; + goto -> bb18; + } + + bb14: { + _0 = const "literal"; + goto -> bb18; + } + + bb15: { + _0 = const "union"; + goto -> bb18; + } + + bb16: { + _0 = const "enum"; + goto -> bb18; + } + + bb17: { + _0 = const "class"; + goto -> bb18; + } + + bb18: { + return; + } +} + +fn user.read_class(view: baml.reflect.class.Type) -> type { + // Locals: + let _0: type // _0 // return + let _1: baml.reflect.class.Type // view // param + + bb0: { + _0 = virtual_call as_type as baml.reflect.TypeView(copy _1) -> [bb1]; + } + + bb1: { + return; + } +} + +fn user.exercise_all_kinds() -> bool { + // Locals: + let _0: bool // _0 // return + let _1: bool + let _2: bool + let _3: bool + let _4: bool + let _5: bool + let _6: bool + let _7: bool + let _8: bool + let _9: string + let _10: type + let _11: string + let _12: type + let _13: string + let _14: type + let _15: string + let _16: type + let _17: string + let _18: type + let _19: string + let _20: type + let _21: string + let _22: type + let _23: string + let _24: type + let _25: string + let _26: type + + bb0: { + _10 = load_type(Foo); + goto -> bb1; + } + + bb1: { + _9 = call const fn user.classify(copy _10) -> [bb2]; + } + + bb2: { + _8 = copy _9 == const "class"; + _7 = short_circuit(&&) copy _8 -> [eval: bb3, join: bb6]; + } + + bb3: { + _12 = load_type(Color); + goto -> bb4; + } + + bb4: { + _11 = call const fn user.classify(copy _12) -> [bb5]; + } + + bb5: { + _7 = copy _11 == const "enum"; + goto -> bb6; + } + + bb6: { + _6 = short_circuit(&&) copy _7 -> [eval: bb7, join: bb10]; + } + + bb7: { + _14 = load_type(int | string); + goto -> bb8; + } + + bb8: { + _13 = call const fn user.classify(copy _14) -> [bb9]; + } + + bb9: { + _6 = copy _13 == const "union"; + goto -> bb10; + } + + bb10: { + _5 = short_circuit(&&) copy _6 -> [eval: bb11, join: bb14]; + } + + bb11: { + _16 = load_type("fixed"); + goto -> bb12; + } + + bb12: { + _15 = call const fn user.classify(copy _16) -> [bb13]; + } + + bb13: { + _5 = copy _15 == const "literal"; + goto -> bb14; + } + + bb14: { + _4 = short_circuit(&&) copy _5 -> [eval: bb15, join: bb18]; + } + + bb15: { + _18 = load_type(Foo[]); + goto -> bb16; + } + + bb16: { + _17 = call const fn user.classify(copy _18) -> [bb17]; + } + + bb17: { + _4 = copy _17 == const "array"; + goto -> bb18; + } + + bb18: { + _3 = short_circuit(&&) copy _4 -> [eval: bb19, join: bb22]; + } + + bb19: { + _20 = load_type(map); + goto -> bb20; + } + + bb20: { + _19 = call const fn user.classify(copy _20) -> [bb21]; + } + + bb21: { + _3 = copy _19 == const "map"; + goto -> bb22; + } + + bb22: { + _2 = short_circuit(&&) copy _3 -> [eval: bb23, join: bb26]; + } + + bb23: { + _22 = load_type(Marker); + goto -> bb24; + } + + bb24: { + _21 = call const fn user.classify(copy _22) -> [bb25]; + } + + bb25: { + _2 = copy _21 == const "interface"; + goto -> bb26; + } + + bb26: { + _1 = short_circuit(&&) copy _2 -> [eval: bb27, join: bb30]; + } + + bb27: { + _24 = load_type(int); + goto -> bb28; + } + + bb28: { + _23 = call const fn user.classify(copy _24) -> [bb29]; + } + + bb29: { + _1 = copy _23 == const "primitive"; + goto -> bb30; + } + + bb30: { + _0 = short_circuit(&&) copy _1 -> [eval: bb31, join: bb34]; + } + + bb31: { + _26 = load_type((int) -> bool throws never); + goto -> bb32; + } + + bb32: { + _25 = call const fn user.classify(copy _26) -> [bb33]; + } + + bb33: { + _0 = copy _25 == const "function"; + goto -> bb34; + } + + bb34: { + return; + } +} diff --git a/baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__04_tir.snap b/baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__04_tir.snap new file mode 100644 index 00000000000..d1c41e76b17 --- /dev/null +++ b/baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__04_tir.snap @@ -0,0 +1,46 @@ +--- +source: crates/baml_tests/src/generated_tests.rs +--- +=== TIR2 === +class user.Foo { + value: int +} +enum user.Color +type user.Callback = (value: int) -> bool throws never +function user.classify(t: type) -> string throws never { + { : "class" | "enum" | "union" | "literal" | "array" | "map" | "interface" | "primitive" | "function" + match (t.kind() : baml.reflect.TypeKind) : "class" | "enum" | "union" | "literal" | "array" | "map" | "interface" | "primitive" | "function" + baml.reflect.class.Type => + "class" : "class" + baml.reflect.enum.Type => + "enum" : "enum" + baml.reflect.union.Type => + "union" : "union" + baml.reflect.literal.Type => + "literal" : "literal" + baml.reflect.array.Type => + "array" : "array" + baml.reflect.map.Type => + "map" : "map" + baml.reflect.interface.Type => + "interface" : "interface" + baml.reflect.primitive.Type => + "primitive" : "primitive" + baml.reflect.function.Type => + "function" : "function" + } +} +function user.read_class(view: baml.reflect.class.Type) -> type throws never { + { : type + view.as_type() : type + } +} +function user.exercise_all_kinds() -> bool throws never { + { : bool + classify(type.of()) == "class" && classify(type.of()) == "enum" && classify(type.of()) == "union" && classify(type.of<"fixed">()) == "literal" && classify(type.of()) == "array" && classify(type.of>()) == "map" && classify(type.of()) == "interface" && classify(type.of()) == "primitive" && classify(type.of()) == "function" : bool + } +} +class user.Foo$stream { + value: int | null +} +type user.Callback$stream = unknown diff --git a/baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__05_diagnostics.snap b/baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__05_diagnostics.snap new file mode 100644 index 00000000000..40cf564409c --- /dev/null +++ b/baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__05_diagnostics.snap @@ -0,0 +1,5 @@ +--- +source: crates/baml_tests/src/generated_tests.rs +--- +=== COMPILER2 DIAGNOSTICS === +No errors found. diff --git a/baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__06_codegen.snap b/baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__06_codegen.snap new file mode 100644 index 00000000000..90bfb9f2b66 --- /dev/null +++ b/baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__06_codegen.snap @@ -0,0 +1,183 @@ +--- +source: crates/baml_tests/src/generated_tests.rs +--- +function boundary.LocalId.capture(self: boundary.LocalId, inputs: bool | null, output: bool | null, error: bool | null) -> boundary.LocalId { +} + +function boundary.id() -> boundary.LocalId { +} + +function boundary.id.current() -> string { +} + +function user.classify(t: type) -> string { + load_var t + call baml.TypeValue.kind + store_var _2 + load_var _2 + is_type baml.reflect.class.Type + pop_jump_if_false L0 + jump L15 + + L0: + load_var _2 + is_type baml.reflect.enum.Type + pop_jump_if_false L1 + jump L14 + + L1: + load_var _2 + is_type baml.reflect.union.Type + pop_jump_if_false L2 + jump L13 + + L2: + load_var _2 + is_type baml.reflect.literal.Type + pop_jump_if_false L3 + jump L12 + + L3: + load_var _2 + is_type baml.reflect.array.Type + pop_jump_if_false L4 + jump L11 + + L4: + load_var _2 + is_type baml.reflect.map.Type + pop_jump_if_false L5 + jump L10 + + L5: + load_var _2 + is_type baml.reflect.interface.Type + pop_jump_if_false L6 + jump L9 + + L6: + load_var _2 + is_type baml.reflect.primitive.Type + pop_jump_if_false L7 + jump L8 + + L7: + load_const "function" + jump L16 + + L8: + load_const "primitive" + jump L16 + + L9: + load_const "interface" + jump L16 + + L10: + load_const "map" + jump L16 + + L11: + load_const "array" + jump L16 + + L12: + load_const "literal" + jump L16 + + L13: + load_const "union" + jump L16 + + L14: + load_const "enum" + jump L16 + + L15: + load_const "class" + + L16: + return +} + +function user.exercise_all_kinds() -> bool { + load_type Foo + call user.classify + load_const "class" + cmp_op == + jump_if_false L0 + pop 1 + load_type Color + call user.classify + load_const "enum" + cmp_op == + + L0: + jump_if_false L1 + pop 1 + load_type int | string + call user.classify + load_const "union" + cmp_op == + + L1: + jump_if_false L2 + pop 1 + load_type "fixed" + call user.classify + load_const "literal" + cmp_op == + + L2: + jump_if_false L3 + pop 1 + load_type Foo[] + call user.classify + load_const "array" + cmp_op == + + L3: + jump_if_false L4 + pop 1 + load_type map + call user.classify + load_const "map" + cmp_op == + + L4: + jump_if_false L5 + pop 1 + load_type Marker + call user.classify + load_const "interface" + cmp_op == + + L5: + jump_if_false L6 + pop 1 + load_type int + call user.classify + load_const "primitive" + cmp_op == + + L6: + jump_if_false L7 + pop 1 + load_type (int) -> bool throws never + call user.classify + load_const "function" + cmp_op == + + L7: + return +} + +function user.read_class(view: baml.reflect.class.Type) -> type { + load_var view + load_type baml.reflect.TypeView + load_const "as_type" + virtual_call nargs=1 ntypeargs=0 + store_var _0 + load_var _0 + return +} diff --git a/baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__10_formatter__main.snap b/baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__10_formatter__main.snap new file mode 100644 index 00000000000..194ea799715 --- /dev/null +++ b/baml_language/crates/baml_tests/snapshots/compiles/type_kinds/baml_tests__compiles__type_kinds__10_formatter__main.snap @@ -0,0 +1,45 @@ +--- +source: crates/baml_tests/src/generated_tests.rs +--- +class Foo { + value: int, +} + +enum Color { + Red, + Blue, +} + +interface Marker {} + +type Callback = (value: int) -> bool throws never; + +function classify(t: type) -> string { + match (t.kind()) { + baml.reflect.class.Type => "class", + baml.reflect.enum.Type => "enum", + baml.reflect.union.Type => "union", + baml.reflect.literal.Type => "literal", + baml.reflect.array.Type => "array", + baml.reflect.map.Type => "map", + baml.reflect.interface.Type => "interface", + baml.reflect.primitive.Type => "primitive", + baml.reflect.function.Type => "function", + } +} + +function read_class(view: baml.reflect.class.Type) -> type throws never { + view.as_type() +} + +function exercise_all_kinds() -> bool { + classify(type.of()) == "class" + && classify(type.of()) == "enum" + && classify(type.of()) == "union" + && classify(type.of<"fixed">()) == "literal" + && classify(type.of()) == "array" + && classify(type.of>()) == "map" + && classify(type.of()) == "interface" + && classify(type.of()) == "primitive" + && classify(type.of()) == "function" +} diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/type_kinds/baml_tests__diagnostic_errors__type_kinds__03_ppir.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/type_kinds/baml_tests__diagnostic_errors__type_kinds__03_ppir.snap new file mode 100644 index 00000000000..b9d9ce6f4a2 --- /dev/null +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/type_kinds/baml_tests__diagnostic_errors__type_kinds__03_ppir.snap @@ -0,0 +1,10 @@ +--- +source: crates/baml_tests/src/generated_tests.rs +--- +=== PPIR === +function user.kinds_are_not_constructible() -> baml.reflect.class.Type [expr] { + { } baml.reflect.class.Type { } +} +function user.non_exhaustive(t: type) -> string [expr] { + { } match (t.kind()) { baml.reflect.class.Type => "class", baml.reflect.enum.Type => "enum", baml.reflect.union.Type => "union", baml.reflect.literal.Type => "literal", baml.reflect.array.Type => "array", baml.reflect.map.Type => "map", baml.reflect.interface.Type => "interface", baml.reflect.primitive.Type => "primitive" } +} diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/type_kinds/baml_tests__diagnostic_errors__type_kinds__04_tir.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/type_kinds/baml_tests__diagnostic_errors__type_kinds__04_tir.snap new file mode 100644 index 00000000000..decd59e6897 --- /dev/null +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/type_kinds/baml_tests__diagnostic_errors__type_kinds__04_tir.snap @@ -0,0 +1,32 @@ +--- +source: crates/baml_tests/src/generated_tests.rs +--- +=== TIR2 === +function user.non_exhaustive(t: type) -> string throws never { + { : "class" | "enum" | "union" | "literal" | "array" | "map" | "interface" | "primitive" + match (t.kind() : baml.reflect.TypeKind) : "class" | "enum" | "union" | "literal" | "array" | "map" | "interface" | "primitive" + baml.reflect.class.Type => + "class" : "class" + baml.reflect.enum.Type => + "enum" : "enum" + baml.reflect.union.Type => + "union" : "union" + baml.reflect.literal.Type => + "literal" : "literal" + baml.reflect.array.Type => + "array" : "array" + baml.reflect.map.Type => + "map" : "map" + baml.reflect.interface.Type => + "interface" : "interface" + baml.reflect.primitive.Type => + "primitive" : "primitive" + } + !! 47..402: non-exhaustive match on `baml.reflect.TypeKind`; missing: baml.reflect.function.Type {} +} +function user.kinds_are_not_constructible() -> baml.reflect.class.Type throws never { + { : baml.reflect.class.Type + baml.reflect.class.Type { } : baml.reflect.class.Type + } + !! 476..502: reflection kind `baml.reflect.class.Type` cannot be constructed; obtain it from a type value +} diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/type_kinds/baml_tests__diagnostic_errors__type_kinds__05_diagnostics.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/type_kinds/baml_tests__diagnostic_errors__type_kinds__05_diagnostics.snap new file mode 100644 index 00000000000..a6cf0d1003e --- /dev/null +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/type_kinds/baml_tests__diagnostic_errors__type_kinds__05_diagnostics.snap @@ -0,0 +1,27 @@ +--- +source: crates/baml_tests/src/generated_tests.rs +--- +=== COMPILER2 DIAGNOSTICS === + [type] E0062 + + × non-exhaustive match on type baml.reflect.TypeKind; missing: baml.reflect.function.Type {} + ╭─[main.baml:2:3] + 2 │ ╭─▶ match (t.kind()) { + 3 │ │ baml.reflect.class.Type => "class", + 4 │ │ baml.reflect.enum.Type => "enum", + 5 │ │ baml.reflect.union.Type => "union", + 6 │ │ baml.reflect.literal.Type => "literal", + 7 │ │ baml.reflect.array.Type => "array", + 8 │ │ baml.reflect.map.Type => "map", + 9 │ │ baml.reflect.interface.Type => "interface", + 10 │ │ baml.reflect.primitive.Type => "primitive" + 11 │ ╰─▶ } + ╰──── + + [type] E0001 + + × reflection kind `baml.reflect.class.Type` cannot be constructed; obtain it from a type value + ╭─[main.baml:15:3] + 15 │ baml.reflect.class.Type {} + · ────────────────────────── + ╰──── diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/type_kinds/baml_tests__diagnostic_errors__type_kinds__10_formatter__main.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/type_kinds/baml_tests__diagnostic_errors__type_kinds__10_formatter__main.snap new file mode 100644 index 00000000000..3293bec1c66 --- /dev/null +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/type_kinds/baml_tests__diagnostic_errors__type_kinds__10_formatter__main.snap @@ -0,0 +1,19 @@ +--- +source: crates/baml_tests/src/generated_tests.rs +--- +function non_exhaustive(t: type) -> string { + match (t.kind()) { + baml.reflect.class.Type => "class", + baml.reflect.enum.Type => "enum", + baml.reflect.union.Type => "union", + baml.reflect.literal.Type => "literal", + baml.reflect.array.Type => "array", + baml.reflect.map.Type => "map", + baml.reflect.interface.Type => "interface", + baml.reflect.primitive.Type => "primitive", + } +} + +function kinds_are_not_constructible() -> baml.reflect.class.Type { + baml.reflect.class.Type { } +} diff --git a/baml_language/crates/baml_tests/src/compiler2_tir/snapshots/baml_tests__compiler2_tir__phase5__snapshot_baml_package_items.snap b/baml_language/crates/baml_tests/src/compiler2_tir/snapshots/baml_tests__compiler2_tir__phase5__snapshot_baml_package_items.snap index 897bb6266bf..212e8f5a126 100644 --- a/baml_language/crates/baml_tests/src/compiler2_tir/snapshots/baml_tests__compiler2_tir__phase5__snapshot_baml_package_items.snap +++ b/baml_language/crates/baml_tests/src/compiler2_tir/snapshots/baml_tests__compiler2_tir__phase5__snapshot_baml_package_items.snap @@ -1,6 +1,5 @@ --- source: crates/baml_tests/src/compiler2_tir/phase5.rs -assertion_line: 396 expression: output --- namespace baml: @@ -22,7 +21,7 @@ namespace baml: class TaggedString { methods: [] } type ToJson type ToString - class TypeValue { methods: [_to_string_impl, implements, implemented_by, implementors, to_string] } + class TypeValue { methods: [kind, as_class, as_enum, as_union, as_literal, as_array, as_map, as_interface, as_primitive, as_function, _to_string_impl, implements, implemented_by, implementors, to_string] } class Uint8Array { methods: [length, at, push, pop, concat, includes, reverse, slice, zeroes, from_array, to_array, from_hex, to_hex, from_base64, to_base64, _to_string_impl, sort, to_string] } function _cleanup_begin function _compare_shim @@ -294,9 +293,33 @@ namespace baml.random: namespace baml.reflect: class Arg { methods: [] } class InvalidArgumentError { methods: [] } + class Meta { methods: [] } class Signature { methods: [] } + type TypeKind + type TypeView function call_any function signature +namespace baml.reflect.array: + class Type { methods: [element_type, as_type] } +namespace baml.reflect.class: + class Field { methods: [] } + class Type { methods: [fields, meta, as_type] } +namespace baml.reflect.enum: + class Type { methods: [values, meta, as_type] } + class Value { methods: [] } +namespace baml.reflect.function: + class Parameter { methods: [] } + class Type { methods: [params, return_type, as_type] } +namespace baml.reflect.interface: + class Type { methods: [implemented_by, implementors, as_type] } +namespace baml.reflect.literal: + class Type { methods: [as_type] } +namespace baml.reflect.map: + class Type { methods: [key_type, value_type, as_type] } +namespace baml.reflect.primitive: + class Type { methods: [as_type] } +namespace baml.reflect.union: + class Type { methods: [member_types, as_type] } namespace baml.sap: function parse function parse_type diff --git a/baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded.snap b/baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded.snap index 15e2db79a77..516b09c118e 100644 --- a/baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded.snap +++ b/baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded.snap @@ -1,6 +1,5 @@ --- source: crates/baml_tests/tests/bytecode_format/main.rs -assertion_line: 42 --- function assert.approx_equal(actual: float, expected: float, eps: float) -> null { 77 0 load_var 3 (eps) @@ -34,19 +33,19 @@ function assert.approx_equal(actual: float, expected: float, eps: float) -> null 81 24 jump +32 (to 56) 84 25 load_var 4 (delta) - 26 call 796 ntypeargs=0 (assert.format_operand) + 26 call 827 ntypeargs=0 (assert.format_operand) 27 store_var 5 (_18) 86 28 load_var 3 (eps) - 29 call 796 ntypeargs=0 (assert.format_operand) + 29 call 827 ntypeargs=0 (assert.format_operand) 30 store_var 6 (_20) 88 31 load_var 1 (actual) - 32 call 796 ntypeargs=0 (assert.format_operand) + 32 call 827 ntypeargs=0 (assert.format_operand) 33 store_var 7 (_21) 90 34 load_var 2 (expected) - 35 call 796 ntypeargs=0 (assert.format_operand) + 35 call 827 ntypeargs=0 (assert.format_operand) 36 store_var 8 (_22) 84 37 load_const 2 ("assertion failed: |left - right| = ") @@ -64,11 +63,11 @@ function assert.approx_equal(actual: float, expected: float, eps: float) -> null 49 bin_op + 50 load_var 8 (_22) 51 bin_op + - 52 call 247 ntypeargs=0 (baml.sys.panic) + 52 call 257 ntypeargs=0 (baml.sys.panic) 53 jump +3 (to 56) 78 54 load_const 6 ("assertion failed: epsilon must be a non-negative number") - 55 call 247 ntypeargs=0 (baml.sys.panic) + 55 call 257 ntypeargs=0 (baml.sys.panic) 56 return } @@ -84,13 +83,13 @@ function assert.contains(haystack: string, needle: string) -> null { 100 6 jump +3 (to 9) 101 7 load_const 1 ("assertion failed: string does not contain expected substring") - 8 call 247 ntypeargs=0 (baml.sys.panic) + 8 call 257 ntypeargs=0 (baml.sys.panic) 9 return } function assert.equal(actual: unknown, expected: unknown) -> null { 50 0 load_var2 1 2 - 1 call 651 ntypeargs=0 (baml.ops.equals_equals) + 1 call 661 ntypeargs=0 (baml.ops.equals_equals) 2 unary_op ! 3 pop_jump_if_false +2 (to 5) 4 jump +3 (to 7) @@ -100,11 +99,11 @@ function assert.equal(actual: unknown, expected: unknown) -> null { 50 6 jump +15 (to 21) 53 7 load_var 1 (actual) - 8 call 796 ntypeargs=0 (assert.format_operand) + 8 call 827 ntypeargs=0 (assert.format_operand) 9 store_var 3 (_8) 55 10 load_var 2 (expected) - 11 call 796 ntypeargs=0 (assert.format_operand) + 11 call 827 ntypeargs=0 (assert.format_operand) 12 store_var 4 (_9) 53 13 load_const 1 ("assertion failed: left = ") @@ -114,7 +113,7 @@ function assert.equal(actual: unknown, expected: unknown) -> null { 17 bin_op + 18 load_var 4 (_9) 19 bin_op + - 20 call 247 ntypeargs=0 (baml.sys.panic) + 20 call 257 ntypeargs=0 (baml.sys.panic) 21 return } @@ -136,14 +135,14 @@ function assert.is_true(condition: bool) -> null { 5 5 jump +3 (to 8) 6 6 load_const 1 ("assertion failed: expected true") - 7 call 247 ntypeargs=0 (baml.sys.panic) + 7 call 257 ntypeargs=0 (baml.sys.panic) 8 return } function assert.not_null(value: unknown | null) -> null { 14 0 load_var 1 (value) 1 load_const 0 (null) - 2 call 651 ntypeargs=0 (baml.ops.equals_equals) + 2 call 661 ntypeargs=0 (baml.ops.equals_equals) 3 pop_jump_if_false +2 (to 5) 4 jump +3 (to 7) @@ -152,7 +151,7 @@ function assert.not_null(value: unknown | null) -> null { 14 6 jump +3 (to 9) 15 7 load_const 1 ("assertion failed: expected non-null value") - 8 call 247 ntypeargs=0 (baml.sys.panic) + 8 call 257 ntypeargs=0 (baml.sys.panic) 9 return } @@ -172,7 +171,7 @@ function testing.$invoke_collector(collector: (testing.TestCollector) -> void th } function testing.FailFast() -> (testing.TestSetChild[]) -> testing.TestSetReport throws never { - 102 0 make_closure 1502 0 + 102 0 make_closure 1560 0 1 return } @@ -193,11 +192,11 @@ function testing.PassRate(threshold: float) -> (testing.TestSetChild[]) -> testi 12 pop_jump_if_false +4 (to 16) 69 13 load_const 2 ("testing.PassRate requires 0.0 <= threshold <= 1.0") - 14 call 247 ntypeargs=0 (baml.sys.panic) + 14 call 257 ntypeargs=0 (baml.sys.panic) 15 pop 1 74 16 load_var 1 (threshold) - 17 make_closure 1497 1 + 17 make_closure 1555 1 18 return } @@ -227,16 +226,16 @@ function testing.Quorum(n: int, m: int) -> (() -> testing.TestReport throws neve 20 pop_jump_if_false +8 (to 28) 5 21 load_const 1 ("testing.Quorum requires 0 <= m <= n") - 22 call 247 ntypeargs=0 (baml.sys.panic) + 22 call 257 ntypeargs=0 (baml.sys.panic) 23 pop 1 24 jump +4 (to 28) 3 25 load_const 2 ("testing.Quorum requires n > 0") - 26 call 247 ntypeargs=0 (baml.sys.panic) + 26 call 257 ntypeargs=0 (baml.sys.panic) 27 pop 1 10 28 load_var2 1 2 - 29 make_closure 1481 2 + 29 make_closure 1539 2 30 return } @@ -251,16 +250,16 @@ function testing.Retry(max_attempts: int) -> (() -> testing.TestReport throws ne 6 pop_jump_if_false +4 (to 10) 36 7 load_const 1 ("testing.Retry requires max_attempts > 0") - 8 call 247 ntypeargs=0 (baml.sys.panic) + 8 call 257 ntypeargs=0 (baml.sys.panic) 9 pop 1 41 10 load_var 1 (max_attempts) - 11 make_closure 1491 1 + 11 make_closure 1549 1 12 return } function testing.Sequential() -> (testing.TestSetChild[]) -> testing.TestSetReport throws never { - 96 0 make_closure 1500 0 + 96 0 make_closure 1558 0 1 return } @@ -456,7 +455,7 @@ function testing.TestCollector.register_test_at(self: testing.TestCollector, own 4 init_instance 0 (testing.TestCollector .prefix, .tests, .testsets) 5 load_var2 3 4 6 load_var 5 (runner) - 7 call 738 ntypeargs=0 (testing.TestCollector.register_test) + 7 call 769 ntypeargs=0 (testing.TestCollector.register_test) 8 return } @@ -576,7 +575,7 @@ function testing.TestCollector.register_test_set_at(self: testing.TestCollector, 4 init_instance 0 (testing.TestCollector .prefix, .tests, .testsets) 5 load_var2 3 4 6 load_var 5 (runner) - 7 call 739 ntypeargs=0 (testing.TestCollector.register_test_set) + 7 call 770 ntypeargs=0 (testing.TestCollector.register_test_set) 8 return } @@ -606,11 +605,11 @@ function testing.TestRegistry.expand_set(self: testing.TestRegistry, name: strin 21 pop_jump_if_false -14 (to 7) 224 22 load_var2 1 5 - 23 call 754 ntypeargs=0 (testing.TestRegistry.expand_testset_registry) + 23 call 785 ntypeargs=0 (testing.TestRegistry.expand_testset_registry) 24 pop 1 225 25 load_var 1 (self) - 26 call 746 ntypeargs=0 (testing.TestRegistry.serialize) + 26 call 777 ntypeargs=0 (testing.TestRegistry.serialize) 27 jump +33 (to 60) 229 28 load_type 5 @@ -647,7 +646,7 @@ function testing.TestRegistry.expand_set(self: testing.TestRegistry, name: strin 57 pop_jump_if_false -20 (to 37) 232 58 load_var2 9 2 - 59 call 753 ntypeargs=0 (testing.TestRegistry.expand_set) + 59 call 784 ntypeargs=0 (testing.TestRegistry.expand_set) 60 return 235 61 load_const 12 ("TestSet not found for expansion: ") @@ -670,7 +669,7 @@ function testing.TestRegistry.expand_testset_registry(self: testing.TestRegistry 10 pop_jump_if_false +2 (to 12) 11 jump +36 (to 47) - 242 12 call 249 ntypeargs=0 (baml.sys.now_ms) + 242 12 call 259 ntypeargs=0 (baml.sys.now_ms) 13 store_var 4 (start) 243 14 load_var 2 (ts) @@ -687,7 +686,7 @@ function testing.TestRegistry.expand_testset_registry(self: testing.TestRegistry 24 call_indirect 25 pop 1 - 245 26 call 249 ntypeargs=0 (baml.sys.now_ms) + 245 26 call 259 ntypeargs=0 (baml.sys.now_ms) 27 store_var 6 (_19) 247 28 load_var 5 (sub_collector) @@ -722,7 +721,7 @@ function testing.TestRegistry.list_filtered(self: testing.TestRegistry, profile_ 215 0 load_var2 1 2 1 load_var2 3 4 2 load_var 5 (cli_exclude) - 3 call 774 ntypeargs=0 (testing.select_names_layered) + 3 call 805 ntypeargs=0 (testing.select_names_layered) 4 return } @@ -739,9 +738,9 @@ function testing.TestRegistry.new(collector: testing.TestCollector) -> testing.T function testing.TestRegistry.run_all(self: testing.TestRegistry) -> testing.TestSetReport { 188 0 load_var 1 (self) - 1 call 755 ntypeargs=0 (testing.testset_children) + 1 call 786 ntypeargs=0 (testing.testset_children) 2 load_const 0 (null) - 3 call 765 ntypeargs=0 (testing.run_testset) + 3 call 796 ntypeargs=0 (testing.run_testset) 4 return } @@ -749,7 +748,7 @@ function testing.TestRegistry.run_filtered(self: testing.TestRegistry, profile_i 204 0 load_var2 1 2 1 load_var2 3 4 2 load_var 5 (cli_exclude) - 3 call 774 ntypeargs=0 (testing.select_names_layered) + 3 call 805 ntypeargs=0 (testing.select_names_layered) 4 store_var 6 (selected_names) 205 5 load_var 2 (profile_include) @@ -786,22 +785,22 @@ function testing.TestRegistry.run_filtered(self: testing.TestRegistry, profile_i 36 jump +4 (to 40) 208 37 load_var2 1 6 - 38 call 750 ntypeargs=0 (testing.TestRegistry.run_selected) + 38 call 781 ntypeargs=0 (testing.TestRegistry.run_selected) 39 jump +3 (to 42) 206 40 load_var 1 (self) - 41 call 749 ntypeargs=0 (testing.TestRegistry.run_all) + 41 call 780 ntypeargs=0 (testing.TestRegistry.run_all) 210 42 load_var 6 (selected_names) - 43 call 780 ntypeargs=0 (testing.flatten_with_tolerated) + 43 call 811 ntypeargs=0 (testing.flatten_with_tolerated) 44 return } function testing.TestRegistry.run_selected(self: testing.TestRegistry, names: string[]) -> testing.TestSetReport { 192 0 load_var2 1 2 - 1 call 756 ntypeargs=0 (testing.testset_children_selected) + 1 call 787 ntypeargs=0 (testing.testset_children_selected) 2 load_const 0 (null) - 3 call 765 ntypeargs=0 (testing.run_testset) + 3 call 796 ntypeargs=0 (testing.run_testset) 4 return } @@ -834,7 +833,7 @@ function testing.TestRegistry.run_test(self: testing.TestRegistry, name: string) 23 load_field 1 (body) 24 load_var 5 (t) 25 load_field 2 (runner) - 26 call 764 ntypeargs=0 (testing.run_test) + 26 call 795 ntypeargs=0 (testing.run_test) 27 jump +33 (to 60) 162 28 load_type 5 @@ -871,7 +870,7 @@ function testing.TestRegistry.run_test(self: testing.TestRegistry, name: string) 57 pop_jump_if_false -20 (to 37) 166 58 load_var2 9 2 - 59 call 747 ntypeargs=0 (testing.TestRegistry.run_test) + 59 call 778 ntypeargs=0 (testing.TestRegistry.run_test) 60 return 169 61 load_const 12 ("Test not found: ") @@ -906,11 +905,11 @@ function testing.TestRegistry.run_testset(self: testing.TestRegistry, name: stri 21 pop_jump_if_false -14 (to 7) 175 22 load_var2 1 5 - 23 call 754 ntypeargs=0 (testing.TestRegistry.expand_testset_registry) - 24 call 755 ntypeargs=0 (testing.testset_children) + 23 call 785 ntypeargs=0 (testing.TestRegistry.expand_testset_registry) + 24 call 786 ntypeargs=0 (testing.testset_children) 25 load_var 5 (ts) 26 load_field 2 (runner) - 27 call 765 ntypeargs=0 (testing.run_testset) + 27 call 796 ntypeargs=0 (testing.run_testset) 28 jump +33 (to 61) 178 29 load_type 5 @@ -947,7 +946,7 @@ function testing.TestRegistry.run_testset(self: testing.TestRegistry, name: stri 58 pop_jump_if_false -20 (to 38) 181 59 load_var2 9 2 - 60 call 748 ntypeargs=0 (testing.TestRegistry.run_testset) + 60 call 779 ntypeargs=0 (testing.TestRegistry.run_testset) 61 return 184 62 load_const 12 ("TestSet not found: ") @@ -1038,7 +1037,7 @@ function testing.TestRegistry.serialize(self: testing.TestRegistry) -> (testing. 69 store_var 10 (_27) 141 70 load_var 9 (sub) - 71 call 746 ntypeargs=0 (testing.TestRegistry.serialize) + 71 call 777 ntypeargs=0 (testing.TestRegistry.serialize) 72 store_var 11 (_28) 139 73 load_var2 2 10 @@ -1094,7 +1093,7 @@ function testing._int_to_float(value: int) -> float { 6 throw_if_panic 127 7 load_const 1 ("failed to convert int to float") - 8 call 247 ntypeargs=0 (baml.sys.panic) + 8 call 257 ntypeargs=0 (baml.sys.panic) 9 return } @@ -1192,7 +1191,7 @@ function testing.aggregate_reports(named_results: testing.NamedChildReport[]) -> 362 54 load_var 12 (outcome) 55 load_const 10 ("pass") - 56 call 651 ntypeargs=0 (baml.ops.equals_equals) + 56 call 661 ntypeargs=0 (baml.ops.equals_equals) 57 pop_jump_if_false +2 (to 59) 58 jump +57 (to 115) @@ -1258,7 +1257,7 @@ function testing.aggregate_reports(named_results: testing.NamedChildReport[]) -> 377 108 load_var 12 (outcome) 109 load_const 16 ("error") - 110 call 651 ntypeargs=0 (baml.ops.equals_equals) + 110 call 661 ntypeargs=0 (baml.ops.equals_equals) 111 pop_jump_if_false -91 (to 20) 378 112 load_const 17 (true) @@ -1327,7 +1326,7 @@ function testing.any_pattern_matches(pats: string[], canonical_id: string) -> bo 510 14 load_var2 2 4 - 509 15 call 767 ntypeargs=0 (testing.glob_match) + 509 15 call 798 ntypeargs=0 (testing.glob_match) 510 16 pop_jump_if_false -11 (to 5) @@ -1366,7 +1365,7 @@ function testing.classify_selected_names(selected_names: string[], hard_failed_n 23 store_var_load_var 7 (name) 753 24 load_var 3 (all_failed_names) - 25 call 783 ntypeargs=0 (testing.failure_matches_selected) + 25 call 814 ntypeargs=0 (testing.failure_matches_selected) 26 pop_jump_if_false +2 (to 28) 27 jump +8 (to 35) @@ -1379,7 +1378,7 @@ function testing.classify_selected_names(selected_names: string[], hard_failed_n 34 jump -21 (to 13) 754 35 load_var2 7 2 - 36 call 783 ntypeargs=0 (testing.failure_matches_selected) + 36 call 814 ntypeargs=0 (testing.failure_matches_selected) 37 pop_jump_if_false +2 (to 39) 38 jump +8 (to 46) @@ -1423,7 +1422,7 @@ function testing.collect_all_failed_names(report: testing.TestSetReport, out: st 16 store_var_load_var 5 (name) 824 17 load_var 2 (out) - 18 call 782 ntypeargs=0 (testing.name_in) + 18 call 813 ntypeargs=0 (testing.name_in) 19 unary_op ! 20 pop_jump_if_false -14 (to 6) 21 load_var2 2 5 @@ -1453,7 +1452,7 @@ function testing.collect_all_failed_names(report: testing.TestSetReport, out: st 43 pop_jump_if_false +2 (to 45) 44 jump -13 (to 31) 45 load_var2 8 2 - 46 call 785 ntypeargs=0 (testing.collect_all_failed_names) + 46 call 816 ntypeargs=0 (testing.collect_all_failed_names) 47 pop 1 48 jump -17 (to 31) @@ -1529,7 +1528,7 @@ function testing.collect_leaf_identities(report: testing.TestSetReport, tolerate 57 load_var 14 (nested) 58 load_field 0 (outcome) 59 load_const 3 ("pass") - 60 call 651 ntypeargs=0 (baml.ops.equals_equals) + 60 call 661 ntypeargs=0 (baml.ops.equals_equals) 61 store_var 15 (nested_tolerated) 804 62 load_var 14 (nested) @@ -1554,7 +1553,7 @@ function testing.collect_leaf_identities(report: testing.TestSetReport, tolerate 79 store_var_load_var 17 (sentinel) 808 80 load_var 3 (selected_names) - 81 call 782 ntypeargs=0 (testing.name_in) + 81 call 813 ntypeargs=0 (testing.name_in) 82 pop_jump_if_false +2 (to 84) 83 jump +21 (to 104) 84 load_var 14 (nested) @@ -1600,14 +1599,14 @@ function testing.collect_leaf_identities(report: testing.TestSetReport, tolerate 805 123 load_var2 14 15 124 load_var2 3 4 - 125 call 784 ntypeargs=0 (testing.collect_leaf_identities) + 125 call 815 ntypeargs=0 (testing.collect_leaf_identities) 126 pop 1 127 jump +30 (to 157) 792 128 load_var 13 (_25) 129 load_field 0 (outcome) 130 load_const 7 ("pass") - 131 call 651 ntypeargs=0 (baml.ops.equals_equals) + 131 call 661 ntypeargs=0 (baml.ops.equals_equals) 794 132 pop_jump_if_false +2 (to 134) 133 jump +18 (to 151) @@ -1676,7 +1675,7 @@ function testing.collect_leaf_messages(results: (testing.TestReport | testing.Te 22 load_var 5 (r) 23 load_field 5 (results) 24 load_var 2 (out) - 25 call 786 ntypeargs=0 (testing.collect_leaf_messages) + 25 call 817 ntypeargs=0 (testing.collect_leaf_messages) 26 pop 1 27 jump -22 (to 5) 28 load_var 6 (_8) @@ -1700,7 +1699,7 @@ function testing.collect_leaf_messages(results: (testing.TestReport | testing.Te 841 45 load_field 0 (outcome) 46 load_const 10 ("pass") - 47 call 651 ntypeargs=0 (baml.ops.equals_equals) + 47 call 661 ntypeargs=0 (baml.ops.equals_equals) 48 unary_op ! 49 pop_jump_if_false -15 (to 34) @@ -1769,7 +1768,7 @@ function testing.collect_leaf_names(registry: testing.TestRegistry) -> string[] 618 40 load_var2 1 6 - 617 41 call 777 ntypeargs=0 (testing.collect_subtree_names) + 617 41 call 808 ntypeargs=0 (testing.collect_subtree_names) 618 42 load_type 10 43 load_const 11 ("iter") @@ -1797,8 +1796,8 @@ function testing.collect_leaf_names(registry: testing.TestRegistry) -> string[] function testing.collect_subtree_names(registry: testing.TestRegistry, ts: testing.TestSetRegistration) -> string[] { 631 0 load_var2 1 2 - 1 call 754 ntypeargs=0 (testing.TestRegistry.expand_testset_registry) - 2 call 776 ntypeargs=0 (testing.collect_leaf_names) + 1 call 785 ntypeargs=0 (testing.TestRegistry.expand_testset_registry) + 2 call 807 ntypeargs=0 (testing.collect_leaf_names) 3 jump +89 (to 92) 4 load_var 3 (e) 5 store_var 4 (_5) @@ -1897,10 +1896,10 @@ function testing.collect_subtree_names(registry: testing.TestRegistry, ts: testi function testing.collect_subtree_names_layered(registry: testing.TestRegistry, ts: testing.TestSetRegistration, profile_include: string[], profile_exclude: string[], cli_include: string[], cli_exclude: string[]) -> string[] { 639 0 load_var2 1 2 - 1 call 754 ntypeargs=0 (testing.TestRegistry.expand_testset_registry) + 1 call 785 ntypeargs=0 (testing.TestRegistry.expand_testset_registry) 2 load_var2 3 4 3 load_var2 5 6 - 4 call 774 ntypeargs=0 (testing.select_names_layered) + 4 call 805 ntypeargs=0 (testing.select_names_layered) 5 jump +109 (to 114) 6 load_var 7 (e) 7 store_var 8 (_9) @@ -1983,7 +1982,7 @@ function testing.collect_subtree_names_layered(registry: testing.TestRegistry, t 647 83 load_var2 3 4 84 load_var2 5 6 - 85 call 770 ntypeargs=0 (testing.leaf_selected_layered) + 85 call 801 ntypeargs=0 (testing.leaf_selected_layered) 86 pop_jump_if_false +2 (to 88) 87 jump +4 (to 91) 88 load_type 18 @@ -2002,7 +2001,7 @@ function testing.collect_subtree_names_layered(registry: testing.TestRegistry, t 643 100 load_var2 3 4 101 load_var2 5 6 - 102 call 770 ntypeargs=0 (testing.leaf_selected_layered) + 102 call 801 ntypeargs=0 (testing.leaf_selected_layered) 103 pop_jump_if_false +2 (to 105) 104 jump +4 (to 108) 105 load_type 18 @@ -2065,7 +2064,7 @@ function testing.count_leaves(results: (testing.TestReport | testing.TestSetRepo 37 load_var 12 (tsr) 38 load_field 0 (outcome) 39 load_const 7 ("pass") - 40 call 651 ntypeargs=0 (baml.ops.equals_equals) + 40 call 661 ntypeargs=0 (baml.ops.equals_equals) 41 store_var 13 (ft) 687 42 load_var 12 (tsr) @@ -2109,14 +2108,14 @@ function testing.count_leaves(results: (testing.TestReport | testing.TestSetRepo 688 74 load_var 12 (tsr) 75 load_field 5 (results) 76 load_var 13 (ft) - 77 call 779 ntypeargs=0 (testing.count_leaves) + 77 call 810 ntypeargs=0 (testing.count_leaves) 78 store_var 10 (delta) 79 jump +30 (to 109) 675 80 load_var 11 (_13) 81 load_field 0 (outcome) 82 load_const 8 ("pass") - 83 call 651 ntypeargs=0 (baml.ops.equals_equals) + 83 call 661 ntypeargs=0 (baml.ops.equals_equals) 677 84 pop_jump_if_false +2 (to 86) 85 jump +18 (to 103) @@ -2201,7 +2200,7 @@ function testing.expansion_error_report(name: string) -> testing.TestSetReport { function testing.failure_matches_selected(selected: string, failed_names: string[]) -> bool { 774 0 load_var2 1 2 - 1 call 782 ntypeargs=0 (testing.name_in) + 1 call 813 ntypeargs=0 (testing.name_in) 2 pop_jump_if_false +2 (to 4) 3 jump +40 (to 43) @@ -2262,7 +2261,7 @@ function testing.flatten_with_tolerated(report: testing.TestSetReport, selected_ 709 0 load_var 1 (report) 1 load_field 0 (outcome) 2 load_const 0 ("pass") - 3 call 651 ntypeargs=0 (baml.ops.equals_equals) + 3 call 661 ntypeargs=0 (baml.ops.equals_equals) 4 store_var 3 (top_tolerated) 710 5 load_var 1 (report) @@ -2306,7 +2305,7 @@ function testing.flatten_with_tolerated(report: testing.TestSetReport, selected_ 711 37 load_var 1 (report) 38 load_field 5 (results) 39 load_var 3 (top_tolerated) - 40 call 779 ntypeargs=0 (testing.count_leaves) + 40 call 810 ntypeargs=0 (testing.count_leaves) 41 store_var 4 (counts) 717 42 load_type 2 @@ -2316,7 +2315,7 @@ function testing.flatten_with_tolerated(report: testing.TestSetReport, selected_ 718 45 load_var 1 (report) 46 load_field 5 (results) 47 load_var 6 (messages) - 48 call 786 ntypeargs=0 (testing.collect_leaf_messages) + 48 call 817 ntypeargs=0 (testing.collect_leaf_messages) 49 pop 1 719 50 load_type 2 @@ -2324,7 +2323,7 @@ function testing.flatten_with_tolerated(report: testing.TestSetReport, selected_ 52 store_var 7 (all_failed_names) 720 53 load_var2 1 7 - 54 call 785 ntypeargs=0 (testing.collect_all_failed_names) + 54 call 816 ntypeargs=0 (testing.collect_all_failed_names) 55 pop 1 721 56 load_type 2 @@ -2338,7 +2337,7 @@ function testing.flatten_with_tolerated(report: testing.TestSetReport, selected_ 722 64 load_var2 1 3 65 load_var2 2 8 - 66 call 784 ntypeargs=0 (testing.collect_leaf_identities) + 66 call 815 ntypeargs=0 (testing.collect_leaf_identities) 67 pop 1 727 68 load_var 8 (executed) @@ -2392,7 +2391,7 @@ function testing.flatten_with_tolerated(report: testing.TestSetReport, selected_ 730 112 load_var2 2 1 113 load_field 4 (failed_names) 114 load_var 7 (all_failed_names) - 115 call 781 ntypeargs=0 (testing.classify_selected_names) + 115 call 812 ntypeargs=0 (testing.classify_selected_names) 116 store_var 9 (identities) 117 jump +3 (to 120) @@ -2552,14 +2551,14 @@ function testing.glob_match(subject: string, pattern: string) -> bool { 485 96 load_var2 1 2 97 call 155 ntypeargs=0 (baml.String.index_of) 98 load_const 6 (null) - 99 call 651 ntypeargs=0 (baml.ops.equals_equals) + 99 call 661 ntypeargs=0 (baml.ops.equals_equals) 100 unary_op ! 101 return } function testing.leaf_selected(name: string, include: string[], exclude: string[]) -> bool { 520 0 load_var2 3 1 - 1 call 768 ntypeargs=0 (testing.any_pattern_matches) + 1 call 799 ntypeargs=0 (testing.any_pattern_matches) 2 pop_jump_if_false +2 (to 4) 3 jump +14 (to 17) @@ -2573,7 +2572,7 @@ function testing.leaf_selected(name: string, include: string[], exclude: string[ 11 jump +4 (to 15) 526 12 load_var2 2 1 - 13 call 768 ntypeargs=0 (testing.any_pattern_matches) + 13 call 799 ntypeargs=0 (testing.any_pattern_matches) 14 jump +4 (to 18) 524 15 load_const 1 (true) @@ -2586,12 +2585,12 @@ function testing.leaf_selected(name: string, include: string[], exclude: string[ function testing.leaf_selected_layered(name: string, profile_include: string[], profile_exclude: string[], cli_include: string[], cli_exclude: string[]) -> bool { 533 0 load_var2 1 2 1 load_var 3 (profile_exclude) - 2 call 769 ntypeargs=0 (testing.leaf_selected) + 2 call 800 ntypeargs=0 (testing.leaf_selected) 3 jump_if_false +5 (to 8) 4 pop 1 5 load_var2 1 4 6 load_var 5 (cli_exclude) - 7 call 769 ntypeargs=0 (testing.leaf_selected) + 7 call 800 ntypeargs=0 (testing.leaf_selected) 8 return } @@ -2687,7 +2686,7 @@ function testing.run_children_parallel(children: testing.TestSetChild[]) -> test 24 store_deref 5 399 25 load_var 5 (child) - 26 make_closure 1382 1 + 26 make_closure 1440 1 27 load_const 6 (null) 28 load_const 6 (null) 29 load_type 7 @@ -2702,10 +2701,10 @@ function testing.run_children_parallel(children: testing.TestSetChild[]) -> test 405 37 load_type 7 38 load_type 8 39 load_var 2 (futures) - 40 call 521 ntypeargs=2 (baml.future.all_complete) + 40 call 531 ntypeargs=2 (baml.future.all_complete) 41 await - 406 42 call 762 ntypeargs=0 (testing.aggregate_reports) + 406 42 call 793 ntypeargs=0 (testing.aggregate_reports) 43 return } @@ -2761,16 +2760,16 @@ function testing.run_children_sequential(children: testing.TestSetChild[], fail_ 43 pop 1 44 load_var 8 (outcome) 45 load_const 7 ("pass") - 46 call 651 ntypeargs=0 (baml.ops.equals_equals) + 46 call 661 ntypeargs=0 (baml.ops.equals_equals) 47 unary_op ! 48 pop_jump_if_false -40 (to 8) 117 49 load_var 3 (named_results) - 50 call 762 ntypeargs=0 (testing.aggregate_reports) + 50 call 793 ntypeargs=0 (testing.aggregate_reports) 51 jump +3 (to 54) 122 52 load_var 3 (named_results) - 53 call 762 ntypeargs=0 (testing.aggregate_reports) + 53 call 793 ntypeargs=0 (testing.aggregate_reports) 54 return } @@ -2780,7 +2779,7 @@ function testing.run_test(body: () -> void throws unknown, runner: ((() -> testi 2 store_var 1 411 3 load_var 1 (body) - 4 make_closure 1393 1 + 4 make_closure 1451 1 5 store_var 3 (base_run) 435 6 load_var 2 (runner) @@ -2812,7 +2811,7 @@ function testing.run_testset(children: testing.TestSetChild[], runner: ((testing 7 jump +3 (to 10) 447 8 load_var 1 (children) - 9 call 763 ntypeargs=0 (testing.run_children_parallel) + 9 call 794 ntypeargs=0 (testing.run_children_parallel) 10 return } @@ -2823,7 +2822,7 @@ function testing.select_names(registry: testing.TestRegistry, include: string[], 3 load_type 0 4 alloc_array 0 5 load_var2 2 3 - 6 call 774 ntypeargs=0 (testing.select_names_layered) + 6 call 805 ntypeargs=0 (testing.select_names_layered) 7 return } @@ -2854,7 +2853,7 @@ function testing.select_names_layered(registry: testing.TestRegistry, profile_in 591 21 load_field 0 (name) 22 load_var2 2 3 23 load_var2 4 5 - 24 call 770 ntypeargs=0 (testing.leaf_selected_layered) + 24 call 801 ntypeargs=0 (testing.leaf_selected_layered) 25 pop_jump_if_false -15 (to 10) 592 26 load_var2 6 9 @@ -2884,19 +2883,19 @@ function testing.select_names_layered(registry: testing.TestRegistry, profile_in 598 49 load_field 0 (name) 50 load_var2 2 3 - 51 call 773 ntypeargs=0 (testing.subtree_may_be_selected) + 51 call 804 ntypeargs=0 (testing.subtree_may_be_selected) 52 jump_if_false +6 (to 58) 53 pop 1 54 load_var 12 (ts) 55 load_field 0 (name) 56 load_var2 4 5 - 57 call 773 ntypeargs=0 (testing.subtree_may_be_selected) + 57 call 804 ntypeargs=0 (testing.subtree_may_be_selected) 58 pop_jump_if_false -20 (to 38) 599 59 load_var2 1 12 60 load_var2 2 3 61 load_var2 4 5 - 62 call 778 ntypeargs=0 (testing.collect_subtree_names_layered) + 62 call 809 ntypeargs=0 (testing.collect_subtree_names_layered) 600 63 load_type 10 64 load_const 11 ("iter") @@ -2947,13 +2946,13 @@ function testing.subtree_excluded(prefix: string, excludes: string[]) -> bool { 21 load_const 6 ("*") 22 call 155 ntypeargs=0 (baml.String.index_of) 23 load_const 7 (null) - 24 call 651 ntypeargs=0 (baml.ops.equals_equals) + 24 call 661 ntypeargs=0 (baml.ops.equals_equals) 25 jump_if_false +7 (to 32) 26 pop 1 27 load_var2 3 6 28 call 155 ntypeargs=0 (baml.String.index_of) 29 load_const 7 (null) - 30 call 651 ntypeargs=0 (baml.ops.equals_equals) + 30 call 661 ntypeargs=0 (baml.ops.equals_equals) 31 unary_op ! 32 pop_jump_if_false +2 (to 34) 33 jump +11 (to 44) @@ -2964,7 +2963,7 @@ function testing.subtree_excluded(prefix: string, excludes: string[]) -> bool { 37 jump_if_false +4 (to 41) 38 pop 1 39 load_var2 3 6 - 40 call 767 ntypeargs=0 (testing.glob_match) + 40 call 798 ntypeargs=0 (testing.glob_match) 41 pop_jump_if_false -32 (to 9) 550 42 load_const 9 (true) @@ -2979,7 +2978,7 @@ function testing.subtree_excluded(prefix: string, excludes: string[]) -> bool { function testing.subtree_may_be_selected(prefix: string, include: string[], exclude: string[]) -> bool { 572 0 load_var2 1 3 - 1 call 771 ntypeargs=0 (testing.subtree_excluded) + 1 call 802 ntypeargs=0 (testing.subtree_excluded) 2 pop_jump_if_false +2 (to 4) 3 jump +32 (to 35) @@ -3007,7 +3006,7 @@ function testing.subtree_may_be_selected(prefix: string, include: string[], excl 24 pop_jump_if_false +2 (to 26) 25 jump +6 (to 31) 26 load_var2 6 1 - 27 call 772 ntypeargs=0 (testing.pattern_may_match_descendant) + 27 call 803 ntypeargs=0 (testing.pattern_may_match_descendant) 579 28 pop_jump_if_false -11 (to 17) @@ -3035,7 +3034,7 @@ function testing.test_child(name: string, body: () -> void throws unknown, runne 294 6 load_var2 1 2 296 7 load_var 3 (runner) - 8 make_closure 1359 2 + 8 make_closure 1417 2 9 init_instance 0 (testing.TestSetChild .name, .run) 10 return } @@ -3052,7 +3051,7 @@ function testing.testset_child(registry: testing.TestRegistry, ts: testing.TestS 7 load_field 0 (name) 303 8 load_var2 1 2 - 9 make_closure 1361 2 + 9 make_closure 1419 2 10 init_instance 0 (testing.TestSetChild .name, .run) 11 return } @@ -3073,7 +3072,7 @@ function testing.testset_child_selected(registry: testing.TestRegistry, ts: test 315 11 load_var2 1 2 12 load_var 3 (names) - 13 make_closure 1363 3 + 13 make_closure 1421 3 14 init_instance 0 (testing.TestSetChild .name, .run) 15 return } @@ -3107,7 +3106,7 @@ function testing.testset_children(registry: testing.TestRegistry) -> testing.Tes 23 load_field 1 (body) 24 load_var 5 (t) 25 load_field 2 (runner) - 26 call 757 ntypeargs=0 (testing.test_child) + 26 call 788 ntypeargs=0 (testing.test_child) 27 store_var 6 (_10) 28 load_var2 2 6 29 call 4 ntypeargs=0 (baml.Array.push) @@ -3133,7 +3132,7 @@ function testing.testset_children(registry: testing.TestRegistry) -> testing.Tes 264 48 load_var2 1 8 - 263 49 call 758 ntypeargs=0 (testing.testset_child) + 263 49 call 789 ntypeargs=0 (testing.testset_child) 50 store_var 9 (_21) 264 51 load_var2 2 9 @@ -3171,7 +3170,7 @@ function testing.testset_children_selected(registry: testing.TestRegistry, names 272 21 load_field 0 (name) 22 load_var 2 (names) - 23 call 760 ntypeargs=0 (testing._name_selected) + 23 call 791 ntypeargs=0 (testing._name_selected) 24 pop_jump_if_false -14 (to 10) 273 25 load_var 6 (t) @@ -3180,7 +3179,7 @@ function testing.testset_children_selected(registry: testing.TestRegistry, names 28 load_field 1 (body) 29 load_var 6 (t) 30 load_field 2 (runner) - 31 call 757 ntypeargs=0 (testing.test_child) + 31 call 788 ntypeargs=0 (testing.test_child) 32 store_var 7 (_13) 33 load_var2 3 7 34 call 4 ntypeargs=0 (baml.Array.push) @@ -3208,12 +3207,12 @@ function testing.testset_children_selected(registry: testing.TestRegistry, names 284 55 load_field 0 (name) 56 load_var 2 (names) - 57 call 761 ntypeargs=0 (testing._has_selected_descendant) + 57 call 792 ntypeargs=0 (testing._has_selected_descendant) 58 pop_jump_if_false -14 (to 44) 285 59 load_var2 1 10 60 load_var 2 (names) - 61 call 759 ntypeargs=0 (testing.testset_child_selected) + 61 call 790 ntypeargs=0 (testing.testset_child_selected) 62 store_var 11 (_26) 63 load_var2 3 11 64 call 4 ntypeargs=0 (baml.Array.push) @@ -3233,7 +3232,7 @@ function user.User.promote(self: User, bonus: int) -> Report { 5 store_field 1 (age) 10 6 load_var2 1 2 - 7 call 801 ntypeargs=0 (user.score) + 7 call 832 ntypeargs=0 (user.score) 8 store_var 3 (base) 11 9 load_var 3 (base) diff --git a/baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded_unoptimized.snap b/baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded_unoptimized.snap index 19c387f7d2e..de96c51dbf5 100644 --- a/baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded_unoptimized.snap +++ b/baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded_unoptimized.snap @@ -1,6 +1,5 @@ --- source: crates/baml_tests/tests/bytecode_format/main.rs -assertion_line: 43 --- function assert.approx_equal(actual: float, expected: float, eps: float) -> null { 77 0 load_var 3 (eps) @@ -34,19 +33,19 @@ function assert.approx_equal(actual: float, expected: float, eps: float) -> null 81 24 jump +32 (to 56) 84 25 load_var 4 (delta) - 26 call 796 ntypeargs=0 (assert.format_operand) + 26 call 827 ntypeargs=0 (assert.format_operand) 27 store_var 5 (_18) 86 28 load_var 3 (eps) - 29 call 796 ntypeargs=0 (assert.format_operand) + 29 call 827 ntypeargs=0 (assert.format_operand) 30 store_var 6 (_20) 88 31 load_var 1 (actual) - 32 call 796 ntypeargs=0 (assert.format_operand) + 32 call 827 ntypeargs=0 (assert.format_operand) 33 store_var 7 (_21) 90 34 load_var 2 (expected) - 35 call 796 ntypeargs=0 (assert.format_operand) + 35 call 827 ntypeargs=0 (assert.format_operand) 36 store_var 8 (_22) 84 37 load_const 2 ("assertion failed: |left - right| = ") @@ -64,11 +63,11 @@ function assert.approx_equal(actual: float, expected: float, eps: float) -> null 49 bin_op + 50 load_var 8 (_22) 51 bin_op + - 52 call 247 ntypeargs=0 (baml.sys.panic) + 52 call 257 ntypeargs=0 (baml.sys.panic) 53 jump +3 (to 56) 78 54 load_const 6 ("assertion failed: epsilon must be a non-negative number") - 55 call 247 ntypeargs=0 (baml.sys.panic) + 55 call 257 ntypeargs=0 (baml.sys.panic) 56 return } @@ -84,13 +83,13 @@ function assert.contains(haystack: string, needle: string) -> null { 100 6 jump +3 (to 9) 101 7 load_const 1 ("assertion failed: string does not contain expected substring") - 8 call 247 ntypeargs=0 (baml.sys.panic) + 8 call 257 ntypeargs=0 (baml.sys.panic) 9 return } function assert.equal(actual: unknown, expected: unknown) -> null { 50 0 load_var2 1 2 - 1 call 651 ntypeargs=0 (baml.ops.equals_equals) + 1 call 661 ntypeargs=0 (baml.ops.equals_equals) 2 unary_op ! 3 pop_jump_if_false +2 (to 5) 4 jump +3 (to 7) @@ -100,11 +99,11 @@ function assert.equal(actual: unknown, expected: unknown) -> null { 50 6 jump +15 (to 21) 53 7 load_var 1 (actual) - 8 call 796 ntypeargs=0 (assert.format_operand) + 8 call 827 ntypeargs=0 (assert.format_operand) 9 store_var 3 (_8) 55 10 load_var 2 (expected) - 11 call 796 ntypeargs=0 (assert.format_operand) + 11 call 827 ntypeargs=0 (assert.format_operand) 12 store_var 4 (_9) 53 13 load_const 1 ("assertion failed: left = ") @@ -114,7 +113,7 @@ function assert.equal(actual: unknown, expected: unknown) -> null { 17 bin_op + 18 load_var 4 (_9) 19 bin_op + - 20 call 247 ntypeargs=0 (baml.sys.panic) + 20 call 257 ntypeargs=0 (baml.sys.panic) 21 return } @@ -136,14 +135,14 @@ function assert.is_true(condition: bool) -> null { 5 5 jump +3 (to 8) 6 6 load_const 1 ("assertion failed: expected true") - 7 call 247 ntypeargs=0 (baml.sys.panic) + 7 call 257 ntypeargs=0 (baml.sys.panic) 8 return } function assert.not_null(value: unknown | null) -> null { 14 0 load_var 1 (value) 1 load_const 0 (null) - 2 call 651 ntypeargs=0 (baml.ops.equals_equals) + 2 call 661 ntypeargs=0 (baml.ops.equals_equals) 3 pop_jump_if_false +2 (to 5) 4 jump +3 (to 7) @@ -152,7 +151,7 @@ function assert.not_null(value: unknown | null) -> null { 14 6 jump +3 (to 9) 15 7 load_const 1 ("assertion failed: expected non-null value") - 8 call 247 ntypeargs=0 (baml.sys.panic) + 8 call 257 ntypeargs=0 (baml.sys.panic) 9 return } @@ -172,7 +171,7 @@ function testing.$invoke_collector(collector: (testing.TestCollector) -> void th } function testing.FailFast() -> (testing.TestSetChild[]) -> testing.TestSetReport throws never { - 102 0 make_closure 1501 0 + 102 0 make_closure 1559 0 1 store_var 1 (_0) 2 load_var 1 (_0) 3 return @@ -195,11 +194,11 @@ function testing.PassRate(threshold: float) -> (testing.TestSetChild[]) -> testi 12 pop_jump_if_false +4 (to 16) 69 13 load_const 2 ("testing.PassRate requires 0.0 <= threshold <= 1.0") - 14 call 247 ntypeargs=0 (baml.sys.panic) + 14 call 257 ntypeargs=0 (baml.sys.panic) 15 pop 1 74 16 load_var 1 (threshold) - 17 make_closure 1496 1 + 17 make_closure 1554 1 18 store_var 2 (_0) 19 load_var 2 (_0) 20 return @@ -231,16 +230,16 @@ function testing.Quorum(n: int, m: int) -> (() -> testing.TestReport throws neve 20 pop_jump_if_false +8 (to 28) 5 21 load_const 1 ("testing.Quorum requires 0 <= m <= n") - 22 call 247 ntypeargs=0 (baml.sys.panic) + 22 call 257 ntypeargs=0 (baml.sys.panic) 23 pop 1 24 jump +4 (to 28) 3 25 load_const 2 ("testing.Quorum requires n > 0") - 26 call 247 ntypeargs=0 (baml.sys.panic) + 26 call 257 ntypeargs=0 (baml.sys.panic) 27 pop 1 10 28 load_var2 1 2 - 29 make_closure 1480 2 + 29 make_closure 1538 2 30 store_var 3 (_0) 31 load_var 3 (_0) 32 return @@ -257,18 +256,18 @@ function testing.Retry(max_attempts: int) -> (() -> testing.TestReport throws ne 6 pop_jump_if_false +4 (to 10) 36 7 load_const 1 ("testing.Retry requires max_attempts > 0") - 8 call 247 ntypeargs=0 (baml.sys.panic) + 8 call 257 ntypeargs=0 (baml.sys.panic) 9 pop 1 41 10 load_var 1 (max_attempts) - 11 make_closure 1490 1 + 11 make_closure 1548 1 12 store_var 2 (_0) 13 load_var 2 (_0) 14 return } function testing.Sequential() -> (testing.TestSetChild[]) -> testing.TestSetReport throws never { - 96 0 make_closure 1499 0 + 96 0 make_closure 1557 0 1 store_var 1 (_0) 2 load_var 1 (_0) 3 return @@ -476,7 +475,7 @@ function testing.TestCollector.register_test_at(self: testing.TestCollector, own 62 6 load_var2 6 3 7 load_var2 4 5 - 8 call 738 ntypeargs=0 (testing.TestCollector.register_test) + 8 call 769 ntypeargs=0 (testing.TestCollector.register_test) 9 return } @@ -600,7 +599,7 @@ function testing.TestCollector.register_test_set_at(self: testing.TestCollector, 67 6 load_var2 6 3 7 load_var2 4 5 - 8 call 739 ntypeargs=0 (testing.TestCollector.register_test_set) + 8 call 770 ntypeargs=0 (testing.TestCollector.register_test_set) 9 return } @@ -630,11 +629,11 @@ function testing.TestRegistry.expand_set(self: testing.TestRegistry, name: strin 21 pop_jump_if_false -14 (to 7) 224 22 load_var2 1 5 - 23 call 754 ntypeargs=0 (testing.TestRegistry.expand_testset_registry) + 23 call 785 ntypeargs=0 (testing.TestRegistry.expand_testset_registry) 24 pop 1 225 25 load_var 1 (self) - 26 call 746 ntypeargs=0 (testing.TestRegistry.serialize) + 26 call 777 ntypeargs=0 (testing.TestRegistry.serialize) 27 jump +33 (to 60) 229 28 load_type 5 @@ -671,7 +670,7 @@ function testing.TestRegistry.expand_set(self: testing.TestRegistry, name: strin 57 pop_jump_if_false -20 (to 37) 232 58 load_var2 9 2 - 59 call 753 ntypeargs=0 (testing.TestRegistry.expand_set) + 59 call 784 ntypeargs=0 (testing.TestRegistry.expand_set) 60 return 235 61 load_const 12 ("TestSet not found for expansion: ") @@ -694,7 +693,7 @@ function testing.TestRegistry.expand_testset_registry(self: testing.TestRegistry 10 pop_jump_if_false +2 (to 12) 11 jump +37 (to 48) - 242 12 call 249 ntypeargs=0 (baml.sys.now_ms) + 242 12 call 259 ntypeargs=0 (baml.sys.now_ms) 13 store_var 5 (start) 243 14 load_var 2 (ts) @@ -711,7 +710,7 @@ function testing.TestRegistry.expand_testset_registry(self: testing.TestRegistry 24 call_indirect 25 pop 1 - 245 26 call 249 ntypeargs=0 (baml.sys.now_ms) + 245 26 call 259 ntypeargs=0 (baml.sys.now_ms) 27 load_var 5 (start) 28 sub_int 29 store_var 7 (elapsed) @@ -750,7 +749,7 @@ function testing.TestRegistry.list_filtered(self: testing.TestRegistry, profile_ 215 0 load_var2 1 2 1 load_var2 3 4 2 load_var 5 (cli_exclude) - 3 call 774 ntypeargs=0 (testing.select_names_layered) + 3 call 805 ntypeargs=0 (testing.select_names_layered) 4 return } @@ -769,9 +768,9 @@ function testing.TestRegistry.new(collector: testing.TestCollector) -> testing.T function testing.TestRegistry.run_all(self: testing.TestRegistry) -> testing.TestSetReport { 188 0 load_var 1 (self) - 1 call 755 ntypeargs=0 (testing.testset_children) + 1 call 786 ntypeargs=0 (testing.testset_children) 2 load_const 0 (null) - 3 call 765 ntypeargs=0 (testing.run_testset) + 3 call 796 ntypeargs=0 (testing.run_testset) 4 return } @@ -779,7 +778,7 @@ function testing.TestRegistry.run_filtered(self: testing.TestRegistry, profile_i 204 0 load_var2 1 2 1 load_var2 3 4 2 load_var 5 (cli_exclude) - 3 call 774 ntypeargs=0 (testing.select_names_layered) + 3 call 805 ntypeargs=0 (testing.select_names_layered) 4 store_var 6 (selected_names) 205 5 load_var 2 (profile_include) @@ -816,22 +815,22 @@ function testing.TestRegistry.run_filtered(self: testing.TestRegistry, profile_i 36 jump +4 (to 40) 208 37 load_var2 1 6 - 38 call 750 ntypeargs=0 (testing.TestRegistry.run_selected) + 38 call 781 ntypeargs=0 (testing.TestRegistry.run_selected) 39 jump +3 (to 42) 206 40 load_var 1 (self) - 41 call 749 ntypeargs=0 (testing.TestRegistry.run_all) + 41 call 780 ntypeargs=0 (testing.TestRegistry.run_all) 210 42 load_var 6 (selected_names) - 43 call 780 ntypeargs=0 (testing.flatten_with_tolerated) + 43 call 811 ntypeargs=0 (testing.flatten_with_tolerated) 44 return } function testing.TestRegistry.run_selected(self: testing.TestRegistry, names: string[]) -> testing.TestSetReport { 192 0 load_var2 1 2 - 1 call 756 ntypeargs=0 (testing.testset_children_selected) + 1 call 787 ntypeargs=0 (testing.testset_children_selected) 2 load_const 0 (null) - 3 call 765 ntypeargs=0 (testing.run_testset) + 3 call 796 ntypeargs=0 (testing.run_testset) 4 return } @@ -864,7 +863,7 @@ function testing.TestRegistry.run_test(self: testing.TestRegistry, name: string) 23 load_field 1 (body) 24 load_var 5 (t) 25 load_field 2 (runner) - 26 call 764 ntypeargs=0 (testing.run_test) + 26 call 795 ntypeargs=0 (testing.run_test) 27 jump +33 (to 60) 162 28 load_type 5 @@ -901,7 +900,7 @@ function testing.TestRegistry.run_test(self: testing.TestRegistry, name: string) 57 pop_jump_if_false -20 (to 37) 166 58 load_var2 9 2 - 59 call 747 ntypeargs=0 (testing.TestRegistry.run_test) + 59 call 778 ntypeargs=0 (testing.TestRegistry.run_test) 60 return 169 61 load_const 12 ("Test not found: ") @@ -936,11 +935,11 @@ function testing.TestRegistry.run_testset(self: testing.TestRegistry, name: stri 21 pop_jump_if_false -14 (to 7) 175 22 load_var2 1 5 - 23 call 754 ntypeargs=0 (testing.TestRegistry.expand_testset_registry) - 24 call 755 ntypeargs=0 (testing.testset_children) + 23 call 785 ntypeargs=0 (testing.TestRegistry.expand_testset_registry) + 24 call 786 ntypeargs=0 (testing.testset_children) 25 load_var 5 (ts) 26 load_field 2 (runner) - 27 call 765 ntypeargs=0 (testing.run_testset) + 27 call 796 ntypeargs=0 (testing.run_testset) 28 jump +33 (to 61) 178 29 load_type 5 @@ -977,7 +976,7 @@ function testing.TestRegistry.run_testset(self: testing.TestRegistry, name: stri 58 pop_jump_if_false -20 (to 38) 181 59 load_var2 9 2 - 60 call 748 ntypeargs=0 (testing.TestRegistry.run_testset) + 60 call 779 ntypeargs=0 (testing.TestRegistry.run_testset) 61 return 184 62 load_const 12 ("TestSet not found: ") @@ -1069,7 +1068,7 @@ function testing.TestRegistry.serialize(self: testing.TestRegistry) -> (testing. 71 store_var 12 (_27) 141 72 load_var 11 (sub) - 73 call 746 ntypeargs=0 (testing.TestRegistry.serialize) + 73 call 777 ntypeargs=0 (testing.TestRegistry.serialize) 74 store_var 13 (_28) 139 75 load_var2 3 12 @@ -1131,7 +1130,7 @@ function testing._int_to_float(value: int) -> float { 6 throw_if_panic 127 7 load_const 1 ("failed to convert int to float") - 8 call 247 ntypeargs=0 (baml.sys.panic) + 8 call 257 ntypeargs=0 (baml.sys.panic) 9 return } @@ -1239,7 +1238,7 @@ function testing.aggregate_reports(named_results: testing.NamedChildReport[]) -> 362 58 load_var 13 (outcome) 59 load_const 10 ("pass") - 60 call 651 ntypeargs=0 (baml.ops.equals_equals) + 60 call 661 ntypeargs=0 (baml.ops.equals_equals) 61 pop_jump_if_false +2 (to 63) 62 jump +59 (to 121) @@ -1306,7 +1305,7 @@ function testing.aggregate_reports(named_results: testing.NamedChildReport[]) -> 377 114 load_var 13 (outcome) 115 load_const 16 ("error") - 116 call 651 ntypeargs=0 (baml.ops.equals_equals) + 116 call 661 ntypeargs=0 (baml.ops.equals_equals) 117 pop_jump_if_false -97 (to 20) 378 118 load_const 17 (true) @@ -1378,7 +1377,7 @@ function testing.any_pattern_matches(pats: string[], canonical_id: string) -> bo 15 store_var 5 (p) 510 16 load_var2 2 5 - 17 call 767 ntypeargs=0 (testing.glob_match) + 17 call 798 ntypeargs=0 (testing.glob_match) 18 pop_jump_if_false -13 (to 5) 511 19 load_const 5 (true) @@ -1416,7 +1415,7 @@ function testing.classify_selected_names(selected_names: string[], hard_failed_n 23 store_var_load_var 8 (name) 753 24 load_var 3 (all_failed_names) - 25 call 783 ntypeargs=0 (testing.failure_matches_selected) + 25 call 814 ntypeargs=0 (testing.failure_matches_selected) 26 pop_jump_if_false +2 (to 28) 27 jump +8 (to 35) @@ -1429,7 +1428,7 @@ function testing.classify_selected_names(selected_names: string[], hard_failed_n 34 jump -21 (to 13) 754 35 load_var2 8 2 - 36 call 783 ntypeargs=0 (testing.failure_matches_selected) + 36 call 814 ntypeargs=0 (testing.failure_matches_selected) 37 pop_jump_if_false +2 (to 39) 38 jump +8 (to 46) @@ -1475,7 +1474,7 @@ function testing.collect_all_failed_names(report: testing.TestSetReport, out: st 16 store_var_load_var 6 (name) 824 17 load_var 2 (out) - 18 call 782 ntypeargs=0 (testing.name_in) + 18 call 813 ntypeargs=0 (testing.name_in) 19 unary_op ! 20 pop_jump_if_false -14 (to 6) 21 load_var2 2 6 @@ -1508,7 +1507,7 @@ function testing.collect_all_failed_names(report: testing.TestSetReport, out: st 46 store_var_load_var 10 (nested) 829 47 load_var 2 (out) - 48 call 785 ntypeargs=0 (testing.collect_all_failed_names) + 48 call 816 ntypeargs=0 (testing.collect_all_failed_names) 49 pop 1 50 jump -19 (to 31) @@ -1586,7 +1585,7 @@ function testing.collect_leaf_identities(report: testing.TestSetReport, tolerate 57 load_var 15 (nested) 58 load_field 0 (outcome) 59 load_const 3 ("pass") - 60 call 651 ntypeargs=0 (baml.ops.equals_equals) + 60 call 661 ntypeargs=0 (baml.ops.equals_equals) 61 store_var 16 (nested_tolerated) 804 62 load_var 15 (nested) @@ -1611,7 +1610,7 @@ function testing.collect_leaf_identities(report: testing.TestSetReport, tolerate 79 store_var_load_var 18 (sentinel) 808 80 load_var 3 (selected_names) - 81 call 782 ntypeargs=0 (testing.name_in) + 81 call 813 ntypeargs=0 (testing.name_in) 82 pop_jump_if_false +2 (to 84) 83 jump +21 (to 104) 84 load_var 15 (nested) @@ -1657,7 +1656,7 @@ function testing.collect_leaf_identities(report: testing.TestSetReport, tolerate 805 123 load_var2 15 16 124 load_var2 3 4 - 125 call 784 ntypeargs=0 (testing.collect_leaf_identities) + 125 call 815 ntypeargs=0 (testing.collect_leaf_identities) 126 pop 1 127 jump +31 (to 158) @@ -1666,7 +1665,7 @@ function testing.collect_leaf_identities(report: testing.TestSetReport, tolerate 794 130 load_field 0 (outcome) 131 load_const 7 ("pass") - 132 call 651 ntypeargs=0 (baml.ops.equals_equals) + 132 call 661 ntypeargs=0 (baml.ops.equals_equals) 133 pop_jump_if_false +2 (to 135) 134 jump +18 (to 152) @@ -1736,7 +1735,7 @@ function testing.collect_leaf_messages(results: (testing.TestReport | testing.Te 849 24 load_field 5 (results) 25 load_var 2 (out) - 26 call 786 ntypeargs=0 (testing.collect_leaf_messages) + 26 call 817 ntypeargs=0 (testing.collect_leaf_messages) 27 pop 1 28 jump -23 (to 5) @@ -1762,7 +1761,7 @@ function testing.collect_leaf_messages(results: (testing.TestReport | testing.Te 841 47 load_field 0 (outcome) 48 load_const 10 ("pass") - 49 call 651 ntypeargs=0 (baml.ops.equals_equals) + 49 call 661 ntypeargs=0 (baml.ops.equals_equals) 50 unary_op ! 51 pop_jump_if_false -15 (to 36) @@ -1836,7 +1835,7 @@ function testing.collect_leaf_names(registry: testing.TestRegistry) -> string[] 43 store_var 9 (ts) 618 44 load_var2 1 9 - 45 call 777 ntypeargs=0 (testing.collect_subtree_names) + 45 call 808 ntypeargs=0 (testing.collect_subtree_names) 46 load_type 10 47 load_const 11 ("iter") 48 virtual_call nargs=1 ntypeargs=0 @@ -1866,8 +1865,8 @@ function testing.collect_leaf_names(registry: testing.TestRegistry) -> string[] function testing.collect_subtree_names(registry: testing.TestRegistry, ts: testing.TestSetRegistration) -> string[] { 631 0 load_var2 1 2 - 1 call 754 ntypeargs=0 (testing.TestRegistry.expand_testset_registry) - 2 call 776 ntypeargs=0 (testing.collect_leaf_names) + 1 call 785 ntypeargs=0 (testing.TestRegistry.expand_testset_registry) + 2 call 807 ntypeargs=0 (testing.collect_leaf_names) 3 jump +91 (to 94) 4 load_var 3 (e) 5 store_var 4 (_5) @@ -1969,10 +1968,10 @@ function testing.collect_subtree_names(registry: testing.TestRegistry, ts: testi function testing.collect_subtree_names_layered(registry: testing.TestRegistry, ts: testing.TestSetRegistration, profile_include: string[], profile_exclude: string[], cli_include: string[], cli_exclude: string[]) -> string[] { 639 0 load_var2 1 2 - 1 call 754 ntypeargs=0 (testing.TestRegistry.expand_testset_registry) + 1 call 785 ntypeargs=0 (testing.TestRegistry.expand_testset_registry) 2 load_var2 3 4 3 load_var2 5 6 - 4 call 774 ntypeargs=0 (testing.select_names_layered) + 4 call 805 ntypeargs=0 (testing.select_names_layered) 5 jump +111 (to 116) 6 load_var 7 (e) 7 store_var 8 (_9) @@ -2055,7 +2054,7 @@ function testing.collect_subtree_names_layered(registry: testing.TestRegistry, t 647 83 load_var2 3 4 84 load_var2 5 6 - 85 call 770 ntypeargs=0 (testing.leaf_selected_layered) + 85 call 801 ntypeargs=0 (testing.leaf_selected_layered) 86 pop_jump_if_false +2 (to 88) 87 jump +4 (to 91) 88 load_type 18 @@ -2074,7 +2073,7 @@ function testing.collect_subtree_names_layered(registry: testing.TestRegistry, t 643 100 load_var2 3 4 101 load_var2 5 6 - 102 call 770 ntypeargs=0 (testing.leaf_selected_layered) + 102 call 801 ntypeargs=0 (testing.leaf_selected_layered) 103 pop_jump_if_false +2 (to 105) 104 jump +4 (to 108) 105 load_type 18 @@ -2140,7 +2139,7 @@ function testing.count_leaves(results: (testing.TestReport | testing.TestSetRepo 37 load_var 13 (tsr) 38 load_field 0 (outcome) 39 load_const 7 ("pass") - 40 call 651 ntypeargs=0 (baml.ops.equals_equals) + 40 call 661 ntypeargs=0 (baml.ops.equals_equals) 41 store_var 14 (ft) 687 42 load_var 13 (tsr) @@ -2184,7 +2183,7 @@ function testing.count_leaves(results: (testing.TestReport | testing.TestSetRepo 688 74 load_var 13 (tsr) 75 load_field 5 (results) 76 load_var 14 (ft) - 77 call 779 ntypeargs=0 (testing.count_leaves) + 77 call 810 ntypeargs=0 (testing.count_leaves) 78 store_var 10 (delta) 79 jump +31 (to 110) @@ -2193,7 +2192,7 @@ function testing.count_leaves(results: (testing.TestReport | testing.TestSetRepo 677 82 load_field 0 (outcome) 83 load_const 8 ("pass") - 84 call 651 ntypeargs=0 (baml.ops.equals_equals) + 84 call 661 ntypeargs=0 (baml.ops.equals_equals) 85 pop_jump_if_false +2 (to 87) 86 jump +18 (to 104) @@ -2279,7 +2278,7 @@ function testing.expansion_error_report(name: string) -> testing.TestSetReport { function testing.failure_matches_selected(selected: string, failed_names: string[]) -> bool { 774 0 load_var2 1 2 - 1 call 782 ntypeargs=0 (testing.name_in) + 1 call 813 ntypeargs=0 (testing.name_in) 2 pop_jump_if_false +2 (to 4) 3 jump +43 (to 46) @@ -2339,7 +2338,7 @@ function testing.flatten_with_tolerated(report: testing.TestSetReport, selected_ 709 0 load_var 1 (report) 1 load_field 0 (outcome) 2 load_const 0 ("pass") - 3 call 651 ntypeargs=0 (baml.ops.equals_equals) + 3 call 661 ntypeargs=0 (baml.ops.equals_equals) 4 store_var 3 (top_tolerated) 710 5 load_var 1 (report) @@ -2383,7 +2382,7 @@ function testing.flatten_with_tolerated(report: testing.TestSetReport, selected_ 711 37 load_var 1 (report) 38 load_field 5 (results) 39 load_var 3 (top_tolerated) - 40 call 779 ntypeargs=0 (testing.count_leaves) + 40 call 810 ntypeargs=0 (testing.count_leaves) 41 store_var 4 (counts) 717 42 load_type 2 @@ -2393,7 +2392,7 @@ function testing.flatten_with_tolerated(report: testing.TestSetReport, selected_ 718 45 load_var 1 (report) 46 load_field 5 (results) 47 load_var 6 (messages) - 48 call 786 ntypeargs=0 (testing.collect_leaf_messages) + 48 call 817 ntypeargs=0 (testing.collect_leaf_messages) 49 pop 1 719 50 load_type 2 @@ -2401,7 +2400,7 @@ function testing.flatten_with_tolerated(report: testing.TestSetReport, selected_ 52 store_var 7 (all_failed_names) 720 53 load_var2 1 7 - 54 call 785 ntypeargs=0 (testing.collect_all_failed_names) + 54 call 816 ntypeargs=0 (testing.collect_all_failed_names) 55 pop 1 721 56 load_type 2 @@ -2415,7 +2414,7 @@ function testing.flatten_with_tolerated(report: testing.TestSetReport, selected_ 722 64 load_var2 1 3 65 load_var2 2 8 - 66 call 784 ntypeargs=0 (testing.collect_leaf_identities) + 66 call 815 ntypeargs=0 (testing.collect_leaf_identities) 67 pop 1 727 68 load_var 8 (executed) @@ -2469,7 +2468,7 @@ function testing.flatten_with_tolerated(report: testing.TestSetReport, selected_ 730 112 load_var2 2 1 113 load_field 4 (failed_names) 114 load_var 7 (all_failed_names) - 115 call 781 ntypeargs=0 (testing.classify_selected_names) + 115 call 812 ntypeargs=0 (testing.classify_selected_names) 116 store_var 9 (identities) 117 jump +3 (to 120) @@ -2630,14 +2629,14 @@ function testing.glob_match(subject: string, pattern: string) -> bool { 485 98 load_var2 1 2 99 call 155 ntypeargs=0 (baml.String.index_of) 100 load_const 6 (null) - 101 call 651 ntypeargs=0 (baml.ops.equals_equals) + 101 call 661 ntypeargs=0 (baml.ops.equals_equals) 102 unary_op ! 103 return } function testing.leaf_selected(name: string, include: string[], exclude: string[]) -> bool { 520 0 load_var2 3 1 - 1 call 768 ntypeargs=0 (testing.any_pattern_matches) + 1 call 799 ntypeargs=0 (testing.any_pattern_matches) 2 pop_jump_if_false +2 (to 4) 3 jump +14 (to 17) @@ -2651,7 +2650,7 @@ function testing.leaf_selected(name: string, include: string[], exclude: string[ 11 jump +4 (to 15) 526 12 load_var2 2 1 - 13 call 768 ntypeargs=0 (testing.any_pattern_matches) + 13 call 799 ntypeargs=0 (testing.any_pattern_matches) 14 jump +4 (to 18) 524 15 load_const 1 (true) @@ -2664,12 +2663,12 @@ function testing.leaf_selected(name: string, include: string[], exclude: string[ function testing.leaf_selected_layered(name: string, profile_include: string[], profile_exclude: string[], cli_include: string[], cli_exclude: string[]) -> bool { 533 0 load_var2 1 2 1 load_var 3 (profile_exclude) - 2 call 769 ntypeargs=0 (testing.leaf_selected) + 2 call 800 ntypeargs=0 (testing.leaf_selected) 3 jump_if_false +5 (to 8) 4 pop 1 5 load_var2 1 4 6 load_var 5 (cli_exclude) - 7 call 769 ntypeargs=0 (testing.leaf_selected) + 7 call 800 ntypeargs=0 (testing.leaf_selected) 8 return } @@ -2768,7 +2767,7 @@ function testing.run_children_parallel(children: testing.TestSetChild[]) -> test 24 store_deref 5 399 25 load_var 5 (child) - 26 make_closure 1382 1 + 26 make_closure 1440 1 27 load_const 6 (null) 28 load_const 6 (null) 29 load_type 7 @@ -2783,10 +2782,10 @@ function testing.run_children_parallel(children: testing.TestSetChild[]) -> test 405 37 load_type 7 38 load_type 8 39 load_var 2 (futures) - 40 call 521 ntypeargs=2 (baml.future.all_complete) + 40 call 531 ntypeargs=2 (baml.future.all_complete) 41 await - 406 42 call 762 ntypeargs=0 (testing.aggregate_reports) + 406 42 call 793 ntypeargs=0 (testing.aggregate_reports) 43 return } @@ -2849,16 +2848,16 @@ function testing.run_children_sequential(children: testing.TestSetChild[], fail_ 47 pop 1 48 load_var 8 (outcome) 49 load_const 7 ("pass") - 50 call 651 ntypeargs=0 (baml.ops.equals_equals) + 50 call 661 ntypeargs=0 (baml.ops.equals_equals) 51 unary_op ! 52 pop_jump_if_false -44 (to 8) 117 53 load_var 3 (named_results) - 54 call 762 ntypeargs=0 (testing.aggregate_reports) + 54 call 793 ntypeargs=0 (testing.aggregate_reports) 55 jump +3 (to 58) 122 56 load_var 3 (named_results) - 57 call 762 ntypeargs=0 (testing.aggregate_reports) + 57 call 793 ntypeargs=0 (testing.aggregate_reports) 58 return } @@ -2868,7 +2867,7 @@ function testing.run_test(body: () -> void throws unknown, runner: ((() -> testi 2 store_var 1 411 3 load_var 1 (body) - 4 make_closure 1393 1 + 4 make_closure 1451 1 5 store_var 3 (base_run) 435 6 load_var 2 (runner) @@ -2904,7 +2903,7 @@ function testing.run_testset(children: testing.TestSetChild[], runner: ((testing 9 jump +3 (to 12) 447 10 load_var 1 (children) - 11 call 763 ntypeargs=0 (testing.run_children_parallel) + 11 call 794 ntypeargs=0 (testing.run_children_parallel) 12 return } @@ -2915,7 +2914,7 @@ function testing.select_names(registry: testing.TestRegistry, include: string[], 3 load_type 0 4 alloc_array 0 5 load_var2 2 3 - 6 call 774 ntypeargs=0 (testing.select_names_layered) + 6 call 805 ntypeargs=0 (testing.select_names_layered) 7 return } @@ -2946,7 +2945,7 @@ function testing.select_names_layered(registry: testing.TestRegistry, profile_in 591 21 load_field 0 (name) 22 load_var2 2 3 23 load_var2 4 5 - 24 call 770 ntypeargs=0 (testing.leaf_selected_layered) + 24 call 801 ntypeargs=0 (testing.leaf_selected_layered) 25 pop_jump_if_false -15 (to 10) 592 26 load_var2 7 10 @@ -2976,19 +2975,19 @@ function testing.select_names_layered(registry: testing.TestRegistry, profile_in 598 49 load_field 0 (name) 50 load_var2 2 3 - 51 call 773 ntypeargs=0 (testing.subtree_may_be_selected) + 51 call 804 ntypeargs=0 (testing.subtree_may_be_selected) 52 jump_if_false +6 (to 58) 53 pop 1 54 load_var 13 (ts) 55 load_field 0 (name) 56 load_var2 4 5 - 57 call 773 ntypeargs=0 (testing.subtree_may_be_selected) + 57 call 804 ntypeargs=0 (testing.subtree_may_be_selected) 58 pop_jump_if_false -20 (to 38) 599 59 load_var2 1 13 60 load_var2 2 3 61 load_var2 4 5 - 62 call 778 ntypeargs=0 (testing.collect_subtree_names_layered) + 62 call 809 ntypeargs=0 (testing.collect_subtree_names_layered) 600 63 load_type 10 64 load_const 11 ("iter") @@ -3043,13 +3042,13 @@ function testing.subtree_excluded(prefix: string, excludes: string[]) -> bool { 21 load_const 6 ("*") 22 call 155 ntypeargs=0 (baml.String.index_of) 23 load_const 7 (null) - 24 call 651 ntypeargs=0 (baml.ops.equals_equals) + 24 call 661 ntypeargs=0 (baml.ops.equals_equals) 25 jump_if_false +7 (to 32) 26 pop 1 27 load_var2 3 6 28 call 155 ntypeargs=0 (baml.String.index_of) 29 load_const 7 (null) - 30 call 651 ntypeargs=0 (baml.ops.equals_equals) + 30 call 661 ntypeargs=0 (baml.ops.equals_equals) 31 unary_op ! 32 pop_jump_if_false +2 (to 34) 33 jump +11 (to 44) @@ -3060,7 +3059,7 @@ function testing.subtree_excluded(prefix: string, excludes: string[]) -> bool { 37 jump_if_false +4 (to 41) 38 pop 1 39 load_var2 3 6 - 40 call 767 ntypeargs=0 (testing.glob_match) + 40 call 798 ntypeargs=0 (testing.glob_match) 41 pop_jump_if_false -32 (to 9) 550 42 load_const 9 (true) @@ -3075,7 +3074,7 @@ function testing.subtree_excluded(prefix: string, excludes: string[]) -> bool { function testing.subtree_may_be_selected(prefix: string, include: string[], exclude: string[]) -> bool { 572 0 load_var2 1 3 - 1 call 771 ntypeargs=0 (testing.subtree_excluded) + 1 call 802 ntypeargs=0 (testing.subtree_excluded) 2 pop_jump_if_false +2 (to 4) 3 jump +34 (to 37) @@ -3106,7 +3105,7 @@ function testing.subtree_may_be_selected(prefix: string, include: string[], excl 27 store_var_load_var 7 (pattern) 579 28 load_var 1 (prefix) - 29 call 772 ntypeargs=0 (testing.pattern_may_match_descendant) + 29 call 803 ntypeargs=0 (testing.pattern_may_match_descendant) 30 pop_jump_if_false -13 (to 17) 580 31 load_const 6 (true) @@ -3133,7 +3132,7 @@ function testing.test_child(name: string, body: () -> void throws unknown, runne 294 6 load_var2 1 2 296 7 load_var 3 (runner) - 8 make_closure 1359 2 + 8 make_closure 1417 2 9 init_instance 0 (testing.TestSetChild .name, .run) 10 store_var 4 (_0) 11 load_var 4 (_0) @@ -3152,7 +3151,7 @@ function testing.testset_child(registry: testing.TestRegistry, ts: testing.TestS 7 load_field 0 (name) 303 8 load_var2 1 2 - 9 make_closure 1361 2 + 9 make_closure 1419 2 10 init_instance 0 (testing.TestSetChild .name, .run) 11 store_var 3 (_0) 12 load_var 3 (_0) @@ -3175,7 +3174,7 @@ function testing.testset_child_selected(registry: testing.TestRegistry, ts: test 315 11 load_var2 1 2 12 load_var 3 (names) - 13 make_closure 1363 3 + 13 make_closure 1421 3 14 init_instance 0 (testing.TestSetChild .name, .run) 15 store_var 4 (_0) 16 load_var 4 (_0) @@ -3211,7 +3210,7 @@ function testing.testset_children(registry: testing.TestRegistry) -> testing.Tes 23 load_field 1 (body) 24 load_var 6 (t) 25 load_field 2 (runner) - 26 call 757 ntypeargs=0 (testing.test_child) + 26 call 788 ntypeargs=0 (testing.test_child) 27 store_var 7 (_10) 28 load_var2 3 7 29 call 4 ntypeargs=0 (baml.Array.push) @@ -3238,7 +3237,7 @@ function testing.testset_children(registry: testing.TestRegistry) -> testing.Tes 49 store_var 10 (ts) 264 50 load_var2 1 10 - 51 call 758 ntypeargs=0 (testing.testset_child) + 51 call 789 ntypeargs=0 (testing.testset_child) 52 store_var 11 (_21) 53 load_var2 3 11 54 call 4 ntypeargs=0 (baml.Array.push) @@ -3277,7 +3276,7 @@ function testing.testset_children_selected(registry: testing.TestRegistry, names 272 21 load_field 0 (name) 22 load_var 2 (names) - 23 call 760 ntypeargs=0 (testing._name_selected) + 23 call 791 ntypeargs=0 (testing._name_selected) 24 pop_jump_if_false -14 (to 10) 273 25 load_var 7 (t) @@ -3286,7 +3285,7 @@ function testing.testset_children_selected(registry: testing.TestRegistry, names 28 load_field 1 (body) 29 load_var 7 (t) 30 load_field 2 (runner) - 31 call 757 ntypeargs=0 (testing.test_child) + 31 call 788 ntypeargs=0 (testing.test_child) 32 store_var 8 (_13) 33 load_var2 4 8 34 call 4 ntypeargs=0 (baml.Array.push) @@ -3314,12 +3313,12 @@ function testing.testset_children_selected(registry: testing.TestRegistry, names 284 55 load_field 0 (name) 56 load_var 2 (names) - 57 call 761 ntypeargs=0 (testing._has_selected_descendant) + 57 call 792 ntypeargs=0 (testing._has_selected_descendant) 58 pop_jump_if_false -14 (to 44) 285 59 load_var2 1 11 60 load_var 2 (names) - 61 call 759 ntypeargs=0 (testing.testset_child_selected) + 61 call 790 ntypeargs=0 (testing.testset_child_selected) 62 store_var 12 (_26) 63 load_var2 4 12 64 call 4 ntypeargs=0 (baml.Array.push) @@ -3341,7 +3340,7 @@ function user.User.promote(self: User, bonus: int) -> Report { 5 store_field 1 (age) 10 6 load_var2 1 2 - 7 call 801 ntypeargs=0 (user.score) + 7 call 832 ntypeargs=0 (user.score) 8 store_var 4 (base) 11 9 load_var 4 (base) diff --git a/baml_language/crates/baml_tests/tests/type_kinds.rs b/baml_language/crates/baml_tests/tests/type_kinds.rs new file mode 100644 index 00000000000..cebd2125b00 --- /dev/null +++ b/baml_language/crates/baml_tests/tests/type_kinds.rs @@ -0,0 +1,261 @@ +//! BEP-066 slice-1 capstone oracles for the nine sealed reflection-kind views. + +use baml_tests::baml_test; +use bex_engine::BexExternalValue; + +#[tokio::test] +async fn kind_union_is_exhaustive_and_classifies_all_nine_kinds() { + let output = baml_test!( + r#" + class Foo { value int } + enum Color { Red Blue } + interface Marker {} + type Callback = (x: int, label: string) -> bool throws never + + function classify(t: type) -> string { + match (t.kind()) { + baml.reflect.class.Type => "class", + baml.reflect.enum.Type => "enum", + baml.reflect.union.Type => "union", + baml.reflect.literal.Type => "literal", + baml.reflect.array.Type => "array", + baml.reflect.map.Type => "map", + baml.reflect.interface.Type => "interface", + baml.reflect.primitive.Type => "primitive", + baml.reflect.function.Type => "function" + } + } + + function main() -> bool { + classify(type.of()) == "class" + && classify(type.of()) == "enum" + && classify(type.of()) == "union" + && classify(type.of<"fixed">()) == "literal" + && classify(type.of()) == "array" + && classify(type.of>()) == "map" + && classify(type.of()) == "interface" + && classify(type.of()) == "primitive" + && classify(type.of()) == "function" + } + "# + ); + assert_eq!(output.result, Ok(BexExternalValue::Bool(true))); +} + +#[tokio::test] +async fn kind_casts_are_nullable_identity_preserving_views() { + let output = baml_test!( + r#" + class Foo { value int } + + function main() -> bool throws string { + let t = type.of(); + let view = t.as_class() ?? throw "expected class kind"; + let optional_count = t.as_class()?.fields()?.length(); + + t.kind().as_type() == t + && view.as_type() == t + && view == (t.as_class() ?? throw "expected the same class kind") + && optional_count == 1 + && t.as_enum() == null + && type.of().as_class() == null + && type.of().as_primitive()?.as_type() == type.of() + && type.of() is type + } + "# + ); + assert_eq!(output.result, Ok(BexExternalValue::Bool(true))); +} + +#[tokio::test] +async fn class_and_enum_readback_preserves_schema_metadata() { + let output = baml_test!( + r#" + /// Person docs + class Person { + /// Name docs + name string @alias("full_name") @description("Name description") @custom("field-extra") + @@alias("PersonAlias") + @@description("Person description") + @@custom("class-extra") + } + + /// Color docs + enum Color { + /// Red docs + Red @alias("rouge") @description("Red description") @custom("variant-extra") + Blue + @@alias("ColorAlias") + @@description("Color description") + @@custom("enum-extra") + } + + function main() -> bool throws string { + let class_view = type.of().as_class() ?? throw "class"; + let class_meta = class_view.meta(); + let field = class_view.fields()[0]; + let enum_view = type.of().as_enum() ?? throw "enum"; + let enum_meta = enum_view.meta(); + let red = enum_view.values()[0]; + + class_meta.alias == "PersonAlias" + && class_meta.description == "Person description" + && class_meta.docstring == "Person docs" + && class_meta.other.get("custom") == "class-extra" + && field.name == "name" + && field.type == type.of() + && field.meta.alias == "full_name" + && field.meta.description == "Name description" + && field.meta.docstring == "Name docs" + && field.meta.other.get("custom") == "field-extra" + && enum_meta.alias == "ColorAlias" + && enum_meta.description == "Color description" + && enum_meta.docstring == "Color docs" + && enum_meta.other.get("custom") == "enum-extra" + && red.name == "Red" + && red.meta.alias == "rouge" + && red.meta.description == "Red description" + && red.meta.docstring == "Red docs" + && red.meta.other.get("custom") == "variant-extra" + } + "# + ); + assert_eq!(output.result, Ok(BexExternalValue::Bool(true))); +} + +#[tokio::test] +async fn nested_type_walker_and_kind_specific_readback_work_end_to_end() { + let output = baml_test!( + r#" + interface Marker { + function mark(self) -> string throws never + } + + class Foo { + value int + implements Marker { + function mark(self) -> string throws never { "foo" } + } + } + enum Color { Red } + type Callback = (x: int, label: string) -> bool throws string + + function walk(t: type) -> int { + match (t.kind()) { + let class_view: baml.reflect.class.Type => 1, + let enum_view: baml.reflect.enum.Type => 1, + let union_view: baml.reflect.union.Type => { + let count = 1; + for let member in union_view.member_types() { + count += walk(member) + } + count + }, + let literal_view: baml.reflect.literal.Type => 1, + let array_view: baml.reflect.array.Type => 1 + walk(array_view.element_type()), + let map_view: baml.reflect.map.Type => 1, + let interface_view: baml.reflect.interface.Type => 1, + let primitive_view: baml.reflect.primitive.Type => 1, + let function_view: baml.reflect.function.Type => 1 + } + } + + function read_views( + union_view: baml.reflect.union.Type, + array_view: baml.reflect.array.Type, + map_view: baml.reflect.map.Type, + interface_view: baml.reflect.interface.Type, + function_view: baml.reflect.function.Type + ) -> bool throws never { + let params = function_view.params(); + let function_schema = match (type.of().kind()) { + let class_view: baml.reflect.class.Type => class_view, + _ => return false + }; + union_view.member_types().length() == 2 + && array_view.element_type() == type.of() + && map_view.key_type() == type.of() + && map_view.value_type() == type.of() + && interface_view.implemented_by(type.of()) + && params.length() == 2 + && params[0].name == "x" + && params[0].type == type.of() + && params[0].optional == false + && params[1].name == "label" + && params[1].type == type.of() + && function_view.return_type() == type.of() + && function_schema.fields().length() == 0 + } + + function intrinsic_checks() -> bool throws never { + type.of_value(1) == type.of() + && type.of_value(1).as_primitive() != null + && type.of().to_string() == "Foo" + && type.of().to_string() == "int" + && type.of().to_string() != "" + && type.of().to_string() != "" + && type.of<"fixed">().to_string() != "" + && type.of().to_string() != "" + && type.of>().to_string() != "" + && type.of().to_string() != "" + && type.of().to_string() != "" + } + + function casts_are_never_throwing(t: type) -> bool throws never { + t.as_class() != null + && t.as_enum() == null + && t.as_union() == null + && t.as_literal() == null + && t.as_array() == null + && t.as_map() == null + && t.as_interface() == null + && t.as_primitive() == null + && t.as_function() == null + } + + function all_positive_kind_casts_work() -> bool throws never { + type.of().as_class() != null + && type.of().as_enum() != null + && type.of().as_union() != null + && type.of<"fixed">().as_literal() != null + && type.of().as_array() != null + && type.of>().as_map() != null + && type.of().as_interface() != null + && type.of().as_primitive() != null + && type.of().as_function() != null + } + + function kind_identity(t: type) -> bool throws never { + t.kind().as_type() == t + } + + function every_kind_preserves_identity() -> bool throws never { + kind_identity(type.of()) + && kind_identity(type.of()) + && kind_identity(type.of()) + && kind_identity(type.of<"fixed">()) + && kind_identity(type.of()) + && kind_identity(type.of>()) + && kind_identity(type.of()) + && kind_identity(type.of()) + && kind_identity(type.of()) + } + + function main() -> bool throws unknown { + let union_view = type.of().as_union() ?? throw "union"; + let array_view = type.of().as_array() ?? throw "array"; + let map_view = type.of>().as_map() ?? throw "map"; + let interface_view = type.of().as_interface() ?? throw "interface"; + let function_view = type.of().as_function() ?? throw "function"; + + walk(type.of<(Foo | string[])[]>()) == 5 + && read_views(union_view, array_view, map_view, interface_view, function_view) + && intrinsic_checks() + && casts_are_never_throwing(type.of()) + && all_positive_kind_casts_work() + && every_kind_preserves_identity() + } + "# + ); + assert_eq!(output.result, Ok(BexExternalValue::Bool(true))); +} diff --git a/baml_language/crates/baml_type/src/lib.rs b/baml_language/crates/baml_type/src/lib.rs index 9f5d45ce02b..e2514a6ce0c 100644 --- a/baml_language/crates/baml_type/src/lib.rs +++ b/baml_language/crates/baml_type/src/lib.rs @@ -43,6 +43,7 @@ mod runtime_ty; pub mod simplify_sap; pub mod template; pub mod throw_facts; +pub mod type_kind; pub mod typetag; pub use attr::*; pub use defs::*; diff --git a/baml_language/crates/baml_type/src/normalize.rs b/baml_language/crates/baml_type/src/normalize.rs index bcd1c6e76d5..bc97b8016f5 100644 --- a/baml_language/crates/baml_type/src/normalize.rs +++ b/baml_language/crates/baml_type/src/normalize.rs @@ -769,6 +769,9 @@ impl NormalTy { NormalTy::Type => Category::Type, NormalTy::Resource => Category::Resource, NormalTy::PromptAst => Category::PromptAst, + NormalTy::Class(name, _) if crate::type_kind::is_type_kind_class(name) => { + Category::Type + } NormalTy::Class(..) => Category::Class, NormalTy::List(_) => Category::List, NormalTy::Map { .. } => Category::Map, @@ -2029,6 +2032,14 @@ impl NormalTy { .zip(a2.iter()) .all(|(a, b)| a.invariant_compatible(b, ctx, assumptions)) } + // BEP-066: the nine reflection-kind classes form one sealed family + // beneath the `type` carrier. Because membership is hard-coded to + // builtin qualified names, user classes cannot acquire this edge. + (NormalTy::Class(name, _), NormalTy::Type) + if crate::type_kind::is_type_kind_class(name) => + { + true + } (NormalTy::List(a), NormalTy::List(b)) => a.invariant_compatible(b, ctx, assumptions), (NormalTy::Map { key: k1, value: v1 }, NormalTy::Map { key: k2, value: v2 }) => { k1.invariant_compatible(k2, ctx, assumptions) diff --git a/baml_language/crates/baml_type/src/type_kind.rs b/baml_language/crates/baml_type/src/type_kind.rs new file mode 100644 index 00000000000..7b0c36bfc68 --- /dev/null +++ b/baml_language/crates/baml_type/src/type_kind.rs @@ -0,0 +1,99 @@ +//! Closed BEP-066 reflection-kind classification. + +use crate::{ConcreteRealizedTy, Name, QualifiedTypeName, RealizedTy, TyAttr}; + +/// The nine sealed runtime views of a reflected `type` value. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum TypeKind { + Class, + Enum, + Union, + Literal, + Array, + Map, + Interface, + Primitive, + Function, +} + +impl TypeKind { + pub const ALL: [Self; 9] = [ + Self::Class, + Self::Enum, + Self::Union, + Self::Literal, + Self::Array, + Self::Map, + Self::Interface, + Self::Primitive, + Self::Function, + ]; + + pub const fn namespace(self) -> &'static str { + match self { + Self::Class => "class", + Self::Enum => "enum", + Self::Union => "union", + Self::Literal => "literal", + Self::Array => "array", + Self::Map => "map", + Self::Interface => "interface", + Self::Primitive => "primitive", + Self::Function => "function", + } + } + + /// The builtin class that is the sealed view for this kind. + pub fn class_name(self) -> QualifiedTypeName { + QualifiedTypeName::new( + Name::new("baml"), + vec![Name::new("reflect"), Name::new(self.namespace())], + Name::new("Type"), + ) + } + + pub fn concrete_class_ty(self) -> ConcreteRealizedTy { + ConcreteRealizedTy::Class(self.class_name(), Vec::new(), TyAttr::default()) + } +} + +/// Classify every realized runtime type into exactly one reflection kind. +pub fn classify_type(ty: &RealizedTy) -> TypeKind { + match ty { + RealizedTy::Class(..) => TypeKind::Class, + RealizedTy::Enum(..) => TypeKind::Enum, + RealizedTy::Union(..) => TypeKind::Union, + RealizedTy::Literal(..) | RealizedTy::EnumVariant(..) => TypeKind::Literal, + RealizedTy::List(..) => TypeKind::Array, + RealizedTy::Map { .. } => TypeKind::Map, + RealizedTy::Interface(..) => TypeKind::Interface, + RealizedTy::Function { .. } => TypeKind::Function, + _ => TypeKind::Primitive, + } +} + +/// Whether a nominal class is one of the nine sealed reflection-kind classes. +pub fn is_type_kind_class(name: &QualifiedTypeName) -> bool { + name.package().as_str() == "baml" + && name.namespace().len() == 2 + && name.namespace()[0].as_str() == "reflect" + && TypeKind::ALL + .iter() + .any(|kind| name.namespace()[1].as_str() == kind.namespace()) + && name.name().as_str() == "Type" +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn kind_names_are_closed_and_recognized() { + for kind in TypeKind::ALL { + assert!(is_type_kind_class(&kind.class_name())); + } + assert!(!is_type_kind_class(&QualifiedTypeName::from_dotted_path( + "baml.reflect.Type" + ))); + } +} diff --git a/baml_language/crates/bex_engine/src/conversion.rs b/baml_language/crates/bex_engine/src/conversion.rs index 4295b7c0b9c..9a08a548373 100644 --- a/baml_language/crates/bex_engine/src/conversion.rs +++ b/baml_language/crates/bex_engine/src/conversion.rs @@ -4070,17 +4070,23 @@ mod union_container_selection_tests { name: "HAPPY".to_string(), description: None, alias: None, + docstring: None, + other: indexmap::IndexMap::new(), skip: false, }, EnumVariant { name: "SAD".to_string(), description: None, alias: None, + docstring: None, + other: indexmap::IndexMap::new(), skip: false, }, ], description: None, alias: None, + docstring: None, + other: indexmap::IndexMap::new(), ty_attr: TyAttr::default(), }))); let happy = RuntimeTy::EnumVariant(mood.clone(), Name::new("HAPPY"), TyAttr::default()); diff --git a/baml_language/crates/bex_heap/src/gc.rs b/baml_language/crates/bex_heap/src/gc.rs index 5c5892434d1..31fe22311e4 100644 --- a/baml_language/crates/bex_heap/src/gc.rs +++ b/baml_language/crates/bex_heap/src/gc.rs @@ -1390,10 +1390,14 @@ mod tests { name: "A".to_string(), description: None, alias: None, + docstring: None, + other: Default::default(), skip: false, }], description: None, alias: None, + docstring: None, + other: Default::default(), ty_attr: baml_type::TyAttr::default(), }))]; let debug = HeapDebuggerConfig { @@ -1434,10 +1438,14 @@ mod tests { }), description: None, alias: None, + docstring: None, + other: Default::default(), skip: false, }], description: None, alias: None, + docstring: None, + other: Default::default(), type_tag: 100, ty_attr: baml_type::TyAttr::default(), has_cleanup: false, @@ -2079,6 +2087,8 @@ mod tests { fields: vec![], description: None, alias: None, + docstring: None, + other: Default::default(), type_tag: 0, ty_attr: TyAttr::default(), has_cleanup: false, @@ -2122,6 +2132,8 @@ mod tests { variants: vec![], description: None, alias: None, + docstring: None, + other: Default::default(), ty_attr: TyAttr::default(), }))); let var_ptr = tlab.alloc_variant(enum_ptr, 1); @@ -2347,6 +2359,8 @@ mod tests { fields: vec![], description: None, alias: None, + docstring: None, + other: Default::default(), type_tag: 42, ty_attr: TyAttr::default(), has_cleanup: false, @@ -2373,6 +2387,8 @@ mod tests { variants: vec![], description: None, alias: None, + docstring: None, + other: Default::default(), ty_attr: TyAttr::default(), }))); @@ -2798,6 +2814,8 @@ mod tests { fields: vec![], description: None, alias: None, + docstring: None, + other: Default::default(), type_tag: 0, ty_attr: TyAttr::default(), has_cleanup: false, @@ -2815,6 +2833,8 @@ mod tests { variants: vec![], description: None, alias: None, + docstring: None, + other: Default::default(), ty_attr: TyAttr::default(), }))); let variant_container = tlab.alloc(Object::Variant(Variant { diff --git a/baml_language/crates/bex_heap/src/tlab.rs b/baml_language/crates/bex_heap/src/tlab.rs index 9d79bad862a..98e71018934 100644 --- a/baml_language/crates/bex_heap/src/tlab.rs +++ b/baml_language/crates/bex_heap/src/tlab.rs @@ -610,6 +610,8 @@ mod tests { }), description: None, alias: None, + docstring: None, + other: Default::default(), skip: false, }, bex_vm_types::ClassField { @@ -622,11 +624,15 @@ mod tests { }), description: None, alias: None, + docstring: None, + other: Default::default(), skip: false, }, ], description: None, alias: None, + docstring: None, + other: Default::default(), type_tag: 100, ty_attr: baml_type::TyAttr::default(), has_cleanup: false, @@ -664,23 +670,31 @@ mod tests { name: "Red".to_string(), description: None, alias: None, + docstring: None, + other: Default::default(), skip: false, }, bex_vm_types::EnumVariant { name: "Green".to_string(), description: None, alias: None, + docstring: None, + other: Default::default(), skip: false, }, bex_vm_types::EnumVariant { name: "Blue".to_string(), description: None, alias: None, + docstring: None, + other: Default::default(), skip: false, }, ], description: None, alias: None, + docstring: None, + other: Default::default(), ty_attr: baml_type::TyAttr::default(), }))); diff --git a/baml_language/crates/bex_vm/src/package_baml/mod.rs b/baml_language/crates/bex_vm/src/package_baml/mod.rs index 0477f978d22..a979908e0c0 100644 --- a/baml_language/crates/bex_vm/src/package_baml/mod.rs +++ b/baml_language/crates/bex_vm/src/package_baml/mod.rs @@ -47,6 +47,7 @@ mod sys; mod time; mod toml; mod type_class; +mod type_kinds; mod uint8array; mod yaml; diff --git a/baml_language/crates/bex_vm/src/package_baml/resolve.rs b/baml_language/crates/bex_vm/src/package_baml/resolve.rs index f952c920e7a..b7bd842a918 100644 --- a/baml_language/crates/bex_vm/src/package_baml/resolve.rs +++ b/baml_language/crates/bex_vm/src/package_baml/resolve.rs @@ -16,6 +16,7 @@ use std::borrow::Cow; use baml_type::{ Literal, MediaKind, Name, RealizedTy, TyAttr, TyTemplate, TypeName, normalize::TypeContext, + type_kind::is_type_kind_class, }; use bex_vm_types::{ errors::VmInternalError, @@ -338,6 +339,15 @@ impl<'vm> ImplResolver<'vm> { concrete: &RealizedTy, bindings: &mut [Option], ) -> bool { + // Reflection kind classes are the sealed runtime refinements of `type`. + // Keep `implement I for type` rules applicable when the dynamic receiver + // is one of those refinements (notably TypeValue's tostring override). + if matches!(pattern, TyTemplate::Type { .. }) + && matches!(concrete, RealizedTy::Class(name, _, _) if is_type_kind_class(name)) + { + return true; + } + // A fully-realized pattern carries no frame refs or holes: compare it to the // concrete type semantically (union-order-insensitive, matching the type // checker) through the canonical fact-opaque `StructuralEquivCtx`. The diff --git a/baml_language/crates/bex_vm/src/package_baml/type_class.rs b/baml_language/crates/bex_vm/src/package_baml/type_class.rs index 99a71c6462c..a8365b6a1df 100644 --- a/baml_language/crates/bex_vm/src/package_baml/type_class.rs +++ b/baml_language/crates/bex_vm/src/package_baml/type_class.rs @@ -22,6 +22,46 @@ impl BamlNamespaceType for PackageBamlImpl { } impl BamlClassTypeValue for PackageBamlImpl { + fn kind(_vm: &BexVm, self_value: &Value) -> Value { + *self_value + } + + fn as_class(vm: &BexVm, self_value: &Value) -> Option { + as_kind(vm, *self_value, baml_type::type_kind::TypeKind::Class) + } + + fn as_enum(vm: &BexVm, self_value: &Value) -> Option { + as_kind(vm, *self_value, baml_type::type_kind::TypeKind::Enum) + } + + fn as_union(vm: &BexVm, self_value: &Value) -> Option { + as_kind(vm, *self_value, baml_type::type_kind::TypeKind::Union) + } + + fn as_literal(vm: &BexVm, self_value: &Value) -> Option { + as_kind(vm, *self_value, baml_type::type_kind::TypeKind::Literal) + } + + fn as_array(vm: &BexVm, self_value: &Value) -> Option { + as_kind(vm, *self_value, baml_type::type_kind::TypeKind::Array) + } + + fn as_map(vm: &BexVm, self_value: &Value) -> Option { + as_kind(vm, *self_value, baml_type::type_kind::TypeKind::Map) + } + + fn as_interface(vm: &BexVm, self_value: &Value) -> Option { + as_kind(vm, *self_value, baml_type::type_kind::TypeKind::Interface) + } + + fn as_primitive(vm: &BexVm, self_value: &Value) -> Option { + as_kind(vm, *self_value, baml_type::type_kind::TypeKind::Primitive) + } + + fn as_function(vm: &BexVm, self_value: &Value) -> Option { + as_kind(vm, *self_value, baml_type::type_kind::TypeKind::Function) + } + /// Returns the `RealizedTy`'s display name. Includes namespaces and (for /// non-`user` packages) the package prefix, so two distinct types never /// collide on this string — package names are unique within a workspace, @@ -118,13 +158,18 @@ impl BamlClassTypeValue for PackageBamlImpl { /// The concrete `RealizedTy` wrapped by a `type` value (class, enum, interface, /// primitive, container, …), or `None` if `value` isn't a `type`. -fn type_value_ty(vm: &BexVm, value: Value) -> Option { +pub(super) fn type_value_ty(vm: &BexVm, value: Value) -> Option { match vm.get_object(value.as_object_ptr()?) { Object::Type(type_value) => Some(type_value.ty.clone()), _ => None, } } +fn as_kind(vm: &BexVm, value: Value, expected: baml_type::type_kind::TypeKind) -> Option { + let ty = type_value_ty(vm, value)?; + (baml_type::type_kind::classify_type(&ty) == expected).then_some(value) +} + /// A realized interface instantiation as reflected off a value: the type's /// qualified name, its realized generic arguments, and its associated bindings. type RealizedTypeInstantiation = ( diff --git a/baml_language/crates/bex_vm/src/package_baml/type_kinds.rs b/baml_language/crates/bex_vm/src/package_baml/type_kinds.rs new file mode 100644 index 00000000000..f9f17ef0513 --- /dev/null +++ b/baml_language/crates/bex_vm/src/package_baml/type_kinds.rs @@ -0,0 +1,268 @@ +//! BEP-066 reflection kind views over the existing minted `Object::Type`. + +use bex_heap::TlabHolder; +use bex_vm_types::types::{Object, Value}; +use indexmap::IndexMap; + +use super::{ + BamlClassReflectArrayType, BamlClassReflectClassType, BamlClassReflectEnumType, + BamlClassReflectFunctionType, BamlClassReflectInterfaceType, BamlClassReflectLiteralType, + BamlClassReflectMapType, BamlClassReflectPrimitiveType, BamlClassReflectUnionType, + BamlClassTypeValue, BamlNamespaceReflectArray, BamlNamespaceReflectClass, + BamlNamespaceReflectEnum, BamlNamespaceReflectFunction, BamlNamespaceReflectInterface, + BamlNamespaceReflectLiteral, BamlNamespaceReflectMap, BamlNamespaceReflectPrimitive, + BamlNamespaceReflectUnion, PackageBamlImpl, copy, +}; +use crate::BexVm; + +impl BamlNamespaceReflectArray for PackageBamlImpl {} +impl BamlNamespaceReflectClass for PackageBamlImpl {} +impl BamlNamespaceReflectEnum for PackageBamlImpl {} +impl BamlNamespaceReflectFunction for PackageBamlImpl {} +impl BamlNamespaceReflectInterface for PackageBamlImpl {} +impl BamlNamespaceReflectLiteral for PackageBamlImpl {} +impl BamlNamespaceReflectMap for PackageBamlImpl {} +impl BamlNamespaceReflectPrimitive for PackageBamlImpl {} +impl BamlNamespaceReflectUnion for PackageBamlImpl {} + +fn reflected_ty(vm: &BexVm, value: Value) -> baml_type::RealizedTy { + super::type_class::type_value_ty(vm, value) + .unwrap_or_else(|| unreachable!("kind method receiver must be Object::Type")) +} + +fn reflected_class(vm: &BexVm, value: Value) -> (bex_vm_types::Class, Vec) { + let baml_type::RealizedTy::Class(name, args, _) = reflected_ty(vm, value) else { + unreachable!("class.Type receiver must wrap a class type") + }; + let ptr = vm + .lookup_type(&name) + .unwrap_or_else(|| unreachable!("reflected class {name} must be loaded")); + let Object::Class(class) = vm.get_object(ptr) else { + unreachable!("reflected class name resolved to a non-class") + }; + ((**class).clone(), args) +} + +fn reflected_enum(vm: &BexVm, value: Value) -> bex_vm_types::Enum { + let baml_type::RealizedTy::Enum(name, _) = reflected_ty(vm, value) else { + unreachable!("enum.Type receiver must wrap an enum type") + }; + let ptr = vm + .lookup_type(&name) + .unwrap_or_else(|| unreachable!("reflected enum {name} must be loaded")); + let Object::Enum(enm) = vm.get_object(ptr) else { + unreachable!("reflected enum name resolved to a non-enum") + }; + (**enm).clone() +} + +fn opt_string(vm: &mut BexVm, value: Option<&str>) -> Value { + value.map_or(Value::NULL, |s| Value::object(vm.alloc_string(s))) +} + +fn alloc_meta( + vm: &mut BexVm, + alias: Option<&str>, + description: Option<&str>, + docstring: Option<&str>, + other: &IndexMap, +) -> Value { + let mut entries = IndexMap::with_capacity(other.len()); + for (key, value) in other { + entries.insert( + bex_str::BexStr::from(key.as_str()), + Value::object(vm.alloc_string(value.as_str())), + ); + } + let other = Value::object(vm.alloc_map( + baml_type::RealizedTy::string(), + baml_type::RealizedTy::string(), + entries, + )); + let alias = opt_string(vm, alias); + let description = opt_string(vm, description); + let docstring = opt_string(vm, docstring); + copy::reflect::Meta { + alias, + description, + docstring, + other, + } + .to_value(vm) +} + +macro_rules! impl_as_type { + ($trait_name:ident) => { + fn as_type(_vm: &BexVm, r#type: &Value) -> Value { + *r#type + } + }; +} + +impl BamlClassReflectClassType for PackageBamlImpl { + impl_as_type!(BamlClassReflectClassType); + + fn fields(vm: &mut BexVm, r#type: &Value) -> Vec { + let (class, args) = reflected_class(vm, *r#type); + class + .fields + .iter() + .map(|field| { + let name = Value::object(vm.alloc_string(field.name.as_str())); + let ty = field + .field_template + .substitute(&args, vm) + .unwrap_or_else(|err| { + unreachable!("emitted class field template must realize: {err}") + }); + let r#type = Value::object(vm.alloc_static_type(ty)); + let meta = alloc_meta( + vm, + field.alias.as_deref(), + field.description.as_deref(), + field.docstring.as_deref(), + &field.other, + ); + copy::reflect::class::Field { name, r#type, meta }.to_value(vm) + }) + .collect() + } + + fn meta(vm: &mut BexVm, r#type: &Value) -> Value { + let (class, _) = reflected_class(vm, *r#type); + alloc_meta( + vm, + class.alias.as_deref(), + class.description.as_deref(), + class.docstring.as_deref(), + &class.other, + ) + } +} + +impl BamlClassReflectEnumType for PackageBamlImpl { + impl_as_type!(BamlClassReflectEnumType); + + fn values(vm: &mut BexVm, r#type: &Value) -> Vec { + let enm = reflected_enum(vm, *r#type); + enm.variants + .iter() + .map(|variant| { + let name = Value::object(vm.alloc_string(variant.name.as_str())); + let meta = alloc_meta( + vm, + variant.alias.as_deref(), + variant.description.as_deref(), + variant.docstring.as_deref(), + &variant.other, + ); + copy::reflect::r#enum::Value { name, meta }.to_value(vm) + }) + .collect() + } + + fn meta(vm: &mut BexVm, r#type: &Value) -> Value { + let enm = reflected_enum(vm, *r#type); + alloc_meta( + vm, + enm.alias.as_deref(), + enm.description.as_deref(), + enm.docstring.as_deref(), + &enm.other, + ) + } +} + +impl BamlClassReflectUnionType for PackageBamlImpl { + impl_as_type!(BamlClassReflectUnionType); + + fn member_types(vm: &mut BexVm, r#type: &Value) -> Vec { + let baml_type::RealizedTy::Union(members, _) = reflected_ty(vm, *r#type) else { + unreachable!("union.Type receiver must wrap a union type") + }; + members + .into_iter() + .map(|ty| Value::object(vm.alloc_static_type(ty))) + .collect() + } +} + +impl BamlClassReflectArrayType for PackageBamlImpl { + impl_as_type!(BamlClassReflectArrayType); + + fn element_type(vm: &mut BexVm, r#type: &Value) -> Value { + let baml_type::RealizedTy::List(element, _) = reflected_ty(vm, *r#type) else { + unreachable!("array.Type receiver must wrap an array type") + }; + Value::object(vm.alloc_static_type(*element)) + } +} + +impl BamlClassReflectMapType for PackageBamlImpl { + impl_as_type!(BamlClassReflectMapType); + + fn key_type(vm: &mut BexVm, r#type: &Value) -> Value { + let baml_type::RealizedTy::Map { key, .. } = reflected_ty(vm, *r#type) else { + unreachable!("map.Type receiver must wrap a map type") + }; + Value::object(vm.alloc_static_type(*key)) + } + + fn value_type(vm: &mut BexVm, r#type: &Value) -> Value { + let baml_type::RealizedTy::Map { value, .. } = reflected_ty(vm, *r#type) else { + unreachable!("map.Type receiver must wrap a map type") + }; + Value::object(vm.alloc_static_type(*value)) + } +} + +impl BamlClassReflectFunctionType for PackageBamlImpl { + impl_as_type!(BamlClassReflectFunctionType); + + fn params(vm: &mut BexVm, r#type: &Value) -> Vec { + let baml_type::RealizedTy::Function { params, .. } = reflected_ty(vm, *r#type) else { + unreachable!("function.Type receiver must wrap a function type") + }; + params + .into_iter() + .map(|param| { + let name = opt_string(vm, param.name.as_ref().map(baml_type::Name::as_str)); + let optional = param.is_optional(); + let r#type = Value::object(vm.alloc_static_type(param.ty)); + copy::reflect::function::Parameter { + name, + r#type, + optional, + } + .to_value(vm) + }) + .collect() + } + + fn return_type(vm: &mut BexVm, r#type: &Value) -> Value { + let baml_type::RealizedTy::Function { ret, .. } = reflected_ty(vm, *r#type) else { + unreachable!("function.Type receiver must wrap a function type") + }; + Value::object(vm.alloc_static_type(*ret)) + } +} + +impl BamlClassReflectInterfaceType for PackageBamlImpl { + impl_as_type!(BamlClassReflectInterfaceType); + + fn implemented_by(vm: &BexVm, r#type: &Value, other: &Value) -> bool { + ::implemented_by(vm, r#type, other) + } + + fn implementors(vm: &mut BexVm, r#type: &Value) -> Vec { + ::implementors(vm, r#type) + } +} + +impl BamlClassReflectLiteralType for PackageBamlImpl { + impl_as_type!(BamlClassReflectLiteralType); +} + +impl BamlClassReflectPrimitiveType for PackageBamlImpl { + impl_as_type!(BamlClassReflectPrimitiveType); +} diff --git a/baml_language/crates/bex_vm/src/vm.rs b/baml_language/crates/bex_vm/src/vm.rs index 5d894f830f4..5f0f342892e 100644 --- a/baml_language/crates/bex_vm/src/vm.rs +++ b/baml_language/crates/bex_vm/src/vm.rs @@ -2089,11 +2089,11 @@ impl BexVm { ObjectType::of(other) ), }, - // A `type` value (e.g. `type.of()`) — its concrete type is - // the `type` primitive, the subject of `implement I for type`. - Object::Type(_) => ConcreteRealizedTy::Type { - attr: TyAttr::default(), - }, + // A `type` value reports its precise sealed reflection-kind class. + // Each kind class is a subtype of the `type` carrier. + Object::Type(type_value) => { + baml_type::type_kind::classify_type(&type_value.ty).concrete_class_ty() + } // Arrays/maps carry their element/key/value types, so the faithful // `list` / `map` is reconstructed from the value itself. Object::Array(arr) => { diff --git a/baml_language/crates/bex_vm/tests/method_class_type_args.rs b/baml_language/crates/bex_vm/tests/method_class_type_args.rs index b6d3b6bcb2a..346332f0a1b 100644 --- a/baml_language/crates/bex_vm/tests/method_class_type_args.rs +++ b/baml_language/crates/bex_vm/tests/method_class_type_args.rs @@ -111,6 +111,8 @@ fn alloc_instance_ntypeargs_stores_class_type_args() { fields: vec![], description: None, alias: None, + docstring: None, + other: indexmap::IndexMap::new(), type_tag: 100, ty_attr: TyAttr::default(), has_cleanup: false, @@ -163,6 +165,8 @@ fn alloc_instance_ntypeargs_zero_gives_empty_class_type_args() { fields: vec![], description: None, alias: None, + docstring: None, + other: indexmap::IndexMap::new(), type_tag: 101, ty_attr: TyAttr::default(), has_cleanup: false, diff --git a/baml_language/crates/bex_vm_types/src/link.rs b/baml_language/crates/bex_vm_types/src/link.rs index fb5676e334e..0ca438681f1 100644 --- a/baml_language/crates/bex_vm_types/src/link.rs +++ b/baml_language/crates/bex_vm_types/src/link.rs @@ -853,6 +853,8 @@ mod tests { fields: Vec::new(), description: None, alias: None, + docstring: None, + other: indexmap::IndexMap::new(), type_tag, ty_attr: baml_type::TyAttr::default(), has_cleanup: false, diff --git a/baml_language/crates/bex_vm_types/src/types/class.rs b/baml_language/crates/bex_vm_types/src/types/class.rs index 2853a3ec297..02d5a87477f 100644 --- a/baml_language/crates/bex_vm_types/src/types/class.rs +++ b/baml_language/crates/bex_vm_types/src/types/class.rs @@ -1,5 +1,6 @@ use baml_type::RuntimeTy; use borsh::{BorshDeserialize, BorshSerialize}; +use indexmap::IndexMap; use crate::{AtomicValueSlot, CleanupLatch, HeapPtr, Value}; @@ -20,6 +21,8 @@ pub struct ClassField { pub field_template: baml_type::TyTemplate, pub description: Option, pub alias: Option, + pub docstring: Option, + pub other: IndexMap, pub skip: bool, } @@ -39,6 +42,10 @@ pub struct Class { /// Class-level serialization alias. pub alias: Option, + /// Class-level source documentation and custom annotations. + pub docstring: Option, + pub other: IndexMap, + /// Type tag for this class, used by `TypeTag` instruction for jump table dispatch. /// Assigned during codegen as `CLASS_BASE + class_index`. pub type_tag: i64, diff --git a/baml_language/crates/bex_vm_types/src/types/enums.rs b/baml_language/crates/bex_vm_types/src/types/enums.rs index 704258149d6..0bb3a8248e7 100644 --- a/baml_language/crates/bex_vm_types/src/types/enums.rs +++ b/baml_language/crates/bex_vm_types/src/types/enums.rs @@ -1,4 +1,5 @@ use borsh::{BorshDeserialize, BorshSerialize}; +use indexmap::IndexMap; use crate::HeapPtr; @@ -8,6 +9,8 @@ pub struct EnumVariant { pub name: String, pub description: Option, pub alias: Option, + pub docstring: Option, + pub other: IndexMap, pub skip: bool, } @@ -27,6 +30,10 @@ pub struct Enum { /// Enum-level serialization alias. pub alias: Option, + /// Enum-level source documentation and custom annotations. + pub docstring: Option, + pub other: IndexMap, + /// Enum-level type attribute. pub ty_attr: baml_type::TyAttr, } diff --git a/baml_language/sdks/python/rust/sdkgen_python_pydantic2/src/lib.rs b/baml_language/sdks/python/rust/sdkgen_python_pydantic2/src/lib.rs index e7d4b6a25df..2c54fe7c7e2 100644 --- a/baml_language/sdks/python/rust/sdkgen_python_pydantic2/src/lib.rs +++ b/baml_language/sdks/python/rust/sdkgen_python_pydantic2/src/lib.rs @@ -824,6 +824,69 @@ mod tests { assert!(!leaf.contains("import enum")); } + #[test] + fn reflect_kind_namespaces_are_routed_legally_across_the_generated_surface() { + let mut pool: SymbolPool = HashMap::new(); + let kind_namespaces = [ + ("class", "class_"), + ("enum", "enum"), + ("interface", "interface"), + ("function", "function"), + ]; + + for (source, _) in kind_namespaces { + let type_name = cg_name("baml", &["reflect", source], "Type"); + pool.insert(type_name.clone(), class(type_name)); + } + + let consumer_name = cg_name("user", &["consumer"], "KindViews"); + pool.insert( + consumer_name.clone(), + Symbol::Class(Class { + generic_params: Vec::new(), + name: consumer_name, + docstring: None, + properties: kind_namespaces + .iter() + .map(|(source, _)| ClassProperty { + name: BaseName::new(format!("{source}_type")), + docstring: None, + ty: class_ty(cg_name("baml", &["reflect", source], "Type"), vec![]), + }) + .collect(), + static_methods: vec![], + instance_methods: vec![], + origin: origin("x.baml", 0), + }), + ); + + let out = to_source_code(&pool, &[], NamingConvention::PreserveCase); + let reflect_pyi = &out[&PathBuf::from("baml/reflect/__init__.pyi")]; + let consumer_py = &out[&PathBuf::from("consumer/__init__.py")]; + let consumer_pyi = &out[&PathBuf::from("consumer/__init__.pyi")]; + let typemap = &out[&PathBuf::from("_typemap.py")]; + + for (source, routed) in kind_namespaces { + assert!(out.contains_key(&PathBuf::from(format!("baml/reflect/{routed}/__init__.py")))); + if source != routed { + assert!( + !out.contains_key(&PathBuf::from(format!("baml/reflect/{source}/__init__.py"))) + ); + } + assert!(reflect_pyi.contains(&format!("from . import {routed}\n"))); + + let reference = format!("baml.reflect.{routed}.Type"); + assert!(consumer_py.contains(&reference)); + assert!(consumer_pyi.contains(&reference)); + assert!(typemap.contains(&format!( + "\"baml.reflect.{source}.Type\": (\"baml_sdk.baml.reflect.{routed}\", \"Type\")" + ))); + } + + assert!(!consumer_py.contains("baml.reflect.class.Type")); + assert!(!consumer_pyi.contains("baml.reflect.class.Type")); + } + #[test] fn enum_body_renders() { let mut pool: SymbolPool = HashMap::new(); diff --git a/baml_language/sdks/python/rust/sdkgen_python_pydantic2/src/routing.rs b/baml_language/sdks/python/rust/sdkgen_python_pydantic2/src/routing.rs index 85a7bbb8329..5bb96f121ba 100644 --- a/baml_language/sdks/python/rust/sdkgen_python_pydantic2/src/routing.rs +++ b/baml_language/sdks/python/rust/sdkgen_python_pydantic2/src/routing.rs @@ -58,27 +58,26 @@ pub(crate) fn route(name: &Name, symbol: &Symbol) -> LeafPath { route_inner(name, !matches!(symbol, Symbol::Function(_))) } +/// Python's hard keywords. A keyword cannot be used in a dotted reference or +/// relative import, so every routed occurrence receives a trailing `_`. +const PYTHON_KEYWORDS: &[&str] = &[ + "False", "None", "True", "and", "as", "assert", "async", "await", "break", "class", "continue", + "def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import", + "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while", + "with", "yield", +]; + /// Sanitize a path segment so it's a usable Python module identifier. -/// Handles `assert` (the BAML stdlib package whose name collides with -/// Python's `assert` keyword — `from . import assert` is a `SyntaxError`) -/// and `type` (the `baml.type` carrier namespace, BEP-066: a submodule -/// named `type` re-exported from `baml/__init__.pyi` shadows the builtin -/// `type` in sibling annotations — pyright's reportInvalidTypeForm -/// "Module cannot be used as a type"; BEP-066 H-1 prescribes the `type_` -/// mangling). The routed leaf becomes `…/assert_/…` / `baml/type_/…` and -/// cross-leaf references render accordingly. The runtime BAML FQN passed -/// to `_define_function` is built from `Name`, not `LeafPath`, so it is -/// *not* affected. /// -/// TODO(reserved-keywords): generalize to all Python keywords and any -/// other invalid identifiers. User packages or namespaces named after -/// keywords (`class`, `def`, `pass`, …) would hit the same issue, but -/// none exist today; broaden this set when one shows up. +/// In addition to the hard keywords above, `type` retains its existing +/// trailing-underscore spelling. The `baml.type` carrier submodule would +/// otherwise shadow the builtin `type` in sibling annotations. Runtime BAML +/// FQNs are built from `Name`, not `LeafPath`, so routing does not alter them. fn sanitize_python_module_segment(seg: &str) -> String { - match seg { - "assert" => "assert_".to_string(), - "type" => "type_".to_string(), - _ => seg.to_string(), + if seg == "type" || PYTHON_KEYWORDS.contains(&seg) { + format!("{seg}_") + } else { + seg.to_string() } } @@ -320,6 +319,29 @@ mod tests { assert_eq!(lp.segments, vec!["assert_".to_string()]); } + #[test] + fn every_python_keyword_segment_is_sanitized_in_packages_and_namespaces() { + for &keyword in PYTHON_KEYWORDS { + let expected = format!("{keyword}_"); + + let package_name = name(keyword, &[], "Thing"); + let package_leaf = route(&package_name, &class_sym(&package_name)); + assert_eq!( + package_leaf.segments, + vec!["vendor".to_string(), expected.clone()], + "package segment {keyword:?}", + ); + + let namespace_name = name("user", &[keyword], "Thing"); + let namespace_leaf = route(&namespace_name, &class_sym(&namespace_name)); + assert_eq!( + namespace_leaf.segments, + vec![expected], + "namespace segment {keyword:?}", + ); + } + } + #[test] fn type_namespace_segment_is_sanitized() { // The `baml.type` carrier namespace (BEP-066): a generated