Skip to content

[codex] Implement interface v2 semantics - #3555

Merged
aaronvg merged 70 commits into
canaryfrom
aaron/interfacs
May 30, 2026
Merged

aaronvg merged 70 commits into
canaryfrom
aaron/interfacs

Conversation

@aaronvg

@aaronvg aaronvg commented May 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Implements BEP-044 interface v2 semantics across the compiler, runtime, tests, and LSP fixtures.

  • Adds/updates interface behavior for field linking with as, .as<I> upcasts, requires chains, generic interface bounds, out-of-body implementation rules, and static Self semantics.
  • Expands compiler diagnostics and LSP fixture coverage for invalid interface member links, downcasts, missing fields/methods, field conflicts, and Self usage.
  • Merges current origin/canary into the branch and preserves the generated rig_tests Python output as untracked local noise.

Validation

  • UPDATE_EXPECT=1 cargo test -p baml_lsp2_actions_tests -- --nocapture
  • cargo test -p baml_tests --test interfaces -- --nocapture
  • cargo test -p baml_lsp2_actions_tests -- --nocapture

Note

High Risk
Large changes across parsing, HIR validation, MIR emit, and runtime type reflection affect core typing and dispatch; incorrect implementor registry or bound handling could cause subtle runtime misbehavior despite broad test coverage.

Overview
This PR lands BEP-044 interface v2 end-to-end: new surface syntax (interface, in-class implements I { … }, top-level implements I for T, requires, generic extends bounds, expr.as<T> upcasts, and contextual default calls), wired through AST lowering, HIR item trees, and a large set of interface-specific diagnostics.

The compiler now models interfaces, implementation blocks, and field links (iface_field as class_field), merges simple local implements … for into classes, and emits a runtime interface_implementors map so reflection on type can answer implements / implemented_by / implementors. Builtin TypeValue gains those VM methods; codegen accepts interface-related keywords as method names (e.g. implements).

Binary size baselines for baml-cli, bridge_wasm, and related CI size-gate configs are bumped (~3–4%) to match the larger artifact after this work.

Reviewed by Cursor Bugbot for commit 74c1f31. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Interfaces and in-class/out-of-body implements with requires/extends and generic bounds
    • .as upcast/projection and default/interface methods
    • Runtime reflection: type.implements(), type.implemented_by(), type.implementors()
  • Editor / LSP

    • Completions, hover, outline, diagnostics, and goto/definition updated for interfaces, implements, projections, and generics
  • Tests

    • Many new syntax/validation/unit tests exercising interfaces, generics, projections, and diagnostics

Review Change Stack

@vercel

vercel Bot commented May 21, 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 May 30, 2026 8:02am
promptfiddle Ready Ready Preview, Comment May 30, 2026 8:02am
promptfiddle2 Ready Ready Preview, Comment May 30, 2026 8:02am

Request Review

@coderabbitai

coderabbitai Bot commented May 21, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds BEP-044 interfaces end-to-end: keywords and .as<T> syntax, AST/HIR/TIR changes for interfaces and generic bounds, an implements registry and MIR dispatch, extended type inference/checking, diagnostics and LSP updates, runtime bytecode/VM reflection, builtin type methods, and many tests.

Changes

BEP-044 interface system

