Repository navigation
Incremental compilation: content-addressed bytecode caching with per-file recompilation - #3924
Conversation
Tier 1 of the incremental-compilation plan (whole-project bytecode cache, Go-build-cache-shaped: no invalidation logic, every compile input folded into a content-addressed key). - emit_determinism test: two fresh ProjectDatabases over identical sources serialize to byte-identical borsh Programs, for both the stdlib-only fixture and the full baml_src project. This is the invariant content-addressing rests on; it also proves FileId assignment is deterministic (per-db counter + sorted discovery). - bex_cache: content-addressed store (bytecode/<xx>/<key>.bexc) with a versioned entry header (magic, format version, key echo, payload sha256), sha256 key over compiler fingerprint + options + manifest + sorted relative-path/content pairs (length-prefixed framing), compiler fingerprint = hash of the running executable memoized by (len, mtime), atomic temp+rename writes, silent-None loads, and an age-based trim. Not yet wired into the CLI — that lands next.
On `baml run` (project mode), compute the content-addressed key from the resolved sources before any compilation: a hit deserializes the cached Program and boots the engine directly, skipping typecheck and emit entirely (measured 7.8s -> 0.49s on a debug-build cold/warm pair). A miss follows the existing compile path and writes through on error-free compiles only. Warnings are already not surfaced by `baml run` on success, so skipping diagnostics on a hit is behavior-identical. - project_load: split the strict load path into resolve_project_sources (root + validated manifest + file contents, no db) and build_db_from_sources; load_project_from_inner now composes them, and the format-hint check reuses the in-memory sources instead of re-reading every file. - bytecode_cache (CLI): CacheContext computes the key (compiler fingerprint, OptLevel::Two, emit_test_cases, manifest, sorted root-relative path/content pairs). Knobs: BAML_NO_BYTECODE_CACHE=1, BAML_CACHE_DIR, BAML_CACHE_VERIFY=1 (tripwire: always compile, hard- fail if bytes differ from the cached entry). - bex_cache: load_raw for the verify path, cargo-style .gitignore in the cache dir, maybe_trim (5-day age, at most one scan/day). baml test is deliberately not cached yet: it needs the db for legacy HIR test discovery; that lands separately. Verified end-to-end: cold run writes an entry; warm run hits with identical output; source edit misses and recompiles; corrupt entry is silently recompiled; error projects cache nothing; BAML_CACHE_VERIFY passes; BAML_NO_BYTECODE_CACHE writes nothing; baml check unaffected.
Function::source_file is display/metadata-only (backtraces, event metadata, reflection locations) — never opened from disk — so strip the project root at emit. Serialized Programs become location-independent (a cached or packed blob no longer embeds the machine's project path) and runtime backtraces are machine-independent. <builtin>/ stubs and paths outside the root are kept verbatim. Verified: cache blob for a test project contains zero absolute-path occurrences and the root-relative baml_src/main.baml; emit-determinism and bytecode-display tests pass.
On a bytecode-cache hit, baml test now skips the database entirely
(typecheck, HIR discovery, emit — measured 19.5s -> 0.63s debug
cold/warm): the compiled Program already carries everything the run
needs. Testsets execute inside the VM off the blob, and the legacy
test list is reconstructed from Program::test_cases — the same
(test, target functions) pairs HIR discovery yields — with the target
function's source_file supplying the --list display path. Cache key
uses emit_test_cases: true, so run and test blobs coexist.
Legacy discovery now renders root-relative paths (matching emit), so
--list output is byte-identical between compiled and cache-served
runs — verified on a project with both a testset and a legacy
functions-block test.
Note: legacy functions-block tests fail identically on both paths
today (FunctionNotFound { name: "[Greet]" } — the list rendering of
the functions ref leaks into the lookup name); pre-existing on canary,
not addressed here.
Two changes that together make the compiled stdlib user-independent — the prerequisite for a precompiled stdlib artifact: 1. compiler2_all_files now returns builtin stubs BEFORE user files. Everything assigned by whole-project iteration order (emit slots, MIR class_type_tags) gives builtins a stable per-group prefix; within-group order is unchanged and no package receives contributions from both groups, so HIR merge behavior is unaffected. 2. Emit passes 1-4.6 are extracted into emit_file_group, run once for the builtin group and once for the user group (EmitTables threads the accumulated name->index maps across groups so user code can reference builtin items). Previously each pass looped all files, so segments interleaved (classes B,U / enums B,U / ... / functions B,U) and builtin enum/function/let indices shifted with user class counts. Now the ObjectPool and globals table are [stdlib...][user...] and the stdlib prefix is byte-stable for a given compiler build. Per-group Pass 1 re-syncs its slot counter to program.globals.len() because $init/helper synthesis consumes slots beyond the named items; package_init_order accumulates across groups (builtins first, as the loader already expects). Also fixes latent nondeterminism in Pass 4.6: the $init_test chainer loop iterated a HashMap while allocating objects/globals — benign today only because a single package ever carries $init_test functions; now sorted. Full baml_tests suite passes; only the two bytecode-display snapshots churn (pure index renumbering). Emit determinism holds (compile-twice byte-equality on the stdlib fixture and full baml_src).
The Go model (post-1.20 GOROOT/pkg removal): the stdlib slice is materialized into the cache on first use, keyed only by compiler fingerprint + opt level, then spliced into every subsequent compile — of any project — instead of re-lowering 55 builtin files per compile. Emit: - generate_stdlib_program(db, opt): full pipeline over the builtin file group alone. Because builtins occupy a contiguous user-independent prefix of every index space (previous commit), the result is byte-identical to the stdlib prefix of any full compile. - generate_project_bytecode_with_stdlib(db, options, opt, base): seeds the output Program and EmitTables from the base slice (name->slot maps from the Program; class/enum/interface metadata recovered from the pool objects; type-tag counter = class count; per-package impl_rules/recursive aliases cleared so the trailing whole-program passes regenerate them verbatim), then emits only the user group. Skips all builtin body lowering — and because salsa is lazy, stdlib TIR inference never runs either. - Oracle test: splice output is asserted byte-identical to a full compile for both fixtures, with the base built from a DIFFERENT project's db — proving the slice is genuinely user-independent (this also settles that builtin dispatch fast-paths don't absorb user impls). CLI: compile_program(db, options, cache) reuses/materializes the stdlib entry on the project-miss path for both baml run and baml test; first-ever run pays roughly one full compile (the stdlib build and the splice share one salsa db, so stdlib queries are already memoized). Measured (debug build): edit->run loop 5.6s (full compile) -> 3.4s (splice); unchanged rerun stays 0.6s (project-blob hit); BAML_CACHE_VERIFY passes over the spliced path.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds content-addressed bytecode caching, relocatable linking, deterministic compiler emission, incremental diagnostics and throw analysis, cache-aware CLI and LSP flows, byte-identical compilation oracles, and refreshed artifact size baselines. ChangesCache-backed compilation
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 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 |
Conflicts: the two bytecode-display snapshots (canary's Array.sum stdlib addition vs this branch's index regrouping) — regenerated on the merged tree. B-628's consume_flat_report fix merged cleanly with the cache restructure. Determinism + splice-oracle tests pass on the merged stdlib.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
baml_language/crates/bex_cache/src/lib.rs (2)
66-96: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winSorted-files invariant only enforced in debug builds.
KeyInputs::filesdocuments a hard "sorted by path" precondition, but the only enforcement isdebug_assert!(line 87-90), which is compiled out in release. If an upstream caller (e.g.CacheContext::open) ever regresses the pre-sort guarantee, this silently degrades to a discovery-order-dependent key in production, defeating cache hit-rate without any signal — the exact kind of contract break the module doc warns about ("Key completeness").Consider sorting defensively inside
compute_keyitself (cheap relative to compilation) instead of just asserting, so correctness doesn't depend on every future caller preserving order.♻️ Proposed defensive sort
pub fn compute_key(inputs: &KeyInputs<'_>) -> CacheKey { let mut h = Sha256::new(); h.update(MAGIC); h.update(FORMAT_VERSION.to_le_bytes()); h.update(inputs.compiler_fingerprint); h.update([inputs.opt_level, u8::from(inputs.emit_test_cases)]); match inputs.manifest { Some(m) => { h.update([1u8]); h.update((m.len() as u64).to_le_bytes()); h.update(m.as_bytes()); } None => h.update([0u8]), } h.update((inputs.files.len() as u64).to_le_bytes()); - debug_assert!( - inputs.files.windows(2).all(|w| w[0].0 <= w[1].0), - "KeyInputs::files must be sorted by path" - ); - for (path, content) in inputs.files { + let mut sorted: Vec<&(String, &str)> = inputs.files.iter().collect(); + sorted.sort_by(|a, b| a.0.cmp(&b.0)); + for (path, content) in sorted { h.update((path.len() as u64).to_le_bytes()); h.update(path.as_bytes()); h.update((content.len() as u64).to_le_bytes()); h.update(content.as_bytes()); } CacheKey(h.finalize().into()) }🤖 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_cache/src/lib.rs` around lines 66 - 96, The sorted-by-path precondition for KeyInputs::files is only checked with debug_assert! in compute_key, so release builds can silently produce order-dependent cache keys if a caller regresses. Update compute_key in bex_cache::lib to defensively enforce sorting itself, using the existing KeyInputs::files input before hashing so correctness does not depend on upstream callers like CacheContext::open preserving order.
342-354: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winPrefer
tempfile::NamedTempFileover hand-rolled atomic write.
write_atomicderives its temp path fromstd::process::id()alone, so two same-process, same-keystore()calls (e.g. concurrent compiles in a future multi-threaded flow) would race on the identical tmp path.tempfileis already a dependency (dev-only) and itsNamedTempFile::new_in(...).persist(...)pattern gives OS-guaranteed unique names plus atomic replace, removing this whole hazard class with less custom code to maintain.♻️ Proposed refactor using tempfile
-fn write_atomic(path: &Path, data: &[u8]) -> io::Result<()> { - let tmp = path.with_extension(format!("tmp.{}", std::process::id())); - let result = (|| { - let mut f = fs::File::create(&tmp)?; - f.write_all(data)?; - f.flush()?; - fs::rename(&tmp, path) - })(); - if result.is_err() { - let _ = fs::remove_file(&tmp); - } - result -} +fn write_atomic(path: &Path, data: &[u8]) -> io::Result<()> { + let dir = path.parent().expect("entry path has parent"); + let mut tmp = tempfile::NamedTempFile::new_in(dir)?; + tmp.write_all(data)?; + tmp.flush()?; + tmp.persist(path).map_err(|e| e.error)?; + Ok(()) +}And move
tempfilefrom[dev-dependencies]to[dependencies]inCargo.toml.🤖 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_cache/src/lib.rs` around lines 342 - 354, `write_atomic` currently builds a temp filename from only `std::process::id()`, which can collide for concurrent writes in the same process; replace this hand-rolled temp-path logic with `tempfile::NamedTempFile` in `write_atomic` and persist it atomically to the target path. Update the `bex_cache` crate so `tempfile` is available at runtime by moving it from dev-dependencies to dependencies, and keep the `store()`/`write_atomic()` flow using the new unique temp-file approach.
🤖 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/bex_cache/src/lib.rs`:
- Around line 66-96: The sorted-by-path precondition for KeyInputs::files is
only checked with debug_assert! in compute_key, so release builds can silently
produce order-dependent cache keys if a caller regresses. Update compute_key in
bex_cache::lib to defensively enforce sorting itself, using the existing
KeyInputs::files input before hashing so correctness does not depend on upstream
callers like CacheContext::open preserving order.
- Around line 342-354: `write_atomic` currently builds a temp filename from only
`std::process::id()`, which can collide for concurrent writes in the same
process; replace this hand-rolled temp-path logic with `tempfile::NamedTempFile`
in `write_atomic` and persist it atomically to the target path. Update the
`bex_cache` crate so `tempfile` is available at runtime by moving it from
dev-dependencies to dependencies, and keep the `store()`/`write_atomic()` flow
using the new unique temp-file approach.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: b68544f6-6618-4281-8cb4-0105a0c7c6f2
⛔ Files ignored due to path filters (3)
baml_language/Cargo.lockis excluded by!**/*.lockbaml_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!**/*.snap
📒 Files selected for processing (13)
baml_language/Cargo.tomlbaml_language/crates/baml_cli/Cargo.tomlbaml_language/crates/baml_cli/src/bytecode_cache.rsbaml_language/crates/baml_cli/src/lib.rsbaml_language/crates/baml_cli/src/project_load.rsbaml_language/crates/baml_cli/src/run_command.rsbaml_language/crates/baml_cli/src/test_command.rsbaml_language/crates/baml_compiler2_emit/src/lib.rsbaml_language/crates/baml_compiler2_hir/src/lib.rsbaml_language/crates/baml_tests/Cargo.tomlbaml_language/crates/baml_tests/tests/emit_determinism.rsbaml_language/crates/bex_cache/Cargo.tomlbaml_language/crates/bex_cache/src/lib.rs
⏭️ Performance benchmarks were skippedPerf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to To run them on this PR, do any of the following, then push a commit (or re-run CI):
|
Binary size checks passed✅ 7 passed
Generated by |
Three independent pieces the per-file relink builds on: 1. Content-addressed class type tags: CLASS_BASE + fnv1a64(fq_name) masked to 47 bits (baml_type::typetag::class_type_tag), replacing the ordinal counter in both emit Pass 2 and MIR's build_class_type_tags — they now agree by construction instead of by iteration-order lockstep. A class keeps its tag no matter what other code exists, so tags baked into match tables, comparison chains, and constants never shift between compiles — eliminating tag relocation entirely (and giving dynamically loaded classes order-independent tags). Hash collisions are a compile error. Codegen note: ordinal tags were small/consecutive, so some type switches compiled to a direct sparse jump table; hashed tags route these through the dense_tag perfect hash (+1 instruction per type match). Full suite passes. 2. bex_vm_types::relink::visit_index_operands: visits every cross-function index operand of a compiled Function (GlobalIndex / ObjectIndex in instructions, constants, class-init plans). The instruction match is exhaustive on purpose — an opcode added with an index operand fails compilation here rather than silently escaping relocation. 3. baml_cli::file_signature: per-file signature hash = source text with FUNCTION_BODY-family node ranges blanked (length-framed gap hashing; body content AND length excluded). Body edits keep the hash; declaration/type/field/attribute edits change it. TS's version/signature split at source level.
Both findings evaluated as legitimate (not false positives): - compute_key now sorts the file list itself instead of debug_assert- trusting callers: an unsorted caller in release would silently produce discovery-order-dependent keys (hit-rate loss with no signal; never incorrectness — content is fully hashed either way). - write_atomic temp names gain a per-process atomic counter and create_new, so same-process concurrent stores can't truncate each other's temp file (the pid-only suffix collided by construction in any threaded host); kept dependency-free rather than promoting tempfile to a runtime dep.
generate_project_bytecode_with_reuse(db, options, opt, base, prev, clean_files): user files listed clean are spliced from the previous Program instead of re-lowered — their TIR inference, MIR lowering, and emit never run (salsa is lazy). Everything else recompiles normally. Relocation is clone-rebase-remap over the previous program using the exhaustive operand walker: a clean file's functions+lambdas occupy a contiguous pool range (grouped by Function::source_file); refs within the range rebase by delta, GlobalIndex operands remap old slot -> fq name -> new slot, object refs to classes/enums/interfaces remap by name. Class type tags are content-addressed and never rewritten. Any construct the splice can't relocate — plus defensive invariants like global-slot misalignment — aborts with LoweringError::ReuseUnsupported so callers fall back to a full (stdlib-spliced) compile. The relink_oracle suite asserts byte-identity with a full compile across genuinely index-shifting edits: body-only edit, added function, added class (class-object translation), added lambda (range growth), deleted file — plus a probe test planting a marker in prev's cosmetic Function field and finding it in the relink output, proving the splice path executes rather than passing vacuously. Not yet wired into the CLI: invalidation policy (which files are clean) lands next with the manifest.
Completes Tier 3. Alongside each project blob the cache now keeps a manifest (fixed per-project key, overwritten each successful compile): per file — content hash, signature hash, defined symbol names, and referenced symbol names extracted from the compiled bytecode via the operand walker (so desugared references are included). On a project-key miss, plan_reuse diffs current sources against the manifest: dirty = changed ∪ added ∪ files whose referenced names intersect Δ (the last-segment names defined by signature-changed, added, or removed files — name-level matching deliberately catches shadowing and impl additions). One round suffices: signature changes only originate from edits. Everything else is clean and splices; diagnostics re-check dirty files only (a clean file references nothing whose signature changed, so its diagnostics match the previous error-free compile). Fallback chain: relink -> ReuseUnsupported -> stdlib splice -> plain; files whose previous object range is non-contiguous ($init let-helpers) are demoted individually instead of failing the relink. BAML_CACHE_VERIFY now forces the full path so the tripwire keeps covering key completeness, and BAML_CACHE_DEBUG=1 reports plan sizes and fallback reasons on stderr. Verified on a 74-file corpus: body edit -> 'reuse plan: 72 clean, 1 dirty', byte-verify passes. Honest numbers: the corpus relink saves only ~12% wall clock in debug — whole-package analyses that per-file reuse does NOT yet skip (throw-set graphs walk every function body at PPIR level, $init lowers every let, build_packages lowers every impl) dominate this interface-heavy corpus, and 'baml test --list' carries a ~30s deserialize+VM-discovery floor. Those are the named follow-ups; the correctness machinery (oracle-tested byte-identity, probe-tested splice execution) is what this PR establishes.
throws is inferred from function bodies, so it is interface the body-blanked signature hash deliberately cannot see: a body edit in one file can change the TRANSITIVE inferred throws of an untouched caller in another file — its stored throws_type metadata and, through catch lowering, potentially its bytecode. Splicing such a file would reproduce stale metadata (demonstrated: new oracle scenario fails without this fix by exactly the stale-throws bytes). The package-wide throw graph is already solved on the relink path, so the gate costs map lookups per clean-file function: compare each freshly solved transitive throws against the previous program's stored throws_type; any mismatch demotes that file to a normal recompile. No fixpoint needed — the graph is solved globally, so every affected file's own set differs and demotes independently.
…code operand walk
…lpers, drop reuse-path narration
…t solver maps and diff-framing comments
…rphaned doc, drop the assertion-free cold-open measurement
… drop PR-staging narration
Canary replaced the exit-blocking telemetry flush with a durable local queue, superseding this branch's flush-grace cap. The old telemetry.rs is deleted in favor of the new module tree; call sites are compatible. Also applies a nightly-clippy question_mark simplification in the new project_id walk.
What this does
BAML stops recompiling the world — across every command, across process restarts, with no daemon. On an 82-file project (release, medians):
baml runin 69ms (173×): the cachedProgramloads and the compiler never runs.baml test --listin 12ms (1403×): the cached discovery list serves and the engine never boots.baml checkis incremental too (4.9× warm): clean files' diagnostics — warnings included — serve from cache and render byte-identically.BAML_CACHE_REMOTE=<url>shares immutable entries through any HTTP GET/PUT store (CI fills, laptops read); a broken remote silently degrades to local.The design follows the convergent architecture of rustc/javac/tsc/Go/Roslyn (surveyed against their sources): persist per-unit typed interfaces and compiled artifacts, never the type-checker's state; re-derive cheap structure every run; gate blast radius on interface hashes; verify with oracles.
How it works
Content-addressed store, no invalidation logic: every compile input — a fingerprint of the compiler binary itself, options,
baml.toml, every file's (path, content) — folds into a sha256 key; the key is the address. Changed inputs are a miss; stale entries age out. Entry kinds: the whole-projectProgramblob, per-fileCompilationUnitentries (content-addressed by their own bytes; the manifest maps rel_path → unit_key), the per-project manifest, thetest --listdiscovery blob, and four per-toolchain build constants (stdlib bytecode, stdlib typed interface, builtin diagnostics — all keyed by fingerprint + opt level only). Every entry self-validates an 80-byte header (magic, format version, key echo, length, payload sha256) before a payload byte is trusted. Format ships as v1.Per-file relocatable units + load-time linker (B-693): each file compiles to a
CompilationUnitwhose cross-file references are symbolic —Local(i)for its own definitions,Import(name)otherwise, plus an export table.link(units)assigns final indices and patches operands through the same exhaustive operand walker the VM already uses — a new opcode with an index fails compilation rather than escaping relocation (this fired twice during development, in both directions: an added opcode forced classification; a deleted feature's opcodes forced removal). Class type tags are content-addressed (CLASS_BASE + fnv1a64(fq_name)) so baked dispatch tables never shift.Each unit carries its typed interface (javac-style unification): a borsh fragment of per-function exact
callable_throws, stored beside the bytecode under the same content key — artifact and interface cannot drift apart.Per-file typecheck skip: on a warm compile, only dirty files run inference.
callable_throwsand the stdlib interface seedspackage_interface, so a dirty file's inference consumes cached typed interfaces instead of pulling clean callees' bodies transitively. Emit provably never reads these seeds — a stale seed could only ever affect diagnostics, never bytecode.The invalidation layer (per-file in the manifest): content hash; signature hash (bodies blanked); layout hash (type/impl declarations only — a function-signature edit in a mixed file no longer dirties the world); defined/referenced/signature-referenced names; per-function throw facts. Dirty = changed ∪ added ∪ name propagation ∪ three mechanisms: a signature-meaning cascade (type-identity edits flowing through content-unchanged callees re-propagate to fixpoint), a throws-taint closure over reverse call edges pruned at closed
throwscontracts (throws is body-inferred but interface-visible), and a layout sentinel (files that bake field offsets/discriminants without naming the type — deliberately coarse: layout edits approach full recompile, and that's the sound trade).Failure posture: only error-free compiles are cached; torn/corrupt/missing entries degrade to recomputing that file (a missing unit dirties exactly its file), then to full compile — never an abort.
baml checkreads the cache but never advances the manifest (a check-written manifest would point at units that were never emitted).Correctness
Three tiers of verification:
BAML_CACHE_VERIFY=1): the honest full compile runs and every servable artifact is compared byte-for-byte — program, diagnostics, fragments, stdlib interface, builtin diagnostics, discovery list. Final pre-merge sweep: 15/15 scenarios × commands, zero bails; 15/15 stdout byte-identity vs no-cache; 545/545 tests.The diff also survived a 30-agent adversarial review (4 dimensions × 2 skeptics per finding); its two confirmed soundness findings (the signature-meaning cascade and a verify-oracle gating bug) are fixed in this PR with regression scenarios.
Knobs:
BAML_CACHE_DIR,BAML_CACHE_REMOTE,BAML_CACHE_VERIFY,BAML_CACHE_SAMPLED_VERIFY,BAML_CACHE_DEBUG, and per-layer opt-outs (BAML_NO_BYTECODE_CACHE,BAML_NO_STDLIB_INTERFACE_CACHE,BAML_NO_DIAGNOSTICS_CACHE,BAML_NO_CALLABLE_THROWS_CACHE,BAML_NO_DISCOVERY_CACHE,BAML_NO_LSP_CACHE_SEED).Measured (release, medians; 82-file corpus)
run)test --listcheck(no-change / body edit)Counters on a warm edit: reuse plan 81 clean/1 dirty · units written 1, reused 81 · scope inferences 15 (cold: 15,644) · stdlib derivations 0.
Not in this PR
functions [...]tests fail withFunctionNotFound { name: "[Greet]" }— filed as B-695.Design lineage: cross-language survey of CPython/Go/V8/rustc/TypeScript/JVM/.NET, a verified salsa-incrementality study (the LSP was already per-scope incremental; the CLI was cold-per-process — this PR is the disk-persistence answer, chosen over a daemon), and per-mechanism research into rustc's red-green/
.rmeta, javac's.classcompletion, tsc's.tsbuildinfo, Go's action IDs, and Roslyn's ref assemblies — reports inthoughts/antonio/(untracked).Summary by CodeRabbit
New Features
baml test --listdiscovery, with optional remote sharing and faster incremental reruns.Bug Fixes