Skip to content

baml_language: implement baml run and baml pack (BEP-027)#3529

Open
codeshaunted wants to merge 1 commit into
canaryfrom
avery/pack
Open

baml_language: implement baml run and baml pack (BEP-027)#3529
codeshaunted wants to merge 1 commit into
canaryfrom
avery/pack

Conversation

@codeshaunted
Copy link
Copy Markdown
Collaborator

@codeshaunted codeshaunted commented May 14, 2026

Summary

  • Implements baml run and baml pack per BEP-027 (standalone execution).
  • baml run dispatches positional namespace mains, --function <name>, -e <expression>, and baml.toml [scripts] aliases through a shared baml_exec dispatcher.
  • baml pack bakes any non-expression target into a self-contained executable via libsui (host binary baml-pack-host with a bitcode-serialized PackEnvelope embedded in an OS-native section).
  • JSON I/O on both input (--json-args) and output is routed through user-overridable baml.json.serialize / baml.json.deserialize so to_json / from_json overrides on user classes are honored at the CLI boundary.

What's new

  • baml_exec crate — shared executor (auto-CLI flag parsing, JSON coercion, dispatch).
  • baml_pack_host crate — runtime binary that decodes the embedded PackEnvelope and reuses the same dispatcher.
  • baml.sys.exit(code) builtin + EngineError::Exit { code }.
  • BexEngine::set_argv / argv() getter and type-args threading (FunctionCallContextBuilder::with_type_args, PendingNativeEntry) so native entry points work without bytecode.
  • baml-cli pack with did-you-mean and cross-compilation via target triple (downloads matching host from the BAML GitHub release, sha256-verified).
  • --list distinguishes "Namespace mains" (namespaces that have a main) from "Functions" in both debug and JSON output.

Spec-conformance fixes from the audit rounds

  • Mutex across <target> / --function / -e / --json-args dispatch modes.
  • Reserved help parameter rejection (validate_help_param runs on both baml run and baml pack).
  • Malformed baml.toml continues with an empty script set + warning rather than erroring.
  • Auto-CLI now rejects every type it can't faithfully represent (class / list / map / union / media / engine-internal) with a --json-args pointer — the previous catchall silently String-coerced these.
  • ExitCode::TargetError = 1, aligned across baml run and the packed runtime (BEP-027 §"Exit codes" mandates non-zero; 1 is the Unix convention).
  • --list empty-targets case honors --output-format json, emitting {"scripts":[], "namespace_mains":[], "functions":[]} instead of the human-readable text.
  • baml pack -e '<expr>' rejected with a clear "expression mode is not packageable" message instead of a confusing clap parse error.
  • Did-you-mean filters to function display names only.

Test plan

  • cargo nextest r --no-fail-fast --no-default-features --features ring-crypto -p baml_cli -p baml_exec — 139 tests pass.
  • cargo clippy --workspace --all-targets -- -D warnings clean.
  • cargo stow --check clean (added baml_exec and baml_pack_host to the same surface-area allowlist as baml_cli).
  • Smoke-tested baml run --list --output-format json on an empty project (returns parseable JSON, not text).
  • Smoke-tested baml pack -e '2+2' (exits 1 with the spec-cited rejection message).
  • Smoke-tested baml run --list showing Namespace mains and Functions sections on a project with namespaces.
  • End-to-end smoke: pack a function, run the resulting binary, verify argv[1] matches the spec.

Summary by CodeRabbit

  • New Features
    • Added baml pack command to create standalone executables from BAML targets
    • Added baml.sys.exit(code) function for process termination control
    • Added serialize<T> and deserialize<T> JSON utility functions
    • Auto-generated --help for typed function entry points with example invocations

Review Change Stack

Adds standalone execution per BEP-027. `baml run` dispatches positional
namespace mains, `--function <name>`, `-e <expression>`, and `baml.toml`
`[scripts]` aliases through a shared dispatcher. `baml pack` bakes any
non-expression target into a self-contained executable via libsui.

Major pieces:
- baml_exec: shared dispatcher, auto-CLI flag derivation from function
  signatures, JSON I/O routed through user-overridable
  `baml.json.serialize` / `baml.json.deserialize` (so `to_json` /
  `from_json` overrides are honored on both input and output)
- baml_pack_host: runtime host binary that extracts the bitcode-
  serialized PackEnvelope embedded by `baml pack` and invokes the
  baked-in target with the same dispatcher
- baml_cli/run_command.rs: rewrite delegating to baml_exec
- baml_cli/pack_command.rs: new `baml pack` entry point with
  did-you-mean and target-triple cross-compilation
- baml.sys.exit(code) builtin + EngineError::Exit { code }
- BexEngine::set_argv / argv() and type-args threading
  (FunctionCallContextBuilder::with_type_args) for native entry points

Spec-conformance fixes from the audit rounds:
- Mutex of --function / -e / positional / --json-args dispatch modes
- Reserved `help` param rejection (validate_help_param)
- Malformed `baml.toml` continues with empty script set + warning
- Auto-CLI rejects all types it can't faithfully represent (class /
  list / map / union / media / engine-internal) with a `--json-args`
  pointer; the previous catchall silently String-coerced them
- ExitCode::TargetError = 1, aligned across `baml run` and packed
  binaries (BEP-027 §"Exit codes" only mandates non-zero; 1 is the
  Unix convention and the packed runtime already used it)
