Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 85 additions & 2 deletions compiler/semtypes.nim
Original file line number Diff line number Diff line change
Expand Up @@ -1736,6 +1736,58 @@ 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: `Foo[]` (empty brackets) instantiates `Foo` using
## the default of every generic param, e.g. `type Foo[T = int]; var f: Foo[]`
## means `var f: Foo[int]`. 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. Returns nil when
## not every generic param has a default (the caller then reports an error).
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..<body.len-1:
let p = body[i]
if p.kind != tyGenericParam or p.sym == nil or p.sym.ast == nil:
return
var bracket = newNodeI(nkBracketExpr, n.info)
if n.kind == nkSym:
bracket.add n
else:
bracket.add newSymNode(s, n.info)
# Provide only the first param's default explicitly (it cannot reference an
# earlier param, so it needs no substitution). The matcher's default-completion
# path then fills the remaining params from their defaults and flags them
# `nfDefaultParam`, so `semGeneric` substitutes references to earlier params
# (e.g. `U = seq[T]` -> `seq[int]`, cascading left-to-right). Synthesizing every
# default as an explicit arg would instead treat them as user-supplied args and
# bypass that substitution.
bracket.add copyTree(body[0].sym.ast)
result = semGeneric(c, bracket, s, prev)

proc semGenericOrEmptyBracket(c: PContext, n: PNode, s: PSym, prev: PType): PType =
## `Foo[]` (empty brackets, i.e. an `nkBracketExpr` whose only child is the
## type head) is an explicit-defaults instantiation: it means `Foo[d1, d2, ...]`
## where every generic param falls back to its default (issues #4086 / #9355).
## Handling it here in the shared type-resolution path means it works in every
## type position — var/let/const, proc params, return types, fields — unlike a
## bare `Foo`, which stays a type class (implicit generic) in parameter position
## and must keep that meaning. Non-empty brackets take the normal path.
if n.len == 1:
result = tryGenericBodyDefaultInvocation(c, n[0], s, prev)
if result == nil:
localError(c.config, n.info,
"cannot instantiate '$1' with '[]': every generic parameter must have a default" %
s.name.s)
result = newOrPrevType(tyError, prev, c)
else:
result = semGeneric(c, n, s, prev)

