Skip to content

fixes #4086; allow type-side generic param defaults to reference other type params#25799

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

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

Conversation

@puffball1567
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 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 of a typedesc
    param references generic params, substitute via prepareTypesInBody
    and wrap the resolved type as tyTypeDesc so the implicit
    tfImplicitTypeParam binding path treats it like a literal type
    default. Skipped when nfDefaultRefsParam is set so defaults that
    reference value parameters (e.g. idx = arr.high) keep using
    the existing param-substitution machinery.

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}: 0 new failures
across 663 tests (one pre-existing tforwardcycletimeout reTimeout
reproduces on clean upstream/devel and is unrelated).

closes #4086
closes #9355

@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
Copy link
Copy Markdown
Collaborator

mratsim commented May 9, 2026

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
# surprising behavior. We must instead fix the expected type of
# the proc to be the unbound typedesc type:
# the proc to be the unbound typedesc type.
# Issue #9355: also handles `proc foo[T](U = T)` where the default
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.

What does proc foo[T](U = T) even mean. You cannot give a parameter a default value which is a type, types are not values.

# extends support to defaults that reference earlier type parameters.

block: # #9355 sample 1: proc default referencing T (untyped form)
func foo[T](U = T): U = discard
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.

Makes no sense. T is a type, not a value. If you want another generic parameter U, declare it in the brackets.

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
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.

type argument's default value can't depend on a generic (+ weird error messages) default type parameters

3 participants