Skip to content

feat: generic instantiation expressions (let f = foo<int>) - #3640

Merged
codeshaunted merged 23 commits into
canaryfrom
avery/instantiation-expr
Jun 4, 2026
Merged

codeshaunted merged 23 commits into
canaryfrom
avery/instantiation-expr

Conversation

@codeshaunted

@codeshaunted codeshaunted commented Jun 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds generic instantiation expressions — referencing a generic callable with explicit type arguments as a value (not a call):

function foo<T>(x: T) -> T { x }

let f = foo<int>      // typed (int) -> int; f("s") is a type error
foo<int>(x)           // (call form, already supported)

Previously foo<int> in value position silently dropped the type args (parsed as a < comparison or lowered to a fully-generic ref), so let f = foo<int>; f("s") was wrongly accepted.

Behavior

let f = foo<int>                              // (int) -> int
let g = foo                                   // still <T>(x: T) -> T (bare ref unchanged)

foo<int> === foo<int>                         // true  (interned, pointer-stable)
foo<int> !== foo<string>                      // distinct specializations

let f = baml.json.from_string<User>; f(s)     // works (T=User reified at runtime)
let g = describe<User>; g()                   // reflect.type_of<T> -> "User"

function fwd<T>() { let g = describe<T>; g() } // param-dependent: resolves T at runtime

Plus enforcement and lowering fixes that fall out of this:

  • BEP-044 generic bounds are now enforced on instantiation values, not only at call sites.
  • Generic calls on non-PATH receivers ((b).foo<int>, arr[0].foo<int>, self.subs[i].m<T>(...)) previously lowered to <missing>; now lowered correctly (this fixed two latent std-lib <missing> bugs).

Implementation

