# # # The Nim Compiler # (c) Copyright 2018 Nim Contributors # # See the file "copying.txt", included in this # distribution, for details about the copyright. # # This file implements closure iterator transformations. # The main idea is to split the closure iterator body to top level statements. # The body is split by yield statement. # # Example: # while a > 0: # echo "hi" # yield a # dec a # # Should be transformed to: # STATE0: # if a > 0: # echo "hi" # :state = 1 # Next state # return a # yield # else: # :state = 2 # Next state # break :stateLoop # Proceed to the next state # STATE1: # dec a # :state = 0 # Next state # break :stateLoop # Proceed to the next state # STATE2: # :state = -1 # End of execution # The transformation should play well with lambdalifting, however depending # on situation, it can be called either before or after lambdalifting # transformation. As such we behave slightly differently, when accessing # iterator state, or using temp variables. If lambdalifting did not happen, # we just create local variables, so that they will be lifted further on. # Otherwise, we utilize existing env, created by lambdalifting. # Lambdalifting treats :state variable specially, it should always end up # as the first field in env. Currently C codegen depends on this behavior. # One special subtransformation is nkStmtListExpr lowering. # Example: # template foo(): int = # yield 1 # 2 # # iterator it(): int {.closure.} = # if foo() == 2: # yield 3 # # If a nkStmtListExpr has yield inside, it has first to be lowered to: # yield 1 # :tmpSlLower = 2 # if :tmpSlLower == 2: # yield 3 # nkTryStmt Transformations: # If the iter has an nkTryStmt with a yield inside # - the closure iter is promoted to have exceptions (ctx.hasExceptions = true) # - exception table is created. This is a const array, where # `abs(exceptionTable[i])` is a state idx to which we should jump from state # `i` should exception be raised in state `i`. For all states in `try` block # the target state is `except` block. For all states in `except` block # the target state is `finally` block. For all other states there is no # target state (0, as the first block can never be neither except nor finally). # `exceptionTable[i]` is < 0 if `abs(exceptionTable[i])` is except block, # and > 0, for finally block. # - local variable :curExc is created # - the iter body is wrapped into a # try: # closureIterSetupExc(:curExc) # ...body... # catch: # :state = exceptionTable[:state] # if :state == 0: raise # No state that could handle exception # :unrollFinally = :state > 0 # Target state is finally # if :state < 0: # :state = -:state # :curExc = getCurrentException() # # nkReturnStmt within a try/except/finally now has to behave differently as we # want the nearest finally block to be executed before the return, thus it is # transformed to: # :tmpResult = returnValue (if return doesn't have a value, this is skipped) # :unrollFinally = true # goto nearestFinally (or -1 if not exists) # # Example: # # try: # yield 0 # raise ... # except: # yield 1 # return 3 # finally: # yield 2 # # Is transformed to (yields are left in place for example simplicity, # in reality the code is subdivided even more, as described above): # # STATE0: # Try # yield 0 # raise ... # :state = 2 # What would happen should we not raise # break :stateLoop # STATE1: # Except # yield 1 # :tmpResult = 3 # Return # :unrollFinally = true # Return # :state = 2 # Goto Finally # break :stateLoop # :state = 2 # What would happen should we not return # break :stateLoop # STATE2: # Finally # yield 2 # if :unrollFinally: # This node is created by `newEndFinallyNode` # if :curExc.isNil: # if nearestFinally == 0: # return :tmpResult # else: # :state = nearestFinally # bubble up # else: # closureIterSetupExc(nil) # raise # state = -1 # Goto next state. In this case we just exit # break :stateLoop import ast, msgs, idents, renderer, magicsys, lowerings, lambdalifting, modulegraphs, lineinfos, tables, options when defined(nimPreviewSlimSystem): import std/assertions type Ctx = object g: ModuleGraph fn: PSym stateVarSym: PSym # :state variable. nil if env already introduced by lambdalifting tmpResultSym: PSym # Used when we return, but finally has to interfere unrollFinallySym: PSym # Indicates that we're unrolling finally states (either exception happened or premature return) curExcSym: PSym # Current exception states: seq[PNode] # The resulting states. Every state is an nkState node. blockLevel: int # Temp used to transform break and continue stmts stateLoopLabel: PSym # Label to break on, when jumping between states. exitStateIdx: int # index of the last state tempVarId: int # unique name counter tempVars: PNode # Temp var decls, nkVarSection exceptionTable: seq[int] # For state `i` jump to state `exceptionTable[i]` if exception is raised hasExceptions: bool # Does closure have yield in try? curExcHandlingState: int # Negative for except, positive for finally nearestFinally: int # Index of the nearest finally block. For try/except it # is their finally. For finally it is parent finally. Otherwise -1 idgen: IdGenerator const nkSkip = {nkEmpty..nkNilLit, nkTemplateDef, nkTypeSection, nkStaticStmt, nkCommentStmt, nkMixinStmt, nkBindStmt} + procDefs proc newStateAccess(ctx: var Ctx): PNode = if ctx.stateVarSym.isNil: result = rawIndirectAccess(newSymNode(getEnvParam(ctx.fn)), getStateField(ctx.g, ctx.fn), ctx.fn.info) else: result = newSymNode(ctx.stateVarSym) proc newStateAssgn(ctx: var Ctx, toValue: PNode): PNode = # Creates state assignment: # :state = toValue newTree(nkAsgn, ctx.newStateAccess(), toValue) proc newStateAssgn(ctx: var Ctx, stateNo: int = -2): PNode = # Creates state assignment: # :state = stateNo ctx.newStateAssgn(newIntTypeNode(stateNo, ctx.g.getSysType(TLineInfo(), tyInt))) proc newEnvVar(ctx: var Ctx, name: string, typ: PType): PSym = result = newSym(skVar, getIdent(ctx.g.cache, name), nextSymId(ctx.idgen), ctx.fn, ctx.fn.info) result.typ = typ assert(not typ.isNil) if not ctx.stateVarSym.isNil: # We haven't gone through labmda lifting yet, so just create a local var, # it will be lifted later if ctx.tempVars.isNil: ctx.tempVars = newNodeI(nkVarSection, ctx.fn.info) addVar(ctx.tempVars, newSymNode(result)) else: let envParam = getEnvParam(ctx.fn) # let obj = envParam.typ.lastSon result = addUniqueField(envParam.typ.lastSon, result, ctx.g.cache, ctx.idgen) proc newEnvVarAccess(ctx: Ctx, s: PSym): PNode = if ctx.stateVarSym.isNil: result = rawIndirectAccess(newSymNode(getEnvParam(ctx.fn)), s, ctx.fn.info) else: result = newSymNode(s) proc newTmpResultAccess(ctx: var Ctx): PNode = if ctx.tmpResultSym.isNil: ctx.tmpResultSym = ctx.newEnvVar(":tmpResult", ctx.fn.typ[0]) ctx.newEnvVarAccess(ctx.tmpResultSym) proc newUnrollFinallyAccess(ctx: var Ctx, info: TLineInfo): PNode = if ctx.unrollFinallySym.isNil: ctx.unrollFinallySym = ctx.newEnvVar(":unrollFinally", ctx.g.getSysType(info, tyBool)) ctx.newEnvVarAccess(ctx.unrollFinallySym) proc newCurExcAccess(ctx: var Ctx): PNode = if ctx.curExcSym.isNil: ctx.curExcSym = ctx.newEnvVar(":curExc", ctx.g.callCodegenProc("getCurrentException").typ) ctx.newEnvVarAccess(ctx.curExcSym) proc newState(ctx: var Ctx, n, gotoOut: PNode): int = # Creates a new state, adds it to the context fills out `gotoOut` so that it # will goto this state. # Returns index of the newly created state result = ctx.states.len let resLit = ctx.g.newIntLit(n.info, result) let s = newNodeI(nkState, n.info) s.add(resLit) s.add(n) ctx.states.add(s) ctx.exceptionTable.add(ctx.curExcHandlingState) if not gotoOut.isNil: assert(gotoOut.len == 0) gotoOut.add(ctx.g.newIntLit(gotoOut.info, result)) proc toStmtList(n: PNode): PNode = result = n if result.kind notin {nkStmtList, nkStmtListExpr}: result = newNodeI(nkStmtList, n.info) result.add(n) proc addGotoOut(n: PNode, gotoOut: PNode): PNode = # Make sure `n` is a stmtlist, and ends with `gotoOut` result = toStmtList(n) if result.len == 0 or result[^1].kind != nkGotoState: result.add(gotoOut) proc newTempVar(ctx: var Ctx, typ: PType): PSym = result = ctx.newEnvVar(":tmpSlLower" & $ctx.tempVarId, typ) inc ctx.tempVarId proc hasYields(n: PNode): bool = # TODO: This is very inefficient. It traverses the node, looking for nkYieldStmt. case n.kind of nkYieldStmt: result = true of nkSkip: discard else: for c in n: if c.hasYields: result = true break proc transformBreaksAndContinuesInWhile(ctx: var Ctx, n: PNode, before, after: PNode): PNode = result = n case n.kind of nkSkip: discard of nkWhileStmt: discard # Do not recurse into nested whiles of nkContinueStmt: result = before of nkBlockStmt: inc ctx.blockLevel result[1] = ctx.transformBreaksAndContinuesInWhile(result[1], before, after) dec ctx.blockLevel of nkBreakStmt: if ctx.blockLevel == 0: result = after else: for i in 0.. 1: var cond: PNode for i in 0.. # BEGIN_STATE: # if e: # s # goto BEGIN_STATE # else: # goto OUT result = newNodeI(nkGotoState, n.info) let s = newNodeI(nkStmtList, n.info) discard ctx.newState(s, result) let ifNode = newNodeI(nkIfStmt, n.info) let elifBranch = newNodeI(nkElifBranch, n.info) elifBranch.add(n[0]) var body = addGotoOut(n[1], result) body = ctx.transformBreaksAndContinuesInWhile(body, result, gotoOut) body = ctx.transformClosureIteratorBody(body, result) elifBranch.add(body) ifNode.add(elifBranch) let elseBranch = newTree(nkElse, gotoOut) ifNode.add(elseBranch) s.add(ifNode) of nkBlockStmt: result[1] = addGotoOut(result[1], gotoOut) result[1] = ctx.transformBreaksInBlock(result[1], result[0], gotoOut) result[1] = ctx.transformClosureIteratorBody(result[1], gotoOut) of nkTryStmt, nkHiddenTryStmt: # See explanation above about how this works ctx.hasExceptions = true result = newNodeI(nkGotoState, n.info) var tryBody = toStmtList(n[0]) var exceptBody = ctx.collectExceptState(n) var finallyBody = newTree(nkStmtList, getFinallyNode(ctx, n)) finallyBody = ctx.transformReturnsInTry(finallyBody) finallyBody.add(ctx.newEndFinallyNode(finallyBody.info)) # The following index calculation is based on the knowledge how state # indexes are assigned let tryIdx = ctx.states.len var exceptIdx, finallyIdx: int if exceptBody.kind != nkEmpty: exceptIdx = -(tryIdx + 1) finallyIdx = tryIdx + 2 else: exceptIdx = tryIdx + 1 finallyIdx = tryIdx + 1 let outToFinally = newNodeI(nkGotoState, finallyBody.info) block: # Create initial states. let oldExcHandlingState = ctx.curExcHandlingState ctx.curExcHandlingState = exceptIdx let realTryIdx = ctx.newState(tryBody, result) assert(realTryIdx == tryIdx) if exceptBody.kind != nkEmpty: ctx.curExcHandlingState = finallyIdx let realExceptIdx = ctx.newState(exceptBody, nil) assert(realExceptIdx == -exceptIdx) ctx.curExcHandlingState = oldExcHandlingState let realFinallyIdx = ctx.newState(finallyBody, outToFinally) assert(realFinallyIdx == finallyIdx) block: # Subdivide the states let oldNearestFinally = ctx.nearestFinally ctx.nearestFinally = finallyIdx let oldExcHandlingState = ctx.curExcHandlingState ctx.curExcHandlingState = exceptIdx if ctx.transformReturnsInTry(tryBody) != tryBody: internalError(ctx.g.config, "transformReturnsInTry != tryBody") if ctx.transformClosureIteratorBody(tryBody, outToFinally) != tryBody: internalError(ctx.g.config, "transformClosureIteratorBody != tryBody") ctx.curExcHandlingState = finallyIdx ctx.addElseToExcept(exceptBody) if ctx.transformReturnsInTry(exceptBody) != exceptBody: internalError(ctx.g.config, "transformReturnsInTry != exceptBody") if ctx.transformClosureIteratorBody(exceptBody, outToFinally) != exceptBody: internalError(ctx.g.config, "transformClosureIteratorBody != exceptBody") ctx.curExcHandlingState = oldExcHandlingState ctx.nearestFinally = oldNearestFinally if ctx.transformClosureIteratorBody(finallyBody, gotoOut) != finallyBody: internalError(ctx.g.config, "transformClosureIteratorBody != finallyBody") of nkGotoState, nkForStmt: internalError(ctx.g.config, "closure iter " & $n.kind) else: for i in 0.. # :state = STATE # return e ########################## 2 # goto STATE # -> # :state = STATE # break :stateLoop ########################## 3 # return e # -> # :state = -1 # return e # result = n case n.kind of nkStmtList, nkStmtListExpr: if n.len != 0 and n[0].kind == nkYieldStmt: assert(n.len == 2) assert(n[1].kind == nkGotoState) result = newNodeI(nkStmtList, n.info) result.add(ctx.newStateAssgn(stateFromGotoState(n[1]))) var retStmt = newNodeI(nkReturnStmt, n.info) if n[0][0].kind != nkEmpty: var a = newNodeI(nkAsgn, n[0][0].info) var retVal = n[0][0] #liftCapturedVars(n[0], owner, d, c) a.add newSymNode(getClosureIterResult(ctx.g, ctx.fn, ctx.idgen)) a.add retVal retStmt.add(a) else: retStmt.add(ctx.g.emptyNode) result.add(retStmt) else: for i in 0.. 0 # if :state < 0: # :state = -:state # :curExc = getCurrentException() result = newNodeI(nkStmtList, info) let intTyp = ctx.g.getSysType(info, tyInt) let boolTyp = ctx.g.getSysType(info, tyBool) # :state = exceptionTable[:state] block: # exceptionTable[:state] let getNextState = newTree(nkBracketExpr, ctx.createExceptionTable(), ctx.newStateAccess()) getNextState.typ = intTyp # :state = exceptionTable[:state] result.add(ctx.newStateAssgn(getNextState)) # if :state == 0: raise block: let cond = newTree(nkCall, ctx.g.getSysMagic(info, "==", mEqI).newSymNode(), ctx.newStateAccess(), newIntTypeNode(0, intTyp)) cond.typ = boolTyp let raiseStmt = newTree(nkRaiseStmt, ctx.g.emptyNode) let ifBranch = newTree(nkElifBranch, cond, raiseStmt) let ifStmt = newTree(nkIfStmt, ifBranch) result.add(ifStmt) # :unrollFinally = :state > 0 block: let cond = newTree(nkCall, ctx.g.getSysMagic(info, "<", mLtI).newSymNode, newIntTypeNode(0, intTyp), ctx.newStateAccess()) cond.typ = boolTyp let asgn = newTree(nkAsgn, ctx.newUnrollFinallyAccess(info), cond) result.add(asgn) # if :state < 0: :state = -:state block: let cond = newTree(nkCall, ctx.g.getSysMagic(info, "<", mLtI).newSymNode, ctx.newStateAccess(), newIntTypeNode(0, intTyp)) cond.typ = boolTyp let negateState = newTree(nkCall, ctx.g.getSysMagic(info, "-", mUnaryMinusI).newSymNode, ctx.newStateAccess()) negateState.typ = intTyp let ifBranch = newTree(nkElifBranch, cond, ctx.newStateAssgn(negateState)) let ifStmt = newTree(nkIfStmt, ifBranch) result.add(ifStmt) # :curExc = getCurrentException() block: result.add(newTree(nkAsgn, ctx.newCurExcAccess(), ctx.g.callCodegenProc("getCurrentException"))) proc wrapIntoTryExcept(ctx: var Ctx, n: PNode): PNode {.inline.} = let setupExc = newTree(nkCall, newSymNode(ctx.g.getCompilerProc("closureIterSetupExc")), ctx.newCurExcAccess()) let tryBody = newTree(nkStmtList, setupExc, n) let exceptBranch = newTree(nkExceptBranch, ctx.newCatchBody(ctx.fn.info)) result = newTree(nkTryStmt, tryBody, exceptBranch) proc wrapIntoStateLoop(ctx: var Ctx, n: PNode): PNode = # while true: # block :stateLoop: # gotoState :state # local vars decl (if needed) # body # Might get wrapped in try-except let loopBody = newNodeI(nkStmtList, n.info) result = newTree(nkWhileStmt, newSymNode(ctx.g.getSysSym(n.info, "true")), loopBody) result.info = n.info let localVars = newNodeI(nkStmtList, n.info) if not ctx.stateVarSym.isNil: let varSect = newNodeI(nkVarSection, n.info) addVar(varSect, newSymNode(ctx.stateVarSym)) localVars.add(varSect) if not ctx.tempVars.isNil: localVars.add(ctx.tempVars) let blockStmt = newNodeI(nkBlockStmt, n.info) blockStmt.add(newSymNode(ctx.stateLoopLabel)) let gs = newNodeI(nkGotoState, n.info) gs.add(ctx.newStateAccess()) gs.add(ctx.g.newIntLit(n.info, ctx.states.len - 1)) var blockBody = newTree(nkStmtList, gs, localVars, n) if ctx.hasExceptions: blockBody = ctx.wrapIntoTryExcept(blockBody) blockStmt.add(blockBody) loopBody.add(blockStmt) proc deleteEmptyStates(ctx: var Ctx) = let goOut = newTree(nkGotoState, ctx.g.newIntLit(TLineInfo(), -1)) ctx.exitStateIdx = ctx.newState(goOut, nil) # Apply new state indexes and mark unused states with -1 var iValid = 0 for i, s in ctx.states: let body = skipStmtList(ctx, s[1]) if body.kind == nkGotoState and i != ctx.states.len - 1 and i != 0: # This is an empty state. Mark with -1. s[0].intVal = -1 else: s[0].intVal = iValid inc iValid for i, s in ctx.states: let body = skipStmtList(ctx, s[1]) if body.kind != nkGotoState or i == 0: discard ctx.skipThroughEmptyStates(s) let excHandlState = ctx.exceptionTable[i] if excHandlState < 0: ctx.exceptionTable[i] = -ctx.skipEmptyStates(-excHandlState) elif excHandlState != 0: ctx.exceptionTable[i] = ctx.skipEmptyStates(excHandlState) var i = 0 while i < ctx.states.len - 1: let fs = skipStmtList(ctx, ctx.states[i][1]) if fs.kind == nkGotoState and i != 0: ctx.states.delete(i) ctx.exceptionTable.delete(i) else: inc i type PreprocessContext = object finallys: seq[PNode] config: ConfigRef blocks: seq[(PNode, int)] idgen: IdGenerator FreshVarsContext = object tab: Table[int, PSym] config: ConfigRef info: TLineInfo idgen: IdGenerator proc freshVars(n: PNode; c: var FreshVarsContext): PNode = case n.kind of nkSym: let x = c.tab.getOrDefault(n.sym.id) if x == nil: result = n else: result = newSymNode(x, n.info) of nkSkip - {nkSym}: result = n of nkLetSection, nkVarSection: result = copyNode(n) for it in n: if it.kind in {nkIdentDefs, nkVarTuple}: let idefs = copyNode(it) for v in 0..it.len-3: if it[v].kind == nkSym: let x = copySym(it[v].sym, nextSymId(c.idgen)) c.tab[it[v].sym.id] = x idefs.add newSymNode(x) else: idefs.add it[v] for rest in it.len-2 ..< it.len: idefs.add it[rest] result.add idefs else: result.add it of nkRaiseStmt: localError(c.config, c.info, "unsupported control flow: 'finally: ... raise' duplicated because of 'break'") else: result = n for i in 0..= 0: result = newNodeI(nkStmtList, n.info) for i in countdown(c.finallys.high, fin): var vars = FreshVarsContext(tab: initTable[int, PSym](), config: c.config, info: n.info, idgen: c.idgen) result.add freshVars(copyTree(c.finallys[i]), vars) c.idgen = vars.idgen result.add n of nkSkip: discard else: for i in 0 ..< n.len: result[i] = preprocess(c, n[i]) proc transformClosureIterator*(g: ModuleGraph; idgen: IdGenerator; fn: PSym, n: PNode): PNode = var ctx: Ctx ctx.g = g ctx.fn = fn ctx.idgen = idgen if getEnvParam(fn).isNil: # Lambda lifting was not done yet. Use temporary :state sym, which will # be handled specially by lambda lifting. Local temp vars (if needed) # should follow the same logic. ctx.stateVarSym = newSym(skVar, getIdent(ctx.g.cache, ":state"), nextSymId(idgen), fn, fn.info) ctx.stateVarSym.typ = g.createClosureIterStateType(fn, idgen) ctx.stateLoopLabel = newSym(skLabel, getIdent(ctx.g.cache, ":stateLoop"), nextSymId(idgen), fn, fn.info) var pc = PreprocessContext(finallys: @[], config: g.config, idgen: idgen) var n = preprocess(pc, n.toStmtList) #echo "transformed into ", n #var n = n.toStmtList discard ctx.newState(n, nil) let gotoOut = newTree(nkGotoState, g.newIntLit(n.info, -1)) var ns = false n = ctx.lowerStmtListExprs(n, ns) if n.hasYieldsInExpressions(): internalError(ctx.g.config, "yield in expr not lowered") # Splitting transformation discard ctx.transformClosureIteratorBody(n, gotoOut) # Optimize empty states away ctx.deleteEmptyStates() # Make new body by concatenating the list of states result = newNodeI(nkStmtList, n.info) for s in ctx.states: assert(s.len == 2) let body = s[1] s.sons.del(1) result.add(s) result.add(body) result = ctx.transformStateAssignments(result) result = ctx.wrapIntoStateLoop(result) # echo "TRANSFORM TO STATES: " # echo renderTree(result) # echo "exception table:" # for i, e in ctx.exceptionTable: # echo i, " -> ", e