Skip to content

feat: add lambda expression support to BAML compiler#3302

Merged
hellovai merged 31 commits intocanaryfrom
vbv/add-lambda-expression-support-to-baml-compiler
Mar 30, 2026
Merged

feat: add lambda expression support to BAML compiler#3302
hellovai merged 31 commits intocanaryfrom
vbv/add-lambda-expression-support-to-baml-compiler

Conversation

@hellovai
Copy link
Copy Markdown
Contributor

@hellovai hellovai commented Mar 29, 2026

What problems was I solving

BAML had no support for lambda expressions (anonymous functions / closures). Users couldn't write inline callbacks, pass functions to higher-order methods like .map(), or use closures that capture variables from enclosing scopes. Every transformation required a named function definition, making code verbose and blocking a broad class of functional programming patterns — from simple transforms to closures returned from functions, IIFEs, and nested lambdas with shared mutable state.

After this PR, lambda expressions are a first-class feature across the entire compiler stack: they parse, format, type-check with bidirectional inference, compile to bytecode with cell-based capture semantics, and execute in the VM with full GC support. Users can write lambdas with minimal annotation — parameter and return types are inferred from context when possible.

What user-facing changes did I ship

  • Lambda expression syntax: (params) -> [RetType] { body } with optional type annotations, optional return type, optional generic parameters (<T>(x: T) -> T { x }), and optional throws clauses
  • Bidirectional type inference: lambda parameter types inferred from call context (e.g. items.map((x) -> { x * 2 }) infers x: int from Array<int>.map)
  • Closure capture semantics: lambdas capture variables by shared reference via cells — mutations in the closure are visible to the parent and vice versa
  • Transitive capture propagation: nested lambdas automatically capture variables from grandparent+ scopes through intermediate lambdas
  • New container methods: Array.map<U>, Map.map<U>, Map.map_keys<U>, Map.map_values<U> — higher-order methods that accept lambda callbacks
  • Error diagnostics: type mismatches, arity mismatches, missing param annotations (when no context available), and param type conflicts are all reported
  • Test projects: lambda_basic (9 functions), lambda_advanced (~33 functions), lambda_errors (4 error cases) with full snapshot coverage across all 8 compiler phases

How I implemented it

This was a full-stack feature spanning every compiler layer, implemented across 18 commits in two tasks: first the frontend (parser → AST → HIR → TIR + formatter), then the backend (MIR → emit → bytecode → VM → GC).

1. Syntax & Parser

  • syntax_kind.rs — Added LAMBDA_EXPR node kind to the CST
  • parser.rs — Added looks_like_lambda() and looks_like_generic_lambda() disambiguation predicates with depth-aware paren scanning (up to 64 tokens lookahead). Added parse_lambda_expr() with parse_lambda_parameter_list() (optional type annotations unlike regular function params). Hooked into parse_primary_expr for both LParen (regular lambdas) and Less (generic lambdas)

2. AST Lowering

  • ast.rs — Added Expr::Lambda(Box<FunctionDef>) variant reusing the existing FunctionDef struct with synthetic name "<anonymous function>"
  • lower_expr_body.rs — Added lower_lambda_expr with a fresh LoweringContext for the lambda's own ExprBody arena. Made lower_params public via lower_cst.rs

3. HIR Scope Registration & Capture Analysis

  • hir/builder.rs — Added pass that pushes ScopeKind::Lambda, registers params, and recursively walks the lambda's own ExprBody. ScopeBindings.captures records which parent-scope variables each lambda references. ScopeBindings.captured_names on function scopes marks which locals are captured by any descendant lambda — the foundation for cell wrapping in MIR

4. TIR Type Inference

  • tir/builder.rs — Three major additions:
    1. infer_expr (synthesis): Lambda with no expected type — requires annotated params, infers return from body
    2. check_expr (checking): Lambda with expected Ty::Function — decomposes expected type for bidirectional param/return inference (the key to items.map((x) -> { x * 2 }))
    3. infer_lambda_body helper: Save/restore approach for locals, declared_types, return_ty, generic_params, and expressions (to prevent ExprId collisions between lambda and parent arenas). Lambda params seeded on top of parent locals so captures work naturally
    4. Two-pass generic call inference: Non-lambda args are inferred first to bind type vars, then lambda args are checked with resolved bindings (e.g. apply((x) -> { x * 2 }, 21) binds T=int from 21 before checking the lambda)
    5. Method-level generic resolution: Fixed resolve_member_access to include method-level generics in bindings so items.map<U>(...) resolves correctly
  • infer_context.rs — Added CannotInferLambdaParamType diagnostic variant
  • normalize.rs — Fixed Optional/Union subtyping to work in both directions

5. Formatter

  • expressions.rs — Added LambdaExpr, GenericParamList, ThrowsClause structs with full FromCST/Printable impls, dispatching on LAMBDA_EXPR CST node
  • tokens.rs / lib.rs — Supporting token and top-level dispatch additions

6. Container Builtins

  • containers.baml — Added Array.map<U>(self, f: (T) -> U) -> U[], Map.map<U>(self, f: (K, V) -> U) -> U[], Map.map_keys<U>, Map.map_values<U>
  • array.rs / map.rs — VM implementations that invoke closures via call_indirect

7. VM Structural Foundation

  • types.rsObject::Closure { function: HeapPtr, captures: Vec<Value> } and Object::Cell { value: Value } with full Display, ObjectType, and value_type_tag support
  • bytecode.rs — 7 new instructions: MakeClosure, MakeCell, LoadDeref, StoreDeref, LoadCapture, StoreCapture, and CaptureRef (7th not in original plan — needed for forwarding cell pointers to inner closures)
  • gc.rsadd_references_to_worklist and fixup_object_references trace Closure/Cell objects
  • vm.rscollect_frame_roots and apply_frame_forwarding ensure frame function pointers survive GC relocation. Execution handlers for all 7 instructions. resolve_callable_target, load_function, execute_call_from_locals_offset, and allocate_real_locals_for_frame all extended for Object::Closure
  • lib.rs (engine) — Frame root collection and forwarding in GC cycle

