Permalink
Fetching contributors…
Cannot retrieve contributors at this time
537 lines (465 sloc) 12.3 KB
var assert = require('chai').assert;
// Custom operators in `test` blocks:
// =>= Deep equals
// =!= Throws exception (lhs will be wrapped in a function)
describe 'Case patterns' {
// Wildcard
// --------
it 'should match anything for wildcards' {
function go {
(1, 2) => 1,
(1, *) => 2
}
test 'match' { go(1, 2) === 1 }
test 'wildcard' { go(1, 5) === 2 }
}
// Literals
// --------
it 'should match literals' {
function go {
42 => 42,
true => true,
'foo' => 'foo',
null => null,
undefined => undefined
}
test 'numbers' { go(42) === 42 }
test 'booleans' { go(true) === true }
test 'strings' { go('foo') === 'foo' }
test 'null' { go(null) === null }
test 'undefined' { go(undefined) === undefined }
}
// Builtins
// --------
it 'should match builtin types' {
function go {
Boolean => 'boolean',
NaN => 'nan',
Number => 'number',
String => 'string',
RegExp => 'regexp',
Date => 'date',
Array => 'array',
Object => 'object',
Function => 'function',
Math => 'math'
}
test 'Boolean' { go(true) === 'boolean' }
test 'Nan' { go(0/0) === 'nan' }
test 'Number' { go(42) === 'number' }
test 'String' { go('foo') === 'string' }
test 'RegExp' { go(/foo/) === 'regexp' }
test 'Date' { go(new Date()) === 'date' }
test 'Array' { go([]) === 'array' }
test 'Object' { go({}) === 'object' }
test 'Function' { go(go) === 'function' }
test 'Math' { go(Math) === 'math' }
}
// Identifiers
// -----------
it 'should match identifiers' {
function go1 { x => x }
function go2 { _x => _x }
function go3 { $x => $x }
test 'x' { go1(1) === 1 }
test '_x' { go2(2) === 2 }
test '$x' { go3(3) === 3 }
}
// Binders
// -------
it 'should match binder patterns' {
function go {
x @ 1 => x,
y @ String => y,
z @ [1, 2, 3] => z
}
test 'literals' { go(1) === 1 }
test 'types' { go('foo') === 'foo' }
test 'destruct' { go([1, 2, 3]) =>= [1, 2, 3] }
}
// Arrays
// ------
it 'should destructure array literals strictly' {
function go {
[x] => x,
[*, x] => x,
[*, *, x] => x
}
test 'length == 1' { go([1]) === 1 }
test 'length == 2' { go([1, 2]) === 2 }
test 'length == 3' { go([1, 2, 3]) === 3 }
test 'no match' { go([1, 2, 3, 4]) =!= TypeError }
}
it 'should collect array rest values' {
function go1 {
[...a, b, c] => [a, b, c]
}
function go2 {
[a, ...b, c] => [a, b, c]
}
function go3 {
[a, b, ...c] => [a, b, c]
}
test 'head' { go1([1, 2, 3, 4]) =>= [[1, 2], 3, 4] }
test 'mid' { go2([1, 2, 3, 4]) =>= [1, [2, 3], 4] }
test 'tail' { go3([1, 2, 3, 4]) =>= [1, 2, [3, 4]] }
}
// Objects
// -------
it 'should destructure object literals loosely' {
function go {
{ x: 1 } => true,
{ 'a': 2 } => true
}
test 'only 1 key' { go({ x: 1 }) }
test 'many keys' { go({ x: 1, y: 2, z: 3 }) }
test 'no keys' { go({}) =!= TypeError }
test 'string key' { go({ a: 2 }) }
}
it 'should bring object identifier keys into scope' {
function go {
{ x, y, z } => x + y + z
}
test 'ident key' { go({ x: 'x', y: 'y', z: 'z' }) === 'xyz' }
}
it 'should bring object binder keys into scope' {
function go {
{ a @ String } => a
}
test 'binder key' { go({ a: 'a' }) === 'a' }
test 'binder key match error' { go({ a: 42 }) =!= TypeError }
}
it 'should check object for a key when using a plain key string' {
function go {
{ 'x', y } => y
}
test 'key is there' { go({ x: 1, y: 'y' }) === 'y' }
test 'key isnt there' { go({ y: 'y' }) =!= TypeError }
}
it 'should box primitive values when destructuring objects' {
function go {
x @ { toString: Function } => x.toString()
}
test 'success' { go(42) === '42' }
}
// Extractors
// ----------
var Digits = {
hasInstance: function {
a @ Number if a < 1000 => true,
* => false
},
unapply: function(x) {
if (Digits.hasInstance(x)) {
return Math.floor(x).toString().split('').map(parseFloat);
}
},
unapplyObject: function(x) {
var ds = Digits.unapply(x);
if (ds) {
return {
hundreds: ds[0],
tens: ds[1],
ones: ds[2]
};
}
}
};
var deep = {
namespace: {
Digits: Digits
}
};
function Foo() {}
it 'should call hasInstance for bare extractors' {
function go {
Digits => true,
* => false
}
test 'success' { go(100) }
test 'failure' { !go(1001) }
}
it 'should fallback to instanceof for bare extractors' {
function go {
Foo => true,
* => false
}
test 'success' { go(new Foo()) }
test 'failure' { !go(12) }
}
it 'should call unapply for array-like destructuring' {
function go {
Digits(h, t, o) => [h, t, o]
}
test 'success' { go(345) =>= [3, 4, 5] }
test 'failure' { go(1001) =!= TypeError }
}
it 'should call unapplyObject for object-like destructuring' {
function go {
Digits{ hundreds: h, tens: t, ones: o } => [h, t, o]
}
test 'success' { go(345) =>= [3, 4, 5] }
test 'failure' { go(1001) =!= TypeError }
}
it 'should call extractors within a deep namespace' {
function go {
deep.namespace.Digits => true
}
test 'success' { go(345) }
}
// Arguments
// ---------
it 'should allow multiple arguments' {
function go {
(x, y, z) => x + y + z
}
test 'success' { go('x', 'y', 'z') === 'xyz' }
}
it 'should match argument length loosely' {
function go {
(x, y, z) => x + y + z
}
test 'success' { go('x', 'y') === 'xyundefined' }
}
it 'should match unit strictly' {
function go {
() => true
}
test 'success' { go() }
test 'failure' { go(1) =!= TypeError }
}
it 'should match on the first successful case when ambiguous' {
function go {
(a) => 1,
(a, b) => 2
}
test 'success' { go(1) === 1 }
}
it 'should collect rest argument values' {
function go {
(a, b, ...c) => [a, b, c]
}
test 'success' { go(1, 2, 3, 4) =>= [1, 2, [3, 4]] }
test 'empty' { go(1, 2) =>= [1, 2, []] }
}
// Rest Patterns
// -------------
it 'should map patterns when combined with rest' {
function go {
[...Number] => true,
* => false
}
test 'success' { go([1, 2, 3]) }
test 'failure' { !go([1, 2, '3']) }
}
it 'should allow and collect nested rest patterns' {
function go {
[...[heads, ...tails]] => [heads, tails]
}
test 'success' {
go([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]) =>= [
[1, 4, 7],
[
[2, 3],
[5, 6],
[8, 9],
]
]
}
}
// Match keyword
// -------------
it 'should support match expressions' {
function go(test) {
return match test {
x @ Number => x.toString(),
* => null
}
}
test 'success' { go(42) === '42' && go(true) === null }
}
it 'should preserve the lexical `this` context with match expressions' {
var foo = {
y: 42,
go: function(x) {
return match x {
* => this.y
}
}
};
test 'success' { foo.go(1) === 42 }
}
it 'should support match statements' {
function go(test) {
var a;
match test {
case x @ Number: a = test;
default: a = null;
}
return a;
}
test 'success' { go(42) === 42 }
}
it 'should support early return in match statements' {
function go(test) {
match test {
case x @ Number: return x;
default: null;
}
return 'foo';
}
test 'success' { go(42) === 42 && go('bar') === 'foo' }
}
it 'should support break/continue in match statements' {
function go(test) {
while (test < 10) {
match test {
case 1:
test += 1;
continue;
case 2:
test += 1;
break;
}
}
return test;
}
test 'success' { go(1) === 3 }
}
it 'should support the default keyword' {
function go1 {
1 => 2,
default => 42,
}
function go2(x) {
return match x {
1 => 2,
default => 42
}
}
function go3(x) {
match x {
case 1: return 2;
default: return 42;
}
}
test 'success 1' { go1(1) === 2 && go1(2) === 42 }
test 'success 2' { go2(1) === 2 && go2(2) === 42 }
test 'success 3' { go3(1) === 2 && go3(2) === 42 }
}
// Backtracking
// ------------
function Counter(c) {
return {
count: 0,
unapply: function(x) {
this.count += 1;
var arr = [], i = c;
while (i--) arr.unshift(i + 1);
return arr;
}
}
}
it 'should backtrack in match expressions' {
var C2 = Counter(2);
var m = match null {
C2(C2(1, 3), *) => 1,
C2(C2(1, 2), *) => 2
}
test 'success' { C2.count === 2 && m === 2 }
}
it 'should backtrack in match statements' {
var C2 = Counter(2);
var m;
match null {
case C2(C2(1, 3), *): m = 1;
case C2(C2(1, 2), *): m = 2;
}
test 'success' { C2.count === 2 && m === 2 }
}
it 'should work with complex patterns in match expressions' {
function go(test) {
return match test {
['black', ['red', ['red', a, x, b], y, c], z, d] => [1, a, x, b, y, c, z, d],
['black', ['red', a, x, ['red', b, y, c]], z, d] => [2, a, x, b, y, c, z, d],
['black', a, x, ['red', ['red', b, y, c], z, d]] => [3, a, x, b, y, c, z, d],
['black', a, x, ['red', b, y, ['red', c, z, d]]] => [4, a, x, b, y, c, z, d]
}
}
test 'success 1' {
go(['black', ['red', ['red', 1, 2, 3], 4, 5], 6, 7]) =>= [1, 1, 2, 3, 4, 5, 6, 7]
}
test 'success 2' {
go(['black', ['red', 1, 2, ['red', 3, 4, 5]], 6, 7]) =>= [2, 1, 2, 3, 4, 5, 6, 7]
}
test 'success 3' {
go(['black', 1, 2, ['red', ['red', 3, 4, 5], 6, 7]]) =>= [3, 1, 2, 3, 4, 5, 6, 7]
}
test 'success 4' {
go(['black', 1, 2, ['red', 3, 4, ['red', 5, 6, 7]]]) =>= [4, 1, 2, 3, 4, 5, 6, 7]
}
}
it 'should work with complex patterns in match statements' {
function go(test) {
var r;
match test {
case ['black', ['red', ['red', a, x, b], y, c], z, d]: r = [1, a, x, b, y, c, z, d];
case ['black', ['red', a, x, ['red', b, y, c]], z, d]: r = [2, a, x, b, y, c, z, d];
case ['black', a, x, ['red', ['red', b, y, c], z, d]]: r = [3, a, x, b, y, c, z, d];
case ['black', a, x, ['red', b, y, ['red', c, z, d]]]: r = [4, a, x, b, y, c, z, d];
}
return r;
}
test 'success 1' {
go(['black', ['red', ['red', 1, 2, 3], 4, 5], 6, 7]) =>= [1, 1, 2, 3, 4, 5, 6, 7]
}
test 'success 2' {
go(['black', ['red', 1, 2, ['red', 3, 4, 5]], 6, 7]) =>= [2, 1, 2, 3, 4, 5, 6, 7]
}
test 'success 3' {
go(['black', 1, 2, ['red', ['red', 3, 4, 5], 6, 7]]) =>= [3, 1, 2, 3, 4, 5, 6, 7]
}
test 'success 4' {
go(['black', 1, 2, ['red', 3, 4, ['red', 5, 6, 7]]]) =>= [4, 1, 2, 3, 4, 5, 6, 7]
}
}
// Regressions
// -----------
it 'should preserve case order in lieu of grafting' {
function go {
(*, false, true ) => 1,
(false, true, * ) => 2,
(*, *, false) => 3,
(*, *, true ) => 4
}
test 'success' { go(false, true, false) === 2 }
}
it 'should concat backtrack nodes correctly' {
function go(x, y) {
match (x, y) {
case (Foo, *): return 1;
case (*, Foo): return 2;
}
}
test 'success' { go(new Foo(), 1) === 1 && go(1, new Foo()) === 2 }
}
it 'should correctly clone syntax in guards' {
function id(x) { return x }
function go {
x if x === id(x) => true,
* => false
}
test 'success' { go(1) === true }
}
it 'should match empty arrays' {
function go {
[] => true,
* => false
}
test 'success' { go([]) }
}
}