proc semGeneric(c: PContext, n: PNode, s: PSym, prev: PType): PType =
if s.typ == nil:
localError(c.config, n.info, "cannot instantiate the '$1' $2" %
Expand Down Expand Up @@ -1791,8 +1843,35 @@ proc semGeneric(c: PContext, n: PNode, s: PSym, prev: PType): PType =
let rType = m.call[0].typ
let mIndex = if rType != nil: rType.len - 1 else: -1
var hasForwardTypeParam = false

# Issues #4086, #9355: when a generic param's default value references
# earlier type params (e.g. `type Foo[T; U = seq[T]]` or `func foo[T](U = T)`)
# substitute those references with already-resolved bindings before
# adding to the invocation. Bindings are accumulated as the loop walks
# left-to-right, so cascade defaults like `[T; U = seq[T]; V = seq[U]]`
# work too (each iteration resolves against the previous ones).
# 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.

for i in 0..<t.len-1:
let p = t[i]
if p.kind == tyGenericParam and p.sym != nil and p.sym.ast != nil:
hasAnyDefault = true
break
var defaultBindings = initLayeredTypeMap()
var hasDefaultBinding = false

for i in 1..<m.call.len:
var typ = m.call[i].typ

if hasDefaultBinding and nfDefaultParam in m.call[i].flags and
containsGenericType(typ):
var cl = initTypeVars(c, defaultBindings, n.info, getCurrOwner(c))
let substituted = replaceTypeVarsT(cl, typ)
if substituted != nil:
typ = substituted

# is this a 'typedesc' *parameter*? If so, use the typedesc type,
# unstripped.
if m.call[i].kind == nkSym and m.call[i].sym.kind == skParam and
Expand All @@ -1807,6 +1886,10 @@ proc semGeneric(c: PContext, n: PNode, s: PSym, prev: PType): PType =
skip = false
addToResult(typ, skip)

if hasAnyDefault and i - 1 < t.kidsLen and t[i - 1].kind == tyGenericParam:
defaultBindings.put(t[i - 1], typ.skipTypes({tyTypeDesc}))
hasDefaultBinding = true

if typ.kind == tyForward:
hasForwardTypeParam = true

Expand Down Expand Up @@ -2365,8 +2448,8 @@ proc semTypeNode(c: PContext, n: PNode, prev: PType): PType =
of "lent": result = semAnyRef(c, n, tyLent, prev)
of "sink": result = semAnyRef(c, n, tySink, prev)
of "owned": result = semAnyRef(c, n, tyOwned, prev)
else: result = semGeneric(c, n, s, prev)
else: result = semGeneric(c, n, s, prev)
else: result = semGenericOrEmptyBracket(c, n, s, prev)
else: result = semGenericOrEmptyBracket(c, n, s, prev)
of nkDotExpr:
let typeExpr = semExpr(c, n)
if typeExpr.typ.isNil:
Expand Down
23 changes: 23 additions & 0 deletions compiler/sigmatch.nim
Original file line number Diff line number Diff line change
Expand Up @@ -3115,6 +3115,29 @@ proc matches*(c: PContext, n, nOrig: PNode, m: var TCandidate) =
if nfDefaultRefsParam in formal.ast.flags:
m.call.flags.incl nfDefaultRefsParam
var defaultValue = copyTree(formal.ast)
# Issues #4086, #9355: when the default value is a bare reference
# to an earlier generic type param (e.g. `func foo[T](U: type = T)`
# or its untyped form `func foo[T](U = T)`) — recognised by the
# default's static type being `tyGenericParam` — substitute T
# against the explicit-instantiation bindings via
# `prepareTypesInBody` (which updates both the AST sym and its
# typ) and wrap the result as `tyTypeDesc` so the
# `tfImplicitTypeParam` binding path below treats it like a
# literal type default (`U: type = int`).
# Other defaults (value expressions like `arr.high`, proc-call
# expressions like `newTensor[T](0)` whose result type is a
# `tyGenericInvocation`/`tyGenericInst`, etc.) have a non-
# `tyGenericParam` type and are left untouched, so the existing
# default-handling machinery takes care of them.
if defaultValue.typ != nil and
defaultValue.typ.kind == tyGenericParam and
m.calleeSym != nil:
defaultValue = prepareTypesInBody(c, m.bindings, defaultValue, m.calleeSym)
if formal.typ.kind == tyTypeDesc and defaultValue.typ != nil and
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.

if defaultValue.kind == nkNilLit:
defaultValue = implicitConv(nkHiddenStdConv, formal.typ, defaultValue, m, c)
# proc foo(x: T = 0.0)
Expand Down
160 changes: 160 additions & 0 deletions tests/generics/tgeneric_param_default_deps.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
discard """
matrix: "; --mm:refc"
"""

import std/deques

# Type-side generic param defaults that reference other type parameters
# (issue #4086).
#
# Currently fails on Nim 2.3.1 devel:
# `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 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<T, allocator<T>>` and `std::unique_ptr<T, default_delete<T>>`.
#
# When every generic param has a default, `Foo[]` (empty brackets) instantiates
# `Foo` using all defaults, e.g. `type Foo[T = int]; var f: Foo[]` is `Foo[int]`.
# `Foo[]` is the explicit-instantiation spelling (mirrors C++ `Foo<>`); a bare
# `Foo` keeps meaning a type class in parameter position, so it is intentionally
# not auto-instantiated. `Foo[]` works uniformly in every type position.
#
# Proc-side: the typedesc-parameter form `func foo[T](U: type = T)` is also
# supported (issue #9355) -- the default `= T` is substituted during overload
# matching (sigmatch). The untyped form `func foo[T](U = T)` is rejected by
# design (a type is not a value), and brackets-internal proc generic defaults
# (`proc foo[T, U = T]()`) are out of scope for this PR.

block: # #4086 type-side: object generic param default `seq[T]`
type Foo[T; U = seq[T]] = object
data: U
var f: Foo[int]
f.data.add 42
doAssert f.data == @[42]

block: # nested reference: U default uses T, V default uses U
type Foo[T; U = seq[T]; V = seq[U]] = object
data: V
var f: Foo[int]
f.data.add @[1, 2]
doAssert f.data == @[@[1, 2]]

block: # type-side direct: U = T
type Foo[T; U = T] = object
a: T
b: U
var f: Foo[int]
f.a = 1
f.b = 2
doAssert f.b is int

block: # type-side compound: U = ref T
type Foo[T; U = ref T] = object
p: U
var f: Foo[int]
f.p = new(int)
f.p[] = 9
doAssert f.p[] == 9

block: # type-side compound: U = array[3, T]
type Foo[T; U = array[3, T]] = object
arr: U
var f: Foo[int]
f.arr[0] = 10
f.arr[2] = 30
doAssert f.arr[0] == 10
doAssert f.arr[2] == 30

block: # #4086 original: all params defaulted, `Foo[]` uses all defaults
type Foo[T = int] = object
x: T
var f: Foo[]
f.x = 42
doAssert f.x == 42
doAssert f.x is int

block: # alias of a defaulted instantiation
type Foo[T; U = T] = object
a: T
b: U
type IntFoo = Foo[int]
var f: IntFoo
f.a = 1
f.b = 2
doAssert f.b is int

block: # distinct of a defaulted instantiation
type Foo[T; U = seq[T]] = object
data: U
type DistFoo = distinct Foo[int]
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

block: # `Foo[]` with all params defaulted AND a dependent default (cascade)
type Foo[T = int; U = seq[T]; V = seq[U]] = object
a: T
b: U
c: V
var f: Foo[]
f.a = 1
f.b.add 2
f.c.add @[3, 4]
doAssert f.a is int
doAssert f.b == @[2]
doAssert f.c == @[@[3, 4]]

block: # `Foo[]` works uniformly in every type position (the point of the syntax)
type Foo[T = int] = object
x: T
# proc param position -> concrete Foo[int], NOT a generic proc
proc takesFoo(g: Foo[]): int = g.x
# return type position
proc makeFoo(): Foo[] =
result.x = 9
# field position
type Holder = object
inner: Foo[]
var h: Holder
h.inner.x = 5
doAssert takesFoo(h.inner) == 5
doAssert makeFoo().x == 9

block: # #9355: typedesc parameter default referencing an earlier generic param
func foo[T](U: type = T): U = default(U)
doAssert foo[int]() is int # U defaults to T
doAssert foo[string]() is string
doAssert foo[int](float) is float # explicit override still works
Loading