Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
20 changes: 20 additions & 0 deletions compiler/semstmts.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}

Expand Down Expand Up @@ -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 = {}

Expand Down
67 changes: 65 additions & 2 deletions compiler/semtypes.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
Araq marked this conversation as resolved.
Outdated
# 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

Expand Down Expand Up @@ -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..<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)
for i in 0..<body.len-1:
bracket.add copyTree(body[i].sym.ast)
result = semGeneric(c, bracket, 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 +1823,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 +1866,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
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
123 changes: 123 additions & 0 deletions tests/generics/tgeneric_param_default_deps.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
discard """
matrix: "; --mm:refc"
"""

import std/deques

# Generic type parameter defaults that reference other type parameters
# (issues #4086, #9355).
#
# 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`
#
# 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 sample 1: proc default referencing T (untyped form)
func foo[T](U = T): U = discard
Comment thread
Araq marked this conversation as resolved.
Outdated
# `U` defaults to whatever T resolves to.
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: # #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, invocation with no args
type Foo[T = int] = object
x: T
var f: Foo
f.x = 42
doAssert f.x == 42

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
Loading