# 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.

## Ordering operators dispatch through `baml.ops.Compare`

0.15.1-nightly.20260731.a · nightly · 2026-08-01

`<`, `<=`, `>`, and `>=` on operands the comparison opcodes cannot order now lower to a virtual call on `baml.ops.Compare`, resolved from the receiver's concrete type. Previously `true < false` aborted with an uncatchable VM internal error; `bool` reaches this path through the stdlib's own `Compare` impl, and user classes or enums that implement `Compare` are now orderable too.

- **Ordering dispatch (`baml.ops.Compare`).** A type that implements `Compare` (which requires `Equals`) can be ordered directly with `<`/`<=`/`>`/`>=`. Only `lt` is required; `le`/`gt`/`ge` fall back to the interface defaults, and each operator dispatches its own method, so an override wins over the default it replaces. Ordering stays restricted to two operands of the same concrete type (or a `Compare`-bounded type variable); a union or interface-existential operand is still a compile error, since single dispatch relies on both operands having one runtime type.
- **Nested media in prompt interpolation (`prompt`).** Interpolating a value into a `prompt` tagged template now renders it with the same recursive `baml.ToString` handling as `string.from`, except `image`, `audio`, `video`, and `pdf` values nested inside classes, arrays, and maps stay structural instead of collapsing to text. A `to_string` override on a parent value still owns its whole subtree.
- **Equirecursive type aliases.** Recursive aliases are now compared as equirecursive types, so `type A = int | A[]` and `type B = int | B[]` are the same type at any unfolding depth. Impl dispatch keyed on a recursive alias used as a generic argument (for example `implements JsonTagged for JsonWrap<json>`) now resolves the impl the compiler selected instead of declining it.

```baml
class Ranked {
    rank int
    implements baml.ops.Equals {
        function eq(self, other: Self) -> bool throws never { self.rank == other.rank }
    }
    implements baml.ops.Compare {
        function lt(self, other: Self) -> bool throws never { self.rank < other.rank }
    }
}
```

Engine version bumped to 0.225.0. Legacy releases are now branded "BAML v0" in release names and the shipped README.

Authors: 2kai2kai2, sxlijin, codeshaunted, rossirpaulo

## Intersection bounds `T extends A & B` now enforce every interface

0.225.0 · engine · 2026-08-01

Generic parameters declared with an intersection bound, `T extends A & B`, now enforce every interface in the intersection. Previously only the first bound was surfaced, so the second and later interfaces were accepted at the declaration but never actually applied. All interfaces in the intersection are now enforced, and a bound member can be reached through any of them. Code that compiled before because an argument only satisfied the first bound may now fail to type-check, so verify your intersection-bounded generics still pass.

```baml
interface Named {
  name string
}

interface Sized {
  size int
}

function pack<U extends Named & Sized>(value: U) -> U {
  value
}
```

Also in this release:

- **Comparison operators dispatch through `baml.ops.Equals` and `baml.ops.Compare`.** Equality and ordering comparisons now emit a virtual call, so they resolve against a type variable's interface bound instead of requiring a concrete type.
- **Nested media survives prompt interpolation.** Interpolated values render through the same `string.from` conversion and `baml.ToString` overrides as an ordinary `${expr}`, but media nested inside a class, array, or map now stays structural instead of collapsing to text. The separate `render_prompt_values` pass is gone.
- **Equirecursive type aliases compare correctly.** `type A = int | A[]` and `type B = int | B[]` are the same type, partial unfoldings included, while a fully unguarded cycle like `type B = B` denotes `never` and is reported as `E0068`. See `TYPE_SYSTEM.md` for the full equivalence rules.

Authors: 2kai2kai2, sxlijin, codeshaunted, rossirpaulo

## First tracked release on the engine channel; no diff available

0.224.0 · engine · 2026-07-31

No predecessor release, commit log, or file diff was available for this version, so there are no changes to describe. This is the earliest tracked release on the engine channel.

## First tracked release on the engine channel with no diff available

0.224.0 · engine · 2026-07-31

No predecessor release, commit log, or file diff is available for this version, so there are no changes to report. This is either the oldest release recorded on the engine channel or the comparison lookup failed.

## Generic bounds written `T extends A & B` now enforce every interface in the intersection

0.15.1-nightly.20260730.e · nightly · 2026-07-30

A generic parameter declared `T extends A & B` now constrains `T` by both `A` and `B`. Previously the compiler surfaced only the first conjunct and silently dropped the rest, so `B` was never enforced.

**Highlights**

- **Intersection bounds are fully applied.** `<T extends A & B>` on functions, classes, interfaces, and interface method signatures now carries every `&`-separated interface, matching what `implements` blocks already did. An argument for `T` must satisfy all of them.
- **Ambiguous members must be qualified.** When a member is declared by more than one conjunct of the intersection, it is now reported as ambiguous instead of resolving to whichever bound came first. Qualify the access (for example `recv.as<A>.member`) to pick one.
- **Engine bumped to 0.224.0.** This pulls in several fixes: generated Python models are rebuilt so class ordering cannot break Pydantic, React streaming hook types are corrected for nullable fields, `baml test` no longer hangs on self-referential `.env` variables, and the OpenAI Responses API now preserves allowlisted content-block metadata including `prompt_cache_breakpoint`.

```baml
interface Named {
  name string
}

interface Sized {
  size int
}

function pack<U extends Named & Sized>(value: U) -> U {
  value
}
```

The rest of the change is an internal refactor: the parallel `generic_params` and `generic_param_bounds` fields were merged into a single `GenericParam { name, bounds }` across the AST, HIR, PPIR, and TIR layers. That merge has no observable effect on its own.

Authors: sxlijin, 2kai2kai2

## `baml feedback` switches to `--title`/`--description`, and `baml login` moves under `baml auth login`

0.15.1-nightly.20260730.d · nightly · 2026-07-30

`baml feedback` now takes `--title` and `--description` instead of `--issue`, `--repro`, and `--context`, and the old flags are gone. Sends are also zero-prompt: with no session, a report goes out anonymously under a locally generated PostHog distinct id, and after `baml auth login` it carries your verified email instead. The `--anonymous` flag is removed because it is no longer needed.

- **`baml login` is removed as a top-level command.** Use `baml auth login` (and `baml auth login --no-open` for headless environments). `baml auth whoami` and `baml auth logout` are unchanged.
- **Attachments.** `baml feedback --files <path>` is repeatable, so you can attach a screenshot and a repro in one report.
- **Local report store.** Every report is recorded in `<BAML_HOME>/feedback.json`. New `baml feedback list` (with `--status open`) and `baml feedback view <id>` read that store, and a report sent while offline is saved locally and synced on a later run instead of failing.
- **JSON on stdin** now uses the new field names: `echo '{"title": "...", "description": "..."}' | baml feedback -`.

```bash
baml feedback --title "Issue (parser): panics on nested unions" \
  --description "Minimum repro: class A { ... }" \
  --files repro.baml
```

Authors: ATX24

## Content-block metadata is preserved on `openai-responses` requests

0.15.1-nightly.20260730.c · nightly · 2026-07-30

Content-block metadata is now preserved on `openai-responses` requests, so `prompt_cache_breakpoint` markers stay attached when you call GPT-5.6.

