# BAML Changelog

> The latest BAML releases, shipped continuously.

Web page: https://boundaryml.com/changelog

This feed contains published BAML release entries in newest-first order. Each entry may include:

- Version
- Release channel
- Publication date
- Release title and notes
- Authors

Release channels can include stable, nightly, canary, alpha, and other prerelease builds. Check the channel before upgrading production projects.

After upgrading BAML, use the local toolchain for version-specific information:

```sh
baml --version
baml describe
```

If the project uses an installed coding-agent skill, refresh it when required:

```sh
baml agent install
```

Published release entries follow below when the changelog data service is available.

## Install BAML with `brew install baml`

0.15.1-nightly.20260726.b · nightly · 2026-07-26

The documented Homebrew install command is now `brew install baml` instead of `brew install boundaryml/tap/baml`, reflecting BAML's availability in Homebrew Core.

```bash
brew install baml
```

This change updates the website quickstart, explore, and intro pages. No compiler or runtime behavior changed. A maintainer warning was also added to the `baml` wrapper crate's `Cargo.toml` about the release consequences of bumping its version, which has no effect on users.

Authors: codeshaunted

## Homebrew install now uses `brew install baml` from Homebrew Core

0.15.1-nightly.20260726.a · nightly · 2026-07-26

The documented Homebrew install command changed from `brew install boundaryml/tap/baml` to `brew install baml`, reflecting that the formula now lives in Homebrew Core instead of the `boundaryml/tap` tap. This is a docs-only change across the website's quickstart, explore, and intro pages, plus the `TryBaml` install picker. No toolchain or language behavior changed.

A warning comment was also added above the `version` field in `crates/baml/Cargo.toml` noting that bumping the wrapper version triggers a new `baml-wrapper-*` tag and an automated Homebrew Core version-bump PR that needs monitoring.

Authors: codeshaunted

## First-class C# SDK ships, plus new stdlib building blocks for custom LLM providers

0.15.1-nightly.20260724.a · nightly · 2026-07-25

BAML now has a first-class C# SDK: the `baml-bridge` NuGet package lets you call BAML functions from .NET, alongside the existing Python, TypeScript, Go, and Java surfaces.

```bash
dotnet add package baml-bridge
```

**Highlights**

- **C# SDK.** A generated C# client and native runtime bridge (`bridge_cffi` loaded at runtime) are wired into the release pipeline and published to nuget.org. The `baml-cli` binary grew (roughly +7%) because the C# generator links into it.
- **New stdlib for building your own providers.** Several standard-library functions are now exposed so BAML-authored LLM providers can reuse the built-in machinery:
  - `path<T>` and `path_or<T>` in `baml.json` navigate a jq-style selector (`.field`, `["key"]`, `[0]`) into a JSON value and coerce the leaf to `T`, throwing `JsonPathError` on a missing field, out-of-bounds index, or coercion failure.
  - `parse<T>` and `parse_type` in `baml.sap` expose Schema-Aligned Parsing, the same coercion BAML's LLM path uses, to your own code.
  - `json_schema(t: type)` in `baml.schema` lowers a BAML `type` to ordinary JSON Schema (optional fields dropped from `required`, unions as `anyOf`, recursive classes via `$defs`/`$ref`).
  - `baml.ws` adds a minimal text WebSocket client (`WsStream` with `send`/`next`/`close`, and `connect`) for realtime providers.
  - `UnknownError` is a new wrapper class for errors that do not map to a known capability channel.
- **Diagnostics render better and stop cascading.** CLI diagnostics now carry syntax highlighting (the renderer moved from `ariadne` to `miette` plus `owo-colors`), and editors dim BAML namespace prefixes. A batch of fixes stops one mistake from spawning a wall of follow-on errors: duplicate fields, duplicate parameters, duplicate destructure and pattern bindings, conflicting or-pattern bindings, lambda signature mismatches, match-usefulness after an invalid pattern, and unresolved class patterns now report once and recover.
- **`baml self-update` is disabled in package-manager builds.** Wrappers installed via a package manager are built with self-update compiled out; running `baml self-update` prints "self-update is disabled in this build. Update BAML with your package manager." instead of the old managed-install heuristic.
- **`baml login` gains anonymous-first auth** via WorkOS agent registration.

Authors: rossirpaulo, hellovai, codeshaunted, ATX24, antoniosarosi, sxlijin, aaronvg

## Property shorthand lets `{ options }` stand in for `{ options: options }`

0.15.1-nightly.20260723.g · nightly · 2026-07-23

Object and map literals now accept property shorthand: writing `{ options }` is equivalent to `{ options: options }`, pulling in the in-scope value whose name matches the field. Shorthand works in class constructors and map literals, and mixes freely with explicit fields.

**Highlights**

- **Shorthand fields.** A bare identifier inside `{ ... }` lowers to a `name: name` pair. You can combine it with explicit entries in any position, for example `PropertyShorthandRequest { options, retries: retries + 1, label }`.
- **Shorthand-aware diagnostics.** When a shorthand name has no matching value, the error explains the exact-name requirement instead of a generic unresolved-name message: `property shorthand \`options\` requires an in-scope value named \`options\`. Did you mean \`options: option\`?`. For class constructors, a resolvable value with a non-matching field name reports `class \`Config\` has no field \`option\` for property shorthand. Did you mean \`options: option\`?`.
- **Unknown class fields now report per field with suggestions.** Explicit constructor fields that a class does not declare are flagged individually under diagnostic `E0007` (NoSuchField) with the field name underlined and close-name suggestions, for example `class \`Color\` has no field \`gree\`. Did you mean \`green\`?`.

```baml
class PropertyShorthandRequest {
    options map<string, string>
    retries int
    label string
}

function property_shorthand_class(
    mode: string,
    retries: int,
    label: string,
) -> PropertyShorthandRequest {
    let options = { "mode": mode };
    PropertyShorthandRequest { options, retries, label }
}
```

The parser also tightens for-in loop headers so an iterable ending in an object literal, such as `for let x in Items { values }`, stays unambiguous with the loop body brace.

Authors: aaronvg

## Error diagnostics and `baml.json.to_string` unions preserve structural values

0.15.1-nightly.20260723.f · nightly · 2026-07-23

Thrown values in error diagnostics now render their full structure instead of a bare class name or an `<error>` placeholder.

- **Error chain traces** show a thrown class instance with its qualified name and fields, like `user.AppError { message: "boom", detail: user.Detail { label: "visible" } }`, and render non-class thrown values (maps, arrays) structurally instead of `<error>`. This rendering runs natively and does not dispatch user `baml.ToString` overrides, so a caught-and-reported failure reads the same as one escaping `baml run`.
- **Panic diagnostics** now carry their fields. `baml.panics.DivisionByZero` prints as `baml.panics.DivisionByZero { dividend: 42 }`.
- **Bounded rendering.** Diagnostic rendering caps at depth 32 and 256 nodes and detects object cycles, truncating with `…` (for example `user.RecursiveError { next: … }`) so a self-referential or very large thrown value cannot recurse forever or produce an unbounded trace.
- **`baml.json.to_string<T>` on union types** now picks the first declared member that contains the runtime value and serializes it exactly as that member would outside the union, using the same membership relation as `is` and typed match arms. A class, enum, `image`, or `uint8array` member keeps its own serialization, and a value outside every member raises `baml.json.JsonSerializationError`.

```baml
class Record {
  name string
}

type Payload = Record | string

function main() -> string {
  baml.json.to_string<Payload>(Record { name: "Ada" })
}
```

Authors: aaronvg

## `baml test --logs LEVEL` prints `log.*` events to stdout

0.15.1-nightly.20260723.e · nightly · 2026-07-23

`baml test` gains a `--logs` flag that surfaces BAML `log.*` events while tests run. Logs stay off by default, so existing invocations are unchanged.

- **`--logs LEVEL`** accepts `off`, `error`, `warn`, `info`, or `debug` (case-insensitive) and prints every `log.*` event at or above that threshold as an `[LEVEL] message` line on stdout. Structured log payloads are rendered too.
- **Streamed output.** When stdout is redirected, each drained batch is flushed while the test is still running, so logs appear before the final PASS/FAIL report instead of only at process exit.
- **Exit codes are preserved.** Passing `--logs` does not change the test-failure exit code, so a failing test still exits with code 2.

```bash
baml test --logs INFO > baml-test.log
```

Separately, compiler codegen for stdlib builtins is now deterministic and keyword-safe: BAML field names that collide with Rust keywords (for example `type`, `match`) are emitted as raw identifiers, names Rust forbids even in raw form (`self`, `Self`, `super`, `crate`, `_`) and names with illegal punctuation use a reversible hex encoding, and package namespace discovery is sorted rather than inheriting `HashSet` ordering. This is internal to how the compiler generates its own code and does not change BAML source behavior. The rest of the range removes dead public APIs across several crates with no observable effect.

Authors: aaronvg

## `baml test --logs` prints BAML log events to stdout

0.15.1-nightly.20260723.d · nightly · 2026-07-23

`baml test` gains a `--logs` flag that prints `log.*` events (such as `log.info` and `log.error`) to stdout while a test runs.

- **`baml test --logs LEVEL`**: opt into log output at a threshold. Accepted values are `OFF`, `ERROR`, `WARN`, `INFO`, and `DEBUG` (case-insensitive). Logs are `OFF` by default, so existing test output is unchanged. Events at or above the level print as `[LEVEL] body` lines, and object bodies like `log.warn({"user": "ada"})` are rendered inline.
- **Exit codes are preserved.** A failing test still exits non-zero when `--logs` is set, and the last captured log is flushed before the PASS/FAIL report.
- **Streaming to a file works.** Because logs go to stdout, `baml test --logs INFO > baml-test.log` retains the raw stream, and lines are flushed as they occur rather than only after the run completes.

```bash
baml test --logs INFO > baml-test.log
```

The release also makes native builtin codegen deterministic and keyword-safe: generated field names that collide with Rust keywords or contain illegal punctuation (for example `type`, `self`, or `dash-name`) now map to legal identifiers via a reversible encoding, and namespace discovery is sorted instead of relying on hash-set ordering. This changes generated Rust, not `.baml` behavior. A round of dead internal APIs was also removed with no observable effect.

Authors: aaronvg

## `baml.sys.collect_garbage()` forces a collection and drains pending `cleanup()` finalizers

0.15.1-nightly.20260723.c · nightly · 2026-07-23

`baml.sys.collect_garbage()` is a new builtin that forces a full garbage collection and drains queued `cleanup()` finalizers before it returns. It is meant for deterministic tests and runtime diagnostics. In production, let the runtime schedule collections itself.

```baml
class Resource {
  log string[]
  function cleanup(self) -> void throws never {
    self.log.push("cleaned")
  }
}
function abandon(log: string[]) -> void throws never {
  let resource = Resource { log: log }
  resource.log.push("created")
}
function main() -> string[] {
  let log: string[] = []
  abandon(log)
  baml.sys.collect_garbage()
  log
}
```

The abandoned `Resource`'s `cleanup()` has already run by the next statement, so `main` returns `["created", "cleaned"]`.

Other changes:

- **Class spread `{ ...factory() }`.** Calls nested inside a spread now get their full call plans, so a factory invoked with omitted defaults emits the right omitted-argument sentinels instead of consuming extra slots from the caller's VM frame. An exact-class spread can also supply otherwise-omitted generic arguments (`Box { ...box_int }` resolves to `Box<int>`), and a spread whose source is a different class or carries incompatible generic arguments is now a type error (`Left<int> { ...Right<int> { .. } }` reports `type mismatch: expected Left<int>, got Right<int>`).
- **SAP string parsing.** When an LLM response is a complete JSON string literal, a `string` parse target now decodes it, so `"hello"` yields `hello`. Plain text, partial strings, and prose containing JSON stay verbatim. Optional and generic union parse targets such as `json?` and `T | PendingCalls` are also flattened correctly before parsing.
- **Formatter.** `spawn { … }` expressions reindent like normal blocks instead of printing verbatim at their original columns, and a call whose last argument is a lambda or `spawn` hugs the parens (`push(spawn { … });`) rather than forcing the whole call to break. Empty blocks with no interior comment collapse to `{}`.
- **Parser.** `client` and `prompt` used as named call arguments (`Ask("hi", client = override_provider())`) are no longer misread as an LLM function body.
- **Interface methods.** A method whose return type is a transparent alias for an array (`type EventStream = Event[]`) is now iterable in a `for` loop.
- **Projection assignment.** A projection whose base is a call (`store.require().info.title = ...`) now evaluates that base exactly once.

Authors: aaronvg

## Duplicate class methods no longer cascade into spurious type errors at every call site

0.15.1-nightly.20260723.b · nightly · 2026-07-23

A class that declares two methods with the same name, such as two `value(self)` methods on `Box`, now reports only the duplicate-method diagnostic and no longer emits a follow-on type error where the method is called.

Previously each call site like `box.value()` produced an extra spurious type error on top of the duplicate declaration itself, so a single mistake surfaced as several diagnostics scattered across the file. Now the call resolves to an error type quietly and the duplicate declaration is the only thing flagged.

This change only affects which diagnostics are reported. It does not change how valid code type-checks or runs.

Authors: codeshaunted

## CI-only change to the Rust SDK publishing workflow, no user-facing changes

0.15.1-nightly.20260723.a · nightly · 2026-07-23

This nightly contains no changes that affect BAML code, the CLI, or the runtime. The only change adds `environment: release` to the Rust SDK trusted-publishing job in the release workflow, scoping its OIDC credentials to a protected GitHub environment. Nothing to do here if you use BAML.

Authors: 2kai2kai2

## `baml check --diagnostic-format agent` emits compact diagnostics, and type narrowing no longer applies to `spawn`-captured locals

0.15.1-nightly.20260722.f · nightly · 2026-07-23

`baml check --diagnostic-format agent` now renders diagnostics as compact, location-first lines without Ariadne source diagrams, intended for coding agents that parse output rather than read it.

**Highlights**

- **Unified output flags.** The CLI gains `--output-preset` (`auto`, `human`, `agent`), `--diagnostic-format` (`human`, `agent`, `concise`), and `--hyperlinks`, alongside the existing `--color`. Each reads a matching environment variable: `BAML_OUTPUT_PRESET`, `BAML_DIAGNOSTIC_FORMAT`, `BAML_HYPERLINKS`, `BAML_COLOR`. With the default `auto` preset, a detected coding-agent environment selects agent output and drops color and terminal hyperlinks; otherwise you get human output. `--color`, `--hyperlinks`, and `--diagnostic-format` override the preset per field.
- **Narrowing is now safe across `spawn`.** A local captured by a `spawn` block is no longer narrowed by `if (x != null)` or by a `match`, because the spawned task can reassign it. Such a variable keeps its declared type. A local that is not captured still narrows as before, and taking an uncaptured snapshot (`let snapshot = x;`) narrows normally.
- **Atomic typed patterns.** A binding pattern like `let foo: Foo =>` now tests and binds the same value in one step via a new `narrow_bind` operation, so the bound value cannot be reread from a mutable cell between the test and the bind.
- **Field/method name conflict.** A class that declares a field and a method with the same name no longer produces a cascade of follow-on diagnostics.

```bash
baml check --diagnostic-format agent
```

Authors: codeshaunted

## A `swift` code generator target lands, and the ItemTree firewall transition finishes

0.15.1-nightly.20260722.e · nightly · 2026-07-22

This nightly adds `swift` as a code generator output type, so `baml generate` can now emit a Swift SDK backed by the `BamlBridge` SwiftPM runtime.

- **`swift` generator target.** `OutputType` gains a `Swift` variant (serialized as `swift`), and `baml generate` routes it through the new `sdkgen_swift` crate. This target is early: the sdk-test crate's codegen soft-fails into build diagnostics, and the Swift toolchain tests and xcframework assembly are macOS-only. If you do not select the `swift` output type, nothing changes for you.
- **ItemTree firewall transition completed (internal, no behavior change).** Downstream crates no longer read the raw item tree. `file_item_tree` is now `pub(crate)` in both the HIR and PPIR crates, and `file_item_tree_source_map` was removed outright from HIR (its `ItemTreeSourceMap` import dropped) because spans are now served by the per-item `*_source_map` firewall queries; PPIR keeps its own `file_item_tree_source_map` as `pub(crate)`. Consumers in the CLI, emit, and LSP now go through the `item_data` firewall queries (`file_functions`/`class_data`/`enum_data`/…). This is a plumbing change with no observable effect on compiled output or diagnostics.

One caveat is unchanged: `infer_scope_types` (and the LSP scope walkers) still read the coarse `file_semantic_index` directly, so a comment-only edit still re-runs type inference. The `comment_edit_does_not_reexecute_type_inference` incremental test remains `#[ignore]`d for exactly that reason.

Authors: ATX24, 2kai2kai2

## New `swift` code generation target backed by the BamlBridge SwiftPM runtime

0.15.1-nightly.20260722.d · nightly · 2026-07-22

BAML can now generate a Swift SDK. A new `swift` output type is available in your generator, and `baml generate` emits Swift sources through `sdkgen_swift` for the `BamlBridge` SwiftPM runtime.

- **`swift` output type.** `OutputType` gains a `Swift` variant (the generator value is the string `swift`). Select it in your generator to produce a Swift SDK; wiring runs through `sdkgen_swift::to_source_code_with_bytecode` in the CLI's generate path.
- **Runtime and toolchain.** The generated code targets the `BamlBridge` SwiftPM package. The build pulls in the FFI pieces (a `BamlBridgeFFI` xcframework assembled from the `bridge_swift` staticlib, `protoc-gen-swift` proto bindings, and a copy of the canonical `baml_cffi.h` ABI header). Swift codegen soft-fails into build diagnostics for now, since `sdkgen_swift` is early.
- **macOS-gated tests.** The Swift SDK-test fixtures need a Swift toolchain and XCTest, so they run only on macOS CI; the fixture tests are `#[ignore]`d elsewhere.

Also in this release: an internal refactor moving HIR/PPIR item-tree readers behind the PPIR `item_data` firewall queries. No intended change to `baml generate` or LSP behavior.

Authors: ATX24, 2kai2kai2

## Release automation switches to dispatch, no user-facing changes

0.15.1-nightly.20260722.c · nightly · 2026-07-22

This nightly contains only CI changes to how releases are triggered and has no effect on the BAML language, runtime, or tooling you install. The release workflow moved off a `workflow_run` trigger to an explicit `gh workflow run` dispatch from CI, because crates.io will not mint a Trusted Publishing token for `workflow_run` runs, which previously blocked publishing `baml_bridge`. Nothing in the diff changes observable behavior for users.

Authors: 2kai2kai2

## Integer literals gain hex, octal, and binary forms with `_` digit separators

0.15.1-nightly.20260722.b · nightly · 2026-07-22

