Skip to content

Commit

Permalink
sem: introduce the notion of explicit contexts
Browse files Browse the repository at this point in the history
This allows for partially restoring the original behaviour of
`compiles`: a sub-compilation now chooses the closest explicit execution
context.
  • Loading branch information
zerbina committed Sep 19, 2023
1 parent c4929e3 commit 513aed4
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 22 deletions.
4 changes: 2 additions & 2 deletions compiler/sem/sem.nim
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,7 @@ proc hasCycle(n: PNode): bool =

proc tryConstExpr(c: PContext, n: PNode): PNode =
addInNimDebugUtils(c.config, "tryConstExpr", n, result)
pushExecCon(c, isStatic=false)
pushExecCon(c, {})
let e = semExprWithType(c, n)
popExecCon(c)
if e.isError:
Expand Down Expand Up @@ -631,7 +631,7 @@ proc semConstExpr(c: PContext, n: PNode): PNode =
# TODO: propagate the error upwards instead of reporting it here. Also
# remove the error correction -- that should be done at the callsite,
# if needed
pushExecCon(c, isStatic=false)
pushExecCon(c, {})
let e = semExprWithType(c, n)
popExecCon(c)
if e.isError:
Expand Down
30 changes: 14 additions & 16 deletions compiler/sem/semdata.nim
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ type
mapping*: TIdTable
localBindStmts*: seq[PNode]

ExecutionConFlag* = enum
ecfStatic ## the context is that of a ``static`` block/expression or of
## a `const`'s initializer
ecfExplicit ## the context is explicit. Sub-compilation (``compiles``)
## always picks the closest explicit context

ExecutionCon* = object
## Stores information about an abstract execution context, that is the
## context in which analyzed code will later be run in.
Expand All @@ -83,16 +89,9 @@ type
caseContext*: seq[tuple[n: PNode, idx: int]]
## the stack of enclosing ``nkCastStmt`` nodes

inStaticContext*: bool
## whether we're in a ``static`` block/expression or in the initializer
## expression of a constant
##
## written:
## - semdata: when pushing a execution context (``pushExecCon``)
## read:
## - semBindSym: whether to resolve the binding or not
## - evalAtCompileTime: whether the procedure should be eagerly
## evaluated with the VM
flags*: set[ExecutionConFlag]
## additional flags describing the context. Initialized once when
## creating an ``ExecutionCon`` and only queried after that.

TMatchedConcept* = object
candidateType*: PType
Expand Down Expand Up @@ -761,10 +760,9 @@ proc popOwner*(c: PContext) =
proc lastOptionEntry*(c: PContext): POptionEntry =
result = c.optionStack[^1]

proc pushExecCon*(c: PContext, isStatic: bool) {.inline.} =
## Pushes a new ``ExecutionCon`` to the stack, with ``isStatic``
## indicating whether it's a static context.
c.executionCons.add ExecutionCon(inStaticContext: isStatic)
proc pushExecCon*(c: PContext, flags: set[ExecutionConFlag]) {.inline.} =
## Pushes a new ``ExecutionCon`` to the stack.
c.executionCons.add ExecutionCon(flags: flags)

proc popExecCon*(c: PContext) {.inline.} =
## Pops the top-most ``ExecutionCon`` from the stack.
Expand All @@ -780,7 +778,7 @@ proc pushProcCon*(c: PContext, owner: PSym) {.inline.} =
c.p = x

# a procedure always starts a new execution context
pushExecCon(c, isStatic=false)
pushExecCon(c, {ecfExplicit})

proc popProcCon*(c: PContext) {.inline.} =
popExecCon(c)
Expand Down Expand Up @@ -1087,7 +1085,7 @@ proc isTopLevelInsideDeclaration*(c: PContext, sym: PSym): bool {.inline.} =
proc inCompileTimeOnlyContext*(c: PContext): bool =
## Returns whether the current analysis happens for code that can only run
## at compile-time
c.execCon.inStaticContext or sfCompileTime in c.p.owner.flags
ecfStatic in c.execCon.flags or sfCompileTime in c.p.owner.flags

proc pushCaseContext*(c: PContext, caseNode: PNode) =
c.execCon.caseContext.add((caseNode, 0))
Expand Down
21 changes: 19 additions & 2 deletions compiler/sem/semexprs.nim
Original file line number Diff line number Diff line change
Expand Up @@ -1154,7 +1154,7 @@ proc evalAtCompileTime(c: PContext, n: PNode): PNode =

