├── .gitignore ├── CHANGELOG.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── adt-simple.js ├── adt-simple.min.js ├── macros └── index.js ├── package.json ├── src ├── compiler.js ├── letstx.js ├── options.js ├── parser.js ├── utils.js └── wrapper.js └── test ├── derive.sjs ├── expand.sjs └── macros.sjs /.gitignore: -------------------------------------------------------------------------------- 1 | test.js 2 | test/*.js 3 | node_modules 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.3 (2014-5-18) 2 | 3 | * Fixed bug where ADTs couldn't be used as constraints 4 | 5 | ## 0.1.2 (2014-5-10) 6 | 7 | * Added compiler pragmas to fine tune output 8 | * Constructors can have keyword fields 9 | * Record fields don't require a contract declaration 10 | 11 | ## 0.1.1 (2014-1-16) 12 | 13 | * Changed positional constructors to use array indexing 14 | 15 | ## 0.1.0 (2014-1-10) 16 | 17 | * Initial release 18 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | uglify: { 4 | options: { 5 | report: 'gzip', 6 | preserveComments: 'some' 7 | }, 8 | main: { 9 | src: 'adt-simple.js', 10 | dest: 'adt-simple.min.js' 11 | } 12 | }, 13 | mochaTest: { 14 | test: { 15 | src: 'test/**/*.js' 16 | } 17 | } 18 | }); 19 | 20 | grunt.registerTask('build', function() { 21 | var macro = grunt.file.read('./src/wrapper.js'); 22 | var lines = mapFilter(macro.split('\n'), function(line) { 23 | var fileName = line.match(/(\s*)\/\/=(.+)/); 24 | if (fileName) { 25 | var include = strip(grunt.file.read('./src/' + fileName[2].trim())); 26 | return include.replace(/^/gm, fileName[1]); 27 | } else { 28 | return line; 29 | } 30 | }); 31 | 32 | grunt.file.write('./macros/index.js', lines.join('\n')); 33 | }); 34 | 35 | grunt.registerTask('build-test', function() { 36 | var path = require('path'); 37 | var files = ['./test/expand.sjs', './test/derive.sjs']; 38 | files.forEach(function(f) { 39 | grunt.file.write(f.replace('.sjs', '.js'), compileFile(f, true)); 40 | }); 41 | }); 42 | 43 | grunt.registerTask('compile', function(fileName) { 44 | grunt.log.write(compileFile(fileName)); 45 | }); 46 | 47 | var moduleCtx; 48 | 49 | function compileFile(fileName, isTest) { 50 | var macro = grunt.file.read('./macros/index.js'); 51 | var test = isTest ? grunt.file.read('./test/macros.sjs') : ''; 52 | var file = grunt.file.read(fileName); 53 | var sweet = require('sweet.js'); 54 | 55 | if (!moduleCtx) moduleCtx = sweet.loadModule(macro); 56 | 57 | return sweet.compile(test + file, { 58 | modules: [moduleCtx], 59 | readableNames: true 60 | }).code; 61 | } 62 | 63 | grunt.registerTask('default', ['build']); 64 | grunt.registerTask('test', ['build', 'build-test', 'mochaTest']); 65 | grunt.loadNpmTasks('grunt-mocha-test'); 66 | grunt.loadNpmTasks('grunt-contrib-uglify'); 67 | }; 68 | 69 | function strip(src) { 70 | return mapFilter(src.split('\n'), function(line) { 71 | var comm = line.indexOf('//'); 72 | if (comm >= 0) line = line.slice(0, comm); 73 | if (line.trim().length) return line; 74 | }).join('\n'); 75 | } 76 | 77 | function mapFilter(arr, fn) { 78 | var res = []; 79 | for (var i = 0, len = arr.length; i < len; i++) { 80 | var item = fn(arr[i]); 81 | if (item != null) res.push(item); 82 | } 83 | return res; 84 | } 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Nathan Faubion 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | adt-simple 2 | ========== 3 | 4 | Native algebraic data types for JavaScript using 5 | [sweet.js](https://github.com/mozilla/sweet.js) macros 6 | 7 | Features 8 | -------- 9 | 10 | * No required runtime dependencies 11 | * `deriving` sugar for mixing in generic behavior 12 | * A catalogue of behavior mixins (`Eq`, `Clone`, `ToJSON`, etc) 13 | 14 | Install 15 | ------- 16 | 17 | npm install -g sweet.js 18 | npm install adt-simple 19 | sjs -m adt-simple/macros myfile.js 20 | 21 | Basic Usage 22 | ----------- 23 | 24 | adt-simple exports a `data` macro for creating simple data constructors. 25 | 26 | ```js 27 | data Singleton 28 | data SingletonVal = 42 29 | data Tuple(*, *) 30 | data Employee { 31 | name: String, 32 | salary: Number 33 | } 34 | 35 | // Singletons can have constant values 36 | SingletonVal.value === 42; 37 | 38 | // Positional fields 39 | var tup = Tuple(1, 2); 40 | tup[0] === 1; 41 | tup[1] === 2; 42 | 43 | // Named fields 44 | var pete = Employee('Peter Gibbons', 85000); 45 | pete.name === 'Peter Gibbons'; 46 | pete.salary === 85000; 47 | ``` 48 | 49 | It also exports a `union` macro for grouping your constructors. 50 | 51 | ```js 52 | union Maybe { 53 | Nothing, 54 | Just { 55 | value: * 56 | } 57 | } 58 | 59 | // Basic inheritance 60 | Nothing instanceof Maybe; 61 | Just(42) instanceof Maybe; 62 | 63 | // Constructors exported on the parent 64 | Maybe.Nothing === Nothing; 65 | Maybe.Just === Just; 66 | ``` 67 | 68 | You can even build recursive unions. 69 | 70 | ```js 71 | union List { 72 | Nil, 73 | Cons { 74 | head: *, 75 | tail: List 76 | } 77 | } 78 | 79 | var list = Cons(1, Cons(2, Cons(3, Nil))); 80 | list.head === 1; 81 | list.tail.head === 2; 82 | list.tail.tail.head === 3; 83 | 84 | // TypeError('Unexpected type for field: List.Cons.tail') 85 | Cons(1, 2) 86 | ``` 87 | 88 | adt-simple doesn't just do instance checking. You can put in your own custom 89 | constraints that validate or transform values: 90 | 91 | ```js 92 | function toString(x) { 93 | return x.toString(); 94 | } 95 | 96 | data OnlyStrings { 97 | value: toString 98 | } 99 | 100 | OnlyStrings(42).value === '42'; 101 | ``` 102 | 103 | It tries to do the right thing: if the identifier starts with a capital letter 104 | (taking namespaces into consideration), it will do instance checking, otherwise 105 | it will call the constraint as a function. The instance checking is smart about 106 | built-in JavaScript types, so it will do proper tag checks for `Boolean`, 107 | `Number`, `Array`, etc beyond just `instanceof`. 108 | 109 | Deriving 110 | -------- 111 | 112 | adt-simple also supports a powerful sugar for deriving generic behaviour: 113 | 114 | ```js 115 | union Maybe { 116 | Nothing, 117 | Just { 118 | value: * 119 | } 120 | } deriving (Eq, Clone) 121 | ``` 122 | 123 | This works for both `union` and `data` constructors. 124 | 125 | Built-in Derivers 126 | ----------------- 127 | 128 | You can import built-in derivers by requiring the `adt-simple` library (< 1KB). 129 | It's available as a `UMD` module, so you can use it in the browser with 130 | require.js or with the global `adt` namespace. 131 | 132 | ```js 133 | var Eq = require('adt-simple').Eq; 134 | ``` 135 | 136 | * [Eq](#eq) 137 | * [Clone](#clone) 138 | * [Setter](#setter) 139 | * [ToString](#tostring) 140 | * [ToJSON](#tojson) 141 | * [Curry](#curry) 142 | * [Extractor](#extractor) 143 | * [Reflect](#reflect) 144 | * [Cata](#cata) 145 | * [LateDeriving](#latederiving) 146 | * [Base](#base) 147 | 148 | Writing Your Own Derivers 149 | ------------------------- 150 | 151 | Derivers are simply objects with a `derive` method. The `derive` method is 152 | called with a template of the ADT so you can traverse, inspect, extend, or 153 | modify it. Here's what a template for a `List` union would look like: 154 | 155 | ```js 156 | union List { 157 | Nil, 158 | Cons { 159 | head: *, 160 | tail: List 161 | } 162 | } 163 | 164 | { 165 | name: 'List', 166 | constructor: List, 167 | prototype: List.prototype, 168 | variants: [ 169 | { 170 | name: 'Nil', 171 | constructor: Nil, 172 | prototype: Nil.prototype 173 | }, 174 | { 175 | name: 'Cons', 176 | constructor: Cons, 177 | prototype: Cons.prototype, 178 | fields: ['head', 'tail'] 179 | } 180 | ] 181 | } 182 | ``` 183 | 184 | Here's how you might write a deriver to get positional fields on a record: 185 | 186 | ```js 187 | var Get = { 188 | derive: function(adt) { 189 | adt.variants.forEach(function(v) { 190 | if (v.fields) { 191 | v.prototype.get = function(i) { 192 | return this[v.fields[i]]; 193 | }; 194 | } else { 195 | v.prototype.get = function() { 196 | throw new Error('No fields'); 197 | }; 198 | } 199 | }) 200 | return adt; 201 | } 202 | }; 203 | ``` 204 | 205 | Notice how you need to return the template at the end of the function to pass 206 | on to the next deriver in the chain. You are free to mutate or tag the template 207 | as needed to communicate between derivers. 208 | 209 | Since the above pattern is so common, you can use the `eachVariant` helper to 210 | shorten your code. 211 | 212 | ```js 213 | var eachVariant = require('adt-simple').eachVariant; 214 | var Get = { 215 | derive: eachVariant(function(v, adt) { 216 | if (v.fields) { 217 | v.prototype.get = function(i) { 218 | return this[v.fields[i]]; 219 | }; 220 | } else { 221 | v.prototype.get = function() { 222 | throw new Error('No fields') 223 | }; 224 | } 225 | }) 226 | }; 227 | ``` 228 | 229 | You can also use `composeDeriving` to compose derivers together into a single 230 | chain. 231 | 232 | ```js 233 | var composeDeriving = require('adt-simple').composeDeriving; 234 | var MyDeriver = composeDeriving(Eq, Clone, Setter, Curry); 235 | 236 | data Foo(*, *) deriving MyDeriver 237 | ``` 238 | 239 | Derivers are just expressions, so you can even parameterize them. 240 | 241 | ```js 242 | var Log = function(prefix) { 243 | return { 244 | derive: eachVariant(function(v) { 245 | v.prototype.log = function() { 246 | console.log(prefix + ' ' + this.toString()); 247 | }; 248 | }) 249 | } 250 | }; 251 | 252 | data Foo(*, *) deriving Log('hello') 253 | 254 | Foo(1, 2).log() // logs: hello Foo(1, 2) 255 | ``` 256 | 257 | Compiler Pragmas 258 | ---------------- 259 | 260 | adt-simple has sensible defaults, but you can also configure the output by 261 | adding one or more pragma comments before your definition. 262 | 263 | ```js 264 | /* @newrequired, @scoped */ 265 | union Foo { 266 | Bar, 267 | Baz 268 | } 269 | ``` 270 | 271 | ### `@newrequired` 272 | 273 | By default, constructors can be called without a `new` keyword. This pragma 274 | disables the `instanceof` check that enables this behavior, leaving you with 275 | a simpler constructor. **Note:** this pragma will conflict with the `Curry` 276 | deriver. 277 | 278 | ### `@scoped` 279 | 280 | All union variants are unwrapped and put in the outer scope. This pragma 281 | disables the unwrapping and leaves them scoped to the parent. 282 | 283 | ### `@overrideapply` 284 | 285 | This pragma lets you define a custom `apply` method on the parent constructor, 286 | which lets you call it as a normal function. 287 | 288 | ```js 289 | /* @overrideapply */ 290 | union List { 291 | Nil, 292 | Cons { 293 | head: *, 294 | tail: List 295 | } 296 | } deriving ToString 297 | 298 | List.apply = function(ctx, args) { 299 | // Turn an array into a list 300 | }; 301 | 302 | List(1, 2, 3).toString() === 'Cons(1, Cons(2, Cons(3, Nil)))'; 303 | ``` 304 | 305 | --- 306 | 307 | ### `Eq` 308 | 309 | Implements an `equals` method for deep equality: 310 | 311 | ```js 312 | data Foo(*) deriving Eq 313 | Foo(Foo(1)).equals(Foo(Foo(1))) === true; 314 | ``` 315 | 316 | By default, `Eq` uses reference equality for anything without an `equals` 317 | method, but you can override it. For example, using `lodash`: 318 | 319 | ```js 320 | Eq.nativeEquals = _.isEqual; 321 | Foo([1, 2, 3]).equals(Foo([1, 2, 3])) === true; 322 | ``` 323 | 324 | ### `Clone` 325 | 326 | Implements a `clone` method for making deep copies: 327 | 328 | ```js 329 | data Foo(*) deriving Clone 330 | 331 | var foo1 = Foo(1); 332 | var foo2 = foo1.clone(); 333 | 334 | foo1 !== foo2 && foo2[0] === 1; 335 | ``` 336 | 337 | Like with `Eq`, `Clone` copies by references anything without a `clone` method. 338 | You can override that behavior in a similar way. Using `lodash`: 339 | 340 | ```js 341 | Clone.nativeClone = _.cloneDeep; 342 | ``` 343 | 344 | ### `Setter` 345 | 346 | Extends constructors with a `create` method and instances with a `set` method 347 | for setting named values. `set` returns a shallow copy with the provided values 348 | changed. 349 | 350 | ```js 351 | data Foo { 352 | bar: *, 353 | baz: * 354 | } deriving Setter 355 | 356 | var foo1 = Foo.create({ bar: 42, baz: 12 }); 357 | var foo2 = foo1.set({ bar: 43 }); 358 | 359 | foo1 !== foo2; 360 | foo2.bar === 43 && foo2.baz === foo1.baz; 361 | ``` 362 | 363 | ### `ToString` 364 | 365 | Extends instances with a good `toString` implementation. 366 | 367 | ```js 368 | union List { 369 | Nil, 370 | Cons { 371 | head: *, 372 | tail: List 373 | } 374 | } deriving ToString 375 | 376 | var list = Cons(1, Cons(2, Cons(3, Nil))); 377 | list.toString() === 'Cons(1, Cons(2, Cons(3, Nil)))'; 378 | ``` 379 | 380 | ### `ToJSON` 381 | 382 | Implements a `toJSON` method. You can configure how singletons are serialized 383 | by assigning a constant value to it. 384 | 385 | ```js 386 | union List { 387 | Nil = null, 388 | Cons { 389 | head: *, 390 | tail: List 391 | } 392 | } deriving ToJSON 393 | 394 | var list = Cons(1, Cons(2, Cons(3, Nil))); 395 | list.toJSON() 396 | { 397 | head: 1, 398 | tail: { 399 | head: 2, 400 | tail: { 401 | head: 3, 402 | tail: null 403 | } 404 | } 405 | } 406 | ``` 407 | 408 | ### `Curry` 409 | 410 | Implements constructor currying and partial application. 411 | 412 | ```js 413 | data Foo(*, *, *) deriving Curry 414 | 415 | Foo(1, 2, 3); 416 | Foo(1)(2)(3); 417 | Foo(1, 2)(3); 418 | Foo(1)(2, 3); 419 | ``` 420 | 421 | ### `Extractor` 422 | 423 | Implements the [sparkler](https://github.com/natefaubion/sparkler) extractor 424 | protocol so you can pattern match on your data instances. 425 | 426 | ```js 427 | union List { 428 | Nil, 429 | Cons { 430 | head: *, 431 | tail: List 432 | } 433 | } deriving Extractor 434 | 435 | List.prototype.map = function(fn) { 436 | return match this { 437 | Nil => Nil, 438 | Cons(x, xs) => Cons(fn(x), xs.map(fn)) 439 | } 440 | } 441 | ``` 442 | 443 | ### `Reflect` 444 | 445 | Implements tag properties and field/union name reflection. 446 | 447 | ```js 448 | union List { 449 | Nil, 450 | Cons { 451 | head: *, 452 | tail: List 453 | } 454 | } deriving Reflect 455 | 456 | 457 | Nil.isNil === true; 458 | Cons(1, Nil).isCons === true; 459 | 460 | List.__names__ // ['Nil', 'Cons'] 461 | Cons.__fields__ // ['head', 'tail'] 462 | ``` 463 | 464 | ### `Cata` 465 | 466 | Implements a `cata` method (ala [daggy](https://github.com/puffnfresh/daggy)) 467 | for doing dispatching and destructuring. 468 | 469 | ```js 470 | union List { 471 | Nil, 472 | Cons { 473 | head: *, 474 | tail: List 475 | } 476 | } deriving Cata 477 | 478 | List.prototype.map = function(fn) { 479 | return this.cata({ 480 | Nil: function() { 481 | return Nil; 482 | }, 483 | Cons: function(x, xs) { 484 | return Cons(fn(x), xs.map(fn)); 485 | } 486 | }) 487 | } 488 | ``` 489 | 490 | ### `LateDeriving` 491 | 492 | Extends constructors with a `deriving` method for deriving after-the-fact. 493 | 494 | ```js 495 | data Foo { 496 | bar: *, 497 | baz: * 498 | } deriving LateDeriving 499 | 500 | Foo.deriving(Eq, Clone); 501 | ``` 502 | 503 | ### `Base` 504 | 505 | A composition of `Eq`, `Clone`, `Setter`, `ToString`, `Reflect`, and `Extractor`. 506 | 507 | --- 508 | 509 | ### Author 510 | Nathan Faubion (@natefaubion) 511 | 512 | ### License 513 | MIT 514 | -------------------------------------------------------------------------------- /adt-simple.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * adt-simple 3 | * ---------- 4 | * author: Nathan Faubion 5 | * version: 0.1.3 6 | * license: MIT 7 | */ 8 | 9 | (function (root, factory) { 10 | if (typeof define === 'function' && define.amd) { 11 | define('adt-simple', factory); 12 | } else if (typeof exports === 'object') { 13 | module.exports = factory(); 14 | } else { 15 | root.adt = exports; 16 | } 17 | })(this, function () { 18 | 19 | var Eq = { 20 | nativeEquals: function(a, b) { 21 | return a === b; 22 | }, 23 | derive: eachVariant(function(v) { 24 | if (v.fields) { 25 | v.prototype.equals = function(that) { 26 | if (this === that) return true; 27 | if (!(that instanceof v.constructor)) return false; 28 | for (var i = 0, len = v.fields.length; i < len; i++) { 29 | var f = v.fields[i]; 30 | var vala = this[f]; 31 | var valb = that[f]; 32 | if (vala && vala.equals) { 33 | if (!vala.equals(valb)) return false; 34 | } else if (!Eq.nativeEquals(vala, valb)) { 35 | return false; 36 | } 37 | } 38 | return true; 39 | }; 40 | } else { 41 | v.prototype.equals = function(that) { 42 | return this === that; 43 | } 44 | } 45 | }) 46 | }; 47 | 48 | var Clone = { 49 | nativeClone: function(a) { 50 | return a; 51 | }, 52 | derive: eachVariant(function(v) { 53 | if (v.fields) { 54 | v.prototype.clone = function() { 55 | var self = this; 56 | var args = map(v.fields, function(f) { 57 | var val = self[f]; 58 | return val && val.clone ? val.clone() : Clone.nativeClone(val); 59 | }); 60 | return unrollApply(v.constructor, args); 61 | }; 62 | } else { 63 | v.prototype.clone = function() { 64 | return this; 65 | }; 66 | } 67 | }) 68 | }; 69 | 70 | var Setter = { 71 | derive: eachVariant(function(v, adt) { 72 | if (v.fields) { 73 | v.constructor.create = function(obj) { 74 | var args = map(v.fields, function(f) { 75 | if (!obj.hasOwnProperty(f)) { 76 | throw new TypeError('Missing field: ' + [adt.name, v.name, f].join('.')); 77 | } 78 | return obj[f]; 79 | }); 80 | return unrollApply(v.constructor, args); 81 | }; 82 | v.prototype.set = function(obj) { 83 | var self = this; 84 | var args = map(v.fields, function(f) { 85 | return obj.hasOwnProperty(f) ? obj[f] : self[f]; 86 | }); 87 | return unrollApply(v.constructor, args); 88 | }; 89 | } 90 | }) 91 | }; 92 | 93 | var ToString = { 94 | toString: function(x) { 95 | if (x === null) return 'null'; 96 | if (x === void 0) return 'undefined'; 97 | if (Object.prototype.toString.call(x) === '[object Array]') { 98 | return '[' + map(x, function(v) { 99 | return ToString.toString(v); 100 | }).join(', ') + ']'; 101 | } 102 | return x.toString(); 103 | }, 104 | derive: eachVariant(function(v) { 105 | if (v.fields) { 106 | v.prototype.toString = function() { 107 | var self = this; 108 | return v.name + '(' + map(v.fields, function(f) { 109 | return ToString.toString(self[f]); 110 | }).join(', ') + ')'; 111 | }; 112 | } else { 113 | v.prototype.toString = function() { 114 | return v.name; 115 | }; 116 | } 117 | }) 118 | }; 119 | 120 | var ToJSON = { 121 | toJSONValue: function(x) { 122 | return x && typeof x === 'object' && x.toJSON ? x.toJSON() : x; 123 | }, 124 | derive: eachVariant(function(v) { 125 | if (v.fields) { 126 | v.prototype.toJSON = function() { 127 | var res = {}; 128 | var self = this; 129 | each(v.fields, function(f) { 130 | res[f] = ToJSON.toJSONValue(self[f]); 131 | }); 132 | return res; 133 | } 134 | } else { 135 | v.prototype.toJSON = function() { 136 | return this.hasOwnProperty('value') ? this.value : v.name 137 | }; 138 | } 139 | }) 140 | }; 141 | 142 | var Curry = { 143 | derive: eachVariant(function(v, adt) { 144 | if (v.fields && v.fields.length) { 145 | var ctr = v.constructor; 146 | function curried() { 147 | var args = arguments; 148 | if (args.length < v.fields.length) { 149 | return function() { 150 | return unrollApply(curried, concat(args, arguments)); 151 | }; 152 | } 153 | var res = unrollApply(ctr, args); 154 | return res; 155 | }; 156 | 157 | v.constructor = curried; 158 | v.constructor.prototype = ctr.prototype; 159 | v.prototype.constructor = curried; 160 | 161 | if (adt.constructor === ctr) { 162 | adt.constructor = v.constructor; 163 | for (var k in ctr) { 164 | if (ctr.hasOwnProperty(k)) { 165 | adt.constructor[k] = ctr[k]; 166 | } 167 | } 168 | } 169 | } 170 | }) 171 | }; 172 | 173 | var Extractor = { 174 | derive: eachVariant(function(v) { 175 | if (v.fields) { 176 | v.constructor.hasInstance = function(x) { 177 | return x && x.constructor === v.constructor; 178 | }; 179 | v.constructor.unapply = function(x) { 180 | if (v.constructor.hasInstance(x)) { 181 | return map(v.fields, function(f) { 182 | return x[f]; 183 | }); 184 | } 185 | }; 186 | v.constructor.unapplyObject = function(x) { 187 | if (v.constructor.hasInstance(x)) { 188 | var res = {}; 189 | each(v.fields, function(f) { res[f] = x[f] }); 190 | return res; 191 | } 192 | }; 193 | } else { 194 | v.prototype.hasInstance = function(x) { 195 | return x === this; 196 | }; 197 | } 198 | }) 199 | }; 200 | 201 | var Reflect = { 202 | derive: function(adt) { 203 | adt.constructor.__names__ = map(adt.variants, function(v) { 204 | v.prototype['is' + v.name] = true; 205 | v.constructor.__fields__ = v.fields ? v.fields.slice() : null; 206 | return v.name; 207 | }); 208 | return adt; 209 | } 210 | }; 211 | 212 | var Cata = { 213 | derive: eachVariant(function(v, adt) { 214 | v.prototype.cata = function(dispatch) { 215 | if (!dispatch.hasOwnProperty(v.name)) { 216 | throw new TypeError('No branch for: ' + [adt.name, v.name].join('.')); 217 | } 218 | var self = this; 219 | var args = v.fields 220 | ? map(v.fields, function(f) { return self[f] }) 221 | : []; 222 | return dispatch[v.name].apply(this, args); 223 | }; 224 | }) 225 | }; 226 | 227 | var LateDeriving = { 228 | derive: function(adt) { 229 | // Singleton data constructors need it on the prototype 230 | var ctr = adt.variants && adt.variants[0] && 231 | adt.variants[0].constructor === adt.constructor && 232 | !adt.variants[0].fields 233 | ? adt.prototype 234 | : adt.constructor 235 | 236 | ctr.deriving = function() { 237 | var res = adt; 238 | for (var i = 0, c; c = arguments[i]; i++) { 239 | res = c.derive(res); 240 | } 241 | } 242 | return adt; 243 | } 244 | }; 245 | 246 | var Base = composeDeriving(Eq, Clone, Setter, ToString, Reflect, Extractor); 247 | 248 | // Export 249 | // ------ 250 | 251 | return { 252 | eachVariant: eachVariant, 253 | composeDeriving: composeDeriving, 254 | Eq: Eq, 255 | Clone: Clone, 256 | Setter: Setter, 257 | ToString: ToString, 258 | ToJSON: ToJSON, 259 | Curry: Curry, 260 | Extractor: Extractor, 261 | Reflect: Reflect, 262 | Cata: Cata, 263 | LateDeriving: LateDeriving, 264 | Base: Base 265 | }; 266 | 267 | // Utilities 268 | // --------- 269 | 270 | function each(arr, fn) { 271 | for (var i = 0, len = arr.length; i < len; i++) { 272 | fn(arr[i], i, arr); 273 | } 274 | } 275 | 276 | function map(arr, fn) { 277 | var res = []; 278 | for (var i = 0, len = arr.length; i < len; i++) { 279 | res[res.length] = fn(arr[i], i, arr); 280 | } 281 | return res; 282 | } 283 | 284 | function eachVariant(fn) { 285 | return function(adt) { 286 | each(adt.variants, function(v) { 287 | fn(v, adt); 288 | }); 289 | return adt; 290 | } 291 | } 292 | 293 | function composeDeriving() { 294 | var classes = arguments; 295 | return { 296 | derive: function(adt) { 297 | var res = adt; 298 | for (var i = 0, len = classes.length; i < len; i++) { 299 | res = classes[i].derive(res); 300 | } 301 | return res; 302 | } 303 | }; 304 | } 305 | 306 | function unrollApply(fn, a) { 307 | switch (a.length) { 308 | case 0: return fn(); 309 | case 1: return fn(a[0]); 310 | case 2: return fn(a[0], a[1]); 311 | case 3: return fn(a[0], a[1], a[2]); 312 | case 4: return fn(a[0], a[1], a[2], a[3]); 313 | default: return fn.apply(null, a); 314 | } 315 | } 316 | 317 | function concat(a, b) { 318 | var res = [], i, len; 319 | for (i = 0, len = a.length; i < len; i++) res[res.length] = a[i]; 320 | for (i = 0, len = b.length; i < len; i++) res[res.length] = b[i]; 321 | return res; 322 | } 323 | }); 324 | -------------------------------------------------------------------------------- /adt-simple.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * adt-simple 3 | * ---------- 4 | * author: Nathan Faubion 5 | * version: 0.1.3 6 | * license: MIT 7 | */ 8 | !function(a,b){"function"==typeof define&&define.amd?define("adt-simple",b):"object"==typeof exports?module.exports=b():a.adt=exports}(this,function(){function a(a,b){for(var c=0,d=a.length;d>c;c++)b(a[c],c,a)}function b(a,b){for(var c=[],d=0,e=a.length;e>d;d++)c[c.length]=b(a[d],d,a);return c}function c(b){return function(c){return a(c.variants,function(a){b(a,c)}),c}}function d(){var a=arguments;return{derive:function(b){for(var c=b,d=0,e=a.length;e>d;d++)c=a[d].derive(c);return c}}}function e(a,b){switch(b.length){case 0:return a();case 1:return a(b[0]);case 2:return a(b[0],b[1]);case 3:return a(b[0],b[1],b[2]);case 4:return a(b[0],b[1],b[2],b[3]);default:return a.apply(null,b)}}function f(a,b){var c,d,e=[];for(c=0,d=a.length;d>c;c++)e[e.length]=a[c];for(c=0,d=b.length;d>c;c++)e[e.length]=b[c];return e}var g={nativeEquals:function(a,b){return a===b},derive:c(function(a){a.prototype.equals=a.fields?function(b){if(this===b)return!0;if(!(b instanceof a.constructor))return!1;for(var c=0,d=a.fields.length;d>c;c++){var e=a.fields[c],f=this[e],h=b[e];if(f&&f.equals){if(!f.equals(h))return!1}else if(!g.nativeEquals(f,h))return!1}return!0}:function(a){return this===a}})},h={nativeClone:function(a){return a},derive:c(function(a){a.prototype.clone=a.fields?function(){var c=this,d=b(a.fields,function(a){var b=c[a];return b&&b.clone?b.clone():h.nativeClone(b)});return e(a.constructor,d)}:function(){return this}})},i={derive:c(function(a,c){a.fields&&(a.constructor.create=function(d){var f=b(a.fields,function(b){if(!d.hasOwnProperty(b))throw new TypeError("Missing field: "+[c.name,a.name,b].join("."));return d[b]});return e(a.constructor,f)},a.prototype.set=function(c){var d=this,f=b(a.fields,function(a){return c.hasOwnProperty(a)?c[a]:d[a]});return e(a.constructor,f)})})},j={toString:function(a){return null===a?"null":void 0===a?"undefined":"[object Array]"===Object.prototype.toString.call(a)?"["+b(a,function(a){return j.toString(a)}).join(", ")+"]":a.toString()},derive:c(function(a){a.prototype.toString=a.fields?function(){var c=this;return a.name+"("+b(a.fields,function(a){return j.toString(c[a])}).join(", ")+")"}:function(){return a.name}})},k={toJSONValue:function(a){return a&&"object"==typeof a&&a.toJSON?a.toJSON():a},derive:c(function(b){b.prototype.toJSON=b.fields?function(){var c={},d=this;return a(b.fields,function(a){c[a]=k.toJSONValue(d[a])}),c}:function(){return this.hasOwnProperty("value")?this.value:b.name}})},l={derive:c(function(a,b){function c(){var b=arguments;if(b.length { 3 | var ctx = #{ $ctx }; 4 | var here = #{ here }; 5 | var name = #{ $name }; 6 | var body = #{ $body }[0].token.inner; 7 | var derivs = #{ $derivs }[0].token.inner; 8 | var options = {}; 9 | 10 | let letstx = macro { 11 | case { $mac $id:ident $punc = $rhs:expr } => { 12 | var mac = #{ $mac }; 13 | var id = #{ $id }; 14 | var val = #{ $val }; 15 | var arg = #{ $($rhs) }; 16 | var punc = #{ $punc }; 17 | var here = #{ here }; 18 | if (punc[0].token.type !== parser.Token.Punctuator || 19 | punc[0].token.value !== '...') { 20 | throw new SyntaxError('Unexpected token: ' + punc[0].token.value + 21 | ' (expected ...)'); 22 | } 23 | if (id[0].token.value[0] !== '$') { 24 | throw new SyntaxError('Syntax identifiers must start with $: ' + 25 | id[0].token.value); 26 | } 27 | return [ 28 | makeIdent('match', mac), 29 | makePunc('.', here), 30 | makeIdent('patternEnv', here), 31 | makeDelim('[]', [makeValue(id[0].token.value, here)], here), 32 | makePunc('=', here), 33 | makeDelim('{}', [ 34 | makeIdent('level', here), makePunc(':', here), makeValue(1, here), makePunc(',', here), 35 | makeIdent('match', here), makePunc(':', here), makeDelim('()', #{ 36 | (function(exp) { 37 | return exp.length 38 | ? exp.map(function(t) { return { level: 0, match: [t] } }) 39 | : [{ level: 0, match: [] }]; 40 | }) 41 | }, here), makeDelim('()', arg, here) 42 | ], here) 43 | ]; 44 | } 45 | case { $mac $id:ident = $rhs:expr } => { 46 | var mac = #{ $mac }; 47 | var id = #{ $id }; 48 | var val = #{ $val }; 49 | var arg = #{ $($rhs) }; 50 | var here = #{ here }; 51 | if (id[0].token.value[0] !== '$') { 52 | throw new SyntaxError('Syntax identifiers must start with $: ' + 53 | id[0].token.value); 54 | } 55 | return [ 56 | makeIdent('match', mac), 57 | makePunc('.', here), 58 | makeIdent('patternEnv', here), 59 | makeDelim('[]', [makeValue(id[0].token.value, here)], here), 60 | makePunc('=', here), 61 | makeDelim('{}', [ 62 | makeIdent('level', here), makePunc(':', here), makeValue(0, here), makePunc(',', here), 63 | makeIdent('match', here), makePunc(':', here), arg[0] 64 | ], here) 65 | ]; 66 | } 67 | } 68 | function syntaxError(tok, err, info) { 69 | if (!err) err = 'Unexpected token'; 70 | if (info) err += ' (' + info + ')'; 71 | throwSyntaxError('adt-simple', err, tok); 72 | } 73 | function matchesToken(tmpl, t) { 74 | if (t && t.length === 1) t = t[0]; 75 | if (!t || tmpl.type && t.token.type !== tmpl.type 76 | || tmpl.value && t.token.value !== tmpl.value) return false; 77 | return true; 78 | } 79 | function input(stx) { 80 | var pos = 0; 81 | var inp = { 82 | length: stx.length, 83 | buffer: stx, 84 | peek: peek, 85 | take: take, 86 | takeAPeek: takeAPeek, 87 | back: back, 88 | rest: rest 89 | }; 90 | return inp; 91 | function peek() { 92 | if (arguments.length === 0) { 93 | return [stx[pos]]; 94 | } 95 | if (typeof arguments[0] === 'number') { 96 | if (inp.length < arguments[0]) return; 97 | return stx.slice(pos, pos + arguments[0]); 98 | } 99 | var res = []; 100 | for (var i = 0, j = pos, t, a, m; i < arguments.length; i++) { 101 | a = arguments[i]; 102 | t = stx[j++]; 103 | if (!matchesToken(a, t)) return; 104 | res.push(t); 105 | } 106 | return res; 107 | } 108 | function take(len) { 109 | var res = stx.slice(pos, pos + (len || 1)); 110 | pos += len || 1; 111 | inp.length -= len || 1; 112 | return res; 113 | } 114 | function takeAPeek() { 115 | var res = peek.apply(null, arguments); 116 | if (res) return take(res.length); 117 | } 118 | function back(len) { 119 | pos -= len || 1; 120 | inp.length += len || 1; 121 | } 122 | function rest() { 123 | return stx.slice(pos); 124 | } 125 | } 126 | var cid = 0; 127 | function makeConstraint() { 128 | return [makeIdent('c' + (++cid), here)]; 129 | } 130 | var pragmas = { 131 | overrideApply: /@overrideapply\b/gmi, 132 | newRequired: /@newrequired\b/gmi, 133 | scoped: /@scoped\b/gmi 134 | }; 135 | if (ctx[0].token.leadingComments) { 136 | ctx[0].token.leadingComments.forEach(function(comment) { 137 | Object.keys(pragmas).forEach(function(optName) { 138 | if (comment.value.match(pragmas[optName])) { 139 | options[optName] = true; 140 | } 141 | }); 142 | }); 143 | } 144 | var T = parser.Token; 145 | var EQ = { type: T.Punctuator, value: '=' }; 146 | var COLON = { type: T.Punctuator, value: ':' }; 147 | var COMMA = { type: T.Punctuator, value: ',' }; 148 | var PERIOD = { type: T.Punctuator, value: '.' }; 149 | var WILDCARD = { type: T.Punctuator, value: '*' }; 150 | var PARENS = { type: T.Delimiter, value: '()' }; 151 | var BRACES = { type: T.Delimiter, value: '{}' }; 152 | var IDENT = { type: T.Identifier }; 153 | var KEYWORD = { type: T.Keyword }; 154 | function parse(stx) { 155 | var inp = input(stx); 156 | var res = commaSeparated(parseConstructor, inp); 157 | if (res.length === 0) { 158 | syntaxError(null, 'Expected constructor'); 159 | } 160 | return res; 161 | } 162 | function parseConstructor(inp) { 163 | return parseRecord(inp) 164 | || parsePositional(inp) 165 | || parseSingleton(inp); 166 | } 167 | function parseRecord(inp) { 168 | var res = inp.takeAPeek(IDENT, BRACES); 169 | if (res) { 170 | return { 171 | name: unwrapSyntax(res[0]), 172 | fields: commaSeparated(parseField, input(res[1].expose().token.inner)) 173 | }; 174 | } 175 | } 176 | function parsePositional(inp) { 177 | var res = inp.takeAPeek(IDENT, PARENS); 178 | if (res) { 179 | var inp2 = input(res[1].expose().token.inner); 180 | return { 181 | name: unwrapSyntax(res[0]), 182 | positional: true, 183 | fields: commaSeparated(parseConstraint, inp2).map(function(c, i) { 184 | return { name: i.toString(), arg: '_' + i.toString(), constraint: c }; 185 | }) 186 | }; 187 | } 188 | } 189 | function parseSingleton(inp) { 190 | var res = inp.takeAPeek(IDENT); 191 | var val; 192 | if (res) { 193 | if (inp.takeAPeek(EQ)) { 194 | val = takeUntil(COMMA, inp); 195 | if (!val) syntaxError(inp.back().take(), 'Expected value'); 196 | } 197 | var ret = { name: unwrapSyntax(res[0]) }; 198 | if (val) ret.value = val; 199 | return ret; 200 | } 201 | } 202 | function parseField(inp) { 203 | var res1 = inp.takeAPeek(IDENT) || inp.takeAPeek(KEYWORD); 204 | if (res1) { 205 | var name = unwrapSyntax(res1[0]); 206 | var arg = res1[0].token.type === T.Keyword ? '_' + name : name; 207 | var res2 = inp.takeAPeek(COLON); 208 | if (res2) { 209 | var cons = parseConstraint(inp); 210 | if (cons) { 211 | return { 212 | name: name, 213 | arg: arg, 214 | constraint: cons 215 | }; 216 | } 217 | syntaxError(res2, 'Expected constraint'); 218 | } else { 219 | return { 220 | name: name, 221 | arg: arg, 222 | constraint: { type: 'any' } 223 | } 224 | } 225 | } 226 | } 227 | function parseConstraint(inp) { 228 | var res = inp.takeAPeek(WILDCARD); 229 | if (res) return { type: 'any' }; 230 | res = parseClassName(inp); 231 | if (res) return { type: 'class', stx: res }; 232 | res = takeUntil(COMMA, inp); 233 | if (res.length) { 234 | var expr = getExpr(res); 235 | if (expr.success && !expr.rest.length) { 236 | return { type: 'literal', stx: expr.result }; 237 | } 238 | syntaxError(expr.success ? expr.rest[0] : res[0]); 239 | } 240 | if (inp.length) { 241 | syntaxError(inp.take()); 242 | } 243 | } 244 | function parseClassName(inp) { 245 | var stx = [], tok; 246 | while (tok = inp.peek()) { 247 | if (stx.length === 0 && matchesToken(IDENT, tok) || 248 | stx.length && matchesToken(IDENT, stx[0]) && matchesToken(PERIOD, tok) || 249 | stx.length && matchesToken(IDENT, tok) && matchesToken(PERIOD, stx[0])) { 250 | stx.unshift(inp.take()[0]); 251 | } else break; 252 | } 253 | if (stx.length) { 254 | if (matchesToken(PERIOD, stx[0])) syntaxError(stx[0]); 255 | var name = stx[0].token.value; 256 | if (name[0].toUpperCase() === name[0] && 257 | name[0] !== '$' && name[0] !== '_') { 258 | return stx.reverse(); 259 | } else { 260 | inp.back(stx.length); 261 | } 262 | } 263 | } 264 | function parseDerivers(stx) { 265 | return stx.map(function(delim) { 266 | return delim.expose().token.inner; 267 | }); 268 | } 269 | function commaSeparated(parser, inp, cb) { 270 | var all = [], res; 271 | while (inp.length) { 272 | res = parser(inp); 273 | if (res && !cb || res && cb(res, inp)) { 274 | all.push(res); 275 | if (!inp.takeAPeek(COMMA) && inp.length) { 276 | syntaxError(inp.take(), null, 'maybe you meant ,'); 277 | } 278 | } else if (!res) { 279 | syntaxError(inp.take()); 280 | } 281 | } 282 | return all; 283 | } 284 | function takeUntil(tok, inp) { 285 | var res = []; 286 | while (inp.length && !inp.peek(tok)) { 287 | res.push(inp.take()[0]); 288 | } 289 | return res; 290 | } 291 | var isData = unwrapSyntax(ctx) === 'data'; 292 | var isUnion = unwrapSyntax(ctx) === 'union'; 293 | function compile(tmpls, derivers) { 294 | letstx $parentName = [makeIdent(unwrapSyntax(name), here)]; 295 | letstx $ctrs ... = compileConstructors(tmpls); 296 | letstx $derived ... = derivers.length ? compileDeriving(tmpls, derivers) : []; 297 | letstx $export ... = compileExport(tmpls, derivers.length); 298 | letstx $unwrapped ... = options.scoped ? [] : compileUnwrap(tmpls); 299 | if (isData) { 300 | if (derivers.length) { 301 | var exp = tmpls[0].fields 302 | ? #{ return derived.constructor; } 303 | : #{ return new derived.constructor(); }; 304 | letstx $export ... = exp; 305 | return #{ 306 | var $name = function() { 307 | $ctrs ... 308 | $derived ... 309 | $export ... 310 | }(); 311 | } 312 | } else { 313 | var exp = tmpls[0].fields 314 | ? #{ return $parentName; } 315 | : #{ return new $parentName(); }; 316 | letstx $export ... = exp; 317 | return #{ 318 | var $name = function() { 319 | $ctrs ... 320 | $export ... 321 | }(); 322 | } 323 | } 324 | } else { 325 | var parentBody = []; 326 | if (options.overrideApply) { 327 | parentBody = #{ 328 | if ($parentName.apply !== Function.prototype.apply) { 329 | return $parentName.apply(this, arguments); 330 | } 331 | } 332 | } 333 | letstx $parentBody ... = parentBody; 334 | return #{ 335 | var $name = function() { 336 | function $parentName() { 337 | $parentBody ... 338 | } 339 | $ctrs ... 340 | $derived ... 341 | $export ... 342 | return $parentName; 343 | }(); 344 | $unwrapped ... 345 | } 346 | } 347 | } 348 | function compileConstructors(tmpls) { 349 | return tmpls.reduce(function(stx, tmpl) { 350 | var res = tmpl.fields 351 | ? compileRecord(tmpl) 352 | : compileSingleton(tmpl); 353 | return stx.concat(res); 354 | }, []); 355 | } 356 | function compileRecord(tmpl) { 357 | var args = tmpl.fields.reduce(function(acc, f) { 358 | f.arg = [makeIdent(f.arg, here)]; 359 | return acc.concat(f.arg); 360 | }, []); 361 | var constraints = tmpl.fields.reduce(function(stx, f) { 362 | if (f.constraint.type !== 'literal') { 363 | return stx; 364 | } 365 | f.constraint.ref = makeConstraint(); 366 | return stx.concat(compileConstraint(f.constraint)); 367 | }, []); 368 | var fields = tmpl.fields.reduce(function(stx, f) { 369 | return stx.concat(compileField(f, tmpl)); 370 | }, []); 371 | if (tmpl.positional) { 372 | letstx $ctrLength = [makeValue(tmpl.fields.length, here)]; 373 | }; 374 | letstx $ctrName = [makeIdent(tmpl.name, here)]; 375 | letstx $ctrArgs ... = args; 376 | var ctrBody; 377 | if (options.newRequired) { 378 | ctrBody = []; 379 | } else { 380 | ctrBody = #{ 381 | if (!(this instanceof $ctrName)) { 382 | return new $ctrName($ctrArgs (,) ...); 383 | } 384 | } 385 | } 386 | letstx $ctrBody ... = ctrBody; 387 | letstx $ctrFields ... = fields; 388 | letstx $ctrCons ... = constraints; 389 | return #{ 390 | $ctrCons ... 391 | function $ctrName($ctrArgs (,) ...) { 392 | $ctrBody ... 393 | $ctrFields ... 394 | } 395 | }.concat(isData ? [] : #{ 396 | $ctrName.prototype = new $parentName(); 397 | $ctrName.prototype.constructor = $ctrName; 398 | }).concat(!tmpl.positional ? [] : #{ 399 | $ctrName.prototype.length = $ctrLength; 400 | }); 401 | } 402 | function compileSingleton(tmpl) { 403 | letstx $ctrVal = tmpl.value || []; 404 | var assign = tmpl.value ? #{ this.value = $ctrVal; } : []; 405 | letstx $ctrName = [makeIdent(tmpl.name, here)]; 406 | letstx $ctrAssign ... = assign; 407 | return #{ 408 | function $ctrName() { 409 | $ctrAssign ... 410 | } 411 | }.concat(isData ? [] : #{ 412 | $ctrName.prototype = new $parentName(); 413 | $ctrName.prototype.constructor = $ctrName; 414 | }); 415 | } 416 | function compileExport(tmpls, derived) { 417 | return tmpls.reduce(function(stx, tmpl, i) { 418 | letstx $ctrName = [makeIdent(tmpl.name, here)]; 419 | letstx $ctrIndex = [makeValue(i, here)]; 420 | var res; 421 | if (derived) { 422 | letstx $derivedRef = [makeIdent('derived', here)]; 423 | res = tmpl.fields 424 | ? #{ $parentName.$ctrName = $derivedRef.variants[$ctrIndex].constructor; } 425 | : #{ $parentName.$ctrName = new $derivedRef.variants[$ctrIndex].constructor(); } 426 | } else { 427 | res = tmpl.fields 428 | ? #{ $parentName.$ctrName = $ctrName; } 429 | : #{ $parentName.$ctrName = new $ctrName(); }; 430 | } 431 | return stx.concat(res); 432 | }, []); 433 | } 434 | function compileField(field, record) { 435 | letstx $fieldArg = field.arg; 436 | letstx $fieldName = record.positional 437 | ? [makeKeyword('this', here), makeDelim('[]', [makeValue(field.name, here)], here)] 438 | : [makeKeyword('this', here), makePunc('.', here), makeIdent(field.name, here)]; 439 | if (field.constraint.type === 'any') { 440 | return #{ 441 | $fieldName = $fieldArg; 442 | } 443 | } 444 | if (field.constraint.type === 'class') { 445 | var fullName = isData 446 | ? [record.name, field.name].join('.') 447 | : [unwrapSyntax(name), record.name, field.name].join('.'); 448 | letstx $fieldCheck ... = compileInstanceCheck(field.constraint); 449 | letstx $fieldError = [makeValue('Unexpected type for field: ' + fullName, here)]; 450 | return #{ 451 | if ($fieldCheck ...) { 452 | $fieldName = $fieldArg; 453 | } else { 454 | throw new TypeError($fieldError); 455 | } 456 | } 457 | } 458 | if (field.constraint.type === 'literal') { 459 | letstx $fieldCons = field.constraint.ref; 460 | return #{ 461 | $fieldName = $fieldCons($fieldArg); 462 | } 463 | } 464 | } 465 | function compileInstanceCheck(cons) { 466 | if (cons.stx.length === 1) { 467 | var name = unwrapSyntax(cons.stx); 468 | switch(name) { 469 | case 'String': 470 | return #{ 471 | typeof $fieldArg === 'string' || 472 | Object.prototype.toString.call($fieldArg) === '[object String]' 473 | } 474 | case 'Number': 475 | return #{ 476 | typeof $fieldArg === 'number' || 477 | Object.prototype.toString.call($fieldArg) === '[object Number]' 478 | } 479 | case 'Boolean': 480 | return #{ 481 | typeof $fieldArg === 'boolean' || 482 | Object.prototype.toString.call($fieldArg) === '[object Boolean]' 483 | } 484 | case 'RegExp': 485 | return #{ 486 | Object.prototype.toString.call($fieldArg) === '[object RegExp]' 487 | } 488 | case 'Date': 489 | return #{ 490 | Object.prototype.toString.call($fieldArg) === '[object Date]' 491 | } 492 | case 'Function': 493 | return #{ 494 | Object.prototype.toString.call($fieldArg) === '[object Function]' 495 | } 496 | case 'Array': 497 | return #{ 498 | Array.isArray 499 | ? Array.isArray($fieldArg) 500 | : Object.prototype.toString.call($fieldArg) === '[object Array]' 501 | } 502 | case 'Object': 503 | return #{ 504 | $fieldArg != null && ($fieldArg = Object($fieldArg)) 505 | } 506 | } 507 | } 508 | letstx $fieldClass ... = cons.stx; 509 | return #{ 510 | $fieldArg instanceof $fieldClass ... 511 | } 512 | } 513 | function compileConstraint(cons) { 514 | letstx $consRef = cons.ref; 515 | letstx $consStx ... = cons.stx; 516 | return #{ 517 | var $consRef = $consStx ...; 518 | } 519 | } 520 | function compileDeriving(tmpls, derivers) { 521 | var variants = tmpls.reduce(function(stx, tmpl) { 522 | return stx.concat(compileTemplate(tmpl)); 523 | }, []); 524 | letstx $derivedRef = [makeIdent('derived', here)]; 525 | letstx $nameStr = [makeValue(unwrapSyntax(name), here)]; 526 | letstx $variants ... = variants; 527 | var template = #{{ 528 | name: $nameStr, 529 | constructor: $parentName, 530 | prototype: $parentName.prototype, 531 | variants: [$variants (,) ...] 532 | }}; 533 | var calls = derivers.reduce(function(stx, d) { 534 | letstx $deriver ... = d; 535 | letstx $deriverArg ... = stx; 536 | return #{ 537 | $deriver ... .derive($deriverArg ...) 538 | } 539 | }, template); 540 | letstx $derivers ... = calls; 541 | return #{ 542 | var $derivedRef = $derivers ...; 543 | } 544 | } 545 | function compileTemplate(tmpl) { 546 | letstx $tmplName = [makeValue(tmpl.name, here)]; 547 | letstx $tmplCtr = [makeIdent(tmpl.name, here)]; 548 | var res = #{ 549 | { 550 | name: $tmplName, 551 | constructor: $tmplCtr, 552 | prototype: $tmplCtr.prototype 553 | } 554 | }; 555 | if (tmpl.fields) { 556 | letstx $tmplFields ... = tmpl.fields.map(function(f) { 557 | return makeValue(f.name, here); 558 | }); 559 | res[0].token.inner = res[0].token.inner.concat(#{ 560 | , fields: [$tmplFields (,) ...] 561 | }); 562 | } 563 | return res; 564 | } 565 | function compileUnwrap(tmpls) { 566 | return tmpls.reduce(function(stx, tmpl) { 567 | letstx $tmplName = [makeIdent(tmpl.name, ctx)]; 568 | return stx.concat(#{ 569 | var $tmplName = $name.$tmplName; 570 | }); 571 | }, []); 572 | } 573 | 574 | return compile(parse(body), parseDerivers(derivs)); 575 | } 576 | } 577 | 578 | macro $adt__deriving { 579 | case { _ $ctx $name $body deriving ( $derivs:expr (,) ... ) } => { 580 | return #{ 581 | $adt__compile $ctx $name $body ($(($derivs)) ...) 582 | } 583 | } 584 | case { _ $ctx $name $body deriving $deriv:expr } => { 585 | return #{ 586 | $adt__compile $ctx $name $body (($deriv)) 587 | } 588 | } 589 | case { _ $ctx $name $body deriving } => { 590 | throwSyntaxError('adt-simple', 'Expected deriver', #{ $ctx }); 591 | } 592 | case { _ $ctx $name $body } => { 593 | return #{ 594 | $adt__compile $ctx $name $body () 595 | } 596 | } 597 | } 598 | 599 | let union = macro { 600 | case { $ctx $name:ident { $body ... } } => { 601 | return #{ 602 | $adt__deriving $ctx $name {$body ...} 603 | } 604 | } 605 | case { _ } => { 606 | return #{ union } 607 | } 608 | } 609 | 610 | let data = macro { 611 | case { $ctx $name:ident { $fields ... } } => { 612 | return #{ 613 | $adt__deriving $ctx $name {$name { $fields ... }} 614 | } 615 | } 616 | case { $ctx $name:ident ( $fields ... ) } => { 617 | return #{ 618 | $adt__deriving $ctx $name {$name ($fields ... )} 619 | } 620 | } 621 | case { $ctx $name:ident = $value:expr } => { 622 | return #{ 623 | $adt__deriving $ctx $name {$name = $value} 624 | } 625 | } 626 | case { $ctx $name:ident } => { 627 | return #{ 628 | $adt__deriving $ctx $name {$name} 629 | } 630 | } 631 | case { _ } => { 632 | return #{ data } 633 | } 634 | } 635 | 636 | export union; 637 | export data; 638 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adt-simple", 3 | "version": "0.1.3", 4 | "description": "Algebraic data types for JavaScript using Sweet.js macros", 5 | "main": "./adt-simple.js", 6 | "scripts": { 7 | "test": "grunt test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/natefaubion/adt-simple.git" 12 | }, 13 | "keywords": [ 14 | "adt", 15 | "adts", 16 | "algebraic", 17 | "data", 18 | "types", 19 | "functional", 20 | "macro", 21 | "macros", 22 | "sweet", 23 | "sweet.js", 24 | "sweet-macros" 25 | ], 26 | "author": "Nathan Faubion ", 27 | "license": "MIT", 28 | "readmeFilename": "README.md", 29 | "devDependencies": { 30 | "grunt": "~0.4.1", 31 | "sweet.js": "~0.6.0", 32 | "chai": "~1.8.0", 33 | "grunt-mocha-test": "~0.7.0", 34 | "grunt-contrib-uglify": "~0.2.7" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/compiler.js: -------------------------------------------------------------------------------- 1 | // Compiler 2 | // -------- 3 | 4 | var isData = unwrapSyntax(ctx) === 'data'; 5 | var isUnion = unwrapSyntax(ctx) === 'union'; 6 | 7 | function compile(tmpls, derivers) { 8 | letstx $parentName = [makeIdent(unwrapSyntax(name), here)]; 9 | letstx $ctrs ... = compileConstructors(tmpls); 10 | letstx $derived ... = derivers.length ? compileDeriving(tmpls, derivers) : []; 11 | letstx $export ... = compileExport(tmpls, derivers.length); 12 | letstx $unwrapped ... = options.scoped ? [] : compileUnwrap(tmpls); 13 | 14 | if (isData) { 15 | if (derivers.length) { 16 | var exp = tmpls[0].fields 17 | ? #{ return derived.constructor; } 18 | : #{ return new derived.constructor(); }; 19 | letstx $export ... = exp; 20 | return #{ 21 | var $name = function() { 22 | $ctrs ... 23 | $derived ... 24 | $export ... 25 | }(); 26 | } 27 | } else { 28 | var exp = tmpls[0].fields 29 | ? #{ return $parentName; } 30 | : #{ return new $parentName(); }; 31 | letstx $export ... = exp; 32 | return #{ 33 | var $name = function() { 34 | $ctrs ... 35 | $export ... 36 | }(); 37 | } 38 | } 39 | } else { 40 | var parentBody = []; 41 | if (options.overrideApply) { 42 | parentBody = #{ 43 | if ($parentName.apply !== Function.prototype.apply) { 44 | return $parentName.apply(this, arguments); 45 | } 46 | } 47 | } 48 | letstx $parentBody ... = parentBody; 49 | return #{ 50 | var $name = function() { 51 | function $parentName() { 52 | $parentBody ... 53 | } 54 | $ctrs ... 55 | $derived ... 56 | $export ... 57 | return $parentName; 58 | }(); 59 | $unwrapped ... 60 | } 61 | } 62 | } 63 | 64 | function compileConstructors(tmpls) { 65 | return tmpls.reduce(function(stx, tmpl) { 66 | var res = tmpl.fields 67 | ? compileRecord(tmpl) 68 | : compileSingleton(tmpl); 69 | return stx.concat(res); 70 | }, []); 71 | } 72 | 73 | function compileRecord(tmpl) { 74 | var args = tmpl.fields.reduce(function(acc, f) { 75 | f.arg = [makeIdent(f.arg, here)]; 76 | return acc.concat(f.arg); 77 | }, []); 78 | 79 | var constraints = tmpl.fields.reduce(function(stx, f) { 80 | if (f.constraint.type !== 'literal') { 81 | return stx; 82 | } 83 | f.constraint.ref = makeConstraint(); 84 | return stx.concat(compileConstraint(f.constraint)); 85 | }, []); 86 | 87 | var fields = tmpl.fields.reduce(function(stx, f) { 88 | return stx.concat(compileField(f, tmpl)); 89 | }, []); 90 | 91 | if (tmpl.positional) { 92 | letstx $ctrLength = [makeValue(tmpl.fields.length, here)]; 93 | }; 94 | 95 | letstx $ctrName = [makeIdent(tmpl.name, here)]; 96 | letstx $ctrArgs ... = args; 97 | 98 | var ctrBody; 99 | if (options.newRequired) { 100 | ctrBody = []; 101 | } else { 102 | ctrBody = #{ 103 | if (!(this instanceof $ctrName)) { 104 | return new $ctrName($ctrArgs (,) ...); 105 | } 106 | } 107 | } 108 | 109 | letstx $ctrBody ... = ctrBody; 110 | letstx $ctrFields ... = fields; 111 | letstx $ctrCons ... = constraints; 112 | return #{ 113 | $ctrCons ... 114 | function $ctrName($ctrArgs (,) ...) { 115 | $ctrBody ... 116 | $ctrFields ... 117 | } 118 | }.concat(isData ? [] : #{ 119 | $ctrName.prototype = new $parentName(); 120 | $ctrName.prototype.constructor = $ctrName; 121 | }).concat(!tmpl.positional ? [] : #{ 122 | $ctrName.prototype.length = $ctrLength; 123 | }); 124 | } 125 | 126 | function compileSingleton(tmpl) { 127 | letstx $ctrVal = tmpl.value || []; 128 | var assign = tmpl.value ? #{ this.value = $ctrVal; } : []; 129 | 130 | letstx $ctrName = [makeIdent(tmpl.name, here)]; 131 | letstx $ctrAssign ... = assign; 132 | return #{ 133 | function $ctrName() { 134 | $ctrAssign ... 135 | } 136 | }.concat(isData ? [] : #{ 137 | $ctrName.prototype = new $parentName(); 138 | $ctrName.prototype.constructor = $ctrName; 139 | }); 140 | } 141 | 142 | function compileExport(tmpls, derived) { 143 | return tmpls.reduce(function(stx, tmpl, i) { 144 | letstx $ctrName = [makeIdent(tmpl.name, here)]; 145 | letstx $ctrIndex = [makeValue(i, here)]; 146 | var res; 147 | if (derived) { 148 | letstx $derivedRef = [makeIdent('derived', here)]; 149 | res = tmpl.fields 150 | ? #{ $parentName.$ctrName = $derivedRef.variants[$ctrIndex].constructor; } 151 | : #{ $parentName.$ctrName = new $derivedRef.variants[$ctrIndex].constructor(); } 152 | } else { 153 | res = tmpl.fields 154 | ? #{ $parentName.$ctrName = $ctrName; } 155 | : #{ $parentName.$ctrName = new $ctrName(); }; 156 | } 157 | return stx.concat(res); 158 | }, []); 159 | } 160 | 161 | function compileField(field, record) { 162 | letstx $fieldArg = field.arg; 163 | letstx $fieldName = record.positional 164 | ? [makeKeyword('this', here), makeDelim('[]', [makeValue(field.name, here)], here)] 165 | : [makeKeyword('this', here), makePunc('.', here), makeIdent(field.name, here)]; 166 | if (field.constraint.type === 'any') { 167 | return #{ 168 | $fieldName = $fieldArg; 169 | } 170 | } 171 | if (field.constraint.type === 'class') { 172 | var fullName = isData 173 | ? [record.name, field.name].join('.') 174 | : [unwrapSyntax(name), record.name, field.name].join('.'); 175 | letstx $fieldCheck ... = compileInstanceCheck(field.constraint); 176 | letstx $fieldError = [makeValue('Unexpected type for field: ' + fullName, here)]; 177 | return #{ 178 | if ($fieldCheck ...) { 179 | $fieldName = $fieldArg; 180 | } else { 181 | throw new TypeError($fieldError); 182 | } 183 | } 184 | } 185 | if (field.constraint.type === 'literal') { 186 | letstx $fieldCons = field.constraint.ref; 187 | return #{ 188 | $fieldName = $fieldCons($fieldArg); 189 | } 190 | } 191 | } 192 | 193 | function compileInstanceCheck(cons) { 194 | if (cons.stx.length === 1) { 195 | var name = unwrapSyntax(cons.stx); 196 | switch(name) { 197 | case 'String': 198 | return #{ 199 | typeof $fieldArg === 'string' || 200 | Object.prototype.toString.call($fieldArg) === '[object String]' 201 | } 202 | case 'Number': 203 | return #{ 204 | typeof $fieldArg === 'number' || 205 | Object.prototype.toString.call($fieldArg) === '[object Number]' 206 | } 207 | case 'Boolean': 208 | return #{ 209 | typeof $fieldArg === 'boolean' || 210 | Object.prototype.toString.call($fieldArg) === '[object Boolean]' 211 | } 212 | case 'RegExp': 213 | return #{ 214 | Object.prototype.toString.call($fieldArg) === '[object RegExp]' 215 | } 216 | case 'Date': 217 | return #{ 218 | Object.prototype.toString.call($fieldArg) === '[object Date]' 219 | } 220 | case 'Function': 221 | return #{ 222 | Object.prototype.toString.call($fieldArg) === '[object Function]' 223 | } 224 | case 'Array': 225 | return #{ 226 | Array.isArray 227 | ? Array.isArray($fieldArg) 228 | : Object.prototype.toString.call($fieldArg) === '[object Array]' 229 | } 230 | case 'Object': 231 | return #{ 232 | $fieldArg != null && ($fieldArg = Object($fieldArg)) 233 | } 234 | } 235 | } 236 | 237 | letstx $fieldClass ... = cons.stx; 238 | return #{ 239 | $fieldArg instanceof $fieldClass ... 240 | } 241 | 242 | } 243 | 244 | function compileConstraint(cons) { 245 | letstx $consRef = cons.ref; 246 | letstx $consStx ... = cons.stx; 247 | return #{ 248 | var $consRef = $consStx ...; 249 | } 250 | } 251 | 252 | function compileDeriving(tmpls, derivers) { 253 | var variants = tmpls.reduce(function(stx, tmpl) { 254 | return stx.concat(compileTemplate(tmpl)); 255 | }, []); 256 | 257 | letstx $derivedRef = [makeIdent('derived', here)]; 258 | letstx $nameStr = [makeValue(unwrapSyntax(name), here)]; 259 | letstx $variants ... = variants; 260 | 261 | var template = #{{ 262 | name: $nameStr, 263 | constructor: $parentName, 264 | prototype: $parentName.prototype, 265 | variants: [$variants (,) ...] 266 | }}; 267 | var calls = derivers.reduce(function(stx, d) { 268 | letstx $deriver ... = d; 269 | letstx $deriverArg ... = stx; 270 | return #{ 271 | $deriver ... .derive($deriverArg ...) 272 | } 273 | }, template); 274 | 275 | letstx $derivers ... = calls; 276 | 277 | return #{ 278 | var $derivedRef = $derivers ...; 279 | } 280 | } 281 | 282 | function compileTemplate(tmpl) { 283 | letstx $tmplName = [makeValue(tmpl.name, here)]; 284 | letstx $tmplCtr = [makeIdent(tmpl.name, here)]; 285 | 286 | var res = #{ 287 | { 288 | name: $tmplName, 289 | constructor: $tmplCtr, 290 | prototype: $tmplCtr.prototype 291 | } 292 | }; 293 | 294 | if (tmpl.fields) { 295 | letstx $tmplFields ... = tmpl.fields.map(function(f) { 296 | return makeValue(f.name, here); 297 | }); 298 | res[0].token.inner = res[0].token.inner.concat(#{ 299 | , fields: [$tmplFields (,) ...] 300 | }); 301 | } 302 | 303 | return res; 304 | } 305 | 306 | function compileUnwrap(tmpls) { 307 | return tmpls.reduce(function(stx, tmpl) { 308 | letstx $tmplName = [makeIdent(tmpl.name, ctx)]; 309 | return stx.concat(#{ 310 | var $tmplName = $name.$tmplName; 311 | }); 312 | }, []); 313 | } 314 | -------------------------------------------------------------------------------- /src/letstx.js: -------------------------------------------------------------------------------- 1 | let letstx = macro { 2 | case { $mac $id:ident $punc = $rhs:expr } => { 3 | var mac = #{ $mac }; 4 | var id = #{ $id }; 5 | var val = #{ $val }; 6 | var arg = #{ $($rhs) }; 7 | var punc = #{ $punc }; 8 | var here = #{ here }; 9 | 10 | if (punc[0].token.type !== parser.Token.Punctuator || 11 | punc[0].token.value !== '...') { 12 | throw new SyntaxError('Unexpected token: ' + punc[0].token.value + 13 | ' (expected ...)'); 14 | } 15 | 16 | if (id[0].token.value[0] !== '$') { 17 | throw new SyntaxError('Syntax identifiers must start with $: ' + 18 | id[0].token.value); 19 | } 20 | 21 | return [ 22 | makeIdent('match', mac), 23 | makePunc('.', here), 24 | makeIdent('patternEnv', here), 25 | makeDelim('[]', [makeValue(id[0].token.value, here)], here), 26 | makePunc('=', here), 27 | makeDelim('{}', [ 28 | makeIdent('level', here), makePunc(':', here), makeValue(1, here), makePunc(',', here), 29 | makeIdent('match', here), makePunc(':', here), makeDelim('()', #{ 30 | (function(exp) { 31 | return exp.length 32 | ? exp.map(function(t) { return { level: 0, match: [t] } }) 33 | : [{ level: 0, match: [] }]; 34 | }) 35 | }, here), makeDelim('()', arg, here) 36 | ], here) 37 | ]; 38 | } 39 | case { $mac $id:ident = $rhs:expr } => { 40 | var mac = #{ $mac }; 41 | var id = #{ $id }; 42 | var val = #{ $val }; 43 | var arg = #{ $($rhs) }; 44 | var here = #{ here }; 45 | 46 | if (id[0].token.value[0] !== '$') { 47 | throw new SyntaxError('Syntax identifiers must start with $: ' + 48 | id[0].token.value); 49 | } 50 | 51 | return [ 52 | makeIdent('match', mac), 53 | makePunc('.', here), 54 | makeIdent('patternEnv', here), 55 | makeDelim('[]', [makeValue(id[0].token.value, here)], here), 56 | makePunc('=', here), 57 | makeDelim('{}', [ 58 | makeIdent('level', here), makePunc(':', here), makeValue(0, here), makePunc(',', here), 59 | makeIdent('match', here), makePunc(':', here), arg[0] 60 | ], here) 61 | ]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | var pragmas = { 2 | overrideApply: /@overrideapply\b/gmi, 3 | newRequired: /@newrequired\b/gmi, 4 | scoped: /@scoped\b/gmi 5 | }; 6 | 7 | if (ctx[0].token.leadingComments) { 8 | ctx[0].token.leadingComments.forEach(function(comment) { 9 | Object.keys(pragmas).forEach(function(optName) { 10 | if (comment.value.match(pragmas[optName])) { 11 | options[optName] = true; 12 | } 13 | }); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | // Parser 2 | // ------ 3 | 4 | var T = parser.Token; 5 | var EQ = { type: T.Punctuator, value: '=' }; 6 | var COLON = { type: T.Punctuator, value: ':' }; 7 | var COMMA = { type: T.Punctuator, value: ',' }; 8 | var PERIOD = { type: T.Punctuator, value: '.' }; 9 | var WILDCARD = { type: T.Punctuator, value: '*' }; 10 | var PARENS = { type: T.Delimiter, value: '()' }; 11 | var BRACES = { type: T.Delimiter, value: '{}' }; 12 | var IDENT = { type: T.Identifier }; 13 | var KEYWORD = { type: T.Keyword }; 14 | 15 | function parse(stx) { 16 | var inp = input(stx); 17 | var res = commaSeparated(parseConstructor, inp); 18 | if (res.length === 0) { 19 | syntaxError(null, 'Expected constructor'); 20 | } 21 | return res; 22 | } 23 | 24 | function parseConstructor(inp) { 25 | return parseRecord(inp) 26 | || parsePositional(inp) 27 | || parseSingleton(inp); 28 | } 29 | 30 | function parseRecord(inp) { 31 | var res = inp.takeAPeek(IDENT, BRACES); 32 | if (res) { 33 | return { 34 | name: unwrapSyntax(res[0]), 35 | fields: commaSeparated(parseField, input(res[1].expose().token.inner)) 36 | }; 37 | } 38 | } 39 | 40 | function parsePositional(inp) { 41 | var res = inp.takeAPeek(IDENT, PARENS); 42 | if (res) { 43 | var inp2 = input(res[1].expose().token.inner); 44 | return { 45 | name: unwrapSyntax(res[0]), 46 | positional: true, 47 | fields: commaSeparated(parseConstraint, inp2).map(function(c, i) { 48 | return { name: i.toString(), arg: '_' + i.toString(), constraint: c }; 49 | }) 50 | }; 51 | } 52 | } 53 | 54 | function parseSingleton(inp) { 55 | var res = inp.takeAPeek(IDENT); 56 | var val; 57 | if (res) { 58 | if (inp.takeAPeek(EQ)) { 59 | val = takeUntil(COMMA, inp); 60 | if (!val) syntaxError(inp.back().take(), 'Expected value'); 61 | } 62 | var ret = { name: unwrapSyntax(res[0]) }; 63 | if (val) ret.value = val; 64 | return ret; 65 | } 66 | } 67 | 68 | function parseField(inp) { 69 | var res1 = inp.takeAPeek(IDENT) || inp.takeAPeek(KEYWORD); 70 | if (res1) { 71 | var name = unwrapSyntax(res1[0]); 72 | var arg = res1[0].token.type === T.Keyword ? '_' + name : name; 73 | var res2 = inp.takeAPeek(COLON); 74 | if (res2) { 75 | var cons = parseConstraint(inp); 76 | if (cons) { 77 | return { 78 | name: name, 79 | arg: arg, 80 | constraint: cons 81 | }; 82 | } 83 | syntaxError(res2, 'Expected constraint'); 84 | } else { 85 | return { 86 | name: name, 87 | arg: arg, 88 | constraint: { type: 'any' } 89 | } 90 | } 91 | } 92 | } 93 | 94 | function parseConstraint(inp) { 95 | var res = inp.takeAPeek(WILDCARD); 96 | if (res) return { type: 'any' }; 97 | 98 | res = parseClassName(inp); 99 | if (res) return { type: 'class', stx: res }; 100 | 101 | res = takeUntil(COMMA, inp); 102 | if (res.length) { 103 | var expr = getExpr(res); 104 | if (expr.success && !expr.rest.length) { 105 | return { type: 'literal', stx: expr.result }; 106 | } 107 | syntaxError(expr.success ? expr.rest[0] : res[0]); 108 | } 109 | if (inp.length) { 110 | syntaxError(inp.take()); 111 | } 112 | } 113 | 114 | function parseClassName(inp) { 115 | var stx = [], tok; 116 | while (tok = inp.peek()) { 117 | if (stx.length === 0 && matchesToken(IDENT, tok) || 118 | stx.length && matchesToken(IDENT, stx[0]) && matchesToken(PERIOD, tok) || 119 | stx.length && matchesToken(IDENT, tok) && matchesToken(PERIOD, stx[0])) { 120 | stx.unshift(inp.take()[0]); 121 | } else break; 122 | } 123 | 124 | if (stx.length) { 125 | if (matchesToken(PERIOD, stx[0])) syntaxError(stx[0]); 126 | var name = stx[0].token.value; 127 | if (name[0].toUpperCase() === name[0] && 128 | name[0] !== '$' && name[0] !== '_') { 129 | return stx.reverse(); 130 | } else { 131 | inp.back(stx.length); 132 | } 133 | } 134 | } 135 | 136 | function parseDerivers(stx) { 137 | return stx.map(function(delim) { 138 | return delim.expose().token.inner; 139 | }); 140 | } 141 | 142 | function commaSeparated(parser, inp, cb) { 143 | var all = [], res; 144 | while (inp.length) { 145 | res = parser(inp); 146 | if (res && !cb || res && cb(res, inp)) { 147 | all.push(res); 148 | if (!inp.takeAPeek(COMMA) && inp.length) { 149 | syntaxError(inp.take(), null, 'maybe you meant ,'); 150 | } 151 | } else if (!res) { 152 | syntaxError(inp.take()); 153 | } 154 | } 155 | return all; 156 | } 157 | 158 | function takeUntil(tok, inp) { 159 | var res = []; 160 | while (inp.length && !inp.peek(tok)) { 161 | res.push(inp.take()[0]); 162 | } 163 | return res; 164 | } 165 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | function syntaxError(tok, err, info) { 2 | if (!err) err = 'Unexpected token'; 3 | if (info) err += ' (' + info + ')'; 4 | throwSyntaxError('adt-simple', err, tok); 5 | } 6 | 7 | function matchesToken(tmpl, t) { 8 | if (t && t.length === 1) t = t[0]; 9 | if (!t || tmpl.type && t.token.type !== tmpl.type 10 | || tmpl.value && t.token.value !== tmpl.value) return false; 11 | return true; 12 | } 13 | 14 | function input(stx) { 15 | var pos = 0; 16 | var inp = { 17 | length: stx.length, 18 | buffer: stx, 19 | peek: peek, 20 | take: take, 21 | takeAPeek: takeAPeek, 22 | back: back, 23 | rest: rest 24 | }; 25 | 26 | return inp; 27 | 28 | function peek() { 29 | if (arguments.length === 0) { 30 | return [stx[pos]]; 31 | } 32 | if (typeof arguments[0] === 'number') { 33 | if (inp.length < arguments[0]) return; 34 | return stx.slice(pos, pos + arguments[0]); 35 | } 36 | var res = []; 37 | for (var i = 0, j = pos, t, a, m; i < arguments.length; i++) { 38 | a = arguments[i]; 39 | t = stx[j++]; 40 | if (!matchesToken(a, t)) return; 41 | res.push(t); 42 | } 43 | return res; 44 | } 45 | 46 | function take(len) { 47 | var res = stx.slice(pos, pos + (len || 1)); 48 | pos += len || 1; 49 | inp.length -= len || 1; 50 | return res; 51 | } 52 | 53 | function takeAPeek() { 54 | var res = peek.apply(null, arguments); 55 | if (res) return take(res.length); 56 | } 57 | 58 | function back(len) { 59 | pos -= len || 1; 60 | inp.length += len || 1; 61 | } 62 | 63 | function rest() { 64 | return stx.slice(pos); 65 | } 66 | } 67 | 68 | var cid = 0; 69 | 70 | function makeConstraint() { 71 | return [makeIdent('c' + (++cid), here)]; 72 | } 73 | -------------------------------------------------------------------------------- /src/wrapper.js: -------------------------------------------------------------------------------- 1 | macro $adt__compile { 2 | case { _ $ctx $name $body $derivs } => { 3 | var ctx = #{ $ctx }; 4 | var here = #{ here }; 5 | var name = #{ $name }; 6 | var body = #{ $body }[0].token.inner; 7 | var derivs = #{ $derivs }[0].token.inner; 8 | var options = {}; 9 | 10 | //= letstx.js 11 | //= utils.js 12 | //= options.js 13 | //= parser.js 14 | //= compiler.js 15 | 16 | return compile(parse(body), parseDerivers(derivs)); 17 | } 18 | } 19 | 20 | macro $adt__deriving { 21 | case { _ $ctx $name $body deriving ( $derivs:expr (,) ... ) } => { 22 | return #{ 23 | $adt__compile $ctx $name $body ($(($derivs)) ...) 24 | } 25 | } 26 | case { _ $ctx $name $body deriving $deriv:expr } => { 27 | return #{ 28 | $adt__compile $ctx $name $body (($deriv)) 29 | } 30 | } 31 | case { _ $ctx $name $body deriving } => { 32 | throwSyntaxError('adt-simple', 'Expected deriver', #{ $ctx }); 33 | } 34 | case { _ $ctx $name $body } => { 35 | return #{ 36 | $adt__compile $ctx $name $body () 37 | } 38 | } 39 | } 40 | 41 | let union = macro { 42 | case { $ctx $name:ident { $body ... } } => { 43 | return #{ 44 | $adt__deriving $ctx $name {$body ...} 45 | } 46 | } 47 | case { _ } => { 48 | return #{ union } 49 | } 50 | } 51 | 52 | let data = macro { 53 | case { $ctx $name:ident { $fields ... } } => { 54 | return #{ 55 | $adt__deriving $ctx $name {$name { $fields ... }} 56 | } 57 | } 58 | case { $ctx $name:ident ( $fields ... ) } => { 59 | return #{ 60 | $adt__deriving $ctx $name {$name ($fields ... )} 61 | } 62 | } 63 | case { $ctx $name:ident = $value:expr } => { 64 | return #{ 65 | $adt__deriving $ctx $name {$name = $value} 66 | } 67 | } 68 | case { $ctx $name:ident } => { 69 | return #{ 70 | $adt__deriving $ctx $name {$name} 71 | } 72 | } 73 | case { _ } => { 74 | return #{ data } 75 | } 76 | } 77 | 78 | export union; 79 | export data; 80 | -------------------------------------------------------------------------------- /test/derive.sjs: -------------------------------------------------------------------------------- 1 | var assert = require('chai').assert; 2 | var adt = require('../adt-simple'); 3 | var Eq = adt.Eq; 4 | var Clone = adt.Clone; 5 | var Setter = adt.Setter; 6 | var ToString = adt.ToString; 7 | var ToJSON = adt.ToJSON; 8 | var Curry = adt.Curry; 9 | var Extractor = adt.Extractor; 10 | var Reflect = adt.Reflect; 11 | var Cata = adt.Cata; 12 | var LateDeriving = adt.LateDeriving; 13 | 14 | describe 'Eq' { 15 | data Test(*, *, *) 16 | deriving Eq 17 | 18 | it 'should recursively compare values' { 19 | var a = Test(1, 2, 3); 20 | var b = Test(1, 2, 3); 21 | var c = Test(1, 2, Test(3, 4, 5)); 22 | var d = Test(1, 2, Test(3, 4, 5)); 23 | var e = Test(1, 2, Test(3, 4, 6)); 24 | 25 | test 'shallow success' { a.equals(b) } 26 | test 'shallow failure' { !a.equals(c) } 27 | test 'nested success' { c.equals(d) } 28 | test 'nested failure' { !c.equals(e) } 29 | } 30 | 31 | it 'should compare native values by reference' { 32 | var a = Test([1, 2]); 33 | var b = Test([1, 2]); 34 | var c = Test(a[0]); 35 | 36 | test 'failure' { !a.equals(b) } 37 | test 'success' { a.equals(c) } 38 | } 39 | 40 | it 'should allow override for native comparisons' { 41 | var a = Test(1); 42 | var b = Test(2); 43 | var oldeq = Eq.nativeEquals; 44 | Eq.nativeEquals = function() { return true }; 45 | test 'success' { a.equals(b) } 46 | Eq.nativeEquals = oldeq; 47 | } 48 | } 49 | 50 | describe 'Clone' { 51 | data Test(*) 52 | deriving (Eq, Clone) 53 | 54 | data Test2 55 | deriving Clone 56 | 57 | it 'should clone' { 58 | var a = Test(1); 59 | var b = a.clone(); 60 | 61 | test 'success' { a !== b && a.equals(b) } 62 | } 63 | 64 | it 'should deep clone' { 65 | var a = Test(Test(1)); 66 | var b = a.clone(); 67 | 68 | test 'success' { a.equals(b) } 69 | } 70 | 71 | it 'should return the same instance for singletons' { 72 | var a = Test2; 73 | var b = a.clone(); 74 | 75 | test 'success' { a === b } 76 | } 77 | 78 | it 'should allow override of native cloning' { 79 | var a = Test(1); 80 | var oldclone = Clone.nativeClone; 81 | Clone.nativeClone = function() { return 'foo' }; 82 | test 'success' { a.clone()[0] === 'foo' } 83 | Clone.nativeClone = oldclone; 84 | } 85 | } 86 | 87 | describe 'Setter' { 88 | data Test { 89 | foo: *, 90 | bar: * 91 | } deriving (Eq, Setter) 92 | 93 | it 'should derive create' { 94 | var a = Test(1, 2); 95 | var b = Test.create({ 96 | foo: 1, 97 | bar: 2 98 | }); 99 | 100 | test 'success' { a.equals(b) } 101 | test 'failure' { Test.create({ foo: 1 }) =!= TypeError } 102 | } 103 | 104 | it 'should return a clone with values changed' { 105 | var a = Test(1, Test(2)); 106 | var b = a.set({ foo: 2 }); 107 | 108 | test 'success' { 109 | a !== b && 110 | b.foo === 2 && 111 | b.bar === a.bar 112 | } 113 | } 114 | } 115 | 116 | describe 'ToString' { 117 | union List { 118 | Nil, 119 | Cons(*, List) 120 | } deriving ToString 121 | 122 | it 'should return a good string representation' { 123 | var a = Cons(1, Cons(2, Cons(3, Nil))); 124 | test 'success' { a.toString() === 'Cons(1, Cons(2, Cons(3, Nil)))' } 125 | } 126 | 127 | it 'should return a good representation for arrays' { 128 | var a = Cons([1, 2, 3], Nil); 129 | test 'success' { a.toString() === 'Cons([1, 2, 3], Nil)' } 130 | } 131 | } 132 | 133 | describe 'ToJSON' { 134 | union Test { 135 | Test1, 136 | Test2 = 'test', 137 | Test3 { 138 | foo: *, 139 | bar: * 140 | } 141 | } deriving ToJSON 142 | 143 | it 'should return the constructor name for singletons' { 144 | test 'success' { Test1.toJSON() === 'Test1' } 145 | } 146 | 147 | it 'should return the value for singletons with values' { 148 | test 'success' { Test2.toJSON() === 'test' } 149 | } 150 | 151 | it 'should return an object for records' { 152 | test 'success' { Test3(1, 2).toJSON() =>= { foo: 1, bar: 2 }} 153 | } 154 | } 155 | 156 | describe 'Curry' { 157 | data Test(*, *, *) 158 | deriving (Eq, Curry) 159 | 160 | it 'should curry the constructor' { 161 | var a = Test(1, 2, 3); 162 | var b = Test(1)(2)(3); 163 | 164 | test 'success' { a.equals(b) } 165 | } 166 | 167 | it 'should allow partial application' { 168 | var a = Test(1, 2, 3); 169 | var b = Test(1, 2)(3); 170 | var c = Test(1)(2, 3); 171 | 172 | test 'success' { a.equals(b) && b.equals(c) } 173 | } 174 | 175 | it 'should retain static values' { 176 | var Foo = { 177 | derive: function(adt) { 178 | adt.constructor.test = 'foo'; 179 | return adt; 180 | } 181 | }; 182 | data Test2(*) 183 | deriving (Foo, Curry) 184 | 185 | test 'success' { Test2.test === 'foo' } 186 | } 187 | } 188 | 189 | describe 'Extractor' { 190 | union Test { 191 | Test1, 192 | Test2 { 193 | foo: *, 194 | bar: * 195 | } 196 | } deriving Extractor 197 | 198 | it 'should derive hasInstance for singletons' { 199 | test 'success' { Test1.hasInstance(Test1) } 200 | test 'failure' { !Test1.hasInstance('foo') } 201 | } 202 | 203 | it 'should derive all extractor methods for records' { 204 | test 'hasInstance success' { Test2.hasInstance(Test2(1, 2)) } 205 | test 'hasInstance failure' { !Test2.hasInstance('foo') } 206 | 207 | test 'unapply success' { Test2.unapply(Test2(1, 2)) =>= [1, 2] } 208 | test 'unapply failure' { !Test2.unapply('foo') } 209 | 210 | test 'unapplyObject success' { Test2.unapplyObject(Test2(1, 2)) =>= { foo: 1, bar: 2 }} 211 | test 'unapplyObject failure' { !Test2.unapplyObject('foo') } 212 | } 213 | } 214 | 215 | describe 'Reflect' { 216 | union Test { 217 | Test1, 218 | Test2 { 219 | foo: *, 220 | bar: * 221 | } 222 | } deriving Reflect 223 | 224 | it 'should export type tags' { 225 | test 'singleton' { Test1.isTest1 } 226 | test 'record' { Test2(1, 2).isTest2 } 227 | } 228 | 229 | it 'should export union constructor names' { 230 | test 'success' { Test.__names__ =>= ['Test1', 'Test2'] } 231 | } 232 | 233 | it 'should export record field names' { 234 | test 'success' { Test2.__fields__ =>= ['foo', 'bar'] } 235 | } 236 | } 237 | 238 | describe 'Cata' { 239 | union Test { 240 | Test1, 241 | Test2(*, *), 242 | Test3 243 | } deriving Cata 244 | 245 | it 'should dispatch on data constructor name' { 246 | var dispatch = { 247 | Test1: function() { return 1 }, 248 | Test2: function() { return 2 } 249 | }; 250 | 251 | test 'success' { 252 | Test1.cata(dispatch) === 1 && 253 | Test2(1, 2).cata(dispatch) === 2 254 | } 255 | test 'failure' { 256 | Test3.cata(dispatch) =!= TypeError 257 | } 258 | } 259 | 260 | it 'should destruct constructor arguments' { 261 | test 'success' { 262 | Test2(1, 2).cata({ 263 | Test2: function(x, y) { 264 | return [x, y]; 265 | } 266 | }) =>= [1, 2] 267 | } 268 | } 269 | } 270 | 271 | describe 'LateDeriving' { 272 | data Test1 273 | deriving LateDeriving 274 | 275 | data Test2(*) 276 | deriving LateDeriving 277 | 278 | it 'should allow late deriving' { 279 | Test1.deriving(Eq, Clone); 280 | Test2.deriving(Eq, Clone); 281 | 282 | test 'success' { 283 | Test1.equals(Test1) && Test1.clone() === Test1 && 284 | Test2(1).equals(Test2(1)) && 285 | Test2(1).clone().equals(Test2(1)) 286 | } 287 | } 288 | } 289 | 290 | describe 'composeDeriving' { 291 | var Foo = adt.composeDeriving(Eq, Clone); 292 | 293 | data Test 294 | deriving Foo 295 | 296 | it 'should compose derivers' { 297 | test 'success' { Test.equals(Test) && Test.clone() === Test } 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /test/expand.sjs: -------------------------------------------------------------------------------- 1 | var assert = require('chai').assert; 2 | 3 | describe 'Expansion' { 4 | it 'should support the data macro' { 5 | data Test1 6 | data Test2 = 'test' 7 | data Test3(*, *) 8 | data Test4 { foo: *, bar } 9 | 10 | var posTest = Test3(1, 2); 11 | var recTest = Test4(3, 4); 12 | 13 | test 'singleton, no value' { Test1 } 14 | test 'singleton, w/ value' { Test2.value === 'test' } 15 | test 'positional' { posTest[0] === 1 && posTest[1] === 2 } 16 | test 'record' { recTest.foo === 3 && recTest.bar === 4 } 17 | } 18 | 19 | it 'should support the union macro' { 20 | union Test { 21 | Test1, 22 | Test2 = 'test', 23 | Test3(*, *), 24 | Test4 { foo: *, bar } 25 | } 26 | 27 | var posTest = Test3(1, 2); 28 | var recTest = Test4(3, 4); 29 | 30 | test 'singleton, no value' { Test1 } 31 | test 'singleton, w/ value' { Test2.value === 'test' } 32 | test 'positional' { posTest[0] === 1 && posTest[1] === 2 } 33 | test 'record' { recTest.foo === 3 && recTest.bar === 4 } 34 | 35 | test 'exported on parent' { 36 | Test.Test1 === Test1 && 37 | Test.Test2 === Test2 && 38 | Test.Test3 === Test3 && 39 | Test.Test4 === Test4 40 | } 41 | 42 | test 'inheritance' { 43 | Test1 instanceof Test && 44 | Test2 instanceof Test && 45 | Test3(1, 2) instanceof Test && 46 | Test4(3, 4) instanceof Test 47 | } 48 | } 49 | 50 | it 'should support deriving sugar' { 51 | var TestDerive1 = { 52 | derive: function(adt) { 53 | adt.prototype.test1 = 'foo'; 54 | return adt; 55 | } 56 | }; 57 | 58 | var TestDerive2 = { 59 | derive: function(adt) { 60 | adt.prototype.test2 = 'bar'; 61 | return adt; 62 | } 63 | }; 64 | 65 | data Test1 66 | deriving TestDerive1 67 | 68 | union Test { 69 | Test2 70 | } deriving (TestDerive1, TestDerive2) 71 | 72 | test 'single deriver' { Test1.test1 === 'foo' } 73 | test 'multiple derivers' { Test2.test1 === 'foo' && Test2.test2 === 'bar' } 74 | } 75 | 76 | it 'should support deriving arbitrary expression' { 77 | var TestDerive1 = function(param) { 78 | return { 79 | derive: function(adt) { 80 | adt.prototype.test1 = param; 81 | return adt; 82 | } 83 | } 84 | }; 85 | 86 | data Test1 87 | deriving TestDerive1('foo') 88 | 89 | data Test2 90 | deriving { 91 | derive: function(adt) { 92 | adt.prototype.test2 = 'bar'; 93 | return adt; 94 | } 95 | } 96 | 97 | test 'success' { Test1.test1 === 'foo' && Test2.test2 === 'bar' } 98 | } 99 | 100 | it 'should derive in order' { 101 | var ord = []; 102 | var TestDerive1 = { 103 | derive: function(adt) { 104 | ord.push(1); 105 | return adt; 106 | } 107 | }; 108 | 109 | var TestDerive2 = { 110 | derive: function(adt) { 111 | ord.push(2); 112 | return adt; 113 | } 114 | }; 115 | 116 | var TestDerive3 = { 117 | derive: function(adt) { 118 | ord.push(3); 119 | return adt; 120 | } 121 | }; 122 | 123 | data Test 124 | deriving (TestDerive1, TestDerive2, TestDerive3) 125 | 126 | test 'order' { [1, 2, 3] =>= ord } 127 | } 128 | 129 | it 'should generate an accurate template' { 130 | var tmpl; 131 | var TestDerive = { 132 | derive: function(adt) { 133 | tmpl = adt; 134 | return adt; 135 | } 136 | }; 137 | 138 | union Test { 139 | Test1, 140 | Test2(*, *), 141 | Test3{ foo: *, bar: * } 142 | } deriving TestDerive 143 | 144 | test 'template' { 145 | { 146 | name: 'Test', 147 | constructor: Test, 148 | prototype: Test.prototype, 149 | variants: [ 150 | { 151 | name: 'Test1', 152 | constructor: Test1.constructor, 153 | prototype: Test1.constructor.prototype 154 | }, 155 | { 156 | name: 'Test2', 157 | constructor: Test2, 158 | prototype: Test2.prototype, 159 | fields: ['0', '1'] 160 | }, 161 | { 162 | name: 'Test3', 163 | constructor: Test3, 164 | prototype: Test3.prototype, 165 | fields: ['foo', 'bar'] 166 | }, 167 | ] 168 | } =>= tmpl 169 | } 170 | } 171 | 172 | it 'should support class instance constraints' { 173 | union Foo { 174 | One, 175 | Two 176 | } 177 | 178 | var deep = { 179 | namespace: { 180 | Foo: Foo 181 | } 182 | }; 183 | 184 | data Bar(Foo) 185 | data Baz(deep.namespace.Foo) 186 | 187 | test 'class success' { Bar(One) } 188 | test 'class failure' { Bar(1) =!= TypeError } 189 | 190 | test 'namespace success' { Baz(One) } 191 | test 'namespace failure' { Baz(1) =!= TypeError } 192 | } 193 | 194 | it 'should support function constraints' { 195 | data Foo { 196 | bar: function(x) { 197 | return x.toString(); 198 | } 199 | } 200 | 201 | test 'success' { Foo(42).bar === '42' } 202 | } 203 | 204 | it 'should support the @scoped pragma' { 205 | /* @scoped */ 206 | union Foo { 207 | Bar, 208 | Baz 209 | } 210 | 211 | test 'success' { Foo.Bar && Foo.Baz } 212 | test 'fail 1' { Bar =!= ReferenceError } 213 | test 'fail 2' { Baz =!= ReferenceError } 214 | } 215 | 216 | it 'should support the @newrequired pragma' { 217 | /* @newrequired */ 218 | data Foo { aaa, bbb } 219 | var a = new Foo(1, 2); 220 | var b = Foo(1, 2); 221 | 222 | test 'success' { a instanceof Foo } 223 | test 'fail' { b === void 0 } 224 | } 225 | 226 | it 'should support the @overrideapply pragma' { 227 | /* @overrideapply */ 228 | union Foo { 229 | Bar, 230 | Baz 231 | } 232 | 233 | Foo.apply = function(ctx, args) { 234 | return args[0] ? Bar : Baz; 235 | }; 236 | 237 | test 'success' { Foo(true) === Bar && Foo(false) === Baz } 238 | } 239 | 240 | it 'should support keyword properties' { 241 | data Foo { case, default, class } 242 | var a = Foo(1, 2, 3); 243 | 244 | test 'success' { a.case === 1 && a.default === 2 && a.class === 3 } 245 | } 246 | 247 | it 'should support other ADTs as constraints' { 248 | data Foo(*) 249 | data Bar { a: Foo } 250 | union Baz { 251 | A { a: Foo } 252 | } 253 | 254 | test 'data' { Bar(Foo(1)) } 255 | test 'union' { A(Foo(1)) } 256 | test 'failure' { A(42) =!= TypeError } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /test/macros.sjs: -------------------------------------------------------------------------------- 1 | let describe = macro { 2 | rule { $name { $body ... } } => { 3 | describe($name, function() { $body ... }); 4 | } 5 | } 6 | 7 | let it = macro { 8 | rule { $name { $body ... } } => { 9 | it($name, function() { $body ... }); 10 | } 11 | } 12 | 13 | let test = macro { 14 | rule { $desc { $a:expr =>= $b:expr } } => { 15 | assert.deepEqual($a, $b, $desc); 16 | } 17 | rule { $desc { $a:expr =!= $b:ident ( $str ) } } => { 18 | assert.throws(function(){ $a }, $b, $str, $desc); 19 | } 20 | rule { $desc { $a:expr =!= $b:ident } } => { 21 | assert.throws(function(){ $a }, $b, null, $desc); 22 | } 23 | rule { $desc { $a:expr } } => { 24 | assert($a, $desc); 25 | } 26 | rule {} => { test } 27 | } 28 | --------------------------------------------------------------------------------