#4584·baml

0.17.0 rejects unary `!` on `bool | null` (E0001) that 0.15/0.16 type-check but panic on at runtime (`VM internal error: cannot apply unary operation: ! any`)

Author: BenSpexCreated Aug 25, 2026Updated Aug 25, 2026

Summary

Unary ! applied to an optional bool (bool | null, produced by an optional chain) behaves differently on every recent line, and two of the three behaviors look unintended:

toolchain baml check runtime
0.15.0 accepts VM internal error: cannot apply unary operation: ! any when the optional is null
0.16.0 accepts (same class as 0.15 — the check admits what the VM can't evaluate)
0.17.0 rejects: error[E0001] mismatched types — expected bool, found bool null``

So 0.15/0.16 have a checker/VM soundness gap (the expression type-checks but panics at runtime on the null branch), and 0.17.0 fixes it by rejecting at check time — a breaking change we could not find in a changelog/migration note. Meanwhile #4498 ("truthiness in condition positions", merged 2026-08-19, apparently post-0.17.0) explicitly makes ! operands truthiness-coercing (null falsy), which would re-legalize the same expression on a later line.

Reproduce

baml_src/main.baml (no clients, no prompts — parses on all three toolchains):

baml
class Feld {
    value string,
    abstained bool,
}

class Doc {
    betreff Feld?,
}

function present() -> Doc { Doc { betreff: Feld { value: "x", abstained: false } } }
function absent() -> Doc { Doc { betreff: null } }

function old_form(d: Doc) -> bool { !d.betreff?.abstained }
bash
$ BAML_VERSION=0.15.0 baml check --from .
Finished checked 1 file(s)            # green
$ BAML_VERSION=0.16.0 baml check --from .
Finished checked 1 file(s)            # green
$ BAML_VERSION=0.17.0 baml check --from .
main.baml:13:37 error[E0001] mismatched types — expected `bool`, found `bool | null`

Runtime on the lines that accept it:

bash
$ BAML_VERSION=0.15.0 baml run --from . -e 'old_form(present())'
false                                  # fine — the optional is populated
$ BAML_VERSION=0.15.0 baml run --from . -e 'old_form(absent())'
VM internal error: cannot apply unary operation: ! any

Where it bit a real consumer

zenzy-atlas (Rust product, CLI seam, 35 BAML functions) migrating 0.15.0 → 0.17.0 per the classic-syntax removal. Two committed test-file asserts of the shape assert.is_true(!f.betreff?.abstained); had been green for months on 0.15 — they only ever passed because the fixtures happened to populate the field; the null branch would have been a hard VM panic, not a failed assert. 0.17.0's E0001 was the first signal. We rewrote them as !(f.betreff?.abstained ?? true) (evaluated on both branches on 0.17.0: present→true, absent→false — the meaning the old form crashed trying to express).

Asks

  1. Confirm the 0.17.0 E0001 rejection is the intended 0.17-line behavior (it looks like the correct fix for the 0.15/0.16 soundness gap), and add it to a 0.17 migration note — !optional is a pattern the old checker admitted, so migrating codebases hit it as a surprise E0001 with no changelog anchor.
  2. Clarify the intended end state relative to #4498: on a truthiness line, !(bool | null) becomes legal again (!null == true). If that ships in 0.17.1/0.18, code fixed for 0.17.0 stays valid, but it would be good to know whether the strict-E0001 window is deliberate.
  3. If 0.15/0.16 get any further releases: the checker admitting an expression the VM answers with VM internal error is worth a backport of either the rejection or the truthiness coercion.

Environment: Linux x86_64, baml wrapper 0.2.0, toolchains 0.15.0 / 0.16.0 / 0.17.0 via BAML_VERSION selector.