Front-end

  • Parser: ports typescript-go's canFollowTypeArgumentsInExpression to disambiguate <…> in value position vs. a < comparison (reuses the existing balanced-> scan).
  • AST: new Expr::GenericApply { base, type_args }; lowering captures GENERIC_ARGS (with a consumed-args marker so foo<int>(x) doesn't double-count).
  • TIR: specializes the signature and clears generic_params, so a later call checks args against the concrete params.

Runtime — a generic-function value carries its type args so an indirect call behaves exactly like the direct foo<int>(...):

  • New Object::GenericFunction { function: GlobalIndex, type_args }. Stored by global slot (not HeapPtr), so it's a compile-time-poolable, GC-leaf value.
  • Concrete instantiations → a pooled, interned Constant::GenericFunction (dedup by (function, type_args) → one object → pointer-stable identity).
  • Param-dependent instantiations (foo<T> where T is the enclosing frame's type param) → a new MakeGenericFunction opcode that resolves the type-arg templates against the current frame's type_args at runtime.
  • The call path seeds both frame.type_args (for reflect.type_of<T> / bytecode bodies) and pending_call_type_args (for type-reifying natives).

Testing

  • Parser disambiguation matrix; TIR snapshot tests (specialization, f("s") rejection, arity, bounds, multi-arg).
  • Runtime fixture ns_instantiation_expr covering: end-to-end call, pointer identity, json-native-through-value, reflect-through-value (concrete + param-dependent), bound violation, deep-copy equality.
  • Full workspace test sweep green (4945 tests).
  • An adversarial review pass surfaced 3 real issues (equality of non-pooled copies, a latent entry-point panic, the param-dependent-reflect hole) — all fixed and regression-tested.

Notes

This is forward-compatible with function mocking: a generic-callable value carries its (function, type_args) structurally, and the VM seeds those at the call — exactly the key mock dispatch needs.

Summary by CodeRabbit

  • New Features

    • Instantiate generic functions as first-class values (e.g., let f = identity<int>;) so specialized callables can be stored, passed, and invoked later.
    • Parser now better distinguishes <...> generic-argument lists from comparison operators in value position.
  • Improvements

    • End-to-end support: type checking, bytecode, runtime representation, and VM dispatch for generic-function values and reflection.
  • Tests

    • Added extensive compile-time and runtime tests and formatter coverage for generic instantiation forms.

Reference a generic callable with explicit type args as a value (not a
call), typed as the specialized function. Ports typescript-go's
canFollowTypeArgumentsInExpression to disambiguate `<...>` in value
position; carries args through AST (Expr::GenericApply) and TIR
(specialize + clear generic_params so `f("s")` is a type error). Type
args are erased at runtime for now.

- Enforce BEP-044 generic bounds on instantiation values (not just calls).
- Lower non-PATH receivers (`(b).foo<int>`, `arr[0].foo<int>`) through the
  chain dispatch instead of silently dropping to Expr::Missing.

Known gap (fixed next): runtime type-arg erasure makes type-reifying
natives fail through a value (`let f = baml.json.from_string<User>; f(s)`
-> "missing type argument"); `reflect.type_of<T>` yields "unknown".
…nterned)

`let f = foo<int>` now lowers to a pooled, interned `Object::GenericFunction`
{ function: GlobalIndex, type_args } instead of erasing the type args. Calling
the value resolves the base function via its global slot and seeds
`frame.type_args` (for bytecode bodies / reflect.type_of<T>) and
`pending_call_type_args` (for type-reifying natives), so an indirect call
behaves exactly like the direct call `foo<int>(...)`.

- Fixes the runtime failure for type-reifying natives through a value
  (`let f = baml.json.from_string<User>; f(s)`) and makes
  `reflect.type_of<T>` through a value return the bound type, not "unknown".
- Interning by (function, type_args) over the shared object pool gives
  pointer-stable identity: `foo<int> === foo<int>`, distinct from `foo<string>`.
- Param-dependent (`foo<T>`) and exotic bases fall back to erasure.
- GenericFunction stores the base by GlobalIndex (not HeapPtr), so it is a
  compile-time-poolable, GC-leaf value.

Also corrects two std-lib generic calls on indexed receivers
(`self.sub_clients[idx].execute_oneshot<T>(...)`) that previously lowered to
`<missing>`.
Two gaps found by adversarial review of the GenericFunction value type:

- deep_equals had no (GenericFunction, GenericFunction) arm, so it fell to
  the `_ => false` catch-all. Interned values still compared equal via the
  pointer-identity fast path, but a non-pooled copy (e.g. from baml.deep_copy)
  wrongly compared unequal. Add a structural arm (function + type_args).
- set_entry_point / set_entry_point_with_type_args panicked on a
  GenericFunction entry (catch-all `other`). Not reachable from BAML today
  (spawned values are called via CallIndirect), but add defensive arms that
  seed type_args and resolve the inner function's kind via its global slot.
`let g = foo<T>` where `T` is the enclosing function's runtime type param
cannot be a compile-time-pooled constant, so previously it erased the type
arg — `reflect.type_of<T>` (and type-reifying natives) through such a value
returned "unknown"/wrong.

Add a runtime-materialization path: a new `Rvalue::MakeGenericFunction`
(carrying type-arg templates) and a `MakeGenericFunction` bytecode opcode
that pops `ntypeargs` `Object::Type` values (resolved against the current
frame's type_args by preceding LoadType instructions) and builds an
`Object::GenericFunction`. Lowering now splits:
- fully-concrete args  -> pooled, interned `Constant::GenericFunction`
  (pointer-stable identity, unchanged)
- param-dependent args -> `MakeGenericFunction` (fresh per eval, correct T)
- exotic bases         -> erase (unchanged)

Now `let g = describe<T>; g()` inside `fn<T>` resolves T correctly.
@vercel

vercel Bot commented Jun 2, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Jun 4, 2026 12:36am
promptfiddle Ready Ready Preview, Comment Jun 4, 2026 12:36am
promptfiddle2 Ready Ready Preview, Comment Jun 4, 2026 12:36am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 2, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Implements explicit generic instantiation as value expressions (foo<int>) end-to-end: parser disambiguation, new Expr::GenericApply, TIR inference/value-call semantics, MIR rvalues/constants, emitter/pull-sink changes, bytecode/opcode and VM support, GC/heap/serialization updates, formatter/visualizer edits, and extensive tests.

Changes

Generic instantiation expressions

Layer / File(s) Summary
Parser disambiguation for generic arguments
baml_language/crates/baml_compiler_parser/src/parser.rs
Newline-aware follow-token logic and TypeScript-inspired helpers detect bare generic-argument lists as GENERIC_ARGS in value position and disambiguate from comparison/shift expressions; regression tests cover value-position instantiation, terminators, member access, and false-positive cases.
AST variant and lowering to GenericApply
baml_language/crates/baml_compiler2_ast/src/ast.rs, baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs, baml_language/crates/baml_compiler2_hir/src/builder.rs
New Expr::GenericApply { base, type_args } variant represents instantiation as a value; lowering tracks consumed GENERIC_ARGS nodes and conditionally wraps path/receiver expressions to avoid duplicate wrapping; HIR builder walks the base expression to record scope and path-root references.
HIR and formatter AST support
baml_language/crates/baml_fmt/src/ast/expressions.rs, baml_language/crates/baml_fmt/src/lib.rs
New GenericApplyExpr variant distinguishes non-path-base generic instantiation from path-based; formatter integrates width estimation, printing, and token-span computation; regression tests verify formatting idempotency and preservation of parenthesized-base trailing arguments.
TIR type inference and value-call semantics
baml_language/crates/baml_compiler2_tir/src/builder.rs, baml_language/crates/baml_compiler2_tir/src/generics.rs, baml_language/crates/baml_compiler2_tir/src/throws_analysis.rs
infer_generic_apply specializes GenericApply by inferring the base type, validating it is generic, checking type-arg arity, lowering type args in namespace context, binding params, enforcing BEP-044 bounds, and producing a concrete function type; CallContext gains is_value_call to restrict generic inference to inferable type vars and filter inferred bindings on call; forward-reference and throws collection traverse GenericApply bases.
MIR IR and lowering
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
New Rvalue::MakeGenericFunction and Rvalue::MakeGenericFunctionFromValue represent generic-function specialization with frame-dependent or evaluated callable operands; Constant::GenericFunction pools concrete instantiations by function and type args; lowering threads lambda_generic_params through enclosing_generic_params() for correct TypeArgRef indexing in lambda bodies; lower_generic_apply resolves bases to function-like ItemRefs and dispatches to pooled, frame-dependent, or value-based paths; pretty-printing renders all forms with type-arg lists.
MIR optimization, analysis, and pull semantics
baml_language/crates/baml_compiler2_mir/src/optimize.rs, baml_language/crates/baml_compiler2_emit/src/analysis.rs, baml_language/crates/baml_compiler2_emit/src/pull_semantics.rs, baml_language/crates/baml_compiler2_emit/src/stack_carry.rs
Copy-propagation, dead-local elimination, and debug verification handle new rvalues; MakeGenericFunction treated like LoadType (no local operands); MakeGenericFunctionFromValue recurses into its value operand; pull semantics gain make_generic_function and make_generic_function_from_value trait methods; rvalue walking loads type args via load_type then invokes sink callbacks; stack simulation pops type args and pushes generic-function or specialized-closure values, failing on underflow.
Constant pooling and emit instruction generation
baml_language/crates/baml_compiler2_emit/src/emit.rs
Constant emission interns pooled Object::GenericFunction keyed by function and concrete type args; spawn-read analysis treats MakeGenericFunction as non-reading; PullSink implementations emit Instruction::MakeGenericFunction with resolved global index and ntypeargs, or Instruction::MakeGenericFunctionFromValue with ntypeargs operand.
Bytecode instructions and object model
baml_language/crates/bex_vm_types/src/bytecode.rs, baml_language/crates/bex_vm_types/src/types.rs, baml_language/crates/bex_vm_types/src/lib.rs
Instruction::MakeGenericFunction { function: GlobalIndex, ntypeargs: u16 } and MakeGenericFunctionFromValue { ntypeargs: u16 } pop type-arg objects and produce specialized values; OpCode variants encode to 7 and 3 bytes respectively with TryFrom<u8> decoding and Display formatting; new public GenericFunction { function: GlobalIndex, type_args: Box<[Ty]> } struct holds runtime specialization; Object::GenericFunction variant with Borsh serialization/deserialization; display, and callable-type classification (ObjectType::Closure).
VM callable dispatch and execution
baml_language/crates/bex_vm/src/vm.rs, baml_language/crates/bex_vm/src/debug.rs
VM classifies GenericFunction as FUNCTION type tag; entry-point setup extracts gf.type_args and normalizes to resolved inner function with correct trampoline; call dispatch preserves GenericFunction identity while resolving inner function arity; execute_call_from_locals_offset extracts concrete type args and propagates them into native calls (temporarily replacing pending_call_type_args) and bytecode frames; opcode execution constructs GenericFunction objects from popped type-arg slots or specializes callable values into closures; debug disassembly displays instruction metadata and compact bytecode operands.
Runtime integration: GC, conversion, and serialization
baml_language/crates/bex_heap/src/gc.rs, baml_language/crates/bex_heap/src/accessor.rs, baml_language/crates/bex_heap/src/heap_debugger/real.rs, baml_language/crates/bex_vm/src/package_baml/*, baml_language/crates/bex_engine/src/conversion.rs
GC treats GenericFunction as non-traced leaf (GlobalIndex base, no HeapPtr refs); owned conversion handles it via lossy substitution or error; heap invariant checks skip heap validation; JSON serialization returns null; deep copy allocates it; deep equals performs structural comparison by function and type args; unstable formatting outputs "<generic_function>"; external conversion and union discrimination reject it; VM sys-op conversion panics on it.
Tests, compiler views, and test utilities
baml_language/crates/baml_tests/src/compiler2_tir/mod.rs, baml_language/crates/baml_tests/src/compiler2_tir/explicit_type_args.rs, baml_language/crates/baml_tests/tests/interfaces.rs, baml_language/crates/baml_tests/tests/instantiation_interning.rs, baml_language/crates/baml_tests/baml_src/ns_instantiation_expr/instantiation_expr.baml, baml_language/crates/baml_tests/baml_src/ns_instantiation_expr/ns_qualified/q.baml, baml_language/crates/tools_onionskin/src/compiler.rs
Compiler visualization renders Expr::GenericApply as base followed by <...> and Object::GenericFunction as placeholder; TIR snapshot helpers format generic-apply expressions; Phase 1 snapshot tests cover value specialization, type mismatch, arity mismatch, multiple type args, parenthesized receivers, param-dependent values, and generic lambda specialization; compile-time regression tests verify BEP-044 bound enforcement on value references; runtime tests validate execution, structural equality, reflection, JSON decoding, qualified namespaces, and local specialization; interning test verifies pool deduplication across specializations.

Sequence Diagram

sequenceDiagram
  participant Parser
  participant TypeInference
  participant MIRLower
  participant Emitter
  participant VM
  Parser->>Parser: Disambiguate foo<int> as GENERIC_ARGS
  Parser->>TypeInference: Expr::GenericApply { base, type_args }
  TypeInference->>TypeInference: Infer base type, validate generic arity
  TypeInference->>TypeInference: Bind type args to params, enforce bounds
  TypeInference->>MIRLower: Concrete Ty::Function with bindings
  MIRLower->>MIRLower: Resolve base to ItemRef
  alt All type args concrete
    MIRLower->>Emitter: Constant::GenericFunction
  else Any type arg frame-dependent
    MIRLower->>Emitter: Rvalue::MakeGenericFunction
  else Cannot resolve to ItemRef
    MIRLower->>Emitter: Rvalue::MakeGenericFunctionFromValue
  end
  Emitter->>VM: Instruction::MakeGenericFunction or MakeGenericFunctionFromValue
  VM->>VM: Pop ntypeargs Type objects
  VM->>VM: Construct Object::GenericFunction
  VM->>VM: Push specialized value to stack
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • BoundaryML/baml#3460: Related generic/type-argument plumbing and runtime type reflection work that intersects with TyTemplate/LoadType/GenericFunction mechanisms.

Poem

🐰 I found a hat of types to wear,

I hopped through parser, VM, and where
foo became a pooled delight,
Stack and heap kept it cozy and tight,
Now generics gleam in morning hare.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: generic instantiation expressions (let f = foo<int>)' clearly and specifically summarizes the main feature being added—support for using generic functions as values with explicit type arguments.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

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.

@vercel
vercel Bot temporarily deployed to Preview – beps June 2, 2026 17:18 Inactive
Cover `name.space.fun<string>` — a generic function in a nested (multi-segment)
namespace, referenced by its fully-qualified name as a value. Adds a nested
`ns_qualified` namespace under the instantiation-expr fixture and tests both the
concrete-pooled reflect-through-value path and a plain identity through the
qualified value. Confirms the multi-segment path resolves to the right ItemRef.
@vercel
vercel Bot temporarily deployed to Preview – beps June 2, 2026 17:36 Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

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)

1696-1705: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix Borsh wire-format compatibility: append ObjectWire::GenericFunction
borsh enums use the variant ordinal (declaration order) as the tag when explicit discriminants aren’t provided (default behavior). Since ObjectWire::GenericFunction is currently inserted before Cell, it renumbers every later ObjectWire variant, breaking backward compatibility for previously serialized payloads.

Suggested fix
 enum ObjectWire {
     Function(Box<Function>),
     Class(Box<Class>),
     Instance(Instance),
     Enum(Box<Enum>),
     Variant(Variant),
     Closure(Closure),
     BoundMethod(BoundMethod),
-    GenericFunction(GenericFunction),
     Cell(Cell),
     String(String),
     // `Arc<BigInt>` isn't directly Borsh-derivable (and we want the same
     // wire form `ConstValue::Bigint` uses), so the proxy holds the inner
     // `BigInt` by value. `num_bigint::BigInt` has no native borsh impl, so
@@
     Float(f64),
     Future(Future),
     UnscheduledFuture(UnscheduledFuture),
     Type(Box<baml_type::Ty>),
+    GenericFunction(GenericFunction),
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/bex_vm_types/src/types.rs` around lines 1696 - 1705, The
ObjectWire enum changed variant ordering by placing GenericFunction before Cell,
which shifts discriminant ordinals and breaks borsh compatibility; update the
enum declaration (ObjectWire) so that the GenericFunction variant is appended
after Cell (i.e., reorder/remove the current GenericFunction entry and add
GenericFunction at the end of the variant list so all existing subsequent
variants keep their original ordinals).
🧹 Nitpick comments (6)
baml_language/crates/baml_compiler2_mir/src/optimize.rs (2)

1356-1358: 💤 Low value

Consider updating comment to cover both variants.

The comment specifically mentions LoadType but the match arm now handles both LoadType and MakeGenericFunction. Consider a more generic comment like "No local operands — nothing to check."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/baml_compiler2_mir/src/optimize.rs` around lines 1356 -
1358, The comment in the match arm for crate::Rvalue::LoadType(_) |
crate::Rvalue::MakeGenericFunction { .. } only mentions LoadType even though
both variants are handled; update the comment to reflect both patterns (e.g.,
"No local operands — nothing to check.") next to that match arm so it accurately
documents that neither LoadType nor MakeGenericFunction require local-operand
checks.

392-394: 💤 Low value

Consider updating comment to cover both variants.

The comment mentions only LoadType but the match arm now handles both LoadType and MakeGenericFunction. Consider a more generic comment like "No local operands to scan."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/baml_compiler2_mir/src/optimize.rs` around lines 392 -
394, The comment for the match arm handling crate::Rvalue::LoadType(_) |
crate::Rvalue::MakeGenericFunction { .. } is inaccurate — it only mentions
LoadType; change it to a generic note such as "No local operands to scan" (or
similar) so it correctly describes both Rvalue::LoadType and
Rvalue::MakeGenericFunction; update the comment adjacent to that match arm in
optimize.rs accordingly.
baml_language/crates/bex_vm_types/src/bytecode.rs (1)

2393-2618: 💤 Low value

Consider adding a unit test for MakeGenericFunction encoding.

The compact_tests module thoroughly tests other instruction encodings but lacks coverage for the new MakeGenericFunction opcode. A test would verify the 7-byte encoding (opcode + u32 function + u16 ntypeargs) and round-trip consistency.

🧪 Suggested test
#[test]
fn encode_make_generic_function() {
    let bc = make_bytecode(
        vec![Instruction::MakeGenericFunction {
            function: GlobalIndex::from_raw(42),
            ntypeargs: 3,
        }],
        vec![],
    );
    let compact = bc.lower_to_compact();
    // MakeGenericFunction = 7 bytes: opcode + u32 function + u16 ntypeargs
    assert_eq!(compact.code.len(), 7);
    assert_eq!(compact.code[0], OpCode::MakeGenericFunction as u8);
    let function = u32::from_le_bytes([
        compact.code[1],
        compact.code[2],
        compact.code[3],
        compact.code[4],
    ]);
    assert_eq!(function, 42);
    let ntypeargs = u16::from_le_bytes([compact.code[5], compact.code[6]]);
    assert_eq!(ntypeargs, 3);
}

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

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/bex_vm_types/src/bytecode.rs` around lines 2393 - 2618,
The test suite misses coverage for the new MakeGenericFunction encoding; add a
unit test in the compact_tests module using the existing helper make_bytecode
that creates an Instruction::MakeGenericFunction { function:
GlobalIndex::from_raw(42), ntypeargs: 3 }, call lower_to_compact(), and assert
the compact.code length equals 7, compact.code[0] == OpCode::MakeGenericFunction
as u8, the 4 bytes at compact.code[1..5] decode to 42 (u32 little-endian), and
the 2 bytes at compact.code[5..7] decode to 3 (u16 little-endian) to verify the
opcode + u32 function + u16 ntypeargs encoding and round-trip consistency.
baml_language/crates/tools_onionskin/src/compiler.rs (1)

5251-5251: ⚡ Quick win

Consider showing more information for GenericFunction objects.

The current display <generic_function> is less informative than similar object types. For better debugging experience in the VM Runner, consider showing at least the function index, similar to how Object::Function(f) shows <fn {}> with the function name at line 5242.

🔍 Suggested enhancement for debugging output

If GenericFunction has an accessible function index field:

-                Object::GenericFunction(_) => "<generic_function>".to_string(),
+                Object::GenericFunction(gf) => format!("<generic_function fn_idx={}>", gf.function_index),

Or if you want to show the function name (requires looking up in the program):

-                Object::GenericFunction(_) => "<generic_function>".to_string(),
+                Object::GenericFunction(gf) => {
+                    // Look up function name from program if available
+                    format!("<generic_function {}>", /* function name from gf.function_index */)
+                }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/tools_onionskin/src/compiler.rs` at line 5251, Replace
the terse "<generic_function>" display for Object::GenericFunction(_) with a
more informative string like the Object::Function(f) arm: extract the function
index/name from the GenericFunction variant (e.g., use its tuple/field (like .0
or .index) or look up the function name in the program's function table) and
format it as "<generic_fn {index}>" or "<generic_fn {name}>" for debugging; if
no index/name is available, include at least the raw variant payload in the
formatted string to avoid the uninformative constant.
baml_language/crates/bex_heap/src/gc.rs (1)

396-405: ⚡ Quick win

Add a unit test that roots Object::GenericFunction through GC.

This PR updates three GC leaf-match sites for the new object kind, but there is no direct test here proving a rooted GenericFunction survives collection unchanged and contributes no outgoing references. A small unit test would pin that contract and make the next missed arm obvious. Please also run cargo test --lib before merge. As per coding guidelines, "Prefer writing Rust unit tests over integration tests where possible" and "Always run cargo test --lib if you changed any Rust code".

Also applies to: 545-546, 806-806

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/bex_heap/src/gc.rs` around lines 396 - 405, Add a unit
test that constructs and roots an Object::GenericFunction, runs a GC collection,
and asserts the GenericFunction instance survives and that no outgoing
references are reported; specifically create a GenericFunction value (using the
same constructor/allocator used elsewhere in tests), root it via the
heap/rooting APIs used in gc.rs, call the GC entrypoint (e.g. the
collect/mark-sweep function exercised by other tests), then assert the root
still points to the same GenericFunction and that tracing/edge-count utilities
show no outgoing pointers; add the test near other gc tests in the same crate,
include cases covering the other mentioned sites (lines ~545 and ~806) if
applicable, and run cargo test --lib before committing.
baml_language/crates/baml_compiler_parser/src/parser.rs (1)

