From 18f250ec87173cd3e194d0a5b445fa462f4c4517 Mon Sep 17 00:00:00 2001
From: puffball1567 <17452514+puffball1567@users.noreply.github.com>
Date: Fri, 8 May 2026 03:17:05 +0900
Subject: [PATCH 1/4] fixes #4086, #9355; allow generic param defaults to
reference other type params
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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 #4086
closes #9355
---
compiler/semstmts.nim | 20 ++++
compiler/semtypes.nim | 67 +++++++++++++-
compiler/sigmatch.nim | 23 +++++
.../generics/tgeneric_param_default_deps.nim | 91 +++++++++++++++++++
4 files changed, 199 insertions(+), 2 deletions(-)
create mode 100644 tests/generics/tgeneric_param_default_deps.nim
diff --git a/compiler/semstmts.nim b/compiler/semstmts.nim
index 465276ffc20a2..6033e29c0be4a 100644
--- a/compiler/semstmts.nim
+++ b/compiler/semstmts.nim
@@ -853,6 +853,16 @@ proc semVarOrLet(c: PContext, n: PNode, symkind: TSymKind): PNode =
if a[^2].kind != nkEmpty:
typ = semTypeNode(c, a[^2], nil)
hasUserSpecifiedType = true
+ # Issue #4086: `var f: Foo` for `type Foo[T = int]` auto-expands to
+ # `Foo[int]` when every generic param has a default. Restricted to
+ # var/let/const declarations so it does not affect type-level
+ # computations like `arity(SomeGeneric)` in template/typetraits
+ # contexts where bare generic-body references are intentional.
+ if typ != nil and typ.kind == tyGenericBody and typ.sym != nil:
+ let auto = tryGenericBodyDefaultInvocation(c,
+ newSymNode(typ.sym, a[^2].info), typ.sym, nil)
+ if auto != nil:
+ typ = auto
var typFlags: TTypeAllowedFlags = {}
@@ -1009,6 +1019,16 @@ proc semConst(c: PContext, n: PNode): PNode =
if a[^2].kind != nkEmpty:
typ = semTypeNode(c, a[^2], nil)
hasUserSpecifiedType = true
+ # Issue #4086: `var f: Foo` for `type Foo[T = int]` auto-expands to
+ # `Foo[int]` when every generic param has a default. Restricted to
+ # var/let/const declarations so it does not affect type-level
+ # computations like `arity(SomeGeneric)` in template/typetraits
+ # contexts where bare generic-body references are intentional.
+ if typ != nil and typ.kind == tyGenericBody and typ.sym != nil:
+ let auto = tryGenericBodyDefaultInvocation(c,
+ newSymNode(typ.sym, a[^2].info), typ.sym, nil)
+ if auto != nil:
+ typ = auto
var typFlags: TTypeAllowedFlags = {}
diff --git a/compiler/semtypes.nim b/compiler/semtypes.nim
index 8009f7293c61e..d5970bb9f2454 100644
--- a/compiler/semtypes.nim
+++ b/compiler/semtypes.nim
@@ -1540,13 +1540,18 @@ proc semProcTypeNode(c: PContext, n, genericParams: PNode,
if isEmptyContainer(typ):
localError(c.config, a.info, "cannot infer the type of parameter '" & $a[0] & "'")
- if typ.kind == tyTypeDesc:
+ if typ.kind == tyTypeDesc or typ.kind == tyGenericParam:
# consider a proc such as:
# proc takesType(T = int)
# a naive analysis may conclude that the proc type is type[int]
# which will prevent other types from matching - clearly a very
# 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
+ # references another generic param. The default is a generic-param
+ # symbol whose typ is tyGenericParam (not tyTypeDesc); we treat
+ # such a parameter as an unbound typedesc param so it behaves like
+ # the explicit `proc foo[T](U: type = T)` form.
typ = newTypeS(tyTypeDesc, c, newTypeS(tyNone, c))
typ.incl tfCheckedForDestructor
@@ -1736,6 +1741,33 @@ proc containsGenericInvocationWithForward(n: PNode): bool =
return true
return false
+proc semGeneric(c: PContext, n: PNode, s: PSym, prev: PType): PType
+
+proc tryGenericBodyDefaultInvocation*(c: PContext, n: PNode, s: PSym,
+ prev: PType): PType =
+ ## Issue #4086 sub-case: `type Foo[T = int]; var f: Foo` is sugar for
+ ## `var f: Foo[int]` when every generic param has a default. Synthesize
+ ## a `Foo[default1, default2, ...]` bracket node and dispatch to the
+ ## existing `semGeneric` so the default-substitution machinery (added
+ ## for issues #4086 / #9355) handles cascading and parameter-referencing
+ ## defaults uniformly.
+ result = nil
+ if s.typ == nil: return
+ let body = s.typ.skipTypes({tyAlias})
+ if body.kind != tyGenericBody or body.len < 2: return
+ for i in 0..
Date: Sun, 10 May 2026 03:08:05 +0900
Subject: [PATCH 2/4] tests/generics: add review-requested tests for #4086,
#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 18f250ec8
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.
---
.../generics/tgeneric_param_default_deps.nim | 32 +++++++++++++++++++
1 file changed, 32 insertions(+)
diff --git a/tests/generics/tgeneric_param_default_deps.nim b/tests/generics/tgeneric_param_default_deps.nim
index 6af3ee8035f35..596e7c53f4234 100644
--- a/tests/generics/tgeneric_param_default_deps.nim
+++ b/tests/generics/tgeneric_param_default_deps.nim
@@ -2,6 +2,8 @@ discard """
matrix: "; --mm:refc"
"""
+import std/deques
+
# Generic type parameter defaults that reference other type parameters
# (issues #4086, #9355).
#
@@ -89,3 +91,33 @@ block: # distinct of a defaulted instantiation
var f: DistFoo
Foo[int](f).data.add 7
doAssert Foo[int](f).data == @[7]
+
+block: # union constraint + default referencing T (review request)
+ type Foo[T; U: seq[T]|Deque[T] = seq[T]] = object
+ data: U
+ var f: Foo[int]
+ f.data.add 42
+ doAssert f.data is seq[int]
+ doAssert f.data == @[42]
+
+block: # typeclass constraint + concrete-type default (review request)
+ type Foo[T: SomeInteger = int] = object
+ x: T
+ var f: Foo
+ f.x = 7
+ doAssert f.x is int
+ var g: Foo[int64]
+ g.x = 9'i64
+ doAssert g.x is int64
+
+block: # concept constraint + default (review request)
+ type HasLen = concept x
+ x.len is int
+ type Foo[T: HasLen = string] = object
+ val: T
+ var f: Foo
+ f.val = "hi"
+ doAssert f.val.len == 2
+ var g: Foo[seq[int]]
+ g.val = @[1, 2, 3]
+ doAssert g.val.len == 3
From 649f346bc7dc1c559cdc087dfdede851bfbe4ee1 Mon Sep 17 00:00:00 2001
From: puffball1567 <17452514+puffball1567@users.noreply.github.com>
Date: Sun, 31 May 2026 21:37:32 +0900
Subject: [PATCH 3/4] respond to review: drop proc-arg type-default pattern,
brackets-only
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 #4086 / #9355) is preserved.
---
compiler/semtypes.nim | 9 ++-------
tests/generics/tgeneric_param_default_deps.nim | 13 ++++++-------
2 files changed, 8 insertions(+), 14 deletions(-)
diff --git a/compiler/semtypes.nim b/compiler/semtypes.nim
index d5970bb9f2454..534ece3ddb384 100644
--- a/compiler/semtypes.nim
+++ b/compiler/semtypes.nim
@@ -1540,18 +1540,13 @@ proc semProcTypeNode(c: PContext, n, genericParams: PNode,
if isEmptyContainer(typ):
localError(c.config, a.info, "cannot infer the type of parameter '" & $a[0] & "'")
- if typ.kind == tyTypeDesc or typ.kind == tyGenericParam:
+ if typ.kind == tyTypeDesc:
# consider a proc such as:
# proc takesType(T = int)
# a naive analysis may conclude that the proc type is type[int]
# which will prevent other types from matching - clearly a very
# surprising behavior. We must instead fix the expected type of
- # the proc to be the unbound typedesc type.
- # Issue #9355: also handles `proc foo[T](U = T)` where the default
- # references another generic param. The default is a generic-param
- # symbol whose typ is tyGenericParam (not tyTypeDesc); we treat
- # such a parameter as an unbound typedesc param so it behaves like
- # the explicit `proc foo[T](U: type = T)` form.
+ # the proc to be the unbound typedesc type:
typ = newTypeS(tyTypeDesc, c, newTypeS(tyNone, c))
typ.incl tfCheckedForDestructor
diff --git a/tests/generics/tgeneric_param_default_deps.nim b/tests/generics/tgeneric_param_default_deps.nim
index 596e7c53f4234..c86b0e51d27b6 100644
--- a/tests/generics/tgeneric_param_default_deps.nim
+++ b/tests/generics/tgeneric_param_default_deps.nim
@@ -16,16 +16,15 @@ import std/deques
# Nim already supports T-independent defaults like `[T; U = int]`; this
# 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
- # `U` defaults to whatever T resolves to.
+block: # #9355: proc-side generic param default referencing T (brackets form)
+ func foo[T, U = T](): U = discard
doAssert foo[int]() is int
doAssert foo[string]() is string
-block: # #9355 sample 2: proc default with `type =` referencing T
- func foo[T](U: type = T): U = discard
- doAssert foo[int]() is int
- doAssert foo[float]() is float
+block: # #9355 variant: proc-side default with compound type expression
+ func bar[T, U = seq[T]](): U = discard
+ doAssert bar[int]() is seq[int]
+ doAssert bar[float]() is seq[float]
block: # #4086 type-side: object generic param default `seq[T]`
type Foo[T; U = seq[T]] = object
From 1f0c6cd2ed44f8f7b7cd59485e9173f98ac9871a Mon Sep 17 00:00:00 2001
From: puffball1567 <17452514+puffball1567@users.noreply.github.com>
Date: Mon, 1 Jun 2026 01:18:32 +0900
Subject: [PATCH 4/4] narrow PR scope to type-side default substitution (#4086)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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 #4086 (type-side `[T; U = seq[T]]` defaults referencing other
type params) and leaves #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.
---
.../generics/tgeneric_param_default_deps.nim | 26 +++++++------------
1 file changed, 10 insertions(+), 16 deletions(-)
diff --git a/tests/generics/tgeneric_param_default_deps.nim b/tests/generics/tgeneric_param_default_deps.nim
index c86b0e51d27b6..81948a3f4c527 100644
--- a/tests/generics/tgeneric_param_default_deps.nim
+++ b/tests/generics/tgeneric_param_default_deps.nim
@@ -4,27 +4,21 @@ discard """
import std/deques
-# Generic type parameter defaults that reference other type parameters
-# (issues #4086, #9355).
+# Type-side generic param defaults that reference other type parameters
+# (issue #4086).
#
# Currently fails on Nim 2.3.1 devel:
-# #9355 sample 1: `Error: type expected`
-# #9355 sample 2: `Error: cannot instantiate: 'U:type'`
-# type-side : `Error: invalid type: 'Foo[system.int, seq[T]]' for var`
+# `Error: invalid type: 'Foo[system.int, seq[T]]' for var`
#
# Other languages (C++, TypeScript, Rust, Scala) all support this pattern.
# Nim already supports T-independent defaults like `[T; U = int]`; this
-# extends support to defaults that reference earlier type parameters.
-
-block: # #9355: proc-side generic param default referencing T (brackets form)
- func foo[T, U = T](): U = discard
- doAssert foo[int]() is int
- doAssert foo[string]() is string
-
-block: # #9355 variant: proc-side default with compound type expression
- func bar[T, U = seq[T]](): U = discard
- doAssert bar[int]() is seq[int]
- doAssert bar[float]() is seq[float]
+# extends support to type-side defaults that reference earlier type params
+# (e.g. `type Foo[T; U = seq[T]]`). This is needed for binding C++ templates
+# like `std::vector>` and `std::unique_ptr>`.
+#
+# Note: proc-side brackets-internal defaults (`proc foo[T, U = T]()`) are
+# out of scope; the PR's logic targets type-side default substitution.
+# A separate change would be needed for proc-side signature defaults.
block: # #4086 type-side: object generic param default `seq[T]`
type Foo[T; U = seq[T]] = object