Layer / File(s) Summary
Lexer & Parser syntax
crates/baml_compiler_lexer/src/tokens.rs, crates/baml_compiler_parser/src/parser.rs, crates/baml_compiler_syntax/src/syntax_kind.rs, crates/baml_compiler_syntax/src/ast.rs
Add keywords interface/implements/implement/extends/requires, .as<T> upcast, UPCAST_EXPR, GENERIC_PARAM_BOUNDS, AST wrappers and CST parsing for interfaces, method signatures, implements blocks, and upcasts.
AST lowering & HIR wiring
crates/baml_compiler2_ast/*, crates/baml_compiler2_hir/*
Lower CST to new AST InterfaceDef/ImplementsFor, record generic-param bounds, add HIR Interface/ImplementsFor/ImplementsBlock, InterfaceLoc, item-tree storage/allocation helpers, Function.generic_param_bounds, and many HIR diagnostics variants.
TIR / MIR / Emit
crates/baml_compiler2_tir/*, crates/baml_compiler2_mir/src/lower.rs, crates/baml_compiler2_emit/src/lib.rs
Introduce Ty::Interface, propagate generic bounds into TIR/MIR, build per-package implements registries, emit Program.interface_implementors, merge class fields with implements, and implement MIR type-tag switch dispatch for interface calls/fields.
Type inference & checking
crates/baml_compiler2_tir/src/builder.rs, crates/baml_compiler2_tir/src/inference.rs, crates/baml_compiler2_tir/src/generics.rs
Add per-scope generic bounds, Self substitution for methods, upcast checking, member resolution for Ty::Interface receivers, subtype short-circuits and generics-bound enforcement, and object-construction/interface-field projection rules.
Implements registry & pattern matching
crates/baml_compiler2_tir/src/interfaces.rs
New ImplementsRegistry, rule/index structures, pattern matching (match_ty_pattern), package_implements_registry query, and helpers to match rules against concrete types (including unions and type-arg binding).
HIR ItemTree & signatures
crates/baml_compiler2_hir/src/item_tree.rs, crates/baml_compiler2_hir/src/signature.rs
Store interfaces, implements blocks, Interface model and InterfaceMethodSig; copy generic_param_bounds to Function; fill omitted nested throws into generic bounds handling.
Diagnostics & check pipeline
crates/baml_lsp2_actions/src/check.rs, crates/baml_compiler_diagnostics/src/diagnostic.rs, crates/baml_compiler2_hir/src/diagnostic.rs
New lowering/HIR/TIR diagnostics for interfaces/implements, mapping to new DiagnosticId variants and error codes, and check_interfaces validation stage wired into check_file.
LSP, display & tests
crates/baml_lsp2_actions/*, crates/baml_lsp2_actions_tests/*, many test fixtures
File-aware type display (display_ty_for_file), LSP completions/hover/usages updated to surface interfaces and out-of-body implements, numerous new/updated tests and fixtures for syntax, diagnostics, hover, and usages.
Runtime & builtins
crates/bex_vm/*, crates/bex_engine, crates/bex_vm_types, crates/baml_builtins2/*
Add Program.interface_implementors to bytecode, thread into BexVm, expose runtime type.implements/type.implemented_by/type.implementors builtin methods returning reflectable Type values, and store registry on engine/VM.
Emit metadata & codegen
crates/baml_compiler2_emit/src/lib.rs, crates/baml_project/src/client_codegen.rs
Collect merged class fields with implements, compute Program.interface_implementors, erase bound typevars for metadata, and treat TirTy::Interface in client codegen as nominal class-like type for codegen.
Aux & tooling
.ci/size-gate.toml, crates/baml_compiler2_mir/src/pretty.rs, many small helpers
Size-gate adjustments, pretty-printer tweaks, and helper APIs for type-expression substitution and normalization.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

"A rabbit scribbles 'interface' in spring,
Upcast moonbeams, dotted fields that sing.
Implements bloom where classes meet,
Default echoes, dispatch sweet.
LSP whispers types by file — hop, reflection complete." 🐇✨

✨ 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 aaron/interfacs

@vercel
vercel Bot temporarily deployed to Preview – beps May 21, 2026 06:42 Inactive
@aaronvg
aaronvg marked this pull request as ready for review May 21, 2026 06:50
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 May 21, 2026 07:00 Inactive
@github-actions

github-actions Bot commented May 21, 2026 •

Copy link
Copy Markdown

Binary size checks passed

✅ 7 passed

Artifact Platform File Gzip Gated on Baseline Delta Status
✅ baml-cli Linux 🔒 20.0 MB 8.3 MB file 19.4 MB +563.1 KB (+2.9%) OK
✅ packed-program Linux 🔒 15.0 MB 6.3 MB file 14.7 MB +324.9 KB (+2.2%) OK
✅ baml-cli macOS 🔒 15.1 MB 7.2 MB file 15.1 MB +16 B (+0.0%) OK
✅ packed-program macOS 🔒 11.4 MB 5.5 MB file 11.2 MB +264.9 KB (+2.4%) OK
✅ baml-cli Windows 🔒 16.3 MB 7.4 MB file 16.3 MB +1.0 KB (+0.0%) OK
✅ packed-program Windows 🔒 12.1 MB 5.6 MB file 11.9 MB +297.3 KB (+2.5%) OK
✅ bridge_wasm WASM 13.4 MB 🔒 3.9 MB gzip 3.8 MB +2.0 KB (+0.1%) 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

@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: 19

Caution

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

⚠️ Outside diff range comments (3)
baml_language/_plan/BEP-044-interfaces/IMPLEMENTATION_PLAN.md (1)

1-690: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

This implementation plan is obsolete and contradicts the current design.

IMPLEMENTATIONv2.md (lines 3-4) explicitly states it supersedes this document. The two plans have fundamentally incompatible designs:

  • This plan (Gap 3, lines 170-278): "Fields must be redeclared inside implements blocks"
  • IMPLEMENTATIONv2.md (lines 31-32): "Fields inside implements blocks. They should become invalid syntax"
  • README.md (line 422): "Fields live on the class, not inside the implements block"

Keeping both documents in the repository will confuse implementers about which design to follow.

📝 Recommended action

Either:

  1. Delete this file and rely solely on IMPLEMENTATIONv2.md, or
  2. Add a prominent deprecation notice at the top:
# ⚠️ OBSOLETE - DO NOT USE

This implementation plan has been superseded by [IMPLEMENTATIONv2.md](./IMPLEMENTATIONv2.md).
The design documented here is not being implemented.

---

# BEP-044 Interfaces — Implementation Fix Plan (OBSOLETE)
🤖 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/_plan/BEP-044-interfaces/IMPLEMENTATION_PLAN.md` around lines 1
- 690, The IMPLEMENTATION_PLAN.md is obsolete and conflicts with
IMPLEMENTATIONv2.md and README.md; remove ambiguity by either deleting BEP-044
Interfaces — Implementation Fix Plan (the file shown) or prepend a clear
deprecation banner referencing IMPLEMENTATIONv2.md and indicating it supersedes
this document (so readers and implementers only follow IMPLEMENTATIONv2.md);
ensure the banner mentions IMPLEMENTATIONv2.md and README.md to make the
canonical source explicit.
baml_language/crates/baml_lsp2_actions/src/describe.rs (1)

687-701: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Interface describe path is still blocked by item-node filtering.

Line 937 adds interface dependency handling, but find_item_range/docstring extraction rely on is_item_node, and Line 687’s matcher does not include interface definition nodes. That can cause describe() for top-level interfaces to return None before this new dependency logic is used.

Suggested fix
 fn is_item_node(kind: SyntaxKind) -> bool {
     matches!(
         kind,
         SyntaxKind::CLASS_DEF
+            | SyntaxKind::INTERFACE_DEF
             | SyntaxKind::FUNCTION_DEF
             | SyntaxKind::ENUM_DEF
             | SyntaxKind::CLIENT_DEF
             | SyntaxKind::GENERATOR_DEF
             | SyntaxKind::TEST_DEF

Also applies to: 937-948

🤖 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_lsp2_actions/src/describe.rs` around lines 687 -
701, The item-node filter in is_item_node currently omits interface definition
nodes, causing find_item_range and docstring extraction used by describe() to
skip top-level interfaces; update is_item_node to include the interface
SyntaxKind variant (e.g., add SyntaxKind::INTERFACE_DEF) so interface
definitions are treated as item nodes, and verify related logic in
find_item_range and describe still applies (also mirror this addition where
equivalent item-node checks exist around the dependency handling added at lines
~937-948).
baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs (1)

65-75: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t treat .as<T> target generics as call-site type arguments.

find_callee_generic_args now picks GENERIC_ARGS from UPCAST_EXPR, which can leak upcast target types into Expr::Call.type_args for calls on upcasted receivers.

Suggested fix
 fn find_callee_generic_args(callee_node: &SyntaxNode) -> Option<SyntaxNode> {
+    // `.as<T>` generic args belong to the upcast target, not to a call site.
+    if callee_node.kind() == SyntaxKind::UPCAST_EXPR {
+        let base = callee_node.children().next()?;
+        return find_callee_generic_args(&base);
+    }
+
     if let Some(args) = callee_node
         .children()
         .find(|n| n.kind() == SyntaxKind::GENERIC_ARGS)
     {
         return Some(args);
     }
     match callee_node.kind() {
         SyntaxKind::FIELD_ACCESS_EXPR
-        | SyntaxKind::UPCAST_EXPR
         | SyntaxKind::OPTIONAL_FIELD_ACCESS_EXPR => {
             let base = callee_node.children().next()?;
             find_callee_generic_args(&base)
         }
         _ => None,
     }
 }
🤖 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_ast/src/lower_expr_body.rs` around lines
65 - 75, find_callee_generic_args currently returns a child GENERIC_ARGS even
when callee_node is an UPCAST_EXPR (and thus picks upcast target generics like
`.as<T>` into Expr::Call.type_args); change the function so it does not treat
GENERIC_ARGS from UPCAST_EXPR (and related upcast/optional-upcast kinds) as
call-site type arguments — e.g., check callee_node.kind() first and skip
returning a child GENERIC_ARGS when kind() == SyntaxKind::UPCAST_EXPR (and/or
SyntaxKind::OPTIONAL_FIELD_ACCESS_EXPR if appropriate), leaving the existing
behavior for FIELD_ACCESS_EXPR and other call-site forms; update the logic in
find_callee_generic_args to only return GENERIC_ARGS for actual call-site nodes
so Expr::Call.type_args no longer receives upcast target types.
🧹 Nitpick comments (7)
baml_language/_plan/BEP-044-interfaces/meta.json (1)

9-9: 💤 Low value

Consider updating the updated date to reflect recent changes.

IMPLEMENTATIONv2.md was written on 2026-05-20 (as noted in its header), but meta.json shows "updated": "2026-05-05". If the BEP has evolved significantly since creation, the metadata should reflect the latest substantial revision.

📅 Proposed update
-  "updated": "2026-05-05",
+  "updated": "2026-05-20",
🤖 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/_plan/BEP-044-interfaces/meta.json` at line 9, Update the
"updated" field in meta.json to match the latest substantial revision date
(e.g., change "updated": "2026-05-05" to "2026-05-20" to align with
IMPLEMENTATIONv2.md header); locate the "updated" property in meta.json and set
it to the most recent revision date that reflects changes described in
IMPLEMENTATIONv2.md so the BEP metadata is accurate.
baml_language/_plan/BEP-044-interfaces/README.md (1)

783-783: ⚡ Quick win

Consider clarifying the historical reference to extends.

The phrase "earlier drafts of this BEP" might confuse readers scanning the document, since the presence of IMPLEMENTATION_PLAN.md (which uses extends) in the same directory could make them think extends is still a valid approach.

✏️ Suggested clarification
-**Why `requires` and not `extends`.** `extends` (as in TS / Java / earlier drafts of this BEP) makes the *child interface* inherit the parent's signatures, so writing `implements Child` silently fulfilled the parent contract too.
+**Why `requires` and not `extends`.** `extends` (as in TS / Java) makes the *child interface* inherit the parent's signatures, so writing `implements Child` silently fulfilled the parent contract too.

This removes the potentially confusing self-reference to "earlier drafts" that might lead readers to hunt for those drafts.

🤖 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/_plan/BEP-044-interfaces/README.md` at line 783, Reword the
sentence that reads "extends (as in TS / Java / earlier drafts of this BEP)" to
remove the ambiguous "earlier drafts of this BEP" reference and explicitly
clarify the history: either remove that clause or replace it with a short, clear
note such as "extends (as in TS / Java — previously used in
IMPLEMENTATION_PLAN.md)" so readers understand that IMPLEMENTATION_PLAN.md used
extends and that the document now prefers requires; update the sentence
referencing 'requires' and 'extends' accordingly to avoid confusion.
baml_language/crates/baml_compiler2_hir/src/builder.rs (1)

1091-1121: 💤 Low value

Interface contribution ordering differs from class pattern.

In lower_class, the type contribution is made before pushing the class scope (lines 1026-1032), whereas here the contribution is made inside the interface scope (after push_scope at line 1098, before pop_scope at line 1148). While this doesn't affect correctness since contributions are just appended to a vector independent of scope state, the inconsistency could be confusing for future maintainers.

Consider moving the contribution before push_scope to match the class pattern, or add a comment explaining why the ordering is intentional here.

🤖 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_hir/src/builder.rs` around lines 1091 -
1121, The interface contribution ordering is inconsistent with lower_class;
update lower_interface to either move the type contribution before calling
push_scope to match lower_class or (preferably) add a brief explanatory comment
above the push_scope call explaining why the contribution must occur after
lowering default methods (because default_method_ids are computed inside the
Class-kind scope and the interface's local_id is allocated only after collecting
them). Reference lower_interface, push_scope, default_method_ids,
item_tree.alloc_interface, and type_contributions when making the change so
future maintainers understand the intentional ordering.
baml_language/crates/baml_compiler_lexer/src/tokens.rs (1)

48-57: ⚡ Quick win

Add a focused lexer regression test for the new interface keywords.

Please add a unit test that covers interface, implements, implement, extends, and requires, and explicitly verifies implements lexes as one token (not implement + Word("s")).

Proposed test addition
 #[test]
+fn test_interface_keywords() {
+    let tokens = lex_no_whitespace("interface implements implement extends requires");
+    assert_eq!(
+        tokens,
+        vec![
+            TokenKind::Interface,
+            TokenKind::Implements,
+            TokenKind::Implement,
+            TokenKind::Extends,
+            TokenKind::Requires,
+        ]
+    );
+
+    let tokens2 = lex_no_whitespace("implements");
+    assert_eq!(tokens2, vec![TokenKind::Implements]);
+}
+
+#[test]
 fn test_exception_keywords() {
🤖 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_lexer/src/tokens.rs` around lines 48 - 57,
Add a unit test that feeds the lexer the string "interface implements implement
extends requires" and asserts the produced token sequence contains the single
tokens Interface, Implements, Implement, Extends, and Requires (in that order);
specifically assert that "implements" produces one Implements token rather than
an Implement token plus a Word/Identifier "s". Locate the test near other lexer
tests and call the public tokenize/lex function used in your crate (e.g.,
tokenize, lex, or Lexer::next_tokens) and compare the token enum values
(Token::Interface, Token::Implements, Token::Implement, Token::Extends,
Token::Requires); include any necessary whitespace handling so the lexer sees
separate lexemes.
baml_language/crates/baml_compiler2_ast/src/lower_cst.rs (1)

1444-1457: 💤 Low value

Misleading class_name in diagnostic for interface field links.

The LoweringDiagnostic::MissingFieldName uses class_name: "interface field link" which is semantically incorrect and may produce confusing error messages since this isn't actually a class context. Consider using a more specific diagnostic variant or a clearer context string.

🛠️ Suggested improvement
     let Some(interface_field) = link.interface_field() else {
-        diags.push(LoweringDiagnostic::MissingFieldName {
-            class_name: "interface field link".to_string(),
+        diags.push(LoweringDiagnostic::MissingInterfaceFieldLinkName {
+            context: "interface field".to_string(),
             span: link.syntax().text_range(),
         });
         return None;
     };
     let Some(class_field) = link.class_field() else {
-        diags.push(LoweringDiagnostic::MissingFieldName {
-            class_name: "interface field link".to_string(),
+        diags.push(LoweringDiagnostic::MissingInterfaceFieldLinkName {
+            context: "class field".to_string(),
             span: link.syntax().text_range(),
         });
         return None;
     };

This would require adding a new LoweringDiagnostic variant, or alternatively just use a clearer string like "implements block field mapping".

🤖 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_ast/src/lower_cst.rs` around lines 1444 -
1457, The diagnostic for missing field names in the interface->class mapping
uses class_name: "interface field link", which is misleading; update the
LoweringDiagnostic usage in the block handling link.interface_field() and
link.class_field() to provide a clearer context string (e.g., "implements block
field mapping") or add a new LoweringDiagnostic variant specifically for
interface-to-class field links and use that variant instead; adjust the
diags.push calls around link.interface_field() and link.class_field() and update
any matching match arms or consumers of LoweringDiagnostic so the new
string/variant is handled consistently.
baml_language/crates/baml_compiler2_tir/src/interfaces.rs (1)

169-188: 💤 Low value

Remove unused db parameter or document future intent.

The db parameter is unused in this function (line 183 just silences the warning). Consider removing it from the signature if it's not needed, or add a brief comment explaining why it's kept for future use.

♻️ Suggested cleanup
 pub fn resolve_path_to_interface<'db>(
-    db: &'db dyn crate::Db,
+    _db: &'db dyn crate::Db,  // Reserved for future cross-package resolution
     target: &baml_compiler2_ast::TypeExpr,
     pkg_items: &baml_compiler2_hir::package::PackageItems<'db>,
     current_ns: &[Name],
 ) -> Option<baml_compiler2_hir::loc::InterfaceLoc<'db>> {
     use baml_compiler2_ast::TypeExpr;
     let TypeExpr::Path { segments, .. } = target else {
         return None;
     };
     let (head, name) = segments
         .split_last()
         .map(|(last, head)| (head, last.clone()))?;
     let lookup_ns: &[Name] = if head.is_empty() { current_ns } else { head };
-    let _ = db;
     let Definition::Interface(loc) = pkg_items.lookup_type(lookup_ns, &name)? else {
         return None;
     };
     Some(loc)
 }
🤖 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/interfaces.rs` around lines 169 -
188, The db parameter of resolve_path_to_interface is unused (silenced by let _
= db); either remove it from the function signature and update all callers to
stop passing a Db, or keep it and add a short comment explaining it's
intentionally reserved for future use (e.g., "kept for potential lookup needs")
and remove the let _ = db line; locate the function by name
resolve_path_to_interface and adjust callers/exports accordingly to ensure
compilation after removing the parameter or add the explanatory comment if
retaining it.
baml_language/crates/bex_vm_types/src/types.rs (1)

95-103: ⚡ Quick win

Add #[serde(default)] to Program::interface_implementors for safer cross-version deserialization.

Program is deserialized by baml-pack-host from the embedded PackEnvelope via bitcode::deserialize, and bitcode is configured with the serde feature (so missing fields will error unless they have serde defaults). Packed binaries bundle the matching host, but if Program/PackEnvelope ever needs to be deserialized across versions (e.g., via a standalone host path or other cached payloads), this field should behave as “empty when absent” to avoid failures.

💡 Proposed fix
     /// Empty for programs without interfaces.
+    #[serde(default)]
     pub interface_implementors: IndexMap<baml_type::TypeName, Vec<baml_type::TypeName>>,
🤖 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 95 - 103, Add
serde's default attribute to the Program interface_implementors field so
deserialization will treat a missing field as an empty map: annotate the field
`interface_implementors: IndexMap<baml_type::TypeName,
Vec<baml_type::TypeName>>` with `#[serde(default)]` (ensure serde feature is
enabled where needed); this makes `Program::interface_implementors` default to
empty on older/variant payloads and prevents deserialization errors across
versions.
🤖 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_compiler_parser/src/parser.rs`:
- Around line 2511-2514: The parser currently treats leading @@ attributes as
standalone when encountering them before a function in parse_interface, so
parse_atat_attribute() is called and the attribute is not attached to the
subsequent interface method; modify parse_interface to collect/consume leading
@@ attributes and attach them to the method node instead of parsing them as
separate items—i.e., detect @@ before deciding between parse_interface_method(),
gather those attributes (the same way
item_keyword_after_leading_block_attributes preserves metadata for class
methods), and ensure they are attached to the METHOD_SIG/default method node
produced by parse_interface_method() so interface methods retain their metadata.
- Around line 2416-2423: The recovery condition is too permissive: don't exempt
interface/implements from ending a body unless it's the class-specific
implements case; update the guard around p.at_top_level_keyword() so only
TokenKind::Function is exempt in the three non-class loops, and in parse_class
allow TokenKind::Function plus only the class-local implements token
(TokenKind::Implements/Implement) if that was the intent—remove
TokenKind::Interface and the broad implements exemption from the other loops;
locate the checks using at_top_level_keyword(), TokenKind::Function,
TokenKind::Interface, TokenKind::Implements/Implement and adjust each loop
(parse_class and the three other recovery loops mentioned) accordingly so
invalid top-level items remain recovery boundaries.
- Around line 2648-2699: interface_method_has_body currently only skips "basic
trivia" which lets line/block comments between the signature and `{` confuse the
scanner; update the code in interface_method_has_body (the initial while that
advances i and the main loop that currently checks
self.is_basic_trivia(token.kind)) to also treat comment tokens as trivia (skip
TokenKind variants for comments, e.g., line/block comment kinds or via an
existing helper like is_comment_trivia or extend is_basic_trivia to include
comments) so braces inside comments are ignored and do not affect
paren/bracket/angle counting; add a regression test with a default interface
method containing intervening // and /* ... */ comments to validate the fix.

