Skip to content

Record what a specialised node was copied from, in one place - #1248

Merged
Frotty merged 1 commit into
masterfrom
refactor/specialisation-origin
Aug 17, 2026
Merged

Record what a specialised node was copied from, in one place#1248
Frotty merged 1 commit into
masterfrom
refactor/specialisation-origin

Conversation

@Frotty

@Frotty Frotty commented Aug 17, 2026

Copy link
Copy Markdown
Member

Stage one of the root-level change: make instantiation a relation the compiler holds, rather than something it re-derives.

Behaviour is unchanged on purpose. This introduces the relation and moves existing users onto it. The passes which still derive structure from mangled names are the next change, and they need this to exist first.

The problem it addresses

Specialising a generic entity makes a new node rather than recording a relation, so a copy has no way to say what it stands for. Three passes recovered that by other means, and each was wrong in a way that reached a user:

Bug What was used instead of the relation
Specialised class allocated no fields (#1239) Reachability, which never saw the copies
Fields kept under a key nothing read (#1239) Independent renaming of copy and original
Value dispatched through the wrong instance, crashing the interpreter (#1244) Type variables matched by name
Slot named after a type argument (#1247) Name composed from a copy's mangled name

Two of those were fixed with hand-rolled side tables on the translator — one for fields, one for type variables. They are the same relation written twice.

What this does

One relation: a copy, what it was copied from, and the type arguments it was made for.

public record Specialisation(Element original, List<ImTypeArgument> typeArguments) {}

void recordSpecialisation(Element copy, Element original, List<ImTypeArgument> typeArguments)
@Nullable Specialisation specialisationOf(Element copy)
<T extends Element> T canonical(T copy)   // follows a chain of copies to its root

Both side tables are gone. Their five callers — in EliminateGenerics, ImOptimizer, RemoveGarbage and LuaTranslator — read the one relation. Classes, functions and method implementations record their origin as well now, with the type arguments; nothing reads those yet, which is deliberate.

Keeping the type arguments is the part that matters for what comes next: a name can be composed from (original, arguments) instead of parsed back out of a string.

Tests

SpecialisationOriginTest covers the relation directly, since three passes depend on it: a copy leads back to its original, a copy of a copy to the root, two nodes sharing a name are not the same node, the arguments are kept, and a cycle is reported rather than followed forever — a cycle cannot arise from specialising, so one means a mistake elsewhere.

Narrow suites green: TypeClassTests, FastHashMapTests, GenericsTests, LuaTranslationTests, GenericsWithTypeclassesTests, SpecialisationOriginTest.

What comes next, and what I have parked

Stage two is the payoff: naming and pruning consume the relation, retiring semanticNameFromMethodName, the two independent slot composers, and ProgramState.identifyGenericStaticGlobals's longest-underscore-prefix search for a global's owning class.

Module bounds is parked on feat/module-instanciation-type-params, unpushed. The grammar change works as far as resolution and dispatch, but it regresses two existing tests: generic modules already resolve their type parameters by matching the receiver type, and declaring the parameters on the instantiation collides with that. Reconciling the two is a design decision rather than a patch, and the branch carries the error chain that got there.

Specialising a generic entity makes a new node rather than recording a
relation, so a copy cannot say what it stands for. Three passes recovered that
by other means and each was wrong in a way which reached a user: fields dropped
as dead because an access still named the original's variable, a value
dispatched through the wrong instance because type variables were matched by
name, and a slot named after a type argument because the name was composed from
a copy's mangled name.

Two of those were fixed by hand-rolled side tables on the translator, one for
fields and one for type variables. This is the same relation once: a copy, what
it was copied from, and the type arguments it was made for. The two tables are
gone and their five callers - in EliminateGenerics, ImOptimizer, RemoveGarbage
and LuaTranslator - read the one relation instead.

Behaviour is unchanged on purpose. Classes, functions and method
implementations now record their origin too, and nothing reads those yet: the
naming and pruning passes which still derive structure from mangled names are
the next step, and they need the relation to exist first.

SpecialisationOriginTest covers the relation directly, since three passes now
depend on it: a copy leads back to its original, a copy of a copy to the root,
two nodes sharing a name are not the same node, the type arguments are kept,
and a cycle is reported rather than followed forever.
@Frotty
Frotty merged commit 4722b56 into master Aug 17, 2026
4 checks passed
@Frotty
Frotty deleted the refactor/specialisation-origin branch August 17, 2026 11:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant