Skip to content

fix: MIR analysis soundness bugs + emit simplifications#3126

Merged
antoniosarosi merged 1 commit intocanaryfrom
antonio/emit-analysis-cleanup
Feb 17, 2026
Merged

fix: MIR analysis soundness bugs + emit simplifications#3126
antoniosarosi merged 1 commit intocanaryfrom
antonio/emit-analysis-cleanup

Conversation

@antoniosarosi
Copy link
Copy Markdown
Contributor

@antoniosarosi antoniosarosi commented Feb 17, 2026

Summary

Fixes three miscompilation bugs in the MIR analysis pass and simplifies the emit infrastructure with several targeted refactors. All changes are in analysis.rs, emit.rs, builder.rs, and VM regression tests.

Bug fixes (3 correctness issues)

  • Cross-block Virtual inlining unsoundnesscan_be_virtual() would re-evaluate rvalues at use sites without checking whether dependencies (including transitive ones and parameters with implicit entry defs) could be modified on intermediate paths. Three related fixes:
    • Multi-def check: refuse to virtualize if any dependency has multiple definitions
    • Parameter awareness: parameters have an implicit entry def not in all_defs, so any explicit reassignment means multiple defs
    • Transitive tracking: use collect_transitive_reads instead of direct walk_rvalue_locals to catch let t = p; let x = t; p = 2; x chains
  • CopyOf propagation for mutable parametersget_copy_source() assumed parameters are never reassigned, but BAML allows x = 3 where x is a param. Added check that source parameter has no entries in all_defs.
  • MIR construction invariant assertions — Added debug_assert! guards at MIR builder boundaries enforcing that Call/Await/DispatchFuture destinations are always Place::Local.

Refactors

  • StatementRef enum — Replaced TERMINATOR_IDX = usize::MAX sentinel with enum StatementRef { Statement(usize), Terminator } for type-safe, exhaustiveness-checked references. All comparison sites now use pattern matching.
  • Unified MIR tree walkers — Consolidated 6 duplicate rvalue/operand/place walker functions into 3 generic walk_{rvalue,operand,place}_locals functions + 3 thin wrappers. Single source of truth for tree traversal.
  • Merged def-use collection — Combined collect_def_use() and collect_all_definitions() into one pass by adding all_defs: Vec<(BlockId, StatementRef)> to LocalDefUse. Eliminated a redundant full-MIR walk.
  • Deduplicated patch_jumpspatch_jumps() now delegates to patch_jump_to() instead of duplicating the match logic.
  • Dead code removal — Removed always-true is_inlinable_rvalue(), unused LocalDefUse::local field, and made emit_store_place Field/Index arms unreachable!().
  • Struct cleanup — Removed dominators/predecessors from AnalysisResult (computation unchanged, just not stored in return value since callers never read them). Simplified entry().or_insert_with() boilerplate to get_mut().unwrap().

Test plan

  • 5 regression tests added for the 3 soundness bugs:
    • virtual_cross_block_soundness — cross-block variable modification
    • virtual_cross_block_param_mutation_soundness — parameter mutation on intermediate path
    • virtual_cross_block_transitive_param_mutation_soundness — transitive t = p; x = t chain
    • copy_of_mutable_param_soundness — CopyOf with reassigned param
    • virtual_multiple_defs_preserve_side_effects — multiple defs must not skip side effects
  • All existing tests pass: 375 baml_tests, 146 emit tests, 270+ VM tests
  • cargo fmt and cargo clippy clean

Summary by CodeRabbit

  • Refactor

    • Improved internal compiler analysis and code generation structure for enhanced maintainability and code organization.
  • Tests

    • Extended test coverage for variable assignment handling and virtual inlining across different control flow scenarios.

Fix three miscompilation bugs in the MIR analysis pass and simplify
the emit infrastructure with several targeted refactors.

Bug fixes:
- Fix cross-block Virtual inlining unsoundness: locals whose rvalue
  dependencies have multiple definitions (including parameters with
  implicit entry defs) are no longer virtualized across blocks.
  Uses transitive dependency tracking to catch indirect mutations.
- Fix CopyOf propagation for mutable parameters: parameters that are
  reassigned anywhere in the function are no longer eligible for copy
  propagation.
- Add debug_assert guards at MIR construction boundaries to enforce
  that Call/Await/DispatchFuture destinations are always local places.

Refactors:
- Replace TERMINATOR_IDX (usize::MAX) sentinel with StatementRef enum
  for type-safe, exhaustiveness-checked statement/terminator references.