8472-8562: ⚡ Quick win

Add explicit newline-focused disambiguation tests.

The new logic is newline-sensitive, but the added cases don’t directly lock newline behavior (for example: foo<int>\n.bar, foo<int>\n;, and comparison-style multiline forms). Adding those would harden this path against regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/baml_compiler_parser/src/parser.rs` around lines 8472 -
8562, The new parser logic is sensitive to newlines but the tests don't cover
newline disambiguation; add explicit tests that assert behavior when a closing
`>` is followed by a newline then a token—specifically add cases like
`foo<int>\n.bar` (should still produce GENERIC_ARGS, update or add a test near
bare_generic_instantiation_followed_by_member_access), `foo<int>\n;` (should
produce GENERIC_ARGS, add near bare_generic_instantiation_in_value_position),
and multiline comparison forms like `a < b\n> c` or `a < b\n` (should NOT
produce GENERIC_ARGS, add near
comparison_chain_is_not_generic_args/simple_comparison_is_not_generic_args); in
each new test call parse_source, assert_no_errors, and assert
has_generic_args(...) or !has_generic_args(...) as appropriate to lock
newline-sensitive behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@baml_language/crates/baml_compiler2_ast/src/ast.rs`:
- Around line 623-632: Add a match arm in ExprBody::display_expr_inner to handle
the Expr::GenericApply variant so diagnostics print the actual `foo<int>` shape
instead of `"..."`; when matching GenericApply { base, type_args } call the
existing expression-display routine for the base (the same helper used for other
Expr variants) and render the type_args by mapping each TypeExpr through the
existing type-expression display helper and joining with ", ", then format the
final string as "{base}<arg1, arg2, ...>" (ensuring empty type_args cannot occur
per invariant).

