Skip to content

fix: thread inferred generic type args through interface dispatch - #3667

Merged
hellovai merged 1 commit into
canaryfrom
hellovai/fix-generic-typearg-dispatch
Jun 3, 2026
Merged

hellovai merged 1 commit into
canaryfrom
hellovai/fix-generic-typearg-dispatch

Conversation

@hellovai

@hellovai hellovai commented Jun 3, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A generic class instance whose <T> is inferred (not written explicitly) must carry the correct runtime class_type_args, and a method dispatched through a multi-implementor interface must receive the resolved class/interface type args in its frame.

The interface-dispatch switch (emit_method_candidate_switch) called the matched implementor's method with only the explicit call-site <...> type args — it never seeded the callee frame. So an interface default method (e.g. a lazy iterator's map building Map<T, U>), or any dispatched method that reads its enclosing T at runtime (reflect.type_of<T>(), constructing Other<T>{}), saw an empty frame.type_args. It then built instances with class_type_args = [unknown], and the downstream IsType dispatch guard fell through to the wrong implementor — a silent wrong answer, or a field-access panic when the sibling class has a different layout.

This was the core compiler blocker for the baml.iter lazy-iterator protocol.

Minimal reproduction (before this fix)

interface Cursor<T> {
  function head(self) -> T throws unknown
  // default method builds a NEW generic from the interface's T
  function take_one(self) -> Cursor<T> throws unknown {
    return SingleCursor<T> { value: self.head() }
  }
}
class ArrayCursor<T> {
  arr T[]
  idx int
  function of(items: T[]) -> ArrayCursor<T> { return ArrayCursor<T> { arr: items, idx: 0 } }
  implements Cursor<T> { function head(self) -> T throws unknown { return self.arr[0] } }
}
class SingleCursor<T> {
  value T
  implements Cursor<T> { function head(self) -> T throws unknown { return self.value } }
}

function take_one_then_head() -> int throws unknown {
  let cursor: Cursor<int> = ArrayCursor.of([10, 20, 30]);  // <int> inferred
  let single: Cursor<int> = cursor.take_one();             // builds SingleCursor<?>
  single.head()                                            // expected 10
}

Before: take_one() (a default method) had empty frame.type_args, so SingleCursor<T> was built with class_type_args = [unknown]. single.head() then failed the SingleCursor<int> guard and dispatched to ArrayCursor.head, which indexed a non-existent arr field. After: returns 10.

Fix (in baml_compiler2_mir::lower)

  1. enclosing_generic_params() now includes the interface's generic params for interface default methods. They are lowered as standalone functions whose bodies reference the interface's T; without this, TypeVar(T) lowered to Concrete(Void) instead of TypeArgRef(N).

  2. The interface-dispatch switch seeds each candidate's callee frame (De Bruijn order: class/iface params first, then explicit call-site args):

    • a class-owned method gets the implementor's class args — statically when the guard fully pins them (common/hot path, allocation-free, unchanged bytecode), otherwise from the matched runtime instance via a BoundMethod (covers Any/partial guards, e.g. a generic class behind a non-generic interface);
    • an inherited interface default gets the resolved interface view's args (these can differ from the implementor's class args when the implements block renames/reorders params).
  3. default.<method>() (the super-like form that forwards into the interface default) seeds the default frame with the interface's type args too.

Tests

New in-BAML regression namespace crates/baml_tests/baml_src/ns_inferred_generic_type_args:

  • showcase_lazy_cursor_pipeline — the lazy-cursor example above, end-to-end.
  • inferred_static_ctor_dispatches_to_correct_implementor — inferred static ctor + non-generic interface.
  • inferred_static_ctor_dispatches_through_generic_interface — inferred static ctor + generic interface, mismatched field shapes.
  • interface_default_method_sees_interface_type_var — default method reads T via reflect.type_of<T>() at runtime.
  • class_method_builds_generic_via_class_type_var — class-owned method builds a generic from its class T.
  • default_method_forward_reads_interface_type_var — default.<method>() forwarding into a generic default.
  • generic_class_behind_nongeneric_interface_reads_type_var — Any-guard case, seeded from the runtime instance.

Full baml_tests suite green. Existing bytecode snapshots unchanged — only the new namespace's .snap changed (no existing namespace exercised this path; the BoundMethod path fires only for previously-broken Any/partial-guard generic arms).

Known limitation (separate, pre-existing — not addressed here)

A generic class whose only field is T[], built via a static constructor with inferred T, gets wrong class_type_args from the producer side (a TIR inference issue in the struct literal, independent of this dispatch fix). Adding any second field (as every real iterator has, e.g. idx: int) avoids it, so it does not affect the ArrayIterator-shaped use case. Worth a separate investigation.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed generic type-argument propagation for interface dispatch so dispatched calls—including default-method forwards and class-owned implementations—receive correct runtime type arguments.
  • Tests

    • Added end-to-end tests validating inferred generic type args flow through interface dispatch, default-method forwarding, class-owned vs instance-seeded cases, and regressions for varied implementor layouts.