- Unify 6 duplicate rvalue/operand/place walker functions into 3 generic
  walk_*_locals walkers + 3 thin convenience wrappers.
- Merge collect_def_use and collect_all_definitions into a single pass
  by adding all_defs field to LocalDefUse.
- Deduplicate patch_jumps/patch_jump_to logic in emit.rs.
- Remove dead code: always-true is_inlinable_rvalue, unused
  LocalDefUse::local field, unreachable emit_store_place arms.
- Remove dominators/predecessors from AnalysisResult (only used
  internally during classification, computation unchanged).
- Simplify entry/or_insert_with boilerplate to get_mut/unwrap.
@vercel
Copy link
Copy Markdown

vercel bot commented Feb 17, 2026

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

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Feb 17, 2026 10:09pm
promptfiddle Building Building Preview, Comment Feb 17, 2026 10:09pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

This pull request refactors the analysis module to replace implicit statement indices with an explicit StatementRef abstraction for distinguishing statements from terminators. The public AnalysisResult struct is simplified by removing dominators and predecessors fields. Supporting changes update def-use tracking, emit handling, and add comprehensive test coverage for virtual inlining soundness.

Changes

Cohort / File(s) Summary
Analysis Refactoring
baml_language/crates/baml_compiler_emit/src/analysis.rs
Introduces StatementRef enum replacing implicit statement indices. Updates DefLocation and UseLocation to use StatementRef. Extends LocalDefUse with all_defs collection tracking all definition sites. Removes dominators and predecessors fields from AnalysisResult. Refactors def-use collection to use generic walk helpers and updates copy propagation, phi-like detection, and dominator-related logic to operate with StatementRef.
Emit and Builder Updates
baml_language/crates/baml_compiler_emit/src/emit.rs, baml_language/crates/baml_compiler_mir/src/builder.rs
Adds patch_jump_to helper method in emit codegen for centralized jump patching. Changes emit_store_place to treat Field/Index destinations as unreachable. Introduces debug assertions in builder methods to enforce Place arguments are Local.
Test Coverage Expansion
baml_language/crates/bex_vm/tests/assignments.rs
Adds five new test functions validating virtual inlining soundness across blocks, parameter mutations, and side-effect preservation. Extends imports to support failing test cases and error assertions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main changes: fixing MIR analysis soundness bugs and simplifying emit infrastructure with refactoring.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch antonio/emit-analysis-cleanup

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

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Feb 17, 2026

Merging this PR will not alter performance

⚠️ Unknown Walltime execution environment detected

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

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

✅ 15 untouched benchmarks
⏩ 84 skipped benchmarks1


Comparing antonio/emit-analysis-cleanup (2cd4224) with canary (eea0740)

Open in CodSpeed

Footnotes

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

@github-actions
Copy link
Copy Markdown

Binary size checks passed

7 passed

Artifact Platform Gzip Baseline Delta Status
bridge_cffi Linux 4.0 MB 4.0 MB +2.6 KB (+0.1%) OK
bridge_cffi-stripped Linux 2.2 MB 2.1 MB +2.8 KB (+0.1%) OK
bridge_cffi macOS 3.3 MB 3.3 MB +3.0 KB (+0.1%) OK
bridge_cffi-stripped macOS 1.7 MB 1.7 MB +2.4 KB (+0.1%) OK
bridge_cffi Windows 3.3 MB 3.3 MB +3.1 KB (+0.1%) OK
bridge_cffi-stripped Windows 1.8 MB 1.8 MB +3.1 KB (+0.2%) OK
bridge_wasm WASM 1.3 MB 1.3 MB +2.0 KB (+0.2%) OK

Generated by cargo size-gate · workflow run

@antoniosarosi antoniosarosi added this pull request to the merge queue Feb 17, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (2)
baml_language/crates/baml_compiler_emit/src/analysis.rs (2)

1006-1078: ⚠️ Potential issue | 🟠 Major

Guard virtualization against locals with multiple definitions.
LocalClassification::Virtual skips all assignments for a local. Without a du.all_defs.len() == 1 gate, a multi-def local can drop earlier side effects (e.g., x = fail(); x = 2; x). Add a hard check before inlining.