- `--list` empty-targets case honors `--output-format json`, emitting
  `{"scripts":[], "namespace_mains":[], "functions":[]}` instead of
  the human-readable "No runnable targets found." text
- `baml pack -e '<expr>'` rejected with a clear "expression mode is
  not packageable" message instead of a confusing clap parse error
- `--list` reports "Namespace mains" (namespaces with a `main`) as a
  distinct section from "Functions"; both debug and JSON outputs
- Did-you-mean filters to function display names only

stow.toml: `baml_exec` and `baml_pack_host` join `baml_cli` as surface-
area crates that may use `anyhow` and depend directly on `bex_*`.

Tests: 139 across baml_cli + baml_exec, plus engine-level argv coverage.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 14, 2026

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

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment May 14, 2026 10:42pm
promptfiddle Ready Ready Preview, Comment May 14, 2026 10:42pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

📝 Walkthrough

Walkthrough

This PR implements standalone executable packaging for BAML programs (BEP-027) by adding Serde serialization across the entire VM type system, introducing a baml.sys.exit(code) syscall, and creating a new baml pack command that compiles BAML targets into self-contained binaries via a pack-host runtime.

Changes

BEP-027: Standalone Executable Packaging

Layer / File(s) Summary
Serde serialization traits: core types and imports
baml_language/Cargo.toml, baml_language/crates/baml_base/Cargo.toml, baml_language/crates/baml_type/Cargo.toml, baml_language/crates/bex_vm_types/Cargo.toml, baml_language/crates/baml_base/src/attr.rs, baml_language/crates/baml_base/src/core_types.rs, baml_language/crates/baml_type/src/lib.rs, baml_language/crates/baml_type/src/template.rs
Add Serde dependency declarations and derive Serialize/Deserialize on core AST, type, and attribute types (FileId, Span, TypePath, MediaKind, Literal, ModuleId, Severity, TyAttr, TyAttrValue, TyAssert, TypeName, Ty, TyTemplate) across baml_base and baml_type crates.
VM bytecode and value type serialization
baml_language/crates/bex_vm_types/src/bytecode.rs, baml_language/crates/bex_vm_types/src/heap_ptr.rs, baml_language/crates/bex_vm_types/src/indexable.rs, baml_language/crates/bex_vm_types/src/types.rs
Add Serde derives to VM bytecode instructions (Instruction, OpCode, JumpTableData, MatchHashTable), operations (BinOp, CmpOp, UnaryOp), and value structures (Value, Instance, Class, Enum, Function, Program); implement custom Serialize/Deserialize for HeapPtr (always null), generic Index<K> and Pool<T, K> wrappers, and runtime-only objects (Object, Future) that explicitly error on serialization.
Exit syscall: builtin through engine to process termination
baml_language/crates/baml_builtins2/baml_std/baml/ns_json/json.baml, baml_language/crates/baml_builtins2/baml_std/baml/ns_sys/sys.baml, baml_language/crates/baml_builtins2/baml_std/baml/ns_panics/panics.baml, baml_language/crates/baml_builtins2_codegen/src/codegen.rs, baml_language/crates/baml_builtins2_codegen/src/codegen_io.rs, baml_language/crates/bex_vm/src/errors.rs, baml_language/crates/bex_vm/src/package_baml/sys.rs, baml_language/crates/bex_vm/src/vm.rs, baml_language/crates/bex_engine/src/function_call_context.rs, baml_language/crates/bex_engine/src/lib.rs, baml_language/crates/bex_engine/tests/host_argv.rs
Add baml.sys.exit(code) builtin returning never and baml.panics.Exit exception class; implement VM-level exit panic (VmPanic::Exit), engine error mapping (EngineError::Exit), and entry point dispatch with type arguments and argv context; add exit code extraction from uncaught panics and threading to process exit; add argv getter/setter and source_file metadata for runtime function info; implement deferred native entry dispatch and panic-to-exception conversion for exit codes.
baml_exec crate: CLI parsing, dispatch, and output formatting
baml_language/crates/baml_exec/Cargo.toml, baml_language/crates/baml_exec/src/lib.rs, baml_language/crates/baml_exec/src/auto_cli.rs, baml_language/crates/baml_exec/src/dispatch.rs, baml_language/crates/baml_exec/src/envelope.rs, baml_language/crates/baml_exec/src/json_coerce.rs, baml_language/crates/baml_exec/src/output.rs
Create shared execution crate with auto-CLI argument parsing (positional sugar for single-required-param functions, --name value / --name=value forms), help text generation with optional-parameter markers and type hints, JSON argument loading from stdin/file/inline, dispatch target invocation with CLI/JSON argument merging and precedence, and output formatting supporting debug (human-readable) and JSON (via baml.json.serialize) modes.
baml pack command: target resolution, compilation, and executable creation
baml_language/crates/baml_cli/Cargo.toml, baml_language/crates/baml_cli/src/commands.rs, baml_language/crates/baml_cli/src/lib.rs, baml_language/crates/baml_cli/src/pack_command.rs
Implement baml pack command with Clap argument parsing (PackArgs); add target resolution for positional targets (main, .baml files, namespace targets) and --function selection with mutual exclusion; validate targets via compilation diagnostic checking; create PackEnvelope containing compiled program, target name, identifier, and output format; implement host binary acquisition (detect, download from GitHub release on mismatch, verify SHA-256 checksum, extract from tar.gz/zip); use libsui to inject envelope into OS-native section (ELF/PE/Mach-O); add ExitCode::TargetError variant for packed-binary failures.
baml-pack-host standalone runtime binary
baml_language/crates/baml_pack_host/Cargo.toml, baml_language/crates/baml_pack_host/build.rs, baml_language/crates/baml_pack_host/src/main.rs
Create standalone baml-pack-host binary that extracts PackEnvelope from OS-native baml_pack section, initializes BexEngine with embedded program and constructed baml.argv, conditionally handles --help/-h for typed targets via derived print_target_help, asynchronously dispatches target execution, and maps DispatchResult outcomes to process exit codes (success → 0, target error → 1, exit(code) → clamped code); add macOS linker flag for Mach-O header padding to prevent libsui segment overflow.
Workspace dependency and validation configuration
baml_language/stow.toml
Expand anyhow allowlist to include baml_exec and baml_pack_host as surface-area boundary crates; permit baml_exec and baml_pack_host to depend on bex_* crates (alongside baml_cli and bridge_cffi); add exceptions in surface namespace to treat these new crates as application-boundary entry points exempt from standard baml-namespace bex_* dependency restrictions.

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

  • BoundaryML/baml#3457: The main PR's Serde integration in bex_vm_types/src/bytecode.rs explicitly updates the Bytecode/compact serialization model, which directly builds on the compact-bytecode types (CompactCode, OpCode, Bytecode::lower_to_compact) introduced by the retrieved perf PR.
  • BoundaryML/baml#3205: Main PR adds serde::Serialize/Deserialize derives to the TyAttr/TyAttrValue/TyAssert types in baml_base/src/attr.rs, which directly complements PR #3205's new ty_attr: TyAttr fields on enum/class definitions.
  • BoundaryML/baml#3342: The main PR's new baml.sys.exit/baml.panics.Exit handling changes the VM/engine panic surface (VmPanic::ExitEngineError::Exit), while the retrieved PR refactors the VM error taxonomy and extends VmPanic (e.g., adds VmPanic::AllocFailure) in the same error/panic model (bex_vm/src/errors.rs), making the changes code-level related.

