Skip to content

Commit

Permalink
sem: don't analyze dependent operands to static (#1345)
Browse files Browse the repository at this point in the history
## Summary

Makes instantiations such as `Generic[someProc(T)]` work, where
`T` is some generic parameter (and receiving type parameter is a
`static` one) -- the `someProc(T)` expression is typed once `T` is
substituted, making the behaviour consistent with `array` and `range`.

## Details

Fully analyzing (i.e., with `semExpr`) arbitrary expressions that might
reference unresolved type variables generally doesn't work. Operands to
generic invocations were eagerly analyzed (in `matchesAux`), usually
resulting in errors when the operand needs to be a `static` value.

Disallowing expressions dependent on unresolved type variables in this
context would be a regression, since some expressions are special-cased
to work (such as `sizeof(T)`).

To address the problem, before typing the operand, it's first checked
whether it depends on unresolved type variables. If it does, the
expression is treated as a generic expression, with a `tyFromExpr`
assigned as its type -- otherwise it's analyzed as usual.

If the operand has an unknown type (`tyFromExpr`), it's wrapped in a
conversion to a type derived from the generic parameter's constraint,
to make sure the operand types later. If no concrete type can be
derived from the constraint, a type mismatch is reported.

To not convolute `matchesAux` further, the argument matching where the
callee is a `tyGenericBody` is moved into the new `matchesType`. It
does largely the same as `matchesAux`, but with everything not
applicable to types removed (such as the varargs handling).

---------

Co-authored-by: Saem Ghani <[email protected]>
  • Loading branch information
zerbina and saem authored Jun 16, 2024
1 parent e3f9a33 commit da65fbf
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 1 deletion.
1 change: 1 addition & 0 deletions compiler/sem/sem.nim
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,7 @@ proc myOpen(graph: ModuleGraph; module: PSym;
c.semTypeNode = semTypeNode
c.instTypeBoundOp = sigmatch.instTypeBoundOp
c.hasUnresolvedArgs = hasUnresolvedArgs
c.semGenericExpr = semGenericExpr
c.templInstCounter = new int

pushProcCon(c, module)
Expand Down
3 changes: 3 additions & 0 deletions compiler/sem/semdata.nim
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,9 @@ type
op: TTypeAttachedOp; col: int): PSym {.nimcall.}
## read to break cyclic dependencies, init in sem during module open and
## read in liftdestructors and semtypinst
semGenericExpr*: proc (c: PContext, n: PNode): PNode {.nimcall.}
## read to break cyclic dependencies, init in sem during module open and
## read in sigmatch
# -------------------------------------------------------------------------
# end: not entirely clear why, function pionters for certain sem calls?
# -------------------------------------------------------------------------
Expand Down
7 changes: 7 additions & 0 deletions compiler/sem/semtypes.nim
Original file line number Diff line number Diff line change
Expand Up @@ -1696,6 +1696,13 @@ proc semStmtListType(c: PContext, n: PNode, prev: PType): PType =
else:
result = nil

proc semGenericExpr(c: PContext, n: PNode): PNode =
## Runs the generic pre-pass on `n` and returns the result. Similar to
## ``semGenericStmt``, but makes sure that all generic parameter symbols
## were bound.
result = semGenericStmt(c, n)
discard fixupTypeVars(c, result)