🛠️ Suggested fix
 fn can_be_virtual(
     du: &LocalDefUse,
     dominators: &Dominators,
     mir: &MirFunction,
     def_use: &HashMap<Local, LocalDefUse>,
     predecessors: &HashMap<BlockId, Vec<BlockId>>,
 ) -> bool {
-    // Must have exactly one definition
-    let Some(def) = &du.def else {
-        return false;
-    };
+    // Must have exactly one definition
+    if du.all_defs.len() != 1 {
+        return false;
+    }
+    let Some(def) = &du.def else {
+        return false;
+    };

145-174: ⚠️ Potential issue | 🟡 Minor

Run the workspace test suite for baml_language.

This crate is part of the baml_language workspace which has specific testing practices. Run cargo test --package baml_tests to verify your changes against the full compiler pipeline, including integration tests. See baml_language/TEST_INSTRUCTIONS.md for detailed testing guidance.

Comment on lines +62 to +175
// Regression: Virtual inlining must not re-evaluate rvalue when a dependency
// is modified in an intermediate block between def and use.
#[test]
fn virtual_cross_block_soundness() -> anyhow::Result<()> {
assert_vm_executes(Program {
source: r#"
function main(c: bool) -> int {
let a = 1;
let b = a;
if (c) {
a = 2;
}
b
}

function entry() -> int {
main(true)
}
"#,
function: "entry",
expected: ExecState::Complete(Value::Int(1)),
})
}

// Regression: Virtual inlining of a rvalue that reads a parameter must not
// re-evaluate when the parameter is reassigned on an intermediate path.
// Parameters have an implicit entry definition NOT tracked in `all_defs`,
// so even a single explicit reassignment means multiple definitions.
#[test]
fn virtual_cross_block_param_mutation_soundness() -> anyhow::Result<()> {
assert_vm_executes(Program {
source: r#"
function main(c: bool, p: int) -> int {
let x = p;
if (c) {
p = 2;
}
x
}

function entry() -> int {
main(true, 42)
}
"#,
function: "entry",
expected: ExecState::Complete(Value::Int(42)),
})
}

// Regression: CopyOf must not propagate through mutable parameters.
#[test]
fn copy_of_mutable_param_soundness() -> anyhow::Result<()> {
assert_vm_executes(Program {
source: r#"
function main(x: int) -> int {
let y = x;
x = 2;
y
}

function entry() -> int {
main(42)
}
"#,
function: "entry",
expected: ExecState::Complete(Value::Int(42)),
})
}

// Regression: cross-block virtualization must account for transitive
// dependencies; `x = t` where `t = p` must not be re-evaluated as latest `p`.
#[test]
fn virtual_cross_block_transitive_param_mutation_soundness() -> anyhow::Result<()> {
assert_vm_executes(Program {
source: r#"
function main(c: bool, p: int) -> int {
let t = p;
let x = t;
if (c) {
p = 2;
}
x
}

function entry() -> int {
main(true, 42)
}
"#,
function: "entry",
expected: ExecState::Complete(Value::Int(42)),
})
}

// Regression: multiple definitions of a local must not allow inlining that
// skips side-effecting call evaluation.
#[test]
fn virtual_multiple_defs_preserve_side_effects() -> anyhow::Result<()> {
assert_vm_fails(FailingProgram {
source: r#"
function fail() -> int {
assert(false);
1
}

function main() -> int {
let x = fail();
x = 2;
x
}
"#,
function: "main",
expected: RuntimeError::AssertionError.into(),
})
}
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.

🧹 Nitpick | 🔵 Trivial

Consider unit-test placement if feasible.
These are integration tests under bex_vm/tests; if the same coverage can live as crate unit tests, prefer that to reduce overhead. If integration scope is required, a brief note is enough.
As per coding guidelines, "Prefer writing Rust unit tests over integration tests where possible".

Merged via the queue into canary with commit ea7f7fb Feb 17, 2026
43 of 44 checks passed
@antoniosarosi antoniosarosi deleted the antonio/emit-analysis-cleanup branch February 17, 2026 22:23
antoniosarosi added a commit that referenced this pull request Feb 17, 2026
Integrates 4 canary PRs:
- #3126: MIR analysis soundness (StatementRef, unified walkers)
- #3124: Type variants for type expressions (parse takes Type)
- #3122: InitLocals(n) instruction
- #3107: Full tracing system (bex_events, Collector)

Conflict resolutions:
- llm.baml: keep orchestration loop with panic, update parse() to use get_return_type
- baml_builtins: keep both Enum and Type TypePattern variants, add get_return_type alongside orchestration builtins
- baml_compiler_emit: keep HIDDEN_LLM_BUILTINS removal
- baml_compiler_tir: add both Enum and Type arms in substitute functions
- bex_vm_types: merge both ClientBuild* and CollectorRef re-exports
- llm_render tests: use new 4-arg call_function signature, fix PromptAst FQN
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