Skip to content

Support invoking returned BAML closures across SDKs - #4220

Merged
sxlijin merged 1 commit into
canaryfrom
agent/bridge-closures
Jul 28, 2026
Merged

sxlijin merged 1 commit into
canaryfrom
agent/bridge-closures

Conversation

@sxlijin

@sxlijin sxlijin commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • return BAML functions, closures, bound methods, and generic functions as typed heap handles
  • add reusable handle-based dispatch to the core runtime, append it to the C ABI, and expose equivalent WASM and JNI entry points
  • decode returned function handles into native callables in Python, TypeScript (Node, browser, and Workers), Go, C++, Swift, C#, and Java, with an explicit BamlFunction::call surface in Rust
  • preserve closure identity, ownership, and mutable captured state across repeated calls
  • add the same canonical argument, structured-return, and mutable-capture tests to every supported SDK bridge
  • add .NET, CMake, and Ninja to mise so every SDK test harness can run locally

Root cause

Callable VM objects could not be converted into outbound bridge values. Existing FUNCTION_REF handles also decoded as inert low-level handles, and bridge runtimes only exposed dispatch by named BAML function rather than by engine-owned callable handle.

User impact

Users can invoke a closure returned by BAML with the native callable shape of their SDK. Arguments follow each SDK's existing generated-function conventions, results and errors use the normal bridge encoding and decoding paths, returned closures remain reusable, and mutable captures retain state between invocations.

Design

Callable VM objects are rooted in the existing BAML handle table and returned as typed FUNCTION_REF rows. Invocation borrows rather than drains the row, resolves named arguments through the callable's own metadata, and runs through the same runtime result encoder as named calls. BamlApiV1 gains an append-only call_handle field; WASM and JNI expose equivalent dispatch.

The ADR is recorded locally at /Users/sam/thoughts/sam-projects/bridge-closures/0001-host-invoked-baml-closures.md and is grounded in the Python and TypeScript calling conventions documented by bridge-ref.

Validation

  • workspace cargo fmt and Clippy with all targets and features
  • deterministic C ABI header generation and Rust/C/C++ ABI layout tests
  • SDK parity lint: 29 additional declarations with no new required gaps
  • all six SDK generator unit suites
  • canonical function_calls closure tests in Python, Node TypeScript, browser TypeScript, Workers TypeScript, Go, C++, Rust, Swift, C#, and Java
  • Java bridge gradle check
  • C# bridge and closure fixture builds plus focused formatting checks

Summary by CodeRabbit

  • New Features
    • Added support for invoking returned BAML closures and function references across C++, C#, Go, Java, Python, Rust, Swift, and TypeScript.
    • Added positional and named argument support, including optional and default parameters.
    • Added callable return-type support in generated SDK bindings.
    • Added handle-based invocation to native, WebAssembly, and CFFI bridges.
  • Bug Fixes
    • Improved validation and structured errors for missing, unexpected, or incorrectly ordered arguments.
  • Tests
    • Expanded cross-language coverage for callable arguments, structured returns, reusable closures, and retained mutable state.

@vercel

vercel Bot commented Jul 27, 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 Jul 28, 2026 12:38am
promptfiddle Ready Ready Preview, Comment Jul 28, 2026 12:38am
promptfiddle2 Ready Ready Preview, Comment Jul 28, 2026 12:38am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR enables BAML function and closure values to round-trip as owned function handles. It adds engine-side named invocation, a call_handle CFFI ABI entry, native and Wasm bridge support, generated callable types across SDKs, and cross-language tests for arguments, structured returns, and stateful closures.

Changes

Callable handle execution