🐰 Program bytecode now packs into binaries,
Exit codes flow through panics so cleanly,
CLI dispatch sings with type-safe harmony,
Standalone worlds spin up so nimbly! 🎁

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and specifically describes the main implementation: adding baml run and baml pack functionality per BEP-027. It is concise, directly related to the substantial changes across multiple crates, and conveys the primary objective without unnecessary noise.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch avery/pack

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

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (1)
baml_language/crates/bex_vm_types/src/types.rs (1)

619-625: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not serialize Value::OmittedArg.

Line 624 says this sentinel is only valid during argument binding and "must not be serialized or exposed to host code". Deriving serde for the whole enum turns it into a normal wire value. Please switch Value to the same proxy-pattern used for Object/FunctionKind and return a serde error for OmittedArg.

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

In `@baml_language/crates/bex_vm_types/src/types.rs` around lines 619 - 625, The
Value enum derives Serialize/Deserialize but must not allow serializing the
sentinel Value::OmittedArg; replace the derive with custom impls: implement
Serialize and Deserialize for Value following the proxy-pattern used for
Object/FunctionKind (create an internal serializable proxy representation for
the valid variants and map to/from Value), and in both impls return a serde
error if encountering Value::OmittedArg (during Serialize) or if the
deserialized proxy would map to OmittedArg (during Deserialize); update/remove
the #[derive(...)] on Value and ensure the impls reference the Value enum and
its OmittedArg variant explicitly.
🤖 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/Cargo.toml`:
- Line 118: The dependency entry bitcode = { version = "0.6", features = [
"serde" ] } is enabling serde-based serialization which may conflict with the
crate's native Encode/Decode and hurt performance for VM type serialization;
decide whether to use bitcode's native encoding instead of serde, then update
the Cargo.toml entry for the bitcode dependency accordingly (either remove the
"serde" feature and enable the native encoding feature if provided by bitcode,
or keep "serde" if interoperability is required), and run unit tests and a
simple benchmark of VM type serialization paths (areas using Encode/Decode) to
verify behavior and performance; refer to the bitcode dependency line and the
crate's Encode/Decode usage to locate and change the configuration.

In `@baml_language/crates/baml_exec/src/auto_cli.rs`:
- Around line 65-73: In the option-value parsing branch inside auto_cli.rs where
(key, val_str) is assigned from raw or the next token, detect and reject a
following token that looks like a flag (starts with '-') instead of accepting it
as the value; specifically, after incrementing i and before using tokens[i],
check tokens[i].starts_with('-') and return an error (e.g.,
anyhow::bail!("Missing value for `--{raw}`")) to treat `--name --other=...` as a
missing-value error; update the logic around variables raw, i, tokens, key, and
val_str so the existing code path still uses tokens[i] when valid but fails fast
when the next token is a flag.

In `@baml_language/crates/baml_exec/src/dispatch.rs`:
- Around line 31-41: validate_help_param currently swallows lookup failures (if
let Ok(...)) and is called with the raw target_name in dispatch_target which can
be non-canonical; update callsite and function to use the resolved canonical
function name and propagate errors: in dispatch_target, resolve func_info first
(the existing func_info lookup) and then call validate_help_param(&engine,
func_info.name()) instead of validate_help_param(&engine, target_name); inside
validate_help_param use engine.function_params(function_name)? (or handle the
Err by returning it) rather than if let Ok(...) so missing/invalid targets don’t
skip validation, then check params.iter().any(|(name, _, _)| *name == "help")
and bail as before if found.

In `@baml_language/crates/bex_vm_types/src/heap_ptr.rs`:
- Around line 153-164: The current Serialize and Deserialize impls for HeapPtr
silently round-trip to a null pointer; instead, make both fail at the serde
boundary: in impl Serialize for HeapPtr, return
Err(serde::ser::Error::custom("HeapPtr is a runtime-only pointer and must not be
serialized")) rather than serializer.serialize_unit(); in impl<'de>
Deserialize<'de> for HeapPtr, return Err(D::Error::custom("HeapPtr cannot be
deserialized: runtime pointer leaked into serialized data")) rather than
producing HeapPtr::null(); reference the impl blocks for Serialize/Deserialize
and the HeapPtr::null symbol when making the changes.

In `@baml_language/crates/bex_vm/src/vm.rs`:
- Around line 360-364: pending_native_entry (Option<PendingNativeEntry>) holds
Vec<Value> outside stack/frames so GC can move/reclaim heap-backed args before
dispatch_native_entry; update BexVm::collect_roots and BexVm::forward_roots to
also walk self.pending_native_entry when Some(..), treating each Value as a
root/forwardable slot (mirror how stack/frames are handled), ensuring you
mark/visit and update any HeapPtr/heap-backed Values in that Vec; keep handling
of Option and ensure set_entry_point_with_type_args stores Values in
PendingNativeEntry consistently so exec/dispatch_native_entry reads forwarded
pointers.
- Around line 2207-2214: The early return when handling pending_native_entry
bypasses the standard VmError::InternalError → VmError::TracedInternalError
conversion; instead of doing `if let Some(entry) =
self.pending_native_entry.take() { return self.dispatch_native_entry(&entry);
}`, call `dispatch_native_entry` without returning immediately (e.g., take the
entry, invoke `self.dispatch_native_entry(&entry)` and assign its Result to a
local variable) and let the normal error-wrapping logic that follows run so
`$rust_function` entry points (including the unsupported `YieldToCall` case from
`dispatch_native_entry`) produce the same traced/internal error variant as
bytecode-dispatched entries.
- Around line 1019-1027: dispatch_native_entry currently treats
NativeCallResult::YieldToCall as an error, which makes yielding builtins invoked
via set_entry_point_with_type_args (e.g., baml.json.to_string<T>) fail at first
exec; fix by either (A) preventing yielding natives from being installed as
entry points in set_entry_point_with_type_args: detect native callees that can
yield (use the callee's may_yield/attribute or check if invoking the native can
return NativeCallResult::YieldToCall) and return an error/Refuse to set the
entry, or (B) extend dispatch_native_entry to handle YieldToCall for native
entry stubs by synthesizing an initial frame or scheduling the yielded call
chain so the VM can continue execution (i.e., convert YieldToCall into creating
the next call frame(s) instead of erroring). Update
set_entry_point_with_type_args and dispatch_native_entry to consistently enforce
the chosen approach and reference NativeCallResult::YieldToCall when
guarding/handling the case.

---

Outside diff comments:
In `@baml_language/crates/bex_vm_types/src/types.rs`:
- Around line 619-625: The Value enum derives Serialize/Deserialize but must not
allow serializing the sentinel Value::OmittedArg; replace the derive with custom
impls: implement Serialize and Deserialize for Value following the proxy-pattern
used for Object/FunctionKind (create an internal serializable proxy
representation for the valid variants and map to/from Value), and in both impls
return a serde error if encountering Value::OmittedArg (during Serialize) or if
the deserialized proxy would map to OmittedArg (during Deserialize);
update/remove the #[derive(...)] on Value and ensure the impls reference the
Value enum and its OmittedArg variant explicitly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3f1aab63-cb82-449a-9568-ce13fcb06e18

📥 Commits

Reviewing files that changed from the base of the PR and between 31fbb80 and db1d36e.

⛔ Files ignored due to path filters (2)
  • baml_language/Cargo.lock is excluded by !**/*.lock
  • baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_package_listing.snap is excluded by !**/*.snap
📒 Files selected for processing (39)
  • baml_language/Cargo.toml
  • baml_language/crates/baml_base/Cargo.toml
  • baml_language/crates/baml_base/src/attr.rs
  • baml_language/crates/baml_base/src/core_types.rs
  • baml_language/crates/baml_builtins2/baml_std/baml/ns_json/json.baml
  • baml_language/crates/baml_builtins2/baml_std/baml/ns_panics/panics.baml
  • baml_language/crates/baml_builtins2/baml_std/baml/ns_sys/sys.baml
  • baml_language/crates/baml_builtins2_codegen/src/codegen.rs
  • baml_language/crates/baml_builtins2_codegen/src/codegen_io.rs
  • baml_language/crates/baml_cli/Cargo.toml
  • baml_language/crates/baml_cli/src/commands.rs
  • baml_language/crates/baml_cli/src/lib.rs
  • baml_language/crates/baml_cli/src/pack_command.rs
  • baml_language/crates/baml_cli/src/run_command.rs
  • baml_language/crates/baml_exec/Cargo.toml
  • baml_language/crates/baml_exec/src/auto_cli.rs
  • baml_language/crates/baml_exec/src/dispatch.rs
  • baml_language/crates/baml_exec/src/envelope.rs
  • baml_language/crates/baml_exec/src/json_coerce.rs
  • baml_language/crates/baml_exec/src/lib.rs
  • baml_language/crates/baml_exec/src/output.rs
  • baml_language/crates/baml_pack_host/Cargo.toml
  • baml_language/crates/baml_pack_host/build.rs
  • baml_language/crates/baml_pack_host/src/main.rs
  • baml_language/crates/baml_type/Cargo.toml
  • baml_language/crates/baml_type/src/lib.rs
  • baml_language/crates/baml_type/src/template.rs
  • baml_language/crates/bex_engine/src/function_call_context.rs
  • baml_language/crates/bex_engine/src/lib.rs
  • baml_language/crates/bex_engine/tests/host_argv.rs
  • baml_language/crates/bex_vm/src/errors.rs
  • baml_language/crates/bex_vm/src/package_baml/sys.rs
  • baml_language/crates/bex_vm/src/vm.rs
  • baml_language/crates/bex_vm_types/Cargo.toml
  • baml_language/crates/bex_vm_types/src/bytecode.rs
  • baml_language/crates/bex_vm_types/src/heap_ptr.rs
  • baml_language/crates/bex_vm_types/src/indexable.rs
  • baml_language/crates/bex_vm_types/src/types.rs
  • baml_language/stow.toml

Comment thread baml_language/Cargo.toml
aws-smithy-types = { git = "https://github.com/boundaryml/aws-sdk-rust.git", rev = "28d4f67bac1214320320905c1f6908ea32b6b0ac" }
aws-types = { git = "https://github.com/boundaryml/aws-sdk-rust.git", rev = "28d4f67bac1214320320905c1f6908ea32b6b0ac" }
axum = { version = "0.8.4", features = [ "ws" ] }
bitcode = { version = "0.6", features = [ "serde" ] }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify bitcode crate version and features
echo "=== Checking bitcode latest version ==="
cargo search bitcode --limit 1

echo -e "\n=== Checking bitcode 0.6 features ==="
cargo metadata --format-version=1 2>/dev/null | jq -r '.packages[] | select(.name == "bitcode" and (.version | startswith("0.6"))) | .features | keys[]' || echo "Package not in current Cargo.lock"

Repository: BoundaryML/baml

Length of output: 533


🌐 Web query:

bitcode rust crate version 0.6 serde feature documentation

💡 Result:

For the Rust crate bitcode version 0.6.x, the serde support is controlled by the crate feature named "serde". A "serde" integration is gated behind the "serde" feature flag [1][2]. Version 0.6.9 feature set (docs.rs / crates.io): - Features present include: default, derive, std, arrayvec, glam, rust_decimal, serde, time, uuid [3]. - The docs for the serde feature explain it is gated behind "serde" [2]. - The crate-level feature table shows default enables "derive" and "std" [4], while "std" conditionally enables std features for several optional deps including serde via serde?/std [4]. What this means when using bitcode 0.6: - Enable serde integration by turning on the "serde" cargo feature for bitcode [1][2]. - Note that the serde integration is not the same as bitcode’s native Encode/Decode derive system; the serde wiki states that it is not compatible with the native bitcode::{Encode, Decode} format and is slower with slightly larger output [5]. If you’re specifically looking for the serde feature documentation for bitcode 0.6.9, start from the crate docs (they contain the serde section and gating statement) and the feature flags page (which lists the serde feature among others) [2][3].

Citations:


Verify bitcode's serde feature is the right serialization approach.

Version 0.6.9 is current and the serde feature is documented and available in bitcode 0.6.x. However, bitcode's serde integration is incompatible with the crate's native Encode/Decode serialization and incurs a performance penalty with larger output. For VM type serialization, confirm whether native bitcode encoding should be used instead of serde mode, or if serde is intentional for interoperability reasons.

🤖 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/Cargo.toml` at line 118, The dependency entry bitcode = {
version = "0.6", features = [ "serde" ] } is enabling serde-based serialization
which may conflict with the crate's native Encode/Decode and hurt performance
for VM type serialization; decide whether to use bitcode's native encoding
instead of serde, then update the Cargo.toml entry for the bitcode dependency
accordingly (either remove the "serde" feature and enable the native encoding
feature if provided by bitcode, or keep "serde" if interoperability is
required), and run unit tests and a simple benchmark of VM type serialization
paths (areas using Encode/Decode) to verify behavior and performance; refer to
the bitcode dependency line and the crate's Encode/Decode usage to locate and
change the configuration.

