Repository navigation
UDT Erasure: include retained library functions when simplifying custom types - #3900
Draft
Ian Davis (idavis) wants to merge 1 commit into
Draft
Ian Davis (idavis) wants to merge 1 commit into
Ian Davis (idavis) wants to merge 1 commit into
Conversation
Ian Davis (idavis)
added this pull request to stack #3892
October 6, 2026 23:21
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The branch fixes bugs where explicitly retained library callables could:
The core fix is to treat pinned callables as additional roots for UDT erasure, validate those roots before traversal, and transform only the entry- and pin-reachable package closure.
This branch fixes bugs where callables explicitly retained for code generation were preserved by dead-code elimination but skipped by UDT erasure.
The affected callables are “pinned” roots: callable items that are not reachable from the package entry point but must remain available because a caller intends to compile or invoke them directly.
1. Pinned callables retaining unerased UDT operations
Previously, UDT erasure followed only entry-point reachability. Item dead-code elimination, however, followed both the entry point and explicitly pinned callables.
As a result, a pinned callable could survive DCE while its body still contained:
Ty::Udttypes;Later compiler stages expect those forms to have been erased. Retaining them could cause invariant failures, validation errors, panics, or incorrect code generation.
The branch adds pinned callables as roots for UDT erasure.
2. Pipeline reachability disagreement
The underlying bug was a disagreement between transformation passes:
This produced a mixed FIR store where entry-reachable code had the post-UDT representation but pinned code still used the pre-erasure representation.
The branch gives UDT erasure and item DCE the same entry-plus-pins reachability roots.
3. Foreign pinned library callables being retained in an invalid form
A pinned callable may live in a library package that the application entry point never calls.
Previously, such a callable could be retained in the foreign package without that package receiving UDT erasure. Its body could then reach execution-graph rebuilding or code generation with unsupported UDT forms.
The branch expands the transformed package closure to include packages reached from pinned library callables.
4. Transitive UDT dependencies of pinned callables being skipped
A pinned callable can call another library function that constructs or returns a UDT:
Even if
Pinneditself is retained, transforming only its immediate package is insufficient. The callable’s transitive dependencies may live in other packages and may also contain UDT types, constructors, struct expressions, or field accesses.The branch computes reachability from the entry point and all pinned roots, then erases UDTs throughout the resulting package closure.
5. Struct-valued pinned callables failing after preservation
A pinned callable may construct a struct and read one of its fields:
Previously, preserving
Pinnedcould leave both the struct construction and field read unerased.The branch ensures that pinned-only bodies receive the same struct-to-scalar-or-tuple lowering as entry-reachable bodies, preserving their runtime values.
6. First-class UDT constructors in pinned callables becoming invalid
Pinned callables can store UDT constructors as values or mix constructors with ordinary factories:
If the pinned body is skipped by UDT erasure, its constructor value can continue referring to a type item that item DCE later removes.
The branch applies runtime-constructor replacement to pinned bodies, converting surviving constructor values into ordinary identity callables before type items are deleted.
7. Constructors and ordinary factories becoming indistinguishable
When a callable array contains both a UDT constructor and a user-defined factory, erasure must preserve their distinct behavior:
Skipping or inconsistently applying erasure to pinned bodies could invalidate the constructor or cause selection between the two callables to produce the wrong value.
The branch verifies both candidates remain distinct after the pinned callable passes through the full pipeline.
8. Pinned callable values changing during compilation
Because pinned bodies were not transformed consistently with their retained dependencies, compiling a pinned callable could change its observable result even though the original FIR evaluated correctly.
The branch adds semantic tests that evaluate pinned callables both:
This covers ordinary structs, constructor values, callable factories, and cross-package UDT dependencies.
9. Pinned items validated too late
Previously, pinned-item validation happened near item DCE and execution-graph rebuilding.
Once pins became roots for UDT erasure, invalid pins needed to be rejected before reachability traversal. Otherwise, a missing package or item could cause traversal to panic while attempting to expand the seeded graph.
The branch moves pin validation ahead of UDT erasure.
10. Missing pinned packages causing traversal panics
A pin can contain a package ID that does not exist in the store.
Passing that ID directly into seeded reachability could panic while looking up the package.
The branch validates the package and item before seed expansion and returns a structured
MissingPinnedItempipeline diagnostic instead.11. Missing pinned items producing failures at the wrong stage
If an item was removed before being supplied as a pin, the pipeline previously detected the problem only at a later backend stage.
That allowed earlier transforms to mutate the store before reporting that the requested retained target did not exist.
The branch reports the missing pin before UDT erasure and before any pin-dependent traversal.
12. Non-callable pins reaching seeded erasure
Only callable items are valid pinned roots. A UDT type item or another non-callable item cannot serve as a directly compiled callable target.
The branch preserves explicit
PinnedItemNotCallablevalidation and performs it before seeded UDT erasure begins.13. Error ownership for invalid foreign pins
Diagnostics for invalid pins need to identify the package that owns the requested pin, including when the package itself is missing.
The branch keeps the pin’s package ID as the diagnostic owner, producing a useful structured error rather than an unrelated traversal failure.
14. Unrelated packages being transformed unnecessarily
A naive fix could run UDT erasure over every package in the store whenever pins are supplied.
That would:
The branch instead transforms only the package closure reachable from the entry point or the supplied pins. Tests verify that an unrelated package remains unchanged.
15. Seeded UDT erasure not being idempotent
Adding extra reachability roots must not cause repeated UDT erasure to generate additional constructor identities or rewrite already-erased pinned packages differently.
The branch verifies that running seed-expanded UDT erasure twice produces the same package representation after the first run.
16. Per-package ID allocation collisions in newly reached packages
Pinned roots can expand UDT erasure into packages that were not reachable from the entry point.
Any expressions, statements, patterns, blocks, or identity callables synthesized in those packages must use assigners initialized from the existing package contents.
The branch uses the pipeline’s per-package assigner registry for the expanded package closure, preventing generated IDs from colliding with existing FIR nodes.
17. Execution graphs being rebuilt from stale UDT bodies
Execution-graph rebuilding runs after structural transformations. If a pinned body was retained but skipped by UDT erasure, its rebuilt graph would describe unsupported pre-erasure expressions.
The branch erases UDTs in pin-reachable packages before DCE and execution-graph rebuilding, so the resulting graph corresponds to the transformed body.
18. Type items being removed while retained pinned bodies still reference them
Item DCE removes UDT type items after UDT erasure. This is safe only when every retained executable body has had its UDT constructor calls and constructor values replaced.
Previously, entry-reachable code satisfied that condition, but pinned-only code might not.
The branch ensures that all entry- or pin-reachable executable references are erased before type-item removal.