11 KiB
Carried-phase stack regression: source analysis
2026-09-25. Read-only source comparison: accepted predecessor 18073a6f2eabce83fc83997f094e331eec835a16 versus carried commit d11b99bb. No source/tests edited, no compiler/debugger/test executable run by this agent. Parent owns the exact-artifact LLDB investigation. These are hypotheses and conditional correction recommendations, not a proven failure cause.
Observed boundary
Parent reports all 12 new carried tests pass, but the full run aborts with Windows STATUS_STACK_OVERFLOW 0xc00000fd at the unchanged existing ordinary_index::tests::small_stack_success_and_late_header_failure_leave_no_partial_owner. Predecessor full run completed 230 pass / 16 inherited failures including this test. The new abort is therefore a current acceptance regression requiring investigation; the earlier unrelated environment-sensitive rename prerequisite observation does not waive it.
The recorded log ends at that test, with no phase markers. It does not say which of its three sections failed:
- Build rich OA bundle, parse/resolve/index/index-ordinary and drop success.
- Independently build the rich typed index, inject a new unowned name into its map, and require late ordinary missing-header rejection and consumed-owner cleanup.
- Construct the unchanged 127-nested-abs literal (128 expression nodes), then parse/resolve/index/index-ordinary and drop success.
Source: mixed/project/ordinary_index/tests.rs:416–445; helper indexed at line6 calls project/tests.rs:147 accepted, which calls resolve(parse(...).unwrap()). Stack allowance remains128 KiB. The test naturally exercises large value returns through several nested helpers; none of those helpers or fixture sizes changed in d11b99bb.
Two concrete source changes affecting stack shape
A. One extra shared-body wrapper on the legacy parse chain
Before: indexed -> accepted -> mixed::parse -> mixed::parse_with_profile [body].
After: indexed -> accepted -> mixed::parse -> mixed::parse_with_profile [forwarder] -> mixed::parse_impl [body].
References: mixed.rs:227–260; diff18073a6..d11b99bb. parse_with_profile now forwards bundle, ProjectLimits, Limits and a new None guard, returning a larger ParsedMixedSyntax Result. Even a tiny Rust forwarding function can have debug-build argument/return temporaries and a native frame. The source does not establish how much stack the compiler allocated or whether any forwarding call was optimized away. Inspect actual frame prologues before assigning blame.
The new checkpoint calls themselves run with carried=None on this existing test. CarriedInput::check, its temporary Ledger, and new carried arithmetic are not executed on the legacy path. Therefore adding a heap allocation inside those checks is not a justified fix for this test.
B. Inline guard makes every syntax/result carrier structurally larger
ParsedMixedSyntax gained Option<CarriedInput> (mixed.rs:46–50). CarriedInput contains SnapshotBacking by value. Its Borrowed variant stores both a bundle reference and the entire SnapshotPolicy by value (mixed/carried.rs:6–27); SnapshotPolicy holds all Limits and ProjectLimits fields. Even None in a legacy witness needs enough inline space for the type's largest Some variant. This increase propagates through ResolvedMixedProject.syntax, IndexedMixedProject.resolved, AuthoredMixedProject, Result temporaries and helper return slots.
On this Windows64 ABI the source policy contains six8-byte Limits fields and seven usize ProjectLimits fields (3 traversal +4 model), i.e.104 bytes of policy payload before the reference/discriminant/alignment. This is field arithmetic, not a measured Rust enum size or compiler frame size. Compiler layout/niches and debug temporaries must be measured from the exact artifact. The source is enough to identify a plausible repeated-inline-payload increase, not enough to quantify the resulting peak native stack.
References: budget.rs:7–14, source_project.rs:52–59, language.rs:32–37, mixed/carried.rs:6–27, mixed/project.rs:17–26,43–46. The retained variant already holds only a reference; its policy stays in the retained external owner. The borrowed variant is the asymmetry.
The test-only phase_state getter is not invoked by this existing test. The resolver's new scalar Slots seed executes but adds no new heap allocation or recursion. It also receives the now-larger syntax value, so its generated frame may grow even if its changed expression is small.
What the parent trace should distinguish
- Identify the exact stage from the active call stack: scanner/ordinary or typed parser, Exact numeric work, resolver/catalog/admission, index verification, ordinary late failure mapping, or drop. No test rewriting is needed to read the faulting call chain.
- Capture stack base/limit/current SP if available; record native frame deltas/prologues for the largest active frames, especially parse_impl, parse_with_profile, accepted/indexed test helpers, resolve and the worker closure. A tiny source function is not evidence its debug frame is tiny.
- If inspectable, compare the prechange and new generated frames or type layouts. At minimum confirm whether the added forwarding frame and inline carrier payload are present. Do not run application code through arbitrary debugger expressions solely to infer type size; symbols/disassembly suffice if available.
- A deepest frame inside BigInt/Windows allocation does not itself establish arithmetic recursion as the regression. The added ancestors may have reduced available headroom before an unchanged allocation call. Likewise a drop frame would require checking which owner is being dropped before any parser-only correction is selected.
- Repeat an exact artifact only as needed to establish whether this failure has the same environment sensitivity previously observed. A passing retry alone does not undo the failed full run or establish a fix.
Smallest allocation-free correction candidates, contingent on evidence
Preferred representation correction if enlarged inline carriers matter
Move the borrowed variant's policy payload to an external immutable fixture backing owner, mirroring the retained variant. For example:
struct BorrowedSnapshot<'s> {
bundle: &'s SourceBundle,
policy: SnapshotPolicy,
}
enum SnapshotBacking<'s> {
Borrowed(&'s BorrowedSnapshot<'s>),
Retained(&'s RetainedSnapshot),
}
struct CarriedInput<'s> { backing: SnapshotBacking<'s> }
All constructors remain cfg(test); fixture code holds the borrowed backing owner while returned syntax/resolved/indexed guards exist, exactly as it already holds RetainedSnapshot. The owner captures exact policy once; CarriedInput borrows it, fixes bundle identity and prevents premature drop. The guard then consists of tagged references instead of a copied104-byte policy payload. No new heap allocation, copied String, logical collection entry or uncharged Box is introduced. The policy is inline scalar state in its external owner, outside every syntax/result copy. Exact guard/layout sizes still need compiler verification.
This changes no check ordering, C arithmetic, Usage, model/public API, allocation ownership or legacy grammar. It strengthens symmetry of both guard variants and respects the accepted borrow-bound ownership design. Required test adjustments would bind a local borrowed fixture backing before obtaining its input; they should not change any expected models, limits, cap values, spans or stack sizes. Existing small-stack tests must remain completely unchanged.
This is more justified than boxing ParsedMixedSyntax/Project/whole Result: such boxing introduces a real allocation and a new accounting/lifecycle choice unrelated to the accepted logical scope. Do not silently add a Box to fix a native frame without its design and reservation being reviewed.
Smaller forwarding correction if the new wrapper is the material delta
For default legacy parse, call the shared parse_impl directly with None instead of routing through the newly trivial parse_with_profile forwarder; this restores the old number of parser-body callers on the actual failing indexed->accepted->parse route without duplicating grammar or touching checks. The tight-profile wrapper would still add a frame for its callers, so this is not a general representation fix.
Alternatively an explicitly forced-inline annotation on the tiny forwarding wrapper can remove that frame if the compiler honors it in this debug configuration. It is a code-generation hint, not a semantic stack guarantee, and must be verified from the new artifact and existing controls. Do not mark the entire large parser body always-inline: it can duplicate/inflate callers and makes the intended correction less predictable.
Do not simultaneously apply unrelated layout/algorithm changes before the trace identifies the relevant ancestors. A narrow representation correction may be preferable if payload replication affects multiple phases rather than one forwarder.
Acceptance after any selected correction
Preserve the original12 carried expected outcomes, all legacy phase timing, exact output-owner Slots and all nine pre-existing passing stack-named controls with unchanged fixtures/stack allowances. Verify production source review for constructor/lifetime/accounting changes and run parent exact-artifact focused/full gates. Retain d11b99bb's full abort record. A proposed fix is not accepted until the original failed control and required broader run complete at the corrected pin, with inherited failures reported separately.
No correction is implemented here. Await the exact-artifact trace before selecting between these concrete candidates or investigating another actual phase.
Trace update and selected candidate
Parent subsequently reproduced the exact-artifact failure in numeric parsing with the new parse_impl/parse_with_profile ancestors visible, and recorded prologues11,144/312 bytes respectively (other measured active ancestors accepted1,752/indexed1,704/worker7,752; not a total). Parent selected the independent critic's smaller representation correction over the external borrowed-policy-owner alternative above: consume the preparse CarriedInput, transfer policy into Budget + syntax.limits, and retain only a slim Borrowed/Retained-owner guard in syntax. Borrowed mode already keeps bundle identity via syntax.bundle; it needs no duplicated policy owner. This avoids a new fixture lifetime requirement or heap allocation, preserves exact policy after transfer, and removes the large inline policy from legacy carriers as well. Parent authorized that narrow source edit, preserving the wrapper and all existing stack tests. Candidate implementation and notes are submitted for review/gates; this report does not claim causal proof or accepted runtime recovery.