@vercel

vercel Bot commented Jun 3, 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 3, 2026 9:37pm
promptfiddle Ready Ready Preview, Comment Jun 3, 2026 9:37pm
promptfiddle2 Ready Ready Preview, Comment Jun 3, 2026 9:37pm

Request Review

@vercel
vercel Bot temporarily deployed to Preview – beps June 3, 2026 18:20 Inactive
@github-actions

github-actions Bot commented Jun 3, 2026 •

Copy link
Copy Markdown

⏭️ Performance benchmarks were skipped

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

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

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

@coderabbitai

coderabbitai Bot commented Jun 3, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2341600f-9bef-4604-85fe-c8d6876de96f

📥 Commits

Reviewing files that changed from the base of the PR and between 5f85c2d and 2a53895.

⛔ 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/baml_src/inferred_generic_type_args.snap is excluded by !**/*.snap
📒 Files selected for processing (2)
  • baml_language/crates/baml_compiler2_mir/src/lower.rs
  • baml_language/crates/baml_tests/baml_src/ns_inferred_generic_type_args/inferred_generic_type_args.baml
🚧 Files skipped from review as they are similar to previous changes (2)
  • baml_language/crates/baml_tests/baml_src/ns_inferred_generic_type_args/inferred_generic_type_args.baml
  • baml_language/crates/baml_compiler2_mir/src/lower.rs

📝 Walkthrough

Walkthrough

Seeds callee frames' generic type arguments per interface dispatch candidate by adding CalleeFrameSeed, threading frame_seed through candidate generation, emitting per-arm LoadType temps when needed, and updating dispatch and default-method lowering. Adds end-to-end tests exercising inferred/generic dispatch and default forwarding.

Changes

Interface Dispatch Frame Type Arguments

Layer / File(s) Summary
Core contracts and generic parameter helpers
baml_language/crates/baml_compiler2_mir/src/lower.rs
Introduces CalleeFrameSeed and frame_seed on interface candidates, adds class_owned_frame_seed/helpers, updates enclosing_generic_params() ordering for interface defaults, and adds emit_frame_type_arg_ops() to produce LoadType operands for frame seeds.
Default-method lowering in implements
baml_language/crates/baml_compiler2_mir/src/lower.rs
Lowering of default.<method>(...) inside implements blocks now computes interface-generic type args from the implements target, emits frame LoadType temps, prepends them to the call, and sets ntypeargs to include the frame seed count.
Candidate generation and resolution
baml_language/crates/baml_compiler2_mir/src/lower.rs
Populates frame_seed across paths: class-owned overrides use class_owned_frame_seed; inherited interface defaults use matched interface-closure view args; union-direct, primitive, and out-of-body implementor candidates use empty/static seeds.
Dispatch codegen emission
baml_language/crates/baml_compiler2_mir/src/lower.rs
Reworks emit_method_candidate_switch to apply candidate.frame_seed per arm: FromReceiverInstance seeds from the receiver instance via a BoundMethod temp and leaves call-site ntypeargs unchanged; Static(tys) emits per-arm LoadType operands prepended to call_with_type_args and updates arm_ntypeargs.
Integration test suite
baml_language/crates/baml_tests/baml_src/ns_inferred_generic_type_args/inferred_generic_type_args.baml
Adds end-to-end tests for inferred generic-type-arg dispatch across Cursor, Animal/Dog, Bag variants, Named reflect checks, class-owned constructors, default-method forwarding, and a generic class behind a non-generic interface.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Switch as emit_method_candidate_switch
  participant Lowering as emit_frame_type_arg_ops
  participant VM
  participant Callee
  Caller->>Switch: invoke interface call lowering (site, explicit type args)
  Switch->>Lowering: for Static frame_seed -> emit LoadType temps (per-Tir2Ty)
  Lowering-->>Switch: return frame-type operands
  Switch->>VM: call_with_type_args(frame-type-ops + explicit-type-ops, receiver/args)
  VM->>Callee: enter callee frame with seeded frame.type_args
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • BoundaryML/baml#3638: Complements this PR by extending interface/union dispatch logic in the same MIR lowering pathways.

Poem

🐰 I hop through lowering, seeds held tight,

Per-candidate T's now land just right,
Defaults forward, class guards stand true,
Each callee frame knows which type to view,
Hooray — no more unknowns in sight!

🚥 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 accurately summarizes the main fix: threading inferred generic type arguments through interface dispatch, which is the core change across all modified files.
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 hellovai/fix-generic-typearg-dispatch

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.

@github-actions

github-actions Bot commented Jun 3, 2026 •

Copy link
Copy Markdown

Binary size checks passed

✅ 7 passed

Artifact Platform File Gzip Gated on Baseline Delta Status
✅ baml-cli Linux 🔒 17.6 MB 7.5 MB file 24.1 MB -6.4 MB (-26.8%) OK
✅ packed-program Linux 🔒 12.8 MB 5.4 MB file 15.1 MB -2.3 MB (-15.0%) OK
✅ baml-cli macOS 🔒 13.4 MB 6.5 MB file 18.2 MB -4.7 MB (-26.1%) OK
✅ packed-program macOS 🔒 9.8 MB 4.7 MB file 11.5 MB -1.7 MB (-14.9%) OK
✅ baml-cli Windows 🔒 14.5 MB 6.7 MB file 19.6 MB -5.1 MB (-26.2%) OK
✅ packed-program Windows 🔒 10.3 MB 4.8 MB file 12.2 MB -1.9 MB (-15.5%) OK
✅ bridge_wasm WASM 11.8 MB 🔒 3.3 MB gzip 3.9 MB -524.0 KB (-13.5%) 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 3, 2026 18:25 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: 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_compiler2_mir/src/lower.rs (1)

7356-7386: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't discard caller generics in blanket default candidates.

current_iface_args here can already contain the caller's generic type vars after instantiate_rule_for_requested_interface() substitutes the blanket impl params. Treating any remaining TypeVar as "unseedable" makes calls like fn f<T>(b: Box<T>) { b.describe() } through implements<U> Named<U> for Box<U> still seed no frame args, so the inherited default sees T as unknown.

🤖 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/lower.rs` around lines 7356 -
7386, The code currently drops all frame type args whenever current_iface_args
contains any TypeVar, which loses caller generics produced by
instantiate_rule_for_requested_interface(); instead, change the logic that
builds frame_type_args so it only discards type variables that originate from
the blanket impl’s own parameters (impl-level), not type variables that came
from the caller; in other words, when constructing frame_type_args for
InterfaceMethodCandidate, inspect each type argument in current_iface_args and
retain those that are caller-generic while only removing/ignoring those proven
to be impl-level (use the same origin/instantiation info produced by
instantiate_rule_for_requested_interface() or equivalent metadata rather than a
blanket baml_compiler2_tir::generics::contains_typevar check).
🤖 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_mir/src/lower.rs`:
- Around line 282-296: The helper fully_pinned_class_args currently returns an
empty Vec when the InterfaceClassGuard is Any or partially pinned, which causes
class_type_args to be dropped for generic overrides; change
fully_pinned_class_args to accept a fallback/runtime class args parameter (e.g.
runtime_class_args: Option<&[Tir2Ty]>) and when the guard is not fully pinned
return runtime_class_args cloned (when present) instead of Vec::new(); update
all call sites (including the class-owned override dispatch) to pass the
frame.type_args (or None) so concrete class_type_args are preserved for
reification.
- Around line 5549-5563: The direct call path for default.<method>() isn't
seeding the frame.type_args with the interface generic params, so generic params
from the interface become unknown at runtime; update the lowering that handles
direct default.<method>() invocations to mirror the interface-default lowering
here: when you detect a default method (via iface_data.default_methods and
func_id) seed the call frame's type args with iface_data.generic_params extended
by the function's own generic params (same ordering used by
emit_method_candidate_switch), ensuring frame.type_args is populated before
emitting the default method body.

---

Outside diff comments:
In `@baml_language/crates/baml_compiler2_mir/src/lower.rs`:
- Around line 7356-7386: The code currently drops all frame type args whenever
current_iface_args contains any TypeVar, which loses caller generics produced by
instantiate_rule_for_requested_interface(); instead, change the logic that
builds frame_type_args so it only discards type variables that originate from
the blanket impl’s own parameters (impl-level), not type variables that came
from the caller; in other words, when constructing frame_type_args for
InterfaceMethodCandidate, inspect each type argument in current_iface_args and
retain those that are caller-generic while only removing/ignoring those proven
to be impl-level (use the same origin/instantiation info produced by
instantiate_rule_for_requested_interface() or equivalent metadata rather than a
blanket baml_compiler2_tir::generics::contains_typevar check).
🪄 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: 69ddb0bc-6bae-4c9c-b152-443678c84442

📥 Commits

Reviewing files that changed from the base of the PR and between bfdd77f and eb5e4c6.

⛔ 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/baml_src/inferred_generic_type_args.snap is excluded by !**/*.snap
📒 Files selected for processing (2)
  • baml_language/crates/baml_compiler2_mir/src/lower.rs
  • baml_language/crates/baml_tests/baml_src/ns_inferred_generic_type_args/inferred_generic_type_args.baml

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 3, 2026 18:38 Inactive
@hellovai
hellovai force-pushed the hellovai/fix-generic-typearg-dispatch branch from eb5e4c6 to 5f85c2d Compare June 3, 2026 19:31
@vercel
vercel Bot temporarily deployed to Preview – beps June 3, 2026 19:32 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: 2