Comment on lines +65 to +73
let (key, val_str) = if let Some(eq_pos) = raw.find('=') {
(&raw[..eq_pos], &raw[eq_pos + 1..])
} else {
i += 1;
if i >= tokens.len() {
anyhow::bail!("Missing value for `--{raw}`");
}
(raw, tokens[i].as_str())
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Treat a following flag token as a missing value for --name value form.

--name --other=... is currently parsed as name="--other=..." instead of erroring. That silently misbinds args (especially for string) and hides the real CLI mistake.

Proposed fix
         let (key, val_str) = if let Some(eq_pos) = raw.find('=') {
             (&raw[..eq_pos], &raw[eq_pos + 1..])
         } else {
             i += 1;
-            if i >= tokens.len() {
+            if i >= tokens.len() || tokens[i].starts_with("--") {
                 anyhow::bail!("Missing value for `--{raw}`");
             }
             (raw, tokens[i].as_str())
         };
📝 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
let (key, val_str) = if let Some(eq_pos) = raw.find('=') {
(&raw[..eq_pos], &raw[eq_pos + 1..])
} else {
i += 1;
if i >= tokens.len() {
anyhow::bail!("Missing value for `--{raw}`");
}
(raw, tokens[i].as_str())
};
let (key, val_str) = if let Some(eq_pos) = raw.find('=') {
(&raw[..eq_pos], &raw[eq_pos + 1..])
} else {
i += 1;
if i >= tokens.len() || tokens[i].starts_with("--") {
anyhow::bail!("Missing value for `--{raw}`");
}
(raw, tokens[i].as_str())
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/baml_exec/src/auto_cli.rs` around lines 65 - 73, In the
option-value parsing branch inside auto_cli.rs where (key, val_str) is assigned
from raw or the next token, detect and reject a following token that looks like
a flag (starts with '-') instead of accepting it as the value; specifically,
after incrementing i and before using tokens[i], check
tokens[i].starts_with('-') and return an error (e.g., anyhow::bail!("Missing
value for `--{raw}`")) to treat `--name --other=...` as a missing-value error;
update the logic around variables raw, i, tokens, key, and val_str so the
existing code path still uses tokens[i] when valid but fails fast when the next
token is a flag.

Comment on lines +31 to +41
pub fn validate_help_param(engine: &BexEngine, function_name: &str) -> Result<()> {
if let Ok(params) = engine.function_params(function_name) {
if params.iter().any(|(name, _, _)| *name == "help") {
anyhow::bail!(
"Target `{function_name}` declares a parameter named `help`, \
which collides with the auto-derived `--help` flag. \
Rename this parameter to be used as an entry point."
);
}
}
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

help-parameter validation can be bypassed by non-canonical target names.

dispatch_target resolves func_info first, but then validates with raw target_name. Combined with the if let Ok(...) in validate_help_param, lookup failures skip validation entirely.

Proposed fix
-    validate_help_param(&engine, target_name)?;
+    validate_help_param(&engine, &func_info.qualified_name)?;
 pub fn validate_help_param(engine: &BexEngine, function_name: &str) -> Result<()> {
-    if let Ok(params) = engine.function_params(function_name) {
-        if params.iter().any(|(name, _, _)| *name == "help") {
-            anyhow::bail!(
-                "Target `{function_name}` declares a parameter named `help`, \
-                 which collides with the auto-derived `--help` flag. \
-                 Rename this parameter to be used as an entry point."
-            );
-        }
+    let params = engine
+        .function_params(function_name)
+        .with_context(|| format!("Failed to load params for target `{function_name}`"))?;
+    if params.iter().any(|(name, _, _)| *name == "help") {
+        anyhow::bail!(
+            "Target `{function_name}` declares a parameter named `help`, \
+             which collides with the auto-derived `--help` flag. \
+             Rename this parameter to be used as an entry point."
+        );
     }
     Ok(())
 }

Also applies to: 72-72

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

In `@baml_language/crates/baml_exec/src/dispatch.rs` around lines 31 - 41,
validate_help_param currently swallows lookup failures (if let Ok(...)) and is
called with the raw target_name in dispatch_target which can be non-canonical;
update callsite and function to use the resolved canonical function name and
propagate errors: in dispatch_target, resolve func_info first (the existing
func_info lookup) and then call validate_help_param(&engine, func_info.name())
instead of validate_help_param(&engine, target_name); inside validate_help_param
use engine.function_params(function_name)? (or handle the Err by returning it)
rather than if let Ok(...) so missing/invalid targets don’t skip validation,
then check params.iter().any(|(name, _, _)| *name == "help") and bail as before
if found.

Comment on lines +153 to +164
impl Serialize for HeapPtr {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_unit()
}
}

impl<'de> Deserialize<'de> for HeapPtr {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
<()>::deserialize(deserializer)?;
Ok(HeapPtr::null())
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject HeapPtr at the serde boundary instead of nulling it out.

This round-trip silently converts every serialized heap reference into HeapPtr::null(). If a runtime-only object ever leaks into a serialized Program, deserialization succeeds with bogus pointers and the first later deref becomes UB instead of a clean load-time failure. Failing fast here is much safer than manufacturing a placeholder pointer.

Suggested fix
 impl Serialize for HeapPtr {
-    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
-        serializer.serialize_unit()
+    fn serialize<S: serde::Serializer>(&self, _serializer: S) -> Result<S::Ok, S::Error> {
+        Err(serde::ser::Error::custom("HeapPtr cannot be serialized"))
     }
 }
 
 impl<'de> Deserialize<'de> for HeapPtr {
-    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
-        <()>::deserialize(deserializer)?;
-        Ok(HeapPtr::null())
+    fn deserialize<D: serde::Deserializer<'de>>(_deserializer: D) -> Result<Self, D::Error> {
+        Err(serde::de::Error::custom("HeapPtr cannot be deserialized"))
     }
 }
📝 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
impl Serialize for HeapPtr {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_unit()
}
}
impl<'de> Deserialize<'de> for HeapPtr {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
<()>::deserialize(deserializer)?;
Ok(HeapPtr::null())
}
}
impl Serialize for HeapPtr {
fn serialize<S: serde::Serializer>(&self, _serializer: S) -> Result<S::Ok, S::Error> {
Err(serde::ser::Error::custom("HeapPtr cannot be serialized"))
}
}
impl<'de> Deserialize<'de> for HeapPtr {
fn deserialize<D: serde::Deserializer<'de>>(_deserializer: D) -> Result<Self, D::Error> {
Err(serde::de::Error::custom("HeapPtr cannot be deserialized"))
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/bex_vm_types/src/heap_ptr.rs` around lines 153 - 164,
The current Serialize and Deserialize impls for HeapPtr silently round-trip to a
null pointer; instead, make both fail at the serde boundary: in impl Serialize
for HeapPtr, return Err(serde::ser::Error::custom("HeapPtr is a runtime-only
pointer and must not be serialized")) rather than serializer.serialize_unit();
in impl<'de> Deserialize<'de> for HeapPtr, return Err(D::Error::custom("HeapPtr
cannot be deserialized: runtime pointer leaked into serialized data")) rather
than producing HeapPtr::null(); reference the impl blocks for
Serialize/Deserialize and the HeapPtr::null symbol when making the changes.

Comment on lines +360 to +364
/// Set when the host invokes a `$rust_function` callee as the entry
/// point. Such callees have no bytecode body, so the exec loop's first
/// step dispatches this native call directly and produces a `Complete`
/// state with its return value instead of reading bytecode.
pending_native_entry: Option<PendingNativeEntry>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Root deferred native-entry args through GC.

pending_native_entry stores Vec<Value> outside stack/frames, but BexVm::collect_roots() and BexVm::forward_roots() only walk the stack, watch state, and frames. If GC runs after set_entry_point_with_type_args() but before the first exec(), any heap-backed argument here can be moved or reclaimed, and dispatch_native_entry() will read stale HeapPtrs.

Suggested fix
+impl RootHaver for PendingNativeEntry {
+    fn collect_roots(&self, roots: &mut Vec<HeapPtr>) {
+        for value in &self.args {
+            if let Value::Object(ptr) = value {
+                roots.push(*ptr);
+            }
+        }
+    }
+
+    fn forward_roots(&mut self, roots: &HashMap<HeapPtr, HeapPtr>) {
+        for value in &mut self.args {
+            if let Value::Object(ptr) = value {
+                if let Some(&new_ptr) = roots.get(ptr) {
+                    *ptr = new_ptr;
+                }
+            }
+        }
+    }
+}

Also thread self.pending_native_entry through BexVm::collect_roots() and BexVm::forward_roots().

Also applies to: 367-379

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

In `@baml_language/crates/bex_vm/src/vm.rs` around lines 360 - 364,
pending_native_entry (Option<PendingNativeEntry>) holds Vec<Value> outside
stack/frames so GC can move/reclaim heap-backed args before
dispatch_native_entry; update BexVm::collect_roots and BexVm::forward_roots to
also walk self.pending_native_entry when Some(..), treating each Value as a
root/forwardable slot (mirror how stack/frames are handled), ensuring you
mark/visit and update any HeapPtr/heap-backed Values in that Vec; keep handling
of Option and ensure set_entry_point_with_type_args stores Values in
PendingNativeEntry consistently so exec/dispatch_native_entry reads forwarded
pointers.

Comment on lines +1019 to +1027
/// Like [`Self::set_entry_point`], but seeds the entry frame's
/// `type_args` slot. Use when the host invokes a generic function
/// (e.g. `baml.json.to_string<T>`) and needs to thread `T` through.
///
/// Native (`$rust_function`) callees have no bytecode body, so for
/// those we synthesize a single-frame stub that produces the native
/// call's return value on the first `exec()` step instead of pushing
/// a bytecode frame that would read an empty instruction stream.
pub fn set_entry_point_with_type_args(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Native helpers that can yield into BAML:"
rg -n -C2 'NativeCallResult::YieldToCall' \
  baml_language/crates/bex_vm/src/package_baml \
  baml_language/crates/baml_builtins2_codegen/src \
  baml_language/crates/baml_exec/src

echo
echo "JSON/native entrypoints referenced by this change:"
rg -n -C2 'baml\.json\.(to_string|from_string|serialize|deserialize)|to_json|from_json' \
  baml_language/crates/baml_builtins2 \
  baml_language/crates/baml_builtins2_codegen/src

Repository: BoundaryML/baml

Length of output: 27722


🏁 Script executed:

cd baml_language/crates/bex_vm/src && sed -n '2164,2192p' vm.rs

Repository: BoundaryML/baml

Length of output: 1530


baml.json.to_string<T> and other yielding builtins fail when invoked as entry points.

set_entry_point_with_type_args() supports host-invoked generic builtins like baml.json.to_string<T>, but dispatch_native_entry() rejects any NativeCallResult::YieldToCall as an error. The JSON serialization path (baml.json.to_json<T>, which to_string<T> calls) is marked //baml:may_yield and returns YieldToCall for nested type dispatch. Similarly, Array<T>.to_json and Map.to_json yield internally. Any of these used as entry points will fail on first exec() with "native entry-point YieldToCall is not supported".

Restrict entry point support to non-yielding builtins, or refactor native entry points to handle yielding dispatches before the first frame.

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

In `@baml_language/crates/bex_vm/src/vm.rs` around lines 1019 - 1027,
dispatch_native_entry currently treats NativeCallResult::YieldToCall as an
error, which makes yielding builtins invoked via set_entry_point_with_type_args
(e.g., baml.json.to_string<T>) fail at first exec; fix by either (A) preventing
yielding natives from being installed as entry points in
set_entry_point_with_type_args: detect native callees that can yield (use the
callee's may_yield/attribute or check if invoking the native can return
NativeCallResult::YieldToCall) and return an error/Refuse to set the entry, or
(B) extend dispatch_native_entry to handle YieldToCall for native entry stubs by
synthesizing an initial frame or scheduling the yielded call chain so the VM can
continue execution (i.e., convert YieldToCall into creating the next call
frame(s) instead of erroring). Update set_entry_point_with_type_args and
dispatch_native_entry to consistently enforce the chosen approach and reference
NativeCallResult::YieldToCall when guarding/handling the case.

Comment on lines +2207 to +2214
// Native (`$rust_function`) entry point set by
// `set_entry_point_with_type_args` — no bytecode to interpret, so
// dispatch the native call here and surface its result as
// `Complete`. Errors flow through the panic/throw machinery just
// like a bytecode-dispatched native call would.
if let Some(entry) = self.pending_native_entry.take() {
return self.dispatch_native_entry(&entry);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep deferred native entries on the normal error-wrapping path.

The early return on Line 2212 bypasses the VmError::InternalErrorVmError::TracedInternalError conversion immediately below. That makes $rust_function entry points report a different error variant than bytecode entry points, including the unsupported-YieldToCall case from dispatch_native_entry().

Suggested fix
-        if let Some(entry) = self.pending_native_entry.take() {
-            return self.dispatch_native_entry(&entry);
-        }
-
-        match self.exec_inner() {
+        let result = if let Some(entry) = self.pending_native_entry.take() {
+            self.dispatch_native_entry(&entry)
+        } else {
+            self.exec_inner()
+        };
+
+        match result {
             Err(VmError::InternalError(err)) => {
                 let trace = self.capture_stack_trace();
                 Err(VmError::TracedInternalError { source: err, trace })
             }
             other => other,
📝 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
// Native (`$rust_function`) entry point set by
// `set_entry_point_with_type_args` — no bytecode to interpret, so
// dispatch the native call here and surface its result as
// `Complete`. Errors flow through the panic/throw machinery just
// like a bytecode-dispatched native call would.
if let Some(entry) = self.pending_native_entry.take() {
return self.dispatch_native_entry(&entry);
}
// Native (`$rust_function`) entry point set by
// `set_entry_point_with_type_args` — no bytecode to interpret, so
// dispatch the native call here and surface its result as
// `Complete`. Errors flow through the panic/throw machinery just
// like a bytecode-dispatched native call would.
let result = if let Some(entry) = self.pending_native_entry.take() {
self.dispatch_native_entry(&entry)
} else {
self.exec_inner()
};
match result {
Err(VmError::InternalError(err)) => {
let trace = self.capture_stack_trace();
Err(VmError::TracedInternalError { source: err, trace })
}
other => other,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/bex_vm/src/vm.rs` around lines 2207 - 2214, The early
return when handling pending_native_entry bypasses the standard
VmError::InternalError → VmError::TracedInternalError conversion; instead of
doing `if let Some(entry) = self.pending_native_entry.take() { return
self.dispatch_native_entry(&entry); }`, call `dispatch_native_entry` without
returning immediately (e.g., take the entry, invoke
`self.dispatch_native_entry(&entry)` and assign its Result to a local variable)
and let the normal error-wrapping logic that follows run so `$rust_function`
entry points (including the unsupported `YieldToCall` case from
`dispatch_native_entry`) produce the same traced/internal error variant as
bytecode-dispatched entries.

@github-actions
Copy link
Copy Markdown

Binary size checks failed

1 violations · ✅ 6 passed

⚠️ Please fix the size gate issues or acknowledge them by updating baselines.

Artifact Platform Gzip Baseline Delta Status
bridge_cffi Linux 6.6 MB 6.4 MB +158.4 KB (+2.5%) FAIL
bridge_cffi-stripped Linux 5.6 MB 5.7 MB -46.9 KB (-0.8%) OK
bridge_cffi macOS 5.4 MB 5.1 MB +330.8 KB (+6.5%) OK
bridge_cffi-stripped macOS 4.6 MB 4.7 MB -46.2 KB (-1.0%) OK
bridge_cffi Windows 5.4 MB 5.1 MB +286.4 KB (+5.6%) OK
bridge_cffi-stripped Windows 4.7 MB 4.7 MB +23.8 KB (+0.5%) OK
bridge_wasm WASM 3.6 MB 3.5 MB +44.2 KB (+1.3%) OK
Details & how to fix

Violations:

  • bridge_cffi (Linux) gzip_bytes: 6.6 MB exceeds limit of 6.5 MB (exceeded by +94.2 KB, policy: max_gzip_bytes)

Add/update baselines:

.ci/size-gate/x86_64-unknown-linux-gnu.toml:

[artifacts.bridge_cffi]
file_bytes = 17671608
stripped_bytes = 17671600
gzip_bytes = 6594191

Generated by cargo size-gate · workflow run

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