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