├── .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 |
--------------------------------------------------------------------------------
/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🟢 for3❔ defer: 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 |
--------------------------------------------------------------------------------