8. MIR IR & Lowering

  • ir.rsRvalue::MakeClosure { lambda_idx, captures }, Place::Capture(idx), MirFunction.lambdas: Vec<MirFunction>, LocalDecl.is_captured: bool
  • lower.rs — Key migration: expr_types key changed from AstExprId to (FileScopeId, AstExprId) with current_scope tracking, eliminating ExprId collisions. Added lower_lambda method with full save/restore of parent state (builder, body, source_map, locals, exit_block, loop/catch context, pending_lambdas, capture_indices). HIR captured_namesis_captured flag. capture_indices: Option<HashMap<Name, usize>> for lambda body variable resolution. transitive_captures_needed: Vec<Name> for nested lambda propagation

9. Emit Layer

  • pull_semantics.rsmake_closure(), load_capture(), store_capture_value() trait methods + walk_rvalue_pull and walk_place_pull arms
  • emit.rslambda_object_indices, lambda_names, captured_locals, loading_for_closure_capture fields on StackifyCodegen. Cell-wrapping preamble: LoadVar → MakeCell → StoreVar for each is_captured local. Parent-side deref: LoadDeref/StoreDeref instead of LoadVar/StoreVar. Capture operand loading: LoadVar (bypassing deref) when loading_for_closure_capture is set. Lambda body: Place::Capture(idx)LoadCapture(idx) / StoreCapture(idx). Transitive forwarding: CaptureRef(idx) via load_capture when loading_for_closure_capture is set
  • lib.rs (emit)compile_lambdas_flat() recursively compiles lambda MirFunctions into bytecode Function objects, registering them in program.objects. lower_let_body changed to return lambdas alongside the body

10. Tests

  • compiler2_tir/mod.rs — 216+ lines of TIR snapshot infrastructure: format_lambda_signature, render_expr_body_untyped, lambda capture annotations, compound argument expansion
  • 52+ new snapshot files across lexer, parser, HIR, TIR, MIR, diagnostics, codegen, and formatter phases for all three lambda test projects
  • Existing snapshot updates — Minor codegen snapshot changes in 8 existing test projects (format_checks, control_flow, etc.) due to new container methods

Deviations from the plans

This feature was implemented across two plans:

  • Plan A ("Lambda Expression Support"): parser → AST → HIR → TIR + formatter
  • Plan B ("Lambda Closure Compilation and Runtime Support"): MIR → emit → VM → GC

Implemented as planned

  • Plan A: Test projects, parser with disambiguation, Expr::Lambda(Box<FunctionDef>), HIR lambda scope pass, formatter LambdaExpr — all match plan
  • Plan B: Phase 1 structural foundation (VM types, GC, bytecode, MIR IR), Phase 2 ExprId fix + emit infrastructure, Phase 3 non-capturing lambda lowering, Phase 4 capturing lambdas with cells, Phase 5 transitive captures — all match plan

Deviations/surprises

  • Save/restore vs nested builder (Plan A): Plan A discussed both approaches and preferred nested builder for TIR. Implementation used the simpler save/restore approach, noting ExprId collisions as fixable (and Plan B fixed them via the (FileScopeId, AstExprId) key migration)
  • Two-pass call inference (Plan A): Plan A specified a single-pass with contains_typevar check. Implementation went further with a true two-pass approach (non-lambda args first, then lambda args) for better generic resolution
  • Method-level generics fix (Plan A): Plan A said resolve_member_access needed "no changes." Implementation found and fixed a bug where method-level generic params (like <U> on Array.map<U>) weren't being added to bindings
  • Optional/Union subtyping fix (Plan A): Plan A said normalize.rs needed "no changes." A bug was found and fixed where Optional<T> wasn't recognized as a subtype of Union types containing T and Null
  • 7th bytecode instruction CaptureRef (Plan B): Plan B specified 6 instructions. Implementation added a 7th — CaptureRef pushes the raw cell pointer from a closure's captures without reading through the cell. Necessary for forwarding captured cells to inner closures during transitive capture propagation
  • loading_for_closure_capture flag (Plan B): Emit layer uses a boolean on StackifyCodegen to distinguish "load for closure capture" (cell pointer) vs "load for use" (cell value). Plan B mentioned the concept but didn't detail this mechanism
  • lower_let_body returns lambdas (Plan B): Plan B didn't account for lambdas inside let-binding initializers. Implementation changed lower_let_body to return Option<(MirFunctionBody, Vec<MirFunction>)>

Additions not in plans

  • lambda_advanced test project: 334-line comprehensive test suite exercising generic inference, chained maps, nested generics, optional/union patterns, shadowing, and complex composition
  • Builtin map/map_keys/map_values methods: Added to Array<T> and Map<K, V> in containers.baml with VM implementations — needed to test real lambda-as-argument patterns
  • 216+ lines of TIR test infrastructure: Dedicated snapshot rendering for lambda signatures, lambda bodies, capture annotations, and compound argument expansion
  • LSP and onionskin exhaustive match updates: check.rs and compiler.rs needed Expr::Lambda arms

Items planned but not implemented

  • FFI/codegen for external targets (Python, TypeScript) — out of scope
  • PPIR streaming support for closures — out of scope
  • Closure inlining/devirtualization — out of scope
  • GC optimizations (escape analysis, open/closed upvalue transitions) — out of scope
  • Recursive closures (closures that capture themselves) — out of scope
  • Expression-body lambdas (without braces) — out of scope
  • Default lambda params — out of scope