Layer / File(s) Summary
Engine callable handles and argument normalization
baml_language/crates/bex_engine/..., baml_language/crates/bex_project/..., baml_language/crates/bridge_ctypes/...
Callable VM objects become tagged function handles, and invocation supports named arguments, defaults, arity checks, and structured errors.
CFFI handle dispatch and ABI surface
baml_language/crates/bridge_cffi/..., release/...
The V1 CFFI table gains call_handle; native and Wasm entry points resolve handles and return encoded results with ABI assertions.
Native SDK handle invocation
baml_language/sdks/{cpp,csharp,go,java,python,rust,swift,typescript}/...
SDK bridges add handle dispatch, ownership, cancellation, result decoding, and callable runtime APIs.
Generated callable types and return decoding
baml_language/sdks/*/sdkgen_*, baml_language/sdks/rust/bridge_rust/...
SDK generators emit callable return types and decode returned function references into host-language callables.
Cross-language closure validation
baml_language/sdk_tests/...
Fixtures and tests cover callable arguments, structured results, reusable closures, and retained mutable captures.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HostSDK
  participant NativeBridge
  participant CFFI
  participant BexEngine
  HostSDK->>NativeBridge: encode callable arguments
  NativeBridge->>CFFI: call_handle(handle key, payload)
  CFFI->>BexEngine: resolve and invoke callable
  BexEngine-->>CFFI: return encoded result
  CFFI-->>NativeBridge: outbound result envelope
  NativeBridge-->>HostSDK: decode callable result
Loading

Possibly related PRs

Suggested reviewers: hellovai

Poem

A rabbit found a function handle bright,
And hopped through bridges day and night.
Named args lined up, defaults tucked away,
Closures remembered what captures say.
“Call me again!” the counter cheered—
Across each SDK, the path appeared.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 79.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and clearly reflects the main change: enabling invocation of returned BAML closures across SDKs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/bridge-closures

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

❤️ Share

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

@github-actions

Copy link
Copy Markdown

⏭️ Performance benchmarks were skipped

Perf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to canary/main.

To run them on this PR, do any of the following, then push a commit (or re-run CI):

  • Add RUN_CODSPEED=1 to the PR description, or
  • Include run-perf or /perf in the PR title or any commit message.

@vercel
vercel Bot temporarily deployed to Preview – beps July 27, 2026 19:26 Inactive
@sxlijin
sxlijin force-pushed the agent/bridge-closures branch 2 times, most recently from 18a30b8 to 571ae4f Compare July 27, 2026 19:32
@vercel
vercel Bot temporarily deployed to Preview – beps July 27, 2026 19:34 Inactive
@sxlijin
sxlijin force-pushed the agent/bridge-closures branch from 571ae4f to bb04b85 Compare July 27, 2026 19:39
@vercel
vercel Bot temporarily deployed to Preview – beps July 27, 2026 19:41 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 July 27, 2026 19:48 Inactive
@github-actions

github-actions Bot commented Jul 27, 2026 •

Copy link
Copy Markdown

Binary size checks passed

✅ 7 passed

Artifact Platform File Gzip Gated on Baseline Delta Status
✅ baml-cli Linux 🔒 27.3 MB 11.6 MB file 27.3 MB +72.5 KB (+0.3%) OK
✅ packed-program Linux 🔒 17.7 MB 7.3 MB file 17.7 MB +47.5 KB (+0.3%) OK
✅ baml-cli macOS 🔒 21.3 MB 10.2 MB file 21.2 MB +66.2 KB (+0.3%) OK
✅ packed-program macOS 🔒 13.9 MB 6.4 MB file 13.8 MB +33.1 KB (+0.2%) OK
✅ baml-cli Windows 🔒 22.9 MB 10.4 MB file 22.8 MB +78.8 KB (+0.3%) OK
✅ packed-program Windows 🔒 14.8 MB 6.5 MB file 14.7 MB +48.5 KB (+0.3%) OK
✅ bridge_wasm WASM 16.6 MB 🔒 4.5 MB gzip 4.5 MB +28.3 KB (+0.6%) OK

🔒 = the size this artifact is GATED on (ceiling + delta). Binaries gate on file size (installed binary); WASM gates on gzip (download size). The other size is shown for information only.


Generated by cargo size-gate · workflow run

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle July 27, 2026 20:02 Inactive
@sxlijin
sxlijin force-pushed the agent/bridge-closures branch from bb04b85 to 844f95c Compare July 27, 2026 23:58
@sxlijin sxlijin changed the title Support invoking returned BAML closures Support invoking returned BAML closures across SDKs Jul 27, 2026
@vercel
vercel Bot temporarily deployed to Preview – beps July 27, 2026 23:59 Inactive
@sxlijin
sxlijin force-pushed the agent/bridge-closures branch from 844f95c to c25ad15 Compare July 28, 2026 00:02
@vercel
vercel Bot temporarily deployed to Preview – beps July 28, 2026 00:03 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 July 28, 2026 00:11 Inactive
@sxlijin
sxlijin force-pushed the agent/bridge-closures branch from c25ad15 to faed036 Compare July 28, 2026 00:12
@vercel
vercel Bot temporarily deployed to Preview – beps July 28, 2026 00:14 Inactive
@blacksmith-sh

This comment has been minimized.

@sxlijin
sxlijin force-pushed the agent/bridge-closures branch from faed036 to 522cbdc Compare July 28, 2026 00:16
@vercel
vercel Bot temporarily deployed to Preview – beps July 28, 2026 00:17 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 July 28, 2026 00:24 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle July 28, 2026 00:38 Inactive
@sxlijin
sxlijin marked this pull request as ready for review July 28, 2026 19:00

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 522cbdcc42

ℹ️ About Codex in GitHub

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

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

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

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

Comment on lines +3375 to +3380
let name = param_names
.get(idx)
.ok_or_else(|| EngineError::TypeMismatch {
message: format!("callable parameter {idx} has no name"),
})?;
if let Some(value) = args.remove(name) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Bind required returned-closure arguments positionally

When a returned closure's declared function type uses unnamed required parameters or names different from the implementation—both legal because required parameter names are type-insignificant—the SDK derives keys from the declared handle type (for example arg0 or value), while this loop looks up the underlying function's source name (for example x). The invocation therefore reports a missing required argument instead of calling the closure; required arguments should be mapped by declaration order, reserving name matching for optional parameters.

Useful? React with 👍 / 👎.

Comment on lines +49 to +53
func (function Function) Call(ctx context.Context, args map[string]Input) (Value, error) {
if function.key == 0 || function.owner == nil {
return Value{}, errors.New("call BAML function: invalid or released function handle")
}
return callHandle(ctx, function.key, args)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep the Go function handle owner alive through the call

When the invoked Function is the last value retaining its result owner, function.owner is no longer referenced after this guard, so its finalizer may release function.key while callHandle is waiting and before the spawned native task resolves the key. Under GC pressure this turns a valid invocation into an intermittent stale/dead-handle error; retain the owner through completion, such as with runtime.KeepAlive(function.owner) after callHandle returns.

Useful? React with 👍 / 👎.

Comment on lines +112 to +116
Ok(vec![
$(wire::InboundMapEntry {
key: Some(Key::StringKey(names[$index].clone())),
value: Some($value.to_baml()),
}),+

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Omit absent Rust optional closure arguments

For a returned callable parameter declared optional, codegen represents omission as Option<T>::None, but this unconditional serialization emits every tuple member and encodes None as an explicit BAML null. Calling a closure such as (y?: int) -> int with (None,) therefore either fails type checking or supplies null instead of omitting y; into_kwargs needs presence-aware handling so the corresponding map entry is absent.

Useful? React with 👍 / 👎.

Comment on lines +862 to +864
if params.iter().all(|parameter| {
parameter.mode == CodegenFunctionParamMode::Required && parameter.name.is_some()
}) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Wrap Java closures that have optional parameters

When a function returns a callable with any optional parameter, this guard leaves returned_callable unset. The generated method then follows the ordinary return path and casts the wire-decoded BamlHandle directly to the generated functional interface, producing a ClassCastException rather than an invokable closure. Optional callable parameters are valid BAML types and need a presence-aware proxy instead of being excluded here.

Useful? React with 👍 / 👎.

coderabbitai[bot]
coderabbitai Bot previously requested changes Jul 28, 2026

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

🧹 Nitpick comments (5)
mise.toml (1)

17-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Pin CMake and Ninja in mise.toml.

cmake = "latest" and ninja = "latest" make the local SDK harness depend on plugin-discovered CLI versions, while CI uses runner-provided or installed tools without the same resolution pin. Use explicit versions for reproducible local/CI parity.

🤖 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 `@mise.toml` around lines 17 - 18, Update the cmake and ninja tool entries in
mise.toml to use explicit, pinned versions instead of "latest". Choose versions
matching the tools used by CI to ensure reproducible local and CI environments.
baml_language/sdks/go/sdkgen_go/src/lib.rs (1)

3687-3689: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use the existing GeneratorIdent::DecodedLocal instead of the literal decoded.

decoded_local is already bound at the top of this function and used for the optional-argument path; emitting a raw decoded here bypasses the ident registry that keeps generated locals collision-safe.

♻️ Proposed change
-                let _ = writeln!(out, "\t\tdecoded, {error} := {output_decoder}({result})");
+                let _ = writeln!(
+                    out,
+                    "\t\t{decoded_local}, {error} := {output_decoder}({result})"
+                );
                 let _ = writeln!(out, "\t\tif {error} != nil {{ panic({error}) }}");
-                out.push_str("\t\treturn decoded\n");
+                let _ = writeln!(out, "\t\treturn {decoded_local}");
🤖 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/sdkgen_go/src/lib.rs` around lines 3687 - 3689, Replace
the literal decoded local in the generated return path with the existing
GeneratorIdent::DecodedLocal binding already defined in the surrounding
function. Update both the decoder assignment and return emission so the same
registry-generated identifier is used consistently, including the
optional-argument path.
baml_language/sdks/rust/bridge_rust/src/runtime.rs (1)

159-187: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add SAFETY comments and a --lib unit test for the zero-key guard.

dispatch documents both unsafe blocks; the new ones here don't. Also, dispatch_handle(0, vec![]) short-circuits before capi::api(), so the rejection is unit-testable in-crate — worth covering since the rest of this path is only exercised by the SDK integration tests.

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/sdks/rust/bridge_rust/src/runtime.rs` around lines 159 - 187,
The unsafe calls in dispatch_handle need explicit SAFETY comments, and the
zero-handle guard needs an in-crate unit test. Add comments immediately before
the unsafe new_function_call and call_handle blocks documenting their safety
invariants, then add a --lib unit test invoking dispatch_handle(0, vec![]) that
asserts the expected SdkError without requiring C API initialization.

Source: Coding guidelines

baml_language/sdks/rust/bridge_rust/src/function.rs (1)

1-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add unit tests for the new module.

FunctionHandle's Drop-based release, BamlFunction's Clone handle sharing, and the ()/tuple FunctionArgs arity checks are all testable without a live runtime (the arity-mismatch paths in particular return before touching the C API).

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/sdks/rust/bridge_rust/src/function.rs` around lines 1 - 99, Add
Rust unit tests in the module containing FunctionHandle, BamlFunction, and
FunctionArgs. Cover FunctionHandle’s Drop release behavior, verify cloned
BamlFunction values share the same Arc-backed handle, and test that ()/tuple
FunctionArgs accept matching parameter counts while returning an arity-mismatch
SdkError for incorrect counts without invoking the runtime.

Source: Coding guidelines

baml_language/sdks/csharp/sdkgen_csharp/src/semantic.rs (1)

3642-3653: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add generator tests for the generated callable decode lambdas.

function_type_source() keeps the decode path consistent with use of global::System.Threading.Tasks.Task and Task<T>, but there is no generator test for the emitted async (...) => decode snippets covering both void and non-void return types, with optional parameters as callable. As Rust-only code is changed here, add a semantic.rs unit test under the existing generator test section.

🤖 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/csharp/sdkgen_csharp/src/semantic.rs` around lines 3642 -
3653, Add a semantic.rs generator unit test in the existing generator test
section covering function_type_source() output for callable decode lambdas with
void and non-void return types, including optional parameters. Assert the
emitted async lambda and Task/Task<T>-compatible decode behavior, reusing
existing test helpers and keeping production generation unchanged.

Source: Coding guidelines

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

Inline comments:
In `@baml_language/sdk_tests/crates/csharp/phase11_host_callable/Program.cs`:
- Around line 13-16: Add a second invocation of the buildPair closure in the
phase11 structured-return test using distinct arguments, await its fresh
ReturnedPerson result, and assert the new name and age. Keep the existing
first-call assertions and ensure the repeated handle dispatch is validated
before the success output.

In `@baml_language/sdks/cpp/sdkgen_cpp/src/lib.rs`:
- Line 1434: Update the Ty::Function branch in translate_ty to return
Translated::Unsupported instead of Translated::NotYet, ensuring nested callable
types produce diagnostics rather than being treated as pending during
emit_class’s fixed-point processing.

In `@baml_language/sdks/go/sdkgen_go/src/lib.rs`:
- Around line 5735-5737: Update the assertions around the generated callback
identifiers to match the complete encoder function name rather than the “func
_bamlEncodeCallback” prefix, so each package still verifies exactly one encoder.
Keep the decoder assertion separate and unchanged.
- Around line 3644-3647: Update the generated decoder flow around the emitted
_decode function and returned host callable to accept and propagate ctx_
context.Context, then pass ctx_ to function.CallPositional instead of
context.Background(). Ensure returned BAML callback calls observe caller
cancellation and deadlines throughout the generated call chain.

In `@baml_language/sdks/java/sdkgen_java/src/emit.rs`:
- Around line 860-896: Update the Java generation path around returned_callable
to explicitly reject Ty::Function returns with optional or unnamed parameters
instead of falling back to None. Mirror the existing Go-side
unsupported-callable guard and emit a generation error before translate_callable
can produce an unsafe functional-interface cast; preserve the current
ReturnedCallable handling for fully required, named parameters.

In `@baml_language/sdks/rust/bridge_rust/src/function.rs`:
- Around line 101-134: Update
baml_language/sdks/rust/bridge_rust/src/function.rs:101-134 around
impl_function_args! to carry parameter modes alongside names and omit optional
tuple slots whose value is None from into_kwargs, while preserving explicit null
values for nullable parameters. Update
baml_language/sdks/rust/sdkgen_rust/src/translate_ty.rs:249-263 to emit a
runtime-distinguishable optional wrapper, or fail closed for callables with
optional parameters until omission support exists.
- Around line 157-194: Update the parameter-name validation in
FunctionRef::from_baml so it rejects both missing names and Some("") values,
matching BamlFunctionHandle.decode. Preserve the existing WrongType error path
for invalid parameter names and only collect non-empty names into
parameter_names.

---

Nitpick comments:
In `@baml_language/sdks/csharp/sdkgen_csharp/src/semantic.rs`:
- Around line 3642-3653: Add a semantic.rs generator unit test in the existing
generator test section covering function_type_source() output for callable
decode lambdas with void and non-void return types, including optional
parameters. Assert the emitted async lambda and Task/Task<T>-compatible decode
behavior, reusing existing test helpers and keeping production generation
unchanged.

In `@baml_language/sdks/go/sdkgen_go/src/lib.rs`:
- Around line 3687-3689: Replace the literal decoded local in the generated
return path with the existing GeneratorIdent::DecodedLocal binding already
defined in the surrounding function. Update both the decoder assignment and
return emission so the same registry-generated identifier is used consistently,
including the optional-argument path.

In `@baml_language/sdks/rust/bridge_rust/src/function.rs`:
- Around line 1-99: Add Rust unit tests in the module containing FunctionHandle,
BamlFunction, and FunctionArgs. Cover FunctionHandle’s Drop release behavior,
verify cloned BamlFunction values share the same Arc-backed handle, and test
that ()/tuple FunctionArgs accept matching parameter counts while returning an
arity-mismatch SdkError for incorrect counts without invoking the runtime.

In `@baml_language/sdks/rust/bridge_rust/src/runtime.rs`:
- Around line 159-187: The unsafe calls in dispatch_handle need explicit SAFETY
comments, and the zero-handle guard needs an in-crate unit test. Add comments
immediately before the unsafe new_function_call and call_handle blocks
documenting their safety invariants, then add a --lib unit test invoking
dispatch_handle(0, vec![]) that asserts the expected SdkError without requiring
C API initialization.

In `@mise.toml`:
- Around line 17-18: Update the cmake and ninja tool entries in mise.toml to use
explicit, pinned versions instead of "latest". Choose versions matching the
tools used by CI to ensure reproducible local and CI environments.
🪄 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 Plus

Run ID: 7dbf1070-68ea-4341-9cac-4f39c04be1dd

📥 Commits

Reviewing files that changed from the base of the PR and between 6b23753 and 522cbdc.

⛔ Files ignored due to path filters (5)
  • baml_language/sdks/csharp/bridge_csharp/src/Generated/V1/BamlGeneratedCodec.cs is excluded by !**/generated/**
  • baml_language/sdks/typescript/bridge_typescript/dist/native.d.ts is excluded by !**/dist/**
  • baml_language/sdks/typescript/bridge_typescript/dist/proto.d.ts.map is excluded by !**/dist/**, !**/*.map
  • baml_language/sdks/typescript/bridge_typescript/dist/proto.js is excluded by !**/dist/**
  • baml_language/sdks/typescript/bridge_typescript/dist/proto.js.map is excluded by !**/dist/**, !**/*.map
