├── .gitignore ├── variant.nimble ├── .github └── workflows │ └── test.yml ├── README.md ├── LICENSE └── variant.nim /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache/ 2 | -------------------------------------------------------------------------------- /variant.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.3.1" 4 | author = "Yuriy Glukhov" 5 | description = "Variant type and type matching" 6 | license = "MIT" 7 | 8 | task test, "Run tests": 9 | exec "nim c -r --mm:refc variant" 10 | exec "nim c -r --mm:refc -d:variantDebugTypes variant" 11 | exec "nim c -r --mm:orc variant" 12 | exec "nim c -r --mm:orc -d:variantDebugTypes variant" 13 | exec "nim js -r variant" 14 | exec "nim js -r -d:variantDebugTypes variant" 15 | exec "nim cpp -r variant" 16 | exec "nim cpp -r -d:variantDebugTypes variant" 17 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: push 4 | jobs: 5 | Test: 6 | if: | 7 | !contains(github.event.head_commit.message, '[skip ci]') 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | os: [ubuntu-latest] 12 | nim-channel: [stable, devel] 13 | 14 | name: ${{ matrix.os }}-${{ matrix.nim-channel }} 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Setup nim 20 | uses: jiro4989/setup-nim-action@v1 21 | with: 22 | nim-version: ${{ matrix.nim-channel }} 23 | 24 | - name: Test 25 | shell: bash 26 | run: | 27 | nim --version 28 | nimble test 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # variant [![Build Status](https://github.com/yglukhov/variant/workflows/CI/badge.svg?branch=master)](https://github.com/yglukhov/variant/actions?query=branch%3Amaster) [![nimble](https://img.shields.io/badge/nimble-black?logo=nim&style=flat&labelColor=171921&color=%23f3d400)](https://nimble.directory/pkg/variant) 2 | Variant type and type matching for Nim 3 | 4 | ```nim 5 | import variant 6 | 7 | var v = newVariant(5) 8 | assert v.ofType(int) 9 | assert v.get(int) == 5 10 | 11 | v = newVariant(3.0) 12 | assert v.ofType(float) 13 | assert v.get(float) == 3.0 14 | 15 | v = newVariant(@[1, 2, 3]) 16 | assert v.ofType(seq[int]) 17 | assert v.get(seq[int])[1] == 2 18 | ``` 19 | 20 | Matching: 21 | ```nim 22 | var v = newVariant(@[1, 2, 3]) 23 | assert v.ofType(seq[int]) 24 | variantMatch case v as u 25 | of int: 26 | echo "u is int: ", u 27 | of seq[int]: 28 | echo "u is seq[int]: ", u 29 | else: 30 | echo "dont know what v is" 31 | ``` 32 | Will output: 33 | ``` 34 | u is seq[int]: @[1, 2, 3] 35 | ``` 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Yuriy Glukhov 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 | 23 | -------------------------------------------------------------------------------- /variant.nim: -------------------------------------------------------------------------------- 1 | import macros 2 | 3 | type TypeId* = int 4 | 5 | var counter {.compileTime.} = 0 6 | 7 | proc nextTypeId(): int {.compileTime.} = 8 | inc counter 9 | counter 10 | 11 | proc typeIdAux[t](): int = 12 | var a {.global.} = nextTypeId() 13 | a 14 | 15 | proc getTypeId*(t: typedesc): TypeId = 16 | return static(typeIdAux[t]()) 17 | 18 | const debugVariantTypes = defined(variantDebugTypes) 19 | 20 | type 21 | Variant* {.inheritable.} = ref object 22 | typeId*: TypeId 23 | when debugVariantTypes: 24 | mangledName*: string 25 | VariantConcrete[T] = ref object of Variant 26 | val: T 27 | 28 | proc getProcTy(t: NimNode): NimNode = 29 | var t = getTypeInst(t) 30 | while true: 31 | if t.kind == nnkBracketExpr and $t[0] == "typeDesc" and t[1].kind == nnkProcTy: 32 | t = t[1] 33 | elif t.kind == nnkBracketExpr and $t[0] == "typeDesc": 34 | t = getTypeInst(t[1]) 35 | elif t.kind == nnkSym: 36 | t = getTypeImpl(t) 37 | elif t.kind == nnkProcTy: 38 | return t 39 | else: 40 | echo treeRepr(t) 41 | assert(false) 42 | 43 | iterator combinations[T](s: openArray[T], k: int): seq[T] = 44 | let n = len(s) 45 | assert k >= 0 and k <= n 46 | var pos = newSeq[int](k) 47 | var current = newSeq[T](k) 48 | for i in 0..k-1: 49 | pos[k-i-1] = i 50 | var done = false 51 | while not done: 52 | for i in 0..k-1: 53 | current[i] = s[pos[k-i-1]] 54 | yield current 55 | var i = 0 56 | while i < k: 57 | pos[i] += 1 58 | if pos[i] < n-i: 59 | for j in 0..i-1: 60 | pos[j] = pos[i] + i - j 61 | break 62 | i += 1 63 | if i >= k: 64 | break 65 | 66 | iterator allCombinations[T](s: openArray[T]): seq[T] = 67 | for i in 1 .. s.len: 68 | for c in combinations(s, i): 69 | yield c 70 | 71 | proc getProcAttrs(ti: NimNode): tuple[isgcsafe, isclosure, isnosideeffect: bool] = 72 | var isnimcall = false 73 | for p in ti[1]: 74 | if p.kind == nnkIdent: 75 | case $p 76 | of "gcsafe": result.isgcsafe = true 77 | of "closure": result.isclosure = true 78 | of "noSideEffect": result.isnosideeffect = true 79 | of "nimcall": isnimcall = true 80 | else: discard 81 | 82 | if result.isclosure and isnimcall: 83 | assert(false, "proc cannot be closure and nimcall at the same time") 84 | if not isnimcall: 85 | result.isclosure = true 86 | 87 | proc applyPragma(ti: NimNode, pr: string) = 88 | if ti[1].kind == nnkEmpty: ti[1] = newNimNode(nnkPragma) 89 | var prs = ti[1] 90 | if pr == "nimcall": 91 | # make sure there's no closure pragma 92 | for i in 0 ..< prs.len: 93 | if prs[i].kind == nnkIdent and $prs[i] == "closure": 94 | prs.del(i) 95 | prs.add(ident(pr)) 96 | 97 | proc procUpcastPermutations(ti: NimNode): seq[NimNode] = 98 | let attrs = getProcAttrs(ti) 99 | var attrA = newSeq[string]() 100 | if attrs.isclosure: attrA.add("nimcall") 101 | if not attrs.isnosideeffect: attrA.add("noSideEffect") 102 | if not attrs.isgcsafe: attrA.add("gcsafe") 103 | result.add(newCall("typeof", ti)) 104 | for c in allCombinations(attrA): 105 | let tic = copyNimTree(ti) 106 | for i in c: 107 | applyPragma(tic, i) 108 | result.add(newCall("typeof", tic)) 109 | 110 | macro procTypeIdArray(t: typedesc[proc]): untyped = 111 | result = newNimNode(nnkBracket) 112 | let getTypeId = bindSym"getTypeId" 113 | for p in procUpcastPermutations(getProcTy(t)): 114 | result.add(newCall(getTypeId, p)) 115 | 116 | macro procTypeIdSwitchStmt(subj: untyped, t: typedesc[proc]): untyped = 117 | result = newNimNode(nnkCaseStmt) 118 | result.add(subj) 119 | let getTypeId = bindSym"getTypeId" 120 | for p in procUpcastPermutations(getProcTy(t)): 121 | let ofBody = quote do: 122 | return cast[VariantConcrete[`p`]](v).val 123 | 124 | result.add(newTree(nnkOfBranch, 125 | newCall(getTypeId, newCall("typeof", p)), ofBody)) 126 | 127 | result.add(newTree(nnkElse, newTree(nnkDiscardStmt, newEmptyNode()))) 128 | 129 | template getMangledName(t: typedesc): string = $t 130 | 131 | proc ofType*(v: Variant, t: typedesc): bool {.inline.} = 132 | if not v.isNil: 133 | when t is (proc): 134 | return v.typeId in procTypeIdArray(t) 135 | else: 136 | return v.typeId == getTypeId(t) 137 | 138 | proc newVariant*(): Variant {.inline.} = discard 139 | 140 | proc newVariant*[T](val: T): Variant = 141 | result = VariantConcrete[T](val: val, typeId: getTypeId(T)) 142 | when debugVariantTypes: 143 | result.mangledName = getMangledName(T) 144 | 145 | proc isEmpty*(v: Variant): bool = 146 | result = v == nil or v.typeId == 0 147 | 148 | proc get*(v: Variant, T: typedesc): T = 149 | if v.isNil: 150 | raise newException(Exception, "Wrong variant type: " & "nil" & ". Expected type: " & getMangledName(T)) 151 | 152 | when T is (proc): 153 | procTypeIdSwitchStmt(v.typeId, T) 154 | else: 155 | if getTypeId(T) == v.typeId: 156 | return cast[VariantConcrete[T]](v).val 157 | 158 | when debugVariantTypes: 159 | raise newException(Exception, "Wrong variant type: " & v.mangledName & ". Expected type: " & getMangledName(T)) 160 | else: 161 | raise newException(Exception, "Wrong variant type. Compile with -d:variantDebugTypes switch to get more type information.") 162 | 163 | proc getTn(v: Variant): TypeId {.inline.} = 164 | if v.isNil: TypeId(0) 165 | else: v.typeId 166 | 167 | template matchConst(a: typed): untyped = 168 | when a is (proc): procTypeIdArray(a) 169 | else: getTypeId(a) 170 | 171 | macro variantMatch*(body: untyped): untyped = 172 | expectKind(body, nnkCaseStmt) 173 | var defaultUnpackSym : NimNode 174 | var variantNode = body[0] 175 | if body[0].kind == nnkInfix and $body[0][0] == "as": 176 | variantNode = body[0][1] 177 | defaultUnpackSym = body[0][2] 178 | 179 | result = newNimNode(nnkCaseStmt) 180 | result.add(newCall(bindSym "getTn", variantNode)) 181 | 182 | for i in 1 ..< body.len: 183 | let c = body[i] 184 | case c.kind 185 | of nnkOfBranch: 186 | expectLen(c, 2) 187 | var unpackSym = defaultUnpackSym 188 | var typeNode = c[0] 189 | if c[0].kind == nnkInfix and $c[0][0] == "as": 190 | typeNode = c[0][1] 191 | unpackSym = c[0][2] 192 | expectKind(unpackSym, nnkIdent) 193 | 194 | result.add(newNimNode(nnkOfBranch).add( 195 | newCall(bindsym"matchConst", typeNode), 196 | newStmtList( 197 | newLetStmt(unpackSym, newCall(bindSym "get", variantNode, typeNode)), 198 | c[1])) 199 | ) 200 | of nnkElse: 201 | result.add(c) 202 | else: 203 | error "Unexpected node type in variant case: " & c.lineinfo 204 | 205 | when isMainModule: 206 | import unittest 207 | 208 | type RefObj = ref object 209 | a: int 210 | 211 | type SomeEnum* = enum 212 | someVal1 213 | someVal2 214 | 215 | type SomeProcType = proc(a: int) 216 | 217 | suite "Variant": # Test mangling 218 | test "Common": 219 | var v = newVariant(5) 220 | check v.ofType(int) 221 | check v.get(int) == 5 222 | when debugVariantTypes: 223 | check v.mangledName == "int" 224 | v = newVariant(3.0) 225 | check v.ofType(float) 226 | check v.get(float) == 3.0 227 | when debugVariantTypes: 228 | check v.mangledName == "float64" 229 | v = newVariant(@[1, 2, 3]) 230 | check v.ofType(seq[int]) 231 | check v.get(seq[int])[1] == 2 232 | when debugVariantTypes: 233 | check v.mangledName == "seq[int]" 234 | 235 | v = newVariant(RefObj.new()) 236 | when debugVariantTypes: 237 | check v.mangledName == getMangledName(RefObj) 238 | 239 | test "Match": 240 | var v = newVariant(@[1, 2, 3]) 241 | check v.ofType(seq[int]) 242 | variantMatch case v: 243 | of int as i: check(false and i == 0) 244 | of seq[int] as s: check s[1] == 2 245 | of SomeProcType as p: p(0) 246 | else: check false 247 | 248 | variantMatch case v as u 249 | of int: check(false and u == 0) 250 | of seq[int]: check(u[1] == 2) 251 | else: fail() 252 | 253 | v = newVariant(5.3) 254 | check v.ofType(float) 255 | variantMatch case v: 256 | of int as i: check(false and i == 0) 257 | of float as f: check f == 5.3 258 | else: fail() 259 | 260 | test "Generic types": 261 | type SomeGeneric[T] = tuple[a: T] 262 | var sng : SomeGeneric[int] 263 | sng.a = 5 264 | let v = newVariant(sng) 265 | check(v.get(type(sng)).a == 5) 266 | 267 | test "Closures": 268 | proc foon(b: int): int = b + 5 269 | proc fooc(b: int): int {.closure.} = b + 6 270 | var v = newVariant(foon) 271 | check(v.get(proc(b: int): int)(6) == 11) 272 | v = newVariant(fooc) 273 | check(v.get(proc(b: int): int {.gcsafe, noSideEffect.})(6) == 12) 274 | 275 | test "Char": 276 | let v = newVariant('a') 277 | check(v.get(char) == 'a') 278 | --------------------------------------------------------------------------------