Skip to content

Spawn options + future combinators - #3625

Merged
antoniosarosi merged 25 commits into
canaryfrom
antoniosarosi/Future-Combinators
Jun 8, 2026
Merged

antoniosarosi merged 25 commits into
canaryfrom
antoniosarosi/Future-Combinators

Conversation

@antoniosarosi

@antoniosarosi antoniosarosi commented Jun 1, 2026 •

Copy link
Copy Markdown
Contributor

BEP-034 — concurrency via spawn / await. Single PR covering the spawn-options core and the future combinators (supersedes #3596).

Public BAML API (BEP-034 concurrency)

This is the complete user-facing surface this PR adds. Everything below is callable from .baml source.

Syntax

spawn { body }                       // -> Future<T, E>; runs body concurrently
await f                              // -> T; blocks until f settles, re-throws its error
spawn with t1 with t2 { body }       // middleware: applies transformers left-to-right
  • spawn { e } evaluates to Future<T, E> where T is the body's value type and E its throws type. The body starts running immediately; the future is the handle.
  • await f suspends until f settles, then yields its value or re-throws its error. await distributes over a union of futures: await (Future<A,E1> | Future<B,E2>) yields A | B (the error side is contributed to the awaiter's throws surface).
  • spawn ... with <transformer> ... runs each transformer over an implicit SpawnParams before the runtime creates the future (see middleware below).

baml.future — futures & combinators

class Future<T, E> {
  fn cancel(self) -> bool throws never        // request cancel; false if already settled
  fn is_settled(self) -> bool throws never     // reached Ready | Error | Cancelled
  fn is_cancelled(self) -> bool throws never
  fn is_result(self) -> bool throws never      // settled with a value
  fn is_error(self) -> bool throws never        // settled with an error
  fn state(self) -> FutureState throws never
}

enum FutureState { Pending  Ready  Error  Cancelled }

// Combinators over a homogeneous Future<T,E>[] — each returns a new Future, so they compose.
fn all_complete<T, E>(futures: Future<T, E>[]) -> Future<T[], E>
//   all values in input order; losers KEEP RUNNING on failure; throws first error (input order)
fn all<T, E>(futures: Future<T, E>[]) -> Future<T[], E>
//   all values in input order; on first failure the rest are CANCELLED, then error re-thrown (JS Promise.all)
fn race<T, E>(futures: Future<T, E>[]) -> Future<T, E>
//   first to settle (success OR failure) wins; losers cancelled (JS Promise.race); empty array never settles
fn any<T, Err>(futures: Future<T, Err>[]) -> Future<T, AllFailed<Err>>
//   first to SUCCEED wins; losers cancelled; if all fail, throws AllFailed<Err> (JS Promise.any)

class AllFailed<E> { errors E[] }   // thrown by `any` when every input failed; errors in input order

baml.spawn — spawn options & with middleware

// Every `spawn` implicitly builds a SpawnParams; the `with` pipeline transforms it.
// A transformer is any function (SpawnParams<T,E>) -> SpawnParams<U,F>.
class SpawnParams<T, E> {
  body   () -> T throws E
  name   string?
  group  TaskGroup?
  cancel CancelToken?
  detach bool
}

// The built-in config transformer. `cancel` links a token; `detach = true` opts out of the
// parent→child cancel cascade and routes unhandled errors to the root task; `group` enrolls
// the spawn in a TaskGroup (parks FIFO until admitted). Leaves `body`/types unchanged.
fn options<T, E>(group: TaskGroup? = null, cancel: CancelToken? = null, detach: bool? = null)
  -> (SpawnParams<T, E>) -> SpawnParams<T, E> throws never

class TaskGroup {                                  // caps concurrency; excess spawns queue FIFO
  fn new(limit: int, name: string? = null) -> TaskGroup throws never
  fn cancel(self, pending: bool? = true, active: bool? = true) -> int throws never  // returns # signalled
  fn set_limit(self, limit: int) -> void throws never   // raise admits queued; lower never preempts; 0 pauses
  fn limit(self) -> int throws never
  fn name(self) -> string? throws never
  fn active_count(self) -> int throws never
  fn queued_count(self) -> int throws never
}

class CancelToken {                                // cooperative, one-shot; fires at next await
  fn new() -> CancelToken throws never
  fn any(tokens: CancelToken[]) -> CancelToken throws never   // fires when ANY input fires (one-directional)
  fn cancel(self) -> int throws never              // 1 if this call transitioned it, else 0
  fn is_cancelled(self) -> bool throws never
}

A cancelled task throws baml.panics.Cancelled at its next await.

Writing your own middleware

A transformer is just a function returning (SpawnParams<T,E>) -> SpawnParams<U,F>. It can set config
fields, wrap body (retry/timing), or replace it entirely (a type-changing transformer — e.g. a
fallback that catches and erases the error type to null):

function withFallback<T, E>(default_value: T) -> (SpawnParams<T, E>) -> SpawnParams<T, null> throws never {
  (params) -> {
    let original = params.body;
    SpawnParams {
      body: () -> { (original()) catch (e) { let e => default_value } },
      name: params.name, group: params.group, cancel: params.cancel, detach: params.detach,
    }
  }
}

// usage — `f : Future<int, null>`, so awaiting needs no catch:
let f = spawn with withFallback(99) { flaky() };

Transformers run eagerly at the spawn site, so a throwing transformer throws there (and is part of
the caller's throws surface). Wrong-shaped transformers are rejected at compile time with a concrete
"must return a SpawnParams" diagnostic.

Spawn options

  • spawn ... with baml.spawn.options(group?, cancel?, detach?) — config-only with clause
  • CancelToken (new / any / cancel / is_cancelled) + parent→child cancel cascade
  • TaskGroup rate limiting (FIFO admission, exact counts)
  • detach (fresh cancel token; unhandled errors routed to the root task)
  • strict with validation (must be a baml.spawn.SpawnConfig)

Future combinators (baml.future)

  • race / any / all / all_complete — pure-BAML stdlib over a new 1-byte AwaitAny engine primitive (__await_any: Future<T,E>[] -> int throws never, suspends until the first input settles)
  • AllFailed<E> aggregate error class for any
  • losers cancelled (race/any/all) or left running (all_complete) per the BEP table

Engine/runtime fixes found along the way

  • fire-and-forget errors (bug 2): a child's unhandled throw is now deferred (GC-rooted stash; future stays Pending with its wake signal fired) and consumed at the single observation point (future_ready, now &mut). The spawner's post-await drain surfaces only never-observed errors — a sibling handling a child's error no longer races the spawner into a spurious UnhandledThrow. (tests/fire_and_forget.rs)
  • AwaitAny index desync (bug 3): global-index bookkeeping fix that broke any
  • bare-bind catch arms (bug 4): let e => in a generic fn emitted a constant-false IsType (TypeVar→Void erasure) and silently rethrew; bare binds are irrefutable — no runtime test
  • copy propagation substitutes Terminator::Await operands (await <param> inside .map closures)
  • unannotated lambda params infer from generic array elements mentioning only the enclosing fn's rigid generics

Generics: inferred type args at runtime

  • TIR records call-site inferred bindings (call_type_instantiations); MIR threads them to frame.type_args (LoadType/TypeArgRef) — instances built inside generic fns carry real class_type_args, and typed arms like let e: AllFailed<string> match (tests/generics_runtime.rs)
  • typed arms naming enclosing TypeVars (let e: AllFailed<E>) lower via type-arg templates instead of erasing

Canary merge (f5df38a)

Absorbs the spawn/GC permit deadlock fix (full baml_tests now completes on this branch), BEP-044/BEP-57, instantiation expressions, bounds, and interface-dispatch type-arg threading. Canary's strict rigid-var checking exposed pre-existing inference gaps, fixed here:

  • await f contributes the future's E to escaping throws
  • callee generics matched only against rigid caller TypeVars now bind (map's U:=T)
  • lambda effective throws feed callee throws-generics — lifts the documented "current limitation" (covered callback throws no longer false-positive) and violations name concrete types
  • miscompile fix: ShortCircuit emit assumed a stack-carried (PhiLike) destination; with LoadType before call args the carry is validly rejected, so a || b as an inferred-generic call argument read an uninitialized local. Emit now materializes the destination on the short-circuit edge (no-op for PhiLike).
  • std any<T, E> → any<T, Err> (works around generic-param name capture vs map<U, E>; proper fix tracked separately)

with middleware (c37027a)

Full BEP middleware: spawn ... with a, b { body } builds a SpawnParams<T,E> and applies each transformer left-to-right — any plain BAML function (SpawnParams<T,E>) -> SpawnParams<U,F> (wrap/replace body, set config, change types). baml.spawn.options is now a pure-BAML transformer (native SpawnConfig deleted). Flushed out + fixed three language gaps: keyword segments in type paths, lambdas as block tail expressions, and unannotated lambdas now INFER their throws surface from the body (two tests asserting the old throws-never default updated). Tests: bex_engine/tests/middleware.rs incl. the BEP's withRetry.

Complete bug ledger (every bug found & fixed on this branch)

Engine / runtime

  1. Fire-and-forget errors bypassed consumers — a spawned child's unhandled throw settled its Future eagerly AND queued on the spawner; a sibling that awaited + handled the error was invisible to the engine, so the spawner re-surfaced an already-handled error as UnhandledThrow. Fixed with the deferred-error model: the error parks in a GC-rooted stash, the future stays Pending (wake signal fired), future_ready is the single observe-and-consume point, and the spawner's post-await drain surfaces only never-observed errors.
  2. AwaitAny index desync — global-index bookkeeping desync broke any (wrong winner index reported).
  3. Copy propagation didn't substitute Terminator::Await operands — await <param> inside a .map closure read an uninitialized slot.
  4. ShortCircuit emit miscompile — the emitter assumed the destination is always stack-carried (PhiLike); when inferred-generic calls push LoadType before args the carry is validly rejected, so a || b as a call argument left the value on the stack and read an uninitialized local (true || false evaluated false). Emit now materializes the destination on the short-circuit edge; MIR normalizes projection destinations. Latent before the canary merge — the suite deadlocked before reaching the || fixtures.

Type system / generics
5. Bare-bind catch arms in generic fns always rethrew — let e => emitted an IsType from its inferred type; a rigid TypeVar erases to Ty::Void in MIR making the test constant-false. Bare binds are irrefutable — no runtime test emitted (also drops tautological field tests in destructures).
6. Inferred call-site type args were discarded — TIR ran inference, substituted the return type, and threw the bindings away; MIR emitted frame type args only for explicit <...> call sites. Instances built inside generic fns carried empty class_type_args; typed arms like let e: AllFailed<string> never matched. Bindings now recorded (call_type_instantiations) and threaded to frame.type_args.
7. Typed arms naming enclosing TypeVars compiled to constant-false — let e: AllFailed<E> inside any<T, E> erased E→Void; now lowers via TypeArgRef templates resolved against the frame.
8. Recorded instantiations poisoned by fresh literal types — mk("hi") recorded T = Literal("hi") so is Box<string> compared unequal; recordings now widen_fresh(). Plus generic→generic chains recorded Unknown (TypeVar actuals refused); fixed with an allow-typevars backfill.
9. Unannotated lambda params didn't infer from generic array elements whose type only mentions the enclosing fn's rigid generics.

Throws tracking (pre-existing gaps exposed by canary's strict rigid-var checking)
10. await f contributed nothing to escaping throws — the future's E is now part of the awaiting body's throw set.
11. Callee generics matched only against rigid caller TypeVars never bound — map's U:=T in futures.map((f) -> { await f }) was skipped as a TypeVar actual, leaving U[] in result types.
12. Rigid TypeVar throw-facts collapsed lambda surfaces to Never — only genuinely open facts (fresh effect slots, Unknown) collapse now.
13. Callee throws-generics fed from lambda SURFACE instead of effective throws — an omitted-throws lambda's open surface left map's E unbound (leaking a raw TypeVar) or symbolic where the body demonstrably throws. Now fed from the side-table effective set: a non-throwing callback binds E:=never, a throwing one binds its concrete type. Lifts a documented "current limitation": a callback throw covered by the caller's declared throws no longer false-positives, and violations name concrete types instead of symbolic E.

Parser / lowering (flushed out by the middleware work)
14. spawn/await keywords rejected as path segments in type annotations (baml.spawn.SpawnParams) — parser type-path loop + CST dotted-name extractor.
15. A lambda as a block's tail expression was silently dropped from block elements — the block typed as void ("missing return value"); any function returning a transformer hit it (the std's json code had been dodging it with explicit return).
16. Unannotated lambdas defaulted to throws never with a local violation — they now infer their throws surface from the body (required for body wraps like () -> { original() } whose throws is a generic E). Omitted throws on callback params remains effect-polymorphic; explicit throws contracts remain enforced (verified: E0096 fires through the forwarding chain — an enforcement that itself depends on fixes 10–13).

Known issues (documented, not fixed here)

  • Generic-param name capture: a callee's generic (map<U, E>) sharing a name with a caller's generic false-binds through the receiver-substituted signature. Worked around (any<T, E> → any<T, Err>); proper fix is alpha-renaming on receiver substitution.
  • await does not yet distribute over unions of futures (BEP §await-distributes).

await over unions (94d2b46)

Awaiting Future<A, E1> | Future<B, E2> types as A | B with both error types tracked (BEP §await-distributes). Tests: bex_engine/tests/await_union.rs.

Review hardening (94d2b46)

CodeRabbit batch: any errors in INPUT order (+test) · explicit lambda throws contracts preserved · with-transformer throws contributed at spawn · copy-prop protects await xs[_i] · TaskGroup.set_limit(+N) admits up to the cap (+test) · deferred-error hygiene on cancel/fulfill · VM Spawn config validation · unified/completed generics walkers. Plus the CI fix: Object enum exceeded its 64-byte budget under heap_debug (UnscheduledFuture boxed) — the cause of the all-legs CI failure.

Canary merge round 4 (8b2c403)

Canary's c957417e7 ([codex] #3702) was a third independent implementation of inferred-generic runtime type args; this merge converges on it and fixes what the convergence broke:

  1. Adopted canary's CallPlan pipeline for inferred type args — removed our parallel call_type_instantiations TIR→MIR fallback (dead code after the switch). Canary's ShortCircuit emit fix (store_on_taken_path) is the same insight as ours; took theirs.
  2. runtime_call_type_args lost literal widening — canary's recording threads T = literal "hi" for mk("hi"), so an escaped instance never matches is Box<string> (runtime class-arg comparison is invariant). Restored widen_fresh on inferred bindings.
  3. Canary's stricter call-result recheck false-positived on unknown holes — spawn with middleware checks transformers against (SpawnParams<T,E>) -> SpawnParams<unknown, unknown>; the normalizer implements class-arg invariance as structural equality, so the holes (and BuiltinUnknown from erased unresolved typevars, e.g. a variable-bound transformer; and equivalent spellings like T = int | 99 from fallback-arg joins) all failed E0001. is_subtype now compares same-class args pairwise: holes match anything, concrete args must be mutual subtypes (invariance as equivalence). Concrete mismatches — including canary's new iterator-stdlib diagnostics — still report.
  4. heap_debug feature gap — FutureRead::ErrorPending arm added to the heap debugger's validity walk.
  5. task_group_cancel_cancels_members timing — the promptness bound times run_main including compilation; the stdlib grew (ns_iter). 2s → 5s, still far under the 10s sleep it guards.

Closes the deferred interface-dispatch inferred-type-args item (canary's #3702 ships it with runtime regressions).

Verified: cargo test --workspace --all-features — 185 suites / 0 failures (sdk_test_* excluded as in CI; they require cargo-nextest setup scripts) + prek clean, rustc 1.93.0.

Deferred (follow-up branch)

  • Result<T, E> type design + all_settled, .then()
  • cancellation-test panic-hook hygiene

Tests: bex_engine 227/0 (incl. combinators, fire_and_forget, generics_runtime, cancellation, task_group); baml_tests diagnostics 418/418, runtime fixtures 1650/1650 (one timing-sensitive fixture flakes only under full parallel suite load); snapshots regenerated.

Summary by CodeRabbit

  • New Features
    • spawn ... with middleware and options(...) for spawn configuration; CancelToken and TaskGroup APIs; future combinators all_complete, all, race, any; await-any (returns winning input index)
  • Tests
    • Comprehensive BEP-034 tests for spawn options, TaskGroup, CancelToken, combinators, middleware, fire-and-forget, and runtime behavior
  • Bug Fixes
    • Improved child-task error propagation and deferred fire-and-forget error semantics across await/catch boundaries

…+ CancelToken (BEP-034)

First slice of BEP-034 "spawn options": the `with` clause on `spawn`, a new
`baml.spawn` namespace with a BAML-surface `CancelToken`
(`new`/`any`/`cancel`/`is_cancelled`), and `baml.spawn.options(group?, cancel?)`
producing an opaque `SpawnConfig`. A user cancel token links into a spawned
task's effective token via a self-terminating watcher; firing it (or an
`any`-composed token) cancels the child, settles its future `Cancelled`, and
`await` re-throws it (catchable).

- Surface: ns_spawn/spawn.baml (CancelToken, SpawnConfig, TaskGroup stub, options)
- Runtime: SpawnConfigData + UnscheduledFuture.config; engine reads the config's
  cancel token and links a watcher in spawn_thread; GC traces the config ptr.
- Compiler: parse `with expr (, expr)*`; Expr::Spawn.with_exprs; thread an
  optional config operand through TIR/MIR/emit and the Spawn bytecode (now pops
  [closure, name, config]).
- `spawn`/`await` are now valid as path segments after `.` so `baml.spawn.X`
  parses (mirrors the existing KW_CLIENT precedent).

Deferred: `detach` (needs an OmittedArg-for-primitive codegen fix) and strict
`with`-clause validation.

Tests: 4 engine cancellation tests + ns_cancel_token fixture; snapshots regenerated.
Adds `detach = true` to `baml.spawn.options(...)` and the codegen fix it
depends on.

- Codegen: `Value::is_omitted()`; the generated optional-arg extraction now
  treats an omitted argument (the `OmittedArg` sentinel) the same as `null`
  (→ `None`), so optional builtin params — including primitives like `bool?` —
  accept being left out. Unblocks `options(group?, cancel?, detach?)`.
- detach: `SpawnConfigData.detach`; a detached spawn gets a fresh effective
  cancel token (`CancellationToken::new()` instead of `parent.child_token()`),
  derived before the user-token watcher links, so the parent cascade / unhandled
  throw does not reach it. An explicit `cancel` token still applies.
- Root-error routing: `BexThread.root_pending_errors` is propagated unchanged
  down the spawn tree; a detached spawn routes its unhandled fire-and-forget
  error to the root task's queue (surfaces at the root's next `await`) rather
  than the spawner's.

Tests: 2 detach engine tests + a detach BAML fixture test; snapshots regenerated.
Adds `baml.spawn.TaskGroup` and `spawn ... with baml.spawn.options(group = g)`.
A group caps how many spawns referencing it run concurrently; excess spawns
queue FIFO and start as earlier ones settle (the `spawn` still returns its
`Future` immediately).

- Surface: `TaskGroup.new(limit, name?)`, `cancel(pending?, active?) -> int`,
  `set_limit`, `limit`, `name`, `active_count`, `queued_count`; `options(group=)`.
- Runtime: `bex_vm_types::TaskGroupInner` admission state (std `Mutex` + a
  per-waiter `oneshot`). Members register SYNCHRONOUSLY at spawn time (so
  `cancel()`/`active_count()`/`queued_count()` right after `spawn` are exact);
  the body task parks on a `TaskGroupTicket` for a slot — without the heap
  permit, so a queued task doesn't block GC — and releases on settle, waking
  the next FIFO waiter. The ticket's select is cancel-biased so a member
  cancelled via `group.cancel()` never runs. `set_limit` grows (drains
  waiters) / shrinks (no preempt) / `0` (pauses).
- Engine: `SpawnConfigData.group` threaded through `spawn_thread`; admission
  runs before `run_thread_event_loop`.

Verified spawns actually parallelize for real IO (execution-only timing):
limit=3 runs three 200ms sleeps in ~200ms; limit=1 serializes them (~600ms).

Tests: 7 engine tests (deterministic counts/cancel/set_limit + parallel/serial
timing) + 2 admission unit tests + a TaskGroup BAML fixture; snapshots regenerated.
…tive (BEP-034)

Completes the spawn-options core.

- TIR now requires each `spawn ... with` expression to produce a
  `baml.spawn.SpawnConfig` — i.e. a `baml.spawn.options(...)` call, or a
  variable bound to one (type-based, so it's forward-compatible) — and rejects
  more than one config. New `TirTypeError::SpawnWithMustBeSpawnConfig` /
  `SpawnWithTooManyConfigs`, mapped through the LSP diagnostic id table.
- Fix `baml_tests/tests/spawn_parallel.rs`: it used `sys_ops` whose `sleep` is
  `Unsupported`, so its "parallel sleeps" were no-ops caught by a `catch` — a
  false positive. It now uses `sys_native` (real `tokio::time::sleep`) and
  genuinely verifies three concurrent 200ms sleeps finish in ~200ms.

Tests: a negative `spawn with <non-options>` compile-rejection test; the fixed
spawn_parallel now exercises real concurrency.
…a-param inference fixes (BEP-034)

- all_complete<T,E>(Future<T,E>[]) -> Future<T[],E>: await every future in
  input order; inputs are already running so this is concurrent. Losers keep
  running on failure (throws the first error in input order).
- generics: infer <T,E> through Ty::Future (substitute/contains/infer/erase),
  and match a Future<T,E> formal against a union-of-futures array literal.
- fix: copy propagation now substitutes the operand of Terminator::Await.
  Previously `await <param>` (e.g. a lambda param awaited inside a closure
  passed to .map) read an uninitialized slot because the propagated
  `tmp = copy param` was dead-eliminated but the await kept pointing at tmp.
- fix: an unannotated lambda parameter now infers from a generic array element
  whose type mentions only the enclosing function's own (rigid) generics
  (contains_non_rigid_typevar), instead of falling back to synthesis + erroring.
- tests: bex_engine/tests/combinators.rs
…e-Combinators

# Conflicts:
#	baml_language/crates/baml_tests/snapshots/baml_src/arrays.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/assignments.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/bigints.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/builtins.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/byte_strings.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/cancel_cascade.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/class_type_args_at_runtime.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/classes.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/closures.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/deep_copy.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/enums.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/env.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/exceptions.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/floats.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/for_loops.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/fs.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/functions.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/future_methods.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/glob.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/if_else.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/ints.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/is_operator.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/json_alias.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/json_auto_derive.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/json_parse_stringify.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/json_to_from_string.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/lambdas.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/lexical_scoping.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/maps.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/match_basics.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/match_optimization.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/match_types.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/null_handling.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/operators.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/optional_function_parameters.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/patterns_new_runtime.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/reflect_type_of.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/reflect_type_of_generic.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/shell.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/soundness.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/spawn_basic.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/spawn_name_object.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/spawn_semantics.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/spawn_throws.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/strings.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/type_error_repro.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/type_reflection.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/typed_inputs.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/typed_outputs.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/watch.snap
#	baml_language/crates/baml_tests/snapshots/baml_src/while_loops.snap
#	baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_5_mir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_expr_basic/baml_tests__compiles__test_expr_basic__03_hir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_expr_basic/baml_tests__compiles__test_expr_basic__04_5_mir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_expr_basic/baml_tests__compiles__test_expr_basic__04_tir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_expr_basic/baml_tests__compiles__test_expr_basic__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_expr_name_concat/baml_tests__compiles__test_expr_name_concat__03_hir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_expr_name_concat/baml_tests__compiles__test_expr_name_concat__04_5_mir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_expr_name_concat/baml_tests__compiles__test_expr_name_concat__04_tir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_expr_name_concat/baml_tests__compiles__test_expr_name_concat__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_expr_throwing_body/baml_tests__compiles__test_expr_throwing_body__03_hir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_expr_throwing_body/baml_tests__compiles__test_expr_throwing_body__04_5_mir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_expr_throwing_body/baml_tests__compiles__test_expr_throwing_body__04_tir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_expr_throwing_body/baml_tests__compiles__test_expr_throwing_body__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_old_and_new/baml_tests__compiles__test_old_and_new__03_hir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_old_and_new/baml_tests__compiles__test_old_and_new__04_5_mir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_old_and_new/baml_tests__compiles__test_old_and_new__04_tir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_old_and_new/baml_tests__compiles__test_old_and_new__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_raw_string_name/baml_tests__compiles__test_raw_string_name__03_hir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_raw_string_name/baml_tests__compiles__test_raw_string_name__04_5_mir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_raw_string_name/baml_tests__compiles__test_raw_string_name__04_tir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_raw_string_name/baml_tests__compiles__test_raw_string_name__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_with_not_keyword/baml_tests__compiles__test_with_not_keyword__03_hir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_with_not_keyword/baml_tests__compiles__test_with_not_keyword__04_5_mir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_with_not_keyword/baml_tests__compiles__test_with_not_keyword__04_tir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/test_with_not_keyword/baml_tests__compiles__test_with_not_keyword__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/compiles/testset_basic/baml_tests__compiles__testset_basic__03_hir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/testset_basic/baml_tests__compiles__testset_basic__04_5_mir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/testset_basic/baml_tests__compiles__testset_basic__04_tir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/testset_basic/baml_tests__compiles__testset_basic__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/compiles/testset_dynamic/baml_tests__compiles__testset_dynamic__03_hir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/testset_dynamic/baml_tests__compiles__testset_dynamic__04_5_mir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/testset_dynamic/baml_tests__compiles__testset_dynamic__04_tir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/testset_dynamic/baml_tests__compiles__testset_dynamic__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/compiles/testset_nested/baml_tests__compiles__testset_nested__03_hir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/testset_nested/baml_tests__compiles__testset_nested__04_5_mir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/testset_nested/baml_tests__compiles__testset_nested__04_tir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/testset_nested/baml_tests__compiles__testset_nested__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/compiles/testset_vibes_nested/baml_tests__compiles__testset_vibes_nested__03_hir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/testset_vibes_nested/baml_tests__compiles__testset_vibes_nested__04_5_mir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/testset_vibes_nested/baml_tests__compiles__testset_vibes_nested__04_tir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/testset_vibes_nested/baml_tests__compiles__testset_vibes_nested__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/compiles/testset_with_setup/baml_tests__compiles__testset_with_setup__03_hir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/testset_with_setup/baml_tests__compiles__testset_with_setup__04_5_mir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/testset_with_setup/baml_tests__compiles__testset_with_setup__04_tir.snap
#	baml_language/crates/baml_tests/snapshots/compiles/testset_with_setup/baml_tests__compiles__testset_with_setup__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/diagnostic_errors/test_expr_name_type_error/baml_tests__diagnostic_errors__test_expr_name_type_error__03_hir.snap
#	baml_language/crates/baml_tests/snapshots/diagnostic_errors/test_expr_name_type_error/baml_tests__diagnostic_errors__test_expr_name_type_error__04_tir.snap
#	baml_language/crates/baml_tests/snapshots/diagnostic_errors/test_expr_with_runner/baml_tests__diagnostic_errors__test_expr_with_runner__03_hir.snap
#	baml_language/crates/baml_tests/snapshots/diagnostic_errors/test_expr_with_runner/baml_tests__diagnostic_errors__test_expr_with_runner__04_tir.snap
#	baml_language/crates/baml_tests/snapshots/diagnostic_errors/test_expr_wrong_runner/baml_tests__diagnostic_errors__test_expr_wrong_runner__03_hir.snap
#	baml_language/crates/baml_tests/snapshots/diagnostic_errors/test_expr_wrong_runner/baml_tests__diagnostic_errors__test_expr_wrong_runner__04_tir.snap
#	baml_language/crates/baml_tests/snapshots/diagnostic_errors/test_with_runner_ambiguity/baml_tests__diagnostic_errors__test_with_runner_ambiguity__03_hir.snap
#	baml_language/crates/baml_tests/snapshots/diagnostic_errors/test_with_runner_ambiguity/baml_tests__diagnostic_errors__test_with_runner_ambiguity__04_tir.snap
#	baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded.snap
#	baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded_unoptimized.snap
#	baml_language/crates/bex_vm/Cargo.toml
Reflects (a) the new all_complete stdlib function (HIR/TIR/MIR/codegen +
package_items + bytecode dumps — additive), (b) the spawn config operand
(extra load_const null before SPAWN in canary's spawn_specialization inline
snapshots), and (c) canary's deterministic test-init function naming
($init_test_<n> -> $init_test_<descriptive>). No behavioral changes; full
baml_tests suite green.
@coderabbitai

coderabbitai Bot commented Jun 1, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements BEP-034 concurrency model across the compiler and runtime. It adds spawn middleware configuration (SpawnParams, TaskGroup, CancelToken), future combinators (all_complete, all, race, any), an AwaitAny suspend primitive, deferred child error routing, FIFO-based rate limiting, and comprehensive engine/VM support for concurrent task execution and cancellation.

Changes

BEP-034 async concurrency model

Layer / File(s) Summary
Future combinators and spawn middleware contracts
baml_std/baml/ns_future/future.baml, baml_std/baml/ns_spawn/spawn.baml, baml_builtins2/src/lib.rs
Introduces all_complete, all, race, any, and __await_any helpers; defines SpawnParams<T, E>, TaskGroup, CancelToken, and options middleware; registers spawn builtin.
Parser and syntax support
baml_compiler_parser/src/parser.rs, baml_compiler_syntax/src/ast.rs, baml_compiler2_ast/src/lower_expr_body.rs
Extends parser to accept spawn name? with expr (, expr)* { body } syntax; allows spawn/await as path segments; updates lower_expr_body to parse and lower with_exprs list.
AST representation and lowering
baml_compiler2_ast/src/ast.rs, baml_compiler2_ast/src/lower_cst.rs, baml_compiler2_hir/src/builder.rs
Adds Expr::Spawn { with_exprs } field and BuiltinKind::AwaitAny variant; lowers $await_any token; updates HIR traversal to walk with_exprs.
TIR type inference and generics
baml_compiler2_tir/src/builder.rs, baml_compiler2_tir/src/inference.rs, baml_compiler2_tir/src/generics.rs, baml_compiler2_tir/src/infer_context.rs
Implements spawn middleware folding by type-checking transformers; tracks per-call generic instantiations for MIR; adds contains_non_rigid_typevar and erase_typevars_where utilities; infers concrete lambda throws from body; adds SpawnWithNotATransformer error.
MIR IR, builder, and lowering
baml_compiler2_mir/src/ir.rs, baml_compiler2_mir/src/builder.rs, baml_compiler2_mir/src/lower.rs
Introduces Terminator::AwaitAny { futures, destination, target, unwind }; extends Terminator::Spawn with optional config; implements middleware application and __await_any builtin dispatch; fixes short-circuit destination handling.
MIR optimization and verification
baml_compiler2_mir/src/optimize.rs, baml_compiler2_emit/src/analysis.rs
Extends optimizer to rewrite and substitute through AwaitAny; updates def-use analysis, dead-block elimination, copy-propagation; extends verification for config/futures operands.
Bytecode emission and VM interpreter
baml_compiler2_emit/src/emit.rs, baml_compiler2_emit/src/stack_carry.rs, baml_compiler2_emit/src/lib.rs, bex_vm_types/src/bytecode.rs, bex_vm/src/vm.rs
Extends Spawn emission to pop/push config operand; emits AwaitAny instruction; implements AwaitAny opcode to scan futures array, yield on pending, resume with winning index; updates stack carry simulation.
Engine spawn dispatch and AwaitAny execution
bex_engine/src/lib.rs
Implements read_spawn_params to decode middleware options by field index; applies cancel/group/detach overrides; links user tokens and registers task-group membership; implements VmExecState::AwaitAny to race multiple futures and resume with index.
Fire-and-forget deferred errors and thread routing
bex_engine/src/future.rs, bex_engine/src/thread.rs
Adds defer_error and take_deferred_error for child error deferral; makes future_ready the unified settlement point; extends BexThread with root error queue arc for detached spawn error propagation.
TaskGroup rate-limiting and CancelToken primitives
bex_vm_types/src/task_group.rs, bex_vm/src/package_baml/spawn.rs
Implements FIFO-queued admission with concurrency cap, dynamic limit updates, and cancellation; implements CancelToken with composition; provides native trait implementations for both primitives.
GC rooting and value representation
bex_heap/src/gc.rs, bex_vm_types/src/types.rs, bex_vm/src/package_baml/root.rs
Extends UnscheduledFuture with config field; updates GC tracing and fixup for config references; adds SpawnConfigData struct; introduces Value::is_omitted predicate.
Tests: combinators, options, and fire-and-forget
bex_engine/tests/combinators.rs, bex_engine/tests/cancellation.rs, bex_engine/tests/middleware.rs, bex_engine/tests/fire_and_forget.rs, baml_src/ns_cancel_token/cancel_token.baml, baml_src/ns_task_group/task_group.baml
Tests all four combinators with concurrency and error semantics; tests spawn options middleware (identity, wrapping, chained, retry); tests CancelToken composition and cancellation-on-option; tests fire-and-forget error propagation with multiple scenarios.
Tests: task group, generics, and regression
bex_engine/tests/task_group.rs, bex_engine/tests/generics_runtime.rs, bex_engine/tests/fn_typed_slots.rs, bex_engine/tests/tail_lambda.rs
Tests TaskGroup admission limits, FIFO queueing, limit updates, and wall-clock concurrency timing; tests runtime reified-generics threading in catch arms; tests lambda typing and catch arm enforcement; regression tests for tail lambdas in block position.
TIR test and debug updates
baml_tests/src/compiler2_tir/phase6.rs, baml_tests/src/compiler2_tir/phase8_exceptions.rs, bex_vm/src/debug.rs, bex_vm/src/package_baml/mod.rs, tools_onionskin/src/compiler.rs
Updates phase-6 tests to expect stored lambdas infer concrete throws from body; updates phase-8 tests to reflect inferred-throws propagation with concrete type names; extends debug styling for AwaitAny; updates onionskin to handle AwaitAny yield state.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • BoundaryML/baml#3520: Main PR is directly related to #3520 because it extends the same baml_std/baml/ns_future/future.baml module introduced there (adds combinators like all_complete/all/race/any, AllFailed, and __await_any on top of the earlier FutureState + Future<T,E> foundation).

"A rabbit spawns a thousand tiny threads,
racing and waiting with careful heads,
it cancels, groups, and collects each fall—
then wakes the winner to answer the call. 🐰"

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch antoniosarosi/Future-Combinators

@vercel

vercel Bot commented Jun 1, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Jun 8, 2026 5:05pm
new-boundaryml-website Error Error Jun 8, 2026 5:05pm
promptfiddle Ready Ready Preview, Comment Jun 8, 2026 5:05pm
promptfiddle2 Ready Ready Preview, Comment Jun 8, 2026 5:05pm

Request Review

@antoniosarosi antoniosarosi changed the title feat(future): combinators (all_complete) + await/closure & lambda-inference fixes (BEP-034) feat(spawn/future): BEP-034 spawn options + future combinators Jun 1, 2026
… fix

- AwaitAny engine primitive (BuiltinKind/Terminator/OpCode/VmExecState) for
  first-of-N settle; full parser/AST/HIR/MIR/optimizer/emit/VM/engine plumbing
- race, all, all_complete combinators + AllFailed<E> + __await_any (stdlib)
- fix: 'await f catch' now parses as '(await f) catch' (mirrors throw); was
  'await (f catch)', leaving the handler dead
- fix stale spawn.baml 'group wired in follow-up' comment

Known issues (ignored tests in tests/combinators.rs):
- any: AwaitAny suspend/resume fault (spurious JsonDecodeError)
- bug2: fire-and-forget child error bypasses the future's consumer
Pass-1 global-slot assignment (emit/lib.rs) skipped Builtin(Intrinsic) but
not the new Builtin(AwaitAny), while Pass-4 skips both. That left every
function after `__await_any` with an off-by-one global index, so a call to
`baml.future.any` resolved to `baml.spawn.SpawnConfig.from_json` (spurious
JsonDecodeError). Skip AwaitAny in Pass-1 too.

any now runs correctly (any_all_success_returns_a_value passes). The remaining
any test failures are bug 2 (fire-and-forget), tracked separately and ignored.
…atch arms (bugs 2 & 4)

Bug 2 — a spawned child's unhandled throw settled its Future to Error
eagerly and queued on the spawner; a SIBLING awaiting the settled future
consumed the error entirely inside the VM (invisible to the engine), so
the spawner's drain re-surfaced an already-handled error as an
UnhandledThrow, bypassing catch. Fix: defer the settle — park the error
in a GC-rooted stash on FutureManagerInner, keep the future Pending with
its wake signal fired so EVERY await routes through future_ready (now
&mut), the single observation point that settles+consumes the entry. The
spawner's drain (post-await only; the pre-drain raced legitimate
consumers like combinators) surfaces only never-observed errors.
Genuinely dropped errors still surface (tests/fire_and_forget.rs guard).

Bug 4 — a bare 'let e =>' catch/match arm emitted an IsType test from
its inferred pattern type; inside a generic function that type is a
rigid TypeVar which convert_tir2_ty erases to Ty::Void, making the test
constant-false: the arm silently RETHREW instead of handling. Invisible
in 'all' (its arm rethrows anyway); 'any' was the first non-rethrowing
generic catch. Fix: a bare bind is irrefutable — no runtime test
(panic fall-through stays on ThrowIfPanic).

Tests: tests/fire_and_forget.rs (4) + combinators all un-ignored and
green (any with failing inputs now passes); bex_engine suite 197/0.
Known follow-up: typed arms (let e: AllFailed<string>) still fail
IsType on instances built inside generic fns (unsubstituted
class_type_args) — pre-existing, documented in any_all_fail test.
- __baml_std__ HIR/TIR/MIR/codegen + package items: race/any/all/
  all_complete/AllFailed stdlib additions and the AwaitAny terminator.
- bytecode_format expanded dumps: AwaitAny opcode.
- patterns_new / match_or_mixed / is_operator / control_flow: bare-bind
  arms (including destructure field binds like 'User { name, age }') no
  longer emit tautological IsType tests — class/shape tests alone guard
  the arm; explicitly annotated subpatterns still test.
- baml_cli describe listing: new stdlib functions.

baml_tests: 3255 passed / 0 failed across 29 binaries.
TIR's check_call_inner ran two-pass generic inference, substituted the
return type, and DISCARDED the bindings; MIR emitted frame type args only
for explicit <...> call sites. Any inferred-generic call therefore ran
with empty frame.type_args, so every downstream TypeArgRef substituted to
unknown: instances built inside generic functions carried wrong/empty
class_type_args and typed arms like 'let e: AllFailed<string>' never
matched (vm IsType compares substituted templates against the instance's
class_type_args).

- TIR: record call_type_instantiations (declared De Bruijn order, values
  pre-erasure so caller-rigid TypeVars survive) at check_call_inner exit;
  declared params resolved via new callee_declared_generic_params helper
  (factored out of resolve_explicit_type_args). Plumbed through
  ScopeInference, DefaultParameterInference, salsa cycle-initial, and the
  defaults/lambda save-restore sites, mirroring call_plans.
- MIR: aggregate the map and fall back to it in call lowering when the
  site has no explicit type args, emitting the same LoadType templates as
  the explicit path. TypeVar values lower to TypeArgRef into the CALLER's
  frame, so generic→generic chains (main → any<T,E> → __await_any<T,E>)
  resolve recursively. Compiler-only: the VM's existing ntypeargs pop
  path is unchanged.
- TIR object literals: qualified-path branch (pkg.Foo<int> { ... })
  dropped explicit literal type args on the floor; apply them now.
- std future.baml: construct AllFailed via a typed let so bidirectional
  checking records AllFailed<E> (bare generic literals record no class
  type args — literal-arg inference from fields is a known follow-up;
  NB: 'pkg.Foo<E> { ... }' literal syntax silently lowers to Null, also
  a follow-up).
- combinators.rs: any_all_fail_throws_allfailed now binds the TYPED arm
  'let e: baml.future.AllFailed<string>' — the original BEP-034 shape.
- drive-by: workspace-wide clippy --fix for lints newly surfaced by the
  cold cache at the moved worktree (collapsible_match, trailing commas,
  redundant format refs, etc.).

bex_engine: 23/23 binaries green.
Three follow-ups to the inferred type-arg threading commit, found by the
new generics_runtime tests:

- MIR: a typed arm naming the enclosing function's TypeVars (e.g.
  'let e: AllFailed<E>' inside 'helper<E>') erased E → Void via
  convert_tir2_ty, compiling to a constant-false IsType. Class patterns
  containing TypeVars now lower via ty_to_template → TypeArgRef templates
  (new emit_is_type_template_branch), resolved against frame.type_args.
  The annotated-bind recursion has no pat_types entry for its subpattern,
  so a new lower_type_annotation_tir fallback lowers the annotation with
  the enclosing generic params in scope. Bare 'let e: E' TypeVar arms
  remain out of scope (need VM-side arbitrary-type matching).
- TIR: recorded instantiation values now widen fresh literal types —
  mk("hi") infers T = Literal("hi"), but the runtime tag must be
  'string' for 'is Box<string>' to compare equal.
- TIR: the type-checking infer_bindings pass refuses TypeVar actuals, so
  a generic→generic call ('any(fs)' inside 'helper<E>' with
  fs: Future<int, E>[]) recorded E2 = Unknown → Void at runtime. The
  recording now fills unbound params with an infer_bindings_allow_typevars
  pass — rigid caller TypeVars are exactly what MIR lowers to TypeArgRef.

New tests (bex_engine/tests/generics_runtime.rs): escaped instance
carries inferred args (+ negative control), typed arm on enclosing
TypeVar through a main → helper<E> → any<T,E> inferred chain, explicit
args regression guard. bex_engine suite: green.
All churn is the designed shape: inferred-generic call sites now emit
load_type ops and pass <...> args (e.g. Array.map -> Array.map<string,
void>) across __baml_std__/__testing_std__ HIR/MIR/codegen and every
fixture with generic calls; plus the std future.baml typed-let change.

Verification note: the full baml_tests run cannot complete on this
branch — it deadlocks ~240 tests into the baml_src runtime binary
(after ns_cancel_cascade, on accumulated spawn state). Bisect shows the
hang at fffaf1a and HEAD alike, isolated and paired repros all PASS,
and the branch predates canary's 5e03373 'Fix baml_tests hang:
spawn/GC permit deadlock'. Merge canary to pick that up before trusting
full-suite runs. Verified instead: all 30 previously-failing snapshot
tests pass post-accept; bex_engine 23/23 binaries green; affected
runtime fixtures pass isolated.
@vercel
vercel Bot temporarily deployed to Preview – new-boundaryml-website June 4, 2026 16:57 Inactive
@vercel
vercel Bot temporarily deployed to Preview – beps June 4, 2026 16:58 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 4, 2026 17:17 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 4, 2026 17:17 Inactive
…iation, short-circuit emit fix

Picks up canary's spawn/GC permit deadlock fix (5e03373) — full
baml_tests now runs to completion on this branch — plus BEP-044
interface destructure, BEP-57 associated types, generic instantiation
expressions, class generic bounds, interface-dispatch type-arg
threading, and the movement superinstructions.

── Conflict resolutions ──────────────────────────────────────────────
- generics.rs: both sides' machinery kept; contains_typevar_where
  (canary) gains a Future arm; contains_non_rigid_typevar (ours) stays
  standalone; infer_bindings' Future arms thread canary's rigid param.
- lower.rs typed-arm dispatch: three-way — canary's generic-interface
  shape test, then our TypeVar-class template test, then the erased
  fast path.
- builder.rs: canary's extracted helpers win; our callee_expr recording
  threaded into them; canary's interface cases ported into
  callee_declared_generic_params; spawn-'with' validation re-threaded
  into infer_spawn_expr (canary's refactor predates the feature);
  canary's infer_object_expr supersedes our inline qualified-literal
  fix (it also implements field-driven literal type-arg inference).
- bex_engine: canary's single-permit spawn discipline (the deadlock
  fix; our spawn_thread_setup's one-shot permit was exactly the
  fairness cycle) + our spawn options — detach token + error-queue
  routing at the dispatch site, user-cancel watcher + TaskGroup
  registration in spawn_thread_inner. spawn_thread_setup deleted.
- bytecode.rs: canary's MakeGenericFunction/LoadVar2/StoreVar2 first,
  AwaitAny appended after; vm alloc helpers resolved to canary's
  TlabHolder trait (spawn.rs adopts it).

── Reconciliation fixes (canary's strict rigid-var checking exposed
   pre-existing inference gaps that surfaced as std diagnostics) ─────
- throws_analysis: 'await f' now contributes the future's E to the
  body's escaping throws (was: nothing).
- check_call_inner: callee generics matched only against the caller's
  rigid TypeVars now bind via an allow-typevars backfill (map's U:=T
  in 'futures.map((f) -> { await f })'); previously skipped, leaving
  U[] to fail strict checking at the use site.
- Lambda effective-throws: rigid enclosing-fn TypeVar facts no longer
  collapse the surface to Never; and instantiated_callee_throws feeds
  callee throws-generics from a lambda arg's EFFECTIVE throws (side
  table), so a non-throwing callback binds E:=Never and a throwing one
  binds its concrete type. Lifts the documented 'current limitation'
  (covered callback throws no longer report) and makes violations name
  concrete types — phase8 expectations updated accordingly.
- std any<T, E> renamed to any<T, Err>: works around generic-param
  name capture against map<U, E>'s receiver-substituted signature
  (filed as a follow-up; the capture bug is pre-existing).

── Miscompile found by the newly-unblocked suite ─────────────────────
ShortCircuit emit assumed its destination is always stack-carried
(PhiLike: result on TOS at the join). Our inferred-type-arg threading
emits LoadType before call args, which validly disqualifies the carry —
the rhs edge then stored to the slot while the short-circuit edge left
the value on the stack, so 'a || b' as an inferred-generic call
argument read an uninitialized local (logical_or fixtures; latent at
e69b136, unreachable pre-merge because the suite deadlocked earlier).
Emit now materializes the destination via emit_store_place on the
short-circuit edge (no-op for PhiLike — codegen unchanged there), and
MIR lowering normalizes projection destinations through a temp.

Verification: bex_engine 227/0; baml_tests 3247/1 (the 1: baml_src
fixture flake under full-suite load only — passes isolated 1650/1650;
under investigation); diagnostics 418/418; snapshots regenerated.
@github-actions

github-actions Bot commented Jun 4, 2026 •

Copy link
Copy Markdown

⏭️ Performance benchmarks were skipped

Perf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to canary/main.

To run them on this PR, do any of the following, then push a commit (or re-run CI):

  • Add RUN_CODSPEED=1 to the PR description, or
  • Include run-perf or /perf in the PR title or any commit message.

@vercel
vercel Bot temporarily deployed to Preview – beps June 4, 2026 19:23 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 4, 2026 19:29 Inactive
@github-actions

github-actions Bot commented Jun 4, 2026 •

Copy link
Copy Markdown

No description provided.

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 4, 2026 19:41 Inactive
@antoniosarosi antoniosarosi changed the title feat(spawn/future): BEP-034 spawn options + future combinators Spawn options + future combinators Jun 5, 2026
…lines

'spawn ... with a, b { body }' now packages the body closure + name into
a 'baml.spawn.SpawnParams<T, E>' instance and applies each 'with'
expression to it left-to-right — every transformer is an ordinary BAML
function '(SpawnParams<T, E>) -> SpawnParams<U, F>' (no registration, no
special syntax) that may set config fields, wrap the body (withRetry,
timing), or replace it entirely (type-changing, e.g. a fallback erasing
the error type). The engine reads body/name/group/cancel/detach from the
FINAL params' fields at the spawn dispatch site, so a replaced body wins
over the spawn's original closure operand.

- std: 'class SpawnParams<T, E>' (fn-typed 'body' field) added to
  ns_spawn; 'baml.spawn.options(...)' rewritten as a pure-BAML generic
  transformer (copies params, overrides provided fields); the native
  SpawnConfig class + options impl deleted (the namespace trait is now
  dispatch-only).
- TIR: infer_spawn_expr folds the with-chain — each expr is CHECKED
  against '(SpawnParams<cur>) -> SpawnParams<?, ?>' so phase-0 reverse
  inference binds a generic transformer's own params from the parameter
  position ('withRetry(3)'); the output type args feed the next link and
  the final pair types the Future. SpawnWithMustBeSpawnConfig /
  TooManyConfigs replaced by SpawnWithNotATransformer.
- MIR: lower_spawn builds the SpawnParams aggregate (fields in
  declaration order; engine reads BY INDEX) and emits one indirect call
  per transformer; plain spawns keep the existing fast path untouched.
- engine: read_spawn_params replaces read_spawn_config (SpawnConfigData
  plumbing removed); params override closure/name and carry the options.

Three pre-existing language gaps the feature flushed out, fixed at the
root:
1. 'spawn'/'await' keywords as TYPE-path segments
   ('baml.spawn.SpawnParams' in annotations) — parser type-path loop +
   the CST dotted-name extractor.
2. A LAMBDA as a block's TAIL expression was silently dropped from
   block elements, typing the block as void ('missing return value') —
   any function returning a transformer hit it; the std's json code had
   been dodging it with explicit 'return'. (tests/tail_lambda.rs)
3. An UNANNOTATED lambda's throws surface defaulted to 'never' and
   reported a local violation when the body throws. It now INFERS the
   surface from the body — a lambda throws what it throws; the (now
   honest) fn type is checked wherever the lambda is used. Required by
   body wraps like '() -> { original() * 2 }' whose throws is the
   enclosing transformer's generic E. Two tests asserting the old rule
   updated (phase6/phase8).

Tests: bex_engine/tests/middleware.rs — identity, options-as-transformer,
body wrap, left-to-right chain ordering ((10*2)+1, not (10+1)*2), and
the BEP's withRetry (flaky body succeeds on 3rd attempt). bex_engine
234/0; baml_tests lib 1588/0, fixtures 1650/1650; snapshots regenerated
(spawn-with fixtures now show the init_instance SpawnParams +
call_indirect pipeline).
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 5, 2026 12:38 Inactive
…me family

Single canary commit (c130c98). Conflicts were unions: bex_vm deps
(our tokio/tokio-util + canary's toml), package_baml lint allows, a
jsonish parser refactor (same logic, canary's shape), bytecode-format
snapshots regenerated.

bex_engine 259/0; baml_tests 3936/0 (toml suite absorbed).
@vercel
vercel Bot temporarily deployed to Preview – beps June 5, 2026 14:45 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 5, 2026 14:50 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 5, 2026 15:02 Inactive
…; CodeRabbit review batch

── await over unions (BEP-034 §await-distributes) ────────────────────
Awaiting 'Future<A, E1> | Future<B, E2>' (if/else or array-element
unions — Future is invariant) now types as value 'A | B', and the
throws analysis contributes EVERY member's error type. Engine untouched
(a union-typed future is still one future object at runtime).
Tests: bex_engine/tests/await_union.rs (value + error sides).

── CI fix: Object enum size assert (the all-legs failure) ────────────
Under --all-features (heap_debug) the instrumented HeapPtr makes
UnscheduledFuture's three pointers push Object past its 64-byte
interpreter budget — invisible locally because the default toolchain
was a nightly with tighter niche layout AND default features. Variant
boxed; local verification now mirrors CI (rustup run 1.93.0 +
--all-features, clean target). Also: broken intra-doc link
(TaskGroupTicket::acquire) failing the cargo-doc leg.

── CodeRabbit review batch (9 fixed, 1 false-positive documented) ────
- std any: AllFailed.errors now in INPUT order — the main loop drains
  settled failures (consuming deferred fire-and-forget errors) and the
  aggregate re-reads them by walking the ORIGINAL futures; settled
  Error futures rethrow on every await. Regression test with
  slow-then-fast failures.
- callee_throws_arg_ty: only an OPEN lambda surface (omitted/effect
  TypeVar) is replaced by the effective set — explicit 'throws A | B'
  contracts are preserved.
- spawn 'with' transformers contribute their callable throws at the
  spawn site (the pipeline invokes them eagerly).
- default-arg forward-reference check no longer descends into the
  DEFERRED spawn body (only name/with are eager).
- contains_non_rigid_typevar re-unified over contains_typevar_where
  (Interface/AssociatedTypeProjection coverage + binder shadowing);
  erase_typevars_where completed (Interface, projections, Evolving
  containers, Function with binder shadowing).
- deferred fire-and-forget errors are cleared on competing terminal
  transitions (cancel/fulfill) — no stale spawner-drain reports, no
  over-long GC roots.
- VM Spawn validates the config operand is an Instance before storing
  it (VM→engine contract).
- copy propagation protects 'await xs[_i]' index locals
  (collect_place_index_locals scans Await::future).
- TaskGroup::promote admits up to the new cap (set_limit(+N) admitted
  only one queued waiter). Regression test.
- AwaitAny empty-input park documented as BEP-specified
  (Promise.race([]) never settles); waiters cover settled inputs via
  ready-waiters, so empty waiters ⟺ empty input (false positive).
- LSP check test updated to the lifted covered-callback limitation
  (concrete-type violations, no provenance related-info).

Deferred (tracked): interface-dispatch inferred type args; test
panic-hook hygiene.

Verification: clean target, pinned 1.93.0, --all-features workspace
sweep — 146 binaries green (incl. baml_tests 1588 lib + 1687 fixtures);
only sdk_test_typescript harness tests fail locally (vitest/pnpm not
installed — CI runs those in dedicated jobs and excludes them from the
cargo-test leg).
@antoniosarosi

Copy link
Copy Markdown
Contributor Author

@coderabbitai Review disposition for the latest push (94d2b46):

Fixed (9): any input-order errors (+ regression test) · explicit lambda throws surfaces preserved in callee_throws_arg_ty · spawn with transformer throws contributed at the spawn site · forward-ref check no longer descends into the deferred spawn body · spawn/await segments in generic-arg lookahead · contains_non_rigid_typevar unified over contains_typevar_where · erase_typevars_where completed (Interface/projection/Evolving/Function-shadowing) · deferred fire-and-forget errors cleared on cancel/fulfill transitions · VM validates the Spawn config operand is an Instance · copy-prop protects await xs[_i] index locals · TaskGroup::promote admits up to the new cap (+ regression test).

False positive (1): AwaitAny empty-waiters park — future_ready returns immediately-ready waiters for settled inputs, so empty waiters ⟺ empty input array, and BEP-034 specifies race([]) never settles (matching JS Promise.race([])). Comment expanded to document this.

Deferred (2, tracked): interface-dispatch inferred type args (emit_method_candidate_switch) and the cancellation-test panic-hook hygiene.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@antoniosarosi Thanks for the detailed disposition — this is very helpful for tracking review status.

Acknowledged:

  • All 9 fixes noted and recorded.
  • False positive clarified — the AwaitAny empty-waiters park is intentional: future_ready returns immediately-ready waiters for already-settled futures, so an empty-waiters set implies an empty input array, and BEP-034 specifies race([]) / any([]) never settles (matching Promise.race([])). Recording this to avoid re-flagging.
  • Deferred (tracked): emit_method_candidate_switch interface-dispatch inferred type args and cancellation-test panic-hook hygiene — noted for follow-up.

✏️ Learnings added
Learnt from: antoniosarosi
URL: https://github.com/BoundaryML/baml/pull/3625

Timestamp: 2026-06-05T16:29:24.459Z
Learning: In `baml_language/crates/bex_engine/src/lib.rs` (PR `#3625`, BEP-034), the `AwaitAny` opcode parking with zero waiters when the input array is empty is intentional and correct. `future_ready` returns immediately-ready waiters for any already-settled futures, so an empty-waiters set is equivalent to an empty input array. BEP-034 specifies that `race([])` and `any([])` never settle, matching the `Promise.race([])` / `Promise.any([])` semantics in JavaScript. Do not re-flag this as a hang or missing edge-case in reviews of PR `#3625`.

Learnt from: antoniosarosi
URL: https://github.com/BoundaryML/baml/pull/3625

Timestamp: 2026-06-05T16:29:24.459Z
Learning: In `baml_language/crates/baml_compiler2_emit/src/emit.rs` and related MIR lowering (PR `#3625`, BEP-034), the interface-dispatch inferred type arguments path (`emit_method_candidate_switch`) does not yet record per-call type instantiations for interface method dispatch. This is a known, tracked gap deferred to a follow-up PR. Do not re-flag this in reviews of PR `#3625`.

Learnt from: antoniosarosi
URL: https://github.com/BoundaryML/baml/pull/3625

Timestamp: 2026-06-05T16:29:24.459Z
Learning: In `baml_language/crates/bex_engine/tests/cancellation.rs` (PR `#3625`), the cancellation test suite uses `catch_unwind` without a custom panic hook, meaning test failure messages may include panic-hook output that pollutes test logs. Panic-hook hygiene (e.g., setting a no-op hook around `catch_unwind` calls) is tracked as a follow-up improvement. Do not re-flag this in reviews of PR `#3625`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: hellovai
Repo: BoundaryML/baml PR: 3369
File: baml_language/crates/baml_tests/src/compiler2_tir/phase6.rs:781-791
Timestamp: 2026-04-16T18:11:44.267Z
Learning: In `baml_language/crates/baml_tests/src/compiler2_tir/phase6.rs` (PR `#3369`), the snapshot `direct_optional_push_establishes_element_type` accepts `_[]`/`expected never` instead of `int[]` because the optional-inner-callee path (`xs?.push?.(1)`) bypasses `try_container_method_call` and does not feed evolving-list inference. This is a known gap caused by a `-X ours` merge of PR `#3374` (both sides had ~700-line changes to builder.rs), so the optional-call container-method integration is not fully wired. The fix is deferred to a follow-up PR. Do not re-flag this snapshot regression in reviews of PR `#3369`.

Learnt from: hellovai
Repo: BoundaryML/baml PR: 3369
File: baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/expr/watch_when.baml:48-74
Timestamp: 2026-04-16T16:05:24.083Z
Learning: In `baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/expr/watch_when.baml` (PR `#3369`), the diagnostic spans for "type `int` has no member `$watch`" errors intentionally cover a wider range than the exact `$watch` token — they extend to include the preceding `watch let x = 10;` statement. This is a known span regression introduced when multi-segment Path expressions replaced nested FieldAccess nodes. A comment in the file documents the issue and links to PR `#3369` discussion r3088162711. The upstream member/path span fix is deferred to a follow-up PR. Do not re-flag this span width issue in reviews of PR `#3369`.

Learnt from: hellovai
Repo: BoundaryML/baml PR: 3302
File: baml_language/crates/bex_engine/src/lib.rs:1113-1115
Timestamp: 2026-03-30T07:42:52.442Z
Learning: In `baml_language/crates/bex_engine/src/lib.rs`, the parked-VM GC loop in `collect_garbage()` is missing a call to `vm.apply_frame_forwarding(&forwarding)` after `vm.watch.apply_forwarding(&forwarding)`. The active-VM path in `maybe_run_gc()` (around line 1143) already has this call. This is a latent bug: it cannot trigger today because parked VMs only exist during concurrent async calls, and lambda bodies do not contain `await` yet, so no closure frame can be live across a parking point. The fix is tracked for a followup PR. Do not flag this again in reviews of PR `#3302`.

Learnt from: hellovai
Repo: BoundaryML/baml PR: 0
File: :0-0
Timestamp: 2026-06-03T21:21:45.704Z
Learning: In `baml_language/crates/baml_compiler2_mir/src/lower.rs` (PR `#3667`), two associated-types findings are pre-existing from BEP-57 (`#3652`, commit c14f14d9) and are intentionally out of scope for this PR and any PR scoped only to generic type-arg dispatch threading:
1. The `assoc.default` lowering around lines 959–967 uses the caller's `pkg_items` — this is a pre-existing limitation in associated-type resolution, not introduced by this PR.
2. `interface_tir_assoc_match` (~lines 7712–7729) is stricter than the type-arg matcher — also a pre-existing divergence in associated-type matching.
Do not re-flag either of these in reviews that do not explicitly touch associated-type resolution.

Learnt from: hellovai
Repo: BoundaryML/baml PR: 3302
File: baml_language/crates/baml_compiler2_hir/src/builder.rs:321-407
Timestamp: 2026-03-30T06:53:01.610Z
Learning: In `baml_language/crates/baml_compiler2_hir/src/builder.rs`, Pass 5 (lambda scopes) runs after Pass 3 (match arm scopes) and Pass 4 (catch arm/clause scopes) have already been popped. This means a lambda nested inside a match arm or catch arm gets its HIR scope parented to the enclosing function/let scope instead of the arm scope. As a result, the capture ancestor walk in the lambda scope never visits the arm scope, so pattern-bound variables (e.g., `Foo(v)` in a match arm) are invisible to lambdas defined in that arm. The fix is to process lambda scopes inline during the match/catch arm passes (while the arm scope is still on the stack). This is a known bug tracked for a followup PR; do not flag it again during reviews of PR `#3302`.

Learnt from: rossirpaulo
Repo: BoundaryML/baml PR: 3383
File: baml_language/crates/baml_compiler2_tir/src/builder.rs:1181-1206
Timestamp: 2026-04-20T23:19:07.163Z
Learning: BoundaryML/baml preference (PR `#3383` and forward): Do not infer lambda throws from the body. In checking mode, when a lambda is typed against an expected function type (e.g., callback position), use the contextual expected throws if the lambda omits a throws annotation. Validation of explicit lambda throws against the body can be added by calling `check_throws_contract` while the lambda scope is active (inside `infer_lambda_body`).

Learnt from: hellovai
Repo: BoundaryML/baml PR: 3302
File: baml_language/crates/baml_compiler2_tir/src/builder.rs:828-890
Timestamp: 2026-03-30T07:43:05.070Z
Learning: In `baml_language/crates/baml_compiler2_tir/src/builder.rs` (PR `#3302`), the `Expr::Lambda` check-mode branch (around line 890) uses `self.generic_params` directly when lowering parameter type annotations, without merging in `func_def.generic_params` (lambda-local generics). This means `<T>(x: T) -> T { x }` in check mode would lower `T` to `Unknown`. The fix (merging `func_def.generic_params` into a local `all_generic_params` as synthesis mode does at line 413) is intentionally deferred because generic lambdas in check mode are unusual — the expected function type cannot express a bound typevar like `T`, so no realistic call-site exercises this path. Fix if real usage emerges.

Learnt from: 2kai2kai2
Repo: BoundaryML/baml PR: 3324
File: baml_language/crates/baml_builtins2/baml_std/baml/uint8array.baml:38-60
Timestamp: 2026-04-03T21:52:16.230Z
Learning: In `baml_language/crates/baml_builtins2/baml_std/baml/uint8array.baml`, the `zeroes` function intentionally lacks a `throws root.errors.InvalidArgument` declaration even though the Rust implementation (`baml_language/crates/bex_vm/src/package_baml/uint8array.rs`) can return a `VmError` for negative sizes. This mismatch is a known, accepted gap deferred until a better error-handling system for builtin functions is implemented. Do not re-flag the missing `throws` on `zeroes` in future reviews.

@vercel
vercel Bot temporarily deployed to Preview – beps June 5, 2026 16:29 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 5, 2026 16:35 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 5, 2026 16:47 Inactive
… dead code, Terminator size

The pre-commit CI leg runs 'mise run clippy' (--all-targets
--all-features -D warnings) on the PINNED toolchain — earlier local
clippy ran neither strict nor pinned, so these accumulated:

- fmt drift in two test files (heredoc-written; hooks reformatted)
- doc-markdown backticks (TypeVar/SpawnParams references)
- as_task_group_arc dead after the native options deletion
- needless borrow (resolved_aliases is already a reference)
- nested helper fns hoisted above statements (items_after_statements)
- erase_unresolved_typevars: explicit allow(only_used_in_recursion) —
  the diagnostics sink is the fn's public contract
- Terminator::Spawn was clippy's large_enum_variant (416B vs 208B
  runner-up): the with-pipeline config operand is now boxed (Spawn is
  rare next to Call/Goto; emit reads through as_deref)

prek run --all-files: all hooks pass. Behavioral sanity: middleware,
combinators, task_group, await_union suites green.
@vercel
vercel Bot temporarily deployed to Preview – beps June 5, 2026 17:00 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 5, 2026 17:06 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 5, 2026 17:18 Inactive
…bservable; middleware diagnostics; review hardening

── ErrorPending (the deep one) ───────────────────────────────────────
The bug-2 deferral kept a failed-but-unobserved future reading PENDING:
'is_error()' lied, so a combinator draining settled losers skipped a
parked failure and the error bypassed every user catch, resurfacing at
the spawner as UnhandledThrow (found by the new 'all' input-order
test). New heap state 'ErrorPending':
- defer_error marks Pending → ErrorPending (wake fired); state queries
  ('is_error', 'state()', FutureType) read it as failed
- 'await' routes it through the engine like Pending; 'future_ready'
  consumes the parked error via settle_error (ErrorPending → Error
  transition) — the single observation point is unchanged
- it is SETTLED for everything else: fulfill/cancel refuse to claim it
  (take_pending parametrized) — cancelling an already-failed future is
  a no-op, so 'all' cancelling its losers no longer orphans a parked
  error mid-drain (and no awaiter livelock from dropped bookkeeping)
- std 'all': back to the clean map shape + a drain of already-failed
  inputs before rethrowing the first INPUT-ORDER failure

── inference: Unknown actuals carry no information ───────────────────
infer_bindings no longer binds (or unions) from a 'Ty::Unknown' actual —
an expected return of 'SpawnParams<unknown, unknown>' driving phase-0
was turning a param-bound 'T = int' into 'int | unknown' (broke
'await a + await b' in task-group fixtures with honest types).

── middleware diagnostics (review feedback: messages must be good) ───
- spawn lowering: bare TOKEN expressions in name/with position were
  silently DROPPED ('spawn with 42 { .. }' compiled clean) — now lowered
  and rejected with a concrete message
- the with-chain seeds widen fresh literal types (messages read
  'SpawnParams<int, null>', never 'SpawnParams<1, null>')
- expected transformer return is 'SpawnParams<unknown, unknown>' (not a
  bare open slot), so wrong shapes fail with readable mismatches
- SpawnWithNotATransformer carries the link's concrete input type:
  "this link receives 'baml.spawn.SpawnParams<int, null>' and must
  return a 'baml.spawn.SpawnParams', got ..."
- variable-bound transformers ('let t = withRetry(3); spawn with t')
  instantiate their generics from the link input; wrong-shaped variable
  refs validate input-side and report
- 6 TIR diagnostic tests pin the messages; 3 runtime tests cover
  type-changing fallback (error type erased to null), a THROWING
  transformer (throws at the spawn site, caught there, counted against
  the caller's surface), and variable-bound application

── tests ─────────────────────────────────────────────────────────────
combinators: any + all input-order error tests (the 'all' one found
ErrorPending); middleware 8 total; fn-typed-slot + tail-lambda
regressions from earlier rounds. bex_engine 267/0.
Semantic resolutions beyond mechanical conflicts:

- Inferred generic runtime type args: three parallel implementations
  converged. Adopted canary's CallPlan-based pipeline (c957417
  [codex]): MIR now materializes inferred type args exclusively via
  lower_call_type_args reading CallPlan.type_args; removed our
  call_type_instantiations TIR->MIR fallback apparatus (dead after the
  switch). Restored Ty::widen_fresh on inferred bindings in
  runtime_call_type_args — without it `mk("hi")` threads
  T = literal "hi" instead of T = string, and an escaped instance
  never matches `is Box<string>` (caught by
  generics_runtime::escaped_instance_carries_inferred_type_args).

- ShortCircuit emit: canary independently shipped the same
  both-paths-must-agree-on-where-the-result-lives fix
  (store_on_taken_path); took theirs, deleted our equivalent
  dest_on_stack variant. Validated by logical_or fixtures.

- is_subtype: same-class generic args now compare pairwise — holes
  (Ty::Unknown error-recovery, Ty::BuiltinUnknown from erased
  unresolved typevars) match anything, concrete args must be MUTUAL
  subtypes (invariance as equivalence, not structural equality).
  Fixes spawn-with false positives introduced by canary's stricter
  call-result recheck: `(SpawnParams<int,Io>) -> SpawnParams<int,Io>`
  vs expected `... -> SpawnParams<unknown,unknown>`, the value-bound
  transformer's erased `SpawnParams<int, unknown>`, and
  `T = int | 99` (== int) from fallback-arg joins. Concrete
  mismatches (iterator stdlib diagnostics) still report.

- AwaitAny lowering/terminators kept through canary's terminator-arm
  dedup (Call|SysOp|Await|AwaitAny unified arms in ir.rs/optimize.rs).

- heap_debugger (all-features): FutureRead::ErrorPending arm added to
  the validity walk (id-carrying like Pending; stash value lives in
  the engine).

- task_group_cancel_cancels_members: promptness bound 2s -> 5s; it
  times run_main INCLUDING compilation and the stdlib grew (ns_iter).
  Still far under the 10s sleep it guards against.

Verified: cargo test --workspace --all-features (185 suites, 0 fail;
sdk_test_* excluded as in CI — they require cargo-nextest setup
scripts) + prek run --all-files clean, on rustc 1.93.0.
@vercel
vercel Bot temporarily deployed to Preview – beps June 7, 2026 02:52 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 7, 2026 02:57 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 7, 2026 03:09 Inactive
Canary advanced 2 commits after round 4: 0a91ec3 [codex] iterable
for loops (#3705) and 4a866e7 (windows lld build chore). The
compiler source (builder.rs, lower.rs, emit.rs, analysis.rs) and
vm.rs auto-merged cleanly against #3705 — only 4 generated snapshots
conflicted; took canary's and regenerated to fold in our changes.

Also fixes a class of flaky timing assertions exposed by cold-build
runs. Three tests timed wall-clock around a call that COMPILES the
(growing) BAML stdlib before running — so a cold/large compile blew
past their promptness bounds, which actually guard against multi-second
cancellation HANGS, not compile time:

  - task_group::task_group_cancel_cancels_members
  - cancel_cascade::cancel_unblocks_await_on_non_descendant
  - spawn_semantics::parent_throw_cancels_running_children

Fix: compile OUTSIDE the timed region and measure only the engine run.
Added a public `run_compiled(program, entry, args, show_auto_derive)`
to the baml_tests engine harness (split out of run_test_with_options)
and a `build_engine`/`run_engine` split in the bex_engine task_group
test, so the assertions time execution alone. Bounds restored to tight
values (2s) well under the sleeps they guard.

Verified: cargo test --workspace --all-features (sdk_test_* excluded as
in CI) — all suites green, including the three above and baml_src on a
warm build; prek run --all-files clean. rustc 1.93.0.
@vercel
vercel Bot temporarily deployed to Preview – beps June 8, 2026 16:48 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 8, 2026 16:53 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 8, 2026 17:05 Inactive
@antoniosarosi
antoniosarosi added this pull request to the merge queue Jun 8, 2026
Merged via the queue into canary with commit ebbdb22 Jun 8, 2026
51 checks passed
@antoniosarosi
antoniosarosi deleted the antoniosarosi/Future-Combinators branch June 8, 2026 17:34

This branch was previously deployed

4 inactive (1 outdated) deployments
Preview – promptfiddle — b188f2a2 Deployed Jun 8, 2026 by vercel[bot]
Preview – promptfiddle2 — b188f2a2 Deployed Jun 8, 2026 by vercel[bot]
Preview – beps — b188f2a2 Deployed Jun 8, 2026 by vercel[bot]
Preview – new-boundaryml-website — e69b1369 Deployed Jun 4, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant