Skip to content

Make type substitution carry what is known about a type - #1229

Merged
Frotty merged 3 commits into
masterfrom
feat/type-subst
Aug 15, 2026
Merged

Make type substitution carry what is known about a type#1229
Frotty merged 3 commits into
masterfrom
feat/type-subst

Conversation

@Frotty

@Frotty Frotty commented Aug 15, 2026

Copy link
Copy Markdown
Member

Substitution of type variables was expressed as two parallel lists — the type variables of a
function or class next to the type arguments given at a use of it — paired up positionally at each
point of use:

int index = typeVars.indexOf(t.getTypeVariable());
...
return generics.get(index).getType();   // unwraps, drops everything else

An ImTypeArgument is a type plus what is known about it, but pairing the lists unwrapped the
argument and returned the bare ImType. The type class binding lives on the argument, so it could
not survive substitution. Callers that needed it re-attached it by hand; the ones with nowhere to
re-attach it from silently produced an argument with no instance.

This replaces that with one primitive, TypeSubst. Substituting into an argument position now
yields the argument that was bound, so the instance travels with the type it belongs to. Lookup is
by identity, which is what positional lookup already did — IM nodes do not override equals.

What it fixes

A type class bound is now usable from inside a closure:

interface Producer
    function produce() returns int

function indexLater<T: Indexable>(T x) returns Producer
    return () -> T.toIndex(x)

Two things were wrong. Substitution dropped the binding on the way into the closure class, and
TypeRewriter never rewrote the one type variable reference that is held directly rather than as a
type — the one on ImTypeVarDispatch. A closure lifting its body into a class of its own therefore
left the dispatch naming a variable of the function it had left, with nothing able to bind it.

Jass only for now. Lua keeps generics erased and specialises what it can reach through a concrete
type; a closure is reached through its interface, so the specialised class gets built but nothing
calls it. That is a change to Lua's erasure rather than to substitution, and is left alone here —
dispatchInsideClosureIsRejectedForLua pins that it is reported clearly rather than mistranslated,
and becomes the success case if that is ever fixed.

Also

  • Drops ProgramState.substituteTypeVars, a second implementation of substitution that disagreed
    with the first about whether bindings survive, and was only ever reached from itself. It also
    silently did not recurse into array and tuple types.
  • substituteType no longer rebuilds a type when there is nothing to replace. It runs for the
    return type of every call, and was allocating an equal copy of every class and tuple type.

Verified

  • Full suite: 1646 tests, 0 failures (1634 before, plus 10 unit tests for the primitive and 2
    for the closure case).
  • Emitted Lua byte-identical to 0d2bb0fbd across the tests that produce it (28 files, no
    differences), and byte-identical across two runs of this branch — AGENTS.md §8.
  • TypeSubstTests covers the primitive directly: binding survives in argument position, through
    nesting, and applied to an argument; a binding already present wins; variables sharing a name stay
    distinct; a missing argument is tolerated until it is named.

Worth knowing, unrelated to this change: emitted Jass is not stable across test runs. Comparing
two runs of 0d2bb0fbd against each other gives 91 differing .j files, the same magnitude as this
branch against itself. The differences are temp names (temp151 vs temp8) from a counter that is
not reset between compilations in one JVM, so the output depends on how many tests ran before. It is
a test-harness artefact rather than a defect in compiling a single map, but it does mean .j output
cannot be diffed across runs to check a change.

Not in scope

The named follow-ups from #1226 stand: Lua constructor and closure dispatch, module bounds, and the
stdlib conversion of HashMap/HashList/HashSet/Table to use Indexable. One name-tolerant
type-variable lookup remains in EliminateGenerics.inheritTypeClassBinding, covering the in-place
rewrite of call-site type arguments; it can go once a type parameter is guaranteed one ImTypeVar.

Frotty added 3 commits August 14, 2026 22:59
Substitution was two parallel lists -- a function's or class's type
variables next to the arguments given at a use -- paired up positionally
at each point of use. Pairing them unwrapped the ImTypeArgument and
returned the bare ImType, so everything else the argument carried was
dropped. A type class binding lives on the argument, which meant it could
not survive being substituted: callers that needed it re-attached it by
hand, and the ones with nowhere to re-attach it from silently produced an
argument with no instance.

Introduce TypeSubst, one primitive for the operation. Substituting into an
argument position now yields the argument that was bound, so the instance
travels with the type it belongs to. Lookup is by identity, matching what
positional lookup already did, since IM nodes do not override equals.

TypeRewriter missed one type variable reference: the one held directly by
ImTypeVarDispatch rather than as a type. A closure lifting its body into a
class of its own therefore left the dispatch naming a variable of the
function it had left, with nothing able to bind it. Rewriting it with the
rest is what makes a bound usable from inside a closure.

Also drops ProgramState.substituteTypeVars, a second implementation of
substitution that disagreed with the first about whether bindings survive,
and was only ever reached from itself.
@Frotty

Frotty commented Aug 15, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: 5041d422f2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Frotty
Frotty merged commit 56e438d into master Aug 15, 2026
6 checks passed
@Frotty
Frotty deleted the feat/type-subst branch August 15, 2026 01:19
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