🤖 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_mir/src/lower.rs`:
- Around line 7712-7729: The assoc matcher interface_tir_assoc_match is too
strict: when a requested associated type is a TypeVar (e.g., Item = T) it must
be treated as a wildcard that matches any implementor binding instead of
requiring the impl assoc to be the same TypeVar; update
interface_tir_assoc_match so that inside the is_some_and closure it first checks
whether requested_ty is a TypeVar and if so returns true, otherwise fall back to
calling baml_compiler2_tir::normalize::is_same_normalized_type(impl_ty,
requested_ty, &self.resolved_aliases.aliases). Ensure you reference the Tir2Ty
representation for the type-var check and keep the rest of the matching logic
unchanged.
- Around line 959-967: The default associated type is being lowered with the
caller's pkg_items which causes package-local names to resolve incorrectly;
update the call to baml_compiler2_tir::generics::lower_type_expr_with_generics
so it passes the target interface's PackageItems (the package items belonging to
target_iface_pkg / the interface's package) instead of the local pkg_items when
lowering assoc.default.expr, and apply the identical change in the duplicated
logic inside resolve_implements_target_view so both sites use the target
interface's package items (keep passing the same namespace_path, bindings, and
diags).
🪄 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: 4e755e68-14e7-430c-b3ad-c939a10e87a2

📥 Commits

Reviewing files that changed from the base of the PR and between eb5e4c6 and 5f85c2d.

⛔ 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/baml_src/inferred_generic_type_args.snap is excluded by !**/*.snap
📒 Files selected for processing (2)
  • baml_language/crates/baml_compiler2_mir/src/lower.rs
  • baml_language/crates/baml_tests/baml_src/ns_inferred_generic_type_args/inferred_generic_type_args.baml

@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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 2

🤖 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_mir/src/lower.rs`:
- Around line 7712-7729: The assoc matcher interface_tir_assoc_match is too
strict: when a requested associated type is a TypeVar (e.g., Item = T) it must
be treated as a wildcard that matches any implementor binding instead of
requiring the impl assoc to be the same TypeVar; update
interface_tir_assoc_match so that inside the is_some_and closure it first checks
whether requested_ty is a TypeVar and if so returns true, otherwise fall back to
calling baml_compiler2_tir::normalize::is_same_normalized_type(impl_ty,
requested_ty, &self.resolved_aliases.aliases). Ensure you reference the Tir2Ty
representation for the type-var check and keep the rest of the matching logic
unchanged.
- Around line 959-967: The default associated type is being lowered with the
caller's pkg_items which causes package-local names to resolve incorrectly;
update the call to baml_compiler2_tir::generics::lower_type_expr_with_generics
so it passes the target interface's PackageItems (the package items belonging to
target_iface_pkg / the interface's package) instead of the local pkg_items when
lowering assoc.default.expr, and apply the identical change in the duplicated
logic inside resolve_implements_target_view so both sites use the target
interface's package items (keep passing the same namespace_path, bindings, and
diags).
🪄 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: 4e755e68-14e7-430c-b3ad-c939a10e87a2

