Field checks for everybody (#8957)

* Field checks for JS backend

* Clean nkCall nodes with no arguments

Generating a nkEmpty in place of no arguments makes no sense form the
AST point of view and also trips up the VM codegen.

* Field checks for VM backend

* Test case for #6612

This patchset fixes #6612

* Add test case for LHS double evaluation

* Prevent LHS double-eval for JS backend

* Prevent double evaluation in VM backend
This commit is contained in:
LemonBoy 2018-10-09 19:54:12 +02:00 • committed by Andreas Rumpf
commit ee14ace5d3
10 changed files with 223 additions and 43 deletions

View file

@ -210,7 +210,7 @@ proc newUnrollFinallyAccess(ctx: var Ctx, info: TLineInfo): PNode =
proc newCurExcAccess(ctx: var Ctx): PNode = proc newCurExcAccess(ctx: var Ctx): PNode =
if ctx.curExcSym.isNil: if ctx.curExcSym.isNil:
ctx.curExcSym = ctx.newEnvVar(":curExc", ctx.g.callCodegenProc("getCurrentException", ctx.g.emptyNode).typ) ctx.curExcSym = ctx.newEnvVar(":curExc", ctx.g.callCodegenProc("getCurrentException").typ)
ctx.newEnvVarAccess(ctx.curExcSym) ctx.newEnvVarAccess(ctx.curExcSym)
proc newState(ctx: var Ctx, n, gotoOut: PNode): int = proc newState(ctx: var Ctx, n, gotoOut: PNode): int =
@ -324,7 +324,7 @@ proc collectExceptState(ctx: var Ctx, n: PNode): PNode {.inline.} =
assert(c[i].kind == nkType) assert(c[i].kind == nkType)
let nextCond = newTree(nkCall, let nextCond = newTree(nkCall,
newSymNode(g.getSysMagic(c.info, "of", mOf)), newSymNode(g.getSysMagic(c.info, "of", mOf)),
g.callCodegenProc("getCurrentException", ctx.g.emptyNode), g.callCodegenProc("getCurrentException"),
c[i]) c[i])
nextCond.typ = ctx.g.getSysType(c.info, tyBool) nextCond.typ = ctx.g.getSysType(c.info, tyBool)
nextCond.info = c.info nextCond.info = c.info
@ -364,7 +364,7 @@ proc addElseToExcept(ctx: var Ctx, n: PNode) =
block: # :curExc = getCurrentException() block: # :curExc = getCurrentException()
branchBody.add(newTree(nkAsgn, branchBody.add(newTree(nkAsgn,
ctx.newCurExcAccess(), ctx.newCurExcAccess(),
ctx.g.callCodegenProc("getCurrentException", ctx.g.emptyNode))) ctx.g.callCodegenProc("getCurrentException")))
block: # goto nearestFinally block: # goto nearestFinally
branchBody.add(newTree(nkGotoState, ctx.g.newIntLit(n.info, ctx.nearestFinally))) branchBody.add(newTree(nkGotoState, ctx.g.newIntLit(n.info, ctx.nearestFinally)))
@ -803,7 +803,7 @@ proc newEndFinallyNode(ctx: var Ctx, info: TLineInfo): PNode =
let branch = newTree(nkElifBranch, cmp, retStmt) let branch = newTree(nkElifBranch, cmp, retStmt)
# The C++ backend requires `getCurrentException` here. # The C++ backend requires `getCurrentException` here.
let raiseStmt = newTree(nkRaiseStmt, ctx.g.callCodegenProc("getCurrentException", ctx.g.emptyNode)) let raiseStmt = newTree(nkRaiseStmt, ctx.g.callCodegenProc("getCurrentException"))
raiseStmt.info = info raiseStmt.info = info
let elseBranch = newTree(nkElse, raiseStmt) let elseBranch = newTree(nkElse, raiseStmt)
@ -1204,7 +1204,7 @@ proc newCatchBody(ctx: var Ctx, info: TLineInfo): PNode {.inline.} =
block: block:
result.add(newTree(nkAsgn, result.add(newTree(nkAsgn,
ctx.newCurExcAccess(), ctx.newCurExcAccess(),
ctx.g.callCodegenProc("getCurrentException", ctx.g.emptyNode))) ctx.g.callCodegenProc("getCurrentException")))
proc wrapIntoTryExcept(ctx: var Ctx, n: PNode): PNode {.inline.} = proc wrapIntoTryExcept(ctx: var Ctx, n: PNode): PNode {.inline.} =
let setupExc = newTree(nkCall, let setupExc = newTree(nkCall,

View file

@ -289,7 +289,7 @@ proc destructiveMoveSink(n: PNode; c: var Con): PNode =
result = newNodeIT(nkStmtListExpr, n.info, n.typ) result = newNodeIT(nkStmtListExpr, n.info, n.typ)
let bit = newSymNode dropBit(c, n.sym) let bit = newSymNode dropBit(c, n.sym)
if optMoveCheck in c.owner.options: if optMoveCheck in c.owner.options:
result.add callCodegenProc(c.graph, "chckMove", bit) result.add callCodegenProc(c.graph, "chckMove", bit.info, bit)
result.add newTree(nkAsgn, bit, result.add newTree(nkAsgn, bit,
newIntTypeNode(nkIntLit, 0, getSysType(c.graph, n.info, tyBool))) newIntTypeNode(nkIntLit, 0, getSysType(c.graph, n.info, tyBool)))
result.add n result.add n

View file

@ -986,13 +986,50 @@ proc genFieldAccess(p: PProc, n: PNode, r: var TCompRes) =
proc genAddr(p: PProc, n: PNode, r: var TCompRes) proc genAddr(p: PProc, n: PNode, r: var TCompRes)
proc genCheckedFieldAddr(p: PProc, n: PNode, r: var TCompRes) = proc genCheckedFieldOp(p: PProc, n: PNode, addrTyp: PType, r: var TCompRes) =
let m = if n.kind == nkHiddenAddr: n.sons[0] else: n internalAssert p.config, n.kind == nkCheckedFieldExpr
internalAssert p.config, m.kind == nkCheckedFieldExpr # nkDotExpr to access the requested field
genAddr(p, m, r) # XXX let accessExpr = n[0]
# nkCall to check if the discriminant is valid
var checkExpr = n[1]
proc genCheckedFieldAccess(p: PProc, n: PNode, r: var TCompRes) = let negCheck = checkExpr[0].sym.magic == mNot
genFieldAccess(p, n.sons[0], r) # XXX if negCheck:
checkExpr = checkExpr[^1]
# Field symbol
var field = accessExpr[1].sym
internalAssert p.config, field.kind == skField
if field.loc.r == nil: field.loc.r = mangleName(p.module, field)
# Discriminant symbol
let disc = checkExpr[2].sym
internalAssert p.config, disc.kind == skField
if disc.loc.r == nil: disc.loc.r = mangleName(p.module, disc)
var setx: TCompRes
gen(p, checkExpr[1], setx)
var obj: TCompRes
gen(p, accessExpr[0], obj)
# Avoid evaluating the LHS twice (one to read the discriminant and one to read
# the field)
let tmp = p.getTemp()
lineF(p, "var $1 = $2;$n", tmp, obj.res)
useMagic(p, "raiseFieldError")
useMagic(p, "makeNimstrLit")
lineF(p, "if ($1[$2.$3]$4undefined) { raiseFieldError(makeNimstrLit($5)); }$n",
setx.res, tmp, disc.loc.r, if negCheck: ~"!==" else: ~"===",
makeJSString(field.name.s))
if addrTyp != nil and mapType(p, addrTyp) == etyBaseIndex:
r.typ = etyBaseIndex
r.res = makeJSString($field.loc.r)
r.address = tmp
else:
r.typ = etyNone
r.res = "$1.$2" % [tmp, field.loc.r]
r.kind = resExpr
proc genArrayAddr(p: PProc, n: PNode, r: var TCompRes) = proc genArrayAddr(p: PProc, n: PNode, r: var TCompRes) =
var var
@ -1071,7 +1108,7 @@ proc genAddr(p: PProc, n: PNode, r: var TCompRes) =
#internalError(p.config, n.info, "genAddr: 4 " & renderTree(n)) #internalError(p.config, n.info, "genAddr: 4 " & renderTree(n))
else: internalError(p.config, n.info, "genAddr: 2") else: internalError(p.config, n.info, "genAddr: 2")
of nkCheckedFieldExpr: of nkCheckedFieldExpr:
genCheckedFieldAddr(p, n, r) genCheckedFieldOp(p, n[0], n.typ, r)
of nkDotExpr: of nkDotExpr:
if mapType(p, n.typ) == etyBaseIndex: if mapType(p, n.typ) == etyBaseIndex:
genFieldAddr(p, n.sons[0], r) genFieldAddr(p, n.sons[0], r)
@ -2140,7 +2177,7 @@ proc gen(p: PProc, n: PNode, r: var TCompRes) =
of nkDerefExpr, nkHiddenDeref: genDeref(p, n, r) of nkDerefExpr, nkHiddenDeref: genDeref(p, n, r)
of nkBracketExpr: genArrayAccess(p, n, r) of nkBracketExpr: genArrayAccess(p, n, r)
of nkDotExpr: genFieldAccess(p, n, r) of nkDotExpr: genFieldAccess(p, n, r)
of nkCheckedFieldExpr: genCheckedFieldAccess(p, n, r) of nkCheckedFieldExpr: genCheckedFieldOp(p, n, nil, r)
of nkObjDownConv: gen(p, n.sons[0], r) of nkObjDownConv: gen(p, n.sons[0], r)
of nkObjUpConv: upConv(p, n, r) of nkObjUpConv: upConv(p, n, r)
of nkCast: genCast(p, n, r) of nkCast: genCast(p, n, r)

View file

@ -281,15 +281,16 @@ proc genDeref*(n: PNode): PNode =
n.typ.skipTypes(abstractInst).sons[0]) n.typ.skipTypes(abstractInst).sons[0])
result.add n result.add n
proc callCodegenProc*(g: ModuleGraph; name: string, arg1: PNode; proc callCodegenProc*(g: ModuleGraph; name: string;
arg2, arg3, optionalArgs: PNode = nil): PNode = info: TLineInfo = unknownLineInfo();
result = newNodeI(nkCall, arg1.info) arg1, arg2, arg3, optionalArgs: PNode = nil): PNode =
result = newNodeI(nkCall, info)
let sym = magicsys.getCompilerProc(g, name) let sym = magicsys.getCompilerProc(g, name)
if sym == nil: if sym == nil:
localError(g.config, arg1.info, "system module needs: " & name) localError(g.config, info, "system module needs: " & name)
else: else:
result.add newSymNode(sym) result.add newSymNode(sym)
result.add arg1 if arg1 != nil: result.add arg1
if arg2 != nil: result.add arg2 if arg2 != nil: result.add arg2
if arg3 != nil: result.add arg3 if arg3 != nil: result.add arg3
if optionalArgs != nil: if optionalArgs != nil:
@ -410,7 +411,8 @@ proc createWrapperProc(g: ModuleGraph; f: PNode; threadParam, argsParam: PSym;
threadLocalBarrier = addLocalVar(g, varSection2, nil, argsParam.owner, threadLocalBarrier = addLocalVar(g, varSection2, nil, argsParam.owner,
barrier.typ, barrier) barrier.typ, barrier)
body.add varSection2 body.add varSection2
body.add callCodegenProc(g, "barrierEnter", threadLocalBarrier.newSymNode) body.add callCodegenProc(g, "barrierEnter", threadLocalBarrier.info,
threadLocalBarrier.newSymNode)
var threadLocalProm: PSym var threadLocalProm: PSym
if spawnKind == srByVar: if spawnKind == srByVar:
threadLocalProm = addLocalVar(g, varSection, nil, argsParam.owner, fv.typ, fv) threadLocalProm = addLocalVar(g, varSection, nil, argsParam.owner, fv.typ, fv)
@ -425,7 +427,8 @@ proc createWrapperProc(g: ModuleGraph; f: PNode; threadParam, argsParam: PSym;
body.add newAsgnStmt(indirectAccess(threadLocalProm.newSymNode, body.add newAsgnStmt(indirectAccess(threadLocalProm.newSymNode,
"owner", fv.info, g.cache), threadParam.newSymNode) "owner", fv.info, g.cache), threadParam.newSymNode)
body.add callCodegenProc(g, "nimArgsPassingDone", threadParam.newSymNode) body.add callCodegenProc(g, "nimArgsPassingDone", threadParam.info,
threadParam.newSymNode)
if spawnKind == srByVar: if spawnKind == srByVar:
body.add newAsgnStmt(genDeref(threadLocalProm.newSymNode), call) body.add newAsgnStmt(genDeref(threadLocalProm.newSymNode), call)
elif fv != nil: elif fv != nil:
@ -444,11 +447,13 @@ proc createWrapperProc(g: ModuleGraph; f: PNode; threadParam, argsParam: PSym;
if barrier == nil: if barrier == nil:
# by now 'fv' is shared and thus might have beeen overwritten! we need # by now 'fv' is shared and thus might have beeen overwritten! we need
# to use the thread-local view instead: # to use the thread-local view instead:
body.add callCodegenProc(g, "nimFlowVarSignal", threadLocalProm.newSymNode) body.add callCodegenProc(g, "nimFlowVarSignal", threadLocalProm.info,
threadLocalProm.newSymNode)
else: else:
body.add call body.add call
if barrier != nil: if barrier != nil:
body.add callCodegenProc(g, "barrierLeave", threadLocalBarrier.newSymNode) body.add callCodegenProc(g, "barrierLeave", threadLocalBarrier.info,
threadLocalBarrier.newSymNode)
var params = newNodeI(nkFormalParams, f.info) var params = newNodeI(nkFormalParams, f.info)
params.add newNodeI(nkEmpty, f.info) params.add newNodeI(nkEmpty, f.info)
@ -710,7 +715,8 @@ proc wrapProcForSpawn*(g: ModuleGraph; owner: PSym; spawnExpr: PNode; retType: P
# create flowVar: # create flowVar:
result.add newFastAsgnStmt(fvField, callProc(spawnExpr[^1])) result.add newFastAsgnStmt(fvField, callProc(spawnExpr[^1]))
if barrier == nil: if barrier == nil:
result.add callCodegenProc(g, "nimFlowVarCreateSemaphore", fvField) result.add callCodegenProc(g, "nimFlowVarCreateSemaphore", fvField.info,
fvField)
elif spawnKind == srByVar: elif spawnKind == srByVar:
var field = newSym(skField, getIdent(g.cache, "fv"), owner, n.info, g.config.options) var field = newSym(skField, getIdent(g.cache, "fv"), owner, n.info, g.config.options)
@ -723,7 +729,7 @@ proc wrapProcForSpawn*(g: ModuleGraph; owner: PSym; spawnExpr: PNode; retType: P
let wrapper = createWrapperProc(g, fn, threadParam, argsParam, let wrapper = createWrapperProc(g, fn, threadParam, argsParam,
varSection, varInit, call, varSection, varInit, call,
barrierAsExpr, fvAsExpr, spawnKind) barrierAsExpr, fvAsExpr, spawnKind)
result.add callCodegenProc(g, "nimSpawn" & $spawnExpr.len, wrapper.newSymNode, result.add callCodegenProc(g, "nimSpawn" & $spawnExpr.len, wrapper.info,
genAddrOf(scratchObj.newSymNode), nil, spawnExpr) wrapper.newSymNode, genAddrOf(scratchObj.newSymNode), nil, spawnExpr)
if spawnKind == srFlowVar: result.add fvField if spawnKind == srFlowVar: result.add fvField

View file

@ -493,6 +493,6 @@ proc liftParallel*(g: ModuleGraph; owner: PSym; n: PNode): PNode =
result = newNodeI(nkStmtList, n.info) result = newNodeI(nkStmtList, n.info)
generateAliasChecks(a, result) generateAliasChecks(a, result)
result.add varSection result.add varSection
result.add callCodegenProc(g, "openBarrier", barrier) result.add callCodegenProc(g, "openBarrier", barrier.info, barrier)
result.add transformSpawn(g, owner, body, barrier) result.add transformSpawn(g, owner, body, barrier)
result.add callCodegenProc(g, "closeBarrier", barrier) result.add callCodegenProc(g, "closeBarrier", barrier.info, barrier)

View file

@ -739,7 +739,7 @@ proc transformExceptBranch(c: PTransf, n: PNode): PTransNode =
let actions = newTransNode(nkStmtListExpr, n[1], 2) let actions = newTransNode(nkStmtListExpr, n[1], 2)
# Generating `let exc = (excType)(getCurrentException())` # Generating `let exc = (excType)(getCurrentException())`
# -> getCurrentException() # -> getCurrentException()
let excCall = PTransNode(callCodegenProc(c.graph, "getCurrentException", newNodeI(nkEmpty, n.info))) let excCall = PTransNode(callCodegenProc(c.graph, "getCurrentException"))
# -> (excType) # -> (excType)
let convNode = newTransNode(nkHiddenSubConv, n[1].info, 2) let convNode = newTransNode(nkHiddenSubConv, n[1].info, 2)
convNode[0] = PTransNode(newNodeI(nkEmpty, n.info)) convNode[0] = PTransNode(newNodeI(nkEmpty, n.info))

View file

@ -562,6 +562,8 @@ proc genIndex(c: PCtx; n: PNode; arr: PType): TRegister =
else: else:
result = c.genx(n) result = c.genx(n)
proc genCheckedObjAccessAux(c: PCtx; n: PNode; dest: var TDest; flags: TGenFlags)
proc genAsgnPatch(c: PCtx; le: PNode, value: TRegister) = proc genAsgnPatch(c: PCtx; le: PNode, value: TRegister) =
case le.kind case le.kind
of nkBracketExpr: of nkBracketExpr:
@ -570,12 +572,16 @@ proc genAsgnPatch(c: PCtx; le: PNode, value: TRegister) =
c.gABC(le, opcWrArr, dest, idx, value) c.gABC(le, opcWrArr, dest, idx, value)
c.freeTemp(dest) c.freeTemp(dest)
c.freeTemp(idx) c.freeTemp(idx)
of nkDotExpr, nkCheckedFieldExpr: of nkCheckedFieldExpr:
# XXX field checks here var objR: TDest = -1
let left = if le.kind == nkDotExpr: le else: le.sons[0] genCheckedObjAccessAux(c, le, objR, {gfNode})
let dest = c.genx(left.sons[0], {gfNode}) let idx = genField(c, le[0].sons[1])
let idx = genField(c, left.sons[1]) c.gABC(le[0], opcWrObj, objR, idx, value)
c.gABC(left, opcWrObj, dest, idx, value) c.freeTemp(objR)
of nkDotExpr:
let dest = c.genx(le.sons[0], {gfNode})
let idx = genField(c, le.sons[1])
c.gABC(le, opcWrObj, dest, idx, value)
c.freeTemp(dest) c.freeTemp(dest)
of nkDerefExpr, nkHiddenDeref: of nkDerefExpr, nkHiddenDeref:
let dest = c.genx(le.sons[0], {gfNode}) let dest = c.genx(le.sons[0], {gfNode})
@ -1419,13 +1425,19 @@ proc genAsgn(c: PCtx; le, ri: PNode; requiresCopy: bool) =
else: else:
c.preventFalseAlias(le, opcWrArr, dest, idx, tmp) c.preventFalseAlias(le, opcWrArr, dest, idx, tmp)
c.freeTemp(tmp) c.freeTemp(tmp)
of nkDotExpr, nkCheckedFieldExpr: of nkCheckedFieldExpr:
# XXX field checks here var objR: TDest = -1
let left = if le.kind == nkDotExpr: le else: le.sons[0] genCheckedObjAccessAux(c, le, objR, {gfNode})
let dest = c.genx(left.sons[0], {gfNode}) let idx = genField(c, le[0].sons[1])
let idx = genField(c, left.sons[1])
let tmp = c.genx(ri) let tmp = c.genx(ri)
c.preventFalseAlias(left, opcWrObj, dest, idx, tmp) c.preventFalseAlias(le[0], opcWrObj, objR, idx, tmp)
c.freeTemp(tmp)
c.freeTemp(objR)
of nkDotExpr:
let dest = c.genx(le.sons[0], {gfNode})
let idx = genField(c, le.sons[1])
let tmp = c.genx(ri)
c.preventFalseAlias(le, opcWrObj, dest, idx, tmp)
c.freeTemp(tmp) c.freeTemp(tmp)
of nkDerefExpr, nkHiddenDeref: of nkDerefExpr, nkHiddenDeref:
let dest = c.genx(le.sons[0], {gfNode}) let dest = c.genx(le.sons[0], {gfNode})
@ -1561,9 +1573,59 @@ proc genObjAccess(c: PCtx; n: PNode; dest: var TDest; flags: TGenFlags) =
c.gABC(n, opcLdObj, dest, a, b) c.gABC(n, opcLdObj, dest, a, b)
c.freeTemp(a) c.freeTemp(a)
proc genCheckedObjAccessAux(c: PCtx; n: PNode; dest: var TDest; flags: TGenFlags) =
internalAssert c.config, n.kind == nkCheckedFieldExpr
# nkDotExpr to access the requested field
let accessExpr = n[0]
# nkCall to check if the discriminant is valid
var checkExpr = n[1]
let negCheck = checkExpr[0].sym.magic == mNot
if negCheck:
checkExpr = checkExpr[^1]
# Discriminant symbol
let disc = checkExpr[2]
internalAssert c.config, disc.sym.kind == skField
# Load the object in `dest`
c.gen(accessExpr[0], dest, flags)
# Load the discriminant
var discVal = c.getTemp(disc.typ)
c.gABC(n, opcLdObj, discVal, dest, genField(c, disc))
# Check if its value is contained in the supplied set
let setLit = c.genx(checkExpr[1])
var rs = c.getTemp(getSysType(c.graph, n.info, tyBool))
c.gABC(n, opcContainsSet, rs, setLit, discVal)
c.freeTemp(setLit)
# If the check fails let the user know
let L1 = c.xjmp(n, if negCheck: opcFJmp else: opcTJmp, rs)
c.freeTemp(rs)
# Not ideal but will do for the moment
c.gABC(n, opcQuit)
c.patch(L1)
proc genCheckedObjAccess(c: PCtx; n: PNode; dest: var TDest; flags: TGenFlags) = proc genCheckedObjAccess(c: PCtx; n: PNode; dest: var TDest; flags: TGenFlags) =
# XXX implement field checks! var objR: TDest = -1
genObjAccess(c, n.sons[0], dest, flags) genCheckedObjAccessAux(c, n, objR, flags)
let accessExpr = n[0]
# Field symbol
var field = accessExpr[1]
internalAssert c.config, field.sym.kind == skField
# Load the content now
if dest < 0: dest = c.getTemp(n.typ)
let fieldPos = genField(c, field)
if needsRegLoad():
var cc = c.getTemp(accessExpr.typ)
c.gABC(n, opcLdObj, cc, objR, fieldPos)
c.gABC(n, opcNodeToReg, dest, cc)
c.freeTemp(cc)
else:
c.gABC(n, opcLdObj, dest, objR, fieldPos)
c.freeTemp(objR)
proc genArrAccess(c: PCtx; n: PNode; dest: var TDest; flags: TGenFlags) = proc genArrAccess(c: PCtx; n: PNode; dest: var TDest; flags: TGenFlags) =
let arrayType = n.sons[0].typ.skipTypes(abstractVarRange-{tyTypeDesc}).kind let arrayType = n.sons[0].typ.skipTypes(abstractVarRange-{tyTypeDesc}).kind

24
tests/js/t6612.nim Normal file
View file

@ -0,0 +1,24 @@
discard """
action: "run"
"""
proc fillWith(sq: var seq[int], n: int, unused: string) =
sq = @[n]
type
Object = object of RootObj
case hasNums: bool
of true:
numbers: seq[int]
of false:
discard
always: seq[int]
var obj = Object(hasNums: true)
obj.always.fillWith(5, "unused")
doAssert obj.always == @[5]
obj.numbers.fillWith(3, "unused")
doAssert obj.numbers == @[3]
doAssert obj.always == @[5]

48
tests/js/tfieldchecks.nim Normal file
View file

@ -0,0 +1,48 @@
discard """
output: '''
foo
C
3.14
foo
3.14
3.14
'''
"""
type
V = enum
A, B, C
X = object
f0: string
case f1: V
of A: f2: string
of B: discard
of C: f3: float
var obj = X(f0: "foo", f1: C, f3: 3.14)
block:
echo obj.f0
echo obj.f1
doAssertRaises(FieldError): echo obj.f2
echo obj.f3
block:
let a0 = addr(obj.f0)
echo a0[]
# let a1 = unsafeAddr(obj.f1)
# echo a1[]
doAssertRaises(FieldError):
let a2 = addr(obj.f2)
echo a2[]
let a3 = addr(obj.f3)
echo a3[]
# Prevent double evaluation of LHS
block:
var flag = false
proc wrap(x: X): X =
doAssert flag == false
flag = true
result = x
echo wrap(obj).f3

View file

@ -0,0 +1,3 @@
static:
doAssertRaises(ValueError):
raise newException(ValueError, "Yes")