From a5f69ef7160dccd5ba31dedc89b5f996ea964ef4 Mon Sep 17 00:00:00 2001 From: Stan Angeloff Date: Mon, 8 Mar 2010 09:31:31 +0200 Subject: [PATCH 01/10] Empty expression interpolations evaluate as empty strings now. --- lib/lexer.js | 20 ++++++++++++-------- src/lexer.coffee | 13 ++++++++----- test/test_string_interpolation.coffee | 3 ++- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/lib/lexer.js b/lib/lexer.js index 161ae53b..fab26120 100644 --- a/lib/lexer.js +++ b/lib/lexer.js @@ -471,17 +471,21 @@ tokens.push(['IDENTIFIER', interp]); i += group.length - 1; pi = i + 1; - } else if (((expr = this.balanced_string(str.substring(i), ['${', '}']))) && expr.length > 3) { - inner = expr.substring(2, expr.length - 1); - nested = lexer.tokenize("(" + inner + ")", { - rewrite: false, - line: this.line - }); - nested.pop(); + } else if (((expr = this.balanced_string(str.substring(i), ['${', '}'])))) { if (pi < i) { tokens.push(['STRING', quote + (str.substring(pi, i)) + quote]); } - tokens.push(['TOKENS', nested]); + inner = expr.substring(2, expr.length - 1); + if (inner.length) { + nested = lexer.tokenize("(" + inner + ")", { + rewrite: false, + line: this.line + }); + nested.pop(); + tokens.push(['TOKENS', nested]); + } else { + tokens.push(['STRING', quote + quote]); + } i += expr.length - 1; pi = i + 1; } diff --git a/src/lexer.coffee b/src/lexer.coffee index 74c96d97..30ba0d9b 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -349,12 +349,15 @@ exports.Lexer: class Lexer tokens.push ['IDENTIFIER', interp] i += group.length - 1 pi: i + 1 - else if (expr: @balanced_string str.substring(i), ['${', '}']) and expr.length > 3 - inner: expr.substring(2, expr.length - 1) - nested: lexer.tokenize "($inner)", {rewrite: no, line: @line} - nested.pop() + else if (expr: @balanced_string str.substring(i), ['${', '}']) tokens.push ['STRING', "$quote${ str.substring(pi, i) }$quote"] if pi < i - tokens.push ['TOKENS', nested] + inner: expr.substring(2, expr.length - 1) + if inner.length + nested: lexer.tokenize "($inner)", {rewrite: no, line: @line} + nested.pop() + tokens.push ['TOKENS', nested] + else + tokens.push ['STRING', "$quote$quote"] i += expr.length - 1 pi: i + 1 i += 1 diff --git a/test/test_string_interpolation.coffee b/test/test_string_interpolation.coffee index 031ad543..5c454544 100644 --- a/test/test_string_interpolation.coffee +++ b/test/test_string_interpolation.coffee @@ -28,7 +28,8 @@ ok "Escaping \$last" is 'Escaping $last' ok "Escaping \${last}" is 'Escaping ${last}' ok "$$" is '$$' -ok "${}" is '${}' +ok "${}" is '' +ok "${}A${} ${} ${}B${}" is 'A B' ok "\\\\\$$" is '\\\\\$$' ok "\\\${}" is '\\${}' From 5e52e7f19e5f563e3fda9af70156f4fccf693ae2 Mon Sep 17 00:00:00 2001 From: Chris Hoffman Date: Mon, 8 Mar 2010 08:02:23 -0600 Subject: [PATCH 02/10] While loop can be a single line expressions so don't indent when it is --- extras/CoffeeScript.tmbundle/Preferences/Indent.tmPreferences | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extras/CoffeeScript.tmbundle/Preferences/Indent.tmPreferences b/extras/CoffeeScript.tmbundle/Preferences/Indent.tmPreferences index 08e29612..b331f305 100644 --- a/extras/CoffeeScript.tmbundle/Preferences/Indent.tmPreferences +++ b/extras/CoffeeScript.tmbundle/Preferences/Indent.tmPreferences @@ -15,8 +15,8 @@ ^\s* (.*class |[a-zA-Z\$_](\w|\$|:|\.)*\s*(?=\:(\s*\(.*\))?\s*((=|-)>\s*$)) # function that is not one line - |[a-zA-Z\$_](\w|\$|\.)*\s*(?=(?!\::)\:(?!(\s*\(.*\))?\s*((=|-)>))):\s*(if(?!.*?then)|while|for|$) # assignment using multiline if/while/for - |if(?!.*?then)|while|for + |[a-zA-Z\$_](\w|\$|\.)*\s*(?=(?!\::)\:(?!(\s*\(.*\))?\s*((=|-)>))):\s*((if|while)(?!.*?then)|for|$) # assignment using multiline if/while/for + |(if|while)(?!.*?then)|for |.*\{$ |.*\[$) From 62a871773bd208353f8b5bec37fbc71554ab7bab Mon Sep 17 00:00:00 2001 From: Will Date: Tue, 9 Mar 2010 01:03:43 +1100 Subject: [PATCH 03/10] Include an extra line `process.mixin require 'sys'` on the top to let the .js output run using node (v0.1.31) --- examples/web_server.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/web_server.coffee b/examples/web_server.coffee index 8bf66d1d..b9927ef1 100644 --- a/examples/web_server.coffee +++ b/examples/web_server.coffee @@ -1,5 +1,6 @@ # Contributed by Jason Huggins +process.mixin require 'sys' http: require 'http' server: http.createServer (req, res) -> From 81955005b9c3df3c8fba0ccea70475ff626d7deb Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Mon, 8 Mar 2010 09:13:15 -0500 Subject: [PATCH 04/10] doc tweaks --- documentation/index.html.erb | 6 +++--- index.html | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/documentation/index.html.erb b/documentation/index.html.erb index 725aa802..cb28f5ef 100644 --- a/documentation/index.html.erb +++ b/documentation/index.html.erb @@ -76,7 +76,7 @@
+alert reverse '.eeffoC yrT'

           
           
@@ -90,8 +90,8 @@ alert reverse '!tpircseeffoC'
         Annotated Source
       
       
