Skip to content

fixes #4086; allow generic type parameter defaults to reference earlier type params#25799

Open
puffball1567 wants to merge 9 commits into
nim-lang:develfrom
puffball1567:fix-generic-type-param-default-deps
Open

fixes #4086; allow generic type parameter defaults to reference earlier type params#25799
puffball1567 wants to merge 9 commits into
nim-lang:develfrom
puffball1567:fix-generic-type-param-default-deps

Conversation

@puffball1567

@puffball1567 puffball1567 commented May 7, 2026

Copy link
Copy Markdown
Contributor

Generic type parameter defaults that reference other type parameters currently fail in Nim, even though C++ / TypeScript / Rust / Scala all support the pattern. This makes them work:

  • type Foo[T; U = seq[T]] (also U = T, U = ref T, U = array[3, T], cascades like [T; U = seq[T]; V = seq[U]], alias/distinct of the instantiation, and union / typeclass / concept constraints).
  • When every parameter has a default, Foo[] (empty brackets) instantiates the type using all of them, e.g. type Foo[T = int]; var f: Foo[] is Foo[int]. It works in every type position (var/let/const, params, return types, fields) and mirrors C++ Foo<>.

Implementation

A parameter whose value is a default that mentions an earlier parameter is resolved during instantiation, against the already-bound parameters — there is no prepass, and proc matches is left to do plain overload resolution:

  • compiler/semtypes.nim (semGeneric): a default-filled parameter (nfDefaultParam) that still mentions earlier params no longer marks the invocation non-concrete, so it reaches instantiation where the value is resolved left-to-right.
  • compiler/semtypinst.nim (handleGenericInvocation): the first pass uses a non-erroring type-var lookup, so a bare-parameter default (U = T) is left for the main left-to-right binding pass rather than failing early.

Tests

tests/generics/tgeneric_param_default_deps.nim covers the variants above on both --mm:orc and --mm:refc.

closes #4086

@puffball1567 puffball1567 marked this pull request as draft May 8, 2026 01:57
…eference other type params

Generic type parameter defaults that reference other type parameters
currently fail in Nim, even though C++ / TypeScript / Rust / Scala all
support the pattern. This change enables four related sub-cases:

1. type-side reference: `type Foo[T; U = seq[T]]`
   (also `U = ref T`, `U = array[3, T]`, alias, distinct, cascade)
2. type-side direct: `type Foo[T; U = T]`
3. type-side 0-arg invocation: `var f: Foo` for `type Foo[T = int]`
4. proc-side: `func foo[T](U = T): U` and `func foo[T](U: type = T): U`

Implementation
--------------
compiler/semtypes.nim
  - semGeneric: accumulate already-resolved generic params into a
    layered type map and substitute them in subsequent default values
    before they are added to the invocation. Handles cascade defaults
    like `[T; U = seq[T]; V = seq[U]]` left-to-right. Gated on
    `hasAnyDefault` so generic types with no defaults pay no overhead.
  - semProcTypeNode: treat a bare generic-param default (`U = T`) as
    an unbound typedesc parameter, equivalent to `U: type = T`, so the
    existing typedesc-param machinery handles it.
  - tryGenericBodyDefaultInvocation: helper that synthesizes a
    `Foo[default1, default2, ...]` invocation when every generic param
    has a default, and dispatches to semGeneric.

compiler/semstmts.nim
  - semVarOrLet / semConst: after type resolution, if the resulting
    type is a tyGenericBody whose every param has a default, expand
    it via tryGenericBodyDefaultInvocation. Restricted to var/let/
    const so type-level computations like `arity(SomeGeneric)` in
    template/typetraits contexts keep treating bare generic-body
    references as intended.

