diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1d9921348c3..f4a9dfab9df 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -346,7 +346,9 @@ jobs: run: | if git diff --quiet "${MERGE_BASE}...HEAD" -- \ ':baml_language/crates/bridge_ctypes/**/*.proto' \ + ':baml_language/crates/bridge_ctypes/build.rs' \ ':baml_language/crates/bridge_cffi/include/**' \ + ':baml_language/sdks/ruby/bridge_ruby/lib/baml_bridge/cffi/v1/**' \ ':baml_language/sdks/swift/Sources/BamlBridge/Proto/**' \ ':baml_language/sdks/swift/Sources/CBamlBridge/include/**' \ ; then @@ -865,7 +867,7 @@ jobs: save-if: false shared-key: "linux-cargo" - # bridge_ctypes/build.rs needs `protoc`; baml_go's generate.sh needs + # bridge_ctypes/build.rs uses vendored protoc; baml_go's generate.sh needs # `protoc` + `protoc-gen-go`; bridge_typescript needs node + pnpm. # The `cargo build -p bridge_ctypes` step uses cargo from rustup # (not mise), so we don't need `go` itself here either. @@ -879,9 +881,9 @@ jobs: run: cargo fetch working-directory: baml_language - # Rust (prost) + Python (protoc) — both driven by bridge_ctypes/build.rs. - # Writes Python pb2/pyi into baml_language/sdks/python/src/baml_bridge/cffi/v1/. - - name: "Generate Rust + Python proto bindings (bridge_ctypes)" + # Rust (prost), Python, and Ruby (protoc) — all driven by bridge_ctypes/build.rs. + # Writes Python pb2/pyi and committed Ruby clients into their SDK trees. + - name: "Generate Rust + Python + Ruby proto bindings (bridge_ctypes)" run: cargo build -p bridge_ctypes working-directory: baml_language @@ -934,6 +936,7 @@ jobs: baml_language/crates/bridge_ctypes baml_language/sdks/go/baml_go/internal/cffi baml_language/sdks/python/src/baml_bridge/cffi + baml_language/sdks/ruby/bridge_ruby/lib/baml_bridge/cffi/v1 baml_language/sdks/rust/bridge_rust/src/wire baml_language/sdks/swift/Sources/BamlBridge/Proto baml_language/sdks/swift/Sources/CBamlBridge/include diff --git a/baml_language/Cargo.lock b/baml_language/Cargo.lock index 9fcfb72c116..61c8efdc37f 100644 --- a/baml_language/Cargo.lock +++ b/baml_language/Cargo.lock @@ -495,6 +495,7 @@ dependencies = [ "sdkgen_go", "sdkgen_java", "sdkgen_python_pydantic2", + "sdkgen_ruby_sorbet", "sdkgen_rust", "sdkgen_swift", "sdkgen_typescript_shared", @@ -5877,6 +5878,7 @@ dependencies = [ "sdkgen_go", "sdkgen_java", "sdkgen_python_pydantic2", + "sdkgen_ruby_sorbet", "sdkgen_rust", "sdkgen_swift", "sdkgen_typescript_shared", @@ -6027,6 +6029,15 @@ dependencies = [ "pretty_assertions", ] +[[package]] +name = "sdkgen_ruby_sorbet" +version = "0.0.0-beta" +dependencies = [ + "baml_base", + "baml_sdkgen_types", + "baml_type", +] + [[package]] name = "sdkgen_rust" version = "0.0.0-beta" diff --git a/baml_language/Cargo.toml b/baml_language/Cargo.toml index a78a69b0c0a..15c5df7cef2 100644 --- a/baml_language/Cargo.toml +++ b/baml_language/Cargo.toml @@ -5,6 +5,7 @@ members = [ "sdks/cpp/sdkgen_cpp", "sdks/csharp/sdkgen_csharp", "sdks/go/sdkgen_go", + "sdks/ruby/sdkgen_ruby_sorbet", "sdks/java/bridge_java", "sdks/java/sdkgen_java", "sdks/typescript/bridge_typescript", @@ -91,6 +92,7 @@ sdkgen_java = { path = "sdks/java/sdkgen_java" } bridge_java = { path = "sdks/java/bridge_java" } sdkgen_swift = { path = "sdks/swift/rust/sdkgen_swift" } sdkgen_go = { path = "sdks/go/sdkgen_go" } +sdkgen_ruby_sorbet = { path = "sdks/ruby/sdkgen_ruby_sorbet" } sdkgen_rust = { path = "sdks/rust/sdkgen_rust" } baml_sdkgen_types = { path = "crates/baml_sdkgen_types" } bridge_encode = { path = "crates/bridge_encode" } diff --git a/baml_language/crates/baml_cli/Cargo.toml b/baml_language/crates/baml_cli/Cargo.toml index ae857b9a825..1fdd5cb4d34 100644 --- a/baml_language/crates/baml_cli/Cargo.toml +++ b/baml_language/crates/baml_cli/Cargo.toml @@ -44,6 +44,7 @@ sdkgen_csharp = { workspace = true } sdkgen_go = { workspace = true } sdkgen_java = { workspace = true } sdkgen_python_pydantic2 = { workspace = true } +sdkgen_ruby_sorbet = { workspace = true } sdkgen_rust = { workspace = true } sdkgen_swift = { workspace = true } sdkgen_typescript_shared = { workspace = true } diff --git a/baml_language/crates/baml_cli/src/generate.rs b/baml_language/crates/baml_cli/src/generate.rs index 10ad2b3cdf7..769480adf0f 100644 --- a/baml_language/crates/baml_cli/src/generate.rs +++ b/baml_language/crates/baml_cli/src/generate.rs @@ -604,6 +604,20 @@ impl GenerateArgs { .map(|(path, content)| (path, content.into_bytes())) .collect() } + OutputType::RubySorbet => { + let (files, warnings) = + sdkgen_ruby_sorbet::to_source_code_with_bytecode_and_warnings( + &pool, + &baml_bytecode, + generator.naming_convention, + Some(&embedded_baml_toml), + ); + report_skipped(&reporter, "Ruby", &warnings, self.show_all_skips); + files + .into_iter() + .map(|(path, content)| (path, content.into_bytes())) + .collect() + } OutputType::Java => sdkgen_java::to_source_code_with_bytecode_and_metadata( &pool, &baml_bytecode, @@ -769,7 +783,7 @@ fn discover_generators(root: &Path) -> (Vec, Vec) { name, "output_type", generator.output_type.as_ref(), - r#"one of: "python/pydantic", "python/pydantic/v1", "typescript/node", "typescript/web", "swift", "go", "rust", "java", "cpp", "csharp""#, + r#"one of: "python/pydantic", "python/pydantic/v1", "typescript/node", "typescript/web", "swift", "go", "rust", "java", "ruby/sorbet", "cpp", "csharp""#, table_range, &mut diags, ); diff --git a/baml_language/crates/baml_sdkgen_types/src/generator_fields.rs b/baml_language/crates/baml_sdkgen_types/src/generator_fields.rs index 13fcd366906..7880819e2c7 100644 --- a/baml_language/crates/baml_sdkgen_types/src/generator_fields.rs +++ b/baml_language/crates/baml_sdkgen_types/src/generator_fields.rs @@ -32,6 +32,9 @@ pub enum OutputType { /// Java SDK (`com.boundaryml:baml-bridge` runtime). #[strum(serialize = "java")] Java, + /// Ruby SDK with Sorbet types (`baml-bridge` gem runtime). + #[strum(serialize = "ruby/sorbet")] + RubySorbet, /// C++17 SDK (self-contained source tree; dlopens the shared runtime). #[strum(serialize = "cpp")] Cpp, @@ -57,6 +60,7 @@ impl OutputType { Self::Go => "go", Self::Rust => "rust", Self::Java => "java", + Self::RubySorbet => "ruby/sorbet", Self::Cpp => "cpp", Self::CSharp => "csharp", } @@ -70,7 +74,9 @@ impl OutputType { /// The naming conventions this generator implements. pub const fn naming_support(self) -> NamingSupport { match self { - Self::Go | Self::CSharp => NamingSupport::Only(NamingConvention::Language), + Self::Go | Self::CSharp | Self::RubySorbet => { + NamingSupport::Only(NamingConvention::Language) + } Self::Rust | Self::PythonPydantic | Self::PythonPydanticV1 => NamingSupport::Either { default: NamingConvention::Language, }, @@ -167,6 +173,7 @@ mod tests { for output_type in [ OutputType::Go, OutputType::CSharp, + OutputType::RubySorbet, OutputType::Rust, OutputType::PythonPydantic, OutputType::PythonPydanticV1, diff --git a/baml_language/crates/bridge_ctypes/README.md b/baml_language/crates/bridge_ctypes/README.md index 47875e842a8..0fcdaea1959 100644 --- a/baml_language/crates/bridge_ctypes/README.md +++ b/baml_language/crates/bridge_ctypes/README.md @@ -9,6 +9,10 @@ cargo build -p bridge_ctypes # -> sdks/rust/bridge_rust/src/wire/baml_bridge.cffi.v1.rs (committed) # -> sdks/python/src/baml_bridge/cffi/v1/*_pb2.py(i) +# Ruby (protoc) — also driven by bridge_ctypes/build.rs with vendored protoc. +# The generated clients are COMMITTED so consumers do not need protoc. +# -> sdks/ruby/bridge_ruby/lib/baml_bridge/cffi/v1/*_pb.rb + # Node / TypeScript (protobufjs + napi loader) cd sdks/typescript/bridge_typescript && pnpm build:debug # -> sdks/typescript/bridge_typescript/typescript_src/proto/baml_cffi.{js,d.ts} @@ -42,4 +46,4 @@ sdks/swift/scripts/generate-protos.sh Other consumers (`bridge_cffi`, `bridge_wasm`, `sdks/python/rust/bridge_python`) use the Rust prost types via `bridge_ctypes` — nothing extra to regenerate. `sdks/rust/bridge_rust` is the exception: it vendors the generated file (see above) because it publishes to crates.io and must not depend on this engine-coupled crate. -No generated clients exist for Ruby, Java/Kotlin, C#/.NET, or PHP. The Java and C# bridges read and write the wire by hand (`sdks/java/baml_bridge` `ProtoReader`, `sdks/csharp/bridge_csharp` `BamlGeneratedCanonicalCodecs.cs`, and the C# generator's metadata encoder), as does the engine's trace encoder (`crates/bex_engine/src/trace_value_encode.rs`): a change to a message's fields is mirrored there by hand. +No generated clients exist for Java/Kotlin, C#/.NET, or PHP. The Java and C# bridges read and write the wire by hand (`sdks/java/baml_bridge` `ProtoReader`, `sdks/csharp/bridge_csharp` `BamlGeneratedCanonicalCodecs.cs`, and the C# generator's metadata encoder), as does the engine's trace encoder (`crates/bex_engine/src/trace_value_encode.rs`): a change to a message's fields is mirrored there by hand. diff --git a/baml_language/crates/bridge_ctypes/build.rs b/baml_language/crates/bridge_ctypes/build.rs index de416fd8ab0..13dfcc0f4db 100644 --- a/baml_language/crates/bridge_ctypes/build.rs +++ b/baml_language/crates/bridge_ctypes/build.rs @@ -1,7 +1,10 @@ use std::path::PathBuf; +mod build_support; + fn main() -> std::io::Result<()> { println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=build_support.rs"); let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); let proto_dir = manifest_dir.join("types"); @@ -72,6 +75,23 @@ fn main() -> std::io::Result<()> { let status = cmd.status().expect("failed to run protoc"); assert!(status.success(), "protoc (python) failed with {status}"); + // Generate Ruby clients into the bridge's load path. These files are + // committed so Ruby consumers do not need protoc at runtime. Remove the + // generated package first so deleted or renamed schemas cannot leave stale + // clients behind. + let ruby_out = manifest_dir.join("../../sdks/ruby/bridge_ruby/lib"); + let ruby_package_out = ruby_out.join("baml_bridge/cffi/v1"); + build_support::allow_missing(std::fs::remove_dir_all(&ruby_package_out))?; + std::fs::create_dir_all(&ruby_out)?; + let mut cmd = std::process::Command::new(&protoc); + cmd.arg(format!("--proto_path={}", proto_dir.display())); + cmd.arg(format!("--ruby_out={}", ruby_out.display())); + for proto in &protos { + cmd.arg(proto); + } + let status = cmd.status().expect("failed to run protoc"); + assert!(status.success(), "protoc (Ruby) failed with {status}"); + Ok(()) } diff --git a/baml_language/crates/bridge_ctypes/build_support.rs b/baml_language/crates/bridge_ctypes/build_support.rs new file mode 100644 index 00000000000..7462d0cafeb --- /dev/null +++ b/baml_language/crates/bridge_ctypes/build_support.rs @@ -0,0 +1,25 @@ +pub fn allow_missing(result: std::io::Result<()>) -> std::io::Result<()> { + match result { + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), + result => result, + } +} + +#[cfg(test)] +mod tests { + use super::allow_missing; + use std::io::{Error, ErrorKind}; + + #[test] + fn cleanup_tolerates_missing_directories_but_preserves_other_errors() { + assert!(allow_missing(Ok(())).is_ok()); + assert!(allow_missing(Err(Error::from(ErrorKind::NotFound))).is_ok()); + let error = allow_missing(Err(Error::new( + ErrorKind::PermissionDenied, + "cleanup refused", + ))) + .unwrap_err(); + assert_eq!(error.kind(), ErrorKind::PermissionDenied); + assert_eq!(error.to_string(), "cleanup refused"); + } +} diff --git a/baml_language/crates/bridge_ctypes/src/lib.rs b/baml_language/crates/bridge_ctypes/src/lib.rs index fa527c215b8..8f940fe3615 100644 --- a/baml_language/crates/bridge_ctypes/src/lib.rs +++ b/baml_language/crates/bridge_ctypes/src/lib.rs @@ -4,6 +4,10 @@ //! between host values and `BexExternalValue`, used by both `bridge_cffi` //! and `bridge_wasm`. +#[cfg(test)] +#[path = "../build_support.rs"] +mod build_support; + mod error; mod handle_table; mod wire_depth; diff --git a/baml_language/sdk_tests/codegen/Cargo.toml b/baml_language/sdk_tests/codegen/Cargo.toml index 388de401b31..efbb35b6226 100644 --- a/baml_language/sdk_tests/codegen/Cargo.toml +++ b/baml_language/sdk_tests/codegen/Cargo.toml @@ -47,6 +47,7 @@ sdkgen_go = { workspace = true } sdkgen_typescript_shared = { workspace = true } sdkgen_java = { workspace = true } sdkgen_rust = { workspace = true } +sdkgen_ruby_sorbet = { workspace = true } baml_sdkgen_types = { workspace = true } baml_base = { workspace = true } baml_db = { workspace = true } diff --git a/baml_language/sdk_tests/codegen/src/lib.rs b/baml_language/sdk_tests/codegen/src/lib.rs index 9ea261767b3..8bf22057ba6 100644 --- a/baml_language/sdk_tests/codegen/src/lib.rs +++ b/baml_language/sdk_tests/codegen/src/lib.rs @@ -47,6 +47,7 @@ pub mod csharp; pub mod go; pub mod java; pub mod python_pydantic2; +pub mod ruby_sorbet; pub mod rust; pub mod swift; pub mod typescript; diff --git a/baml_language/sdk_tests/codegen/src/main.rs b/baml_language/sdk_tests/codegen/src/main.rs index 38c7708c60d..f8d18980f6d 100644 --- a/baml_language/sdk_tests/codegen/src/main.rs +++ b/baml_language/sdk_tests/codegen/src/main.rs @@ -31,6 +31,7 @@ const GENERATORS: &[(&str, RunAll)] = &[ "python_pydantic2", sdk_test_codegen::python_pydantic2::run_all, ), + ("ruby_sorbet", sdk_test_codegen::ruby_sorbet::run_all), ("rust", sdk_test_codegen::rust::run_all), ("swift", sdk_test_codegen::swift::run_all), ("typescript", sdk_test_codegen::typescript::run_all), diff --git a/baml_language/sdk_tests/codegen/src/ruby_sorbet.rs b/baml_language/sdk_tests/codegen/src/ruby_sorbet.rs new file mode 100644 index 00000000000..aba1109143f --- /dev/null +++ b/baml_language/sdk_tests/codegen/src/ruby_sorbet.rs @@ -0,0 +1,29 @@ +//! Stage the Ruby SDK fixtures. +use std::fs; + +use baml_sdkgen_types::NamingConvention; +use sdk_test_harness_runner::fixtures; + +use crate::{CodegenCtx, Overlay, load_fixture, write_codegen_output}; + +pub fn run_all(ctx: &CodegenCtx) { + for fixture in fixtures::checked_shared(&ctx.fixtures_root) { + let loaded = load_fixture(&ctx.fixtures_root, fixture); + let (files, skipped) = sdkgen_ruby_sorbet::to_source_code_with_bytecode_and_skipped( + &loaded.pool, + &loaded.baml_bytecode, + NamingConvention::Language, + None, + ); + eprintln!("Ruby {fixture} skipped: {}", skipped.join(", ")); + let root = ctx.crate_dir.join(fixture); + let generated = root.join("generated"); + if generated.exists() { + fs::remove_dir_all(&generated).unwrap(); + } + write_codegen_output(&generated, files, fixture); + let mut overlay = Overlay::new(&root); + overlay.copy_tree(&root.join("customizable"), ""); + overlay.install(); + } +} diff --git a/baml_language/sdk_tests/crates/parity_analysis.md b/baml_language/sdk_tests/crates/parity_analysis.md index 937b1f76a2f..60a623e861a 100644 --- a/baml_language/sdk_tests/crates/parity_analysis.md +++ b/baml_language/sdk_tests/crates/parity_analysis.md @@ -2,9 +2,9 @@ This report inventories checked-in test declarations. It does not report whether tests passed. -Distinct exact test IDs: 981. IDs with complete required parity: 232. Required gaps: 5143. +Distinct exact test IDs: 988. IDs with complete required parity: 240. Required gaps: 5587. -Baseline ratchet: BETTER. Required gaps: 5143 (baseline: 5143). Present declarations: 2610 (baseline: 2608). Newly missing required pairs: 0. Resolved baseline gaps: 0. Weakened requirements: 0. +Baseline ratchet: UNCHANGED. Required gaps: 5587 (baseline: 5587). Present declarations: 2924 (baseline: 2924). Newly missing required pairs: 0. Resolved baseline gaps: 0. Weakened requirements: 0. ## Python-baselined parity @@ -22,992 +22,999 @@ Parity is the share of the 457 test IDs declared in `python_pydantic2` that are | go | 21 / 457 | 4.6% | | java | 307 / 457 | 67.2% | | swift | 191 / 457 | 41.8% | +| ruby_sorbet | 307 / 457 | 67.2% | -| Test case | python_pydantic2 | typescript_node | typescript_web_chromium | typescript_web_cloudflare_workers | cpp | csharp | rust | go | java | swift | Required in | Reason | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| docstrings_etc/class_doc_summary_and_attributes | - | - | - | - | - | - | - | y | - | - | all | | -| docstrings_etc/enum_doc_summary_and_members | - | - | - | - | - | - | - | y | - | - | all | | -| docstrings_etc/enum_summary_only_omits_member_comments | - | - | - | - | - | - | - | y | - | - | all | | -| docstrings_etc/go_codegen_function_doc_comment | - | - | - | - | - | - | - | y | - | - | all | | -| docstrings_etc/imports | - | - | - | - | - | - | - | y | - | - | all | | -| docstrings_etc/main_class_doc_summary_and_attributes_section | y | y | - | - | y | - | y | - | y | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| docstrings_etc/main_class_doc_summary_present | - | - | - | - | - | - | - | - | - | y | all | | -| docstrings_etc/main_enum_and_variant_docs_attached | - | - | - | - | - | - | - | - | - | y | all | | -| docstrings_etc/main_enum_doc_summary_and_members_section | y | y | - | - | y | - | y | - | y | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| docstrings_etc/main_enum_summary_only_omits_members_section | y | - | - | - | y | - | y | - | y | - | all | | -| docstrings_etc/main_field_docs_attached | - | - | - | - | - | - | - | - | - | y | all | | -| docstrings_etc/main_imports_symbols_reachable | y | y | y | y | - | - | y | - | y | y | all | | -| docstrings_etc/main_multi_line_class_doc_preserved | - | - | - | - | - | - | - | - | - | y | all | | -| docstrings_etc/main_no_inline_field_or_variant_doc_artifacts | y | y | - | - | y | - | y | - | y | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| docstrings_etc/main_undocumented_field_listed_as_bare_name_under_attributes | y | - | - | - | y | - | y | - | y | - | all | | -| docstrings_etc/no_inline_field_or_variant_doc_artifacts | - | - | - | - | - | - | - | y | - | - | all | | -| docstrings_etc/undocumented_field_has_no_doc_artifact | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/ambient_context_survives_await_rust_only | - | - | - | - | - | - | y | - | - | - | rust | Rust task scopes and lazy future cancellation are specific to Rust | -| function_calls/async_callback_can_use_originating_loop_resources_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | -| function_calls/async_callback_can_use_originating_loop_resources_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/async_callback_inherits_application_context_across_suspension_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | -| function_calls/async_callback_preserves_ambient_frame_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node async carrier, native signals, and entry-time snapshots are specific to TypeScript | -| function_calls/async_callback_reenters_async_baml_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python loop/ContextVar and Node AsyncLocalStorage assertions | -| function_calls/async_callback_reenters_sync_baml_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | sync callback re-entry differs between Python and Node | -| function_calls/async_entry_sync_callback_inherits_application_context_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | -| function_calls/async_host_runs_in_calling_task_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | covers Python asyncio task and event-loop identity | -| function_calls/async_reentry_preserves_async_local_storage_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/async_stream_schema_failure_is_parse_failed | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | exercises the Python asyncio streaming bridge | -| function_calls/baml_closure_decodes_multiple_args_and_structured_return_values | y | y | y | y | y | - | y | y | y | y | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, cpp, rust, go, java, swift | C# covers this canonical behavior in its native integration harness | -| function_calls/baml_closure_is_a_native_callable_with_host_language_arguments | y | y | y | y | y | - | y | y | y | y | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, cpp, rust, go, java, swift | C# covers this canonical behavior in its native integration harness | -| function_calls/baml_closure_is_reusable_and_retains_mutable_captures | y | y | y | y | y | - | y | y | y | y | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, cpp, rust, go, java, swift | C# covers this canonical behavior in its native integration harness | -| function_calls/baml_error_carries_baml_trace | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/baml_time_class_and_field_wire_names_are_exact | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/baml_time_constructors_and_parsers_return_lossless_internal_values | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/baml_time_go_constructed_values_round_trip_at_numeric_boundaries | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/baml_time_malformed_values_fail_at_the_earliest_typed_boundary | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/baml_time_nested_containers_and_baml_field_inspection | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/baml_time_nullable_and_defaulted_positions | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/baml_time_raw_class_transport_does_not_enforce_semantic_invariants | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/baml_trace_is_embedded_in_go_error_string | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/boolean_timeout_rejected_python_only | y | - | - | - | - | - | - | - | - | - | all | | -| function_calls/callable_entry_invokes_callback | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/callable_entry_waits_for_callback_completion | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/callback_bound_method_adoption_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | covers Python bound method identity via its exact SDK function | -| function_calls/callback_captures_internal_baml_context | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/callback_frame_and_reentry | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/callback_frame_is_installed_and_restored | - | - | - | - | - | - | y | - | - | - | all | | -| function_calls/callback_marker_adopts_once_during_recursion | y | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| function_calls/callback_marker_adopts_sync_body | y | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| function_calls/callback_marker_concurrent_reuse_and_later_direct_call | y | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| function_calls/callback_marker_context_precedence | y | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| function_calls/callback_marker_defaults_override_inherited_context | y | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| function_calls/callback_marker_only_outer_wrapper_adopts | y | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| function_calls/callback_marker_sync_entry_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | covers Python synchronous BAML entry and inline callback dispatch | -| function_calls/callback_reenters_baml | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/callback_reuse_across_event_loop_turns_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/callback_reuse_after_previous_application_loop_is_closed_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | -| function_calls/callback_suppresses_task_cancellation_and_returns_late_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python callback suppression of cancellation and late results | -| function_calls/callback_task_cancelled_before_first_execution_completes_call_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python task factory cancellation before callback execution | -| function_calls/callback_third_party_wrapper_is_not_adopted | y | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| function_calls/callback_throws_caught_and_replaced_makes_the_function_infallible | - | - | - | - | - | - | y | - | - | - | rust | validates Rust-specific inferred callback error unions | -| function_calls/callback_throws_caught_then_rethrown_value_is_the_replacement_union | - | - | - | - | - | - | y | - | - | - | rust | validates Rust-specific inferred callback error unions | -| function_calls/callback_throws_rethrown_carries_the_effect_param_into_the_error_union | - | - | - | - | - | - | y | - | - | - | rust | validates Rust-specific inferred callback error unions | -| function_calls/cancel_controls_with_cancelled_context_go_only | - | - | - | - | - | - | - | y | - | - | go | Go cancellation controls remain usable with a canceled context | -| function_calls/cancellation_async_call_returns_none | y | - | - | - | y | - | y | - | y | y | all | | -| function_calls/cancellation_async_cancel_skips_later_step | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | drives Python asyncio cancellation (task.cancel / wait_for) | -| function_calls/cancellation_async_cancel_via_asyncio_timeout | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/cancellation_async_cancel_via_call_context | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/cancellation_async_cancel_via_future_cancel | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/cancellation_async_cancel_via_task_cancel | y | - | - | - | - | - | y | - | y | y | all | | -| function_calls/cancellation_async_cancel_via_task_group_sibling | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/cancellation_async_cancel_via_timeout_race | - | - | - | - | - | - | - | - | - | y | all | | -| function_calls/cancellation_can_pre_abort_async_baml_cancellation | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/cancellation_cancels_an_async_generated_free_function | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/cancellation_cancels_an_async_generated_instance_method | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/cancellation_detaches_a_completed_call_before_aborting_the_remaining_call | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/cancellation_future_destruction_detaches | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/cancellation_future_second_get_throws_future_error | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/cancellation_future_wait_for_times_out_while_in_flight | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/cancellation_future_wait_then_get | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/cancellation_immediately_cancels_every_call_attached_after_abort | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/cancellation_pre_aborts_a_generated_synchronous_call | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/cancellation_surfaces_a_reused_call_context_as_abort_error_with_baml_reason | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/cancellation_surfaces_async_cancellation_as_abort_error_with_baml_reason | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/cancellation_surfaces_cancellation_through_promise_all | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/cancellation_surfaces_sync_pre_aborted_cancellation_as_abort_error | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/cancellation_sync_call_returns_none | y | - | - | - | y | - | y | - | y | y | all | | -| function_calls/cancellation_sync_cancel_via_call_context | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/cancelled_waiter_does_not_end_host_execution | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/cancelled_waiter_keeps_host_context_through_cleanup_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python task unwinding and ContextVar semantics | -| function_calls/cancelled_waiter_keeps_host_resource_until_promise_exit_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node AsyncResource destruction and AsyncLocalStorage | -| function_calls/cancelled_waiter_preserves_callback_context | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/cancelled_waiter_stays_cancelled_when_callback_returns_late | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/cancelling_call_delivers_cancellation_and_allows_callback_cleanup_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python callback task and event loop ownership | -| function_calls/canonical_json_class_union_uses_declared_field_codecs | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/canonical_json_composes_through_containers_and_classes | - | - | - | - | - | - | y | y | - | - | all | | -| function_calls/canonical_json_defaults_and_callbacks | - | - | - | - | - | - | y | y | - | - | all | | -| function_calls/canonical_json_dynamic_union | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/canonical_json_rejects_extensions_before_dispatch | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/canonical_json_round_trips_at_top_level_and_through_alias | - | - | - | - | - | - | y | y | - | - | all | | -| function_calls/child_cancellation_does_not_cancel_input | y | y | y | y | - | - | y | - | - | - | all | | -| function_calls/clean_exit_helper_process | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/clean_exit_terminates_process_with_code | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/closure_call_async_can_be_reused_after_creation_loop_closes_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python returned closure reuse across application loops | -| function_calls/closure_call_async_cancels_its_retained_callback_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python closure async entry cancellation delivery | -| function_calls/closure_call_async_preserves_binding_and_exception_identity_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python closure async entry argument and error semantics | -| function_calls/closure_call_async_uses_invoking_loop_and_context_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python explicit async closure entry and context capture | -| function_calls/coawait_co_await_cancelled_call_throws_cancelled | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/coawait_co_await_completed_future_fast_path | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/coawait_co_await_pending_future_resumes | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/coawait_co_await_throws_typed_into_the_coroutine | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/coawait_co_await_yields_the_decoded_value | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/coawait_uncaught_coroutine_exception_reaches_join | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/completed_call_can_reuse_callback | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/completed_call_does_not_pin_reused_callback_to_old_context_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | -| function_calls/composite_token_observes_every_source | y | y | y | y | - | - | y | - | y | - | all | | -| function_calls/concurrent_calls_isolate_async_local_storage_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/concurrent_calls_keep_callback_results_independent | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/concurrent_calls_share_callback_without_sharing_context_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | -| function_calls/concurrent_host_invocations_isolate_context | y | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| function_calls/concurrent_invocations_isolate_context | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/concurrent_reservation_attaches_once | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/configuration_snapshot_is_not_live | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/cooperative_abort_allows_callback_cleanup_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/cooperative_abort_cleans_up_retained_callback_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/coroutine_entry_uses_execution_context_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | covers Python coroutine creation versus first execution | -| function_calls/current_context_returns_detached_snapshot | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/deadline_reentry_does_not_reset_budget | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/dropping_started_future_cancels_rust_only | - | - | - | - | - | - | y | - | - | - | rust | Rust task scopes and lazy future cancellation are specific to Rust | -| function_calls/dynamic_application_map_preserves_control_like_keys | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/dynamic_call_accepts_controls | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/dynamic_call_async_accepts_controls | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/dynamic_name_and_handle_accept_controls | - | - | - | - | - | - | y | - | y | - | all | | -| function_calls/dynamic_name_and_handle_accept_controls_async | - | - | - | - | - | - | y | - | - | - | all | | -| function_calls/dynamic_type_bindings_async_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python dynamic type bindings accept Python classes and reflected BamlType handles | -| function_calls/empty_class_self_round_trip | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/empty_controls | y | y | y | y | - | - | y | - | y | - | all | | -| function_calls/empty_json_containers_round_trip | - | - | - | - | - | - | - | - | y | - | all | | -| function_calls/encoding_time_counts_against_deadline_python_only | y | - | - | - | - | - | - | - | - | - | all | | -| function_calls/error_call_cancellation_preserves_context_identity | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/error_string_is_non_empty | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/errors_async_sibling_throws_typed | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/errors_baml_error_carries_baml_trace | y | - | - | - | - | - | y | - | y | y | all | | -| function_calls/errors_baml_trace_spliced_into_python_traceback | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/errors_cancellation_surfaces_as_baml_panic | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/errors_clean_exit_terminates_process_with_code | - | - | - | - | - | - | y | - | - | - | all | | -| function_calls/errors_clean_exit_terminates_process_with_code_0 | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/errors_clean_exit_terminates_process_with_code_7 | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/errors_host_invalid_argument_wraps_baml_errors_invalid_argument | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/errors_stdlib_error_surfaces_as_baml_error | y | - | - | - | - | - | y | - | y | y | all | | -| function_calls/errors_stdlib_error_surfaces_typed | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/errors_str_is_non_empty | y | - | - | - | - | - | y | - | y | y | all | | -| function_calls/errors_typed_throw_is_still_a_baml_error | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/errors_union_throws_preserves_class_name | y | y | y | y | y | - | y | - | y | y | all | | -| function_calls/errors_user_panic_surfaces_as_baml_panic | y | - | - | - | - | - | y | - | y | y | all | | -| function_calls/errors_user_throw_surfaces_declared_instance | y | - | - | - | y | - | y | - | y | y | all | | -| function_calls/explicit_controls_are_applied | - | - | - | - | - | - | - | - | - | y | all | | -| function_calls/explicit_controls_are_applied_async | - | - | - | - | - | - | - | - | - | y | all | | -| function_calls/failed_admission_does_not_consume_reservation | - | - | - | - | - | - | y | - | y | - | all | | -| function_calls/four_call_forms | y | y | y | y | - | - | y | - | y | y | all | | -| function_calls/four_call_forms_async | y | y | y | y | - | - | y | - | y | y | all | | -| function_calls/generic_calls_choose_explicit | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/generic_calls_consume_int_wrapper_baseline | y | - | - | - | - | - | y | - | y | y | all | | -| function_calls/generic_calls_extract_explicit | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/generic_calls_generic_free_fn_requires_binding | y | - | - | - | - | - | y | - | y | y | all | | -| function_calls/generic_calls_generic_static_infers_binding | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/generic_calls_genericbox_get_explicit | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/generic_calls_genericbox_new_static_explicit | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/generic_calls_genericbox_pair_with_explicit | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/generic_calls_identity_async_explicit | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/generic_calls_identity_explicit | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/generic_calls_identity_explicit_class_cannot_restate_an_instance | - | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_calls_instance_method_unparameterized_receiver_raises | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/generic_calls_list_head_explicit | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/generic_calls_make_int_box_reified | y | - | - | - | - | - | y | - | y | y | all | | -| function_calls/generic_calls_make_int_container_reified | y | - | - | - | - | - | y | - | y | y | all | | -| function_calls/generic_calls_make_int_str_bool_triple_reified | y | - | - | - | - | - | y | - | y | y | all | | -| function_calls/generic_calls_make_nested_box_reified | y | - | - | - | - | - | y | - | y | y | all | | -| function_calls/generic_calls_make_triple_explicit | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/generic_calls_named_static_distinct_typevar_names | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/generic_calls_one_type_arg_explicit | y | - | - | - | - | - | y | - | y | y | all | | -| function_calls/generic_calls_optional_only_unset_names_its_type_arg | - | - | - | - | - | - | - | - | - | y | all | | -| function_calls/generic_calls_parse_as_explicit | y | - | - | - | - | - | y | - | y | y | all | | -| function_calls/generic_calls_read_items_explicit | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/generic_calls_second_of_explicit | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/generic_calls_subscript_wrong_arity_raises | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/generic_calls_tag_or_value_explicit | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/generic_calls_two_type_args_explicit | y | - | - | - | - | - | y | - | y | y | all | | -| function_calls/generic_calls_wrap_explicit | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/generic_class_instantiated_at_int | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/generic_class_with_a_json_field_round_trips | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/generic_classes_containers_and_concrete_outputs | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/generic_defaulted_parameters_use_option_setters | - | - | - | - | - | - | - | y | - | - | go | pins Go's functional-option setters, generic when the defaulted value mentions a type parameter | -| function_calls/generic_host_callbacks_share_the_function_type_parameters | - | - | - | - | - | - | - | y | - | - | go | pins Go's generic host-callback adapters | -| function_calls/generic_identity_accepts_nested_dynamic_containers | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/generic_identity_inference_and_explicit_types | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/generic_inference_apply_closure_poisons_typevars_must_specify | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_apply_closure_typevars_specified_succeeds | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_body_only_var_still_requires_binding | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_choose_divergent_generic_instances_union | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_choose_infers_divergent_union | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_choose_infers_unified_typevar | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_choose_union_outside_container_is_sound | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_combine_invariant_class_arg_conflict_rejects | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_default_only_value_position | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | validates Python default-argument stubs and runtime TypeVar inference | -| function_calls/generic_inference_elem_type_heterogeneous_array_unifies | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_elem_type_homogeneous_array_is_single_type | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_elem_type_three_way_heterogeneous_array_unifies | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_extract_fully_unbound_nested_pair_recovers_all_vars | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_extract_infers_four_typevars_from_nesting | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_first_or_empty_list_requires_binding | y | - | - | - | - | - | - | - | y | - | python_pydantic2, java | a dynamic host has no static element type for an empty container; the typed SDKs state theirs and round-trip | -| function_calls/generic_inference_first_or_empty_list_round_trips_none | - | - | - | - | - | - | - | - | - | y | all | | -| function_calls/generic_inference_first_or_empty_list_with_explicit_type_round_trips | - | - | - | - | - | - | - | - | - | y | swift | Swift names `T` statically, so an empty list cannot leave it unbound; this pins the explicit binding round trip | -| function_calls/generic_inference_first_or_nonempty_infers_element | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_generic_static_infers_own_typevar | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_genericbox_get_infers_class_var_from_receiver | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_genericbox_pair_with_infers_method_typevar | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_genericbox_pair_with_unbound_receiver_recovers_class_var | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_glue_invariant_and_covariant_agree_binds | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_glue_invariant_vs_covariant_conflict_rejects | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_host_only_object_not_encodable_from_python | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_identity_async_infers | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_identity_enum_round_trips | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_identity_infers_generic_instance | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_identity_infers_primitives | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_identity_infers_user_class | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_identity_nested_unbound_round_trips | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_identity_null_binds_null | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_identity_null_with_explicit_optional_type_round_trips | - | - | - | - | - | - | - | - | - | y | swift | Swift names `T` statically, so a null argument infers nothing here; this pins the explicit binding round trip | -| function_calls/generic_inference_identity_unbound_generic_instance_round_trips | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_list_head_infers_from_recursive_generic | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_list_head_unbound_recursive_recovers_t_from_fields | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_make_triple_full_subscript_contradicted_by_actual_rejects | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_make_triple_heterogeneous_list_element_unions | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_make_triple_infers_multiple_typevars | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_make_triple_partial_explicit_then_infer | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_make_triple_partial_subscript_requires_full_arity | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_make_triple_subscript_fully_bound | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_make_triple_types_kwarg_contradicted_by_actual_rejects | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_maybe_id_null_round_trips | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_maybe_id_present_value_infers | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_merge_invariant_map_value_conflict_rejects | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_named_static_infers_distinct_typevars | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_one_type_arg_explicit_types_succeeds | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_pair_invariant_list_agree_binds | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_pair_invariant_list_conflict_rejects | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_parse_as_explicit_types_succeeds | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_read_items_infers_from_instance_wire_args | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_read_items_unbound_container_recovers_t_from_fields | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_return_only_var_still_requires_binding | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_second_of_infers_from_nested_generic | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_second_of_unbound_instance_recovers_field_type | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_tag_or_value_binds_generic_instance | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_triple_choose_join_includes_concrete_class | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_triple_choose_join_includes_enum_variant | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_triple_choose_three_covariant_join | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_two_typevar_union_is_uninferrable_rejects | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_union_concrete_sibling_absorbs_value_binds_never | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_union_concrete_sibling_keeps_the_static_type_arg | - | - | - | - | - | - | - | - | - | y | all | | -| function_calls/generic_inference_union_null_actual_binds_never | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_union_null_actual_keeps_the_static_type_arg | - | - | - | - | - | - | - | - | - | y | all | | -| function_calls/generic_inference_union_with_concrete_sibling_infers_typevar | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_values_of_empty_map_requires_binding | y | - | - | - | - | - | - | - | y | - | python_pydantic2, java | a dynamic host has no static value type for an empty map; the typed SDKs state theirs and round-trip | -| function_calls/generic_inference_values_of_empty_map_round_trips_empty_list | - | - | - | - | - | - | - | - | - | y | all | | -| function_calls/generic_inference_values_of_nonempty_returns_values | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_inference_wrap_infers_and_returns_bound_generic | y | - | - | - | - | - | - | - | y | - | all | | -| function_calls/generic_inference_wrap_infers_and_returns_generic | y | - | - | - | - | - | - | - | y | y | all | | -| function_calls/generic_nullable_type_variables_preserve_every_pointer_boundary | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/generic_receiver_and_static_helpers | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/generic_return_only_type_arguments | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/generic_union_input_and_engine_validation | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/go_codegen_context_deadline | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/go_codegen_default_argument_serialization_error_names_argument | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/go_codegen_defaulted_argument_type_matrix | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/go_codegen_defaulted_void | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/go_codegen_option_name_collisions | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/go_codegen_optional_arg_last_value_wins | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/go_codegen_person_round_trip | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/hello_world_returns_literal | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/hidden_mode_does_not_inherit | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/host_baml_cancellation_remains_live_after_exit | y | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| function_calls/host_baml_trace_override_and_callback_context_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | explicit Python call controls override instrumentation defaults | -| function_calls/host_callable_cancellation_while_dispatched | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/host_callable_class_argument | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/host_callable_closed_union_containers_and_nominal_arms | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/host_callable_closed_union_literal_optional_and_selected_empty_container_arms | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/host_callable_closed_union_round_trips | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/host_callable_declared_throw_is_catchable | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/host_callable_failure_under_open_throws_is_catchable | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/host_callable_hof_receives_a_baml_closure | y | y | y | y | y | - | y | - | y | y | all | | -| function_calls/host_callable_late_and_uncaught_failures_release_native_identity | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/host_callable_media_round_trip | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/host_callable_nullable_optional_distinguishes_all_three_states | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/host_callable_optional_arguments | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/host_callable_optional_names_avoid_generated_and_projection_collisions | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/host_callable_panic_does_not_cross_cgo_boundary | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/host_callable_primitive_and_multiple_arguments | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/host_callable_reentrant_call_does_not_deadlock | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/host_callable_repeated_and_concurrent_reuse | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/host_callable_structured_round_trips | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/host_callable_two_cbs_callbacks_named_f_and_f_returns_both_run | y | y | y | y | y | - | y | y | y | y | all | | -| function_calls/host_callable_void_signatures | y | y | y | y | y | - | y | y | y | - | all | | -| function_calls/host_callables_adopts_a_custom_thenable_exactly_once | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/host_callables_adopts_a_promise_from_a_separate_browser_realm | - | - | y | - | - | - | - | - | - | - | python_pydantic2, typescript_web_chromium, cpp, csharp, rust, go, java, swift | | -| function_calls/host_callables_async_callable_future_completing_exceptionally_round_trips_original | - | - | - | - | - | - | - | - | y | - | all | | -| function_calls/host_callables_async_callable_returning_future_is_awaited_by_bridge | - | - | - | - | - | - | - | - | y | - | all | | -| function_calls/host_callables_async_callable_runs_to_completion | y | - | - | - | - | - | y | - | y | y | all | | -| function_calls/host_callables_awaits_a_promise_returning_callback | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/host_callables_baml_error_panicked_with_is_thrown_on | - | - | - | - | - | - | - | y | - | - | go | Go's completion classification of a BAML error a callback forwards by panicking with it | -| function_calls/host_callables_call_repeatedly_invokes_callback_n_times | y | y | y | y | y | - | y | - | y | y | all | | -| function_calls/host_callables_call_repeatedly_with_zero_n_returns_empty_list | y | y | y | y | y | - | y | - | y | y | all | | -| function_calls/host_callables_call_with_throwing_in_baml_catches_host_callable_error | y | y | y | y | y | - | y | - | y | y | all | | -| function_calls/host_callables_cancels_through_the_originating_runtime_after_runtime_replacement | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/host_callables_class_callback_round_trips_class_value | - | - | - | - | y | - | - | - | - | y | all | | -| function_calls/host_callables_class_callback_round_trips_pydantic_model | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/host_callables_completes_a_pending_host_call_after_runtime_replacement | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/host_callables_completes_through_the_metadata_fallback_for_a_hostile_thrown_object | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/host_callables_concurrent_throws_in_flight_rehydrate_to_their_own_object | - | - | - | - | - | - | - | - | y | - | all | | -| function_calls/host_callables_delivers_a_single_supplied_optional_by_name_defaulting_the_rest | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/host_callables_delivers_both_supplied_optionals_in_one_opts_object | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/host_callables_forwarded_async_cancellation_stays_a_panic | - | - | - | - | - | - | - | - | y | y | java, swift | pins a typed SDK's completion of a nested call's cancellation, which it surfaces as its native cancellation | -| function_calls/host_callables_forwarded_baml_error_is_held_to_the_declared_throws | - | - | - | - | - | - | - | y | - | - | go | Go's completion classification of a forwarded BAML error held to the declared throws type | -| function_calls/host_callables_forwarded_baml_error_is_thrown_as_itself | - | - | - | - | y | - | - | - | - | y | cpp, swift | pins a forwarded BAML error completed as thrown | -| function_calls/host_callables_forwarded_cancellation_stays_a_panic_under_throws | - | - | - | - | y | - | - | - | - | - | cpp | C++ surfaces BAML cancellation as baml::cancelled. | -| function_calls/host_callables_forwarded_panic_stays_a_panic_under_a_declared_throws_class | - | - | - | - | - | - | - | y | y | y | go, java, swift | pins a typed SDK's completion of a forwarded panic under a single-class throws | -| function_calls/host_callables_forwarded_type_mismatch_crosses_as_the_baml_error_it_is | - | - | - | - | - | - | - | - | y | - | java | pins the Java completion of a forwarded TypeMismatch, which Java surfaces as an IllegalArgumentException | -| function_calls/host_callables_ignores_a_host_promise_settlement_after_its_outer_call_is_cancelled | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/host_callables_int_return_callable_round_trip | y | y | y | y | y | - | y | - | y | y | all | | -| function_calls/host_callables_lambda_round_trip | y | - | - | - | y | - | y | - | y | y | all | | -| function_calls/host_callables_literal_union_callback_states_its_literal_types | - | - | - | - | - | - | - | - | - | y | all | | -| function_calls/host_callables_media_callback_states_the_media_type | - | - | - | - | - | - | - | - | - | y | all | | -| function_calls/host_callables_multiple_callable_keys_are_distinct | y | y | y | y | y | - | y | - | y | y | all | | -| function_calls/host_callables_multiple_throws_in_flight_do_not_collide_in_registry | y | - | - | - | y | - | y | - | y | y | all | | -| function_calls/host_callables_non_finite_json_result_is_a_contract_violation | - | - | - | - | - | - | - | y | - | - | go | Go's completion classification of a returned value that does not fit its declared type | -| function_calls/host_callables_omits_both_optionals_so_the_callback_s_own_defaults_apply | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/host_callables_optional_args_all_set_deliver_both | y | - | - | - | y | - | y | - | y | y | all | | -| function_calls/host_callables_optional_args_all_unset_apply_host_defaults | y | - | - | - | y | - | y | - | y | y | all | | -| function_calls/host_callables_optional_args_partially_set_deliver_by_name | y | - | - | - | y | - | y | - | y | y | all | | -| function_calls/host_callables_overlapping_container_union_callback_states_the_selected_list | - | - | - | - | - | - | - | - | - | y | all | | -| function_calls/host_callables_passes_a_generated_class_instance_into_the_callback | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/host_callables_preserves_a_rejected_promise_reason_by_identity | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/host_callables_preserves_an_error_whose_stack_is_not_a_string | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/host_callables_preserves_arbitrary_thrown_js_values_without_hanging | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/host_callables_preserves_number_kind_for_float_bigint_union | - | y | y | y | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | exercises JavaScript's distinct number and bigint host representations | -| function_calls/host_callables_preserves_same_realm_thrown_object_identity | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/host_callables_rejects_callable_args_on_the_generated_sync_path_instead_of_hanging | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/host_callables_release_fires_on_drop_of_callable | y | y | y | y | - | - | y | - | y | - | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, rust, java | callable release coverage depends on host weak-reference support and remains nondeterministic | -| function_calls/host_callables_resolves_integral_number_for_float_class_field | - | y | y | y | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | exercises the Node bridge's JavaScript number encoding in generated classes | -| function_calls/host_callables_resolves_integral_number_for_float_return | - | y | y | y | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | exercises the Node bridge's JavaScript number encoding for float host returns | -| function_calls/host_callables_returns_and_invokes_a_host_callable_nested_in_a_list | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/host_callables_returns_and_invokes_a_nested_host_callable | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/host_callables_round_trips_a_typed_baml_error_through_typed_catch_and_propagation | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/host_callables_round_trips_an_arrow_function_callback | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/host_callables_simple_sync_callable_returns_string | y | y | y | y | y | - | y | - | y | y | all | | -| function_calls/host_callables_surfaces_a_throwing_callback_as_a_baml_error | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/host_callables_surfaces_a_wrong_callback_return_type_as_host_contract_violation | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/host_callables_throwing_async_callable_round_trips_original_error | - | - | - | - | - | - | - | - | - | y | all | | -| function_calls/host_callables_throwing_async_callable_round_trips_original_python_exception | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/host_callables_throwing_callable_bamlerror_propagates_back_with_typed_fields | y | - | - | - | - | - | y | y | y | - | all | | -| function_calls/host_callables_throwing_callable_bamlerror_wrapping_codegenned_class_is_caught_in_baml | y | - | - | - | - | - | y | y | y | - | all | | -| function_calls/host_callables_throwing_callable_custom_host_exception_round_trips_with_identity | - | - | - | - | y | - | - | - | - | y | all | | -| function_calls/host_callables_throwing_callable_custom_python_exception_round_trips_with_identity | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/host_callables_throwing_callable_hostthrow_codegenned_class_is_caught_in_baml | - | - | - | - | y | - | - | - | - | y | all | | -| function_calls/host_callables_throwing_callable_hostthrow_propagates_back_with_typed_fields | - | - | - | - | y | - | - | - | - | y | all | | -| function_calls/host_callables_throwing_callable_keyerror_round_trips_with_identity | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/host_callables_throwing_callable_out_of_range_round_trips_with_identity | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/host_callables_throwing_callable_round_trips_original_host_exception | - | - | - | - | y | - | - | - | - | y | all | | -| function_calls/host_callables_throwing_callable_round_trips_original_python_exception | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/host_callables_thrown_value_of_another_go_type_is_a_contract_violation | - | - | - | - | - | - | - | y | - | - | go | Go's completion classification of a value thrown as a Go type other than the declared one | -| function_calls/host_callables_two_arg_callable_unpacks_positional_args | y | y | y | y | y | - | y | - | y | y | all | | -| function_calls/host_callables_typed_throw_too_deep_is_a_bridge_failure | - | - | - | - | y | - | - | - | - | - | cpp | C++ throws a BAML value with baml::host_throw. | -| function_calls/host_callables_unfit_callback_result_is_a_contract_violation | - | - | - | - | y | - | - | - | - | - | cpp | C++ can return only a malformed bigint as unfit. | -| function_calls/host_callback_cleanup_retains_context | y | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| function_calls/host_callback_reentry_context | y | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| function_calls/host_cancellation_waits_for_cleanup_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | covers Python task cancellation and asynchronous finally cleanup | -| function_calls/host_capture_avoids_getters_and_proxies_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | covers JS getters, Proxy traps, and custom thenables | -| function_calls/host_capture_preserves_application_values | y | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| function_calls/host_capture_wire_stays_decodable_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | checks the Python capture adapter's wire output directly | -| function_calls/host_capture_wire_stays_decodable_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | checks the Node capture adapter's wire output directly. | -| function_calls/host_child_outlives_parent | y | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| function_calls/host_common_types_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node observations are checked alongside Python by local SQL. | -| function_calls/host_context_inherits_and_restores | y | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| function_calls/host_current_exposes_generated_cancel_token | y | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| function_calls/host_defaults_baml_span_and_error_capture_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python instrumentation defaults Python-to-BAML span/capture requests | -| function_calls/host_errors_preserve_identity_and_restore_context | y | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| function_calls/host_preserves_call_shape_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | covers JS receivers, variadic arguments, and native Promise results | -| function_calls/host_recording_audit_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | runs the Node half of the independent local SQL audit | -| function_calls/host_rejects_unsupported_configuration_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | covers unsupported Python generator/decorator shapes | -| function_calls/host_rejects_unsupported_configuration_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | covers JS generator and marker configuration rejection | -| function_calls/host_supplied_empty_and_null_bearing_json_containers_state_the_alias | - | - | - | - | - | - | - | - | - | y | all | | -| function_calls/host_supplied_json_supports_typed_narrowing | y | y | y | y | y | - | y | y | y | y | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, cpp, rust, go, java, swift | C# declares no function_calls suite (its native coverage is Rust-wrapped integration tests) | -| function_calls/host_thread_context_handoff_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | covers Python ContextVar handoff to a thread pool | -| function_calls/instance_method_cancellation_returns_exact_context_error | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/instance_method_media_receiver_default_and_ownership_round_trip | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/instance_method_optional_arguments | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/instance_method_throw_preserves_current_go_error_contract | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/instance_methods_on_classes_round_trip | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/instance_never_method_has_error_only_signature_and_returns_panic | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/instrument_decorator_forms_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | covers Python decorator forms | -| function_calls/instrument_preserves_sync_execution_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | covers Python signatures and positional argument binding | -| function_calls/invalid_controls_rejected | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/invalid_function_arguments_surface_baml_error | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/invalid_response_preserves_wire_diagnostics | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | exercises Python BamlError and generated Pydantic payloads | -| function_calls/invalid_timeout_rejected | y | y | y | y | - | - | y | - | y | - | all | | -| function_calls/invocation_inheritance | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/invocation_inheritance_multi_layer_context_patch_inherits_and_restores | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/invocation_lifecycle | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/invocation_lifecycle_failed_admission_does_not_consume_reservation | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/invocation_lifecycle_pre_cancelled_call_does_not_enter_callback | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/invocation_lifecycle_reservation_is_single_use | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/invocation_lifecycle_retained_effective_token_observes_late_parent_cancellation | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/invocation_lifecycle_zero_timeout_does_not_enter_callback | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/invocation_options | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/invocation_options_four_call_forms | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/invocation_options_four_call_forms_async | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/invocation_options_invalid_timeout_rejected | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/invocation_surfaces | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/invocation_surfaces_returned_callable_accepts_controls | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/json_bigint_round_trips_as_bigint | - | - | - | - | - | - | - | - | y | - | all | | -| function_calls/json_nested_at_the_wire_limit_crosses_and_one_level_more_is_refused | - | - | - | - | - | - | - | - | y | y | java, swift | pins a typed SDK encoder's exact message count at the wire's limit | -| function_calls/json_nested_deeper_than_the_wire_carries_is_refused_as_a_callback_result | y | y | y | y | y | - | y | y | y | y | all | | -| function_calls/json_nested_deeper_than_the_wire_carries_is_refused_as_an_argument | y | y | y | y | y | - | y | y | y | y | all | | -| function_calls/json_nested_to_the_wire_limit_crosses_and_one_level_more_does_not | - | - | - | - | y | - | - | - | - | - | cpp | pins the C++ encoder's exact wire measure at its cut. | -| function_calls/json_returned_from_host_callback_supports_typed_narrowing | y | y | y | y | y | - | y | y | y | y | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, cpp, rust, go, java, swift | C# declares no function_calls suite (its native coverage is Rust-wrapped integration tests) | -| function_calls/late_callback_keeps_async_local_storage_after_abort_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node callback Promises can outlive an aborted SDK waiter | -| function_calls/layered_callback_context_inheritance_and_restoration_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python sync/async re-entry and ContextVar propagation | -| function_calls/layered_callback_context_inheritance_and_restoration_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node async carrier, native signals, and entry-time snapshots are specific to TypeScript | -| function_calls/layered_callback_context_restores_after_returned_callable | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/legacy_controls_rejected | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/linking_the_sdk_leaves_serde_json_features_alone | - | - | - | - | - | - | y | - | - | - | rust | cargo feature unification is Rust-only | -| function_calls/live_token_cancels_after_admission | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/main_hello_world_returns_literal | y | - | - | - | y | - | y | - | y | y | all | | -| function_calls/main_rejects_mistyped_int_argument | y | y | y | y | - | - | - | - | - | - | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | statically typed SDKs cannot pass a mistyped argument | -| function_calls/main_returns_the_literal_async | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/main_returns_the_literal_sync | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/main_round_trips_a_single_positional_argument | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/main_round_trips_ints_bools_strings_and_floats | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/main_single_required_arg_round_trips | y | - | - | - | y | - | y | - | y | y | all | | -| function_calls/method_generated_name_collisions_stay_callable | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/method_self_all_supported_positions_round_trip | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/methods_accept_controls | y | y | y | y | - | - | y | - | y | - | all | | -| function_calls/methods_accept_controls_async | y | y | y | y | - | - | y | - | - | - | all | | -| function_calls/methods_on_classes_create_constructs_a_greeter_async_plus_sync | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/methods_on_classes_exposes_sync_plus_async_bindings_for_both_flavors | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/methods_on_classes_greet_arg_echoes_a_non_self_argument_async_plus_sync | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/methods_on_classes_instance_greet_async_with_arg_round_trips | y | - | - | - | y | - | y | - | y | y | all | | -| function_calls/methods_on_classes_instance_greet_with_arg_round_trips | y | - | - | - | y | - | y | - | y | y | all | | -| function_calls/methods_on_classes_instance_who_async_round_trips | y | - | - | - | y | - | y | - | y | y | all | | -| function_calls/methods_on_classes_instance_who_round_trips | y | - | - | - | y | - | y | - | y | y | all | | -| function_calls/methods_on_classes_method_bindings_exist | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/methods_on_classes_static_create_async_round_trips | y | - | - | - | y | - | y | - | y | y | all | | -| function_calls/methods_on_classes_static_create_round_trips | y | - | - | - | y | - | y | - | y | y | all | | -| function_calls/methods_on_classes_who_returns_a_field_off_self_async_plus_sync | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/multi_layer_context_patch_inherits_and_restores | - | - | - | - | - | - | y | - | y | - | all | | -| function_calls/native_closure_can_cross_sync_and_async_entries_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node sync calls support native closures whose captures need no host dispatch | -| function_calls/native_signal_cancels_without_cancelling_input_token_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node async carrier, native signals, and entry-time snapshots are specific to TypeScript | -| function_calls/nil_host_callable_fails_before_dispatch | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/null_controls_preserve_inherited_context | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/omitted_argument_is_not_null | y | y | y | y | - | - | y | - | y | y | all | | -| function_calls/optional_args_async_samples | y | y | y | y | y | - | y | - | y | y | all | | -| function_calls/optional_args_bigint_in_i64_range | - | - | - | - | - | - | - | - | y | - | all | | -| function_calls/optional_args_covers_static_and_instance_optional_args | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/optional_args_covers_the_runtime_matrix | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/optional_args_negative_runtime_cases_reject | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/optional_args_opt_box_method_matrix | y | - | - | - | y | - | y | - | y | y | all | | -| function_calls/optional_args_python_unset_and_none_differ_in_one_call | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/optional_args_rejects_invalid_runtime_calls_that_bypass_types | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/optional_args_runtime_matrix | y | - | - | - | y | - | y | y | y | y | all | | -| function_calls/optional_args_treats_undefined_as_omitted_and_keeps_null_distinct | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/optional_args_unset_and_null_differ_in_one_call | - | - | - | - | y | - | - | - | - | y | all | | -| function_calls/options_snapshot_at_async_entry_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node async carrier, native signals, and entry-time snapshots are specific to TypeScript | -| function_calls/options_snapshot_when_coroutine_starts_python_only | y | - | - | - | - | - | - | - | - | - | all | | -| function_calls/parse_json_successful_value_uses_generated_json_projection | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/pre_aborted_call_does_not_dispatch_callback_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/pre_aborted_native_signal_does_not_enter_callback_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node async carrier, native signals, and entry-time snapshots are specific to TypeScript | -| function_calls/pre_cancelled_call_does_not_enter_callback | y | y | y | y | - | - | y | - | y | - | all | | -| function_calls/promise_all_failure_with_explicit_sibling_abort_preserves_error_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/promise_callback_preserves_async_local_storage_across_suspension_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/promise_callback_sync_reentry_rejects_host_callback_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/provider_error_preserves_native_error_metadata | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | exercises Python BamlError and generated Pydantic payloads | -| function_calls/raises_async_sibling_also_has_raises | y | - | - | - | y | - | y | - | y | - | all | | -| function_calls/raises_imports | - | - | - | - | - | - | y | - | - | - | all | | -| function_calls/raises_imports_symbols_reachable | y | - | - | - | y | - | - | - | y | - | all | | -| function_calls/raises_inferred_contract_without_clause_still_raises | y | - | - | - | y | - | y | - | y | - | all | | -| function_calls/raises_method_raises_block_in_pyi | y | - | - | - | - | - | y | - | y | - | all | | -| function_calls/raises_method_raises_blocks | - | - | - | - | y | - | - | - | - | - | all | | -| function_calls/raises_non_throwing_function_has_no_raises_block | y | - | - | - | y | - | y | - | y | - | all | | -| function_calls/raises_single_throws | y | - | - | - | y | - | y | - | y | - | all | | -| function_calls/raises_summary_precedes_raises_block | y | - | - | - | y | - | y | - | y | - | all | | -| function_calls/raises_union_throws_lists_all_names | y | - | - | - | y | - | y | - | y | - | all | | -| function_calls/reflect_facade_builds_runtime_declarations_and_decodes_their_instances | - | - | - | - | - | - | - | y | - | - | go | Go's hand-written reflect facade; the host_reflect fixture that would cover it does not compile | -| function_calls/reflected_type_closed_dynamic_unions_and_callback | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/reflected_type_composes_through_optional_containers_and_classes | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/reflected_type_primitive_literal_and_nominal_descriptors | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/reflected_type_top_level_and_runtime_produced_values | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/rejected_admission_does_not_consume_reservation | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/repeated_abort_does_not_interrupt_async_callback_cleanup_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python callback cancellation under an explicit controller | -| function_calls/repeated_cooperative_abort_does_not_interrupt_callback_cleanup_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/repeated_dispatches_each_start_with_entry_async_local_storage_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/repeated_dispatches_each_start_with_entry_context_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python ContextVar isolation between dispatch tasks | -| function_calls/repeated_dispatches_invoke_callback_in_order | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/reservation_does_not_inherit | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/reservation_is_single_use | - | - | - | - | - | - | y | - | y | - | all | | -| function_calls/retained_callback_uses_invocation_async_local_storage_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/retained_context_survives_parent_completion | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/retained_effective_token_observes_late_parent_cancellation | - | - | - | - | - | - | y | - | y | - | all | | -| function_calls/retained_effective_token_stays_live_after_callback | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/retained_host_callback_uses_later_invocations_application_context_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python callback task and event loop ownership | -| function_calls/retained_invocation_resolves_token_after_callback | y | - | - | - | - | - | - | - | - | - | all | | -| function_calls/returned_callable_accepts_controls | y | y | y | y | - | - | y | - | y | - | all | | -| function_calls/returned_callable_accepts_controls_async | - | - | - | - | - | - | y | - | - | - | all | | -| function_calls/returned_closure_preserves_callback_error_and_remains_reusable | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/returned_closure_retains_host_callback | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/returned_function_is_accepted_as_an_argument | - | - | - | - | - | - | y | - | - | - | all | | -| function_calls/reused_callback_uses_current_async_local_storage_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/runtime_executes_the_generated_sdk_in_a_browser | - | - | y | - | - | - | - | - | - | - | python_pydantic2, typescript_web_chromium, cpp, csharp, rust, go, java, swift | | -| function_calls/runtime_executes_the_generated_sdk_in_node | - | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| function_calls/runtime_executes_the_generated_sdk_in_workerd | - | - | - | y | - | - | - | - | - | - | python_pydantic2, typescript_web_cloudflare_workers, cpp, csharp, rust, go, java, swift | | -| function_calls/runtime_imports_the_generated_sdk_in_the_configured_runtime | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/same_callback_recurses_through_baml | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/same_callback_recurses_through_baml_without_reusing_an_entry_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | sync callback re-entry differs between Python and Node | -| function_calls/same_named_throws_ok_passes_through | - | - | - | - | - | - | y | - | - | - | rust | validates Rust-specific throws variant naming | -| function_calls/same_named_throws_selects_the_json_parse_error | - | - | - | - | - | - | y | - | - | - | rust | validates Rust-specific throws variant naming | -| function_calls/same_named_throws_selects_the_stdlib_parse_error | - | - | - | - | - | - | y | - | - | - | rust | validates Rust-specific throws variant naming | -| function_calls/single_required_arg_round_trips | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/specialized_callable_rejects_type_bindings | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/static_method_errors_never_cancellation_and_collision_names | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/static_method_media_json_type_and_rust_type_round_trips | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/static_method_required_default_and_structured_round_trips | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/stdlib_entrypoints_compiler_intrinsics_are_not_emitted_as_entry_points | y | y | - | - | y | - | y | - | y | y | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| function_calls/stdlib_entrypoints_native_argv_callable_as_entry_point | y | - | - | - | y | - | y | - | y | y | all | | -| function_calls/stdlib_entrypoints_native_baml_sys_argv_is_callable_as_an_entry_point | - | y | y | y | - | - | - | - | - | - | all | | -| function_calls/stdlib_entrypoints_sysop_fs_exists_callable_as_entry_point | y | y | - | - | y | - | y | - | y | y | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| function_calls/stdlib_error_surfaces_as_go_error | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/stream_schema_failure_is_parse_failed | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | exercises Python BamlError and generated Pydantic payloads | -| function_calls/suspended_callback_survives_gc_until_cancellation_cleanup_finishes_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | asyncio tasks retain callback bodies until actual completion | -| function_calls/sync_call_returns_null | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/sync_callback_can_run_async_baml_with_its_own_application_loop_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Node cannot synchronously drive an application loop | -| function_calls/sync_callback_inherits_call_time_async_local_storage_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/sync_callback_inherits_call_time_context_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | -| function_calls/sync_callback_of_async_entry_reenters_sync_baml_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | sync callback re-entry differs between Python and Node | -| function_calls/sync_callback_of_async_entry_runs_on_originating_loop_thread_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | -| function_calls/sync_callback_of_async_entry_runs_on_originating_loop_thread_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/sync_callback_reenters_sync_baml_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | sync callback re-entry differs between Python and Node | -| function_calls/sync_callback_runs_on_sync_callers_thread_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | -| function_calls/sync_callback_sync_reentry_rejects_host_callback_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/sync_cancel_via_context | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/sync_entry_async_callback_copies_context_across_suspension_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python ContextVar semantics with a bridge-owned loop | -| function_calls/sync_entry_async_callback_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | -| function_calls/sync_entry_cancellation_reaches_async_callback_on_worker_loop_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python cancellation on a bridge-owned callback loop | -| function_calls/sync_entry_in_running_loop_with_self_contained_async_callback_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | -| function_calls/sync_entry_in_running_loop_with_sync_callback_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | -| function_calls/sync_entry_rejection_does_not_start_promise_callback_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/sync_entry_rejection_preserves_application_context_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/sync_entry_rejects_before_dispatch_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/sync_entry_rejects_callback_before_async_reentry_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/sync_entry_rejects_callback_before_recursive_reentry_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/sync_entry_rejects_callback_before_sync_reentry_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/sync_entry_rejects_promise_callback_between_event_loop_turns_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/sync_entry_rejects_promise_callback_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/sync_entry_rejects_retained_callback_and_allows_async_reuse_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node sync entries must reject callbacks hidden in native closure captures | -| function_calls/sync_entry_rejects_sync_callback_between_event_loop_turns_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/sync_entry_rejects_sync_callback_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | -| function_calls/sync_entry_sync_callback_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | -| function_calls/sync_retained_closure_rejects_before_dispatch_typescript_only | - | y | - | - | - | - | - | - | - | - | typescript_node | Node cannot run JS callbacks while a sync native call blocks its event loop | -| function_calls/task_factory_failure_completes_dispatch_and_preserves_exception_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python asyncio task factory failures during dispatch | -| function_calls/taskgroup_failure_cancels_sibling_baml_call_and_preserves_error_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python callback task and event loop ownership | -| function_calls/timeout_upper_bound_accepted | y | y | y | y | - | - | y | - | y | - | all | | -| function_calls/to_thread_sync_entry_inherits_copied_application_context_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | -| function_calls/two_application_loops_on_two_threads_share_callback_without_rerouting_python_only | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | -| function_calls/unhandled_spawn_error_uses_host_default | - | - | - | - | y | - | - | y | y | y | cpp, go, java, swift | requires subprocess-level SDK harness support | -| function_calls/union_throws_preserves_concrete_class_identity | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/unknown_control_rejected | y | y | y | y | - | - | - | - | - | - | all | | -| function_calls/unmarked_callback_explicit_context_and_reentry | y | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| function_calls/unpolled_future_starts_nothing_rust_only | - | - | - | - | - | - | y | - | - | - | rust | Rust task scopes and lazy future cancellation are specific to Rust | -| function_calls/unset_and_none_differ_in_one_call | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/user_panic_surfaces_as_go_error_without_panicking | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/user_throw_surfaces_declared_class_identity | - | - | - | - | - | - | - | y | - | - | all | | -| function_calls/web_sysops_maps_fetch_failures_and_timeouts_into_declared_baml_errors | - | - | y | y | - | - | - | - | - | - | python_pydantic2, typescript_web_chromium, typescript_web_cloudflare_workers, cpp, csharp, rust, go, java, swift | | -| function_calls/web_sysops_rejects_http_streaming_and_unrelated_sysops | - | - | y | y | - | - | - | - | - | - | python_pydantic2, typescript_web_chromium, typescript_web_cloudflare_workers, cpp, csharp, rust, go, java, swift | | -| function_calls/web_sysops_rejects_sync_and_async_baml_fs_read_promptly | - | - | y | - | - | - | - | - | - | - | python_pydantic2, typescript_web_chromium, cpp, csharp, rust, go, java, swift | | -| function_calls/web_sysops_rejects_sync_http_before_dispatching_fetch | - | - | y | y | - | - | - | - | - | - | python_pydantic2, typescript_web_chromium, typescript_web_cloudflare_workers, cpp, csharp, rust, go, java, swift | | -| function_calls/web_sysops_rejects_unsupported_filesystem_operations | - | - | y | y | - | - | - | - | - | - | python_pydantic2, typescript_web_chromium, typescript_web_cloudflare_workers, cpp, csharp, rust, go, java, swift | | -| function_calls/web_sysops_supports_sync_and_async_baml_fs_read_through_node_fs_read_file_sync | - | - | - | y | - | - | - | - | - | - | python_pydantic2, typescript_web_cloudflare_workers, cpp, csharp, rust, go, java, swift | | -| function_calls/web_sysops_trampolines_baml_http_fetch_to_global_fetch_and_buffers_the_response | - | - | y | y | - | - | - | - | - | - | python_pydantic2, typescript_web_chromium, typescript_web_cloudflare_workers, cpp, csharp, rust, go, java, swift | | -| function_calls/web_sysops_trampolines_baml_http_send_with_method_headers_and_body | - | - | y | y | - | - | - | - | - | - | python_pydantic2, typescript_web_chromium, typescript_web_cloudflare_workers, cpp, csharp, rust, go, java, swift | | -| function_calls/zero_timeout_does_not_enter_callback | y | y | y | y | - | - | y | - | y | - | all | | -| host_reflect/a_runtime_type_is_one_declaration_and_rejects_serialization | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | BEP-066 host reflection is currently exposed only by Python, TypeScript, and Go | -| host_reflect/compiled_package_returns_class_graph | y | y | y | y | - | - | - | y | - | - | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, go | BEP-066 host reflection is currently exposed only by Python, TypeScript, and Go | -| host_reflect/generated_class_subclasses_resolve_to_declared_type | y | y | y | y | - | - | - | - | - | - | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | Python and TypeScript have generated-class subclass tokens; Go has no subclass construct | -| host_reflect/host_handles_expose_composition_only | y | y | y | y | - | - | - | y | - | - | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, go | BEP-066 host reflection is currently exposed only by Python, TypeScript, and Go | -| host_reflect/known_type_tokens_compose_and_reject_unknowns | y | y | y | y | - | - | - | y | - | - | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, go | BEP-066 host reflection is currently exposed only by Python, TypeScript, and Go | -| host_reflect/reflection_compile_errors_are_typed | y | y | y | y | - | - | - | y | - | - | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, go | BEP-066 host reflection is currently exposed only by Python, TypeScript, and Go | -| host_reflect/runtime_class_definition_preserves_nested_metadata | y | y | y | y | - | - | - | y | - | - | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, go | BEP-066 host reflection is currently exposed only by Python, TypeScript, and Go | -| host_reflect/runtime_enum_definition_decodes_alias | y | y | y | y | - | - | - | y | - | - | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, go | BEP-066 host reflection is currently exposed only by Python, TypeScript, and Go | -| host_reflect/wire_occurrences_are_fresh_and_handles_reject_serialization | - | y | y | y | - | - | - | y | - | - | all | | -| integration/baml_closures_execute_host_arguments_structured_returns_and_mutable_captures | - | - | - | - | - | y | - | - | - | - | csharp | C# canonical coverage executes through its native integration harness | -| integration/basic_calls_executes_sync_and_async | - | - | - | - | - | y | - | - | - | - | csharp | exercises C#-specific native SDK integration coverage | -| integration/cancel_token_any_propagates_native_cancellation | - | - | - | - | - | y | - | - | - | - | csharp | isolates the flaky native cancellation propagation check | -| integration/canonical_documentation_consumer_compiles_and_executes | - | - | - | - | - | y | - | - | - | - | csharp | validates the C#-specific documentation consumer | -| integration/checked_in_union_runtime_source_matches_generator | - | - | - | - | - | y | - | - | - | - | csharp | validates C#-specific generated union runtime source | -| integration/dynamic_values_executes_native_dynamic_value_parity | - | - | - | - | - | y | - | - | - | - | csharp | exercises C#-specific native SDK integration coverage | -| integration/dynamic_values_type_naming_a_runtime_declaration_twice_round_trips | - | - | - | - | - | y | - | - | - | - | csharp | exercises C#-specific ownership of a runtime declaration's repeated key | -| integration/failures_and_cancellation_executes_typed_failures_cancellation_and_exit | - | - | - | - | - | y | - | - | - | - | csharp | exercises C#-specific native SDK integration coverage | -| integration/generated_baml_clients_are_not_tracked | - | - | - | - | - | y | - | - | - | - | csharp | validates C#-specific generated-client repository hygiene | -| integration/generics_executes_inferred_and_explicit_generics | - | - | - | - | - | y | - | - | - | - | csharp | exercises C#-specific native SDK integration coverage | -| integration/generics_generated_surface_rejects_ambiguous_generic_calls | - | - | - | - | - | y | - | - | - | - | csharp | exercises C#-specific generated-surface compile coverage | -| integration/host_callable_hof_receives_a_baml_closure | - | - | - | - | - | y | - | - | - | - | csharp | C# canonical coverage executes through its native integration harness | -| integration/host_callables_callback_faults_complete_by_whose_they_are | - | - | - | - | - | y | - | - | - | - | csharp | C# completion-contract coverage executes through its native integration harness | -| integration/host_callables_json_nested_deeper_than_the_wire_carries_is_refused_as_a_callback_result | - | - | - | - | - | y | - | - | - | - | csharp | C# measures typed lists and class chains at the wire's exact limit | -| integration/host_callables_json_nested_deeper_than_the_wire_carries_is_refused_as_an_argument | - | - | - | - | - | y | - | - | - | - | csharp | C# measures typed lists and class chains at the wire's exact limit | -| integration/media_executes_media_in_both_directions | - | - | - | - | - | y | - | - | - | - | csharp | exercises C#-specific native SDK integration coverage | -| integration/primitive_edges_executes_native_primitive_and_nullable_edges | - | - | - | - | - | y | - | - | - | - | csharp | exercises C#-specific native SDK integration coverage | -| integration/release_abi_lifetimes_match_current_native_library | - | - | - | - | - | y | - | - | - | - | csharp | exercises the C# release ABI probe before publication | -| integration/stdlib_resources_executes_native_typed_resource_apis_lifetimes_and_state | - | - | - | - | - | y | - | - | - | - | csharp | exercises C#-specific native SDK integration coverage | -| integration/stdlib_structurals_executes_native_stdlib_structural_roundtrips | - | - | - | - | - | y | - | - | - | - | csharp | exercises C#-specific native SDK integration coverage | -| integration/streaming_executes_generated_native_stream_and_request_failure | - | - | - | - | - | y | - | - | - | - | csharp | exercises C#-specific native SDK integration coverage | -| integration/type_roundtrips_executes_nominals_collections_defaults_and_unions | - | - | - | - | - | y | - | - | - | - | csharp | exercises C#-specific native SDK integration coverage | -| llm_functions/a_runtime_declarations_type_value_holds_it_and_states_it_again | - | - | - | - | - | - | - | - | y | - | java | pins the Java BamlType holding a runtime declaration's engine reference | -| llm_functions/constructs_a_live_spec_without_a_synthetic_fqn | - | y | y | y | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | validates TypeScript's live FunctionSpec metadata surface | -| llm_functions/dynamic_runtime_stream_identity_and_flat_projection_parity | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | validates Python BamlRuntimeValue identity through the generated sync stream surface | -| llm_functions/dynamic_runtime_stream_is_an_elegant_async_iterable | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | validates Python's generated async-iterable stream surface | -| llm_functions/flat_stream_calls_exact_companion_fqn | - | - | - | - | y | - | - | - | - | - | cpp | C++-only exact flat-stream companion dispatch check. | -| llm_functions/flat_stream_controls_are_typed_options | - | - | - | - | - | - | - | y | - | - | go | pins the Go generator's typed option surface for flat stream controls. | -| llm_functions/function_spec_helpers_accept_invocation_controls | y | - | - | - | - | - | - | - | - | - | all | | -| llm_functions/function_spec_helpers_accept_invocation_controls_async | y | - | - | - | - | - | - | - | - | - | all | | -| llm_functions/function_spec_parse_replaces_the_parse_companion | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | validates Python's generated FunctionSpec parse surface | -| llm_functions/function_spec_parse_returns_closed_enum | - | - | - | - | - | - | - | y | - | - | go | pins Go's closed-enum FunctionSpec decoder surface. | -| llm_functions/function_spec_parse_returns_runtime_error_for_invalid_output | - | - | - | - | - | - | - | y | - | - | go | pins Go's FunctionSpec parse-error translation. | -| llm_functions/function_spec_parse_returns_typed_class_and_fills_missing_nullable_field | - | - | - | - | - | - | - | y | - | - | go | pins Go's typed FunctionSpec class decoder surface. | -| llm_functions/function_spec_prompt_is_accepted_as_an_argument | - | - | - | - | y | - | - | - | - | - | cpp | pins that a C++ prompt crosses back as an argument. | -| llm_functions/function_spec_prompt_uses_generated_ai_prompt_type_and_is_reusable | - | - | - | - | - | - | - | y | - | - | go | pins the Go FunctionSpec prompt facade and reusable context behavior. | -| llm_functions/keeps_a_portable_prompt_reusable_across_engine_reentry | - | y | y | y | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | validates TypeScript's portable Prompt and Image wrappers | -| llm_functions/legacy_function_companion_wire_bindings_are_absent | - | - | - | - | - | - | - | y | - | - | go | pins the Go generator's emitted wire-binding names. | -| llm_functions/main_baml_sdk_lorem_and_baml_sdk_ipsum_are_reachable | - | y | y | y | - | - | - | - | - | - | all | | -| llm_functions/main_classify_sentiment_factory_bindings | y | - | - | - | - | - | y | - | y | - | all | | -| llm_functions/main_extract_resume_factory_bindings | y | - | - | - | - | - | y | - | y | - | all | | -| llm_functions/main_extract_resume_operation_bindings | y | - | - | - | - | - | - | - | y | - | python_pydantic2, java | pins Python and Java flat operation binding spellings | -| llm_functions/main_extract_resume_spec_and_stream_bindings | - | - | - | - | - | - | y | - | - | - | rust | pins Rust generated spec and stream binding spellings | -| llm_functions/main_flat_stream_controls_live_on_the_stream_options | - | - | - | - | - | - | - | - | y | - | all | | -| llm_functions/main_function_spec_prompt_is_portable_and_reusable | - | - | - | - | - | - | - | - | y | - | all | | -| llm_functions/main_ipsum_classify_sentiment_sync_plus_async_factories_are_callable | - | y | y | y | - | - | - | - | - | - | all | | -| llm_functions/main_ipsum_sentiment_enum_has_positive_negative_neutral_members | - | y | y | y | - | - | - | - | - | - | all | | -| llm_functions/main_ipsum_sentiment_enum_shape | y | - | - | - | - | - | y | - | y | y | all | | -| llm_functions/main_lorem_extract_resume_exposes_flat_spec_and_stream_bindings | - | y | y | y | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | pins TypeScript's dollar-preserving flat stream binding names | -| llm_functions/main_lorem_extract_resume_sync_plus_async_factories_are_callable | - | y | y | y | - | - | - | - | - | - | all | | -| llm_functions/main_lorem_resume_class_shape | y | - | - | - | - | - | y | - | y | - | all | | -| llm_functions/main_lorem_resume_is_reachable | - | y | y | y | - | - | - | - | - | - | all | | -| llm_functions/main_lorem_streaming_doc_class_shape | y | - | - | - | - | - | y | - | y | - | all | | -| llm_functions/main_lorem_streaming_doc_is_reachable | - | y | y | y | - | - | - | - | - | - | all | | -| llm_functions/main_lorem_streaming_extract_exposes_flat_spec_and_stream_bindings | - | y | y | y | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | pins TypeScript's dollar-preserving flat streaming binding names | -| llm_functions/main_lorem_streaming_extract_sync_plus_async_factories_are_callable | - | y | y | y | - | - | - | - | - | - | all | | -| llm_functions/main_namespaces_reachable_via_explicit_import | y | - | - | - | - | - | y | - | y | - | all | | -| llm_functions/main_nullable_model_field_can_be_omitted | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | pins the Python generator's nullable-field `= None` default | -| llm_functions/main_replay_server_namespace_bindings | y | - | - | - | - | - | y | - | y | - | all | | -| llm_functions/main_root_imports_cleanly | y | y | y | y | - | - | y | - | y | - | all | | -| llm_functions/main_spec_replaces_prompt_parse_and_request_companions | - | - | - | - | - | - | y | - | - | - | rust | validates Rust's generated FunctionSpec prompt, parse, and request surface | -| llm_functions/main_streaming_extract_factory_bindings | y | - | - | - | - | - | y | - | y | - | all | | -| llm_functions/main_streaming_extract_operation_bindings | y | - | - | - | - | - | - | - | y | - | python_pydantic2, java | pins Python and Java flat streaming operation binding spellings | -| llm_functions/main_streaming_extract_spec_and_stream_bindings | - | - | - | - | - | - | y | - | - | - | rust | pins Rust generated streaming spec and stream binding spellings | -| llm_functions/main_types_and_bindings_reachable | - | - | - | - | - | - | - | - | - | y | all | | -| llm_functions/on_event_plain_call_delivers_events | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | host on_event listener coverage lands Python-first; other SDKs port separately | -| llm_functions/on_event_plain_call_delivers_model_call_model | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | host on_event listener coverage lands Python-first; other SDKs port separately | -| llm_functions/on_event_plain_call_raising_listener_does_not_fail | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | host on_event listener coverage lands Python-first; other SDKs port separately | -| llm_functions/on_event_stream_delivers_settle_events | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | host on_event listener coverage lands Python-first; other SDKs port separately | -| llm_functions/on_event_stream_raising_listener_does_not_fail | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | host on_event listener coverage lands Python-first; other SDKs port separately | -| llm_functions/projects_static_and_instance_llm_methods_without_invoking_a_provider | - | y | y | y | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | validates TypeScript static and instance operation member spellings | -| llm_functions/prompt_is_reusable_and_media_survives_request_preview | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | validates Python's portable Prompt and media wrapper surface | -| llm_functions/recording_lands_in_cwd_project | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | native Python recording lands where `baml query --local` reads | -| llm_functions/spec_projection_honors_cancellation | - | - | - | - | - | - | - | y | - | - | go | pins Go context cancellation through the Spec projection. | -| llm_functions/stream_async_for_keeps_context_after_creator_returns | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python stream lifetime/context regression | -| llm_functions/stream_contexts_are_isolated_during_concurrent_iteration | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python concurrent stream context isolation | -| llm_functions/stream_final_caches_only_settled_errors | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python final caching must distinguish results from control flow | -| llm_functions/stream_final_keeps_context_and_allows_a_step_patch | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python sync stream defaults and explicit step patches | -| llm_functions/stream_final_parse_failure_is_preserved_after_eof | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python EOF settlement retains final parse errors | -| llm_functions/stream_final_timeout_is_not_cached | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python task cancellation must not poison later final awaits | -| llm_functions/stream_keeps_creation_cancel_token | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python stream cancellation survives constructor return | -| llm_functions/stream_precancelled_admission_preserves_reservation | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python stream reservations must survive rejected admission | -| llm_functions/stream_recording_parent_chain | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | native Python recording ancestry, duration, values and step-count audit | -| llm_functions/streaming_e2e_async_class_typed_next_async_yields_10_partials | - | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| llm_functions/streaming_e2e_async_next_async_yields_10_partials | - | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| llm_functions/streaming_e2e_baml_driven_collect_keeps_the_s_stream_finished_union_engine_side | - | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| llm_functions/streaming_e2e_baml_driven_collect_returns_the_final_doc | - | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| llm_functions/streaming_e2e_next_yields_10_doc_partials_final_is_a_typed_streaming_doc | - | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| llm_functions/streaming_e2e_next_yields_10_partials_and_drains_to_stream_finished | - | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| llm_functions/streaming_e2e_stream | y | - | - | - | - | - | y | - | y | y | all | | -| llm_functions/streaming_e2e_stream_async | y | - | - | - | - | - | y | - | y | y | all | | -| llm_functions/streaming_e2e_stream_collect_in_baml | y | - | - | - | - | - | y | - | y | y | all | | -| llm_functions/streaming_e2e_stream_doc | y | - | - | - | - | - | y | - | y | y | all | | -| llm_functions/streaming_e2e_stream_doc_async | y | - | - | - | - | - | y | - | y | y | all | | -| llm_functions/streaming_e2e_stream_doc_collect_in_baml | y | - | - | - | - | - | y | - | y | y | all | | -| package_edges/compile_cross_package_types_compile | - | - | - | - | - | - | - | y | - | - | all | | -| package_edges/compile_llm_projection_default_overrides | - | - | - | - | - | - | - | y | - | - | go | pins Go generator options for authored Spec and Stream defaults. | -| type_shapes/alias_container_composition_and_defaults | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/alias_package_scope_collisions_compile_and_run | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/aliases_round_trip_alias_container | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/aliases_round_trip_maybe_rec | - | - | - | - | y | - | - | - | - | - | all | | -| type_shapes/aliases_round_trip_rec_list | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/aliases_round_trip_string_list | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/bridge_handles_media_clones_ordinary_owners_and_wire_keys_independently | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/bridge_handles_media_clones_ownership_through_to_handle_and_from_handle | - | y | y | y | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | exercises TypeScript bridge media handle ownership APIs | -| type_shapes/bridge_handles_media_constructs_url_file_and_base64_descriptors | - | y | y | y | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | exercises TypeScript bridge media descriptor APIs | -| type_shapes/bridge_handles_media_exposes_stable_seed_tags | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/bridge_handles_media_keeps_host_value_tags_outside_ordinary_clone_and_release | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/bridge_handles_media_normalizes_key_halves_losslessly_and_returns_defensive_key_objects | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/bridge_handles_media_rejects_an_invalid_ordinary_key_only_when_an_operation_resolves_it | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/bridge_surface_exports_the_same_runtime_values_in_node_browsers_and_workers | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/bridge_surface_preserves_the_public_constructor_names | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/class_refs_make_outer | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/class_refs_round_trip_async_by_value_or_reference_rust_only | - | - | - | - | - | - | y | - | - | - | rust | Rust class parameters take `impl Borrow`, so callers pass a value or a reference | -| type_shapes/class_refs_round_trip_by_value_or_reference_rust_only | - | - | - | - | - | - | y | - | - | - | rust | Rust class parameters take `impl Borrow`, so callers pass a value or a reference | -| type_shapes/class_refs_round_trip_inner | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/class_refs_round_trip_outer | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/closed_union_zero_value_returns_an_input_error | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/complex_models_round_trip_complex_profile_accepts_plain_object_literals_no_class_constructors | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/complex_models_round_trip_complex_profile_preserves_deeply_nested_mixed_shape_class | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/container_recursive_class_round_trip | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/declared_enum_functions_and_class_round_trip | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/deep_namespace_thing_reachable | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/defaulted_enum_argument_and_invalid_values | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/dynamic_union_accepts_natural_go_integers | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/dynamic_union_candidates_round_trip_as_concrete_go_values | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/dynamic_union_delegates_semantic_validation_to_baml | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/dynamic_union_nested_containers_round_trip | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/dynamic_union_of_containers_uses_selected_type_metadata | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/dynamic_union_rejects_unserializable_go_values_in_bridge | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/enum_composition_matrix_round_trip | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/enum_package_scope_collisions_compile_and_run | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/enums_pick_positive | y | y | y | y | y | - | y | - | y | - | all | | -| type_shapes/enums_pick_sentiment | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/enums_round_trip_enums | y | y | y | y | y | - | y | - | y | - | all | | -| type_shapes/enums_round_trip_sentiment | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/enums_round_trip_sentiment_positive | y | y | y | y | y | - | y | - | y | - | all | | -| type_shapes/every_supported_leaf_through_lists_and_maps | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/forward_refs_round_trip_g_node_int | y | y | y | y | - | - | y | - | y | - | all | | -| type_shapes/forward_refs_round_trip_node_symbol_exists | - | - | - | - | y | - | - | - | - | y | all | | -| type_shapes/forward_refs_round_trip_other | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/forward_refs_round_trip_rec_list | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/forward_refs_round_trip_rec_list_with_other | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/generic_generic | y | y | y | y | - | - | y | - | y | y | all | | -| type_shapes/generic_generic_wrapper_get_value | y | - | - | - | - | - | y | - | y | y | all | | -| type_shapes/generic_wrapper_get_value | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/generics_round_trip_box_int | y | y | y | y | - | - | y | - | y | y | all | | -| type_shapes/generics_round_trip_differing_instantiation | y | y | y | y | - | - | y | - | y | y | all | | -| type_shapes/generics_round_trip_generic_binary_tree_int | y | y | y | y | - | - | y | - | y | y | all | | -| type_shapes/generics_round_trip_generic_linked_list_int | y | y | y | y | - | - | y | - | y | y | all | | -| type_shapes/generics_round_trip_nested_generics | y | y | y | y | - | - | y | - | y | y | all | | -| type_shapes/generics_round_trip_wrapper_int | y | y | y | y | - | - | y | - | y | y | all | | -| type_shapes/handles_baml_fs_open_returns_a_typed_file_handle | - | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| type_shapes/handles_file_cursor_state_persists_across_calls | y | y | - | - | - | - | y | - | y | y | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| type_shapes/handles_http_get_response_fields_and_methods | y | y | - | - | - | - | y | - | y | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift | | -| type_shapes/handles_image_from_base64_roundtrips_payload | y | y | y | y | - | - | y | - | y | y | all | | -| type_shapes/handles_open_file_returns_file_handle | y | - | - | - | - | - | y | - | y | y | all | | -| type_shapes/host_created_media_round_trips_through_optional_list | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | validates Python host-created media wrapper encoding | -| type_shapes/host_from_file_reads_now_and_keeps_the_base_name | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | validates the Python media wrapper's from_file, which is BAML's from_file | -| type_shapes/json_interchange_both_reject_what_baml_rejects | - | - | - | - | - | - | y | - | - | - | rust | validates the Rust SDK's optional serde impls | -| type_shapes/json_interchange_deserialize_matches_baml | - | - | - | - | - | - | y | - | - | - | rust | validates the Rust SDK's optional serde impls | -| type_shapes/json_interchange_json_fields_match_baml | - | - | - | - | - | - | y | - | - | - | rust | validates the Rust SDK's optional serde impls | -| type_shapes/json_interchange_optional_fields_may_be_absent_or_null | - | - | - | - | - | - | y | - | - | - | rust | validates the Rust SDK's optional serde impls | -| type_shapes/json_interchange_serialize_matches_baml | - | - | - | - | - | - | y | - | - | - | rust | validates the Rust SDK's optional serde impls | -| type_shapes/json_interchange_serialize_matches_baml_outside_a_class | - | - | - | - | - | - | y | - | - | - | rust | validates the Rust SDK's optional serde impls | -| type_shapes/json_interchange_unions_take_the_first_arm_that_fits | - | - | - | - | - | - | y | - | - | - | rust | validates the Rust SDK's optional serde impls | -| type_shapes/lists_round_trip_empty_list | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/lists_round_trip_ints | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/lists_round_trip_list_container | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/lists_round_trip_optional_strings | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/lists_round_trip_union_list | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/literals_literal_values_convert_implicitly | - | - | - | - | y | - | - | - | - | - | all | | -| type_shapes/literals_return_literals | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/literals_round_trip_flag_mixed_literal_union | y | - | - | - | y | - | - | - | - | - | all | | -| type_shapes/literals_round_trip_literal42 | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/literals_round_trip_literal_draft | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/literals_round_trip_literal_escaped | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/literals_round_trip_literal_false | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/literals_round_trip_literal_true | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/literals_round_trip_literals | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/lorem_resume_reachable | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/main_all_namespaces_reachable | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/main_deep_namespace_thing_reachable | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/main_generated_models_ignore_extra_fields | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | validates Python Pydantic extra-field compatibility | -| type_shapes/main_lorem_resume_reachable | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/main_root_foo_reachable | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/main_root_imports_cleanly | y | y | y | y | - | - | y | - | y | - | all | | -| type_shapes/main_runtime_owned_builtin_leaves_expose_their_public_names | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/make_foo | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/make_outer | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/maps_round_trip_enum_keyed_map | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/maps_round_trip_list_valued_map | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/maps_round_trip_map_container | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/maps_round_trip_resume | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/maps_round_trip_sentiment | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/maps_round_trip_simple_map | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/media_constructors_and_accessors | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/media_from_file_reads_now_and_keeps_the_base_name | - | - | - | - | - | - | - | - | y | - | java | validates the Java media wrapper's from_file, which is BAML's from_file | -| type_shapes/media_nested_optional_and_containers | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/media_return_and_round_trip_all_kinds | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/media_return_audio | y | - | - | - | - | - | y | - | y | y | all | | -| type_shapes/media_return_audio_preserves_url_and_mime | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/media_return_image | y | - | - | - | - | - | y | - | y | y | all | | -| type_shapes/media_return_image_preserves_url_and_mime | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/media_return_pdf | y | - | - | - | - | - | y | - | y | y | all | | -| type_shapes/media_return_pdf_preserves_url_and_mime | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/media_return_video | y | - | - | - | - | - | y | - | y | y | all | | -| type_shapes/media_return_video_preserves_url_and_mime | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/media_round_trip_all_media | y | y | y | y | y | - | - | y | y | y | all | | -| type_shapes/media_round_trip_audio | y | - | - | - | - | - | y | - | y | y | all | | -| type_shapes/media_round_trip_audio_preserves_url_and_mime | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/media_round_trip_image | y | - | - | - | - | - | y | - | y | y | all | | -| type_shapes/media_round_trip_image_can_reuse_the_same_wrapper | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/media_round_trip_image_list | - | - | - | - | - | - | - | - | - | y | all | | -| type_shapes/media_round_trip_image_map | - | - | - | - | - | - | - | - | - | y | all | | -| type_shapes/media_round_trip_image_or_audio | - | - | - | - | - | - | - | - | y | y | all | | -| type_shapes/media_round_trip_media | y | - | - | - | - | - | y | - | y | y | all | | -| type_shapes/media_round_trip_media_or_image_class | y | y | y | y | y | - | - | y | y | y | all | | -| type_shapes/media_round_trip_media_preserves_all_four_media_fields | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/media_round_trip_media_shapes | - | - | - | - | - | - | - | - | - | y | all | | -| type_shapes/media_round_trip_optional_image_list | - | - | - | - | - | - | - | - | - | y | all | | -| type_shapes/media_round_trip_pdf | y | - | - | - | - | - | y | - | y | y | all | | -| type_shapes/media_round_trip_pdf_preserves_url_and_mime | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/media_round_trip_video | y | - | - | - | - | - | y | - | y | y | all | | -| type_shapes/media_round_trip_video_preserves_url_and_mime | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/media_uses_the_runtime_owned_wrapper_constructors | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/nested_class_round_trip | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/no_op | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/nullable_class_fields_round_trip | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/nullable_container_boundaries_round_trip | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/nullable_container_fields_round_trip | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/nullable_recursive_classes_round_trip | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/opaque_rust_type_default_and_host_callback_positions | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/opaque_rust_type_nested_containers_classes_and_null | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/opaque_rust_type_round_trips_and_remains_reusable | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/optional_omitted_nullable_keys_read_as_null | - | y | y | y | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | TypeScript object literals and constructor inits may leave nullable keys out | -| type_shapes/optional_omitted_required_key_is_rejected | - | y | y | y | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | only TypeScript has a type-level check that a required key is present | -| type_shapes/optional_round_trip_optional_container | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/optional_round_trip_optional_int | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/optional_round_trip_optional_resume | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/optional_round_trip_optional_union | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/optional_round_trip_resume | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/optional_top_level_round_trips | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/overlapping_list_arms_preserve_exact_kind_for_empty_and_nonempty_values | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/overlapping_literal_arms_round_trip_with_exact_kind | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/overlapping_map_arms_preserve_exact_kind_for_empty_and_nonempty_values | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/pick_positive | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/pick_sentiment | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/primitive_class_round_trip | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/primitive_literal_union_constructors_round_trip_with_exact_kind_and_value | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/primitives_return_bigint | y | - | - | - | y | - | - | - | y | - | all | | -| type_shapes/primitives_return_bool | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/primitives_return_float | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/primitives_return_int | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/primitives_return_null | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/primitives_return_string | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/primitives_round_trip_bigint | y | - | - | - | y | - | - | - | y | - | all | | -| type_shapes/primitives_round_trip_bool | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/primitives_round_trip_float | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/primitives_round_trip_float_accepts_int | y | - | - | - | y | - | y | - | y | - | all | | -| type_shapes/primitives_round_trip_float_refuses_an_int_on_the_wire | - | - | - | - | - | - | y | - | - | - | rust | only the low-level Rust entry point can put an int on the wire where a float is declared | -| type_shapes/primitives_round_trip_int | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/primitives_round_trip_int_async | - | - | - | - | - | - | - | - | - | y | all | | -| type_shapes/primitives_round_trip_null | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/primitives_round_trip_primitives | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/primitives_round_trip_primitives_float_field_accepts_int | y | - | - | - | y | - | y | - | y | y | all | | -| type_shapes/primitives_round_trip_string | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/primitives_round_trip_uint8_array | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/recursion_int_binary_tree_deeper_than_the_wire_is_refused | y | y | y | y | y | - | y | y | y | y | all | | -| type_shapes/recursion_round_trip_int_binary_tree | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/recursion_round_trip_mutual_recursion | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/recursion_round_trip_scc_t1_t2_t3 | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/recursion_round_trip_scc_t4_t5_t6 | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/required_container_round_trips | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/return_bool | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/return_float | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/return_int | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/return_literals | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/return_null | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/return_string | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/root_foo_reachable | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/root_imports_cleanly | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_bool | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_box_int | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_complex_profile_preserves_deeply_nested_mixed_shape_class | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_dedup | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_deep | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_deep_thing_from_a | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_deep_thing_from_lorem | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_differing_instantiation | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_empty_list | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_empty_list_preserves_selected_arm | - | - | - | - | - | - | - | - | - | y | all | | -| type_shapes/round_trip_enums | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_fizz_buzz_foo_bar | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_fizz_foo_bar | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_float | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_float_accepts_int_constant | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_foo | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_foo_bar | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_forward_ref_g_node_int | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_forward_ref_other | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_generic_binary_tree_int | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_generic_linked_list_int | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_inner | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_int | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_int_binary_tree | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_ints | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_ipsum | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_list_container | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_list_valued_map | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_literal42 | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_literal_draft | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_literal_escaped | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_literal_false | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_literal_true | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_literals | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_lorem_resume | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_lorem_resume_from_ipsum | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_map_resume | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_map_sentiment | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_mutual_recursion | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_nested_generics | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_null | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_null_to_end | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_optional_container | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_optional_int | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_optional_plus_null | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_optional_resume | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_optional_strings | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_optional_union | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_outer | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_primitives | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_primitives_float_field_accepts_int_constant | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_required_resume | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_resume_or_http_response | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_root_foo_from_ab | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_root_foo_from_lorem | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_scct1_t2_t3 | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_scct4_t5_t6 | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_sentiment | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_sentiment_positive | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_simple_map | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_singleton_unwrap | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_string | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_string_list | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_thing_from_ab | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_uint8_array | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_union_container | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_union_list | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_union_t | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/round_trip_wrapper_int | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/routing_make_foo | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/routing_round_trip_deep_thing_from_a | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/routing_round_trip_deep_thing_from_lorem | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/routing_round_trip_foo | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/routing_round_trip_lorem_resume_from_ipsum | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/routing_round_trip_resume | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/routing_round_trip_resume_or_http_response | y | y | y | y | - | - | - | - | y | y | all | | -| type_shapes/routing_round_trip_root_foo | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/routing_round_trip_root_foo_from_ab | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/routing_round_trip_thing_from_ab | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/supported_namespaces_reachable | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/symbol_collisions_round_trip_deep | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/symbol_collisions_round_trip_fizz_buzz_foo_bar | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/symbol_collisions_round_trip_fizz_foo_bar | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/symbol_collisions_round_trip_foo_bar | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/symbol_collisions_round_trip_ipsum | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/transparent_alias_functions_round_trip | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/type_alias_declared_before_classes_is_importable | y | - | - | - | - | - | - | - | - | - | python_pydantic2 | validates Python-specific generated type alias ordering | -| type_shapes/typemap_is_installed_during_root_module_evaluation | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/typemap_preserves_user_enum_and_generic_mappings | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/typemap_resolves_every_runtime_owned_base_to_one_bridge_constructor_identity | - | y | y | y | - | - | - | - | - | - | all | | -| type_shapes/union_aliases_are_transparent_and_flatten_before_thresholding | - | - | - | - | - | - | - | y | - | - | all | | -| type_shapes/unions_consumption_surfaces | - | - | - | - | - | - | - | - | - | y | all | | -| type_shapes/unions_match_dispatches_by_type | - | - | - | - | y | - | - | - | - | - | all | | -| type_shapes/unions_round_trip_dedup | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/unions_round_trip_null_to_end | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/unions_round_trip_optional_plus_null | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/unions_round_trip_singleton_unwrap | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/unions_round_trip_str_or_int_list | y | - | - | - | y | - | y | - | y | - | all | | -| type_shapes/unions_round_trip_t | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/unions_round_trip_union_container | y | y | y | y | y | - | y | - | y | y | all | | -| type_shapes/unions_union_is_a_plain_std_variant | - | - | - | - | y | - | - | - | - | - | all | | -| type_shapes/void_no_op | y | y | y | y | y | - | y | - | y | y | all | | -| unsupported_only/compile_unsupported_only_package_compiles | - | - | - | - | - | - | - | y | - | - | all | | +| Test case | python_pydantic2 | typescript_node | typescript_web_chromium | typescript_web_cloudflare_workers | cpp | csharp | rust | go | java | swift | ruby_sorbet | Required in | Reason | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| docstrings_etc/class_doc_summary_and_attributes | - | - | - | - | - | - | - | y | - | - | - | all | | +| docstrings_etc/enum_doc_summary_and_members | - | - | - | - | - | - | - | y | - | - | - | all | | +| docstrings_etc/enum_summary_only_omits_member_comments | - | - | - | - | - | - | - | y | - | - | - | all | | +| docstrings_etc/go_codegen_function_doc_comment | - | - | - | - | - | - | - | y | - | - | - | all | | +| docstrings_etc/imports | - | - | - | - | - | - | - | y | - | - | - | all | | +| docstrings_etc/main_class_doc_summary_and_attributes_section | y | y | - | - | y | - | y | - | y | - | y | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| docstrings_etc/main_class_doc_summary_present | - | - | - | - | - | - | - | - | - | y | - | all | | +| docstrings_etc/main_enum_and_variant_docs_attached | - | - | - | - | - | - | - | - | - | y | - | all | | +| docstrings_etc/main_enum_doc_summary_and_members_section | y | y | - | - | y | - | y | - | y | - | y | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| docstrings_etc/main_enum_summary_only_omits_members_section | y | - | - | - | y | - | y | - | y | - | y | all | | +| docstrings_etc/main_field_docs_attached | - | - | - | - | - | - | - | - | - | y | - | all | | +| docstrings_etc/main_imports_symbols_reachable | y | y | y | y | - | - | y | - | y | y | y | all | | +| docstrings_etc/main_multi_line_class_doc_preserved | - | - | - | - | - | - | - | - | - | y | - | all | | +| docstrings_etc/main_no_inline_field_or_variant_doc_artifacts | y | y | - | - | y | - | y | - | y | - | y | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| docstrings_etc/main_undocumented_field_listed_as_bare_name_under_attributes | y | - | - | - | y | - | y | - | y | - | y | all | | +| docstrings_etc/no_inline_field_or_variant_doc_artifacts | - | - | - | - | - | - | - | y | - | - | - | all | | +| docstrings_etc/undocumented_field_has_no_doc_artifact | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/ambient_context_survives_await_rust_only | - | - | - | - | - | - | y | - | - | - | - | rust | Rust task scopes and lazy future cancellation are specific to Rust | +| function_calls/async_callback_can_use_originating_loop_resources_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | +| function_calls/async_callback_can_use_originating_loop_resources_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/async_callback_inherits_application_context_across_suspension_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | +| function_calls/async_callback_preserves_ambient_frame_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node async carrier, native signals, and entry-time snapshots are specific to TypeScript | +| function_calls/async_callback_reenters_async_baml_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python loop/ContextVar and Node AsyncLocalStorage assertions | +| function_calls/async_callback_reenters_sync_baml_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | sync callback re-entry differs between Python and Node | +| function_calls/async_entry_sync_callback_inherits_application_context_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | +| function_calls/async_host_runs_in_calling_task_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | covers Python asyncio task and event-loop identity | +| function_calls/async_reentry_preserves_async_local_storage_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/async_stream_schema_failure_is_parse_failed | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | exercises the Python asyncio streaming bridge | +| function_calls/baml_closure_decodes_multiple_args_and_structured_return_values | y | y | y | y | y | - | y | y | y | y | y | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, cpp, rust, go, java, swift, ruby_sorbet | C# covers this canonical behavior in its native integration harness | +| function_calls/baml_closure_is_a_native_callable_with_host_language_arguments | y | y | y | y | y | - | y | y | y | y | y | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, cpp, rust, go, java, swift, ruby_sorbet | C# covers this canonical behavior in its native integration harness | +| function_calls/baml_closure_is_reusable_and_retains_mutable_captures | y | y | y | y | y | - | y | y | y | y | y | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, cpp, rust, go, java, swift, ruby_sorbet | C# covers this canonical behavior in its native integration harness | +| function_calls/baml_error_carries_baml_trace | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/baml_time_class_and_field_wire_names_are_exact | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/baml_time_constructors_and_parsers_return_lossless_internal_values | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/baml_time_go_constructed_values_round_trip_at_numeric_boundaries | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/baml_time_malformed_values_fail_at_the_earliest_typed_boundary | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/baml_time_nested_containers_and_baml_field_inspection | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/baml_time_nullable_and_defaulted_positions | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/baml_time_raw_class_transport_does_not_enforce_semantic_invariants | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/baml_trace_is_embedded_in_go_error_string | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/boolean_timeout_rejected_python_only | y | - | - | - | - | - | - | - | - | - | - | all | | +| function_calls/bounded_class_parameter_unused_by_static_method_is_sent | - | - | - | - | - | - | - | - | - | - | y | ruby_sorbet | Ruby named _types must include bounded-only static class parameters | +| function_calls/callable_entry_invokes_callback | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/callable_entry_waits_for_callback_completion | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/callback_bound_method_adoption_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | covers Python bound method identity via its exact SDK function | +| function_calls/callback_captures_internal_baml_context | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/callback_frame_and_reentry | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/callback_frame_is_installed_and_restored | - | - | - | - | - | - | y | - | - | - | - | all | | +| function_calls/callback_marker_adopts_once_during_recursion | y | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/callback_marker_adopts_sync_body | y | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/callback_marker_concurrent_reuse_and_later_direct_call | y | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/callback_marker_context_precedence | y | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/callback_marker_defaults_override_inherited_context | y | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/callback_marker_only_outer_wrapper_adopts | y | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/callback_marker_sync_entry_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | covers Python synchronous BAML entry and inline callback dispatch | +| function_calls/callback_reenters_baml | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/callback_reuse_across_event_loop_turns_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/callback_reuse_after_previous_application_loop_is_closed_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | +| function_calls/callback_suppresses_task_cancellation_and_returns_late_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python callback suppression of cancellation and late results | +| function_calls/callback_task_cancelled_before_first_execution_completes_call_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python task factory cancellation before callback execution | +| function_calls/callback_third_party_wrapper_is_not_adopted | y | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/callback_throws_caught_and_replaced_makes_the_function_infallible | - | - | - | - | - | - | y | - | - | - | - | rust | validates Rust-specific inferred callback error unions | +| function_calls/callback_throws_caught_then_rethrown_value_is_the_replacement_union | - | - | - | - | - | - | y | - | - | - | - | rust | validates Rust-specific inferred callback error unions | +| function_calls/callback_throws_rethrown_carries_the_effect_param_into_the_error_union | - | - | - | - | - | - | y | - | - | - | - | rust | validates Rust-specific inferred callback error unions | +| function_calls/cancel_controls_with_cancelled_context_go_only | - | - | - | - | - | - | - | y | - | - | - | go | Go cancellation controls remain usable with a canceled context | +| function_calls/cancellation_async_call_returns_none | y | - | - | - | y | - | y | - | y | y | y | all | | +| function_calls/cancellation_async_cancel_skips_later_step | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | drives Python asyncio cancellation (task.cancel / wait_for) | +| function_calls/cancellation_async_cancel_via_asyncio_timeout | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/cancellation_async_cancel_via_call_context | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/cancellation_async_cancel_via_future_cancel | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/cancellation_async_cancel_via_task_cancel | y | - | - | - | - | - | y | - | y | y | y | all | | +| function_calls/cancellation_async_cancel_via_task_group_sibling | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/cancellation_async_cancel_via_timeout_race | - | - | - | - | - | - | - | - | - | y | - | all | | +| function_calls/cancellation_can_pre_abort_async_baml_cancellation | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/cancellation_cancels_an_async_generated_free_function | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/cancellation_cancels_an_async_generated_instance_method | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/cancellation_detaches_a_completed_call_before_aborting_the_remaining_call | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/cancellation_future_destruction_detaches | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/cancellation_future_second_get_throws_future_error | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/cancellation_future_wait_for_times_out_while_in_flight | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/cancellation_future_wait_then_get | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/cancellation_immediately_cancels_every_call_attached_after_abort | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/cancellation_pre_aborts_a_generated_synchronous_call | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/cancellation_surfaces_a_reused_call_context_as_abort_error_with_baml_reason | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/cancellation_surfaces_async_cancellation_as_abort_error_with_baml_reason | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/cancellation_surfaces_cancellation_through_promise_all | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/cancellation_surfaces_sync_pre_aborted_cancellation_as_abort_error | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/cancellation_sync_call_returns_none | y | - | - | - | y | - | y | - | y | y | y | all | | +| function_calls/cancellation_sync_cancel_via_call_context | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/cancelled_waiter_does_not_end_host_execution | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/cancelled_waiter_keeps_host_context_through_cleanup_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python task unwinding and ContextVar semantics | +| function_calls/cancelled_waiter_keeps_host_resource_until_promise_exit_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node AsyncResource destruction and AsyncLocalStorage | +| function_calls/cancelled_waiter_preserves_callback_context | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/cancelled_waiter_stays_cancelled_when_callback_returns_late | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/cancelling_call_delivers_cancellation_and_allows_callback_cleanup_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python callback task and event loop ownership | +| function_calls/canonical_json_class_union_uses_declared_field_codecs | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/canonical_json_composes_through_containers_and_classes | - | - | - | - | - | - | y | y | - | - | - | all | | +| function_calls/canonical_json_defaults_and_callbacks | - | - | - | - | - | - | y | y | - | - | - | all | | +| function_calls/canonical_json_dynamic_union | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/canonical_json_rejects_extensions_before_dispatch | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/canonical_json_round_trips_at_top_level_and_through_alias | - | - | - | - | - | - | y | y | - | - | - | all | | +| function_calls/caught_ruby_exception_decodes_as_data_and_round_trips_with_identity | - | - | - | - | - | - | - | - | - | - | y | ruby_sorbet | Ruby exceptions returned as HostCallable data retain their original opaque object | +| function_calls/child_cancellation_does_not_cancel_input | y | y | y | y | - | - | y | - | - | - | - | all | | +| function_calls/clean_exit_helper_process | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/clean_exit_terminates_process_with_code | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/closure_call_async_can_be_reused_after_creation_loop_closes_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python returned closure reuse across application loops | +| function_calls/closure_call_async_cancels_its_retained_callback_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python closure async entry cancellation delivery | +| function_calls/closure_call_async_preserves_binding_and_exception_identity_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python closure async entry argument and error semantics | +| function_calls/closure_call_async_uses_invoking_loop_and_context_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python explicit async closure entry and context capture | +| function_calls/coawait_co_await_cancelled_call_throws_cancelled | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/coawait_co_await_completed_future_fast_path | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/coawait_co_await_pending_future_resumes | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/coawait_co_await_throws_typed_into_the_coroutine | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/coawait_co_await_yields_the_decoded_value | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/coawait_uncaught_coroutine_exception_reaches_join | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/completed_call_can_reuse_callback | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/completed_call_does_not_pin_reused_callback_to_old_context_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | +| function_calls/composite_token_observes_every_source | y | y | y | y | - | - | y | - | y | - | y | all | | +| function_calls/concurrent_calls_isolate_async_local_storage_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/concurrent_calls_keep_callback_results_independent | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/concurrent_calls_share_callback_without_sharing_context_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | +| function_calls/concurrent_host_invocations_isolate_context | y | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/concurrent_invocations_isolate_context | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/concurrent_reservation_attaches_once | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/configuration_snapshot_is_not_live | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/cooperative_abort_allows_callback_cleanup_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/cooperative_abort_cleans_up_retained_callback_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/coroutine_entry_uses_execution_context_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | covers Python coroutine creation versus first execution | +| function_calls/current_context_returns_detached_snapshot | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/deadline_reentry_does_not_reset_budget | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/dropping_started_future_cancels_rust_only | - | - | - | - | - | - | y | - | - | - | - | rust | Rust task scopes and lazy future cancellation are specific to Rust | +| function_calls/dynamic_application_map_preserves_control_like_keys | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/dynamic_call_accepts_controls | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/dynamic_call_async_accepts_controls | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/dynamic_name_and_handle_accept_controls | - | - | - | - | - | - | y | - | y | - | - | all | | +| function_calls/dynamic_name_and_handle_accept_controls_async | - | - | - | - | - | - | y | - | - | - | - | all | | +| function_calls/dynamic_type_bindings_async_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python dynamic type bindings accept Python classes and reflected BamlType handles | +| function_calls/empty_class_self_round_trip | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/empty_controls | y | y | y | y | - | - | y | - | y | - | y | all | | +| function_calls/empty_json_containers_round_trip | - | - | - | - | - | - | - | - | y | - | - | all | | +| function_calls/encoding_time_counts_against_deadline_python_only | y | - | - | - | - | - | - | - | - | - | - | all | | +| function_calls/error_call_cancellation_preserves_context_identity | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/error_message_prefers_detail_and_falls_back_to_the_type_name | - | - | - | - | - | - | - | - | - | - | y | ruby_sorbet | Ruby error messages use a detail field and never inspect a struct | +| function_calls/error_string_is_non_empty | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/errors_async_sibling_throws_typed | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/errors_baml_error_carries_baml_trace | y | - | - | - | - | - | y | - | y | y | y | all | | +| function_calls/errors_baml_trace_spliced_into_python_traceback | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/errors_cancellation_surfaces_as_baml_panic | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/errors_clean_exit_terminates_process_with_code | - | - | - | - | - | - | y | - | - | - | - | all | | +| function_calls/errors_clean_exit_terminates_process_with_code_0 | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/errors_clean_exit_terminates_process_with_code_7 | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/errors_host_invalid_argument_wraps_baml_errors_invalid_argument | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/errors_stdlib_error_surfaces_as_baml_error | y | - | - | - | - | - | y | - | y | y | y | all | | +| function_calls/errors_stdlib_error_surfaces_typed | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/errors_str_is_non_empty | y | - | - | - | - | - | y | - | y | y | y | all | | +| function_calls/errors_typed_throw_is_still_a_baml_error | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/errors_union_throws_preserves_class_name | y | y | y | y | y | - | y | - | y | y | y | all | | +| function_calls/errors_user_panic_surfaces_as_baml_panic | y | - | - | - | - | - | y | - | y | y | y | all | | +| function_calls/errors_user_throw_surfaces_declared_instance | y | - | - | - | y | - | y | - | y | y | y | all | | +| function_calls/explicit_controls_are_applied | - | - | - | - | - | - | - | - | - | y | - | all | | +| function_calls/explicit_controls_are_applied_async | - | - | - | - | - | - | - | - | - | y | - | all | | +| function_calls/failed_admission_does_not_consume_reservation | - | - | - | - | - | - | y | - | y | - | - | all | | +| function_calls/four_call_forms | y | y | y | y | - | - | y | - | y | y | y | all | | +| function_calls/four_call_forms_async | y | y | y | y | - | - | y | - | y | y | y | all | | +| function_calls/generic_calls_choose_explicit | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/generic_calls_consume_int_wrapper_baseline | y | - | - | - | - | - | y | - | y | y | y | all | | +| function_calls/generic_calls_extract_explicit | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/generic_calls_generic_free_fn_requires_binding | y | - | - | - | - | - | y | - | y | y | y | all | | +| function_calls/generic_calls_generic_static_infers_binding | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/generic_calls_genericbox_get_explicit | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/generic_calls_genericbox_new_static_explicit | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/generic_calls_genericbox_pair_with_explicit | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/generic_calls_identity_async_explicit | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/generic_calls_identity_explicit | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/generic_calls_identity_explicit_class_cannot_restate_an_instance | - | - | - | - | - | - | - | - | y | - | - | all | | +| function_calls/generic_calls_instance_method_unparameterized_receiver_raises | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/generic_calls_list_head_explicit | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/generic_calls_make_int_box_reified | y | - | - | - | - | - | y | - | y | y | y | all | | +| function_calls/generic_calls_make_int_container_reified | y | - | - | - | - | - | y | - | y | y | y | all | | +| function_calls/generic_calls_make_int_str_bool_triple_reified | y | - | - | - | - | - | y | - | y | y | y | all | | +| function_calls/generic_calls_make_nested_box_reified | y | - | - | - | - | - | y | - | y | y | y | all | | +| function_calls/generic_calls_make_triple_explicit | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/generic_calls_named_static_distinct_typevar_names | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/generic_calls_one_type_arg_explicit | y | - | - | - | - | - | y | - | y | y | y | all | | +| function_calls/generic_calls_optional_only_unset_names_its_type_arg | - | - | - | - | - | - | - | - | - | y | - | all | | +| function_calls/generic_calls_parse_as_explicit | y | - | - | - | - | - | y | - | y | y | y | all | | +| function_calls/generic_calls_read_items_explicit | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/generic_calls_second_of_explicit | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/generic_calls_subscript_wrong_arity_raises | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/generic_calls_tag_or_value_explicit | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/generic_calls_two_type_args_explicit | y | - | - | - | - | - | y | - | y | y | y | all | | +| function_calls/generic_calls_wrap_explicit | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/generic_class_instantiated_at_int | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/generic_class_with_a_json_field_round_trips | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/generic_classes_containers_and_concrete_outputs | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/generic_defaulted_parameters_use_option_setters | - | - | - | - | - | - | - | y | - | - | - | go | pins Go's functional-option setters, generic when the defaulted value mentions a type parameter | +| function_calls/generic_host_callbacks_share_the_function_type_parameters | - | - | - | - | - | - | - | y | - | - | - | go | pins Go's generic host-callback adapters | +| function_calls/generic_identity_accepts_nested_dynamic_containers | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/generic_identity_inference_and_explicit_types | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/generic_inference_apply_closure_poisons_typevars_must_specify | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_apply_closure_typevars_specified_succeeds | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_body_only_var_still_requires_binding | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_choose_divergent_generic_instances_union | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_choose_infers_divergent_union | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_choose_infers_unified_typevar | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_choose_union_outside_container_is_sound | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_combine_invariant_class_arg_conflict_rejects | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_default_only_value_position | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | validates Python default-argument stubs and runtime TypeVar inference | +| function_calls/generic_inference_elem_type_heterogeneous_array_unifies | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_elem_type_homogeneous_array_is_single_type | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_elem_type_three_way_heterogeneous_array_unifies | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_extract_fully_unbound_nested_pair_recovers_all_vars | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_extract_infers_four_typevars_from_nesting | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_first_or_empty_list_requires_binding | y | - | - | - | - | - | - | - | y | - | y | python_pydantic2, java, ruby_sorbet | a dynamic host has no static element type for an empty container; the typed SDKs state theirs and round-trip | +| function_calls/generic_inference_first_or_empty_list_round_trips_none | - | - | - | - | - | - | - | - | - | y | - | all | | +| function_calls/generic_inference_first_or_empty_list_with_explicit_type_round_trips | - | - | - | - | - | - | - | - | - | y | - | swift | Swift names `T` statically, so an empty list cannot leave it unbound; this pins the explicit binding round trip | +| function_calls/generic_inference_first_or_nonempty_infers_element | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_generic_static_infers_own_typevar | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_genericbox_get_infers_class_var_from_receiver | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_genericbox_pair_with_infers_method_typevar | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_genericbox_pair_with_unbound_receiver_recovers_class_var | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_glue_invariant_and_covariant_agree_binds | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_glue_invariant_vs_covariant_conflict_rejects | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_host_only_object_not_encodable_from_python | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_identity_async_infers | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_identity_enum_round_trips | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_identity_infers_generic_instance | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_identity_infers_primitives | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_identity_infers_user_class | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_identity_nested_unbound_round_trips | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_identity_null_binds_null | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_identity_null_with_explicit_optional_type_round_trips | - | - | - | - | - | - | - | - | - | y | - | swift | Swift names `T` statically, so a null argument infers nothing here; this pins the explicit binding round trip | +| function_calls/generic_inference_identity_unbound_generic_instance_round_trips | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_list_head_infers_from_recursive_generic | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_list_head_unbound_recursive_recovers_t_from_fields | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_make_triple_full_subscript_contradicted_by_actual_rejects | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_make_triple_heterogeneous_list_element_unions | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_make_triple_infers_multiple_typevars | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_make_triple_partial_explicit_then_infer | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_make_triple_partial_subscript_requires_full_arity | y | - | - | - | - | - | - | - | y | - | y | python_pydantic2, java, ruby_sorbet | Ruby uses named _types hashes and has no callable-subscript arity surface | +| function_calls/generic_inference_make_triple_subscript_fully_bound | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_make_triple_types_kwarg_contradicted_by_actual_rejects | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_maybe_id_null_round_trips | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_maybe_id_present_value_infers | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_merge_invariant_map_value_conflict_rejects | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_named_static_infers_distinct_typevars | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_one_type_arg_explicit_types_succeeds | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_pair_invariant_list_agree_binds | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_pair_invariant_list_conflict_rejects | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_parse_as_explicit_types_succeeds | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_read_items_infers_from_instance_wire_args | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_read_items_unbound_container_recovers_t_from_fields | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_return_only_var_still_requires_binding | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_second_of_infers_from_nested_generic | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_second_of_unbound_instance_recovers_field_type | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_tag_or_value_binds_generic_instance | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_triple_choose_join_includes_concrete_class | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_triple_choose_join_includes_enum_variant | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_triple_choose_three_covariant_join | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_two_typevar_union_is_uninferrable_rejects | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_union_concrete_sibling_absorbs_value_binds_never | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_union_concrete_sibling_keeps_the_static_type_arg | - | - | - | - | - | - | - | - | - | y | - | all | | +| function_calls/generic_inference_union_null_actual_binds_never | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_union_null_actual_keeps_the_static_type_arg | - | - | - | - | - | - | - | - | - | y | - | all | | +| function_calls/generic_inference_union_with_concrete_sibling_infers_typevar | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_values_of_empty_map_requires_binding | y | - | - | - | - | - | - | - | y | - | y | python_pydantic2, java, ruby_sorbet | a dynamic host has no static value type for an empty map; the typed SDKs state theirs and round-trip | +| function_calls/generic_inference_values_of_empty_map_round_trips_empty_list | - | - | - | - | - | - | - | - | - | y | - | all | | +| function_calls/generic_inference_values_of_nonempty_returns_values | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_inference_wrap_infers_and_returns_bound_generic | y | - | - | - | - | - | - | - | y | - | y | all | | +| function_calls/generic_inference_wrap_infers_and_returns_generic | y | - | - | - | - | - | - | - | y | y | y | all | | +| function_calls/generic_nullable_type_variables_preserve_every_pointer_boundary | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/generic_receiver_and_static_helpers | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/generic_return_only_type_arguments | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/generic_union_input_and_engine_validation | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/go_codegen_context_deadline | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/go_codegen_default_argument_serialization_error_names_argument | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/go_codegen_defaulted_argument_type_matrix | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/go_codegen_defaulted_void | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/go_codegen_option_name_collisions | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/go_codegen_optional_arg_last_value_wins | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/go_codegen_person_round_trip | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/hello_world_returns_literal | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/hidden_mode_does_not_inherit | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/host_baml_cancellation_remains_live_after_exit | y | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/host_baml_trace_override_and_callback_context_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | explicit Python call controls override instrumentation defaults | +| function_calls/host_callable_cancellation_while_dispatched | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/host_callable_class_argument | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/host_callable_closed_union_containers_and_nominal_arms | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/host_callable_closed_union_literal_optional_and_selected_empty_container_arms | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/host_callable_closed_union_round_trips | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/host_callable_declared_throw_is_catchable | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/host_callable_failure_under_open_throws_is_catchable | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/host_callable_hof_receives_a_baml_closure | y | y | y | y | y | - | y | - | y | y | y | all | | +| function_calls/host_callable_late_and_uncaught_failures_release_native_identity | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/host_callable_media_round_trip | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/host_callable_nullable_optional_distinguishes_all_three_states | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/host_callable_optional_arguments | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/host_callable_optional_names_avoid_generated_and_projection_collisions | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/host_callable_panic_does_not_cross_cgo_boundary | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/host_callable_primitive_and_multiple_arguments | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/host_callable_reentrant_call_does_not_deadlock | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/host_callable_repeated_and_concurrent_reuse | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/host_callable_structured_round_trips | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/host_callable_two_cbs_callbacks_named_f_and_f_returns_both_run | y | y | y | y | y | - | y | y | y | y | y | all | | +| function_calls/host_callable_void_signatures | y | y | y | y | y | - | y | y | y | - | y | all | | +| function_calls/host_callables_adopts_a_custom_thenable_exactly_once | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/host_callables_adopts_a_promise_from_a_separate_browser_realm | - | - | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_web_chromium, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/host_callables_async_callable_future_completing_exceptionally_round_trips_original | - | - | - | - | - | - | - | - | y | - | - | all | | +| function_calls/host_callables_async_callable_returning_future_is_awaited_by_bridge | - | - | - | - | - | - | - | - | y | - | - | all | | +| function_calls/host_callables_async_callable_runs_to_completion | y | - | - | - | - | - | y | - | y | y | y | all | | +| function_calls/host_callables_awaits_a_promise_returning_callback | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/host_callables_baml_error_panicked_with_is_thrown_on | - | - | - | - | - | - | - | y | - | - | - | go | Go's completion classification of a BAML error a callback forwards by panicking with it | +| function_calls/host_callables_call_repeatedly_invokes_callback_n_times | y | y | y | y | y | - | y | - | y | y | y | all | | +| function_calls/host_callables_call_repeatedly_with_zero_n_returns_empty_list | y | y | y | y | y | - | y | - | y | y | y | all | | +| function_calls/host_callables_call_with_throwing_in_baml_catches_host_callable_error | y | y | y | y | y | - | y | - | y | y | y | all | | +| function_calls/host_callables_cancels_through_the_originating_runtime_after_runtime_replacement | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/host_callables_class_callback_round_trips_class_value | - | - | - | - | y | - | - | - | - | y | - | all | | +| function_calls/host_callables_class_callback_round_trips_pydantic_model | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/host_callables_completes_a_pending_host_call_after_runtime_replacement | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/host_callables_completes_through_the_metadata_fallback_for_a_hostile_thrown_object | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/host_callables_concurrent_throws_in_flight_rehydrate_to_their_own_object | - | - | - | - | - | - | - | - | y | - | - | all | | +| function_calls/host_callables_delivers_a_single_supplied_optional_by_name_defaulting_the_rest | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/host_callables_delivers_both_supplied_optionals_in_one_opts_object | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/host_callables_forwarded_async_cancellation_stays_a_panic | - | - | - | - | - | - | - | - | y | y | - | java, swift | pins a typed SDK's completion of a nested call's cancellation, which it surfaces as its native cancellation | +| function_calls/host_callables_forwarded_baml_error_is_held_to_the_declared_throws | - | - | - | - | - | - | - | y | - | - | - | go | Go's completion classification of a forwarded BAML error held to the declared throws type | +| function_calls/host_callables_forwarded_baml_error_is_thrown_as_itself | - | - | - | - | y | - | - | - | - | y | - | cpp, swift | pins a forwarded BAML error completed as thrown | +| function_calls/host_callables_forwarded_cancellation_stays_a_panic_under_throws | - | - | - | - | y | - | - | - | - | - | - | cpp | C++ surfaces BAML cancellation as baml::cancelled. | +| function_calls/host_callables_forwarded_panic_stays_a_panic_under_a_declared_throws_class | - | - | - | - | - | - | - | y | y | y | - | go, java, swift | pins a typed SDK's completion of a forwarded panic under a single-class throws | +| function_calls/host_callables_forwarded_type_mismatch_crosses_as_the_baml_error_it_is | - | - | - | - | - | - | - | - | y | - | - | java | pins the Java completion of a forwarded TypeMismatch, which Java surfaces as an IllegalArgumentException | +| function_calls/host_callables_ignores_a_host_promise_settlement_after_its_outer_call_is_cancelled | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/host_callables_int_return_callable_round_trip | y | y | y | y | y | - | y | - | y | y | y | all | | +| function_calls/host_callables_lambda_round_trip | y | - | - | - | y | - | y | - | y | y | y | all | | +| function_calls/host_callables_literal_union_callback_states_its_literal_types | - | - | - | - | - | - | - | - | - | y | - | all | | +| function_calls/host_callables_media_callback_states_the_media_type | - | - | - | - | - | - | - | - | - | y | - | all | | +| function_calls/host_callables_multiple_callable_keys_are_distinct | y | y | y | y | y | - | y | - | y | y | y | all | | +| function_calls/host_callables_multiple_throws_in_flight_do_not_collide_in_registry | y | - | - | - | y | - | y | - | y | y | y | all | | +| function_calls/host_callables_non_finite_json_result_is_a_contract_violation | - | - | - | - | - | - | - | y | - | - | - | go | Go's completion classification of a returned value that does not fit its declared type | +| function_calls/host_callables_omits_both_optionals_so_the_callback_s_own_defaults_apply | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/host_callables_optional_args_all_set_deliver_both | y | - | - | - | y | - | y | - | y | y | y | all | | +| function_calls/host_callables_optional_args_all_unset_apply_host_defaults | y | - | - | - | y | - | y | - | y | y | y | all | | +| function_calls/host_callables_optional_args_partially_set_deliver_by_name | y | - | - | - | y | - | y | - | y | y | y | all | | +| function_calls/host_callables_overlapping_container_union_callback_states_the_selected_list | - | - | - | - | - | - | - | - | - | y | - | all | | +| function_calls/host_callables_passes_a_generated_class_instance_into_the_callback | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/host_callables_preserves_a_rejected_promise_reason_by_identity | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/host_callables_preserves_an_error_whose_stack_is_not_a_string | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/host_callables_preserves_arbitrary_thrown_js_values_without_hanging | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/host_callables_preserves_number_kind_for_float_bigint_union | - | y | y | y | - | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | exercises JavaScript's distinct number and bigint host representations | +| function_calls/host_callables_preserves_same_realm_thrown_object_identity | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/host_callables_rejects_callable_args_on_the_generated_sync_path_instead_of_hanging | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/host_callables_release_fires_on_drop_of_callable | y | y | y | y | - | - | y | - | y | - | y | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, rust, java, ruby_sorbet | callable release coverage depends on host weak-reference support and remains nondeterministic | +| function_calls/host_callables_resolves_integral_number_for_float_class_field | - | y | y | y | - | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | exercises the Node bridge's JavaScript number encoding in generated classes | +| function_calls/host_callables_resolves_integral_number_for_float_return | - | y | y | y | - | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | exercises the Node bridge's JavaScript number encoding for float host returns | +| function_calls/host_callables_returns_and_invokes_a_host_callable_nested_in_a_list | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/host_callables_returns_and_invokes_a_nested_host_callable | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/host_callables_round_trips_a_typed_baml_error_through_typed_catch_and_propagation | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/host_callables_round_trips_an_arrow_function_callback | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/host_callables_simple_sync_callable_returns_string | y | y | y | y | y | - | y | - | y | y | y | all | | +| function_calls/host_callables_surfaces_a_throwing_callback_as_a_baml_error | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/host_callables_surfaces_a_wrong_callback_return_type_as_host_contract_violation | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/host_callables_throwing_async_callable_round_trips_original_error | - | - | - | - | - | - | - | - | - | y | - | all | | +| function_calls/host_callables_throwing_async_callable_round_trips_original_python_exception | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/host_callables_throwing_callable_bamlerror_propagates_back_with_typed_fields | y | - | - | - | - | - | y | y | y | - | y | all | | +| function_calls/host_callables_throwing_callable_bamlerror_wrapping_codegenned_class_is_caught_in_baml | y | - | - | - | - | - | y | y | y | - | y | all | | +| function_calls/host_callables_throwing_callable_custom_host_exception_round_trips_with_identity | - | - | - | - | y | - | - | - | - | y | - | all | | +| function_calls/host_callables_throwing_callable_custom_python_exception_round_trips_with_identity | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/host_callables_throwing_callable_hostthrow_codegenned_class_is_caught_in_baml | - | - | - | - | y | - | - | - | - | y | - | all | | +| function_calls/host_callables_throwing_callable_hostthrow_propagates_back_with_typed_fields | - | - | - | - | y | - | - | - | - | y | - | all | | +| function_calls/host_callables_throwing_callable_keyerror_round_trips_with_identity | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/host_callables_throwing_callable_out_of_range_round_trips_with_identity | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/host_callables_throwing_callable_round_trips_original_host_exception | - | - | - | - | y | - | - | - | - | y | - | all | | +| function_calls/host_callables_throwing_callable_round_trips_original_python_exception | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/host_callables_thrown_value_of_another_go_type_is_a_contract_violation | - | - | - | - | - | - | - | y | - | - | - | go | Go's completion classification of a value thrown as a Go type other than the declared one | +| function_calls/host_callables_two_arg_callable_unpacks_positional_args | y | y | y | y | y | - | y | - | y | y | y | all | | +| function_calls/host_callables_typed_throw_too_deep_is_a_bridge_failure | - | - | - | - | y | - | - | - | - | - | - | cpp | C++ throws a BAML value with baml::host_throw. | +| function_calls/host_callables_unfit_callback_result_is_a_contract_violation | - | - | - | - | y | - | - | - | - | - | - | cpp | C++ can return only a malformed bigint as unfit. | +| function_calls/host_callback_cleanup_retains_context | y | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/host_callback_reentry_context | y | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/host_cancellation_waits_for_cleanup_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | covers Python task cancellation and asynchronous finally cleanup | +| function_calls/host_capture_avoids_getters_and_proxies_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | covers JS getters, Proxy traps, and custom thenables | +| function_calls/host_capture_preserves_application_values | y | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/host_capture_wire_stays_decodable_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | checks the Python capture adapter's wire output directly | +| function_calls/host_capture_wire_stays_decodable_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | checks the Node capture adapter's wire output directly. | +| function_calls/host_child_outlives_parent | y | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/host_common_types_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node observations are checked alongside Python by local SQL. | +| function_calls/host_context_inherits_and_restores | y | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/host_current_exposes_generated_cancel_token | y | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/host_defaults_baml_span_and_error_capture_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python instrumentation defaults Python-to-BAML span/capture requests | +| function_calls/host_errors_preserve_identity_and_restore_context | y | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/host_preserves_call_shape_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | covers JS receivers, variadic arguments, and native Promise results | +| function_calls/host_recording_audit_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | runs the Node half of the independent local SQL audit | +| function_calls/host_rejects_unsupported_configuration_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | covers unsupported Python generator/decorator shapes | +| function_calls/host_rejects_unsupported_configuration_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | covers JS generator and marker configuration rejection | +| function_calls/host_supplied_empty_and_null_bearing_json_containers_state_the_alias | - | - | - | - | - | - | - | - | - | y | - | all | | +| function_calls/host_supplied_json_supports_typed_narrowing | y | y | y | y | y | - | y | y | y | y | y | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, cpp, rust, go, java, swift, ruby_sorbet | C# declares no function_calls suite (its native coverage is Rust-wrapped integration tests) | +| function_calls/host_thread_context_handoff_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | covers Python ContextVar handoff to a thread pool | +| function_calls/instance_method_cancellation_returns_exact_context_error | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/instance_method_media_receiver_default_and_ownership_round_trip | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/instance_method_optional_arguments | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/instance_method_throw_preserves_current_go_error_contract | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/instance_methods_on_classes_round_trip | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/instance_never_method_has_error_only_signature_and_returns_panic | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/instrument_decorator_forms_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | covers Python decorator forms | +| function_calls/instrument_preserves_sync_execution_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | covers Python signatures and positional argument binding | +| function_calls/invalid_controls_rejected | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/invalid_function_arguments_surface_baml_error | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/invalid_response_preserves_wire_diagnostics | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | exercises Python BamlError and generated Pydantic payloads | +| function_calls/invalid_timeout_rejected | y | y | y | y | - | - | y | - | y | - | y | all | | +| function_calls/invocation_inheritance | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/invocation_inheritance_multi_layer_context_patch_inherits_and_restores | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/invocation_lifecycle | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/invocation_lifecycle_failed_admission_does_not_consume_reservation | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/invocation_lifecycle_pre_cancelled_call_does_not_enter_callback | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/invocation_lifecycle_reservation_is_single_use | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/invocation_lifecycle_retained_effective_token_observes_late_parent_cancellation | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/invocation_lifecycle_zero_timeout_does_not_enter_callback | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/invocation_options | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/invocation_options_four_call_forms | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/invocation_options_four_call_forms_async | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/invocation_options_invalid_timeout_rejected | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/invocation_surfaces | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/invocation_surfaces_returned_callable_accepts_controls | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/json_bigint_round_trips_as_bigint | - | - | - | - | - | - | - | - | y | - | - | all | | +| function_calls/json_nested_at_the_wire_limit_crosses_and_one_level_more_is_refused | - | - | - | - | - | - | - | - | y | y | - | java, swift | pins a typed SDK encoder's exact message count at the wire's limit | +| function_calls/json_nested_deeper_than_the_wire_carries_is_refused_as_a_callback_result | y | y | y | y | y | - | y | y | y | y | y | all | | +| function_calls/json_nested_deeper_than_the_wire_carries_is_refused_as_an_argument | y | y | y | y | y | - | y | y | y | y | y | all | | +| function_calls/json_nested_to_the_wire_limit_crosses_and_one_level_more_does_not | - | - | - | - | y | - | - | - | - | - | - | cpp | pins the C++ encoder's exact wire measure at its cut. | +| function_calls/json_returned_from_host_callback_supports_typed_narrowing | y | y | y | y | y | - | y | y | y | y | y | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, cpp, rust, go, java, swift, ruby_sorbet | C# declares no function_calls suite (its native coverage is Rust-wrapped integration tests) | +| function_calls/late_callback_keeps_async_local_storage_after_abort_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node callback Promises can outlive an aborted SDK waiter | +| function_calls/layered_callback_context_inheritance_and_restoration_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python sync/async re-entry and ContextVar propagation | +| function_calls/layered_callback_context_inheritance_and_restoration_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node async carrier, native signals, and entry-time snapshots are specific to TypeScript | +| function_calls/layered_callback_context_restores_after_returned_callable | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/legacy_controls_rejected | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/linking_the_sdk_leaves_serde_json_features_alone | - | - | - | - | - | - | y | - | - | - | - | rust | cargo feature unification is Rust-only | +| function_calls/live_token_cancels_after_admission | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/main_hello_world_returns_literal | y | - | - | - | y | - | y | - | y | y | y | all | | +| function_calls/main_rejects_mistyped_int_argument | y | y | y | y | - | - | - | - | - | - | - | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | statically typed SDKs cannot pass a mistyped argument | +| function_calls/main_returns_the_literal_async | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/main_returns_the_literal_sync | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/main_round_trips_a_single_positional_argument | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/main_round_trips_ints_bools_strings_and_floats | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/main_single_required_arg_round_trips | y | - | - | - | y | - | y | - | y | y | y | all | | +| function_calls/method_generated_name_collisions_stay_callable | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/method_self_all_supported_positions_round_trip | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/methods_accept_controls | y | y | y | y | - | - | y | - | y | - | y | all | | +| function_calls/methods_accept_controls_async | y | y | y | y | - | - | y | - | - | - | - | all | | +| function_calls/methods_on_classes_create_constructs_a_greeter_async_plus_sync | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/methods_on_classes_exposes_sync_plus_async_bindings_for_both_flavors | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/methods_on_classes_greet_arg_echoes_a_non_self_argument_async_plus_sync | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/methods_on_classes_instance_greet_async_with_arg_round_trips | y | - | - | - | y | - | y | - | y | y | y | all | | +| function_calls/methods_on_classes_instance_greet_with_arg_round_trips | y | - | - | - | y | - | y | - | y | y | y | all | | +| function_calls/methods_on_classes_instance_who_async_round_trips | y | - | - | - | y | - | y | - | y | y | y | all | | +| function_calls/methods_on_classes_instance_who_round_trips | y | - | - | - | y | - | y | - | y | y | y | all | | +| function_calls/methods_on_classes_method_bindings_exist | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/methods_on_classes_static_create_async_round_trips | y | - | - | - | y | - | y | - | y | y | y | all | | +| function_calls/methods_on_classes_static_create_round_trips | y | - | - | - | y | - | y | - | y | y | y | all | | +| function_calls/methods_on_classes_who_returns_a_field_off_self_async_plus_sync | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/multi_layer_context_patch_inherits_and_restores | - | - | - | - | - | - | y | - | y | - | - | all | | +| function_calls/native_closure_can_cross_sync_and_async_entries_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node sync calls support native closures whose captures need no host dispatch | +| function_calls/native_signal_cancels_without_cancelling_input_token_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node async carrier, native signals, and entry-time snapshots are specific to TypeScript | +| function_calls/nil_host_callable_fails_before_dispatch | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/null_controls_preserve_inherited_context | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/omitted_argument_is_not_null | y | y | y | y | - | - | y | - | y | y | y | all | | +| function_calls/optional_args_async_samples | y | y | y | y | y | - | y | - | y | y | y | all | | +| function_calls/optional_args_bigint_in_i64_range | - | - | - | - | - | - | - | - | y | - | - | all | | +| function_calls/optional_args_covers_static_and_instance_optional_args | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/optional_args_covers_the_runtime_matrix | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/optional_args_negative_runtime_cases_reject | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/optional_args_opt_box_method_matrix | y | - | - | - | y | - | y | - | y | y | y | all | | +| function_calls/optional_args_python_unset_and_none_differ_in_one_call | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/optional_args_rejects_invalid_runtime_calls_that_bypass_types | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/optional_args_runtime_matrix | y | - | - | - | y | - | y | y | y | y | y | all | | +| function_calls/optional_args_treats_undefined_as_omitted_and_keeps_null_distinct | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/optional_args_unset_and_null_differ_in_one_call | - | - | - | - | y | - | - | - | - | y | - | all | | +| function_calls/options_snapshot_at_async_entry_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node async carrier, native signals, and entry-time snapshots are specific to TypeScript | +| function_calls/options_snapshot_when_coroutine_starts_python_only | y | - | - | - | - | - | - | - | - | - | - | all | | +| function_calls/parse_json_successful_value_uses_generated_json_projection | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/pre_aborted_call_does_not_dispatch_callback_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/pre_aborted_native_signal_does_not_enter_callback_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node async carrier, native signals, and entry-time snapshots are specific to TypeScript | +| function_calls/pre_cancelled_call_does_not_enter_callback | y | y | y | y | - | - | y | - | y | - | y | all | | +| function_calls/primitive_thrown_error | - | - | - | - | - | - | - | - | - | - | y | ruby_sorbet | Ruby error wrapper for a thrown string | +| function_calls/promise_all_failure_with_explicit_sibling_abort_preserves_error_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/promise_callback_preserves_async_local_storage_across_suspension_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/promise_callback_sync_reentry_rejects_host_callback_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/provider_error_preserves_native_error_metadata | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | exercises Python BamlError and generated Pydantic payloads | +| function_calls/raises_async_sibling_also_has_raises | y | - | - | - | y | - | y | - | y | - | y | all | | +| function_calls/raises_imports | - | - | - | - | - | - | y | - | - | - | - | all | | +| function_calls/raises_imports_symbols_reachable | y | - | - | - | y | - | - | - | y | - | y | all | | +| function_calls/raises_inferred_contract_without_clause_still_raises | y | - | - | - | y | - | y | - | y | - | y | all | | +| function_calls/raises_method_raises_block_in_pyi | y | - | - | - | - | - | y | - | y | - | y | all | | +| function_calls/raises_method_raises_blocks | - | - | - | - | y | - | - | - | - | - | - | all | | +| function_calls/raises_non_throwing_function_has_no_raises_block | y | - | - | - | y | - | y | - | y | - | y | all | | +| function_calls/raises_single_throws | y | - | - | - | y | - | y | - | y | - | y | all | | +| function_calls/raises_summary_precedes_raises_block | y | - | - | - | y | - | y | - | y | - | y | all | | +| function_calls/raises_union_throws_lists_all_names | y | - | - | - | y | - | y | - | y | - | y | all | | +| function_calls/reflect_facade_builds_runtime_declarations_and_decodes_their_instances | - | - | - | - | - | - | - | y | - | - | - | go | Go's hand-written reflect facade; the host_reflect fixture that would cover it does not compile | +| function_calls/reflected_type_closed_dynamic_unions_and_callback | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/reflected_type_composes_through_optional_containers_and_classes | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/reflected_type_primitive_literal_and_nominal_descriptors | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/reflected_type_top_level_and_runtime_produced_values | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/rejected_admission_does_not_consume_reservation | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/repeated_abort_does_not_interrupt_async_callback_cleanup_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python callback cancellation under an explicit controller | +| function_calls/repeated_cooperative_abort_does_not_interrupt_callback_cleanup_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/repeated_dispatches_each_start_with_entry_async_local_storage_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/repeated_dispatches_each_start_with_entry_context_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python ContextVar isolation between dispatch tasks | +| function_calls/repeated_dispatches_invoke_callback_in_order | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/reservation_does_not_inherit | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/reservation_is_single_use | - | - | - | - | - | - | y | - | y | - | - | all | | +| function_calls/reserved_binding_field_does_not_overwrite_type_arguments | - | - | - | - | - | - | - | - | - | - | y | ruby_sorbet | Ruby stores erased generic bindings in a reserved instance variable | +| function_calls/retained_callback_uses_invocation_async_local_storage_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/retained_context_survives_parent_completion | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/retained_effective_token_observes_late_parent_cancellation | - | - | - | - | - | - | y | - | y | - | - | all | | +| function_calls/retained_effective_token_stays_live_after_callback | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/retained_host_callback_uses_later_invocations_application_context_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python callback task and event loop ownership | +| function_calls/retained_invocation_resolves_token_after_callback | y | - | - | - | - | - | - | - | - | - | - | all | | +| function_calls/returned_callable_accepts_controls | y | y | y | y | - | - | y | - | y | - | y | all | | +| function_calls/returned_callable_accepts_controls_async | - | - | - | - | - | - | y | - | - | - | - | all | | +| function_calls/returned_closure_preserves_callback_error_and_remains_reusable | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/returned_closure_retains_host_callback | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/returned_function_is_accepted_as_an_argument | - | - | - | - | - | - | y | - | - | - | - | all | | +| function_calls/reused_callback_uses_current_async_local_storage_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/ruby_lifecycle_hooks_generate_and_load | - | - | - | - | - | - | - | - | - | - | y | ruby_sorbet | Ruby lifecycle hook names must not run while requiring the generated SDK | +| function_calls/ruby_object_and_module_names_generate_load_and_call | - | - | - | - | - | - | - | - | - | - | y | ruby_sorbet | Ruby Object properties and Module callables need escaped names | +| function_calls/runtime_executes_the_generated_sdk_in_a_browser | - | - | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_web_chromium, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/runtime_executes_the_generated_sdk_in_node | - | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/runtime_executes_the_generated_sdk_in_workerd | - | - | - | y | - | - | - | - | - | - | - | python_pydantic2, typescript_web_cloudflare_workers, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/runtime_imports_the_generated_sdk_in_the_configured_runtime | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/same_callback_recurses_through_baml | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/same_callback_recurses_through_baml_without_reusing_an_entry_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | sync callback re-entry differs between Python and Node | +| function_calls/same_named_throws_ok_passes_through | - | - | - | - | - | - | y | - | - | - | - | rust | validates Rust-specific throws variant naming | +| function_calls/same_named_throws_selects_the_json_parse_error | - | - | - | - | - | - | y | - | - | - | - | rust | validates Rust-specific throws variant naming | +| function_calls/same_named_throws_selects_the_stdlib_parse_error | - | - | - | - | - | - | y | - | - | - | - | rust | validates Rust-specific throws variant naming | +| function_calls/single_required_arg_round_trips | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/specialized_callable_rejects_type_bindings | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/static_method_errors_never_cancellation_and_collision_names | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/static_method_media_json_type_and_rust_type_round_trips | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/static_method_required_default_and_structured_round_trips | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/stdlib_entrypoints_compiler_intrinsics_are_not_emitted_as_entry_points | y | y | - | - | y | - | y | - | y | y | y | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/stdlib_entrypoints_native_argv_callable_as_entry_point | y | - | - | - | y | - | y | - | y | y | y | all | | +| function_calls/stdlib_entrypoints_native_baml_sys_argv_is_callable_as_an_entry_point | - | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/stdlib_entrypoints_sysop_fs_exists_callable_as_entry_point | y | y | - | - | y | - | y | - | y | y | y | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/stdlib_error_surfaces_as_go_error | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/stream_schema_failure_is_parse_failed | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | exercises Python BamlError and generated Pydantic payloads | +| function_calls/suspended_callback_survives_gc_until_cancellation_cleanup_finishes_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | asyncio tasks retain callback bodies until actual completion | +| function_calls/sync_call_returns_null | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/sync_callback_can_run_async_baml_with_its_own_application_loop_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Node cannot synchronously drive an application loop | +| function_calls/sync_callback_inherits_call_time_async_local_storage_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/sync_callback_inherits_call_time_context_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | +| function_calls/sync_callback_of_async_entry_reenters_sync_baml_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | sync callback re-entry differs between Python and Node | +| function_calls/sync_callback_of_async_entry_runs_on_originating_loop_thread_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | +| function_calls/sync_callback_of_async_entry_runs_on_originating_loop_thread_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/sync_callback_reenters_sync_baml_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | sync callback re-entry differs between Python and Node | +| function_calls/sync_callback_runs_on_sync_callers_thread_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | +| function_calls/sync_callback_sync_reentry_rejects_host_callback_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/sync_cancel_via_context | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/sync_entry_async_callback_copies_context_across_suspension_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python ContextVar semantics with a bridge-owned loop | +| function_calls/sync_entry_async_callback_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | +| function_calls/sync_entry_cancellation_reaches_async_callback_on_worker_loop_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python cancellation on a bridge-owned callback loop | +| function_calls/sync_entry_in_running_loop_with_self_contained_async_callback_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | +| function_calls/sync_entry_in_running_loop_with_sync_callback_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | +| function_calls/sync_entry_rejection_does_not_start_promise_callback_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/sync_entry_rejection_preserves_application_context_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/sync_entry_rejects_before_dispatch_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/sync_entry_rejects_callback_before_async_reentry_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/sync_entry_rejects_callback_before_recursive_reentry_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/sync_entry_rejects_callback_before_sync_reentry_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/sync_entry_rejects_promise_callback_between_event_loop_turns_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/sync_entry_rejects_promise_callback_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/sync_entry_rejects_retained_callback_and_allows_async_reuse_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node sync entries must reject callbacks hidden in native closure captures | +| function_calls/sync_entry_rejects_sync_callback_between_event_loop_turns_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/sync_entry_rejects_sync_callback_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node event loop, AsyncLocalStorage, or cooperative Promise cancellation | +| function_calls/sync_entry_sync_callback_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | +| function_calls/sync_retained_closure_rejects_before_dispatch_typescript_only | - | y | - | - | - | - | - | - | - | - | - | typescript_node | Node cannot run JS callbacks while a sync native call blocks its event loop | +| function_calls/task_factory_failure_completes_dispatch_and_preserves_exception_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python asyncio task factory failures during dispatch | +| function_calls/taskgroup_failure_cancels_sibling_baml_call_and_preserves_error_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python callback task and event loop ownership | +| function_calls/timeout_upper_bound_accepted | y | y | y | y | - | - | y | - | y | - | y | all | | +| function_calls/to_thread_sync_entry_inherits_copied_application_context_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | +| function_calls/two_application_loops_on_two_threads_share_callback_without_rerouting_python_only | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python thread, event loop, and ContextVar semantics | +| function_calls/unhandled_spawn_error_uses_host_default | - | - | - | - | y | - | - | y | y | y | - | cpp, go, java, swift | requires subprocess-level SDK harness support | +| function_calls/union_throws_preserves_concrete_class_identity | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/unknown_control_rejected | y | y | y | y | - | - | - | - | - | - | - | all | | +| function_calls/unmarked_callback_explicit_context_and_reentry | y | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/unpolled_future_starts_nothing_rust_only | - | - | - | - | - | - | y | - | - | - | - | rust | Rust task scopes and lazy future cancellation are specific to Rust | +| function_calls/unset_and_none_differ_in_one_call | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/user_panic_surfaces_as_go_error_without_panicking | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/user_throw_surfaces_declared_class_identity | - | - | - | - | - | - | - | y | - | - | - | all | | +| function_calls/web_sysops_maps_fetch_failures_and_timeouts_into_declared_baml_errors | - | - | y | y | - | - | - | - | - | - | - | python_pydantic2, typescript_web_chromium, typescript_web_cloudflare_workers, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/web_sysops_rejects_http_streaming_and_unrelated_sysops | - | - | y | y | - | - | - | - | - | - | - | python_pydantic2, typescript_web_chromium, typescript_web_cloudflare_workers, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/web_sysops_rejects_sync_and_async_baml_fs_read_promptly | - | - | y | - | - | - | - | - | - | - | - | python_pydantic2, typescript_web_chromium, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/web_sysops_rejects_sync_http_before_dispatching_fetch | - | - | y | y | - | - | - | - | - | - | - | python_pydantic2, typescript_web_chromium, typescript_web_cloudflare_workers, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/web_sysops_rejects_unsupported_filesystem_operations | - | - | y | y | - | - | - | - | - | - | - | python_pydantic2, typescript_web_chromium, typescript_web_cloudflare_workers, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/web_sysops_supports_sync_and_async_baml_fs_read_through_node_fs_read_file_sync | - | - | - | y | - | - | - | - | - | - | - | python_pydantic2, typescript_web_cloudflare_workers, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/web_sysops_trampolines_baml_http_fetch_to_global_fetch_and_buffers_the_response | - | - | y | y | - | - | - | - | - | - | - | python_pydantic2, typescript_web_chromium, typescript_web_cloudflare_workers, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/web_sysops_trampolines_baml_http_send_with_method_headers_and_body | - | - | y | y | - | - | - | - | - | - | - | python_pydantic2, typescript_web_chromium, typescript_web_cloudflare_workers, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| function_calls/zero_timeout_does_not_enter_callback | y | y | y | y | - | - | y | - | y | - | y | all | | +| host_reflect/a_runtime_type_is_one_declaration_and_rejects_serialization | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | BEP-066 host reflection is currently exposed only by Python, TypeScript, and Go | +| host_reflect/compiled_package_returns_class_graph | y | y | y | y | - | - | - | y | - | - | - | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, go | BEP-066 host reflection is currently exposed only by Python, TypeScript, and Go | +| host_reflect/generated_class_subclasses_resolve_to_declared_type | y | y | y | y | - | - | - | - | - | - | - | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | Python and TypeScript have generated-class subclass tokens; Go has no subclass construct | +| host_reflect/host_handles_expose_composition_only | y | y | y | y | - | - | - | y | - | - | - | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, go | BEP-066 host reflection is currently exposed only by Python, TypeScript, and Go | +| host_reflect/known_type_tokens_compose_and_reject_unknowns | y | y | y | y | - | - | - | y | - | - | - | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, go | BEP-066 host reflection is currently exposed only by Python, TypeScript, and Go | +| host_reflect/reflection_compile_errors_are_typed | y | y | y | y | - | - | - | y | - | - | - | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, go | BEP-066 host reflection is currently exposed only by Python, TypeScript, and Go | +| host_reflect/runtime_class_definition_preserves_nested_metadata | y | y | y | y | - | - | - | y | - | - | - | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, go | BEP-066 host reflection is currently exposed only by Python, TypeScript, and Go | +| host_reflect/runtime_enum_definition_decodes_alias | y | y | y | y | - | - | - | y | - | - | - | python_pydantic2, typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers, go | BEP-066 host reflection is currently exposed only by Python, TypeScript, and Go | +| host_reflect/wire_occurrences_are_fresh_and_handles_reject_serialization | - | y | y | y | - | - | - | y | - | - | - | all | | +| integration/baml_closures_execute_host_arguments_structured_returns_and_mutable_captures | - | - | - | - | - | y | - | - | - | - | - | csharp | C# canonical coverage executes through its native integration harness | +| integration/basic_calls_executes_sync_and_async | - | - | - | - | - | y | - | - | - | - | - | csharp | exercises C#-specific native SDK integration coverage | +| integration/cancel_token_any_propagates_native_cancellation | - | - | - | - | - | y | - | - | - | - | - | csharp | isolates the flaky native cancellation propagation check | +| integration/canonical_documentation_consumer_compiles_and_executes | - | - | - | - | - | y | - | - | - | - | - | csharp | validates the C#-specific documentation consumer | +| integration/checked_in_union_runtime_source_matches_generator | - | - | - | - | - | y | - | - | - | - | - | csharp | validates C#-specific generated union runtime source | +| integration/dynamic_values_executes_native_dynamic_value_parity | - | - | - | - | - | y | - | - | - | - | - | csharp | exercises C#-specific native SDK integration coverage | +| integration/dynamic_values_type_naming_a_runtime_declaration_twice_round_trips | - | - | - | - | - | y | - | - | - | - | - | csharp | exercises C#-specific ownership of a runtime declaration's repeated key | +| integration/failures_and_cancellation_executes_typed_failures_cancellation_and_exit | - | - | - | - | - | y | - | - | - | - | - | csharp | exercises C#-specific native SDK integration coverage | +| integration/generated_baml_clients_are_not_tracked | - | - | - | - | - | y | - | - | - | - | - | csharp | validates C#-specific generated-client repository hygiene | +| integration/generics_executes_inferred_and_explicit_generics | - | - | - | - | - | y | - | - | - | - | - | csharp | exercises C#-specific native SDK integration coverage | +| integration/generics_generated_surface_rejects_ambiguous_generic_calls | - | - | - | - | - | y | - | - | - | - | - | csharp | exercises C#-specific generated-surface compile coverage | +| integration/host_callable_hof_receives_a_baml_closure | - | - | - | - | - | y | - | - | - | - | - | csharp | C# canonical coverage executes through its native integration harness | +| integration/host_callables_callback_faults_complete_by_whose_they_are | - | - | - | - | - | y | - | - | - | - | - | csharp | C# completion-contract coverage executes through its native integration harness | +| integration/host_callables_json_nested_deeper_than_the_wire_carries_is_refused_as_a_callback_result | - | - | - | - | - | y | - | - | - | - | - | csharp | C# measures typed lists and class chains at the wire's exact limit | +| integration/host_callables_json_nested_deeper_than_the_wire_carries_is_refused_as_an_argument | - | - | - | - | - | y | - | - | - | - | - | csharp | C# measures typed lists and class chains at the wire's exact limit | +| integration/media_executes_media_in_both_directions | - | - | - | - | - | y | - | - | - | - | - | csharp | exercises C#-specific native SDK integration coverage | +| integration/primitive_edges_executes_native_primitive_and_nullable_edges | - | - | - | - | - | y | - | - | - | - | - | csharp | exercises C#-specific native SDK integration coverage | +| integration/release_abi_lifetimes_match_current_native_library | - | - | - | - | - | y | - | - | - | - | - | csharp | exercises the C# release ABI probe before publication | +| integration/stdlib_resources_executes_native_typed_resource_apis_lifetimes_and_state | - | - | - | - | - | y | - | - | - | - | - | csharp | exercises C#-specific native SDK integration coverage | +| integration/stdlib_structurals_executes_native_stdlib_structural_roundtrips | - | - | - | - | - | y | - | - | - | - | - | csharp | exercises C#-specific native SDK integration coverage | +| integration/streaming_executes_generated_native_stream_and_request_failure | - | - | - | - | - | y | - | - | - | - | - | csharp | exercises C#-specific native SDK integration coverage | +| integration/type_roundtrips_executes_nominals_collections_defaults_and_unions | - | - | - | - | - | y | - | - | - | - | - | csharp | exercises C#-specific native SDK integration coverage | +| llm_functions/a_runtime_declarations_type_value_holds_it_and_states_it_again | - | - | - | - | - | - | - | - | y | - | - | java | pins the Java BamlType holding a runtime declaration's engine reference | +| llm_functions/constructs_a_live_spec_without_a_synthetic_fqn | - | y | y | y | - | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | validates TypeScript's live FunctionSpec metadata surface | +| llm_functions/dynamic_runtime_stream_identity_and_flat_projection_parity | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | validates Python BamlRuntimeValue identity through the generated sync stream surface | +| llm_functions/dynamic_runtime_stream_is_an_elegant_async_iterable | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | validates Python's generated async-iterable stream surface | +| llm_functions/flat_stream_calls_exact_companion_fqn | - | - | - | - | y | - | - | - | - | - | - | cpp | C++-only exact flat-stream companion dispatch check. | +| llm_functions/flat_stream_controls_are_typed_options | - | - | - | - | - | - | - | y | - | - | - | go | pins the Go generator's typed option surface for flat stream controls. | +| llm_functions/function_spec_helpers_accept_invocation_controls | y | - | - | - | - | - | - | - | - | - | - | all | | +| llm_functions/function_spec_helpers_accept_invocation_controls_async | y | - | - | - | - | - | - | - | - | - | - | all | | +| llm_functions/function_spec_parse_replaces_the_parse_companion | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | validates Python's generated FunctionSpec parse surface | +| llm_functions/function_spec_parse_returns_closed_enum | - | - | - | - | - | - | - | y | - | - | - | go | pins Go's closed-enum FunctionSpec decoder surface. | +| llm_functions/function_spec_parse_returns_runtime_error_for_invalid_output | - | - | - | - | - | - | - | y | - | - | - | go | pins Go's FunctionSpec parse-error translation. | +| llm_functions/function_spec_parse_returns_typed_class_and_fills_missing_nullable_field | - | - | - | - | - | - | - | y | - | - | - | go | pins Go's typed FunctionSpec class decoder surface. | +| llm_functions/function_spec_prompt_is_accepted_as_an_argument | - | - | - | - | y | - | - | - | - | - | - | cpp | pins that a C++ prompt crosses back as an argument. | +| llm_functions/function_spec_prompt_uses_generated_ai_prompt_type_and_is_reusable | - | - | - | - | - | - | - | y | - | - | - | go | pins the Go FunctionSpec prompt facade and reusable context behavior. | +| llm_functions/keeps_a_portable_prompt_reusable_across_engine_reentry | - | y | y | y | - | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | validates TypeScript's portable Prompt and Image wrappers | +| llm_functions/legacy_function_companion_wire_bindings_are_absent | - | - | - | - | - | - | - | y | - | - | - | go | pins the Go generator's emitted wire-binding names. | +| llm_functions/main_baml_sdk_lorem_and_baml_sdk_ipsum_are_reachable | - | y | y | y | - | - | - | - | - | - | - | all | | +| llm_functions/main_classify_sentiment_factory_bindings | y | - | - | - | - | - | y | - | y | - | y | all | | +| llm_functions/main_extract_resume_factory_bindings | y | - | - | - | - | - | y | - | y | - | y | all | | +| llm_functions/main_extract_resume_operation_bindings | y | - | - | - | - | - | - | - | y | - | y | python_pydantic2, java, ruby_sorbet | pins Python and Java flat operation binding spellings | +| llm_functions/main_extract_resume_spec_and_stream_bindings | - | - | - | - | - | - | y | - | - | - | - | rust | pins Rust generated spec and stream binding spellings | +| llm_functions/main_flat_stream_controls_live_on_the_stream_options | - | - | - | - | - | - | - | - | y | - | - | all | | +| llm_functions/main_function_spec_prompt_is_portable_and_reusable | - | - | - | - | - | - | - | - | y | - | - | all | | +| llm_functions/main_ipsum_classify_sentiment_sync_plus_async_factories_are_callable | - | y | y | y | - | - | - | - | - | - | - | all | | +| llm_functions/main_ipsum_sentiment_enum_has_positive_negative_neutral_members | - | y | y | y | - | - | - | - | - | - | - | all | | +| llm_functions/main_ipsum_sentiment_enum_shape | y | - | - | - | - | - | y | - | y | y | y | all | | +| llm_functions/main_lorem_extract_resume_exposes_flat_spec_and_stream_bindings | - | y | y | y | - | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | pins TypeScript's dollar-preserving flat stream binding names | +| llm_functions/main_lorem_extract_resume_sync_plus_async_factories_are_callable | - | y | y | y | - | - | - | - | - | - | - | all | | +| llm_functions/main_lorem_resume_class_shape | y | - | - | - | - | - | y | - | y | - | y | all | | +| llm_functions/main_lorem_resume_is_reachable | - | y | y | y | - | - | - | - | - | - | - | all | | +| llm_functions/main_lorem_streaming_doc_class_shape | y | - | - | - | - | - | y | - | y | - | y | all | | +| llm_functions/main_lorem_streaming_doc_is_reachable | - | y | y | y | - | - | - | - | - | - | - | all | | +| llm_functions/main_lorem_streaming_extract_exposes_flat_spec_and_stream_bindings | - | y | y | y | - | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | pins TypeScript's dollar-preserving flat streaming binding names | +| llm_functions/main_lorem_streaming_extract_sync_plus_async_factories_are_callable | - | y | y | y | - | - | - | - | - | - | - | all | | +| llm_functions/main_namespaces_reachable_via_explicit_import | y | - | - | - | - | - | y | - | y | - | y | all | | +| llm_functions/main_nullable_model_field_can_be_omitted | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | pins the Python generator's nullable-field `= None` default | +| llm_functions/main_replay_server_namespace_bindings | y | - | - | - | - | - | y | - | y | - | y | all | | +| llm_functions/main_root_imports_cleanly | y | y | y | y | - | - | y | - | y | - | y | all | | +| llm_functions/main_spec_replaces_prompt_parse_and_request_companions | - | - | - | - | - | - | y | - | - | - | - | rust | validates Rust's generated FunctionSpec prompt, parse, and request surface | +| llm_functions/main_streaming_extract_factory_bindings | y | - | - | - | - | - | y | - | y | - | y | all | | +| llm_functions/main_streaming_extract_operation_bindings | y | - | - | - | - | - | - | - | y | - | y | python_pydantic2, java, ruby_sorbet | pins Python and Java flat streaming operation binding spellings | +| llm_functions/main_streaming_extract_spec_and_stream_bindings | - | - | - | - | - | - | y | - | - | - | - | rust | pins Rust generated streaming spec and stream binding spellings | +| llm_functions/main_types_and_bindings_reachable | - | - | - | - | - | - | - | - | - | y | - | all | | +| llm_functions/on_event_plain_call_delivers_events | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | host on_event listener coverage lands Python-first; other SDKs port separately | +| llm_functions/on_event_plain_call_delivers_model_call_model | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | host on_event listener coverage lands Python-first; other SDKs port separately | +| llm_functions/on_event_plain_call_raising_listener_does_not_fail | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | host on_event listener coverage lands Python-first; other SDKs port separately | +| llm_functions/on_event_stream_delivers_settle_events | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | host on_event listener coverage lands Python-first; other SDKs port separately | +| llm_functions/on_event_stream_raising_listener_does_not_fail | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | host on_event listener coverage lands Python-first; other SDKs port separately | +| llm_functions/projects_static_and_instance_llm_methods_without_invoking_a_provider | - | y | y | y | - | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | validates TypeScript static and instance operation member spellings | +| llm_functions/prompt_is_reusable_and_media_survives_request_preview | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | validates Python's portable Prompt and media wrapper surface | +| llm_functions/recording_lands_in_cwd_project | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | native Python recording lands where `baml query --local` reads | +| llm_functions/spec_projection_honors_cancellation | - | - | - | - | - | - | - | y | - | - | - | go | pins Go context cancellation through the Spec projection. | +| llm_functions/stream_async_for_keeps_context_after_creator_returns | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python stream lifetime/context regression | +| llm_functions/stream_contexts_are_isolated_during_concurrent_iteration | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python concurrent stream context isolation | +| llm_functions/stream_final_caches_only_settled_errors | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python final caching must distinguish results from control flow | +| llm_functions/stream_final_keeps_context_and_allows_a_step_patch | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python sync stream defaults and explicit step patches | +| llm_functions/stream_final_parse_failure_is_preserved_after_eof | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python EOF settlement retains final parse errors | +| llm_functions/stream_final_timeout_is_not_cached | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python task cancellation must not poison later final awaits | +| llm_functions/stream_keeps_creation_cancel_token | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python stream cancellation survives constructor return | +| llm_functions/stream_precancelled_admission_preserves_reservation | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | Python stream reservations must survive rejected admission | +| llm_functions/stream_recording_parent_chain | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | native Python recording ancestry, duration, values and step-count audit | +| llm_functions/streaming_e2e_async_class_typed_next_async_yields_10_partials | - | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| llm_functions/streaming_e2e_async_next_async_yields_10_partials | - | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| llm_functions/streaming_e2e_baml_driven_collect_keeps_the_s_stream_finished_union_engine_side | - | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| llm_functions/streaming_e2e_baml_driven_collect_returns_the_final_doc | - | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| llm_functions/streaming_e2e_next_yields_10_doc_partials_final_is_a_typed_streaming_doc | - | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| llm_functions/streaming_e2e_next_yields_10_partials_and_drains_to_stream_finished | - | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| llm_functions/streaming_e2e_stream | y | - | - | - | - | - | y | - | y | y | y | all | | +| llm_functions/streaming_e2e_stream_async | y | - | - | - | - | - | y | - | y | y | y | all | | +| llm_functions/streaming_e2e_stream_collect_in_baml | y | - | - | - | - | - | y | - | y | y | y | all | | +| llm_functions/streaming_e2e_stream_doc | y | - | - | - | - | - | y | - | y | y | y | all | | +| llm_functions/streaming_e2e_stream_doc_async | y | - | - | - | - | - | y | - | y | y | y | all | | +| llm_functions/streaming_e2e_stream_doc_collect_in_baml | y | - | - | - | - | - | y | - | y | y | y | all | | +| package_edges/compile_cross_package_types_compile | - | - | - | - | - | - | - | y | - | - | - | all | | +| package_edges/compile_llm_projection_default_overrides | - | - | - | - | - | - | - | y | - | - | - | go | pins Go generator options for authored Spec and Stream defaults. | +| type_shapes/alias_container_composition_and_defaults | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/alias_package_scope_collisions_compile_and_run | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/aliases_round_trip_alias_container | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/aliases_round_trip_maybe_rec | - | - | - | - | y | - | - | - | - | - | - | all | | +| type_shapes/aliases_round_trip_rec_list | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/aliases_round_trip_string_list | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/bridge_handles_media_clones_ordinary_owners_and_wire_keys_independently | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/bridge_handles_media_clones_ownership_through_to_handle_and_from_handle | - | y | y | y | - | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | exercises TypeScript bridge media handle ownership APIs | +| type_shapes/bridge_handles_media_constructs_url_file_and_base64_descriptors | - | y | y | y | - | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | exercises TypeScript bridge media descriptor APIs | +| type_shapes/bridge_handles_media_exposes_stable_seed_tags | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/bridge_handles_media_keeps_host_value_tags_outside_ordinary_clone_and_release | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/bridge_handles_media_normalizes_key_halves_losslessly_and_returns_defensive_key_objects | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/bridge_handles_media_rejects_an_invalid_ordinary_key_only_when_an_operation_resolves_it | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/bridge_surface_exports_the_same_runtime_values_in_node_browsers_and_workers | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/bridge_surface_preserves_the_public_constructor_names | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/class_refs_make_outer | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/class_refs_round_trip_async_by_value_or_reference_rust_only | - | - | - | - | - | - | y | - | - | - | - | rust | Rust class parameters take `impl Borrow`, so callers pass a value or a reference | +| type_shapes/class_refs_round_trip_by_value_or_reference_rust_only | - | - | - | - | - | - | y | - | - | - | - | rust | Rust class parameters take `impl Borrow`, so callers pass a value or a reference | +| type_shapes/class_refs_round_trip_inner | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/class_refs_round_trip_outer | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/closed_union_zero_value_returns_an_input_error | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/complex_models_round_trip_complex_profile_accepts_plain_object_literals_no_class_constructors | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/complex_models_round_trip_complex_profile_preserves_deeply_nested_mixed_shape_class | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/container_recursive_class_round_trip | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/declared_enum_functions_and_class_round_trip | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/deep_namespace_thing_reachable | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/defaulted_enum_argument_and_invalid_values | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/dynamic_union_accepts_natural_go_integers | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/dynamic_union_candidates_round_trip_as_concrete_go_values | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/dynamic_union_delegates_semantic_validation_to_baml | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/dynamic_union_nested_containers_round_trip | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/dynamic_union_of_containers_uses_selected_type_metadata | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/dynamic_union_rejects_unserializable_go_values_in_bridge | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/enum_composition_matrix_round_trip | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/enum_package_scope_collisions_compile_and_run | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/enums_pick_positive | y | y | y | y | y | - | y | - | y | - | y | all | | +| type_shapes/enums_pick_sentiment | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/enums_round_trip_enums | y | y | y | y | y | - | y | - | y | - | y | all | | +| type_shapes/enums_round_trip_sentiment | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/enums_round_trip_sentiment_positive | y | y | y | y | y | - | y | - | y | - | y | all | | +| type_shapes/every_supported_leaf_through_lists_and_maps | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/forward_refs_round_trip_g_node_int | y | y | y | y | - | - | y | - | y | - | y | all | | +| type_shapes/forward_refs_round_trip_node_symbol_exists | - | - | - | - | y | - | - | - | - | y | - | all | | +| type_shapes/forward_refs_round_trip_other | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/forward_refs_round_trip_rec_list | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/forward_refs_round_trip_rec_list_with_other | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/generic_generic | y | y | y | y | - | - | y | - | y | y | y | all | | +| type_shapes/generic_generic_wrapper_get_value | y | - | - | - | - | - | y | - | y | y | y | all | | +| type_shapes/generic_wrapper_get_value | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/generics_round_trip_box_int | y | y | y | y | - | - | y | - | y | y | y | all | | +| type_shapes/generics_round_trip_differing_instantiation | y | y | y | y | - | - | y | - | y | y | y | all | | +| type_shapes/generics_round_trip_generic_binary_tree_int | y | y | y | y | - | - | y | - | y | y | y | all | | +| type_shapes/generics_round_trip_generic_linked_list_int | y | y | y | y | - | - | y | - | y | y | y | all | | +| type_shapes/generics_round_trip_nested_generics | y | y | y | y | - | - | y | - | y | y | y | all | | +| type_shapes/generics_round_trip_wrapper_int | y | y | y | y | - | - | y | - | y | y | y | all | | +| type_shapes/handles_baml_fs_open_returns_a_typed_file_handle | - | y | - | - | - | - | - | - | - | - | - | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| type_shapes/handles_file_cursor_state_persists_across_calls | y | y | - | - | - | - | y | - | y | y | y | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| type_shapes/handles_http_get_response_fields_and_methods | y | y | - | - | - | - | y | - | y | - | y | python_pydantic2, typescript_node, cpp, csharp, rust, go, java, swift, ruby_sorbet | | +| type_shapes/handles_image_from_base64_roundtrips_payload | y | y | y | y | - | - | y | - | y | y | y | all | | +| type_shapes/handles_open_file_returns_file_handle | y | - | - | - | - | - | y | - | y | y | y | all | | +| type_shapes/host_created_media_round_trips_through_optional_list | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | validates Python host-created media wrapper encoding | +| type_shapes/host_from_file_reads_now_and_keeps_the_base_name | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | validates the Python media wrapper's from_file, which is BAML's from_file | +| type_shapes/json_interchange_both_reject_what_baml_rejects | - | - | - | - | - | - | y | - | - | - | - | rust | validates the Rust SDK's optional serde impls | +| type_shapes/json_interchange_deserialize_matches_baml | - | - | - | - | - | - | y | - | - | - | - | rust | validates the Rust SDK's optional serde impls | +| type_shapes/json_interchange_json_fields_match_baml | - | - | - | - | - | - | y | - | - | - | - | rust | validates the Rust SDK's optional serde impls | +| type_shapes/json_interchange_optional_fields_may_be_absent_or_null | - | - | - | - | - | - | y | - | - | - | - | rust | validates the Rust SDK's optional serde impls | +| type_shapes/json_interchange_serialize_matches_baml | - | - | - | - | - | - | y | - | - | - | - | rust | validates the Rust SDK's optional serde impls | +| type_shapes/json_interchange_serialize_matches_baml_outside_a_class | - | - | - | - | - | - | y | - | - | - | - | rust | validates the Rust SDK's optional serde impls | +| type_shapes/json_interchange_unions_take_the_first_arm_that_fits | - | - | - | - | - | - | y | - | - | - | - | rust | validates the Rust SDK's optional serde impls | +| type_shapes/lists_round_trip_empty_list | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/lists_round_trip_ints | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/lists_round_trip_list_container | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/lists_round_trip_optional_strings | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/lists_round_trip_union_list | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/literals_literal_values_convert_implicitly | - | - | - | - | y | - | - | - | - | - | - | all | | +| type_shapes/literals_return_literals | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/literals_round_trip_flag_mixed_literal_union | y | - | - | - | y | - | - | - | - | - | - | all | | +| type_shapes/literals_round_trip_literal42 | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/literals_round_trip_literal_draft | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/literals_round_trip_literal_escaped | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/literals_round_trip_literal_false | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/literals_round_trip_literal_true | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/literals_round_trip_literals | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/lorem_resume_reachable | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/main_all_namespaces_reachable | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/main_deep_namespace_thing_reachable | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/main_generated_models_ignore_extra_fields | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | validates Python Pydantic extra-field compatibility | +| type_shapes/main_lorem_resume_reachable | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/main_root_foo_reachable | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/main_root_imports_cleanly | y | y | y | y | - | - | y | - | y | - | y | all | | +| type_shapes/main_runtime_owned_builtin_leaves_expose_their_public_names | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/make_foo | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/make_outer | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/maps_round_trip_enum_keyed_map | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/maps_round_trip_list_valued_map | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/maps_round_trip_map_container | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/maps_round_trip_resume | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/maps_round_trip_sentiment | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/maps_round_trip_simple_map | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/media_constructors_and_accessors | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/media_from_file_reads_now_and_keeps_the_base_name | - | - | - | - | - | - | - | - | y | - | - | java | validates the Java media wrapper's from_file, which is BAML's from_file | +| type_shapes/media_nested_optional_and_containers | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/media_return_and_round_trip_all_kinds | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/media_return_audio | y | - | - | - | - | - | y | - | y | y | y | all | | +| type_shapes/media_return_audio_preserves_url_and_mime | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/media_return_image | y | - | - | - | - | - | y | - | y | y | y | all | | +| type_shapes/media_return_image_preserves_url_and_mime | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/media_return_pdf | y | - | - | - | - | - | y | - | y | y | y | all | | +| type_shapes/media_return_pdf_preserves_url_and_mime | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/media_return_video | y | - | - | - | - | - | y | - | y | y | y | all | | +| type_shapes/media_return_video_preserves_url_and_mime | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/media_round_trip_all_media | y | y | y | y | y | - | - | y | y | y | y | all | | +| type_shapes/media_round_trip_audio | y | - | - | - | - | - | y | - | y | y | y | all | | +| type_shapes/media_round_trip_audio_preserves_url_and_mime | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/media_round_trip_image | y | - | - | - | - | - | y | - | y | y | y | all | | +| type_shapes/media_round_trip_image_can_reuse_the_same_wrapper | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/media_round_trip_image_list | - | - | - | - | - | - | - | - | - | y | - | all | | +| type_shapes/media_round_trip_image_map | - | - | - | - | - | - | - | - | - | y | - | all | | +| type_shapes/media_round_trip_image_or_audio | - | - | - | - | - | - | - | - | y | y | - | all | | +| type_shapes/media_round_trip_media | y | - | - | - | - | - | y | - | y | y | y | all | | +| type_shapes/media_round_trip_media_or_image_class | y | y | y | y | y | - | - | y | y | y | y | all | | +| type_shapes/media_round_trip_media_preserves_all_four_media_fields | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/media_round_trip_media_shapes | - | - | - | - | - | - | - | - | - | y | - | all | | +| type_shapes/media_round_trip_optional_image_list | - | - | - | - | - | - | - | - | - | y | - | all | | +| type_shapes/media_round_trip_pdf | y | - | - | - | - | - | y | - | y | y | y | all | | +| type_shapes/media_round_trip_pdf_preserves_url_and_mime | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/media_round_trip_video | y | - | - | - | - | - | y | - | y | y | y | all | | +| type_shapes/media_round_trip_video_preserves_url_and_mime | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/media_uses_the_runtime_owned_wrapper_constructors | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/nested_class_round_trip | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/no_op | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/nullable_class_fields_round_trip | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/nullable_container_boundaries_round_trip | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/nullable_container_fields_round_trip | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/nullable_recursive_classes_round_trip | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/opaque_rust_type_default_and_host_callback_positions | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/opaque_rust_type_nested_containers_classes_and_null | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/opaque_rust_type_round_trips_and_remains_reusable | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/optional_omitted_nullable_keys_read_as_null | - | y | y | y | - | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | TypeScript object literals and constructor inits may leave nullable keys out | +| type_shapes/optional_omitted_required_key_is_rejected | - | y | y | y | - | - | - | - | - | - | - | typescript_node, typescript_web_chromium, typescript_web_cloudflare_workers | only TypeScript has a type-level check that a required key is present | +| type_shapes/optional_round_trip_optional_container | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/optional_round_trip_optional_int | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/optional_round_trip_optional_resume | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/optional_round_trip_optional_union | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/optional_round_trip_resume | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/optional_top_level_round_trips | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/overlapping_list_arms_preserve_exact_kind_for_empty_and_nonempty_values | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/overlapping_literal_arms_round_trip_with_exact_kind | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/overlapping_map_arms_preserve_exact_kind_for_empty_and_nonempty_values | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/pick_positive | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/pick_sentiment | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/primitive_class_round_trip | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/primitive_literal_union_constructors_round_trip_with_exact_kind_and_value | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/primitives_return_bigint | y | - | - | - | y | - | - | - | y | - | y | all | | +| type_shapes/primitives_return_bool | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/primitives_return_float | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/primitives_return_int | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/primitives_return_null | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/primitives_return_string | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/primitives_round_trip_bigint | y | - | - | - | y | - | - | - | y | - | y | all | | +| type_shapes/primitives_round_trip_bool | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/primitives_round_trip_float | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/primitives_round_trip_float_accepts_int | y | - | - | - | y | - | y | - | y | - | y | all | | +| type_shapes/primitives_round_trip_float_refuses_an_int_on_the_wire | - | - | - | - | - | - | y | - | - | - | - | rust | only the low-level Rust entry point can put an int on the wire where a float is declared | +| type_shapes/primitives_round_trip_int | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/primitives_round_trip_int_async | - | - | - | - | - | - | - | - | - | y | - | all | | +| type_shapes/primitives_round_trip_null | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/primitives_round_trip_primitives | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/primitives_round_trip_primitives_float_field_accepts_int | y | - | - | - | y | - | y | - | y | y | y | all | | +| type_shapes/primitives_round_trip_string | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/primitives_round_trip_uint8_array | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/recursion_int_binary_tree_deeper_than_the_wire_is_refused | y | y | y | y | y | - | y | y | y | y | y | all | | +| type_shapes/recursion_round_trip_int_binary_tree | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/recursion_round_trip_mutual_recursion | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/recursion_round_trip_scc_t1_t2_t3 | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/recursion_round_trip_scc_t4_t5_t6 | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/required_container_round_trips | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/return_bool | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/return_float | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/return_int | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/return_literals | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/return_null | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/return_string | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/root_foo_reachable | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/root_imports_cleanly | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_bool | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_box_int | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_complex_profile_preserves_deeply_nested_mixed_shape_class | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_dedup | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_deep | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_deep_thing_from_a | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_deep_thing_from_lorem | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_differing_instantiation | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_empty_list | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_empty_list_preserves_selected_arm | - | - | - | - | - | - | - | - | - | y | - | all | | +| type_shapes/round_trip_enums | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_fizz_buzz_foo_bar | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_fizz_foo_bar | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_float | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_float_accepts_int_constant | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_foo | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_foo_bar | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_forward_ref_g_node_int | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_forward_ref_other | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_generic_binary_tree_int | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_generic_linked_list_int | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_inner | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_int | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_int_binary_tree | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_ints | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_ipsum | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_list_container | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_list_valued_map | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_literal42 | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_literal_draft | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_literal_escaped | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_literal_false | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_literal_true | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_literals | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_lorem_resume | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_lorem_resume_from_ipsum | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_map_resume | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_map_sentiment | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_mutual_recursion | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_nested_generics | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_null | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_null_to_end | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_optional_container | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_optional_int | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_optional_plus_null | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_optional_resume | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_optional_strings | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_optional_union | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_outer | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_primitives | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_primitives_float_field_accepts_int_constant | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_required_resume | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_resume_or_http_response | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_root_foo_from_ab | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_root_foo_from_lorem | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_scct1_t2_t3 | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_scct4_t5_t6 | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_sentiment | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_sentiment_positive | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_simple_map | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_singleton_unwrap | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_string | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_string_list | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_thing_from_ab | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_uint8_array | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_union_container | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_union_list | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_union_t | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/round_trip_wrapper_int | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/routing_make_foo | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/routing_round_trip_deep_thing_from_a | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/routing_round_trip_deep_thing_from_lorem | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/routing_round_trip_foo | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/routing_round_trip_lorem_resume_from_ipsum | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/routing_round_trip_resume | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/routing_round_trip_resume_or_http_response | y | y | y | y | - | - | - | - | y | y | y | all | | +| type_shapes/routing_round_trip_root_foo | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/routing_round_trip_root_foo_from_ab | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/routing_round_trip_thing_from_ab | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/supported_namespaces_reachable | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/symbol_collisions_round_trip_deep | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/symbol_collisions_round_trip_fizz_buzz_foo_bar | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/symbol_collisions_round_trip_fizz_foo_bar | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/symbol_collisions_round_trip_foo_bar | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/symbol_collisions_round_trip_ipsum | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/transparent_alias_functions_round_trip | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/type_alias_declared_before_classes_is_importable | y | - | - | - | - | - | - | - | - | - | - | python_pydantic2 | validates Python-specific generated type alias ordering | +| type_shapes/typemap_is_installed_during_root_module_evaluation | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/typemap_preserves_user_enum_and_generic_mappings | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/typemap_resolves_every_runtime_owned_base_to_one_bridge_constructor_identity | - | y | y | y | - | - | - | - | - | - | - | all | | +| type_shapes/union_aliases_are_transparent_and_flatten_before_thresholding | - | - | - | - | - | - | - | y | - | - | - | all | | +| type_shapes/unions_consumption_surfaces | - | - | - | - | - | - | - | - | - | y | - | all | | +| type_shapes/unions_match_dispatches_by_type | - | - | - | - | y | - | - | - | - | - | - | all | | +| type_shapes/unions_round_trip_dedup | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/unions_round_trip_null_to_end | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/unions_round_trip_optional_plus_null | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/unions_round_trip_singleton_unwrap | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/unions_round_trip_str_or_int_list | y | - | - | - | y | - | y | - | y | - | y | all | | +| type_shapes/unions_round_trip_t | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/unions_round_trip_union_container | y | y | y | y | y | - | y | - | y | y | y | all | | +| type_shapes/unions_union_is_a_plain_std_variant | - | - | - | - | y | - | - | - | - | - | - | all | | +| type_shapes/void_no_op | y | y | y | y | y | - | y | - | y | y | y | all | | +| unsupported_only/compile_unsupported_only_package_compiles | - | - | - | - | - | - | - | y | - | - | - | all | | ## Baseline comparison No required coverage pairs changed. - diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/Gemfile.lock b/baml_language/sdk_tests/crates/ruby_sorbet/Gemfile.lock index f0cd4e4fa73..4266dbae65d 100644 --- a/baml_language/sdk_tests/crates/ruby_sorbet/Gemfile.lock +++ b/baml_language/sdk_tests/crates/ruby_sorbet/Gemfile.lock @@ -1,6 +1,7 @@ GEM remote: https://rubygems.org/ specs: + bigdecimal (4.1.2) ffi (1.17.4) ffi (1.17.4-aarch64-linux-gnu) ffi (1.17.4-aarch64-linux-musl) @@ -13,7 +14,39 @@ GEM ffi (1.17.4-x86_64-darwin) ffi (1.17.4-x86_64-linux-gnu) ffi (1.17.4-x86_64-linux-musl) + google-protobuf (4.35.1) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-aarch64-linux-gnu) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-aarch64-linux-musl) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-arm64-darwin) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-x64-mingw-ucrt) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-x86-linux-gnu) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-x86-linux-musl) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-x86_64-darwin) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-x86_64-linux-gnu) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-x86_64-linux-musl) + bigdecimal + rake (~> 13.3) minitest (5.27.0) + rake (13.4.2) + sorbet-runtime (0.6.13506) PLATFORMS aarch64-linux-gnu @@ -32,9 +65,12 @@ PLATFORMS DEPENDENCIES ffi (>= 1.17.4, < 2) + google-protobuf (~> 4.0) minitest (~> 5.0) + sorbet-runtime (~> 0.5) CHECKSUMS + bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39 @@ -47,7 +83,19 @@ CHECKSUMS ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496 ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e + google-protobuf (4.35.1) sha256=a3a6471331d918f58dfa4d014a8f6286f0af2cf4840216bde52fcf2ea3fe3726 + google-protobuf (4.35.1-aarch64-linux-gnu) sha256=50ca44d0eeff3f8475e630a1accdd974256f3510694d574e2c9d6119ea8bc9e1 + google-protobuf (4.35.1-aarch64-linux-musl) sha256=d5c65cef6bd6498a9e5ed5f88cf6cf7e341c10b0a005e32137d5d1a2b6e8c18a + google-protobuf (4.35.1-arm64-darwin) sha256=d9c957df04fa89c749fa9a72a7b383eb4296efc9b2303dc6fd6fbe39c698ad6b + google-protobuf (4.35.1-x64-mingw-ucrt) sha256=0660b716c44a4d3baa5cb648cd95dc7faed87ea92cd08fe92cf9156415b35033 + google-protobuf (4.35.1-x86-linux-gnu) sha256=cc7492566b27ad8b5dfa3a7b6f9b11e905050cc53c2fa8ff22de375a9e7a70fb + google-protobuf (4.35.1-x86-linux-musl) sha256=ab403790b59e4dc588ffbed1eaacf05d4ca2f0d12ac9c13d6c64e69380d8c99e + google-protobuf (4.35.1-x86_64-darwin) sha256=66b62b4df00931018a692806df66393efa960d6d2b7da69735187249f950d3ee + google-protobuf (4.35.1-x86_64-linux-gnu) sha256=c786439087512a3fbd199e9897d265b855f951d4027e218ea55e858d45969edd + google-protobuf (4.35.1-x86_64-linux-musl) sha256=91890eb0002934a339fdb7d77a147c46b7474b6799db27872b747b905837f744 minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5 + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + sorbet-runtime (0.6.13506) sha256=94a7ff795cf2a67818a954883c0301de0b7222a9f732b24ab30ea5253e7568f6 BUNDLED WITH 4.0.4 diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/docstrings_etc/customizable/test_main.rb b/baml_language/sdk_tests/crates/ruby_sorbet/docstrings_etc/customizable/test_main.rb new file mode 100644 index 00000000000..de11f4667b5 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/docstrings_etc/customizable/test_main.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" + +class MainTest < Minitest::Test + def source + File.read(File.join(__dir__, "baml_sdk/docs.rb")) + end + + # Ruby comments, like Java's Javadoc, are a source-only documentation surface. + def doc_for(name) + lines = source.split("class #{name} <", 2).first.lines + lines.pop while lines.last&.strip == "" + lines.reverse.take_while { |line| line.lstrip.start_with?("#") } + .reverse.map { |line| line.sub(/^\s*# ?/, "").rstrip }.join("\n") + end + + def test_main_imports_symbols_reachable + [BamlSdk::Docs::Doc, BamlSdk::Docs::Note, BamlSdk::Docs::Priority, BamlSdk::Docs::Sentiment].each do |symbol| + assert_kind_of Class, symbol + end + end + + def test_main_class_doc_summary_and_attributes_section + assert_equal "A document with a title and an optional body.\n\nAttributes:\n" \ + " title: Title shown in lists and search results.\n body: Free-form body text.", doc_for("Doc") + end + + def test_main_undocumented_field_listed_as_bare_name_under_attributes + doc = doc_for("Note") + assert doc.start_with?("A multi-line summary.\nContinuation line"), doc + assert_includes doc, "\n\nAttributes:\n id: Stable identifier" + assert doc.end_with?("\n text"), doc + end + + def test_main_enum_doc_summary_and_members_section + assert_equal "Sentiment labels surfaced by the model.\n\nMembers:\n" \ + " HAPPY: Smiling face.\n SAD: Frowning face.\n NEUTRAL", doc_for("Sentiment") + end + + def test_main_enum_summary_only_omits_members_section + doc = doc_for("Priority") + assert_equal "Pin the \"summary only, no member rollup\" case: this enum has a\n" \ + "class-level `///` but every variant is bare.", doc + refute_includes doc, "Members:" + assert_equal %w[HIGH LOW MEDIUM], BamlSdk::Docs::Priority.values.map(&:serialize).sort + end + + def test_main_no_inline_field_or_variant_doc_artifacts + refute_includes source, "# Title shown in lists" + refute_includes source, "# Smiling face" + refute_match(/(?:const :|= new\().*#/, source) + assert_equal 4, source.scan(/(?:^\s*#.*\n)+(?=\s*class )/).length + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_cancellation.rb b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_cancellation.rb new file mode 100644 index 00000000000..ee898e1056c --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_cancellation.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" + +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class CancellationTest < Minitest::Test + def test_cancellation_sync_call_returns_none + assert_nil BamlSdk::ThrowsTest.sleep_ms(1) + end + + def test_cancellation_async_call_returns_none + assert_nil BamlSdk::ThrowsTest.sleep_ms_async(1).wait + end + + def test_cancellation_sync_cancel_via_call_context + token = BamlSdk::Baml::Spawn::CancelToken.baml_new + start = monotonic + timer = Thread.new { sleep 0.05; token.cancel } + error = assert_raises(Baml::CancelledError) do + BamlSdk::ThrowsTest.sleep_ms(60_000, _baml: Baml::Options.new(cancel: token)) + end + assert_cancelled(error, start) + ensure + timer&.kill&.join + end + + def test_cancellation_async_cancel_via_call_context + token = BamlSdk::Baml::Spawn::CancelToken.baml_new + start = monotonic + call = BamlSdk::ThrowsTest.sleep_ms_async(60_000, _baml: Baml::Options.new(cancel: token)) + sleep 0.05 + token.cancel + assert_cancelled(assert_raises(Baml::CancelledError) { call.wait(timeout: 5) }, start) + end + + def test_cancellation_async_cancel_via_task_cancel + start = monotonic + call = BamlSdk::ThrowsTest.sleep_ms_async(60_000) + sleep 0.05 + assert call.cancel + assert_cancelled(assert_raises(Baml::CancelledError) { call.wait(timeout: 5) }, start) + end + + def test_cancellation_async_cancel_via_task_group_sibling + # Ruby has no TaskGroup. Like Java's port, a failing sibling explicitly + # cancels the other call, and the original failure remains observable. + start = monotonic + call = BamlSdk::ThrowsTest.sleep_ms_async(60_000) + sibling = Thread.new do + sleep 0.05 + raise "cancel siblings" + rescue RuntimeError => error + call.cancel + error + end + assert_instance_of RuntimeError, sibling.value + assert_equal "cancel siblings", sibling.value.message + assert_cancelled(assert_raises(Baml::CancelledError) { call.wait(timeout: 5) }, start) + end + + def test_cancellation_async_cancel_via_asyncio_timeout + # A bounded wait leaves the call running; explicit cancel cleans it up. + start = monotonic + call = BamlSdk::ThrowsTest.sleep_ms_async(60_000) + assert_raises(Baml::WaitTimeout) { call.wait(timeout: 0.05) } + refute call.done? + assert call.cancel + assert_cancelled(assert_raises(Baml::CancelledError) { call.wait(timeout: 5) }, start) + + start = monotonic + timed = BamlSdk::ThrowsTest.sleep_ms_async(60_000, _baml: Baml::Options.new(timeout_ms: 50)) + assert_cancelled(assert_raises(Baml::CancelledError) { timed.wait(timeout: 5) }, start) + end + + private + + def monotonic + Process.clock_gettime(Process::CLOCK_MONOTONIC) + end + + def assert_cancelled(error, start) + assert_kind_of Baml::Panic, error + refute_kind_of StandardError, error + assert_instance_of BamlSdk::Baml::Panics::Cancelled, error.value + # Java's 5s bound avoids loaded-runner flakes; the call would take 60s. + assert_operator monotonic - start, :<, 5 + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_errors.rb b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_errors.rb new file mode 100644 index 00000000000..37ee8e0a18d --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_errors.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "open3" +require "rbconfig" +require "baml_sdk" + +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class ErrorsTest < Minitest::Test + def test_errors_user_panic_surfaces_as_baml_panic + error = assert_raises(Baml::Panic) { BamlSdk::ThrowsTest.do_panic("user-initiated boom") } + assert_instance_of BamlSdk::Baml::Panics::UserPanic, error.value + assert_equal "baml.panics.UserPanic", error.type_name + assert_equal "user-initiated boom", error.message + refute_kind_of StandardError, error + end + + def test_errors_union_throws_preserves_class_name + single = assert_raises(Baml::Error) { BamlSdk::RaisesTest.reparse("x") } + union = assert_raises(Baml::Error) { BamlSdk::RaisesTest.load_doc("x") } + assert_equal "user.raises_test.ParseError", single.type_name + assert_equal single.type_name, union.type_name + assert_instance_of BamlSdk::RaisesTest::ParseError, union.value + assert_equal "x", single.message + assert_equal single.message, union.message + end + + def test_errors_stdlib_error_surfaces_as_baml_error + error = assert_raises(Baml::Error) { BamlSdk::ThrowsTest.parse_json("{not valid json") } + assert_instance_of BamlSdk::Baml::Json::ParseError, error.value + end + + def test_errors_user_throw_surfaces_declared_instance + error = assert_raises(Baml::Error) { BamlSdk::ThrowsTest.throw_my_error } + assert_instance_of BamlSdk::ThrowsTest::MyError, error.value + end + + def test_errors_host_invalid_argument_wraps_baml_errors_invalid_argument + # Ruby binds generated arguments before calling BAML. An undeclared + # keyword raises ArgumentError, rather than a synthesized BAML payload. + assert_raises(ArgumentError) { BamlSdk.hello_world(not_a_param: 2) } + error = assert_raises(Baml::Error) do + Baml::Bridge.call_async("user.hello_world", { "not_a_param" => 2 }).wait + end + assert_instance_of BamlSdk::Baml::Errors::InvalidArgument, error.value + end + + def test_errors_cancellation_surfaces_as_baml_panic + token = BamlSdk::Baml::Spawn::CancelToken.baml_new + call = BamlSdk::ThrowsTest.sleep_ms_async(60_000, _baml: Baml::Options.new(cancel: token)) + timer = Thread.new { sleep 0.1; token.cancel } + error = assert_raises(Baml::CancelledError) { call.wait(timeout: 5) } + assert_kind_of Baml::Panic, error + refute_kind_of StandardError, error + assert_instance_of BamlSdk::Baml::Panics::Cancelled, error.value + timer.join + ensure + timer&.kill&.join + end + + def test_errors_str_is_non_empty + error = assert_raises(Baml::Error) { BamlSdk::ThrowsTest.parse_json("{not valid json") } + refute_empty error.to_s + end + + def test_errors_baml_error_carries_baml_trace + error = assert_raises(Baml::Error) { BamlSdk::ThrowsTest.throw_my_error } + assert_instance_of Array, error.baml_trace + refute_empty error.baml_trace + frame = /\AFile "([^"]*)", line (\d+), in (.*)\z/.match(error.baml_trace.last) + refute_nil frame + assert frame[1].end_with?("types.baml"), frame[1] + assert_operator frame[2].to_i, :>=, 1 + assert_equal "user.throws_test.ThrowMyError", frame[3] + end + + def test_errors_baml_trace_spliced_into_python_traceback + # Like Java's port, assert Ruby's native rendering includes BAML frames. + error = assert_raises(Baml::Error) { BamlSdk::ThrowsTest.parse_json("{not valid json") } + refute_empty error.baml_trace + rendered = error.full_message + assert_includes rendered, "types.baml" + assert_includes rendered, "user.throws_test.ParseJson" + end + + def test_errors_clean_exit_terminates_process_with_code_0 + assert_exit_code(0) + end + + def test_errors_clean_exit_terminates_process_with_code_7 + assert_exit_code(7) + end + + # SDK_PARITY_LINT(skip): Ruby error wrapper for a thrown string + def test_primitive_thrown_error + error = assert_raises(Baml::Error) { BamlSdk.unhandled_spawn_error } + assert_equal "string", error.type_name + assert_equal "boom", error.message + assert_equal "boom", error.value + end + + # SDK_PARITY_LINT(skip): Ruby error messages use a detail field and never inspect a struct + def test_error_message_prefers_detail_and_falls_back_to_the_type_name + value = BamlSdk::Ai::Errors::PreviewUnsupported.new(provider: "test", detail: "image output is unavailable") + error = Baml::Error.new(value, type_name: "ai.errors.PreviewUnsupported") + assert_equal "image output is unavailable", error.message + assert_same value, error.value + assert_equal "user.throws_test.MyError", Baml::Error.new( + BamlSdk::ThrowsTest::MyError.new(code: 7, detail: ""), type_name: "user.throws_test.MyError" + ).message + end + + private + + def assert_exit_code(code) + %w[do_exit do_exit_async].each do |method| + stdout, stderr, status = Open3.capture3( + RbConfig.ruby, "-I", $LOAD_PATH.join(File::PATH_SEPARATOR), "-rbaml_sdk", "-e", + "at_exit { puts 'UNREACHABLE' }; print 'before exit'; BamlSdk::ThrowsTest.#{method}(#{code}); sleep 30; puts 'UNREACHABLE'" + ) + assert_equal code, status.exitstatus, "#{stdout}\n#{stderr}" + assert_equal "before exit", stdout + refute_includes stdout, "UNREACHABLE" + end + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_generic_calls.rb b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_generic_calls.rb new file mode 100644 index 00000000000..00a04e31b0e --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_generic_calls.rb @@ -0,0 +1,209 @@ +# frozen_string_literal: true + +require "minitest/autorun" +require "baml_sdk" +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class GenericCallsTest < Minitest::Test + G = BamlSdk::GenericTests + Types = Baml::Types + + def bound(klass, *types, **fields) + Baml.bind_types(klass.new(**fields), *types) + end + + # SDK_PARITY_LINT(skip): Ruby stores erased generic bindings in a reserved instance variable + def test_reserved_binding_field_does_not_overwrite_type_arguments + m = BamlSdk::MethodsOnClasses + plain = m::BindingField.new(__baml_type_args_: "plain") + assert_equal "plain", m.binding_field(plain).__baml_type_args_ + generic = m::GenericBindingField.new(__baml_type_args_: "generic", value: 3) + assert_same generic, Baml.bind_types(generic, Integer) + assert_equal "generic", generic.__baml_type_args_ + result = m.generic_binding_field(generic) + assert_equal "generic", result.__baml_type_args_ + assert_equal 3, result.value + assert_equal [[:int]], Types.instance_args(generic) + assert_equal [[:int]], Types.instance_args(result) + end + + # SDK_PARITY_LINT(skip): Ruby named _types must include bounded-only static class parameters + def test_bounded_class_parameter_unused_by_static_method_is_sent + klass = BamlSdk::MethodsOnClasses::RubyTagged + assert_raises(TypeError) { klass.zero } + assert_equal 0, klass.zero(_types: { T: Integer }) + assert_equal 0, klass.zero_async(_types: { T: Integer }).wait + assert_raises(TypeError) { klass.zero(_types: { Unknown: Integer }) } + end + + def test_generic_calls_identity_explicit + assert_equal 5, G.identity(5, _types: { T: Integer }) + assert_equal "hi", G.identity("hi", _types: { T: String }) + pair = G::StringIntPair.new(my_string: "a", my_int: 1) + assert_equal pair, G.identity(pair, _types: { T: G::StringIntPair }) + box = bound(G::GenericBox, Types.generic(G::GenericBox, String), value: bound(G::GenericBox, String, value: "hello")) + assert_equal box, G.identity(box, _types: { T: Types.generic(G::GenericBox, Types.generic(G::GenericBox, String)) }) + triple = bound(G::GenericTriple, Types.generic(G::GenericBox, String), Float, T::Boolean, + first: bound(G::GenericBox, String, value: "hello"), second: [1.1, 2.2], third: { "lorem" => true, "ipsum" => false }) + assert_equal triple, G.identity(triple, _types: { T: Types.generic(G::GenericTriple, Types.generic(G::GenericBox, String), Float, T::Boolean) }) + end + + def test_generic_calls_identity_async_explicit + assert_equal 7, G.identity_async(7, _types: { T: Integer }).wait + end + + def test_generic_calls_tag_or_value_explicit + assert_equal "int", G.tag_or_value(5, _types: { T: Integer }) + assert_equal "string", G.tag_or_value("plain", _types: { T: String }) + pair = G::StringIntPair.new(my_string: "b", my_int: 2) + assert_includes G.tag_or_value(pair, _types: { T: G::StringIntPair }), "StringIntPair" + end + + def test_generic_calls_make_triple_explicit + triple = G.make_triple(1, ["a", "b"], { "k" => true }, _types: { A: Integer, B: String, C: T::Boolean }) + assert_instance_of G::GenericTriple, triple + assert_equal 1, triple.first + assert_equal ["a", "b"], triple.second + assert_equal({ "k" => true }, triple.third) + end + + def test_generic_calls_one_type_arg_explicit + assert_equal "int", G.one_type_arg(_types: { T: Integer }) + assert_equal "string", G.one_type_arg(_types: { T: String }) + nested = G.one_type_arg(_types: { T: Types.generic(G::GenericBox, Integer) }) + assert_includes nested, "GenericBox" + assert_includes nested, "int" + end + + def test_generic_calls_two_type_args_explicit + assert_equal "int | string", G.two_type_args(_types: { A: Integer, B: String }) + end + + def test_generic_calls_generic_free_fn_requires_binding + error = assert_raises(TypeError) { G.one_type_arg } + assert_includes error.message, "could not infer a type" + assert_includes error.message, "one_type_arg" + error = assert_raises(TypeError) { G.two_type_args } + assert_includes error.message, "could not infer a type" + end + + def test_generic_calls_subscript_wrong_arity_raises + # Like Java, named bindings permit partial arity; body-only B cannot infer. + assert_raises(TypeError) { G.two_type_args(_types: { A: Integer }) } + end + + def test_generic_calls_consume_int_wrapper_baseline + assert_equal 9, G.consume_int_wrapper(bound(G::GenericBox, Integer, value: 9)) + end + + def test_generic_calls_genericbox_get_explicit + assert_equal "int", bound(G::GenericBox, Integer, value: 5).get + end + + def test_generic_calls_genericbox_pair_with_explicit + box = bound(G::GenericBox, Integer, value: 5) + assert_equal "int | string", box.pair_with("hello world", _types: { U: String }) + end + + def test_generic_calls_genericbox_new_static_explicit + box = G::GenericBox.baml_new(5, _types: { V: Integer }) + assert_instance_of G::GenericBox, box + assert_equal 5, box.value + end + + def test_generic_calls_generic_static_infers_binding + box = G::GenericBox.baml_new(5) + assert_instance_of G::GenericBox, box + assert_equal 5, box.value + assert_equal 9, G::GenericBox.static_echo(9, _types: { T: Integer }) + assert_equal "echo", G::GenericBox.static_echo_async("echo", _types: { T: String }).wait + assert_equal "int | string", G::GenericBox.static_pair(1, "x", _types: { T: Integer, U: String }) + assert_equal [7, 7], BamlSdk::Baml::Array.filled(2, 7, _types: { T: Integer }) + assert_equal 1, G::GenericBox.baml_new(1, _types: { T: Integer }).value + assert_raises(TypeError) { G::GenericBox.baml_new(1, _types: { Unknown: Integer }) } + end + + def test_generic_calls_named_static_distinct_typevar_names + assert_equal "int | string", G::NamedStatic.make(1, "x", _types: { D: Integer, E: String }) + end + + def test_generic_calls_instance_method_unparameterized_receiver_raises + error = assert_raises(TypeError) { G::GenericBox.new(value: 5).pair_with("x", _types: { U: String }) } + assert_includes error.message, "receiver" + end + + def test_generic_calls_extract_explicit + pair = bound(G::GenericPair, Types.generic(G::GenericPair, Integer, String), Types.generic(G::GenericPair, T::Boolean, Float), + first: bound(G::GenericPair, Integer, String, first: 1, second: "a"), + second: bound(G::GenericPair, T::Boolean, Float, first: true, second: 1.5)) + assert_equal "int | string | bool | float", G.extract(pair, _types: { A: Integer, B: String, C: T::Boolean, D: Float }) + end + + def test_generic_calls_parse_as_explicit + assert_equal G::StringIntPair.new(my_string: "x", my_int: 3), G.parse_as('{"my_string": "x", "my_int": 3}', _types: { T: G::StringIntPair }) + assert_equal 42, G.parse_as("42", _types: { T: Integer }) + end + + def test_generic_calls_second_of_explicit + assert_equal "hi", G.second_of(bound(G::GenericPair, Integer, String, first: 1, second: "hi"), _types: { T: String }) + pair = G::StringIntPair.new(my_string: "z", my_int: 9) + assert_equal pair, G.second_of(bound(G::GenericPair, Integer, G::StringIntPair, first: 0, second: pair), _types: { T: G::StringIntPair }) + end + + def test_generic_calls_list_head_explicit + linked = bound(G::GenericRecursive, Integer, value: 7, next_: bound(G::GenericRecursive, Integer, value: 8, next_: nil)) + assert_equal 7, G.list_head(linked, _types: { T: Integer }) + end + + def test_generic_calls_choose_explicit + assert_equal 1, G.choose(1, 2, _types: { T: Integer }) + assert_equal "a", G.choose("a", "b", _types: { T: String }) + end + + def test_generic_calls_read_items_explicit + container = bound(G::ContainerShapes, Integer, item: 1, items: [1, 2, 3], by_key: { "k" => 4 }, maybe: nil, mixed: nil) + assert_equal [1, 2, 3], G.read_items(container, _types: { T: Integer }) + end + + def test_generic_calls_wrap_explicit + box = G.wrap(5, _types: { T: Integer }) + assert_instance_of G::GenericBox, box + assert_equal 5, box.value + end + + def test_generic_calls_make_int_box_reified + box = G.make_int_box + assert_instance_of G::GenericBox, box + assert_equal [Types.token(Integer)], Types.instance_args(box) + assert_equal 7, box.value + end + + def test_generic_calls_make_int_container_reified + container = G.make_int_container + assert_instance_of G::ContainerShapes, container + assert_equal [Types.token(Integer)], Types.instance_args(container) + assert_equal 1, container.item + assert_equal [1, 2, 3], container.items + assert_equal({ "k" => 4 }, container.by_key) + assert_nil container.maybe + assert_equal 5, container.mixed + end + + def test_generic_calls_make_nested_box_reified + outer = G.make_nested_box + assert_instance_of G::GenericBox, outer + assert_equal [Types.generic(G::GenericBox, Integer)], Types.instance_args(outer) + assert_instance_of G::GenericBox, outer.value + assert_equal [Types.token(Integer)], Types.instance_args(outer.value) + assert_equal 9, outer.value.value + end + + def test_generic_calls_make_int_str_bool_triple_reified + triple = G.make_int_str_bool_triple + assert_instance_of G::GenericTriple, triple + assert_equal [Types.token(Integer), Types.token(String), Types.token(T::Boolean)], Types.instance_args(triple) + assert_equal 1, triple.first + assert_equal ["a", "b"], triple.second + assert_equal({ "k" => true }, triple.third) + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_generic_inference.rb b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_generic_inference.rb new file mode 100644 index 00000000000..0d14b1dbb86 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_generic_inference.rb @@ -0,0 +1,337 @@ +# frozen_string_literal: true + +require "minitest/autorun" +require "baml_sdk" +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class GenericInferenceTest < Minitest::Test + G = BamlSdk::GenericTests + Types = Baml::Types + + def bound(klass, *types, **fields) + Baml.bind_types(klass.new(**fields), *types) + end + + def assert_type_error(*needles) + error = assert_raises(TypeError) { yield } + needles.each { |needle| assert_includes error.message, needle } + end + + def test_generic_inference_apply_closure_poisons_typevars_must_specify + assert_type_error("could not infer a type", "apply") { G.apply(->(value) { value + 1 }, 5) } + end + + def test_generic_inference_apply_closure_typevars_specified_succeeds + assert_equal 6, G.apply(->(value) { value + 1 }, 5, _types: { T: Integer, R: Integer }) + end + + def test_generic_inference_identity_infers_primitives + assert_equal 5, G.identity(5) + assert_equal "hi", G.identity("hi") + assert_same true, G.identity(true) + end + + def test_generic_inference_identity_infers_user_class + pair = G::StringIntPair.new(my_string: "a", my_int: 1) + assert_equal pair, G.identity(pair) + end + + def test_generic_inference_identity_infers_generic_instance + box = bound(G::GenericBox, Integer, value: 5) + assert_equal box, G.identity(box) + nested = bound(G::GenericBox, Types.generic(G::GenericBox, String), value: bound(G::GenericBox, String, value: "hello")) + assert_equal nested, G.identity(nested) + end + + def test_generic_inference_identity_async_infers + assert_equal 7, G.identity_async(7).wait + end + + def test_generic_inference_identity_null_binds_null + assert_nil G.identity(nil) + end + + def test_generic_inference_identity_unbound_generic_instance_round_trips + unbound = G::GenericBox.new(value: 5) + assert_equal bound(G::GenericBox, Integer, value: 5), G.identity(unbound) + assert_empty Types.instance_args(unbound) + assert_equal 5, unbound.value + end + + def test_generic_inference_make_triple_infers_multiple_typevars + triple = G.make_triple(1, ["a", "b"], { "k" => true }) + assert_instance_of G::GenericTriple, triple + assert_equal 1, triple.first + assert_equal ["a", "b"], triple.second + assert_equal({ "k" => true }, triple.third) + end + + def test_generic_inference_second_of_infers_from_nested_generic + assert_equal "hi", G.second_of(bound(G::GenericPair, Integer, String, first: 1, second: "hi")) + pair = G::StringIntPair.new(my_string: "z", my_int: 9) + assert_equal pair, G.second_of(bound(G::GenericPair, Integer, G::StringIntPair, first: 0, second: pair)) + end + + def test_generic_inference_read_items_infers_from_instance_wire_args + container = bound(G::ContainerShapes, Integer, item: 1, items: [1, 2, 3], by_key: { "k" => 4 }, maybe: nil, mixed: nil) + assert_equal [1, 2, 3], G.read_items(container) + empty_fields = bound(G::ContainerShapes, Integer, item: 1, items: [], by_key: {}, maybe: nil, mixed: nil) + assert_equal [], G.read_items(empty_fields) + end + + def test_generic_inference_list_head_infers_from_recursive_generic + linked = bound(G::GenericRecursive, Integer, value: 7, next_: bound(G::GenericRecursive, Integer, value: 8, next_: nil)) + assert_equal 7, G.list_head(linked) + end + + def test_generic_inference_extract_infers_four_typevars_from_nesting + pair = bound(G::GenericPair, Types.generic(G::GenericPair, Integer, String), Types.generic(G::GenericPair, T::Boolean, Float), + first: bound(G::GenericPair, Integer, String, first: 1, second: "a"), + second: bound(G::GenericPair, T::Boolean, Float, first: true, second: 1.5)) + assert_equal "int | string | bool | float", G.extract(pair) + end + + def test_generic_inference_choose_infers_unified_typevar + assert_equal 5, G.choose(5, 6) + assert_equal "a", G.choose("a", "b") + end + + def test_generic_inference_choose_infers_divergent_union + assert_equal 5, G.choose(5, "asdf") + end + + def test_generic_inference_make_triple_partial_explicit_then_infer + triple = G.make_triple(1, ["x", "y"], { "k" => true }, _types: { A: Integer }) + assert_instance_of G::GenericTriple, triple + assert_equal 1, triple.first + assert_equal ["x", "y"], triple.second + assert_equal({ "k" => true }, triple.third) + end + + def test_generic_inference_wrap_infers_and_returns_generic + box = G.wrap(5) + assert_instance_of G::GenericBox, box + assert_equal 5, box.value + end + + def test_generic_inference_genericbox_pair_with_infers_method_typevar + assert_equal "int | string", bound(G::GenericBox, Integer, value: 5).pair_with("hello world") + end + + def test_generic_inference_generic_static_infers_own_typevar + box = G::GenericBox.baml_new(5) + assert_instance_of G::GenericBox, box + assert_equal 5, box.value + end + + def test_generic_inference_named_static_infers_distinct_typevars + assert_equal "int | string", G::NamedStatic.make(1, "x") + end + + def test_generic_inference_union_with_concrete_sibling_infers_typevar + assert_equal "int", G.tag_or_value(5) + end + + def test_generic_inference_union_concrete_sibling_absorbs_value_binds_never + assert_equal "never", G.tag_or_value("hi") + assert_equal "int", G.tag_or_value("hi", _types: { T: Integer }) + end + + def test_generic_inference_union_null_actual_binds_never + assert_equal "never", G.tag_or_value(nil) + end + + def test_generic_inference_return_only_var_still_requires_binding + assert_type_error("could not infer a type", "parse_as") { G.parse_as("42") } + end + + def test_generic_inference_body_only_var_still_requires_binding + assert_type_error("could not infer a type", "one_type_arg") { G.one_type_arg } + end + + def test_generic_inference_pair_invariant_list_conflict_rejects + assert_type_error("can't be reconciled", "pair", "int", "string") { G.pair([1, 2], ["a", "b"]) } + end + + def test_generic_inference_pair_invariant_list_agree_binds + assert_equal "int", G.pair([1, 2], [3, 4]) + end + + def test_generic_inference_choose_union_outside_container_is_sound + assert_equal [1, 2], G.choose([1, 2], ["a"]) + end + + def test_generic_inference_merge_invariant_map_value_conflict_rejects + assert_type_error("can't be reconciled", "merge") { G.merge({ "k" => 1 }, { "k" => "a" }) } + end + + def test_generic_inference_combine_invariant_class_arg_conflict_rejects + assert_type_error("can't be reconciled", "combine") do + G.combine(bound(G::GenericBox, Integer, value: 1), bound(G::GenericBox, String, value: "x")) + end + end + + def test_generic_inference_glue_invariant_vs_covariant_conflict_rejects + assert_type_error("can't be reconciled", "glue") { G.glue(1, ["a"]) } + end + + def test_generic_inference_glue_invariant_and_covariant_agree_binds + assert_equal "int", G.glue(1, [2, 3]) + end + + def test_generic_inference_two_typevar_union_is_uninferrable_rejects + assert_type_error("two_in_union") { G.two_in_union("hello") } + end + + def test_generic_inference_triple_choose_three_covariant_join + assert_equal "int | string | bool", G.triple_choose(5, "asdf", true) + end + + def test_generic_inference_make_triple_heterogeneous_list_element_unions + triple = G.make_triple(1, [1, "x"], { "k" => true }) + assert_instance_of G::GenericTriple, triple + assert_equal [1, "x"], triple.second + end + + def test_generic_inference_choose_divergent_generic_instances_union + left = bound(G::GenericBox, Integer, value: 1) + assert_equal left, G.choose(left, bound(G::GenericBox, String, value: "x")) + end + + def test_generic_inference_tag_or_value_binds_generic_instance + rendered = G.tag_or_value(bound(G::GenericBox, String, value: "asdf")) + assert_includes rendered, "GenericBox" + assert_includes rendered, "string" + end + + def test_generic_inference_first_or_empty_list_requires_binding + assert_type_error("could not infer a type", "first_or") { G.first_or([]) } + assert_nil G.first_or([], _types: { T: Integer }) + end + + def test_generic_inference_first_or_nonempty_infers_element + assert_equal 7, G.first_or([7, 8, 9]) + end + + def test_generic_inference_values_of_empty_map_requires_binding + assert_type_error("could not infer a type", "values_of") { G.values_of({}) } + assert_equal [], G.values_of({}, _types: { T: Integer }) + end + + def test_generic_inference_values_of_nonempty_returns_values + assert_equal [1, 2], G.values_of({ "a" => 1, "b" => 2 }) + end + + # SDK_PARITY_LINT(skip): Ruby uses named _types hashes and has no callable-subscript arity surface + def test_generic_inference_make_triple_partial_subscript_requires_full_arity + skip "Ruby has no callable-subscript arity surface" + end + + def test_generic_inference_make_triple_subscript_fully_bound + triple = G.make_triple(5, ["x"], { "k" => true }, _types: { A: Integer, B: String, C: T::Boolean }) + assert_instance_of G::GenericTriple, triple + assert_equal 5, triple.first + assert_equal ["x"], triple.second + assert_equal({ "k" => true }, triple.third) + end + + def test_generic_inference_one_type_arg_explicit_types_succeeds + assert_equal "int", G.one_type_arg(_types: { T: Integer }) + end + + def test_generic_inference_parse_as_explicit_types_succeeds + assert_equal 42, G.parse_as("42", _types: { T: Integer }) + end + + def test_generic_inference_second_of_unbound_instance_recovers_field_type + assert_equal "hi", G.second_of(G::GenericPair.new(first: 1, second: "hi")) + end + + def test_generic_inference_identity_nested_unbound_round_trips + nested = G::GenericBox.new(value: G::GenericBox.new(value: "hello")) + expected = bound(G::GenericBox, Types.generic(G::GenericBox, String), value: bound(G::GenericBox, String, value: "hello")) + assert_equal expected, G.identity(nested) + assert_empty Types.instance_args(nested) + assert_empty Types.instance_args(nested.value) + assert_equal "hello", nested.value.value + end + + def test_generic_inference_wrap_infers_and_returns_bound_generic + assert_equal bound(G::GenericBox, Integer, value: 5), G.wrap(5) + end + + def test_generic_inference_maybe_id_present_value_infers + assert_equal 5, G.maybe_id(5) + end + + def test_generic_inference_maybe_id_null_round_trips + assert_nil G.maybe_id(nil) + assert_nil G.maybe_id(nil, _types: { T: Integer }) + end + + def test_generic_inference_identity_enum_round_trips + result = G.identity(G::SomeEnum::VARIANT) + assert_same G::SomeEnum::VARIANT, result + assert_instance_of G::SomeEnum, result + end + + def test_generic_inference_host_only_object_not_encodable_from_python + error = assert_raises(TypeError) { G.identity(Object.new) } + assert_includes error.message, "cannot encode" + assert_includes error.message, "no BAML type" + end + + def test_generic_inference_genericbox_get_infers_class_var_from_receiver + assert_equal "int", bound(G::GenericBox, Integer, value: 5).get + end + + def test_generic_inference_genericbox_pair_with_unbound_receiver_recovers_class_var + assert_equal "int | string", G::GenericBox.new(value: 5).pair_with("x") + end + + def test_generic_inference_make_triple_types_kwarg_contradicted_by_actual_rejects + assert_type_error("make_triple") { G.make_triple("nope", ["x"], { "k" => true }, _types: { A: Integer }) } + end + + def test_generic_inference_make_triple_full_subscript_contradicted_by_actual_rejects + assert_type_error("make_triple") { G.make_triple("nope", ["x"], { "k" => true }, _types: { A: Integer, B: String, C: T::Boolean }) } + end + + def test_generic_inference_elem_type_heterogeneous_array_unifies + assert_equal "int | string", G.elem_type([1, "x"]) + end + + def test_generic_inference_elem_type_homogeneous_array_is_single_type + assert_equal "int", G.elem_type([1, 2, 3]) + end + + def test_generic_inference_elem_type_three_way_heterogeneous_array_unifies + rendered = G.elem_type([1, "x", true]) + %w[int string bool].each { |type| assert_includes rendered, type } + end + + def test_generic_inference_read_items_unbound_container_recovers_t_from_fields + unbound = G::ContainerShapes.new(item: 1, items: [1, 2, 3], by_key: { "k" => 4 }, maybe: nil, mixed: nil) + assert_equal [1, 2, 3], G.read_items(unbound) + end + + def test_generic_inference_list_head_unbound_recursive_recovers_t_from_fields + unbound = G::GenericRecursive.new(value: 7, next_: G::GenericRecursive.new(value: 8, next_: nil)) + assert_equal 7, G.list_head(unbound) + end + + def test_generic_inference_extract_fully_unbound_nested_pair_recovers_all_vars + pair = G::GenericPair.new(first: G::GenericPair.new(first: 1, second: "a"), second: G::GenericPair.new(first: true, second: 1.5)) + assert_equal "int | string | bool | float", G.extract(pair) + end + + def test_generic_inference_triple_choose_join_includes_concrete_class + rendered = G.triple_choose(5, G::StringIntPair.new(my_string: "a", my_int: 1), "x") + %w[int StringIntPair string].each { |type| assert_includes rendered, type } + end + + def test_generic_inference_triple_choose_join_includes_enum_variant + rendered = G.triple_choose(5, G::SomeEnum::VARIANT, G::StringIntPair.new(my_string: "a", my_int: 1)) + %w[int SomeEnum StringIntPair].each { |type| assert_includes rendered, type } + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_host_callables.rb b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_host_callables.rb new file mode 100644 index 00000000000..c9c17a94b19 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_host_callables.rb @@ -0,0 +1,238 @@ +# frozen_string_literal: true + +require "minitest/autorun" +require "baml_sdk" + +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class HostCallablesTest < Minitest::Test + Fns = BamlSdk::HostCallableTests + + class MyDomainError < StandardError + attr_reader :code + + def initialize(message, code) + @code = code + super(message) + end + end + + def test_host_callables_simple_sync_callable_returns_string + cb = Object.new + cb.define_singleton_method(:call) { |x| "got #{x}" } + assert_equal "got 5", Fns.call_with_callback(cb, 5) + assert_equal [0, 2, 4], BamlSdk::Baml::Array.generate(3, ->(index) { index * 2 }, _types: { T: Integer, E: [:never] }) + end + + def test_host_callables_two_arg_callable_unpacks_positional_args + assert_equal "answer:7", Fns.call_with_two_args(->(x, prefix) { "#{prefix}:#{x}" }, 7, "answer") + end + + def test_host_callables_int_return_callable_round_trip + assert_equal 42, Fns.call_int_callback(->(x) { x * 2 }, 21) + end + + def test_host_callable_two_cbs_callbacks_named_f_and_f_returns_both_run + assert_equal 210, Fns.two_cbs(->(x) { x * 10 }, ->(x) { x * 100 }) + end + + def test_host_callable_hof_receives_a_baml_closure + received = [] + cb = ->(f) { + received << f + assert_instance_of Baml::Closure, f + f.call(41) * 100 + f.call(x: 1) * 2 + } + assert_equal 4204, Fns.hof(cb) + assert_equal 1, received.length + ensure + received&.each(&:close) + end + + def test_baml_closure_is_a_native_callable_with_host_language_arguments + add_ten = T.let(Fns.make_adder(10), T.any(T.proc.params(value: Integer).returns(Integer), Baml::Closure)) + assert_respond_to add_ten, :call + assert_instance_of Baml::Closure, add_ten + assert_equal 15, add_ten.call(5) + assert_equal 17, add_ten.call_async(7).wait + ensure + add_ten&.close + end + + def test_baml_closure_decodes_multiple_args_and_structured_return_values + build = Fns.make_pair_builder(30) + first = build.call(12, "Ada") + assert_instance_of Fns::Person, first + assert_equal ["Ada", 42], [first.name, first.age] + second = build.call(delta: 5, label: "Grace") + assert_instance_of Fns::Person, second + assert_equal ["Grace", 35], [second.name, second.age] + ensure + build&.close + end + + def test_baml_closure_is_reusable_and_retains_mutable_captures + next_value = Fns.make_counter(40) + assert_equal 41, next_value.call + assert_equal 42, next_value.call + ensure + next_value&.close + end + + def test_host_callable_void_signatures + seen = [] + one = ->(value) { seen << [value]; nil } + two = ->(value, label) { seen << [value, label]; nil } + three = ->(value, label, flag) { seen << [value, label, flag]; nil } + assert_equal 1, Fns.call_void_callback(one, 1) + assert_equal 2, Fns.call_void_callback_two_args(two, 2, "two") + assert_equal 3, Fns.call_void_callback_three_args(three, 3, "three", true) + assert_nil Fns.call_throwing_void_callback(one, 4) + raised = ArgumentError.new("void failed") + assert_same raised, assert_raises(ArgumentError) { + Fns.call_throwing_void_callback(->(_value) { raise raised }, 4) + } + void = Fns.make_void_callback + assert_nil void.call + forward = Fns.make_void_forwarder(two) + assert_nil forward.call(5, "five") + assert_nil forward.call(value: 6, label: "six") + assert_equal [[1], [2, "two"], [3, "three", true], [4], [5, "five"], [6, "six"]], seen + ensure + void&.close + forward&.close + end + + # SDK_PARITY_LINT(skip): Ruby exceptions returned as HostCallable data retain their original opaque object + def test_caught_ruby_exception_decodes_as_data_and_round_trips_with_identity + raised = MyDomainError.new("caught as data", 17) + model = Fns.catch_ruby_exception(-> { raise raised }) + assert_instance_of BamlSdk::Baml::Errors::HostCallable, model + assert_same raised, model._handle + received = nil + assert_equal 42, Fns.forward_ruby_exception(model, ->(value) { + received = value._handle + 42 + }) + assert_same raised, received + end + + def test_host_callables_throwing_callable_round_trips_original_python_exception + raised = ArgumentError.new("nope") + caught = assert_raises(ArgumentError) { Fns.call_with_callback(->(_x) { raise raised }, 1) } + assert_same raised, caught + assert_equal "nope", caught.message + end + + def test_host_callables_throwing_callable_keyerror_round_trips_with_identity + raised = KeyError.new("missing") + assert_same raised, assert_raises(KeyError) { Fns.call_with_callback(->(_x) { raise raised }, 1) } + end + + def test_host_callables_throwing_callable_custom_python_exception_round_trips_with_identity + raised = MyDomainError.new("custom domain failure", 42) + caught = assert_raises(MyDomainError) { Fns.call_with_callback(->(_x) { raise raised }, 1) } + assert_same raised, caught + assert_equal 42, caught.code + end + + def test_host_callables_throwing_callable_bamlerror_wrapping_codegenned_class_is_caught_in_baml + cb = ->(_x) { + raise Baml::Error.new(Fns::ValidationError.new(code: 4, message: "bad shape", fields: %w[name age email phone])) + } + assert_equal "caught: bad shape", Fns.call_with_typed_throws(cb, 1) + end + + def test_host_callables_throwing_callable_bamlerror_propagates_back_with_typed_fields + raised = Baml::Error.new(Fns::ValidationError.new(code: 7, message: "propagated through", fields: %w[x y])) + caught = assert_raises(Baml::Error) { Fns.call_with_typed_throws_propagating(->(_x) { raise raised }, 1) } + decoded = caught.value + assert_instance_of Fns::ValidationError, decoded + assert_equal 7, decoded.code + assert_equal "propagated through", decoded.message + assert_equal %w[x y], decoded.fields + end + + def test_host_callables_throwing_async_callable_round_trips_original_python_exception + # Like Java, join asynchronous work inside the callback; Ruby has no async def. + raised = ArgumentError.new("async nope") + cb = ->(_x) { + worker = Thread.new { Thread.current.report_on_exception = false; raise raised } + worker.value + } + assert_same raised, assert_raises(ArgumentError) { Fns.call_with_callback(cb, 1) } + end + + def test_host_callables_multiple_throws_in_flight_do_not_collide_in_registry + first = ArgumentError.new("first") + second = ArgumentError.new("second") + caught_first = assert_raises(ArgumentError) { Fns.call_with_callback(->(_x) { raise first }, 1) } + caught_second = assert_raises(ArgumentError) { Fns.call_with_callback(->(_x) { raise second }, 2) } + assert_same first, caught_first + assert_same second, caught_second + refute_same caught_first, caught_second + end + + # Ruby GC does not trigger engine GC, matching Python's xfail and Java's Disabled. + # SDK_PARITY_LINT(skip): callable release coverage depends on host weak-reference support and remains nondeterministic + def test_host_callables_release_fires_on_drop_of_callable + skip "Callable release follows engine GC, not Ruby GC or call completion" + end + + def test_host_callables_lambda_round_trip + assert_equal "lambda-99", Fns.call_with_callback(->(x) { "lambda-#{x}" }, 99) + end + + def test_host_callables_async_callable_runs_to_completion + # Resolve asynchronous work inside the callback, as the Java port does. + cb = ->(x) { Thread.new { Thread.pass; "async-#{x}" }.value } + assert_equal "async-4", Fns.call_with_callback(cb, 4) + end + + def test_host_callables_multiple_callable_keys_are_distinct + counter = { a: 0, b: 0 } + a = ->(x) { counter[:a] += 1; "a:#{x}" } + b = ->(x) { counter[:b] += 1; "b:#{x}" } + assert_equal "a:1", Fns.call_with_callback(a, 1) + assert_equal "b:2", Fns.call_with_callback(b, 2) + assert_equal({ a: 1, b: 1 }, counter) + end + + def test_host_callables_class_callback_round_trips_pydantic_model + person = Fns::Person.new(name: "Ada", age: 37) + assert_equal "Ada is 37", Fns.call_with_class_callback(->(p) { "#{p.name} is #{p.age}" }, person) + end + + def test_host_callables_call_repeatedly_invokes_callback_n_times + invocations = [] + cb = ->(x) { invocations << x; "item-#{x}" } + assert_equal 5.times.map { |i| "item-#{i}" }, Fns.call_repeatedly(cb, 5) + assert_equal (0...5).to_a, invocations + end + + def test_host_callables_call_repeatedly_with_zero_n_returns_empty_list + invocations = [] + assert_equal [], Fns.call_repeatedly(->(x) { invocations << x; "" }, 0) + assert_equal [], invocations + end + + def test_host_callables_call_with_throwing_in_baml_catches_host_callable_error + assert_equal "caught:RuntimeError", Fns.call_with_throwing(->(_x) { raise "boom from host" }, 1) + end + + def optional_args_cb + ->(x, y: 8, z: 9) { x * 100 + y * 10 + z } + end + + def test_host_callables_optional_args_all_unset_apply_host_defaults + assert_equal [589], Fns.call_callback_with_optional_args_all_unset(optional_args_cb, 5) + end + + def test_host_callables_optional_args_partially_set_deliver_by_name + assert_equal [529, 583], Fns.call_callback_with_optional_args_partially_set(optional_args_cb, 5) + end + + def test_host_callables_optional_args_all_set_deliver_both + assert_equal [523], Fns.call_callback_with_optional_args_all_set(optional_args_cb, 5) + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_invocation.rb b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_invocation.rb new file mode 100644 index 00000000000..5388817def3 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_invocation.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" + +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class InvocationTest < Minitest::Test + def test_four_call_forms + options = Baml::Options.new(timeout_ms: 1000) + assert_equal [1, 5, 99], BamlSdk.optional_args_probe(1) + assert_equal [1, 7, 99], BamlSdk.optional_args_probe(1, opt1: 7) + assert_equal [1, 5, 99], BamlSdk.optional_args_probe(1, _baml: options) + assert_equal [1, 7, 99], BamlSdk.optional_args_probe(1, opt1: 7, _baml: options) + end + + def test_four_call_forms_async + options = Baml::Options.new(timeout_ms: 1000) + assert_equal [1, 5, 99], BamlSdk.optional_args_probe_async(1).wait + assert_equal [1, 7, 99], BamlSdk.optional_args_probe_async(1, opt1: 7).wait + assert_equal [1, 5, 99], BamlSdk.optional_args_probe_async(1, _baml: options).wait + assert_equal [1, 7, 99], BamlSdk.optional_args_probe_async(1, opt1: 7, _baml: options).wait + end + + def test_empty_controls + assert_equal "hello world", BamlSdk.hello_world + assert_equal "hello world", BamlSdk.hello_world(_baml: Baml::Options.new) + assert_equal "hello world", BamlSdk.hello_world(_baml: nil) + assert_equal "hello world", BamlSdk.hello_world(_baml: Baml::Options.new(trace: nil, cancel: nil, timeout_ms: nil)) + end + + def test_omitted_argument_is_not_null + assert_equal [1, 5, 99], BamlSdk.optional_args_probe(1, _baml: Baml::Options.new) + assert_equal [1, nil, 99], BamlSdk.optional_args_probe(1, opt1: nil, _baml: Baml::Options.new) + end + + def test_invalid_timeout_rejected + [-1, 0.5, Float::INFINITY, Float::NAN, 2_147_483_648, true].each do |timeout| + assert_raises(ArgumentError) { BamlSdk.hello_world(_baml: Baml::Options.new(timeout_ms: timeout)) } + end + assert_raises(TypeError) { BamlSdk.hello_world(_baml: {}) } + assert_raises(ArgumentError) { Baml::Options.new(trace: Object.new) } + assert_raises(TypeError) { BamlSdk.hello_world(_baml: Baml::Options.new(cancel: Object.new)) } + end + + def test_timeout_upper_bound_accepted + assert_equal "hello world", BamlSdk.hello_world(_baml: Baml::Options.new(timeout_ms: 2_147_483_647)) + end + + def test_methods_accept_controls + options = Baml::Options.new + box = BamlSdk::OptBox.make(1, _baml: options) + assert_equal 8, box.base + assert_equal [8, 2, 5], box.probe(2, _baml: options) + assert_equal [8, 2, nil], box.probe(2, opt1: nil, _baml: options) + end + + def test_returned_callable_accepts_controls + add = BamlSdk::HostCallableTests.make_adder(3) + options = Baml::Options.new + assert_equal 7, add.call(4, _baml: options) + assert_equal 7, add.call_async(4, _baml: options).wait(timeout: 5) + ensure + add&.close + end + + def test_pre_cancelled_call_does_not_enter_callback + token = BamlSdk::Baml::Spawn::CancelToken.baml_new + token.cancel + calls = [] + callback = ->(value) { calls << value; value } + error = assert_raises(Baml::CancelledError) do + BamlSdk::HostCallableTests.call_int_callback_async(callback, 1, _baml: Baml::Options.new(cancel: token)).wait(timeout: 5) + end + assert_instance_of BamlSdk::Baml::Panics::Cancelled, error.value + assert_empty calls + end + + def test_zero_timeout_does_not_enter_callback + calls = [] + callback = ->(value) { calls << value; value } + error = assert_raises(Baml::CancelledError) do + BamlSdk::HostCallableTests.call_int_callback_async(callback, 1, _baml: Baml::Options.new(timeout_ms: 0)).wait(timeout: 5) + end + assert_instance_of BamlSdk::Baml::Panics::Cancelled, error.value + assert_empty calls + end + + def test_composite_token_observes_every_source + [0, 1].each do |index| + sources = Array.new(2) { BamlSdk::Baml::Spawn::CancelToken.baml_new } + combined = BamlSdk::Baml::Spawn::CancelToken.any(sources) + sources[index].cancel + assert combined.is_cancelled + error = assert_raises(Baml::CancelledError) do + BamlSdk.hello_world_async(_baml: Baml::Options.new(cancel: combined)).wait + end + assert_instance_of BamlSdk::Baml::Panics::Cancelled, error.value + refute sources[1 - index].is_cancelled + end + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_json.rb b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_json.rb new file mode 100644 index 00000000000..022f093369d --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_json.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" + +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class JsonTest < Minitest::Test + def test_json_returned_from_host_callback_supports_typed_narrowing + assert_equal "object", BamlSdk::GoJsonTests.json_callback_kind(->(value) { { "wrapped" => value } }, "payload") + end + + def test_host_supplied_json_supports_typed_narrowing + json = BamlSdk::GoJsonTests + obj = { "type" => "ok", "nested" => { "list" => [1, { "deep" => "found" }] } } + assert_equal "object", json.json_kind(obj) + assert_equal "array", json.json_kind([1]) + assert_equal "string", json.json_kind("text") + assert_equal "other", json.json_kind(3) + assert_equal "ok", json.json_path_string(obj, ".type") + assert_equal "found", json.json_path_string(obj, ".nested.list[1].deep") + assert_equal "fallback", json.json_path_string_or(obj, ".missing", "fallback") + error = assert_raises(Baml::Error) { json.json_path_string(obj, ".absent") } + assert_includes error.message, "missing field" + end + + def test_json_nested_deeper_than_the_wire_carries_is_refused_as_an_argument + assert_equal nested_json(30), BamlSdk::GoJsonTests.round_trip_json(nested_json(30)) + [60, 1000].each do |depth| + error = assert_raises(TypeError) { BamlSdk::GoJsonTests.round_trip_json(nested_json(depth)) } + assert_includes error.message, "nested too deeply" + end + end + + def test_json_nested_deeper_than_the_wire_carries_is_refused_as_a_callback_result + json = BamlSdk::GoJsonTests + assert_equal nested_json(30), json.call_json_callback(->(_) { nested_json(30) }, 1) + [60, 200].each do |depth| + error = assert_raises(Baml::Panic) { json.call_json_callback(->(_) { nested_json(depth) }, 1) } + assert_instance_of BamlSdk::Baml::Panics::SdkPanic, error.value + assert_includes error.value.message, "nested too deeply" + end + end + + private + + def nested_json(depth) + depth.times.reduce(1) { |value, _| [value] } + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_main.rb b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_main.rb new file mode 100644 index 00000000000..ef70a03c4bc --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_main.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" + +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class MainTest < Minitest::Test + def test_main_hello_world_returns_literal + assert_raises(ArgumentError) { BamlSdk.hello_world("extra") } + assert_equal "hello world", BamlSdk.hello_world + end + + def test_main_single_required_arg_round_trips + assert_raises(ArgumentError) { BamlSdk.single_required_arg } + assert_equal "hi", BamlSdk.single_required_arg("hi") + assert_raises(TypeError) { BamlSdk.single_required_arg(1) } + end + +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_methods_on_classes.rb b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_methods_on_classes.rb new file mode 100644 index 00000000000..67f21d6a796 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_methods_on_classes.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" + +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class MethodsOnClassesTest < Minitest::Test + Greeter = BamlSdk::MethodsOnClasses::Greeter + + # SDK_PARITY_LINT(skip): Ruby lifecycle hook names must not run while requiring the generated SDK + def test_ruby_lifecycle_hooks_generate_and_load + assert_equal 1, BamlSdk::MethodsOnClasses.baml_singleton_method_added + assert_equal 2, BamlSdk::MethodsOnClasses::HookNames.baml_method_added + end + + # SDK_PARITY_LINT(skip): Ruby Object properties and Module callables need escaped names + def test_ruby_object_and_module_names_generate_load_and_call + type = BamlSdk::MethodsOnClasses::HookNames + value = type.new(baml_display: "display", baml_tap: "tap", then_: "then", baml_gem: "gem") + result = BamlSdk::MethodsOnClasses.baml_name(value) + assert_equal "display", result.baml_display + assert_equal "tap", result.baml_tap + assert_equal "then", result.then_ + assert_equal "gem", result.baml_gem + assert_equal 3, type.baml_ancestors + assert_equal 4, type.baml_decorator + end + + def test_methods_on_classes_method_bindings_exist + assert_respond_to Greeter, :create + assert_respond_to Greeter, :create_async + [:who, :who_async, :greet, :greet_async].each do |name| + assert_includes Greeter.instance_methods, name + end + end + + def test_methods_on_classes_static_create_round_trips + g = Greeter.create("ada") + assert_instance_of Greeter, g + assert_equal "ada", g.name + end + + def test_methods_on_classes_static_create_async_round_trips + g = Greeter.create_async("grace").wait + assert_instance_of Greeter, g + assert_equal "grace", g.name + end + + def test_methods_on_classes_instance_who_round_trips + assert_equal "hopper", Greeter.create("hopper").who + end + + def test_methods_on_classes_instance_who_async_round_trips + assert_equal "hopper", Greeter.create_async("hopper").wait.who_async.wait + end + + def test_methods_on_classes_instance_greet_with_arg_round_trips + assert_equal "hi", Greeter.create("lovelace").greet("hi") + end + + def test_methods_on_classes_instance_greet_async_with_arg_round_trips + assert_equal "hi", Greeter.create_async("lovelace").wait.greet_async("hi").wait + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_optional_args.rb b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_optional_args.rb new file mode 100644 index 00000000000..8eb03f852c4 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_optional_args.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" + +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class OptionalArgsTest < Minitest::Test + def test_optional_args_runtime_matrix + assert_equal [1, 5, 99], BamlSdk.optional_args_probe(1) + assert_equal [1, 5, 99], BamlSdk.optional_args_probe(1, opt1: Baml::UNSET, opt2: Baml::UNSET) + # Required arguments are positional in Ruby, as in Java's port. + assert_equal [1, 5, 99], BamlSdk.optional_args_probe(1) + assert_equal [1, nil, 99], BamlSdk.optional_args_probe(1, opt1: nil) + assert_equal [1, 8, 99], BamlSdk.optional_args_probe(1, opt1: 8) + assert_equal [1, 5, nil], BamlSdk.optional_args_probe(1, opt2: nil) + assert_equal [1, 5, 9], BamlSdk.optional_args_probe(1, opt2: 9) + assert_equal [1, nil, nil], BamlSdk.optional_args_probe(1, opt1: nil, opt2: nil) + assert_equal [1, 8, 9], BamlSdk.optional_args_probe(1, opt1: 8, opt2: 9) + end + + def test_optional_args_python_unset_and_none_differ_in_one_call + refute_nil Baml::UNSET + assert_equal [1, 5, nil], BamlSdk.optional_args_probe(1, opt1: Baml::UNSET, opt2: nil) + assert_equal [1, nil, 99], BamlSdk.optional_args_probe(1, opt1: nil, opt2: Baml::UNSET) + end + + def test_optional_args_async_samples + assert_equal [1, 5, 99], BamlSdk.optional_args_probe_async(1).wait + assert_equal [1, nil, 99], BamlSdk.optional_args_probe_async(1, opt1: nil).wait + assert_equal [1, 5, 9], BamlSdk.optional_args_probe_async(1, opt2: 9).wait + end + + def test_optional_args_opt_box_method_matrix + box = BamlSdk::OptBox.make(10) + assert_equal 17, box.base + box2 = BamlSdk::OptBox.make(10, opt1: 0) + assert_equal 10, box2.base + assert_equal [10, 1, 5], box2.probe(1) + assert_equal [10, 1, 8], box2.probe(1, opt1: 8) + end + + def test_optional_args_negative_runtime_cases_reject + assert_raises(ArgumentError) { BamlSdk.optional_args_probe(1, opt3: 1) } + assert_raises(ArgumentError) { BamlSdk.optional_args_probe } + assert_raises(ArgumentError) { BamlSdk.optional_args_probe(1, arg0: 1) } + # Nullable parameters without a BAML default are still required. + assert_raises(ArgumentError) { BamlSdk::StaticMethodEdges::Edge.nullable } + assert_nil BamlSdk::StaticMethodEdges::Edge.nullable(nil) + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_raises.rb b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_raises.rb new file mode 100644 index 00000000000..30643a42f1a --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_raises.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" + +class RaisesTest < Minitest::Test + def doc_for(marker) + source = File.read(File.join(__dir__, "baml_sdk/raises_test.rb")) + assert_includes source, marker + lines = source.split(marker, 2).first.lines + lines.pop while lines.last && (lines.last.strip.empty? || lines.last.lstrip.start_with?("sig ")) + lines.reverse.take_while { |line| line.lstrip.start_with?("#") } + .reverse.map { |line| line.sub(/^\s*# ?/, "").rstrip }.join("\n") + end + + def test_raises_imports_symbols_reachable + assert_kind_of Class, BamlSdk::RaisesTest::DocLoader + %i[inferred_throw load_doc pure_len reparse].each { |name| assert_respond_to BamlSdk::RaisesTest, name } + end + + def test_raises_union_throws_lists_all_names + assert doc_for("def self.load_doc(").end_with?("Raises:\n ParseError, TimeoutError") + end + + def test_raises_async_sibling_also_has_raises + assert doc_for("def self.load_doc_async(").end_with?("Raises:\n ParseError, TimeoutError") + end + + def test_raises_single_throws + assert doc_for("def self.reparse(").end_with?("Raises:\n ParseError") + end + + def test_raises_summary_precedes_raises_block + doc = doc_for("def self.load_doc(") + assert doc.start_with?("Load a document from a path."), doc + assert_includes doc, "\n\nRaises:\n" + end + + def test_raises_inferred_contract_without_clause_still_raises + assert doc_for("def self.inferred_throw(").end_with?("Raises:\n ParseError") + end + + def test_raises_non_throwing_function_has_no_raises_block + refute_includes doc_for("def self.pure_len("), "Raises:" + end + + def test_raises_method_raises_block_in_pyi + # Ruby has no separate stub: both method comments live in generated source. + load = doc_for("def load(") + assert_includes load, "Raises:" + assert_includes load, "ParseError" + create = doc_for("def self.create(") + assert_includes create, "Raises:" + assert_includes create, "TimeoutError" + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_stdlib_entrypoints.rb b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_stdlib_entrypoints.rb new file mode 100644 index 00000000000..0b72a85ddd1 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_stdlib_entrypoints.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" + +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class StdlibEntrypointsTest < Minitest::Test + def test_stdlib_entrypoints_native_argv_callable_as_entry_point + assert_instance_of Array, BamlSdk::Baml::Sys.argv + end + + def test_stdlib_entrypoints_sysop_fs_exists_callable_as_entry_point + assert_equal true, BamlSdk::Baml::Fs.exists(".") + end + + def test_stdlib_entrypoints_compiler_intrinsics_are_not_emitted_as_entry_points + assert Dir.exist?(File.join(__dir__, "baml_sdk")), "generated baml_sdk/ must exist" + { + "__vendor/log.rb" => %w[info debug warn error], + "baml/events.rb" => ["send"] + }.each do |file, names| + path = File.join(__dir__, "baml_sdk", file) + next unless File.file?(path) + + source = File.read(path) + names.each do |name| + wire = file == "__vendor/log.rb" ? "log.#{name}" : "baml.events.#{name}" + refute_includes source, "\"#{wire}\"" + refute_match(/def self\.#{name}(?:_async)?\(/, source) + end + end + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/typed_consumer.rb b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/typed_consumer.rb new file mode 100644 index 00000000000..9de8f498667 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/function_calls/customizable/typed_consumer.rb @@ -0,0 +1,38 @@ +# typed: strict +require "sorbet-runtime" +require_relative "baml_sdk" + +T.let(BamlSdk.initialize!, NilClass) +T.let(BamlSdk.hello_world, String) +T.let(BamlSdk.hello_world_async, Baml::Call[String]) +T.let(BamlSdk::Baml::Float.parse("1.5"), Float) +T.let(BamlSdk::Baml::String.from_code_points([65]), String) +T.let(BamlSdk::Baml::Spawn::CancelToken.baml_new_async, Baml::Call[BamlSdk::Baml::Spawn::CancelToken]) +T.let(BamlSdk.single_required_arg("hello"), String) +T.let(BamlSdk::HostCallableTests.call_int_callback(->(value) { value * 2 }, 21), Integer) +adder = T.let(BamlSdk::HostCallableTests.make_adder(10), T.any(T.proc.params(value: Integer).returns(Integer), Baml::Closure)) +T.let(adder.call(5), Integer) +person = BamlSdk::Person.new(person: "person", name: "Ryan", age: 30) +T.let(person.person, String) +T.let(person.name, String) +T.let(person.age, Integer) +result = T.let(BamlSdk.round_trip_person(person), BamlSdk::Person) +T.let(result.name, String) +T.let(result.age, Integer) +# Omission, UNSET and nil remain distinct, with typed per-call controls. +T.let(BamlSdk.optional_args_probe(1), T::Array[T.nilable(Integer)]) +T.let(BamlSdk.optional_args_probe(1, opt1: Baml::UNSET, opt2: nil), T::Array[T.nilable(Integer)]) +T.let(BamlSdk.optional_args_probe(1, opt1: nil, opt2: 2, _baml: Baml::Options.new(timeout_ms: 1000)), T::Array[T.nilable(Integer)]) +T.let(BamlSdk::StaticMethodEdges::Edge.nullable(nil), T.nilable(Integer)) +T.let(BamlSdk.default_name_collisions("a", options: "b", option: "c"), T::Array[String]) + +T.let(BamlSdk::GenericTests.identity(5), Integer) +T.let(BamlSdk::GenericTests.identity_async("hi").wait, String) +box = Baml.bind_types(BamlSdk::GenericTests::GenericBox[Integer].new(value: 5), Integer) +T.let(box, BamlSdk::GenericTests::GenericBox[Integer]) +T.let(box.value, Integer) +T.let(box.get, String) +T.let(box.pair_with("hi", _types: { U: String }), String) +T.let(BamlSdk::GenericTests.wrap(5), BamlSdk::GenericTests::GenericBox[Integer]) +T.let(BamlSdk::GenericTests::GenericBox.baml_new("hi"), BamlSdk::GenericTests::GenericBox[String]) +T.let(BamlSdk::GenericTests::GenericNameCollision[String].new(value: "hi").pair(1), String) diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/llm_functions/customizable/replay_harness.rb b/baml_language/sdk_tests/crates/ruby_sorbet/llm_functions/customizable/replay_harness.rb new file mode 100644 index 00000000000..36075de1711 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/llm_functions/customizable/replay_harness.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require "net/http" +require "pathname" +require "tempfile" +require "timeout" + +module ReplayHarness + module_function + + def recording_path(name) + Pathname.new(__dir__).ascend do |parent| + next unless parent.basename.to_s == "sdk_tests" + + recording = parent.join("fixtures/llm_functions/recordings/#{name}.snap.sse") + raise "missing recording #{recording}" unless recording.file? + + return recording.to_s + end + raise "could not locate the sdk_tests/ ancestor directory" + end + + ENV_KEYS = %w[BAML_REPLAY_BASE_URL BAML_REPLAY_API_KEY].freeze + + def with_server(recording) + recording = recording_path(recording) + saved_env = ENV.to_h.slice(*ENV_KEYS) + Tempfile.create("baml-ruby-replay") do |file| + addr_file = file.path + file.close + addr = nil + thread = Thread.new do + Thread.current.report_on_exception = false + BamlSdk::Replay.replay_serve_until_shutdown(recording, addr_file) + end + begin + deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 10 + loop do + unless thread.alive? + thread.value # Re-raise the original bridge/server failure. + raise "replay server exited before binding" + end + text = File.read(addr_file).strip + unless text.empty? + addr = text + break + end + if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline + raise Timeout::Error, "replay server did not bind within 10s" + end + sleep 0.02 + end + ENV["BAML_REPLAY_BASE_URL"] = "http://#{addr}" + ENV["BAML_REPLAY_API_KEY"] = "replay-test-key" + yield addr + ensure + begin + if addr && thread.alive? + uri = URI("http://#{addr}/__replay__/shutdown") + Net::HTTP.start(uri.host, uri.port, open_timeout: 5, read_timeout: 5) do |http| + response = http.post(uri.request_uri, "") + raise "replay shutdown failed: #{response.code}" unless response.is_a?(Net::HTTPSuccess) + end + end + ensure + begin + # A server that will not stop fails this test; the process exit reaps it. + raise Timeout::Error, "replay server did not stop within 10s" unless thread.join(10) + ensure + ENV_KEYS.each { |key| saved_env.key?(key) ? ENV[key] = saved_env[key] : ENV.delete(key) } + end + end + end + end + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/llm_functions/customizable/test_import.rb b/baml_language/sdk_tests/crates/ruby_sorbet/llm_functions/customizable/test_import.rb new file mode 100644 index 00000000000..00d9975fcb5 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/llm_functions/customizable/test_import.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" + +class ImportTest < Minitest::Test + def test_main_root_imports_cleanly + assert_kind_of Module, BamlSdk + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/llm_functions/customizable/test_main.rb b/baml_language/sdk_tests/crates/ruby_sorbet/llm_functions/customizable/test_main.rb new file mode 100644 index 00000000000..c8fa48931f2 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/llm_functions/customizable/test_main.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" + +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class MainTest < Minitest::Test + def test_main_lorem_resume_class_shape + assert_operator BamlSdk::Lorem::Resume, :<, T::Struct + assert_equal %i[email name], BamlSdk::Lorem::Resume.props.keys.sort + end + + def test_main_lorem_streaming_doc_class_shape + assert_operator BamlSdk::Lorem::StreamingDoc, :<, T::Struct + props = BamlSdk::Lorem::StreamingDoc.props + assert_equal %i[body title word_count], props.keys.sort + assert_equal String, props.fetch(:title).fetch(:type) + assert_equal Integer, props.fetch(:word_count).fetch(:type) + assert_equal "T.nilable(String)", props.fetch(:body).fetch(:type_object).to_s + doc = BamlSdk::Lorem::StreamingDoc.new(title: "title", body: nil, word_count: 1) + refute_respond_to doc, :title= + assert_raises(TypeError) { BamlSdk::Lorem::StreamingDoc.new(title: 1, word_count: 1) } + end + + def test_main_ipsum_sentiment_enum_shape + assert_operator BamlSdk::Ipsum::Sentiment, :<, T::Enum + assert_equal %w[NEGATIVE NEUTRAL POSITIVE], BamlSdk::Ipsum::Sentiment.values.map(&:serialize).sort + # T::Enum members serialize to strings; unlike Python's enum, they aren't strings themselves. + assert_equal "POSITIVE", BamlSdk::Ipsum::Sentiment::POSITIVE.serialize + assert_instance_of String, BamlSdk::Ipsum::Sentiment::POSITIVE.serialize + end + + def test_main_namespaces_reachable_via_explicit_import + require "baml_sdk/lorem" + require "baml_sdk/ipsum" + assert_kind_of Module, BamlSdk::Lorem + assert_kind_of Module, BamlSdk::Ipsum + end + + def test_main_extract_resume_factory_bindings + assert_respond_to BamlSdk::Lorem, :extract_resume + assert_respond_to BamlSdk::Lorem, :extract_resume_async + end + + def test_main_extract_resume_operation_bindings + assert_operation_bindings(:extract_resume) + spec = BamlSdk::Lorem.extract_resume_spec("Ada Lovelace") + assert_instance_of Baml::FunctionSpec, spec + assert_respond_to spec, :build_request + assert_respond_to spec, :build_request_async + spec.close + call = BamlSdk::Lorem.extract_resume_spec_async("Ada Lovelace") + assert_instance_of Baml::Call, call + assert_instance_of Baml::FunctionSpec, call.wait + call.wait.close + end + + def test_main_streaming_extract_factory_bindings + assert_respond_to BamlSdk::Lorem, :streaming_extract + assert_respond_to BamlSdk::Lorem, :streaming_extract_async + end + + def test_main_streaming_extract_operation_bindings + assert_operation_bindings(:streaming_extract) + end + + def test_main_classify_sentiment_factory_bindings + assert_respond_to BamlSdk::Ipsum, :classify_sentiment + assert_respond_to BamlSdk::Ipsum, :classify_sentiment_async + end + + def test_main_replay_server_namespace_bindings + %i[replay_serve_until_shutdown replay_serve_detached].each do |name| + assert_respond_to BamlSdk::Replay, name + assert_respond_to BamlSdk::Replay, "#{name}_async" + end + end + + private + + def assert_operation_bindings(name) + %w[spec spec_async stream stream_async].each do |suffix| + assert_respond_to BamlSdk::Lorem, "#{name}_#{suffix}" + end + %w[render_prompt build_request parse].each do |removed| + refute_respond_to BamlSdk::Lorem, "#{name}__#{removed}" + refute_respond_to BamlSdk::Lorem, "#{name}__#{removed}_async" + end + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/llm_functions/customizable/test_streaming_e2e.rb b/baml_language/sdk_tests/crates/ruby_sorbet/llm_functions/customizable/test_streaming_e2e.rb new file mode 100644 index 00000000000..e4c3c70a00a --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/llm_functions/customizable/test_streaming_e2e.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" +require_relative "replay_harness" + +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class StreamingE2ETest < Minitest::Test + def test_streaming_e2e_stream + ReplayHarness.with_server("replay_extract_string") do + stream = BamlSdk::Lorem.stream_e2e_extract_stream("ignored-by-replay-server") + assert_instance_of Baml::Stream, stream + assert_partials(stream, String) + assert_instance_of String, stream.final + stream.close + + stream = BamlSdk::Lorem.stream_e2e_extract_stream("ignored-by-replay-server") + assert_instance_of Enumerator, stream.each + results = 0 + returned = stream.each do |partial| + assert_instance_of String, partial + results += 1 + assert_operator results, :<, 10_000, "stream.each failed to terminate" + end + assert_same stream, returned + assert_operator results, :>=, 10 + assert_instance_of String, stream.final + stream.close + + spec = BamlSdk::Lorem.stream_e2e_extract_spec("ignored-by-replay-server") + request = spec.build_request + assert_instance_of BamlSdk::Baml::Http::Request, request + assert_equal "POST", request.baml_method + assert_equal "#{ENV.fetch('BAML_REPLAY_BASE_URL')}/responses", request.url + spec.close + end + end + + def test_streaming_e2e_stream_async + ReplayHarness.with_server("replay_extract_string") do + call = BamlSdk::Lorem.stream_e2e_extract_stream_async("ignored-by-replay-server") + assert_instance_of Baml::Call, call + stream = call.wait + assert_instance_of Baml::Stream, stream + assert_partials(stream, String, asynchronous: true) + final = stream.final_async + assert_instance_of Baml::Call, final + assert_instance_of String, final.wait + stream.close + + spec = BamlSdk::Lorem.stream_e2e_extract_spec_async("ignored-by-replay-server").wait + request = spec.build_request_async + assert_instance_of Baml::Call, request + assert_instance_of BamlSdk::Baml::Http::Request, request.wait + assert_equal "#{ENV.fetch('BAML_REPLAY_BASE_URL')}/responses", request.wait.url + spec.close + end + end + + def test_streaming_e2e_stream_doc + ReplayHarness.with_server("replay_extract_doc") do + stream = BamlSdk::Lorem.stream_e2e_extract_doc_stream("ignored-by-replay-server") + assert_instance_of Baml::Stream, stream + assert_partials(stream, BamlSdk::Lorem::StreamingDoc) + assert_instance_of BamlSdk::Lorem::StreamingDoc, stream.final + stream.close + end + end + + def test_streaming_e2e_stream_doc_async + ReplayHarness.with_server("replay_extract_doc") do + call = BamlSdk::Lorem.stream_e2e_extract_doc_stream_async("ignored-by-replay-server") + assert_instance_of Baml::Call, call + stream = call.wait + assert_instance_of Baml::Stream, stream + assert_partials(stream, BamlSdk::Lorem::StreamingDoc, asynchronous: true) + final = stream.final_async + assert_instance_of Baml::Call, final + assert_instance_of BamlSdk::Lorem::StreamingDoc, final.wait + stream.close + end + end + + def test_streaming_e2e_stream_collect_in_baml + ReplayHarness.with_server("replay_extract_string") do + result = BamlSdk::Lorem.stream_e2e_collect("ignored-by-replay-server") + assert_instance_of BamlSdk::Lorem::StreamE2ECollectResult, result + assert_operator result.next_calls.length, :>=, 10 + result.next_calls.each { |value| assert_instance_of String, value } + assert_instance_of String, result.final_call + end + end + + def test_streaming_e2e_stream_doc_collect_in_baml + ReplayHarness.with_server("replay_extract_doc") do + doc = BamlSdk::Lorem.stream_e2e_collect_doc("ignored-by-replay-server") + assert_instance_of BamlSdk::Lorem::StreamingDoc, doc + assert_respond_to doc, :title + assert_instance_of String, doc.title + assert_instance_of Integer, doc.word_count + assert doc.body.nil? || doc.body.is_a?(String) + end + end + + private + + def assert_partials(stream, type, asynchronous: false) + Timeout.timeout(30) do + results = 0 + loop do + value = if asynchronous + call = stream.next_async + assert_instance_of Baml::Call, call + call.wait + else + stream.next + end + break if value.is_a?(BamlSdk::Ai::Stream::Done) + + results += 1 + assert_instance_of type, value + assert_operator results, :<, 10_000, "stream.next failed to terminate" + end + assert_operator results, :>=, 10, "expected at least 10 partials" + end + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/llm_functions/customizable/typed_consumer.rb b/baml_language/sdk_tests/crates/ruby_sorbet/llm_functions/customizable/typed_consumer.rb new file mode 100644 index 00000000000..05337110e1f --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/llm_functions/customizable/typed_consumer.rb @@ -0,0 +1,23 @@ +# typed: strict +require "sorbet-runtime" +require_relative "baml_sdk" + +doc = BamlSdk::Lorem::StreamingDoc.new(title: "Title", body: nil, word_count: 30) +T.let(doc.title, String) +T.let(doc.body, T.nilable(String)) +T.let(doc.word_count, T.nilable(Integer)) +T.let(BamlSdk::Lorem::StreamingDoc.new(title: "Partial").word_count, T.nilable(Integer)) +collected = BamlSdk::Lorem::StreamE2ECollectResult.new(next_calls: ["first"], final_call: "last") +T.let(collected.next_calls, T::Array[String]) +T.let(BamlSdk::Lorem.stream_e2e_collect("text"), BamlSdk::Lorem::StreamE2ECollectResult) +T.let(BamlSdk::Lorem.streaming_extract("text"), BamlSdk::Lorem::StreamingDoc) +stream = BamlSdk::Lorem.streaming_extract_stream("text") +T.let(stream, Baml::Stream[BamlSdk::Lorem::StreamingDoc]) +T.let(stream.final, BamlSdk::Lorem::StreamingDoc) +T.let(stream.final_async, Baml::Call[BamlSdk::Lorem::StreamingDoc]) +stream.each { |partial| T.let(partial, BamlSdk::Lorem::StreamingDoc) } +T.let(BamlSdk::Lorem.streaming_extract_stream_async("text"), Baml::Call[Baml::Stream[BamlSdk::Lorem::StreamingDoc]]) +T.let(BamlSdk::Lorem.extract_resume_spec("text"), Baml::FunctionSpec) +T.let(BamlSdk::Lorem.extract_resume_spec_async("text"), Baml::Call[Baml::FunctionSpec]) +T.let(BamlSdk::Ipsum::Sentiment::POSITIVE, BamlSdk::Ipsum::Sentiment) +T.let(BamlSdk::Ipsum.classify_sentiment("text"), BamlSdk::Ipsum::Sentiment) diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/setup.ps1 b/baml_language/sdk_tests/crates/ruby_sorbet/setup.ps1 index eab33c7508f..ac0ffd73686 100644 --- a/baml_language/sdk_tests/crates/ruby_sorbet/setup.ps1 +++ b/baml_language/sdk_tests/crates/ruby_sorbet/setup.ps1 @@ -16,14 +16,16 @@ try { cargo build -p bridge_cffi if ($LASTEXITCODE -ne 0) { throw "cargo build -p bridge_cffi failed" } - cargo run --quiet -p sdk_test_codegen -- emit-bytecode --fixture function_calls --out (Join-Path $fixtureDir "function-calls.bytecode") - if ($LASTEXITCODE -ne 0) { throw "function_calls bytecode generation failed" } + cargo build -p baml_cli --bin baml-cli + if ($LASTEXITCODE -ne 0) { throw "cargo build -p baml_cli failed" } + + cargo run --quiet -p sdk_test_codegen -- ruby_sorbet + if ($LASTEXITCODE -ne 0) { throw "ruby_sorbet SDK generation failed" } } finally { Pop-Location } $bridgeFixture = Join-Path $fixtureDir "baml_ruby_test_fixture.dll" -$missingGetter = Join-Path $fixtureDir "baml_ruby_missing_getter.dll" $threadFixture = Join-Path $fixtureDir "baml_ruby_thread_callback.dll" & cl.exe /nologo /std:c11 /W4 /WX /LD "/I$includeDir" ` @@ -31,18 +33,12 @@ $threadFixture = Join-Path $fixtureDir "baml_ruby_thread_callback.dll" "/Fe:$bridgeFixture" "/Fo:$(Join-Path $fixtureDir 'bridge_fixture.obj')" if ($LASTEXITCODE -ne 0) { throw "bridge fixture compilation failed" } -& cl.exe /nologo /std:c11 /W4 /WX /LD ` - (Join-Path $testRoot "test/native/missing_getter.c") ` - "/Fe:$missingGetter" "/Fo:$(Join-Path $fixtureDir 'missing_getter.obj')" -if ($LASTEXITCODE -ne 0) { throw "missing-getter fixture compilation failed" } - & cl.exe /nologo /std:c++17 /EHsc /W4 /WX /LD ` (Join-Path $testRoot "test/native/thread_callback.cpp") ` "/Fe:$threadFixture" "/Fo:$(Join-Path $fixtureDir 'thread_callback.obj')" if ($LASTEXITCODE -ne 0) { throw "thread-callback fixture compilation failed" } -$invalidLibrary = Join-Path $fixtureDir "not-a-library" -[IO.File]::WriteAllText($invalidLibrary, "not a dynamic library`n") +Write-Host "Sorbet static gate unavailable: checker 0.6.13506 has no Windows binary; runtime tests still run." $env:BUNDLE_GEMFILE = Join-Path $testRoot "Gemfile" $env:BUNDLE_PATH = Join-Path $targetDir "ruby-bundle" @@ -54,9 +50,6 @@ if ($env:NEXTEST_ENV) { Add-Content -Path $env:NEXTEST_ENV -Value "BUNDLE_GEMFILE=$env:BUNDLE_GEMFILE" Add-Content -Path $env:NEXTEST_ENV -Value "BUNDLE_PATH=$env:BUNDLE_PATH" Add-Content -Path $env:NEXTEST_ENV -Value "BAML_RUBY_TEST_FIXTURE=$bridgeFixture" - Add-Content -Path $env:NEXTEST_ENV -Value "BAML_RUBY_TEST_MISSING_GETTER=$missingGetter" - Add-Content -Path $env:NEXTEST_ENV -Value "BAML_RUBY_TEST_INVALID_LIBRARY=$invalidLibrary" Add-Content -Path $env:NEXTEST_ENV -Value "BAML_RUBY_TEST_THREAD_FIXTURE=$threadFixture" Add-Content -Path $env:NEXTEST_ENV -Value "BAML_RUBY_TEST_REAL_RUNTIME=$(Join-Path $targetDir 'debug/bridge_cffi.dll')" - Add-Content -Path $env:NEXTEST_ENV -Value "BAML_RUBY_TEST_REAL_BYTECODE=$(Join-Path $fixtureDir 'function-calls.bytecode')" } diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/setup.sh b/baml_language/sdk_tests/crates/ruby_sorbet/setup.sh index 6d242d67a6b..d8dd970d7f8 100755 --- a/baml_language/sdk_tests/crates/ruby_sorbet/setup.sh +++ b/baml_language/sdk_tests/crates/ruby_sorbet/setup.sh @@ -15,9 +15,8 @@ mkdir -p "$FIXTURE_DIR" ( cd "$WORKSPACE_ROOT" cargo build -p bridge_cffi - cargo run --quiet -p sdk_test_codegen -- \ - emit-bytecode --fixture function_calls \ - --out "$FIXTURE_DIR/function-calls.bytecode" + cargo build -p baml_cli --bin baml-cli + cargo run --quiet -p sdk_test_codegen -- ruby_sorbet ) if [[ "$(uname -s)" == "Darwin" ]]; then @@ -36,15 +35,30 @@ cc -std=c11 -Wall -Wextra -Werror -D_POSIX_C_SOURCE=200809L \ "${C_SHARED[@]}" -pthread -I"$INCLUDE_DIR" \ "$TEST_ROOT/test/native/bridge_fixture.c" \ -o "$FIXTURE_DIR/libbaml_ruby_test_fixture.$LIB_EXT" -cc -std=c11 -Wall -Wextra -Werror \ - "${C_SHARED[@]}" \ - "$TEST_ROOT/test/native/missing_getter.c" \ - -o "$FIXTURE_DIR/libbaml_ruby_missing_getter.$LIB_EXT" c++ -std=c++17 -Wall -Wextra -Werror \ "${CXX_SHARED[@]}" \ "$TEST_ROOT/test/native/thread_callback.cpp" \ -o "$FIXTURE_DIR/libbaml_ruby_thread_callback.$LIB_EXT" -printf 'not a dynamic library\n' > "$FIXTURE_DIR/not-a-library" + +# The pinned checker has no Windows, 32-bit Linux or musl binary. Keep its +# bundle separate so those hosts can still install and run the runtime suite. +case "$(uname -s):$(uname -m)" in + Darwin:x86_64|Darwin:arm64) STATIC_SUPPORTED=1 ;; + Linux:x86_64|Linux:aarch64) + if getconf GNU_LIBC_VERSION >/dev/null 2>&1; then STATIC_SUPPORTED=1; else STATIC_SUPPORTED=0; fi + ;; + *) STATIC_SUPPORTED=0 ;; +esac +if [[ "$STATIC_SUPPORTED" == 1 ]]; then + ( + export BUNDLE_GEMFILE="$TEST_ROOT/test/static/Gemfile" + export BUNDLE_PATH="$TARGET_DIR/ruby-sorbet-static-bundle" + export BUNDLE_FROZEN=true + ruby -S bundle install --jobs 4 --retry 3 + ) +else + echo "Sorbet static gate unavailable: checker 0.6.13506 requires x86_64/arm64 macOS or x86_64/aarch64 glibc Linux; runtime tests still run." +fi export BUNDLE_GEMFILE="$TEST_ROOT/Gemfile" export BUNDLE_PATH="$TARGET_DIR/ruby-bundle" @@ -56,10 +70,7 @@ if [[ -n "${NEXTEST_ENV:-}" ]]; then echo "BUNDLE_GEMFILE=$BUNDLE_GEMFILE" echo "BUNDLE_PATH=$BUNDLE_PATH" echo "BAML_RUBY_TEST_FIXTURE=$FIXTURE_DIR/libbaml_ruby_test_fixture.$LIB_EXT" - echo "BAML_RUBY_TEST_MISSING_GETTER=$FIXTURE_DIR/libbaml_ruby_missing_getter.$LIB_EXT" - echo "BAML_RUBY_TEST_INVALID_LIBRARY=$FIXTURE_DIR/not-a-library" echo "BAML_RUBY_TEST_THREAD_FIXTURE=$FIXTURE_DIR/libbaml_ruby_thread_callback.$LIB_EXT" echo "BAML_RUBY_TEST_REAL_RUNTIME=$REAL_RUNTIME" - echo "BAML_RUBY_TEST_REAL_BYTECODE=$FIXTURE_DIR/function-calls.bytecode" } >> "$NEXTEST_ENV" fi diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/src/gem.rs b/baml_language/sdk_tests/crates/ruby_sorbet/src/gem.rs new file mode 100644 index 00000000000..18b39b825b6 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/src/gem.rs @@ -0,0 +1,66 @@ +use std::process::Command; + +#[test] +fn clean_app() { + let arch = std::env::consts::ARCH; + let target = if cfg!(target_os = "macos") { + format!("{arch}-apple-darwin") + } else if cfg!(target_os = "windows") { + format!("{arch}-pc-windows-msvc") + } else if cfg!(target_os = "linux") { + let libc = if cfg!(target_env = "musl") { + "musl" + } else { + "gnu" + }; + format!("{arch}-unknown-linux-{libc}") + } else { + panic!("unsupported Ruby bridge platform"); + }; + let engine = sdk_test_harness_runner::engine_library(); + let cli = engine + .parent() + .expect("engine directory") + .join(if cfg!(windows) { + "baml-cli.exe" + } else { + "baml-cli" + }); + let mut command = Command::new("ruby"); + command + .args(["-S", "bundle", "exec", "ruby", "test/gem_smoke.rb"]) + .arg(&engine) + .arg(target) + .arg(cli) + .current_dir(env!("CARGO_MANIFEST_DIR")); + // Same hosts as the Sorbet gate; setup installs the checker only there. + let sorbet = cfg!(all( + any(target_arch = "x86_64", target_arch = "aarch64"), + any( + target_os = "macos", + all(target_os = "linux", target_env = "gnu") + ) + )); + if sorbet { + let target_dir = engine + .parent() + .and_then(|profile| profile.parent()) + .expect("target directory"); + command.arg(target_dir.join("ruby-sorbet-static-bundle")); + } + let output = sdk_test_harness_runner::run_command( + &mut command, + "installed baml-bridge gem in a clean app", + ); + sdk_test_harness_runner::assert_stdout_contains( + &output, + "Installed baml-bridge gem: clean app returned hello world", + ); + if sorbet { + sdk_test_harness_runner::assert_stdout_contains( + &output, + "Sorbet accepted the clean app with the installed RBI", + ); + } + print!("{}", String::from_utf8_lossy(&output.stdout)); +} diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/src/lib.rs b/baml_language/sdk_tests/crates/ruby_sorbet/src/lib.rs index b8fa7b8ae22..873ce98759a 100644 --- a/baml_language/sdk_tests/crates/ruby_sorbet/src/lib.rs +++ b/baml_language/sdk_tests/crates/ruby_sorbet/src/lib.rs @@ -1,8 +1,63 @@ #[cfg(test)] -sdk_test_harness_runner::setup_guard!("SDK_TEST_RUBY_SORBET_SETUP"); +sdk_test_harness_runner::ruby_sorbet::test_suite! { + fixture docstrings_etc { minitest: on, sorbet: on } + fixture function_calls { minitest: on, sorbet: on } + fixture llm_functions { minitest: on, sorbet: on } + fixture type_shapes { minitest: on, sorbet: on } + fixture unsupported_only { minitest: on, sorbet: on } +} + +#[cfg(test)] +mod gem; + +#[cfg(test)] +mod wrong_argument { + sdk_test_harness_runner::__ruby_sorbet_gate!(on, sorbet, { + let mut command = sdk_test_harness_runner::ruby_sorbet::sorbet_command("function_calls"); + command.arg( + std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("test/static/wrong_argument.rb"), + ); + let output = command.output().expect("run Sorbet negative consumer"); + let diagnostics = String::from_utf8_lossy(&output.stderr); + assert_eq!(output.status.code(), Some(100), "{diagnostics}"); + assert_eq!( + diagnostics.matches("https://srb.help/").count(), + 1, + "{diagnostics}" + ); + assert!( + diagnostics.contains("wrong_argument.rb:4:"), + "{diagnostics}" + ); + assert!( + diagnostics.contains("https://srb.help/7002"), + "{diagnostics}" + ); + }); +} #[cfg(test)] mod bridge_tests { + #[test] + fn encoding_walker() { + sdk_test_harness_runner::run_workspace_cmd( + "sdk_tests/crates/ruby_sorbet", + "ruby -S bundle exec ruby test/bridge_encode_test.rb", + "ruby-bundle", + "BUNDLE_PATH", + ); + } + + #[test] + fn synchronous_call_runtime() { + sdk_test_harness_runner::run_workspace_cmd( + "sdk_tests/crates/ruby_sorbet", + "ruby -S bundle exec ruby test/bridge_call_test.rb", + "ruby-bundle", + "BUNDLE_PATH", + ); + } + #[test] fn loader_and_lifecycle() { sdk_test_harness_runner::run_workspace_cmd( diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/test/bridge_call_test.rb b/baml_language/sdk_tests/crates/ruby_sorbet/test/bridge_call_test.rb new file mode 100644 index 00000000000..eed1db84ccc --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/test/bridge_call_test.rb @@ -0,0 +1,695 @@ +# frozen_string_literal: true + +require "minitest/autorun" +require "timeout" +require "open3" +$LOAD_PATH.unshift File.expand_path("../../../../sdks/ruby/bridge_ruby/lib", __dir__) +require "baml/bridge" + +class BridgeCallTest < Minitest::Test + Wire = BamlBridge::Cffi::V1 + Native = Baml::Bridge.const_get(:Native, false) + + class FakeApi + attr_reader :callback, :initialize_count, :released_calls, :cancelled_calls + attr_accessor :dispatch + attr_reader :host_dispatch, :host_release, :host_cancel, :host_completions, :released_handles + + def initialize + @initialize_count = 0 + @engine_id = 2**40 + @released_calls = [] + @cancelled_calls = [] + @host_completions = Queue.new + @released_handles = Queue.new + end + + def register_bridge!(_toolchain, _version); end + def ensure_not_forked!; end + def register_unhandled_spawn_error_callback(_callback); end + def shutdown_runtime; end + + def handle_clone(key) + key + end + + def initialize_runtime(_bytes, _metadata = nil) + @initialize_count += 1 + end + + def register_result_callback(callback) + @callback = callback + end + + def register_host_callbacks(dispatch, release, cancel) + @host_dispatch, @host_release, @host_cancel = dispatch, release, cancel + end + + def complete_host_call(id, bytes) + @host_completions << [id, Wire::HostCallCompletion.decode(bytes)] + end + + def handle_release(key) + @released_handles << key + end + + def new_function_call + @engine_id += 1 + end + + def release_function_call(call_id) + @released_calls << call_id + end + + def cancel_function_call(call_id) + @cancelled_calls << call_id + 0 + end + + def call_function(bytes, callback_id) + @dispatch.call(Wire::CallFunctionArgs.decode(bytes), callback_id) + end + + def deliver(callback_id, **result) + bytes = Wire::BamlOutboundResult.encode(Wire::BamlOutboundResult.new(**result)) + pointer = FFI::MemoryPointer.new(:uint8, [bytes.bytesize, 1].max) + pointer.put_bytes(0, bytes) + @callback.call(callback_id, pointer, bytes.bytesize) + end + end + + def setup + @runtime = Baml::Bridge.const_get(:ProcessRuntime, false).new + @api = FakeApi.new + @runtime.define_singleton_method(:configured_runtime_path!) { "fake-runtime" } + @runtime.define_singleton_method(:open_library) { |*_args| Object.new } + @runtime.register_program("bytecode") + Native::Api.stub(:new, @api) { @runtime.initialize! } + end + + def released_handle + Timeout.timeout(5) { @api.released_handles.pop } + end + + def test_exit_panic_flushes_piped_output_when_decoded + source = <<~'RUBY' + require "baml/bridge" + protocol = Baml::Bridge::Protocol + result = protocol::Wire::BamlOutboundResult.new(panic: { is_exit_panic: true, exit_code: 7 }) + captured = protocol::Result.new(result, nil, nil) + at_exit { puts "UNREACHABLE" } + print "before exit" + warn "before exit on stderr" + protocol.decode_result(captured) + RUBY + stdout, stderr, status = Open3.capture3(RbConfig.ruby, "-I", $LOAD_PATH.first, "-e", source) + assert_equal 7, status.exitstatus, stderr + assert_equal "before exit", stdout + assert_equal "before exit on stderr\n", stderr + end + + def test_exit_panic_flushes_original_streams_when_globals_are_replaced + source = <<~'RUBY' + require "baml/bridge" + require "stringio" + STDOUT.sync = STDERR.sync = false + STDOUT.print "original stdout" + STDERR.print "original stderr" + $stdout = StringIO.new + $stderr = StringIO.new + Baml::Bridge::Protocol.exit_process(7) + RUBY + stdout, stderr, status = Open3.capture3(RbConfig.ruby, "-I", $LOAD_PATH.first, "-e", source) + assert_equal 7, status.exitstatus, stderr + assert_equal "original stdout", stdout + assert_equal "original stderr", stderr + end + + def test_cancel_waiting_for_the_mutex_is_interruptible + pending = Baml::Call.new(@api, 123) + mutex = pending.instance_variable_get(:@mutex) + entered, release = Queue.new, Queue.new + holder = Thread.new { mutex.synchronize { entered << true; release.pop } } + Timeout.timeout(5) { entered.pop } + assert_raises(Timeout::Error) { Timeout.timeout(0.05) { pending.cancel } } + assert_empty @api.cancelled_calls + refute pending.instance_variable_get(:@cancel_requested) + release << true + holder.join + assert pending.cancel + assert_equal [123], @api.cancelled_calls + ensure + release << true if release + holder&.join(5) + end + + def test_cancel_defers_an_interrupt_until_native_cancellation + @api.define_singleton_method(:cancel_function_call) do |id| + Thread.current.raise Interrupt + super(id) + end + pending = Baml::Call.new(@api, 123) + assert_raises(Interrupt) { pending.cancel } + assert_equal [123], @api.cancelled_calls + refute pending.cancel + end + + def test_unregistered_error_classes_preserve_message_trace_fields_and_handle_ownership + payload = { class_value: { name: "ai.errors.Unregistered", fields: [ + { key: "message", value: { string_value: "render failed" } }, + { key: "nested", value: { class_value: { name: "ai.errors.Nested", fields: [ + { key: "detail", value: { string_value: "nested detail" } }, + { key: "handle", value: { handle_value: { key: 41, handle_type: :ADT_TAGGED_HEAP_HANDLE } } } + ] } } } + ] } } + trace = ['File "test.baml", line 3, in render'] + { error: Baml::Error, panic: Baml::Panic }.each do |kind, klass| + @api.dispatch = ->(_args, id) { @api.deliver(id, **{ kind => { value: payload, trace: trace } }) } + failure = assert_raises(klass) { call } + assert_equal "ai.errors.Unregistered", failure.type_name + assert_equal "render failed", failure.message + assert_equal trace, failure.baml_trace + assert failure.value.frozen? + assert failure.value.fetch("nested").frozen? + assert_equal "nested detail", failure.value.fetch("nested").fetch("detail") + owner = failure.value.fetch("nested").fetch("handle") + assert_instance_of Baml::Handle, owner + assert @api.released_handles.empty? + owner.close + assert_equal 41, released_handle + assert @api.released_handles.empty? + end + @api.dispatch = ->(_args, id) { @api.deliver(id, ok: payload) } + assert_raises(Baml::Bridge::UnsupportedTypeError) { call } + assert_equal 41, released_handle + assert @api.released_handles.empty? + end + + def test_registered_error_with_an_unregistered_child_falls_back_without_double_adopting + protocol = Baml::Bridge::Protocol + type = Struct.new(:handle, :nested, keyword_init: true) + protocol.define_class("user.RegisteredErrorParent", type, fields: { + "handle" => [:handle, [:unknown]], "nested" => [:nested, [:unknown]] + }) + @api.dispatch = ->(_args, id) { @api.deliver(id, error: { value: { class_value: { + name: "user.RegisteredErrorParent", fields: [ + { key: "handle", value: { handle_value: { key: 42, handle_type: :ADT_TAGGED_HEAP_HANDLE } } }, + { key: "nested", value: { class_value: { name: "user.UnregisteredChild", fields: [] } } } + ] + } } }) } + failure = assert_raises(Baml::Error) { call } + assert_instance_of Hash, failure.value + assert failure.value.frozen? + assert_equal({}, failure.value.fetch("nested")) + assert failure.value.fetch("nested").frozen? + assert @api.released_handles.empty? + failure.value.fetch("handle").close + assert_equal 42, released_handle + assert @api.released_handles.empty? + end + + def test_unregistered_error_message_uses_detail_then_type_name + [ [{ key: "detail", value: { string_value: "detail only" } }, "detail only"], + [{ key: "message", value: { int_value: 42 } }, "ai.errors.Unregistered"] ].each do |field, message| + @api.dispatch = ->(_args, id) { @api.deliver(id, error: { value: { class_value: { + name: "ai.errors.Unregistered", fields: [field] + } } }) } + assert_equal message, assert_raises(Baml::Error) { call }.message + end + end + + def test_stream_each_propagates_stop_iteration_from_the_consumer + stream = Baml::Stream.new(@api, 99, :ADT_TAGGED_HEAP_HANDLE) + done = Class.new + values = [nil, "partial", done.new] + stream.define_singleton_method(:next) { values.shift } + Baml::Bridge::Protocol.stub(:registered_type, done) do + failure = StopIteration.new("consumer failed") + assert_same failure, assert_raises(StopIteration) { stream.each { raise failure } } + assert_equal 1, values.length + assert_same stream, stream.each { flunk "done must not be yielded" } + assert_instance_of Enumerator, stream.each + end + ensure + stream&.close + end + + def test_callback_ids_wrap_and_skip_pending_ids + first = Object.new + last = Object.new + wrapped = Object.new + assert_equal 1, @runtime.send(:register_call, first) + @runtime.instance_variable_set(:@next_callback_id, 0xffff_ffff) + assert_equal 0xffff_ffff, @runtime.send(:register_call, last) + assert_equal 2, @runtime.send(:register_call, wrapped) + pending = @runtime.instance_variable_get(:@pending) + assert_same first, pending.fetch(1) + assert_same last, pending.fetch(0xffff_ffff) + assert_same wrapped, pending.fetch(2) + end + + def test_result_handles_are_owned_before_wait_and_after_partial_decode + handle = { handle_value: { key: 41, handle_type: :ADT_TAGGED_HEAP_HANDLE } } + @api.dispatch = ->(_args, id) { @api.deliver(id, ok: { list_value: { items: [handle, handle] } }) } + Thread.new { @runtime.call_async("user.test", {}); nil }.join + Timeout.timeout(5) { GC.start until @api.released_handles.size == 2 } + assert_equal [41, 41], 2.times.map { released_handle } + + @api.dispatch = lambda do |_args, id| + @api.deliver(id, ok: { list_value: { items: [handle, { prompt_ast_value: {} }, handle] } }) + end + assert_raises(Baml::Bridge::UnsupportedTypeError) { call } + assert_equal 41, released_handle # The occurrence decoding never reached. + Timeout.timeout(5) { GC.start until @api.released_handles.size == 1 } + assert_equal 41, released_handle + + @api.dispatch = ->(_args, id) { @api.deliver(id, ok: { list_value: { items: [handle, handle] } }) } + owners = call + refute_same owners.first, owners.last + GC.start + assert @api.released_handles.empty? + owners.each(&:close) + assert_equal [41, 41], 2.times.map { released_handle } + end + + def test_reflected_types_release_every_live_head_on_refusal_and_abandonment + reflected = { ty_value: { function: { + params: [{ ty: { map: { + key: { enum: { live: 41 } }, + value: { class_ty: { live: 42, type_args: [{ type_alias: { live: 41 } }] } } + } } }], + ret: { interface: { live: 43, bindings: [{ name: "Item", ty: { enum_variant: { live: 44, variant: "V" } } }] } }, + throws: { optional: { inner: { union: { options: [{ enum: { live: 45 } }, { class_ty: { anonymous_label: "display" } }] } } } } + } } } + @api.dispatch = ->(_args, id) { @api.deliver(id, ok: reflected) } + assert_raises(Baml::Bridge::UnsupportedTypeError) { call } + assert_equal [41, 41, 42, 43, 44, 45], 6.times.map { released_handle }.sort + + Thread.new { @runtime.call_async("user.reflected", {}); nil }.join + Timeout.timeout(5) { GC.start until @api.released_handles.size == 6 } + assert_equal [41, 41, 42, 43, 44, 45], 6.times.map { released_handle }.sort + GC.start + assert @api.released_handles.empty? + end + + def test_reflected_type_references_are_released_after_partial_decode + @api.dispatch = lambda do |_args, id| + @api.deliver(id, ok: { list_value: { items: [ + { handle_value: { key: 40, handle_type: :ADT_TAGGED_HEAP_HANDLE } }, + { ty_value: { list: { item: { class_ty: { live: 41 } } } } }, + { handle_value: { key: 42, handle_type: :ADT_TAGGED_HEAP_HANDLE } } + ] } }) + end + assert_raises(Baml::Bridge::UnsupportedTypeError) { call } + assert_equal [41, 42], 2.times.map { released_handle }.sort + Timeout.timeout(5) { GC.start until @api.released_handles.size == 1 } + assert_equal 40, released_handle + end + + def test_interrupted_callback_registration_is_retained_and_completed_once + runtime = Baml::Bridge.const_get(:ProcessRuntime, false).new + runtime.register_program("bytecode") + runtime.instance_variable_set(:@library, Object.new) + api = FakeApi.new + attempts = Hash.new(0) + { register_bridge!: nil, register_result_callback: :@result_callback, + register_host_callbacks: :@host_callbacks, register_unhandled_spawn_error_callback: :@spawn_callback }.each do |method, owner| + api.define_singleton_method(method) do |*args| + raise "callback was not retained" if owner && !runtime.instance_variable_get(owner) + attempts[method] += 1 + Thread.current.raise Interrupt + super(*args) + end + end + Native::Api.stub(:new, api) do + 4.times { assert_raises(Interrupt) { runtime.initialize! } } + runtime.initialize! + end + assert_equal [1, 1, 1, 1], attempts.values + assert_equal 1, api.initialize_count + assert_same runtime.instance_variable_get(:@result_callback).function, api.callback + refute_nil api.host_dispatch + refute_nil api.host_release + refute_nil api.host_cancel + end + + def test_callback_argument_decode_releases_unadopted_live_types_and_later_handles + protocol = Baml::Bridge::Protocol + scope = Baml::Bridge::EncodingScope.new + key = Baml::Bridge::HostValues.register(->(*) { flunk "decoding must fail before invoking Ruby" }, scope, + [:function, [], [:int], [:never]]) + scope.submitted! + retained = [] + decode = protocol.method(:decode_value) + protocol.stub(:decode_value, ->(*args, **kwargs) { value = decode.call(*args, **kwargs); retained << value if value.is_a?(Baml::Handle); value }) do + frame = Wire::HostInvocation.new(host_value_key: key, callback_id: 123, effective_state: 77, + application_args: Wire::BamlToHostCall.encode(Wire::BamlToHostCall.new(args: [ + { value: { handle_value: { key: 40, handle_type: :ADT_TAGGED_HEAP_HANDLE } } }, + { value: { ty_value: { class_ty: { live: 41 } } } }, + { value: { handle_value: { key: 42, handle_type: :ADT_TAGGED_HEAP_HANDLE } } } + ]))) + Baml::Bridge::HostCallbacks.new(@api).send(:invoke, frame) + end + id, completion = @api.host_completions.pop + assert_equal 123, id + assert_equal :bridge_failure, completion.outcome + assert_includes completion.bridge_failure, "ty_value is not yet supported" + assert_equal [41, 42, 77], 3.times.map { released_handle }.sort + GC.start + assert @api.released_handles.empty?, "adopted handle must stay owned by its Ruby wrapper" + assert_equal 1, retained.length + retained.first.close + assert_equal 40, released_handle + assert @api.released_handles.empty? + ensure + retained&.each(&:close) + Baml::Bridge::HostValues.handle_release(key) if key + scope&.close + end + + class CallbackField < T::Struct + const :callback, T.any(T.proc.params(value: Integer).returns(Integer), Baml::Closure) + end + + def test_non_proc_host_callable_in_a_struct_field_decodes_as_a_proc + callable = Object.new + callable.define_singleton_method(:call) { |value| value + 1 } + scope = Baml::Bridge::EncodingScope.new + key = Baml::Bridge::HostValues.register(callable, scope, [:function, [[nil, [:int], false]], [:int], [:never]]) + scope.submitted! + Baml::Bridge.define_class("user.CallbackField", CallbackField, + fields: { "callback" => [:callback, [:function, [[nil, [:int], false]], [:int], [:never]]] }) + handle = { handle_value: { key: key, handle_type: :HOST_VALUE_CALLABLE } } + @api.dispatch = ->(_args, id) { @api.deliver(id, ok: { class_value: { + name: "user.CallbackField", fields: [{ key: "callback", value: handle }] + } }) } + result = call + Baml::Bridge::HostValues.handle_release(key) + key = nil + assert_instance_of CallbackField, result + assert_instance_of Proc, result.callback + assert_equal 42, result.callback.call(41) + assert_same callable, result.callback.binding.receiver + ensure + Baml::Bridge::HostValues.handle_release(key) if key + scope&.close + end + + def test_callback_returns_and_closure_arguments_preserve_bytes + Baml::Bridge.define_function("user.bytes_callback", params: [["cb", [:function, [], [:bytes], [:never]]]]) + @api.dispatch = lambda do |args, id| + key = args.kwargs.first.value.handle.key + frame = Wire::HostInvocation.encode(Wire::HostInvocation.new( + host_value_key: key, callback_id: 123, + application_args: Wire::BamlToHostCall.encode(Wire::BamlToHostCall.new) + )) + pointer = FFI::MemoryPointer.from_string(frame) + @api.host_dispatch.call(pointer, frame.bytesize) + _, completion = Timeout.timeout(5) { @api.host_completions.pop } + assert_equal :returned, completion.outcome + value = completion.returned + assert_equal :uint8array_value, value.value + @api.host_release.call(key) + @api.deliver(id, ok: { uint8array_value: value.uint8array_value }) + end + assert_equal "\0bytes".b, @runtime.call_async("user.bytes_callback", { "cb" => -> { "\0bytes".b } }).wait + + @api.dispatch = lambda do |_args, id| + @api.deliver(id, ok: { handle_value: { key: 77, handle_type: :FUNCTION_REF, ty: { function: { + params: [{ name: "data", mode: :BAML_TY_FUNCTION_PARAM_MODE_REQUIRED, + ty: { primitive: { kind: :BAML_TY_PRIMITIVE_BYTES } } }], + ret: { primitive: { kind: :BAML_TY_PRIMITIVE_BYTES } }, throws: { never: {} } + } } } }) + end + closure = call + @api.dispatch = lambda do |args, id| + assert_equal :uint8array_value, args.kwargs.first.value.value + @api.deliver(id, ok: { uint8array_value: args.kwargs.first.value.uint8array_value }) + end + Baml::Bridge.stub(:call_async, ->(target, args, **options) { @runtime.call_async(target, args, **options) }) do + assert_equal "\0arg".b, closure.call("\0arg".b) + end + ensure + closure&.close + end + + def test_closure_parameters_keep_names_modes_and_complete_descriptors + @api.dispatch = lambda do |_args, id| + @api.deliver(id, ok: { handle_value: { key: 77, handle_type: :FUNCTION_REF, ty: { function: { + params: [{ mode: :BAML_TY_FUNCTION_PARAM_MODE_REQUIRED, ty: { primitive: { kind: :BAML_TY_PRIMITIVE_FLOAT } } }, + { name: "count", mode: :BAML_TY_FUNCTION_PARAM_MODE_OPTIONAL, ty: { primitive: { kind: :BAML_TY_PRIMITIVE_INT } } }], + ret: { list: { item: { primitive: { kind: :BAML_TY_PRIMITIVE_FLOAT } } } }, throws: { never: {} } + } } } }) + end + closure = call + assert_equal({ "arg0" => [:float], "count" => [:int] }, closure.parameter_descriptors) + @api.dispatch = lambda do |args, id| + assert_equal 77, args.function_handle + assert_equal 5.0, args.kwargs.first.value.float_value + @api.deliver(id, ok: { int_value: args.kwargs.length }) + end + Baml::Bridge.stub(:call_async, ->(target, args, **options) { @runtime.call_async(target, args, **options) }) do + assert_equal 1, closure.call(5) + assert_equal 2, closure.call(arg0: 5, count: 2) + assert_raises(ArgumentError) { closure.call(5, 2) } + assert_raises(TypeError) { closure.call(5, count: "wrong") } + assert @api.released_handles.size == 2, "refused arguments must not clone the closure target" + end + ensure + closure&.close + end + + class GenericValue < T::Struct + const :value, String + + def call + raise "a registered class must not be encoded as a callback" + end + end + + def test_decoded_generic_preserves_string_literal_and_alias_hints + protocol = Baml::Bridge::Protocol + Baml::Bridge.define_class("user.GenericValue", GenericValue, fields: { "value" => [:value, [:param, 0, "T"]] }, type_params: ["T"]) + protocol.define_alias("user.Text", [:string]) + [{ primitive: { kind: :BAML_TY_PRIMITIVE_STRING } }, { literal: { string_value: "text" } }, + { type_alias: { name: "user.Text" } }].each do |text_type| + type = { union: { options: [text_type, { primitive: { kind: :BAML_TY_PRIMITIVE_BYTES } }] } } + [:string_value, :uint8array_value].each do |kind| + @api.dispatch = lambda do |args, id| + if args.kwargs.empty? + @api.deliver(id, ok: { class_value: { name: "user.GenericValue", type_args: [type], fields: [ + { key: "value", value: { kind => "text" } } + ] } }) + else + assert_equal kind, args.kwargs.first.value.class_value.fields.first.value.value + @api.deliver(id, ok: { bool_value: true }) + end + end + assert call("value" => call) + end + end + end + + def test_concurrent_calls_receive_only_their_own_result + dispatched = Queue.new + @api.dispatch = ->(args, id) { dispatched << [args, id] } + threads = 2.times.map { |i| Thread.new { call("value" => i) } } + Timeout.timeout(5) do + calls = 2.times.map { dispatched.pop } + refute_equal calls[0][0].call_id, calls[1][0].call_id + refute_equal calls[0][1], calls[1][1] + calls.reverse_each do |args, id| + @api.deliver(id, ok: { int_value: args.kwargs.first.value.int_value }) + end + assert_equal [0, 1], threads.map(&:value) + end + assert_empty pending + assert_equal 1, @api.initialize_count + end + + def test_callback_capture_errors_reach_caller + @api.dispatch = ->(_args, id) { @api.deliver(id, ok: { int_value: 1 }) } + Baml::Bridge::Protocol.stub(:capture_result, ->(*) { raise "capture failed" }) do + error = Timeout.timeout(5) { assert_raises(RuntimeError) { call } } + assert_equal "capture failed", error.message + end + assert_empty pending + end + + def test_dispatch_failure_removes_pending_call + Baml::Bridge.define_function("user.namespace.OriginalName", params: [["callback", [:function, [["value", [:int], false]], [:int], [:never]]]]) + callback = ->(value) { value } + registry = Baml::Bridge.const_get(:HostValues, false) + key = nil + @api.dispatch = lambda do |args, _id| + key = args.kwargs.first.value.handle.key + assert_same callback, registry.fetch(key) + raise "dispatch failed" + end + error = assert_raises(RuntimeError) { call("callback" => callback) } + assert_equal "dispatch failed", error.message + assert_raises(Baml::Bridge::Error) { registry.fetch(key) } + assert_equal [(2**40) + 1], @api.released_calls + assert_empty pending + end + + def test_interrupt_after_allocation_releases_call + @api.define_singleton_method(:new_function_call) do + call_id = super() + Thread.current.raise Interrupt + call_id + end + assert_raises(Interrupt) { call } + assert_equal [(2**40) + 1], @api.released_calls + assert_empty pending + end + + def test_call_waiters_cache_outcomes_and_survive_interrupt_and_cancel + dispatched = Queue.new + @api.dispatch = ->(args, id) { dispatched << [args.call_id, id] } + result = @runtime.call_async("user.test", {}) + engine_id, callback_id = dispatched.pop + refute result.done? + assert_raises(Baml::WaitTimeout) { result.wait(timeout: 0.001) } + refute result.done? + + interrupted = Thread.new do + result.wait + rescue Interrupt => error + error + end + Thread.pass until interrupted.status == "sleep" + interrupted.raise(Interrupt) + assert_instance_of Interrupt, interrupted.value + refute result.done? + + waiters = 3.times.map { Thread.new { result.wait(timeout: 5) } } + Thread.pass until waiters.all? { |thread| thread.status == "sleep" } + assert result.cancel + refute result.cancel + assert_equal [engine_id], @api.cancelled_calls + refute result.done? + + callback = @runtime.instance_variable_get(:@result_callback) + handler = callback.instance_variable_get(:@handler) + delivering_threads = Queue.new + callback.instance_variable_set(:@handler, lambda do |id, bytes| + delivering_threads << Thread.current + handler.call(id, bytes) + end) + library = FFI::DynamicLibrary.open(ENV.fetch("BAML_RUBY_TEST_THREAD_FIXTURE"), FFI::DynamicLibrary::RTLD_NOW) + invoke = FFI::Function.new( + :void, %i[pointer uint32 pointer size_t], + library.find_function("baml_test_invoke_on_native_thread"), blocking: true + ) + bytes = Wire::BamlOutboundResult.encode(Wire::BamlOutboundResult.new(ok: { list_value: { items: [{ int_value: 42 }] } })) + pointer = FFI::MemoryPointer.new(:uint8, bytes.bytesize) + pointer.put_bytes(0, bytes) + GC.start + invoke.call(@api.callback, callback_id, pointer, bytes.bytesize) + refute_same Thread.current, delivering_threads.pop + values = waiters.map(&:value) + assert_equal [42], values.first + values.each { |value| assert_same values.first, value } + assert_same values.first, result.wait(timeout: 0) + assert result.done? + refute result.cancel + + failed = @runtime.call_async("user.test", {}) + _, callback_id = dispatched.pop + @api.deliver(callback_id, error: { value: { string_value: "cached error" } }) + assert failed.done? + error = assert_raises(Baml::Error) { failed.wait } + assert_equal "cached error", error.message + assert_same error, assert_raises(Baml::Error) { failed.wait } + assert_empty pending + end + + def test_thread_start_failure_releases_callback_arguments_and_completes + frame = Wire::HostInvocation.new( + host_value_key: 999, callback_id: 124, effective_state: 77, + application_args: Wire::BamlToHostCall.encode(Wire::BamlToHostCall.new(args: [ + { value: { handle_value: { key: 66, handle_type: :ADT_TAGGED_HEAP_HANDLE } } } + ])), + cancel: { handle_value: { key: 88, handle_type: :ADT_TAGGED_HEAP_HANDLE } } + ) + bytes = Wire::HostInvocation.encode(frame) + pointer = FFI::MemoryPointer.new(:uint8, bytes.bytesize) + pointer.put_bytes(0, bytes) + Thread.stub(:new, ->(*) { raise ThreadError, "thread limit" }) do + @api.host_dispatch.call(pointer, bytes.bytesize) + end + id, completion = @api.host_completions.pop + assert_equal 124, id + assert_equal :bridge_failure, completion.outcome + assert_includes completion.bridge_failure, "thread limit" + assert_equal [66, 77, 88], 3.times.map { released_handle }.sort + assert @api.released_handles.empty? + assert @api.host_completions.empty? + end + + def test_cancelled_host_callback_keeps_resources_until_proc_returns + registry = Baml::Bridge.const_get(:HostValues, false) + scope = Baml::Bridge.const_get(:EncodingScope, false).new + entered = Queue.new + unblock = Queue.new + exited = Queue.new + cb = ->(value) { entered << value; unblock.pop; exited << true; "finished" } + Baml::Bridge.define_function("user.cancel_callback", params: [["cb", [:function, [["value", [:int], false]], [:string], [:never]]]]) + request = Wire::CallFunctionArgs.decode(Baml::Bridge::Protocol.encode_call(1, "user.cancel_callback", { "cb" => cb }, scope: scope)) + key = request.kwargs.first.value.handle.key + scope.submitted! + frame = Wire::HostInvocation.new( + host_value_key: key, callback_id: 123, effective_state: 77, + application_args: Wire::BamlToHostCall.encode(Wire::BamlToHostCall.new(args: [{ value: { int_value: 5 } }])), + cancel: { class_value: { name: "baml.spawn.CancelToken", fields: [ + { key: "inner", value: { handle_value: { key: 88, handle_type: :ADT_TAGGED_HEAP_HANDLE } } } + ] } } + ) + bytes = Wire::HostInvocation.encode(frame) + pointer = FFI::MemoryPointer.new(:uint8, bytes.bytesize) + pointer.put_bytes(0, bytes) + Timeout.timeout(5) do + # Dispatch must return while the proc is blocked. Overwrite the borrowed + # buffer immediately to prove the invocation owns its request bytes. + @api.host_dispatch.call(pointer, bytes.bytesize) + pointer.put_bytes(0, "\0" * bytes.bytesize) + assert_equal 5, entered.pop + @api.host_cancel.call(123) + assert_same cb, registry.fetch(key) + assert @api.released_handles.empty? + assert @api.host_completions.empty? + assert exited.empty? + unblock << true + assert exited.pop + id, completion = @api.host_completions.pop + assert_equal [123, :returned, "finished"], [id, completion.outcome, completion.returned.string_value] + assert_equal [77, 88], 2.times.map { released_handle }.sort + @api.host_cancel.call(123) + assert @api.host_completions.empty?, "late cancellation must not complete twice" + assert @api.released_handles.empty?, "late cancellation must not release twice" + # Host keys in results are borrowed too: pin before engine release, + # even when the first waiter does not run until after that release. + @api.dispatch = lambda do |_args, id| + @api.deliver(id, ok: { handle_value: { key: key, handle_type: :HOST_VALUE_CALLABLE } }) + @api.host_release.call(key) + end + result = @runtime.call_async("user.return_callback", {}) + assert_raises(Baml::Bridge::Error) { registry.fetch(key) } + assert_same cb, result.wait + end + ensure + unblock << true if unblock + registry&.handle_release(key) if key + end + + private + + def call(arguments = {}) + @runtime.call_async("user.namespace.OriginalName", arguments).wait + end + + def pending + @runtime.instance_variable_get(:@pending) + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/test/bridge_encode_test.rb b/baml_language/sdk_tests/crates/ruby_sorbet/test/bridge_encode_test.rb new file mode 100644 index 00000000000..bbc63bb0d44 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/test/bridge_encode_test.rb @@ -0,0 +1,1449 @@ +# frozen_string_literal: true + +require "minitest/autorun" +require "timeout" +require "sorbet-runtime" +$LOAD_PATH.unshift File.expand_path("../../../../sdks/ruby/bridge_ruby/lib", __dir__) +require "baml/bridge" + +class BridgeEncodeTest < Minitest::Test + Encode = Baml::Bridge::Encode + Wire = BamlBridge::Cffi::V1 + Protocol = Baml::Bridge::Protocol + + class Color < T::Enum + enums do + Red = new("red") + Blue = new("blue") + end + end + Box = Struct.new(:value, :note, keyword_init: true) + + class Registry + def initialize + @types = {} + @fields = {} + @aliases = {} + @normalized_aliases = {} + @crossings = {} + @params = {} + end + + def define(type, name, fields = nil, crossing: :fields, type_params: []) + @params[name] = type_params + Baml::Types.register(type, name, fields ? type_params.length : nil) + @types[type] = [name, fields] + @fields[name] = fields + @crossings[name] = crossing + end + + def class_type_params(name) + @params.fetch(name) + end + + def class_crossing(name) + @crossings.fetch(name, :fields) + end + + def define_alias(name, body, recursive: false) + @aliases[name] = [body, recursive] + @normalized_aliases.clear + end + + def normalized_alias(name) + @normalized_aliases[name] ||= yield @aliases[name] + end + + def alias_definition(name) + @aliases[name] + end + + def encoding_type(type) + @types.fetch(type, [nil, nil]) + end + + def encoding_fields(name) + @fields.fetch(name) + end + + def enum_values(_name) + %w[red blue] + end + end + + class Api + attr_reader :cloned, :released, :completions + + def initialize + @cloned = [] + @released = [] + @completions = [] + end + + def complete_host_call(id, bytes) + @completions << [id, Wire::HostCallCompletion.decode(bytes)] + end + + def handle_clone(key) + @cloned << key + key + end + + def handle_release(key) + @released << key + end + end + + def setup + @host_keys = [] + @registry = Registry.new + @registry.define(Color, "user.Color") + @registry.define(Box, "user.Box", { + "value" => [:value, [:param, 0, "T"]], + "note" => [:note, [:union, [[:null], [:string]]]] + }, type_params: ["T"]) + end + + def test_scope_attempts_every_release_before_raising_the_first_error + api = Api.new + failure = RuntimeError.new("release failed") + api.define_singleton_method(:handle_release) do |key| + super(key) + raise failure if key == 1 + end + scope = Baml::Bridge::EncodingScope.new + [1, 2, 3].each { |key| scope.own(api, key) } + assert_same failure, assert_raises(RuntimeError) { scope.close } + assert_equal [1, 2, 3], api.released + scope.close + assert_equal [1, 2, 3], api.released + end + + def test_string_slots_validate_binary_utf8_values_and_keys + assert_equal "café", encode("café".b, [:string]).string_value + assert_raises(TypeError) { encode("\xff".b, [:string]) } + slot = [:map, [:string], [:int]] + assert_equal "café", encode({ "café".b => 1 }, slot).map_value.entries.first.string_key + assert_raises(TypeError) { encode({ "\xff".b => 1 }, slot) } + end + + def test_alias_normalization_is_cached_per_registry_and_invalidated_on_definition + @registry.define_alias("user.A", [:list, [:int]]) + @registry.define_alias("user.B", [:map, [:string], [:alias, "user.A"]]) + first = Encode::Slot.stated_form([:alias, "user.B"], @registry) + assert_same first, Encode::Slot.stated_form([:alias, "user.B"], @registry) + @registry.define_alias("user.A", [:string]) + assert_equal [:map, [:string], [:string]], Encode::Slot.stated_form([:alias, "user.B"], @registry) + other = Registry.new + other.define_alias("user.B", [:bool]) + assert_equal [:bool], Encode::Slot.stated_form([:alias, "user.B"], other) + end + + def test_alias_cache_does_not_publish_a_normalization_from_before_redefinition + name = "user.CacheRace" + Protocol.define_alias(name, [:int]) + entered, resume = Queue.new, Queue.new + normalizer = Thread.new do + Protocol.normalized_alias(name) do |definition| + entered << true + resume.pop + definition.first + end + end + Timeout.timeout(5) { entered.pop } + Protocol.define_alias(name, [:string]) + resume << true + assert_equal [:int], normalizer.value + assert_equal [:string], Encode::Slot.stated_form([:alias, name], Protocol) + ensure + resume << true if resume + normalizer&.join(5) + end + + def test_expanding_aliases_spend_the_planning_budget_even_with_a_warm_cache + @registry.define_alias("user.A0", [:int]) + 1.upto(14) do |index| + previous = [:alias, "user.A#{index - 1}"] + @registry.define_alias("user.A#{index}", union([:list, previous], [:map, [:string], previous])) + end + tree = Encode::Read.new(@registry).read([], "alias") + 2.times do + Timeout.timeout(1) do + error = assert_raises(Encode::Fault) { Encode::Plan.new(@registry, budget: 10).plan(tree, [:alias, "user.A14"]) } + assert_includes error.message, "planning-step budget" + end + end + # Prime a smaller alias, then ensure cache hits cannot evade later union work. + Encode::Slot.stated_form([:alias, "user.A6"], @registry) + Timeout.timeout(1) do + assert_raises(Encode::Fault) { Encode::Plan.new(@registry, budget: 10).plan(tree, [:alias, "user.A6"]) } + end + end + + def expanding_aliases(depth) + @registry.define_alias("user.Deep0", [:int]) + 1.upto(depth) do |index| + previous = [:alias, "user.Deep#{index - 1}"] + @registry.define_alias("user.Deep#{index}", union([:list, previous], [:map, [:string], previous])) + end + end + + def plan_alias_or_refuse(tree, slot) + assert_equal :list, Encode::Plan.new(@registry).plan(tree, slot).kind + rescue Encode::Fault => error + assert_includes error.message, "planning-step budget" + end + + def test_default_budget_bounds_direct_aliases_and_warm_cache_repeats + expanding_aliases(16) + tree = Encode::Read.new(@registry).read([], "alias") + 2.times do + Timeout.timeout(0.75) do + assert_equal :list, Encode::Plan.new(@registry).plan(tree, [:alias, "user.Deep14"]).kind + plan_alias_or_refuse(tree, [:alias, "user.Deep16"]) + end + Timeout.timeout(0.1) do + error = assert_raises(Encode::Fault) { Encode::Plan.new(@registry, budget: 10).plan(tree, [:alias, "user.Deep14"]) } + assert_includes error.message, "planning-step budget" + end + end + end + + def test_class_field_alias_normalization_spends_the_budget + expanding_aliases(18) + klass = Class.new(Struct.new(:items)) + @registry.define(klass, "user.DeepField", { "items" => [:items, [:alias, "user.Deep18"]] }) + tree = Encode::Read.new(@registry).read(klass.new([]), "field") + 2.times do + Timeout.timeout(0.1) do + error = assert_raises(Encode::Fault) { Encode::Plan.new(@registry, budget: 10).plan(tree, [:class, "user.DeepField", []]) } + assert_includes error.message, "planning-step budget" + end + end + end + + def test_generic_class_field_alias_normalization_spends_the_budget + expanding_aliases(18) + klass = Class.new(Struct.new(:value, :items)) + @registry.define(klass, "user.DeepGeneric", { + "value" => [:value, [:param, 0, "T"]], + "items" => [:items, [:alias, "user.Deep18"]] + }, type_params: ["T"]) + tree = Encode::Read.new(@registry).read(klass.new(1, []), "generic") + [[:hole], [:int]].each do |arg| + 2.times do + Timeout.timeout(0.1) do + error = assert_raises(Encode::Fault) { Encode::Plan.new(@registry, budget: 10).plan(tree, [:class, "user.DeepGeneric", [arg]]) } + assert_includes error.message, "planning-step budget" + end + end + end + end + + def teardown + @host_keys.each { |key| Baml::Bridge::HostValues.handle_release(key) } + end + + def test_each_opaque_host_send_survives_release_of_the_other_sends + object = StandardError.new("opaque") + registry = Baml::Bridge::HostValues + scope = Baml::Bridge::EncodingScope.new + original = registry.register(object, scope) + @host_keys << original + scope.submitted! + first_scope = Baml::Bridge::EncodingScope.new + second_scope = Baml::Bridge::EncodingScope.new + first = encode(object, [:handle], scope: first_scope).handle + second = encode(object, [:handle], scope: second_scope).handle + @host_keys.concat([first.key, second.key]) + first_scope.submitted! + second_scope.submitted! + assert_equal :HOST_VALUE_OPAQUE, first.handle_type + assert_equal :HOST_VALUE_OPAQUE, second.handle_type + assert_equal 3, [original, first.key, second.key].uniq.length + registry.handle_release(original) + assert_same object, registry.fetch(first.key) + assert_same object, registry.fetch(second.key) + registry.handle_release(first.key) + assert_same object, registry.fetch(second.key) + registry.handle_release(second.key) + retry_scope = Baml::Bridge::EncodingScope.new + retry_key = encode(object, [:handle], scope: retry_scope).handle.key + assert_same object, registry.fetch(retry_key) + retry_scope.close + assert_raises(Baml::Bridge::Error) { registry.fetch(retry_key) } + ensure + scope&.close + first_scope&.close + second_scope&.close + retry_scope&.close + end + + def test_opaque_host_send_rollback_releases_only_its_own_key + object = StandardError.new("opaque") + registry = Baml::Bridge::HostValues + original_scope = Baml::Bridge::EncodingScope.new + original = registry.register(object, original_scope) + original_scope.submitted! + registry.handle_release(original) + first_scope = Baml::Bridge::EncodingScope.new + second_scope = Baml::Bridge::EncodingScope.new + first = encode(object, [:handle], scope: first_scope).handle.key + second = encode(object, [:handle], scope: second_scope).handle.key + @host_keys << second + refute_equal first, second + second_scope.submitted! + second_scope.close + first_scope.close + assert_raises(Baml::Bridge::Error) { registry.fetch(first) } + assert_same object, registry.fetch(second) + ensure + original_scope&.close + first_scope&.close + second_scope&.close + end + + def test_ordinary_values_do_not_check_the_opaque_host_registry + Baml::Bridge::HostValues.stub(:opaque?, ->(*) { flunk "ordinary value checked the host registry" }) do + encode([nil, true, false, 1, 2.5, "text", "bytes".b, [], {}]) + end + end + + def test_callback_argument_result_failure_completes_with_a_bridge_failure + api = Api.new + frame = Wire::HostInvocation.new(callback_id: 7, application_args: "\xff".b) + Baml::Bridge::HostCallbacks.new(api).send(:invoke, frame) + assert_equal 1, api.completions.length + assert_equal 7, api.completions.first.first + assert_equal :bridge_failure, api.completions.first.last.outcome + end + + def test_a_callable_binds_to_one_function_member_and_is_ambiguous_among_several + cb = ->(value) { value } + signature = [:function, [["value", [:int], false], [nil, [:bytes], true]], [:int], [:never]] + other = [:function, [["value", [:string], false]], [:string], [:never]] + scope = Baml::Bridge::EncodingScope.new + result = encode(cb, union([:unknown], signature), scope: scope) + assert_equal signature, Encode::Slot.from_wire(result.value_type) + assert_equal :HOST_VALUE_CALLABLE, result.handle.handle_type + assert_equal [cb, signature], Baml::Bridge::HostValues.entry(result.handle.key) + assert_raises(Encode::Ambiguous) { encode(cb, union(signature, other), scope: scope) } + throws_param = [:function, [], [:int], [:list, [:param, 0, "E"]]] + result = encode(cb, throws_param, scope: scope) + assert_equal [:function, [], [:int], [:unknown]], Encode::Slot.from_wire(result.value_type) + [[:function, [["x", [:param, 0, "T"], false]], [:int], [:never]], + [:function, [], [:param, 0, "T"], [:never]]].each do |slot| + assert_raises(Encode::UnboundTypeArgs) { encode(cb, slot, scope: scope) } + end + ensure + scope&.close + end + + def test_reordered_unions_inside_function_signatures_are_one_candidate + first = union([:int], [:string]) + reordered = union([:string], [:int]) + signature = ->(ty) { [:function, [["value", ty, false]], [:list, ty], ty] } + scope = Baml::Bridge::EncodingScope.new + result = encode(->(value) { [value] }, union(signature.call(first), signature.call(reordered)), scope: scope) + assert_equal signature.call(first), Encode::Slot.from_wire(result.value_type) + ensure + scope&.close + end + + def test_a_losing_member_creates_nothing_and_a_refusal_creates_nothing + signature = [:function, [], [:int], [:never]] + cb = -> { 1 } + scope = Baml::Bridge::EncodingScope.new + registry = Baml::Bridge::HostValues + registrations = [] + register = registry.method(:register) + registry.stub(:register, ->(*args) { key = register.call(*args); registrations << key; key }) do + result = encode([cb, 1], union([:list, signature], [:list, union(signature, [:int])]), scope: scope) + assert_equal 1, registrations.length + assert_same cb, registry.fetch(result.list_value.values.first.handle.key) + assert_raises(Encode::UntypedCallable) { encode(cb, scope: scope) } + Protocol.define_function("user.untyped_callback", params: [["typed", signature]]) + assert_raises(Encode::UntypedCallable) do + Protocol.encode_call(1, "user.untyped_callback", { "typed" => cb, "untyped" => cb }, scope: scope) + end + assert_raises(Encode::Ambiguous) do + encode(cb, union(signature, [:function, [], [:string], [:never]]), scope: scope) + end + assert_equal 1, registrations.length + end + scope.close + assert_raises(Baml::Bridge::Error) { registry.fetch(registrations.first) } + ensure + scope&.close + end + + def test_a_hosts_own_exception_is_raised_as_it_is + signature = [:function, [], [:unknown], [:string]] + error = ArgumentError.new("host failed") + completion = complete(-> { raise error }, signature) + assert_equal :raised, completion.outcome + assert_equal ["host failed", "ArgumentError", "ruby"], + [completion.raised.message, completion.raised.class_name, completion.raised.language] + assert_same error, Baml::Bridge::HostValues.fetch(completion.raised.handle) + refute_empty completion.raised.traceback + + type = Class.new { attr_accessor :error; def value; raise error; end } + Protocol.define_class("user.CallbackGetter", type, fields: { "value" => [:value, [:int]] }) + value = type.new + value.error = error + completion = complete(-> { value }, signature) + assert_equal :raised, completion.outcome + assert_same error, Baml::Bridge::HostValues.fetch(completion.raised.handle) + value.error = Baml::Error.new("typed throw") + completion = complete(-> { value }, signature) + assert_equal :thrown, completion.outcome + assert_equal "typed throw", completion.thrown.string_value + completion = complete(-> { raise Baml::Panic.new("panic") }, signature) + assert_equal :thrown, completion.outcome + assert_equal "panic", completion.thrown.string_value + completion = complete(-> { raise Baml::CancelledError.new("cancelled") }, signature) + assert_equal :thrown, completion.outcome + assert_equal "cancelled", completion.thrown.string_value + map = Class.new(Hash) { define_method(:each) { |&_| raise error } }.new + completion = complete(-> { map }, signature) + assert_equal :raised, completion.outcome + assert_same error, Baml::Bridge::HostValues.fetch(completion.raised.handle) + end + + def test_binary_or_invalid_host_exception_text_is_scrubbed_without_losing_identity + error = IOError.new("café".b) + error.set_backtrace(["café".b + "\xff".b]) + completion = complete(-> { raise error }, [:function, [], [:int], [:never]]) + assert_equal :raised, completion.outcome + assert_equal "café", completion.raised.message + assert_equal "café�", completion.raised.traceback + assert completion.raised.message.valid_encoding? + assert completion.raised.traceback.valid_encoding? + assert_same error, Baml::Bridge::HostValues.fetch(completion.raised.handle) + end + + def test_invalid_callback_text_is_a_contract_violation_not_a_bridge_failure + ["\xff".b, "\xff".b.force_encoding(Encoding::UTF_8)].each do |value| + completion = complete(-> { value }, [:function, [], [:string], [:never]]) + assert_equal :contract_violation, completion.outcome + end + end + + def test_a_value_the_bridge_cannot_state_is_the_bridges_failure_not_the_callables + [ [[], union([:list, [:int]], [:list, [:string]])], + [-> { 1 }, [:unknown]], [[], [:list, [:param, 0, "T"]]], + [60.times.reduce(1) { |value, _| [value] }, [:unknown]], + [[], [:list, [:other]]], [[], [:list, [:handle]]] ].each do |value, returns| + completion = complete(-> { value }, [:function, [], returns, [:never]]) + assert_equal :bridge_failure, completion.outcome + end + phantom = Class.new(Struct.new(:value)) + Protocol.define_class("user.CallbackPhantom", phantom, fields: { "value" => [:value, [:int]] }, type_params: ["T"]) + assert_equal :bridge_failure, complete(-> { phantom.new(1) }, [:function, [], [:unknown], [:never]]).outcome + completion = complete(-> { "not int" }, [:function, [], [:int], [:never]]) + assert_equal :contract_violation, completion.outcome + completion = complete(-> { raise Baml::Error.new(1) }, [:function, [], [:int], [:string]]) + assert_equal :contract_violation, completion.outcome + null = complete(-> { nil }, [:function, [], [:null], [:never]]) + assert_equal :returned, null.outcome + assert_nil null.returned.value + end + + def test_callable_results_state_their_signature_and_wire_depth_precedes_registration + signature = [:function, [["x", [:float], false]], [:float], [:never]] + cb = ->(x) { x } + completion = complete(-> { cb }, [:function, [], signature, [:never]]) + assert_equal :returned, completion.outcome + assert_equal signature, Encode::Slot.from_wire(completion.returned.value_type) + key = completion.returned.handle.key + @host_keys << key + assert_equal [cb, signature], Baml::Bridge::HostValues.entry(key) + deep = 49.times.reduce([:int]) { |ty, _| [:list, ty] } + scope = Baml::Bridge::EncodingScope.new + Baml::Bridge::HostValues.stub(:register, ->(*) { flunk "too-deep signature registered" }) do + assert_raises(Encode::Fault) { encode(cb, [:function, [], deep, [:never]], scope: scope) } + end + api = Api.new + frame = Wire::HostInvocation.new(host_value_key: 0, callback_id: 7) + Baml::Bridge::HostCallbacks.new(api).send(:invoke, frame) + assert_equal :bridge_failure, api.completions.first.last.outcome + assert_includes api.completions.first.last.bridge_failure, "Unknown BAML host-value key" + ensure + scope&.close + end + + def test_commit_failures_roll_back_callback_results_and_are_bridge_failures + api = Api.new + handle = Baml::Handle.new(api, 41, :ADT_TAGGED_HEAP_HANDLE) + api.define_singleton_method(:handle_clone) { |_| raise ArgumentError, "clone failed" } + signature = [:function, [], [:int], [:never]] + registry = Baml::Bridge::HostValues + registrations = [] + register = registry.method(:register) + registry.stub(:register, ->(*args) { key = register.call(*args); registrations << key; key }) do + value = [-> { 1 }, handle] + completion = complete(-> { value }, [:function, [], [:list, signature], [:never]]) + assert_equal :bridge_failure, completion.outcome + assert_includes completion.bridge_failure, "clone failed" + assert_equal 2, registrations.length + registrations.each { |key| assert_raises(Baml::Bridge::Error) { registry.fetch(key) } } + end + ensure + handle&.close + end + + def test_scalars_follow_the_slot_and_the_hosts_rules + [[nil, :null, nil], [true, :bool, :bool_value], [false, :bool, :bool_value], + [7, :int, :int_value], [2.5, :float, :float_value], ["text", :string, :string_value]].each do |value, kind, arm| + encoded = encode(value, [kind]) + arm ? assert_equal(arm, encoded.value) : assert_nil(encoded.value) + assert_nil encoded.value_type + assert_equal value, encoded.public_send(arm) if arm + end + assert_equal 7.0, encode(7, [:float]).float_value + assert_raises(TypeError) { encode(true, [:int]) } + assert_raises(TypeError) { encode(1.0, [:int]) } + assert_raises(TypeError) { encode(2**62, [:int]) } + assert_equal -(2**62), encode(-(2**62), [:int]).int_value + assert_equal 2**62 - 1, encode(2**62 - 1, [:int]).int_value + assert_raises(TypeError) { encode(Object.new) } + end + + def test_integer_to_float_conversion_is_exact_at_any_magnitude + assert_raises(TypeError) { encode(2**53 + 1, [:float]) } + [2**53, 2**63, 2**80, -(2**80)].each do |value| + assert_equal value.to_f, encode(value, [:float]).float_value + end + assert_raises(TypeError) { encode(2**1024, [:float]) } + end + + def test_enums_take_members_as_is_and_strings_by_conversion + [Color::Red, "red"].each do |value| + encoded = encode(value, [:enum, "user.Color"]) + assert_equal "user.Color", encoded.enum_value.name + assert_equal "red", encoded.enum_value.value + assert_nil encoded.value_type + end + assert_raises(TypeError) { encode("green", [:enum, "user.Color"]) } + assert_raises(TypeError) { encode(Color::Red, [:enum, "user.Other"]) } + end + + def test_literals_are_matched_exactly + [7, "draft", true, false].each do |value| + assert_nil encode(value, [:literal, value]).value_type + end + assert_raises(TypeError) { encode(8, [:literal, 7]) } + assert_raises(TypeError) { encode(1, [:literal, true]) } + assert_raises(TypeError) { encode(1.0, [:literal, 1.0]) } + assert_equal :int_value, encode(1, union([:literal, 1.0], [:literal, 1])).value + end + + def test_bigint_literal_types_are_decimal_while_values_stay_hex + [42, -42, 2**80].each do |value| + slot = [:literal, :bigint, value] + wire = Encode::Slot.to_wire(slot) + assert_equal value.to_s(10), wire.literal.bigint_value + assert_equal slot, Encode::Slot.from_wire(Wire::BamlTy.new(literal: { bigint_value: value.to_s(10) })) + assert_equal value.to_s(16), encode(value, slot).bigint_value + assert_equal value, Protocol.decode_value(Wire::BamlOutboundValue.new(literal_value: { bigint_value: value.to_s(16) })) + end + end + + def test_media_equality_includes_kind_and_every_content_field + image = Baml::Image.from_url("https://example.com/image", mime_type: "image/png") + same = Baml::Image.from_url("https://example.com/image", mime_type: "image/png") + assert_equal image, same + assert image.eql?(same) + assert_equal image.hash, same.hash + assert_equal 1, { image => 1 }[same] + [Baml::Audio.from_url(image.url, mime_type: image.mime_type), + Baml::Image.from_url(image.url), Baml::Image.from_url("other", mime_type: image.mime_type), + Baml::Image.from_base64("YQ=="), Baml::Image.from_file_content("image.png", "YQ==")].each do |other| + refute_equal image, other + refute image.eql?(other) + end + refute_equal Baml::Image.from_base64("YQ=="), Baml::Image.from_base64("Yg==") + refute_equal Baml::Image.from_file_content("a.png", "YQ=="), Baml::Image.from_file_content("b.png", "YQ==") + end + + def test_bigints_bytes_and_literal_kinds_follow_the_slot + [7, 2**62, -(2**80)].each do |value| + result = encode(value, [:bigint]) + assert_equal value.to_s(16), result.bigint_value + assert_nil result.value_type + end + assert_equal :bigint_value, encode(2**62).value + assert_equal :int_value, encode(7, union([:bigint], [:int])).value + assert_equal "2a", encode(42, [:literal, :bigint, 42]).bigint_value + assert_raises(TypeError) { encode(43, [:literal, :bigint, 42]) } + assert_equal :int_value, encode(42, union([:literal, :bigint, 42], [:literal, 42])).value + assert_equal :uint8array_value, encode("ab".b).value + assert_equal :string_value, encode("ab".b, [:string]).value + assert_equal :uint8array_value, encode("ab", [:bytes]).value + assert_equal :string_value, encode("ab", union([:bytes], [:string])).value + assert_equal :uint8array_value, encode("ab".b, union([:string], [:bytes])).value + assert_equal [:list, [:bytes]], type_of(["ab".b]) + end + + def test_bytes_convert_to_text_only_when_valid_utf8_including_map_keys + assert_raises(Encode::Refusal) { encode("\xff".b, [:string]) } + assert_raises(Encode::Refusal) { encode({ "\xff".b => 1 }) } + assert_equal "café", encode("café".b, [:string]).string_value + [[:unknown], [:map, [:string], [:int]]].each do |slot| + result = encode({ "café".b => 1 }, slot) + assert_equal "café", result.map_value.entries.first.string_key + assert_equal Encoding::UTF_8, result.map_value.entries.first.string_key.encoding + end + assert_equal :uint8array_value, encode("\xff".b).value + end + + def test_binary_strings_do_not_convert_to_enums + assert_raises(Encode::Refusal) { encode("red".b, [:enum, "user.Color"]) } + assert_equal :string_value, encode("red".b, union([:enum, "user.Color"], [:string])).value + end + + def test_enum_variants_admit_only_the_named_variant + [Color::Red, "red"].each do |value| + assert_equal "red", encode(value, [:enum_variant, "user.Color", "red"]).enum_value.value + assert_raises(TypeError) { encode(value, [:enum_variant, "user.Color", "blue"]) } + assert_equal "red", encode(value, union([:enum_variant, "user.Color", "blue"], [:enum_variant, "user.Color", "red"])).enum_value.value + end + end + + def test_recursive_aliases_are_judged_by_their_bodies_and_terminate + rec = [:alias, "user.Rec"] + @registry.define_alias("user.Rec", union([:int], [:list, rec]), recursive: true) + result = encode([1, [2]], rec) + assert_equal [:list, rec], Encode::Slot.from_wire(result.value_type) + assert_equal [:list, rec], Encode::Slot.from_wire(result.list_value.values.last.value_type) + assert_raises(TypeError) { encode(["not an int"], rec) } + @registry.define_alias("user.Ints", [:list, [:int]]) + assert_equal [:list, [:int]], type_of([1], [:alias, "user.Ints"]) + @registry.define_alias("user.Number", [:int]) + assert_equal [:list, [:int]], Encode::Slot.stated_form([:list, [:alias, "user.Number"]], @registry) + + a, b = [:alias, "user.A"], [:alias, "user.B"] + @registry.define_alias("user.A", b, recursive: true) + @registry.define_alias("user.B", a, recursive: true) + assert_raises(TypeError) { encode(1, a) } + @registry.define_alias("user.A", union(b, [:list, a]), recursive: true) + assert_equal [:list, a], type_of([[]], a) + assert_raises(TypeError) { encode([1], a) } + @registry.define_alias("user.A", b) + @registry.define_alias("user.B", a) + assert_raises(Encode::Fault) { encode(1, a) } + end + + def test_default_budget_bounds_overlapping_mutually_recursive_alias_trials + a, b = [:alias, "user.A"], [:alias, "user.B"] + @registry.define_alias("user.A", union([:list, a], [:list, b], [:int]), recursive: true) + @registry.define_alias("user.B", union([:list, a], [:list, b], [:string]), recursive: true) + tree = Encode::Read.new(@registry).read(30.times.reduce(1) { |value, _| [value] }, "recursive") + error = Timeout.timeout(15) do + assert_raises(Encode::Fault) { Encode::Plan.new(@registry).plan(tree, a) } + end + assert_includes error.message, "planning-step budget" + end + + def test_overlapping_mutually_recursive_alias_trials_share_the_planning_budget + a, b = [:alias, "user.A"], [:alias, "user.B"] + @registry.define_alias("user.A", union([:list, a], [:list, b], [:int]), recursive: true) + @registry.define_alias("user.B", union([:list, a], [:list, b], [:string]), recursive: true) + tree = Encode::Read.new(@registry).read(8.times.reduce(1) { |value, _| [value] }, "recursive") + error = assert_raises(TypeError) { Encode::Plan.new(@registry, budget: 100).plan(tree, a) } + assert_includes error.message, "planning-step budget" + error = assert_raises(TypeError) { encode([[1]], a) } + assert_includes error.message, "fits more than one" + end + + def test_leaves_fit_their_kind_any_open_slot_and_open_union_members + @registry.define(Class.new, "user.ImageWrapper", {}, crossing: :media_image) + image = Baml::Image.from_url("https://example.com/asset", mime_type: "image/png") + [[:media, :image], [:media, :generic], [:class, "user.ImageWrapper", []], + [:unknown], [:interface, "user.I"], union([:int], [:media, :image]), union([:int], [:unknown])].each do |slot| + result = encode(image, slot) + assert_nil result.value_type + assert_equal :IMAGE, result.media_value.media + assert_equal image.url, result.media_value.url + end + assert_raises(TypeError) { encode(image, [:media, :audio]) } + assert_raises(TypeError) { encode(image, [:class, "user.ImageWrapper", [[:int]]]) } + assert_equal "scan.pdf", encode(Baml::Pdf.from_file_content("/tmp/scan.pdf", "JVBERg==")).media_value.file_content.name + assert_equal "YWJj", encode(Baml::Audio.from_base64("YWJj")).media_value.base64 + opaque = Class.new(Struct.new(:value)) + @registry.define(opaque, "user.Opaque", { "value" => [:value, [:int]] }, crossing: :opaque) + error = assert_raises(TypeError) { encode(opaque.new(1)) } + assert_includes error.message, "built only by BAML" + api = Api.new + handle = Baml::Handle.new(api, 41, :ADT_TAGGED_HEAP_HANDLE) + scope = Baml::Bridge::EncodingScope.new + [[:int], [:unknown], union([:int], [:string])].each do |slot| + assert_equal 41, encode(handle, slot, scope: scope).handle.key + end + assert_equal [41, 41, 41], api.cloned + ensure + scope&.close + handle&.close + end + + def test_unions_rank_identity_over_natural_shape_over_conversion + assert_equal :int_value, encode(5, union([:float], [:int])).value + assert_equal :float_value, encode(5, union([:float], [:string])).value + assert_equal :int_value, encode(5, union([:float], [:unknown])).value + assert_equal :string_value, encode("red", union([:enum, "user.Color"], [:string])).value + assert_equal :enum_value, encode(Color::Red, union([:unknown], [:enum, "user.Color"])).value + list = encode([5], union([:unknown], [:list, [:float]])) + assert_equal [:list, [:float]], Encode::Slot.from_wire(list.value_type) + assert_equal 5.0, list.list_value.values.first.float_value + assert_nil encode(nil, union([:int], [:null])).value + assert_equal :string_value, encode("text", union([:int], union([:float], union([:null], [:string])))).value + end + + def test_several_fits_collapse_when_identical_and_are_ambiguous_otherwise + assert_nil encode(nil, union(union([:int], [:null]), union([:string], [:null]))).value + assert_equal :int_value, encode(5, union([:literal, 5], [:int])).value + natural = encode([1], union([:list, union([:int], [:string])], [:list, [:int]])) + assert_equal [:list, [:int]], Encode::Slot.from_wire(natural.value_type) + error = assert_raises(TypeError) { encode([], union([:list, [:int]], [:list, [:string]])) } + assert_includes error.message, "fits more than one" + assert Encode::Slot.equivalent?(union([:int], [:string]), union([:string], [:int])) + end + + def test_distinct_mixed_scalar_lists_plan_in_linear_time + scalars = [1, 1.5, "s", true, nil] + subsets = (1...(1 << scalars.length)).map do |mask| + scalars.each_index.select { |index| mask[index] == 1 }.map { |index| scalars[index] } + end + value = subsets.combination(3).first(4_000) + tree = Encode::Read.new(@registry).read(value, "mixed") + planned = Timeout.timeout(10) { Encode::Plan.new(@registry).plan(tree, [:unknown]) } + assert_equal 4_000, planned.children.length + assert_equal :union, planned.ty[1][0] + assert_equal 4_000, planned.ty[1][1].length + end + + def test_canonical_union_order_does_not_depend_on_array_sharing + int = [:int] + left = union([:map, int, int], [:map, int, [:string]]) + right = union([:map, [:int], [:string]], [:map, [:int], [:int]]) + assert Encode::Slot.equivalent?(left, right) + assert_equal left, Encode::Slot.union([left, right]) + end + + def test_type_identity_recurses_through_every_composite_and_function_part + left = union([:int], union([:string], [:int])) + right = union([:string], [:int]) + wrap = ->(ty) { [:map, [:string], [:list, [:class, "user.Box", [ty]]]] } + signature = ->(ty) { [:function, [["value", wrap.call(ty), false]], wrap.call(ty), ty] } + a, b = signature.call(left), signature.call(right) + assert Encode::Slot.equivalent?(a, b) + assert_equal a, Encode::Slot.union([a, b]) + changed_name = signature.call(right) + changed_name[1][0][0] = "other" + refute Encode::Slot.equivalent?(a, changed_name) + changed_mode = signature.call(right) + changed_mode[1][0][2] = true + refute Encode::Slot.equivalent?(a, changed_mode) + refute Encode::Slot.equivalent?(a, [:function, [], wrap.call(right), right]) + refute Encode::Slot.equivalent?(a, [:function, b[1], [:int], right]) + refute Encode::Slot.equivalent?(a, [:function, b[1], b[2], [:never]]) + end + + def test_invalid_nonbinary_strings_are_refused_while_reading + invalid = "\xff".b.force_encoding(Encoding::UTF_8) + [invalid, { invalid => 1 }].each do |value| + error = assert_raises(Encode::Refusal) { Encode::Read.new(@registry).read(value, "invalid") } + assert_includes error.message, "invalid string" + end + assert_equal :string_value, encode("café").value + end + + def test_unstatable_types_are_bridge_faults_and_still_type_errors_for_calls + [[:other], [:handle], [:literal, 1.5]].each do |slot| + assert_raises(Encode::Fault) { Encode::Slot.to_wire(slot) } + end + Protocol.define_function("user.unstatable", params: [["items", [:list, [:other]]]]) + assert_raises(TypeError) { Protocol.encode_call(1, "user.unstatable", { "items" => [] }) } + end + + def test_open_slots_take_the_values_own_type_and_fill_holes_with_unknown + assert_equal [:list, [:unknown]], type_of([]) + assert_equal [:map, [:string], [:unknown]], type_of({}) + assert_equal [:list, union([:int], [:string])], type_of([1, "a"]) + result = encode([[], [1]]) + assert_equal [:list, [:list, [:int]]], Encode::Slot.from_wire(result.value_type) + assert_equal [:list, [:int]], Encode::Slot.from_wire(result.list_value.values.first.value_type) + assert_equal [:list, [:list, [:list, [:int]]]], type_of([[[]], [[1]]]) + assert_equal [:list, [:int]], type_of([1], [:interface, "user.Anything"]) + mixed = [:list, union([:list, [:int]], [:list, [:string]])] + assert_equal mixed, type_of([[1], ["a"]]) + assert_equal mixed, type_of([[], [1], ["a"]]) + assert_equal [:list, union([:map, [:string], [:int]], [:map, [:string], [:string]])], + type_of([{ "a" => 1 }, { "b" => "text" }]) + end + + def test_realized_containers_state_the_slot_not_the_narrower_elements + slot = [:list, union([:int], [:string])] + assert_equal slot, type_of([1, 2], slot) + assert_equal [:list, [:float]], type_of([], [:list, [:float]]) + map = encode({ "a" => 3 }, [:map, [:string], [:float]]) + assert_equal [:map, [:string], [:float]], Encode::Slot.from_wire(map.value_type) + assert_equal 3.0, map.map_value.entries.first.value.float_value + end + + def test_fixed_class_arguments_drive_fields_and_optional_fields_are_present + result = encode(Box.new(value: 3), [:class, "user.Box", [[:float]]]) + assert_equal [:class, "user.Box", [[:float]]], Encode::Slot.from_wire(result.value_type) + assert_equal %w[value note], result.class_value.fields.map(&:string_key) + assert_equal 3.0, result.class_value.fields.first.value.float_value + assert_nil result.class_value.fields.last.value.value + assert_raises(TypeError) { encode({ "value" => 3 }, [:class, "user.Box", [[:int]]]) } + end + + def test_generic_class_arguments_come_from_the_slot_the_value_or_the_fields + box = Box.new(value: 3) + assert_equal [:class, "user.Box", [[:int]]], type_of(box) + assert_empty Baml::Types.instance_args(box) + assert_equal [:class, "user.Box", [[:float]]], type_of(box, [:class, "user.Box", [[:float]]]) + Baml.bind_types(box, Integer) + assert_equal [:class, "user.Box", [[:int]]], type_of(box) + assert_raises(TypeError) { encode(box, [:class, "user.Box", [[:float]]]) } + assert_equal [:class, "user.Box", [[:int]]], type_of(Box.new(value: 3), union( + [:class, "user.Box", [[:int]]], [:class, "user.Box", [[:string]]] + )) + assert_raises(TypeError) do + encode(Box.new(value: []), union([:class, "user.Box", [[:list, [:int]]]], [:class, "user.Box", [[:list, [:string]]]])) + end + + param = [:param, 0, "T"] + pair = Class.new(Struct.new(:a, :b)) + @registry.define(pair, "user.Pair", { "a" => [:a, [:list, param]], "b" => [:b, param] }, type_params: ["T"]) + result = encode(pair.new([], 1)) + assert_equal [:class, "user.Pair", [[:int]]], Encode::Slot.from_wire(result.value_type) + assert_equal [:list, [:int]], Encode::Slot.from_wire(result.class_value.fields.first.value.value_type) + error = assert_raises(Encode::TypeArgConflict) { encode(pair.new([1], "x")) } + assert_includes error.message, "user.Pair" + assert_includes error.message, "T" + assert_raises(Encode::TypeArgConflict) { encode(pair.new(["x"], 1)) } + assert_raises(Encode::MissingTypeArgs) { encode(pair.new([], [])) } + @registry.define(pair, "user.Pair", { "a" => [:a, param], "b" => [:b, [:param, 1, "U"]] }, type_params: %w[T U]) + result = encode(pair.new("x", 1), [:class, "user.Pair", [param, [:float]]]) + assert_equal [:class, "user.Pair", [[:string], [:float]]], Encode::Slot.from_wire(result.value_type) + assert_equal 1.0, result.class_value.fields.last.value.float_value + phantom = Class.new(Struct.new(:value)) + @registry.define(phantom, "user.Phantom", { "value" => [:value, [:int]] }, type_params: ["T"]) + assert_raises(Encode::MissingTypeArgs) { encode(phantom.new(1)) } + end + + def test_fixed_literal_union_arm_does_not_contribute_parameter_evidence + param = [:param, 0, "T"] + klass = Class.new(Struct.new(:value, :status)) + @registry.define(klass, "user.LiteralStatus", { + "value" => [:value, param], "status" => [:status, union(param, [:literal, "none"])] + }, type_params: ["T"]) + [[:unknown], [:class, "user.LiteralStatus", [param]]].each do |slot| + assert_equal [:class, "user.LiteralStatus", [[:int]]], type_of(klass.new(1, "none"), slot) + assert_equal [:class, "user.LiteralStatus", [[:string]]], type_of(klass.new("a", "none"), slot) + assert_raises(Encode::TypeArgConflict) { encode(klass.new(1, "other"), slot) } + end + end + + def test_fixed_literal_union_arms_survive_list_and_map_inference + param = [:param, 0, "T"] + klass = Class.new(Struct.new(:value, :status)) + [[:list, union(param, [:literal, "none"])], + [:map, [:string], union(param, [:literal, "none"])]].each do |container| + @registry.define(klass, "user.ContainerStatus", { + "value" => [:value, param], "status" => [:status, container] + }, type_params: ["T"]) + wrap = ->(items) { container[0] == :list ? items : items.each_with_index.to_h { |item, i| [i.to_s, item] } } + [[1, ["none"], [:int]], [1, [2, "none"], [:int]], + ["a", ["none"], [:string]], [1, ["none", 2], [:int]]].each do |value, items, expected| + [[:unknown], [:class, "user.ContainerStatus", [param]]].each do |slot| + assert_equal [:class, "user.ContainerStatus", [expected]], type_of(klass.new(value, wrap.call(items)), slot) + end + end + assert_raises(Encode::TypeArgConflict) { encode(klass.new(1, wrap.call(["other"]))) } + @registry.define(klass, "user.ContainerStatus", { "status" => [:status, container] }, type_params: ["T"]) + assert_equal [:class, "user.ContainerStatus", [[:never]]], type_of(klass.new(nil, wrap.call(["none"]))) + end + end + + def test_fixed_integer_arm_alone_leaves_the_parameter_never_but_a_value_binds_it + param = [:param, 0, "T"] + klass = Class.new(Struct.new(:value, :status)) + status = [:status, union(param, [:literal, 1])] + @registry.define(klass, "user.IntegerStatus", { "status" => status }, type_params: ["T"]) + assert_equal [:class, "user.IntegerStatus", [[:never]]], type_of(klass.new(nil, 1)) + @registry.define(klass, "user.IntegerStatus", { "status" => status, "value" => [:value, param] }, type_params: ["T"]) + assert_equal [:class, "user.IntegerStatus", [[:int]]], type_of(klass.new(1, 1)) + @registry.define(klass, "user.IntegerStatus", { + "status" => [:status, union(param, [:param, 1, "U"]) ] + }, type_params: %w[T U]) + assert_raises(Encode::MissingTypeArgs) { encode(klass.new(nil, 1)) } + end + + def test_fixed_enum_variant_arms_do_not_learn_the_widened_enum_type + param = [:param, 0, "T"] + status = union(param, [:enum_variant, "user.Color", "red"]) + klass = Class.new(Struct.new(:value, :status)) + [[status, ->(value) { value }], + [[:list, status], ->(value) { [value] }], + [[:map, [:string], status], ->(value) { { "status" => value } }]].each do |slot, wrap| + @registry.define(klass, "user.EnumStatus", { + "value" => [:value, param], "status" => [:status, slot] + }, type_params: ["T"]) + [[1, [:int]], [Color::Blue, [:enum, "user.Color"]]].each do |value, expected| + result = encode(klass.new(value, wrap.call(Color::Red))) + assert_equal [:class, "user.EnumStatus", [expected]], Encode::Slot.from_wire(result.value_type) + bound = Baml.bind_types(klass.new(value, wrap.call(Color::Red)), expected) + assert_equal encode(bound), result + end + assert_raises(Encode::TypeArgConflict) { encode(klass.new(1, wrap.call(Color::Blue))) } + end + end + + def test_equivalent_fixed_and_open_arms_keep_the_existing_wire_output + param = [:param, 0, "T"] + slot = [:list, union([:list, param], [:list, [:int]])] + expected = [:list, union([:list, [:never]], [:list, [:int]])] + result = encode([[1]], slot) + assert_equal expected, Encode::Slot.from_wire(result.value_type) + assert_equal [:list, [:int]], Encode::Slot.from_wire(result.list_value.values.first.value_type) + assert_equal encode([[1]], expected), result + end + + def test_empty_and_nonempty_fixed_containers_are_absorbed_in_lists_and_maps + param = [:param, 0, "T"] + [[[:list, [:int]], [], [1]], + [[:map, [:string], [:int]], {}, { "n" => 1 }]].each do |fixed, empty, full| + member = union(param, fixed) + [[[:list, member], ->(items) { items }], + [[:map, [:string], member], ->(items) { items.each_with_index.to_h { |item, i| [i.to_s, item] } }]].each do |slot, wrap| + [[empty], [full], [empty, full]].each do |items| + expected = Encode::Slot.substitute(slot, [[:never]]) + assert_equal expected, type_of(wrap.call(items), slot) + assert_equal encode(wrap.call(items), expected), encode(wrap.call(items), slot) + end + [[empty, "a"], ["a", full]].each do |items| + assert_equal Encode::Slot.substitute(slot, [[:string]]), type_of(wrap.call(items), slot) + end + assert_raises(Encode::UnboundTypeArgs) { encode(wrap.call([]), slot) } + end + end + end + + def test_fixed_container_fields_do_not_conflict_with_other_parameter_evidence + param = [:param, 0, "T"] + klass = Class.new(Struct.new(:value, :status)) + [[[:list, [:int]], []], [[:map, [:string], [:int]], {}], + [[:list, [:literal, "none"]], ["none"]], + [[:map, [:string], [:literal, "none"]], { "status" => "none" }]].each do |fixed, status| + fields = { "status" => [:status, union(param, fixed)] } + @registry.define(klass, "user.FixedContainer", fields, type_params: ["T"]) + assert_equal [:class, "user.FixedContainer", [[:never]]], type_of(klass.new(nil, status)) + @registry.define(klass, "user.FixedContainer", fields.merge("value" => [:value, param]), type_params: ["T"]) + result = encode(klass.new(1, status)) + assert_equal [:class, "user.FixedContainer", [[:int]]], Encode::Slot.from_wire(result.value_type) + assert_equal encode(Baml.bind_types(klass.new(1, status), Integer)), result + end + end + + def test_fixed_arm_choices_survive_recursive_container_learning + param = [:param, 0, "T"] + klass = Class.new(Struct.new(:value, :status)) + status = [:list, [:map, [:string], [:list, union(param, [:enum_variant, "user.Color", "red"])]]] + @registry.define(klass, "user.NestedStatus", { + "value" => [:value, param], "status" => [:status, status] + }, type_params: ["T"]) + [[{ "red" => [Color::Red] }], + [{ "red" => [Color::Red] }, { "int" => [2] }], + [{ "int" => [2] }, { "red" => [Color::Red] }]].each do |items| + result = encode(klass.new(1, items)) + assert_equal [:class, "user.NestedStatus", [[:int]]], Encode::Slot.from_wire(result.value_type) + assert_equal encode(Baml.bind_types(klass.new(1, items), Integer)), result + end + @registry.define(klass, "user.NestedStatus", { "status" => [:status, status] }, type_params: ["T"]) + assert_equal [:class, "user.NestedStatus", [[:never]]], type_of(klass.new(nil, [{ "red" => [Color::Red] }])) + end + + def test_fixed_arm_choices_survive_nested_class_argument_learning + param = [:param, 0, "T"] + inner = Class.new(Struct.new(:status)) + outer = Class.new(Struct.new(:value, :inner)) + @registry.define(inner, "user.InnerStatus", { + "status" => [:status, union(param, [:enum_variant, "user.Color", "red"])] + }, type_params: ["T"]) + @registry.define(outer, "user.OuterStatus", { + "value" => [:value, param], "inner" => [:inner, [:class, "user.InnerStatus", [param]]] + }, type_params: ["T"]) + result = encode(outer.new(1, inner.new(Color::Red))) + assert_equal [:class, "user.OuterStatus", [[:int]]], Encode::Slot.from_wire(result.value_type) + assert_equal encode(Baml.bind_types(outer.new(1, inner.new(Color::Red)), Integer)), result + assert_raises(Encode::TypeArgConflict) { encode(outer.new(1, inner.new(Color::Blue))) } + nested = [:class, "user.InnerStatus", [[:never]]] + assert_equal [:class, "user.OuterStatus", [nested]], type_of(outer.new(inner.new(Color::Red), inner.new(Color::Red))) + + member = [:class, "user.InnerStatus", [param]] + [[[:list, member], ->(items) { items }], + [[:map, [:string], member], ->(items) { items.each_with_index.to_h { |item, i| [i.to_s, item] } }]].each do |slot, wrap| + [ [Color::Red, Color::Blue], [Color::Blue, Color::Red] ].each do |colors| + values = wrap.call(colors.map { |color| inner.new(color) }) + expected = Encode::Slot.substitute(slot, [[:enum, "user.Color"]]) + assert_equal expected, type_of(values, slot) + assert_equal encode(values, expected), encode(values, slot) + end + end + end + + def test_fixed_unions_in_nested_class_arguments_do_not_bind_the_parent_parameter + param = [:param, 0, "T"] + klass = Class.new(Struct.new(:value, :status)) + [[[:int], 1, "a", [:string]], [[:string], "s", 1, [:int]], + [[:list, [:int]], [1], "a", [:string]], + [[:map, [:string], [:int]], { "n" => 1 }, "a", [:string]]].each do |fixed, status, value, expected| + field = [:status, [:class, "user.Box", [union(param, fixed)]]] + fields = { "value" => [:value, param], "status" => field } + [fields, fields.to_a.reverse.to_h].each do |ordered| + @registry.define(klass, "user.NestedArgument", ordered, type_params: ["T"]) + result = encode(klass.new(value, Box.new(value: status))) + assert_equal [:class, "user.NestedArgument", [expected]], Encode::Slot.from_wire(result.value_type) + assert_equal encode(Baml.bind_types(klass.new(value, Box.new(value: status)), expected)), result + end + @registry.define(klass, "user.NestedArgument", { "status" => field }, type_params: ["T"]) + result = encode(klass.new(nil, Box.new(value: status))) + assert_equal [:class, "user.NestedArgument", [[:never]]], Encode::Slot.from_wire(result.value_type) + assert_equal encode(Baml.bind_types(klass.new(nil, Box.new(value: status)), [:never])), result + end + end + + def test_fixed_unions_nested_inside_class_argument_containers_are_absorbed + param = [:param, 0, "T"] + klass = Class.new(Struct.new(:value, :status)) + [[[:list, union(param, [:int])], [1]], + [[:map, [:string], union(param, [:int])], { "n" => 1 }], + [union([:list, param], [:list, [:int]]), [1]]].each do |argument, status| + @registry.define(klass, "user.NestedArgumentContainer", { + "value" => [:value, param], "status" => [:status, [:class, "user.Box", [argument]]] + }, type_params: ["T"]) + result = encode(klass.new("a", Box.new(value: status))) + assert_equal [:class, "user.NestedArgumentContainer", [[:string]]], Encode::Slot.from_wire(result.value_type) + assert_equal encode(Baml.bind_types(klass.new("a", Box.new(value: status)), String)), result + assert_raises(Encode::Refusal) { encode(klass.new("a", Box.new(value: true))) } + end + end + + def test_absorbed_evidence_beats_empty_list_and_map_siblings_in_either_order + param = [:param, 0, "T"] + red = [:enum_variant, "user.Color", "red"] + [[[:list, [:list, union(param, red)]], [[], [Color::Red]], [[], [Color::Red], [1]]], + [[:map, [:string], [:map, [:string], union(param, red)]], + [{}, { "r" => Color::Red }], [{}, { "r" => Color::Red }, { "n" => 1 }]]].each do |slot, absorbed, concrete| + wrap = ->(items) { slot[0] == :list ? items : items.each_with_index.to_h { |item, i| [i.to_s, item] } } + [[absorbed, [:never]], [concrete, [:int]]].each do |items, binding| + [items, items.reverse].each do |ordered| + expected = Encode::Slot.substitute(slot, [binding]) + result = encode(wrap.call(ordered), slot) + assert_equal expected, Encode::Slot.from_wire(result.value_type) + assert_equal encode(wrap.call(ordered), expected), result + end + end + end + end + + def test_absorbed_evidence_beats_empty_class_fields_in_either_order + param = [:param, 0, "T"] + klass = Class.new(Struct.new(:a, :b)) + red = union(param, [:enum_variant, "user.Color", "red"]) + [[[:list, red], [], [Color::Red]], + [[:map, [:string], red], {}, { "r" => Color::Red }]].each do |slot, empty, absorbed| + fields = { "a" => [:a, slot], "b" => [:b, slot] } + [fields, fields.to_a.reverse.to_h].each do |ordered| + @registry.define(klass, "user.EmptyAndAbsorbed", ordered, type_params: ["T"]) + [[empty, absorbed], [absorbed, empty]].each do |values| + result = encode(klass.new(*values)) + assert_equal [:class, "user.EmptyAndAbsorbed", [[:never]]], Encode::Slot.from_wire(result.value_type) + assert_equal encode(Baml.bind_types(klass.new(*values), [:never])), result + end + assert_raises(Encode::MissingTypeArgs) { encode(klass.new(empty, empty)) } + end + end + end + + def test_a_fixed_arm_does_not_determine_two_open_union_arms + t, u = [:param, 0, "T"], [:param, 1, "U"] + klass = Class.new(Struct.new(:status)) + @registry.define(klass, "user.TwoOpen", { + "status" => [:status, union(t, u, [:enum_variant, "user.Color", "red"])] + }, type_params: %w[T U]) + assert_raises(Encode::MissingTypeArgs) { encode(klass.new(Color::Red)) } + assert_raises(Encode::UnboundTypeArgs) { encode([Color::Red], [:list, union(t, u, [:enum_variant, "user.Color", "red"])]) } + end + + def test_fixed_arm_inference_shares_the_budget_and_scales_with_the_elements + param = [:param, 0, "T"] + slot = [:list, [:map, [:string], union(param, [:enum_variant, "user.Color", "red"])]] + tree = Encode::Read.new(@registry).read(Array.new(2_000) { { "status" => Color::Red } }, "fixed") + assert_raises(Encode::Fault) { Encode::Plan.new(@registry, budget: 100).plan_root(tree, slot) } + planned = Timeout.timeout(5) { Encode::Plan.new(@registry, budget: 100_000).plan_root(tree, slot) } + assert_equal 2_000, planned.children.length + assert_equal Encode::Slot.substitute(slot, [[:never]]), planned.ty + end + + def test_stored_bindings_match_normalized_decoded_bindings + klass = Class.new(T::Struct) do + include Baml::ValueEquality + const :value, T.nilable(Integer) + end + fields = { "value" => [:value, [:param, 0, "T"]] } + @registry.define(klass, "user.NormalizedBox", fields, type_params: ["T"]) + Protocol.define_class("user.NormalizedBox", klass, fields: fields, type_params: ["T"]) + [Baml::Types.optional(Baml::Types.optional(Integer)), union([:int], [:int], [:null], [:null])].each do |binding| + original = Baml.bind_types(klass.new(value: 1), binding) + encoded = encode(original) + outbound = Wire::BamlOutboundValue.new(class_value: { + name: "user.NormalizedBox", type_args: encoded.value_type.class_ty.type_args.to_a, + fields: [{ key: "value", value: { int_value: 1 } }] + }) + decoded = Protocol.decode_value(Wire::BamlOutboundValue.decode(Wire::BamlOutboundValue.encode(outbound))) + assert_equal original, decoded + assert original.eql?(decoded) + assert_equal original.hash, decoded.hash + assert_equal [union([:int], [:null])], Baml::Types.instance_args(original) + end + end + + def test_alias_and_inline_field_templates_infer_the_same_never_argument + param = [:param, 0, "T"] + @registry.define_alias("user.Name", [:string]) + [[:alias, "user.Name"], [:string]].each do |known| + box = Class.new(Struct.new(:value)) + @registry.define(box, "user.AliasBox", { "value" => [:value, union(param, known)] }, type_params: ["T"]) + assert_equal [:class, "user.AliasBox", [[:never]]], type_of(box.new("hi")) + end + end + + def test_a_solved_element_slot_is_normalized_once_not_for_every_child + param = [:param, 0, "T"] + value = (1..30).map { |size| [Array.new(size, 1), ["a"]] } + tree = Encode::Read.new(@registry).read(value, "solved") + normalizations = Hash.new(0) + original = Encode::Slot.method(:stated_form) + Encode::Slot.stub(:stated_form, ->(slot, *args, **kwargs, &work) { normalizations[slot.object_id] += 1; original.call(slot, *args, **kwargs, &work) }) do + planned = Encode::Plan.new(@registry).plan_root(tree, [:list, param]) + assert_equal 30, planned.children.length + end + assert_equal 1, normalizations.values.max + end + + def test_a_known_sibling_taking_the_value_leaves_the_variable_never + param = [:param, 0, "T"] + slot = [:list, union(param, [:null])] + opt = Class.new(Struct.new(:items)) + @registry.define(opt, "user.Opt", { "items" => [:items, slot] }, type_params: ["T"]) + assert_equal [:class, "user.Opt", [[:never]]], type_of(opt.new([nil])) + [ [nil, 1], [1, nil] ].each do |items| + result = encode(opt.new(items)) + assert_equal [:class, "user.Opt", [[:int]]], Encode::Slot.from_wire(result.value_type) + assert_equal [:list, union([:int], [:null])], Encode::Slot.from_wire(result.class_value.fields.first.value.value_type) + end + assert_equal [:list, union([:never], [:null])], type_of([nil], slot) + assert_raises(Encode::MissingTypeArgs) { encode(opt.new([])) } + assert_equal [:list, union([:class, "user.Box", [[:int]]], [:null])], type_of( + [Box.new(value: 1), nil], [:list, union([:class, "user.Box", [param]], [:null])] + ) + end + + def test_a_handle_nested_under_an_open_variable_is_not_typed_by_a_sibling + param = [:param, 0, "T"] + slot = [:list, [:list, param]] + api = Api.new + handle = Baml::Handle.new(api, 41, :ADT_TAGGED_HEAP_HANDLE) + scope = Baml::Bridge::EncodingScope.new + [[[handle], [1]], [[1], [handle]]].each do |value| + error = assert_raises(Encode::UnboundTypeArgs) { encode(value, slot, scope: scope) } + assert_includes error.message, "could not infer a type" + end + assert_empty api.cloned + assert_equal [:list, [:list, [:int]]], type_of([[1], [2]], slot) + assert_equal [:list, [:list, [:int]]], type_of([[], [1]], slot) + [ [[], param], [[], [:list, param]], [{}, [:map, [:string], param]] ].each do |value, open| + assert_raises(Encode::UnboundTypeArgs) { encode(value, open) } + end + assert_equal [:map, [:string], [:int]], type_of({ "x" => 1 }, [:map, param, [:int]]) + boxes = [Baml.bind_types(Box.new(value: 1), Integer), Baml.bind_types(Box.new(value: "x"), String)] + assert_raises(Encode::UnboundTypeArgs) { encode(boxes, [:list, [:class, "user.Box", [param]]]) } + assert_equal 41, encode(handle, param, scope: scope).handle.key + ensure + scope&.close + handle&.close + end + + def test_arguments_are_keyed_by_declared_name_and_type_args_substitute + param = [:param, 0, "T"] + Protocol.define_function("user.bound_args", params: [["a", [:int]], ["p", [:list, param]]], type_params: ["T"]) + types = Protocol.prepare_type_args("user.bound_args", {}, { T: String }) + result = Wire::CallFunctionArgs.decode(Protocol.encode_call(1, "user.bound_args", { "p" => ["s"], "a" => 1 }, type_args: types)) + assert_equal %w[p a], result.kwargs.map(&:string_key) + assert_equal [:list, [:string]], Encode::Slot.from_wire(result.kwargs.first.value.value_type) + error = assert_raises(TypeError) { Protocol.encode_call(1, "user.bound_args", { "p" => [1] }, type_args: types) } + assert_includes error.message, "user.bound_args: p[0]" + [{ Wrong: Integer }, { "T" => Integer }, []].each do |types| + assert_raises(TypeError) { Protocol.prepare_type_args("user.bound_args", {}, types) } + end + + Protocol.define_class("user.Receiver", Box, fields: { "value" => [:value, param], "note" => [:note, [:null]] }, type_params: ["T"]) + Protocol.define_function("user.Receiver.method", params: [["self", [:class, "user.Receiver", [param]]], ["p", [:list, param]], + ["other", [:param, 1, "U"]]], class_type_params: ["T"], type_params: ["U"]) + receiver = Baml.bind_types(Box.new(value: 1), Float) + arguments = { "self" => receiver, "p" => [2], "other" => "s" } + types = Protocol.prepare_type_args("user.Receiver.method", arguments, { U: String }) + assert_equal %w[T U], types.map(&:type_var) + result = Wire::CallFunctionArgs.decode(Protocol.encode_call(1, "user.Receiver.method", arguments, type_args: types)) + assert_equal 1.0, result.kwargs.first.value.class_value.fields.first.value.float_value + assert_equal [:list, [:float]], Encode::Slot.from_wire(result.kwargs[1].value.value_type) + signature = [:function, [["x", param, false]], [:list, param], [:param, 1, "U"]] + assert_equal [:function, [["x", [:float], false]], [:list, [:float]], [:string]], + Encode::Slot.substitute(signature, { "T" => [:float], "U" => [:string] }, indexed: false) + end + + def test_type_tokens_and_instance_arguments_are_immutable_descriptors + type = Baml::Types.generic(Box, Baml::Types.optional(Baml::Types.list(Integer))) + assert_equal [:class, "user.Box", [[:union, [[:list, [:int]], [:null]]]]], type + assert type.frozen? + assert type[2].first[1].first[1].frozen? + assert_raises(TypeError) { Baml::Types.generic(Box) } + assert_raises(TypeError) { Baml::Types.token(Box) } + value = Box.new(value: []) + assert_same value, Baml.bind_types(value, type) + assert_equal [type], Baml::Types.instance_args(value) + assert Baml::Types.instance_args(value).frozen? + end + + def test_map_keys_are_strings_and_errors_carry_the_entry + [1, true, :symbol, Color::Red].each do |key| + error = assert_raises(TypeError) { encode({ key => 1 }) } + assert_includes error.message, "argument[key]" + assert_includes error.message, "map keys must be string" + end + error = assert_raises(TypeError) { encode({ "bad" => "not int" }, [:map, [:string], [:int]]) } + assert_includes error.message, 'argument["bad"]' + assert_raises(TypeError) { encode({ "red" => 1 }, [:map, [:enum, "user.Color"], [:int]]) } + end + + def test_outbound_map_keys_keep_their_existing_decode_types + [["7", [:int], 7], ["18446744073709551616", [:bigint], 2**64], + ["true", [:bool], true], ["false", [:literal, false], false], + ["42", [:literal, :bigint, 42], 42], ["text", [:literal, "text"], "text"]].each do |key, ty, expected| + assert_equal expected, Protocol.decode_map_key(key, Encode::Slot.to_wire(ty)) + end + end + + def test_a_value_the_wire_cannot_carry_is_refused_before_anything_is_created + api = Api.new + handle = Baml::Handle.new(api, 41, :ADT_TAGGED_HEAP_HANDLE) + scope = Baml::Bridge::EncodingScope.new + nested = 34.times.reduce(handle) { |value, _| { "nested" => value } } + error = assert_raises(TypeError) { encode(nested, scope: scope) } + assert_includes error.message, "nested too deeply" + assert_empty api.cloned + assert_raises(TypeError) { encode([handle], union([:list, [:int]], [:list, [:string]]), scope: scope) } + assert_empty api.cloned + result = encode([1, handle], scope: scope) + assert_equal [:list, [:unknown]], Encode::Slot.from_wire(result.value_type) + assert_equal [41], api.cloned + cyclic = [] + cyclic << cyclic + assert_raises(TypeError) { encode(cyclic) } + handle.close + ensure + scope&.close + end + + def test_an_endless_container_is_refused_at_the_node_budget + endless = Class.new(Array) do + def each + loop { yield 1 } + end + end.new + reader = Encode::Read.new(@registry, budget: 16) + error = assert_raises(TypeError) { reader.read(endless, "endless") } + assert_includes error.message, "node budget" + assert_equal 1 << 20, Encode::Read::NODE_BUDGET + reader = Encode::Read.new(@registry, budget: 2) + reader.read(1, "first") + assert_raises(TypeError) { reader.read([1], "second") } + end + + def test_planning_trials_share_one_budget_even_for_recursive_class_unions + left = Class.new(Struct.new(:child)) + right = Class.new(Struct.new(:child)) + a = [:class, "user.Left", [[:int]]] + b = [:class, "user.Left", [[:unknown]]] + c = [:class, "user.Right", [[:int]]] + d = [:class, "user.Right", [[:unknown]]] + @registry.define(left, "user.Left", { "child" => [:child, union([:null], c, d)] }, type_params: ["T"]) + @registry.define(right, "user.Right", { "child" => [:child, union([:null], a, b)] }, type_params: ["T"]) + value = 8.times.reduce(nil) { |child, index| (index.even? ? right : left).new(child) } + tree = Encode::Read.new(@registry).read(value, "recursive") + error = assert_raises(TypeError) { Encode::Plan.new(@registry, budget: 100).plan(tree, union(a, b)) } + assert_includes error.message, "planning-step budget" + assert_equal 1 << 20, Encode::Plan::STEP_BUDGET + assert_raises(TypeError) { encode(1, [:alias, "unsupported.cycle"]) } + end + + def test_reading_happens_once_and_host_exceptions_are_not_reclassified + reads = 0 + type = Class.new do + define_method(:value) { reads += 1; 1 } + end + @registry.define(type, "user.ReadOnce", { "value" => [:value, [:param, 0, "T"]] }, type_params: ["T"]) + slot = union([:class, "user.ReadOnce", [[:int]]], [:class, "user.ReadOnce", [[:string]]]) + assert_equal :class_value, encode(type.new, slot).value + assert_equal 1, reads + type.define_method(:value) { raise "getter failed" } + error = assert_raises(RuntimeError) { encode(type.new, slot) } + assert_equal "getter failed", error.message + end + + def test_encode_call_plans_and_checks_every_argument_before_cloning + api = Api.new + handle = Baml::Handle.new(api, 41, :ADT_TAGGED_HEAP_HANDLE) + scope = Baml::Bridge::EncodingScope.new + Protocol.define_function("user.encode_test", params: [["bad", [:int]], ["value", [:float]]]) + error = assert_raises(TypeError) do + Protocol.encode_call(1, "user.encode_test", { "handle" => handle, "bad" => "wrong" }, scope: scope) + end + assert_match(/\Auser.encode_test: bad:/, error.message) + assert_empty api.cloned + nested = 34.times.reduce(1) { |value, _| { "nested" => value } } + assert_raises(TypeError) do + Protocol.encode_call(1, "user.encode_test", { "handle" => handle, "deep" => nested }, scope: scope) + end + assert_empty api.cloned + args = Wire::CallFunctionArgs.decode(Protocol.encode_call(2, "user.encode_test", { "value" => 5, "handle" => handle }, scope: scope)) + assert_equal 5.0, args.kwargs.first.value.float_value + assert_equal [41], api.cloned + scope.close + assert_equal [41], api.released + handle.close + end + + def test_unset_arguments_are_omitted_before_reading_and_nil_is_explicit + Protocol.define_function("user.encode_defaults", params: [["omitted", [:int]], ["explicit", [:union, [[:int], [:null]]]]]) + args = Wire::CallFunctionArgs.decode(Protocol.encode_call(1, "user.encode_defaults", { + "omitted" => Baml::UNSET, "explicit" => nil, "undeclared" => Baml::UNSET + })) + assert_equal ["explicit"], args.kwargs.map(&:string_key) + assert_nil args.kwargs.first.value.value + end + + def test_invocation_cancel_uses_the_walker_and_keeps_the_class_check + token_type = Class.new(Struct.new(:handle)) + Protocol.define_class("baml.spawn.CancelToken", token_type, fields: { "handle" => [:handle, [:other]] }) + api = Api.new + handle = Baml::Handle.new(api, 41, :ADT_TAGGED_HEAP_HANDLE) + scope = Baml::Bridge::EncodingScope.new + invocation = Protocol.encode_invocation(1, Baml::Options.new(cancel: token_type.new(handle)), api, scope: scope) + assert_equal [:class, "baml.spawn.CancelToken", []], Encode::Slot.from_wire(invocation.cancel.value_type) + assert_equal 41, invocation.cancel.class_value.fields.first.value.handle.key + assert_equal [41], api.cloned + error = assert_raises(TypeError) do + Protocol.encode_invocation(1, Baml::Options.new(cancel: handle), api, scope: scope) + end + assert_includes error.message, "cancel must be a generated baml.spawn.CancelToken" + error = assert_raises(TypeError) do + Protocol.encode_invocation(1, Baml::Options.new(cancel: token_type.new(Object.new)), api, scope: scope) + end + assert_includes error.message, "_baml.cancel.handle" + assert_equal [41], api.cloned + scope.close + assert_equal [41], api.released + ensure + scope&.close + handle&.close + end + + def test_complete_host_call_uses_the_abi5_signature + native = Baml::Bridge.const_get(:Native, false) + received = [] + callback = FFI::Function.new(:void, %i[uint32 pointer size_t]) do |id, pointer, length| + received << [id, length.zero? ? "".b : pointer.read_bytes(length)] + end + table = native::ApiV1.new + table[:complete_host_call] = callback + api = native::Api.allocate + api.instance_variable_set(:@owner_pid, Process.pid) + api.instance_variable_set(:@functions, {}) + api.instance_variable_set(:@table, table) + completion = Wire::HostCallCompletion.encode(Wire::HostCallCompletion.new(returned: { int_value: 5 })) + api.complete_host_call(7, completion) + api.complete_host_call(8, "".b) + assert_equal [[7, completion], [8, "".b]], received + end + + def test_type_descriptors_round_trip_and_refuse_nonstatic_heads + slots = [[:null], [:never], [:unknown], [:enum, "user.Color"], [:interface, "user.I"], + [:bigint], [:bytes], [:media, :image], [:media, :generic], [:literal, :bigint, 42], + [:enum_variant, "user.Color", "red"], [:alias, "user.Rec"], + [:literal, 3], [:literal, true], [:literal, "x"], [:class, "user.Box", [[:int]]], + [:map, [:string], [:list, union([:int], [:null])]]] + slots.each { |slot| assert_equal slot, Encode::Slot.from_wire(Encode::Slot.to_wire(slot)) } + %i[class_ty enum interface enum_variant type_alias].each do |kind| + klass = Wire.const_get({ class_ty: :BamlTyClass, enum: :BamlTyEnum, interface: :BamlTyInterface, + enum_variant: :BamlTyEnumVariant, type_alias: :BamlTyTypeAlias }.fetch(kind)) + [:live, :anonymous_label].each do |head| + ty = Wire::BamlTy.new(**{ kind => klass.new(**{ head => head == :live ? 41 : "display" }) }) + assert_equal [:other], Encode::Slot.from_wire(ty) + assert_raises(TypeError) { Encode::Slot.to_wire(Encode::Slot.from_wire(ty)) } + end + end + end + + def test_wire_depth_counts_statements_entries_and_the_argument_envelope + assert_equal 0, Encode::Slot.wire_depth(encode(1)) + assert_equal 4, Encode::Slot.wire_depth(encode([], [:list, [:int]])) + assert_equal 4, Encode::Slot.wire_depth(encode({ "x" => 1 })) + assert_equal 1, Encode::Slot.wire_depth(encode(Color::Red)) + nested = 48.times.reduce(1) { |value, _| [value] } + assert_equal 98, Encode::Slot.wire_depth(encode(nested)) + assert_raises(TypeError) { encode([nested]) } + end + + private + + def complete(callback, signature) + scope = Baml::Bridge::EncodingScope.new + key = Baml::Bridge::HostValues.register(callback, scope, signature) + scope.submitted! + api = Api.new + frame = Wire::HostInvocation.new(host_value_key: key, callback_id: 7, + application_args: Wire::BamlToHostCall.encode(Wire::BamlToHostCall.new)) + Baml::Bridge::HostCallbacks.new(api).send(:invoke, frame) + id, completion = api.completions.fetch(0) + assert_equal 7, id + assert_equal 1, api.completions.length + @host_keys << completion.raised.handle if completion.outcome == :raised + completion + ensure + Baml::Bridge::HostValues.handle_release(key) if key + scope&.close + end + + def union(*members) + [:union, members] + end + + def encode(value, slot = [:unknown], scope: nil) + tree = Encode::Read.new(@registry).read(value, "argument") + plan = Encode::Plan.new(@registry).plan_root(tree, slot) + Encode::Commit.check_depth!(Encode::Commit.depth(plan), "argument") + result = Encode::Commit.commit(plan, scope: scope) + assert_equal Encode::Commit.depth(plan), Encode::Slot.wire_depth(result) + result + end + + def type_of(value, slot = [:unknown]) + Encode::Slot.from_wire(encode(value, slot).value_type) + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/test/bridge_loader_test.rb b/baml_language/sdk_tests/crates/ruby_sorbet/test/bridge_loader_test.rb index 22aae85db9a..1c438453fbd 100644 --- a/baml_language/sdk_tests/crates/ruby_sorbet/test/bridge_loader_test.rb +++ b/baml_language/sdk_tests/crates/ruby_sorbet/test/bridge_loader_test.rb @@ -3,88 +3,108 @@ require "minitest/autorun" require "open3" require "rbconfig" -require_relative "../../../../sdks/ruby/bridge_ruby/lib/baml/bridge" +require "timeout" +require "fileutils" +require "tmpdir" +$LOAD_PATH.unshift File.expand_path("../../../../sdks/ruby/bridge_ruby/lib", __dir__) +require "baml/bridge" class BridgeLoaderTest < Minitest::Test - REQUIRED_FUNCTION_FIELDS = Baml::Bridge - .const_get(:Native, false) - .const_get(:ApiV1, false) - .const_get(:FUNCTION_FIELDS, false) - .map(&:to_s) - .freeze - - TERMINAL_MODES = %w[ - null-table - wrong-abi - truncated - version-mismatch - invalid-version-utf8 - null-version-pointer - registration-rejected - ].freeze - - def test_configuration_and_open_failures_can_retry - run_scenario("configuration_retry") - run_scenario("open_retry") - run_scenario("concurrent_open_failure_preserves_claim") + def test_truncated_table + run_scenario("truncated") end - def test_opened_incompatible_libraries_are_terminal - TERMINAL_MODES.each do |mode| - run_scenario("terminal_mode", mode) - end - run_scenario("terminal_missing_getter") + def test_null_function_slot + run_scenario("null_function") end - def test_every_required_v1_function_is_validated - REQUIRED_FUNCTION_FIELDS.each do |field| - run_scenario("terminal_null_field", field) - end + def test_pid_check + run_scenario("pid_check") end - def test_invalid_bytecode_can_retry_and_ready_program_uses_exact_bytes - run_scenario("invalid_bytecode_retry_and_identity") + def test_different_program + run_scenario("different_program") end - def test_concurrent_initialization_calls_native_once - run_scenario("concurrent_initialization") + def test_abandoned_call_is_released + run_scenario("abandoned_call") end - def test_fork_policy - run_scenario("fork_before_native_use") - run_scenario("fork_after_native_use") - run_scenario("fork_during_initialization") - run_scenario("fork_during_open_failure") - end - - def test_process_runtime_retains_registered_callback_across_gc - run_scenario("registered_callback_retention") - end + def test_fork_during_native_open_rejects_before_touching_the_inherited_mutex + skip "fork is unavailable" unless Process.respond_to?(:fork) - def test_foreign_native_thread_callback_copies_bytes_and_contains_exceptions - run_scenario("foreign_thread_callback") - end + runtime = Baml::Bridge.const_get(:ProcessRuntime, false).new + runtime.register_program("bytecode") + runtime.define_singleton_method(:configured_runtime_path!) { "fake-runtime" } + runtime.define_singleton_method(:open_library) do |*| + child = fork do + begin + Timeout.timeout(2) { runtime.initialize! } + exit! 1 + rescue Baml::Bridge::ForkSafetyError + exit! 0 + end + end + _, status = Process.wait2(child) + raise "child touched inherited loader state" unless status.success? + raise LoadError, "test open failed" + end + assert_raises(Baml::Bridge::RuntimeLoadError) { runtime.initialize! } + assert_equal false, runtime.instance_variable_get(:@native_load_in_progress) - def test_real_bridge_initializes_real_bytecode - run_scenario("real_runtime_initialization") + # A completed failed open left no native state, so a child may retry. + runtime.define_singleton_method(:open_library) { |*| raise LoadError, "retry reached open" } + child = fork do + begin + runtime.initialize! + exit! 1 + rescue Baml::Bridge::RuntimeLoadError => error + exit!(error.message.include?("retry reached open") ? 0 : 1) + end + end + _, status = Process.wait2(child) + assert status.success?, "failed native open must permit a child retry" end - def test_initialize_requires_byte_string - run_scenario("type_validation") + def test_cache_targets_match_the_shared_bridge_layout + targets = [ + ["x86_64", "linux", "x86_64-linux", "x86_64-unknown-linux-gnu", "libbridge_cffi.so"], + ["aarch64", "linux", "aarch64-linux-musl", "aarch64-unknown-linux-musl", "libbridge_cffi.so"], + ["x86_64", "linux-musl", "x86_64-linux", "x86_64-unknown-linux-musl", "libbridge_cffi.so"], + ["aarch64", "linux", "aarch64-linux", "aarch64-unknown-linux-gnu", "libbridge_cffi.so"], + ["x86_64", "darwin25", "x86_64-darwin25", "x86_64-apple-darwin", "libbridge_cffi.dylib"], + ["arm64", "darwin25", "arm64-darwin25", "aarch64-apple-darwin", "libbridge_cffi.dylib"], + ["x64", "mingw32", "x64-mingw-ucrt", "x86_64-pc-windows-msvc", "bridge_cffi.dll"], + ["arm64", "mswin64", "arm64-mswin64", "aarch64-pc-windows-msvc", "bridge_cffi.dll"] + ] + with_cache do |runtime, home| + targets.each do |cpu, os, arch, target, filename| + RbConfig::CONFIG.stub(:values_at, [cpu, os, arch]) do + assert_equal File.join(home, "bridges", Baml::Bridge::TOOLCHAIN_VERSION, target, filename), + runtime.send(:cached_runtime_path).to_s + end + end + end end private - def run_scenario(name, *arguments) + def with_cache + original = ENV.to_h.slice("BAML_HOME", "BAML_BRIDGE_PATH") + Dir.mktmpdir("baml-loader-") do |home| + ENV["BAML_HOME"] = home + ENV.delete("BAML_BRIDGE_PATH") + yield Baml::Bridge.const_get(:ProcessRuntime, false).new, home + end + ensure + %w[BAML_HOME BAML_BRIDGE_PATH].each { |key| ENV[key] = original[key] } + end + + def run_scenario(name) scenario = File.expand_path("bridge_scenario.rb", __dir__) - stdout, stderr, status = Open3.capture3( - RbConfig.ruby, - scenario, - name, - *arguments - ) + stdout, stderr, status = Open3.capture3(RbConfig.ruby, scenario, name) assert status.success?, <<~MESSAGE - Ruby bridge scenario #{([name] + arguments).join(" ")} failed + Ruby bridge scenario #{name} failed stdout: #{stdout} stderr: diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/test/bridge_scenario.rb b/baml_language/sdk_tests/crates/ruby_sorbet/test/bridge_scenario.rb index 0d938cab612..34708ebcf4d 100644 --- a/baml_language/sdk_tests/crates/ruby_sorbet/test/bridge_scenario.rb +++ b/baml_language/sdk_tests/crates/ruby_sorbet/test/bridge_scenario.rb @@ -1,481 +1,61 @@ # frozen_string_literal: true -require "ffi" -require "thread" require "timeout" -require "tmpdir" -require_relative "../../../../sdks/ruby/bridge_ruby/lib/baml/bridge" +$LOAD_PATH.unshift File.expand_path("../../../../sdks/ruby/bridge_ruby/lib", __dir__) +require "baml/bridge" ENV["BAML_FAKE_RUNTIME_VERSION"] = Baml::Bridge::VERSION +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_FIXTURE") +ENV.delete("BAML_FAKE_NATIVE_MODE") +Baml::Bridge.register_program("valid-bytecode".b) -FIXTURE = ENV.fetch("BAML_RUBY_TEST_FIXTURE") -MISSING_GETTER = ENV.fetch("BAML_RUBY_TEST_MISSING_GETTER") -INVALID_LIBRARY = ENV.fetch("BAML_RUBY_TEST_INVALID_LIBRARY") -THREAD_FIXTURE = ENV.fetch("BAML_RUBY_TEST_THREAD_FIXTURE") -REAL_RUNTIME = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") -REAL_BYTECODE = ENV.fetch("BAML_RUBY_TEST_REAL_BYTECODE") -VALID_PROGRAM = "valid-bytecode".b.freeze - -module ScenarioAssertions - module_function - - def assert(condition, message = "assertion failed") - raise message unless condition - end - - def assert_equal(expected, actual) - raise "expected #{expected.inspect}, got #{actual.inspect}" unless expected == actual - end - - def assert_raises(error_class) +def assert_raises(error_class) + begin yield rescue error_class => error return error - rescue Exception => error - raise "expected #{error_class}, got #{error.class}: #{error.message}" end -end - -include ScenarioAssertions - -class FixtureInspection - def initialize(path) - flags = FFI::DynamicLibrary::RTLD_NOW | FFI::DynamicLibrary::RTLD_LOCAL - @library = FFI::DynamicLibrary.open(path, flags) - end - - def count(symbol) - FFI::Function.new(:uint32, [], @library.find_function(symbol)).call - end -end - -def fixture_inspection - @fixture_inspection ||= FixtureInspection.new(FIXTURE) -end - -def wait_for_child(child, timeout: 2) - Timeout.timeout(timeout) { Process.wait2(child).last } -rescue Timeout::Error - Process.kill("KILL", child) - Process.wait(child) - raise "forked child #{child} did not exit promptly" -end - -def use_fixture - ENV["BAML_RUNTIME_PATH"] = FIXTURE - ENV.delete("BAML_FAKE_NATIVE_MODE") - ENV.delete("BAML_FAKE_NULL_FIELD") -end - -def initialize_fixture(program = VALID_PROGRAM) - use_fixture - Baml::Bridge.initialize!(program) -end - -def terminal_failure(mode: nil, null_field: nil, path: FIXTURE) - ENV["BAML_RUNTIME_PATH"] = path - ENV["BAML_FAKE_NATIVE_MODE"] = mode - ENV["BAML_FAKE_NULL_FIELD"] = null_field - first = assert_raises(Baml::Bridge::IncompatibleRuntimeError) do - Baml::Bridge.initialize!(VALID_PROGRAM) - end - - ENV["BAML_RUNTIME_PATH"] = FIXTURE - ENV.delete("BAML_FAKE_NATIVE_MODE") - ENV.delete("BAML_FAKE_NULL_FIELD") - second = assert_raises(Baml::Bridge::IncompatibleRuntimeError) do - Baml::Bridge.initialize!(VALID_PROGRAM) - end - assert_equal(first.message, second.message) - - expected_free_count = case mode - when "version-mismatch", "invalid-version-utf8", "null-version-pointer" - 1 - when "registration-rejected" - 2 - else - 0 - end - assert_equal(expected_free_count, fixture_inspection.count("baml_test_free_count")) if path == FIXTURE - first + raise "expected #{error_class}" end case ARGV.fetch(0) -when "configuration_retry" - ENV.delete("BAML_RUNTIME_PATH") - assert_raises(Baml::Bridge::RuntimeConfigurationError) do - Baml::Bridge.initialize!(VALID_PROGRAM) - end - ENV["BAML_RUNTIME_PATH"] = "relative/library" - assert_raises(Baml::Bridge::RuntimeConfigurationError) do - Baml::Bridge.initialize!(VALID_PROGRAM) - end - ENV["BAML_RUNTIME_PATH"] = File.join(Dir.tmpdir, "missing-baml-runtime") - assert_raises(Baml::Bridge::RuntimeConfigurationError) do - Baml::Bridge.initialize!(VALID_PROGRAM) - end - initialize_fixture - assert_equal(1, fixture_inspection.count("baml_test_initialize_count")) - assert_equal(3, fixture_inspection.count("baml_test_free_count")) -when "open_retry" - ENV["BAML_RUNTIME_PATH"] = INVALID_LIBRARY - assert_raises(Baml::Bridge::RuntimeLoadError) do - Baml::Bridge.initialize!(VALID_PROGRAM) - end - - if Process.respond_to?(:fork) - read_pipe, write_pipe = IO.pipe - child = fork do - read_pipe.close - begin - initialize_fixture - write_pipe.write("ok") - exit! 0 - rescue Exception => error - write_pipe.write("#{error.class}: #{error.message}") - exit! 1 - end - end - write_pipe.close - status = wait_for_child(child) - result = read_pipe.read - assert(status.success?, result) - assert_equal("ok", result) - end - - initialize_fixture - assert_equal(1, fixture_inspection.count("baml_test_initialize_count")) - assert_equal(3, fixture_inspection.count("baml_test_free_count")) -when "concurrent_open_failure_preserves_claim" - runtime_class = Baml::Bridge.const_get(:ProcessRuntime, false) - runtime = runtime_class.new - first_claimed = Queue.new - second_claimed = Queue.new - first_open_failed = Queue.new - - runtime.define_singleton_method(:configured_runtime_path!) do - Thread.current[:baml_test_runtime_path] - end - runtime.define_singleton_method(:claim_process!) do - super() - claim_count = (Thread.current[:baml_test_claim_count] || 0) + 1 - Thread.current[:baml_test_claim_count] = claim_count - if claim_count == 1 - if Thread.current[:baml_test_runtime_path] == INVALID_LIBRARY - first_claimed << true - second_claimed.pop - else - second_claimed << true - first_open_failed.pop - end - end - end - - first_error = Queue.new - first = Thread.new do - Thread.current[:baml_test_runtime_path] = INVALID_LIBRARY - runtime.initialize!(VALID_PROGRAM) - rescue Exception => error - first_error << error - first_open_failed << true - end - first_claimed.pop - - second_error = Queue.new - second = Thread.new do - Thread.current[:baml_test_runtime_path] = FIXTURE - runtime.initialize!(VALID_PROGRAM) - rescue Exception => error - second_error << error - end - - [first, second].each(&:join) - error = first_error.pop - assert(error.is_a?(Baml::Bridge::RuntimeLoadError), error.inspect) - assert(second_error.empty?, "surviving initializer failed") - assert_equal(Process.pid, runtime.instance_variable_get(:@owner_pid)) - - if Process.respond_to?(:fork) - read_pipe, write_pipe = IO.pipe - child = fork do - read_pipe.close - error = assert_raises(Baml::Bridge::ForkSafetyError) do - runtime.initialize!(VALID_PROGRAM) - end - write_pipe.write(error.message) - exit! 0 - end - write_pipe.close - status = wait_for_child(child) - message = read_pipe.read - assert(status.success?, message) - assert(message.include?("must exec before using BAML"), message) - end -when "terminal_mode" - terminal_failure(mode: ARGV.fetch(1)) -when "terminal_missing_getter" - error = terminal_failure(path: MISSING_GETTER) - assert_equal( - "Unable to resolve #{MISSING_GETTER.inspect}!baml_get_api_v1: symbol not found", - error.message - ) -when "terminal_null_field" - terminal_failure(null_field: ARGV.fetch(1)) -when "invalid_bytecode_retry_and_identity" - use_fixture - original = VALID_PROGRAM.dup - assert_raises(Baml::Bridge::ProgramInitializationError) do - Baml::Bridge.initialize!("invalid".b) - end - Baml::Bridge.initialize!(original) - original.replace("changed-program") - Baml::Bridge.initialize!(VALID_PROGRAM.dup) - assert_raises(Baml::Bridge::ProgramConflictError) do - Baml::Bridge.initialize!(original) - end - assert_equal(2, fixture_inspection.count("baml_test_initialize_count")) - assert_equal(1, fixture_inspection.count("baml_test_register_count")) - assert_equal(4, fixture_inspection.count("baml_test_free_count")) -when "concurrent_initialization" - use_fixture - ENV["BAML_FAKE_INIT_DELAY_MS"] = "25" - errors = Queue.new - threads = Array.new(12) do - Thread.new do - Baml::Bridge.initialize!(VALID_PROGRAM.dup) - rescue Exception => error - errors << error - end - end - threads.each(&:join) - assert(errors.empty?, "concurrent initialization errors: #{errors.size}") - assert_equal(1, fixture_inspection.count("baml_test_initialize_count")) - assert_equal(1, fixture_inspection.count("baml_test_register_count")) - assert_equal(3, fixture_inspection.count("baml_test_free_count")) -when "fork_before_native_use" - unless Process.respond_to?(:fork) - exit 0 - end - read_pipe, write_pipe = IO.pipe +when "truncated", "null_function" + ENV["BAML_FAKE_NATIVE_MODE"] = ARGV[0] + error = assert_raises(Baml::Bridge::IncompatibleRuntimeError) { Baml::Bridge.initialize! } + expected = ARGV[0] == "truncated" ? "BamlApiV1 is truncated" : "BamlApiV1.media_from_file_content is null" + raise error.message unless error.message.include?(expected) +when "pid_check" + exit 0 unless Process.respond_to?(:fork) + + Baml::Bridge.initialize! + api = Baml::Bridge.instance_variable_get(:@process_runtime).instance_variable_get(:@api) + pending = Baml::Call.new(api, 1) + settled = Baml::Call.new(api, 2) + wire = BamlBridge::Cffi::V1 + settled.complete(wire::BamlOutboundResult.encode(wire::BamlOutboundResult.new(ok: { int_value: 42 }))) + raise "parent result changed" unless settled.wait == 42 child = fork do - read_pipe.close - begin - initialize_fixture - write_pipe.write("ok") - exit! 0 - rescue Exception => error - write_pipe.write("#{error.class}: #{error.message}") - exit! 1 - end - end - write_pipe.close - status = wait_for_child(child) - result = read_pipe.read - assert(status.success?, result) - assert_equal("ok", result) -when "fork_after_native_use" - unless Process.respond_to?(:fork) - exit 0 - end - initialize_fixture - read_pipe, write_pipe = IO.pipe - child = fork do - read_pipe.close - error = assert_raises(Baml::Bridge::ForkSafetyError) do - Baml::Bridge.initialize!(VALID_PROGRAM) - end - write_pipe.write(error.message) + assert_raises(Baml::Bridge::ForkSafetyError) { Baml::Bridge.initialize! } + assert_raises(Baml::Bridge::ForkSafetyError) { pending.wait } + assert_raises(Baml::Bridge::ForkSafetyError) { settled.wait } exit! 0 end - write_pipe.close - status = wait_for_child(child) - message = read_pipe.read - assert(status.success?, message) - assert(message.include?("must exec before using BAML"), message) - Baml::Bridge.initialize!(VALID_PROGRAM.dup) - assert_equal(1, fixture_inspection.count("baml_test_initialize_count")) -when "fork_during_initialization" - unless Process.respond_to?(:fork) - exit 0 - end - use_fixture - ENV["BAML_FAKE_INIT_DELAY_MS"] = "750" - worker_errors = Queue.new - worker = Thread.new do - Baml::Bridge.initialize!(VALID_PROGRAM) - rescue Exception => error - worker_errors << error - end - deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5 - until fixture_inspection.count("baml_test_initialize_started") == 1 - raise "native initialization did not start" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline - - sleep 0.01 - end - - read_pipe, write_pipe = IO.pipe - child = fork do - read_pipe.close - error = assert_raises(Baml::Bridge::ForkSafetyError) do - Baml::Bridge.initialize!(VALID_PROGRAM) - end - write_pipe.write(error.message) - exit! 0 - end - write_pipe.close - status = wait_for_child(child) - message = read_pipe.read - assert(status.success?, message) - assert(message.include?("must exec before using BAML"), message) - worker.join - assert(worker_errors.empty?, "parent initialization failed") - Baml::Bridge.initialize!(VALID_PROGRAM.dup) - assert_equal(1, fixture_inspection.count("baml_test_initialize_count")) -when "fork_during_open_failure" - unless Process.respond_to?(:fork) - exit 0 - end - - runtime_class = Baml::Bridge.const_get(:ProcessRuntime, false) - runtime = runtime_class.new - open_started = Queue.new - release_open = Queue.new - open_failed = Queue.new - release_failure = Queue.new - runtime.define_singleton_method(:open_library) do |path, flags| - if path == INVALID_LIBRARY - open_started << true - release_open.pop - end - super(path, flags) - end - runtime.define_singleton_method(:load_api!) do |path| - super(path) - rescue Baml::Bridge::RuntimeLoadError - open_failed << true - release_failure.pop + begin + status = Timeout.timeout(10) { Process.wait2(child).last } + raise "forked child failed" unless status.success? + rescue Timeout::Error + Process.kill("KILL", child) + Process.wait(child) raise end - - ENV["BAML_RUNTIME_PATH"] = INVALID_LIBRARY - worker_error = Queue.new - worker = Thread.new do - runtime.initialize!(VALID_PROGRAM) - rescue Exception => error - worker_error << error - end - open_started.pop - - read_pipe, write_pipe = IO.pipe - child = fork do - read_pipe.close - error = assert_raises(Baml::Bridge::ForkSafetyError) do - runtime.initialize!(VALID_PROGRAM) - end - write_pipe.write(error.message) - exit! 0 - end - write_pipe.close - status = wait_for_child(child) - message = read_pipe.read - assert(status.success?, message) - assert(message.include?("must exec before using BAML"), message) - - release_open << true - open_failed.pop - ENV["BAML_RUNTIME_PATH"] = FIXTURE - read_pipe, write_pipe = IO.pipe - child = fork do - read_pipe.close - begin - runtime.initialize!(VALID_PROGRAM) - write_pipe.write("ok") - exit! 0 - rescue Exception => error - write_pipe.write("#{error.class}: #{error.message}") - exit! 1 - end - end - write_pipe.close - status = wait_for_child(child) - result = read_pipe.read - assert(status.success?, result) - assert_equal("ok", result) - - release_failure << true - worker.join - error = worker_error.pop - assert(error.is_a?(Baml::Bridge::RuntimeLoadError), error.inspect) -when "registered_callback_retention" - initialize_fixture - runtime = Baml::Bridge.instance_variable_get(:@process_runtime) - callback = runtime.instance_variable_get(:@result_callback) - assert(!callback.nil?, "process runtime did not retain its result callback") - callback = nil - 3.times { GC.start(full_mark: true, immediate_sweep: true) } - - flags = FFI::DynamicLibrary::RTLD_NOW | FFI::DynamicLibrary::RTLD_LOCAL - library = FFI::DynamicLibrary.open(FIXTURE, flags) - invoke = FFI::Function.new( - :int32, - %i[uint32 pointer size_t], - library.find_function("baml_test_invoke_registered_callback_on_thread"), - blocking: true - ) - payload_bytes = "retained\x00\xff".b - payload = FFI::MemoryPointer.new(:uint8, payload_bytes.bytesize) - payload.put_bytes(0, payload_bytes) - caller_thread = Thread.current.object_id - assert_equal(0, invoke.call(73, payload, payload_bytes.bytesize)) - call_id, copied, callback_thread = runtime.send(:pop_result_event) - assert_equal(73, call_id) - assert_equal(payload_bytes, copied) - assert(copied.frozen?, "registered callback bytes were not frozen") - assert_equal(Encoding::BINARY, copied.encoding) - assert(callback_thread != caller_thread, "registered callback did not use a foreign thread") - retained_callback = runtime.instance_variable_get(:@result_callback) - assert(!retained_callback.nil?, "process runtime released its result callback") - assert_equal(nil, retained_callback.pop_error) -when "foreign_thread_callback" - native = Baml::Bridge.const_get(:Native, false) - callbacks = Queue.new - callback_class = native.const_get(:BorrowedBytesCallback, false) - callback = callback_class.new do |call_id, bytes| - callbacks << [call_id, bytes, Thread.current.object_id] - end - flags = FFI::DynamicLibrary::RTLD_NOW | FFI::DynamicLibrary::RTLD_LOCAL - library = FFI::DynamicLibrary.open(THREAD_FIXTURE, flags) - invoke = FFI::Function.new( - :void, - %i[pointer uint32 pointer size_t], - library.find_function("baml_test_invoke_on_native_thread"), - blocking: true - ) - payload_bytes = "a\x00\xffz".b - payload = FFI::MemoryPointer.new(:uint8, payload_bytes.bytesize) - payload.put_bytes(0, payload_bytes) - caller_thread = Thread.current.object_id - invoke.call(callback.function, 41, payload, payload_bytes.bytesize) - call_id, copied, callback_thread = callbacks.pop - assert_equal(41, call_id) - assert_equal(payload_bytes, copied) - assert(copied.frozen?, "callback bytes were not frozen") - assert_equal(Encoding::BINARY, copied.encoding) - assert(callback_thread != caller_thread, "callback did not enter Ruby from a foreign thread") - assert_equal(nil, callback.pop_error) - - failing = callback_class.new { raise "callback boom" } - invoke.call(failing.function, 42, payload, payload_bytes.bytesize) - error = failing.pop_error - assert(error.is_a?(RuntimeError), "callback exception was not contained") - assert_equal("callback boom", error.message) -when "real_runtime_initialization" - ENV["BAML_RUNTIME_PATH"] = REAL_RUNTIME - bytecode = File.binread(REAL_BYTECODE).freeze - Baml::Bridge.initialize!(bytecode) - Baml::Bridge.initialize!(bytecode.dup) -when "type_validation" - assert_raises(TypeError) { Baml::Bridge.initialize!(Object.new) } +when "different_program" + Baml::Bridge.initialize! + assert_raises(Baml::Bridge::ProgramConflictError) { Baml::Bridge.register_program("different") } +when "abandoned_call" + assert_raises(TypeError) { Baml::Bridge.call_async("user.test", { "x" => Object.new }) } + library = FFI::DynamicLibrary.open(ENV.fetch("BAML_RUBY_TEST_FIXTURE"), FFI::DynamicLibrary::RTLD_NOW) + count = FFI::Function.new(:uint32, [], library.find_function("baml_test_release_count")).call + raise "abandoned call released #{count} times" unless count == 1 else raise "unknown scenario #{ARGV.first.inspect}" end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/test/gem_smoke.rb b/baml_language/sdk_tests/crates/ruby_sorbet/test/gem_smoke.rb new file mode 100644 index 00000000000..f9a5f1c031e --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/test/gem_smoke.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require "bundler" +require "fileutils" +require "open3" +require "rbconfig" +require "tmpdir" + +runtime, target, cli, sorbet_bundle = ARGV +unless runtime && target && cli + raise "usage: gem_smoke.rb [Sorbet bundle path]" +end + +test_root = File.expand_path("..", __dir__) +repo_root = File.expand_path("../../../..", test_root) +gem_root = File.join(repo_root, "baml_language/sdks/ruby/bridge_ruby") +# Use the bundle's exact dependency archives, including transitive dependencies. +# The install itself must not see the bundle or any already-installed gems. +archives = Bundler.load.specs.reject { |spec| spec.name == "bundler" }.map(&:cache_file) + +Dir.mktmpdir("baml-gem-") do |root| + raise "clean app must be outside the checkout" if root.start_with?("#{repo_root}/") + + app = File.join(root, "app") + gem_home = File.join(root, "gems") + baml_home = File.join(root, "baml") + packages = File.join(root, "packages") + FileUtils.mkdir_p([app, gem_home, packages]) + FileUtils.cp(archives, packages) + File.write(File.join(app, "baml.toml"), "[package]\nname = \"ruby_smoke\"\n") + FileUtils.mkdir_p(File.join(app, "baml_src")) + File.write(File.join(app, "baml_src/main.baml"), 'function HelloWorld() -> string { "hello world" }') + + Bundler.with_unbundled_env do + env = { + "GEM_HOME" => gem_home, "GEM_PATH" => gem_home, + "RUBYLIB" => nil, "RUBYOPT" => nil, "RUBYGEMS_GEMDEPS" => nil, + "BAML_HOME" => baml_home, "BAML_BRIDGE_PATH" => nil, + "DEV_BAML_CLI_DISABLE_AGENT_DETECTION" => "1", "BAML_AGENT_SKILL_CHECK" => "off" + } + run = lambda do |directory, *args| + stdout, stderr, status = Open3.capture3(env, RbConfig.ruby, *args, chdir: directory) + raise "#{args.inspect} failed (#{status.exitstatus}):\n#{stdout}\n#{stderr}" unless status.success? + + stdout + end + + [["generate", "add", "ruby/sorbet"], ["generate", "--output-dir", app]].each do |args| + stdout, stderr, status = Open3.capture3(env, cli, *args, chdir: app) + raise "baml-cli #{args.join(' ')} failed (#{status.exitstatus}):\n#{stdout}\n#{stderr}" unless status.success? + end + raise "CLI did not generate Ruby" unless File.file?(File.join(app, "baml_sdk.rb")) + + package = File.join(packages, "baml-bridge.gem") + run.call(gem_root, "-S", "gem", "build", "--norc", "baml-bridge.gemspec", "--output", package) + run.call(packages, "-S", "gem", "install", "--norc", "--local", "--no-document", package) + + version = run.call(app, "-rbaml/bridge/version", "-e", "print Baml::Bridge::TOOLCHAIN_VERSION") + cached_library = File.join(baml_home, "bridges", version, target, File.basename(runtime)) + FileUtils.mkdir_p(File.dirname(cached_library)) + FileUtils.cp(runtime, cached_library) + + output = run.call(app, "-I", app, "-e", <<~RUBY) + require "baml_sdk" + raise "missing embedded metadata" unless BamlSdk::METADATA&.include?("ruby/sorbet") + puts BamlSdk.hello_world + RUBY + raise "unexpected clean-app output: #{output.inspect}" unless output == "hello world\n" + + # Check the actual loaded files as well as the isolated environment. + run.call(app, "-I", app, "-e", <<~RUBY) + require "baml_sdk" + raise "repository load path" if ($LOAD_PATH + $LOADED_FEATURES).any? { |path| path.start_with?(#{repo_root.dump}) } + %w[baml-bridge ffi google-protobuf sorbet-runtime].each do |name| + raise "external gem: \#{name}" unless Gem.loaded_specs.fetch(name).full_gem_path.start_with?(#{gem_home.dump}) + end + spec = Gem.loaded_specs.fetch("baml-bridge") + raise "gem version does not match stamped runtime" unless spec.version == Gem::Version.new(Baml::Bridge::BRIDGE_RUNTIME_VERSION) + %w[README.md LICENSE rbi/baml.rbi lib/baml_bridge/cffi/v1/baml_inbound_pb.rb].each do |file| + raise "missing packaged file: \#{file}" unless File.file?(File.join(spec.full_gem_path, file)) + end + RUBY + + if sorbet_bundle + # Check the app as a Tapioca user would: Tapioca's default config ignores + # every vendor/ directory, and the installed gem supplies the runtime RBI. + gem_path = run.call(app, "-e", 'print Gem::Specification.find_by_name("baml-bridge").full_gem_path') + FileUtils.mkdir_p(File.join(app, "sorbet")) + File.write(File.join(app, "sorbet/config"), "--dir\n.\n--ignore=vendor/\n#{File.join(gem_path, 'rbi/baml.rbi')}\n") + File.write(File.join(app, "typed.rb"), <<~RUBY) + # typed: strict + T.let(BamlSdk.hello_world, String) + T.let(BamlSdk.hello_world_async, Baml::Call[String]) + RUBY + sorbet_env = { + "BUNDLE_GEMFILE" => File.join(test_root, "test/static/Gemfile"), + "BUNDLE_PATH" => sorbet_bundle, "BUNDLE_FROZEN" => "true" + } + stdout, stderr, status = Open3.capture3(sorbet_env, RbConfig.ruby, "-S", "bundle", "exec", "srb", "tc", chdir: app) + raise "Sorbet rejected the clean app (#{status.exitstatus}):\n#{stdout}\n#{stderr}" unless status.success? + end + end +end + +puts "Installed baml-bridge gem: clean app returned hello world" +puts "Sorbet accepted the clean app with the installed RBI" if sorbet_bundle diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/test/native/bridge_fixture.c b/baml_language/sdk_tests/crates/ruby_sorbet/test/native/bridge_fixture.c index c9d1205fa7e..3aa760588f4 100644 --- a/baml_language/sdk_tests/crates/ruby_sorbet/test/native/bridge_fixture.c +++ b/baml_language/sdk_tests/crates/ruby_sorbet/test/native/bridge_fixture.c @@ -9,115 +9,48 @@ #include #include -#if defined(_WIN32) -# include -#else -# include -# include -#endif - -static uint32_t initialize_count = 0; -static volatile uint32_t initialize_started = 0; -static uint32_t free_count = 0; -static uint32_t register_count = 0; -static BamlResultCallback registered_result_callback = NULL; +static uint32_t release_count = 0; -static BamlBuffer buffer_from_literal(const char *value) { - BamlBuffer buffer = {(const int8_t *)value, strlen(value)}; +static BamlBuffer empty_buffer(void) { + BamlBuffer buffer = {NULL, 0}; return buffer; } -static const char *fixture_version(void) { - const char *version = getenv("BAML_FAKE_RUNTIME_VERSION"); - return version == NULL ? "missing BAML_FAKE_RUNTIME_VERSION" : version; -} - static BamlBuffer probe_version(void) { - const char *mode = getenv("BAML_FAKE_NATIVE_MODE"); - if (mode != NULL && strcmp(mode, "version-mismatch") == 0) { - return buffer_from_literal("9.9.9"); - } - if (mode != NULL && strcmp(mode, "invalid-version-utf8") == 0) { - static const uint8_t invalid[] = {UINT8_C(0xff)}; - BamlBuffer buffer = {(const int8_t *)invalid, sizeof(invalid)}; - return buffer; - } - if (mode != NULL && strcmp(mode, "null-version-pointer") == 0) { - BamlBuffer buffer = {NULL, 1}; - return buffer; - } - return buffer_from_literal(fixture_version()); -} - -static void delay_initialize_if_requested(void) { - const char *raw = getenv("BAML_FAKE_INIT_DELAY_MS"); - if (raw == NULL) { - return; - } - unsigned long milliseconds = strtoul(raw, NULL, 10); -#if defined(_WIN32) - Sleep((DWORD)milliseconds); -#else - struct timespec duration = { - .tv_sec = (time_t)(milliseconds / 1000), - .tv_nsec = (long)((milliseconds % 1000) * 1000000), - }; - nanosleep(&duration, NULL); -#endif + const char *version = getenv("BAML_FAKE_RUNTIME_VERSION"); + BamlBuffer buffer = {(const int8_t *)version, strlen(version)}; + return buffer; } static BamlBuffer probe_initialize(const uint8_t *bytecode, size_t length) { - static const uint8_t valid[] = "valid-bytecode"; - initialize_count += 1; - initialize_started = 1; - delay_initialize_if_requested(); - if (length == sizeof(valid) - 1 && bytecode != NULL && - memcmp(bytecode, valid, sizeof(valid) - 1) == 0) { - BamlBuffer buffer = {NULL, 0}; - return buffer; - } - return buffer_from_literal("invalid test bytecode"); -} - -static void probe_free(BamlBuffer buffer) { - (void)buffer; - free_count += 1; + (void)bytecode; + (void)length; + return empty_buffer(); } -static void probe_register_result(BamlResultCallback callback) { - registered_result_callback = callback; -} +static void probe_free(BamlBuffer buffer) { (void)buffer; } +static void probe_register_result(BamlResultCallback callback) { (void)callback; } -static void probe_call( - const uint8_t *encoded_args, - size_t length, - uint32_t callback_id) { - (void)encoded_args; +static void probe_call(const uint8_t *args, size_t length, uint32_t callback_id) { + (void)args; (void)length; (void)callback_id; } -static uint64_t probe_new_call(void) { - return 1; -} - -static int32_t probe_cancel(uint64_t id) { - return id == 0 ? 1 : 0; -} +static uint64_t probe_new_call(void) { return 1; } +static int32_t probe_cancel(uint64_t id) { return id == 0 ? 1 : 0; } -static void probe_register_host_dispatch(BamlHostDispatchCallback callback) { - (void)callback; +static int32_t probe_release_call(uint64_t id) { + (void)id; + release_count += 1; + return 0; } -static void probe_register_host_release(BamlHostReleaseCallback callback) { - (void)callback; -} +static void probe_register_host_dispatch(BamlHostDispatchCallback callback) { (void)callback; } +static void probe_register_host_release(BamlHostReleaseCallback callback) { (void)callback; } -static void probe_complete_host_call( - uint32_t call_id, - const int8_t *content, - size_t length) { - (void)call_id; +static void probe_complete_host_call(uint32_t id, const int8_t *content, size_t length) { + (void)id; (void)content; (void)length; } @@ -134,78 +67,31 @@ static BamlCffiStatus probe_release(uint64_t key) { } static BamlCffiStatus probe_media_constructor( - int32_t media_kind, - const char *value, - const char *mime_type_or_null, - uint64_t *out_key, - int32_t *out_handle_type) { - (void)media_kind; + int32_t kind, const char *value, const char *mime, uint64_t *key, int32_t *type) { + (void)kind; (void)value; - (void)mime_type_or_null; - (void)out_key; - (void)out_handle_type; + (void)mime; + (void)key; + (void)type; return BAML_CFFI_STATUS_UNSUPPORTED_HANDLE_TYPE; } -static BamlCffiStatus probe_media_accessor( - uint64_t key, - int32_t handle_type, - BamlBuffer *out) { +static BamlCffiStatus probe_media_accessor(uint64_t key, int32_t type, BamlBuffer *out) { (void)key; - (void)handle_type; + (void)type; (void)out; return BAML_CFFI_STATUS_INVALID_HANDLE; } static BamlBuffer probe_register_bridge(const BamlBridgeInfoV1 *info) { - const char *expected_version = fixture_version(); - size_t expected_version_len = strlen(expected_version); - register_count += 1; - if (getenv("BAML_FAKE_NATIVE_MODE") != NULL && - strcmp(getenv("BAML_FAKE_NATIVE_MODE"), "registration-rejected") == 0) { - return buffer_from_literal("registration rejected by test fixture"); - } - if (info == NULL || info->struct_size < sizeof(BamlBridgeInfoV1)) { - return buffer_from_literal("invalid bridge info"); - } - if (info->language != BAML_BRIDGE_LANGUAGE_RUBY) { - return buffer_from_literal("expected Ruby bridge language 10"); - } - if (info->sdk_version_len != expected_version_len || - info->sdk_version == NULL || - memcmp(info->sdk_version, expected_version, expected_version_len) != 0) { - return buffer_from_literal("unexpected Ruby toolchain version"); - } - static const char expected_name[] = "Baml::Bridge"; - if (info->bridge_runtime_name_len != sizeof(expected_name) - 1 || - info->bridge_runtime_name == NULL || - memcmp(info->bridge_runtime_name, expected_name, sizeof(expected_name) - 1) != 0) { - return buffer_from_literal("unexpected Ruby bridge runtime name"); - } - if (info->bridge_runtime_version_len != expected_version_len || - info->bridge_runtime_version == NULL || - memcmp(info->bridge_runtime_version, expected_version, expected_version_len) != 0) { - return buffer_from_literal("unexpected Ruby bridge runtime version"); - } - BamlBuffer buffer = {NULL, 0}; - return buffer; + (void)info; + return empty_buffer(); } -static void probe_register_unhandled_spawn_error( - BamlUnhandledSpawnErrorCallback callback) { - (void)callback; -} - -static BamlBuffer probe_shutdown_runtime(void) { - BamlBuffer buffer = {NULL, 0}; - return buffer; -} +static void probe_register_unhandled_spawn_error(BamlUnhandledSpawnErrorCallback callback) { (void)callback; } -static BamlBuffer probe_initialize_with_metadata( - const uint8_t *bytecode, - size_t length, - const char *baml_toml) { - (void)baml_toml; +static BamlBuffer probe_initialize_with_metadata(const uint8_t *bytecode, size_t length, const char *metadata) { + (void)metadata; return probe_initialize(bytecode, length); } @@ -214,11 +100,31 @@ static BamlCffiStatus probe_clock(uint64_t id, uint64_t *out) { *out = 0; return BAML_CFFI_STATUS_OK; } -static int32_t probe_release_call(uint64_t id) { (void)id; return 0; } + static void probe_dispatch_v2(BamlHostDispatchV2 callback) { (void)callback; } static void probe_cancel_callback(BamlHostCancel callback) { (void)callback; } static uint32_t probe_protocol(void) { return 1; } +static BamlCffiStatus probe_trace_selection(uint64_t id, uint64_t key, BamlBuffer *out, uint64_t *reservation) { + (void)id; + (void)key; + (void)out; + (void)reservation; + return BAML_CFFI_STATUS_INVALID_HANDLE; +} + +static BamlCffiStatus probe_invocation_context(uint64_t key, BamlBuffer *out) { + (void)key; + (void)out; + return BAML_CFFI_STATUS_INVALID_HANDLE; +} + +static BamlCffiStatus probe_media_from_file_content( + int32_t kind, const char *file, const char *base64, const char *mime, uint64_t *key, int32_t *type) { + (void)file; + return probe_media_constructor(kind, base64, mime, key, type); +} + static const BamlApiV1 default_api = { .abi_version = BAML_API_V1_ABI_VERSION, .struct_size = sizeof(BamlApiV1), @@ -243,138 +149,29 @@ static const BamlApiV1 default_api = { .media_mime_type = probe_media_accessor, .register_bridge = probe_register_bridge, .register_unhandled_spawn_error_callback = probe_register_unhandled_spawn_error, - .shutdown_runtime = probe_shutdown_runtime, + .shutdown_runtime = empty_buffer, .initialize_runtime_from_blob_with_metadata = probe_initialize_with_metadata, .invocation_clock_ns = probe_clock, .release_function_call = probe_release_call, .register_host_dispatch_v2 = probe_dispatch_v2, .register_host_cancel_callback = probe_cancel_callback, .invocation_protocol_version = probe_protocol, + .trace_selection = probe_trace_selection, + .invocation_context = probe_invocation_context, + .media_name = probe_media_accessor, + .media_from_file_content = probe_media_from_file_content, }; -static BamlApiV1 probe_api; - -#define NULL_FIELD(field_name) \ - if (strcmp(field, #field_name) == 0) { \ - probe_api.field_name = NULL; \ - return; \ - } - -static void null_requested_field(const char *field) { - if (field == NULL) { - return; - } - NULL_FIELD(version) - NULL_FIELD(initialize_runtime_from_blob) - NULL_FIELD(free_buffer) - NULL_FIELD(register_callback) - NULL_FIELD(call_function) - NULL_FIELD(new_function_call) - NULL_FIELD(cancel_function_call) - NULL_FIELD(register_host_dispatch_callback) - NULL_FIELD(register_host_release_callback) - NULL_FIELD(complete_host_call) - NULL_FIELD(handle_clone) - NULL_FIELD(handle_release) - NULL_FIELD(media_from_url) - NULL_FIELD(media_from_file) - NULL_FIELD(media_from_base64) - NULL_FIELD(media_url) - NULL_FIELD(media_file) - NULL_FIELD(media_base64) - NULL_FIELD(media_mime_type) - NULL_FIELD(register_bridge) - NULL_FIELD(register_unhandled_spawn_error_callback) - NULL_FIELD(shutdown_runtime) - NULL_FIELD(initialize_runtime_from_blob_with_metadata) - NULL_FIELD(invocation_clock_ns) - NULL_FIELD(release_function_call) - NULL_FIELD(register_host_dispatch_v2) - NULL_FIELD(register_host_cancel_callback) - NULL_FIELD(invocation_protocol_version) -} - BAML_CFFI_API const BamlApiV1 *baml_get_api_v1(void) { + static BamlApiV1 api; const char *mode = getenv("BAML_FAKE_NATIVE_MODE"); - probe_api = default_api; - - if (mode != NULL && strcmp(mode, "null-table") == 0) { - return NULL; + api = default_api; + if (mode != NULL && strcmp(mode, "truncated") == 0) { + api.struct_size = offsetof(BamlApiV1, media_from_file_content); + } else if (mode != NULL && strcmp(mode, "null_function") == 0) { + api.media_from_file_content = NULL; } - if (mode != NULL && strcmp(mode, "wrong-abi") == 0) { - probe_api.abi_version = 999; - } else if (mode != NULL && strcmp(mode, "truncated") == 0) { - probe_api.struct_size = offsetof(BamlApiV1, register_bridge); - } - null_requested_field(getenv("BAML_FAKE_NULL_FIELD")); - return &probe_api; + return &api; } -BAML_CFFI_API uint32_t baml_test_initialize_count(void) { - return initialize_count; -} - -BAML_CFFI_API uint32_t baml_test_initialize_started(void) { - return initialize_started; -} - -BAML_CFFI_API uint32_t baml_test_free_count(void) { - return free_count; -} - -BAML_CFFI_API uint32_t baml_test_register_count(void) { - return register_count; -} - -typedef struct CallbackInvocation { - uint32_t call_id; - const uint8_t *content; - size_t length; -} CallbackInvocation; - -#if defined(_WIN32) -static DWORD WINAPI invoke_registered_callback(LPVOID raw_invocation) { - const CallbackInvocation *invocation = (const CallbackInvocation *)raw_invocation; - registered_result_callback( - invocation->call_id, - (const int8_t *)invocation->content, - invocation->length); - return 0; -} -#else -static void *invoke_registered_callback(void *raw_invocation) { - const CallbackInvocation *invocation = (const CallbackInvocation *)raw_invocation; - registered_result_callback( - invocation->call_id, - (const int8_t *)invocation->content, - invocation->length); - return NULL; -} -#endif - -BAML_CFFI_API int32_t baml_test_invoke_registered_callback_on_thread( - uint32_t call_id, - const uint8_t *content, - size_t length) { - if (registered_result_callback == NULL) { - return 1; - } - CallbackInvocation invocation = {call_id, content, length}; -#if defined(_WIN32) - HANDLE thread = CreateThread(NULL, 0, invoke_registered_callback, &invocation, 0, NULL); - if (thread == NULL) { - return 2; - } - WaitForSingleObject(thread, INFINITE); - CloseHandle(thread); -#else - pthread_t thread; - if (pthread_create(&thread, NULL, invoke_registered_callback, &invocation) != 0) { - return 2; - } - if (pthread_join(thread, NULL) != 0) { - return 3; - } -#endif - return 0; -} +BAML_CFFI_API uint32_t baml_test_release_count(void) { return release_count; } diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/test/native/missing_getter.c b/baml_language/sdk_tests/crates/ruby_sorbet/test/native/missing_getter.c deleted file mode 100644 index 17d0d43d110..00000000000 --- a/baml_language/sdk_tests/crates/ruby_sorbet/test/native/missing_getter.c +++ /dev/null @@ -1,9 +0,0 @@ -#if defined(_WIN32) -# define TEST_EXPORT __declspec(dllexport) -#else -# define TEST_EXPORT __attribute__((visibility("default"))) -#endif - -TEST_EXPORT int baml_test_not_the_api(void) { - return 1; -} diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/test/static/Gemfile b/baml_language/sdk_tests/crates/ruby_sorbet/test/static/Gemfile new file mode 100644 index 00000000000..941424866e8 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/test/static/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +# Separate from the runtime bundle: Sorbet has no Windows or 32-bit Linux binary. +gem "sorbet", "= 0.6.13506", require: false diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/test/static/Gemfile.lock b/baml_language/sdk_tests/crates/ruby_sorbet/test/static/Gemfile.lock new file mode 100644 index 00000000000..4ab484a6ea4 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/test/static/Gemfile.lock @@ -0,0 +1,27 @@ +GEM + remote: https://rubygems.org/ + specs: + sorbet (0.6.13506) + sorbet-static (= 0.6.13506) + sorbet-static (0.6.13506-aarch64-linux) + sorbet-static (0.6.13506-universal-darwin) + sorbet-static (0.6.13506-x86_64-linux) + +PLATFORMS + aarch64-linux + arm64-darwin + universal-darwin + x86_64-darwin + x86_64-linux + +DEPENDENCIES + sorbet (= 0.6.13506) + +CHECKSUMS + sorbet (0.6.13506) sha256=b6fd9f9ab2563c709b9308437cb40754e190667fddfe841723a7dda430978cd6 + sorbet-static (0.6.13506-aarch64-linux) sha256=04405070206de265a87e6544454092cd4c7b275415dccf76a4ab41649819ae9f + sorbet-static (0.6.13506-universal-darwin) sha256=744918a125841c84d0804410bfda068a6dc8182285ba50ddb1e8ce5955b24700 + sorbet-static (0.6.13506-x86_64-linux) sha256=b57d36d1db374e10109bbd5af3bec0edf6f94f3e86dbffe69199763909ad2c91 + +BUNDLED WITH + 4.0.4 diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/test/static/minitest.rbi b/baml_language/sdk_tests/crates/ruby_sorbet/test/static/minitest.rbi new file mode 100644 index 00000000000..3f82dd7ad28 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/test/static/minitest.rbi @@ -0,0 +1,6 @@ +# typed: true +# Runtime tests use minitest without type annotations; the strict consumers +# exercise generated signatures directly, without test-framework assertions. +module Minitest + class Test; end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/test/static/wrong_argument.rb b/baml_language/sdk_tests/crates/ruby_sorbet/test/static/wrong_argument.rb new file mode 100644 index 00000000000..8780550a54d --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/test/static/wrong_argument.rb @@ -0,0 +1,4 @@ +# typed: strict +require_relative "../../function_calls/generated/baml_sdk" + +BamlSdk.single_required_arg(42) # error: 7002 diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_aliases.rb b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_aliases.rb new file mode 100644 index 00000000000..f214d0acbb2 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_aliases.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class AliasesTest < Minitest::Test + def test_aliases_round_trip_string_list + assert_equal ["a", "b"], BamlSdk::Aliases.round_trip_string_list(["a", "b"]) + end + + def test_aliases_round_trip_rec_list + assert_equal 1, BamlSdk::Aliases.round_trip_rec_list(1) + assert_equal [1, [2, 3]], BamlSdk::Aliases.round_trip_rec_list([1, [2, 3]]) + end + + def test_aliases_round_trip_alias_container + c = BamlSdk::Aliases::AliasContainer.new(list_field: ["x"], rec_field: [1, [2]]) + assert_equal c, BamlSdk::Aliases.round_trip_alias_container(c) + end + +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_class_refs.rb b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_class_refs.rb new file mode 100644 index 00000000000..616a1a2ac62 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_class_refs.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class ClassRefsTest < Minitest::Test + def test_class_refs_make_outer + o = BamlSdk::ClassRefs.make_outer(5) + assert_equal 5, o.inner.value + end + + def test_class_refs_round_trip_inner + i = BamlSdk::ClassRefs::Inner.new(value: 3) + assert_equal i, BamlSdk::ClassRefs.round_trip_inner(i) + end + + def test_class_refs_round_trip_outer + o = BamlSdk::ClassRefs::Outer.new(inner: BamlSdk::ClassRefs::Inner.new(value: 9)) + assert_equal o, BamlSdk::ClassRefs.round_trip_outer(o) + end + +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_complex_models.rb b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_complex_models.rb new file mode 100644 index 00000000000..32543acc94a --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_complex_models.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class ComplexModelsTest < Minitest::Test + Models = BamlSdk::ComplexModels + + def test_complex_models_round_trip_complex_profile_preserves_deeply_nested_mixed_shape_class + home = Models::PostalAddress.new( + line1: "1 Compiler Way", line2: nil, city: "San Francisco", region: "CA", postal_code: "94107", + location: Models::GeoPoint.new(lat: 37.7749, lng: -122.4194) + ) + office = Models::PostalAddress.new( + line1: "200 Type Lane", line2: "Suite 42", city: "Oakland", region: "CA", postal_code: "94612", location: nil + ) + email = Models::ContactMethod.new(label: "email", value: "ada@example.com", verified: true) + phone = Models::ContactMethod.new(label: "phone", value: "+1-555-0100", verified: false) + invoice = Models::Invoice.new( + id: "inv-001", status: "sent", + items: [Models::LineItem.new( + sku: "sdk-pro", quantity: 2, unit_price: 19.5, tags: ["sdk", "typescript"], + attributes: { "language" => "ts", "support" => "priority" } + )], + payment: Models::CardPayment.new(brand: "visa", last4: "4242", billing_address: home), + notes: "first invoice" + ) + wire_invoice = Models::Invoice.new( + id: "inv-002", status: "paid", + items: [Models::LineItem.new( + sku: "sdk-enterprise", quantity: 1, unit_price: 250, tags: ["sdk", "enterprise"], + attributes: { "language" => "python", "term" => "annual" } + )], + payment: Models::WirePayment.new(bank_name: "Boundary Bank", routing_code: "110000000", reference: nil), + notes: nil + ) + profile = Models::ComplexProfile.new( + id: "profile-001", tier: Models::AccountTier::Enterprise, + owner: Models::ProfileOwner.new(name: "Ada Lovelace", primary_contact: email, backup_contacts: [phone]), + addresses: [home, office], invoices: [invoice, wire_invoice], + audit_trail: [ + Models::AuditEvent.new(actor: "system", action: "created", context: { "source" => "fixture" }), + Models::AuditEvent.new(actor: "reviewer", action: "approved", context: { "level" => "2", "region" => "us" }) + ], + metadata: { "cohort" => "beta", "owner_kind" => "internal" }, featured: invoice, + flags: [7, "manual-review", true] + ) + assert_equal profile, Models.round_trip_complex_profile(profile) + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_enums.rb b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_enums.rb new file mode 100644 index 00000000000..ba2fc56444b --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_enums.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class EnumsTest < Minitest::Test + def test_enums_pick_sentiment + assert_equal BamlSdk::Enums::Sentiment::Positive, BamlSdk::Enums.pick_sentiment(true) + assert_equal BamlSdk::Enums::Sentiment::Negative, BamlSdk::Enums.pick_sentiment(false) + end + + def test_enums_pick_positive + assert_equal BamlSdk::Enums::Sentiment::Positive, BamlSdk::Enums.pick_positive() + end + + def test_enums_round_trip_sentiment + assert_equal BamlSdk::Enums::Sentiment::Negative, BamlSdk::Enums.round_trip_sentiment(BamlSdk::Enums::Sentiment::Negative) + end + + def test_enums_round_trip_sentiment_positive + assert_equal BamlSdk::Enums::Sentiment::Positive, BamlSdk::Enums.round_trip_sentiment_positive(BamlSdk::Enums::Sentiment::Positive) + end + + def test_enums_round_trip_enums + e = BamlSdk::Enums::Enums.new(bare_enum: BamlSdk::Enums::Sentiment::Positive, variant_as_type: BamlSdk::Enums::Sentiment::Positive) + assert_equal e, BamlSdk::Enums.round_trip_enums(e) + end + +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_forward_refs.rb b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_forward_refs.rb new file mode 100644 index 00000000000..6866fabf468 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_forward_refs.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class ForwardRefsTest < Minitest::Test + def test_forward_refs_round_trip_other + # Node's required self-reference has no finite host value; check reachability. + assert_operator BamlSdk::ForwardRefs::Node, :<, T::Struct + assert_respond_to BamlSdk::ForwardRefs, :round_trip_node + o = BamlSdk::ForwardRefs::Other.new(v: 7) + assert_equal o, BamlSdk::ForwardRefs.round_trip_other(o) + end + + def test_forward_refs_round_trip_rec_list + assert_equal [1, [2, 3]], BamlSdk::ForwardRefs.round_trip_rec_list([1, [2, 3]]) + end + + def test_forward_refs_round_trip_g_node_int + node = BamlSdk::ForwardRefs::GNode + leaf = Baml.bind_types(node[Integer].new(children: []), Integer) + tree = Baml.bind_types(node[Integer].new(children: [leaf]), Integer) + assert_equal tree, BamlSdk::ForwardRefs.round_trip_g_node_int(tree) + end + + def test_forward_refs_round_trip_rec_list_with_other + assert_equal 1, BamlSdk::ForwardRefs.round_trip_rec_list_with_other(1) + assert_equal [1, 2], BamlSdk::ForwardRefs.round_trip_rec_list_with_other([1, 2]) + end + +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_generics.rb b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_generics.rb new file mode 100644 index 00000000000..dcd116b530f --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_generics.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require "minitest/autorun" +require "baml_sdk" +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class GenericsTest < Minitest::Test + G = BamlSdk::Generics + + def test_generic_generic + wrapper = G.make_wrapper_methods("hello") + assert_equal "hello", wrapper.get_value_or_marker + end + + def test_generic_generic_wrapper_get_value + wrapper = G.make_wrapper_methods("hello") + assert_equal "hello", wrapper.get_value + end + + # Bind expected values so equality checks their concrete BAML arguments too. + def test_generics_round_trip_wrapper_int + wrapper = Baml.bind_types(G::Wrapper[Integer].new(value: 5), Integer) + assert_equal wrapper, G.round_trip_wrapper_int(wrapper) + end + + def test_generics_round_trip_generic_linked_list_int + tail = Baml.bind_types(G::GenericLinkedList[Integer].new(value: 2, next_: nil), Integer) + linked = Baml.bind_types(G::GenericLinkedList[Integer].new(value: 1, next_: tail), Integer) + assert_equal linked, G.round_trip_generic_linked_list_int(linked) + end + + def test_generics_round_trip_generic_binary_tree_int + tree = Baml.bind_types(G::GenericBinaryTree[Integer].new(value: 1, left: nil, right: nil), Integer) + assert_equal tree, G.round_trip_generic_binary_tree_int(tree) + end + + def test_generics_round_trip_box_int + wrapped = Baml.bind_types(G::Wrapper[Integer].new(value: 4), Integer) + box = Baml.bind_types(G::Box[Integer].new(value: 3, wrapped: wrapped), Integer) + assert_equal box, G.round_trip_box_int(box) + end + + def test_generics_round_trip_nested_generics + wrapped = Baml.bind_types(G::Wrapper[Integer].new(value: 1), Integer) + linked = Baml.bind_types(G::GenericLinkedList[Integer].new(value: 9, next_: nil), Integer) + nested = G::NestedGenerics.new( + ww: Baml.bind_types(G::Wrapper[G::Wrapper[Integer]].new(value: wrapped), Baml::Types.generic(G::Wrapper, Integer)), + wl: Baml.bind_types(G::Wrapper[T::Array[Integer]].new(value: [1, 2]), Baml::Types.list(Integer)), + wr: Baml.bind_types(G::Wrapper[G::GenericLinkedList[Integer]].new(value: linked), Baml::Types.generic(G::GenericLinkedList, Integer)) + ) + assert_equal nested, G.round_trip_nested_generics(nested) + end + + def test_generics_round_trip_differing_instantiation + wrapped = Baml.bind_types(G::Wrapper[Integer].new(value: 1), Integer) + linked = Baml.bind_types(G::GenericLinkedList[G::Wrapper[Integer]].new(value: wrapped, next_: nil), Baml::Types.generic(G::Wrapper, Integer)) + different = G::DifferingInstantiation.new(list: linked) + assert_equal different, G.round_trip_differing_instantiation(different) + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_handles.rb b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_handles.rb new file mode 100644 index 00000000000..583f62bf79b --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_handles.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") +require "socket" +require "tempfile" + +class HandlesTest < Minitest::Test + PNG_B64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk" \ + "+M8AAAQEAQB9eIv5AAAAAElFTkSuQmCC" + + def test_handles_image_from_base64_roundtrips_payload + payload = PNG_B64.dup + image = Baml::Image.from_base64(payload, mime_type: "image/png") + payload.replace("changed") + assert image.frozen? + assert image.base64.frozen? + assert_equal "image/png", image.mime_type + assert_equal PNG_B64, image.base64 + result = BamlSdk::Media.round_trip_image(image) + assert_equal PNG_B64, result.base64 + assert_equal "image/png", BamlSdk::Media.image_mime_type(image) + assert_nil result.url + assert_nil result.name + + # File values own their contents, not a path or an engine capability. + Tempfile.create(["scan", ".pdf"]) do |file| + file.write("%PDF-1.7") + file.close + pdf = Baml::Pdf.from_file(file.path) + File.unlink(file.path) + assert_equal "JVBERi0xLjc=", pdf.base64 + assert_equal File.basename(file.path), pdf.name + assert_nil pdf.mime_type + decoded = BamlSdk::Media.round_trip_pdf(pdf) + assert_equal pdf.name, decoded.name + assert_equal pdf.base64, decoded.base64 + assert_equal "application/pdf", decoded.mime_type + assert_raises(Errno::ENOENT) { Baml::Pdf.from_file(file.path) } + end + audio = Baml::Audio.from_file_content("recording.webm", "YQ==") + assert_nil audio.mime_type + assert_equal "recording.webm", BamlSdk::Media.round_trip_audio(audio).name + video = Baml::Video.from_url("https://example.com/video.mp4", mime_type: "video/mp4") + assert_equal video.mime_type, BamlSdk::Media.round_trip_video(video).mime_type + end + + def test_handles_http_get_response_fields_and_methods + server = TCPServer.new("127.0.0.1", 0) + body = "hello from localhost" + thread = Thread.new do + client = server.accept + while (line = client.gets) && line != "\r\n" + # Consume the request headers before writing a response. + end + client.write("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: #{body.bytesize}\r\nConnection: close\r\n\r\n#{body}") + ensure + client&.close + end + response = BamlSdk::Baml::Http.fetch("http://127.0.0.1:#{server.addr[1]}/", timeout: nil) + assert_equal 200, response.status_code + assert_equal true, response.ok + assert_equal body, response.text + ensure + server&.close + thread&.join(2) + thread&.kill if thread&.alive? + end + + def test_handles_open_file_returns_file_handle + Tempfile.create("digits") do |file| + file.write("0123456789") + file.flush + value = BamlSdk::Baml::Fs.open(file.path, "r") + assert_instance_of BamlSdk::Baml::Fs::File, value + assert_nil value.close + end + end + + def test_handles_file_cursor_state_persists_across_calls + Tempfile.create("digits") do |file| + file.write("0123456789") + file.flush + value = BamlSdk::Baml::Fs.open(file.path, "r") + begin + assert_equal 3, value.seek_from("current", 3) + assert_equal 6, value.seek_from("current", 3) + assert_equal 0, value.seek_from("start", 0) + assert_equal 2, value.seek_from("current", 2) + assert_equal "23456789", value.text + ensure + assert_nil value.close + end + end + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_lists.rb b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_lists.rb new file mode 100644 index 00000000000..e5f770d6bc9 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_lists.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class ListsTest < Minitest::Test + def test_lists_round_trip_ints + assert_equal [1, 2, 3], BamlSdk::Lists.round_trip_ints([1, 2, 3]) + end + + def test_lists_round_trip_empty_list + assert_equal [], BamlSdk::Lists.round_trip_ints([]) + end + + def test_lists_round_trip_optional_strings + assert_equal ["a", nil, "b"], BamlSdk::Lists.round_trip_optional_strings(["a", nil, "b"]) + end + + def test_lists_round_trip_union_list + assert_equal [1, "two", 3], BamlSdk::Lists.round_trip_union_list([1, "two", 3]) + end + + def test_lists_round_trip_list_container + c = BamlSdk::Lists::ListContainer.new(ints: [1, 2], optional_strings: [nil, "z"], union_list: [1, "x"]) + assert_equal c, BamlSdk::Lists.round_trip_list_container(c) + end + +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_literals.rb b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_literals.rb new file mode 100644 index 00000000000..0a0adf7761a --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_literals.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class LiteralsTest < Minitest::Test + def test_literals_return_literals + assert_equal 42, BamlSdk::Literals.return_literal42() + assert_equal -1, BamlSdk::Literals.return_literal_neg_one() + assert_equal "draft", BamlSdk::Literals.return_literal_draft() + assert_equal "has \"quotes\"", BamlSdk::Literals.return_literal_escaped() + assert_equal true, BamlSdk::Literals.return_literal_true() + assert_equal false, BamlSdk::Literals.return_literal_false() + end + + def test_literals_round_trip_literal42 + assert_equal 42, BamlSdk::Literals.round_trip_literal42(42) + end + + def test_literals_round_trip_literal_draft + assert_equal "draft", BamlSdk::Literals.round_trip_literal_draft("draft") + end + + def test_literals_round_trip_literal_escaped + assert_equal "has \"quotes\"", BamlSdk::Literals.round_trip_literal_escaped("has \"quotes\"") + end + + def test_literals_round_trip_literal_true + assert_equal true, BamlSdk::Literals.round_trip_literal_true(true) + end + + def test_literals_round_trip_literal_false + assert_equal false, BamlSdk::Literals.round_trip_literal_false(false) + end + + def test_literals_round_trip_literals + lit = BamlSdk::Literals::Literals.new(literal_42: 42, literal_draft: "draft", literal_escaped: "has \"quotes\"", literal_true: true, literal_false: false) + assert_equal lit, BamlSdk::Literals.round_trip_literals(lit) + end + +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_main.rb b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_main.rb new file mode 100644 index 00000000000..bf37413f451 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_main.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class MainTest < Minitest::Test + def test_main_root_imports_cleanly + assert_kind_of Module, BamlSdk + end + + def test_main_all_namespaces_reachable + %i[Primitives Media Enums Literals ClassRefs Aliases AliasesConsumer Optional + Lists Maps Unions Recursion Generics ForwardRefs ComplexModels Lorem A].each do |name| + assert_kind_of Module, BamlSdk.const_get(name, false) + end + end + + def test_main_root_foo_reachable + assert_operator BamlSdk::Foo, :<, T::Struct + end + + def test_main_lorem_resume_reachable + assert_operator BamlSdk::Lorem::Resume, :<, T::Struct + end + + def test_main_deep_namespace_thing_reachable + assert_operator BamlSdk::A::B::Thing, :<, T::Struct + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_maps.rb b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_maps.rb new file mode 100644 index 00000000000..262793842c7 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_maps.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class MapsTest < Minitest::Test + def test_maps_round_trip_simple_map + assert_equal({ "a" => 1, "b" => 2 }, BamlSdk::Maps.round_trip_simple_map({ "a" => 1, "b" => 2 })) + end + + def test_maps_round_trip_list_valued_map + assert_equal({ "k" => [1, 2] }, BamlSdk::Maps.round_trip_list_valued_map({ "k" => [1, 2] })) + end + + def test_maps_round_trip_sentiment + assert_equal BamlSdk::Maps::Sentiment::Positive, BamlSdk::Maps.round_trip_sentiment(BamlSdk::Maps::Sentiment::Positive) + end + + def test_maps_round_trip_resume + r = BamlSdk::Maps::Resume.new(name: "n") + assert_equal r, BamlSdk::Maps.round_trip_resume(r) + end + +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_media.rb b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_media.rb new file mode 100644 index 00000000000..a131704b3e4 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_media.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class MediaTest < Minitest::Test + URL = "https://example.com/asset" + + def test_media_return_image + image = BamlSdk::Media.return_image(URL, nil) + assert_instance_of Baml::Image, image + assert_equal URL, image.url + end + + def test_media_return_audio + audio = BamlSdk::Media.return_audio(URL, nil) + assert_instance_of Baml::Audio, audio + assert_equal URL, audio.url + end + + def test_media_return_video + video = BamlSdk::Media.return_video(URL, nil) + assert_instance_of Baml::Video, video + assert_equal URL, video.url + end + + def test_media_return_pdf + pdf = BamlSdk::Media.return_pdf(URL, nil) + assert_instance_of Baml::Pdf, pdf + assert_equal URL, pdf.url + end + + def test_media_round_trip_image + image = BamlSdk::Media.return_image(URL, nil) + assert_equal URL, BamlSdk::Media.round_trip_image(image).url + end + + def test_media_round_trip_audio + audio = BamlSdk::Media.return_audio(URL, nil) + assert_equal URL, BamlSdk::Media.round_trip_audio(audio).url + end + + def test_media_round_trip_video + video = BamlSdk::Media.return_video(URL, nil) + assert_equal URL, BamlSdk::Media.round_trip_video(video).url + end + + def test_media_round_trip_pdf + pdf = BamlSdk::Media.return_pdf(URL, nil) + assert_equal URL, BamlSdk::Media.round_trip_pdf(pdf).url + end + + def test_media_round_trip_all_media + [Baml::Image, Baml::Audio, Baml::Video, Baml::Pdf].each do |kind| + result = BamlSdk::Media.round_trip_all_media(kind.from_url(URL)) + assert_instance_of kind, result + assert_equal URL, result.url + end + end + + def test_media_round_trip_media_or_image_class + media = BamlSdk::Media.round_trip_media_or_image_class(Baml::Image.from_url(URL)) + assert_instance_of Baml::Image, media + assert_equal URL, media.url + labelled = BamlSdk::Media::Image.new(label: "cover") + assert_equal labelled, BamlSdk::Media.round_trip_media_or_image_class(labelled) + end + + def test_media_round_trip_media + media = BamlSdk::Media::Media.new( + image_field: BamlSdk::Media.return_image(URL, nil), + audio_field: BamlSdk::Media.return_audio(URL, nil), + video_field: BamlSdk::Media.return_video(URL, nil), + pdf_field: BamlSdk::Media.return_pdf(URL, nil) + ) + result = BamlSdk::Media.round_trip_media(media) + assert_equal media, result + assert media.eql?(result) + assert_equal media.hash, result.hash + assert_equal "found", { media => "found" }[result] + %i[image_field audio_field video_field pdf_field].each do |field| + assert_equal URL, result.public_send(field).url + end + end +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_optional.rb b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_optional.rb new file mode 100644 index 00000000000..b3dcb064eca --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_optional.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class OptionalTest < Minitest::Test + def test_optional_round_trip_optional_int + assert_equal 5, BamlSdk::Optional.round_trip_optional_int(5) + assert_nil BamlSdk::Optional.round_trip_optional_int(nil) + end + + def test_optional_round_trip_optional_resume + r = BamlSdk::Optional::Resume.new(name: "ada") + assert_equal r, BamlSdk::Optional.round_trip_optional_resume(r) + assert_nil BamlSdk::Optional.round_trip_optional_resume(nil) + end + + def test_optional_round_trip_optional_union + assert_equal 3, BamlSdk::Optional.round_trip_optional_union(3) + assert_equal "s", BamlSdk::Optional.round_trip_optional_union("s") + assert_nil BamlSdk::Optional.round_trip_optional_union(nil) + end + + def test_optional_round_trip_resume + r = BamlSdk::Optional::Resume.new(name: "grace") + assert_equal r, BamlSdk::Optional.round_trip_resume(r) + end + + def test_optional_round_trip_optional_container + c = BamlSdk::Optional::OptionalContainer.new(optional_int: nil, optional_class: BamlSdk::Optional::Resume.new(name: "x"), optional_union: "y") + assert_equal c, BamlSdk::Optional.round_trip_optional_container(c) + end + +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_primitives.rb b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_primitives.rb new file mode 100644 index 00000000000..46a453a0cf8 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_primitives.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class PrimitivesTest < Minitest::Test + def test_primitives_return_int + assert_equal 42, BamlSdk::Primitives.return_int() + end + + def test_primitives_return_float + assert_equal 3.14, BamlSdk::Primitives.return_float() + end + + def test_primitives_return_string + assert_equal "hello", BamlSdk::Primitives.return_string() + end + + def test_primitives_return_bool + assert_equal true, BamlSdk::Primitives.return_bool() + end + + def test_primitives_return_bigint + assert_equal 12345678901234567890, BamlSdk::Primitives.return_bigint() + end + + def test_primitives_return_null + assert_nil BamlSdk::Primitives.return_null() + end + + def test_primitives_round_trip_int + assert_equal 7, BamlSdk::Primitives.round_trip_int(7) + end + + def test_primitives_round_trip_float + assert_equal 2.5, BamlSdk::Primitives.round_trip_float(2.5) + end + + def test_primitives_round_trip_float_accepts_int + result = BamlSdk::Primitives.round_trip_float(7) + assert_instance_of Float, result + assert_equal 7.0, result + end + + def test_primitives_round_trip_string + assert_equal "hi", BamlSdk::Primitives.round_trip_string("hi") + end + + def test_primitives_round_trip_bool + assert_equal false, BamlSdk::Primitives.round_trip_bool(false) + end + + def test_primitives_round_trip_bigint + big = 2**80 + assert_equal big, BamlSdk::Primitives.round_trip_bigint(big) + assert_equal -big, BamlSdk::Primitives.round_trip_bigint(-big) + assert_equal 7, BamlSdk::Primitives.round_trip_bigint(7) + assert_equal 42, BamlSdk::GoCodegen::PrimitiveEdges.round_trip_literal_bigint(42) + hex_boundary = 2**64 + assert_equal hex_boundary, BamlSdk::Primitives.round_trip_bigint(hex_boundary) + end + + def test_primitives_round_trip_null + assert_nil BamlSdk::Primitives.round_trip_null(nil) + end + + def test_primitives_round_trip_uint8_array + assert_equal "\x00\x01\x02".b, BamlSdk::Primitives.round_trip_uint8_array("\x00\x01\x02".b) + assert_equal Encoding::BINARY, BamlSdk::Primitives.round_trip_uint8_array("ab").encoding + + # Ruby has one String type: only binary strings choose bytes when both + # string and bytes occur in a union, including nested containers/aliases. + protocol = Baml::Bridge.const_get(:Protocol, false) + union = [:union, [[:string], [:bytes]]] + protocol.define_function("user.byte_choice", params: [["value", union]]) + wire = protocol.const_get(:Wire, false) + encode = ->(name, value) { wire::CallFunctionArgs.decode(protocol.encode_call(0, name, { "value" => value })).kwargs.first.value } + assert_equal :string_value, encode.call("user.byte_choice", "ab").value + assert_equal :uint8array_value, encode.call("user.byte_choice", "ab".b).value + protocol.define_alias("user.ByteChoice", union) + hint = [:map, [:string], [:list, [:alias, "user.ByteChoice"]]] + protocol.define_function("user.nested_bytes", params: [["value", hint]]) + values = encode.call("user.nested_bytes", { "k" => ["text", "bytes".b] }) + .map_value.entries.first.value.list_value.values + assert_equal [:string_value, :uint8array_value], values.map(&:value) + end + + def test_primitives_round_trip_primitives + p = BamlSdk::Primitives::Primitives.new(int_field: 1, float_field: 1.5, string_field: "s", bool_field: true, null_field: nil, uint8array_field: "\x61\x62".b) + assert_equal p, BamlSdk::Primitives.round_trip_primitives(p) + end + + def test_primitives_round_trip_primitives_float_field_accepts_int + p = BamlSdk::Primitives::Primitives.new(int_field: 1, float_field: 2, string_field: "s", bool_field: true, null_field: nil, uint8array_field: "\x61\x62".b) + assert_equal 2, p.float_field # Ruby accepts Integer input; the SDK widens it. + result = BamlSdk::Primitives.round_trip_primitives(p) + assert_equal 2.0, result.float_field + assert_instance_of Float, result.float_field + end + +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_recursion.rb b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_recursion.rb new file mode 100644 index 00000000000..4dabb25f3f8 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_recursion.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class RecursionTest < Minitest::Test + def test_recursion_round_trip_int_binary_tree + t = BamlSdk::Recursion::IntBinaryTree.new(value: 1, left: BamlSdk::Recursion::IntBinaryTree.new(value: 2, left: nil, right: nil), right: nil) + assert_equal t, BamlSdk::Recursion.round_trip_int_binary_tree(t) + end + + def test_recursion_int_binary_tree_deeper_than_the_wire_is_refused + build = ->(nodes) do + nodes.downto(1).reduce(nil) do |left, value| + BamlSdk::Recursion::IntBinaryTree.new(value: value, left: left, right: nil) + end + end + at_the_limit = build.call(32) + assert_equal at_the_limit, BamlSdk::Recursion.round_trip_int_binary_tree(at_the_limit) + [33, 1000].each do |nodes| + error = assert_raises(TypeError) { BamlSdk::Recursion.round_trip_int_binary_tree(build.call(nodes)) } + assert_includes error.message, "nested too deeply" + end + end + + def test_recursion_round_trip_mutual_recursion + a = BamlSdk::Recursion::A.new(b: BamlSdk::Recursion::B.new(a: nil)) + b = BamlSdk::Recursion::B.new(a: BamlSdk::Recursion::A.new(b: nil)) + assert_equal a, BamlSdk::Recursion.round_trip_a(a) + assert_equal b, BamlSdk::Recursion.round_trip_b(b) + end + + def test_recursion_round_trip_scc_t1_t2_t3 + t1 = BamlSdk::Recursion::T1.new(via2: BamlSdk::Recursion::T2.new(via1: nil, via3: nil), via3: nil) + t2 = BamlSdk::Recursion::T2.new(via1: nil, via3: BamlSdk::Recursion::T3.new(via1: nil, via2: nil)) + t3 = BamlSdk::Recursion::T3.new(via1: nil, via2: nil) + assert_equal t1, BamlSdk::Recursion.round_trip_t1(t1) + assert_equal t2, BamlSdk::Recursion.round_trip_t2(t2) + assert_equal t3, BamlSdk::Recursion.round_trip_t3(t3) + end + + def test_recursion_round_trip_scc_t4_t5_t6 + t4 = BamlSdk::Recursion::T4.new(via5: BamlSdk::Recursion::T5.new(via4: nil, via6: nil), via6: nil) + t5 = BamlSdk::Recursion::T5.new(via4: nil, via6: BamlSdk::Recursion::T6.new(via4: nil, via5: nil)) + t6 = BamlSdk::Recursion::T6.new(via4: nil, via5: nil) + assert_equal t4, BamlSdk::Recursion.round_trip_t4(t4) + assert_equal t5, BamlSdk::Recursion.round_trip_t5(t5) + assert_equal t6, BamlSdk::Recursion.round_trip_t6(t6) + end + +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_routing.rb b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_routing.rb new file mode 100644 index 00000000000..b7b59f930fd --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_routing.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class RoutingTest < Minitest::Test + def test_routing_make_foo + assert_equal 3, BamlSdk.make_foo(3).v + end + + def test_routing_round_trip_foo + f = BamlSdk::Foo.new(v: 10) + assert_equal f, BamlSdk.round_trip_foo(f) + end + + def test_routing_round_trip_thing_from_ab + t = BamlSdk::A::B::Thing.new(v: 1) + assert_equal t, BamlSdk::A::B.round_trip_thing_from_ab(t) + end + + def test_routing_round_trip_root_foo_from_ab + f = BamlSdk::Foo.new(v: 2) + assert_equal f, BamlSdk::A::B.round_trip_root_foo_from_ab(f) + end + + def test_routing_round_trip_deep_thing_from_a + t = BamlSdk::A::B::Thing.new(v: 4) + assert_equal t, BamlSdk::A.round_trip_deep_thing_from_a(t) + end + + def test_routing_round_trip_deep_thing_from_lorem + t = BamlSdk::A::B::Thing.new(v: 5) + assert_equal t, BamlSdk::Lorem.round_trip_deep_thing_from_lorem(t) + end + + def test_routing_round_trip_resume + r = BamlSdk::Lorem::Resume.new(name: "ada", email: nil) + assert_equal r, BamlSdk::Lorem.round_trip_resume(r) + end + + def test_routing_round_trip_root_foo + f = BamlSdk::Foo.new(v: 6) + assert_equal f, BamlSdk::Lorem.round_trip_root_foo(f) + end + + def test_routing_round_trip_lorem_resume_from_ipsum + r = BamlSdk::Lorem::Resume.new(name: "grace", email: "g@x.com") + assert_equal r, BamlSdk::Ipsum.round_trip_lorem_resume_from_ipsum(r) + end + + def test_routing_round_trip_resume_or_http_response + r = BamlSdk::Lorem::Resume.new(name: "lovelace", email: "a@x.com") + assert_equal r, BamlSdk::Lorem.round_trip_resume_or_http_response(r) + end + +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_symbol_collisions.rb b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_symbol_collisions.rb new file mode 100644 index 00000000000..35496a139ea --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_symbol_collisions.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class SymbolCollisionsTest < Minitest::Test + def test_symbol_collisions_round_trip_foo_bar + bar = BamlSdk::SymbolCollisions::Foo.make_foo_bar("hi", 2) + assert_equal bar, BamlSdk::SymbolCollisions::Foo.round_trip_foo_bar(bar) + end + + def test_symbol_collisions_round_trip_fizz_foo_bar + bar = BamlSdk::SymbolCollisions::Fizz::Foo.make_fizz_foo_bar("t", 1.5) + assert_equal bar, BamlSdk::SymbolCollisions::Fizz::Foo.round_trip_fizz_foo_bar(bar) + end + + def test_symbol_collisions_round_trip_fizz_buzz_foo_bar + bar = BamlSdk::SymbolCollisions::Fizz::Buzz::Foo.make_fizz_buzz_foo_bar("f", 2.5, true) + assert_equal bar, BamlSdk::SymbolCollisions::Fizz::Buzz::Foo.round_trip_fizz_buzz_foo_bar(bar) + end + + def test_symbol_collisions_round_trip_ipsum + ipsum = BamlSdk::SymbolCollisions::Lorem.make_ipsum(BamlSdk::SymbolCollisions::Foo.make_foo_bar("a", 1), BamlSdk::SymbolCollisions::Fizz::Foo.make_fizz_foo_bar("b", 2.0), BamlSdk::SymbolCollisions::Fizz::Buzz::Foo.make_fizz_buzz_foo_bar("c", 3.0, false)) + assert_equal ipsum, BamlSdk::SymbolCollisions::Lorem.round_trip_ipsum(ipsum) + end + + def test_symbol_collisions_round_trip_deep + ipsum = BamlSdk::SymbolCollisions::Lorem.make_ipsum(BamlSdk::SymbolCollisions::Foo.make_foo_bar("a", 1), BamlSdk::SymbolCollisions::Fizz::Foo.make_fizz_foo_bar("b", 2.0), BamlSdk::SymbolCollisions::Fizz::Buzz::Foo.make_fizz_buzz_foo_bar("c", 3.0, false)) + deep = BamlSdk::SymbolCollisions::A::B::C::D.make_deep(BamlSdk::SymbolCollisions::Foo.make_foo_bar("h", 9), BamlSdk::SymbolCollisions::Fizz::Foo.make_fizz_foo_bar("th", 4.0), BamlSdk::SymbolCollisions::Fizz::Buzz::Foo.make_fizz_buzz_foo_bar("fu", 5.0, true), ipsum) + assert_equal deep, BamlSdk::SymbolCollisions::A::B::C::D.round_trip_deep(deep) + end + +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_unions.rb b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_unions.rb new file mode 100644 index 00000000000..227d000b256 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_unions.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class UnionsTest < Minitest::Test + def test_unions_round_trip_null_to_end + assert_equal 1, BamlSdk::Unions.round_trip_null_to_end(1) + assert_equal "s", BamlSdk::Unions.round_trip_null_to_end("s") + assert_nil BamlSdk::Unions.round_trip_null_to_end(nil) + end + + def test_unions_round_trip_dedup + assert_equal 2, BamlSdk::Unions.round_trip_dedup(2) + assert_equal "x", BamlSdk::Unions.round_trip_dedup("x") + end + + def test_unions_round_trip_singleton_unwrap + assert_equal 7, BamlSdk::Unions.round_trip_singleton_unwrap(7) + end + + def test_unions_round_trip_optional_plus_null + assert_equal BamlSdk::Unions::T.new(v: 1), BamlSdk::Unions.round_trip_optional_plus_null(BamlSdk::Unions::T.new(v: 1)) + assert_equal "s", BamlSdk::Unions.round_trip_optional_plus_null("s") + assert_nil BamlSdk::Unions.round_trip_optional_plus_null(nil) + end + + def test_unions_round_trip_str_or_int_list + assert_equal [1, 2, 3], BamlSdk::Unions.round_trip_str_or_int_list([1, 2, 3]) + assert_equal ["a", "b"], BamlSdk::Unions.round_trip_str_or_int_list(["a", "b"]) + error = assert_raises(TypeError) { BamlSdk::Unions.round_trip_str_or_int_list([]) } + assert_includes error.message, "fits more than one" + end + + def test_unions_round_trip_t + assert_equal BamlSdk::Unions::T.new(v: 4), BamlSdk::Unions.round_trip_t(BamlSdk::Unions::T.new(v: 4)) + end + + def test_unions_round_trip_union_container + c = BamlSdk::Unions::UnionContainer.new(null_to_end: nil, dedup: "d", singleton_unwrap: 5, optional_plus_null: BamlSdk::Unions::T.new(v: 2)) + assert_equal c, BamlSdk::Unions.round_trip_union_container(c) + end + +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_void.rb b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_void.rb new file mode 100644 index 00000000000..25fa0b2afd1 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_void.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true +require "minitest/autorun" +require "baml_sdk" +ENV["BAML_BRIDGE_PATH"] = ENV.fetch("BAML_RUBY_TEST_REAL_RUNTIME") + +class VoidTest < Minitest::Test + def test_void_no_op + assert_nil BamlSdk::Void.no_op() + end + +end diff --git a/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/typed_consumer.rb b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/typed_consumer.rb new file mode 100644 index 00000000000..b1f950c8984 --- /dev/null +++ b/baml_language/sdk_tests/crates/ruby_sorbet/type_shapes/customizable/typed_consumer.rb @@ -0,0 +1,16 @@ +# typed: strict +require "sorbet-runtime" +require_relative "baml_sdk" + +T.let(BamlSdk::Primitives.return_bigint, Integer) +T.let(BamlSdk::Primitives.round_trip_float(7), Float) +T.let(BamlSdk::Primitives.round_trip_uint8_array("bytes".b), String) +T.let(BamlSdk::Aliases.round_trip_string_list(["a"]), BamlSdk::Aliases::StringList) +T.let(BamlSdk::Aliases.round_trip_rec_list([1, [2]]), BamlSdk::Aliases::RecList) +T.let(BamlSdk::Unions.round_trip_dedup("s"), T.any(Integer, String)) +T.let(BamlSdk::Enums.pick_positive, BamlSdk::Enums::Sentiment) +image = Baml::Image.from_url("https://example.com/image.png") +T.let(BamlSdk::Media.round_trip_image(image), Baml::Image) +T.let(BamlSdk::Void.no_op, NilClass) +node = BamlSdk::Recursion::IntBinaryTree.new(value: 1, left: nil, right: nil) +T.let(BamlSdk::Recursion.round_trip_int_binary_tree(node), BamlSdk::Recursion::IntBinaryTree) diff --git a/baml_language/sdk_tests/fixtures/function_calls/baml_src/ns_host_callable_tests/ruby_exception_value.baml b/baml_language/sdk_tests/fixtures/function_calls/baml_src/ns_host_callable_tests/ruby_exception_value.baml new file mode 100644 index 00000000000..36ec920069d --- /dev/null +++ b/baml_language/sdk_tests/fixtures/function_calls/baml_src/ns_host_callable_tests/ruby_exception_value.baml @@ -0,0 +1,10 @@ +function catch_ruby_exception(callback: () -> int throws baml.errors.HostCallable) -> baml.errors.HostCallable { + callback() catch (e) { + baml.errors.HostCallable => { return e; } + }; + throw "expected callback to throw" +} + +function forward_ruby_exception(value: baml.errors.HostCallable, callback: (baml.errors.HostCallable) -> int throws never) -> int { + callback(value) +} diff --git a/baml_language/sdk_tests/fixtures/function_calls/baml_src/ns_methods_on_classes/ruby_generics.baml b/baml_language/sdk_tests/fixtures/function_calls/baml_src/ns_methods_on_classes/ruby_generics.baml new file mode 100644 index 00000000000..515730b825e --- /dev/null +++ b/baml_language/sdk_tests/fixtures/function_calls/baml_src/ns_methods_on_classes/ruby_generics.baml @@ -0,0 +1,24 @@ +class BindingField { + __baml_type_args string +} + +class GenericBindingField { + __baml_type_args string + value T +} + +function binding_field(value: BindingField) -> BindingField { value } +function generic_binding_field(value: GenericBindingField) -> GenericBindingField { value } + +interface RubyNamed { + function label(self, other: X) -> string throws never; +} + +implement RubyNamed for int { + function label(self, other: int) -> string throws never { "int" } +} + +class RubyTagged> { + value T + function zero() -> int { 0 } +} diff --git a/baml_language/sdk_tests/fixtures/function_calls/baml_src/ns_methods_on_classes/ruby_hooks.baml b/baml_language/sdk_tests/fixtures/function_calls/baml_src/ns_methods_on_classes/ruby_hooks.baml new file mode 100644 index 00000000000..770d30722a2 --- /dev/null +++ b/baml_language/sdk_tests/fixtures/function_calls/baml_src/ns_methods_on_classes/ruby_hooks.baml @@ -0,0 +1,15 @@ +// Ruby calls these lifecycle hooks while loading generated declarations. +function singleton_method_added() -> int { 1 } + +class HookNames { + display string + tap string + then string + gem string + + function method_added() -> int { 2 } + function ancestors() -> int { 3 } + function decorator() -> int { 4 } +} + +function name(value: HookNames) -> HookNames { value } diff --git a/baml_language/sdk_tests/harness_runner/src/lib.rs b/baml_language/sdk_tests/harness_runner/src/lib.rs index 3357b1d8562..f257d5b0ff6 100644 --- a/baml_language/sdk_tests/harness_runner/src/lib.rs +++ b/baml_language/sdk_tests/harness_runner/src/lib.rs @@ -32,6 +32,7 @@ use std::{ mod command; pub mod csharp; pub mod fixtures; +pub mod ruby_sorbet; pub use command::{assert_stdout_contains, run_command}; diff --git a/baml_language/sdk_tests/harness_runner/src/ruby_sorbet.rs b/baml_language/sdk_tests/harness_runner/src/ruby_sorbet.rs new file mode 100644 index 00000000000..29bfe579ac3 --- /dev/null +++ b/baml_language/sdk_tests/harness_runner/src/ruby_sorbet.rs @@ -0,0 +1,117 @@ +//! Ruby's fixture gates keep each minitest file in its own process, and check +//! the entire generated package and test overlay against the gem's shipped RBI. +use std::{env, fs, path::PathBuf, process::Command}; + +use crate::{fixture_path, run_command, workspace_root_from_manifest}; + +pub fn minitest(fixture: &str) { + let generated = fixture_path(fixture, "generated"); + let mut tests: Vec<_> = fs::read_dir(&generated) + .expect("generated Ruby fixture") + .map(|entry| entry.expect("generated fixture entry").path()) + .filter(|path| { + path.file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.starts_with("test_") && name.ends_with(".rb")) + }) + .collect(); + tests.sort(); + let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR")); + let runtime = workspace_root_from_manifest(&manifest).join("sdks/ruby/bridge_ruby/lib"); + // Fixtures without ports still prove the generated SDK loads. + if tests.is_empty() { + tests.push(PathBuf::from("-e0")); + } + for test in tests { + run_command( + Command::new("ruby") + .args(["-S", "bundle", "exec", "ruby", "-I"]) + .arg(&runtime) + .arg("-I") + .arg(&generated) + .arg("-rbaml_sdk") + .arg(&test) + .current_dir(&generated), + &format!("Ruby {fixture} {}", test.display()), + ); + } +} + +/// Also used by the single negative consumer, which must reject a wrong type. +pub fn sorbet_command(fixture: &str) -> Command { + let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR")); + let workspace = workspace_root_from_manifest(&manifest); + let target = env::var_os("CARGO_TARGET_DIR") + .map_or_else(|| workspace.join("target"), |path| workspace.join(path)); + let mut command = Command::new("ruby"); + command + .args([ + "-S", + "bundle", + "exec", + "srb", + "tc", + "--no-config", + "--color=never", + "--no-error-sections", + "--no-error-count", + ]) + .env("BUNDLE_GEMFILE", manifest.join("test/static/Gemfile")) + .env("BUNDLE_PATH", target.join("ruby-sorbet-static-bundle")) + .env("BUNDLE_FROZEN", "true") + .arg(workspace.join("sdks/ruby/bridge_ruby/rbi/baml.rbi")) + .arg(manifest.join("test/static/minitest.rbi")) + .arg(".") + .current_dir(fixture_path(fixture, "generated")); + command +} + +/// Expand a Ruby check, preserving the static checker's supported platforms. +#[macro_export] +#[doc(hidden)] +macro_rules! __ruby_sorbet_gate { + (on, minitest, $body:block) => { + #[test] + fn minitest() $body + }; + (on, sorbet, $body:block) => { + #[test] + #[cfg_attr( + not(all( + any(target_arch = "x86_64", target_arch = "aarch64"), + any(target_os = "macos", all(target_os = "linux", target_env = "gnu")) + )), + ignore = "Sorbet 0.6.13506 binaries require x86_64/arm64 macOS or x86_64/aarch64 glibc Linux; runtime tests still run" + )] + fn sorbet() $body + }; + (later, $name:ident, $body:block) => { + #[test] + #[ignore = "generated Ruby API not complete enough for this fixture yet"] + fn $name() $body + }; +} + +#[macro_export] +macro_rules! ruby_sorbet_test_suite { + ( $( fixture $name:ident { minitest: $minitest:ident, sorbet: $sorbet:ident } )+ ) => { + $crate::setup_guard!("SDK_TEST_RUBY_SORBET_SETUP"); + $crate::fixture_manifest!( $( $name ),+ ); + + $( + mod $name { + $crate::__ruby_sorbet_gate!($minitest, minitest, { + $crate::ruby_sorbet::minitest(stringify!($name)); + }); + $crate::__ruby_sorbet_gate!($sorbet, sorbet, { + $crate::run_command( + &mut $crate::ruby_sorbet::sorbet_command(stringify!($name)), + concat!("Ruby ", stringify!($name), " srb tc"), + ); + }); + } + )+ + }; +} + +pub use crate::ruby_sorbet_test_suite as test_suite; diff --git a/baml_language/sdks/agent-docs/bridge-ref/ref-ruby-state-of-completeness.md b/baml_language/sdks/agent-docs/bridge-ref/ref-ruby-state-of-completeness.md new file mode 100644 index 00000000000..5e94d031d9d --- /dev/null +++ b/baml_language/sdks/agent-docs/bridge-ref/ref-ruby-state-of-completeness.md @@ -0,0 +1,130 @@ +--- +date: 2026-10-18 +repository: baml4 +--- +# State of BAML↔Ruby completeness + +The Ruby/Sorbet SDK generates typed functions, classes and enums for user code and supported standard-library symbols. It runs sync and async calls through the V1 C bridge, with call controls, generics, host callbacks, callable BAML closures, media and host-driven streaming. The tables below separate tested support from remaining gaps. + +✅ means a named Ruby test proves the stated support, 🚧 means only part of the row is tested or supported, and ❌ means unsupported or not yet parity-verified. The Python column is copied from `ref-python-state-of-completeness.md` at commit `b74b83a3ab`, including its qualifications; it is not a claim about today's Python SDK. Links to `customizable/test_*.rb` identify real-engine tests; streaming tests use the keyless replay server. Fake-bridge tests are identified separately. + +Validation: `cargo nextest run -p sdk_test_ruby_sorbet` passes all 17 harness tests, including minitest and Sorbet checks for all five shared fixtures, the fixture manifest, runtime and CLI-generated local-gem checks. Individual Ruby tests still skip callable release on engine GC and callable-subscript arity; a passing harness does not make those capabilities supported. + +## Function-call forms + +| Call form | Python | Ruby | Ruby surface and evidence | +| --- | --- | --- | --- | +| Free function (sync) | ✅ | ✅ | Generated namespace methods call the engine: [`test_main_hello_world_returns_literal`][function-main], [`test_main_single_required_arg_round_trips`][function-main]. | +| Free function (async) | ✅ | ✅ | `_async` siblings return `Baml::Call[T]`; `.wait` returns the result: [`test_cancellation_async_call_returns_none`][cancellation], [`test_four_call_forms_async`][invocation]. | +| Static method | ✅ | ✅ | Generated class methods and `_async` siblings: [`test_methods_on_classes_static_create_round_trips`][methods], [`test_methods_on_classes_static_create_async_round_trips`][methods]. | +| Instance method | ✅ | ✅ | Generated instance methods encode the receiver: [`test_methods_on_classes_instance_greet_with_arg_round_trips`][methods], [`test_methods_on_classes_instance_greet_async_with_arg_round_trips`][methods]. | +| Required args (positional) | ✅ | ✅ | Required parameters are positional: [`test_main_single_required_arg_round_trips`][function-main]. Multiple arguments: [`test_host_callables_two_arg_callable_unpacks_positional_args`][host-callables]. | +| Required args (keyword) | ✅ | n/a | Generated methods require positional arguments. Returned closures also accept named arguments, as tested by [`test_baml_closure_decodes_multiple_args_and_structured_return_values`][host-callables]. | +| Optional args (omitted → default) | ✅ | ✅ | Keywords default to `Baml::UNSET`; omitted values let BAML evaluate defaults: [`test_optional_args_runtime_matrix`][optional-args]. | +| Optional args (supplied) | ✅ | ✅ | Explicit values and `nil` differ from omission: [`test_optional_args_python_unset_and_none_differ_in_one_call`][optional-args], [`test_optional_args_opt_box_method_matrix`][optional-args]. Nullable parameters without defaults remain required: [`test_optional_args_negative_runtime_cases_reject`][optional-args]. | +| Streaming | ✅ | ✅ | `_stream` / `_stream_async` return `Baml::Stream[T]`. `next` returns a partial or generated `Done`; `each` and `final` are available: [`test_streaming_e2e_stream`][streaming], [`test_streaming_e2e_stream_async`][streaming]. Structured partials: [`test_streaming_e2e_stream_doc`][streaming], [`test_streaming_e2e_stream_doc_async`][streaming]. | +| `$build_request` companion | ✅ | ✅ | Ruby uses `_spec` / `_spec_async`, then `Baml::FunctionSpec#build_request` / `build_request_async`, not a direct `$build_request` method: [`test_streaming_e2e_stream`][streaming], [`test_streaming_e2e_stream_async`][streaming]. | +| Generic function / method (inferred) | ✅ | ✅ | Engine inference from values and receivers: [`test_generic_inference_identity_infers_primitives`][generic-inference], [`test_generic_inference_genericbox_pair_with_unbound_receiver_recovers_class_var`][generic-inference]. | +| Generic function / method (`_types=` kwarg) | ✅ | ✅ | Named tokens, e.g. `_types: { T: Integer }`: [`test_generic_calls_identity_explicit`][generic-calls], [`test_generic_calls_identity_async_explicit`][generic-calls], [`test_generic_calls_genericbox_pair_with_explicit`][generic-calls]. | +| Generic function / method (subscript) | ✅ | ❌ | No callable-subscript API; use `_types:`. [`test_generic_inference_make_triple_partial_subscript_requires_full_arity`][generic-inference] explicitly skips this surface. | +| Host callback param | ✅ | ✅ | Objects responding to `#call`, including lambdas, receive decoded arguments: [`test_host_callables_simple_sync_callable_returns_string`][host-callables], [`test_host_callables_class_callback_round_trips_pydantic_model`][host-callables]. Optional arguments use Ruby keywords: [`test_host_callables_optional_args_partially_set_deliver_by_name`][host-callables]. | + +`_baml: Baml::Options.new(timeout_ms:, cancel:)` supplies per-call controls to free functions, methods and returned closures: [`test_four_call_forms`][invocation], [`test_methods_accept_controls`][invocation], [`test_returned_callable_accepts_controls`][invocation]. Trace options are unsupported: only `trace: nil` is accepted, and [`test_invalid_timeout_rejected`][invocation] checks rejection of a non-nil trace. Watcher declarations are not generated. + +Sorbet erases `GenericBox[Integer]` at runtime. Use `Baml.bind_types(instance, Integer)` when a host-built instance must carry concrete type arguments; decoded instances retain them automatically. Explicit method bindings require a bound receiver: [`test_generic_calls_genericbox_pair_with_explicit`][generic-calls], [`test_generic_calls_instance_method_unparameterized_receiver_raises`][generic-calls]. `T::Struct#with` makes a new instance and drops the binding; bind the copy again when needed. + +## Runtime behaviors + +These rows describe what happens when a call returns, throws, or terminates. + +| Runtime behavior | Python | Ruby | Ruby outcome and evidence | +| --- | --- | --- | --- | +| Normal return | ✅ | ✅ | Decodes the `ok` value: [`test_main_hello_world_returns_literal`][function-main], [`test_class_refs_round_trip_inner`][class-refs], [`test_streaming_e2e_stream_doc_collect_in_baml`][streaming]. | +| BAML error | ✅ (docs-only) | ✅ | Raises `Baml::Error < StandardError` with decoded `value`, `type_name`, `message` and `baml_trace`: [`test_errors_union_throws_preserves_class_name`][errors], [`test_errors_baml_error_carries_baml_trace`][errors]. BAML frames appear in Ruby's backtrace: [`test_errors_baml_trace_spliced_into_python_traceback`][errors]. | +| BAML panic | ✅ | ✅ | Raises `Baml::Panic < Exception`, not `StandardError`, with a decoded payload: [`test_errors_user_panic_surfaces_as_baml_panic`][errors]. Ordinary `rescue => e` does not catch it. | +| Host-callback error | ✅ | ✅ | Re-raises the original Ruby exception by identity: [`test_host_callables_throwing_callable_keyerror_round_trips_with_identity`][host-callables]. A callback's `Baml::Error` can be caught in BAML or return with typed fields: [`test_host_callables_throwing_callable_bamlerror_wrapping_codegenned_class_is_caught_in_baml`][host-callables], [`test_host_callables_throwing_callable_bamlerror_propagates_back_with_typed_fields`][host-callables]. | +| Cancellation | ✅ (async only) | ✅ | Tokens from `BamlSdk::Baml::Spawn::CancelToken.baml_new` cancel sync and async calls; `Call#cancel` cancels an async call. Cancellation raises `Baml::CancelledError < Baml::Panic`: [`test_cancellation_sync_cancel_via_call_context`][cancellation], [`test_cancellation_async_cancel_via_call_context`][cancellation], [`test_cancellation_async_cancel_via_task_cancel`][cancellation]. A `wait(timeout:)` timeout raises `Baml::WaitTimeout` without cancelling; `timeout_ms` cancels the engine call: [`test_cancellation_async_cancel_via_asyncio_timeout`][cancellation]. | +| OS exit | ✅ | ✅ | `baml.sys.exit` terminates the process without Ruby `at_exit` hooks: [`test_errors_clean_exit_terminates_process_with_code_0`][errors], [`test_errors_clean_exit_terminates_process_with_code_7`][errors]. | + +The fake-bridge test [`test_call_waiters_cache_outcomes_and_survive_interrupt_and_cancel`][bridge-call] checks multiple waiters, cached results and errors, an interrupted waiter, foreign-thread completion and a cancellation race. Host callbacks run on their own Ruby threads. Cancellation does not kill a running callback or free its resources before it returns: fake-bridge [`test_cancelled_host_callback_keeps_resources_until_proc_returns`][bridge-call]. Nested calls do not inherit the callback's invocation context. + +Generated packages initialize on their first call or through `initialize!`. The [clean-app smoke script][gem-smoke] generates with `baml generate`, then proves the first call initializes with embedded `baml.toml` metadata. Loading a different program in the same process and using an inherited runtime after fork raise errors: fixture-backed [`test_different_program`][loader], [`test_pid_check`][loader]. + +The `baml-bridge` gem is pure Ruby, with committed protobuf clients and dependencies on `ffi`, `google-protobuf` and `sorbet-runtime`. The [clean-app smoke script][gem-smoke], run by `gem::clean_app`, builds and installs it locally outside the checkout and calls an SDK generated by `baml-cli generate` with `output_type = "ruby/sorbet"`. The gem neither bundles nor downloads the native bridge; supply it through `BAML_BRIDGE_PATH` or the versioned `$BAML_HOME/bridges/` cache. Publishing and automatic bridge download remain out of scope. + +## Static typing + +The [shared Ruby harness][static-gate] runs `srb tc` on each of the five generated packages and its tests with Sorbet **0.6.13506**, pinned in a separate [test bundle][static-gemfile]. Every generated Ruby file, including bytecode and model declarations, uses `# typed: strict`. No SDK functions or models are recreated in test RBIs. + +Strict consumers in the fixture overlays cover sync and async returns, stdlib methods, callbacks and returned callables, optional arguments, call controls, generics, streams, specs, bigint, bytes, media, unions, aliases, classes and enums. One negative consumer requires exactly one wrong-argument diagnostic (`7002`), so a regression to `T.untyped` fails the gate. + +The gem ships the [boundary RBI][static-bridge], which describes the runtime-facing API, including call, stream and generic helpers; it does not recreate generated SDK declarations. On supported hosts, the [clean-app smoke script][gem-smoke] also runs `srb tc` on the installed app, using the installed gem's RBI and Tapioca's default `--ignore=vendor/`. Generated vendor packages live under `baml_sdk/__vendor/`, so that ignore does not hide them. The bridge's decoded return remains dynamic, so this gate proves generated declarations and consumer checking, not the runtime's adherence to BAML return types. Real-engine and replay tests cover runtime behavior separately, including recursion, forward references and symbol collisions. + +Typing still has limits: literal values use their Ruby primitive types, recursive alias back-edges and JSON use `T.untyped`, and `_types:` token values are dynamic. Callable signatures accept typed Procs or `Baml::Closure`. Generated methods with callable parameters or results disable Sorbet's runtime signature checks so other callable objects also work. Other method signatures retain Sorbet's lazy validation. + +The pinned checker runs on x86_64/arm64 macOS and x86_64/aarch64 glibc Linux. Windows, 32-bit Linux and musl hosts explicitly ignore the static test because no compatible binary is supplied; their runtime tests still run. Setup installs the checker under `target/ruby-sorbet-static-bundle` only on supported hosts, and installation failures there fail setup. + +## Value kinds + +Encode means Ruby → BAML; decode means BAML → Ruby. The generated-package tests make real calls. + +| Value kind | Python | Ruby | Ruby value and evidence | +| --- | --- | --- | --- | +| Null | ✅ | ✅ | `nil`: [`test_primitives_round_trip_null`][primitives]. Void returns also become `nil`: [`test_void_no_op`][void]. | +| Bool | ✅ | ✅ | `true` / `false`: [`test_primitives_round_trip_bool`][primitives]. | +| Int (Int63) | ✅ | ✅ | `Integer` within −2⁶²..2⁶²−1: [`test_primitives_round_trip_int`][primitives]. | +| Bigint | ✅ | ✅ | Arbitrary-precision `Integer`: [`test_primitives_round_trip_bigint`][primitives] covers positive and negative 2⁸⁰ and the 2⁶⁴ boundary; [`test_primitives_return_bigint`][primitives] checks decode. | +| Float | ✅ | ✅ | `Float`: [`test_primitives_round_trip_float`][primitives]. Integer inputs widen only when exactly representable (`2**53 + 1` raises `TypeError`): [`test_primitives_round_trip_float_accepts_int`][primitives]. | +| String | ✅ | ✅ | `String`: [`test_primitives_round_trip_string`][primitives]. | +| Bytes | ✅ | ✅ | Binary `String`, encoded using the declared bytes hint. When a union allows both string and bytes, binary encoding selects bytes: [`test_primitives_round_trip_uint8_array`][primitives] covers a real round trip and protocol selection through nested containers and aliases. | +| List | ✅ | ✅ | `Array`, including empty and mixed-element lists: [`test_lists_round_trip_ints`][lists], [`test_lists_round_trip_empty_list`][lists], [`test_lists_round_trip_union_list`][lists]. | +| Map | ✅ | ✅ | `Hash`: [`test_maps_round_trip_simple_map`][maps], [`test_maps_round_trip_list_valued_map`][maps]. | +| Enum | ✅ | ✅ | Generated `T::Enum`: [`test_enums_round_trip_sentiment`][enums], [`test_enums_round_trip_sentiment_positive`][enums]. Dynamic enums are unsupported. | +| Class | ✅ | ✅ | Generated `T::Struct`, registered by BAML type name: [`test_class_refs_round_trip_inner`][class-refs], [`test_streaming_e2e_stream_doc_collect_in_baml`][streaming]. | +| Generic explicitly reified by BAML-known type | ✅ | ✅ | `Baml.bind_types` attaches runtime type arguments to generated generic structs: [`test_generic_calls_identity_explicit`][generic-calls]. Decoded type arguments are retained, including nested ones: [`test_generic_calls_make_int_box_reified`][generic-calls], [`test_generic_calls_make_nested_box_reified`][generic-calls]. | +| Generic implicitly reified by BAML-known type | ✅ | ✅ | The engine infers from unbound instance fields: [`test_generic_inference_identity_unbound_generic_instance_round_trips`][generic-inference], [`test_generic_inference_extract_fully_unbound_nested_pair_recovers_all_vars`][generic-inference]. | +| Generic reified by host-only type | ❌ (need more rules for serializing Python-only types) | ❌ | Unregistered Ruby objects cannot be encoded: [`test_generic_inference_host_only_object_not_encodable_from_python`][generic-inference]. | +| Union | ✅ (union metadata dropped) | ✅ | Decodes to the Ruby variant value; metadata is dropped. Ambiguous inputs, such as `[]` for `int[] \| string[]`, raise `TypeError` instead of choosing an arm. Generated signatures use `T.any` / `T.nilable`: [`test_unions_round_trip_null_to_end`][unions], [`test_unions_round_trip_optional_plus_null`][unions], [`test_unions_round_trip_union_container`][unions]. | +| BAML interface | ❌ | ❌ | No interface value support; affected declarations are not generated. | +| Media | ✅ | ✅ | Frozen value objects `Baml::Image`, `Audio`, `Video` and `Pdf`, not engine handles. URL round trips: [`test_media_round_trip_media`][media]. Base64, file contents, names, MIME types and local-file loading: [`test_handles_image_from_base64_roundtrips_payload`][handles]. | +| Stream | ✅ | ✅ | `Baml::Stream[T]` owns an engine handle and supports sync/async `next` and `final`, plus `each` and `close`: [`test_streaming_e2e_stream`][streaming], [`test_streaming_e2e_stream_async`][streaming]. | +| Host callable | ✅ | ✅ | Ruby callable objects and lambdas are registered and dispatched with decoded values: [`test_host_callables_simple_sync_callable_returns_string`][host-callables], [`test_host_callables_class_callback_round_trips_pydantic_model`][host-callables]. Release on engine GC remains unverified; [`test_host_callables_release_fires_on_drop_of_callable`][host-callables] is skipped. | +| Host callable (async) | ✅ (works, but returned coroutine is driven to completion on new asyncio event loop) | ❌ | Returned async work is not awaited by the bridge. [`test_host_callables_async_callable_runs_to_completion`][host-callables] joins its own thread with `.value`; it does not prove automatic async callback support. | +| BAML closure (function reference) | ❌ | ❌ | The runtime maps `FUNCTION_REF` handles to `Baml::Closure`, but no named Ruby test covers a bare BAML function reference. | +| BAML closure (closure / bound method / generic function / function) | ❌ | 🚧 | Returned closures support `call`, `call_async`, positional/keyword arguments and mutable captures: [`test_baml_closure_is_a_native_callable_with_host_language_arguments`][host-callables], [`test_baml_closure_decodes_multiple_args_and_structured_return_values`][host-callables], [`test_baml_closure_is_reusable_and_retains_mutable_captures`][host-callables]. Bound-method and generic-function values are not parity-verified; specialized closures reject `_types:`. | +| BAML type reference values | ❌ | ❌ | No first-class BAML type-value encoding or decoding. `Baml::Types` tokens are call metadata, not ordinary argument values. | +| BAML type definition values | ❌ | ❌ | No type-definition values. | +| BAML `$rust_type` values: files, sockets, etc. | ✅ | 🚧 | Generated resource structs retain `Baml::Handle` fields. Files preserve cursor state: [`test_handles_open_file_returns_file_handle`][handles], [`test_handles_file_cursor_state_persists_across_calls`][handles]. HTTP response fields and methods: [`test_handles_http_get_response_fields_and_methods`][handles]. Socket values are not parity-verified. | +| Native exception thrown by a host callback | ✅ | ✅ | Opaque host-value keys preserve the original exception object and custom fields: [`test_host_callables_throwing_callable_custom_python_exception_round_trips_with_identity`][host-callables]. | +| Unused SDK heap handles (`Handle`) | 🚧 | 🚧 | Handles work inside tested streams and resources: [`test_streaming_e2e_stream`][streaming], [`test_handles_file_cursor_state_persists_across_calls`][handles]. Standalone unused heap-handle ownership and GC release are not parity-verified. | +| Unused SDK collector values (`Collector`) | 🚧 | ❌ | No collector wrapper or verified collector-value path. | +| Unused SDK prompt values (`PromptAst`) | 🚧 | ❌ | No prompt-value support. | +| BAML builtin type (`Future` / `UnscheduledFuture`) | ❌ | ❌ | No first-class engine future values. `Baml::Call` is the host call promise, not a BAML future value. | +| Arbitrary unsupported host object | ❌ | ❌ | Encoding raises `TypeError`: [`test_generic_inference_host_only_object_not_encodable_from_python`][generic-inference]. | +| Cyclic / self-referential objects | ❌ | ❌ | No cyclic-object encoding or identity preservation. Recursive type declarations and acyclic recursive values do not provide cycle handling. | + +[function-main]: ../../../sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_main.rb +[errors]: ../../../sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_errors.rb +[streaming]: ../../../sdk_tests/crates/ruby_sorbet/llm_functions/customizable/test_streaming_e2e.rb +[class-refs]: ../../../sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_class_refs.rb +[void]: ../../../sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_void.rb +[bridge-call]: ../../../sdk_tests/crates/ruby_sorbet/test/bridge_call_test.rb +[static-gemfile]: ../../../sdk_tests/crates/ruby_sorbet/test/static/Gemfile +[static-bridge]: ../../ruby/bridge_ruby/rbi/baml.rbi +[static-gate]: ../../../sdk_tests/harness_runner/src/ruby_sorbet.rs +[methods]: ../../../sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_methods_on_classes.rb +[optional-args]: ../../../sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_optional_args.rb +[cancellation]: ../../../sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_cancellation.rb +[invocation]: ../../../sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_invocation.rb +[generic-calls]: ../../../sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_generic_calls.rb +[generic-inference]: ../../../sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_generic_inference.rb +[host-callables]: ../../../sdk_tests/crates/ruby_sorbet/function_calls/customizable/test_host_callables.rb +[primitives]: ../../../sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_primitives.rb +[lists]: ../../../sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_lists.rb +[maps]: ../../../sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_maps.rb +[enums]: ../../../sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_enums.rb +[unions]: ../../../sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_unions.rb +[media]: ../../../sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_media.rb +[handles]: ../../../sdk_tests/crates/ruby_sorbet/type_shapes/customizable/test_handles.rb +[loader]: ../../../sdk_tests/crates/ruby_sorbet/test/bridge_loader_test.rb +[gem-smoke]: ../../../sdk_tests/crates/ruby_sorbet/test/gem_smoke.rb diff --git a/baml_language/sdks/ruby/bridge_ruby/Gemfile b/baml_language/sdks/ruby/bridge_ruby/Gemfile index ab0bfee05ca..4cfe6252ff4 100644 --- a/baml_language/sdks/ruby/bridge_ruby/Gemfile +++ b/baml_language/sdks/ruby/bridge_ruby/Gemfile @@ -1,3 +1,5 @@ source "https://rubygems.org" gem "ffi", ">= 1.17.4", "< 2" +gem "google-protobuf", "~> 4.0" +gem "sorbet-runtime", "~> 0.5" diff --git a/baml_language/sdks/ruby/bridge_ruby/LICENSE b/baml_language/sdks/ruby/bridge_ruby/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/baml_language/sdks/ruby/bridge_ruby/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/baml_language/sdks/ruby/bridge_ruby/README.md b/baml_language/sdks/ruby/bridge_ruby/README.md index 977d57c88fb..d93cf85789c 100644 --- a/baml_language/sdks/ruby/bridge_ruby/README.md +++ b/baml_language/sdks/ruby/bridge_ruby/README.md @@ -1,11 +1,96 @@ -# BAML Ruby V1 bridge +# BAML Ruby/Sorbet runtime -This directory contains private runtime plumbing for generated Ruby/Sorbet SDKs. Generated code calls: +The `baml-bridge` gem runs generated BAML Ruby/Sorbet SDKs. It contains pure Ruby code and the committed protobuf clients, and depends on `ffi`, `google-protobuf` and `sorbet-runtime`. Ruby 3.2 or newer is required. + +Add `gem "baml-bridge"` to your application's Gemfile. Add a Ruby generator to your BAML project and generate the SDK: + +```sh +baml generate add ruby/sorbet +baml generate --output-dir ./generated +``` + +The generator uses `output_type = "ruby/sorbet"` and `naming_convention = "language"` in `baml.toml`. It embeds the program and runtime metadata in `generated/baml_sdk.rb` and `generated/baml_sdk/`. Put `generated/` on the application's load path: ```ruby -Baml::Bridge.initialize!(compiled_program_bytes) +require "baml_sdk" + +puts BamlSdk.hello_world +call = BamlSdk.hello_world_async +puts call.wait ``` -The bridge loads the absolute library path in `BAML_RUNTIME_PATH`, validates the complete V1 C table, requires an exact canonical BAML toolchain version, registers `Baml::Bridge` as bridge language `10` with its stamped bridge runtime version, and initializes one exact generated program per process. +Generated functions initialize the runtime on their first call. `BamlSdk.initialize!` initializes it eagerly. The process can load only one generated BAML program; a forked child must exec before using an inherited runtime. + +## Sorbet types + +The gem ships `rbi/baml.rbi`, the runtime contract used to check generated code. +Tapioca includes it when it generates the gem's RBI. Without Tapioca, copy it to +your application's `sorbet/rbi/` directory, or add its installed path to +`sorbet/config`: + +```sh +echo "$(bundle info --path baml-bridge)/rbi/baml.rbi" >> sorbet/config +``` + +Check it alongside your generated `baml_sdk.rb` and `baml_sdk/` sources. + +Generated structs compare by their fields and bound generic type arguments. +Function types accept either a typed Proc or a `Baml::Closure` returned by BAML. + +## Generic values and calls + +Generated generic classes use Sorbet's normal `type_member` and `[]`. The engine +infers generic function arguments from values where possible. To bind them +explicitly, pass named tokens, for example `identity(5, _types: { T: Integer })`. +Tokens include `Integer`, `String`, `Float`, `T::Boolean`, `NilClass`, generated +classes and enums, and `Baml::Types.list(t)`, `.map(k, v)`, `.optional(t)` and +`.generic(Klass, *args)` for compound types. Sorbet's erased `Klass[T]` is not a +runtime token for a parameterized class; use `Baml::Types.generic(Klass, T)`. + +For a host-built instance that must retain concrete runtime arguments, use: + +```ruby +box = Baml.bind_types(BamlSdk::GenericTests::GenericBox[Integer].new(value: 5), Integer) +box.pair_with("hello", _types: { U: String }) +``` + +`bind_types` returns the same instance and preserves its Sorbet type. Decoded +instances retain their runtime arguments automatically. `Baml::Types.instance_args` +returns these bindings in canonical form, with optionals and unions flattened. +`T::Struct#with` creates a new instance and drops this binding, so bind the copy again when needed. +Explicit method bindings require a bound receiver; bare calls can infer from +an unbound receiver's fields. + +## Native library lookup + +The gem does not include or download the native bridge library. Install a bridge built for the gem's canonical BAML toolchain version in one of these locations: + +1. Set `BAML_BRIDGE_PATH` to the absolute library filename. This overrides the cache. An invalid override is an error, with no fallback. +2. Otherwise, install the library at `$BAML_HOME/bridges///`. `BAML_HOME` defaults to `~/.baml`. An empty environment variable is treated as unset. + +For example, version `0.23.0` on x86-64 glibc Linux uses: + +```text +~/.baml/bridges/0.23.0/x86_64-unknown-linux-gnu/libbridge_cffi.so +``` + +Targets match the Rust bridge cache: `x86_64` or `aarch64`, followed by `apple-darwin`, `unknown-linux-gnu`, `unknown-linux-musl`, or `pc-windows-msvc`. The filename is `libbridge_cffi.dylib` on macOS, `libbridge_cffi.so` on Linux, or `bridge_cffi.dll` on Windows. The loader derives the target from Ruby's `RbConfig` and checks the bridge ABI and canonical toolchain version before use. + +## Build locally + +From this directory: + +```sh +gem build baml-bridge.gemspec +gem install --local baml-bridge-*.gem +``` + +The gemspec reads `lib/baml/bridge/version.rb`, which `scripts/baml-language-version` stamps during releases. RubyGems normalizes prerelease versions for the package; the native cache still uses the canonical version, such as `0.23.1-nightly.20261101.a`. + +The committed `google-protobuf` clients under `lib/baml_bridge/cffi/v1/` come from the authoritative CFFI V1 schemas. Regenerate them from `baml_language/` with: + +```sh +cargo build -p bridge_ctypes +``` -This checkpoint deliberately has no public gem packaging, runtime discovery, download, unload, reset, or project-switching API. Packaging and the public gem name remain release work. +A clean regeneration must leave these clients unchanged. Publishing the gem and downloading the bridge are separate release work. diff --git a/baml_language/sdks/ruby/bridge_ruby/baml-bridge.gemspec b/baml_language/sdks/ruby/bridge_ruby/baml-bridge.gemspec new file mode 100644 index 00000000000..ad017eb26dc --- /dev/null +++ b/baml_language/sdks/ruby/bridge_ruby/baml-bridge.gemspec @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "lib/baml/bridge/version" + +Gem::Specification.new do |spec| + spec.name = "baml-bridge" + spec.version = Baml::Bridge::BRIDGE_RUNTIME_VERSION + spec.authors = ["BoundaryML"] + spec.email = ["contact@boundaryml.com"] + spec.summary = "Runtime for generated BAML Ruby/Sorbet SDKs" + spec.homepage = "https://github.com/BoundaryML/baml" + spec.license = "Apache-2.0" + spec.required_ruby_version = ">= 3.2" + + spec.files = Dir.chdir(__dir__) { Dir["lib/**/*", "rbi/**/*", "README.md", "LICENSE"].select { |path| File.file?(path) } } + spec.require_paths = ["lib"] + + spec.add_dependency "ffi", ">= 1.17.4", "< 2" + spec.add_dependency "google-protobuf", "~> 4.0" + spec.add_dependency "sorbet-runtime", "~> 0.5" +end diff --git a/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge.rb b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge.rb index ed346d9cdae..e3952418229 100644 --- a/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge.rb +++ b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge.rb @@ -2,7 +2,14 @@ require_relative "bridge/version" require_relative "bridge/errors" +require_relative "options" require_relative "bridge/native" +require_relative "bridge/call" +require_relative "bridge/handle" +require_relative "bridge/types" +require_relative "value_equality" +require_relative "bridge/protocol" +require_relative "bridge/host_callbacks" require_relative "bridge/process_runtime" module Baml @@ -10,8 +17,32 @@ module Bridge @process_runtime = ProcessRuntime.new class << self - def initialize!(compiled_program_bytes) - @process_runtime.initialize!(compiled_program_bytes) + def register_program(bytecode, metadata = nil) + @process_runtime.register_program(bytecode, metadata) + end + + def define_class(wire, klass, fields:, crossing: :fields, type_params: []) + Protocol.define_class(wire, klass, fields: fields, crossing: crossing, type_params: type_params) + end + + def define_enum(wire, klass) + Protocol.define_enum(wire, klass) + end + + def define_alias(wire, descriptor, recursive: false) + Protocol.define_alias(wire, descriptor, recursive: recursive) + end + + def define_function(wire, params:, class_type_params: [], type_params: []) + Protocol.define_function(wire, params: params, class_type_params: class_type_params, type_params: type_params) + end + + def call_async(wire, args, types: nil, baml: nil) + @process_runtime.call_async(wire, args, types: types, baml: baml) + end + + def initialize! + @process_runtime.initialize! end end diff --git a/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/call.rb b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/call.rb new file mode 100644 index 00000000000..3c325ffd9a3 --- /dev/null +++ b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/call.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require "sorbet-runtime" +require "thread" + +module Baml + class WaitTimeout < StandardError; end + + class Call + extend T::Generic + Value = type_member + + def initialize(api, call_id) + @api = api + @call_id = call_id + @mutex = Mutex.new + @condition = ConditionVariable.new + @state = :pending + @decoded = false + @cancel_requested = false + end + + def wait(timeout: nil) + @api.ensure_not_forked! + deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout if timeout + @mutex.synchronize do + while @state == :pending + remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC) if deadline + raise WaitTimeout, "BAML call is still running" if remaining && remaining <= 0 + + @condition.wait(@mutex, remaining) + end + # A waiter's interrupt must not become the shared decoding error. + Thread.handle_interrupt(Exception => :never) do + unless @decoded + begin + raise @outcome if @outcome.is_a?(Exception) + + @value = Bridge::Protocol.decode_result(@outcome, @api) + rescue Exception => error # rubocop:disable Lint/RescueException -- cache the engine outcome + @error = error + end + @decoded = true + @outcome = nil + end + end + raise @error if @error + + @value + end + end + + def cancel + @mutex.synchronize do + return false if @cancel_requested || @state == :settled + + # Acquiring the mutex is interruptible; once flagged, cancellation must + # reach the bridge even if an interrupt arrives before the native call. + Thread.handle_interrupt(Exception => :never) do + @cancel_requested = true + @api.cancel_function_call(@call_id).zero? + end + end + end + + def done? + @mutex.synchronize { @state == :settled } + end + + # Internal: the callback delivers an envelope owning its handle references + # and pinned host values (or an error). Waiters build and cache Ruby values. + def complete(outcome) + @mutex.synchronize do + return if @state == :settled + + @outcome = outcome + @state = :settled + @condition.broadcast + end + end + end +end diff --git a/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/encode/commit.rb b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/encode/commit.rb new file mode 100644 index 00000000000..8b067ece3e5 --- /dev/null +++ b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/encode/commit.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +module Baml + module Bridge + module Encode + module Commit + WIRE_LIMIT = 100 + ARGUMENT_DEPTH = 2 + module_function + + def depth(node) + statement = %i[list map class callable].include?(node.kind) ? 1 + Slot.wire_depth(Slot.to_wire(node.ty)) : 0 + payload = case node.kind + when :enum, :handle, :host_object, :callable then 1 + when :media then node.value.name ? 2 : 1 + when :list then 1 + (node.children.map { |child| 1 + depth(child) }.max || 0) + when :map, :class then 1 + (node.children.map { |_, child| 2 + depth(child) }.max || 0) + else 0 + end + [statement, payload].max + end + + def check_depth!(depth, path, enclosing: ARGUMENT_DEPTH) + raise Fault, "#{path}: nested too deeply" if enclosing + depth > WIRE_LIMIT + end + + def media(node) + value = node.value + fields = { media: node.ty[1].upcase } + fields[:mime_type] = value.mime_type if value.mime_type + if value.url + fields[:url] = value.url + elsif value.name + fields[:file_content] = Wire::BamlValueMediaFileContent.new(name: value.name, base64: value.base64) + else + fields[:base64] = value.base64 + end + Wire::BamlValueMedia.new(**fields) + end + + def commit(node, scope: nil) + fields = case node.kind + when :null then {} + when :int, :float, :bool, :string then { "#{node.kind}_value".to_sym => node.value } + when :bigint then { bigint_value: node.value.to_s(16) } + when :bytes then { uint8array_value: node.value.b } + when :media then { media_value: media(node) } + when :enum + { enum_value: Wire::InboundEnumValue.new(name: node.ty[1], value: node.value) } + when :callable + key = HostValues.register(node.value, scope, node.ty) + { value_type: Slot.to_wire(node.ty), handle: Wire::BamlHandle.new(key: key, handle_type: :HOST_VALUE_CALLABLE) } + when :host_object + key = HostValues.register(node.value, scope) + { handle: Wire::BamlHandle.new(key: key, handle_type: :HOST_VALUE_OPAQUE) } + when :handle + raise Error, "BAML handles can only be sent inside a call" unless scope + + key, kind = node.value.clone_for_wire(scope) + { handle: Wire::BamlHandle.new(key: key, handle_type: kind) } + when :list + { value_type: Slot.to_wire(node.ty), list_value: Wire::InboundListValue.new( + values: node.children.map { |child| commit(child, scope: scope) } + ) } + when :map, :class + entries = node.children.map do |key, child| + Wire::InboundMapEntry.new(string_key: key, value: commit(child, scope: scope)) + end + payload = if node.kind == :map + { map_value: Wire::InboundMapValue.new(entries: entries) } + else + { class_value: Wire::InboundClassValue.new(fields: entries) } + end + { value_type: Slot.to_wire(node.ty), **payload } + else raise Error, "Undecided BAML encoding node #{node.kind}" + end + Wire::InboundValue.new(**fields) + end + end + end + end +end diff --git a/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/encode/plan.rb b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/encode/plan.rb new file mode 100644 index 00000000000..e2e98f2179d --- /dev/null +++ b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/encode/plan.rb @@ -0,0 +1,479 @@ +# frozen_string_literal: true + +module Baml + module Bridge + module Encode + Node = Struct.new(:kind, :ty, :value, :children, :path, :evidence, keyword_init: true) + + # Fields learn one binding per parameter. A known union member absorbs + # a variable as never; empty containers defer to later concrete evidence. + class Solution + def initialize(work: nil, substitutions: nil) + @work = work + @substitutions = substitutions + @bindings = {} + @conflicts = {} + end + + def learn(template, found, type_argument: false) + if found[0] == :absorbed && template[0] != :union + absorb(template) + return + end + + case template[0] + when :param + index = template[1] + bound = @bindings[index] + return if bound == :absorbed && found == [:hole] + + if !bound || bound == :absorbed || (Slot.open?(bound, &@work) && !Slot.open?(found, &@work)) + @bindings[index] = found + elsif !Slot.equivalent?(bound, found, &@work) && !Slot.open?(found, &@work) + @conflicts[index] = true + end + when :list + learn(template[1], found[1], type_argument: type_argument) if found[0] == :list + when :map + if found[0] == :map + learn(template[1], found[1], type_argument: type_argument) + learn(template[2], found[2], type_argument: type_argument) + end + when :class + if found[0] == :class && template[1] == found[1] && template[2].length == found[2].length + template[2].zip(found[2]).each { |arg, actual| learn(arg, actual, type_argument: true) } + end + when :union + open, fixed = template[1].partition { |member| Slot.mentions_param?(member, &@work) } + return unless open.length == 1 + + if type_argument && found[0] != :absorbed + # A nested class solves its own parameters, without choosing the + # parent's argument union. Only type evidence crosses that boundary. + known = fixed.to_h { |member| [Slot.canonical(member, &@work), true] } + rest = (found[0] == :union ? found[1] : [found]).reject do |member| + known.key?(Slot.canonical(member, &@work)) + end + found = rest.empty? ? [:absorbed] : Slot.union(rest, &@work) + end + if found[0] == :absorbed + absorb(open.first) + else + learn(open.first, found, type_argument: type_argument) + end + when :function + if found[0] == :function && template[1].length == found[1].length + template[1].zip(found[1]).each { |param, actual| learn(param[1], actual[1], type_argument: type_argument) } + learn(template[2], found[2], type_argument: type_argument) + learn(template[3], found[3], type_argument: type_argument) + end + end + end + + def binding(index, inference: false) + bound = @bindings[index] + bound == :absorbed ? [inference ? :absorbed : :never] : bound + end + + def conflict?(index) + @conflicts[index] + end + + def apply(template) + bindings = @bindings.to_h { |index, _| [index, binding(index)] } + Slot.substitute(template, bindings, memo: @substitutions, &@work) + end + + private + + def absorb(template) + Slot.params(template, &@work).each do |param| + bound = @bindings[param[1]] + @bindings[param[1]] = :absorbed if !bound || bound == [:hole] + end + end + end + + class Plan + # Ruby has no memo, and a step costs about 5 µs: use less fuel than Rust. + STEP_BUDGET = 1 << 20 + + def initialize(registry, budget: STEP_BUDGET) + @registry = registry + @remaining = budget + @work = ->(steps = 1) { spend(steps, @path || "value") } + @members = {}.compare_by_identity + @substitutions = {}.compare_by_identity + @normalized = {}.compare_by_identity + end + + def plan_root(tree, slot) + planned = plan(tree, slot) + if !%i[handle host_object].include?(planned.kind) && Slot.open?(planned.ty, &@work) && Slot.mentions_param?(slot, &@work) + name = Slot.params(slot, &@work).first[2] + raise UnboundTypeArgs, "#{tree.path}: could not infer a type for `#{name}`; pass the call's type arguments" + end + restate(planned, fill_type(planned.ty)) + end + + def plan(tree, slot) + @path = tree.path + plan_normalized(tree, Slot.stated_form(slot, @registry, memo: @normalized, &@work)) + end + + private + + def plan_normalized(tree, slot) + @path = tree.path + spend(1, tree.path) + slot = Slot.unfold(slot, @registry, &@work) + return choose(tree, slot[1]) if slot[0] == :union + return node(tree, [:opaque]) if %i[handle host_object].include?(tree.kind) + if %i[unknown interface].include?(slot[0]) + own = natural(tree) + return restate(own, fill_type(own.ty)) + end + + case slot[0] + when :param, :hole, :opaque + return natural(tree) + when :null + return node(tree, [:null]) if tree.kind == :null + when :int, :bigint, :float, :bool, :string, :bytes + if (fit = Rules.fit(tree.value, slot[0])) && scalar?(tree) + return node(tree, [slot[0]], kind: slot[0], value: fit[1]) + end + when :literal + expected = slot.last + kind = Slot.literal_kind(slot) + if %i[int bigint string bool].include?(kind) && scalar?(tree) && + (fit = Rules.fit(tree.value, kind)) && fit[1] == expected + return node(tree, [kind], kind: kind, value: fit[1]) + end + when :enum, :enum_variant + if (tree.kind == :enum && tree.name == slot[1]) || tree.kind == :string + if @registry.enum_values(slot[1]).include?(tree.value) + refuse(tree, "variant does not match #{slot[2]}") if slot[0] == :enum_variant && tree.value != slot[2] + return node(tree, [:enum, slot[1]], kind: :enum) + end + refuse(tree, "unknown variant #{tree.value.inspect} of #{slot[1]}") + end + when :list + if tree.kind == :list + inner, found = solve_elements(tree.children, slot[1], tree.path) + return node(tree, [:list, inner], children: tree.children.map { |child| plan_normalized(child, inner) }, + evidence: found && [:list, found]) + end + when :map + if tree.kind == :map + key_slot = Slot.mentions_param?(slot[1], &@work) ? [:string] : slot[1] + inner, found = solve_elements(tree.children.map(&:last), slot[2], tree.path) + entries = tree.children.map do |key, child| + [map_key(key, key_slot), plan_normalized(child, inner)] + end + return node(tree, [:map, key_slot, inner], children: entries, evidence: found && [:map, key_slot, found]) + end + when :class + return instance(tree, slot) if tree.kind == :class && tree.name == slot[1] + when :function + if tree.kind == :callable + throws = Slot.mentions_param?(slot[3], &@work) ? [:unknown] : slot[3] + signature = [:function, slot[1], slot[2], throws] + holes = Slot.params(signature, &@work).to_h { |param| [param[1], [:hole]] } + return node(tree, Slot.substitute(signature, holes, &@work)) + end + end + return node(tree, [:media, tree.name]) if tree.kind == :media && media_fits?(tree, slot) + refuse(tree, "cannot encode #{tree.kind} as #{slot.inspect}") + end + + def spend(steps, path) + @remaining -= steps + raise Fault, "#{path}: encoding exceeds the planning-step budget" if @remaining.negative? + end + + def scalar?(tree) + %i[int bigint float bool string bytes].include?(tree.kind) + end + + def refuse(tree, message) + raise Refusal, "#{tree.path}: #{message}" + end + + def node(tree, ty, kind: tree.kind, value: tree.value, children: nil, evidence: nil) + Node.new(kind: kind, ty: ty, value: value, children: children, path: tree.path, evidence: evidence) + end + + # Evidence follows the original arm choices, not the wire types rebuilt + # after solving a container. :absorbed is inference-only, never a wire type. + def evidence(node) + node.evidence || node.ty + end + + def solve_elements(children, template, path) + return [template, nil] unless Slot.mentions_param?(template, &@work) + + planned = children.map { |child| plan_normalized(child, template) } + spend(children.length, path) + found = join(planned, inference: true) + solved = Solution.new(work: @work, substitutions: @substitutions) + solved.learn(template, found) + result = solved.apply(template) + holes = Slot.params(result, &@work).to_h { |param| [param[1], [:hole]] } + [Slot.stated_form(Slot.substitute(result, holes, memo: @substitutions, &@work), @registry, memo: @normalized, &@work), found] + end + + def instance(tree, slot = nil) + params = @registry.class_type_params(tree.name) + fields = @registry.encoding_fields(tree.name) + args = slot ? slot[2] : Array.new(params.length) + unless args.length == params.length + raise Fault, "#{tree.path}: wrong number of type arguments for #{tree.name}" + end + fixed = args.map { |arg| arg unless arg && (Slot.mentions_param?(arg, &@work) || Slot.open?(arg, &@work)) } + unless tree.type_args.empty? + own = tree.type_args.map { |arg| Slot.stated_form(arg, @registry, memo: @normalized, &@work) } + unless own.length == params.length && own.zip(fixed).all? { |arg, known| !known || Slot.equivalent?(arg, known, &@work) } + refuse(tree, "instance type arguments do not match #{slot.inspect}") + end + fixed = own + end + unless fixed.all? + solved = Solution.new(work: @work, substitutions: @substitutions) + tree.children.each do |name, child| + template = Slot.stated_form(Slot.substitute(fields.fetch(name)[1], fixed, memo: @substitutions, &@work), @registry, memo: @normalized, &@work) + planned = plan_normalized(child, template) + solved.learn(template, evidence(planned)) + end + fixed = fixed.each_with_index.map do |known, index| + next known if known + if solved.conflict?(index) + raise TypeArgConflict, "#{tree.path}: the fields of this #{tree.name} disagree about its type argument `#{params[index]}`" + end + found = solved.binding(index) + unless found && !Slot.open?(found, &@work) + raise MissingTypeArgs, "#{tree.path}: the fields of this #{tree.name} do not determine its type arguments; use Baml.bind_types(instance, ...)" + end + found + end + if slot && Slot.mentions_param?(slot, &@work) + inferred = fixed.each_index.map { |index| solved.binding(index, inference: true) || fixed[index] } + end + end + children = tree.children.map do |name, child| + template = Slot.stated_form(Slot.substitute(fields.fetch(name)[1], fixed, memo: @substitutions, &@work), @registry, memo: @normalized, &@work) + [name, plan_normalized(child, template)] + end + node(tree, [:class, tree.name, fixed], children: children, evidence: inferred && [:class, tree.name, inferred]) + end + + def natural(tree) + spend(1, tree.path) + case tree.kind + when :null, :int, :bigint, :float, :bool, :string, :bytes + node(tree, [tree.kind]) + when :media then node(tree, [:media, tree.name]) + when :enum + unless @registry.enum_values(tree.name).include?(tree.value) + refuse(tree, "unknown variant #{tree.value.inspect} of #{tree.name}") + end + node(tree, [:enum, tree.name]) + when :class then instance(tree) + when :callable + raise UntypedCallable, "#{tree.path}: cannot encode an untyped callable; it needs a function slot" + when :handle, :host_object then node(tree, [:opaque]) + when :list + children = tree.children.map { |child| natural(child) } + spend(children.length, tree.path) + inner = join(children) + node(tree, [:list, inner], children: children) + when :map + children = tree.children.map do |key, child| + [map_key(key, [:string]), natural(child)] + end + spend(children.length, tree.path) + values = children.map(&:last) + inner = join(values) + node(tree, [:map, [:string], inner], children: children.map(&:first).zip(values)) + else refuse(tree, "cannot encode #{tree.value.class}: it has no BAML type") + end + end + + def map_key(key, slot) + unless %i[string bytes].include?(key.kind) + refuse(key, "map keys must be string") + end + planned = plan_normalized(key, slot) + refuse(key, "map keys must be string") unless planned.kind == :string + planned.value + end + + def media_fits?(tree, slot) + kind = if slot[0] == :media + slot[1] + elsif slot[0] == :class && (slot[2] || []).empty? + @registry.class_crossing(slot[1]).to_s.delete_prefix("media_").to_sym + end + kind == :generic || kind == tree.name + end + + def rank(tree, slot) + slot = Slot.unfold(slot, @registry, &@work) + return 0 if tree.kind == :media && media_fits?(tree, slot) + return 0 if tree.kind == :enum && %i[enum enum_variant].include?(slot[0]) && tree.name == slot[1] + return 2 if %i[unknown interface param hole opaque].include?(slot[0]) || %i[handle host_object].include?(tree.kind) + return 1 if tree.kind == :callable && slot[0] == :function + return 0 if tree.kind == :null && slot[0] == :null + return 0 if %i[class enum].include?(tree.kind) && tree.kind == slot[0] && tree.name == slot[1] + return 1 if %i[list map].include?(tree.kind) && tree.kind == slot[0] + return 3 if tree.kind == :string && %i[enum enum_variant].include?(slot[0]) + return unless scalar?(tree) + + kind = slot[0] == :literal ? Slot.literal_kind(slot) : slot[0] + fit = Rules.fit(tree.value, kind) + fit && (fit[0] == :as_is ? 0 : 3) + end + + def choose(tree, members) + spend(1, tree.path) + members = @members[members] ||= Slot.union(Slot.members(members, @registry, &@work), &@work) + return plan_normalized(tree, members) unless members[0] == :union + + inferring = Slot.mentions_param?(members, &@work) + ranked = members[1].filter_map do |member| + priority = rank(tree, member) + [priority, member] if priority + end.group_by(&:first).sort + failures = [] + ranked.each do |_, group| + fits = [] + group.each do |_, member| + begin + candidate = plan_normalized(tree, member) + candidate.evidence = [:absorbed] if inferring && !Slot.mentions_param?(member, &@work) + seen = fits.find { |fit| Slot.equivalent?(fit.ty, candidate.ty, &@work) } + if !seen + fits << candidate + elsif candidate.evidence == [:absorbed] + # Equivalent fixed arms win inference ties; keep the first plan's wire spelling and order. + seen.evidence = candidate.evidence + end + rescue Fault + raise + rescue Refusal => error + failures << error + end + end + next if fits.empty? + return fits.first if fits.length == 1 + + own = begin + natural(tree).ty + rescue Fault + raise + rescue Refusal + nil + end + preferred = fits.find { |candidate| own && Slot.equivalent?(candidate.ty, own, &@work) } + return preferred if preferred + + raise Ambiguous, "#{tree.path}: fits more than one union member" + end + raise failures.first if failures.length == 1 + + refuse(tree, "cannot encode #{tree.kind}: no union member fits") + end + + # Only holes borrow a sibling's wire type. Inference also skips absorbed + # evidence, including inside containers and class arguments. Closed + # containers keep their types: [[1], ["a"]] is (int[] | string[])[], + # not (int | string)[][]. + def join(nodes, inference: false) + if inference + nodes = nodes.map { |child| child.dup.tap { |copy| copy.ty = evidence(child) } } + contributing = nodes.reject { |child| child.ty[0] == :absorbed } + return [:absorbed] if !nodes.empty? && contributing.empty? + + # Absorbed evidence can borrow a concrete type, but holes must borrow + # absorbed evidence, never erase it. Visit the latter before holes. + priorities = {}.compare_by_identity + groups = contributing.group_by { |child| priorities[child.ty] ||= evidence_priority(child.ty) } + nodes = groups.values_at(0, 1, 2).compact.flatten(1) + end + open = ->(ty) { inference ? priorities.fetch(ty).positive? : Slot.open?(ty, &@work) } + distinct = {} + add = ->(ty) { distinct[Slot.canonical(ty, &@work)] ||= ty } + nodes.each { |child| add.call(child.ty) unless open.call(child.ty) } + nodes.map! do |child| + if open.call(child.ty) + target = distinct.each_value.find { |ty| fills?(child.ty, ty, inference: inference) } + if target + inference ? child.ty = target : child = restate(child, target) + else + add.call(child.ty) + end + end + child + end + distinct.empty? ? [:hole] : Slot.union(distinct.values, &@work) + end + + def evidence_priority(ty) + return 2 if %i[hole opaque].include?(ty[0]) + return 1 if ty[0] == :absorbed + + @work.call(1) + Slot.children(ty).reduce(0) { |priority, child| [priority, evidence_priority(child)].max } + end + + def fills?(own, target, inference: false) + return true if own[0] == :hole + return target[0] != :hole if inference && own[0] == :absorbed + return false if own[0] == :opaque || own[0] != target[0] + + case own[0] + when :list then fills?(own[1], target[1], inference: inference) + when :map then fills?(own[1], target[1], inference: inference) && fills?(own[2], target[2], inference: inference) + when :class + if inference && own[1] == target[1] && own[2].length == target[2].length + own[2].zip(target[2]).all? { |arg, actual| fills?(arg, actual, inference: true) } + else + Slot.equivalent?(own, target, &@work) + end + else Slot.equivalent?(own, target, &@work) + end + end + + def fill_type(ty) + case ty[0] + when :hole, :opaque then [:unknown] + when :list then [:list, fill_type(ty[1])] + when :map then [:map, fill_type(ty[1]), fill_type(ty[2])] + when :union + members = ty[1].map { |item| fill_type(item) } + members.include?([:unknown]) ? [:unknown] : Slot.union(members, &@work) + else ty + end + end + + def restate(node, target) + return node unless Slot.open?(node.ty, &@work) + + ty = if fills?(node.ty, target) + target + elsif target[0] == :union && (member = target[1].find { |item| fills?(node.ty, item) }) + member + else + fill_type(node.ty) + end + children = case node.kind + when :list then node.children.map { |child| restate(child, ty[1]) } + when :map then node.children.map { |key, child| [key, restate(child, ty[2])] } + else return node + end + Node.new(kind: node.kind, ty: ty, value: node.value, children: children, path: node.path, evidence: node.evidence) + end + end + end + end +end diff --git a/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/encode/read.rb b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/encode/read.rb new file mode 100644 index 00000000000..48029f42ab4 --- /dev/null +++ b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/encode/read.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Baml + module Bridge + module Encode + Tree = Struct.new(:kind, :value, :name, :children, :path, :type_args, keyword_init: true) + + # Only reading runs host code. All arguments share one budget, charged + # while pulling children, never trusting a host-reported collection size. + class Read + MAX_DEPTH = 50 + NODE_BUDGET = 1 << 20 + + def initialize(registry, budget: NODE_BUDGET) + @registry = registry + @remaining = budget + end + + def read(value, path, depth = 1) + raise Fault, "#{path}: nested too deeply" if depth > MAX_DEPTH + raise Fault, "#{path}: value exceeds the node budget" if @remaining.zero? + + @remaining -= 1 + name, fields = @registry.encoding_type(value.class) + children = nil + type_args = [] + kind = if value.nil? + :null + elsif value.is_a?(::Baml::Handle) + :handle + elsif value.is_a?(::Baml::Media) + name = { ::Baml::Image => :image, ::Baml::Audio => :audio, ::Baml::Video => :video, ::Baml::Pdf => :pdf } + .fetch(value.class) { raise Refusal, "#{path}: cannot encode media kind #{value.class}" } + :media + elsif name && !fields + value = value.serialize.dup.freeze + :enum + elsif name + unless @registry.class_crossing(name) == :fields + raise Refusal, "#{path}: #{name} is built only by BAML; pass the instance you received" + end + type_args = ::Baml::Types.instance_args(value) + children = fields.map do |wire, (ruby, _descriptor)| + [wire, read(value.public_send(ruby), "#{path}.#{wire}", depth + 1)] + end + :class + elsif (scalar = Rules.kind(value)) != :other + scalar + elsif value.is_a?(Array) + children = [] + value.each_with_index { |item, index| children << read(item, "#{path}[#{index}]", depth + 1) } + :list + elsif value.is_a?(Hash) + children = [] + value.each do |key, item| + key_tree = read(key, "#{path}[key]", depth + 1) + entry_path = "#{path}[#{key.is_a?(String) ? key.inspect : 'entry'}]" + children << [key_tree, read(item, entry_path, depth + 1)] + end + :map + elsif HostValues.opaque?(value) + :host_object + else + value.respond_to?(:call) ? :callable : :other + end + if value.is_a?(String) + unless value.encoding == Encoding::BINARY || value.valid_encoding? + raise Refusal, "#{path}: cannot encode an invalid string" + end + value = value.dup.freeze + end + Tree.new(kind: kind, value: value, name: name, children: children, path: path, type_args: type_args) + end + end + end + end +end diff --git a/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/encode/rules.rb b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/encode/rules.rb new file mode 100644 index 00000000000..90bea7c8438 --- /dev/null +++ b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/encode/rules.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Baml + module Bridge + module Encode + module Rules + INT_RANGE = (-(2**62)..(2**62 - 1)).freeze + module_function + + def kind(value) + case value + when nil then :null + when true, false then :bool + when Integer then INT_RANGE.cover?(value) ? :int : :bigint + when Float then :float + when String then value.encoding == Encoding::BINARY ? :bytes : :string + else :other + end + end + + # The rank and converted payload come from the same rule. + def fit(value, slot) + if slot == :string && value.is_a?(String) && value.encoding == Encoding::BINARY + text = value.dup.force_encoding(Encoding::UTF_8) + return text.valid_encoding? ? [:converted, text] : nil + end + natural = kind(value) + return [:as_is, value] if natural == slot && natural != :other + return [:converted, value] if natural == :int && slot == :bigint + return [:converted, value.b] if natural == :string && slot == :bytes + return unless value.is_a?(Integer) && slot == :float + + float = value.to_f + [:converted, float] if float.finite? && float.to_i == value + end + end + end + end +end diff --git a/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/encode/slot.rb b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/encode/slot.rb new file mode 100644 index 00000000000..7dfdecffeed --- /dev/null +++ b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/encode/slot.rb @@ -0,0 +1,324 @@ +# frozen_string_literal: true + +module Baml + module Bridge + module Encode + Wire = ::BamlBridge::Cffi::V1 + + class Refusal < TypeError; end + class Fault < Refusal; end + class UnboundTypeArgs < Refusal; end + class MissingTypeArgs < Refusal; end + class TypeArgConflict < Refusal; end + class Ambiguous < Refusal; end + class UntypedCallable < Refusal; end + + module Slot + module_function + + def union(members, &work) + members = flatten(members, &work) + distinct = members.uniq { |item| canonical(item, &work) } + distinct.length == 1 ? distinct.first : [:union, distinct] + end + + def flatten(members, &work) + members.flat_map do |item| + work&.call(1) + item[0] == :union ? flatten(item[1], &work) : [item] + end + end + + def unfold(slot, registry = Protocol, &work) + return slot unless slot[0] == :alias + + seen = [] + while slot[0] == :alias && !seen.include?(slot[1]) + work&.call(1) + seen << slot[1] + definition = registry.alias_definition(slot[1]) + break unless definition + + slot = definition[0] + end + stated_form(slot, registry, &work) + end + + # A repeated alias on one union path proves no membership. Containers + # end that path: their children are judged against the alias afresh. + def members(slots, registry, seen = [], &work) + slots.flat_map do |slot| + work&.call(1) + case slot[0] + when :alias + definition = registry.alias_definition(slot[1]) + if seen.include?(slot[1]) + [] + elsif definition + members([stated_form(definition[0], registry, &work)], registry, seen + [slot[1]], &work) + else + [slot] + end + when :union then members(slot[1], registry, seen, &work) + else [slot] + end + end + end + + def stated_form(slot, registry = Protocol, seen = [], memo: {}.compare_by_identity, &work) + work&.call(1) + return memo[slot] if memo.key?(slot) + + state = ->(item) { stated_form(item, registry, seen, memo: memo, &work) } + memo[slot] = case slot[0] + when :alias + raise Fault, "cannot encode cyclic alias #{slot[1]}" if seen.include?(slot[1]) + + normalized = registry.normalized_alias(slot[1]) do |definition| + if definition && !definition[1] + stated_form(definition[0], registry, seen + [slot[1]], memo: memo, &work) + else + slot + end + end + charge(normalized, &work) + normalized + when :union then union(slot[1].map(&state), &work) + when :list then [:list, state.call(slot[1])] + when :map then [:map, state.call(slot[1]), state.call(slot[2])] + when :class then [:class, slot[1], (slot[2] || []).map(&state)] + when :function + [:function, slot[1].map { |name, ty, optional| [name, state.call(ty), optional] }, state.call(slot[2]), state.call(slot[3])] + else slot + end + end + + def literal_kind(slot) + slot.length == 3 ? slot[1] : Rules.kind(slot[1]) + end + + # Hash keys use one spelling for every type, including unions nested + # inside function signatures. Keep the first-seen spelling on the wire. + # Charge the expanded size before Ruby's native hashing and Marshal + # walks. Count shared subtrees once, but charge each occurrence. This + # also bounds cache hits without re-expanding a diamond-shaped alias. + def charge(value, &work) + return unless work + + visited = 0 + size = walk_size(value, {}.compare_by_identity) { visited += 1; work.call(1) } + work.call(size - visited) + end + + def walk_size(value, memo, &work) + work.call(1) + return memo[value] if memo.key?(value) + + memo[value] = 1 + children(value).sum { |child| walk_size(child, memo, &work) } + end + + def canonical(slot, &work) + charge(slot, &work) + canonical_form(slot, {}.compare_by_identity, {}) + end + + # Preserve sharing in canonical forms too. Without this memo, expanding + # each union's children again makes an alias DAG exponential in depth. + def canonical_form(slot, memo, intern) + return memo[slot] if memo.key?(slot) + + visit = ->(item) { canonical_form(item, memo, intern) } + result = case slot[0] + when :union + members = flatten(slot[1]).map(&visit).uniq.sort_by { |item| Marshal.dump(item) } + members.length == 1 ? members.first : [:union, members] + when :literal then [:literal, slot.length == 3 ? slot[1] : Rules.kind(slot.last), slot.last] + when :list then [:list, visit.call(slot[1])] + when :map then [:map, visit.call(slot[1]), visit.call(slot[2])] + when :class then [:class, slot[1], (slot[2] || []).map(&visit)] + when :function + [:function, slot[1].map { |name, ty, optional| [name, visit.call(ty), optional] }, visit.call(slot[2]), visit.call(slot[3])] + else slot + end + # Marshal includes object references. Intern equal subtrees so sorting + # cannot depend on whether the input happened to share its arrays. + # Child IDs keep this table's hashing shallow, even for an alias DAG. + key = case result[0] + when :union then [:union, result[1].map(&:object_id)] + when :list then [:list, result[1].object_id] + when :map then [:map, result[1].object_id, result[2].object_id] + when :class then [:class, result[1], result[2].map(&:object_id)] + when :function + [:function, result[1].map { |name, ty, optional| [name, ty.object_id, optional] }, result[2].object_id, result[3].object_id] + else result + end + memo[slot] = intern[key] ||= result + end + + def equivalent?(left, right, &work) + canonical(left, &work) == canonical(right, &work) + end + + def map_children(slot, work: nil, &map) + work&.call(1) + case slot[0] + when :list then [:list, map.call(slot[1])] + when :map then [:map, map.call(slot[1]), map.call(slot[2])] + when :union then union(slot[1].map(&map), &work) + when :class then [:class, slot[1], (slot[2] || []).map(&map)] + when :function + [:function, slot[1].map { |name, ty, optional| [name, map.call(ty), optional] }, map.call(slot[2]), map.call(slot[3])] + else slot + end + end + + def substitute(slot, args, indexed: true, memo: nil, &work) + if memo + values = args.is_a?(Hash) ? args.values : args + values.each { |arg| charge(arg, &work) if arg } + cached = (memo[slot] ||= {}) + key = [indexed, args] + work&.call(1) + return cached[key] if cached.key?(key) + + return cached[key] = substitute(slot, args, indexed: indexed, &work) + end + if slot[0] == :param + work&.call(1) + return args.fetch(slot[indexed ? 1 : 2], nil) || slot + end + + map_children(slot, work: work) { |child| substitute(child, args, indexed: indexed, &work) } + end + + def children(slot) + case slot[0] + when :list then [slot[1]] + when :map then [slot[1], slot[2]] + when :union then slot[1] + when :class then slot[2] || [] + when :function then slot[1].map { |param| param[1] } + [slot[2], slot[3]] + else [] + end + end + + def params(slot, &work) + work&.call(1) + return [slot] if slot[0] == :param + + children(slot).flat_map { |child| params(child, &work) } + end + + def mentions_param?(slot, &work) + work&.call(1) + slot[0] == :param || children(slot).any? { |child| mentions_param?(child, &work) } + end + + def open?(slot, &work) + work&.call(1) + return true if %i[hole opaque].include?(slot[0]) + + children(slot).any? { |child| open?(child, &work) } + end + + def to_wire(slot) + slot = stated_form(slot) + kind, *args = slot + fields = case kind + when :int, :bigint, :float, :string, :bool, :null, :bytes + { primitive: Wire::BamlTyPrimitive.new(kind: "BAML_TY_PRIMITIVE_#{kind.upcase}".to_sym) } + when :media + { media: Wire::BamlTyMedia.new(kind: "BAML_TY_MEDIA_KIND_#{args[0].upcase}".to_sym) } + when :alias then { type_alias: Wire::BamlTyTypeAlias.new(name: args[0]) } + when :enum_variant then { enum_variant: Wire::BamlTyEnumVariant.new(name: args[0], variant: args[1]) } + when :unknown then { unknown: Wire::BamlTyUnknown.new } + when :never then { never: Wire::BamlTyNever.new } + when :class + { class_ty: Wire::BamlTyClass.new(name: args[0], type_args: (args[1] || []).map { |item| to_wire(item) }) } + when :enum then { enum: Wire::BamlTyEnum.new(name: args[0]) } + when :interface then { interface: Wire::BamlTyInterface.new(name: args[0]) } + when :list then { list: Wire::BamlTyList.new(item: to_wire(args[0])) } + when :map then { map: Wire::BamlTyMap.new(key: to_wire(args[0]), value: to_wire(args[1])) } + when :union then { union: Wire::BamlTyUnion.new(options: args[0].map { |item| to_wire(item) }) } + when :function + params = args[0].map do |name, ty, optional| + fields = { ty: to_wire(ty), mode: optional ? :BAML_TY_FUNCTION_PARAM_MODE_OPTIONAL : :BAML_TY_FUNCTION_PARAM_MODE_REQUIRED } + fields[:name] = name if name + Wire::BamlTyFunctionParam.new(**fields) + end + { function: Wire::BamlTyFunction.new(params: params, ret: to_wire(args[1]), throws: to_wire(args[2])) } + when :literal + literal_kind = literal_kind(slot) + unless %i[int bigint string bool].include?(literal_kind) + raise Fault, "cannot encode this literal type" + end + value = literal_kind == :bigint ? slot.last.to_s(10) : slot.last + { literal: Wire::BamlTyLiteral.new(**{ "#{literal_kind}_value".to_sym => value }) } + else raise Fault, "cannot encode type #{slot.inspect}" + end + Wire::BamlTy.new(**fields) + end + + def from_wire(ty) + return [:other] unless ty + + body = ty.public_send(ty.ty) if ty.ty + if body.respond_to?(:head) && body.head != :name + return [:other] + end + case ty.ty + when :primitive + kind = body.kind.to_s.delete_prefix("BAML_TY_PRIMITIVE_").downcase.to_sym + %i[int bigint float string bool null bytes].include?(kind) ? [kind] : [:other] + when :unknown then [:unknown] + when :never then [:never] + when :function + return [:other] unless body.generic_params.empty? + + [:function, body.params.map { |param| [param.has_name? ? param.name : nil, from_wire(param.ty), + param.mode == :BAML_TY_FUNCTION_PARAM_MODE_OPTIONAL] }, from_wire(body.ret), from_wire(body.throws)] + when :class_ty then [:class, body.name, body.type_args.map { |item| from_wire(item) }] + when :enum then [:enum, body.name] + when :enum_variant then [:enum_variant, body.name, body.variant] + when :type_alias then body.type_args.empty? ? [:alias, body.name] : [:other] + when :media then [:media, body.kind.to_s.delete_prefix("BAML_TY_MEDIA_KIND_").downcase.to_sym] + when :interface + body.type_args.empty? && body.bindings.empty? ? [:interface, body.name] : [:other] + when :list then [:list, from_wire(body.item)] + when :map then [:map, from_wire(body.key), from_wire(body.value)] + when :optional then union([from_wire(body.inner), [:null]]) + when :union then union(body.options.map { |item| from_wire(item) }) + when :literal + if body.literal == :bigint_value + [:literal, :bigint, Integer(body.bigint_value, 10)] + else + %i[int_value string_value bool_value].include?(body.literal) ? [:literal, body.public_send(body.literal)] : [:other] + end + else [:other] + end + end + + # Protobuf reflection includes every nested message, including type + # annotations. Use the schema itself for depth and live-reference walks. + def each_message(message) + message.class.descriptor.each do |field| + next unless field.type == :message + + value = field.get(message) + if field.label == :repeated + value.each { |child| yield child } + elsif value + yield value + end + end + end + + def wire_depth(message) + deepest = 0 + each_message(message) { |child| deepest = [deepest, 1 + wire_depth(child)].max } + deepest + end + end + end + end +end diff --git a/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/errors.rb b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/errors.rb index c4258b1834f..3900402924b 100644 --- a/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/errors.rb +++ b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/errors.rb @@ -1,22 +1,57 @@ # frozen_string_literal: true module Baml + module Failure + attr_reader :value, :baml_trace, :type_name + + def initialize(value, baml_trace: [], type_name: nil) + @value = value + @baml_trace = baml_trace.dup.freeze + @type_name = type_name + message = if value.is_a?(String) + value + else + %i[message detail].filter_map do |field| + text = if value.is_a?(Hash) + item = value[field.to_s] + item if item.is_a?(String) + elsif value.respond_to?(field) + value.public_send(field).to_s + end + text unless text.nil? || text.empty? + end.first + end + super(message.nil? || message.empty? ? (type_name || value.class.name) : message) + end + end + + class Error < StandardError + include Failure + end + + # Panics must escape ordinary `rescue => error`, just like Python's BaseException. + class Panic < Exception # rubocop:disable Lint/InheritException + include Failure + end + + class CancelledError < Panic; end + module Bridge class Error < StandardError; end + class UnsupportedTypeError < Error; end # The runtime path is absent or unusable before a native library is opened. - # Correcting BAML_RUNTIME_PATH and retrying is safe. + # Correcting BAML_BRIDGE_PATH and retrying is safe. class RuntimeConfigurationError < Error; end class RuntimeLoadError < Error; end - # The configured library opened but cannot safely satisfy the V1 contract. - # The process caches this failure and never unloads or replaces the library. + # The configured library cannot satisfy the V1 contract. class IncompatibleRuntimeError < Error; end # The native table rejected bytecode without changing the current runtime. class ProgramInitializationError < Error; end - # One process may initialize only one exact generated BAML program. + # One process may register only one exact generated BAML program. class ProgramConflictError < Error; end # Native state inherited across fork is intentionally never touched. diff --git a/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/handle.rb b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/handle.rb new file mode 100644 index 00000000000..5049cefc653 --- /dev/null +++ b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/handle.rb @@ -0,0 +1,169 @@ +# frozen_string_literal: true + +module Baml + class Handle + def initialize(api, key, handle_type) + @api = api + @key = [key] + @handle_type = handle_type + @mutex = Mutex.new + ObjectSpace.define_finalizer(self, self.class.finalizer(api, @key, Process.pid)) + end + + # The finalizer owns only the key cell, never the Ruby handle itself. + def self.finalizer(api, key, owner_pid) + proc do + if Process.pid == owner_pid && key[0] + owned = key[0] + key[0] = nil + begin + api.handle_release(owned) + rescue StandardError + # Finalization cannot report a native cleanup failure to a caller. + end + end + end + end + + def close + Thread.handle_interrupt(Exception => :never) do + @mutex.synchronize do + if @key[0] + @api.handle_release(@key[0]) + @key[0] = nil + end + end + end + nil + end + + def clone_for_wire(scope, borrowed: false) + Thread.handle_interrupt(Exception => :never) do + @mutex.synchronize do + raise Bridge::Error, "BAML handle is closed" unless @key[0] + + key = @api.handle_clone(@key[0]) + borrowed ? scope.borrow(@api, key) : scope.own(@api, key) + [key, @handle_type] + end + end + end + end + + class Stream < Handle + extend T::Generic + Value = type_member + + def next(_baml: nil) + next_async(_baml: _baml).wait + end + + def next_async(_baml: nil) + Bridge.call_async("ai.stream.Stream.next", { "self" => self }, baml: _baml) + end + + def final(_baml: nil) + final_async(_baml: _baml).wait + end + + def final_async(_baml: nil) + Bridge.call_async("ai.stream.Stream.final", { "self" => self }, baml: _baml) + end + + def each + return enum_for(:each) unless block_given? + + # Resolve the generated sentinel through the registry, not an SDK namespace. + done = Bridge::Protocol.registered_type("ai.stream.Done") + while true + partial = self.next + break if partial.is_a?(done) + + yield partial unless partial.nil? + end + self + end + end + + class FunctionSpec < Handle + def build_request(client: nil, _baml: nil) + build_request_async(client: client, _baml: _baml).wait + end + + def build_request_async(client: nil, _baml: nil) + Bridge.call_async("ai.FunctionSpec.build_request", { "self" => self, "client" => client }, baml: _baml) + end + end + class Closure < Handle + attr_reader :parameter_descriptors + + def initialize(api, key, handle_type, function_type = nil) + wire = Bridge::Protocol::Wire + descriptor = Bridge::Encode::Slot.from_wire(wire::BamlTy.new(function: function_type)) + @parameters = descriptor[0] == :function ? descriptor[1].each_with_index.map do |(name, ty, optional), index| + [name || "arg#{index}", ty, optional] + end : [] + @parameter_descriptors = @parameters.to_h { |name, ty, _optional| [name, ty] } + super(api, key, handle_type) + end + + def call(*args, **kwargs) + call_async(*args, **kwargs).wait + end + + def call_async(*args, _baml: nil, **kwargs) + required = @parameters.reject(&:last).map(&:first) + raise ArgumentError, "Too many positional arguments for BAML closure" if args.length > required.length + raise ArgumentError, "Specialized BAML closures do not accept _types" if kwargs.key?(:_types) + + values = required.zip(args).first(args.length).to_h + kwargs.each do |name, value| + wire = name.to_s + raise ArgumentError, "Unknown BAML closure argument #{name}" unless @parameters.any? { |param, _ty, _optional| param == wire } + raise ArgumentError, "Multiple values for BAML closure argument #{name}" if values.key?(wire) + + values[wire] = value + end + Bridge.call_async(self, values, baml: _baml) + end + end + + module Bridge + class EncodingScope + def initialize + @handles = [] + @borrowed = [] + end + + def own(api, key) + @handles << [api, key] + end + + # Callable targets are borrowed, unlike argument handles: retain a clone + # through native submission, then release it on either submission or rollback. + def borrow(api, key) + @borrowed << [api, key] + end + + def submitted! + @handles.clear + close + end + + def close + handles = @handles + @borrowed + @handles.clear + @borrowed.clear + first_error = nil + handles.each do |api, key| + begin + api.handle_release(key) + rescue Exception => error # rubocop:disable Lint/RescueException -- attempt every owned release + first_error ||= error + end + end + raise first_error if first_error + end + end + end +end diff --git a/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/host_callbacks.rb b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/host_callbacks.rb new file mode 100644 index 00000000000..78e366163fe --- /dev/null +++ b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/host_callbacks.rb @@ -0,0 +1,194 @@ +# frozen_string_literal: true + +module Baml + module Bridge + # Host keys are borrowed on the outbound wire. Only the engine's release + # callback (or rollback of an unsubmitted encoding) removes their owners. + module HostValues + @mutex = Mutex.new + @values = {} + @opaque_objects = ObjectSpace::WeakMap.new + @next_key = 1 + + def self.register(value, scope, descriptor = nil) + raise Error, "BAML host values can only be sent inside an encoding scope" unless scope + + Thread.handle_interrupt(Exception => :never) do + @mutex.synchronize do + raise Error, "BAML host-value keys are exhausted" if @next_key > 0xffff_ffff_ffff_ffff + + key = @next_key + @next_key += 1 + @values[key] = [value, descriptor].freeze + @opaque_objects[value] = true unless descriptor + scope.own(self, key) + key + end + end + end + + def self.opaque?(value) + @mutex.synchronize { @opaque_objects.key?(value) } + end + + def self.fetch(key) + entry(key).first + end + + def self.entry(key) + @mutex.synchronize { @values.fetch(key) { raise Error, "Unknown BAML host-value key #{key}" } } + end + + def self.handle_release(key) + @mutex.synchronize { @values.delete(key) } + nil + end + end + + class HostCallbacks + Wire = Protocol::Wire + + def initialize(api) + @api = api + @dispatch = FFI::Function.new(:void, %i[pointer size_t]) do |pointer, length| + frame = nil + arguments = nil + begin + # The engine lends these bytes only until this callback returns. + frame = Wire::HostInvocation.decode(pointer.read_bytes(length)) + arguments = argument_result(frame) + Thread.new { invoke(frame, arguments) } + rescue Exception => error # rubocop:disable Lint/RescueException -- exceptions cannot cross FFI + arguments&.close + finish(frame, bridge_failure(error.message), EncodingScope.new) if frame + end + end + @release = FFI::Function.new(:void, [:uint64]) { |key| HostValues.handle_release(key) } + # Ruby callbacks cancel cooperatively: never kill their thread or drop + # their resources here. complete_host_call reports actual host exit. + @cancel = FFI::Function.new(:void, [:uint32]) { |_id| } + end + + def register! + @api.register_host_callbacks(@dispatch, @release, @cancel) + end + + private + + def argument_result(frame) + application = Wire::BamlToHostCall.decode(frame.application_args) + values = Wire::BamlOutboundValue.new(list_value: { items: application.args.map(&:value) }) + Protocol::Result.new(nil, values, @api) + end + + def invoke(frame, arguments = nil) + scope = EncodingScope.new + completion = nil + begin + arguments ||= argument_result(frame) + application = Wire::BamlToHostCall.decode(frame.application_args) + callable, descriptor = HostValues.entry(frame.host_value_key) + args = [] + kwargs = {} + application.args.each do |arg| + value = Protocol.decode_value(arg.value, @api, host_values: arguments.host_values, handles: arguments) + arg.is_optional_arg ? kwargs[arg.arg_name.to_sym] = value : args << value + end + unless descriptor&.first == :function + raise Error, "BAML callback has no function signature" + end + begin + value = callable.call(*args, **kwargs) + completion = encode_completion(value, descriptor[2], :returned, scope) + rescue Exception => error # rubocop:disable Lint/RescueException -- includes host errors while reading the result + completion = exception_completion(error, descriptor[3], scope) + end + rescue Exception => error # rubocop:disable Lint/RescueException -- infrastructure failures must complete too + scope.close + completion = bridge_failure(error.message) + ensure + arguments&.close + finish(frame, completion, scope) + end + end + + # Reading may run getters or enumerators. Their exceptions leave this + # method as host throws; once a plan is decided, failures are the bridge's. + def encode_completion(value, slot, outcome, scope) + begin + tree = Encode::Read.new(Protocol).read(value, "result") + rescue Encode::Refusal => error + return refused_completion(error) + end + begin + plan = Encode::Plan.new(Protocol).plan_root(tree, slot) + Encode::Commit.check_depth!(Encode::Commit.depth(plan), "result", enclosing: 1) + rescue Encode::Refusal => error + return refused_completion(error) + rescue Exception => error # rubocop:disable Lint/RescueException -- planning runs no host code + return bridge_failure(error.message) + end + begin + encoded = Encode::Commit.commit(plan, scope: scope) + Encode::Commit.check_depth!(Encode::Slot.wire_depth(encoded), "result", enclosing: 1) + Wire::HostCallCompletion.new(**{ outcome => encoded }) + rescue Exception => error # rubocop:disable Lint/RescueException -- commit failures are never host throws + scope.close + bridge_failure(error.message) + end + end + + def refused_completion(error) + if error.is_a?(Encode::Fault) || error.is_a?(Encode::Ambiguous) || + error.is_a?(Encode::MissingTypeArgs) || error.is_a?(Encode::UnboundTypeArgs) || error.is_a?(Encode::UntypedCallable) + bridge_failure(error.message) + else + Wire::HostCallCompletion.new(contract_violation: error.message) + end + end + + def exception_completion(error, throws, scope) + if error.is_a?(::Baml::Error) || error.is_a?(::Baml::Panic) + slot = error.is_a?(::Baml::Panic) ? [:unknown] : throws + begin + return encode_completion(error.value, slot, :thrown, scope) + rescue Exception => reading_error # rubocop:disable Lint/RescueException -- a thrown value's getter may also raise + error = reading_error + end + end + key = HostValues.register(error, scope) + fields = { message: utf8(error.message), class_name: error.class.name || error.class.to_s, language: "ruby", handle: key } + fields[:traceback] = utf8(error.backtrace.join("\n")) if error.backtrace + Wire::HostCallCompletion.new(raised: Wire::HostException.new(**fields)) + end + + def utf8(text) + text = text.dup.force_encoding(Encoding::UTF_8) if text.encoding == Encoding::BINARY + text.encode(Encoding::UTF_8, invalid: :replace, undef: :replace) + end + + def bridge_failure(message) + Wire::HostCallCompletion.new(bridge_failure: message) + end + + def finish(frame, completion, scope) + Thread.handle_interrupt(Exception => :never) do + begin + @api.complete_host_call(frame.callback_id, Wire::HostCallCompletion.encode(completion)) + scope.submitted! + ensure + begin + scope.close + ensure + begin + @api.handle_release(frame.effective_state) unless frame.effective_state.zero? + ensure + Protocol.release_outbound_references(frame.cancel, @api) + end + end + end + end + end + end + end +end diff --git a/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/native.rb b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/native.rb index 1d2461b8a56..ca1d0aca2bb 100644 --- a/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/native.rb +++ b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/native.rb @@ -57,6 +57,10 @@ class ApiV1 < FFI::Struct release_function_call register_host_dispatch_v2 register_host_cancel_callback + trace_selection + invocation_context + media_name + media_from_file_content ].freeze layout :abi_version, :uint32, @@ -71,15 +75,34 @@ class ApiV1 < FFI::Struct %i[pointer size_t], { blocking: true } ], + initialize_runtime_from_blob_with_metadata: [ + Buffer.by_value, + %i[pointer size_t string], + { blocking: true } + ], free_buffer: [:void, [Buffer.by_value]], register_callback: [:void, [:pointer]], - register_bridge: [Buffer.by_value, [:pointer]] + register_host_dispatch_v2: [:void, [:pointer]], + register_host_release_callback: [:void, [:pointer]], + register_host_cancel_callback: [:void, [:pointer]], + complete_host_call: [:void, %i[uint32 pointer size_t], { blocking: true }], + new_function_call: [:uint64, []], + release_function_call: [:int32, [:uint64]], + cancel_function_call: [:int32, [:uint64]], + invocation_clock_ns: [:int32, %i[uint64 pointer]], + handle_clone: [:int32, %i[uint64 pointer]], + handle_release: [:int32, [:uint64]], + call_function: [:void, %i[pointer size_t uint32], { blocking: true }], + register_bridge: [Buffer.by_value, [:pointer]], + register_unhandled_spawn_error_callback: [:void, [:pointer]], + shutdown_runtime: [Buffer.by_value, [], { blocking: true }] }.freeze class Api attr_reader :library, :runtime_version - def initialize(library, path, toolchain_version, bridge_runtime_version) + def initialize(library, path, toolchain_version) + @owner_pid = Process.pid @library = library @path = path @functions = {} @@ -91,26 +114,115 @@ def initialize(library, path, toolchain_version, bridge_runtime_version) "BAML runtime version mismatch: Ruby bridge requires #{toolchain_version}, " \ "but #{path.inspect} reports #{@runtime_version.inspect}" end - - register_bridge!(toolchain_version, bridge_runtime_version) end def register_result_callback(callback) function(:register_callback).call(callback) end - def initialize_runtime(bytecode) + def register_unhandled_spawn_error_callback(callback) + function(:register_unhandled_spawn_error_callback).call(callback) + end + + def shutdown_runtime + diagnostic = read_owned_utf8(function(:shutdown_runtime).call, "shutdown_runtime") + raise Error, "BAML runtime shutdown failed: #{diagnostic}" unless diagnostic.empty? + end + + def register_host_callbacks(dispatch, release, cancel) + function(:register_host_dispatch_v2).call(dispatch) + function(:register_host_release_callback).call(release) + function(:register_host_cancel_callback).call(cancel) + end + + def initialize_runtime(bytecode, metadata = nil) pointer = memory_for(bytecode) - diagnostic = read_owned_utf8( - function(:initialize_runtime_from_blob).call(pointer, bytecode.bytesize), - "initialize_runtime_from_blob" - ) + operation = metadata.nil? ? :initialize_runtime_from_blob : :initialize_runtime_from_blob_with_metadata + arguments = [pointer, bytecode.bytesize] + arguments << metadata unless metadata.nil? + diagnostic = read_owned_utf8(function(operation).call(*arguments), operation) return if diagnostic.empty? raise ProgramInitializationError, "Native BAML program initialization failed: #{diagnostic}" end + def complete_host_call(callback_id, bytes) + function(:complete_host_call).call(callback_id, memory_for(bytes), bytes.bytesize) + end + + def new_function_call + call_id = function(:new_function_call).call + raise Error, "BAML engine call IDs are exhausted" if call_id.zero? + + call_id + end + + def release_function_call(call_id) + function(:release_function_call).call(call_id) + end + + def cancel_function_call(call_id) + function(:cancel_function_call).call(call_id) + end + + def invocation_clock_ns(call_id) + out_now = FFI::MemoryPointer.new(:uint64) + status = function(:invocation_clock_ns).call(call_id, out_now) + raise Error, "Unable to read BAML invocation clock: status #{status}" unless status.zero? + + out_now.read_uint64 + end + + def handle_clone(key) + out_key = FFI::MemoryPointer.new(:uint64) + status = function(:handle_clone).call(key, out_key) + raise Error, "Unable to clone BAML handle #{key}: status #{status}" unless status.zero? + + out_key.read_uint64 + end + + def handle_release(key) + status = function(:handle_release).call(key) + raise Error, "Unable to release BAML handle #{key}: status #{status}" unless status.zero? + end + + def call_function(encoded_args, callback_id) + pointer = memory_for(encoded_args) + function(:call_function).call(pointer, encoded_args.bytesize, callback_id) + end + + def ensure_not_forked! + if @owner_pid != Process.pid + raise ForkSafetyError, "Forked child #{Process.pid} must exec before using BAML" + end + end + + def register_bridge!(toolchain_version, bridge_runtime_version) + toolchain_pointer = memory_for(toolchain_version.b) + name_pointer = memory_for(BRIDGE_RUNTIME_NAME.b) + bridge_version_pointer = memory_for(bridge_runtime_version.b) + info = BridgeInfoV1.new + info[:struct_size] = BridgeInfoV1.size + info[:language] = RUBY_BRIDGE_LANGUAGE + info[:sdk_version] = toolchain_pointer + info[:sdk_version_len] = toolchain_version.bytesize + info[:bridge_runtime_name] = name_pointer + info[:bridge_runtime_name_len] = BRIDGE_RUNTIME_NAME.bytesize + info[:bridge_runtime_version] = bridge_version_pointer + info[:bridge_runtime_version_len] = bridge_runtime_version.bytesize + + diagnostic = read_owned_utf8( + function(:register_bridge).call(info.pointer), + "register_bridge" + ) + return if diagnostic.empty? + + raise IncompatibleRuntimeError, + "Native bridge registration rejected #{BRIDGE_RUNTIME_NAME} " \ + "#{bridge_runtime_version}: #{diagnostic}" + end + private def read_table @@ -154,32 +266,9 @@ def validate_table! end end - def register_bridge!(toolchain_version, bridge_runtime_version) - toolchain_pointer = memory_for(toolchain_version.b) - name_pointer = memory_for(BRIDGE_RUNTIME_NAME.b) - bridge_version_pointer = memory_for(bridge_runtime_version.b) - info = BridgeInfoV1.new - info[:struct_size] = BridgeInfoV1.size - info[:language] = RUBY_BRIDGE_LANGUAGE - info[:sdk_version] = toolchain_pointer - info[:sdk_version_len] = toolchain_version.bytesize - info[:bridge_runtime_name] = name_pointer - info[:bridge_runtime_name_len] = BRIDGE_RUNTIME_NAME.bytesize - info[:bridge_runtime_version] = bridge_version_pointer - info[:bridge_runtime_version_len] = bridge_runtime_version.bytesize - - diagnostic = read_owned_utf8( - function(:register_bridge).call(info.pointer), - "register_bridge" - ) - return if diagnostic.empty? - - raise IncompatibleRuntimeError, - "Native bridge registration rejected #{BRIDGE_RUNTIME_NAME} " \ - "#{bridge_runtime_version}: #{diagnostic}" - end - def function(name) + ensure_not_forked! + @functions[name] ||= begin return_type, parameter_types, options = FUNCTION_SIGNATURES.fetch(name) FFI::Function.new(return_type, parameter_types, @table[name], **(options || {})) @@ -226,39 +315,28 @@ def memory_for(bytes) end end - # Owns an FFI callback and copies borrowed bytes before invoking Ruby. - # Every exception is retained for the Ruby caller instead of crossing C. + # Copy borrowed bytes, or pass the copy error to receive_result. The + # handler settles failures inside Ruby; no exception may cross FFI. class BorrowedBytesCallback attr_reader :function def initialize(&handler) - raise ArgumentError, "callback handler is required" unless handler - @handler = handler - @errors = Queue.new @function = FFI::Function.new(:void, %i[uint32 pointer size_t]) do |call_id, pointer, length| - begin - bytes = if length.zero? - String.new(encoding: Encoding::BINARY) - elsif pointer.nil? || pointer.null? - raise ArgumentError, "native callback pointer is null with #{length} bytes" - else - pointer.read_bytes(length) - end - @handler.call(call_id, bytes.force_encoding(Encoding::BINARY).freeze) - rescue Exception => error # rubocop:disable Lint/RescueException -- exceptions cannot cross FFI - @errors << error + outcome = begin + if length.zero? + "".b.freeze + elsif pointer.nil? || pointer.null? + raise ArgumentError, "native callback pointer is null with #{length} bytes" + else + pointer.read_bytes(length).b.freeze + end + rescue Exception => error # rubocop:disable Lint/RescueException -- settle copy errors inside Ruby + error end + @handler.call(call_id, outcome) end end - - # The generated call layer will drain this queue after callbacks and - # surface errors on the caller's Ruby thread. - def pop_error - @errors.pop(true) - rescue ThreadError - nil - end end end end diff --git a/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/process_runtime.rb b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/process_runtime.rb index 54112053910..f24501d13c1 100644 --- a/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/process_runtime.rb +++ b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/process_runtime.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true require "pathname" +require "rbconfig" require "thread" module Baml module Bridge class ProcessRuntime - RUNTIME_PATH_ENV = "BAML_RUNTIME_PATH" + RUNTIME_PATH_ENV = "BAML_BRIDGE_PATH" def initialize @mutex = Mutex.new @@ -14,160 +15,244 @@ def initialize @native_load_in_progress = false @library = nil @api = nil + @bridge_registered = false @result_callback = nil - @result_events = Queue.new + @result_registered = false + @host_callbacks = nil + @host_registered = false + @spawn_callback = nil + @spawn_registered = false + @initialized = false @program_bytes = nil - @terminal_error = nil + @metadata = nil + @pending_mutex = Mutex.new + @pending = {} + @next_callback_id = 1 end - def initialize!(compiled_program_bytes) - program = owned_program_bytes(compiled_program_bytes) + def register_program(bytecode, metadata = nil) ensure_not_forked! + raise TypeError, "compiled BAML program must be a String of bytes" unless bytecode.is_a?(String) + raise TypeError, "BAML metadata must be a String or nil" unless metadata.nil? || metadata.is_a?(String) - candidate_path = nil - if @api.nil? && @terminal_error.nil? - candidate_path = configured_runtime_path! - claim_process! + program = bytecode.dup.force_encoding(Encoding::BINARY).freeze + metadata = metadata&.dup&.freeze + @mutex.synchronize do + if @program_bytes && (@program_bytes != program || @metadata != metadata) + raise ProgramConflictError, "This Ruby process already registered a different generated BAML program" + end + + @program_bytes = program + @metadata = metadata end + nil + end + def initialize! + ensure_not_forked! + claim_process! @mutex.synchronize do - ensure_not_forked! - raise @terminal_error if @terminal_error - - if @program_bytes - return nil if @program_bytes == program + return if @initialized + raise RuntimeConfigurationError, "No BAML program is registered" unless @program_bytes - raise ProgramConflictError, - "This Ruby process already initialized a different generated BAML program" + unless @library + @runtime_path = configured_runtime_path! + flags = FFI::DynamicLibrary::RTLD_NOW | FFI::DynamicLibrary::RTLD_LOCAL + begin + @native_load_in_progress = true + @library = open_library(@runtime_path, flags) + rescue LoadError => error + raise RuntimeLoadError, "Unable to open BAML runtime #{@runtime_path.inspect}: #{error.message}" + ensure + @native_load_in_progress = false + end end - - load_api!(candidate_path) unless @api - begin - @api.initialize_runtime(program) - rescue IncompatibleRuntimeError => error - @terminal_error = error - raise + @api ||= Native::Api.new(@library, @runtime_path, TOOLCHAIN_VERSION) + unless @bridge_registered + Thread.handle_interrupt(Exception => :never) do + @api.register_bridge!(TOOLCHAIN_VERSION, BRIDGE_RUNTIME_VERSION) + @bridge_registered = true + end + end + unless @result_registered + @result_callback ||= Native::BorrowedBytesCallback.new(&method(:receive_result)) + Thread.handle_interrupt(Exception => :never) do + @api.register_result_callback(@result_callback.function) + @result_registered = true + end + end + unless @host_registered + @host_callbacks ||= HostCallbacks.new(@api) + Thread.handle_interrupt(Exception => :never) do + @host_callbacks.register! + @host_registered = true + end + end + unless @spawn_registered + @spawn_callback ||= FFI::Function.new(:void, %i[pointer size_t int32]) do |pointer, length, cancelled| + begin + Protocol.decode_result(pointer.read_bytes(length), @api) + rescue Exception => error # rubocop:disable Lint/RescueException -- match Python's spawn-error policy + warn error.full_message + $stderr.flush + Kernel.exit!(1) if cancelled.zero? + end + end + Thread.handle_interrupt(Exception => :never) do + @api.register_unhandled_spawn_error_callback(@spawn_callback) + @spawn_registered = true + end + end + Thread.handle_interrupt(Exception => :never) do + @api.initialize_runtime(@program_bytes, @metadata) + @initialized = true + at_exit do + begin + @api.shutdown_runtime + rescue ForkSafetyError + # A forked child must not shut down the parent's runtime. + end + end end - @program_bytes = program nil end end - private + def call_async(function_name, arguments, types: nil, baml: nil) + raise TypeError, "_baml must be Baml::Options or nil" unless baml.nil? || baml.is_a?(::Baml::Options) + type_args = Protocol.prepare_type_args(function_name, arguments, types) - def owned_program_bytes(value) - unless value.is_a?(String) - raise TypeError, "compiled BAML program must be a String of bytes" + call_id = callback_id = nil + submitted = false + scope = EncodingScope.new + initialize! + # Retain native ownership before a deferred interrupt can reach Ruby. + Thread.handle_interrupt(Exception => :never) { call_id = @api.new_function_call } + invocation = Protocol.encode_invocation(call_id, baml, @api, scope: scope) + encoded = Protocol.encode_call(call_id, function_name, arguments, scope: scope, invocation: invocation, type_args: type_args) + Thread.handle_interrupt(Exception => :never) do + call = ::Baml::Call.new(@api, call_id) + callback_id = register_call(call) + @api.call_function(encoded, callback_id) + submitted = true + scope.submitted! + call + end + ensure + Thread.handle_interrupt(Exception => :never) do + unless submitted + @pending_mutex.synchronize { @pending.delete(callback_id) } if callback_id + begin + scope&.close + ensure + @api.release_function_call(call_id) if call_id + end + end end - - value.dup.force_encoding(Encoding::BINARY).freeze end - def configured_runtime_path! - value = ENV.fetch(RUNTIME_PATH_ENV, "") - if value.empty? - raise RuntimeConfigurationError, - "#{RUNTIME_PATH_ENV} must name the bridge_cffi library" - end + private - path = Pathname.new(value) - unless path.absolute? - raise RuntimeConfigurationError, - "#{RUNTIME_PATH_ENV} must be an absolute path: #{value.inspect}" - end - unless path.file? - raise RuntimeConfigurationError, - "#{RUNTIME_PATH_ENV} does not name a file: #{value.inspect}" + def register_call(call) + @pending_mutex.synchronize do + first = @next_callback_id + loop do + callback_id = @next_callback_id + @next_callback_id = callback_id == 0xffff_ffff ? 1 : callback_id + 1 + unless @pending.key?(callback_id) + @pending[callback_id] = call + return callback_id + end + raise Error, "BAML callback correlation IDs are exhausted" if @next_callback_id == first + end end - - path.to_s.freeze end def claim_process! - current_pid = Process.pid - owner_pid = @owner_pid - if owner_pid && owner_pid != current_pid - raise_fork_error(owner_pid, current_pid) - end - - # Assignment occurs before entering @mutex. A child created while a - # load is in progress therefore rejects inherited state without ever - # touching a mutex that may have been locked by a vanished thread. - @owner_pid ||= current_pid + ensure_not_forked! + # Claim before the mutex and native open: a fork during loading must + # reject inherited state without touching the vanished thread's lock. + @owner_pid ||= Process.pid end def ensure_not_forked! - owner_pid = @owner_pid - current_pid = Process.pid - return unless owner_pid && owner_pid != current_pid - - # Once a failed native open returns, a child has no native state to - # inherit. Replace the mutex that may still be locked by a vanished - # parent thread, then let the child retry with its own process claim. - if @library.nil? && @api.nil? && @terminal_error.nil? && !@native_load_in_progress + owner = @owner_pid + return unless owner && owner != Process.pid + + if @library.nil? && @api.nil? && !@native_load_in_progress @mutex = Mutex.new @owner_pid = nil return end - raise_fork_error(owner_pid, current_pid) - end - - def raise_fork_error(owner_pid, current_pid) raise ForkSafetyError, - "BAML native state belongs to process #{owner_pid}; forked child " \ - "#{current_pid} must exec before using BAML" + "BAML native state belongs to process #{owner}; forked child #{Process.pid} must exec before using BAML" end - def receive_result(call_id, bytes) - @result_events << [call_id, bytes, Thread.current.object_id].freeze - end + def configured_runtime_path! + value = ENV.fetch(RUNTIME_PATH_ENV, "") + unless value.empty? + path = Pathname.new(value) + unless path.absolute? + raise RuntimeConfigurationError, "#{RUNTIME_PATH_ENV} must be an absolute path: #{value.inspect}" + end + unless path.file? + raise RuntimeConfigurationError, "#{RUNTIME_PATH_ENV} does not name a file: #{value.inspect} (no cache fallback)" + end - # The generated call layer will drain one event for each native result - # before result delivery is enabled. - def pop_result_event - @result_events.pop(true) - rescue ThreadError - nil - end + return path.to_s.freeze + end - def open_library(path, flags) - FFI::DynamicLibrary.open(path, flags) + path = cached_runtime_path + unless path.file? + raise RuntimeConfigurationError, + "Unable to find BAML runtime: #{RUNTIME_PATH_ENV} is not set; " \ + "cache path #{path.to_s.inspect} does not name a file. " \ + "Install the library there or set #{RUNTIME_PATH_ENV}. Automatic download is not supported." + end + + path.to_s.freeze end - def load_api!(path) - flags = FFI::DynamicLibrary::RTLD_NOW | FFI::DynamicLibrary::RTLD_LOCAL - begin - @native_load_in_progress = true - @library = open_library(path, flags) - rescue LoadError => error - raise RuntimeLoadError, - "Unable to open BAML runtime #{path.inspect}: #{error.message}" - ensure - @native_load_in_progress = false + def cached_runtime_path + cpu, os, arch, host = RbConfig::CONFIG.values_at("host_cpu", "host_os", "arch", "host") + cpu = case cpu + when "x86_64", "amd64", "x64" then "x86_64" + when "aarch64", "arm64" then "aarch64" + end + target, filename = case os + when /darwin/ + ["#{cpu}-apple-darwin", "libbridge_cffi.dylib"] + when /linux/ + libc = [os, arch, host].join.include?("musl") ? "musl" : "gnu" + ["#{cpu}-unknown-linux-#{libc}", "libbridge_cffi.so"] + when /mswin|mingw/ + ["#{cpu}-pc-windows-msvc", "bridge_cffi.dll"] + end + unless cpu && target + raise RuntimeConfigurationError, + "No cached BAML runtime target for #{arch}; set #{RUNTIME_PATH_ENV} explicitly" end + home = ENV.fetch("BAML_HOME", "") + home = File.join(Dir.home, ".baml") if home.empty? + Pathname.new(home).join("bridges", TOOLCHAIN_VERSION, target, filename).expand_path + end + + def receive_result(callback_id, outcome) + call = @pending_mutex.synchronize { @pending.delete(callback_id) } + return unless call + begin - api = Native::Api.new( - @library, - path, - TOOLCHAIN_VERSION, - BRIDGE_RUNTIME_VERSION - ) - callback = Native::BorrowedBytesCallback.new(&method(:receive_result)) - api.register_result_callback(callback.function) - @result_callback = callback - @api = api - rescue Error => error - @terminal_error = error - raise - rescue LoadError, StandardError => error - wrapped = IncompatibleRuntimeError.new( - "Unable to use BAML runtime #{path.inspect}: #{error.class}: #{error.message}" - ) - @terminal_error = wrapped - raise wrapped + outcome = Protocol.capture_result(outcome, @api) unless outcome.is_a?(Exception) + rescue Exception => error # rubocop:disable Lint/RescueException -- settle parse/pinning failures inside Ruby + outcome = error end + call.complete(outcome) + end + + def open_library(path, flags) + FFI::DynamicLibrary.open(path, flags) end end end diff --git a/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/protocol.rb b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/protocol.rb new file mode 100644 index 00000000000..a7580010c1c --- /dev/null +++ b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/protocol.rb @@ -0,0 +1,504 @@ +# frozen_string_literal: true + +require "baml_bridge/cffi/v1/baml_inbound_pb" +require "baml_bridge/cffi/v1/baml_outbound_pb" +require_relative "../media" +require_relative "encode/slot" +require_relative "encode/rules" +require_relative "encode/read" +require_relative "encode/plan" +require_relative "encode/commit" + +module Baml + module Bridge + module Protocol + Wire = ::BamlBridge::Cffi::V1 + # Each wire occurrence owns a reference, even when several share a key. + # Wait transfers those references to wrappers; an abandoned or failed + # decode releases the rest without retaining the captured result itself. + class Result + attr_reader :message, :host_values + + def initialize(message, value, api) + @message = message + @host_values = {} + @keys = [] + Protocol.visit_handles(value) do |handle| + @keys << handle.key unless %i[HOST_VALUE_CALLABLE HOST_VALUE_OPAQUE].include?(handle.handle_type) + end + @cleanup = self.class.finalizer(api, @keys, Process.pid) + ObjectSpace.define_finalizer(self, @cleanup) + Protocol.visit_handles(value) do |handle| + if %i[HOST_VALUE_CALLABLE HOST_VALUE_OPAQUE].include?(handle.handle_type) + @host_values[handle.key] = HostValues.fetch(handle.key) + end + end + @host_values.freeze + rescue Exception # rubocop:disable Lint/RescueException -- release the inventory if pinning fails + close + raise + end + + def self.finalizer(api, keys, owner_pid) + proc do + if Process.pid == owner_pid + until keys.empty? + key = keys.pop + begin + api.handle_release(key) + rescue StandardError + # Cleanup cannot replace the result's decoding error. + end + end + end + end + end + + def adopt(key) + Thread.handle_interrupt(Exception => :never) do + index = @keys.index(key) or raise Error, "BAML result handle #{key} was already adopted" + owner = yield + @keys.delete_at(index) + owner + end + end + + def close + @cleanup&.call + end + end + @types = {} + @names = {} + @fields = {} + @functions = {} + @function_type_params = {} + @class_type_params = {} + @enums = {} + @aliases = {} + @normalized_aliases = {} + @alias_mutex = Mutex.new + @alias_generation = 0 + @crossings = {} + module_function + + # The generator owns Ruby naming, including renamed fields. No constant + # lookup or naming conversion happens while decoding untrusted wire data. + def define_class(name, type, fields:, crossing: :fields, type_params: []) + ::Baml::Types.register(type, name, type_params.length) + @class_type_params[name] = type_params.freeze + @types[name] = type + @names[type] = name + @crossings[name] = crossing + @fields[type] = fields.transform_values { |ruby, descriptor| [ruby, descriptor].freeze }.freeze + end + + def define_enum(name, type) + ::Baml::Types.register(type, name) + @types[name] = type + @names[type] = name + @fields[type] = nil + @enums[name] = type.values.map(&:serialize).freeze + end + + def define_alias(name, descriptor, recursive: false) + @alias_mutex.synchronize do + @aliases[name] = [descriptor, recursive].freeze + @alias_generation += 1 + @normalized_aliases.clear + end + end + + def normalized_alias(name) + definition, generation = @alias_mutex.synchronize do + return @normalized_aliases[name] if @normalized_aliases.key?(name) + + [@aliases[name], @alias_generation] + end + value = yield definition + @alias_mutex.synchronize do + @normalized_aliases[name] = value if generation == @alias_generation + end + value + end + + def alias_definition(name) + @alias_mutex.synchronize { @aliases[name] } + end + + def class_crossing(name) + @crossings.fetch(name, :fields) + end + + def class_type_params(name) + @class_type_params.fetch(name) + end + + def define_function(name, params:, class_type_params: [], type_params: []) + @functions[name] = params.to_h.freeze + @function_type_params[name] = [class_type_params.freeze, type_params.freeze].freeze + end + + def registered_type(name) + @types.fetch(name) do + raise UnsupportedTypeError, "BAML type #{name.inspect} is not registered" + end + end + + def encoding_type(type) + [@names[type], @fields[type]] + end + + def encoding_fields(name) + @fields.fetch(registered_type(name)) + end + + def enum_values(name) + @enums.fetch(name) + end + + def encode_invocation(call_id, options, api, scope: nil) + invocation = Wire::InvocationOptions.new + return invocation unless options + + unless options.timeout_ms.nil? + deadline = api.invocation_clock_ns(call_id) + options.timeout_ms * 1_000_000 + raise ArgumentError, "timeout deadline exceeds the runtime clock range" if deadline > 0xffff_ffff_ffff_ffff + + invocation.deadline_ns = deadline + end + unless options.cancel.nil? + unless @names[options.cancel.class] == "baml.spawn.CancelToken" + raise TypeError, "cancel must be a generated baml.spawn.CancelToken" + end + path = "_baml.cancel" + tree = Encode::Read.new(self).read(options.cancel, path) + plan = Encode::Plan.new(self).plan(tree, [:unknown]) + Encode::Commit.check_depth!(Encode::Commit.depth(plan), path) + invocation.cancel = Encode::Commit.commit(plan, scope: scope) + Encode::Commit.check_depth!(Encode::Slot.wire_depth(invocation.cancel), path) + end + invocation + end + + def prepare_type_args(function_name, arguments, types) + unless types.nil? || types.is_a?(Hash) + raise TypeError, "_types must be a Hash of Symbol names to BAML type tokens" + end + class_params, method_params = @function_type_params.fetch(function_name, [[], []]) + types ||= {} + types.each_key do |name| + unless name.is_a?(Symbol) && method_params.include?(name.to_s) + raise TypeError, "unknown type binding #{name.inspect} for #{function_name}; expected #{method_params.inspect}" + end + end + explicit = method_params.filter_map do |name| + [name, ::Baml::Types.token(types[name.to_sym])] if types.key?(name.to_sym) + end + receiver_args = arguments.key?("self") ? ::Baml::Types.instance_args(arguments["self"]) : [] + if !explicit.empty? && !class_params.empty? && receiver_args.empty? + raise TypeError, "_types on a generic method requires a parameterized receiver; use Baml.bind_types(receiver, ...)" + end + unless receiver_args.empty? + ::Baml::Types.check_arity("#{function_name} receiver", class_params.length, receiver_args.length) + end + inherited = receiver_args.each_with_index.map { |type, index| [class_params.fetch(index), type] } + (inherited + explicit).map do |name, type| + Wire::BamlTyArg.new(type_var: name, type_value: Encode::Slot.to_wire(type)) + end + end + + def encode_call(call_id, function_name, arguments, scope: nil, invocation: Wire::InvocationOptions.new, type_args: []) + hints = function_name.is_a?(::Baml::Closure) ? function_name.parameter_descriptors : @functions.fetch(function_name, {}) + bindings = type_args.to_h { |arg| [arg.type_var, Encode::Slot.from_wire(arg.type_value)] } + reader = Encode::Read.new(self) + planner = Encode::Plan.new(self) + trees = arguments.filter_map do |name, value| + [name, reader.read(value, name)] unless value.equal?(::Baml::UNSET) + end + plans = trees.map do |name, tree| + slot = Encode::Slot.substitute(hints.fetch(name, [:unknown]), bindings, indexed: false) + [name, planner.plan_root(tree, slot)] + end + plans.each { |name, plan| Encode::Commit.check_depth!(Encode::Commit.depth(plan), name) } + target = if function_name.is_a?(::Baml::Closure) + { function_handle: function_name.clone_for_wire(scope, borrowed: true)[0] } + else + { function_name: function_name } + end + kwargs = plans.map do |name, plan| + value = Encode::Commit.commit(plan, scope: scope) + Encode::Commit.check_depth!(Encode::Slot.wire_depth(value), name) + Wire::InboundMapEntry.new(string_key: name, value: value) + end + Wire::CallFunctionArgs.encode(Wire::CallFunctionArgs.new( + call_id: call_id, **target, kwargs: kwargs, type_args: type_args, + invocation: invocation + )) + rescue Encode::Refusal => error + raise error.class, "#{function_name}: #{error.message}" + end + + # Outbound host keys are borrowed. Pin their Ruby owners before returning + # from the native callback: engine GC may release them before Call#wait. + def capture_result(bytes, api = nil) + message = Wire::BamlOutboundResult.decode(bytes) + if message.result == :panic && message.panic.is_exit_panic + exit_process(message.panic.exit_code) + end + value = case message.result + when :ok then message.ok + when :error then message.error.value + when :panic then message.panic.value + end + Result.new(message, value, api) + end + + def visit_handles(value, &block) + return unless value + + case value.value + when :handle_value + block.call(value.handle_value) + when :class_value + value.class_value.fields.each { |field| visit_handles(field.value, &block) } + when :list_value + value.list_value.items.each { |item| visit_handles(item, &block) } + when :map_value + value.map_value.entries.each { |entry| visit_handles(entry.value, &block) } + when :union_variant_value + visit_handles(value.union_variant_value.value, &block) + when :ty_value + visit_type_handles(value.ty_value, &block) + end + end + + def visit_type_handles(type, &block) + if type.respond_to?(:head) && type.head == :live + block.call(Wire::BamlHandle.new(key: type.live, handle_type: :ADT_TAGGED_HEAP_HANDLE)) + end + Encode::Slot.each_message(type) { |child| visit_type_handles(child, &block) } + end + + def exit_process(code) + [$stdout, $stderr, STDOUT, STDERR].uniq(&:object_id).each do |output| + begin + output.flush + rescue StandardError + # A broken output must not prevent the requested process exit. + end + end + Kernel.exit!(code) + end + + def decode_result(bytes, api = nil) + captured = bytes.is_a?(Result) ? bytes : capture_result(bytes, api) + result = captured.message + owners = { host_values: captured.host_values, handles: captured } + case result.result + when :ok + decode_value(result.ok, api, **owners) + when :error + raise_failure(::Baml::Error, result.error.value, api, trace: result.error.trace.to_a, **owners) + when :panic + return exit_process(result.panic.exit_code) if result.panic.is_exit_panic + + raise_failure(::Baml::Panic, result.panic.value, api, trace: result.panic.trace.to_a, **owners) + else + raise Error, "BAML returned an empty result envelope" + end + ensure + captured&.close + end + + def decode_media(media) + type = { IMAGE: ::Baml::Image, AUDIO: ::Baml::Audio, VIDEO: ::Baml::Video, PDF: ::Baml::Pdf }[media.media] + raise UnsupportedTypeError, "BAML media kind #{media.media} is not supported" unless type + + mime = media.has_mime_type? ? media.mime_type : nil + case media.value + when :url then type.from_url(media.url, mime_type: mime) + when :base64 then type.from_base64(media.base64, mime_type: mime) + when :file_content + type.from_file_content(media.file_content.name, media.file_content.base64, mime_type: mime) + else raise Error, "BAML returned media with no content" + end + end + + # Error payloads can mention classes deliberately omitted by the generator. + # Inspect before decoding, so a fallback never adopts a handle twice. + def unregistered_class?(value) + return false unless value + + case value.value + when :class_value + !@types.key?(value.class_value.name) || value.class_value.fields.any? { |field| unregistered_class?(field.value) } + when :list_value + value.list_value.items.any? { |item| unregistered_class?(item) } + when :map_value + value.map_value.entries.any? { |entry| unregistered_class?(entry.value) } + when :union_variant_value + unregistered_class?(value.union_variant_value.value) + else false + end + end + + def decode_value(value, api = nil, host_values: nil, handles: nil, as_proc: false, error_payload: false) + raise Error, "BAML returned an empty value" unless value + + case value.value + when :string_value, :int_value, :float_value, :bool_value + value.public_send(value.value) + when :uint8array_value + value.uint8array_value.b + when :bigint_value + Integer(value.bigint_value, 16) + when :media_value + decode_media(value.media_value) + when :handle_value + handle = value.handle_value + klass = case handle.handle_type + when :HOST_VALUE_CALLABLE, :HOST_VALUE_OPAQUE + owner = host_values ? host_values.fetch(handle.key) : HostValues.fetch(handle.key) + if as_proc && handle.handle_type == :HOST_VALUE_CALLABLE && !owner.is_a?(Proc) + return owner.method(:call).to_proc + end + return owner + when :FUNCTION_REF then ::Baml::Closure + when :ADT_FUNCTION_SPEC then ::Baml::FunctionSpec + when :ADT_TAGGED_HEAP_HANDLE + handle.ty&.class_ty&.name == "ai.stream.Stream" ? ::Baml::Stream : ::Baml::Handle + else ::Baml::Handle + end + build = lambda do + if klass == ::Baml::Closure + klass.new(api, handle.key, handle.handle_type, handle.ty&.function) + else + klass.new(api, handle.key, handle.handle_type) + end + end + handles ? handles.adopt(handle.key, &build) : build.call + when nil, :null_value + # V1 currently emits an absent oneof for null. + nil + when :class_value + object = value.class_value + if error_payload && unregistered_class?(value) + return object.fields.to_h do |field| + [field.key, decode_value(field.value, api, host_values: host_values, handles: handles, as_proc: true, error_payload: true)] + end.freeze + end + type = registered_type(object.name) + fields = @fields.fetch(type) + decoded = object.fields.to_h { |field| [fields.fetch(field.key)[0], decode_value(field.value, api, host_values: host_values, handles: handles, as_proc: true, error_payload: error_payload)] } + instance = type.new(**decoded) + unless object.type_args.empty? + ::Baml::Types.bind(instance, *object.type_args.map { |arg| Encode::Slot.from_wire(arg) }) + end + instance + when :enum_value + enum = value.enum_value + if enum.is_dynamic + raise UnsupportedTypeError, "BAML dynamic enum #{enum.name} is not yet supported" + end + registered_type(enum.name).deserialize(enum.value) + when :list_value + value.list_value.items.map { |item| decode_value(item, api, host_values: host_values, handles: handles, as_proc: as_proc, error_payload: error_payload) } + when :map_value + map = value.map_value + map.entries.to_h { |entry| [decode_map_key(entry.key, map.key_type), decode_value(entry.value, api, host_values: host_values, handles: handles, as_proc: as_proc, error_payload: error_payload)] } + when :union_variant_value + decode_value(value.union_variant_value.value, api, host_values: host_values, handles: handles, as_proc: as_proc, error_payload: error_payload) + when :literal_value + literal = value.literal_value + case literal.literal + when :string_value, :int_value, :bool_value + literal.public_send(literal.literal) + when :float_value + Float(literal.float_value) + when :bigint_value + Integer(literal.bigint_value, 16) + else + raise UnsupportedTypeError, "BAML literal kind #{literal.literal.inspect} is not yet supported" + end + else + raise UnsupportedTypeError, "BAML result kind #{value.value} is not yet supported" + end + end + + def decode_map_key(key, type) + case type&.ty + when :enum + registered_type(type.enum.name).deserialize(key) + when :enum_variant + registered_type(type.enum_variant.name).deserialize(key) + when :union, :type_alias, :never + # Compiler map keys (including aliases and literal unions) denote strings. + key + when :primitive + case type.primitive.kind + when :BAML_TY_PRIMITIVE_STRING then key + when :BAML_TY_PRIMITIVE_INT, :BAML_TY_PRIMITIVE_BIGINT then Integer(key, 10) + when :BAML_TY_PRIMITIVE_BOOL then decode_bool_key(key) + else + raise UnsupportedTypeError, "BAML map key type #{type.primitive.kind} is not yet supported" + end + when :literal + case type.literal.literal + when :string_value then key + when :int_value, :bigint_value then Integer(key, 10) + when :bool_value then decode_bool_key(key) + else + raise UnsupportedTypeError, "BAML map key literal #{type.literal.literal} is not yet supported" + end + else + raise UnsupportedTypeError, "BAML map key type #{type&.ty.inspect} is not yet supported" + end + end + + def decode_bool_key(key) + return true if key == "true" + return false if key == "false" + + raise Error, "BAML returned an invalid bool map key #{key.inspect}" + end + + def raise_failure(error_class, value, api = nil, trace: [], host_values: nil, handles: nil) + while value&.value == :union_variant_value + value = value.union_variant_value.value + end + if value&.value == :class_value && value.class_value.name == "baml.errors.HostCallable" + handle = value.class_value.fields.find { |field| field.key == "_handle" }&.value + original = decode_value(handle, api, host_values: host_values, handles: handles) if handle + raise original if original.is_a?(Exception) + end + name = if value&.value == :class_value + value.class_value.name + else + kind = value&.value == :literal_value ? value.literal_value.literal : (value&.value || :null_value) + kind.to_s.delete_suffix("_value") + end + if error_class == ::Baml::Error && name == "baml.errors.TypeMismatch" + message = value.class_value.fields.find { |field| field.key == "message" }&.value + error = TypeError.new(message ? decode_value(message, api, host_values: host_values, handles: handles).to_s : name) + else + error_class = ::Baml::CancelledError if error_class == ::Baml::Panic && name == "baml.panics.Cancelled" + error = error_class.new(decode_value(value, api, host_values: host_values, handles: handles, error_payload: true), type_name: name, baml_trace: trace) + end + # Ruby backtraces use path:line:in `function`, while the wire uses + # Python's frame format. Keep the original lines on #baml_trace. + frames = trace.reverse.map do |frame| + frame.sub(/\AFile "([^"]*)", line (\d+), in (.*)\z/, '\1:\2:in `\3\'') + end + error.set_backtrace(frames + caller) + raise error + end + + def release_outbound_references(value, api) + visit_handles(value) do |handle| + api.handle_release(handle.key) unless %i[HOST_VALUE_CALLABLE HOST_VALUE_OPAQUE].include?(handle.handle_type) + end + end + + end + end +end diff --git a/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/types.rb b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/types.rb new file mode 100644 index 00000000000..06802c01557 --- /dev/null +++ b/baml_language/sdks/ruby/bridge_ruby/lib/baml/bridge/types.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require "sorbet-runtime" + +module Baml + # Sorbet erases Klass[T] at runtime. Immutable descriptors retain the complete + # BAML type, including nested arguments, until the walker commits to wire. + module Types + INSTANCE_ARGS = :@__baml_type_args + @registered = {} + module_function + + def register(klass, name, arity = nil) + @registered[klass] = [name, arity].freeze + end + + def list(item) + [:list, token(item)].freeze + end + + def map(key, value) + [:map, token(key), token(value)].freeze + end + + def optional(inner) + [:union, [token(inner), [:null].freeze].freeze].freeze + end + + def generic(klass, *args) + name, arity = @registered.fetch(klass) { raise TypeError, "#{klass} is not a registered BAML class" } + raise TypeError, "#{klass} is not a generic BAML class" unless arity && arity.positive? + check_arity(klass, arity, args.length) + [:class, name, args.map { |arg| token(arg) }.freeze].freeze + end + + def token(type) + return immutable(type) if type.is_a?(Array) + + primitive = { Integer => :int, String => :string, Float => :float, T::Boolean => :bool, NilClass => :null }[type] + return [primitive].freeze if primitive + + name, arity = @registered.fetch(type) { raise TypeError, "#{type.inspect} is not a BAML type token" } + if arity + raise TypeError, "Use Baml::Types.generic(#{type}, ...) to supply its #{arity} type argument(s)" if arity.positive? + [:class, name, [].freeze].freeze + else + [:enum, name].freeze + end + end + + def immutable(value) + case value + when Array then value.map { |item| immutable(item) }.freeze + when String then value.dup.freeze + else value + end + end + private_class_method :immutable + + def bind(instance, *types) + _name, arity = @registered.fetch(instance.class) { raise TypeError, "#{instance.class} is not a registered BAML class" } + raise TypeError, "#{instance.class} is not a generic BAML class" unless arity && arity.positive? + check_arity(instance.class, arity, types.length) + args = types.map { |type| immutable(Bridge::Encode::Slot.stated_form(token(type))) }.freeze + instance.instance_variable_set(INSTANCE_ARGS, args) + instance + end + + def instance_args(instance) + instance.instance_variable_get(INSTANCE_ARGS) || [] + end + + def check_arity(klass, expected, actual) + return if expected == actual + raise TypeError, "expected #{expected} type argument(s) for #{klass}, got #{actual}" + end + end + + # Preserve the instance's Sorbet type and identity. T::Struct#with constructs + # a new object and drops this binding; bind the copy again when needed. + def self.bind_types(instance, *types) + Types.bind(instance, *types) + end +end diff --git a/baml_language/sdks/ruby/bridge_ruby/lib/baml/media.rb b/baml_language/sdks/ruby/bridge_ruby/lib/baml/media.rb new file mode 100644 index 00000000000..8d6e7114b57 --- /dev/null +++ b/baml_language/sdks/ruby/bridge_ruby/lib/baml/media.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Baml + # Portable media content. Construction copies its strings so neither the + # caller nor an engine handle can mutate a value after it has been built. + class Media + attr_reader :url, :base64, :mime_type, :name + + def self.from_url(url, mime_type: nil) + new(url: url, mime_type: mime_type) + end + + def self.from_base64(base64, mime_type: nil) + new(base64: base64, mime_type: mime_type) + end + + def self.from_file(file, mime_type: nil) + from_file_content(file, [File.binread(file)].pack("m0"), mime_type: mime_type) + end + + def self.from_file_content(file, base64, mime_type: nil) + name = File.basename(file) + new(base64: base64, name: name, mime_type: mime_type) + end + + def initialize(url: nil, base64: "", mime_type: nil, name: nil) + @url, @base64, @mime_type, @name = [url, base64, mime_type, name].map do |value| + raise TypeError, "Media content must be a String" unless value.nil? || value.is_a?(String) + + value&.dup&.freeze + end + freeze + end + def ==(other) + other.instance_of?(self.class) && + [url, base64, mime_type, name] == [other.url, other.base64, other.mime_type, other.name] + end + alias eql? == + + def hash + [self.class, url, base64, mime_type, name].hash + end + + private_class_method :new + end + + class Image < Media; end + class Audio < Media; end + class Video < Media; end + class Pdf < Media; end +end diff --git a/baml_language/sdks/ruby/bridge_ruby/lib/baml/options.rb b/baml_language/sdks/ruby/bridge_ruby/lib/baml/options.rb new file mode 100644 index 00000000000..54db44487a8 --- /dev/null +++ b/baml_language/sdks/ruby/bridge_ruby/lib/baml/options.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Baml + class Unset + private_class_method :new + end + UNSET = Unset.send(:new).freeze + + # Call controls are immutable. The engine clock fixes the deadline before + # argument encoding, so conversion time counts against the timeout too. + class Options + attr_reader :timeout_ms, :cancel, :trace + + def initialize(timeout_ms: nil, cancel: nil, trace: nil) + unless timeout_ms.nil? || (timeout_ms.is_a?(Integer) && (0..2_147_483_647).cover?(timeout_ms)) + raise ArgumentError, "timeout_ms must be an integer in 0…2147483647 or nil" + end + raise ArgumentError, "trace only supports nil for now" unless trace.nil? + + @timeout_ms = timeout_ms + @cancel = cancel + @trace = trace + freeze + end + end +end diff --git a/baml_language/sdks/ruby/bridge_ruby/lib/baml/value_equality.rb b/baml_language/sdks/ruby/bridge_ruby/lib/baml/value_equality.rb new file mode 100644 index 00000000000..e430ce4e46c --- /dev/null +++ b/baml_language/sdks/ruby/bridge_ruby/lib/baml/value_equality.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Baml + # Generated structs are values. Exact runtime type arguments are part of a + # generic value's identity, even though Sorbet erases its [] specialization. + module ValueEquality + def ==(other) + other.instance_of?(self.class) && + Types.instance_args(self) == Types.instance_args(other) && + self.class.props.each_key.all? { |field| public_send(field) == other.public_send(field) } + end + + def eql?(other) + other.instance_of?(self.class) && + Types.instance_args(self).eql?(Types.instance_args(other)) && + self.class.props.each_key.all? { |field| public_send(field).eql?(other.public_send(field)) } + end + + def hash + [self.class, Types.instance_args(self), self.class.props.keys.map { |field| public_send(field) }].hash + end + end +end diff --git a/baml_language/sdks/ruby/bridge_ruby/lib/baml_bridge/cffi/v1/baml_handle_pb.rb b/baml_language/sdks/ruby/bridge_ruby/lib/baml_bridge/cffi/v1/baml_handle_pb.rb new file mode 100644 index 00000000000..fe6022b3e86 --- /dev/null +++ b/baml_language/sdks/ruby/bridge_ruby/lib/baml_bridge/cffi/v1/baml_handle_pb.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: baml_bridge/cffi/v1/baml_handle.proto + +require 'google/protobuf' + + +descriptor_data = "\n%baml_bridge/cffi/v1/baml_handle.proto\x12\x13\x62\x61ml_bridge.cffi.v1\"S\n\nBamlHandle\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\x38\n\x0bhandle_type\x18\x02 \x01(\x0e\x32#.baml_bridge.cffi.v1.BamlHandleType*\xa7\x03\n\x0e\x42\x61mlHandleType\x12\x16\n\x12HANDLE_UNSPECIFIED\x10\x00\x12\x16\n\x12UNTAGGED_RUST_DATA\x10\x01\x12\x15\n\x11UNTAGGED_BEX_HEAP\x10\x02\x12\x10\n\x0c\x46UNCTION_REF\x10\x05\x12\x13\n\x0f\x41\x44T_MEDIA_IMAGE\x10\x06\x12\x13\n\x0f\x41\x44T_MEDIA_AUDIO\x10\x07\x12\x13\n\x0f\x41\x44T_MEDIA_VIDEO\x10\x08\x12\x11\n\rADT_MEDIA_PDF\x10\t\x12\x15\n\x11\x41\x44T_MEDIA_GENERIC\x10\n\x12\x12\n\x0e\x41\x44T_PROMPT_AST\x10\x0b\x12\x0c\n\x08\x41\x44T_TYPE\x10\r\x12\x1a\n\x16\x41\x44T_TAGGED_HEAP_HANDLE\x10\x0e\x12\x17\n\x13HOST_VALUE_CALLABLE\x10\x0f\x12\x15\n\x11HOST_VALUE_OPAQUE\x10\x10\x12\x15\n\x11\x41\x44T_FUNCTION_SPEC\x10\x11\x12\x15\n\x11\x41\x44T_RUNTIME_VALUE\x10\x12\x12\x14\n\x10INVOCATION_STATE\x10\x13\x12\x15\n\x11TRACE_RESERVATION\x10\x14\"\x04\x08\x03\x10\x03\"\x04\x08\x04\x10\x04\x42\nH\x03Z\x06./cffib\x06proto3" + +pool = ::Google::Protobuf::DescriptorPool.generated_pool +pool.add_serialized_file(descriptor_data) + +module BamlBridge + module Cffi + module V1 + BamlHandle = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlHandle").msgclass + BamlHandleType = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlHandleType").enummodule + end + end +end diff --git a/baml_language/sdks/ruby/bridge_ruby/lib/baml_bridge/cffi/v1/baml_inbound_pb.rb b/baml_language/sdks/ruby/bridge_ruby/lib/baml_bridge/cffi/v1/baml_inbound_pb.rb new file mode 100644 index 00000000000..9d0dd9e5c19 --- /dev/null +++ b/baml_language/sdks/ruby/bridge_ruby/lib/baml_bridge/cffi/v1/baml_inbound_pb.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: baml_bridge/cffi/v1/baml_inbound.proto + +require 'google/protobuf' + +require 'baml_bridge/cffi/v1/baml_handle_pb' +require 'baml_bridge/cffi/v1/baml_type_pb' +require 'baml_bridge/cffi/v1/baml_outbound_pb' + + +descriptor_data = "\n&baml_bridge/cffi/v1/baml_inbound.proto\x12\x13\x62\x61ml_bridge.cffi.v1\x1a%baml_bridge/cffi/v1/baml_handle.proto\x1a#baml_bridge/cffi/v1/baml_type.proto\x1a\'baml_bridge/cffi/v1/baml_outbound.proto\"\xde\x05\n\x0cInboundValue\x12/\n\nvalue_type\x18\x01 \x01(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTy\x12\x16\n\x0cstring_value\x18\x02 \x01(\tH\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x12\x15\n\x0b\x66loat_value\x18\x04 \x01(\x01H\x00\x12\x14\n\nbool_value\x18\x05 \x01(\x08H\x00\x12;\n\nlist_value\x18\x06 \x01(\x0b\x32%.baml_bridge.cffi.v1.InboundListValueH\x00\x12\x39\n\tmap_value\x18\x07 \x01(\x0b\x32$.baml_bridge.cffi.v1.InboundMapValueH\x00\x12=\n\x0b\x63lass_value\x18\x08 \x01(\x0b\x32&.baml_bridge.cffi.v1.InboundClassValueH\x00\x12;\n\nenum_value\x18\t \x01(\x0b\x32%.baml_bridge.cffi.v1.InboundEnumValueH\x00\x12\x31\n\x06handle\x18\n \x01(\x0b\x32\x1f.baml_bridge.cffi.v1.BamlHandleH\x00\x12\x1a\n\x10uint8array_value\x18\x0b \x01(\x0cH\x00\x12\x16\n\x0c\x62igint_value\x18\x0c \x01(\tH\x00\x12/\n\x08ty_value\x18\r \x01(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTyH\x00\x12:\n\x0bmedia_value\x18\x0f \x01(\x0b\x32#.baml_bridge.cffi.v1.BamlValueMediaH\x00\x12\x43\n\x10prompt_ast_value\x18\x10 \x01(\x0b\x32\'.baml_bridge.cffi.v1.BamlValuePromptAstH\x00\x12\x19\n\x0fjs_number_value\x18\x11 \x01(\x01H\x00\x42\x07\n\x05valueJ\x04\x08\x0e\x10\x0fR\x0cty_def_value\"E\n\x10InboundListValue\x12\x31\n\x06values\x18\x01 \x03(\x0b\x32!.baml_bridge.cffi.v1.InboundValue\"H\n\x0fInboundMapValue\x12\x35\n\x07\x65ntries\x18\x01 \x03(\x0b\x32$.baml_bridge.cffi.v1.InboundMapEntry\"\xc2\x01\n\x0fInboundMapEntry\x12\x14\n\nstring_key\x18\x01 \x01(\tH\x00\x12\x11\n\x07int_key\x18\x02 \x01(\x03H\x00\x12\x12\n\x08\x62ool_key\x18\x03 \x01(\x08H\x00\x12\x39\n\x08\x65num_key\x18\x05 \x01(\x0b\x32%.baml_bridge.cffi.v1.InboundEnumValueH\x00\x12\x30\n\x05value\x18\x06 \x01(\x0b\x32!.baml_bridge.cffi.v1.InboundValueB\x05\n\x03key\"O\n\x11InboundClassValue\x12\x34\n\x06\x66ields\x18\x02 \x03(\x0b\x32$.baml_bridge.cffi.v1.InboundMapEntryJ\x04\x08\x01\x10\x02\"/\n\x10InboundEnumValue\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"e\n\tBamlTyArg\x12\x10\n\x08type_var\x18\x01 \x01(\t\x12/\n\ntype_value\x18\x02 \x01(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTyJ\x04\x08\x03\x10\x04R\x0ftype_definition\"\x8b\x02\n\x10\x43\x61llFunctionArgs\x12\x34\n\x06kwargs\x18\x01 \x03(\x0b\x32$.baml_bridge.cffi.v1.InboundMapEntry\x12\x0f\n\x07\x63\x61ll_id\x18\x02 \x01(\x04\x12\x31\n\ttype_args\x18\x03 \x03(\x0b\x32\x1e.baml_bridge.cffi.v1.BamlTyArg\x12\x17\n\rfunction_name\x18\x04 \x01(\tH\x00\x12\x19\n\x0f\x66unction_handle\x18\x05 \x01(\x04H\x00\x12:\n\ninvocation\x18\x06 \x01(\x0b\x32&.baml_bridge.cffi.v1.InvocationOptionsB\r\n\x0b\x63\x61ll_target\"\xd7\x01\n\x11InvocationOptions\x12\x32\n\x05trace\x18\x01 \x01(\x0b\x32#.baml_bridge.cffi.v1.TraceSelection\x12\x31\n\x06\x63\x61ncel\x18\x02 \x01(\x0b\x32!.baml_bridge.cffi.v1.InboundValue\x12\x18\n\x0b\x64\x65\x61\x64line_ns\x18\x03 \x01(\x04H\x00\x88\x01\x01\x12\x17\n\x0finherited_state\x18\x04 \x01(\x04\x12\x18\n\x10host_environment\x18\x05 \x01(\x04\x42\x0e\n\x0c_deadline_ns\"j\n\x0eTraceSelection\x12\x34\n\x07options\x18\x01 \x01(\x0b\x32!.baml_bridge.cffi.v1.TraceOptionsH\x00\x12\x15\n\x0breservation\x18\x02 \x01(\x04H\x00\x42\x0b\n\tselection\"\xef\x02\n\x0cTraceOptions\x12\x31\n\x04mode\x18\x01 \x01(\x0e\x32\x1e.baml_bridge.cffi.v1.TraceModeH\x00\x88\x01\x01\x12\x13\n\x06inputs\x18\x02 \x01(\x08H\x01\x88\x01\x01\x12\x13\n\x06output\x18\x03 \x01(\x08H\x02\x88\x01\x01\x12\x12\n\x05\x65rror\x18\x04 \x01(\x08H\x03\x88\x01\x01\x12\x18\n\x0b\x64istinct_id\x18\x05 \x01(\tH\x04\x88\x01\x01\x12\x41\n\x08metadata\x18\x06 \x03(\x0b\x32/.baml_bridge.cffi.v1.TraceOptions.MetadataEntry\x1aX\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.baml_bridge.cffi.v1.TraceMetadataValue:\x02\x38\x01\x42\x07\n\x05_modeB\t\n\x07_inputsB\t\n\x07_outputB\x08\n\x06_errorB\x0e\n\x0c_distinct_id\"\x89\x01\n\x12TraceMetadataValue\x12\x16\n\x0cstring_value\x18\x01 \x01(\tH\x00\x12\x13\n\tint_value\x18\x02 \x01(\x12H\x00\x12\x15\n\x0b\x66loat_value\x18\x03 \x01(\x01H\x00\x12\x14\n\nbool_value\x18\x04 \x01(\x08H\x00\x12\x10\n\x06remove\x18\x05 \x01(\x08H\x00\x42\x07\n\x05value\"&\n\x07\x43\x61llAck\x12\x0f\n\x05\x65rror\x18\x01 \x01(\tH\x00\x42\n\n\x08response\"\xf9\x01\n\x12HostCallCompletion\x12\x35\n\x08returned\x18\x01 \x01(\x0b\x32!.baml_bridge.cffi.v1.InboundValueH\x00\x12\x33\n\x06thrown\x18\x02 \x01(\x0b\x32!.baml_bridge.cffi.v1.InboundValueH\x00\x12\x34\n\x06raised\x18\x03 \x01(\x0b\x32\".baml_bridge.cffi.v1.HostExceptionH\x00\x12\x1c\n\x12\x63ontract_violation\x18\x04 \x01(\tH\x00\x12\x18\n\x0e\x62ridge_failure\x18\x05 \x01(\tH\x00\x42\t\n\x07outcome\"|\n\rHostException\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x12\n\nclass_name\x18\x02 \x01(\t\x12\x10\n\x08language\x18\x03 \x01(\t\x12\x16\n\ttraceback\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x0e\n\x06handle\x18\x05 \x01(\x04\x42\x0c\n\n_traceback*j\n\tTraceMode\x12\x1a\n\x16TRACE_MODE_UNSPECIFIED\x10\x00\x12\x15\n\x11TRACE_MODE_HIDDEN\x10\x01\x12\x15\n\x11TRACE_MODE_TIMING\x10\x02\x12\x13\n\x0fTRACE_MODE_SPAN\x10\x03\x42\nH\x03Z\x06./cffib\x06proto3" + +pool = ::Google::Protobuf::DescriptorPool.generated_pool +pool.add_serialized_file(descriptor_data) + +module BamlBridge + module Cffi + module V1 + InboundValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.InboundValue").msgclass + InboundListValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.InboundListValue").msgclass + InboundMapValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.InboundMapValue").msgclass + InboundMapEntry = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.InboundMapEntry").msgclass + InboundClassValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.InboundClassValue").msgclass + InboundEnumValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.InboundEnumValue").msgclass + BamlTyArg = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyArg").msgclass + CallFunctionArgs = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.CallFunctionArgs").msgclass + InvocationOptions = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.InvocationOptions").msgclass + TraceSelection = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.TraceSelection").msgclass + TraceOptions = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.TraceOptions").msgclass + TraceMetadataValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.TraceMetadataValue").msgclass + CallAck = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.CallAck").msgclass + HostCallCompletion = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.HostCallCompletion").msgclass + HostException = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.HostException").msgclass + TraceMode = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.TraceMode").enummodule + end + end +end diff --git a/baml_language/sdks/ruby/bridge_ruby/lib/baml_bridge/cffi/v1/baml_outbound_pb.rb b/baml_language/sdks/ruby/bridge_ruby/lib/baml_bridge/cffi/v1/baml_outbound_pb.rb new file mode 100644 index 00000000000..17146865bbb --- /dev/null +++ b/baml_language/sdks/ruby/bridge_ruby/lib/baml_bridge/cffi/v1/baml_outbound_pb.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: baml_bridge/cffi/v1/baml_outbound.proto + +require 'google/protobuf' + +require 'baml_bridge/cffi/v1/baml_handle_pb' +require 'baml_bridge/cffi/v1/baml_type_pb' + + +descriptor_data = "\n\'baml_bridge/cffi/v1/baml_outbound.proto\x12\x13\x62\x61ml_bridge.cffi.v1\x1a%baml_bridge/cffi/v1/baml_handle.proto\x1a#baml_bridge/cffi/v1/baml_type.proto\"\xc6\x01\n\x12\x42\x61mlOutboundResult\x12\x34\n\x02ok\x18\x01 \x01(\x0b\x32&.baml_bridge.cffi.v1.BamlOutboundValueH\x00\x12\x37\n\x05\x65rror\x18\x02 \x01(\x0b\x32&.baml_bridge.cffi.v1.BamlOutboundErrorH\x00\x12\x37\n\x05panic\x18\x03 \x01(\x0b\x32&.baml_bridge.cffi.v1.BamlOutboundPanicH\x00\x42\x08\n\x06result\"Y\n\x11\x42\x61mlOutboundError\x12\x35\n\x05value\x18\x01 \x01(\x0b\x32&.baml_bridge.cffi.v1.BamlOutboundValue\x12\r\n\x05trace\x18\x02 \x03(\t\"\x83\x01\n\x11\x42\x61mlOutboundPanic\x12\x35\n\x05value\x18\x01 \x01(\x0b\x32&.baml_bridge.cffi.v1.BamlOutboundValue\x12\r\n\x05trace\x18\x02 \x03(\t\x12\x15\n\ris_exit_panic\x18\x03 \x01(\x08\x12\x11\n\texit_code\x18\x04 \x01(\x03\"\xde\x06\n\x11\x42\x61mlOutboundValue\x12\x38\n\nnull_value\x18\x02 \x01(\x0b\x32\".baml_bridge.cffi.v1.BamlValueNullH\x00\x12\x16\n\x0cstring_value\x18\x03 \x01(\tH\x00\x12\x13\n\tint_value\x18\x04 \x01(\x03H\x00\x12\x15\n\x0b\x66loat_value\x18\x05 \x01(\x01H\x00\x12\x14\n\nbool_value\x18\x06 \x01(\x08H\x00\x12:\n\x0b\x63lass_value\x18\x07 \x01(\x0b\x32#.baml_bridge.cffi.v1.BamlValueClassH\x00\x12\x38\n\nenum_value\x18\x08 \x01(\x0b\x32\".baml_bridge.cffi.v1.BamlValueEnumH\x00\x12>\n\rliteral_value\x18\t \x01(\x0b\x32%.baml_bridge.cffi.v1.BamlLiteralValueH\x00\x12\x38\n\nlist_value\x18\x0b \x01(\x0b\x32\".baml_bridge.cffi.v1.BamlValueListH\x00\x12\x36\n\tmap_value\x18\x0c \x01(\x0b\x32!.baml_bridge.cffi.v1.BamlValueMapH\x00\x12I\n\x13union_variant_value\x18\r \x01(\x0b\x32*.baml_bridge.cffi.v1.BamlValueUnionVariantH\x00\x12?\n\x0chandle_value\x18\x10 \x01(\x0b\x32\'.baml_bridge.cffi.v1.BamlOutboundHandleH\x00\x12:\n\x0bmedia_value\x18\x11 \x01(\x0b\x32#.baml_bridge.cffi.v1.BamlValueMediaH\x00\x12\x43\n\x10prompt_ast_value\x18\x12 \x01(\x0b\x32\'.baml_bridge.cffi.v1.BamlValuePromptAstH\x00\x12\x1a\n\x10uint8array_value\x18\x13 \x01(\x0cH\x00\x12\x16\n\x0c\x62igint_value\x18\x14 \x01(\tH\x00\x12/\n\x08ty_value\x18\x15 \x01(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTyH\x00\x42\x07\n\x05valueJ\x04\x08\x16\x10\x17R\x0cty_def_value\"\x84\x01\n\x12\x42\x61mlOutboundHandle\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\x38\n\x0bhandle_type\x18\x02 \x01(\x0e\x32#.baml_bridge.cffi.v1.BamlHandleType\x12\'\n\x02ty\x18\x03 \x01(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTy\"\x0f\n\rBamlValueNull\"v\n\rBamlValueList\x12.\n\titem_type\x18\x01 \x01(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTy\x12\x35\n\x05items\x18\x02 \x03(\x0b\x32&.baml_bridge.cffi.v1.BamlOutboundValue\"Z\n\x14\x42\x61mlOutboundMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x35\n\x05value\x18\x02 \x01(\x0b\x32&.baml_bridge.cffi.v1.BamlOutboundValue\"\xaa\x01\n\x0c\x42\x61mlValueMap\x12-\n\x08key_type\x18\x01 \x01(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTy\x12/\n\nvalue_type\x18\x02 \x01(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTy\x12:\n\x07\x65ntries\x18\x03 \x03(\x0b\x32).baml_bridge.cffi.v1.BamlOutboundMapEntry\"\x89\x01\n\x0e\x42\x61mlValueClass\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x39\n\x06\x66ields\x18\x02 \x03(\x0b\x32).baml_bridge.cffi.v1.BamlOutboundMapEntry\x12.\n\ttype_args\x18\x03 \x03(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTy\"@\n\rBamlValueEnum\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\x12\x12\n\nis_dynamic\x18\x03 \x01(\x08\"\x9b\x02\n\x15\x42\x61mlValueUnionVariant\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0bis_optional\x18\x02 \x01(\x08\x12\x19\n\x11is_single_pattern\x18\x03 \x01(\x08\x12.\n\tself_type\x18\x04 \x01(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTy\x12\x19\n\x11value_option_name\x18\x05 \x01(\t\x12\x35\n\x05value\x18\x06 \x01(\x0b\x32&.baml_bridge.cffi.v1.BamlOutboundValue\x12\"\n\x15selected_option_index\x18\x08 \x01(\rH\x00\x88\x01\x01\x42\x18\n\x16_selected_option_indexJ\x04\x08\x07\x10\x08\"\xe7\x01\n\x0e\x42\x61mlValueMedia\x12\x31\n\x05media\x18\x01 \x01(\x0e\x32\".baml_bridge.cffi.v1.MediaTypeEnum\x12\x16\n\tmime_type\x18\x02 \x01(\tH\x01\x88\x01\x01\x12\r\n\x03url\x18\x03 \x01(\tH\x00\x12\x10\n\x06\x62\x61se64\x18\x04 \x01(\tH\x00\x12\x46\n\x0c\x66ile_content\x18\x06 \x01(\x0b\x32..baml_bridge.cffi.v1.BamlValueMediaFileContentH\x00\x42\x07\n\x05valueB\x0c\n\n_mime_typeJ\x04\x08\x05\x10\x06R\x04\x66ile\"9\n\x19\x42\x61mlValueMediaFileContent\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x62\x61se64\x18\x02 \x01(\t\"\xe6\x01\n\x12\x42\x61mlValuePromptAst\x12?\n\x06simple\x18\x01 \x01(\x0b\x32-.baml_bridge.cffi.v1.BamlValuePromptAstSimpleH\x00\x12\x41\n\x07message\x18\x02 \x01(\x0b\x32..baml_bridge.cffi.v1.BamlValuePromptAstMessageH\x00\x12\x43\n\x08multiple\x18\x03 \x01(\x0b\x32/.baml_bridge.cffi.v1.BamlValuePromptAstMultipleH\x00\x42\x07\n\x05value\"\x83\x01\n\x19\x42\x61mlValuePromptAstMessage\x12\x0c\n\x04role\x18\x01 \x01(\t\x12>\n\x07\x63ontent\x18\x02 \x01(\x0b\x32-.baml_bridge.cffi.v1.BamlValuePromptAstSimple\x12\x18\n\x10metadata_as_json\x18\x03 \x01(\t\"T\n\x1a\x42\x61mlValuePromptAstMultiple\x12\x36\n\x05items\x18\x01 \x03(\x0b\x32\'.baml_bridge.cffi.v1.BamlValuePromptAst\"\xb6\x01\n\x18\x42\x61mlValuePromptAstSimple\x12\x10\n\x06string\x18\x01 \x01(\tH\x00\x12\x34\n\x05media\x18\x02 \x01(\x0b\x32#.baml_bridge.cffi.v1.BamlValueMediaH\x00\x12I\n\x08multiple\x18\x03 \x01(\x0b\x32\x35.baml_bridge.cffi.v1.BamlValuePromptAstSimpleMultipleH\x00\x42\x07\n\x05value\"`\n BamlValuePromptAstSimpleMultiple\x12<\n\x05items\x18\x01 \x03(\x0b\x32-.baml_bridge.cffi.v1.BamlValuePromptAstSimple\"B\n\x0e\x42\x61mlToHostCall\x12\x30\n\x04\x61rgs\x18\x01 \x03(\x0b\x32\".baml_bridge.cffi.v1.BamlToHostArg\"\xec\x01\n\x0eHostInvocation\x12\x16\n\x0ehost_value_key\x18\x01 \x01(\x04\x12\x13\n\x0b\x63\x61llback_id\x18\x02 \x01(\r\x12\x18\n\x10\x61pplication_args\x18\x03 \x01(\x0c\x12\x17\n\x0f\x65\x66\x66\x65\x63tive_state\x18\x04 \x01(\x04\x12\x18\n\x10host_environment\x18\x05 \x01(\x04\x12\x36\n\x06\x63\x61ncel\x18\x06 \x01(\x0b\x32&.baml_bridge.cffi.v1.BamlOutboundValue\x12\x18\n\x0b\x64\x65\x61\x64line_ns\x18\x07 \x01(\x04H\x00\x88\x01\x01\x42\x0e\n\x0c_deadline_ns\"q\n\rBamlToHostArg\x12\x35\n\x05value\x18\x01 \x01(\x0b\x32&.baml_bridge.cffi.v1.BamlOutboundValue\x12\x10\n\x08\x61rg_name\x18\x02 \x01(\t\x12\x17\n\x0fis_optional_arg\x18\x03 \x01(\x08\"\x8f\x01\n\x10\x42\x61mlLiteralValue\x12\x16\n\x0cstring_value\x18\x01 \x01(\tH\x00\x12\x13\n\tint_value\x18\x02 \x01(\x03H\x00\x12\x14\n\nbool_value\x18\x03 \x01(\x08H\x00\x12\x16\n\x0c\x62igint_value\x18\x04 \x01(\tH\x00\x12\x15\n\x0b\x66loat_value\x18\x05 \x01(\tH\x00\x42\t\n\x07literal*`\n\rMediaTypeEnum\x12\x1a\n\x16MEDIA_TYPE_UNSPECIFIED\x10\x00\x12\t\n\x05IMAGE\x10\x01\x12\t\n\x05\x41UDIO\x10\x02\x12\x07\n\x03PDF\x10\x03\x12\t\n\x05VIDEO\x10\x04\x12\t\n\x05OTHER\x10\x05\x42\nH\x03Z\x06./cffib\x06proto3" + +pool = ::Google::Protobuf::DescriptorPool.generated_pool +pool.add_serialized_file(descriptor_data) + +module BamlBridge + module Cffi + module V1 + BamlOutboundResult = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlOutboundResult").msgclass + BamlOutboundError = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlOutboundError").msgclass + BamlOutboundPanic = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlOutboundPanic").msgclass + BamlOutboundValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlOutboundValue").msgclass + BamlOutboundHandle = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlOutboundHandle").msgclass + BamlValueNull = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlValueNull").msgclass + BamlValueList = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlValueList").msgclass + BamlOutboundMapEntry = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlOutboundMapEntry").msgclass + BamlValueMap = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlValueMap").msgclass + BamlValueClass = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlValueClass").msgclass + BamlValueEnum = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlValueEnum").msgclass + BamlValueUnionVariant = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlValueUnionVariant").msgclass + BamlValueMedia = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlValueMedia").msgclass + BamlValueMediaFileContent = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlValueMediaFileContent").msgclass + BamlValuePromptAst = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlValuePromptAst").msgclass + BamlValuePromptAstMessage = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlValuePromptAstMessage").msgclass + BamlValuePromptAstMultiple = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlValuePromptAstMultiple").msgclass + BamlValuePromptAstSimple = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlValuePromptAstSimple").msgclass + BamlValuePromptAstSimpleMultiple = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlValuePromptAstSimpleMultiple").msgclass + BamlToHostCall = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlToHostCall").msgclass + HostInvocation = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.HostInvocation").msgclass + BamlToHostArg = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlToHostArg").msgclass + BamlLiteralValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlLiteralValue").msgclass + MediaTypeEnum = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.MediaTypeEnum").enummodule + end + end +end diff --git a/baml_language/sdks/ruby/bridge_ruby/lib/baml_bridge/cffi/v1/baml_type_pb.rb b/baml_language/sdks/ruby/bridge_ruby/lib/baml_bridge/cffi/v1/baml_type_pb.rb new file mode 100644 index 00000000000..b64279100d4 --- /dev/null +++ b/baml_language/sdks/ruby/bridge_ruby/lib/baml_bridge/cffi/v1/baml_type_pb.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: baml_bridge/cffi/v1/baml_type.proto + +require 'google/protobuf' + + +descriptor_data = "\n#baml_bridge/cffi/v1/baml_type.proto\x12\x13\x62\x61ml_bridge.cffi.v1\"\xfd\t\n\x06\x42\x61mlTy\x12\x39\n\tprimitive\x18\x01 \x01(\x0b\x32$.baml_bridge.cffi.v1.BamlTyPrimitiveH\x00\x12\x34\n\x08\x63lass_ty\x18\x02 \x01(\x0b\x32 .baml_bridge.cffi.v1.BamlTyClassH\x00\x12/\n\x04\x65num\x18\x03 \x01(\x0b\x32\x1f.baml_bridge.cffi.v1.BamlTyEnumH\x00\x12/\n\x04list\x18\x04 \x01(\x0b\x32\x1f.baml_bridge.cffi.v1.BamlTyListH\x00\x12-\n\x03map\x18\x05 \x01(\x0b\x32\x1e.baml_bridge.cffi.v1.BamlTyMapH\x00\x12\x37\n\x08optional\x18\x06 \x01(\x0b\x32#.baml_bridge.cffi.v1.BamlTyOptionalH\x00\x12\x31\n\x05union\x18\x07 \x01(\x0b\x32 .baml_bridge.cffi.v1.BamlTyUnionH\x00\x12\x35\n\x07literal\x18\x08 \x01(\x0b\x32\".baml_bridge.cffi.v1.BamlTyLiteralH\x00\x12:\n\ntype_alias\x18\t \x01(\x0b\x32$.baml_bridge.cffi.v1.BamlTyTypeAliasH\x00\x12\x35\n\x07unknown\x18\n \x01(\x0b\x32\".baml_bridge.cffi.v1.BamlTyUnknownH\x00\x12\x31\n\x05media\x18\x0b \x01(\x0b\x32 .baml_bridge.cffi.v1.BamlTyMediaH\x00\x12\x39\n\tinterface\x18\x0c \x01(\x0b\x32$.baml_bridge.cffi.v1.BamlTyInterfaceH\x00\x12>\n\x0c\x65num_variant\x18\r \x01(\x0b\x32&.baml_bridge.cffi.v1.BamlTyEnumVariantH\x00\x12\x37\n\x08\x66unction\x18\x0e \x01(\x0b\x32#.baml_bridge.cffi.v1.BamlTyFunctionH\x00\x12\x33\n\x06\x66uture\x18\x0f \x01(\x0b\x32!.baml_bridge.cffi.v1.BamlTyFutureH\x00\x12\x38\n\trust_type\x18\x10 \x01(\x0b\x32#.baml_bridge.cffi.v1.BamlTyRustTypeH\x00\x12\x38\n\tmeta_type\x18\x11 \x01(\x0b\x32#.baml_bridge.cffi.v1.BamlTyMetaTypeH\x00\x12\x37\n\x08resource\x18\x12 \x01(\x0b\x32#.baml_bridge.cffi.v1.BamlTyResourceH\x00\x12:\n\nprompt_ast\x18\x13 \x01(\x0b\x32$.baml_bridge.cffi.v1.BamlTyPromptAstH\x00\x12\x36\n\x08type_var\x18\x16 \x01(\x0b\x32\".baml_bridge.cffi.v1.BamlTyTypeVarH\x00\x12Y\n\x1a\x61ssociated_type_projection\x18\x17 \x01(\x0b\x32\x33.baml_bridge.cffi.v1.BamlTyAssociatedTypeProjectionH\x00\x12\x31\n\x05never\x18\x18 \x01(\x0b\x32 .baml_bridge.cffi.v1.BamlTyNeverH\x00\x42\x04\n\x02tyJ\x04\x08\x14\x10\x15R\x04void\"I\n\x0f\x42\x61mlTyPrimitive\x12\x36\n\x04kind\x18\x01 \x01(\x0e\x32(.baml_bridge.cffi.v1.BamlTyPrimitiveKind\"\x80\x01\n\x0b\x42\x61mlTyClass\x12\x0e\n\x04name\x18\x01 \x01(\tH\x00\x12\x0e\n\x04live\x18\x03 \x01(\x04H\x00\x12\x19\n\x0f\x61nonymous_label\x18\x04 \x01(\tH\x00\x12.\n\ttype_args\x18\x02 \x03(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTyB\x06\n\x04head\"\x84\x01\n\x0f\x42\x61mlTyTypeAlias\x12\x0e\n\x04name\x18\x01 \x01(\tH\x00\x12\x0e\n\x04live\x18\x03 \x01(\x04H\x00\x12\x19\n\x0f\x61nonymous_label\x18\x04 \x01(\tH\x00\x12.\n\ttype_args\x18\x02 \x03(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTyB\x06\n\x04head\"O\n\nBamlTyEnum\x12\x0e\n\x04name\x18\x01 \x01(\tH\x00\x12\x0e\n\x04live\x18\x02 \x01(\x04H\x00\x12\x19\n\x0f\x61nonymous_label\x18\x03 \x01(\tH\x00\x42\x06\n\x04head\"7\n\nBamlTyList\x12)\n\x04item\x18\x01 \x01(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTy\"a\n\tBamlTyMap\x12(\n\x03key\x18\x01 \x01(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTy\x12*\n\x05value\x18\x02 \x01(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTy\"<\n\x0e\x42\x61mlTyOptional\x12*\n\x05inner\x18\x01 \x01(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTy\";\n\x0b\x42\x61mlTyUnion\x12,\n\x07options\x18\x01 \x03(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTy\"\x0f\n\rBamlTyUnknown\"\x8c\x01\n\rBamlTyLiteral\x12\x16\n\x0cstring_value\x18\x01 \x01(\tH\x00\x12\x13\n\tint_value\x18\x02 \x01(\x03H\x00\x12\x14\n\nbool_value\x18\x03 \x01(\x08H\x00\x12\x16\n\x0c\x62igint_value\x18\x04 \x01(\tH\x00\x12\x15\n\x0b\x66loat_value\x18\x05 \x01(\tH\x00\x42\t\n\x07literal\"A\n\x0b\x42\x61mlTyMedia\x12\x32\n\x04kind\x18\x01 \x01(\x0e\x32$.baml_bridge.cffi.v1.BamlTyMediaKind\"\xc4\x01\n\x0f\x42\x61mlTyInterface\x12\x0e\n\x04name\x18\x01 \x01(\tH\x00\x12\x0e\n\x04live\x18\x04 \x01(\x04H\x00\x12\x19\n\x0f\x61nonymous_label\x18\x05 \x01(\tH\x00\x12.\n\ttype_args\x18\x02 \x03(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTy\x12>\n\x08\x62indings\x18\x03 \x03(\x0b\x32,.baml_bridge.cffi.v1.BamlTyAssociatedBindingB\x06\n\x04head\"P\n\x17\x42\x61mlTyAssociatedBinding\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\'\n\x02ty\x18\x02 \x01(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTy\"g\n\x11\x42\x61mlTyEnumVariant\x12\x0e\n\x04name\x18\x01 \x01(\tH\x00\x12\x0e\n\x04live\x18\x03 \x01(\x04H\x00\x12\x19\n\x0f\x61nonymous_label\x18\x04 \x01(\tH\x00\x12\x0f\n\x07variant\x18\x02 \x01(\tB\x06\n\x04head\"\x96\x01\n\x13\x42\x61mlTyFunctionParam\x12\x11\n\x04name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\'\n\x02ty\x18\x02 \x01(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTy\x12:\n\x04mode\x18\x03 \x01(\x0e\x32,.baml_bridge.cffi.v1.BamlTyFunctionParamModeB\x07\n\x05_name\"\xbf\x01\n\x0e\x42\x61mlTyFunction\x12\x16\n\x0egeneric_params\x18\x01 \x03(\t\x12\x38\n\x06params\x18\x03 \x03(\x0b\x32(.baml_bridge.cffi.v1.BamlTyFunctionParam\x12(\n\x03ret\x18\x04 \x01(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTy\x12+\n\x06throws\x18\x05 \x01(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTyJ\x04\x08\x02\x10\x03\"f\n\x0c\x42\x61mlTyFuture\x12*\n\x05value\x18\x01 \x01(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTy\x12*\n\x05\x65rror\x18\x02 \x01(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTy\"\x10\n\x0e\x42\x61mlTyRustType\"\x10\n\x0e\x42\x61mlTyMetaType\"\x10\n\x0e\x42\x61mlTyResource\"\x11\n\x0f\x42\x61mlTyPromptAst\",\n\rBamlTyTypeVar\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05index\x18\x02 \x01(\r\"\x8b\x01\n\x1e\x42\x61mlTyAssociatedTypeProjection\x12)\n\x04\x62\x61se\x18\x01 \x01(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTy\x12.\n\tinterface\x18\x02 \x01(\x0b\x32\x1b.baml_bridge.cffi.v1.BamlTy\x12\x0e\n\x06member\x18\x03 \x01(\t\"\r\n\x0b\x42\x61mlTyNever*\x81\x02\n\x13\x42\x61mlTyPrimitiveKind\x12!\n\x1d\x42\x41ML_TY_PRIMITIVE_UNSPECIFIED\x10\x00\x12\x1c\n\x18\x42\x41ML_TY_PRIMITIVE_STRING\x10\x01\x12\x19\n\x15\x42\x41ML_TY_PRIMITIVE_INT\x10\x02\x12\x1b\n\x17\x42\x41ML_TY_PRIMITIVE_FLOAT\x10\x03\x12\x1a\n\x16\x42\x41ML_TY_PRIMITIVE_BOOL\x10\x04\x12\x1a\n\x16\x42\x41ML_TY_PRIMITIVE_NULL\x10\x05\x12\x1b\n\x17\x42\x41ML_TY_PRIMITIVE_BYTES\x10\x06\x12\x1c\n\x18\x42\x41ML_TY_PRIMITIVE_BIGINT\x10\x07*\xcb\x01\n\x0f\x42\x61mlTyMediaKind\x12\"\n\x1e\x42\x41ML_TY_MEDIA_KIND_UNSPECIFIED\x10\x00\x12\x1c\n\x18\x42\x41ML_TY_MEDIA_KIND_IMAGE\x10\x01\x12\x1c\n\x18\x42\x41ML_TY_MEDIA_KIND_AUDIO\x10\x02\x12\x1c\n\x18\x42\x41ML_TY_MEDIA_KIND_VIDEO\x10\x03\x12\x1a\n\x16\x42\x41ML_TY_MEDIA_KIND_PDF\x10\x04\x12\x1e\n\x1a\x42\x41ML_TY_MEDIA_KIND_GENERIC\x10\x05*\x9a\x01\n\x17\x42\x61mlTyFunctionParamMode\x12+\n\'BAML_TY_FUNCTION_PARAM_MODE_UNSPECIFIED\x10\x00\x12(\n$BAML_TY_FUNCTION_PARAM_MODE_REQUIRED\x10\x01\x12(\n$BAML_TY_FUNCTION_PARAM_MODE_OPTIONAL\x10\x02\x42\nH\x03Z\x06./cffib\x06proto3" + +pool = ::Google::Protobuf::DescriptorPool.generated_pool +pool.add_serialized_file(descriptor_data) + +module BamlBridge + module Cffi + module V1 + BamlTy = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTy").msgclass + BamlTyPrimitive = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyPrimitive").msgclass + BamlTyClass = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyClass").msgclass + BamlTyTypeAlias = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyTypeAlias").msgclass + BamlTyEnum = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyEnum").msgclass + BamlTyList = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyList").msgclass + BamlTyMap = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyMap").msgclass + BamlTyOptional = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyOptional").msgclass + BamlTyUnion = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyUnion").msgclass + BamlTyUnknown = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyUnknown").msgclass + BamlTyLiteral = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyLiteral").msgclass + BamlTyMedia = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyMedia").msgclass + BamlTyInterface = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyInterface").msgclass + BamlTyAssociatedBinding = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyAssociatedBinding").msgclass + BamlTyEnumVariant = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyEnumVariant").msgclass + BamlTyFunctionParam = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyFunctionParam").msgclass + BamlTyFunction = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyFunction").msgclass + BamlTyFuture = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyFuture").msgclass + BamlTyRustType = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyRustType").msgclass + BamlTyMetaType = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyMetaType").msgclass + BamlTyResource = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyResource").msgclass + BamlTyPromptAst = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyPromptAst").msgclass + BamlTyTypeVar = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyTypeVar").msgclass + BamlTyAssociatedTypeProjection = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyAssociatedTypeProjection").msgclass + BamlTyNever = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyNever").msgclass + BamlTyPrimitiveKind = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyPrimitiveKind").enummodule + BamlTyMediaKind = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyMediaKind").enummodule + BamlTyFunctionParamMode = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("baml_bridge.cffi.v1.BamlTyFunctionParamMode").enummodule + end + end +end diff --git a/baml_language/sdks/ruby/bridge_ruby/rbi/baml.rbi b/baml_language/sdks/ruby/bridge_ruby/rbi/baml.rbi new file mode 100644 index 00000000000..2bcc5736d2c --- /dev/null +++ b/baml_language/sdks/ruby/bridge_ruby/rbi/baml.rbi @@ -0,0 +1,267 @@ +# typed: strict +# frozen_string_literal: true + +# Contract between generated `baml_sdk/` code and the Ruby runtime. +# +# Generated code may call only what is declared here. The runtime implements it. +# The Sorbet static gate type-checks generated code against this file, so a +# mismatch between generator and runtime fails the gate rather than a test run. +# +# Later changes extend this file; they do not rename or remove anything in it. + +module Baml + extend T::Sig + + # Attach runtime arguments without changing the instance's Sorbet type. + # T::Struct#with builds a new instance and drops the binding. + sig { type_parameters(:Instance).params(instance: T.type_parameter(:Instance), types: T.untyped).returns(T.type_parameter(:Instance)) } + def self.bind_types(instance, *types); end + + module ValueEquality + extend T::Sig + sig { params(other: T.untyped).returns(T::Boolean) } + def ==(other); end + sig { params(other: T.untyped).returns(T::Boolean) } + def eql?(other); end + sig { returns(Integer) } + def hash; end + end + + module Types + extend T::Sig + + sig { params(item: T.untyped).returns(T.untyped) } + def self.list(item); end + + sig { params(key: T.untyped, value: T.untyped).returns(T.untyped) } + def self.map(key, value); end + + sig { params(inner: T.untyped).returns(T.untyped) } + def self.optional(inner); end + + sig { params(klass: T.untyped, args: T.untyped).returns(T.untyped) } + def self.generic(klass, *args); end + end + + # Raised by Call#wait(timeout:) when the timeout passes. The call keeps running. + class WaitTimeout < StandardError; end + + class Unset; end + UNSET = T.let(T.unsafe(nil), Unset) + + class Options + extend T::Sig + # cancel is the generated baml.spawn.CancelToken, checked at the boundary. + sig { params(timeout_ms: T.nilable(Integer), cancel: T.untyped, trace: NilClass).void } + def initialize(timeout_ms: nil, cancel: nil, trace: nil); end + sig { returns(T.nilable(Integer)) } + def timeout_ms; end + sig { returns(T.untyped) } + def cancel; end + sig { returns(NilClass) } + def trace; end + end + + class Error < StandardError + extend T::Sig + sig { params(value: T.untyped, baml_trace: T::Array[String], type_name: T.nilable(String)).void } + def initialize(value, baml_trace: [], type_name: nil); end + sig { returns(T.untyped) } + def value; end + sig { returns(T::Array[String]) } + def baml_trace; end + sig { returns(T.nilable(String)) } + def type_name; end + end + class Panic < Exception + extend T::Sig + sig { params(value: T.untyped, baml_trace: T::Array[String], type_name: T.nilable(String)).void } + def initialize(value, baml_trace: [], type_name: nil); end + sig { returns(T.untyped) } + def value; end + sig { returns(T::Array[String]) } + def baml_trace; end + sig { returns(T.nilable(String)) } + def type_name; end + end + class CancelledError < Panic; end + + # The promise every BAML call returns. Sync generated methods are `call_async(...).wait`. + class Call + extend T::Sig + extend T::Generic + Value = type_member + + # Block until the outcome is known, then return the decoded value or raise + # the decoded error. Repeated and concurrent waits see the same outcome. + # With a timeout (seconds), raise Baml::WaitTimeout and leave the call running. + sig { params(timeout: T.nilable(Numeric)).returns(Value) } + def wait(timeout: nil); end + + # Ask the engine to cancel. True when the request was accepted, not when + # cancellation won; the eventual outcome still comes from the engine. + sig { returns(T::Boolean) } + def cancel; end + + sig { returns(T::Boolean) } + def done?; end + end + + # An owned engine handle. Released by #close or when garbage-collected. + # Sending a handle to BAML clones it first, so the Ruby object stays valid. + class Handle + extend T::Sig + sig { void } + def close; end + end + + # Frozen portable media values; file constructors read content immediately. + class Media + extend T::Sig + sig { params(url: String, mime_type: T.nilable(String)).returns(T.attached_class) } + def self.from_url(url, mime_type: nil); end + sig { params(base64: String, mime_type: T.nilable(String)).returns(T.attached_class) } + def self.from_base64(base64, mime_type: nil); end + sig { params(file: String, mime_type: T.nilable(String)).returns(T.attached_class) } + def self.from_file(file, mime_type: nil); end + sig { params(file: String, base64: String, mime_type: T.nilable(String)).returns(T.attached_class) } + def self.from_file_content(file, base64, mime_type: nil); end + sig { returns(T.nilable(String)) } + def url; end + sig { returns(String) } + def base64; end + sig { returns(T.nilable(String)) } + def mime_type; end + sig { returns(T.nilable(String)) } + def name; end + end + class Image < Media; end + class Audio < Media; end + class Video < Media; end + class Pdf < Media; end + + # Whole-object engine handles. The runtime never imports the generated SDK. + class Stream < Handle + extend T::Sig + extend T::Generic + Value = type_member + + # Returns a Value partial or the generated ai.stream.Done sentinel. + # The generated sentinel's Ruby name is not known to the gem. + sig { params(_baml: T.untyped).returns(T.untyped) } + def next(_baml: nil); end + + sig { params(_baml: T.untyped).returns(::Baml::Call[T.untyped]) } + def next_async(_baml: nil); end + + sig { params(_baml: T.untyped).returns(Value) } + def final(_baml: nil); end + + sig { params(_baml: T.untyped).returns(::Baml::Call[Value]) } + def final_async(_baml: nil); end + + # Yield non-null partials until Done; without a block, return an enumerator. + sig do + params(blk: T.nilable(T.proc.params(value: Value).void)) + .returns(T.any(::Baml::Stream[Value], T::Enumerator[Value])) + end + def each(&blk); end + end + + class FunctionSpec < Handle + extend T::Sig + + sig { params(client: T.untyped, _baml: T.untyped).returns(T.untyped) } + def build_request(client: nil, _baml: nil); end + + sig { params(client: T.untyped, _baml: T.untyped).returns(::Baml::Call[T.untyped]) } + def build_request_async(client: nil, _baml: nil); end + end + class Closure < Handle + extend T::Sig + sig { params(args: T.untyped, kwargs: T.untyped).returns(T.untyped) } + def call(*args, **kwargs); end + sig { params(args: T.untyped, _baml: T.untyped, kwargs: T.untyped).returns(::Baml::Call[T.untyped]) } + def call_async(*args, _baml: nil, **kwargs); end + end + + module Bridge + extend T::Sig + + class Error < StandardError; end + class UnsupportedTypeError < Error; end + + # Type descriptors are frozen arrays mirroring the generator's Ty. Grammar: + # [:int] [:bigint] [:float] [:string] [:bool] [:null] [:bytes] [:never] [:handle] + # [:literal, value] value: String, Integer, Float, true or false + # [:literal, :bigint, integer] preserves the bigint literal kind + # [:media, kind] kind: :image, :audio, :video, :pdf, :generic + # [:param, index, "T"] compiler type-parameter index and name + # [:alias, "pkg.ns.Name"] + # [:class, "pkg.ns.Name", [type_args...]] + # [:enum, "pkg.ns.Name"] [:enum_variant, "pkg.ns.Name", "Variant"] + # [:unknown] [:interface, "pkg.ns.Name"] + # [:list, d] [:map, key, value] [:union, [members...]] + # [:function, [[name_or_nil, d, optional?], ...], ret, throws] + # [:other] unsupported type + # Values are planned against these slots before any handles are cloned. + Descriptor = T.type_alias { T::Array[T.untyped] } + + # Called once when the generated package loads. Registering a different + # program in the same process raises. + sig { params(bytecode: String, metadata: T.nilable(String)).void } + def self.register_program(bytecode, metadata = nil); end + + # Load the library and initialize the registered program now, instead of + # on the first call. Calling it again is a no-op. + sig { void } + def self.initialize!; end + + # fields: wire field name => [Ruby field name, descriptor] + sig do + params( + wire: String, + klass: T.class_of(T::Struct), + fields: T::Hash[String, [Symbol, Descriptor]], + crossing: Symbol, + type_params: T::Array[String] + ).void + end + def self.define_class(wire, klass, fields:, crossing: :fields, type_params: []); end + + sig { params(wire: String, klass: T.class_of(T::Enum)).void } + def self.define_enum(wire, klass); end + + # Non-recursive aliases state their bodies; recursive aliases state their names. + sig { params(wire: String, descriptor: Descriptor, recursive: T::Boolean).void } + def self.define_alias(wire, descriptor, recursive: false); end + + # params: [wire name, descriptor] in declaration order. + # Instance methods include their receiver as the first param, named "self". + sig do + params( + wire: String, + params: T::Array[[String, Descriptor]], + class_type_params: T::Array[String], + type_params: T::Array[String] + ).void + end + def self.define_function(wire, params:, class_type_params: [], type_params: []); end + + # Start a call. args: wire parameter name => Ruby value; omitted keys are not sent. + # Returned closures use the same call primitive, with an owned target handle. + # types: explicit type bindings (Change D). baml: per-call options (Change C). + # UNSET arguments are omitted. Binding names are method/function parameters; + # class bindings come from the receiver's hidden runtime arguments and + # precede method bindings. + sig do + params( + wire: T.any(String, ::Baml::Closure), + args: T::Hash[String, T.untyped], + types: T.nilable(T::Hash[Symbol, T.untyped]), + baml: T.nilable(::Baml::Options) + ).returns(::Baml::Call[T.untyped]) + end + def self.call_async(wire, args, types: nil, baml: nil); end + end +end diff --git a/baml_language/sdks/ruby/sdkgen_ruby_sorbet/Cargo.toml b/baml_language/sdks/ruby/sdkgen_ruby_sorbet/Cargo.toml new file mode 100644 index 00000000000..fd25355a76c --- /dev/null +++ b/baml_language/sdks/ruby/sdkgen_ruby_sorbet/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "sdkgen_ruby_sorbet" +version = { workspace = true } +publish = false +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +[dependencies] +baml_base = { workspace = true } +baml_sdkgen_types = { workspace = true } +baml_type = { workspace = true } + +[lib] +doctest = false + +[lints] +workspace = true diff --git a/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/descriptors.rs b/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/descriptors.rs new file mode 100644 index 00000000000..72fbb019e3d --- /dev/null +++ b/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/descriptors.rs @@ -0,0 +1,92 @@ +use baml_base::Literal; +use baml_sdkgen_types::{CodegenFunctionParamMode, Ty}; + +use crate::names::quote; + +/// Keep the wire shape independent of which Sorbet types are supported. +pub(crate) fn descriptor(ty: &Ty) -> String { + let body = match ty { + Ty::Int => ":int".into(), + Ty::Bigint => ":bigint".into(), + Ty::Uint8Array => ":bytes".into(), + Ty::Media(kind) => format!( + ":media, :{}", + if matches!(kind, baml_base::MediaKind::Generic) { + "generic" + } else { + kind.tag_str() + } + ), + Ty::Float => ":float".into(), + Ty::String => ":string".into(), + Ty::Bool => ":bool".into(), + Ty::Null => ":null".into(), + Ty::Never => ":never".into(), + Ty::RustType => ":handle".into(), + Ty::Literal(value, _) => format!( + ":literal, {}", + match value { + Literal::String(s) => quote(s), + Literal::Int(v) => v.to_string(), + Literal::Bigint(v) => + format!(":bigint, ::Kernel.Integer({})", quote(&v.to_string())), + Literal::Float(v) => v.clone(), + Literal::Bool(v) => v.to_string(), + } + ), + Ty::Class(name, args) => format!( + ":class, {}, {}", + quote(&name.to_string()), + descriptors(args) + ), + Ty::Enum(name) => format!(":enum, {}", quote(&name.to_string())), + Ty::EnumVariant(name, variant) => format!( + ":enum_variant, {}, {}", + quote(&name.to_string()), + quote(variant.as_str()) + ), + Ty::TypeAlias(name) => format!(":alias, {}", quote(&name.to_string())), + Ty::Unknown => ":unknown".into(), + Ty::Interface(name, ..) => format!(":interface, {}", quote(&name.to_string())), + Ty::Function { + params, + ret, + throws, + .. + } => format!( + ":function, [{}].freeze, {}, {}", + params + .iter() + .map(|param| format!( + "[{}, {}, {}].freeze", + param + .name + .as_ref() + .map_or_else(|| "nil".into(), |name| quote(name.as_str())), + descriptor(¶m.ty), + param.mode == CodegenFunctionParamMode::Optional + )) + .collect::>() + .join(", "), + descriptor(ret), + descriptor(throws) + ), + Ty::List(inner) => format!(":list, {}", descriptor(inner)), + Ty::Map { key, value } => { + format!(":map, {}, {}", descriptor(key), descriptor(value)) + } + Ty::Union(items) => { + format!(":union, {}", descriptors(items)) + } + Ty::TypeVar(param) => format!(":param, {}, {}", param.index(), quote(param.as_str())), + Ty::Type | Ty::Resource | Ty::PromptAst | Ty::Future(..) => ":other".into(), + }; + format!("[{body}].freeze") +} + +fn descriptors(items: &[Ty]) -> String { + format!( + "[{}].freeze", + items.iter().map(descriptor).collect::>().join(", ") + ) +} diff --git a/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/emit.rs b/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/emit.rs new file mode 100644 index 00000000000..34f0703f5d6 --- /dev/null +++ b/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/emit.rs @@ -0,0 +1,477 @@ +use std::{collections::BTreeSet, fmt::Write as _}; + +use baml_sdkgen_types::{ + Class, Enum, Function, FunctionArgument, Name, NamingConvention, Symbol, SymbolPool, Ty, + TypeAlias, +}; + +use crate::{ + descriptors::descriptor, + names::{NameTable, callable_parts, identifier, parameter, quote, variant_constant}, + types::{ruby_alias_type, ruby_input_type, ruby_type, ruby_type_with_params}, +}; + +pub(crate) struct Emitter<'a> { + pub(crate) pool: &'a SymbolPool, + pub(crate) supported: &'a BTreeSet, + pub(crate) naming: NamingConvention, + pub(crate) names: NameTable, + pub(crate) registry: String, + pub(crate) shells: String, + pub(crate) aliases: String, + pub(crate) skipped: BTreeSet, +} + +impl Emitter<'_> { + pub(crate) fn class(&mut self, name: &Name, class: &Class, body: &mut String) { + let scope = self.names.qualified(name); + self.shell(name, "Struct"); + doc_comment( + body, + &member_doc( + class.docstring.as_deref(), + "Attributes", + class + .properties + .iter() + .map(|p| (p.name.as_str(), p.docstring.as_deref())), + ), + ); + writeln!( + body, + "class {} < ::T::Struct\n extend ::T::Sig\n include ::Baml::ValueEquality", + scope.rsplit("::").next().unwrap() + ) + .unwrap(); + let class_params: Vec<_> = class + .generic_params + .iter() + .enumerate() + .map(|(index, _)| format!("BamlTypeParam{index}")) + .collect(); + if !class_params.is_empty() { + // Forward references may apply [] before the class body is loaded. + writeln!(self.shells, "{scope}.extend(::T::Generic)").unwrap(); + body.push_str(" extend ::T::Generic\n"); + for param in &class_params { + writeln!(body, " {param} = type_member").unwrap(); + } + } + let mut fields = Vec::new(); + for property in &class.properties { + let field = self.names.claim( + &scope, + &identifier(property.name.as_str(), self.naming), + &format!("field {name}.{}", property.name), + ); + writeln!( + body, + " const :{field}, {}", + ruby_input_type( + &property.ty, + self.supported, + &self.names, + self.pool, + &class_params + ) + .unwrap() + ) + .unwrap(); + fields.push(format!( + "{} => [:{field}, {}]", + quote(property.name.as_str()), + descriptor(&property.ty) + )); + } + writeln!( + self.registry, + "::Baml::Bridge.define_class({}, ::{scope}, fields: {{{}}}, type_params: [{}], crossing: :{})", + quote(&name.to_string()), + fields.join(", "), + class.generic_params.iter().map(|name| quote(name)).collect::>().join(", "), + match class.crossing { + baml_type::ClassCrossing::Fields => "fields".into(), + baml_type::ClassCrossing::Opaque => "opaque".into(), + baml_type::ClassCrossing::Stream => "stream".into(), + baml_type::ClassCrossing::FunctionSpec => "function_spec".into(), + baml_type::ClassCrossing::Prompt => "prompt".into(), + baml_type::ClassCrossing::Media(kind) => format!( + "media_{}", + if matches!(kind, baml_base::MediaKind::Generic) { + "generic" + } else { + kind.tag_str() + } + ), + } + ) + .unwrap(); + for (instance, methods) in [ + (false, &class.static_methods), + (true, &class.instance_methods), + ] { + for method in methods { + let mut source = String::new(); + self.function( + &scope, + &format!("{name}.{}", method.name), + method, + instance.then_some(name), + &class.generic_params, + &mut source, + ); + for line in source.lines() { + writeln!(body, " {line}").unwrap(); + } + } + } + body.push_str("end\n"); + } + + pub(crate) fn enum_(&mut self, name: &Name, enum_: &Enum, body: &mut String) { + self.shell(name, "Enum"); + let scope = self.names.qualified(name); + doc_comment( + body, + &member_doc( + enum_.docstring.as_deref(), + "Members", + enum_ + .variants + .iter() + .map(|v| (v.name.as_str(), v.docstring.as_deref())), + ), + ); + writeln!( + body, + "class {} < ::T::Enum\n enums do", + scope.rsplit("::").next().unwrap() + ) + .unwrap(); + for variant in &enum_.variants { + let ruby = self.names.claim( + &scope, + &variant_constant(variant.name.as_str()), + &format!("{name}.{}", variant.name), + ); + writeln!(body, " {ruby} = new({})", quote(&variant.value)).unwrap(); + } + body.push_str(" end\nend\n"); + writeln!( + self.registry, + "::Baml::Bridge.define_enum({}, ::{})", + quote(&name.to_string()), + scope + ) + .unwrap(); + } + + pub(crate) fn type_alias(&mut self, name: &Name, alias: &TypeAlias, pool: &SymbolPool) { + let ty = ruby_alias_type( + &Ty::TypeAlias(name.clone()), + pool, + self.supported, + &self.names, + ) + .unwrap(); + writeln!( + self.aliases, + "{} = ::T.type_alias {{ {ty} }}", + self.names.qualified(name) + ) + .unwrap(); + writeln!( + self.registry, + "::Baml::Bridge.define_alias({}, {}, recursive: {})", + quote(&name.to_string()), + descriptor(&alias.resolves_to), + alias.recursive + ) + .unwrap(); + } + + fn shell(&mut self, name: &Name, kind: &str) { + writeln!( + self.shells, + "{}.const_set(:{}, Class.new(::T::{kind}))", + self.names.module_name(name), + self.names.qualified(name).rsplit("::").next().unwrap() + ) + .unwrap(); + } + + pub(crate) fn function( + &mut self, + scope: &str, + wire: &str, + function: &Function, + receiver: Option<&Name>, + class_params: &[baml_base::Name], + body: &mut String, + ) { + if wire.contains('$') + || !function.watchers.is_empty() + || ruby_type(&function.return_type, self.supported, &self.names).is_none() + || function + .arguments + .iter() + .filter(|a| !omit_injected_argument(a, self.supported, &self.names)) + .any(|a| ruby_type(&a.ty, self.supported, &self.names).is_none()) + { + self.skipped.insert(wire.to_owned()); + return; + } + let mut signature_params = Vec::new(); + let mut type_params = Vec::new(); + for index in 0..class_params.len() + function.generic_params.len() { + let name = format!("BamlTypeParam{index}"); + if receiver.is_some() && index < class_params.len() { + signature_params.push(name); + } else { + signature_params.push(format!("::T.type_parameter(:{name})")); + type_params.push(format!(":{name}")); + } + } + let render = |ty| ruby_type_with_params(ty, self.supported, &self.names, &signature_params); + let result = render(&function.return_type).unwrap(); + let argument_types: Vec<_> = function + .arguments + .iter() + .map(|a| { + ruby_input_type( + &a.ty, + self.supported, + &self.names, + self.pool, + &signature_params, + ) + }) + .collect(); + let (base, suffix) = callable_parts(function.name.as_str()); + let base = identifier(base, self.naming); + // Instance and singleton methods have separate Ruby scopes. Fields share + // the instance scope, so an emitted method cannot overwrite a reader. + let name_scope = if receiver.is_some() { + scope.to_owned() + } else { + format!("{scope}.self") + }; + let base = self.names.callable(&name_scope, &base, wire); + let method = format!("{base}{suffix}"); + let mut params = Vec::new(); + let mut keyword_params = Vec::new(); + let mut required = Vec::new(); + let mut keywords = Vec::new(); + let mut arguments = Vec::new(); + let mut descriptors = Vec::new(); + let mut used = BTreeSet::from(["_baml".into(), "_types".into()]); + if let Some(class) = receiver { + arguments.push("\"self\" => self".into()); + descriptors.push(format!( + "[\"self\", {}]", + descriptor(&baml_sdkgen_types::Ty::Class( + class.clone(), + class_params + .iter() + .enumerate() + .map(|(index, name)| { + baml_sdkgen_types::Ty::TypeVar(baml_sdkgen_types::ParamTy::new( + u32::try_from(index).expect("type parameter index fits u32"), + name.clone(), + )) + }) + .collect(), + )) + )); + } + for (argument, argument_type) in function + .arguments + .iter() + .zip(argument_types) + .filter(|(a, _)| !omit_injected_argument(a, self.supported, &self.names)) + { + let arg = parameter(argument.name.as_str(), self.naming, &mut used); + arguments.push(format!("{} => {arg}", quote(argument.name.as_str()))); + let optional = argument.default.is_some(); + let ty = argument_type.unwrap(); + let ty = if optional { + format!("::T.any({ty}, ::Baml::Unset)") + } else { + ty + }; + let signature = format!("{arg}: {ty}"); + if optional { + keyword_params.push(signature); + } else { + params.push(signature); + } + descriptors.push(format!( + "[{}, {}]", + quote(argument.name.as_str()), + descriptor(&argument.ty) + )); + if optional { + keywords.push(format!("{arg}: ::Baml::UNSET")); + } else { + required.push(arg); + } + } + params.extend(keyword_params); + params.push("_baml: ::T.nilable(::Baml::Options)".into()); + keywords.push("_baml: nil".into()); + let generic = !function.generic_params.is_empty() || !class_params.is_empty(); + if generic { + params.push("_types: ::T.nilable(::T::Hash[::Symbol, ::T.untyped])".into()); + keywords.push("_types: nil".into()); + } + required.extend(keywords); + let receiver_params = if receiver.is_some() { + class_params + } else { + &[] + }; + let caller_params: Vec<_> = class_params + .iter() + .filter(|_| receiver.is_none()) + .chain(function.generic_params.iter()) + .collect(); + let receiver_params = receiver_params + .iter() + .map(|name| quote(name)) + .collect::>() + .join(", "); + let caller_params = caller_params + .iter() + .map(|name| quote(name)) + .collect::>() + .join(", "); + writeln!(self.registry, "::Baml::Bridge.define_function({}, params: [{}], class_type_params: [{}], type_params: [{}])", quote(wire), descriptors.join(", "), receiver_params, caller_params).unwrap(); + let generic_signature = if type_params.is_empty() { + String::new() + } else { + format!("type_parameters({}).", type_params.join(", ")) + }; + let type_bindings = if generic { ", types: _types" } else { "" }; + let mut has_callable = false; + for ty in std::iter::once(&function.return_type).chain( + function + .arguments + .iter() + .filter(|arg| !omit_injected_argument(arg, self.supported, &self.names)) + .map(|arg| &arg.ty), + ) { + has_callable |= contains_function(ty, self.pool, &mut BTreeSet::new()); + } + let checked = if has_callable { ".checked(:never)" } else { "" }; + for asynchronous in [false, true] { + let method = format!("{method}{}", if asynchronous { "_async" } else { "" }); + let returns = if asynchronous { + format!("::Baml::Call[{result}]") + } else { + result.clone() + }; + let target = if receiver.is_some() { "" } else { "self." }; + if has_callable { + body.push_str( + "# BAML accepts any callable object, not only Proc or Closure handles.\n", + ); + } + let mut doc = function.docstring.clone().unwrap_or_default(); + if let Some(throws) = &function.throws { + let mut names = BTreeSet::new(); + throws_names(throws, &mut names); + if !names.is_empty() { + if !doc.is_empty() { + doc.push_str("\n\n"); + } + write!( + doc, + "Raises:\n {}", + names.into_iter().collect::>().join(", ") + ) + .unwrap(); + } + } + doc_comment(body, &doc); + writeln!(body, "sig {{ {generic_signature}params({}).returns({returns}){checked} }}\ndef {target}{method}({})\n ::Baml::Bridge.call_async({}, {{{}}}, baml: _baml{type_bindings}){}\nend", params.join(", "), required.join(", "), quote(wire), arguments.join(", "), if asynchronous { "" } else { ".wait" }).unwrap(); + } + } +} + +fn contains_function(ty: &Ty, pool: &SymbolPool, visiting: &mut BTreeSet) -> bool { + let mut found = false; + baml_sdkgen_types::visit_type(ty, &mut |ty| match ty { + Ty::Function { .. } => found = true, + Ty::TypeAlias(name) if visiting.insert(name.clone()) => { + if let Some(Symbol::TypeAlias(alias)) = pool.get(name) { + found |= contains_function(&alias.resolves_to, pool, visiting); + } + visiting.remove(name); + } + _ => {} + }); + found +} + +fn doc_comment(body: &mut String, doc: &str) { + for line in doc.lines() { + if line.is_empty() { + body.push_str("#\n"); + } else { + writeln!(body, "# {line}").unwrap(); + } + } +} + +fn member_doc<'a>( + summary: Option<&str>, + section: &str, + members: impl Iterator)>, +) -> String { + let mut doc = summary.unwrap_or_default().to_owned(); + let members: Vec<_> = members.collect(); + if members.iter().any(|(_, doc)| doc.is_some()) { + if !doc.is_empty() { + doc.push_str("\n\n"); + } + write!(doc, "{section}:").unwrap(); + for (name, comment) in members { + write!(doc, "\n {name}").unwrap(); + if let Some(comment) = comment { + let mut lines = comment.lines(); + if let Some(first) = lines.next() { + write!(doc, ": {first}").unwrap(); + } + for line in lines { + write!(doc, "\n {line}").unwrap(); + } + } + } + } + doc +} + +fn throws_names(ty: &Ty, names: &mut BTreeSet) { + match ty { + Ty::Union(items) => items.iter().for_each(|t| throws_names(t, names)), + Ty::Class(name, _) | Ty::Enum(name) | Ty::TypeAlias(name) => { + names.insert(name.name().to_string()); + } + Ty::Never => {} + _ => { + names.insert(ty.to_string()); + } + } +} + +// Leave unsupported compiler-injected options to the engine's default, without +// relaxing the supported-type requirement for user-declared arguments. +fn omit_injected_argument( + argument: &FunctionArgument, + supported: &BTreeSet, + names: &NameTable, +) -> bool { + argument.injected + && argument.default.is_some() + && ruby_type(&argument.ty, supported, names).is_none() +} diff --git a/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/lib.rs b/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/lib.rs new file mode 100644 index 00000000000..0e2b455034a --- /dev/null +++ b/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/lib.rs @@ -0,0 +1,146 @@ +//! Ruby/Sorbet declarations and registrations for the whole BAML symbol pool. +mod descriptors; +mod emit; +mod names; +mod package; +mod types; + +use std::{ + collections::{BTreeMap, BTreeSet, HashMap}, + path::PathBuf, +}; + +use baml_sdkgen_types::{Name, NamingConvention, SkipWarning, Symbol, SymbolPool}; +use emit::Emitter; +use names::NameTable; +use types::{ruby_alias_type, ruby_type}; + +pub fn to_source_code_with_bytecode( + pool: &SymbolPool, + bytecode: &[u8], + naming: NamingConvention, + metadata: Option<&str>, +) -> HashMap { + to_source_code_with_bytecode_and_skipped(pool, bytecode, naming, metadata).0 +} + +/// Reports omitted declarations, including methods, in the CLI's shared format. +pub fn to_source_code_with_bytecode_and_warnings( + pool: &SymbolPool, + bytecode: &[u8], + naming: NamingConvention, + metadata: Option<&str>, +) -> (HashMap, Vec) { + let (files, skipped) = + to_source_code_with_bytecode_and_skipped(pool, bytecode, naming, metadata); + let warnings = skipped + .into_iter() + .map(|fqn| { + let local = pool.keys().any(|name| { + name.is_local() && (fqn == name.to_string() || fqn.starts_with(&format!("{name}."))) + }); + SkipWarning { + fqn, + local, + reason: "declaration uses a type or callable feature unsupported by Ruby/Sorbet" + .into(), + } + }) + .collect(); + (files, warnings) +} + +/// Also returns sorted BAML names omitted from the generated surface. +pub fn to_source_code_with_bytecode_and_skipped( + pool: &SymbolPool, + bytecode: &[u8], + naming: NamingConvention, + metadata: Option<&str>, +) -> (HashMap, Vec) { + let names = NameTable::build(pool, naming); + let symbols: BTreeMap<_, _> = pool.iter().collect(); + let mut supported: BTreeSet = symbols + .iter() + .filter_map(|(n, s)| match s { + Symbol::Class(class) if class.crossing == baml_sdkgen_types::ClassCrossing::Prompt => { + None + } + Symbol::Class(_) | Symbol::Enum(_) | Symbol::TypeAlias(_) => Some((*n).clone()), + Symbol::Function(_) => None, + }) + .collect(); + // Remove declarations referring to omitted types, including transitive edges. + loop { + let rejected: Vec<_> = supported + .iter() + .filter(|n| match pool.get(*n) { + Some(Symbol::Class(c)) => c + .properties + .iter() + .any(|p| ruby_type(&p.ty, &supported, &names).is_none()), + Some(Symbol::TypeAlias(_)) => ruby_alias_type( + &baml_sdkgen_types::Ty::TypeAlias((*n).clone()), + pool, + &supported, + &names, + ) + .is_none(), + _ => false, + }) + .cloned() + .collect(); + if rejected.is_empty() { + break; + } + for n in rejected { + supported.remove(&n); + } + } + let mut emitter = Emitter { + pool, + supported: &supported, + naming, + names, + registry: String::new(), + shells: String::new(), + aliases: String::new(), + skipped: BTreeSet::new(), + }; + let mut namespaces: BTreeMap = BTreeMap::new(); + for (name, symbol) in symbols { + let module = emitter.names.module_name(name); + let body = namespaces.entry(module.clone()).or_default(); + match symbol { + Symbol::Class(_) | Symbol::Enum(_) if supported.contains(name) => match symbol { + Symbol::Class(class) => emitter.class(name, class, body), + Symbol::Enum(enum_) => emitter.enum_(name, enum_, body), + _ => unreachable!(), + }, + Symbol::TypeAlias(alias) if supported.contains(name) => { + emitter.type_alias(name, alias, pool); + } + Symbol::Function(function) => { + emitter.function(&module, &name.to_string(), function, None, &[], body); + } + _ => { + emitter.skipped.insert(name.to_string()); + if let Symbol::Class(class) = symbol { + emitter.skipped.extend( + class + .static_methods + .iter() + .chain(&class.instance_methods) + .map(|m| format!("{name}.{}", m.name)), + ); + } + } + } + } + let files = package::emit(namespaces, &emitter, bytecode, metadata); + (files, emitter.skipped.into_iter().collect()) +} + +#[cfg(test)] +mod review_tests; +#[cfg(test)] +mod tests; diff --git a/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/names.rs b/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/names.rs new file mode 100644 index 00000000000..a0d22bbca79 --- /dev/null +++ b/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/names.rs @@ -0,0 +1,611 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use baml_sdkgen_types::{Name, NamingConvention, Symbol, SymbolPool, namespace_segments}; + +/// Claims include every callable sibling, even when its type is not mapped yet. +#[derive(Default)] +pub(crate) struct NameTable { + used: BTreeSet<(String, String)>, + allocated: BTreeMap<(String, String), String>, + modules: BTreeMap, String>, +} + +impl NameTable { + pub(crate) fn build(pool: &SymbolPool, naming: NamingConvention) -> Self { + let mut table = Self::default(); + for reserved in ["BYTECODE", "METADATA"] { + table.used.insert(("BamlSdk".into(), reserved.into())); + } + let symbols: BTreeMap<_, _> = pool.iter().collect(); + // Namespace modules and type constants share Ruby's constant scope. + let mut children: BTreeMap, BTreeSet<(String, String)>> = BTreeMap::new(); + for (name, symbol) in &symbols { + let path = namespace_segments(name); + for depth in 0..path.len() { + children.entry(path[..depth].to_vec()).or_default().insert(( + path[depth].clone(), + format!("namespace {}", path[..=depth].join(".")), + )); + } + if !matches!(symbol, Symbol::Function(_)) { + children + .entry(path) + .or_default() + .insert((name.name().to_string(), name.to_string())); + } + } + table.modules.insert(vec![], "BamlSdk".into()); + for (parent, children) in children { + let scope = table.modules[&parent].clone(); + let mut children: Vec<_> = children.into_iter().collect(); + children.sort_by(|(a, a_wire), (b, b_wire)| { + (allocation_key(a), a_wire).cmp(&(allocation_key(b), b_wire)) + }); + for (raw, wire) in children { + let ruby = table.claim(&scope, &constant(&raw), &wire); + if wire.starts_with("namespace ") { + let mut path = parent.clone(); + path.push(raw); + table.modules.insert(path, format!("{scope}::{ruby}")); + } + } + } + let mut symbols: Vec<_> = symbols.into_iter().collect(); + symbols.sort_by_key(|(name, _)| (allocation_key(name.name().as_str()).0, name.to_string())); + for (name, symbol) in symbols { + match symbol { + Symbol::Class(class) => { + let scope = table.qualified(name); + let mut fields: Vec<_> = class.properties.iter().collect(); + fields.sort_by_key(|field| allocation_key(field.name.as_str())); + for field in fields { + table.claim( + &scope, + &identifier(field.name.as_str(), naming), + &format!("field {name}.{}", field.name), + ); + } + let mut methods: Vec<_> = class + .static_methods + .iter() + .map(|m| (false, m)) + .chain(class.instance_methods.iter().map(|m| (true, m))) + .collect(); + methods.sort_by_key(|(_, method)| allocation_key(method.name.as_str())); + for (instance, method) in methods { + let method_scope = if instance { + scope.clone() + } else { + format!("{scope}.self") + }; + table.callable( + &method_scope, + &callable_identifier( + callable_parts(method.name.as_str()).0, + naming, + !instance, + ), + &format!("{name}.{}", method.name), + ); + } + } + Symbol::Enum(enum_) => { + let scope = table.qualified(name); + let mut variants: Vec<_> = enum_.variants.iter().collect(); + variants.sort_by_key(|variant| allocation_key(variant.name.as_str())); + for variant in variants { + table.claim( + &scope, + &variant_constant(variant.name.as_str()), + &format!("{name}.{}", variant.name), + ); + } + } + Symbol::Function(function) => { + table.callable( + &format!("{}.self", table.module_name(name)), + &callable_identifier( + callable_parts(function.name.as_str()).0, + naming, + true, + ), + &name.to_string(), + ); + } + Symbol::TypeAlias(_) => {} + } + } + table + } + + pub(crate) fn claim(&mut self, scope: &str, ruby: &str, baml: &str) -> String { + let key = (scope.to_owned(), baml.to_owned()); + if let Some(allocated) = self.allocated.get(&key) { + return allocated.clone(); + } + let mut ruby = ruby.to_owned(); + while self.used.contains(&(scope.to_owned(), ruby.clone())) { + ruby.push('_'); + } + self.used.insert((scope.to_owned(), ruby.clone())); + self.allocated.insert(key, ruby.clone()); + ruby + } + + pub(crate) fn callable(&mut self, scope: &str, ruby: &str, wire: &str) -> String { + let (base, _) = callable_parts(wire); + let key = (scope.to_owned(), base.to_owned()); + if let Some(allocated) = self.allocated.get(&key) { + return allocated.clone(); + } + let suffixes = [ + "", + "_async", + "_stream", + "_stream_async", + "_spec", + "_spec_async", + ]; + let mut ruby = ruby.to_owned(); + // Fields are allocated first. Escape the whole family until every reader + // and callable spelling is free, including repeated underscore collisions. + while suffixes.iter().any(|suffix| { + self.used + .contains(&(scope.to_owned(), format!("{ruby}{suffix}"))) + }) { + ruby.push('_'); + } + for suffix in suffixes { + self.used + .insert((scope.to_owned(), format!("{ruby}{suffix}"))); + } + self.allocated.insert(key, ruby.clone()); + ruby + } + + pub(crate) fn module_name(&self, name: &Name) -> String { + self.modules + .get(&namespace_segments(name)) + .cloned() + .unwrap_or_else(|| module_name(name)) + } + + pub(crate) fn qualified(&self, name: &Name) -> String { + let module = self.module_name(name); + let ruby = self + .allocated + .get(&(module.clone(), name.to_string())) + .cloned() + .unwrap_or_else(|| constant(name.name().as_str())); + format!("{module}::{ruby}") + } +} + +// Match Python names.rs::allocate: legal authored identifiers first, then BAML +// identity. Ruby projection must not decide which BAML declaration wins. +fn allocation_key(raw: &str) -> (bool, &str) { + let python_keyword = [ + "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", + ] + .contains(&raw); + let mut chars = raw.chars(); + let legal = chars.next().is_some_and(|c| c == '_' || c.is_alphabetic()) + && chars.all(|c| c == '_' || c.is_alphanumeric()) + && !python_keyword; + (!legal, raw) +} + +pub(crate) fn callable_parts(name: &str) -> (&str, &str) { + for (wire, ruby) in [("@stream", "_stream"), ("@spec", "_spec")] { + if let Some(base) = name.strip_suffix(wire) { + return (base, ruby); + } + } + (name, "") +} + +pub(crate) fn module_name(name: &Name) -> String { + std::iter::once("BamlSdk".to_owned()) + .chain(namespace_segments(name).iter().map(|n| constant(n))) + .collect::>() + .join("::") +} + +pub(crate) fn constant(name: &str) -> String { + let mut capitalize = true; + let mut result = String::new(); + for c in name.chars() { + if c == '_' { + capitalize = true; + } else if capitalize { + result.extend(c.to_uppercase()); + capitalize = false; + } else { + result.push(c); + } + } + if name.starts_with('_') { + format!("Baml{result}") + } else { + result + } +} + +pub(crate) fn variant_constant(name: &str) -> String { + if name.starts_with('_') { + format!("Baml{}", variant_constant(name.trim_start_matches('_'))) + } else { + let mut chars = name.chars(); + chars.next().map_or_else(String::new, |first| { + first.to_uppercase().chain(chars).collect() + }) + } +} + +pub(crate) fn snake_case(name: &str) -> String { + project(name, NamingConvention::Language) +} + +fn project(name: &str, naming: NamingConvention) -> String { + let mut result = String::new(); + let chars: Vec<_> = name.chars().collect(); + for (i, c) in chars.iter().enumerate() { + if naming == NamingConvention::Language && c.is_uppercase() { + if i > 0 + && chars[i - 1] != '_' + && (chars[i - 1].is_lowercase() + || chars.get(i + 1).is_some_and(|c| c.is_lowercase())) + { + result.push('_'); + } + result.extend(c.to_lowercase()); + } else { + result.push(*c); + } + } + result +} + +const KEYWORDS: &[&str] = &[ + "BEGIN", + "END", + "alias", + "and", + "begin", + "break", + "case", + "class", + "def", + "defined", + "do", + "else", + "elsif", + "end", + "ensure", + "false", + "for", + "if", + "in", + "module", + "next", + "nil", + "not", + "or", + "redo", + "rescue", + "retry", + "return", + "self", + "super", + "then", + "true", + "undef", + "unless", + "until", + "when", + "while", + "yield", + "__FILE__", + "__LINE__", + "__ENCODING__", +]; + +// Generated once with Ruby 3.2.2, `require "bundler/setup"` and +// `require "sorbet-runtime"` (Bundler makes Kernel#gem public): +// Object.instance_methods.map(&:to_s).sort +// (Module.instance_methods | Class.instance_methods).map(&:to_s).sort +// T::Struct.singleton_methods.map(&:to_s).sort +// Class adds singleton callables such as allocate and superclass. +const OBJECT_METHODS: &[&str] = &[ + "!", + "!=", + "!~", + "<=>", + "==", + "===", + "__id__", + "__send__", + "class", + "clone", + "define_singleton_method", + "display", + "dup", + "enum_for", + "eql?", + "equal?", + "extend", + "freeze", + "frozen?", + "gem", + "hash", + "inspect", + "instance_eval", + "instance_exec", + "instance_of?", + "instance_variable_defined?", + "instance_variable_get", + "instance_variable_set", + "instance_variables", + "is_a?", + "itself", + "kind_of?", + "method", + "methods", + "nil?", + "object_id", + "pretty_inspect", + "pretty_print", + "pretty_print_cycle", + "pretty_print_inspect", + "pretty_print_instance_variables", + "private_methods", + "protected_methods", + "public_method", + "public_methods", + "public_send", + "remove_instance_variable", + "respond_to?", + "send", + "singleton_class", + "singleton_method", + "singleton_methods", + "tap", + "then", + "to_enum", + "to_s", + "yield_self", +]; +const MODULE_METHODS: &[&str] = &[ + "!", + "!=", + "!~", + "<", + "<=", + "<=>", + "==", + "===", + ">", + ">=", + "__id__", + "__send__", + "alias_method", + "allocate", + "ancestors", + "attached_object", + "attr", + "attr_accessor", + "attr_reader", + "attr_writer", + "autoload", + "autoload?", + "class", + "class_eval", + "class_exec", + "class_variable_defined?", + "class_variable_get", + "class_variable_set", + "class_variables", + "clone", + "const_defined?", + "const_get", + "const_missing", + "const_set", + "const_source_location", + "constants", + "define_method", + "define_singleton_method", + "deprecate_constant", + "display", + "dup", + "enum_for", + "eql?", + "equal?", + "extend", + "freeze", + "frozen?", + "gem", + "hash", + "include", + "include?", + "included_modules", + "inspect", + "instance_eval", + "instance_exec", + "instance_method", + "instance_methods", + "instance_of?", + "instance_variable_defined?", + "instance_variable_get", + "instance_variable_set", + "instance_variables", + "is_a?", + "itself", + "kind_of?", + "method", + "method_defined?", + "methods", + "module_eval", + "module_exec", + "name", + "new", + "nil?", + "object_id", + "prepend", + "pretty_inspect", + "pretty_print", + "pretty_print_cycle", + "pretty_print_inspect", + "pretty_print_instance_variables", + "private_class_method", + "private_constant", + "private_instance_methods", + "private_method_defined?", + "private_methods", + "protected_instance_methods", + "protected_method_defined?", + "protected_methods", + "public_class_method", + "public_constant", + "public_instance_method", + "public_instance_methods", + "public_method", + "public_method_defined?", + "public_methods", + "public_send", + "refinements", + "remove_class_variable", + "remove_instance_variable", + "remove_method", + "respond_to?", + "send", + "singleton_class", + "singleton_class?", + "singleton_method", + "singleton_methods", + "subclasses", + "superclass", + "tap", + "then", + "to_enum", + "to_s", + "undef_method", + "undefined_instance_methods", + "yield_self", +]; + +const STRUCT_METHODS: &[&str] = &[ + "const", + "decorator", + "decorator_class", + "extended", + "from_hash", + "from_hash!", + "included", + "inherited", + "plugin", + "plugins", + "prepended", + "prop", + "prop_by_serialized_forms", + "props", + "reload_decorator!", + "validate_prop_value", +]; + +pub(crate) fn identifier(name: &str, naming: NamingConvention) -> String { + callable_identifier(name, naming, false) +} + +fn callable_identifier(name: &str, naming: NamingConvention, singleton: bool) -> String { + let mut result = project(name, naming); + // Keywords retain their suffix (notably then_), even if Object defines them. + if KEYWORDS.contains(&result.as_str()) + || ["_baml", "_types", "__baml_type_args"].contains(&result.as_str()) + { + result.push('_'); + return result; + } + // Keep lifecycle hooks and the original T::Struct/runtime protections. + if OBJECT_METHODS.contains(&result.as_str()) + || (singleton + && (MODULE_METHODS.contains(&result.as_str()) + || STRUCT_METHODS.contains(&result.as_str()))) + || [ + "method", + "methods", + "new", + "send", + "public_send", + "object_id", + "hash", + "freeze", + "dup", + "clone", + "inspect", + "to_s", + "to_h", + "initialize", + "instance_variable_get", + "instance_variable_set", + "define_method", + "const_get", + "extend", + "include", + "sig", + "props", + "serialize", + "deserialize", + "method_added", + "method_removed", + "method_undefined", + "singleton_method_added", + "singleton_method_removed", + "singleton_method_undefined", + "inherited", + "included", + "extended", + "prepended", + "const_added", + "const_missing", + "method_missing", + "respond_to_missing?", + "initialize_copy", + "initialize_clone", + "initialize_dup", + ] + .contains(&result.as_str()) + { + return format!("baml_{result}"); + } + result +} + +pub(crate) fn parameter( + name: &str, + naming: NamingConvention, + used: &mut BTreeSet, +) -> String { + let mut result = project(name, naming); + while KEYWORDS.contains(&result.as_str()) || used.contains(&result) { + result.push('_'); + } + used.insert(result.clone()); + result +} + +pub(crate) fn quote(value: &str) -> String { + let mut quoted = String::from("\""); + for ch in value.chars() { + match ch { + '\0' => quoted.push_str("\\u0000"), + '#' => quoted.push_str("\\#"), + '\'' => quoted.push(ch), + _ => quoted.extend(ch.escape_debug()), + } + } + quoted.push('"'); + quoted +} diff --git a/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/package.rs b/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/package.rs new file mode 100644 index 00000000000..72542549328 --- /dev/null +++ b/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/package.rs @@ -0,0 +1,108 @@ +use std::{ + collections::{BTreeMap, BTreeSet, HashMap}, + fmt::Write as _, + path::PathBuf, +}; + +use crate::{ + emit::Emitter, + names::{quote, snake_case}, +}; + +pub(crate) fn emit( + namespaces: BTreeMap, + emitter: &Emitter<'_>, + bytecode: &[u8], + metadata: Option<&str>, +) -> HashMap { + let mut root = String::from( + "# typed: strict\n# frozen_string_literal: true\nrequire \"baml/bridge\"\nrequire \"sorbet-runtime\"\nrequire_relative \"baml_sdk/__bytecode\"\nmodule BamlSdk\n extend ::T::Sig\n sig { returns(::NilClass) }\n def self.initialize!\n ::Baml::Bridge.initialize!\n nil\n end\nend\n", + ); + // Runtime shells preserve forward/recursive references. Create them dynamically + // so Sorbet sees each T::Struct/T::Enum declaration once, with all its fields; + // a normal empty T::Struct declaration synthesizes a zero-argument constructor. + let mut modules = BTreeSet::new(); + for module in namespaces.keys() { + let parts: Vec<_> = module.split("::").collect(); + for end in 2..=parts.len() { + modules.insert(parts[..end].join("::")); + } + } + for module in modules { + writeln!(root, "module {module}; end").unwrap(); + } + root.push_str(&emitter.shells); + root.push_str(&emitter.aliases); + let mut files = HashMap::new(); + let mut paths = BTreeSet::from(["__root".to_owned(), "__bytecode".to_owned()]); + for (module, body) in namespaces { + // Ruby constants never start with underscores, so namespaces cannot + // produce the reserved declarations, bytecode or vendor filenames. + // Vendored packages avoid `vendor/`: Tapioca's default Sorbet config + // ignores every directory with that name. + let mut relative = module.strip_prefix("BamlSdk::").map_or_else( + || "__root".to_owned(), + |namespace| { + namespace + .split("::") + .map(snake_case) + .map(|part| { + if part == "vendor" { + "__vendor".to_owned() + } else { + part + } + }) + .collect::>() + .join("/") + }, + ); + if module != "BamlSdk" { + while !paths.insert(relative.clone()) { + relative.push('_'); + } + } + let path = format!("baml_sdk/{relative}.rb"); + let body = body.lines().fold(String::new(), |mut out, line| { + writeln!(out, " {line}").unwrap(); + out + }); + insert( + &mut files, + &path, + format!( + "# typed: strict\n# frozen_string_literal: true\nmodule {module}\n extend ::T::Sig\n{body}end\n" + ), + ); + writeln!( + root, + "require_relative {}", + quote(path.strip_suffix(".rb").unwrap()) + ) + .unwrap(); + } + root.push_str("::Baml::Bridge.register_program(::BamlSdk::BYTECODE, ::BamlSdk::METADATA)\n"); + root.push_str(&emitter.registry); + insert(&mut files, "baml_sdk.rb", root); + let bytes = baml_sdkgen_types::embedded_bytecode_base64(bytecode); + let metadata = metadata.map_or_else(|| "nil".into(), |s| format!("{}.freeze", quote(s))); + insert( + &mut files, + "baml_sdk/__bytecode.rb", + format!( + "# typed: strict\n# frozen_string_literal: true\nmodule BamlSdk\n BYTECODE = ::T.let(\"{bytes}\".b.freeze, ::String)\n METADATA = ::T.let({metadata}, ::T.nilable(::String))\nend\n" + ), + ); + files +} + +fn insert(files: &mut HashMap, path: &str, source: String) { + match files.entry(PathBuf::from(path)) { + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(source); + } + std::collections::hash_map::Entry::Occupied(_) => { + panic!("duplicate Ruby output path: {path}"); + } + } +} diff --git a/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/review_tests.rs b/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/review_tests.rs new file mode 100644 index 00000000000..add32e1d317 --- /dev/null +++ b/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/review_tests.rs @@ -0,0 +1,178 @@ +use super::*; +use baml_sdkgen_types::{Enum, EnumVariant, Function, Origin, Ty}; + +#[test] +fn prompt_return_is_skipped_with_a_warning() { + use baml_sdkgen_types::{Class, ClassCrossing}; + let prompt = Name::new("ai".into(), vec![], "Prompt".into()); + let origin = Origin { + source_file_path: "test.baml".into(), + span_start: 0, + }; + let function = Name::new("user".into(), vec![], "get_prompt".into()); + let pool = SymbolPool::from([ + ( + prompt.clone(), + Symbol::Class(Class { + name: prompt.clone(), + crossing: ClassCrossing::Prompt, + generic_params: vec![], + docstring: None, + properties: vec![], + static_methods: vec![], + instance_methods: vec![], + origin: origin.clone(), + }), + ), + ( + function, + Symbol::Function(Function { + name: "get_prompt".into(), + generic_params: vec![], + docstring: None, + arguments: vec![], + return_type: Ty::Class(prompt, Box::new([])), + throws: None, + watchers: vec![], + origin, + }), + ), + ]); + let (files, warnings) = + to_source_code_with_bytecode_and_warnings(&pool, &[], NamingConvention::Language, None); + assert!( + warnings + .iter() + .any(|warning| warning.fqn == "user.get_prompt" && warning.local) + ); + assert!( + files + .values() + .all(|source| !source.contains("def self.get_prompt")) + ); +} + +#[test] +fn skips_report_the_users_package_not_a_hardcoded_user_prefix() { + let mut pool = SymbolPool::new(); + // Even a project named my_app has local names spelled user in the pool. + for package in ["user", "dependency"] { + let name = Name::new(package.into(), vec![], "unsupported".into()); + pool.insert( + name, + Symbol::Function(Function { + name: "unsupported".into(), + generic_params: vec![], + docstring: None, + arguments: vec![], + return_type: Ty::Literal( + baml_base::Literal::Float("1.5".into()), + baml_type::Freshness::Regular, + ), + throws: None, + watchers: vec![], + origin: Origin { + source_file_path: "test.baml".into(), + span_start: 0, + }, + }), + ); + } + let (files, warnings) = to_source_code_with_bytecode_and_warnings( + &pool, + &[], + NamingConvention::Language, + Some("[package]\nname = \"my_app\"\n"), + ); + assert_eq!(warnings.len(), 2); + for warning in warnings { + assert_eq!(warning.local, warning.fqn.starts_with("user.")); + assert!(!warning.reason.is_empty()); + } + assert!( + files + .values() + .all(|source| !source.contains("def self.unsupported")) + ); +} + +#[test] +fn enum_variants_keep_underscores_and_escape_collisions() { + let name = Name::new("user".into(), vec![], "Choice".into()); + let pool = SymbolPool::from([( + name.clone(), + Symbol::Enum(Enum { + name, + docstring: None, + variants: [ + "UPDATE_QUANTITY", + "update_quantity", + "_private", + "BamlPrivate", + "_update_quantity", + "BamlUpdate_quantity", + ] + .into_iter() + .map(|raw| EnumVariant { + name: raw.into(), + value: raw.into(), + docstring: None, + }) + .collect(), + origin: Origin { + source_file_path: "test.baml".into(), + span_start: 0, + }, + }), + )]); + let files = to_source_code_with_bytecode(&pool, &[], NamingConvention::Language, None); + let source = &files[&PathBuf::from("baml_sdk/__root.rb")]; + for expected in [ + "UPDATE_QUANTITY = new(\"UPDATE_QUANTITY\")", + "Update_quantity = new(\"update_quantity\")", + "BamlPrivate = new(\"BamlPrivate\")", + "BamlPrivate_ = new(\"_private\")", + "BamlUpdate_quantity = new(\"BamlUpdate_quantity\")", + "BamlUpdate_quantity_ = new(\"_update_quantity\")", + ] { + assert!(source.contains(expected), "{source}"); + } +} + +#[test] +fn package_constants_are_reserved_and_vendor_is_escaped_at_every_depth() { + let mut pool = SymbolPool::new(); + for (namespace, leaf) in [ + (vec![], "BYTECODE"), + (vec![], "METADATA"), + (vec!["nested", "vendor"], "Choice"), + ] { + let name = Name::new( + "user".into(), + namespace.into_iter().map(Into::into).collect(), + leaf.into(), + ); + pool.insert( + name.clone(), + Symbol::Enum(Enum { + name, + docstring: None, + variants: vec![], + origin: Origin { + source_file_path: "test.baml".into(), + span_start: 0, + }, + }), + ); + } + let files = to_source_code_with_bytecode(&pool, &[], NamingConvention::Language, None); + let root_types = &files[&PathBuf::from("baml_sdk/__root.rb")]; + assert!(root_types.contains("class BYTECODE_ <"), "{root_types}"); + assert!(root_types.contains("class METADATA_ <"), "{root_types}"); + assert!(files.contains_key(&PathBuf::from("baml_sdk/nested/__vendor.rb"))); + assert!( + files + .keys() + .all(|path| !path.components().any(|part| part.as_os_str() == "vendor")) + ); +} diff --git a/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/tests.rs b/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/tests.rs new file mode 100644 index 00000000000..649553f51f5 --- /dev/null +++ b/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/tests.rs @@ -0,0 +1,647 @@ +use super::*; +use crate::names::{constant, identifier}; +use baml_sdkgen_types::Ty; + +#[test] +fn abi5_descriptors_keep_slots_and_callable_signatures() { + use baml_sdkgen_types::{CallableParam, CodegenFunctionParamMode}; + + let name = Name::new("user".into(), vec![], "Choice".into()); + assert_eq!(descriptors::descriptor(&Ty::Unknown), "[:unknown].freeze"); + assert_eq!( + descriptors::descriptor(&Ty::Literal( + baml_base::Literal::Bigint(42.into()), + baml_type::Freshness::Regular + )), + "[:literal, :bigint, ::Kernel.Integer(\"42\")].freeze" + ); + assert_eq!( + descriptors::descriptor(&Ty::Interface(name.clone(), Box::new([]), Box::new([]))), + "[:interface, \"user.Choice\"].freeze" + ); + assert_eq!( + descriptors::descriptor(&Ty::EnumVariant(name.clone(), "Red".into())), + "[:enum_variant, \"user.Choice\", \"Red\"].freeze" + ); + assert_eq!( + descriptors::descriptor(&Ty::Class(name.clone(), Box::new([Ty::Int]))), + "[:class, \"user.Choice\", [[:int].freeze].freeze].freeze" + ); + assert_eq!( + descriptors::descriptor(&Ty::Union(Box::new([Ty::Int, Ty::String, Ty::Null]))), + "[:union, [[:int].freeze, [:string].freeze, [:null].freeze].freeze].freeze" + ); + let callable = Ty::Function { + params: Box::new([ + CallableParam::required(Some("value".into()), Ty::Int), + CallableParam { + name: None, + ty: Ty::String, + mode: CodegenFunctionParamMode::Optional, + }, + ]), + ret: Box::new(Ty::Float), + throws: Box::new(Ty::Enum(name)), + }; + assert_eq!( + descriptors::descriptor(&callable), + "[:function, [[\"value\", [:int].freeze, false].freeze, [nil, [:string].freeze, true].freeze].freeze, [:float].freeze, [:enum, \"user.Choice\"].freeze].freeze" + ); + assert_eq!(descriptors::descriptor(&Ty::Type), "[:other].freeze"); +} + +#[test] +fn ruby_names_and_container_types() { + assert_eq!(crate::names::quote("\0\u{31}"), "\"\\u00001\""); + assert_eq!(crate::names::quote(r"\01"), r#""\\01""#); + assert_eq!(crate::names::quote("\"#{x}\n'"), "\"\\\"\\#{x}\\n'\""); + assert_eq!( + crate::names::variant_constant("UPDATE_QUANTITY"), + "UPDATE_QUANTITY" + ); + assert_eq!( + crate::names::variant_constant("update_quantity"), + "Update_quantity" + ); + assert_eq!(crate::names::variant_constant("_private"), "BamlPrivate"); + assert_eq!( + ruby_type( + &Ty::Literal( + baml_base::Literal::Float("1.5".into()), + baml_type::Freshness::Regular + ), + &BTreeSet::new(), + &NameTable::default(), + ), + None + ); + assert_eq!(identifier("class", NamingConvention::Language), "class_"); + assert_eq!( + identifier("HTTPCall", NamingConvention::Language), + "http_call" + ); + assert_eq!( + identifier("SomeCall", NamingConvention::PreserveCase), + "SomeCall" + ); + assert_eq!(constant("throws_test"), "ThrowsTest"); + assert_eq!( + identifier("method", NamingConvention::Language), + "baml_method" + ); + assert_eq!(identifier("sig", NamingConvention::Language), "baml_sig"); + let mut names = NameTable::default(); + assert_eq!( + names.claim("BamlSdk", "hello_world", "user.HelloWorld"), + "hello_world" + ); + assert_eq!( + names.claim("BamlSdk", "hello_world", "user.HelloWorld"), + "hello_world" + ); + assert_eq!( + names.claim("BamlSdk", "hello_world", "user.hello_world"), + "hello_world_" + ); + assert_eq!( + names.claim("BamlSdk", "hello_world_", "user.hello_world_"), + "hello_world__" + ); + assert_eq!( + names.claim("BamlSdk::Other", "hello_world", "user.other.hello_world"), + "hello_world" + ); + assert_eq!(constant("StreamE2ECollectResult"), "StreamE2ECollectResult"); + assert_eq!(constant("_private"), "BamlPrivate"); + assert_eq!( + ruby_type(&Ty::Never, &BTreeSet::new(), &names).as_deref(), + Some("::T.noreturn") + ); + let string = Ty::String; + let optional = Ty::Union(Box::new([string.clone(), Ty::Null])); + let map = Ty::Map { + key: Box::new(string), + value: Box::new(Ty::List(Box::new(optional))), + }; + assert_eq!( + ruby_type(&map, &BTreeSet::new(), &names).as_deref(), + Some("::T::Hash[::String, ::T::Array[::T.nilable(::String)]]") + ); + let union = Ty::Union(Box::new([Ty::Int, Ty::Bool])); + assert_eq!( + ruby_type(&union, &BTreeSet::new(), &names).as_deref(), + Some("::T.any(::Integer, ::T::Boolean)") + ); + assert_eq!(ruby_type(&Ty::Unknown, &BTreeSet::new(), &names), None); + let parameter = Ty::TypeVar(baml_sdkgen_types::ParamTy::new(1, "foo_bar".into())); + let generic_union = Ty::Union(Box::new([parameter.clone(), Ty::String, Ty::Null])); + assert_eq!( + crate::types::ruby_type_with_params( + &generic_union, + &BTreeSet::new(), + &names, + &[ + "BamlTypeParam0".into(), + "::T.type_parameter(:BamlTypeParam1)".into() + ], + ), + Some("::T.any(::T.type_parameter(:BamlTypeParam1), ::String, ::NilClass)".into()) + ); + assert_eq!( + crate::descriptors::descriptor(¶meter), + "[:param, 1, \"foo_bar\"].freeze" + ); +} + +#[test] +fn stream_and_spec_types_use_runtime_handles() { + let names = NameTable::default(); + let supported = BTreeSet::new(); + let stream = Name::new("ai".into(), vec!["stream".into()], "Stream".into()); + let spec = Name::new("ai".into(), vec![], "FunctionSpec".into()); + assert_eq!( + ruby_type( + &Ty::Class(stream.clone(), Box::new([Ty::String])), + &supported, + &names + ), + Some("::Baml::Stream[::String]".into()) + ); + assert_eq!( + ruby_type(&Ty::Class(spec, Box::new([Ty::String])), &supported, &names), + Some("::Baml::FunctionSpec".into()) + ); + assert_eq!( + ruby_type( + &Ty::Class(stream, Box::new([Ty::Unknown])), + &supported, + &names + ), + None + ); + // Matching uses the whole BAML name, never just a class's leaf name. + let user = Name::new("user".into(), vec![], "Stream".into()); + assert_eq!( + ruby_type(&Ty::Class(user, Box::new([Ty::String])), &supported, &names), + None + ); +} + +#[test] +fn callback_types_use_proc_signatures() { + let names = NameTable::default(); + let callback = Ty::Function { + params: Box::new([baml_sdkgen_types::CallableParam::required(None, Ty::Int)]), + ret: Box::new(Ty::String), + throws: Box::new(Ty::Never), + }; + assert_eq!( + ruby_type(&callback, &BTreeSet::new(), &names).as_deref(), + Some("::T.any(::T.proc.params(arg0: ::Integer).returns(::String), ::Baml::Closure)") + ); + assert_eq!( + crate::descriptors::descriptor(&callback), + "[:function, [[nil, [:int].freeze, false].freeze].freeze, [:string].freeze, [:never].freeze].freeze" + ); + + // Aliases must retain the same callable-object runtime-check exemption. + let alias_name = Name::new("user".into(), vec![], "Callback".into()); + let function_name = Name::new("user".into(), vec![], "apply".into()); + let origin = baml_sdkgen_types::Origin { + source_file_path: "callbacks.baml".into(), + span_start: 0, + }; + let pool = SymbolPool::from([ + ( + alias_name.clone(), + Symbol::TypeAlias(baml_sdkgen_types::TypeAlias { + name: alias_name.clone(), + resolves_to: callback, + recursive: false, + origin: origin.clone(), + }), + ), + ( + function_name, + Symbol::Function(baml_sdkgen_types::Function { + name: "apply".into(), + generic_params: vec![], + docstring: None, + arguments: vec![baml_sdkgen_types::FunctionArgument { + name: "callback".into(), + docstring: None, + ty: Ty::TypeAlias(alias_name.clone()), + default: None, + injected: false, + }], + return_type: Ty::TypeAlias(alias_name), + throws: None, + watchers: vec![], + origin, + }), + ), + ]); + let files = to_source_code_with_bytecode(&pool, &[], NamingConvention::Language, None); + let source = &files[&PathBuf::from("baml_sdk/__root.rb")]; + assert_eq!(source.matches(".checked(:never)").count(), 2, "{source}"); + assert!(!files[&PathBuf::from("baml_sdk.rb")].contains("signature_for_method")); +} + +#[test] +fn colliding_constants_and_namespaces_keep_wire_identities() { + use baml_sdkgen_types::{Enum, EnumVariant, Origin}; + + let declarations: Vec<_> = [ + (vec![], "fooBar"), + (vec!["fooBar"], "fooBar"), + (vec!["fooBar"], "foo_bar"), + (vec!["foo_bar"], "Choice"), + ] + .into_iter() + .map(|(namespace, leaf)| { + let name = Name::new( + "user".into(), + namespace.into_iter().map(Into::into).collect(), + leaf.into(), + ); + let symbol = Symbol::Enum(Enum { + name: name.clone(), + docstring: None, + // Deliberately not in allocation order. + variants: ["foo_bar", "fooBar", "foo__bar"] + .into_iter() + .map(|raw| EnumVariant { + name: raw.into(), + docstring: None, + value: raw.into(), + }) + .collect(), + origin: Origin { + source_file_path: "names.baml".into(), + span_start: 0, + }, + }); + (name, symbol) + }) + .collect(); + let pool: SymbolPool = declarations.iter().cloned().collect(); + let names = NameTable::build(&pool, NamingConvention::Language); + let expected = [ + "BamlSdk::FooBar_", + "BamlSdk::FooBar::FooBar", + "BamlSdk::FooBar::FooBar_", + "BamlSdk::FooBar__::Choice", + ]; + for ((name, _), expected) in declarations.iter().zip(expected) { + assert_eq!(names.qualified(name), expected); + assert_eq!( + ruby_type( + &Ty::Enum(name.clone()), + &pool.keys().cloned().collect(), + &names + ), + Some(format!("::{expected}")) + ); + } + let generate = |pool: &SymbolPool| { + to_source_code_with_bytecode_and_skipped(pool, &[], NamingConvention::Language, None) + }; + let (files, skipped) = generate(&pool); + assert!(skipped.is_empty()); + assert_eq!( + (files.clone(), skipped), + generate(&declarations.into_iter().rev().collect()) + ); + let registry = &files[&PathBuf::from("baml_sdk.rb")]; + assert!(registry.contains("define_enum(\"user.fooBar.foo_bar\", ::BamlSdk::FooBar::FooBar_)")); + let source = &files[&PathBuf::from("baml_sdk/foo_bar.rb")]; + for expected in [ + "FooBar = new(\"fooBar\")", + "Foo__bar = new(\"foo__bar\")", + "Foo_bar = new(\"foo_bar\")", + ] { + assert!(source.contains(expected), "{source}"); + } +} + +fn namespace_pool(namespaces: &[&str]) -> SymbolPool { + namespaces + .iter() + .map(|namespace| { + let path = if namespace.is_empty() { + vec![] + } else { + vec![(*namespace).into()] + }; + let name = Name::new("user".into(), path, "Choice".into()); + let symbol = Symbol::Enum(baml_sdkgen_types::Enum { + name: name.clone(), + docstring: None, + variants: vec![], + origin: baml_sdkgen_types::Origin { + source_file_path: "paths.baml".into(), + span_start: 0, + }, + }); + (name, symbol) + }) + .collect() +} + +#[test] +fn namespace_paths_are_snake_case_and_cannot_replace_package_files() { + let pool = namespace_pool(&["", "root", "bytecode", "throws_test", "throwstest"]); + let files = to_source_code_with_bytecode(&pool, &[42], NamingConvention::Language, None); + for namespace in ["root", "bytecode", "throws_test", "throwstest"] { + let source = &files[&PathBuf::from(format!("baml_sdk/{namespace}.rb"))]; + assert!(source.contains("class Choice"), "{source}"); + } + assert!(files[&PathBuf::from("baml_sdk/__root.rb")].contains("class Choice")); + assert!(files[&PathBuf::from("baml_sdk/__bytecode.rb")].contains("BYTECODE =")); + assert_eq!(files.len(), pool.len() + 2); +} + +#[test] +fn vendored_packages_avoid_directories_tapioca_ignores() { + let name = Name::new("anthropic".into(), vec![], "Choice".into()); + let symbol = Symbol::Enum(baml_sdkgen_types::Enum { + name: name.clone(), + docstring: None, + variants: vec![], + origin: baml_sdkgen_types::Origin { + source_file_path: "anthropic.baml".into(), + span_start: 0, + }, + }); + let pool: SymbolPool = [(name, symbol)].into_iter().collect(); + let files = to_source_code_with_bytecode(&pool, &[], NamingConvention::Language, None); + let source = &files[&PathBuf::from("baml_sdk/__vendor/anthropic.rb")]; + assert!( + source.contains("module BamlSdk::Vendor::Anthropic"), + "{source}" + ); + assert!( + files[&PathBuf::from("baml_sdk.rb")] + .contains("require_relative \"baml_sdk/__vendor/anthropic\"") + ); +} + +#[test] +fn duplicate_namespace_paths_get_distinct_files_and_requires() { + let pool = namespace_pool(&["FooBar", "FOOBar", "foo_bar_"]); + let files = to_source_code_with_bytecode(&pool, &[], NamingConvention::Language, None); + assert_eq!(files.len(), pool.len() + 2); + let root = &files[&PathBuf::from("baml_sdk.rb")]; + for path in files.keys().filter(|path| { + path.to_string_lossy().starts_with("baml_sdk/") + && path.to_string_lossy() != "baml_sdk/__bytecode.rb" + }) { + assert!(root.contains(&format!("require_relative {:?}", path.with_extension("")))); + assert!(files[path].contains("class Choice")); + } +} + +#[test] +fn unsupported_injected_defaults_are_omitted_without_dropping_user_arguments() { + use baml_sdkgen_types::{Function, FunctionArgument, FunctionArgumentDefault, Origin}; + + let function = Function { + name: "extract".into(), + generic_params: vec![], + docstring: None, + arguments: vec![ + FunctionArgument { + name: "text".into(), + docstring: None, + ty: Ty::String, + default: None, + injected: false, + }, + FunctionArgument { + // Deliberately not named on_event: only the marker matters. + name: "callback".into(), + docstring: None, + ty: Ty::Unknown, + default: Some(FunctionArgumentDefault::Null), + injected: true, + }, + ], + return_type: Ty::String, + throws: None, + watchers: vec![], + origin: Origin { + source_file_path: "test.baml".into(), + span_start: 0, + }, + }; + let name = Name::new("user".into(), vec![], "extract".into()); + let generate = |function| { + to_source_code_with_bytecode_and_skipped( + &SymbolPool::from([(name.clone(), Symbol::Function(function))]), + &[], + NamingConvention::Language, + None, + ) + }; + let (files, skipped) = generate(function.clone()); + let source = &files[&PathBuf::from("baml_sdk/__root.rb")]; + assert!(skipped.is_empty()); + assert!( + source.contains( + "sig { params(text: ::String, _baml: ::T.nilable(::Baml::Options)).returns(::String) }" + ), + "{source}" + ); + assert!( + source.contains("def self.extract(text, _baml: nil)"), + "{source}" + ); + assert!( + source.contains( + "::Baml::Bridge.call_async(\"user.extract\", {\"text\" => text}, baml: _baml).wait" + ), + "{source}" + ); + assert!(!source.contains("callback"), "{source}"); + + let mut user_callback = function.clone(); + user_callback.arguments[1].injected = false; + user_callback.arguments[1].name = "on_event".into(); + let (files, skipped) = generate(user_callback); + assert_eq!(skipped, ["user.extract"]); + assert!(!files[&PathBuf::from("baml_sdk/__root.rb")].contains("def self.extract")); + + let mut required_callback = function.clone(); + required_callback.arguments[1].default = None; + assert_eq!(generate(required_callback).1, ["user.extract"]); + + // Defaulted values are keywords; omission leaves evaluation to BAML. + for injected in [false, true] { + let mut supported_default = function.clone(); + supported_default.arguments[1].injected = injected; + supported_default.arguments[1].ty = Ty::String; + let (files, skipped) = generate(supported_default); + let source = &files[&PathBuf::from("baml_sdk/__root.rb")]; + assert!(skipped.is_empty()); + assert!(source.contains("callback: ::Baml::UNSET")); + assert!(source.contains("callback: ::T.any(::String, ::Baml::Unset)")); + assert!(source.contains("\"callback\" => callback")); + } + + // Nullable alone does not make a parameter optional. + let mut nullable = function; + nullable.arguments.truncate(1); + nullable.arguments[0].ty = Ty::Union(vec![Ty::String, Ty::Null].into_boxed_slice()); + let (files, skipped) = generate(nullable); + assert!(skipped.is_empty()); + let source = &files[&PathBuf::from("baml_sdk/__root.rb")]; + assert!(source.contains("def self.extract(text, _baml: nil)")); + assert!(source.contains("text: ::T.nilable(::String)")); + assert!( + files[&PathBuf::from("baml_sdk.rb")] + .contains("[\"text\", [:union, [[:string].freeze, [:null].freeze].freeze].freeze]") + ); + for asynchronous in [false, true] { + assert!(source.contains(&format!( + "def self.extract{}(text, _baml: nil)", + if asynchronous { "_async" } else { "" } + ))); + } +} + +#[test] +fn classes_register_how_they_cross_the_bridge() { + use baml_sdkgen_types::{Class, ClassCrossing, Origin}; + + for (crossing, expected) in [ + (ClassCrossing::Fields, "fields"), + (ClassCrossing::Opaque, "opaque"), + (ClassCrossing::Stream, "stream"), + (ClassCrossing::FunctionSpec, "function_spec"), + (ClassCrossing::Prompt, "prompt"), + ( + ClassCrossing::Media(baml_base::MediaKind::Image), + "media_image", + ), + ( + ClassCrossing::Media(baml_base::MediaKind::Generic), + "media_generic", + ), + ] { + let name = Name::new("user".into(), vec![], "Boundary".into()); + let pool = SymbolPool::from([( + name.clone(), + Symbol::Class(Class { + name, + crossing, + generic_params: vec![], + docstring: None, + properties: vec![], + static_methods: vec![], + instance_methods: vec![], + origin: Origin { + source_file_path: "types.baml".into(), + span_start: 0, + }, + }), + )]); + let (files, warnings) = + to_source_code_with_bytecode_and_warnings(&pool, &[], NamingConvention::Language, None); + if expected == "prompt" { + assert_eq!(warnings.len(), 1); + assert_eq!(warnings[0].fqn, "user.Boundary"); + assert!(!files[&PathBuf::from("baml_sdk.rb")].contains("define_class")); + } else { + assert!(warnings.is_empty()); + assert!(files[&PathBuf::from("baml_sdk.rb")].contains(&format!( + "::Baml::Bridge.define_class(\"user.Boundary\", ::BamlSdk::Boundary, fields: {{}}, type_params: [], crossing: :{expected})" + ))); + } + } +} + +#[test] +fn aliases_preserve_descriptors_and_widen_only_recursive_back_edges() { + use baml_sdkgen_types::{Origin, TypeAlias}; + + let recursive = Name::new("user".into(), vec![], "Rec".into()); + let floats = Name::new("user".into(), vec![], "Floats".into()); + let pool = SymbolPool::from([ + ( + recursive.clone(), + Symbol::TypeAlias(TypeAlias { + name: recursive.clone(), + resolves_to: Ty::Union(Box::new([ + Ty::Int, + Ty::List(Box::new(Ty::TypeAlias(recursive))), + Ty::Uint8Array, + ])), + recursive: true, + origin: Origin { + source_file_path: "types.baml".into(), + span_start: 0, + }, + }), + ), + ( + floats.clone(), + Symbol::TypeAlias(TypeAlias { + name: floats.clone(), + resolves_to: Ty::List(Box::new(Ty::Float)), + recursive: false, + origin: Origin { + source_file_path: "types.baml".into(), + span_start: 1, + }, + }), + ), + ]); + let supported = pool.keys().cloned().collect(); + let names = NameTable::build(&pool, NamingConvention::Language); + let ty = Ty::TypeAlias(floats); + assert_eq!( + ruby_type(&ty, &supported, &names).as_deref(), + Some("::BamlSdk::Floats") + ); + assert_eq!( + types::ruby_input_type(&ty, &supported, &names, &pool, &[]).as_deref(), + Some("::T::Array[::T.any(::Float, ::Integer)]") + ); + let (files, skipped) = + to_source_code_with_bytecode_and_skipped(&pool, &[], NamingConvention::Language, None); + assert!(skipped.is_empty()); + let root = &files[&PathBuf::from("baml_sdk.rb")]; + assert!(root.contains("BamlSdk::Rec = ::T.type_alias { ::T.any(::Integer, ::T::Array[::T.untyped], ::String) }"), "{root}"); + assert!( + root.contains("BamlSdk::Floats = ::T.type_alias { ::T::Array[::Float] }"), + "{root}" + ); + assert!(root.contains("::Baml::Bridge.define_alias(\"user.Rec\", [:union, [[:int].freeze, [:list, [:alias, \"user.Rec\"].freeze].freeze, [:bytes].freeze].freeze].freeze, recursive: true)"), "{root}"); + assert!(root.contains("::Baml::Bridge.define_alias(\"user.Floats\", [:list, [:float].freeze].freeze, recursive: false)"), "{root}"); +} + +#[test] +fn bytecode_is_binary_and_generation_is_stable() { + let first = to_source_code_with_bytecode( + &SymbolPool::new(), + &[0, 255, 34, 92], + NamingConvention::Language, + None, + ); + let second = to_source_code_with_bytecode( + &SymbolPool::new(), + &[0, 255, 34, 92], + NamingConvention::Language, + None, + ); + assert_eq!(first, second); + let embedded = baml_sdkgen_types::embedded_bytecode_base64(&[0, 255, 34, 92]); + assert_eq!( + first[&PathBuf::from("baml_sdk/__bytecode.rb")], + format!( + "# typed: strict\n# frozen_string_literal: true\nmodule BamlSdk\n BYTECODE = ::T.let(\"{embedded}\".b.freeze, ::String)\n METADATA = ::T.let(nil, ::T.nilable(::String))\nend\n" + ) + ); + assert!(embedded.is_ascii() && !embedded.contains(['\n', '\r'])); + assert!(first[&PathBuf::from("baml_sdk.rb")].contains("::Baml::Bridge.initialize!\n")); +} diff --git a/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/types.rs b/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/types.rs new file mode 100644 index 00000000000..0e2bb0883fd --- /dev/null +++ b/baml_language/sdks/ruby/sdkgen_ruby_sorbet/src/types.rs @@ -0,0 +1,190 @@ +use std::collections::BTreeSet; + +use baml_sdkgen_types::{Name, Symbol, SymbolPool, Ty}; + +use crate::names::NameTable; + +pub(crate) fn ruby_type(ty: &Ty, supported: &BTreeSet, names: &NameTable) -> Option { + ruby_type_with_params(ty, supported, names, &[]) +} + +pub(crate) fn ruby_type_with_params( + ty: &Ty, + supported: &BTreeSet, + names: &NameTable, + params: &[String], +) -> Option { + render( + ty, + supported, + names, + false, + None, + params, + &mut BTreeSet::new(), + ) +} + +pub(crate) fn ruby_input_type( + ty: &Ty, + supported: &BTreeSet, + names: &NameTable, + pool: &SymbolPool, + params: &[String], +) -> Option { + render( + ty, + supported, + names, + true, + Some(pool), + params, + &mut BTreeSet::new(), + ) +} + +pub(crate) fn ruby_alias_type( + ty: &Ty, + pool: &SymbolPool, + supported: &BTreeSet, + names: &NameTable, +) -> Option { + render( + ty, + supported, + names, + false, + Some(pool), + &[], + &mut BTreeSet::new(), + ) +} + +fn render( + ty: &Ty, + supported: &BTreeSet, + names: &NameTable, + input: bool, + aliases: Option<&SymbolPool>, + params: &[String], + visiting: &mut BTreeSet, +) -> Option { + let mut child = |ty: &Ty| render(ty, supported, names, input, aliases, params, visiting); + Some(match ty { + // The pool also defines baml.String and baml.Float; do not resolve to those wrappers. + Ty::Literal(baml_base::Literal::String(_), ..) => "::String".into(), + Ty::Literal(baml_base::Literal::Int(_) | baml_base::Literal::Bigint(_), ..) => { + "::Integer".into() + } + Ty::Literal(baml_base::Literal::Float(_), ..) => return None, + Ty::Literal(baml_base::Literal::Bool(_), ..) => "::T::Boolean".into(), + Ty::String => "::String".into(), + Ty::Int | Ty::Bigint => "::Integer".into(), + // Sorbet rejects Integer for Float; the SDK widens integer inputs. + Ty::Float if input => "::T.any(::Float, ::Integer)".into(), + Ty::Float => "::Float".into(), + Ty::Uint8Array => "::String".into(), + Ty::Media(kind) => match kind { + baml_base::MediaKind::Image => "::Baml::Image", + baml_base::MediaKind::Audio => "::Baml::Audio", + baml_base::MediaKind::Video => "::Baml::Video", + baml_base::MediaKind::Pdf => "::Baml::Pdf", + baml_base::MediaKind::Generic => "::Baml::Media", + } + .into(), + Ty::Bool => "::T::Boolean".into(), + Ty::Null => "::NilClass".into(), + Ty::RustType => "::T.untyped".into(), + // JSON is a recursive Ruby value, not a generated wrapper. + Ty::TypeAlias(name) if name.to_string() == "baml.json.json" => "::T.untyped".into(), + Ty::Never => "::T.noreturn".into(), + Ty::Function { params, ret, .. } => { + let params = params + .iter() + .enumerate() + .map(|(index, param)| Some(format!("arg{index}: {}", child(¶m.ty)?))) + .collect::>>()?; + let builder = if params.is_empty() { + "::T.proc".into() + } else { + format!("::T.proc.params({})", params.join(", ")) + }; + format!( + "::T.any({builder}.returns({}), ::Baml::Closure)", + child(ret)? + ) + } + Ty::TypeVar(param) => params + .get(param.index() as usize) + .cloned() + .unwrap_or_else(|| "::T.untyped".into()), + Ty::List(t) => format!("::T::Array[{}]", child(t)?), + Ty::Map { key, value } => format!("::T::Hash[{}, {}]", child(key)?, child(value)?), + Ty::Class(n, args) if n.to_string() == "ai.stream.Stream" && args.len() == 1 => { + format!("::Baml::Stream[{}]", child(&args[0])?) + } + Ty::Class(n, args) if n.to_string() == "ai.FunctionSpec" && args.len() == 1 => { + "::Baml::FunctionSpec".into() + } + Ty::Class(n, args) if supported.contains(n) => { + let base = format!("::{}", names.qualified(n)); + if args.is_empty() { + base + } else { + format!( + "{base}[{}]", + args.iter() + .map(&mut child) + .collect::>>()? + .join(", ") + ) + } + } + Ty::Enum(n) | Ty::EnumVariant(n, _) if supported.contains(n) => { + format!("::{}", names.qualified(n)) + } + Ty::TypeAlias(n) if supported.contains(n) => { + if let Some(pool) = aliases { + // Sorbet cannot resolve recursive aliases. Widen only the back-edge; + // the runtime retains the exact recursive descriptor for encoding. + if !visiting.insert(n.clone()) { + return Some("::T.untyped".into()); + } + let Symbol::TypeAlias(alias) = pool.get(n)? else { + return None; + }; + let result = render( + &alias.resolves_to, + supported, + names, + input, + aliases, + params, + visiting, + ); + visiting.remove(n); + result? + } else { + format!("::{}", names.qualified(n)) + } + } + Ty::Union(items) => { + let mut types = items.iter().map(&mut child).collect::>>()?; + // Literal unions often project several members to the same Ruby type. + let mut seen = BTreeSet::new(); + types.retain(|t| seen.insert(t.clone())); + if types.iter().any(|t| t == "::T.untyped") { + return Some("::T.untyped".into()); + } + match types.as_slice() { + [] => "::T.noreturn".into(), + [only] => only.clone(), + [a, b] if a == "::NilClass" || b == "::NilClass" => { + format!("::T.nilable({})", if a == "::NilClass" { b } else { a }) + } + _ => format!("::T.any({})", types.join(", ")), + } + } + _ => return None, + }) +} diff --git a/tools/sdk-parity-lint/README.md b/tools/sdk-parity-lint/README.md index d8f71636482..a293dd74e9e 100644 --- a/tools/sdk-parity-lint/README.md +++ b/tools/sdk-parity-lint/README.md @@ -37,7 +37,7 @@ def test_optional_args_python_unset_and_none_differ_in_one_call(): ... ``` -Use `//` in C++, C#, Go, Java, Rust, Swift, and TypeScript. The reason after the colon is required. The annotation applies to its canonical test ID across the matrix: environments where the declaration is currently absent are waived, while present declarations remain required by the baseline ratchet. +Use `#` in Python and Ruby, and `//` in C++, C#, Go, Java, Rust, Swift, and TypeScript. The reason after the colon is required. The annotation applies to its canonical test ID across the matrix: environments where the declaration is currently absent are waived, while present declarations remain required by the baseline ratchet. ## Test diff --git a/tools/sdk-parity-lint/ast-grep-rules/ruby.yml b/tools/sdk-parity-lint/ast-grep-rules/ruby.yml new file mode 100644 index 00000000000..ab2314d0a54 --- /dev/null +++ b/tools/sdk-parity-lint/ast-grep-rules/ruby.yml @@ -0,0 +1,23 @@ +id: ruby-test +language: Ruby +rule: + pattern: 'def $NAME $$$BODY end' +constraints: + NAME: + regex: '^test_[A-Za-z0-9_]+$' +--- +id: ruby-audit-test +language: Ruby +rule: + all: + - kind: method + - has: + field: name + regex: '^test[A-Za-z0-9_]*[!?]?$' +--- +id: sdk-parity-lint-skip +language: Ruby +rule: + all: + - kind: comment + - regex: 'SDK_PARITY_LINT\(skip\):' diff --git a/tools/sdk-parity-lint/baml_src/annotations.baml b/tools/sdk-parity-lint/baml_src/annotations.baml index 1f235a50787..a006a0b2d86 100644 --- a/tools/sdk-parity-lint/baml_src/annotations.baml +++ b/tools/sdk-parity-lint/baml_src/annotations.baml @@ -33,6 +33,15 @@ function line_is_annotation_layout(language: string, line: string) -> bool { true } else if (language == "python" || language == "java") { trimmed.starts_with("@") + } else if (language == "ruby") { + if (!trimmed.starts_with("#")) { + return false; + } + let comment = trimmed.slice(1, trimmed.length()).trim(); + if (comment.starts_with("def ") || comment.starts_with("def\t")) { + return !comment.slice(3, comment.length()).trim().starts_with("test"); + } + true } else if (language == "rust" || language == "csharp") { trimmed.starts_with("#[") && trimmed.ends_with("]") } else { diff --git a/tools/sdk-parity-lint/baml_src/ast_grep.baml b/tools/sdk-parity-lint/baml_src/ast_grep.baml index 0da98a06bfc..704a64ea424 100644 --- a/tools/sdk-parity-lint/baml_src/ast_grep.baml +++ b/tools/sdk-parity-lint/baml_src/ast_grep.baml @@ -128,9 +128,9 @@ function scan_source(repo_root: string, config: SourceConfig) -> SourceScan { } function scan_all_sources(repo_root: string, configs: SourceConfig[]) -> SourceScan[] { - if (configs.length() != 8) { + if (configs.length() != 9) { throw baml.errors.InvalidArgument { - message: `expected eight SDK source configurations, got ${configs.length()}`, + message: `expected nine SDK source configurations, got ${configs.length()}`, }; } let python = spawn { scan_source(repo_root, configs[0]) }; @@ -141,6 +141,7 @@ function scan_all_sources(repo_root: string, configs: SourceConfig[]) -> SourceS let go = spawn { scan_source(repo_root, configs[5]) }; let java = spawn { scan_source(repo_root, configs[6]) }; let swift = spawn { scan_source(repo_root, configs[7]) }; + let ruby = spawn { scan_source(repo_root, configs[8]) }; [ await python, await typescript, @@ -150,6 +151,7 @@ function scan_all_sources(repo_root: string, configs: SourceConfig[]) -> SourceS await go, await java, await swift, + await ruby, ] } diff --git a/tools/sdk-parity-lint/baml_src/config.baml b/tools/sdk-parity-lint/baml_src/config.baml index f0841893efd..90eeb29e5d0 100644 --- a/tools/sdk-parity-lint/baml_src/config.baml +++ b/tools/sdk-parity-lint/baml_src/config.baml @@ -10,6 +10,7 @@ function sdk_environments() -> string[] { "go", "java", "swift", + "ruby_sorbet", ] } @@ -144,6 +145,17 @@ function source_configs(repo_root: string) -> SourceConfig[] { "NAME", "test_", ), + make_source_config( + "ruby", + "ruby_sorbet", + "baml_language/sdk_tests/crates/ruby_sorbet", + `${rules_root}/ruby.yml`, + "**/customizable/**/*.rb", + ".rb", + ["ruby-test"], + "NAME", + "test_", + ), ] } diff --git a/tools/sdk-parity-lint/baml_src/normalize.baml b/tools/sdk-parity-lint/baml_src/normalize.baml index 19dec69b37f..65a5137e384 100644 --- a/tools/sdk-parity-lint/baml_src/normalize.baml +++ b/tools/sdk-parity-lint/baml_src/normalize.baml @@ -259,6 +259,7 @@ function audit_rule_ids(language: string) -> string[] { || language == "java" || language == "swift" || language == "csharp" + || language == "ruby" ) { [`${language}-audit-test`] } else { diff --git a/tools/sdk-parity-lint/baml_src/tests.baml b/tools/sdk-parity-lint/baml_src/tests.baml index 5d7d5592037..8277ad08d48 100644 --- a/tools/sdk-parity-lint/baml_src/tests.baml +++ b/tools/sdk-parity-lint/baml_src/tests.baml @@ -49,6 +49,7 @@ test "ast-grep rules find framework tests and ignore ordinary helpers" { let go = scan_source(repo_root, fixture_config(repo_root, "go")); let java = scan_source(repo_root, fixture_config(repo_root, "java")); let swift = scan_source(repo_root, fixture_config(repo_root, "swift")); + let ruby = scan_source(repo_root, fixture_config(repo_root, "ruby_sorbet")); validate_audit_coverage(python); validate_audit_coverage(typescript); validate_audit_coverage(cpp); @@ -57,6 +58,7 @@ test "ast-grep rules find framework tests and ignore ordinary helpers" { validate_audit_coverage(go); validate_audit_coverage(java); validate_audit_coverage(swift); + validate_audit_coverage(ruby); assert.equal(declaration_count(python), 2); assert.equal(declaration_count(typescript), 8); assert.equal(declaration_count(cpp), 1); @@ -64,7 +66,8 @@ test "ast-grep rules find framework tests and ignore ordinary helpers" { assert.equal(declaration_count(rust), 4); assert.equal(declaration_count(go), 1); assert.equal(declaration_count(java), 2); - assert.equal(declaration_count(swift), 1) + assert.equal(declaration_count(swift), 1); + assert.equal(declaration_count(ruby), 2) } test "audit coverage rejects unsupported TypeScript declaration forms" { @@ -132,6 +135,78 @@ test "normalization derives category IDs and associates an annotation" { assert.equal(annotations[0].reason, "fixture-only behavior") } +test "Ruby normalization derives category IDs and associates a hash comment annotation" { + let repo_root = "."; + let scan = scan_source(repo_root, fixture_config(repo_root, "ruby_sorbet")); + let tests = discover_tests([scan]); + let annotations = build_annotation_configs(repo_root, [scan], tests); + let ids = tests.map((test_case) -> { + test_case.sdk_test_id + }); + ids.sort_by_key((sdk_test_id) -> { + sdk_test_id + }); + assert.equal(ids, ["fixture/sync_case", "fixture/with_parameters_case"]); + assert.equal(annotations.length(), 1); + assert.equal(annotations[0].sdk_test_id, "fixture/sync_case"); + assert.equal(annotations[0].reason, "fixture-only behavior"); + assert.equal(line_is_annotation_layout("ruby", " # comment"), true); + assert.equal(line_is_annotation_layout("ruby", " @instance_variable = 1"), false) +} + +test "Ruby skip cannot cross a commented-out test declaration" { + assert.equal(line_is_annotation_layout("ruby", "# def test_disabled"), false); + assert.equal(line_is_annotation_layout("ruby", "# def test_disabled!"), false); + let base = fixture_config(".", "ruby_sorbet"); + let config = make_source_config( + base.language, base.source_directory, `${base.source_root}-adjacency`, base.rule_file, + base.file_glob, base.suffix, base.declaration_rule_ids, + base.raw_name_field, base.prefix, + ); + let scan = scan_source(".", config); + let tests = discover_tests([scan]); + assert.equal(tests.length(), 1); + { + let _ = build_annotation_configs(".", [scan], tests); + baml.sys.panic("expected skip adjacency rejection") + } catch (e) { + baml.errors.ParseError => assert.equal(e.message.includes("is not immediately before"), true) + } +} + +test "Ruby audit detects predicates bangs and endless definitions" { + let base = fixture_config(".", "ruby_sorbet"); + let config = make_source_config( + base.language, base.source_directory, `${base.source_root}-audit`, base.rule_file, + base.file_glob, base.suffix, base.declaration_rule_ids, + base.raw_name_field, base.prefix, + ); + let scan = scan_source(".", config); + let audits = scan.matches.filter((ast_match: AstMatch) -> bool { + ast_match.ruleId == "ruby-audit-test" + }); + assert.equal(audits.length(), 3); + for (let ast_match in audits) { + { + validate_audit_coverage(SourceScan { config: config, matches: [ast_match] }); + baml.sys.panic("expected unsupported Ruby method rejection") + } catch (e) { + baml.errors.ParseError => null + } + } +} + +test "audit coverage rejects Ruby methods without canonical test IDs" { + let config = fixture_config(".", "ruby_sorbet"); + let scan = SourceScan { config: config, matches: [fixture_ast_match("ruby-audit-test", 0, 24)] }; + { + validate_audit_coverage(scan); + baml.sys.panic("expected noncanonical Ruby test declaration rejection") + } catch (e) { + baml.errors.ParseError => null + } +} + test "dynamic TypeScript titles are rejected instead of undercounted" { { let _ = decode_typescript_id("`${prefix}_dynamic_case`", "fixture/customizable/sample.test.ts", 1); @@ -328,7 +403,7 @@ test "baseline report round trips escaped Markdown cells" { let row = fixture_parity_row( "type_shapes/round_trip", sdk_envs, - [true, false, true, false, true, false, true, false, true, false], + [true, false, true, false, true, false, true, false, true, false, true], ["python_pydantic2", "typescript_node"], "reason with a | pipe and \\ slash", ); @@ -416,5 +491,6 @@ test "TypeScript runtime membership narrows only TypeScript requirements" { assert.equal(array_contains(required, "typescript_node"), false); assert.equal(array_contains(required, "typescript_web_chromium"), true); assert.equal(array_contains(required, "typescript_web_cloudflare_workers"), true); - assert.equal(array_contains(required, "python_pydantic2"), true) + assert.equal(array_contains(required, "python_pydantic2"), true); + assert.equal(array_contains(required, "ruby_sorbet"), true) } diff --git a/tools/sdk-parity-lint/test-fixtures/ruby-adjacency/fixture/customizable/test_layout.rb b/tools/sdk-parity-lint/test-fixtures/ruby-adjacency/fixture/customizable/test_layout.rb new file mode 100644 index 00000000000..984b7b31bf3 --- /dev/null +++ b/tools/sdk-parity-lint/test-fixtures/ruby-adjacency/fixture/customizable/test_layout.rb @@ -0,0 +1,6 @@ +# SDK_PARITY_LINT(skip): must not attach to the next live test +# def test_fixture_disabled +# end + +def test_fixture_live +end diff --git a/tools/sdk-parity-lint/test-fixtures/ruby-audit/fixture/customizable/test_unsupported.rb b/tools/sdk-parity-lint/test-fixtures/ruby-audit/fixture/customizable/test_unsupported.rb new file mode 100644 index 00000000000..e94200c02d3 --- /dev/null +++ b/tools/sdk-parity-lint/test-fixtures/ruby-audit/fixture/customizable/test_unsupported.rb @@ -0,0 +1,11 @@ +class UnsupportedFormsTest + def test_predicate? + true + end + + def test_bang! + true + end + + def test_endless = true +end diff --git a/tools/sdk-parity-lint/test-fixtures/ruby/fixture/customizable/test_sample.rb b/tools/sdk-parity-lint/test-fixtures/ruby/fixture/customizable/test_sample.rb new file mode 100644 index 00000000000..ce2d5efdb6c --- /dev/null +++ b/tools/sdk-parity-lint/test-fixtures/ruby/fixture/customizable/test_sample.rb @@ -0,0 +1,14 @@ +class SampleTest + def helper + nil + end + + def test_with_parameters_case(value = nil) + value + end + + # SDK_PARITY_LINT(skip): fixture-only behavior + def test_sync_case + nil + end +end