├── tests ├── config.nims ├── timport.nim ├── tsingleinvoke.nim ├── tmulti.nim ├── toptions.nim ├── tnotuples.nim ├── ttree.nim ├── tbasic.nim ├── taliasissue.nim └── tgenerics.nim ├── .gitignore ├── fungus.nimble ├── .github └── workflows │ └── test.yml ├── LICENSE ├── README.md └── src └── fungus.nim /tests/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !*.* 3 | !*/ 4 | *.nim.kate-swp 5 | nimcache/ 6 | nimblecache/ 7 | htmldocs/ 8 | -------------------------------------------------------------------------------- /fungus.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.19" 4 | author = "Jason Beetham" 5 | description = "A new awesome nimble package" 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | 10 | # Dependencies 11 | 12 | requires "nim >= 1.9.1" 13 | requires "micros >= 0.1.10" 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/timport.nim: -------------------------------------------------------------------------------- 1 | import tbasic 2 | import fungus 3 | 4 | let a: Shape = Circle.init(0, 0, 1) 5 | 6 | match a: 7 | of Circle as a: 8 | echo $a 9 | of Line: 10 | discard 11 | else: 12 | discard 13 | 14 | 15 | if (myVal: Circle) from a: 16 | discard 17 | elif (var myVal: Line) from a: 18 | discard 19 | -------------------------------------------------------------------------------- /tests/tsingleinvoke.nim: -------------------------------------------------------------------------------- 1 | import fungus 2 | adtEnum(Letter): 3 | A 4 | B 5 | C 6 | D 7 | 8 | var invokes = 0 9 | 10 | proc choose(): Letter = 11 | inc invokes 12 | Letter D.init() 13 | 14 | match choose(): 15 | of A: discard 16 | of B: discard 17 | of C: discard 18 | of D: discard 19 | 20 | assert invokes == 1 21 | -------------------------------------------------------------------------------- /tests/tmulti.nim: -------------------------------------------------------------------------------- 1 | import fungus 2 | adtEnum(Shape): 3 | None 4 | Circle: tuple[x, y, r: int] 5 | Rectangle: tuple[x, y, w, h: int] 6 | Line: tuple[x1, y1, x2, y2: int] 7 | 8 | var a = Shape Circle.init(10, 10, 100) 9 | 10 | match a: 11 | of Circle as shape, Rectangle as shape: 12 | echo shape.x, " ", shape.y 13 | of Line, None: 14 | echo "A different shape" 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - '**' 8 | 9 | env: 10 | nim-version: 'stable' 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - uses: jiro4989/setup-nim-action@v1 20 | with: 21 | nim-version: ${{ env.nim-version }} 22 | repo-token: ${{ secrets.GITHUB_TOKEN }} 23 | 24 | - run: nimble install -Y 25 | 26 | - name: Run Tests 27 | run: nimble test -------------------------------------------------------------------------------- /tests/toptions.nim: -------------------------------------------------------------------------------- 1 | import fungus 2 | import std/unittest 3 | 4 | 5 | adtEnum(Option[T]): 6 | None 7 | Some: tuple[val: T] 8 | 9 | proc isSome[T](opt: Option[T]): bool = 10 | match opt: 11 | of Some: 12 | true 13 | else: 14 | false 15 | 16 | proc isNone[T](opt: Option[T]): bool = not opt.isSome 17 | 18 | proc unsafeGet*[T](opt: Option[T]): T = opt.ValueData.val 19 | 20 | var a = Option[int] Some.init(100) 21 | check a.isSome() 22 | 23 | match a: 24 | of Some as (val, ): 25 | check val == 100 26 | else: 27 | check false 28 | 29 | a = Option[int] None[int].init() 30 | check a.isNone() 31 | -------------------------------------------------------------------------------- /tests/tnotuples.nim: -------------------------------------------------------------------------------- 1 | import fungus 2 | adtEnum(Data): 3 | None 4 | String: string 5 | Int: int 6 | Float: float 7 | Arr: array[3, int] 8 | Vector2: tuple[x, y: float32] 9 | 10 | 11 | var a = Data Float.init(30d) 12 | echo a.to(Float) 13 | a = Vector2.init(100, 200) 14 | echo a.to(Vector2) 15 | 16 | proc doThing(f: float) = discard 17 | 18 | match a: 19 | of Vector2 as (x, y): 20 | echo x, " ", y 21 | of Float as mut x: 22 | x.toInternal += 0.3 # Need `.toInternal` for some cases 23 | doThing(x) 24 | else: discard 25 | 26 | a = Int.init(100) 27 | 28 | if (myInt: Int) from a: 29 | echo myInt 30 | -------------------------------------------------------------------------------- /tests/ttree.nim: -------------------------------------------------------------------------------- 1 | import fungus 2 | 3 | adtEnum(Tree): 4 | Branch: tuple[branches: seq[Tree]] 5 | Leaf: tuple[value: string] 6 | 7 | proc toTree(collection: seq[string]): Tree = 8 | if collection.len == 1: 9 | result = Tree Leaf.init("") 10 | discard (var result: Leaf) from result 11 | result.value.add "." 12 | 13 | else: 14 | result = Tree Branch.init(@[]) 15 | discard (var result: Branch) from result 16 | result.branches.add collection[0.. 1: 35 | result.setLen(result.len - 2) 36 | result.add "]" 37 | 38 | var myList = LinkedList[int] newNode(10) 39 | myList = myList.prepend(11) 40 | myList = myList.prepend(12) 41 | myList = myList.prepend(13) 42 | check $myList == "[13, 12, 11, 10]" 43 | check myList.len == 4 44 | 45 | var myList2 = LinkedList[string] newNode("world") 46 | myList2 = myList2.prepend "cruel" 47 | myList2 = myList2.prepend "hello" 48 | 49 | check $myList2 == "[hello, cruel, world]" 50 | check myList2.len == 3 51 | 52 | check adtEqual(Nil[int].init(), (Nil[int].init())) 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fungus 2 | 3 | Rust-like tuple enums. 4 | Give me a macro and a fulcrum to rest it on and I'll likely make an obnoxious macro. 5 | 6 | ## What does it do? 7 | 8 | Does what it says on the tin... generates Rust-like ADT enums, those weird ones with fields. 9 | Consider the following idiomatic Nim code: 10 | ```nim 11 | type 12 | ShapeKind = enum 13 | NoneKind, CircleKind, RectangleKind, LineKind 14 | 15 | Shape = object 16 | case kind: ShapeKind 17 | of LineKind: 18 | lx1, ly1, lx2, ly2: int 19 | of RectangleKind: 20 | rx, ry, rw, rh: int 21 | of CircleKind: 22 | cRadius, cx, cy: int 23 | of NoneKind: 24 | discard 25 | ``` 26 | 27 | This is a bit messy due to rightly not being able to share field names(Nim variants are great for specific things, for other things they are a bit in the way). 28 | Other languages get around it doing something similar to what this library does, statically enforcing unpacking each kind. 29 | What one writes with fungus instead is: 30 | ```nim 31 | import fungus 32 | adtEnum(Shape): 33 | None 34 | Circle: tuple[x, y, r: int] 35 | Rectangle: tuple[x, y, w, h: int] 36 | Line: tuple[x1, y1, x2, y2: int] 37 | ``` 38 | 39 | This generates a static type per each branch, and also a bunch of accessor procedures. 40 | The type definitions look like: 41 | 42 | ```nim 43 | type 44 | ShapeKind = enum 45 | NoneKind, CircleKind, RectangleKind, LineKind 46 | Shape = object 47 | case kind: ShapeKind 48 | of LineKind: 49 | LineData: 50 | tuple[x1, y1, x2, y2: int] 51 | of RectangleKind: 52 | RectangleData: 53 | tuple[x, y, w, h: int] 54 | of CircleKind: 55 | CircleData: 56 | tuple[x, y, r: int] 57 | of NoneKind: 58 | nil 59 | 60 | None = distinct Shape 61 | Circle = distinct Shape 62 | Rectangle = distinct Shape 63 | Line = distinct Shape 64 | ``` 65 | 66 | Which means the following is possible: 67 | ```nim 68 | var a = Shape Circle.init(10, 10, 100) 69 | a.to(Circle).r = 300 70 | echo a.to(Circle) 71 | a = Shape Line.init(0, 0, 1, 1) 72 | ``` 73 | 74 | This doesnt solve much altogether, which is what the `match` and `from` macros are for. 75 | 76 | ```nim 77 | if (var myVar: Line) from a: # notice `var` this is a mutable match 78 | inc myVar.x1 79 | echo myVar 80 | elif (myOtherVar: Circle) from a: 81 | echo myOtherVar 82 | ``` 83 | 84 | What this macro expands into is `a.kind == CircleKind and (let myOtherVar = Circle(a); true)`. 85 | This allows you to work similarly to Rust's `if let` construct, but this composes better(it should not bug out as easily :P ). 86 | Now to do some primitive pattern matching! 87 | 88 | ```nim 89 | match a: 90 | of Circle as mut circ: 91 | circ.r = 1000 92 | echo circ 93 | of Rectangle as rect: 94 | echo rect 95 | of Line as (mut x1, _, x2, _): 96 | inc x1 97 | echo a.to(Line) 98 | else: discard 99 | ``` 100 | 101 | The `match` macro is very very basic pattern matching. 102 | It is not too disimilar to the `from` macro. 103 | As one can see though it allows a `as mut name` to introduce a mutable name to the matching variable. 104 | There is also the `as name` which is an immutable variable match. 105 | Finally there is a `tuple` match which is `(x, y, z, w)`, much like the other match `mut` can prefix a name to indicate it is a mutable match. 106 | Tuple unpacking works just like normally for this matching, although one can match as few positional fields as they want. 107 | 108 | ### Non tuple types 109 | 110 | ```nim 111 | import fungus 112 | adtEnum(Data): 113 | None 114 | String: string 115 | Int: int 116 | Float: float 117 | Arr: array[3, int] 118 | Vector2: tuple[x, y: float32] 119 | ``` 120 | 121 | Is valid code, there exists `toInternal` converts to implicitly convert, but some operations will require explicit invocation. 122 | For example: 123 | ```nim 124 | proc doThing(f: float) = discard 125 | match a: 126 | of Vector2 as (x, y): 127 | echo x, " ", y 128 | of Float as mut x: 129 | x.toInternal += 0.3 # Need `.toInternal` for some cases 130 | doThing(x) 131 | else: discard 132 | ``` 133 | 134 | ### Generics?! 135 | Yes this also supports some subset of generics. 136 | 137 | ```nim 138 | import fungus 139 | adtEnum(LinkedList[T]): 140 | Nil 141 | Node: tuple[n: ref LinkedList[T], val: T] 142 | 143 | proc newNode[T](val: T): Node[T] = 144 | result = Node[T].init((ref LinkedList[T])(), val) 145 | result.n[] = LinkedList[T] Nil[T].init() 146 | 147 | proc prepend[T](node: LinkedList[T], val: T): LinkedList[T] = 148 | result = LinkedList[T] Node[T].init(new LinkedList[T], val) 149 | Node[T](result).n[] = node 150 | 151 | proc len[T](list: LinkedList[T]): int = 152 | var theList = list 153 | while (node: Node) from theList: 154 | inc result 155 | theList = node.n[] 156 | 157 | proc `$`[T](list: LinkedList[T]): string = 158 | var theList = list 159 | result = "[" 160 | while (node: Node) from theList: 161 | result.add $node.val 162 | result.add ", " 163 | theList = node.n[] 164 | 165 | if result.len > 1: 166 | result.setLen(result.len - 2) 167 | result.add "]" 168 | 169 | var myList = LinkedList[int] newNode(10) 170 | myList = myList.prepend(11) 171 | myList = myList.prepend(12) 172 | myList = myList.prepend(13) 173 | echo myList 174 | echo myList.len 175 | 176 | var myList2 = LinkedList[string] newNode("world") 177 | myList2 = myList2.prepend "cruel" 178 | myList2 = myList2.prepend "hello" 179 | echo myList2 180 | echo myList2.len 181 | ``` 182 | 183 | The above *just works*(thanks to the Rust book for a small example of linked lists using an ADT). 184 | 185 | 186 | ## Implementing custom `==` 187 | 188 | Fungus has a `adtEqual` procedure which can be used to invoke default comparisons for a type. 189 | This means kind is checked then the tuples are compared. 190 | An example for thel linked list looks like: 191 | ```nim 192 | proc `==`[T](a, b: LinkedList[T]): bool = adtEqual(a, b) 193 | ``` 194 | 195 | ## Implementing custom `$` 196 | 197 | Fungus has a `$` procedure defined as a generic, which means you can overload it for your own type. 198 | Otherwise it should call this one. 199 | ```nim 200 | proc `$`[T](nl: Nil[T]): string = "nil" 201 | ``` 202 | 203 | 204 | ## Optional fields 205 | 206 | One can use `tuple[x: int = 3]` to indicate an optional field. 207 | -------------------------------------------------------------------------------- /src/fungus.nim: -------------------------------------------------------------------------------- 1 | import std/[macros, genasts, strutils, macrocache, decls, sets, typetraits, strformat] 2 | import pkg/micros 3 | 4 | 5 | const adtTable = CacheTable"FungusTable" 6 | 7 | proc hashName(n: NimNode): string = 8 | if n.kind == nnkBracketExpr: 9 | n[0].signatureHash 10 | else: 11 | n.signatureHash 12 | 13 | macro isAdt(t: typed): untyped = 14 | newLit(t.getTypeInst.hashName() in adtTable) 15 | 16 | type 17 | ADTBase* = concept t 18 | isAdt(t) 19 | AdtChild* = concept t 20 | isAdt(distinctBase(t)) 21 | not isAdt(t) 22 | 23 | macro adtChildStrImpl(val: typed, base: typedesc): untyped = 24 | let 25 | typ = val.getTypeInst() 26 | baseTyp = base.getTypeImpl[^1] 27 | table = adtTable[baseTyp.hashName] 28 | 29 | for i, x in table[2]: 30 | if x.eqIdent(typ): 31 | if table[3][i].kind == nnkEmpty: 32 | result = newLit(typ.repr & "()") 33 | else: 34 | result = genast(typ, val, baseTyp, res = ident"result", fieldName = table[3][i]): 35 | res = astToStr(typ) 36 | 37 | when typeof(val.baseTyp.fieldName) isnot tuple: 38 | res.add "(" 39 | res.add $val.baseTyp.fieldName 40 | when typeof(val.baseTyp.fieldName) isnot tuple: 41 | res.add ")" 42 | 43 | macro adtEqImpl(a, b: typed): untyped = 44 | let 45 | typ = a.getTypeInst() 46 | table = adtTable[typ.hashName] 47 | result = newStmtList() 48 | result.add: 49 | genast(a, b): 50 | if a.kind != b.kind: 51 | return false 52 | let caseStmt = caseStmt(NimName nnkDotExpr.newTree(a, ident"kind")) 53 | for i, x in table[1]: 54 | if table[3][i].kind == nnkEmpty: 55 | caseStmt.add ofBranch(x, newLit(true)) 56 | else: 57 | caseStmt.add: 58 | ofBranch(x): 59 | genast(a, b, fieldName = table[3][i]): 60 | a.fieldName == b.fieldName 61 | result.add NimNode caseStmt 62 | 63 | proc `$`*[T: AdtChild](adtChild: T): string = 64 | ## `$` for ADT subtypes, generic to allow overloading specifically 65 | adtChildStrImpl(adtChild, distinctBase(T)) 66 | 67 | 68 | proc adtEqual*[T: AdtBase](a, b: T): bool = 69 | ## Base implementation for implementing automatic `==` operators 70 | adtEqImpl(a, b) 71 | 72 | proc adtEqual*[T: AdtChild](a, b: T): bool = 73 | ## Base implementation for implementing automatic `==` operators 74 | adtEqual(distinctBase(a), distinctBase(b)) 75 | 76 | type FungusConvDefect = object of Defect 77 | 78 | macro subscribeAdt(name: typed, enumFields, typeNames, dataNames: untyped) = 79 | adtTable[name.hashName] = newStmtList(name, enumFields, typenames, dataNames) 80 | 81 | when defined(fungusExportEnumImpl): 82 | proc adtEnumImpl*(origName, body: NimNode): NimNode 83 | else: 84 | proc adtEnumImpl(origName, body: NimNode): NimNode 85 | 86 | proc adtEnumImpl(origName, body: NimNode): NimNode = 87 | var typeNames, enumFields, addons, dataNames: seq[NimNode] 88 | let 89 | origNameInfo = origName.lineInfoObj 90 | name = 91 | if origName.kind == nnkBracketExpr: 92 | origName[0] 93 | else: 94 | origName 95 | 96 | caseDef = caseStmt(NimName postfix(ident"kind", "*")) 97 | genericParams = 98 | if origName.kind == nnkBracketExpr: 99 | origName[1..^1] 100 | else: 101 | @[newEmptyNode()] 102 | instantiatedType = 103 | if origName.kind == nnkBracketExpr: 104 | let expr = nnkBracketExpr.newTree(origName[0]) 105 | for param in genericParams: 106 | if param.kind == nnkIdent: 107 | expr.add param 108 | elif param.kind == nnkExprColonExpr: 109 | expr.add param[0] 110 | expr 111 | else: 112 | origName 113 | 114 | for entry in body: 115 | case entry.kind 116 | of nnkIdent: 117 | typeNames.add entry 118 | dataNames.add newEmptyNode() 119 | let enumName = ident($entry & "Kind") 120 | enumFields.add NimNode enumField(enumName) 121 | caseDef.add ofBranch(enumName, newNilLit()) 122 | 123 | let 124 | typ = 125 | if origName.kind == nnkBracketExpr: 126 | let theExpr = copyNimTree(instantiatedType) 127 | theExpr[0] = entry 128 | theExpr 129 | else: 130 | entry 131 | 132 | addons.add: 133 | genAst( 134 | name = instantiatedType, 135 | enumName, 136 | typ, 137 | procName = ident("to" & $entry), 138 | res = ident"result" 139 | ): 140 | 141 | proc to*(val: name, _: typedesc[typ]): lent typ = 142 | if val.kind != enumName: 143 | raise (ref FungusConvDefect)(msg: "Cannot convert '$#' to '$#'." % [$val.kind, $enumName]) 144 | typ name(val) 145 | 146 | proc init*(_: typedesc[typ]): typ = typ name(kind: enumName) 147 | 148 | of nnkCall, nnkCommand: 149 | if entry.len != 2: 150 | error("Invalid entry expected `name: type`", entry) 151 | typeNames.add entry[0] 152 | let 153 | enumName = ident($entry[0] & "Kind") 154 | dataName = ident(entry[0].repr & "Data") 155 | typ = 156 | if origName.kind == nnkBracketExpr: 157 | let theExpr = copyNimTree(instantiatedType) 158 | theExpr[0] = entry[0] 159 | theExpr 160 | else: 161 | entry[0] 162 | 163 | dataNames.add dataName 164 | 165 | enumFields.add NimNode enumField(enumName) 166 | caseDef.add ofBranch(enumName, NimNode identDef(NimName postfix(dataName, "*"), typ = entry[1])) 167 | addons.add: 168 | genAst( 169 | name, 170 | enumName, 171 | dataName, 172 | typ, 173 | tupl = entry[1], 174 | procName = ident("to" & $entry[0]), 175 | res = ident"result", 176 | instTyp = instantiatedType 177 | ): 178 | converter `to name`*(arg: typ): instTyp = instTyp(arg) 179 | converter `to name`*(arg: var typ): var instTyp = instTyp(arg) 180 | 181 | proc to*(val: instTyp, _: typedesc[typ]): lent typ = 182 | if val.kind != enumName: 183 | raise (ref FungusConvDefect)(msg: "Cannot convert '$#' to '$#'." % [$val.kind, $enumName]) 184 | typ instTyp(val) 185 | 186 | proc to*(val: var instTyp, _: typedesc[typ]): var typ = 187 | if val.kind != enumName: 188 | raise (ref FungusConvDefect)(msg: "Cannot convert '$#' to '$#'." % [$val.kind, $enumName]) 189 | typ instTyp(val) 190 | 191 | 192 | let 193 | initProc = routineNode(NimName postfix(ident "init", "*")) 194 | tupleConstr = nnkTupleConstr.newTree() 195 | initProc.addParam identDef(NimName ident"_", makeTypeDesc typ) 196 | initProc.returnType = typ 197 | if entry[1][0].kind == nnkTupleTy: 198 | # Tuples emit field accessors 199 | for iDef in entry[1][0]: 200 | let fieldTyp = iDef[^2] 201 | for field in iDef[0..^3]: 202 | 203 | let 204 | fieldLineInfo = field.lineInfoObj() 205 | fieldAccess = 206 | genast(field, typ, fieldTyp, name, dataName, instTyp = instantiatedType): 207 | proc field*(val: typ): lent fieldTyp = instTyp(val).dataName.field 208 | proc field*(val: var typ): var fieldTyp = instTyp(val).dataName.field 209 | proc `field=`*(val: var typ, newVal: fieldTyp) = instTyp(val).dataName.field = newVal 210 | for n in fieldAccess: 211 | n[0][1].setLineInfo(fieldLineInfo) 212 | addons.add fieldAccess 213 | 214 | for val in entry[1][0]: 215 | for name in val[0..^3]: 216 | initProc.addParam identDef newIdentDefs(name, val[^2], val[^1]) 217 | tupleConstr.add name 218 | 219 | initProc.addToBody: 220 | genast(enumName, dataName, tupleConstr, typ, instTyp = instantiatedType): 221 | typ instTyp(kind: enumName, dataName: tupleConstr) 222 | else: 223 | # Handle the case of `Arr: array[3, int]`s special code 224 | initProc.addParam(identDef(NimName ident"param", entry[1])) 225 | initProc.addToBody: 226 | genast(enumName, dataName, tupleConstr, typ, instTyp = instantiatedType, paramName = ident"param"): 227 | typ instTyp(kind: enumName, dataName: paramName) 228 | addons.add: 229 | genast(typ, dataName, realTyp = entry[1], instTyp = instantiatedType): 230 | converter `toInternal`*(val: typ): realTyp = instTyp(val).dataName 231 | converter `toInternal`*(val: var typ): var realTyp = instTyp(val).dataName 232 | 233 | addons[^1].add NimNode initProc 234 | else: 235 | error("Invalid entry, expected either an 'name' or 'name: tuple[...]'.", entry) 236 | 237 | let enumName = ident $name & "Kind" 238 | result = newStmtList(NimNode enumDef(NimName enumName, enumFields, true)) 239 | NimNode(caseDef)[0] = NimNode identDef(NimName NimNode(caseDef)[0], enumName) 240 | let 241 | objDef = objectDef(NimName postFix(name, "*")) 242 | recCase = nnkRecCase.newTree() 243 | 244 | objDef.NimNode[0][1].setLineInfo(origNameInfo) 245 | NimNode(caseDef).copyChildrenTo(recCase) 246 | objDef.recList = nnkRecList.newTree recCase 247 | 248 | if origName.kind == nnkBracketExpr: 249 | # We need to add generic parameters 250 | for param in origName[1..^1]: 251 | case param.kind 252 | of nnkExprColonExpr: 253 | objDef.addGeneric identDef(NimName param[0], param[1]) 254 | of nnkIdent: 255 | objDef.addGeneric identDef(NimName param, ident"auto") # have to use `auto` here otherwise compiler errors 256 | else: 257 | error("Unexpected generic constraint", param) 258 | 259 | result[0].add NimNode objDef 260 | 261 | for i, typeName in typeNames: 262 | 263 | let 264 | lineInfoName = copyNimNode(typeName) 265 | def = 266 | genast(instantiatedType, typeName, field = enumFields[i]): 267 | type typeName* = distinct instantiatedType 268 | 269 | def[0][0][1].copyLineInfo(lineInfoName) 270 | def[0][1] = objDef.genericParamList() 271 | result[0].add def[0] 272 | 273 | for node in addons: 274 | for subNode in node: 275 | if subNode.kind in {nnkProcDef, nnkFuncDef, nnkConverterDef}: 276 | subNode[2] = objDef.genericParamList 277 | 278 | 279 | result.add addons 280 | result.add newCall(bindSym"subscribeAdt", name, 281 | nnkBracket.newTree(enumFields), 282 | nnkBracket.newTree(typeNames), 283 | nnkBracket.newTree(dataNames) 284 | ) 285 | 286 | macro adtEnum*(origName, body: untyped): untyped = adtEnumImpl(origName, body) 287 | 288 | proc getKindAndDataName(data, toLookFor: NimNode): (NimNode, NimNode) = 289 | for i, name in data[2]: 290 | if name.eqIdent(toLookFor): 291 | return (data[1][i], data[3][i]) 292 | error(fmt"Invalid kind '{toLookFor.repr}'." , toLookFor) 293 | 294 | 295 | proc desym(root: NimNode) = 296 | for i, x in root: 297 | case x.kind 298 | of nnkSym: 299 | root[i] = ident $x 300 | of nnkOpenSymChoice, nnkClosedSymChoice: 301 | root[i] = ident $x[0] 302 | else: 303 | desym(x) 304 | 305 | macro match*(val: ADTBase, branches: varargs[untyped]): untyped = 306 | result = nnkIfStmt.newTree() 307 | let 308 | adtData = adtTable[val.getTypeInst.hashName] 309 | valIsNotMut = val.kind != nnkSym or val.symKind != nskVar 310 | branches = branches.copyNimTree() 311 | 312 | branches.desym() 313 | 314 | var implemented: HashSet[string] 315 | 316 | let 317 | origVal = val 318 | val = 319 | if val.kind in nnkCallKinds: 320 | genSym(nskLet) 321 | else: 322 | val 323 | 324 | for branch in branches: 325 | if branch.kind in {nnkElse, nnkElifBranch}: 326 | result.add branch 327 | else: 328 | branch.expectKind(nnkOfBranch) 329 | for condition in branch[0..^2]: 330 | case condition.kind 331 | of nnkInfix: # We're doing a named match 332 | if condition[0].kind != nnkIdent or not condition[0].eqIdent"as": 333 | error("Invalid operation expected 'as'.", condition[0]) 334 | 335 | let (kind, dataName) = getKindAndDataName(adtData, condition[1]) 336 | case condition[^1].kind 337 | of nnkIdent: # emit a `let` 338 | let injection = branch[^1].copyNimTree 339 | injection.insert 0, newLetStmt(condition[^1], newCall("to", val, condition[1])) 340 | result.add nnkElifBranch.newTree( 341 | infix(nnkDotExpr.newTree(val, ident"kind"), "==", kind), 342 | injection) 343 | 344 | of nnkCall, nnkCommand: # Check if it's 'mut', and `val` is mut, emit a template 345 | if not condition[^1][0].eqIdent"mut": 346 | error("Can only make a 'mut' call.", condition[^1][0]) 347 | 348 | if valIsNotMut: 349 | error("Can only make a 'mut' reference to a mutable variable.", val) 350 | 351 | let 352 | name = condition[^1][1] 353 | injection = branch[^1].copyNimTree 354 | nameInfo = name.lineInfoObj 355 | injection.insert 0: 356 | genAst(val, byaddr = bindSym"byaddr", name, destType = condition[1]): 357 | var tmp = to(val, destType).addr 358 | template name: untyped = tmp[] 359 | injection[0][^1][0].setLineInfo(nameInfo) 360 | 361 | result.add nnkElifBranch.newTree( 362 | infix(nnkDotExpr.newTree(val, ident"kind"), "==", kind), 363 | injection) 364 | 365 | 366 | of nnkTupleConstr: # same as a call check if each a param is a `mut`, if soe emit a template per param 367 | let injection = branch[^1].copyNimTree 368 | for i, x in condition[^1]: 369 | case x.kind 370 | of nnkCall, nnkCommand: 371 | if not x[0].eqIdent"mut": 372 | error("Invalid call inside match.", x) 373 | 374 | if valIsNotMut: 375 | error("Can only make a 'mut' reference to a mutable variable.", val) 376 | let nameInfo = x[1].lineInfoObj 377 | injection.insert 0: 378 | genast(val, dataName, name = x[1], index = newLit(i), destType = condition[1], expr = condition.repr): 379 | when val.dataName isnot tuple: 380 | {.error: "attempted to unpack a type that is not a tuple: '" & expr & "'.".} 381 | var tmp = val.dataName[index].addr 382 | template name: untyped = tmp[] 383 | injection[0][^1][0].setLineInfo(nameInfo) 384 | 385 | 386 | of nnkIdent: 387 | if not x.eqIdent"_": 388 | let nameInfo = x.lineInfoObj 389 | injection.insert 0: 390 | genast(val, dataName, name = x, index = newLit(i), destType = condition[1], expr = condition.repr): 391 | when val.dataName isnot tuple: 392 | {.error: "attempted to unpack a type that is not a tuple: '" & expr & "'.".} 393 | let name = val.dataName[index] 394 | injection[0][^1][0][0].setLineInfo(nameInfo) 395 | 396 | else: 397 | error("Invalid capture statement.", x) 398 | result.add nnkElifBranch.newTree( 399 | infix(nnkDotExpr.newTree(val, ident"kind"), "==", kind), 400 | injection) 401 | 402 | else: 403 | error("Invalid alias statement", condition[^1]) 404 | implemented.incl condition[1].repr 405 | 406 | of nnkIdent, nnkSym: # Just a kind match 407 | let (kind, _)= getKindAndDataName(adtData, condition) 408 | result.add nnkElifBranch.newTree( 409 | infix(nnkDotExpr.newTree(val, ident"kind"), "==", kind), 410 | branch[^1]) 411 | implemented.incl condition.repr 412 | else: 413 | error("Invalid branch not doing a match.", condition) 414 | 415 | if result[^1].kind != nnkElse: 416 | var unimplemented: HashSet[string] 417 | for kind in adtData[2]: 418 | let theRepr = kind.repr 419 | if theRepr notin implemented: 420 | unimplemented.incl theRepr 421 | if unimplemented.len > 0: 422 | error("Unhandled type branch for: " & $unimplemented) 423 | 424 | 425 | if origVal.kind in nnkCallKinds: 426 | result = newStmtList(newLetStmt(val, origVal), result) 427 | 428 | proc getDeclInfo(matcher: NimNode): (NimNode, NimNode, bool) = 429 | let matcher = 430 | if matcher.kind in {nnkStmtList, nnkStmtListExpr}: 431 | matcher[0] 432 | else: 433 | matcher 434 | 435 | case matcher.kind 436 | of nnkLetSection, nnkVarSection: 437 | if matcher.len != 1 or matcher[0].len != 3: 438 | error("Too many declared variables.") 439 | let def = matcher[0] 440 | if def[^1].kind != nnkEmpty: 441 | error("No reason to have a value") 442 | 443 | result = (def[0], def[1], matcher.kind == nnkVarSection) 444 | 445 | of nnkTupleConstr: 446 | if matcher.len != 1: 447 | error("Attempting to declare more than one match.", matcher) 448 | 449 | if matcher[0].kind != nnkExprColonExpr: 450 | error("Invalid match declaration expected 'a: type'.", matcher) 451 | 452 | result = (matcher[0][0], matcher[0][1], false) 453 | else: 454 | error("Invalid match, expected '(a: type)' or '(var a: type)'", matcher) 455 | 456 | 457 | macro kindOf(name: untyped, typ: typed): untyped = 458 | let typ = typ.getTypeInst 459 | result = 460 | if typ.kind == nnkBracketExpr: 461 | nnkBracketExpr.newTree(@[name] & typ[1..^1]) 462 | else: 463 | name 464 | 465 | macro `from`*(matcher, val: untyped): untyped = 466 | let 467 | (name, T, isVar) = getDeclInfo(matcher) 468 | nameInfo = name.lineInfoObj 469 | if isVar: 470 | result = 471 | genast(name, T, val, typKind = ident($T & "Kind")): 472 | val.kind == typKind and 473 | ( 474 | var tmp = kindOf(T, val)(val).addr 475 | template name: untyped = tmp[] 476 | true 477 | ) 478 | result[^1][1][0].setLineInfo(nameInfo) # This is really dumb 479 | else: 480 | result = 481 | genast(name, T, val, typKind = ident($T & "Kind")): 482 | val.kind == typKind and 483 | (let name = kindOf(T, val)(val); true) 484 | result[^1][0][0][0].setLineInfo(nameInfo) # This is dumb 485 | --------------------------------------------------------------------------------