In `@baml_language/crates/baml_compiler_syntax/src/ast.rs`:
- Around line 163-171: The new AST node kinds (InterfaceDef / INTERFACE_DEF and
ImplementsFor / IMPLEMENTS_FOR) are registered but not exposed via Item::cast,
so SourceFile::items() silently drops them; update the Item type to include
variants for InterfaceDef and ImplementsFor (and any other newly added top-level
wrappers like ImplementsBlock/ImplementsTarget/InterfaceFieldLink if intended as
top-level) and extend Item::cast to map INTERFACE_DEF and IMPLEMENTS_FOR to
those Item variants so that SourceFile::items() iterates over and returns
interface declarations and out‑of‑body implements blocks correctly.

In `@baml_language/crates/baml_compiler2_emit/src/lib.rs`:
- Around line 198-222: collect_class_fields_with_implements currently only
iterates class_data.fields and thus omits fields contributed by implemented
interfaces; update this function to also traverse class_data.implements (or use
the TIR/HIR API to resolve each implemented interface/trait) and merge their
public field entries into out while still deduplicating via seen, preserving
field.type_expr, attributes, generic_params and pkg_ns; also apply the same fix
to the sibling helper that merges fields for implemented interfaces (the other
merged-field helper mentioned in the review) so both runtime class field/index
maps include interface-injected fields per BEP-044.

In `@baml_language/crates/baml_compiler2_mir/src/lower.rs`:
- Around line 5475-5480: The emitted interface-dispatch call currently uses
self.builder.call(callee_op, all_args, dest.clone(), bb_join, None) which omits
the active catch/unwind target and the type-argument info that lower_call
threads through; update this dispatch to use the same ABI path as lower_call by
passing the current unwind target and the type-argument list (not None) into
builder.call (or invoke lower_call directly) so that callee_op, all_args (which
should still start with Operand::Copy(Place::Local(recv_local)) plus arg_ops),
dest, and bb_join get the same unwind and generic/type metadata as
non-dispatched calls.
- Around line 5366-5392: try_lower_interface_dispatch only handles when
self.expr_types[base] is Tir2Ty::Interface, so generic receivers (type variables
with an interface bound) fall through; update try_lower_interface_dispatch to
also detect when the metadata type for base is a type variable whose
trait/bounds include the target interface (qtn / iface_type_args) and treat that
case like Tir2Ty::Interface: lookup the type-var bounds via your type/bound
tables, match an Interface bound for the same qtn and type args, and then
proceed to lower the receiver (using lower_to_operand/expr_ty and
operand_to_local as before) and call emit_interface_dispatch_switch; keep the
same local/operand handling (receiver_op, receiver_ty, recv_local) but add the
type-var-with-interface-bound branch before returning false so generic-bound
receivers use the tag-switch path (refer to try_lower_interface_dispatch,
expr_types, expr_metadata_key, lower_to_operand, expr_ty, operand_to_local, and
emit_interface_dispatch_switch).
- Around line 3694-3718: The code currently builds ItemRef::Method using
package/namespace from self.file and only iface_path.last(), causing
default.<method> to resolve to the current file's interface; instead resolve
target_te.expr to the interface's defining file/definition and take its
package/namespace from that definition. Concretely: in the branch that matches
TypeExpr::Path (target_te.expr) and computes iface_name, call the resolver/DB to
find the interface's definition/file (the declaration that path points to)
rather than using file_package::file_package(self.db, self.file), then call
file_package::file_package(self.db, def_file_or_def) (or equivalent API that
returns package/namespace for the resolved interface) and use that package and
namespace when constructing ItemRef::Method (keep using iface_name and
method_name as before); update implements_block_iface_target()/resolution logic
to obtain the def file if necessary.

In `@baml_language/crates/baml_compiler2_tir/src/builder.rs`:
- Around line 6407-6439: The path-root materialization helpers currently only
create classes/enums/type aliases so qualified names like Iface.method(...)
never become Ty::Interface and thus never hit resolve_interface_member; update
the helper(s) that materialize path roots (the functions that currently produce
class/enum/alias Ty variants) to also detect interface declarations from
package_items.lookup_type and construct a Ty::Interface (including its type_args
and any bound info or invoking try_resolve_generic_bound_alias where applicable)
so that later code reaches the Ty::Interface arm and calls
resolve_interface_member; ensure any related diagnostic/related-note behavior is
preserved when constructing the interface Ty.
- Around line 7460-7466: The current bindings initialization only seeds
interface-level generics when iface_type_args is non-empty, so method-level
generic parameters (sig.user_generic_params) are missing and get reduced to
recovery types during lower_type_expr_with_generics; to fix, ensure bindings
includes entries for the method's own generic params as TypeVar placeholders
before any lowering: when constructing bindings (the block that refers to
iface_type_args, bindings, and iface_data.generic_params), after binding
interface generics also seed the method generics (sig.user_generic_params) into
bindings (mirroring how lookup_class_method seeds method generics) so
lower_type_expr_with_generics sees those names as Ty::TypeVar; update the
bindings creation logic to add method-generic entries when iface_type_args is
present.
- Around line 8016-8024: qualified_interface_field_for_construction currently
returns None so interface-backed extra fields never trigger
InterfaceFieldRequiresQualifiedConstruction; implement it to look up whether
field_name belongs to a qualified interface on the given class and, if so,
return the interface field's declared Name and its Ty instantiated with
class_type_args. Specifically, in qualified_interface_field_for_construction use
class_name to find the class's implemented interfaces (or its metadata/AST
entry), search those interface declarations for a field named field_name,
perform type parameter substitution using class_type_args to produce the
concrete Ty, and return Some((field_name.clone(), instantiated_ty)); otherwise
return None.
- Around line 7451-7611: The code returns a function type for interface methods
without recording a callee resolution, so explicit type-arg handling in
resolve_explicit_type_args never runs; before returning fn_ty in the
interface-default branch (the loop over iface_data.default_methods that
constructs fn_ty) — and likewise in the other interface-method branch around
7614–7774 — record the callee resolution using the same mechanism the rest of
the builder uses to enable resolve_explicit_type_args: construct the appropriate
resolution variant (UnboundMethod if not bound, BoundMethod if bound, including
iface_name, fn_id, member/method name, iface_type_args and any explicit type
args resolved) and store it via the existing callee-resolution API used
elsewhere in this module so the explicit type-arg arity/bound checks run before
returning fn_ty.

In `@baml_language/crates/baml_compiler2_tir/src/lower_type_expr.rs`:
- Around line 57-107: The current substitute_paths_in/walk implementation skips
TypeExpr::Function nodes so any Self/bound path inside function parameters,
return type or throws is not substituted; add a match arm for TypeExpr::Function
(match the exact field names used in your enum, e.g., params, return_type,
throws, attrs) and recursively call walk on each parameter type, the return_type
(if present), and the throws type (if present), returning a new
TypeExpr::Function with those transformed fields; make the same change in the
other walker mentioned (the second function with the same walk logic) so
function-typed nodes are consistently handled.

In `@baml_language/crates/baml_compiler2_tir/src/normalize.rs`:
- Around line 425-431: The normalization added handling for Ty::Interface
(producing StructuralTy::Interface) but the recursion/cycle walkers (substitute,
ty_has_cycle, extract_type_alias_deps) were not updated to descend into
interface type arguments; update each of those functions to treat interface
nodes like class nodes by iterating and recursing into the interface's type_args
(the same way you already recurse into class type args), so alias cycles like
type A = I<A> are detected and substitution/cycle checks traverse
interface-contained types as well.