In `@baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs`:
- Around line 2029-2039: The code marks only the call-site GENERIC_ARGS node
(callee_generic_args) as consumed, but when lowering chains like
Container<int>.method<U>(x) the receiver's <int> remains unconsumed and can be
wrapped into Expr::GenericApply; update lowering to detect and also
mark/suppress receiver generic-args that are shadowed by a subsequent
method-level generic args before calling lower_expr_in_chain. Concretely, after
computing callee_generic_args (and before lower_expr_in_chain) use
find_callee_generic_args / lower_generic_args_node results to locate and insert
the receiver's GENERIC_ARGS text_range() into self.consumed_generic_args (or
otherwise suppress it) whenever a method-level generic arg is present so that
lower_expr_in_chain will not wrap the receiver path into Expr::GenericApply.

In `@baml_language/crates/baml_compiler2_mir/src/lower.rs`:
- Around line 5561-5650: The bug: lower_generic_apply currently erases type args
when try_resolve_generic_apply_base(base) returns None (e.g., local/captured
generic functions or bound method values), causing g<int>/obj.method<int> to
lower to an unspecialized callable; fix it by handling the "non-ItemRef" case
instead of just calling self.lower_expr(base, dest): lower the base into an
Operand (use self.lower_expr_to_operand / lower_expr into a temp/Place), then
emit a runtime specialization Rvalue that builds a specialized callable from
that value and the templates (e.g., add and use a new Rvalue variant like
MakeGenericFunctionFromValue { value: Operand, type_arg_templates: templates }
and assign it to dest inside lower_generic_apply), or alternatively make the
compiler reject such bases earlier; update lower_generic_apply,
try_resolve_generic_apply_base usage, and the builder/Rvalue enum to support the
runtime-specialize-from-value path.

In `@baml_language/crates/baml_compiler2_tir/src/builder.rs`:
- Around line 1580-1594: The current substitution creates a Ty::Function with
generic_params cleared, which turns ambient type variables from the enclosing
scope into fresh inference targets and allows calls like let f = foo<T>; f(1) to
re-infer T incorrectly; fix by preserving metadata that marks this signature as
an already-instantiated function (do not erase the fact it came from a generic
declaration) or by restricting call-site inference: either (A) carry through the
original function's generic parameter info instead of setting generic_params:
Vec::new() when building the Ty::Function in this substitution (so the
substituted signature still records which generics belong to the callee), or (B)
modify check_call_inner to only perform inference over the callee’s declared
generic parameters (the original generic_params from the function declaration)
rather than every Ty::TypeVar appearing in the substituted signature; locate the
construction in builder.rs around Ty::Function and the substitution call
crate::generics::substitute_ty, and adjust the data kept or the inference logic
in check_call_inner accordingly.
- Around line 1497-1517: The code reports TypeIsNotGeneric for generic
application even when the base already failed to type-check; update the
generic-application path in builder.rs to short-circuit when base_ty is
Ty::Unknown or Ty::Error (the result of self.infer_expr(base, body)) and simply
return that Unknown/Error instead of emitting a new
TirTypeError::TypeIsNotGeneric for the same expr_id; modify the match that
destructures base_ty (the Ty::Function else branch) to first check for
Unknown/Error and propagate it (returning the original Ty::Unknown/Ty::Error)
before reporting TypeIsNotGeneric (use Self::generic_apply_base_name for the
message as before).