📒 Files selected for processing (69)
  • baml_language/crates/bex_engine/src/conversion.rs
  • baml_language/crates/bex_engine/src/lib.rs
  • baml_language/crates/bex_project/src/bex.rs
  • baml_language/crates/bridge_cffi/cbindgen.toml
  • baml_language/crates/bridge_cffi/include/baml_cffi.h
  • baml_language/crates/bridge_cffi/src/api.rs
  • baml_language/crates/bridge_cffi/src/baml_to_host.rs
  • baml_language/crates/bridge_cffi/src/lib.rs
  • baml_language/crates/bridge_cffi/src/lib_native.rs
  • baml_language/crates/bridge_cffi/src/lib_wasm.rs
  • baml_language/crates/bridge_cffi/tests/abi_assertions.h
  • baml_language/crates/bridge_cffi/tests/abi_layout.c
  • baml_language/crates/bridge_cffi/tests/abi_layout.rs
  • baml_language/crates/bridge_ctypes/src/handle_table.rs
  • baml_language/sdk_tests/crates/cpp/function_calls/customizable/tests/test_host_callables.cc
  • baml_language/sdk_tests/crates/csharp/phase11_host_callable/Program.cs
  • baml_language/sdk_tests/crates/csharp/phase11_host_callable/baml_src/ns_csharp_phase11/main.baml
  • baml_language/sdk_tests/crates/csharp/src/lib.rs
  • baml_language/sdk_tests/crates/go/function_calls/customizable/test_host_callables_test.go
  • baml_language/sdk_tests/crates/java/function_calls/customizable/TestHostCallables.java
  • baml_language/sdk_tests/crates/python_pydantic2/function_calls/customizable/test_host_callables.py
  • baml_language/sdk_tests/crates/rust/function_calls/customizable/test_host_callables.rs
  • baml_language/sdk_tests/crates/swift/function_calls/customizable/TestHostCallables.swift
  • baml_language/sdk_tests/crates/typescript/function_calls/customizable/host_callables.test.ts
  • baml_language/sdk_tests/fixtures/function_calls/baml_src/ns_host_callable_tests/main.baml
  • baml_language/sdks/cpp/bridge_cpp/include/baml/codec.h
  • baml_language/sdks/cpp/bridge_cpp/include/baml/detail/call.h
  • baml_language/sdks/cpp/bridge_cpp/include/baml/detail/loader.h
  • baml_language/sdks/cpp/sdkgen_cpp/src/lib.rs
  • baml_language/sdks/csharp/bridge_csharp/src/Cffi/NativeApi.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Cffi/NativeTypes.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Proto/PrimitiveProtocol.cs
  • baml_language/sdks/csharp/sdkgen_csharp/src/semantic.rs
  • baml_language/sdks/go/baml_go/function.go
  • baml_language/sdks/go/baml_go/internal/cffi/include/baml_cffi.h
  • baml_language/sdks/go/baml_go/native_unix.go
  • baml_language/sdks/go/baml_go/native_unsupported.go
  • baml_language/sdks/go/baml_go/native_windows.go
  • baml_language/sdks/go/baml_go/runtime.go
  • baml_language/sdks/go/sdkgen_go/src/lib.rs
  • baml_language/sdks/java/baml_bridge/src/main/java/baml_bridge/BamlFfi.java
  • baml_language/sdks/java/baml_bridge/src/main/java/baml_bridge/BamlHandle.java
  • baml_language/sdks/java/bridge_java/src/lib.rs
  • baml_language/sdks/java/sdkgen_java/src/emit.rs
  • baml_language/sdks/python/rust/bridge_python/src/baml_core/baml_py/__init__.pyi
  • baml_language/sdks/python/rust/bridge_python/src/runtime.rs
  • baml_language/sdks/python/src/baml_bridge/baml_py.pyi
  • baml_language/sdks/python/src/baml_bridge/proto.py
  • baml_language/sdks/python/tests/test_decode_handle.py
  • baml_language/sdks/python/tests/test_engine.py
  • baml_language/sdks/rust/bridge_rust/src/capi.rs
  • baml_language/sdks/rust/bridge_rust/src/function.rs
  • baml_language/sdks/rust/bridge_rust/src/lib.rs
  • baml_language/sdks/rust/bridge_rust/src/runtime.rs
  • baml_language/sdks/rust/sdkgen_rust/src/translate_ty.rs
  • baml_language/sdks/swift/Sources/BamlBridge/Api.swift
  • baml_language/sdks/swift/Sources/BamlBridge/Handle.swift
  • baml_language/sdks/swift/Sources/BamlBridge/Runtime.swift
  • baml_language/sdks/swift/Sources/CBamlBridge/include/baml_cffi.h
  • baml_language/sdks/swift/rust/sdkgen_swift/src/emit.rs
  • baml_language/sdks/typescript/bridge_typescript/src/runtime.rs
  • baml_language/sdks/typescript/bridge_typescript/tests/test_engine.test.ts
  • baml_language/sdks/typescript/bridge_typescript/typescript_src/native.d.ts
  • baml_language/sdks/typescript/bridge_typescript/typescript_src/proto.ts
  • baml_language/sdks/typescript/bridge_typescript_web/src/runtime.rs
  • baml_language/sdks/typescript/bridge_typescript_web/typescript_src/native.ts
  • baml_language/sdks/typescript/bridge_typescript_web/typescript_src/wasm/bridge_web_core.d.ts
  • mise.toml
  • release/bridge-cffi-public-exports.txt