How to verify it

Setup

git fetch
git checkout vbv/add-lambda-expression-support-to-baml-compiler

Automated Tests

# Full test suite
cargo test -p baml_tests

# Lambda-specific tests
cargo test -p baml_tests -- lambda_basic
cargo test -p baml_tests -- lambda_advanced
cargo test -p baml_tests -- lambda_errors

# Component tests
cargo test -p bex_vm
cargo test -p baml_compiler_parser
cargo test -p baml_compiler2_tir

Manual Testing

  • Review lambda_basic.baml — verify each lambda pattern (annotated, inferred, zero-param, capture, nested, generic, multi-param)
  • Review lambda_advanced.baml — verify complex patterns (chained maps, nested captures, higher-order return, IIFEs, shadowing)
  • Review lambda_errors.baml — verify each error case produces clear diagnostics
  • Review TIR snapshots — verify correct inferred types for all lambda expressions
  • Review MIR snapshots — verify [captured] annotations and make_closure rvalues with capture operands
  • Review codegen snapshots — verify cell-wrapping preamble, LoadDeref/StoreDeref, LoadCapture/StoreCapture, CaptureRef, MakeClosure instructions

Description for the changelog

Add lambda expression support to BAML: anonymous functions with (params) -> { body } syntax, bidirectional type inference, cell-based closure captures with shared mutation semantics, transitive capture propagation for nested lambdas, and higher-order container methods (Array.map, Map.map, Map.map_keys, Map.map_values).

Summary by CodeRabbit

  • New Features

    • Lambda expressions: generics, parameter/return annotations, optional throws, block bodies, closures with captured variables.
    • Container mapping helpers: Array.map, Map.map, Map.map_keys, Map.map_values.
  • Bug Fixes / Diagnostics

    • New diagnostic for uninferrable lambda parameter types.
  • Tests

    • Extensive new unit/integration tests exercising lambdas, captures, map usage, and error cases.
  • Runtime / Tooling

    • VM, bytecode, GC, formatter and diagnostic integrations updated to support closures, capture cells, and new bytecode.

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 29, 2026

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

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Mar 30, 2026 11:09pm
promptfiddle Ready Ready Preview, Comment Mar 30, 2026 11:09pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 29, 2026

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

Adds end-to-end lambda/closure support (parsing, CST→AST lowering, HIR capture analysis, TIR inference/checking, MIR lowering, bytecode/runtime closure & cell support), Array/Map map* builtin declarations, formatter updates, extensive tests, and VM/GC/emitter integration for captures.

Changes

Cohort / File(s) Summary
Parser & Syntax
baml_language/crates/baml_compiler_syntax/src/syntax_kind.rs, baml_language/crates/baml_compiler_parser/src/parser.rs
Introduce LAMBDA_EXPR SyntaxKind and parsing for generic params, parameter lists, -> return type, optional throws, block body, and lookahead heuristics.
CST → AST Lowering
baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs, baml_language/crates/baml_compiler2_ast/src/ast.rs, baml_language/crates/baml_compiler2_ast/src/lower_cst.rs
Lower lambda CST into Expr::Lambda(Box<FunctionDef>), add lower_lambda_expr, and promote some lowering helpers to pub(crate).
HIR / Capture Analysis
baml_language/crates/baml_compiler2_hir/src/builder.rs, baml_language/crates/baml_compiler2_hir/src/semantic_index.rs
Create lambda scopes, seed params, perform capture resolution, and record captures; add captures and captured_names to ScopeBindings.
TIR: Inference & Checking
baml_language/crates/baml_compiler2_tir/src/builder.rs, .../infer_context.rs, .../normalize.rs, .../inference.rs
Add lambda inference/checking (including save/restore of inference state), infer_lambda_body helper, new CannotInferLambdaParamType error, two-pass call-site inference to handle lambda args, and subtype rule adjustments for Optional/Union.
MIR: IR, Lowering & Cleanup
baml_language/crates/baml_compiler2_mir/src/ir.rs, .../lower.rs, .../builder.rs, .../cleanup.rs, .../analysis.rs, .../pretty.rs
Extend MIR with MirFunction.lambdas, Rvalue::MakeClosure, Place::Capture, and LocalDecl.is_captured; lower lambdas to child MIR, thread scope-aware type maps, and update cleanup/analysis/pretty to handle captures/closures.
Emit / Pull Semantics / Codegen
baml_language/crates/baml_compiler2_emit/src/lib.rs, .../emit.rs, .../pull_semantics.rs, .../stack_carry.rs
Add codegen context fields for precompiled lambdas, new sink APIs (make_closure,load_capture,store_capture_value), stack simulation updates, closure emission (MakeClosure/MakeCell), and capture-load/store handling.
Bytecode & Runtime Types
baml_language/crates/bex_vm_types/src/bytecode.rs, baml_language/crates/bex_vm_types/src/types.rs
Add bytecode instructions for closures/cells/captures and runtime types Object::Closure and Object::Cell plus corresponding structs and ObjectType variants.
VM & Heap Integration
baml_language/crates/bex_vm/src/vm.rs, baml_language/crates/bex_heap/src/gc.rs, baml_language/crates/bex_heap/src/accessor.rs, baml_language/crates/bex_engine/src/conversion.rs, babl_language/crates/bex_heap/src/heap_debugger/real.rs
VM handlers for closure/cell instructions, GC tracing/forwarding and frame-root handling, conversion/accessor rejections for closures/cells, and heap invariant checks/trace/fixup for closures and cells.
VM Packaging & Formatting
baml_language/crates/bex_vm/src/package_baml/..., baml_language/crates/bex_vm/src/debug.rs, .../unstable.rs, .../root.rs, baml_language/crates/bex_vm/src/package_baml/array.rs, .../map.rs
Formatting/debug updates for closures/cells, deep-copy allocation for closures/cells, and added stub PackageBaml map/map_keys/map_values implementations that currently todo!() panic.
Formatter & AST Printing
baml_language/crates/baml_fmt/src/ast/expressions.rs, .../tokens.rs, .../lib.rs
Add LambdaExpr, GenericParamList, ThrowsClause, Throws token, formatter printing for lambdas, and idempotency tests.
Tests & Snapshots
baml_language/crates/baml_tests/..., baml_language/crates/baml_tests/src/compiler2_tir/mod.rs, baml_language/crates/baml_tests/tests/lambdas.rs
Add many BAML test projects (basic/advanced/errors), Rust async runtime tests, and TIR/HIR snapshot rendering for lambda signatures, bodies, and capture annotations.
Builtins Declarations
baml_language/crates/baml_builtins2/baml_std/baml/containers.baml
Declare Array.map, Map.map, Map.map_keys, and Map.map_values methods (implemented via $rust_function).
Small API / Visibility
baml_language/crates/baml_compiler2_ast/src/lower_cst.rs, baml_language/crates/baml_compiler2_mir/src/builder.rs
Promote some lowering helpers to pub(crate), add MirBuilder accessors, and initialize LocalDecl.is_captured = false in locals.