In
`@baml_language/crates/baml_tests/baml_src/ns_instantiation_expr/instantiation_expr.baml`:
- Around line 21-31: The test identity_same_specialization uses
baml.deep_equals(a, b) which only checks structural equality and doesn't prove
pointer-stable interning; replace that call with an identity-sensitive check
(e.g. use a pointer-equality helper such as baml.ptr_eq(a, b) or the language's
identity operator) so the test truly asserts the two values are the same pooled
object, or alternatively move the identity check into a Rust-side harness that
inspects the runtime object identity for identity_same_specialization and update
the test same_specialization_is_identical to call that harness.

In `@baml_language/crates/bex_vm/src/vm.rs`:
- Around line 1417-1423: The entry point handling needs to normalize
GenericFunction to its specialized Function before storing/dispatching: update
set_entry_point_with_type_args (and any callers that accept a raw function
object) to resolve Object::GenericFunction -> its concrete
Function/specialization (using the same logic as get_object/set_entry_point did
for type_args) so the VM never retains a GenericFunction pointer as an entry
point; ensure push_trampoline_frame and native/sysop entry paths look up the
callee by the normalized concrete Function pointer (not the GenericFunction),
and remove the panic path by matching Object::Function after normalization.
Reference: get_object, set_entry_point_with_type_args, set_entry_point,
Object::GenericFunction, push_trampoline_frame.

---

Outside diff comments:
In `@baml_language/crates/bex_vm_types/src/types.rs`:
- Around line 1696-1705: The ObjectWire enum changed variant ordering by placing
GenericFunction before Cell, which shifts discriminant ordinals and breaks borsh
compatibility; update the enum declaration (ObjectWire) so that the
GenericFunction variant is appended after Cell (i.e., reorder/remove the current
GenericFunction entry and add GenericFunction at the end of the variant list so
all existing subsequent variants keep their original ordinals).

---

Nitpick comments:
In `@baml_language/crates/baml_compiler_parser/src/parser.rs`:
- Around line 8472-8562: The new parser logic is sensitive to newlines but the
tests don't cover newline disambiguation; add explicit tests that assert
behavior when a closing `>` is followed by a newline then a token—specifically
add cases like `foo<int>\n.bar` (should still produce GENERIC_ARGS, update or
add a test near bare_generic_instantiation_followed_by_member_access),
`foo<int>\n;` (should produce GENERIC_ARGS, add near
bare_generic_instantiation_in_value_position), and multiline comparison forms
like `a < b\n> c` or `a < b\n` (should NOT produce GENERIC_ARGS, add near
comparison_chain_is_not_generic_args/simple_comparison_is_not_generic_args); in
each new test call parse_source, assert_no_errors, and assert
has_generic_args(...) or !has_generic_args(...) as appropriate to lock
newline-sensitive behavior.

In `@baml_language/crates/baml_compiler2_mir/src/optimize.rs`:
- Around line 1356-1358: The comment in the match arm for
crate::Rvalue::LoadType(_) | crate::Rvalue::MakeGenericFunction { .. } only
mentions LoadType even though both variants are handled; update the comment to
reflect both patterns (e.g., "No local operands — nothing to check.") next to
that match arm so it accurately documents that neither LoadType nor
MakeGenericFunction require local-operand checks.
- Around line 392-394: The comment for the match arm handling
crate::Rvalue::LoadType(_) | crate::Rvalue::MakeGenericFunction { .. } is
inaccurate — it only mentions LoadType; change it to a generic note such as "No
local operands to scan" (or similar) so it correctly describes both
Rvalue::LoadType and Rvalue::MakeGenericFunction; update the comment adjacent to
that match arm in optimize.rs accordingly.

In `@baml_language/crates/bex_heap/src/gc.rs`:
- Around line 396-405: Add a unit test that constructs and roots an
Object::GenericFunction, runs a GC collection, and asserts the GenericFunction
instance survives and that no outgoing references are reported; specifically
create a GenericFunction value (using the same constructor/allocator used
elsewhere in tests), root it via the heap/rooting APIs used in gc.rs, call the
GC entrypoint (e.g. the collect/mark-sweep function exercised by other tests),
then assert the root still points to the same GenericFunction and that
tracing/edge-count utilities show no outgoing pointers; add the test near other
gc tests in the same crate, include cases covering the other mentioned sites
(lines ~545 and ~806) if applicable, and run cargo test --lib before committing.

In `@baml_language/crates/bex_vm_types/src/bytecode.rs`:
- Around line 2393-2618: The test suite misses coverage for the new
MakeGenericFunction encoding; add a unit test in the compact_tests module using
the existing helper make_bytecode that creates an
Instruction::MakeGenericFunction { function: GlobalIndex::from_raw(42),
ntypeargs: 3 }, call lower_to_compact(), and assert the compact.code length
equals 7, compact.code[0] == OpCode::MakeGenericFunction as u8, the 4 bytes at
compact.code[1..5] decode to 42 (u32 little-endian), and the 2 bytes at
compact.code[5..7] decode to 3 (u16 little-endian) to verify the opcode + u32
function + u16 ntypeargs encoding and round-trip consistency.

