Rework exception handling in the VM (#10544)

* Rework exception handling in the VM

Make the safepoint handling more precise and less forgiving.
The new code is clearer and more commented.
Perform cleanup on `return`.
The no-exception-thrown case in a try block should be slightly faster
since we don't parse the whole set of exceptions every time.
More tests.

* Fix silly error that broke a few tests

* Testament doesn't like files having the same name

* Remove test case that failed compilation to js
This commit is contained in:
LemonBoy 2019-02-08 11:57:47 +01:00 • committed by Andreas Rumpf
commit 710cfcecd3
4 changed files with 238 additions and 106 deletions

View file

@ -23,7 +23,7 @@ from evaltempl import evalTemplate
from modulegraphs import ModuleGraph, PPassContext from modulegraphs import ModuleGraph, PPassContext
const const
traceCode = debugEchoCode traceCode = defined(nimVMDebug)
when hasFFI: when hasFFI:
import evalffi import evalffi
@ -259,64 +259,101 @@ proc pushSafePoint(f: PStackFrame; pc: int) =
f.safePoints.add(pc) f.safePoints.add(pc)
proc popSafePoint(f: PStackFrame) = proc popSafePoint(f: PStackFrame) =
# XXX this needs a proper fix! discard f.safePoints.pop()
if f.safePoints.len > 0:
discard f.safePoints.pop()
proc cleanUpOnException(c: PCtx; tos: PStackFrame): type
tuple[pc: int, f: PStackFrame] = ExceptionGoto = enum
let raisedType = c.currentExceptionA.typ.skipTypes(abstractPtrs) ExceptionGotoHandler,
var f = tos ExceptionGotoFinally,
while true: ExceptionGotoUnhandled
while f.safePoints.len == 0:
f = f.next
if f.isNil: return (-1, nil)
var pc2 = f.safePoints[f.safePoints.high]
var nextExceptOrFinally = -1 proc findExceptionHandler(c: PCtx, f: PStackFrame, exc: PNode):
if c.code[pc2].opcode == opcExcept: tuple[why: ExceptionGoto, where: int] =
nextExceptOrFinally = pc2 + c.code[pc2].regBx - wordExcess let raisedType = exc.typ.skipTypes(abstractPtrs)
inc pc2
while c.code[pc2].opcode == opcExcept:
let excIndex = c.code[pc2].regBx-wordExcess
let exceptType = if excIndex > 0: c.types[excIndex].skipTypes(
abstractPtrs)
else: nil
#echo typeToString(exceptType), " ", typeToString(raisedType)
if exceptType.isNil or inheritanceDiff(raisedType, exceptType) <= 0:
# mark exception as handled but keep it in B for
# the getCurrentException() builtin:
c.currentExceptionB = c.currentExceptionA
c.currentExceptionA = nil
# execute the corresponding handler:
while c.code[pc2].opcode == opcExcept: inc pc2
discard f.safePoints.pop
return (pc2, f)
inc pc2
if c.code[pc2].opcode != opcExcept and nextExceptOrFinally >= 0:
# we're at the end of the *except list*, but maybe there is another
# *except branch*?
pc2 = nextExceptOrFinally+1
if c.code[pc2].opcode == opcExcept:
nextExceptOrFinally = pc2 + c.code[pc2].regBx - wordExcess
if nextExceptOrFinally >= 0: while f.safePoints.len > 0:
pc2 = nextExceptOrFinally var pc = f.safePoints.pop()
if c.code[pc2].opcode == opcFinally:
# execute the corresponding handler, but don't quit walking the stack: var matched = false
discard f.safePoints.pop var pcEndExcept = pc
return (pc2+1, f)
# not the right one: # Scan the chain of exceptions starting at pc.
discard f.safePoints.pop # The structure is the following:
# pc - opcExcept, <end of this block>
# - opcExcept, <pattern1>
# - opcExcept, <pattern2>
# ...
# - opcExcept, <patternN>
# - Exception handler body
# - ... more opcExcept blocks may follow
# - ... an optional opcFinally block may follow
#
# Note that the exception handler body already contains a jump to the
# finally block or, if that's not present, to the point where the execution
# should continue.
# Also note that opcFinally blocks are the last in the chain.
while c.code[pc].opcode == opcExcept:
# Where this Except block ends
pcEndExcept = pc + c.code[pc].regBx - wordExcess
inc pc
# A series of opcExcept follows for each exception type matched
while c.code[pc].opcode == opcExcept:
let excIndex = c.code[pc].regBx - wordExcess
let exceptType =
if excIndex > 0: c.types[excIndex].skipTypes(abstractPtrs)
else: nil
# echo typeToString(exceptType), " ", typeToString(raisedType)
# Determine if the exception type matches the pattern
if exceptType.isNil or inheritanceDiff(raisedType, exceptType) <= 0:
matched = true
break
inc pc
# Skip any further ``except`` pattern and find the first instruction of
# the handler body
while c.code[pc].opcode == opcExcept:
inc pc
if matched:
break
# If no handler in this chain is able to catch this exception we check if
# the "parent" chains are able to. If this chain ends with a `finally`
# block we must execute it before continuing.
pc = pcEndExcept
# Where the handler body starts
let pcBody = pc
if matched:
return (ExceptionGotoHandler, pcBody)
elif c.code[pc].opcode == opcFinally:
# The +1 here is here because we don't want to execute it since we've
# already pop'd this statepoint from the stack.
return (ExceptionGotoFinally, pc + 1)
return (ExceptionGotoUnhandled, 0)
proc cleanUpOnReturn(c: PCtx; f: PStackFrame): int = proc cleanUpOnReturn(c: PCtx; f: PStackFrame): int =
for s in f.safePoints: # Walk up the chain of safepoints and return the PC of the first `finally`
var pc = s # block we find or -1 if no such block is found.
# Note that the safepoint is removed once the function returns!
result = -1
# Traverse the stack starting from the end in order to execute the blocks in
# the inteded order
for i in 1 .. f.safePoints.len:
var pc = f.safePoints[^i]
# Skip the `except` blocks
while c.code[pc].opcode == opcExcept: while c.code[pc].opcode == opcExcept:
pc = pc + c.code[pc].regBx - wordExcess pc += c.code[pc].regBx - wordExcess
if c.code[pc].opcode == opcFinally: if c.code[pc].opcode == opcFinally:
return pc discard f.safePoints.pop
return -1 return pc + 1
proc opConv(c: PCtx; dest: var TFullReg, src: TFullReg, desttyp, srctyp: PType): bool = proc opConv(c: PCtx; dest: var TFullReg, src: TFullReg, desttyp, srctyp: PType): bool =
if desttyp.kind == tyString: if desttyp.kind == tyString:
@ -449,6 +486,9 @@ const
proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg = proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg =
var pc = start var pc = start
var tos = tos var tos = tos
# Used to keep track of where the execution is resumed.
var savedPC = -1
var savedFrame: PStackFrame
var regs: seq[TFullReg] # alias to tos.slots for performance var regs: seq[TFullReg] # alias to tos.slots for performance
move(regs, tos.slots) move(regs, tos.slots)
#echo "NEW RUN ------------------------" #echo "NEW RUN ------------------------"
@ -456,27 +496,31 @@ proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg =
#{.computedGoto.} #{.computedGoto.}
let instr = c.code[pc] let instr = c.code[pc]
let ra = instr.regA let ra = instr.regA
#if c.traceActive:
when traceCode: when traceCode:
echo "PC ", pc, " ", c.code[pc].opcode, " ra ", ra, " rb ", instr.regB, " rc ", instr.regC echo "PC ", pc, " ", c.code[pc].opcode, " ra ", ra, " rb ", instr.regB, " rc ", instr.regC
# message(c.config, c.debug[pc], warnUser, "Trace")
case instr.opcode case instr.opcode
of opcEof: return regs[ra] of opcEof: return regs[ra]
of opcRet: of opcRet:
# XXX perform any cleanup actions let newPc = c.cleanUpOnReturn(tos)
pc = tos.comesFrom # Perform any cleanup action before returning
tos = tos.next if newPc < 0:
let retVal = regs[0] pc = tos.comesFrom
if tos.isNil: tos = tos.next
#echo "RET ", retVal.rendertree let retVal = regs[0]
return retVal if tos.isNil:
return retVal
move(regs, tos.slots) move(regs, tos.slots)
assert c.code[pc].opcode in {opcIndCall, opcIndCallAsgn} assert c.code[pc].opcode in {opcIndCall, opcIndCallAsgn}
if c.code[pc].opcode == opcIndCallAsgn: if c.code[pc].opcode == opcIndCallAsgn:
regs[c.code[pc].regA] = retVal regs[c.code[pc].regA] = retVal
#echo "RET2 ", retVal.rendertree, " ", c.code[pc].regA else:
savedPC = pc
savedFrame = tos
# The -1 is needed because at the end of the loop we increment `pc`
pc = newPc - 1
of opcYldYoid: assert false of opcYldYoid: assert false
of opcYldVal: assert false of opcYldVal: assert false
of opcAsgnInt: of opcAsgnInt:
@ -1025,7 +1069,7 @@ proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg =
# it's a callback: # it's a callback:
c.callbacks[-prc.offset-2].value( c.callbacks[-prc.offset-2].value(
VmArgs(ra: ra, rb: rb, rc: rc, slots: cast[pointer](regs), VmArgs(ra: ra, rb: rb, rc: rc, slots: cast[pointer](regs),
currentException: c.currentExceptionB, currentException: c.currentExceptionA,
currentLineInfo: c.debug[pc])) currentLineInfo: c.debug[pc]))
elif sfImportc in prc.flags: elif sfImportc in prc.flags:
if allowFFI notin c.features: if allowFFI notin c.features:
@ -1118,44 +1162,55 @@ proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg =
tos.pushSafePoint(pc + rbx) tos.pushSafePoint(pc + rbx)
assert c.code[pc+rbx].opcode in {opcExcept, opcFinally} assert c.code[pc+rbx].opcode in {opcExcept, opcFinally}
of opcExcept: of opcExcept:
# just skip it; it's followed by a jump; # This opcode is never executed, it only holds informations for the
# we'll execute in the 'raise' handler # exception handling routines.
let rbx = instr.regBx - wordExcess - 1 # -1 for the following 'inc pc' doAssert(false)
inc pc, rbx
while c.code[pc+1].opcode == opcExcept:
let rbx = c.code[pc+1].regBx - wordExcess - 1
inc pc, rbx
#assert c.code[pc+1].opcode in {opcExcept, opcFinally}
if c.code[pc+1].opcode != opcFinally:
# in an except handler there is no active safe point for the 'try':
tos.popSafePoint()
of opcFinally: of opcFinally:
# just skip it; it's followed by the code we need to execute anyway # Pop the last safepoint introduced by a opcTry. This opcode is only
# executed _iff_ no exception was raised in the body of the `try`
# statement hence the need to pop the safepoint here.
doAssert(savedPC < 0)
tos.popSafePoint() tos.popSafePoint()
of opcFinallyEnd: of opcFinallyEnd:
if c.currentExceptionA != nil: # The control flow may not resume at the next instruction since we may be
# we are in a cleanup run: # raising an exception or performing a cleanup.
let (newPc, newTos) = cleanUpOnException(c, tos) if not savedPC < 0:
if newPc-1 < 0: pc = savedPC - 1
bailOut(c, tos) savedPC = -1
return if tos != savedFrame:
pc = newPc-1 tos = savedFrame
if tos != newTos:
tos = newTos
move(regs, tos.slots) move(regs, tos.slots)
of opcRaise: of opcRaise:
let raised = regs[ra].node let raised = regs[ra].node
c.currentExceptionA = raised c.currentExceptionA = raised
c.exceptionInstr = pc c.exceptionInstr = pc
let (newPc, newTos) = cleanUpOnException(c, tos)
# -1 because of the following 'inc' var frame = tos
if newPc-1 < 0: var jumpTo = findExceptionHandler(c, frame, raised)
while jumpTo.why == ExceptionGotoUnhandled and not frame.next.isNil:
frame = frame.next
jumpTo = findExceptionHandler(c, frame, raised)
case jumpTo.why:
of ExceptionGotoHandler:
# Jump to the handler, do nothing when the `finally` block ends.
savedPC = -1
pc = jumpTo.where - 1
if tos != frame:
tos = frame
move(regs, tos.slots)
of ExceptionGotoFinally:
# Jump to the `finally` block first then re-jump here to continue the
# traversal of the exception chain
savedPC = pc
savedFrame = tos
pc = jumpTo.where - 1
if tos != frame:
tos = frame
move(regs, tos.slots)
of ExceptionGotoUnhandled:
# Nobody handled this exception, error out.
bailOut(c, tos) bailOut(c, tos)
return
pc = newPc-1
if tos != newTos:
tos = newTos
move(regs, tos.slots)
of opcNew: of opcNew:
ensureKind(rkNode) ensureKind(rkNode)
let typ = c.types[instr.regBx - wordExcess] let typ = c.types[instr.regBx - wordExcess]
@ -1295,7 +1350,7 @@ proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg =
idx = int(regs[rb+rc-1].intVal) idx = int(regs[rb+rc-1].intVal)
callback = c.callbacks[idx].value callback = c.callbacks[idx].value
args = VmArgs(ra: ra, rb: rb, rc: rc, slots: cast[pointer](regs), args = VmArgs(ra: ra, rb: rb, rc: rc, slots: cast[pointer](regs),
currentException: c.currentExceptionB, currentException: c.currentExceptionA,
currentLineInfo: c.debug[pc]) currentLineInfo: c.debug[pc])
callback(args) callback(args)
regs[ra].node.flags.incl nfIsRef regs[ra].node.flags.incl nfIsRef

View file

@ -83,9 +83,12 @@ proc codeListing(c: PCtx, result: var string, start=0; last = -1) =
elif opc < firstABxInstr: elif opc < firstABxInstr:
result.addf("\t$#\tr$#, r$#, r$#", opc.toStr, x.regA, result.addf("\t$#\tr$#, r$#, r$#", opc.toStr, x.regA,
x.regB, x.regC) x.regB, x.regC)
elif opc in relativeJumps: elif opc in relativeJumps + {opcTry}:
result.addf("\t$#\tr$#, L$#", opc.toStr, x.regA, result.addf("\t$#\tr$#, L$#", opc.toStr, x.regA,
i+x.regBx-wordExcess) i+x.regBx-wordExcess)
elif opc in {opcExcept}:
let idx = x.regBx-wordExcess
result.addf("\t$#\t$#, $#", opc.toStr, x.regA, $idx)
elif opc in {opcLdConst, opcAsgnConst}: elif opc in {opcLdConst, opcAsgnConst}:
let idx = x.regBx-wordExcess let idx = x.regBx-wordExcess
result.addf("\t$#\tr$#, $# ($#)", opc.toStr, x.regA, result.addf("\t$#\tr$#, $# ($#)", opc.toStr, x.regA,
@ -480,10 +483,13 @@ proc genType(c: PCtx; typ: PType): int =
proc genTry(c: PCtx; n: PNode; dest: var TDest) = proc genTry(c: PCtx; n: PNode; dest: var TDest) =
if dest < 0 and not isEmptyType(n.typ): dest = getTemp(c, n.typ) if dest < 0 and not isEmptyType(n.typ): dest = getTemp(c, n.typ)
var endings: seq[TPosition] = @[] var endings: seq[TPosition] = @[]
let elsePos = c.xjmp(n, opcTry, 0) let ehPos = c.xjmp(n, opcTry, 0)
c.gen(n.sons[0], dest) c.gen(n.sons[0], dest)
c.clearDest(n, dest) c.clearDest(n, dest)
c.patch(elsePos) # Add a jump past the exception handling code
endings.add(c.xjmp(n, opcJmp, 0))
# This signals where the body ends and where the exception handling begins
c.patch(ehPos)
for i in 1 ..< n.len: for i in 1 ..< n.len:
let it = n.sons[i] let it = n.sons[i]
if it.kind != nkFinally: if it.kind != nkFinally:
@ -499,14 +505,14 @@ proc genTry(c: PCtx; n: PNode; dest: var TDest) =
c.gABx(it, opcExcept, 0, 0) c.gABx(it, opcExcept, 0, 0)
c.gen(it.lastSon, dest) c.gen(it.lastSon, dest)
c.clearDest(n, dest) c.clearDest(n, dest)
if i < sonsLen(n)-1: if i < sonsLen(n):
endings.add(c.xjmp(it, opcJmp, 0)) endings.add(c.xjmp(it, opcJmp, 0))
c.patch(endExcept) c.patch(endExcept)
for endPos in endings: c.patch(endPos)
let fin = lastSon(n) let fin = lastSon(n)
# we always generate an 'opcFinally' as that pops the safepoint # we always generate an 'opcFinally' as that pops the safepoint
# from the stack # from the stack if no exception is raised in the body.
c.gABx(fin, opcFinally, 0, 0) c.gABx(fin, opcFinally, 0, 0)
for endPos in endings: c.patch(endPos)
if fin.kind == nkFinally: if fin.kind == nkFinally:
c.gen(fin.sons[0]) c.gen(fin.sons[0])
c.clearDest(n, dest) c.clearDest(n, dest)
@ -2214,9 +2220,9 @@ proc genProc(c: PCtx; s: PSym): int =
c.gABC(body, opcEof, eofInstr.regA) c.gABC(body, opcEof, eofInstr.regA)
c.optimizeJumps(result) c.optimizeJumps(result)
s.offset = c.prc.maxSlots s.offset = c.prc.maxSlots
#if s.name.s == "calc": # if s.name.s == "fun1":
# echo renderTree(body) # echo renderTree(body)
# c.echoCode(result) # c.echoCode(result)
c.prc = oldPrc c.prc = oldPrc
else: else:
c.prc.maxSlots = s.offset c.prc.maxSlots = s.offset

View file

@ -74,3 +74,54 @@ block: #10417
moo() moo()
doAssert(bar == 1) doAssert(bar == 1)
# Make sure the VM handles the exceptions correctly
block:
proc fun1(): seq[int] =
try:
try:
raise newException(ValueError, "xx")
except:
doAssert("xx" == getCurrentExceptionMsg())
raise newException(KeyError, "yy")
except:
doAssert("yy" == getCurrentExceptionMsg())
result.add(1212)
try:
try:
raise newException(AssertionError, "a")
finally:
result.add(42)
except AssertionError:
result.add(99)
finally:
result.add(10)
result.add(4)
result.add(0)
try:
result.add(1)
except KeyError:
result.add(-1)
except ValueError:
result.add(-1)
except IndexError:
result.add(2)
except:
result.add(3)
try:
try:
result.add(1)
return
except:
result.add(-1)
finally:
result.add(2)
except KeyError:
doAssert(false)
finally:
result.add(3)
let x1 = fun1()
const x2 = fun1()
doAssert(x1 == x2)

View file

@ -49,3 +49,23 @@ static:
echo "caught Defect" echo "caught Defect"
except ValueError: except ValueError:
echo "caught ValueError" echo "caught ValueError"
# bug #10538
block:
proc fun1(): seq[int] =
try:
try:
result.add(1)
return
except:
result.add(-1)
finally:
result.add(2)
finally:
result.add(3)
result.add(4)
let x1 = fun1()
const x2 = fun1()
doAssert(x1 == x2)