From 113152ea8a56188d776e948ebb7e22cf90da813a Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Fri, 8 Nov 2024 01:48:43 +0000 Subject: [PATCH] mirgen: lower `finally` in `mirgen` Instead of leaving the implementation of `finally` and the exception stack to the code generators (or a theoretical MIR pass), `mirgen` now does the lowering itself. This means turning `finally` into what is effectively a block + try/except + dispatcher, which is the same thing that `ccgflow` already did (just with access to the original structure, which makes the translation much simpler). Emitting the calls to the exception runtime is also moved to `mirgen`. In order to be able to continue raising an in-flight instruction (a re-raise statement can only raise a *caught* exception), the `mResumeRaising` magic is introduced. `mnkRaise` should eventually become low-level enough for the magic is no longer necessary. --- compiler/ast/ast_types.nim | 1 + compiler/mir/mirgen.nim | 179 +++++++++++++++---- compiler/mir/mirgen_blocks.nim | 305 ++++++++++++++++----------------- 3 files changed, 291 insertions(+), 194 deletions(-) diff --git a/compiler/ast/ast_types.nim b/compiler/ast/ast_types.nim index ff49afbb6a1..e1be4720f8b 100644 --- a/compiler/ast/ast_types.nim +++ b/compiler/ast/ast_types.nim @@ -848,6 +848,7 @@ type mCopyInternal ## copyInternal(a, b); copies backend-specific internal data stored ## on non-pure objects from a to b + mResumeRaising # things that we can evaluate safely at compile time, even if not asked for it: const diff --git a/compiler/mir/mirgen.nim b/compiler/mir/mirgen.nim index a071314d883..c6550a580a5 100644 --- a/compiler/mir/mirgen.nim +++ b/compiler/mir/mirgen.nim @@ -1294,8 +1294,7 @@ proc genReturn(c: var TCtx, n: PNode) = 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 +1559,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 +1582,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 +1727,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("nimLeaveException") + c.builder.buildCall c.env.procedures.add(p), VoidType: + discard + c.subTree mnkContinue: + raiseExit(c) c.subTree mnkEndStruct: c.add labelNode(label) @@ -1765,26 +1784,107 @@ 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 leave actions that need to emitted too + 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 mnkVoid: + c.buildCheckedMagicCall mResumeRaising, VoidType: + discard + + elif exc.id.isSome: + # always resume raising the exception + c.subTree mnkVoid: + c.buildCheckedMagicCall mResumeRaising, VoidType: + discard proc genTry(c: var TCtx, n: PNode, dest: Destination) = let @@ -1795,10 +1895,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(Int32Type) + 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 +2247,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: @@ -2334,7 +2441,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 +2487,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..f73592b401e 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), Int32Type) + + # 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