Integer and `bigint` literals now accept hex, octal, and binary forms with `_` digit separators (#4065).

Highlights:

- **Integer literals**: hex, octal, and binary `int`/`bigint` literals are now supported, with `_` digit separators for readability (#4065).
- **Type errors inside `${...}`**: the compiler now reports type errors inside `${...}` string interpolation, which previously went unchecked (#4096, B-836).
- **Faster cold compiles**: parallel check/MIR/emit plus inference dedup cut cold compile time from 0.63s to 0.27s with byte-identical output (#4073). A separate re-land of earlier cold-compile optimizations brought it from 2.4s to 0.81s (#4058). Net across the release, cold compile time fell from 2.4s to 0.27s.
- **LLM client defaults**: client defaults and validation are now centralized in one place in the runtime (#4063, B-868).

New SDK targets are landing but are still scaffolding and release plumbing at this stage, not yet a stable consumer API: a Rust `baml_bridge` crate (#4045, #4093), a Go standalone runtime and generated SDK (#4067), a Java/JVM bridge published as `com.boundaryml:baml-bridge` (#4060, #4106), a C++ bridge with a checked-in C ABI header (#4004, #4071, #4078), and a Web/Workers TypeScript build (#4052, #4006). The Node bridge sources moved from `sdks/nodejs/bridge_nodejs` to `sdks/typescript/bridge_typescript`, which affects contributors building the SDK from source but not published package names.

Authors: sxlijin, hellovai, codeshaunted, 2kai2kai2, antoniosarosi, rossirpaulo

## Integer literals gain hex, octal, and binary forms, plus first slices of the Rust, C++, Java, and Go SDKs

0.15.1-nightly.20260722.a · nightly · 2026-07-22

BAML integer literals now accept hexadecimal (`0x`), octal (`0o`), and binary (`0b`) forms with `_` digit separators for both int and bigint values.

Highlights:

- **Numeric literal syntax.** `0x`, `0o`, and `0b` prefixes and `_` separators are now valid in int and bigint literals (#4065).
- **Type errors inside `${...}`.** Expressions in string interpolation are now type-checked and their errors reported, where they were previously missed (B-836, #4096).
- **New SDK targets.** First end-to-end slices landed for the Rust SDK (the `baml_bridge` crate packaged for crates.io), the C++ SDK (`baml-cpp` tarball with a public C ABI header, async `{name}Async` call forms, and static/instance methods), and the Java/JVM SDK (`com.boundaryml:baml-bridge` Maven artifacts, with JSpecify nullness for Kotlin). A generated Go SDK with a standalone runtime also shipped.
- **TypeScript for the browser and Workers.** A new `@boundaryml/baml-bridge-web` package provides browser and Cloudflare Workers runtimes built on WASM, and the existing SDK moved from `sdks/nodejs/bridge_nodejs` to `sdks/typescript/bridge_typescript`.
- **LLM client defaults.** Client defaults and their validation were centralized (B-868, #4063), so invalid client configuration is caught in one place.
- **Faster cold compiles.** Cold compile time dropped from roughly 2.4s to 0.27s via parallel check/MIR/emit and inference dedup, with byte-identical output (#4058, #4073).

The rest of the range is release plumbing: build matrices for every SDK are now generated from a single `release/platforms.json` platform contract rather than hand-copied YAML, and dedicated verify workflows exercise the Rust and C++ artifacts as a real consumer would. The `baml-cli` size baselines rose about 4 percent because the Rust code generator now formats output with `prettyplease`.

Authors: sxlijin, hellovai, codeshaunted, 2kai2kai2, antoniosarosi, rossirpaulo

## Type and value declarations now share one namespace, so a name reused across `type Backend` and `client<llm> Backend` is reported as a conflict

0.15.1-nightly.20260716.b · nightly · 2026-07-16

Namespace conflict detection previously looked up type declarations and value declarations in separate maps, so a name reused across a type and a value went undetected. Declaring `type Backend = string` in one file and `client<llm> Backend` in another now produces a single conflict diagnostic that lists every location where the name is defined.

**What changed**

- **Unified declaration namespace.** All namespace-level declarations with the same spelling contribute to one conflict, regardless of whether they are types (`class`, `enum`, `type`) or values (`client`, `retry_policy`, `function`). A name spread across `class`, `enum`, `type`, and `function` now collapses into one report (`Name ... defined 4 times as:` followed by the kinds `class`, `enum`, `type`, `function`) instead of separate type-only and value-only diagnostics, and the annotation covers every conflicting source location.
- **Source-kind labels in diagnostics.** Conflict and namespace-shadow messages now report the kind as written in BAML source. Declarations that are lowered to internal top-level lets (clients and retry policies) are labeled `client` and `retry policy` rather than `let`.
- **Namespace isolation preserved.** The same name in two different BAML namespaces (for example `ns_models/` and `ns_api/`) is still legal and does not conflict.
- **Function-scoped tests unchanged.** Same-named `test` blocks attached to different functions remain legal, since a test's identity is `functionName/testName`.

**Impact.** Existing projects that reuse a name across a type and a value kind (a client, retry policy, or function) may now surface a new conflict diagnostic where none appeared before. Rename one of the declarations to resolve it.

This release also adds an internal `tools_compile_profile` binary for profiling the compiler pipeline. It has no effect on your BAML code.

Authors: hellovai

## Names that collide across declaration kinds now report one unified conflict

0.15.1-nightly.20260716.a · nightly · 2026-07-16

Declarations that share a name now belong to a single namespace, so a name reused across different declaration kinds produces one complete conflict diagnostic instead of separate type-only and value-only errors.

- **Mixed-kind conflicts are reported once.** A name like `Shared` defined as a `class`, an `enum`, a `type`, and a `function` across four files now yields a single diagnostic that points at all four source locations, rather than fragmenting into per-kind errors.
- **Types and values share one namespace.** A `type Backend` and a `client<llm> Backend` with the same spelling now collide, where previously they were tracked separately. The client is reported under its source declaration kind (`client`), not its internal lowered `let` form. The same applies to retry policies, which are reported under their source declaration kind rather than leaking `let`.
- **Function-scoped tests stay legal.** Two `test` blocks named `Shared` attached to different functions do not conflict, since test identity is `functionName/testName`.
- **Namespace isolation is preserved.** The same name used in two different `ns_`-prefixed directories does not conflict, because each namespace is its own scope.

This release also adds an internal dev tool, `tools_compile_profile`, a standalone harness for profiling the compiler pipeline. It does not affect your builds.

Authors: hellovai

## Function types now require an explicit `throws` clause (E0151)

0.15.1-nightly.20260715.f · nightly · 2026-07-15

A function type that omits its `throws` clause is now rejected with `E0151` (`function type must declare an explicit throws clause; add throws never if calling it cannot throw`) in every position except an immediate callback parameter of a function declaration. Previously an omitted `throws` was silently filled in, either as `never` or, in some nested and return positions, as a synthetic effect parameter. That implicit behavior is gone.

- **Where you now need `throws`.** Type aliases, class fields, `let` annotations, lambda parameters, nested function-type positions, and function-type return positions must all spell out the clause. Add `throws never` when the callable cannot throw, or an explicit error type otherwise.
- **The one exception.** An immediate callback parameter of a `function` declaration may still omit `throws`. There the compiler opens it to a fresh effect parameter so the parameter stays effect-polymorphic. A lambda parameter has no such binder, so it must declare `throws` explicitly.
- **Migration.** Existing code that relied on the implicit `never` will now fail to compile. The fix is mechanical: append `throws never` to the affected function types.

Before:

```baml
type Callback = (x: int) -> int
```

After:

```baml
type Callback = (x: int) -> int throws never
```

An immediate callback parameter still compiles without the clause:

```baml
function immediate_callback(cb: (value: int) -> int) -> int {
  cb(1)
}
```

Authors: codeshaunted

## SDK and native runtime version mismatches now fail loudly at import

0.15.1-nightly.20260715.e · nightly · 2026-07-15

The Python and Node.js SDKs now register themselves with the native runtime on load and require an exact canonical release match. If the versions differ, the Python SDK raises a `PyImportError` and the Node.js SDK writes an error to stderr, instead of silently running against an incompatible runtime.

- **Strict lockstep check.** Compatibility is deliberately stricter than SemVer. The SDK version and the runtime's `CANONICAL_VERSION` must be identical, or registration fails with a message telling you to install the matching native runtime or update the SDK.
- **Single conflicting bridge per process.** The runtime records the first bridge that registers. A second registration from a different SDK (for example loading the Python and Node.js bridges into one process) is rejected with a diagnostic naming both.
- **Versioned C ABI foundation.** A new `baml_get_api_v1` entry point returns a versioned `BamlApiV1` function table. This only affects host-bridge implementers, not BAML code you write. There is no change to `.baml` syntax or generated client APIs.

Most of this release is ABI plumbing for how SDKs load the runtime. The one behavior you can observe is the new import-time version check.

Authors: hellovai

## Host SDKs now register with the native runtime and require an exact version match

0.15.1-nightly.20260715.d · nightly · 2026-07-15

This nightly adds a versioned C bridge foundation, `baml_get_api_v1`, and wires the Python and Node.js SDKs to register themselves with the native runtime on load. The check is stricter than SemVer: the SDK and native runtime must report the same canonical release version, so a mismatched pairing now fails fast instead of misbehaving later.

- **`baml_get_api_v1` / `BamlApiV1`**: a manually loaded host bridge resolves a single symbol, `baml_get_api_v1`, and obtains every callable it needs from the returned version-1 function table. Fields are append-only for the lifetime of ABI version 1, which makes completeness and compatibility validation explicit.
- **Python**: importing `baml_py` against a mismatched native runtime now raises an `ImportError`. The message is the one surfaced through `register_bridge`, for example "BAML Python SDK <version> cannot use native runtime <version>. Install the matching native runtime or update the Python SDK".
- **Node.js**: the bridge registers on module init and, on failure, writes the same diagnostic to stderr rather than aborting the load.

Authors: hellovai

## New `baml telemetry` command to opt out of anonymous CLI telemetry, plus incremental bytecode caching

0.15.1-nightly.20260715.c · nightly · 2026-07-15

The CLI now collects anonymous usage telemetry, and you can opt out with `baml telemetry disable`. This release also adds a content-addressed bytecode cache that makes warm `check`, `run`, and `test` runs skip recompiling unchanged files.

**Highlights**

- **`baml telemetry` subcommand.** A new (help-hidden) command manages telemetry: `baml telemetry` shows status, `baml telemetry disable` opts out, and `baml telemetry enable` opts back in. A first-run notice announces collection. What ships is anonymous (subcommand name, version and channel, coarse machine info, a random per-machine ID, and a salted one-way hash of the project root), and never includes source, prompts, secrets, or file paths. See the new `TELEMETRY.md` for the full field list.
- **Opt-out environment variables.** `BAML_TELEMETRY_DISABLED=1` disables collection, the cross-tool `DO_NOT_TRACK=1` is honored, and the legacy `BAML_TELEMETRY=0` still works. Set `BAML_TELEMETRY_DEBUG=1` to print exactly what would be sent to stderr (prefixed `[telemetry]`) without transmitting anything.
- **Incremental bytecode cache.** `check`, `run`, and `test` now reuse compiled bytecode for files that did not change, recompiling per file instead of the whole project. Editing only a function body no longer forces a project-wide recompile, because a file's signature hash blanks out body regions. Disable the cache with `BAML_NO_BYTECODE_CACHE=1`, or force a verifying full compile with `BAML_CACHE_VERIFY=1`.
- **Standalone `bridge_cffi` cdylib.** The release now builds and publishes a language-neutral engine cdylib per target for dylib-loader SDKs, including a musl build fix that disables `crt-static` so rustc can emit a shared object. This is release plumbing and does not change how you write BAML.

```bash
baml telemetry           # show current status
baml telemetry disable   # opt out
baml telemetry enable    # opt back in
```

Authors: hellovai, antoniosarosi, 2kai2kai2

## TIR type comparisons consolidate onto the canonical `baml_type::normalize` algebra

0.15.1-nightly.20260715.b · nightly · 2026-07-15

This release is almost entirely an internal consolidation: the TIR crate's duplicate structural-type algebra (the old `StructuralTy` enum and `is_same_normalized_type` in `normalize.rs`) is deleted, and every invariant type-equality site now goes through the canonical `baml_type::normalize` relation via a new `AliasEquivCtx` type context. `normalize.rs` keeps only recursive-alias detection and the ill-founded-recursion cycle diagnostics. For the equality-based sites (impl-head matching, coherence unification, MIR dispatch matching), this is a behavior-preserving refactor.

One comparison genuinely tightens: matching a builtin `Array`/`Map` argument against its `List`/`Map` shape in type inference now requires the element types to be *equivalent* rather than merely subtype-compatible, matching the documented invariance of these containers. In practice an `int[]` argument no longer satisfies an `(int | string)[]` slot, which the surrounding code comment already claimed but the implementation did not enforce.

The change also adds test coverage (no behavior change) for function-type subtyping in `baml_type`: `throws` covariance, insignificance of required-parameter names, and the optional-parameter superset rule.

## Array patterns can bind the rest with `..let name`

0.15.1-nightly.20260715.a · nightly · 2026-07-15

Array patterns can now bind the unmatched middle: `..let rest` captures a copy of the elements a match skips, typed as the element list `elem[]`. Previously any sub-pattern after `..` was rejected outright and only bare `..` was allowed.

```baml
function middle(xs: int[]) -> int {
    match (xs) {
        [let first, ..let rest, let last] => first + rest.length() + last,
        _ => 0
    }
}
```

- **`..let name` binds a slice.** The rest binding matches against the slice, not an element, so it is typed `elem[]`. It holds a shallow copy of the middle: pushing to it does not mutate the scrutinee, and pushing to the scrutinee does not mutate it.
- **`.._` and list ascriptions.** A wildcard rest `.._` is allowed and lowers exactly like bare `..`, with no slice copy. A rest binding may carry a list-typed ascription, e.g. `..let rest: int[]`, checked against the slice type.
- **Non-binding shapes after `..` are rejected** with the renamed `RestSubPatternNotBinding` diagnostic, which reads "rest pattern `..` can only carry a binding; write `..let name` or `..let name: T[]`". This covers bare type patterns like `..int`, class destructures like `..Wrapper { value }`, and nested array rests.
- **A binding with a non-list or narrowing ascription is rejected differently.** `..let r: int` on an `int[]`, or `..let r: int[]` on a `(int | string)[]`, is binding-shaped but fails as a type mismatch against the slice type `elem[]`, not through the `RestSubPatternNotBinding` diagnostic. A narrowing ascription can never match at runtime because the slice always carries the scrutinee's element tag.

Authors: codeshaunted

## A spawned task that errors now propagates to its awaiter instead of deadlocking

0.15.1-nightly.20260714.h · nightly · 2026-07-14

When a task started with `spawn` terminates with an engine error on a path that previously left its future `Pending`, the runtime now settles that future with the error so the awaiting task re-raises it and the failure travels up to the host. Before this change such a task left every awaiter parked forever.

Secondary change, at the host FFI boundary:

- **Non-realized host-supplied types are now rejected loudly.** An array element type, map key/value type, or class type argument that is not fully realized (an unfilled type variable or an associated-type projection) now surfaces as `EngineError::TypeMismatch` rather than being silently accepted. A non-realized entry-point type argument surfaces separately as `EngineError::VmInternalError` (`VmInternalError::TypeSubstitution`).

The bulk of the diff swaps the compiler and runtime from `RuntimeTy` to a `RealizedTy` representation for stored value types. That is an internal refactor with no change to BAML source, syntax, or the types you write.

## BEP sidebar keeps its scroll position when navigating between pages

0.15.1-nightly.20260714.g · nightly · 2026-07-14

The BEPs app sidebar now preserves its own scroll offset when you navigate, so a long page list no longer jumps back to the top when the route shell remounts. Scroll positions are tracked per BEP number, and the active item (marked with `aria-current="page"`) is scrolled into view inside the sidebar's own container rather than via `scrollIntoView`, which would also scroll the window. This is a UI fix in the `beps` web app only. It does not touch the BAML language, compiler, or generators.

Authors: aaronvg

## The BEPs sidebar keeps its scroll position when you navigate between pages

0.15.1-nightly.20260714.f · nightly · 2026-07-14

This nightly contains a single fix to the BEPs proposal viewer (`beps` app), not the BAML language or toolchain. The sidebar nav (`BepNav`) no longer jumps back to the top when you move between pages. Two things changed: the active item now carries `aria-current="page"` and is scrolled into view within the sidebar's own container rather than via `scrollIntoView` (which would also scroll the window), and the sidebar's scroll offset is now saved per BEP in a module-level map and restored when the route shell remounts. If you don't use the BEPs site, nothing in your BAML code is affected.

Authors: aaronvg

## Docs site drops the custom "Ask Baaaml" search for Fern's built-in search

0.15.1-nightly.20260714.e · nightly · 2026-07-14

This nightly contains no changes to the BAML language, compiler, or runtime. The only change is to the documentation site's build: the custom `Ask Baaaml` search widget was removed in favor of Fern's built-in search. The `deploy-backend` and `update-pinecone` docs CI jobs, the `sage-backend` and `ask-baml-client` app builds, and the injected `custom.js` were all deleted from `fern/docs.yml` and the docs workflow. Nothing you write in BAML is affected.

Authors: aaronvg

## BEP pages support one level of nesting via `parentSlug`

0.15.1-nightly.20260714.d · nightly · 2026-07-14

This release is entirely to the BEPs app. Pages can now nest one level deep through a new optional `parentSlug` field.

- **`parentSlug` on page objects.** A page can name a parent page's slug, and it renders indented under that parent in the sidebar and in exported README/metadata. On import, a markdown file in a subfolder of `pages/` (for example `pages/design/api.md`) becomes a child of the `design` page. Folders deeper than one level are skipped, and slugs must stay unique per BEP, so the same filename in two subfolders is rejected as a duplicate.
- **TOC anchors and hash links now land on real headings.** Heading id generation moved to a shared `heading-utils` module (`extractHeadings`, `slugifyHeading`, `stripFrontmatter`) so the rendered content and the table of contents compute ids the same way. Ids are keyed by source line instead of a render-order counter, fenced code blocks are skipped, and the TOC retries attaching to headings that render asynchronously, including honoring a `#hash` on initial load.
- **Sticky sidebar scrolls.** A long page list in the sidebar is now scrollable while pinned, via `max-h` plus `overflow-y-auto`.
- **`importVersion` now accepts an optional `title`; previously title changes sent to `PUT /api/agent/beps` were ignored** because `route.ts` never forwarded one. It also treats omitted `title`, `content`, and `pages` as leave-unchanged (applying `args.title?.trim() || bep.title`), while an explicit empty `pages` array still deletes all pages.

```json
{
  "slug": "api",
  "title": "API Details",
  "content": "# API Details\n\n...",
  "parentSlug": "background"
}
```

Authors: aaronvg

## `baml describe` highlighting now honors token modifiers and stays on the terminal's named palette

0.15.1-nightly.20260714.c · nightly · 2026-07-14

Syntax highlighting in `baml describe` output now derives its styles from LSP semantic tokens, restricted to the terminal's named ANSI palette plus dim and bold attributes rather than fixed 256-color values. Output stays legible on both light and dark backgrounds because ordinary names keep the terminal's default foreground instead of a forced white.

- **Token modifiers now overlay attributes.** Declarations render bold, `DEFAULT_LIBRARY` (stdlib) entities italic, and `DEPRECATED` ones struck through, matching how editor themes distinguish them.
- **Quiet tokens use dim instead of a fixed gray.** Comments and operators now use a dimmed default color with no forced color, while decorators are dimmed magenta.
- **Palette tweaks.** Parameters are now dimmed yellow, and escape sequences are bright magenta.
- Variables and events keep the terminal's default foreground rather than a forced white, so no background is assumed.

Authors: codeshaunted

## Size-gate baselines refreshed, no user-facing changes

0.15.1-nightly.20260714.b · nightly · 2026-07-14

This nightly only refreshes the CI size-gate baselines in `.cargo/size-gate.toml` and `.ci/size-gate/*.toml`, raising the recorded artifact sizes for the `baml-cli`, `packed-program`, and `bridge_wasm` builds. Nothing in the language, compiler, or runtime changed, so there is nothing to adopt.

Authors: sxlijin

## Version stamp to 0.15.0 with no functional changes

0.15.1-nightly.20260714.a · nightly · 2026-07-14

No user-visible changes. This release stamps the product version to `0.15.0` across the crates and SDK manifests, and is otherwise an internal cleanup: it removes obsolete design-doc labels (`B1`, `B2`, `D2`, `I7`, and similar) from LSP source comments, deletes the `docs/design/lsp-latency-and-mutex-fix.md` design note, and renames one internal test. Compiler and LSP behavior are unchanged.

Authors: rossirpaulo

## `google-ai` clients gain a `GoogleAiOptions` block, and `retry_policy` no longer crashes when delay fields are omitted

0.15.0 · canary · 2026-07-14

`google-ai` clients now accept a provider-options block, and a `retry_policy` with missing delay fields no longer crashes at runtime.

**Highlights**

- **`GoogleAiOptions` for `google-ai` clients.** A `google-ai` client can now carry `enterprise`, `credentials`, `credentials_content`, `location`, and `project_id` options. Setting `enterprise` (or the `GOOGLE_GENAI_USE_ENTERPRISE` environment flag) routes the client through the Vertex AI backend. The SDK also reads `GOOGLE_CLOUD_PROJECT`, `GOOGLE_CLOUD_LOCATION`, and `GOOGLE_GENAI_USE_ENTERPRISE`.
- **`retry_policy` no longer crashes on missing delay fields (B-488).** `initial_delay_ms`, `multiplier`, and `max_delay_ms` are now optional. When omitted they default to 200ms, 1.5x, and 10000ms through new `*_or_default` accessors, instead of arriving as `null` in the backoff math and triggering a `null + 0.0` VM type error.
- **Bitwise NOT (`~`) actually flips bits now (B-766).** `~` now performs a real bitwise NOT (bit-flip). Before this release it was a silent identity operator that returned its operand unchanged. This is not arithmetic negation.
- **Enum variants serialize by name in union/optional JSON fields (B-728).** An enum value in a union- or optional-typed JSON field now serializes as its variant name.
- **Named `client<llm>` defaults `api_key` to the provider env var (B-489).** A named client no longer requires an explicit `api_key`; it falls back to the provider's environment variable.
- **Class and array interpolation renders in prompt strings (B-563).**
- **Standard-library interfaces qualify associated types with `Self.`.** `Iterator`, `Iterable`, `Comparable`, `Sortable`, and `Summable` now project their associated types as `Self.Item`, `Self.Error`, `Self.CompareError`, and `Self.Sum`.
- **`baml agent install` fetches the `main`-branch tarball over codeload.** It no longer calls the GitHub REST API, so installs are no longer blocked by the unauthenticated 60-requests/hour rate limit. The `--latest` flag is removed; use `--from <url-or-path>` for custom sources.
- **`baml ide install --dir <path>`.** Copies the active toolchain's VSIX into a directory for manual install, with clearer errors when no `code` or `cursor` CLI is found on PATH.
- **`bridge-python` accepts ints where a float is expected.** The Python bridge now coerces integer values into float-typed positions.
- **React streaming hook data types are correct for nullable fields.**
- **Grammar distribution.** The BAML TextMate grammar is now published through a self-updating mirror, with additional ports for Sublime, KDE, highlight.js, and tree-sitter.

```baml
client<llm> Gemini {
  provider google-ai
  options {
    model "gemini-3.5-flash"
    api_key env.GOOGLE_API_KEY
  }
}
```

Authors: sxlijin, antoniosarosi, mvanhorn, jubjub727, aaronvg, rossirpaulo, hellovai, ATX24, 2kai2kai2, codeshaunted

## The `watch` keyword and `$watch` accessor are removed

0.14.2-nightly.20260714.e · nightly · 2026-07-14

The `watch` keyword is gone, so `watch let` bindings and the `.$watch` accessor no longer parse. This removes an unfinished reactive-binding feature whose lowering was still a TODO and was never produced by the type checker.

- **`watch let` no longer exists.** Statements like `watch let x = SimulateHumanGuess(history);` are now a parse error. Rewrite them as a plain `let`.
- **`.$watch` is removed.** `x.$watch.options(...)` and `x.$watch.notify()` no longer resolve. The `WatchOptions` and `WatchNotify` machinery is gone from the compiler.
- **`watch` is no longer reserved.** Since it is no longer a keyword, `watch` is now available as an ordinary identifier.
- **Diagnostics E0065 (`$watch` on a non-variable) and E0066 (`$watch` on a variable not declared with `watch let`) are removed**, since the constructs that produced them no longer exist.

If you were using `watch let`, drop the `watch` prefix:

```baml
function GuessGameAgent() -> string {
  let history: string[] = []
  let user_input = SimulateHumanGuess(history)
  user_input
}
```

Authors: sxlijin

## `google-ai` clients can route through Vertex AI via `GOOGLE_GENAI_USE_ENTERPRISE`, `GOOGLE_CLOUD_PROJECT`, and `GOOGLE_CLOUD_LOCATION`

0.14.2-nightly.20260714.d · nightly · 2026-07-14

A `google-ai` client now delegates to the Vertex AI backend when you set `GOOGLE_GENAI_USE_VERTEXAI`, `GOOGLE_GENAI_USE_ENTERPRISE`, or the new `enterprise` option, matching google-genai's behavior. Location and project are resolved from `GOOGLE_CLOUD_LOCATION` and `GOOGLE_CLOUD_PROJECT` (or the credential chain) at request time.

- **New `GoogleAiOptions` provider block.** `google-ai` clients accept `provider_options` with `enterprise`, `credentials`, `credentials_content`, `location`, and `project_id`. The Google Cloud fields are ignored while the client uses the Gemini API, and take on their `VertexAiOptions` meaning once `enterprise` or a `GOOGLE_GENAI_USE_*` flag selects the Vertex backend. Setting `enterprise true` defaults an unset location to `global`.
- **Env-driven Vertex routing.** With `GOOGLE_GENAI_USE_VERTEXAI=true` (or enterprise mode), a `google-ai` client with no `api_key` builds a Vertex `aiplatform.googleapis.com` request, pulling location and project from `GOOGLE_CLOUD_LOCATION` / `GOOGLE_CLOUD_PROJECT`. An explicitly set `options.enterprise` wins over the env var, so `enterprise false` disables it even when `GOOGLE_GENAI_USE_ENTERPRISE=true`.
- **Vertex credential resolution changed (breaking).** The order is now `options.credentials` (a credential JSON file path), then `options.credentials_content` (inline JSON), then Application Default Credentials (the `GOOGLE_APPLICATION_CREDENTIALS` file, the well-known ADC config, then the GCE metadata server). Inline JSON in `credentials` or in env vars is no longer accepted, and the `gcloud` CLI fallback is gone. `GOOGLE_APPLICATION_CREDENTIALS` is treated as a file path only, and a set-but-unreadable path is a hard error rather than a fallthrough to the next source. An explicitly set option is used as-is: a broken value is an error, never a silent cascade. Vertex additionally accepts workload identity federation and impersonated service accounts, and caches minted tokens process-wide until shortly before expiry.
- **`google-ai` now fails fast without a key.** A `google-ai` client with no `api_key` (and no Vertex routing) errors at `$build_request` with `Missing api_key for Google AI. See \`baml describe google-ai\` for how to use Google AI Studio, or \`baml describe vertex-ai\` if you meant to use models hosted on GCP.` Previously the request was built with the auth header simply omitted.
- **`vertex-ai` lost its compile-time `location` check.** Clients no longer need `base_url` or `location` set at compile time. `location` can come from `GOOGLE_CLOUD_LOCATION` at request time, and a client that still cannot resolve one fails at `$build_request` with `Could not resolve location for Vertex AI. Set options.location (e.g. us-central1) or the GOOGLE_CLOUD_LOCATION env var.`
- **New describe topics.** `baml describe google-ai` and `baml describe vertex-ai` document Google AI Studio usage, enterprise delegation to Vertex, and Google Cloud setup.

```baml
client<llm> GoogleViaVertex {
  provider google-ai
  options {
    model "gemini-3.5-flash"
  }
}
```

Run the above with `GOOGLE_GENAI_USE_VERTEXAI=1`, `GOOGLE_CLOUD_PROJECT`, and `GOOGLE_CLOUD_LOCATION` set, and it authenticates through Application Default Credentials with no `api_key`.

Authors: sxlijin

## LSP semantic tokens now classify names by what they resolve to, with `declaration` and `defaultLibrary` modifiers

0.14.2-nightly.20260714.c · nightly · 2026-07-14

Semantic highlighting in the language server was reworked to classify each identifier by what it *resolves to* rather than by syntactic context, matching rust-analyzer's model. This changes how your editor colors BAML code.

- **Token modifiers.** Semantic tokens now support LSP modifiers. Definition sites get `declaration` and names from the `baml` standard library get `defaultLibrary`, so a theme can distinguish a declaration from a use and a builtin from your own code. (The legend also reserves `deprecated`, `readonly`, and `async` for future use.)
- **New token types.** `true` and `false` are now tagged `boolean` and string escape sequences (`\n`, `\u{1f600}`) are tagged `escapeSequence`, so a theme can style them apart from plain numbers and string bodies.
- **Go-to-definition on interface members.** Jumping to definition on a virtual interface method or field accessed through `p.as<Named>.fullname` now lands on the declaration inside the `interface`, rather than returning nothing.
- **Hover types for `catch` and pattern bindings.** Hovering the error binding of a `catch (e) { ... }` clause, or a pattern binding, now reports the resolved binding type instead of `unknown`.
- **Range requests.** Semantic tokens can be resolved on demand for a viewport range, so a large file no longer classifies every scope up front.

Contextually, `as`, `type`, `true`, `false`, and `null` are now re-lexed into dedicated keyword token kinds internally, which is what enables the boolean classification above. This does not change BAML syntax.

Separately, the C FFI surface now exports a bytecode runtime initializer.

Authors: hellovai, codeshaunted

## Semantic highlighting is now driven by name resolution, with `boolean` and `escapeSequence` token types and a full modifier set

0.14.2-nightly.20260714.b · nightly · 2026-07-14

Semantic highlighting now classifies each name by what it resolves to (a definition, a member, a local binding) rather than by its surrounding syntax or inferred type. A reference is highlighted the same way as its definition, and definition sites additionally carry the `declaration` modifier.

- **New token types.** `boolean` (`true`/`false`) and `escapeSequence` (string escapes like `\n`, `\u{1f600}`) are now emitted as distinct types instead of being lumped in with numbers and strings.
- **New token modifiers.** The server now advertises `declaration`, `defaultLibrary`, `deprecated`, `readonly`, and `async`. For example, primitive types (`string`, `int`) and `baml`-package entities carry `defaultLibrary`, and declaration sites carry `declaration`.
- **`catch (e)` bindings** are now highlighted as parameters, matching how the value is scoped to the clause body, and go-to-definition and hover on them report the inferred binding type instead of `unknown`.
- **Go-to-definition on virtual interface members** now jumps to the declaration in the interface: a required method's signature or the default method's declaration for methods, and the field declaration for fields. Previously these resolved to nothing.
- **On-demand range highlighting** via `semantic_tokens_in_range`, so an editor viewport only pays to classify the scopes it touches. The range result is guaranteed to equal the full result filtered to that range.

Under the hood, the contextual keywords `as`, `type`, `true`, `false`, and `null` are now re-lexed into dedicated `KW_AS`, `KW_TYPE`, `KW_TRUE`, `KW_FALSE`, and `KW_NULL` syntax kinds rather than being matched by text, but this does not change parsing behavior for your BAML code.

The CFFI layer also gained an exported bytecode runtime initializer, relevant only if you embed the runtime through the C ABI.

Authors: hellovai, codeshaunted

## Find-references and workspace symbol search reuse one position codec per file

0.14.2-nightly.20260714.a · nightly · 2026-07-14

Find-references (`usages_at`) and workspace symbol search (`search_symbols`) now build a single `PositionCodec` per distinct file instead of one per result. Constructing a codec scans the whole file to build its line table, so when many results land in the same file, this avoids re-scanning that file once per hit.

The change is a performance optimization with no effect on the results returned. The rest of the release is test and doc work: new Salsa memoization tests pinning that `file_annotations` and `semantic_tokens` are tracked queries (recomputed only when a file's source changes), plus corrections to `UPDATE_EXPECT` casing in `TEST_INSTRUCTIONS.md`.

Authors: rossirpaulo

## Agent-skill warnings move into the toolchain and fire only on `init`, `run`, `generate`, and `pack`.

0.14.2-nightly.20260713.e · nightly · 2026-07-13

The agent-skill freshness warning now lives in the toolchain binary instead of the `baml` wrapper, so it ships with every nightly and prints only on the core authoring commands (`init`, `run`, `generate`, `pack`) rather than on every invocation. Machine-facing and utility commands like `check` and `fmt` stay quiet.

Highlights:

- **Skill warnings relocated.** The passive "no baml skill installed" and "baml skill is outdated" nags are gone from the wrapper and are decided from local caches by the toolchain, with a TTL-throttled background refresh of the latest-commit cache. `[update] auto_check = false` in `~/.baml/config.toml` still disables the network refresh without silencing the cache-based warning.
- **`baml agent install` archives the replaced skill.** When an install overwrites an existing skill, the previous version is moved into a `baml-old_skills/` slot inside the skills directory (one slot per skill) instead of a temporary backup that got deleted. A skill named `baml-old_skills` in the source is now rejected because it would collide with that archive directory.
- **Incomplete raw strings no longer panic.** A run of bare hashes at end of file (for example a file ending in `##`) previously let the parser index past the token buffer. It now surfaces diagnostics instead of crashing, which matters most while typing in the editor.
- **LSP latency regression addressed.** `semantic_tokens` and inlay-hint generation are now Salsa-cached queries, so they are not recomputed on every scroll while a file is unchanged, and the server now enforces bounded inbound and outbound queues. This targets the mutex/latency regression introduced in 0.14.

Authors: rossirpaulo, ATX24

## Editor semantic-token modifiers and interface-member goto-definition are temporarily reverted

0.14.2-nightly.20260713.d · nightly · 2026-07-13

This nightly reverts a batch of LSP editor work (#3867) so that a later change (#4000) can land first, so several highlighting and navigation behaviors regress in editors backed by the language server.

- **Semantic token modifiers are gone.** The `ModifierSet` bitset and the `declaration`, `defaultLibrary`, `deprecated`, `readonly`, and `async` modifiers are removed from the token legend, so declarations and standard-library references no longer carry those decorations.
- **`boolean` and `escapeSequence` token types are removed.** `true` and `false` now highlight as ordinary tokens rather than a dedicated `boolean` type, and string escape sequences lose their own color.
- **Goto-definition on virtual interface members no longer navigates.** Jumping to definition on a `p.as<Named>.fullname` field or a `d.as<Serializer>.encode()` method now resolves to nothing instead of landing on the interface declaration, because only the slot is known statically.
- **Hover types on pattern and catch bindings report `unknown`.** Type info for a binding introduced by a match arm or a `catch (e)` clause is no longer inferred and shows as `unknown`.

Internally, the contextual keyword kinds `KW_AS`, `KW_TYPE`, `KW_TRUE`, `KW_FALSE`, and `KW_NULL` are dropped and those words are parsed as plain identifiers again, and the separate `DefinitionSite::CatchBinding` is folded back into `PatternBinding`. Your BAML source compiles the same as before. This is a stated temporary revert, so expect these editor features to return once #4000 is in.

Authors: rossirpaulo

## Semantic highlighting is now resolution-first, with go-to-definition into interface members

0.14.2-nightly.20260713.c · nightly · 2026-07-13

The LSP semantic-tokens layer was rewritten to classify identifiers by what they *resolve to* rather than by their inferred type, matching rust-analyzer's model. Editor highlighting is now driven by name resolution facts (`MemberResolution`, `ResolvedName`, `DefinitionKind`), so a reference is colored the same way as its definition.

Highlights:

- **New token types.** `true`/`false` are now tagged `boolean` and string escapes get a new `escapeSequence` type. In the terminal `describe` palette, `escapeSequence` picks up a distinct color (`color256(214)`); `boolean` is styled the same yellow as numbers.
- **New token modifiers.** The LSP legend (`TOKEN_MODIFIERS`) now advertises `declaration`, `defaultLibrary`, `deprecated`, `readonly`, and `async`. In practice `declaration` marks definition sites (`function FirstTwo` -> `FirstTwo`) and `defaultLibrary` marks builtin/primitive names like `string` and `int`, as shown in the semantic-tokens fixtures.
- **Go-to-definition into interfaces.** Jumping from a virtual interface call like `d.as<Serializer>.encode()` now lands on the declaration inside the `interface` (the required signature or the default method), and a virtual field access lands on the interface field. Previously these had no jump target.
- **Catch bindings highlight as parameters.** The error binding of `catch (e) { ... }` is now classified and colored like a function parameter, and hover on a pattern or catch binding reports its real inferred type instead of `unknown`.
- **Range-based highlighting.** A new `semantic_tokens_in_range` path resolves only the scopes a viewport touches, cached per scope, so large files no longer classify every body up front.

Under the hood, the contextual keywords `as`, `type`, `true`, `false`, and `null` are now re-lexed from bare words into their own syntax kinds (`KW_AS`, `KW_TYPE`, `KW_TRUE`, `KW_FALSE`, `KW_NULL`). This is invisible in valid source, and a parser fix ensures incomplete raw strings (a trailing `#` run at end of file) no longer index past the token stream.

Authors: rossirpaulo, codeshaunted

## Editor semantic highlighting is now driven by name resolution, with token modifiers and interface goto-definition

0.14.2-nightly.20260713.b · nightly · 2026-07-13

The LSP semantic-tokens layer was rewritten so that identifiers inside expression bodies are colored by what they *resolve to* rather than by their inferred type or by substring scanning. In practice your editor now distinguishes a `defaultLibrary` name (from the `baml` package) from your own declarations, marks definition sites with a `declaration` modifier, and highlights parameters and `catch` bindings distinctly from plain variables.

- **Token modifiers.** Semantic tokens now carry a modifier bitset with `declaration`, `defaultLibrary`, `deprecated`, `readonly`, and `async`. A name at its definition site (`function FirstTwo` -> `FirstTwo`) gets `declaration`; names resolved into the `baml` standard library get `defaultLibrary`.
- **New token types.** `true` and `false` now emit a `boolean` token, and string escape sequences emit `escapeSequence`, so both are colored separately from numbers and string bodies.
- **Goto-definition on virtual interface members.** `p.as<Named>.fullname` and `d.as<Serializer>.encode()` previously had no jump target. They now navigate to the field or method declaration inside the interface (the required signature, or the default method's definition).
- **Hover on pattern and `catch` bindings.** Type info for a `catch (e)` binding or a destructured pattern binding now reports the resolved type instead of `unknown`, matching what completions already showed.
- **Range requests.** A new `semantic_tokens_in_range` resolves only the scopes a viewport touches, and its output is guaranteed to equal the full-document result filtered to that range.

Internally, the contextual words `as`, `type`, `true`, `false`, and `null` are now re-lexed into dedicated keyword kinds (`KW_AS`, `KW_TYPE`, and so on) instead of matching on raw `Word` text. This changes how these words are classified for highlighting but does not change what the language accepts.

Authors: rossirpaulo, codeshaunted

## BAML syntax highlighting now ships for highlight.js and tree-sitter

0.14.2-nightly.20260713.a · nightly · 2026-07-13

BAML syntax highlighting is now maintained for highlight.js and tree-sitter, in addition to the existing TextMate grammar, so `.baml` files can highlight in tree-sitter editors (Neovim, Helix, Zed) and highlight.js-based renderers (docs sites, markdown-it, rehype-highlight).

Highlights:

- **`@boundaryml/baml-highlightjs`**: a new highlight.js language definition (`pkg-grammar-hljs`) covering the full BAML surface. It handles declaration blocks (`class`, `enum`, `interface`, `function`, `client<llm>`, `generator`, `retry_policy`, `template_string`, `test`), Jinja `{{ ... }}` / `{% ... %}` markup inside `#"..."#` prompt bodies, `${ ... }` interpolation in backtick strings, and `env.VAR` references. It uses no `illegal` patterns, so highlighting never bails out on arbitrary prompt prose.
- **tree-sitter grammar** (`pkg-grammar-treesitter`, mirrored to `BoundaryML/baml-treesitter`): a full grammar plus `queries/highlights.scm` and `queries/injections.scm`, with prompt bodies injected as `jinja`. Consumers pin it by commit for nvim-treesitter, Helix, and Zed. It ships a generated `src/parser.c` so the mirror builds directly.
- **Constructor field shorthand** highlighting: the TextMate grammar now recognizes shorthand constructor fields (`name value` without a `:`), so a field name followed by a literal, `[`, `env`, or a string is colored as a property.

Both new definitions validate against the shared `pkg-grammar/tests/fixtures/*.baml` corpus in CI, and the sync workflow propagates changes to the read-only mirror repos on every grammar edit. Note the first npm publish of each package is a one-time manual bootstrap, so availability on npm depends on that step being completed.

Registering the highlight.js definition:

```javascript
import hljs from "highlight.js/lib/core";
import baml from "@boundaryml/baml-highlightjs";

hljs.registerLanguage("baml", baml);
const { value } = hljs.highlight(code, { language: "baml" });
```

Authors: hellovai

## Grammar mirror sync now stages new files before committing

0.14.2-nightly.20260712.d · nightly · 2026-07-12

No user-facing BAML changes. This nightly only fixes the `sync-grammar-mirror.yml` release workflow, which used `git commit -am` and silently dropped untracked files (`dist/`, `package.json`, the publish workflow) from the mirror commit. It now runs `git add -A` before committing so newly assembled files are included.

Authors: hellovai

## The BAML TextMate grammar is now published to npm as `@boundaryml/baml-grammar`

0.14.2-nightly.20260712.c · nightly · 2026-07-12

The BAML TextMate grammar is now published to npm as `@boundaryml/baml-grammar`, with a ready-to-use Shiki `LanguageRegistration` as its default export. If you build your own tooling that highlights BAML, you no longer need to vendor the grammar by hand.

- **`@boundaryml/baml-grammar` on npm.** The package exports the grammar inlined as a JS object literal (`dist/index.js`), so you can import it without JSON import attributes (`with { type: "json" }`), which some bundlers such as Metro cannot parse. The raw `baml.tmLanguage.json` and a `language-configuration.json` are also exported for vscode-textmate and Monaco. The package is assembled from the monorepo and pushed to the `BoundaryML/textMate-baml` mirror on every grammar change, so it stays in lockstep with the editor extension.
- **`.baml` highlighting on github.com.** The mirror is the source GitHub Linguist vendors, so `.baml` files will render with the same grammar. A new `linguist-compile-gate` CI job runs Linguist's own Oniguruma-to-PCRE grammar compiler against every grammar change, so a regex that would silently break github.com fails the PR instead.
- **Auto-close fix.** Typing `{#` now auto-closes with `#}` instead of the previous `}`, a corrected auto-closing pair in `language-configuration.json`.

```typescript
import { createHighlighter } from "shiki";
import baml from "@boundaryml/baml-grammar";

const highlighter = await createHighlighter({
  themes: ["github-dark"],
  langs: [baml],
});

highlighter.codeToHtml(code, { lang: "baml", theme: "github-dark" });
```

Authors: hellovai

## `baml agent install` works without GitHub API credentials and installs into the right directory

0.14.2-nightly.20260712.b · nightly · 2026-07-12

`baml agent install` no longer calls the GitHub REST commits API, so a default install no longer hard-fails against the unauthenticated 60-requests-per-hour rate limit.

**Highlights**

- **No API dependency.** The default install now downloads the skill repo's `main`-branch tarball directly from codeload, which needs no credentials. The recorded commit provenance is recovered from the tarball's pax global header (written by `git archive`) instead of a separate commits lookup. A mirror whose tarball omits that header still installs fine, just with no recorded provenance.
- **Better failure guidance.** When the tarball host is unreachable, the error now tells you to install from a local copy with `baml agent install --from <url-or-path>`.
- **Install-root selection.** With `--dir` omitted, BAML now picks the nearest ancestor holding `baml.toml` or `baml_src` within your git repo, falling back to the repo root. Outside a git repo the walk stops before your home directory, so a stray `~/baml_src` no longer drags installs into `$HOME`, and the last resort is the current directory.

```bash
baml agent install --from <url-or-path>
```

Authors: ATX24

## Associated types inside an interface body must now be written `Self.Item`

0.14.2-nightly.20260712.a · nightly · 2026-07-12

References to an interface's own associated types must now be qualified with `Self.` inside the interface body. Where `Iterable` declares `type Item`, the method signatures write `Self.Item`, not the bare `Item`. The entire standard library was migrated to match, so this is the shape your own interfaces need too.

**Highlights**

- **`Self.`-qualified associated types.** Across `iter.baml`, `comparable.baml`, and `containers.baml`, the bare `Item`, `Error`, `Sum`, and `CompareError` references became `Self.Item`, `Self.Error`, `Self.Sum`, and `throws Self.CompareError`. Bare associated-type references inside an interface no longer resolve to the declaring interface.
- **Interface methods require an explicit `throws`.** A required method like `function find(self) -> Self.Record` must now declare its throw set. Add `throws never` for methods that cannot throw.
- **`as<I>` casts pin associated types.** Casting a value through an interface now carries the associated bindings: `account.as<PublicIdentity<Key = string>>.key` rather than `account.as<PublicIdentity>.key`.
- **Canonical projection rendering.** A projection off a bounded type variable such as `T.Item` now prints in its fully determined triple form `(T as BoxLike).Item` in `baml run` and `baml list` output.
- **Bound method values off interface receivers.** You can now take a bound method value (`let f = x.next`) from a receiver whose concrete type is not known statically (an interface existential or a bounded type variable); the impl is resolved at bind time.

```baml
interface Summable {
  type Sum

  function sum(self) -> Self.Sum throws never
}
```

The rest of the change is a large internal rework of interface and associated-type resolution in TIR, including removal of the legacy `implements_for` item-tree representation in favor of the unified `impls` store. Those are not observable beyond the surface changes above.

Authors: 2kai2kai2

## Remove the `baml_language/demo` example project from the repository

0.14.2-nightly.20260711.a · nightly · 2026-07-11

This release deletes the `baml_language/demo/baml_src` demo folder and its example `.baml` files. No compiler, runtime, or language behavior changes, so nothing in your own projects is affected.

Authors: sxlijin

## `baml` warns when your toolchain or agent skill is outdated, and `agent install` now tracks the skill repo head

0.14.2-nightly.20260710.f · nightly · 2026-07-10

Running any `baml` command now prints a `warning:` on stderr when your active toolchain has fallen behind its channel's latest release, or when your project's installed agent skill is stale or missing. The warnings point you at the exact fix (`baml toolchain update` or `baml agent install`), and the checks that back them run in the background against a 5 second budget so they never stall the command you actually ran.

Highlights:

- **Toolchain and skill freshness warnings.** When the cached channel manifest is newer than your active toolchain you get `Your version of baml for toolchain: <channel> is outdated. Update it with baml toolchain update.` When your installed skill commit is behind the repo head you get `Your baml skill is outdated, use baml agent install to upgrade it.`, and a project with no skills installed gets `No baml skill is installed, set it up with baml agent install.` Color is dropped when stderr is not a TTY.
- **`baml agent install` now installs from the skill repo head.** The default source resolves the current head commit of `BoundaryML/baml-skill`, downloads the tarball at that exact commit, and records the installed commit under a `[skills]` section in `~/.baml/state.toml`. That provenance clears the outdated warning immediately after install.
- **`--latest` removed.** The `baml agent install --latest` flag is gone because installing from the repo head is now the default. `--from` (a tar.gz URL, a local archive, or a local directory) still works and no longer conflicts with anything; installs from a custom `--from` source record no commit provenance.
- **`[update] auto_check` opt-out.** The background freshness checks are on by default. Set `auto_check = false` in `~/.baml/config.toml` to turn them off.

To disable the background checks:

```toml
[update]
auto_check = false
```

Authors: ATX24

## Host integers now widen to `float` at the call boundary

0.14.2-nightly.20260710.e · nightly · 2026-07-10

Passing a host integer into a BAML `float` slot now hands back a genuine float instead of the int riding through unconverted. Value-shaped host encoders (Python `7`, integral JS `Number`s) arrive on the Rust side as ints, and a declared `-> float` return or a `float` parameter now coerces them at the FFI boundary.

- **`int` to `float` widening.** A `-> float` function called with a Python int returns a real float. `round_trip_float(x=7)` now returns `7.0` and `isinstance(result, float)` holds.
- **Numeric union member selection.** A host int arriving at a numeric union picks the best-fitting member rather than the first declared one. An exact `int` member wins outright, so `int | float | bigint` lands on `int`. Between `bigint` and `float` the lossless `bigint` wins regardless of declaration order, so both `bigint | float` and `float | bigint` land on `bigint`.
- **`baml playground` session token removed.** Browser mode no longer mints a per-session token, so the printed URL is a bare `http://localhost:<port>/` with no `?token=` query parameter, and `/api` requests are no longer rejected for a missing token.

```python
result = round_trip_float(x=7)
assert isinstance(result, float)
assert result == 7.0
```

Authors: sxlijin, rossirpaulo

## Playground seeds function args from your `test` blocks, and `baml ide install` renames `--path` to `--dir`

0.14.2-nightly.20260710.d · nightly · 2026-07-10

The playground now reads the `functions` and `args` from your legacy `test` blocks and offers each one as a one-click preview that fills in a function's arguments without running anything.

- **Test previews in the function sidebar.** Each `test` block that names one or more functions shows up as a `preview` row. Clicking it loads that test's `args` into the selected function's args editor (form and raw modes both populate), so you can inspect or tweak the inputs before running. A test that targets multiple functions produces one preview row per function. Selecting a preview never sends a run.
- **Typed args, not strings.** Test `args` are now parsed into their real JSON shapes. Ints, floats, bools, `null`, arrays, nested maps, and raw strings (`#"..."#`) all round-trip into the args editor instead of collapsing to text.
- **`baml ide install --path` is now `--dir`.** The flag that copies the VSIX into a directory was renamed because `--path` was ambiguous about file versus directory. Update any scripts that used `--path`.
- **Clearer `baml ide install` failures.** When the `code` or `cursor` CLI is not on your PATH, the error now explains the automatic install could not run and walks through the manual VSIX install, with OS-specific wording for the download directory and Command Palette shortcut.

```bash
baml ide install --dir ~/Downloads
```

Authors: rossirpaulo, sxlijin

## `baml playground` reuses one browser window instead of opening a new one on every open

0.14.2-nightly.20260710.c · nightly · 2026-07-10

Running `baml playground` and triggering "Open Playground" repeatedly no longer stacks a new browser tab each time. Browser mode now navigates the already-open page over the WebSocket and only spawns a system window when nothing is connected, with a short debounce so a second window is not launched while the first is still loading. A page that connects after the request (a fresh window or a reconnect) is replayed the last open target on `RequestState`, so it lands on the right function and test.

Highlights:

- **Browser-mode playground window reuse.** Repeated opens navigate the existing page in place instead of opening additional windows.
- **Args form reconciliation on schema change.** The playground arguments form now reconciles cached values against the current function schema through a new `reconcileArgs` path (replacing the old seed-plus-normalize logic). If you hot-reload a function and its class gains a required field, the form adds that field's default while keeping the values you already typed, and required nested classes are populated deeply enough that every default shown by a widget is also present in the encoded value. Parameters with declared BAML defaults stay absent so the runtime evaluates their defaults.
- **Gemini model docs updated.** Reference and guide examples now use Gemini 3.x model names such as `gemini-3.5-flash` and `gemini-3.1-pro-preview`, and the `google-ai` `model` option is documented as required with no default. This is a documentation change only.

Authors: sxlijin, rossirpaulo, hellovai

## `baml playground` reuses one browser window instead of stacking a new one per open

0.14.2-nightly.20260710.b · nightly · 2026-07-10

Running `baml playground` in browser mode now navigates the already-open page instead of spawning a fresh window on every `OpenPlayground`. A new window is spawned only when no page is connected, with a 5 second debounce so a second window is not opened while the first is still loading.

- **Browser-window reuse.** In browser mode, `OpenPlayground` broadcasts a navigation to any connected page over the WebSocket and records the target. A page that connects after the request (a freshly spawned window or a reconnect) is navigated to that same target when it requests state, so repeated opens land in one window rather than piling up new ones.
- **Form state reconciliation on hot reload.** The playground args form now reconciles cached argument values against the current function schema via a new `reconcileArgs` path that replaces the old seed-and-normalize logic. When a same-name function changes type, for example a class gaining a required field, compatible values are kept, incompatible ones reset to schema defaults, and newly required fields are filled in. Parameters with declared BAML defaults are left absent so the runtime evaluates their defaults.
- **Docs: Gemini model references updated.** The Google AI and Vertex provider docs and examples now reference current Gemini models (`gemini-3.5-flash`, `gemini-3.1-pro-preview`, `gemini-3.1-flash-lite`). The `model` field on the `google-ai` provider is now documented as required with no default.

Authors: sxlijin, rossirpaulo, hellovai

## `baml ide install` gains a `--path` flag, and the `watch` graph no longer retains objects past scope

0.14.2-nightly.20260710.a · nightly · 2026-07-10

`baml ide install` now accepts `--path <PATH>`, which copies the active toolchain's BAML VSIX into a directory as `baml-vscode.vsix` for manual install instead of shelling out to an editor.

Highlights:

- **`--path <PATH>`** writes the VSIX to the given directory (creating it if needed) and prints where it landed, for when you want to install the extension by hand.
- **`--code` and `--cursor` no longer conflict**, and both now verify the editor CLI is actually on `PATH`. If it is missing you get a direct error like ``VS Code CLI `code` was not found on PATH`` instead of a failed spawn.
- **Windows editor resolution** now prefers the `.cmd` shim over a bare match when scanning `PATH`, which fixes locating the `code` CLI for the IDE install.
- **`watch` no longer over-retains**: ordinary field, array, and map mutations in code that does not use `watch` no longer register objects in the Watch graph, and frame teardown (including a native panic unwinding a callee) unregisters that frame's watched locals. Objects are released once a `watch let` binding leaves scope rather than being held as stale GC roots.

```bash
baml ide install --path ./dist
```

Authors: jubjub727, aaronvg

## CI-only release: size-gate now tolerates fork pull requests

0.14.2-nightly.20260709.g · nightly · 2026-07-10

No user-facing changes. This release only touches GitHub Actions workflows. The size-gate jobs were renamed (`measure-baml-size` per platform, `enforce-baml-size` for the aggregate report) and the PR comment step is now best effort with `continue-on-error`, so fork PRs running with a read-only `GITHUB_TOKEN` no longer fail when the comment cannot be posted. The report is still printed to the workflow log and the summary tab. The docs and backend preview jobs also switched to `marocchino/sticky-pull-request-comment`. Nothing in the BAML language, compiler, or runtime changed.

Authors: sxlijin

## React streaming hooks now type `data` and `onData` as the partial streaming shape

0.14.2-nightly.20260709.f · nightly · 2026-07-09

The `data` field and `onData` callback on the generated React hooks no longer widen to `FinalDataType | StreamDataType` when streaming. They now resolve to `StreamDataType` alone, so nullable and incomplete fields carry the correct partial types while a response is still streaming.

- **`HookOutput` and `HookInput`**: for `{ stream: true }`, `data?` and `onData` are typed as `StreamDataType<FunctionName>` instead of a union with the final type. Fields that are not yet complete are correctly nullable, and accessing a final-only field without null handling is now a type error.
- **`server_streaming_types.ts`**: the generated `StreamingServerTypes` map now references `partial_types.*` (for example `partial_types.Recipe`, `partial_types.Resume`) rather than the final `types`, matching the partial shape delivered during a stream.

```typescript
const options: HookInput<'MakeSemanticContainer', { stream: true }> = {
  stream: true,
  onData: response => {
    if (!response) return
    const status: 'Pending' | 'Incomplete' | 'Complete' = response.class_needed.s_20_words.state
    void status
  },
}
```

Authors: mvanhorn

## Named `client<llm>` blocks default `api_key` to the provider's env var

0.14.2-nightly.20260709.e · nightly · 2026-07-09

A named `client<llm>` using `provider openai` or `provider anthropic` with no explicit `api_key` now defaults the key to the provider's conventional environment variable, matching what the inline `"openai/model"` shorthand already did.

```baml
client<llm> C {
  provider openai
  options { model "gpt-4o" }
}
```

With this client and no `api_key` set, the compiler synthesizes a read of `OPENAI_API_KEY` (and `ANTHROPIC_API_KEY` for `anthropic`, `OPENAI_API_KEY` for `openai-responses`).

- **Soft read, no panic.** The default reads through `baml.env.get`, so an unset variable yields `null` rather than failing. Offline paths like `render_prompt` keep working without the variable present, and the key is never eagerly required.
- **Explicit config wins.** Writing `api_key` in `options`, including `api_key null`, suppresses the default so your configuration is never overridden.
- **Other providers untouched.** Providers without a ubiquitous env-var convention (for example `vertex-ai`) get no default and behave as before.
- **LSP visibility.** The synthesized dependency is surfaced to env-var tooling, so the defaulted variable shows up in the playground's env panel anchored at the `options` block.

Authors: antoniosarosi

## Interpolating a class, array, or map inside a `prompt` string now renders instead of producing an empty string

0.14.2-nightly.20260709.d · nightly · 2026-07-09

Interpolating a composite value (`${class_value}`, `${array_value}`, or a map) inside a `prompt` backtick string used to silently render an empty string. The assembler only stringified scalars and dropped every composite. Now every interpolated value routes through `string.from`, the same implicit `to_string` an ordinary backtick string applies, so the `prompt` form is byte-identical to the ordinary-string form.

- **Composites render in prompts.** A class or array in `${...}` renders (for example `Point { x: 1, y: 2 }` and `[10, 20, 30]`) rather than vanishing.
- **`baml.ToString` overrides are honored.** Because values go through the real implicit `to_string`, a user `to_string` override applies inside a `prompt` exactly as in an ordinary string.
- **`${role(...)}` unchanged.** A `Role` marker still passes through raw, so `role(...)` continues to split a prompt into chat messages.

Authors: antoniosarosi

## `~` now computes bitwise NOT, enum fields in unions serialize by name, and delay-less `retry_policy` blocks stop crashing

0.14.2-nightly.20260709.c · nightly · 2026-07-09

Three bug fixes this nightly, all observable at runtime.

- **`~` (bitwise NOT) now works.** It previously lowered to the identity, so `~x` silently returned `x`. It now computes two's-complement complement (`~x == -x - 1`): `~0` is `-1`, `~255` is `-256`, `~(-1)` is `0`. The one corner is `~INT_MIN`, which throws on the intermediate negation of `INT_MIN` rather than returning a wrong answer.
- **Enum and string-literal-union fields serialize as strings.** A class field whose declared type is a union containing an enum, including the optional enum `E?` (which is `E | null`), serialized as `null` through the typed `baml.json.to_string<T>` path. It now renders the variant name, matching the direct-enum path. So `class C { e E? }` with `e: E.A` serializes to `{"e":"A"}`, and a `"X" | "Y"` field serializes to the plain string.
- **`retry_policy` blocks that omit delay fields no longer crash.** A policy with only `max_retries` (or a `strategy { ... }` sub-block) left `initial_delay_ms`, `multiplier`, and `max_delay_ms` as `null`, and the retry-delay arithmetic aborted every call with `VM internal error: cannot apply binary operation: any + float` before any request went out. The fields are now optional and fall back to the legacy defaults (200ms initial, 1.5x multiplier, 10000ms max).

```baml
~255 == -256
```

Authors: antoniosarosi

## `retry_policy` blocks that omit delay fields no longer crash the retry loop

0.14.2-nightly.20260709.b · nightly · 2026-07-09

Three correctness fixes land in this nightly, all from `antoniosarosi`.

- **`retry_policy` with defaulted delay fields no longer aborts every call.** `initial_delay_ms`, `multiplier`, and `max_delay_ms` are now optional on `RetryPolicy`. A policy that sets only `max_retries`, or that uses a `strategy { ... }` sub-block, previously left those fields `null`, and the retry-delay math evaluated `null + 0.0`, failing with `VM internal error: cannot apply binary operation: any + float` before any HTTP request went out. The backoff now substitutes the legacy defaults (200ms initial, 1.5x multiplier, 10000ms cap) when a field is unset.
- **Bitwise NOT (`~`) now computes the complement instead of returning its operand.** `~x` was silently lowered to the identity, so `~x == x`. It now desugars to `-x - 1`, so `~0` is `-1` and `~255` is `-256`. The one corner case, `~INT_MIN`, throws on the intermediate negation like any other `-INT_MIN`, rather than returning a wrong answer.
- **Enum fields in union or optional types serialize as their variant name, not `null`.** A class field typed as a union containing an enum, including the optional enum `E?` (which is `E | null`), rendered the held variant as `null` through the typed `baml.json.to_string<T>` walker. It now emits the variant-name string, matching the plain-enum path.

```baml
baml.deep_equals(~1, -2)
```

Authors: antoniosarosi

## Enum-typed union and optional (`E?`) class fields now serialize as the variant name instead of `null`

0.14.2-nightly.20260709.a · nightly · 2026-07-09

A class field whose declared type is a union containing an enum, including the optional enum `E?` (equivalent to `E | null`), previously serialized to `null` through the typed `baml.json.to_string<T>` walker instead of the enum variant name. The typed walker's `RuntimeTy::Union` arm delegates to the untyped converter, which had no case for enum variants and fell through to `null`. That converter now renders a variant as its name string, matching the structural and typed paths.

**Highlights**

- **Enum-typed union and optional fields serialize as the variant name, not `null`.** A field typed `E?` (`E | null`) or `E | F` now serializes the held variant as its plain-string name through both `.to_json()` and `baml.json.to_string`. Before this fix such fields came out as `null`. String-literal-union fields (`"X" | "Y"`) already serialized correctly as the plain string; the new `string_literal_union_field_serializes_as_string` test only locks that in.
- **`retry_policy` no longer crashes when delay fields are omitted.** The delay fields `initial_delay_ms`, `multiplier`, and `max_delay_ms` are now optional. A policy that gives only `max_retries` (or a `strategy { ... }` sub-block) leaves them `null` at runtime, and the retry-delay math used to evaluate `null + 0.0`, aborting every call with `VM internal error: cannot apply binary operation: any + float` before any request. New `*_or_default` accessors substitute the canonical defaults (200ms initial delay, 1.5x multiplier, 10000ms max delay) so the arithmetic stays well-typed.
- **Bitwise NOT (`~`) now computes the complement instead of the identity.** `~` previously lowered to a silent identity (`~x == x`). It now desugars to `-x - 1`, so `~0 == -1`, `~255 == -256`, and `~(-1) == 0`. The one corner is `~INT_MIN`: its result is representable but the intermediate `-INT_MIN` overflows, so it errors like any other `-INT_MIN` rather than returning a wrong value.

```baml
test "bitwise_not" {
    assert.is_true(baml.deep_equals(~0, -1))
    assert.is_true(baml.deep_equals(~1, -2))
    assert.is_true(baml.deep_equals(~255, -256))
    assert.is_true(baml.deep_equals(~(-1), 0))
}
```

Authors: antoniosarosi

## Test-only nightly: SDK coverage for reified generics returned by non-generic functions

0.14.2-nightly.20260708.a · nightly · 2026-07-08

No user-facing changes. This nightly adds Python SDK test coverage for reified generics flowing outbound from non-generic functions and does not alter compiler, runtime, or SDK behavior.

The new fixtures in `ns_generic_tests/types.baml` declare functions whose return types pin the class type arguments at the definition site (no caller binding, no inference): `make_int_box() -> GenericBox<int>`, `make_int_container() -> ContainerShapes<int>`, `make_nested_box() -> GenericBox<GenericBox<int>>`, and `make_int_str_bool_triple() -> GenericTriple<int, string, bool>`. The matching `test_generic_calls.py` tests assert the decoded Pydantic instances carry the expected bound type args via `__pydantic_generic_metadata__`. If you are not working on BAML's own test suite, there is nothing to act on here.

Authors: sxlijin

## Version stamp bump to 0.14.1 with no code changes

0.14.1-nightly.20260708.b · nightly · 2026-07-08

This release only bumps the stamped version from `0.14.0` to `0.14.1` across `CANONICAL_VERSION`, `PYPI_VERSION`, `STABLE_VERSION`, `release.toml`, the Python and Node.js SDK packages, and the VS Code extension. There are no behavioral changes.

Authors: rossirpaulo

## `baml.math` is removed: `sum`, `mean`, and `median` are now `float[]` methods

0.14.1 · canary · 2026-07-08

The `baml.math` namespace no longer exists. Its aggregates moved onto numeric-array types, so `baml.math.mean(xs)` and `baml.math.median(xs)` are now the methods `xs.mean()` and `xs.median()`, and `baml.math.sum(xs)` is `xs.sum()`. This channel also makes `baml playground` usable over SSH tunnels.

**Highlights**

- **`baml.math` aggregates are now methods.** `mean` and `median` attach to `float[]` via the new `FloatStats` interface and both return `float`; `sum` remains a method on `int[]` (returning `int`) and `float[]` (returning `float`). `mean` and `median` throw `root.errors.InvalidArgument` on an empty array, and `median` sorts a copy so your input is left untouched. Rewrite `baml.math.mean(xs)` as `xs.mean()`, and likewise for `median` and `sum`.
- **SDK imports change.** The generated `baml.math` module is gone, so `from baml_sdk.baml.math import trunc` (Python) and `import { trunc } from ".../baml/math"` (TypeScript) no longer resolve.
- **`baml.math.trunc` has no public replacement.** It became a private saturating helper used internally for retry-delay math. For user code, use the range-checked `float.itrunc()` or the float-returning `float.trunc()` instead.
- **`baml playground` over SSH.** New `--port` pins the listen port (erroring if it is taken) and `--no-open` skips launching a browser. A browser is now also skipped automatically in headless sessions (SSH, or a Linux session with no display). The default port is `4265` (scanning up to `4364`), previously `3700`.
- **Per-session playground token.** In browser mode the printed URL carries a `?token=...` query parameter that authorizes `/api` requests. Open the URL verbatim and treat the token as a per-session secret; requests without it are rejected.

```baml
function summary_stats() -> float[] {
  let xs = [3.0, 1.0, 2.0, 4.0];
  [xs.sum(), xs.mean(), xs.median()]
}
```

To reach the playground on a remote host, forward the port and open the printed URL locally:

```bash
ssh -L 4265:localhost:4265 user@host
```

Authors: rossirpaulo, antoniosarosi

## The `baml.math` namespace is gone: `sum`/`mean`/`median` are now `float[]` methods, and `baml playground` runs over SSH tunnels

0.14.1-nightly.20260708.a · nightly · 2026-07-08

The `baml.math` namespace has been removed. Its aggregations are now methods on the array itself: call `[1.0, 2.0].mean()` instead of `baml.math.mean([1.0, 2.0])`. This is a breaking change. Any code that referenced `baml.math.sum`, `baml.math.mean`, `baml.math.median`, or `baml.math.trunc` will no longer compile.

**Highlights**

- **`float[].mean()` and `float[].median()`** are new methods (via a `FloatStats` interface). Both return `float` and throw `baml.errors.InvalidArgument` on an empty array. `median` sorts a copy, so your array is left untouched.
- **`sum()` is unchanged in behavior** but is now the only public surface for summation. It stays a method on both `int[]` (returns `int`, can raise `IntegerOverflow`) and `float[]` (returns `float`). `int` and `float` do not implicitly widen, so map integer data first: `ints.map((x: int) -> float { x * 1.0 }).sum()`.
- **`baml.math.trunc` has no drop-in replacement.** The saturating truncation moved to an internal helper. Use the range-checked `float.itrunc()` (throwing) or the float-returning `float.trunc()` in your own code.
- **`baml playground` now works over SSH tunnels.** New `--port` pins the listen port (errors if taken) and `--no-open` suppresses the browser, which is also skipped automatically in headless or SSH sessions. The default port scan now starts at 4265 (was 3700), and the printed URL carries a `?token=...` that authorizes the session, so open it verbatim and treat it as a per-session secret.

```baml
[1.0, 2.0, 3.0, 4.0].mean()
```

```bash
ssh -L 4265:localhost:4265 user@host
baml playground --no-open
```

Authors: aaronvg, rossirpaulo, antoniosarosi

## The runtime package is renamed from `baml_core` to `baml_bridge`

0.14.0 · canary · 2026-07-08

The BAML runtime package is renamed from `baml_core` to `baml_bridge`. Update your install and dependency commands before upgrading, because the old package names no longer resolve.

**Runtime package rename (action required).**
- Python: install `baml_bridge` instead of `baml_core`.
- Node.js: install `@boundaryml/baml-bridge`, which replaces `@boundaryml/baml-core-node`.

```bash
pip install baml_bridge
npm install @boundaryml/baml-bridge
```

**Other changes:**

- **`baml init` scaffolds generator blocks.** New projects get commented-out `[generator.python_client]` and `[generator.node_client]` blocks in `baml.toml`, each with the matching `baml_bridge` / `@boundaryml/baml-bridge` install instructions inline.
- **`Array.generate(length, f)`** builds an array by calling `f` once per index, giving an independent value per slot. Use it instead of `Array.filled` when the value is a reference type, since `Array.filled` shares one object across every slot (and now warns on mutable-literal aliasing).
- **`Array.sum()`** is available on `int[]` (returning `int`) and `float[]` (returning `float`), alongside new `baml.math.sum`, `baml.math.mean`, and `baml.math.median` over `float[]`. `mean` and `median` throw `InvalidArgument` on an empty array.
- **`String.code_point_at(index)` and `String.to_code_points()`** expose Unicode code points as `int`s. `to_code_points` is the exact inverse of `string.from_code_points`.
- **`baml.fs.remove_dir` and `baml.fs.remove_dir_all`** delete directories; `remove` now documents that it handles regular files only.
- **Cause chains for caught errors.** A `catch (e, ctx)` handler binds an `ErrorContext` (error value, stack trace, and superseded `cause`), with `root_cause()` and a Python-style `to_string`.
- **Readable `PromptAst`.** `<Fn>$render_prompt` output now has `text()` and `messages()` accessors (the latter returning `PromptMessage[]`), and renders as readable text through `to_string`. `$render_prompt` also works offline without the client `api_key` env var set.
- **`_` type inference.** The `_` wildcard is now inferred in type-arg, `throws`-clause, and expression-context positions, e.g. `throws AppError | _` keeps `AppError` in the contract and infers the rest.
- **`$parse` throws `ParseError`.** An LLM function's `$parse` companion now surfaces a schema mismatch as `baml.errors.ParseError` rather than `LlmClient`, so `catch (baml.errors.ParseError)` is reachable.
- **Compiler is 2.9x faster on cold compile** (an empty project drops from 1.41s to 482ms).

**Other fixes:**

- `baml.json.to_json` and `baml.json.serialize` no longer list `JsonParseError` in their `throws` clause; they throw only `JsonSerializationError`.
- `baml run` renders an uncaught throw value readably instead of dumping Rust `Debug` output.
- `map<K, V>` renders as a JSON object shape in `ctx.output_format`.
- Duplicate `@alias` serialized keys are rejected within a class and on enum variants, and duplicate attributes on a single field now emit `E0014`.

Authors: hellovai, antoniosarosi, sxlijin, aaronvg, rossirpaulo, ATX24, 2kai2kai2

## `PromptAst.text()` and `.messages()` read a rendered prompt from BAML

0.13.1-nightly.20260708.f · nightly · 2026-07-08

A `PromptAst` (returned by `render_prompt` and the `prompt` tag) now has `text()` and `messages()` accessors, so you can inspect a rendered prompt from BAML instead of getting the opaque handle's Rust debug dump.

- **`PromptAst.text()`** renders the prompt as plain text: each chat message becomes a `[role]` header line followed by its content, with messages separated by a blank line. Role-less content (a prompt with no `${role(...)}` markers) has no header.
- **`PromptAst.messages()`** returns an ordered `PromptMessage[]`, where the new `PromptMessage` class has `role` and `content` string fields. Media parts render as a readable placeholder such as `image::url(...)`.
- **Readable `to_string`.** `PromptAst` now implements `baml.ToString`, so `string.from(prompt)`, string interpolation, and `to_string` all yield `text()` rather than leaking the internal handle. The CLI's value print is readable for the same reason.

```baml
function main() -> string {
  let cc = baml.llm.ContextClient { name: "c", provider: "openai", default_role: "system", allowed_roles: ["system", "user"] }
  let ctx = baml.llm.Context { client: cc, tags: {} }
  let render = prompt`${role("system")}You are helpful.${role("user")}Hi World!`
  let ast = render(ctx)
  ast.text()
}
```

The above evaluates to `[system]\nYou are helpful.\n\n[user]\nHi World!`. To inspect the fully-resolved provider request (URL, headers, remapped roles) instead, use `build_request`.

Authors: antoniosarosi

## `<Fn>$render_prompt` renders offline without the client's `api_key` env var set

0.13.1-nightly.20260708.e · nightly · 2026-07-08

`<Fn>$render_prompt` no longer requires the client's credential env var to be set. Rendering only needs the client's provider and role metadata, so the generated constructor now reads `env.X` options leniently on this path: a missing variable yields the empty string instead of panicking. Previously, rendering a prompt for a client whose `api_key` pointed at an unset env var panicked with `env var not found` before producing any output.

The network paths are unchanged. `<Fn>$call` and `<Fn>$build_request` still construct the client strictly, so a missing `api_key` env var still panics for them exactly as before. This keeps offline preview working without silently building an unauthenticated request.

```baml
client Fast {
    provider openai
    options {
        model "gpt-4o-mini"
        api_key env.OPENAI_API_KEY_UNSET
    }
}

function Extract(raw: string) -> string {
    client Fast
    prompt #"
        Extract from {{ raw }}.
        {{ ctx.output_format }}
    "#
}
```

With `OPENAI_API_KEY_UNSET` unset, `Extract$render_prompt("hello")` now returns the rendered prompt, while `Extract$build_request("hello")` still surfaces the missing variable.

Authors: antoniosarosi

## `map` fields now render as a JSON object shape in `ctx.output_format`

0.13.1-nightly.20260708.d · nightly · 2026-07-08

The default rendering of `map<K, V>` in `ctx.output_format` changed from the literal BAML type `map<string, int>` to a JSON object shape `{ "<string>": int }`, so the schema hint mirrors the object the model must actually emit instead of leaking BAML type syntax.

- **`map` output format (B-630).** Any function returning a `map`, or a class with a `map` field, now renders the object-literal shape by default. If you relied on the old angle-bracket form, opt back in with the `map_style='type_parameters'` kwarg, which is now the escape hatch.
- **`Array.generate(length, f)`.** New stdlib factory that calls `f` once per index `0..length-1` and stores each result in its own slot. Unlike `Array.filled`, which reuses one shared value across every slot, `generate` produces an independent value per slot, so building runtime-sized grids of arrays, maps, or class instances no longer aliases. A negative or zero `length` returns an empty array and never calls `f`; an error thrown by `f` propagates and halts generation. The `Array.filled` aliasing diagnostic now points to `Array.generate` instead of a `while` loop.
- **LLM `$parse` throws `ParseError` (B-625).** A local `Fn$parse(json)` on a JSON string is network-free, so a schema mismatch now surfaces as `baml.errors.ParseError` rather than `LlmClient`. The documented `catch (baml.errors.ParseError)` pattern is now reachable and no longer flagged E0063 unreachable arm.

```baml
baml.Array.generate(3, (i: int) -> int { i * i })   // [0, 1, 4]
```

Authors: antoniosarosi

## Uncaught `throw` values now render readably instead of as Rust `Debug` output

0.13.1-nightly.20260708.c · nightly · 2026-07-08

An uncaught `throw` that unwinds to the top now prints the error's structural form instead of leaking Rust internals. A thrown `baml.errors.Io { message: "boom" }` renders as `uncaught throw: baml.errors.Io {message: "boom"}` rather than `uncaught throw: Instance { class_name: "baml.errors.Io", type_args: [], fields: {"message": String("boom")} }`. A bare string throw now shows `"boom"` instead of `String("boom")`, and generic error instances omit their `type_args` rather than dumping `QualifiedTypeName` and `TyAttr` shapes.

- **Uncaught throw rendering.** The CLI traceback footer and `baml run` debug output share one structural renderer, so a leaked `throw` and normal value output now look identical. This affects the text you see in error output only, not caught-error handling.
- **`baml describe defer` and `baml describe cleanup`.** Both topics now resolve to keyword docs instead of falling through to "No symbol found". `defer` documents the LIFO block that runs on every scope exit; `cleanup` documents the by-name finalizer that runs at most once per instance.
- **`baml describe catch_all` clarified.** The docs now spell out that panics (`baml.panics.*`) are not part of an expression's throws-set, so a `_` wildcard in `catch_all` does not catch them. To intercept a panic you must name its type explicitly, for example `risky_call() catch (e) { baml.panics.Cancelled => ... }`.

```bash
baml describe defer
baml describe cleanup
```

Authors: antoniosarosi

## The playground Run tab gets a typed args form driven by function parameter schemas

0.13.1-nightly.20260708.b · nightly · 2026-07-08

The playground Run tab now renders a typed form, one widget per function parameter, instead of a single raw-JSON box. The form is built from parameter schemas the engine now ships alongside each function, so strings get text inputs, enums get chips or a dropdown, and lists, maps, and class fields get their own nested sections. A `form`/`raw` toggle lets you drop back to editing the JSON directly, and both views write the same `argsJson`, so switching between them never loses your edits.

- **Typed args form.** Selecting a function in the Run tab populates the form from its parameters. Optional parameters (those with a default) show a `set` switch and, when off, display "omitted, uses the declared default". A function with no parameters shows "This function takes no arguments" rather than an empty box.
- **Enum arguments now encode correctly.** A `$baml` enum marker on the args path serializes to a real enum variant. Before this, nothing coerced a plain string into an enum variant, so an expr function's `param == Color.Red` was silently false. The form emits the marker for you; if you hand-edit raw JSON, the shape is `{ "$baml": { "enum": "user.Color", "value": "Red" } }`, and a malformed marker now throws instead of being misread as a map.
- **Run shortcut.** Cmd+Enter (Ctrl+Enter on non-Mac) from any args field runs the selected function.

```json
{
  "name": "Ada",
  "color": { "$baml": { "enum": "user.Color", "value": "Green" } }
}
```

An older WASM binary that ships no schema falls back to raw-only mode, so nothing breaks if the engine and playground are out of step.

Authors: rossirpaulo

## The LSP no longer grows to tens of GB of RSS during long editing sessions

0.13.1-nightly.20260708.a · nightly · 2026-07-08

This release fixes a memory leak in the BAML language server that could grow resident memory into the tens of GB (50GB was reported) over a long editing session. If your editor's BAML process has been ballooning as you type, this is the fix.

The leak had several independent sources, each addressed:

- **Terminal playground runs are now capped in memory.** The in-memory run store keeps at most 100 completed runs; older ones are evicted and rehydrated on demand from the disk-backed history store when you open them. Opening a run in the playground works the same as before.
- **Deleting and recreating a `.baml` file no longer leaks.** Salsa never frees inputs, so branch switches or codegen rewriting `.baml` files used to mint a new immortal input every cycle. Removed files are now emptied and parked in a tombstone map, then revived if the same path reappears.
- **Inlined control-flow graphs are capped and built lazily.** Fully-inlined graphs grow with call-site fan-out and were snapshotted eagerly for every function on every compile. Graphs are now capped at 5,000 nodes and built only for the function a run actually executes, keyed by `(generation, function name)`.
- **Workspace discovery prunes ignored directories.** Project discovery now walks with the `ignore` crate, so `.gitignore`d paths and `target/`, `node_modules/`, and `dist/` are skipped instead of scanned.

Engine rebuilds are also debounced off the keystroke path, so a burst of edits no longer triggers a rebuild per character. There is no change to BAML syntax or any command you run.

Authors: hellovai

## `match` and `is` now discriminate lists and maps by element type

0.13.1-nightly.20260707.f · nightly · 2026-07-07

`match` and `is` now distinguish container types by their element type. Previously `int[]` and `string[]` both lowered to a single coarse `LIST` type tag, so a `string[]` value could take an `int[]` arm, and a `map<string, int>` arm accepted a `map<string, string>` value. The fix routes element-discriminating container templates through the VM's structural value matcher, which relates generic-argument positions invariantly.

```baml
function classify_list(x: int[] | string[]) -> string {
    match (x) {
        let a: int[] => "ints",
        let b: string[] => "strings",
    }
}
```

Highlights:

- **Container element matching.** `match` and `is` discriminate lists by element type and maps by value type. Generic-argument positions are invariant, so `int[]` is not `string[]` and `map<string, int>` is not `map<string, string>`.
- **Enum type identity.** A bare enum type test now compares enum-pointer identity, so `Color` and `Status` no longer conflate. Previously both shared the single `ENUM` tag and a `Status` value could route to a `Color` arm.
- **Type-variable arms.** `match` arms and `is` tests on `T`, `T[]`, and `map<string, T>` compare the value against the type variable's realized binding in the enclosing call frame. A bare `T` arm previously lowered to a constant-false test and silently fell through.
- **`Self` in a method body is now an error.** Using `Self` in a body type position (a `let` or cast annotation, a `match`/`is` pattern type, or a `type_of<Self>()` turbofish) is rejected with a diagnostic instead of silently resolving to a stray type variable. `Self` remains supported in signatures. In a class method, name the enclosing type explicitly.
- **Runtime package renamed.** The Python runtime is now installed as `baml_bridge` (was `baml_core`), and the Node.js runtime as `@boundaryml/baml-bridge` (was `@boundaryml/baml-core-node`). The generated `baml_sdk` package you import is unchanged.

Authors: sxlijin, 2kai2kai2

## `match` and `is` discriminate list, map, and enum types precisely, and the runtime package is renamed to `baml_bridge`

0.13.1-nightly.20260707.e · nightly · 2026-07-07

`match` arms and `is` tests on container and enum types now discriminate by element and identity instead of collapsing to a coarse tag, so `int[]` no longer captures a `string[]` value and `Color` no longer captures a `Status` value.

**Highlights**

- **Container element matching.** `int[]` and `string[]` used to lower to a single `LIST` tag, so a `string[]` value wrongly matched an `int[]` arm. The same held for maps by value type. Both now route through the structural value matcher and relate generic-argument positions invariantly, so `int[]` is not `string[]` and `map<string, int>` is not `map<string, string>`.
- **Enum type matching.** A bare enum type test previously lowered to the shared `ENUM` tag and could not tell `Color` from `Status`. `is Color` now compares enum identity. Enum *variant* matching (`Status.Active`) is unchanged.
- **Type-variable arms.** Arms like `let t: T`, `let t: T[]`, and `let t: map<string, T>` now compare against the realized type bound to `T` in the enclosing frame, rather than missing silently (`T`) or matching any list (`T[]`).
- **`Self` in a method body is now rejected.** Using `Self` in a body type position (a `let`/cast annotation, a `match`/`is` pattern type, or a `type_of<Self>()` turbofish) is a compile error instead of silently resolving to a stray type variable. In a class method, name the enclosing type explicitly. `Self` in signatures is unaffected.
- **Runtime package renamed.** The Python runtime is now `baml_bridge` (was `baml_core`) and the Node runtime is `@boundaryml/baml-bridge` (was `@boundaryml/baml-core-node`). Update your install commands.

```baml
function classify_list(x: int[] | string[]) -> string {
    match (x) {
        let a: int[] => "ints",
        let b: string[] => "strings",
    }
}

function classify_string_list_fn() -> string {
    classify_list(["a", "b"])
}

test "match_container_string_list" {
    assert.equal(classify_string_list_fn(), "strings")
}
```

```bash
pip install baml_bridge
npm install @boundaryml/baml-bridge
```

Authors: sxlijin, 2kai2kai2

## The runtime package is renamed to `baml_bridge`, and `match`/`is` now discriminate containers by element type

0.13.1-nightly.20260707.d · nightly · 2026-07-07

The Python and Node runtime package is renamed from `baml_core` to `baml_bridge`, and `match`/`is` type tests now distinguish containers by their element type.

**Highlights**

- **Runtime rename.** Install the runtime as `baml_bridge` (Python) and `@boundaryml/baml-bridge` (Node), replacing `baml_core` and `@boundaryml/baml-core-node`. You still import the generated `baml_sdk` package as before. Update your install commands: `pip install baml_bridge`, `uv add baml_bridge`, `npm install @boundaryml/baml-bridge`.
- **Container element discrimination.** `int[]` and `string[]`, and `map<string, int>` and `map<string, string>`, now match by element type. Previously both list arms lowered to one coarse list tag, so a `string[]` value could take an `int[]` arm and a `map<string, string>` could satisfy a `map<string, int>` arm. This holds on both the arm-chain and the jump-table switch path.
- **Enum type tests.** `match (x: Color | Status)` and `x is Color` now compare enum identity, so a `Status` value no longer routes to a `Color` arm.
- **Type-variable arms.** Bare `T`, `T[]`, and `map<string, T>` arms, plus `x is T`, now compare against the realized type bound to `T` in the current call frame. Before, a bare `T` arm matched nothing and a `T[]` arm matched any list.
- **`Self` in a method body is now an error.** Using `Self` in a body type position (a `let` or cast annotation, a `match`/`is` pattern type, or `type_of<Self>()`) reports a compile error instead of silently resolving to a stray type variable. `Self` remains supported in signatures. In a method body, name the enclosing type explicitly.

```baml
function classify_list(x: int[] | string[]) -> string {
  match (x) {
    let a: int[] => "ints",
    let b: string[] => "strings",
  }
}
```

Authors: sxlijin, 2kai2kai2

## Left-panel nav items in the BEP viewer support open-in-new-tab

0.13.1-nightly.20260707.c · nightly · 2026-07-07

The BEP viewer's left-panel navigation now renders each section link as a real anchor with an `href` instead of a button, so you can middle-click or Cmd/Ctrl/Shift-click a section to open it in a new tab. Ordinary clicks still navigate in place via `onSectionClick`; the `handleNavClick` guard only lets the browser take over for modified clicks. `BepNav` now takes `bepNumber` and `versionNumber` to build the target paths through `buildBepPath`. This only affects the BEPS app UI; nothing in the BAML language, CLI, or runtime changed.

Authors: aaronvg

## `_` inference holes now work in expression-context type positions like `Box<_>` and `race<T, _>`

0.13.1-nightly.20260707.b · nightly · 2026-07-07

The `_` wildcard type is now handled in expression-context type positions. Previously a `_` written in a call turbofish, an object construction, a bare generic-apply value, or an `.as<...>` upcast target reached runtime lowering as a raw inference hole and panicked. Now each position either solves the hole from context or reports a clean diagnostic.

- **Object construction** (`Box<_> { ... }`) infers the hole from the field values, exactly like the bare `Box { ... }` form. Partial annotations such as `Pair<int, _> { a: 1, b: "hi" }` pin the first argument and infer the rest, and nested holes like `Box<Box<_>>` are filled structurally.
- **Call turbofish** (`race<T, _>`) treats a `_` as a partial annotation: the pinned arguments stay fixed and the holes are solved from ordinary argument inference.
- **Uninferable holes** now produce a `cannot infer type parameter` diagnostic instead of crashing. This covers a turbofish position that appears in no argument or return (`pick<int, _>(5)`), a bare generic-apply value (`id<_>`), and an upcast target (`c.as<Show<_>>`).

```baml
class Box<T> { v T }

function main() -> int {
  let b = Box<_> { v: 5 };
  b.v
}
```

The rest of the range is CI-only: the wasm32 toolchain install in the test workflows now retries on a stalled download instead of hanging the merge queue.

Authors: antoniosarosi, hellovai

## Fire-and-forget `spawn` errors now surface at end of run, and self-referential `.env` values no longer hang

0.13.1-nightly.20260707.a · nightly · 2026-07-07

The runtime now waits for every outstanding non-detached `spawn` child to run to completion before a run finalizes, so an unhandled error from a fire-and-forget `spawn { throw ... }` surfaces deterministically instead of being silently dropped when the root exits.

**Highlights**

- **`spawn` end-of-run wait.** A racing or delayed throw in a fire-and-forget child is now waited for and surfaced, rather than dropped if its task had not been polled when the root completed. The wait does not cancel outstanding work, so a child that sleeps and then throws still runs to the throw. Cancelling an awaiter still does not cancel the awaited future, so a sibling's long sleep can keep a run alive at shutdown unless you cancel it explicitly.
- **Detached spawns are exempt.** A `spawn with baml.spawn.options(detach = true)` is decoupled from its spawner and is not waited on, so a long-lived detached task (for example a server that returns its bound address immediately and is torn down later) no longer blocks the run that created it. The wait is also scoped to a single run's own descendants, so concurrent runs on one engine do not block on each other.
- **Self-referential `.env` values no longer hang.** `PATH=$PATH:/added/bin` and reference cycles like `A=${B}` / `B=${A}` previously looped forever, doubling memory each pass until the process hung or ran out of memory. A self-reference now resolves against the process environment only, and an undefined reference is left literal.
- **AWS Bedrock is now an optional `bedrock` feature.** It is enabled by default, so existing builds are unaffected. Building with `--no-default-features` drops the entire `aws-*` SDK dependency tree. Using an `aws-bedrock` client in a build compiled without the feature fails with "AWS Bedrock support was not compiled into this build (enable the `bedrock` feature)".

A `.env` value that appends to an existing process variable now terminates and appends as expected:

```bash
# .env — resolves against the process PATH, no longer hangs
PATH=$PATH:/added/bin
```

The rest of the change slims the engine dependency graph (replacing `eventsource-stream` with an in-tree SSE decoder, moving cancellation to `tokio-util`, and vendoring the minijinja fork). These are internal and do not change streaming or runtime behavior.

Authors: antoniosarosi, hellovai

## `break` and `continue` are now valid braceless `match` arm values

0.13.1-nightly.20260706.g · nightly · 2026-07-06

A bare `break` or `continue` can now be used directly as a `match` (or `catch`) arm value, without wrapping it in a block. Previously `0 => break` emitted `E0010 "Expected expression, found break"`, and you had to write `0 => { break; }`. Both forms now behave identically.

- **`break`/`continue` in expression position** parse as diverging expressions of type `never`, mirroring the existing `return` handling. Their `never` type unifies with the other arms, so a bare jump can sit next to a value-producing arm (e.g. `let step = match (i) { 0 => break, _ => i };`).
- **The formatter** wraps a braceless jump arm into a block with a trailing `;`, so `0 => break,` round-trips to `0 => { break; }` and stays idempotent.

```baml
function SumSkipThreeBreakAtSix() -> int {
    let total = 0;
    let i = 0;
    while (true) {
        i = i + 1;
        match (i) {
            6 => break,
            3 => continue,
            _ => { total = total + i; }
        }
    }
    total
}
```

Authors: antoniosarosi

## `baml fmt` no longer drops trailing comments on `defer` statements

0.13.1-nightly.20260706.f · nightly · 2026-07-06

`baml fmt` now preserves a trailing line comment on a `defer { … }` statement instead of silently deleting it (B-629).

A `defer` statement is a node the formatter prints verbatim. It previously reported its whole-node span as its first and last token, but the trivia classifier keys comments to individual token ranges, so a comment attached to the closing `}` never matched and was dropped. The same fix now anchors trivia to the node's true first and last tokens.

- **`defer` trailing comments survive.** A comment like `defer { cleanup() } // bye` is kept, whether the `defer` is mid-block or the last statement in the block.
- **Leading comments re-indent correctly.** A comment on its own line before a `defer` is now re-indented to the block indent instead of being reprinted verbatim at its original column.
- **Braceless `return` arms fixed too.** A trailing comment on a comma-less `return` arm in a `match`/`catch` (`_ => return 2 // fallback`) is no longer dropped when the arm is wrapped into a `{ return …; }` block.

Authors: antoniosarosi

## Single-line LLM function bodies no longer report a misleading missing-`prompt` error

0.13.1-nightly.20260706.e · nightly · 2026-07-06

Writing an LLM function body on a single line, with `client` and `prompt` on the same line, now parses correctly instead of failing with a misleading missing-`prompt` error (B-621).

The unquoted client-value scanner used to consume every token until a newline or brace, so in a body like `{ client: Fast prompt: `hi` }` it swallowed the `prompt` field into the client value and then reported `prompt` as missing. The scan now stops at the start of the next field, so both `client` and `prompt` are recognized.

As part of the same fix, putting a `,` or `;` between LLM function fields produces an accurate diagnostic ("unexpected `,` between LLM function fields; separate `client` and `prompt` with a newline") instead of the old missing-`prompt` message.

Authors: antoniosarosi

## A guard `if` followed by a parenthesized expression on the next line no longer parses as a call on the void `if` result

0.13.1-nightly.20260706.d · nightly · 2026-07-06

A block-terminated statement such as a guard `if (cond) { ... }` with no `else`, followed on the next line by a parenthesized expression, now parses as two statements instead of gluing `{ ... }(expr)` into a call on the discarded `if` result. Previously the trailing `(` was treated as a postfix call, invoking the `void` value of the `if` and producing a misleading E0006 ("`void` is not a function").

The fix keys strictly on a block-terminating `}` callee plus an intervening newline. Ordinary calls are unaffected: same-line and multi-line-argument calls like `g(1, 2)` still parse as calls, and a non-block callee (for example a chained `g()`) followed by `(` on the next line still parses as `g()(1)`.

```baml
function double(x: int) -> int {
    if (x < 0) { x }
    (x * 2)
}
```

The only other change is a new regression test for cancel-after-GC (B-665), verifying that `f.cancel()` still settles a spawned future that a garbage collection relocated on the heap. It adds test coverage only and changes no runtime behavior.

Authors: antoniosarosi

## `sum()` on `int[]` and `float[]`, plus no more phantom PASS on empty test selectors

0.13.1-nightly.20260706.c · nightly · 2026-07-06

Numeric arrays now have a `sum()` method: `xs.sum()` adds every element left-to-right from zero and stays in the array's own number domain.

- **`sum()` for `int[]` and `float[]`.** `int[].sum()` returns an `int`, `float[].sum()` returns a `float`. There is no implicit widening between them, so to sum integer data as floats you map first: `ints.map((x: int) -> float { x * 1.0 }).sum()`. The empty array sums to `0` (or `0.0`), and `int[].sum()` raises a catchable `baml.panics.IntegerOverflow` when the running total leaves the `int` range, exactly like repeated `+`. The existing free function `baml.math.sum` over `float[]` still works for a functional call style.
- **`baml test` no longer prints a green `PASS testing::*` for a no-match selector (B-628).** A filter that matched no tests used to fold to a vacuous pass and print `PASS`, contradicting the exit code 5 (`NoTestsRun`) the command then returned. The aggregate line is now suppressed and you get the `no tests selected` message instead. A zero-test report that actually reports a failure still propagates as a failure.

```baml
[1, 2, 3].sum()
```

Authors: antoniosarosi

## `int[]` and `float[]` gain a `sum()` method

0.13.1-nightly.20260706.b · nightly · 2026-07-06

Numeric arrays now have a `sum()` method via the new `Summable` interface in the standard library. `int[].sum()` returns an `int` and `float[].sum()` returns a `float`, each staying in its own number domain with no implicit widening.

- **`sum()` is the canonical surface for array summation.** `[1, 2, 3].sum()` gives `6` and `[1.0, 2.0, 3.0].sum()` gives `6.0`. The empty array sums to `0` (or `0.0`), so there is no error to handle for an empty input.
- **No `int` to `float` widening.** `int` and `float` are distinct types. To sum integers as floats, map first: `ints.map((x: int) -> float { x * 1.0 }).sum()`. The free function `baml.math.sum` over `float[]` still exists for a functional call style.
- **`int[].sum()` overflow raises `baml.panics.IntegerOverflow`.** The running total is range-checked at each step, exactly like repeated `+`, so a total that leaves the `int` range panics and is catchable. `float[].sum()` never throws.
- **`baml test` no longer prints a green `PASS` for a no-match selector.** A filter that selected zero tests previously aggregated to a vacuous pass and printed `PASS testing::*` while the command exited `5` (`NoTestsRun`). That line is now suppressed, so stdout no longer contradicts the exit code. A zero-test report that actually failed still prints and fails.

```baml
[1, 2, 3].sum()
```

Authors: antoniosarosi

## Repeating `@alias`, `@description`, or `@skip` on one declaration now errors with E0014

0.13.1-nightly.20260706.a · nightly · 2026-07-06

Applying the same single-valued schema attribute twice to one declaration is now a compile error, `E0014` (`DuplicateAttribute`). Previously a repeat was silently accepted and the last write won, dropping the earlier value.

- **`@alias`, `@description`, and `@skip`** are the checked attributes. Two of any one of them on a single class, enum, field, or enum variant is rejected. The diagnostic points at the first occurrence with `@alias first applied here` and marks each later one with `duplicate @alias — only the last takes effect`.
- **Mixing different attributes is still fine.** `@alias("id") @description("the identifier")` on one field is two distinct attributes, not a duplicate.
- **Repeatable and pass-through attributes** (such as `@stream.*`) are left alone.
- A field with two `@alias` collapses to one effective serialized key, so the cross-field `E0149` (`DuplicateFieldAlias`) check no longer fires in that case. Only `E0014` is reported, with no double-reporting.

Authors: antoniosarosi

## Cold compiles are 2.9x faster

0.13.1-nightly.20260704.a · nightly · 2026-07-04

Cold compilation is now about 2.9x faster: an empty project drops from 1.41s to 482ms. The gains come from memoizing interface-dispatch resolution per package, skipping redundant MIR lowering during bytecode emit, and faster alias-cycle and subtype checks. No BAML syntax, CLI, or API changed.

Authors: hellovai

## Duplicate serialized-key detection now covers enum variants

0.13.1-nightly.20260702.e · nightly · 2026-07-02

Enum variants that serialize to the same JSON key are now rejected with E0149, the same check that already applied to class fields.

Highlights:

- **Enum variant `@alias` collisions (E0149).** The duplicate-serialized-key check that previously applied only to class fields now also runs over enum variants. The error message names the container, so you get `Duplicate serialized key ... in class` or `... in enum` accordingly. Two variants sharing an `@alias`, or a plain variant name colliding with another variant's `@alias`, are flagged because they render duplicate labels in the output schema and a returned value can't be resolved back to a unique variant.
- **Case-sensitive matching.** Keys are compared exactly, so a variant `Value` and another variant with `@alias("value")` are distinct and not flagged.
- **`@skip` and self-alias are exempt.** A `@skip`'d variant is excluded from the schema entirely and cannot collide, and a variant whose `@alias` equals its own name is the sole occupant of that key.
- **No double-reporting.** A pure duplicate variant name (no aliasing) is still handled by the existing duplicate-variant check and does not additionally trigger E0149.

```baml
enum AliasVsAlias {
  A @alias("x")
  B @alias("x")
}
```

Both variants above serialize to the key `x`, so this now produces `Duplicate serialized key `x` in enum`.

Authors: antoniosarosi

## Type family conversions now reuse memory via transmute, with no change to behavior

0.13.1-nightly.20260702.d · nightly · 2026-07-02

This nightly is an internal-only optimization to how the compiler's type representations convert between each other. There is no change to BAML syntax, the CLI, or generated client code.

Inside `baml_type`, the related type views (`Ty`, `RuntimeTy`, `RealizedTy`, and their concrete variants) are laid out identically where they overlap, so converting between deep, equal-size members now reinterprets the same bytes instead of walking and rebuilding the tree. Widening gains borrowed upcasts such as `RuntimeTy::as_ty` and `RealizedTy::as_runtime_ty`, narrowing validates once and reuses the bytes, and `RuntimeTy`'s rendering and subtyping now call `self.as_ty()` rather than cloning through `Ty::from`. The results are structurally equal to the previous conversions, so observable behavior is unchanged. New unit tests plus a Miri run in CI guard the layout assumptions the transmutes rely on.

Authors: 2kai2kai2

## Method calls on bare numeric literals no longer crash, plus fixes for panics, spawns, and error chains

0.13.1-nightly.20260702.c · nightly · 2026-07-02

Calling a method directly on a bare numeric literal, like `7.to_string()` or `2.5.floor()`, no longer crashes or fails to type-check. The receiver used to lower to a `Missing` expression, so `.to_string()` panicked at runtime with "parse error" and `.abs()` reported E0007. The literal is now correctly used as the receiver.

```baml
function IntToStringBareLiteral() -> string {
    7.to_string()
}
```

Other fixes in this release:

- **Duplicate serialized keys are now rejected (E0149).** A class whose fields serialize to the same JSON key, either two fields sharing `@alias("x")` or a plain field named `x` colliding with another field's `@alias("x")`, now produces `DuplicateFieldAlias`. Such a schema was unsatisfiable: `ctx.output_format` rendered duplicate keys and only one field could ever be parsed. A field aliased to its own name and a `@skip`'d field do not collide, and a pure duplicate field name is still reported as `DuplicateField` (E0012).
- **Panics escaping a `throws` union surface cleanly.** A `baml.panics.*` value (for example `StackOverflow` or `DivisionByZero`) unwinding out of a function whose `throws` clause is a 2-or-more-member union used to leak an internal `TypeMismatch` error. It now bypasses the declared-throws re-typing and surfaces the actual panic. A genuine in-contract throw is still wrapped with union metadata, and `baml.panics.Exit` still routes to the clean process-exit path.
- **Never-awaited `spawn` errors surface at end of run.** A fire-and-forget child (a default `spawn` never awaited, or a `detach = true` spawn) that throws used to be silently swallowed if the root completed without ever awaiting. Its error is now drained when the root finalizes and surfaces as an unhandled throw. A child that completes successfully still returns cleanly.
- **Generic `match` arms honor subtyping.** An arm like `let s: Opt<T>`, where `T` is the enclosing function's type var, used an exact reified type-arg comparison. When inference pinned `T` to a supertype union of the scrutinee's actual arg, the arm silently missed and fell through to the default. The check now uses subtype-or-wildcard semantics, matching the interface class-dispatch path.
- **`defer` on the unwind path preserves the cause chain.** A non-throwing `defer` armed on an unwinding frame used to wipe the propagating error's cause, so `ctx.root_cause()` and `ctx.to_string()` dropped the original error. Expression-position throws now route through the exception funnel, so the cause chain survives the re-raise.

Authors: antoniosarosi

## Method calls on bare numeric literals like `7.to_string()` no longer crash

0.13.1-nightly.20260702.b · nightly · 2026-07-02

Calling a method directly on a bare numeric literal (no wrapping parentheses) now works. Previously the receiver in expressions like `7.to_string()`, `3.abs()`, `2.5.floor()`, and `42n.to_string()` lowered to a `Missing` expression, so `.to_string()` panicked at runtime with "parse error" and `.abs()`/`.floor()` failed to type-check (E0007).

```baml
function IntToString() -> string {
    7.to_string()
}
```

Other fixes in this release:

- **Duplicate serialized keys are now rejected (E0149).** A class whose fields serialize to the same JSON key, either two fields sharing an `@alias("x")` or a plain field named `x` colliding with another field's `@alias("x")`, is now an error. Such a schema is unsatisfiable because an aliased field's real name is never matched, so `ctx.output_format` renders duplicate keys and a shadowed field can never be parsed. A field aliased to its own name and `@skip`'d fields do not collide, and pure duplicate field names are still reported by the existing `DuplicateField` (E0012) check.
- **Panics escaping to the host bypass the declared `throws` contract.** A panic like `baml.panics.StackOverflow` or `baml.panics.DivisionByZero` that unwinds out of a function with a multi-member `throws` clause now surfaces as the clean panic instead of leaking an internal `TypeMismatch`. `baml.panics.Exit { code }` still routes through the clean process-exit path, and genuine in-contract throws keep their union-metadata wrapping.
- **Never-awaited spawn errors surface at end of run.** A fire-and-forget child (a default `spawn` whose spawner never awaits it, or a `detach = true` spawn) that throws now surfaces its error when the root task completes, rather than being silently dropped. A child that completes successfully does not false-surface, and an error already observed by an awaiter is not re-surfaced.
- **`defer` on the unwind path preserves the error cause chain.** A non-throwing `defer` that runs while an error is propagating no longer wipes the propagating error's cause chain, so `ctx.root_cause()` and `ctx.to_string()` still reach the original error and keep the "During handling of the above error" section.
- **Generic `match` arms honor subtyping in their reified type-arg check.** An arm like `let s: Opt<T>`, where `T` is the enclosing function's type var, now compares the reified frame type-arg with `is_subtype_of` rather than exact equality. When inference pins `T` to a supertype union of the value's actual type arg, the arm now matches instead of silently falling through to the default. Concrete parametric arms such as `Foo<int>` versus `Foo<string>` still discriminate exactly.

Authors: antoniosarosi

## Calling a method on a bare numeric literal like `7.to_string()` no longer crashes

0.13.1-nightly.20260702.a · nightly · 2026-07-02

Calling a method directly on a bare numeric literal now works. `7.to_string()`, `3.abs()`, `2.5.floor()`, and `42n.to_string()` used to lower their receiver to a missing expression: the value-returning ones panicked at runtime with a "parse error" and the others failed to type-check with E0007. The receiver is now the literal itself.

```baml
7.to_string()
```

Other fixes in this release:

- **Duplicate serialized keys are now rejected (E0149).** Two fields in a class that serialize to the same JSON key are reported as `DuplicateFieldAlias`. This fires when two fields share an `@alias("x")`, or when one field's declared name equals another field's `@alias`. Such a schema is unsatisfiable because `ctx.output_format` renders duplicate keys and only one field can ever be parsed. A field aliased to its own name and `@skip`'d fields are not flagged, and a plain duplicate field name is still left to the existing `DuplicateField` (E0012) check.
- **Panics escaping to the host bypass the declared `throws` contract.** A panic like `baml.panics.StackOverflow` or `baml.panics.DivisionByZero` unwinding out of a function whose `throws` clause is a two-or-more-member union used to leak an internal `TypeMismatch`. It now surfaces as the clean panic value, matching the behavior for functions with no `throws` clause. A genuine in-contract throw is still wrapped with its union metadata, and `baml.panics.Exit { code }` still surfaces as a clean process exit.
- **Never-awaited `spawn` errors surface at end of run.** A fire-and-forget child (a default `spawn` that is never awaited, or a `detach = true` spawn) that throws used to have its error dropped when the root completed normally, exiting 0. The error is now drained when the root finalizes and surfaced as an unhandled throw. A child that completes successfully still returns cleanly with no false surfacing, and a child whose error was already awaited and caught is not re-surfaced.
- **Generic `match` arms honor subtyping in their type-arg check.** An arm like `let s: Opt<T>`, where `T` is the enclosing function's type var, used to compare the reified frame type-arg exactly. When inference pinned `T` to a supertype union of the scrutinee's actual arg, the exact check missed and the arm silently fell through to the default. The arm now compares with subtype-or-wildcard semantics, so a narrower value matches a wider pinned `T`, while a strictly wider runtime arg still does not match.
- **A non-throwing `defer` on the unwind path preserves the error cause chain.** A `throw` in expression position inside a `defer` region used to route through a static jump that dropped its BEP-042 cause chain, so `ctx.root_cause()` and `ctx.to_string()` lost the original error. Throws now route through the exception funnel, so the cause survives the re-raise, including across stacked defers and defers whose own body throws.

Authors: antoniosarosi

## `baml.csv.decode` now resolves user-defined enum columns

0.13.1-nightly.20260701.e · nightly · 2026-07-01

`baml.csv.decode<T>` now decodes columns typed as a user-defined `enum`. Previously a user enum field resolved through a package-eliding lookup that only worked for builtin enums, so decoding failed with `enum ... not found` even when the variant name matched.

- **CSV typed decode**: a `class` field whose type is your own `enum` decodes correctly. The decoder keeps the enum's `TypeName` (rather than a rendered key that dropped the `user.` prefix) and resolves it through the package index.
- **LSP crash fix**: requesting type info over a binding declared inside a nested `testset` or lambda no longer panics. The hover path referenced a statement index that was arena-local to a different body, and the out-of-bounds index aborted the whole wasm runtime. It now bounds-checks and bails.

```baml
enum Color { Red Green Blue }
class Row {
    name string
    color Color
}
function main() -> string {
    let rows = baml.csv.decode<Row>("name,color\nsky,Blue\ngrass,Green\n");
    let out = "";
    for (let r in rows) {
        out = out + r.name + ";";
    }
    out
}
```

The bulk of this release is internal groundwork with no observable behavior change: the type algebra was unified behind a single interface-membership seam, and interface, package, and impl-rule objects now live on the runtime heap. A new runtime `TypeContext` is staged but not yet wired into any call site, so subtyping behavior is unchanged.

Authors: aaronvg, 2kai2kai2

## `baml.math.sum`, `baml.math.mean`, and `baml.math.median` aggregation builtins

0.13.1-nightly.20260701.d · nightly · 2026-07-01

Three aggregation builtins are now available on `float[]`: `baml.math.sum`, `baml.math.mean`, and `baml.math.median`.

- **`baml.math.sum(values: float[]) -> float`** adds elements left-to-right from `0.0`, so an empty array sums to `0.0`. Never throws.
- **`baml.math.mean(values: float[]) -> float`** returns `sum / length`. Throws `root.errors.InvalidArgument` on an empty array.
- **`baml.math.median(values: float[]) -> float`** sorts a copy (leaving your array untouched) and returns the middle element for odd counts, or the mean of the two middle elements for even counts. Ordering follows IEEE 754 `totalOrder`, matching `float[].sort()`. Throws `root.errors.InvalidArgument` on an empty array.

All three take `float[]` only. There is no implicit widening from `int`, so map integer data to floats first, for example `ints.map((x: int) -> float { x * 1.0 })`.

```baml
baml.math.median([3.0, 1.0, 2.0])
```

Authors: ATX24

## `Array.filled` warns on mutable-literal aliasing, and `length()` `let` bindings snapshot eagerly

0.13.1-nightly.20260701.c · nightly · 2026-07-01

`baml.Array.filled(n, value)` now emits a warning (`E0148`) when `value` is a mutable literal, because every slot shares the same reference.

**Highlights**

- **`Array.filled` aliasing lint (`E0148`).** Calling `baml.Array.filled` with an array literal `[]`, a map literal `{}`, or a class-instance literal now warns that every slot aliases the same object, so mutating one slot mutates all of them. Detection is syntactic and fires for both positional and named (`value = ...`) forms. Binding the literal to a variable first (`let x = [0]; baml.Array.filled(3, x)`) does not warn. The doc comment on `Array.filled` now spells out the aliasing behavior and suggests a `while` loop that pushes a fresh literal per iteration for independent slots.
- **Class destructuring reports unknown fields directly.** A pattern that names a field the class does not declare, like `let Point { valeu } = ...`, now reports `class `Point` has no field `valeu`. Did you mean `value`?` instead of cascading into unresolved-name errors. This is validated before reachability analysis, so the bad arm no longer makes later `match` arms look unreachable or non-exhaustive, and applies to `let`, `match`, and `for` bindings.
- **`length()` `let` bindings snapshot eagerly.** `let n = a.length()` now captures the length at the binding site, so a later `a.push(...)` does not change `n`. Previously the length could be re-evaluated after intervening mutations. This is fixed at all optimization levels.

```baml
function bug() -> int {
  let a: int[] = [];
  let n = a.length();
  a.push(9);
  n
}
```

This returns `0`.

Authors: ATX24

## `length()` let bindings now snapshot eagerly, and `Array.filled` warns on mutable-literal aliasing

0.13.1-nightly.20260701.b · nightly · 2026-07-01

`let n = a.length()` now captures the length at the binding site instead of silently re-evaluating it after later mutations. Previously the binding could be virtualized, so a `push` between the binding and its use changed the observed value.

**Highlights**

- **`length()` snapshots are eager.** A `let` bound to `a.length()` is now always materialized where it is written. Given `let n = a.length()` followed by `a.push(9)`, reading `n` yields the length at binding time (0), not the post-push length. This holds at both optimization levels.
- **New lint `E0148` for `Array.filled` aliasing.** Calling `baml.Array.filled(n, value)` with a mutable literal (`[]`, `{}`, or a class-instance literal) now warns, because every slot shares the same object reference and mutating one slot mutates all of them. The lint fires for both positional and named (`value = ...`) forms. It is purely syntactic: a value bound to a variable first still aliases at runtime but does not warn.
- **Better class-destructuring field errors.** A class pattern that names a field the class does not declare now reports `class \`X\` has no field \`y\`` with a `Did you mean` typo suggestion, in `let`, `match`, and `for` bindings. The field error no longer cascades into spurious unreachable-arm, unresolved-name, or non-exhaustive-match diagnostics.

```baml
function bug() -> int {
    let a: int[] = [];
    let n = a.length();
    a.push(9);
    n
}
```

This now returns `0`.

Authors: ATX24

## README documentation links updated

0.13.1-nightly.20260701.a · nightly · 2026-07-01

This nightly only touches `README.md`. The docs quickstart link now points to `boundaryml.com/quickstart` and the top-of-readme link row drops the separate Docs link. No compiler, runtime, or library behavior changed.

Authors: aaronvg

## `baml run` now prints only your program's output on success

0.13.1-nightly.20260630.e · nightly · 2026-06-30

`baml run`, `baml test`, and `baml pack` no longer print the `Compiling N file(s)` status line during compilation. Compile progress is now reserved for `baml check` and `baml generate`.

- **`baml run`**: on a successful run, stderr stays empty and only your program's stdout is shown. The sole exception is the `Code is unformatted` advisory, which still prints when your source needs formatting.
- **`baml test` and `baml pack`**: the `Compiling`/`Compiled` file-count lines are gone. `baml pack` still shows its packaging progress.
- **`baml generate`**: unchanged, it keeps `Compiling 1 file(s)`.
- **`--verbose` on `baml run`**: no longer surfaces warning diagnostics or per-file loading progress. It now affects only explicit listing output, not successful execution. The animated spinner shown on TTYs has been removed entirely.

Authors: aaronvg

## New `baml.fs.remove_dir` and `baml.fs.remove_dir_all` for deleting directories

0.13.1-nightly.20260630.d · nightly · 2026-06-30

`baml.fs.remove_dir` and `baml.fs.remove_dir_all` now let you delete directories from BAML code. Previously `baml.fs.remove` handled regular files only, so there was no way to remove a directory.

- **`baml.fs.remove_dir(path)`** removes an empty directory. It throws `Io` if the path is not a directory, is not empty, or does not exist, mirroring `rmdir`.
- **`baml.fs.remove_dir_all(path)`** recursively removes a directory and everything under it. It is idempotent on a missing path (returns successfully with `force: true` semantics), but unlike `rm -rf` it targets directories only and throws `Io` if handed a regular file, so it can never silently delete a file.
- **Clearer `baml.fs.remove` error on a directory.** Calling `remove` on a directory now throws an `Io` error that states "it is a directory" and points you at `remove_dir` or `remove_dir_all`, instead of the opaque platform OS error (EISDIR on Linux, EPERM on macOS). The directory is left untouched.

All three behaviors are implemented for both the native and WASM (playground) runtimes.

```baml
// Idempotent: a missing path is not an error, so this returns null.
baml.fs.remove_dir_all("/tmp/baml-changelog-demo-missing")
```

The playground also fixes graph value previews. Root run inputs and results now attach to the root function node rather than leaking onto a single visible descendant, and a group node's value previews render on their own child node.

Authors: antoniosarosi, aaronvg, rossirpaulo

## New `baml.fs.remove_dir` and `remove_dir_all`, and a directory hint on `remove`

0.13.1-nightly.20260630.c · nightly · 2026-06-30

`baml.fs` gains two directory-deletion functions, `remove_dir` and `remove_dir_all`, so you no longer have to reach for a Rust escape hatch to clean up directories.

- **`baml.fs.remove_dir(path)`** removes an empty directory. It throws `root.errors.Io` if the path is not a directory, is not empty, or does not exist. It mirrors `rmdir`.
- **`baml.fs.remove_dir_all(path)`** recursively removes a directory and everything under it. It is idempotent: a missing path returns successfully (`force: true` semantics). Unlike `rm -rf`, it targets directories only and throws `Io` when handed a regular file, so it can never silently delete a file. Both native and WASM backends enforce this.
- **`baml.fs.remove` now explains directory errors.** Calling `remove` on a directory previously surfaced the raw platform error (`EISDIR` on Linux, `EPERM` on macOS). It now throws an `Io` whose message says it is a directory and names `baml.fs.remove_dir` and `baml.fs.remove_dir_all`. The directory is left untouched, and a symlink pointing at a directory is still deleted as a link.

```baml
function CleanupTempTree() -> bool {
    baml.fs.mkdir("/tmp/baml_demo/sub", baml.fs.MkdirOptions { recursive: true });
    baml.fs.write("/tmp/baml_demo/a.txt", "a");
    baml.fs.remove_dir_all("/tmp/baml_demo");
    baml.deep_equals(baml.fs.exists("/tmp/baml_demo"), false)
}
```

In the playground, graph value previews now attach a run's root input and result to the root function node instead of leaking onto a single visible child call node, keeping child call values separate from root values (including on errors).

The README was also trimmed to point at the website.

Authors: antoniosarosi, aaronvg, rossirpaulo

## The `_` wildcard infers individual type slots, and `catch (e, ctx)` exposes error cause chains

0.13.1-nightly.20260630.b · nightly · 2026-06-30

You can now write `_` in a generic type argument or a `throws` clause and have the compiler fill just that slot, and a caught error's `ctx` binding now carries the chain of errors it superseded.

**Highlights**

- **`_` in a `let` type argument** is inferred from the initializer while the rest of the annotation stays explicit. `let fs: baml.future.Future<int, _>[] = [spawn { slow(1) }]` keeps the value type `int` and adopts the spawned body's real error type. The filled type is exact, not erased to `unknown`, so a `throws never` function that awaits it is still rejected, and `Future<string, _>` against an `int` body is still a type error. `never` keeps its meaning: annotating a fallible spawn as `Future<int, never>` remains an error.
- **`_` as a `throws` member** (`throws AppError | _`) is an open contract: it keeps your named domain errors in the signature and lets the body's inferred throws (for example `baml.json.*`) fill the rest, instead of widening the whole error type to `unknown`. A plain `throws T` with no `_` stays exhaustive.
- **`_` where nothing can be inferred** (a parameter, return type, class field, generic bound, `requires` clause, or nested inside a thrown type) is a clean error, `E0147`, rather than a compiler panic.
- **`catch (e, ctx)` cause chains.** The second catch binding is now an `ErrorContext` with fields `error`, `stack_trace`, and `cause`, plus `root_cause()` (walks to the original failure) and a `to_string` that renders the chain Python-style. Throwing while handling another error, or from sibling `defer` blocks during unwinding, links the new error's `cause` to the one being handled.
- **`baml.json.to_json` and `baml.json.serialize`** now declare `throws JsonSerializationError` only, dropping the unreachable `JsonParseError` from their signatures.

```baml
class MyErr { msg string }
function slow(x: int) -> int throws MyErr {
  if (x < 0) { throw MyErr { msg: "neg" }; }
  x
}
function main() -> int {
  let fs: baml.future.Future<int, _>[] = [spawn { slow(1) }];
  await fs[0]
}
```

Authors: antoniosarosi

## The `_` wildcard infers a single type slot, and `catch (e, ctx)` exposes error cause chains

0.13.1-nightly.20260630.a · nightly · 2026-06-30

Two features land in this nightly: the `_` inference hole in type positions, and a second `catch` binding that carries the caught error's cause chain.

**Highlights**

- **`_` wildcard in type-argument and `throws` positions.** You can now write `_` for a single generic type argument in a `let` annotation and have it filled from the initializer, keeping the rest explicit: `let fs: baml.future.Future<int, _>[] = [spawn { slow(1) }]` keeps the value type `int` and infers the spawned body's error type. The filled type is exact, not erased to `unknown`, so a `throws never` caller awaiting that future is still rejected, and a wrong explicit slot like `Future<string, _>` against an `int` body still fails. `_` is only valid where the slot can be inferred (a `let` annotation or a top-level `throws` member); using it in a return type, parameter, field, generic bound, or nested inside a thrown type is a hard error, the new **E0147** (`the _ wildcard type can only be inferred in a let binding or a throws clause`).
- **Partial `throws` clauses.** `throws AppError | _` is an open contract: it keeps `AppError` in the signature and fills the `_` with the body's inferred throw set, so callers see the full union without you re-declaring infrastructural stdlib throws. A plain `throws T` with no `_` stays exhaustive.
- **`catch (e, ctx)` cause chains.** The second binding of a `catch` handler is now a `baml.errors.ErrorContext` with fields `error`, `stack_trace`, and `cause`, plus a `root_cause()` method that walks to the original failure and a `to_string` that renders the chain Python-style. An error thrown while handling another (a nested `catch`, or sibling `defer`s that throw while a scope unwinds) chains onto the in-flight error via `cause`. A bare rethrow does not self-link.

```baml
function fail_a() -> string { throw "A" }
function fail_b() -> string { throw "B" }

function main() -> string {
  fail_a() catch (e, ctx) {
    _ => {
      // B is thrown while A is being handled, so B.cause == A.
      fail_b() catch (e2, ctx2) {
        _ => {
          match (ctx2.root_cause().error) {
            let s: string => s
            _ => "no cause found"
          }
        }
      }
    }
  }
}
```

One related cleanup: `baml.json`'s `to_json` and `serialize` now declare `throws JsonSerializationError` only, dropping the `JsonParseError` member that serialization never actually raised.

Authors: antoniosarosi

## The CLI now sends anonymous usage telemetry, opt out with `DO_NOT_TRACK`

0.13.1-nightly.20260629.e · nightly · 2026-06-29

The `baml` CLI now sends anonymous telemetry on each invocation, and you can turn it off by setting `DO_NOT_TRACK=1` or `BAML_TELEMETRY=0`.

- **`cli_invocation` telemetry.** Every `baml <subcommand>` run posts a single event to PostHog carrying the top-level subcommand name plus coarse metadata (CLI version, release channel, OS, arch). You are identified only by a random anonymous id stored under your BAML home directory. No emails and no project content are collected. The send runs on a background thread and waits at most one second after your command finishes, so it never noticeably slows the CLI, and every error is swallowed. Language-server (`lsp`) and CI invocations are flagged so they can be filtered from analytics.
- **Opt out.** Telemetry is disabled when `DO_NOT_TRACK` is truthy or `BAML_TELEMETRY` is set to a falsy value.

```bash
export DO_NOT_TRACK=1
```

- **`baml init` writes explicit generator blocks.** The generated `baml.toml` now includes commented-out `[generator.python_client]` and `[generator.node_client]` sections, each with an `output_type`, an `output_dir` (`./baml_python` and `./baml_ts`), and install notes for the runtime bridge. Uncomment the one you want to start generating a client.

The `baml_home()` resolution logic was also consolidated into `baml_release::baml_home()` and shared between the wrapper and the toolchain binary, with no change to how the home directory is resolved.

Authors: sxlijin

## The CLI now sends anonymous invocation telemetry, with `DO_NOT_TRACK` opt-out

0.13.1-nightly.20260629.d · nightly · 2026-06-29

Every `baml <subcommand>` invocation now sends a single anonymous `cli_invocation` event to PostHog, capturing the subcommand name and coarse environment metadata (CLI version, release channel, OS, arch). No emails or project content are collected, and you are identified only by a random id persisted under your BAML home directory (`~/.baml`). The send runs on a background thread and never delays or fails a command.

- **Opt out** by setting `DO_NOT_TRACK` to a truthy value or `BAML_TELEMETRY` to a falsy one. With no key configured at build time, telemetry is off entirely.
- **`baml init` now writes explicit generator blocks.** The generated `baml.toml` includes commented-out `[generator.python_client]` and `[generator.node_client]` sections with `output_type`, `output_dir`, and the runtime-bridge install commands for each language, replacing the single terse `[generator.my_client]` stub.

To disable telemetry for a shell session:

```bash
export DO_NOT_TRACK=1
```

Authors: sxlijin

## `baml fmt` keeps single-line unmodeled expressions inline

0.13.1-nightly.20260629.c · nightly · 2026-06-29

`baml fmt` no longer force-wraps an expression just because it contains a node the formatter's AST doesn't model. Constructs like `await f`, `spawn { … }`, the `.as<T>` projection, `throw`, and bigint literals such as `100n` are printed verbatim as `Unknown` nodes. Previously they always reported themselves as multi-line with unknown width, which poisoned the single-line attempt of every enclosing expression and exploded concise one-liners into deeply-indented blocks (B-231).

- **Inline arithmetic and parens.** `"got " + (await f)` stays on one line instead of blowing up into a triple-indented block.
- **Access chains.** `self.as<Dog>.name` no longer splits across lines at the `.as<T>` projection.
- **Call chains with bigints.** `baml.sys.sleep(baml.time.Duration.from_milliseconds(100n))` stays as a single call instead of fanning out.
- **Braceless `catch` arms.** A fitting `=> throw Boom {},` arm stays braceless instead of being wrapped into a `=> { throw Boom {} }` block.

Now `Unknown` reports its shape honestly from the raw source: single-line text stays inline, multi-line text still wraps. The other commit in this release adds only regression tests (B-251) for a shared-`Concat` emptying bug that was already fixed upstream in #3776; no runtime behavior changed there.

Authors: antoniosarosi

## `return` works as a braceless `catch` and `match` arm value

0.13.1-nightly.20260629.b · nightly · 2026-06-29

A braceless `return` can now be used directly as a `catch` or `match` arm value, like `_ => return -1`. Previously `return` was statement-only, so putting it in an arm without a surrounding block emitted a cascade of misleading parse and type errors. The block form (`_ => { return x }`) already worked; this makes the braceless form behave the same.

`return` in expression position is a diverging expression of type `never`, exactly like `throw`. It transfers control to the enclosing function's exit, not to the surrounding `catch`, and its value is still checked against the function's declared return type. Because it is `never`-typed, a `return` arm unifies with the other arms, so a mix like `let s: string => return s.length()` alongside an `int` arm keeps the result `int`.

```baml
function may_throw(x: int) -> int throws string {
    if (x == 0) {
        throw "boom"
    }
    x
}

function via_catch(x: int) -> int {
    may_throw(x) catch (e) {
        _ => return -1
    }
}
```

The formatter wraps a braceless `return` arm into `{ return ...; }`, adding the statement `;` so the output round-trips. A genuinely wrong return value, such as returning a `string` from a `-> bool` function, now produces a single clear type-mismatch diagnostic instead of a wall of downstream parse failures.

Authors: antoniosarosi

## `code_point_at` and `to_code_points` land on `String`, and braceless lambdas report a single syntax error

0.13.1-nightly.20260629.a · nightly · 2026-06-29

Two new methods on `String` give you the numeric view of text: `code_point_at` and `to_code_points`.

- **`String.code_point_at(index)`** returns the Unicode code point at a codepoint index as an `int`, the numeric counterpart of `char_at`. It uses the same indexing rules: negative indices count from the end, and an out-of-range index raises `IndexOutOfBounds`. Code points are indexed per character, so `"😀hello".code_point_at(1)` is `104` (`"h"`), not a UTF-16 unit.
- **`String.to_code_points()`** returns the whole string as an `int[]`, one element per character. It never throws, and it is the exact inverse of `from_code_points`, so `string.from_code_points(s.to_code_points())` round-trips back to `s`. Use it for char-to-integer work like checksums, hashing, or base-N encoding instead of indexing into a literal alphabet.

```baml
"hi".to_code_points()
```

Separately, a braceless lambda body now surfaces only the actionable syntax error. Previously `(x: int) => x + 1` (missing the `{ }` body) made the parser consume `x` as the lambda's return type and left `+ 1` dangling, so you got `unresolved type: x` and an operator type error *before* the real `Expected lambda body '{'`. Type-inference diagnostics are now suppressed for a scope whose body failed to parse, along with its descendant scopes, so the syntax error stands alone. This is scoped to the broken body: genuine type errors in other, well-formed functions in the same file are still reported.

Authors: antoniosarosi

## `catch_all_panics` clauses now parse and lower correctly

0.13.1-nightly.20260628.a · nightly · 2026-06-28

The `catch_all_panics` catch clause is now wired into the parser, so `expr catch_all_panics (e) { ... }` parses where it previously failed. Before this release the AST lowering knew about the clause kind, but the parser only recognized `catch` and `catch_all` in catch-clause position, so a `catch_all_panics` clause never reached lowering.

- **`catch_all_panics` is a contextual keyword.** It introduces a catch clause only in catch-clause position. Everywhere else it remains a normal identifier, so you can still name a function or binding `catch_all_panics`.
- Like `catch_all`, it exhaustively handles errors, and it additionally swallows panics under a wildcard arm instead of re-throwing.

```baml
function MayFailIntOrString(x: int) -> string {
    match (x) {
        0 => throw "string error",
        1 => throw 42,
        _ => "ok"
    }
}

function CatchAllPanicsWildcard(x: int) -> string {
    MayFailIntOrString(x) catch_all_panics (e) {
        _ => "caught all"
    }
}
```

The canary version stamp also moved to 0.13.0.

Authors: rossirpaulo, hellovai, antoniosarosi

## Backtick string literals with interpolation, `defer`/`cleanup`, and conversion interfaces land

0.13.0 · canary · 2026-06-28

Backtick string literals arrive with interpolation, escape handling, and multi-line auto-dedent, alongside a batch of new stdlib interfaces and stricter compile-time checks.

**Highlights**

- **Backtick strings (BEP-049).** New `` `...` `` literals support `${...}` interpolation, control flow, and tagged templates. Escape decoding is now shared across `"..."` and backtick forms (`\n`, `\t`, `\r`, `\0`, `\b`, `\v`, `\f`, plus `` \` `` and `\$` in backtick literals), and backtick text normalizes CRLF and lone CR to LF. Multi-line backtick literals auto-dedent using the longest common leading-whitespace prefix, where tabs and spaces do not mix. Interpolation also gets syntax highlighting and inlay-hint suppression.
- **`defer` and `cleanup` (BEP-042).** A `defer` statement runs code on scope exit, and `cleanup` adds a finalizer magic method.
- **Conversion interfaces.** `baml.ToString` (with `obj.to_string()` sugar and nested-override support), `baml.ToJson`, and the symmetric `baml.FromJson` replace the older duck-typed `to_json` magic. `string.from<T>` and `baml.json.to<T>` are the driver functions, and `baml.unstable.string` is removed in favor of `string.from`.
- **Per-call timeouts.** HTTP calls accept per-call timeouts via `baml.time.Duration`, and `baml.sys.sleep` now takes a `Duration`.
- **`render_null_as` output format.** A new output-format option controls how null values are rendered in output.
- **Compiler soundness.** Unsound local reassignment is now rejected and index subscripts are validated (B-236). Integer overflow is handled consistently and out-of-range integer literals are rejected (B-266). Function types must be fully resolved, and comparison operators were added along with interface fixes.
- **Numeric literals.** Scientific-notation float literals (`1e10`, `1.5e-3`) and negative indexing are now supported.
- **Stdlib additions.** `Array.filled(n, value)`, `float.to_fixed()` with JavaScript `toFixed` parity, `assert.approx_equal` for tolerant float assertions, and improved `assert.equal` failure output. `String.index_of` now returns null when missing (B-544).
- **Tooling.** Code generators can be configured in `baml.toml`. `baml fmt` formats directory paths, emits `{}` for empty map literals, and accepts backtick strings in prompt, attribute, and template-string slots. `baml describe` resolves unqualified builtin class methods, emits package-qualified builtin paths, gains SDK and pattern topics, and adds syntax highlighting plus hyperlinks. `.baml` files are now executable via a shebang using `baml run --file`.
- **SDK builds.** The embedded Python and Node SDKs are now built with `panic = "unwind"` so an engine panic becomes a catchable `BamlPanic` or JS error instead of aborting the host process.

```bash
baml run --file script.baml
```

Streaming support in the Python and Node bridges saw substantial work this cycle, and generics gained inbound and outbound value inference across the bridges.

Authors: sxlijin, antoniosarosi, rossirpaulo, 2kai2kai2, ATX24, codeshaunted, hellovai, aaronvg, GouravSingal-code

## Build commands print a single `Compiling N file(s)` line instead of stacked `Checking`/`Compiling`/`Loading` progress

0.12.2-nightly.20260627.j · nightly · 2026-06-27

The build-style CLI commands (`run`, `test`, `generate`, `check`, `pack`) now share one project loader and print quieter progress. The redundant `Checking N file(s)` line is gone: a single `Compiling N file(s)` spinner now covers both diagnostics and bytecode emit, since they report the same file count and the same wait.

**Highlights**

- **Quieter default output.** The per-file `Loading <path>` lines are now verbose-only. By default a build shows just the aggregate `Compiling N file(s)` line. Pass `--verbose` to `baml run` to restore the per-file loading progress.
- **`baml run` finishes on `Compiled N file(s)`.** After compilation it clears the spinner and lets the program run silently, so its stdout is the output that matters. The old `Running`/`Finished` status lines are removed. `baml run --list` uses the verb `Resolving` instead of `Compiling`, since nothing is executed.
- **`[toolchain]` in `baml.toml` no longer warns.** A `[toolchain]` table (or bare `toolchain = "…"`) is read by the `baml` wrapper to pick a toolchain, so it is no longer flagged as an unrecognized top-level key. Genuine typos before any table header still warn.
- **`baml test` selector help clarified.** The `-i`/`-x` help now documents the `Testset::TestName` form explicitly: the part before `::` is the enclosing testset (or, for a legacy function-attached `test`, the function name), and either side may be empty or use `*`. Behavior is unchanged.

```bash
# per-file Loading lines are now behind --verbose
baml run --verbose MyFunction
```

Authors: aaronvg

## `assert.approx_equal` compares floats within a tolerance

0.12.2-nightly.20260627.i · nightly · 2026-06-27

`assert.approx_equal(actual, expected, eps)` lets float assertions pass when the absolute difference `|actual - expected|` is within `eps`, so ordinary floating-point rounding no longer fails a test.

- **`assert.approx_equal`** is a new function in the `assert` standard module. It panics when the difference exceeds `eps`, and also panics up front when `eps` is negative or NaN, or when the delta is NaN. Use it instead of `assert.equal` for computed float results.
- **`assert.equal` docs clarified.** The docstring now states that its comparison is exact for floats and points to `assert.approx_equal` for tolerant comparisons. Behavior of `assert.equal` is unchanged.

```baml
assert.approx_equal(9.99 + 5.50 + 2.00, 17.49, 0.000001)
```

Authors: ATX24

## Prompt Fiddle can call OpenAI and Anthropic through a shared gateway, no API key required

0.12.2-nightly.20260627.h · nightly · 2026-06-27

The Prompt Fiddle playground now routes LLM requests through a Boundary-hosted proxy, so you can run functions against OpenAI and Anthropic without bringing your own keys. A new "Use Boundary LLM gateway free tier" toggle in the env-vars dialog controls it, and it defaults on for a fresh playground.

- **`BOUNDARY_PROXY_URL`**: the WASM runtime reads this env var and, when set, rewrites each request to `{proxy}/<path>` while carrying the real target origin in a new `baml-original-url` header. The proxy reconstructs the upstream URL and injects the server-side API key. AWS Bedrock is excluded because its SigV4 signature is bound to the host and would not survive the rewrite.
- **Gateway toggle**: turning it on sets `BOUNDARY_PROXY_URL`; turning it off removes it and falls back to the API keys you enter below. The toggle is Prompt Fiddle only. The VS Code extension and the `baml-cli` playground hide it, and the runtime treats `BOUNDARY_PROXY_URL` as optional so toggling off never triggers a missing-key prompt.
- **Request log de-proxying**: the playground request list now shows the original upstream URL (from `baml-original-url`) annotated with the proxy it went through, for example `https://api.anthropic.com/v1/messages (via https://proxy.promptfiddle.com)`, instead of the rewritten proxy URL.
- **Browser Anthropic calls**: in WASM builds, requests to the Anthropic API now include `anthropic-dangerous-direct-browser-access: true`, which Anthropic requires to accept browser-origin requests.

Bedrock aside, this is playground infrastructure. It does not change how you write BAML.

Authors: sxlijin

## The WASM playground routes LLM requests through a proxy via `BOUNDARY_PROXY_URL`

0.12.2-nightly.20260627.g · nightly · 2026-06-27

When `BOUNDARY_PROXY_URL` is set, the WASM runtime now rewrites each outgoing LLM request to go through that proxy, carrying the real target origin in a `baml-original-url` header. This lets the browser playground reach providers that CORS would otherwise block, and lets a proxy inject server-side API keys so users don't have to supply their own.

- **`BOUNDARY_PROXY_URL` proxy routing.** In WASM builds, the request URL is rewritten to `{proxy}/<path+query>` and the original origin is placed in the `baml-original-url` header. Auth headers are applied before the rewrite. AWS Bedrock is excluded, since its SigV4 signature is bound to the host and would not survive the rewrite.
- **Anthropic direct browser access.** WASM requests to the Anthropic API now send `anthropic-dangerous-direct-browser-access: true`, which Anthropic requires to accept browser-origin requests.
- **Gateway toggle in promptfiddle.** The env-vars dialog gains a "Use Boundary LLM gateway free tier" switch that sets or removes `BOUNDARY_PROXY_URL`. It is shown only in promptfiddle; the VS Code extension and CLI playground hide it, and the worker treats the var as optional so turning it off never triggers a missing-key prompt.
- **Run logs de-proxy the URL.** For proxied requests, the fetch log now shows the real upstream URL with the proxy noted, for example `https://api.anthropic.com/v1/messages (via https://proxy.promptfiddle.com)`. The `baml-original-url` header is treated as display-safe and passed through unredacted; all other header values stay redacted.

The promptfiddle proxy server itself was rebuilt under `typescript2/app-fiddleproxy` with a required `PROXY_PROMPTFIDDLE_COM_TOKEN` gate that fails closed when unset.

Authors: sxlijin

## Internal AST refactor: type expressions now carry a source span on every node

0.12.2-nightly.20260627.f · nightly · 2026-06-27

This nightly is an internal compiler refactor with no user-visible behavior change. The AST's `TypeExpr` enum was renamed to `TypeExprKind`, and `TypeExpr` is now a struct pairing a `TypeExprKind` with its own `span`. Because the span attaches recursively to every child node (each `Box<TypeExpr>` and `Vec<TypeExpr>`), a diagnostic about a nested sub-type, such as one unresolved member of a union or map, can be pointed at exactly. Equality and hashing on `TypeExpr` remain structural and ignore the span, so type identity and Salsa dedup behave as before.

The old `SpannedTypeExpr` wrapper (which only carried a single span for the whole annotation) is removed; the item tree, HIR signatures, and codegen now hold `TypeExpr` directly. This is the plumbing for more precise unresolved-type spans. No diagnostic message, syntax, or API that BAML code depends on changed in this release.

Authors: codeshaunted

## Non-`string` map keys are now a compile error, and strings gain `.chars()`

0.12.2-nightly.20260627.e · nightly · 2026-06-27

`map<K, V>` now rejects any key type other than `string` at type-check time with error `E0067`. Runtime maps are string-keyed, so declarations like `map<int, string>`, `map<float, V>`, `map<string?, V>`, or an enum-keyed map previously compiled and then misbehaved when a key was coerced. They now fail up front with a message telling you to declare the map as `map<string, V>` and convert non-string keys with `.to_string()` before `.set()` or `.get()`.

- **Map key validation.** Only `string` (including string literals, unions of string, and type aliases that resolve to `string`) is accepted as a key. Unresolved type variables in generic signatures like `map<K, V>` are deferred and still compile. Aliases resolving to non-string types, such as `type IntKey = int`, are rejected.
- **`string.chars()`.** New method returning the string's Unicode code points as one-character strings. `"é😀".chars()` is `["é", "😀"]`, and `"".chars()` is `[]`.
- **`str.split("")` fixed.** Splitting on an empty delimiter now returns the characters with no leading or trailing empty entries. `"aé😀".split("")` is `["a", "é", "😀"]`, and `"".split("")` is `[]`.
- **Strings are iterable.** You can now iterate a string directly, yielding one character at a time, so `for (let c in "aé😀")` walks `a`, `é`, `😀`.

```baml
function main() -> string[] {
  "é😀".chars()
}
```

Authors: aaronvg

## Map keys must be `string`, plus a new `chars()` method and character iteration on strings

0.12.2-nightly.20260627.d · nightly · 2026-06-27

`map<K, V>` now requires a `string` key type, and non-string keys are rejected at compile time with the new `E0067` diagnostic. Runtime maps are string-keyed, so declarations like `map<int, string>` previously compiled and then failed when a key was coerced. They now surface an error up front.

**Highlights**

- **`string`-only map keys.** Any map annotation whose key is not `string` (for example `map<int, string>`, `map<MapDummy, string>`, `map<string?, string>`, or `map<string | int, string>`) now emits `error: map keys must be \`string\``. `string` type aliases, unions of string, and generic type variables in signatures like `map<K, V>` are still accepted, so the check does not fire on unresolved generics. To migrate, declare the map as `map<string, V>` and convert keys with `.to_string()` before `.set()` or `.get()`.
- **`String.chars()`.** New method returning the string's Unicode code points as one-character strings, so `"é😀".chars()` yields `["é", "😀"]` and `"".chars()` yields `[]`.
- **Strings are iterable.** A `string` can now be used directly in a `for` loop, iterating one character at a time.
- **`split("")` returns characters.** Splitting on an empty delimiter now returns each character with no leading or trailing empty-string padding, matching `chars()`.

```baml
function main() -> string {
  let out = "";
  for (let c in "aé😀") {
    out += "[" + c + "]";
  }
  out
}
```

Authors: aaronvg

## Unresolved-name errors on dotted access now underline just the root segment

0.12.2-nightly.20260627.c · nightly · 2026-06-27

When the root of a dotted access like `o.value` is an unresolved name, the `unresolved name` diagnostic (error code E0003) now underlines only `o` instead of the whole `o.value` expression.

```baml
function f() -> string {
  return o.value;
}
```

Previously the caret spanned all of `o.value`; now it points at `o`, the segment that actually failed to resolve. This is a diagnostic-span change only, with no effect on which programs compile.

Authors: codeshaunted

## `String.index_of` now returns `int?` and yields `null` instead of `-1` when the search is not found

0.12.2-nightly.20260627.b · nightly · 2026-06-27

`String.index_of` no longer returns `-1` for a missing substring. Its signature is now `int?`, and it returns `null` when `search` is not present. Code that compared the result against `-1` must be rewritten to check for `null`, typically with an `if let` binding.

```baml
"abc".index_of("z")  // null
```

**Highlights**

- **`String.index_of`** returns `int?`. A miss is now `null` rather than `-1`, so callers should pattern-match the result: `if let idx: int = s.index_of("/") { ... } else { ... }`. The stdlib testing registry helpers were updated to this form.
- **`float.to_fixed()`** is added, matching JavaScript's `toFixed` behavior.
- **`baml toolchain <version>`** output now appends a parenthetical showing the active channel and where the selector was resolved from, for example `(canary, from /work/demo/baml.toml)` for a project file, `(nightly, from $BAML_VERSION)` for the environment override, or `(canary)` for the default source. Exact-version selectors get no channel annotation.

Authors: aaronvg, rossirpaulo, ATX24

## `baml fmt` no longer bails on backtick strings in `prompt:`, attribute, and `template_string` slots

0.12.2-nightly.20260627.a · nightly · 2026-06-27

`baml fmt` now accepts backtick string literals in `prompt:` fields, attribute arguments, and `template_string` bodies instead of giving up on the entire file.

- **Backtick strings in declarative slots**: A backtick `` `...` `` literal used as a `prompt:` value, an attribute argument such as `@description(...)`, or a `template_string` body used to make the formatter bail on the whole file. It is now formatted like any other string. Multi-line interiors are re-indented to sit one level past the surrounding block, but only when the re-indent is provably value-preserving. Literals containing `${for}`/`${if}` block tags or multi-line `${...}` interpolations are left verbatim, since re-indenting those could change the dedented runtime value.
- **Tighter diagnostic spans**: Error and warning carets now exclude leading and trailing trivia (whitespace, newlines, comments), so an underline covers the construct itself rather than reaching into the whitespace after it. Reported columns shift by a character or two as a result.
- **`E0146` for unreachable code**: Dead code now reports under its own `E0146` diagnostic instead of being folded into the generic type-mismatch error.

Authors: codeshaunted

## `baml fmt` now formats backtick strings in `prompt`, attribute, and `template_string` slots instead of bailing

0.12.2-nightly.20260626.h · nightly · 2026-06-27

`baml fmt` no longer bails on a whole file when a backtick string appears as a `prompt:` value, an attribute argument like `@description(...)`, or a `template_string` body. Those slots now accept `` `...` `` alongside `"..."` and `#"..."#`, and a multi-line interior is re-indented to sit one level past the surrounding block, matching how raw strings are handled.

- **Backtick strings in declarative slots.** A `prompt: \`...\`` or `template_string Foo(...) \`...\`` used to make the formatter fail on the entire file. It now parses and re-emits them. The re-indent only applies when it is provably value-preserving: literals containing a `${for}`/`${if}` block tag or a multi-line `${...}` interpolation are printed verbatim so the dedented runtime value is never changed.
- **Tighter diagnostic spans.** Error underlines no longer bleed into trailing whitespace or newlines. Spans for return types, parameters, class fields, and match arms now tightly cover the construct, so carets shift by a column in many messages. End-of-input errors now point at a zero-width caret just past the last real token instead of covering a trailing newline.
- **New error code `E0146` (`UnreachableCode`).** Dead-code diagnostics were previously reported under a type-mismatch code. They now have their own `E0146` identifier.

```baml
function Demo(name: string) -> string {
    client "openai/gpt-4o"
    prompt `
        Hello ${name}
        Goodbye
    `
}
```

Authors: codeshaunted

## Internal refactor of the syntax-highlighting grammar with no behavior change

0.12.2-nightly.20260626.g · nightly · 2026-06-26

This nightly only refactors the TextMate grammar in `baml.ts`, factoring the duplicated method-call, field-access, and `is`-pattern rule shapes into shared helpers (`memberCall`, `memberAccess`, `isPatternRule`). The generated highlighting rules are equivalent, so there is no observable change to how BAML code is tokenized or highlighted.

Authors: codeshaunted

## `baml.unstable.string` is removed in favor of `string.from`

0.12.2-nightly.20260626.f · nightly · 2026-06-26

The `baml.unstable.string` builtin is gone. Use `string.from` to convert a value to its string representation.

- **`string.from` replaces `baml.unstable.string`.** Every call site in the stdlib (test registry naming, `_int_to_float`) now uses `string.from`, and the old `baml.unstable` namespace is no longer registered, so `baml.unstable.string(...)` will fail to resolve. Rename any call to `string.from(...)`.
- **Instance stringification format changed.** `string.from` renders a class instance inline as `Class { field: value, ... }` with unqualified class names, rather than the old multi-line output that prefixed class names with their package and namespace path. Nested strings and map keys stay quoted. If you were matching the exact output in asserts, update the expected strings.

```baml
string.from(3.14)
```

The BAML TextMate grammar is now authored in TypeScript under `typescript2/pkg-grammar/src/baml.ts` and compiled to `baml.tmLanguage.json`, with snapshot tests wired into CI. As part of this, the separate `baml-jinja` embedded language was removed from the VS Code extension and prompt-template highlighting folded into the single BAML grammar. This does not change BAML syntax, only how editors highlight it.

Authors: codeshaunted

## Internal test cleanup with no user-facing changes

0.12.2-nightly.20260626.e · nightly · 2026-06-26

No changes to the BAML language, runtime, or generated clients. This nightly removes the dead `rig_tests` directory (superseded by `sdk_tests`) and adds an internal `scripts/find_uncovered_rs.py` helper that flags `.rs` files not covered by the Cargo workspace. Both are repository-internal and do not affect anything you type or generate.

Authors: sxlijin

## `baml test` filter patterns now match by glob instead of regex

0.12.2-nightly.20260626.d · nightly · 2026-06-26

The `baml test` command's `-i`/`-x` filter patterns are now matched with `*`-only glob semantics instead of being compiled to a regex. `*` matches any run of characters (including `/`), and every other character is matched literally.

- **Filter semantics changed.** Previously each pattern was turned into a regex (`*` became `.*`), so `.`, `(`, `[`, and friends silently carried regex meaning and an invalid pattern was rejected with a parse error. Now those characters match literally, and the only wildcard is `*`. A pattern like `Foo.Bar` used to match `FooXBar`; it now matches only the literal `Foo.Bar`.
- **Testset execution moved into the `testing` stdlib.** Testset discovery, filtering, and running now happen entirely inside the BAML `testing` package through the new `run_filtered` and `list_filtered` functions on `TestRegistry`; the Rust driver just prints the returned report. Legacy `test` blocks attached to functions still run on the Rust side. How you write tests does not change.
- **`tolerated` is now reported separately.** The CLI report (`FlatTestReport`) distinguishes `tolerated` failures, failing leaves whose enclosing testset runner still passes (for example under a `PassRate` runner), from hard `failed` counts, rather than folding them together.

Filter patterns are still split into a function and test part at the first `::`:

```bash
baml test -i "MyFunc::*edge case*"
```

The Rust and BAML matchers share identical glob semantics, so legacy and testset tests filter the same way.

Authors: antoniosarosi

## Filtered test runs now honor their parent testset's runner

0.12.2-nightly.20260626.c · nightly · 2026-06-26

`baml test -i "suite::..."` now runs the selected tests under their parent testset's runner instead of bypassing it. Previously, filtering down to a single leaf or a subset of a testset ran those tests bare, so a leaf covered by a tolerant runner like `testing.PassRate` would hard-fail on its own.

**Highlights:**

- **Filtered selections respect runners.** Selecting a leaf with `-i "suite::failing leaf"` or a whole testset with `-i "suite::"` executes it through the registry under the parent runner, so a failure the runner tolerates no longer fails the command.
- **Tolerated failures are reported separately.** Runner-tolerated leaf failures are counted apart from real failures and shown in the summary, for example `aggregate passed — 2 passed, 1 tolerated failure, 3 total`. Hard failures in sibling testsets still fail the command and stay out of the tolerated count.

```bash
baml test --from . -i "suite::"
# aggregate passed — 2 passed, 1 tolerated failure, 3 total
```

Authors: aaronvg

## Routine nightly build with no traceable user-facing changes

0.12.2-nightly.20260626.b · nightly · 2026-06-26

This nightly snapshot carries no user-facing changes that can be traced to the build range. The provided context includes no commit log or file-level diff for this release, so there is nothing concrete to report. If you are on the nightly channel, you can stay on the prior build without missing functionality.

## A `cleanup(self) -> void` magic method gives classes a run-once finalizer

0.12.2-nightly.20260626.a · nightly · 2026-06-26

A class that defines `function cleanup(self) -> void { ... }` now gets a finalizer whose body runs at most once per instance (BEP-042), whether you call it explicitly or through `defer`.

**Highlights**

- **`cleanup` finalizer.** Define `cleanup(self) -> void` on a class and its body runs at most once per instance. The run-once guarantee is a per-instance latch: a second `cleanup` on the same instance (explicit call or `defer`) skips the body. The latch is set on entry, so a `cleanup` that throws still counts as cleaned.
- **`throws never` accepted on `cleanup`.** A `cleanup(self) -> void throws never` is treated the same as no `throws` clause and gets the finalizer guard. A `cleanup` whose signature is anything other than `(self) -> void` (extra parameter, non-`void` return, a default on `self`, or a propagating `throws`) is now a compile error, `E0144`.
- **Generic bounds must be interfaces.** A generic bound like `implements<T extends Widget> ...` where `Widget` is a class is now rejected with `E0145` instead of being silently dropped. Bounds resolve to interfaces (BEP-044).
- **Pydantic model construction.** Generated Pydantic models are now rebuilt so class declaration order no longer breaks model construction (#793).
- **Zed extension.** The bundled playground version is pinned to the Zed extension version, so the two no longer drift apart.
- **Python bridge.** Optional arguments now use PEP 661 sentinels to distinguish "not passed" from an explicit `None`.

Internally, `implements` blocks (both in-body and out-of-body) are now unified behind a single `impl_data` resolution query, and an unresolved interface type argument in an `implements` clause is reported exactly once rather than duplicated.

```baml
class Resource {
  log string[]
  function cleanup(self) -> void {
    self.log.push("cleaned")
  }
}

function main() -> string[] {
  let r = Resource { log: [] }
  r.cleanup()
  r.cleanup()
  r.log
}
```

Authors: antoniosarosi, GouravSingal-code, 2kai2kai2, sxlijin

## Array and map literals now carry their element and value types at runtime

0.12.2-nightly.20260625.f · nightly · 2026-06-25

Array and map literals now record their declared element, key, and value types at runtime instead of being erased to `unknown`. Most of this release is the internal plumbing for that change (an interface-object refactor in the type checker and threading container types through the MIR `Rvalue::Array` and `Rvalue::Map`), but a few effects are observable.

- **Config arrays keep their element types.** A heterogeneous config array such as `allowed_roles ["user", 123]` no longer erases its non-string element to null, which previously surfaced as a spurious `got null` type error. A `{ ... }` element inside a config array (for example `tools [{ url "..." }]`) now lowers to a map instead of being dropped.
- **Clearer associated-type projection errors.** Three new diagnostics fire when a projection's `as X` qualifier names a non-interface, when an interface or class does not declare the projected associated type, and when an unqualified `Base.member` projection is ambiguous. The ambiguity error names the candidate interfaces and suggests writing `(Base as Interface).member`.
- **`baml check` on Windows.** The compiler binary now reserves a 64 MiB main-thread stack, fixing stack overflows when compiling even small programs. Windows defaults to roughly 1 MiB where Linux and macOS get about 8 MiB.

Known gap in the config fix: a negative number like `-5`, a bigint literal like `42n`, and an out-of-range integer still lower to a string rather than their literal value, because the config lexer does not surface those forms to the lowering pass.

Authors: 2kai2kai2

## JSON decoding moves to the opt-in `baml.FromJson` interface and `baml.json.to<T>`

0.12.2-nightly.20260625.e · nightly · 2026-06-25

Custom JSON decoding is now an opt-in interface. A type controls how it is built from a `json` value by implementing `baml.FromJson` (the inverse of `baml.ToJson`), instead of declaring a bare `from_json` method.

- **`baml.FromJson` interface.** Override decoding inside an `implements baml.FromJson { ... }` block. Its `from_json` returns `Self` and throws `baml.json.JsonDecodeError`. Declaring `from_json` directly on a class is now a compile error, `FromJsonMustImplementInterface` (E0143), with a fix-it pointing you into the interface block.
- **`baml.json.to<T>(j)`.** New decode counterpart to `baml.json.from<T>`. It dispatches to a type's `baml.FromJson` override when present and otherwise decodes structurally per field, walking lists, maps, and optionals while honoring nested overrides. `Type.from_json(j)` now desugars to `baml.json.to<Type>(j)`.
- **Per-class `from_json` is no longer auto-derived.** Types without an override decode through the structural default in `baml.json.to`, so you no longer need to write boilerplate decoders.
- **`from_json` no longer throws `JsonParseError`.** Decoding an already-parsed `json` value never re-parses, so `baml.json.from_json`, `baml.json.to`, and `baml.yaml.deserialize` throw only `JsonDecodeError`. Update any `throws baml.json.JsonParseError | baml.json.JsonDecodeError` clauses on your decoders accordingly.

The `Instant`, `PlainDate`, `PlainDateTime`, `PlainTime`, `ZonedDateTime`, and `Table` stdlib types were migrated to the new interface.

```baml
class Temp {
  celsius float
  implements baml.FromJson {
    function from_json(j: baml.json.json) -> Self throws baml.json.JsonDecodeError {
      Temp { celsius: baml.json.to<float>(baml.json.field(j, "c")) }
    }
  }
}
```

Authors: codeshaunted

## `baml describe` now syntax-highlights and hyperlinks its output, with a global `--color` flag

0.12.2-nightly.20260625.d · nightly · 2026-06-25

`baml describe` now renders BAML with syntax highlighting and terminal hyperlinks, classified by the compiler's own semantic tokens so the coloring tracks the language exactly.

- **`--color` flag.** A global `--color` option takes `auto` (default), `always`, or `never`. `auto` enables color on an interactive terminal and disables it when output is piped or captured by a known AI coding agent (detected via env vars like `CLAUDECODE`, `CODEX_SANDBOX`, and `CURSOR_TRACE_ID`). An explicit `CLICOLOR_FORCE` still wins over agent suppression.
- **Colored "Did you mean?" hints.** Suggestions printed when a path does not resolve now color each leaf by its definition kind, and write through a stderr-bound painter so the color decision matches the stderr stream.

```bash
baml describe MyClass --color always
```

Authors: codeshaunted

## Playground graph adds semantic zoom and an Expand mode menu, plus `//#` header fixes in the control flow view

0.12.2-nightly.20260625.c · nightly · 2026-06-25

The playground's control flow graph now renders at a level of detail that follows how far you have zoomed in, so deeply nested workflows stay readable instead of drawing every node at once.

- **Expand mode menu.** A new control in the graph's bottom-right corner switches between `Zoom` (reveal subgraphs as you zoom in), `Click` (collapsed by default, click a node to open its subgraph), and `All` (expand everything). Collapsed containers render as a single leaf with a `+N` badge showing how many nodes are hidden, and clicking one expands it.
- **Deeper nodes render smaller.** Nodes lay out and draw at a reduced scale as nesting depth increases, except when a node shows a result, image, or error preview, which keeps its full size.
- **`//#` headers above `if`/`match` keep their branches.** A `//#` header placed directly above a branch now keeps the whole branch group and all of its arms in the graph, even when no arm contains its own anchor. Branch groups without an anchor that are not under a header are still pruned.
- **Cursor selection inside a header region.** Clicking anywhere a `//#` header governs, including inside an `if` that is not itself a rendered node, now selects that header node. The nearest preceding header in the same block wins, and any real node under the cursor still takes priority.

The selected node also stays visible under level of detail: if a graph update would hide it, its ancestor containers reopen automatically.

Authors: aaronvg

## JSON serialization moves from a `to_json` method to the `baml.ToJson` interface

0.12.2-nightly.20260625.b · nightly · 2026-06-25

Custom JSON serialization is now provided by implementing `baml.ToJson` instead of declaring a bare `to_json` method on a class. A directly declared `to_json` is now rejected with `E0142` (`to_json cannot be defined as a method on class ...; implement the baml.ToJson interface instead`), the JSON analog of last release's `baml.ToString` rule. To customize a class's JSON shape, move the method into an `implements baml.ToJson { ... }` block:

```baml
class Temp {
  celsius float
  implements baml.ToJson {
    function to_json(self) -> baml.json.json throws baml.json.JsonSerializationError {
      { "c": baml.json.from(self.celsius), "f": baml.json.from(self.celsius * 1.8 + 32.0) }
    }
  }
}
```

- **`baml.json.from(value)`** is the new universal driver, the JSON analog of `string.from`. It renders any value structurally (primitives naturally, lists and maps and class instances as `{"field": value}`, enums as their variant name, media in tagged form) and routes any value whose runtime class implements `baml.ToJson` through that override at every depth. The `obj.to_json()` call shape still works: it now desugars to `baml.json.from(obj)`, which throws `JsonSerializationError`.
- **`to_json` is no longer auto-derived.** Classes that do not implement `baml.ToJson` serialize through the structural default, so you no longer get a synthesized per-class `to_json`. `from_json` is still auto-derived. The built-in types (`string`, `int`, `float`, `bool`, arrays, maps, media, `uint8array`, and others) also dropped their own `to_json` methods in favor of `baml.json.from`.
- **`baml describe` gained topics** for cross-language SDKs and pattern matching: `baml describe python`, `baml describe typescript`, `baml describe baml_sdk`, and `baml describe patterns` (with `pattern` as an alias).
- **Clearer callback throws diagnostic.** When a body may throw through a callback whose type does not declare its throws, the error now spells out the fix: annotate an infallible host callback with `throws never`, otherwise catch the call or let the enclosing function declare or propagate the callback's throws.

Authors: aaronvg, codeshaunted

## JSON serialization moves from duck-typed `to_json` methods to the `baml.ToJson` interface

0.12.2-nightly.20260625.a · nightly · 2026-06-25

Custom JSON serialization now goes through the `baml.ToJson` interface instead of a duck-typed `to_json` method. A class that wants to control its own JSON shape declares `implements baml.ToJson { function to_json(self) -> baml.json.json throws baml.json.JsonSerializationError { ... } }`, and `baml.json.from(value)` is the universal driver: it renders any value structurally (primitives natural, lists and maps and class instances structural, enums as their variant name, media as the tagged form) and routes through a class's override at every depth.

- **`to_json` is no longer a magic method.** Declaring a bare `to_json` directly on a class is now rejected with `E0142` (`ToJsonMustImplementInterface`), the JSON analog of the existing `baml.ToString` rule. Move it into an `implements baml.ToJson { ... }` block. The built-in `to_json` methods on `string`, `int`, `float`, `bool`, `null`, `bigint`, arrays, maps, `uint8array`, and the media types (`Image`, `Audio`, `Video`, `Pdf`) were removed in favor of the driver, and the stdlib time and TOML types (`Instant`, `PlainDate`, `PlainDateTime`, `PlainTime`, `ZonedDateTime`, `Table`) now expose serialization through `implements baml.ToJson` instead.
- **`to_json` is no longer auto-derived for your classes.** The compiler still synthesizes `from_json`, but `to_json` is owned by the interface, so `obj.to_json()` on a class without an override desugars to `baml.json.from(obj)` and renders structurally. Calling `.to_json()` can now surface `baml.json.JsonSerializationError`, since `baml.json.from` throws it (unlike `string.from`, which is infallible).
- **`baml describe` gains SDK and pattern topics.** `baml describe python`, `baml describe typescript`, and `baml describe baml_sdk` document generating and calling a typed client from another language, and `baml describe patterns` (alias `pattern`) covers `match` arms, the `is` operator, and `if let`.
- **Clearer callback-throws diagnostic.** When a body may throw through a callback whose type does not declare what it throws, the message now spells out both fixes: annotate an infallible host callback with `throws never`, or catch the call or let the enclosing function declare and propagate the callback's throws.

```bash
baml describe python
baml describe typescript
baml describe patterns
```

Authors: aaronvg, codeshaunted

## `.to_string()` works on any value and honors `baml.ToString` overrides at every depth

0.12.2-nightly.20260624.b · nightly · 2026-06-24

You can now call `.to_string()` on any value, whether or not its type implements `baml.ToString`. The call lowers to `string.from(value)`, so a primitive, array, map, or plain class instance renders the same way `string.from` would, and a type that does implement `baml.ToString` still dispatches its own method.

```baml
[1, 2, 3].to_string()
```

**Highlights:**

- **`obj.to_string()` sugar.** A 0-arg `to_string` call on a value with no real `to_string` method now type-checks as `string` and renders via `string.from`. `x.to_string()` and `string.from(x)` produce identical output. A `to_string` defined through `implements baml.ToString` is dispatched as before and is never hijacked.
- **Nested `baml.ToString` overrides are honored.** `string.from` (and the new sugar) now render any nested value whose runtime class overrides `to_string` via that override, at every depth, instead of falling back to structural rendering. A `Line { start Point  end Point }` whose `Point` overrides `to_string` renders as `Line { start: (1, 2), end: (3, 4) }`. The two builtin implementors `type` and `uint8array` are now covered too, so they no longer diverge from a direct `.to_string()`.
- **Clearer diagnostics for field access on `unknown`.** Accessing a member on an `unknown`-typed value now reports `cannot access field \`x\` on \`unknown\`` with a hint to use `match` to narrow the value first, rather than the generic "has no member" message.
- **Formatter fix for keyword path segments.** The formatter no longer breaks on dotted paths whose segments are keywords, such as `baml.spawn.TaskGroup.new` and `baml.spawn.options`, and these now round-trip idempotently.

Authors: codeshaunted, aaronvg

## Built-in test runners let you attach `testing.PassRate`, `Quorum`, and `Retry` to tests and testsets

0.12.2-nightly.20260624.a · nightly · 2026-06-24

This release adds a set of built-in runners you attach to a `test` or `testset` with the `with` keyword, so you control how a test is repeated and how its children are aggregated without writing the runner yourself.

**New runners** (in the `testing` package):
- **`testing.Quorum(n, m)`** and **`testing.Retry(max_attempts)`** wrap a single test. `Quorum` runs the body `n` times and passes when at least `m` runs pass. `Retry` re-runs the body until it passes or `max_attempts` is reached. Both validate their arguments and panic on invalid input (for example, `Quorum` requires `0 <= m <= n`).
- **`testing.PassRate(threshold)`**, **`testing.Sequential()`**, and **`testing.FailFast()`** aggregate a testset's children. `PassRate` passes when the fraction of passing children is at least `threshold`. `Sequential` runs children in order; `FailFast` does the same but stops at the first non-passing child.

**`baml test` aggregates through the runner.** An unfiltered run of a testset now reports the aggregate verdict its runner produces, so a `PassRate(0.6)` suite with one failing leaf still passes overall. Failed child names are printed (for example, `failed: suite/two`), and a runner that fails the aggregate while marking zero children failed is still counted as a failure in the summary. Filtering to a specific leaf with `-i suite::three` bypasses the parent runner and runs that test directly.

```baml
testset "flaky_suite" with testing.PassRate(0.6) {
  test "one" { assert.is_true(true) }
  test "two" { assert.is_true(true) }
  test "three" { assert.is_true(false) }
}
```

`TestSetReport` gained a `failed_names` field and `RunReport` gained an optional `message`, both surfaced in CLI failure output. The internal `TestSetRunner` type is now `(TestSetChild[]) -> TestSetReport`, receiving lazy children so a runner decides when and whether to invoke each one.

Authors: rossirpaulo

## No change data available for this release.

0.223.0 · engine · 2026-06-23

No commit log or file diff was available for this release, so there are no user-visible changes to report. If you need details on what shipped in `0.223.0`, consult the release tag directly.

## `Array.filled` initializer and a `render_null_as` output-format option

0.12.2-nightly.20260623.c · nightly · 2026-06-23

Two additions to the stdlib and prompt rendering.

- **`Array.filled(length, value)`.** A new static constructor on `Array<T>` that allocates a runtime-sized array with every slot set to `value`, replacing the manual `while`/`push` loop for buffers like DP tables or adjacency lists. The element type is inferred from `value`, and a zero or negative `length` returns an empty array instead of erroring.
- **`render_null_as` on `ctx.output_format`.** The output-format helper takes a new `render_null_as` string kwarg that controls how the `null` type is labeled in the rendered schema. With `ctx.output_format(render_null_as="omit")`, a `string?` field renders as `string or omit` instead of `string or null`, and nullable media instructions read `Return an image output or omit.`. Passing a non-string value is a type error.

```baml
baml.Array.filled(3, 0)
```

This range also unifies the outbound bridge wire format onto the shared `baml_type.proto` types (the `Ty` messages are renamed to `BamlTy`) and bumps the package version to 0.223.0. Those are internal to the SDK bridges and do not change how you write BAML.

Authors: ATX24, aaronvg, sxlijin

## `Array.filled` stdlib constructor and a `render_null_as` output format option

0.12.2-nightly.20260623.b · nightly · 2026-06-23

Two new stdlib and prompt features land in this nightly, alongside an internal bridge protocol refactor.

- **`Array.filled(length, value)`**: a new static constructor on `Array<T>` that builds a `T[]` of `length` elements all equal to `value`, the idiomatic way to allocate a runtime-sized, pre-initialized buffer without a manual `while`/`push` loop. The element type is inferred from `value`, and a zero or negative `length` clamps to an empty array instead of failing. Every slot shares the same `value`, so for reference types each slot refers to the same object.
- **`render_null_as` output format option**: `ctx.output_format(...)` and `output_format_with(...)` accept a new `render_null_as` string kwarg that controls how the `null` type is rendered in the schema shown to the model. For example, `render_null_as='omit'` renders a `string?` field as `string or omit` instead of `string or null`. It defaults to `null`, so existing prompts are unchanged, and passing a non-string value is a type error.
- **Bridge protocol**: the outbound CFFI wire types were unified onto `baml_type.proto`, renaming `Ty` and its variants to `BamlTy` and replacing the old `BamlTyName` wrappers with bare FQN strings plus positional `type_args`. This is an internal refactor of the host bridge with no change to BAML source behavior. As a visible side effect, the Go SDK now decodes bigint and float literal values returned from the engine.

```baml
baml.Array.filled(3, 0)
```

Authors: ATX24, aaronvg, sxlijin

## Function values can no longer declare generics, and `.baml` files are executable via `baml run --file`

0.12.2-nightly.20260623.a · nightly · 2026-06-23

Generic parameters are gone from function *values*: a lambda or function type can no longer declare its own `<...>`, and a generic function referenced bare must be specialized before you use it as a value.

- **Lambdas and function types reject generic parameters.** `<T>(x: T) -> T { x }` and a `<T>(T) -> R` function type are now parse errors, reported as "a lambda cannot declare generic parameters" and "a function type cannot declare generic parameters". Write a monomorphic lambda, or specialize a named generic function instead.
- **A bare generic function must be specialized.** `let f = identity` where `identity<T>` is generic now fails with "generic function `identity` must be specialized before it is used as a value (e.g. `identity<int>`)". Write `let f = identity<int>`.
- **Type arguments require a function reference.** A parenthesized or other non-path base such as `(foo)<int>` is now rejected with "type arguments can only be applied to a function reference".
- **Executable `.baml` scripts via `baml run --file`.** A `#!` line at the top of a `.baml` file is parsed as a comment, so you can make the file executable. `baml run --file` with no target runs `main`, and a shebang can name a different function (`run greet --file` runs `greet`). Arguments passed after a `--` separator reach `baml.sys.argv()`, and `baml fmt` preserves the shebang as the first line.

```baml
function main() -> string[] {
    baml.sys.argv()
}
```

Authors: 2kai2kai2, sxlijin

## Generic class instances can be passed directly as arguments from the Node.js SDK

0.12.2-nightly.20260622.f · nightly · 2026-06-22

The Node.js SDK can now pass a generic class instance, such as `new Wrapper({ value: 5 })`, straight into a BAML function. Previously the TypeScript encoder serialized every class instance as a bare map, and the engine rejected coercing a bare map into a generic class slot ("a bare map carries no class type arguments"), so these calls failed.

**Highlights**

- **Generic instances carry their type args.** A generic class instance now encodes as an FQN-tagged `class_value` with a value-level type-args channel (`class_ty`) instead of a bare map, so the engine decodes it to an `Instance` and accepts it into a generic slot. Calls like `round_trip_wrapper_int(new Wrapper({ value: 5 }))` and `round_trip_box_int(new Box({ value: 3, wrapped: new Wrapper({ value: 4 }) }))` now work.
- **Explicit `$types` bindings.** Generated generic classes gain an optional `$types` field, and generic function and method calls accept a `$types` option to bind type parameters by name. TypeScript erases generics at runtime, so unlike the Python SDK these bindings must be spelled out. `Never`, `lowerTypeToWireTy`, `BamlType`, and `GenericParams` are now exported from `@boundaryml/baml`. An absent binding lowers to the unknown/top type, which the engine treats as a wildcard.
- **Generic instance methods round-trip.** When a BAML function returns a generic instance, the outbound decoder repopulates its `$types` from the wire, so a follow-up instance-method call recovers its type parameter. `make_wrapper_methods("hello").get_value_or_marker()` now returns `"hello"` instead of failing the engine's generic gate.
- **Python outbound generics.** A generic instance returned to the Python SDK now carries its concrete class type args (as positional `generic_args`) so the host can rebuild the parameterized type via `cls[args]`.
- The previously-skipped round-trip tests for passing generic class instances as arguments are now enabled.

```typescript
import { Never } from "@boundaryml/baml";
import { Wrapper, round_trip_wrapper_int } from "./baml_client";

// Pass a generic class instance directly as a function argument.
const w = new Wrapper({ value: 5 });
round_trip_wrapper_int(w);

// Bind a type parameter explicitly with the `$types` field.
const empty = new Wrapper({ value: 0, $types: { T: Never } });
```

CONTRIBUTING.md was also rewritten, and now states that robot-generated drive-by pull requests will be rejected.

Authors: sxlijin

## A trailing `!` now gets a targeted diagnostic instead of an `if`-without-`else` cascade

0.12.2-nightly.20260622.e · nightly · 2026-06-22

Writing a trailing `!` such as `xs.at(0)!` now produces a single diagnostic that points at the `!` itself, instead of a cascade of unrelated errors.

BAML has no postfix `!` (non-null assertion) operator: `!` is only a prefix unary operator. Previously a trailing `!` was left unconsumed, and the statement loop re-parsed it as a prefix `!` applied to whatever followed, a closing brace or an `else`, producing a misleading "expected expression" / "if without else" cascade that never blamed the `!`. The parser now consumes the stray `!` with the message "unexpected '!'; BAML has no non-null assertion operator" and suggests unwrapping optionals with `?? <default>` or `if (let x) = opt { ... }`.

Two existing behaviors are preserved: `!=` still lexes as a single not-equals token, and a `!` that starts the next line stays a prefix operator on a fresh statement (BAML separates statements by newlines), so it is not absorbed as a postfix on the previous line.

Authors: ATX24

## Scientific-notation float literals now work in `.baml` source

0.12.2-nightly.20260622.d · nightly · 2026-06-22

Scientific-notation float literals (`1e10`, `1E3`, `1e-3`, `2e+5`, `1.5e3`, `1.5e-3`) now lex and compile in `.baml` source. Previously `1e10` lexed as the integer `1` followed by an identifier `e10`, surfacing a misleading "unresolved name: e10" error. A `projects/compiles/` snapshot test covers the full path from lexer through codegen for these literals.

- **Exponent digits are mandatory.** A bare `1e` still lexes as an integer followed by a word, and a space breaks the form (`1 e10` stays integer plus word).
- **Member access is unchanged.** `3.foo` is still integer, dot, word, and plain numbers like `3.14` and `42` lex as before.

```baml
function DecimalMantissaNegativeExponent() -> float {
    1.5e-3
}
```

This release also tightens generics handling at the host SDK boundary: a generic SDK call must now bind every type parameter through the `_types=` channel, or it is rejected with a type error rather than being silently run with an unbound type variable.

Authors: ATX24, sxlijin

## Integer overflow now throws a catchable `baml.panics.IntegerOverflow` instead of wrapping

0.12.2-nightly.20260622.c · nightly · 2026-06-22

`int` arithmetic that leaves the representable range now throws a catchable `baml.panics.IntegerOverflow` rather than silently wrapping. This applies to `+`, `-`, `*`, `/`, unary `-`, and `<<`. The same value is delivered whether or not the expression is constant-folded at compile time, so folded and runtime overflows behave identically.

- **`int` is a 63-bit signed integer.** Its range is `-2^62` to `2^62-1` (`-4611686018427387904` to `4611686018427387903`), one bit narrower than i64 because the runtime reserves a tag bit. Use `bigint` when you need more. The `int.baml` and `float.baml` doc comments were updated to match.
- **Out-of-range int literals are now a compile error (`E0139`).** A bare literal whose magnitude exceeds the `int` range, for example `4611686018427387904`, is rejected at compile time with a diagnostic that points you at `bigint`, instead of panicking the VM at engine load. `int.min_value()` stays writable as the negated literal `-4611686018427387904`, but a parenthesized `-(4611686018427387904)` is rejected like a bare oversized literal.
- **`<<` overflow throws `IntegerOverflow`; a negative shift count throws `NegativeBitShift`.** For example `1 << 62` exceeds the range and throws.
- **`%` by zero now throws `DivisionByZero`** on the generic binary-op path, matching `/` by zero (previously it could raw-panic the VM).

`IntegerOverflow` is a member of the `baml.panics.Panic` union, so you can catch it specifically or alongside other panics:

```baml
function safe_increment(x: int) -> int {
    x + 1 catch (e) { baml.panics.IntegerOverflow => -1 }
}
```

Authors: antoniosarosi

## New `defer` statement runs cleanup code on every block exit

0.12.2-nightly.20260622.b · nightly · 2026-06-22

This release adds the `defer` statement (BEP-042) to the compiler2 language. A `defer { ... }` block runs when the enclosing block exits, on every exit path: normal fall-through, `return`, `break`/`continue`, and error unwinding out of a call. Defers are block-scoped, not function-scoped, and multiple defers in the same block run last-declared-first.

```baml
function cleanup_demo() -> string {
  let log = ""
  {
    defer { log = log + "B;" }
    defer { log = log + "A;" }
    log = log + "body;"
  }
  log
}
```

Here the inner block produces `body;A;B;`: the body runs, then at the block's exit the defers fire in reverse order.

- **Live scope at exit.** A defer body reads the current value of enclosing locals when it runs, not a snapshot taken at the `defer` site. It is not a closure that captures values.
- **Control flow is restricted.** `return`, `break`, and `continue` that would escape a defer body are rejected with error `E0141` ("`<keyword>` cannot leave a `defer` body"). A `break`/`continue` targeting a loop declared inside the defer is allowed. `throw` may propagate out of a defer.
- **Runs on unwinding.** A defer also runs when an exception propagates out of a call inside the block, so it works as a cleanup hook around code that can throw.

The same change includes a VM fix for exception routing: a throw escaping a called function (or a runtime panic) is now caught by the innermost enclosing `catch`, not the outermost. Previously the exception table picked the first covering entry and could mis-route to an outer handler.

Authors: antoniosarosi

## Code generators are now configured in baml.toml instead of `generator` blocks

0.12.2-nightly.20260622.a · nightly · 2026-06-22

Code generators are now declared in `baml.toml` under `[generator.<name>]` sections instead of `generator { }` blocks in your `.baml` files. This is a breaking change to how you configure codegen.

- **`[generator.<name>]` in baml.toml.** `baml generate` reads its targets from the manifest now. Each section takes `output_type`, `output_dir`, and the required `naming_convention`. A project with no `[generator.<name>]` section prints a missing-generator hint and exits non-zero rather than generating nothing.
- **`generator { }` blocks are ignored.** A stale `generator` block left in a `.baml` file no longer parses its body. It emits warning `E0017` (`GeneratorBlockUnsupported`) telling you to move the config to `baml.toml`. Your other declarations in that file are unaffected.
- **`generator` is no longer a BAML item.** Editor completions for the top-level `generator` keyword are gone, and `baml grep --kind generator` is no longer a valid filter (the accepted kinds list drops it).
- **`baml init`** now scaffolds a commented `[generator.my_client]` example in the generated `baml.toml`.

Move an existing block like this:

```toml
[generator.my_client]
output_type = "python/pydantic"
output_dir = "../python"
naming_convention = "preserve-case"
```

Unknown keys in `baml.toml` are now reported as warnings rather than silently ignored, so a typo such as `[scriptz]` or `outpt_type` surfaces instead of being dropped.

Authors: sxlijin

## Unquoted non-ASCII string values in LLM output now parse correctly

0.12.2-nightly.20260621.a · nightly · 2026-06-21

The JSON-ish fixing parser used by `bex_sap` now counts characters instead of bytes when skipping unquoted values, so unquoted multi-byte UTF-8 text no longer corrupts the tokens that follow it.

Previously, an unquoted value containing non-ASCII characters (for example a Cyrillic or Greek sentence) could over-advance the parser by the byte length rather than the character count, eating the front of the next object key and causing the surrounding object to fail to parse. Quoting the value worked around it. This is the case covered by the new `test_word_bug` suite, which isolates the failure to unquoted multi-word non-ASCII values.

If you parse model output that returns unquoted non-ASCII strings, these inputs now coerce the same as their quoted equivalents.

Authors: hellovai

## Internal-only release: Windows streaming e2e tests re-enabled and repo cleanup

0.12.2-nightly.20260619.g · nightly · 2026-06-19

No user-facing changes. This release only touches CI and test infrastructure. The checked-in SSE replay recordings (`*.snap.sse`) are now forced to LF line endings via `.gitattributes`, so Windows `autocrlf` no longer collapses the paced `split("\n\n")` stream into a single chunk. With that fixed, the previously Windows-skipped streaming end-to-end tests in the Python and TypeScript SDK suites are unskipped. A few vestigial files (`commit-viz.html`, `dump_git_stats.py`, and a stray `thoughts` symlink) were also removed from the repo root.

Authors: codeshaunted

## Windows streaming e2e tests re-enabled and stray repo-root files removed

0.12.2-nightly.20260619.f · nightly · 2026-06-19

No changes to the BAML language or SDKs in this nightly. The work is test infrastructure and repo cleanup. The checked-in SSE replay recordings (`*.snap.sse`) now get a `text eol=lf` rule in `.gitattributes`, so Windows `autocrlf` no longer rewrites their `\n\n` chunk separators into `\r\n\r\n`. That was causing the replay server to stream each body as a single chunk, so the `>= 10 partials` streaming assertions saw zero partials. With consistent line endings, the `skipif`/`skipIf` Windows guards are dropped from the Python (`test_streaming_e2e.py`) and TypeScript (`streaming_e2e.test.ts`) streaming e2e suites. Separately, the `commit-viz.html` and `dump_git_stats.py` scratch files and a `thoughts` symlink were removed from the repo root.

Authors: codeshaunted

## New `baml playground` command, and `--from` now finds your project from any subdirectory

0.12.2-nightly.20260619.e · nightly · 2026-06-19

This release adds a `baml playground` command and changes how every project command locates your code.

**Highlights**

- **`baml playground`**: a new command that opens the BAML playground in your browser. It takes `--from <PATH>` for project mode or `--file <PATH>` to load a single standalone `.baml` file. The two flags are mutually exclusive.
- **`--from` now walks up to the project root.** Across `run`, `test`, `generate`, `pack`, `check`, `fmt`, `grep`, and `describe`, `--from` changed from a directory that defaulted to `.` to an optional search starting point. It walks up the ancestor directories looking for a `baml.toml` or `baml_src/` marker, Cargo-style, so these commands now work from a nested subdirectory instead of only the project root. Omitting `--from` starts the search from the current directory.
- **`--file` / `--from` rejection is now exact.** Passing both flags is rejected whenever both are set. Previously the check compared `--from` against the literal `.`, so an explicit `--from .` alongside `--file` slipped through.
- **`baml describe` emits package-qualified builtin paths.** Listing a builtin package now prints names like `baml.iter.Range` instead of `iter.Range`, so the output can be passed straight back into `baml describe` without guessing the package prefix.
- **String interpolation no longer produces spurious inlay hints.** Backtick `${...}` interpolations lower to a synthetic `string.from(...)` call and a concat accumulator binding. These compiler-generated nodes are now tracked and skipped, so editors stop showing `value:` parameter hints on every interpolation and `: T` type hints on accumulators you never wrote. The release also adds syntax highlighting for interpolations.
- **bridges: thrown class FQN is preserved through multi-member throws unions**, so generated clients keep the fully-qualified name of a thrown class when the throws clause has more than one member.

The LSP `--playground-via-browser` flag is gone, replaced by a repeatable `--workspace <PATH>` flag on the language server.

```bash
# open the playground for the project containing the current directory
baml playground

# run from a nested directory; --from walks up to the project root
baml run --from baml_src/nested SomeFunction
```

Authors: codeshaunted, ATX24, rossirpaulo

## Failing `assert.equal` now prints both operands, and `baml run -e` accepts leading-hyphen expressions

0.12.2-nightly.20260619.d · nightly · 2026-06-19

Two CLI and test-output fixes.

- **`assert.equal` failure messages now show the values.** A mismatch used to print the generic `assertion failed: expected equal values`. It now prints `assertion failed: left = <actual>, right = <expected>`, rendering each operand via `string.from`. The failure output also no longer leaks internal `Span` and `FileId` debug structs, and `baml test` failures print errors with `{e}` instead of `{e:?}`.
- **`baml run -e` accepts expressions that start with a hyphen.** The `-e`/`--expression` flag now sets `allow_hyphen_values`, so a value like `-7 % 3` is parsed as the expression rather than being mistaken for another flag. Other run flags after it, such as `--from`, are still honored.

```bash
baml run -e "-7 % 3" --from project
```

Authors: ATX24

## `assert.equal` failures print both operands, and `baml run -e` accepts hyphen-prefixed expressions

0.12.2-nightly.20260619.c · nightly · 2026-06-19

Two fixes to the test and run workflow.

- **`assert.equal` failure messages now show the operands.** A failing `assert.equal(actual, expected)` previously printed only `assertion failed: expected equal values`. It now renders both sides, for example `assertion failed: left = 4611686018427387903, right = -4611686018427387904`, using a new `format_operand` helper. Test failure output also no longer leaks internal `Span` and `FileId` debug structs, and `FAIL` lines print the error with `Display` rather than the `{:?}` debug form.
- **`baml run -e` accepts leading-hyphen expressions.** Passing an expression that starts with `-`, such as a negative number, no longer trips clap argument parsing. This is now handled via `allow_hyphen_values` on the `-e`/`--expression` flag.

```bash
baml run -e '-7 % 3' --from project
```

Authors: ATX24

## `baml.sys.sleep` now takes a `Duration` instead of a millisecond `int`

0.12.2-nightly.20260619.b · nightly · 2026-06-19

`baml.sys.sleep` now takes a `root.time.Duration` argument instead of a bare millisecond `int`. The old `sleep(ms: int)` signature is gone, so a call like `baml.sys.sleep(200)` must become `baml.sys.sleep(baml.time.Duration.from_milliseconds(200))`. This makes the unit explicit at every call site and lets you express sub-millisecond and multi-hour delays with the same API.

```baml
function nap() -> null throws baml.errors.Io {
  baml.sys.sleep(baml.time.Duration.from_milliseconds(200))
}
```

- **`Duration` constructors accept `int` or `bigint`.** `Duration.from_nanoseconds`, `from_microseconds`, `from_milliseconds`, `from_seconds`, `from_minutes`, and `from_hours` now take `int | bigint`, so you can write `from_milliseconds(200)` without the `n` bigint suffix.
- **Out-of-range and negative delays.** The native and WASM sleep implementations clamp a negative `Duration` to zero and saturate oversized values rather than erroring.

Authors: aaronvg

## Internal refactor: the Ty type family is now generated by a ty_family! macro

0.12.2-nightly.20260619.a · nightly · 2026-06-19

This nightly is an internal compiler refactor with no change to BAML language behavior. The `Ty` type family in the `baml_type` crate (`RuntimeTy`, `RealizedTy`, `ConcreteTy`, and the new `ConcreteRealizedTy`), their satellite structs, `attr`/`with_attr` accessors, and the full `From`/`TryFrom` conversion matrix are now generated from a single tagged definition by a new `ty_family!` procedural macro, replacing the hand-written mirrors and the `subenum` dependency. If you only write BAML, nothing observable changes here.

Authors: 2kai2kai2

## Host callables can declare optional parameters with `?`

0.12.2-nightly.20260618.b · nightly · 2026-06-19

Host callables passed into a BAML function can now declare optional parameters with the `?` marker, and BAML can invoke them with named optional arguments like `callback(x, y = 2)`. Previously a callback type could only take required positional parameters.

```baml
function call_callback_with_optionals(callback: (x: int, y?: int, z?: int) -> int, x: int) -> int[] {
    [callback(x), callback(x, y = 2), callback(x, y = 2, z = 3)]
}
```

- **Optional callback parameters.** A callback type like `(x: int, y?: int, z?: int) -> int` is now legal. Each optional crosses the boundary by name, so you can supply any subset. An omitted optional is dropped before dispatch, so the host callable's own language-level default fills it.
- **Per-SDK calling convention.** TypeScript groups the supplied optionals into a trailing `$opts` object, so the codegen'd callback type reads `(x: number, $opts?: { y?: number; z?: number }) => number`. Python passes them as keyword arguments. Go invokes positionally and does not support skipping a middle optional.
- **Defaults stay disallowed in callable types.** Writing a default inside a callable type, such as `(x: int, y: int = 10) -> string`, is still rejected (`E0010`). A default is a property of a declaration, not a type. Use the `?` marker instead.

Authors: sxlijin

## Backtick string literals, `string.from`, and a `baml.ToString` interface

0.12.2-nightly.20260618.a · nightly · 2026-06-18

Backtick string literals (`` `...` ``) with `${...}` interpolation land in the language, alongside a new `string.from<T>` conversion driver and a `baml.ToString` interface for controlling how values render.

**Backtick templates (BEP-049 M1-M4).** A `` `...` `` literal supports `${expr}` interpolation, `${for (let x in xs)}...${endfor}` and `${if (c)}...${else}...${endif}` control flow, and tagged templates of the form `` tag`...` ``. A tagged template lowers to a `baml.TaggedString { parts, values }` value passed to a tag function marked `//baml:tagged_string`, where `parts` and `values` alternate. Malformed blocks now get specific diagnostics, for example an unclosed `${for}`, a `${for}` closed by `${endif}`, or a stray `${endif}`. An empty `${}` is a warning and still renders to the empty string.

```baml
function Greeting(name: string, count: int) -> string {
  `Hello ${name}, you have ${count} messages`
}
```

**`string.from` and `baml.ToString`.** `string.from(value)` renders any value to a string and never throws, falling back to a default structural rendering (`[1, 2, 3]`, `Point { x: 1, y: 2 }`) for types that do not opt in. A class controls its own rendering by implementing `baml.ToString`. Interpolation inside a backtick template renders each `${...}` through `string.from`.

**`to_string` must go through the interface.** A class can no longer define a bare `to_string` method. Defining one now raises `ToStringMustImplementInterface`. Put it inside `implements baml.ToString { ... }` instead. Auto-derive no longer synthesizes a `to_string` for user classes (it still derives `to_json` and `from_json`).

**Date and time `to_string` now panics instead of throwing.** `Instant`, `PlainDate`, `PlainDateTime`, and `ZonedDateTime` moved their `to_string` onto `baml.ToString` with `throws never`. A value that cannot be formatted (such as a year outside the RFC 3339 or ISO 8601 range) now panics rather than surfacing `InvalidArgument`. The corresponding `to_json` still surfaces it as a `JsonSerializationError`.

**Unicode codepoint indexing fix.** A SWAR continuation-byte mask in `bex_str` codepoint indexing was corrected, fixing wrong indices on multibyte UTF-8 strings.

Authors: codeshaunted

## Comparison operators and the `Equals` / `Compare` interfaces

0.12.2-nightly.20260617.b · nightly · 2026-06-17

BAML now has comparison operators: `==` and `!=` work on any compatible pair of operands, and `<`, `<=`, `>`, `>=` work on any type that implements `baml.ops.Compare`. Built-in implementations ship for `int`, `bigint`, `float`, `bool`, `string`, `null`, `uint8array`, lists, and maps, so most code can compare values without any setup.

```baml
["a", "b"] == ["a", "b"]
```

- **`Equals` and `Compare` interfaces.** Implement `baml.ops.Equals` (`eq`, `neq`) and `baml.ops.Compare` (`lt`, `le`, `gt`, `ge`) for your own types to make them usable with these operators. A class without `Equals` still compares structurally field by field, and enums compare by identity.
- **Stricter ordering checks.** Ordering requires both operands to have the same type and that type to implement `Compare`. Comparing two types that share no value (for example `int` and a string literal) is now flagged as always true or always false. A new orphan-rule diagnostic (`E0139`) and clearer coherence errors reject conflicting `implement` blocks.
- **Operator errors read better.** Diagnostics now print the operator you wrote, so a bad `+` reports ``operator `+` cannot be applied to `int` and `"five"` `` instead of the internal `Add` name.
- **`let` locals in test bodies.** Fixed a bug where `let`-bound locals inside a test body lost their runtime type tag, causing reads to fall back to null. They now resolve correctly.
- **`baml agent install`.** The command help and post-install summary now state that skill names are prefixed with `baml-` (upstream `core` installs as `baml-core`) to avoid registry collisions.

Authors: 2kai2kai2, hellovai, ATX24

## Negative indexing across arrays, strings, and byte arrays, plus streaming HTTP server responses

0.12.2-nightly.20260617.a · nightly · 2026-06-17

Negative indices now count from the end across sequence operations, the way they do in Python and JavaScript. `at`, `remove_at`, `insert`, `splice`, `slice`, `char_at`, `substring`, and the subscript `[]` operator all accept them, so `-1` is the last element.

```baml
["a", "b", "c"].at(-1)
```

**Highlights**

- **Negative indexing.** `[10, 20, 30].at(-1)` returns `30`, `xs.remove_at(-1)` removes the last element, `xs.insert(9, -1)` inserts before the last element, and `slice` counts both bounds from the end and clamps out-of-range bounds into `[0, length]`. Behavior changes to be aware of when upgrading: `char_at` at or past the end now raises `baml.panics.IndexOutOfBounds` instead of returning an empty string, `at` and `remove_at` with a negative index now resolve from the end instead of returning `null`, and `insert` and `splice` with a negative `start` now insert from the end instead of raising `InvalidArgument`. An index that still falls outside the sequence after counting from the end follows each operation's existing out-of-range rule (`null`, panic, or `InvalidArgument`).

- **Streaming HTTP server responses.** `baml.http.Response.new_streaming(status_code, headers)` builds a response whose body is written incrementally with `write(data)` and finished with `end()`. hyper frames it with chunked transfer-encoding and flushes each `write` as its own chunk, so an `http.Server` handler can stream Server-Sent Events in real time. `write` applies backpressure until the previous chunk is accepted, and raises `Io` if the response was not built with `new_streaming`, has already been ended, or the client hung up. Do not set `Content-Length` on a streamed body.

- **SDK panics no longer abort the host process.** The embedded Python and Node SDKs are now built with `panic = "unwind"`, so a Rust panic in the engine (for example a bounds check) is caught at the bridge boundary and surfaced as `baml.panics.SdkPanic` rather than calling `process::abort()` and killing the interpreter with exit 134.

- **Class-typed streaming through the SDK bridge.** Calling a generic instance method such as `Stream.next` or `Stream.final` by name from the host now substitutes the receiver's concrete type arguments into the declared return type, so a streaming function whose `T` is a multi-field class returns typed partials instead of panicking during host-return conversion.

Authors: sxlijin, 2kai2kai2, codeshaunted

## Cross-kind reassignment and mistyped index subscripts are now compile errors

0.12.2-nightly.20260616.f · nightly · 2026-06-16

A family of programs that used to compile and then abort the VM at runtime are now caught by the type checker. The headline case (B-236): `let x = {}; x = []` reassigned an array into a map-typed local, so a later `x["a"]` crashed with `expected map, got array`. That reassignment, and the related index-key holes below, now report a `type mismatch` at compile time.

- **Cross-kind reassignment of an unannotated local is rejected.** Reassigning a value of an incompatible kind into a local inferred from its initializer (`let n = 1; n = "hello"`, or swapping `{}` for `[]`) now errors. An empty evolving container still grows normally: `let a = []; a = [1, 2, 3]` and the map equivalent stay valid, since the empty list/map has not yet committed to an element type.
- **Index keys are type-checked.** A list or byte array must be subscripted by an `int` and a map by a `string`. There is no key coercion at runtime, so `list["a"]`, `map[0]`, and the index-assign forms `list["a"] = v` / `map[0] = v` are now compile errors instead of VM aborts.
- **A nullable subscript is rejected.** `arr[i]` where `i: int?` is null used to abort with the confusing `got any`. It now reports `type mismatch: expected int, got int | null`. Narrowing the index to non-null first (`if i != null { return arr[i]; }`) keeps it valid.
- **`?.[]` is null-safe in the index, not just the base.** A null subscript through the optional-index operator short-circuits the whole expression to null rather than crashing, mirroring the base guard.

Well-typed access is unaffected:

```baml
function main() -> int {
    let x = [];
    x[0] = 1;
    let m = {};
    m["k"] = 2;
    return x[0] + m["k"];
}
```

Authors: codeshaunted

## The formatter no longer adds stray spaces inside empty map literals

0.12.2-nightly.20260616.e · nightly · 2026-06-16

`baml fmt` now formats an empty map literal as `{}` instead of `{  }`. The printer previously emitted the interior padding spaces unconditionally, so a map with no fields came out with two stray spaces. Padding is now added only when there is something to surround, so populated maps still format as `{ "a": 1 }` and a map holding only a block comment keeps its padding as `{ /* keep */ }`.

Authors: codeshaunted

## baml test no longer drops testset tests with long qualified names

0.12.2-nightly.20260616.d · nightly · 2026-06-16

`baml test` now finds testset tests whose qualified id (`testset/test`) runs past about 54 bytes. Previously those tests registered with an empty name, showed up in `baml test --list` as `::`, and failed at runtime with `Test not found:`.

The cause was in the internal `bex_str` string type. Flattening a concatenation destructively emptied the inner nodes it folded in, which is wrong when a node is shared (for example `let n = a + b; let p = n + c`), so every other reference to that node read back as an empty string. Strings longer than the 54-byte inline limit are the ones that defer into shared nodes, which is why only long test names were affected. Flatten now clones the child handles and leaves the shared node intact.

Authors: codeshaunted

## Keyword docs in `baml describe` now match current BAML syntax

0.12.2-nightly.20260616.c · nightly · 2026-06-16

The keyword reference printed by `baml describe` was rewritten to match the syntax BAML actually parses. If you read these docs to remember a form, several entries were wrong and are now corrected. This is a documentation fix only, the language itself did not change.

Corrected entries:

- **`for`, `in`, `break`, `continue`**: loop headers are parenthesized and the binding uses `let`. The docs now show `for (let item in items) { ... }` instead of the old unparenthesized `for item in items`.
- **`match`**: the scrutinee is parenthesized and type arms bind with `let`. The example is now `match (value) { let x: string => ..., let x: int => ..., _ => ... }` rather than the old `x is string => ...` form.
- **`catch` and `catch_all`**: both now show the `(e)` dispatch form. `catch (e) { ErrorType => handler, ... }` handles selected error types and lets the rest propagate, while `catch_all (e) { _ => fallback }` must be exhaustive.
- **`watch`**: documented as a prefix on a `let` binding, `watch let user_input = SimulateHumanGuess(history);`, replacing the old `on_value`/`on_done` stream-block form. The binding is reactive and cannot take an `else` clause.
- **`testset`**: now named with a quoted string and holding `let` setup plus nested `test` blocks, `testset "math suite" { let answer = 42; test "adds" { ... } }`. The old `functions` and `cases` keys are gone.
- **`generator`**: `output_type` takes a quoted value. The example now reads `output_type "python/pydantic"`.
- **`implement` / `impl`**: the example method body now returns `"${self}"` instead of `self.to_string()`.

```baml
testset "math suite" {
  let answer = 42

  test "adds" {
    assert.equal(answer, 42)
  }

  test "is positive" {
    assert.is_true(answer > 0)
  }
}
```

Authors: ATX24

## Networking and HTTP operations now accept an optional per-call timeout

0.12.2-nightly.20260616.b · nightly · 2026-06-16

The blocking `baml.net` and `baml.http` operations now take an optional `timeout: baml.time.Duration? = null`. Pass a `Duration` to bound how long the call waits, and on expiry it throws `baml.errors.Timeout`. The default, `null`, blocks indefinitely (matching Rust's `Option<Duration>`, where `None` means block forever).

**Operations that gained a `timeout` parameter:**
- **`net.TcpStream.connect(addr, timeout?)`**, **`read(timeout?)`**, and **`write(data, timeout?)`**. On a read or recv timeout the stream remains usable for a later call.
- **`net.UdpSocket.send_to(data, addr, timeout?)`** and **`recv_from(timeout?)`**. Both now declare `throws root.errors.Timeout` in addition to `Io`.
- **`http.fetch(url, timeout?)`** and **`http.send(request, timeout?)`**, where `timeout` is the total deadline for the request (connection plus response headers plus body).

**Behavior change:** `TcpStream.connect` previously applied a fixed 10 second connect deadline. It now defaults to unbounded (subject only to the OS default) unless you pass an explicit `timeout`.

**Note on wasm:** the browser `fetch` backend does not yet honor an explicit `timeout`, since reqwest's wasm client has no per-request timeout hook. A `null` timeout is unbounded there regardless.

```baml
function ReadWithTimeout(addr: string) -> uint8array throws root.errors.Io | root.errors.Timeout {
  let sock = baml.net.TcpStream.connect(addr, timeout = baml.time.Duration.from_seconds(5n));
  sock.read(timeout = baml.time.Duration.from_milliseconds(50n))
}
```

Authors: codeshaunted

## `baml describe Array.reduce` resolves builtin class members without the `baml.` prefix

0.12.2-nightly.20260616.a · nightly · 2026-06-16

`baml describe` now drills into builtin class methods spelled with their unqualified class name, so `Array.reduce`, `String.split`, and `Map.get` resolve to the stdlib method instead of returning NOT FOUND. Previously only the explicit `baml.Array.reduce` form or the bare class name worked.

The fallback runs after the user-package lookup, so a user-defined class that shares a builtin name keeps precedence. If you define your own `class Array` with a `reduce` method, `baml describe Array.reduce` still shows your method, and `baml.Array.reduce` remains the explicit spelling for the builtin.

```bash
baml describe Array.reduce
```

Authors: ATX24

## `baml fmt` accepts directory paths and formats them recursively

0.12.2-nightly.20260615.c · nightly · 2026-06-16

Passing a directory to `baml fmt` now formats every `.baml` file inside it recursively, instead of only accepting individual files.

- **Directory arguments**: `baml fmt ./baml_src` walks the directory and formats each `.baml` file it finds. Non-`.baml` files are left untouched.
- **Overlapping paths deduplicated**: if you pass both a directory and a file inside it, each file is formatted once.
- **Clearer empty result**: when no `.baml` files are found, the error message now names the paths you passed rather than only the working directory.

```bash
baml fmt ./baml_src
```

Authors: ATX24

## Removes internal tracing design notes, no user-facing changes

0.12.2-nightly.20260615.b · nightly · 2026-06-15

This nightly only deletes internal scratchpad documents under `baml_language/TASK/` (the BEX event identity ticket, review, and post-implementation notes). There are no code or behavior changes, so nothing in your BAML projects is affected.

Authors: rossirpaulo

## Lower BEX tracing overhead on the VM call hot path

0.12.2-nightly.20260615.a · nightly · 2026-06-15

This nightly is an internal performance change to BEX profiling, with no user-visible API, syntax, or behavior change. The VM now serializes `CallFunction` and `EndFunction` records straight into the ring buffer slot instead of encoding to a stack buffer and copying, the tick-to-nanosecond conversion uses a precomputed multiply-shift reciprocal rather than a per-record divide, and the consumer accumulates a whole drained range into one write instead of one write per event. If you do not run with BAML profiling enabled, nothing about your code or its output changes.

Authors: antoniosarosi

## Auto-derived to_json now works on interface- and generic-typed fields

0.12.2-nightly.20260613.a · nightly · 2026-06-13

Auto-derived `to_json()` now works on classes whose fields are interface-typed or type-variable-typed. The per-field synthesizer routes each field through the free function `baml.json.to_json(self.<field>)`, which dispatches on the value's runtime type, instead of a direct `self.<field>.to_json()` call whose static type exposed no `to_json` method to resolve against.

This nightly hardens the boundary between compile-time inference types and runtime types, so inference-only types (`Unknown`, `Error`) can no longer be fed to the VM. The user-visible effects:

- **`to_json` derivation.** Fields with interface or generic types now derive correctly rather than triggering a runtime panic or a fallback path. Null handling moves into the free function, so nullable fields no longer need special optional-chaining synthesis.
- **Assignment in expression position.** Writing an assignment where a value is expected, such as `(x = 5)`, now reports the compile error "assignment is not allowed in expression position; assignment is a statement" instead of lowering to code that only fails at runtime.
- **Broken projects fail cleanly.** When a project still has unresolved compile errors, bytecode generation is skipped and the diagnostics are surfaced, rather than passing `Unknown`/`Error` types across the runtime boundary.
- **Generic and interface resolution.** Type parameters on `implements<T> Iface for Target` blocks, interface default and required method signatures, and `$stream` companion references now resolve to their real types instead of erasing to `unknown`, removing a class of runtime panics in generic and interface code.

The bulk of the change is an internal rename of the runtime type from `Ty` to `RuntimeTy` with no behavior change of its own. A prescriptive `TYPE_SYSTEM.md` describing BAML's set-theoretic type system was also added.

Authors: 2kai2kai2

## Class-typed LLM streaming works in Python, and `CmpError` is renamed to `CompareError`

0.12.2-nightly.20260612.e · nightly · 2026-06-12

Streaming a BAML function whose return type is a class (not just `string`) now works end to end in the Python SDK. Calling the generated `<Function>_stream` / `<Function>_stream_async` companion on a function that returns a multi-field class previously crashed at runtime with a "Non-parsable type" error; it now streams partials and returns a fully typed final value. Functions declared inside a namespace are covered too.

- **Class-typed streaming.** A function returning a class (e.g. `StreamingDoc { title, body, word_count }`) can now be driven through `next()` / `final()` (and the async `next_async()` / `final_async()`) without the old `Non-parsable type` crash.
- **`CmpError` renamed to `CompareError`.** The associated type on the stdlib `Comparable` interface is now `CompareError`. Update any `type CmpError = ...` bindings to `type CompareError = ...`, and any `T.CmpError` projections (such as in `throws` clauses or `SortError` bindings) to `T.CompareError`. The old name no longer compiles.

```baml
class ComparableScore {
  points int
  implements baml.Comparable {
    type CompareError = never
    function compare(self, other: Self) -> int throws never {
      self.points.compare(other.points)
    }
  }
}
```

Authors: sxlijin

## Rename the `Comparable` error type from `CmpError` to `CompareError`, and fix Python streaming

0.12.2-nightly.20260612.d · nightly · 2026-06-12

The associated type on the standard library `Comparable` interface is renamed from `CmpError` to `CompareError`. If you implement `Comparable` for your own type, rename the binding `type CmpError = ...` to `type CompareError = ...`.

- **`CompareError` rename.** The change flows through the rest of the ordering APIs: `Sortable.SortError` and the error type of `Array.sort_by_key` are derived from a type's `CompareError`, so they pick up the new name automatically. Only your explicit `type CmpError = ...` bindings need updating.
- **Python streaming.** Streaming from LLM functions now works in the Python runtime. No changes are needed in your BAML or Python code.

Authors: sxlijin

## Release tooling now validates the generated Node loader version

0.12.2-nightly.20260612.c · nightly · 2026-06-12

Internal release tooling only, with no changes to the BAML language or SDK behavior. The `scripts/baml-language-version` release check now verifies that the generated Node bridge loader (`dist/native.js`) embeds napi version guards matching the release version, and `sync` regenerates the Node bridge after stamping versions so the loader stays in sync. If you do not build the repository from source, this release does not affect you.

Authors: rossirpaulo

## Profiling clock now reads raw hardware ticks, with conversion moved off the hot path

0.12.2-nightly.20260612.b · nightly · 2026-06-12

Internal change to the `.bamlprof` profiling pipeline, with no effect on BAML programs or their output timestamps. The producer side now stamps records with raw counter reads (`now_ticks`: `rdtsc` on x86_64, `CNTVCT_EL0` on aarch64, a monotonic `Instant` fallback otherwise) instead of nanoseconds, dropping the `minstant` dependency and its pre-main TSC-calibration ctor. The consumer converts ticks to nanoseconds at transcode via `TickConverter`, so `timestamp_ns` fields in `.bamlprof` files remain nanoseconds and existing readers are unaffected. The file header gains diagnostic-only fields (`clock_kind`, `tick_ns_numer`, `tick_ns_denom`, `clock_quality`) describing the tick source; nothing needs them to interpret timestamps.

Authors: antoniosarosi

## `baml pack` on Linux no longer corrupts the packed executable

0.12.2-nightly.20260612.a · nightly · 2026-06-12

`baml pack` now produces working executables on Linux. The previous ELF writer placed the embedded `PackEnvelope` note at a virtual address that could overlap the host binary's `.bss`, so packed binaries crashed at startup with a SIGSEGV or failed to read their own envelope (surfacing as borsh "invalid utf-8" or "Unexpected variant tag" errors). The bug only triggered when the host's `.bss` grew past a page boundary, which is why it shipped from a macOS-based dev setup where the Mach-O path is used instead.

- **Linux pack writer**: `baml pack --target` for a Linux triple now goes through a custom ELF note appender instead of `libsui::Elf::append`. The note is placed past the segment's full memory image so its address always clears `.bss`. The on-wire note format is unchanged, so existing readers parse it as before.
- **Local host warning**: when you pack for a non-native `--target` while a workspace-built host sits next to the CLI, `baml pack` now warns that it is ignoring the local host and downloading a matching release host instead.
- **Internal rename**: the host-value handle kind `HOST_VALUE_ERROR` was renamed to `HOST_VALUE_OPAQUE` (and `HostValueKind::Error` to `Opaque`) to reflect that it carries arbitrary opaque host values, not just exceptions. This is an internal cffi enum with no change to BAML-visible behavior; host exceptions still surface as `baml.errors.HostCallable`.

Authors: rossirpaulo, sxlijin, antoniosarosi

## The standard library gains `Array.filter`, lazy array iterators, and a `baml.csv` streaming namespace

0.12.1 · canary · 2026-06-12

`Array.filter` and `Array.remove_at` are now part of the standard library, and array iterators are lazy, so `filter` and `map` chains do no work until you `collect()` them.

- **`Array.filter` and `Array.remove_at`.** New stdlib array operations (#3741). `filter` keeps the elements matching a predicate, `remove_at` drops the element at a given index.
- **Lazy array iterators.** Chained transforms now defer evaluation until you call `.collect()` (#3747). `Array.filter_map` stays eager, and a follow-up fixes handling of nullable elements in `ArrayIterator` (#3749).
- **`baml.csv` streaming namespace.** A new `baml.csv` namespace adds streaming CSV readers and writers (BEP-060, #3744).
- **CLI.** `baml generate` now prints the CLI version in its output (#3748), and `baml agent install` produces reproducible installs (#3753).

```baml
[1, 2, 3, 4].filter((x: int) -> bool { x > 2 }).collect()
```

Internally, the BEX event-stream tracing surface was trimmed for a fresh start (#3616), and the lock-free profiling ring (`bex_events::prof`) gained loom and miri CI coverage. Profiling artifacts now write under `.baml/profiles/` by default, and you can opt out with `BAML_PROFILE=0`.

Authors: antoniosarosi, hellovai, aaronvg, rossirpaulo

## `baml agent install` now pulls version-pinned, checksum-verified skills by default

0.12.1-nightly.20260612.e · nightly · 2026-06-12

`baml agent install` no longer fetches skills from the live `BoundaryML/baml-skill` main branch by default. It now downloads a release archive pinned to your CLI version and verifies its `.sha256` checksum before installing, so a given CLI build always installs the same skills.

- **Default behavior changed.** Without flags, `baml agent install` resolves `baml-agent-skills-<version>.tar.gz` from the matching `baml-language-<version>` release and fails if the checksum does not match.
- **`--latest`** opts back into the previous behavior, installing from the current `BoundaryML/baml-skill` main branch.
- **`--from <URL_OR_PATH>`** installs from a tar.gz URL, a local tar.gz archive, or a local directory. It conflicts with `--latest`.
- **Override hooks.** `BAML_AGENT_SKILLS_RELEASE_VERSION`, `BAML_AGENT_SKILLS_RELEASE_BASE_URL`, and `BAML_AGENT_SKILLS_RELEASE_REPO` let you point the default resolution at a different version, base URL, or repository.

The release workflow now builds and publishes the `baml-agent-skills-<version>.tar.gz` archive (via the new `scripts/package-agent-skills`) alongside the toolchain assets.

```bash
baml agent install --from ./baml-agent-skills-0.12.1.tar.gz
```

Authors: rossirpaulo

## Internal CI and build scaffolding for the profiling ring

0.12.1-nightly.20260612.d · nightly · 2026-06-12

This nightly is internal only. It adds the build and CI plumbing for the in-progress `bex_events::prof` profiling ring: three workspace dependencies (`crossbeam-utils`, `loom`, `minstant`), a `cfg(baml_loom)` check-cfg entry, and a `prof-concurrency` CI job that runs loom model checking plus miri over the ring's concurrency tests. The implementation of the ring itself is not in this range, only the scaffolding that supports it.

The one user-facing note: `baml_language/.gitignore` now ignores `**/.baml/profiles/`, where profiling writes its `.bamlprof` artifacts. Profiling is on by default, so set `BAML_PROFILE=0` to opt out.

Authors: rossirpaulo

## `Array.filter_map` runs eagerly and `ArrayIterator` now yields null elements

0.12.1-nightly.20260612.c · nightly · 2026-06-12

This release adds an eager `filter_map` method on `Array<T>` and fixes `ArrayIterator` so nullable elements are no longer dropped during iteration.

- **`Array.filter_map`**: A new method on `Array<T>` that applies `fn` to every element left-to-right and collects the non-null results into a new array. It does not mutate the receiver. Previously you reached for `iter().filter_map(fn)`, which stays available for the lazy version.
- **Nullable elements in `for` loops**: Iterating an `int?[]` (or any array whose element type is itself nullable) used to stop at the first stored `null`, because `ArrayIterator.next` matched `at()` against null to detect the end. It now bounds-checks by index, so a real `null` element is yielded instead of ending iteration early.

```baml
[1, 2, 3, 4].filter_map((x: int) -> int? {
  if (x % 2 == 0) {
    x * 10
  } else {
    null
  }
})
```

Authors: aaronvg

## Array iterator adapters now require `.iter()` first

0.12.1-nightly.20260612.b · nightly · 2026-06-12

The lazy iterator adapters `filter_map`, `step_by`, `chain`, `peekable`, `collect`, and `count` are no longer direct methods on arrays. Call `.iter()` to get an iterator first, then chain them.

- **Adapters moved behind `.iter()`.** Code like `[1, 2, 3].collect()` or `[1, 2].chain([3, 4]).collect()` no longer type-checks. Insert `.iter()`: `[1, 2, 3].iter().collect()` and `[1, 2].iter().chain([3, 4]).collect()`. The eager `filter` and `map` methods that return arrays directly are unchanged.
- **`.iter()` is lazy over the live array.** `ArrayIterator.new` now references the source array instead of taking a snapshot copy, so elements pushed after the iterator is created but before it is consumed are included, and per-element closures run only on `collect()`.
- **CLI version in `generate`.** `baml generate` now prints a status line `Generating clients with CLI version: <version>`.

```baml
function lazy_filter_map_collect() -> int[] throws unknown {
  [1, 2, 3, 4].iter().filter((x: int) -> bool {
    x % 2 == 0
  }).map((x: int) -> int {
    x + 10
  }).collect()
}
```

Authors: aaronvg

## Array iterator methods now require `.iter()` and evaluate lazily

0.12.1-nightly.20260612.a · nightly · 2026-06-12

Lazy iterator methods like `filter_map`, `chain`, `peekable`, `step_by`, `collect`, and `count` are no longer defined directly on arrays. Call `.iter()` first to get an `Iterator`, then chain. This is a breaking change if your code called any of these methods on an array value.

- **Lazy iteration over the live source.** `.iter()` no longer copies the array. The iterator reads from the source array as it is consumed, so elements pushed before you `collect()` are included. `filter` and `map` called directly on an array stay eager and still return an array.
- **Migration.** Replace `arr.filter_map(...)`, `arr.chain(...)`, `arr.peekable()`, `arr.collect()`, and `arr.count()` with `arr.iter().filter_map(...)`, `arr.iter().chain(...)`, and so on.
- **`baml generate` prints the CLI version.** Generation now reports the CLI version it ran with in its status output, so you can confirm which version produced a given set of clients.

```baml
function lazy_pipeline() -> int[] throws unknown {
  [1, 2, 3, 4].iter().filter((x: int) -> bool {
    x % 2 == 0
  }).map((x: int) -> int {
    x + 10
  }).collect()
}
```

Authors: aaronvg

## Add the `baml.csv` standard library for streaming CSV reads and writes

0.12.1-nightly.20260611.c · nightly · 2026-06-11

This build adds `baml.csv`, a CSV standard library (BEP-060) with streaming `CsvReader` / `CsvWriter` handles, typed decode, and structured `CsvError` diagnostics. One-shot helpers like `baml.csv.parse` wrap the streaming reader, and `ReaderOptions` / `WriterOptions` cover delimiters, quoting, headers, trimming, ragged-record policy, null values, encoding, and per-record error handling.

```baml
baml.csv.parse("name,age\nAda,36", options = baml.csv.ReaderOptions { has_header: false })
```

Other changes carried in this build:

- **Run actions for `test` and `testset` blocks.** Expression-body `test "..."` and `testset "..."` blocks now surface run actions in the editor, including a new "Run all tests in this testset" action, even though they desugar into a synthesized registration function and never appear as normal declarations.
- **Explicit type arguments on method calls.** A call like `f.zero<int>()` on a local receiver now resolves the method's declared generic parameters instead of falling through, so the explicit type argument is honored.
- **Playground control-flow graphs prune to anchored nodes.** The visualization now keeps only header comments (`//#`), LLM functions and calls, and the structure that contains them, and it renders `for` loops with labels and early `return` statements as their own nodes.
- **Inlay hints reach `test`/`testset` bodies and methods.** Inline type hints on un-annotated `let` bindings and parameter-name hints now appear inside `test` and `testset` block bodies (which lower to lambdas) and inside class and interface methods. LLM declarative functions remain skipped.

Authors: aaronvg, rossirpaulo

## The `events.send` intrinsic is removed while the observability layer is rebuilt

0.12.1-nightly.20260611.b · nightly · 2026-06-11

The `events.send` intrinsic (the `baml.events.send` custom-event call) has been removed. Code that emitted custom events through this namespace will no longer compile, and the `ns_events` namespace is now empty pending a rebuild of the observability layer.

The rest of this release is internal teardown that supports the same change. The event-stream plumbing has been pulled out of the engine and CLI: `BexEngine::new` no longer takes an event-sink argument, the `bex_events_native` crate is gone, and the compiler no longer emits block notifications or viz-node metadata. There is no replacement event API in this release.

Authors: hellovai

## `Array.filter` now returns a new array, and `Array.remove_at` is added

0.12.1-nightly.20260611.a · nightly · 2026-06-11

`Array.filter` now returns a new `T[]` instead of a `baml.iter.Iterator`. Calling it gives you the matching elements directly, with no `.collect()` needed.

```baml
[1, 2, 3, 4, 5, 6].filter((x: int) -> bool { x % 2 == 0 })
// [2, 4, 6]
```

**Highlights:**

- **`Array.filter` returns `T[]`.** The predicate runs once per element, left-to-right, and `self` is not mutated. If you relied on the old iterator return value to chain `.map`, `.flat_map`, `.reduce`, `.step_by`, or `.for_each` directly off `filter`, wrap the array first: `baml.iter.ArrayIterator.new(arr).filter(...)`. The error type also changed from `never` to `E`, so a predicate that throws now propagates through `filter`.
- **`Array.remove_at(index)` added.** Removes and returns the element at `index`, mutating `self` in place, or returns `null` when `index` is out of bounds or negative. It is `O(n)` because later elements shift back. For `O(1)` removal from the end, use `pop`.

Authors: rossirpaulo, antoniosarosi

## A structured concurrency model and new standard library namespaces for time, config parsing, and iteration

0.12.0 · canary · 2026-06-11

0.12.0 lands a concurrency model built on `spawn` and `await`, several new standard library namespaces, and a wave of language and tooling fixes. Highlights for code you write:

- **Concurrency: `spawn ... with` and `baml.future` combinators.** A `spawn` can now take a `with baml.spawn.options(...)` clause to set its `group`, `cancel`, and `detach`. `baml.spawn.TaskGroup` caps how many spawns run concurrently (excess queue FIFO), and `baml.spawn.CancelToken` is a one-shot cooperative cancellation handle whose firing makes the task's next `await` throw `baml.panics.Cancelled`. The `baml.future` namespace adds `all`, `all_complete`, `race`, and `any` over arrays of futures, where `any` throws `AllFailed<E>` if every future fails. Note that `await f catch (e) { ... }` now parses as `(await f) catch (e) { ... }`.

- **`baml.time` date/time family (BEP-021).** New `ZonedDateTime`, `PlainDateTime`, `PlainDate`, `PlainTime`, and `TimeZoneOffset` following TC39 Temporal semantics. `PlainDateTime.to_zoned` and `ZonedDateTime.from_components` resolve DST gaps and overlaps via a `Disambiguation` argument, throwing `UnknownTimezoneError` or `AmbiguousTimeError`. Resolution uses `jiff` on native targets and the JS `Temporal` API on wasm.

- **`baml.toml` and `baml.yaml` parsers.** `baml.toml.Table.parse` maps TOML's four datetime kinds onto the new `baml.time` types and throws `TomlParseError`. `baml.yaml.parse` projects YAML into the `baml.json` value algebra and rejects anything outside it (tags, non-string keys, non-finite floats, multi-document streams, out-of-range integers) with `YamlParseError` rather than converting lossily.

- **`baml.iter` and iterable `for` loops.** `for (let x in ...)` now works over anything implementing `baml.iter.Iterable`, not just `T[]`, deriving the element type through `Iterable.Item` and picking up the iterable's `Error` type in throws inference. `Array<T>` gains fluent `for_each`, `filter`, `filter_map`, `step_by`, `chain`, `peekable`, `collect`, and `count` returning `baml.iter.Iterator` adapters, and `chain` now accepts iterables with a different `Error` type, widening the result to `Error | E2`.

- **Array sorting.** `sort`, `sort_by`, and `sort_by_key` sort in place and return `self`. Sorting is generic via a `Comparable` interface (with builtin impls for int, float, bigint, and string) and a `Sortable` blanket impl for `T[]`. `int[]`, `string[]`, and `bigint[]` sorts are infallible, while `float[]` rejects NaN at runtime. `sort_by` follows the JavaScript comparator convention and `sort_by_key` is stable.

- **`const` bindings.** You can now write `const` anywhere you write `let` to introduce a binding. It currently behaves exactly like `let` and emits diagnostic `E0010` warning that immutability is not enforced yet. Using `const` as a bound name (for example `let const = x`) is an error.

- **Optional types render as `T | null`.** `T?` now lowers directly to the union `T | null` (non-null member first). Source syntax is unchanged, but diagnostics, hovers, and `reflect.type_of` now print `string | null` instead of `string?`, and a nullable function member renders parenthesized as `((x: int) -> int) | null`.

- **Typed throws on host callables.** `call_host_value<T, E>` now carries a declared throws contract `E` alongside its return type `T`. A return that does not inhabit `T`, or a throw that is not a subtype of `E`, becomes a catchable `baml.panics.HostContractViolation`. `baml.errors.HostCallable` was reworked to carry an opaque `_handle` enabling same-process round-trip identity, and `baml.fs`, `baml.http`, and `baml.env` I/O failures now unwind as catchable `baml.errors.*` throws (including a new `ParseError` for non-UTF-8 input) rather than the removed `ExternalOpFailed`.

- **Networking and HTTP server.** `baml.net` was restructured to mirror Rust's `std::net`: `baml.net.connect`/`listen` become `baml.net.TcpStream.connect` and `baml.net.TcpListener.bind`, reads and writes now use `uint8array`, and a new `baml.net.UdpSocket` adds `bind`, `send_to`, `recv_from`, and `close`. A new `baml.http.Server` serves on a given port, with HTTPS when passed a `baml.http.TlsConfig`.

- **`log.*` accept any value.** `log.info`, `log.debug`, `log.warn`, and `log.error` now take `unknown` instead of `map<string, unknown>`, so you can pass any BAML value directly.

- **CLI: project discovery without a manifest.** `baml describe` and other introspection commands now treat a directory containing `baml_src/` as a real project, matching how `run` and `pack` already discover projects. Running them from a subdirectory walks up to the enclosing `baml_src/` even when there is no `baml.toml`. Relatedly, `baml run --list` now shows real generic signatures (for example `read_item<T extends BoxLike>(box: T) -> T.Item`) with a `generic_params` field in JSON output, and concrete associated-type projections no longer erase to void at the VM boundary.

- **`baml fmt` catch-arm indentation.** Nested catch arm bodies are now formatted with correct nested indentation instead of the previous mixed or under-indented output.

- **Diagnostics.** A non-exhaustive `catch_all` now reports an `E0062` diagnostic when typed catch arms leave inferred throw types unhandled, explicit object constructors report unresolved names in checked contexts (so a typo like `ValidationIssu { ... }` errors), and diagnostics from synthesized test lambda bodies now point at the real call site.

The fluent array iterator methods compose directly:

```baml
[1, 2, 3, 4].filter((x: int) -> bool { x % 2 == 0 }).step_by(2).collect()
```

Authors: sxlijin, codeshaunted, 2kai2kai2, rossirpaulo, antoniosarosi, aaronvg, ATX24

## Array `sort()` is now backed by the `Comparable`/`Sortable` interfaces

0.11.3-nightly.20260611.a · nightly · 2026-06-11

`[T].sort()` is no longer a built-in `Array<T>` method. It is now provided by a new stdlib `Sortable` interface, blanket-implemented for `T[]` whenever `T implements Comparable`. The practical effect: sorting a non-orderable element type is a compile error (the diagnostic points you at `sort_by`) instead of an `InvalidArgument` thrown at runtime, and sorting `int`, `bigint`, or `string` arrays is now infallible (`throws never`).

**What changed for your code:**

- **`Comparable` and `Sortable` interfaces** ship in the stdlib (`baml/comparable.baml`). `Comparable` exposes `function compare(self, other: Self) -> int throws CmpError`, with built-in implementations for `int`, `bigint`, `string`, and `float`, all binding `type CmpError = never`.
- **Non-orderable `sort()` calls now fail at compile time.** `(int | float)[].sort()`, `int?[].sort()` (an optional is a union), and a class array whose element type has no `Comparable` impl no longer compile. Previously these reached runtime and threw `InvalidArgument`.
- **`float[].sort()` no longer rejects `NaN`.** Floats order by IEEE 754 total order (`f64::total_cmp`), so `NaN` has a defined position and sorts after `+inf` rather than throwing.
- **User types can opt in** by implementing `Comparable`. A total order binds `type CmpError = never`; a fallible comparator binds `type CmpError = MyError` and that error propagates to anyone who sorts the values.
- **`sort_by_key` now requires a `Comparable` key.** Its signature is `sort_by_key<U extends Comparable, E>`, and the key's `U.CmpError` propagates alongside your callback's `E`. A nullable key is now a compile error rather than a runtime `InvalidArgument`. `sort_by` is unchanged.

`sort_by` and `sort_by_key` are documented as stable: if a comparison or key callback throws mid-sort, the array is left in its pre-sort state.

Sorting a primitive array works as before:

```baml
[3, 1, 2].sort()
```

A user type opts into `sort()` by implementing `Comparable`:

```baml
class Resume {
  name string
  implements baml.Comparable {
    type CmpError = never
    function compare(self, other: Self) -> int throws never {
      self.name.compare(other.name)
    }
  }
}
```

Authors: sxlijin

## `log.info` and the other log functions now accept any BAML value

0.11.3-nightly.20260610.d · nightly · 2026-06-10

The `log.info`, `log.debug`, `log.warn`, and `log.error` functions now take `data: unknown` instead of `data: map<string, unknown>`. You can pass a string, number, bool, list, class instance, enum variant, or null directly, and the value is emitted unchanged as the log event's `data`. Maps still work and represent structured fields as before.

```baml
function test_logging_any_values() -> void {
    log.info("plain message")
    log.debug(2)
    log.warn(true)
    log.error(null)
    log.info(["a", "b"])
    log.info({ event: "structured", count: 3 })
}
```

Authors: rossirpaulo

## catch_all now requires an arm for every thrown type

0.11.3-nightly.20260610.c · nightly · 2026-06-10

A `catch_all` block now reports error `E0062` when it omits an arm for a throw type the caught expression can produce. Previously a partial `catch_all` was accepted silently, letting an uncaught throw slip through.

**Highlights**

- **Non-exhaustive `catch_all` (`E0062`).** If a function can throw, say, `BuildError | string`, the `catch_all` must handle both. Missing arms produce `non-exhaustive catch_all on type ...; missing: ...`.
- **Diagnostic spans point at the offending identifier.** Unresolved-name and unresolved-type diagnostics now underline the actual name (for example the callee in a function call) instead of pointing at the start of the file.
- **Misspelled explicit constructors are now caught.** A typo'd class name in a constructor (for example `ValidationIssu` instead of `ValidationIssue`) reports `unresolved type` rather than being accepted.

An exhaustive `catch_all` that compiles cleanly:

```baml
class BuildError {
  reason string
}

function Generate(ok: bool) -> string {
  if (!ok) {
    throw BuildError { reason: "generate failed" };
  };
  if (ok) {
    throw "string"
  }
  "ok"
}

function Build(ok: bool) -> string {
  Generate(ok) catch_all (e) {
    BuildError => "recovered: " + e.reason,
    string => "caught string",
  }
}
```

Authors: aaronvg

## Cancel in-flight calls with `BamlCallContext` and `ctx.abort()`

0.11.3-nightly.20260610.a · nightly · 2026-06-10

Cancellation now flows through the SDK bridges with a new `BamlCallContext`, which replaces the old `AbortController`. Construct a context, pass it to a call, and call `ctx.abort()` to interrupt the call at its next cancellation check point.

- **`BamlCallContext` replaces `AbortController`.** The `AbortController` class is gone. Create a `BamlCallContext`, hand it to the call (`_ctx=ctx` in Python, the trailing context argument in the Node SDK), then call `ctx.abort()` to cancel. One context can cover several concurrent calls.
- **Cancellation surfaces as native cancellation.** An aborted async call now raises `asyncio.CancelledError` in Python and rejects with an `AbortError` in the Node SDK, instead of a `BamlPanic`. In both cases the error's `reason` is a `BamlCancelledError`, so you can tell BAML cancellation apart from other cancellations.
- **Pre-cancellation is race-free.** Calling `ctx.abort()` before the call starts still cancels it. Aborting a not-yet-registered context is no longer a no-op, so the later call starts already cancelled rather than running to completion.
- **Call IDs must be a nonzero `uint64`.** Call IDs widened from 32-bit to 64-bit, and a zero ID is now rejected with `BamlInvalidArgumentError: call_id must be a nonzero uint64`.

```python
import asyncio
from baml_core import BamlCallContext
from baml_sdk import throws_test

ctx = BamlCallContext()
task = asyncio.create_task(throws_test.SleepMs_async(2000, _ctx=ctx))
await asyncio.sleep(0.05)
ctx.abort()  # the awaiting task raises asyncio.CancelledError
```

Authors: sxlijin

## New `baml.yaml` namespace with `parse` and `deserialize`

0.11.3-nightly.20260610.b · nightly · 2026-06-10

A new `baml.yaml` standard-library namespace parses YAML into BAML values. It projects YAML onto the existing `baml.json.json` algebra, so the values you get back are the same null, bool, int, float, string, sequence, and string-keyed mapping shapes that `baml.json.parse` produces.

- **`baml.yaml.parse(s: string) -> baml.json.json`** parses a single YAML document and throws `baml.yaml.YamlParseError` on anything that cannot map cleanly into JSON-compatible values. Rejected inputs include multi-document streams, custom tags, non-string mapping keys, duplicate keys, non-finite floats (`.inf`, `.nan`), and integers outside the BAML `int` range. Anchors and aliases are resolved. Empty input parses to `null`.
- **`baml.yaml.deserialize<T>(s: string) -> T`** parses YAML and decodes it to `T`, honoring user-defined `from_json` overrides via `baml.json.from_json<T>`. It is the YAML counterpart of `baml.json.deserialize<T>` and throws `baml.yaml.YamlParseError` or `baml.json.JsonDecodeError`.
- The crate now depends on `yaml_serde` 0.10, a maintained fork of the archived `serde_yaml`, imported under the same `serde_yaml` path.

```baml
baml.json.stringify(baml.yaml.parse("user:\n  name: Ada\n  age: 30\nok: true\n"))
```

Authors: rossirpaulo

## Add `baml.http.Server` for serving HTTP and HTTPS requests

0.11.3-nightly.20260609.e · nightly · 2026-06-09

BAML can now run an HTTP/HTTPS server: bind a listener with `baml.http.Server.bind` and start handling requests with `server.serve`. The new `ns_http/server.baml` builtin, the native hyper-backed implementation, and an end-to-end test suite all ship in this release.

- **`baml.http.Server.bind(addr)`** opens a TCP listener and returns a `Server`. Passing `":0"` (e.g. `"127.0.0.1:0"`) asks the OS for an ephemeral port, and the resolved address is readable from `server.addr`.
- **`server.serve(handler, ...)`** dispatches each request to your `handler` closure on its own BAML thread, so requests are handled concurrently (including multiplexed HTTP/2 streams). A handler that panics fails only that request (the client gets a `500`) and the server keeps serving. Optional arguments cover `tls_config`, `allow_http1`/`allow_http2`, `max_body_size` (oversized bodies are rejected with `413`, default 100 MiB), `max_connections` (default 1024), and `header_read_timeout` (default 30s Slowloris defense).
- **`baml.http.Response.new(status_code, headers, body)`** builds the response a handler returns. `Content-Length` is set for you, and framing headers like a stale `Content-Length` or `Connection` are stripped.
- **`baml.http.TlsConfig`** turns a server into HTTPS from a PEM certificate chain and private key. TLS versions below 1.2 are not offered.
- **`Request`** now also models an incoming server request: `url` is the request-target as received, `body` is the bytes decoded lossily as UTF-8, and a header sent multiple times has its values joined with `, `.

The server primitives are native-only. On wasm and in the playground proxy they raise an unsupported-platform error. One related behavior change: consuming an already-consumed response body (`text()`/`bytes()` called twice) now throws `baml.errors.Io` instead of `baml.errors.InvalidArgument`, so it stays catchable in-contract.

```baml
function start_echo_server() -> never throws baml.errors.Io {
    let server = baml.http.Server.bind("127.0.0.1:0");
    server.serve((req: baml.http.Request) -> baml.http.Response {
        baml.http.Response.new(200, { "x-echo-method": req.method }, req.body.to_utf8())
    })
}
```

Authors: 2kai2kai2

## Internal compiler refactor unifying the shared type representation

0.11.3-nightly.20260609.d · nightly · 2026-06-09

This nightly is a single internal compiler refactor (#3722) that unifies the type representation shared across compiler stages. There is no change to BAML syntax, the CLI, or generated output, and no action is required.

Authors: 2kai2kai2

## `baml describe --budget` now applies to method, dependency, and reference sections

0.11.3-nightly.20260609.c · nightly · 2026-06-09

The `--budget` line limit on `baml describe` now extends past the body into the `methods`, `static_methods`, `dependencies`, and `references` sections. Previously those method sections bypassed the budget and always rendered in full.

- **Soft budget across sections.** Whatever the body doesn't consume flows into the list sections in priority order. Section headers are always emitted so a symbol's surface stays discoverable, the `references` header still shows the total count, and entries are never split mid-unit (a method's docstring and signature stay together).
- **Explicit elision marker.** When a section runs out of budget, the elided lines are replaced with `  … <n> more lines (re-run with a higher --budget)` instead of being dropped silently.
- **Fields-only bodies still fit.** A class body with no docstring continues to render in full at `--budget 5`; only the later method and reference sections give way under a tight budget.

```bash
baml describe String --budget 5
```

Under a tight budget this now elides later methods with a marker rather than printing all 40-plus in full.

Authors: aaronvg

## The formatter now indents catch clause bodies inside their enclosing block

0.11.3-nightly.20260609.b · nightly · 2026-06-09

The BAML formatter now understands `catch` expressions and indents their clause bodies relative to the enclosing block instead of leaving them flush.

- **`catch` formatting** A `<expr> catch (binding) { ... }` clause now has its arms indented one level deeper than before, and formatting is idempotent on the result.
- **New keywords recognized** The formatter's keyword set now includes `throw`, `catch`, and `catch_all`.

```baml
function demo(s: string) -> int {
    baml.json.from_string<int>(s) catch (e) {
        baml.json.JsonParseError => 0,
        baml.json.JsonDecodeError => 0,
    };
    42
}
```

Authors: ATX24

## Array sort, sort_by, and sort_by_key

0.11.3-nightly.20260609.a · nightly · 2026-06-09

Arrays now have `sort`, `sort_by`, and `sort_by_key` methods that sort in place and return `self`.

- **`sort`** orders an array in place by natural ascending order. It supports ints, floats, bigints, strings, and int/float or int/bigint mixes. Nulls, NaN, mixing strings with numbers, mixing floats with bigints, and non-orderable values (such as class instances) throw `InvalidArgument`. A failed sort leaves the array unchanged.
- **`sort_by`** takes a comparator following the JavaScript convention: negative sorts the first argument before the second, zero preserves relative order, positive sorts it after. The comparator may throw, and the array is left unchanged if it does.
- **`sort_by_key`** computes a key once per element, left to right, and sorts by the natural ascending order of those keys. Sorts are stable, so equal keys keep their original relative order.

```baml
let xs = [3, 1, 2];
xs.sort();                                  // [1, 2, 3]
xs.sort_by((a: int, b: int) -> int { b - a });   // [3, 2, 1]

let items = [
  ArraySortItem { key: 3, label: "c", nullable: 3 },
  ArraySortItem { key: 1, label: "a", nullable: 1 },
];
items.sort_by_key((x: ArraySortItem) -> int { x.key });
```

This release also drops a batch of unused crate dependencies (cargo-shear) and adds a `parking_lot` deadlock watchdog thread to the LSP server that logs blocked thread backtraces if a deadlock is detected. Neither changes observable behavior of BAML programs.

Authors: sxlijin

## for loops now iterate over any Iterable, plus new fluent Array iterator methods

0.11.3-nightly.20260608.a · nightly · 2026-06-08

`for (let x in ...)` now works over anything implementing `baml.iter.Iterable`, not just `T[]`. The element type is derived through `Iterable.Item`, so you can loop over `baml.iter.Range`, `baml.iter.ArrayIterator`, and your own iterables, and the loop correctly picks up the iterable's `Error` type in its throws inference.

Highlights:

- **New Array iterator methods.** `Array<T>` gains `for_each`, `filter`, `filter_map`, `step_by`, `chain`, `peekable`, `collect`, and `count`. These return `baml.iter.Iterator` adapters so they compose fluently:

```baml
[1, 2, 3, 4].filter((x: int) -> bool { x % 2 == 0 }).step_by(2).collect()
```

- **`chain` across mixed error types.** `Iterator.chain` now accepts any `Iterable<Item = Item, Error = E2>` rather than requiring a matching `Error`, and the result type is `Iterator<Item = Item, Error = Error | E2>`. The underlying `Chain` class is now parameterized as `Chain<T, E, E2>`.
- **`Iterator.for_each`.** Added `for_each` on `Iterator` that drains the iterator and runs `fn` on each item, throwing `Error | E2`.
- **Shadowed parameter dispatch fix.** When a parameter typed as `baml.iter.Iterable` is shadowed by a local binding of a concrete class, method calls like `source.iter()` now dispatch to the local class method instead of going through interface dispatch.

Authors: aaronvg

## spawn options with `with` middleware, and concurrent future combinators

0.11.3-nightly.20260608.b · nightly · 2026-06-08

A `spawn` can now be configured with a `with baml.spawn.options(...)` clause, and the `baml.future` namespace gains combinators over arrays of futures: `all`, `all_complete`, `race`, and `any`. Together these give you a way to control how spawned work runs, when it gets cancelled, and how its results combine, without writing that bookkeeping by hand.

Before this release, every `spawn` ran with the same fixed behavior: there was no way to cap concurrency, cancel work you no longer needed, or wait on a group of futures and react to the first one to finish. You had to track futures in arrays and reimplement the await-and-combine logic yourself. The `with` clause and the future combinators move that into the standard library so the common patterns (rate limiting, cancellation, "first success wins") are one call.

- **`spawn ... with`**: `spawn` accepts an optional `with` clause carrying middleware transformers. In v1 the only accepted form is a single call to `baml.spawn.options(...)`, which sets the spawn's `group`, `cancel`, and `detach`. A `with` expression that is not a transformer `(SpawnParams<T, E>) -> SpawnParams<U, F>` is rejected at type-check time.
- **`CancelToken`**: a cooperative, one-shot cancellation handle. Passing one via `options(cancel = ...)` links it into the task's effective token; once fired, the task's next `await` throws `baml.panics.Cancelled`. `CancelToken.any(tokens)` composes several tokens into one. Use this to stop work you started but no longer need, for example abandoning slower requests once you have an answer.
- **`TaskGroup`**: caps how many spawns referencing it run concurrently. Excess spawns queue FIFO and start as earlier ones settle, so you can fan out over a large input without overwhelming a downstream service. The `spawn` still returns its `Future` immediately, so queueing is invisible. `set_limit`, `cancel`, `active_count`, and `queued_count` manage the group.
- **Future combinators**: `all` awaits every future and cancels the rest on the first failure (like JS `Promise.all`), while `all_complete` lets the losers keep running when they have side effects that must finish. `race` settles with the first future to settle, success or failure. `any` settles with the first success and throws `AllFailed<E>` (carrying every error in input order) if all of them fail.
- **`await` over multiple futures**: a new `AwaitAny` suspend point backs `race` and `any`. `await f catch (e) { ... }` now parses as `(await f) catch (e) { ... }`, so the handler catches the error that `await` re-throws.

Rate-limit a fan-out with a `TaskGroup` and wait for all of it:

```baml
function fetch_all(urls: string[]) -> baml.future.Future<string[], MyError> {
  let group = baml.spawn.TaskGroup.new(limit: 4);
  let futures = urls.map((u) -> {
    spawn with baml.spawn.options(group: group) { fetch(u) }
  });
  baml.future.all(futures)
}
```

Take the first success and drop the rest with `any`, falling back to `AllFailed` when every source fails:

```baml
function first_mirror(urls: string[]) -> baml.future.Future<string, baml.future.AllFailed<MyError>> {
  let futures = urls.map((u) -> { spawn { fetch(u) } });
  baml.future.any(futures)
}
```

Cancel pending work cooperatively with a `CancelToken`:

```baml
function with_cancel() -> baml.future.Future<string, MyError> {
  let token = baml.spawn.CancelToken.new();
  let f = spawn with baml.spawn.options(cancel: token) { slow_work() };
  token.cancel();
  f
}
```

Authors: antoniosarosi

## Windows CI builds now link with rust-lld

0.11.3-nightly.20260607.a · nightly · 2026-06-07

CI-only release: Windows Cargo builds now use `rust-lld` as the linker.

This is a build infrastructure change with no user-visible effect. A new `baml_language/.cargo/config.toml` sets `linker = "rust-lld"` for the `x86_64-pc-windows-msvc` target, and the size-gate workflow drops the Chocolatey LLVM install in favor of `clang` from mise for `llvm-strip`.

Authors: sxlijin

## Release plumbing for the Node.js SDK, no user-facing BAML changes

0.11.3-nightly.20260606.a · nightly · 2026-06-06

This nightly is entirely release-infrastructure work and ships no changes to the BAML language, compiler, or runtime behavior. It wires the Node.js SDK build and npm publish into the `release-baml-language.yml` release graph.

- **Node.js SDK build matrix.** A new `build2-nodejs-sdk.reusable.yaml` replaces `build-nodejs-sdk.reusable.yaml`, building per-platform `@boundaryml/baml-core-node-<platform>` sub-packages across the same 8 targets and uploading them as `nodejs-sdk-<target>` artifacts.
- **npm publishing.** A new `publish2-nodejs-sdk.yaml` job downloads those sub-packages, wires the umbrella package's `optionalDependencies`, and publishes via npm OIDC trusted publishing. It is gated to `canary` and only on non-dry-run releases.
- **Python release graph.** The PyPI build now uses `build2-python-sdk.reusable.yaml`, and the inline "Check existing PyPI release" step was removed in favor of `skip-existing` on the publish step.
- **Build script split.** In the `@boundaryml/baml-core-node` `package.json`, the native `.dts` copy moved out of `build:napi-debug`/`build:napi-release` into a separate `build:copy-native-dts` step, and the package now declares `repository`, `publishConfig`, and `napi.npmClient`.

If you only consume BAML, there is nothing to do here.

Authors: sxlijin

## Internal cleanup: remove the legacy builtins codegen crates

0.11.3-nightly.20260606.b · nightly · 2026-06-06

Internal-only release. The superseded `baml_builtins`, `baml_builtins_codegen`, and `baml_builtins_macros` crates were deleted from the workspace, along with the now-unused `clap-cargo` and `baml_lsp_types` workspace dependencies.

No user-visible behavior changes. The active builtins path remains `baml_builtins2` and `baml_builtins2_codegen`. The only other change is documentation: `TEST_INSTRUCTIONS.md` now points at the current crate names (`baml_compiler2_hir`, `baml_compiler2_tir`).

Authors: aaronvg

## `const` is now accepted as a binding introducer, treated like `let`

0.11.3-nightly.20260605.e · nightly · 2026-06-05

You can now write `const` wherever you write `let` to introduce a binding, and it currently behaves exactly like `let`. BAML does not enforce immutability yet, so `const` is accepted in let statements, `if`/`while let` heads, C-style and iterator `for` loops, and destructure and array patterns.

- **Warning on every `const` introducer.** Each `const` binding emits diagnostic `E0010`: "`const` is currently treated like `let`; BAML does not enforce immutability yet. Use `let` for current BAML semantics." Use `let` if you want stable semantics today.
- **`const` is reserved as a binding name.** Using `const` as the bound identifier (for example `let const = x` or `const const = 1`) is now an error, `E0010`: "`const` is reserved and cannot be used as a binding name." This applies in destructure patterns, match arms, and `for` heads as well.
- **`const` stays contextual.** It is only treated as a keyword in binding position when followed by a pattern-shaped token, so field access like `record.const` and a field named `const` still parse as identifiers.
- **Formatter support.** `const` bindings round-trip through the formatter, including trailing trivia such as `const /*keep*/ x = 1`.

```baml
function WithConst() -> int {
    const sum = 0;
    for (const i = 0; i < 4; i += 1) {
        sum += i;
    }
    sum
}
```

Authors: rossirpaulo

## Host callable errors are now catchable, typed throws, and round-trippable

0.11.3-nightly.20260605.d · nightly · 2026-06-05

Errors raised by host callables now flow through BAML's normal exception machinery, and `call_host_value` carries a declared error contract `E` in addition to its return type `T`.

Highlights:

- **`call_host_value<T, E>` now has a throws contract.** The signature in `baml.host` changed from `call_host_value<T>(handle, args) -> T throws root.errors.HostCallable` to `call_host_value<T, E>(handle, args) -> T throws E`. The completion site validates the host's returned value against `T` and its thrown value against `E`. A return that doesn't inhabit `T`, or a throw that isn't a subtype of `E`, becomes a `baml.panics.HostContractViolation`. A host value left untyped at the boundary erases `E` to `unknown`, which accepts any thrown value.

- **New `baml.panics.HostContractViolation`.** Added to the `Panic` union, with fields `message`, `class_name?`, and `language?`. It is catchable like any other panic and fires when a host callable violates its typed contract.

- **`baml.errors.HostCallable` reworked.** The `category int` field is replaced by an opaque `_handle` (`$rust_type`) that references the originating host exception, so a same-process round-trip recovers `raised === caught` identity. The previous `baml.errors.HostPanic` class is removed.

- **Sysop errors now surface as catchable throws.** Uncaught I/O failures from `baml.fs` and `baml.http` now unwind as a `baml.errors.*` instance through `UnhandledThrow` rather than `ExternalOpFailed`. `EngineError::ExternalOpFailed` was removed.

- **New `ParseError` error category.** `baml.fs.read`, `File.text`, `File.read`, and `baml.env.get` now declare `throws ... | root.errors.ParseError` for non-UTF-8 input. The `File.*` methods additionally declare `InvalidArgument` (e.g. operating on a closed handle, or a negative `offset` with `whence="start"`), and `baml.fs.open` now declares `InvalidArgument`.

```baml
function call_host_value<T, E>(handle: HostValue, args: unknown[])
    -> T throws E {
  $rust_io_function
}
```

CI: native Windows Rust builds moved to `blacksmith-8vcpu-windows-2025` with `CARGO_BUILD_JOBS=16`, the Linux cargo-test job dropped to 8 vCPU, and `cargo-nextest` is now installed via mise instead of `taiki-e/install-action`.

Authors: sxlijin, 2kai2kai2

## Host-callable throws contracts and a typed `HostCallable` error shape

0.11.3-nightly.20260605.c · nightly · 2026-06-05

Host callables now carry a declared error contract: `call_host_value<T, E>` packs the throws type `E` alongside the return type `T`, and a host throw is checked against `E` at the completion site.

- **Typed throws on host callables.** `call_host_value` is now generic over both `T` (return) and `E` (throws). A host value left untyped at the boundary erases `E` to `unknown`, which accepts any thrown value. A throw that is not a subtype of `E`, or a return that does not inhabit `T`, becomes a catchable `baml.panics.HostContractViolation` panic.
- **New `HostContractViolation` panic.** A `HostContractViolation` class is added in `ns_panics/panics.baml` and joins the `Panic` union. It carries `message` plus optional `class_name` and `language`, and surfaces in BAML as `baml.panics.HostContractViolation`. It is catchable like any other panic.
- **Reworked `HostCallable` error.** `baml.errors.HostCallable` drops the `category` field and gains a required `_handle` (`$rust_type`) slot that references the originating host exception, enabling same-host round-trip where the recovered exception is identity-equal to the one caught. The old `HostPanic` error class is removed.
- **New `ParseError` throws on filesystem and env builtins.** `baml.fs.File.text`, `read`, `baml.fs.read`, and `baml.env.get` now declare `ParseError` for non-UTF-8 input, and the `File.*` methods plus `baml.fs.open` declare `InvalidArgument` (for example, `File.seek_from` throws `InvalidArgument` when `offset` is negative and `whence` is `"start"`). `ParseError` is now a distinct `SysOpErrorCategory`, separate from `InvalidArgument`.
- **Sysop errors now unwind as BAML throws.** Uncaught sysop failures surface as `EngineError::UnhandledThrow` carrying a `baml.errors.*` instance (for example `baml.errors.Io` or `baml.errors.InvalidArgument`) instead of the removed `ExternalOpFailed`. The `OpErrorKind` enum is gone, replaced by `VmBamlError` / `VmRustFnError`, and the VM error and panic types now live in `bex_vm_types::errors` so sysop impls can construct them without depending on the VM crate.

```baml
function call_host_value<T, E>(handle: HostValue, args: unknown[])
    -> T throws E {
  $rust_io_function
}
```

CI: native Windows Rust builds move to `blacksmith-8vcpu-windows-2025` with `CARGO_BUILD_JOBS=16`, the Linux `cargo test` job drops to an 8-vCPU runner, and `cargo-nextest` is now installed via mise instead of `taiki-e/install-action`.

Authors: sxlijin, 2kai2kai2

## Optional types now lower to T | null, changing how nullable types are displayed

0.11.3-nightly.20260605.b · nightly · 2026-06-05

The `Ty::Optional` variant is gone, and `T?` now lowers directly to the union `T | null`. The change is mostly internal, but it is visible wherever a nullable type is rendered: diagnostics, hovers, and `type_of` reflection now print `T | null` instead of `T?`.

- **Display change.** A nullable type renders as `T | null` rather than `T?`. For example, a type mismatch that used to read `expected string, got string?` now reads `expected string, got string | null`, and `reflect.type_of` on an optional now returns `"User | null"` instead of `"User?"`. The non-null member comes first, so `?` lowers to `T | null` (not `null | T`), which also reorders streamed/partial fields.

```baml
function search(query: string, max_results?: int, filter?: string?) -> int
// hover now shows:
// function search(query: string, max_results?: int, filter?: string | null) -> int
```

- **Nullable callbacks are parenthesized.** A function member in a nullable union renders as `((x: int) -> int) | null` so it is not misread as a function whose `throws` clause is `... | null`.
- **Narrowing fix for nested optional union members.** Subtracting a matched pattern type now flattens nested unions on both sides, so the else branch of `v is string?` on a value typed `int | string?` correctly narrows the residual to `int`.
- **Optional member access on multi-member unions.** `(NhCat | NhDog)?.field` now strips `null` from the receiver and routes through the union rather than assuming a single class.

This is a structural representation change. Existing `T?` source syntax is unchanged and continues to work.

Authors: codeshaunted

## Handle and media lifecycle now go through a shared C ABI with explicit status codes

0.11.3-nightly.20260605.a · nightly · 2026-06-05

The handle and media bridge surface is now backed by a single C ABI in `bridge_cffi`, with every entry point returning a `BamlCffiStatus` instead of silently failing. This is internal SDK plumbing for handle-typed values (images, audio, PDFs, video, function refs) and does not change BAML language syntax.

- **Status codes replace silent failures.** Handle operations now report `Ok`, `InvalidHandle`, `TypeMismatch`, `UnsupportedHandleType`, `InternalError`, or `UnexpectedNullptr`. The old `clone_handle`/`release_handle` C symbols are replaced by `baml_handle_clone` and `baml_handle_release`, which take an out-parameter and return a status.
- **Media constructors moved into the C ABI.** `baml_media_from_url`, `baml_media_from_file`, and `baml_media_from_base64`, plus accessors `baml_media_url`, `baml_media_base64`, `baml_media_file`, and `baml_media_mime_type`, are now the single implementation shared by the Python, Node.js, and Go bridges.
- **Errors surface to users on invalid input.** Cloning or releasing an invalid handle now raises rather than returning a zero key. In Python, `copy.copy` of a dropped handle raises `RuntimeError` matching `invalid handle`; in Node.js, `clone()` on an unknown key throws matching `invalid handle`.
- **API surface trimmed.** The `take_pyhandle_from_table` / `put_pyhandle_into_table` Python functions and the `takeHandleFromTable` / `putHandleIntoTable` Node.js exports are removed. Decoding now constructs a `BamlPyHandle(key, handle_type)` (or `new BamlHandle(key, ht)`) directly, and inbound wire encoding uses `_clone_key_for_wire`.
- **Go handle API now returns errors.** `BamlHandle.Clone()` and `BamlHandle.Release()` in the Go SDK return an `error` instead of nothing, and `CloneHandle` returns `(uint64, error)`.

```python
from baml_core.baml_py import BamlPyHandle

# Construct a handle wrapper directly from a wire (key, handle_type) pair.
handle = BamlPyHandle(key, handle_type)
# Clone for inbound wire ownership.
new_key, ht = handle._clone_key_for_wire()
```

Authors: sxlijin

## baml run --list shows real generic signatures, and concrete associated-type projections no longer erase to void

0.11.3-nightly.20260605.g · nightly · 2026-06-05

Functions whose return type is an associated-type projection like `(AccountRecord as PublicIdentity).Key` now resolve to their concrete type at the VM boundary instead of being erased, so a value-returning function no longer silently prints nothing through `baml run`.

- **`baml run --list` keeps generic signatures.** Compiled function metadata now carries separate display fields, so listed signatures show type parameters and unresolved projections instead of `unknown` or `void`. A generic function lists as `read_item<T extends BoxLike>(box: T) -> T.Item` rather than `read_item(box: unknown) -> unknown`, and `get_public_key(account: AccountRecord) -> string` shows the resolved projection. The JSON output gains a `generic_params` field per function.

```bash
baml run --list --from . --features beta
```

- **Project discovery accepts `baml_src/` without a manifest.** `baml describe` and other introspection commands now treat a directory containing `baml_src/` as a real project, matching how `run` and `pack` already discover projects. Running these commands from a subdirectory walks up to the enclosing `baml_src/` project even when there is no `baml.toml`.

- **New `baml.iter` standard library.** A `ns_iter/iter.baml` module ships `Iterable` and `Iterator` interfaces with default methods including `map`, `filter`, `filter_map`, `flat_map`, `collect`, `reduce`, `count`, `find`, `step_by`, and `chain`, plus `Range`, `Repeat`, and `ArrayIterator` types.

- **Generic binding inference over unions and interfaces.** Type variable inference now descends through nullable unions (`T?`), same-length unions, and interface associated-type bindings, and union construction drops `never` members and flattens nested unions. The `WrongNumberOfTypeArgs` diagnostic now reads `type` instead of `class` since it also covers interfaces.

- **`baml describe` and keyword docs updated for interfaces.** New describe topics cover `interface`, `implements`, `method`, `field`, `requires`, `associated`, `type`, `generics`, `extends`, `as`, `projection`, and `Self`. The `new` keyword hint now states that BAML constructs values with object literals like `User { name: "Ada" }` rather than `ClassName.new()`.

Authors: aaronvg

## New baml.time date/time family and a baml.toml parser

0.11.3-nightly.20260605.f · nightly · 2026-06-05

Adds the `baml.time` date/time family from BEP-021 and a `baml.toml` standard-library namespace built on top of it.

- **`baml.time` types.** New classes `ZonedDateTime`, `PlainDateTime`, `PlainDate`, `PlainTime`, and `TimeZoneOffset`, following TC39 Temporal semantics. `Plain*` types are timezone-free wall-clock values, while `ZonedDateTime` carries either a fixed offset or an IANA identifier. `PlainDateTime.to_zoned` and `ZonedDateTime.from_components` resolve DST gaps and overlaps per a `Disambiguation` argument (`"compatible"`, `"earlier"`, `"later"`, `"reject"`), with `UnknownTimezoneError` and `AmbiguousTimeError` on failure.
- **Timezone resolution.** `system_timezone`, plus IANA offset and instant resolution, go through the host timezone database via `jiff` on native targets and through the JavaScript `Temporal` API on wasm. Platforms without a backing implementation return `Unsupported`.
- **`baml.toml`.** New `baml.toml.Table` with native `parse`, plus `to_json` / `from_json` and the `item_to_json` / `item_from_json` helpers. TOML's four datetime kinds map onto the `baml.time` types: offset datetime to `ZonedDateTime`, local datetime to `PlainDateTime`, local date to `PlainDate`, local time to `PlainTime`. Parse failures throw `TomlParseError`, and `from_json` skips JSON `null` values.

```baml
let dt = baml.time.PlainDateTime.parse("1979-05-27T07:32:00");
let z = dt.to_zoned("America/Los_Angeles", disambiguation = "earlier");

let t = baml.toml.Table.parse("dt = 1979-05-27T07:32:00Z");
baml.json.stringify(t.to_json())
```

The rest of the diff is non-behavioral cleanup (clippy-style simplifications in the HIR builder, MIR lowering, and the jsonish fixing parser).

Authors: antoniosarosi

## First 0.11.1 canary build with no changes from the prior canary

0.11.1 · canary · 2026-06-04

This canary build of the BAML toolchain has no commits or file changes relative to its predecessor on the canary channel. There is nothing user-visible to report.

## Class generic parameters can now declare `extends` bounds

0.11.1-nightly.20260604.a · nightly · 2026-06-04

Class generic parameters now accept `extends` bounds, so `class Box<T extends Named>` constrains `T` to types that implement the `Named` interface.

- **Bounded class type parameters.** Declare a bound with `extends` on a class type parameter. Inside the class, members from the bound interface are accessible (for example `self.value.name` when `T extends Named`), including through `requires` chains and inside lambda bodies.
- **Bound enforcement.** Passing a type that does not satisfy the bound is now a compile error, both for explicit type arguments (`Box<int>`) and for inferred ones (`Box { value: 1 }`).
- **Associated-type bindings keep outer type variables.** Generic bounds of the form `S extends Source<Item = T>` now preserve the outer `T`, including for nested shapes like `Item = T?`, `Item = T[]`, `Item = map<string, T>`, and `Item = (x: T) -> T`. Runtime dispatch substitutes a class type variable into an associated-type binding correctly.

```baml
interface Named {
    name: string
}

class Dog {
    name: string
    implements Named {}
}

class Box<T extends Named> {
    value: T

    function label(self) -> string {
        return self.value.name
    }
}
```

Authors: aaronvg

## Reference generic functions as values with explicit type arguments

0.11.1-nightly.20260604.b · nightly · 2026-06-04

You can now write a generic instantiation expression like `let f = identity<int>`, referencing a generic callable with explicit type arguments without calling it. The result is a concrete, specialized function value.

- **Specialized value type.** `identity<int>` binds `T = int` and produces the concrete type `(x: int) -> int`. A later call is checked against the substituted parameter types, so `let f = identity<int>; f("string")` is now a type error instead of silently re-inferring `T = string`. A bare `let f = identity` (no type args) still stays fully generic.
- **Runtime identity.** Concrete instantiations are pooled and interned, so two references to `identity<int>` share one object and compare equal. Calling the value seeds `frame.type_args`, so type-reifying bodies such as `reflect.type_of<T>()` and `baml.json.from_string<T>` resolve `T` correctly through an uncalled value.
- **Param-dependent and local-value bases.** `identity<T>` inside a generic function (resolved against the enclosing frame) and `g<int>` where `g` is a local holding a generic function are both specialized at runtime instead of erasing the type arguments.
- **Bound enforcement.** BEP-044 generic bounds are checked on instantiation values too. `let f = first_name<int>` where `first_name<T extends Named>` reports a type error.
- **Parsing.** The `<...>` after a generic callable is now disambiguated from a `<` comparison using a follow-token rule ported from TypeScript, so value-position forms like `let f = foo<int>` parse correctly.

```baml
function identity<T>(x: T) -> T { x }

function caller() -> int {
    let f = identity<int>;
    f(5)
}
```

Authors: codeshaunted

## baml.toml is now opt-in; a baml_src/ directory alone is a valid project

0.11.1-nightly.20260604.c · nightly · 2026-06-04

`baml.toml` is no longer required to run, pack, format, or introspect a project. A directory now counts as a BAML project if it has *either* a `baml.toml` *or* a `baml_src/` directory, and the manifest is only needed when you actually use one of its features (dependencies, version locks, `[scripts]`, multiple packages).

- **Manifest-less projects.** `baml run`, `baml test`, `baml generate`, and `baml pack` now work on a `baml_src/`-only project. Sources are loaded from `baml_src/`, which scopes discovery and avoids the workspace-slurp hang the old "`baml.toml` required" rule guarded against. `baml run -e` also picks up the surrounding project's definitions when either marker is present.
- **`baml describe` / `baml grep` never fail on a missing manifest.** These read-only commands fall back to a stdlib-only default state, so `baml describe baml.String` resolves anywhere, even in a directory with no project at all. They also walk up to the nearest ancestor `baml.toml`, so introspection from a subdirectory resolves the enclosing project, and a malformed manifest no longer blocks introspection.
- **`baml fmt` with no project is a no-op success.** Running `baml fmt` in a directory with neither a `baml.toml` nor a `baml_src/` now finishes cleanly instead of erroring with "doesn't look like a BAML project". Pass explicit file paths to format loose `.baml` files.
- **`baml pack` output naming.** For a manifest-less `baml_src/` project, the default output name falls back to the project directory name. In `--file` mode, packing stays hermetic: the name comes from the file stem, and a path with no usable file name (for example `..`) now errors with a hint to pass `-o` instead of consulting the project manifest.

```bash
# No baml.toml needed for any of these:
baml describe baml.String        # resolves against the stdlib anywhere
baml run --list                  # works on a baml_src/-only project
baml pack main -o ./out          # packs a manifest-less project
```

Authors: codeshaunted

## New CLI commands for project validation, agent skill installs, and toolchain freshness checks

0.11.2 · canary · 2026-06-04

This canary adds three CLI commands and cleans up the help that the wrapper presents.

- **`baml check`**: load a project (`--from`, default `.`), render compiler diagnostics, and run a bytecode compile pass. It exits non-zero (code 4) when there are compiler errors, so it drops into CI as a fast validation step. Plain `baml check` is sugar for `baml check --from .`.
- **`baml agent install [--dir <path>]`**: fetch the latest official BAML agent skills from `BoundaryML/baml-skill` and refresh the managed `.agents/skills/baml-*` and `.claude/skills/baml-*` folders. Without `--dir` it installs at the nearest ancestor with a `baml.toml`, then the git root, then the current directory. Restart any running Claude Code, Codex, or OpenCode session afterward.
- **`baml toolchain status`**: a read-only remote freshness check for the active selector. It reports the active version, the latest remote version, and whether you are up to date, without installing a toolchain or changing your selection. `baml toolchain list` stays local-only and now points here for remote checks.
- **CLI help and `--version`**: generated help now shows `Usage: baml ...` instead of the internal `baml-cli` binary name across the root, `pack`, `ide`, and `run` commands. `baml --version` reports the wrapper version plus the locally resolved toolchain state without a network call, and `baml self-update` rejects unexpected arguments.

```bash
baml check --from .
baml agent install --dir ./packages/my-baml-project
baml toolchain status
```

Authors: rossirpaulo

## CLI help now shows the public baml command name instead of baml-cli

0.11.2-nightly.20260604.a · nightly · 2026-06-04

`baml --help` and its subcommand help screens now print `baml` as the command name instead of the internal `baml-cli` binary name.

- **Usage strings**: Root, `pack`, `ide`, and `run` help now render as `Usage: baml ...` rather than `Usage: baml-cli ...`, matching the command you actually type.

```bash
baml --help
# Usage: baml [OPTIONS] <COMMAND>
```

The canary language version was also bumped to `0.11.1` across the version stamp and SDK package manifests.

Authors: rossirpaulo

## New baml toolchain status command for read-only remote freshness checks

0.11.2-nightly.20260604.b · nightly · 2026-06-04

Adds `baml toolchain status`, a read-only command that checks the active channel against the latest remote metadata without installing a toolchain or changing your selection.

- **`baml toolchain status`**: checks the remote channel pointer and reports whether your active channel is at the latest remote head. It may refresh the manifest cache but never installs a toolchain or mutates `[default].selector` or channel state. Use this for freshness checks now that `baml toolchain list` is strictly local-only and never hits the network.
- **Help output**: `baml toolchain` with no subcommand, `--help`, or `-h` now prints a help screen describing each subcommand and its network behavior, and unknown subcommands print that help instead of a one-line usage string. `baml self-update --help` prints its own help, and `baml self-update` now rejects extra arguments.
- **`baml --version`**: now prints `baml wrapper <version>` along with the resolved toolchain state, for example `baml toolchain <version>`, `baml toolchain not installed`, or `baml toolchain corrupt` with the command to fix it.

```bash
baml toolchain status
```

Authors: rossirpaulo

## New baml agent install command for project-local agent skills

0.11.2-nightly.20260604.c · nightly · 2026-06-04

Adds a `baml agent install` command that fetches the latest official BAML agent skills and writes them into your project.

- **`baml agent install`**: Fetches the latest skill content from `BoundaryML/baml-skill` and refreshes the managed skill folders under `.agents/skills/baml-*` and `.claude/skills/baml-*`. Skills include `baml-core`, `baml-llm-functions`, `baml-pipelines`, `baml-testing`, and `baml-bridges`, used by Codex, OpenCode, and Claude Code.
- **`--dir <path>`**: Installs into an explicit directory. Without it, BAML installs at the nearest ancestor containing `baml.toml`, then the git root, then the current directory.
- The skill content is not tied to a BAML language release. Each run pulls the latest default-branch content, so restart any running Claude Code, Codex, or OpenCode session afterward.

```bash
baml agent install
baml agent install --dir ./packages/my-baml-project
```

Authors: rossirpaulo

## New baml check command and optional arguments in the Python and Node SDKs

0.11.3-nightly.20260604.a · nightly · 2026-06-04

Adds the `baml check` command and wires optional, defaulted function arguments through the generated Python and TypeScript SDKs.

- **`baml check`**: A new CLI subcommand that compiles your BAML project and reports compiler errors without generating code. It takes an optional `--from` flag (defaults to the current directory) and exits non-zero (code `4`) when compilation fails.

```bash
baml check --from .
```

- **Optional arguments in generated SDKs**: Functions and methods with defaulted parameters now split into required positional args and optional keyword args. In Python, optional arguments are keyword-only. In TypeScript, they are passed in a trailing `$opts` object. Passing an unknown option key or an extra positional argument raises at the callsite.

```python
optional_args_probe(1)              # [1, 5, 99]
optional_args_probe(1, opt1=None)   # [1, None, 99]
optional_args_probe(1, opt1=8, opt2=9)
```

```typescript
optional_args_probe(1);                 // [1, 5, 99]
optional_args_probe(1, { opt1: null }); // [1, null, 99]
optional_args_probe(1, { opt1: 8, opt2: 9 });
```

- **`baml.Unset` sentinel**: The Python `UNSET` sentinel is now backed by a singleton `Unset` type that is exported and appears in generated `.pyi` stubs. Generated signatures now type defaulted parameters as `typing.Union[T, baml.Unset]`. Passing `baml.UNSET` for an optional argument is treated as omitted, which stays distinct from passing `None`. The `Unset` class is exported from `baml_core`.

This release also bumps the canary product version to `0.11.2` and moves engine CI jobs from blacksmith runners to `ubuntu-latest`.

Authors: rossirpaulo, sxlijin

## baml.net restructured around TcpStream, TcpListener, and a new UdpSocket

0.11.3-nightly.20260604.b · nightly · 2026-06-04

`baml.net` now mirrors Rust's `std::net`: the free functions `baml.net.connect` and `baml.net.listen` are gone, and you call constructors on named types instead. This is a breaking change to every `baml.net` call site.

- **TCP renamed and reshaped.** The `Socket` class is now `TcpStream`. Connect with `baml.net.TcpStream.connect(addr)` instead of `baml.net.connect(addr)`, and bind a listener with `baml.net.TcpListener.bind(addr)` instead of `baml.net.listen(addr)`. `TcpListener.accept()` returns a `TcpStream`.
- **Bytes instead of strings.** `TcpStream.read()` now returns `uint8array` (empty on EOF) and `write(data)` takes `uint8array`, replacing the previous `string` signatures. Concatenate reads with `data1.concat(data2)` rather than `+`, and convert text with `"...".to_utf8()`.
- **New `UdpSocket`.** Bind with `baml.net.UdpSocket.bind(addr)`, send a datagram with `send_to(data, addr)` (returns the number of bytes sent), and receive with `recv_from()`, which returns a new `Datagram` class carrying `data: uint8array` and `addr: string`.
- **Connect timeout.** `TcpStream.connect` now applies a fixed 10 second deadline, so the `throws root.errors.Timeout` clause actually fires against an unresponsive host.
- **Deterministic close.** Calling `close()` on a `TcpStream`, `TcpListener`, or `UdpSocket` invalidates the shared handle, so any later `read`, `accept`, `send_to`, or `recv_from` on the same value fails instead of hitting an already-released socket.

```baml
function main() -> uint8array {
    let sock = baml.net.UdpSocket.bind("0.0.0.0:0");
    sock.send_to("ping".to_utf8(), "127.0.0.1:9");
    let dgram = sock.recv_from();
    dgram.data
}
```

Authors: codeshaunted

## First nightly on this channel, no code changes from the predecessor

0.11.1-nightly.20260603.a · nightly · 2026-06-03

No user-visible changes. This is the initial nightly build on this channel, with no commits and no diff relative to its predecessor.

## Methods dispatched through an interface now receive their inferred generic type args

0.11.1-nightly.20260603.b · nightly · 2026-06-03

Inferred generic type args are now threaded correctly through interface dispatch, so a method body that reads its enclosing `T` at runtime resolves it instead of falling back to `unknown`.

Previously, when a generic class's `<T>` was inferred (for example from a static constructor like `ArrayCursor.of([10, 20, 30])`) rather than written explicitly, a method dispatched through an interface received empty `class_type_args`. A body that read `T` via `reflect.type_of<T>()` or constructed a new generic such as `Other<T>{}` resolved `T` to `unknown`, which could make the runtime `IsType` dispatch guard pick the wrong implementor. With siblings of differing field layouts this returned a silently wrong answer or panicked on field access.

- **Interface dispatch seeds the callee frame's `type_args` per candidate.** A class-owned method gets the implementor's class args, statically when the guard pins them and otherwise from the matched runtime instance via a `BoundMethod` (covering `Any` and partial guards). An inherited interface default gets the interface's args.
- **`default.<method>()` forwarding** seeds the frame the same way, so an explicit call into a default method that reads `T` resolves it correctly.
- **`enclosing_generic_params`** now includes interface params for default methods.

```baml
interface Cursor<T> {
  function head(self) -> T throws unknown
  function take_one(self) -> Cursor<T> throws unknown {
    return SingleCursor<T> { value: self.head() }
  }
}

let cursor: Cursor<int> = ArrayCursor.of([10, 20, 30]);  // <int> inferred
let single: Cursor<int> = cursor.take_one();             // T propagated
single.head()                                            // 10
```

Authors: hellovai

## Release CI builds and publishes baml_core wheels to PyPI

0.11.1-nightly.20260603.c · nightly · 2026-06-03

This release is internal to BAML's release tooling and does not change the BAML language, compiler, or runtime.

PyPI publishing for the `baml-core` package now lives inside `release-baml-language.yml`. The standalone `publish-python-pypi.yml` workflow was removed, and the release graph now builds wheels through `build-python-sdk.reusable.yaml` and uploads them from a top-level `publish-pypi` job (gated to the `canary` branch) so the upload runs under the workflow identity that PyPI trusted publishing is bound to.

The Homebrew formula generator in `scripts/baml-package-manager-artifacts` now installs the `baml` binary whether it lands at `bin/baml` or at the archive root.

Nothing here affects code you write against BAML.

Authors: rossirpaulo

## Interface default methods can call Self-typed methods on self

0.11.1-nightly.20260603.d · nightly · 2026-06-03

Inside an interface, `self` is now treated as a `Self` type variable bound by the interface, so a default method can call a `Self`-parameter method on `self` (`return !self.eq(other)`) and have it dispatch through the concrete implementor.

- **`Self`-pinned method calls.** When you call a `Self`-parameter method through a generic bound (`T extends Equatable`) or through `self` in a default method, `Self` is rigid: it is fixed to the receiver's type and never inferred from an argument. Passing an unrelated type for a `Self` parameter is now a type error, including nested positions like `Self[]`. Calling such a method on a bare interface ("dyn") value remains rejected by object safety.
- **`Self`-typed method values.** Referencing an interface method as a value on a generic- or interface-typed receiver (`let f = x.eq`) now binds the implementor's method by the receiver's runtime type, with `Self` still pinned.
- **Associated types in default bodies.** A default method that returns an associated type by delegating to a `self` method (`return self.next()` with return `Self.Item?`) now type-checks, including across `requires` inheritance and blanket out-of-body impls (`implements<T> Items for Box<T>`).
- **Generic-class `self`.** An unannotated `self` in a generic class is now typed `Wrap<T>` rather than bare `Wrap`, fixing a failure in the auto-derived `to_json` path.
- **New diagnostic `E0138` (`ImplTargetNotConcrete`).** An `implements ... for <target>` whose `for` target is not a single concrete type (a union, optional, interface, or `unknown`) is now reported.

```baml
interface Equatable {
    function eq(self, other: Self) -> bool
    function neq(self, other: Self) -> bool {
        return !self.eq(other)
    }
}
```

Authors: 2kai2kai2
