Skip to content

Add generic alias support - #16955

Draft
stakach wants to merge 6 commits into
crystal-lang:masterfrom
stakach:feat-generic-alias
Draft

stakach wants to merge 6 commits into
crystal-lang:masterfrom
stakach:feat-generic-alias

Conversation

@stakach

@stakach stakach commented May 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Resolves #2803. alias now accepts type parameters, so platform-, container-, and option-style aliases can be defined parametrically instead of being copy-pasted:

alias Maybe(T) = T | Nil
alias StringKeyed(V) = Hash(String, V)
alias Pair(K, V) = Tuple(K, V)

x : Maybe(Int32) = 1                # resolves to Int32 | Nil
y = StringKeyed(Int32).new          # resolves to Hash(String, Int32)

Generic aliases work everywhere a regular type can appear: type declarations, restrictions, generic instantiations, and — importantly — inside forall T restrictions.

What's preserved

The existing recursive-alias support is untouched. The classic JSON-style recursive alias still works exactly as before:

alias Json = Int32 | String | Hash(String, Json) | Array(Json)

A regression test for Alias = Int32 | Foo(Alias) is in the new spec block.

What's new

Use site Behaviour
Bar(Int32) as a metaclass expression resolves to the body with T = Int32
: Bar(Int32) as a restriction / type declaration matches the substituted type
def f(x : Bar(T)) forall T unifies T from the argument
Wrong arity (Pair(Int32) for a 2-param alias) clear "wrong number of type vars" error
Splat in alias type params (alias Foo(*T) = ...) rejected at parse time — no defined semantics yet

Implementation

  • AST (Alias): adds type_vars : Array(String)?.
  • Parser: parse_alias calls parse_type_vars after the name. Splat params raise.
  • AliasType: holds type_vars and a generic_instances cache. process_value early-returns for generic aliases (the body is unresolvable without arguments); instantiate(type_args) builds free_vars from the type parameters, runs the body through lookup_type, and caches the result per argument tuple.
  • Type lookup (type_lookup.cr#lookup(Generic)) and MainVisitor#visit(Generic): when a Generic node's name resolves to an AliasType with type_vars, validate arity and call instantiate. MainVisitor uses check_type_in_type_args so the resulting node carries the instance type inside restrictions and the metaclass outside.
  • Overload matcher (restrictions.cr): a new expand_generic_alias_restriction helper clones the alias body and substitutes each Path("T") with the matching argument AST node, then re-enters restrict. Wired into both Type#restrict(Generic) and GenericInstanceType#restrict(Generic). This is what makes forall T work — the matcher sees the expanded restriction and unifies as it always would.
  • recursive_struct_checker: skips generic aliases (they don't have a single resolved aliased_type).
  • to_s / formatter: emit Foo(T, U) when type params are present.

Test plan

  • Parser specs for alias Foo(T) = Bar(T), alias Maybe(T) = T | Nil, alias Pair(K, V) = Tuple(K, V)
  • Semantic specs in alias_spec.cr:
    • Metaclass expression Maybe(Int32) → (Int32 | Nil).class
    • Multi-parameter substitution StringKeyed(Int32) → TwoTypes(String, Int32)
    • Wrong arity error
    • Generic alias as a method-argument restriction
    • Generic alias inside forall T restriction (Boxed(T) = T, unbox(1) resolves T = Int32)
    • Generic alias body with multiple unified type vars (KV(K, V) = Pair(K, V))
    • Recursive non-generic alias preserved (regression test for Alias = Int32 | Foo(Alias))

Out of scope

  • Splat parameters in generic aliases (alias Foo(*T) = ...) — rejected at parse, semantics unclear

Comment thread src/compiler/crystal/semantic/main_visitor.cr Outdated
Co-authored-by: Sijawusz Pur Rahnama <sija@sija.pl>
@stakach
stakach requested a review from Sija May 16, 2026 02:32
Comment thread src/compiler/crystal/semantic/restrictions.cr Outdated
Comment thread src/compiler/crystal/semantic/restrictions.cr Outdated
Comment thread src/compiler/crystal/types.cr
stakach and others added 3 commits May 16, 2026 23:30
Co-authored-by: Sijawusz Pur Rahnama <sija@sija.pl>
Co-authored-by: Sijawusz Pur Rahnama <sija@sija.pl>
Co-authored-by: Sijawusz Pur Rahnama <sija@sija.pl>
@stakach
stakach requested a review from Sija May 16, 2026 13:33

@Sija Sija left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the points raised in #2803 (comment) and #2803 (comment) should be resolved and covered as well.

@straight-shoota

Copy link
Copy Markdown
Member

Hey @stakach, thanks for this contribution and many others in the past few days. Seems you're on a streak! 💪
Unfortunately, reviewing them might take a while.

Do you have a special motivation for picking up this specific feature? Also for the other PRs. Some of these discussions have been going and (unfortunately) stagnating for a while. I suppose this is certainly one of the most asked-for features, though.

The main obstacle why this feature (and other similar ones) is not yet available is usually not the missing implementation, but lack of a proper discussion and specification, exploring sytanx and semantics options as well as effects on the overall language. Any substantial change must be presented and documented in form of an RFC.

So currently, without an RFC this PR (and others) are not mergeable.
It's certainly still useful as a proof of concept and allows experimenting with the feature. It can help writing an RFC.

The point is, if this feature (and others) are to land in the language, contributions to the RFC process are perhaps more important than the actual implementations.

@stakach

stakach commented May 16, 2026

Copy link
Copy Markdown
Contributor Author

no special motivation beyond squashing some long-standing bugs.
every now and then I get stung by something like #16936 (second time in a few years that's got me) or just frustrated by #16957 (#8685) and it's getting a lot easier to solve them so figured why not.

@straight-shoota

Copy link
Copy Markdown
Member

There is an RFC pending on this feature: crystal-lang/rfcs#28

@straight-shoota
straight-shoota marked this pull request as draft June 2, 2026 12:43

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Request for generic alias

4 participants