├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── _config.yml ├── patty.nim ├── patty.nimble ├── test.nim └── testhelp.nim /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache 2 | /test 3 | *.ndb -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | compiler: 3 | - gcc 4 | before_install: 5 | # Install nim 6 | - git clone -b devel git://github.com/nim-lang/Nim.git --depth 1 7 | - cd Nim 8 | - git clone --depth 1 git://github.com/nim-lang/csources 9 | - cd csources && sh build.sh 10 | - cd .. 11 | - bin/nim c koch 12 | - ./koch boot -d:release 13 | - export PATH=$PWD/bin:$PATH 14 | - cd .. 15 | script: 16 | - nim c --run test.nim 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Patty - A pattern matching library 2 | ================================== 3 | 4 | [![Build Status](https://travis-ci.org/andreaferretti/patty.svg?branch=master)](https://travis-ci.org/andreaferretti/patty) 5 | [![nimble](https://raw.githubusercontent.com/yglukhov/nimble-tag/master/nimble_js.png)](https://github.com/yglukhov/nimble-tag) 6 | 7 | Patty is a library to perform pattern matching in Nim. Make sure to also have a 8 | look at [Gara](https://github.com/alehander42/gara), which is a more complete 9 | solution. Unlike Patty, Gara misses a macro to generate variant objects, but it 10 | is otherwise much more comprehensive. You can follow [here](https://github.com/alehander42/gara/issues/5) 11 | about adding a macro for object variants in Gara. 12 | 13 | The patterns have to be 14 | [variant objects](http://nim-lang.org/docs/manual.html#types-object-variants), 15 | which in Nim are encoded with a field (usually called `kind`) which varies in 16 | an enum, and a different object layout based on the value of this tag. 17 | An example would be 18 | 19 | ```nim 20 | type 21 | ShapeKind = enum 22 | Circle, Rectangle 23 | Shape = object 24 | case kind: ShapeKind 25 | of Circle: 26 | r: float 27 | of Rectangle: 28 | w, h: float 29 | ``` 30 | 31 | If you have such an algebraic data type, you can do the following with Patty: 32 | 33 | ```nim 34 | import patty 35 | 36 | proc makeRect(w, h: float): Shape = Shape(kind: Rectangle, w: w, h: h) 37 | 38 | match makeRect(3, 4): 39 | Circle(r: radius): 40 | echo "it is a circle of radius ", radius 41 | Rectangle(w: width, h: height): 42 | echo "it is a rectangle of height ", height 43 | ``` 44 | 45 | This will be translated by the `match` macro into the following form 46 | 47 | ```nim 48 | let :tmp = makeRect(3, 4) 49 | case :tmp.kind 50 | of Circle: 51 | let radius = :tmp.r 52 | echo "it is a circle of radius ", radius 53 | of Rectangle: 54 | let 55 | width = :tmp.w 56 | height = :tmp.h 57 | echo "it is a rectangle of height ", height 58 | ``` 59 | 60 | Matching by position is also valid, like this: 61 | 62 | ```nim 63 | match makeRect(3, 4): 64 | Circle(radius): 65 | echo "it is a circle of radius ", radius 66 | Rectangle(width, height): 67 | echo "it is a rectangle of height ", height 68 | ``` 69 | 70 | One can also use `_` for a variable, in which case it will not be bound. 71 | That is, the following 72 | 73 | ```nim 74 | import patty 75 | 76 | proc makeRect(w, h: float): Shape = Shape(kind: Rectangle, w: w, h: h) 77 | 78 | match makeRect(3, 4): 79 | Circle(r: radius): 80 | echo "it is a circle of radius ", radius 81 | Rectangle(w: _, h: height): 82 | echo "it is a rectangle of height ", height 83 | ``` 84 | 85 | becomes 86 | 87 | ```nim 88 | let :tmp = makeRect(3, 4) 89 | case :tmp.kind 90 | of Circle: 91 | let radius = :tmp.r 92 | echo "it is a circle of radius ", radius 93 | of Rectangle: 94 | let height = :tmp.h 95 | echo "it is a rectangle of height ", height 96 | ``` 97 | 98 | The wildcard `_` can also be used as a stand alone pattern, in which case it 99 | will match anything. 100 | It is translated into an `else` clause, that is, the following 101 | 102 | ```nim 103 | import patty 104 | 105 | proc makeRect(w, h: float): Shape = Shape(kind: Rectangle, w: w, h: h) 106 | 107 | match makeRect(3, 4): 108 | Circle(r: radius): 109 | echo "it is a circle of radius ", radius 110 | _: 111 | echo "it is not a circle" 112 | ``` 113 | 114 | becomes 115 | 116 | ```nim 117 | let :tmp = makeRect(3, 4) 118 | case :tmp.kind 119 | of Circle: 120 | let radius = :tmp.r 121 | echo "it is a circle of radius ", radius 122 | else: 123 | echo "it is not a circle" 124 | ``` 125 | 126 | Notice that in the examples, the field you dispatch on is called `kind`, but 127 | any other name would do. Also, checks are exhaustive: if you miss a case, the 128 | compiler will complain. 129 | 130 | One can instead pattern-match on non-variant objects, which essentially amounts 131 | to deconstructing fields: 132 | 133 | ```nim 134 | type Person = object 135 | name: string 136 | age: int 137 | let p = Person(name: "John Doe", age: 37) 138 | match p: 139 | Person(name: n, age: a): 140 | echo n, "is ", a, " years old" 141 | ``` 142 | 143 | Again, this is the same as 144 | 145 | ```nim 146 | match p: 147 | Person(n, a): 148 | echo n, "is ", a, " years old" 149 | ``` 150 | 151 | Pattern matching also works as an expression: 152 | 153 | ```nim 154 | let coord = match c: 155 | Circle(x: x, y: y, r: r): 156 | x 157 | Rectangle(w: w, h: h): 158 | h 159 | ``` 160 | 161 | Constructing variant objects 162 | ---------------------------- 163 | 164 | Patty also provides another macro to create algebraic data types. It looks like 165 | 166 | ```nim 167 | variant Shape: 168 | Circle(r: float) 169 | Rectangle(w: float, h: float) 170 | UnitCircle 171 | ``` 172 | 173 | and expands to 174 | 175 | ```nim 176 | type 177 | ShapeKind {.pure.} = enum 178 | Circle, Rectangle, UnitCircle 179 | Shape = object 180 | case kind: ShapeKind 181 | of ShapeKind.Circle: 182 | r: float 183 | of ShapeKind.Rectangle: 184 | w: float 185 | h: float 186 | of ShapeKind.UnitCircle: 187 | nil 188 | 189 | proc `==`(a: Shape; b: Shape): bool = 190 | if a.kind == b.kind: 191 | case a.kind 192 | of ShapeKind.Circle: 193 | return a.r == b.r 194 | of ShapeKind.Rectangle: 195 | return a.w == b.w and a.h == b.h 196 | of ShapeKind.UnitCircle: 197 | return true 198 | else: 199 | return false 200 | 201 | proc Circle(r: float): Shape = 202 | Shape(kind: ShapeKind.Circle, r: r) 203 | 204 | proc Rectangle(w: float; h: float): Shape = 205 | Shape(kind: ShapeKind.Rectangle, w: w, h: h) 206 | 207 | proc UnitCircle(): Shape = 208 | Shape(kind: ShapeKind.UnitCircle) 209 | ``` 210 | 211 | Notice that the macro also generates three convenient constructors 212 | (`Circle` ,`Rectangle` and `UnitCircle`), and in fact the enum is pure to avoid 213 | a name conflict. Also, a proper definition of equality based on the actual 214 | contents of the record is generated. 215 | 216 | **By default the generated ADT is private to the module**. If you want to 217 | generate a public ADT use the `variantp` macro, which has the same syntax 218 | as `variant` but makes the types, fields, equality definition and generated 219 | constructors public. 220 | 221 | A limitation of the `variant` macro is that field names must be unique across 222 | branches (that is, different variants cannot have two fields with the same name). 223 | This is actually a limitation of Nim. 224 | 225 | In the future, Patty may also add copy constructors. Also, some work needs to 226 | be done to make it easier to use the generated contructors with `ref` types, 227 | in particular for the important case of recursive algebraic data types. 228 | 229 | Example 230 | ------- 231 | 232 | The following example uses the `variant` macro to define a linked list type, 233 | and then uses pattern matching to define the sum of a list of integers: 234 | 235 | ```nim 236 | import patty 237 | 238 | proc `~`[A](a: A): ref A = 239 | new(result) 240 | result[] = a 241 | 242 | variant List[A]: 243 | Nil 244 | Cons(x: A, xs: ref List[A]) 245 | 246 | proc listHelper[A](xs: seq[A]): List[A] = 247 | if xs.len == 0: Nil[A]() 248 | else: Cons(xs[0], ~listHelper(xs[1 .. xs.high])) 249 | 250 | proc list[A](xs: varargs[A]): List[A] = listHelper(@xs) 251 | 252 | proc sum(xs: List[int]): int = (block: 253 | match xs: 254 | Nil: 0 255 | Cons(y, ys): y + sum(ys[]) 256 | ) 257 | 258 | echo sum(list(1, 2, 3, 4, 5)) 259 | ``` 260 | 261 | Versions 262 | -------- 263 | 264 | Patty 0.3.0 works for latest Nim (devel). For older versions of Nim (up to 0.16.0), 265 | use Patty 0.2.1. 266 | 267 | Things that do not work (yet) 268 | ----------------------------- 269 | 270 | One would expect many forms of pattern matching but, at least for now, the 271 | support in Patty is very limited. Things that would be nice to support but do 272 | not work yet include: 273 | 274 | * matching a constant 275 | 276 | ```nim 277 | match c: 278 | "hello": 279 | echo "the string was hello" 280 | ``` 281 | 282 | * matching an existing variable 283 | 284 | ```nim 285 | let x = 5 286 | match c: 287 | x: 288 | echo "c == 5" 289 | ``` 290 | 291 | * nested pattern matching 292 | 293 | ```nim 294 | match c: 295 | Circle(Point(x: x, y: y), r: r): 296 | echo "the abscissa of the center is ", x 297 | ``` 298 | 299 | * matching without binding 300 | 301 | ```nim 302 | match c: 303 | Circle: 304 | echo "it is a circle!" 305 | ``` 306 | 307 | * binding subpatterns 308 | 309 | ```nim 310 | match getMeACircle(): 311 | c@Circle(x, y, r): 312 | echo "there you have ", c 313 | ``` 314 | 315 | * unification 316 | 317 | ```nim 318 | match r: 319 | Rectangle(w: x, h: x): 320 | echo "it is a square" 321 | ``` 322 | 323 | * guards 324 | 325 | ```nim 326 | match c: 327 | Circle(x: x, y: y, r: r) if r < 0: 328 | echo "the circle has negative length" 329 | ``` 330 | 331 | * variable-length pattern matching, such as with arrays 332 | 333 | ```nim 334 | match c: 335 | [a, b, c]: 336 | echo "the length is 3 and the first elements is ", a 337 | ``` 338 | 339 | * custom pattern matchers, such as in regexes 340 | 341 | ```nim 342 | let Email = r"(\w+)@(\w+).(\w+)" 343 | match c: 344 | Email(name, domain, tld): 345 | echo "hello ", name 346 | ``` 347 | 348 | * combining patterns with `or` 349 | 350 | ```nim 351 | match c: 352 | Circle or Rectangle: 353 | echo "it is a shape" 354 | ``` -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /patty.nim: -------------------------------------------------------------------------------- 1 | import macros, sequtils 2 | 3 | const enumSuffix = "Kind" 4 | 5 | iterator tail(a: NimNode): NimNode = 6 | var first = true 7 | for x in children(a): 8 | if not first: yield x 9 | first = false 10 | 11 | proc `&`(n: NimNode, s: string): NimNode = 12 | n.expectKind(nnkIdent) 13 | result = ident($(n) & s) 14 | 15 | proc expectKinds(n: NimNode, kinds: varargs[NimNodeKind]) = 16 | if not @kinds.contains(n.kind): 17 | error("Expected a node of kind among " & $(@kinds) & ", got " & $n.kind, n) 18 | 19 | proc getFields(n: NimNode, pub: bool): seq[tuple[name, ty: NimNode]] = 20 | result = @[] 21 | var identsWithoutTypeDec: seq[NimNode] = @[] 22 | for e in tail(n): 23 | if e.kind == nnkExprColonExpr: 24 | e.expectMinLen(2) 25 | let 26 | fieldName = if pub: postfix(e[0], "*") else: e[0] 27 | fieldType = e[1] 28 | for name in identsWithoutTypeDec: 29 | result.add((name: name, ty: fieldType.copyNimTree)) 30 | identsWithoutTypeDec = @[] 31 | result.add((name: fieldName, ty: fieldType.copyNimTree)) 32 | elif e.kind == nnkIdent: 33 | let name = if pub: postfix(e, "*") else: e 34 | identsWithoutTypeDec.add(name) 35 | else: 36 | error("Invalid field declaration:" & $(toStrLit(e))) 37 | if identsWithoutTypeDec.len > 0: 38 | error("Invalid ADT case: " & $(toStrLit(n)) & n.treeRepr) 39 | 40 | proc enumsIn(n: NimNode): seq[NimNode] = 41 | result = @[] 42 | for c in children(n): 43 | if c.kind == nnkObjConstr or c.kind == nnkCall: 44 | let id = c[0] 45 | id.expectKind(nnkIdent) 46 | result.add(id) 47 | elif c.kind == nnkIdent: 48 | result.add(c) 49 | elif c.kind == nnkCommentStmt: 50 | discard 51 | else: 52 | error("Invalid ADT case: " & $(toStrLit(c))) 53 | 54 | proc newEnum(name: NimNode, idents: seq[NimNode]): NimNode = 55 | result = newNimNode(nnkTypeDef).add( 56 | newNimNode(nnkPragmaExpr).add(name).add( 57 | newNimNode(nnkPragma).add(ident("pure"))), 58 | newEmptyNode()) 59 | var choices = newNimNode(nnkEnumTy).add(newEmptyNode()) 60 | for ident in idents: 61 | choices.add(ident) 62 | result.add(choices) 63 | 64 | proc makeBranch(base, n: NimNode, pub: bool): NimNode = 65 | result = newNimNode(nnkOfBranch) 66 | if n.kind == nnkObjConstr or n.kind == nnkCall: 67 | let id = newNimNode(nnkDotExpr).add(base, n[0]) 68 | var 69 | list = newNimNode(nnkRecList) 70 | for field in getFields(n, pub): 71 | list.add(newIdentDefs(field.name, field.ty)) 72 | result.add(id, list) 73 | elif n.kind == nnkIdent: 74 | result.add(newNimNode(nnkDotExpr).add(base, n), newNimNode(nnkRecList).add(newNilLit())) 75 | elif n.kind == nnkCommentStmt: 76 | discard 77 | else: 78 | error("Invalid ADT case: " & $(toStrLit(n))) 79 | 80 | proc makeGenerics(e: NimNode): NimNode = 81 | e.expectKinds(nnkIdent, nnkBracketExpr) 82 | if e.kind == nnkIdent: 83 | result = newEmptyNode() 84 | else: 85 | result = newNimNode(nnkGenericParams) 86 | for i in 1..= 0.14.0" 10 | 11 | 12 | task tests, "run tests": 13 | --hints: off 14 | --linedir: on 15 | --stacktrace: on 16 | --linetrace: on 17 | --debuginfo 18 | --path: "." 19 | --run 20 | setCommand "c", "test.nim" 21 | 22 | task test, "run tests": 23 | setCommand "tests" -------------------------------------------------------------------------------- /test.nim: -------------------------------------------------------------------------------- 1 | import unittest, patty, testhelp 2 | 3 | proc `~`[A](a: A): ref A = 4 | new(result) 5 | result[] = a 6 | 7 | suite "variant construction": 8 | test "basic creation": 9 | variant Shape: 10 | Circle(r: float, x: float, y: float) 11 | Rectangle(w: float, h: float) 12 | Square(side: int) 13 | 14 | let c = Shape(kind: ShapeKind.Circle, r: 4, x: 2, y: 0) 15 | check c.r == 4.0 16 | 17 | test "allowing empty objects": 18 | variant Shape: 19 | Circle(r: float, x: float, y: float) 20 | Rectangle(w: float, h: float) 21 | Square(side: int) 22 | UnitCircle 23 | 24 | let r = Shape(kind: ShapeKind.Rectangle, w: 2, h: 5) 25 | check r.h == 5.0 26 | 27 | test "constructor creation": 28 | variant Shape: 29 | Circle(r: float, x: float, y: float) 30 | Rectangle(w: float, h: float) 31 | Square(side: int) 32 | 33 | let c = Circle(r = 4, x = 2, y = 0) 34 | check c.kind == ShapeKind.Circle 35 | check c.r == 4.0 36 | 37 | test "constructor of constant objects": 38 | variant Shape: 39 | Circle(r: float, x: float, y: float) 40 | Rectangle(w: float, h: float) 41 | Square(side: int) 42 | UnitCircle 43 | 44 | let c = UnitCircle() 45 | check c.kind == ShapeKind.UnitCircle 46 | 47 | test "variant types with documentation comments": 48 | variant Shape: 49 | ## This is a shape 50 | Circle(r: float, x: float, y: float) 51 | Rectangle(w: float, h: float) 52 | Square(side: int) 53 | ## This is a constant object 54 | UnitCircle 55 | 56 | let c = UnitCircle() 57 | check c.kind == ShapeKind.UnitCircle 58 | 59 | test "recursive types": 60 | variant IntList: 61 | Nil 62 | Cons(head: int, tail: ref IntList) 63 | 64 | let d = Cons(3, ~(Cons(2, ~(Cons(1, ~(Nil())))))) 65 | check d.head == 3 66 | check d.tail.head == 2 67 | 68 | test "generic types": 69 | # There is a conflict with later types due to Nim bug: 70 | # https://github.com/nim-lang/Nim/issues/5170 71 | variant List2[A]: 72 | Nil 73 | Cons(head: A, tail: ref List2[A]) 74 | 75 | var d = Cons(3, ~(Cons(2, ~(Cons(1, ~(Nil[int]())))))) 76 | check d.head == 3 77 | check d.tail.head == 2 78 | 79 | # Check that equality behaves as expected 80 | let nilInt = ~(Nil[int]()) 81 | let nilString = ~(Nil[string]()) 82 | check: Cons(123, nilInt) == Cons(123, nilInt) 83 | check: Cons(321, nilInt) != Cons(123, nilInt) 84 | check: Cons("foo", nilString) == Cons("foo", nilString) 85 | 86 | test "generic types with multiple parameters": 87 | variant Either[A, B]: 88 | Left(a: A) 89 | Right(b: B) 90 | 91 | match Left[int, string](123): 92 | Left(a): 93 | check: a == 123 94 | Right(b): 95 | check: false 96 | 97 | test "generated equality": 98 | variant Shape: 99 | Circle(r: float, x: float, y: float) 100 | Rectangle(w: float, h: float) 101 | Square(side: int) 102 | UnitCircle 103 | 104 | let 105 | c1 = Circle(r = 3, x = 2, y = 5) 106 | c2 = Circle(r = 3, x = 2, y = 5) 107 | c3 = Circle(r = 2, x = 3, y = 5) 108 | s = Square(3) 109 | u1 = UnitCircle() 110 | u2 = UnitCircle() 111 | check c1 == c2 112 | check c1 != c3 113 | check c1 != s 114 | check u1 == u2 115 | 116 | test "one type declaration to multiple fields": 117 | variant Shape: 118 | Circle(r: float, x, y: float) 119 | Rectangle(w, h: float) 120 | Square(side: int) 121 | UnitCircle 122 | 123 | let 124 | c = Circle(3, 2, 5) 125 | r = Rectangle(4, 2) 126 | s = Square(42) 127 | 128 | check c.r == 3 129 | check c.x == 2 130 | check c.y == 5 131 | check r.w == 4 132 | check r.h == 2 133 | check s.side == 42 134 | 135 | test "handling visibility": 136 | let car = Vehicle(kind: VehicleKind.Car, brand: "Fiat", model: "Punto") 137 | 138 | check car.brand == "Fiat" 139 | 140 | test "handling visibility in equality": 141 | let 142 | car1 = Vehicle(kind: VehicleKind.Car, brand: "Fiat", model: "Punto") 143 | car2 = Vehicle(kind: VehicleKind.Car, brand: "Fiat", model: "Punto") 144 | 145 | check car1 == car2 146 | 147 | test "handling visibility in constructors": 148 | let 149 | car1 = Car(brand = "Fiat", model = "Punto") 150 | car2 = Car(brand = "Fiat", model = "Punto") 151 | bike = Bycicle() 152 | truck = Truck(length = 12, tires = 8) 153 | 154 | check car1 == car2 155 | check truck.kind == VehicleKind.Truck 156 | check bike.kind == VehicleKind.Bycicle 157 | check truck.tires == 8 158 | 159 | suite "pattern matching": 160 | type 161 | ShapeKind = enum 162 | Circle, Rectangle 163 | Shape = object 164 | case kind: ShapeKind 165 | of Circle: 166 | x, y, r: float 167 | of Rectangle: 168 | w, h: float 169 | Person = object 170 | name, surname: string 171 | age: int 172 | FruitKind = enum 173 | Apple, Pear 174 | Fruit = object 175 | case fruit: FruitKind 176 | of Apple: 177 | radius, weight: float 178 | of Pear: 179 | circumference, height: float 180 | 181 | test "basic matching": 182 | let c = Shape(kind: Circle, r: 4, x: 2, y: 0) 183 | var res: float = 0 184 | match c: 185 | Circle(x: x, y: y, r: r): 186 | res = r 187 | Rectangle(w: w, h: h): 188 | res = 1 189 | check res == 4.0 190 | 191 | test "binding to different variable names": 192 | let c = Shape(kind: Circle, r: 4, x: 2, y: 0) 193 | var res: float = 0 194 | match c: 195 | Circle(x: x, y: y, r: someNumber): 196 | res = someNumber 197 | Rectangle(w: w, h: h): 198 | res = 1 199 | check res == 4.0 200 | 201 | test "binding a complex expression": 202 | proc makeRect(w, h: float): Shape = 203 | Shape(kind: Rectangle, w: w, h: h) 204 | 205 | var res: float = 0 206 | match makeRect(3, 4): 207 | Circle(x: x, y: y, r: r): 208 | res = r 209 | Rectangle(w: w, h: h): 210 | res = w + h 211 | check res == 7.0 212 | 213 | test "ignoring _ bindings": 214 | let c = Shape(kind: Circle, r: 4, x: 2, y: 0) 215 | var res: float = 0 216 | match c: 217 | Circle(x: _, y: _, r: r): 218 | res = r 219 | Rectangle(w: w, h: h): 220 | res = w + h 221 | check res == 4.0 222 | 223 | test "matching a simple object": 224 | let c = Person(name: "Andrea", surname: "Ferretti", age: 34) 225 | var res: string = "" 226 | match c: 227 | Person(name: n, surname: s, age: a): 228 | res = n 229 | check res == "Andrea" 230 | 231 | test "basic matching with a different discriminator": 232 | let a = Fruit(fruit: Apple, radius: 4, weight: 200) 233 | var res: float = 0 234 | match a: 235 | Apple(radius: r, weight: w): 236 | res = w 237 | Pear(circumference: c, height: h): 238 | res = 1 239 | check res == 200.0 240 | 241 | test "matching a type generated by the variant macro": 242 | variant Shape: 243 | Circle(r: float, x: float, y: float) 244 | Rectangle(w: float, h: float) 245 | Square(side: int) 246 | let c = Circle(3, 5, 6) 247 | var res: float = 0 248 | match c: 249 | Circle(r: r, x: x, y: y): 250 | res = r 251 | Rectangle(w: w, h: h): 252 | res = 1 253 | Square(side: s): 254 | res = 1 255 | check res == 3.0 256 | 257 | test "matching a variant type with implicit field names": 258 | let c = Shape(kind: Circle, x: 2, y: 0, r: 4) 259 | var res: float = 0 260 | match c: 261 | Circle(x, y, r): 262 | res = r 263 | Rectangle(w, h): 264 | res = 1 265 | check res == 4.0 266 | 267 | test "matching a variant type with implicit field names using other identifiers": 268 | let c = Shape(kind: Circle, x: 2, y: 0, r: 4) 269 | var res: float = 0 270 | match c: 271 | Circle(x, y, radius): 272 | res = radius 273 | Rectangle(width, height): 274 | res = 1 275 | check res == 4.0 276 | 277 | test "matching as an expression": 278 | let c = Shape(kind: Circle, x: 2, y: 0, r: 4) 279 | let res = match c: 280 | Circle(x, y, radius): 281 | radius 282 | Rectangle(width, height): 283 | 1.0 284 | check res == 4.0 285 | 286 | test "matching a simple object with implicit fields names": 287 | let c = Person(name: "Andrea", surname: "Ferretti", age: 34) 288 | var res: string = "" 289 | match c: 290 | Person(name, surname, age): 291 | res = name 292 | check res == "Andrea" 293 | 294 | test "matching a simple object with implicit fields names using other identifiers": 295 | let c = Person(name: "Andrea", surname: "Ferretti", age: 34) 296 | var res: string = "" 297 | match c: 298 | Person(n, s, a): 299 | res = n 300 | check res == "Andrea" 301 | 302 | test "catch-all pattern": 303 | let c = Shape(kind: Rectangle, w: 4, h: 3) 304 | var res: float = 0 305 | let debug = true 306 | match c: 307 | Circle(x: _, y: _, r: r): 308 | res = r 309 | _: 310 | res = 13 311 | check res == 13.0 312 | 313 | test "matching on a generic type": 314 | type 315 | ListKind = enum Nil, Cons 316 | List[A] = object 317 | case disc: ListKind 318 | of Nil: 319 | discard 320 | of Cons: 321 | head: A 322 | tail: ref List[A] 323 | 324 | proc `<>`[A](x: A, xs: List[A]): List[A] = 325 | List[A](disc: Cons, head: x, tail: ~xs) 326 | 327 | proc listHelper[A](xs: seq[A]): List[A] = 328 | if xs.len == 0: List[A](disc: Nil) 329 | else: xs[0] <> listHelper(xs[1 .. xs.high]) 330 | 331 | proc list[A](xs: varargs[A]): List[A] = listHelper(@xs) 332 | 333 | let x = list(1, 2, 3) 334 | var res = 0 335 | match x: 336 | Cons(head, tail): 337 | res = head 338 | Nil: 339 | res = 5 340 | 341 | check(res == 1) 342 | 343 | test "generic variant": 344 | type AccProc[T] = proc(): T {.nimcall.} 345 | 346 | variant Accept[T]: 347 | NotAcc 348 | Acc(fun: AccProc[T]) 349 | 350 | discard Acc[int](nil) 351 | 352 | test "matching inside generic context": 353 | variant Foo: 354 | mkA 355 | 356 | proc bar[T](t: T): int = 357 | let m = mkA() 358 | result = 0 359 | match m: 360 | mkA: 361 | result = 1 362 | check(bar(1) == 1) 363 | 364 | test "matching with a variant which share some fields with other variants": 365 | type XKind {.pure.} = enum 366 | A 367 | B 368 | 369 | type X = object 370 | case kind: XKind 371 | of A, B: 372 | x: int 373 | 374 | let 375 | x: X = X(kind: XKind.A, x: 0) 376 | y: X = X(kind: XKind.B, x: 42) 377 | 378 | proc test_match(x :X): int = 379 | match x: 380 | A(x): return x 381 | B(x): return x 382 | 383 | check(test_match(x) == 0) 384 | check(test_match(y) == 42) 385 | -------------------------------------------------------------------------------- /testhelp.nim: -------------------------------------------------------------------------------- 1 | import patty 2 | 3 | 4 | variantp Vehicle: 5 | Bycicle 6 | Car(brand: string, model: string) 7 | Truck(length: float, tires: int) --------------------------------------------------------------------------------