crates/core/src/stack.rs — New primitives: #378
No reviewers
Labels
No labels
bug
dependencies
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
refactor
rust
technical-debt
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
navicore/patch-seq!378
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "heap-value-pop-push"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
crates/runtime/src/list_ops.rs — list.push rewritten:
Results: 944 unit tests pass, 443 integration tests pass, clippy clean, fmt clean.
Code Review — PR #378
Overall this is a clean optimization with good test coverage (944 unit + 443 integration tests passing). A few issues worth addressing before merge, ranging from a soundness concern to minor nits.
1.
'staticlifetime onheap_value_mutis unsound (important)The returned reference is not actually
'static— its true lifetime is bounded by the Arc on the stack (which in turn is bounded by the stack's lifetime). Returning'staticbypasses the borrow checker entirely, which means:&mut Valueand use it after the stack has been popped or freed, creating a dangling reference.heap_value_muton the same slot and hold two&'static mutreferences simultaneously, violating Rust's aliasing rules.The fast path in
list_pushhappens to be correct (the reference is consumed before any conflicting access), but the API makes it easy to write unsound callers accidentally. A bounded lifetime tied to the raw pointer's provenance would be safer:This keeps the unsafe contract explicit — callers choose
'aand must ensure it doesn't outlive the stack slot.2. Non-atomic sole-ownership check (correctness note)
heap_value_mutchecks sole ownership via:Arc::strong_countis explicitly documented as non-atomic. The oldpush_in_placeusedArc::get_mut(&mut outer_arc)which is the proper atomic sole-ownership gate. Although the doc comment notes strands are single-threaded, this divergence from the established pattern (Arc::get_mut) is worth flagging — it's a subtle regression in robustness and will be confusing to future readers expectingArc::get_mutas the sole-ownership idiom throughout the codebase.3. Missing sentinel-value coverage in
heap_value_mutThe inline-value guard currently checks three cases:
If the tagged-value encoding has any additional sentinel (e.g., a nil/null tag, a unit tag, or anything else that isn't a heap pointer), it would fall through to
Arc::from_raw, which would be UB. It would be worth either adding an explicit assertion, aTAG_NILcheck if one exists, or a comment that these three are exhaustive.4. Subtle borrow-across-
popin the fast pathdatais a&mut VariantDataderived from theValueat sp-2.popdecrements sp and reads/clears sp-1, so it doesn't actually touch sp-2's memory — the fast path is correct. But this relies on a non-obvious invariant about the stack layout, and the'staticlifetime ondatameans the compiler won't catch it if that invariant is ever broken by refactoring. A comment here explaining whypopis safe whiledatais live would help maintainability.5. Migration doc references a non-existent function
docs/design/PEEK_HEAP_MUT_MIGRATION.mdline 45 shows:peek_heap_mut_thirddoesn't exist yet (it would be added whenmap.setis migrated). The example is a forward-looking sketch, but it could be confusing. Marking it as pseudocode or adding a note like// peek_heap_mut_third would be added alongside map.setwould clarify intent.Minor / Positive notes
push_to_variantrefactor from "main COW helper" to "slow-path helper" is clean; the naming in the updated doc comment accurately reflects its new role.push_in_placeto a thin alias rather than duplicating the logic is the right call for the v4.x compat shim.heap_value_mutclearly calls out the "MUST NOT move or replace" invariant — that's good.Summary: Items 1 and 2 are the most important to address before merge — specifically the
'staticlifetime (soundness) and the regression fromArc::get_muttostrong_count(correctness robustness). Items 3–5 are lower priority but worth fixing in the same pass since the code is already under review.Code Review - PR #378: heap_value_mut stack primitives + list.push fast path
Overall this is a well-motivated optimization with good documentation. The core idea - avoid the Arc alloc/dealloc round-trip for sole-owned values via Arc::get_mut - is sound. A few items worth addressing before merge:
Potential Soundness Issue - Mutable reference across pop call
crates/runtime/src/list_ops.rs - fast path
datais a&mut VariantDataderived from the Arc atsp-2at the momentpeek_heap_mut_secondwas called. Afterpop(stack), the stack pointer advances and what wassp-2is nowsp-1. The reference remains valid only becausepopdoes not zero/free the underlying memory - it only decrementssp.This is a load-bearing implicit invariant. It should be made explicit with a
SAFETYcomment in the calling code: "pop only decrements sp and does not overwrite sp-2; the Value at the old sp-2 slot remains live and unaliased."Without this documentation, a future refactor to
pop(e.g., zeroing slots for GC-friendliness) could silently introduce a use-after-free.Missing SAFETY comments at call sites
The three new functions in
stack.rshave excellent doc comments explaining the invariants they require. However, the call sites inlist_ops.rsshould have matchingSAFETYannotations explaining why those invariants hold at each call site. This is standard practice forunsafecalls in Rust and is whatclippy::undocumented_unsafe_blocksenforces.Bounds checking in peek_heap_mut / peek_heap_mut_second
stack.sub(N)is unconditional pointer arithmetic. If the stack has fewer thanNitems, this is immediate UB. TheOptionreturn only discriminates between heap and inline values - it does not protect against an empty/shallow stack.The callers are responsible for ensuring sufficient stack depth (which
patch_seq_list_pushpresumably guarantees by contract), but this precondition is not documented on the functions. Either add a safety precondition note, or consider a debug-mode depth assertion.let-chain syntax - MSRV check
The
let ... && let ...chain syntax was stabilized in Rust 1.88 (May 2025). Please confirm this is at or below the project MSRV. If not, a nestedif letis needed.mem::forget in heap_value_mut - fragility note
The
Arc::from_rawtomem::forgetdance is correct but fragile: any early return between those two lines would drop the Arc and corrupt the stack refcount. Consider a comment block clearly marking the "do not drop this Arc" region, or a ManuallyDrop wrapper.Missing benchmark numbers
The PR claims "Zero Arc alloc/dealloc" for the fast path. Including at least one microbenchmark result (before/after) would validate the claim and give future readers a baseline. The design doc already mentions "benchmark verification" as a checkpoint.
Minor: patch_seq_list_push_in_place deprecation
The doc comment labels this "to be removed in v5.0." Adding a
#[deprecated]attribute or a tracking issue link would make it easier to find at cleanup time.What looks good
heap_value_mut->peek_heap_mut->peek_heap_mut_second) is clean and well-layered.Arc::get_mutreturningNonefor shared lists - no silent data corruption possible.PEEK_HEAP_MUT_MIGRATION.mdis a useful artifact for reviewers and future contributors.list.push!alias reduction is the right call; a 55-line duplicate was a maintenance hazard.Summary: The core optimization is sound. The main asks before merge are: (1)
SAFETYcomments at thelist_ops.rscall sites, (2) explicit documentation of the "pop does not invalidate sp-2" invariant, and (3) MSRV/let-chain verification. The bounds-checking and benchmark gaps are lower priority but worth noting.Code Review — PR #378 (stack.rs + list_ops.rs)
What's good
heap_value_mutcorrectly usesArc::get_mut(the atomic sole-ownership gate) rather thanArc::strong_count. This matches the established COW idiom in the codebase.'alifetime annotation is present, and the doc comment explicitly states the caller must ensure the reference doesn't outlive the stack slot — correct contract for anunsafefn.push_in_placereduced to a thin alias is the right call for a compat shim; no logic duplication.push_to_variantdoc comment update ("slow-path helper") accurately reflects its new role.Issues worth addressing
1. Double
Arc::get_mutoverhead in the fast path (minor perf note)peek_heap_mut_secondalready callsArc::get_muton the outerArc<Value>to produce&mut Value. Then the fast path callsArc::get_mutagain on the innerArc<VariantData>. Two atomic refcount checks for one mutation. This is unavoidable given the two-level Arc structure (Arc<Value>→Value::Variant(Arc<VariantData>)), but it's worth a comment so future readers don't wonder if the second check is redundant.2. Borrow-across-
poprelies on non-obvious stack invariantdata: &'a mut VariantDataborrows from the Arc at sp-2.popmodifies sp-1 and decrements sp — it does not touch sp-2's memory, so the borrow is valid. But this relies onpop's internal implementation not touching sp-2. The'alifetime is unconstrained (chosen by the caller), so the compiler cannot catch a regression here ifpopis ever changed to clear or reuse stack slots beyond sp-1. A single comment explaining whypopis safe whiledatais live would make this easier to audit in the future.3. Inline-value exhaustiveness comment
The comment says "all non-heap encodings … exhaustively" — this is a load-bearing claim. If the encoding ever gains a new sentinel (unit, nil, etc.), the
Arc::from_rawcall below would be UB. Adebug_assertor a reference to where the encoding is formally defined (a single source of truth) would make this guard more resilient than the comment alone.Migration doc (docs/design/PEEK_HEAP_MUT_MIGRATION.md)
The pseudocode for
map.setcorrectly usesheap_value_mut(stack.sub(3))(not a newpeek_heap_mut_third— the generic fn already handles arbitrary depths). The candidate table and ordering look reasonable. One suggestion: add a note that the fast path formap.setwill needArc::get_muton the inner map structure as well (same two-level pattern as list), in case a future contributor misses that and only checks the outer Arc.Summary
The core optimization is sound —
Arc::get_mutis the right atomic guard, the fast/slow path fallback is correct, and the compat alias is clean. The main actionable items are:popis safe whiledatais live (borrow-across-pop).debug_assertbacking the "exhaustive" inline-value claim.Arc::get_mutpattern in the fast path for future readers.None of these are blockers, but items 1 and 2 would meaningfully improve future auditability of this unsafe code.