In `@baml_language/crates/baml_lsp2_actions/src/check.rs`:
- Around line 1228-1240: type_exprs_compatible currently compares TypeExprs by
a.to_string() == b.to_string(), which is too strict; update it to perform a
structural/semantic compatibility check instead of string equality by resolving
both baml_compiler2_ast::TypeExpr into their canonical/normalized forms or into
the compiler's TIR/Ty representation and then using the existing
subtype/compatibility logic (e.g., convert TypeExpr -> Ty and call the subtyping
check) so that subtypes and equivalent-but-differently-rendered types are
accepted; modify the function type_exprs_compatible to call the
resolver/normalizer and then use the proper subtype/equals routine rather than
relying on to_string().
- Around line 693-797: The traversal currently truncates parent identities by
using segments.last() and probing with a single-segment TypeExpr, which loses
qualification and causes wrong matches and incorrect visited checks; fix
collect_interface_members_with_subst by preserving the full path: when iterating
parent_te, match TypeExpr::Path { segments, .. } and if segments is non-empty,
use the entire segments.clone() (not just last) to build the probe
TypeExpr::Path (including empty generic_args/attrs as before) and change the
visited set to track the full path identity (e.g., HashSet<Vec<Name>> or another
representation of the full segments) so you call
visited.insert(segments.clone()) and skip only exact full-path repeats; apply
the same pattern used in interface_has_cycle to ensure the graph walk keeps
fully-qualified parent identities and probes resolve_interface_path with the
full segments.
- Around line 898-911: The code records implemented interfaces into
seen_target_names and seen_targets from raw syntax, which allows unresolved or
differently-namespaced references to bypass duplicate and
MissingRequiredInterface checks; instead, after calling resolve_interface_path
for each implements entry, use the resolved identity (e.g., the resolved Name or
InterfaceId returned by resolve_interface_path) as the key when inserting into
seen_target_names and seen_targets, and attach the original span
(block.target.span) to that resolved-key entry; update logic that currently uses
block.interface_name(), block.target.expr, and format!("{}", block.target.expr)
to reference the resolved symbol so duplicates like Foo vs ns.Foo are detected
and missing-required-interface checks (e.g., MissingRequiredInterface) use
resolved identities.

In `@baml_language/crates/baml_lsp2_actions/src/usages_at_tests.rs`:
- Around line 139-143: The assertion is brittle because it pins the fixture
coordinates to "test.baml:8:12"; in the assert! that checks
formatted.iter().any(|usage| usage.ends_with("-> Animal") &&
usage.starts_with("test.baml:8:12")), relax the coordinate check to only verify
the filename (e.g., use starts_with("test.baml:") or contains("test.baml")) so
the test still ensures the "-> Animal" usage is reported from the test.baml
fixture without depending on exact line/column positions; update the predicate
in the assert to use the relaxed check (replace starts_with("test.baml:8:12")
with starts_with("test.baml:") or contains("test.baml")).

---

Outside diff comments:
In `@baml_language/_plan/BEP-044-interfaces/IMPLEMENTATION_PLAN.md`:
- Around line 1-690: The IMPLEMENTATION_PLAN.md is obsolete and conflicts with
IMPLEMENTATIONv2.md and README.md; remove ambiguity by either deleting BEP-044
Interfaces — Implementation Fix Plan (the file shown) or prepend a clear
deprecation banner referencing IMPLEMENTATIONv2.md and indicating it supersedes
this document (so readers and implementers only follow IMPLEMENTATIONv2.md);
ensure the banner mentions IMPLEMENTATIONv2.md and README.md to make the
canonical source explicit.

In `@baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs`:
- Around line 65-75: find_callee_generic_args currently returns a child
GENERIC_ARGS even when callee_node is an UPCAST_EXPR (and thus picks upcast
target generics like `.as<T>` into Expr::Call.type_args); change the function so
it does not treat GENERIC_ARGS from UPCAST_EXPR (and related
upcast/optional-upcast kinds) as call-site type arguments — e.g., check
callee_node.kind() first and skip returning a child GENERIC_ARGS when kind() ==
SyntaxKind::UPCAST_EXPR (and/or SyntaxKind::OPTIONAL_FIELD_ACCESS_EXPR if
appropriate), leaving the existing behavior for FIELD_ACCESS_EXPR and other
call-site forms; update the logic in find_callee_generic_args to only return
GENERIC_ARGS for actual call-site nodes so Expr::Call.type_args no longer
receives upcast target types.

In `@baml_language/crates/baml_lsp2_actions/src/describe.rs`:
- Around line 687-701: The item-node filter in is_item_node currently omits
interface definition nodes, causing find_item_range and docstring extraction
used by describe() to skip top-level interfaces; update is_item_node to include
the interface SyntaxKind variant (e.g., add SyntaxKind::INTERFACE_DEF) so
interface definitions are treated as item nodes, and verify related logic in
find_item_range and describe still applies (also mirror this addition where
equivalent item-node checks exist around the dependency handling added at lines
~937-948).

---

Nitpick comments:
In `@baml_language/_plan/BEP-044-interfaces/meta.json`:
- Line 9: Update the "updated" field in meta.json to match the latest
substantial revision date (e.g., change "updated": "2026-05-05" to "2026-05-20"
to align with IMPLEMENTATIONv2.md header); locate the "updated" property in
meta.json and set it to the most recent revision date that reflects changes
described in IMPLEMENTATIONv2.md so the BEP metadata is accurate.

In `@baml_language/_plan/BEP-044-interfaces/README.md`:
- Line 783: Reword the sentence that reads "extends (as in TS / Java / earlier
drafts of this BEP)" to remove the ambiguous "earlier drafts of this BEP"
reference and explicitly clarify the history: either remove that clause or
replace it with a short, clear note such as "extends (as in TS / Java —
previously used in IMPLEMENTATION_PLAN.md)" so readers understand that
IMPLEMENTATION_PLAN.md used extends and that the document now prefers requires;
update the sentence referencing 'requires' and 'extends' accordingly to avoid
confusion.

In `@baml_language/crates/baml_compiler_lexer/src/tokens.rs`:
- Around line 48-57: Add a unit test that feeds the lexer the string "interface
implements implement extends requires" and asserts the produced token sequence
contains the single tokens Interface, Implements, Implement, Extends, and
Requires (in that order); specifically assert that "implements" produces one
Implements token rather than an Implement token plus a Word/Identifier "s".
Locate the test near other lexer tests and call the public tokenize/lex function
used in your crate (e.g., tokenize, lex, or Lexer::next_tokens) and compare the
token enum values (Token::Interface, Token::Implements, Token::Implement,
Token::Extends, Token::Requires); include any necessary whitespace handling so
the lexer sees separate lexemes.

In `@baml_language/crates/baml_compiler2_ast/src/lower_cst.rs`:
- Around line 1444-1457: The diagnostic for missing field names in the
interface->class mapping uses class_name: "interface field link", which is
misleading; update the LoweringDiagnostic usage in the block handling
link.interface_field() and link.class_field() to provide a clearer context
string (e.g., "implements block field mapping") or add a new LoweringDiagnostic
variant specifically for interface-to-class field links and use that variant
instead; adjust the diags.push calls around link.interface_field() and
link.class_field() and update any matching match arms or consumers of
LoweringDiagnostic so the new string/variant is handled consistently.

In `@baml_language/crates/baml_compiler2_hir/src/builder.rs`:
- Around line 1091-1121: The interface contribution ordering is inconsistent
with lower_class; update lower_interface to either move the type contribution
before calling push_scope to match lower_class or (preferably) add a brief
explanatory comment above the push_scope call explaining why the contribution
must occur after lowering default methods (because default_method_ids are
computed inside the Class-kind scope and the interface's local_id is allocated
only after collecting them). Reference lower_interface, push_scope,
default_method_ids, item_tree.alloc_interface, and type_contributions when
making the change so future maintainers understand the intentional ordering.

In `@baml_language/crates/baml_compiler2_tir/src/interfaces.rs`:
- Around line 169-188: The db parameter of resolve_path_to_interface is unused
(silenced by let _ = db); either remove it from the function signature and
update all callers to stop passing a Db, or keep it and add a short comment
explaining it's intentionally reserved for future use (e.g., "kept for potential
lookup needs") and remove the let _ = db line; locate the function by name
resolve_path_to_interface and adjust callers/exports accordingly to ensure
compilation after removing the parameter or add the explanatory comment if
retaining it.

In `@baml_language/crates/bex_vm_types/src/types.rs`:
- Around line 95-103: Add serde's default attribute to the Program
interface_implementors field so deserialization will treat a missing field as an
empty map: annotate the field `interface_implementors:
IndexMap<baml_type::TypeName, Vec<baml_type::TypeName>>` with
`#[serde(default)]` (ensure serde feature is enabled where needed); this makes
`Program::interface_implementors` default to empty on older/variant payloads and
prevents deserialization errors across versions.
🪄 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: 2c5f69ca-66a5-448f-bd3b-6221d96034d8

📥 Commits

Reviewing files that changed from the base of the PR and between 2ee067a and 865c343.

