Repository navigation
perf(compiler2): ~32x faster cold compile — remove redundant work, memoize package-invariant queries - #4016
perf(compiler2): ~32x faster cold compile — remove redundant work, memoize package-invariant queries#4016hellovai wants to merge 2 commits into
Conversation
…moize package-invariant queries Cold `check` + `emit` on the test corpus (78 files, 25.8k lines) drops from 16.0s to ~0.50s; end-to-end `baml check` wall time from ~17s to ~0.55s. Single-threaded throughout — every win is deleted duplicate work, a new Salsa-tracked query for package/project-invariant computation, or reduced allocation churn. Ships a new `baml_compiler2_profile` harness whose README documents the full audit, methodology, and per-change measurements. Also fixes two user-visible bugs found along the way: diagnostics inside lambdas were reported twice (lambda bodies were type-inferred twice), and diagnostic rendering rebuilt ariadne's per-project line index once per diagnostic (34% of `baml check` wall time on warning-heavy projects). Co-authored-by: Cursor <[email protected]>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR adds cached AST and alias queries, reuses nested lambda inference, optimizes MIR dispatch and type normalization, introduces compiler profiling tooling, uses mimalloc in the CLI, reuses diagnostic source caches, and filters builtin-file diagnostics. ChangesCompiler pipeline
Tooling and runtime support
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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 |
The profiling harness is a dev tool, not a compiler crate — move it to
the tools_ namespace ("compile" added to approved prefixes). This also
drops the anyhow allowlist entry added for it: tools_* is already
covered by the global rule.
Co-authored-by: Cursor <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@baml_language/crates/baml_compiler2_tir/src/normalize.rs`:
- Around line 90-129: Update heads_definitely_differ to treat List/EvolvingList,
Map/EvolvingMap, and RustType/Future/Resource/PromptAst/WatchAccessor as
same-family constructor pairs, returning false so their inner structure is
normalized and compared. Add these explicit match arms before the
discriminant-based wildcard, while preserving true for genuinely incompatible
stable heads.
🪄 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: 7e3b1ff0-2a41-402a-a38b-b6e02305d3a6
⛔ Files ignored due to path filters (5)
baml_language/Cargo.lockis excluded by!**/*.lockbaml_language/crates/baml_tests/snapshots/diagnostic_errors/patterns_class_destructure/baml_tests__diagnostic_errors__patterns_class_destructure__04_tir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/patterns_class_destructure/baml_tests__diagnostic_errors__patterns_class_destructure__05_diagnostics.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/test_expr_name_type_error/baml_tests__diagnostic_errors__test_expr_name_type_error__04_tir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/src/compiler2_tir/snapshots/baml_tests__compiler2_tir__phase3a__optional_param_default_forward_reference_checks_lambda_bodies.snapis excluded by!**/*.snap
📒 Files selected for processing (30)
baml_language/Cargo.tomlbaml_language/crates/baml_cli/Cargo.tomlbaml_language/crates/baml_cli/src/main.rsbaml_language/crates/baml_compiler2_ast/src/lower_expr_body.rsbaml_language/crates/baml_compiler2_hir/src/lib.rsbaml_language/crates/baml_compiler2_mir/Cargo.tomlbaml_language/crates/baml_compiler2_mir/src/lower.rsbaml_language/crates/baml_compiler2_ppir/src/lib.rsbaml_language/crates/baml_compiler2_tir/src/builder.rsbaml_language/crates/baml_compiler2_tir/src/builder/associated_projection.rsbaml_language/crates/baml_compiler2_tir/src/builder/interface_resolution.rsbaml_language/crates/baml_compiler2_tir/src/callable.rsbaml_language/crates/baml_compiler2_tir/src/inference.rsbaml_language/crates/baml_compiler2_tir/src/interfaces.rsbaml_language/crates/baml_compiler2_tir/src/interfaces/coherence.rsbaml_language/crates/baml_compiler2_tir/src/interfaces/impl_rules.rsbaml_language/crates/baml_compiler2_tir/src/lower_type_expr.rsbaml_language/crates/baml_compiler2_tir/src/normalize.rsbaml_language/crates/baml_compiler2_tir/src/type_context.rsbaml_language/crates/baml_compiler_diagnostics/src/render.rsbaml_language/crates/baml_lsp2_actions/Cargo.tomlbaml_language/crates/baml_lsp2_actions/src/check.rsbaml_language/crates/baml_project/src/check.rsbaml_language/crates/baml_tests/src/compiler2_hir.rsbaml_language/crates/baml_tests/src/compiler2_tir/inference.rsbaml_language/crates/baml_type/src/normalize.rsbaml_language/crates/tools_compile_profile/Cargo.tomlbaml_language/crates/tools_compile_profile/README.mdbaml_language/crates/tools_compile_profile/src/main.rsbaml_language/stow.toml
| /// True only when `lhs` and `rhs` provably normalize to different canonical | ||
| /// forms, judged from their outermost constructor alone. | ||
| /// | ||
| /// Soundness rests on `normalize_impl` being head-stable for every variant | ||
| /// this decides on: a `Class` normalizes to a `Class` with the same qualified | ||
| /// name (only its args are rewritten), primitives map to fixed leaves, and so | ||
| /// on. Variants whose normal form can change shape entirely — `TypeAlias` | ||
| /// (expands), `Union` (canonicalization can collapse it to a single member) — | ||
| /// return `false` (undecided), as does any same-kind pair whose equality | ||
| /// depends on inner structure. | ||
| fn heads_definitely_differ(lhs: &Ty, rhs: &Ty) -> bool { | ||
| // Unstable heads: normalization may rewrite these into any other shape. | ||
| fn unstable(t: &Ty) -> bool { | ||
| matches!( | ||
| t, | ||
| Ty::TypeAlias(..) | ||
| | Ty::Union(..) | ||
| | Ty::AssociatedTypeProjection { .. } | ||
| | Ty::Media(baml_type::MediaKind::Generic, _) | ||
| | Ty::Infer { .. } | ||
| ) | ||
| } | ||
| if unstable(lhs) || unstable(rhs) { | ||
| return false; | ||
| } | ||
| match (lhs, rhs) { | ||
| // Nominal heads: different names can never normalize equal. | ||
| (Ty::Class(q1, ..), Ty::Class(q2, ..)) | ||
| | (Ty::Interface(q1, ..), Ty::Interface(q2, ..)) | ||
| | (Ty::Enum(q1, _), Ty::Enum(q2, _)) => q1 != q2, | ||
| (Ty::EnumVariant(q1, v1, _), Ty::EnumVariant(q2, v2, _)) => q1 != q2 || v1 != v2, | ||
| (Ty::Literal(l1, _, _), Ty::Literal(l2, _, _)) => l1 != l2, | ||
| (Ty::Media(k1, _), Ty::Media(k2, _)) => k1 != k2, | ||
| (Ty::TypeVar(n1, _), Ty::TypeVar(n2, _)) => n1 != n2, | ||
| // Same shape, differing only in inner structure: undecided here. | ||
| _ if std::mem::discriminant(lhs) == std::mem::discriminant(rhs) => false, | ||
| // Different stable head constructors never normalize equal. | ||
| _ => true, | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
sed -n '1,220p' baml_language/crates/baml_compiler2_tir/src/normalize.rsRepository: BoundaryML/baml
Length of output: 8872
🏁 Script executed:
rg -n "Ty::(List|EvolvingList|Map|EvolvingMap|RustType|Future|Resource|PromptAst|WatchAccessor|Media\\(|TypeAlias|Union|AssociatedTypeProjection|Infer|Class|Interface|EnumVariant|Literal|TypeVar)" baml_language/crates/baml_compiler2_tir/src/normalize.rs baml_language/crates/baml_compiler2_tir/src -g '!target'Repository: BoundaryML/baml
Length of output: 50372
🏁 Script executed:
python3 - <<'PY'
from pathlib import Path
p = Path("baml_language/crates/baml_compiler2_tir/src/normalize.rs")
lines = p.read_text().splitlines()
for start, end in [(620, 780), (2235, 2715), (1, 140)]:
print(f"\n--- {start}-{end} ---")
for i in range(start-1, min(end, len(lines))):
print(f"{i+1}:{lines[i]}")
PYRepository: BoundaryML/baml
Length of output: 32062
🏁 Script executed:
python3 - <<'PY'
from pathlib import Path
files = [
"baml_language/crates/baml_compiler2_tir/src/normalize.rs",
"baml_language/crates/baml_compiler2_tir/src/interfaces.rs",
"baml_language/crates/baml_compiler2_tir/src/builder/associated_projection.rs",
]
for file in files:
p = Path(file)
text = p.read_text().splitlines()
print(f"\n### {file} ({len(text)} lines)")
if file.endswith("normalize.rs"):
for start, end in [(620, 780), (2235, 2715)]:
print(f"\n--- lines {start}-{end} ---")
for i in range(start-1, min(end, len(text))):
print(f"{i+1}:{text[i]}")
else:
# print only relevant snippets around candidate call sites
for needle in ["is_same_normalized_type", "match_impl_head", "Ty::EvolvingList", "Ty::EvolvingMap"]:
for idx, line in enumerate(text, 1):
if needle in line:
lo = max(1, idx-6)
hi = min(len(text), idx+12)
print(f"\n--- {needle} at {idx} ---")
for j in range(lo, hi+1):
print(f"{j}:{text[j-1]}")
break
PYRepository: BoundaryML/baml
Length of output: 29816
🏁 Script executed:
python3 - <<'PY'
from pathlib import Path
import re
text = Path("baml_language/crates/baml_compiler2_tir/src/normalize.rs").read_text()
# Extract the normalize_impl arms in a coarse way.
# We only care about surface Ty variants that map to the same StructuralTy head.
targets = {
"Ty::List": "StructuralTy::List",
"Ty::EvolvingList": "StructuralTy::List",
"Ty::Map": "StructuralTy::Map",
"Ty::EvolvingMap": "StructuralTy::Map",
"Ty::RustType": "StructuralTy::Unknown",
"Ty::Future": "StructuralTy::Unknown",
"Ty::Resource": "StructuralTy::Unknown",
"Ty::PromptAst": "StructuralTy::Unknown",
"Ty::WatchAccessor": "StructuralTy::Unknown",
}
heads_cases = {
"Ty::Class": "same-discriminant / nominal-name",
"Ty::Interface": "same-discriminant / nominal-name",
"Ty::EnumVariant": "same-discriminant / nominal-name",
"Ty::Literal": "same-discriminant / literal",
"Ty::Media": "same-discriminant / media-kind",
"Ty::TypeVar": "same-discriminant / typevar",
}
for ty, target in targets.items():
present = ty in text
print(f"{ty:20} -> {target:22} present={present}")
print("\nHeads filter explicit cases:")
for ty, why in heads_cases.items():
print(f"{ty:20} {why}")
print("\nWildcard note:")
m = re.search(r"_ if std::mem::discriminant\(lhs\) == std::mem::discriminant\(rhs\) => false,\n\s*// Different stable head constructors never normalize equal\.\n\s*_\s*=> true,", text)
print("wildcard+discriminant branch present =", bool(m))
PYRepository: BoundaryML/baml
Length of output: 1095
🏁 Script executed:
python3 - <<'PY'
from pathlib import Path
p = Path("baml_language/crates/baml_compiler2_tir/src/normalize.rs")
lines = p.read_text().splitlines()
# Print the test cases around the evolving list/map comparisons.
for start, end in [(2238, 2710), (2598, 2710)]:
print(f"\n--- {start}-{end} ---")
for i in range(start - 1, min(end, len(lines))):
line = lines[i]
if "EvolvingList" in line or "EvolvingMap" in line or "is_same_normalized_type" in line or "assert!" in line or "test" in line:
print(f"{i+1}:{line}")
PYRepository: BoundaryML/baml
Length of output: 2954
heads_definitely_differ needs same-family cases for constructors that collapse to the same normal form.
Ty::List/Ty::EvolvingList, Ty::Map/Ty::EvolvingMap, and Ty::RustType/Ty::Future/Ty::Resource/Ty::PromptAst/Ty::WatchAccessor all normalize to the same structural head, but they fall through to _ => true because their discriminants differ. That makes is_same_normalized_type return false without normalizing and can skip valid impl matches. Add explicit arms for these collapsing variants before the wildcard.
🤖 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/normalize.rs` around lines 90 -
129, Update heads_definitely_differ to treat List/EvolvingList, Map/EvolvingMap,
and RustType/Future/Resource/PromptAst/WatchAccessor as same-family constructor
pairs, returning false so their inner structure is normalized and compared. Add
these explicit match arms before the discriminant-based wildcard, while
preserving true for genuinely incompatible stable heads.
Source: Coding guidelines
…rness (BoundaryML#4038) ## Summary - Adds `baml_language/crates/tools_compile_profile`, a standalone black-box profiling harness for the compiler pipeline (parse → HIR → PPIR → TIR → MIR → emit). It reports wall-clock per phase, per-Salsa-query execution / cache-hit / blocked counts, cold-run variance, cold-vs-warm comparison (`--warm-runs`), and JSON output. - Ported verbatim from PR BoundaryML#4016 (`perf/compiler2-cold-compile`), with two adaptations: the `PROF_*` TIR audit counters (which only existed on that branch) are removed rather than ported, and the "July 2026 cold-compile audit" README section is annotated as a historical record predating BoundaryML#4032 / BoundaryML#3924. - Workspace changes: `mimalloc = "0.1.52"` added to workspace deps (used only by this tool, as its global allocator — noted in the README), and `"compile"` added to the `tools` namespace `approved_prefixes` in `stow.toml`. Provenance: BoundaryML#4016. The remaining findings from that audit are being re-landed as small independent PRs measured with this tool. ## Baseline on current canary (2660b8b) Protocol: `cargo build --release -p tools_compile_profile`, then `./target/release/tools_compile_profile baml_language/crates/baml_tests/baml_src --repeat 5` with `BAML_NO_BYTECODE_CACHE=1` and `BAML_CACHE_DIR` pointed at a fresh temp dir (so BoundaryML#3924's disk cache is out of the picture). Corpus: 77 files, 25,212 lines. Apple Silicon, single-threaded, medians of 5 cold runs. | phase | median | |---|---| | check | 1.089 s | | emit (bytecode) | 1.330 s | | **total** | **2.420 s** (min 2.385, mean 2.442, max 2.524) | For comparison, BoundaryML#4016's pre-optimization baseline on this corpus was 16.0s and its final state ~0.50s. Canary's own movement (BoundaryML#4032 deleting the old TIR type algebra, BoundaryML#3924's caching work) already brought cold compile from 16.0s to ~2.4s; the still-orthogonal optimizations from BoundaryML#4016 target the remainder. ### Top 10 queries by executions (cold, representative run) | exec | query | |---|---| | 15,590 | `infer_scope_types` | | 2,262 | `function_in_scope_generic_param_bounds` | | 2,256 | `function_body` | | 2,131 | `callable_throws` | | 625 | `class_generic_param_bounds` | | 477 | `resolve_class_fields` | | 419 | `function_parameter_defaults` | | 419 | `function_signature` | | 147 | `impl_data` | | 147 | `impl_data_source_map` | 25,972 total query executions; TIR is 18,697 of them (72%). ## Test plan - [x] `cargo build --release -p tools_compile_profile` clean - [x] Clean run against `baml_language/crates/baml_tests/baml_src` (`--repeat 5`) - [x] Pre-commit hooks: cargo fmt, workspace clippy (`-D warnings`), cargo stow, markdown validation Made with [Cursor](https://cursor.com) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced a standalone compiler profiling harness to measure compilation phase timings and query execution/caching behavior, including cold vs warm runs. * Generates human-readable and JSON reports, with optional compact summary output and validation-only mode. * **Documentation** * Added comprehensive documentation covering CLI usage, report interpretation, flamegraph pairing guidance, and profiling methodology. * **Tests** * Added tests for query-to-phase mapping and report-related behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cursor <[email protected]>
|
Superseded — closing in favor of the re-landed work, but keeping this branch as the reference implementation (do not delete). Canary moved underneath this PR, so rather than rebase it we re-derived each still-orthogonal optimization from scratch against current
Result on the test corpus (cold, disk cache disabled, medians of 5): current canary This PR's branch |
…ect-wide tag map once, not per lowered function build_class_type_tags walked every file's item tree and re-rendered + re-hashed every class's fully-qualified name inside LoweringContext construction — i.e. once per lowered function/let (~420x on the test corpus; it was the single hottest MIR frame in a CPU sample, plus most of the core::fmt time). Tags are content-addressed, so the map is a pure function of the project's classes: it is now a #[salsa::tracked] query keyed on the Project input, and LoweringContext borrows it like the rest of the package-invariant data. Re-land of #4016 audit item #4 (class_type_tags_for_project), which was initially assumed superseded by #3924's content-addressed tags — #3924 changed the tag values but left the per-function rebuild in place. Also adds the new tracked queries from this PR chain to the profiler's phase_for_query table (per its README convention) so they stop landing in the "other" bucket. Cold check+emit on the corpus: 0.99s -> ~0.85s (medians of 5, disk cache disabled); emit 0.64s -> 0.33s. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…ns (2.4s → 0.81s cold) (BoundaryML#4058) Re-lands the still-orthogonal cold-compile optimizations from BoundaryML#4016, re-derived from scratch against current `canary` (which moved underneath BoundaryML#4016 via BoundaryML#4032 and BoundaryML#3924). Not a rebase of BoundaryML#4016 — every change was re-derived and re-measured. BoundaryML#4016's single biggest win (recursive-alias hoist) is intentionally **not** here: BoundaryML#4032 already captured it by deleting the old TIR `StructuralTy` algebra. The profiler itself already landed separately as BoundaryML#4038. ## Measurement Corpus: `crates/baml_tests/baml_src` (77 files, 25,212 lines). Protocol: `tools_compile_profile ... --repeat 5`, disk cache disabled (`BAML_NO_BYTECODE_CACHE=1`, fresh `BAML_CACHE_DIR`). Cold-cache medians of 5 runs, single-threaded. | | check | emit | **total** | |---|---|---|---| | canary (`8c29c827e`) | 1.114 s | 1.277 s | **2.392 s** (min 2.354 / max 2.537) | | this branch | 0.462 s | 0.345 s | **0.808 s** (min 0.791 / max 0.828) | **3.0x faster** cold check+emit, single-threaded. (For reference, the pre-BoundaryML#4032 baseline this work originally started from was ~16 s; BoundaryML#4032 alone brought cold compile to a few seconds, and this branch takes it under ~1 s.) Key query-count deltas (cold, corpus): `infer_scope_types` 15,590 → **13,331** (PPIR→HIR `file_semantic_index` delegation removes duplicate scope inference); `package_resolved_aliases` / `package_impl_locs` no longer rebuilt inside every one of those inference calls (now a handful of per-package executions); new memoized queries `file_ast` (131, once/file), `callee_generics_for_func` (1,834), tracked PPIR `function_body`. ## What's in it (one commit per track) - **`file_ast` tracked query** — lower CST→AST once per file (items + lowering diagnostics + env refs), shared by both `file_semantic_index` queries, `ppir_expansion_items`, the project-wide expansion collectors, and the LSP check path; PPIR `file_semantic_index` delegates to HIR's when a file has no `$stream` expansions; PPIR `function_body` tracked. - **package-level TIR queries** — `package_resolved_aliases` (+ `cycle_initial` seeding an empty env, mirroring `infer_scope_types` — it sits in a real salsa cycle via associated-type-projection alias RHS) and `package_impl_locs` as tracked queries, plus `callee_generics_for_func`, so the alias map / impl-block list / callee generics stop being rebuilt per inference call. - **nested-lambda inference projection** — lambda bodies were inferred twice (inline in the owner scope, then again by the standalone `ScopeKind::Lambda` query), which also emitted duplicate diagnostics inside lambdas. The inline pass now captures the lambda's tables and the Lambda arm projects them; synthetic desugared `test`/`testset` bodies fall through to standalone inference so their diagnostics are still emitted. Snapshot updates where the duplicate lambda diagnostics disappear are the point. - **MIR dispatch prefilter + subtype fast paths** — `dispatch_target_for_concrete` gates its per-call impl enumeration behind a package-wide `FxHashSet` of interface-declared method names (own package + dependency closure); `baml_type::normalize` gets a reflexivity + `heads_definitely_differ` fast-reject in `equivalent()` (conservative: same-kind nominal pairs only — List/EvolvingList collapse to the same canonical head post-BoundaryML#4032) and restricts `is_subtype_of` co-inductive assumption bookkeeping to the expanding arms (Mu / TypeVar / AssociatedTypeProjection) via `is_subtype_of_inner`, with a termination argument in-comment. Re-derived onto the post-BoundaryML#4032 `baml_type` algebra (the only equivalence path now). - **memoized `class_type_tags_for_project`** — the project-wide class → type-tag map was rebuilt (every file's item tree walked, every class name re-rendered and re-hashed) inside every `LoweringContext` construction, i.e. once per lowered function (~420x on the corpus; the hottest MIR frame in a CPU sample). Now a `#[salsa::tracked]` query keyed on the `Project` input; `LoweringContext` borrows it. This is BoundaryML#4016 audit item BoundaryML#4, initially assumed superseded by BoundaryML#3924's content-addressed tags — BoundaryML#3924 changed the tag *values* but left the per-function rebuild in place. Also adds this PR chain's new tracked queries to the profiler's `phase_for_query` table. - **match usefulness report reuse + emit buffer pre-sizing** — match checking ran the full usefulness matrix twice per `match` (exhaustiveness, then an identical second pass for unreachable-arm detection whenever no arm had a pattern error); the reachability pass now reuses the exhaustiveness report (exhaustiveness: ~11% -> ~1.7% of CPU inclusive). `StackifyCodegen` pre-sizes its bytecode/meta buffers and local/block maps from the MIR's shape instead of growing from empty per function. - **CLI mimalloc + diagnostic rendering** — mimalloc as `baml_cli`'s global allocator; build the ariadne `SourceCache` once per diagnostic batch instead of once per diagnostic. Verified byte-for-byte identical rendered diagnostics and clean `BAML_CACHE_VERIFY=1` (so BoundaryML#3924's cached-diagnostic replay does not diverge). ## Deliberately not re-landed - The BoundaryML#4016 recursive-alias hoist — superseded by BoundaryML#4032. - Skip-builtin-diagnostics — superseded by BoundaryML#3924's per-toolchain builtin-diagnostics cache. ## Testing Full workspace test suite green except two pre-existing/environmental failures unrelated to this change: a Python cancellation pytest that fails identically on clean `canary` (local Python < 3.11: `ExceptionGroup`/`CancelledError.reason`). `cargo fmt` + `clippy -D warnings` clean. Provenance: BoundaryML#4016 (reference implementation, kept as reference, not merged). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <[email protected]>
TL;DR
Cold
check+emiton the in-repo test corpus (78 files, 25,838 lines) goes from 16.0s → ~0.50s (~32×). End-to-endbaml checkwall time on the same corpus goes from ~17s → ~0.55s. Everything here is single-threaded — no parallelism, no on-disk cache. Every win is one of three things:It also fixes two user-visible bugs found along the way (details below), and ships a new profiling harness (
crates/tools_compile_profile) whose README documents the entire audit so the next person doesn't have to rediscover any of this.Why
baml checkon a real project was taking ~17 minutes. The pipeline is Salsa-based, but a cold CLI run gets zero benefit from Salsa's memoization across invocations — so cold-path cost is pure single-run efficiency. Profiling (query-event counts +samplyCPU sampling) showed the time wasn't in any one algorithm; it was the same few computations being repeated tens of thousands of times.How to review this PR
Each change is independent and documented at its call site with a comment explaining what was being recomputed and roughly what it cost. The full narrative, methodology, and per-change measurements live in
crates/tools_compile_profile/README.md(section "The July 2026 cold-compile audit") — reading that first will make every diff hunk make sense.The changes
Stop recomputing package/project-invariant data (the big one)
find_recursive_aliases, a whole-alias-graph DFS)baml_type::ResolvedAliases(alias map + precomputed recursive set), threaded by reference through TIR/MIRTyclones + project walk) inside everyinfer_scope_typesexecution (~16k)package_resolved_aliases/package_impl_locstracked queriesfile_ast(db, file)tracked queryclass_type_tags_for_projecttracked querycallee_generics_for_functracked queryExprBodyarena per call siteppir::function_bodytracked, returnsArcStop doing the same inference twice
NestedLambdaInference, moved not cloned) and the standalone query projects them. This is also a bug fix: diagnostics inside lambdas were previously reported twice.$streamexpansions had their semantic index built twice (identical results); PPIR'sfile_semantic_indexnow delegates to HIR's.baml check(a fixed cost floor even for a 1-line project) even though it can't produce user-actionable diagnostics. Diagnostics collection now skips builtin files; builtin scopes user code touches are still inferred lazily.Make the remaining hot path cheap
baml_type::normalize): reflexivity short-circuit; co-inductive assumption bookkeeping (deep clone + full-tree hash per step) now only happens in the arms that can actually regress a cycle (μ-types, type vars, projections) — the millions of purely structural steps skip it. The safety argument is written out in a comment at the site.heads_definitely_differfilter: MIR impl dispatch probes are overwhelmingly misses between types with different nominal heads — now decided without the two allocation-heavy normalization walks.dispatch_target_for_concreteenumerated every impl block in the package closure for every method call and field access. A per-packageFxHashSetof interface-declared method names rejects the common plain-method case in one lookup (is_same_normalized_typecalls: 952k → 275k).baml_cli(and the profiler). System malloc was ~35% of remaining CPU — the workload is small-allocation heavy (Tytrees have deep value semantics). This is mitigation; the foundational fix (interningTy) is scoped as future work in the README.CLI-side bug fix
render_diagnosticsconstructed a freshSourceCache— a line index over every project file — for each diagnostic. With 79 warnings that was 34% of totalbaml checkwall time. Built once per batch now. (CLI 0.94s → 0.55s.)New:
crates/tools_compile_profileA standalone profiling harness that runs the exact
baml checkpipeline against any project directory and reports wall-clock per stage, Salsa query execution/cache-hit counts by pipeline phase, top queries, and a "suspect" table (high-exec, low-hit queries — i.e. caching gaps). Supports--warm-runs,--repeat,--json,--check-only, and pairs withsamplyfor line-level CPU profiles. The README documents how to use it, how to read its output, and the full audit above.Correctness
package_resolved_aliasesquery participates in a legitimate dependency cycle (aliases whose RHS is an associated-type projection resolve through inference, which consults the alias map); it now declarescycle_initialand seeds fixpoint iteration with the empty environment, mirroringinfer_scope_types.Numbers (test corpus, cold, median of 5)
baml checkCLI wallExplicitly out of scope (documented as future work in the README)
Tyinterning / hash-consing — ~25% of remaining self-time is still clone/drop/malloc ofTytrees with deep value semantics. Staged plan (internQualifiedTypeName→ internTy→ arenaNormalTytemporaries) is written up in the README.Made with Cursor
Summary by CodeRabbit
Performance
baml checkandbaml build, particularly on macOS.Bug Fixes
Developer Tools