- **`openai-responses` metadata (#4155).** Per-content-block metadata is no longer dropped on the responses API path, which is what let `prompt_cache_breakpoint` markers survive the request.
- **`baml pack` on Linux (#4283).** Packing now writes its embedded envelope through `libsui::Elf::append` again (libsui bumped from 0.14 to 0.16.4), and the hand-rolled ELF note writer (`pack_elf.rs`) plus its pinned `object` dependency are gone. The on-disk format is unchanged, so previously packed behavior is preserved. CI also now runs the `pack_e2e` suite under musl in addition to glibc.
- **Lambda lowering (internal, #4282).** Lambda bodies now lower into the enclosing function's expression arena rather than each getting their own, and a dedicated `LambdaDef`/`LambdaKind` replaces the old synthetic-`FunctionDef` representation. This is a structural refactor. It does not change how lambdas, `spawn` bodies, or `test`/`testset` blocks type-check or run.

Authors: conrad-scherb, sxlijin, 2kai2kai2, codeshaunted

## Internal size-gate tooling now reads and writes human-readable byte sizes

0.15.1-nightly.20260730.b · nightly · 2026-07-30

No user-facing changes. This nightly refreshes the internal `size-gate` CI baselines and reworks the tool that maintains them. The per-platform ceilings in `.cargo/size-gate.toml` and the recorded baselines in `.ci/size-gate/*.toml` now store values like `20.9 MiB` and `4.5 MiB` instead of raw integers such as `21_854_475`, backed by a new `bytesize`-based `human_size` module. This affects only BAML contributors working on the build-size gate. It does not change the compiler, runtime, CLI behavior, or any generated code.

Authors: sxlijin

## Field access on an interface-typed value dispatches to the implementing class at run time

0.15.1-nightly.20260730.a · nightly · 2026-07-30

A field access like `receiver.field` on an interface-typed value now reads and writes the implementing class's field at run time, the same way a virtual method call resolves its method.

- **Virtual field reads.** A field read on a receiver whose concrete type is only known dynamically lowers to a new open-world load backed by the `VirtualLoadField` opcode. It indexes the interface's declared field list, then maps that position to the class's own slot through the resolved implementation's `field_links` table. Two classes that implement the same interface can back that field with slots at different offsets, and each reads correctly.
- **Virtual field writes.** Writing through an interface field is handled symmetrically by the `VirtualStoreField` opcode, and is treated as a side-effecting mutation of the receiver so optimization passes do not drop or reorder it.
- **Field types are now always represented internally.** A class or interface field written without a type annotation, already flagged by the parser, now recovers as an error type instead of an absent one. This is a representation change threaded across the AST, HIR, PPIR, and TIR layers, with no new user-visible diagnostic.

One path is still incomplete: a field type that names `Self` or `Self.Assoc`, such as `key: Self.Key`, does not yet resolve when read through the interface field and falls back to error recovery. That case is already reported separately by the checker, so it does not surface as a silent wrong result.

Authors: 2kai2kai2

## Intersection bounds like `T extends Named & Sized` now keep every conjunct

0.15.1-nightly.20260730.e · nightly · 2026-07-30

Generic parameters declared with multiple `&`-separated bounds, such as `<T extends Named & Sized>`, now enforce all of them. Previously only the first bound was kept, so a second bound like `Sized` in `T extends Named & Sized` was silently ignored on functions, classes, interfaces, and interface method signatures.

- **Every bound is honored.** An argument for `T extends A & B` must now satisfy both `A` and `B`. This applies to functions, classes, interfaces, and interface method signatures. Code that relied on the old behavior (where the second bound was dropped) may now surface type errors it should have reported before.
- **Member access over a conjunction searches every bound.** Accessing a member on a value typed by `T extends A & B` now looks through both `A` and `B` in declaration order. If the same member is declared by two of the bounds, the access is ambiguous and must be qualified with `recv.as<I>.member`, the same way two incomparable interfaces are.
- **Engine bumped to 0.224.0**, which includes: Pydantic model generation no longer breaks on class declaration order (#793, #3819), React streaming hook data types are corrected for nullable fields (#3905), `baml test` no longer hangs on self-referential `.env` variables (#3927), and the OpenAI Responses API now preserves allowlisted content-block metadata including `prompt_cache_breakpoint` (#4155).

```baml
interface Named {
  name string
}

interface Sized {
  size int
}

function pack<U extends Named & Sized>(value: U) -> U {
  value
}
```

Authors: sxlijin, 2kai2kai2

## `baml feedback` sends without a prompt and gains `list`/`view` subcommands

0.15.1-nightly.20260730.d · nightly · 2026-07-30

The `baml feedback` command was reworked, and `baml login` moved under `baml auth`. If you script or use either command, your invocations change.

- **`baml login` is gone.** Authenticate with `baml auth login` (and `baml auth login --no-open` for headless environments). The old top-level `baml login` no longer exists.
- **New feedback flags.** `--issue` is now `--title`, `--repro` is now `--description`, and `--context` was dropped. Attach files with repeatable `--files`.
- **Zero-prompt sends.** The `--anonymous` flag was removed. With no session, reports go out anonymously under a locally generated PostHog id. After `baml auth login`, they carry your verified email. Either way there is no interactive prompt.
- **Manage past reports.** `baml feedback list --status open` and `baml feedback view <id>` read a local store at `<BAML_HOME>/feedback.json`. Reports start `open` and flip to `reported` once delivered, so a send made while offline is saved and synced on a later run instead of failing.

```bash
baml feedback --title "Issue (parser): panics on nested unions" --description "Minimum repro: class A { ... }" --files repro.baml
baml feedback list --status open
```

Authors: ATX24

## openai-responses now preserves content-block metadata such as prompt_cache_breakpoint

0.15.1-nightly.20260730.c · nightly · 2026-07-30

Content-block metadata, including `prompt_cache_breakpoint`, now survives the round trip through the `openai-responses` provider, so prompt caching against GPT-5.6 behaves as configured instead of dropping the cache markers.

- **openai-responses metadata**: The `openai-responses` code path no longer strips metadata off content blocks when building the request, which is what previously discarded `prompt_cache_breakpoint`.
- **`baml pack` on Linux**: The custom ELF note writer (`pack_elf.rs`) is gone. `baml pack` now embeds its payload through `libsui::Elf::append` again, using libsui `0.16.4` (up from `0.14`), which no longer places the note where it can overlap the host binary's `.bss`. The pinned `object` dependency was removed alongside it. Behavior for a packed executable is unchanged.
- **CI coverage**: The pack end-to-end tests now run against `x86_64-unknown-linux-musl` in addition to glibc, so both Linux libc variants exercise the ELF pack path.

The bulk of this release is an internal refactor: lambda bodies are now lowered into the enclosing function's expression arena (a new `LambdaDef` type replaces the synthetic `FunctionDef`, and a new `traverse` module walks a body's tree while skipping lambda interiors). This is a structural change with no intended change to how your BAML types or runs. The BEPs app also moved from `typescript/apps/beps` to `typescript2/app-beps`, which only affects CI path filters.

Authors: conrad-scherb, sxlijin, 2kai2kai2, codeshaunted

## Internal size-gate baselines switch to human-readable byte units, no user-facing change.

0.15.1-nightly.20260730.b · nightly · 2026-07-30

This nightly is internal tooling only. The `tools_size_gate` crate now reads and writes its size ceilings and baselines as human-readable strings like `20.9 MiB` instead of raw integers such as `21_956_782`, and the checked-in `.cargo/size-gate.toml` and `.ci/size-gate/*.toml` files were refreshed accordingly. Nothing in the BAML language, runtime, generators, or CLI behavior changed.

Authors: sxlijin

## Interface fields can now be read and written through a receiver whose concrete type is unknown at compile time

0.15.1-nightly.20260730.a · nightly · 2026-07-30

Reading or assigning a field declared on an interface now works when the receiver is an interface-typed value whose concrete class is only known at run time. Previously a field access needed a statically known slot in the receiver's own layout. The compiler now emits a virtual access that resolves the correct slot through the resolved implementation at run time.

**Highlights**
- **Virtual field reads and writes.** Accessing `x.field` where `x` has an interface type dispatches through the implementation's field-link table, so two classes implementing the same interface can back the same interface field with fields at different offsets. Reads and assignments both go through this path.
- **Per-impl field links.** An in-body `implements` block can link an interface field to a differently named class field, and the emitted impl rule carries a table from interface field index to class slot. An out-of-body impl of a field-bearing interface is still rejected as `E0126`, and a class that fails to cover an interface field is still `E0124`.
- **Untyped-field representation.** A class or interface field written without a type now recovers internally as an error type instead of an absent type. This is a compiler-internal cleanup that removes the many `Option` stand-ins scattered across lowering and inference. The diagnostic for a field missing its type annotation is unchanged, so no BAML source that compiled before behaves differently.

Authors: 2kai2kai2

## Internal size-gate baseline refresh, no user-facing changes

0.15.1-nightly.20260730.b · nightly · 2026-07-30

This nightly contains only build-tooling changes to the internal `tools_size_gate` crate, with nothing that affects BAML code, the CLI surface, or generated clients. The size-gate config (`.cargo/size-gate.toml`) and recorded baselines now store thresholds as human-readable strings like `"20.9 MiB"` instead of raw byte counts, backed by a new `bytesize` dependency and a `human_size` module. If you never touch BAML's size-gate CI, there is nothing to do here.

Authors: sxlijin

## Interface fields can be read and written through an interface-typed receiver

0.15.1-nightly.20260730.a · nightly · 2026-07-30

Reading or writing a field declared on an interface, through a value whose concrete type is only known at run time, now resolves to the correct class slot. Two classes that implement the same interface can store an interface field at different offsets in their own layout, so a static field index cannot work here. The access is now dispatched: `field_index` is the field's position in the interface's declared field list, and the runtime maps it to the concrete class slot through a per-impl link table.

- **Virtual field reads and writes.** Accessing an interface field on an interface-typed receiver lowers to a dispatched load (`VirtualLoadField`) or store (`VirtualStoreField`) instead of a fixed-offset access. The interface, not the receiver's class, defines the index space, so the same code works across every implementer.
- **Field links baked per impl.** Each in-body `implements` block records how its interface fields map to class slots, honoring an explicit `field as class_field` link or falling back to the same-named class field. Out-of-body impls of a field-bearing interface remain rejected (E0126).
- **Known limitation.** An interface field whose type names `Self` or `Self.Assoc` (for example `key: Self.Key`) still resolves as an error-recovery type in one path. This only surfaces after the compiler has already reported that the interface field requires qualified construction, so it does not add new diagnostics.

A large supporting change makes a class or interface field's type always present internally: a field written with no type is reported by the parser and recovers as an error type rather than an absent one. This is not observable in your BAML code; the missing-type-annotation diagnostic is unchanged.

Authors: 2kai2kai2

## Playground graph nodes size to fit multi-line error tracebacks

0.15.1-nightly.20260729.g · nightly · 2026-07-29

Error and traceback previews on playground graph nodes now grow to fit the full diagnostic instead of being clipped by a fixed-height box.

- **Traceback previews no longer clip.** When a graph node reports an error, its preview card now sizes to fit every line of the diagnostic, growing with the wrapped-line count so multi-line tracebacks are shown in full rather than cut off by a fixed-height box.
- **Output values are left-aligned and pretty-printed.** Captured output values rendered inside graph nodes are now left-aligned and pretty-printed.
- **Explicit argument placeholders are hidden.** In the args form, the greyed `param.defaultExpression` placeholder now shows only for a parameter that has a default and is currently omitted. A parameter with no default, or one you have filled in, no longer displays a placeholder.

These changes are confined to the playground UI. No BAML syntax, function, or CLI behavior changed.

Authors: sxlijin

## Playground node error cards size to their full traceback and hide default placeholders on explicit args

0.15.1-nightly.20260729.f · nightly · 2026-07-29

Error and value previews on playground graph nodes now render legibly, and argument fields stop showing a default's expression as placeholder text once you type a value.

- **Node error cards size to the actual traceback.** `capturedValueCardDiagnosticHeight` computes card height from the real diagnostic text, wrapping at 54 characters per line and allocating one row per line. This replaces the old fixed `hasDiagnostic ? 18 : 0` reservation, so multi-line tracebacks render in full instead of being clipped to a single line.
- **Value previews are pretty-printed and left-aligned.** `CapturedValueCard` gains `prettyPrintValue` and `preserveDiagnosticLines` props, both set by `NodeOutputPreview`, so nested input values and diagnostics inside centered React Flow groups read left-aligned rather than centered.
- **Explicit arguments no longer show the default expression as a placeholder.** In `ArgsForm`, a parameter row only renders `param.defaultExpression` as placeholder text when the argument is both defaulted and omitted. Once you supply a value, the placeholder clears instead of continuing to display the default.

Authors: sxlijin

## Playground graph tab opens a selected run from history

0.15.1-nightly.20260729.e · nightly · 2026-07-29

The Playground graph view can now open a specific run from history instead of only the latest snapshot. `findLatestGraphRunSnapshot` gained a `preferredBoundaryId` argument, so clicking a historical run for a function loads that exact run in the graph, falling back to the newest matching snapshot when the requested run is gone.

- **Run selection is now generation-aware.** `findLatestGraphRunSnapshot` takes a project generation and skips runs whose `projectGeneration` does not match the current one, so a run from an obsolete engine generation is no longer paired against the current graph. The `ProjectUpdate` payload now carries a `generation` field to support this.
- **Trace values render more cleanly.** `ValueRenderer` was reworked so JSON tree rows place each key beside its collapse toggle, internal `$type` and BAML type metadata are hidden, deeply nested inline collections are bounded with `{…}` or `[…]`, and registered custom renderers apply inside the tree.
- **Copy buttons can defer their text.** `CopyButton` accepts a `getText` callback that computes the copied content lazily on click.

The only non-Playground change is skipping a flaky C# native-resource SDK test, which does not affect generated code.

Authors: sxlijin

## The playground Run tab now shows each optional parameter's default expression

0.15.1-nightly.20260729.d · nightly · 2026-07-29

Optional function parameters now carry their default expression's exact source text into the playground, exposed as a new `defaultExpression` field on `ParamSchema`.

- **`defaultExpression` on `ParamSchema`.** The param schema now includes the unevaluated source text of a parameter's default expression, for example `pair(b = 2, a = 1)` for `function Def(x: int, y: int = pair(b = 2, a = 1))`. The field is omitted when a parameter has no default.
- **Default hints in the Run args form.** When an optional parameter is left unset and its field widget has no native text placeholder to fall back on, `ArgsForm` now shows the declared default so you can see what value will be used.
- **Args form layout and controls.** The per-parameter "set" toggle switch is replaced with a checkbox (labeled to choose an explicit value over the default), rows moved to a two-column grid, and tooltips were added. These are presentation changes to the Run tab only.

The param schema wire shape changed to include the new field, and the golden fixture that pins it (`param-schema-golden.json`) was updated to match.

Authors: sxlijin

## `baml toolchain use <path>` selects a local baml-cli binary

0.15.1-nightly.20260729.c · nightly · 2026-07-29

`baml toolchain use` now accepts a filesystem path, so you can point the wrapper at a local build of `baml-cli` instead of a managed release. Any selector containing a path separator is treated as an unmanaged binary.

```bash
baml toolchain use ~/repos/baml/target/debug/baml-cli
BAML_VERSION=./target/debug/baml-cli baml check
```

**Highlights**

- **Local path toolchains.** `install`, `update`, and `uninstall` do not apply to a path toolchain, and `status` is local-only since there is nothing remote to check. A path toolchain is deliberately not settable from `baml.toml`, so a checked-out repository cannot choose which binary runs on your machine. `baml ide install` still requires a managed toolchain and now says so plainly when the active one is a local binary.
- **Hover documentation across the editor.** Hovering a primitive (`int`, `string`, `bool`, `float`), a literal (`"fast"`, `42`, `true`, `null`), a keyword, a schema attribute (`@description`, `@alias`, `@skip`), a client config key (`provider`, `options`, `model`, `http`, `request_timeout_ms`), or an intrinsic type (`void`, `never`, `unknown`) now shows its reference documentation.
- **Snippet completions for top-level declarations.** Accepting `class`, `enum`, `function`, `client`, `test`, and the other top-level keywords now inserts a skeleton with tabstops rather than the bare keyword.
- **Generic fields realize their type arguments.** Completions and hovers on a value like `Lorem<int>` now show field types specialized to the receiver's arguments (for example `a: int` instead of `a: T`).
- **`baml describe` topics.** `alias`, `description`, `skip`, `void`, `never`, and `unknown` now resolve to language-reference topics.
- **Playground.** The run view shows baml.io output, the Monaco file explorer styling is fixed, and starting the server without playground configuration is logged as info rather than an error.

Authors: sxlijin, hellovai

## Selecting a top-level `test` in the playground now renders its workflow graph

0.15.1-nightly.20260729.b · nightly · 2026-07-29

Selecting a top-level `test "..." { ... }` declaration in the playground now renders its control flow graph, matching the behavior you already got when selecting a function. New-style tests are lowered into lambdas registered through a synthesized `$init_test_*` function, so they were not appearing in the function map and produced no graph. The playground now recovers the matching lambda from that registration and graphs the test body directly, keyed on the canonical test name (`root[.namespace]::name`, with `#N` suffixes for duplicates).

Other playground fixes:

- **"Run test" button** stays in a stable position, and the test status is now shown to the left of "run" instead of shifting the button.
- **Group node titles** clamp their width by default so long titles no longer stretch the node.

Tests whose names are computed at runtime still cannot be graphed, since their canonical name cannot be resolved statically.

The rest of the range is CI and tooling only, with no effect on BAML behavior: Ruff is pinned to `0.15.22` ahead of its 0.16 rule expansion, the obsolete Sage apps are removed, and Biome v2 linting is isolated into its own `typescript2` workflow.

Authors: sxlijin

## A global `--project` flag replaces per-command `--from`, and `baml generate add` writes generators into `baml.toml`

0.15.1-nightly.20260729.a · nightly · 2026-07-29

The CLI's project-discovery flag is now the global `--project`, and every command that used to take `--from` still accepts it as a hidden, deprecated alias.

Highlights:

- **`--project` replaces `--from`.** `--from` is now a hidden "Deprecated alias for `--project`" on `run`, `pack`, `fmt`, `check`, `describe`, `playground`, `test`, and `generate`. Existing invocations keep working, but move to `--project` (or the global form, which applies to any command).
- **`baml agent install` flag renames.** The old `--dir` project flag becomes the hidden `--project` alias, and the source flag `--from` is renamed to `--source` (with `--from` kept as an alias). The unreachable-download hint and error text now say `--source`.
- **`baml generate add <output_type>`.** A new subcommand appends a `[generator.<name>]` section to `baml.toml` for a chosen target (for example `python/pydantic2`, `typescript/node`, `go`). The Go target requires `--sdk-import-path <MODULE>/baml_sdk`. `baml init` now scaffolds a comment pointing at this command instead of pre-written generator blocks.
- **Reworked global options.** `--features` is now `-F/--features`, accepting a comma-separated or repeated list (`beta`, `display_all_warnings`). New global flags `--quiet`, `--verbose`, `--directory`, and `--no-progress` are available on every command; `--no-progress` suppresses spinner-style progress lines.
- **`baml help <command>`.** A new `help` subcommand prints detailed, non-paged help for any command path (for example `baml help run`), avoiding interactive pagers that can hang coding agents.
- **`baml run` flag changes.** `--verbose` and the run-local `-h/--help` are gone (use the global `--verbose` and `-h`), and `--include-generated` now controls whether compiler-synthesized functions appear in `--list` output.

Add a generator and generate its client:

```bash
baml generate add python/pydantic2
baml generate
```

Most of the remaining churn is help-text and heading cleanup across the command surface with no behavior change.

Authors: 2kai2kai2, codeshaunted

## Returned BAML closures bind their required arguments by position

0.15.1-nightly.20260728.f · nightly · 2026-07-29

Required parameters of a returned BAML callable now bind by position, so the parameter names inside a closure implementation no longer need to match the names on its declared callable type. Optional parameters are still matched by name.

```baml
function make_adder(offset: int) -> (value: int) -> int throws never {
    return (x: int) -> int { offset + x }
}
```

Here the returned type declares `value` but the implementation names its parameter `x`, and the call still resolves.

Highlights:

- **Required callable arguments bind positionally.** `BexArgs` now carries `required` (ordered) and `optional` (by name) separately, and a returned callable that receives the wrong number of required arguments fails with a `callable expects N required argument(s)` type mismatch.
- **`call_function` and `call_handle` are merged.** The CFFI function table drops the `call_handle` slot. `call_function` now takes a single protobuf-encoded `CallFunctionArgs` whose `call_target` oneof selects either `function_name` or `function_handle`. The function-table ABI revision moves from 1 to 2, and revision-1 and revision-2 builds reject one another before reading the changed slot.
- **Go host callables now take `context.Context`.** Closures returned to Go accept a `ctx` as their first argument, for example `addTen(ctx, 5)` instead of `addTen(5)`.
- **Nested callable types report a clearer error.** The C++ generator now emits an `nested callable type` unsupported diagnostic for a callable nested inside another type instead of silently deferring.
- **Playground styling.** Function and test rows in the sidebar now share the same row styling.

Authors: sxlijin

## Playground graph nodes size error cards from the real diagnostic text

0.15.1-nightly.20260729.g · nightly · 2026-07-29

Node error previews in the playground graph now grow to fit their actual diagnostic instead of reserving a fixed single line.

- **Diagnostic-aware node height.** `capturedValueCardHeightForContent` now takes the height returned by the new `capturedValueCardDiagnosticHeight`, which counts newlines and wraps long lines at `CAPTURED_VALUE_CARD_DIAGNOSTIC_CHARACTERS_PER_LINE` (54). This replaces the previous flat `hasDiagnostic ? 18 : 0` reservation, so a multi-line traceback no longer gets clipped inside the node card.
- **Truncation footer spacing.** When a node has more value previews than `NODE_VALUE_PREVIEW_MAX`, the layout now reserves `NODE_VALUE_PREVIEW_FOOTER_HEIGHT` for the "+N more" footer row so the extra line has room.
- **Left-aligned graph values.** Value cards rendered inside graph nodes pass `prettyPrintValue`, which left-aligns the printed value within React Flow's centered node groups.
- **Argument placeholders for explicit args.** In the args form, the default-expression placeholder now shows only when a parameter both `hasDefault` and is `omitted`. Parameters you have supplied a value for, or that have no default, no longer show a stray placeholder.

Authors: sxlijin

## The playground graph renders node errors as sized, multi-line cards and stops showing default placeholders for explicitly-set args

0.15.1-nightly.20260729.f · nightly · 2026-07-29

Two playground fixes. Both are web UI changes, so no BAML syntax, CLI, or API shape changed.

- **Node error cards in the graph view.** `CapturedValueCard` now sizes error diagnostics by wrapping traceback text across lines instead of reserving a fixed 18px, via the new `capturedValueCardDiagnosticHeight` calculation. Multi-line errors on a graph node get enough vertical room to display fully, and error cards render with the red error styling. Values in graph previews are also left-aligned and pretty-printed.
- **Argument placeholders.** In the args form, a parameter's default expression is shown as a placeholder only when the argument has a default and is currently omitted. When you supply a value explicitly, the input no longer displays the default as a ghost placeholder.

An internal issue-tracking file was also removed from the tree.

Authors: sxlijin

## Playground opens a selected historical run in the graph tab

0.15.1-nightly.20260729.e · nightly · 2026-07-29

Selecting a past run in the Playground now opens that specific run in the graph tab instead of always jumping to the newest snapshot. This is a Playground and VS Code webview release; no BAML language, runtime, or CLI behavior changed.

- **History runs in the graph.** `findLatestGraphRunSnapshot` now takes a preferred boundary id and returns the matching historical run when it exists, falling back to the newest matching snapshot when the requested run is unavailable.
- **Runs are matched to the project generation.** Snapshot selection now checks `projectGeneration`, so an obsolete run generation no longer pairs with the current graph. `ProjectUpdate` carries a new `generation` field (omitted by older runtimes) to support this.
- **Cleaner trace and value rendering.** `ValueRenderer` was rewritten so JSON tree rows show each key beside its collapse toggle, internal `$type` metadata is hidden, and deeply nested inline collections are bounded with `{…}` or `[…]` rather than expanded in full.
- **Copy button lazy text.** `CopyButton` accepts a `getText` callback to compute clipboard content on click.

The C# `test_phase12_executes_native_typed_resource_apis_lifetimes_and_state` test is marked ignored as flaky (B-1059) and does not affect shipped behavior.

Authors: sxlijin

## The playground Run tab now shows a parameter's default expression as a hint

0.15.1-nightly.20260729.d · nightly · 2026-07-29

The `ParamSchema` wire shape gains a `defaultExpression` field carrying the exact, unevaluated source text of a parameter's declared default. The playground's Run tab args form uses it to hint the default value on parameters you leave unset.

- **`defaultExpression` on `ParamSchema`.** When a function parameter has a default, the playground now knows the literal source text of that default (for example `pair(b = 2, a = 1)`), not just that a default exists. The field is omitted from the wire payload when there is no default.
- **Default hint in the args form.** For a parameter with a default that you have not explicitly set, the form shows the default expression, but only when the underlying widget does not already render its own text placeholder, so simple string and number fields are left as-is.
- **Set/omit control reworked.** The per-parameter "set" switch is now a checkbox with a tooltip explaining that toggling it uses an explicit value instead of the default. Behavior is unchanged; you still opt a parameter in or out of the request.

The surfaced text is whatever you wrote in the `.baml` source:

```baml
function pair(a: int, b: int) -> int { a + b }
function Def(x: int, y: int = pair(b = 2, a = 1)) -> int { 1 }
```

Here `Def`'s `y` parameter reports `pair(b = 2, a = 1)` as its `defaultExpression`.

Authors: sxlijin

## `baml toolchain use` accepts a path to a local baml-cli binary

0.15.1-nightly.20260729.c · nightly · 2026-07-29

`baml toolchain use` now accepts a filesystem path to a baml-cli binary, so you can point the wrapper at a local build instead of a managed release. Any selector containing a path separator is treated as a local toolchain.

**Highlights**

- **Local toolchains by path.** Pass a path to `baml toolchain use`, or set `BAML_VERSION` to one. `install`, `update`, and `uninstall` do not apply to a path toolchain, and `status` becomes local-only since there is nothing remote to check. This is deliberately not settable from `baml.toml`: a checked-out repository cannot choose which binary runs on your machine. `baml ide install` still requires a managed toolchain and now says so explicitly when the active one is a local binary.
- **Editor hovers cover more of the language.** Hovering now resolves primitive types (`int`, `string`, `bool`, `float`), literal types, keywords, schema attributes (`@description`, `@alias`, `@skip`), client config keys (`provider`, `options`, `model`, `http`, `request_timeout_ms`), and intrinsic types (`void`, `never`, `unknown`), pulling their documentation into the hover.
- **Generic member types are realized on hover and completion.** For a receiver like `Lorem<int>`, a field declared `a: T` now shows and completes as `int` rather than `T`.
- **Snippet completions for top-level declarations.** Accepting `class`, `enum`, `function`, `client`, `test`, and the other top-level keywords now inserts a skeleton with tabstops instead of the bare keyword.
- **Playground.** The run view now shows `baml.io` output, the Monaco file explorer styling is fixed, and an unconfigured playground is logged as info rather than an error on exit.

```bash
baml toolchain use ~/repos/baml/target/debug/baml-cli
BAML_VERSION=./target/debug/baml-cli baml check
```

The `baml describe` command also now reads its keyword and attribute documentation from a shared registry in `baml_builtins2`, so `baml describe alias`, `baml describe skip`, and `baml describe never` resolve to the same topics the editor shows. This is a consolidation with no change to the rendered output.

Authors: sxlijin, hellovai

## The playground renders workflow graphs when you select a `test`

0.15.1-nightly.20260729.b · nightly · 2026-07-29

Selecting a top-level `test "..." { ... }` in the playground now renders its workflow graph. Previously only functions produced a control flow graph; new-style tests are lowered into lambdas registered via `$init_test_*` and did not appear in the function map, so selecting one showed nothing. The playground now recovers the test's lambda body from that registration and graphs it directly, using the test's canonical `owner::name` (with `#N` suffixes for duplicate names).

Other playground fixes:

- **Stable "run test" button.** The run button keeps a fixed position, and test status is shown to its left instead of shifting the button around as state changes.
- **Group node titles.** Titles on group nodes are width-clamped by default so long names no longer stretch the node.

The rest of this release is CI and tooling only: obsolete Sage apps (`ask-baml-client`, `sage-backend`, `sage-interface`) were removed, Biome moved to an isolated `typescript2` workflow, and Ruff was pinned to `0.15.22` ahead of its 0.16 rule expansion. None of this affects generated clients or the BAML language.

Authors: sxlijin

## New `baml generate add` command and a reworked CLI where `--project` replaces `--from`

0.15.1-nightly.20260729.a · nightly · 2026-07-29

The new `baml generate add <output-type>` command writes a `[generator.<name>]` section into your `baml.toml`, so you no longer hand-edit generator config to wire up a client.

```bash
baml generate add python/pydantic2
baml generate add go --sdk-import-path example.com/my-project/baml_sdk
baml generate
```

This release also reworks the CLI's global options and help.

- **`baml generate add`**: accepts targets like `python/pydantic2`, `typescript/node`, `swift`, and `go`. The Go target requires `--sdk-import-path <MODULE>/baml_sdk`, and passing it to any other target is rejected.
- **`--project` replaces `--from`**: every project-discovery command (`run`, `check`, `test`, `fmt`, `pack`, `playground`, `describe`, `generate`) now takes `--project`. `--from` still works as a hidden deprecated alias, so existing invocations keep running.
- **New global options**: `--quiet`, `--verbose`, `--directory`, and `--project` are now global. Compiler features move to `-F/--features` and accept a comma-separated list, for example `-F beta,display_all_warnings`.
- **`baml help <command>`**: a new command that prints help directly without paging, for example `baml help run` or `baml help test`.
- **`--output-dir` standardizes output flags**: `baml ide install --dir` and `baml generate -o/--output` are now `--output-dir`, with the old names kept as aliases.
- **`baml agent install --source`**: replaces `--from` for the skills archive location, with `--from` kept as an alias.
- **`baml run`**: the old `--verbose` flag is gone. Use `--include-generated` to include compiler-synthesized functions in `--list` output.
- **`--no-progress`**: suppresses progress output on the human preset.
- **`baml init`** now scaffolds a `baml.toml` that points you at `baml generate add python/pydantic2` instead of shipping commented-out generator blocks.

On the language side, #4258 changes how function-typed values are handled in pattern matching. The standard library comments that previously called out `E0155` refutable function patterns have been removed accordingly.

Authors: 2kai2kai2, codeshaunted

## Returned BAML closures bind required arguments by position, not by parameter name

0.15.1-nightly.20260728.f · nightly · 2026-07-29

When a BAML function returns a closure, its required parameters now bind by position, so the implementation's parameter names may differ from the names declared in the callable type. Previously the runtime matched required arguments by source name through the handle-dispatch path.

```baml
function make_adder(offset: int) -> (value: int) -> int throws never {
    return (x: int) -> int { offset + x }
}
```

Here the declared type names the parameter `value` while the returned closure names it `x`. Both refer to the same positional argument, and calling the returned closure with a single `int` resolves correctly.

**Other changes:**

- **Go SDK closures take `context.Context`.** Returned closures generated for Go now accept a leading `ctx` argument, for example `addTen(ctx, 5)` and `nextValue(ctx)`. Update call sites to thread the context through.
- **CFFI `call_handle` folded into `call_function`.** The C ABI drops the separate `call_handle` slot. `CallFunctionArgs` gains a `call_target` oneof selecting either `function_name` or `function_handle`, and `call_function` now takes `(encoded_args, length, callback_id)`. The function-table ABI revision bumps from 1 to 2, so hosts and runtimes built against different revisions reject one another before reading the changed slot. Invoking a function handle with type arguments is now reported as `baml.errors.InvalidArgument` rather than an internal error.
- **Playground sidebar.** Function and test sidebar rows now share consistent styling.

Authors: sxlijin

## Playground graph nodes size error cards to their full traceback and hide default placeholders on explicit args

0.15.1-nightly.20260729.g · nightly · 2026-07-29

Error cards in the playground run graph now grow to fit their diagnostic instead of reserving a fixed stub of space.

- **Node error rendering.** Card height is now computed from the diagnostic text, allocating one row per traceback line plus extra rows for wrapping (`capturedValueCardDiagnosticHeight`), replacing the previous flat 18px reservation. Long multi-line tracebacks are no longer clipped.
- **Pretty-printed nested values.** A new `prettyPrintValue` prop renders nested input and output values left-aligned inside the centered React Flow node groups, so structured previews read cleanly rather than being center-justified.
- **Argument placeholders.** In the args form, the default-expression placeholder is now shown only when a parameter has a default and is omitted. When you pass an explicit value, the greyed default placeholder no longer appears behind your input.

Authors: sxlijin

## Playground graph renders node errors with full tracebacks and hides default placeholders for filled-in arguments

0.15.1-nightly.20260729.f · nightly · 2026-07-29

Two playground fixes. The graph view now sizes error nodes to fit their full diagnostic, and the argument form stops showing a default-value placeholder for arguments you have already supplied.

- **Error nodes in the graph** now render multi-line tracebacks instead of a fixed single-line stub. Node height is computed from the actual diagnostic text (line count plus wrapping at 54 characters per line), and error cards get their own red background and border so a failed call is visible at a glance.
- **Argument form placeholders** for explicit arguments are gone. A parameter's `defaultExpression` is now shown as a placeholder only when the parameter has a default and you have left it omitted. Once you fill the field in, the placeholder is cleared, so a supplied value no longer sits next to a ghosted default that does not apply.

No language, CLI, or API surface changed. These affect only the playground UI.

Authors: sxlijin

## Playground history runs open in the graph tab, and trace values render without internal metadata

0.15.1-nightly.20260729.e · nightly · 2026-07-29

Selecting a past run from playground history now opens that specific run in the graph tab instead of always jumping to the newest snapshot. `findLatestGraphRunSnapshot` takes a preferred run identifier and returns the matching historical run when it is available, falling back to the newest matching snapshot otherwise.

- **Graph runs are scoped to the current project generation.** Run selection now compares each run's `projectGeneration` against the live project, so a stale run from a previous engine generation is no longer paired with the current graph. `ProjectUpdate` carries a new `generation` field to support this; older runtimes that omit it are treated as before.
- **Trace and result values hide internal type keys.** The playground value renderer no longer shows `$type` metadata in either the JSON tree or inline view, and routes tagged values (for example `$media`) through their registered renderer. Unregistered typed values just show their visible fields.
- **Deeply nested inline values are bounded.** Inline rendering now collapses collections past depth 2 to `{…}` or `[…]` rather than expanding without limit.
- **Copy buttons can defer their text.** `CopyButton` accepts a `getText` callback so copy content is computed on click instead of up front.

A flaky C# SDK resource lifetime test was marked ignored; no runtime behavior changed.

Authors: sxlijin

## The playground Run form now shows a parameter's default expression when you omit it

0.15.1-nightly.20260729.d · nightly · 2026-07-29

The playground's Run tab args form now surfaces each optional parameter's default via a new `defaultExpression` field: the exact, unevaluated source text of the default is carried through the worker protocol and shown as a hint when you leave that parameter unset.

- **`defaultExpression` on the param schema.** `ParamSchema` in `worker-protocol.ts` gains an optional `defaultExpression` field. For a parameter declared like `y: int = pair(b = 2, a = 1)`, it holds the literal string `"pair(b = 2, a = 1)"` rather than an evaluated value. The field is omitted entirely when a parameter has no default.
- **Args form UI.** For a parameter that has a default and is currently omitted, the form shows the default expression as a hint (only when the underlying widget does not already render a native placeholder). The row layout moved to a two-column grid and the per-parameter "set" toggle is now a checkbox with a tooltip explaining that checking it uses an explicit value instead of the default.

The following declares a parameter whose default is a function call, which is what the form now displays back to you:

```baml
function pair(a: int, b: int) -> int { a + b }
function Def(x: int, y: int = pair(b = 2, a = 1)) -> int { x + y }
```

Authors: sxlijin

## `baml toolchain use <path>` runs a local baml-cli build, plus richer editor hovers

0.15.1-nightly.20260729.c · nightly · 2026-07-29

You can now point the wrapper at a local `baml-cli` binary instead of a managed toolchain. A selector containing a path separator is treated as an unmanaged binary, so `baml toolchain use ~/repos/baml/target/debug/baml-cli` selects your own build, and `BAML_VERSION=./target/debug/baml-cli baml check` does the same for a single command.

```bash
baml toolchain use ~/repos/baml/target/debug/baml-cli
BAML_VERSION=./target/debug/baml-cli baml check
```

Highlights:

- **Local toolchains.** `install`, `update`, and `uninstall` do not apply to a path toolchain, and `status` is local-only since there is nothing remote to check. A path toolchain is deliberately not settable from `baml.toml`, so a checked-out repository cannot choose which binary runs on your machine. `baml ide install` still requires a managed toolchain and now says so plainly when the active one is a local binary.
- **Hover documentation.** Hovering primitive types (`string`, `int`, `bool`, `float`), intrinsic types (`void`, `never`, `unknown`), schema attributes (`@description`, `@alias`, `@skip`), client config keys (`provider`, `options`, `model`, `http`, `request_timeout_ms`), and keywords now shows their reference docs. `baml describe alias`, `baml describe skip`, `baml describe void`, and friends resolve to the same topics.
- **Generic member types.** Field completions and hovers on a generic receiver now use the realized type argument. For `a: Lorem<int>`, `a.a` reports `int` rather than the unbound `T`.
- **Snippet completions.** Top-level keyword completions (`class`, `enum`, `function`, `client`, `test`, and others) now insert declaration skeletons with editable tabstops.
- **Playground.** The run view shows baml.io output, and the Monaco file explorer styling is fixed.

A missing playground configuration is now logged at info level instead of being reported as a server error.

Authors: sxlijin, hellovai

## Selecting a top-level `test` in the playground now renders its workflow graph

0.15.1-nightly.20260729.b · nightly · 2026-07-29

Selecting a statically named top-level `test "..." { ... }` in the playground now renders its workflow graph. New-style tests are lowered into lambdas registered through a per-file `$init_test_*` function, so they never appeared in the ordinary function list and the playground had nothing to graph when you clicked one. The control flow builder now recovers the matching lambda from that registration by its canonical `owner::name` identity (with `#N` suffixes for duplicates) and graphs the test body directly, with source spans anchored to the test's name.

Other playground fixes in this build:

- **Run test button** keeps a stable position instead of shifting, and the test status indicator now sits to the left of "run".
- **Group node titles** are width-clamped by default so long names no longer stretch the node.

The rest of this release is repository cleanup with no effect on BAML behavior: the obsolete Sage apps (`ask-baml-client`, `sage-backend`, `sage-interface`) and the root Biome v1 config were removed, TypeScript linting moved to a separate `typescript2` workflow, and Ruff was pinned to `0.15.22` to avoid the 0.16 rule expansion.

Authors: sxlijin

## New `baml generate add` command and a `--project` flag that replaces `--from` across the CLI

0.15.1-nightly.20260729.a · nightly · 2026-07-29

`baml generate add` writes a `[generator.<name>]` section into your `baml.toml` so you no longer hand-edit the manifest to wire up a client.

This release reworks the CLI surface. The changes are additive: deprecated flags still parse, but the help now points at the new names.

**Highlights**

- **`baml generate add <output_type>`.** A new subcommand appends a generator to `baml.toml` and picks a sensible default naming convention. Accepts `python/pydantic2`, `python/pydantic/v1`, `typescript/node`, `typescript/web`, `swift`, `go`, `rust`, `java`, `cpp`, and `csharp`. The Go target requires `--sdk-import-path <MODULE>/baml_sdk` and rejects an invalid import path. The `baml init` template now points at this flow instead of shipping large commented-out generator blocks.
- **`--project` replaces `--from`.** Project-aware commands (`baml check`, `run`, `pack`, `fmt`, `playground`, `generate`, `test`, `describe`) now discover the project from `--project`. `--from` is kept as a hidden, deprecated alias, so existing scripts keep working. The mutual-exclusion error with `--file` now reads `--file and --project are mutually exclusive`.
- **New global options.** `--quiet`, `--verbose` (repeatable), and `--directory <PATH>` are available on every command, and `--features` is now `-F/--features` and accepts comma-separated values. `--no-progress` suppresses progress output.
- **`baml help <command>`.** A new command renders detailed help for a command path without invoking an interactive pager, which avoids trapping coding agents that run through a pseudo-terminal.
- **Flag renames.** `baml ide install --dir` is now `--output-dir` (old `--dir` kept as an alias), `baml agent install --from` is now `--source` and its `--dir` is now `--project`, and `baml generate -o/--output` is now `--output-dir`.
- **`baml run` list flags.** `--verbose` is removed from `baml run`; use the global `--verbose`. A new `--include-generated` adds compiler-synthesized functions to `--list` output.

```bash
baml generate add python/pydantic2
baml generate
```

The `#4258` change also touches pattern matching on function types, but the visible diff for it is limited to standard-library comment rewordings and a compiler test, so no user-facing syntax change is described here.

Authors: 2kai2kai2, codeshaunted

## Returned closures bind required arguments by position, and the CFFI unifies `call_function` and `call_handle`.

0.15.1-nightly.20260728.f · nightly · 2026-07-29

Returned BAML closures now bind their required arguments by position instead of by the parameter names used inside the returning function, so a closure's declared type signature and its implementation can name parameters differently.

- **Returned closures**: `make_adder(offset: int) -> (value: int) -> int` can return `(x: int) -> int { offset + x }`, and the host still supplies the argument under the declared name `value`. Required callable parameter names are now type-insignificant, matching the positional convention used across SDKs. Omitting a required callable argument returns an `InvalidArgument` error that names the missing parameter.
- **Go host callables**: generated closures returned to Go now take a `context.Context` as their first argument, so `addTen(5)` becomes `addTen(ctx, 5)`.
- **CFFI ABI revision 2**: the `call_handle` function-table slot is removed and folded into `call_function`, which now carries a `call_target` oneof selecting either a function name or a function handle. Hosts and runtimes reject a mismatched revision before dispatching, so an SDK and its runtime must be upgraded together.
- **Playground**: function and test sidebar rows now share the same row styling.

```baml
function make_adder(offset: int) -> (value: int) -> int throws never {
    return (x: int) -> int { offset + x }
}
```

Authors: sxlijin

## Operator impls now need an explicit `type Output`, and bare `Self` in an associated type default is rejected as E0157

0.15.1-nightly.20260728.e · nightly · 2026-07-28

Implementing an arithmetic operator interface such as `baml.ops.Add` now requires you to bind its `Output` associated type explicitly. The `Add`, `Subtract`, `Multiply`, `Divide`, `Remainder`, and `Negate` interfaces dropped their `type Output = Self` default, because bare `Self` is no longer allowed in an associated type's default.

**Highlights**

- **`type Output = ...` required in operator impls.** If you `implement baml.ops.Negate` (or any of the other five operator interfaces) for your own class, add a binding like `type Output = Vec2` inside the `implements` block. Without it the impl no longer picks up a `Self` default.
- **E0157: bare `Self` in an associated type default is rejected.** Writing `type Out = Self` on an interface now errors, and so do nested forms like `type Out = Self[]` or a `Holder<Item = Self>` binding. `Self` names each implementor, so it has nothing to resolve against where the interface is used as an existential type. A `Self.Assoc` projection stays legal, since the existential's own pins already fix it.
- **Playground graph navigation lands in the right file and on the right span.** Clicking a control-flow graph node now opens the `.baml` file named by the node's source span instead of guessing from the active editor, so it works in multi-file projects. Column math was also corrected to use UTF-16 code units, so nodes in lines containing emoji (for example `"🚀"`) select the correct range.
- **Python codegen orders type aliases correctly.** Non-recursive generated type aliases are now emitted after the classes their right-hand sides reference and topologically sorted, so the generated modules import without a forward-reference error.

A class implementing an operator interface now looks like this:

```baml
class Vec2 {
  x: int
  implements baml.ops.Negate {
    type Output = Vec2
    function neg(self) -> Vec2 throws never { Vec2 { x: -self.x } }
  }
}
```

Runtime interface dispatch was also reworked to select over a single program-wide impl-rule index rather than a per-package search. This is an internal refactor with no change to which impl resolves for a given call.

Authors: codeshaunted, 2kai2kai2, sxlijin

## `baml describe` no longer surfaces compiler-synthesized internal functions

0.15.1-nightly.20260728.d · nightly · 2026-07-28

`baml describe` and the other language-surface views now hide compiler-synthesized functions that were never part of BAML's user-facing surface, such as the `$init_test` functions generated for each `test` block and the `$new` companions synthesized for primitive clients.

A new `is_language_internal` flag on function metadata drives this. Functions marked language-internal are dropped from `describe`, package and namespace listings, the file outline, LSP code actions, and Python client codegen (so these plumbing functions no longer appear as factory bindings or class methods on generated SDKs).

- **Internal test and client plumbing is hidden.** `$init_test` init functions, synthesized `register` calls, and `<Client>$new` companions no longer resolve through `describe` or listing lookups.
- **LLM companions stay visible.** Per-function companions like `Summarize$render_prompt`, `Summarize$build_request`, and `Summarize$build_request_stream` are still user-facing and continue to appear in `describe` output.

The visibility checks previously keyed off the `AutoDerive` origin in several places; they now key off `is_language_internal`, which is a broader and more accurate signal for what should stay off the language surface.

Authors: rossirpaulo, codeshaunted

## BAML functions that return closures can now be invoked from host SDKs

0.15.1-nightly.20260728.c · nightly · 2026-07-28

A BAML function whose return type is itself a function, such as `make_adder(offset: int) -> (value: int) -> int`, now hands back a callable that the host language can invoke directly. The returned closure is a real native callable in C++, C#, Go, Java, Python, and Rust, carries its captured state, and is reusable across calls.

```baml
function make_adder(offset: int) -> (value: int) -> int throws never {
    return (value: int) -> int { offset + value }
}
```

- **Returned closures across SDKs.** Closures, bound methods, and generic functions returned from a BAML function are now converted to a `FUNCTION_REF` handle instead of failing with `CannotConvert`. A new `call_handle` entry in the C ABI invokes the handle with host-language arguments (positional or named) and delivers the result through the same callback path as `call_function`.
- **`baml grep` removed.** The `grep` subcommand and its `--def`, `--refs`, `--symbols`, and text-search modes are gone. Use `baml describe` for symbol introspection.
- **`baml describe` reports the exact budget.** A truncated `describe` now tells you the precise `--budget` value that renders the complete output, accounting for method sections, container references, and dependencies, rather than an estimate based only on body line count. The hint text is now "re-run with a higher `--budget` to see more; use `--budget N`".

Authors: sxlijin, codeshaunted

## `baml feedback` previews the payload before asking and remembers a "Don't send" for a day

0.15.1-nightly.20260728.b · nightly · 2026-07-28

`baml feedback` now prints exactly what it will send before you choose, and a "Don't send" choice is remembered for 24 hours instead of re-prompting on every run.

- **Payload preview.** Before the prompt, `baml feedback` prints a "Here's what will be sent:" block showing your `Issue`, `Repro`, and `Context` fields, plus the CLI version and your OS/architecture. Nothing is transmitted until you pick a reporting mode.
- **Day-long decline cooldown.** Choosing "Don't send" (option 3) now records the decision in `creds.json` as `feedback_declined_at`. For the next 24 hours, `baml feedback` declines quietly ("Nothing sent (you declined recently)") and exits 0 rather than asking again. Previously declining was not sticky and re-prompted immediately.
- **Explicit flags still send.** Passing `--anonymous` or `--email` overrides the cooldown, sends the report, and clears the decline state. A successful send also clears any earlier cooldown.
- **Reworded prompt.** The interactive prompt now reads "How should I report it?" (was "Report it to Boundary?"), following the new preview.

```bash
# Cooldown active from an earlier "Don't send"? Send anyway:
baml feedback --anonymous --issue "parser panics on empty enum"
```

Authors: ATX24

## New `baml feedback` command and email-based `baml login`

0.15.1-nightly.20260728.a · nightly · 2026-07-28

This nightly adds `baml feedback`, a command for reporting issues or improvements directly from the CLI, and reworks `baml login` into a plain email login.

**Highlights**

- **`baml feedback`** reports an issue or improvement to Boundary. You can send anonymously (a locally generated id persisted in `~/.baml/creds.json` keeps repeated reports as one reporter) or with your email after `baml login`. It takes `--issue`, `--repro`, and `--context` flags, or a JSON object via an argument, a file path, or `-` for stdin.
- **`baml login` is now an email login.** The earlier anonymous-first flow is gone: `baml login` runs the WorkOS device authorization grant and attaches your verified email. Logging in after filing anonymous feedback merges those prior reports into your account. The `baml auth login` claim ceremony no longer exists, and `baml auth` is now described as "Manage your Boundary session".
- **`spawn` futures are now reflectable.** A `spawn` carries the `T` and `E` of its `Future<T, E>` at runtime, so `is` and `match` can inspect a future's generic parameters. A spawned body that provably cannot throw resolves to `Future<T, never>`, and awaiting it is infallible with no `catch` needed.
- **Swift codegen** emits `nonisolated` generated types for `MainActor`-default consumers, so generated types can be used off the main actor.
- **Fixes** closure resolution in loop headers, and an incomplete `spawn` (no block body) now lowers to a missing expression instead of producing a malformed spawn.

```bash
baml feedback --issue "parser panics on nested unions" --repro "class A { ... }"
baml feedback --anonymous --issue "..."
```

A large internal refactor also moves generic parameters from name-based to indexed `ParamTy` identities across the type checker and emit layers. This is not user-visible on its own but underpins the future reflection work.

Authors: rossirpaulo, 2kai2kai2, codeshaunted, ATX24

## `baml feedback` reports issues from the CLI, and `match`/`is` now work on `Future<T, E>`

0.15.1-nightly.20260727.g · nightly · 2026-07-28

`baml feedback` is a new CLI command for reporting an issue or improvement to Boundary without leaving your terminal.

**Highlights**

- **`baml feedback`**: send an issue, a repro snippet, and extra context. You can report anonymously or attach your email so you can be notified when a fix ships. Fields can be passed as flags or as JSON on stdin.
- **`baml login` simplified**: `baml login` is now a plain email login through the WorkOS device authorization flow. The earlier anonymous-first agent-registration and the `baml auth login` claim ceremony are gone. `baml auth` now just manages your Boundary session.
- **Reflection and matching on futures**: a `spawn`ed `Future<T, E>` now carries its `T` and `E` at runtime, so `is`/`match` and reflection can see a future's type arguments. A spawned body that provably cannot throw is typed `Future<T, never>`, and awaiting such futures is infallible with no `catch` needed.
- **Swift codegen**: generated types are emitted as `nonisolated` for MainActor-default consumers, so they can be used off the main actor.
- **Closure resolution in loop headers**: closures written in a loop header now resolve their captured names correctly.

```bash
baml feedback --issue "parser panics on nested unions"
```

Authors: rossirpaulo, 2kai2kai2, codeshaunted, ATX24

## Operator `implements` blocks now require an explicit `type Output`

0.15.1-nightly.20260728.e · nightly · 2026-07-28

Implementing a `baml.ops` operator interface such as `Add` or `Negate` on your own class now requires an explicit `type Output = ...` in the `implements` block. The associated `Output` type no longer defaults to `Self`, so a class that previously relied on that default will fail to compile until you add the line.

- **Operator `Output` is now explicit.** The builtin operator interfaces (`Add`, `Subtract`, `Multiply`, `Divide`, `Remainder`, `Negate`) dropped their `type Output = Self` default. Each `implement` block now states its `Output` directly, and user classes must do the same.
- **New diagnostic E0157.** Bare `Self` in an associated type's default is now rejected. `interface I { type Out = Self }` (and nested forms like `Self[]`) reports `SelfInAssociatedTypeDefault`, because `Self` names each implementor and has nothing to resolve against at an interface-existential type. A `Self.Assoc` projection default is still allowed.
- **Playground: graph navigation opens the right file.** Clicking a control-flow graph node now navigates using the node's own source file path instead of assuming the active editor, so an inlined node that lives in another `.baml` file jumps to the correct location.
- **Playground: emoji columns fixed.** Source spans now translate UTF-8 byte offsets to the UTF-16 columns VS Code and the playground wire expect, so positions after an emoji like `"🚀"` land correctly.
- **Python: type alias ordering.** Generated Python now emits non-recursive type aliases after the classes they reference and topologically orders same-leaf alias chains, fixing import-time ordering failures.

A class implementing an operator interface now looks like:

```baml
class Vec2 {
    x: int
    implements baml.ops.Negate {
        type Output = Vec2
        function neg(self) -> Vec2 throws never { Vec2 { x: -self.x } }
    }
}
```

Authors: codeshaunted, 2kai2kai2, sxlijin

## `baml describe` no longer surfaces compiler-synthesized functions

0.15.1-nightly.20260728.d · nightly · 2026-07-28

`baml describe`, package and namespace listings, the file outline, and code actions no longer surface functions the compiler synthesizes for you. These are the language-internal helpers you never wrote, for example the `$init_test` functions generated for `test` blocks and the `<Client>$new` companions generated for primitive clients. LLM function companions like `Summarize$render_prompt` stay visible.

- **Generated Python client bindings** no longer include these internal plumbing functions, so they no longer appear as static or instance methods on your classes.
- Internally this is tracked by a new `is_language_internal` flag on function metadata, which is what the visibility views now filter on.

Separately, a CI fix skips an unused protoc setup step in the bridge CFFI build.

Authors: rossirpaulo, codeshaunted

## Returned BAML closures are now callable across every SDK

0.15.1-nightly.20260728.c · nightly · 2026-07-28

`make_adder(offset: int) -> (value: int) -> int` and other BAML functions that return a closure can now have that returned closure invoked directly from your host language.

**Highlights**

- **Returned closures are callable across every SDK.** A closure, bound method, or generic function returned from a BAML function now converts to a `FUNCTION_REF` handle instead of failing with a "cannot convert" error. A new `call_handle` C ABI entry invokes an engine-owned callable by its handle, so Python, Go, Java, C#, Rust, and C++ can each call the returned value as a native callable, reuse it, and preserve mutable captures.
- **`baml grep` is removed.** The `grep` subcommand, along with its `--def`, `--refs`, and `--symbols` flags, no longer exists. Use `baml describe` for symbol lookup and definitions.
- **`baml describe` reports the exact budget for full output.** When output is truncated, the hint now names the precise `--budget` value that prints the complete description, and every truncation marker in a single run reports the same value.
- **Error graphs render for message-only errors.** An error that carries only a message now displays correctly in the graph output.

```baml
function make_adder(offset: int) -> (value: int) -> int throws never {
    return (value: int) -> int { offset + value }
}
```

```python
add_ten = make_adder(offset=10)
assert add_ten(5) == 15
assert add_ten(7) == 17
```

Authors: sxlijin, codeshaunted

## `baml feedback` previews the payload before sending and remembers a "Don't send" for a day

0.15.1-nightly.20260728.b · nightly · 2026-07-28

`baml feedback` now prints exactly what a report will contain before it prompts you, and choosing "Don't send" quiets the prompt for the next 24 hours instead of asking again on the next run.

- **Payload preview.** Before the prompt, `baml feedback` prints the `Issue`, `Repro`, and `Context` text along with the CLI version and your OS/arch, so you can see what leaves your machine before deciding.
- **Decline cooldown.** Picking "Don't send" now records `feedback_declined_at` in `creds.json` and suppresses the prompt for a day. During that window the command exits without sending and prints that you declined recently. This changes prior behavior, where declining persisted nothing and re-prompted immediately on the next run.
- **Overrides still send.** Passing `--anonymous` or `--email` sends regardless of the cooldown and clears it, as does any successfully sent report.

The prompt wording also changed: the intro is now "I found an issue with BAML. I can report it to Boundary." and the choice line reads "How should I report it?".

Authors: ATX24

## New `baml feedback` command and email-based `baml login`

0.15.1-nightly.20260728.a · nightly · 2026-07-28

`baml feedback` reports a language issue or improvement to Boundary straight from the CLI, and `baml login` is now a plain email login.

**Highlights**

- **`baml feedback`**: a new command for reporting an issue or improvement. Pass `--issue`, `--repro`, and `--context`, or pipe a JSON object on stdin. Reports go out anonymously by default under a locally generated id stored in `~/.baml/creds.json`; add `--email` (after `baml login`) to attach your verified email and get notified when a fix ships. Logging in later merges your earlier anonymous reports into your account.
- **`baml login`**: now runs the WorkOS device flow to log in with your email. The earlier anonymous-first agent registration and the `baml auth login` claim ceremony are removed. `baml auth` now just manages your Boundary session.
- **Reflection and `match` on futures**: a `Future<T, E>` value now carries its `T` and `E` at runtime, so `is` and `match` work on futures. A spawned body that provably cannot throw yields `Future<T, never>`, and awaiting it is infallible, so no `catch` clause is required (the standard-library `run_children_parallel` was updated to drop its `catch_all`).
- **Swift codegen**: generated types are emitted `nonisolated` so `@MainActor`-default consumers can use them off the main actor.
- Closure resolution inside loop headers is fixed.

```bash
baml feedback --issue "parser panics on nested unions" --repro "class A { ... }"
```

Authors: rossirpaulo, 2kai2kai2, codeshaunted, ATX24

## New `baml feedback` command, a simplified `baml login`, and reflection over `Future` values

0.15.1-nightly.20260727.g · nightly · 2026-07-28

`baml feedback` is a new CLI command for reporting an issue or improvement to Boundary, either anonymously or attached to your email.

- **`baml feedback`**: describe a problem with `--issue`, `--repro`, and `--context`, or pipe a JSON object on stdin. Reports go out anonymously by default under a locally stored identifier, so repeated reports from one machine are grouped. Running `baml login` first attaches your verified email and retroactively links your earlier anonymous reports, so you can be notified when a fix ships.
- **`baml login` is now a plain email login.** It runs the OAuth 2.0 device authorization flow: it prints a one-time code, you confirm it in a browser, and the CLI polls for tokens. The earlier anonymous-first project registration and the `baml auth login` claim ceremony have been removed.
- **Reflection and `match` on futures.** A `Future<T, E>` now carries its `T` and `E` at runtime, so `is`/`match` type patterns and reflection can observe a future's type arguments.
- **`throws never` futures.** A spawned body that provably cannot throw yields `Future<T, never>`, and awaiting it requires no `catch`. The standard library's `run_children_parallel` uses this to drop its former `catch_all` panic arm.
- **Closure resolution in loop headers.** Closures written in loop headers and parameter defaults now resolve to the correct binding. Expression identity is keyed on its arena, so default-value and nested-lambda expressions no longer collide.
- **Swift codegen** emits generated types as `nonisolated` for MainActor-default consumers.
- **Windows** now publishes no-self-update wrapper ZIPs alongside the other targets.

```bash
baml feedback --issue "parser panics on nested unions" --repro "class A { ... }"
```

Authors: rossirpaulo, 2kai2kai2, codeshaunted, ATX24

## Operator interfaces now require an explicit `type Output`

0.15.1-nightly.20260728.e · nightly · 2026-07-28

Classes that implement the built-in operator interfaces (`baml.ops.Add`, `baml.ops.Negate`, and the rest) must now declare `type Output` in the `implements` block, because those interfaces no longer default it to `Self`.

```baml
class Vec2 {
    x: int
    implements baml.ops.Negate {
        type Output = Vec2
        function neg(self) -> Vec2 throws never { Vec2 { x: -self.x } }
    }
}
```

- **Bare `Self` in associated type defaults (E0157).** A default like `type Out = Self` on an interface is now rejected. `Self` names each implementor, so it has nothing to resolve against where the interface is used as an existential type. A `Self.Assoc` projection stays legal, and nested forms (`type Out = Self[]`, `Holder<Item = Self>`) are caught too.
- **Operator interfaces.** `baml.ops.Add`, `Subtract`, `Multiply`, `Divide`, `Remainder`, and `Negate` dropped their `Output = Self` default, which E0157 now forbids, so every `implements` block for them must bind `type Output` explicitly.
- **Python code generation.** Generated type aliases are ordered relative to the classes they reference: non-recursive aliases emit after class definitions and forward chains are topologically sorted, fixing import failures in the generated SDK.
- **Playground graph navigation.** Clicking a control-flow graph node opens the correct `.baml` file instead of assuming the active editor, and source columns are mapped through UTF-16 so nodes near emoji land on the right span.

The runtime impl resolver was also moved to a single program-wide `PackageIndex` instead of a per-package search. This is an internal refactor with no change to which impl resolves.

Authors: codeshaunted, 2kai2kai2, sxlijin

## `baml describe` no longer surfaces synthesized language-internal functions

0.15.1-nightly.20260728.d · nightly · 2026-07-28

`baml describe` and the LSP visibility surfaces (listings, outlines, code actions) now hide compiler-synthesized functions that are not part of BAML's user-facing surface, such as the `$init_test` function generated for each `test` block and the internal `$new` companions synthesized for primitive clients.

- **New `is_language_internal` marker.** Functions carry a `FunctionMetadata` flag distinguishing user-facing declarations from internal plumbing. Synthesized test-init, register-call, and collector functions are marked internal and dropped from `describe`, `resolve_target`, and the file outline, so browsing a package no longer turns up names you cannot call.
- **LLM companions stay visible.** Prompt and request companions like `Summarize$render_prompt`, `Summarize$build_request`, and `Summarize$build_request_stream` remain listed and describable, unchanged from before. Only true internal functions are hidden.

This replaces the previous ad-hoc checks against `FunctionOrigin::AutoDerive` in several visibility paths, so the hiding rule is now consistent across describe, outline, code actions, and client codegen. The build2 bridge CFFI CI job also skips an unused protoc setup step, with no effect on the shipped compiler.

Authors: rossirpaulo, codeshaunted

## BAML functions can return closures that host code invokes as native callables across every SDK.

0.15.1-nightly.20260728.c · nightly · 2026-07-28

A BAML function that returns a function type, like `(value: int) -> int`, now hands back a value your host language can call directly. Python, Go, Rust, Java, C#, and C++ all decode the returned closure into a native callable, pass host-language arguments into it, and receive the typed result back.

```baml
class ReturnedPerson {
    name string
    age int
}

function make_pair_builder(base: int) -> (delta: int, label: string) -> ReturnedPerson throws never {
    return (delta: int, label: string) -> ReturnedPerson {
        ReturnedPerson {
            name: label,
            age: base + delta,
        }
    }
}
```

Calling `make_pair_builder(30)` gives you a callable; invoking it with `(12, "Ada")` returns `ReturnedPerson { name: "Ada", age: 42 }`. The closures are reusable and keep their captures, so a `make_counter(40)` closure returns 41, then 42 on successive calls.

Other changes:

- **`baml grep` is removed.** The `grep` subcommand and its `--def`, `--refs`, `--symbols`, and text-search modes are gone. Use `baml describe` for symbol introspection.
- **`baml describe` reports the exact budget.** When output is truncated, the hint now tells you the precise value to pass, for example `use --budget 97`, instead of a vague "re-run with a higher --budget". The reported number accounts for dependencies and references, so passing it renders the complete description.
- **Graph display for message-only errors is fixed**, so those errors render in the error graph instead of being dropped.

Authors: sxlijin, codeshaunted

## `baml feedback` previews the report payload and remembers a declined prompt for 24 hours

0.15.1-nightly.20260728.b · nightly · 2026-07-28

`baml feedback` now shows exactly what it will send before you decide, and remembers a "Don't send" choice for 24 hours so it stops re-asking on every run.

- **Payload preview.** Before the prompt, `baml feedback` prints the `Issue`, `Repro`, and `Context` fields it will send, plus the CLI version and your OS/ARCH. Nothing is transmitted until you pick a mode.
- **Decline cooldown.** Choosing "Don't send" now records `feedback_declined_at` in `creds.json`. For the next 24 hours, `baml feedback` declines quietly with a message pointing you at `--anonymous` or `--email`, instead of prompting again.
- **Explicit flags bypass the cooldown.** Passing `--anonymous` or `--email` sends even during the cooldown window, and a sent report clears `feedback_declined_at`.
- **Prompt wording.** The interactive prompt changed from "Report it to Boundary?" to "How should I report it?", since the preview above now covers the what and why.

```bash
# Declining once suppresses the prompt for a day; send anyway with a flag.
baml feedback --issue "never mind"      # preview, then "Don't send"
baml feedback --anonymous --issue "ok fine"  # sends and clears the cooldown
```

Authors: ATX24

## New `baml feedback` command, and `match`/reflection now work on `Future<T, E>` values

0.15.1-nightly.20260728.a · nightly · 2026-07-28

`baml feedback` is a new CLI command for reporting an issue or improvement to Boundary, either anonymously or with your email.

**Highlights**

- **`baml feedback`**: a new subcommand that sends a report (issue, repro, and context) to Boundary. It runs anonymously under a locally generated id by default, or attaches your verified email if you have run `baml login`. Fields can be passed as flags or as JSON on stdin.
- **`baml login` simplified**: `baml login` is now a plain email login via the WorkOS device authorization flow, and `baml auth` manages that session. The earlier anonymous-first agent registration and project claim flow (`baml auth login` claim ceremony) has been removed.
- **`match` and reflection on futures**: a `Future<T, E>` produced by `spawn` now carries its `T` and `E` at runtime, so `is`/`match` pattern arms and reflection can inspect a future's type. A spawned body that provably cannot throw is typed `Future<T, never>`; the test registry's `run_children_parallel` now declares `throws never` and awaits its children without a `catch_all`.
- **Swift codegen**: generated types are now emitted `nonisolated` so they can be used from MainActor-default consumers.
- **Closure resolution in loop headers** is fixed. Expression identity is now keyed by its arena (body vs parameter default) plus id, so closures referenced in a loop header resolve to the correct binding.

```bash
baml feedback --issue "parser panics on nested unions" --repro "class A { ... }"
```

The release also reworks the wrapper build matrix and publishes no-self-update Windows wrapper ZIPs, with no effect on the compiler itself.

Authors: rossirpaulo, 2kai2kai2, codeshaunted, ATX24

## New `baml feedback` command and a simplified `baml login`

0.15.1-nightly.20260727.g · nightly · 2026-07-28

`baml feedback` is a new CLI command for reporting an issue or improvement directly to Boundary, and `baml login` has been reworked into a plain email login.

Highlights:

- **`baml feedback`**: report a problem or a suggestion from the terminal. You can send anonymously or attach your email so you can be notified when a fix ships. Fields come from `--issue`, `--repro`, and `--context`, or you can pipe a JSON object in.
- **`baml login` / `baml auth`**: the old anonymous-first model (start a project, claim it later with `baml auth login`) is gone. `baml login` now runs a straightforward WorkOS device login with your email, and `baml auth` manages your Boundary session. If you filed feedback anonymously first and then log in, past reports are re-attributed to your identified account.
- **Reflection and pattern matching over futures**: `Future<T, E>` values now carry their `T` and `E` at runtime, so `is`/`match` and reflection see the future's type arguments. A spawned body that provably cannot throw is typed `Future<T, never>`, so awaiting it needs no `catch`. This is visible in the standard testing library, where `run_children_parallel` is now `throws never`.
- **Swift codegen**: generated types are emitted `nonisolated` so they can be used from MainActor-default consumers without isolation errors.
- **Closure resolution in loop headers** is fixed, so a closure that references bindings from a `for` loop header now resolves correctly.

```bash
baml feedback --issue "parser panics on nested unions"
baml feedback --anonymous --issue "..." --repro "class A { ... }"
```

Much of the rest of the range is an internal refactor to give generic parameters indexed identities (`ParamTy`), with no intended change to how your code type-checks.

Authors: rossirpaulo, 2kai2kai2, codeshaunted, ATX24

## `string + string` now satisfies the `Add` operator interface, and literal arithmetic is validated through it

0.15.1-nightly.20260727.f · nightly · 2026-07-27

`string` concatenation now goes through the real `baml.ops.Add` interface instead of a compiler-only fast path, so a `string` value satisfies an `Add<string>` bound and can drive generic operator-interface code the same way numeric primitives do.

- **`implement Add<string> for string`** is now a declared impl (in `baml_std/baml/ns_ops/math.baml`, backed by a native `BexStr::concat`). Previously `string` never reached the operator interface, so a driver function that dispatched `+` through the interface rejected `string` operands. A `drv_add("hello", " world")`-style call now resolves.
- **Arithmetic validity comes solely from the interface registry.** Constant folding on two literals now runs only after the operator impl resolves, so a literal pair can no longer make an operation type-check without a matching impl. A compound assignment like `g += "add"` where `g: float` is now rejected during inference with `operator \`+\` cannot be applied to \`float\` and \`"add"\``, and structural operands (arrays, maps) with no impl produce the same `cannot be applied` diagnostic.

```baml
function Greet(name: string) -> string {
    "hello, " + name
}
```

Authors: sxlijin, codeshaunted, rossirpaulo

## The formatter repairs `=>` in `function` signatures, semicolon enum delimiters, and comma-less maps

0.15.1-nightly.20260727.e · nightly · 2026-07-27

The formatter now accepts a JS/TS-style `=>` in a top-level `function` signature and rewrites it to canonical `->`, the same repair it already applied inside lambda expressions. If you paste a signature with the wrong arrow, `baml fmt` fixes it in place instead of erroring.

```baml
function AddOne(x: int) => int {
  x + 1
}
```

The formatter emits `AddOne(x: int) -> int`, and comments between the arrow and the return type survive the rewrite.

Highlights:

- **Enum variant delimiters.** A stray semicolon between enum variants (`enum Status { Pending; Complete; }`) now parses and is repaired to the canonical trailing comma instead of failing.
- **Map literals without commas.** A map entry followed by an unambiguous next key (a word, quote, or `#`) no longer requires an explicit comma to parse.
- **Signed literal types.** Negative numeric literals such as `-42` are now recognized as type atoms, including inside unions, and are printed verbatim.
- **`baml-format: ignore` is honored consistently.** The ignore directive is now checked at the shared `format_salsa` boundary, so CLI and LSP behave identically, and the directive takes effect even when the file still has parse errors. Detection reads parser-classified comment tokens, so directive-like text inside a string does not disable formatting.
- **`default` scoping fix.** Inside an interface default method, the `default` keyword is scoped to the method body and is no longer captured across a closure boundary, so a lambda nested in a default method resolves `default` correctly (BEP-044).

Expression-bodied lambdas without braces remain a hard error for both `->` and `=>`; only the arrow spelling is repaired, not the missing block. The release also refreshes size-gate baselines and adds Slack notification when a nightly release fails, neither of which changes compiler behavior.

Authors: rossirpaulo, sxlijin

## The formatter now repairs a JS/TS-style `=>` in function signatures to `->`

0.15.1-nightly.20260727.d · nightly · 2026-07-27

The formatter now repairs a JS/TS-style `=>` in top-level function signatures back to canonical `->`, matching the behavior it already had for lambda expressions. The parser accepts the `=>` slip and the formatter rewrites it, so `function add(a: int, b: int) => int { a + b }` reformats to use `->`.

Highlights:

- **`=>` in function signatures.** Both the shared parser and the formatter treat `=>` as an unambiguous punctuation slip and normalize it to `->`. Comments attached to the arrow (`-> /* result */ int`) survive the rewrite.
- **Semicolon enum delimiters.** `enum Status { Pending; Complete; }` now parses without errors and the formatter emits the canonical trailing-comma form.
- **Comma-free map literals.** A map literal that omits commas between entries (`{ "left": 1 "right": 2 }`) is accepted when the next token unambiguously starts another key, and the formatter inserts the commas.
- **Negative literal types.** Signed numeric literals such as `-42` are now representable in the formatter's type AST, including as union members, so files that use them format instead of erroring.
- **`baml-format: ignore` honored consistently.** The ignore directive is now checked at the shared library boundary and before the parse-error gate, so CLI and LSP behave identically and the directive can protect a file that does not fully parse. The formatter default indent is fixed at four spaces via the exported `CANONICAL_INDENT_WIDTH`.

Expression-bodied lambdas without braces are still rejected; the diagnostic reports an expected `lambda body '{'`. The rest of this release is CI and release-pipeline plumbing (Slack failure notifications, guarded release-tail conditions, refreshed size-gate baselines) with no effect on the language or generated code.

Authors: rossirpaulo, sxlijin

## Release-only change to the npm publish workflows, no BAML behavior change

0.15.1-nightly.20260727.c · nightly · 2026-07-27

This nightly contains no changes to BAML itself. The only diff is to the internal npm publishing workflows (`publish2-nodejs-sdk.yaml` and `publish2-web-sdk.yaml`), which now call `actions/setup-node@v6` directly instead of the repository's composite `setup-node` action. The composite always enabled pnpm caching, whose post-job cache save failed after a successful publish since these jobs never install a pnpm store. A contract test was added to keep the npm publishers off the unused pnpm cache. Nothing here affects generated clients, the CLI, or the language.

Authors: rossirpaulo

## CI-only: npm SDK publishers stop enabling an unused pnpm cache

0.15.1-nightly.20260727.b · nightly · 2026-07-27

No user-facing changes. The Node.js and web TypeScript SDK publishers (`publish2-nodejs-sdk.yaml`, `publish2-web-sdk.yaml`) now call `actions/setup-node@v6` directly with `registry-url` set to `https://registry.npmjs.org`, instead of the repository's `setup-node` composite. The composite always enabled pnpm caching, whose post-job save step failed after a successful publish because these jobs never install a pnpm store. A contract test (`test_npm_publishers_do_not_enable_an_unused_pnpm_cache`) was added to keep the pnpm cache off these publishers. This only affects the release pipeline; the published packages are unchanged.

Authors: rossirpaulo

## Exhaustiveness distinguishes class type arguments, and function-typed patterns must be untested

0.15.1-nightly.20260727.a · nightly · 2026-07-27

Exhaustiveness checking now treats a class's type arguments as significant. `Box<int>` and `Box<string>` are distinct constructors, so a `match` arm for one no longer counts as covering the other.

**Highlights**

- **Invariant type arguments in exhaustiveness.** A `match` over both `Box<int>` and `Box<string>` now needs an arm (or wildcard) for each. Constructors that differ only by an unresolved or arity-mismatched type argument stay lenient, so an unresolved pattern does not stack a spurious non-exhaustive error on top of its own resolution error.
- **Function-typed patterns must be untested.** Testing a value against a function type is now rejected with `FunctionTypedPatternNotTestable` unless the pattern is the final arm of an exhaustive, guardless, non-`Or` `match`. There, coverage already proves the match, so no runtime test is emitted. The runtime cannot yet reconstruct a callable's signature (a bound method reconstructs nothing, a closure from a generic frame reconstructs a coarsened type), so such a test would silently misroute those values. Relatedly, `ArrayIterator.next` now indexes the backing array directly (`self.arr[self.idx]`) instead of gating each element on a refutable `let value: T` test, so arrays of function-typed elements iterate.
- **`Self` in body positions.** Inside interface methods and `implements`-block methods, `Self` written in annotations, patterns, and explicit type arguments now resolves. It becomes the rigid `Self` variable for an interface's own method, or the block's `for` target for an implements block. Plain class methods are unchanged.
- **Projecting off an interface is an error.** Writing `Iterator.Element` (the interface itself as the base of a dotted projection) now reports `InterfaceProjectionBase`. The base of a projection is an implementor, so use a concrete type, a bounded type variable, `Self`, or the qualified form `(Base as Iterator).Element`.
- **Associated-type bindings survive partial and stream expansion.** A use-site binding like `Iterator<Item = int>` is now carried through PPIR round-tripping, so synthesized `$stream` fields materialize at the pinned type the source spelled rather than an under-pinned one.

Authors: 2kai2kai2, aaronvg, rossirpaulo

## `string + string` concatenation is now backed by a real `baml.ops.Add<string>` impl

0.15.1-nightly.20260727.f · nightly · 2026-07-27

String concatenation with `+` now resolves through the operator interface registry via a new `implement Add<string> for string`, instead of being a compiler fast-path that never satisfied an `Add` bound. The result is the same concatenation you already get, but `string` now behaves like the other operand types under the interface machinery.

```baml
"hello" + " world"
```

- **Operator validity comes only from the interface registry.** Arithmetic constant folding now runs only after the operand's `baml.ops.{Add,Subtract,Multiply,Divide,Remainder}` impl resolves. Previously, folding two literals could make an operation type-check without a matching impl.
- **Invalid mixed-type arithmetic is caught during inference.** A compound assignment like `g += "add"` where `g` is `float` now fails with `operator `+` cannot be applied to `float` and `"add"``, rather than slipping through folding.

Authors: sxlijin, codeshaunted, rossirpaulo

## The formatter repairs `=>` to `->` in top-level `function` signatures

0.15.1-nightly.20260727.e · nightly · 2026-07-27

The formatter now accepts a JS/TS-style `=>` in a top-level `function` signature and rewrites it to canonical `->`, matching the repair it already performed for lambda arrows. Write the slip and format:

```baml
function add(a: int, b: int) => int {
    a + b
}
```

formats to `function add(a: int, b: int) -> int { ... }`. Comments sitting between the arrow and the return type survive the rewrite.

Other changes:

- **Enum variant delimiters.** A semicolon after an enum variant (`enum Status { Pending; Complete; }`) is now accepted and normalized to a comma.
- **Map literals.** A missing comma between map entries is accepted when the next token unambiguously starts another key, instead of raising a parse error.
- **Negative literal types.** Signed numeric literals such as `-42` are now handled as type atoms, so unions like `-1 | 0 | 1` format correctly.
- **`baml-format: ignore` directive.** The escape hatch is now honored at the shared library boundary, so CLI and LSP behavior stay in lockstep, and it is checked before the parse-error gate so it can protect an incomplete or intentionally non-canonical file.
- **`default` scoping (BEP-044).** Inside an interface default method, `default` no longer leaks across a closure boundary; the binding is restored after lambda inference so it stays scoped to the method body.

Expression-bodied lambdas without braces remain a hard syntax error with either arrow spelling. The default formatter indentation is documented as four spaces.

Authors: rossirpaulo, sxlijin

## The formatter repairs `=>` in function signatures and other unambiguous punctuation slips

0.15.1-nightly.20260727.d · nightly · 2026-07-27

The formatter now repairs a JS/TS-style `=>` in a top-level function signature to the canonical `->`, matching the permissive handling that already applied to lambda expressions. Writing `function AddOne(x: int) => int { ... }` no longer fails formatting; it comes back as `-> int`.

Highlights:

- **Fat arrow in signatures.** `=>` is accepted in function declarations and rewritten to `->`. Comments sitting between the arrow and the return type survive the rewrite.
- **Enum variant delimiters.** A semicolon after an enum variant (`enum Status { Pending; Complete; }`) is now accepted and normalized to a trailing comma instead of being reported as an error.
- **Map literal commas.** A missing comma between map entries is tolerated when the next token unambiguously starts a new key, so `{ "left": 1 "right": 2 }` formats cleanly.
- **Negative literal types.** Signed numeric literals such as `-42` in union types now print verbatim rather than tripping the formatter's strong AST.
- **`baml-format: ignore` is consistent.** The ignore directive is now checked at the shared library boundary before the parse-error gate, so it disables formatting the same way in the CLI and the LSP, and it protects a file even when that file still has parse errors.

Expression-bodied lambdas without braces remain a syntax error, for both `->` and `=>`.

```baml
function AddOne(x: int) => int {
  x + 1
}
```

Authors: rossirpaulo, sxlijin

## Release-pipeline fix for npm publishing, no changes to BAML itself

0.15.1-nightly.20260727.c · nightly · 2026-07-27

This nightly contains only CI changes and does not affect BAML behavior. The npm publishers (`publish2-nodejs-sdk.yaml` and `publish2-web-sdk.yaml`) now use `actions/setup-node@v6` directly instead of the repository's composite action, because that composite always enabled pnpm caching and its post-job cache save failed after a successful publish when no pnpm store was installed. A contract test was added to keep both publishers from re-enabling the unused pnpm cache.

Authors: rossirpaulo

## npm SDK publishers no longer fail their post-job pnpm cache save

0.15.1-nightly.20260727.b · nightly · 2026-07-27

This nightly contains no changes to BAML itself. It fixes the Node.js and web SDK publish workflows (`publish2-nodejs-sdk.yaml`, `publish2-web-sdk.yaml`), which previously used the repository's composite `setup-node` action. That composite always enables pnpm caching, and its post-job cache save failed because these publishers only use npm and never install a pnpm store. Both jobs now use `actions/setup-node@v6` directly with `registry-url` set to `https://registry.npmjs.org`. A release pipeline contract test was added to keep the pnpm cache out of these publishers. Nothing in the installed SDKs changes.

Authors: rossirpaulo

## Exhaustiveness checking now distinguishes generic class instantiations like `Box<int>` from `Box<string>`

0.15.1-nightly.20260727.a · nightly · 2026-07-27

`match` over a generic class now treats each type-argument instantiation as its own case, so exhaustiveness and reachability account for the difference between `Box<int>` and `Box<string>`.

**Highlights**

- **Type-argument-aware exhaustiveness.** `match` on a generic class now treats each instantiation as a distinct case, so `Box<int>` and `Box<string>` are separate arms and exhaustiveness accounts for the difference. Because generics are invariant, a `Box<T>` arm does not cover a `Box<int>` scrutinee. When a pattern's type args failed to resolve (arity mismatch, or an `Unknown`/`Error` recovery arg), coverage stays lenient so you get only the resolution error, not a stacked non-exhaustiveness error.
- **Function-typed patterns are rejected where they would emit a runtime test.** A pattern that claims a function-typed value now errors with `FunctionTypedPatternNotTestable` unless it sits in the one sound spot: the final arm of an exhaustive, guardless, non-`Or` `match`, whose test is elided because coverage already proved every reaching value matches. The runtime cannot yet faithfully reconstruct every callable's signature (bound methods reconstruct nothing, closures from generic frames reconstruct a coarsened type), so such a test would silently misroute those values.
- **Clearer error for projecting off an interface.** Writing `Iterator.Element` (the interface itself as the projection base) now reports `InterfaceProjectionBase`. A projection's base must be an implementor: a concrete type, a bounded type variable, or `Self`. To name the interface explicitly, use a qualified projection like `(Base as Iterator).Element`.
- **Body-position `Self` resolves in annotations, patterns, and explicit type arguments.** Inside an interface's own method, body-position `Self` now lowers to the rigid `Self` type variable; inside an `implements` block it lowers to the block's `for` target. Plain class methods and free functions are unchanged.
- **Associated-type bindings survive the PPIR round-trip.** Bindings written at a use site, such as `Iterator<Item = int>`, are now carried through partial/stream expansion verbatim instead of being dropped, so synthesized `$stream` fields materialize at the type you actually spelled.

Internally this release also adds a coarse `IsTypeTag` MIR rvalue (splitting the deliberately container-blind tag check out of `IsType`) and switches `ArrayIterator.next` to direct indexed element access. These are compiler-internal and do not change observable behavior.

Authors: 2kai2kai2, aaronvg, rossirpaulo

## `string + string` now resolves through the `Add` operator interface

0.15.1-nightly.20260727.f · nightly · 2026-07-27

String concatenation with `+` is now backed by a real `implement Add<string> for string`, so `string` satisfies an `Add` bound and dispatches through the same operator interface as the numeric primitives instead of being a compiler fast-path only. A `string` value passed to a function that adds through `baml.ops.Add` now type-checks and runs.

- **`Add<string> for string`**: concatenation is no longer special-cased away from the interface path, so `string` reaches the operator drivers like the numeric types do.
- **Operator validity comes from the interface registry**: arithmetic type checking now resolves the impl before any constant folding, so two literals can no longer make an operator valid without a matching impl. Invalid combinations are rejected consistently, including through compound assignment. `float += "add"` now reports `operator '+' cannot be applied to 'float' and '"add"'` (E0004) during type inference.

```baml
function Greet(name: string) -> string {
  "hello " + name
}
```

Authors: sxlijin, codeshaunted, rossirpaulo

## The formatter repairs `=>` to `->` in function signatures and widens its repair coverage.

0.15.1-nightly.20260727.e · nightly · 2026-07-27

The formatter now accepts a JS/TS-style `=>` in a top-level function signature and rewrites it to the canonical `->`, matching the permissive spelling already allowed in lambda expressions. Several other unambiguous punctuation slips are now repaired instead of rejected.

**Highlights**

- **`=>` in function signatures.** `function add(a: int, b: int) => int { ... }` parses and formats to `->`. Comments sitting between the arrow and the return type survive the rewrite.
- **Semicolon-delimited enum variants.** `enum Status { Pending; Complete; }` is accepted and formatted back to comma-delimited variants.
- **Comma-less map entries.** A map literal whose entries are separated only by whitespace (for example `{ "left": 1 "right": 2 }`) is accepted when the next token unambiguously starts another key.
- **Negative literal types.** A signed numeric literal used as a type atom, such as `-42` in a union, now round-trips through the formatter without loss.
- **`baml-format: ignore` honored consistently.** The ignore directive is now detected at the shared library boundary from parser-classified comment tokens, so CLI and LSP formatting behave identically, and the directive takes effect even in a file that does not fully parse.

Expression-bodied lambdas without braces remain a hard error. The `=>` relaxation applies to signatures, not to lambda bodies.

```baml
function add(a: int, b: int) => int {
    a + b
}
```

The rest of the range is CI and release-pipeline work with no effect on the compiler or formatter output.

Authors: rossirpaulo, sxlijin

## The formatter repairs `=>` in function signatures and other unambiguous punctuation slips

0.15.1-nightly.20260727.d · nightly · 2026-07-27

`baml fmt` now accepts a JS/TS-style `=>` in a top-level function signature and rewrites it to the canonical `->`, matching the repair it already did for lambda arrows. The parser accepts the alternate spelling permissively and the formatter owns canonicalization, so a signature written with `=>` no longer fails to format.

```baml
function AddOne(x: int) => int {
    x + 1
}
```

The above formats to `function AddOne(x: int) -> int { ... }`.

Highlights:

- **`=>` in function signatures** is repaired to `->`, so the accepted syntax and the formatter stay in lockstep across the CLI and LSP.
- **Enum variants** may be separated by `;` (`enum Status { Pending; Complete; }`); the formatter normalizes the delimiter to a trailing comma.
- **Map literals** no longer require a comma between entries when the next token unambiguously starts another key.
- **Negative numeric literal types** such as `-42` are now recognized as type atoms (for example in unions), so files that use them format instead of being rejected.
- **`baml-format: ignore`** is honored at the shared library boundary, so the CLI and LSP behave identically. The directive is now checked before the parse-error gate, so it can protect an incomplete or intentionally non-canonical file.
- **Braceless lambda bodies** (`(x: int) -> x + 1`) are rejected with a focused required-block diagnostic rather than cascading type errors.

The rest of the release is internal: release-pipeline guards so expected nightly skips no longer poison the release tail, a Slack notification when a nightly release fails, refreshed size-gate baselines, and an LSP formatting path that reuses the existing Salsa input instead of reparsing cloned source.

Authors: rossirpaulo, sxlijin

## Release-pipeline fix so npm SDK publishing no longer fails after a successful publish

0.15.1-nightly.20260727.c · nightly · 2026-07-27

No user-facing changes. This nightly only touches the npm publishing workflows. The Node.js and web SDK publishers (`publish2-nodejs-sdk.yaml`, `publish2-web-sdk.yaml`) now call `actions/setup-node@v6` directly instead of the repository's composite action, which always enabled a pnpm cache whose post-job save step failed after an otherwise successful publish. A contract test was added to keep these publishers from re-enabling the unused pnpm cache. Nothing in the language, runtime, or SDKs changed.

Authors: rossirpaulo

## Release-pipeline fix so npm SDK publishing does not fail after a successful publish

0.15.1-nightly.20260727.b · nightly · 2026-07-27

No user-facing changes. This nightly only touches BAML's release automation: the Node.js and web SDK npm publishers now use `actions/setup-node@v6` directly instead of the repository's composite setup action, which always enabled pnpm caching. Those jobs never install a pnpm store, so the composite's post-job cache save failed after an otherwise successful publish. A regression test in `test_release_pipeline_contract.py` now asserts both publishers use `actions/setup-node@v6` with `registry-url` set to the npm registry and do not enable a pnpm cache.

Authors: rossirpaulo

## Pattern matching now distinguishes generic type arguments

0.15.1-nightly.20260727.a · nightly · 2026-07-27

`match` arms on generic classes now distinguish type arguments. `Box<int>` and `Box<string>` are separate constructors for exhaustiveness checking, so an arm covering one no longer counts as covering the other.

- **Type arguments in pattern matching** (#4203): generics are treated invariantly in match analysis, so a `match` over `Box<int>` is not made exhaustive by an arm for `Box<string>`. A bare `Box` pattern missing its type args, or one whose args failed to resolve, still covers as before, so you get the resolution diagnostic alone rather than a non-exhaustive error stacked on top.
- **Function-typed patterns are rejected where they cannot be tested**: a pattern that would emit a runtime test against a function type now errors unless it is the final arm of an exhaustive, guardless, non-`Or` `match`, whose test coverage already proved and so elides. Callable signatures cannot yet be reconstructed for every value (bound methods, closures from generic frames), so such a test would silently misroute them.
- **`ArrayIterator` (std lib) now yields null and function-typed elements**: it switched to index access (`self.arr[self.idx]`) instead of `at()`-plus-match, so a stored `null` element is yielded instead of ending iteration, and arrays of function-typed elements iterate instead of tripping the new function-typed-pattern rule.
- **Projecting an associated type off an interface is now an error**: writing `Iterator.Element` reports that a projection's base must be an implementor type, a bounded type variable, or `Self`, and points to the qualified form `(Base as Iterator).Element`.
- **Body-position `Self`** in annotations, patterns, and explicit type arguments now resolves inside interface and implements-block methods.
- Associated-type bindings such as `Iterator<Item = int>` are now preserved through partial and `$stream` type expansion instead of being dropped.

The mermaid change (#4214) renders diagrams in BEP proposal content, not authored BAML, so it is out of scope for this changelog.

Authors: 2kai2kai2, aaronvg, rossirpaulo

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

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

Documentation now installs the toolchain with `brew install baml` instead of `brew install boundaryml/tap/baml`, reflecting that the formula lives in Homebrew Core rather than a custom tap. The install script and other install paths are unchanged.

```bash
brew install baml
```

The only other change is an internal warning comment added to the `baml` wrapper crate's `Cargo.toml` about the release consequences of bumping its version. No code or behavior changes ship in this release.

Authors: codeshaunted

## Install docs now use `brew install baml` from Homebrew Core

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

The install instructions across the website and quickstart now recommend `brew install baml` instead of `brew install boundaryml/tap/baml`, since the formula is available in Homebrew Core and no longer requires tapping the custom repository.

```bash
brew install baml
```

The only other change is a comment in the `baml` crate's `Cargo.toml` warning that bumping the wrapper version triggers a `baml-wrapper-*` tag and a Homebrew Core version-bump PR. No behavior changed for BAML code or the toolchain.

Authors: codeshaunted

## Docs now install BAML via Homebrew Core 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 that the formula is available from Homebrew Core rather than the custom tap.

```bash
brew install baml
```

This change is limited to the website and quickstart docs (the intro page installer, `explore.md`, `index.md`, and `quickstart.md`). The only other change is an internal warning comment in the `baml` wrapper `Cargo.toml` cautioning that bumping its version triggers a Homebrew Core version-bump PR. No compiler, runtime, or generated-code behavior changed in this release.

Authors: codeshaunted

## Docs now install the CLI via `brew install baml`

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

The documented Homebrew command is now `brew install baml` instead of `brew install boundaryml/tap/baml`, reflecting the move to the Homebrew Core formula.

```bash
brew install baml
```

This is a docs-only release. The install snippets on the website, quickstart, and explore pages were updated, and a comment was added to the `baml` wrapper crate's `Cargo.toml` warning that a canary release after a version bump there triggers an automated Homebrew Core PR. No BAML language or runtime behavior changed.

Authors: codeshaunted

## 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

## The Homebrew formula passes `brew audit` and installs from either binary layout

0.15.1-nightly.20260725.b · nightly · 2026-07-25

This nightly is packaging-only. The generated Homebrew `baml.rb` formula drops the top-level `version` field (which `brew audit` rejects for versioned binary formulas) and its `install` block now falls back to `bin.install "baml"` when `bin/baml` is absent, so the formula installs regardless of how the release archive lays out the binary. No changes to the BAML language, compiler, or runtime.

Authors: codeshaunted

## `baml update` now errors and points you to `baml toolchain update` or `baml self-update`

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

Running `baml update` no longer does something ambiguous. It now exits non-zero with a message telling you which command you actually want: `baml toolchain update` to move to the latest version of BAML, or `baml self-update` to update the toolchain selector itself.

```bash
$ baml update
`baml update` is ambiguous.
To use the latest version of BAML, run `baml toolchain update`.
To use the latest BAML toolchain selector, run `baml self-update`.
```

Everything else in this nightly is release and CI plumbing with no effect on generated code or runtime behavior: the macOS release now builds the package-manager wrapper for `aarch64-apple-darwin` and `x86_64-apple-darwin` (previously Linux-only), the Homebrew binary tap formula was restored, crates.io publishing moved to a new release environment, and a large batch of SDK test functions were renamed to carry file-scoped prefixes to satisfy a new internal SDK parity lint.

Authors: sxlijin, rossirpaulo, codeshaunted

## First-class C# SDK: BAML functions are now callable from C# via the `baml-bridge` NuGet package.

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

The `baml-bridge` NuGet package ships, so BAML functions are now callable from C# alongside Python, TypeScript, Go, and Java.

**Highlights**

- **C# SDK.** A first-class C# generator (`sdkgen_csharp`) and runtime (`bridge_csharp`) land, published as the `baml-bridge` NuGet package. Generated `.g.cs` clients load the same language-neutral `bridge_cffi` engine that the other dylib-loader SDKs use.
- **`baml.json.path`.** New `path<T>(j, selector)` and `path_or<T>(j, selector, default)` navigate jq-style selectors into a `json` value and coerce the leaf to `T`, throwing `JsonPathError` on a missing field, out-of-bounds index, malformed selector, or type mismatch.
- **`baml.sap.parse`.** Schema-Aligned Parsing, the coercion behind BAML's built-in LLM path, is now public so BAML-authored providers can call `parse<T>(text)`, or `parse_type(t, text)` against a runtime `type` value.
- **`baml.schema.json_schema`.** Lower a BAML `type` to ordinary JSON Schema. Optional fields are omitted from `required`, maps become schema-valued `additionalProperties`, unions become `anyOf`, and recursive class graphs use `$defs` and `$ref`.
- **`baml.ws`.** A minimal text WebSocket client for realtime providers. `connect(url, headers, timeout)` returns a `WsStream` with `send`, `next`, and `close`.
- **Colorized diagnostics.** CLI diagnostics now render with syntax highlighting (the renderer moved from `ariadne` to `miette`), and editors dim BAML namespace prefixes. Several duplicate-diagnostic cascades (duplicate fields, parameters, pattern bindings, and or-pattern bindings) are suppressed so a single mistake reports once.
- **`baml auth`.** A new `baml auth` command supports anonymous-first login, wired to WorkOS through the `BAML_WORKOS_AUTHKIT_DOMAIN` and `BAML_WORKOS_CLIENT_ID` public identifiers captured at build time.
- **`baml self-update`.** Package-manager builds are now compiled with self-update disabled and fail with "self-update is disabled in this build." rather than inspecting install paths.

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

## The generated Homebrew formula now passes `brew audit` and installs correctly

0.15.1-nightly.20260725.b · nightly · 2026-07-25

This nightly only touches Homebrew packaging. The generated `baml.rb` formula drops the top-level `version` stanza (which `brew audit` rejects for binary formulae) and makes `install` fall back to `baml` when `bin/baml` is not present in the extracted archive. If you install BAML through Homebrew, the formula now audits clean and installs the launcher regardless of the archive's directory layout. No language, CLI, or runtime behavior changed.

Authors: codeshaunted

## `baml update` now errors and points you to `baml toolchain update` or `baml self-update`

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

Running `baml update` now fails immediately with a message explaining the command is ambiguous, instead of doing something unexpected. It directs you to `baml toolchain update` to move to the latest version of BAML, or `baml self-update` to update the toolchain selector itself.

```bash
baml update
# error: `baml update` is ambiguous.
# To use the latest version of BAML, run `baml toolchain update`.
# To use the latest BAML toolchain selector, run `baml self-update`.
```

Everything else in this release is internal and does not change observable behavior: a new SDK parity lint and CI job, SDK test renames to satisfy that lint, a Homebrew formula fix, and a switch of the crates.io release environment to `boundary-tools-prod`.

Authors: sxlijin, rossirpaulo, codeshaunted

## Call BAML functions from C# via `baml generate` and the `baml-bridge` NuGet package

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

BAML now has a first-class C# SDK: `baml generate` emits C# client sources, and the runtime ships as the `baml-bridge` NuGet package, so you can call BAML functions from C# the same way you already can from Python, TypeScript, Go, and Java.

**Highlights**

- **C# SDK.** Generated C# clients are produced by `baml generate` and load a language-neutral `bridge_cffi` engine at runtime. The NuGet package is `baml-bridge`, versioned in lockstep with the rest of the release.
- **`baml login`.** New anonymous-first authentication via WorkOS agent registration. The public (non-secret) identifiers `BAML_WORKOS_AUTHKIT_DOMAIN` and `BAML_WORKOS_CLIENT_ID` are baked in at build time from repo variables.
- **Stdlib building blocks for custom LLM providers.** `baml.sap.parse<T>` exposes Schema-Aligned Parsing to BAML-authored providers, `baml.schema.json_schema` lowers a BAML `type` to ordinary JSON Schema, and `baml.ws` adds a minimal text WebSocket client (`connect`, `send`, `next`, `close`) for realtime providers.
- **JSON path access.** `baml.json.path<T>(j, selector)` navigates a jq-style selector (dot, quoted-bracket, and integer-index steps) and coerces the leaf to `T`, throwing `JsonPathError` on a missing field, out-of-bounds index, or type mismatch. `baml.json.path_or` returns a default instead of throwing.
- **Diagnostics.** CLI diagnostics now render with syntax highlighting, and editors dim BAML namespace prefixes so the meaningful part of a qualified name stands out. The diagnostic renderer moved from `ariadne` to `miette`.
- **Self-update opt-out.** Builds can now disable self-update (the package-manager wrappers are built this way). In such builds `baml self-update` fails fast with a message directing you to your package manager instead of reaching the network.
- **Compiler performance.** Cold compiles are now linear, a single warm cache is shared across every command, and editor latency is flatter.
- **Go SDK.** The generated Go SDK surface was expanded toward completeness.

```bash
baml login
```

Several diagnostic-recovery fixes also landed, suppressing cascades from duplicate fields, duplicate parameters, conflicting or-pattern bindings, invalid match patterns, and unresolved class-pattern bindings, so a single mistake produces one error instead of a chain of follow-on noise.

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

## 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 literals, map literals, and class constructors now accept property shorthand: a bare identifier `{ options }` lowers to `{ options: options }`, pulling the value from an in-scope binding of the same name. Shorthand and explicit fields can be mixed in any position.

```baml
function property_shorthand_map(count: int, retries: int) -> map<string, int> {
    { count, retries }
}
```

Highlights:

- **Shorthand in class constructors.** `Config { options }` resolves `options` as both the field name and the value, and nests through multiple classes.
- **Shorthand-specific diagnostics.** A shorthand whose name has no matching binding reports `property shorthand \`options\` requires an in-scope value named \`options\`` with a suggested explicit mapping such as `options: option`. When the value resolves but the class has no matching field, the error reads `class \`Config\` has no field \`option\` for property shorthand. Did you mean \`options: option\`?`.
- **Unknown explicit fields now diagnosed.** Explicitly naming a field a class does not declare, as in `Bar { a: "hello", c: 12 }`, now reports `class \`Bar\` has no field \`c\`` (error code E0007) with near-name suggestions, rather than being silently accepted.
- **Formatting preserves shorthand.** `{ options, explicit: options }` and `Config { options }` round-trip unchanged through the formatter.

The parser tightens for-in header handling so an unparenthesized `for let x in xs { body }` keeps `{ body }` as the loop body, while iterables that legitimately contain a brace, such as `Items { values }`, still parse. Config lowering for `retry_policy` marks its synthesized object as compiler-generated so these field diagnostics do not apply to config keys.

Authors: aaronvg

## Thrown error values keep their qualified class name and fields in diagnostics

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

Error diagnostics no longer collapse a thrown value to its bare class name or an `<error>` placeholder. A caught error read through `ctx.to_string()` now renders structurally, so an `AppError` carrying a nested `Detail` shows as `user.AppError { message: "boom", detail: user.Detail { label: "visible" } }` instead of just `AppError`. Panic tracebacks gain the same treatment: a `DivisionByZero` now prints as `baml.panics.DivisionByZero { dividend: 42 }`.

- **Structural error context.** Thrown non-class values (maps, arrays, and other structural values) render as themselves rather than falling back to `<error>`. This rendering runs inside a native call, so it does not dispatch to user `baml.ToString` overrides.
- **Bounded diagnostics.** Structural error rendering is guarded against cycles and large payloads. A self-referential class truncates to `user.RecursiveError { next: … }`, and deep or wide values collapse to a single `…` so an error report stays bounded.
- **Type-directed union JSON.** `baml.json.to_string<T>` where `T` is a union now selects the first declared member the runtime value belongs to, using the same membership relation as `is` and typed match arms. A class, enum, `image`, or `uint8array` member serializes exactly as it would outside the union, and a value that matches no 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` streams `log.*` events to stdout at a chosen level

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

`baml test` now takes a `--logs` flag that prints BAML `log.*` events to stdout. Logs are off by default, so existing runs are unchanged. Pass a threshold to opt in.

- **`--logs <LEVEL>`** accepts `Off`, `Error`, `Warn`, `Info`, or `Debug` (case-insensitive) and prints every matching log event at or above that level, formatted as `[LEVEL] body`. Both string bodies and structured bodies like `log.warn({"user": "ada"})` are rendered.
- **Exit codes are unchanged.** A failing test still exits non-zero when `--logs` is set, so you can capture logs without affecting pass/fail behavior.
- **Streamed during the run.** stdout is flushed as logs are drained rather than held until the final report, so `baml test --logs INFO > baml-test.log` shows output from a long-running test while it is still executing.

```bash
baml test --logs INFO
```

This release also makes stdlib builtin codegen deterministic and keyword-safe: BAML field names that collide with Rust keywords or contain punctuation (`type`, `self`, `dash-name`) are now mapped to legal, collision-free Rust identifiers while preserving the original field key, and namespace discovery is sorted rather than dependent on per-process hash ordering. This is internal to generated code and does not change BAML source behavior. The remaining commit removes dead internal APIs with no observable effect.

Authors: aaronvg

## `baml test --logs` streams `log.*` output to stdout

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

`baml test` now takes a `--logs LEVEL` flag that prints BAML `log.*` events to stdout. Logs are off by default, so existing output is unchanged unless you opt in.

- **`--logs LEVEL`**: accepts `off`, `error`, `warn`, `info`, or `debug` (case-insensitive). Each captured event prints as `[LEVEL] body`, and events below the threshold are dropped. Structured payloads like `log.warn({"user": "ada"})` are rendered inline.
- **Streaming**: logs are flushed while a test is still running, not held until the final report, so `baml test --logs INFO > baml-test.log` shows output as it happens rather than in one batch at the end.
- **Exit codes unchanged**: passing `--logs` does not alter the pass/fail exit code. A failing test still exits `2` while its captured logs are printed.

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

The rest of the release is internal: a large removal of dead public APIs across the compiler crates, and a codegen refactor that makes native-builtin code generation deterministic and safe for field names that collide with Rust keywords. Neither changes the behavior of code you write.

Authors: aaronvg

## New `baml.sys.collect_garbage()` builtin, plus class-spread inference and SAP parse fixes

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

`baml.sys.collect_garbage()` is a new builtin that forces a major garbage collection and drains queued `cleanup()` finalizers before returning, so a finalizer is observable at the next statement. It is meant for deterministic tests and runtime diagnostics; production code should let the runtime schedule collections.

```baml
function main() -> null throws never {
  baml.sys.collect_garbage()
}
```

**Highlights:**

- **Class spread (`...`)** now infers omitted generic arguments from an exact-class spread, so `Box { ...box_int }` resolves to `Box<int>`. Spreads are also checked nominally and invariantly: spreading a different class or a mismatched type argument (`Left<int> { ...Left<string> { .. } }`) reports a type mismatch. Several codegen fixes keep spread-with-call-result initialization correct at runtime, including a factory called with omitted defaults, where the caller's VM frame was previously corrupted.
- **`baml.llm.parse` fidelity.** Declared nullable container types survive the nullable wrapper, so `ParsedChild[]?` and `ParsedStatus[]?` parse back as their declared types instead of collapsing. A recursive union alias used directly as another union's member (`json | null`) is now flattened, and a response that is a complete JSON string literal (`"Fred"`) decodes to its string value while plain prose stays verbatim.
- **Formatter.** `spawn { … }` expressions reindent as ordinary blocks rather than printing at their original columns, and a trailing lambda or `spawn` argument hugs the call parens (`push(spawn { … });`) instead of forcing the whole call to break.
- **Parser.** `client = …` and `prompt = …` passed as named call arguments no longer misparse as an LLM directive body.
- **Interface method array aliases.** Iterating over an interface method whose return type is a transparent array alias (`type EventStream = Event[]`) now works.
- **Projection base evaluated once.** A projection assignment whose base is a call, such as `store.require().info.title = "triage"`, materializes that base exactly once.

Authors: aaronvg

## Duplicate methods on a class no longer trigger a cascade of follow-on errors

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

Declaring two methods with the same name on a class (for example two `function value(self)` definitions on a `Box`) now reports the duplicate once and resolves calls to that method as an error type, instead of cascading extra diagnostics onto the call sites.

Method lookup previously deferred any name that matched more than once to interface disambiguation, which fit genuinely ambiguous cases across `implements` blocks but misfired when the duplication came from two inherent declarations on the same class. Such a call now yields an error type immediately, so a call like `box.value()` surfaces the real duplicate-method diagnostic and nothing more. This is a diagnostic-quality fix. Valid code and the existing cross-interface ambiguity check (E0121) are unchanged.

Authors: codeshaunted

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

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

This release contains no changes to BAML itself. The only diff adds `environment: release` to the `release-baml-language.yml` GitHub Actions workflow so the Rust SDK trusted-publishing job runs in the `release` environment. Nothing about the language, runtime, or CLI changes for you.

Authors: 2kai2kai2

## Type narrowing is now sound across `spawn`, and diagnostics gain an agent output format

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

Type narrowing no longer applies to a local variable that a `spawn` block can reassign, closing a soundness hole where a narrowed value could change type before its narrowed use.

Highlights:

- **Narrowing is skipped for captured locals.** When a variable is captured by an inner scope that mutates it (for example a `spawn` block), `if (x != null)` and `match` arms no longer narrow it, because the concurrent write could invalidate the refined type. Code that relied on the old, unsound narrowing may now report a type error such as expected `int`, got `int | null`. Snapshot the value first (`let snapshot = x;`) and match on the snapshot to narrow safely.
- **Typed pattern bindings test and bind atomically.** A pattern like `let foo: Foo => ...` now binds the exact value it tested, through a new `narrow_bind` step, so the bound name cannot observe a different value if the source cell is mutated between the test and the bind.
- **Agent-friendly diagnostic output.** New global flags `--output-preset` (`auto`, `human`, `agent`), `--diagnostic-format` (`human`, `agent`, `concise`), `--color`, and `--hyperlinks`, each with a matching environment variable (`BAML_OUTPUT_PRESET`, `BAML_DIAGNOSTIC_FORMAT`, `BAML_COLOR`, `BAML_HYPERLINKS`). The `agent` diagnostic format prints compact, location-first errors without source diagrams. With the default `--output-preset auto`, BAML detects known coding-agent environments and drops color and hyperlinks automatically.
- **Field and method name conflicts no longer cascade.** Declaring a field and a method with the same name on a class now reports a single diagnostic instead of a cascade of follow-on errors.

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

Authors: codeshaunted

## Object and map literals support property shorthand

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

Object constructors and map literals now accept property shorthand, so `{ options }` is equivalent to `{ options: options }` when a value named `options` is in scope.

**Highlights**

- **Shorthand in object constructors and map literals.** A bare identifier in `{ ... }` lowers to the same key/value pair as the explicit form. This works for class constructors (`Config { options }`) and maps (`{ count, retries }`), and you can mix shorthand with explicit fields in any position (`{ options, retries: retries + 1, label }`).
- **Shorthand-specific diagnostics.** If a shorthand value has no in-scope binding, the error names the requirement and suggests explicit mappings from nearby names, for example `property shorthand `options` requires an in-scope value named `options`. Did you mean `options: option`?`. If the value resolves but the class has no matching field, you get `class `Config` has no field `option` for property shorthand. Did you mean `options: option`?`.
- **Explicit unknown constructor fields are now reported.** Naming a field a class does not declare, such as `Bar { a: "hello", c: 12 }`, now raises E0007 pointing at the field name, with singular/plural and missing-suffix suggestions (`re` suggests `red`, `green`).
- **For-in parsing tightened.** Unparenthesized `for let x in xs { body }` stays unambiguous while iterables like `Values { items }.items` still parse, since an object literal is only permitted before the loop body when the braces are followed by continuing syntax.

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

function build_config(retries: int) -> Config {
    let options = { "mode": "safe" };
    Config { options, retries }
}
```

Authors: aaronvg

## Caught errors and typed-union JSON now preserve the thrown value's structure

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

`ctx.to_string()` on a caught error now renders the thrown value structurally instead of collapsing it to a bare class name or an `<error>` placeholder, so a reported failure carries the same detail as one escaping directly from `baml run`.

**Highlights**

- **Structural error context.** A thrown class instance now renders with its qualified name and recursive fields, for example `user.AppError { message: "boom", detail: user.Detail { label: "visible" } }`. Non-class thrown values (maps, arrays) render structurally too. This rendering runs in a native call and does not dispatch to your `baml.ToString` overrides, so an overriding class still shows its real structure in the diagnostic. Panic traces gain the same treatment: `baml.panics.DivisionByZero` now prints as `baml.panics.DivisionByZero { dividend: 42 }`.
- **Bounded rendering.** Diagnostic rendering is capped at depth 32 and 256 nodes and detects object cycles, degrading to a `…` marker rather than recursing forever. A self-referential class renders as `user.RecursiveError { next: … }`, and a 300-element array uses a single trailing ellipsis.
- **Type-directed union JSON.** `baml.json.to_string<Payload>(value)` on a union type now selects the first declared member that contains the runtime value (the same membership relation as `is` and typed match arms) and serializes as that member. A class, enum, media, or `uint8array` member serializes exactly as it would outside the union, and a value that matches no member raises `baml.json.JsonSerializationError` instead of silently serializing structurally.

```baml
class Record {
  name string
}

type Payload = Record | string

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

Authors: aaronvg

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

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

`baml test` gains a `--logs <LEVEL>` flag that routes `log.*` events from your tests to stdout. Logging is off by default, so existing runs are unchanged.

- **`--logs <LEVEL>`** accepts `Off` (default), `Error`, `Warn`, `Info`, or `Debug`. Only events at or above the threshold print, each as a `[LEVEL] message` line. Both message strings and structured payloads (for example `log.warn({"user": "ada"})`) are rendered. Level parsing is case-insensitive, so `--logs INFO` and `--logs info` are equivalent.
- Logs stream while the test runs. stdout is flushed periodically (roughly every 50ms) so you see output from long-running tests as it happens, not only alongside the final PASS/FAIL report.
- Exit codes are unaffected. A failing test still exits with code 2 whether or not `--logs` is set.
- Output goes to stdout, so you can capture the raw stream: `baml test --logs INFO > baml-test.log`.

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

This release also makes compiler builtin codegen deterministic and keyword-safe: BAML field names that collide with Rust keywords or use punctuation (`type`, `self`, `Self`, `dash-name`, and similar) now map to stable, collision-free generated identifiers, and package namespace discovery is sorted rather than dependent on hash-set iteration order. The remaining changes are internal dead-code removal with no observable behavior change.

Authors: aaronvg

## `baml test` gains a `--logs` flag that prints BAML `log.*` events to stdout

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

`baml test` now takes a `--logs LEVEL` flag that prints your `log.*` events to stdout as tests run. Logs are off by default, so existing output is unchanged unless you opt in.

**Highlights**

- **`--logs LEVEL`** accepts `Off`, `Error`, `Warn`, `Info`, or `Debug` (case-insensitive). Events at or above the chosen level are printed as `[LEVEL] message` lines. `Off` is the default. Object payloads, such as `log.warn({"user": "ada"})`, are rendered too.
- **Streaming.** Logs are drained and flushed periodically while a test runs, not held until the final report, so a long test emits its logs as it goes. Because they go to stdout, you can keep the raw stream with a redirect.
- **Exit codes are preserved.** Passing tests still exit 0 and failing tests still exit 2 regardless of `--logs`.

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

The rest of this release is internal: the compiler's native codegen was made deterministic and keyword-safe (field names that collide with Rust keywords or reserved words are now escaped reversibly), and a batch of dead internal APIs was removed. Neither changes the behavior of your BAML code.

Authors: aaronvg

## `baml.sys.collect_garbage()` lands, plus class-spread and SAP parsing fixes

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

This release adds `baml.sys.collect_garbage()`, a builtin that forces a full major collection and drains queued `cleanup()` finalizers before returning. It is meant for deterministic tests and runtime diagnostics; production code should let the runtime schedule collections on its own.

```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
}
```

After `collect_garbage()` returns, `log` already contains both `"created"` and `"cleaned"`.

The rest of the release is correctness fixes:

- **Class spread** (`Class { ...source }`) now infers otherwise-omitted generic arguments from the spread source, so `Box { ...box_int }` resolves to `Box<int>`. A spread from a different class or an incompatible generic argument is now a type mismatch (`expected Left<int>, got Right<int>`). Several runtime bugs where spreading a factory call with omitted defaults corrupted the caller's VM frame are also fixed.
- **SAP string parsing** decodes a complete JSON string response: `ParseString$parse(`"Fred"`)` now returns `Fred`. Plain text, partial strings, and prose containing JSON stay verbatim.
- **SAP union aliases**: a recursive union alias used directly as a union member (`json | null`) now flattens instead of hiding a nested union, and nullable container types like `json[]?` and `map<string, json>?` are preserved through parsing.
- **Parser**: `client` and `prompt` used as named call arguments, as in `Ask("hi", client = c)`, are no longer misread as an LLM function body.
- **Projection assignment**: the base of `store.require().info.title = ...` is now evaluated exactly once.
- Interface methods whose return type is a transparent array alias (`type EventStream = Event[]`) are now iterable.

The formatter also gained real support for `spawn { … }`: relocated spawn bodies reindent like any block, and a trailing lambda or `spawn` argument hugs the call parens (`push(spawn { … })`) instead of forcing the whole call to break.

Authors: aaronvg

## Duplicate methods on a class now report one error at the call site instead of a cascade

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

Calling a method that a class declares more than once, such as two `function value(self)` on the same class, now produces a single error at the call site instead of a cascade of follow-on diagnostics. No user action is needed.

Authors: codeshaunted

## CI-only change to the Rust SDK trusted publishing workflow

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

This nightly contains no user-facing changes. The only change adds an `environment: release` setting to the Rust SDK publishing job in the release workflow, which affects how trusted publishing runs in CI and does not alter any BAML behavior.

Authors: 2kai2kai2

## Agent-friendly CLI output presets and sound type narrowing across `spawn`

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

A new `--output-preset` flag and `BAML_OUTPUT_PRESET` environment variable give the CLI a family of output dials tuned for coding agents, and type narrowing is now sound across `spawn`.

Highlights:
- **Output dials.** `--output-preset` takes `auto`, `human`, or `agent`; `auto` selects the agent preset when a known coding-agent environment is detected, and `human` otherwise. Three independent overrides sit on top: `--color` (auto/always/never), `--hyperlinks` (auto/always/never), and `--diagnostic-format` (human/agent/concise). Each also reads a matching environment variable: `BAML_COLOR`, `BAML_HYPERLINKS`, `BAML_DIAGNOSTIC_FORMAT`. `--color` now controls color only. Hyperlinks moved to their own `--hyperlinks` flag.
- **Agent diagnostic format.** `--diagnostic-format agent` renders compact, location-first diagnostics without the ariadne source diagrams, which the `agent` preset picks for you.
- **Narrowing across `spawn`.** A local captured by a `spawn` block is no longer narrowed by a preceding null check, `is` test, or match arm, because the spawned task can reassign it. Code that relied on the previous unsound narrowing will now see the variable keep its declared union type. Uncaptured locals (including a snapshot copy of a captured one) still narrow as before.
- **Field/method name conflict.** A class that declares a field and a method with the same name reports the conflict once instead of cascading follow-on errors at each use site.

```bash
# Compact diagnostics for a coding agent, no color
baml check --diagnostic-format agent --color never
# Or drive it all from the environment
BAML_OUTPUT_PRESET=agent baml check
```

Authors: codeshaunted

## 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

## Add `swift` code generation target

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

`baml generate` can now emit a Swift SDK (the `BamlBridge` SwiftPM runtime) via the new `swift` output type.

**Highlights:**

- **`swift` output type.** A new `OutputType::Swift` (serialized as `swift`) is wired through `baml generate`, backed by the `sdkgen_swift` generator and a Swift bridge. Point a generator at `swift` to get Swift source alongside the existing TypeScript, Python, Go, Java, and Rust targets. The Swift toolchain pieces (protoc-gen-swift bindings, the `BamlBridgeFFI` xcframework, the SDK test harness) are macOS-only.
- **Item-tree access moved behind the PPIR `item_data` firewall.** This is an internal compiler refactor: every consumer that used to read the raw item tree now goes through per-item queries (`function_data`, `class_data`, `enum_data`, and friends). Compiled output and diagnostics are unchanged. The only observable effect is finer-grained incremental invalidation, per item rather than per file, so an edit to one declaration re-runs less downstream work.

Authors: ATX24, 2kai2kai2

## New `swift` code generation target for a BamlBridge/SwiftPM SDK

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

This release adds `swift` as a code generation target, so a generator can now emit a Swift SDK backed by the `BamlBridge` SwiftPM runtime alongside the existing TypeScript, Python, Go, Java, and Rust targets.

- **`swift` output type.** `OutputType` gains a `Swift` variant (serialized as `swift`), and `baml generate` now routes it through `sdkgen_swift` to produce Swift sources. This target is early: its build-side codegen soft-fails into build diagnostics rather than hard-erroring, and its toolchain tests are macOS-only, so treat generated Swift output as preliminary.

The rest of the release is an internal refactor with no user-visible behavior change: the `ItemTree` firewall transition is finished, making the raw `file_item_tree` `pub(crate)` in both the HIR and PPIR crates so every downstream consumer (TIR, MIR, emit, LSP, CLI, tests) now reads items through the `item_data` firewall queries. Per the architecture notes, the item layer is fully behind the firewall while the scope tree is not yet, so incremental recompute behavior for edits like comment-only changes is unchanged.

Authors: ATX24, 2kai2kai2

## Release plumbing only: no user-facing changes

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

This nightly contains only changes to the release CI workflows. The `release-baml-language.yml` workflow switched from a `workflow_run` trigger to being dispatched explicitly by CI after a green push to canary, because crates.io will not mint a Trusted Publishing token for `workflow_run` runs. A `source_sha` input was added so the release pins to the exact commit CI validated. Nothing in the language, runtime, or client generators changed, so there is nothing to update in your BAML code.

Authors: 2kai2kai2

## Integer and bigint literals gain hex, octal, and binary forms with underscore separators

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

Integer and bigint literals can now be written in hexadecimal, octal, and binary, with underscore separators allowed for readability.

**Highlights**

- **Numeric literals**: hex, octal, and binary `int`/`bigint` literals with underscore separators are now accepted (#4065).
- **`${...}` interpolation**: type errors inside interpolations are now reported instead of being silently skipped (B-836, #4096). Code that referenced a mistyped or out-of-scope value inside an interpolation now fails to compile where it previously slipped through.
- **LLM client defaults**: client defaults and validation are centralized in one place (B-868, #4063), so invalid or partial client configs surface consistently rather than per call site.
- **New SDK targets**: this range lands the first slices of the Rust (`baml_bridge` crate), Java/JVM (`com.boundaryml:baml-bridge`), Go, C++, and TypeScript Web/Workers bridges, along with their release and verification workflows. These are early and still being wired into the release graph.

Much of the rest of the range is release plumbing and compiler internals: cold-compile time dropped substantially (parallel check/MIR/emit, inference dedup) with byte-identical output, and the CI build matrices for the SDK bridges now derive from a single `release/platforms.json` platform contract.

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

## New Rust, C++, Java, and Go SDKs plus browser and Workers runtimes arrive on nightly

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

This nightly publishes the first end-to-end slices of BAML SDKs for Rust, C++, Java/JVM, and Go, and adds browser and Cloudflare Workers runtimes to the TypeScript SDK. On the language side, integer literals gain new bases and digit separators, and type errors inside `${...}` interpolations are now reported.

**New, nightly-only SDKs**

- **Rust:** a thin `baml_bridge` crate packaged for crates.io that dlopens the shared engine runtime. This slice wires class methods, unbounded generics, and host callbacks.
- **C++:** a `baml-cli generate` flow that vendors the bridge header into your source tree and loads the shared `bridge_cffi` runtime. Adds `baml::Union` and `baml::match`, typed error unions, `{name}Async` siblings with `baml::Future` and `co_await`, static and instance methods, and host callables via `std::function`.
- **Java/JVM:** the `com.boundaryml:baml-bridge` Maven artifact family, with a Kotlin-friendly variant (`baml-bridge-kotlin`, JSpecify nullness).
- **Go:** a generated SDK plus a standalone runtime release. This is distinct from the bridge SDKs above.
- **Browser and Workers:** `@boundaryml/baml-bridge-web`, a WASM package that runs the TypeScript SDK in browsers and Cloudflare Workers.

Each of these is new on the nightly channel and shipped as an early slice, so treat them as preview.

**Language changes**

- **Integer literals:** hex, octal, and binary int and bigint literals now parse, with `_` digit separators.
- **Interpolation type checking:** type errors inside `${...}` interpolations are now reported (`B-836`) instead of passing silently.
- **LLM client defaults:** default resolution and validation for LLM clients was centralized in the runtime (`B-868`).

**Compiler internals**

- `baml.ops` mathematical operator interfaces (`B-289`) and `baml.AnyFunction` with `reflect.signature` and `reflect.call_any` (BEP-062) landed as compiler2 slices. These are internal building blocks and are not confirmed as callable from user `.baml` code in this nightly.

**Performance**

- Cold compiles are now around 0.27s.

Note: the Node bridge sources moved from `sdks/nodejs/bridge_nodejs` to `sdks/typescript/bridge_typescript`. The published npm package name is unchanged.

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

## Swift joins the code generators with output_type "swift"

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

A new `swift` code generation target lands: a generator with `output_type "swift"` now emits a Swift SDK backed by the `BamlBridge` SwiftPM runtime, and `baml generate` writes those files like any other target.

- **`output_type "swift"`**: the `OutputType` enum gains a `Swift` variant (serialized as `swift`), wired through `baml generate` via `sdkgen_swift::to_source_code_with_bytecode`. This target is early. The Swift toolchain tests are macOS-only and codegen soft-fails into build diagnostics for now, so treat it as preview rather than a stable SDK.
- **Internal firewall migration (no behavior change)**: the second commit finishes moving every downstream consumer (emit, LSP actions, CLI bytecode cache) off the raw `file_item_tree` onto the PPIR `item_data` queries (`file_functions`, `class_data`, `scope_owner`, and friends), and makes `file_item_tree` `pub(crate)` in both HIR and PPIR. This is a refactor toward finer-grained incremental recompute. It does not change compiler output or diagnostics you observe. A `function_llm_prompt` query and a few name-span source maps were added to support it.

If you do not target Swift, this release is invisible to your BAML code.

```baml
generator my_app {
  output_type "swift"
}
```

Authors: ATX24, 2kai2kai2

## New `swift` generator output type for a SwiftPM SDK

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

The code generator gains a `swift` `output_type`, emitting a `BamlBridge` SwiftPM SDK alongside the existing TypeScript, Python, Go, Java, and Rust targets.

- **`swift` output type.** `OutputType::Swift` is now a recognized generator target (serialized as `swift`), and `baml generate` routes it through the new `sdkgen_swift` backend to produce Swift sources. This is an early target: its build-time codegen soft-fails into build diagnostics rather than hard-erroring, and the toolchain tests run on macOS only.
- **`ItemTree` firewall transition finished.** Every downstream consumer (emit, LSP actions, CLI bytecode cache) now reads item data through the PPIR `item_data` queries (`file_functions`, `class_data`, `enum_data`, and friends) instead of the raw item tree, which is now `pub(crate)` in both HIR and PPIR. This is an internal refactor with no change to compiled output or diagnostics; it is a plumbing change that sets up finer-grained incremental invalidation, not a behavior change you can observe today.

No BAML source syntax changed in this release.

Authors: ATX24, 2kai2kai2

## Internal release-pipeline change only, no user-visible changes

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

This nightly contains only CI and release-workflow plumbing with no effect on BAML itself. The language release now fires through an explicit `gh workflow run` dispatch from the `CI - BAML Language` workflow after a green push to canary, replacing the previous `workflow_run` trigger on `release-baml-language.yml`. The reason is that crates.io will not mint a Trusted Publishing token for `workflow_run` runs, so the old trigger could never publish `baml_bridge`. Nothing about the compiler, runtime, CLI, or generated code changed.

Authors: 2kai2kai2

## Integer and bigint literals now accept hex, octal, and binary forms with underscore separators

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

Hex, octal, and binary integer and bigint literals now accept underscore separators (#4065), so you can write numeric constants in the base and grouping that reads most clearly.

**Highlights**

- **Type errors inside `${...}` interpolation are now reported (#4096).** Expressions embedded in interpolated strings are type-checked, so a bad reference or type mismatch inside `${...}` surfaces as a diagnostic instead of being silently accepted.
- **LLM client default resolution and validation were consolidated into a single path (#4063).**
- **New language SDKs are being scaffolded and released.** This range adds generated SDK and runtime plumbing for Go (#4067), Rust (#4045, #4093), Java/JVM with Kotlin-friendly nullness annotations (#4060, #4106), C++ (bridge header, tarball packaging, async call forms, host callables), and a Web/Workers TypeScript build (#4052, #4006). These are additive and do not change existing SDK behavior.
- **The Node bridge moved from `sdks/nodejs/bridge_nodejs` to `sdks/typescript/bridge_typescript`.** This is an internal directory rename; the published npm package is unaffected, but local references to the old path will break.

Under the hood, cold compile time dropped further (0.63s to 0.27s cold, byte-identical output, #4073) via parallel check/MIR/emit and inference dedup, and `ItemTree` salsa firewall queries were introduced across TIR and MIR (#4064, #4092). These are performance and internal-structure changes with no observable difference in compiler output.

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

## Hexadecimal, octal, and binary integer literals with digit separators

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

BAML now parses hexadecimal, octal, and binary integer and `bigint` literals, with underscore digit separators for readability.

**Highlights**

- **Non-decimal integer literals.** `int` and `bigint` values can now be written in hex, octal, and binary, and underscore separators are allowed between digits. (#4065)
- **Type errors inside `${...}` interpolation.** The type checker now reports errors in interpolated expressions that it previously skipped, so a bad reference inside a string interpolation surfaces at check time instead of slipping through. (#4096, B-836)
- **Centralized LLM client defaults.** Default client resolution and validation now run through one code path, so invalid client configuration is rejected consistently rather than in scattered spots. (#4063, B-868)
- **Faster cold compiles.** Cold-compile time dropped substantially across this range, roughly from 2.4s to 0.81s and then further to about 0.27s cold, with the later pass producing byte-identical output. (#4058, #4073)
- **New SDK bridges landing.** Initial Rust, C++, Java (with Kotlin-friendly nullness), and Go bridges arrived this range, along with a Web and Cloudflare Workers TypeScript build. These are incremental first slices, not yet at parity with the Python and Node SDKs.

Most of the remaining changes are build and CI plumbing that do not affect your BAML code. The Node SDK tree moved from `sdks/nodejs/bridge_nodejs` to `sdks/typescript/bridge_typescript`, and several release build matrices now derive their target lists from a single `release/platforms.json` platform contract.

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

## 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 that share a name now report one unified conflict

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

BAML now treats every top-level declaration with the same spelling as sharing a single namespace, so a name reused across a `type` and a `client` (or any other declaration kind) produces one complete diagnostic instead of separate type-only and value-only errors.

**Highlights**

- **One conflict across mixed declaration kinds.** A name defined as a `class`, `enum`, `type`, and `function` across different files now yields a single diagnostic that lists every conflicting location, rather than fragmented per-lookup errors. The message reads `Name \`Backend\` defined 2 times as: client, type`, with locations sorted by file path.
- **Source-kind reporting for lowered declarations.** `client` and `retry_policy` declarations are lowered to top-level `let`s internally, but conflict diagnostics now report their source kind (for example `client`) instead of the internal `let`, so the error names what you actually wrote.
- **Cross-namespace names stay legal.** The same name in two different `ns_*/` directories does not conflict, since those are distinct BAML namespaces.
- **Tests remain function-scoped.** Two tests named the same for different functions are still legal, because a test's identity is `functionName/testName`.

Authors: hellovai

## Duplicate-name diagnostics now treat every top-level declaration as one namespace

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

A `type` and a `client` (or a `class` and a `function`) that share a name now produce a single duplicate-name diagnostic, and that diagnostic reports each declaration's real source kind.

- **Unified namespace conflicts.** Previously type declarations and value declarations were checked for name collisions separately, so a name reused across both spaces could slip through or report incompletely. Now all namespace-level declarations with the same spelling contribute to one conflict, and the diagnostic annotates every location. A reused name across four files (`class`, `enum`, `type`, `function`) yields one `Name defined 4 times as: ...` diagnostic pointing at all four.
- **Correct kind labels for config declarations.** `client` and `retry_policy` declarations are lowered to top-level lets internally. Diagnostic messages no longer leak that: a duplicated client now reads `duplicate client definition` instead of `duplicate let definition`.
- **Unaffected cases.** Same-named `test` blocks for different functions remain legal (tests are function-scoped), and same-named declarations in different BAML namespaces (`ns_*/` directories) do not collide.

Also in this release, an internal `tools_compile_profile` harness was added for profiling the compiler pipeline. It is a development tool and does not change compile behavior.

Authors: hellovai

## Duplicate-name diagnostics report the source declaration kind and cover every location in one message

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

When two declarations share a name, the conflict diagnostic now labels each one by its source declaration kind rather than the internal `let` that clients and retry policies are lowered to before HIR. If a `client` and a `type` collide, the message reports them as `client` and `type` instead of surfacing the internal `let` representation.

- **Unified namespace for conflicts.** Type-level declarations (`class`, `enum`, `type`) and value-level declarations (`function`, `client`, and others) that share a spelling now produce a single conflict diagnostic listing every conflicting location, instead of separate type-only and value-only diagnostics. A name defined four times across four files yields one diagnostic with four annotations.
- **Source kind in labels.** Duplicate messages, the "first defined as ... here" note, and namespace-shadowing diagnostics now use the source declaration kind. A duplicated `client` reads as a `client` conflict rather than pointing at the `let` it desugars to.
- **Same-named tests stay legal.** Tests keep their function-scoped identity, so two `test Shared` blocks attached to different functions do not conflict.
- **Cross-namespace names stay legal.** The same name in different `ns_` directories does not conflict.

This release also adds `tools_compile_profile`, an internal, black-box harness for profiling the compile pipeline (parse to emit) when `baml check` is slow. It is a standalone dev tool and does not change the CLI you run.

Authors: hellovai

## A `type` and a `client` sharing a name now report a single unified conflict

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

Namespace-level declarations that share a name now produce one conflict diagnostic, even when they mix type declarations and value declarations. Previously `class`, `enum`, and `type` names were checked against each other in one bucket while `function`, `client`, and `retry_policy` names were checked in a separate bucket, so a `type Backend` and a `client<llm> Backend` could coexist without complaint. They now collide, and a name spelled four different ways (say `class`, `enum`, `type`, and `function`) yields a single diagnostic that points at all four source locations instead of several partial ones.

- **Cross-kind collisions are caught.** A type name and a value name that match are now reported as a duplicate. `class`/`enum`/`type` no longer live in a separate world from `function`/`client`/`retry_policy`.
- **Diagnostics name the declaration you wrote.** `client` and `retry_policy` blocks are lowered to internal `let`s before this check runs. The conflict message now labels them `client` and `retry_policy` rather than leaking the internal `let` kind.
- **Two exceptions preserved.** Same-named `test` blocks attached to different functions remain legal (test identity is `functionName/testName`), and the same name declared in different `ns_*/` namespaces does not conflict.

Authors: hellovai

## 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 outside immediate callback parameters (E0151)

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

Function types must now declare their `throws` clause everywhere except an immediate callback parameter of a function declaration. Omitting it in a type alias, class field, `let` annotation, nested position, or return position is now rejected with `E0151` ("function type must declare an explicit `throws` clause; add `throws never` if calling it cannot throw").

The most common change to existing code: any bare function type such as `(x: int) -> int` in these positions needs a trailing throws clause.

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

- **Explicit `throws` required.** Type aliases, class fields, `let` annotations, and function-valued return positions no longer default an omitted `throws` to `never`. Write `throws never` when the callable cannot throw, or `throws unknown` to leave the effect open.
- **Immediate callback parameters are the one exception.** A function declaration's immediate callback parameter, for example `cb` in `function f(cb: (value: int) -> int) -> int`, may still omit `throws`. The compiler opens it to a synthetic effect parameter, so `f` stays effect-polymorphic over its callback.
- **Lambda parameters must be explicit too.** A lambda's own parameters have no generic binder to open an effect parameter on, so a function-typed lambda parameter like `(f: (value: int) -> int throws never, x: int) -> int { ... }` must spell out the callback's `throws`.
- **Nested and return-position callbacks are no longer auto-opened.** A callback nested inside another parameter type, or exposed through a returned function type, is left unfilled and rejected rather than silently opened to an effect parameter.

The standard library callable signatures (CSV `on_skip`, the LLM `prompt_closure` and related builders) were updated to carry `throws never` accordingly.

Authors: codeshaunted

## The Python SDK now fails at import when its version does not match the native runtime

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

Importing `baml_py` now raises a `PyImportError` when the Python SDK and the loaded native BAML runtime report different release versions, instead of continuing against a mismatched runtime. The check is an exact canonical-version match, deliberately stricter than SemVer, so in normal installs where the SDK and runtime ship in lockstep nothing changes. You only see the new error if the two were installed at different versions.

- **Python**: bridge registration runs during `baml_py` module init and surfaces mismatches as an `ImportError` at import time, with a message telling you to install the matching native runtime or update the SDK.
- **Node.js**: the same registration runs on module load but writes a `failed to register BAML Node.js bridge` line to stderr rather than aborting the load.
- **Foundation**: a new versioned C entry point, `baml_get_api_v1`, exposes the bridge callables as a single `BamlApiV1` function table. This is internal plumbing for host bridges and does not change how you write or run BAML functions.

The registration is first-call-wins: once one language bridge registers the process-wide runtime, a second bridge reporting a different language or version is rejected with a diagnostic.

Authors: hellovai

## Python and Node.js SDKs now reject a native runtime whose version does not match exactly

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

Loading the Python SDK against a mismatched native runtime now raises `PyImportError` at import time. On startup both official SDKs register themselves with the native runtime and require the SDK and runtime to report the same canonical release version. Compatibility is stricter than SemVer here: the strings must be identical.

- **Python:** `import baml_py` calls `register_bridge` during module init and maps any failure to `PyImportError`, so a version skew fails the import with a message naming both the SDK version and the runtime version.
- **Node.js:** the addon registers on init as well, but a failure is written to stderr rather than raised, so the process continues.
- **First registration wins:** once one SDK registers the process-wide runtime, a second SDK of a different language or version is rejected with a diagnostic naming the already-registered bridge.

The rest of this release is ABI scaffolding for a versioned bridge contract (the `baml_get_api_v1` function table and the `BamlApiV1` type). Hosts still resolve individual C symbols in this build, so that table is not yet the load path and carries no observable change on its own.

Authors: hellovai

## Anonymous CLI telemetry with an opt-out, plus incremental bytecode caching

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

The BAML CLI now sends anonymous usage telemetry, and you can turn it off with `baml telemetry disable`. On first run the CLI prints a one-time notice pointing at the opt-out. Only the top-level subcommand (`baml test`, `baml check`, `baml fmt`, and so on, never its arguments), the CLI version and channel, coarse machine facts (OS, CPU arch, CPU count, whether you are in CI, Docker, or WSL), and a random per-machine ID plus a salted one-way hash of your project root are collected. No file contents, prompts, model responses, API keys, or environment variables are sent. See `TELEMETRY.md` for the full field list.

Highlights:

- **`baml telemetry`**: new command to show or change your preference (`baml telemetry`, `baml telemetry disable`, `baml telemetry enable`). It is hidden from `baml --help`. You can also opt out with `BAML_TELEMETRY_DISABLED=1`, the cross-tool `DO_NOT_TRACK=1`, or the legacy `BAML_TELEMETRY=0`. Set `BAML_TELEMETRY_DEBUG=1` to print each event to stderr and send nothing. Delivery is handled by a detached child process after the command exits, so it never delays your invocation.
- **Incremental compilation**: `baml run`, `baml check`, and `baml test` now back onto a content-addressed on-disk bytecode cache with per-file recompilation. A body-only edit no longer forces a whole-project recompile: a file's signature hash ignores function, method, and LLM bodies, so only files whose observable surface changed are re-checked. A full cache hit skips compilation entirely. Disable it with `BAML_NO_BYTECODE_CACHE`, or force a verifying full compile with `BAML_CACHE_VERIFY`. Served diagnostics are checked byte-for-byte against an honest full check by an oracle test suite, so cached results match a cold run.
- **`bridge_cffi` cdylib**: the release now builds a language-neutral engine cdylib once per target (macOS, Linux gnu and musl, Windows) and publishes it as `bridge-cffi-<target>` assets. This is infrastructure for dylib-loader SDKs 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

## Function types must declare `throws` explicitly, or the compiler reports E0151

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

A function type now has to spell out its `throws` clause in every position except one, and omitting it raises `E0151` (`function type must declare an explicit throws clause; add throws never if calling it cannot throw`). Previously an omitted nested `throws` was silently filled in as `never`. That implicit fill is gone.

The only place you can still leave `throws` off is an immediate callback parameter of a function declaration, where the compiler opens it to a fresh effect parameter so the callback stays effect-polymorphic. Everywhere else you must write it:

- **Type aliases**: `type IntToInt = (x: int) -> int` becomes `type IntToInt = (x: int) -> int throws never`.
- **Class and interface fields**: `cb (value: int) -> int` becomes `cb (value: int) -> int throws never`.
- **`let` annotations**: `let f: (x: int) -> int = ...` needs `let f: (x: int) -> int throws never = ...`.
- **Return positions**: `function make() -> (value: int) -> int` needs a `throws` on the returned type.
- **Nested positions**: an inner curried arrow like `(int) -> (int) -> int` must annotate each level, for example `(int) -> ((int) -> int throws never) throws never`.
- **Lambda parameters**: a lambda's function-typed parameter has no generic binder to open an effect parameter on, so it must declare `throws` too, for example `(f: (int) -> int throws never, x: int) -> int { ... }`.

Use `throws never` when calling the function cannot throw, or name a concrete error type otherwise.

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

Authors: codeshaunted

## Python SDK now fails to import against a version-mismatched native runtime (Node.js logs a warning)

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

Importing `baml_py` now registers the Python bridge with the native runtime and aborts the import if their canonical release versions do not match. The two official bridges handle a mismatch differently: Python raises `PyImportError` and the `import baml_py` fails, while Node.js writes a diagnostic to stderr via `eprintln!` and continues loading.

**Highlights**

- **Python import now enforces version lockstep.** On load, the Python bridge calls `register_bridge` and propagates any failure through `.map_err(PyImportError::new_err)?`, so a mismatched native library stops the import instead of surfacing later. The raised message is `BAML Python SDK <sdk-version> cannot use native runtime <runtime-version>. Install the matching native runtime or update the Python SDK`.
- **Node.js only warns.** The Node.js bridge registers the same way but does not fail load. On mismatch it prints `failed to register BAML Node.js bridge: <error>` to stderr and keeps going.
- **Versioned C bridge API (groundwork, no behavior change).** A new `baml_get_api_v1` symbol returns an immutable `BamlApiV1` function table so a manually loaded host bridge resolves one symbol and obtains every ABI-1 callable from it. This is foundation for future host bridges and does not change behavior for existing SDK users.

The compatibility check is exact, not SemVer: the SDK and runtime must report the same canonical release version.

Authors: hellovai

## The Python SDK now raises `ImportError` when its version does not match the native runtime

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

Importing `baml_py` now registers the Python SDK with the native runtime and requires an exact canonical release match. If the versions differ, the import fails with a `PyImportError` naming both versions, instead of continuing with a mismatched runtime.

- **Version lockstep is enforced.** Compatibility is stricter than SemVer: `register_bridge` requires the SDK and native library to report the same canonical release version. The error tells you to install the matching native runtime or update the SDK.
- **Node.js reports registration failures.** The Node.js bridge attempts the same registration on module init and prints a `failed to register BAML Node.js bridge` message to stderr on failure, rather than raising.
- **Conflicting SDK registrations are rejected.** The runtime records the first bridge to register (Node.js, Python, Go, Rust, or C#), and a second registration from a different SDK returns a diagnostic instead of silently taking over.
- **New C entry point `baml_get_api_v1`.** Host bridges resolve a single symbol that returns a versioned function table (`BamlApiV1`) covering runtime init, callbacks, media handles, and bridge registration. This is foundation for dynamically loaded host bridges and is not something you call directly from BAML.

Most of this is internal bridge plumbing. The one change that can affect you today: a Python environment with a mismatched `baml_py` and native runtime will now fail at import time with a clear message.

Authors: hellovai

## Opt out of anonymous CLI telemetry with `baml telemetry disable`

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

The BAML CLI now collects anonymous usage telemetry, and you can turn it off with `baml telemetry disable`.

- **Telemetry controls.** A new (hidden) `baml telemetry` subcommand reports the current status, and `baml telemetry disable` / `baml telemetry enable` toggle it. You can also opt out with the environment variable `BAML_TELEMETRY_DISABLED=1`, the cross-tool `DO_NOT_TRACK=1`, or the legacy `BAML_TELEMETRY=0`. Set `BAML_TELEMETRY_DEBUG=1` to print exactly what would be sent to stderr without sending anything. A first-run notice announces that collection is on. What ships is the subcommand name (not its arguments), the CLI version and channel, coarse machine info (OS, arch, CPU count, whether you are in CI/Docker/WSL), and a random per-machine ID plus a salted one-way hash of the project root. Source, prompts, responses, secrets, and file paths are never collected. See `TELEMETRY.md` for the full field list.
- **Incremental compilation.** `baml check`, `baml run`, and `baml test` now serve from a content-addressed on-disk bytecode cache with per-file recompilation. Editing one file's function body no longer forces a whole-project recompile, since a body-only edit leaves a file's signature hash unchanged. Set `BAML_NO_BYTECODE_CACHE=1` to force a full compile.

```bash
baml telemetry          # show current status
baml telemetry disable  # opt out
BAML_TELEMETRY_DEBUG=1 baml test  # print events to stderr, send nothing
```

Authors: hellovai, antoniosarosi, 2kai2kai2

## Internal type-algebra cleanup with no intended change to type checking

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

This release deletes the old duplicate type algebra in `baml_compiler2_tir::normalize` and routes every equivalence check through the canonical implementation in `baml_type::normalize`. There is no intended change to how your BAML types check.

The removed `is_same_normalized_type` and `is_subtype_of` helpers are replaced by a shared `TypeContext`. Impl-head matching, coherence unification, and MIR dispatch now go through a new `AliasEquivCtx` that expands type aliases but treats enums, interfaces, type variables, and associated-type projections as opaque leaves, which is the same conservative answer those sites relied on before.

One spot in container matching changed from a subtype check to an equivalence check for the element types of `List` and `Map`, matching their invariance: an `int[]` argument does not satisfy an `(int | string)[]` slot. The rest is a pure refactor, plus added tests covering function `throws` covariance, insignificance of required parameter names, and the optional-parameter superset rule.

Authors: 2kai2kai2

## Array patterns can now bind the rest with `..let r`

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

`..let r` in an array pattern now binds a copy of the unmatched middle of the scrutinee, typed as the element list. Previously any sub-pattern after `..` was rejected outright; only bare `..` was allowed.

- **Rest bindings.** `..let r` binds the elements between the prefix and suffix as a fresh `elem[]` list. The copy is shallow (elements are shared with the source) and independent of the scrutinee, so pushing to one does not mutate the other.
- **Wildcard rest.** `.._` is allowed and lowers exactly like bare `..`: it binds nothing and pays for no slice copy.
- **List ascription.** You can annotate the binding with the slice type, `..let r: int[]`. A non-list ascription (`..let r: int`) or a narrowing one (`..let r: int[]` on `(int | string)[]`) is a type mismatch, since the slice tag is always exactly `elem[]`.
- **Rejected shapes.** Type patterns (`..int`), class destructures (`..Wrapper { value }`), nested array rests (`..[a, ..r]`), and or-patterns after `..` are still rejected, now with the message "rest pattern `..` can only carry a binding; write `..let name` or `..let name: T[]`".

```baml
function f(xs: int[]) -> int {
    match (xs) {
        [let a, ..let r, let z] => a + r.length() + z,
        _ => 0
    }
}
```

Authors: codeshaunted

## 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

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

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

This release only touches the BEPs web app. The section sidebar (`BepNav`) no longer jumps when you move between pages. Two changes cause this. The active section link now carries `aria-current="page"`, and an effect scrolls that link into view within the sidebar's own scroll container instead of calling `scrollIntoView`, which would have also scrolled the window. Separately, the sidebar's scroll offset is now saved per BEP and restored when its container remounts (for example, when the loading skeleton is replaced by content), so a long page list stays where you left it. There are no changes to the BAML language, compiler, or client libraries.

Authors: aaronvg

## BEP sidebar keeps its scroll position when you navigate between pages

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

The BEP navigation sidebar no longer jumps back to the top when you move between pages. `BepNav` now tracks the active item via `aria-current="page"` and scrolls only its own container to keep that item in view, rather than calling `scrollIntoView` (which would also drag the window and ancestors along). The route shell persists each BEP's sidebar `scrollTop` in a module-level map keyed by BEP number, so the offset survives remounts such as the loading skeleton giving way to content.

This is scoped to the internal BEPs web app. There are no changes to the BAML language, compiler, or runtime in this nightly.

Authors: aaronvg

## Documentation search switches to Fern's built-in search

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

This release only touches the documentation site infrastructure. The custom `Ask Baaaml` search client and its `sage-backend` deployment were removed in favor of Fern's built-in search, along with the associated `custom.js` injection in `fern/docs.yml` and the backend and Pinecone deploy jobs in CI. Nothing in the BAML compiler, runtime, or generated clients changes.

Authors: aaronvg

## Internal-only changes to the BEPs proposal app, nothing affecting BAML usage

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

This nightly touches only the internal BEP (BAML Enhancement Proposal) web app under `typescript/apps/beps/`. Nothing in the BAML language, CLI, runtime, or client libraries changed, so there is no action for BAML users.

For completeness, the app-level changes were: a `parentSlug` field on pages to support one level of page nesting on import and export, plus fixes to sidebar scrolling, table-of-contents anchor clicks, and a title bug in the update API. These are documented here only for traceability and do not affect how you write or run BAML.

Authors: aaronvg

## `baml describe` highlighting now follows your terminal theme instead of hardcoded colors

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

Syntax highlighting in `baml describe` output now draws from the terminal's named ANSI palette rather than fixed 256-color values, so it stays legible on both light and dark backgrounds. The renderer also switched from lexer-based coloring to the same LSP semantic tokens used by the editor, so token modifiers now affect styling.

- **Theme-controlled colors.** Token types like `Parameter`, `Comment`, `Decorator`, `Operator`, and `EscapeSequence` no longer emit concrete 256-color codes. Ordinary variables keep the terminal's default foreground instead of forcing white, and quiet token types are rendered dim so your theme decides the actual color.
- **Modifier-aware styling.** Declarations are now bold, default-library (stdlib) entities italic, and deprecated ones struck through, matching how editor themes overlay these attributes.
- Coloring remains forced per output stream, gated on stdout vs stderr, rather than console's ambient stdout flag.

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. The recorded `baml-cli` Linux x86_64 `file_bytes` grew from 21.53 MB to 22.08 MB (`.ci/size-gate/x86_64-unknown-linux-gnu.toml`), with matching updates to the macOS, Windows, and WASM records and the `max_file_bytes`/`max_gzip_bytes` ceilings in `.cargo/size-gate.toml`. Nothing in the compiler, runtime, or CLI behavior changed. No action required.

Authors: sxlijin

## Version stamp bumped to `0.15.0` with no observable behavior change

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

This release stamps `CANONICAL_VERSION`, `PYPI_VERSION`, and `STABLE_VERSION` to `0.15.0` and carries no user-facing changes. The rest of the diff removes an obsolete LSP design document and strips its internal design-phase references (`B1`, `B2`, `D3`, `I7`, and similar labels) from code comments and test names. The LSP request handling, position encoding negotiation, diagnostics publication, and engine rebuild logic are untouched, so your BAML code and editor behavior are unaffected.

Authors: rossirpaulo

## Bitwise `~` now complements its operand, and BAML syntax highlighting ships to external editor ecosystems

0.15.0 · canary · 2026-07-14

The bitwise NOT operator `~` now actually computes a bitwise complement. Previously it was a silent identity: `~x` returned `x` unchanged (B-766).

**Highlights**

- **`~` (bitwise NOT)** now performs a bitwise complement instead of silently returning its operand unchanged. Any code that relied on the old no-op behavior will see different results.
- **Syntax highlighting distribution.** BAML highlighting is now published to external mirrors and packages covering Sublime, KDE, highlight.js, and tree-sitter (consumed by nvim, Zed, and Helix), alongside the TextMate grammar that GitHub Linguist vendors for `.baml` highlighting on github.com. A CI gate compiles the grammar with Linguist's own compiler so a grammar change cannot silently break github.com rendering.
- **`retry_policy` no longer crashes.** The delay fields (`initial_delay_ms`, `multiplier`, `max_delay_ms`) are now optional. When omitted they fall back to 200ms / 1.5x / 10000ms instead of raising an `any + float` VM type error during backoff arithmetic (B-488).
- **`google-ai` provider options.** A new `GoogleAiOptions` type lets a `google-ai` client route through the Gemini Enterprise (Vertex AI) backend via `enterprise`, `project_id`, `location`, and credential fields. The runtime also reads `GOOGLE_CLOUD_PROJECT`, `GOOGLE_CLOUD_LOCATION`, and `GOOGLE_GENAI_USE_ENTERPRISE`.
- **Named `client<llm>` defaults `api_key`.** A named LLM client now defaults its `api_key` to the provider's environment variable instead of leaving it unset (B-489).
- **Enum serialization.** Enum variants now serialize as their name in union and optional JSON fields (B-728).
- **LSP responsiveness.** The mutex/latency regression introduced in 0.14 is fixed, restoring editor responsiveness (#4000).
- **`baml ide install --dir <path>`** copies the active toolchain's VSIX into a directory for manual install, and the not-found error now walks you through installing from VSIX by hand.

The standard library's interface associated types are now consistently projected with `Self.` (for example `throws Self.CompareError`, `Iterator<Item = Self.Item>`); this is an internal correctness change with no effect on your own code.

```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. `watch let` bindings no longer parse, and the `.$watch` accessor along with its `.$watch.options(...)` and `.$watch.notify()` methods have been removed.

- **`watch let`**: Source using `watch let x = ...` will no longer tokenize `watch` as a keyword. It now lexes as an ordinary identifier, so existing code that relied on this reactive binding form will fail to parse.
- **`$watch` member access**: The `.$watch` accessor and the `WatchOptions` shape are removed, so `x.$watch.options(...)` and `x.$watch.notify()` no longer exist.
- **Diagnostics**: The error codes `E0065` (`$watch` on a non-variable) and `E0066` (`$watch` on a variable not declared with `watch let`) are deleted, since the conditions that produced them can no longer occur.

This feature was never fully implemented (the lowering for watched bindings was a stub), and this release removes the parser, AST, MIR, and type-checker plumbing behind it. If you were not using `watch`, nothing in your code changes.

Authors: sxlijin

## `google-ai` clients can route through Vertex AI and read Google Cloud env vars

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

A `google-ai` client can now delegate to the Vertex AI backend, and both Google providers resolve project and location from Google Cloud environment variables at request time. This matches google-genai's behavior: set `GOOGLE_GENAI_USE_VERTEXAI` or `GOOGLE_GENAI_USE_ENTERPRISE`, or the new `enterprise` option, to send a `google-ai` client to Vertex, and it will pick up `GOOGLE_CLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION`.

**Highlights**

- **New `GoogleAiOptions` provider options** for `google-ai` clients: `enterprise`, `credentials`, `credentials_content`, `location`, and `project_id`. With `enterprise true` (or `GOOGLE_GENAI_USE_ENTERPRISE=true`), the client uses the Vertex backend and applies Vertex defaults instead of Gemini API defaults; an unset location then defaults to `global`. An explicit `enterprise false` overrides the env var.
- **`vertex-ai` no longer requires `location` at compile time.** The old diagnostic "vertex-ai requires either base_url or location" is gone. `location` and `project_id` can come from `GOOGLE_CLOUD_LOCATION` / `GOOGLE_CLOUD_PROJECT` (or the credential chain) at request time. If none resolves, `$build_request` fails with an actionable error naming both `options.location` and `GOOGLE_CLOUD_LOCATION`.
- **`google-ai` now requires an `api_key`.** A `google-ai` client with no `api_key` (and not routed to Vertex) fails at request construction with "Missing api_key for Google AI", pointing at both `baml describe google-ai` and `baml describe vertex-ai`. Previously the request was built with the auth header silently omitted.
- **New keyword docs.** `baml describe google-ai` documents the Gemini API path and enterprise delegation to Vertex; `baml describe vertex-ai` documents Google Cloud setup, credentials, and the `global` location.

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

Credential resolution for Vertex was also reworked to mirror google-auth: `options.credentials` is a file path only, `options.credentials_content` carries inline JSON, and an explicitly set but broken credential is a hard error rather than a silent fallthrough. Tokens are now cached process-wide until shortly before expiry.

Authors: sxlijin

## 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

The LSP semantic-tokens layer was reworked to classify identifiers by resolution rather than type inference, and it now emits token modifiers. In your editor, definition sites (a `function` name, a parameter, a field) carry the `declaration` modifier, and entities from the `baml` standard library carry `defaultLibrary`. The modifier legend also reserves `deprecated`, `readonly`, and `async`, but no code path sets those yet, so you will not see them in output.

Highlights:

- **New token types `boolean` and `escapeSequence`.** `true` and `false` now highlight as `boolean` instead of `number`, and string escapes get their own `escapeSequence` type.
- **On-demand range highlighting.** `semantic_tokens_in_range` classifies only the scopes a viewport touches, and a test asserts it matches the full-document result filtered to that range.
- **Go-to-definition on virtual interface members.** `d.as<Serializer>.encode()` now jumps to the `encode` signature declared on the interface, and `p.as<Named>.fullname` jumps to the interface field declaration, instead of resolving to nothing.
- **Hover type info for `catch` and pattern bindings.** The binding in `foo() catch (e) { ... }`, and bindings from destructuring patterns, now report their inferred type on hover rather than `unknown`. Catch bindings are highlighted as parameters.
- **Contextual keywords `as`, `type`, `true`, `false`, and `null` are re-lexed to dedicated keyword kinds** at parse time. This is an internal parser and AST cleanup with no change to what these words mean in your BAML source.

The C FFI now exports a bytecode runtime initializer for embedders linking against the native runtime.

Authors: hellovai, codeshaunted

## Semantic tokens are now resolution-driven and carry modifiers like `declaration` and `defaultLibrary`.

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

The LSP semantic-tokens layer was reworked to classify names by what they resolve to rather than by syntax, and it now emits token modifiers alongside token types. If your editor consumes BAML's semantic highlighting, expect richer and more accurate coloring.

- **Token modifiers.** Tokens now carry a modifier bitset advertised in `TOKEN_MODIFIERS`: `declaration`, `defaultLibrary`, `deprecated`, `readonly`, and `async`. Declaration sites (for example the `foo` in `function foo`) are tagged `declaration`, and names resolved to the `baml` standard library are tagged `defaultLibrary`.
- **New token types.** `true` and `false` are now classified as a distinct `boolean` type instead of `number`, and string escape sequences get their own `escapeSequence` type. This is backed by `as`, `type`, `true`, `false`, and `null` becoming re-lexed contextual keywords (`KW_AS`, `KW_TYPE`, `KW_TRUE`, `KW_FALSE`, `KW_NULL`) at parse time.
- **Go-to-definition on interface virtual members.** Jumping to definition on a virtual interface method or field access such as `p.as<Named>.fullname` now navigates to the declaration in the interface. Previously these resolved to a slot with no jump target and returned nothing.
- **Hover on catch and pattern bindings.** Hover now reports the inferred type instead of `unknown` for both `catch (e) { ... }` bindings and match-arm / pattern bindings. In highlighting, catch bindings are colored as `parameter` (like function parameters, since they are values bound and scoped to the clause body), while match-arm and other pattern bindings remain `variable`.
- **Range-scoped highlighting.** `semantic_tokens_in_range` resolves only the scopes a viewport touches and is guaranteed to match the full-document result filtered to that range.

Authors: hellovai, codeshaunted

## LSP find-references and workspace symbol search build one position codec per file

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

Find-references and workspace symbol search in the LSP now build one `PositionCodec` per distinct file instead of one per result. Constructing a codec scans the whole file to build its line table, so when many usages or symbol matches land in the same file, the previous code rescanned that file once per result. Both `usages_at` and `search_symbols` result handling now memoize the codec by file id, which cuts the redundant scans on broad queries.

The rest of this release is test and documentation only: new Salsa memoization tests that pin `file_annotations` and `semantic_tokens` as tracked queries, and casing fixes for `UPDATE_EXPECT` in the test instructions. No language or API behavior changed.

Authors: rossirpaulo

## `spawn` now propagates engine errors to awaiters instead of parking them forever

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

A spawned task that terminates with an engine error now settles its future with that error, so an `await` on it re-raises the failure instead of blocking forever. Previously such an error escaped the task's event loop with the future still `Pending`, and every awaiter up the chain parked indefinitely.

The rest of the release is the `RealizedTy` runtime refactor: runtime values now carry a type that cannot contain type variables or associated-type projections, and that constraint is enforced at the boundaries rather than silently erased.

- **`RealizedTy` on runtime values.** Heap values (array element types, map key and value types, instance class type args, reflected `type` values) now store a `RealizedTy` instead of a `RuntimeTy`. This is an internal representation change with no change to how well-typed programs behave.
- **`TyTemplate::substitute` returns `Result<RealizedTy, SubstituteError>`.** Materializing a template against a frame's type arguments now either produces a fully realized type or fails with a `SubstituteError` (`Wildcard`, `UnreducibleProjection`, `ProjectionNotRealized`, or `ProjectionFuelExhausted`), rather than falling back to `unknown`. The older symbolic `RuntimeTy` path moved to a separate `substitute_symbolic`.
- **`Ty::AssociatedTypeProjection` now always carries its interface.** The `interface` field is no longer optional, so a resolved projection renders as `(base as I).member` in diagnostics rather than sometimes as `base.member`. The TIR resolves the declaring interface or lowers to `Ty::Error` when it cannot.
- **FFI type checks.** Host-supplied types that are not fully realized are now rejected loudly instead of erased. A non-realized array element type, map key or value type, or instance type argument fails as `EngineError::TypeMismatch`. A non-realized host entry-point type argument fails as `EngineError::VmInternalError(VmInternalError::TypeSubstitution)`.

Authors: 2kai2kai2

## The BEPs viewer sidebar keeps its scroll position while navigating

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

This nightly is a single fix to the BEPs web app (the BAML Enhancement Proposals viewer), with no changes to the BAML language, CLI, or runtime. The sidebar nav in `bep-route-shell.tsx` now persists its scroll offset per BEP in a module-level `navScrollPositions` map and restores it when the shell remounts, so switching sections no longer jumps a long page list back to the top. `BepNav` also marks the active item with `aria-current="page"` and scrolls only its own container to keep that item visible, avoiding the window-level jump that `scrollIntoView` caused. Nothing here affects your BAML code.

Authors: aaronvg

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

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

The BEPs app sidebar no longer jumps back to the top or drags the window when you move between pages. `BepNav` now scrolls the active item into view within the sidebar's own scroll container instead of calling `scrollIntoView`, which had been scrolling ancestor elements too. The active link is also marked with `aria-current="page"`, and `bep-route-shell` remembers each BEP's sidebar scroll offset across remounts (such as the loading skeleton to content transition) so a long page list stays where you left it. This is scoped to the BEPs web app and does not affect the BAML language or compiler.

Authors: aaronvg

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

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

The BAML documentation site now uses Fern's built-in search instead of the custom Ask Baaaml chat widget. This is a docs-infrastructure change only. It removes the `custom.js` bundle from `fern/docs.yml`, deletes the `sage-backend` and `ask-baml-client` deploy jobs from the docs CI workflow, and does not touch the BAML compiler, runtime, or generated clients. Your BAML code and toolchain behavior are unchanged.

Authors: aaronvg

## Internal BEPs proposal app adds one-level nested pages; no BAML language changes

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

This nightly touches only the internal BEPs (BAML Enhancement Proposals) web app under `typescript/apps/beps/`. Nothing in the BAML language, CLI, runtime, or client libraries changed, so your BAML code and generated clients are unaffected.

For contributors who author proposals through that app:

- **`parentSlug` on pages.** Page objects (in the schema, the agent API, import, and export) gained an optional `parentSlug` field that nests a page under another page, one level deep. On import, a markdown file in a subfolder of `pages/` (for example `pages/design/api.md`) becomes a child of the `design` page. Slugs remain unique per BEP, so the same filename in two folders is rejected as a duplicate, and folders deeper than one level are skipped.
- **Table-of-contents anchors fixed.** Heading id generation moved to a shared `heading-utils` module keyed by source line, so TOC clicks now land on the correct heading instead of drifting across re-renders. A `#hash` in the URL is also honored on initial load once the async-rendered markdown appears.
- **Sticky sidebar scrolls.** The proposal nav sidebar now has a max height and its own scroll, so a long page list stays reachable.
- **`importVersion` title bug.** Omitted `title`, `content`, and `pages` fields now keep their current values instead of blanking them; passing `pages: []` still clears all pages.

Authors: aaronvg

## `baml describe` highlighting now uses LSP-exact tokens and adapts to any terminal background

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

Syntax highlighting in `baml describe` output is now driven by the same semantic tokens the language server produces (`semantic_tokens` from `baml_lsp2_actions`) instead of a separate lexer pass, so the colors you see in the terminal match what your editor shows.

- **Background-agnostic colors.** `style_for` now restricts output to the terminal's named ANSI palette plus dim and bold attributes. Fixed 256-color values and concrete white/black are gone, so ordinary names keep your terminal's default foreground and the output stays legible on both light and dark backgrounds.
- **Token modifiers are honored.** Highlighting reflects a token's modifiers: declarations render bold, standard-library entities italic, and deprecated ones struck through.
- **Quiet token types dim instead of coloring.** Comments and operators now use a dimmed default color rather than a fixed gray, and a declaration trades that dim for bold instead of stacking both.

Authors: codeshaunted

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

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

This release only refreshes the CI size-gate baselines. The recorded artifact sizes and `max_file_bytes` limits in `.cargo/size-gate.toml` were bumped to match the current build (for example, the Linux x86_64 `baml-cli` file grew from about 21.51 MB to 22.08 MB), with no changes to BAML behavior.

Authors: sxlijin

## Version stamp to 0.15.0 with no user-visible changes

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

This release stamps the product version to `0.15.0` across the Python, Node, and VS Code packages and carries no behavior changes. The rest of the diff is internal cleanup: a resolved LSP design doc (`docs/design/lsp-latency-and-mutex-fix.md`) was deleted and its shorthand references (B1, B2, I7, D4, and similar labels) were stripped from source comments. One internal test was renamed. Nothing in the compiler, LSP handlers, or runtime behavior changed for BAML code.

Authors: rossirpaulo

## `google-ai` clients gain a `GoogleAiOptions` block and Vertex AI delegation, plus fixes to `retry_policy`, bitwise `~`, and enum JSON serialization

0.15.0 · canary · 2026-07-14

`google-ai` clients now accept provider options through a new `GoogleAiOptions` block and can delegate to the Vertex AI backend when configured for Google Cloud.

**Highlights**

- **`google-ai` provider options.** The new `GoogleAiOptions` class adds `enterprise`, `credentials`, `credentials_content`, `location`, and `project_id`. Setting `enterprise` (equivalent to `GOOGLE_GENAI_USE_ENTERPRISE=true`) routes the client through Vertex AI using Google Cloud credentials, with an unset location defaulting to `global`. The runtime now also reads `GOOGLE_CLOUD_PROJECT`, `GOOGLE_CLOUD_LOCATION`, and `GOOGLE_GENAI_USE_ENTERPRISE`.
- **`retry_policy` no longer crashes on partial config (B-488).** `initial_delay_ms`, `multiplier`, and `max_delay_ms` are now optional. When omitted they fall back to 200ms, 1.5x, and 10000ms instead of failing the backoff arithmetic with an `any + float` type error.
- **Bitwise NOT (`~`) now negates (B-766).** `~x` was previously a silent identity that returned its operand unchanged.
- **Enum variants serialize by name (B-728).** Enum values in union and optional JSON fields now serialize as their variant name.
- **Class and array interpolation renders in prompt strings (B-563).**
- **Named `client<llm>` defaults `api_key` to the provider's env var (B-489).**
- **Associated types are projected with `Self.` inside interface bodies.** As part of the interface type-inference rework, the standard library's `Iterator`, `Comparable`, and `Summable` interfaces now reference their own associated types as `Self.Item`, `Self.Error`, `Self.CompareError`, and `Self.Sum` rather than the bare names.
- **`baml ide install`** adds `--dir <path>` to copy the VSIX out for a manual install, gives clearer errors when the `code` or `cursor` CLI is not on PATH, and no longer treats `--code` and `--cursor` as conflicting.
- **`baml agent install`** now downloads skills from the repo's main-branch tarball on codeload instead of the GitHub REST API, so unauthenticated installs no longer hit the API rate limit. The `--latest` flag is removed; `--from` accepts a URL, a local `.tar.gz`, or a directory.
- **Broader syntax highlighting.** BAML grammars now publish to TextMate, highlight.js, tree-sitter, and Sublime mirrors, so `.baml` highlights on github.com and in more editors.
- **React streaming hooks** now report correct data types for nullable fields.

```bash
# Copy the VSIX out for a manual "Extensions: Install from VSIX..."
baml ide install --dir ~/Downloads
```

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: `watch let` bindings no longer parse, and the `.$watch` accessor along with `.$watch.options(...)` and `.$watch.notify()` no longer exist.

This was an incomplete reactive-binding feature (its MIR lowering still carried `TODO: Handle watched let statements`), and it has been deleted end to end.

- **`watch let` no longer parses.** Rewrite any `watch let x = ...` to a plain `let x = ...`. The `WATCH_LET` syntax node and `KW_WATCH` token are removed, so the formatter, semantic tokens, and parser now treat `watch` as an ordinary identifier.
- **`.$watch` member access is removed.** Expressions like `x.$watch.options(...)` and `x.$watch.notify()` are no longer recognized.
- **Diagnostics E0065 and E0066 are gone.** `WatchOnNonVariable` ("$watch can only be used on simple variable expressions") and `WatchOnUnwatchedVariable` no longer exist.

If your code used any of these, replace watched bindings with plain `let`. Watched variables were reactive, so re-running dependent work on change is now on you to express explicitly.

Authors: sxlijin

## `google-ai` clients can route through Vertex AI via `GOOGLE_GENAI_USE_VERTEXAI`, `GOOGLE_GENAI_USE_ENTERPRISE`, and `GOOGLE_CLOUD_*`

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

A `google-ai` client now delegates to the Vertex AI backend when `GOOGLE_GENAI_USE_VERTEXAI` or `GOOGLE_GENAI_USE_ENTERPRISE` is set, or when the new `enterprise` provider option is true. This matches google-genai's behavior: location and project come from `GOOGLE_CLOUD_LOCATION` and `GOOGLE_CLOUD_PROJECT` (or the credential chain), and authentication uses Google Cloud Application Default Credentials instead of an API key.

- **New `google-ai` provider options.** A `GoogleAiOptions` block adds `enterprise`, `credentials`, `credentials_content`, `location`, and `project_id`. `enterprise true` selects the Vertex backend and defaults an unset location to `global`; an explicit `enterprise false` disables routing even when `GOOGLE_GENAI_USE_ENTERPRISE=true`. The remaining fields carry the same meaning as on `VertexAiOptions` once the Vertex backend is in use.
- **`vertex-ai` no longer requires `location` at compile time.** The compiler previously rejected a `vertex-ai` client that set neither `base_url` nor `location`. That diagnostic is gone: `location` and `project_id` can be resolved at request time from `GOOGLE_CLOUD_LOCATION` / `GOOGLE_CLOUD_PROJECT`. If none resolve, `$build_request` fails with an actionable error naming both `options.location` and the env var.
- **`google-ai` without an `api_key` now fails fast.** Building a request for an api-key `google-ai` client with no key set returns an error (`Missing api_key for Google AI...`) pointing at `baml describe google-ai` and `baml describe vertex-ai`, instead of silently sending a request with no auth header.
- **`vertex-ai` credential resolution narrowed.** `credentials` is now strictly a credential JSON file path and `credentials_content` is the inline JSON string. Inline JSON in `credentials` or in `GOOGLE_APPLICATION_CREDENTIALS`, the `GOOGLE_APPLICATION_CREDENTIALS_CONTENT` env var, and `gcloud` CLI shell-outs are no longer credential sources. An explicitly set option that is broken is now a hard error rather than a fall-through to the next source.
- **`baml describe google-ai`** is a new keyword topic documenting the Gemini API path and the enterprise/Vertex delegation.

End-to-end, a `google-ai` client can run against Vertex with no API key and no client-side options:

```bash
GOOGLE_GENAI_USE_VERTEXAI=1 \
  GOOGLE_CLOUD_PROJECT=<project> \
  GOOGLE_CLOUD_LOCATION=<location> \
  baml run --from demo2 main
```

Authors: sxlijin

## Semantic tokens are now resolution-first, with modifiers and goto-definition into interfaces

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

The LSP semantic-tokens engine was reworked so that identifiers inside expression bodies are classified by what they *resolve to*, not by type inference or substring scanning. Names now carry LSP modifiers, and `true`/`false`/`null`/`as`/`type` are re-lexed as distinct keyword tokens rather than plain words.

**Highlights**

- **Boolean and escape-sequence highlighting.** `true` and `false` now emit a dedicated `boolean` token type, and string escapes get `escapeSequence`. Both the CLI painter (`baml_cli::paint`) and the LSP legend recognize them.
- **Token modifiers.** Semantic tokens now carry a modifier bitset. The modifier legend advertises `declaration`, `defaultLibrary`, `deprecated`, `readonly`, and `async`; currently `declaration` (on definition sites like `function Foo` -> `Foo`) and `defaultLibrary` (on built-ins such as `string`/`int` and `baml`-package namespaces) are applied. The other three are reserved legend entries and are not yet emitted.
- **Goto-definition into interfaces.** Jumping to a virtual interface method or field (via `d.as<Serializer>.encode()` or `p.as<Named>.fullname`) now lands on the declaration inside the interface, using new `interface_field_spans` / `interface_method_spans` source maps. Previously these resolved to nothing.
- **`catch (e)` bindings are parameters.** The error binding of a `catch (e) { … }` clause is now tracked under a distinct `CatchBinding` definition site and highlighted as a `parameter`, matching how it behaves (a value bound by the clause and scoped to its body). Hover and completions report its inferred type instead of `unknown`.
- **Contextual keywords.** `as`, `type`, `true`, `false`, and `null` are re-lexed from words into `KW_AS`, `KW_TYPE`, `KW_TRUE`, `KW_FALSE`, and `KW_NULL`. AST accessors that previously matched these by text now match by kind. This is an internal parser change with no effect on valid source, but it is what makes the boolean/keyword-literal highlighting above possible.

```baml
interface Named {
  fullname: string
}

class Person {
  display_name: string

  implements Named {
    fullname as display_name
  }
}

function ReadName(p: Person) -> string {
  return p.as<Named>.fullname
}
```

On the runtime side, the C FFI now exports a bytecode runtime initializer.

Authors: hellovai, codeshaunted

## Editor highlighting now classifies names by what they resolve to

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

Semantic highlighting in the language server was reworked to be resolution-first: each name in an expression body is now tagged by what it resolves to (a local, a parameter, an interface field, an enum variant, a `baml` package member) instead of by its syntactic position. The type system is no longer consulted to pick a color, only resolution facts are.

Highlights:

- **New token modifiers.** The legend now advertises `declaration`, `defaultLibrary`, `deprecated`, `readonly`, and `async`. Definition sites carry `declaration` (a `function Foo` name, a `let` binding), and entities from the `baml` standard library carry `defaultLibrary`, so primitives like `string` and `int` read differently from your own types.
- **New token types `boolean` and `escapeSequence`.** `true` and `false` now highlight as booleans rather than plain identifiers, and string escapes get their own type.
- **Goto-definition on virtual interface members.** Jumping to definition on `p.as<Named>.fullname` or `d.as<Serializer>.encode()` now lands on the field or method declaration inside the interface. Previously these returned no location.
- **Range-scoped highlighting.** `semantic_tokens_in_range` classifies only the tokens intersecting a requested range, resolving each scope on demand instead of indexing the whole file up front. The range result matches the full-document result filtered to that range.
- **Catch bindings highlight as parameters.** The `e` in `catch (e) { ... }` is now classified like a function parameter, matching how it is scoped.

Under the hood, the contextual words `as`, `type`, `true`, `false`, and `null` are re-lexed into dedicated syntax kinds (`KW_AS`, `KW_TYPE`, `KW_TRUE`, `KW_FALSE`, `KW_NULL`) rather than matched by text, which is what makes the boolean and null classification above possible. This does not change the syntax you write.

Separately, the C FFI now exports a bytecode runtime initializer for embedders (#4009).

Authors: hellovai, codeshaunted

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

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

This nightly is a follow-up cleanup to the LSP, with one change you might feel on large files. `usages_at` (find all references) and `search_symbols` (workspace symbol search) now build a single `PositionCodec` per distinct file instead of one per result. Constructing a codec scans the whole file for its line table, so a query that returned many matches in the same file previously rescanned that file once per match. Results are unchanged; only the redundant work is gone.

The rest of the release is internal. A new `memoization` test in `baml_lsp2_actions_tests` pins that `file_annotations` and `semantic_tokens` stay Salsa tracked queries, so an unchanged file does not recompute them and a source edit does. `TEST_INSTRUCTIONS.md` gained a `cargo test --lib` note and corrected the `UPDATE_EXPECT` casing.

Authors: rossirpaulo

## 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

## The 0.14 LSP latency regression is fixed and agent-skill warnings move into the toolchain

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

The 0.14 LSP mutex and latency regression is fixed: `semantic_tokens` and inlay-hint results (`file_annotations`) are now memoized Salsa tracked queries instead of being recomputed on every editor request.

**Highlights**

- **Editor responsiveness.** `semantic_tokens` and `file_annotations` are now `#[salsa::tracked]` queries. Each walk re-classifies every token or function body against type inference (measured at 40 to 150ms on real projects), and editors re-request both on every scroll. They now reuse cached results while the file is unchanged. Note that `annotations` was renamed to `file_annotations`; if you depend on `baml_lsp2_actions` directly, update the import.
- **Bounded LSP ingress.** The server now serializes each outbound JSON-RPC frame once behind a shared budget (64MB of queued response bytes, 4MB per frame). A full or oversized queue surfaces typed `OutboundSaturated` / `OutboundOversized` backpressure rather than growing an unbounded writer queue. A new process-owned ingress scheduler serves both the stdio and browser-playground transports through one dispatch path.
- **Agent-skill warnings now live in the toolchain.** The "No baml skill is installed" and "Your baml skill is outdated" warnings print from the toolchain binary, only on the authoring commands `init`, `run`, `generate`, and `pack`. They ship with every nightly instead of waiting on a wrapper release, and no longer appear on machine-facing commands like `check` or `fmt`.
- **Skill installs archive the previous version.** Replacing a skill moves the prior copy into a `baml-old_skills/` slot inside the skills directory instead of a temporary backup dir. Because that slot sits one level below the `<skills>/<name>/SKILL.md` layout, it is not counted as an installed skill, and the name `baml-old_skills` is now reserved (a source that ships a skill by that name is rejected).
- **Parser fix.** An incomplete raw string whose hashes run to end-of-file (for example `##`, or `key ##` inside a client block) no longer indexes past the token buffer. `find_token_after_hashes` returns `None` at EOF, so these inputs report diagnostics instead of panicking.

Authors: rossirpaulo, ATX24

## Revert the recent LSP semantic-tokens change to unblock a pending fix

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

This nightly reverts the LSP semantic-tokens rewrite from #3867 so another change (#4000) can land first, which rolls back several editor features that shipped with it.

- **Semantic highlighting** in `baml_lsp2_actions` drops token modifiers (`declaration`, `defaultLibrary`, `deprecated`, `readonly`, `async`) and the `boolean` and `escapeSequence` token types. `true`, `false`, and `null` are no longer emitted as distinct boolean/null literals; they revert to plain identifiers.
- **Goto-definition** on virtual interface members no longer jumps to a declaration. Accessing an interface field or method through `p.as<Iface>.member` now resolves to no location instead of the interface's field or method signature.
- **Hover** on pattern and catch bindings (`match` arms, `catch (e)`) reports the type as `unknown` rather than the inferred type.

Internally, the contextual keywords `as`, `type`, `true`, `false`, and `null` are again parsed as `WORD` tokens rather than dedicated `KW_*` kinds, and `CatchBinding` folds back into `PatternBinding`. Compiled BAML behavior is unchanged; this only affects editor tooling, and the reverted features are expected to return once #4000 is in.

Authors: rossirpaulo

## Semantic highlighting is now driven by name resolution, and goto-definition works on virtual interface members

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

LSP semantic tokens were reworked so that identifiers inside expression bodies are classified by what they *resolve to* rather than by their inferred type. The practical effect in your editor is more accurate highlighting plus a set of token modifiers that were not emitted before.

- **Token modifiers.** Names now carry `declaration` at their definition site and `defaultLibrary` when they come from the `baml` standard library or a dependency package, so a stdlib call reads differently from one of your own functions. The modifier legend is `declaration`, `defaultLibrary`, `deprecated`, `readonly`, `async`.
- **New token types.** `true`/`false` are highlighted as a distinct `boolean` type and string escape sequences as `escapeSequence`, both in the LSP and in terminal output (`baml describe`).
- **Goto-definition on virtual interface members.** Jumping to definition on a virtual call or field access through an interface projection, for example `d.as<Serializer>.encode()` or `p.as<Named>.fullname`, now navigates to the declaration in the interface (the required signature, the default method, or the field). Previously this returned nothing.
- **Catch and pattern bindings.** The error binding of `catch (e) { ... }` is now highlighted as a parameter, and hover/type info on catch-clause and pattern bindings reports the resolved binding type instead of `unknown`.
- **Range requests.** `semantic_tokens_in_range` classifies only the scopes a viewport touches, so a range query returns exactly the full-document tokens filtered to that range.

Under this the contextual words `as`, `type`, `true`, `false`, and `null` are re-lexed into dedicated keyword tokens (`KW_AS`, `KW_TYPE`, `KW_TRUE`, `KW_FALSE`, `KW_NULL`). This is internal to the parser and does not change what you write. Separately, the parser no longer panics on an incomplete raw string (a trailing run of `#` with no closing quote) while you are still typing.

Authors: rossirpaulo, codeshaunted

## Resolution-first LSP semantic tokens, with go-to-definition for interface virtual members

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

Editor highlighting now classifies every identifier inside an expression body by what it *resolves to* rather than by inferred type, following rust-analyzer's model. The old type-map approach is gone: a name is tagged from resolution facts (`MemberResolution`, `ResolvedName`, `DefinitionKind`), and a reference is highlighted the same way as its definition.

Highlights:

- **Token modifiers.** Semantic tokens now carry a modifier bitset (`declaration`, `defaultLibrary`, `deprecated`, `readonly`, `async`), advertised in `TOKEN_MODIFIERS`. Declaration sites (`function foo` -> `foo`) get `declaration`; `baml` standard-library entities get `defaultLibrary`.
- **New token types.** `true`/`false` now highlight as a new `boolean` type, and string escape sequences like `\n` and `\u{1f600}` get a new `escapeSequence` type. These are the same two additions picked up by the CLI terminal palette. Separately, `true`, `false`, and `null` are now re-lexed as distinct token kinds (`KW_TRUE`/`KW_FALSE`/`KW_NULL`), but only `true`/`false` map to the `boolean` type.
- **Range requests.** A new `semantic_tokens_in_range` resolves only the scopes a viewport touches, cached per scope, so large files no longer re-classify on every request. Its output is guaranteed to equal the full token stream filtered to the range.
- **Go-to-definition on interface virtual members.** `d.as<Serializer>.encode()` and `p.as<Named>.fullname` now navigate to the declaration in the interface (the required signature or the default method), where previously there was no jump target.
- **Catch bindings.** The error binding of `catch (e) { ... }` is now tracked as its own definition site and highlighted as a parameter, matching how it is scoped.
- **Hover on pattern and catch bindings.** Hovering a pattern binding or a `catch` binding now shows the resolved type instead of `unknown`, agreeing with completion type info.

The parser also stops indexing past end-of-input when a raw-string opener (`#"`) runs to EOF, fixing a panic on incomplete input, and a typing-robustness test now runs the classifier over every prefix and mid-scope edit of a set of sources.

Authors: rossirpaulo, codeshaunted

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

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

This release distributes BAML syntax highlighting beyond VSCode, adding a highlight.js definition published as `@boundaryml/baml-highlightjs` and a tree-sitter grammar mirrored to `BoundaryML/baml-treesitter` for Neovim, Helix, and Zed.

**Highlights**

- **highlight.js.** A new `@boundaryml/baml-highlightjs` package registers a `baml` language covering declaration blocks (`class`, `enum`, `interface`, `function`, `client<llm>`, `generator`, `retry_policy`, `template_string`, `test`), expression forms (`let`/`const`/`match`/`spawn`/`defer`/`watch`), `env.VAR` references, and Jinja `{{ ... }}` / `{% ... %}` markup inside `#"..."#` prompt bodies. It ships a plain ES module plus a self-registering `dist/baml.js` for classic script tags and CDN use.
- **tree-sitter.** A new grammar (name `baml`, scope `source.baml`) is mirrored to `BoundaryML/baml-treesitter` with a committed generated `src/parser.c`, so Neovim (nvim-treesitter), Helix, and Zed can build it directly from the repo. It ships `queries/highlights.scm` and `queries/injections.scm`, which inject prompt bodies as `jinja`.
- **VSCode/TextMate.** The bundled grammar now highlights shorthand constructor fields (a field name immediately followed by a value, without a colon), so shorthand and colon forms render consistently.

The hljs port and tree-sitter grammar both validate against the same `.baml` fixtures as the existing TextMate grammar, and CI now gates on all three. To use 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" });
```

This is a distribution and tooling change. The BAML language itself is unchanged.

Authors: hellovai

## The 0.14 editor latency regression is fixed, and incomplete raw strings no longer panic the parser.

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

This release fixes the 0.14 LSP mutex and latency regression, so editor features stay responsive under load again.

- **Inlay hints and semantic highlighting are memoized.** `semantic_tokens` and `file_annotations` (the inlay-hint pass, renamed from `annotations`) are now Salsa tracked queries. Both walk every function body against type inference, measured at 40 to 150ms on real projects, and editors re-request them on every scroll. They are no longer recomputed while the file is unchanged.
- **Bounded LSP ingress.** The server now runs a single process-owned ingress scheduler behind bounded outbound accounting. Serialized frames are capped (`MAX_OUTBOUND_FRAME_BYTES`, `OUTBOUND_QUEUE_BYTES`), and a saturated or oversized outbound queue now surfaces as `OutboundSaturated` or `OutboundOversized` backpressure instead of an unbounded writer queue or a dropped message.
- **Incomplete raw strings no longer panic.** A run of hashes at end of input (for example `##` or `client<llm> C { key ##`) previously let `find_token_after_hashes` return a one-past-the-end index that callers used to index the token buffer. It now returns `None` at EOF, so these malformed inputs produce diagnostics rather than crashing.
- **Agent-skill warnings moved into the toolchain.** The passive skill warning now lives in the `baml-cli` toolchain binary instead of the `baml` wrapper, so it ships with every nightly and fires even when the toolchain is invoked directly. It prints only on the `init`, `run`, `generate`, and `pack` authoring commands, not on machine-facing or utility invocations. `[update] auto_check = false` still disables the network refresh.
- **`baml agent install` archives the replaced skill.** An install that overwrites a skill now moves the previous version into a `baml-old_skills` slot inside the skills directory rather than a throwaway backup. The name `baml-old_skills` is reserved: a skills source that ships a skill by that name is rejected, and an archived copy under it does not count as an installed skill.

Authors: rossirpaulo, ATX24

## Revert of the LSP semantic-token rework drops token modifiers and interface-member goto-definition

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

This nightly reverts the LSP semantic-tokens rework (#3867) to land a separate change first, so editor behavior rolls back to its prior state. Nothing new ships here.

What rolls back:

- **Semantic token modifiers are gone.** The `declaration`, `defaultLibrary`, `deprecated`, `readonly`, and `async` modifiers (the whole `ModifierSet`) are removed from the LSP output, so identifiers are highlighted by base token type only. A definition name and a reference to it now look the same.
- **The `boolean` and `escapeSequence` token types are removed.** `true` and `false` no longer get a distinct `boolean` classification, and string escapes lose their own token type.
- **Goto-definition on virtual interface members no longer resolves.** An access like `d.as<Serializer>.encode` (`InterfaceVirtualMethod` / `InterfaceVirtualField`) now returns no location instead of jumping to the interface's method or field declaration.
- **Hover on pattern and catch bindings reports `unknown`.** The inferred type for `PatternBinding` sites is no longer computed for type info.

Internally this also restores `true`, `false`, `null`, `as`, and `type` to plain `WORD` tokens rather than the contextual `KW_*` kinds, and folds the separate `CatchBinding` definition site back into `PatternBinding`. These are not observable in your `.baml` source. The commit describes the revert as temporary.

Authors: rossirpaulo

## Semantic highlighting is now resolution-driven, and go-to-definition works on interface members accessed through `.as<Interface>`

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

The LSP semantic-tokens layer was rewritten to classify every name by what it resolves to rather than by inferred type, so highlighting now matches how the compiler actually sees your code. Editor users get the visible payoff; the change is otherwise internal to the language server.

**Highlights**

- **Resolution-first semantic tokens.** Identifiers in expression bodies are now colored by their resolution facts (`MemberResolution`, `ResolvedName`, `DefinitionKind`) instead of the type system. Two new token types ship: `boolean` (for `true`/`false`) and `escapeSequence`. Five token modifiers are now advertised: `declaration`, `defaultLibrary`, `deprecated`, `readonly`, and `async`. A definition site carries `declaration`, and `baml`-package entities carry `defaultLibrary`.
- **Go-to-definition on interface members.** Accessing a virtual interface field or method through `p.as<Named>.fullname` (or `.encode()`) now jumps to the declaration inside the interface. Previously both cases resolved to nothing and the request did nothing.
- **Hover on `catch` and pattern bindings.** Hovering the binding in `catch (e) { ... }`, or a pattern binding, now reports the real inferred type instead of `unknown`, matching what completions already showed. These bindings are tracked under a distinct `CatchBinding` site and highlight as parameters.
- **Contextual keywords in the syntax tree.** `as`, `type`, `true`, `false`, and `null` are now re-lexed into dedicated `KW_*` syntax kinds instead of bare words. This is what lets the tools above treat them uniformly.
- **Parser robustness while typing.** Incomplete raw strings such as a lone `#"...` no longer let the parser index past the end of input, fixing panics on mid-edit source.

Authors: rossirpaulo, codeshaunted

## Editor highlighting now colors each name by what it resolves to

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

The LSP semantic-token provider (`semantic_tokens`) was rewritten to classify every name by what it resolves to rather than by its inferred type. Identifiers in your `.baml` files are now colored as the class, enum variant, interface field, parameter, local, or `baml` standard-library symbol they actually refer to, following the same resolution facts the compiler uses.

**Highlights:**

- **Token modifiers are now emitted.** Declaration sites carry `declaration`, and primitives like `string` and `int`, plus anything from the `baml` package, carry `defaultLibrary`. The legend also advertises `deprecated`, `readonly`, and `async`.
- **New token types `boolean` and `escapeSequence`.** `true` and `false` and string escapes such as `\n` now highlight distinctly, in both the editor and the terminal `describe` output.
- **Go-to-definition works on virtual interface members.** Jumping from `d.as<Serializer>.encode()` or `p.as<Named>.fullname` now lands on the method or field declaration inside the interface. Previously these resolved to no location.
- **Range-scoped tokens.** `semantic_tokens_in_range` classifies only the requested viewport on demand instead of the whole file.
- **Hover on catch bindings reports a real type.** The binding in `catch (e) { ... }` now shows its inferred type on hover instead of `unknown`, and it is highlighted as a parameter.

Under the hood, the contextual words `as`, `type`, `true`, `false`, and `null` are now re-lexed into distinct syntax kinds (`KW_AS`, `KW_TYPE`, and so on) rather than plain words, which is what lets these classifications and the escape or boolean coloring line up. This does not change how you write BAML.

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 distributed as standalone grammar packages beyond the VSCode extension: `@boundaryml/baml-highlightjs` for highlight.js and a tree-sitter grammar mirrored to `BoundaryML/baml-treesitter` for Neovim, Helix, and Zed.

- **highlight.js**: a new npm package `@boundaryml/baml-highlightjs` registers a `baml` language for hljs-based renderers (markdown-it, marked-highlight, rehype-highlight). It covers declaration blocks (`class`, `enum`, `interface`, `function`, `client<llm>`, `generator`, `retry_policy`, `template_string`, `test`), expression bodies (`let`/`const`/`match`/`for`/`spawn`/`defer`/`watch`), `env.VAR` references, and Jinja markup (`{{ ... }}`, `{% ... %}`) inside `#"..."#` prompt bodies.
- **tree-sitter**: a full grammar (`grammar.js`) with `highlights.scm` and `injections.scm` queries, consumed by commit pin. Prompt and `template_string` bodies are injected as `jinja`. Raw strings are supported for 1 to 8 hash levels and backtick strings for 1 to 3 tick ladders, with no external scanner.
- **TextMate grammar**: the VSCode grammar adds a `constructor-field-shorthand` rule, so shorthand constructor fields (`name value` without a colon) now highlight correctly.

Both new packages are the single source of truth under `typescript2/pkg-grammar-hljs` and `typescript2/pkg-grammar-treesitter`, validated in CI against the shared `.baml` fixture corpus, and synced to read-only mirror repos.

To use the highlight.js definition:

```bash
npm install highlight.js @boundaryml/baml-highlightjs
```

Authors: hellovai

## 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

## Internal grammar-mirror sync workflow now stages untracked files before committing

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

No user-facing changes. This nightly only touches release automation: the `sync-grammar-mirror.yml` workflow now runs `git add -A` before committing instead of relying on `commit -am`, so newly assembled files (`dist/`, `package.json`, the publish workflow) that are untracked in the mirror repo get included in the sync commit rather than dropped. If you consume BAML through published packages this does not change any language behavior, CLI command, or API.

Authors: hellovai

## BAML syntax highlighting ships as the npm package `@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` and mirrored to `BoundaryML/textMate-baml`, the repo GitHub Linguist vendors for `.baml` highlighting on github.com and the source Shiki's registry fetches. If you render BAML in your own tooling, you no longer need to copy the grammar by hand.

- **`@boundaryml/baml-grammar`**: the default export is a ready-to-use Shiki `LanguageRegistration`, and `./baml.tmLanguage.json` plus `./language-configuration.json` are exported for vscode-textmate and Monaco. The default export is the grammar inlined as a JS object literal, so bundlers that reject JSON import attributes (Metro, some Vite configs) work without `with { type: "json" }`.
- **Editor auto-close fix**: in `language-configuration.json` the auto-closing pair for the `{# ... #}` template block was corrected from `["{#", "}"]` to `["{#", "#}"]`, so typing `{#` now closes with `#}` in the VS Code extension.
- **Grammar invariants are now guarded**: `scopeName: source.baml` and the `.baml` file type are pinned by tests, and a new CI job compiles the grammar with Linguist's own Oniguruma-to-PCRE compiler so a change that would break github.com highlighting fails the PR instead of surfacing at a later Linguist release.

Using it with Shiki:

```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" });
```

The grammar itself is unchanged; this release is packaging and distribution plus the `{# #}` auto-close correction.

Authors: hellovai

## `baml agent install` no longer hits the GitHub API and picks a better install directory

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

`baml agent install` now downloads the skill repo's `main`-branch tarball directly from codeload instead of resolving the head commit through the GitHub REST API. The unauthenticated REST API caps you at 60 requests per hour, which used to hard-block installs for anyone without GitHub credentials. The tarball endpoint needs no credentials, so a brand-new user can install with nothing configured.

- **No API dependency on the default path.** The commit recorded as provenance in `state.toml` is now read from the tarball's pax global header (`comment=<sha>`, written by `git archive`) rather than a separate API call. A mirror that omits the header still installs fine, just with no recorded provenance.
- **Smarter install directory.** With `--dir` omitted, BAML installs at the nearest ancestor containing `baml.toml` or `baml_src`. Inside a git repo the search never leaves the repo, falling back to the repo root (where agent harnesses look for `.claude/skills/`). Outside a repo the search stops before your home directory, so a stray `~/baml_src` won't pull the install into `$HOME`.
- **Actionable failure.** If the download fails because GitHub is unreachable, the error now points you at the offline path.

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

Authors: ATX24

## Interface associated types now require a `Self.` qualifier

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

Associated types referenced inside an interface body now require a `Self.` qualifier: `Item`, `Error`, and `SortError` must be written `Self.Item`, `Self.Error`, and `Self.SortError`. The bare forms no longer resolve.

- **`Self.` on associated types.** Interface method signatures and bodies now reference their own associated types through `Self.`. In the standard library, `iter(self) -> Iterator<Item = Item, Error = Error>` became `Iterator<Item = Self.Item, Error = Self.Error>`, and `sort(self) -> Self throws SortError` became `throws Self.SortError`. Update any interface that names an associated type without the prefix.
- **`throws` required on interface methods.** Every method declared in an interface must now include a `throws` clause. Declarations like `find(self) -> Self.Record` and `get(self) -> Self.Item` must add one, for example `find(self) -> Self.Record throws never`. Use the method's real throw set if it can throw.
- **Explicit projection qualifiers take associated bindings.** The `as<I>` qualifier on a projection now pins the interface's associated types: `account.as<PublicIdentity>.key` becomes `account.as<PublicIdentity<Key = string>>.key`.
- **Canonical projection rendering.** A determined type-variable projection now prints as its fully qualified triple. `read_item<T extends BoxLike>(box: T) -> T.Item` renders as `-> (T as BoxLike).Item` in printed signatures and list metadata.

The remaining changes are internal reorganization with no user-facing syntax or behavior: interface impls are consolidated into a single `impls` store (the `implements_for` representation is removed), and associated-projection lowering moves into `builder/associated_projection.rs`.

Authors: 2kai2kai2

## Grammar mirror sync now stages untracked files

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

No user-facing changes. This nightly only fixes the `sync-grammar-mirror.yml` release workflow: it now runs `git add -A` before committing instead of `git commit -am`, so newly assembled but untracked files (`dist/`, `package.json`, the publish workflow) are included in the mirror sync rather than dropped.

Authors: hellovai

## The BAML TextMate grammar now ships as the npm package `@boundaryml/baml-grammar`

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

The BAML TextMate grammar is now published as the npm package `@boundaryml/baml-grammar`, assembled from `typescript2/pkg-grammar` and pushed to the `BoundaryML/textMate-baml` mirror on every grammar change on `canary`. The package's default export is a Shiki `LanguageRegistration`, so you can highlight BAML without wiring up JSON imports.

- **`@boundaryml/baml-grammar`**: new external package. The default export is a ready-to-use Shiki `LanguageRegistration`; for vscode-textmate or Monaco you can instead import `@boundaryml/baml-grammar/baml.tmLanguage.json` and `@boundaryml/baml-grammar/language-configuration.json`. The grammar is self-contained and embeds no other language scopes, so no second grammar needs loading.
- **`.baml` highlighting on github.com**: the same mirror is what GitHub Linguist vendors, so `.baml` files will render highlighted on github.com once Linguist next pulls it. A new `linguist-compile-gate` CI job runs Linguist's own grammar compiler against every grammar change, so a regex that Linguist's Oniguruma-to-PCRE conversion cannot handle fails the PR instead of silently breaking github.com later.
- **`{#` auto-close fix**: in editors the `{#` pair now closes with `#}` instead of `}`.

```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` no longer hits the GitHub API and picks its install directory more carefully

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

`baml agent install` now downloads the skill repo's main-branch tarball directly from codeload instead of calling the GitHub REST API. That API path was subject to the unauthenticated 60-requests/hour rate limit, which could hard-block installs; the tarball endpoint needs no credentials and is not rate-limited the same way, so a brand-new user with no GitHub credentials can install.

- **No API dependency for the default install.** The commit recorded as provenance is now recovered from the tarball's pax global header (the `comment=<sha>` that `git archive` writes) rather than a separate API lookup. A mirror whose archive omits that header still installs fine, just with no recorded provenance.
- **Clearer failure path.** If the download fails, the error now tells you to install from a local copy with `baml agent install --from <url-or-path>`.
- **Install directory selection changed.** With `--dir` omitted, BAML installs at the nearest ancestor holding `baml.toml` (else `baml_src`) within the current 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 pulls installs into `$HOME`; the final fallback is the current directory.

Authors: ATX24

## Associated types in interface method signatures must now be qualified with `Self.`

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

Associated types referenced inside interface method signatures must now be written `Self.`-qualified. A bare `Item` or `CompareError` in a signature no longer resolves; write `Self.Item`, `Self.CompareError`, and so on.

**Highlights**

- **`Self.`-qualified associated types in signatures.** The standard-library interfaces were rewritten to qualify associated-type references: `Comparable.compare` now throws `Self.CompareError`, `Sortable.sort` throws `Self.SortError`, `Summable.sum` returns `Self.Sum`, and every `Iterable`/`Iterator` method threads `Self.Item` and `Self.Error` (e.g. `iter(self) -> Iterator<Item = Self.Item, Error = Self.Error>`, `collect(self) -> Self.Item[] throws Self.Error`). If you declare your own interface with associated types, apply the same qualification in its method signatures.
- **`$stream` companion classes no longer inherit `implements` blocks.** A generated `Foo$stream` class now drops the in-body `implements` blocks it cloned from `Foo` (`stream_class.implements.clear()`), so it only participates in an interface when you write an explicit out-of-body `implement Foo for Bar$stream { ... }`. A method valid for the base class is no longer silently required of its stream companion.
- **Interface projections in `.as<>` must bind the associated type.** Test call sites that project through an interface were updated to bind the associated type, e.g. `account.as<PublicIdentity<Key = string>>.key` instead of `account.as<PublicIdentity>.key`.

A projected type variable now prints in its canonical form in CLI output: a return type of `T.Item` for `T extends BoxLike` renders as `(T as BoxLike).Item`.

```baml
interface Container {
  type Item
  type Error = never

  function first(self) -> Self.Item throws Self.Error
}
```

The bulk of this release is an internal reworking of how interfaces flow through the typed IR. It introduces a `MakeVirtualBoundMethod` rvalue for binding an interface method on a receiver whose impl is unknown at compile time (the value analogue of a virtual call), and it collapses the legacy `implements_for` item-tree representation into the unified `impls` store. These are compiler-internal changes with no direct user-facing surface beyond the signature and printing changes above.

Authors: 2kai2kai2

## 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

## Internal cleanup: demo `baml_src` files removed, no user-facing change.

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

This nightly only deletes the internal `baml_language/demo/baml_src/` demo files (type-checking samples for arithmetic, control flow, higher-order functions, recursion, and similar). Nothing in the compiler, CLI, or generated clients changed, so there is no observable effect on your BAML code.

Authors: sxlijin

## Internal cleanup: demo `.baml` sources removed from the repository

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

This nightly only deletes the `baml_language/demo/baml_src/` directory and its example `.baml` files (`arithmetic.baml`, `control_flow.baml`, `higher_order.baml`, `types.baml`, and others). These were in-repo demos, not shipped functionality, so there is no user-visible change to the language, compiler, or CLI.

Authors: sxlijin

## 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 agent install` installs skills from the repo head, plus outdated warnings for the toolchain and skills

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

`baml agent install` now installs agent skills from the current head of `BoundaryML/baml-skill` by default, and the `--latest` flag has been removed since that behavior is now the default.

- **`baml agent install`**: the default source is the skill repo's head commit, resolved first so the downloaded tarball and recorded provenance match. The `--latest` flag is gone. `--from` still accepts a tar.gz URL, a local tar.gz archive, or a local directory. A default install records the installed commit under a `[skills]` section in `~/.baml/state.toml`; installs from a custom `--from` source clear it, since they carry no commit identity.
- **Outdated warnings**: normal `baml` commands now print a `warning` to stderr when your toolchain is behind the latest for its channel ("Update it with baml toolchain update"), when your installed skill is behind the repo head ("use baml agent install to upgrade it"), or when a project has no skill installed. Warnings from existing caches print immediately with no network call.
- **`[update] auto_check`**: the background freshness check now defaults to on and runs at most once per 24 hour window, hidden behind the command's own runtime with a 5 second timeout so an unreachable network can't stall your work. Opt out in config:

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

The release pipeline no longer packages skills as a separate artifact (the `build-agent-skills` job and `scripts/package-agent-skills` were removed), consistent with installing from the repo head at runtime.

Authors: ATX24

## Host integers passed to a `float` parameter now widen instead of riding through as ints

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

A host integer arriving at a declared `float` slot is now coerced to a genuine float at the FFI boundary, so a function declared `-> float` hands back a float rather than the int unconverted. This closes a gap where value-shaped host encoders (a Python `7`, a JS integral `Number`) arrived Rust-side as an `Int` and skipped the `float` conversion.

**Highlights**

- **`int` to `float` widening at the call boundary.** Passing an integer to a `float` parameter now returns a real float. Note this widening exists only when crossing the host boundary; the type system still does not relate `int` and `float`, so it is not expressible from a BAML call site.
- **Numeric union member selection.** A host int against a numeric union now picks the best member regardless of declaration order: an exact `int` member wins outright, and between `bigint` and `float` the lossless `bigint` (exact for every i64) beats the lossy `float` (which rounds above 2^53). So `int | float | bigint` and `float | bigint | int` both land on `int`, and `float | bigint` lands on `bigint`.
- **`baml playground` session token removed.** The browser-mode URL no longer carries a `?token=...` query parameter, and `/api` requests no longer require one. The origin check on loopback still guards the API. If you scripted against the old token-bearing URL, drop the `?token=` segment.

```python
# a Python int into a float parameter now widens
result = round_trip_float(x=7)
assert isinstance(result, float)
assert result == 7.0
```

Authors: sxlijin, rossirpaulo

## `baml ide install` renames `--path` to `--dir`, and legacy tests seed playground previews

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

The `--path` flag on `baml ide install` is now `--dir`, so any script that copies the VSIX to a directory for manual install must be updated.

**Highlights**

- **`baml ide install --dir <PATH>`**: the flag that copies the active toolchain's VSIX into a directory was renamed from `--path` to `--dir`, since `--path` was ambiguous about whether it wanted a file or a folder.
- **Clearer install failures**: when `code` or `cursor` is installed but its CLI shim is not on your PATH, the error no longer dead-ends. It now walks through saving `baml-vscode.vsix` and running "Extensions: Install from VSIX...", with OS-specific download paths and Command Palette chords.
- **Legacy tests seed function previews**: `test` blocks that use the older `functions [...]` and `args { ... }` config form now have their `args` parsed into typed JSON instead of being dropped. The playground sidebar lists each such test under its function with a `preview` label, and clicking it loads those args into the function's args form without running anything. A test targeting multiple functions produces one preview entry per function.

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

Authors: rossirpaulo, sxlijin

## `baml playground` reuses one browser window instead of stacking new ones on every open

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

Running `baml playground` and re-triggering an open no longer spawns a fresh browser window each time. In browser mode, `OpenPlayground` now navigates the page that is already connected over the WebSocket and only spawns a system browser window when nothing is connected, with a five second debounce so a second window is not opened while the first is still loading.

- **Repeated opens reuse the window.** The most recent open target (project, function, test, testset) is recorded and broadcast to connected pages so they navigate in place. A page that connects after the request, a freshly spawned window or a reconnect, is replayed the same target when it sends `RequestState`.
- **Playground args form reconciles on schema change.** The old per-function `normalizeArgs` seed-and-normalize guards never re-ran when a function kept its name but changed type on hot-reload, leaving stale cached form values. They are replaced by `reconcileArgs`, which re-runs per project/function/schema: compatible values and surplus raw-JSON keys are preserved, missing required params and fields get schema defaults, incompatible values reset to defaults, and parameters with declared BAML defaults stay absent so the runtime evaluates them.
- **Required nested classes seed fully.** `defaultValueForSchema` now populates required acyclic class refs recursively, so every default a typed widget shows is present in the serialized value, with a marker-only cut point for irreducibly recursive types.
- **Docs.** Gemini model references across the client provider and guide pages were updated to current model names, and `google-ai` now marks `model` as required.

Authors: sxlijin, rossirpaulo, hellovai

## Browser-mode `baml playground` reuses one window, and the args form reconciles across hot-reloads

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

In browser mode, repeated `baml playground` opens now navigate the already-connected page instead of stacking a new browser window each time.

- **`baml playground` window reuse.** A browser-mode `OpenPlayground` now sends the navigation over the existing WebSocket so a connected page moves to the requested function or test in place. A system browser window is spawned only when no page is connected, guarded by a 5-second debounce so a still-loading window is not opened twice. A page that connects after the request (a freshly spawned window or a reconnect) is replayed the last open target when it sends `RequestState`.
- **Args form reconciliation.** `reconcileArgs` replaces the old `normalizeArgs`. When a function keeps its name but its schema changes (a hot-reload), the playground args form now reconciles your cached values against the new schema: it fills defaults for newly required fields, resets values that no longer fit their type, and leaves parameters with declared BAML defaults absent so the runtime evaluates them. Previously the seed and normalize passes ran once per function name and never re-ran on a same-name schema change, so form state could go stale.
- **Docs.** Gemini model references were updated to the gemini-3 series (for example `gemini-3.5-flash` and `gemini-3.1-pro-preview`), and the `model` option 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 clearer errors when the editor CLI is missing

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

`baml ide install --path <dir>` now copies the active toolchain's VSIX into that directory as `baml-vscode.vsix` instead of invoking an editor, so you can install the extension by hand.

- **`--path <PATH>`**: new flag on `baml ide install`. It creates the target directory if needed and writes `baml-vscode.vsix` there, printing the destination path.
- **Editor CLI resolution**: `--code` and `--cursor` now fail with an explicit message when the CLI is not on `PATH` (for example, `VS Code CLI \`code\` was not found on PATH`) rather than trying to run a missing command. `--code` no longer conflicts with `--cursor`, and on Windows the `.cmd` shim is checked before the bare command name.
- **`watch` retention fix**: the VM prunes stale Watch graph roots when a watched local leaves scope, including non-local exits such as a native panic unwinding a frame before its `Unwatch` runs. Programs that do not use `watch` no longer retain objects through the Watch graph.

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

Authors: jubjub727, aaronvg

## CI-only release: size-gate PR comments now work on 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 and docs-preview jobs switched from `thollander/actions-comment-pull-request` to `marocchino/sticky-pull-request-comment`, and the size report step now prints to stdout and the run summary before attempting the PR comment. On fork PRs the `GITHUB_TOKEN` is read-only, so the comment post fails there; making it best-effort (`continue-on-error: true`) keeps the report visible without failing CI. Several jobs were also renamed (`size-gate (linux)` to `measure-baml-size (linux)`, `size-gate (report)` to `enforce-baml-size`). None of this affects the BAML toolchain or generated code.

Authors: sxlijin

## `baml agent install` now installs skills from the repo head, and the wrapper warns when your toolchain or skills are outdated

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

The `baml` wrapper now prints a `warning` before running your command when either your toolchain or your installed agent skills are behind, and `baml agent install` now defaults to installing skills from the `BoundaryML/baml-skill` repo head.

**Highlights**

- **Outdated warnings.** Before a command runs, the wrapper surfaces a warning when your active toolchain is behind the latest channel release ("Update it with `baml toolchain update`.") or when your agent skills are behind or missing ("use `baml agent install`"). Warnings come from local caches, so they print immediately, and a freshness refresh runs in the background at most once per TTL window while your command executes.
- **`baml agent install` default changed.** With no `--from`, it now resolves the head commit of `BoundaryML/baml-skill`'s `main` branch, downloads the tarball at that exact commit, and records the installed commit in `~/.baml/state.toml` under a `[skills]` section. This provenance is what lets the wrapper tell you when your skills go stale.
- **`--latest` flag removed.** Installing from the repo head is now the default, so the `--latest` flag on `baml agent install` is gone. `--from` (a tar.gz URL, local archive, or directory) still overrides the default; installs from a custom `--from` source record no commit and clear any stale provenance.
- **`[update] auto_check` is now opt-out.** It defaults to on. Set `auto_check = false` in `config.toml` to skip the background freshness checks entirely.

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

The previous versioned, checksum-verified skill archive path was dropped along with its release-time packaging, so skills are no longer shipped as release artifacts.

Authors: ATX24

## Python ints passed to a `float` parameter now arrive as floats

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

Passing a host int into a `float`-typed slot now widens to a float at the call boundary, so a function declared `-> float` hands back a genuine float instead of letting the int ride through unconverted. This is a boundary-only coercion: the type system still does not relate `int` and `float`, so it is not expressible from a BAML call site.

**Highlights**

- **`int` to `float` widening.** A Python `7` (or a JS integral `Number`) into a `float` parameter arrives Rust-side as a float. `round_trip_float(x=7)` returns `7.0`, and an `int` set on a `float` field is coerced the same way.
- **Numeric union member selection.** A host int lands on the best union member rather than the first declared one. An exact `int` member always wins; between `bigint` and `float` the lossless `bigint` beats the lossy `float` regardless of declaration order, so `float | bigint` routes an int to `bigint`.
- **`baml playground` no longer uses a session token.** The printed URL is now just `http://localhost:<port>/` with no `?token=...` to copy. Browser-mode `/api` access relies on the loopback origin check alone, and the "open the exact URL including the token" banner and retry warning are gone.

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

Authors: sxlijin, rossirpaulo

## Legacy config-block tests now seed playground previews, and `baml ide install` takes `--dir`

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

Tests written with the legacy `functions`/`args` config blocks now surface in the playground sidebar as clickable preview cases that seed a function's args form (and its prompt and cURL previews) without running anything.

- **Legacy test args reach the playground.** Previously a legacy test's `args` block was dropped on the way to the playground, so cURL previews had nothing to fill in. Now the arguments are parsed with their types preserved (`int`, `float`, `bool`, `string`, `null`, arrays, and nested maps) and serialized to JSON. Clicking a test row under a project hydrates the args editor for its target function; the selection stays in sync with source edits and releases when you navigate away.
- **Multi-function tests expand.** A test that lists several functions (`functions [Alpha, Beta]`) now produces one preview row per function rather than only targeting the first. Qualified references such as `workflows.Classify` are kept intact.
- **`baml ide install --path` is now `--dir`.** The flag was renamed because it only ever accepted a directory. Update any scripts that passed `--path`.
- **Clearer `baml ide install` failures.** When neither the `code` nor `cursor` CLI is found on PATH, the error no longer dead-ends. It explains what could not be done and walks through the manual VSIX install with OS-specific guidance (the download directory and the Command Palette chord).

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

Authors: rossirpaulo, sxlijin

## `baml playground` reuses one browser window instead of stacking new ones

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

Running `baml playground` in browser mode now navigates the already-open page over the WebSocket when you trigger Open Playground again, rather than spawning a fresh browser window every time.

**Highlights**

- **Browser window reuse.** In browser mode, `OpenPlayground` broadcasts a navigation to any connected page and only spawns a system browser when nothing is connected. A 5-second debounce (`BROWSER_SPAWN_DEBOUNCE`) prevents a second window from opening while the first is still loading. A page that connects after the request (a freshly spawned window or a reconnect) is replayed the last open target when it sends `RequestState`, so it lands on the right function or test.
- **Args form reconciliation.** The playground args form now reconciles cached form values against the current schema via `reconcileArgs`, replacing the old per-function seed and normalize guards that never re-ran when a same-name function changed type. Hot-reloading a function that gains a required field now fills in the new field's default while preserving values you already typed, and required nested class fields are populated so what the typed widgets show matches what gets encoded.
- **Gemini docs updated.** Model references across the provider and client docs moved to the 3.x line (for example `gemini-3.5-flash` and `gemini-3.1-pro-preview`), and the `google-ai` `model` option is now documented as required with no default.

Authors: sxlijin, rossirpaulo, hellovai

## `baml playground` reuses one browser window and reconciles the args form when a function's schema hot-reloads

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

Two playground fixes for browser mode (`baml playground`).

- **Repeated opens reuse the browser window.** Triggering `OpenPlayground` while a playground page is already connected now navigates that page in place over the WebSocket instead of spawning a new window each time. A system browser window is only opened when nothing is connected, and a 5 second debounce prevents a second window from stacking while the first is still loading. A page that connects after the request (a freshly spawned window or a reconnect) is sent to the last requested target when it asks for state.
- **The args form reconciles against schema changes.** The form's `normalizeArgs` pass is replaced by `reconcileArgs`, which re-runs for each project/function/schema combination rather than once per function name. Editing a function so a same-name signature gains a required field now keeps your typed-in values and fills the new field with its default, instead of leaving the cached form stale. Required nested classes are seeded recursively so every default a widget displays is also present in the serialized value, with recursive class paths cut off at a marker-only node. Parameters with declared BAML defaults stay omitted so the runtime evaluates them.

Docs updates refresh Gemini model references (for example `gemini-2.5-flash` becomes `gemini-3.5-flash`) across the client provider pages; `model` is now marked required on the `google-ai` provider.

Authors: sxlijin, rossirpaulo, hellovai

## `baml ide install --path` extracts the VSIX for manual install, plus Windows CLI detection fixes

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

`baml ide install` gains a `--path <PATH>` flag that copies the active toolchain's VSIX into a directory as `baml-vscode.vsix` instead of invoking an editor, so you can install the extension by hand.

**Highlights**

- **`--path` for manual install.** `baml ide install --path ./out` creates the directory if needed, writes `baml-vscode.vsix` into it, and prints `extracted BAML IDE extension to <dest>`. No editor CLI is required in this mode.
- **VS Code / Cursor CLI now found on Windows.** `baml ide install --code` and `--cursor` now locate the `code.cmd` / `cursor.cmd` shim before the bare command name, so the editor CLI is detected on Windows where it ships as a `.cmd`.
- **Clearer errors when the CLI is missing.** `--cursor` and `--code` now fail with `Cursor CLI \`cursor\` was not found on PATH` or `VS Code CLI \`code\` was not found on PATH` instead of blindly running a command that does not exist.
- **`--code` and `--cursor` no longer conflict.** The `conflicts_with = "cursor"` restriction was dropped from `--code`.

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

**Watch graph retention fix.** Objects mutated in BAML code that does not use `watch` are no longer retained through the Watch dependency graph, and frame teardown (including a native panic unwinding a frame before its explicit `Unwatch`) now unregisters that frame's watched locals. Notification behavior is unchanged: watched array, map, and byte-array element assignments still emit exactly one notification before teardown.

Authors: jubjub727, aaronvg

## CI-only release: no changes to BAML itself

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

This nightly contains no changes to the BAML language, runtime, or generated clients. The only commit reworks internal CI so the size-gate and docs-preview jobs behave on fork pull requests, where a read-only `GITHUB_TOKEN` previously made the PR-comment step fail. Nothing here affects your code.

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 type `data` and `onData` as the partial stream shape

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

The `onData` callback and `data` field returned by BAML's generated React hooks now resolve to `StreamDataType<FunctionName>` when `stream: true`, instead of a union of the final and streaming shapes. This makes the hook types match what actually arrives mid-stream.

Highlights:

- **`HookInput.onData` and `HookOutput.data`** no longer widen to `FinalDataType<FunctionName> | StreamDataType<FunctionName>` in streaming mode. Both are now `StreamDataType<FunctionName>`, so nullable partial fields carry their streaming state rather than appearing already-final.
- **`server_streaming_types.ts`** entries now resolve to `partial_types.*` (for example `partial_types.Recipe`, `partial_types.Person`) so the nullable streaming typing is accurate.

Because streaming `data` and `onData` are narrowed to `StreamDataType<FunctionName>`, existing `stream: true` code that read fields as if they were final (non-`@stream.not_null` nested fields) may now surface type errors and need null handling.

```typescript
const options: HookInput<'MakeSemanticContainer', { stream: true }> = {
  stream: true,
  onData: response => {
    if (!response) return
    // streaming state on a partial field, not the final string
    const status = response.class_needed.s_20_words.state
    void status
  },
}
```

Authors: mvanhorn

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

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

A named `client<llm>` with `provider openai` or `provider anthropic` and no explicit `api_key` now defaults the key to the provider's conventional env var, matching the inline `"openai/model"` shorthand. You no longer need to write `api_key env.OPENAI_API_KEY` for the common case.

- **`openai` and `openai-responses`** default `api_key` to `OPENAI_API_KEY`; **`anthropic`** defaults to `ANTHROPIC_API_KEY`. Other providers are untouched.
- The read is soft: it goes through `baml.env.get`, so an unset variable yields `null` instead of panicking. Offline `render_prompt` keeps working without the variable set, and the request goes out unauthenticated when it is absent, as before.
- An explicit `api_key` in `options` always wins, including `api_key null`, which suppresses the default entirely.
- The synthesized dependency is surfaced to the LSP env-var tooling, so the variable shows up in the playground's env panel anchored at the `options` block.

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

Here `C` picks up `OPENAI_API_KEY` automatically.

Authors: antoniosarosi

## Class, array, and map values now render inside `prompt` backtick strings instead of vanishing

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

Interpolating a composite value with `${...}` inside a `prompt` backtick string now produces its `to_string` form, matching an ordinary backtick string. Previously the prompt assembler only stringified scalars (string, int, float, bool) and silently dropped every class, array, or map, yielding an empty string (B-563).

- **`${expr}` composites render via implicit `to_string`.** A new `render_prompt_values` routes each interpolated value through `string.from`, the same conversion an ordinary `${expr}` applies. So `prompt\`Point ${p}\`` and `\`Point ${p}\`` now yield byte-identical output for a class value `p`.
- **`baml.ToString` overrides are honored.** Because composites go through the real implicit `to_string`, a user-defined `implements baml.ToString` renderer applies in a `prompt` exactly as it does elsewhere.
- **`${role(...)}` is unaffected.** `Role` markers pass through raw, so `assemble_prompt_ast` still uses them to split a prompt into chat messages.

Authors: antoniosarosi

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

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

A `retry_policy` that omits `initial_delay_ms`, `multiplier`, or `max_delay_ms` (for example one with only `max_retries`, or one using a `strategy { ... }` sub-block) no longer aborts every LLM call. Those fields are now optional on `RetryPolicy`, and when unset they arrive as `null`; the backoff arithmetic previously evaluated `null + 0.0` and failed with `VM internal error: cannot apply binary operation: any + float` before any HTTP request went out.

**Highlights**

- **`retry_policy` delay fields are now optional (B-488).** `initial_delay_ms`, `multiplier`, and `max_delay_ms` default to 200ms, 1.5x, and 10000ms respectively when omitted, matching the legacy engine. Retries with a bare `max_retries` or a `constant_delay` / `exponential_backoff` strategy now run instead of crashing.
- **Bitwise NOT (`~`) now computes the correct value (B-766).** `~` was previously a silent identity (`~x == x`). It is now two's-complement complement (`~x == -x - 1`), so `~0 == -1`, `~1 == -2`, and `~255 == -256`.
- **Enum and string-literal-union fields serialize correctly to JSON (B-728).** A class field whose type is a union containing an enum, including an optional enum (`E?` == `E | null`), serialized as `null` through the typed `baml.json.to_string<T>` walker. It now renders the variant name, matching `.to_json()`.

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

Authors: antoniosarosi

## `retry_policy` blocks that omit delay fields no longer crash, plus enum-union JSON and bitwise `~` fixes

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

A `retry_policy` that leaves out the delay fields no longer aborts every call with a VM type error. Three independent fixes shipped in this release.

- **`retry_policy` delay fields are now optional (B-488).** `initial_delay_ms`, `multiplier`, and `max_delay_ms` on `RetryPolicy` became optional. When a policy omits them (for example, one that sets only `max_retries`, or uses a `strategy { ... }` sub-block that does not lower into these fields), they arrive as `null` at runtime. Previously the retry-delay math evaluated `null + 0.0` and crashed with `VM internal error: cannot apply binary operation: any + float` before any request went out. The math now substitutes the canonical defaults (200ms initial delay, 1.5x multiplier, 10000ms max delay).
- **Enum and enum-union fields serialize to their variant name (B-728).** A class field whose declared type is a union containing an enum, including the optional enum form `E?` (which is `E | null`), previously serialized as `null` through the typed `baml.json.to_string<T>` path. It now renders the variant name as a plain string, matching the structural `.to_json()` path.
- **Bitwise NOT (`~`) now computes the complement (B-766).** `~` previously lowered to a silent identity, so `~x` returned `x`. It now evaluates to `-x - 1`, the two's-complement complement.

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

Authors: antoniosarosi

## Bitwise NOT, enum-in-union JSON, and a `retry_policy` crash fixed

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

A `retry_policy` that omits its delay fields no longer crashes every LLM call with a VM type error. This nightly ships three fixes.

- **`retry_policy` delay fields are now optional.** `initial_delay_ms`, `multiplier`, and `max_delay_ms` may be omitted (for example a block with only `max_retries`, or one using a `strategy { ... }` sub-block). Previously an omitted field arrived as `null` at runtime, and the retry-delay math evaluated `null + 0.0`, aborting the call with `cannot apply binary operation: any + float` before any HTTP request. Missing fields now fall back to 200ms / 1.5x / 10000ms.
- **Bitwise NOT (`~`) now computes the two's-complement complement** instead of silently returning its operand. `~x` is `-x - 1`, so `~0` is `-1` and `~255` is `-256`. The one corner is `~INT_MIN`, which errors on the intermediate negation rather than returning a wrong value.
- **Enum fields inside a union no longer serialize as `null`.** A field typed as a union of enums (`E | F`) or the optional enum `E?` (which is `E | null`) now renders the held variant's name through both `baml.json.to_string` and `.to_json()`. String-literal-union fields were unaffected and continue to serialize as the plain string.

```baml
~255  // -256 now; was silently 255 before
```

Authors: antoniosarosi

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

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

For BAML React hooks called with `{ stream: true }`, both the `data` field and the `onData` callback argument are now typed as `StreamDataType<FunctionName>` instead of the union `FinalDataType<FunctionName> | StreamDataType<FunctionName>`. During streaming, nested fields can still be null or absent until they complete, so the union let TypeScript treat those fields as fully populated. The corrected type forces you to handle the partial shape.

- **`server_streaming_types.ts` generation** now resolves streaming return types against `partial_types.*` rather than the final `types.*`. So a hook streaming `ExtractResume` reports `partial_types.Resume`, where nested fields carry their streaming state instead of assuming completion.
- **Non-streaming hooks are unchanged.** With `{ stream: false }`, `data` and `onData` still use `FinalDataType<FunctionName>`, and accessing a field as its final non-null type continues to type-check.

```typescript
const { data } = useMakeSemanticContainer({ stream: true })
// data?.class_needed.s_20_words.state is 'Pending' | 'Incomplete' | 'Complete'
// data?.class_needed.i_16_digits is number | null | undefined (must null-check)
```

Authors: mvanhorn

## A named `client<llm>` with no `api_key` now defaults to the provider's conventional 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 the inline `"openai/model"` shorthand.

- **`openai` and `openai-responses`** default `api_key` to `OPENAI_API_KEY`, and **`anthropic`** defaults to `ANTHROPIC_API_KEY`. Other providers (for example `vertex-ai`) are untouched and still require explicit credentials.
- The read is soft: it goes through `baml.env.get`, so an unset variable yields `null` rather than panicking. Offline paths like `render_prompt` keep working without the variable set, and an unauthenticated request goes out exactly as before.
- An explicit `api_key` in `options` (including `api_key null`) suppresses the default, so your config always wins.
- The synthesized dependency is surfaced to LSP env-var tooling, so the variable shows up in the playground's env panel anchored at the client's `options` block.

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

With `OPENAI_API_KEY` set, requests built from `C` now carry an `Authorization: Bearer` header sourced from that variable.

Authors: antoniosarosi

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

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

Interpolating a composite value with `${expr}` inside a `prompt` backtick string now renders it, fixing B-563 where a class, array, or map silently produced an empty string. The `prompt` tag pushed each interpolated value into the assembler raw, and the assembler only stringified scalars (string, int, float, bool), so any composite was dropped. Values now route through `string.from`, the same implicit `to_string` an ordinary backtick string applies.

- A `prompt` string is now byte-identical to the equivalent ordinary backtick string for the same interpolated values.
- A `baml.ToString` override is honored inside a `prompt`, exactly as it is in an ordinary string.
- A `${role(...)}` marker still stays raw, so it continues to split the prompt into chat messages.

```baml
class Point {
  x int
  y int
}

function main() -> string {
  let p = Point { x: 1, y: 2 }
  let xs = [10, 20, 30]
  let cc = baml.llm.ContextClient { name: "c", provider: "openai", default_role: "system", allowed_roles: ["system"] }
  let ctx = baml.llm.Context { client: cc, tags: {} }
  let render = prompt`Point ${p} list ${xs}`
  render(ctx).text()
}
```

Authors: antoniosarosi

## A `retry_policy` that omits its delay fields no longer crashes every call

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

Three correctness fixes land in this nightly, each removing a wrong result or a crash.

- **`retry_policy` with omitted delay fields no longer aborts calls.** A `retry_policy` block that specifies only `max_retries` (or uses a `strategy { ... }` sub-block) leaves `initial_delay_ms`, `multiplier`, and `max_delay_ms` unset, so they arrived as `null` at runtime. The retry-delay math then evaluated `null + 0.0` and every call died with `VM internal error: cannot apply binary operation: any + float` before any HTTP request. Those fields are now optional and fall back to the legacy defaults (200ms initial delay, 1.5x multiplier, 10000ms cap), keeping the backoff arithmetic well-typed.
- **Enum and string-literal-union fields serialize correctly.** A class field whose 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 as a plain string, matching the structural `.to_json()` path.
- **Bitwise NOT (`~`) now computes the complement.** `~x` previously lowered to a silent identity (`~x == x`). It now correctly evaluates to `-x - 1`, so `~0` is `-1` and `~255` is `-256`.

The delay fields are now genuinely optional:

```baml
retry_policy P {
    max_retries 3
}
```

Authors: antoniosarosi

## `retry_policy` no longer crashes on omitted delays, plus fixes for `~` and enum JSON fields

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

Three correctness fixes land in this nightly: bitwise NOT (`~`), enum serialization in union and optional JSON fields, and `retry_policy` blocks that omit their delay settings.

- **`retry_policy` with omitted delays no longer crashes (B-488).** A `retry_policy` that sets only `max_retries`, or uses a `strategy { ... }` sub-block, leaves `initial_delay_ms`, `multiplier`, and `max_delay_ms` unset. Those arrived as `null` at runtime, and the retry-delay math evaluated `null + 0.0`, aborting every call with `cannot apply binary operation: any + float` before any request went out. The delay fields are now optional (`int?` / `float?`) and the backoff math falls back to the canonical defaults (200ms, 1.5x, 10000ms).
- **Bitwise NOT (`~`) now computes the complement (B-766).** `~x` previously lowered to a silent identity, so `~x` returned `x`. It now evaluates two's-complement complement, `~x == -x - 1`. The one corner case is `~INT_MIN`, which throws because the intermediate `-INT_MIN` overflows, rather than returning a wrong answer.
- **Enum variants in union and optional JSON fields serialize as their name (B-728).** A class field typed as a union containing an enum, including the optional enum `E?` (which is `E | null`), serialized the held variant as `null` when rendered through the typed `baml.json.to_string<T>` path. It now renders the variant name.

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

For B-728, an optional enum field now round-trips through the typed serializer as the variant name instead of `null`:

```baml
enum E { A  B }
class OptEnum { e E? }
baml.json.to_string(OptEnum { e: E.A }) == "{\"e\":\"A\"}"
```

Authors: antoniosarosi

## Bitwise `~`, enum fields in union JSON, and delay-less `retry_policy` all stop misbehaving

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

Three grounded bug fixes land in this nightly, each affecting behavior you can observe at runtime.

- **Bitwise NOT (`~`) now computes the two's-complement complement.** Previously `~` lowered to a silent identity, so `~x` returned `x`. It now correctly desugars to `-x - 1`: `~0` is `-1`, `~1` is `-2`, `~255` is `-256`, and `~(-1)` is `0` (B-766).
- **Enum-typed fields in union or optional positions serialize as the variant name.** A class field whose declared type was 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 string, matching the plain enum-field case: `C { e: E.A }` serializes to `{"e":"A"}` (B-728).
- **`retry_policy` no longer crashes when delay fields are omitted.** `initial_delay_ms`, `multiplier`, and `max_delay_ms` are now optional. Omitting them (for example, a policy with only `max_retries`, or one using a `strategy { ... }` sub-block) used to abort every call with `VM internal error: cannot apply binary operation: any + float` before any request went out. The retry-delay math now substitutes the canonical defaults (200ms, 1.5x, 10000ms) and stays well-typed (B-488).

Authors: antoniosarosi

## 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 release adding Python SDK coverage for outbound reified generics

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

This release contains no user-visible changes. It adds Python SDK test coverage for non-generic BAML functions that return fully-concrete generic instances (reified generics flowing outbound).

The diff touches only test files: new `make_int_box`, `make_int_container`, `make_nested_box`, and `make_int_str_bool_triple` fixtures in `ns_generic_tests/types.baml`, and matching assertions in `test_generic_calls.py` that verify the Pydantic 2 host decoder binds the correct type args (via `__pydantic_generic_metadata__`) when a concrete `GenericBox<int>`, `ContainerShapes<int>`, nested `GenericBox<GenericBox<int>>`, or `GenericTriple<int, string, bool>` is returned. No language, runtime, or generator behavior changed.

Authors: sxlijin

## Version bump to 0.14.1 across all packages

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

This release only stamps the `0.14.1` version number across the Rust crate, Python and Node.js SDKs, and the VS Code extension. There are no behavior, syntax, or API changes.

Authors: rossirpaulo

## `baml.math` is removed: array reductions are now `float[]` methods

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

The `baml.math` namespace is gone (B-712). Its aggregations are now methods on numeric arrays, so `baml.math.sum(xs)` becomes `xs.sum()`, and `mean`/`median` attach to `float[]`.

- **`baml.math.sum` / `mean` / `median` removed.** Call them as methods instead. `sum()` is defined on both `int[]` (returning `int`) and `float[]` (returning `float`); `mean()` and `median()` are defined on `float[]` and return `float`. `mean` and `median` throw `baml.errors.InvalidArgument` on an empty array, while `sum` of an empty array is `0` / `0.0`.
- **`baml.math.trunc` removed with no public replacement.** It was only backing internal retry-delay math and is now a private helper. Use the range-checked, throwing `float.itrunc()` or the float-returning `float.trunc()` instead.
- **SDK imports changed.** The generated `baml.math` module no longer exists. Code importing from `baml_sdk.baml.math` (Python) or `./baml_sdk/baml/math` (TypeScript) must be updated.
- **`baml playground` works over SSH tunnels.** New `--port` pins the listen port (erroring if taken) and `--no-open` suppresses the browser launch, which is now also skipped automatically in headless sessions (SSH, or no display on Linux). The auto-picked port range moved from 3700 to 4265, and browser sessions mint a per-session token carried on the URL as `?token=` that every `/api` request must present.

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

```bash
ssh -L 4265:localhost:4265 user@host
baml playground --no-open   # forward the port, then open the printed ?token= URL locally
```

Authors: aaronvg, rossirpaulo, antoniosarosi

## `baml.math` aggregates become `float[]` methods, and `baml playground` works over SSH tunnels

0.14.1 · canary · 2026-07-08

The `baml.math` namespace is gone. Its aggregates are now methods on `float[]`, so `baml.math.mean(xs)` and `baml.math.median(xs)` become `xs.mean()` and `xs.median()`, matching the existing `xs.sum()`. If your code called `baml.math.*`, you must update the call sites.

**Highlights**

- **`float[].mean()` / `float[].median()`.** These replace `baml.math.mean` and `baml.math.median`. Both still return `float` and both throw `baml.errors.InvalidArgument` on an empty array. `median` sorts a copy (IEEE 754 `totalOrder`, matching `float[].sort()`), leaving your array untouched.
- **`baml.math.sum` removed.** `float[].sum()` remains the surface for summing; it no longer routes through `baml.math.sum`. `int[].sum()` is unchanged and still raises `baml.panics.IntegerOverflow` on overflow.
- **`baml.math.trunc` removed from public surface.** The saturating truncation it provided is now an internal helper used only by retry-delay math. For truncating a `float` yourself, use `float.itrunc()` (range-checked, throwing) or `float.trunc()`.
- **`baml playground` over SSH.** Two new flags: `--port` pins a port (erroring if taken) and `--no-open` suppresses the browser. Browser opening is now also skipped automatically in headless sessions (SSH, or Linux with no display). The default port scan moved from 3700 to 4265.
- **Playground session token.** In browser mode the server mints a per-session token and prints a URL carrying `?token=...`. Open that URL verbatim and treat the token as a secret; `/api` requests without it are rejected. The playground UI now shows a banner when it can't reach the server, prompting you to open the exact printed URL.

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

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

The new statistical methods:

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

Authors: rossirpaulo, antoniosarosi

## First tracked canary release; no diff available to summarize changes.

0.14.0 · canary · 2026-07-08

This is the earliest canary release on record, with no predecessor to diff against and no commit log available. There is nothing concrete to report about what changed in this build. Consult the corresponding release tag or the stable changelog for the substantive changes in this line.

## `PromptAst` gains `.text()` and `.messages()` accessors for reading rendered prompts

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

`PromptAst` values, returned by `<Fn>$render_prompt` and the built-in `prompt` tag, now have accessors that let you read the rendered prompt from BAML. Previously it was an opaque `$rust_type` handle with no way to inspect it, and converting it to a string leaked the Rust `Debug` of the handle.

- **`PromptAst.text()`** renders the prompt as plain text: each chat message becomes a `[role]` header line followed by its content, messages separated by a blank line. Role-less content (a prompt with no `${role(...)}` markers) has no header.
- **`PromptAst.messages()`** returns an ordered `PromptMessage[]`. `PromptMessage` is a new class with `role` and `content` string fields; media parts render as a readable placeholder like `image::url(...)`.
- **`to_string` is now readable.** `string.from(prompt)`, string interpolation, and `to_string` all yield `text()` instead of the opaque handle dump. The CLI's readable value print does the same.

Both accessors are declared `throws never`.

```baml
function render_readable() -> 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()
}
```

This returns `[system]\nYou are helpful.\n\n[user]\nHi World!`.

Authors: antoniosarosi

## `$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` now renders a prompt without requiring the client's credential env var to be set. Previously, calling `Extract$render_prompt(...)` eagerly built the real client, which read `api_key env.OPENAI_API_KEY` and panicked with `env var not found` if the variable was unset, even though rendering only needs the client's provider and role metadata, not its credentials.

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. Only the offline render path reads env options leniently, substituting the empty string for a missing variable.

```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 }}."#
}
```

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

Authors: antoniosarosi

## First tracked nightly on this channel with no diff to report.

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

This is the earliest nightly recorded on this channel, so there is no predecessor to compare against and no commit log or file diff is available. No user-visible changes can be described for this release.

## Test-only release adding SDK coverage for outbound reified generics

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

This nightly contains no user-facing changes. The only diff adds Python SDK test coverage and BAML fixtures for non-generic functions that return fully-concrete generic instances, the outbound mirror of the existing `consume_int_wrapper` case.

The new fixtures (`make_int_box`, `make_int_container`, `make_nested_box`, `make_int_str_bool_triple`) exercise four reification shapes the host decoder must handle: one TypeVar used once, one TypeVar reified across every field shape, a nested generic type arg, and multiple TypeVars over mixed fields. The corresponding tests assert the returned Pydantic instances carry the expected bound type args. No runtime, compiler, or codegen behavior changed.

Authors: sxlijin

## Version bump to 0.14.1

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

This release only stamps the version number from `0.14.0` to `0.14.1` across the Rust crate, the Python and Node bridge SDKs, and the VS Code extension. There are no functional or behavioral changes.

Authors: rossirpaulo

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

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

The `baml.math` namespace has been removed. Its aggregates are now methods on `float[]`: call `xs.mean()` and `xs.median()` instead of `baml.math.mean(xs)` and `baml.math.median(xs)`, and `xs.sum()` (already the canonical form) instead of `baml.math.sum(xs)`. Both `mean()` and `median()` return `float` and throw `baml.errors.InvalidArgument` on an empty array. `median()` sorts a copy, so your array is left untouched.

```baml
test "reductions" {
  assert.is_true(baml.deep_equals([1.0, 2.0, 3.0, 4.0].mean(), 2.5))
  assert.is_true(baml.deep_equals([3.0, 1.0, 2.0].median(), 2.0))
}
```

Other changes:

- **`baml.math.trunc` is no longer public.** It backed internal retry-delay math and moved to a private saturating helper. For truncating a `float` to an `int` in your own code, use the range-checked, throwing `float.itrunc()`, or the float-returning `float.trunc()`.
- **`baml playground` works over SSH tunnels.** New `--port` pins an exact port (erroring if it is taken) and `--no-open` suppresses the browser launch, which is now also skipped automatically in headless sessions (SSH, or Linux with no display). The default port scan starts at 4265 instead of 3700. On a remote host, forward the port and open the printed URL locally:

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

- **Per-session token on the playground URL.** In browser mode the printed URL now carries a `?token=...` query parameter that authorizes `/api` requests. Open it verbatim and treat it as a per-session secret. Requests without the token are rejected.
- **The playground UI only receives the env vars your project references.** Instead of shipping the whole server process environment, it sends the variable names your BAML source names (resolved via `all_env_var_names()`, with dynamic keys still resolved lazily through the `EnvVarRequest` round-trip), so unrelated secrets are no longer exposed.

These reductions are backed by private VM natives now. There is no change to their observable results.

Authors: aaronvg, rossirpaulo, antoniosarosi

## `baml.math` is gone: use `float[]` methods `.mean()` and `.median()`, and run the playground over SSH

0.14.1 · canary · 2026-07-08

The `baml.math` namespace has been removed. The aggregate free functions `baml.math.sum`, `baml.math.mean`, and `baml.math.median` no longer exist. `mean` and `median` are now methods on `float[]`, alongside the existing `sum`, so rewrite `baml.math.mean(xs)` as `xs.mean()`. `baml.math.trunc` is also gone from the public surface; prefer the throwing `float.itrunc()` or the float-returning `float.trunc()`.

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

**Highlights**

- **`baml.math` removed.** `sum`, `mean`, and `median` are attached to the structural `float[]` type as methods. `mean()` and `median()` throw `baml.errors.InvalidArgument` on an empty array. `median()` sorts a copy, so your array is left untouched. `int[].sum()` still returns `int` and raises `IntegerOverflow` on overflow.
- **`baml playground` works over SSH tunnels.** Forward the port (`ssh -L 4265:localhost:4265 user@host`) and open the printed URL locally. The playground now binds the first free port starting at 4265 (was 3700).
- **`--port` and `--no-open` flags.** `--port` pins an exact port and errors if it is taken. `--no-open` suppresses the browser, which is also skipped automatically in headless sessions (SSH, or no display on Linux).
- **Per-session token.** In browser mode the printed URL carries a `?token=...` query parameter that authorizes `/api` requests. Open it verbatim and treat it as a per-session secret. The playground now sends only the environment variables your project actually references, not the full process environment.

Authors: rossirpaulo, antoniosarosi

## First tracked canary release, with no diff available to summarize.

0.14.0 · canary · 2026-07-08

This is the oldest release recorded on the canary channel, so there is no predecessor to compare against and no commit log or file diff was available. No user-visible changes can be described from the provided context.

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

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

`baml.llm.PromptAst`, the handle returned by `<Fn>$render_prompt` and the `prompt` tag, now has accessors that read the rendered prompt instead of exposing an opaque handle.

- **`text()`** returns the prompt as plain text: each chat message rendered as a `[role]` header line followed by its content, messages separated by a blank line. Role-less content has no header.
- **`messages()`** returns an ordered `baml.llm.PromptMessage[]`. `PromptMessage` is a new class with `role` and `content` string fields. Media parts render as a placeholder such as `image::url(...)`.
- **`to_string` is now readable.** `string.from(prompt)`, string interpolation, and `to_string` all yield `text()` instead of dumping the handle's Rust `Debug` representation. The CLI's value print for a `PromptAst` 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 example above yields `[system]\nYou are helpful.\n\n[user]\nHi World!`. To inspect the fully-resolved provider request (URL, headers, body, remapped roles) rather than the rendered prompt, keep using `<Fn>$build_request`.

Authors: antoniosarosi

## `$render_prompt` renders offline without the client's `api_key` env var

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

Rendering a prompt no longer requires the client's credential env var to be set. `<Fn>$render_prompt` only needs the client's provider and role metadata, so it now builds the client leniently: a missing variable like `env.OPENAI_API_KEY` yields the empty string instead of panicking with `env var not found`. Playground previews and cURL renders work with no key in the environment.

- **Offline render tolerates missing env vars.** The generated client constructor takes a `lenient` flag, and the render path passes `lenient = true`, so `env.X` client options read via `get_or_panic_lenient` return `""` when unset rather than panicking.
- **Network paths stay strict.** `<Fn>$call` and `<Fn>$build_request` construct with `lenient = false`, so a missing `api_key` env var still panics and names the variable. This avoids silently building an unauthenticated request.

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