⛔ Files ignored due to path filters (5)
  • 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____06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/attribute_validation/baml_tests__diagnostic_errors__attribute_validation__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/src/compiler2_tir/snapshots/baml_tests__compiler2_tir__phase5__snapshot_baml_package_items.snap is excluded by !**/*.snap
📒 Files selected for processing (117)
  • baml_language/_plan/BEP-044-interfaces/IMPLEMENTATION_PLAN.md
  • baml_language/_plan/BEP-044-interfaces/IMPLEMENTATIONv2.md
  • baml_language/_plan/BEP-044-interfaces/README.md
  • baml_language/_plan/BEP-044-interfaces/meta.json
  • baml_language/crates/baml_builtins2/baml_std/baml/type_class.baml
  • baml_language/crates/baml_builtins2_codegen/src/extract.rs
  • baml_language/crates/baml_compiler2_ast/src/ast.rs
  • baml_language/crates/baml_compiler2_ast/src/auto_derive_json.rs
  • baml_language/crates/baml_compiler2_ast/src/companions.rs
  • baml_language/crates/baml_compiler2_ast/src/lower_cst.rs
  • baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs
  • baml_language/crates/baml_compiler2_ast/src/lowering_diagnostic.rs
  • baml_language/crates/baml_compiler2_emit/src/lib.rs
  • baml_language/crates/baml_compiler2_hir/src/builder.rs
  • baml_language/crates/baml_compiler2_hir/src/contributions.rs
  • baml_language/crates/baml_compiler2_hir/src/diagnostic.rs
  • baml_language/crates/baml_compiler2_hir/src/ids.rs
  • baml_language/crates/baml_compiler2_hir/src/item_tree.rs
  • baml_language/crates/baml_compiler2_hir/src/loc.rs
  • baml_language/crates/baml_compiler2_mir/src/lower.rs
  • baml_language/crates/baml_compiler2_ppir/src/lib.rs
  • baml_language/crates/baml_compiler2_tir/architecture-slides.html
  • baml_language/crates/baml_compiler2_tir/src/builder.rs
  • baml_language/crates/baml_compiler2_tir/src/exhaustiveness.rs
  • baml_language/crates/baml_compiler2_tir/src/infer_context.rs
  • baml_language/crates/baml_compiler2_tir/src/inference.rs
  • baml_language/crates/baml_compiler2_tir/src/interfaces.rs
  • baml_language/crates/baml_compiler2_tir/src/lib.rs
  • baml_language/crates/baml_compiler2_tir/src/lower_type_expr.rs
  • baml_language/crates/baml_compiler2_tir/src/normalize.rs
  • baml_language/crates/baml_compiler2_tir/src/package_interface.rs
  • baml_language/crates/baml_compiler2_tir/src/throws_analysis.rs
  • baml_language/crates/baml_compiler2_tir/src/ty.rs
  • baml_language/crates/baml_compiler_diagnostics/src/diagnostic.rs
  • baml_language/crates/baml_compiler_lexer/src/tokens.rs
  • baml_language/crates/baml_compiler_parser/src/parser.rs
  • baml_language/crates/baml_compiler_syntax/src/ast.rs
  • baml_language/crates/baml_compiler_syntax/src/syntax_kind.rs
  • baml_language/crates/baml_lsp2_actions/src/check.rs
  • baml_language/crates/baml_lsp2_actions/src/completions.rs
  • baml_language/crates/baml_lsp2_actions/src/describe.rs
  • baml_language/crates/baml_lsp2_actions/src/listing_tests.rs
  • baml_language/crates/baml_lsp2_actions/src/type_info.rs
  • baml_language/crates/baml_lsp2_actions/src/type_info_tests.rs
  • baml_language/crates/baml_lsp2_actions/src/usages_at_tests.rs
  • baml_language/crates/baml_lsp2_actions/src/utils.rs
  • baml_language/crates/baml_lsp2_actions_tests/test_files/completion/top_level_keywords.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/class/dependency_cycle.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/class/incomplete_class.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/class/spelling_error.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/class/unknown_type.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/class/valid_classes.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/client/remap_role_invalid_type.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/expr/constructors_invalid.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/hover/out_of_body_implements_class.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/ambiguous_method.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/conflicting_field_types.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/duplicate_implements.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_ambiguous_interface_field_access.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_ambiguous_method_call.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_as_downcast_interface_to_child.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_concrete_field_through_interface_type.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_default_on_required_method.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_default_outside_implements.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_duplicate_interface_field_link.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_field_declared_in_implements_block.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_generic_bound_violation.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_interface_extends_field_conflict.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_invariant_generic_assignment.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_match_without_wildcard.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_missing_field_in_implements_block.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_missing_required_interface.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_multi_self_call_on_interface_receiver.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_old_qualified_projection.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_out_of_body_field_interface.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_qualified_call_non_implementor.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_qualified_call_unknown_interface.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_self_outside_type.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_unknown_class_field_link.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_unknown_interface_field_link.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/extends_cycle.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/field_type_mismatch.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/method_signature_mismatch.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/missing_required_method.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/unknown_interface.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/unknown_interface_member.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_as_upcast_child_interface_to_parent.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_basic.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_class_methods_outside_interface.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_default_call_from_override.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_default_methods.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_diamond_independent_resolution.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_extends_chain_three_levels.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_field_rule_3_merged_same_type.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_field_rule_5_subtype_field.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_generic_bound.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_generic_concrete_type_param.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_generic_different_type_params_on_one_class.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_generic_interface_bound_instantiation.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_implement_singular.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_implements_for_class.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_inheritance.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_interface_field_alias.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_interface_field_via_extends.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_interface_typed_var_method_call.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_match_narrows_to_concrete.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_match_with_wildcard.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_qualified_method_call.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_self_qualified_call_in_implements.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_self_return_interface_receiver.baml
  • baml_language/crates/baml_project/src/client_codegen.rs
  • baml_language/crates/baml_tests/tests/interfaces.rs
  • baml_language/crates/bex_engine/src/lib.rs
  • baml_language/crates/bex_project/src/bex_lsp/multi_project/request.rs
  • baml_language/crates/bex_vm/src/package_baml/type_class.rs
  • baml_language/crates/bex_vm/src/vm.rs
  • baml_language/crates/bex_vm_types/src/types.rs

@vercel
vercel Bot temporarily deployed to Preview – beps May 21, 2026 19:13 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 May 21, 2026 19:31 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: 9

Caution

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

⚠️ Outside diff range comments (6)
baml_language/crates/baml_compiler2_tir/src/builder.rs (1)

7370-7410: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Inherited generic interfaces are lowered without their instantiated type arguments.

Only root_iface_loc gets iface_type_args; every parent reached through interface_closure_locs is lowered with empty bindings. That means Child<int> requires Parent<int> will still resolve Parent<T> members as T here, so inherited generic fields/methods keep type vars instead of the concrete instantiation. The same raw-closure traversal also backs qualified construction, so the fix should be shared.

baml_language/crates/baml_compiler2_tir/src/interfaces.rs (1)

366-369: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Resolve each transitive parent in its own namespace.

interface_closure_locs keeps passing the original current_ns through the whole BFS. Once traversal reaches an interface declared in another namespace, its extends clauses are resolved relative to the wrong scope, so ancestors can be missed or mis-resolved. interface_closure already does the right thing by recomputing the namespace from loc.file(db).

🤖 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/interfaces.rs` around lines 366 -
369, interface_closure_locs incorrectly reuses the original current_ns for all
BFS steps causing extends clauses to be resolved in the wrong namespace; update
the BFS so that when you discover a parent interface via
resolve_path_to_interface(db, &parent.expr, pkg_items, current_ns) you recompute
the namespace from the parent's location (use parent_loc.file(db) or the same
approach used in interface_closure to derive the new current_ns) and pass that
recomputed namespace into subsequent resolve_path_to_interface calls and when
pushing into interface_closure_locs, ensuring each transitive parent is resolved
relative to its own file/namespace.
baml_language/crates/baml_compiler2_tir/src/inference.rs (2)

611-637: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Include enclosing interface generics in method inference.

This merge only pulls generic params from a parent ScopeKind::Class. For interface BoxLike<T> { func unwrap(self) -> T }, T never enters generic_params, so interface method signatures lower to Unknown and report bogus unresolved-type diagnostics.

🤖 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/inference.rs` around lines 611 -
637, The code only merges parent generics when parent.kind is ScopeKind::Class,
so interface-level generics are never added; update the parent-kind check to
handle ScopeKind::Interface as well and look up the corresponding interface
entry in item_tree (e.g. item_tree.interfaces) just like you do for classes:
check for name match, report method-level type param shadowing against
interface.generic_params using builder.report_at_span and
TirTypeError::TypeParamShadowed, then merge interface.generic_params into the
existing generic_params (same pattern as class_data.generic_params) before
continuing.

696-788: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Derive Self and implicit self from interface parents too.

enclosing_class_name is only populated for class parents, but the same lowering path is used for interface default methods. In an interface method like func clone(self) -> Self, both Self substitution and the unannotated self fallback currently collapse to Unknown, so the method loses the interface receiver contract during inference.

🤖 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/inference.rs` around lines 696 -
788, The code only sets enclosing_class_name when the parent scope is
ScopeKind::Class, which prevents Self substitution and implicit unannotated
`self` handling for interface default methods; update the logic that computes
enclosing_class_name (the closure using scope.parent and reading
index.scopes[..].kind) to treat ScopeKind::Interface the same as
ScopeKind::Class so self_replacement, lower_with_self, and the implicit `self`
branch (the param handling that calls pkg_items.lookup_type and matches
Definition::Interface) can find and use the enclosing interface name.
baml_language/crates/baml_compiler_parser/src/parser.rs (1)

2804-2819: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

These recovery guards make keyword-named members unreachable.

Both loops break on any top-level keyword before looks_like_interface_field_link() / at_member_name() run. That means entries like interface as real_field or implements string can never parse inside implements bodies, even though the later helpers explicitly allow those tokens there.

Also applies to: 2893-2908

🤖 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 2804 -
2819, The recovery guard that breaks on p.at_top_level_keyword() runs too early
and prevents parsing keyword-named members; move or narrow that break so the
parser first checks for function, block-attribute function
(item_keyword_after_leading_block_attributes()),
looks_like_interface_field_link()/parse_interface_field_link() and
at_member_name()/parse_field() before deciding to break. Concretely, reorder the
checks in the loop so the branches that call parse_function(),
parse_interface_field_link(), and parse_field() run before invoking
p.at_top_level_keyword() (or restrict the break condition to only apply when not
inside a implements/interface body), keeping the existing helpers
item_keyword_after_leading_block_attributes(),
looks_like_interface_field_link(), at_member_name(), parse_function(),
parse_interface_field_link(), and parse_field() intact.
baml_language/crates/baml_compiler2_mir/src/lower.rs (1)

3962-3976: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Propagate type arguments through default.<method>(...).

This early-return path still emits a plain builder.call(...) with only self plus value args, so default.foo<T>() drops explicit type arguments and diverges from the normal lower_call ABI. Please route this through the same call_with_type_args flow before returning.

🤖 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 3962 -
3976, The early-return path that handles `default.<method>(...)` currently
constructs a callee and args then calls builder.call(...) which drops explicit
type arguments; instead, route this path through the existing
call_with_type_args flow. Replace the builder.call(...) invocation in this
branch with a call to the same helper used for normal lowers
(call_with_type_args) passing the constructed callee_op (ItemRef::Method wrapped
in Constant::Function), the assembled all_args (including
Operand::Copy(Place::Local(self_local)) and lower_call_arg_operands(...)), the
dest, the created target, the unwind from catch_context, and the type-argument
list used by normal lower_call so the explicit type parameters are preserved.
Ensure you reuse the same target/unwind/dest semantics as the original
builder.call invocation.
♻️ Duplicate comments (1)
baml_language/crates/baml_compiler_parser/src/parser.rs (1)

2464-2473: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't exempt top-level implements ... for ... from class-body recovery.

This still treats every implements token as class-local. If a class is missing its closing }, the next top-level implements Named for int { ... } will enter parse_implements_block(), fail on for, and corrupt recovery for the rest of the file. Only the in-class form should bypass the recovery boundary here. A small unit test next to interface_keyword_recovers_as_top_level_after_missing_class_brace would pin this down.

🤖 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 2464 -
2473, The current recovery condition incorrectly exempts
TokenKind::Implements/Implement as if they were class-local, allowing a
top-level "implements ... for ..." to be parsed inside a missing-class body and
break recovery; update the condition around recover_top_level_item so only the
in-class form bypasses the recovery boundary — remove the special-case
exemptions for p.at(TokenKind::Implements) and p.at(TokenKind::Implement) (i.e.,
do not treat those tokens as class-local here) and rely on the existing
looks_like_interface_declaration_start()/p.at(TokenKind::Interface) check to
permit in-class behavior; add a unit test (next to
interface_keyword_recovers_as_top_level_after_missing_class_brace) that ensures
a missing class '}' followed by a top-level "implements Named for int { ... }"
is recovered as a top-level item rather than entering parse_implements_block().
🧹 Nitpick comments (3)
baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs (1)

73-79: ⚡ Quick win

Add a unit test for upcasted receiver calls.

The UPCAST_EXPR bailout here is subtle enough that a small lowering unit test covering Type<T>.method() vs expr.as<I>.method() would make regressions in call-site generic extraction much harder to miss.

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/baml_compiler2_ast/src/lower_expr_body.rs` around lines
73 - 79, Add a Rust unit test in the same module to cover upcasted receiver
calls so the UPCAST_EXPR bailout isn't regressed: write a #[cfg(test)] mod tests
with cases that parse & lower a field call where the receiver is a generic type
(e.g. "Type<T>.method()" or "value.method()" that produces a callee_node whose
first child is not UPCAST_EXPR) and a case where the receiver is an explicit
upcast (e.g. "expr.as<I>.method()") and assert the lowering behavior differs
(the former extracts GENERIC_ARGS from the base while the latter follows the
current code path that returns None for UPCAST_EXPR); reference
SyntaxKind::FIELD_ACCESS_EXPR, SyntaxKind::OPTIONAL_FIELD_ACCESS_EXPR,
callee_node and SyntaxKind::UPCAST_EXPR to locate the matching code and invoke
the lowering function exercised by lower_expr_body.rs.
baml_language/crates/baml_compiler2_emit/src/lib.rs (1)

340-342: ⚡ Quick win

Add an emit unit test for the new runtime-field contract.

These lines change the intended semantics from “merge interface fields” to “emit only concrete class storage”. A focused unit test here would lock that behavior down and prevent future regressions back to interface-backed runtime slots.

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/baml_compiler2_emit/src/lib.rs` around lines 340 - 342,
Add a Rust unit test in this crate's lib.rs (under #[cfg(test)]) that verifies
the BEP-044 runtime-field contract: construct a small IR where a class
implements an interface that declares fields, call the emitter path that
collects/returns runtime fields (the code guarded by the "collect only the
class's actual runtime fields" comment), and assert that only the concrete class
storage fields are emitted and interface-declared fields are not present; name
it something like test_emit_only_concrete_class_storage_runtime_fields and use
existing IR/test helpers in this module to build the class/interface and invoke
the emitter function responsible for collecting/emitting runtime fields.
baml_language/crates/baml_compiler2_hir/src/builder.rs (1)

1036-1076: 💤 Low value

Duplicate detection may miss methods from implements blocks.

The duplicate detection at lines 1036-1053 iterates over c.fields and c.methods, but lines 1065-1076 lower additional methods from c.implements[].methods that are added to method_ids without being checked for name collisions.

If a class declares fn foo() at the class level and also provides fn foo() inside an implements I { } block, no duplicate diagnostic would be emitted here.

♻️ Suggested addition to include implements-block methods in duplicate detection
     for method in &c.methods {
         seen.entry(method.name.clone())
             .or_default()
             .push(MemberSite {
                 range: method.name_span,
                 kind: DefinitionKind::Method,
             });
     }
+    for impl_block in &c.implements {
+        for method in &impl_block.methods {
+            seen.entry(method.name.clone())
+                .or_default()
+                .push(MemberSite {
+                    range: method.name_span,
+                    kind: DefinitionKind::Method,
+                });
+        }
+    }
 
     self.emit_duplicate_diagnostics(seen);
🤖 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_hir/src/builder.rs` around lines 1036 -
1076, The duplicate-detection map `seen` is only populated from `c.fields` and
`c.methods`, so methods coming from `c.implements[].methods` are never checked;
before calling self.emit_duplicate_diagnostics(seen) add entries for each method
in each impl block (using the same MemberSite with range = method.name_span and
kind = DefinitionKind::Method) so impl-block methods are recorded and will
trigger duplicates; keep the existing lowering (lower_function -> fid,
item_tree.method_to_iface_target insert, and pushing to method_ids) but ensure
the impl methods are also inserted into `seen` first.
🤖 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_compiler_parser/src/parser.rs`:
- Around line 912-956: The helper looks_like_interface_declaration_start
incorrectly stops on TokenKind::Newline; update it to keep scanning across
newlines the same way parse_interface allows trivia so multiline declarations
recover properly—locate looks_like_interface_declaration_start and remove the
early-return branch that maps TokenKind::Newline to false, instead treat Newline
like Whitespace (i.e., advance i and continue) and keep the existing handling
for LBrace, RBrace, Semicolon and other tokens; ensure the
skip_trivia_and_comments_from / skip_comment_at / skip_header_comment_at logic
remains used as before.

In `@baml_language/crates/baml_compiler2_mir/src/lower.rs`:
- Around line 5371-5381: The call to try_lower_interface_field_access is
currently ignoring its bool result, which causes the branch to short-circuit
when interface dispatch fails; change the code to check the returned bool from
try_lower_interface_field_access (using its result) and only return/skip further
handling if it returns true, otherwise allow execution to fall through to the
existing union/map/error-recovery code paths so dest can still be assigned;
locate the branch that uses interface_receiver_for_field_access and use the bool
from try_lower_interface_field_access (passing expr_id, base_local, &iface_tn,
&iface_type_args, field, &dest) to decide whether to stop or continue.
- Around line 5733-5741: The current loop skips implementors when
type_tag_for_ty(impl_ty) returns None, so non-class concrete types (uint8array,
enums, lists, maps) get omitted from dispatch; instead, keep those implementors
by recording the None tag and emitting alternative dispatch arms later: change
the push to include the tag option (or otherwise record impl_ty.to_string()) for
every implementor found by resolve_type_implementor_method, and update the
switch-emission logic that consumes interface_type_implementors/resolved to
generate runtime type-name or structural checks for entries with a None tag
(i.e., emit branches that match by impl_ty string or appropriate is-X runtime
test when tag is None) so non-class implementors are covered.
- Around line 1018-1022: The code currently drops instantiated type arguments
for non-root interfaces by setting iface_args = Vec::new() whenever iface_loc !=
root_iface_loc; instead ensure you preserve the actual instantiated type
arguments for that interface (e.g. look them up from whatever instantiation
mapping or the interface's metadata) rather than using an empty Vec: replace the
Vec::new() branch with a call that retrieves the instantiated args for iface_loc
(or clone the existing instantiation entry) so iface_args reflects the concrete
type parameters; apply the same fix to the other occurrences that use Vec::new()
in this walk (the similar blocks around the other noted spots).

In `@baml_language/crates/baml_compiler2_tir/src/builder.rs`:
- Around line 467-470: interface_method_generic_params is an arena-local side
table keyed by ExprId but is not being saved/restored when swapping inference
state in check_function_parameter_defaults and infer_lambda_body, which lets
stale ExprId entries leak across nested/default bodies; update the same
save/restore logic that handles expressions and resolutions to include
interface_method_generic_params so it is pushed before swapping and restored
after (i.e., treat interface_method_generic_params as part of the arena-local
inference state used by resolve_explicit_type_args).
- Around line 1395-1448: Unbound interface calls ignore the interface's generic
args; when you use self.interface_method_generic_params to build (callee_name,
declared_params) also fetch any stored interface-level generic params (e.g. from
a map like self.interface_instantiation_generic_params or similar) and prepend
them to declared_params just like class_params is prepended for static methods,
keeping func_loc_for_bound_checks as None; update the branch that returns
(callee_name, declared_params, None) so declared_params =
[interface_generic_params..., method_user_generic_params...] using the same Name
type.

In `@baml_language/crates/baml_compiler2_tir/src/interfaces.rs`:
- Around line 75-105: implementation_key_for_ty currently preserves union member
order so Ty::Union keys are order-sensitive; canonicalize union members by
mapping each member with implementation_key_for_ty, filtering out None results,
deduping and sorting the resulting Vec (stable ordering) before building
Ty::Union so that unions like int|string and string|int produce the same
implementation key; update the Ty::Union arm to collect Option<Vec<_>> into a
Vec, sort and dedupe it (using a deterministic comparison of the element Ty
values or their serialized/key form) and then wrap into
Some(Ty::Union(sorted_deduped_vec, TyAttr::default())).

In `@baml_language/crates/baml_lsp2_actions/src/check.rs`:
- Around line 941-1079: validate_implements_for currently only checks
imp.methods and flags any other required interface methods as missing; instead
resolve the target type from imp.for_target (using the same resolution used
elsewhere), locate the target class/impl block and include its declared methods,
inherited/sibling implementations and class-wide `requires` logic when building
the set of provided methods, and then run the same satisfaction checks used by
validate_class_implements (or extract its core check into a shared helper)
against the members returned by collect_interface_members_with_subst; update
validate_implements_for to consult the target's existing method set (not only
imp.methods) before emitting MissingInterfaceMethod or signature-mismatch
diagnostics.

In `@baml_language/crates/baml_lsp2_actions/src/definition.rs`:
- Around line 410-412: The go-to-definition path adds InterfaceDefaultMethod to
the MemberResolution match but definition_at still returns early for any token
that is not a WORD, so keyword-named members (lexed as KW_*) never reach this
resolution. Update definition_at to treat keyword tokens used as identifiers the
same as WORD tokens (e.g., accept KW_* or normalize KW_* to WORD before lookup)
so reflection-style members like "implements" are allowed through; change the
early bail-out that checks token kind to include KW_* (or add a helper like
token_is_identifier_or_keyword) and keep the MemberResolution match
(MemberResolution::BoundMethod, ::UnboundMethod, ::InterfaceDefaultMethod) as-is
to resolve keyword-named members.

---

Outside diff comments:
In `@baml_language/crates/baml_compiler_parser/src/parser.rs`:
- Around line 2804-2819: The recovery guard that breaks on
p.at_top_level_keyword() runs too early and prevents parsing keyword-named
members; move or narrow that break so the parser first checks for function,
block-attribute function (item_keyword_after_leading_block_attributes()),
looks_like_interface_field_link()/parse_interface_field_link() and
at_member_name()/parse_field() before deciding to break. Concretely, reorder the
checks in the loop so the branches that call parse_function(),
parse_interface_field_link(), and parse_field() run before invoking
p.at_top_level_keyword() (or restrict the break condition to only apply when not
inside a implements/interface body), keeping the existing helpers
item_keyword_after_leading_block_attributes(),
looks_like_interface_field_link(), at_member_name(), parse_function(),
parse_interface_field_link(), and parse_field() intact.

In `@baml_language/crates/baml_compiler2_mir/src/lower.rs`:
- Around line 3962-3976: The early-return path that handles
`default.<method>(...)` currently constructs a callee and args then calls
builder.call(...) which drops explicit type arguments; instead, route this path
through the existing call_with_type_args flow. Replace the builder.call(...)
invocation in this branch with a call to the same helper used for normal lowers
(call_with_type_args) passing the constructed callee_op (ItemRef::Method wrapped
in Constant::Function), the assembled all_args (including
Operand::Copy(Place::Local(self_local)) and lower_call_arg_operands(...)), the
dest, the created target, the unwind from catch_context, and the type-argument
list used by normal lower_call so the explicit type parameters are preserved.
Ensure you reuse the same target/unwind/dest semantics as the original
builder.call invocation.

In `@baml_language/crates/baml_compiler2_tir/src/inference.rs`:
- Around line 611-637: The code only merges parent generics when parent.kind is
ScopeKind::Class, so interface-level generics are never added; update the
parent-kind check to handle ScopeKind::Interface as well and look up the
corresponding interface entry in item_tree (e.g. item_tree.interfaces) just like
you do for classes: check for name match, report method-level type param
shadowing against interface.generic_params using builder.report_at_span and
TirTypeError::TypeParamShadowed, then merge interface.generic_params into the
existing generic_params (same pattern as class_data.generic_params) before
continuing.
- Around line 696-788: The code only sets enclosing_class_name when the parent
scope is ScopeKind::Class, which prevents Self substitution and implicit
unannotated `self` handling for interface default methods; update the logic that
computes enclosing_class_name (the closure using scope.parent and reading
index.scopes[..].kind) to treat ScopeKind::Interface the same as
ScopeKind::Class so self_replacement, lower_with_self, and the implicit `self`
branch (the param handling that calls pkg_items.lookup_type and matches
Definition::Interface) can find and use the enclosing interface name.

In `@baml_language/crates/baml_compiler2_tir/src/interfaces.rs`:
- Around line 366-369: interface_closure_locs incorrectly reuses the original
current_ns for all BFS steps causing extends clauses to be resolved in the wrong
namespace; update the BFS so that when you discover a parent interface via
resolve_path_to_interface(db, &parent.expr, pkg_items, current_ns) you recompute
the namespace from the parent's location (use parent_loc.file(db) or the same
approach used in interface_closure to derive the new current_ns) and pass that
recomputed namespace into subsequent resolve_path_to_interface calls and when
pushing into interface_closure_locs, ensuring each transitive parent is resolved
relative to its own file/namespace.

---

Duplicate comments:
In `@baml_language/crates/baml_compiler_parser/src/parser.rs`:
- Around line 2464-2473: The current recovery condition incorrectly exempts
TokenKind::Implements/Implement as if they were class-local, allowing a
top-level "implements ... for ..." to be parsed inside a missing-class body and
break recovery; update the condition around recover_top_level_item so only the
in-class form bypasses the recovery boundary — remove the special-case
exemptions for p.at(TokenKind::Implements) and p.at(TokenKind::Implement) (i.e.,
do not treat those tokens as class-local here) and rely on the existing
looks_like_interface_declaration_start()/p.at(TokenKind::Interface) check to
permit in-class behavior; add a unit test (next to
interface_keyword_recovers_as_top_level_after_missing_class_brace) that ensures
a missing class '}' followed by a top-level "implements Named for int { ... }"
is recovered as a top-level item rather than entering parse_implements_block().

---

Nitpick comments:
In `@baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs`:
- Around line 73-79: Add a Rust unit test in the same module to cover upcasted
receiver calls so the UPCAST_EXPR bailout isn't regressed: write a #[cfg(test)]
mod tests with cases that parse & lower a field call where the receiver is a
generic type (e.g. "Type<T>.method()" or "value.method()" that produces a
callee_node whose first child is not UPCAST_EXPR) and a case where the receiver
is an explicit upcast (e.g. "expr.as<I>.method()") and assert the lowering
behavior differs (the former extracts GENERIC_ARGS from the base while the
latter follows the current code path that returns None for UPCAST_EXPR);
reference SyntaxKind::FIELD_ACCESS_EXPR, SyntaxKind::OPTIONAL_FIELD_ACCESS_EXPR,
callee_node and SyntaxKind::UPCAST_EXPR to locate the matching code and invoke
the lowering function exercised by lower_expr_body.rs.

In `@baml_language/crates/baml_compiler2_emit/src/lib.rs`:
- Around line 340-342: Add a Rust unit test in this crate's lib.rs (under
#[cfg(test)]) that verifies the BEP-044 runtime-field contract: construct a
small IR where a class implements an interface that declares fields, call the
emitter path that collects/returns runtime fields (the code guarded by the
"collect only the class's actual runtime fields" comment), and assert that only
the concrete class storage fields are emitted and interface-declared fields are
not present; name it something like
test_emit_only_concrete_class_storage_runtime_fields and use existing IR/test
helpers in this module to build the class/interface and invoke the emitter
function responsible for collecting/emitting runtime fields.

In `@baml_language/crates/baml_compiler2_hir/src/builder.rs`:
- Around line 1036-1076: The duplicate-detection map `seen` is only populated
from `c.fields` and `c.methods`, so methods coming from `c.implements[].methods`
are never checked; before calling self.emit_duplicate_diagnostics(seen) add
entries for each method in each impl block (using the same MemberSite with range
= method.name_span and kind = DefinitionKind::Method) so impl-block methods are
recorded and will trigger duplicates; keep the existing lowering (lower_function
-> fid, item_tree.method_to_iface_target insert, and pushing to method_ids) but
ensure the impl methods are also inserted into `seen` first.
🪄 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: 6bec940c-22f7-4d20-a14b-6865182a44e7

📥 Commits

Reviewing files that changed from the base of the PR and between 865c343 and 4663d3c.

⛔ Files ignored due to path filters (16)
  • 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/__testing_std__/baml_tests__compiles____testing_std____04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/comment_after_string_in_config/baml_tests__compiles__comment_after_string_in_config__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/comment_in_type/baml_tests__compiles__comment_in_type__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/config_dictionary/baml_tests__compiles__config_dictionary__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/config_model_string/baml_tests__compiles__config_model_string__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/json_llm_return_type/baml_tests__compiles__json_llm_return_type__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/llm_image_outputs/baml_tests__compiles__llm_image_outputs__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/o1_allowed_roles/baml_tests__compiles__o1_allowed_roles__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/optional_function_parameters/baml_tests__compiles__optional_function_parameters__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/retry_policy/baml_tests__compiles__retry_policy__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/stream_llm_inferred_typeargs/baml_tests__compiles__stream_llm_inferred_typeargs__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/testset_vibes_nested/baml_tests__compiles__testset_vibes_nested__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/generics/baml_tests__diagnostic_errors__generics__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/generics/baml_tests__diagnostic_errors__generics__05_diagnostics.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/type_aliases/baml_tests__diagnostic_errors__type_aliases__04_tir.snap is excluded by !**/*.snap