# only attempt to fold the expression if doing so doesn't affect
# compile-time state
if not c.execCon.inStaticContext or sfNoSideEffect in callee.flags:
if ecfStatic notin c.execCon.flags or sfNoSideEffect in callee.flags:
if sfCompileTime in callee.flags:
result = evalStaticExpr(c.module, c.idgen, c.graph, call, c.p.owner)
result =
Expand All @@ -1177,7 +1177,7 @@ proc semStaticExpr(c: PContext, n: PNode): PNode =
## at compile-time, producing either the AST representation of the resulting
## value or an error.
openScope(c)
pushExecCon(c, isStatic=true)
pushExecCon(c, {ecfStatic, ecfExplicit})
var a = semExprWithType(c, n)
popExecCon(c)
closeScope(c)
Expand Down Expand Up @@ -2799,13 +2799,30 @@ proc semCompiles(c: PContext, n: PNode, flags: TExprFlags): PNode =
# defensively, as inclusion of nkError nodes may mutate the original AST
# that was passed in via the compiles call.

# the AST is analyzed as if appearing within the closest explicit execution
# context
var saveStack: seq[ExecutionCon]
block:
# backup the frames that are implicit
var i = c.executionCons.high
while i >= 0 and ecfExplicit notin c.executionCons[i].flags:
saveStack.add(move c.executionCons[i])
dec i

c.executionCons.setLen(i + 1)

let
exprVal = tryExpr(c, n[1], flags)
didCompile = exprVal != nil and exprVal.kind != nkError
## this is the one place where we don't propagate nkError, wrapping the
## parent because this is a `compiles` call and should not leak across
## the AST boundary

# restore the original execution context stack. The items were saved in
# reverse, so we need to restore them in reverse order
for i in countdown(saveStack.high, 0):
c.executionCons.add(move saveStack[i])

result = newIntNode(nkIntLit, ord(didCompile))
result.info = n.info
result.typ = getSysType(c.graph, n.info, tyBool)
Expand Down
4 changes: 2 additions & 2 deletions compiler/sem/semstmts.nim
Original file line number Diff line number Diff line change
Expand Up @@ -1056,7 +1056,7 @@ proc semNormalizedConst(c: PContext, n: PNode): PNode =
block:
# don't evaluate here since the type compatibility check below may add
# a converter
pushExecCon(c, isStatic=true)
pushExecCon(c, {ecfStatic})
let temp = semExprWithType(c, defInitPart)
popExecCon(c)

Expand Down Expand Up @@ -3253,7 +3253,7 @@ proc semStaticStmt(c: PContext, n: PNode): PNode =
#echo "semStaticStmt"
#writeStackTrace()
openScope(c)
pushExecCon(c, isStatic=true)
pushExecCon(c, {ecfStatic, ecfExplicit})
var a = semStmt(c, n[0], {})
popExecCon(c)
closeScope(c)
Expand Down
43 changes: 43 additions & 0 deletions tests/lang_exprs/compiles/tcompiles_context.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
discard """
description: '''
Ensure that the code passed to a ``compiles`` is compiled as if appearing
within the nearest explicit execution context
'''
"""

# the ``break`` statement passed to the ``compiles`` procedure is
# wrapped in an ``if true:``, otherwise the code would not be
# syntactically valid

proc f(x: static bool): bool =
x

# ``when`` condition
block label:
# the ``when`` condition opens a new context, but it's implicit
when compiles(if true: break label):
discard "all fine"
else:
{.error: "`compiles` failed".}

# ``compiles`` within nested implicit contexts
block label:
# each initializer expression happens within a new context, but they're
# implicit
const a = (const b = compiles(if true: break label); b)
doAssert a

# ``compiles`` within implicit contexts created for arguments
block label:
# the separate context the argument expression is anaylzed within
# is *implicit*, so the ``break label`` statement is analyzed as if
# within the context the ``block label`` is also part of
doAssert f(compiles(if true: break label))

block label:
static:
# a ``static`` block opens an *explicit* context
doAssert not compiles(if true: break label)

# a coercion to ``static`` also opens an *explicit* context
doAssert static(not compiles(if true: break label))

0 comments on commit 513aed4

Please sign in to comment.