Comment on lines +13 to +16
Func<long, string, CancellationToken, Task<ReturnedPerson>> buildPair = Functions.MakePairBuilder(30L);
ReturnedPerson ada = await buildPair(12L, "Ada", CancellationToken.None);
Require(ada.Name == "Ada" && ada.Age == 42L, "returned closure structured result changed");
Console.WriteLine("baml_closure_decodes_multiple_args_and_structured_return_values=ok");

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exercise structured-return closure reuse.

This invokes buildPair only once, unlike the supplied C++ and Go parity tests. Add a second call with distinct arguments to verify that repeated handle dispatch still decodes a fresh ReturnedPerson.

Proposed test addition
 ReturnedPerson ada = await buildPair(12L, "Ada", CancellationToken.None);
 Require(ada.Name == "Ada" && ada.Age == 42L, "returned closure structured result changed");
+ReturnedPerson grace = await buildPair(5L, "Grace", CancellationToken.None);
+Require(grace.Name == "Grace" && grace.Age == 35L, "returned closure reuse changed");
 Console.WriteLine("baml_closure_decodes_multiple_args_and_structured_return_values=ok");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Func<long, string, CancellationToken, Task<ReturnedPerson>> buildPair = Functions.MakePairBuilder(30L);
ReturnedPerson ada = await buildPair(12L, "Ada", CancellationToken.None);
Require(ada.Name == "Ada" && ada.Age == 42L, "returned closure structured result changed");
Console.WriteLine("baml_closure_decodes_multiple_args_and_structured_return_values=ok");
Func<long, string, CancellationToken, Task<ReturnedPerson>> buildPair = Functions.MakePairBuilder(30L);
ReturnedPerson ada = await buildPair(12L, "Ada", CancellationToken.None);
Require(ada.Name == "Ada" && ada.Age == 42L, "returned closure structured result changed");
ReturnedPerson grace = await buildPair(5L, "Grace", CancellationToken.None);
Require(grace.Name == "Grace" && grace.Age == 35L, "returned closure reuse changed");
Console.WriteLine("baml_closure_decodes_multiple_args_and_structured_return_values=ok");
🤖 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/sdk_tests/crates/csharp/phase11_host_callable/Program.cs`
around lines 13 - 16, Add a second invocation of the buildPair closure in the
phase11 structured-return test using distinct arguments, await its fresh
ReturnedPerson result, and assert the new name and age. Keep the existing
first-call assertions and ensure the repeated handle dispatch is validated
before the success output.

other => return other,
}
}
Ty::Function { .. } => return Translated::NotYet,

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# How does the emission driver treat Translated::NotYet? Does it bound retries?
rg -nP -C6 'NotYet' baml_language/sdks/cpp/sdkgen_cpp/src/lib.rs

Repository: BoundaryML/baml

Length of output: 8997


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Relevant translate/equiv slices =="
sed -n '750,830p' baml_language/sdks/cpp/sdkgen_cpp/src/lib.rs
echo "---"
sed -n '970,1015p' baml_language/sdks/cpp/sdkgen_cpp/src/lib.rs
echo "---"
sed -n '1110,1145p' baml_language/sdks/cpp/sdkgen_cpp/src/lib.rs
echo "---"
sed -n '1430,1500p' baml_language/sdks/cpp/sdkgen_cpp/src/lib.rs

echo "== Equiv helpers and notyet/unsupported handling =="
rg -n "Equiv|not_yet|notyet|NotYet|Unsupported|func.*Equiv|function.*Equiv" baml_language/sdks/cpp/sdkgen_cpp/src/lib.rs

Repository: BoundaryML/baml

Length of output: 11284


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Candidate emit/loop definitions =="
rg -n "fn emit|fixed|loop|while|Ok\\(None\\)|emitted_types|Typed|Equiv" baml_language/sdks/cpp/sdkgen_cpp/src/lib.rs | head -160

echo "---"
awk '/fn emit[^{].*(pool|names|emitted|Fixed|emission)/ || /emitted_types|Translated::NotYet|Translated::Unsupported/ {
  for(i=1;i<=NF;i++) if($i~/^fn /) line=$i
}' baml_language/sdks/cpp/sdkgen_cpp/src/lib.rs | head -50

echo "== outline around emit =="
ast-grep outline baml_language/sdks/cpp/sdkgen_cpp/src/lib.rs --view compact | rg -n "emit" | head -80

Repository: BoundaryML/baml

Length of output: 4362


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Top-level fixed-point and emit_type =="
sed -n '1,175p' baml_language/sdks/cpp/sdkgen_cpp/src/lib.rs
echo "---"
sed -n '1808,1865p' baml_language/sdks/cpp/sdkgen_cpp/src/lib.rs

Repository: BoundaryML/baml

Length of output: 9336


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Translate helper signatures and ty enum variants =="
sed -n '1268,1282p' baml_language/sdks/cpp/sdkgen_cpp/src/lib.rs
echo "---"
rg -n "Ty::Function|struct Ty|enum Ty|pub enum Ty|pub struct Ty" baml_language -S

echo "---"
rg -n "pub enum Ty|pub struct Ty|Ty::Function|Function\\{|fn ty\\(|impl Ty" --glob '*.rs' | head -120

echo "== Files mentioning Ty =="
rg -n "^pub (enum|struct) Ty|Ty::Function|Function\\{|impl Ty" --glob '*.rs'

Repository: BoundaryML/baml

Length of output: 43166


Return Unsupported for nested callable types in translate_ty.

Ty::Function cannot become translatable just because more types are emitted later. Returning Translated::NotYet makes emit_class treat the class as still pending; with a nested function type the fixed-point loop eventually stops and this type is silently skipped instead of being reported with diagnostics.

🤖 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/cpp/sdkgen_cpp/src/lib.rs` at line 1434, Update the
Ty::Function branch in translate_ty to return Translated::Unsupported instead of
Translated::NotYet, ensuring nested callable types produce diagnostics rather
than being treated as pending during emit_class’s fixed-point processing.