📒 Files selected for processing (29)
  • baml_language/crates/baml_compiler2_ast/src/ast.rs
  • baml_language/crates/baml_compiler2_ast/src/auto_derive_json.rs
  • baml_language/crates/baml_compiler2_ast/src/companions.rs
  • baml_language/crates/baml_compiler2_ast/src/lib.rs
  • baml_language/crates/baml_compiler2_ast/src/lower_cst.rs
  • baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs
  • baml_language/crates/baml_compiler2_emit/src/lib.rs
  • baml_language/crates/baml_compiler2_hir/src/builder.rs
  • baml_language/crates/baml_compiler2_hir/src/item_tree.rs
  • baml_language/crates/baml_compiler2_mir/src/lower.rs
  • baml_language/crates/baml_compiler2_ppir/src/lib.rs
  • baml_language/crates/baml_compiler2_tir/src/builder.rs
  • baml_language/crates/baml_compiler2_tir/src/inference.rs
  • baml_language/crates/baml_compiler2_tir/src/interfaces.rs
  • baml_language/crates/baml_compiler2_tir/src/lower_type_expr.rs
  • baml_language/crates/baml_compiler2_tir/src/normalize.rs
  • baml_language/crates/baml_compiler_parser/src/parser.rs
  • baml_language/crates/baml_compiler_syntax/src/ast.rs
  • baml_language/crates/baml_lsp2_actions/src/check.rs
  • baml_language/crates/baml_lsp2_actions/src/definition.rs
  • baml_language/crates/baml_lsp2_actions/src/usages_at_tests.rs
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_default_method_non_exhaustive.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_field_rule_invariant_union.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_generic_bound_alias_not_supported.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_method_throws_mismatch.baml
  • baml_language/crates/baml_tests/src/compiler2_tir/mod.rs
  • baml_language/crates/baml_tests/src/compiler2_tir/phase3a.rs
  • baml_language/crates/baml_tests/tests/interfaces.rs
  • baml_language/crates/tools_onionskin/src/compiler.rs
