Skip to content

Sam/generics#3727

Closed
sxlijin wants to merge 6 commits into
canaryfrom
sam/generics
Closed

Sam/generics#3727
sxlijin wants to merge 6 commits into
canaryfrom
sam/generics

Conversation

@sxlijin

@sxlijin sxlijin commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

New Features

  • Opaque host values: Host objects now preserve reference identity when passed through generic BAML functions without serialization.
  • Explicit type arguments: Functions support explicit $types syntax to bind concrete types for generic parameters, enabling precise type resolution at call time.

@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Jun 10, 2026 4:49am
promptfiddle Ready Ready Preview, Comment Jun 10, 2026 4:49am
promptfiddle2 Error Error Jun 10, 2026 4:49am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements explicit generic type arguments (BEP-039) and opaque host-only values across the BAML compiler, engine, VM, and SDKs. Generic function signatures are captured as un-erased templates during compilation, then resolved and substituted at call boundaries with explicit host type arguments. Opaque host values are sealed handles that round-trip through generics while preserving identity, binding only to unknown-accepting type positions. The changes span the compiler, engine boundary logic, VM comparison semantics, C-FFI bridge, and Node.js/Python SDKs, with comprehensive integration tests.

Changes

Core Infrastructure for Generics & Opaque Values

Layer / File(s) Summary
Type System & Data Structures
crates/bex_vm_types/src/types.rs, crates/bex_vm_types/src/lib.rs, crates/bex_resource_types/src/host_value.rs, crates/bridge_ctypes/types/baml_core/cffi/v1/baml_inbound.proto, crates/bridge_ctypes/types/baml_core/cffi/v1/baml_inbound_pb2.pyi
Add FunctionSignatureTemplate struct to store un-erased parameter and return types with generic TypeArgRef(N) positions; extend Function with optional signature_template field; add HostValueKind::Opaque variant for host-only sealed values; introduce InboundTypeRef proto message for recursive type structure; add HOST_VALUE_OPAQUE handle type.
Compiler: Extract & Store Signature Templates
crates/baml_compiler2_emit/src/emit.rs, crates/baml_compiler2_emit/src/lib.rs, crates/bex_vm/src/package_baml/mod.rs, crates/bex_vm/src/vm.rs, crates/bex_vm/tests/load_type.rs, crates/bex_vm/tests/method_class_type_args.rs
During function compilation, detect generic type variables in parameter and return types, build un-erased TyTemplate values via tir2_to_template, construct FunctionSignatureTemplate, and store in compiled function metadata; set signature_template: None for builtins and synthesized functions.
Engine: Host Type Reference Resolution
crates/bex_engine/src/lib.rs, crates/bex_project/src/bex.rs
Implement resolve_host_type_ref(name, args) public API to convert host-supplied $types structural references (builtins, class/enum names) into runtime Ty with arity validation and error discrimination.
Engine: Boundary Signatures with Type Substitution
crates/bex_engine/src/lib.rs
Introduce boundary_signature helper to derive parameter, return, and throws types from signature_template, substituting generic TypeArgRef(N) positions with provided host type_args, enabling correct inbound coercion and validation.
Value Conversion: Opaque Host Values & Binding
crates/bex_engine/src/conversion.rs
Extend convert_external_to_vm_value_with_ty to handle opaque host values: non-callable host values without declared type become RustData; callable host values require function types or error; types accepting unknown (directly or in unions) bind opaque values via ty_accepts_opaque_host_value helper.
VM: RustData Comparison Semantics
crates/bex_vm/src/vm.rs
Update exec_cmpop for Object::RustData: equality (==/!=) based on HostValueArc value or Arc pointer identity; ordering comparisons (<, <=, >, >=) return CannotApplyCmpOp, enabling host-identity equality for opaque values.
C-FFI Bridge: Type Args Resolution & Injection
crates/bridge_cffi/src/lib.rs
Add resolve_type_args helper to recursively resolve inbound InboundTypeRef entries into concrete Ty values; inject resolved type_args into FunctionCallContextBuilder in call_function_inner C-ABI path.
Bridge Wire Protocol: Opaque Handle Encoding/Decoding
crates/bridge_ctypes/src/value_decode.rs, crates/bridge_ctypes/src/value_encode.rs, crates/bridge_wasm/src/handle.rs, crates/bridge_wasm/tests/host_callable.rs
Recognize HOST_VALUE_OPAQUE handles on inbound path, map to HostValueKind::Opaque during decode; encode HostValueKind::Opaque to BamlHandleType::HostValueOpaque; include opaque in host-owned drop logic to preserve registry keyspace.