Comment on lines +3644 to +3647
let _ = writeln!(
out,
"\t\t{result_binding}, {error} := function.CallPositional(context.Background(), requiredArgs_, optionalArgs_)"
);

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# How is the "context" import decided for functions.go?
rg -nP -C6 '"context"' baml_language/sdks/go/sdkgen_go/src/lib.rs | head -60
# Which package owns a callback key vs. which package declares the consuming function?
rg -nP -C8 'callback_keys' baml_language/sdks/go/sdkgen_go/src/lib.rs
rg -nP -C6 'fn callback_ident' baml_language/sdks/go/sdkgen_go/src

Repository: BoundaryML/baml

Length of output: 7768


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file outline around render functions =="
ast-grep outline baml_language/sdks/go/sdkgen_go/src/lib.rs --match generate_functions_go --view expanded || true
ast-grep outline baml_language/sdks/go/sdkgen_go/src/lib.rs --match render_callback_codecs --view expanded || true

echo "== functions.go generation block =="
sed -n '1320,1425p' baml_language/sdks/go/sdkgen_go/src/lib.rs | cat -n

echo "== callback codec generation =="
sed -n '3335,3675p' baml_language/sdks/go/sdkgen_go/src/lib.rs | cat -n

echo "== decode signatures and calls in generated files (repo-wide examples) =="
rg -n "_decode\\([^)]*\\bvalue_\\s+baml_go\\.Value|_decode\\([^)]*\\bctx_\\s+context\\.Context|Context\\.WithCancel|context\\.Background\\(\\)|context\\.Background\\(\\)" baml_language/sdks/go/sdkgen_go/src/lib.rs baml_language -g '*.rs' | head -120

Repository: BoundaryML/baml

Length of output: 21973


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== render_functions later import callback codecs block =="
sed -n '1420,1520p' baml_language/sdks/go/sdkgen_go/src/lib.rs | cat -n

echo "== call sites for render_callback_codecs and render_functions =="
rg -nP -C4 'render_callback_codecs|render_functions|functions.go|generate.*functions|WireCodecs::for_functions' baml_language/sdks/go/sdkgen_go/src/lib.rs

echo "== project function argument decoding/import context =="
rg -nP -C5 'ArgumentParameter|ctx_|context.Context|function_call|CallPositional|generate_function_with_receiver|project.*argument|_argument(_|$)' baml_language/sdks/go/sdkgen_go/src/lib.rs