💤 Files with no reviewable changes (4)
  • baml_language/crates/baml_compiler2_ppir/src/lib.rs
  • baml_language/crates/baml_compiler2_ast/src/ast.rs
  • baml_language/crates/baml_compiler2_ast/src/auto_derive_json.rs
  • baml_language/crates/baml_compiler2_ast/src/companions.rs
✅ Files skipped from review due to trivial changes (2)
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_field_rule_invariant_union.baml
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/error_generic_bound_alias_not_supported.baml

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle May 21, 2026 19:32 Inactive
@codspeed

codspeed Bot commented May 21, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ Unknown Walltime execution environment detected

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

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

⚡ 1 improved benchmark
❌ 3 regressed benchmarks
✅ 15 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ WallTime e2e_arithmetic 96.3 ms 113.8 ms -15.37%
⚡ WallTime vm_mixed_ops 10.6 ms 7 ms +50.25%
❌ WallTime compile_to_engine 97.9 ms 114.5 ms -14.47%
❌ WallTime e2e_100_functions 157.5 ms 179.7 ms -12.32%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing aaron/interfacs (5de26df) with canary (1c9c296)

Open in CodSpeed

@vercel
vercel Bot temporarily deployed to Preview – beps May 21, 2026 19: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: 1

Caution

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

⚠️ Outside diff range comments (3)
baml_language/crates/baml_lsp2_actions/src/check.rs (2)

1043-1050: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Resolve inherited interface members from the interface's namespace.

Both call sites still pass the caller's namespace_path into collect_interface_members_with_subst. That walker uses the supplied namespace to resolve relative extends, so implementing an interface from another namespace can traverse the wrong parent chain and report bogus missing members. The walk needs to use the namespace of each interface being visited, not the namespace of the class or file doing the validation.

Also applies to: 1240-1247