Sequence Diagram(s)

sequenceDiagram
    participant Src as Source
    participant Parser as Parser
    participant CST as CST
    participant Lower as Lowering/AST
    participant HIR as HIR/Scopes
    participant TIR as TIR/Infer
    participant MIR as MIR/Lower
    participant Codegen as Codegen/Emit
    participant VM as VM/Runtime

    Src->>Parser: parse lambda syntax (<...>(params) -> { ... })
    Parser->>CST: produce LAMBDA_EXPR node
    CST->>Lower: lower_lambda_expr -> Expr::Lambda(FunctionDef)
    Lower->>HIR: create lambda scope, register params
    HIR->>TIR: infer_lambda_body / type-check lambda
    TIR->>MIR: lower lambda into child MirFunction + Rvalue::MakeClosure
    MIR->>Codegen: collect/compile nested lambdas, emit MakeClosure/MakeCell
    Codegen->>VM: runtime allocates Closure/Cell and wires captures
Loading

Estimated Code Review Effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • imalsogreg
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly summarizes the main change: adding lambda expression support to the BAML compiler, which aligns directly with the extensive changes across parser, AST, type inference, MIR, emit, and runtime components.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch vbv/add-lambda-expression-support-to-baml-compiler

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
baml_language/crates/baml_compiler_parser/src/parser.rs (1)

3562-3599: 🧹 Nitpick | 🔵 Trivial

Add parser-level tests for the new lambda disambiguation branches.