echo "== deterministic source-shape check for context import vs callback-only package generation =="
python3 - <<'PY'
from pathlib import Path
import re
src = Path('baml_language/sdks/go/sdkgen_go/src/lib.rs').read_text()
checks = {
    'context_import_when_functions_empty': 'if !functions.is_empty() {' in src and src.index('if !functions.is_empty() {') < src.index('imports.add_generator(context_package, "context");'),
    'callback_codecs_included_in_has_codecs': '!codecs.callback_keys.is_empty()' in src and 'has_codecs = !codecs.class_names.is_empty()' in src,
    'callback_decoder_signature_contains_value': 'func {decoder}({value} {runtime}.Value) ({{go_type}}, error)' in src,
    'returned_closure_uses_context_background': 'function.CallPositional(context.Background(), requiredArgs_, optionalArgs_)' in src,
}
for k, v in checks.items():
    print(f'{k}={v}')
PY

Repository: BoundaryML/baml

Length of output: 43384


Make returned BAML callback calls observe call context

The emitted decoder surface is _decode(value_ baml_go.Value), so callers cannot pass context.Context into CallPositional; returned callback functions always ignore deadlines/cancellation. Thread ctx_ context.Context through the generated decoder, returned host callable, and the CallPositional call instead of hard-coding context.Background().

🤖 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/sdkgen_go/src/lib.rs` around lines 3644 - 3647, Update
the generated decoder flow around the emitted _decode function and returned host
callable to accept and propagate ctx_ context.Context, then pass ctx_ to
function.CallPositional instead of context.Background(). Ensure returned BAML
callback calls observe caller cancellation and deadlines throughout the
generated call chain.

Comment on lines +5735 to +5737
assert_eq!(root.matches("func _bamlEncodeCallback").count(), 2);
assert_eq!(models.matches("func _bamlEncodeCallback").count(), 2);
assert!(root.contains("_decode(value_ baml_go.Value)"));

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

These counts now include the decoder, weakening the encoder assertion.

func _bamlEncodeCallback0_decode is also matched by the prefix func _bamlEncodeCallback, so count() == 2 no longer distinguishes "one encoder per package" from "encoder + decoder". Anchor the assertions to the full identifiers.

💚 Proposed change
-        assert_eq!(root.matches("func _bamlEncodeCallback").count(), 2);
-        assert_eq!(models.matches("func _bamlEncodeCallback").count(), 2);
-        assert!(root.contains("_decode(value_ baml_go.Value)"));
+        assert_eq!(root.matches("func _bamlEncodeCallback0(").count(), 1);
+        assert_eq!(models.matches("func _bamlEncodeCallback0(").count(), 1);
+        assert_eq!(root.matches("func _bamlEncodeCallback0_decode(").count(), 1);
+        assert_eq!(models.matches("func _bamlEncodeCallback0_decode(").count(), 1);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert_eq!(root.matches("func _bamlEncodeCallback").count(), 2);
assert_eq!(models.matches("func _bamlEncodeCallback").count(), 2);
assert!(root.contains("_decode(value_ baml_go.Value)"));
assert_eq!(root.matches("func _bamlEncodeCallback0(").count(), 1);
assert_eq!(models.matches("func _bamlEncodeCallback0(").count(), 1);
assert_eq!(root.matches("func _bamlEncodeCallback0_decode(").count(), 1);
assert_eq!(models.matches("func _bamlEncodeCallback0_decode(").count(), 1);
🤖 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/sdkgen_go/src/lib.rs` around lines 5735 - 5737, Update
the assertions around the generated callback identifiers to match the complete
encoder function name rather than the “func _bamlEncodeCallback” prefix, so each
package still verifies exactly one encoder. Keep the decoder assertion separate
and unchanged.

Comment on lines +860 to +896
let returned_callable = match &function.return_type {
Ty::Function { params, ret, .. }
if params.iter().all(|parameter| {
parameter.mode == CodegenFunctionParamMode::Required && parameter.name.is_some()
}) =>
{
let raw_type = ret_boxed
.split_once('<')
.map_or_else(|| ret_boxed.clone(), |(raw, _)| raw.to_string());
let parameter_names = format!(
"new java.lang.String[] {{{}}}",
params
.iter()
.map(|parameter| format!(
"{:?}",
parameter
.name
.as_ref()
.expect("returned callable parameters have wire names")
.as_str()
))
.collect::<Vec<_>>()
.join(", ")
);
let return_descriptor = match crate::translate_ty::descriptor_expr_opt(ret, ctx.aliases)
{
None => "null".to_string(),
Some(expr) => pool.intern(expr),
};
Some(ReturnedCallable {
raw_type,
parameter_names,
return_descriptor,
})
}
_ => None,
};

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# What Java type does translate_ty produce for Ty::Function, and is the symbol skipped anywhere?
rg -nP -C8 'Ty::Function' baml_language/sdks/java/sdkgen_java/src/translate_ty.rs
rg -nP -C4 'Ty::Function' baml_language/sdks/java/sdkgen_java/src --glob '!translate_ty.rs'

Repository: BoundaryML/baml

Length of output: 5503


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== emit.rs surrounding relevant sections =="
sed -n '800,905p' baml_language/sdks/java/sdkgen_java/src/emit.rs
sed -n '1190,1255p' baml_language/sdks/java/sdkgen_java/src/emit.rs

echo "== locate translate_callable implementation =="
rg -n -C12 'fn translate_callable|translate_callable\(' baml_language/sdks/java/sdkgen_java/src/emit.rs baml_language/sdks/java/sdkgen_java/src/lib.rs baml_language/sdks/java/sdkgen_java/src/translate_ty.rs

echo "== test around optional callable params =="
sed -n '1205,1300p' baml_language/sdks/java/sdkgen_java/src/lib.rs

Repository: BoundaryML/baml

Length of output: 17638


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== translate_callable full implementation =="
sed -n '696,845p' baml_language/sdks/java/sdkgen_java/src/translate_ty.rs

echo "== Go reference supported_function =="
if [ -f baml_language/sdks/go/sdkgen_go/src/lib.rs ]; then
  rg -n -C10 'supported_function' baml_language/sdks/go/sdkgen_go/src/lib.rs
else
  echo "Go source not present"
fi

echo "== tests for required-only callable parameters =="
rg -n -C10 'FunctionalInterface|BamlFfi\.returnedClosure|returned_closure|call_cb|Function.*return_type|Ty::Function' baml_language/sdks/java/sdkgen_java/src/lib.rs baml_language/sdks/java/sdkgen_java/src/emit.rs

Repository: BoundaryML/baml

Length of output: 33093


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== inspect Java returned closure implementation =="
rg -n -C12 'returnedClosure|returnedClosureAsync|record|interface.*BamlHostCallable|BamlHostCallable' baml -g '*.java' -g '*.kt'

echo "== inspect Go support failure path around function return Ty::Function =="
sed -n '1246,1260p' baml_language/sdks/go/sdkgen_go/src/lib.rs

Repository: BoundaryML/baml

Length of output: 204


🌐 Web query:

BoundaryML BAML Java BamlFfi returnedClosure returnedClosureAsync returned closure implementation

💡 Result:

As of July 28, 2026, BoundaryML's BAML does not provide a native Java client library [1][2]. While BAML supports several languages like Python, TypeScript, Rust, and Go, Java integration is currently achieved via the OpenAPI/REST API integration rather than direct FFI (Foreign Function Interface) bindings [1][3]. The terms "BamlFfi", "returnedClosure", and "returnedClosureAsync" appear to be related to the internal C FFI (Foreign Function Interface) layer of the BAML engine, which is written in Rust [4][5][6]. This layer uses C-style function exports to communicate between the BAML core engine and language-specific clients [4]. Concepts like "closure" and "callback" handling in this context typically refer to how the FFI layer manages asynchronous cross-language invocations (e.g., passing a function pointer from a higher-level language back to the Rust core to handle a response or stream update) [4]. If you encountered "returnedClosure" or "returnedClosureAsync" in documentation or code, it is likely part of the internal implementation details of the C FFI bindings (libbaml_cffi), which are not intended for direct use in standard Java application development [4][5]. For Java developers looking to use BAML, the recommended approach is: 1. Define your BAML functions in your BAML project [3]. 2. Configure the BAML generator to output an OpenAPI/REST specification [3]. 3. Use a standard Java OpenAPI generator to create a client that interacts with the BAML service via HTTP [2][3]. This method avoids the complexity and instability associated with manual FFI integration [1].

Citations:


Fail loudly instead of casting an unsupported callable return

When a function returns Ty::Function with optional or unnamed params, returned_callable falls back to None and the generator emits return (Callable) baml_bridge.BamlFfi.callSync(...). Since translate_callable also translates Ty::Function returns as Java functional-interface types, this path can produce generated code whose runtime return value is a handle, not the callable interface, and then fails with ClassCastException instead of being skipped during generation. Mirror the Go-side guard and treat such returns as unsupported.

🤖 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/java/sdkgen_java/src/emit.rs` around lines 860 - 896,
Update the Java generation path around returned_callable to explicitly reject
Ty::Function returns with optional or unnamed parameters instead of falling back
to None. Mirror the existing Go-side unsupported-callable guard and emit a
generation error before translate_callable can produce an unsafe
functional-interface cast; preserve the current ReturnedCallable handling for
fully required, named parameters.

Comment on lines +101 to +134
macro_rules! impl_function_args {
($count:expr; $(($type:ident, $value:ident, $index:tt)),+ $(,)?) => {
impl<$($type: BamlValue),+> FunctionArgs for ($($type,)+) {
fn into_kwargs(
self,
names: &[String],
) -> Result<Vec<wire::InboundMapEntry>, SdkError> {
if names.len() != $count {
return Err(SdkError::new("BAML function handle arity mismatch"));
}
let ($($value,)+) = self;
Ok(vec![
$(wire::InboundMapEntry {
key: Some(Key::StringKey(names[$index].clone())),
value: Some($value.to_baml()),
}),+
])
}

fn parameter_types() -> Vec<wire::BamlTy> {
vec![$($type::baml_ty()),+]
}
}
};
}

impl_function_args!(1; (A0, a0, 0));
impl_function_args!(2; (A0, a0, 0), (A1, a1, 1));
impl_function_args!(3; (A0, a0, 0), (A1, a1, 1), (A2, a2, 2));
impl_function_args!(4; (A0, a0, 0), (A1, a1, 1), (A2, a2, 2), (A3, a3, 3));
impl_function_args!(5; (A0, a0, 0), (A1, a1, 1), (A2, a2, 2), (A3, a3, 3), (A4, a4, 4));
impl_function_args!(6; (A0, a0, 0), (A1, a1, 1), (A2, a2, 2), (A3, a3, 3), (A4, a4, 4), (A5, a5, 5));
impl_function_args!(7; (A0, a0, 0), (A1, a1, 1), (A2, a2, 2), (A3, a3, 3), (A4, a4, 4), (A5, a5, 5), (A6, a6, 6));
impl_function_args!(8; (A0, a0, 0), (A1, a1, 1), (A2, a2, 2), (A3, a3, 3), (A4, a4, 4), (A5, a5, 5), (A6, a6, 6), (A7, a7, 7));

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Optional parameters on returned callables lose their BAML defaults in the Rust SDK. One root cause: the generated Rust type models an optional callable parameter as a plain Option<T> tuple slot, and into_kwargs unconditionally encodes every slot, so None rides the wire as an explicit null instead of being omitted — the engine then uses null rather than the declared BAML default. Swift (_appendIfSet), C# (TryGetValue), and TypeScript (decodeBamlClosure) all omit unset optionals.

  • baml_language/sdks/rust/bridge_rust/src/function.rs#L101-L134: thread the parameter modes alongside parameter_names and skip None slots for optional parameters instead of emitting a null kwarg.
  • baml_language/sdks/rust/sdkgen_rust/src/translate_ty.rs#L249-L263: either emit a dedicated optional wrapper the runtime can distinguish from a nullable value, or fail closed on callables with optional parameters until the omit path exists.
📍 Affects 2 files
  • baml_language/sdks/rust/bridge_rust/src/function.rs#L101-L134 (this comment)
  • baml_language/sdks/rust/sdkgen_rust/src/translate_ty.rs#L249-L263
🤖 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/rust/bridge_rust/src/function.rs` around lines 101 - 134,
Update baml_language/sdks/rust/bridge_rust/src/function.rs:101-134 around
impl_function_args! to carry parameter modes alongside names and omit optional
tuple slots whose value is None from into_kwargs, while preserving explicit null
values for nullable parameters. Update
baml_language/sdks/rust/sdkgen_rust/src/translate_ty.rs:249-263 to emit a
runtime-distinguishable optional wrapper, or fail closed for callables with
optional parameters until omission support exists.

