From 42945cd94b8bd9bdda4bd4fb9a3d8d6cc6f580da Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Fri, 15 Nov 2024 23:09:36 +0100 Subject: [PATCH] mirgen: lower `finally` (#1468) ## Summary * simplify MIR control-flow constructs * lower `finally` into a `block` + `case` dispatcher in `mirgen` * emit exception stack management code in `mirgen` already * inline scope cleanup directly at `break` and `return` ## Details The goals are to: * use a unified exception runtime across all backends * make the MIR simpler * reduce the complexity of the language reaching the code generators, and thus the amount of translation work for the code generators For the MIR: * target lists are removed; jumps only need to specify a single target * `Finally` can only be used as the target for exceptional jumps; unstructured control-flow out of a `Finally` section is disallowed * only a single exceptional target can be specified for `Continue` * `Raise` is decoupled from exception management; it only initiates unwinding now The various MIR processing, rendering, and translation is adjusted accordingly. The CGIR equivalents to the MIR constructs change in the same way. Since `Finally` can no longer be used with non-exceptional control-flow (`Goto`, `Case`, etc.), translation of both scope cleanup and `finally` needs to change in `mirgen`. The exception runtime calls for managing the exception stack also have to be injected in `mirgen` already. ### `finally` Lowering `finally` is translated into continuation passing. However, instead of reifying the `finally` into a standalone procedure, all "invocations" of (read, jumps to) the `finally` record their original destination in a local variable, which a dispatcher emitted at the end of the `finally` clause then uses to jump to the target (or another intercepting `finally`). ### Scope Cleanup Scope cleanup in response to an exception being raised still uses `Finally`. For cleanup in response to `break` or `return`, all necessary cleanup is emitted directly before the `Goto`. This increases the workload for the move analysis / destructor elision pass, but it also results in more efficient code (since there are usually less dynamic invocations of destructors). ### Code Generation All three code generators are updated to consider the new syntax and semantics of `Finally`, `Continue`, etc. Notably: * `ccgflow` is obsolete and thus removed; code generation for `Finally`, `Continue`, etc. is now simple enough to implement directly in `cgen` * `jsgen` now translates `Finally` to a JavaScript `catch` clause (so as to not interfere with `break`s), which simplifies code generation (no more enabling/disabling of `finally` clauses for breaks) and also improves code size / performance in some cases ### Exception Runtime * `nimAbortException` has a `viaRaise` parameter now, as the C-specific error flag cannot be used anymore for detecting what causes the abort * `prepareException` is merged back into `raiseExceptionAux` The JavaScript target also using the C exception runtime now fixes a bug where breaking out of a `finally` didn't clean up the current exception properly. ### VM Exception stack management is partially decoupled from the core VM and moved to `vmops` (which hooks and implements the various exception runtime procedures). Generating the stacktrace still needs to be done directly by the VM, which prevents the exception stack management from being fully decoupled. --------- Co-authored-by: Saem Ghani --- compiler/backend/ccgexprs.nim | 19 +- compiler/backend/ccgflow.nim | 543 ------------------ compiler/backend/ccgstmts.nim | 119 +--- compiler/backend/cgen.nim | 14 +- compiler/backend/cgir.nim | 3 - compiler/backend/cgirgen.nim | 34 +- compiler/backend/jsflow.nim | 90 +-- compiler/backend/jsgen.nim | 126 +--- compiler/front/msgs.nim | 10 + compiler/mir/mirgen.nim | 219 +++++-- compiler/mir/mirgen_blocks.nim | 305 +++++----- compiler/mir/mirpasses.nim | 3 +- compiler/mir/mirtrees.nim | 20 +- compiler/mir/utils.nim | 30 +- compiler/sem/injectdestructors.nim | 2 - compiler/sem/mirexec.nim | 45 +- compiler/vm/vm.nim | 212 ++----- compiler/vm/vm_enums.nim | 5 +- compiler/vm/vmdef.nim | 25 +- compiler/vm/vmgen.nim | 225 ++------ compiler/vm/vmops.nim | 52 +- doc/mir.rst | 74 +-- lib/system.nim | 19 +- lib/system/excpt.nim | 28 +- lib/system/jssys.nim | 50 +- tests/arc/topt_cursor.nim | 59 +- tests/arc/topt_no_cursor.nim | 95 +-- tests/arc/topt_no_loop_cursor.nim | 16 +- tests/arc/topt_refcursors.nim | 16 +- tests/arc/topt_wasmoved_destroy_pairs.nim | 51 +- tests/exception/tfinally6.nim | 1 - tests/lang_objects/destructor/tv2_cast.nim | 32 +- .../tno_destroy_for_empty_result.nim | 4 +- 33 files changed, 846 insertions(+), 1700 deletions(-) delete mode 100644 compiler/backend/ccgflow.nim diff --git a/compiler/backend/ccgexprs.nim b/compiler/backend/ccgexprs.nim index 0410ff813bb..f16afe3d684 100644 --- a/compiler/backend/ccgexprs.nim +++ b/compiler/backend/ccgexprs.nim @@ -1753,8 +1753,14 @@ proc expr(p: BProc, n: CgNode, d: var TLoc) = of cnkLoopJoinStmt: startBlock(p, "while (1) {$n") of cnkFinally: - startBlock(p) - of cnkEnd, cnkContinueStmt, cnkLoopStmt: + startBlock(p, "$1: {$n", [$n[0].label]) + linefmt(p, cpsStmts, "*nimErr_ = NIM_FALSE;$n", []) + of cnkContinueStmt: + p.flags.incl nimErrorFlagAccessed + linefmt(p, cpsStmts, "*nimErr_ = NIM_TRUE;$n", []) + linefmt(p, cpsStmts, "$1$n", [raiseInstr(p, n[0])]) + endBlock(p) + of cnkEnd, cnkLoopStmt: endBlock(p) of cnkDef: genSingleVar(p, n[0], n[1]) of cnkCaseStmt: genCase(p, n) @@ -1770,11 +1776,12 @@ proc expr(p: BProc, n: CgNode, d: var TLoc) = of cnkExcept: genExcept(p, n) of cnkRaiseStmt: genRaiseStmt(p, n) - of cnkJoinStmt, cnkGotoStmt: - unreachable("handled separately") + of cnkJoinStmt: + linefmt(p, cpsStmts, "$1:;$n", [n[0].label]) + of cnkGotoStmt: + linefmt(p, cpsStmts, "goto $1;$n", [n[0].label]) of cnkInvalid, cnkType, cnkAstLit, cnkMagic, cnkRange, cnkBinding, cnkBranch, - cnkLabel, cnkTargetList, cnkField, cnkStmtList, - cnkLeave, cnkResume: + cnkLabel, cnkField, cnkStmtList, cnkResume: internalError(p.config, n.info, "expr(" & $n.kind & "); unknown node kind") proc getDefaultValue(p: BProc; typ: PType; info: TLineInfo): Rope = diff --git a/compiler/backend/ccgflow.nim b/compiler/backend/ccgflow.nim deleted file mode 100644 index 8c54e85efe5..00000000000 --- a/compiler/backend/ccgflow.nim +++ /dev/null @@ -1,543 +0,0 @@ -## Implements the translation of CGIR to a code listing for an abstract -## machine that focuses on control-flow and exception handling. -## -## This code listing is intended for consumption by the C code generator. - -import - std/[ - options, - packedsets, - tables - ], - compiler/backend/[ - cgir - ], - compiler/utils/[ - idioms - ] - -type - COpcode* = enum - opJump ## unconditional jump - opErrJump ## jump if in error mode - opDispJump ## jump part of a dispatcher - opLabel ## jump target - - opSetTarget ## set the value of a dispatcher's discriminator - opDispatcher ## start of a dispatcher - - opBackup ## backup the error state in a local variable and clear it - opRestore ## restore the error from a local variable - - opStmts ## slice of statements - opStmt ## control-flow relevant single statement. A label - ## specifier is passed along - # future direction: ``CStmt`` should be removed and all unstructured - # control-flow bits modeled with the other instructions. Checked calls - # used as an assignment source currently block this, as they might - # require an assignment-to-temporary - - opAbort - opPopHandler - - JumpOp = range[opJump..opDispJump] - - CLabelId* = distinct uint32 - CLabelSpecifier* = uint32 - ## used for identifying the extra labels attached to finally sections - - CLabel* = tuple - ## Name of a label. - id: CLabelId - specifier: Option[CLabelSpecifier] - - CInstr* = object - case op*: COpcode - of opJump, opErrJump, opLabel, opDispJump: - label*: CLabel - of opSetTarget, opDispatcher: - discr*: uint32 ## ID of the discriminator variable - value*: int ## either the value, or number of dispatcher branches - of opStmts: - stmts*: Slice[int] - of opStmt: - stmt*: int - specifier*: CLabelSpecifier - of opBackup, opRestore, opAbort: - local*: uint32 ## ID of the backup variable - of opPopHandler: - discard - - FinallyInfo* = object - routes: seq[PathIndex] - numExits: int - ## number of exits the finally has. Pre-computed for efficiency - numErr: int - ## number of exceptional jump paths going through this finalizer - numNormal: int - ## number of non-exception jump paths going through this finalizer - - discriminator: uint32 - ## ID of the discriminator variable to use for the dispatcher - errBackupId: uint32 - ## only valid if the finally is entered by exceptional control-flow - - PathKind = enum - pkError - pkNormal - PathIndex = uint32 - ## Index of a ``PathItem`` within the item storage. - PathItemTarget = object - label: CLabelId - isCleanup: bool - PathItem = object - ## Represents a step in a jump path. A jump path is a chain of finally - ## sections plus final target an intercepted goto visits. - ## - ## An item is part of two intrusive linked-lists: one doubly-linked-list - ## representing a single chain, and one singly-linked-list for the - ## adjacent chains. The "none" value for a pointer is represented by it - ## pointing to the node itself. - prev, next: PathIndex - sibling: PathIndex - - target: PathItemTarget - ## the identifier of the jump target - kinds: set[PathKind] - ## the kinds of control-flow (exception or normal) reaching the path - ## item - - Paths = seq[PathItem] - ## The data storage for multiple jump paths, with the items layed out - ## "tail first", meaning that the final target of a jump chain comes - ## *before* the others. The idea is to uniquely identify jump paths - ## within a body while merging common trailing paths. - ## - ## Consider the two jump paths E->D->C->B->A and G->F->C->B->A. If - ## both are added to the storage, the content would look like this: - ## - ## (0: A) -> (1: B) -> (2: C) -> (3: D) -> (4: E) - ## \ (5: F) -> (6: G) - ## - ## The numbers represent the items' index in the sequence. The `sibling` - ## item of 3 is 5 (all other items have no siblings); the `next` pointer - ## of 5 points to 2. As can be seen, common trailing paths are merged into - ## one. - - Context = object - ## Local state used during the translation bundled into an object for - ## convenience. - paths: Paths - stmtToPath: Table[int, int] - finallys: Table[CLabelId, FinallyInfo] - cleanups: Table[CLabelId, FinallyInfo] - ## cleanup here refers to the exception-related cleanup when - ## exiting a finally or except section - -const - ExitLabel* = CLabelId(0) - ## The label of the procedure exit. - ResumeLabel* = ExitLabel - ## The C label that a ``cnkResume`` targets. - -func `==`*(a, b: CLabelId): bool {.borrow.} - -func toCLabel*(n: CgNode): CLabelId = - ## Returns the ID of the C label the label-like node `n` represents. - case n.kind - of cnkResume: - ResumeLabel - of cnkLabel: - CLabelId(ord(n.label) + 2) - of cnkLeave: - toCLabel(n[0]) - else: - unreachable(n.kind) - -func toCLabel*(n: CgNode, specifier: Option[CLabelSpecifier] - ): CLabel {.inline.} = - (toCLabel(n), specifier) - -func toBlockId*(id: CLabelId): BlockId = - ## If `id` was converted to from a valid CGIR label, converts it back to - ## the CGIR label. - BlockId(ord(id) - 2) - -func rawAdd(p: var Paths, x: openArray[PathItemTarget]): PathIndex = - ## Appends the chain `x` to `p` without any deduplication or - ## linking with the existing items. Returns the index of the - ## tail item. - result = p.len.PathIndex - for i in countdown(x.high, 0): - let pos = p.len.PathIndex - p.add PathItem(prev: (if i > 0: pos + 1 else: pos), - next: (if i < x.high: pos - 1 else: pos), - sibling: pos, - target: x[i]) - -func add(p: var Paths, path: openArray[PathItemTarget]): PathIndex = - ## Adds `path` to the `p`. Only the sub-path of `path` not yet present in - ## `p` is added. The index of the *head* item of the added (or existing) - ## path is returned. - if p.len == 0: - discard rawAdd(p, path) - p[0].next = 0'u32 - return p.high.PathIndex - - var pos = 0'u32 ## the current search position - for i in countdown(path.len-1, 0): - # search the sibling list for a matching item: - while p[pos].target != path[i] and pos != p[pos].sibling: - pos = p[pos].sibling - - if p[pos].target != path[i]: - # no item was found, meaning that this is the end of the common paths. - # Add the remaining items to the storage. - let next = rawAdd(p, path.toOpenArray(0, i)) - p[pos].sibling = next - # only set the next pointer if there was a common sub-path (otherwise - # there's no next item): - if i != path.high: - p[next].next = p[pos].next - return p.high.PathIndex - - # it's a match! continue down the chain - if i > 0: - if p[pos].prev == pos: - # there's no next item, append the remaining new targets to the - # pre-existing path - let next = rawAdd(p, path.toOpenArray(0, i-1)) - p[pos].prev = next - p[next].next = pos - return p.high.PathIndex - else: - pos = p[pos].prev - - # the chain `path` already exists in `p` - result = pos - -func incl(p: var Paths, at: PathIndex, kind: PathKind) = - ## Marks all items following and including `at` with `kind`. - var i = at - while p[i].next != i: - p[i].kinds.incl kind - i = p[i].next - p[i].kinds.incl kind - -func needsDispatcher(f: FinallyInfo): bool = - # a dispatcher is required if re are more than one exits. An exception is - # the case where one exit is only taken when in error mode and the other is - # not. If a dispatcher is required, the finally has sub-labels. - f.numExits > 1 and - not(f.routes.len == 2 and f.numErr == 1 and f.numNormal == 1) - -func needsSpecifier(c: Context, target: PathItemTarget): bool = - # cleanup sections don't have a unique label themselves, so using a - # specifier is required - target.isCleanup or - ((target.label in c.finallys) and - needsDispatcher(c.finallys[target.label])) - -proc append(targets: var seq[PathItemTarget], - redirects: Table[BlockId, CgNode], - exits: PackedSet[BlockId], n: CgNode) = - ## Appends all jump targets `n` represents to `targets`, following - ## `redirects` and turning all labels part of `exits` into the - ## "before return" label. - template addTarget(t: CLabelId; cleanup = false) = - targets.add PathItemTarget(label: t, isCleanup: cleanup) - - case n.kind - of cnkLabel: - if n.label in redirects: - append(targets, redirects, exits, redirects[n.label]) - elif n.label in exits: - addTarget ExitLabel - else: - addTarget toCLabel(n) - of cnkTargetList: - # only the final target could possibly be redirected - let hasRedir = n[^1].kind == cnkLabel and n[^1].label in redirects - for i in 0.. 1 or targets[0].label in c.finallys: - let id = c.paths.add(targets) - if isErr: incl(c.paths, id, pkError) - else: incl(c.paths, id, pkNormal) - - # remember the path associated with the statement for later: - c.stmtToPath[i] = id.int - - case it.kind - of cnkDef, cnkAsgn, cnkFastAsgn: - if it[1].kind == cnkCheckedCall: - exit(it[1][^1], true) - of cnkRaiseStmt, cnkCheckedCall: - exit(it[^1], true) - of cnkGotoStmt: - exit(it[0]) - of cnkCaseStmt: - for j in 1.. 1: - exit(it[^1], true) - else: - discard - - # register every path item with the finally section it targets, and compute - # some statistics that are used during the later code generation: - for i, it in c.paths.pairs: - func setup(f: var FinallyInfo, it: PathItem) = - f.routes.add i.PathIndex - f.numExits += ord(it.next.int != i) - f.numErr += ord(pkError in it.kinds) - f.numNormal += ord(pkNormal in it.kinds) - - if it.target.isCleanup: - setup(c.cleanups.mgetOrPut(it.target.label, FinallyInfo()), it) - elif it.target.label in c.finallys: - setup(c.finallys[it.target.label], it) - - # construction of the instruction list follows - - proc label(code: var seq[CInstr], id: CLabelId; - spec = none(CLabelSpecifier)) {.nimcall.} = - # a label must always be preceded by some code, so no length guard is - # required - if code[^1].op in {opJump, opErrJump} and code[^1].label.id == id and - code[^1].label.specifier == spec: - # optimization: remove the preceding jump if it targets the label - code.setLen(code.len - 1) - code.add CInstr(op: opLabel, label: (id, spec)) - - proc jump(code: var seq[CInstr], target: CLabelId) {.nimcall.} = - code.add CInstr(op: opJump, label: (target, none CLabelSpecifier)) - - proc jump(code: var seq[CInstr], op: JumpOp, c: Context, - path: PathIndex) {.nimcall.} = - let target = c.paths[path].target - if needsSpecifier(c, target): - code.add CInstr(op: op, label: (target.label, some path)) - else: - code.add CInstr(op: op, label: (target.label, none CLabelSpecifier)) - - proc stmt(code: var seq[CInstr], c: Context, pos: int) {.nimcall.} = - if (let path = c.stmtToPath.getOrDefault(pos, -1); path != -1 and - needsSpecifier(c, c.paths[path].target)): - # a label specifier, and thus a separate instruction, is needed - code.add CInstr(op: opStmt, stmt: pos, specifier: CLabelSpecifier path) - elif code.len > 0 and code[^1].op == opStmts and - code[^1].stmts.b == pos + 1: - # append to the sequence - inc code[^1].stmts.b - else: - # start a new sequence - code.add CInstr(op: opStmts, stmts: pos..pos) - - var - code: seq[CInstr] - nextDispId = 0'u32 - nextRecoverID = 0'u32 - - for i, it in stmts.pairs: - case it.kind - of cnkFinally: - stmt code, c, i - let - clabel = toCLabel(it[0]) - f = addr c.finallys[clabel] - - # allocate and set the ID for the discriminator variable: - f.discriminator = nextDispId - inc nextDispId - - # emit the entry-point(s); one for each route - if needsDispatcher(f[]): - # an entry point looks like this: - # L1_1_: - # Target = ... - # goto L1_ - for i, entry in f.routes.pairs: - label code, clabel, some(entry) - code.add CInstr(op: opSetTarget, discr: f.discriminator, value: i) - # jump to the main code: - jump code, clabel - - # the body follows: - label code, clabel - if f.numErr > 0: - # backing up the error state is only needed when the finally is - # entered by exceptional control-flow - f.errBackupId = nextRecoverID - code.add CInstr(op: opBackup, local: nextRecoverID) - inc nextRecoverID - - of cnkContinueStmt: - let - clabel = toCLabel(it[0]) - f {.cursor.} = c.finallys[clabel] - - # no need to restore the error state if control-flow never reaches the - # end of the finally anyway - if f.numErr > 0 and f.numExits > 0: - code.add CInstr(op: opRestore, local: f.errBackupId) - - if f.numExits == 0: - discard "the end is never reached; nothing to do" - elif not needsDispatcher(f) and f.routes.len == 2: - # optimization: if two paths go through a finally, with one of them - # an exceptional jump path and the other one not, instead of using a - # full dispatcher we emit: - # if err: goto error_exit - # goto normal_exit - let exit = if c.paths[f.routes[0]].kinds == {pkError}: 0 else: 1 - jump code, opErrJump, c, c.paths[f.routes[exit]].next - jump code, opJump, c, c.paths[f.routes[1 - exit]].next - else: - assert f.routes.len == f.numExits - # a dispatcher is only required if there is more than one exit - let op = if f.numExits > 1: opDispJump - else: opJump - - if op == opDispJump: - code.add CInstr(op: opDispatcher, discr: f.discriminator, - value: f.numExits) - - for it in f.routes.items: - jump code, op, c, c.paths[it].next - - # emit the exception-related cleanup after the dispatcher: - if clabel in c.cleanups: - let cleanup {.cursor.} = c.cleanups[clabel] - # a dispatcher is not worth the overhead, emit an abort instruction - # for each route - for entry in cleanup.routes.items: - label code, clabel, some(entry) - # TODO: omit the cleanup logic as a whole, if the finally section is - # never entered via an exception - if f.numErr > 0: - code.add CInstr(op: opAbort, local: f.errBackupId) - jump code, opJump, c, PathIndex c.paths[entry].next - - stmt code, c, i - - of cnkJoinStmt: - # XXX: labels that were redirected cannot be eliminated yet, as case - # statements (which are handled outside of ccgflow) might still - # target them - label code, toCLabel(it[0]) - of cnkExcept: - # an except section is a label followed by the filter logic - label code, toCLabel(it[0]) - stmt code, c, i - of cnkEnd: - let clabel = toCLabel(it[0]) - # emit the cleanup for except sections: - if clabel in c.cleanups: - let cleanup {.cursor.} = c.cleanups[clabel] - # a dispatcher is not worth the overhead, emit a pop instruction - # for each route - for entry in cleanup.routes.items: - label code, clabel, some(entry) - code.add CInstr(op: opPopHandler) - jump code, opJump, c, PathIndex c.paths[entry].next - - stmt code, c, i - - of cnkGotoStmt: - let target = it[0] - if (let path = c.stmtToPath.getOrDefault(i, -1); path != -1): - jump code, opJump, c, PathIndex path - elif target.kind == cnkLabel: - jump code, toCLabel(target) - else: - jump code, toCLabel(target[^1]) - of cnkRaiseStmt: - stmt code, c, i # the statement handles the exception setup part - # the goto part is the same as for a normal goto - let target = it[^1] - if target.kind == cnkLabel: - jump code, toCLabel(target) - elif (let path = c.stmtToPath.getOrDefault(i, -1); path != -1): - jump code, opJump, c, PathIndex path - else: - jump code, toCLabel(target[^1]) - - else: - stmt code, c, i - - result = code diff --git a/compiler/backend/ccgstmts.nim b/compiler/backend/ccgstmts.nim index 6e16b590c6a..bf85773eca5 100644 --- a/compiler/backend/ccgstmts.nim +++ b/compiler/backend/ccgstmts.nim @@ -103,25 +103,18 @@ proc exit(n: CgNode): CgNode = of cnkCheckedCall: n[^1] else: nil -proc useLabel(p: BProc, label: CLabel) {.inline.} = - if label.id == ExitLabel: - p.flags.incl beforeRetNeeded - proc raiseInstr(p: BProc, n: CgNode): Rope = if n != nil: case n.kind of cnkLabel: - # easy case, simply goto the target: result = ropecg(p.module, "goto $1;", [n.label]) - of cnkTargetList: - # the first non-leave operand is the initial jump target - let label = toCLabel(n[0], p.specifier) - useLabel(p, label) - result = ropecg(p.module, "goto $1;", [label]) + of cnkResume: + p.flags.incl beforeRetNeeded + result = "goto BeforeRet_;" else: unreachable(n.kind) else: - # absence of an node storing the target means "never exits" + # absence of a node storing the target means "never exits" if hasAssume in CC[p.config.cCompiler].props: result = "__asume(0);" else: @@ -138,27 +131,14 @@ proc raiseExit(p: BProc, n: CgNode) = [raiseInstr(p, n)]) proc genRaiseStmt(p: BProc, t: CgNode) = - if t[0].kind != cnkEmpty: - var a: TLoc - initLocExprSingleUse(p, t[0], a) - var e = rdLoc(a) - discard getTypeDesc(p.module, t[0].typ) - var typ = skipTypes(t[0].typ, abstractPtrs) - genLineDir(p, t) - if isImportedException(typ, p.config): - lineF(p, cpsStmts, "throw $1;$n", [e]) - else: - lineCg(p, cpsStmts, "#raiseException2((#Exception*)$1, $2, $3, $4);$n", - [e, - makeCString(if p.prc != nil: p.prc.name.s else: p.module.module.name.s), - quotedFilename(p.config, t.info), toLinenumber(t.info)]) - + if nimErrorFlagDisabled in p.flags: + # the error flag needs to be set to true, but the ``nimErr_`` local was + # disabled. Query the pointer manually + linefmt(p, cpsStmts, "*#nimErrorFlag() = NIM_TRUE;$n", []) else: - genLineDir(p, t) - # reraise the last exception: - linefmt(p, cpsStmts, "#reraiseException();$n", []) - - # the goto is emitted separately + p.flags.incl nimErrorFlagAccessed + linefmt(p, cpsStmts, "*nimErr_ = NIM_TRUE;$n", []) + linefmt(p, cpsStmts, "$1$n", [raiseInstr(p, t[0])]) template genCaseGenericBranch(p: BProc, b: CgNode, e: TLoc, rangeFormat, eqFormat: FormatStr, labl: BlockId) = @@ -276,6 +256,7 @@ proc bodyCanRaise(p: BProc; n: CgNode): bool = proc genExcept(p: BProc, n: CgNode) = ## Generates and emits the C code for an ``Except`` join point. + linefmt(p, cpsStmts, "$1:$n", [n[0].label]) if n.len > 1: # it's a handler with a filter/matcher @@ -302,10 +283,6 @@ proc genExcept(p: BProc, n: CgNode) = p.flags.incl nimErrorFlagAccessed # exit error mode: lineCg(p, cpsStmts, "*nimErr_ = NIM_FALSE;$n", []) - # setup the handler frame: - var tmp: TLoc - getTemp(p, p.module.g.graph.getCompilerProc("ExceptionFrame").typ, tmp) - lineCg(p, cpsStmts, "#nimCatchException($1);$n", [addrLoc(p.module, tmp)]) proc genAsmOrEmitStmt(p: BProc, t: CgNode, isAsmStmt=false): Rope = var res = "" @@ -421,78 +398,10 @@ proc genStmt(p: BProc, t: CgNode) = if isPush: popInfoContext(p.config) internalAssert p.config, a.k in {locNone, locTemp, locLocalVar, locExpr} -proc gen(p: BProc, code: openArray[CInstr], stmts: CgNode) = - ## Generates and emits the C code for `code` and `stmts`. This is the main - ## driver of C code generation. - var pos = 0 - while pos < code.len: - let it = code[pos] - case it.op - of opLabel: - lineCg(p, cpsStmts, "$1:;$n", [it.label]) - of opJump: - useLabel(p, it.label) - lineCg(p, cpsStmts, "goto $1;$n", [it.label]) - of opDispJump: - # must only be part of a dispatcher - unreachable() - of opSetTarget: - lineCg(p, cpsStmts, "Target$1_ = $2;$n", [$it.discr, it.value]) - of opDispatcher: - lineF(p, cpsLocals, "NU8 Target$1_;$N", [$it.discr]) - lineF(p, cpsStmts, "switch (Target$1_) {$n", [$it.discr]) - for i in 0.. [L0, L1] - # goto [L0, L2] - # L0: # finalizer - # ... - # Continue + # def y = f() -> [L1] + # goto [L2] # L1: # exception handler # ... # L2: ... @@ -255,7 +208,7 @@ proc toStructureList*(stmts: openArray[CgNode]): StructDesc = # handler. A labeled JS block must enclose the `goto`. for i, it in stmts.pairs: template exit(n: CgNode, isError: bool) = - spawnOpens(structs, i, n, isError, finallys, marker) + spawnOpens(structs, i, n, isError, marker) template terminator() = structs.add Structure(kind: stkTerminator, stmt: i) @@ -271,14 +224,7 @@ proc toStructureList*(stmts: openArray[CgNode]): StructDesc = exit(it[^1], isError=true) of cnkGotoStmt: exit(it[0], isError=false) - let target = finalTarget(it[0]) - # if the goto jumps to a finally, there's no label for the break. - # Since this can only happen when structured control-flow never - # leaves the finally, a JavaScript 'return' can be used - if target.label in finallys: - structs.add Structure(kind: stkReturn, stmt: i) - else: - terminator() + terminator() of cnkRaiseStmt: exit(it[^1], isError=true) terminator() @@ -291,8 +237,24 @@ proc toStructureList*(stmts: openArray[CgNode]): StructDesc = struct(stkStructStart, it[0].label) of cnkIfStmt: struct(stkStructStart, it[1].label) - of cnkEnd, cnkContinueStmt, cnkLoopStmt: + of cnkEnd, cnkLoopStmt: struct(stkEnd, it[0].label) + of cnkContinueStmt: + var + depth = 1 + j = structs.high + # look for the first unmatched opening 'finally': + while j >= 0 and (depth != 1 or structs[j].kind != stkFinally): + depth += ord(structs[j].kind == stkEnd) + depth -= ord(structs[j].kind in + {stkCatch, stkFinally, stkTry, stkStructStart, stkBlock}) + dec j + + assert structs[j].kind == stkFinally + exit(it[0], isError=true) + terminator() + # attach the end to the next statement: + structs.add Structure(kind: stkEnd, stmt: i + 1, label: structs[j].label) of cnkJoinStmt: assert it[0].label in marker struct(stkEnd, it[0].label) @@ -352,7 +314,7 @@ proc toStructureList*(stmts: openArray[CgNode]): StructDesc = of cnkGotoStmt: # we don't inline the target at bare gotos. Mark the block the goto # targets as ineligible by incrementing the counter by two - inline.mgetOrPut(finalTarget(n[0]).label, 0) += 2 + inline.mgetOrPut(n[0].label, 0) += 2 else: discard "only gotos are interesting" of stkEnd: @@ -384,4 +346,4 @@ proc toStructureList*(stmts: openArray[CgNode]): StructDesc = # structured control-flow would take same route # * a chain of exception could be merged into a single JavaScript catch - result = (structs, finallys, inline) + result = (structs, inline) diff --git a/compiler/backend/jsgen.nim b/compiler/backend/jsgen.nim index a30ba00aef8..971c35cb51e 100644 --- a/compiler/backend/jsgen.nim +++ b/compiler/backend/jsgen.nim @@ -145,13 +145,10 @@ type BlockKind = enum bkBlock - bkTryFinally ## try with attached 'finally' - bkTryCatch ## try with attached 'catch' - bkFinally + bkTry ## try with attached 'catch' bkCatch BlockFlag = enum - needsRecover needsEnableFlag BlockInfo = object @@ -168,9 +165,6 @@ type options: TOptions module: BModule g: PGlobals - lastErrorBackupNeeded: bool - ## signals whether the value of ``lastJSError`` needs to be captured - ## on procedure entry unique: int # for temp identifier generation blocks: seq[BlockInfo] ## enclosing exception handlers, finallys, and labeled blocks. Used @@ -244,12 +238,6 @@ proc indentLine(p: PProc, r: Rope): Rope = result.add " " result.add r -template line(p: PProc, added: string) = - p.body.add(indentLine(p, rope(added))) - -template line(p: PProc, added: Rope) = - p.body.add(indentLine(p, added)) - template lineF(p: PProc, frmt: FormatStr, args: varargs[Rope]) = p.body.add(indentLine(p, ropes.`%`(frmt, args))) @@ -697,11 +685,11 @@ proc genLineDir(p: PProc, n: CgNode) = if hasFrameInfo(p): lineF(p, "F.line = $1;$n", [rope(line)]) -proc handleJump(p: PProc, n: CgNode, fromError: bool): seq[BlockId] = - ## Makes sure the control-flow described by jump action description `n` - ## matches the actual JavaScript control-flow. If catch or finally - ## sections would be entered that shouldn't be, they're flagged as - ## requiring an "is enabled" guard and a boolean local is spawned. +proc handleErrorJump(p: PProc, n: CgNode): seq[BlockId] = + ## Makes sure the jump described by target node `n` matches the actual + ## JavaScript control-flow. If catch or finally sections would be entered + ## that shouldn't be, they're marked as requiring an "is enabled" guard + ## and a boolean local is spawned. ## ## Returns the list of sections that need to be disabled. @@ -710,8 +698,8 @@ proc handleJump(p: PProc, n: CgNode, fromError: bool): seq[BlockId] = yield (i, s[i]) template onMiss(idx: int, b: var BlockInfo) = - if b.kind == bkTryFinally or (b.kind == bkTryCatch and fromError): - # JavaScript control-flow enters a 'finally' or 'catch' it shouldn't. + if b.kind == bkTry: + # JavaScript control-flow enters a 'catch' it shouldn't. # The section needs to be disabled; a boolean flag is used for this. # Thanks to `var`, the local can be spawned without regards to # scoping. @@ -731,37 +719,11 @@ proc handleJump(p: PProc, n: CgNode, fromError: bool): seq[BlockId] = break # found the target else: onMiss(i, b) - - of cnkTargetList: - # marking blocks as having to restore the current exceptions is also done - # here - var - t = 0 - wasLeave = fromError - # ^^ exceptional control-flow could come from within a procedure call, where - # an exception handler boundary crossed - for i, b in mreverse(p.blocks): - wasLeave = wasLeave or n[t].kind == cnkLeave - # skip leave actions: - while n[t].kind == cnkLeave: - inc t - - if n[t].kind == cnkLabel and n[t].label == b.label: - inc t - if wasLeave: - b.flags.incl needsRecover - # the next jump target doesn't need to recover the exception, - # unless there's another leave action in-between - wasLeave = false - # stop searching when we reach the end - if t >= n.len: - break - else: - onMiss(i, b) - + of cnkResume: + discard "nothing to do" of cnkCheckedCall: # to reduce conditionals at the callsite - result = handleJump(p, n[^1], fromError) + result = handleErrorJump(p, n[^1]) else: unreachable() @@ -798,7 +760,7 @@ proc genExcept(p: PProc, n: CgNode) = [$id, orExpr]) p.nested: # disable the necessary sections before throwing - setEnabled(p, handleJump(p, n[^1], fromError=true), "false") + setEnabled(p, handleErrorJump(p, n[^1]), "false") lineF(p, "throw Exception$1_;\L", [$id]) lineF(p, "}\L") @@ -821,18 +783,8 @@ proc genExcept(p: PProc, n: CgNode) = proc genRaiseStmt(p: PProc, n: CgNode) = # disable the necessary sections before throwing: - setEnabled(p, handleJump(p, n[^1], fromError=true), "false") - if n[0].kind != cnkEmpty: - var a: TCompRes - gen(p, n[0], a) - genLineDir(p, n) - useMagic(p, "raiseException") - lineF(p, "raiseException($1);$n", - [a.rdLoc]) - else: - genLineDir(p, n) - useMagic(p, "reraiseException") - line(p, "reraiseException();\L") + setEnabled(p, handleErrorJump(p, n[^1]), "false") + lineF(p, "throw lastJSError;$n", []) func intLiteral(v: Int128, typ: PType): string = if typ.kind == tyBool: @@ -894,7 +846,6 @@ proc genCaseJS(p: PProc, desc: StructDesc, stmts: openArray[CgNode], n: CgNode) gen(p, desc, stmts, desc.inline[target]) else: # cannnot inline; a jump is needed - setEnabled(p, handleJump(p, it[^1], fromError=false), "false") lineF(p, "break Label$1;$n", [$target]) lineF(p, "}$n", []) @@ -2096,9 +2047,6 @@ proc genProcBody(p: PProc, prc: PSym): Rope = else: result = "" - if p.lastErrorBackupNeeded: - result.add(p.indentLine("var Exception0_ = lastJSError;$n" % [])) - result.add(p.body) if prc.typ.callConv == ccSysCall: result = ("try {$n$1} catch (e) {$n" & @@ -2217,15 +2165,6 @@ proc finishProc*(p: PProc): string = #if gVerbosity >= 3: # echo "END generated code for: " & prc.name.s -proc handleRecover(p: PProc, b: BlockInfo) = - if needsRecover in b.flags: - let nesting = p.numHandlers - lineF(p, "lastJSError = Exception$1_;$n", [$nesting]) - if nesting == 0: - # there's no enclosing 'catch'; the value of ``lastJSError`` needs to - # be captured on procedure entry - p.lastErrorBackupNeeded = true - proc handleSectionStart(p: PProc) = # wrap the section in an 'if' if it can be disabled at run-time (only the # opening is handled here) @@ -2240,13 +2179,6 @@ proc popBlock(p: PProc) = case blk.kind of bkBlock: endBlock(p) - # restore - handleRecover(p, blk) - of bkFinally: - if needsEnableFlag in blk.flags: - # close the wrapper 'if' and re-enable the section - endBlock(p, "} else { Enabled$1_ = true; }$n", $blk.label) - endBlock(p) of bkCatch: # the counterpart to the opening logic if needsEnableFlag in blk.flags: @@ -2256,7 +2188,7 @@ proc popBlock(p: PProc) = endBlock(p) # release the name: dec p.numHandlers - of bkTryCatch, bkTryFinally: + of bkTry: discard "nothing to do when exiting these" proc gen(p: PProc, desc: StructDesc, stmts: openArray[CgNode], start: int) = @@ -2293,9 +2225,7 @@ proc gen(p: PProc, desc: StructDesc, stmts: openArray[CgNode], start: int) = case it.kind of stkTry: p.blocks.add BlockInfo(label: it.label) - p.blocks[^1].kind = - if it.label in desc.finallys: bkTryFinally - else: bkTryCatch + p.blocks[^1].kind = bkTry startBlock(p, "try {$n", []) gen(it.stmt, structs[i+1].stmt) inc depth @@ -2310,20 +2240,14 @@ proc gen(p: PProc, desc: StructDesc, stmts: openArray[CgNode], start: int) = # indentation is managed by ``genStmt`` here gen(it.stmt, structs[i+1].stmt) inc depth - of stkCatch: + of stkCatch, stkFinally: + # MIR finally sections are too translated to JS 'catch' clauses endBlock(p) p.blocks[^1].kind = bkCatch # replace the try block inc p.numHandlers startBlock(p, "catch(Exception$1_) {$n", [$p.numHandlers]) handleSectionStart(p) gen(it.stmt, structs[i+1].stmt) - of stkFinally: - endBlock(p) - startBlock(p, "finally {$n", []) - p.blocks[^1].kind = bkFinally # replace the try block - handleSectionStart(p) - handleRecover(p, p.blocks[^1]) - gen(it.stmt, structs[i+1].stmt) of stkTerminator: let n = stmts[it.stmt] if n.kind == cnkCaseStmt: @@ -2416,7 +2340,7 @@ proc genStmt(p: PProc, n: CgNode) = if n.kind == cnkCheckedCall or (n.kind in {cnkAsgn, cnkFastAsgn, cnkDef} and n[1].kind == cnkCheckedCall): # XXX: somewhat hacky way to handle checked calls - let sections = handleJump(p, n[^1], fromError=true) + let sections = handleErrorJump(p, n[^1]) setEnabled(p, sections, "false") gen(p, n, r) if r.res != "": lineF(p, "$#;$n", [r.res]) @@ -2617,10 +2541,7 @@ proc gen(p: PProc, n: CgNode, r: var TCompRes) = of cnkType: r.res = genTypeInfo(p, n.typ) of cnkDef: genDef(p, n) of cnkGotoStmt: - setEnabled(p, handleJump(p, n[0], fromError=false), "false") - # jump directly to the final target. Placement of 'try' blocks made - # sure that finally sections are visited correctly - lineF(p, "break Label$1;$n", [$finalTarget(n[0]).label]) + lineF(p, "break Label$1;$n", [$n[0].label]) of cnkLoopJoinStmt: startBlock(p, "while (true) {$n") of cnkExcept: @@ -2650,9 +2571,12 @@ proc gen(p: PProc, n: CgNode, r: var TCompRes) = lineF(p, "($1);$n", [a.res]) of cnkAsmStmt, cnkEmitStmt: genAsmOrEmitStmt(p, n) of cnkRaiseStmt: genRaiseStmt(p, n) - of cnkJoinStmt, cnkEnd, cnkLoopStmt, cnkContinueStmt: + of cnkContinueStmt: + # end of finally section. Re-raise the caught exception + lineF(p, "throw Exception$1_;$n", [$p.numHandlers]) + of cnkJoinStmt, cnkEnd, cnkLoopStmt: discard "terminators or endings for which no special handling is needed" - of cnkInvalid, cnkMagic, cnkRange, cnkBinding, cnkLeave, cnkTargetList, + of cnkInvalid, cnkMagic, cnkRange, cnkBinding, cnkResume, cnkBranch, cnkAstLit, cnkLabel, cnkStmtList, cnkCaseStmt, cnkField: internalError(p.config, n.info, "gen: unknown node type: " & $n.kind) diff --git a/compiler/front/msgs.nim b/compiler/front/msgs.nim index 79b785b28da..721604386be 100644 --- a/compiler/front/msgs.nim +++ b/compiler/front/msgs.nim @@ -1016,6 +1016,16 @@ proc quotedFilename*(conf: ConfigRef; i: TLineInfo): Rope = else: result = conf[i.fileIndex].quotedName +proc unquotedFilename*(conf: ConfigRef, i: TLineInfo): Rope = + ## Returns the unqouted filename for `i`, for the purpose of being used for + ## run-time stacktraces. + if i.fileIndex.int32 < 0: + result = "???" + elif optExcessiveStackTrace in conf.globalOptions: + result = conf[i.fileIndex].fullPath.string + else: + result = conf[i.fileIndex].shortName + proc listWarnings*(conf: ConfigRef) = conf.localReport(InternalReport( kind: rintListWarnings, diff --git a/compiler/mir/mirgen.nim b/compiler/mir/mirgen.nim index a071314d883..0367f555648 100644 --- a/compiler/mir/mirgen.nim +++ b/compiler/mir/mirgen.nim @@ -101,6 +101,8 @@ import std/options as std_options when defined(nimCompilerStacktraceHints): import compiler/utils/debugutils +from compiler/front/msgs import unquotedFilename, toLinenumber + type DestFlag = enum ## Extra information about an assignment destination. The flags are used to @@ -164,6 +166,7 @@ type # input: userOptions: set[TOption] graph: ModuleGraph + owner: PSym config: TranslationConfig @@ -1265,37 +1268,51 @@ proc genRaise(c: var TCtx, n: PNode) = # skip the 'materialize' node genx(c, e, e.high - 1, fromMove=true) - # emit the preparation code: + # raising an exception consists of two parts: + # 1. filling in various state for it (stacktrace, name, etc.) and pushing + # it to the exception stack + # 2. unwinding to the next handler + # The first part is done with a call to the relevant exception runtime + # procedure. The second part is done by ``mnkRaise``. let typ = skipTypes(n[0].typ, abstractPtrs) - cp = c.graph.getCompilerProc("prepareException") + cp = c.graph.getCompilerProc("raiseExceptionEx") c.buildStmt mnkVoid: c.builder.buildCall c.env.procedures.add(cp), VoidType: - c.subTree mnkArg: + c.subTree mnkConsume: # lvalue conversion to the base ``Exception`` type: c.buildTree mnkPathConv, c.typeToMir(cp.typ[1]): c.use tmp + # exception name: c.emitByVal strLiteral(c.env, typ.sym.name.s, CstringType) - - # emit the raise statement: - c.buildStmt mnkRaise: - c.use tmp - raiseExit(c) + # procedure name: + if c.owner.isNil: + c.emitByVal strLiteral(c.env, "???", CstringType) + else: + c.emitByVal strLiteral(c.env, c.owner.name.s, CstringType) + # file name: + c.emitByVal strLiteral(c.env, unquotedFilename(c.graph.config, n.info), + CstringType) + # file number: + c.emitByVal intLiteral(c.env, toLinenumber(n.info), + c.env.types.sizeType) else: # a re-raise statement - c.buildStmt mnkRaise: - c.add MirNode(kind: mnkNone) - raiseExit(c) + let cp = c.graph.getCompilerProc("reraiseException") + c.buildStmt mnkVoid: + c.builder.buildCall c.env.procedures.add(cp), VoidType: + discard + c.buildStmt mnkRaise: + raiseExit(c) proc genReturn(c: var TCtx, n: PNode) = assert n.kind == nkReturnStmt if n[0].kind != nkEmpty: gen(c, n[0]) - c.buildStmt mnkGoto: - blockExit(c.blocks, c.builder, 0) + blockExit(c.blocks, c.graph, c.env, c.builder, 0) proc genAsgnSource(c: var TCtx, e: PNode, status: set[DestFlag]) = ## Generates the MIR code for the right-hand side of an assignment. @@ -1560,11 +1577,6 @@ template withBlock(c: var TCtx, k: BlockKind, body: untyped) = body c.closeBlock() -template withBlock(c: var TCtx, k: BlockKind, lbl: LabelId, body: untyped) = - c.blocks.add Block(kind: k, id: some lbl) - body - c.closeBlock() - proc genBlock(c: var TCtx, n: PNode, dest: Destination) = ## Generates and emits the MIR code for a ``block`` expression or statement. ## A block translates to a scope and, optionally, a join. @@ -1588,12 +1600,7 @@ proc genBranch(c: var TCtx, n: PNode, dest: Destination) = proc leaveBlock(c: var TCtx) = ## Emits a goto for jumping to the exit of first enclosing block. - if c.scopeDepth > 0: - # only emit the early scope exit if still within a scope - earlyExit(c.blocks, c.builder) - - c.subTree mnkGoto: - blockExit(c.blocks, c.builder, closest(c.blocks)) + blockExit(c.blocks, c.graph, c.env, c.builder, closest(c.blocks)) proc genScopedBranch(c: var TCtx, n: PNode, dest: Destination, withLeave: bool) = @@ -1738,9 +1745,39 @@ proc genExceptBranch(c: var TCtx, n: PNode, label: LabelId, # continue raising raiseExit(c) + # emit the setup for the handler frame: + let + p = c.graph.getCompilerProc("nimCatchException") + tmp = c.allocTemp(c.typeToMir(p.typ[1][0])) + c.subTree mnkDef: + c.use tmp + c.add MirNode(kind: mnkNone) + + let + typ = c.typeToMir(p.typ[1]) + arg = c.wrapTemp typ: + c.buildTree mnkAddr, typ: + c.use tmp + + c.subTree mnkVoid: + c.builder.buildCall c.env.procedures.add(p), VoidType: + c.emitByVal arg + # generate the body of the except branch: - c.withBlock bkExcept, label: - c.genScopedBranch(n.lastSon, dest, withLeave=true) + c.blocks.add Block(kind: bkExcept) + c.genScopedBranch(n.lastSon, dest, withLeave=true) + let exc = c.blocks.pop() + if exc.id.isSome: + # the handler may be exited via an exception at run-time -> a finally is + # needed + c.subTree mnkFinally: + c.add labelNode(exc.id.unsafeGet) + c.subTree mnkVoid: + let p = c.graph.getCompilerProc("nimLeaveExcept") + c.builder.buildCall c.env.procedures.add(p), VoidType: + discard + c.subTree mnkContinue: + raiseExit(c) c.subTree mnkEndStruct: c.add labelNode(label) @@ -1765,26 +1802,105 @@ proc genExcept(c: var TCtx, n: PNode, len: int, dest: Destination) = c.genExceptBranch(n[i], curr, none LabelId, dest) proc genFinally(c: var TCtx, n: PNode) = - let blk = c.blocks.pop() - if blk.id.isNone: + let + exc = c.blocks.pop() + blk = c.blocks.pop() + + if blk.id.isNone and exc.id.isNone: # the finally is never entered, omit it return c.builder.useSource(c.sp, n) - c.subTree mnkFinally: - c.add labelNode(blk.id.unsafeGet) + + if exc.id.isSome: + # the handler catches the exception and sets the selector for the + # finally's outgoing target accordingly + c.subTree mnkExcept: + c.add labelNode(exc.id.unsafeGet) + if blk.selector.isSome: + c.subTree mnkInit: + c.use blk.selector.unsafeGet + c.use intLiteral(c.env, blk.exits.len, UInt32Type) + c.subTree mnkEndStruct: + c.add labelNode(exc.id.unsafeGet) + + if blk.id.isSome: + # entry point for break/return/normal try exits + c.join blk.id.unsafeGet + + if exc.id.isSome: + # the exception through which the finally was entered might need to be + # aborted + c.blocks.add Block(kind: bkFinally, + selectorVar: blk.selector.unsafeGet, + excState: blk.exits.len) # translate the body: - c.withBlock bkFinally, blk.id.unsafeGet: - c.scope(not blk.doesntExit): - c.gen(n[^1]) + c.scope(doesReturn(n[^1])): + c.gen(n[^1]) + + if exc.id.isSome: + let err = c.blocks.pop() + # emit the abort handler if needed. When an exception is raised from the + # finally clause, the previously in-flight exception needs to be aborted + if err.id.isSome: + var next: LabelId + if doesReturn(n[^1]): + next = c.allocLabel() + c.builder.goto next # jump over the finally - # the continue statement is always necessary, even if the body has no - # structured exit - c.subTree mnkContinue: - c.add labelNode(blk.id.unsafeGet) - for it in blk.exits.items: - c.add labelNode(it) + c.subTree mnkFinally: + c.add labelNode(err.id.unsafeGet) + let val = c.wrapTemp BoolType: + c.buildMagicCall mEqI, BoolType: + c.emitByVal blk.selector.unsafeGet + c.emitByVal intLiteral(c.env, blk.exits.len, UInt32Type) + c.builder.buildIf (;c.use val): + c.subTree mnkVoid: + let p = c.graph.getCompilerProc("nimAbortException") + c.builder.buildCall c.env.procedures.add(p), VoidType: + c.emitByVal intLiteral(c.env, 1, BoolType) + c.subTree mnkContinue: + raiseExit(c) + + if doesReturn(n[^1]): + c.join next + + if doesReturn(n[^1]): + # emit the dispatcher. The finally body not being noreturn implies the + # existence of a selector + var labels = newSeq[LabelId](blk.exits.len + 1) + c.subTree mnkCase: + c.use blk.selector.unsafeGet + for i, it in blk.exits.pairs: + c.subTree mnkBranch: + c.use intLiteral(c.env, i, UInt32Type) + labels[i] = c.builder.allocLabel() + c.add labelNode(labels[i]) + + if exc.id.isSome: + labels[^1] = c.builder.allocLabel() + c.subTree mnkBranch: + c.use intLiteral(c.env, labels.high, UInt32Type) + c.add labelNode(labels[^1]) + + # emit the branch bodies. The dispatcher cannot jump directly to target + # blocks, since there may be leave actions that need to take place + for i, it in blk.exits.pairs: + c.join labels[i] + blockExit(c.blocks, c.graph, c.env, c.builder, it) + + if exc.id.isSome: + # resume raising the in-flight exception. Using a re-raise would be + # wrong, because the exception wasn't (technically) caught yet + c.join labels[^1] + c.subTree mnkRaise: + raiseExit(c) + + elif exc.id.isSome: + # always resume raising the exception + c.subTree mnkRaise: + raiseExit(c) proc genTry(c: var TCtx, n: PNode, dest: Destination) = let @@ -1795,10 +1911,17 @@ proc genTry(c: var TCtx, n: PNode, dest: Destination) = c.blocks.add Block(kind: bkBlock) if hasFinally: - # the finally clause also applies to the except clauses, so it's - # pushed first - c.blocks.add Block(kind: bkTryFinally, - doesntExit: not doesReturn(n[^1][0])) + # a selector is needed for both the exception aborting and dispatcher. We + # know whether a dispatcher is needed already, but not whether there can + # be an exception. Therefore a selector is always generated + let selector = c.allocTemp(UInt32Type) + c.buildStmt mnkDef: + c.use selector + c.add MirNode(kind: mnkNone) + c.blocks.add Block(kind: bkTryFinally, selector: some(selector)) + + # for translation of 'finally's, an additional exception handler is needed + c.blocks.add Block(kind: bkTryExcept) if hasExcept: c.blocks.add Block(kind: bkTryExcept) @@ -2140,8 +2263,8 @@ proc gen(c: var TCtx, n: PNode) = of nkPragmaBlock: gen(c, n.lastSon) of nkBreakStmt: - c.buildStmt mnkGoto: - blockExit(c.blocks, c.builder, findBlock(c.blocks, n[0].sym)) + blockExit(c.blocks, c.graph, c.env, c.builder, + findBlock(c.blocks, n[0].sym)) of nkVarSection, nkLetSection: genVarSection(c, n) of nkAsgn: @@ -2247,7 +2370,7 @@ proc genWithDest(c: var TCtx, n: PNode; dest: Destination) = proc initCtx(graph: ModuleGraph, config: TranslationConfig, owner: PSym, env: sink MirEnv): TCtx = - result = TCtx(graph: graph, config: config, env: move env) + result = TCtx(graph: graph, config: config, owner: owner, env: move env) if owner != nil: result.userOptions = owner.options result.injectDestructors = @@ -2334,7 +2457,7 @@ proc generateCode*(graph: ModuleGraph, env: var MirEnv, owner: PSym, if needsCleanup: # the result variable only needs to be cleaned up when the procedure # exits via an exception - c.blocks.add Block(kind: bkTryFinally, errorOnly: true) + c.blocks.add Block(kind: bkTryExcept) c.scope(doesReturn): if owner.kind in routineKinds: @@ -2380,7 +2503,7 @@ proc generateCode*(graph: ModuleGraph, env: var MirEnv, owner: PSym, # guaranteed that no one can observe the result location when the # procedure raises c.subTree mnkContinue: - c.add labelNode(b.id.unsafeGet) + raiseExit(c) if needsTerminate and (let b = c.blocks.pop(); b.id.isSome): if doesReturn and isFirst: diff --git a/compiler/mir/mirgen_blocks.nim b/compiler/mir/mirgen_blocks.nim index 8772505049b..58f0b459f1c 100644 --- a/compiler/mir/mirgen_blocks.nim +++ b/compiler/mir/mirgen_blocks.nim @@ -12,10 +12,13 @@ import ], compiler/mir/[ mirconstr, - mirtrees + mirenv, + mirtrees, + mirtypes ], - compiler/utils/[ - idioms + compiler/modules/[ + magicsys, + modulegraphs ] type @@ -40,16 +43,18 @@ type of bkScope: numRegistered: int ## number of entities registered for the scope in the to-destroy list - scopeExits: seq[LabelId] - ## unordered set of follow-up targets of bkTryFinally: - doesntExit*: bool - ## whether structured control-flow doesn't reach the end of the finally - errorOnly*: bool - ## whether only exceptional control-flow is intercepted - exits*: seq[LabelId] - ## unordered set of follow-up targets - of bkTryExcept, bkFinally, bkExcept: + selector*: Option[Value] + ## the variable to store the destination index in + exits*: seq[int] + ## a set of all original target block indices + of bkFinally: + selectorVar*: Value + ## the selector storing the dispatcher's target index + excState*: int + ## the selector value representing the "finally entered via exception" + ## state + of bkTryExcept, bkExcept: discard BlockCtx* = object @@ -81,81 +86,71 @@ proc emitDestroy(bu; val: Value) = bu.subTree mnkDestroy: bu.use val -proc emitFinalizerLabels(c; bu; locs: Slice[int]) = - ## Emits the labels for all scope finalizers required for cleaning up the - ## registered entities in `locs`. - # destruction happens in reverse, so iterate from high to low - for i in countdown(locs.b, locs.a): - if c.toDestroy[i].label.isSome: - bu.add labelNode(c.toDestroy[i].label.unsafeGet) - -proc blockLeaveActions(c; bu; targetBlock: int): bool = - ## Emits the actions for leaving the blocks up until (but not including) - ## `targetBlock`. Returns false when there's an intercepting - ## ``finally`` clause that doesn't exit (meaning that `targetBlock` won't - ## be reached), true otherwise. - proc incl[T](s: var seq[T], it: T) {.inline.} = - if it notin s: - s.add it - - proc inclExit(b: var Block, it: LabelId) {.inline.} = - case b.kind - of bkTryFinally: b.exits.incl it - of bkScope: b.scopeExits.incl it - else: unreachable() +proc isInFinally*(c: BlockCtx): bool = + c.blocks.len > 0 and c.blocks[^1].kind == bkFinally + +proc blockExit*(c; graph: ModuleGraph; env: var MirEnv; bu; targetBlock: int) = + ## Emits a goto jumping to the `targetBlock`, together with the necessary scope + ## and exception cleanup logic. If the jump crosses a try/finally, the + ## finally is jumped to instead. + proc incl[T](s: var seq[T], val: T): int = + for i, it in s.pairs: + if it == val: + return i + + result = s.len + s.add val - var - last = c.toDestroy.high - previous = -1 + var last = c.toDestroy.high for i in countdown(c.blocks.high, targetBlock + 1): let b {.cursor.} = c.blocks[i] case b.kind - of bkBlock, bkTryExcept: - discard "nothing to do" - of bkExcept, bkFinally: - # needs a leave action - bu.add MirNode(kind: mnkLeave, label: b.id.get) of bkScope: - if b.numRegistered > 0: - # there are some locations that require cleanup - if c.toDestroy[last].label.isNone: - c.toDestroy[last].label = some bu.allocLabel() + let start = last - b.numRegistered + var j = last + while j > start: + bu.emitDestroy(c.toDestroy[j].entity) + dec j - if previous != -1: - c.blocks[previous].inclExit c.toDestroy[last].label.unsafeGet - - previous = i - # emit the labels for all scope finalizers that need to be run - emitFinalizerLabels(c, bu, (last-b.numRegistered+1)..last) - - last -= b.numRegistered + last = start of bkTryFinally: - if c.blocks[i].errorOnly and targetBlock >= 0 and - c.blocks[targetBlock].kind != bkTryExcept: - # ignore the finally; it only applies to exceptional control-flow - continue - - let label = bu.requestLabel(c.blocks[i]) - # register as outgoing edge of the preceding finally (if any): - if previous != -1: - c.blocks[previous].inclExit label - - previous = i - - # enter the finally clause: - bu.add labelNode(label) - if b.doesntExit: - # structured control-flow doesn't leave the finally; the finally is - # the final jump target - return false - - if targetBlock >= 0 and previous != -1 and - c.blocks[targetBlock].kind in {bkBlock, bkTryExcept}: - # register the target as the follow-up for the previous finally - c.blocks[previous].inclExit bu.requestLabel(c.blocks[targetBlock]) + if b.selector.isSome: + # add the target as an exit of the try: + let pos = c.blocks[i].exits.incl(targetBlock) + bu.subTree mnkAsgn: + bu.use b.selector.unsafeGet + bu.use literal(mnkIntLit, env.getOrIncl(pos.BiggestInt), UInt32Type) + + # enter to the intercepting finally + bu.subTree mnkGoto: + bu.add labelNode(bu.requestLabel(c.blocks[i])) + return + of bkFinally: + # emit a conditional abort: + let tmp = bu.wrapTemp BoolType: + bu.buildMagicCall mEqI, BoolType: + bu.emitByVal b.selectorVar + bu.emitByVal: + literal(mnkIntLit, env.getOrIncl(b.excState.BiggestInt), + UInt32Type) + + bu.buildIf (;bu.use tmp): + bu.subTree mnkVoid: + let p = graph.getCompilerProc("nimAbortException") + bu.buildCall env.procedures.add(p), VoidType: + bu.emitByVal literal(mnkIntLit, env.getOrIncl(0), BoolType) + of bkExcept: + bu.subTree mnkVoid: + let p = graph.getCompilerProc("nimLeaveExcept") + bu.buildCall env.procedures.add(p), VoidType: + discard + of bkBlock, bkTryExcept: + discard "nothing to do" - result = true + # no intercepting finally exists + bu.subTree mnkGoto: + bu.add labelNode(bu.requestLabel(c.blocks[targetBlock])) template add*(c: var BlockCtx; b: Block) = c.blocks.add b @@ -178,28 +173,32 @@ proc findBlock*(c: BlockCtx, label: PSym): int = assert i >= 0, "no enclosing block?" result = i -proc blockExit*(c; bu; targetBlock: int) = - ## Emits the jump target description for a jump to `targetBlock`. - # XXX: a target list is only necessary if there's more than one jump - # target - bu.subTree mnkTargetList: - if blockLeaveActions(c, bu, targetBlock): - bu.add labelNode(bu.requestLabel(c.blocks[targetBlock])) proc raiseExit*(c; bu) = - ## Emits the jump target description for a jump to the nearest enclosing + ## Emits the jump target for a jump to the nearest enclosing ## exception handler. - var i = c.blocks.high - while i >= 0 and c.blocks[i].kind != bkTryExcept: - dec i + var last = c.toDestroy.high - bu.subTree mnkTargetList: - if blockLeaveActions(c, bu, i): - if i == -1: - # nothing handles the exception within the current procedure - bu.add MirNode(kind: mnkResume) - else: - bu.add labelNode(bu.requestLabel(c.blocks[i])) + for i in countdown(c.blocks.high, 0): + let b {.cursor.} = c.blocks[i] + case b.kind + of bkBlock: + discard "nothing to do" + of bkScope: + if b.numRegistered > 0: + # there are some locations that require cleanup + if c.toDestroy[last].label.isNone: + c.toDestroy[last].label = some bu.allocLabel() + + bu.add labelNode(c.toDestroy[last].label.unsafeGet) + return + of bkTryExcept, bkTryFinally, bkFinally, bkExcept: + # something that intercepts the exceptional control-flow + bu.add labelNode(bu.requestLabel(c.blocks[i])) + return + + # no local exception handler exists + bu.add MirNode(kind: mnkResume) proc closeBlock*(c; bu): bool = ## Finishes the current block. If required for the block (because it is a @@ -224,20 +223,6 @@ proc startScope*(c): int = c.blocks.add Block(kind: bkScope) c.currScope = c.blocks.high -proc earlyExit*(c; bu) = - ## Emits the destroy operations for when structured control-flow reaches the - ## current scope's end. All entities for which a destroy operation is - ## emitted are unregistered already. - let start = c.toDestroy.len - c.blocks[c.currScope].numRegistered - var i = c.toDestroy.high - - while i >= start and c.toDestroy[i].label.isNone: - bu.emitDestroy(c.toDestroy[i].entity) - dec i - - # unregister the entities for which a destroy operation was emitted: - c.blocks[c.currScope].numRegistered = i - start + 1 - c.toDestroy.setLen(i + 1) proc closeScope*(c; bu; nextScope: int, hasStructuredExit: bool) = ## Pops the scope from the stack and emits the scope exit actions. @@ -247,58 +232,62 @@ proc closeScope*(c; bu; nextScope: int, hasStructuredExit: bool) = ## `next` is the index of the scope index returns by the previous ## `startScope <#startScope,BlockCtx>`_ call. # emit all destroy operations that don't need a finally - earlyExit(c, bu) - var scope = c.blocks.pop() assert scope.kind == bkScope let start = c.toDestroy.len - scope.numRegistered - var next = none LabelId - if start < c.toDestroy.len and hasStructuredExit: - # there are destroy operations that need a finally. A goto is required - # for visiting them - next = some bu.allocLabel() - bu.subTree mnkGoto: - bu.subTree mnkTargetList: - emitFinalizerLabels(c, bu, start..c.toDestroy.high) - bu.add labelNode(next.unsafeGet) - - scope.scopeExits.add next.unsafeGet - - # emit all finally sections for the scope. Since not all entities requiring - # destruction necessarily start their existence at the start of the scope, - # multiple sections may be required - var curr = none LabelId - for i in countdown(c.toDestroy.high, start): - # if a to-destroy entry has a label, it marks the start of a new finally - if c.toDestroy[i].label.isSome: - if curr.isSome: - # finish the previous finally by emitting the corresponding 'continue': - bu.subTree MirNode(kind: mnkContinue, len: 2): + if hasStructuredExit: + var i = c.toDestroy.high + while i >= start: + bu.emitDestroy(c.toDestroy[i].entity) + dec i + + # look for the first destroy that needs a 'finally' + var i = c.toDestroy.high + while i >= start and c.toDestroy[i].label.isNone: + dec i + + if i >= start: + # some exceptional exits need cleanup + var next = none LabelId + if hasStructuredExit: + # emit a jump over the finalizers: + next = some bu.allocLabel() + bu.goto next.unsafeGet + + # emit all finally sections for the scope. Since not all entities requiring + # destruction necessarily start their existence at the start of the scope, + # multiple sections may be required + var curr = none LabelId + for i in countdown(i, start): + # if a to-destroy entry has a label, it marks the start of a new finally + if c.toDestroy[i].label.isSome: + if curr.isSome: + # finish the previous finally by emitting the corresponding + # 'continue': + bu.subTree mnkContinue: + bu.add labelNode(c.toDestroy[i].label.unsafeGet) + + curr = c.toDestroy[i].label + bu.subTree mnkFinally: bu.add labelNode(curr.unsafeGet) - # a finally section that's not the last one always continues with - # the next finally - bu.add labelNode(c.toDestroy[i].label.unsafeGet) - - curr = c.toDestroy[i].label - bu.subTree mnkFinally: - bu.add labelNode(curr.unsafeGet) - - bu.emitDestroy(c.toDestroy[i].entity) - - if curr.isSome: - # finish the final finally. `scopeExits` stores all possible follow-up - # targets for the finally - bu.subTree MirNode(kind: mnkContinue, len: uint32(1 + scope.scopeExits.len)): - bu.add labelNode(curr.unsafeGet) - for it in scope.scopeExits.items: - bu.add labelNode(it) - - if next.isSome: - # the join point for the structured scope exit - bu.join next.unsafeGet - - # unregister all entities registered with the scope: - c.toDestroy.setLen(start) + + bu.emitDestroy(c.toDestroy[i].entity) + + # unregister all entities registered with the scope. This needs to happen + # before the ``raiseExitActions`` call below + c.toDestroy.setLen(start) + + if curr.isSome: + # continue raising the exception + bu.subTree mnkContinue: + raiseExit(c, bu) + + if next.isSome: + bu.join next.unsafeGet + else: + # unregister all entities registered with the scope: + c.toDestroy.setLen(start) + c.currScope = nextScope diff --git a/compiler/mir/mirpasses.nim b/compiler/mir/mirpasses.nim index cbb5ab38bf8..7a033ac6a89 100644 --- a/compiler/mir/mirpasses.nim +++ b/compiler/mir/mirpasses.nim @@ -809,8 +809,7 @@ proc splitAssignments(tree: MirTree, changes: var Changeset) = # * if the destination is a local, does the exceptional path enter a # local exception handler? if tree[tree.getRoot(tree.operand(p, 0))].kind notin Locals or - tree[target].kind != mnkTargetList or - tree[tree.last(target)].kind != mnkResume: + tree[target].kind != mnkResume: # future direction: this can be optimized. The assignment only needs to # be split if the assignment destination's value is observed on the # exceptional control-flow path diff --git a/compiler/mir/mirtrees.nim b/compiler/mir/mirtrees.nim index ad27128e6a8..75cb6331ebc 100644 --- a/compiler/mir/mirtrees.nim +++ b/compiler/mir/mirtrees.nim @@ -90,9 +90,6 @@ type mnkResume ## special action in a target list that means "resume ## exception handling in caller" - mnkLeave ## a leave action within a target list - mnkTargetList## describes the actions to perform prior to jumping, as well - ## as the final jump mnkDef ## marks the start of existence of a local, global, procedure, ## or temporary. Supports an optional intial value (except for @@ -175,9 +172,8 @@ type # future direction: the arithmetic operations should also apply to # unsigned integers - mnkRaise ## if the operand is an ``mnkNone`` node, reraises the - ## currently active exception. Otherwise, consumes the operand - ## and sets it as the active exception + mnkRaise ## starts exceptional control-flow and jumps to the specified + ## handler mnkSetConstr ## constructor for set values mnkRange ## range constructor. May only appear in set constructions @@ -276,7 +272,7 @@ type strVal*: StringId of mnkAstLit: ast*: AstId - of mnkLabel, mnkLeave: + of mnkLabel: label*: LabelId of mnkImmediate: imm*: uint32 ## meaning depends on the context @@ -284,7 +280,7 @@ type magic*: TMagic of mnkNone, mnkNilLit, mnkType, mnkResume: discard - of {low(MirNodeKind)..high(MirNodeKind)} - {mnkNone .. mnkLeave}: + of {low(MirNodeKind)..high(MirNodeKind)} - {mnkNone..mnkResume}: len*: uint32 MirTree* = seq[MirNode] @@ -309,7 +305,7 @@ const ## Node kinds that represent definition statements (i.e. something that ## introduces a named entity) - AtomNodes* = {mnkNone..mnkLeave} + AtomNodes* = {mnkNone..mnkResume} ## Nodes that don't support sub nodes. SubTreeNodes* = AllNodeKinds - AtomNodes @@ -317,7 +313,7 @@ const SingleOperandNodes* = {mnkPathNamed, mnkPathPos, mnkPathVariant, mnkPathConv, mnkAddr, mnkDeref, mnkView, mnkDerefView, mnkStdConv, - mnkConv, mnkCast, mnkRaise, mnkArg, + mnkConv, mnkCast, mnkArg, mnkName, mnkConsume, mnkVoid, mnkCopy, mnkMove, mnkSink, mnkDestroy, mnkMutView, mnkToMutSlice} ## Nodes that start sub-trees but that always have a single sub node. @@ -329,7 +325,7 @@ const ## Assignment modifiers. Nodes that can only appear directly in the source ## slot of assignments. - LabelNodes* = {mnkLabel, mnkLeave} + LabelNodes* = {mnkLabel} LiteralDataNodes* = {mnkNilLit, mnkIntLit, mnkUIntLit, mnkFloatLit, mnkStrLit, mnkAstLit} @@ -421,7 +417,7 @@ template `[]`*(tree: MirTree, i: NodePosition | OpValue): untyped = template isAtom(kind: MirNodeKind): bool = # much faster than an `in SubTreeNodes` test - ord(kind) <= ord(mnkLeave) + ord(kind) <= ord(mnkResume) func parent*(tree: MirTree, n: NodePosition): NodePosition = result = n diff --git a/compiler/mir/utils.nim b/compiler/mir/utils.nim index 4a4362feb80..a4d66ed64a9 100644 --- a/compiler/mir/utils.nim +++ b/compiler/mir/utils.nim @@ -53,7 +53,7 @@ func `$`(n: MirNode): string = of mnkMagic: result.add " magic: " result.add $n.magic - of mnkLabel, mnkLeave: + of mnkLabel: result.add " label: " result.addInt n.label.uint32 of mnkImmediate: @@ -241,7 +241,7 @@ proc singleToStr(n: MirNode, result: var string, c: RenderCtx) = result.add "type(" typeToStr(result, n.typ, c.env) result.add ")" - of AllNodeKinds - Atoms - mnkProc + {mnkResume, mnkLeave}: + of AllNodeKinds - Atoms - mnkProc + {mnkResume}: result.error(n) proc singleToStr(tree: MirTree, i: var int, result: var string, c: RenderCtx) = @@ -348,18 +348,11 @@ proc targetToStr(nodes: MirTree, i: var int, result: var string) = var n {.cursor.} = next(nodes, i) case n.kind of mnkLabel: - result.add n.label - of mnkTargetList: result.add "[" - commaSeparated n.len: - n = next(nodes, i) - case n.kind - of mnkLabel: result.add n.label - of mnkLeave: result.add "Leave(L" & $n.label.int & ")" - of mnkResume: result.add "Resume" - else: result.error(n) - + result.add n.label result.add "]" + of mnkResume: + result.add "[Resume]" else: result.error(n) @@ -610,9 +603,7 @@ proc stmtToStr(nodes: MirTree, i: var int, indent: var int, result: var string, exprToStr() result.add "\n" of mnkRaise: - tree "raise ": - valueToStr() - result.add " -> " + tree "raise -> ": targetToStr() result.add "\n" of mnkDestroy: @@ -636,13 +627,8 @@ proc stmtToStr(nodes: MirTree, i: var int, indent: var int, result: var string, result.add ":\n" of mnkContinue: tree "continue ": - inc i # skip the label - result.add "{" - for j in 1.. 1: - result.add ", " - labelToStr(nodes, i, result) - result.add "}\n" + targetToStr(nodes, i, result) + result.add "\n" dec indent of AllNodeKinds - StmtNodes: diff --git a/compiler/sem/injectdestructors.nim b/compiler/sem/injectdestructors.nim index 2edee0e4c2c..781c679fdcb 100644 --- a/compiler/sem/injectdestructors.nim +++ b/compiler/sem/injectdestructors.nim @@ -551,8 +551,6 @@ proc rewriteAssignments(tree: MirTree, ctx: AnalyseCtx, ar: AnalysisResults, of mnkConsume: # we must be processing a call/construction argument consumeArg(tree, ctx, ar, tree.parent(parent), val, i, c) - of mnkRaise: - consumeArg(tree, ctx, ar, NodePosition val, val, i, c) of mnkMove, mnkSink: # assignments are handled separately discard diff --git a/compiler/sem/mirexec.nim b/compiler/sem/mirexec.nim index ff9e68f1b0a..42517d128c0 100644 --- a/compiler/sem/mirexec.nim +++ b/compiler/sem/mirexec.nim @@ -170,19 +170,6 @@ func `[]`(c: DataFlowGraph, pc: SomeInteger): lent Instr {.inline.} = # ---- data-flow graph setup ---- -proc firstTarget(tree: MirTree, n: NodePosition): NodePosition = - ## Returns the first label or resume in the jump target description. - case tree[n].kind - of mnkLabel: - result = n - of mnkTargetList: - for p in subNodes(tree, n): - if tree[p].kind in {mnkResume, mnkLabel}: - return p - unreachable("ill-formed target list") - else: - unreachable(tree[n].kind) - func map(env: var ClosureEnv, id: LabelId): JoinId = if id in env.labelToJoin: result = env.labelToJoin[id] @@ -210,7 +197,6 @@ func getResumeLabel(env: var ClosureEnv): JoinId = func raiseExit(env: var ClosureEnv, opc: Opcode, tree: MirTree, at, target: NodePosition) = - let target = firstTarget(tree, target) # compute the join ID to use, accounting for the special 'resume' action: let join = case tree[target].kind @@ -262,7 +248,7 @@ func emitForArgs(env: var ClosureEnv, tree: MirTree, at, source: NodePosition) = case tree[it].kind of mnkArg, mnkConsume, mnkName: emitForArg(env, tree, at, it) - of mnkMagic, mnkProc, mnkLabel, mnkTargetList, mnkImmediate: + of mnkMagic, mnkProc, mnkLabel, mnkImmediate, mnkResume: discard else: emitLvalueOp(env, opUse, tree, at, OpValue it) @@ -410,9 +396,7 @@ func computeDfg*(tree: MirTree): DataFlowGraph = for i, n in tree.pairs: case n.kind of mnkGoto: - let first = tree.firstTarget(tree.child(i, 0)) - # the node for the target is guaranteed to be a label - goto i, tree[first].label + goto i, tree[i, 0].label of mnkLoop: loop i, tree[i, 0].label of mnkIf: @@ -449,29 +433,8 @@ func computeDfg*(tree: MirTree): DataFlowGraph = raiseExit(env, opFork, tree, i, tree.child(i, n.len - 1)) of mnkFinally: join i, tree[i, 0].label - of mnkContinue: - var j = 0 - # a continue acts much like a dispatcher - for it in subNodes(tree, i): - if j == 0: - discard "label of the associated finally; ignore" - elif j < n.len.int - 1: - fork i, tree[it].label - else: - goto i, tree[it].label - inc j - - if n.len == 1: - # no follow-up targets means that the finally continues exceptional - # control-flow in the caller - let target = env.getResumeLabel() - env.instrs.add Instr(op: opGoto, node: i, dest: target) - of mnkRaise: - # raising an exception consumes it: - if tree[tree.operand(i)].kind != mnkNone: - emitLvalueOp(env, opConsume, tree, i, tree.operand(i)) - - raiseExit(env, opGoto, tree, i, tree.child(i, 1)) + of mnkContinue, mnkRaise: + raiseExit(env, opGoto, tree, i, tree.child(i, 0)) of mnkEndStruct: # emit a join at the end of an 'if' if ifs.len > 0 and tree[i, 0].label == ifs[^1]: diff --git a/compiler/vm/vm.nim b/compiler/vm/vm.nim index 548e5d61372..8fce437c94c 100644 --- a/compiler/vm/vm.nim +++ b/compiler/vm/vm.nim @@ -82,13 +82,6 @@ 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 @@ -101,9 +94,9 @@ type loopIterations: int ## the number of remaining jumps backwards - currentException: HeapSlotHandle - ## the exception ref that's returned when querying the current exception - ehStack: seq[tuple[ex: VmException, pc: uint32]] + exState: ExceptionState + ## the data for the exception runtime + ehStack: seq[uint32] ## the stack of currently executed EH threads. A stack is needed since ## exceptions can be raised while another exception is in flight @@ -150,15 +143,10 @@ 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, @@ -223,11 +211,6 @@ template `[]=`(r: Registers, i: SomeInteger, val: TFullReg) = regIndexCheck(r, i) r.data[x] = val -func getReg(t: var VmThread, i: int): var TFullReg {.inline.} = - ## Shortcut for accessing the the `i`-th register belonging to the topmost - ## stack frame. - t.regs[t.sframes[^1].start + i] - func setNodeValue(dest: LocHandle, node: PNode) = assert dest.typ.kind == akPNode deref(dest).nodeVal = node @@ -536,81 +519,50 @@ proc findEh(c: TCtx, t: VmThread, at: PrgCtr, frame: int # no handler exists -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) - - t.currentException = ex - -proc decodeControl(x: BiggestInt): tuple[fromEh: bool, val: uint32] = - let x = cast[BiggestUInt](x) - result.fromEh = bool(x shr 63) - result.val = uint32(x) - -proc runEh(t: var VmThread, c: var TCtx): Result[PrgCtr, VmException] = +proc runEh(t: var VmThread, c: var TCtx): Option[PrgCtr] = ## 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 + template pc: untyped = t.ehStack[^1] while true: - let instr = c.ehCode[tos.pc] + let instr = c.ehCode[pc] # already move to the next instruction - inc tos.pc + inc pc - template yieldControl() = - setCurrentException(t, c.memory, tos.ex.refVal) - result.initSuccess(instr.b.PrgCtr) + template yieldControl(pop: static bool) = + when pop: + t.ehStack.shrink(t.ehStack.len - 1) + result = some(instr.b.PrgCtr) return case instr.opcode - of ehoExcept, ehoFinally: + of ehoExcept: # enter exception handler - yieldControl() + yieldControl(true) + of ehoFinally: + yieldControl(false) of ehoExceptWithFilter: let - raised = c.heap.tryDeref(tos.ex.refVal, noneType).value() + raised = c.heap.tryDeref(t.exState.current, noneType).value() if getTypeRel(raised.typ, c.types[instr.a]) in {vtrSub, vtrSame}: # success: the filter matches - yieldControl() + yieldControl(true) else: discard "not handled, try the next instruction" 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 - # control register - let (fromEh, b) = decodeControl(t.getReg(instr.b.TRegister).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") + pc += instr.b - 1 # account for the ``inc`` above of ehoEnd: # terminate the thread and return the unhandled exception - result.initFailure(move t.ehStack[^1].ex) + result = none(PrgCtr) t.ehStack.setLen(t.ehStack.len - 1) break -proc resumeEh(c: var TCtx, t: var VmThread, - frame: int): Result[PrgCtr, VmException] = +proc resumeEh(c: var TCtx, t: var VmThread, frame: int): Option[PrgCtr] = ## 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 @@ -618,7 +570,7 @@ proc resumeEh(c: var TCtx, t: var VmThread, var frame = frame while true: let r = runEh(t, c) - if r.isOk: + if r.isSome: # an exception handler or finalizer is entered. Unwind to the target # frame: if frame < t.sframes.len - 1: @@ -635,12 +587,11 @@ proc resumeEh(c: var TCtx, t: var VmThread, 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) + t.ehStack.add pos.get().ehInstr else: return r -proc opRaise(c: var TCtx, t: var VmThread, at: PrgCtr, - ex: sink VmException): Result[PrgCtr, VmException] = +proc opRaise(c: var TCtx, t: var VmThread, at: PrgCtr): Option[PrgCtr] = ## 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` @@ -648,29 +599,22 @@ proc opRaise(c: var TCtx, t: var VmThread, at: PrgCtr, 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) + t.ehStack.add pos.get().ehInstr result = resumeEh(c, t, pos.get().frame) else: # no exception handler exists: - result.initFailure(ex) + result = none(PrgCtr) -proc handle(res: sink Result[PrgCtr, VmException], c: var TCtx, +proc handle(res: sink Option[PrgCtr], 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.getReg(reg).initIntReg(fromEhBit or t.ehStack.high, c.memory) - inc result - + if res.isSome: + result = res.unsafeGet() else: # report to the exception to the supervisor (by raising an event) - let ex = res.takeErr() - reportException(c, ex.trace, - c.heap.tryDeref(ex.refVal, noneType).value()) + reportException(c, t.exState.stack[^1].trace, + c.heap.tryDeref(t.exState.current, noneType).value()) template atomVal(r: TFullReg): untyped = cast[ptr Atom](r.handle.rawPointer)[] @@ -1994,7 +1938,7 @@ proc rawExecute(c: var TCtx, t: var VmThread, pc: var int): YieldReason = c.callbacks[entry.cbOffset]( VmArgs(ra: ra, rb: rb, rc: rc, slots: regs.data, - currentExceptionPtr: addr t.currentException, + exState: addr t.exState, currentLineInfo: c.debug[pc], typeCache: addr c.typeInfoCache, mem: addr c.memory, @@ -2085,77 +2029,20 @@ 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 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: - # 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 + discard "a no-op" of opcFinallyEnd: # 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: - # not entered through exceptional control-flow; jump to target stored - # in the register - pc = PrgCtr(target) - 1 - + pc = resumeEh(c, t, t.sframes.high).handle(c, t) - 1 + updateRegsAlias() of opcRaise: - decodeBImm() - discard rb # fix the "unused" warning - checkHandle(regs[ra]) - - # `imm == 0` -> raise; `imm == 1` -> reraise current exception - let isReraise = imm == 1 - - 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 - exception.trace.newSeq(t.sframes.len) + if t.exState.stack.len > 0 and t.exState.stack[^1].trace.len == 0: + # the most-recent exception (which is considered to be the one that + # was just raised) has no stacktrace -> generate one + var + trace = newSeq[(PSym, PrgCtr)](t.sframes.len) + pc = pc for i, it in t.sframes.pairs: let p = @@ -2164,16 +2051,16 @@ proc rawExecute(c: var TCtx, t: var VmThread, pc: var int): YieldReason = else: pc - exception.trace[i] = (it.prc, p) + trace[i] = (it.prc, p) # TODO: store the trace in the exception's `trace` field and move this - # setup logic to the ``prepareException`` implementation + # setup logic to the ``raiseExceptionEx`` implementation + t.exState.stack[^1].trace = trace - exception.refVal = regs[ra].atomVal.refVal - # keep the exception alive during exception handling: - c.heap.heapIncRef(exception.refVal) + # set the current exception to the active one: + asgnRef(t.exState.current, t.exState.stack[^1].refVal, c.memory, true) - pc = opRaise(c, t, pc, exception).handle(c, t) - 1 + pc = opRaise(c, t, pc).handle(c, t) - 1 updateRegsAlias() of opcNew: let typ = c.types[instr.regBx - wordExcess] @@ -2955,8 +2842,11 @@ proc dispose*(c: var TCtx, t: sink VmThread) = ## Cleans up and frees all VM data owned by `t`. c.memory.cleanUpLocations(t.regs, 0) - if t.currentException.isNotNil: - c.heap.heapDecRef(c.allocator, t.currentException) + for it in t.exState.stack.items: + c.heap.heapDecRef(c.allocator, it.refVal) + + if t.exState.current.isNotNil: + c.heap.heapDecRef(c.allocator, t.exState.current) # free heap slots that are pending cleanup cleanUpPending(c.memory) diff --git a/compiler/vm/vm_enums.nim b/compiler/vm/vm_enums.nim index 087c97776b7..eb89a2ac71b 100644 --- a/compiler/vm/vm_enums.nim +++ b/compiler/vm/vm_enums.nim @@ -140,9 +140,6 @@ type opcJmp, # jump Bx opcJmpBack, # jump Bx; resulting from a while loop opcBranch, # branch for 'case' - 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, @@ -174,4 +171,4 @@ const firstABxInstr* = opcTJmp largeInstrs* = { # instructions which use 2 int32s instead of 1: opcConv, opcObjConv, opcCast, opcNewSeq, opcOf} - relativeJumps* = {opcTJmp, opcFJmp, opcJmp, opcJmpBack, opcEnter, opcFinally} + relativeJumps* = {opcTJmp, opcFJmp, opcJmp, opcJmpBack} diff --git a/compiler/vm/vmdef.nim b/compiler/vm/vmdef.nim index a58de2cd708..6a259c3064c 100644 --- a/compiler/vm/vmdef.nim +++ b/compiler/vm/vmdef.nim @@ -372,7 +372,7 @@ type slots*: ptr UncheckedArray[TFullReg] # TODO: rework either the callback or exception handling (or both) so that # no pointer is required here - currentExceptionPtr*: ptr HeapSlotHandle + exState*: ptr ExceptionState currentLineInfo*: TLineInfo # XXX: These are only here as a temporary measure until callback handling @@ -675,8 +675,6 @@ type ## 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 @@ -761,6 +759,23 @@ type ## maps the symbol of a procedure to the associated data gathered by the ## profiler + VmException* = object + ## Internal-only. Has to be exposed here because ``VmArgs`` needs access + ## to the type. + 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``) + caught*: bool + ## whether the exception was already caught + + ExceptionState* = object + ## Thread-local exception runtime state. + stack*: seq[VmException] + ## previously caught but not yet full handled exceptions + current*: HeapSlotHandle + ## the current exception, which is what ``getCurrentException`` returns + func `<`*(a, b: FieldIndex): bool {.borrow.} func `<=`*(a, b: FieldIndex): bool {.borrow.} func `==`*(a, b: FieldIndex): bool {.borrow.} @@ -968,11 +983,11 @@ template isValid*(handle: LocHandle): bool = template currentException*(a: VmArgs): HeapSlotHandle = ## A temporary workaround for the exception handle being stored as a pointer - a.currentExceptionPtr[] + a.exState.current template `currentException=`*(a: VmArgs, h: HeapSlotHandle) = ## A temporary workaround for the exception handle being stored as a pointer - a.currentExceptionPtr[] = h + a.exState.current = h func unpackedConvDesc*(info: uint16 ): tuple[op: NumericConvKind, dstbytes, srcbytes: int] = diff --git a/compiler/vm/vmgen.nim b/compiler/vm/vmgen.nim index acada37c8ad..83d669e38dc 100644 --- a/compiler/vm/vmgen.nim +++ b/compiler/vm/vmgen.nim @@ -137,8 +137,12 @@ type ## upper bound of allocated registers at the beginning of the block label: BlockId case kind: BlockKind - of bkBlock, bkFinally: + of bkBlock: start: TPosition + of bkFinally: + patchPos: uint32 + ## the ``ehoNext`` instruction that needs to be patched once the + ## follow-up handler is known of bkExcept: discard @@ -166,9 +170,8 @@ type ehExits: seq[tuple[label: BlockId, pos: uint32]] ## EH instructions that need patching once position and type of the ## target EH instruction is known - lastPath: CgNode - ## the path corresponding to the previously emitted EH instruction - ## sequence, or nil. Prevents excessive EH code duplication + ehPatch: seq[tuple[label: BlockId, pos: uint32]] + ## EH table entries that need patching once the handler is generated CodeGenCtx* = object ## Bundles all input, output, and other contextual data needed for the @@ -456,59 +459,17 @@ proc genEhCode(c: var TCtx, n: CgNode) proc registerEh(c: var TCtx, n: CgNode) = ## Emits an exception-handling table entry for the instruction at the head - ## of the instruction list (i.e., the one emitted next). `n` must be either - ## a label or target list. - proc isEqual(a, b: CgNode): bool = - ## Compares two label-like nodes for equality. - if a.kind != b.kind: - return false - - case a.kind - of cnkLeave: a[0].label == b[0].label - of cnkLabel: a.label == b.label - of cnkResume: true - else: - unreachable() - - proc comparePaths(a, b: CgNode): int = - ## Returns the number of actions `a` and `b` share at the end. 0 - ## means that both share no trailing actions. - let (a, b) = - if a.kind == cnkTargetList: (a, b) - else: (b, a) - # because of the above swap, if `a` is not a list of targets, then neither - # is `b` - if a.kind == cnkTargetList: - if b.kind == cnkLabel: - result = if isEqual(a[^1], b): 1 else: 0 - else: - result = min(a.len, b.len) - for i in 1..result: - if not isEqual(a[^i], b[^i]): - return i - 1 - # one target list is a subset of the other - else: - result = if isEqual(a, b): 1 else: 0 - - let pos = uint32(c.code.len - c.prc.baseOffset.int) + ## of the instruction list (i.e., the one emitted next). case n.kind of cnkLabel: - # un-intercepted jump - if c.prc.lastPath == nil or comparePaths(c.prc.lastPath, n) == 0: - genEhCode(c, n) - - c.ehTable.add (pos, uint32(c.ehCode.len - 1)) - of cnkTargetList: - if n.len == 1 and n[0].kind == cnkResume: - # if there's nothing responding to the exception within the current - # procedure, no EH code needs to be associated with the instruction - return - - if c.prc.lastPath == nil or comparePaths(n, c.prc.lastPath) < n.len: - # cannot re-use the previous instruction sequence - genEhCode(c, n) - - c.ehTable.add (pos, uint32(c.ehCode.len - n.len)) + # a local handler or finally exists + c.ehTable.add (uint32(c.code.len - c.prc.baseOffset.int), 0'u32) + # the real EH instruction is associated later + c.prc.ehPatch.add (n.label, c.ehTable.high.uint32) + of cnkResume: + # if there's nothing responding to the exception within the current + # procedure, no EH code needs to be associated with the instruction + discard else: unreachable(n.kind) @@ -719,46 +680,6 @@ proc popBlock(c: var TCtx) = doAssert false, "leaking temporary " & $i & " " & $c.prc.regInfo[i].kind c.prc.regInfo[i] = RegInfo(kind: slotEmpty) -func controlReg(c: TCtx, blk: BlockInfo): TRegister = - c.code[blk.start.int].regA - -proc genGoto(c: var TCtx; n: CgNode) = - ## Generates and emits the code for a ``cnkGoto``. Depending on whether it's - ## an intercepted jump, the goto can translate to more than one instruction. - let - target = n[0] - info = n.info - case target.kind - of cnkLabel: - c.prc.exits.add (target.label, c.xjmp(n, opcJmp)) - of cnkTargetList: - # there are some leave actions - for i in 0.. 0 and - c.prc.ehExits[^1] == (n[0].label, c.ehCode.high.uint32): + c.prc.ehExits[^1] == (label, c.ehCode.high.uint32): c.ehCode.setLen(c.ehCode.len - 1) c.prc.ehExits.setLen(c.prc.ehExits.len - 1) # patch all EH instructions targeting the handler: - for it in take(c.prc.ehExits, n[0].label): + for it in take(c.prc.ehExits, label): c.ehCode[it.pos] = (ehoNext, 0'u16, c.ehCode.len.uint32 - it.pos) + # patch all EH mappings targeting the handler: + for it in take(c.prc.ehPatch, label): + c.ehTable[it.pos].instr = c.ehCode.len.uint32 + +proc genExcept(c: var TCtx, n: CgNode) = + ## Emits the EH code for a ``cnkExcept``. + patchEh(c, n[0].label) + pushBlock(c): BlockInfo(kind: bkExcept, label: n[0].label) let pc = uint32 c.genLabel() @@ -1054,33 +955,10 @@ proc genExcept(c: var TCtx, n: CgNode) = else: # catch-all exception handler c.ehCode.add (ehoExcept, 0'u16, pc) - # new EH code was emitted, invalidating the cached path: - c.prc.lastPath = nil - -proc genFinally(c: var TCtx, n: CgNode) = - let pc = c.genLabel() - - # update all EH instructions targeting the finally: - for it in take(c.prc.ehExits, n[0].label): - c.ehCode[it.pos] = (ehoFinally, 0'u16, uint32 pc) - - pushBlock(c): BlockInfo(kind: bkFinally, label: n[0].label, start: pc) - - let control = c.getTemp(slotTempInt) - c.patch(n[0].label) # patch the jumps targeting the finally - c.gABC(n, opcFinally, control) - # the control register is freed at the end of the finally section proc genRaise(c: var TCtx; n: CgNode) = - if n[0].kind != cnkEmpty: - let dest = c.genx(n[0]) - c.registerEh(n[^1]) - c.gABI(n, opcRaise, dest, 0, imm=0) - c.freeTemp(dest) - else: - # reraise - c.registerEh(n[^1]) - c.gABI(n, opcRaise, 0, 0, imm=1) + c.registerEh(n[^1]) + c.gABC(n, opcRaise) proc writeBackResult(c: var TCtx, info: CgNode) = ## If the result value fits into a register but is not stored in one @@ -3096,7 +2974,7 @@ proc gen(c: var TCtx; n: CgNode; dest: var TDest) = of cnkRaiseStmt: genRaise(c, n) of cnkGotoStmt: - genGoto(c, n) + c.prc.exits.add (n[0].label, c.xjmp(n, opcJmp)) of cnkStmtList: # XXX: supported for a transition period (``cgir.merge`` creates nested # statement lists) @@ -3105,17 +2983,6 @@ proc gen(c: var TCtx; n: CgNode; dest: var TDest) = of cnkVoidStmt: unused(c, n, dest) gen(c, n[0]) - of cnkContinueStmt: - # marks the end of a finally section - let - blk {.cursor.} = c.prc.blocks[^1] - control = c.controlReg(blk) - # patch the ``opcFinally`` instruction: - c.patch(blk.start) - c.gABx(n, opcFinallyEnd, control, 0) - # now free the control register - c.freeTemp(control) - popBlock(c) of cnkJoinStmt: c.patch(n[0].label) of cnkLoopJoinStmt: @@ -3128,7 +2995,23 @@ proc gen(c: var TCtx; n: CgNode; dest: var TDest) = of cnkExcept: genExcept(c, n) of cnkFinally: - genFinally(c, n) + patchEh(c, n[0].label) + c.ehCode.add (ehoFinally, 0'u16, uint32 c.genLabel()) + c.ehCode.add (ehoNext, 0'u16, 0'u32) # patched later + + pushBlock(c): BlockInfo(kind: bkFinally, patchPos: c.ehCode.high.uint32) + c.gABC(n, opcFinally) + of cnkContinueStmt: + # patch the ehoNext instruction: + let pos = c.prc.blocks[^1].patchPos + if n[0].kind == cnkResume: + c.ehCode[pos] = (ehoEnd, 0'u16, 0'u32) + else: + # cannot be patched just yet + c.prc.ehExits.add (n[0].label, pos) + + c.gABC(n, opcFinallyEnd) + popBlock(c) # pop the finally block of cnkEnd: if c.prc.blocks[^1].kind == bkBlock: c.patch(c.prc.blocks[^1].start) @@ -3160,7 +3043,7 @@ proc gen(c: var TCtx; n: CgNode; dest: var TDest) = unused(c, n, dest) of cnkInvalid, cnkMagic, cnkRange, cnkBranch, cnkBinding, cnkLabel, cnkField, cnkToSlice, - cnkResume, cnkTargetList, cnkLeave: + cnkResume: unreachable(n.kind) proc initProc(c: TCtx, owner: PSym, body: sink Body): BProc = diff --git a/compiler/vm/vmops.nim b/compiler/vm/vmops.nim index 3d7db173587..755a0073302 100644 --- a/compiler/vm/vmops.nim +++ b/compiler/vm/vmops.nim @@ -156,7 +156,35 @@ proc setCurrentExceptionWrapper(a: VmArgs) {.nimcall.} = asgnRef(a.currentException, deref(a.getHandle(0)).refVal, a.mem[], reset=true) -proc prepareExceptionWrapper(a: VmArgs) {.nimcall.} = +proc updateCurrentExc(a: VmArgs) = + if a.exState.stack.len == 0: + a.exState.current.asgnRef(HeapSlotHandle(0), a.mem[], true) + else: + a.exState.current.asgnRef(a.exState.stack[^1].refVal, a.mem[], true) + +proc nimCatchExceptionWrapper(a: VmArgs) {.nimcall.} = + # ignore the ExceptionFrame pointer; the "caught" stack is managed directly + # by the VM + a.exState.stack[^1].caught = true + +proc popException(a: VmArgs, previous: bool) = + if previous: + a.mem.heap.heapDecRef(a.mem.allocator, a.exState.stack[^2].refVal) + a.exState.stack.delete(a.exState.stack.len - 2) + else: + a.mem.heap.heapDecRef(a.mem.allocator, a.exState.stack[^1].refVal) + a.exState.stack.shrink(a.exState.stack.len - 1) + updateCurrentExc(a) + +proc nimAbortExceptionWrapper(a: VmArgs) {.nimcall.} = + popException(a, a.getInt(0) == 1) + +proc nimLeaveExceptWrapper(a: VmArgs) {.nimcall.} = + # if the except block is left via a raised exception, the topmost stack + # entry is the raise exception and must not be popped + popException(a, not a.exState.stack[^1].caught) + +proc raiseExceptionExWrapper(a: VmArgs) {.nimcall.} = let raised = a.heap[].tryDeref(deref(a.getHandle(0)).refVal, noneType).value() nameField = raised.getFieldHandle(1.fpos) @@ -170,6 +198,17 @@ proc prepareExceptionWrapper(a: VmArgs) {.nimcall.} = deref(a.getHandle(1)).strVal, a.mem.allocator) + # push to the exception stack: + a.mem.heap.heapIncRef(deref(a.getHandle(0)).refVal) + a.exState.stack.add VmException(refVal: deref(a.getHandle(0)).refVal) + +proc reraiseExceptionWrapper(a: VmArgs) {.nimcall.} = + # the following nimLeaveExcept call needs something valid to pop, so the + # caught exception is duplicated + a.exState.stack.add a.exState.stack[^1] + a.mem.heap.heapIncRef(a.exState.stack[^1].refVal) + a.exState.stack[^1].caught = false + proc nimUnhandledExceptionWrapper(a: VmArgs) {.nimcall.} = # setup the exception AST: let @@ -177,9 +216,8 @@ proc nimUnhandledExceptionWrapper(a: VmArgs) {.nimcall.} = ast = toExceptionAst($exc.getFieldHandle(1.fpos).deref().strVal, $exc.getFieldHandle(2.fpos).deref().strVal) # report the unhandled exception: - # XXX: the current stack-trace should be passed along, but we don't - # have access to it here - raiseVmError(VmEvent(kind: vmEvtUnhandledException, exc: ast)) + raiseVmError(VmEvent(kind: vmEvtUnhandledException, + trace: a.exState.stack[^1].trace, exc: ast)) proc prepareMutationWrapper(a: VmArgs) {.nimcall.} = discard "no-op" @@ -257,8 +295,12 @@ iterator basicOps*(): Override = # system operations systemop(getCurrentExceptionMsg) systemop(getCurrentException) - systemop(prepareException) + systemop(raiseExceptionEx) + systemop(reraiseException) systemop(nimUnhandledException) + systemop(nimCatchException) + systemop(nimLeaveExcept) + systemop(nimAbortException) systemop(prepareMutation) override("stdlib.system.closureIterSetupExc", setCurrentExceptionWrapper) diff --git a/doc/mir.rst b/doc/mir.rst index b34a8661273..a4c8a54c4a1 100644 --- a/doc/mir.rst +++ b/doc/mir.rst @@ -48,14 +48,8 @@ Semantics | | LVALUE - INTERMEDIATE_TARGET =