Node.js & Python SDK Integration

Layer / File(s) Summary
Node.js SDK: Type Args & Opaque Support
sdks/nodejs/bridge_nodejs/typescript_src/{index.ts,proto.ts,host_error_registry.ts,proto/baml_cffi.d.ts,proto/baml_cffi.js}
Add BamlOpaque class and opaque() wrapper for explicit marking; extend encodeCallArgs to accept typeArgs and serialize via buildTypeRef helper supporting primitives, builtins, and structural tokens; implement same-process opaque rehydration with per-encode deduplication via registerHostOpaque and tryRehydrateHostValueByKey.
Node.js Runtime & Tests
sdks/nodejs/bridge_nodejs/src/runtime.rs, sdks/nodejs/bridge_nodejs/tests/{host_opaque.test.ts,type_args.test.ts}
Wire type_args through call_function_sync and call_function, resolve via bridge_cffi::resolve_type_args, inject into context builder; add comprehensive test coverage for opaque identity preservation, equality semantics, list/field propagation, concrete parameter rejection, and type argument binding effects.
Python SDK: Type Args & Opaque Registration
sdks/python/rust/bridge_python/src/{host_value.rs,lib.rs,py_handle.rs}, sdks/python/src/baml_core/{baml_py.pyi,proto.py,cffi/v1/baml_inbound_pb2.{py,pyi}}
Add register_host_opaque() PyO3-exposed function to store Python objects in host registry; extend Python encode_call_args to accept type_args, build InboundTypeRef via _set_type_ref supporting primitives/builtins/Pydantic tokens, thread opaque_keys dedup map through recursive encoding; rehydrate opaque handles on decode via lookup_host_value.
Python Runtime & Tests
sdks/python/rust/bridge_python/src/runtime.rs, sdks/python/src/baml_core/__init__.py, sdks/python/tests/{test_host_opaque.py,test_type_args.py,test_host_callable.py}
Resolve type_args from inbound proto in both sync and async paths, inject into context builder; extend Python SDK's call_function_sync and call_function to accept type_args parameter; add comprehensive test coverage for opaque round-trip identity, primitive coercion with type bindings, class promotion from untagged maps, and contract violation detection.

Rust Engine Tests & Enablement

Layer / File(s) Summary
Rust Engine Tests: Generics & Opaque Boundaries
crates/bex_engine/tests/{host_boundary_generics.rs,host_boundary_opaque.rs}
Add integration tests for generic function boundaries: identity round-trips with/without explicit type args, union return handling, generic method dispatch, untagged map promotion, opaque value round-trips with identity preservation, opaque equality (==/!=) based on registry key, rejection when bound to concrete parameters, ordering comparison failures, union arm routing.
Test Cleanup & Enablement
sdk_tests/crates/python_pydantic2/type_shapes/customizable/test_generic.py
Unskip test_generic() test now that generic type argument implementation is complete.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • BoundaryML/baml#3169: Extends the opaque host-value wire model introduced in earlier work, adding HOST_VALUE_OPAQUE handle encoding/decoding in bridge_ctypes CFFI protocols.
  • BoundaryML/baml#3571: Related to host-value boundary machinery; modifies bex_engine conversion and bex_resource_types to route opaque host-value behavior at call boundaries.