Comment on lines +157 to +194
fn from_baml(value: wire::BamlOutboundValue) -> Result<Self, DecodeError> {
let value = crate::decode::unwrap(value);
let got = crate::baml_value::wire_variant_kind(&value);
let Some(Out::HandleValue(handle)) = value.value else {
return Err(DecodeError::WrongType {
expected: "function",
got,
});
};
if handle.key == 0 || handle.handle_type != wire::BamlHandleType::FunctionRef as i32 {
return Err(DecodeError::WrongType {
expected: "function",
got: "handle",
});
}
let owned_handle = Arc::new(FunctionHandle { key: handle.key });
let Some(wire::baml_ty::Ty::Function(function_type)) = handle.ty.and_then(|ty| ty.ty)
else {
return Err(DecodeError::WrongType {
expected: "function",
got: "handle",
});
};
let parameter_names = function_type
.params
.into_iter()
.map(|parameter| parameter.name)
.collect::<Option<Vec<_>>>()
.ok_or(DecodeError::WrongType {
expected: "function with named parameters",
got: "handle",
})?;
Ok(Self {
handle: owned_handle,
parameter_names: parameter_names.into(),
marker: PhantomData,
})
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Empty parameter names pass validation here but are rejected by Swift.

collect::<Option<Vec<_>>>() only rejects absent names; Some("") is accepted, whereas BamlFunctionHandle.decode in baml_language/sdks/swift/Sources/BamlBridge/Handle.swift rejects empty names explicitly. An empty wire name would produce a kwarg with an empty key at call time instead of failing at decode.

🛡️ Proposed fix
         let parameter_names = function_type
             .params
             .into_iter()
-            .map(|parameter| parameter.name)
+            .map(|parameter| parameter.name.filter(|name| !name.is_empty()))
             .collect::<Option<Vec<_>>>()
             .ok_or(DecodeError::WrongType {
                 expected: "function with named parameters",
                 got: "handle",
             })?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn from_baml(value: wire::BamlOutboundValue) -> Result<Self, DecodeError> {
let value = crate::decode::unwrap(value);
let got = crate::baml_value::wire_variant_kind(&value);
let Some(Out::HandleValue(handle)) = value.value else {
return Err(DecodeError::WrongType {
expected: "function",
got,
});
};
if handle.key == 0 || handle.handle_type != wire::BamlHandleType::FunctionRef as i32 {
return Err(DecodeError::WrongType {
expected: "function",
got: "handle",
});
}
let owned_handle = Arc::new(FunctionHandle { key: handle.key });
let Some(wire::baml_ty::Ty::Function(function_type)) = handle.ty.and_then(|ty| ty.ty)
else {
return Err(DecodeError::WrongType {
expected: "function",
got: "handle",
});
};
let parameter_names = function_type
.params
.into_iter()
.map(|parameter| parameter.name)
.collect::<Option<Vec<_>>>()
.ok_or(DecodeError::WrongType {
expected: "function with named parameters",
got: "handle",
})?;
Ok(Self {
handle: owned_handle,
parameter_names: parameter_names.into(),
marker: PhantomData,
})
}
fn from_baml(value: wire::BamlOutboundValue) -> Result<Self, DecodeError> {
let value = crate::decode::unwrap(value);
let got = crate::baml_value::wire_variant_kind(&value);
let Some(Out::HandleValue(handle)) = value.value else {
return Err(DecodeError::WrongType {
expected: "function",
got,
});
};
if handle.key == 0 || handle.handle_type != wire::BamlHandleType::FunctionRef as i32 {
return Err(DecodeError::WrongType {
expected: "function",
got: "handle",
});
}
let owned_handle = Arc::new(FunctionHandle { key: handle.key });
let Some(wire::baml_ty::Ty::Function(function_type)) = handle.ty.and_then(|ty| ty.ty)
else {
return Err(DecodeError::WrongType {
expected: "function",
got: "handle",
});
};
let parameter_names = function_type
.params
.into_iter()
.map(|parameter| parameter.name.filter(|name| !name.is_empty()))
.collect::<Option<Vec<_>>>()
.ok_or(DecodeError::WrongType {
expected: "function with named parameters",
got: "handle",
})?;
Ok(Self {
handle: owned_handle,
parameter_names: parameter_names.into(),
marker: PhantomData,
})
}
🤖 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/rust/bridge_rust/src/function.rs` around lines 157 - 194,
Update the parameter-name validation in FunctionRef::from_baml so it rejects
both missing names and Some("") values, matching BamlFunctionHandle.decode.
Preserve the existing WrongType error path for invalid parameter names and only
collect non-empty names into parameter_names.

@sxlijin
sxlijin dismissed coderabbitai[bot]’s stale review July 28, 2026 20:13

not in critical paths

@sxlijin
sxlijin added this pull request to the merge queue Jul 28, 2026
Merged via the queue into canary with commit 384a6ff Jul 28, 2026
82 checks passed
@sxlijin
sxlijin deleted the agent/bridge-closures branch July 28, 2026 20:25
meefs pushed a commit to meefs/baml that referenced this pull request Jul 28, 2026
…diverged (BoundaryML#4245)

## Summary

- Move named-function and function-handle selection into
`CallFunctionArgs.call_target`.
- Route both targets through the existing `call_function` C ABI export
and remove the appended `call_handle` surface.
- Update the native, WASM, Python, Node, browser, Workers, Go, C++,
Rust, Swift, C#, and Java bridge layers plus generated protobuf/header
artifacts.
- Add `protoc-gen-swift` to the mise toolchain so the Swift SDK bindings
can be regenerated locally.

## Review follow-up from BoundaryML#4220

- Bind returned-callable required parameters positionally, while keeping
supplied optionals name-keyed and presence-aware; the shared fixture
deliberately gives the declared parameter and closure implementation
different names so every SDK exercises this behavior.
- Keep Go handle owners alive through dispatch and make returned Go
closures accept and propagate `context.Context`.
- Represent Rust optional callable parameters with `OptionalArg<T>`,
reject empty wire parameter names, document unsafe handle dispatch, and
add handle lifetime/arity/omission tests.
- Give unnamed Java required parameters canonical `argN` names and fail
generation explicitly for optional returned callables until Java can
preserve omitted versus explicit-null values.
- Address the remaining C++, C#, Go generator, runtime safety, and
reproducible mise-tool review comments with focused tests.

## Review follow-up from BoundaryML#4245

- Bump the `BamlApiV1` function-table ABI revision to 2 so revision-1
hosts and runtimes reject the changed `call_function` pointer signature
before reading the slot.
- Classify `type_args` supplied with a function-handle target and
omitted required callable arguments as invalid arguments across the
shared outbound error path.
- Keep Rust raw ABI slots nullable until validation and make both Rust
and C++ reject every null required table operation before use.
- Require Go call encoders to carry both an explicit target and nonzero
call ID, and migrate the legacy Go host-error path to the canonical
`baml.errors.HostCallable` value with same-process error identity.
- Update generated headers and native loader/test fixtures, including
the C# ABI probes and Go mismatch tests, to enforce revision 2
consistently.

## Stack

Built on BoundaryML#4220, which has now merged into `canary`; this PR was rebased
onto its merge commit and contains one follow-up commit.

## Test plan

- `mise exec -- cargo test -p bridge_cffi --tests`
- `mise exec -- cargo test -p baml_bridge -p sdkgen_cpp -p sdkgen_csharp
-p bridge_typescript -p bridge_typescript_web -p bridge_python -p
bridge_java`
- `mise exec -- go test ./...` in both `sdks/go/baml_go` and
`sdks/go/bridge_go`
- C++ bridge smoke target with warnings-as-errors
- Managed C# ABI foundation suite plus ABI probe builds
- Engine regression for positional binding when declared and
implementation parameter names differ
- Complete `function_calls` matrix across Python, Node, browser,
Workers, Go, C++, Rust, Swift, C#, and Java
- Focused Java and C# returned-closure suites
- Full workspace `mise run clippy`, formatting, and repository prek
hooks

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

* **New Features**
* Call requests can now target either a named function or a function
handle via the encoded request payload.
* Named and handle-based calls follow a unified invocation path across
supported SDKs and platforms.

* **Breaking Changes**
* Runtime/native call APIs no longer accept function names as separate
parameters.
  * Dedicated handle-call entry points were removed.
  * The underlying C ABI for the function table is revised to version 2.

* **Bug Fixes**
  * Missing call targets now return a clear invalid-argument error.
* Function-handle calls that include unsupported type arguments are
rejected.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

This branch was previously deployed

3 inactive deployments
Preview – promptfiddle — 522cbdcc Deployed Jul 28, 2026 by vercel[bot]
Preview – promptfiddle2 — 522cbdcc Deployed Jul 28, 2026 by vercel[bot]
Preview – beps — 522cbdcc Deployed Jul 28, 2026 by vercel[bot]
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