Skip to content

Commit

Permalink
treat when within generic objects as generic (#867)
Browse files Browse the repository at this point in the history
## Summary

Clarify the specification regarding how `when` statements within generic
object definitions work and adjust the implementation accordingly.

`when` statements inside generic object definitions are now treated as
generic. This is a breaking change, but it makes using generic
parameters in more complex expressions possible. Most notably,
`isnot` now works properly inside generic objects' `when` conditions.

## Details

* process `nkElifBranch` conditions of `nkWhenStmt` nodes within generic
  objects via `semGenericStmt` when analyzing the definition
* remove the redundant AST checks in `replaceObjBranches` and
  `replaceTypeVarsN`; the  already take place in `semRecordNode`
* use `semConstBoolExpr` instead of `semConstExpr` when evaluating
  generic `when` conditions
* `semConstBoolExpr` not returning an `nkIntLit` happens in case of
  cascading errors, so don't treat this as an internal error
  • Loading branch information
zerbina authored Sep 2, 2023
1 parent 90ce933 commit 0121a7f
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 41 deletions.
4 changes: 3 additions & 1 deletion compiler/sem/semtypes.nim
Original file line number Diff line number Diff line change
Expand Up @@ -944,7 +944,9 @@ proc semRecordNodeAux(c: PContext, n: PNode, check: var IntSet, pos: var int,
if e.kind != nkIntLit: discard "don't report followup error"
elif e.intVal != 0 and branch == nil: branch = it[1]
else:
it[0] = forceBool(c, semExprWithType(c, it[0]))
it[0] = semGenericStmt(c, it[0])
# ensure that all type variables have their symbol bound:
discard fixupTypeVars(c, it[0])
of nkElse:
checkSonsLen(it, 1, c.config)
if branch == nil: branch = it[0]
Expand Down
54 changes: 17 additions & 37 deletions compiler/sem/semtypinst.nim
Original file line number Diff line number Diff line change
Expand Up @@ -280,37 +280,24 @@ proc replaceObjBranches(cl: TReplTypeVars, n: PNode): PNode =
discard
of nkRecWhen:
var branch: PNode = nil # the branch to take
for i in 0 ..< n.len:
var it = n[i]
if it == nil:
cl.c.config.globalReport(reportAst(
rsemIllformedAst, n, str = "subnode at idx $1 is 'nil'" % [$i]))

for it in n.items:
case it.kind
of nkElifBranch:
checkSonsLen(it, 2, cl.c.config)
var cond = it[0]
var e = cl.c.semConstExpr(cl.c, cond)
cl.c.config.internalAssert(e.kind == nkIntLit, e.info):
"ReplaceTypeVarsN: when condition not a bool"

if e.intVal != 0 and branch == nil: branch = it[1]
let e = cl.c.semConstBoolExpr(cl.c, it[0])
if e.kind == nkIntLit and e.intVal != 0 and branch == nil:
branch = it[1]
of nkElse:
checkSonsLen(it, 1, cl.c.config)
if branch == nil:
branch = it[0]
else:
cl.c.config.globalReport(reportAst(
rsemIllformedAst, n,
str = "Expected else or elif for subnode at idx $1, but found $2" % [
$i, $it.kind]))
unreachable(it.kind)
if branch != nil:
result = replaceObjBranches(cl, branch)
else:
result = newNodeI(nkRecList, n.info)
else:
for i in 0..<n.len:
n[i] = replaceObjBranches(cl, n[i])
for it in n.sons.mitems:
it = replaceObjBranches(cl, it)

proc replaceTypeVarsN(cl: var TReplTypeVars, n: PNode; start=0): PNode =
## Replaces references to unresolved types in AST associated with types (i.e.:
Expand All @@ -333,28 +320,21 @@ proc replaceTypeVarsN(cl: var TReplTypeVars, n: PNode; start=0): PNode =
result = newNodeI(nkRecList, n.info)
of nkRecWhen:
var branch: PNode = nil # the branch to take
for i in 0..<n.len:
var it = n[i]
if it == nil:
cl.c.config.globalReport(reportAst(
rsemIllformedAst, n,
str = "subnode at idx $1 is 'nil'" % [$i]))
for it in n.items:
case it.kind
of nkElifBranch:
checkSonsLen(it, 2, cl.c.config)
var cond = prepareNode(cl, it[0])
var e = cl.c.semConstExpr(cl.c, cond)
cl.c.config.internalAssert(e.kind == nkIntLit, e.info):
"ReplaceTypeVarsN: when condition not a bool"
if e.intVal != 0 and branch == nil: branch = it[1]
let e = cl.c.semConstBoolExpr(cl.c, prepareNode(cl, it[0]))
# note: `e` may not be an int literal in case of cascading
# errors
if e.kind == nkIntLit and e.intVal != 0 and branch == nil:
branch = it[1]
of nkElse:
checkSonsLen(it, 1, cl.c.config)
if branch == nil: branch = it[0]
else:
cl.c.config.globalReport(reportAst(
rsemIllformedAst, n,
str = "Expected else or elif for subnode at idx $1, but found $2" % [
$i, $it.kind]))
# should have already been rejected when first analysing the record
# structure
unreachable(it.kind)

if branch != nil:
result = replaceTypeVarsN(cl, branch)
else:
Expand Down
18 changes: 16 additions & 2 deletions doc/manual.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3084,10 +3084,24 @@ exceptions:
translated by the compiler, the other statements are not checked for
semantics! However, each condition is checked for semantics.

The `when` statement enables conditional compilation techniques. As
a special syntactic extension, the `when` construct is also available
The `when` statement enables conditional compilation techniques.

When statement inside `object`
------------------------------

As a special syntactic extension, the `when` construct is also available
within `object` definitions.

At which point the `when` construct is evaluated and collapsed depends on
whether the object is *generic*:
* if it is not, the conditions are evaluated and a branch is chosen
when inside the definition
* if it is, all condition expressions are treated as generic expressions
that are only instantiated, and a branch chosen, when the object type is
instantiated

Same as for `when` statements outside of `object` definitions, each condition
is checked for semantics and evaluated.

When nimvm statement
--------------------
Expand Down
41 changes: 40 additions & 1 deletion tests/lang_callable/generics/tgenerics_issues.nim
Original file line number Diff line number Diff line change
Expand Up @@ -1045,4 +1045,43 @@ block type_inference_from_nested_invocation:
result = name(T)

var x: (Type[string],)
doAssert f(x) == "string"
doAssert f(x) == "string"

block isnot_in_record_when:
# the `is` operator used inside ``when`` conditions of generic objects
# must work even if used in an argument context
type Object[A] = object
when not(A is (string | float)):
a: int
elif not(A is float):
b: string
else:
c: float

proc f[T](x: T): Object[T] =
when T is int:
result.a = 1
elif T is string:
result.b = "2"
else:
result.c = 3.0

doAssert f(0).a == 1
doAssert f("").b == "2"
doAssert f(0.0).c == 3.0

block typed_macro_in_generic_object_when:
# semantic macros (those not operating on syntax only) are
# used in ``when`` conditions of generic object when the
# type is instantiated
macro m(x: static int): int =
result = quote: `x`

type Object[N: static int] = object
when m(N) == 1:
val: int

var o1 = Object[0]()
doAssert not compiles(o1.val)
var o2 = Object[1](val: 2)
doAssert o2.val == 2

0 comments on commit 0121a7f

Please sign in to comment.