Skip to content

Host values in BAML#3571

Merged
2kai2kai2 merged 7 commits into
canaryfrom
kai/host-value-resource
May 26, 2026
Merged

Host values in BAML#3571
2kai2kai2 merged 7 commits into
canaryfrom
kai/host-value-resource

Conversation

@2kai2kai2

@2kai2kai2 2kai2kai2 commented May 25, 2026

Copy link
Copy Markdown
Collaborator

The problem: we want to be able to pass lambdas/closures/function pointers into BAML from a host language in a type-safe way and let BAML call it as if it were a normal BAML function. BAML->host handles already exist in the language, allowing the host language to own opaque BAML values. This PR implements the opposite: a way to have opaque handles to host values in BAML.

Lifecycle

  1. A code-genned BAML function is called by the host language, passing in an opaque/non-serializable value (such as a callable/function/closure)
  2. The bridge (on the host side) saves a reference to the value and creates a new host value id. It ensures the host will not deallocate the value until BAML informs it that it no longer has any references.
  3. The bridge passes the newly created host value id across the ffi boundary as an opaque handle.
  4. The BAML side decodes the host value based on type. Currently this is only host callables (BAML function-typed).
  5. (for callables) When a BAML function call occurs on a host callable, it will dispatch back across the host boundary with a new host call operation.
  6. When a host value handle is dropped in BAML (there are no references to it and the GC runs on it), a message is sent to the bridge to notify it that it may release the (real) value.

Summary by CodeRabbit

  • New Features
    • Pass host-language functions as callable parameters and invoke them from BAML.
    • Structured host-callable errors surfaced as a catchable baml.errors.HostCallable with message/class/category/traceback.
    • Cross-platform runtime support (Python, Node.js, Go, WASM) for registering, dispatching and completing host callables.
    • Strict host-return validation against declared types; sync-path encoding fast-fails for host-callable args to avoid main-thread hangs.

Review Change Stack

2kai2kai2 added 5 commits May 24, 2026 17:31
It's an Arc containing a key and some metadata. It represents some external object from the host. When dropped, it notifies the host that it can free the object.
- `HOST_VALUE_CALLABLE` handle type (unlike other handles, this is issued from host->BAML. However it can be passed either direction)
- Host callable error messages
Represents a function-typed host object in the VM. In the type system it looks like just a normal function.
@vercel

vercel Bot commented May 25, 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 May 26, 2026 8:07am
promptfiddle Ready Ready Preview, Comment May 26, 2026 8:07am
promptfiddle2 Error Error May 26, 2026 8:07am

Request Review

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds host-callable values end-to-end: new HostValue/HostClosure types, VM CallIndirect dispatch to a host sys-op, engine conversions and return-type validation, structured HostCallable errors, updated serializers/protos, and C/WASM/Node/Python/Go bridges with extensive tests.

Changes

End-to-end host-callable plumbing

Layer / File(s) Summary
Full host-callable implementation and bridges
baml_language/..., bridge_*/*, sdks/*, sys_*/*, typescript2/...
Defines HostCallable error and HostValue/HostClosure types, routes VM indirect calls through a new sys-op, adds engine conversions and schema-aware return validation, extends protobufs and serializers, and implements C/WASM/Node/Python/Go dispatch/registry with tests.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(200, 230, 255, 0.5)
  participant Client as VM
  participant Engine
  participant SysOps
  end
  rect rgba(220, 255, 220, 0.5)
  participant Bridge as C/WASM/Node/Py/Go
  participant Host
  end

  Client->>Engine: CallIndirect HostClosure(args, ret_ty)
  Engine->>SysOps: io.host.call_host_value(handle,args,ret_ty)
  SysOps->>Bridge: dispatch(key, callId, argsBytes)
  Bridge->>Host: invoke callable(callId, args)
  Host-->>Bridge: complete(callId, result | HostCallableError)
  Bridge-->>SysOps: completeHostCall(callId, isError, content)
  SysOps-->>Engine: value or OpError(HostCallable)
  Engine-->>Client: value or throw baml.errors.HostCallable
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • BoundaryML/baml#3099 — Sys-ops builder/glue for WASM aligns with this PR’s host namespace override via IoSysOpsBuilder.
  • BoundaryML/baml#3169 — Both modify bridge_ctypes handle/protobuf wiring; this PR adds HOST_VALUE_CALLABLE.
  • BoundaryML/baml#3103 — WASM callback wiring and runtime callback surface overlap with this PR’s host_dispatch additions.

Poem

A rabbit taps the wire, ears aglow,
Bridging VM dreams to hosts below.
Keys hop across, calls come and go—
Types stand guard, errors neatly show.
WASM, Node, Python, Go in tow,
One loop completes—then carrots grow. 🥕

✨ Finishing Touches
📝 Generate docstrings
  • ✅ Generated successfully - (🔄 Check to regenerate)
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch kai/host-value-resource

@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: 12

Caution

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

⚠️ Outside diff range comments (1)
baml_language/crates/bridge_wasm/src/handle.rs (1)

108-111: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Skip HANDLE_TABLE.release for HOST_VALUE_CALLABLE in Drop.

Line 110 currently releases every key through HANDLE_TABLE, but HOST_VALUE_CALLABLE keys belong to the host-value registry. This can accidentally release unrelated HANDLE_TABLE entries if keys overlap.

🔧 Proposed fix
 impl Drop for BamlHandle {
     fn drop(&mut self) {
-        HANDLE_TABLE.release(self.key);
+        match self.handle_type {
+            BamlHandleType::HostValueCallable => {
+                // HostValue lifetime is released via host_release_dispatch when
+                // the engine drops the last HostValueArc clone.
+            }
+            _ => HANDLE_TABLE.release(self.key),
+        }
     }
 }