In `@baml_language/crates/tools_onionskin/src/compiler.rs`:
- Line 5251: Replace the terse "<generic_function>" display for
Object::GenericFunction(_) with a more informative string like the
Object::Function(f) arm: extract the function index/name from the
GenericFunction variant (e.g., use its tuple/field (like .0 or .index) or look
up the function name in the program's function table) and format it as
"<generic_fn {index}>" or "<generic_fn {name}>" for debugging; if no index/name
is available, include at least the raw variant payload in the formatted string
to avoid the uninformative constant.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 99d69b1e-0e29-4ec9-8b9b-af40a0c0409f

📥 Commits

Reviewing files that changed from the base of the PR and between 3206605 and 7f3542b.

⛔ Files ignored due to path filters (8)
  • baml_language/crates/baml_tests/snapshots/baml_src/_root.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/baml_src/instantiation_expr.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded_unoptimized.snap is excluded by !**/*.snap
📒 Files selected for processing (31)
  • baml_language/crates/baml_compiler2_ast/src/ast.rs
  • baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs
  • 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/pull_semantics.rs
  • baml_language/crates/baml_compiler2_emit/src/stack_carry.rs
  • baml_language/crates/baml_compiler2_hir/src/builder.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/optimize.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/throws_analysis.rs
  • baml_language/crates/baml_compiler_parser/src/parser.rs
  • baml_language/crates/baml_tests/baml_src/ns_instantiation_expr/instantiation_expr.baml
  • baml_language/crates/baml_tests/src/compiler2_tir/explicit_type_args.rs
  • baml_language/crates/baml_tests/src/compiler2_tir/mod.rs
  • baml_language/crates/baml_tests/tests/interfaces.rs
  • baml_language/crates/bex_engine/src/conversion.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/json.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/lib.rs
  • baml_language/crates/bex_vm_types/src/types.rs
  • baml_language/crates/tools_onionskin/src/compiler.rs

Comment thread baml_language/crates/bex_vm/src/vm.rs Resolved
@github-actions

github-actions Bot commented Jun 2, 2026 •

Copy link
Copy Markdown

Binary size checks passed

✅ 7 passed

Artifact Platform File Gzip Gated on Baseline Delta Status
✅ baml-cli Linux 🔒 17.7 MB 7.6 MB file 24.1 MB -6.4 MB (-26.4%) OK
✅ packed-program Linux 🔒 12.9 MB 5.5 MB file 15.1 MB -2.2 MB (-14.6%) OK
✅ baml-cli macOS 🔒 13.5 MB 6.6 MB file 18.2 MB -4.7 MB (-25.6%) OK
✅ packed-program macOS 🔒 9.8 MB 4.8 MB file 11.5 MB -1.7 MB (-14.4%) OK
✅ baml-cli Windows 🔒 14.6 MB 6.7 MB file 19.6 MB -5.1 MB (-25.8%) OK
✅ packed-program Windows 🔒 10.4 MB 4.8 MB file 12.2 MB -1.8 MB (-15.0%) OK
✅ bridge_wasm WASM 11.9 MB 🔒 3.4 MB gzip 3.9 MB -504.8 KB (-13.0%) OK

🔒 = the size this artifact is GATED on (ceiling + delta). Binaries gate on file size (installed binary); WASM gates on gzip (download size). The other size is shown for information only.


Generated by cargo size-gate · workflow run

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 2, 2026 17:54 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 2, 2026 17:55 Inactive
- ast.rs: render Expr::GenericApply in display_expr_inner (was "...") so
  diagnostics show the actual foo<int> shape.
- lower_expr_body.rs: a call now marks ALL GENERIC_ARGS in the callee subtree
  as consumed, not just the one find_callee_generic_args returns. Fixes
  Container<int>.method<U>(x) wrongly wrapping the receiver <int> into an
  Expr::GenericApply (which surfaced as a spurious "not generic" error);
  restores the pre-existing class-type-arg behavior.
- builder.rs: infer_generic_apply short-circuits on an Unknown/Error base
  instead of cascading a second TypeIsNotGeneric diagnostic.
- vm.rs: normalize a GenericFunction entry point to its concrete inner
  function + stored type args before dispatch, so the native/sysop trampoline
  and bytecode frame never carry a GenericFunction pointer.
@vercel
vercel Bot temporarily deployed to Preview – beps June 2, 2026 20:14 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 2, 2026 20:32 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 2, 2026 20:36 Inactive
Addresses two more CodeRabbit comments on generic instantiation exprs:

#3 (lower.rs): a `GenericApply` whose base is not a compile-time function
reference (a local/captured generic function value, e.g. `let g = foo;
let h = g<int>`) previously fell back to lowering the base with its type
args ERASED — reintroducing the type/runtime mismatch the feature removes.
Now the runtime value is specialized: a new `Rvalue::MakeGenericFunctionFromValue`
+ `MakeGenericFunctionFromValue` opcode wraps the evaluated callable in a
Closure carrying the (frame-resolved) type args, reusing the closure call
path that already seeds `frame.type_args`. So `g<int>` runs end-to-end and a
type-reifying body (`reflect.type_of<T>`) resolves T correctly, including the
param-dependent `g<T>` case. No new GC-traced object: the only reachable
non-ItemRef base is a local holding a bare function (static methods take the
pooled ItemRef path, lambdas/GenericFunction bases are rejected by TIR).

#6 (instantiation_interning.rs): `baml.deep_equals` is structural, so the
fixture couldn't prove the pooled, pointer-stable identity (`foo<int> ===
foo<int>`); BAML has no in-language identity operator. Added a Rust harness
that inspects the compiled object pool: two `identity<Marker>` references
intern to one `Object::GenericFunction`, distinct from `identity<Other>`.

Full workspace suite green (5006 passed).
@vercel
vercel Bot temporarily deployed to Preview – beps June 2, 2026 21:37 Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
baml_language/crates/baml_tests/tests/instantiation_interning.rs (1)

40-89: 💤 Low value

Consider placing this as a unit test rather than an integration test.

baml_tests::engine::compile_source is a public helper, so this coverage could live as a #[cfg(test)] module inside the baml_tests crate src/ rather than under tests/. As per coding guidelines: "Prefer writing Rust unit tests over integration tests where possible".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/baml_tests/tests/instantiation_interning.rs` around
lines 40 - 89, The test concrete_instantiation_values_are_interned_and_distinct
should be converted from an integration test into a unit test inside the
baml_tests crate by moving its body into a #[cfg(test)] mod in the crate's src
(e.g., create a tests module in src/lib.rs or src/main.rs), keep the #[test] on
concrete_instantiation_values_are_interned_and_distinct and continue to call
compile_source and generic_function_indices_mentioning as before, ensuring any
helpers used are public (compile_source is already public) or made accessible to
the crate test module; remove the file under tests/ and run cargo test to verify
visibility and compilation.
baml_language/crates/baml_compiler2_emit/src/stack_carry.rs (1)

1283-1291: ⚡ Quick win

Add a unit test for the new stack effect.

make_generic_function_from_value is now part of the simulator contract, but the test module does not pin the ntypeargs + callable -> 1 result behavior yet. A small unit test here would catch emitter/simulator drift early. Please also run cargo test --lib locally after adding it.

Suggested unit test
+    #[test]
+    fn make_generic_function_from_value_stack_effect_pops_callable_and_type_args() {
+        let mut sim = StackCarrySim {
+            depth: Some(3),
+            used: false,
+        };
+        let classifications = HashMap::new();
+        let def_use = HashMap::new();
+        let mut sink = StackCarryPullSink {
+            sim: &mut sim,
+            carried_local: Local(0),
+            classifications: &classifications,
+            def_use: &def_use,
+        };
+
+        sink.make_generic_function_from_value(2).unwrap();
+
+        assert_eq!(sim.depth, Some(1));
+    }

As per coding guidelines, **/*.rs: Prefer writing Rust unit tests over integration tests where possible; Always run cargo test --lib if you changed any Rust code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/baml_compiler2_emit/src/stack_carry.rs` around lines
1283 - 1291, Add a unit test that exercises make_generic_function_from_value to
assert the simulator stack effect: given a simulated stack with a callable plus
N type-arg values, calling make_generic_function_from_value(ntypeargs) consumes
ntypeargs+1 items (verify sim.pop_n was successful or stack shrunk) and results
in a single pushed result (verify stack size increased by 1 after push or top is
present); place the test in the same Rust module's #[cfg(test)] mod tests so it
is a unit test, invoke make_generic_function_from_value with a few ntypeargs
values (e.g., 0 and >0) and run cargo test --lib to confirm it passes.
baml_language/crates/bex_vm_types/src/bytecode.rs (1)

698-709: ⚡ Quick win

Add a focused compact-opcode test for MakeGenericFunctionFromValue.

This opcode now threads through size calculation, decoding, display, and byte emission, but compact_tests still doesn't lock that round-trip down. One unit test here would make future opcode-table edits much safer. Please also run cargo test --lib locally after adding it.

Suggested unit test
+    #[test]
+    fn encode_make_generic_function_from_value() {
+        let bc = make_bytecode(
+            vec![Instruction::MakeGenericFunctionFromValue { ntypeargs: 2 }],
+            vec![],
+        );
+        let compact = bc.lower_to_compact();
+
+        assert_eq!(compact.code.len(), 3);
+        assert_eq!(compact.code[0], OpCode::MakeGenericFunctionFromValue as u8);
+        assert_eq!(u16::from_le_bytes([compact.code[1], compact.code[2]]), 2);
+        assert_eq!(
+            OpCode::try_from(compact.code[0]),
+            Ok(OpCode::MakeGenericFunctionFromValue)
+        );
+        assert_eq!(
+            Instruction::MakeGenericFunctionFromValue { ntypeargs: 2 }.to_string(),
+            "MAKE_GENERIC_FUNCTION_FROM_VALUE ntypeargs=2"
+        );
+    }

As per coding guidelines, **/*.rs: Prefer writing Rust unit tests over integration tests where possible; Always run cargo test --lib if you changed any Rust code.

Also applies to: 915-916, 1039-1040, 1174-1176, 1300-1300, 1556-1558, 2094-2097, 2410-2412

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/bex_vm_types/src/bytecode.rs` around lines 698 - 709,
Add a focused unit test that covers the compact-opcode round-trip for the
MakeGenericFunctionFromValue opcode: construct an instruction with a
representative ntypeargs (e.g. 0 and a non-zero like 3), assert its
size/encoding, run it through the existing encode/decode/display/emit helpers
used by compact_tests to ensure the decoded opcode equals the original and that
display/byte-emission are stable; place the test as a Rust #[cfg(test)] unit
test near the other compact_tests in the same module (bex_vm_types::bytecode)
and run cargo test --lib to verify.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@baml_language/crates/bex_vm/src/vm.rs`:
- Around line 5088-5108: When specializing a closure value, preserve any
existing captured_type_args from the original Closure (the c.captured_type_args
when matching Object::Closure(c)) instead of discarding them; create a combined
vector by taking c.captured_type_args, converting to a Vec, appending the
incoming type_args in the same order used by the normal call path, then set
Closure { ..., captured_type_args: combined.into_boxed_slice() } so the new
closure wrapper retains outer/class generic context.

---

Nitpick comments:
In `@baml_language/crates/baml_compiler2_emit/src/stack_carry.rs`:
- Around line 1283-1291: Add a unit test that exercises
make_generic_function_from_value to assert the simulator stack effect: given a
simulated stack with a callable plus N type-arg values, calling
make_generic_function_from_value(ntypeargs) consumes ntypeargs+1 items (verify
sim.pop_n was successful or stack shrunk) and results in a single pushed result
(verify stack size increased by 1 after push or top is present); place the test
in the same Rust module's #[cfg(test)] mod tests so it is a unit test, invoke
make_generic_function_from_value with a few ntypeargs values (e.g., 0 and >0)
and run cargo test --lib to confirm it passes.

In `@baml_language/crates/baml_tests/tests/instantiation_interning.rs`:
- Around line 40-89: The test
concrete_instantiation_values_are_interned_and_distinct should be converted from
an integration test into a unit test inside the baml_tests crate by moving its
body into a #[cfg(test)] mod in the crate's src (e.g., create a tests module in
src/lib.rs or src/main.rs), keep the #[test] on
concrete_instantiation_values_are_interned_and_distinct and continue to call
compile_source and generic_function_indices_mentioning as before, ensuring any
helpers used are public (compile_source is already public) or made accessible to
the crate test module; remove the file under tests/ and run cargo test to verify
visibility and compilation.

In `@baml_language/crates/bex_vm_types/src/bytecode.rs`:
- Around line 698-709: Add a focused unit test that covers the compact-opcode
round-trip for the MakeGenericFunctionFromValue opcode: construct an instruction
with a representative ntypeargs (e.g. 0 and a non-zero like 3), assert its
size/encoding, run it through the existing encode/decode/display/emit helpers
used by compact_tests to ensure the decoded opcode equals the original and that
display/byte-emission are stable; place the test as a Rust #[cfg(test)] unit
test near the other compact_tests in the same module (bex_vm_types::bytecode)
and run cargo test --lib to verify.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 75fd2cf5-886d-4efb-8d8b-99f0d3b4a3e1

📥 Commits

Reviewing files that changed from the base of the PR and between 00f7d7b and 34a4132.

⛔ Files ignored due to path filters (1)
  • baml_language/crates/baml_tests/snapshots/baml_src/instantiation_expr.snap is excluded by !**/*.snap
📒 Files selected for processing (13)
  • 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/pull_semantics.rs
  • baml_language/crates/baml_compiler2_emit/src/stack_carry.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/optimize.rs
  • baml_language/crates/baml_compiler2_mir/src/pretty.rs
  • baml_language/crates/baml_tests/baml_src/ns_instantiation_expr/instantiation_expr.baml
  • baml_language/crates/baml_tests/tests/instantiation_interning.rs
  • baml_language/crates/bex_vm/src/debug.rs
  • baml_language/crates/bex_vm/src/vm.rs
  • baml_language/crates/bex_vm_types/src/bytecode.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • baml_language/crates/bex_vm/src/debug.rs
  • baml_language/crates/baml_compiler2_emit/src/analysis.rs
  • baml_language/crates/baml_compiler2_mir/src/pretty.rs
  • baml_language/crates/baml_compiler2_mir/src/lower.rs
  • baml_language/crates/baml_compiler2_emit/src/emit.rs

Comment thread baml_language/crates/bex_vm/src/vm.rs Resolved
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 2, 2026 21:55 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 2, 2026 21:55 Inactive
…#5)

A param-dependent instantiation value (`let f = foo<T>; f(1)` inside a
generic body) has type `(T) -> T` where `T` is the enclosing scope's rigid
type param. Calling it with a mismatched concrete argument was silently
accepted: the call checker treated the `T` in the value's signature as an
inference variable and bound `T = int`, collapsing `foo<T>` to `foo<int>`.

Fix: when the callee is a function *value* (a bare local/param holding a
function, not a direct reference to a function/method declaration), restrict
call-site inference to the value's own inferable type vars — its declared
`generic_params` plus synthetic effect-polymorphism params — and treat
everything else (rigid ambient vars) as opaque, matched structurally. So
`f(1)` now errors (`int` is not a subtype of a rigid `T`), while:
  - valid forwarding `f(y)` with `y : T` still type-checks,
  - an uninstantiated value `let g = foo; g(5)` still infers `T = int`,
  - higher-order effect inference (`let run = fwd; run(cb)`) still binds the
    callback's `throws` effect param.

This is gated on value calls specifically: a *declaration* callee
(`arr.map`, `wrap(42)`, `StreamCache.new`) reaches the checker with its
`generic_params` cleared by receiver/class substitution yet its own params
still inferable, so the same restriction would wrongly freeze them — those
keep the existing behavior untouched.

Adds TIR regression tests pinning both the rejection and the preserved
inference. Full workspace suite green (5008 passed).
@vercel
vercel Bot temporarily deployed to Preview – beps June 2, 2026 22:49 Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@baml_language/crates/baml_compiler2_tir/src/generics.rs`:
- Around line 419-454: contains_inferable_typevar is missing a Ty::Future arm so
type variables inside Future<value, error, ...> are ignored; add a
Ty::Future(value, error, ..) match arm that recurses into the value and error
types (calling contains_inferable_typevar on both) and returns true if either
contains an inferable typevar; also update the sibling contains_typevar
analogously so Future-wrapped typevars are consistently visited during
still_a_hole/value-call validation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 10101439-e0a3-42c8-95d2-cde891f578e1

📥 Commits

Reviewing files that changed from the base of the PR and between 34a4132 and bb76d78.

📒 Files selected for processing (3)
  • baml_language/crates/baml_compiler2_tir/src/builder.rs
  • baml_language/crates/baml_compiler2_tir/src/generics.rs
  • baml_language/crates/baml_tests/src/compiler2_tir/explicit_type_args.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • baml_language/crates/baml_compiler2_tir/src/builder.rs

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 2, 2026 23:07 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 2, 2026 23:07 Inactive
… (PR review)

Two follow-ups from CodeRabbit on the #5 / #3 fixes:

- generics.rs: `contains_typevar` and `contains_inferable_typevar` had no
  `Ty::Future(value, error, _)` arm, so type vars nested under `Future<...>`
  were invisible — inconsistent with `substitute_ty`, which already recurses
  into both. On a value call that made a `Future`-wrapped type var look rigid;
  add the arm to both walkers.

- vm.rs (MakeGenericFunctionFromValue): when specializing a callable that is
  already a Closure, carry over its existing `captured_type_args` (the
  outer/class generic environment) and append the new instantiation args in
  call order, instead of dropping them — otherwise a later indirect call would
  seed an incomplete `frame.type_args`.

Full workspace suite green (5008 passed).
@vercel
vercel Bot temporarily deployed to Preview – beps June 2, 2026 23:33 Inactive
@vercel
vercel Bot temporarily deployed to Preview – beps June 3, 2026 20:37 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 3, 2026 20:43 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 3, 2026 20:55 Inactive
`baml_fmt` errored on `(expr)<args>` — e.g. an inline generic-lambda
instantiation `(<T>(x: T) -> T { x })<int>` or `(foo)<int>` — failing the
whole file with `UnexpectedAdditionalElement`. The parser wraps any postfix
`<...>` in a `PATH_EXPR`, but the formatter's `PathExpr` only models a
word/dotted-path base, so a parenthesized (or otherwise non-path) base hit the
catch-all error arm.

Add an `Expression::GenericApply { base, generic_args }` variant for that case.
`Expression::from_cst` routes a `PATH_EXPR` to `PathExpr` when its first child
is a word/path and to `GenericApply` otherwise; printing emits the base
followed by the generic args. Path/qualified-path bases (`foo<int>`,
`a.b.foo<int>`) are unchanged.

All forms format and are idempotent (test added). baml_fmt suite + stdlib
formatter stage green.
@vercel
vercel Bot temporarily deployed to Preview – beps June 3, 2026 21:31 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 3, 2026 21:36 Inactive
@vercel
vercel Bot temporarily deployed to Preview – beps June 3, 2026 21:46 Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@baml_language/crates/baml_fmt/tests/generic_instantiation.rs`:
- Around line 1-42: The tests in generic_instantiation.rs are integration tests
(functions generic_instantiation_forms_format_and_are_idempotent and
paren_base_keeps_generic_args) and must be run with cargo test --test
generic_instantiation rather than cargo test --lib; update any test invocation
(CI config, README, or scripts) that calls cargo test --lib to use cargo test
--test generic_instantiation (or simply cargo test) so these integration tests
are executed correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: bdc7256f-8baf-4aae-94e9-3f689db1480d

📥 Commits

Reviewing files that changed from the base of the PR and between ab7e5bc and 4bcdbdc.

📒 Files selected for processing (2)
  • baml_language/crates/baml_fmt/src/ast/expressions.rs
  • baml_language/crates/baml_fmt/tests/generic_instantiation.rs

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 3, 2026 21:50 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 3, 2026 22:03 Inactive
CodeRabbit verified with `cargo test --lib`, which skips `tests/` integration
tests. baml_fmt's tests are conventionally inline unit tests in lib.rs; move the
generic-instantiation formatting tests into the existing `lambda_format_tests`
module so they run under `cargo test --lib` (and nextest). No behavior change.
@vercel
vercel Bot temporarily deployed to Preview – beps June 3, 2026 22:14 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 3, 2026 22:19 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 3, 2026 22:32 Inactive
…-expr

# Conflicts:
#	baml_language/crates/baml_compiler2_tir/src/builder.rs
#	baml_language/crates/baml_compiler2_tir/src/generics.rs
@vercel
vercel Bot temporarily deployed to Preview – beps June 4, 2026 00:19 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 4, 2026 00:24 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 4, 2026 00:36 Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

497-556: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Descend into Ty::Future in contains_typevar_where.

contains_typevar_where is now the value-call deferral/check gate, but unlike contains_typevar it still treats Future<value, error> as a leaf. That makes builder.rs Line 2893 miss rigid or still-uninferred vars hidden under either branch and can wrongly treat Future<T> as fully determined.

Suggested fix
 pub fn contains_typevar_where(ty: &Ty, pred: &dyn Fn(&Name) -> bool) -> bool {
     match ty {
         Ty::TypeVar(name, _) => pred(name),
         Ty::List(inner, _) | Ty::Optional(inner, _) | Ty::EvolvingList(inner, _) => {
             contains_typevar_where(inner, pred)
         }
         Ty::Map(k, v, _) | Ty::EvolvingMap(k, v, _) => {
             contains_typevar_where(k, pred) || contains_typevar_where(v, pred)
         }
         Ty::Union(tys, _) => tys.iter().any(|t| contains_typevar_where(t, pred)),
+        Ty::Future(value, error, _) => {
+            contains_typevar_where(value, pred) || contains_typevar_where(error, pred)
+        }
         Ty::Function {
             generic_params,
             generic_param_bounds,
             params,
             ret,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/baml_compiler2_tir/src/generics.rs` around lines 497 -
556, contains_typevar_where currently treats Ty::Future as a leaf so it misses
typevars hidden in the future's value or error branches; update the match in
contains_typevar_where to handle Ty::Future(value, error, _) (or whatever tuple
form it uses) and recurse into both contained types (i.e., call
contains_typevar_where on the value and on the error) so rigid/uninferred vars
inside Future<value, error> are detected just like for
List/Map/Function/AssociatedTypeProjection.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@baml_language/crates/baml_compiler2_tir/src/generics.rs`:
- Around line 497-556: contains_typevar_where currently treats Ty::Future as a
leaf so it misses typevars hidden in the future's value or error branches;
update the match in contains_typevar_where to handle Ty::Future(value, error, _)
(or whatever tuple form it uses) and recurse into both contained types (i.e.,
call contains_typevar_where on the value and on the error) so rigid/uninferred
vars inside Future<value, error> are detected just like for
List/Map/Function/AssociatedTypeProjection.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0458bdfb-acb4-4ab6-a8f6-f733c90bde86

📥 Commits

Reviewing files that changed from the base of the PR and between fd298d0 and 19deac5.

⛔ Files ignored due to path filters (2)
  • baml_language/crates/baml_tests/snapshots/baml_src/_root.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_5_mir.snap is excluded by !**/*.snap
📒 Files selected for processing (5)
  • 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/generics.rs
  • baml_language/crates/baml_fmt/src/lib.rs
  • baml_language/crates/baml_tests/tests/interfaces.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • baml_language/crates/baml_tests/tests/interfaces.rs
  • baml_language/crates/baml_compiler2_mir/src/lower.rs
  • baml_language/crates/baml_compiler2_tir/src/builder.rs

@codeshaunted
codeshaunted added this pull request to the merge queue Jun 4, 2026
Merged via the queue into canary with commit cd48746 Jun 4, 2026
51 checks passed
@codeshaunted
codeshaunted deleted the avery/instantiation-expr branch June 4, 2026 01:31

This branch was previously deployed

3 inactive deployments
Preview – promptfiddle — 19deac5c Deployed Jun 4, 2026 by vercel[bot]
Preview – promptfiddle2 — 19deac5c Deployed Jun 4, 2026 by vercel[bot]
Preview – beps — 19deac5c Deployed Jun 4, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant