Skip to content
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
008ffa4
Infer generic bindings
Jul 22, 2023
bc08ff2
Simple test
Jul 22, 2023
257fd33
Add t
Jul 22, 2023
0c6d8bb
Allow it to work for templates too
Jul 22, 2023
d5cbc7e
Fix some builds by putting bindings in a template
Jul 22, 2023
f2ed402
Fix builtins
Jul 23, 2023
cf36fa8
Slightly more exotic seq test
Jul 23, 2023
a235f14
Test value-based generics using array
Jul 23, 2023
e331214
Pass expectedType into buildBindings
Jul 23, 2023
851fbcf
Put buildBindings into a proc
Jul 23, 2023
04f7e08
Manual entry
Jul 23, 2023
d607d26
Remove leftover `
Jul 23, 2023
f915036
Improve language used in the manual
Jul 24, 2023
165673b
Experimental flag and fix basic constructors
Jul 24, 2023
9e42bbc
Tiny commend cleanup
Jul 24, 2023
e66af09
Move to experimental manual
Jul 24, 2023
444c169
Use 'kind' so tuples continue to fail like before
Jul 24, 2023
b554162
Explicitly disallow tuples
Jul 24, 2023
1cad79f
Table test and document tuples
Jul 24, 2023
189c9e1
Test type reduction
Jul 24, 2023
6d85aeb
Disable inferGenericTypes check for CI tests
Jul 24, 2023
b006c4e
Remove tuple info in manual
Jul 24, 2023
347eb20
Always reduce types. Testing CI
Jul 26, 2023
2a8353c
Fixes
Jul 26, 2023
dccee38
Ignore tyGenericInst
Jul 26, 2023
be98387
Prevent binding already bound generic params
Jul 29, 2023
fe2b42e
tyUncheckedArray
Jul 29, 2023
524a289
Few more types
Jul 29, 2023
d926772
Update manual and check for flag again
Jul 29, 2023
fbde75b
Update tests/generics/treturn_inference.nim
Araq Aug 2, 2023
970fafc
var candidate, remove flag check again for CI
Aug 2, 2023
18465fa
Enable check once more
Aug 2, 2023
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
3 changes: 2 additions & 1 deletion compiler/options.nim
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,8 @@ type
unicodeOperators, # deadcode
flexibleOptionalParams,
strictDefs,
strictCaseObjects
strictCaseObjects,
inferGenericTypes

LegacyFeature* = enum
allowSemcheckedAstModification,
Expand Down
65 changes: 60 additions & 5 deletions compiler/semcall.nim
Original file line number Diff line number Diff line change
Expand Up @@ -562,8 +562,62 @@ proc getCallLineInfo(n: PNode): TLineInfo =
discard
result = n.info

proc inheritBindings(c: PContext, x: TCandidate, expectedType: PType): TIdTable =
## Helper proc to inherit bound generic parameters from expectedType into a new TIdTable.
## Returns existing bindings if 'inferGenericTypes' isn't in c.features
result = x.bindings
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)]()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the effects of this computation on the overall compile-times?

Copy link
Contributor Author

@SirOlaf SirOlaf Aug 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, asked the same before without receiving a response. Will copy and paste a million lines or so to test (on that note it seems that hints/errors are all on the same line after line of uint16's max is reached)

Copy link
Contributor Author

@SirOlaf SirOlaf Aug 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using this code to generate two files, they both take about the same amount of time (immeasurable, they go back and forth. If you know of a better way to test please tell me). The cache was cleared before each compile. Memory usage in this specific test seems to be slightly better for the inferred version, but for other tests the one without inference uses less memory. The version of the nim compiler I used had the check for inferGenericTypes enabled.

Code
const copies = 10_000

block:
  var code = """{.experimental: "inferGenericTypes".}

type
  TestType[A, B, C, D, E, F, G, H, I, J] = object

proc giveData[A, B, C, D, E, F, G, H, I, J](): TestType[A, B, C, D, seq[E], F, G, H, I, seq[seq[J]]] = discard
var x: TestType[int, float, int, char, seq[byte], int16, uint32, seq[float], int, seq[seq[int]]] = giveData()
"""

  for i in 0 ..< copies:
    code.add("x = giveData()" & "\n")
  writeFile("out1.nim", code)

block:
  var code = """type
  TestType[A, B, C, D, E, F, G, H, I, J] = object

proc giveData[A, B, C, D, E, F, G, H, I, J](): TestType[A, B, C, D, seq[E], F, G, H, I, seq[seq[J]]] = discard
var x = giveData[int, float, int, char, seq[byte], int16, uint32, seq[float], int, seq[seq[int]]]()
"""

  for i in 0 ..< copies:
    code.add("x = giveData[int, float, int, char, seq[byte], int16, uint32, seq[float], int, seq[seq[int]]]()" & "\n")
  writeFile("out2.nim", code)


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 result.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():
result.idTablePut(flatUnbound[i], flatBound[i])

proc semResolvedCall(c: PContext, x: TCandidate,
n: PNode, flags: TExprFlags): PNode =
n: PNode, flags: TExprFlags;
expectedType: PType = nil): PNode =
assert x.state == csMatch
var finalCallee = x.calleeSym
let info = getCallLineInfo(n)
Expand All @@ -583,11 +637,11 @@ proc semResolvedCall(c: PContext, x: TCandidate,
if x.calleeSym.magic in {mArrGet, mArrPut}:
finalCallee = x.calleeSym
else:
finalCallee = generateInstance(c, x.calleeSym, x.bindings, n.info)
finalCallee = generateInstance(c, x.calleeSym, c.inheritBindings(x, expectedType), n.info)
else:
# For macros and templates, the resolved generic params
# are added as normal params.
for s in instantiateGenericParamList(c, gp, x.bindings):
for s in instantiateGenericParamList(c, gp, c.inheritBindings(x, expectedType)):
case s.kind
of skConst:
if not s.astdef.isNil:
Expand Down Expand Up @@ -615,7 +669,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:
Expand All @@ -625,7 +680,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)
Expand Down
2 changes: 1 addition & 1 deletion compiler/semdata.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 5 additions & 5 deletions compiler/semexprs.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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));
Expand Down
81 changes: 81 additions & 0 deletions doc/manual_experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----------------

Expand Down
139 changes: 139 additions & 0 deletions tests/generics/treturn_inference.nim
Original file line number Diff line number Diff line change
@@ -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]