Repository navigation
Support invoking returned BAML closures across SDKs - #4220
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR enables BAML function and closure values to round-trip as owned function handles. It adds engine-side named invocation, a ChangesCallable handle execution
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
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
⏭️ Performance benchmarks were skippedPerf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to To run them on this PR, do any of the following, then push a commit (or re-run CI):
|
18a30b8 to
571ae4f
Compare
571ae4f to
bb04b85
Compare
Binary size checks passed✅ 7 passed
Generated by |
bb04b85 to
844f95c
Compare
844f95c to
c25ad15
Compare
c25ad15 to
faed036
Compare
This comment has been minimized.
This comment has been minimized.
faed036 to
522cbdc
Compare
There was a problem hiding this comment.
💡 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".
| 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) { |
There was a problem hiding this comment.
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 👍 / 👎.
| 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) |
There was a problem hiding this comment.
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 👍 / 👎.
| Ok(vec![ | ||
| $(wire::InboundMapEntry { | ||
| key: Some(Key::StringKey(names[$index].clone())), | ||
| value: Some($value.to_baml()), | ||
| }),+ |
There was a problem hiding this comment.
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 👍 / 👎.
| if params.iter().all(|parameter| { | ||
| parameter.mode == CodegenFunctionParamMode::Required && parameter.name.is_some() | ||
| }) => |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (5)
mise.toml (1)
17-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winPin CMake and Ninja in
mise.toml.
cmake = "latest"andninja = "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 valueUse the existing
GeneratorIdent::DecodedLocalinstead of the literaldecoded.
decoded_localis already bound at the top of this function and used for the optional-argument path; emitting a rawdecodedhere 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 winAdd SAFETY comments and a
--libunit test for the zero-key guard.
dispatchdocuments bothunsafeblocks; the new ones here don't. Also,dispatch_handle(0, vec![])short-circuits beforecapi::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 winAdd unit tests for the new module.
FunctionHandle's Drop-based release,BamlFunction'sClonehandle sharing, and the()/tupleFunctionArgsarity 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 winAdd generator tests for the generated callable decode lambdas.
function_type_source()keeps the decode path consistent with use ofglobal::System.Threading.Tasks.TaskandTask<T>, but there is no generator test for the emittedasync (...) =>decode snippets covering bothvoidand non-void return types, with optional parameters as callable. As Rust-only code is changed here, add asemantic.rsunit 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
⛔ Files ignored due to path filters (5)
baml_language/sdks/csharp/bridge_csharp/src/Generated/V1/BamlGeneratedCodec.csis excluded by!**/generated/**baml_language/sdks/typescript/bridge_typescript/dist/native.d.tsis excluded by!**/dist/**baml_language/sdks/typescript/bridge_typescript/dist/proto.d.ts.mapis excluded by!**/dist/**,!**/*.mapbaml_language/sdks/typescript/bridge_typescript/dist/proto.jsis excluded by!**/dist/**baml_language/sdks/typescript/bridge_typescript/dist/proto.js.mapis excluded by!**/dist/**,!**/*.map
📒 Files selected for processing (69)
baml_language/crates/bex_engine/src/conversion.rsbaml_language/crates/bex_engine/src/lib.rsbaml_language/crates/bex_project/src/bex.rsbaml_language/crates/bridge_cffi/cbindgen.tomlbaml_language/crates/bridge_cffi/include/baml_cffi.hbaml_language/crates/bridge_cffi/src/api.rsbaml_language/crates/bridge_cffi/src/baml_to_host.rsbaml_language/crates/bridge_cffi/src/lib.rsbaml_language/crates/bridge_cffi/src/lib_native.rsbaml_language/crates/bridge_cffi/src/lib_wasm.rsbaml_language/crates/bridge_cffi/tests/abi_assertions.hbaml_language/crates/bridge_cffi/tests/abi_layout.cbaml_language/crates/bridge_cffi/tests/abi_layout.rsbaml_language/crates/bridge_ctypes/src/handle_table.rsbaml_language/sdk_tests/crates/cpp/function_calls/customizable/tests/test_host_callables.ccbaml_language/sdk_tests/crates/csharp/phase11_host_callable/Program.csbaml_language/sdk_tests/crates/csharp/phase11_host_callable/baml_src/ns_csharp_phase11/main.bamlbaml_language/sdk_tests/crates/csharp/src/lib.rsbaml_language/sdk_tests/crates/go/function_calls/customizable/test_host_callables_test.gobaml_language/sdk_tests/crates/java/function_calls/customizable/TestHostCallables.javabaml_language/sdk_tests/crates/python_pydantic2/function_calls/customizable/test_host_callables.pybaml_language/sdk_tests/crates/rust/function_calls/customizable/test_host_callables.rsbaml_language/sdk_tests/crates/swift/function_calls/customizable/TestHostCallables.swiftbaml_language/sdk_tests/crates/typescript/function_calls/customizable/host_callables.test.tsbaml_language/sdk_tests/fixtures/function_calls/baml_src/ns_host_callable_tests/main.bamlbaml_language/sdks/cpp/bridge_cpp/include/baml/codec.hbaml_language/sdks/cpp/bridge_cpp/include/baml/detail/call.hbaml_language/sdks/cpp/bridge_cpp/include/baml/detail/loader.hbaml_language/sdks/cpp/sdkgen_cpp/src/lib.rsbaml_language/sdks/csharp/bridge_csharp/src/Cffi/NativeApi.csbaml_language/sdks/csharp/bridge_csharp/src/Cffi/NativeTypes.csbaml_language/sdks/csharp/bridge_csharp/src/Proto/PrimitiveProtocol.csbaml_language/sdks/csharp/sdkgen_csharp/src/semantic.rsbaml_language/sdks/go/baml_go/function.gobaml_language/sdks/go/baml_go/internal/cffi/include/baml_cffi.hbaml_language/sdks/go/baml_go/native_unix.gobaml_language/sdks/go/baml_go/native_unsupported.gobaml_language/sdks/go/baml_go/native_windows.gobaml_language/sdks/go/baml_go/runtime.gobaml_language/sdks/go/sdkgen_go/src/lib.rsbaml_language/sdks/java/baml_bridge/src/main/java/baml_bridge/BamlFfi.javabaml_language/sdks/java/baml_bridge/src/main/java/baml_bridge/BamlHandle.javabaml_language/sdks/java/bridge_java/src/lib.rsbaml_language/sdks/java/sdkgen_java/src/emit.rsbaml_language/sdks/python/rust/bridge_python/src/baml_core/baml_py/__init__.pyibaml_language/sdks/python/rust/bridge_python/src/runtime.rsbaml_language/sdks/python/src/baml_bridge/baml_py.pyibaml_language/sdks/python/src/baml_bridge/proto.pybaml_language/sdks/python/tests/test_decode_handle.pybaml_language/sdks/python/tests/test_engine.pybaml_language/sdks/rust/bridge_rust/src/capi.rsbaml_language/sdks/rust/bridge_rust/src/function.rsbaml_language/sdks/rust/bridge_rust/src/lib.rsbaml_language/sdks/rust/bridge_rust/src/runtime.rsbaml_language/sdks/rust/sdkgen_rust/src/translate_ty.rsbaml_language/sdks/swift/Sources/BamlBridge/Api.swiftbaml_language/sdks/swift/Sources/BamlBridge/Handle.swiftbaml_language/sdks/swift/Sources/BamlBridge/Runtime.swiftbaml_language/sdks/swift/Sources/CBamlBridge/include/baml_cffi.hbaml_language/sdks/swift/rust/sdkgen_swift/src/emit.rsbaml_language/sdks/typescript/bridge_typescript/src/runtime.rsbaml_language/sdks/typescript/bridge_typescript/tests/test_engine.test.tsbaml_language/sdks/typescript/bridge_typescript/typescript_src/native.d.tsbaml_language/sdks/typescript/bridge_typescript/typescript_src/proto.tsbaml_language/sdks/typescript/bridge_typescript_web/src/runtime.rsbaml_language/sdks/typescript/bridge_typescript_web/typescript_src/native.tsbaml_language/sdks/typescript/bridge_typescript_web/typescript_src/wasm/bridge_web_core.d.tsmise.tomlrelease/bridge-cffi-public-exports.txt
| 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"); |
There was a problem hiding this comment.
🎯 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.
| 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, |
There was a problem hiding this comment.
🎯 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.rsRepository: 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.rsRepository: 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 -80Repository: 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.rsRepository: 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.
| let _ = writeln!( | ||
| out, | ||
| "\t\t{result_binding}, {error} := function.CallPositional(context.Background(), requiredArgs_, optionalArgs_)" | ||
| ); |
There was a problem hiding this comment.
🩺 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/srcRepository: 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 -120Repository: 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}')
PYRepository: 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.
| assert_eq!(root.matches("func _bamlEncodeCallback").count(), 2); | ||
| assert_eq!(models.matches("func _bamlEncodeCallback").count(), 2); | ||
| assert!(root.contains("_decode(value_ baml_go.Value)")); |
There was a problem hiding this comment.
📐 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.
| 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.
| 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, | ||
| }; |
There was a problem hiding this comment.
🎯 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.rsRepository: 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.rsRepository: 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.rsRepository: 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:
- 1: [feat] Java library #2799
- 2: https://github.com/BoundaryML/baml/discussions/885
- 3: https://itnext.io/type-safe-llm-response-handling-with-boundaryml-for-java-developers-4741809d9389
- 4: https://github.com/BoundaryML/baml/blob/53df3e24/engine/language_client_cffi/src/lib.rs
- 5: https://docs.rs/baml-sys
- 6: https://crates.io/crates/baml-sys
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.
| 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)); |
There was a problem hiding this comment.
🗄️ 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 alongsideparameter_namesand skipNoneslots for optional parameters instead of emitting anullkwarg.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.
| 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, | ||
| }) | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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.
…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 -->
Summary
BamlFunction::callsurface in RustRoot cause
Callable VM objects could not be converted into outbound bridge values. Existing
FUNCTION_REFhandles 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_REFrows. 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.BamlApiV1gains an append-onlycall_handlefield; WASM and JNI expose equivalent dispatch.The ADR is recorded locally at
/Users/sam/thoughts/sam-projects/bridge-closures/0001-host-invoked-baml-closures.mdand is grounded in the Python and TypeScript calling conventions documented bybridge-ref.Validation
cargo fmtand Clippy with all targets and featuresfunction_callsclosure tests in Python, Node TypeScript, browser TypeScript, Workers TypeScript, Go, C++, Rust, Swift, C#, and Javagradle checkSummary by CodeRabbit