fix: thread inferred generic type args to callee frames at runtime#3698
fix: thread inferred generic type args to callee frames at runtime#3698hellovai wants to merge 2 commits into
Conversation
A generic call whose type args are inferred rather than written — `ArrA.of([1, 2, 3])`, `tn([1, 2, 3])` — ran its callee with `T = unknown`: TIR inferred the bindings but discarded them after computing the result type, and MIR seeded `frame.type_args` from receiver class args ++ explicit `<...>` args only. Instances built by inferred static ctors were born with `class_type_args = [unknown]`, every runtime `IsType C<int>` test against them failed, and the interface-dispatch switch — whose last candidate arm is an unguarded else — silently ran whichever implementor was last in hash order. (#3667's regression suite passed by luck of that arm order; this is the residual root cause behind the iterator-blocker bug class.) Three-part fix: * TIR: `check_call_inner` now records the post-inference bindings per call expr (`call_inferred_type_args`, sorted by param name), plumbed through `ScopeInference`/`DefaultParameterInference` with the same save/restore discipline as `call_plans`. * MIR: `emit_inferred_call_type_arg_ops` seeds the callee frame with those bindings as leading `LoadType` args in the callee's De Bruijn order (`callee_frame_generic_params` = owner ++ fn params, mirroring `enclosing_generic_params`). Conservative gates: explicit args, sys-ops (positional Rust glue), function values, misaligned receiver prefixes, all-unknown seeds. Bindings nesting a TypeVar outside the caller's generic params (a rigid `Self`) are demoted to `unknown` — naive templating would lower them to `void`. Also stop prepending a stray `null` receiver to self-less static path calls (`ArrA.of(...)`); the VM's arity-based pop tolerated the orphan, but it corrupts the leading type-arg window. * EMIT: fix a pre-existing ShortCircuit miscompile this surfaced (`fwd<int>(true || false)` returned null on canary): when the stack-carry pass rejects the destination carry, the taken path never stored the slot while `eval_rhs` did — a half-applied phi. The taken path now stores whenever the destination is not stack-carried. Regression tests: free-fn reflect, instance `IsType` after inferred ctor, the full lazy-iterator collect shape, and the nested-`Self` demotion, plus a `generic-apply-inferred-1m` speedtest workload (the existing bench corpus is monomorphic and cannot observe this path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThe compiler now records inferred generic type arguments at call sites during type inference and propagates them through MIR lowering to seed callee frames at runtime, enabling type dispatch and reflection in called functions. A short-circuit bytecode fix improves destination slot storage logic. ChangesInferred Generic Type Arguments Dispatch
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
baml_language/crates/baml_compiler2_mir/src/lower.rs (1)
6057-6156: ⚡ Quick winAdd unit coverage for the inferred-call type-arg emitter.
The new ordering and alignment rules in
emit_inferred_call_type_arg_opsare the core behavior change here, but the added unit tests only exercise interface guard helpers. A small in-module test for a static generic-class call and a receiver-seeded method call would lock in the owner/fn slot ordering and the misalignment gates cheaply. As per coding guidelines, "Prefer writing Rust unit tests over integration tests where possible."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@baml_language/crates/baml_compiler2_mir/src/lower.rs` around lines 6057 - 6156, Add in-module Rust unit tests that directly exercise emit_inferred_call_type_arg_ops to cover (1) a static generic-class call where owner params precede fn params to assert the emitted frame type arg operands follow owner-then-fn ordering, and (2) a receiver-seeded method call where already_seeded equals owner params to assert proper emission and when already_seeded is misaligned (non-zero but not equal to owner params) to assert it returns empty. Use the same module-level helpers/state setup the file uses: populate call_inferred_type_args for the expr_id, set resolutions/path_member_resolutions so func_loc resolves to a function (so callee_frame_generic_params returns owner_params and fn_params), and vary already_seeded; assert on the returned Vec<Operand> contents and that all-unknown or misaligned cases return Vec::new(). Ensure tests live in the same file as the function (unit tests) and reference emit_inferred_call_type_arg_ops, call_inferred_type_args, resolutions/path_member_resolutions, and callee_frame_generic_params so they validate ordering and misalignment gates.
🤖 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.
Nitpick comments:
In `@baml_language/crates/baml_compiler2_mir/src/lower.rs`:
- Around line 6057-6156: Add in-module Rust unit tests that directly exercise
emit_inferred_call_type_arg_ops to cover (1) a static generic-class call where
owner params precede fn params to assert the emitted frame type arg operands
follow owner-then-fn ordering, and (2) a receiver-seeded method call where
already_seeded equals owner params to assert proper emission and when
already_seeded is misaligned (non-zero but not equal to owner params) to assert
it returns empty. Use the same module-level helpers/state setup the file uses:
populate call_inferred_type_args for the expr_id, set
resolutions/path_member_resolutions so func_loc resolves to a function (so
callee_frame_generic_params returns owner_params and fn_params), and vary
already_seeded; assert on the returned Vec<Operand> contents and that
all-unknown or misaligned cases return Vec::new(). Ensure tests live in the same
file as the function (unit tests) and reference emit_inferred_call_type_arg_ops,
call_inferred_type_args, resolutions/path_member_resolutions, and
callee_frame_generic_params so they validate ordering and misalignment gates.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 509f0e45-5708-494f-8305-7895a7348bf3
⛔ Files ignored due to path filters (39)
baml_language/crates/baml_tests/snapshots/baml_src/builtins.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/baml_src/closures.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/baml_src/deep_copy.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/baml_src/fs.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/baml_src/inferred_generic_type_args.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/baml_src/instantiation_expr.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/baml_src/json_auto_derive.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/baml_src/lambdas.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/baml_src/type_error_repro.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/baml_src/type_reflection.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__testing_std__/baml_tests__compiles____testing_std____04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__testing_std__/baml_tests__compiles____testing_std____06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/closures/baml_tests__compiles__closures__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/closures/baml_tests__compiles__closures__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/comment_in_type/baml_tests__compiles__comment_in_type__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/comment_in_type/baml_tests__compiles__comment_in_type__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_llm_return_type/baml_tests__compiles__json_llm_return_type__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_llm_return_type/baml_tests__compiles__json_llm_return_type__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/lambda_advanced/baml_tests__compiles__lambda_advanced__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/lambda_advanced/baml_tests__compiles__lambda_advanced__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/lambda_basic/baml_tests__compiles__lambda_basic__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/lambda_basic/baml_tests__compiles__lambda_basic__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/llm_image_outputs/baml_tests__compiles__llm_image_outputs__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/llm_image_outputs/baml_tests__compiles__llm_image_outputs__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/optional_function_parameters/baml_tests__compiles__optional_function_parameters__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/optional_function_parameters/baml_tests__compiles__optional_function_parameters__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/stream_llm_inferred_typeargs/baml_tests__compiles__stream_llm_inferred_typeargs__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/stream_llm_inferred_typeargs/baml_tests__compiles__stream_llm_inferred_typeargs__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/testset_vibes_nested/baml_tests__compiles__testset_vibes_nested__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/testset_vibes_nested/baml_tests__compiles__testset_vibes_nested__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/type_builder_errors/baml_tests__compiles__type_builder_errors__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/type_builder_errors/baml_tests__compiles__type_builder_errors__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/type_builder_test/baml_tests__compiles__type_builder_test__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/type_builder_test/baml_tests__compiles__type_builder_test__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded_unoptimized.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_textual.snapis excluded by!**/*.snap
📒 Files selected for processing (7)
baml_language/crates/baml_compiler2_emit/src/emit.rsbaml_language/crates/baml_compiler2_mir/src/lower.rsbaml_language/crates/baml_compiler2_tir/src/builder.rsbaml_language/crates/baml_compiler2_tir/src/inference.rsbaml_language/crates/baml_lsp2_actions/src/check.rsbaml_language/crates/baml_tests/baml_src/ns_inferred_generic_type_args/inferred_generic_type_args.bamlbaml_language/tools/speedtest/workloads/compute/generic-apply-inferred-1m.md
|
superseded by this (which adds the type args in the CallPlan instead) |
What was wrong
A generic call whose type args are inferred rather than written ran its callee with
T = unknown. TIR inferred the bindings, used them to compute the result type, then threw them away; MIR seededframe.type_argsfrom receiver class args ++ explicit<...>args only. On current canary (d968182, verified pre-merge):function tn<T>(a: T[]) -> string { reflect.type_of<T>().to_string() }—tn([1,2,3])"unknown""int"Holder.tn([1,2,3])"unknown""int"let it: IterA<int, never> = ArrA.of([1,2,3]); it.collect().length()(2 implementors)match (ArrBag.make([5,6,7])) { let a: ArrBag<int> => ... }_arm — instance carriesclass_type_args=[unknown]ArrBag<int>fwd<int>(true || false)wherefwd<T>(x: bool) -> bool { x }null(pre-existing emitter miscompile, surfaced by this work)trueThe dispatch fallout is the nasty part: instances built by inferred static ctors are born with
class_type_args = [unknown], every pinnedis_type C<int>dispatch guard fails, and the interface-dispatch switch's last candidate arm is an unguarded else — so whichever implementor is last in hash order silently runs. That made the bug look "fragile" (renames, extra declarations in the file, or method names flip the arm order and mask it), and it is why #3667's regression suite stayed green: those tests pass by luck of arm order — their instances carry the same broken[unknown]args (now pinned by a directIsTyperegression test that fails without this fix).What the fix does
1. TIR (
baml_compiler2_tir):check_call_innerrecords the post-inference bindings per call expr incall_inferred_type_args(sortedVec<(Name, Ty)>), plumbed throughScopeInference/DefaultParameterInferencewith the same save/restore discipline ascall_plans(parameter defaults + inline lambda inference).2. MIR (
baml_compiler2_mir): at the call-lowering merge point,emit_inferred_call_type_arg_opsemits those bindings as leadingLoadTypeframe args in the callee's De Bruijn order (callee_frame_generic_params= owner params ++ fn params, mirroringenclosing_generic_params). Conservative gates preserve the status quo when seeding can't be done soundly: explicit<...>args, sys-ops (positional Rust glue), function values (no resolution), misaligned receiver prefixes, all-unknown seeds. Bindings nesting a TypeVar outside the caller's generic params — a rigidSelffrom a Self-pinned call — are demoted tounknown: naive templating lowers them tovoid, which would seedSelf[]asvoid[](caught by adversarial review; guarded bynested_self_inferred_binding_not_voided).Also: the path-callee branch no longer prepends a stray
Constant::Nullreceiver to self-less static methods (ArrA.of(...)). The VM's arity-based pop silently tolerated the orphan, but it corrupts the leading type-arg window once calls carry seeds.3. EMIT (
baml_compiler2_emit): pre-existingShortCircuitmiscompile, reproducible on canary without this PR. The emitter assumed the destination local was always stack-carried; when the stack-carry pass rejects the carry (e.g. aLoadTypeoperand pushes before the use), the taken path never stored the slot whileeval_rhsdid — a half-applied phi, soa || bfed into such a call reads an unwritten local. The taken path now stores whenever the destination is not stack-carried (&&via a small trampoline). Bonus: the previously-failingif_else_assigned_then_passed_to_callshape now passes.Verification
origin/canaryand flip with this PR.+load_typeseeds at inferred generic call sites,-const nullstray receivers, store/load pairs where short-circuit carries are now correctly rejected.ns_inferred_generic_type_args(free-fn reflect, instanceIsTypeafter inferred ctor, the lazy-iterator collect shape with 2 implementors, nested-Selfdemotion).Known remaining (pre-existing, intentionally out of scope)
<U>readingUat runtime).let f = ArrA.of; f([1])) stay unseeded — no resolution at the call site.letbindings consumed by a later statement in the block lower toConstant(Null)— separate lowering bug discovered during this work.Perf note
Every inferred generic call now carries
LoadTypeseeds. The existing bench corpus is monomorphic and cannot observe this path, so a green bench run is not evidence — this PR adds acompute/generic-apply-inferred-1mspeedtest workload that exercises it; a release-mode A/B (total cycles + allocs/iter) against canary is the meaningful pre-merge measurement.🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Tests