Repository navigation
test(heap): pin write-barrier behavior through write_barrier itself - #5408
harivansh-afk wants to merge 240 commits into
Conversation
Two pieces, both inside the VM crates. `bex_vm::compiled_rt` is the one implementation of each operation that the interpreter and natively compiled BAML functions both run: the op helpers that were private methods on `BexVm` become `pub`, and the operations that lived inline in `run_compact`'s dispatch arms (array index load/store, field load/store, closure allocation, the int fast paths, negation) move into methods the arms now call. Behaviour-preserving. `Function.native_body` is a runtime-only, type-erased pointer to a compiled body for a `FunctionKind::Bytecode` function. The Bytecode arm of the call funnel, after telemetry entry and the frame push, runs the body instead of entering the run loop and completes the frame through `finish_bytecode_frame`, the epilogue factored out of `OpCode::Return`. To every observer (telemetry, `$trace`, call paths, the unwinder, stack traces, reflection) a compiled function is the bytecode function it replaces. `cur_pc` is parked on a sentinel while a body runs so the unwinder cannot match a handler inside the compiled frame. The `CallExactArgs` fast path falls back to the funnel for compiled callees. Bodies attach at load from `EngineConfig.native_bodies`, keyed by `Function::name`, in `convert_program_with_natives` beside `attach_builtins` and before compact code is prepared. Test: a hand-written body is indistinguishable from bytecode in result, telemetry records (by resolved name), an exception caught by the bytecode caller, and the innermost frame of the thrown error's trace. Co-authored-by: Claude Fable 5.1 <[email protected]>
One Rust function per admitted BAML function, with the VM's native call signature. Statically typed int/float/bool locals are raw i64/f64/bool; everything else is a `Value`. Every operation is a call into `bex_vm::compiled_rt`, so results and exceptions match the interpreter by construction. Control flow is structured (Beyond Relooper over the MIR graph; `structure.rs` with its tests comes from the earlier `bex_aot` backend unchanged). Errors are `Result<Value, Box<VmError>>`; a `catch` binds the thrown value into the handler's landing locals and jumps, as the interpreter's unwinder does within one frame. Admission is a fixpoint over the call graph: suspension points, calls that are not statically resolved admitted functions or non-yielding builtins, type arguments, captured locals, and anything needing a runtime type template keep a function bytecode, with a reason. Functions with loops or more than 64 statements are observed (never inlined; called through the VM's call path so telemetry records them); the rest fold into their caller. Reached through `baml_db::native`, like the rest of the compiler. `DESIGN.txt` is the decision record. Co-authored-by: Claude Fable 5.1 <[email protected]>
`baml pack --native --native-workspace <baml_language>` compiles the program as today, emits the native module for its admitted functions, writes a standalone cargo project that embeds the packed program and links the generated bodies, builds it with cargo, and places the binary at the output path. The generated crate embeds the exact program the bodies were compiled from, so the two cannot drift apart and no hash check is needed. `--native-report` lists what compiled natively and why the rest did not. `baml_pack_host` becomes a library plus a thin binary: `run_envelope` and `run_embedded` take the optional body table and pass it to the engine. Co-authored-by: Claude Fable 5.1 <[email protected]>
The compute-shaped subset is admitted (loops over arrays, recursion, class field arithmetic, a catch arm, a float loop), closures and generics are rejected with reasons, a caller of a rejected function is rejected for that reason, loops make a function observed, and the generated module parses as Rust with every body registered under its function name. Co-authored-by: Claude Fable 5.1 <[email protected]>
A VM call nested inside another VM call's arguments borrows `vm` twice (`vm.store_array_element(a, i, vm.load_array_element(b, j)?)`), which rustc rejects. Every sub-expression that touches the VM is now bound to a temporary before the statement that uses it, so each generated statement holds one VM call at most. Co-authored-by: Claude Fable 5.1 <[email protected]>
Canary and dev builds accept only artifacts from their exact commit. The native host is built from the workspace checkout, which may sit at another commit than the CLI that encodes the envelope; pass this CLI's fingerprint through `BAML_GIT_SHA`, the override `baml_artifact`'s build script already honours, so the host accepts the program it embeds. Co-authored-by: Claude Fable 5.1 <[email protected]>
Routing LoadField, StoreField, ContainerLen, Load/StoreArrayElement, AddInt..ModInt, Neg, MakeClosure and Return through shared helpers slowed pure bytecode, with nothing compiled. Interpreter A/B on aarch64, telemetry off, median of 9, fixed startup subtracted, against the merge base: fields +28% index_sum +14% arith +10% fib(30) ~0% Forcing #[inline(always)] on the helpers made it worse (arith +31%): the cost is the changed shape of run_compact, not a missed inline. With the arms restored and the native hooks kept (Function.native_body, the funnel branch, the fast-path guard) every workload is within noise of the merge base. Compiled code keeps calling compiled_rt, which composes the same semantic primitives (try_int, tagged checked arithmetic, resolve_index, the panic constructors) the arms use.
…der admission Frames. Calls between compiled functions were split into "folded" (a plain Rust call, no frame, for functions under 64 statements with no loop) and "observed" (through the VM's call path). A folded call skipped everything a frame carries, so recursion ignored MAX_FRAMES (deep(300) returned 300 where bytecode throws StackOverflow; deep(1000000) aborted the process on a native stack overflow), traces lost frames, and small functions vanished from default telemetry by a size threshold. Every compiled call now gets a frame: compiled_rt::call_compiled pushes one with no telemetry and calls the body directly when telemetry is off, and goes through the call funnel when it is on. Errors. Bodies return Result<Value, Box<VmError>> (CompiledBody) instead of the builtin NativeCallResult, whose error type has no context. A compiled catch calls BexVm::catch, which builds the error's baml.errors.Context with the unwinder's own rules (factored into in_flight_context) and pops the frames compiled callees left above the handler. Before, catching an error from an observed callee left its frame pushed and the next return popped the wrong frame (a VM panic); catch (e, ctx) got ctx = null; and a rethrow lost the original trace. Admission is one fixpoint: a function is admitted when its body generates and every user callee is admitted; the old one-pass recheck could leave a caller calling a dropped function. Newly compiled: classes and enums are found by their qualified name (the display-name lookup missed every user class), for-in over an array (an index walk with ArrayIterator's documented semantics), generic builtins with static type arguments (push, to_string), string literals from the program's string objects instead of an allocation per use. Defaulted parameters stay Value until MIR's OmittedArg test (with_default(1) returned 1, not 6), and unboxing is checked instead of reading zero. The table binds bodies by object index, with name and arity checked at load. Tests: VM tests with hand-written bodies for the frame limit, compiled catch and rethrow context, with telemetry on and off. End to end on aarch64, a probe program packed with --native matches baml run on every entry point with telemetry off and default.
…t of other consumers `pack --native` stamped the host with BAML_GIT_SHA set to the CLI's fingerprint whatever commit the workspace was at, defeating the check that exists to refuse a host built from other sources. It now requires the workspace to be at the CLI's commit, and the host derives the same fingerprint itself. The generated crate copies the workspace's Cargo.lock, so the host is built against the dependency versions the workspace tests (it resolved 482 packages fresh before). baml_db exposes the backend behind a `native` feature, enabled only by baml_cli and baml_tests, so syn and prettyplease no longer reach every consumer of the compiler (LSP, wasm, bridges).
…eal lines
A compiled body ran in a BytecodeFrame disguised as bytecode: cur_pc and the
frame's faulting_pc were parked on a sentinel no exception range covers, and
two dozen position readers interpreted a bytecode offset that did not exist
(traces named a function's last line-table line, raise evidence decoded no
opcode, the engine's shield check would hit `unreachable!` on one).
BytecodeFrame now carries `body: FrameBody` (Bytecode, or Compiled { line }).
The call itself (identity, locals, type arguments, telemetry, context) stays
shared; each position reader checks the body: the unwinder skips a compiled
frame's bytecode handlers, the cause and handler walks skip it, raise evidence
reports no pc and takes the kind from the throw, the engine's shield check
skips it, and calls made by compiled code record COMPILED_CALLER_PC. The
bytecode position stays on the frame, not in the enum, so the interpreter's
call and return path has no added check (A/B against the merge base: within
2% on every workload).
Compiled code keeps the current statement's line in a local and writes it to
its frame only when an error leaves the function or lands in its catch.
Lines use the bytecode rule, now one function both share
(baml_compiler2_emit::statement_line), so a compiled trace matches the
bytecode one frame for frame.
Tests: a VM test that an error leaves a compiled frame past its bytecode's
catch. End to end, a packed probe's full stack trace (function and line per
frame) matches baml run, telemetry off and default. Corpus: 5432 passed, 2
tolerated, as at the merge base.
An entry point the host calls (engine.call_function) had its frame pushed directly and its bytecode interpreted: native bodies ran only when one BAML function called another. A packed program whose hot function is the one the host calls ran no native code at all; the native bench's `run` is exactly that, so its array scenarios measured bytecode in both rows. set_entry_point now marks the entry frame compiled when its function has a body (not hooked, no type arguments), entering telemetry exactly as for a bytecode entry, and the outer exec loop runs a compiled top frame's body and completes it through Return's epilogue. That loop is where a suspended compiled body will resume. Also: the epilogue expects the result on the stack, as Return has it; a compiled function with no slots (zero arguments, zero locals) now pushes its result first instead of tripping the stack's bounds assertion.
… into bytecode
A compiled body ran to completion inside one Rust call, so anything that
needs the VM mid-body kept a function bytecode, and so did every caller:
await, spawn, sys ops, LLM calls, string templates and json.to_string
(stdlib BAML), callback builtins, function values.
A function that can suspend now compiles to a resumable state machine
(compiled_rt::ResumableBody) the VM drives. Its arguments and every local it
keeps across a suspension live in its frame's stack slots, which the
collector scans and forwards, and FrameBody::Compiled { resume } records where
it continues. It returns a Step:
Call the VM calls the callee through its call path, as the Call opcode
does (bytecode, LLM function, sys op, callback builtin, closure,
another resumable body) and resumes the body with the result.
Pause the VM pushes a telemetry-less frame running that one instruction
(Await, AwaitAny, Spawn), so their semantics are bytecode's exactly.
An error from the callee is delivered: the unwinder stops at a suspended
compiled frame and the body lands the error, with its context, in its catch
or returns it. Bodies that cannot suspend keep the structured form and direct
calls. The interpreter's Return arm gains one check: a callee returning into
a compiled frame hands control to the run loop. Interpreter A/B against the
merge base stays within 2%.
Admission no longer drops callers of uncompiled functions; they become
resumable. Also compiled now: wildcard catch arms (ThrowIfPanic), calls with
named or omitted arguments to a named callee (laid over the callee's slots at
compile time, as remap_call_arguments does at run time), named functions as
values, and non-generic classes as static type arguments and array element
types (push of instances, json.deserialize<State>). baml_compiler2_mir
exposes decl_link_name for the latter.
Native bench admission: 12 of 14 cases now compile entirely (prepare and
run); generate-sort and json-aggregate keep `.sort()` (interface dispatch).
Tests: a VM test of a resumable body calling bytecode, catching its error, and
passing an uncaught one through. End to end on aarch64, packed probes match
baml run with telemetry off and default: calls into bytecode and stdlib
BAML, json.to_string, templates, a sys op, compiled awaits of spawned
futures (a local kept across two), a failing future caught by a wildcard
arm, and the earlier probe (20 of 20 functions compiled). Corpus: 5432
passed, 2 tolerated, as at the merge base.
# Conflicts: # baml_language/crates/baml_compiler2_mir/src/lib.rs # baml_language/crates/baml_compiler2_mir/src/lower.rs
A resumable body saved every local into its frame's slots at each suspension and restored every one, with a checked unbox, on each resume. MIR temporaries count as locals, so a function with ~40 source locals spilled 128 slots per call into bytecode. Expose MIR's backward liveness (`live_in`) and save, per suspension point, the locals live into the block the resume continues at (less the local the resume writes) plus those live into its handler. Restores move into each point's own resume arm: a dead local keeps its default instead of being unboxed from a slot that was never written. b2_many (40 live locals, loop calling bytecode): 199.6 -> 119.4 ns/iter (bytecode: 127.1). fib and tight-loop controls unchanged.
…5289) Stacked on #5264. - **Frame size:** `BytecodeFrame` is back to canary's 112 bytes and `FrameBody` is gone. - A compiled frame's `instruction_ptr` is `RUNNING`, or an offset into a resume stub appended to its function's bytecode. Its `faulting_pc` holds its source line. - `BexVm::compiled_at` works out whether a frame is compiled, for the cold paths that need to know. - The interpreter loop matches canary except for one rarely-run `ResumeCompiled` arm. The `pc` stays inside the loop: handing it to the outlined handler had cost every opcode 8–29%. - Interpreter against canary 6e6878a (n=40, 11 workloads): within ±0.7%. On #5264, recursion was 4–15% slower. - **`await` / `await_any` / `spawn`:** in compiled code these run the interpreter's own instruction on the compiled frame, so no `Function` is built per await. A future that has already settled is read inline. - Settled `await`: 1.75 ns native against 21.1 ns bytecode, down from 442 ns on #5264. Peak memory is the same in both modes (30 MB, against 65 MB on #5264). - **Suspensions:** - A resumable body saves only the locals still live afterwards, using liveness that counts a call's or await's result as an assignment. - Locals assigned only outside loops are written to their slot once, at assignment, and never saved again. - Call arguments and type arguments go onto the VM stack and into a buffer the VM reuses, so `Step` is 16 bytes and a suspending call allocates nothing. - Resumable loops yield every 16th iteration, counted in a register. - **Native / bytecode** (same executable, n=24): | Case | Native / bytecode | |---|---| | fib(30) | 0.39 | | Tight loop | 0.03 | | Suspending loop | 0.05 | | Loop calling bytecode, few locals | 0.57 | | Same, ~40 live locals | 0.75 (1.15 on #5264) | | Callee creates a closure | 1.06 | | String interpolation | 0.98 | The closure case needs compiled closures to close the gap. - **Correctness:** - A raise inside a compiled `catch` keeps its cause chain through a cause slot. - `unreachable` throws a catchable `baml.panics.Unreachable`. - Line numbers follow bytecode's rule for spans that cross one line. - `call_compiled` checks every call against a stack floor read once per `exec`, so deep recursion raises a catchable `StackOverflow` and never aborts. Before, 535×3-local recursion in a spawned task aborted the process. - The pack host's worker threads get 8 MiB stacks. - **CI native bench:** 20 of 21 scenarios are unchanged against #5264. `allocation-high-retention` is slower (85 → 150 ms native; 453 ms in the VM). - Compiled loops now reach GC safepoints, so they run the same collections bytecode does (instrumented locally: 4 collections against bytecode's 4). #5264's native code ran 2 and let the heap grow 65 MB. - Outside GC, it is 3% faster than #5264. - **Cleanup:** - `NativeBody` can only be built through an `unsafe` constructor. - The unboxers that read a type mismatch as zero are removed. - `baml_pack_host` no longer depends on `bex_vm` directly. - `baml_tests` enables the native feature only in its own tests. - Docs link only public items.
…ecode alone For differential testing: the same binary, same program bytes and same argv run twice, once with the linked bodies and once interpreted, so the two runs differ only in which implementation of each function ran. An unset or unrecognized value keeps the binary's default; a binary without native bodies ignores the variable.
`release` is the manifest the native build always wrote (fat LTO, one codegen unit, opt-level 3, symbols stripped). `fast` compiles the generated crate at opt-level 1 without LTO, keeping line tables, for correctness runs that would otherwise wait on a release build; its speed and size say nothing about a shipped pack. The manifest text is one function of the profile, with its output under test.
…ever the cache The native bodies are compiled from the MIR the pack command lowers out of its own database and replace the bytecode of the same functions at run time, so a served `Program`, a served package output or a seeded database would make bodies and bytecode the outputs of two lowerings. A `CacheUse::StoreOnly` session opens a `CacheContext` that serves nothing (the honest path of the verify tripwire, without its compares) and still stores the compile's outputs, so the pack warms the cache for the commands after it.
`emit.rs` (2,795 lines) becomes `admit.rs` (admission and the program index), `analysis.rs` (per-function facts), `codegen/` (the generator: body shapes in `mod.rs`, then `stmt`, `expr`, `place`, `constant`, `call`) and `render.rs` (the module's text and the spellings of generated expressions). `bex_vm::compiled_rt` becomes `compiled_rt/` with the types and free functions in `mod.rs` and the `BexVm` methods by subject: `arith`, `heap`, `call`. Every item keeps its path. A pure move: the generated source for the backend's fixture and four corpus programs is byte-identical before and after. The generator's directory is `codegen` because `gen` is reserved in edition 2024.
`Analysis::new` structured every function's graph and turned a
structuring failure (an irreducible graph) into `Rejection::Invalid`,
keeping the function bytecode. A resumable body is emitted as a state
machine over its blocks (`loop { match bb }`) and never reads the
structure, so it was rejected for a shape it never uses.
The analysis now keeps the graph's shape (`Cfg`); `Generator::function`
structures it only for the inline shape, and `unwind_jump` reads the
generator's copy. A function that structures is emitted exactly as
before: the generated source for the backend's fixture and four corpus
programs is byte-identical. A rejected function now drops out in the
admission fixpoint's first round instead of before it; the fixpoint
recomputes every round from scratch, so the result is the same.
No unit test: MIR comes from BAML source, which is structured, so an
irreducible graph cannot be built from a fixture, and hand-building
`MirFunction` is not cheap. The structurer's own tests cover its
`Irreducible` error.
`BytecodeFrame::instruction_ptr` was `pub`, so a reader outside the interpreter could take a compiled frame's stub offset (or `RUNNING`) for a bytecode position. The design rule is that every reader of a position asks `compiled_at` first. The field is now `pub(crate)`; outside readers go through `BexVm::frame_position`, which returns `FramePosition::Bytecode(pc)`, `CompiledRunning` or `CompiledStub(offset)` and asks `compiled_at` itself. The one outside reader, `bex_engine`'s shield walk, is migrated and keeps its behaviour: a compiled frame has no shielded regions. The interpreter's own hot paths read the field as before.
`EngineConfig::native_bodies` is keyed by object index, with name and arity checked at load, not by function name. The stack guard runs on every compiled call against a floor read once per `exec`, not every sixteenth frame. The fib figure in section 9 predates the frame work; replace it with the native/VM ratios of the 2026-10-16 Freestyle run at 80d8310, telemetry off.
Admission ran over the root package's files only, so every stdlib body a compiled function called was a suspension into bytecode and the sorts, json and calls benchmarks spent their time there. Enumerate the program's whole file set instead (`world_files`: the root, the stdlib, every dependency it reaches), with the same skips bytecode emit applies; the program index already covered every package, so stdlib functions attach under the names the loader renders for them. The admission histogram now reports the root's numbers and the stdlib's separately (user 3138/4385 unchanged; stdlib 501 admitted, 528 rejected, no invalid MIR). The well-formedness test compacts the expected entry the way it compacts the source: an impl body's rendered name has spaces in it and `prettyplease` wraps a long entry with a trailing comma.
The emitter's index of the program was keyed by rendered display name: `pkg.Foo.m` for a namespace `Foo` with a function `m` and for a class `Foo` with a method `m` alike, so the second to be indexed replaced the first and a function could be compiled against another's object index. Key it by identity instead: the package placing a declaration (its ordinal in `Program::packages`, found by walking the program's edge tables from the root beside the database's) and its `DeclPath` there, read off the package tables the loader itself places objects from. The emit crate exposes the coordinates it slots declarations by (`function_coords`, `class_coords`, `enum_coords`), so both sides derive them from one function. Admission, the suspension fixpoint and the generator work on object indices; the rendered name is read back from the program only for the module's entry table, where the loader checks it, and for messages. A symbol is the name for readers plus the object index for uniqueness. Two objects at one identity (a malformed program) are detected at index construction, asserted in debug builds, and refused on every lookup in release so neither is compiled against the other.
A compiled body with thousands of locals takes tens of KiB of Rust stack, so 256 of them did not fit in an 8 MiB main thread minus the red zone: call_compiled raised StackOverflow at the floor, frames short of the limit the interpreter enforces (sg/u_huge_plain: 168 frames native vs 256 bytecode; hugecatch: depth 166 vs 254). The frame limit is now checked first and is the only check that raises. Below it, every path that runs a compiled body on the Rust stack (call_compiled, the call funnel's inline arm, an inline entry body in resume_compiled) goes through BexVm::with_stack_room: one compare of the stack address against the floor on the hot path; below the floor the body runs on a fresh 8 MiB segment (stacker::grow) with the floor set to that segment's for the duration and restored after. The floor is per stack (read once per exec and once per segment). wasm32 keeps an unknown floor and never grows. A new test recurses through 64 KiB frames to the limit, with telemetry off and on, on the test thread and on a 2 MiB thread, and asserts the catchable StackOverflow lands at exactly MAX_FRAMES. DESIGN.txt section 7 describes the mechanism; the pack host keeps its 8 MiB workers, which now size a task's first segment.
Compiled code resolves an interface method call, field, bound method or function exactly as the interpreter's VirtualCall, VirtualLoadField and MakeVirtual* arms do, through the same ImplResolver, with the same frame type arguments. Each compiled call site owns an inline cache (VirtualSite, a static in the generated crate): one atomic word pairing the receiver's type tag with the resolved callee, filled only for static implementations, with the implementation's frame type arguments beside it. Arguments are laid over the resolved callee's slots at run time, as the interpreter does. Also makes the VM test runner name interfaces, so hand-driven bodies can dispatch through them.
An interface method call on a receiver whose implementation is not known at compile time is a suspension point: the resumable body resolves the callee through its site's inline cache, pushes the arguments laid over that callee's slots, and requests the call (Step::Call), so a bytecode, generic or suspending implementation runs as it would from bytecode. Interface field loads and stores, bound method values and (Self as Iface).method values resolve inline. Array iteration keeps its special case; a call with a $trace attachment stays bytecode. The emitter needs the interface's object and a static instantiation, so the program index now lists interfaces, and the differential gains an interface probe.
A virtual call whose resolved callee is a compiled body that cannot suspend, called with no type arguments, runs it as a direct compiled call (the frame, the depth limit and the catch of a direct call) instead of suspending and requesting it from the VM; any other callee still goes through the request. The site cache also notes which heap image its frame's heads belong to, so a VM over another image of the same program resolves instead of reading them.
A function passed as a value keeps a GenericFunction wrapper in its VM frame. compiled_at read it without unwrapping, so a parked compiled frame looked like bytecode and cancellation popped it before its frame records, panicking in thread-finish telemetry. The differential now also fails on any Rust panic in stderr.
On x86_64-pc-windows-msvc at opt-level >= 1, LLVM inlined the fiber loop into corosensei's root `catch_unwind` and emitted a cleanup funclet that uses `rsi` without saving it. The CRT unwinder keeps its `FuncInfo*` in `rsi`, so any panic or forced unwind out of a compiled body faulted with 0xc0000005 in `__FrameHandler3::FrameUnwindToState`. Moving the loop into an `#[inline(never)]` function leaves the root frame with a plain call, and no funclet in bex_vm clobbers an unsaved callee-saved register. Verified on a Windows runner in #5394.
Adapt compiled bodies to the per-thread stack budget (#5386): VM frames reserve through reserve_call_frame, eval-stack growth is fallible, and compiled records are bounded by the fiber's stack_room instead of the removed MAX_FRAMES. Update test sources for const globals, let type, baml.json.stringify and the namespace collision check.
The thread stack budget lets bytecode recurse ~70k levels while compiled bodies stop at the fiber's limit, so depth probes now check that overflow is caught and depths stay stable instead of printing them. Bound RecursiveWork so RecursiveCancellation finishes within the probe timeout.
…ts (#5406) Stacked on #5264. ## Bug `should_yield_at_call` (from 450247c) was: ```rust self.early_yield.tick() && (super::gc_stress() || self.poll_for_yield()) ``` Under `BAML_GC_STRESS=1` the `||` skips `poll_for_yield()`, and that call is the only thing that restarts the early-yield countdown. The counter stays at 0, so the next `tick()` underflows: - **Debug assertions on:** panics with "attempt to subtract with overflow". In rust_differential, `NoLeaks` panics in all four gc-stress modes, and `RecursiveCancellation` times out in those modes. - **Release:** the counter wraps to `u64::MAX`. After the first expiry, neither compiled code nor the interpreter (which shares the counter) yields again, so gc-stress quietly stops testing the native backend. Production is not affected; only `BAML_GC_STRESS=1` takes this path. ## Fix Poll first, then force the yield under stress. The logic moves into `yield_at_call(stress)` so a unit test can exercise the stress path without the process-wide `OnceLock`. ## Testing - New unit test `call_safepoints_keep_yielding_under_gc_stress`: interval 3, 9 ticks, expects yields at 3, 6 and 9. With the old ordering it panics with the overflow. With the fix it passes. - `cargo test -p bex_vm --lib compiled_rt`: 44 passed. - `cargo fmt --check` and `cargo clippy -p bex_vm --all-targets` are clean. - Not run: the rust_differential `NoLeaks,RecursiveCancellation` probes with `RUSTFLAGS="-C debug-assertions=on"`.
Existing card tests call mark_card_for_ptr directly. These go through write_barrier and conservative_write_barrier, checking the card count and, for old-to-young stores, that the young object survives a minor collection with the card as its only root. One case spreads Gen2 over three chunks so a generation lookup that only finds the first chunk fails. Checked against two mutants: a barrier that never marks (3 failures) and a membership check limited to the first chunk (1 failure).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Warning Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption. Comment |
⏭️ 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 failed❌ 2 violations · ✅ 1 passed
Details & how to fixViolations:
Add/update baselines:
[artifacts.bridge_wasm]
file_bytes = 27362344
gzip_bytes = 8027815
[artifacts.packed-program]
file_bytes = 41372320
gzip_bytes = 16868167Generated by |
|
Closing: superseded by the shared-runtime consolidation tracked from #5566. Branch kept for reference. |
TLDR
Stacked on #5264. First of three write-barrier PRs (this → #5409 → #5410); tests only. The existing card tests call
mark_card_for_ptrdirectly, so nothing pins whatwrite_barrierandconservative_write_barrierthemselves decide. wb-2 and wb-3 change exactly that decision, so they need a fixed matrix to be checked against.How
bex_heap/tests/write_barrier.rs, 7 cases:gen2_dirty_card_count().conservative_write_barriermarks only Gen2 containers.Minor collections trace Gen0 and Gen1 together, so a Gen2 container holding a reference to something outside Gen2 is the only case that needs a card. The tests pin that.
Checked against mutants
write_barriernever marksDesign and decisions:
linear/native-perf/05-write-barrier-generation-check.md.