🤖 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_lsp2_actions/src/check.rs` around lines 1043 -
1050, The call to collect_interface_members_with_subst is using the caller's
namespace_path which makes the walker resolve relative extends incorrectly;
change the call sites that build members (where iface, iface_file, pkg_items,
namespace_path, &subst are passed) to pass the namespace belonging to the
interface being visited (e.g., iface.namespace or resolve namespace from
iface_file/iface) instead of the caller's namespace_path so the walker uses the
interface's own namespace when resolving extends; make the same change at the
other similar call site later in the file (the second
collect_interface_members_with_subst invocation).

894-935: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep inherited field types in their original namespace.

seen only retains the raw TypeExpr, then type_exprs_compatible re-lowers both sides as if they were declared in the current namespace_path. If a parent interface comes from another namespace and uses relative type names, this will compare the wrong types and can invent or miss InterfaceExtendsFieldConflicts. Store each inherited field's originating namespace (or its already-lowered Ty) alongside the entry before comparing.

🤖 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_lsp2_actions/src/check.rs` around lines 894 - 935,
The seen map currently stores raw TypeExpr strings which get re-lowered against
the current namespace when calling type_exprs_compatible, causing incorrect
comparisons for inherited fields from other namespaces; change the seen entries
(where you insert in the iface.fields seed and in the parent-members loop) to
also include the originating namespace/file or—better—store the already-lowered
type representation (the Ty or lowered form) along with the origin (use
parent_loc.file(db) or equivalent) so comparisons use the original namespace
context; update the places that call seen.get(field_name) and the
type_exprs_compatible invocation to pass the stored origin/lowered Ty instead of
re-lowering the raw TypeExpr to avoid spurious InterfaceExtendsFieldConflict
results.
baml_language/crates/baml_compiler2_ast/src/lower_cst.rs (1)

160-198: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only merge bare local class targets here.

This matches for_target by segments.last(), so a qualified or specialized target like dep.Dog or Dog<int> will be attached to the local Dog class if one exists. That changes the target before package-level resolution and loses the original ImplementsFor identity. Restrict this eager merge to an unqualified, non-generic local class name; leave everything else as Item::ImplementsFor.

Suggested narrowing
-        let target_name = match &imp.for_target.expr {
-            crate::ast::TypeExpr::Path { segments, .. } => segments.last().cloned(),
+        let target_name = match &imp.for_target.expr {
+            crate::ast::TypeExpr::Path {
+                segments,
+                generic_args,
+                ..
+            } if segments.len() == 1 && generic_args.is_empty() => segments.first().cloned(),
             _ => None,
         };
🤖 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_ast/src/lower_cst.rs` around lines 160 -
198, The current merge finds a local class by using segments.last() on
impl.for_target.expr and wrongly merges qualified or generic targets; change the
check to only merge when for_target is an unqualified, non-generic simple path:
match crate::ast::TypeExpr::Path { segments, .. } and require segments.len() ==
1 AND that the single segment has no type/generic arguments (e.g., its
generics/parameters list is empty); only then look up Item::Class by comparing
class.name to that single segment and merge into class.implements, otherwise
retain the original Item::ImplementsFor(imp) in items.
♻️ Duplicate comments (1)
baml_language/crates/baml_compiler2_tir/src/interfaces.rs (1)

256-266: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Union target keys are still order-sensitive here.

This new insert path relies on implementation_key_for_ty(&target_ty), but that helper still preserves union member order. implements I for int | string and a later lookup for string | int will land in different buckets, so equivalent union targets can still miss nominal matches.

🤖 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/interfaces.rs` around lines 256 -
266, The insertion path uses implementation_key_for_ty(&target_ty) which
preserves union member order, causing keys like `int|string` and `string|int` to
differ; modify implementation_key_for_ty (or add a canonicalize_union_key
helper) to produce an order-independent canonical key for union types (e.g.,
sort or otherwise deterministically normalize union members) and then use that
canonical key when inserting into type_implements_type_args and type_implements
so equivalent unions map to the same bucket; update references to
implementation_key_for_ty/target_key in this block to use the canonicalized key.
🤖 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/normalize.rs`:
- Around line 31-39: The canonicalization currently preserves parameter names
and positional order of named optional parameters causing equivalent function
shapes to compare unequal; update the function-type canonicalization logic (used
by is_same_normalized_type which calls normalize(...).canonicalize()) so that
required parameters are compared only by their types in positional order (ignore
parameter names) and optional/keyword parameters are compared in an
order-insensitive way (treat them as a multiset keyed by parameter type and
optional flag, ignoring the parameter name), and ensure this change is applied
in the StructuralTy / function-canonicalize code paths that produce
canonicalized Ty values (the same logic to fix in any canonicalize
implementation referenced by normalize and StructuralTy) as well as other
similar canonicalize usages mentioned in the review.

---

Outside diff comments:
In `@baml_language/crates/baml_compiler2_ast/src/lower_cst.rs`:
- Around line 160-198: The current merge finds a local class by using
segments.last() on impl.for_target.expr and wrongly merges qualified or generic
targets; change the check to only merge when for_target is an unqualified,
non-generic simple path: match crate::ast::TypeExpr::Path { segments, .. } and
require segments.len() == 1 AND that the single segment has no type/generic
arguments (e.g., its generics/parameters list is empty); only then look up
Item::Class by comparing class.name to that single segment and merge into
class.implements, otherwise retain the original Item::ImplementsFor(imp) in
items.

In `@baml_language/crates/baml_lsp2_actions/src/check.rs`:
- Around line 1043-1050: The call to collect_interface_members_with_subst is
using the caller's namespace_path which makes the walker resolve relative
extends incorrectly; change the call sites that build members (where iface,
iface_file, pkg_items, namespace_path, &subst are passed) to pass the namespace
belonging to the interface being visited (e.g., iface.namespace or resolve
namespace from iface_file/iface) instead of the caller's namespace_path so the
walker uses the interface's own namespace when resolving extends; make the same
change at the other similar call site later in the file (the second
collect_interface_members_with_subst invocation).
- Around line 894-935: The seen map currently stores raw TypeExpr strings which
get re-lowered against the current namespace when calling type_exprs_compatible,
causing incorrect comparisons for inherited fields from other namespaces; change
the seen entries (where you insert in the iface.fields seed and in the
parent-members loop) to also include the originating namespace/file
or—better—store the already-lowered type representation (the Ty or lowered form)
along with the origin (use parent_loc.file(db) or equivalent) so comparisons use
the original namespace context; update the places that call seen.get(field_name)
and the type_exprs_compatible invocation to pass the stored origin/lowered Ty
instead of re-lowering the raw TypeExpr to avoid spurious
InterfaceExtendsFieldConflict results.

---

Duplicate comments:
In `@baml_language/crates/baml_compiler2_tir/src/interfaces.rs`:
- Around line 256-266: The insertion path uses
implementation_key_for_ty(&target_ty) which preserves union member order,
causing keys like `int|string` and `string|int` to differ; modify
implementation_key_for_ty (or add a canonicalize_union_key helper) to produce an
order-independent canonical key for union types (e.g., sort or otherwise
deterministically normalize union members) and then use that canonical key when
inserting into type_implements_type_args and type_implements so equivalent
unions map to the same bucket; update references to
implementation_key_for_ty/target_key in this block to use the canonicalized key.
🪄 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: d668d561-160a-4e1e-a87b-c9022f57ab8e

📥 Commits

Reviewing files that changed from the base of the PR and between 4663d3c and 210d212.

📒 Files selected for processing (9)
  • baml_language/crates/baml_compiler2_ast/src/lib.rs
  • baml_language/crates/baml_compiler2_ast/src/lower_cst.rs
  • baml_language/crates/baml_compiler2_tir/src/builder.rs
  • baml_language/crates/baml_compiler2_tir/src/interfaces.rs
  • baml_language/crates/baml_compiler2_tir/src/normalize.rs
  • baml_language/crates/baml_lsp2_actions/src/check.rs
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_field_rule_equivalent_union_order.baml
  • baml_language/crates/baml_tests/src/compiler2_tir/inference.rs
  • baml_language/crates/baml_tests/tests/interfaces.rs
✅ Files skipped from review due to trivial changes (1)
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/interface/valid_field_rule_equivalent_union_order.baml

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 May 21, 2026 19:54 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle May 21, 2026 19:55 Inactive
@vercel
vercel Bot temporarily deployed to Preview – beps May 22, 2026 02:09 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle May 22, 2026 02:27 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 May 22, 2026 02:27 Inactive
aaronvg and others added 2 commits May 29, 2026 16:42
…r dedup

Type rendering was scattered across ~3 duplicated `Ty` recursions plus
magic-string handling of the implicit `user` package, which leaked into
diagnostics (`user.Dog` instead of `Dog`).

- Collapse all `Ty` rendering into one `Ty::render_with(&dyn TyRenderStrategy)`
  in `baml_compiler2_tir`. Canonical/user-facing, the LSP's context-aware hover,
  and context-free rendering are now thin strategy impls. Consolidate qualified-
  name rendering into `render_dotted`/`render_qualified`; retire the string-level
  `humanize_type_string` from all `Ty` paths (effect-param -> `callback` now in
  the strategy's `type_var`).
- Route the remaining HIR diagnostic name paths through `render_user_facing`,
  removing the last compile-time `user.` leaks (LSP/interface snapshots updated;
  every diff is a pure `user.` removal).
- Represent a qualified name's package as `enum Package { Local, Dep(Name) }`
  instead of a bare `Name`; the `"user"` literal now lives only in
  `Package::from_name`. `package()`/`new()` signatures are unchanged
  (`Local` resolves to a `Name::new_inline` static), and `Package` orders by
  name to preserve `QualifiedTypeName`'s `Ord`. Convert the MIR `== "user"`
  check to `is_local()`.
- Fix the interface-implementor dedup in emit Pass 7.6: the `seen_pairs` key
  used `format!("{iface_args:?}")` (the `Debug` of `Vec<baml_type::Ty>`), which
  is not contractually unique across distinct types and could silently drop a
  legitimate implementor. Key the args structurally (`Vec<Ty>` derives Eq/Hash).
- Refresh the wasm size-gate baseline.

Suites green: interfaces 336, baml_tests, LSP 112 + 369; rustdoc -D warnings clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Comment thread baml_language/.cargo/size-gate.toml Resolved
aaronvg and others added 5 commits May 29, 2026 17:46
Add 5 BAML benchmark workloads exercising methods, interfaces, and
dispatch (mirrors verified runtime patterns from interfaces.rs):

- classes/method-call, classes/method-chain (run on canary too)
- interfaces/polymorphic-dispatch, match-dispatch, default-method
  (interfaces-only; canary fails to compile, gets skipped)

All return int for clean cross-language output verification.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
# Conflicts:
#	baml_language/.cargo/size-gate.toml
#	baml_language/.ci/size-gate/aarch64-apple-darwin.toml
#	baml_language/.ci/size-gate/x86_64-pc-windows-msvc.toml
#	baml_language/.ci/size-gate/x86_64-unknown-linux-gnu.toml
# Conflicts:
#	baml_language/crates/bex_engine/src/lib.rs

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b7989d5. Configure here.

This branch was previously deployed

3 inactive deployments
Preview – promptfiddle2 — 74c1f31c Deployed May 30, 2026 by vercel[bot]
Preview – promptfiddle — 74c1f31c Deployed May 30, 2026 by vercel[bot]
Preview – beps — 74c1f31c Deployed May 30, 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