🤖 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_wasm/src/handle.rs` around lines 108 - 111, The
Drop impl for BamlHandle currently always calls HANDLE_TABLE.release(self.key)
which incorrectly releases keys that belong to the host-value registry; change
the Drop implementation to first check the handle kind/variant (the BamlHandle
field that indicates HOST_VALUE_CALLABLE) and skip calling HANDLE_TABLE.release
when the handle is HOST_VALUE_CALLABLE (i.e., return early or match on the
variant and only call HANDLE_TABLE.release for non-HOST_VALUE_CALLABLE cases),
referencing the BamlHandle Drop impl, the HOST_VALUE_CALLABLE constant/variant,
and HANDLE_TABLE.release to locate the code to change.
🧹 Nitpick comments (1)
baml_language/sdks/nodejs/bridge_nodejs/tests/host_callable.test.ts (1)

214-221: ⚡ Quick win

Strengthen the concurrency assertion.

maxActive >= 1 is guaranteed as soon as the first callback starts, so this still passes if all 8 invocations run serially. If this test is meant to prove interleaving, assert >= 2 or rename it to match what it actually checks.

🤖 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/tests/host_callable.test.ts` around
lines 214 - 221, The current concurrency assertion is too weak
(expect(maxActive).toBeGreaterThanOrEqual(1)); update the test to assert actual
interleaving by requiring at least two concurrent callbacks: change the
assertion to expect(maxActive).toBeGreaterThanOrEqual(2). Locate the block
creating promises with callFunction(rt, 'CallCb', ...) and the maxActive
assertion in host_callable.test.ts and replace the >=1 check with >=2 (or
alternatively rename the test if you intend to only assert that callbacks ran).
🤖 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_builtins2/baml_std/baml/ns_errors/errors.baml`:
- Around line 58-63: Add a message field to the HostCallable BAML error struct
and populate it from the host error when translating OpErrorKind::HostCallable
so the host diagnostic is not dropped; specifically, update the HostCallable
definition to include a "message string" (or "message string?"/nullable if
appropriate for existing callers) and update the code that constructs
baml.errors.HostCallable (the translation path handling
OpErrorKind::HostCallable) to pass OpErrorKind::HostCallable.message into that
new field.

In `@baml_language/crates/bex_engine/src/conversion.rs`:
- Around line 822-863: The Type::Class branch currently accepts
BexExternalValue::Map and returns Ok(()), which lets a Map be pushed into the
VM; change the Ty::Class match arm so that BexExternalValue::Map is treated as a
schema mismatch and returns an Err with a clear message (e.g. "expected instance
of `{tn}` but got Map") instead of Ok(()). Locate the Ty::Class arm in
conversion.rs (the method that calls self.validate_host_return_schema) and
replace the BexExternalValue::Map => Ok(()) case with a return Err(...) that
includes the declared class name (tn) and the fact a Map was returned.

In `@baml_language/crates/bex_engine/src/lib.rs`:
- Around line 2348-2377: The code currently returns EngineError::UnhandledThrow
when op_error_to_catchable_throw yields a value, which causes
run_thread_event_loop to exit and makes HostCallable throws uncaught by in-VM
try/catch; instead, change this path to schedule/propagate the throw via the
same VM-reentry mechanism used by the adjacent await handling so the exception
table runs: do not return EngineError::UnhandledThrow from this spot in lib.rs,
but convert the throw_value into the EngineError variant or call the helper used
by the await arm (the same error-construction/path that run_thread_event_loop
uses for await/future errors) so call_host_value's declared throws
(root.errors.HostCallable) are delivered back into the VM and can be caught.

In `@baml_language/crates/bex_external_types/src/host_return.rs`:
- Around line 190-195: The match currently falls through with `_ => true` in
host_return.rs which permissively accepts function-typed return shapes; replace
the catch-all with explicit match arms and add an explicit branch for the
function-shaped type (e.g., `Ty::Function` or the exact variant name
representing function types in the `Ty` enum) that returns `false` to reject
non-callable values at this boundary, keep allowed variants returning `true`,
and add unit tests for function-typed host returns to assert rejection.

In `@baml_language/crates/bex_resource_types/src/host_value.rs`:
- Around line 224-234: The pending release list can contain duplicates causing
the same key to be released multiple times; modify the collection logic around
INTERNER and pending (where to_release is built) to deduplicate keys before
invoking the release callbacks (e.g., collect into a HashSet or use
.fold/.unique) so each u64 key from pending is only released once; apply the
same deduplication change to the other occurrence around lines 253-257 (the
other drain/release path) and ensure subsequent code still iterates the
deduplicated set when calling the host unregister/release callbacks.

In `@baml_language/crates/bex_vm/src/vm.rs`:
- Around line 4081-4151: CallIndirect currently builds a VmExecState::SysOp for
Object::HostClosure (using BamlHostCallHostValue), but the native callback path
(NativeCallResult::YieldToCall -> execute_call_from_locals_offset) still rejects
HostClosure; factor the callable-dispatch into a shared helper (e.g.,
dispatch_callable_or_sysop) or modify execute_call_from_locals_offset to
recognize Object::HostClosure and return the same VmExecState::SysOp payload
(operation bex_vm_types::SysOp::BamlHostCallHostValue with args: callee handle,
args_array pointer, ret_ty pointer) instead of rejecting it, ensuring the same
arg layout and TLAB allocations (args array and ret_ty) and preserving existing
checks (arity, stack drainage) used in the CallIndirect branch.

In `@baml_language/crates/bridge_cffi/src/ffi/host_value.rs`:
- Around line 89-94: The code currently treats a null `content` pointer with a
non-zero `length` as an empty slice; change the branch that builds `bytes` so
that if `content.is_null()` and `length > 0` you explicitly fail the ABI (e.g.,
return an Err / propagate an error or abort) instead of producing `&[]`. Locate
the `bytes: &[u8]` creation that uses `content`, `length`, and
`std::slice::from_raw_parts` and add a guard: allow `&[]` only when `length ==
0`, otherwise detect `content.is_null()` and return an explicit error describing
the invalid null pointer with non-zero length.

In `@baml_language/crates/sys_native/src/host_dispatch.rs`:
- Around line 121-127: The debug_assert allows silent overwrite in release;
change the insert to a runtime-checked insert so we never replace an existing
in-flight entry. For example, replace table.insert(call_id, completion) with a
checked insert that detects a previous value (e.g., if table.insert(call_id,
completion).is_some() { panic!("host-call id {call_id} collided with a live
in-flight entry; the u32 call-id space wrapped while an entry was still pending
(this should be impossible now that cancelled calls are evicted via
InflightGuard)") }), or alternatively use
table.entry(call_id).or_insert_with(...) with explicit handling when Occupied to
panic or return an error; keep the check tied to call_id, completion and
reference the InflightGuard semantics.
- Around line 72-74: The current use of tokio::task::block_in_place in
fire_dispatch risks panics on current_thread runtimes; replace the
block_in_place call in fire_dispatch with tokio::task::spawn_blocking to run the
blocking callback off the async runtime thread (i.e., call
tokio::task::spawn_blocking(move || { f(host_value_key, call_id, args.as_ptr(),
args.len()); }) and handle the returned JoinHandle result appropriately), or if
fire_dispatch is synchronous and cannot await, document/require a multi-threaded
runtime and assert or return an error instead; update the code paths referencing
fire_dispatch to await the spawn_blocking JoinHandle or propagate errors
accordingly.

In `@baml_language/sdks/go/bridge_go/pkg/host_value.go`:
- Around line 161-163: Add a guard in bamlHostDispatch before calling C.GoBytes:
check that the incoming length (C.size_t) does not exceed the max value
representable by C.int (use math.MaxInt32 or equivalent), and if it does return
a HostCallableError instead of performing the conversion; only call
C.GoBytes(unsafe.Pointer(args), C.int(length)) when length is within bounds and
args != nil, ensuring you avoid C.size_t → C.int truncation and potential unsafe
reads.

In `@baml_language/sdks/nodejs/bridge_nodejs/proto.js`:
- Around line 268-275: The code calls result.then(...) for values passing
isPromiseLike which can be arbitrary thenables; change this to use
Promise.resolve(result).then(...) so the standard promise resolution procedure
enforces single-settlement and consistent error handling. Update the block that
currently checks isPromiseLike(result) and replace the direct result.then(...)
invocation with Promise.resolve(result).then(resolved =>
sendHostCallableResult(callId, resolved), err => sendHostCallableError(callId,
err)), keeping the same callId and handler functions.

In `@baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto.ts`:
- Around line 295-309: sendHostCallableResult must be made rollback- and
completion-safe: allocate a local registered array (e.g. const registered:
Array<...> = []) and pass it to setInboundValue({ syncMode: false, registered
}), then if setInboundValue/InboundValue.encode throws, call
sendHostCallableError(callId, err) AND iterate the registered array to
release/unregister those host-callable keys (use your existing
cleanup/unregister helper used elsewhere), and finally ensure
completeHostCall(callId, 0, bytes) is invoked in a finally block (wrap the call
in its own try/catch so any native throw cannot escape the promise). Refer to
sendHostCallableResult, setInboundValue, the local registered array,
sendHostCallableError, and completeHostCall when making the changes.

---

Outside diff comments:
In `@baml_language/crates/bridge_wasm/src/handle.rs`:
- Around line 108-111: The Drop impl for BamlHandle currently always calls
HANDLE_TABLE.release(self.key) which incorrectly releases keys that belong to
the host-value registry; change the Drop implementation to first check the
handle kind/variant (the BamlHandle field that indicates HOST_VALUE_CALLABLE)
and skip calling HANDLE_TABLE.release when the handle is HOST_VALUE_CALLABLE
(i.e., return early or match on the variant and only call HANDLE_TABLE.release
for non-HOST_VALUE_CALLABLE cases), referencing the BamlHandle Drop impl, the
HOST_VALUE_CALLABLE constant/variant, and HANDLE_TABLE.release to locate the
code to change.

---

Nitpick comments:
In `@baml_language/sdks/nodejs/bridge_nodejs/tests/host_callable.test.ts`:
- Around line 214-221: The current concurrency assertion is too weak
(expect(maxActive).toBeGreaterThanOrEqual(1)); update the test to assert actual
interleaving by requiring at least two concurrent callbacks: change the
assertion to expect(maxActive).toBeGreaterThanOrEqual(2). Locate the block
creating promises with callFunction(rt, 'CallCb', ...) and the maxActive
assertion in host_callable.test.ts and replace the >=1 check with >=2 (or
alternatively rename the test if you intend to only assert that callbacks ran).
🪄 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: c95b7e5e-2ffa-45e6-ba12-bac31715416f

📥 Commits

Reviewing files that changed from the base of the PR and between a9a1cae and 7c84e78.

⛔ Files ignored due to path filters (74)
  • baml_language/Cargo.lock is excluded by !**/*.lock
  • baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_package_listing.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__01_lexer__main.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__02_parser__main.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__05_diagnostics.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__10_formatter__main.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_expr_basic/baml_tests__compiles__test_expr_basic__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_expr_basic/baml_tests__compiles__test_expr_basic__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_expr_basic/baml_tests__compiles__test_expr_basic__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_expr_basic/baml_tests__compiles__test_expr_basic__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_expr_name_concat/baml_tests__compiles__test_expr_name_concat__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_expr_name_concat/baml_tests__compiles__test_expr_name_concat__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_expr_name_concat/baml_tests__compiles__test_expr_name_concat__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_expr_name_concat/baml_tests__compiles__test_expr_name_concat__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_expr_throwing_body/baml_tests__compiles__test_expr_throwing_body__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_expr_throwing_body/baml_tests__compiles__test_expr_throwing_body__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_expr_throwing_body/baml_tests__compiles__test_expr_throwing_body__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_expr_throwing_body/baml_tests__compiles__test_expr_throwing_body__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_old_and_new/baml_tests__compiles__test_old_and_new__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_old_and_new/baml_tests__compiles__test_old_and_new__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_old_and_new/baml_tests__compiles__test_old_and_new__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_old_and_new/baml_tests__compiles__test_old_and_new__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_raw_string_name/baml_tests__compiles__test_raw_string_name__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_raw_string_name/baml_tests__compiles__test_raw_string_name__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_raw_string_name/baml_tests__compiles__test_raw_string_name__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_raw_string_name/baml_tests__compiles__test_raw_string_name__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_with_not_keyword/baml_tests__compiles__test_with_not_keyword__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_with_not_keyword/baml_tests__compiles__test_with_not_keyword__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_with_not_keyword/baml_tests__compiles__test_with_not_keyword__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/test_with_not_keyword/baml_tests__compiles__test_with_not_keyword__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/testset_basic/baml_tests__compiles__testset_basic__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/testset_basic/baml_tests__compiles__testset_basic__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/testset_basic/baml_tests__compiles__testset_basic__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/testset_basic/baml_tests__compiles__testset_basic__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/testset_dynamic/baml_tests__compiles__testset_dynamic__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/testset_dynamic/baml_tests__compiles__testset_dynamic__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/testset_dynamic/baml_tests__compiles__testset_dynamic__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/testset_dynamic/baml_tests__compiles__testset_dynamic__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/testset_nested/baml_tests__compiles__testset_nested__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/testset_nested/baml_tests__compiles__testset_nested__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/testset_nested/baml_tests__compiles__testset_nested__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/testset_nested/baml_tests__compiles__testset_nested__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/testset_vibes_nested/baml_tests__compiles__testset_vibes_nested__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/testset_vibes_nested/baml_tests__compiles__testset_vibes_nested__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/testset_vibes_nested/baml_tests__compiles__testset_vibes_nested__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/testset_vibes_nested/baml_tests__compiles__testset_vibes_nested__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/testset_with_setup/baml_tests__compiles__testset_with_setup__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/testset_with_setup/baml_tests__compiles__testset_with_setup__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/testset_with_setup/baml_tests__compiles__testset_with_setup__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/testset_with_setup/baml_tests__compiles__testset_with_setup__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/test_expr_name_type_error/baml_tests__diagnostic_errors__test_expr_name_type_error__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/test_expr_name_type_error/baml_tests__diagnostic_errors__test_expr_name_type_error__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/test_expr_with_runner/baml_tests__diagnostic_errors__test_expr_with_runner__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/test_expr_with_runner/baml_tests__diagnostic_errors__test_expr_with_runner__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/test_expr_wrong_runner/baml_tests__diagnostic_errors__test_expr_wrong_runner__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/test_expr_wrong_runner/baml_tests__diagnostic_errors__test_expr_wrong_runner__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/test_with_runner_ambiguity/baml_tests__diagnostic_errors__test_with_runner_ambiguity__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/test_with_runner_ambiguity/baml_tests__diagnostic_errors__test_with_runner_ambiguity__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/src/compiler2_tir/snapshots/baml_tests__compiler2_tir__phase5__snapshot_baml_package_items.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded_unoptimized.snap is excluded by !**/*.snap
  • baml_language/sdks/nodejs/bridge_nodejs/index.d.ts.map is excluded by !**/*.map
  • baml_language/sdks/nodejs/bridge_nodejs/index.js.map is excluded by !**/*.map
  • baml_language/sdks/nodejs/bridge_nodejs/proto.d.ts.map is excluded by !**/*.map
  • baml_language/sdks/nodejs/bridge_nodejs/proto.js.map is excluded by !**/*.map
  • baml_language/sdks/python/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (76)
  • baml_language/crates/baml_builtins2/baml_std/baml/ns_errors/errors.baml
  • baml_language/crates/baml_builtins2/baml_std/baml/ns_host/host.baml
  • baml_language/crates/baml_builtins2/src/lib.rs
  • baml_language/crates/baml_tests/projects/compiles/host_callable_call/main.baml
  • baml_language/crates/bex_engine/Cargo.toml
  • baml_language/crates/bex_engine/src/conversion.rs
  • baml_language/crates/bex_engine/src/lib.rs
  • baml_language/crates/bex_engine/tests/host_value_callable.rs
  • baml_language/crates/bex_events/src/serialize.rs
  • baml_language/crates/bex_external_types/Cargo.toml
  • baml_language/crates/bex_external_types/src/bex_external_value.rs
  • baml_language/crates/bex_external_types/src/host_return.rs
  • baml_language/crates/bex_external_types/src/lib.rs
  • baml_language/crates/bex_heap/src/accessor.rs
  • baml_language/crates/bex_heap/src/gc.rs
  • baml_language/crates/bex_heap/src/heap_debugger/real.rs
  • baml_language/crates/bex_project/src/lib.rs
  • baml_language/crates/bex_resource_types/Cargo.toml
  • baml_language/crates/bex_resource_types/src/host_value.rs
  • baml_language/crates/bex_resource_types/src/lib.rs
  • baml_language/crates/bex_vm/src/package_baml/json.rs
  • baml_language/crates/bex_vm/src/package_baml/root.rs
  • baml_language/crates/bex_vm/src/package_baml/unstable.rs
  • baml_language/crates/bex_vm/src/vm.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/ffi/host_value.rs
  • baml_language/crates/bridge_cffi/src/ffi/mod.rs
  • baml_language/crates/bridge_cffi/src/lib.rs
  • baml_language/crates/bridge_ctypes/src/handle_table.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/src/host_value.rs
  • baml_language/crates/bridge_wasm/src/lib.rs
  • baml_language/crates/bridge_wasm/tests/host_callable.rs
  • baml_language/crates/bridge_wasm/tests/wasm_fs_glob.rs
  • baml_language/crates/sys_llm/src/build_request/mod.rs
  • baml_language/crates/sys_llm/src/jinja/value_conversion.rs
  • baml_language/crates/sys_native/Cargo.toml
  • baml_language/crates/sys_native/src/host_dispatch.rs
  • baml_language/crates/sys_native/src/host_impls.rs
  • baml_language/crates/sys_native/src/lib.rs
  • baml_language/crates/sys_ops/src/lib.rs
  • baml_language/crates/sys_types/src/lib.rs
  • baml_language/crates/tools_onionskin/src/compiler.rs
  • baml_language/sdk_tests/crates/python_pydantic2/host_callables/customizable/test_main.py
  • baml_language/sdk_tests/fixtures/host_callables/baml_src/main.baml
  • baml_language/sdks/go/bridge_go/cffi/bridge.h
  • baml_language/sdks/go/bridge_go/cffi/lib.go
  • baml_language/sdks/go/bridge_go/cffi/proto/baml_core/cffi/v1/host_callable.go
  • baml_language/sdks/go/bridge_go/pkg/host_value.go
  • baml_language/sdks/go/bridge_go/pkg/host_value_internal_test.go
  • baml_language/sdks/go/bridge_go/pkg/init_callbacks.go
  • baml_language/sdks/go/bridge_go/pkg/proto.go
  • baml_language/sdks/go/bridge_go/pkg/tests/host_callable_test.go
  • baml_language/sdks/nodejs/bridge_nodejs/index.js
  • baml_language/sdks/nodejs/bridge_nodejs/native.d.ts
  • baml_language/sdks/nodejs/bridge_nodejs/native.js
  • baml_language/sdks/nodejs/bridge_nodejs/proto.d.ts
  • baml_language/sdks/nodejs/bridge_nodejs/proto.js
  • baml_language/sdks/nodejs/bridge_nodejs/src/host_value.rs
  • baml_language/sdks/nodejs/bridge_nodejs/src/lib.rs
  • baml_language/sdks/nodejs/bridge_nodejs/tests/host_callable.test.ts
  • baml_language/sdks/nodejs/bridge_nodejs/typescript_src/index.ts
  • baml_language/sdks/nodejs/bridge_nodejs/typescript_src/native.d.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/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

Comment thread baml_language/crates/bex_engine/src/conversion.rs Outdated
Comment thread baml_language/crates/bex_engine/src/lib.rs Outdated
Comment thread baml_language/crates/bex_external_types/src/host_return.rs Outdated
Comment thread baml_language/crates/bex_resource_types/src/host_value.rs
Comment thread baml_language/crates/sys_native/src/host_dispatch.rs Outdated
Comment thread baml_language/crates/sys_native/src/host_dispatch.rs
Comment thread baml_language/sdks/go/bridge_go/pkg/host_value.go
Comment thread baml_language/sdks/nodejs/bridge_nodejs/proto.js
Comment thread baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto.ts
@github-actions

github-actions Bot commented May 25, 2026

Copy link
Copy Markdown

Binary size checks passed

7 passed

Artifact Platform Gzip Baseline Delta Status
bridge_cffi Linux 6.6 MB 6.6 MB -11.9 KB (-0.2%) OK
bridge_cffi-stripped Linux 5.6 MB 5.7 MB -61.8 KB (-1.1%) OK
bridge_cffi macOS 5.4 MB 5.1 MB +335.1 KB (+6.6%) OK
bridge_cffi-stripped macOS 4.6 MB 4.7 MB -46.2 KB (-1.0%) OK
bridge_cffi Windows 5.4 MB 5.1 MB +270.1 KB (+5.3%) OK
bridge_cffi-stripped Windows 4.7 MB 4.7 MB +4.4 KB (+0.1%) OK
bridge_wasm WASM 3.6 MB 3.5 MB +52.1 KB (+1.5%) OK

Generated by cargo size-gate · workflow run

@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: 1

♻️ Duplicate comments (1)
baml_language/sdks/go/bridge_go/pkg/host_value.go (1)

161-163: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard length before C.GoBytes to prevent truncation across FFI.

On Line 162, C.size_t is cast to C.int without a max-bound check. Large length values can truncate and cause incorrect/unsafe reads.

💡 Suggested fix
 	var argsBytes []byte
 	if length > 0 && args != nil {
+		if length > C.size_t(math.MaxInt32) {
+			sendHostCallableError(id, "DecodeError",
+				fmt.Sprintf("host-call args buffer too large: %d bytes", uint64(length)),
+				pb.HostCallableErrorCategory_HOST_CALLABLE_INVALID_ARGUMENT)
+			return
+		}
 		argsBytes = C.GoBytes(unsafe.Pointer(args), C.int(length))
 	}
🤖 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/go/bridge_go/pkg/host_value.go` around lines 161 - 163,
The code calls C.GoBytes(unsafe.Pointer(args), C.int(length)) and casts C.size_t
length to C.int which can truncate large values; update the guard in the
host_value.go snippet that sets argsBytes so you validate length before casting:
check that args != nil and length > 0 and that length is <= the maximum
representable value of C.int (e.g., INT_MAX) and handle the overflow case
(return an error, cap safely, or avoid calling C.GoBytes) so you never pass a
truncated C.int to C.GoBytes; reference the variables/functions argsBytes,
length, args and the use of C.GoBytes to locate and fix the code.
🧹 Nitpick comments (1)
baml_language/crates/sys_ops/src/lib.rs (1)

