Implement ES2015-like destructuring defaults

This let's you do things like:

    fullName = ({first = 'John', last = 'Doe'}) -> "#{first} #{last}"

Note: CoffeeScrits treats `undefined` and `null` the same, and that's true in
the case of destructuring defaults as well, as opposed to ES2015 which only uses
the default value if the target is `undefined`. A similar ES2015 difference
already exists for function parameter defaults. It is important for CoffeeScript
to be consistent with itself.

    fullName2 = (first = 'John', last = 'Doe') -> "#{first} #{last}"
    assert fullName('Bob', null) is fullName2(first: 'Bob', last: null)

Fixes #1558, #3288 and #4005.
This commit is contained in:
Simon Lydell 2015-08-22 21:39:26 +02:00
parent 66716cd730
commit 6d9553a016
9 changed files with 396 additions and 183 deletions

View File

@ -108,9 +108,18 @@
return new Assign(LOC(1)(new Value($1)), $3, 'object'); return new Assign(LOC(1)(new Value($1)), $3, 'object');
}), o('ObjAssignable : INDENT Expression OUTDENT', function() { }), o('ObjAssignable : INDENT Expression OUTDENT', function() {
return new Assign(LOC(1)(new Value($1)), $4, 'object'); return new Assign(LOC(1)(new Value($1)), $4, 'object');
}), o('SimpleObjAssignable = Expression', function() {
return new Assign(LOC(1)(new Value($1)), $3, null, {
operatorToken: LOC(2)(new Literal($2))
});
}), o('SimpleObjAssignable = INDENT Expression OUTDENT', function() {
return new Assign(LOC(1)(new Value($1)), $4, null, {
operatorToken: LOC(2)(new Literal($2))
});
}), o('Comment') }), o('Comment')
], ],
ObjAssignable: [o('Identifier'), o('AlphaNumeric'), o('ThisProperty')], SimpleObjAssignable: [o('Identifier'), o('ThisProperty')],
ObjAssignable: [o('SimpleObjAssignable'), o('AlphaNumeric')],
Return: [ Return: [
o('RETURN Expression', function() { o('RETURN Expression', function() {
return new Return($2); return new Return($2);

View File

@ -1310,8 +1310,13 @@
if (hasDynamic && i < dynamicIndex) { if (hasDynamic && i < dynamicIndex) {
indent += TAB; indent += TAB;
} }
if (prop instanceof Assign && prop.variable instanceof Value && prop.variable.hasProperties()) { if (prop instanceof Assign) {
prop.variable.error('invalid object key'); if (prop.context !== 'object') {
prop.operatorToken.error("unexpected " + prop.operatorToken.value);
}
if (prop.variable instanceof Value && prop.variable.hasProperties()) {
prop.variable.error('invalid object key');
}
} }
if (prop instanceof Value && prop["this"]) { if (prop instanceof Value && prop["this"]) {
prop = new Assign(prop.properties[0].name, prop, 'object'); prop = new Assign(prop.properties[0].name, prop, 'object');
@ -1631,8 +1636,10 @@
this.variable = variable1; this.variable = variable1;
this.value = value1; this.value = value1;
this.context = context; this.context = context;
this.param = options && options.param; if (options == null) {
this.subpattern = options && options.subpattern; options = {};
}
this.param = options.param, this.subpattern = options.subpattern, this.operatorToken = options.operatorToken;
forbidden = (ref3 = (name = this.variable.unwrapAll().value), indexOf.call(STRICT_PROSCRIBED, ref3) >= 0); forbidden = (ref3 = (name = this.variable.unwrapAll().value), indexOf.call(STRICT_PROSCRIBED, ref3) >= 0);
if (forbidden && this.context !== 'object') { if (forbidden && this.context !== 'object') {
this.variable.error("variable name may not be \"" + name + "\""); this.variable.error("variable name may not be \"" + name + "\"");
@ -1713,7 +1720,7 @@
}; };
Assign.prototype.compilePatternMatch = function(o) { Assign.prototype.compilePatternMatch = function(o) {
var acc, assigns, code, expandedIdx, fragments, i, idx, isObject, ivar, j, len1, name, obj, objects, olen, ref, ref3, ref4, ref5, ref6, ref7, ref8, rest, top, val, value, vvar, vvarText; var acc, assigns, code, defaultValue, expandedIdx, fragments, i, idx, isObject, ivar, j, len1, name, obj, objects, olen, ref, ref3, ref4, ref5, ref6, ref7, rest, top, val, value, vvar, vvarText;
top = o.level === LEVEL_TOP; top = o.level === LEVEL_TOP;
value = this.value; value = this.value;
objects = this.variable.base.objects; objects = this.variable.base.objects;
@ -1731,17 +1738,29 @@
} }
isObject = this.variable.isObject(); isObject = this.variable.isObject();
if (top && olen === 1 && !(obj instanceof Splat)) { if (top && olen === 1 && !(obj instanceof Splat)) {
if (obj instanceof Assign) { defaultValue = null;
if (obj instanceof Assign && obj.context === 'object') {
ref3 = obj, (ref4 = ref3.variable, idx = ref4.base), obj = ref3.value; ref3 = obj, (ref4 = ref3.variable, idx = ref4.base), obj = ref3.value;
if (obj instanceof Assign) {
defaultValue = obj.value;
obj = obj.variable;
}
} else { } else {
if (obj instanceof Assign) {
defaultValue = obj.value;
obj = obj.variable;
}
idx = isObject ? obj["this"] ? obj.properties[0].name : obj : new Literal(0); idx = isObject ? obj["this"] ? obj.properties[0].name : obj : new Literal(0);
} }
acc = IDENTIFIER.test(idx.unwrap().value || 0); acc = IDENTIFIER.test(idx.unwrap().value);
value = new Value(value); value = new Value(value);
value.properties.push(new (acc ? Access : Index)(idx)); value.properties.push(new (acc ? Access : Index)(idx));
if (ref5 = obj.unwrap().value, indexOf.call(RESERVED, ref5) >= 0) { if (ref5 = obj.unwrap().value, indexOf.call(RESERVED, ref5) >= 0) {
obj.error("assignment to a reserved word: " + (obj.compile(o))); obj.error("assignment to a reserved word: " + (obj.compile(o)));
} }
if (defaultValue) {
value = new Op('?', value, defaultValue);
}
return new Assign(obj, value, null, { return new Assign(obj, value, null, {
param: this.param param: this.param
}).compileToFragments(o, LEVEL_TOP); }).compileToFragments(o, LEVEL_TOP);
@ -1758,17 +1777,6 @@
for (i = j = 0, len1 = objects.length; j < len1; i = ++j) { for (i = j = 0, len1 = objects.length; j < len1; i = ++j) {
obj = objects[i]; obj = objects[i];
idx = i; idx = i;
if (isObject) {
if (obj instanceof Assign) {
ref6 = obj, (ref7 = ref6.variable, idx = ref7.base), obj = ref6.value;
} else {
if (obj.base instanceof Parens) {
ref8 = new Value(obj.unwrapAll()).cacheReference(o), obj = ref8[0], idx = ref8[1];
} else {
idx = obj["this"] ? obj.properties[0].name : obj;
}
}
}
if (!expandedIdx && obj instanceof Splat) { if (!expandedIdx && obj instanceof Splat) {
name = obj.name.unwrap().value; name = obj.name.unwrap().value;
obj = obj.unwrap(); obj = obj.unwrap();
@ -1798,17 +1806,29 @@
} }
continue; continue;
} else { } else {
name = obj.unwrap().value;
if (obj instanceof Splat || obj instanceof Expansion) { if (obj instanceof Splat || obj instanceof Expansion) {
obj.error("multiple splats/expansions are disallowed in an assignment"); obj.error("multiple splats/expansions are disallowed in an assignment");
} }
if (typeof idx === 'number') { defaultValue = null;
idx = new Literal(expandedIdx || idx); if (obj instanceof Assign && obj.context === 'object') {
acc = false; ref6 = obj, (ref7 = ref6.variable, idx = ref7.base), obj = ref6.value;
if (obj instanceof Assign) {
defaultValue = obj.value;
obj = obj.variable;
}
} else { } else {
acc = isObject && IDENTIFIER.test(idx.unwrap().value || 0); if (obj instanceof Assign) {
defaultValue = obj.value;
obj = obj.variable;
}
idx = isObject ? obj["this"] ? obj.properties[0].name : obj : new Literal(expandedIdx || idx);
} }
name = obj.unwrap().value;
acc = IDENTIFIER.test(idx.unwrap().value);
val = new Value(new Literal(vvarText), [new (acc ? Access : Index)(idx)]); val = new Value(new Literal(vvarText), [new (acc ? Access : Index)(idx)]);
if (defaultValue) {
val = new Op('?', val, defaultValue);
}
} }
if ((name != null) && indexOf.call(RESERVED, name) >= 0) { if ((name != null) && indexOf.call(RESERVED, name) >= 0) {
obj.error("assignment to a reserved word: " + (obj.compile(o))); obj.error("assignment to a reserved word: " + (obj.compile(o)));
@ -2129,6 +2149,9 @@
ref3 = name.objects; ref3 = name.objects;
for (j = 0, len1 = ref3.length; j < len1; j++) { for (j = 0, len1 = ref3.length; j < len1; j++) {
obj = ref3[j]; obj = ref3[j];
if (obj instanceof Assign && (obj.context == null)) {
obj = obj.variable;
}
if (obj instanceof Assign) { if (obj instanceof Assign) {
this.eachName(iterator, obj.value.unwrap()); this.eachName(iterator, obj.value.unwrap());
} else if (obj instanceof Splat) { } else if (obj instanceof Splat) {

File diff suppressed because one or more lines are too long

View File

@ -168,18 +168,27 @@ grammar =
# the ordinary **Assign** is that these allow numbers and strings as keys. # the ordinary **Assign** is that these allow numbers and strings as keys.
AssignObj: [ AssignObj: [
o 'ObjAssignable', -> new Value $1 o 'ObjAssignable', -> new Value $1
o 'ObjAssignable : Expression', -> new Assign LOC(1)(new Value($1)), $3, 'object' o 'ObjAssignable : Expression', -> new Assign LOC(1)(new Value $1), $3, 'object'
o 'ObjAssignable : o 'ObjAssignable :
INDENT Expression OUTDENT', -> new Assign LOC(1)(new Value($1)), $4, 'object' INDENT Expression OUTDENT', -> new Assign LOC(1)(new Value $1), $4, 'object'
o 'SimpleObjAssignable = Expression', -> new Assign LOC(1)(new Value $1), $3, null,
operatorToken: LOC(2)(new Literal $2)
o 'SimpleObjAssignable =
INDENT Expression OUTDENT', -> new Assign LOC(1)(new Value $1), $4, null,
operatorToken: LOC(2)(new Literal $2)
o 'Comment' o 'Comment'
] ]
ObjAssignable: [ SimpleObjAssignable: [
o 'Identifier' o 'Identifier'
o 'AlphaNumeric'
o 'ThisProperty' o 'ThisProperty'
] ]
ObjAssignable: [
o 'SimpleObjAssignable'
o 'AlphaNumeric'
]
# A return statement from a function body. # A return statement from a function body.
Return: [ Return: [
o 'RETURN Expression', -> new Return $2 o 'RETURN Expression', -> new Return $2

View File

@ -951,8 +951,11 @@ exports.Obj = class Obj extends Base
',\n' ',\n'
indent = if prop instanceof Comment then '' else idt indent = if prop instanceof Comment then '' else idt
indent += TAB if hasDynamic and i < dynamicIndex indent += TAB if hasDynamic and i < dynamicIndex
if prop instanceof Assign and prop.variable instanceof Value and prop.variable.hasProperties() if prop instanceof Assign
prop.variable.error 'invalid object key' if prop.context isnt 'object'
prop.operatorToken.error "unexpected #{prop.operatorToken.value}"
if prop.variable instanceof Value and prop.variable.hasProperties()
prop.variable.error 'invalid object key'
if prop instanceof Value and prop.this if prop instanceof Value and prop.this
prop = new Assign prop.properties[0].name, prop, 'object' prop = new Assign prop.properties[0].name, prop, 'object'
if prop not instanceof Comment if prop not instanceof Comment
@ -1168,9 +1171,8 @@ exports.Class = class Class extends Base
# The **Assign** is used to assign a local variable to value, or to set the # The **Assign** is used to assign a local variable to value, or to set the
# property of an object -- including within object literals. # property of an object -- including within object literals.
exports.Assign = class Assign extends Base exports.Assign = class Assign extends Base
constructor: (@variable, @value, @context, options) -> constructor: (@variable, @value, @context, options = {}) ->
@param = options and options.param {@param, @subpattern, @operatorToken} = options
@subpattern = options and options.subpattern
forbidden = (name = @variable.unwrapAll().value) in STRICT_PROSCRIBED forbidden = (name = @variable.unwrapAll().value) in STRICT_PROSCRIBED
if forbidden and @context isnt 'object' if forbidden and @context isnt 'object'
@variable.error "variable name may not be \"#{name}\"" @variable.error "variable name may not be \"#{name}\""
@ -1225,8 +1227,6 @@ exports.Assign = class Assign extends Base
# Brief implementation of recursive pattern matching, when assigning array or # Brief implementation of recursive pattern matching, when assigning array or
# object literals to a value. Peeks at their properties to assign inner names. # object literals to a value. Peeks at their properties to assign inner names.
# See the [ECMAScript Harmony Wiki](http://wiki.ecmascript.org/doku.php?id=harmony:destructuring)
# for details.
compilePatternMatch: (o) -> compilePatternMatch: (o) ->
top = o.level is LEVEL_TOP top = o.level is LEVEL_TOP
{value} = this {value} = this
@ -1239,19 +1239,31 @@ exports.Assign = class Assign extends Base
obj.error 'Destructuring assignment has no target' obj.error 'Destructuring assignment has no target'
isObject = @variable.isObject() isObject = @variable.isObject()
if top and olen is 1 and obj not instanceof Splat if top and olen is 1 and obj not instanceof Splat
# Unroll simplest cases: `{v} = x` -> `v = x.v` # Pick the property straight off the value when theres just one to pick
if obj instanceof Assign # (no need to cache the value into a variable).
defaultValue = null
if obj instanceof Assign and obj.context is 'object'
# A regular object pattern-match.
{variable: {base: idx}, value: obj} = obj {variable: {base: idx}, value: obj} = obj
if obj instanceof Assign
defaultValue = obj.value
obj = obj.variable
else else
if obj instanceof Assign
defaultValue = obj.value
obj = obj.variable
idx = if isObject idx = if isObject
# A shorthand `{a, b, @c} = val` pattern-match.
if obj.this then obj.properties[0].name else obj if obj.this then obj.properties[0].name else obj
else else
# A regular array pattern-match.
new Literal 0 new Literal 0
acc = IDENTIFIER.test idx.unwrap().value or 0 acc = IDENTIFIER.test idx.unwrap().value
value = new Value value value = new Value value
value.properties.push new (if acc then Access else Index) idx value.properties.push new (if acc then Access else Index) idx
if obj.unwrap().value in RESERVED if obj.unwrap().value in RESERVED
obj.error "assignment to a reserved word: #{obj.compile o}" obj.error "assignment to a reserved word: #{obj.compile o}"
value = new Op '?', value, defaultValue if defaultValue
return new Assign(obj, value, null, param: @param).compileToFragments o, LEVEL_TOP return new Assign(obj, value, null, param: @param).compileToFragments o, LEVEL_TOP
vvar = value.compileToFragments o, LEVEL_LIST vvar = value.compileToFragments o, LEVEL_LIST
vvarText = fragmentsToText vvar vvarText = fragmentsToText vvar
@ -1263,18 +1275,7 @@ exports.Assign = class Assign extends Base
vvar = [@makeCode ref] vvar = [@makeCode ref]
vvarText = ref vvarText = ref
for obj, i in objects for obj, i in objects
# A regular array pattern-match.
idx = i idx = i
if isObject
if obj instanceof Assign
# A regular object pattern-match.
{variable: {base: idx}, value: obj} = obj
else
# A shorthand `{a, b, @c} = val` pattern-match.
if obj.base instanceof Parens
[obj, idx] = new Value(obj.unwrapAll()).cacheReference o
else
idx = if obj.this then obj.properties[0].name else obj
if not expandedIdx and obj instanceof Splat if not expandedIdx and obj instanceof Splat
name = obj.name.unwrap().value name = obj.name.unwrap().value
obj = obj.unwrap() obj = obj.unwrap()
@ -1297,15 +1298,29 @@ exports.Assign = class Assign extends Base
assigns.push val.compileToFragments o, LEVEL_LIST assigns.push val.compileToFragments o, LEVEL_LIST
continue continue
else else
name = obj.unwrap().value
if obj instanceof Splat or obj instanceof Expansion if obj instanceof Splat or obj instanceof Expansion
obj.error "multiple splats/expansions are disallowed in an assignment" obj.error "multiple splats/expansions are disallowed in an assignment"
if typeof idx is 'number' defaultValue = null
idx = new Literal expandedIdx or idx if obj instanceof Assign and obj.context is 'object'
acc = no # A regular object pattern-match.
{variable: {base: idx}, value: obj} = obj
if obj instanceof Assign
defaultValue = obj.value
obj = obj.variable
else else
acc = isObject and IDENTIFIER.test idx.unwrap().value or 0 if obj instanceof Assign
defaultValue = obj.value
obj = obj.variable
idx = if isObject
# A shorthand `{a, b, @c} = val` pattern-match.
if obj.this then obj.properties[0].name else obj
else
# A regular array pattern-match.
new Literal expandedIdx or idx
name = obj.unwrap().value
acc = IDENTIFIER.test idx.unwrap().value
val = new Value new Literal(vvarText), [new (if acc then Access else Index) idx] val = new Value new Literal(vvarText), [new (if acc then Access else Index) idx]
val = new Op '?', val, defaultValue if defaultValue
if name? and name in RESERVED if name? and name in RESERVED
obj.error "assignment to a reserved word: #{obj.compile o}" obj.error "assignment to a reserved word: #{obj.compile o}"
assigns.push new Assign(obj, val, null, param: @param, subpattern: yes).compileToFragments o, LEVEL_LIST assigns.push new Assign(obj, val, null, param: @param, subpattern: yes).compileToFragments o, LEVEL_LIST
@ -1503,6 +1518,9 @@ exports.Param = class Param extends Base
# * at-params `@foo` # * at-params `@foo`
return atParam name if name instanceof Value return atParam name if name instanceof Value
for obj in name.objects for obj in name.objects
# * destructured parameter with default value
if obj instanceof Assign and not obj.context?
obj = obj.variable
# * assignments within destructured parameters `{foo:bar}` # * assignments within destructured parameters `{foo:bar}`
if obj instanceof Assign if obj instanceof Assign
@eachName iterator, obj.value.unwrap() @eachName iterator, obj.value.unwrap()

View File

@ -299,6 +299,99 @@ test "destructuring with dynamic keys", ->
eq 3, c eq 3, c
throws -> CoffeeScript.compile '{"#{a}"} = b' throws -> CoffeeScript.compile '{"#{a}"} = b'
test "simple array destructuring defaults", ->
[a = 1] = []
eq 1, a
[a = 2] = [undefined]
eq 2, a
[a = 3] = [null]
eq 3, a
[a = 4] = [0]
eq 0, a
arr = [a = 5]
eq 5, a
arrayEq [5], arr
test "simple object destructuring defaults", ->
{b = 1} = {}
eq b, 1
{b = 2} = {b: undefined}
eq b, 2
{b = 3} = {b: null}
eq b, 3
{b = 4} = {b: 0}
eq b, 0
{b: c = 1} = {}
eq c, 1
{b: c = 2} = {b: undefined}
eq c, 2
{b: c = 3} = {b: null}
eq c, 3
{b: c = 4} = {b: 0}
eq c, 0
test "multiple array destructuring defaults", ->
[a = 1, b = 2, c] = [null, 12, 13]
eq a, 1
eq b, 12
eq c, 13
[a, b = 2, c = 3] = [null, 12, 13]
eq a, null
eq b, 12
eq c, 13
[a = 1, b, c = 3] = [11, 12]
eq a, 11
eq b, 12
eq c, 3
test "multiple object destructuring defaults", ->
{a = 1, b: bb = 2, 'c': c = 3, "#{0}": d = 4} = {"#{'b'}": 12}
eq a, 1
eq bb, 12
eq c, 3
eq d, 4
test "array destructuring defaults with splats", ->
[..., a = 9] = []
eq a, 9
[..., b = 9] = [19]
eq b, 19
test "deep destructuring assignment with defaults", ->
[a, [{b = 1, c = 3}] = [c: 2]] = [0]
eq a, 0
eq b, 1
eq c, 2
test "destructuring assignment with context (@) properties and defaults", ->
a={}; b={}; c={}; d={}; e={}
obj =
fn: () ->
local = [a, {b, c: null}, d]
[@a, {b: @b = b, @c = c}, @d, @e = e] = local
eq undefined, obj[key] for key in ['a','b','c','d','e']
obj.fn()
eq a, obj.a
eq b, obj.b
eq c, obj.c
eq d, obj.d
eq e, obj.e
test "destructuring assignment with defaults single evaluation", ->
callCount = 0
fn = -> callCount++
[a = fn()] = []
eq 0, a
eq 1, callCount
[a = fn()] = [10]
eq 10, a
eq 1, callCount
{a = fn(), b: c = fn()} = {a: 20, b: null}
eq 20, a
eq c, 1
eq callCount, 2
# Existential Assignment # Existential Assignment

View File

@ -776,6 +776,22 @@ test "invalid object keys", ->
@a: 1 @a: 1
^^ ^^
''' '''
assertErrorFormat '''
{a=2}
''', '''
[stdin]:1:3: error: unexpected =
{a=2}
^
'''
test "invalid destructuring default target", ->
assertErrorFormat '''
{'a' = 2} = obj
''', '''
[stdin]:1:6: error: unexpected =
{'a' = 2} = obj
^
'''
test "#4070: lone expansion", -> test "#4070: lone expansion", ->
assertErrorFormat ''' assertErrorFormat '''

View File

@ -140,6 +140,33 @@ test "destructuring in function definition", ->
eq 2, c eq 2, c
) {a: [1], c: 2} ) {a: [1], c: 2}
context = {}
(([{a: [b, c = 2], @d, e = 4}]...) ->
eq 1, b
eq 2, c
eq @d, 3
eq context.d, 3
eq e, 4
).call context, {a: [1], d: 3}
ajax = (url, {
async = true,
beforeSend = (->),
cache = true,
method = 'get',
data = {}
}) ->
{url, async, beforeSend, cache, method, data}
fn = ->
deepEqual ajax('/home', beforeSend: fn, cache: null, method: 'post'), {
url: '/home', async: true, beforeSend: fn, cache: true, method: 'post', data: {}
}
test "#4005: `([a = {}]..., b) ->` weirdness", ->
fn = ([a = {}]..., b) -> [a, b]
deepEqual fn(5), [{}, 5]
test "default values", -> test "default values", ->
nonceA = {} nonceA = {}
nonceB = {} nonceB = {}

View File

@ -77,12 +77,18 @@ test "duplicate formal parameters are prohibited", ->
strict '(@_,@_)->', 'two @params' strict '(@_,@_)->', 'two @params'
strict '(@case,@case)->', 'two @reserved' strict '(@case,@case)->', 'two @reserved'
strict '(_,{_})->', 'param, {param}' strict '(_,{_})->', 'param, {param}'
strict '(_,{_=true})->', 'param, {param=}'
strict '({_,_})->', '{param, param}' strict '({_,_})->', '{param, param}'
strict '({_=true,_})->', '{param=, param}'
strict '(_,[_])->', 'param, [param]' strict '(_,[_])->', 'param, [param]'
strict '(_,[_=true])->', 'param, [param=]'
strict '([_,_])->', '[param, param]' strict '([_,_])->', '[param, param]'
strict '([_=true,_])->', '[param=, param]'
strict '(_,[_]=true)->', 'param, [param]=' strict '(_,[_]=true)->', 'param, [param]='
strict '(_,[_=true]=true)->', 'param, [param=]='
strict '(_,[@_,{_}])->', 'param, [@param, {param}]' strict '(_,[@_,{_}])->', 'param, [@param, {param}]'
strict '(_,[_,{@_}])->', 'param, [param, {@param}]' strict '(_,[_,{@_}])->', 'param, [param, {@param}]'
strict '(_,[_,{@_=true}])->', 'param, [param, {@param=}]'
strict '(_,[_,{_}])->', 'param, [param, {param}]' strict '(_,[_,{_}])->', 'param, [param, {param}]'
strict '(_,[_,{__}])->', 'param, [param, {param2}]' strict '(_,[_,{__}])->', 'param, [param, {param2}]'
strict '(_,[__,{_}])->', 'param, [param2, {param}]' strict '(_,[__,{_}])->', 'param, [param2, {param}]'
@ -95,7 +101,9 @@ test "duplicate formal parameters are prohibited", ->
strictOk '(_,@_ = true)->' strictOk '(_,@_ = true)->'
strictOk '(@_,{_})->' strictOk '(@_,{_})->'
strictOk '({_,@_})->' strictOk '({_,@_})->'
strictOk '({_,@_ = true})->'
strictOk '([_,@_])->' strictOk '([_,@_])->'
strictOk '([_,@_ = true])->'
strictOk '({},_arg)->' strictOk '({},_arg)->'
strictOk '({},{})->' strictOk '({},{})->'
strictOk '([]...,_arg)->' strictOk '([]...,_arg)->'