parse_primary_expr now relies on custom ( / < lookahead to choose between lambdas and non-lambda expressions. A few unit tests in this module for (x) -> {}, <T>(x) -> {}, (x), and nested parameter types would make regressions much easier to localize than end-to-end fixtures alone.

As per coding guidelines, **/*.rs: Prefer writing Rust unit tests over integration tests where possible.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 06170aa1-cc8f-4342-8db1-3a9d22fdd0b7

📥 Commits

Reviewing files that changed from the base of the PR and between 1388039 and 0b6cc48.

⛔ Files ignored due to path filters (37)
  • baml_language/crates/baml_tests/snapshots/__baml_std__/baml_tests____baml_std____03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/__baml_std__/baml_tests____baml_std____04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/__baml_std__/baml_tests____baml_std____06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/comment_after_string_in_config/baml_tests__comment_after_string_in_config__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/comment_in_type/baml_tests__comment_in_type__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/config_dictionary/baml_tests__config_dictionary__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/config_model_string/baml_tests__config_model_string__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/control_flow/baml_tests__control_flow__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/format_checks/baml_tests__format_checks__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/header_in_llm_function/baml_tests__header_in_llm_function__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_advanced/baml_tests__lambda_advanced__01_lexer__lambda_advanced.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_advanced/baml_tests__lambda_advanced__02_parser__lambda_advanced.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_advanced/baml_tests__lambda_advanced__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_advanced/baml_tests__lambda_advanced__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_advanced/baml_tests__lambda_advanced__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_advanced/baml_tests__lambda_advanced__05_diagnostics.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_advanced/baml_tests__lambda_advanced__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_advanced/baml_tests__lambda_advanced__10_formatter__lambda_advanced.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_basic/baml_tests__lambda_basic__01_lexer__lambda_basic.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_basic/baml_tests__lambda_basic__02_parser__lambda_basic.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_basic/baml_tests__lambda_basic__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_basic/baml_tests__lambda_basic__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_basic/baml_tests__lambda_basic__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_basic/baml_tests__lambda_basic__05_diagnostics.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_basic/baml_tests__lambda_basic__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_basic/baml_tests__lambda_basic__10_formatter__lambda_basic.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_errors/baml_tests__lambda_errors__01_lexer__lambda_errors.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_errors/baml_tests__lambda_errors__02_parser__lambda_errors.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_errors/baml_tests__lambda_errors__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_errors/baml_tests__lambda_errors__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_errors/baml_tests__lambda_errors__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_errors/baml_tests__lambda_errors__05_diagnostics.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_errors/baml_tests__lambda_errors__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_errors/baml_tests__lambda_errors__10_formatter__lambda_errors.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/o1_allowed_roles/baml_tests__o1_allowed_roles__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/retry_policy/baml_tests__retry_policy__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/src/compiler2_tir/snapshots/baml_tests__compiler2_tir__phase5__snapshot_baml_package_items.snap is excluded by !**/*.snap
📒 Files selected for processing (22)
  • baml_language/crates/baml_builtins2/baml_std/baml/containers.baml
  • baml_language/crates/baml_compiler2_ast/src/ast.rs
  • baml_language/crates/baml_compiler2_ast/src/lower_cst.rs
  • baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs
  • baml_language/crates/baml_compiler2_hir/src/builder.rs
  • baml_language/crates/baml_compiler2_mir/src/lower.rs
  • baml_language/crates/baml_compiler2_tir/src/builder.rs
  • baml_language/crates/baml_compiler2_tir/src/infer_context.rs
  • baml_language/crates/baml_compiler2_tir/src/normalize.rs
  • baml_language/crates/baml_compiler_parser/src/parser.rs
  • baml_language/crates/baml_compiler_syntax/src/syntax_kind.rs
  • baml_language/crates/baml_fmt/src/ast/expressions.rs
  • baml_language/crates/baml_fmt/src/ast/tokens.rs
  • baml_language/crates/baml_fmt/src/lib.rs
  • baml_language/crates/baml_lsp2_actions/src/check.rs
  • baml_language/crates/baml_tests/projects/lambda_advanced/lambda_advanced.baml
  • baml_language/crates/baml_tests/projects/lambda_basic/lambda_basic.baml
  • baml_language/crates/baml_tests/projects/lambda_errors/lambda_errors.baml
  • baml_language/crates/baml_tests/src/compiler2_tir/mod.rs
  • baml_language/crates/bex_vm/src/package_baml/array.rs
  • baml_language/crates/bex_vm/src/package_baml/map.rs
  • baml_language/crates/tools_onionskin/src/compiler.rs

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 29, 2026

Binary size checks passed

7 passed

Artifact Platform Gzip Baseline Delta Status
bridge_cffi Linux 4.3 MB 5.7 MB -1.4 MB (-24.6%) OK
bridge_cffi-stripped Linux 2.8 MB 4.3 MB -1.5 MB (-35.4%) OK
bridge_cffi macOS 3.5 MB 4.6 MB -1.1 MB (-24.1%) OK
bridge_cffi-stripped macOS 2.2 MB 3.5 MB -1.2 MB (-35.5%) OK
bridge_cffi Windows 3.5 MB 4.6 MB -1.1 MB (-24.6%) OK
bridge_cffi-stripped Windows 2.3 MB 3.5 MB -1.3 MB (-35.3%) OK
bridge_wasm WASM 2.2 MB 3.0 MB -738.9 KB (-24.9%) OK

Generated by cargo size-gate · workflow run

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Mar 29, 2026

Merging this PR will not alter performance

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

✅ 15 untouched benchmarks
⏩ 98 skipped benchmarks1


Comparing vbv/add-lambda-expression-support-to-baml-compiler (5669b4c) with canary (fa79810)

Open in CodSpeed

Footnotes

  1. 98 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
baml_language/crates/bex_vm_types/src/types.rs (1)

809-817: ⚠️ Potential issue | 🟠 Major

Object::Variant is still classified as enum.

While touching ObjectType::of(), the variant arm below still returns Self::Enum. That keeps runtime type checks and diagnostics wrong for actual enum values.

🩹 Proposed fix
         match ob {
             Object::Function(func) => Self::Function(FunctionType::from(&func.kind)),
             Object::Closure(_) => Self::Closure,
             Object::Cell(_) => Self::Cell,
             Object::Class(_) => Self::Class,
             Object::Instance(_) => Self::Instance,
             Object::Enum(_) => Self::Enum,
-            Object::Variant(_) => Self::Enum,
+            Object::Variant(_) => Self::Variant,
             Object::String(_) => Self::String,
             Object::Array(_) => Self::Array,
             Object::Map(_) => Self::Map,
♻️ Duplicate comments (2)
baml_language/crates/baml_tests/src/compiler2_tir/mod.rs (2)

253-284: ⚠️ Potential issue | 🟡 Minor

Render lambda throws clauses in signatures.

FunctionDef::throws is still ignored here, so throwing lambdas snapshot exactly like non-throwing ones in both the TIR and HIR renderers.

🩹 Proposed fix
         let ret = func_def
             .return_type
             .as_ref()
             .map(|te| format!(" {}", type_expr_to_string(&te.expr)))
             .unwrap_or_default();
+        let throws = func_def
+            .throws
+            .as_ref()
+            .map(|te| format!(" throws {}", type_expr_to_string(&te.expr)))
+            .unwrap_or_default();
         let generics = if func_def.generic_params.is_empty() {
             String::new()
         } else {
             format!(
                 "<{}>",
@@
-        format!("{generics}({}) ->{ret} {{ ... }}", params.join(", "))
+        format!("{generics}({}) ->{ret}{throws} {{ ... }}", params.join(", "))

402-410: ⚠️ Potential issue | 🟠 Major

Keep lambda body snapshots typed.

This path still drops into render_expr_body_untyped, so inferred parameter, capture, and return types inside lambdas stop being asserted by the TIR snapshots. Please plumb the lambda scope’s own ScopeInference here instead of using the untyped fallback.

Also applies to: 447-497


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3ee3e52f-6c8a-40e2-9abe-9f140b3c3833

📥 Commits

Reviewing files that changed from the base of the PR and between 0b6cc48 and b34dba1.

⛔ Files ignored due to path filters (10)
  • baml_language/crates/baml_tests/snapshots/lambda_advanced/baml_tests__lambda_advanced__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_advanced/baml_tests__lambda_advanced__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_advanced/baml_tests__lambda_advanced__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_advanced/baml_tests__lambda_advanced__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_basic/baml_tests__lambda_basic__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_basic/baml_tests__lambda_basic__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_basic/baml_tests__lambda_basic__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_basic/baml_tests__lambda_basic__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_errors/baml_tests__lambda_errors__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_errors/baml_tests__lambda_errors__06_codegen.snap is excluded by !**/*.snap
📒 Files selected for processing (29)
  • baml_language/crates/baml_compiler2_emit/src/analysis.rs
  • baml_language/crates/baml_compiler2_emit/src/emit.rs
  • baml_language/crates/baml_compiler2_emit/src/lib.rs
  • baml_language/crates/baml_compiler2_emit/src/pull_semantics.rs
  • baml_language/crates/baml_compiler2_emit/src/stack_carry.rs
  • baml_language/crates/baml_compiler2_emit/src/verifier.rs
  • baml_language/crates/baml_compiler2_hir/src/builder.rs
  • baml_language/crates/baml_compiler2_hir/src/semantic_index.rs
  • baml_language/crates/baml_compiler2_mir/src/builder.rs
  • baml_language/crates/baml_compiler2_mir/src/cleanup.rs
  • baml_language/crates/baml_compiler2_mir/src/ir.rs
  • baml_language/crates/baml_compiler2_mir/src/lower.rs
  • baml_language/crates/baml_compiler2_mir/src/pretty.rs
  • baml_language/crates/baml_compiler2_tir/src/builder.rs
  • baml_language/crates/baml_compiler2_tir/src/inference.rs
  • baml_language/crates/baml_tests/src/compiler2_tir/mod.rs
  • baml_language/crates/baml_tests/tests/lambdas.rs
  • baml_language/crates/bex_engine/src/conversion.rs
  • baml_language/crates/bex_engine/src/lib.rs
  • baml_language/crates/bex_heap/src/accessor.rs
  • baml_language/crates/bex_heap/src/gc.rs
  • baml_language/crates/bex_heap/src/heap_debugger/real.rs
  • baml_language/crates/bex_vm/src/debug.rs
  • baml_language/crates/bex_vm/src/package_baml/root.rs
  • baml_language/crates/bex_vm/src/package_baml/unstable.rs
  • baml_language/crates/bex_vm/src/vm.rs
  • baml_language/crates/bex_vm_types/src/bytecode.rs
  • baml_language/crates/bex_vm_types/src/types.rs
  • baml_language/crates/tools_onionskin/src/compiler.rs

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
baml_language/crates/baml_compiler2_tir/src/builder.rs (1)

4287-4294: ⚠️ Potential issue | 🟠 Major

Don't leak lambda-local ExprId state back into the parent builder.

infer_lambda_body isolates expressions and bindings, but resolutions, catch_residual_throws, and exhaustive_matches still accumulate lambda-local entries keyed by the lambda arena's ExprIds. Those ids can collide with the parent scope and corrupt member resolution, throw tracking, or match-exhaustiveness data after the lambda returns.

Suggested fix
         let saved_generic_params = self.generic_params.clone();
         let saved_expressions = std::mem::take(&mut self.expressions);
         let saved_bindings = std::mem::take(&mut self.bindings);
+        let saved_resolutions = std::mem::take(&mut self.resolutions);
+        let saved_catch_residual_throws = std::mem::take(&mut self.catch_residual_throws);
+        let saved_exhaustive_matches = std::mem::take(&mut self.exhaustive_matches);
@@
         let lambda_expressions = std::mem::replace(&mut self.expressions, saved_expressions);
         self.bindings = saved_bindings;
+        self.resolutions = saved_resolutions;
+        self.catch_residual_throws = saved_catch_residual_throws;
+        self.exhaustive_matches = saved_exhaustive_matches;
         self.locals = saved_locals;
         self.declared_types = saved_declared;
         self.declared_return_ty = saved_return_ty;

Also applies to: 4357-4363


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9463ebcc-78a5-4553-ae90-aa12e8c44294

📥 Commits

Reviewing files that changed from the base of the PR and between b34dba1 and 582ccaf.

⛔ Files ignored due to path filters (21)
  • baml_language/crates/baml_tests/snapshots/__baml_std__/baml_tests____baml_std____03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/__baml_std__/baml_tests____baml_std____04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/__baml_std__/baml_tests____baml_std____04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/__baml_std__/baml_tests____baml_std____06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_advanced/baml_tests__lambda_advanced__01_lexer__lambda_advanced.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_advanced/baml_tests__lambda_advanced__02_parser__lambda_advanced.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_advanced/baml_tests__lambda_advanced__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_advanced/baml_tests__lambda_advanced__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_advanced/baml_tests__lambda_advanced__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_advanced/baml_tests__lambda_advanced__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_advanced/baml_tests__lambda_advanced__10_formatter__lambda_advanced.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_basic/baml_tests__lambda_basic__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_basic/baml_tests__lambda_basic__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_errors/baml_tests__lambda_errors__01_lexer__lambda_errors.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_errors/baml_tests__lambda_errors__02_parser__lambda_errors.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_errors/baml_tests__lambda_errors__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_errors/baml_tests__lambda_errors__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_errors/baml_tests__lambda_errors__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_errors/baml_tests__lambda_errors__05_diagnostics.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_errors/baml_tests__lambda_errors__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_errors/baml_tests__lambda_errors__10_formatter__lambda_errors.snap is excluded by !**/*.snap
📒 Files selected for processing (6)
  • baml_language/crates/baml_builtins2/baml_std/baml/containers.baml
  • baml_language/crates/baml_compiler2_tir/src/builder.rs
  • baml_language/crates/baml_tests/projects/lambda_advanced/lambda_advanced.baml
  • baml_language/crates/baml_tests/projects/lambda_errors/lambda_errors.baml
  • baml_language/crates/baml_tests/src/compiler2_tir/mod.rs
  • baml_language/crates/bex_vm/src/package_baml/map.rs

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (3)
baml_language/crates/baml_tests/src/compiler2_tir/mod.rs (1)

253-292: ⚠️ Potential issue | 🟡 Minor

Use an HIR-aware formatter for lambda signatures.

This shared helper still hard-codes type_expr_to_string(), so the HIR path prints lambda param/return/throws annotations without the package qualification that render_hir2() preserves elsewhere. Please split this into TIR/HIR variants or pass a type-formatting callback.

Also applies to: 1381-1395

baml_language/crates/baml_compiler2_tir/src/builder.rs (2)

693-739: ⚠️ Potential issue | 🟠 Major

Lambda-arg inference is still source-order dependent.

Pass 2 still visits lambda arguments once in source order. If an earlier lambda needs a type variable that only a later lambda binds, the earlier one still falls back to synthesis and can emit CannotInferLambdaParamType for an otherwise-valid call. Defer unresolved lambda args and retry until the bindings map stops changing.


838-890: ⚠️ Potential issue | 🟠 Major

Check-mode lambdas still ignore local generics and can widen the expected return.

These annotation lowerings only see self.generic_params, so lambda-local <T> annotations still become Unknown here even though synthesis mode handles them. effective_ret also takes the explicit annotation without first checking annotation <: expected_ret, which accepts wider return contracts than the expected function type.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7348f974-0193-405f-ac5d-ea1d55e42290

📥 Commits

Reviewing files that changed from the base of the PR and between 582ccaf and e706654.

📒 Files selected for processing (7)
  • baml_language/crates/baml_compiler2_hir/src/builder.rs
  • baml_language/crates/baml_compiler2_hir/src/semantic_index.rs
  • baml_language/crates/baml_compiler2_mir/src/lower.rs
  • baml_language/crates/baml_compiler2_tir/src/builder.rs
  • baml_language/crates/baml_compiler2_tir/src/inference.rs
  • baml_language/crates/baml_tests/src/compiler2_tir/mod.rs
  • baml_language/crates/baml_tests/tests/lambdas.rs

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
baml_language/crates/baml_tests/src/compiler2_tir/mod.rs (1)

475-500: ⚠️ Potential issue | 🟡 Minor

IIFEs and nested lambda call sites still hide their bodies in TIR snapshots.

The typed Expr::Call path only expands compound arguments, not a compound callee, and render_expr_body_untyped() has no call-specific recursion at all. (() -> { ... })() and higher-order calls inside lambda bodies therefore collapse back to the { ... } placeholder, which underasserts some of the new closure cases.

Also applies to: 511-558


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c1ccefaa-582e-45d0-a29c-294f9358cf5f

📥 Commits

Reviewing files that changed from the base of the PR and between e706654 and 21fa705.

⛔ Files ignored due to path filters (3)
  • baml_language/crates/baml_tests/snapshots/lambda_advanced/baml_tests__lambda_advanced__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_basic/baml_tests__lambda_basic__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/lambda_errors/baml_tests__lambda_errors__04_5_mir.snap is excluded by !**/*.snap
📒 Files selected for processing (5)
  • baml_language/crates/baml_compiler2_mir/src/pretty.rs
  • baml_language/crates/baml_fmt/src/ast/expressions.rs
  • baml_language/crates/baml_tests/src/compiler2_tir/mod.rs
  • baml_language/crates/baml_tests/tests/lambdas.rs
  • baml_language/crates/bex_engine/src/lib.rs

hellovai added 12 commits March 30, 2026 14:55
Create lambda_basic and lambda_errors test projects with BAML files
exercising lambda syntax (annotated params, inferred return types,
captures, nested lambdas, generics, error cases). Generate baseline
snapshots showing expected parse errors before parser support is added.
Add LAMBDA_EXPR to SyntaxKind, implement looks_like_lambda and
looks_like_generic_lambda predicates for disambiguating parenthesized
expressions from lambda expressions, and add parse_lambda_expr to
handle the full lambda syntax including optional generics, params,
return type, throws clause, and block body.
Add Lambda(Box<FunctionDef>) to the Expr enum with CST lowering that
creates a fresh ExprBody for the lambda's block. Register lambda scopes
in HIR with ScopeKind::Lambda and seed param bindings. Add MIR panic
stub for lambda expressions. Wire up exhaustive match arms in TIR,
test harness, and tools_onionskin.
Implement synthesis and checking modes for Expr::Lambda in the TIR.
Lambdas in synthesis mode require annotated param types; in checking
mode, unannotated params get their types from the expected function
type context. Variable capture works via save/restore of locals.
Fix partially-resolved function type checking so lambdas passed to
generic higher-order functions get contextual param types even when
the return type contains unresolved type vars.
Handle LAMBDA_EXPR CST nodes in the formatter with proper printing
of optional generic params, parameter list, arrow, optional return
type, optional throws clause, and block body. Add GenericParamList
and ThrowsClause formatter types. Include idempotency unit tests.
Add parse_lambda_parameter_list/parse_lambda_parameter that allow
type annotations to be omitted (e.g. (x) -> { x * 2 }). This removes
the spurious E0010 parser error for unannotated lambda params.

Update HIR and TIR snapshot rendering to recursively print lambda
bodies instead of just showing "<lambda>", making it possible to
verify the internal expression structure.
The formatter expects if-conditions wrapped in parentheses (PAREN_EXPR),
matching BAML's standard syntax. Change `if x < 0` to `if (x < 0)` in
the lambda_basic test so the formatter can round-trip the file correctly.
Replace the catch-all <stmt> placeholder in render_stmt_untyped with
proper rendering of throw, return, assign, break, continue, and
assert statements so lambda body snapshots show full expression trees.
When a call expression has compound arguments (lambdas, blocks,
if-expressions), expand them recursively below the call line
so lambda bodies passed to higher-order functions are visible.
Add higher-order map methods to Array<T> and Map<K,V> with VM stubs.
Array.map<U>(f: (T) -> U) -> U[] and Map.map/map_keys/map_values
enable lambda expressions as arguments to container methods.
In resolve_builtin_method, add method-level generic params (e.g. U in
map<U>) as TypeVar entries in the bindings map before lowering the
method signature. This allows method generics to survive as TypeVars
through lower_type_expr_with_generics, enabling call-site inference
to bind them from argument types (e.g. U=int from a lambda returning
int). Previously method generics became Unknown, causing map() to
return unknown[] instead of int[].
When inferring types for a generic lambda like <T>(x: T) -> T { x },
combine the lambda's own generic_params with the parent scope's
generic_params before calling lower_type_expr_in_ns. Previously only
parent generics were passed, so T was unrecognized and became Unknown.
hellovai and others added 11 commits March 30, 2026 14:56
Identify captured variables at HIR time in SemanticIndexBuilder.
For each lambda scope, walk the body's ExprBody to find single-segment
Path names that resolve to ancestor scope bindings (up to the Function
boundary). Record captures on ScopeBindings and mark defining scopes'
captured_names for downstream MIR cell-wrapping decisions.
…tructions, MIR data structures

Add Object::Closure and Object::Cell variants, 6 new bytecode instructions
(MakeClosure, MakeCell, LoadDeref, StoreDeref, LoadCapture, StoreCapture),
Rvalue::MakeClosure, MirFunction.lambdas, LocalDecl.is_captured, GC tracing,
and VM execution handlers. All additive — no existing behavior changes.
…osures

- Save/restore self.expressions and self.bindings in infer_lambda_body to
  prevent arena ID collisions between lambda and parent scopes
- Change expr_types and pat_types keys from bare AstExprId/AstPatId to
  (FileScopeId, AstExprId/AstPatId) for proper scope isolation
- Populate ScopeKind::Lambda with real inference in inference.rs
- Add PullSink::make_closure, StackifyCodegen::make_closure, and
  StackCarryPullSink::make_closure for emit layer support
Replace MIR panic stub with real lambda lowering via save/restore on
LoweringContext. Lambda bodies compile to separate MirFunctions stored
in parent's lambdas vec. Pass 4 recursively compiles lambda children
and maps lambda_idx to ObjectIndex for MakeClosure emission.

Non-capturing lambdas and IIFEs now compile and execute correctly.
Add Place::Capture(usize) to MIR for captured variable access in lambda
bodies. Wire HIR captured_names into LocalDecl.is_captured. Emit MakeCell
preamble for captured locals, LoadDeref/StoreDeref in parent functions,
LoadCapture/StoreCapture in lambda bodies. Captures are passed as operands
to MakeClosure and shared via Cell objects for mutation semantics.
Add CaptureRef instruction for forwarding cell pointers to inner closures
without dereferencing. Fix transitive capture propagation to avoid duplicate
entries. Mark lambda params as captured when nested lambdas reference them.

Add 7 execution tests: shared mutation counter, transitive captures across
3 scopes, IIFE returning closure, multiple closures sharing cells, deep
3-level nesting, and loop variable accumulation.
…snapshots, fix test types

- Report type expression diagnostics in all 4 lambda `lower_type_expr_in_ns`
  callsites (synthesis param/return, checking param/return) instead of silently
  dropping them. Added `test_unknown_param_type` and `test_unknown_return_type`
  error test cases that now correctly produce `unresolved type: NonExistentType`.
- Render `throws` clauses in `format_lambda_signature` for TIR/HIR snapshots.
- Fix `test_nested_map` return type from `int[]` to `int[][]`.
- Remove unused `map_keys`/`map_values` VM stubs (defined in BAML source, not
  `$rust_function`).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…hrough captures

Fix D: When a lambda body assigns a function call result to a captured
variable (e.g. `x = helper()`), the MIR builder panicked with "Call
destination must be a local place". Now lower_call routes non-Local
destinations through a temp local, then assigns to the real destination.

Fix F: Thread `DefinitionSite` through HIR capture tracking for
shadowing safety. `ScopeBindings.captures` is now `Vec<(Name,
DefinitionSite)>` instead of `Vec<Name>`, uniquely identifying which
declaration is captured. Mark `is_captured` at the capture-operand
building site in MIR (where the exact Local is known) instead of
relying solely on the name-based post-pass.

Also adds 9 execution tests probing issues identified in PR review:
- Issue A (virtual inlining): 2 tests — both pass
- Issue B (@watch + cells): 1 test — passes
- Issue D (capture call dest): 1 test — was crashing, now fixed
- Issue E (resolution key collision): 2 tests — both pass
- Issue F (shadowing captures): 2 tests — ignored (BAML disallows shadowing)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests for Issue B: verify that `watch let` variables captured by lambdas
work correctly with cell wrapping. Both `watch let x` with lambda-only
mutation and mixed parent + lambda mutation produce correct values.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…t, HIR type formatting

- Fix parked VMs missing frame-pointer forwarding after GC. The active
  VM path already called apply_frame_forwarding but parked VMs did not,
  leaving stale frame.function pointers after moving GC.
- Add `// lambda[idx]` labels to MIR pretty printer so child lambdas
  are unambiguously identified by their MakeClosure slot index.
- Propagate multi_lined from ThrowsClause inner type print so parent
  layout decisions get correct signal.
- Add format_lambda_signature_hir for HIR-qualified type names in
  lambda signatures (uses prefix for local type names).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@hellovai hellovai enabled auto-merge March 30, 2026 22:39
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