- The Grammar — src/grammar - The Lexer — src/lexer + Grammar Rules — src/grammar + Lexing Tokens — src/lexer The Rewriter — src/rewriter The Syntax Tree — src/nodes Lexical Scope — src/scope diff --git a/index.html b/index.html index 580f2f67..f3014190 100644 --- a/index.html +++ b/index.html @@ -62,7 +62,7 @@
+alert reverse '.eeffoC yrT'

           
           
@@ -76,8 +76,8 @@ alert reverse '!tpircseeffoC'
         Annotated Source
       
       
- The Grammar — src/grammar - The Lexer — src/lexer + Grammar Rules — src/grammar + Lexing Tokens — src/lexer The Rewriter — src/rewriter The Syntax Tree — src/nodes Lexical Scope — src/scope From f755580b1192974b0341b9c66fd80d0d3b27fe6b Mon Sep 17 00:00:00 2001 From: Chris Hoffman Date: Mon, 8 Mar 2010 08:18:37 -0600 Subject: [PATCH 05/10] Match regex highlighting to how coffee parses it --- extras/CoffeeScript.tmbundle/Syntaxes/CoffeeScript.tmLanguage | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extras/CoffeeScript.tmbundle/Syntaxes/CoffeeScript.tmLanguage b/extras/CoffeeScript.tmbundle/Syntaxes/CoffeeScript.tmLanguage index 0a9729ee..1bbbeaf0 100644 --- a/extras/CoffeeScript.tmbundle/Syntaxes/CoffeeScript.tmLanguage +++ b/extras/CoffeeScript.tmbundle/Syntaxes/CoffeeScript.tmLanguage @@ -140,7 +140,7 @@ begin - (?<=[=(:]|^|return)\s*(/)(?![/*+{}?]) + (?<=[=(:\s]|^|return)\s*(/)(?![\s/*+{}?]) beginCaptures 1 From 81af8f296eb41665afc954d30602903f115d09e6 Mon Sep 17 00:00:00 2001 From: Stan Angeloff Date: Mon, 8 Mar 2010 20:05:02 +0200 Subject: [PATCH 06/10] Regular expression interpolations; fixed bug in string interpolations when all tokens were identifiers. --- lib/lexer.js | 108 ++++++++++++++++++-------- lib/nodes.js | 8 +- src/lexer.coffee | 63 ++++++++++----- src/nodes.coffee | 2 +- test/test_regexp_interpolation.coffee | 17 ++++ test/test_string_interpolation.coffee | 5 ++ 6 files changed, 146 insertions(+), 57 deletions(-) create mode 100644 test/test_regexp_interpolation.coffee diff --git a/lib/lexer.js b/lib/lexer.js index fab26120..1986ba76 100644 --- a/lib/lexer.js +++ b/lib/lexer.js @@ -1,5 +1,5 @@ (function(){ - var ACCESSORS, ASSIGNMENT, BEFORE_WHEN, CALLABLE, CODE, COFFEE_KEYWORDS, COMMENT, COMMENT_CLEANER, HEREDOC, HEREDOC_INDENT, IDENTIFIER, INTERPOLATION, JS_CLEANER, JS_FORBIDDEN, JS_KEYWORDS, KEYWORDS, LAST_DENT, LAST_DENTS, Lexer, MULTILINER, MULTI_DENT, NOT_REGEX, NO_NEWLINE, NUMBER, OPERATOR, REGEX, RESERVED, Rewriter, STRING_NEWLINES, WHITESPACE, compact, count, include, starts; + var ACCESSORS, ASSIGNMENT, BEFORE_WHEN, CALLABLE, CODE, COFFEE_KEYWORDS, COMMENT, COMMENT_CLEANER, HEREDOC, HEREDOC_INDENT, IDENTIFIER, INTERPOLATION, JS_CLEANER, JS_FORBIDDEN, JS_KEYWORDS, KEYWORDS, LAST_DENT, LAST_DENTS, Lexer, MULTILINER, MULTI_DENT, NOT_REGEX, NO_NEWLINE, NUMBER, OPERATOR, RESERVED, Rewriter, STRING_NEWLINES, WHITESPACE, compact, count, include, starts; // The CoffeeScript Lexer. Uses a series of token-matching regexes to attempt // matches against the beginning of the source code. When a match is found, // a token is produced, we consume the match, and start again. Tokens are in the @@ -129,18 +129,18 @@ // Matches strings, including multi-line strings. Ensures that quotation marks // are balanced within the string's contents, and within nested interpolations. Lexer.prototype.string_token = function string_token() { - var string; + var merge, string, supress; if (!(starts(this.chunk, '"') || starts(this.chunk, "'"))) { return false; } - string = this.balanced_token(['"', '"'], ['${', '}']); + string = this.balanced_token((supress = false), ['"', '"'], ['${', '}']); if (!(string)) { - string = this.balanced_token(["'", "'"]); + string = this.balanced_token((supress = false), ["'", "'"]); } if (!(string)) { return false; } - this.interpolate_string(string.replace(STRING_NEWLINES, " \\\n")); + this.interpolate_string(string.replace(STRING_NEWLINES, " \\\n"), (merge = true)); this.line += count(string, "\n"); this.i += string.length; return true; @@ -160,11 +160,11 @@ }; // Matches JavaScript interpolated directly into the source via backticks. Lexer.prototype.js_token = function js_token() { - var script; + var script, supress; if (!(starts(this.chunk, '`'))) { return false; } - if (!((script = this.balanced_token(['`', '`'])))) { + if (!((script = this.balanced_token((supress = false), ['`', '`'])))) { return false; } this.token('JS', script.replace(JS_CLEANER, '')); @@ -175,23 +175,57 @@ // to distinguish from division, so we borrow some basic heuristics from // JavaScript and Ruby. Lexer.prototype.regex_token = function regex_token() { - var regex; - if (!((regex = this.match(REGEX, 1)))) { + var _a, _b, _c, _d, _e, each, flags, i, index, interp_tokens, merge, regex, str, supress; + if (!((regex = this.balanced_token((supress = true), ['/', '/'])))) { + return false; + } + if (regex.length < 3 || regex.match(/^\/\s+|\n/)) { return false; } if (include(NOT_REGEX, this.tag())) { return false; } - this.token('REGEX', regex); + flags = ['i', 'm', 'g', 'y']; + while (((index = flags.indexOf(this.chunk.substr(regex.length, 1)))) >= 0) { + regex += flags[index]; + flags.splice(index, 1); + } + if (((0 < (_e = regex.indexOf('${'))) && (_e < regex.indexOf('}'))) || regex.match(/[^\\]\$[a-zA-Z_@]/)) { + _a = regex.substring(1).split('/'); + str = _a[0]; + flags = _a[1]; + str = str.replace(/\\[^\$]/g, function(escaped) { + return '\\' + escaped; + }); + this.tokens = this.tokens.concat([['(', '('], ['NEW', 'new'], ['IDENTIFIER', 'RegExp'], ['CALL_START', '(']]); + interp_tokens = this.interpolate_string("\"" + str + "\"", (merge = false)); + _b = interp_tokens; + for (i = 0, _c = _b.length; i < _c; i++) { + each = _b[i]; + if ((_d = each[0]) === 'TOKENS') { + this.tokens = this.tokens.concat(each[1]); + } else if (_d === 'STRING') { + this.token(each[0], each[1].substring(0, 1) + each[1].substring(1, each[1].length - 1).replace(/"/g, '\\"') + each[1].substring(0, 1)); + } else { + this.token(each[0], each[1]); + } + if (i < interp_tokens.length - 1) { + this.token('+', '+'); + } + } + this.tokens = this.tokens.concat([[',', ','], ['STRING', "'" + flags + "'"], [')', ')'], [')', ')']]); + } else { + this.token('REGEX', regex); + } this.i += regex.length; return true; }; // Matches a token in which which the passed delimiter pairs must be correctly // balanced (ie. strings, JS literals). - Lexer.prototype.balanced_token = function balanced_token() { + Lexer.prototype.balanced_token = function balanced_token(supress) { var delimited; - delimited = Array.prototype.slice.call(arguments, 0); - return this.balanced_string.apply(this, [this.chunk].concat(delimited)); + delimited = Array.prototype.slice.call(arguments, 1); + return this.balanced_string.apply(this, [this.chunk].concat([supress]).concat(delimited)); }; // Matches and conumes comments. We pass through comments into JavaScript, // so they're treated as real tokens, like any other part of the language. @@ -395,9 +429,9 @@ // a series of delimiters, all of which must be nested correctly within the // contents of the string. This method allows us to have strings within // interpolations within strings etc... - Lexer.prototype.balanced_string = function balanced_string(str) { + Lexer.prototype.balanced_string = function balanced_string(str, supress) { var _a, _b, _c, _d, close, delimited, i, levels, open, pair; - delimited = Array.prototype.slice.call(arguments, 1); + delimited = Array.prototype.slice.call(arguments, 2); levels = []; i = 0; while (i < str.length) { @@ -429,7 +463,10 @@ i += 1; } if (levels.length) { - throw new Error("SyntaxError: Unterminated " + (levels.pop()[0]) + " starting on line " + (this.line + 1)); + if (!(supress)) { + throw new Error("SyntaxError: Unterminated " + (levels.pop()[0]) + " starting on line " + (this.line + 1)); + } + return false; } if (i === 0) { return false; @@ -444,8 +481,8 @@ // If it encounters an interpolation, this method will recursively create a // new Lexer, tokenize the interpolated contents, and merge them into the // token stream. - Lexer.prototype.interpolate_string = function interpolate_string(str) { - var _a, _b, _c, _d, _e, each, expr, group, i, inner, interp, lexer, match, nested, pi, quote, tokens; + Lexer.prototype.interpolate_string = function interpolate_string(str, merge) { + var _a, _b, _c, _d, _e, _f, _g, each, expr, group, has_string, i, inner, interp, lexer, match, nested, pi, quote, supress, tokens; if (str.length < 3 || !starts(str, '"')) { return this.token('STRING', str); } else { @@ -466,14 +503,14 @@ interp = "this." + (interp.substring(1)); } if (pi < i) { - tokens.push(['STRING', quote + (str.substring(pi, i)) + quote]); + tokens.push(['STRING', '' + quote + (str.substring(pi, i)) + quote]); } tokens.push(['IDENTIFIER', interp]); i += group.length - 1; pi = i + 1; - } else if (((expr = this.balanced_string(str.substring(i), ['${', '}'])))) { + } else if (((expr = this.balanced_string(str.substring(i), (supress = false), ['${', '}'])))) { if (pi < i) { - tokens.push(['STRING', quote + (str.substring(pi, i)) + quote]); + tokens.push(['STRING', '' + quote + (str.substring(pi, i)) + quote]); } inner = expr.substring(2, expr.length - 1); if (inner.length) { @@ -484,7 +521,7 @@ nested.pop(); tokens.push(['TOKENS', nested]); } else { - tokens.push(['STRING', quote + quote]); + tokens.push(['STRING', '' + quote + quote]); } i += expr.length - 1; pi = i + 1; @@ -492,19 +529,27 @@ i += 1; } if (pi < i && pi < str.length - 1) { - tokens.push(['STRING', quote + (str.substring(pi, i)) + quote]); + tokens.push(['STRING', '' + quote + (str.substring(pi, i)) + quote]); } - _c = []; _d = tokens; - for (i = 0, _e = _d.length; i < _e; i++) { - each = _d[i]; - _c.push((function() { + _c = tokens; + for (_d = 0, _e = _c.length; _d < _e; _d++) { + each = _c[_d]; + each[0] === 'STRING' ? ((has_string = true)) : null; + } + if (!has_string) { + tokens.unshift(['STRING', "''"]); + } + if (((typeof merge !== "undefined" && merge !== null) ? merge : true)) { + _f = tokens; + for (i = 0, _g = _f.length; i < _g; i++) { + each = _f[i]; each[0] === 'TOKENS' ? (this.tokens = this.tokens.concat(each[1])) : this.token(each[0], each[1]); if (i < tokens.length - 1) { - return this.token('+', '+'); + this.token('+', '+'); } - }).call(this)); + } } - return _c; + return tokens; } }; // Helpers @@ -568,7 +613,7 @@ // be used as identifiers or properties. JS_FORBIDDEN = JS_KEYWORDS.concat(RESERVED); // Token matching regexes. - IDENTIFIER = /^([a-zA-Z$_](\w|\$)*)/; + IDENTIFIER = /^([a-zA-Z\$_](\w|\$)*)/; NUMBER = /^(\b((0(x|X)[0-9a-fA-F]+)|([0-9]+(\.[0-9]+)?(e[+\-]?[0-9]+)?)))\b/i; HEREDOC = /^("{6}|'{6}|"{3}\n?([\s\S]*?)\n?([ \t]*)"{3}|'{3}\n?([\s\S]*?)\n?([ \t]*)'{3})/; INTERPOLATION = /^\$([a-zA-Z_@]\w*(\.\w+)*)/; @@ -576,7 +621,6 @@ WHITESPACE = /^([ \t]+)/; COMMENT = /^(((\n?[ \t]*)?#[^\n]*)+)/; CODE = /^((-|=)>)/; - REGEX = /^(\/(\S.*?)?([^\\]|\\\\)\/[imgy]{0,4})/; MULTI_DENT = /^((\n([ \t]*))+)(\.)?/; LAST_DENTS = /\n([ \t]*)/g; LAST_DENT = /\n([ \t]*)/; diff --git a/lib/nodes.js b/lib/nodes.js index 3ea376a6..bca3d9a8 100644 --- a/lib/nodes.js +++ b/lib/nodes.js @@ -289,7 +289,7 @@ idt += TAB var end, idt; idt = this.is_statement() ? this.idt() : ''; end = this.is_statement() ? ';' : ''; - return idt + this.value + end; + return '' + idt + this.value + end; }; LiteralNode.prototype.toString = function toString(idt) { return " \"" + this.value + "\""; @@ -762,7 +762,7 @@ idt += TAB props = props.empty() ? '' : props.compile(o) + '\n'; extension = extension ? this.idt() + extension.compile(o) + ';\n' : ''; returns = ret ? '\n' + this.idt() + 'return ' + this.variable.compile(o) + ';' : ''; - return construct + extension + props + returns; + return '' + construct + extension + props + returns; }; return ClassNode; }).call(this); @@ -1540,7 +1540,7 @@ idt += TAB indent: this.idt(), chain_child: true })) : " else {\n" + (Expressions.wrap([this.else_body]).compile(o)) + "\n" + this.tab + "}"; - return if_part + else_part; + return '' + if_part + else_part; }; // Compile the IfNode as a ternary operator. IfNode.prototype.compile_ternary = function compile_ternary(o) { @@ -1559,7 +1559,7 @@ idt += TAB // with Git. TRAILING_WHITESPACE = /\s+$/gm; // Keep this identifier regex in sync with the Lexer. - IDENTIFIER = /^[a-zA-Z$_](\w|\$)*$/; + IDENTIFIER = /^[a-zA-Z\$_](\w|\$)*$/; // Utility Functions // ----------------- // Merge objects, returning a fresh copy with attributes from both sides. diff --git a/src/lexer.coffee b/src/lexer.coffee index 30ba0d9b..b8822a51 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -96,10 +96,10 @@ exports.Lexer: class Lexer # are balanced within the string's contents, and within nested interpolations. string_token: -> return false unless starts(@chunk, '"') or starts(@chunk, "'") - string: @balanced_token ['"', '"'], ['${', '}'] - string: @balanced_token ["'", "'"] unless string + string: @balanced_token supress: false, ['"', '"'], ['${', '}'] + string: @balanced_token supress: false, ["'", "'"] unless string return false unless string - @interpolate_string string.replace STRING_NEWLINES, " \\\n" + @interpolate_string string.replace(STRING_NEWLINES, " \\\n"), merge: true @line += count string, "\n" @i += string.length true @@ -117,7 +117,7 @@ exports.Lexer: class Lexer # Matches JavaScript interpolated directly into the source via backticks. js_token: -> return false unless starts @chunk, '`' - return false unless script: @balanced_token ['`', '`'] + return false unless script: @balanced_token supress: false, ['`', '`'] @token 'JS', script.replace(JS_CLEANER, '') @i += script.length true @@ -126,16 +126,34 @@ exports.Lexer: class Lexer # to distinguish from division, so we borrow some basic heuristics from # JavaScript and Ruby. regex_token: -> - return false unless regex: @match REGEX, 1 + return false unless regex: @balanced_token supress: true, ['/', '/'] + return false if regex.length < 3 or regex.match /^\/\s+|\n/ return false if include NOT_REGEX, @tag() - @token 'REGEX', regex + flags: ['i', 'm', 'g', 'y'] + while (index: flags.indexOf @chunk.substr regex.length, 1) >= 0 + regex += flags[index] + flags.splice index, 1 + if (0 < regex.indexOf('${') < regex.indexOf('}')) or regex.match /[^\\]\$[a-zA-Z_@]/ + [str, flags]: regex.substring(1).split('/') + str: str.replace /\\[^\$]/g, (escaped) -> '\\' + escaped + @tokens: @tokens.concat [['(', '('], ['NEW', 'new'], ['IDENTIFIER', 'RegExp'], ['CALL_START', '(']] + interp_tokens: @interpolate_string "\"$str\"", merge: false + for each, i in interp_tokens + switch each[0] + when 'TOKENS' then @tokens: @tokens.concat each[1] + when 'STRING' then @token each[0], each[1].substring(0, 1) + each[1].substring(1, each[1].length - 1).replace(/"/g, '\\"') + each[1].substring(0, 1) + else @token each[0], each[1] + @token '+', '+' if i < interp_tokens.length - 1 + @tokens: @tokens.concat [[',', ','], ['STRING', "'$flags'"], [')', ')'], [')', ')']] + else + @token 'REGEX', regex @i += regex.length true # Matches a token in which which the passed delimiter pairs must be correctly # balanced (ie. strings, JS literals). - balanced_token: (delimited...) -> - @balanced_string @chunk, delimited... + balanced_token: (supress, delimited...) -> + @balanced_string @chunk, supress, delimited... # Matches and conumes comments. We pass through comments into JavaScript, # so they're treated as real tokens, like any other part of the language. @@ -297,7 +315,7 @@ exports.Lexer: class Lexer # a series of delimiters, all of which must be nested correctly within the # contents of the string. This method allows us to have strings within # interpolations within strings etc... - balanced_string: (str, delimited...) -> + balanced_string: (str, supress, delimited...) -> levels: [] i: 0 while i < str.length @@ -317,7 +335,9 @@ exports.Lexer: class Lexer break break unless levels.length i += 1 - throw new Error "SyntaxError: Unterminated ${levels.pop()[0]} starting on line ${@line + 1}" if levels.length + if levels.length + throw new Error "SyntaxError: Unterminated ${levels.pop()[0]} starting on line ${@line + 1}" unless supress + return false return false if i is 0 return str.substring(0, i) @@ -331,7 +351,7 @@ exports.Lexer: class Lexer # If it encounters an interpolation, this method will recursively create a # new Lexer, tokenize the interpolated contents, and merge them into the # token stream. - interpolate_string: (str) -> + interpolate_string: (str, merge) -> if str.length < 3 or not starts str, '"' @token 'STRING', str else @@ -349,7 +369,7 @@ exports.Lexer: class Lexer tokens.push ['IDENTIFIER', interp] i += group.length - 1 pi: i + 1 - else if (expr: @balanced_string str.substring(i), ['${', '}']) + else if (expr: @balanced_string str.substring(i), supress: false, ['${', '}']) tokens.push ['STRING', "$quote${ str.substring(pi, i) }$quote"] if pi < i inner: expr.substring(2, expr.length - 1) if inner.length @@ -362,12 +382,16 @@ exports.Lexer: class Lexer pi: i + 1 i += 1 tokens.push ['STRING', "$quote${ str.substring(pi, i) }$quote"] if pi < i and pi < str.length - 1 - for each, i in tokens - if each[0] is 'TOKENS' - @tokens: @tokens.concat each[1] - else - @token each[0], each[1] - @token '+', '+' if i < tokens.length - 1 + (has_string: yes) for each in tokens when each[0] is 'STRING' + tokens.unshift ['STRING', "''"] if not has_string + if (merge ? true) + for each, i in tokens + if each[0] is 'TOKENS' + @tokens: @tokens.concat each[1] + else + @token each[0], each[1] + @token '+', '+' if i < tokens.length - 1 + tokens # Helpers # ------- @@ -440,7 +464,7 @@ RESERVED: [ JS_FORBIDDEN: JS_KEYWORDS.concat RESERVED # Token matching regexes. -IDENTIFIER : /^([a-zA-Z$_](\w|\$)*)/ +IDENTIFIER : /^([a-zA-Z\$_](\w|\$)*)/ NUMBER : /^(\b((0(x|X)[0-9a-fA-F]+)|([0-9]+(\.[0-9]+)?(e[+\-]?[0-9]+)?)))\b/i HEREDOC : /^("{6}|'{6}|"{3}\n?([\s\S]*?)\n?([ \t]*)"{3}|'{3}\n?([\s\S]*?)\n?([ \t]*)'{3})/ INTERPOLATION : /^\$([a-zA-Z_@]\w*(\.\w+)*)/ @@ -448,7 +472,6 @@ OPERATOR : /^([+\*&|\/\-%=<>:!?]+)/ WHITESPACE : /^([ \t]+)/ COMMENT : /^(((\n?[ \t]*)?#[^\n]*)+)/ CODE : /^((-|=)>)/ -REGEX : /^(\/(\S.*?)?([^\\]|\\\\)\/[imgy]{0,4})/ MULTI_DENT : /^((\n([ \t]*))+)(\.)?/ LAST_DENTS : /\n([ \t]*)/g LAST_DENT : /\n([ \t]*)/ diff --git a/src/nodes.coffee b/src/nodes.coffee index 142928f3..bcd18331 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -1192,7 +1192,7 @@ TAB: ' ' TRAILING_WHITESPACE: /\s+$/gm # Keep this identifier regex in sync with the Lexer. -IDENTIFIER: /^[a-zA-Z$_](\w|\$)*$/ +IDENTIFIER: /^[a-zA-Z\$_](\w|\$)*$/ # Utility Functions # ----------------- diff --git a/test/test_regexp_interpolation.coffee b/test/test_regexp_interpolation.coffee new file mode 100644 index 00000000..c5b2c74b --- /dev/null +++ b/test/test_regexp_interpolation.coffee @@ -0,0 +1,17 @@ +name: 'Bob' + +ok not not '"Bob"'.match(/^"${name}"$/i) +ok '"Bobby"'.match(/^"${name}"$/i) is null + +ok not not 'Bob'.match(/^$name$/) +ok 'Bobby'.match(/^$name/) + +ok 'Bobby'.match(/${"${"${"$name"}"}"}/imgy) + +ok '$a$b$c'.match(/\$A\$B\$C/i) + +a: 1 +b: 2 +c: 3 + +ok '123'.match(/$a$b$c/i) diff --git a/test/test_string_interpolation.coffee b/test/test_string_interpolation.coffee index 5c454544..a69c2780 100644 --- a/test/test_string_interpolation.coffee +++ b/test/test_string_interpolation.coffee @@ -61,3 +61,8 @@ ok "Where is ${"the nested ${obj["name"]}"}?" is 'Where is the nested Joe?' ok "Hello ${world ? "$hello"}" is 'Hello World' ok "Hello ${"${"${obj["name"]}" + '!'}"}" is 'Hello Joe!' + +a: 1 +b: 2 +c: 3 +ok "$a$b$c" is '123' From c04b43e047f42362e1023fd731e969886fb10cba Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Mon, 8 Mar 2010 19:27:38 -0500 Subject: [PATCH 07/10] adding documentation for var shadowing --- documentation/css/docs.css | 1 + documentation/index.html.erb | 7 +++++++ index.html | 9 ++++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/documentation/css/docs.css b/documentation/css/docs.css index d5e3d91b..1c74cfce 100644 --- a/documentation/css/docs.css +++ b/documentation/css/docs.css @@ -105,6 +105,7 @@ div.code { position: fixed; z-index: 100; height: 50px; + min-width: 490px; left: 40px; right: 40px; top: 25px; background: #ddd; padding-left: 235px; diff --git a/documentation/index.html.erb b/documentation/index.html.erb index cb28f5ef..8f610888 100644 --- a/documentation/index.html.erb +++ b/documentation/index.html.erb @@ -395,6 +395,13 @@ coffee --print app/scripts/*.coffee > concatenation.js should not be able to change the value of the external variable of the same name, and therefore has a declaration of its own.

+

+ This behavior is effectively identical to Ruby's scope for local variables. + Because you don't have direct access to the var keyword, + it's impossible to shadow an outer variable on purpose, you may only refer + to it. So be careful that you're not reusing the name of an external + variable accidentally, if you're writing a deeply nested function. +

Although suppressed within this documentation for clarity, all CoffeeScript output is wrapped in an anonymous function: diff --git a/index.html b/index.html index f3014190..a315faa2 100644 --- a/index.html +++ b/index.html @@ -566,6 +566,13 @@ new_num = change_numbers(); should not be able to change the value of the external variable of the same name, and therefore has a declaration of its own.

+

+ This behavior is effectively identical to Ruby's scope for local variables. + Because you don't have direct access to the var keyword, + it's impossible to shadow an outer variable on purpose, you may only refer + to it. So be careful that you're not reusing the name of an external + variable accidentally, if you're writing a deeply nested function. +

Although suppressed within this documentation for clarity, all CoffeeScript output is wrapped in an anonymous function: @@ -995,7 +1002,7 @@ numbers_copy = numbers.slice(0, numbers.length);

numbers: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
 
-numbers[3..6]: [-3, -4, -5, -6]
+numbers[3..6]: [-3, -4, -5, -6]
 
 
 
var numbers;

From 121f01c06fb0268e53c42e79f230cdef574b4b0c Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas 
Date: Mon, 8 Mar 2010 19:43:12 -0500
Subject: [PATCH 08/10] merged stan's interpolation_3, a couple of tweaks

---
 lib/lexer.js        | 5 ++++-
 lib/rewriter.js     | 2 +-
 src/lexer.coffee    | 3 ++-
 src/rewriter.coffee | 2 +-
 4 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/lib/lexer.js b/lib/lexer.js
index 1986ba76..988fb32e 100644
--- a/lib/lexer.js
+++ b/lib/lexer.js
@@ -33,7 +33,7 @@
     Lexer.prototype.tokenize = function tokenize(code, options) {
       var o;
       o = options || {};
-      this.code = code;
+      this.code = code || '';
       // The remainder of the source code.
       this.i = 0;
       // Current character position we're parsing.
@@ -176,6 +176,9 @@
     // JavaScript and Ruby.
     Lexer.prototype.regex_token = function regex_token() {
       var _a, _b, _c, _d, _e, each, flags, i, index, interp_tokens, merge, regex, str, supress;
+      if (!(starts(this.chunk, '/'))) {
+        return false;
+      }
       if (!((regex = this.balanced_token((supress = true), ['/', '/'])))) {
         return false;
       }
diff --git a/lib/rewriter.js b/lib/rewriter.js
index cbf63c26..e09299c4 100644
--- a/lib/rewriter.js
+++ b/lib/rewriter.js
@@ -81,7 +81,7 @@
     Rewriter.prototype.remove_leading_newlines = function remove_leading_newlines() {
       var _a;
       _a = [];
-      while (this.tokens[0][0] === 'TERMINATOR') {
+      while (this.tokens[0] && this.tokens[0][0] === 'TERMINATOR') {
         _a.push(this.tokens.shift());
       }
       return _a;
diff --git a/src/lexer.coffee b/src/lexer.coffee
index b8822a51..6f5e4024 100644
--- a/src/lexer.coffee
+++ b/src/lexer.coffee
@@ -36,7 +36,7 @@ exports.Lexer: class Lexer
   # unless explicitly asked not to.
   tokenize: (code, options) ->
     o        : options or {}
-    @code    : code         # The remainder of the source code.
+    @code    : code or ''   # The remainder of the source code.
     @i       : 0            # Current character position we're parsing.
     @line    : o.line or 0  # The current line.
     @indent  : 0            # The current indentation level.
@@ -126,6 +126,7 @@ exports.Lexer: class Lexer
   # to distinguish from division, so we borrow some basic heuristics from
   # JavaScript and Ruby.
   regex_token: ->
+    return false unless starts @chunk, '/'
     return false unless regex: @balanced_token supress: true, ['/', '/']
     return false if regex.length < 3 or regex.match /^\/\s+|\n/
     return false if include NOT_REGEX, @tag()
diff --git a/src/rewriter.coffee b/src/rewriter.coffee
index 1fe3a8d8..8848f6db 100644
--- a/src/rewriter.coffee
+++ b/src/rewriter.coffee
@@ -62,7 +62,7 @@ exports.Rewriter: class Rewriter
   # Leading newlines would introduce an ambiguity in the grammar, so we
   # dispatch them here.
   remove_leading_newlines: ->
-    @tokens.shift() while @tokens[0][0] is 'TERMINATOR'
+    @tokens.shift() while @tokens[0] and @tokens[0][0] is 'TERMINATOR'
 
   # Some blocks occur in the middle of expressions -- when we're expecting
   # this, remove their trailing newlines.

From 3291bd2a4af700e8c3508a43a64c576b433a73b9 Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas 
Date: Mon, 8 Mar 2010 20:07:19 -0500
Subject: [PATCH 09/10] removing the 'supress' option from the lexer, just look
 at the slash.

---
 Cakefile                                      |  2 +-
 .../Syntaxes/CoffeeScript.tmLanguage          |  2 +-
 lib/lexer.js                                  | 32 ++++++++-----------
 src/lexer.coffee                              | 20 ++++++------
 4 files changed, 26 insertions(+), 30 deletions(-)

diff --git a/Cakefile b/Cakefile
index 46a0df74..dc436688 100644
--- a/Cakefile
+++ b/Cakefile
@@ -29,7 +29,7 @@ task 'build', 'build the CoffeeScript language from source', ->
 
 
 task 'build:full', 'checkout /lib, rebuild the source twice, and run the tests', ->
-  exec 'git co lib && bin/cake build && bin/cake build && bin/cake test', (err, stdout, stderr) ->
+  exec 'git checkout lib && bin/cake build && bin/cake build && bin/cake test', (err, stdout, stderr) ->
     print stdout if stdout
     print stderr if stderr
     throw err    if err
diff --git a/extras/CoffeeScript.tmbundle/Syntaxes/CoffeeScript.tmLanguage b/extras/CoffeeScript.tmbundle/Syntaxes/CoffeeScript.tmLanguage
index 1bbbeaf0..16a325bb 100644
--- a/extras/CoffeeScript.tmbundle/Syntaxes/CoffeeScript.tmLanguage
+++ b/extras/CoffeeScript.tmbundle/Syntaxes/CoffeeScript.tmLanguage
@@ -150,7 +150,7 @@
 				
 			
 			end
-			(/)[igm]*
+			(/)[igmy]*
 			endCaptures
 			
 				1
diff --git a/lib/lexer.js b/lib/lexer.js
index 988fb32e..bef8b616 100644
--- a/lib/lexer.js
+++ b/lib/lexer.js
@@ -129,15 +129,11 @@
     // Matches strings, including multi-line strings. Ensures that quotation marks
     // are balanced within the string's contents, and within nested interpolations.
     Lexer.prototype.string_token = function string_token() {
-      var merge, string, supress;
+      var merge, string;
       if (!(starts(this.chunk, '"') || starts(this.chunk, "'"))) {
         return false;
       }
-      string = this.balanced_token((supress = false), ['"', '"'], ['${', '}']);
-      if (!(string)) {
-        string = this.balanced_token((supress = false), ["'", "'"]);
-      }
-      if (!(string)) {
+      if (!((string = this.balanced_token(['"', '"'], ['${', '}']) || this.balanced_token(["'", "'"])))) {
         return false;
       }
       this.interpolate_string(string.replace(STRING_NEWLINES, " \\\n"), (merge = true));
@@ -160,11 +156,11 @@
     };
     // Matches JavaScript interpolated directly into the source via backticks.
     Lexer.prototype.js_token = function js_token() {
-      var script, supress;
+      var script;
       if (!(starts(this.chunk, '`'))) {
         return false;
       }
-      if (!((script = this.balanced_token((supress = false), ['`', '`'])))) {
+      if (!((script = this.balanced_token(['`', '`'])))) {
         return false;
       }
       this.token('JS', script.replace(JS_CLEANER, ''));
@@ -175,11 +171,11 @@
     // to distinguish from division, so we borrow some basic heuristics from
     // JavaScript and Ruby.
     Lexer.prototype.regex_token = function regex_token() {
-      var _a, _b, _c, _d, _e, each, flags, i, index, interp_tokens, merge, regex, str, supress;
+      var _a, _b, _c, _d, _e, each, flags, i, index, interp_tokens, merge, regex, str;
       if (!(starts(this.chunk, '/'))) {
         return false;
       }
-      if (!((regex = this.balanced_token((supress = true), ['/', '/'])))) {
+      if (!((regex = this.balanced_token(['/', '/'])))) {
         return false;
       }
       if (regex.length < 3 || regex.match(/^\/\s+|\n/)) {
@@ -225,10 +221,10 @@
     };
     // Matches a token in which which the passed delimiter pairs must be correctly
     // balanced (ie. strings, JS literals).
-    Lexer.prototype.balanced_token = function balanced_token(supress) {
+    Lexer.prototype.balanced_token = function balanced_token() {
       var delimited;
-      delimited = Array.prototype.slice.call(arguments, 1);
-      return this.balanced_string.apply(this, [this.chunk].concat([supress]).concat(delimited));
+      delimited = Array.prototype.slice.call(arguments, 0);
+      return this.balanced_string.apply(this, [this.chunk].concat(delimited));
     };
     // Matches and conumes comments. We pass through comments into JavaScript,
     // so they're treated as real tokens, like any other part of the language.
@@ -432,9 +428,9 @@
     // a series of delimiters, all of which must be nested correctly within the
     // contents of the string. This method allows us to have strings within
     // interpolations within strings etc...
-    Lexer.prototype.balanced_string = function balanced_string(str, supress) {
+    Lexer.prototype.balanced_string = function balanced_string(str) {
       var _a, _b, _c, _d, close, delimited, i, levels, open, pair;
-      delimited = Array.prototype.slice.call(arguments, 2);
+      delimited = Array.prototype.slice.call(arguments, 1);
       levels = [];
       i = 0;
       while (i < str.length) {
@@ -466,7 +462,7 @@
         i += 1;
       }
       if (levels.length) {
-        if (!(supress)) {
+        if (!(delimited[0][0] === '/')) {
           throw new Error("SyntaxError: Unterminated " + (levels.pop()[0]) + " starting on line " + (this.line + 1));
         }
         return false;
@@ -485,7 +481,7 @@
     // new Lexer, tokenize the interpolated contents, and merge them into the
     // token stream.
     Lexer.prototype.interpolate_string = function interpolate_string(str, merge) {
-      var _a, _b, _c, _d, _e, _f, _g, each, expr, group, has_string, i, inner, interp, lexer, match, nested, pi, quote, supress, tokens;
+      var _a, _b, _c, _d, _e, _f, _g, each, expr, group, has_string, i, inner, interp, lexer, match, nested, pi, quote, tokens;
       if (str.length < 3 || !starts(str, '"')) {
         return this.token('STRING', str);
       } else {
@@ -511,7 +507,7 @@
             tokens.push(['IDENTIFIER', interp]);
             i += group.length - 1;
             pi = i + 1;
-          } else if (((expr = this.balanced_string(str.substring(i), (supress = false), ['${', '}'])))) {
+          } else if (((expr = this.balanced_string(str.substring(i), ['${', '}'])))) {
             if (pi < i) {
               tokens.push(['STRING', '' + quote + (str.substring(pi, i)) + quote]);
             }
diff --git a/src/lexer.coffee b/src/lexer.coffee
index 6f5e4024..27957c85 100644
--- a/src/lexer.coffee
+++ b/src/lexer.coffee
@@ -96,9 +96,9 @@ exports.Lexer: class Lexer
   # are balanced within the string's contents, and within nested interpolations.
   string_token: ->
     return false unless starts(@chunk, '"') or starts(@chunk, "'")
-    string: @balanced_token supress: false, ['"', '"'], ['${', '}']
-    string: @balanced_token supress: false, ["'", "'"] unless string
-    return false unless string
+    return false unless string:
+      @balanced_token(['"', '"'], ['${', '}']) or
+      @balanced_token ["'", "'"]
     @interpolate_string string.replace(STRING_NEWLINES, " \\\n"), merge: true
     @line += count string, "\n"
     @i += string.length
@@ -117,7 +117,7 @@ exports.Lexer: class Lexer
   # Matches JavaScript interpolated directly into the source via backticks.
   js_token: ->
     return false unless starts @chunk, '`'
-    return false unless script: @balanced_token supress: false, ['`', '`']
+    return false unless script: @balanced_token ['`', '`']
     @token 'JS', script.replace(JS_CLEANER, '')
     @i += script.length
     true
@@ -127,7 +127,7 @@ exports.Lexer: class Lexer
   # JavaScript and Ruby.
   regex_token: ->
     return false unless starts @chunk, '/'
-    return false unless regex: @balanced_token supress: true, ['/', '/']
+    return false unless regex: @balanced_token ['/', '/']
     return false if regex.length < 3 or regex.match /^\/\s+|\n/
     return false if include NOT_REGEX, @tag()
     flags: ['i', 'm', 'g', 'y']
@@ -153,8 +153,8 @@ exports.Lexer: class Lexer
 
   # Matches a token in which which the passed delimiter pairs must be correctly
   # balanced (ie. strings, JS literals).
-  balanced_token: (supress, delimited...) ->
-    @balanced_string @chunk, supress, delimited...
+  balanced_token: (delimited...) ->
+    @balanced_string @chunk, delimited...
 
   # Matches and conumes comments. We pass through comments into JavaScript,
   # so they're treated as real tokens, like any other part of the language.
@@ -316,7 +316,7 @@ exports.Lexer: class Lexer
   # a series of delimiters, all of which must be nested correctly within the
   # contents of the string. This method allows us to have strings within
   # interpolations within strings etc...
-  balanced_string: (str, supress, delimited...) ->
+  balanced_string: (str, delimited...) ->
     levels: []
     i: 0
     while i < str.length
@@ -337,7 +337,7 @@ exports.Lexer: class Lexer
       break unless levels.length
       i += 1
     if levels.length
-      throw new Error "SyntaxError: Unterminated ${levels.pop()[0]} starting on line ${@line + 1}" unless supress
+      throw new Error "SyntaxError: Unterminated ${levels.pop()[0]} starting on line ${@line + 1}" unless delimited[0][0] is '/'
       return false
     return false if i is 0
     return str.substring(0, i)
@@ -370,7 +370,7 @@ exports.Lexer: class Lexer
           tokens.push ['IDENTIFIER', interp]
           i += group.length - 1
           pi: i + 1
-        else if (expr: @balanced_string str.substring(i), supress: false, ['${', '}'])
+        else if (expr: @balanced_string str.substring(i), ['${', '}'])
           tokens.push ['STRING', "$quote${ str.substring(pi, i) }$quote"] if pi < i
           inner: expr.substring(2, expr.length - 1)
           if inner.length

From 3396dce2bbbe2dbd2eb29cc257279997b2dd83a9 Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas 
Date: Mon, 8 Mar 2010 20:57:28 -0500
Subject: [PATCH 10/10] waypoint -- going to try to clean up regex_token

---
 Cakefile         |  4 ++--
 lib/lexer.js     | 14 +++++++-------
 src/lexer.coffee |  8 ++++----
 3 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/Cakefile b/Cakefile
index dc436688..1bd9240e 100644
--- a/Cakefile
+++ b/Cakefile
@@ -28,8 +28,8 @@ task 'build', 'build the CoffeeScript language from source', ->
   run ['-c', '-o', 'lib'].concat(files)
 
 
-task 'build:full', 'checkout /lib, rebuild the source twice, and run the tests', ->
-  exec 'git checkout lib && bin/cake build && bin/cake build && bin/cake test', (err, stdout, stderr) ->
+task 'build:full', 'rebuild the source twice, and run the tests', ->
+  exec 'bin/cake build && bin/cake build && bin/cake test', (err, stdout, stderr) ->
     print stdout if stdout
     print stderr if stderr
     throw err    if err
diff --git a/lib/lexer.js b/lib/lexer.js
index bef8b616..ee349cce 100644
--- a/lib/lexer.js
+++ b/lib/lexer.js
@@ -175,13 +175,13 @@
       if (!(starts(this.chunk, '/'))) {
         return false;
       }
+      if (include(NOT_REGEX, this.tag())) {
+        return false;
+      }
       if (!((regex = this.balanced_token(['/', '/'])))) {
         return false;
       }
-      if (regex.length < 3 || regex.match(/^\/\s+|\n/)) {
-        return false;
-      }
-      if (include(NOT_REGEX, this.tag())) {
+      if (regex.length < 3 || regex.match(/^\/\s+/m)) {
         return false;
       }
       flags = ['i', 'm', 'g', 'y'];
@@ -462,10 +462,10 @@
         i += 1;
       }
       if (levels.length) {
-        if (!(delimited[0][0] === '/')) {
-          throw new Error("SyntaxError: Unterminated " + (levels.pop()[0]) + " starting on line " + (this.line + 1));
+        if (delimited[0][0] === '/') {
+          return false;
         }
-        return false;
+        throw new Error("SyntaxError: Unterminated " + (levels.pop()[0]) + " starting on line " + (this.line + 1));
       }
       if (i === 0) {
         return false;
diff --git a/src/lexer.coffee b/src/lexer.coffee
index 27957c85..9e71baf3 100644
--- a/src/lexer.coffee
+++ b/src/lexer.coffee
@@ -127,9 +127,9 @@ exports.Lexer: class Lexer
   # JavaScript and Ruby.
   regex_token: ->
     return false unless starts @chunk, '/'
-    return false unless regex: @balanced_token ['/', '/']
-    return false if regex.length < 3 or regex.match /^\/\s+|\n/
     return false if include NOT_REGEX, @tag()
+    return false unless regex: @balanced_token ['/', '/']
+    return false if regex.length < 3 or regex.match /^\/\s+/m
     flags: ['i', 'm', 'g', 'y']
     while (index: flags.indexOf @chunk.substr regex.length, 1) >= 0
       regex += flags[index]
@@ -337,8 +337,8 @@ exports.Lexer: class Lexer
       break unless levels.length
       i += 1
     if levels.length
-      throw new Error "SyntaxError: Unterminated ${levels.pop()[0]} starting on line ${@line + 1}" unless delimited[0][0] is '/'
-      return false
+      return false if delimited[0][0] is '/'
+      throw new Error "SyntaxError: Unterminated ${levels.pop()[0]} starting on line ${@line + 1}"
     return false if i is 0
     return str.substring(0, i)