compiler/sigmatch.nim
  - matches default-completion: when the default value is a bare
    reference to an earlier generic type param (recognised by the
    default's static type being `tyGenericParam`), substitute T
    against the explicit-instantiation bindings via prepareTypesInBody
    and wrap the result as `tyTypeDesc` so the implicit
    `tfImplicitTypeParam` binding path treats it like a literal type
    default. Defaults whose type is anything else — value expressions
    like `arr.high`, proc-call expressions like `newTensor[T](0)` whose
    result type is `tyGenericInvocation`/`tyGenericInst`, etc. — are
    left untouched, so the existing default-handling machinery (and
    later sem stages) continue to handle them as before. Verified by
    locally building tests/test_indep_import.nim from arraymancer
    against ~/.nimble pkgs2.

Tests
-----
tests/generics/tgeneric_param_default_deps.nim covers all four
sub-cases and the cascade / alias / distinct variants. Local
regression on tests/{generic,generics,objects,typerel,types,
metatype,statictypes,concepts,proc,overload,template,macros,tuples,
collections,distinct,varstmt,let,varres,assign,init}: 663 reSuccess
across 19 categories. The single non-success entry,
tests/types/tforwardcycletimeout reTimeout, reproduces on clean
upstream/devel and is unrelated.

closes nim-lang#4086
closes nim-lang#9355
@puffball1567 puffball1567 force-pushed the fix-generic-type-param-default-deps branch from dade56f to 18f250e Compare May 8, 2026 07:21
@puffball1567 puffball1567 marked this pull request as ready for review May 8, 2026 19:28
@puffball1567

Copy link
Copy Markdown
Contributor Author

The Azure Pipelines nim-lang.Nim (packages OSX_arm64_cpp) job hit the 90-minute agent timeout and was cancelled mid-koch, Run CI — not a code-side failure. All other Azure jobs (Linux / Windows / OSX without cpp / etc.) and every GitHub Actions run are green.

Could a maintainer kick off a re-run of just that one job?

cc @Araq @ringabout for review when convenient — generic-param-default fix (4 files, +199/-2). Closes #4086 and #9355.

@mratsim

mratsim commented May 9, 2026

Copy link
Copy Markdown
Collaborator

For 1, 2, 3, we would also need to test this kind of syntax:

type Foo[T; U: seq[T]|Deque[T] = seq[T]]

type Foo[T: SomeInteger = int]

And probably interation with concepts as well.

Note that the fact that higher kinded types actually work is an accident and the original RFC was auto-closed as not planned: nim-lang/RFCs#5

…g#9355

Adds three review-requested tests to tgeneric_param_default_deps.nim
covering generic param defaults in combination with:

  1. union constraint:    type Foo[T; U: seq[T]|Deque[T] = seq[T]]
  2. typeclass constraint: type Foo[T: SomeInteger = int]
  3. concept constraint:   type Foo[T: HasLen = string]

These exercise the default-completion machinery added in 18f250e
across constraint kinds adjacent to higher-kinded-type accidental
behavior (RFCs#5). All three pass under both --mm:orc (default) and
--mm:refc.

Local regression on tests/concepts: 45/46 reSuccess, 0 reFail
(1 reDisabled is unrelated). Local regression on tests/generics:
116/116 reSuccess, 0 reFail.
@puffball1567

Copy link
Copy Markdown
Contributor Author

@mratsim Thanks. Pushed 305c731b8 with three test blocks for the syntaxes you flagged:

  1. union: type Foo[T; U: seq[T]|Deque[T] = seq[T]]
  2. typeclass: type Foo[T: SomeInteger = int]
  3. concept: type Foo[T: HasLen = string]

All three pass on --mm:orc (default) and --mm:refc. Local regression: tests/concepts 45/46 (0 reFail, 1 reDisabled unrelated), tests/generics 116/116 (0 reFail).

On HKT (RFCs#5): the patch is scoped to default-completion only. The guards (tyGenericParam in sigmatch; nfDefaultParam + containsGenericType in semGeneric; tyTypeDesc or tyGenericParam in semProcTypeNode; tyGenericBody + every-param-has-default in the semstmts/semGeneric body-invocation helper) keep tyGenericInvocation/tyGenericInst paths untouched, so accidental HKT behavior should be preserved. If you have a specific HKT pattern from constantine / arraymancer you'd like me to add as a regression check, happy to do so.

Comment thread compiler/semtypes.nim Outdated
Comment thread tests/generics/tgeneric_param_default_deps.nim Outdated
Per @Araq's review feedback:
- `proc foo[T](U = T)` and `proc foo[T](U: type = T)` mix proc-arg
  defaults with types, which is not valid Nim semantics
  ("types are not values"). The proper Nim way is brackets-internal
  generic params: `proc foo[T, U = T]()`.

Changes:
- compiler/semtypes.nim: revert the `tyGenericParam` OR condition
  and the associated comment in `semProcTypeNode`; the original
  `tyTypeDesc`-only logic is preserved.
- tests/generics/tgeneric_param_default_deps.nim: replace the
  proc-arg default samples with brackets-internal forms.

The PR core (type-side `[Tp; Alloc = StdAllocator[Tp]]` and
proc-side brackets-internal `[T, U = T]` generic param defaults
referencing other type params, closing nim-lang#4086 / nim-lang#9355) is preserved.
@puffball1567

Copy link
Copy Markdown
Contributor Author

@Araq Thank you for the review and the design feedback. You're right — mixing proc-arg defaults with type references is not valid Nim semantics. I've pushed 649f346bc dropping that path entirely and keeping only the brackets-internal generic param default form.

Changes in this update:

  • compiler/semtypes.nim: reverted the tyGenericParam OR condition in semProcTypeNode (= original tyTypeDesc-only logic restored, removed the Issue type argument's default value can't depend on a generic (+ weird error messages) #9355 comment that described the proc-arg path)
  • tests/generics/tgeneric_param_default_deps.nim: replaced the proc-arg samples (proc foo[T](U = T) / (U: type = T)) with brackets-internal forms (proc foo[T, U = T]())

The PR core — type-side [Tp; Alloc = StdAllocator[Tp]] and proc-side brackets-internal [T, U = T] generic param defaults that reference earlier type params (closing #4086 / #9355) — is preserved.

Thanks again for the careful review!

After @Araq's review feedback, the proc-arg type-default pattern was
dropped from the compiler logic. The remaining brackets-internal
proc-side form (`proc foo[T, U = T]()`) is also out of scope because
the PR's substitution machinery targets type bodies, not proc
signatures — separate work would be needed for the proc-side path.

This commit removes the proc-side test samples and updates the
header comment to reflect the type-side-only scope. The PR now
closes nim-lang#4086 (type-side `[T; U = seq[T]]` defaults referencing other
type params) and leaves nim-lang#9355 (proc-side) for a follow-up change.

Tested locally with `nim c --run` under both `--mm:orc` and
`--mm:refc`; all remaining blocks pass.
@puffball1567

Copy link
Copy Markdown
Contributor Author

Quick follow-up on 1f0c6cd2e: after dropping the proc-arg type-default path, the brackets-internal proc-side form (proc foo[T, U = T]()) also turned out to be out of scope — the substitution machinery in this PR targets type bodies (semGeneric for tyGenericBody), not proc signatures.

To match what the patch actually fixes, I removed the proc-side test samples and narrowed the PR scope to type-side default substitution (#4086) only:

  • type Foo[T; U = seq[T]] and similar (cascading defaults, U = T, U = ref T, U = array[3, T], etc.) — fully covered
  • proc-side brackets-internal defaults (proc foo[T, U = T]()) — left for a separate follow-up

Tested locally with both --mm:orc and --mm:refc; all remaining blocks compile and run.

I'll keep #9355 open and tackle the proc-signature path in a separate PR once this lands. Title/description can be updated to reflect the narrower scope when convenient — happy to do that myself if preferred.

@puffball1567 puffball1567 changed the title fixes #4086, #9355; allow generic param defaults to reference other type params fixes #4086; allow type-side generic param defaults to reference other type params May 31, 2026
@Araq

Araq commented Jun 13, 2026

Copy link
Copy Markdown
Member

If I read things correctly, this doesn't work for parameters.

var f: Foo for type Foo[T = int]

works but

proc p(f: Foo) for type Foo[T = int]

does not. And it cannot because Foo is also a type class. The inconsistency will cause numerous bug reports.

How about var f: Foo[]? It's an instantiation, clearly, using defaults. It makes sense, IMO, albeit being a bit uglier than your syntax.

Araq pointed out that auto-instantiating a bare `Foo` (for a fully defaulted `type Foo[T = int]`) is inconsistent: it works for `var f: Foo` but cannot work for `proc p(f: Foo)`, because there `Foo` is a type class (implicit generic). That asymmetry would cause bug reports.

Switch the explicit-defaults instantiation to `Foo[]` (empty brackets), mirroring C++ `Foo<>`. It is handled in the shared type-resolution path (`semTypeNode`'s nkBracketExpr case), so it works uniformly in every type position -- var/let/const, proc params, return types, fields. A bare `Foo` keeps its type-class meaning everywhere.

- semtypes: route empty-bracket `Foo[]` to the default-instantiation helper via new `semGenericOrEmptyBracket`; report a clean error instead of an IndexDefect crash when not every param has a default.
- the helper now provides only the first param's default explicitly and lets the matcher complete the rest, so dependent defaults among fully-defaulted params (e.g. `type Foo[T = int; U = seq[T]]; var f: Foo[]`) substitute correctly.
- semstmts: drop the bare-`Foo` var/let/const auto-expansion (restores the original semVarOrLet/semConst code).
- tests: use `Foo[]`; add cascade and param/return/field position coverage.
…ide scope

The typedesc-parameter form `func foo[T](U: type = T)` (issue nim-lang#9355) is resolved by the sigmatch default-substitution that was retained through the scope narrowing; add a regression test for it (default falls back to T, explicit override works) and update the header note to record which proc-side forms are supported vs. rejected by design (untyped `U = T`) / out of scope (`proc foo[T, U = T]()`).
@puffball1567 puffball1567 changed the title fixes #4086; allow type-side generic param defaults to reference other type params fixes #4086, #9355; allow generic type parameter defaults to reference earlier type params Jun 14, 2026
Comment thread compiler/semtypes.nim Outdated
# Skip the bookkeeping entirely when no param has a default — this
# generic body cannot need substitution and the work would just be
# pure overhead (matters for large generic graphs / forward cycles).
var hasAnyDefault = false

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

But why is this prepass even necessary... You instantiate the type, when the typevar wasn't bound, you see if it had a default and use that, else it's an error. That's the algorithm and Nim seems to never do things properly and instead special cases everything and thus sem.nim grows like a cancer.

Comment thread compiler/sigmatch.nim Outdated
defaultValue.typ.kind != tyTypeDesc:
let typedescTyp = newTypeS(tyTypeDesc, c, defaultValue.typ)
typedescTyp.incl tfCheckedForDestructor
defaultValue.typ = typedescTyp

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Look, this is proc matches, it is supposed to do overload resolution. It is not its job to compute typevar-bindings per se, it got reused in a creative manner for that purpose. Time to refactor things properly and to undo this creative solution that spiralled into a mess of bullshit.

@Araq

Araq commented Jun 14, 2026

Copy link
Copy Markdown
Member

Sorry, I know this is much to ask for but before we tackle more features on top of this silly never ending nightmare of what is ultimately a very simple thing we should refactor things.

EDIT: The simple thing is called "unification".

…ot a prepass/matches

Per review: a generic param whose value is a default referencing an earlier param (e.g. type Foo[T; U = seq[T]]) is now resolved during instantiation against the already-bound params, instead of via a prepass in semGeneric or typevar substitution added to proc matches. matches is back to plain overload resolution and the prepass is gone.

semtypinst.handleGenericInvocation: the first pass uses a non-erroring typevar lookup, so a bare-param default (U = T) is left for the main left-to-right binding pass instead of failing early. semtypes.semGeneric: a default-filled (nfDefaultParam) param that still mentions earlier params no longer marks the invocation non-concrete; instantiation resolves it.

Drop nim-lang#9355 (typedesc-param default) from this PR: it relied on the matches substitution removed here; a proper fix needs separate work in the proc-instantiation path.
@puffball1567 puffball1567 changed the title fixes #4086, #9355; allow generic type parameter defaults to reference earlier type params fixes #4086; allow generic type parameter defaults to reference earlier type params Jun 14, 2026
The previous commit made the first instantiation pass use a non-erroring type-var lookup for every type-var arg. That changed binding for *outer* type-vars and broke real packages in CI (arraymancer/datamancer and zero_functional failed with 'type mismatch').

Restrict it: outer type-vars keep the original lookupTypeVar path; only a default that references one of this body's own params (e.g. U = T) is treated specially, and only when it is not yet bound (a top-level invocation filled by an earlier arg) is it left for the main left-to-right pass. Own params that are already bound are still processed, so recursive / illegal-recursion handling is unchanged.
semGenericOrEmptyBracket emitted 'every generic parameter must have a default' even when the type is not generic at all (it has no params). Route a non-generic-body empty bracket (e.g. Plain[]) to semGeneric so it reports the proper 'no generic parameters allowed for X'; the 'must have a default' message now only applies to a generic body where some param lacks a default.
@puffball1567

Copy link
Copy Markdown
Contributor Author

@Araq — reworked. The semGeneric prepass is gone, and so is the typevar
substitution I'd added to proc matches. #4086 now resolves at instantiation:
when a param's value is a default that refers to an earlier param (U = seq[T]),
it's resolved against the already-bound params during instantiation rather than
pre-substituted.

I've also taken #9355 back out of this PR — the matches substitution is what it
was relying on, and doing it properly needs separate work in the
proc-instantiation path, so it's better as its own follow-up. This PR is now
just #4086.

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.

default type parameters

3 participants