1583-1610: ⚡ Quick win

Add a unit test for with_host_instance dispatch wiring.

This new namespace override path should have a focused unit test in this module to ensure baml_host_call_host_value is routed through the injected IoNamespaceHost implementation.

As per coding guidelines "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/sys_ops/src/lib.rs` around lines 1583 - 1610, Add a
focused unit test that verifies with_host_instance wires the host dispatch:
implement a small test-only struct that implements io::IoNamespaceHost and
records when __glue_baml_host_call_host_value is invoked (e.g., via an
AtomicBool or channel), call
SysOps::builder().with_host_instance(Arc::new(your_impl)) to produce the
builder/instance, then invoke the stored closure
self.inner.baml_host_call_host_value (or obtain the SysOps and call the
equivalent dispatch entry) with dummy heap/permit/args/ctx/call_id and assert
your recorder was triggered; reference with_host_instance,
baml_host_call_host_value, io::IoNamespaceHost, and
__glue_baml_host_call_host_value to locate where to wire the test.
🤖 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/sdks/go/bridge_go/pkg/host_value.go`:
- Around line 304-317: The code in runHostCallable currently treats a two-value
return as an error by inspecting out[1] at runtime; update the case that handles
two returns to first validate the function signature by checking
fnType.Out(1).Implements(errorType) before casting out[1] to error, and if that
check fails sendHostCallableError(callID, "TypeError" or appropriate class,
"host callable second return must implement error",
pb.HostCallableErrorCategory_HOST_CALLABLE_HOST_ERROR, withTraceback("invalid
host callable signature")) and return; otherwise proceed to cast out[1] to error
and handle as before (references: runHostCallable, fnType, out, errorType,
sendHostCallableError, callID, withTraceback).

---

Duplicate comments:
In `@baml_language/sdks/go/bridge_go/pkg/host_value.go`:
- Around line 161-163: The code calls C.GoBytes(unsafe.Pointer(args),
C.int(length)) and casts C.size_t length to C.int which can truncate large
values; update the guard in the host_value.go snippet that sets argsBytes so you
validate length before casting: check that args != nil and length > 0 and that
length is <= the maximum representable value of C.int (e.g., INT_MAX) and handle
the overflow case (return an error, cap safely, or avoid calling C.GoBytes) so
you never pass a truncated C.int to C.GoBytes; reference the variables/functions
argsBytes, length, args and the use of C.GoBytes to locate and fix the code.

---

Nitpick comments:
In `@baml_language/crates/sys_ops/src/lib.rs`:
- Around line 1583-1610: Add a focused unit test that verifies
with_host_instance wires the host dispatch: implement a small test-only struct
that implements io::IoNamespaceHost and records when
__glue_baml_host_call_host_value is invoked (e.g., via an AtomicBool or
channel), call SysOps::builder().with_host_instance(Arc::new(your_impl)) to
produce the builder/instance, then invoke the stored closure
self.inner.baml_host_call_host_value (or obtain the SysOps and call the
equivalent dispatch entry) with dummy heap/permit/args/ctx/call_id and assert
your recorder was triggered; reference with_host_instance,
baml_host_call_host_value, io::IoNamespaceHost, and
__glue_baml_host_call_host_value to locate where to wire the test.
🪄 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: 9b2d2800-1105-47eb-9a73-d61ae5fcf33a

📥 Commits

Reviewing files that changed from the base of the PR and between 7c84e78 and ea19ff6.

⛔ Files ignored due to path filters (13)
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__01_lexer__main.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__02_parser__main.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__05_diagnostics.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__10_formatter__main.snap is excluded by !**/*.snap
  • baml_language/sdks/nodejs/bridge_nodejs/index.d.ts.map is excluded by !**/*.map
  • baml_language/sdks/nodejs/bridge_nodejs/index.js.map is excluded by !**/*.map
  • baml_language/sdks/nodejs/bridge_nodejs/proto.d.ts.map is excluded by !**/*.map
  • baml_language/sdks/nodejs/bridge_nodejs/proto.js.map is excluded by !**/*.map
  • baml_language/sdks/python/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (44)
  • baml_language/crates/baml_tests/projects/compiles/host_callable_call/main.baml
  • baml_language/crates/bex_engine/src/conversion.rs
  • baml_language/crates/bex_engine/src/lib.rs
  • baml_language/crates/bex_engine/tests/host_value_callable.rs
  • baml_language/crates/bex_external_types/src/host_return.rs
  • baml_language/crates/bex_external_types/src/lib.rs
  • baml_language/crates/bex_project/src/lib.rs
  • baml_language/crates/bex_resource_types/src/host_value.rs
  • baml_language/crates/bex_vm/src/vm.rs
  • baml_language/crates/bridge_ctypes/src/value_decode.rs
  • baml_language/crates/bridge_wasm/src/host_value.rs
  • baml_language/crates/bridge_wasm/src/lib.rs
  • baml_language/crates/bridge_wasm/tests/host_callable.rs
  • baml_language/crates/bridge_wasm/tests/wasm_fs_glob.rs
  • baml_language/crates/sys_native/src/host_dispatch.rs
  • baml_language/crates/sys_native/src/host_impls.rs
  • baml_language/crates/sys_ops/src/lib.rs
  • baml_language/sdk_tests/crates/python_pydantic2/host_callables/customizable/test_main.py
  • baml_language/sdk_tests/fixtures/host_callables/baml_src/main.baml
  • baml_language/sdks/go/bridge_go/cffi/lib.go
  • baml_language/sdks/go/bridge_go/cffi/proto/baml_core/cffi/v1/host_callable.go
  • baml_language/sdks/go/bridge_go/pkg/host_value.go
  • baml_language/sdks/go/bridge_go/pkg/host_value_internal_test.go
  • baml_language/sdks/go/bridge_go/pkg/init_callbacks.go
  • baml_language/sdks/go/bridge_go/pkg/proto.go
  • baml_language/sdks/go/bridge_go/pkg/tests/host_callable_test.go
  • baml_language/sdks/nodejs/bridge_nodejs/Cargo.toml
  • baml_language/sdks/nodejs/bridge_nodejs/index.js
  • baml_language/sdks/nodejs/bridge_nodejs/native.d.ts
  • baml_language/sdks/nodejs/bridge_nodejs/native.js
  • baml_language/sdks/nodejs/bridge_nodejs/proto.d.ts
  • baml_language/sdks/nodejs/bridge_nodejs/proto.js
  • baml_language/sdks/nodejs/bridge_nodejs/src/host_value.rs
  • baml_language/sdks/nodejs/bridge_nodejs/src/lib.rs
  • baml_language/sdks/nodejs/bridge_nodejs/tests/host_callable.test.ts
  • baml_language/sdks/nodejs/bridge_nodejs/typescript_src/index.ts
  • baml_language/sdks/nodejs/bridge_nodejs/typescript_src/native.d.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/src/baml_core/proto.py
  • baml_language/sdks/python/tests/test_host_callable.py
✅ Files skipped from review due to trivial changes (5)
  • baml_language/sdks/nodejs/bridge_nodejs/native.d.ts
  • baml_language/sdks/nodejs/bridge_nodejs/native.js
  • baml_language/sdks/nodejs/bridge_nodejs/proto.js
  • 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

Comment thread baml_language/sdks/go/bridge_go/pkg/host_value.go Outdated
@codspeed-hq

codspeed-hq Bot commented May 25, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 12.44%

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 2 regressed benchmarks
✅ 17 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
WallTime vm_string_concat_5k 24.1 ms 27.8 ms -13.58%
WallTime vm_class_create_50k 11.2 ms 12.6 ms -11.28%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing kai/host-value-resource (4128115) with canary (a9a1cae)

Open in CodSpeed

@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: 1

🧹 Nitpick comments (6)
baml_language/crates/sys_ops/src/lib.rs (1)

1583-1610: ⚡ Quick win

Add unit coverage for host builder wiring.

with_host_instance / with_host add a new dispatch path; please add a unit test in this module that verifies baml_host_call_host_value is actually overridden and forwards calls as expected.

As per coding guidelines "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/sys_ops/src/lib.rs` around lines 1583 - 1610, Add a unit
test in this module that constructs the same builder type that exposes
with_host_instance/with_host, supplies a test implementation of
io::IoNamespaceHost (implementing __glue_baml_host_call_host_value and Default
or wrapped in Arc) which records that it was called (e.g. via an
Arc<Mutex<...>>), calls the builder.with_host_instance(...) (or
with_host::<YourTestImpl>()), then invokes the stored closure
inner.baml_host_call_host_value (tests in-module can access private inner) or
exercises the public path that triggers it, and assert the recorded
arguments/side-effects to prove the inner.baml_host_call_host_value field was
overridden and forwards calls to your test impl. Ensure the test lives under
#[cfg(test)] and uses the exact symbols with_host_instance, with_host,
inner.baml_host_call_host_value, and
io::IoNamespaceHost::__glue_baml_host_call_host_value.
baml_language/sdks/go/bridge_go/pkg/tests/host_callable_test.go (1)