🐰 Type args dance and opaque values play,
Generic boundaries now have their way,
Host objects sealed, round-trips stay the same,
Identity preserved in BAML's name!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Sam/generics' is a branch reference name formatted as 'Author/Topic' rather than a descriptive commit message explaining what the PR actually implements. Use a clear, specific title like 'Add support for generic type arguments at function call boundary' or 'Implement host-boundary generic type resolution and opaque value handling' to better describe the actual changes made.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sam/generics
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch sam/generics

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.43.0)
baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto/baml_cffi.js

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f4e8e4305e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +430 to +439
if (ht === BamlHandleType.HOST_VALUE_OPAQUE) {
// Same-process rehydration (bridge generics): an opaque
// host-only value decodes back to the ORIGINAL JS reference.
// Dead/foreign keys degrade to the bare BamlHandle.
const original = tryRehydrateHostValueByKey(
holder.handleValue.key as unknown as import('./native.js').HandleKey,
ht,
);
if (original !== undefined) return original;
return handle;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid wrapping host-value keys in BamlHandle

When this HOST_VALUE_OPAQUE branch runs, handle has already been constructed as a normal BamlHandle, and the miss path returns it. BamlHandle finalization always calls baml_handle_release, but HOST_VALUE_* keys are explicitly not in HANDLE_TABLE and the Node host-value registry and CFFI handle table both allocate keys starting at 1, so decoding an opaque value can release an unrelated media/heap handle with the same numeric key (even same-process hits create a temporary BamlHandle before returning the original). This needs the same host-value skip behavior added for Python/WASM, or avoid constructing a BamlHandle for HOST_VALUE_OPAQUE unless it is non-releasing.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
baml_language/crates/bex_engine/src/conversion.rs (1)

547-638: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject non-callable host values before function binding.

At Line 559, HostValueKind::Opaque/HostValueKind::Error can still flow into peel_function_ty(ty) and get materialized as Object::HostClosure when the declared type is function-shaped. That violates the host-boundary contract (host-only values should only bind to unknown/generic positions).

🔧 Proposed fix
                 if accepts_unknown && prefers_opaque {
                     let dyn_arc: std::sync::Arc<dyn std::any::Any + Send + Sync> = arc;
                     return Ok(Value::object(
                         holder.holder_mut().tlab_mut().alloc_rust_data(dyn_arc),
                     ));
                 }
+                if !matches!(arc.kind, bex_external_types::HostValueKind::Callable) {
+                    return Err(EngineError::TypeMismatch {
+                        message: format!(
+                            "host-only value cannot be passed where the declared type \
+                             is `{ty}`; host-only values bind only to generic/unknown \
+                             positions"
+                        ),
+                    });
+                }
                 // Peel through Optional / Union to land on the function type.
                 let function_ty = peel_function_ty(ty).ok_or_else(|| {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/bex_engine/src/conversion.rs` around lines 547 - 638,
The code must reject host values that are host-only (arc.kind not
HostValueKind::Callable) before converting them into a HostClosure: add a guard
that if arc.kind is not Callable and peel_function_ty(ty).is_some() then return
an EngineError::TypeMismatch (same wording used elsewhere: e.g. "host-only value
cannot be passed where the declared type is `{ty}`; host-only values bind only
to generic/unknown positions"). Place this check alongside the existing
accepts_unknown / prefers_opaque logic (using the same symbols
ty_accepts_opaque_host_value, prefers_opaque, arc.kind, peel_function_ty, and
the EngineError::TypeMismatch path) so non-callable HostValueKind::Opaque/Error
never reach the HostClosure construction.
baml_language/crates/bex_engine/src/lib.rs (1)

1760-1775: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

call_callable still drops instantiated generic types at the host boundary.

This path seeds seed_type_args into the VM entry frame, but it coerces arguments and re-types the return value from func.param_types / func.return_type before that. For generic closures and bound methods, those fields are the erased versions, so handle-based calls no longer enforce the same instantiated signature that call_function_bound_args() now uses.

Also applies to: 1800-1830

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/bex_engine/src/lib.rs` around lines 1760 - 1775,
call_callable is using the erased Function.param_types and Function.return_type
when coercing args and re-typing returns, which drops instantiated generic type
information seeded by seed_type_args; update call_callable to apply the
seeded/instantiated type arguments from the entry frame (the same source
call_function_bound_args uses) before performing argument coercion and before
wrapping the return value so generic closures and bound methods use the
instantiated signature, i.e. fetch the entry frame/type args (seed_type_args)
from the VM frame created around seed_type_args and use that instantiated
param_types/return_type for coercion and return re-typing instead of
func.param_types/func.return_type; keep the existing NativeUnresolved check
(matches!(func.kind, FunctionKind::NativeUnresolved)) and only change where
param/return types are derived for coercion and wrapping.
🧹 Nitpick comments (3)
baml_language/crates/bridge_ctypes/src/value_decode.rs (1)

62-79: ⚡ Quick win

Add a unit test for HostValueOpaque decode mapping.

This branch is new but only callable/error are directly asserted in tests. A dedicated opaque case will lock the wire contract.

🧪 Suggested test addition
+    #[test]
+    fn decode_inbound_host_value_opaque() {
+        let table = CffiHandleTable::new();
+        let handle = BamlHandle {
+            key: 555,
+            handle_type: BamlHandleType::HostValueOpaque as i32,
+        };
+        let inbound = InboundValue {
+            value: Some(InboundValueVariant::Handle(handle)),
+        };
+        let result = inbound_to_external(inbound, &table).expect("decode succeeds");
+        match result {
+            BexExternalValue::HostValue(arc) => {
+                assert_eq!(arc.key, 555);
+                assert_eq!(arc.kind, bex_project::HostValueKind::Opaque);
+            }
+            other => panic!("unexpected variant: {other:?}"),
+        }
+        assert!(
+            table.resolve(555).is_none(),
+            "HOST_VALUE_OPAQUE must not touch HANDLE_TABLE"
+        );
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/bridge_ctypes/src/value_decode.rs` around lines 62 - 79,
Add a unit test that asserts the wire-to-enum mapping for the opaque host-value
branch: construct a Baml handle with handle_type ==
BamlHandleType::HostValueOpaque as i32 and a sample key, call the same decode
function that produces host_value_kind (the code branch that creates arc via
bex_project::HostValueArc::intern and returns BexExternalValue::HostValue), and
assert the result is a BexExternalValue::HostValue containing an interned
HostValueArc with kind == bex_project::HostValueKind::Opaque and that the key is
preserved; place the test alongside the other HostValue tests so it exercises
the HostValueOpaque branch (referencing host_value_kind,
BamlHandleType::HostValueOpaque, bex_project::HostValueKind::Opaque,
bex_project::HostValueArc::intern, and BexExternalValue::HostValue).
baml_language/crates/bex_resource_types/src/host_value.rs (1)

30-37: ⚡ Quick win

Add a unit test for HostValueKind::Opaque lifecycle parity.

Opaque is a new handle kind, but tests currently pin release semantics for Callable/Error only. Add a unit test that verifies last-drop release behavior (and optionally intern identity reuse) for Opaque as well.

As per coding guidelines, **/*.rs: Prefer writing Rust unit tests over integration tests where possible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/bex_resource_types/src/host_value.rs` around lines 30 -
37, Add a unit test under the same host_value tests module mirroring the
existing lifecycle tests for Callable/Error that exercises
HostValueKind::Opaque: create two interned Opaque HostValue instances (using
HostValue::intern or the same factory used by other tests), clone/drop them to
ensure the host-side release callback is invoked only on the final drop (verify
last-drop release behavior), and optionally assert that interning reuses
identity after complete release; place assertions and setup/teardown the same
way the Callable/Error tests do so the Opaque kind gets identical lifecycle
coverage.

Source: Coding guidelines

baml_language/crates/bex_vm_types/src/types.rs (1)

375-391: Please confirm the Rust lib test suite was run for this change.

Run cargo test --lib before merge since this PR modifies Rust code.
As per coding guidelines, Always run cargo test --lib if you changed any Rust code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/bex_vm_types/src/types.rs` around lines 375 - 391, This
change touches the FunctionSignatureTemplate struct (param_types, return_type)
in bex_vm_types which may affect downstream behavior; run the Rust library test
suite with `cargo test --lib` from the crate root, fix any failing tests caused
by the changes (update tests, adjust implementation or serialization for
FunctionSignatureTemplate, and ensure BorshSerialize/BorshDeserialize behavior
remains correct for baml_type::TyTemplate), then re-run the tests until they
pass before merging.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@baml_language/crates/baml_compiler2_emit/src/lib.rs`:
- Around line 1505-1512: The current code only stores param_types/return_type in
signature_template (when signature_has_type_var) but leaves
compiled_fn.throws_type coming from the erased compute_throws_type() path, so
generic `throws` information is lost; fix by preserving the throws template
alongside parameters/return by including the throws template when building
bex_vm_types::FunctionSignatureTemplate (or by making compiled_fn.throws_type
take the template result when signature_has_type_var is true), i.e., call the
generic-aware throws computation used for compute_throws_type() and attach that
throws template into signature_template (or override compiled_fn.throws_type
from that template) so calls with concrete type_args can specialize `throws`
correctly (refer to signature_template, signature_has_type_var,
compiled_fn.throws_type, compute_throws_type()).

In `@baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto.ts`:
- Around line 94-102: The bug is that ctx.registered mixes callable and opaque
keys but rollback only releases callables, leaking opaque registrations; update
registration to record the kind for each entry (e.g., push an object/tuple like
{ kind: "opaque", key } instead of raw key) in encodeOpaque (which calls
registerHostOpaque) and in the analogous callable registration sites, and change
the rollback logic to inspect each entry's kind and call the correct release
function (e.g., releaseHostOpaque for "opaque" and releaseHostCallable for
"callable"). Also update any other sites mentioned (the other encode/register
locations) to use the kind-tagged registration so rollback uniformly frees the
right resource.

---

Outside diff comments:
In `@baml_language/crates/bex_engine/src/conversion.rs`:
- Around line 547-638: The code must reject host values that are host-only
(arc.kind not HostValueKind::Callable) before converting them into a
HostClosure: add a guard that if arc.kind is not Callable and
peel_function_ty(ty).is_some() then return an EngineError::TypeMismatch (same
wording used elsewhere: e.g. "host-only value cannot be passed where the
declared type is `{ty}`; host-only values bind only to generic/unknown
positions"). Place this check alongside the existing accepts_unknown /
prefers_opaque logic (using the same symbols ty_accepts_opaque_host_value,
prefers_opaque, arc.kind, peel_function_ty, and the EngineError::TypeMismatch
path) so non-callable HostValueKind::Opaque/Error never reach the HostClosure
construction.

In `@baml_language/crates/bex_engine/src/lib.rs`:
- Around line 1760-1775: call_callable is using the erased Function.param_types
and Function.return_type when coercing args and re-typing returns, which drops
instantiated generic type information seeded by seed_type_args; update
call_callable to apply the seeded/instantiated type arguments from the entry
frame (the same source call_function_bound_args uses) before performing argument
coercion and before wrapping the return value so generic closures and bound
methods use the instantiated signature, i.e. fetch the entry frame/type args
(seed_type_args) from the VM frame created around seed_type_args and use that
instantiated param_types/return_type for coercion and return re-typing instead
of func.param_types/func.return_type; keep the existing NativeUnresolved check
(matches!(func.kind, FunctionKind::NativeUnresolved)) and only change where
param/return types are derived for coercion and wrapping.

---

Nitpick comments:
In `@baml_language/crates/bex_resource_types/src/host_value.rs`:
- Around line 30-37: Add a unit test under the same host_value tests module
mirroring the existing lifecycle tests for Callable/Error that exercises
HostValueKind::Opaque: create two interned Opaque HostValue instances (using
HostValue::intern or the same factory used by other tests), clone/drop them to
ensure the host-side release callback is invoked only on the final drop (verify
last-drop release behavior), and optionally assert that interning reuses
identity after complete release; place assertions and setup/teardown the same
way the Callable/Error tests do so the Opaque kind gets identical lifecycle
coverage.

In `@baml_language/crates/bex_vm_types/src/types.rs`:
- Around line 375-391: This change touches the FunctionSignatureTemplate struct
(param_types, return_type) in bex_vm_types which may affect downstream behavior;
run the Rust library test suite with `cargo test --lib` from the crate root, fix
any failing tests caused by the changes (update tests, adjust implementation or
serialization for FunctionSignatureTemplate, and ensure
BorshSerialize/BorshDeserialize behavior remains correct for
baml_type::TyTemplate), then re-run the tests until they pass before merging.

In `@baml_language/crates/bridge_ctypes/src/value_decode.rs`:
- Around line 62-79: Add a unit test that asserts the wire-to-enum mapping for
the opaque host-value branch: construct a Baml handle with handle_type ==
BamlHandleType::HostValueOpaque as i32 and a sample key, call the same decode
function that produces host_value_kind (the code branch that creates arc via
bex_project::HostValueArc::intern and returns BexExternalValue::HostValue), and
assert the result is a BexExternalValue::HostValue containing an interned
HostValueArc with kind == bex_project::HostValueKind::Opaque and that the key is
preserved; place the test alongside the other HostValue tests so it exercises
the HostValueOpaque branch (referencing host_value_kind,
BamlHandleType::HostValueOpaque, bex_project::HostValueKind::Opaque,
bex_project::HostValueArc::intern, and BexExternalValue::HostValue).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: df0a1053-4f0d-48c5-968f-c37af25c76c4

📥 Commits

Reviewing files that changed from the base of the PR and between bf5aa42 and f4e8e43.

⛔ Files ignored due to path filters (14)
  • baml_language/sdks/nodejs/bridge_nodejs/dist/host_error_registry.d.ts is excluded by !**/dist/**
  • baml_language/sdks/nodejs/bridge_nodejs/dist/host_error_registry.d.ts.map is excluded by !**/dist/**, !**/*.map
  • baml_language/sdks/nodejs/bridge_nodejs/dist/host_error_registry.js is excluded by !**/dist/**
  • baml_language/sdks/nodejs/bridge_nodejs/dist/host_error_registry.js.map is excluded by !**/dist/**, !**/*.map
  • baml_language/sdks/nodejs/bridge_nodejs/dist/index.d.ts is excluded by !**/dist/**
  • baml_language/sdks/nodejs/bridge_nodejs/dist/index.d.ts.map is excluded by !**/dist/**, !**/*.map
  • baml_language/sdks/nodejs/bridge_nodejs/dist/index.js is excluded by !**/dist/**
  • baml_language/sdks/nodejs/bridge_nodejs/dist/index.js.map is excluded by !**/dist/**, !**/*.map
  • baml_language/sdks/nodejs/bridge_nodejs/dist/proto.d.ts is excluded by !**/dist/**
  • baml_language/sdks/nodejs/bridge_nodejs/dist/proto.d.ts.map is excluded by !**/dist/**, !**/*.map
  • baml_language/sdks/nodejs/bridge_nodejs/dist/proto.js is excluded by !**/dist/**
  • baml_language/sdks/nodejs/bridge_nodejs/dist/proto.js.map is excluded by !**/dist/**, !**/*.map
  • baml_language/sdks/nodejs/bridge_nodejs/dist/proto/baml_cffi.d.ts is excluded by !**/dist/**
  • baml_language/sdks/nodejs/bridge_nodejs/dist/proto/baml_cffi.js is excluded by !**/dist/**
📒 Files selected for processing (41)
  • baml_language/crates/baml_compiler2_emit/src/emit.rs
  • baml_language/crates/baml_compiler2_emit/src/lib.rs
  • baml_language/crates/bex_engine/src/conversion.rs
  • baml_language/crates/bex_engine/src/lib.rs
  • baml_language/crates/bex_engine/tests/host_boundary_generics.rs
  • baml_language/crates/bex_engine/tests/host_boundary_opaque.rs
  • baml_language/crates/bex_project/src/bex.rs
  • baml_language/crates/bex_resource_types/src/host_value.rs
  • baml_language/crates/bex_vm/src/package_baml/mod.rs
  • baml_language/crates/bex_vm/src/vm.rs
  • baml_language/crates/bex_vm/tests/load_type.rs
  • baml_language/crates/bex_vm/tests/method_class_type_args.rs
  • baml_language/crates/bex_vm_types/src/lib.rs
  • baml_language/crates/bex_vm_types/src/types.rs
  • baml_language/crates/bridge_cffi/src/lib.rs
  • baml_language/crates/bridge_ctypes/src/value_decode.rs
  • baml_language/crates/bridge_ctypes/src/value_encode.rs
  • baml_language/crates/bridge_ctypes/types/baml_core/cffi/v1/baml_inbound.proto
  • baml_language/crates/bridge_wasm/src/handle.rs
  • baml_language/crates/bridge_wasm/tests/host_callable.rs
  • baml_language/sdk_tests/crates/python_pydantic2/type_shapes/customizable/test_generic.py
  • baml_language/sdks/nodejs/bridge_nodejs/src/runtime.rs
  • baml_language/sdks/nodejs/bridge_nodejs/tests/host_opaque.test.ts
  • baml_language/sdks/nodejs/bridge_nodejs/tests/type_args.test.ts
  • baml_language/sdks/nodejs/bridge_nodejs/typescript_src/host_error_registry.ts
  • baml_language/sdks/nodejs/bridge_nodejs/typescript_src/index.ts
  • baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto.ts
  • baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto/baml_cffi.d.ts
  • baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto/baml_cffi.js
  • baml_language/sdks/python/rust/bridge_python/src/host_value.rs
  • baml_language/sdks/python/rust/bridge_python/src/lib.rs
  • baml_language/sdks/python/rust/bridge_python/src/py_handle.rs
  • baml_language/sdks/python/rust/bridge_python/src/runtime.rs
  • baml_language/sdks/python/src/baml_core/__init__.py
  • baml_language/sdks/python/src/baml_core/baml_py.pyi
  • baml_language/sdks/python/src/baml_core/cffi/v1/baml_inbound_pb2.py
  • baml_language/sdks/python/src/baml_core/cffi/v1/baml_inbound_pb2.pyi
  • baml_language/sdks/python/src/baml_core/proto.py
  • baml_language/sdks/python/tests/test_host_callable.py
  • baml_language/sdks/python/tests/test_host_opaque.py
  • baml_language/sdks/python/tests/test_type_args.py
💤 Files with no reviewable changes (1)
  • baml_language/sdk_tests/crates/python_pydantic2/type_shapes/customizable/test_generic.py

Comment on lines +1505 to +1512
// Only carry a template when some signature position actually references
// a generic parameter — the erased `param_types`/`return_type` are
// authoritative otherwise, and most functions stay template-free.
let signature_template =
signature_has_type_var.then(|| bex_vm_types::FunctionSignatureTemplate {
param_types: param_templates,
return_type: return_template,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Generic throws contracts are still erased here.

signature_template only preserves parameter and return templates, while compiled_fn.throws_type is still populated from the separately erased compute_throws_type() path. That means a signature like throws E can never be specialized from explicit host type_args, so the engine will keep validating/converting throws against unknown even when the call boundary has concrete generic arguments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/baml_compiler2_emit/src/lib.rs` around lines 1505 -
1512, The current code only stores param_types/return_type in signature_template
(when signature_has_type_var) but leaves compiled_fn.throws_type coming from the
erased compute_throws_type() path, so generic `throws` information is lost; fix
by preserving the throws template alongside parameters/return by including the
throws template when building bex_vm_types::FunctionSignatureTemplate (or by
making compiled_fn.throws_type take the template result when
signature_has_type_var is true), i.e., call the generic-aware throws computation
used for compute_throws_type() and attach that throws template into
signature_template (or override compiled_fn.throws_type from that template) so
calls with concrete type_args can specialize `throws` correctly (refer to
signature_template, signature_has_type_var, compiled_fn.throws_type,
compute_throws_type()).

Comment on lines +94 to 102
function encodeOpaque(iv: baml_core.cffi.v1.IInboundValue, inner: unknown, ctx: EncodeCtx): void {
let key = ctx.opaqueKeys?.get(inner);
if (key === undefined) {
key = registerHostOpaque(inner);
ctx.registered.push(key);
ctx.opaqueKeys?.set(inner, key);
}
iv.handle = { key, handleType: BamlHandleType.HOST_VALUE_OPAQUE };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Opaque registrations are leaked on encode rollback paths.

ctx.registered currently mixes callable and opaque keys, but rollback only calls releaseHostCallable(...). When an encode fails before dispatch to Rust, opaque entries remain in the TS registry and retain host objects indefinitely.

Suggested fix (separate cleanup by registration kind)
 interface EncodeCtx {
     syncMode: boolean;
-    registered: HandleKey[];
+    registeredCallables: HandleKey[];
+    registeredOpaque: HandleKey[];
     opaqueKeys?: Map<unknown, HandleKey>;
 }

+function cleanupEncodeCtx(ctx: EncodeCtx): void {
+    for (const k of ctx.registeredCallables) {
+        try { releaseHostCallable(k); } catch {}
+    }
+    for (const k of ctx.registeredOpaque) {
+        // add/export a host_error_registry helper that deletes the TS map entry by key
+        try { releaseHostOpaque(k); } catch {}
+    }
+}
+
 function encodeOpaque(iv: ..., inner: unknown, ctx: EncodeCtx): void {
   let key = ctx.opaqueKeys?.get(inner);
   if (key === undefined) {
     key = registerHostOpaque(inner);
-    ctx.registered.push(key);
+    ctx.registeredOpaque.push(key);
     ctx.opaqueKeys?.set(inner, key);
   }
   iv.handle = { key, handleType: BamlHandleType.HOST_VALUE_OPAQUE };
 }

 // callable registration site
-ctx.registered.push(key);
+ctx.registeredCallables.push(key);

 // all rollback sites
-for (const k of ctx.registered) { ...releaseHostCallable(k)... }
+cleanupEncodeCtx(ctx);

Also applies to: 289-295, 727-741, 833-857

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto.ts` around lines
94 - 102, The bug is that ctx.registered mixes callable and opaque keys but
rollback only releases callables, leaking opaque registrations; update
registration to record the kind for each entry (e.g., push an object/tuple like
{ kind: "opaque", key } instead of raw key) in encodeOpaque (which calls
registerHostOpaque) and in the analogous callable registration sites, and change
the rollback logic to inspect each entry's kind and call the correct release
function (e.g., releaseHostOpaque for "opaque" and releaseHostCallable for
"callable"). Also update any other sites mentioned (the other encode/register
locations) to use the kind-tagged registration so rollback uniformly frees the
right resource.

@sxlijin sxlijin closed this Jul 8, 2026
@sxlijin sxlijin deleted the sam/generics branch July 8, 2026 20:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant