Repository navigation
Spawn options + future combinators - #3625
Conversation
…+ 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.
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR implements BEP-034 concurrency model across the compiler and runtime. It adds spawn middleware configuration ( ChangesBEP-034 async concurrency model
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
… 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.
…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.
⏭️ Performance benchmarks were skippedPerf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to To run them on this PR, do any of the following, then push a commit (or re-run CI):
|
|
No description provided. |
…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).
…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).
…; 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).
|
@coderabbitai Review disposition for the latest push (94d2b46): Fixed (9): False positive (1): AwaitAny empty-waiters park — Deferred (2, tracked): interface-dispatch inferred type args ( |
|
Tip For best results, initiate chat on the files or code changes.
Acknowledged:
✏️ Learnings added
🧠 Learnings used |
… 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.
…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.
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.
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
.bamlsource.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-rightspawn { e }evaluates toFuture<T, E>whereTis the body's value type andEits throws type. The body starts running immediately; the future is the handle.await fsuspends untilfsettles, then yields its value or re-throws its error.awaitdistributes over a union of futures:await (Future<A,E1> | Future<B,E2>)yieldsA | B(the error side is contributed to the awaiter's throws surface).spawn ... with <transformer> ...runs each transformer over an implicitSpawnParamsbefore the runtime creates the future (see middleware below).baml.future— futures & combinatorsbaml.spawn— spawn options &withmiddlewareA cancelled task throws
baml.panics.Cancelledat its nextawait.Writing your own middleware
A transformer is just a function returning
(SpawnParams<T,E>) -> SpawnParams<U,F>. It can set configfields, wrap
body(retry/timing), or replace it entirely (a type-changing transformer — e.g. afallback that catches and erases the error type to
null):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-onlywithclauseCancelToken(new/any/cancel/is_cancelled) + parent→child cancel cascadeTaskGrouprate limiting (FIFO admission, exact counts)detach(fresh cancel token; unhandled errors routed to the root task)withvalidation (must be abaml.spawn.SpawnConfig)Future combinators (
baml.future)race/any/all/all_complete— pure-BAML stdlib over a new 1-byteAwaitAnyengine primitive (__await_any: Future<T,E>[] -> int throws never, suspends until the first input settles)AllFailed<E>aggregate error class foranyrace/any/all) or left running (all_complete) per the BEP tableEngine/runtime fixes found along the way
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 spuriousUnhandledThrow. (tests/fire_and_forget.rs)anylet e =>in a generic fn emitted a constant-falseIsType(TypeVar→Void erasure) and silently rethrew; bare binds are irrefutable — no runtime testTerminator::Awaitoperands (await <param>inside.mapclosures)Generics: inferred type args at runtime
call_type_instantiations); MIR threads them toframe.type_args(LoadType/TypeArgRef) — instances built inside generic fns carry realclass_type_args, and typed arms likelet e: AllFailed<string>match (tests/generics_runtime.rs)let e: AllFailed<E>) lower via type-arg templates instead of erasingCanary merge (f5df38a)
Absorbs the spawn/GC permit deadlock fix (full
baml_testsnow 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 fcontributes the future'sEto escaping throwsmap'sU:=T)ShortCircuitemit assumed a stack-carried (PhiLike) destination; withLoadTypebefore call args the carry is validly rejected, soa || bas an inferred-generic call argument read an uninitialized local. Emit now materializes the destination on the short-circuit edge (no-op for PhiLike).any<T, E>→any<T, Err>(works around generic-param name capture vsmap<U, E>; proper fix tracked separately)withmiddleware (c37027a)Full BEP middleware:
spawn ... with a, b { body }builds aSpawnParams<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.optionsis 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.rsincl. the BEP'swithRetry.Complete bug ledger (every bug found & fixed on this branch)
Engine / runtime
UnhandledThrow. Fixed with the deferred-error model: the error parks in a GC-rooted stash, the future stays Pending (wake signal fired),future_readyis the single observe-and-consume point, and the spawner's post-await drain surfaces only never-observed errors.AwaitAnyindex desync — global-index bookkeeping desync brokeany(wrong winner index reported).Terminator::Awaitoperands —await <param>inside a.mapclosure read an uninitialized slot.ShortCircuitemit miscompile — the emitter assumed the destination is always stack-carried (PhiLike); when inferred-generic calls pushLoadTypebefore args the carry is validly rejected, soa || bas a call argument left the value on the stack and read an uninitialized local (true || falseevaluated 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 anIsTypefrom its inferred type; a rigid TypeVar erases toTy::Voidin 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 emptyclass_type_args; typed arms likelet e: AllFailed<string>never matched. Bindings now recorded (call_type_instantiations) and threaded toframe.type_args.7. Typed arms naming enclosing TypeVars compiled to constant-false —
let e: AllFailed<E>insideany<T, E>erased E→Void; now lowers viaTypeArgReftemplates resolved against the frame.8. Recorded instantiations poisoned by fresh literal types —
mk("hi")recordedT = Literal("hi")sois Box<string>compared unequal; recordings nowwiden_fresh(). Plus generic→generic chains recordedUnknown(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 fcontributed nothing to escaping throws — the future'sEis now part of the awaiting body's throw set.11. Callee generics matched only against rigid caller TypeVars never bound —
map'sU:=Tinfutures.map((f) -> { await f })was skipped as a TypeVar actual, leavingU[]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'sEunbound (leaking a raw TypeVar) or symbolic where the body demonstrably throws. Now fed from the side-table effective set: a non-throwing callback bindsE:=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 symbolicE.Parser / lowering (flushed out by the middleware work)
14.
spawn/awaitkeywords 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
jsoncode had been dodging it with explicitreturn).16. Unannotated lambdas defaulted to
throws neverwith a local violation — they now infer their throws surface from the body (required for body wraps like() -> { original() }whose throws is a genericE). Omitted throws on callback params remains effect-polymorphic; explicitthrowscontracts remain enforced (verified: E0096 fires through the forwarding chain — an enforcement that itself depends on fixes 10–13).Known issues (documented, not fixed here)
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.awaitdoes not yet distribute over unions of futures (BEP §await-distributes).awaitover unions (94d2b46)Awaiting
Future<A, E1> | Future<B, E2>types asA | Bwith both error types tracked (BEP §await-distributes). Tests:bex_engine/tests/await_union.rs.Review hardening (94d2b46)
CodeRabbit batch:
anyerrors in INPUT order (+test) · explicit lambda throws contracts preserved ·with-transformer throws contributed at spawn · copy-prop protectsawait 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:Objectenum exceeded its 64-byte budget underheap_debug(UnscheduledFutureboxed) — 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:call_type_instantiationsTIR→MIR fallback (dead code after the switch). Canary's ShortCircuit emit fix (store_on_taken_path) is the same insight as ours; took theirs.runtime_call_type_argslost literal widening — canary's recording threadsT = literal "hi"formk("hi"), so an escaped instance never matchesis Box<string>(runtime class-arg comparison is invariant). Restoredwiden_freshon inferred bindings.unknownholes —spawn withmiddleware checks transformers against(SpawnParams<T,E>) -> SpawnParams<unknown, unknown>; the normalizer implements class-arg invariance as structural equality, so the holes (andBuiltinUnknownfrom erased unresolved typevars, e.g. a variable-bound transformer; and equivalent spellings likeT = int | 99from fallback-arg joins) all failed E0001.is_subtypenow 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.heap_debugfeature gap —FutureRead::ErrorPendingarm added to the heap debugger's validity walk.task_group_cancel_cancels_memberstiming — the promptness bound timesrun_mainincluding 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()Tests:
bex_engine227/0 (incl.combinators,fire_and_forget,generics_runtime,cancellation,task_group);baml_testsdiagnostics 418/418, runtime fixtures 1650/1650 (one timing-sensitive fixture flakes only under full parallel suite load); snapshots regenerated.Summary by CodeRabbit