-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
fixes #4086; allow generic type parameter defaults to reference earlier type params #25799
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: devel
Are you sure you want to change the base?
Changes from 2 commits
18f250e
305c731
649f346
1f0c6cd
a201fea
5faad66
f2551c5
56c1984
267264a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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..<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" % | ||
|
|
@@ -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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
@@ -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 | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Look, this is |
||
| if defaultValue.kind == nkNilLit: | ||
| defaultValue = implicitConv(nkHiddenStdConv, formal.typ, defaultValue, m, c) | ||
| # proc foo(x: T = 0.0) | ||
|
|
||
| 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 | ||
|
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 | ||
Uh oh!
There was an error while loading. Please reload this page.