Skip to content

Project: decided storage — burn down the residual-bound stack-slot population #9371

Open

Description

@richlander

Project: decided storage — burn down the residual-bound stack-slot population to zero or a named floor

Companion to #2095 (the thin writer). #9247 moved the printer's residual slot policy into ResidualSlotBindingPass, so every stack-slot web that SlotMaterializationPass declines is now a counted, provenance-carrying pipeline fact instead of print-time policy. That made the remaining population visible; this project retires it. Design owner: docs/design/value-typed-emission.md (Instance 2 — stack-slot materialization and typing; Residual storage binding). The pass itself is frozen and shrink-only: every slice here lands in materialization or a testimony owner and is measured by the census moving a group to zero.

Why a separate program from #2095

  • Project: the thin writer — make CSharpPrinter a total function of a decided, fully-typed IR #2095 (Program A) removes decisions from CSharpPrinter and must be output-neutral: its gates are Render A/B valid→invalid 0 and no Full method rendering worse.
  • This program (Program B) changes output on purpose by typing more storage: its gates are the --residual-binding-census delta plus changed-method fidelity.
  • They touch disjoint code (Ir/CSharpPrinter* vs Passes/*), so one PR per program can be in flight at once. The shared contract is the pass→printer surface (Coerce nodes, ResidualSlotBindings, the local declaration plan); neither program widens it without the other's design section changing.

Baseline (main 5a00671a9, 14 assemblies / 89,065 methods, --residual-binding-census)

329 bound webs / 396 locals / 213 methods; 52 split webs; 0 late-decidable; 12 untyped visible failures; 1 web in a nested body. (The opening figures at 8076ab86f were 333 / 402 / 214 / 54; #9358, merged after #9247, removed two ConflictingTypeTestimony split webs and two IncompleteCopyComponent webs — see the re-measure comment.) --slot-residual-census post-materialization residual slots: 358 / 232 methods.

Webs Kind Veto flags Locals
114 Unified OutsideCoercionDomain 114
62 Unified OutsideCoercionDomain, IncompleteCopyComponent 62
58 Unified MissingLoad 58
21 Unified IncompleteCopyComponent 21
21 Split ConflictingTypeTestimony 52
18 Split OutsideCoercionDomain, UnrenderableStoreType 36
12 Unified UnderivableTypeTestimony 12
5 Unified MissingLoad, IncompleteCopyComponent 5
5 Split UnrenderableStoreType 10
3 Unified BooleanSinkIdentityRecovery 3
2 Split ConflictingTypeTestimony, IncompleteCopyComponent 7
2 Unified ConflictingTypeTestimony 2
2 Split BooleanSinkIdentityRecovery 4
2 Split BooleanSinkIdentityRecovery, IncompleteCopyComponent 4
2 Split OutsideCoercionDomain, UnrenderableStoreType, IncompleteCopyComponent 6

After #9390 (96d12bcb1): 317 bound webs / 383 locals / 201 methods; 51 split; 0 late-decidable; 4 untyped visible failures; 0 in nested bodies. Groups that moved: UnderivableTypeTestimony 12 → 0; OutsideCoercionDomain 114 → 115; MissingLoad 58 → 57; OutsideCoercionDomain+UnrenderableStoreType split 18 → 17; BooleanSinkIdentityRecovery 3 → 4 (integer loads passed to bool parameters now count as Boolean sinks).

After #9398 (2e9d47e5d): 204 bound webs / 270 locals / 143 methods; 51 split; 0 late-decidable; 4 untyped visible failures; 0 in nested bodies. Groups that moved: Unified OutsideCoercionDomain 115 → 49; Unified OutsideCoercionDomain + IncompleteCopyComponent 62 → 15.

Reproduce: dotnet run --project tools/DecompilerHarness -c Release -- <14 assemblies from eng/prepare-decompiler-corpus.sh> --residual-binding-census.

Docket (one group per slice; each slice names its real corpus witness, moves its group to zero or an explained floor, and runs changed-method fidelity)

Rules

  • ResidualSlotBindingPass gains no admission rule; a group retires by materialization or testimony typing the web before the pass runs.
  • Every slice publishes the census before/after at exact base/head and its changed-method fidelity; a Full-method fidelity regression is the stop signal.
  • Late-decidable webs (no veto at the pass position) are materialization's defect, not a binding target; the count stays 0.
  • Residual byref-like split pieces of one slot collide in the printer up-front declaration map #9367 (byref-like split pieces colliding in the printer's declaration map) is Program A's, since it is a printer defect.

Generated by Claude Code

Activity

  1. richlander commented on Oct 5, 2026

    @richlander
    OwnerAuthor

    Fresh --residual-binding-census update on exact main
    5a00671a924dc45866310fd1b6355af1729c7415, using the fixed 14-assembly,
    89,065-method corpus:

    Metric #9371 baseline Current Delta
    Residual-bound webs 333 329 -4
    Residual-bound locals 402 396 -6
    Methods with residual-bound webs 214 213 -1
    Split webs 54 52 -2
    Late-decidable webs 0 0 0
    Untyped visible failures 12 12 0
    Webs in nested bodies 1 1 0

    The group movement is confined to three rows:

    Kind Veto flags Before webs/locals Current webs/locals
    Unified IncompleteCopyComponent 23 / 23 21 / 21
    Split ConflictingTypeTestimony 22 / 54 21 / 52
    Split ConflictingTypeTestimony, IncompleteCopyComponent 3 / 9 2 / 7

    All other groups are unchanged. In particular, the materialization hand-off
    remains sound (Late-decidable stays zero), and the 12 untyped webs continue
    to fail visibly rather than becoming success-shaped output.

    Reproduction:

    bash eng/prepare-decompiler-corpus.sh /tmp/corpus-assemblies.txt
    mapfile -t assemblies < /tmp/corpus-assemblies.txt
    dotnet run --project tools/DecompilerHarness -c Release -- \
      "${assemblies[@]}" --residual-binding-census

    The census exits 1 by design for the 12 visible untyped-pass failures.

  2. richlander commented on Oct 5, 2026

    @richlander
    OwnerAuthor

    Adopted as the baseline: the body now reads 329 / 396 / 213 / 52 / 0 / 12 / 1 at 5a00671a9, with the three moved rows updated and the 8076ab86f opening figures kept as history. The movement is exactly #9358's expected effect (StackSlotLiveRangePass splitting the loop-contained entry-reset ranges upstream of binding: DiffCommand.BuildAnalysisDiff S_6/S_8 leave the ConflictingTypeTestimony split group), so no admission changed in materialization or binding. I will confirm with my own run at the same head when the current experiment finishes; the 12 untyped webs are the first slice's docket and are unchanged.


    Generated by Claude Code

  3. richlander commented on Oct 5, 2026

    @richlander
    OwnerAuthor

    Slice 1 is up as #9390: argument and comparison sinks testify for untyped slot loads, plus the object-carrier coalesce witness. At its head against the effective base fd5c6343f (14 assemblies / 89,065 methods): bound webs 329 → 317, locals 396 → 383, methods 213 → 201, split 52 → 51, late-decidable 0 → 0, untyped visible failures 12 → 3, nested webs 1 → 0. The UnderivableTypeTestimony group (12 webs) is gone; the three remaining failures are the two IncompleteCopyComponent chains (#9249's docket) and GetTypeInfoForNode's slot reuse (the ConflictingTypeTestimony docket). Render A/B against the same base: valid→invalid 0, 3 methods newly rendering, invalid→valid 1. The five enum carriers now bind as RefKind/SpecialType locals under the frozen policy because their integer-constant stores keep them out of materialization (UnrenderableStoreType): that is the next group to retire.


    Generated by Claude Code

  4. richlander commented on Oct 5, 2026

    @richlander
    OwnerAuthor

    Slice 1 merged: #9390 (96d12bcb1)

    Untyped visible failures 12 → 4; UnderivableTypeTestimony 12 → 0; nested-body webs 1 → 0; bound population 329 → 317 webs. Three review rounds (Anthropic seat): round 1 found that narrow comparison siblings truncated (byte S = c ? (byte)e : (byte)x), round 2 that a uint sibling on a signed clt flipped the comparison's signedness, round 3 clean. The rule that landed: a comparison sibling testifies only when its static type is exactly the operand type the IL comparison fixes — by family, width, and (ordering kinds) signedness.

    Docket items from the reviews (none blocking; pre-existing unless noted):

    • Enum comparison siblings decline until the derivation sees enum backing widths (new, by design in Let argument and comparison sinks testify for untyped slot loads #9390; added to the docket above).
    • Design wording: "floats are width-exact" is literally false for float32 and harmless only because the importer canonicalizes float joins to double; say so in the next slice touching that paragraph of value-typed-emission.md.
    • ClassicInverseSinkRules.SinkType maps argument and comparison sinks with different filters from CoercionSinks.LoadSinkTargetType; review for one shared abstraction.
    • An inline two-reference ?? passed straight to an object argument (no slot) still renders invalid C#; the witness only applies at a slot carrier.
    • Frozen-policy spellings that are wrong but contained: IB S_1 for vetoed narrow-reference-sibling webs; (BE)S_1 at a byte-backed enum comparison (truncates one step later); byte S_1 = !c ? b : se for a short/byte join — all ResidualSlotBindingPass policy outputs, candidates for the UnrenderableStoreType slice.
    • BooleanSinkIdentityRecovery 3 → 4: the Boolean-sink identity rule now sees argument sinks (BooleanSlotLoadType → SemanticLoadSinkTargetType); the new web is disclosed in the Let argument and comparison sinks testify for untyped slot loads #9390 body's group table.

    Next slice candidates, in docket order: OutsideCoercionDomain (coercion-domain admission, 196 webs) or the enum-sibling derivation above as a small follow-on.


    Generated by Claude Code

  5. richlander commented on Oct 5, 2026

    @richlander
    OwnerAuthor

    Slice 2 merged: #9398 (2e9d47e5d)

    Bound webs 317 → 204, locals 383 → 270, methods with bound webs 201 → 143. The 113 retired webs are reference-typed compiler-generated storage (closure display classes, reference iterator/async state machines, anonymous types inside generics, collection-expression types) whose stores already carried the exact type; the explicit-type name check was the only refusal. Render A/B: 34 changed, all invalid→invalid, declaration form only; render hash: 39 changed, all Partial → Partial. One review round, clean.

    Found on the way: #9395 — a struct this spilled to a slot materializes as a value copy and loses writes, in 26 Full-labelled corpus methods today (for example FileLinePositionSpan::.ctor drops this.Path). It is the most valuable correctness item adjacent to this program and blocks the next 47 OutsideCoercionDomain webs, which must not be admitted as value storage until this keeps reference identity.

    Remaining docket candidates, by yield: MissingLoad (57 + 5), #9395 then the struct/generic-definition this webs (47), ConflictingTypeTestimony (25 webs / 61 locals), #9249 IncompleteCopyComponent (21 + combinations), SectionPipeline1.Add` shape acquisition (15), the enum comparison-sibling follow-on (1).


    Generated by Claude Code

  6. richlander commented on Oct 6, 2026

    @richlander
    OwnerAuthor

    Program B slice merged in #9427: array literals filled through dup copy chains now raise. It closes #9249.

    • Change: ArrayLiteralFromStoresPass treats the importer's per-dup slot copies as one place. A conditional element spilled to its own slot joins the run, for local and slot arrays alike. Literal elements keep the element-store coercion.
    • Design owner: docs/design/array-literal-fill-raise.md.
    • Corpus:
      • Array literals across the 14-assembly corpus: 2,072 → 4,072, in 1,062 methods.
      • 29 methods move Partial→Full.
      • No method became invalid (Render A/B over 13 assemblies, plus a per-method validity diff on Microsoft.CodeAnalysis.CSharp).
      • Newtonsoft compile-back: exact 2,451 → 2,457.
      • Residual webs: 204 → 205 (HelpOption::.ctor, whose literal feeds an Unsupported .ctor marker).

    This redirected #9249: the fix belongs to the raise owner, not to storage admission.

    Follow-ups filed during review: #9436 (keyword parameter loses its @ at address-taking uses) and #9437 (boxed narrow constants print as int). The unresolved-enum conditional reproduction was added to #3106.

    Remaining Program B candidates from the survey:

    • Source-closed copy components (Rule 2): webs 205 → about 194.
    • Still-blocked allocations: 584, interrupted by nested allocations, ?./?? elements or shared carriers.
    • Never-assigned-piece reads: Split ?? joins whose renders read an unassigned piece while labelled Full.

    Generated by Claude Code

  7. richlander commented on Oct 6, 2026

    @richlander
    OwnerAuthor

    Checkpoint 2026-10-06: source-closed copy components merged (#9454, 79a0e4fb2).

    • Rule change: a direct slot-copy component no longer materializes all-or-nothing. Its decided members materialize when two conditions hold:
      • no copy chain reaches them from an undecided member;
      • no undecided peer's store would coerce their local load (CoercionSinks.RequiresSlotStoreCoercion).
    • Managed-reference components: they stay atomic.
    • Invariant: the Release SlotMaterializationInvariant re-derives the boundary.
    • Residual binding census:
      • webs 205 → 194
      • locals 271 → 260
      • pass bugs 4 → 4
    • Render A/B: 0 valid → invalid.
    • Interaction with Decide integer box operands as exact coercion sinks before printing #9462: none (render hashes over 89,065 methods).

    Carried into the next slice:

    • Two stale doc passages: the SlotMaterializationPass <summary>, and the pointer-admission line in value-typed-emission.md that still excludes incomplete copy components.
    • Store-only peers (about 4 more webs) remain MissingLoad residuals.
    • The undecided-source shapes stay with the Boolean-identity and live-range owners: Boolean identity carriers and reused conflicting carriers.

    Generated by Claude Code

  8. richlander commented on Oct 7, 2026

    @richlander
    OwnerAuthor

    Published the MissingLoad slice as PR #9702. The pass retires producer-only slot webs before materialization, preserving effectful and contextual producers as typed discards. On the fixed 89,065-method corpus, residual binding moves 143 webs / 209 locals / 98 methods to 94 / 160 / 67; all 49 MissingLoad decisions disappear, with zero valid-to-invalid transitions in the available Render A/B population.

    #9702

  9. richlander commented on Oct 8, 2026

    @richlander
    OwnerAuthor

    Published the receiver-alias slice as #9770.

    The pipeline now retires exact aliases of the owner-issued receiver binder when
    storage would either copy value-type this or require an invalid open-generic
    reference local. Ordinary non-generic and spellable generic reference storage
    continues through normal materialization.

    Fixed 14-assembly / 89,065-method evidence:

    • residual-bound webs: 94 → 84;
    • residual-bound locals: 160 → 150;
    • methods with residual bindings: 67 → 57;
    • unified OutsideCoercionDomain: 14 → 4;
    • split webs: 51 → 51;
    • late-decidable webs: 0 → 0;
    • visible pass bugs: 4 → 4.

    The 13-assembly Render A/B comparison covers 46,945 methods: six structural
    changes, two valid→valid, four invalid→valid, and zero valid→invalid.

    #9770

  10. richlander commented on Oct 9, 2026

    @richlander
    OwnerAuthor

    PR #9799 retires the final two valid unified OutsideCoercionDomain storage
    webs by moving their residual-equivalent named-reference decisions into
    SlotMaterializationPass.

    Residual binding moves from 84 webs / 150 locals / 57 methods to
    82 / 148 / 55. The two remaining outside-domain webs belong to the already
    unsupported SectionPipeline<T>.Add method. Exact product output is unchanged.

    #9799

  11. richlander commented on Oct 9, 2026

    @richlander
    OwnerAuthor

    Slice 8 merged: #9799 (c0d5bc012)

    The final two valid unified OutsideCoercionDomain webs now materialize through
    the typed storage pass. Residual binding is 82 webs / 148 locals / 55 methods;
    the two remaining outside-domain webs belong to the already unsupported
    SectionPipeline<T>.Add method. The squash merge preserves the reviewed patch
    exactly (8f096362b3dcff200f4ad593a1d2c695b9ccfc8d).

    The next slice moves from unified storage admission to the 51 split residual
    webs: classify ConflictingTypeTestimony and select the smallest testimony rule
    that retires a coherent group without widening residual binding.

  12. richlander commented on Oct 9, 2026

    @richlander
    OwnerAuthor

    Slice 9 is published as #9813 at exact head
    e39cffd766ff9adb483ad023363d249b0bc88a2f.

    It splits the two importer-proven widening reference-coalesce ranges in Roslyn
    accessor-location methods. The fixed corpus moves from 82 to 80 residual webs,
    148 to 144 residual locals, 55 to 53 affected methods, and 51 to 49 split webs;
    exact pass impact adds only the two named witnesses.

    #9813

  13. richlander commented on Oct 10, 2026

    @richlander
    OwnerAuthor

    #9813 merged into main as
    a016381cd560d3eb8f6633b6261731ac2a8f1316.

    The reviewed candidate and squash merge have the identical stable patch ID:
    584ba8431980968f7fae22a8e5b3366bb8e72355.

    This slice retires the two importer-proven widening self-coalesce residual webs.
    The next independently mergeable slice is to classify the remaining 49 split
    webs and select the next coherent proof-backed group rather than broadening
    testimony reconciliation.

    #9813

  14. richlander commented on Oct 10, 2026

    @richlander
    OwnerAuthor

    Slice 10 is published as #9883 at exact head
    06e428763140a92d1c7122ecdb91a88a939529f5.

    It retires all 19 named-reference coalesce split webs whose operands have exact importer-issued widening evidence to one testified storage target. The fixed corpus moves from 80 to 61 residual webs, 144 to 104 residual locals, 53 to 35 affected methods, and 49 to 30 split webs; exact pass impact adds only the 18 affected methods.

    #9883

  15. richlander commented on Oct 10, 2026

    @richlander
    OwnerAuthor

    Slice 10 merged: #9883 (f3e613223)

    All 19 named-reference coalesce split webs now materialize through exact importer-issued widening proof. Residual binding moves from 80 webs / 144 locals / 53 methods to 61 / 104 / 35, and split webs move from 49 to 30. The reviewed candidate and squash merge have the identical stable patch ID: 7cc87e6e1bbc5e3d75ad521872014dd0b21302c3.

    The next independently mergeable slice is to classify the five remaining non-reference UnrenderableStoreType split webs and select the smallest proof-backed family without broadening residual testimony.

    #9883

  16. richlander commented on Oct 11, 2026

    @richlander
    OwnerAuthor

    The next printer-thinning slice is published as #9946 at exact head
    0cdb3dbefdb0bfe01c9630a620b4429b89af9fe3.

    It retires four non-reference UnrenderableStoreType split webs through two
    bounded owners: exact integer identity for complete Boolean/int arithmetic
    slots, and complete Boolean-I4-family accounting before Boolean
    materialization. The fixed corpus moves from 61 to 57 residual webs, 104 to 96
    residual locals, 35 to 31 affected methods, and 30 to 26 split webs. The
    remaining UnrenderableStoreType web is the deliberately excluded
    Boolean-to-ArgumentRefKind join.

    #9946

  17. richlander commented on Oct 12, 2026

    @richlander
    OwnerAuthor

    Slice 11 merged: #9946 (2d4a2f4c4)

    Four non-reference UnrenderableStoreType split webs now retain their bounded
    I4 identity in the typed pass pipeline. The fixed corpus moves from 61 to 57
    residual webs, 104 to 96 residual locals, 35 to 31 affected methods, and 30 to
    26 split webs. The reviewed candidate and squash merge have the identical
    stable patch ID: 137cfc86a9d332b1c27e5b73ae22ffdfa5864b0e.

    The checksum-verified Roslyn source comparison exposed the next independent
    printer-thinning slice: fully raise
    ForEachLoopOperation.ChildOperationsCount by eliminating the remaining
    single-use arithmetic and struct-receiver temporaries. That work is tracked by
    #9969. The separate Boolean-to-ArgumentRefKind enum split remains outside
    that issue.

    #9946

    #9969

  18. richlander commented on Oct 12, 2026

    @richlander
    OwnerAuthor

    Published #9979 for the next printer-thinning slice.

    The PR fully raises Roslyn's
    ForEachLoopOperation.ChildOperationsCount to one direct four-term expression,
    removing both the arithmetic stack carrier and value-type receiver temporary.
    The late pass changes exactly the intended 1/89,065 fixed-corpus methods, while
    the residual-storage census remains unchanged at
    57 webs / 96 locals / 31 methods / 26 split webs.

    Candidate head: ce09e35a06a49315c4e58f58cc32d5edd61377ce

    #9979

  19. richlander commented on Oct 13, 2026

    @richlander
    OwnerAuthor

    #9979 merged as 8ab566d908f804b1b00766001033438fc6670402.
    Its stable patch ID exactly matches reviewed head
    25244f0bbb2bb095fb1ad51c4c0e048be1bbe5de.

    Theme: C# printer thinning

    • Tracking issue: Project: decided storage — burn down the residual-bound stack-slot population #9371
    • Completed: 12/13 currently planned slices
    • Result: Roslyn's
      ForEachLoopOperation.ChildOperationsCount now raises to one direct
      four-term expression without synthetic arithmetic or struct-receiver
      temporaries.
    • Next slice: classify the remaining Boolean-to-ArgumentRefKind
      enum-identity split in TypeRef.FunctionPointerParameterText.
    • Separate spelling question: null-count ternary polarity is semantically
      erased by IL. Any default-spelling change must consult both facets of the
      dotnet/runtime style oracle rather than extending the semantic raise.
  20. richlander commented on Oct 13, 2026

    @richlander
    OwnerAuthor

    The final planned printer-thinning slice is published as #10032:
    #10032

    It preserves the already-decided ArgumentRefKind result through Boolean
    folding, retiring the fixed corpus's final UnrenderableStoreType split:

    • residual webs: 57 → 56;
    • residual locals: 96 → 94;
    • affected methods: 31 → 30;
    • split webs: 26 → 25.

    Exact boolean-folding impact changes only
    TypeRef.FunctionPointerParameterText.

  21. richlander commented on Oct 13, 2026

    @richlander
    OwnerAuthor

    #10032 merged:
    #10032

    This completes the 13th of 13 planned C# printer-thinning slices. The final
    slice preserves decided ArgumentRefKind identity through Boolean folding and
    retires the fixed corpus's final UnrenderableStoreType split:

    • residual webs: 57 → 56;
    • residual locals: 96 → 94;
    • affected methods: 31 → 30;
    • split webs: 26 → 25.

    The remaining residual groups are named, supported floors outside the completed
    plan rather than deferred work required by this slice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions