Merge pull request #2712 from troels/implicit-object-implicit-call-stuff

Implicit object/implicit call interaction handling
This commit is contained in:
Jeremy Ashkenas
2013-02-27 17:32:31 -08:00
9 changed files with 700 additions and 304 deletions

View File

@@ -878,33 +878,6 @@
return ifn;
};
Call.prototype.filterImplicitObjects = function(list) {
var node, nodes, obj, prop, properties, _i, _j, _len, _len1, _ref2;
nodes = [];
for (_i = 0, _len = list.length; _i < _len; _i++) {
node = list[_i];
if (!((typeof node.isObject === "function" ? node.isObject() : void 0) && node.base.generated)) {
nodes.push(node);
continue;
}
obj = null;
_ref2 = node.base.properties;
for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
prop = _ref2[_j];
if (prop instanceof Assign || prop instanceof Comment) {
if (!obj) {
nodes.push(obj = new Obj(properties = [], true));
}
properties.push(prop);
} else {
nodes.push(prop);
obj = null;
}
}
}
return nodes;
};
Call.prototype.compileNode = function(o) {
var arg, args, code, _ref2;
if ((_ref2 = this.variable) != null) {
@@ -913,16 +886,16 @@
if (code = Splat.compileSplattedArray(o, this.args, true)) {
return this.compileSplat(o, code);
}
args = this.filterImplicitObjects(this.args);
args = ((function() {
var _i, _len, _results;
var _i, _len, _ref3, _results;
_ref3 = this.args;
_results = [];
for (_i = 0, _len = args.length; _i < _len; _i++) {
arg = args[_i];
for (_i = 0, _len = _ref3.length; _i < _len; _i++) {
arg = _ref3[_i];
_results.push(arg.compile(o, LEVEL_LIST));
}
return _results;
})()).join(', ');
}).call(this)).join(', ');
if (this.isSuper) {
return this.superReference(o) + (".call(" + (this.superThis(o)) + (args && ', ' + args) + ")");
} else {
@@ -1240,27 +1213,25 @@
Arr.prototype.children = ['objects'];
Arr.prototype.filterImplicitObjects = Call.prototype.filterImplicitObjects;
Arr.prototype.compileNode = function(o) {
var code, obj, objs;
var code, obj;
if (!this.objects.length) {
return '[]';
}
o.indent += TAB;
objs = this.filterImplicitObjects(this.objects);
if (code = Splat.compileSplattedArray(o, objs)) {
if (code = Splat.compileSplattedArray(o, this.objects)) {
return code;
}
code = ((function() {
var _i, _len, _results;
var _i, _len, _ref2, _results;
_ref2 = this.objects;
_results = [];
for (_i = 0, _len = objs.length; _i < _len; _i++) {
obj = objs[_i];
for (_i = 0, _len = _ref2.length; _i < _len; _i++) {
obj = _ref2[_i];
_results.push(obj.compile(o, LEVEL_LIST));
}
return _results;
})()).join(', ');
}).call(this)).join(', ');
if (code.indexOf('\n') >= 0) {
return "[\n" + o.indent + code + "\n" + this.tab + "]";
} else {

View File

@@ -16,7 +16,7 @@
var js;
input = input.replace(/\uFF00/g, '\n');
input = input.replace(/(^|[\r\n]+)(\s*)##?(?:[^#\r\n][^\r\n]*|)($|[\r\n])/, '$1$2$3');
if (/^(\()?\s*(\n\))?$/.test(input)) {
if (/^(\s*|\(\s*\))$/.test(input)) {
return cb(null);
}
try {

View File

@@ -1,9 +1,16 @@
// Generated by CoffeeScript 1.5.0
(function() {
var BALANCED_PAIRS, EXPRESSION_CLOSE, EXPRESSION_END, EXPRESSION_START, IMPLICIT_BLOCK, IMPLICIT_CALL, IMPLICIT_END, IMPLICIT_FUNC, IMPLICIT_UNSPACED_CALL, INVERSES, LINEBREAKS, SINGLE_CLOSERS, SINGLE_LINERS, left, rite, _i, _len, _ref,
var BALANCED_PAIRS, EXPRESSION_CLOSE, EXPRESSION_END, EXPRESSION_START, IMPLICIT_BLOCK, IMPLICIT_CALL, IMPLICIT_END, IMPLICIT_FUNC, IMPLICIT_UNSPACED_CALL, INVERSES, LINEBREAKS, SINGLE_CLOSERS, SINGLE_LINERS, generate, left, rite, _i, _len, _ref,
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
__slice = [].slice;
generate = function(tag, value) {
var tok;
tok = [tag, value];
tok.generated = true;
return tok;
};
exports.Rewriter = (function() {
function Rewriter() {}
@@ -16,8 +23,7 @@
this.closeOpenIndexes();
this.addImplicitIndentation();
this.tagPostfixConditionals();
this.addImplicitBraces();
this.addImplicitParentheses();
this.addImplicitBracesAndParens();
this.addLocationDataToGeneratedTokens();
return this.tokens;
};
@@ -112,140 +118,237 @@
});
};
Rewriter.prototype.addImplicitBraces = function() {
var action, condition, sameLine, stack, start, startIndent, startIndex, startsLine;
stack = [];
start = null;
startsLine = null;
sameLine = true;
startIndent = 0;
startIndex = 0;
condition = function(token, i) {
var one, tag, three, two, _ref, _ref1;
_ref = this.tokens.slice(i + 1, +(i + 3) + 1 || 9e9), one = _ref[0], two = _ref[1], three = _ref[2];
if ('HERECOMMENT' === (one != null ? one[0] : void 0)) {
Rewriter.prototype.matchTags = function() {
var fuzz, i, j, pattern, _i, _ref, _ref1;
i = arguments[0], pattern = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
fuzz = 0;
for (j = _i = 0, _ref = pattern.length; 0 <= _ref ? _i < _ref : _i > _ref; j = 0 <= _ref ? ++_i : --_i) {
while (this.tag(i + j + fuzz) === 'HERECOMMENT') {
fuzz += 2;
}
if (pattern[j] == null) {
continue;
}
if (typeof pattern[j] === 'string') {
pattern[j] = [pattern[j]];
}
if (_ref1 = this.tag(i + j + fuzz), __indexOf.call(pattern[j], _ref1) < 0) {
return false;
}
tag = token[0];
if (__indexOf.call(LINEBREAKS, tag) >= 0) {
sameLine = false;
}
return (((tag === 'TERMINATOR' || tag === 'OUTDENT') || (__indexOf.call(IMPLICIT_END, tag) >= 0 && sameLine && !(i - startIndex === 1))) && ((!startsLine && this.tag(i - 1) !== ',') || !((two != null ? two[0] : void 0) === ':' || (one != null ? one[0] : void 0) === '@' && (three != null ? three[0] : void 0) === ':'))) || (tag === ',' && one && ((_ref1 = one[0]) !== 'IDENTIFIER' && _ref1 !== 'NUMBER' && _ref1 !== 'STRING' && _ref1 !== '@' && _ref1 !== 'TERMINATOR' && _ref1 !== 'OUTDENT'));
};
action = function(token, i) {
var tok;
tok = this.generate('}', '}');
return this.tokens.splice(i, 0, tok);
};
return this.scanTokens(function(token, i, tokens) {
var ago, idx, prevTag, tag, tok, value, _ref, _ref1;
if (_ref = (tag = token[0]), __indexOf.call(EXPRESSION_START, _ref) >= 0) {
stack.push([(tag === 'INDENT' && this.tag(i - 1) === '{' ? '{' : tag), i]);
return 1;
}
if (__indexOf.call(EXPRESSION_END, tag) >= 0) {
start = stack.pop();
return 1;
}
if (!(tag === ':' && ((ago = this.tag(i - 2)) === ':' || ((_ref1 = stack[stack.length - 1]) != null ? _ref1[0] : void 0) !== '{'))) {
return 1;
}
sameLine = true;
startIndex = i + 1;
stack.push(['{']);
idx = ago === '@' ? i - 2 : i - 1;
while (this.tag(idx - 2) === 'HERECOMMENT') {
idx -= 2;
}
prevTag = this.tag(idx - 1);
startsLine = !prevTag || (__indexOf.call(LINEBREAKS, prevTag) >= 0);
value = new String('{');
value.generated = true;
tok = this.generate('{', value);
tokens.splice(idx, 0, tok);
this.detectEnd(i + 2, condition, action);
return 2;
});
}
return true;
};
Rewriter.prototype.addImplicitParentheses = function() {
var action, callIndex, condition, noCall, seenControl, seenSingle;
noCall = seenSingle = seenControl = false;
callIndex = null;
condition = function(token, i) {
var post, tag, _ref, _ref1;
tag = token[0];
if (!seenSingle && token.fromThen) {
return true;
Rewriter.prototype.looksObjectish = function(j) {
return this.matchTags(j, '@', null, ':') || this.matchTags(j, null, ':');
};
Rewriter.prototype.findTagsBackwards = function(i, tags) {
var backStack, _ref, _ref1, _ref2, _ref3, _ref4, _ref5;
backStack = [];
while (i >= 0 && (backStack.length || (_ref2 = this.tag(i), __indexOf.call(tags, _ref2) < 0) && ((_ref3 = this.tag(i), __indexOf.call(EXPRESSION_START, _ref3) < 0) || this.tokens[i].generated) && (_ref4 = this.tag(i), __indexOf.call(LINEBREAKS, _ref4) < 0))) {
if (_ref = this.tag(i), __indexOf.call(EXPRESSION_END, _ref) >= 0) {
backStack.push(this.tag(i));
}
if (tag === 'IF' || tag === 'ELSE' || tag === 'CATCH' || tag === '->' || tag === '=>' || tag === 'CLASS') {
seenSingle = true;
if ((_ref1 = this.tag(i), __indexOf.call(EXPRESSION_START, _ref1) >= 0) && backStack.length) {
backStack.pop();
}
if (tag === 'IF' || tag === 'ELSE' || tag === 'SWITCH' || tag === 'TRY' || tag === '=') {
seenControl = true;
}
if ((tag === '.' || tag === '?.' || tag === '::') && this.tag(i - 1) === 'OUTDENT') {
return true;
}
return !token.generated && this.tag(i - 1) !== ',' && (__indexOf.call(IMPLICIT_END, tag) >= 0 || (tag === 'INDENT' && !seenControl)) && (tag !== 'INDENT' || (((_ref = this.tag(i - 2)) !== 'CLASS' && _ref !== 'EXTENDS') && (_ref1 = this.tag(i - 1), __indexOf.call(IMPLICIT_BLOCK, _ref1) < 0) && !(callIndex === i - 1 && (post = this.tokens[i + 1]) && post.generated && post[0] === '{')));
};
action = function(token, i) {
return this.tokens.splice(i, 0, this.generate('CALL_END', ')'));
};
i -= 1;
}
return _ref5 = this.tag(i), __indexOf.call(tags, _ref5) >= 0;
};
Rewriter.prototype.addImplicitBracesAndParens = function() {
var stack;
stack = [];
return this.scanTokens(function(token, i, tokens) {
var callObject, current, next, prev, tag, _ref, _ref1, _ref2;
var endImplicitCall, endImplicitObject, forward, inImplicit, inImplicitCall, inImplicitControl, inImplicitObject, nextTag, prevTag, s, sameLine, stackIdx, stackTag, stackTop, startIdx, startImplicitCall, startImplicitObject, startsLine, tag, _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6;
tag = token[0];
if (tag === 'CLASS' || tag === 'IF' || tag === 'FOR' || tag === 'WHILE') {
noCall = true;
prevTag = (i > 0 ? tokens[i - 1] : [])[0];
nextTag = (i < tokens.length - 1 ? tokens[i + 1] : [])[0];
stackTop = function() {
return stack[stack.length - 1];
};
startIdx = i;
forward = function(n) {
return i - startIdx + n;
};
inImplicit = function() {
var _ref, _ref1;
return (_ref = stackTop()) != null ? (_ref1 = _ref[2]) != null ? _ref1.ours : void 0 : void 0;
};
inImplicitCall = function() {
var _ref;
return inImplicit() && ((_ref = stackTop()) != null ? _ref[0] : void 0) === '(';
};
inImplicitObject = function() {
var _ref;
return inImplicit() && ((_ref = stackTop()) != null ? _ref[0] : void 0) === '{';
};
inImplicitControl = function() {
var _ref;
return inImplicit && ((_ref = stackTop()) != null ? _ref[0] : void 0) === 'CONTROL';
};
startImplicitCall = function(j) {
var idx;
idx = j != null ? j : i;
stack.push([
'(', idx, {
ours: true
}
]);
tokens.splice(idx, 0, generate('CALL_START', '('));
if (j == null) {
return i += 1;
}
};
endImplicitCall = function() {
stack.pop();
tokens.splice(i, 0, generate('CALL_END', ')'));
return i += 1;
};
startImplicitObject = function(j, startsLine) {
var idx;
if (startsLine == null) {
startsLine = true;
}
idx = j != null ? j : i;
stack.push([
'{', idx, {
sameLine: true,
startsLine: startsLine,
ours: true
}
]);
tokens.splice(idx, 0, generate('{', generate(new String('{'))));
if (j == null) {
return i += 1;
}
};
endImplicitObject = function() {
stack.pop();
tokens.splice(i, 0, generate('}', '}'));
return i += 1;
};
if (inImplicitCall() && (tag === 'IF' || tag === 'CLASS' || tag === 'SWITCH' || tag === 'CATCH')) {
stack.push([
'CONTROL', i, {
ours: true
}
]);
return forward(1);
}
_ref = tokens.slice(i - 1, +(i + 1) + 1 || 9e9), prev = _ref[0], current = _ref[1], next = _ref[2];
callObject = !noCall && tag === 'INDENT' && next && next.generated && next[0] === '{' && prev && (_ref1 = prev[0], __indexOf.call(IMPLICIT_FUNC, _ref1) >= 0);
seenSingle = false;
seenControl = false;
if (__indexOf.call(LINEBREAKS, tag) >= 0) {
noCall = false;
if (tag === 'INDENT' && inImplicit()) {
if (prevTag !== '=>' && prevTag !== '->' && prevTag !== '[' && prevTag !== '(' && prevTag !== ',' && prevTag !== '{' && prevTag !== 'TRY' && prevTag !== 'ELSE' && prevTag !== '=') {
while (inImplicitCall()) {
endImplicitCall();
}
}
if (inImplicitControl()) {
stack.pop();
}
stack.push([tag, i]);
return forward(1);
}
if (prev && !prev.spaced && tag === '?') {
token.call = true;
if (__indexOf.call(EXPRESSION_START, tag) >= 0) {
stack.push([tag, i]);
return forward(1);
}
if (token.fromThen) {
return 1;
if (__indexOf.call(EXPRESSION_END, tag) >= 0) {
while (inImplicit()) {
if (inImplicitCall()) {
endImplicitCall();
} else if (inImplicitObject()) {
endImplicitObject();
} else {
stack.pop();
}
}
stack.pop();
}
if (!(callObject || (prev != null ? prev.spaced : void 0) && (prev.call || (_ref2 = prev[0], __indexOf.call(IMPLICIT_FUNC, _ref2) >= 0)) && (__indexOf.call(IMPLICIT_CALL, tag) >= 0 || !(token.spaced || token.newLine) && __indexOf.call(IMPLICIT_UNSPACED_CALL, tag) >= 0))) {
return 1;
if ((__indexOf.call(IMPLICIT_FUNC, tag) >= 0 && token.spaced || tag === '?' && i > 0 && !tokens[i - 1].spaced) && (__indexOf.call(IMPLICIT_CALL, nextTag) >= 0 || __indexOf.call(IMPLICIT_UNSPACED_CALL, nextTag) >= 0 && !((_ref = tokens[i + 1]) != null ? _ref.spaced : void 0) && !((_ref1 = tokens[i + 1]) != null ? _ref1.newLine : void 0))) {
if (tag === '?') {
tag = token[0] = 'FUNC_EXIST';
}
startImplicitCall(i + 1);
return forward(2);
}
callIndex = i;
tokens.splice(i, 0, this.generate('CALL_START', '(', token[2]));
this.detectEnd(i + 1, condition, action);
if (prev[0] === '?') {
prev[0] = 'FUNC_EXIST';
if (this.matchTags(i, IMPLICIT_FUNC, 'INDENT') && ((_ref2 = stackTop()) != null ? _ref2[0] : void 0) !== '[' && !this.findTagsBackwards(i, ['CLASS', 'EXTENDS', 'IF', 'CATCH', 'SWITCH', 'LEADING_WHEN', 'FOR', 'WHILE', 'UNTIL'])) {
startImplicitCall(i + 1);
stack.push(['INDENT', i + 2]);
return forward(3);
}
return 2;
if (tag === ':') {
if (this.tag(i - 2) === '@') {
s = i - 2;
} else {
s = i - 1;
}
while (this.tag(s - 2) === 'HERECOMMENT') {
s -= 2;
}
startsLine = s === 0 || (_ref3 = this.tag(s - 1), __indexOf.call(LINEBREAKS, _ref3) >= 0) || tokens[s - 1].newLine;
if (stackTop()) {
_ref4 = stackTop(), stackTag = _ref4[0], stackIdx = _ref4[1];
if ((stackTag === '{' || stackTag === 'INDENT' && this.tag(stackIdx - 1) === '{') && (startsLine || this.tag(s - 1) === ',' || this.tag(s - 1) === '{')) {
return forward(1);
}
}
startImplicitObject(s, !!startsLine);
return forward(2);
}
if (prevTag === 'OUTDENT' && inImplicitCall() && (tag === '.' || tag === '?.' || tag === '::')) {
endImplicitCall();
return forward(1);
}
if (inImplicitObject() && __indexOf.call(LINEBREAKS, tag) >= 0) {
stackTop()[2].sameLine = false;
}
if (__indexOf.call(IMPLICIT_END, tag) >= 0) {
while (inImplicit()) {
_ref5 = stackTop(), stackTag = _ref5[0], stackIdx = _ref5[1], (_ref6 = _ref5[2], sameLine = _ref6.sameLine, startsLine = _ref6.startsLine);
if (inImplicitCall() && prevTag !== ',') {
endImplicitCall();
} else if (inImplicitObject() && sameLine && !startsLine) {
endImplicitObject();
} else if (inImplicitObject() && tag === 'TERMINATOR' && prevTag !== ',' && !(startsLine && this.looksObjectish(i + 1))) {
endImplicitObject();
} else {
break;
}
}
}
if (tag === ',' && !this.looksObjectish(i + 1) && inImplicitObject() && (nextTag !== 'TERMINATOR' || !this.looksObjectish(i + 2))) {
if (nextTag === 'OUTDENT') {
i += 1;
}
while (inImplicitObject()) {
endImplicitObject();
}
}
return forward(1);
});
};
Rewriter.prototype.addLocationDataToGeneratedTokens = function() {
return this.scanTokens(function(token, i, tokens) {
var prevToken, tag;
tag = token[0];
if ((token.generated || token.explicit) && (!token[2])) {
if (i > 0) {
prevToken = tokens[i - 1];
token[2] = {
first_line: prevToken[2].last_line,
first_column: prevToken[2].last_column,
last_line: prevToken[2].last_line,
last_column: prevToken[2].last_column
};
} else {
token[2] = {
first_line: 0,
first_column: 0,
last_line: 0,
last_column: 0
};
}
var last_column, last_line, _ref, _ref1, _ref2;
if (token[2]) {
return 1;
}
if (!(token.generated || token.explicit)) {
return 1;
}
_ref2 = (_ref = (_ref1 = tokens[i - 1]) != null ? _ref1[2] : void 0) != null ? _ref : {
last_line: 0,
last_column: 0
}, last_line = _ref2.last_line, last_column = _ref2.last_column;
token[2] = {
first_line: last_line,
first_column: last_column,
last_line: last_line,
last_column: last_column
};
return 1;
});
};
@@ -330,12 +433,7 @@
return [indent, outdent];
};
Rewriter.prototype.generate = function(tag, value) {
var tok;
tok = [tag, value];
tok.generated = true;
return tok;
};
Rewriter.prototype.generate = generate;
Rewriter.prototype.tag = function(i) {
var _ref;

View File

@@ -566,31 +566,12 @@ exports.Call = class Call extends Base
ifn = unfoldSoak o, call, 'variable'
ifn
# Walk through the objects in the arguments, moving over simple values.
# This allows syntax like `call a: b, c` into `call({a: b}, c);`
filterImplicitObjects: (list) ->
nodes = []
for node in list
unless node.isObject?() and node.base.generated
nodes.push node
continue
obj = null
for prop in node.base.properties
if prop instanceof Assign or prop instanceof Comment
nodes.push obj = new Obj properties = [], true if not obj
properties.push prop
else
nodes.push prop
obj = null
nodes
# Compile a vanilla function call.
compileNode: (o) ->
@variable?.front = @front
if code = Splat.compileSplattedArray o, @args, true
return @compileSplat o, code
args = @filterImplicitObjects @args
args = (arg.compile o, LEVEL_LIST for arg in args).join ', '
args = (arg.compile o, LEVEL_LIST for arg in @args).join ', '
if @isSuper
@superReference(o) + ".call(#{@superThis(o)}#{ args and ', ' + args })"
else
@@ -840,14 +821,11 @@ exports.Arr = class Arr extends Base
children: ['objects']
filterImplicitObjects: Call::filterImplicitObjects
compileNode: (o) ->
return '[]' unless @objects.length
o.indent += TAB
objs = @filterImplicitObjects @objects
return code if code = Splat.compileSplattedArray o, objs
code = (obj.compile o, LEVEL_LIST for obj in objs).join ', '
return code if code = Splat.compileSplattedArray o, @objects
code = (obj.compile o, LEVEL_LIST for obj in @objects).join ', '
if code.indexOf('\n') >= 0
"[\n#{o.indent}#{code}\n#{@tab}]"
else

View File

@@ -5,6 +5,12 @@
# shorthand into the unambiguous long form, add implicit indentation and
# parentheses, and generally clean things up.
# Create a generated token: one that exists due to a use of implicit syntax.
generate = (tag, value) ->
tok = [tag, value]
tok.generated = yes
tok
# The **Rewriter** class is used by the [Lexer](lexer.html), directly against
# its internal array of tokens.
class exports.Rewriter
@@ -24,8 +30,7 @@ class exports.Rewriter
@closeOpenIndexes()
@addImplicitIndentation()
@tagPostfixConditionals()
@addImplicitBraces()
@addImplicitParentheses()
@addImplicitBracesAndParens()
@addLocationDataToGeneratedTokens()
@tokens
@@ -71,7 +76,6 @@ class exports.Rewriter
# its paired close. We have the mis-nested outdent case included here for
# calls that close on the same line, just before their outdent.
closeOpenCalls: ->
condition = (token, i) ->
token[0] in [')', 'CALL_END'] or
token[0] is 'OUTDENT' and @tag(i - 1) is ')'
@@ -86,7 +90,6 @@ class exports.Rewriter
# The lexer has tagged the opening parenthesis of an indexing operation call.
# Match it with its paired close.
closeOpenIndexes: ->
condition = (token, i) ->
token[0] in [']', 'INDEX_END']
@@ -97,121 +100,237 @@ class exports.Rewriter
@detectEnd i + 1, condition, action if token[0] is 'INDEX_START'
1
# Object literals may be written with implicit braces, for simple cases.
# Insert the missing braces here, so that the parser doesn't have to.
addImplicitBraces: ->
# Match tags in token stream starting at i with pattern, skipping HERECOMMENTs
# Pattern may consist of strings (equality), an array of strings (one of)
# or null (wildcard)
matchTags: (i, pattern...) ->
fuzz = 0
for j in [0 ... pattern.length]
fuzz += 2 while @tag(i + j + fuzz) is 'HERECOMMENT'
continue if not pattern[j]?
pattern[j] = [pattern[j]] if typeof pattern[j] is 'string'
return no if @tag(i + j + fuzz) not in pattern[j]
yes
stack = []
start = null
startsLine = null
sameLine = yes
startIndent = 0
startIndex = 0
# yes iff standing in front of something looking like
# @<x>: or <x>:, skipping over 'HERECOMMENT's
looksObjectish: (j) ->
@matchTags(j, '@', null, ':') or @matchTags(j, null, ':')
condition = (token, i) ->
[one, two, three] = @tokens[i + 1 .. i + 3]
return no if 'HERECOMMENT' is one?[0]
[tag] = token
sameLine = no if tag in LINEBREAKS
return (
(tag in ['TERMINATOR', 'OUTDENT'] or
(tag in IMPLICIT_END and sameLine and not (i - startIndex is 1))) and
((!startsLine and @tag(i - 1) isnt ',') or
not (two?[0] is ':' or one?[0] is '@' and three?[0] is ':'))) or
(tag is ',' and one and
one[0] not in ['IDENTIFIER', 'NUMBER', 'STRING', '@', 'TERMINATOR', 'OUTDENT']
)
# yes iff current line of tokens contain an element of tags on same
# expression level. Stop searching at LINEBREAKS or explicit start of
# containing balanced expression.
findTagsBackwards: (i, tags) ->
backStack = []
while i >= 0 and (backStack.length or
@tag(i) not in tags and
(@tag(i) not in EXPRESSION_START or @tokens[i].generated) and
@tag(i) not in LINEBREAKS)
backStack.push @tag(i) if @tag(i) in EXPRESSION_END
backStack.pop() if @tag(i) in EXPRESSION_START and backStack.length
i -= 1
@tag(i) in tags
action = (token, i) ->
tok = @generate '}', '}'
@tokens.splice i, 0, tok
# Look for signs of implicit calls and objects in the token stream and
# add them.
addImplicitBracesAndParens: ->
# Track current balancing depth (both implicit and explicit) on stack.
stack = []
@scanTokens (token, i, tokens) ->
if (tag = token[0]) in EXPRESSION_START
stack.push [(if tag is 'INDENT' and @tag(i - 1) is '{' then '{' else tag), i]
return 1
[tag] = token
[prevTag] = if i > 0 then tokens[i - 1] else []
[nextTag] = if i < tokens.length - 1 then tokens[i + 1] else []
stackTop = -> stack[stack.length - 1]
startIdx = i
# Helper function, used for keeping track of the number of tokens consumed
# and spliced, when returning for getting a new token.
forward = (n) -> i - startIdx + n
# Helper functions
inImplicit = -> stackTop()?[2]?.ours
inImplicitCall = -> inImplicit() and stackTop()?[0] is '('
inImplicitObject = -> inImplicit() and stackTop()?[0] is '{'
# Unclosed control statement inside implicit parens (like
# class declaration or if-conditionals)
inImplicitControl = -> inImplicit and stackTop()?[0] is 'CONTROL'
startImplicitCall = (j) ->
idx = j ? i
stack.push ['(', idx, ours: yes]
tokens.splice idx, 0, generate 'CALL_START', '('
i += 1 if not j?
endImplicitCall = ->
stack.pop()
tokens.splice i, 0, generate 'CALL_END', ')'
i += 1
startImplicitObject = (j, startsLine = yes) ->
idx = j ? i
stack.push ['{', idx, sameLine: yes, startsLine: startsLine, ours: yes]
tokens.splice idx, 0, generate '{', generate(new String('{'))
i += 1 if not j?
endImplicitObject = ->
stack.pop()
tokens.splice i, 0, generate '}', '}'
i += 1
# Don't end an implicit call on next indent if any of these are in an argument
if inImplicitCall() and tag in ['IF', 'CLASS', 'SWITCH', 'CATCH']
stack.push ['CONTROL', i, ours: true]
return forward(1)
if tag is 'INDENT' and inImplicit()
# An INDENT closes an implicit call unless
# 1. We have seen a CONTROL argument on the line.
# 2. The last token before the indent is part of the list below
if prevTag not in ['=>', '->', '[', '(', ',', '{', 'TRY', 'ELSE', '=']
endImplicitCall() while inImplicitCall()
stack.pop() if inImplicitControl()
stack.push [tag, i]
return forward(1)
# Straightforward start of explicit expression
if tag in EXPRESSION_START
stack.push [tag, i]
return forward(1)
# Close all implicit expressions inside of explicitly closed expressions.
if tag in EXPRESSION_END
start = stack.pop()
return 1
return 1 unless tag is ':' and
((ago = @tag i - 2) is ':' or stack[stack.length - 1]?[0] isnt '{')
sameLine = yes
startIndex = i + 1
stack.push ['{']
idx = if ago is '@' then i - 2 else i - 1
idx -= 2 while @tag(idx - 2) is 'HERECOMMENT'
prevTag = @tag(idx - 1)
startsLine = not prevTag or (prevTag in LINEBREAKS)
value = new String('{')
value.generated = yes
tok = @generate '{', value
tokens.splice idx, 0, tok
@detectEnd i + 2, condition, action
2
while inImplicit()
if inImplicitCall()
endImplicitCall()
else if inImplicitObject()
endImplicitObject()
else
stack.pop()
stack.pop()
# Methods may be optionally called without parentheses, for simple cases.
# Insert the implicit parentheses here, so that the parser doesn't have to
# deal with them.
addImplicitParentheses: ->
# Recognize standard implicit calls like
# f a, f() b, f? c, h[0] d etc.
if (tag in IMPLICIT_FUNC and token.spaced or
tag is '?' and i > 0 and not tokens[i - 1].spaced) and
(nextTag in IMPLICIT_CALL or
nextTag in IMPLICIT_UNSPACED_CALL and
not tokens[i + 1]?.spaced and not tokens[i + 1]?.newLine)
tag = token[0] = 'FUNC_EXIST' if tag is '?'
startImplicitCall i + 1
return forward(2)
noCall = seenSingle = seenControl = no
callIndex = null
# Implicit call taking an implicit indented object as first argument.
# f
# a: b
# c: d
# and
# f
# 1
# a: b
# b: c
# Don't accept implicit calls of this type, when on the same line
# as the control strucutures below as that may misinterpret constructs like:
# if f
# a: 1
# as
# if f(a: 1)
# which is probably always unintended.
# Furthermore don't allow this in literal arrays, as
# that creates grammatical ambiguities.
if @matchTags(i, IMPLICIT_FUNC, 'INDENT') and
stackTop()?[0] isnt '[' and
not @findTagsBackwards(i, ['CLASS', 'EXTENDS', 'IF', 'CATCH',
'SWITCH', 'LEADING_WHEN', 'FOR', 'WHILE', 'UNTIL'])
startImplicitCall i + 1
stack.push ['INDENT', i + 2]
return forward(3)
condition = (token, i) ->
[tag] = token
return yes if not seenSingle and token.fromThen
seenSingle = yes if tag in ['IF', 'ELSE', 'CATCH', '->', '=>', 'CLASS']
seenControl = yes if tag in ['IF', 'ELSE', 'SWITCH', 'TRY', '=']
return yes if tag in ['.', '?.', '::'] and @tag(i - 1) is 'OUTDENT'
not token.generated and @tag(i - 1) isnt ',' and (tag in IMPLICIT_END or
(tag is 'INDENT' and not seenControl)) and
(tag isnt 'INDENT' or
(@tag(i - 2) not in ['CLASS', 'EXTENDS'] and @tag(i - 1) not in IMPLICIT_BLOCK and
not (callIndex is i - 1 and (post = @tokens[i + 1]) and post.generated and post[0] is '{')))
# Implicit objects start here
if tag is ':'
# Go back to the (implicit) start of the object
if @tag(i - 2) is '@' then s = i - 2 else s = i - 1
s -= 2 while @tag(s - 2) is 'HERECOMMENT'
action = (token, i) ->
@tokens.splice i, 0, @generate 'CALL_END', ')'
startsLine = s is 0 or @tag(s - 1) in LINEBREAKS or tokens[s - 1].newLine
# Are we just continuing an already declared object?
if stackTop()
[stackTag, stackIdx] = stackTop()
if (stackTag is '{' or stackTag is 'INDENT' and @tag(stackIdx - 1) is '{') and
(startsLine or @tag(s - 1) is ',' or @tag(s - 1) is '{')
return forward(1)
@scanTokens (token, i, tokens) ->
tag = token[0]
noCall = yes if tag in ['CLASS', 'IF', 'FOR', 'WHILE']
[prev, current, next] = tokens[i - 1 .. i + 1]
callObject = not noCall and tag is 'INDENT' and
next and next.generated and next[0] is '{' and
prev and prev[0] in IMPLICIT_FUNC
seenSingle = no
seenControl = no
noCall = no if tag in LINEBREAKS
token.call = yes if prev and not prev.spaced and tag is '?'
return 1 if token.fromThen
return 1 unless callObject or
prev?.spaced and (prev.call or prev[0] in IMPLICIT_FUNC) and
(tag in IMPLICIT_CALL or not (token.spaced or token.newLine) and tag in IMPLICIT_UNSPACED_CALL)
callIndex = i
tokens.splice i, 0, @generate 'CALL_START', '(', token[2]
@detectEnd i + 1, condition, action
prev[0] = 'FUNC_EXIST' if prev[0] is '?'
2
startImplicitObject(s, !!startsLine)
return forward(2)
# End implicit calls when chaining method calls
# like e.g.:
# f ->
# a
# .g b, ->
# c
# .h a
if prevTag is 'OUTDENT' and inImplicitCall() and tag in ['.', '?.', '::']
endImplicitCall()
return forward(1)
stackTop()[2].sameLine = no if inImplicitObject() and tag in LINEBREAKS
if tag in IMPLICIT_END
while inImplicit()
[stackTag, stackIdx, {sameLine, startsLine}] = stackTop()
# Close implicit calls when reached end of argument list
if inImplicitCall() and prevTag isnt ','
endImplicitCall()
# Close implicit objects such as:
# return a: 1, b: 2 unless true
else if inImplicitObject() and sameLine and not startsLine
endImplicitObject()
# Close implicit objects when at end of line, line didn't end with a comma
# and the implicit object didn't start the line or the next line doesn't look like
# the continuation of an object.
else if inImplicitObject() and tag is 'TERMINATOR' and prevTag isnt ',' and
not (startsLine and @looksObjectish(i + 1))
endImplicitObject()
else
break
# Close implicit object if comma is the last character
# and what comes after doesn't look like it belongs.
# This is used for trailing commas and calls, like:
# x =
# a: b,
# c: d,
# e = 2
#
# and
#
# f a, b: c, d: e, f, g: h: i, j
if tag is ',' and not @looksObjectish(i + 1) and inImplicitObject() and
(nextTag isnt 'TERMINATOR' or not @looksObjectish(i + 2))
# When nextTag is OUTDENT the comma is insignificant and
# should just be ignored so embed it in the implicit object.
#
# When it isn't the comma go on to play a role in a call or
# array further up the stack, so give it a chance.
if nextTag is 'OUTDENT'
i += 1
while inImplicitObject()
endImplicitObject()
return forward(1)
# Add location data to all tokens generated by the rewriter.
addLocationDataToGeneratedTokens: ->
@scanTokens (token, i, tokens) ->
tag = token[0]
if (token.generated or token.explicit) and (not token[2])
if i > 0
prevToken = tokens[i-1]
token[2] =
first_line: prevToken[2].last_line
first_column: prevToken[2].last_column
last_line: prevToken[2].last_line
last_column: prevToken[2].last_column
else
token[2] =
first_line: 0
first_column: 0
last_line: 0
last_column: 0
return 1
return 1 if token[2]
return 1 unless token.generated or token.explicit
{last_line, last_column} = tokens[i - 1]?[2] ? last_line: 0, last_column: 0
token[2] =
first_line: last_line
first_column: last_column
last_line: last_line
last_column: last_column
1
# Because our grammar is LALR(1), it can't handle some single-line
# expressions that lack ending delimiters. The **Rewriter** adds the implicit
@@ -276,11 +395,7 @@ class exports.Rewriter
indent.explicit = outdent.explicit = yes if not implicit
[indent, outdent]
# Create a generated token: one that exists due to a use of implicit syntax.
generate: (tag, value) ->
tok = [tag, value]
tok.generated = yes
tok
generate: generate
# Look up a tag by token index.
tag: (i) -> @tokens[i]?[0]
@@ -330,7 +445,8 @@ IMPLICIT_UNSPACED_CALL = ['+', '-']
IMPLICIT_BLOCK = ['->', '=>', '{', '[', ',']
# Tokens that always mark the end of an implicit call for single-liners.
IMPLICIT_END = ['POST_IF', 'FOR', 'WHILE', 'UNTIL', 'WHEN', 'BY', 'LOOP', 'TERMINATOR']
IMPLICIT_END = ['POST_IF', 'FOR', 'WHILE', 'UNTIL', 'WHEN', 'BY',
'LOOP', 'TERMINATOR']
# Single-line flavors of block expressions that have unclosed endings.
# The grammar can't disambiguate them, so we insert the implicit indentation.

View File

@@ -696,3 +696,14 @@ test "#2359: constructors should not return an explicit value", ->
return bar: 7
baz()
"""
test "#2319: fn class n extends o.p [INDENT] x = 123", ->
first = ->
base = onebase: ->
first class OneKeeper extends base.onebase
one = 1
one: -> one
eq new OneKeeper().one(), 1

View File

@@ -422,7 +422,6 @@ test "Issue #997. Switch doesn't fallthrough.", ->
test "Throw should be usable as an expression.", ->
try
false or throw 'up'
throw new Error 'failed'

View File

@@ -558,3 +558,116 @@ test "#2617: implicit call before unrelated implicit object", ->
result = if pass 1
one: 1
eq result.one, 1
test "#2292, b: f (z),(x)", ->
f = (x, y) -> y
one = 1
two = 2
o = b: f (one),(two)
eq o.b, 2
test "#2297, Different behaviors on interpreting literal", ->
foo = (x, y) -> y
bar =
baz: foo 100, on
eq bar.baz, on
qux = (x) -> x
quux = qux
corge: foo 100, true
eq quux.corge, on
xyzzy =
e: 1
f: foo
a: 1
b: 2
,
one: 1
two: 2
three: 3
g:
a: 1
b: 2
c: foo 2,
one: 1
two: 2
three: 3
d: 3
four: 4
h: foo one: 1, two: 2, three: three: three: 3,
2
eq xyzzy.f.two, 2
eq xyzzy.g.c.three, 3
eq xyzzy.four, 4
eq xyzzy.h, 2
thud = foo
1
one: 1
two: 2
three: 3
2
3
eq thud.two, 2
test "#2715, Chained implicit calls", ->
first = (x) -> x
second = (x, y) -> y
foo = first first
one: 1
eq foo.one, 1
bar = first second
one: 1, 2
eq bar, 2
baz = first second
one: 1,
2
eq baz, 2
qux = first second
1
2
3
4
eq qux, 2
test "More implicit calls", ->
first = (x) -> x
second = (x, y) -> y
foo = no
if (first
yes)
foo = yes
eq foo, yes
foo = no
if not first
no
foo = yes
eq foo, no
test "Implicit calls and new", ->
first = (x) -> x
foo = (@x) ->
bar = first new foo first 1
eq bar.x, 1
third = (x, y, z) -> z
baz = first new foo
new
foo third
one: 1
two: 2
1
three: 3
2
eq baz.x.x.three, 3

View File

@@ -222,6 +222,118 @@ test "#1436: `for` etc. work as normal property names", ->
obj.for = 'foo' of obj
eq yes, obj.hasOwnProperty 'for'
test "#2706, Un-bracketed object as argument causes inconsistent behavior", ->
foo = (x, y) -> y
bar = baz: yes
eq yes, foo x: 1, bar.baz
test "#2608, Allow inline objects in arguments to be followed by more arguments", ->
foo = (x, y) -> y
eq yes, foo x: 1, y: 2, yes
test "#2308, a: b = c:1", ->
foo = a: b = c: yes
eq b.c, yes
eq foo.a.c, yes
test "#2317, a: b c: 1", ->
foo = (x) -> x
bar = a: foo c: yes
eq bar.a.c, yes
test "#1896, a: func b, {c: d}", ->
first = (x) -> x
second = (x, y) -> y
third = (x, y, z) -> z
one = 1
two = 2
three = 3
four = 4
foo = a: second one, {c: two}
eq foo.a.c, two
bar = a: second one, c: two
eq bar.a.c, two
baz = a: second one, {c: two}, e: first first h: three
eq baz.a.c, two
qux = a: third one, {c: two}, e: first first h: three
eq qux.a.e.h, three
quux = a: third one, {c: two}, e: first(three), h: four
eq quux.a.e, three
eq quux.a.h, four
corge = a: third one, {c: two}, e: second three, h: four
eq corge.a.e.h, four
test "Implicit objects, functions and arrays", ->
first = (x) -> x
second = (x, y) -> y
foo = [
1
one: 1
two: 2
three: 3
more:
four: 4
five: 5, six: 6
2, 3, 4
5]
eq foo[2], 2
eq foo[1].more.six, 6
bar = [
1
first first first second 1,
one: 1, twoandthree: twoandthree: two: 2, three: 3
2,
2
one: 1
two: 2
three: first second ->
no
, ->
3
3
4]
eq bar[2], 2
eq bar[1].twoandthree.twoandthree.two, 2
eq bar[3].three(), 3
eq bar[4], 3
test "#2549, Brace-less Object Literal as a Second Operand on a New Line", ->
foo = no or
one: 1
two: 2
three: 3
eq foo.one, 1
bar = yes and one: 1
eq bar.one, 1
baz = null ?
one: 1
two: 2
eq baz.two, 2
test "#1865, syntax regression 1.1.3", ->
foo = (x, y) -> y
bar = a: foo (->),
c: yes
eq bar.a.c, yes
baz = a: foo (->), c: yes
eq baz.a.c, yes
test "#1322: implicit call against implicit object with block comments", ->
((obj, arg) ->
eq obj.x * obj.y, 6
@@ -273,7 +385,5 @@ test "#1961, #1974, regression with compound assigning to an implicit object", -
test "#2207: Immediate implicit closes don't close implicit objects", ->
func = ->
key: for i in [1, 2, 3] then i
eq func().key.join(' '), '1 2 3'