389-396: 💤 Low value

Use strings.Contains instead of manual implementation.

The contains helper reimplements strings.Contains from the standard library. Using the stdlib function is more idiomatic and avoids maintaining custom code.

♻️ Proposed fix
+import (
+	"strings"
+	// ... other imports
+)

-func contains(haystack, needle string) bool {
-	for i := 0; i+len(needle) <= len(haystack); i++ {
-		if haystack[i:i+len(needle)] == needle {
-			return true
-		}
-	}
-	return false
-}
+func contains(haystack, needle string) bool {
+	return strings.Contains(haystack, needle)
+}

Or simply replace contains(...) calls with strings.Contains(...) directly.

🤖 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/go/bridge_go/pkg/tests/host_callable_test.go` around lines
389 - 396, The custom contains(haystack, needle string) function reimplements
standard library behavior—replace usages of contains(...) with
strings.Contains(...) and remove the contains function; import the "strings"
package where needed and ensure any tests referencing contains now call
strings.Contains to use the stdlib implementation.
baml_language/sdks/go/bridge_go/pkg/proto.go (1)

130-154: 💤 Low value

Consider guarding against nil func registration.

The reflective fallback registers any value with Kind() == reflect.Func, including typed nil funcs (e.g., var f func() = nil). Invoking a nil func will panic at dispatch time. If this is intentional (letting the dispatch path surface a clear error), this is fine; otherwise, adding an !rv.IsNil() check would reject nil funcs early with a clearer error message.

♻️ Optional: reject nil funcs early
 		rv := reflect.ValueOf(v)
 		if rv.IsValid() && rv.Kind() == reflect.Func {
+			if rv.IsNil() {
+				return nil, fmt.Errorf("nil func cannot be used as a host callable")
+			}
 			key := registerHostValue(rv)
🤖 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/go/bridge_go/pkg/proto.go` around lines 130 - 154, The
code currently registers any reflect.Func, including typed nil funcs
(rv.Kind()==reflect.Func) which will panic at dispatch; modify the guard in the
reflective fallback to also check that the func value is non-nil (e.g., require
rv.IsValid() && rv.Kind()==reflect.Func && !rv.IsNil()) before calling
registerHostValue(rv), and if the func is nil return a clear error (instead of
registering) so callers see an immediate "nil func" / unsupported value error;
update the branch that appends to registered and returns the pb.InboundValue
accordingly, leaving registerHostValue and the pb.InboundValue construction
unchanged.
baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto.ts (1)

296-311: 💤 Low value

Minor: Result-encode path lacks rollback for nested callables.

The registered: [] array passed to setInboundValue is created inline and discarded if encoding fails. If a host callable returns a value containing another callable and encoding fails after registering that inner callable, the key leaks (the engine never sees it to release).

This is a narrow edge case (callable returning callable, then encode failure), but for completeness the fix would be to hoist ctx and iterate ctx.registered in the catch block, similar to encodeCallArgs.

♻️ Optional fix for completeness
 function sendHostCallableResult(callId: number, value: unknown): void {
-    let bytes: Buffer;
+    const ctx: EncodeCtx = { syncMode: false, registered: [] };
     try {
         const iv: baml_core.cffi.v1.IInboundValue = {};
-        // Result-encode path (host → engine): no sync guard (we're already
-        // on libuv) and no rollback tracking — the result is handed to the
-        // engine, which releases any callables it decodes.
-        setInboundValue(iv, value, { syncMode: false, registered: [] });
+        setInboundValue(iv, value, ctx);
         const msg = InboundValue.create(iv);
-        bytes = Buffer.from(InboundValue.encode(msg).finish());
+        const bytes = Buffer.from(InboundValue.encode(msg).finish());
+        completeHostCall(callId, 0, bytes);
     } catch (err) {
+        for (const key of ctx.registered) {
+            try { releaseHostCallable(key); } catch { /* best-effort */ }
+        }
         sendHostCallableError(callId, err);
-        return;
     }
-    completeHostCall(callId, 0, bytes);
 }
🤖 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
296 - 311, sendHostCallableResult creates its registered array inline so if
setInboundValue registers nested callables then encoding fails those keys are
leaked; hoist the inbound context (e.g., const ctx:
baml_core.cffi.v1.IInboundValue = { registered: [] }) and pass ctx to
setInboundValue, and in the catch block iterate ctx.registered to
release/unregister any registered callables (matching the pattern used in
encodeCallArgs) before calling sendHostCallableError; update references to use
ctx (or iv) consistently and ensure completeHostCall is only called on success.
baml_language/sdks/nodejs/bridge_nodejs/src/host_value.rs (1)

96-116: 💤 Low value

Inconsistent mutex poisoning handling.

register_host_callable uses .unwrap() on the mutex lock (line 114), which will panic if a previous thread panicked while holding the lock. However, drop_registry_entry (line 147) and host_dispatch_callback (line 211) both handle the poisoned case gracefully. For consistency and robustness, consider handling the poisoned case here as well.

Proposed fix
 pub fn register_host_callable(callable: Function<'_, DispatchArgs, ()>) -> napi::Result<HandleKey> {
     let tsfn: DispatchTsfn = callable
         .build_threadsafe_function()
         .callee_handled::<false>()
         .weak::<false>()
         .build()?;
     let key = next_key();
-    REGISTRY.table.lock().unwrap().insert(key, Arc::new(tsfn));
+    match REGISTRY.table.lock() {
+        Ok(mut t) => { t.insert(key, Arc::new(tsfn)); }
+        Err(poisoned) => { poisoned.into_inner().insert(key, Arc::new(tsfn)); }
+    }
     Ok(HandleKey::from_u64(key))
 }
