Add experimental inferGenericTypes switch (#22317)

* Infer generic bindings

* Simple test

* Add t

* Allow it to work for templates too

* Fix some builds by putting bindings in a template

* Fix builtins

* Slightly more exotic seq test

* Test value-based generics using array

* Pass expectedType into buildBindings

* Put buildBindings into a proc

* Manual entry

* Remove leftover `

* Improve language used in the manual

* Experimental flag and fix basic constructors

* Tiny commend cleanup

* Move to experimental manual

* Use 'kind' so tuples continue to fail like before

* Explicitly disallow tuples

* Table test and document tuples

* Test type reduction

* Disable inferGenericTypes check for CI tests

* Remove tuple info in manual

* Always reduce types. Testing CI

* Fixes

* Ignore tyGenericInst

* Prevent binding already bound generic params

* tyUncheckedArray

* Few more types

* Update manual and check for flag again

* Update tests/generics/treturn_inference.nim

* var candidate, remove flag check again for CI

* Enable check once more

---------

Co-authored-by: SirOlaf <>
Co-authored-by: Andreas Rumpf <rumpf_a@web.de>
This commit is contained in:
SirOlaf
2023-08-03 22:49:52 +02:00
committed by GitHub
parent 6b913b4741
commit 8d8d75706c
6 changed files with 288 additions and 11 deletions

View File

@@ -220,7 +220,8 @@ type
unicodeOperators, # deadcode
flexibleOptionalParams,
strictDefs,
strictCaseObjects
strictCaseObjects,
inferGenericTypes
LegacyFeature* = enum
allowSemcheckedAstModification,

View File

@@ -562,8 +562,61 @@ proc getCallLineInfo(n: PNode): TLineInfo =
discard
result = n.info
proc semResolvedCall(c: PContext, x: TCandidate,
n: PNode, flags: TExprFlags): PNode =
proc inheritBindings(c: PContext, x: var TCandidate, expectedType: PType) =
## Helper proc to inherit bound generic parameters from expectedType into x.
## Does nothing if 'inferGenericTypes' isn't in c.features
if inferGenericTypes notin c.features: return
if expectedType == nil or x.callee[0] == nil: return # required for inference
var
flatUnbound: seq[PType]
flatBound: seq[PType]
# seq[(result type, expected type)]
var typeStack = newSeq[(PType, PType)]()
template stackPut(a, b) =
## skips types and puts the skipped version on stack
# It might make sense to skip here one by one. It's not part of the main
# type reduction because the right side normally won't be skipped
const toSkip = { tyVar, tyLent, tyStatic, tyCompositeTypeClass }
let
x = a.skipTypes(toSkip)
y = if a.kind notin toSkip: b
else: b.skipTypes(toSkip)
typeStack.add((x, y))
stackPut(x.callee[0], expectedType)
while typeStack.len() > 0:
let (t, u) = typeStack.pop()
if t == u or t == nil or u == nil or t.kind == tyAnything or u.kind == tyAnything:
continue
case t.kind
of ConcreteTypes, tyGenericInvocation, tyUncheckedArray:
# nested, add all the types to stack
let
startIdx = if u.kind in ConcreteTypes: 0 else: 1
endIdx = min(u.sons.len() - startIdx, t.sons.len())
for i in startIdx ..< endIdx:
# early exit with current impl
if t[i] == nil or u[i] == nil: return
stackPut(t[i], u[i])
of tyGenericParam:
if x.bindings.idTableGet(t) != nil: return
# fully reduced generic param, bind it
if t notin flatUnbound:
flatUnbound.add(t)
flatBound.add(u)
else:
discard
for i in 0 ..< flatUnbound.len():
x.bindings.idTablePut(flatUnbound[i], flatBound[i])
proc semResolvedCall(c: PContext, x: var TCandidate,
n: PNode, flags: TExprFlags;
expectedType: PType = nil): PNode =
assert x.state == csMatch
var finalCallee = x.calleeSym
let info = getCallLineInfo(n)
@@ -583,10 +636,12 @@ proc semResolvedCall(c: PContext, x: TCandidate,
if x.calleeSym.magic in {mArrGet, mArrPut}:
finalCallee = x.calleeSym
else:
c.inheritBindings(x, expectedType)
finalCallee = generateInstance(c, x.calleeSym, x.bindings, n.info)
else:
# For macros and templates, the resolved generic params
# are added as normal params.
c.inheritBindings(x, expectedType)
for s in instantiateGenericParamList(c, gp, x.bindings):
case s.kind
of skConst:
@@ -615,7 +670,8 @@ proc tryDeref(n: PNode): PNode =
result.add n
proc semOverloadedCall(c: PContext, n, nOrig: PNode,
filter: TSymKinds, flags: TExprFlags): PNode =
filter: TSymKinds, flags: TExprFlags;
expectedType: PType = nil): PNode =
var errors: CandidateErrors = @[] # if efExplain in flags: @[] else: nil
var r = resolveOverloads(c, n, nOrig, filter, flags, errors, efExplain in flags)
if r.state == csMatch:
@@ -625,7 +681,7 @@ proc semOverloadedCall(c: PContext, n, nOrig: PNode,
message(c.config, n.info, hintUserRaw,
"Non-matching candidates for " & renderTree(n) & "\n" &
candidates)
result = semResolvedCall(c, r, n, flags)
result = semResolvedCall(c, r, n, flags, expectedType)
else:
if efDetermineType in flags and c.inGenericContext > 0 and c.matchedConcept == nil:
result = semGenericStmt(c, n)

View File

@@ -135,7 +135,7 @@ type
semOperand*: proc (c: PContext, n: PNode, flags: TExprFlags = {}): PNode {.nimcall.}
semConstBoolExpr*: proc (c: PContext, n: PNode): PNode {.nimcall.} # XXX bite the bullet
semOverloadedCall*: proc (c: PContext, n, nOrig: PNode,
filter: TSymKinds, flags: TExprFlags): PNode {.nimcall.}
filter: TSymKinds, flags: TExprFlags, expectedType: PType = nil): PNode {.nimcall.}
semTypeNode*: proc(c: PContext, n: PNode, prev: PType): PType {.nimcall.}
semInferredLambda*: proc(c: PContext, pt: TIdTable, n: PNode): PNode
semGenerateInstance*: proc (c: PContext, fn: PSym, pt: TIdTable,

View File

@@ -952,17 +952,17 @@ proc semStaticExpr(c: PContext, n: PNode; expectedType: PType = nil): PNode =
result = fixupTypeAfterEval(c, result, a)
proc semOverloadedCallAnalyseEffects(c: PContext, n: PNode, nOrig: PNode,
flags: TExprFlags): PNode =
flags: TExprFlags; expectedType: PType = nil): PNode =
if flags*{efInTypeof, efWantIterator, efWantIterable} != {}:
# consider: 'for x in pReturningArray()' --> we don't want the restriction
# to 'skIterator' anymore; skIterator is preferred in sigmatch already
# for typeof support.
# for ``typeof(countup(1,3))``, see ``tests/ttoseq``.
result = semOverloadedCall(c, n, nOrig,
{skProc, skFunc, skMethod, skConverter, skMacro, skTemplate, skIterator}, flags)
{skProc, skFunc, skMethod, skConverter, skMacro, skTemplate, skIterator}, flags, expectedType)
else:
result = semOverloadedCall(c, n, nOrig,
{skProc, skFunc, skMethod, skConverter, skMacro, skTemplate}, flags)
{skProc, skFunc, skMethod, skConverter, skMacro, skTemplate}, flags, expectedType)
if result != nil:
if result[0].kind != nkSym:
@@ -1138,7 +1138,7 @@ proc semDirectOp(c: PContext, n: PNode, flags: TExprFlags; expectedType: PType =
# this seems to be a hotspot in the compiler!
let nOrig = n.copyTree
#semLazyOpAux(c, n)
result = semOverloadedCallAnalyseEffects(c, n, nOrig, flags)
result = semOverloadedCallAnalyseEffects(c, n, nOrig, flags, expectedType)
if result != nil: result = afterCallActions(c, result, nOrig, flags, expectedType)
else: result = errorNode(c, n)
@@ -3120,7 +3120,7 @@ proc semExpr(c: PContext, n: PNode, flags: TExprFlags = {}, expectedType: PType
elif s.magic == mNone: result = semDirectOp(c, n, flags, expectedType)
else: result = semMagic(c, n, s, flags, expectedType)
of skProc, skFunc, skMethod, skConverter, skIterator:
if s.magic == mNone: result = semDirectOp(c, n, flags)
if s.magic == mNone: result = semDirectOp(c, n, flags, expectedType)
else: result = semMagic(c, n, s, flags, expectedType)
else:
#liMessage(n.info, warnUser, renderTree(n));

View File

@@ -124,6 +124,87 @@ would not match the type of the variable, and an error would be given.
The extent of this varies, but there are some notable special cases.
Inferred generic parameters
---------------------------
In expressions making use of generic procs or templates, the expected
(unbound) types are often able to be inferred based on context.
This feature has to be enabled via `{.experimental: "inferGenericTypes".}`
```nim test = "nim c $1"
{.experimental: "inferGenericTypes".}
import std/options
var x = newSeq[int](1)
# Do some work on 'x'...
# Works!
# 'x' is 'seq[int]' so 'newSeq[int]' is implied
x = newSeq(10)
# Works!
# 'T' of 'none' is bound to the 'T' of 'noneProducer', passing it along.
# Effectively 'none.T = noneProducer.T'
proc noneProducer[T](): Option[T] = none()
let myNone = noneProducer[int]()
# Also works
# 'myOtherNone' binds its 'T' to 'float' and 'noneProducer' inherits it
# noneProducer.T = myOtherNone.T
let myOtherNone: Option[float] = noneProducer()
# Works as well
# none.T = myOtherOtherNone.T
let myOtherOtherNone: Option[int] = none()
```
This is achieved by reducing the types on the lhs and rhs until the *lhs* is left with only types such as `T`.
While lhs and rhs are reduced together, this does *not* mean that the *rhs* will also only be left
with a flat type `Z`, it may be of the form `MyType[Z]`.
After the types have been reduced, the types `T` are bound to the types that are left on the rhs.
If bindings *cannot be inferred*, compilation will fail and manual specification is required.
An example for *failing inference* can be found when passing a generic expression
to a function/template call:
```nim test = "nim c $1" status = 1
{.experimental: "inferGenericTypes".}
proc myProc[T](a, b: T) = discard
# Fails! Unable to infer that 'T' is supposed to be 'int'
myProc(newSeq[int](), newSeq(1))
# Works! Manual specification of 'T' as 'int' necessary
myProc(newSeq[int](), newSeq[int](1))
```
Combination of generic inference with the `auto` type is also unsupported:
```nim test = "nim c $1" status = 1
{.experimental: "inferGenericTypes".}
proc produceValue[T]: auto = default(T)
let a: int = produceValue() # 'auto' cannot be inferred here
```
**Note**: The described inference does not permit the creation of overrides based on
the return type of a procedure. It is a mapping mechanism that does not attempt to
perform deeper inference, nor does it modify what is a valid override.
```nim test = "nim c $1" status = 1
# Doesn't affect the following code, it is invalid either way
{.experimental: "inferGenericTypes".}
proc a: int = 0
proc a: float = 1.0 # Fails! Invalid code and not recommended
```
Sequence literals
-----------------

View File

@@ -0,0 +1,139 @@
{.experimental: "inferGenericTypes".}
import std/tables
block:
type
MyOption[T, Z] = object
x: T
y: Z
proc none[T, Z](): MyOption[T, Z] =
when T is int:
result.x = 22
when Z is float:
result.y = 12.0
proc myGenericProc[T, Z](): MyOption[T, Z] =
none() # implied by return type
let a = myGenericProc[int, float]()
doAssert a.x == 22
doAssert a.y == 12.0
let b: MyOption[int, float] = none() # implied by type of b
doAssert b.x == 22
doAssert b.y == 12.0
# Simple template based result with inferred type for errors
block:
type
ResultKind {.pure.} = enum
Ok
Err
Result[T] = object
case kind: ResultKind
of Ok:
data: T
of Err:
errmsg: cstring
template err[T](msg: static cstring): Result[T] =
Result[T](kind : ResultKind.Err, errmsg : msg)
proc testproc(): Result[int] =
err("Inferred error!") # implied by proc return
let r = testproc()
doAssert r.kind == ResultKind.Err
doAssert r.errmsg == "Inferred error!"
# Builtin seq
block:
let x: seq[int] = newSeq(1)
doAssert x is seq[int]
doAssert x.len() == 1
type
MyType[T, Z] = object
x: T
y: Z
let y: seq[MyType[int, float]] = newSeq(2)
doAssert y is seq[MyType[int, float]]
doAssert y.len() == 2
let z = MyType[seq[float], string](
x : newSeq(3),
y : "test"
)
doAssert z.x is seq[float]
doAssert z.x.len() == 3
doAssert z.y is string
doAssert z.y == "test"
# array
block:
proc giveArray[N, T](): array[N, T] =
for i in 0 .. N.high:
result[i] = i
var x: array[2, int] = giveArray()
doAssert x == [0, 1]
# tuples
block:
proc giveTuple[T, Z]: (T, Z, T) = discard
let x: (int, float, int) = giveTuple()
doAssert x is (int, float, int)
doAssert x == (0, 0.0, 0)
proc giveNamedTuple[T, Z]: tuple[a: T, b: Z] = discard
let y: tuple[a: int, b: float] = giveNamedTuple()
doAssert y is (int, float)
doAssert y is tuple[a: int, b: float]
doAssert y == (0, 0.0)
proc giveNestedTuple[T, Z]: ((T, Z), Z) = discard
let z: ((int, float), float) = giveNestedTuple()
doAssert z is ((int, float), float)
doAssert z == ((0, 0.0), 0.0)
# nesting inside a generic type
type MyType[T] = object
x: T
let a = MyType[(int, MyType[float])](x : giveNamedTuple())
doAssert a.x is (int, MyType[float])
# basic constructors
block:
type MyType[T] = object
x: T
proc giveValue[T](): T =
when T is int:
12
else:
default(T)
let x = MyType[int](x : giveValue())
doAssert x.x is int
doAssert x.x == 12
let y = MyType[MyType[float]](x : MyType[float](x : giveValue()))
doAssert y.x is MyType[float]
doAssert y.x.x is float
doAssert y.x.x == 0.0
# 'MyType[float]' is bound to 'T' directly
# instead of mapping 'T' to 'float'
let z = MyType[MyType[float]](x : giveValue())
doAssert z.x is MyType[float]
doAssert z.x.x == 0.0
type Foo = object
x: Table[int, float]
let a = Foo(x: initTable())
doAssert a.x is Table[int, float]