├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── cps.nim ├── cps.nimble ├── cps ├── callbacks.nim ├── defers.nim ├── environment.nim ├── exprs.nim ├── help.nim ├── hooks.nim ├── normalizedast.nim ├── returns.nim ├── rewrites.nim ├── spec.nim └── transform.nim ├── docs ├── README.md ├── coroutines.md ├── cps.svg ├── demo.svg ├── taste.svg ├── techo.svg └── tzevv.svg ├── examples ├── README.md ├── coroutine.nim ├── cpscps.nim ├── goto.nim ├── iterator.nim ├── lazy.nim ├── lua_coroutines.nim ├── pipes.nim ├── threadpool.nim ├── threadpool.nim.cfg ├── trycatch.nim └── work.nim ├── experiments ├── README.md ├── chain.nim ├── eventqueue.nim ├── main.nim ├── main_tcp.nim ├── tr.nim ├── tr_case.nim ├── try │ ├── README.md │ ├── original.nim │ └── transform.nim ├── xfrm.nim ├── xfrm2.nim └── xfrm3.nim ├── papers ├── 1011.4558.pdf ├── README.md ├── cpc-manual.pdf └── cpc.pdf ├── stash ├── README.md ├── bench.nim ├── bench.nim.cfg ├── brokenbreak.nim ├── echo_server_client.nim ├── iteratorT.nim ├── lost.nim ├── performance.nim ├── performance.nim.cfg └── standalone_tcp_server.nim ├── talk-talk ├── README.md ├── manual1.nim ├── manual1_stack.nim ├── manual1_stack_crash.nim └── manual2.nim ├── tests ├── .gitignore ├── exports.nim ├── foreign.nim ├── killer.nim ├── preamble.nim ├── t00_smoke.nim ├── t10_loops.nim ├── t20_api.nim ├── t30_cc.nim ├── t40_ast.nim ├── t50_hooks.nim ├── t60_returns.nim ├── t70_locals.nim ├── t80_try1.nim ├── t80_try2.nim ├── t90_exprs1.nim ├── t90_exprs2.nim ├── t90_exprs3.nim ├── t90_exprs4.nim ├── t90_exprs5.nim └── zevv.nim └── tutorial ├── README.md ├── cpstut1.nim ├── cpstut2.nim ├── cpstut3.nim └── cpstut4.nim /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | insert_final_newline = true 4 | indent_size = 2 5 | trim_trailing_whitespace = true 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | schedule: 4 | - cron: '30 5 * * *' 5 | 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | branches: 11 | - '*' 12 | 13 | 14 | jobs: 15 | changes: 16 | # Disable the filter on scheduled runs because we don't want to skip those 17 | if: github.event_name != 'schedule' 18 | continue-on-error: true # Makes sure errors won't stop us 19 | runs-on: ubuntu-latest 20 | outputs: 21 | src: ${{ steps.filter.outputs.src }} 22 | steps: 23 | # For PRs the path filter check with Github API, so no need to checkout 24 | # for them. 25 | - if: github.event_name != 'pull_request' 26 | name: Checkout (if not PR) 27 | uses: actions/checkout@v4 28 | 29 | - uses: dorny/paths-filter@v3 30 | id: filter 31 | with: 32 | filters: | 33 | src: 34 | - '**.cfg' 35 | - '**.nims' 36 | - '**.nim' 37 | - '**.nimble' 38 | - 'tests/**' 39 | - '.github/workflows/ci.yml' 40 | 41 | build: 42 | # Build if the files we care about are changed. 43 | needs: changes 44 | # Make sure to always run regardless of whether the filter success or not. 45 | # When the filter fails there won't be an output, so checking for `false` 46 | # state is better than checking for `true`. 47 | # 48 | # The always() function here is required for the job to always run despite 49 | # what Github docs said, see: https://github.com/actions/runner/issues/491 50 | if: always() && !cancelled() && needs.changes.outputs.src != 'false' 51 | 52 | strategy: 53 | fail-fast: false 54 | matrix: 55 | os: [ubuntu-latest] 56 | compiler: 57 | - name: nim 58 | version: devel 59 | broken: true 60 | 61 | - name: nim 62 | version: version-2-0 63 | broken: false 64 | 65 | - name: nimskull 66 | version: "*" 67 | broken: false 68 | 69 | - name: nimskull 70 | version: "0.1.0-dev.21407" 71 | broken: false 72 | 73 | name: "${{ matrix.os }} (${{ matrix.compiler.name }} ${{ matrix.compiler.version }}${{ matrix.compiler.broken && ', broken' || '' }})" 74 | runs-on: ${{ matrix.os }} 75 | continue-on-error: ${{ matrix.compiler.broken }} 76 | steps: 77 | - name: Checkout 78 | uses: actions/checkout@v4 79 | with: 80 | path: project 81 | 82 | - name: Compiler (nim) 83 | if: matrix.compiler.name == 'nim' 84 | uses: alaviss/setup-nim@0.1.1 85 | with: 86 | path: nim 87 | version: ${{ matrix.compiler.version }} 88 | 89 | - name: Compiler (nimskull) 90 | id: nimskull 91 | if: matrix.compiler.name == 'nimskull' 92 | uses: nim-works/setup-nimskull@0.1.2 93 | with: 94 | nimskull-version: ${{ matrix.compiler.version }} 95 | 96 | - if: matrix.compiler.name == 'nimskull' 97 | name: Fetch nimble's fork for nimskull 98 | uses: actions/checkout@v4 99 | with: 100 | path: nimble 101 | repository: alaviss/nimble 102 | ref: nimskull 103 | 104 | - if: matrix.compiler.name == 'nimskull' 105 | name: Build nimble and add to PATH 106 | shell: bash 107 | run: | 108 | cd nimble 109 | nim c -d:release -o:nimble src/nimble.nim 110 | cp nimble "$NIMSKULL_BIN/nimble" 111 | # Add nimble binary folder to PATH too 112 | echo "$HOME/.nimble/bin" >> $GITHUB_PATH 113 | env: 114 | NIMSKULL_BIN: ${{ steps.nimskull.outputs.bin-path }} 115 | 116 | - name: Valgrind 117 | shell: bash 118 | run: | 119 | sudo apt-get update 120 | sudo apt install --fix-missing valgrind 121 | 122 | - name: Dependencies 123 | shell: bash 124 | run: | 125 | cd project 126 | nimble --accept develop 127 | nimble --accept install "https://github.com/disruptek/balls" 128 | env: 129 | NIM: ${{ matrix.compiler.name }} 130 | 131 | - name: Examples 132 | shell: bash 133 | run: | 134 | cd project 135 | cd examples 136 | balls '***' --path=".." --backend:c --mm:arc --mm:orc 137 | env: 138 | NIM: ${{ matrix.compiler.name }} 139 | 140 | - name: Tutorial 141 | shell: bash 142 | run: | 143 | cd project 144 | cd tutorial 145 | balls '***' --path=".." --backend:c --mm:arc --mm:orc 146 | env: 147 | NIM: ${{ matrix.compiler.name }} 148 | 149 | - name: Tests 150 | shell: bash 151 | run: | 152 | cd project 153 | balls --path="." --backend:c --mm:arc --mm:orc --errorMax:3 154 | 155 | - name: Docs 156 | if: ${{ matrix.docs }} == 'true' 157 | shell: bash 158 | run: | 159 | cd project 160 | branch=${{ github.ref }} 161 | branch=${branch##*/} 162 | nimble doc --project --outdir:docs --path="." \ 163 | '--git.url:https://github.com/${{ github.repository }}' \ 164 | '--git.commit:${{ github.sha }}' \ 165 | "--git.devel:$branch" \ 166 | cps.nim 167 | # Ignore failures for older Nim 168 | cp docs/{the,}index.html || true 169 | 170 | - name: Pages 171 | if: > 172 | github.event_name == 'push' && github.ref == 'refs/heads/master' && 173 | matrix.os == 'ubuntu-latest' && matrix.nim == 'devel' 174 | uses: crazy-max/ghaction-github-pages@v4.0.0 175 | with: 176 | build_dir: project/docs 177 | env: 178 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 179 | 180 | # Set check-required on this 181 | success: 182 | needs: build 183 | if: always() 184 | runs-on: ubuntu-latest 185 | name: 'All check passes' 186 | steps: 187 | - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') 188 | name: 'Fail when previous jobs fails' 189 | run: | 190 | echo "::error::One of the previous jobs failed" 191 | exit 1 192 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | nim.cfg 2 | bin 3 | deps 4 | .tool-versions 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Andy Davidoff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cps.nimble: -------------------------------------------------------------------------------- 1 | version = "0.11.4" 2 | author = "disruptek" 3 | description = "continuation-passing style" 4 | license = "MIT" 5 | 6 | when declared(taskRequires): 7 | taskRequires "test", "https://github.com/disruptek/balls >= 3.0.0" 8 | 9 | task demo, "generate the demos": 10 | exec """demo docs/tzevv.svg "nim c --out=\$1 tests/zevv.nim"""" 11 | exec """demo docs/taste.svg "nim c --out=\$1 tests/taste.nim"""" 12 | 13 | task matrix, "generate the matrix": 14 | exec """demo docs/test-matrix.svg "balls" 34""" 15 | -------------------------------------------------------------------------------- /cps/callbacks.nim: -------------------------------------------------------------------------------- 1 | ##[ 2 | 3 | An attempt to collect all the callback code in one place. 4 | 5 | NOTE: currently, cps/rewrites defines `isCallback(NimNode): bool` 6 | 7 | ]## 8 | import std/macros 9 | 10 | import cps/[spec, rewrites, environment] 11 | import cps/normalizedast except newTree, newStmtList 12 | 13 | template cpsCallback*() {.pragma.} ## this is a callback typedef 14 | template cpsCallbackShim*(whelp: typed) {.pragma.} ## 15 | ## the symbol for creating a continuation which returns a continuation base 16 | template cpsCallbackRecovery*(base: typed) {.pragma.} ## 17 | ## a block that can be abbreviated to a child continuation call inside cps 18 | 19 | type 20 | Callback*[C; R; P] = object 21 | fn*: P ## 22 | ## the bootstrap for continuation C 23 | rs*: proc (c: var C): R {.nimcall.} ## 24 | ## the result fetcher for continuation C 25 | 26 | proc cpsCallbackTypeDef*(tipe: NimNode, n: NimNode): NimNode = 27 | ## looks like cpsTransformProc but applies to proc typedefs; 28 | ## this is where we create our calling convention concept 29 | let params = copyNimTree n[0] 30 | let r = copyOrVoid params[0] 31 | params[0] = tipe 32 | let p = nnkProcTy.newTree(params, 33 | nnkPragma.newTree(ident"nimcall", bindSym"cpsCallback")) 34 | result = nnkBracketExpr.newTree(bindSym"Callback", tipe, r, p) 35 | result = workaroundRewrites result.NormNode 36 | 37 | proc createCallbackShim*(env: Env; whelp: ProcDef): ProcDef = 38 | ## this is a version of whelp that returns the base continuation type 39 | result = clone(whelp, newStmtList()) 40 | result.returnParam = env.inherits 41 | result.name = genProcName(procedure env, "callback", info=whelp) 42 | # whelp_234(a, b, c) 43 | result.body = newCall whelp.name 44 | for defs in result.callingParams: 45 | result.body.add defs.name 46 | # C: whelp_234(a, b, c) 47 | result.body = newCall(result.returnParam, result.body) 48 | 49 | proc createCallback*(sym: NimNode): NimNode = 50 | ## create a new Callback object construction 51 | let fn = sym.getImpl.ProcDef.pragmaArgument"cpsCallbackShim" 52 | let impl = fn.getImpl.ProcDef # convenience 53 | let rs = impl.pragmaArgument"cpsResult" 54 | let tipe = nnkBracketExpr.newTree bindSym"Callback" 55 | tipe.add impl.returnParam # the base cps environment type 56 | tipe.add: # the return type of the result fetcher 57 | copyOrVoid impl.pragmaArgument"cpsReturnType" 58 | var params = copyNimTree impl.formalParams # prepare params list 59 | # consider desym'ing foo(a: int; b = a) before deleting this loop 60 | for defs in impl.callingParams: 61 | params = desym(params, defs.name) 62 | tipe.add: # the proc() type of the bootstrap 63 | nnkProcTy.newTree(params, nnkPragma.newTree ident"nimcall") 64 | result = 65 | NimNode: 66 | nnkObjConstr.newTree(tipe, "fn".colon fn.NimNode, "rs".colon rs.NimNode) 67 | 68 | proc createCastCallback*(whelp, callback, sym: NimNode): NimNode = 69 | ## Given a `callback` typedesc and a CPS continuation procedure, 70 | ## apply a (proc ()) type specifier to help disambiguate overloads. 71 | let tipe = getImpl(callback)[2] # recover bootstrap proc type 72 | when not defined(isNimSkull): 73 | # erase the pragma so it doesn't blow old nim's mind 74 | tipe[1] = nnkPragma.newTree() 75 | result = newCall(whelp, newCall(tipe, sym)) 76 | 77 | proc recover*[C, R, P](callback: Callback[C, R, P]; continuation: var C): R = 78 | ## Using a `callback`, recover the `result` of the given `continuation`. 79 | ## This is equivalent to running `()` on a continuation which was 80 | ## created with `whelp` against a procedure call. 81 | ## 82 | ## If the continuation is in the `running` `State`, this operation will 83 | ## `trampoline` the continuation until it is `finished`. The `result` 84 | ## will then be recovered from the continuation environment. 85 | ## 86 | ## It is a `Defect` to attempt to recover the `result` of a `dismissed` 87 | ## `continuation`. 88 | callback.rs(continuation) 89 | 90 | macro call*[C; R; P](callback: Callback[C, R, P]; arguments: varargs[typed]): C = 91 | ## Invoke a `callback` with the given `arguments`; returns a continuation. 92 | result = newCall(callback.dot ident"fn") 93 | for argument in arguments.items: 94 | result.add argument 95 | 96 | when cpsCallOperatorSupported and not defined cpsNoCallOperator: 97 | {.push experimental: "callOperator".} 98 | macro `()`*[C; R; P](callback: Callback[C, R, P]; arguments: varargs[typed]): untyped = 99 | ## Allows for natural use of call syntax to invoke a callback and 100 | ## recover its result in a single expression. 101 | let call = newCall(bindSym"call", callback) 102 | for argument in arguments.items: 103 | call.add argument 104 | let mutable = genSymVar("callback_continuation", callback.NormNode).NimNode 105 | result = newStmtList() 106 | result.add: 107 | newTree nnkVarSection: 108 | newTree(nnkIdentDefs, mutable, newEmptyNode(), call) 109 | result.add: 110 | newCall(bindSym"recover", callback, mutable) 111 | var cbr = newCall(bindSym"cpsCallbackRecovery", getTypeInst C) 112 | cbr = nnkPragma.newTree(cbr) 113 | result = nnkPragmaBlock.newTree(cbr, result) 114 | {.pop.} 115 | 116 | proc isCallbackRecovery*(n: NimNode): bool = 117 | ## the node appears to be a {.cpsCallbackRecovery.} pragma block 118 | if n.isNil: return false 119 | case n.kind 120 | of nnkPragmaBlock: 121 | n.len > 0 and n[0].isCallbackRecovery 122 | of nnkPragma: 123 | n.len > 0 and n[0].isCallbackRecovery 124 | of nnkCall: 125 | n.len > 0 and n[0].isCallbackRecovery 126 | of nnkSym: 127 | n.strVal == "cpsCallbackRecovery" 128 | else: 129 | false 130 | 131 | proc baseContinuationType*(n: NimNode): NimNode = 132 | ## given a callable symbol presumed to be a callback, 133 | ## recover the (base) continuation return type of the proc. 134 | case n.kind 135 | of nnkDotExpr: 136 | # continuationEnvironment.callbackLocal.fn(arguments...) 137 | if n[0].kind in {nnkDotExpr, nnkSym}: 138 | let fun = n.last.getTypeImpl # proctype from first object record (fn) 139 | result = fun[0][0] # recover proc return type 140 | elif not n.isCallback: 141 | raise Defect.newException "callable is not a cps callback" 142 | else: 143 | discard 144 | if result.isNil: 145 | raise Defect.newException "unable to recover base type from callback" 146 | 147 | proc setupCallbackChild*(env: var Env; call: Call): (Name, TypeExpr) = 148 | ## create a new child continuation variable to receive the result of 149 | ## the callback and add it to the environment. return the child's 150 | ## symbol along with the base continuation type of the child. 151 | let ctype = baseContinuationType(call[0].NimNode).TypeExpr 152 | let child = genSymVar("callbackChild", info = call) 153 | env.localSection newIdentDef(child, ctype) 154 | result = (child, ctype) 155 | 156 | when false: 157 | macro naturalize(kind: static[NimNodeKind]; callback: typed; 158 | args: varargs[untyped]): untyped = 159 | ## perform a conditional typed rewrite for natural callback syntax inside cps 160 | if callback.looksLikeCallback: 161 | # convert it to callback.call(...) 162 | result = macros.newTree(kind, newDotExpr(callback, bindSym"call")) 163 | for arg in args.items: 164 | result.add arg 165 | # wrap that in recover(callback, ...) 166 | result = newCall(bindSym"recover", callback, result) 167 | else: 168 | result = kind.newTree(desym callback) 169 | for arg in args.items: 170 | result.add arg 171 | 172 | proc unwrapAnyDotExpr(n: NimNode): seq[NimNode] = 173 | ## turn a caller like foo.inc into @[inc, foo] so that we can flatten/reorder 174 | ## arguments correctly 175 | case n.kind 176 | of nnkDotExpr: 177 | @[n[1], n[0]] 178 | else: 179 | @[n] 180 | 181 | proc rewriteCalls(n: NimNode): NimNode = 182 | ## rewriting `callback(x)` into `recover(callback, call(callback, x))` for use 183 | ## inside of an untyped pass; this should be applied only to Callback symbols... 184 | proc recall(n: NimNode): NimNode = 185 | case n.kind 186 | of CallNodes: 187 | result = newCall(bindSym"naturalize", newLit(n.kind)) 188 | result.add unwrapAnyDotExpr(n[0]) # help foo.inc(...) into inc(foo, ...) 189 | result.add n[1..^1] 190 | else: 191 | discard 192 | result = filter(n, recall) 193 | 194 | proc performUntypedPass(tipe: NimNode; n: NimNode): NimNode = 195 | ## Perform any rewrites needed prior to a `.cps: T.` transformation. 196 | if n.kind != nnkProcDef: return n 197 | result = n 198 | result.body = rewriteCalls result.body 199 | -------------------------------------------------------------------------------- /cps/defers.nim: -------------------------------------------------------------------------------- 1 | import cps/[normalizedast, rewrites] 2 | import std/macros except newStmtList 3 | 4 | template isNotNil*(x: untyped): bool = not(isNil(x)) 5 | 6 | proc findTree(n: NormNode, traverseKinds: set[NimNodeKind], 7 | cond: proc(n: NormNode): bool): NormNode = 8 | ## Find the first node in the AST tree `n` satisfying `cond`. 9 | ## 10 | ## :traverseKinds: 11 | ## The AST node kinds to traverse. 12 | if cond(n): 13 | result = n 14 | elif n.kind in traverseKinds: 15 | for child in n.items: 16 | result = findTree(child, traverseKinds, cond) 17 | if result.isNotNil: 18 | break 19 | 20 | proc splitStmtList(n, splitNode: NormNode): seq[NormNode] = 21 | ## Split the StmtList `n` at `splitNode` to up to two splits. 22 | ## 23 | ## The same StmtList hierarchy will be shared on both splits. 24 | if n == splitNode: 25 | discard "The node to be split upon should not be in result" 26 | 27 | elif n.kind in {nnkStmtList, nnkStmtListExpr}: 28 | result.add: copyNimNode(n) 29 | for idx, child in n.pairs: 30 | template listTail(): seq[NormNode] = 31 | ## The remaining nodes in this list, excluding the current node 32 | n[idx + 1 .. ^1] 33 | 34 | let childSplits = splitStmtList(n[idx], splitNode) 35 | if childSplits.len > 0: 36 | # Merge the first split 37 | result[0].add: childSplits[0] 38 | 39 | # The inner StmtList has two splits 40 | if childSplits.len > 1: 41 | # Construct the other split 42 | result.add: copyNimNode(n) 43 | # Add the inner split 44 | result[^1].add childSplits[1] 45 | # Add the remaining nodes of this list 46 | result[^1].add listTail() 47 | # Done 48 | break 49 | 50 | else: 51 | # There are no splits, thus this is the split node 52 | # 53 | # Construct the other split with the remaining nodes in this list 54 | result.add: 55 | copyNimNode(n).add: 56 | listTail() 57 | # Done 58 | break 59 | 60 | else: 61 | # If it's not a StmtList, just return as is 62 | result.add n 63 | 64 | proc rewriteDefer*(n: NormNode): NormNode = 65 | ## Rewrite the AST of `n` so that all `defer` nodes are 66 | ## transformed into try-finally 67 | proc rewriter(n: NormNode): NormNode = 68 | let deferNode = 69 | findTree(n, {nnkStmtList, nnkStmtListExpr}) do (n: NormNode) -> bool: 70 | n.kind == nnkDefer 71 | 72 | if deferNode.isNotNil: 73 | let 74 | splits = splitStmtList(n, deferNode) 75 | # Construct a finally node with lineinfo of the defer node 76 | finallyNode = newNimNode(nnkFinally, deferNode).add: 77 | # Use the defer body as the finally body 78 | deferNode.last 79 | 80 | if splits.len > 0: 81 | # Add the first split, or "nodes before defer" 82 | result = splits[0] 83 | 84 | # Construct a try-finally with the remainder and add it to the end 85 | result.add: 86 | # Create a new try statement with the lineinfo of the second split 87 | newNimNode(nnkTryStmt, splits[1]).add( 88 | # Put the second split, or "nodes after defer", as the try body 89 | splits[1], 90 | finallyNode 91 | ) 92 | else: 93 | # There are no splits, thus this is a defer without a container 94 | # 95 | # Construct a naked try-finally for it. 96 | result = NormNode: 97 | newNimNode(nnkTryStmt, deferNode).add( 98 | # Use an empty statement list for the body 99 | newNimNode(nnkStmtList, deferNode), 100 | finallyNode 101 | ) 102 | 103 | # Also rewrite the result to eliminate all defers in it 104 | result = rewriteDefer(result) 105 | 106 | result = filter(n, rewriter) 107 | -------------------------------------------------------------------------------- /cps/help.nim: -------------------------------------------------------------------------------- 1 | import std/macros 2 | 3 | const 4 | cpsDebug* {.strdefine.} = "" ## produce gratuitous output 5 | comments* = cpsDebug != "" ## embed comments within the transformation 6 | 7 | func doc*(s: string): NimNode = 8 | ## generate a doc statement for debugging 9 | when comments: 10 | newCommentStmtNode(s) 11 | else: 12 | newEmptyNode() 13 | 14 | proc doc*(n: NimNode; s: string) = 15 | ## add a doc statement to the ast for debugging 16 | when comments: 17 | if n.kind == nnkStmtList: 18 | n.add doc(s) 19 | 20 | when cpsDebug == "": 21 | template debug*(ignore: varargs[untyped]) = discard 22 | template lineAndFile*(n: NimNode): string = "(no debug)" 23 | else: 24 | import std/strutils 25 | import os 26 | 27 | type 28 | AstKind* {.pure.} = enum 29 | ## The type of the passed AST 30 | Original = "original" 31 | Transformed = "transformed" 32 | 33 | template lineAndFile*(n: NimNode): string = 34 | $n.lineInfoObj.line & " of " & extractFilename($n.lineInfoObj.filename) 35 | 36 | proc numberedLines*(s: string; first = 1): string = 37 | for n, line in pairs(splitLines(s, keepEol = true)): 38 | result.add "$1 $2" % [ align($(n + first), 3), line ] 39 | 40 | proc snippet*(n: NimNode; name: string): string = 41 | result &= "----8<---- " & name & "\t" & "vvv" 42 | result &= "\n" & n.repr.numberedLines(n.lineInfoObj.line) & "\n" 43 | result &= "----8<---- " & name & "\t" & "^^^" 44 | 45 | func debug*(id: string, n: NimNode, kind: AstKind, info: NimNode = nil) = 46 | ## Debug print the given node `n`, with `id` is a string identifying the 47 | ## caller and `info` specifies the node to retrieve the line information 48 | ## from. 49 | ## 50 | ## If `info` is `nil`, the line information will be retrieved from `n`. 51 | if cpsDebug != id: return 52 | let info = 53 | if info.isNil: 54 | n 55 | else: 56 | info 57 | 58 | let lineInfo = info.lineInfoObj 59 | 60 | let procName = 61 | if info.kind in RoutineNodes: 62 | repr info.name 63 | else: 64 | "" 65 | 66 | debugEcho "=== $1 $2($3) === $4" % [ 67 | id, 68 | if procName.len > 0: "on " & procName else: "", 69 | $kind, 70 | $lineInfo 71 | ] 72 | when defined(cpsTree): 73 | debugEcho treeRepr(n) 74 | else: 75 | debugEcho repr(n).numberedLines(lineInfo.line) 76 | -------------------------------------------------------------------------------- /cps/hooks.nim: -------------------------------------------------------------------------------- 1 | import std/macros except newStmtList, newTree 2 | 3 | import cps/[spec, normalizedast] 4 | 5 | # we use mixin instead 6 | #{.experimental: "dynamicBindSym".} 7 | 8 | ##[ 9 | 10 | The idea is that you can reimplement one of a few procedures which we will 11 | perform a late binding to by name. 12 | 13 | ]## 14 | 15 | proc introduce*(hook: Hook; n: NormNode) = 16 | ## introduce a hook into the given scope whatfer later use therein 17 | var n = n 18 | case n.kind 19 | of nnkStmtList: 20 | n.insert(0, nnkMixinStmt.newTree ident($hook)) 21 | of nnkProcDef: 22 | introduce hook, asRoutineDef(n).body # TODO: maybe a pragmas at some point? 23 | else: 24 | n.insert(0, n.errorAst "you cannot add a " & $hook & " to a " & $n.kind) 25 | 26 | proc introduce*(n: NormNode; hooks: set[Hook]) = 27 | ## convenience to introduce a set of hooks 28 | for hook in hooks.items: 29 | hook.introduce n 30 | 31 | proc findColonLit*(n: NimNode; s: string; T: typedesc): T = 32 | let child = 33 | n.findChild: 34 | it.kind == nnkExprColonExpr and it.len == 2 and it[0].strVal == s 35 | if child.isNil: 36 | raise ValueError.newException "parse error: " & treeRepr(n) 37 | else: 38 | when T is BiggestInt: 39 | result = child[1].intVal 40 | elif T is int: 41 | result = child[1].intVal.int 42 | elif T is string: 43 | result = child[1].strVal 44 | else: 45 | raise Defect.newException "we can't be friends" 46 | 47 | proc makeLineInfo*(n: NimNode): LineInfo = 48 | ## return a run-time LineInfo into a compile-time LineInfo object 49 | LineInfo(filename: n.findColonLit("filename", string), 50 | line: n.findColonLit("line", int), 51 | column: n.findColonLit("column", int)) 52 | 53 | template makeLineInfo*(n: LineInfo): NimNode = 54 | ## turn a compile-time LineInfo object into a runtime LineInfo object 55 | newLit n 56 | 57 | proc sym*(hook: Hook): Name = 58 | ## produce a symbol|ident for the hook procedure 59 | when false: 60 | # this is where we can experiment with .dynamicBindSym 61 | bindSym($hook, brForceOpen) 62 | else: 63 | # rely on a `mixin $hook` in (high) scope 64 | ident($hook).Name 65 | 66 | proc abbreviation(n: NimNode): NimNode = 67 | ## "abbreviate" a node so it can be passed succinctly 68 | case n.kind 69 | of nnkProcDef: 70 | n.name 71 | of nnkSym, nnkIdent, nnkDotExpr: 72 | n 73 | of NormalCallNodes: 74 | n[0] 75 | else: 76 | n.errorAst "dunno how to abbreviate " & $n.kind 77 | 78 | proc nameForNode*(n: NimNode): string = 79 | ## produce some kind of useful string that names a node 80 | let abbrev = abbreviation n 81 | case abbrev.kind 82 | of nnkSym, nnkIdent: 83 | $abbrev 84 | else: 85 | repr n 86 | 87 | when defined(cpsNoTrace): 88 | template entrace(hook: static[Hook]; c, n, body: NormNode): NormNode = 89 | let call = NormNode body.nilAsEmpty 90 | copyLineInfo(call, n) 91 | call 92 | else: 93 | template entrace(hook: static[Hook]; c, n, body: NormNode): NormNode = 94 | let event = bindSym(etype hook) 95 | let info = makeLineInfo n.lineInfoObj 96 | let fun = newLit(nameForNode n.NimNode) 97 | let call = newCall(Trace.sym, event, c.NimNode, abbreviation n.NimNode, 98 | "fun".eq fun, "info".eq info, body.NimNode).NormNode 99 | copyLineInfo(call, n) 100 | call 101 | 102 | proc hook*(hook: static[Hook]; n: NormNode): NormNode = 103 | ## execute the given hook on the given node 104 | case hook 105 | of Boot, Coop, Head: 106 | # hook(continuation) 107 | hook.entrace NilNormNode, n: 108 | newCall(hook.sym, n) 109 | else: 110 | # cast to `Call` avoids type mismatch as converters can't figure this out 111 | Call n.errorAst "the " & $hook & " hook doesn't take one argument" 112 | 113 | proc hook*(hook: static[Hook]; a, b: NormNode): NormNode = 114 | ## execute the given hook with two arguments 115 | case hook 116 | of Unwind: 117 | # hook(continuation, Cont) 118 | let unwind = hook.sym.NimNode 119 | Unwind.entrace a, b: 120 | NormNode: 121 | quote: 122 | if not `a`.ex.isNil: 123 | return `unwind`(`a`, `a`.ex).`b` 124 | of Pass: 125 | Pass.entrace a, b: 126 | newCall(hook.sym, a, b) 127 | of Tail: 128 | Tail.entrace a, b: 129 | newCall(hook.sym, a, b) 130 | of Alloc: 131 | Alloc.entrace a, b: 132 | newCall(hook.sym, a, b) 133 | of Dealloc: 134 | Dealloc.entrace a, b: 135 | newCall(hook.sym, a, b) 136 | of Stack: 137 | # hook(source, destination), or 138 | # dealloc(continuation, env_234234), or 139 | # alloc(Cont, env_234234), or 140 | # stack(symbol, continuation) 141 | when cpsStackFrames: 142 | Stack.entrace a, b: 143 | newCall(hook.sym, a, b) 144 | else: 145 | b 146 | of Trace: 147 | # trace(Pass, continuation, "whileLoop_2323", 148 | # LineInfo(filename: "...", line: 23, column: 44)): nil 149 | Trace.entrace a, b: 150 | NormNode newNilLit() # FIXME: nnkEmpty more appropriate 151 | else: 152 | b.errorAst "the " & $hook & " hook doesn't take two arguments" 153 | 154 | proc initFrame*(hook: Hook; fun: string; info: LineInfo): NimNode = 155 | ## prepare a tracing frame constructor 156 | result = nnkObjConstr.newTree bindSym"TraceFrame" 157 | result.add: "hook".colon newCall(bindSym"Hook", hook.ord.newLit) 158 | result.add: "info".colon info.makeLineInfo 159 | result.add: "fun".colon fun 160 | 161 | proc updateLineInfoForContinuationStackFrame*(c, n: NimNode): NimNode = 162 | ## `c` holds the continuation symbol, while `n` is a node with info 163 | when cpsStackFrames: 164 | newAssignment(c.dot("stack").dot("info"), n.lineInfoObj.makeLineInfo) 165 | else: 166 | newEmptyNode() 167 | -------------------------------------------------------------------------------- /cps/returns.nim: -------------------------------------------------------------------------------- 1 | import std/macros except newStmtList, items 2 | 3 | import cps/[spec, hooks, normalizedast] 4 | 5 | proc firstReturn*(p: NormNode): NormNode = 6 | ## Find the first control-flow return statement or cps 7 | ## control-flow within statement lists; else, nil. 8 | case p.kind 9 | of nnkReturnStmt, nnkRaiseStmt: 10 | result = p 11 | of nnkTryStmt, nnkStmtList, nnkStmtListExpr: 12 | for child in p.items: 13 | result = child.firstReturn 14 | if not result.isNil: 15 | break 16 | of nnkBlockStmt, nnkBlockExpr, nnkFinally, nnkPragmaBlock: 17 | result = p.last.firstReturn 18 | elif p.isScopeExit: 19 | result = p 20 | else: 21 | result = NilNormNode 22 | 23 | proc makeReturn*(contType: Name; n: NormNode): NormNode = 24 | ## generate a `return` of the node if it doesn't already contain a return 25 | if n.firstReturn.isNil: 26 | let toAdd = 27 | if n.kind in NormalCallNodes: 28 | n # what we're saying here is, don't hook Coop on magics 29 | else: 30 | Coop.hook: 31 | newCall contType: 32 | n # but we will hook Coop on child continuations 33 | nnkReturnStmt.newNimNode(n).add(toAdd) 34 | else: 35 | n 36 | 37 | proc makeReturn*(contType: Name; pre, n: NormNode): NormNode = 38 | ## if `pre` holds no `return`, produce a `return` of `n` after `pre` 39 | if not pre.firstReturn.isNil: 40 | result.add: 41 | n.errorAst "i want to know about this" 42 | result = newStmtList pre 43 | result.add: 44 | if pre.firstReturn.isNil: 45 | makeReturn(contType, n) 46 | else: 47 | newEmptyNode().NormNode 48 | #else: 49 | # doc "omitted a return of " & repr(n) 50 | 51 | template pass*(source: Continuation; destination: Continuation): Continuation {.used.} = 52 | ## This symbol may be reimplemented to introduce logic during 53 | ## the transfer of control between parent and child continuations. 54 | ## The return value specifies the destination continuation. 55 | Continuation destination 56 | 57 | proc dismiss*(continuation: sink Continuation): Continuation {.cpsMagic, used.} = 58 | ## A convenience which simply discards the continuation. 59 | discard 60 | 61 | proc terminator*(c: Name; contType: Name; tipe: NormNode): NormNode = 62 | ## produce the terminating return statement of the continuation; 63 | ## this should return control to the mom and dealloc the continuation, 64 | ## or simply set the fn to nil and return the continuation. 65 | let coop = NimNode hook(Coop, asName"result") 66 | let pass = NimNode hook(Pass, newCall(contType, c), c.dot "mom") 67 | let dealloc = NimNode hook(Dealloc, newCall(contType, c), tipe) 68 | let c = NimNode c 69 | NormNode: 70 | quote: 71 | if `c`.isNil: 72 | result = nil 73 | else: 74 | `c`.fn = nil 75 | if `c`.mom.isNil: 76 | result = `c` 77 | else: 78 | # pass(continuation, c.mom) 79 | #result = (typeof `c`) `pass` Error: expected type, but got: Continuation(continuation.mom) 80 | result = `pass` 81 | if result != `c`: 82 | `c`.mom = nil 83 | # perform a cooperative yield if pass() chose mom 84 | result = `coop` 85 | # dealloc(env_234234, continuation) 86 | discard `dealloc` 87 | # critically, terminate control-flow here! 88 | return 89 | 90 | proc tailCall*(cont, contType, to: Name; jump: NormNode = NilNormNode): NormNode = 91 | ## a tail call to `to` with `cont` as the continuation; if the `jump` 92 | ## is supplied, return that call instead of the continuation itself 93 | result = newStmtList: 94 | newAssignment(newDotExpr(cont, "fn".asName), to) 95 | 96 | # figure out what the return value will be... 97 | result = makeReturn(contType, result): 98 | if jump.isNil: 99 | cont.NormNode # just return our continuation 100 | else: 101 | jump # return the jump target as requested 102 | 103 | proc jumperCall*(cont, contType, to: Name; via: NormNode): NormNode = 104 | ## Produce a tail call to `to` with `cont` as the continuation 105 | ## The `via` argument is expected to be a cps jumper call. 106 | let jump = asCall via.copyNimTree 107 | # we may need to insert an argument if the call is magical 108 | if jump.impl.hasPragma "cpsMagicCall": 109 | # https://github.com/nim-lang/Nim/issues/18365 (fixed; inheritance) 110 | jump.prependArg newCall(contType, cont) 111 | # we need to desym the jumper; it is currently sem-ed to the 112 | # variant that doesn't take a continuation. 113 | desym jump 114 | result = tailCall(cont, contType, to, jump) 115 | -------------------------------------------------------------------------------- /docs/coroutines.md: -------------------------------------------------------------------------------- 1 | ### Walkthrough for the `coroutine.nim` example 2 | 3 | [examples/coroutine.nim](https://github.com/nim-works/cps/blob/master/examples/coroutine.nim) 4 | shows a basic implementation of coroutines communicating with each other on top 5 | of CPS. 6 | 7 | We're going to walk through the execution of this example looking into the 8 | details as we go, but first let's introduce the `Coroutine` type: 9 | 10 | ```nim 11 | type 12 | Coroutine = ref object of Continuation 13 | data: int 14 | next: Coroutine 15 | ``` 16 | 17 | This is a `Continuation` extended with the `data` on which we're going to perform 18 | a computation and a reference to some other `Coroutine` object. This will allow 19 | us to resume the execution of one coroutine from the other. 20 | 21 | The execution starts with instantiating our coroutines, which is necessary for 22 | continuations. The order is reversed because we need to pass a specific instance 23 | of the consumer to the filter. `filter` also takes an anonymous function as its 24 | second argument; in our case it's a simple doubling written in a short form 25 | provided by the `std/sugar` module. 26 | 27 | ```nim 28 | let coro2 = whelp consumer() 29 | let coro1 = whelp filter(coro2, x => x * 2) 30 | ``` 31 | 32 | Both coroutines are already "running" (`coroX.running == true`) but no work 33 | has been performed yet. Let's add a `resume` proc as our dispatcher for the 34 | coroutines. 35 | 36 | ```nim 37 | proc resume(c: Coroutine): Coroutine {.discardable.} = 38 | var c = Continuation c 39 | while c.running: 40 | c = c.fn(c) 41 | result = Coroutine c 42 | ``` 43 | 44 | Now we can `resume()` each coroutine. 45 | 46 | ```nim 47 | coro1.resume() 48 | coro2.resume() 49 | ``` 50 | 51 | Actually, this is such a basic pattern for CPS code that the library provides a 52 | [trampoline](https://nim-works.github.io/cps/cps/spec.html#trampoline%2CsinkT) 53 | template for this, and it would be preferable to just use it here. Also, notice 54 | how before invoking the function pointer we needed to convert a `Coroutine` to 55 | its base type so we could set `c` to `fn(c)`, which returns a `Continuation`. 56 | 57 | When invoked, `resume` launches the `filter` coroutine: 58 | 59 | ```nim 60 | proc filter(dest: Coroutine, f: proc(x: int): int) {.cps: Coroutine.} = 61 | while true: 62 | jield() 63 | let n = f(recv()) 64 | dest.send(n) 65 | ``` 66 | 67 | As the first coroutine launches, it yields (suspends execution) by calling 68 | `jield` immediately (*yield* is a keyword in Nim). This is necessary because we 69 | haven't actually sent any data to process, yet we need the Coroutines launched 70 | and waiting for it. We could move the suspension points later, but we don't want 71 | the coroutines processing the initialized-by-default value prior to the data we 72 | send them (try moving "jields" around and inspect the results). 73 | 74 | ```nim 75 | proc jield(c: Coroutine): Coroutine {.cpsMagic.} = 76 | c.next = c 77 | return nil 78 | ``` 79 | 80 | Our `jield` proc is a special function, as signified by the `{.cpsMagic.}` 81 | pragma. It allows controlling the execution flow of the continuation by 82 | returning the continuation leg to run next. Usually, it's the same as the proc's 83 | argument, but in our case, we store the passed continuation in the `next`. 84 | This way our coroutine could be resumed later. 85 | 86 | Since the `cpsMagic` `jield` returns `nil` the coroutine gets suspended. Notice, 87 | how CPS manages the control flow for us and hides the implementation details 88 | behind ~~a veil of magic~~ code transformations. 89 | 90 | `coro2` is launched next by calling `resume` and in the same manner `consumer` 91 | starts running and yields immediately. 92 | 93 | Next we start feeding our coroutines the data in a loop: 94 | 95 | ```nim 96 | for i in 1..10: 97 | coro1.send(i) 98 | ``` 99 | 100 | The `send` proc just sets the data the coroutine holds to the supplied number 101 | and calls `resume`, which takes us back to the previous `jield`, currently in 102 | the `filter` continuation. There we receive the data and try to process it with 103 | the passed function: 104 | 105 | ```nim 106 | let n = f(recv()) 107 | ``` 108 | 109 | The `recv` proc is another special one. `{.cpsVoodoo.}` allows returning the 110 | contents of the concrete instance of the running continuation (which is not 111 | accessible directly from the coroutine code of `filter` and `consumer`). 112 | `cpsVoodoo` functions are very similar to `cpsMagic` ones, as they both can 113 | access the contents of continuation and can only be called from the CPS 114 | functions (`filter` and `consumer`). While `cpsMagic` procs return the 115 | continuation (see `jield` description above), `cpsVoodoo` procs return 116 | arbitrary data. 117 | 118 | ```nim 119 | proc recv(c: Coroutine): int {.cpsVoodoo.} = 120 | c.data 121 | ``` 122 | 123 | Then we pass the processed data to the destination `consumer` coroutine: 124 | 125 | ```nim 126 | dest.send(n) 127 | ``` 128 | 129 | Again, the *sending* is just updating the state of the continuation and resuming 130 | it from `dest.next`. *Receiving*, then done by `consumer`, is just getting that 131 | data from the continuation. 132 | 133 | After setting the `value` to the received integer, we're finally able to print 134 | it: 135 | 136 | ```nim 137 | let value = recv() 138 | echo value 139 | ``` 140 | 141 | Since we're at the end of the code of both coroutines, they loop and yield 142 | again, now in the opposite order: first the `consumer` and then the `filter` 143 | suspends, which returns us to the main loop, ready to go again. 144 | 145 | -------------------------------------------------------------------------------- /docs/techo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 18 | 41 | 42 | 43 | 44 | 45 | 46 | zevv's echo service: off for now## 1 tests ❔1 47 | -------------------------------------------------------------------------------- /docs/tzevv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 18 | 41 | 42 | 43 | 44 | 45 | 46 | 🟢 nocall🟢 onecall🟢 twocall🟢 if true🟢 if false🟢 if true if false🟢 nested if 1🟢 nested if 2🟢 nested if 3🟢 block1🟢 while1🟢 break1🟢 break2🟢 for1🟢 for2🟢 multiple variables in one varone 0🟢 wrongreturn🟢 continue🟢 for3defer: pending try-finally rewrite🟢 nested while🟢 paper example 1 proc foo(c: C; fd: int16): C {.cpsMagic.} = discard 🟢 int🟢 'i16🟢 int16()🟢 .int16🟢 type problem01🟢 Running -> Lampable -> Done## 28 tests 🟢27 ❔1 47 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # examples 2 | 3 | A small collection of examples provides good demonstration of multiple patterns 4 | of CPS composition. Each example runs independently, with no other requirements, 5 | yet demonstrates different exploits of `cps`. 6 | 7 | | Example | Description | 8 | | --: | :-- | 9 | |[Channels](/examples/channels.nim)|A channel connects sender and receiver continuations| 10 | |[Goto](/examples/goto.nim)|Implementation of `label` and `goto` statements using CPS| 11 | |[Iterator](/examples/iterator.nim)|A simple demonstration of a CPS-based iterator| 12 | |[Coroutines](/examples/coroutine.nim)|A pair of continuations communicate as coroutines. [Walkthrough](/docs/coroutines.md).| 13 | |[Lazy](/examples/lazy.nim)|Lazy streams are composed by continuations in a functional style| 14 | |[Pipes](/examples/pipes.nim)|Coroutines compose streams which connect arbitrarily| 15 | |[TryCatch](/examples/trycatch.nim)|Exception handling is reimplemented using only CPS| 16 | |[CpsCps](/examples/cpscps.nim)|Continuations can efficiently call other continuations| 17 | |[Work](/examples/work.nim)|Implementation of a simple continuation scheduler| 18 | |[LuaCoroutines](/examples/lua_coroutines.nim)|Coroutines implemented in the style of Lua| 19 | |[ThreadPool](/examples/threadpool.nim)|1,000,000 continuations run across all your CPU cores| 20 | -------------------------------------------------------------------------------- /examples/coroutine.nim: -------------------------------------------------------------------------------- 1 | # Import cps and short `=>` syntax for anonymous functions 2 | import cps, std/sugar 3 | 4 | type 5 | Coroutine = ref object of Continuation 6 | data: int 7 | suspended: Coroutine 8 | 9 | # Used to both launch and continue the execution of coroutines 10 | template resume(c: Coroutine): untyped = 11 | discard trampoline c 12 | 13 | proc recv(c: Coroutine): int {.cpsVoodoo.} = 14 | c.data 15 | 16 | # Suspend execution of the coroutine 17 | proc suspend(c: Coroutine): Coroutine {.cpsMagic.} = 18 | c.suspended = c 19 | 20 | proc send(c: Coroutine, n: int) = 21 | c.data = n 22 | resume c.suspended 23 | 24 | # This coroutine receives the data, applies f and sends the result to consumer 25 | proc filter(dest: Coroutine, f: proc(x: int): int) {.cps:Coroutine.} = 26 | while true: 27 | suspend() 28 | let n = f(recv()) 29 | dest.send(n) 30 | 31 | # This coroutine receives ints through filter and prints them 32 | proc consumer() {.cps:Coroutine.} = 33 | while true: 34 | suspend() 35 | let value = recv() 36 | echo value 37 | 38 | let coro2 = whelp consumer() 39 | let coro1 = whelp filter(coro2, x => x * 2) 40 | 41 | resume coro1 42 | resume coro2 43 | 44 | # This prints numbers from 2 to 20 in 2 increment. 45 | for i in 1..10: 46 | coro1.send(i) 47 | 48 | # break the cycles 49 | reset coro1.suspended 50 | reset coro2.suspended 51 | -------------------------------------------------------------------------------- /examples/cpscps.nim: -------------------------------------------------------------------------------- 1 | 2 | import cps, deques 3 | 4 | ########################################################################### 5 | # Implementation of a minimal scheduler, just a dequeue of work 6 | ########################################################################### 7 | 8 | type 9 | Work = ref object of Continuation 10 | pool: Pool 11 | 12 | Pool = ref object 13 | workQueue: Deque[Work] 14 | yields: int 15 | 16 | proc push(pool: Pool, c: Work) = 17 | if c.running: 18 | echo "pool was supplied to push" 19 | c.pool = pool 20 | pool.workQueue.addLast(c) 21 | 22 | proc jield(c: Work): Work {.cpsMagic.} = 23 | inc c.pool.yields 24 | c.pool.push c 25 | 26 | proc run(pool: Pool) = 27 | while pool.workQueue.len > 0: 28 | var c = Continuation: pool.workQueue.popFirst 29 | c = trampoline c 30 | pool.push c.Work 31 | 32 | proc tail(mom: Continuation; c: Work): Work = 33 | echo "tail copied the pool" 34 | result = c 35 | result.mom = mom 36 | result.pool = mom.Work.pool 37 | 38 | ########################################################################### 39 | # Main code 40 | ########################################################################### 41 | 42 | var total: int 43 | 44 | proc deeper(b: ref int) {.cps:Work.} = 45 | echo " deeper() in, b: ", b[] 46 | jield() 47 | inc total, b[] 48 | echo " deeper() out" 49 | 50 | proc foo(a: int) {.cps:Work.} = 51 | echo " foo() in a: ", a 52 | echo " foo() yield()" 53 | jield() 54 | echo " foo() yield done()" 55 | echo " foo() calls deeper()" 56 | # CPS does not support var parameters yet. We can box an int tho 57 | var b = new int; 58 | b[] = a * 2 59 | deeper(b) 60 | echo " foo() returned from deeper(), b: ", b[] 61 | echo " foo() out" 62 | 63 | proc bar() {.cps:Work.} = 64 | echo "bar() in" 65 | echo "bar() yield" 66 | jield() 67 | echo "bar() yield done" 68 | echo "bar() calls foo(1)" 69 | foo(1) 70 | echo "bar() returned from foo()" 71 | echo "bar() calls foo(2)" 72 | foo(2) 73 | echo "bar() returned from foo()" 74 | echo "bar() out" 75 | 76 | 77 | var pool = Pool() 78 | pool.push: Work whelp bar() 79 | pool.run() 80 | 81 | echo pool.yields 82 | doAssert pool.yields == 5 83 | echo total 84 | doAssert total == 6 85 | -------------------------------------------------------------------------------- /examples/goto.nim: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # This is an example implementation for a basic CPS-based iterator. 4 | # 5 | 6 | import cps, tables 7 | 8 | 9 | type 10 | C = ref object of Continuation 11 | labels: Table[string, ContinuationFn] 12 | 13 | # Define the CPS magic 'label' and 'goto' procs 14 | 15 | proc label(c: C, id: string): C {.cpsMagic.} = 16 | c.labels[id] = c.fn 17 | return c 18 | 19 | proc goto(c: C, id: string): C {.cpsMagic.} = 20 | c.fn = c.labels[id] 21 | result = c 22 | 23 | 24 | # A little function with gotos 25 | 26 | proc foo() {.cps:C.} = 27 | echo "one" 28 | label"here" 29 | echo "two" 30 | echo "three" 31 | goto"here" 32 | echo "four" 33 | 34 | 35 | # Trampoline 36 | 37 | var c = whelp foo() 38 | var x = 0 39 | trampolineIt c: 40 | if x > 100: 41 | break 42 | inc x 43 | -------------------------------------------------------------------------------- /examples/iterator.nim: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # This is an example implementation for a basic CPS-based iterator. 4 | # 5 | 6 | import cps, options 7 | 8 | # This is our iterator type. It holds the continuation function 9 | # and an Option[int] to pass the last produced value 10 | 11 | type Iterator = ref object of Continuation 12 | val: Option[int] 13 | 14 | # The `produce` proc is called to pump the iterator. It will trampoline the 15 | # continuation until a value is available in `val`. 16 | 17 | proc produce(c: sink Iterator): Option[int] = 18 | trampolineIt c: 19 | if (Iterator it).val.isSome: 20 | break 21 | if not c.dismissed: 22 | if c.val.isSome: 23 | result = c.val 24 | c.val = none(int) 25 | 26 | 27 | # The `jield` proc is cps magic to generate a new value from within an 28 | # interator 29 | 30 | proc jield(c: Iterator, val: int): Iterator {.cpsMagic.} = 31 | c.val = some(val) 32 | return c 33 | 34 | 35 | # A simple counting iterator, will produce all integers from 'lo' to 'high', 36 | # inclusive 37 | 38 | proc counter(lo, hi: int) {.cps:Iterator.} = 39 | var i = lo 40 | while i <= hi: 41 | jield(i) 42 | inc i 43 | 44 | 45 | # Create an instance of the iterator, counting from 3 up to 7 46 | 47 | var a = whelp counter(3, 7) 48 | 49 | # Resume the iterator a bunch of times 50 | 51 | echo "produced ", a.produce() 52 | echo "produced ", a.produce() 53 | echo "produced ", a.produce() 54 | echo "produced ", a.produce() 55 | echo "produced ", a.produce() 56 | echo "produced ", a.produce() 57 | 58 | -------------------------------------------------------------------------------- /examples/lazy.nim: -------------------------------------------------------------------------------- 1 | import std/deques 2 | import std/macros 3 | import std/sugar 4 | 5 | import pkg/cps 6 | 7 | ########################################################################### 8 | # Lazy streams 9 | ########################################################################### 10 | 11 | type 12 | Stream = ref object of Continuation ## a stream chains transformations... 13 | val: int ## ...of integers. 14 | next: Stream 15 | 16 | macro stream(n: untyped): untyped = 17 | ## prettify stream api 18 | n.addPragma nnkExprColonExpr.newTree(ident"cps", ident"Stream") 19 | n 20 | 21 | proc jield(s: Stream; val: int = 0): Stream {.cpsMagic.} = 22 | ## emit a value into the stream 23 | s.val = val 24 | 25 | proc next(s: Stream): var Stream {.cpsVoodoo.} = 26 | ## the next stream in the chain 27 | s.next 28 | 29 | proc run(s: var Stream): int {.discardable.} = 30 | ## run a stream to produce a value 31 | discard trampoline s 32 | result = s.val 33 | 34 | template `->`(ca: Stream, b: typed): Stream = 35 | ## composition operator 36 | let cb = whelp(b) 37 | cb.next = ca 38 | cb 39 | 40 | template `->`(a, b: typed): Stream = 41 | ## composition operator 42 | let ca = whelp(a) 43 | let cb = whelp(b) 44 | cb.next = ca 45 | cb 46 | 47 | 48 | ########################################################################### 49 | 50 | proc toStream(slice: Slice[int]) {.stream.} = 51 | ## turn any slice into a lazy stream 52 | var i = slice.a 53 | while i <= slice.b: 54 | jield i 55 | inc i 56 | 57 | proc map(fn: proc(x: int): int) {.stream.} = 58 | ## lazily apply a function to a stream 59 | while true: 60 | let v = fn: run next() 61 | if next().finished: break 62 | jield(v) 63 | 64 | proc filter(fn: proc(x: int): bool) {.stream.} = 65 | ## lazily apply a predicate to a stream 66 | while true: 67 | let v = run next() 68 | if next().finished: break 69 | if fn(v): 70 | jield(v) 71 | 72 | proc print() {.stream.} = 73 | ## lazily echo a stream 74 | while true: 75 | let v = run next() 76 | if next().finished: break 77 | echo v 78 | jield() 79 | 80 | proc pump() {.stream.} = 81 | ## iterate the stream processor 82 | while next().running: 83 | run next() 84 | 85 | 86 | when isMainModule: 87 | # create a lazy stream 88 | var s = 89 | toStream(1..50) -> 90 | map(x => x * 3) -> 91 | filter(x => (x mod 2) == 0) -> 92 | print() -> 93 | pump() 94 | 95 | # lazily run it 96 | run s 97 | -------------------------------------------------------------------------------- /examples/lua_coroutines.nim: -------------------------------------------------------------------------------- 1 | 2 | ########################################################################### 3 | # 4 | # Lua-style assymetrical coroutines 5 | # 6 | # resume(co, val): 7 | # 8 | # Starts or continues the execution of coroutine co. The first time you 9 | # resume a coroutine, it starts running its body. If the coroutine has 10 | # yielded, resume() restarts it; the value `val` is passed as the results 11 | # from the yield(). 12 | # 13 | # send(val): 14 | # yield(): 15 | # 16 | # Suspends the execution of the calling coroutine. The value `val` 17 | # passed to sent is returned as results from resume(). 18 | # 19 | ########################################################################### 20 | 21 | import cps, options, deques 22 | 23 | type 24 | Coroutine = ref object of Continuation 25 | val: int 26 | 27 | # Magic procs for yielding and receiving. Note: we actually want 28 | # to have yield() and receive() in one single operation so we can 29 | # do `vlaOut = yield(valIn)` 30 | 31 | proc jield(c: Coroutine, val: int): Coroutine {.cpsMagic.} = 32 | c.val = val 33 | 34 | proc recv(c: Coroutine): int {.cpsVoodoo.} = 35 | return c.val 36 | 37 | proc resume(c: Coroutine, val: int): int = 38 | c.val = val 39 | discard c.trampoline() 40 | return c.val 41 | 42 | # This coroutine calculates the running total of the passed numbers 43 | 44 | proc fn_coro1() {.cps:Coroutine.} = 45 | var sum = 0 46 | while true: 47 | sum += recv() 48 | jield(sum) 49 | 50 | var coro = whelp fn_coro1() 51 | 52 | for i in 0..10: 53 | echo coro.resume(i) 54 | -------------------------------------------------------------------------------- /examples/pipes.nim: -------------------------------------------------------------------------------- 1 | import std/deques 2 | import std/macros 3 | import std/options 4 | import std/strutils 5 | 6 | import pkg/cps 7 | 8 | ########################################################################### 9 | # Pipes implementation 10 | ########################################################################### 11 | 12 | type 13 | Pipe = ref object ## A pipe connects sender and receiver CPS procs. 14 | sender: Continuation 15 | receiver: Continuation 16 | value: Option[int] ## Pipes carry integer values. 17 | 18 | State = enum 19 | Empty 20 | Full 21 | 22 | proc state(pipe: Pipe): State = 23 | ## Pipes can be Empty or Full. 24 | case pipe.value.isNone 25 | of true: Empty 26 | of false: Full 27 | 28 | ## Conveniences. 29 | template isEmpty(pipe: Pipe): bool = pipe.state == Empty 30 | template isFull(pipe: Pipe): bool = pipe.state == Full 31 | 32 | template isSending(pipe: Pipe): bool = pipe.sender.state == Running 33 | template isReceiving(pipe: Pipe): bool = pipe.receiver.state == Running 34 | 35 | proc `$`(pipe: Pipe): string = 36 | "<$# pipe $# $#>" % [ $pipe.state, cast[int](pipe).toHex(2), $pipe.value ] 37 | 38 | proc wait(c: Continuation; pipe: Pipe): Continuation {.cpsMagic.} = 39 | ## Waits until the `pipe` is ready to be read. Raises a ValueError 40 | ## if the writing side of the pipe has terminated. 41 | case pipe.state 42 | of Empty: # the pipe is empty, and 43 | if pipe.isSending: 44 | echo "stall" # nothing is available to receive; 45 | pipe.receiver = c # store ourselves in the pipe, and 46 | result = nil # rely on pump() to resume us later. 47 | else: 48 | echo "hang-up" # the sender is no longer running! 49 | raise ValueError.newException "unexpected hang-up" 50 | of Full: 51 | result = c # no need to wait on a full pipe! 52 | 53 | proc recv(pipe: Pipe): int {.cps: Continuation.} = 54 | ## Read a value from the `pipe`. 55 | wait pipe # wait until the pipe is ready. 56 | result = get pipe.value # recover a result from the pipe, 57 | reset pipe.value # the pipe is now empty. 58 | echo "recv ", result 59 | 60 | proc isComplete(pipe: Pipe): bool = 61 | ## Truthy if the `pipe` has ceased. 62 | case pipe.state 63 | of Empty: 64 | not pipe.isSending # it's empty and there's no sender. 65 | of Full: 66 | false # if it's Full, it's not complete. 67 | 68 | proc send(c: Continuation; pipe: Pipe; value: int): Continuation {.cpsMagic.} = 69 | ## Send a `value` into the `pipe`. 70 | case pipe.state 71 | of Full: # we cannot send into a Full pipe; 72 | pipe.sender = nil # rely on pump() to resume us later. 73 | echo "block" 74 | of Empty: 75 | pipe.value = some value # deposit a value in the pipe. 76 | echo "send ", value 77 | 78 | proc pump(pool: openArray[Pipe]) = 79 | ## Run all pipes to completion. 80 | var pool = pool.toDeque 81 | while pool.len > 0: 82 | let pipe = pool.popFirst() 83 | echo "\n-----", pipe, "-----" 84 | case pipe.state 85 | of Empty: 86 | echo " ", pipe.sender.state, " sender" 87 | if pipe.isSending: 88 | discard trampoline pipe.sender 89 | pool.addLast pipe 90 | of Full: 91 | echo " ", pipe.receiver.state, " receiver" 92 | if pipe.isReceiving: 93 | discard trampoline pipe.receiver 94 | pool.addLast pipe 95 | 96 | 97 | ########################################################################### 98 | # Main program 99 | # 100 | # source --> speaker 101 | # ^ 102 | # | 103 | # pipe 104 | # 105 | ########################################################################### 106 | 107 | proc source(pipe: Pipe; lo: int; hi: int) {.cps: Continuation.} = 108 | ## A simple source, generating numbers. 109 | var i = lo 110 | while i <= hi: 111 | pipe.send(i) 112 | inc i 113 | 114 | proc speaker(pipe: Pipe) {.cps: Continuation.} = 115 | ## A simple sink, echoing what is received. 116 | var saying: int 117 | while not pipe.isComplete: 118 | echo recv(pipe), ", sayeth the speaker" 119 | 120 | block: 121 | ## Create a pipe and hook it up to a source and a speaker. 122 | var pipe = new Pipe 123 | pipe.sender = whelp source(pipe, 10, 12) 124 | pipe.receiver = whelp speaker(pipe) 125 | 126 | pump [pipe] 127 | echo " (end of program)\n\n" 128 | 129 | ########################################################################### 130 | # Main program two. 131 | # 132 | # source --> filter --> speaker 133 | # ^ ^ 134 | # | | 135 | # one two 136 | # 137 | ########################################################################### 138 | 139 | type 140 | BinaryOp = proc(x: int; y: int): int {.cps: Continuation.} 141 | 142 | proc filter(inputs, outputs: Pipe; mutate: BinaryOp) {.cps: Continuation.} = 143 | ## The filter connects two pipes; it reads from one, 144 | ## applies a binary operation to a running value, 145 | ## and sends that value to the second pipe. 146 | var total: int 147 | while not inputs.isComplete: 148 | echo "filter value $# awaits mutation" % [ $total ] 149 | try: 150 | let value = recv inputs 151 | total = mutate(total, value) 152 | echo "filter value $# post mutation by $#" % [ $total, $value ] 153 | except ValueError: 154 | echo "broken pipe" # exception handling 155 | break 156 | outputs.send(total) # no error control here 157 | 158 | 159 | proc addition(running: int; value: int): int {.cps: Continuation.} = 160 | ## An example mutating continuation. 161 | running + value 162 | 163 | 164 | block: 165 | ## Create two pipes. 166 | ## - one connects source and filter 167 | ## - two connects filter and speaker 168 | 169 | const 170 | Adder {.used.} = whelp addition 171 | var one, two = new Pipe 172 | let transformer = whelp filter(one, two, Adder) 173 | 174 | one.sender = whelp source(one, 10, 20) 175 | one.receiver = transformer 176 | 177 | two.sender = transformer 178 | two.receiver = whelp speaker(two) 179 | 180 | pump [one, two] 181 | echo " (end of program)\n\n" 182 | -------------------------------------------------------------------------------- /examples/threadpool.nim: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Simple test of a naive threadpool for scheduling CPS threads. This demo 4 | # creates many 'threads' scheduled on all available CPUs in the system 5 | # 6 | # nim r --gc:arc --threads:on --threadanalysis:off stash/threadpool.nim 7 | # 8 | 9 | import cps, math, std/locks, deques, cpuinfo 10 | 11 | ########################################################################### 12 | # Implementation of the thread pool 13 | ########################################################################### 14 | 15 | type Cont = ref object of Continuation 16 | 17 | 18 | type Pool = ref object 19 | work: Deque[Cont] 20 | lock: Lock 21 | 22 | proc newPool(): Pool = 23 | result = Pool() 24 | initLock(result.lock) 25 | 26 | 27 | var pool = newPool() 28 | 29 | proc doWork(pool: Pool) {.thread.} = 30 | 31 | while true: 32 | 33 | pool.lock.acquire 34 | if pool.work.len == 0: 35 | pool.lock.release 36 | return 37 | 38 | var c = Continuation: pool.work.popFirst 39 | pool.lock.release 40 | 41 | if c.dismissed: 42 | break 43 | else: 44 | c = trampoline c 45 | 46 | 47 | proc work(nThreads: int) = 48 | var threads: seq[Thread[Pool]] 49 | newSeq(threads, nThreads) 50 | for i in 0.. 0: 28 | var w = pool.workQueue.popFirst 29 | pool.push w.trampoline 30 | 31 | ########################################################################### 32 | # Main code 33 | ########################################################################### 34 | 35 | proc job(id: string, n: int) {.cps:Work.} = 36 | echo "job ", id, " in" 37 | var i = 0 38 | while i < n: 39 | echo "job ", id, ": ", i 40 | jield() 41 | inc i 42 | echo "job ", id, " out" 43 | 44 | let pool = Pool() 45 | pool.push job("cat", 3) 46 | pool.push job("dog", 5) 47 | pool.push job("pig", 3) 48 | pool.run() 49 | 50 | -------------------------------------------------------------------------------- /experiments/README.md: -------------------------------------------------------------------------------- 1 | # experiments 2 | 3 | A bunch of toys and tests that may inform design decisions. 4 | -------------------------------------------------------------------------------- /experiments/chain.nim: -------------------------------------------------------------------------------- 1 | import std/strutils 2 | import std/macros 3 | import std/os 4 | import std/deques 5 | import std/options 6 | import std/algorithm 7 | 8 | {.experimental: "dotOperators".} 9 | {.experimental: "callOperator".} 10 | 11 | type 12 | ## just a generic continuation concept 13 | C[T] = concept c 14 | c.fn is P 15 | c.data is T 16 | Pv[C] = proc (c: var C): C {.nimcall.} 17 | Pl[C] = proc (c: C): C {.nimcall.} 18 | P[C] = Pv[C] or Pl[C] 19 | 20 | proc next(c: C): C = 21 | echo "next" 22 | if c.fn.isNil: 23 | result = c 24 | else: 25 | result = c.fn(c) 26 | 27 | proc `()`[T](c: var C[T]): T = 28 | ## run a continuation; returns a result 29 | while not c.fn.isNil: 30 | #stdout.write "run " 31 | c = c.fn(c) 32 | result = c.data 33 | 34 | proc `...`[T](d: T; c: C[T]) = 35 | # asgn 36 | echo "asgn" 37 | c.data = d 38 | 39 | proc `...`[T](c: C[T]; d: T): C[T] = 40 | # asgn 41 | echo "asgn" 42 | result = c 43 | result.data = d 44 | 45 | proc `...`[T](a, b: C[T]): C[T] = 46 | # and 47 | echo "and" 48 | result = a(b) 49 | 50 | proc `...`[T](c: var C[T]; p: proc (m: var T)): C[T] = 51 | echo "mutate" 52 | result = c 53 | p(result.data) 54 | 55 | proc `...`[T](c: var C[T]; p: proc (m: T): T): C[T] = 56 | echo "replace" 57 | result = c 58 | result.data = p(c.data) 59 | 60 | proc `...`(c: C; p: P[C]): C = 61 | # continue 62 | echo "continue" 63 | result = c 64 | result.fn = p 65 | 66 | 67 | when false: 68 | type 69 | ## we'll design our own generic datatype 70 | Fibber[T] = object 71 | fn: proc (c: var Fibber[T]): Fibber[T] {.nimcall.} 72 | data: seq[T] 73 | 74 | static: 75 | assert Fibber[int] is C[seq[int]] 76 | 77 | proc grow[T](c: var Fibber[T]): Fibber[T] = 78 | ## "grow" a continuation; whatever that means 79 | echo "grow()" 80 | result = c 81 | let d = c.data[^2].ord + c.data[^1].ord 82 | if d > high(T).ord: 83 | echo "except high enough ", d 84 | result.fn = nil 85 | else: 86 | result.data.add T(d) 87 | 88 | proc shrink[T](c: var Fibber[T]): Fibber[T] = 89 | ## the opposite operation 90 | echo "shrink()" 91 | result = c 92 | if len(c.data) > 0: 93 | var d = initDeque[T]() 94 | for n in items(c.data): 95 | d.addLast n 96 | echo "shrunk off ", popFirst(d) 97 | result.data = newSeq[T](len(d)) 98 | for i, n in pairs(d): 99 | result.data[i] = n 100 | if len(result.data) <= 8: 101 | echo "except low enough" 102 | result.fn = nil 103 | 104 | proc calculate[T: Ordinal](n: T): seq[T] = 105 | assert grow is P # grow is a continuation proc 106 | 107 | # make a "computer" using code and data 108 | var 109 | c: Fibber[T] 110 | 111 | assert c is C # c is continuation 112 | assert c is C[seq[T]] # c is abstraction 113 | assert c is Fibber[T] # c is concrete type 114 | 115 | let a = @[n, n.succ] 116 | #c = a ... grow[T] 117 | #c = c ... a # stuff data in 118 | #c.data = a 119 | #c.fn = grow[T] # stuff code in 120 | c = Fibber[T](fn: grow[T], data: a) 121 | 122 | # call the continuation to produce a result 123 | result = c() 124 | let l = len(c()) 125 | echo c() 126 | assert l == len(c()) 127 | 128 | var d = c ... shrink[T] 129 | var e = d ... shrink[T] # uses current value of c! 130 | assert d is C # these are continuations, 131 | assert e is C # what else? 132 | 133 | # c() does not run grow() again 134 | assert len(e()) == 8 # e has run shrink() once 135 | assert len(d()) == 8 # d has run shrink() once 136 | 137 | echo "pre-shrunk: ", c() # noop; yields result 138 | echo " shrunk: ", d() # noop; yields result 139 | echo "veryshrunk: ", e() # noop; yields result 140 | 141 | # do it all at once 142 | var f = e.shrink() # resolved continuation 143 | assert f is C # of course 144 | echo " frunk: ", f() # noop; yields result 145 | 146 | assert calculate(0'i8) == @[0'i8, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89] 147 | assert calculate(0'u8) == @[0'u8, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233] 148 | 149 | block: 150 | type 151 | Foo = object 152 | fn: proc (c: var Foo): Foo {.nimcall.} 153 | data: string 154 | M = proc (s: string): string 155 | 156 | static: 157 | assert Foo is C 158 | assert Foo is C[string] 159 | 160 | proc `<-`(c: var Foo; s: string) = 161 | c.data = s 162 | 163 | proc `->`(c: var Foo; p: Foo.fn): Foo = 164 | result = c 165 | result.fn = p 166 | 167 | macro `->`[T](d: T; c: var Foo): Foo = 168 | result = newStmtList() 169 | result.add newAssignment(newDotExpr(c, ident"data"), d) 170 | result.add c 171 | echo treeRepr(result) 172 | 173 | macro `->`(c: var Foo; p: M): Foo = 174 | result = copyNimNode(c) 175 | var body = newStmtList() 176 | let data = newDotExpr(c, ident"data") 177 | body.add newAssignment(newDotExpr(ident"c", ident"data"), 178 | newCall(p, data)) 179 | body.add newAssignment(newDotExpr(c, ident"fn"), newNilLit()) 180 | var lambda = newProc(newEmptyNode(), body = body, 181 | params = [ident"Foo", 182 | newIdentDefs(ident"c", 183 | newTree(nnkVarTy, ident"Foo"), 184 | newEmptyNode())]) 185 | result = newStmtList() 186 | result.add newAssignment(newDotExpr(ident"c", ident"fn"), lambda) 187 | result.add ident"c" 188 | echo treeRepr(result) 189 | 190 | var c: Foo 191 | expandMacros: 192 | var upper = "hello" -> c -> toUpperAscii 193 | echo "c = ", c() 194 | echo "u = ", upper() 195 | -------------------------------------------------------------------------------- /experiments/eventqueue.nim: -------------------------------------------------------------------------------- 1 | import std/locks 2 | import std/selectors 3 | import std/monotimes 4 | import std/nativesockets 5 | import std/tables 6 | import std/times 7 | import std/deques 8 | 9 | ## Basic poll based event loop 10 | 11 | type 12 | Id = int 13 | 14 | Ticks = int64 15 | Clock = MonoTime 16 | Fd = int32 17 | 18 | HandlerKind = enum ## handlers come in different flavors 19 | Time = "trigger after a duration or at a specific time" 20 | Sock = "trigger in response to read/write status on a socket" 21 | Poll = "trigger according to various events on a polled selector" 22 | File = "trigger via a simple select() on a file descriptor" 23 | Ring = "trigger when async i/o is available for a linux fd" 24 | #When = "trigger when a condition variable is signalled" 25 | 26 | Handler[C] = object 27 | id: Id 28 | cont: C 29 | fn: Fn[C] 30 | deleted: bool 31 | 32 | case kind: HandlerKind 33 | of Time: 34 | interval: Duration 35 | hence: Clock 36 | of Sock: 37 | sock: SocketHandle 38 | of Poll: 39 | selector: Selector[Fd] 40 | of Ring, File: 41 | fd: Fd 42 | #of When: 43 | # cond: Cond 44 | 45 | Evq*[C] = object ## event queue 46 | stop: bool ## stop the dispatcher 47 | clock: Clock ## current time 48 | sockers: Table[Id, Handler[C]] ## socket handlers 49 | timers: Table[Id, Handler[C]] ## timer handlers 50 | selectors: Deque[Handler[C]] ## selector handlers 51 | lastId: Id ## id of last-issued handler 52 | 53 | Fn*[C] = proc(c: var C): pointer {.nimcall.} 54 | Cont* = object 55 | i*: int 56 | Continuation = concept c, type C 57 | C is object 58 | 59 | # Continuation trampoline 60 | 61 | proc run*[C: Continuation](c: var C; fn: Fn[C]) = 62 | var fn = fn 63 | while fn != nil: 64 | fn = cast[Fn[C]](fn(c)) 65 | 66 | proc run*[C: Continuation](c: C; fn: Fn[C]) = 67 | var c = c 68 | run(c, fn) 69 | 70 | proc init[C](evq: var Evq[C]) = 71 | evq.selectors = initDeque[Handler[C]](4) 72 | 73 | var evq {.threadvar.}: Evq[Cont] 74 | init evq 75 | 76 | template rightNow(): Clock = getMonoTime() 77 | 78 | proc nextId(): Id = 79 | inc evq.lastId 80 | result = evq.lastId 81 | 82 | # Register/unregister a file descriptor to the loop 83 | 84 | proc initHandler[C](kind: HandlerKind; cont: C; fn: Fn): Handler[C] = 85 | ## create a new handler for the given continuation 86 | result = Handler[C](kind: kind, cont: cont, fn: fn, id: nextId()) 87 | 88 | template add(tab: var Table[Id, Handler]; handler: Handler) = 89 | tab.add(handler.id, handler) 90 | 91 | proc addFd*(sock: SocketHandle; cont: Continuation; fn: Fn) = 92 | var handler = initHandler(Sock, cont, fn) 93 | handler.sock = sock 94 | evq.sockers.add handler 95 | 96 | proc delFd*(id: Id) = 97 | evq.sockers[id].deleted = true 98 | evq.sockers.del id 99 | 100 | # Register/unregister timers 101 | 102 | proc addTimer*[C](cont: C; fn: Fn[C]; interval: Duration) = 103 | var handler = initHandler(Time, cont, fn) 104 | handler.interval = interval 105 | handler.hence = rightNow() + interval 106 | evq.timers.add handler 107 | 108 | proc addTimer*[C](cont: C; fn: Fn[C]; ticks: int64) = 109 | let interval = initDuration(nanoseconds = ticks) 110 | addTimer(cont, fn, interval) 111 | 112 | proc addTimer*[C](cont: C; fn: Fn[C]; time: float) = 113 | let interval = initDuration(nanoseconds = (time * 1_000_000).int64) 114 | addTimer(cont, fn, interval) 115 | 116 | proc delTimer*(id: Id) = 117 | evq.timers[id].deleted = true 118 | evq.timers.del id 119 | 120 | proc pollTimers[C](evq: var Evq[C]) = 121 | ## Call expired timer handlers. Don't call while iterating because 122 | ## callbacks might mutate the list 123 | 124 | if len(evq.timers) > 0: 125 | evq.clock = rightNow() 126 | var timers: seq[Handler[C]] 127 | 128 | for timer in values(evq.timers): 129 | if not timer.deleted: 130 | if evq.clock > timer.hence: 131 | timers.add timer 132 | 133 | # FIXME: sort the list here 134 | 135 | for timer in items(timers): 136 | if not timer.deleted: 137 | run(timer.cont, timer.fn) 138 | delTimer timer.id 139 | 140 | proc soonestTimer(sleepy: var Duration) = 141 | ## the sleep duration may be reduced by a pending timer 142 | if len(evq.timers) > 0: 143 | for timer in values(evq.timers): 144 | if not timer.deleted: 145 | let until = timer.hence - evq.clock 146 | if until > DurationZero: 147 | sleepy = min(sleepy, until) 148 | else: 149 | sleepy = DurationZero 150 | break 151 | 152 | proc poll[C](evq: var Evq[C]) = 153 | ## Run one iteration of the dispatcher 154 | 155 | # Calculate sleep time 156 | 157 | evq.clock = rightNow() 158 | var sleepy = initDuration(seconds = 1) 159 | 160 | # perhaps we should wait less than `sleepy`? 161 | soonestTimer(sleepy) 162 | 163 | # Collect sockets for select 164 | var ready: seq[SocketHandle] 165 | for socker in values(evq.sockers): 166 | if not socker.deleted: 167 | ready.add socker.sock 168 | 169 | let r = selectRead(ready, sleepy.inMilliseconds.int) 170 | assert r != -1 171 | 172 | # run any timers that are ready 173 | pollTimers evq 174 | 175 | # if sockets are ready, run them 176 | if r > 0: 177 | 178 | # Call sock handlers with events. Don't call while iterating because 179 | # callbacks might mutate the list 180 | 181 | var sockers: seq[Handler[C]] 182 | 183 | for socket in items(ready): 184 | for socker in values(evq.sockers): 185 | if not socker.deleted and socker.sock == socket: 186 | sockers.add socker 187 | 188 | for socker in items(sockers): 189 | if not socker.deleted: 190 | run(socker.cont, socker.fn) 191 | delFd socker.id 192 | 193 | 194 | proc stop*() = 195 | ## tell the dispatcher to stop 196 | evq.stop = true 197 | 198 | proc forever*() = 199 | ## Run forever 200 | while not evq.stop: 201 | poll evq 202 | if evq.timers.len == 0: 203 | break 204 | -------------------------------------------------------------------------------- /experiments/main.nim: -------------------------------------------------------------------------------- 1 | import eventqueue 2 | 3 | proc ticker(c: var Cont): pointer = 4 | inc c.i 5 | echo "tick ", c.i 6 | if c.i < 200: 7 | c.addTimer(ticker, 0.2 * 1000) 8 | 9 | run Cont(i: 0): ticker 10 | run Cont(i: 100): ticker 11 | 12 | forever() 13 | -------------------------------------------------------------------------------- /experiments/main_tcp.nim: -------------------------------------------------------------------------------- 1 | 2 | import eventqueue 3 | import nativesockets 4 | import os 5 | 6 | const port = 9000 7 | 8 | 9 | # All procs happen to use the same continuation type, so I made 10 | # only 1 here 11 | 12 | type SocketCont = ref object of Cont 13 | sock: SocketHandle 14 | 15 | 16 | # Some helper procs 17 | 18 | proc write(s: SocketHandle, buf: string) = 19 | discard send(s, buf[0].unsafeAddr, buf.len.cint, 0) 20 | 21 | proc read(sock: SocketHandle, len=256): string = 22 | result = newString(len) 23 | let r = recv(sock, result[0].addr, len.cint, 0) 24 | result.setlen(r) 25 | 26 | # proc doClient(sock: SocketHandle) = 27 | # sock.write("Hello! Please type something.\n") 28 | # while true: 29 | # waitForSock(sock) 30 | # var buf = sock.read() 31 | # echo "buf ", buf, " ", buf.len 32 | # if buf.len > 0: 33 | # sock.write("You sent " & $buf.len & " characters\n") 34 | # discard 35 | # else: 36 | # echo "Client went away" 37 | # break 38 | 39 | proc doClient2(cont: Cont): Cont 40 | 41 | proc doClient1(cont: Cont): Cont = 42 | var sock = cont.SocketCont.sock 43 | var buf = sock.read() 44 | echo "buf ", buf, " ", buf.len 45 | if buf.len > 0: 46 | sock.write("You sent " & $buf.len & " characters\n") 47 | return SocketCont(fn: doClient2, sock: sock) 48 | else: 49 | echo "Client went away" 50 | 51 | proc doClient2(cont: Cont): Cont = 52 | var sock = cont.SocketCont.sock 53 | addFd sock, SocketCont(fn: doClient1, sock: sock) 54 | 55 | proc doClient(cont: Cont): Cont = 56 | var sock = cont.SocketCont.sock 57 | sock.write("Hello! Please type something.\n") 58 | SocketCont(fn: doClient2, sock: sock) 59 | 60 | 61 | # proc doServer(sock: SocketHandle) = 62 | # while true: 63 | # waitForSock(sock) 64 | # var sa: Sockaddr_in 65 | # var saLen = sizeof(sa).SockLen 66 | # let sockc = accept(sock, cast[ptr SockAddr](sa.addr), saLen.addr) 67 | # echo "Accepted new client" 68 | # doClient(sockc) 69 | 70 | proc doServer2(c: Cont): Cont 71 | 72 | proc doServer1(c: Cont): Cont = 73 | var sock = c.SocketCont.sock 74 | var sa: Sockaddr_in 75 | var saLen = sizeof(sa).SockLen 76 | let sockc = accept(sock, cast[ptr SockAddr](sa.addr), saLen.addr) 77 | echo "Accepted new client" 78 | SocketCont(fn: doClient, sock: sockc).run() 79 | SocketCont(fn: doServer2, sock: sock) 80 | 81 | proc doServer2(c: Cont): Cont = 82 | var sock = c.SocketCont.sock 83 | addFd sock, SocketCont(fn: doServer1, sock: sock) 84 | 85 | proc doServer(c: Cont): Cont = 86 | var sock = c.SocketCont.sock 87 | SocketCont(fn: doServer2, sock: sock) 88 | 89 | 90 | # Create TCP server socket and coroutine 91 | 92 | let sock = createNativeSocket() 93 | var sa: Sockaddr_in 94 | sa.sin_family = AF_INET.uint16 95 | sa.sin_port = ntohs(port) 96 | sa.sin_addr.s_addr = INADDR_ANY 97 | setSockOptInt(sock, SOL_SOCKET, SO_REUSEADDR, 1) 98 | discard bindAddr(sock, cast[ptr SockAddr](sa.addr), sizeof(sa).SockLen) 99 | discard listen(sock, SOMAXCONN) 100 | 101 | SocketCont(fn: doServer, sock: sock).run() 102 | 103 | echo "TCP server ready on port ", port 104 | 105 | # Run eventqueue main loop 106 | 107 | run() 108 | 109 | -------------------------------------------------------------------------------- /experiments/tr.nim: -------------------------------------------------------------------------------- 1 | import balls 2 | 3 | suite "tr": 4 | ## define the basic types 5 | type 6 | C = ref object of RootObj 7 | fn: proc (c: C): C {.nimcall.} 8 | mom: C 9 | 10 | CT[T] = ref object of C 11 | data: ref T 12 | 13 | CR[T, R] = ref object of CT[T] 14 | result: R 15 | 16 | ## result procs 17 | proc res[T; R: void](c: CR[T, R]): R = 18 | ## an empty result should never be queried. 19 | raise ValueError.newException: "you're better than this" 20 | 21 | proc res[T; R: not void](c: CR[T, R]): R = 22 | ## fine, sure. 23 | c.result 24 | 25 | ## upscaler for result-less continuations 26 | converter toR[T](c: sink CT[T]): CR[T, void] = 27 | ## i know, i know 28 | result = CR[T, void](data: c.data, fn: c.fn, mom: c.mom) 29 | 30 | ## a voodoo or something 31 | proc bif(c: CT[int]; v: var int) = 32 | v = 10 33 | 34 | ## a child continuation or whatever 35 | proc bar[T, R](c: CR[T, R]; v: var T): CR[T, R] = 36 | result = c 37 | result.mom = c 38 | bif(c, c.data[]) 39 | when R is string: # added guard? terrible. 40 | c.result = "it's " & $v 41 | 42 | ## define foo 43 | proc foo[T, R](c: CR[T, R]; results: static[bool]): CR[T, R] = 44 | # choose a child type dynamically 45 | var e = 46 | when R is void: 47 | CT[int](data: c.data) 48 | else: 49 | CR[int, R](data: c.data, result: default R) 50 | # run a child with/without result 51 | var d = bar(e, e.data[]) 52 | when results: 53 | when R is void: 54 | raise Defect.newException "this is highly irregular" 55 | check d.res != "" 56 | else: 57 | when R isnot void: 58 | raise Defect.newException "this is highly irregular" 59 | expect ValueError: 60 | # d.res is void 61 | # d.res should raise 62 | d.res 63 | result = d 64 | 65 | type Cont[R] = CR[int, R] 66 | 67 | proc trampoline[T](c: T): T = 68 | var c = c.C 69 | while c != nil and c.fn != nil: 70 | c = c.fn(c) 71 | result = c.T 72 | 73 | proc newCont(x: ref int): Cont[void] = 74 | Cont[void](data: x) 75 | 76 | proc newCont(x: ref int; s: string): Cont[string] = 77 | Cont[string](data: x, result: s) 78 | 79 | var x = new int 80 | 81 | block: 82 | ## test runtime C 83 | var o = newCont x 84 | o = o.foo off 85 | check o.data[] == 10 86 | 87 | block: 88 | ## test runtime CR 89 | var o = newCont(x, "no result") 90 | o = o.foo on 91 | check o.data[] == 10 92 | check o.res == "it's 10" 93 | -------------------------------------------------------------------------------- /experiments/tr_case.nim: -------------------------------------------------------------------------------- 1 | import balls 2 | 3 | suite "tr": 4 | ## define the basic types 5 | type 6 | Kind = enum Basic, Result, Data 7 | C[R, T] = ref object of RootObj 8 | fn: proc (c: C[R, T]): C[R, T] {.nimcall.} 9 | mom: C[R, T] 10 | case kind: Kind 11 | of Basic: 12 | discard 13 | of Result: 14 | r: R 15 | of Data: 16 | tr: R 17 | data: ref T 18 | 19 | proc newC[R, T](): C[R, T] = 20 | when R is void and T is void: 21 | C[R, T](kind: Basic) 22 | elif T is void: 23 | C[R, T](kind: Result) 24 | else: 25 | C[R, T](kind: Data) 26 | 27 | proc newC[R, T](x: ref T): C[R, T] = 28 | C[R, T](data: x, kind: Data) 29 | 30 | ## result proc 31 | proc res[R: void; T](c: C[R, T]) = 32 | raise ValueError.newException: "just no" 33 | 34 | proc res[R, T](c: C[R, T]): R = 35 | ## fine, sure. 36 | case c.kind 37 | of Result: 38 | result = c.r 39 | of Data: 40 | result = c.tr 41 | else: 42 | raise ValueError.newException: "you're better than this" 43 | 44 | ## a voodoo or something 45 | proc bif(c: C[auto, int]; v: var int) = 46 | v = 10 47 | 48 | ## a child continuation or whatever 49 | proc bar[R, T](c: C[R, T]; v: var T): C[R, T] = 50 | result = c 51 | result.mom = c 52 | bif(c, c.data[]) 53 | when R is string: # added guard? terrible. 54 | c.tr = "it's " & $v 55 | 56 | ## define foo 57 | proc foo[R, T](c: C[R, T]; results: static[bool]): C[R, T] = 58 | # choose a child type dynamically 59 | var e = 60 | when results: 61 | newC[R, T]() 62 | else: 63 | newC[void, T]() 64 | e.data = c.data 65 | # run a child with/without result 66 | var d = bar(e, e.data[]) 67 | when results: 68 | when R is void: 69 | raise Defect.newException "this is highly irregular" 70 | check d.res != "" 71 | else: 72 | when R isnot void: 73 | raise Defect.newException "this is highly irregular" 74 | expect ValueError: 75 | # d.res is void 76 | # d.res should raise 77 | d.res 78 | result = d 79 | 80 | proc trampoline[T](c: T): T = 81 | var c = c.C 82 | while c != nil and c.fn != nil: 83 | c = c.fn(c) 84 | result = c.T 85 | 86 | type Cont[R] = C[R, int] 87 | 88 | proc newCont(x: ref int): Cont[void] = 89 | result = newC[void, int](x) 90 | 91 | proc newCont(x: ref int; s: string): Cont[string] = 92 | result = newC[string, int](x) 93 | result.tr = s 94 | 95 | var x = new int 96 | 97 | block: 98 | ## test runtime CT 99 | var o = newCont x 100 | o = o.foo off 101 | check o.data[] == 10 102 | 103 | block: 104 | ## test runtime CR 105 | var o = newCont(x, "no result") 106 | o = o.foo on 107 | check o.data[] == 10 108 | check o.res == "it's 10" 109 | -------------------------------------------------------------------------------- /experiments/try/README.md: -------------------------------------------------------------------------------- 1 | # try 2 | An original piece of exception-heavy code and the matching (crude, manual) 3 | transform. 4 | -------------------------------------------------------------------------------- /experiments/try/original.nim: -------------------------------------------------------------------------------- 1 | proc noop() = echo "noop" 2 | 3 | var r = 0 4 | proc foo() = 5 | try: 6 | try: 7 | try: 8 | raise newException(CatchableError, "some error") 9 | except CatchableError as e: 10 | echo "e" 11 | noop() 12 | inc r 13 | doAssert e.msg == "some error" 14 | 15 | raise newException(ValueError, "something") 16 | finally: 17 | echo "f1" 18 | noop() 19 | inc r 20 | doAssert getCurrentExceptionMsg() == "something" 21 | 22 | doAssert false, "this should not run" 23 | finally: 24 | echo "f2" 25 | inc r 26 | 27 | doAssertRaises ValueError: 28 | foo() 29 | 30 | doAssert r == 3 31 | -------------------------------------------------------------------------------- /experiments/try/transform.nim: -------------------------------------------------------------------------------- 1 | var r = 0 2 | 3 | type 4 | C = ref object 5 | fn: proc (c: C): C {.nimcall.} 6 | e: ref CatchableError 7 | x1: ref Exception 8 | x2: ref Exception 9 | 10 | proc noop(c: C): C = 11 | echo "noop" 12 | c 13 | 14 | template post_exception_body(goto: typed; body: untyped) {.dirty.} = 15 | if getCurrentException().isNil: 16 | body 17 | c.fn = goto 18 | 19 | template sensitive_exit {.dirty.} = 20 | if c.fn.isNil and not getCurrentException().isNil: 21 | raise 22 | return c 23 | 24 | template catch_and_go(ex: untyped; goto: typed; body: untyped) {.dirty.} = 25 | try: 26 | body 27 | except Exception as `ex`: 28 | c.`ex` = `ex` 29 | c.fn = goto 30 | sensitive_exit() 31 | 32 | proc f2(c: C): C = 33 | ## finally from the "toplevel" try 34 | echo "f2" 35 | 36 | # we look for non-nil exceptions 37 | if not c.x2.isNil: 38 | # it's code that's inside a try 39 | setCurrentException c.x2 # added boilerplate 40 | elif not c.x1.isNil: 41 | # it's code that's inside a try 42 | setCurrentException c.x1 # added boilerplate 43 | elif not c.e.isNil: 44 | # finally when exception was thrown 45 | setCurrentException c.e # added boilerplate 46 | 47 | inc r 48 | 49 | post_exception_body nil: 50 | # this would be the body after the toplevel try statement 51 | discard 52 | 53 | sensitive_exit() 54 | 55 | proc b2(c: C): C = 56 | # we look for non-nil exceptions 57 | if not c.x1.isNil: 58 | # it's code that's inside a try 59 | setCurrentException c.x1 # added boilerplate 60 | elif not c.e.isNil: 61 | # finally when exception was thrown 62 | setCurrentException c.e # added boilerplate 63 | 64 | # it's in a try so we need to catch it 65 | catch_and_go x2, f2: 66 | inc r 67 | doAssert getCurrentExceptionMsg() == "something" 68 | 69 | post_exception_body f2: 70 | # proceed into the toplevel try body 71 | doAssert false, "this should not run" 72 | 73 | proc f1(c: C): C = 74 | ## finally from the "middle" try 75 | echo "f1" 76 | 77 | # we look for non-nil exceptions 78 | if not c.x1.isNil: 79 | # it's code that's inside a try 80 | setCurrentException c.x1 # added boilerplate 81 | elif not c.e.isNil: 82 | # finally when exception was thrown 83 | setCurrentException c.e # added boilerplate 84 | 85 | catch_and_go x2, f2: 86 | # finally body starts with a noop, so ... 87 | c.fn = b2 88 | return noop c 89 | 90 | proc b(c: C): C = 91 | ## inner-most try body successful; this is the code 92 | ## after that try statement 93 | 94 | catch_and_go x1, f1: 95 | # user code for body after the try 96 | raise newException(ValueError, "something") 97 | 98 | proc b1(c: C): C = 99 | # interior of clause 100 | if not c.e.isNil: 101 | setCurrentException c.e # added boilerplate 102 | 103 | catch_and_go x1, f1: 104 | # user code for clause 105 | inc r 106 | doAssert c.e.msg == "some error" 107 | 108 | # after the try 109 | raise newException(ValueError, "something") 110 | 111 | proc foo(): C = 112 | var c = C() 113 | 114 | try: 115 | raise newException(CatchableError, "some error") 116 | c.fn = b 117 | return noop c 118 | except CatchableError as e: 119 | echo "e" 120 | c.e = e 121 | c.fn = b1 122 | return noop c 123 | 124 | doAssertRaises ValueError: 125 | var c = foo() 126 | while c != nil and c.fn != nil: 127 | c = c.fn(c) 128 | 129 | doAssert r == 3, "r is " & $r 130 | -------------------------------------------------------------------------------- /experiments/xfrm.nim: -------------------------------------------------------------------------------- 1 | import macros 2 | import nativesockets 3 | import strutils 4 | import eventqueue 5 | 6 | # Any call to a proc starting with "cps_" is a CPS call 7 | proc isCpsCall(n: NimNode): bool = 8 | n.kind == nnkCall and ($n[0]).find("cps_") == 0 9 | 10 | # Every block with calls to CPS procs is a CPS block 11 | proc isCpsBlock(n: NimNode): bool = 12 | if n.kind != nnkProcDef: 13 | for nc in n: 14 | if nc.isCpsCall() or isCpsBlock(nc): 15 | return true 16 | 17 | proc split(n: Nimnode): NimNode = 18 | 19 | let name = $n[0] 20 | var contTypes = newStmtList() # Continuation types for split procs 21 | var contProcs = newStmtList() # Forward declaratiosn of all split procs 22 | var id = 0 23 | 24 | # Create a new CPS proc with prelude and the given body 25 | proc addProc(body: NimNode): NimNode = 26 | inc id 27 | let procId = ident("cps_" & name & "_" & $id) 28 | let contId = ident("cont") 29 | let contT = ident("Cont_" & name & "_" & $id) 30 | 31 | # hack: the prelude now has one hardcoded val, this should be 32 | # generated from the lifted locals of the proc instead 33 | let prelude = newVarSection( 34 | "j", 35 | newEmptyNode(), 36 | newDotExpr(newDotExpr(contId, contT), ident("j")) 37 | ) 38 | ) 39 | 40 | contTypes.add quote do: 41 | type `contT` = ref object of Cont 42 | # hack: continuation types should be derived from lambda lifting 43 | j: int 44 | 45 | contProcs.add quote do: 46 | proc `procId$`(`contId`: Cont): Cont 47 | 48 | result = quote do: 49 | result = `contT`(fn: `procId`, j: j) 50 | proc `procId`(`contId`: Cont): Cont = 51 | `prelude` 52 | `body` 53 | 54 | 55 | # Split on 'while' CPS blocks 56 | proc auxWhile(n: Nimnode): NimNode = 57 | if n.kind == nnkWhileStmt and n[1].isCpsBlock(): 58 | let (expr, stmt) = (n[0], auxWhile(n[1])) 59 | let body = quote do: 60 | if `expr`: 61 | `stmt` 62 | result = addProc(body) 63 | # Hack in call to self at end of body block. This is nasty 64 | result[1][6][1][0][1][0].add result[0] 65 | else: 66 | result = copyNimNode(n) 67 | for nc in n: 68 | result.add auxWhile(nc) 69 | 70 | 71 | # Split on CPS calls 72 | proc auxSplit(n: Nimnode): NimNode = 73 | 74 | if n.kind == nnkStmtList and n.isCpsBlock(): 75 | 76 | type State = enum sPre, sCPSCalls, sPost 77 | var state = sPre 78 | var a0 = newStmtList() 79 | var a1 = newStmtList() 80 | 81 | for nc in n: 82 | case state 83 | of sPre: 84 | a0.add auxSplit(nc) 85 | if nc.isCpsCall(): 86 | state = sCPSCalls 87 | of sCPSCalls: 88 | if nc.isCpsCall(): 89 | a0.add auxSplit(nc) 90 | else: 91 | state = sPost 92 | a1.add auxSplit(nc) 93 | of sPost: 94 | a1.add auxSplit(nc) 95 | 96 | result = copyNimNode(n) 97 | result.add a0 98 | if a1.len > 0: 99 | result.add addProc auxSplit(a1) 100 | 101 | else: 102 | result = copyNimNode(n) 103 | for nc in n: 104 | result.add auxSplit(nc) 105 | 106 | 107 | # Move all procs to top level 108 | proc auxToplevel(n: NimNode): NimNode = 109 | var procs = newStmtList() 110 | proc aux(n: NimNode): NimNode = 111 | result = copyNimNode(n) 112 | for nc in n: 113 | if nc.kind == nnkProcDef: 114 | procs.add aux(nc) 115 | else: 116 | result.add aux(nc) 117 | procs.add aux(n) 118 | return procs 119 | 120 | # hack: chain csp_sleep() calls 121 | var chainNode: NimNode 122 | proc auxChain(n: NimNode): NimNode = 123 | result = copyNimNode(n) 124 | for nc in n: 125 | if nc.kind == nnkCall and ($nc[0]).find("cps_") == 0: 126 | chainNode = nc 127 | else: 128 | if chainNode != nil: 129 | let rv = nc[0][1] 130 | result.add quote do: 131 | result = cps_sleep(`rv`, 0.3) 132 | chainNode = nil 133 | else: 134 | result.add auxChain(nc) 135 | 136 | # Chain all the above transformations 137 | var body = n.auxWhile.auxSplit.auxToplevel.auxChain 138 | 139 | # Move al the types into one type block 140 | let types = nnkTypeSection.newTree() 141 | for t in contTypes: types.add t[0] 142 | 143 | result = newStmtList(types, contProcs, body) 144 | 145 | 146 | proc xfrmCps(n: NimNode): NimNode = 147 | result = split(n) 148 | echo "=====================" 149 | echo n.repr 150 | echo "=====================" 151 | echo result.repr 152 | echo "=====================" 153 | 154 | 155 | macro cps(n: untyped) = 156 | xfrmCps(n) 157 | 158 | 159 | proc cps_sleep(cont: Cont, t: float): Cont = 160 | discard addTimer(t, cont) 161 | return Cont() 162 | 163 | # This is the proc we are converting to CPS form 164 | 165 | proc tocker(cont: Cont): Cont {.cps.} = 166 | var j = cont.Cont_tocker_1.j 167 | echo "start" 168 | while true: 169 | cps_sleep(0.3) 170 | echo "tick ", j 171 | inc j 172 | cps_sleep(0.3) 173 | echo "tock ", j 174 | inc j 175 | 176 | # Instantiate two parallel tockers, starting at different numbers 177 | 178 | Cont_Tocker_1(fn: tocker, j: 0).run() 179 | Cont_Tocker_1(fn: tocker, j: 100).run() 180 | 181 | # Forever run the event queue 182 | 183 | run() 184 | -------------------------------------------------------------------------------- /experiments/xfrm2.nim: -------------------------------------------------------------------------------- 1 | 2 | import macros 3 | import os 4 | import nativesockets 5 | import strutils 6 | import eventqueue 7 | 8 | # Any call to a proc starting with "cps_" is a CPS call 9 | proc isCpsCall(n: NimNode): bool = 10 | n.kind == nnkCall and ($n[0]).find("cps_") == 0 11 | 12 | # Every block with calls to CPS procs is a CPS block 13 | proc isCpsBlock(n: NimNode): bool = 14 | if n.isCpsCall(): 15 | return true 16 | else: 17 | for nc in n: 18 | if isCpsBlock(nc): 19 | return true 20 | 21 | template cpsMagic() {.pragma.} 22 | 23 | type NimNodeFilter = 24 | proc(n: NimNode): NimNode 25 | 26 | proc ident(n: NimNode): NimNode = 27 | n 28 | 29 | proc filter(n: NimNode, fn: NimNodeFilter=ident): NimNode = 30 | result = fn(n) 31 | if result == nil: 32 | result = copyNimNode(n) 33 | for nc in n: 34 | result.add filter(nc, fn) 35 | 36 | 37 | 38 | # Pre-semcheck transformation 39 | 40 | proc doPre(n: NimNode): NimNode = 41 | 42 | proc mkLabel(s: string): NimNode = 43 | #return ident(s) 44 | return genSym(nskLabel, s) 45 | 46 | 47 | proc auxSleep(n: NimNode): NimNode = 48 | if n.kind == nnkCall and n[0].eqIdent("sleep"): 49 | result = quote do: 50 | cps_sleep() 51 | result.add n[1] 52 | 53 | # Split on 'while' CPS blocks 54 | proc auxWhile(n: Nimnode, curLabel: NimNode=nil): NimNode = 55 | if n.kind == nnkWhileStmt and n.isCpsBlock: 56 | let lWhile = mkLabel("while") 57 | let lBreak = mkLabel("break") 58 | let (expr, stmt) = (n[0], auxWhile(n[1], lBreak)) 59 | result = quote do: 60 | goto `lWhile` 61 | label `lWhile` 62 | if `expr`: 63 | `stmt` 64 | goto `lWhile` 65 | goto `lBreak` 66 | label `lBreak` 67 | elif n.kind == nnkBreakStmt: 68 | result = quote do: 69 | goto `curLabel` 70 | else: 71 | result = copyNimNode(n) 72 | for nc in n: 73 | result.add auxWhile(nc, curLabel) 74 | 75 | result = n.filter(auxSleep) 76 | result = result.auxWhile() 77 | 78 | echo "======== pre out" 79 | echo result.repr 80 | echo "========" 81 | 82 | macro spawnPre(n: untyped) = 83 | doPre(n) 84 | 85 | 86 | template goto(n: untyped) = discard 87 | template label(n: untyped) = discard 88 | 89 | proc cps_sleep(f: float) {.cpsMagic.} = 90 | os.sleep(int(f * 1000)) 91 | discard 92 | 93 | 94 | proc main() = 95 | echo "main start" 96 | var i = 0 97 | spawnPre: 98 | echo "spawnPre start" 99 | var j = 100 100 | while true: 101 | inc i 102 | inc j 103 | if i < 3: 104 | echo i, " ", j 105 | else: 106 | break 107 | sleep(0.3) 108 | echo "spawnPre done" 109 | echo "main done" 110 | 111 | main() 112 | -------------------------------------------------------------------------------- /papers/1011.4558.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nim-works/cps/7e01e94046b77d600f93dc2f2b706a519799059f/papers/1011.4558.pdf -------------------------------------------------------------------------------- /papers/README.md: -------------------------------------------------------------------------------- 1 | # papers 2 | The inspiration for the CPS project. 3 | -------------------------------------------------------------------------------- /papers/cpc-manual.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nim-works/cps/7e01e94046b77d600f93dc2f2b706a519799059f/papers/cpc-manual.pdf -------------------------------------------------------------------------------- /papers/cpc.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nim-works/cps/7e01e94046b77d600f93dc2f2b706a519799059f/papers/cpc.pdf -------------------------------------------------------------------------------- /stash/README.md: -------------------------------------------------------------------------------- 1 | # stash 2 | More toys and experiments in various stages of decay. 3 | -------------------------------------------------------------------------------- /stash/bench.nim: -------------------------------------------------------------------------------- 1 | # 2 | # This is a performance comparison between CPS and native closure iterators. 3 | # 4 | when not defined(danger): 5 | {.error: "define danger for benchmark purposes".} 6 | 7 | when not defined(gcArc): 8 | {.warning: "cps is designed for --gc:arc".} 9 | 10 | import cps 11 | import criterion 12 | 13 | var cfg = newDefaultConfig() 14 | cfg.brief = false 15 | 16 | const 17 | loops = 100 18 | 19 | type Iterator = ref object of RootObj 20 | fn*: proc(c: Iterator): Iterator {.nimcall.} 21 | val: int 22 | 23 | proc jield(it: Iterator; val: int): Iterator {.cpsMagic.} = 24 | it.val = val 25 | return it 26 | 27 | proc cps_counter(lo: int, hi: int) {.cps: Iterator.} = 28 | var i = lo 29 | while i <= hi: 30 | jield i 31 | inc i 32 | 33 | iterator closure_counter(lo: int, hi: int): int {.closure.} = 34 | var i = lo 35 | while i <= hi: 36 | yield i 37 | inc i 38 | 39 | benchmark cfg: 40 | proc cps_iterator() {.measure.} = 41 | var a = cps_counter(1, loops) 42 | while a != nil and a.fn != nil: 43 | a = a.fn(a) 44 | 45 | proc closure_iterator() {.measure.} = 46 | let f = closure_counter 47 | while not finished(f): 48 | discard f(1, loops) 49 | -------------------------------------------------------------------------------- /stash/bench.nim.cfg: -------------------------------------------------------------------------------- 1 | #--define:cpsMoves 2 | --gc:arc 3 | --define:cpsDebug 4 | --panics:on 5 | --run 6 | --hint[Cc]=off 7 | --hint[Conf]=off 8 | --hint[Link]=off 9 | --hint[Exec]=off 10 | -------------------------------------------------------------------------------- /stash/brokenbreak.nim: -------------------------------------------------------------------------------- 1 | 2 | import cps 3 | 4 | type 5 | 6 | Cont = ref object of RootObj 7 | fn*: proc(c: Cont): Cont {.nimcall.} 8 | 9 | 10 | proc test(): Cont {.cps.} = 11 | 12 | while true: 13 | cps sleep() 14 | if true: 15 | break 16 | 17 | 18 | # brokenbreak.nim(10, 21) template/generic instantiation of `cps` from here 19 | t# brokenbreak.nim(15, 7) Error: invalid control flow: break 20 | 21 | -------------------------------------------------------------------------------- /stash/echo_server_client.nim: -------------------------------------------------------------------------------- 1 | 2 | 3 | import cps 4 | import epoll 5 | import posix 6 | import tables 7 | import deques 8 | 9 | proc timerfd_create(clock_id: ClockId, flags: cint): cint 10 | {.cdecl, importc: "timerfd_create", header: "".} 11 | 12 | proc timerfd_settime(ufd: cint, flags: cint, 13 | utmr: ptr Itimerspec, otmr: ptr Itimerspec): cint 14 | {.cdecl, importc: "timerfd_settime", header: "".} 15 | 16 | 17 | type 18 | 19 | Cont = ref object of RootObj 20 | fn*: proc(c: Cont): Cont {.nimcall.} 21 | 22 | Evq = ref object 23 | epfd: cint 24 | work: Deque[Cont] 25 | fds: Table[cint, Cont] 26 | running: bool 27 | 28 | Timer = proc() 29 | 30 | 31 | 32 | 33 | # Event queue implementation 34 | 35 | proc newEvq(): Evq = 36 | new result 37 | result.epfd = epoll_create(1) 38 | 39 | proc stop(evq: Evq) = 40 | evq.running = false 41 | 42 | proc addWork(evq: Evq, cont: Cont) = 43 | evq.work.addLast cont 44 | 45 | proc addFd(evq: Evq, fd: SocketHandle | cint, cont: Cont) = 46 | evq.fds[fd.cint] = cont 47 | 48 | proc delFd(evq: Evq, fd: SocketHandle | cint) = 49 | evq.fds.del(fd.cint) 50 | 51 | proc io(evq: Evq, c: Cont, fd: SocketHandle | cint, event: int): Cont = 52 | var epv = EpollEvent(events: event.uint32) 53 | epv.data.u64 = fd.uint 54 | discard epoll_ctl(evq.epfd, EPOLL_CTL_ADD, fd.cint, epv.addr) 55 | evq.addFd(fd, c) 56 | 57 | proc sleep(evq: Evq, c: Cont, timeout: int): Cont = 58 | let fd = timerfd_create(CLOCK_MONOTONIC, 0) 59 | var ts: Itimerspec 60 | ts.it_interval.tv_sec = Time(timeout div 1_000) 61 | ts.it_interval.tv_nsec = (timeout %% 1_000) * 1_000_000 62 | ts.it_value.tv_sec = ts.it_interval.tv_sec 63 | ts.it_value.tv_nsec = ts.it_interval.tv_nsec 64 | doAssert timerfd_settime(fd.cint, 0.cint, ts.addr, nil) != -1 65 | evq.io(c, fd, POLLIN) 66 | 67 | proc run(evq: Evq) = 68 | evq.running = true 69 | while true: 70 | 71 | # Pump the queue until empty 72 | while evq.work.len > 0: 73 | let c = evq.work.popFirst 74 | let c2 = c.fn(c) 75 | if c2 != nil: 76 | evq.addWork c2 77 | 78 | if not evq.running: 79 | break 80 | 81 | # Wait for all registered file descriptors 82 | var events: array[8, EpollEvent] 83 | let n = epoll_wait(evq.epfd, events[0].addr, events.len.cint, 1000) 84 | 85 | # Put continuations for all ready fds back into the queue 86 | for i in 0..= 0: 120 | result.setlen(n) 121 | else: 122 | result.setlen(0) 123 | 124 | proc sockSend(fd: SocketHandle, s: string) = 125 | let n = posix.send(fd, s[0].unsafeAddr, s.len, 0) 126 | assert(n == s.len) 127 | 128 | proc sockConnect(address: string, port: int): SocketHandle = 129 | discard 130 | let fd = posix.socket(AF_INET, SOCK_STREAM, 0) 131 | var sas: Sockaddr_in 132 | sas.sin_family = AF_INET.uint16 133 | sas.sin_port = htons(port.uint16) 134 | sas.sin_addr.s_addr = inet_addr(address) 135 | var yes: int = 1 136 | doAssert connect(fd, cast[ptr SockAddr](sas.addr), sizeof(sas).SockLen) != -1 137 | return fd 138 | 139 | 140 | cps Cont: 141 | 142 | var evq = newEvq() 143 | var count = 0 144 | var clients = 0 145 | 146 | # CPS server session hander 147 | 148 | proc handleClient(fdc: SocketHandle) = 149 | 150 | inc clients 151 | 152 | while true: 153 | cps evq.io(fdc, POLLIN) 154 | let s: string = sockRecv(fdc) 155 | if s.len == 0: break 156 | inc count 157 | cps evq.io(fdc, POLLOUT) 158 | sockSend(fdc, s) 159 | 160 | dec clients 161 | discard fdc.close() 162 | 163 | 164 | # CPS server listener handler 165 | 166 | proc doEchoServer(port: int) = 167 | let fds: SocketHandle = sockBind(port) 168 | echo "listening fd: ", fds.int 169 | while true: 170 | cps evq.io(fds, POLLIN) 171 | let fdc: SocketHandle = sockAccept(fds) 172 | #echo "accepted fd:", fdc.int 173 | # Create new client and add to work queue 174 | evq.addWork handleClient(fdc) 175 | 176 | 177 | # CPS client handler 178 | 179 | proc doEchoClient(address: string, port: int, n: int, msg: string) = 180 | let fd: SocketHandle = sockConnect(address, port) 181 | #echo "connected fd: ", fd.int 182 | 183 | var i: int = 0 184 | while i < n: 185 | cps evq.io(fd, POLLOUT) 186 | sockSend(fd, msg) 187 | cps evq.io(fd, POLLIN) 188 | let msg2: string = sockRecv(fd) 189 | doAssert msg2 == msg 190 | inc i 191 | 192 | discard fd.close() 193 | #echo "disconnected fd: ", fd.int 194 | 195 | 196 | # Progress reporting 197 | 198 | proc doTicker() = 199 | while true: 200 | cps evq.sleep(1000) 201 | echo "tick. clients: ", clients, " echoed ", count, " messages" 202 | if clients == 0: 203 | evq.stop() 204 | 205 | 206 | # Spawn workers 207 | 208 | evq.addWork doTicker() 209 | 210 | evq.addWork doEchoServer(8000) 211 | 212 | for i in 1..100: 213 | evq.addWork doEchoClient("127.0.0.1", 8000, 214 | 2000, "The quick brown fox jumped over the lazy dog") 215 | 216 | # Forever run the event queue 217 | 218 | evq.run() 219 | 220 | -------------------------------------------------------------------------------- /stash/iteratorT.nim: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # This is an example implementation for a basic CPS-based iterator. 4 | # 5 | 6 | import cps, options 7 | 8 | # This is our iterator type. It holds the continuation function 9 | # and an Option[int] to pass the last produced value 10 | 11 | type Iterator[T] = ref object of RootObj 12 | fn*: proc(c: Iterator[T]): Iterator[T] {.nimcall.} 13 | val: Option[T] 14 | 15 | # The `produce` proc is called to pump the iterator. It will trampoline the 16 | # continuation until a value is available in `val`. 17 | 18 | proc produce[T](c: var Iterator[T]): Option[T] = 19 | while c != nil and c.fn != nil and c.val.isNone: 20 | c = c.fn(c) 21 | if c != nil and c.val.isSome: 22 | result = c.val 23 | c.val = none(T) 24 | 25 | 26 | # The `jield` proc is cps magic to generate a new value from within an 27 | # interator 28 | 29 | proc jield[T](c: Iterator[T], val: T): Iterator[T] = 30 | c.val = some(val) 31 | return c 32 | 33 | 34 | # A simple counting iterator, will produce all integers from 'lo' to 'high', 35 | # inclusive 36 | 37 | proc counter[T](lo: T, hi: T) {.cps:Iterator[T].} = 38 | var i: T = lo 39 | while i <= hi: 40 | cps jield(i) 41 | inc i 42 | 43 | 44 | # Create an instance of the iterator, counting from 3 up to 7 45 | 46 | var a = counter[int](3, 7) 47 | 48 | 49 | # Resume the iterator a bunch of times 50 | 51 | var v = a.produce() 52 | 53 | echo "produced ", a.produce() 54 | echo "produced ", a.produce() 55 | echo "produced ", a.produce() 56 | echo "produced ", a.produce() 57 | echo "produced ", a.produce() 58 | echo "produced ", a.produce() 59 | 60 | -------------------------------------------------------------------------------- /stash/lost.nim: -------------------------------------------------------------------------------- 1 | 2 | import cps 3 | 4 | type 5 | C = ref object of RootObj 6 | fn*: proc(c: C): C {.nimcall.} 7 | mom: C 8 | val: CData 9 | 10 | CData = ref object 11 | v1: int 12 | v2: string 13 | 14 | proc `$`(c: C): string = 15 | $cast[int](c) 16 | 17 | proc pass(cFrom, cTo: C): C = 18 | echo "pass ", $cFrom, " -> ", $cTo 19 | cTo.val = move cfrom.val 20 | cTo 21 | 22 | proc boot(c: C): C = 23 | echo "boot ", c 24 | new c.val 25 | c 26 | 27 | proc jield(c: C): C {.cpsMagic.} = 28 | echo "jield ", c 29 | c 30 | 31 | proc send(c: C, v: int) {.cpsVoodoo.} = 32 | echo "send ",c 33 | c.val.v1 = v 34 | c.val.v2 = "hello" 35 | 36 | proc recv(c: C): int {.cpsVoodoo.} = 37 | echo "recv ", c 38 | c.val.v1 39 | 40 | proc level_two() {.cps:C.} = 41 | echo " two in" 42 | send(42) 43 | jield() 44 | echo " two recv: ", recv() 45 | 46 | proc level_one() {.cps:C.} = 47 | echo "one in" 48 | level_two() 49 | echo "one recv" # <--- we never get here 50 | let v = recv() 51 | echo "one recv: ", recv() 52 | 53 | var a = whelp level_one() 54 | 55 | while a.running: 56 | echo "tramp ", a 57 | a = a.fn(a) 58 | -------------------------------------------------------------------------------- /stash/performance.nim: -------------------------------------------------------------------------------- 1 | import std/[hashes, times, monotimes] 2 | 3 | # 4 | # This is a performance comparison between CPS and native closure iterators. 5 | # 6 | when not defined(danger): 7 | {.error: "define danger for benchmark purposes".} 8 | 9 | when not defined(gcArc): 10 | {.warning: "cps is designed for --gc:arc".} 11 | 12 | import cps 13 | 14 | template howLong(what, code): Duration = 15 | let start = getMonoTime() 16 | block: 17 | code 18 | let duration = getMonoTime() - start 19 | echo what, ": ", duration 20 | duration 21 | 22 | const iterations = 1_000_000_000 23 | var h: Hash = 0 24 | 25 | let t1 = howLong "cps iterator": 26 | 27 | type 28 | Iterator = ref object of Continuation 29 | yielded: bool 30 | val: int 31 | 32 | proc jield(it: Iterator; val: int): Iterator {.cpsMagic.} = 33 | it.yielded = true 34 | it.val = val 35 | return it 36 | 37 | proc next(it: var Iterator): int = 38 | while true: 39 | it = Iterator it.fn(it) 40 | if it.finished: break 41 | if it.yielded: 42 | it.yielded = false 43 | return it.val 44 | 45 | proc counter(lo: int, hi: int) {.cps: Iterator.} = 46 | var i = lo 47 | while i <= hi: 48 | jield i 49 | inc i 50 | 51 | var a = Iterator: whelp counter(1, iterations) 52 | while true: 53 | let next = a.next() 54 | if a.finished: break 55 | h = h !& hash(next) 56 | 57 | echo !$h 58 | h = 0 59 | 60 | let t2 = howLong "closure iterator": 61 | 62 | proc counter(lo: int, hi: int): iterator(): int = 63 | result = 64 | iterator (): int = 65 | var i = lo 66 | while i <= hi: 67 | yield i 68 | inc i 69 | 70 | let f = counter(1, iterations) 71 | while true: 72 | let next = f() 73 | if f.finished: break 74 | h = h !& hash(next) 75 | 76 | echo !$h 77 | 78 | let ratio = t2.inNanoseconds.float / t1.inNanoseconds.float 79 | if ratio < 1: 80 | echo "Nim closure iterators are ", 1 / ratio, " times faster" 81 | else: 82 | echo "Nim closure iterators are ", ratio, " times slower" 83 | -------------------------------------------------------------------------------- /stash/performance.nim.cfg: -------------------------------------------------------------------------------- 1 | #--define:cpsMoves 2 | --gc:arc 3 | #--define:cpsDebug 4 | --panics:on 5 | --run 6 | --hint[Cc]=off 7 | --hint[Conf]=off 8 | --hint[Link]=off 9 | --hint[Exec]=off 10 | --exceptions:goto 11 | --stacktrace:off 12 | --define:cpsStackFrames=off 13 | --define:cpsTraceDeque=off 14 | --define:cpsNoTrace 15 | -------------------------------------------------------------------------------- /stash/standalone_tcp_server.nim: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 4 | # Tine stand alone CPS http server. Goal is to get this to absolute minimal 5 | # system call overhead to get maximum performance serving static HTTP content 6 | # 7 | # Todo, in more or less this order: 8 | # 9 | # - Get CPS to accept the code flow 10 | # - Implement basic HTTP protocol for serving a static html page 11 | # - Only do epoll_ctl when needed, instead of adding/removing for every iteration 12 | # - Add IO scheduling for send, with proper send buffer handling using writev() 13 | # - Make multi threaded with a single event queue per thread, using EPOLLEXCLUSIVE 14 | # 15 | 16 | import cps 17 | import epoll 18 | import posix 19 | import tables 20 | import deques 21 | 22 | 23 | type 24 | 25 | Cont = ref object of Continuation 26 | 27 | Evq = object 28 | work: Deque[Cont] 29 | readers: Table[SocketHandle, Cont] 30 | 31 | 32 | var evq: Evq 33 | let epfd = epoll_create(1) 34 | 35 | 36 | 37 | # CPS 'io' primitive, registers the fd with epoll and stashes the continuation. 38 | # Will be revived later by the main loop when the fd gets ready 39 | 40 | proc io(c: Cont, fd: SocketHandle, event: int): Cont {.cpsMagic.} = 41 | var epv = EpollEvent(events: event.uint32) 42 | epv.data.u64 = fd.uint 43 | discard epoll_ctl(epfd, EPOLL_CTL_ADD, fd.cint, epv.addr) 44 | evq.readers[fd] = c 45 | 46 | 47 | # Some convenience functions to hide the dirty socket stuff, this keeps the CPS 48 | # functions as clean and readable as possible 49 | 50 | proc sockBind(port: int): SocketHandle = 51 | result = posix.socket(AF_INET, SOCK_STREAM, 0) 52 | var sas: Sockaddr_in 53 | sas.sin_family = AF_INET.uint16 54 | sas.sin_port = htons(port.uint16) 55 | sas.sin_addr.s_addr = INADDR_ANY 56 | var yes: int = 1 57 | doAssert setsockopt(result, SOL_SOCKET, SO_REUSEADDR, yes.addr, sizeof(yes).SockLen) != -1 58 | doAssert bindSocket(result, cast[ptr SockAddr](sas.addr), sizeof(sas).SockLen) != -1 59 | doAssert listen(result, SOMAXCONN) != -1 60 | 61 | proc sockAccept(fds: SocketHandle): SocketHandle = 62 | var sac: Sockaddr_in 63 | var sacLen: SockLen 64 | posix.accept(fds, cast[ptr SockAddr](sac.addr), sacLen.addr) 65 | 66 | proc sockRecv(fd: SocketHandle): string = 67 | result = newString(1024) 68 | let n = posix.recv(fd, result[0].addr, result.len, 0) 69 | if n >= 0: 70 | result.setlen(n) 71 | else: 72 | result.setlen(0) 73 | 74 | proc sockSend(fd: SocketHandle, s: string) = 75 | let n = posix.send(fd, s[0].unsafeAddr, s.len, 0) 76 | assert(n == s.len) 77 | 78 | 79 | const response = """ 80 | HTTP/1.1 200 OK 81 | Content-Type: text/html 82 | Content-Length: 14 83 | Connection: keep-alive 84 | 85 | Hello, there! 86 | """ 87 | 88 | 89 | # CPS client hander 90 | 91 | var clients = 0 92 | var requests = 0 93 | 94 | proc doClient(fdc: SocketHandle) {.cps:Cont} = 95 | 96 | echo "Serving client ", clients 97 | inc clients 98 | 99 | while true: 100 | io(fdc, POLLIN) 101 | echo " Serving request ", requests 102 | let s: string = sockRecv(fdc) 103 | if s.len == 0: 104 | return 105 | io(fdc, POLLOUT) 106 | sockSend(fdc, response) 107 | inc requests 108 | echo "remove this line and the while breaks" 109 | 110 | 111 | # CPS server handler 112 | 113 | proc doServer(port: int) {.cps:Cont} = 114 | let fds: SocketHandle = sockBind(port) 115 | while true: 116 | io(fds, 1.int16) 117 | let fdc: SocketHandle = sockAccept(fds) 118 | # Create new client and add to work queue 119 | evq.work.addLast: whelp doClient(fdc) 120 | echo "remove this line and the while breaks" 121 | 122 | 123 | # Create new http server and add to work queue 124 | 125 | evq.work.addLast: whelp doServer(8000) 126 | 127 | 128 | # Main event queue worker 129 | 130 | while true: 131 | 132 | # Pump the queue until empty 133 | 134 | while evq.work.len > 0: 135 | let c = evq.work.popFirst 136 | let c2 = c.fn(c) 137 | if c2 != nil: 138 | evq.work.addLast: 139 | (typeof c) c2 140 | 141 | # Wait for all registered file descriptors 142 | 143 | var events: array[8, EpollEvent] 144 | let n = epoll_wait(epfd, events[0].addr, events.len.cint, 1000) 145 | 146 | # Put continuations for all ready fds back into the queue 147 | 148 | for i in 0.. 1000: 37 | raise InfiniteLoop.newException: $jumps & " iterations" 38 | if jumps == 0: 39 | raise EmptyLoop.newException: 40 | "continuations test best when they, uh, bounce" 41 | 42 | proc noop*(c: Cont): Cont {.cpsMagic.} = c 43 | 44 | # We have a lot of these for the purpose of control-flow validation 45 | {.warning[UnreachableCode]: off.} 46 | 47 | template shouldRun(wanted: int; body: untyped) {.used.} = 48 | var measured {.inject.} = 0 49 | try: 50 | body 51 | finally: 52 | check measured == wanted: 53 | if wanted == 0: "oops; continuation ran" 54 | elif measured == 0: "continuation never ran" 55 | elif measured > wanted: "continuation ran too often" 56 | else: "continuation ran too rarely" 57 | 58 | template ran {.used, dirty.} = inc measured 59 | -------------------------------------------------------------------------------- /tests/t00_smoke.nim: -------------------------------------------------------------------------------- 1 | import std/[macros, genasts] 2 | import pkg/cps/normalizedast 3 | 4 | include preamble 5 | 6 | suite "basic testing assumptions": 7 | 8 | block: 9 | ## the trampoline runs continuations, uh, continuously 10 | var r = 0 11 | proc foo() {.cps: Cont.} = 12 | while true: 13 | noop() 14 | inc r 15 | expect InfiniteLoop: 16 | trampoline whelp(foo()) 17 | check r > 1 18 | 19 | block: 20 | ## the noop magic smoke test demonstrates shedding scope 21 | var r = 0 22 | proc foo() {.cps: Cont.} = 23 | inc r 24 | noop() 25 | inc r 26 | trampoline whelp(foo()) 27 | check r == 2, "who let the smoke out?" 28 | 29 | 30 | # these tests were initially motivated because the normalizedast was not 31 | # forgiving enough for various return types. 32 | 33 | macro checkType(s: untyped): untyped = 34 | ## checks if a type is a valid type expression 35 | let 36 | n = s[0][0][2] # unwrap as it's nnkStmtList > nnkTypeSection > nnkTypeDef 37 | r = NimNode asTypeExpr(NormNode n) 38 | isError = r.kind == nnkError # did it work? 39 | 40 | # test output parts 41 | rep = if isError: treeRepr(n) else: repr(n) 42 | msgPrefix = if isError: "valid" else: "invalid" 43 | checkStatus = not isError 44 | msg = msgPrefix & " type expression: " & rep 45 | 46 | result = genast(checkStatus, msg): 47 | check checkStatus, msg 48 | 49 | suite "normalizedast tests to quickly test APIs": 50 | # the expectation is that these tests can easily be changed if in the way 51 | 52 | block: 53 | ## tuple type expressions (nnkTupleConstr) 54 | checkType: 55 | type Foo = (int, string) 56 | 57 | block: 58 | ## seq type expressions (nnkBracketExpr) 59 | checkType: 60 | type Foo = seq[int] 61 | 62 | block: 63 | ## seq type expressions (nnkProcTy) 64 | checkType: 65 | type Foo = proc (i: int): int 66 | -------------------------------------------------------------------------------- /tests/t10_loops.nim: -------------------------------------------------------------------------------- 1 | include preamble 2 | 3 | suite "loops": 4 | 5 | var r = 0 6 | 7 | block: 8 | ## while loops correctly, uh, loop 9 | r = 0 10 | proc foo() {.cps: Cont.} = 11 | inc r 12 | var i = 0 13 | while i < 2: 14 | inc r 15 | inc i 16 | inc r 17 | check i == 2 18 | foo() 19 | check r == 4 20 | 21 | block: 22 | ## a continue statement within a while statement works 23 | r = 0 24 | proc foo() {.cps: Cont.} = 25 | inc r 26 | var i = 0 27 | while i < 3: 28 | inc r 29 | noop() 30 | inc i 31 | if i <= 2: 32 | continue 33 | check i == 3 34 | inc r 35 | inc r 36 | foo() 37 | check r == 6 38 | 39 | block: 40 | ## a while statement supports a local variable inside 41 | r = 0 42 | proc foo() {.cps: Cont.} = 43 | inc r 44 | var i = 0 45 | while i < 2: 46 | inc r 47 | let x = i 48 | noop() 49 | inc r 50 | inc i 51 | noop() 52 | inc r 53 | check x == i - 1 54 | inc r 55 | foo() 56 | check r == 8 57 | 58 | block: 59 | ## a while loop with only a single continuing call works 60 | proc jield(c: Cont): Cont {.cpsMagic.} = 61 | discard 62 | 63 | r = 0 64 | proc count() {.cps: Cont.} = 65 | inc r 66 | var i = 0 67 | 68 | while i < 2: 69 | jield() 70 | 71 | fail "this statement should not run" 72 | 73 | count() 74 | check r == 1 75 | 76 | block: 77 | ## whelp instantiates continuations with arguments 78 | r = 0 79 | proc foo(x: int) {.cps: Cont.} = 80 | check x == 5 81 | inc r 82 | let i = 3 83 | noop() 84 | inc r 85 | check i == 3 86 | check x == 5 87 | let c = whelp foo(5) 88 | trampoline c 89 | check r == 2 90 | 91 | block: 92 | ## a while statement with a continuation and a break 93 | r = 0 94 | proc foo() {.cps: Cont.} = 95 | inc r 96 | while true: 97 | inc r 98 | noop() 99 | inc r 100 | if true: 101 | inc r 102 | break 103 | inc r 104 | fail"block break failed to break block" 105 | inc r 106 | foo() 107 | check r == 5 108 | 109 | block: 110 | ## a break inside a multiply-nested else inside a while 111 | r = 0 112 | proc foo() {.cps: Cont.} = 113 | inc r 114 | while true: 115 | inc r 116 | noop() 117 | inc r 118 | if true: 119 | inc r 120 | check r == 4 121 | if r != 4: 122 | fail"unexpected clause" 123 | else: 124 | inc r 125 | break 126 | inc r 127 | foo() 128 | check r == 6 129 | 130 | block: 131 | ## for loops with a continue and a break work correctly 132 | r = 0 133 | proc foo() {.cps: Cont.} = 134 | inc r 135 | for i in 0 .. 3: 136 | if i == 0: 137 | continue 138 | if i > 2: 139 | break 140 | r.inc i 141 | 142 | foo() 143 | check r == 4 144 | 145 | block: 146 | ## for loops with a continue and break across continuations 147 | when true: 148 | skip"pending #48" 149 | else: 150 | r = 0 151 | proc foo() {.cps: Cont.} = 152 | inc r 153 | for i in 0 .. 3: 154 | noop() 155 | if i == 0: 156 | continue 157 | if i > 2: 158 | break 159 | r.inc i 160 | 161 | foo() 162 | check r == 4 163 | 164 | block: 165 | ## named breaks work from inside a while statement 166 | var k = newKiller 6 167 | proc foo() {.cps: Cont.} = 168 | step 1 169 | block: 170 | block found: 171 | step 2 172 | while true: 173 | step 3 174 | noop() 175 | step 4 176 | if true: 177 | step 5 178 | break found 179 | fail"loop tail should be unreachable" 180 | fail"post loop should be unreachable" 181 | step 6 182 | break 183 | step 7 184 | foo() 185 | check k 186 | -------------------------------------------------------------------------------- /tests/t20_api.nim: -------------------------------------------------------------------------------- 1 | include preamble 2 | 3 | import tests/exports 4 | 5 | suite "cps api": 6 | 7 | block: 8 | ## bootstrap 9 | var k = newKiller 1 10 | proc bootstrap(): int {.cps: Cont.} = 11 | noop() 12 | step 1 13 | noop() 14 | return 3 15 | 16 | var i = bootstrap() 17 | check i is int, "bootstrap's output is not an int" 18 | check i == 3, "bootstrap's output has the wrong value" 19 | check k 20 | 21 | block: 22 | ## whelp 23 | var k = newKiller 0 24 | proc whelped(): int {.cps: Cont.} = 25 | noop() 26 | step 1 27 | noop() 28 | return 3 29 | 30 | var c = whelp whelped() 31 | check "whelp's output is bogus": 32 | c is Cont 33 | check k 34 | 35 | block: 36 | ## state symbols and trampoline 37 | var k = newKiller 1 38 | proc states(): int {.cps: Cont.} = 39 | noop() 40 | step 1 41 | noop() 42 | return 3 43 | 44 | var c = whelp states() 45 | check "whelp initial state surprised us": 46 | not c.dismissed 47 | not c.finished 48 | c.state == State.Running 49 | c.running 50 | c = cps.trampoline c 51 | check "whelp state after trampoline surprised us": 52 | not c.dismissed 53 | c.state == State.Finished 54 | c.finished 55 | not c.running 56 | check k 57 | 58 | block: 59 | ## trampolineIt 60 | var k = newKiller 1 61 | proc boing(): int {.cps: Cont.} = 62 | noop() 63 | step 1 64 | noop() 65 | return 3 66 | 67 | var c = whelp boing() 68 | trampolineIt c: 69 | check "state inside trampolineIt is " & $it.state: 70 | not it.dismissed 71 | it.running 72 | check "state post-trampolineIt is " & $c.state: 73 | not c.dismissed 74 | c.finished 75 | check k 76 | 77 | block: 78 | ## magic voodoo 79 | var k = newKiller 4 80 | 81 | proc magic(c: Cont): Cont {.cpsMagic.} = 82 | step 2 83 | check not c.dismissed, "continuation was dismissed" 84 | check not c.finished, "continuation was finished" 85 | result = c 86 | 87 | proc voodoo(c: Cont): int {.cpsVoodoo.} = 88 | step 3 89 | check not c.dismissed, "continuation was dismissed" 90 | check not c.finished, "continuation was finished" 91 | result = 2 92 | 93 | proc foo(): int {.cps: Cont.} = 94 | noop() 95 | step 1 96 | check k.step == 1, "right" 97 | magic() 98 | check k.step == 2, "magic failed to run" 99 | noop() 100 | check voodoo() == 2, "voodoo failed to yield 2" 101 | noop() 102 | step 4 103 | return 3 104 | 105 | check foo() == 3 106 | check k 107 | 108 | block: 109 | ## exporting CPS procedures works 110 | check entry() == 42 111 | 112 | block: 113 | ## accessing exported continuation's result from a continuation works 114 | var k = newKiller 2 115 | proc foo() {.cps: Cont.} = 116 | step 1 117 | check entry() == 42 118 | step 2 119 | 120 | foo() 121 | check k 122 | 123 | block: 124 | ## one can whelp a cps'd proc that was borrowed 125 | type 126 | D = distinct int 127 | 128 | proc bar(x: int) {.cps: Continuation.} = 129 | discard 130 | 131 | proc bar(d: D) {.borrow.} 132 | 133 | discard whelp bar(42.D) 134 | 135 | block: 136 | ## one can call a cps'd proc that was borrowed... from inside cps 137 | type 138 | D = distinct int 139 | 140 | var k = newKiller 3 141 | 142 | proc bar(x: int; y = 0.0) {.cps: Continuation.} = 143 | check y == 3.5 144 | check x == 42 145 | step 2 146 | 147 | proc bar(d: D; y = 0.0) {.borrow.} 148 | 149 | proc foo() {.cps: Continuation.} = 150 | step 1 151 | bar(y = 3.5, d = 42.D) 152 | step 3 153 | 154 | foo() 155 | check k 156 | 157 | block: 158 | ## calling magic that is not defined for the base type should not compile 159 | skip "FIXME: Figure out a way to test compile-time failures": 160 | type AnotherCont = ref object of Continuation 161 | proc magic(c: AnotherCont): AnotherCont {.cpsMagic.} = discard 162 | 163 | proc foo() {.cps: Cont.} = 164 | magic() 165 | 166 | block: 167 | ## calling magic/voodoo with generics continuation parameter works 168 | var k = newKiller 3 169 | 170 | type AnotherCont = ref object of Continuation 171 | proc magic(c: Cont or AnotherCont): auto {.cpsMagic.} = 172 | k.step 3 173 | c 174 | 175 | proc voodoo(c: Cont or AnotherCont) {.cpsVoodoo.} = 176 | k.step 2 177 | 178 | proc foo() {.cps: Cont.} = 179 | step 1 180 | voodoo() 181 | magic() 182 | 183 | foo() 184 | check k 185 | 186 | block: 187 | ## magic/voodoo can be defined with `using` 188 | skip "`using` can only be used on top-level, and if it is, balls fails the test in debug mode": 189 | using c: Cont 190 | var k = newKiller 4 191 | 192 | proc magic(c): Cont {.cpsMagic.} = 193 | step 2 194 | check not c.dismissed, "continuation was dismissed" 195 | check not c.finished, "continuation was finished" 196 | result = c 197 | 198 | proc voodoo(c): int {.cpsVoodoo.} = 199 | step 3 200 | check not c.dismissed, "continuation was dismissed" 201 | check not c.finished, "continuation was finished" 202 | result = 2 203 | 204 | proc foo(): int {.cps: Cont.} = 205 | noop() 206 | step 1 207 | check k.step == 1, "right" 208 | magic() 209 | check k.step == 2, "magic failed to run" 210 | noop() 211 | check voodoo() == 2, "voodoo failed to yield 2" 212 | noop() 213 | step 4 214 | return 3 215 | 216 | check foo() == 3 217 | check k 218 | 219 | block: 220 | ## parent-child voodoo works correctly 221 | # https://github.com/nim-works/cps/issues/145 222 | type 223 | C = ref object of Continuation 224 | val: int 225 | 226 | proc send(c: C, v: int) {.cpsVoodoo.} = 227 | c.val = v 228 | 229 | proc recv(c: C): int {.cpsVoodoo.} = 230 | c.val 231 | 232 | var k = newKiller(6) 233 | 234 | proc level_two() {.cps:C.} = 235 | step 2 236 | send(42) 237 | step 3 238 | echo "level_two: ", recv() 239 | let x = recv() 240 | echo "level_two: ", x 241 | step 4 242 | 243 | proc level_one() {.cps:C.} = 244 | step 1 245 | level_two() 246 | step 5 247 | echo "level_one: ", recv() 248 | let v = recv() 249 | step 6 250 | check v == 0 251 | 252 | var a = whelp level_one() 253 | trampoline a 254 | check k 255 | -------------------------------------------------------------------------------- /tests/t30_cc.nim: -------------------------------------------------------------------------------- 1 | include preamble 2 | 3 | suite "calling convention": 4 | 5 | block: 6 | ## some different callback types 7 | type 8 | C = ref object of Continuation 9 | H {.used.} = proc(x: int): float {.cps: C.} 10 | I {.used.} = proc(): float {.cps: C.} 11 | P {.used.} = proc(x: int) {.cps: C.} 12 | S {.used.} = proc() {.cps: C.} 13 | 14 | block: 15 | ## produce callbacks from a symbol 16 | type 17 | C = ref object of Continuation 18 | 19 | proc toH(x: int): float {.cps: C.} = discard 20 | proc toI(): float {.cps: C.} = discard 21 | proc toP(x: int) {.cps: C.} = discard 22 | proc toS() {.cps: C.} = discard 23 | 24 | let h {.used.} = whelp toH 25 | let i {.used.} = whelp toI 26 | let p {.used.} = whelp toP 27 | let s {.used.} = whelp toS 28 | 29 | block: 30 | ## produce callbacks that match whelp types 31 | type 32 | C = ref object of Continuation 33 | H {.used.} = proc(x: int): float {.cps: C.} 34 | I {.used.} = proc(): float {.cps: C.} 35 | P {.used.} = proc(x: int) {.cps: C.} 36 | S {.used.} = proc() {.cps: C.} 37 | 38 | proc toH(x: int): float {.cps: C.} = discard 39 | proc toI(): float {.cps: C.} = discard 40 | proc toP(x: int) {.cps: C.} = discard 41 | proc toS() {.cps: C.} = discard 42 | 43 | let h {.used.}: H = whelp toH 44 | let i {.used.}: I = whelp toI 45 | let p {.used.}: P = whelp toP 46 | let s {.used.}: S = whelp toS 47 | 48 | block: 49 | ## execute a callback with arguments 50 | type 51 | C = ref object of Continuation 52 | 53 | proc foo(x: int): float {.cps: C.} = 54 | result = x.float 55 | 56 | const cb = whelp foo 57 | var c = cb.call(3) 58 | var d = cb.call(5) 59 | check cb.recover(c) == 3.0 60 | check cb.recover(d) == 5.0 61 | 62 | block: 63 | ## callbacks run from inside cps run pass, tail, etc. 64 | var k = newKiller 7 65 | 66 | type 67 | ContCall = proc(a: int): int {.cps: Cont.} 68 | 69 | proc tail(c: Continuation; d: Cont): Cont = 70 | d.mom = c 71 | step 3 72 | d 73 | 74 | proc pass(c: Cont; d: Cont): Cont = 75 | step 4 76 | d 77 | 78 | proc head(c: Cont): Cont = 79 | step 1 80 | c 81 | 82 | proc bar(a: int): int {.cps: Cont.} = 83 | noop() 84 | step 5 85 | return a * 2 86 | 87 | proc foo(c: ContCall) {.cps: Cont.} = 88 | step 2 89 | var x = c(4) 90 | step 6 91 | check x == 8 92 | step 7 93 | 94 | foo: whelp bar 95 | check k 96 | 97 | block: 98 | ## whelp helps disambiguate cps callbacks at instantiation 99 | var k = newKiller 2 100 | 101 | type 102 | ContCall {.used.} = proc(a: int): int {.cps: Cont.} 103 | MoreCall {.used.} = proc(a: float): float {.cps: Cont.} 104 | 105 | proc bar(a: float): float {.cps: Cont.} = 106 | noop() 107 | return a * 2.0 108 | 109 | proc bar(a: int): int {.cps: Cont.} = 110 | step 2 111 | noop() 112 | return a * 2 113 | 114 | proc foo(c: ContCall) {.cps: Cont.} = 115 | step 1 116 | var x = c(4) 117 | check x == 8 118 | 119 | foo: whelp(ContCall, bar) 120 | check k 121 | 122 | block: 123 | ## run a callback in cps with natural syntax 124 | when not cpsCallOperatorSupported or defined(cpsNoCallOperator): 125 | skip "unsupported on nim " & NimVersion 126 | else: 127 | 128 | type 129 | ContCall = proc(a: int): int {.cps: Cont.} 130 | 131 | var k = newKiller 3 132 | 133 | proc bar(a: int): int {.cps: Cont.} = 134 | noop() 135 | step 2 136 | return a * 2 137 | 138 | proc foo(c: ContCall) {.cps: Cont.} = 139 | step 1 140 | let x = c(4) 141 | check x == 8 142 | step 3 143 | 144 | foo: whelp bar 145 | check k 146 | 147 | block: 148 | ## run a callback with no return value in cps 149 | when not cpsCallOperatorSupported or defined(cpsNoCallOperator): 150 | skip "unsupported on nim " & NimVersion 151 | else: 152 | 153 | type 154 | ContCall = proc(a: int) {.cps: Cont.} 155 | 156 | var k = newKiller 3 157 | 158 | proc bar(a: int) {.cps: Cont.} = 159 | noop() 160 | step 2 161 | 162 | proc foo(c: ContCall) {.cps: Cont.} = 163 | step 1 164 | c(4) 165 | step 3 166 | 167 | foo: whelp bar 168 | check k 169 | 170 | block: 171 | ## callback illustration 172 | type 173 | C = ref object of Continuation 174 | Callback = proc(x: int): float {.cps: C.} 175 | 176 | proc bar(a: int): float {.cps: C.} = 177 | return 2.0 * a.float 178 | 179 | const 180 | cb: Callback = whelp bar 181 | 182 | var x: C = cb.call(2) 183 | var y: C = cb.call(4) 184 | check cb.recover(x) == 4.0 185 | check cb.recover(y) == 8.0 186 | -------------------------------------------------------------------------------- /tests/t50_hooks.nim: -------------------------------------------------------------------------------- 1 | include preamble 2 | 3 | import std/genasts 4 | import std/macros 5 | import std/os 6 | import std/sequtils 7 | import std/strutils 8 | 9 | from cps/spec import Hook, cpsStackFrames 10 | from cps/hooks import findColonLit 11 | 12 | suite "hooks": 13 | 14 | block: 15 | ## cooperative yield hooks are used automatically 16 | shouldRun 6: 17 | proc coop(c: Cont): Cont {.cpsMagic, used.} = 18 | ran() 19 | result = c 20 | 21 | proc foo() {.cps: Cont.} = 22 | var i = 0 23 | while i < 3: 24 | ran() 25 | noop() 26 | inc i 27 | if i == 0: 28 | continue 29 | if i > 2: 30 | break 31 | 32 | foo() 33 | 34 | block: 35 | ## control-flow tracing hooks are used automatically 36 | var found: seq[string] 37 | macro trace(hook: static[Hook]; c, n: typed; 38 | fun: string; info: LineInfo; body: typed): untyped = 39 | var body = 40 | if body.kind == nnkNilLit: 41 | newEmptyNode() 42 | else: 43 | body 44 | let fun = 45 | if hook == Stack: c.findColonLit("fun", string) 46 | else: fun.strVal 47 | genAst(c, hook, fun, info, body): 48 | let sub = fun.split("_", maxsplit=1)[0] 49 | var last = 50 | case hook 51 | of Dealloc: "😎" 52 | of Stack: sub 53 | else: astToStr c 54 | var path = info.filename.lastPathPart 55 | path = if path == "t50_hooks.nim": "👍" else: path 56 | found.add "$#: $# $# $#" % [ $hook, $sub, last, path ] 57 | body 58 | 59 | proc bar() {.cps: Cont.} = 60 | noop() 61 | 62 | proc foo() {.cps: Cont.} = 63 | var i = 0 64 | while i < 3: 65 | noop() 66 | inc i 67 | if i == 0: 68 | continue 69 | if i > 2: 70 | break 71 | bar() 72 | 73 | foo() 74 | let s = found.join("\10") 75 | 76 | proc normalize(s: string): seq[string] = 77 | var s = strip s 78 | result = splitLines s 79 | result = map(result, proc(x: string): string = strip(x)) 80 | 81 | when cpsStackFrames: 82 | const 83 | expected = """ 84 | alloc: cps:foo() env Cont 👍 85 | head: trace nil 👍 86 | stack: foo foo 👍 87 | boot: c nil 👍 88 | trace: foo continuation 👍 89 | coop: Cont nil environment.nim 90 | trace: cps:foo() loop continuation 👍 91 | trace: cps:foo() jump noop() continuation 👍 92 | tail: Cont continuation 👍 93 | alloc: cps:bar() env Cont 👍 94 | stack: bar bar 👍 95 | boot: Cont nil 👍 96 | pass: cps:foo() env Cont(continuation) 👍 97 | trace: bar continuation 👍 98 | trace: cps:bar() jump noop() continuation 👍 99 | pass: continuation.mom Cont(continuation) environment.nim 100 | coop: result nil normalizedast.nim 101 | dealloc: cps:bar() env 😎 environment.nim 102 | trace: cps:foo() child bar() continuation normalizedast.nim 103 | coop: Cont nil environment.nim 104 | trace: cps:foo() loop continuation 👍 105 | trace: cps:foo() jump noop() continuation 👍 106 | tail: Cont continuation 👍 107 | alloc: cps:bar() env Cont 👍 108 | stack: bar bar 👍 109 | boot: Cont nil 👍 110 | pass: cps:foo() env Cont(continuation) 👍 111 | trace: bar continuation 👍 112 | trace: cps:bar() jump noop() continuation 👍 113 | pass: continuation.mom Cont(continuation) environment.nim 114 | coop: result nil normalizedast.nim 115 | dealloc: cps:bar() env 😎 environment.nim 116 | trace: cps:foo() child bar() continuation normalizedast.nim 117 | coop: Cont nil environment.nim 118 | trace: cps:foo() loop continuation 👍 119 | trace: cps:foo() jump noop() continuation 👍 120 | dealloc: cps:foo() env 😎 👍 121 | """ 122 | else: 123 | const 124 | expected = """ 125 | alloc: cps:foo() env Cont 👍 126 | head: trace nil 👍 127 | boot: c nil 👍 128 | trace: foo continuation 👍 129 | coop: Cont nil environment.nim 130 | trace: cps:foo() loop continuation 👍 131 | trace: cps:foo() jump noop() continuation 👍 132 | tail: Cont continuation 👍 133 | alloc: cps:bar() env Cont 👍 134 | boot: Cont nil 👍 135 | pass: cps:foo() env Cont(continuation) 👍 136 | trace: bar continuation 👍 137 | trace: cps:bar() jump noop() continuation 👍 138 | pass: continuation.mom Cont(continuation) environment.nim 139 | coop: result nil normalizedast.nim 140 | dealloc: cps:bar() env 😎 environment.nim 141 | trace: cps:foo() child bar() continuation normalizedast.nim 142 | coop: Cont nil environment.nim 143 | trace: cps:foo() loop continuation 👍 144 | trace: cps:foo() jump noop() continuation 👍 145 | tail: Cont continuation 👍 146 | alloc: cps:bar() env Cont 👍 147 | boot: Cont nil 👍 148 | pass: cps:foo() env Cont(continuation) 👍 149 | trace: bar continuation 👍 150 | trace: cps:bar() jump noop() continuation 👍 151 | pass: continuation.mom Cont(continuation) environment.nim 152 | coop: result nil normalizedast.nim 153 | dealloc: cps:bar() env 😎 environment.nim 154 | trace: cps:foo() child bar() continuation normalizedast.nim 155 | coop: Cont nil environment.nim 156 | trace: cps:foo() loop continuation 👍 157 | trace: cps:foo() jump noop() continuation 👍 158 | dealloc: cps:foo() env 😎 👍 159 | """ 160 | let x = expected.normalize 161 | let y = s.normalize 162 | if x != y: 163 | if x.len != y.len: 164 | checkpoint "expected:" 165 | for n in x.items: 166 | checkpoint n 167 | checkpoint "\n" 168 | checkpoint "received:" 169 | for n in y.items: 170 | checkpoint n 171 | else: 172 | var i = 0 173 | for (a, b) in zip(x, y).items: 174 | if a != b: 175 | checkpoint "#", i, " < ", a 176 | checkpoint "#", i, " > ", b 177 | inc i 178 | fail "trace output doesn't match" 179 | 180 | block: 181 | ## custom continuation allocators are used automatically 182 | shouldRun 1: 183 | proc alloc[T](root: typedesc[Cont]; c: typedesc[T]): T = 184 | ran() 185 | new c 186 | 187 | proc foo(x: int) {.cps: Cont.} = 188 | check x == 3 189 | noop() 190 | check x == 3 191 | 192 | foo(3) 193 | 194 | block: 195 | ## custom continuation deallocators can nil the continuation 196 | shouldRun 5: 197 | proc dealloc[T: Cont](c: sink T; E: typedesc[T]): E = 198 | ran() # (runs twice) 199 | c = nil 200 | 201 | proc bar() {.cps: Cont.} = 202 | ran() 203 | noop() 204 | ran() 205 | 206 | proc foo(x: int) {.cps: Cont.} = 207 | check x == 3 208 | noop() 209 | check x == 3 210 | bar() 211 | ran() 212 | 213 | foo(3) 214 | 215 | block: 216 | ## custom continuation deallocators work with whelp 217 | shouldRun 4: 218 | proc dealloc[T: Cont](c: sink T; E: typedesc[T]): E = 219 | ran() 220 | c = nil 221 | 222 | proc bar() {.cps: Cont.} = 223 | ran() 224 | noop() 225 | ran() 226 | 227 | proc foo(x: int) {.cps: Cont.} = 228 | check x == 3 229 | noop() 230 | check x == 3 231 | bar() 232 | ran() 233 | 234 | let c = whelp foo(3) 235 | trampoline c 236 | 237 | block: 238 | ## custom continuation passing hook works 239 | shouldRun 34: 240 | proc pass(a: Cont; b: Continuation): Continuation = 241 | echo "pass to parent" 242 | for n in 0..9: ran() 243 | result = b 244 | 245 | proc pass(a: Cont; b: Cont): Continuation = 246 | echo "pass to child" 247 | for n in 0..9: ran() 248 | for n in 0..9: ran() 249 | result = b 250 | 251 | proc bar() {.cps: Cont.} = 252 | ran() 253 | noop() 254 | ran() 255 | 256 | proc foo() {.cps: Cont.} = 257 | ran() 258 | bar() 259 | ran() 260 | 261 | foo() 262 | 263 | block: 264 | ## custom continuation bootstrap hook works 265 | shouldRun 2: 266 | 267 | proc bar() {.cps: Cont.} = 268 | noop() 269 | 270 | proc boot(c: Cont): Cont = 271 | ran() 272 | result = c 273 | 274 | proc foo() {.cps: Cont.} = 275 | bar() 276 | 277 | foo() 278 | 279 | var c = whelp foo() 280 | c = cps.trampoline c 281 | 282 | block: 283 | ## custom continuation head/tail setup hooks work 284 | var h, t = 0 285 | 286 | proc head(c: Cont): Cont = 287 | inc h 288 | result = c 289 | 290 | proc tail(mom: Continuation; c: Cont): Continuation = 291 | inc t 292 | result = c 293 | result.mom = mom 294 | 295 | proc bar() {.cps: Cont.} = 296 | check h == 1, "parent triggered second" 297 | noop() 298 | 299 | proc foo() {.cps: Cont.} = 300 | check h == 1, "parent triggered first" 301 | check t == 0, "child triggered first" 302 | bar() 303 | check t == 1, "child triggered second" 304 | 305 | var c = whelp foo() 306 | c = cps.trampoline c 307 | check "bzzzt whelped": 308 | h == t 309 | t == 1 310 | 311 | h = 0 312 | t = 0 313 | foo() 314 | check "bzzzt bootstrapped": 315 | h == t 316 | t == 1 317 | 318 | block: 319 | ## custom continuation exception handling works 320 | var k = newKiller 4 321 | proc unwind(c: Cont; ex: ref Exception): Continuation {.cpsMagic, used.} = 322 | inc k 323 | result = cps.unwind(c, ex) 324 | 325 | proc bar() {.cps: Cont.} = 326 | step 2 327 | noop() 328 | raise IOError.newException "hol' up" 329 | 330 | proc foo() {.cps: Cont.} = 331 | step 1 332 | bar() 333 | step 4 334 | 335 | expect IOError: 336 | foo() 337 | check k 338 | -------------------------------------------------------------------------------- /tests/t60_returns.nim: -------------------------------------------------------------------------------- 1 | import std/macros 2 | 3 | include preamble 4 | 5 | suite "returns and results": 6 | 7 | block: 8 | ## local assignment to a continuation return value 9 | var k = newKiller 3 10 | proc bar(a: int): int {.cps: Cont.} = 11 | noop() 12 | step 2 13 | return a * 2 14 | 15 | proc foo() {.cps: Cont.} = 16 | step 1 17 | let x = bar(4) 18 | step 3 19 | check x == 8 20 | 21 | foo() 22 | check k 23 | 24 | block: 25 | ## continuations can return values via bootstrap 26 | var k = newKiller 1 27 | proc foo(x: int): int {.cps: Cont.} = 28 | noop() 29 | step 1 30 | return x * x 31 | 32 | let x = foo(3) 33 | check x == 9 34 | check k 35 | 36 | block: 37 | ## continuations can return values via whelp 38 | var k = newKiller 1 39 | proc foo(x: int): int {.cps: Cont.} = 40 | noop() 41 | step 1 42 | return x * x 43 | 44 | var c = whelp foo(5) 45 | trampoline c 46 | check "recover operator works correctly": 47 | recover(c) == 25 48 | check k 49 | 50 | block: 51 | ## assignments to the special result symbol work 52 | block: 53 | var k = newKiller 1 54 | proc foo(x: int): int {.cps: Cont.} = 55 | noop() 56 | step 1 57 | result = x * x 58 | 59 | let x = foo(3) 60 | check x == 9 61 | check k 62 | 63 | block: 64 | var k = newKiller 1 65 | proc foo(x: int): int {.cps: Cont.} = 66 | noop() 67 | step 1 68 | result = x * x 69 | 70 | var c = whelp foo(5) 71 | trampoline c 72 | check k 73 | 74 | block: 75 | ## naked returns in continuations with a complication are fine 76 | var k = newKiller 1 77 | proc foo() {.cps: Cont.} = 78 | noop() 79 | step 1 80 | if true: 81 | return 82 | step 2 83 | 84 | foo() 85 | check k 86 | 87 | block: 88 | ## dismissing a child continuation is fun 89 | var k = newKiller 2 90 | proc bar(a: int): int {.cps: Cont.} = 91 | noop() 92 | step 2 93 | result = a * 2 94 | dismiss() 95 | 96 | proc foo() {.cps: Cont.} = 97 | step 1 98 | let x = bar(4) 99 | step 3 100 | check x == 8 101 | 102 | foo() 103 | check k 104 | 105 | block: 106 | ## assignment to a continuation return value 107 | var k = newKiller 3 108 | proc bar(a: int): int {.cps: Cont.} = 109 | noop() 110 | step 2 111 | return a * 2 112 | 113 | proc foo() {.cps: Cont.} = 114 | step 1 115 | var x: int 116 | x = bar(4) 117 | step 3 118 | check x == 8 119 | 120 | foo() 121 | check k 122 | 123 | block: 124 | ## local assignment tuple unpacking a continution return value 125 | var k = newKiller 3 126 | proc bar(): (int, int) {.cps: Cont.} = 127 | noop() 128 | step 2 129 | return (1, 2) 130 | 131 | proc foo(): int {.cps: Cont.} = 132 | step 1 133 | let (a, b) = bar() 134 | step 3 135 | check a == 1 136 | check b == 2 137 | (a + b) * 2 138 | 139 | var c = whelp foo() 140 | trampoline c 141 | check "recover operator works correctly": 142 | 6 == recover c 143 | check k 144 | 145 | block: 146 | ## discarding a continuation return value works 147 | var k = newKiller(2) 148 | proc bar(): string {.cps: Cont.} = 149 | step 1 150 | result = "test" 151 | 152 | proc foo() {.cps: Cont.} = 153 | discard bar() 154 | step 2 155 | 156 | foo() 157 | check k 158 | 159 | block: 160 | ## returning a continuation return value works 161 | var k = newKiller(1) 162 | proc bar(): string {.cps: Cont.} = 163 | step 1 164 | result = "test" 165 | 166 | proc foo(): string {.cps: Cont.} = 167 | return bar() 168 | 169 | check foo() == "test" 170 | check k 171 | 172 | block: 173 | ## returning an anonymous tuple declaration type 174 | var k = newKiller(2) 175 | proc bar(): tuple[x, y: int] {.cps: Cont.} = 176 | noop() 177 | step 1 178 | result.x = 10 179 | result.y = 20 180 | 181 | proc foo() {.cps: Cont.} = 182 | let (x, y) = bar() 183 | step 2 184 | check (x, y) == (10, 20) 185 | 186 | foo() 187 | check k 188 | 189 | block: 190 | ## returning a named tuple type 191 | var k = newKiller(2) 192 | 193 | type T = tuple[x, y: int] 194 | 195 | proc bar(): T {.cps: Cont.} = 196 | noop() 197 | step 1 198 | result.x = 10 199 | result.y = 20 200 | 201 | proc foo() {.cps: Cont.} = 202 | let (x, y) = bar() 203 | step 2 204 | check (x, y) == (10, 20) 205 | 206 | foo() 207 | check k 208 | 209 | block: 210 | ## converting a cps return value 211 | var k = newKiller(3) 212 | 213 | proc bar(): int {.cps: Cont.} = 214 | noop() 215 | 42 216 | 217 | proc foo() {.cps: Cont.} = 218 | let x = Natural bar() 219 | let x1 = Natural int Natural bar() 220 | 221 | step 1 222 | check x == 42 223 | 224 | var y: Natural 225 | y = Natural bar() 226 | y = Natural int Natural bar() 227 | 228 | step 2 229 | check y == 42 230 | 231 | discard Natural bar() 232 | discard Natural int Natural bar() 233 | step 3 234 | 235 | foo() 236 | check k 237 | 238 | block: 239 | ## calling continuation with variant object access as parameter 240 | type 241 | O = object 242 | case switch: bool 243 | of true: x: int 244 | of false: discard 245 | 246 | proc bar(x: int): int {.cps: Continuation.} = x 247 | 248 | proc foo(o: O) {.cps: Continuation.} = 249 | check bar(o.x) == 42 250 | 251 | foo(O(switch: true, x: 42)) 252 | -------------------------------------------------------------------------------- /tests/t80_try1.nim: -------------------------------------------------------------------------------- 1 | import std/strutils 2 | 3 | include preamble 4 | 5 | from cps/spec import cpsStackFrames 6 | 7 | suite "try statements": 8 | 9 | var r = 0 10 | 11 | block: 12 | ## try-except statements may be split across continuations 13 | r = 0 14 | proc foo() {.cps: Cont.} = 15 | inc r 16 | try: 17 | noop() 18 | inc r 19 | except CatchableError: 20 | fail "this branch should not run" 21 | inc r 22 | 23 | trampoline whelp(foo()) 24 | check r == 3 25 | 26 | block: 27 | ## try-except statements may split and also raise exceptions 28 | r = 0 29 | proc foo() {.cps: Cont.} = 30 | inc r 31 | try: 32 | noop() 33 | inc r 34 | raise newException(CatchableError, "test") 35 | fail "statement run after raise" 36 | except CatchableError: 37 | check getCurrentExceptionMsg() == "test" 38 | inc r 39 | inc r 40 | 41 | trampoline whelp(foo()) 42 | check r == 4 43 | 44 | block: 45 | ## exception clauses may split across continuations 46 | r = 0 47 | proc foo() {.cps: Cont.} = 48 | inc r 49 | try: 50 | noop() 51 | inc r 52 | raise newException(CatchableError, "test") 53 | fail "statement run after raise" 54 | except CatchableError: 55 | inc r 56 | noop() 57 | check getCurrentExceptionMsg() == "test" 58 | inc r 59 | inc r 60 | 61 | trampoline whelp(foo()) 62 | check r == 5 63 | 64 | block: 65 | ## exceptions raised in the current continuation work 66 | r = 0 67 | proc foo() {.cps: Cont.} = 68 | inc r 69 | try: 70 | inc r 71 | raise newException(CatchableError, "test") 72 | fail "statement run after raise" 73 | except CatchableError: 74 | inc r 75 | noop() 76 | check getCurrentExceptionMsg() == "test" 77 | inc r 78 | inc r 79 | 80 | trampoline whelp(foo()) 81 | check r == 5 82 | 83 | block: 84 | ## except statement catching multiple exception types across splits 85 | proc foo() {.cps: Cont.} = 86 | inc r 87 | try: 88 | noop() 89 | inc r 90 | raise newException(ValueError, "test") 91 | fail "statement run after raise" 92 | except ValueError, IOError: 93 | check getCurrentExceptionMsg() == "test" 94 | inc r 95 | 96 | inc r 97 | 98 | proc bar() {.cps: Cont.} = 99 | # Same as foo(), but with the constraints switched 100 | inc r 101 | try: 102 | noop() 103 | inc r 104 | raise newException(ValueError, "test") 105 | fail "statement run after raise" 106 | except IOError, ValueError: 107 | check getCurrentExceptionMsg() == "test" 108 | inc r 109 | 110 | inc r 111 | 112 | r = 0 113 | trampoline whelp(foo()) 114 | check r == 4 115 | 116 | r = 0 117 | trampoline whelp(bar()) 118 | check r == 4 119 | 120 | block: 121 | ## try statements with a finally clause 122 | r = 0 123 | proc foo() {.cps: Cont.} = 124 | inc r 125 | try: 126 | noop() 127 | inc r 128 | finally: 129 | inc r 130 | 131 | trampoline whelp(foo()) 132 | check r == 3 133 | 134 | block: 135 | ## try statements with a finally and a return 136 | r = 0 137 | 138 | proc foo() {.cps: Cont.} = 139 | inc r 140 | try: 141 | noop() 142 | inc r 143 | return 144 | fail"statement run after return" 145 | finally: 146 | inc r 147 | 148 | fail"statement run after try-finally containing a return" 149 | 150 | trampoline whelp(foo()) 151 | check r == 3 152 | 153 | block: 154 | ## try statements with an exception and a finally 155 | r = 0 156 | proc foo() {.cps: Cont.} = 157 | inc r 158 | try: 159 | noop() 160 | inc r 161 | raise newException(CatchableError, "") 162 | fail "statement run after raise" 163 | except CatchableError: 164 | inc r 165 | finally: 166 | inc r 167 | inc r 168 | 169 | trampoline whelp(foo()) 170 | check r == 5 171 | 172 | block: 173 | ## try statements with a split in finally 174 | r = 0 175 | proc foo() {.cps: Cont.} = 176 | inc r 177 | 178 | try: 179 | noop() 180 | inc r 181 | finally: 182 | noop() 183 | inc r 184 | 185 | inc r 186 | 187 | trampoline whelp(foo()) 188 | check r == 4 189 | 190 | block: 191 | ## try statements with a split in finally with an unhandled exception 192 | r = 0 193 | proc foo() {.cps: Cont.} = 194 | inc r 195 | 196 | try: 197 | noop() 198 | inc r 199 | raise newException(ValueError, "test") 200 | fail"code run after raise" 201 | finally: 202 | noop() 203 | inc r 204 | 205 | fail"code run after raising try-finally" 206 | 207 | expect ValueError: 208 | trampoline whelp(foo()) 209 | check r == 3 210 | 211 | block: 212 | ## nested try statements within the except branch 213 | r = 0 214 | proc foo() {.cps: Cont.} = 215 | inc r 216 | try: 217 | noop() 218 | inc r 219 | raise newException(CatchableError, "test") 220 | fail "statement run after raise" 221 | except CatchableError: 222 | check getCurrentExceptionMsg() == "test" 223 | inc r 224 | 225 | try: 226 | noop() 227 | inc r 228 | raise newException(CatchableError, "test 2") 229 | fail "statement run after raise" 230 | except CatchableError: 231 | check getCurrentExceptionMsg() == "test 2" 232 | inc r 233 | 234 | check getCurrentExceptionMsg() == "test" 235 | inc r 236 | 237 | inc r 238 | 239 | trampoline whelp(foo()) 240 | check r == 7 241 | 242 | block: 243 | ## calling a continuation that handles exception while handling an exception 244 | r = 0 245 | proc foo() {.cps: Cont.} = 246 | inc r 247 | 248 | try: 249 | noop() 250 | inc r 251 | raise newException(CatchableError, "test") 252 | except CatchableError: 253 | noop() 254 | inc r 255 | check getCurrentExceptionMsg() == "test" 256 | 257 | inc r 258 | 259 | try: 260 | raise newException(CatchableError, "outside cps test") 261 | except CatchableError: 262 | trampoline whelp(foo()) 263 | 264 | check r == 4 265 | check getCurrentExceptionMsg() == "outside cps test" 266 | 267 | block: 268 | ## running a continuation that handles exception then raises while handling 269 | ## an exception in the exception handler 270 | when defined(isNimSkull): 271 | skip "semantically invalid under this compiler" 272 | else: 273 | 274 | r = 0 275 | 276 | # This is a very delicate test designed to demonstrate an issue with 277 | # Nim's exception stack mechanism and CPS 278 | 279 | proc foo() {.cps: Cont.} = 280 | inc r 281 | 282 | try: 283 | noop() 284 | inc r 285 | raise newException(CatchableError, "test") 286 | except CatchableError: 287 | noop() 288 | inc r 289 | check getCurrentExceptionMsg() == "test" 290 | raise 291 | 292 | fail"this statement cannot be run" 293 | 294 | var c: Continuation = whelp foo() 295 | # Run two iterations, which should place us right after the raise 296 | # 297 | # At this point, the parent of our `raise` is `nil`, because there wasn't 298 | # any exception being handled at the point of raise. 299 | for _ in 1 .. 2: 300 | c = c.fn(c) 301 | 302 | try: 303 | raise newException(CatchableError, "outside cps test") 304 | except CatchableError: 305 | # Now we handle an exception, which the current exception is now 306 | # "outside cps test" 307 | try: 308 | # Run the tramp to finish `c`, which will end in a re-raise. 309 | trampoline c 310 | fail"continuing `c` should raise" 311 | except CatchableError: 312 | check r == 3 313 | # Confirm that this is the exception from cps 314 | check getCurrentExceptionMsg() == "test" 315 | 316 | # Confirm that the stack has been fixed and the parent of the inner 317 | # exception is the outer. 318 | check getCurrentExceptionMsg() == "outside cps test" 319 | 320 | block: 321 | ## calling a continuation with finally while handling an exception 322 | r = 0 323 | proc foo() {.cps: Cont.} = 324 | inc r 325 | 326 | try: 327 | noop() 328 | inc r 329 | finally: 330 | noop() 331 | inc r 332 | 333 | inc r 334 | 335 | try: 336 | raise newException(CatchableError, "outside cps test") 337 | except CatchableError: 338 | trampoline whelp(foo()) 339 | 340 | check r == 4 341 | check getCurrentExceptionMsg() == "outside cps test" 342 | 343 | block: 344 | ## except T as e keep the type T in cps 345 | r = 0 346 | 347 | type 348 | SpecialError = object of CatchableError 349 | extra: int ## An extra field so we can verify that we can access it 350 | 351 | proc newSpecialError(msg: string, extra: int): ref SpecialError = 352 | result = newException(SpecialError, msg) 353 | result.extra = extra 354 | 355 | proc foo() {.cps: Cont.} = 356 | inc r 357 | try: 358 | noop() 359 | inc r 360 | raise newSpecialError("test", 42) 361 | fail "statement run after raise" 362 | except SpecialError as e: 363 | noop() 364 | inc r 365 | check e.msg == "test" 366 | # The reason we test access is because `is` is expanded before `e` is 367 | # processed by cps. By testing access we can be sure that even after 368 | # cps processing it's still the correct type. 369 | check e.extra == 42 370 | 371 | foo() 372 | check r == 3 373 | 374 | block: 375 | ## try statement with one cps jump as the body 376 | r = 0 377 | 378 | proc noop(c: Cont): Cont {.cpsMagic.} = 379 | inc r 380 | result = c 381 | 382 | proc foo() {.cps: Cont.} = 383 | try: 384 | noop() 385 | except CatchableError: 386 | fail"this except branch should not run" 387 | 388 | inc r 389 | 390 | trampoline whelp(foo()) 391 | check r == 2 392 | -------------------------------------------------------------------------------- /tests/t80_try2.nim: -------------------------------------------------------------------------------- 1 | import std/strutils 2 | 3 | include preamble 4 | 5 | from cps/spec import cpsStackFrames 6 | 7 | suite "try statements": 8 | 9 | block: 10 | ## handling exception across multiple continuations 11 | var k = newKiller(6) 12 | proc foo() {.cps: Cont.} = 13 | noop() 14 | step 4 15 | raise newException(ValueError, "foo") 16 | 17 | proc bar() {.cps: Cont.} = 18 | noop() 19 | step 3 20 | foo() 21 | 22 | proc barbar() {.cps: Cont.} = 23 | try: 24 | noop() 25 | step 2 26 | bar() 27 | except ValueError as e: 28 | step 5 29 | doAssert e.msg == "foo" 30 | 31 | proc foobar() {.cps: Cont.} = 32 | step 1 33 | barbar() 34 | step 6 35 | 36 | trampoline whelp(foobar()) 37 | check k 38 | 39 | block: 40 | ## try statement with a single statement which is a cps assignment 41 | var k = newKiller(2) 42 | proc bar(): int {.cps: Cont.} = 43 | step 1 44 | 42 45 | 46 | proc foo() {.cps: Cont.} = 47 | var x = 0 48 | try: 49 | x = bar() 50 | except CatchableError: 51 | fail "This branch should not be executed" 52 | 53 | step 2 54 | check x == 42 55 | 56 | trampoline whelp(foo()) 57 | check k 58 | 59 | block: 60 | ## try-finally-reraise escape via break statements. 61 | var k = newKiller(1) 62 | 63 | proc foo() {.cps: Cont.} = 64 | while true: 65 | try: 66 | noop() 67 | step 1 68 | raise newException(ValueError, "") 69 | finally: 70 | break 71 | fail "statement in while-loop after break" 72 | fail "statement after unhandled exception" 73 | 74 | expect ValueError: 75 | trampoline whelp(foo()) 76 | check k 77 | 78 | block: 79 | ## try-finally-reraise escape via continue statements. 80 | var k = newKiller(1) 81 | 82 | proc foo() {.cps: Cont.} = 83 | while true: 84 | try: 85 | noop() 86 | step 1 87 | raise newException(ValueError, "") 88 | finally: 89 | continue 90 | fail "statement in while-loop after finally" 91 | fail "statement after unhandled exception" 92 | 93 | expect ValueError: 94 | trampoline whelp(foo()) 95 | check k 96 | 97 | block: 98 | ## try: raise() except: continue 99 | var k = newKiller(2) 100 | 101 | proc foo() {.cps: Cont.} = 102 | try: 103 | while true: 104 | noop() 105 | inc k 106 | if k.step == 2: 107 | break 108 | try: 109 | raise newException(ValueError, "oops") 110 | except ValueError: 111 | continue 112 | fail "statement in while-loop after continue" 113 | except ValueError: 114 | fail "uncaught exception" 115 | 116 | trampoline whelp(foo()) 117 | check k 118 | 119 | block: 120 | ## try-finally-reraise escape via return statements. 121 | var k = newKiller(1) 122 | 123 | proc foo() {.cps: Cont.} = 124 | try: 125 | noop() 126 | step 1 127 | raise newException(ValueError, "") 128 | finally: 129 | return 130 | fail "statement after return" 131 | 132 | expect ValueError: 133 | trampoline whelp(foo()) 134 | check k 135 | 136 | block: 137 | ## try-finally-reraise handle after escape attempt 138 | var k = newKiller(2) 139 | 140 | proc foo() {.cps: Cont.} = 141 | try: 142 | while true: 143 | try: 144 | noop() 145 | step 1 146 | raise newException(ValueError, "") 147 | finally: 148 | break 149 | except ValueError: 150 | step 2 151 | 152 | trampoline whelp(foo()) 153 | check k 154 | 155 | block: 156 | ## the stack trace probably still works 157 | when not cpsStackFrames: 158 | skip"--stacktrace:off specified" 159 | else: 160 | var r = 0 161 | proc foo() {.cps: Cont.} = 162 | noop() 163 | inc r 164 | try: 165 | raise newException(CatchableError, "test") 166 | except CatchableError: 167 | let frames = renderStackFrames() 168 | check frames.len > 0, "expected at least one stack trace record" 169 | check "t80_try2.nim" in frames[0], "couldn't find t80_try2.nim in the trace" 170 | raise 171 | 172 | try: 173 | trampoline whelp(foo()) 174 | inc r 175 | except CatchableError as e: 176 | check e.msg == "test", "unable to pass exception message from cps" 177 | check r == 1 178 | 179 | suite "defer statements": 180 | 181 | var r = 0 182 | 183 | block: 184 | ## a defer statement works across a continuation 185 | r = 0 186 | proc foo() {.cps: Cont.} = 187 | defer: 188 | check r == 2, "defer run before end of scope" 189 | inc r 190 | 191 | inc r 192 | noop() 193 | inc r 194 | 195 | foo() 196 | check r == 3 197 | 198 | block: 199 | ## a basic defer statement is supported 200 | r = 0 201 | proc foo() {.cps: Cont.} = 202 | inc r 203 | noop() 204 | defer: 205 | check r == 4 206 | inc r 207 | inc r 208 | defer: 209 | check r == 3 210 | inc r 211 | inc r 212 | 213 | foo() 214 | check r == 5 215 | 216 | block: 217 | ## a defer in a nested template is supported 218 | r = 0 219 | 220 | template deferChk(i: int) = 221 | inc r 222 | defer: 223 | check r == i 224 | inc r 225 | 226 | proc foo() {.cps: Cont.} = 227 | deferChk(5) 228 | inc r 229 | deferChk(4) 230 | inc r 231 | 232 | foo() 233 | check r == 6 234 | 235 | block: 236 | ## a defer inside a block statement works 237 | r = 0 238 | proc foo() {.cps: Cont.} = 239 | inc r 240 | block: 241 | defer: 242 | check r == 2 243 | inc r 244 | inc r 245 | defer: 246 | check r == 4 247 | inc r 248 | inc r 249 | 250 | foo() 251 | check r == 5 252 | 253 | block: 254 | ## a naked defer is not a problem 255 | r = 0 256 | proc foo() {.cps: Cont.} = 257 | defer: 258 | inc r 259 | 260 | foo() 261 | check r == 1 262 | 263 | 264 | when defined(gcArc) or defined(gcOrc): 265 | suite "breaking deterministic memory managers": 266 | block: 267 | ## try-except-statement splits 268 | proc foo() {.cps: Cont.} = 269 | var k = newKiller(3) 270 | step 1 271 | try: 272 | noop() 273 | step 2 274 | except CatchableError: 275 | fail "this branch should not run" 276 | step 3 277 | check k 278 | 279 | foo() 280 | 281 | block: 282 | ## try-except splits with raise 283 | proc foo() {.cps: Cont.} = 284 | var k = newKiller(4) 285 | step 1 286 | try: 287 | noop() 288 | step 2 289 | raise newException(CatchableError, "") 290 | fail "statement run after raise" 291 | except CatchableError: 292 | step 3 293 | step 4 294 | check k 295 | 296 | foo() 297 | 298 | block: 299 | ## try-finally-statement splits 300 | proc foo() {.cps: Cont.} = 301 | var k = newKiller(4) 302 | step 1 303 | try: 304 | noop() 305 | step 2 306 | finally: 307 | step 3 308 | step 4 309 | check k 310 | 311 | foo() 312 | 313 | block: 314 | ## try-except-finally splits with raise 315 | proc foo() {.cps: Cont.} = 316 | var k = newKiller(5) 317 | step 1 318 | try: 319 | noop() 320 | step 2 321 | raise newException(CatchableError, "") 322 | fail "statement run after raise" 323 | except CatchableError: 324 | step 3 325 | finally: 326 | step 4 327 | step 5 328 | check k 329 | 330 | foo() 331 | -------------------------------------------------------------------------------- /tests/t90_exprs1.nim: -------------------------------------------------------------------------------- 1 | include preamble 2 | 3 | suite "expression flattening": 4 | test "flatten expression list in var/let": 5 | var k = newKiller(3) 6 | proc foo() {.cps: Cont.} = 7 | let 8 | x = (noop(); step 1; 42) 9 | y = (noop(); step 2; x) 10 | 11 | check x == y 12 | 13 | var (a, b) = (noop(); step 3; (10, y)) 14 | check a == 10 15 | check b == y 16 | 17 | foo() 18 | check k 19 | 20 | test "flatten block expression": 21 | var k = newKiller(3) 22 | proc foo() {.cps: Cont.} = 23 | step 1 24 | 25 | let x = block: 26 | noop() 27 | step 2 28 | 42 29 | 30 | step 3 31 | check x == 42 32 | 33 | foo() 34 | check k 35 | 36 | test "flatten if expression": 37 | var k = newKiller(5) 38 | proc foo() {.cps: Cont.} = 39 | step 1 40 | 41 | let x = 42 | if true: 43 | noop() 44 | step 2 45 | 42 46 | elif true: 47 | fail "this branch should not run" 48 | 0 # needed because the compiler doesn't recognize fail as noreturn 49 | else: 50 | fail "this branch should not run" 51 | -1 # needed because the compiler doesn't recognize fail as noreturn 52 | 53 | step 3 54 | check x == 42 55 | 56 | let y = 57 | if false: 58 | fail "this branch should not run" 59 | -1 60 | elif false: 61 | fail "this branch should not run" 62 | 0 63 | else: 64 | noop() 65 | step 4 66 | 30 67 | 68 | step 5 69 | check y == 30 70 | 71 | foo() 72 | check k 73 | 74 | test "flatten case expression": 75 | var k = newKiller(3) 76 | proc foo() {.cps: Cont.} = 77 | step 1 78 | 79 | let x = 80 | case "true" 81 | of "truer", "truest", "very true": 82 | fail "this branch should not run" 83 | 0 84 | of "false": 85 | fail "this branch should not run" 86 | -1 87 | of "true": 88 | noop() 89 | step 2 90 | 42 91 | elif true: 92 | fail "this branch should not run" 93 | -3 94 | else: 95 | fail "this branch should not run" 96 | -2 97 | 98 | step 3 99 | check x == 42 100 | 101 | foo() 102 | check k 103 | 104 | test "flatten try statement": 105 | var k = newKiller(4) 106 | proc foo() {.cps: Cont.} = 107 | step 1 108 | 109 | let x = 110 | try: 111 | raise newException(ValueError, "something") 112 | 0 113 | except ValueError, IOError: 114 | noop() 115 | let e = getCurrentException() 116 | check e of ValueError 117 | check e.msg == "something" 118 | step 2 119 | 42 120 | except CatchableError: 121 | fail "this branch should not run" 122 | -1 123 | finally: 124 | step 3 125 | 126 | step 4 127 | check x == 42 128 | 129 | foo() 130 | check k 131 | 132 | test "flatten if condition": 133 | var k = newKiller(5) 134 | proc foo() {.cps: Cont.} = 135 | step 1 136 | 137 | if (noop(); step 2; false): 138 | fail "This branch should not be run" 139 | elif (noop(); step 3; true): 140 | step 4 141 | elif (noop(); fail"This expression should not be evaluated"; false): 142 | fail "This branch should not be run" 143 | else: 144 | fail "This branch should not be run" 145 | 146 | step 5 147 | 148 | foo() 149 | check k 150 | -------------------------------------------------------------------------------- /tests/t90_exprs2.nim: -------------------------------------------------------------------------------- 1 | include preamble 2 | 3 | suite "expression flattening": 4 | test "flatten case matching expression": 5 | var k = newKiller(4) 6 | proc foo() {.cps: Cont.} = 7 | step 1 8 | 9 | case (noop(); step 2; "string") 10 | of "str": 11 | fail "This branch should not be run" 12 | of "string": 13 | step 3 14 | else: 15 | fail "This branch should not be run" 16 | 17 | step 4 18 | 19 | foo() 20 | check k 21 | 22 | test "flatten case elif branches": 23 | var k = newKiller(4) 24 | proc foo() {.cps: Cont.} = 25 | step 1 26 | 27 | case "string" 28 | of "str": 29 | fail "This branch should not be run" 30 | of "String": 31 | fail "This branch should not be run" 32 | elif (noop(); step 2; true): 33 | step 3 34 | else: 35 | fail "This branch should not be run" 36 | 37 | step 4 38 | 39 | foo() 40 | check k 41 | 42 | test "flatten while condition": 43 | var k = newKiller(4) 44 | proc foo() {.cps: Cont.} = 45 | step 1 46 | 47 | var x = 2 48 | while (noop(); step x; inc x; x < 4): 49 | discard 50 | 51 | step 4 52 | 53 | foo() 54 | check k 55 | 56 | test "flatten assignments with LHS being a symbol": 57 | var k = newKiller(3) 58 | proc foo() {.cps: Cont.} = 59 | step 1 60 | var x: int 61 | x = 62 | if true: 63 | noop() 64 | step 2 65 | 42 66 | else: 67 | fail "this branch should not be run" 68 | -1 69 | 70 | step 3 71 | 72 | check x == 42 73 | 74 | foo() 75 | check k 76 | 77 | test "flatten assignments with LHS being an object access": 78 | type 79 | A = object 80 | i: int 81 | O = object 82 | a: A 83 | 84 | var k = newKiller(3) 85 | proc foo() {.cps: Cont.} = 86 | step 1 87 | var o: O 88 | o.a.i = 89 | if true: 90 | noop() 91 | step 2 92 | 42 93 | else: 94 | fail "this branch should not be run" 95 | -1 96 | 97 | step 3 98 | 99 | check o.a.i == 42 100 | 101 | foo() 102 | check k 103 | 104 | test "flatten assignments with LHS being a ref access from immutable location": 105 | type 106 | A = object 107 | i: int 108 | O = ref object 109 | a: A 110 | 111 | var k = newKiller(3) 112 | proc foo() {.cps: Cont.} = 113 | step 1 114 | let o = O() 115 | o.a.i = 116 | if true: 117 | noop() 118 | step 2 119 | 42 120 | else: 121 | fail "this branch should not be run" 122 | return 123 | 124 | step 3 125 | 126 | check o.a.i == 42 127 | 128 | foo() 129 | check k 130 | -------------------------------------------------------------------------------- /tests/t90_exprs3.nim: -------------------------------------------------------------------------------- 1 | include preamble 2 | 3 | suite "expression flattening": 4 | test "flatten unpacking assignments": 5 | type 6 | O = object 7 | x: int 8 | y: int 9 | 10 | var k = newKiller(3) 11 | proc foo() {.cps: Cont.} = 12 | step 1 13 | var o = O() 14 | (o.x, o.y) = 15 | if true: 16 | noop() 17 | step 2 18 | (42, 10) 19 | else: 20 | fail "this branch should not be run" 21 | return 22 | 23 | step 3 24 | 25 | check o.x == 42 26 | check o.y == 10 27 | 28 | foo() 29 | check k 30 | 31 | test "flatten upcasting assignments": 32 | when not defined(release) and not defined(isNimSkull): 33 | skip"compiler crashes on debug" 34 | else: 35 | type 36 | O = ref object of RootObj 37 | x: int 38 | y: int 39 | I = ref object of O 40 | 41 | var k = newKiller(3) 42 | proc foo() {.cps: Cont.} = 43 | step 1 44 | var o = O() 45 | o = 46 | if true: 47 | noop() 48 | step 2 49 | I(x: 42, y: 10) 50 | else: 51 | fail "this branch should not be run" 52 | I(x: 42, y: 20) 53 | 54 | step 3 55 | 56 | check o of I 57 | check o.x == 42 58 | check o.y == 10 59 | 60 | foo() 61 | check k 62 | 63 | test "flatten implicitly converted assignments": 64 | var k = newKiller(3) 65 | proc foo() {.cps: Cont.} = 66 | step 1 67 | let o: int = 68 | if true: 69 | noop() 70 | step 2 71 | Natural(42) 72 | else: 73 | fail "this branch should not be run" 74 | return 75 | 76 | step 3 77 | 78 | check o == 42 79 | 80 | foo() 81 | check k 82 | 83 | test "flatten explicitly converted assignments": 84 | var k = newKiller(3) 85 | proc foo() {.cps: Cont.} = 86 | step 1 87 | let i = int(block: (noop(); step 2; 42.Natural)) 88 | 89 | step 3 90 | 91 | check i == 42 92 | 93 | foo() 94 | check k 95 | 96 | test "flatten discard statements": 97 | var k = newKiller(3) 98 | proc foo() {.cps: Cont.} = 99 | step 1 100 | discard (block: (noop(); step 2; 42.Natural)) 101 | 102 | step 3 103 | 104 | foo() 105 | check k 106 | 107 | test "flatten return statements": 108 | var k = newKiller(2) 109 | proc foo(): int {.cps: Cont.} = 110 | step 1 111 | return (block: (noop(); step 2; 42.Natural)) 112 | 113 | check foo() == 42 114 | check k 115 | -------------------------------------------------------------------------------- /tests/t90_exprs4.nim: -------------------------------------------------------------------------------- 1 | include preamble 2 | 3 | suite "expression flattening": 4 | test "flatten array construction": 5 | var k = newKiller(5) 6 | proc foo() {.cps: Cont.} = 7 | step 1 8 | 9 | let x = [1, 2, (step 2; 42), (noop(); step 3; 10), (step 4; 20)] 10 | 11 | step 5 12 | check x == [1, 2, 42, 10, 20] 13 | 14 | foo() 15 | check k 16 | 17 | test "flatten tuple construction": 18 | var k = newKiller(5) 19 | proc foo() {.cps: Cont.} = 20 | step 1 21 | 22 | let x = (a: 1, b: 2, c: (step 2; 42), d: (noop(); step 3; 10), e: (step 4; 20)) 23 | 24 | step 5 25 | # A few checks to verify that the names stay 26 | check x.a == 1 27 | check x.b == 2 28 | check x == (1, 2, 42, 10, 20) 29 | 30 | foo() 31 | check k 32 | 33 | test "flatten object construction": 34 | type 35 | O = object of RootObj 36 | x: int 37 | y: int 38 | z: float 39 | 40 | var k = newKiller(5) 41 | proc foo() {.cps: Cont.} = 42 | step 1 43 | 44 | let x = O(x: (step 2; 42), y: (noop(); step 3; 10), z: (step 4; 20)) 45 | 46 | step 5 47 | # A few checks to verify that the names stay 48 | check x == O(x: 42, y: 10, z: 20) 49 | 50 | foo() 51 | check k 52 | 53 | test "flatten calls": 54 | var k = newKiller(5) 55 | 56 | proc bar(a, b: int) = 57 | step 3 58 | check a == 42 59 | check b == 10 60 | 61 | proc barvar(a: var int, b: int) = 62 | step 5 63 | check a == 20 64 | check b == 20 65 | 66 | proc foo() {.cps: Cont.} = 67 | step 1 68 | var x = 42 69 | bar(x, (noop(); step 2; x = 10; x)) 70 | 71 | x = 42 72 | barvar(x, (noop(); step 4; x = 20; x)) 73 | 74 | foo() 75 | check k 76 | 77 | test "flatten and/or with short circuiting": 78 | var k = newKiller(7) 79 | 80 | proc foo() {.cps: Cont.} = 81 | step 1 82 | check (noop(); step 2; true) and (noop(); step 3; true) 83 | check not((noop(); step 4; false) and (noop(); fail "this should not run"; true)) 84 | check (noop(); step 5; false) or (noop(); step 6; true) 85 | check (noop(); step 7; true) or (noop(); fail "this should not run"; false) 86 | 87 | foo() 88 | check k 89 | 90 | test "flatten raise statement": 91 | var k = newKiller(3) 92 | 93 | proc foo() {.cps: Cont.} = 94 | step 1 95 | try: 96 | raise (noop(); step 2; newException(CatchableError, "test")) 97 | except CatchableError as e: 98 | step 3 99 | check e.msg == "test" 100 | 101 | foo() 102 | check k 103 | -------------------------------------------------------------------------------- /tests/t90_exprs5.nim: -------------------------------------------------------------------------------- 1 | include preamble 2 | 3 | suite "expression flattening": 4 | test "flatten pragma block expression": 5 | var k = newKiller(3) 6 | 7 | proc foo() {.cps: Cont.} = 8 | step 1 9 | let x = 10 | block: 11 | {.cast(gcsafe).}: 12 | noop() 13 | step 2 14 | 10 15 | step 3 16 | check x == 10 17 | 18 | foo() 19 | check k 20 | 21 | test "flatten result expressions": 22 | var k = newKiller(1) 23 | proc foo(): int {.cps: Cont.} = 24 | noop() 25 | step 1 26 | 42.Natural 27 | 28 | check foo() == 42 29 | check k 30 | 31 | test "flatten bracket expressions (array access)": 32 | var k = newKiller(2) 33 | 34 | proc foo() {.cps: Cont.} = 35 | check (noop(); step 1; [42])[(noop(); step 2; 0)] == 42 36 | 37 | foo() 38 | check k 39 | 40 | test "flatten dot expressions": 41 | type 42 | P = object 43 | val: int 44 | 45 | var k = newKiller(1) 46 | 47 | proc foo() {.cps: Cont.} = 48 | check (noop(); step 1; P(val: 42)).val == 42 49 | 50 | foo() 51 | check k 52 | 53 | test "flatten dereference expressions": 54 | type 55 | P = ref object 56 | val: int 57 | 58 | var k = newKiller(1) 59 | 60 | proc foo() {.cps: Cont.} = 61 | check (noop(); step 1; P(val: 42))[].val == 42 62 | 63 | foo() 64 | check k 65 | 66 | test "flatten hidden dereference expressions": 67 | type 68 | P = ref object 69 | val: int 70 | 71 | var k = newKiller(1) 72 | 73 | proc foo() {.cps: Cont.} = 74 | check (noop(); step 1; P(val: 42)).val == 42 75 | 76 | foo() 77 | check k 78 | 79 | test "flatten magic calls with mutable variables": 80 | var k = newKiller(3) 81 | 82 | proc foo() {.cps: Cont.} = 83 | var x: string 84 | # add(var string, string) is a magic 85 | x.add (noop(); step 1; "test") 86 | check x == "test" 87 | 88 | var y: seq[string] 89 | # add(var seq[T], T) is a magic with generics 90 | y.add (noop(); step 2; "test") 91 | check y == @["test"] 92 | 93 | step 3 94 | 95 | foo() 96 | check k 97 | -------------------------------------------------------------------------------- /tests/zevv.nim: -------------------------------------------------------------------------------- 1 | const 2 | strictTrampoline {.booldefine.} = false 3 | 4 | import balls 5 | import posix 6 | import std/macros 7 | #import std/unittest 8 | 9 | import cps 10 | 11 | 12 | type 13 | C = ref object of Continuation 14 | 15 | # Trampoline with count safeguard 16 | 17 | var jumps = 0 18 | 19 | when strictTrampoline: 20 | proc run(c: C) = 21 | jumps = 0 22 | var c = c 23 | while c != nil and c.fn != nil: 24 | c = c.fn(c) 25 | inc jumps 26 | doAssert jumps < 1000, "Too many iterations on trampoline, looping?" 27 | else: 28 | template run(c) = c 29 | 30 | # Helper templates. 31 | 32 | # This checks if the trampoline jumped the right number of times 33 | 34 | template expJumps(expect: int, body: untyped) = 35 | body 36 | doAssert jumps == expect, "Trampoline jumped " & $jumps & " times, expected " & $expect 37 | 38 | # is a primitive that keeps track of how often it is called, verify with 39 | # `expPrims` macro 40 | 41 | var prims = 0 42 | 43 | proc prim(c: C): C {.cpsMagic.} = 44 | inc prims 45 | return c 46 | 47 | template expPrims(expect: int, body: untyped) = 48 | prims = 0 49 | body 50 | doAssert prims == expect, "prim was called " & $prims & " times, expected " & $expect 51 | 52 | 53 | # Wrapper for defining a function and sending it to the trampoline 54 | 55 | template runCps(body: untyped) = 56 | proc t() {.cps:C.} = body 57 | run t() 58 | 59 | # We have a lot of these for the purpose of control-flow validation 60 | {.warning[UnreachableCode]: off.} 61 | 62 | var r: int 63 | 64 | suite "suite, suite zevv": 65 | 66 | test "nocall": 67 | expPrims 0: runCps: 68 | discard 69 | 70 | test "onecall": 71 | expPrims 1: runCps: 72 | prim() 73 | 74 | test "twocall": 75 | expPrims 2: runCps: 76 | prim() 77 | prim() 78 | 79 | test "if true": 80 | expPrims 3: runCps: 81 | var a: int 82 | prim() 83 | if true: 84 | prim() 85 | prim() 86 | 87 | test "if false": 88 | expPrims 2: runCps: 89 | var a: int 90 | prim() 91 | if false: 92 | prim() 93 | prim() 94 | 95 | test "if true if false": 96 | expPrims 3: runCps: 97 | prim() 98 | if true: 99 | prim() 100 | if false: 101 | prim() 102 | prim() 103 | 104 | test "nested if 1": 105 | expPrims 4: runCps: 106 | var a: int 107 | prim() 108 | if true: 109 | prim() 110 | if true: 111 | prim() 112 | prim() 113 | 114 | test "nested if 2": 115 | expPrims 3: runCps: 116 | var a: int 117 | prim() 118 | if true: 119 | prim() 120 | if false: 121 | prim() 122 | prim() 123 | 124 | test "nested if 3": 125 | expPrims 2: runCps: 126 | prim() 127 | if false: 128 | prim() 129 | if true: 130 | prim() 131 | prim() 132 | 133 | test "block1": 134 | expPrims 3: runCps: 135 | prim() 136 | block: 137 | prim() 138 | prim() 139 | 140 | test "while1": 141 | expPrims 5: runCps: 142 | prim() 143 | var a: int = 0 144 | while a < 3: 145 | prim() 146 | inc a 147 | prim() 148 | 149 | test "break1": 150 | expPrims 3: runCps: 151 | prim() 152 | while true: 153 | prim() 154 | break 155 | prim() 156 | prim() 157 | 158 | test "break2": 159 | expPrims 3: runCps: 160 | prim() 161 | block: 162 | prim() 163 | break 164 | prim() 165 | prim() 166 | prim() 167 | 168 | test "for1": 169 | runCps: 170 | var a: int = 0 171 | for i in 0..3: 172 | inc a, 1 173 | check a == 4 174 | 175 | test "for2": 176 | expPrims 1: runCps: 177 | var a: int = 0 178 | prim() 179 | for i in 0..3: 180 | inc a, 1 181 | check a == 4 182 | 183 | test "multiple variables in one var": 184 | runCps: 185 | var a, b: int16 186 | check $type(a) == "int16" 187 | check $type(b) == "int16" 188 | 189 | test "wrongreturn": 190 | runCps: 191 | var n = 0 192 | while n == 0: 193 | echo "one ", n 194 | let s = len("") 195 | inc n 196 | 197 | test "continue": 198 | expPrims 8: runCps: 199 | prim() 200 | var i: int = 0 201 | while i < 10: 202 | inc i 203 | if i < 5: 204 | continue 205 | prim() 206 | prim() 207 | 208 | test "for3": 209 | expPrims 1: runCps: 210 | var a: int = 0 211 | for i in 0..3: 212 | inc a, 1 213 | check a == 4 214 | prim() 215 | 216 | test "defer": 217 | expPrims 3: runCps: 218 | prim() 219 | defer: 220 | prim() 221 | prim() 222 | 223 | test "nested while": 224 | expPrims 100: runCps: 225 | var i: int 226 | var j: int 227 | while i < 10: 228 | inc i 229 | j = 0 230 | while j < 10: 231 | inc j 232 | prim() 233 | 234 | test "paper example 1": 235 | expPrims 2: runCps: 236 | var t: bool = false 237 | while not t: 238 | prim() 239 | break 240 | prim() 241 | prim() 242 | 243 | proc foo(c: C, fd: int16): C {.cpsMagic.} = 244 | discard 245 | 246 | test "int": 247 | proc test1() {.cps:C} = 248 | foo(1) 249 | test1() 250 | 251 | test "'i16": 252 | proc test1() {.cps:C} = 253 | foo(1'i16) 254 | test1() 255 | 256 | test "int16()": 257 | proc test1() {.cps:C} = 258 | foo(int16(1)) 259 | test1() 260 | 261 | test ".int16": 262 | proc test1() {.cps:C} = 263 | foo(1.int16) 264 | test1() 265 | 266 | test "type problem": 267 | type Thing = distinct int 268 | proc foo(): Thing = 1.Thing 269 | runCps: 270 | var a = foo() 271 | 272 | test "Running -> Lampable -> Done": 273 | 274 | proc running(c: C): bool = c != nil and c.fn != nil 275 | proc dismissed(c: C): bool = c == nil 276 | proc done(c: C): bool = c != nil and c.fn == nil 277 | 278 | var save: C 279 | 280 | proc jield(c: C): C {.cpsMagic.} = 281 | save = c 282 | 283 | proc count() {.cps:C.} = 284 | var i = 0 285 | while i < 2: 286 | jield() 287 | echo i 288 | inc i 289 | 290 | var c: Continuation = whelp count() 291 | c = c.fn(c) # boot 292 | check c.state == Running 293 | c = c.fn(c) # first jield 294 | check c.state == Dismissed 295 | c = save 296 | check c.state == Running 297 | c = c.fn(c) # echo 298 | check c.state == Running 299 | c = c.fn(c) # second jield 300 | check c.state == Dismissed 301 | c = save 302 | check c.state == Running 303 | c = c.fn(c) # echo 304 | check c.state == Running 305 | c = c.fn(c) # done 306 | check c.state == Finished 307 | 308 | -------------------------------------------------------------------------------- /tutorial/cpstut1.nim: -------------------------------------------------------------------------------- 1 | 2 | # Baby steps: my first cps program 3 | 4 | import cps 5 | import deques 6 | 7 | type 8 | MyCont = ref object of Continuation 9 | 10 | proc hello() {.cps:MyCont.} = 11 | echo "Hello, world!" 12 | 13 | var c: Continuation = whelp hello() 14 | 15 | doAssert c.running() 16 | 17 | c = c.fn(c) 18 | 19 | doAssert c.finished 20 | -------------------------------------------------------------------------------- /tutorial/cpstut2.nim: -------------------------------------------------------------------------------- 1 | 2 | # A more elaborate example: cooperative scheduling 3 | 4 | import cps 5 | import deques 6 | 7 | type 8 | MyCont = ref object of Continuation 9 | 10 | var work: Deque[Continuation] 11 | 12 | proc runWork() = 13 | while work.len > 0: 14 | discard trampoline work.popFirst() 15 | 16 | proc schedule(c: MyCont): MyCont {.cpsMagic.} = 17 | work.addLast c 18 | return nil 19 | 20 | proc animal(name: string) {.cps:MyCont.}= 21 | var i = 0 22 | while i < 4: 23 | inc i 24 | echo name, " ", i 25 | schedule() 26 | echo "" 27 | 28 | work.addLast whelp animal("donkey") 29 | work.addLast whelp animal("tiger") 30 | 31 | runWork() 32 | -------------------------------------------------------------------------------- /tutorial/cpstut3.nim: -------------------------------------------------------------------------------- 1 | 2 | # Growing your own continuations 3 | 4 | import cps 5 | import deques 6 | 7 | type 8 | 9 | Work = ref object 10 | queue: Deque[Continuation] 11 | 12 | MyCont = ref object of Continuation 13 | work: Work 14 | 15 | proc schedule(c: MyCont): MyCont {.cpsMagic.} = 16 | c.work.queue.addLast c 17 | return nil 18 | 19 | proc push(work: Work, c: MyCont) = 20 | work.queue.addLast c 21 | c.work = work 22 | 23 | proc run(work: Work) = 24 | while work.queue.len > 0: 25 | discard trampoline work.queue.popFirst() 26 | 27 | proc animal(name: string) {.cps:MyCont.}= 28 | var i = 0 29 | while i < 4: 30 | inc i 31 | echo name, " ", i 32 | schedule() 33 | echo "" 34 | 35 | var mywork = Work() 36 | mywork.push whelp animal("donkey") 37 | mywork.push whelp animal("tiger") 38 | mywork.run() 39 | -------------------------------------------------------------------------------- /tutorial/cpstut4.nim: -------------------------------------------------------------------------------- 1 | 2 | # Growing your own continuations 3 | 4 | import cps 5 | import deques 6 | 7 | type 8 | 9 | Work = ref object 10 | queue: Deque[Continuation] 11 | 12 | MyCont = ref object of Continuation 13 | work: Work 14 | 15 | proc pass(cFrom, cTo: MyCont): MyCont = 16 | cTo.work = cFrom.work 17 | return cTo 18 | 19 | proc schedule(c: MyCont): MyCont {.cpsMagic.} = 20 | c.work.queue.addLast c 21 | return nil 22 | 23 | proc push(work: Work, c: MyCont) = 24 | work.queue.addLast c 25 | c.work = work 26 | 27 | proc work(work: Work) = 28 | while work.queue.len > 0: 29 | discard trampoline work.queue.popFirst() 30 | 31 | proc sayHi(name: string, i: int) {.cps:MyCont.} = 32 | echo "Hi ", name, " ", i 33 | schedule() 34 | 35 | proc runner(name: string) {.cps:MyCont.}= 36 | var i = 0 37 | while i < 4: 38 | inc i 39 | sayHi(name, i) 40 | echo "" 41 | 42 | var mywork = Work() 43 | mywork.push whelp runner("donkey") 44 | mywork.push whelp runner("tiger") 45 | mywork.work() 46 | --------------------------------------------------------------------------------