diff --git a/compiler/vm/packed_env.nim b/compiler/vm/packed_env.nim index 9aeff675c88..1fade3859eb 100644 --- a/compiler/vm/packed_env.nim +++ b/compiler/vm/packed_env.nim @@ -158,6 +158,8 @@ type code*: seq[TInstr] debug*: seq[uint32] # Packed version of `TCtx.debug`. Indices into `infos` + ehTable*: seq[HandlerTableEntry] + ehCode*: seq[EhInstr] # rtti related data: nimNodes: seq[PackedNodeLite] @@ -928,6 +930,9 @@ func storeEnv*(enc: var PackedEncoder, dst: var PackedEnv, c: TCtx) = mapList(dst.debug, c.debug, d): dst.infos.getOrIncl(d).uint32 + dst.ehTable = c.ehTable + dst.ehCode = c.ehCode + mapList(dst.files, c.config.m.fileInfos, fi): fi.fullPath.string @@ -960,6 +965,8 @@ proc writeToFile*(p: PackedEnv, file: AbsoluteFile): RodFileError = f.storePrim p.entryPoint f.storeSeq p.code f.storeSeq p.debug + f.storeSeq p.ehTable + f.storeSeq p.ehCode f.storeSection symsSection f.store p.infos @@ -1002,6 +1009,8 @@ proc readFromFile*(p: var PackedEnv, file: AbsoluteFile): RodFileError = f.loadPrim p.entryPoint f.loadSeq p.code f.loadSeq p.debug + f.loadSeq p.ehTable + f.loadSeq p.ehCode f.loadSection symsSection f.load p.infos diff --git a/compiler/vm/vm.nim b/compiler/vm/vm.nim index e2bfb5ec365..7ef25b891e1 100644 --- a/compiler/vm/vm.nim +++ b/compiler/vm/vm.nim @@ -82,6 +82,13 @@ import std/options as stdoptions from std/math import round, copySign type + VmException = object + ## Internal-only. + refVal: HeapSlotHandle + trace: VmRawStackTrace + # XXX: the trace should be stored in the exception object, which would + # also make it accessible to the guest (via ``getStackTrace``) + VmThread* = object ## This is beginning of splitting up ``TCtx``. A ``VmThread`` is ## meant to encapsulate the state that makes up a single execution. This @@ -92,15 +99,11 @@ type loopIterations: int ## the number of remaining jumps backwards - # exception state: currentException: HeapSlotHandle ## the exception ref that's returned when querying the current exception - activeException: HeapSlotHandle - ## the exception that is currently in-flight (i.e. being raised), or - ## nil, if none is in-flight. Note that `activeException` is different - ## from `currentException` - activeExceptionTrace: VmRawStackTrace - ## the stack-trace of where the exception was raised from + ehStack: seq[tuple[ex: VmException, pc: uint32]] + ## the stack of currently executed EH threads. A stack is needed since + ## exceptions can be raised while another exception is in flight YieldReasonKind* = enum yrkDone @@ -139,10 +142,15 @@ type const traceCode = defined(nimVMDebugExecute) + fromEhBit = cast[BiggestInt](0x8000_0000_0000_0000'u64) + ## the presence in a finally's control register signals that the finally + ## was entered as part of exception handling const errIllegalConvFromXtoY = "illegal conversion from '$1' to '$2'" +func `$`(x: VmException) {.error.} + proc createStackTrace*( c: TCtx, thread: VmThread, @@ -241,7 +249,7 @@ template toException(x: DerefFailureCode): untyped = ## `Result` -> exception translation toVmError(x, instLoc()) -proc reportException(c: TCtx; trace: VmRawStackTrace, raised: LocHandle) = +proc reportException(c: TCtx; trace: sink VmRawStackTrace, raised: LocHandle) = ## Reports the exception represented by `raised` by raising a `VmError` let name = $raised.getFieldHandle(1.fpos).deref().strVal @@ -472,141 +480,173 @@ proc regToNode*(c: TCtx, x: TFullReg; typ: PType, info: TLineInfo): PNode = of rkHandle, rkLocation: result = c.deserialize(x.handle, typ, info) of rkNimNode: result = x.nimNode -proc pushSafePoint(f: var TStackFrame; pc: int) = - f.safePoints.add(pc) +# ---- exception handling ---- -proc popSafePoint(f: var TStackFrame) = - discard f.safePoints.pop() +proc findEh(c: TCtx, t: VmThread, at: PrgCtr, frame: int + ): Option[tuple[frame: int, ehInstr: uint32]] = + ## Searches for the EH instruction that is associated with `at`. If none is + ## found on the current stack frame, the caller's call instruction is + ## inspected, then the caller of the caller, etc. + ## + ## On success, the EH instruction position and the stack frame the handler + ## is attached to are returned. + var + pc = at + frame = frame -type - ExceptionGoto = enum - ExceptionGotoHandler, - ExceptionGotoFinally, - ExceptionGotoUnhandled - -proc findExceptionHandler(c: TCtx, f: var TStackFrame, raisedType: PVmType): - tuple[why: ExceptionGoto, where: int] = - - while f.safePoints.len > 0: - var pc = f.safePoints.pop() - - var matched = false - var pcEndExcept = pc - - # Scan the chain of exceptions starting at pc. - # The structure is the following: - # pc - opcExcept, - # - opcExcept, - # - opcExcept, - # ... - # - opcExcept, - # - 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 + while frame >= 0: + let + handlers = t.sframes[frame].eh + offset = uint32(pc - t.sframes[frame].baseOffset) - # 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] - else: nil + # search for the instruction's asscoiated exception handler: + for i in handlers.items: + if c.ehTable[i].offset == offset: + return some (frame, c.ehTable[i].instr) - # echo typeToString(exceptType), " ", typeToString(raisedType) + # no handler was found, try the above frame + pc = t.sframes[frame].comesFrom + dec frame - # Determine if the exception type matches the pattern - if exceptType.isNil or getTypeRel(raisedType, exceptType) in {vtrSub, vtrSame}: - matched = true - break + # no handler exists - inc pc +proc setCurrentException(t: var VmThread, mem: var VmMemoryManager, + ex: HeapSlotHandle) = + ## Sets `ex` as `t`'s current exception, freeing the previous exception, + ## if necessary. + if ex.isNotNil: + mem.heap.heapIncRef(ex) + if t.currentException.isNotNil: + mem.heap.heapDecRef(mem.allocator, t.currentException) - # Skip any further ``except`` pattern and find the first instruction of - # the handler body - while c.code[pc].opcode == opcExcept: - inc pc + t.currentException = ex - if matched: - break +proc decodeControl(x: BiggestInt): tuple[fromEh: bool, val: uint32] = + let x = cast[BiggestUInt](x) + result.fromEh = bool(x shr 63) + result.val = uint32(x) - # 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 +proc runEh(t: var VmThread, c: var TCtx): Result[PrgCtr, VmException] = + ## Executes the active EH thread. Returns either the bytecode position to + ## resume main execution at, or the uncaught exception. + ## + ## This implements the VM-in-VM for executing the EH instructions. + template tos: untyped = + # top-of-stack + t.ehStack[^1] - # Where the handler body starts - let pcBody = pc + while true: + let instr = c.ehCode[tos.pc] + # already move to the next instruction + inc tos.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) + template yieldControl() = + setCurrentException(t, c.memory, tos.ex.refVal) + result.initSuccess(instr.b.PrgCtr) + return - return (ExceptionGotoUnhandled, 0) + case instr.opcode + of ehoExcept, ehoFinally: + # enter exception handler + yieldControl() + of ehoExceptWithFilter: + let + raised = c.heap.tryDeref(tos.ex.refVal, noneType).value() -proc resumeRaise(c: var TCtx, t: var VmThread): PrgCtr = - ## Resume raising the active exception and returns the program counter - ## (adjusted by -1) of the instruction to execute next. The stack is unwound - ## until either an exception handler matching the active exception's type or - ## a finalizer is found. - let - raised = c.heap.tryDeref(t.activeException, noneType).value() - excType = raised.typ + if getTypeRel(raised.typ, c.types[instr.a]) in {vtrSub, vtrSame}: + # success: the filter matches + yieldControl() + else: + discard "not handled, try the next instruction" - var - frame = t.sframes.len - jumpTo = (why: ExceptionGotoUnhandled, where: 0) + of ehoNext: + tos.pc += instr.b - 1 # account for the ``inc`` above + of ehoLeave: + case instr.a + of 0: + # discard the parent thread + swap(tos, t.ehStack[^2]) + t.ehStack.setLen(t.ehStack.len - 1) + of 1: + # discard the parent thread if it's associated with the provided + # ``finally`` + let instr = c.code[instr.b] + vmAssert instr.opcode == opcFinallyEnd + let (fromEh, b) = decodeControl(t.sframes[^1].slots[instr.regA].intVal) + if fromEh: + vmAssert b.int == t.ehStack.high - 1 + swap(tos, t.ehStack[^2]) + t.ehStack.setLen(t.ehStack.len - 1) + else: + vmUnreachable("illegal operand") + of ehoEnd: + # terminate the thread and return the unhandled exception + result.initFailure(move t.ehStack[^1].ex) + t.ehStack.setLen(t.ehStack.len - 1) + break + +proc resumeEh(c: var TCtx, t: var VmThread, + frame: int): Result[PrgCtr, VmException] = + ## Continues raising the exception from the top-most EH thread. If exception + ## handling code is found, unwinds the stack till where the handler is + ## located and returns the program counter where to resume. Otherwise + ## returns the unhandled exception. + var frame = frame + while true: + let r = runEh(t, c) + if r.isOk: + # an exception handler or finalizer is entered. Unwind to the target + # frame: + for j in (frame+1).. the exception is unhandled + return r + else: + # exception was not handled on the current frame, try the frame above + let pos = findEh(c, t, t.sframes[frame].comesFrom, frame) + if pos.isSome: + # EH code exists in a frame above. Run it + frame = pos.get().frame # update to the frame the EH code is part of + t.ehStack.add (r.takeErr(), pos.get().ehInstr) + else: + return r + +proc opRaise(c: var TCtx, t: var VmThread, at: PrgCtr, + ex: sink VmException): Result[PrgCtr, VmException] = + ## Searches for an exception handler for the instruction at `at`. If one is + ## found, the stack is unwound till the frame the handler is in and the + ## position where to resume is returned. If there no handler is found, `ex` + ## is returned. + let pos = findEh(c, t, at, t.sframes.high) + if pos.isSome: + # spawn and run the EH thread: + t.ehStack.add (ex, pos.get().ehInstr) + result = resumeEh(c, t, pos.get().frame) + else: + # no exception handler exists: + result.initFailure(ex) + +proc handle(res: sink Result[PrgCtr, VmException], c: var TCtx, + t: var VmThread): PrgCtr = + ## If `res` is an unhandled exception, reports the exception to the + ## supervisor. Otherwise returns the position where to continue. + if res.isOk: + result = res.take() + if c.code[result].opcode == opcFinally: + # setup the finally section's control register + let reg = c.code[result].regA + t.sframes[^1].slots[reg].initIntReg(fromEhBit or t.ehStack.high, c.memory) + inc result - # search for the first enclosing matching handler or finalizer: - while jumpTo.why == ExceptionGotoUnhandled and frame > 0: - dec frame - jumpTo = findExceptionHandler(c, t.sframes[frame], excType) - - case jumpTo.why: - of ExceptionGotoHandler, ExceptionGotoFinally: - # unwind till the frame of the handler or finalizer - for i in (frame+1).. 0 and t.sframes[prevFrame].prc != nil: + let genSymOwner = if prevFrame > 0: t.sframes[prevFrame].prc else: c.module @@ -1967,7 +2003,7 @@ proc rawExecute(c: var TCtx, t: var VmThread, pc: var int): YieldReason = # logic as for loops: if newPc < pc: handleJmpBack() #echo "new pc ", newPc, " calling: ", prc.name.s - var newFrame = TStackFrame(prc: prc, comesFrom: pc, savedPC: -1) + var newFrame = TStackFrame(prc: prc, comesFrom: pc) newFrame.slots.newSeq(regCount) if instr.opcode == opcIndCallAsgn: # the destination might be a temporary complex location (`ra` is an @@ -2030,37 +2066,60 @@ proc rawExecute(c: var TCtx, t: var VmThread, pc: var int): YieldReason = let instr2 = c.code[pc] let rbx = instr2.regBx - wordExcess - 1 # -1 for the following 'inc pc' inc pc, rbx - of opcTry: - let rbx = instr.regBx - wordExcess - t.sframes[tos].pushSafePoint(pc + rbx) - assert c.code[pc+rbx].opcode in {opcExcept, opcFinally} - of opcExcept: - # This opcode is never executed, it only holds information for the - # exception handling routines. - doAssert(false) + of opcEnter: + # enter the finalizer to the target but consider finalizers associated + # with the instruction + let target = pc + c.code[pc].regBx - wordExcess + if c.code[target].opcode == opcFinally: + # remember where to jump back when leaving the finally section + let reg = c.code[target].regA + regs[reg].initIntReg(pc + 1, c.memory) + # jump to the instruction following the 'Finally' + pc = target + else: + vmUnreachable("target is not a 'Finally' instruction") + of opcLeave: + case (instr.regC - byteExcess) + of 0: # exit the EH thread + c.heap.heapDecRef(c.allocator, t.ehStack[^1].ex.refVal) + t.ehStack.setLen(t.ehStack.len - 1) + of 1: # exit the finally section + let (fromEh, b) = decodeControl(regs[ra].intVal) + if fromEh: + # only the topmost EH thread can be aborted + vmAssert t.ehStack.high == int(b) + c.heap.heapDecRef(c.allocator, t.ehStack[^1].ex.refVal) + t.ehStack.setLen(t.ehStack.len - 1) + + # the instruction is a no-op when leaving a finally section that wasn't + # entered through an exception + else: + vmUnreachable("invalid operand") + + setCurrentException(t, c.memory): + if t.ehStack.len > 0: + t.ehStack[^1].ex.refVal + else: + HeapSlotHandle(0) + of opcFinally: - # 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(currFrame.savedPC < 0) - t.sframes[tos].popSafePoint() + # when entered by normal control-flow, the corresponding exit will jump + # to the target specified on this instruction + decodeBx(rkInt) + regs[ra].intVal = pc + rbx of opcFinallyEnd: - # The control flow may not resume at the next instruction since we may be - # raising an exception or performing a cleanup. - # XXX: the handling here is wrong in many scenarios, but it works okay - # enough until ``finally`` handling is reworked - if currFrame.savedPC >= 0: - # resume clean-up - pc = currFrame.savedPC - 1 - currFrame.savedPC = -1 - elif t.activeException.isNotNil: - # the finally was entered through a raise -> resume. A return can abort - # unwinding, thus an active exception is only considered when there's - # no cleanup action in progress - pc = resumeRaise(c, t) + # where control-flow resumes depends on how the finally section was + # entered + let (isError, target) = decodeControl(regs[ra].intVal) + if isError: + # continue the EH thread + pc = resumeEh(c, t, t.sframes.high).handle(c, t) - 1 updateRegsAlias() else: - discard "fall through" + # not entered through exceptional control-flow; jump to target stored + # in the register + pc = PrgCtr(target) - 1 + of opcRaise: decodeBImm() discard rb # fix the "unused" warning @@ -2069,30 +2128,15 @@ proc rawExecute(c: var TCtx, t: var VmThread, pc: var int): YieldReason = # `imm == 0` -> raise; `imm == 1` -> reraise current exception let isReraise = imm == 1 - let raisedRef = - if isReraise: - # TODO: must raise a defect when there's no current exception - t.currentException - else: - assert regs[ra].handle.typ.kind == akRef - regs[ra].atomVal.refVal - - # XXX: the exception is never freed right now - - # Keep the exception alive during exception handling - c.heap.heapIncRef(raisedRef) - if not t.currentException.isNil: - c.heap.heapDecRef(c.allocator, t.currentException) - - t.currentException = raisedRef - t.activeException = raisedRef - - # gather the stack-trace for the exception: - # TODO: store the trace in the exception's `trace` field and move this - # setup logic to the ``prepareException`` implementation - block: + var exception: VmException + if isReraise: + # re-raise the current exception + exception = move t.ehStack[^1].ex + # popping the thread is the responsibility of the spawned EH thread + else: + # gather the stack-trace for the exception: var pc = pc - t.activeExceptionTrace.setLen(t.sframes.len) + exception.trace.newSeq(t.sframes.len) for i, it in t.sframes.pairs: let p = @@ -2101,9 +2145,16 @@ proc rawExecute(c: var TCtx, t: var VmThread, pc: var int): YieldReason = else: pc - t.activeExceptionTrace[i] = (it.prc, p) + exception.trace[i] = (it.prc, p) + + # TODO: store the trace in the exception's `trace` field and move this + # setup logic to the ``prepareException`` implementation + + exception.refVal = regs[ra].atomVal.refVal + # keep the exception alive during exception handling: + c.heap.heapIncRef(exception.refVal) - pc = resumeRaise(c, t) + pc = opRaise(c, t, pc, exception).handle(c, t) - 1 updateRegsAlias() of opcNew: let typ = c.types[instr.regBx - wordExcess] @@ -2864,7 +2915,6 @@ proc `=copy`*(x: var VmThread, y: VmThread) {.error.} proc initVmThread*(c: var TCtx, pc: int, frame: sink TStackFrame): VmThread = ## Sets up a ``VmThread`` instance that will start execution at `pc`. ## `frame` provides the initial stack frame. - frame.savedPC = -1 # initialize the field here VmThread(pc: pc, loopIterations: c.config.maxLoopIterationsVM, sframes: @[frame]) diff --git a/compiler/vm/vm_enums.nim b/compiler/vm/vm_enums.nim index e42b8ac955a..087c97776b7 100644 --- a/compiler/vm/vm_enums.nim +++ b/compiler/vm/vm_enums.nim @@ -20,6 +20,8 @@ type opcYldYoid, # yield with no value opcYldVal, # yield with a value + opcSetEh # sets the active instruction-to-EH mappings list + opcAsgnInt, opcAsgnFloat, opcAsgnComplex, @@ -138,8 +140,9 @@ type opcJmp, # jump Bx opcJmpBack, # jump Bx; resulting from a while loop opcBranch, # branch for 'case' - opcTry, - opcExcept, + opcEnter, # jump Bx; target must be a ``opcFinally`` instruction + opcLeave, # if C == 1: abort EH thread associated with finally; + # if C == 0; abort active EH thread opcFinally, opcFinallyEnd, opcNew, @@ -171,4 +174,4 @@ const firstABxInstr* = opcTJmp largeInstrs* = { # instructions which use 2 int32s instead of 1: opcConv, opcObjConv, opcCast, opcNewSeq, opcOf} - relativeJumps* = {opcTJmp, opcFJmp, opcJmp, opcJmpBack} + relativeJumps* = {opcTJmp, opcFJmp, opcJmp, opcJmpBack, opcEnter, opcFinally} diff --git a/compiler/vm/vmbackend.nim b/compiler/vm/vmbackend.nim index 5ee71f988e7..4084bce8c8a 100644 --- a/compiler/vm/vmbackend.nim +++ b/compiler/vm/vmbackend.nim @@ -291,6 +291,8 @@ proc generateCode*(g: ModuleGraph, mlist: sink ModuleList) = env.config = c.gen.config # currently needed by the packer env.code = move c.gen.code env.debug = move c.gen.debug + env.ehTable = move c.gen.ehTable + env.ehCode = move c.gen.ehCode env.functions = move base(c.functions) env.constants = move c.gen.constants env.rtti = move c.gen.rtti diff --git a/compiler/vm/vmdef.nim b/compiler/vm/vmdef.nim index 36d6acc0d9b..fe83da78c4f 100644 --- a/compiler/vm/vmdef.nim +++ b/compiler/vm/vmdef.nim @@ -28,6 +28,7 @@ import ], compiler/utils/[ debugutils, + idioms ], compiler/vm/[ identpatterns @@ -658,10 +659,41 @@ type VmRawStackTrace* = seq[tuple[sym: PSym, pc: PrgCtr]] + HandlerTableEntry* = tuple + offset: uint32 ## instruction offset + instr: uint32 ## position of the EH instruction to spawn a thread with + + EhOpcode* = enum + ehoExcept + ## unconditional exception handler + ehoExceptWithFilter + ## conditionl exception handler. If the exception is a subtype or equal + ## to the specified type, the handler is entered + ehoFinally + ## enter the ``finally`` handler + ehoNext + ## relative jump to another instruction + ehoLeave + ## abort the parent thread + ehoEnd + ## ends the thread without treating the exception as handled + + EhInstr* = tuple + ## Exception handling instruction. 8-byte in size. + opcode: EhOpcode + a: uint16 ## meaning depends on the opcode + b: uint32 ## meaning depends on the opcode + TCtx* = object code*: seq[TInstr] debug*: seq[TLineInfo] # line info for every instruction; kept separate # to not slow down interpretation + ehTable*: seq[HandlerTableEntry] + ## stores the instruction-to-EH mappings. Used to look up the EH + ## instruction to start exception handling with in case of a normal + ## instruction raising + ehCode*: seq[EhInstr] + ## stores the instructions for the exception handling (EH) mechanism globals*: seq[HeapSlotHandle] ## Stores each global's corresponding heap slot constants*: seq[VmConstant] ## constant data complexConsts*: seq[LocHandle] ## complex constants (i.e. everything that @@ -704,15 +736,13 @@ type prc*: PSym # current prc; proc that is evaluated slots*: seq[TFullReg] # parameters passed to the proc + locals; # parameters come first + eh*: HOslice[int] + ## points to the active list of instruction-to-EH mappings + baseOffset*: PrgCtr + ## the instruction that all offsets in the instruction-to-EH list are + ## relative to. Only valid when `eh` is not empty comesFrom*: int - safePoints*: seq[int] # used for exception handling - # XXX 'break' should perform cleanup actions - # What does the C backend do for it? - - savedPC*: PrgCtr ## remembers the program counter of the ``Ret`` - ## instruction during cleanup. -1 indicates that - ## no clean-up is happening ProfileInfo* = object ## Profiler data for a single procedure. diff --git a/compiler/vm/vmerrors.nim b/compiler/vm/vmerrors.nim index 8d585ee2884..a843d7260fd 100644 --- a/compiler/vm/vmerrors.nim +++ b/compiler/vm/vmerrors.nim @@ -28,9 +28,24 @@ func raiseVmError*( event.instLoc = inst raise (ref VmError)(event: event) +func vmUnreachable(msg: sink string, inst: InstantiationInfo + ) {.noinline, noreturn.} = + ## Raises an internal VM error with `msg` as the message. + raiseVmError(VmEvent(kind: vmEvtErrInternal, msg: msg), inst) + # templates below are required as InstantiationInfo isn't captured otherwise template raiseVmError*(event: VmEvent) = ## Raises a `VmError`, using the source code position of the callsite as the ## `inst` value. - raiseVmError(event, instLoc(-2)) \ No newline at end of file + raiseVmError(event, instLoc(-2)) + +template vmUnreachable*(msg: sink string) = + ## Raises an internal VM error with `msg` as the message. + vmUnreachable(msg, instLoc(-2)) + +template vmAssert*(cond: bool) = + ## Raises an ``AssertionDefect`` or VM error depending on the compile- + ## time configuration. + # XXX: implement this properly + assert cond \ No newline at end of file diff --git a/compiler/vm/vmgen.nim b/compiler/vm/vmgen.nim index 2d62f92a68e..28bb258e214 100644 --- a/compiler/vm/vmgen.nim +++ b/compiler/vm/vmgen.nim @@ -125,8 +125,15 @@ type isIndirect: bool ## whether the local uses a handle while its value ## would fit it into a register + BlockKind = enum + bkBlock ## labeled block + bkTry ## the ``try`` and ``except`` clause of a ``finally``-having + ## try statement + bkExcept ## ``except`` clause + bkFinally ## ``finally`` clause + BProc = object - blocks: seq[seq[TPosition]] + blocks: seq[tuple[kind: BlockKind, exits: seq[TPosition], cr: TRegister]] ## for each block, the jump instructions targeting the block's exit. ## These need to be patched once the code for the block is generated sym: PSym @@ -140,6 +147,17 @@ type locals: OrdinalSeq[LocalId, LocalLoc] ## current state of all locals + # exception handling state: + baseOffset: TPosition + ## the bytecode position that instruction-to-EH mappings need to be + ## relative to + hasEh: int + ## > 0 when some form of exception handling exists for the current + ## node + raiseExits: int + ## used to establish a relation between two points during code + ## generation (e.g., "are there new exceptional exits since X?") + CodeGenCtx* = object ## Bundles all input, output, and other contextual data needed for the ## code generator @@ -160,6 +178,8 @@ type # input-output parameters: code*: seq[TInstr] debug*: seq[TLineInfo] + ehTable*: seq[HandlerTableEntry] + ehCode*: seq[EhInstr] constants*: seq[VmConstant] typeInfoCache*: TypeInfoCache rtti*: seq[VmTypeInfo] @@ -401,6 +421,32 @@ proc patch(c: var TCtx, p: TPosition) = c.code[p] = ((oldInstr.TInstrType and regBxMask).TInstrType or TInstrType(diff+wordExcess) shl regBxShift).TInstr +proc genSetEh(c: var TCtx, info: TLineInfo): TPosition = + # the correct values are set at a later point + result = c.code.len.TPosition + c.prc.baseOffset = result + c.gABC(info, opcSetEh, c.ehTable.len, 0) + +proc patchSetEh(c: var TCtx, p: TPosition) = + ## Patches the ``SetEh`` instruction at `p` with the mapping list's upper + ## bound (using the current end of the mapping list). + let + p = p.int + fin = c.ehTable.len + instr = c.code[p] + assert instr.opcode == opcSetEh + # opcode and regA stay the same, only regB is updated: + c.code[p] = TInstr(instr.TInstrType or TInstrType(fin shl regBShift)) + +proc registerEh(c: var TCtx) = + ## If a jump-list designated for exception handling is active, associates it + ## with the next-emitted instruction. + if c.prc.hasEh > 0: + inc c.prc.raiseExits + let pos = c.code.len + c.ehTable.add: + (uint32(pos - c.prc.baseOffset.int), c.ehCode.len.uint32) + proc getSlotKind(t: PType): TSlotKind = case t.skipTypes(IrrelevantTypes+{tyRange}).kind of tyBool, tyChar, tyInt..tyInt64, tyUInt..tyUInt64: @@ -596,13 +642,19 @@ proc genRepeat(c: var TCtx; n: CgNode) = c.gen(n[0]) c.jmpBack(n, lab1) +func initBlock(kind: BlockKind, cr = TRegister(0)): typeof(BProc().blocks[0]) = + ## NOTE: this procedure is a workaround for a bug of the current csources + ## compiler. `isTry` being a literal bool value would lead to run-time + ## crashes. + result = (kind, @[], cr) + proc genBlock(c: var TCtx; n: CgNode) = let oldRegisterCount = c.prc.regInfo.len - c.prc.blocks.add @[] # push a new block + c.prc.blocks.add initBlock(bkBlock) # push a new block c.gen(n[1]) # fixup the jumps: - for pos in c.prc.blocks[^1].items: + for pos in c.prc.blocks[^1].exits.items: c.patch(pos) # pop the block again: c.prc.blocks.setLen(c.prc.blocks.len - 1) @@ -618,9 +670,36 @@ proc genBlock(c: var TCtx; n: CgNode) = doAssert false, "leaking temporary " & $i & " " & $c.prc.regInfo[i].kind c.prc.regInfo[i] = RegInfo(kind: slotEmpty) +proc blockLeaveActions(c: var TCtx, info: CgNode, target: Natural) = + ## Emits the bytecode for leaving a block. `target` is the index of the + ## block to exit. + # perform the leave actions from innermost to outermost + for i in countdown(c.prc.blocks.high, target): + case c.prc.blocks[i].kind + of bkBlock: + discard "no leave action to perfrom" + of bkTry: + # enter the finally clause + c.prc.blocks[i].exits.add c.xjmp(info, opcEnter) + of bkExcept: + # leave the except clause + c.gABI(info, opcLeave, 0, 0, 0) + of bkFinally: + # leave the finally clause + c.gABI(info, opcLeave, c.prc.blocks[i].cr, 0, 1) + proc genBreak(c: var TCtx; n: CgNode) = - let lab1 = c.xjmp(n, opcJmp) - c.prc.blocks[n[0].label.int].add lab1 + # find the labeled block corresponding to the block ID: + var i, b = 0 + while b < n[0].label.int or c.prc.blocks[i].kind != bkBlock: + b += ord(c.prc.blocks[i].kind == bkBlock) + inc i + + blockLeaveActions(c, n, i) + + # emit the actual jump to the end of targeted labeled block: + let label = c.xjmp(n, opcJmp) + c.prc.blocks[i].exits.add label proc genIf(c: var TCtx, n: CgNode) = # if (!expr1) goto lab1; @@ -863,47 +942,179 @@ proc genTypeInfo(c: var TCtx, typ: PType): int = internalAssert(c.config, result <= regBxMax, "") -proc genTry(c: var TCtx; n: CgNode) = - var endings: seq[TPosition] = @[] - let ehPos = c.xjmp(n, opcTry, 0) - c.gen(n[0]) - # Add a jump past the exception handling code - let jumpToFinally = c.xjmp(n, opcJmp, 0) - # This signals where the body ends and where the exception handling begins - c.patch(ehPos) - for i in 1.. 1: + # exception handler with filter for j in 0.. firstExit: + # all exceptional exits from within the ``except`` need to close the + # thread that entered it + c.ehCode.add (ehoNext, 0'u16, 2'u32) + c.ehCode.add (ehoLeave, 0'u16, 0'u32) + c.prc.raiseExits = firstExit + hasRaiseExits = true + + if i < last: + # emit a jump past the following handlers + exits.add c.xjmp(n[i], opcJmp) + + for endPos in exits.items: + c.patch(endPos) + + if n[last].len > 1 or hasRaiseExits: + # exceptional control-flow possibly leaves the handler section (because + # there's no catch-all handler), OR one of the handlers potentially + # raises + inc c.prc.raiseExits + +proc genFinally(c: var TCtx, n: CgNode, firstExit: int) = + ## Generates and emits the code for a ``cnkFinally`` clause. + let + enteredViaExcept = c.prc.raiseExits > firstExit + startEh = c.ehCode.len + + # patch the 'Enter' instructions entering the finalizer and then pop the + # block + for pos in c.prc.blocks[^1].exits.items: + c.patch(pos) + c.prc.blocks.setLen(c.prc.blocks.len - 1) + + # omit the EH 'Finally' instruction if there are no exceptional exits + if enteredViaExcept: + c.ehCode.add (ehoFinally, 0'u16, uint32 c.genLabel()) + # add a tentative 'Next' instruction; it's removed again if not needed + c.ehCode.add (ehoNext, 0'u16, 0'u32) + # all exceptional threads are handled + c.prc.raiseExits = firstExit + + let + control = c.getTemp(slotTempInt) + start = c.xjmp(n, opcFinally, control) + + # generate the code for the body + c.prc.blocks.add initBlock(bkFinally, control) + c.prc.hasEh += ord(enteredViaExcept) + c.gen(n[0]) + c.prc.hasEh -= ord(enteredViaExcept) + c.prc.blocks.setLen(c.prc.blocks.len - 1) + + if enteredViaExcept: + if c.prc.raiseExits > firstExit: + # the 'finally' could be part of an active exceptional thread, and + # the 'finally' clause has an exceptional exit. Patch the earlier + # 'Next' instruction to point *past* the 'Leave' + c.ehCode[startEh + 1].b = uint32(c.ehCode.len - (startEh+1) + 1) + c.ehCode.add (ehoLeave, 1'u16, uint32 c.genLabel()) + else: + # remove the unneeded 'Next' + c.ehCode.setLen(startEh + 1) + + # continue the exceptional control-flow + inc c.prc.raiseExits + + c.gABx(n, opcFinallyEnd, control, 0) + c.patch(start) + c.freeTemp(control) + +proc genTry(c: var TCtx; n: CgNode) = + let + hasExcept = n[1].kind == cnkExcept + hasFinally = n[^1].kind == cnkFinally + startEh = c.ehCode.len + firstExit = c.prc.raiseExits + needsSkip = firstExit > 0 and c.ehCode[^1].opcode != ehoNext + + if needsSkip: + # an unclosed EH chain on the same level exists, e.g.: + # try: + # try: ... finally: ... # <- this is the unclosed chain + # try: ... finally: ... + # except: ... + # + # make sure the new chain doesn't enter the finally/except clauses emitted + # for the current 'try' + c.ehCode.add (ehoNext, 0'u16, 0'u32) + + if hasFinally: + c.prc.blocks.add initBlock(bkTry) + + # emit the bytecode for the 'try' body: + inc c.prc.hasEh + c.gen(n[0]) + dec c.prc.hasEh + + # omit the exception handlers if there are no exceptional exits from within + # the try clause + if hasExcept and c.prc.raiseExits > firstExit: + let eh = c.xjmp(n, opcJmp) # jump past the exception handling + c.prc.hasEh += ord(hasFinally) + genExcept(c, n, firstExit) + c.prc.hasEh -= ord(hasFinally) + c.patch(eh) + + if hasFinally: + genFinally(c, n[^1], firstExit) + + if needsSkip: + # patch the 'Next' instruction skipping the handler chain + c.ehCode[startEh].b = uint32(c.ehCode.len - startEh) + + if c.prc.hasEh == 0: + # end the EH chain if there are no more applicable handlers within the + # current procedure + assert firstExit == 0 + c.ehCode.add (ehoEnd, 0'u16, 0'u32) + c.prc.raiseExits = firstExit + # echo "emit: end" proc genRaise(c: var TCtx; n: CgNode) = if n[0].kind != cnkEmpty: let dest = c.genx(n[0]) + c.registerEh() c.gABI(n, opcRaise, dest, 0, imm=0) c.freeTemp(dest) else: # reraise + c.registerEh() c.gABI(n, opcRaise, 0, 0, imm=1) proc writeBackResult(c: var TCtx, info: CgNode) = @@ -923,10 +1134,10 @@ proc writeBackResult(c: var TCtx, info: CgNode) = c.freeTemp(tmp) proc genReturn(c: var TCtx; n: CgNode) = + blockLeaveActions(c, n, 0) writeBackResult(c, n) c.gABC(n, opcRet) - proc genLit(c: var TCtx; n: CgNode; lit: int; dest: var TDest) = ## `lit` is the index of a constant as returned by `genLiteral` # load the literal into the *register* @@ -1019,6 +1230,7 @@ proc genCall(c: var TCtx; n: CgNode; dest: var TDest) = internalAssert(c.config, tfVarargs in fntyp.flags) c.gABx(n, opcSetType, r, c.genType(n[i].typ)) + c.registerEh() if res.isUnset: c.gABC(n, opcIndCall, 0, x, n.len) else: @@ -3007,7 +3219,9 @@ proc genStmt*(c: var TCtx; body: sink Body): Result[int, VmGenDiag] = var d: TDest = -1 try: + let eh = genSetEh(c, n.info) c.gen(n, d) + c.patchSetEh(eh) except VmGenError as e: return typeof(result).err(move e.diag) @@ -3021,6 +3235,7 @@ proc genExpr*(c: var TCtx; body: sink Body): Result[int, VmGenDiag] = var d: TDest = -1 try: + let eh = genSetEh(c, n.info) if n.kind == cnkStmtListExpr: # special case the expression here so that ``gen`` doesn't have to for i in 0..try ''' exitCode: 1 - knownIssue.vm: ''' - Exception/finally handling is largely disfunctional in the VM - ''' """ # Test break in try statement: diff --git a/tests/exception/tfinally6.nim b/tests/exception/tfinally6.nim new file mode 100644 index 00000000000..113a747c78c --- /dev/null +++ b/tests/exception/tfinally6.nim @@ -0,0 +1,172 @@ +discard """ + description: ''' + Multiple tests regarding ``finally`` interaction with exception handlers + and raised exceptions. + ''' + knownIssue.c js: "The current exception is not properly cleared" +""" + +var steps: seq[int] + +template test(call: untyped, expect: untyped) = + steps = @[] # reset the step list + {.line.}: + call + doAssert steps == expect, $steps + doAssert getCurrentException().isNil, "current exception wasn't cleared" + +# ------ the tests follow ------ + +proc simpleFinally() = + try: + try: + raise ValueError.newException("a") + finally: + steps.add 1 + steps.add 2 + except ValueError as e: + steps.add 3 + doAssert e.msg == "a" + +test simpleFinally(), [1, 3] + +proc raiseFromFinally() = + try: + try: + raise ValueError.newException("a") + finally: + steps.add 1 + raise ValueError.newException("b") + doAssert false, "unreachable" + except ValueError as e: + # the exception raised in the finally clause overrides the one raised + # earlier + steps.add 2 + doAssert e.msg == "b" + doAssert getCurrentException() == e + + steps.add 3 + +test raiseFromFinally(), [1, 2, 3] + +proc reraiseFromFinally() = + try: + try: + raise ValueError.newException("a") + finally: + steps.add 1 + # abort the exception but immediately re-raise it + raise + doAssert false, "unreachable" + except ValueError as e: + steps.add 2 + doAssert e.msg == "a" + doAssert getCurrentException() == e + + steps.add 3 + +test reraiseFromFinally(), [1, 2, 3] + +proc exceptionInFinally() = + ## Raise, and fully handle, an exception within a finally clause that was + ## entered through exceptional control-flow. + try: + try: + raise ValueError.newException("a") + finally: + steps.add 1 + try: + raise ValueError.newException("b") + except ValueError as e: + steps.add 2 + doAssert e.msg == "b" + doAssert getCurrentException() == e + + steps.add 3 + # the current exception must be the one with which the finally section + # was entered + doAssert getCurrentException().msg == "a" + + doAssert false, "unreachable" + except ValueError as e: + steps.add 4 + doAssert e.msg == "a" + + steps.add 5 + +test exceptionInFinally(), [1, 2, 3, 4, 5] + +proc leaveFinally1() = + ## Ensure that exiting a finally clause entered through exceptional control- + ## flow via unstructured control-flow (break) works and properly clears the + ## current exception. + block exit: + try: + raise ValueError.newException("a") + finally: + steps.add 1 + doAssert getCurrentException().msg == "a" + break exit + doAssert false, "unreachable" + + steps.add 2 + +test leaveFinally1(), [1, 2] + +proc leaveFinally2() = + ## Ensure that aborting an exception raised within a finally clause entered + ## through exceptional control-flow doesn't interfere with the original + ## exception. + try: + try: + raise ValueError.newException("a") + finally: + block exit: + steps.add 1 + try: + raise ValueError.newException("b") + finally: + steps.add 2 + # discards the in-flight exception 'b' + break exit + doAssert false, "unreachable" + + steps.add 3 + # the current exception must be the one the finally was entered with: + doAssert getCurrentException().msg == "a" + # unwinding continues as usual + doAssert false, "unreachable" + except ValueError as e: + steps.add 4 + doAssert e.msg == "a" + doAssert getCurrentException() == e + +test leaveFinally2(), [1, 2, 3, 4] + +proc leaveFinally3(doExit: bool) = + ## Ensure that aborting an exception in a finally clause still visits all + ## enclosing finally clauses, and that the finally clauses observe the + ## correct current exception. + block exit: + try: + try: + raise ValueError.newException("a") + finally: + steps.add 1 + try: + if doExit: # obfuscate the break + break exit + finally: + steps.add 2 + # the current exception must still be set + doAssert getCurrentException().msg == "a" + doAssert false, "unreachable" + doAssert false, "unreachable" + finally: + steps.add 5 + # the finally section is not part of the inner try statement, so the + # current exception is nil + doAssert getCurrentException() == nil + doAssert false, "unreachable" + +test leaveFinally3(true), [1, 2, 5] diff --git a/tests/exception/thandle_in_finally.nim b/tests/exception/thandle_in_finally.nim new file mode 100644 index 00000000000..968ebfcd8ca --- /dev/null +++ b/tests/exception/thandle_in_finally.nim @@ -0,0 +1,35 @@ +discard """ + description: ''' + A test for tracking the (uncertain) behaviour of entering a `finally` + clause through an exception, handling the exception within the + `finally`, and then leaving through structured control-flow. + ''' + matrix: "-d:noSignalHandler" + knownIssue: "true" +""" + +# XXX: disabling the signal handler is currently necessary for the test +# to crash rather than entering an infinite loop + +var steps: seq[int] + +try: + try: + raise CatchableError.newException("a") + finally: + try: + raise # re-raise the active exception + except CatchableError: + # catch the exception + steps.add 1 + # leaving this except handler means that the exception is handled + steps.add 2 + doAssert getCurrentException() == nil + # unwinding cannot continue when leaving the finally, since no exception + # is active anymore + steps.add 3 +except CatchableError: + # never reached, since the exception was handled above + doAssert false, "unreachable" + +doAssert steps == [1, 2, 3] \ No newline at end of file diff --git a/tests/exception/tleave_except.nim b/tests/exception/tleave_except.nim new file mode 100644 index 00000000000..f85a9f383dd --- /dev/null +++ b/tests/exception/tleave_except.nim @@ -0,0 +1,30 @@ +discard """ + description: ''' + Ensure that exiting an exception handler via unstructured control-flow + (``break``) works and properly clears the current exception. + ''' + output: "done" + knownIssue.js: "The current exception is not properly cleared" +""" + +var steps: seq[int] + +block exit: + try: + raise ValueError.newException("a") + except ValueError as e: + steps.add 1 + if e.msg == "a": + # an unstructured exit of the except block needs to pop the current + # exception + break exit + else: + doAssert false, "unreachable" + doAssert false, "unreachable" + +steps.add 2 + +doAssert getCurrentException() == nil, "current exception was cleared" +doAssert steps == [1, 2] + +echo "done" # ensures that the assertions weren't jumped over diff --git a/tests/exception/traise_and_handle_in_except.nim b/tests/exception/traise_and_handle_in_except.nim new file mode 100644 index 00000000000..f970a91594d --- /dev/null +++ b/tests/exception/traise_and_handle_in_except.nim @@ -0,0 +1,32 @@ +discard """ + description: ''' + Ensure that raising and fully handling an exception within an ``except`` + branch works. + ''' + output: "done" + knownIssue.js: "The current exception is not properly cleared" +""" + +var steps: seq[int] + +try: + raise ValueError.newException("a") +except ValueError as e: + steps.add 1 + try: + raise ValueError.newException("b") + except ValueError as e: + steps.add 2 + doAssert e.msg == "b" + doAssert getCurrentException() == e + + steps.add 3 + # make sure the current exception is still the correct one + doAssert e.msg == "a" + doAssert getCurrentException() == e + +steps.add 4 +doAssert getCurrentException() == nil, "current exception wasn't cleared" +doAssert steps == [1, 2, 3, 4] + +echo "done" # make sure the assertions weren't skipped over diff --git a/tests/exception/twrong_handler.nim b/tests/exception/twrong_handler.nim new file mode 100644 index 00000000000..0aa3e09186b --- /dev/null +++ b/tests/exception/twrong_handler.nim @@ -0,0 +1,26 @@ +discard """ + description: ''' + Ensure that the correct except branch is jumped to after exiting a try + block through unstructured, but non-exceptional, control-flow. + ''' +""" + +proc test(doExit: bool): bool = + try: + block exit: + try: + if doExit: + break exit + else: + discard "fall through" + except ValueError: + doAssert false, "unreachable" + + # the above except branch previously caught the exception + raise ValueError.newException("a") + except ValueError as e: + doAssert e.msg == "a" + result = true + +doAssert test(true) +doAssert test(false) \ No newline at end of file