├── .github ├── scripts │ └── install-nim.ps1 └── workflows │ └── test.yml ├── .gitignore ├── cascade.nim ├── cascade.nimble ├── license ├── readme.md └── tests ├── tests.nim └── tests.nim.cfg /.github/scripts/install-nim.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [parameter(mandatory)] 3 | [string] $nimVersion 4 | ) 5 | 6 | set-strictMode -version 3 7 | $ErrorActionPreference = 'Stop' 8 | 9 | if (-not $isWindows) { 10 | $initPath = join-path $home "init.sh" 11 | 12 | $request = @{ 13 | uri = "https://nim-lang.org/choosenim/init.sh" 14 | outFile = $initPath 15 | } 16 | 17 | invoke-webrequest @request 18 | 19 | $env:CHOOSENIM_CHOOSE_VERSION = $nimVersion 20 | sh $initPath -y 21 | 22 | $nimDir = get-content (join-path $home .choosenim current) 23 | write-host "::add-path::$(join-path $nimDir bin)" 24 | write-host "::add-path::$(join-path $home .nimble bin)" 25 | } else { 26 | # TODO: use choosenim on Windows once x64 is supported 27 | # https://github.com/dom96/choosenim/issues/128 28 | 29 | $packageName = "nim-1.0.0" 30 | $zipPath = join-path $home "nim.zip" 31 | 32 | $request = @{ 33 | uri = "https://nim-lang.org/download/${packageName}_x64.zip" 34 | outFile = $zipPath 35 | } 36 | 37 | invoke-webrequest @request 38 | 39 | $extractDir = split-path $zipPath -parent 40 | expand-archive $zipPath -destinationPath $extractDir 41 | 42 | join-path $extractDir $packageName | set-location 43 | 44 | & .\finish -y 45 | 46 | write-host "::add-path::$(join-path $extractDir $packageName bin)" 47 | write-host "::add-path::$(join-path $home .nimble bin)" 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | push: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | test: 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [windows-latest, ubuntu-latest, macOS-latest] 16 | nim: [stable, devel] 17 | exclude: 18 | # only testing stable on Windows until choosenim supports x64 19 | - os: windows-latest 20 | nim: devel 21 | name: Nim ${{ matrix.nim }} (${{ matrix.os }}) 22 | steps: 23 | - uses: actions/checkout@master 24 | - name: Install Nim 25 | shell: pwsh 26 | run: ./.github/scripts/install-nim.ps1 ${{ matrix.nim }} 27 | - name: Install dependencies 28 | run: nimble install -y 29 | - name: Run tests 30 | run: nimble test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE configurations 2 | .c9 3 | .idea 4 | .vscode 5 | 6 | # built files 7 | nimcache 8 | *.exe 9 | cascade 10 | tests/tests 11 | -------------------------------------------------------------------------------- /cascade.nim: -------------------------------------------------------------------------------- 1 | import macros 2 | 3 | # rewrite a tree of dot expressions to make the 4 | # given `id` the topmost object in the chain 5 | proc rewriteDotExpr (id, dotExpr: NimNode): NimNode = 6 | var lhs = dotExpr[0] 7 | var rhs = dotExpr[1] 8 | 9 | if lhs.kind != nnkDotExpr: 10 | return newDotExpr(newDotExpr(id, lhs), rhs) 11 | 12 | result = newDotExpr(id, rhs) 13 | var chain: seq[NimNode] 14 | 15 | while true: 16 | if lhs.kind == nnkDotExpr: 17 | rhs = lhs[1] 18 | lhs = lhs[0] 19 | chain.add rhs 20 | else: 21 | chain.add lhs 22 | break 23 | 24 | for i in countdown(chain.len - 1, 0): 25 | result[0] = newDotExpr(result[0], chain[i]) 26 | 27 | macro cascade* (obj: typed, body: untyped): untyped = 28 | let statements = 29 | if body.kind == nnkDo: body[6] 30 | else: body 31 | statements.expectKind(nnkStmtList) 32 | 33 | proc transform (id, statementList: NimNode, init = newStmtList()): NimNode = 34 | result = init 35 | for statement in statementList: 36 | case statement.kind 37 | of nnkIfStmt, nnkWhenStmt: 38 | # recurse into each branch of `if` or `when` 39 | 40 | for branch in statement: 41 | case branch.kind 42 | of nnkElifBranch: 43 | branch[1] = transform(id, branch[1]) 44 | of nnkElse: 45 | branch[0] = transform(id, branch[0]) 46 | else: 47 | discard 48 | 49 | result.add statement 50 | of nnkAsgn: 51 | var lhs = statement[0] 52 | if lhs.kind == nnkDotExpr: 53 | lhs = rewriteDotExpr(id, lhs) 54 | else: 55 | lhs = newDotExpr(id, lhs) 56 | 57 | result.add newAssignment(lhs, statement[1]) 58 | of nnkCall, nnkCommand: 59 | var call: NimNode 60 | if statement[0].kind == nnkDotExpr: 61 | call = newCall(rewriteDotExpr(id, statement[0])) 62 | else: 63 | call = newCall(statement[0], id) 64 | 65 | for i in 1 ..< statement.len: 66 | call.add statement[i] 67 | 68 | result.add call 69 | else: 70 | result.add statement 71 | 72 | # create a `var` declaration with a unique generated identifier name 73 | let id = genSym(kind = nskVar) 74 | # assign the initial object to this new `var` 75 | let assignment = newVarStmt(id, obj) 76 | 77 | # kick off transformation on a new statement list 78 | # each `statement` in `statements` will be added to this 79 | # new statement list after being bound to the initial object 80 | result = transform(id, statements, newStmtList(assignment)) 81 | 82 | # return the initial object 83 | result.add id 84 | -------------------------------------------------------------------------------- /cascade.nimble: -------------------------------------------------------------------------------- 1 | version = "1.0.0" 2 | author = "citycide" 3 | description = "Method & assignment cascades for Nim, inspired by Smalltalk & Dart." 4 | license = "MIT" 5 | skipDirs = @["tests"] 6 | 7 | requires "nim >= 1.0.0" 8 | 9 | task test, "Run tests": 10 | exec "nim c -r " & "tests/tests.nim" 11 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Bo Lingen (github.com/haltcase) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # cascade · [![nimble](https://flat.badgen.net/badge/available%20on/nimble/yellow)](https://nimble.directory/pkg/cascade) ![license](https://flat.badgen.net/github/license/haltcase/cascade) 2 | 3 | > Method & assignment cascades for Nim, inspired by Smalltalk & Dart. 4 | 5 | cascade is a macro for Nim that implements _method cascades_, a feature 6 | originally from Smalltalk that's made its way into modern languages like 7 | [Dart][dart] and [Kotlin][kotlin]. 8 | 9 | It allows you to avoid repeating an object's name for each method call 10 | or assignment. A common case is something like a button: 11 | 12 | ```nim 13 | # before 14 | var res = Button() 15 | res.text = "ok" 16 | res.width = 30 17 | res.color = "#13a89e" 18 | res.enable() 19 | ``` 20 | 21 | With cascade, you don't need to repeat yourself: 22 | 23 | ```nim 24 | # after 25 | let btn = cascade Button(): 26 | text = "ok" 27 | width = 30 28 | color = "#13a89e" 29 | enable() 30 | ``` 31 | 32 | Also notice you can avoid declaring a `var` if you don't need to modify 33 | the target object after the fact — the object is mutable within the 34 | cascade block but becomes a `let` binding outside of that block. 35 | 36 | ## installation & usage 37 | 38 | Install using [Nimble][nimble]: 39 | 40 | ```shell 41 | nimble install cascade 42 | ``` 43 | 44 | Then `import` and use: 45 | 46 | ```nim 47 | import cascade 48 | 49 | let x = cascade y: 50 | z = 10 51 | f() 52 | ``` 53 | 54 | ## supported constructs 55 | 56 | * field assignment 57 | 58 | ```nim 59 | let foo = cascade Foo(): 60 | bar = 100 61 | 62 | # ↑ equivalent ↓ 63 | 64 | var foo = Foo() 65 | foo.bar = 100 66 | ``` 67 | 68 | * nested field assignment 69 | 70 | ```nim 71 | let foo = cascade Foo(): 72 | bar.baz.qux = "awesome" 73 | 74 | # ↑ equivalent ↓ 75 | 76 | var foo = Foo() 77 | foo.bar.baz.qux = "awesome" 78 | ``` 79 | 80 | * proc/template/method calls 81 | 82 | ```nim 83 | let foo = cascade Foo(): 84 | fn("hello", "world") 85 | 86 | # ↑ equivalent ↓ 87 | 88 | var foo = Foo() 89 | foo.fn("hello", "world") 90 | ``` 91 | 92 | * nested calls on fields 93 | 94 | ```nim 95 | let foo = cascade Foo(): 96 | bar.baz.seqOfStrings.add "more awesome" 97 | 98 | # ↑ equivalent ↓ 99 | 100 | var foo = Foo() 101 | foo.bar.baz.seqOfStrings.add "more awesome" 102 | ``` 103 | 104 | * `if` and `when` conditionals 105 | 106 | ```nim 107 | let foo = cascade Foo(): 108 | if someCondition: bar.baz = 2 109 | 110 | # ↑ equivalent ↓ 111 | 112 | var foo = Foo() 113 | if someCondition: foo.bar.baz = 2 114 | ``` 115 | 116 | * `cascade`s can be nested within each other 117 | 118 | ```nim 119 | let foo = cascade Foo(): 120 | bar = cascade Bar(): 121 | baz = cascade Baz(): 122 | str = "we're down here now!" 123 | 124 | # ↑ equivalent ↓ 125 | 126 | var foo = Foo() 127 | foo.bar = Bar() 128 | foo.bar.baz = Baz(str: "we're down here now!") 129 | ``` 130 | 131 | > Is something missing? Check the open [issues][issues] first or open a new 132 | one. Pull requests are appreciated! 133 | 134 | ## building 135 | 136 | To build cascade from source you'll need to have [Nim][nim] installed, 137 | and should also have [Nimble][nimble], Nim's package manager. 138 | 139 | 1. Clone the repo: `git clone https://github.com/haltcase/cascade.git` 140 | 2. Move into the newly cloned directory: `cd cascade` 141 | 3. Make your changes: `cascade.nim`, `tests/tests.nim` 142 | 4. Run tests: `nimble test` 143 | 144 | ## contributing 145 | 146 | You can check the [issues][issues] for anything unresolved, search for a 147 | problem you're encountering, or open a new one. Pull requests for improvements 148 | are also welcome. 149 | 150 | ## license 151 | 152 | MIT © [Bo Lingen / haltcase](https://github.com/haltcase) 153 | 154 | [dart]: https://dart.dev/language/operators#cascade-notation 155 | [kotlin]: https://kotlinlang.org/docs/scope-functions.html#apply 156 | [nim]: https://github.com/nim-lang/nim 157 | [nimble]: https://github.com/nim-lang/nimble 158 | [issues]: https://github.com/haltcase/cascade/issues 159 | -------------------------------------------------------------------------------- /tests/tests.nim: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import cascade 4 | 5 | type 6 | Qux = object 7 | ns: seq[int] 8 | 9 | Baz = object 10 | qux: Qux 11 | 12 | Bar = object 13 | n: int 14 | baz: Baz 15 | 16 | Foo = object 17 | str: string 18 | num: int 19 | list: seq[string] 20 | bar: Bar 21 | 22 | Button = object 23 | text: string 24 | width: int 25 | color: string 26 | 27 | method enable (btn: Button) {.base.} = 28 | discard "some method" 29 | 30 | proc setStr (foo: var Foo, val: string, suffix: string) = 31 | foo.str = "hello " & val & suffix 32 | 33 | template blk (foo: Foo, body: untyped) = 34 | body 35 | 36 | suite "cascade": 37 | test "basic field assignment": 38 | let foo = cascade Foo(): 39 | str = "hello" 40 | num = 10 41 | 42 | check foo == Foo(str: "hello", num: 10) 43 | 44 | test "calling procs on fields": 45 | let foo = cascade Foo(): 46 | list.add "one" 47 | list.add "two" 48 | 49 | check foo == Foo(list: @["one", "two"]) 50 | 51 | test "calling procs on fields using named parameters": 52 | let foo = cascade Foo(): 53 | setStr(suffix = "!", val = "world") 54 | 55 | check foo == Foo(str: "hello world!") 56 | 57 | test "nested field assignment": 58 | let foo = cascade Foo(): 59 | bar.n = 100 60 | 61 | check foo == Foo(bar: Bar(n: 100)) 62 | 63 | test "nested cascades": 64 | let foo = cascade Foo(): 65 | bar = cascade Bar(): 66 | n = 6 67 | baz = cascade Baz(): 68 | qux = cascade Qux(): 69 | ns = @[1, 2, 3, 4] 70 | 71 | let expected = Foo( 72 | bar: Bar( 73 | n: 6, 74 | baz: Baz( 75 | qux: Qux( 76 | ns: @[1, 2, 3, 4] 77 | ) 78 | ) 79 | ) 80 | ) 81 | 82 | check foo == expected 83 | 84 | test "deeply nested proc calls": 85 | let foo = cascade Foo(): 86 | bar.baz.qux.ns.add 1 87 | 88 | let expected = Foo( 89 | bar: Bar( 90 | baz: Baz( 91 | qux: Qux( 92 | ns: @[1] 93 | ) 94 | ) 95 | ) 96 | ) 97 | 98 | check foo == expected 99 | 100 | test "bodies passed to calls are not modified": 101 | var condition = false 102 | let foo = cascade Foo(): 103 | blk: 104 | condition = true 105 | 106 | check condition 107 | 108 | test "handles `if` conditionals": 109 | let foo = cascade Foo(): 110 | if true: 111 | str = "yes" 112 | else: 113 | str = "no" 114 | 115 | check foo == Foo(str: "yes") 116 | 117 | test "recursively handles `if` conditionals": 118 | let foo = cascade Foo(): 119 | if true: 120 | if false: 121 | str = "yes" 122 | else: 123 | str = "yes, but no" 124 | else: 125 | str = "no" 126 | 127 | check foo == Foo(str: "yes, but no") 128 | 129 | test "handles `when` conditionals": 130 | let foo = cascade Foo(): 131 | when true: 132 | str = "yes" 133 | else: 134 | str = "no" 135 | 136 | check foo == Foo(str: "yes") 137 | 138 | test "recursively handles `when` conditionals": 139 | let foo = cascade Foo(): 140 | when true: 141 | when false: 142 | str = "yes" 143 | else: 144 | str = "yes, but no" 145 | else: 146 | str = "no" 147 | 148 | check foo == Foo(str: "yes, but no") 149 | 150 | test "GUI button example": 151 | let btn = cascade Button(): 152 | text = "ok" 153 | width = 30 154 | color = "#13a89e" 155 | enable() 156 | 157 | var res = Button() 158 | res.text = "ok" 159 | res.width = 30 160 | res.color = "#13a89e" 161 | res.enable() 162 | 163 | check btn == res 164 | -------------------------------------------------------------------------------- /tests/tests.nim.cfg: -------------------------------------------------------------------------------- 1 | path: "$project_path/../" 2 | hints: off 3 | --------------------------------------------------------------------------------