🤖 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/src/host_value.rs` around lines 96 -
116, The mutex lock in register_host_callable currently uses .unwrap() on
REGISTRY.table.lock(), which can panic on a poisoned mutex; change it to handle
the poisoned case like the other functions (drop_registry_entry and
host_dispatch_callback) by matching the Result from lock() and calling
into_inner() on the PoisonError (or using unwrap_or_else(|e| e.into_inner())) to
recover the inner guard, then insert the Arc<DispatchTsfn> as before; keep
references to next_key(), REGISTRY.table, and HandleKey usage intact.
baml_language/sdks/python/rust/bridge_python/src/host_value.rs (1)

71-122: 💤 Low value

Inconsistent mutex poisoning handling.

register_host_callable uses .unwrap() on the mutex lock (line 79), which will panic if a previous thread panicked while holding the lock. However, drop_registry_entry (line 91) handles the poisoned case gracefully by returning early. For consistency, consider handling the poisoned case here as well.

Proposed fix
 pub fn register_host_callable(callable: Py<PyAny>) -> u64 {
     let key = next_key();
-    REGISTRY.table.lock().unwrap().insert(key, callable);
+    match REGISTRY.table.lock() {
+        Ok(mut t) => { t.insert(key, callable); }
+        Err(poisoned) => { poisoned.into_inner().insert(key, callable); }
+    }
     key
 }
🤖 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/python/rust/bridge_python/src/host_value.rs` around lines
71 - 122, The register_host_callable function currently uses
REGISTRY.table.lock().unwrap() which will panic on a poisoned mutex; make it
handle the poisoned case like drop_registry_entry does by replacing the unwrap
with a match on REGISTRY.table.lock() and if Err(_) return the generated key
without inserting (or otherwise no-op) to avoid panics; specifically update
register_host_callable to use match REGISTRY.table.lock() { Ok(mut t) =>
t.insert(key, callable), Err(_) => return key } so behavior is consistent with
drop_registry_entry.
🤖 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/bex_vm/src/vm.rs`:
- Around line 1802-1815: The host-closure branch currently skips runtime arity
checks and drains the stack immediately; change it to validate arg_count against
the HostClosure's expected arity before touching the stack and return
Err(InvalidArgumentCount) on mismatch. Concretely, when matching
self.get_object(callee_ptr) to Object::HostClosure(hc) extract the closure's
arity from hc (or its field), compare it to arg_count, and only if they match
call self.host_closure_call_sysop(callee_ptr, user_args) after draining via
self.stack.drain(locals_offset..); otherwise return the InvalidArgumentCount
error without draining.

---

Nitpick comments:
In `@baml_language/crates/sys_ops/src/lib.rs`:
- Around line 1583-1610: Add a unit test in this module that constructs the same
builder type that exposes with_host_instance/with_host, supplies a test
implementation of io::IoNamespaceHost (implementing
__glue_baml_host_call_host_value and Default or wrapped in Arc) which records
that it was called (e.g. via an Arc<Mutex<...>>), calls the
builder.with_host_instance(...) (or with_host::<YourTestImpl>()), then invokes
the stored closure inner.baml_host_call_host_value (tests in-module can access
private inner) or exercises the public path that triggers it, and assert the
recorded arguments/side-effects to prove the inner.baml_host_call_host_value
field was overridden and forwards calls to your test impl. Ensure the test lives
under #[cfg(test)] and uses the exact symbols with_host_instance, with_host,
inner.baml_host_call_host_value, and
io::IoNamespaceHost::__glue_baml_host_call_host_value.

In `@baml_language/sdks/go/bridge_go/pkg/proto.go`:
- Around line 130-154: The code currently registers any reflect.Func, including
typed nil funcs (rv.Kind()==reflect.Func) which will panic at dispatch; modify
the guard in the reflective fallback to also check that the func value is
non-nil (e.g., require rv.IsValid() && rv.Kind()==reflect.Func && !rv.IsNil())
before calling registerHostValue(rv), and if the func is nil return a clear
error (instead of registering) so callers see an immediate "nil func" /
unsupported value error; update the branch that appends to registered and
returns the pb.InboundValue accordingly, leaving registerHostValue and the
pb.InboundValue construction unchanged.

In `@baml_language/sdks/go/bridge_go/pkg/tests/host_callable_test.go`:
- Around line 389-396: The custom contains(haystack, needle string) function
reimplements standard library behavior—replace usages of contains(...) with
strings.Contains(...) and remove the contains function; import the "strings"
package where needed and ensure any tests referencing contains now call
strings.Contains to use the stdlib implementation.

In `@baml_language/sdks/nodejs/bridge_nodejs/src/host_value.rs`:
- Around line 96-116: The mutex lock in register_host_callable currently uses
.unwrap() on REGISTRY.table.lock(), which can panic on a poisoned mutex; change
it to handle the poisoned case like the other functions (drop_registry_entry and
host_dispatch_callback) by matching the Result from lock() and calling
into_inner() on the PoisonError (or using unwrap_or_else(|e| e.into_inner())) to
recover the inner guard, then insert the Arc<DispatchTsfn> as before; keep
references to next_key(), REGISTRY.table, and HandleKey usage intact.

In `@baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto.ts`:
- Around line 296-311: sendHostCallableResult creates its registered array
inline so if setInboundValue registers nested callables then encoding fails
those keys are leaked; hoist the inbound context (e.g., const ctx:
baml_core.cffi.v1.IInboundValue = { registered: [] }) and pass ctx to
setInboundValue, and in the catch block iterate ctx.registered to
release/unregister any registered callables (matching the pattern used in
encodeCallArgs) before calling sendHostCallableError; update references to use
ctx (or iv) consistently and ensure completeHostCall is only called on success.

In `@baml_language/sdks/python/rust/bridge_python/src/host_value.rs`:
- Around line 71-122: The register_host_callable function currently uses
REGISTRY.table.lock().unwrap() which will panic on a poisoned mutex; make it
handle the poisoned case like drop_registry_entry does by replacing the unwrap
with a match on REGISTRY.table.lock() and if Err(_) return the generated key
without inserting (or otherwise no-op) to avoid panics; specifically update
register_host_callable to use match REGISTRY.table.lock() { Ok(mut t) =>
t.insert(key, callable), Err(_) => return key } so behavior is consistent with
drop_registry_entry.
🪄 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: 4257afa5-7d53-48d8-b024-4d8bbdfcae22

📥 Commits

Reviewing files that changed from the base of the PR and between ea19ff6 and 93e63b7.

⛔ Files ignored due to path filters (19)
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__01_lexer__main.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__02_parser__main.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__05_diagnostics.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__10_formatter__main.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded_unoptimized.snap is excluded by !**/*.snap
  • baml_language/sdks/nodejs/bridge_nodejs/index.d.ts.map is excluded by !**/*.map
  • baml_language/sdks/nodejs/bridge_nodejs/index.js.map is excluded by !**/*.map
  • baml_language/sdks/nodejs/bridge_nodejs/proto.d.ts.map is excluded by !**/*.map
  • baml_language/sdks/nodejs/bridge_nodejs/proto.js.map is excluded by !**/*.map
  • baml_language/sdks/python/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (47)
  • baml_language/crates/baml_builtins2/baml_std/baml/ns_errors/errors.baml
  • baml_language/crates/baml_tests/projects/compiles/host_callable_call/main.baml
  • baml_language/crates/bex_engine/src/conversion.rs
  • baml_language/crates/bex_engine/src/lib.rs
  • baml_language/crates/bex_engine/tests/host_value_callable.rs
  • baml_language/crates/bex_external_types/src/host_return.rs
  • baml_language/crates/bex_external_types/src/lib.rs
  • baml_language/crates/bex_project/src/lib.rs
  • baml_language/crates/bex_resource_types/src/host_value.rs
  • baml_language/crates/bex_vm/src/vm.rs
  • baml_language/crates/bridge_cffi/src/ffi/host_value.rs
  • baml_language/crates/bridge_ctypes/src/value_decode.rs
  • baml_language/crates/bridge_wasm/src/host_value.rs
  • baml_language/crates/bridge_wasm/src/lib.rs
  • baml_language/crates/bridge_wasm/tests/host_callable.rs
  • baml_language/crates/bridge_wasm/tests/wasm_fs_glob.rs
  • baml_language/crates/sys_native/src/host_dispatch.rs
  • baml_language/crates/sys_native/src/host_impls.rs
  • baml_language/crates/sys_ops/src/lib.rs
  • baml_language/sdk_tests/crates/python_pydantic2/host_callables/customizable/test_main.py
  • baml_language/sdk_tests/fixtures/host_callables/baml_src/main.baml
  • baml_language/sdks/go/bridge_go/cffi/lib.go
  • baml_language/sdks/go/bridge_go/cffi/proto/baml_core/cffi/v1/host_callable.go
  • baml_language/sdks/go/bridge_go/pkg/host_value.go
  • baml_language/sdks/go/bridge_go/pkg/host_value_internal_test.go
  • baml_language/sdks/go/bridge_go/pkg/init_callbacks.go
  • baml_language/sdks/go/bridge_go/pkg/proto.go
  • baml_language/sdks/go/bridge_go/pkg/tests/host_callable_test.go
  • baml_language/sdks/nodejs/bridge_nodejs/Cargo.toml
  • baml_language/sdks/nodejs/bridge_nodejs/index.js
  • baml_language/sdks/nodejs/bridge_nodejs/native.d.ts
  • baml_language/sdks/nodejs/bridge_nodejs/native.js
  • baml_language/sdks/nodejs/bridge_nodejs/proto.d.ts
  • baml_language/sdks/nodejs/bridge_nodejs/proto.js
  • baml_language/sdks/nodejs/bridge_nodejs/src/host_value.rs
  • baml_language/sdks/nodejs/bridge_nodejs/src/lib.rs
  • baml_language/sdks/nodejs/bridge_nodejs/tests/host_callable.test.ts
  • baml_language/sdks/nodejs/bridge_nodejs/typescript_src/index.ts
  • baml_language/sdks/nodejs/bridge_nodejs/typescript_src/native.d.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/src/baml_core/proto.py
  • baml_language/sdks/python/tests/test_host_callable.py
  • typescript2/app-vscode-webview/src/bridge_wasm.test.ts
✅ Files skipped from review due to trivial changes (6)
  • baml_language/sdks/nodejs/bridge_nodejs/Cargo.toml
  • baml_language/sdks/nodejs/bridge_nodejs/native.js
  • baml_language/crates/baml_tests/projects/compiles/host_callable_call/main.baml
  • baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto/baml_cffi.d.ts
  • baml_language/sdks/nodejs/bridge_nodejs/proto.js
  • baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto/baml_cffi.js

Comment thread baml_language/crates/bex_vm/src/vm.rs

@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: 8

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/lib.rs (1)

2303-2345: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the declared ret_ty when materializing successful host-call results.

validate_host_return_schema can now accept BexExternalValue::HostValue for Ty::Function, but Line 2345 still goes through convert_external_to_vm_value(...), which passes expected_ty = None. A host callable declared as returning another callable will therefore pass validation and then fail with CannotConvert("host_value (no declared function type in context)").

Suggested fix
-                            let value = self.convert_external_to_vm_value(&mut thread, external)?;
+                            let declared_ret_ty =
+                                (operation == SysOp::BamlHostCallHostValue)
+                                    .then(|| host_call_return_ty(args.get(2).copied()))
+                                    .flatten();
+                            let value = self.convert_external_to_vm_value_with_ty(
+                                &mut thread,
+                                external,
+                                declared_ret_ty.as_ref(),
+                            )?;
                             thread.vm.stack.push(value);
🤖 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 2303 - 2345, The
code validates host-call returns with validate_host_return_schema using the
declared return type (ret_ty) but then calls convert_external_to_vm_value with
expected_ty = None, causing host-returned HostValue for function types to fail
materialization; update the SysOp::BamlHostCallHostValue path so that when
host_call_return_ty(args.get(2).copied()) yields Some(ret_ty) you pass that
ret_ty (or a clone/owned copy) as the expected_ty argument into
convert_external_to_vm_value (i.e., call convert_external_to_vm_value(&mut
thread, external, Some(ret_ty)) or the equivalent API) so
HostValue/BexExternalValue::HostValue is materialized according to the declared
Ty::Function; keep existing error/throw handling intact.
baml_language/crates/bridge_wasm/src/lib.rs (1)

162-173: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make host_dispatch optional in the WASM callback bundle

WasmCallbacks’ TypeScript type marks host_dispatch as required, and BamlWasmRuntime::create() unconditionally calls callbacks.host_dispatch() and always injects WasmHost via with_host_instance(...). That’s a breaking change for callers that never use host callables (the crate’s own usage example omits host_dispatch). Keep host_dispatch optional and only construct/inject the host path when it’s present; otherwise leave the host namespace unsupported. If the wasm-bindgen structural getter can’t return Option<Function>, probe the JS object with Reflect::get / is_undefined before wiring .with_host_instance(...).

🤖 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_wasm/src/lib.rs` around lines 162 - 173, The TS
type for WasmCallbacks currently requires host_dispatch and
BamlWasmRuntime::create unconditionally calls callbacks.host_dispatch() and uses
with_host_instance(WasmHost) which breaks callers that omit host_dispatch;
update the wiring so host_dispatch is optional: make the wasm-bindgen binding or
runtime probe the JS callbacks object for host_dispatch (e.g., via Reflect::get
/ is_undefined if the getter can't return Option) and only call
callbacks.host_dispatch() and call .with_host_instance(WasmHost) when
host_dispatch is present; otherwise skip injecting the host (leave the host
namespace unsupported) so WasmCallbacks and BamlWasmRuntime::create handle
absent host_dispatch safely.
♻️ Duplicate comments (2)
baml_language/crates/bex_vm/src/vm.rs (1)

1802-1815: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep HostClosure arity validation in the indirect callback path.

Line 1812 still dispatches Object::HostClosure without checking arg_count against the closure's declared arity, so a YieldToCall producer can forward the wrong number of args in release builds instead of failing with InvalidArgumentCount.

Suggested fix
-        if matches!(self.get_object(callee_ptr), Object::HostClosure(_)) {
-            let user_args: Vec<Value> = self.stack.drain(locals_offset..).collect();
-            return Ok(Some(self.host_closure_call_sysop(callee_ptr, user_args)));
+        if let Object::HostClosure(hc) = self.get_object(callee_ptr) {
+            if arg_count != hc.arity {
+                return Err(VmInternalError::InvalidArgumentCount {
+                    expected: hc.arity,
+                    got: arg_count,
+                }
+                .into());
+            }
+            let user_args: Vec<Value> = self.stack.drain(locals_offset..).collect();
+            return Ok(Some(self.host_closure_call_sysop(callee_ptr, user_args)));
         }
🤖 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/src/vm.rs` around lines 1802 - 1815, The indirect
host-callback path currently calls HostClosure without checking arg_count, so
validate the requested arg_count against the HostClosure's declared arity before
draining the stack or yielding: match self.get_object(callee_ptr) to extract the
HostClosure (e.g., HostClosure(closure)), compare closure.arity (or
closure.arity()) with the local arg_count and if they differ return the
InvalidArgumentCount error; only if they match drain the stack from
locals_offset into user_args and call self.host_closure_call_sysop(callee_ptr,
user_args) as before.
baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto.ts (1)

296-310: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

sendHostCallableResult is still not rollback/completion-safe.

This still has the two problems called out earlier: result encoding can register nested host callables and then leak them on a later encode failure, and completeHostCall(callId, 0, bytes) still sits outside the try, so a native throw on the promise-fulfillment path can escape the .then(...) handler and leave the call uncompleted.

Suggested fix
 function sendHostCallableResult(callId: number, value: unknown): void {
-    let bytes: Buffer;
+    const ctx: EncodeCtx = { syncMode: false, registered: [] };
     try {
         const iv: baml_core.cffi.v1.IInboundValue = {};
-        setInboundValue(iv, value, { syncMode: false, registered: [] });
+        setInboundValue(iv, value, ctx);
         const msg = InboundValue.create(iv);
-        bytes = Buffer.from(InboundValue.encode(msg).finish());
+        const bytes = Buffer.from(InboundValue.encode(msg).finish());
+        completeHostCall(callId, 0, bytes);
     } catch (err) {
+        for (const key of ctx.registered) {
+            try {
+                releaseHostCallable(key);
+            } catch {
+                // Best-effort cleanup.
+            }
+        }
         sendHostCallableError(callId, err);
-        return;
     }
-    completeHostCall(callId, 0, bytes);
 }
🤖 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
296 - 310, The sendHostCallableResult function can leak nested host callables
and may leave the call uncompleted if encoding or completion throws; to fix,
create and pass a local registered array to setInboundValue (e.g., const
registered: number[] = []; setInboundValue(iv, value, { syncMode: false,
registered })), wrap the entire encode+complete sequence in a single try/catch
so both InboundValue.encode and completeHostCall(callId, 0, bytes) are inside
the try, and on any catch call sendHostCallableError(callId, err) and then roll
back by unregistering any ids recorded in registered (use the existing
host-unregister helper used elsewhere). Ensure no native throw can escape the
handler and that registered callables are cleaned up on any error.
🧹 Nitpick comments (4)
baml_language/crates/bridge_ctypes/src/value_decode.rs (1)

44-50: ⚡ Quick win

Assert Arc identity in the regression test.

The important behavior change here is shared identity/refcounting, but the test only checks key and kind. Re-decoding the same handle twice and asserting pointer equality would catch a regression back to constructing fresh HostValueArcs.

As per coding guidelines, "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/bridge_ctypes/src/value_decode.rs` around lines 44 - 50,
Add an assertion in the unit test that re-decodes the same handle and verifies
the two HostValueArc instances are the same Arc identity: after calling
bex_project::HostValueArc::intern (or whatever helper decodes the wire key)
twice for the same wire key/handle, use Arc::ptr_eq to assert the returned Arcs
share the same pointer (and optionally check strong_count increased) so the test
fails if fresh HostValueArc instances are constructed instead of interned ones.
baml_language/crates/bridge_wasm/tests/host_callable.rs (1)

408-422: ⚡ Quick win

Make the error-path test assert the surfaced host error.

result.is_err() will also pass for unrelated runtime/setup failures, so this test doesn't actually pin the HostCallableError mapping added in this PR. Assert that the thrown JsValue includes test boom or RuntimeError.

🤖 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_wasm/tests/host_callable.rs` around lines 408 -
422, Update the test host_callable_error_surfaces_as_baml_error to assert the
actual surfaced host error instead of only checking result.is_err(): after
calling call_function(...).await, unwrap the Err (e.g. let err =
result.unwrap_err()), convert the error/js value to a string (or format!("{:?}",
err)) and assert that the message contains either "test boom" or "RuntimeError"
so the failure specifically verifies the HostCallableError mapping added by
make_dispatch_error / register_host_callable.
baml_language/crates/bridge_cffi/src/ffi/host_value.rs (1)

89-108: ⚡ Quick win

Add a unit test for the null-pointer ABI violation path.

This guard is the only thing preventing (content = NULL, length > 0) from regressing back to the old “treat it as empty” behavior, but the current tests never hit that branch. A small unit test here would lock in the fix.

As per coding guidelines, "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/bridge_cffi/src/ffi/host_value.rs` around lines 89 -
108, Add a unit test in the same module (host_value.rs) that exercises the
null-pointer ABI guard: call the function that contains this code path (the
complete_host_call / host-value handling function shown) with content =
std::ptr::null() and length > 0 and a test call_id, then assert that
host_dispatch::complete_with_error was invoked with an OpError whose op is
SysOp::BamlHostCallHostValue and whose message contains "null content pointer"
(or the formatted "null content pointer with length {length}") to lock in the
guard behavior and prevent regression.
baml_language/sdks/nodejs/bridge_nodejs/Cargo.toml (1)

11-16: 🏗️ Heavy lift

Preserve a Rust-unit-testable path for this crate.

Disabling both test and doctest removes Rust-level checks for newly added registry/dispatch logic and pushes all validation to external Jest tests. Consider adding a testable Rust target path (e.g., an rlib/split testable module) so cargo test --lib remains meaningful for Rust changes in this crate.

As per coding guidelines "**/*.rs: Prefer writing Rust unit tests over integration tests where possible" and "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/sdks/nodejs/bridge_nodejs/Cargo.toml` around lines 11 - 16, The
crate currently disables Rust-level tests by setting "test = false" and "doctest
= false" in Cargo.toml which prevents running cargo test --lib; restore a
Rust-testable path by either removing those flags or declaring a proper library
target so unit tests can run: edit Cargo.toml to remove or scope the "test" and
"doctest" false flags and/or add a [lib] section (crate-type/path) so the crate
builds as an rlib and supports #[cfg(test)] unit tests in your Rust modules
(e.g., add a [lib] with path = "src/lib.rs" or re-enable doctests) ensuring
cargo test --lib exercises the new registry/dispatch logic.
🤖 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/bex_engine/src/conversion.rs`:
- Around line 796-915: The validator currently allows
BexExternalValue::HostValue to appear inside container and class-field positions
(in validate_host_return_schema), which later fails in
convert_external_to_vm_value; update the Ty::List, Ty::Map and Ty::Class
branches in validate_host_return_schema to explicitly reject HostValue when the
expected nested type is a function (match inner / v_ty / class_field.field_type
against Ty::Function) and return a clear Err for nested host callables;
reference BexExternalValue::HostValue, Ty::Function,
validate_host_return_schema, and convert_external_to_vm_value so the failure is
surfaced here until nested typed conversion is implemented.

In `@baml_language/crates/bridge_wasm/src/host_value.rs`:
- Around line 158-183: The insert_in_flight function currently swallows
collisions by completing the incoming CompletionHandle but returns () so callers
(e.g., call_host_value) still proceed to dispatch the host call; change
insert_in_flight to return a boolean or Result indicating whether the insert
succeeded (e.g., Ok(true) or Ok(()), Err/false on collision), update its
signature and all call sites (including the other occurrence around the 417-455
region) to check the return and immediately return/abort when a collision is
reported, ensuring host_dispatch / call3 is NOT invoked after insert_in_flight
signals failure; keep the existing error-completion behavior inside
insert_in_flight and only change control flow via the new return value.

In `@baml_language/crates/sys_native/src/host_dispatch.rs`:
- Around line 129-154: The insert function currently swallows the collision by
completing the provided CompletionHandle and returning (), so callers like
call_host_value cannot stop before fire_dispatch; change insert(call_id: u32,
completion: CompletionHandle) to return a Result<(), OpError> (or a bool/Result
indicating success) and on collision keep completing the new CompletionHandle
with the OpError::new(SysOp::BamlHostCallHostValue, ...) but then return
Err(...) instead of returning unit; update callers (e.g. call_host_value) to
check the returned Result and bail out (skip firing the host callback) when
insert signals a collision so a rejected call cannot proceed to fire_dispatch.
Ensure symbols referenced: insert, call_host_value, CompletionHandle, TABLE,
OpError, SysOp::BamlHostCallHostValue, InflightGuard.

In `@baml_language/sdks/go/bridge_go/pkg/proto.go`:
- Around line 141-152: The current block treats typed nil funcs as valid and
registers them; change the encode-time validation so that when rv.IsValid() &&
rv.Kind() == reflect.Func you first check rv.IsNil() and return a clear
validation error instead of calling registerHostValue and returning a
pb.InboundValue handle; ensure registerHostValue is not invoked for nil function
values (the symbols to update are the rv.IsValid()/rv.Kind() branch, the
rv.IsNil() check, and the registerHostValue call that produces the
pb.InboundValue/BamlHandle).

In `@baml_language/sdks/nodejs/bridge_nodejs/src/host_value.rs`:
- Around line 107-113: The threadsafe function created in register_host_callable
(the tsfn assigned from callable.build_threadsafe_function()) currently has no
max_queue_size set so its default is unbounded; change the build call chain to
set a sensible max_queue_size (e.g., 256 or 1024) on the ThreadsafeFunction
builder before .build() so the DispatchTsfn created by
build_threadsafe_function() cannot accumulate an unlimited queue while using the
NonBlocking dispatcher; keep the rest of the builder options intact and ensure
the chosen limit prevents unbounded memory growth.

In `@baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto.ts`:
- Around line 68-89: The branch that handles value instanceof BamlHandle must
also reject HOST_VALUE_CALLABLE on the sync path: inside that branch (where you
set iv.handle = { key: value.key, handleType: value.handleType }), check
ctx.syncMode and if value.handleType === BamlHandleType.HOST_VALUE_CALLABLE
throw the same HostCallableSyncError used for raw functions; this ensures
callable handles are fast-failed on callFunctionSync just like registered JS
callables.

In `@baml_language/sdks/python/rust/bridge_python/src/host_value.rs`:
- Around line 305-315: encode_result_inbound currently calls
proto._set_inbound_value without the rollback accumulator used in
encode_call_args, so any callbacks registered during encoding may leak when
encoding fails; fix by mirroring encode_call_args: create a Py list (e.g.
pyo3::types::PyList::empty(py)) named registered, set it into kwargs_dict with
key "registered" before calling _set_inbound_value, and if any step returns Err
(before successfully returning the bytes) iterate the registered list and
unregister/release each entry using the same cleanup logic used by
encode_call_args, then return the error.

In `@baml_language/sdks/python/tests/test_host_callable.py`:
- Around line 243-255: The test test_encode_success_does_not_release leaves a
real host-value key registered because encode_call_args registers a callable but
the encoded bytes are not sent and release_host_callable is monkeypatched to
only record releases; to fix, either monkeypatch/stub
_proto.register_host_callable to return a fake key (so no real registration
occurs) or call _proto.register_host_callable to capture the real key from
encode_call_args's output and then invoke the real _proto.release_host_callable
in teardown to remove it; reference encode_call_args, register_host_callable and
release_host_callable when making the change.

---

Outside diff comments:
In `@baml_language/crates/bex_engine/src/lib.rs`:
- Around line 2303-2345: The code validates host-call returns with
validate_host_return_schema using the declared return type (ret_ty) but then
calls convert_external_to_vm_value with expected_ty = None, causing
host-returned HostValue for function types to fail materialization; update the
SysOp::BamlHostCallHostValue path so that when
host_call_return_ty(args.get(2).copied()) yields Some(ret_ty) you pass that
ret_ty (or a clone/owned copy) as the expected_ty argument into
convert_external_to_vm_value (i.e., call convert_external_to_vm_value(&mut
thread, external, Some(ret_ty)) or the equivalent API) so
HostValue/BexExternalValue::HostValue is materialized according to the declared
Ty::Function; keep existing error/throw handling intact.

In `@baml_language/crates/bridge_wasm/src/lib.rs`:
- Around line 162-173: The TS type for WasmCallbacks currently requires
host_dispatch and BamlWasmRuntime::create unconditionally calls
callbacks.host_dispatch() and uses with_host_instance(WasmHost) which breaks
callers that omit host_dispatch; update the wiring so host_dispatch is optional:
make the wasm-bindgen binding or runtime probe the JS callbacks object for
host_dispatch (e.g., via Reflect::get / is_undefined if the getter can't return
Option) and only call callbacks.host_dispatch() and call
.with_host_instance(WasmHost) when host_dispatch is present; otherwise skip
injecting the host (leave the host namespace unsupported) so WasmCallbacks and
BamlWasmRuntime::create handle absent host_dispatch safely.

---

Duplicate comments:
In `@baml_language/crates/bex_vm/src/vm.rs`:
- Around line 1802-1815: The indirect host-callback path currently calls
HostClosure without checking arg_count, so validate the requested arg_count
against the HostClosure's declared arity before draining the stack or yielding:
match self.get_object(callee_ptr) to extract the HostClosure (e.g.,
HostClosure(closure)), compare closure.arity (or closure.arity()) with the local
arg_count and if they differ return the InvalidArgumentCount error; only if they
match drain the stack from locals_offset into user_args and call
self.host_closure_call_sysop(callee_ptr, user_args) as before.

In `@baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto.ts`:
- Around line 296-310: The sendHostCallableResult function can leak nested host
callables and may leave the call uncompleted if encoding or completion throws;
to fix, create and pass a local registered array to setInboundValue (e.g., const
registered: number[] = []; setInboundValue(iv, value, { syncMode: false,
registered })), wrap the entire encode+complete sequence in a single try/catch
so both InboundValue.encode and completeHostCall(callId, 0, bytes) are inside
the try, and on any catch call sendHostCallableError(callId, err) and then roll
back by unregistering any ids recorded in registered (use the existing
host-unregister helper used elsewhere). Ensure no native throw can escape the
handler and that registered callables are cleaned up on any error.

---

Nitpick comments:
In `@baml_language/crates/bridge_cffi/src/ffi/host_value.rs`:
- Around line 89-108: Add a unit test in the same module (host_value.rs) that
exercises the null-pointer ABI guard: call the function that contains this code
path (the complete_host_call / host-value handling function shown) with content
= std::ptr::null() and length > 0 and a test call_id, then assert that
host_dispatch::complete_with_error was invoked with an OpError whose op is
SysOp::BamlHostCallHostValue and whose message contains "null content pointer"
(or the formatted "null content pointer with length {length}") to lock in the
guard behavior and prevent regression.

In `@baml_language/crates/bridge_ctypes/src/value_decode.rs`:
- Around line 44-50: Add an assertion in the unit test that re-decodes the same
handle and verifies the two HostValueArc instances are the same Arc identity:
after calling bex_project::HostValueArc::intern (or whatever helper decodes the
wire key) twice for the same wire key/handle, use Arc::ptr_eq to assert the
returned Arcs share the same pointer (and optionally check strong_count
increased) so the test fails if fresh HostValueArc instances are constructed
instead of interned ones.

In `@baml_language/crates/bridge_wasm/tests/host_callable.rs`:
- Around line 408-422: Update the test
host_callable_error_surfaces_as_baml_error to assert the actual surfaced host
error instead of only checking result.is_err(): after calling
call_function(...).await, unwrap the Err (e.g. let err = result.unwrap_err()),
convert the error/js value to a string (or format!("{:?}", err)) and assert that
the message contains either "test boom" or "RuntimeError" so the failure
specifically verifies the HostCallableError mapping added by make_dispatch_error
/ register_host_callable.

In `@baml_language/sdks/nodejs/bridge_nodejs/Cargo.toml`:
- Around line 11-16: The crate currently disables Rust-level tests by setting
"test = false" and "doctest = false" in Cargo.toml which prevents running cargo
test --lib; restore a Rust-testable path by either removing those flags or
declaring a proper library target so unit tests can run: edit Cargo.toml to
remove or scope the "test" and "doctest" false flags and/or add a [lib] section
(crate-type/path) so the crate builds as an rlib and supports #[cfg(test)] unit
tests in your Rust modules (e.g., add a [lib] with path = "src/lib.rs" or
re-enable doctests) ensuring cargo test --lib exercises the new
registry/dispatch logic.
🪄 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: 09b8e402-d636-40b4-b078-2d5d843590d9

📥 Commits

Reviewing files that changed from the base of the PR and between 93e63b7 and e16ed99.

⛔ Files ignored due to path filters (21)
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__01_lexer__main.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__02_parser__main.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__05_diagnostics.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__10_formatter__main.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded_unoptimized.snap is excluded by !**/*.snap
  • baml_language/sdks/go/bridge_go/cffi/proto/baml_core/cffi/v1/baml_inbound.pb.go is excluded by !**/*.pb.go
  • baml_language/sdks/go/bridge_go/cffi/proto/baml_core/cffi/v1/baml_outbound.pb.go is excluded by !**/*.pb.go
  • baml_language/sdks/nodejs/bridge_nodejs/index.d.ts.map is excluded by !**/*.map
  • baml_language/sdks/nodejs/bridge_nodejs/index.js.map is excluded by !**/*.map
  • baml_language/sdks/nodejs/bridge_nodejs/proto.d.ts.map is excluded by !**/*.map
  • baml_language/sdks/nodejs/bridge_nodejs/proto.js.map is excluded by !**/*.map
  • baml_language/sdks/python/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (46)
  • baml_language/crates/baml_builtins2/baml_std/baml/ns_errors/errors.baml
  • baml_language/crates/baml_tests/projects/compiles/host_callable_call/main.baml
  • baml_language/crates/bex_engine/src/conversion.rs
  • baml_language/crates/bex_engine/src/lib.rs
  • baml_language/crates/bex_engine/tests/host_value_callable.rs
  • baml_language/crates/bex_external_types/src/host_return.rs
  • baml_language/crates/bex_external_types/src/lib.rs
  • baml_language/crates/bex_project/src/lib.rs
  • baml_language/crates/bex_resource_types/src/host_value.rs
  • baml_language/crates/bex_vm/src/vm.rs
  • baml_language/crates/bridge_cffi/src/ffi/host_value.rs
  • baml_language/crates/bridge_ctypes/src/value_decode.rs
  • baml_language/crates/bridge_wasm/src/host_value.rs
  • baml_language/crates/bridge_wasm/src/lib.rs
  • baml_language/crates/bridge_wasm/tests/host_callable.rs
  • baml_language/crates/bridge_wasm/tests/wasm_fs_glob.rs
  • baml_language/crates/sys_native/src/host_dispatch.rs
  • baml_language/crates/sys_native/src/host_impls.rs
  • baml_language/crates/sys_ops/src/lib.rs
  • baml_language/sdk_tests/crates/python_pydantic2/host_callables/customizable/test_main.py
  • baml_language/sdk_tests/fixtures/host_callables/baml_src/main.baml
  • baml_language/sdks/go/bridge_go/cffi/lib.go
  • baml_language/sdks/go/bridge_go/pkg/host_value.go
  • baml_language/sdks/go/bridge_go/pkg/host_value_internal_test.go
  • baml_language/sdks/go/bridge_go/pkg/init_callbacks.go
  • baml_language/sdks/go/bridge_go/pkg/proto.go
  • baml_language/sdks/go/bridge_go/pkg/tests/host_callable_test.go
  • baml_language/sdks/nodejs/bridge_nodejs/Cargo.toml
  • baml_language/sdks/nodejs/bridge_nodejs/index.js
  • baml_language/sdks/nodejs/bridge_nodejs/native.d.ts
  • baml_language/sdks/nodejs/bridge_nodejs/native.js
  • baml_language/sdks/nodejs/bridge_nodejs/proto.d.ts
  • baml_language/sdks/nodejs/bridge_nodejs/proto.js
  • baml_language/sdks/nodejs/bridge_nodejs/src/host_value.rs
  • baml_language/sdks/nodejs/bridge_nodejs/src/lib.rs
  • baml_language/sdks/nodejs/bridge_nodejs/tests/host_callable.test.ts
  • baml_language/sdks/nodejs/bridge_nodejs/typescript_src/index.ts
  • baml_language/sdks/nodejs/bridge_nodejs/typescript_src/native.d.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/src/baml_core/proto.py
  • baml_language/sdks/python/tests/test_host_callable.py
  • typescript2/app-vscode-webview/src/bridge_wasm.test.ts
✅ Files skipped from review due to trivial changes (4)
  • baml_language/crates/bridge_wasm/tests/wasm_fs_glob.rs
  • baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto/baml_cffi.d.ts
  • baml_language/sdks/nodejs/bridge_nodejs/proto.js
  • baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto/baml_cffi.js

Comment thread baml_language/crates/bex_engine/src/conversion.rs
Comment thread baml_language/crates/bridge_wasm/src/host_value.rs Outdated
Comment thread baml_language/crates/sys_native/src/host_dispatch.rs Outdated
Comment thread baml_language/sdks/go/bridge_go/pkg/proto.go
Comment thread baml_language/sdks/nodejs/bridge_nodejs/src/host_value.rs
Comment thread baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto.ts
Comment thread baml_language/sdks/python/rust/bridge_python/src/host_value.rs Outdated
Comment thread baml_language/sdks/python/tests/test_host_callable.py
and other LLM-found fixes

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
baml_language/sdks/go/bridge_go/pkg/host_value.go (1)

356-380: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Rollback nested host-value registrations if success completion panics.

goToInboundValueTracking can register host callables inside resultValue. If completeHostCallSuccess panics, the outer recover switches to the error path, but registered is never released because the engine never decoded the success payload.

Suggested fix
 	resultBytes, err := proto.Marshal(iv)
 	if err != nil {
 		rollbackRegisteredHostValues(registered)
 		sendHostCallableError(callID, "EncodeError",
 			fmt.Sprintf("failed to marshal host-call result: %v", err),
 			pb.HostCallableErrorCategory_HOST_CALLABLE_HOST_ERROR)
 		return
 	}
-	completeHostCallSuccess(callID, resultBytes)
+	func() {
+		defer func() {
+			if r := recover(); r != nil {
+				rollbackRegisteredHostValues(registered)
+				panic(r)
+			}
+		}()
+		completeHostCallSuccess(callID, resultBytes)
+	}()
 }
🤖 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/go/bridge_go/pkg/host_value.go` around lines 356 - 380,
The code registers host-callables in `registered` via `goToInboundValueTracking`
but never ensures those registrations are rolled back if
`completeHostCallSuccess` panics; add a defer immediately after
`goToInboundValueTracking` that checks if `registered` is non-nil and calls
`rollbackRegisteredHostValues(registered)` (and leaves `sendHostCallableError`
behavior unchanged), and after the successful `completeHostCallSuccess(callID,
resultBytes)` set `registered = nil` (or otherwise mark it cleared) so the defer
does not roll back on normal success; this ensures registrations are cleaned up
on panic while not undoing a real success.
♻️ Duplicate comments (1)
baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto.ts (1)

310-336: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep completeHostCall inside the defended success path.

sendHostCallableResult still calls completeHostCall(callId, 0, bytes) after the try/catch. If that native call throws in the promise-fulfillment branch, the rejection bypasses the last-resort completion path, and any keys recorded in ctx.registered stay leaked because cleanup only runs in catch.

Suggested fix
 function sendHostCallableResult(callId: number, value: unknown): void {
-    let bytes: Buffer;
     // Result-encode path (host → engine): no sync guard (we're already on
     // libuv). We do track registrations, though — a callable nested in the
     // result is registered before encoding finishes, and if encoding then
     // throws, the bytes never reach the engine, so it never decodes (and
@@
     try {
         const iv: baml_core.cffi.v1.IInboundValue = {};
         setInboundValue(iv, value, ctx);
         const msg = InboundValue.create(iv);
-        bytes = Buffer.from(InboundValue.encode(msg).finish());
+        const bytes = Buffer.from(InboundValue.encode(msg).finish());
+        completeHostCall(callId, 0, bytes);
     } catch (err) {
         for (const k of ctx.registered) {
             try {
                 releaseHostCallable(k);
             } catch {
@@
         }
         sendHostCallableError(callId, err);
-        return;
     }
-    completeHostCall(callId, 0, bytes);
 }
🤖 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
310 - 336, sendHostCallableResult currently calls completeHostCall(callId, 0,
bytes) after the try/catch, so if completeHostCall throws the registered
callables in ctx.registered leak; move the success-path native call inside the
protected block or add a guarded finally: perform completeHostCall(callId, 0,
bytes) inside the try after bytes are created and if completeHostCall throws run
the same rollback loop over ctx.registered (calling releaseHostCallable) and
call sendHostCallableError(callId, err) to report the failure; ensure
ctx.registered cleanup is executed on any error and only call completeHostCall
on a successful, fully protected path.
🤖 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/bex_engine/src/lib.rs`:
- Around line 1836-1863: The helper currently returns
Err(EngineError::from(op_err)) when op_error_to_catchable_throw(...) is None,
but it must still settle a spawned child's future if vm_thread_settles_future()
is set; change the None branch to check thread.vm_thread_settles_future() and,
if Some(future_id), call the appropriate settling helper (e.g.,
settle_child_fatal(thread, future_id, EngineError::from(op_err)).await? or the
existing async settling API) and return Ok(ThreadOutcome::SettledChild); only
return Err(EngineError::from(op_err)) when no future_id is present. Use the
existing symbols vm_thread_settles_future, settle_child_fatal (or
settle_child_errored if that API expects a value) and
ThreadOutcome::SettledChild to locate and implement the fix.

---

Outside diff comments:
In `@baml_language/sdks/go/bridge_go/pkg/host_value.go`:
- Around line 356-380: The code registers host-callables in `registered` via
`goToInboundValueTracking` but never ensures those registrations are rolled back
if `completeHostCallSuccess` panics; add a defer immediately after
`goToInboundValueTracking` that checks if `registered` is non-nil and calls
`rollbackRegisteredHostValues(registered)` (and leaves `sendHostCallableError`
behavior unchanged), and after the successful `completeHostCallSuccess(callID,
resultBytes)` set `registered = nil` (or otherwise mark it cleared) so the defer
does not roll back on normal success; this ensures registrations are cleaned up
on panic while not undoing a real success.

---

Duplicate comments:
In `@baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto.ts`:
- Around line 310-336: sendHostCallableResult currently calls
completeHostCall(callId, 0, bytes) after the try/catch, so if completeHostCall
throws the registered callables in ctx.registered leak; move the success-path
native call inside the protected block or add a guarded finally: perform
completeHostCall(callId, 0, bytes) inside the try after bytes are created and if
completeHostCall throws run the same rollback loop over ctx.registered (calling
releaseHostCallable) and call sendHostCallableError(callId, err) to report the
failure; ensure ctx.registered cleanup is executed on any error and only call
completeHostCall on a successful, fully protected path.
🪄 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: 19a64e1e-5f84-45dc-b0b5-09597ad79edf

📥 Commits

Reviewing files that changed from the base of the PR and between e16ed99 and 4128115.

⛔ Files ignored due to path filters (2)
  • baml_language/sdks/nodejs/bridge_nodejs/proto.d.ts.map is excluded by !**/*.map
  • baml_language/sdks/nodejs/bridge_nodejs/proto.js.map is excluded by !**/*.map
📒 Files selected for processing (24)
  • baml_language/crates/bex_engine/src/conversion.rs
  • baml_language/crates/bex_engine/src/lib.rs
  • baml_language/crates/bex_engine/tests/host_value_callable.rs
  • baml_language/crates/bridge_cffi/src/ffi/host_value.rs
  • baml_language/crates/bridge_wasm/src/host_value.rs
  • baml_language/crates/bridge_wasm/src/lib.rs
  • baml_language/crates/sys_native/src/host_dispatch.rs
  • baml_language/crates/sys_native/src/host_impls.rs
  • baml_language/crates/sys_ops/src/lib.rs
  • baml_language/sdks/go/bridge_go/cffi/lib.go
  • baml_language/sdks/go/bridge_go/pkg/host_value.go
  • baml_language/sdks/go/bridge_go/pkg/host_value_internal_test.go
  • baml_language/sdks/go/bridge_go/pkg/proto.go
  • baml_language/sdks/go/bridge_go/pkg/tests/host_callable_test.go
  • baml_language/sdks/nodejs/bridge_nodejs/package.json
  • baml_language/sdks/nodejs/bridge_nodejs/proto.js
  • baml_language/sdks/nodejs/bridge_nodejs/src/host_value.rs
  • baml_language/sdks/nodejs/bridge_nodejs/tests/host_callable.test.ts
  • baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto.ts
  • baml_language/sdks/python/rust/bridge_python/src/baml_core/baml_py/__init__.pyi
  • baml_language/sdks/python/rust/bridge_python/src/host_value.rs
  • baml_language/sdks/python/src/baml_core/baml_py.pyi
  • baml_language/sdks/python/src/baml_core/proto.py
  • baml_language/sdks/python/tests/test_host_callable.py
✅ Files skipped from review due to trivial changes (2)
  • baml_language/sdks/python/src/baml_core/baml_py.pyi
  • baml_language/sdks/python/rust/bridge_python/src/baml_core/baml_py/init.pyi

Comment thread baml_language/crates/bex_engine/src/lib.rs
@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

Note

Docstrings generation - IN PROGRESS

Generating docstrings for this pull request

@2kai2kai2 2kai2kai2 added this pull request to the merge queue May 26, 2026
Merged via the queue into canary with commit a74b24a May 26, 2026
48 of 51 checks passed
@2kai2kai2 2kai2kai2 deleted the kai/host-value-resource branch May 26, 2026 17:31
@codeshaunted codeshaunted mentioned this pull request May 27, 2026
4 tasks
meefs pushed a commit to meefs/baml that referenced this pull request May 27, 2026
## Summary

Refutable pattern binding in condition position:

```baml
if let User { name } = u {
  name
} else {
  "no match"
}
```

* New `Expr::IfLet` AST node — distinct from `Expr::If`, no parse-time
desugaring (cleaner diagnostics + LSP).
* TIR: `infer_if_let_expr` / `check_if_let_expr` reuse match's pattern
infrastructure (`analyze_and_lower`, `finalize_pattern_lowering`,
exhaustiveness matrix) for binding registration and refutability
detection.
* MIR: `lower_if_let` via `lower_pattern_test` + `bind_pattern` (no
desugaring).
* `else if let` chaining handled by the existing else-branch dispatch.
* Scrutinee narrowing in then-branch (matched type) and else-branch
(residual) — same shape as `<expr> is <pat>`, plus pattern bindings.
* New warning `E0112 IrrefutablePatternInIfLet` when the matrix shows
the pattern covers every value — `else` would be dead.
* Formatter (`baml_fmt`) handles the new node with `IfLetExpr` +
`ElseExpr::IfLet`.

### Drive-by fix: `is`-destructure ambiguity

`if x is Class { ... }` used to greedily consume the then-block as a
destructure pattern body, breaking common code like `else if r is Empty
{ ... }`. Mirrored Rust's `NO_STRUCT_LITERAL` restriction — destructure
patterns are suppressed in `if`/`while` condition position, with parens
as an escape hatch (`if (x is Class { f })`).

## Test plan

24 new tests:

- [x] 4 parser unit tests (CST shape, ambiguity regression, parens
escape hatch)
- [x] 11 LSP fixture tests covering: basic narrowing & binding scope,
destructuring, irrefutable-pattern warning, `else if`/`else if let`
chaining, binding-doesn't-leak (3 cases), or-patterns, nested shadowing,
composite scrutinee, optional scrutinee, throws-analysis flow
- [x] 6 formatter tests (basic, no-else, chain, mixed plain/let,
destructure, or-pattern)
- [x] 11 end-to-end runtime tests in `bex_engine` (then/else dispatch,
destructure binding at runtime, or-pattern alternatives, nested
shadowing, composite scrutinee, optional scrutinee some/null, statement
form, chain dispatch + fall-through)

Full workspace test run: **5710 / 5714 pass**. The 4 failures are
pre-existing `sdk_test_python_pydantic2 *::pytest` cases blocked by a
stale precompiled `baml_py.abi3.so` missing the `register_host_callable`
symbol added by BoundaryML#3571 — unrelated to this PR.

## Follow-ups (not in this PR)

- `while let` — natural counterpart; same infrastructure would extend.
- Promote scrutinee narrowing through `&&` / `\|\|` chains for `if let`?
(Rust doesn't; we don't either today.)
- Quick-fix code action for the irrefutable warning (convert to plain
`let`).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added `if let` conditional pattern matching with scoped then-branch
bindings, type narrowing, destructuring and or-pattern support, plus
`else if let` / mixed `else if` chains.
* Formatter and editor tooling now recognize and format `if let`;
completions treat `if let` as a value-position expression.
* **Diagnostics**
* New warning for irrefutable `if let` patterns (E0112) when the else
branch is unreachable.
* **Tests**
* Extensive unit/integration tests and fixtures covering scoping,
chaining, throws, destructuring, nullable scrutinees, and formatting.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/BoundaryML/baml/pull/3579?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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