📥 Commits

Reviewing files that changed from the base of the PR and between eb5e4c6 and 5f85c2d.

⛔ 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/baml_src/inferred_generic_type_args.snap is excluded by !**/*.snap
📒 Files selected for processing (2)
  • baml_language/crates/baml_compiler2_mir/src/lower.rs
  • baml_language/crates/baml_tests/baml_src/ns_inferred_generic_type_args/inferred_generic_type_args.baml
🛑 Comments failed to post (2)
baml_language/crates/baml_compiler2_mir/src/lower.rs (2)

959-967: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Resolve default associated types with the target interface's package items.

default.expr belongs to the interface declared at target_loc, but this path still lowers it with the caller's pkg_items. For imported interfaces, package-local names in the default will resolve against the implementor package, degrade to Unknown, and then assoc-based guard/method matching can silently miss valid implementors. Please use the target interface's PackageItems here, and mirror the same fix in the duplicated logic in resolve_implements_target_view.

🤖 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/lower.rs` around lines 959 - 967,
The default associated type is being lowered with the caller's pkg_items which
causes package-local names to resolve incorrectly; update the call to
baml_compiler2_tir::generics::lower_type_expr_with_generics so it passes the
target interface's PackageItems (the package items belonging to target_iface_pkg
/ the interface's package) instead of the local pkg_items when lowering
assoc.default.expr, and apply the identical change in the duplicated logic
inside resolve_implements_target_view so both sites use the target interface's
package items (keep passing the same namespace_path, bindings, and diags).

7712-7729: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Treat requested associated TypeVars like wildcard interface args during matching.

This helper is stricter than interface_tir_type_args_match_preserving_typevars: a requested assoc binding such as Item = T only matches an implementor whose assoc is literally the same TypeVar, so generic callers can lose primitive/out-of-body candidates before dispatch even checks the runtime receiver. The assoc matcher needs the same “requested typevar matches any concrete implementor binding” rule as the type-arg matcher.

🤖 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/lower.rs` around lines 7712 -
7729, The assoc matcher interface_tir_assoc_match is too strict: when a
requested associated type is a TypeVar (e.g., Item = T) it must be treated as a
wildcard that matches any implementor binding instead of requiring the impl
assoc to be the same TypeVar; update interface_tir_assoc_match so that inside
the is_some_and closure it first checks whether requested_ty is a TypeVar and if
so returns true, otherwise fall back to calling
baml_compiler2_tir::normalize::is_same_normalized_type(impl_ty, requested_ty,
&self.resolved_aliases.aliases). Ensure you reference the Tir2Ty representation
for the type-var check and keep the rest of the matching logic unchanged.

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 3, 2026 19:38 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 3, 2026 19:49 Inactive
A generic class instance whose `<T>` is *inferred* (not written explicitly)
must carry the correct runtime `class_type_args`, and a method dispatched
through a multi-implementor interface must receive the resolved class/interface
type args in its frame. Previously the interface-dispatch switch
(`emit_method_candidate_switch`) called the matched implementor's method with
only the explicit call-site `<...>` type args, never seeding the callee frame.

The effect: an interface **default method** (e.g. a lazy iterator's `map`
building `Map<T, U>`), or any dispatched method that reads its enclosing `T` at
runtime, saw empty `frame.type_args` -> built instances with
`class_type_args = [unknown]` -> the downstream `IsType` dispatch guard fell
through to the WRONG implementor (a silent wrong answer, or a field-access
panic when the sibling has a different layout). This blocked the `baml.iter`
lazy-iterator protocol.

Fix:
- `enclosing_generic_params()` now includes the interface's generic params for
  interface default methods (lowered as standalone functions whose bodies
  reference the interface's `T`; without this `TypeVar(T)` lowered to
  `Concrete(Void)` instead of `TypeArgRef(N)`).
- The interface-dispatch switch seeds each candidate's callee frame
  (De Bruijn order: class/iface params, then explicit call-site args). A
  class-owned method gets the implementor's class args — statically when the
  guard pins them, otherwise from the matched runtime instance via a
  `BoundMethod` (covering `Any`/partial guards, e.g. a generic class behind a
  non-generic interface). An inherited default gets the resolved interface args.
- `default.<method>()` (super-like) forwarding into a generic interface default
  seeds the default frame with the interface args too.

Regression coverage: new in-BAML namespace
`crates/baml_tests/baml_src/ns_inferred_generic_type_args` with a lazy-cursor
showcase plus focused cases (inferred static ctor, interface default reading T,
class-owned method building a generic, `default.<method>()` forwarding, and a
generic class behind a non-generic interface). Full suite green; existing
bytecode snapshots unchanged (no existing namespace exercised this path).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@hellovai
hellovai force-pushed the hellovai/fix-generic-typearg-dispatch branch from 5f85c2d to 2a53895 Compare June 3, 2026 21:18
@vercel
vercel Bot temporarily deployed to Preview – beps June 3, 2026 21:19 Inactive
@hellovai

hellovai commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai — re: the two associated-types findings (the assoc.default lowering using the caller's pkg_items around lower.rs 959–967, and interface_tir_assoc_match being stricter than the type-arg matcher around 7712–7729):

We're intentionally not addressing these in this PR. Both are valid-looking observations, but they belong to the BEP-57 associated-types feature (#3652, commit c14f14d9) — they are pre-existing on canary and are not part of this PR's diff. This PR is scoped to generic type-arg dispatch threading (seeding frame.type_args for interface-dispatched methods); it doesn't touch associated-type resolution or either of those functions (it consumes only the type-args element of requested_views, never the assoc element).

Folding an associated-types fix into this PR would expand the diff into a separate feature, blur the change's purpose, and would need dedicated cross-package / generic-caller assoc fixtures to validate properly. We'll track them separately for the associated-types owner.

The two findings that were in this PR's scope — the Any/partial-guard seeding and the default.<method>() forwarding — are fixed in 2a53895bc.

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Tip

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

@hellovai — understood, thanks for the context. I'll note that both the assoc.default/pkg_items lowering (~line 959–967) and the interface_tir_assoc_match strictness mismatch (~line 7712–7729) are pre-existing on canary from BEP-57 (#3652, commit c14f14d9) and are out of scope for this PR. I've recorded them so they won't be re-flagged here.

Would you like me to open a GitHub issue to track those two associated-types findings for the BEP-57 owner?


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

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

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


🧠 Learnings used
Learnt from: hellovai
Repo: BoundaryML/baml PR: 3369
File: baml_language/crates/baml_compiler2_mir/src/lower.rs:1917-1969
Timestamp: 2026-04-16T16:05:15.791Z
Learning: In `baml_language/crates/baml_compiler2_mir/src/lower.rs`, `class_field_ty()` re-lowers the field's declared type via `lower_type_expr_in_ns` but never applies concrete generic type arguments from the receiver. For example, `Box<User>.value.name` loses the `User` substitution, causing subsequent segments to degrade into the map/unknown path. This is a known, pre-existing limitation acknowledged by hellovai (PR `#3369`) — do not re-flag it in reviews. Generic substitution threading through `class_field_ty` is deferred to a future PR.

Learnt from: hellovai
Repo: BoundaryML/baml PR: 3667
File: baml_language/crates/baml_compiler2_mir/src/lower.rs:0-0
Timestamp: 2026-06-03T19:32:24.139Z
Learning: In `baml_language/crates/baml_compiler2_mir/src/lower.rs`, when class-owned override arms are dispatched through an interface with `InterfaceClassGuard::Any` or a partial guard on a generic class, seed the callee frame from the matched receiver instance by binding the receiver into a `BoundMethod`; do not use the caller's `frame.type_args` as a fallback because caller frame args are unrelated to the callee instance's class type args. Fully-pinned guards may keep static seeding. This was clarified by hellovai in PR `#3667` and covered by the regression test `generic_class_behind_nongeneric_interface_reads_type_var`.

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

Learnt from: hellovai
Repo: BoundaryML/baml PR: 3302
File: baml_language/crates/baml_compiler2_mir/src/lower.rs:335-343
Timestamp: 2026-03-30T07:35:48.460Z
Learning: In `baml_language/crates/baml_compiler2_mir/src/lower.rs`, the `resolutions` (`FxHashMap<AstExprId, MemberResolution>`) and `exhaustive_matches` (`FxHashSet<AstExprId>`) fields intentionally use bare `AstExprId` keys (not scoped by `FileScopeId`). This is safe in practice because `lower_lambda` does a full save/restore of `self.body`, so the ExprIds queried during parent-body lowering come from the parent's ExprBody arena and those queried during lambda-body lowering come from the lambda's arena. However, if two scopes share the same bare ExprId in `merge_scope`, the last-writer-wins and could silently return the wrong resolution. A full migration to `(FileScopeId, AstExprId)` keys (matching `expr_types`/`pat_types`) is deferred as a follow-up if edge cases emerge. Do not flag this as a bug in reviews of PR `#3302`.

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

Learnt from: codeshaunted
Repo: BoundaryML/baml PR: 3417
File: baml_language/crates/baml_compiler2_mir/src/lower.rs:4823-4843
Timestamp: 2026-04-27T19:57:00.090Z
Learning: In `baml_language/crates/baml_compiler2_mir/src/lower.rs` (PR `#3417`), `bind_pattern()` does not descend into `AstPatternKind::Class` subpatterns — it only materializes `pat.binding_name()` and skips field bindings like `Foo { v }`. This is intentionally deferred until the new match syntax is implemented. Do not re-flag this as a bug in reviews of PR `#3417`.

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

Learnt from: codeshaunted
Repo: BoundaryML/baml PR: 3417
File: baml_language/crates/baml_compiler2_mir/src/lower.rs:0-0
Timestamp: 2026-04-27T20:06:01.292Z
Learning: In `baml_language/crates/baml_compiler2_mir/src/lower.rs` (PR `#3417`), the fallback path in `lower_pattern_test` for `AstPatternKind::EnumVariant` constructs `ItemRef::EnumType` using a dotted join of all `enum_name` segments as the `name` field (not just the last segment). This divergence from the TIR-driven path (which uses `qtn.name()`) was marked stale by the author (codeshaunted); do not re-flag this in future reviews of this file.

Learnt from: hellovai
Repo: BoundaryML/baml PR: 3300
File: baml_language/crates/baml_compiler2_tir/src/builder.rs:369-371
Timestamp: 2026-03-29T01:07:11.876Z
Learning: In `baml_language/crates/baml_compiler2_tir/src/builder.rs` (PR `#3300`), bare name lookups (object constructors, type-pattern sugar, value/type inference) intentionally use only `package_items.lookup_type(&self.ns_context, name)` / `lookup_value(&self.ns_context, name)` scoped to the file's local namespace. Do NOT flag these as bypassing PRC fallback — the design goal is that bare names resolve only within the declaring file's namespace (root files in root, ns_llm files in llm, etc.). Routing through `PackageResolutionContext::resolve_type/resolve_value` would reintroduce implicit cross-namespace bare-name fallback that this PR explicitly removes.

Learnt from: sxlijin
Repo: BoundaryML/baml PR: 3431
File: baml_language/crates/bridge_ctypes/src/value_encode.rs:72-75
Timestamp: 2026-04-29T19:17:57.279Z
Learning: In `baml_language/crates/bridge_ctypes/src/value_encode.rs`, all `BamlTyName` constructions (e.g., for `BamlValueClass`, `BamlValueEnum`, `BamlValueUnionVariant`, and `BamlTyClass` in `ty_to_field_type`) hard-code `generic_args: vec![]`, intentionally erasing structured generic type arguments. This is a known limitation acknowledged by sxlijin (PR `#3431`) — the fix to propagate real generic args through the outbound FFI payload is deferred to a follow-up PR. Do not re-flag generic_args erasure in this file during reviews.

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

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

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

@hellovai

hellovai commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit, not, but can you share a small repro in BAML code that might trigger the issue?

@hellovai
hellovai enabled auto-merge June 3, 2026 21:23
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find BAML files related to associated types
fd -e baml | xargs grep -l "assoc\|type Item\|associated" 2>/dev/null | head -20

# Look for assoc-related test fixtures
fd -e baml -p "assoc" | head -20
fd "assoc" --type f | head -20

Length of output: 745


🏁 Script executed:

#!/bin/bash
# Look at the lower.rs context around line 959-967 to understand what assoc.default does
sed -n '940,990p' baml_language/crates/baml_compiler2_mir/src/lower.rs 2>/dev/null || \
  fd lower.rs -p baml_compiler2_mir | xargs -I{} sed -n '940,990p' {}

Length of output: 2468


🏁 Script executed:

#!/bin/bash
# Look at interface_tir_assoc_match around line 7712-7729
fd lower.rs -p baml_compiler2_mir | xargs -I{} sed -n '7700,7740p' {}

Length of output: 488


🏁 Script executed:

#!/bin/bash
# Read the associated types test file
cat baml_language/crates/baml_tests/tests/interfaces_associated_types.rs | head -200

Length of output: 5587


🏁 Script executed:

#!/bin/bash
# Find and read the lower.rs file properly
LOWER=$(fd "lower.rs" baml_language/ | grep baml_compiler2_mir | head -1)
echo "Found: $LOWER"
# Look at interface_tir_assoc_match
grep -n "interface_tir_assoc_match" "$LOWER" | head -10
sed -n '7700,7750p' "$LOWER"

Length of output: 2454


🏁 Script executed:

#!/bin/bash
# Find baml fixtures related to associated types
fd -e baml baml_language/crates/baml_tests/ | xargs grep -l "assoc\|type [A-Z]" 2>/dev/null | head -10
# Also look for any .baml files in projects related to associated types
fd -e baml baml_language/ | xargs grep -l "impl.*assoc\|type [A-Z][a-z]*\b" 2>/dev/null | head -10

Length of output: 1373



@hellovai — here are minimal BAML repros for each finding:


Issue 1 — assoc.default lowering uses caller's pkg_items

This only bites cross-namespace, where the interface's default references a type that lives in the interface's own namespace but is invisible from the implementor's namespace.

// --- ns_lib/types.baml (namespace: lib) ---
interface Serializable {
    // Default references `Payload`, which is declared in the same (lib) namespace
    type Format = Payload
    function serialize(self) -> Format
}

class Payload {
    data: string
}
// --- ns_app/widget.baml (namespace: app) ---
// Widget relies on the default `type Format = Payload`.
// When lowering the default, `lower_type_expr_with_generics` is called with the
// *caller's* `pkg_items` (from `app`) but the interface's `namespace_path` (from `lib`).
// `Payload` is not in `app`'s `pkg_items`, so it resolves to Unknown/errors.
class Widget {
    name: string

    implements lib.Serializable {
        // No explicit `type Format = ...` binding — relies on the default
        function serialize(self) -> Format {
            return Format { data: self.name }
        }
    }
}

Expected: compiles; Widget.serialize() returns a Payload.
Actual (bug): Format / Payload resolves to Unknown or a compile error because the default is lowered with app's pkg_items, where Payload doesn't exist.


Issue 2 — interface_tir_assoc_match is stricter than the type-arg matcher

Single-file repro. A generic class binds its associated type to the class type variable T. At dispatch time the assoc matcher calls is_same_normalized_type(TypeVar(T), Concrete(int)), which returns false — so Box<int> is never selected as the matching implementor.

interface Container {
    type Item
    function get(self) -> Item
}

class Box<T> {
    value: T

    implements Container {
        type Item = T     // bound to the class type-var T

        function get(self) -> T {
            return self.value
        }
    }
}

// Dispatch through a fully-pinned associated-type view.
// `interface_tir_assoc_match` checks is_same_normalized_type(T, int)
// which fails because T is a TypeVar — even though Box<int>.Item == int.
// The type-arg matcher (interface_tir_type_args_match_preserving_typevars)
// *would* handle this via typevar substitution.
function unwrap(c: Container { type Item = int }) -> int {
    return c.get()
}

test T {
    input: Box<int> { value: 42 }
    fn: unwrap
    expected: 42   // panics / wrong implementor at runtime due to assoc-match failure
}

Both are self-contained enough to add directly to interfaces_associated_types.rs (or a dedicated BEP-57 fixture file) whenever the associated-types work is resumed. Let me know if you'd like me to open a tracking issue for these.


🧠 Learnings used
Learnt from: hellovai
Repo: BoundaryML/baml PR: 3369
File: baml_language/crates/baml_compiler2_mir/src/lower.rs:1917-1969
Timestamp: 2026-04-16T16:05:15.791Z
Learning: In `baml_language/crates/baml_compiler2_mir/src/lower.rs`, `class_field_ty()` re-lowers the field's declared type via `lower_type_expr_in_ns` but never applies concrete generic type arguments from the receiver. For example, `Box<User>.value.name` loses the `User` substitution, causing subsequent segments to degrade into the map/unknown path. This is a known, pre-existing limitation acknowledged by hellovai (PR `#3369`) — do not re-flag it in reviews. Generic substitution threading through `class_field_ty` is deferred to a future PR.

Learnt from: hellovai
Repo: BoundaryML/baml PR: 3667
File: baml_language/crates/baml_compiler2_mir/src/lower.rs:0-0
Timestamp: 2026-06-03T19:32:24.139Z
Learning: In `baml_language/crates/baml_compiler2_mir/src/lower.rs`, when class-owned override arms are dispatched through an interface with `InterfaceClassGuard::Any` or a partial guard on a generic class, seed the callee frame from the matched receiver instance by binding the receiver into a `BoundMethod`; do not use the caller's `frame.type_args` as a fallback because caller frame args are unrelated to the callee instance's class type args. Fully-pinned guards may keep static seeding. This was clarified by hellovai in PR `#3667` and covered by the regression test `generic_class_behind_nongeneric_interface_reads_type_var`.

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

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

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

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

Learnt from: hellovai
Repo: BoundaryML/baml PR: 3302
File: baml_language/crates/baml_compiler2_mir/src/lower.rs:335-343
Timestamp: 2026-03-30T07:35:48.460Z
Learning: In `baml_language/crates/baml_compiler2_mir/src/lower.rs`, the `resolutions` (`FxHashMap<AstExprId, MemberResolution>`) and `exhaustive_matches` (`FxHashSet<AstExprId>`) fields intentionally use bare `AstExprId` keys (not scoped by `FileScopeId`). This is safe in practice because `lower_lambda` does a full save/restore of `self.body`, so the ExprIds queried during parent-body lowering come from the parent's ExprBody arena and those queried during lambda-body lowering come from the lambda's arena. However, if two scopes share the same bare ExprId in `merge_scope`, the last-writer-wins and could silently return the wrong resolution. A full migration to `(FileScopeId, AstExprId)` keys (matching `expr_types`/`pat_types`) is deferred as a follow-up if edge cases emerge. Do not flag this as a bug in reviews of PR `#3302`.

Learnt from: codeshaunted
Repo: BoundaryML/baml PR: 3417
File: baml_language/crates/baml_compiler2_mir/src/lower.rs:4823-4843
Timestamp: 2026-04-27T19:57:00.090Z
Learning: In `baml_language/crates/baml_compiler2_mir/src/lower.rs` (PR `#3417`), `bind_pattern()` does not descend into `AstPatternKind::Class` subpatterns — it only materializes `pat.binding_name()` and skips field bindings like `Foo { v }`. This is intentionally deferred until the new match syntax is implemented. Do not re-flag this as a bug in reviews of PR `#3417`.

Learnt from: sxlijin
Repo: BoundaryML/baml PR: 3431
File: baml_language/crates/bridge_ctypes/src/value_encode.rs:72-75
Timestamp: 2026-04-29T19:17:57.279Z
Learning: In `baml_language/crates/bridge_ctypes/src/value_encode.rs`, all `BamlTyName` constructions (e.g., for `BamlValueClass`, `BamlValueEnum`, `BamlValueUnionVariant`, and `BamlTyClass` in `ty_to_field_type`) hard-code `generic_args: vec![]`, intentionally erasing structured generic type arguments. This is a known limitation acknowledged by sxlijin (PR `#3431`) — the fix to propagate real generic args through the outbound FFI payload is deferred to a follow-up PR. Do not re-flag generic_args erasure in this file during reviews.

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

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

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 3, 2026 21:25 Inactive
@hellovai
hellovai added this pull request to the merge queue Jun 3, 2026
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 3, 2026 21:37 Inactive
Merged via the queue into canary with commit a684ec6 Jun 3, 2026
51 checks passed
@hellovai
hellovai deleted the hellovai/fix-generic-typearg-dispatch branch June 3, 2026 21:38

This branch was previously deployed

3 inactive deployments
Preview – promptfiddle — 2a53895b Deployed Jun 3, 2026 by vercel[bot]
Preview – promptfiddle2 — 2a53895b Deployed Jun 3, 2026 by vercel[bot]
Preview – beps — 2a53895b Deployed Jun 3, 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