proc semGenericParamInInvocation(c: PContext, n: PNode): PType =
result = semTypeNode(c, n, nil)
n.typ = makeTypeDesc(c, result)
Expand Down
126 changes: 125 additions & 1 deletion compiler/sem/sigmatch.nim
Original file line number Diff line number Diff line change
Expand Up @@ -3120,6 +3120,127 @@ proc matchesAux(c: PContext, n, nOrig: PNode, m: var TCandidate, marker: var Int
m.error.firstMismatch.pos = a
m.error.firstMismatch.formal = formal

proc matchesType(c: PContext, n: PNode, m: var TCandidate,
marker: var IntSet) =
## Matches the arguments taken from invocation expression `n` against the
## ``tyGenericBody`` callee and fills `m` with the results. `marker` is
## updated with the matched-against formal positions.
m.state = csMatch # until proven otherwise
m.error.firstMismatch = MismatchInfo()

# pre-pass: make sure the AST is valid. `n` is production AST, so it can be
# modified in-place
var hasError = false
for i in 1..<n.len:
if n[i].kind == nkExprEqExpr:
let (ident, err) = considerQuotedIdent(c, n[i][0])
if err != nil:
n[i][0] = err
hasError = true
else:
n[i][0] = newIdentNode(ident, n[i][0].info)

if hasError:
# abort early
m.state = csNoMatch
m.call = c.config.wrapError(n)
return

m.call = newNodeI(n.kind, n.info, m.callee.len)
m.call[0] = n[0]

var f = 0
var i = 1
var formal: PSym

while i < n.len:
# select the formal parameter:
var operand: PNode
case n[i].kind
of nkExprEqExpr:
# explicit parameter
formal = getNamedParamFromList(m.callee.n, n[i][0].ident)
if formal.isNil:
m.error.firstMismatch.kind = kUnknownNamedParam
break

operand = n[i][1]
elif f < m.callee.n.len:
# implicit parameter
formal = m.callee.n[f].sym
operand = n[i]
else:
m.error.firstMismatch.kind = kExtraArg
break

if containsOrIncl(marker, formal.position):
m.error.firstMismatch.kind = kAlreadyGiven
break

# reset the per-parameter state:
m.typedescMatched = false

m.error.firstMismatch.kind = kTypeMismatch

# match the argument against the formal type:
var arg: PNode

if (tfHasStatic in formal.typ.skipTypes({tyDistinct}).flags or
formal.typ.kind == tyStatic) and c.hasUnresolvedArgs(c, operand):
# the expression depends on not-yet resolved generic parameters,
# ``semOperand`` won't work
operand = c.semGenericExpr(c, operand)
if operand.kind == nkError or operand.typ != nil:
arg = paramTypesMatch(m, formal.typ, operand.typ, operand)
elif formal.typ.kind == tyStatic:
# some expression that's more complex than just being a generic
# parameter symbol
if formal.typ.base.kind == tyNone:
# no constraints
arg = copyNodeWithKids(operand)
arg.typ = makeTypeFromExpr(c, operand)
else:
# the static is constrained. We don't know the argument's type yet,
# so we cannot know up-front whether the expression will fits once
# all type variables it depends on are resolved
# XXX: to support this at least somewhat, the argument is wrapped
# in a conversion to the expected type. If the types are
# wholly incompatible, later analysis of the conversion will
# yield an error. Non-exact matches where the types have a
# "convertible" relationship will not result in an error
arg = newTreeI(nkConv, operand.info,
newNodeIT(nkType, operand.info, formal.typ.base),
operand)
arg.typ = makeTypeFromExpr(c, copyNodeWithKids(arg))
else:
# we don't know the argument's type, nor can we enforce that it'll
# match the formal type later -> type mismatch
m.call[formal.position + 1] = copyNodeWithKids(operand)
m.call[formal.position + 1].typ = makeTypeFromExpr(c, operand)
break
else:
operand = m.c.semOperand(m.c, operand)
arg = paramTypesMatch(m, formal.typ, operand.typ, operand)

if arg != nil:
# errors don't need to be considered here; they're handled through
# `fauxMatch`
m.call[formal.position + 1] = arg
else:
# legacy error handling
m.call[formal.position + 1] = operand
break

f = max(formal.position + 1, f + 1)
inc i

if i < n.len:
# an error occurred
m.state = csNoMatch
m.error.firstMismatch.pos = i
m.error.firstMismatch.arg = n[i]
m.error.firstMismatch.formal = formal

proc semFinishOperands*(c: PContext, n: PNode) =
# this needs to be called to ensure that after overloading resolution every
# argument has been sem'checked:
Expand Down Expand Up @@ -3155,7 +3276,10 @@ proc matches*(c: PContext, n, nOrig: PNode, m: var TCandidate) =
return

var marker = initIntSet()
matchesAux(c, n, nOrig, m, marker)
if m.callee.kind == tyGenericBody:
matchesType(c, n, m, marker)
else:
matchesAux(c, n, nOrig, m, marker)

if m.state == csNoMatch:
return
Expand Down
27 changes: 27 additions & 0 deletions tests/lang_callable/generics/tdependent_operands_to_static.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
discard """
description: '''
Various tests for operands to `static` constrained generic type parameters
where the operand's type depends on unresolved type variables.
'''
"""

type
Type1[T: static] = object ## only must be *some* static value
Type2[T: static int] = object ## must a be a static int

proc eval[T](x: T): T {.compileTime.} =
x

proc p1[T](): Type1[eval(default(T))] = discard
proc p2[T](): Type2[eval(default(T))] = discard
# ^^ whether the ``Type2`` can be instantiated depends on the later
# supplied `T`

discard p1[int]() # works
discard p1[float]() # works
discard p1[string]() # works

discard p2[int]() # int is convertible to int -> works
discard p2[float]() # float is convertible to int -> works
# string is not convertible to float -> fails:
doAssert not compiles(p3[string]())
21 changes: 21 additions & 0 deletions tests/lang_callable/generics/tdependent_operands_to_static_2.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
discard """
description: '''
Operands with a type not known upfront cannot be used as arguments to
complex static
'''
errormsg: "cannot instantiate Type"
line: 21
knownIssue: '''
`Type`s generic parameter is not detected as containing a `static`, thus
full analysis is not disabled (which subsequently fails)
'''
"""

type
Type[T: string | static float] = object ## must be a string or static float

proc eval[T](x: T): T {.compileTime.} = x

# the operand's type is not known and no concrete type can be derived from
# the constraint -> reject early
proc p[T](): Type[eval(default(T))] = discard

0 comments on commit da65fbf

Please sign in to comment.