merging node into master -- you can now pass the --narwhal flag to use narwhal instead. All tests are executing successfully against both Node.js and Narwhal/Rhino backends

This commit is contained in:
Jeremy Ashkenas
2010-02-07 12:52:07 -05:00
61 changed files with 3918 additions and 1236 deletions

View File

@@ -17,9 +17,14 @@ namespace :build do
sh "racc #{args[:racc_args]} -o lib/coffee_script/parser.rb lib/coffee_script/grammar.y"
end
desc "Compile the Narwhal interface for --interactive and --run"
desc "Compile the Narwhal interface"
task :narwhal do
sh "bin/coffee lib/coffee_script/narwhal/*.coffee -o lib/coffee_script/narwhal"
sh "bin/coffee src/narwhal/*.coffee -o lib/coffee_script/narwhal"
end
desc "Continually compile the CoffeeScript/Node.js components with --watch"
task :node do
sh "bin/coffee -w src/*.coffee -o lib/coffee_script/"
end
desc "Compile and install the Ultraviolet syntax highlighter"

View File

@@ -118,7 +118,7 @@ gem install coffee-script</pre>
Installing the gem provides the <tt>coffee</tt> command, which can
be used to compile CoffeeScript <tt>.coffee</tt> files into JavaScript, as
well as debug them. In conjunction with
<a href="http://narwhaljs.org/">Narwhal</a>, the <tt>coffee</tt>
<a href="http://nodejs.org/">Node.js</a>, the <tt>coffee</tt>
command also provides direct evaluation and an interactive REPL.
When compiling to JavaScript, <tt>coffee</tt> writes the output
as <tt>.js</tt> files in the same directory by default, but output
@@ -130,14 +130,14 @@ gem install coffee-script</pre>
<td width="25%"><code>-i, --interactive</code></td>
<td>
Launch an interactive CoffeeScript session.
Requires <a href="http://narwhaljs.org/">Narwhal</a>.
Requires <a href="http://nodejs.org/">Node.js</a>.
</td>
</tr>
<tr>
<td><code>-r, --run</code></td>
<td>
Compile and execute scripts without saving the intermediate
JavaScript. Requires <a href="http://narwhaljs.org/">Narwhal</a>.
JavaScript. Requires <a href="http://nodejs.org/">Node.js</a>.
</td>
</tr>
<tr>
@@ -194,7 +194,7 @@ gem install coffee-script</pre>
<td><code>-n, --no-wrap</code></td>
<td>
Compile the JavaScript without the top-level function safety wrapper.
(Used for CoffeeScript as a Narwhal module.)
(Used for CoffeeScript as a Node.js module.)
</td>
</tr>
<tr>

View File

@@ -11,6 +11,6 @@ index: (list, target) ->
if val < target then low: mid + 1 else high: mid
return -1
print(2 is index([10, 20, 30, 40, 50], 30))
print(4 is index([-97, 35, 67, 88, 1200], 1200))
print(0 is index([0, 45, 70], 0))
puts 2 is index([10, 20, 30, 40, 50], 30)
puts 4 is index([-97, 35, 67, 88, 1200], 1200)
puts 0 is index([0, 45, 70], 0)

View File

@@ -8,6 +8,6 @@ runtime: (N) ->
t: n - 1 + sum / n
t
print(runtime(3) is 2.6666666666666665)
print(runtime(5) is 7.4)
print(runtime(8) is 16.92142857142857)
puts runtime(3) is 2.6666666666666665
puts runtime(5) is 7.4
puts runtime(8) is 16.92142857142857

View File

@@ -26,9 +26,9 @@ match_star: (c, regexp, text) ->
return false unless text and (text[0] is c or c is '.')
text: text.slice(1)
print(match("ex", "some text"))
print(match("s..t", "spit"))
print(match("^..t", "buttercup"))
print(match("i..$", "cherries"))
print(match("o*m", "vrooooommm!"))
print(match("^hel*o$", "hellllllo"))
puts match("ex", "some text")
puts match("s..t", "spit")
puts match("^..t", "buttercup")
puts match("i..$", "cherries")
puts match("o*m", "vrooooommm!")
puts match("^hel*o$", "hellllllo")

View File

@@ -19,7 +19,7 @@ binary_search: (items, value) ->
# Test the function.
print(2 is binary_search([10, 20, 30, 40, 50], 30))
print(4 is binary_search([-97, 35, 67, 88, 1200], 1200))
print(0 is binary_search([0, 45, 70], 0))
print(-1 is binary_search([0, 45, 70], 10))
puts(2 is binary_search([10, 20, 30, 40, 50], 30))
puts(4 is binary_search([-97, 35, 67, 88, 1200], 1200))
puts(0 is binary_search([0, 45, 70], 0))
puts(-1 is binary_search([0, 45, 70], 10))

View File

@@ -7,5 +7,5 @@ bubble_sort: (list) ->
# Test the function.
print(bubble_sort([3, 2, 1]).join(' ') is '1 2 3')
print(bubble_sort([9, 2, 7, 0, 1]).join(' ') is '0 1 2 7 9')
puts(bubble_sort([3, 2, 1]).join(' ') is '1 2 3')
puts(bubble_sort([9, 2, 7, 0, 1]).join(' ') is '0 1 2 7 9')

View File

@@ -91,16 +91,16 @@ LinkedList::toString: -> this.toArray().toString()
list: new LinkedList()
list.add("Hi")
print(list.size() is 1)
print(list.item(0) is "Hi")
print(list.item(1) is null)
puts(list.size() is 1)
puts(list.item(0) is "Hi")
puts(list.item(1) is null)
list: new LinkedList()
list.add("zero").add("one").add("two")
print(list.size() is 3)
print(list.item(2) is "two")
print(list.remove(1) is "one")
print(list.item(0) is "zero")
print(list.item(1) is "two")
print(list.size() is 2)
print(list.item(-10) is null)
puts(list.size() is 3)
puts(list.item(2) is "two")
puts(list.remove(1) is "one")
puts(list.item(0) is "zero")
puts(list.item(1) is "two")
puts(list.size() is 2)
puts(list.item(-10) is null)

View File

@@ -31,6 +31,6 @@ is_valid_identifier: (identifier) ->
# Tests.
print(is_valid_identifier("49927398716") is true)
print(is_valid_identifier("4408041234567893") is true)
print(is_valid_identifier("4408041234567890") is false)
puts(is_valid_identifier("49927398716") is true)
puts(is_valid_identifier("4408041234567893") is true)
puts(is_valid_identifier("4408041234567890") is false)

View File

@@ -15,5 +15,5 @@ merge_sort: (list) ->
# Test the function.
print(merge_sort([3, 2, 1]).join(' ') is '1 2 3')
print(merge_sort([9, 2, 7, 0, 1]).join(' ') is '0 1 2 7 9')
puts(merge_sort([3, 2, 1]).join(' ') is '1 2 3')
puts(merge_sort([9, 2, 7, 0, 1]).join(' ') is '0 1 2 7 9')

View File

@@ -9,7 +9,7 @@ selection_sort: (list) ->
min: i
# Check the rest of the array to see if anything is smaller.
(min: j if list[j] < list[min]) for j in [i+1...len]
(min: j if list[j] < list[min]) for j in [(i+1)...len]
# Swap if a smaller item has been found.
[list[i], list[min]]: [list[min], list[i]] if i isnt min
@@ -19,5 +19,5 @@ selection_sort: (list) ->
# Test the function.
print(selection_sort([3, 2, 1]).join(' ') is '1 2 3')
print(selection_sort([9, 2, 7, 0, 1]).join(' ') is '0 1 2 7 9')
puts(selection_sort([3, 2, 1]).join(' ') is '1 2 3')
puts(selection_sort([9, 2, 7, 0, 1]).join(' ') is '0 1 2 7 9')

View File

@@ -215,7 +215,7 @@ gem install coffee-script</pre>
Installing the gem provides the <tt>coffee</tt> command, which can
be used to compile CoffeeScript <tt>.coffee</tt> files into JavaScript, as
well as debug them. In conjunction with
<a href="http://narwhaljs.org/">Narwhal</a>, the <tt>coffee</tt>
<a href="http://nodejs.org/">Node.js</a>, the <tt>coffee</tt>
command also provides direct evaluation and an interactive REPL.
When compiling to JavaScript, <tt>coffee</tt> writes the output
as <tt>.js</tt> files in the same directory by default, but output
@@ -227,14 +227,14 @@ gem install coffee-script</pre>
<td width="25%"><code>-i, --interactive</code></td>
<td>
Launch an interactive CoffeeScript session.
Requires <a href="http://narwhaljs.org/">Narwhal</a>.
Requires <a href="http://nodejs.org/">Node.js</a>.
</td>
</tr>
<tr>
<td><code>-r, --run</code></td>
<td>
Compile and execute scripts without saving the intermediate
JavaScript. Requires <a href="http://narwhaljs.org/">Narwhal</a>.
JavaScript. Requires <a href="http://nodejs.org/">Node.js</a>.
</td>
</tr>
<tr>
@@ -291,7 +291,7 @@ gem install coffee-script</pre>
<td><code>-n, --no-wrap</code></td>
<td>
Compile the JavaScript without the top-level function safety wrapper.
(Used for CoffeeScript as a Narwhal module.)
(Used for CoffeeScript as a Node.js module.)
</td>
</tr>
<tr>

View File

@@ -0,0 +1,50 @@
(function(){
var compiler, path;
// Executes the `coffee` Ruby program to convert from CoffeeScript to JavaScript.
path = require('path');
// The path to the CoffeeScript executable.
compiler = path.normalize(path.dirname(__filename) + '/../../bin/coffee');
// Compile a string over stdin, with global variables, for the REPL.
exports.compile = function compile(code, callback) {
var coffee, js;
js = '';
coffee = process.createChildProcess(compiler, ['--eval', '--no-wrap', '--globals']);
coffee.addListener('output', function(results) {
if ((typeof results !== "undefined" && results !== null)) {
return js += results;
}
});
coffee.addListener('exit', function() {
return callback(js);
});
coffee.write(code);
return coffee.close();
};
// Compile a list of CoffeeScript files on disk.
exports.compile_files = function compile_files(paths, callback) {
var coffee, exit_ran, js;
js = '';
coffee = process.createChildProcess(compiler, ['--print'].concat(paths));
coffee.addListener('output', function(results) {
if ((typeof results !== "undefined" && results !== null)) {
return js += results;
}
});
// NB: we have to add a mutex to make sure it doesn't get called twice.
exit_ran = false;
coffee.addListener('exit', function() {
if (exit_ran) {
return null;
}
exit_ran = true;
return callback(js);
});
return coffee.addListener('error', function(message) {
if (!(message)) {
return null;
}
puts(message);
throw new Error("CoffeeScript compile error");
});
};
})();

View File

@@ -28,8 +28,11 @@ Usage:
# Path to the root of the CoffeeScript install.
ROOT = File.expand_path(File.dirname(__FILE__) + '/../..')
# Command to execute in Narwhal
LAUNCHER = "narwhal -p #{ROOT} -e 'require(\"coffee-script\").run(system.args);'"
# Commands to execute CoffeeScripts.
RUNNERS = {
:node => "node #{ROOT}/lib/coffee_script/runner.js",
:narwhal => "narwhal -p #{ROOT} -e 'require(\"coffee-script\").run(system.args);'"
}
# Run the CommandLine off the contents of ARGV.
def initialize
@@ -114,20 +117,20 @@ Usage:
puts js
end
# Use Narwhal to run an interactive CoffeeScript session.
# Use Node.js or Narwhal to run an interactive CoffeeScript session.
def launch_repl
exec "#{LAUNCHER}"
exec "#{RUNNERS[@options[:runner]]}"
rescue Errno::ENOENT
puts "Error: Narwhal must be installed to use the interactive REPL."
puts "Error: #{@options[:runner]} must be installed to use the interactive REPL."
exit(1)
end
# Use Narwhal to compile and execute CoffeeScripts.
# Use Node.js or Narwhal to compile and execute CoffeeScripts.
def run_scripts
sources = @sources.join(' ')
exec "#{LAUNCHER} #{sources}"
exec "#{RUNNERS[@options[:runner]]} #{sources}"
rescue Errno::ENOENT
puts "Error: Narwhal must be installed in order to execute CoffeeScripts."
puts "Error: #{@options[:runner]} must be installed in order to execute scripts."
exit(1)
end
@@ -166,12 +169,12 @@ Usage:
# Use OptionParser for all the options.
def parse_options
@options = {}
@options = {:runner => :node}
@option_parser = OptionParser.new do |opts|
opts.on('-i', '--interactive', 'run a CoffeeScript REPL (requires Narwhal)') do |i|
opts.on('-i', '--interactive', 'run an interactive CoffeeScript REPL') do |i|
@options[:interactive] = true
end
opts.on('-r', '--run', 'compile and run a script (requires Narwhal)') do |r|
opts.on('-r', '--run', 'compile and run a CoffeeScript') do |r|
@options[:run] = true
end
opts.on('-o', '--output [DIR]', 'set the directory for compiled JavaScript') do |d|
@@ -202,6 +205,9 @@ Usage:
opts.on('-g', '--globals', 'attach all top-level variable as globals') do |n|
@options[:globals] = true
end
opts.on_tail('--narwhal', 'use Narwhal instead of Node.js') do |n|
@options[:runner] = :narwhal
end
opts.on_tail('--install-bundle', 'install the CoffeeScript TextMate bundle') do |i|
install_bundle
exit

View File

@@ -13,6 +13,7 @@ token FOR IN OF BY WHEN WHILE
token SWITCH LEADING_WHEN
token DELETE INSTANCEOF TYPEOF
token SUPER EXTENDS
token ASSIGN RETURN
token NEWLINE
token COMMENT
token JS
@@ -20,24 +21,20 @@ token INDENT OUTDENT
# Declare order of operations.
prechigh
left '?'
nonassoc UMINUS UPLUS NOT '!' '!!' '~' '++' '--'
left '*' '/' '%'
left '*' '/' '%' '?' '.'
left '+' '-'
left '<<' '>>' '>>>'
left '&' '|' '^'
left '<<' '>>' '>>>' '&' '|' '^'
left '<=' '<' '>' '>='
right '==' '!=' IS ISNT
left '&&' '||' AND OR
right '-=' '+=' '/=' '*=' '%='
right '-=' '+=' '/=' '*=' '%=' '||=' '&&=' '?='
right DELETE INSTANCEOF TYPEOF
left '.'
right INDENT
left OUTDENT
right WHEN LEADING_WHEN IN OF BY
right THROW FOR NEW SUPER
left EXTENDS
left '||=' '&&=' '?='
right ASSIGN RETURN
right '->' '=>' UNLESS IF ELSE WHILE
preclow

362
lib/coffee_script/lexer.js Normal file
View File

@@ -0,0 +1,362 @@
(function(){
var ASSIGNMENT, CALLABLE, CODE, COMMENT, COMMENT_CLEANER, HEREDOC, HEREDOC_INDENT, IDENTIFIER, JS, JS_CLEANER, KEYWORDS, LAST_DENT, LAST_DENTS, MULTILINER, MULTI_DENT, NOT_REGEX, NO_NEWLINE, NUMBER, OPERATOR, REGEX, Rewriter, STRING, STRING_NEWLINES, WHITESPACE, lex, sys;
sys = require('sys');
Rewriter = require('./rewriter').Rewriter;
// The lexer reads a stream of CoffeeScript and divvys it up into tagged
// tokens. A minor bit of the ambiguity in the grammar has been avoided by
// pushing some extra smarts into the Lexer.
exports.Lexer = (lex = function lex() { });
// Constants ============================================================
// The list of keywords passed verbatim to the parser.
KEYWORDS = ["if", "else", "then", "unless", "true", "false", "yes", "no", "on", "off", "and", "or", "is", "isnt", "not", "new", "return", "arguments", "try", "catch", "finally", "throw", "break", "continue", "for", "in", "of", "by", "where", "while", "delete", "instanceof", "typeof", "switch", "when", "super", "extends"];
// Token matching regexes.
IDENTIFIER = /^([a-zA-Z$_](\w|\$)*)/;
NUMBER = /^(\b((0(x|X)[0-9a-fA-F]+)|([0-9]+(\.[0-9]+)?(e[+\-]?[0-9]+)?)))\b/i;
STRING = /^(""|''|"([\s\S]*?)([^\\]|\\\\)"|'([\s\S]*?)([^\\]|\\\\)')/;
HEREDOC = /^("{6}|'{6}|"{3}\n?([\s\S]*?)\n?([ \t]*)"{3}|'{3}\n?([\s\S]*?)\n?([ \t]*)'{3})/;
JS = /^(``|`([\s\S]*?)([^\\]|\\\\)`)/;
OPERATOR = /^([+\*&|\/\-%=<>:!?]+)/;
WHITESPACE = /^([ \t]+)/;
COMMENT = /^(((\n?[ \t]*)?#.*$)+)/;
CODE = /^((-|=)>)/;
REGEX = /^(\/(.*?)([^\\]|\\\\)\/[imgy]{0,4})/;
MULTI_DENT = /^((\n([ \t]*))+)(\.)?/;
LAST_DENTS = /\n([ \t]*)/g;
LAST_DENT = /\n([ \t]*)/;
ASSIGNMENT = /^(:|=)$/;
// Token cleaning regexes.
JS_CLEANER = /(^`|`$)/g;
MULTILINER = /\n/g;
STRING_NEWLINES = /\n[ \t]*/g;
COMMENT_CLEANER = /(^[ \t]*#|\n[ \t]*$)/mg;
NO_NEWLINE = /^([+\*&|\/\-%=<>:!.\\][<>=&|]*|and|or|is|isnt|not|delete|typeof|instanceof)$/;
HEREDOC_INDENT = /^[ \t]+/g;
// Tokens which a regular expression will never immediately follow, but which
// a division operator might.
// See: http://www.mozilla.org/js/language/js20-2002-04/rationale/syntax.html#regular-expressions
NOT_REGEX = ['IDENTIFIER', 'NUMBER', 'REGEX', 'STRING', ')', '++', '--', ']', '}', 'FALSE', 'NULL', 'TRUE'];
// Tokens which could legitimately be invoked or indexed.
CALLABLE = ['IDENTIFIER', 'SUPER', ')', ']', '}', 'STRING'];
// Scan by attempting to match tokens one character at a time. Slow and steady.
lex.prototype.tokenize = function tokenize(code) {
this.code = code;
// Cleanup code by remove extra line breaks, TODO: chomp
this.i = 0;
// Current character position we're parsing
this.line = 1;
// The current line.
this.indent = 0;
// The current indent level.
this.indents = [];
// The stack of all indent levels we are currently within.
this.tokens = [];
// Collection of all parsed tokens in the form [:TOKEN_TYPE, value]
this.spaced = null;
// The last token that has a space following it.
while (this.i < this.code.length) {
this.chunk = this.code.slice(this.i);
this.extract_next_token();
}
// sys.puts "original stream: " + this.tokens if process.ENV['VERBOSE']
this.close_indentation();
return (new Rewriter()).rewrite(this.tokens);
};
// At every position, run through this list of attempted matches,
// short-circuiting if any of them succeed.
lex.prototype.extract_next_token = function extract_next_token() {
if (this.identifier_token()) {
return null;
}
if (this.number_token()) {
return null;
}
if (this.heredoc_token()) {
return null;
}
if (this.string_token()) {
return null;
}
if (this.js_token()) {
return null;
}
if (this.regex_token()) {
return null;
}
if (this.indent_token()) {
return null;
}
if (this.comment_token()) {
return null;
}
if (this.whitespace_token()) {
return null;
}
return this.literal_token();
};
// Tokenizers ==========================================================
// Matches identifying literals: variables, keywords, method names, etc.
lex.prototype.identifier_token = function identifier_token() {
var id, tag;
if (!((id = this.match(IDENTIFIER, 1)))) {
return false;
}
// Keywords are special identifiers tagged with their own name,
// 'if' will result in an ['IF', "if"] token.
tag = KEYWORDS.indexOf(id) >= 0 ? id.toUpperCase() : 'IDENTIFIER';
if (tag === 'WHEN' && (this.tag() === 'OUTDENT' || this.tag() === 'INDENT')) {
tag = 'LEADING_WHEN';
}
if (tag === 'IDENTIFIER' && this.value() === '::') {
this.tag(-1, 'PROTOTYPE_ACCESS');
}
if (tag === 'IDENTIFIER' && this.value() === '.' && !(this.value(-2) === '.')) {
if (this.tag(-2) === '?') {
this.tag(-1, 'SOAK_ACCESS');
this.tokens.splice(-2, 1);
} else {
this.tag(-1, 'PROPERTY_ACCESS');
}
}
this.token(tag, id);
this.i += id.length;
return true;
};
// Matches numbers, including decimals, hex, and exponential notation.
lex.prototype.number_token = function number_token() {
var number;
if (!((number = this.match(NUMBER, 1)))) {
return false;
}
this.token('NUMBER', number);
this.i += number.length;
return true;
};
// Matches strings, including multi-line strings.
lex.prototype.string_token = function string_token() {
var escaped, string;
if (!((string = this.match(STRING, 1)))) {
return false;
}
escaped = string.replace(STRING_NEWLINES, " \\\n");
this.token('STRING', escaped);
this.line += this.count(string, "\n");
this.i += string.length;
return true;
};
// Matches heredocs, adjusting indentation to the correct level.
lex.prototype.heredoc_token = function heredoc_token() {
var doc, indent, match;
if (!((match = this.chunk.match(HEREDOC)))) {
return false;
}
doc = match[2] || match[4];
indent = doc.match(HEREDOC_INDENT).sort()[0];
doc = doc.replace(new RegExp("^" + indent, 'g'), '').replace(MULTILINER, "\\n").replace('"', '\\"');
this.token('STRING', '"' + doc + '"');
this.line += this.count(match[1], "\n");
this.i += match[1].length;
return true;
};
// Matches interpolated JavaScript.
lex.prototype.js_token = function js_token() {
var script;
if (!((script = this.match(JS, 1)))) {
return false;
}
this.token('JS', script.replace(JS_CLEANER, ''));
this.i += script.length;
return true;
};
// Matches regular expression literals.
lex.prototype.regex_token = function regex_token() {
var regex;
if (!((regex = this.match(REGEX, 1)))) {
return false;
}
if (NOT_REGEX.indexOf(this.tag()) >= 0) {
return false;
}
this.token('REGEX', regex);
this.i += regex.length;
return true;
};
// Matches and conumes comments.
lex.prototype.comment_token = function comment_token() {
var comment;
if (!((comment = this.match(COMMENT, 1)))) {
return false;
}
this.line += comment.match(MULTILINER).length;
this.token('COMMENT', comment.replace(COMMENT_CLEANER, '').split(MULTILINER));
this.token("\n", "\n");
this.i += comment.length;
return true;
};
// Record tokens for indentation differing from the previous line.
lex.prototype.indent_token = function indent_token() {
var diff, indent, next_character, no_newlines, size;
if (!((indent = this.match(MULTI_DENT, 1)))) {
return false;
}
this.line += indent.match(MULTILINER).length;
this.i += indent.length;
next_character = this.chunk.match(MULTI_DENT)[4];
no_newlines = next_character === '.' || (this.value().match(NO_NEWLINE) && this.tokens[this.tokens.length - 2][0] !== '.' && !this.value().match(CODE));
if (no_newlines) {
return this.suppress_newlines(indent);
}
size = indent.match(LAST_DENTS).reverse()[0].match(LAST_DENT)[1].length;
if (size === this.indent) {
return this.newline_token(indent);
}
if (size > this.indent) {
diff = size - this.indent;
this.token('INDENT', diff);
this.indents.push(diff);
} else {
this.outdent_token(this.indent - size);
}
this.indent = size;
return true;
};
// Record an oudent token or tokens, if we're moving back inwards past
// multiple recorded indents.
lex.prototype.outdent_token = function outdent_token(move_out) {
var last_indent;
while (move_out > 0 && this.indents.length) {
last_indent = this.indents.pop();
this.token('OUTDENT', last_indent);
move_out -= last_indent;
}
this.token("\n", "\n");
return true;
};
// Matches and consumes non-meaningful whitespace.
lex.prototype.whitespace_token = function whitespace_token() {
var space;
if (!((space = this.match(WHITESPACE, 1)))) {
return false;
}
this.spaced = this.value();
this.i += space.length;
return true;
};
// Multiple newlines get merged together.
// Use a trailing \ to escape newlines.
lex.prototype.newline_token = function newline_token(newlines) {
if (!(this.value() === "\n")) {
this.token("\n", "\n");
}
return true;
};
// Tokens to explicitly escape newlines are removed once their job is done.
lex.prototype.suppress_newlines = function suppress_newlines(newlines) {
if (this.value() === "\\") {
this.tokens.pop();
}
return true;
};
// We treat all other single characters as a token. Eg.: ( ) , . !
// Multi-character operators are also literal tokens, so that Racc can assign
// the proper order of operations.
lex.prototype.literal_token = function literal_token() {
var match, tag, value;
match = this.chunk.match(OPERATOR);
value = match && match[1];
if (value && value.match(CODE)) {
this.tag_parameters();
}
value = value || this.chunk.substr(0, 1);
tag = value.match(ASSIGNMENT) ? 'ASSIGN' : value;
if (this.value() !== this.spaced && CALLABLE.indexOf(this.tag()) >= 0) {
if (value === '(') {
tag = 'CALL_START';
}
if (value === '[') {
tag = 'INDEX_START';
}
}
this.token(tag, value);
this.i += value.length;
return true;
};
// Helpers =============================================================
// Add a token to the results, taking note of the line number.
lex.prototype.token = function token(tag, value) {
return this.tokens.push([tag, value]);
// this.tokens.push([tag, Value.new(value, @line)])
};
// Look at a tag in the current token stream.
lex.prototype.tag = function tag(index, tag) {
var tok;
if (!((tok = this.tokens[this.tokens.length - (index || 1)]))) {
return null;
}
if ((typeof tag !== "undefined" && tag !== null)) {
return (tok[0] = tag);
}
return tok[0];
};
// Look at a value in the current token stream.
lex.prototype.value = function value(index, val) {
var tok;
if (!((tok = this.tokens[this.tokens.length - (index || 1)]))) {
return null;
}
if ((typeof val !== "undefined" && val !== null)) {
return (tok[1] = val);
}
return tok[1];
};
// Count the occurences of a character in a string.
lex.prototype.count = function count(string, char) {
var num, pos;
num = 0;
pos = string.indexOf(char);
while (pos !== -1) {
count += 1;
pos = string.indexOf(char, pos + 1);
}
return count;
};
// Attempt to match a string against the current chunk, returning the indexed
// match.
lex.prototype.match = function match(regex, index) {
var m;
if (!((m = this.chunk.match(regex)))) {
return false;
}
return m ? m[index] : false;
};
// A source of ambiguity in our grammar was parameter lists in function
// definitions (as opposed to argument lists in function calls). Tag
// parameter identifiers in order to avoid this. Also, parameter lists can
// make use of splats.
lex.prototype.tag_parameters = function tag_parameters() {
var i, tok;
if (this.tag() !== ')') {
return null;
}
i = 0;
while (true) {
i += 1;
tok = this.tokens[this.tokens.length - i];
if (!tok) {
return null;
}
if (tok[0] === 'IDENTIFIER') {
tok[0] = 'PARAM';
} else if (tok[0] === ')') {
tok[0] = 'PARAM_END';
} else if (tok[0] === '(') {
return (tok[0] = 'PARAM_START');
}
}
return true;
};
// Close up all remaining open blocks. IF the first token is an indent,
// axe it.
lex.prototype.close_indentation = function close_indentation() {
return this.outdent_token(this.indent);
};
})();

View File

@@ -228,8 +228,7 @@ module CoffeeScript
# Helpers ==========================================================
# Add a token to the results, taking note of the line number, and
# immediately-preceding comment.
# Add a token to the results, taking note of the line number.
def token(tag, value)
@tokens << [tag, Value.new(value, @line)]
end

View File

@@ -1,5 +1,5 @@
(function(){
var File, OS, Readline, checkForErrors, coffeePath, factories, loader;
var File, OS, Readline, checkForErrors, coffeePath, factories, loader, puts;
// The Narwhal-compatibility wrapper for CoffeeScript.
// Require external dependencies.
OS = require('os');
@@ -15,6 +15,8 @@
system.stderr.print(coffeeProcess.stderr.read());
throw new Error("CoffeeScript compile error");
};
// Alias print to "puts", for Node.js compatibility:
puts = print;
// Run a simple REPL, round-tripping to the CoffeeScript compiler for every
// command.
exports.run = function run(args) {

View File

@@ -0,0 +1,34 @@
(function(){
exports.Node = function Node() {
var __a;
var arguments = Array.prototype.slice.call(arguments, 0);
__a = this.values = arguments;
return Node === this.constructor ? this : __a;
};
exports.Expressions = exports.Node;
exports.LiteralNode = exports.Node;
exports.ReturnNode = exports.Node;
exports.CommentNode = exports.Node;
exports.CallNode = exports.Node;
exports.ExtendsNode = exports.Node;
exports.ValueNode = exports.Node;
exports.AccessorNode = exports.Node;
exports.IndexNode = exports.Node;
exports.RangeNode = exports.Node;
exports.SliceNode = exports.Node;
exports.AssignNode = exports.Node;
exports.OpNode = exports.Node;
exports.CodeNode = exports.Node;
exports.SplatNode = exports.Node;
exports.ObjectNode = exports.Node;
exports.ArrayNode = exports.Node;
exports.PushNode = exports.Node;
exports.ClosureNode = exports.Node;
exports.WhileNode = exports.Node;
exports.ForNode = exports.Node;
exports.TryNode = exports.Node;
exports.ThrowNode = exports.Node;
exports.ExistenceNode = exports.Node;
exports.ParentheticalNode = exports.Node;
exports.IfNode = exports.Node;
})();

620
lib/coffee_script/parser.js Normal file
View File

@@ -0,0 +1,620 @@
(function(){
var Parser, __a, __b, __c, __d, __e, __f, bnf, grammar, name, non_terminal, o, operators, option, parser, part, tokens, unwrap;
var __hasProp = Object.prototype.hasOwnProperty;
Parser = require('jison').Parser;
// DSL ===================================================================
// Detect functions: [
unwrap = /function\s*\(\)\s*\{\s*return\s*([\s\S]*);\s*\}/;
// Quickie DSL for Jison access.
o = function o(pattern_string, func) {
var match;
if (func) {
func = (match = (func + "").match(unwrap)) ? match[1] : '(' + func + '())';
return [pattern_string, '$$ = ' + func + ';'];
} else {
return [pattern_string, '$$ = $1;'];
}
};
// Precedence ===========================================================
operators = [["left", '?'], ["right", 'NOT', '!', '!!', '~', '++', '--'], ["left", '*', '/', '%'], ["left", '+', '-'], ["left", '<<', '>>', '>>>'], ["left", '&', '|', '^'], ["left", '<=', '<', '>', '>='], ["right", '==', '!=', 'IS', 'ISNT'], ["left", '&&', '||', 'AND', 'OR'], ["right", '-=', '+=', '/=', '*=', '%='], ["right", 'DELETE', 'INSTANCEOF', 'TYPEOF'], ["left", '.'], ["right", 'INDENT'], ["left", 'OUTDENT'], ["right", 'WHEN', 'LEADING_WHEN', 'IN', 'OF', 'BY'], ["right", 'THROW', 'FOR', 'NEW', 'SUPER'], ["left", 'EXTENDS'], ["left", '||=', '&&=', '?='], ["right", 'ASSIGN', 'RETURN'], ["right", '->', '=>', 'UNLESS', 'IF', 'ELSE', 'WHILE']];
// Grammar ==============================================================
grammar = {
// All parsing will end in this rule, being the trunk of the AST.
Root: [o("", function() {
return new Expressions();
}), o("Terminator", function() {
return new Expressions();
}), o("Expressions"), o("Block Terminator")
],
// Any list of expressions or method body, seperated by line breaks or semis.
Expressions: [o("Expression", function() {
return Expressions.wrap([$1]);
}), o("Expressions Terminator Expression", function() {
return $1.push($3);
}), o("Expressions Terminator")
],
// All types of expressions in our language. The basic unit of CoffeeScript
// is the expression.
Expression: [o("Value"), o("Call"), o("Code"), o("Operation"), o("Assign"), o("If"), o("Try"), o("Throw"), o("Return"), o("While"), o("For"), o("Switch"), o("Extends"), o("Splat"), o("Existence"), o("Comment")],
// A block of expressions. Note that the Rewriter will convert some postfix
// forms into blocks for us, by altering the token stream.
Block: [o("INDENT Expressions OUTDENT", function() {
return $2;
}), o("INDENT OUTDENT", function() {
return new Expressions();
})
],
// Tokens that can terminate an expression.
Terminator: [o("\n"), o(";")],
// All hard-coded values. These can be printed straight to JavaScript.
Literal: [o("NUMBER", function() {
return new LiteralNode($1);
}), o("STRING", function() {
return new LiteralNode($1);
}), o("JS", function() {
return new LiteralNode($1);
}), o("REGEX", function() {
return new LiteralNode($1);
}), o("BREAK", function() {
return new LiteralNode($1);
}), o("CONTINUE", function() {
return new LiteralNode($1);
}), o("ARGUMENTS", function() {
return new LiteralNode($1);
}), o("TRUE", function() {
return new LiteralNode(true);
}), o("FALSE", function() {
return new LiteralNode(false);
}), o("YES", function() {
return new LiteralNode(true);
}), o("NO", function() {
return new LiteralNode(false);
}), o("ON", function() {
return new LiteralNode(true);
}), o("OFF", function() {
return new LiteralNode(false);
})
],
// Assignment to a variable (or index).
Assign: [o("Value ASSIGN Expression", function() {
return new AssignNode($1, $3);
})
],
// Assignment within an object literal (can be quoted).
AssignObj: [o("IDENTIFIER ASSIGN Expression", function() {
return new AssignNode(new ValueNode($1), $3, 'object');
}), o("STRING ASSIGN Expression", function() {
return new AssignNode(new ValueNode(new LiteralNode($1)), $3, 'object');
}), o("Comment")
],
// A return statement.
Return: [o("RETURN Expression", function() {
return new ReturnNode($2);
}), o("RETURN", function() {
return new ReturnNode(new ValueNode(new LiteralNode('null')));
})
],
// A comment.
Comment: [o("COMMENT", function() {
return new CommentNode($1);
})
],
// Arithmetic and logical operators
// For Ruby's Operator precedence, see: [
// https://www.cs.auckland.ac.nz/references/ruby/ProgrammingRuby/language.html
Operation: [o("! Expression", function() {
return new OpNode($1, $2);
}), o("!! Expression", function() {
return new OpNode($1, $2);
}), o("- Expression", function() {
return new OpNode($1, $2);
}), o("+ Expression", function() {
return new OpNode($1, $2);
}), o("NOT Expression", function() {
return new OpNode($1, $2);
}), o("~ Expression", function() {
return new OpNode($1, $2);
}), o("-- Expression", function() {
return new OpNode($1, $2);
}), o("++ Expression", function() {
return new OpNode($1, $2);
}), o("DELETE Expression", function() {
return new OpNode($1, $2);
}), o("TYPEOF Expression", function() {
return new OpNode($1, $2);
}), o("Expression --", function() {
return new OpNode($2, $1, null, true);
}), o("Expression ++", function() {
return new OpNode($2, $1, null, true);
}), o("Expression * Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression / Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression % Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression + Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression - Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression << Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression >> Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression >>> Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression & Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression | Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression ^ Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression <= Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression < Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression > Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression >= Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression == Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression != Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression IS Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression ISNT Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression && Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression || Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression AND Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression OR Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression ? Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression -= Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression += Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression /= Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression *= Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression %= Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression ||= Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression &&= Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression ?= Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression INSTANCEOF Expression", function() {
return new OpNode($2, $1, $3);
}), o("Expression IN Expression", function() {
return new OpNode($2, $1, $3);
})
],
// Try abbreviated expressions to make the grammar build faster:
// UnaryOp: [
// o "!"
// o "!!"
// o "NOT"
// o "~"
// o "--"
// o "++"
// o "DELETE"
// o "TYPEOF"
// ]
//
// BinaryOp: [
// o "*"
// o "/"
// o "%"
// o "+"
// o "-"
// o "<<"
// o ">>"
// o ">>>"
// o "&"
// o "|"
// o "^"
// o "<="
// o "<"
// o ">"
// o ">="
// o "=="
// o "!="
// o "IS"
// o "ISNT"
// o "&&"
// o "||"
// o "AND"
// o "OR"
// o "?"
// o "-="
// o "+="
// o "/="
// o "*="
// o "%="
// o "||="
// o "&&="
// o "?="
// o "INSTANCEOF"
// o "IN"
// ]
//
// Operation: [
// o "Expression BinaryOp Expression", -> new OpNode($2, $1, $3)
// o "UnaryOp Expression", -> new OpNode($1, $2)
// ]
// The existence operator.
Existence: [o("Expression ?", function() {
return new ExistenceNode($1);
})
],
// Function definition.
Code: [o("PARAM_START ParamList PARAM_END FuncGlyph Block", function() {
return new CodeNode($2, $5, $4);
}), o("FuncGlyph Block", function() {
return new CodeNode([], $2, $1);
})
],
// The symbols to signify functions, and bound functions.
FuncGlyph: [o("->", function() {
return 'func';
}), o("=>", function() {
return 'boundfunc';
})
],
// The parameters to a function definition.
ParamList: [o("Param", function() {
return [$1];
}), o("ParamList , Param", function() {
return $1.push($3);
})
],
// A Parameter (or ParamSplat) in a function definition.
Param: [o("PARAM"), o("PARAM . . .", function() {
return new SplatNode($1);
})
],
// A regular splat.
Splat: [o("Expression . . .", function() {
return new SplatNode($1);
})
],
// Expressions that can be treated as values.
Value: [o("IDENTIFIER", function() {
return new ValueNode($1);
}), o("Literal", function() {
return new ValueNode($1);
}), o("Array", function() {
return new ValueNode($1);
}), o("Object", function() {
return new ValueNode($1);
}), o("Parenthetical", function() {
return new ValueNode($1);
}), o("Range", function() {
return new ValueNode($1);
}), o("Value Accessor", function() {
return $1.push($2);
}), o("Invocation Accessor", function() {
return new ValueNode($1, [$2]);
})
],
// Accessing into an object or array, through dot or index notation.
Accessor: [o("PROPERTY_ACCESS IDENTIFIER", function() {
return new AccessorNode($2);
}), o("PROTOTYPE_ACCESS IDENTIFIER", function() {
return new AccessorNode($2, 'prototype');
}), o("SOAK_ACCESS IDENTIFIER", function() {
return new AccessorNode($2, 'soak');
}), o("Index"), o("Slice", function() {
return new SliceNode($1);
})
],
// Indexing into an object or array.
Index: [o("INDEX_START Expression INDEX_END", function() {
return new IndexNode($2);
})
],
// An object literal.
Object: [o("{ AssignList }", function() {
return new ObjectNode($2);
})
],
// Assignment within an object literal (comma or newline separated).
AssignList: [o("", function() {
return [];
}), o("AssignObj", function() {
return [$1];
}), o("AssignList , AssignObj", function() {
return $1.push($3);
}), o("AssignList Terminator AssignObj", function() {
return $1.push($3);
}), o("AssignList , Terminator AssignObj", function() {
return $1.push($4);
}), o("INDENT AssignList OUTDENT", function() {
return $2;
})
],
// All flavors of function call (instantiation, super, and regular).
Call: [o("Invocation", function() {
return $1;
}), o("NEW Invocation", function() {
return $2.new_instance();
}), o("Super", function() {
return $1;
})
],
// Extending an object's prototype.
Extends: [o("Value EXTENDS Value", function() {
return new ExtendsNode($1, $3);
})
],
// A generic function invocation.
Invocation: [o("Value Arguments", function() {
return new CallNode($1, $2);
}), o("Invocation Arguments", function() {
return new CallNode($1, $2);
})
],
// The list of arguments to a function invocation.
Arguments: [o("CALL_START ArgList CALL_END", function() {
return $2;
})
],
// Calling super.
Super: [o("SUPER CALL_START ArgList CALL_END", function() {
return new CallNode('super', $3);
})
],
// The range literal.
Range: [o("[ Expression . . Expression ]", function() {
return new RangeNode($2, $5);
}), o("[ Expression . . . Expression ]", function() {
return new RangeNode($2, $6, true);
})
],
// The slice literal.
Slice: [o("INDEX_START Expression . . Expression INDEX_END", function() {
return new RangeNode($2, $5);
}), o("INDEX_START Expression . . . Expression INDEX_END", function() {
return new RangeNode($2, $6, true);
})
],
// The array literal.
Array: [o("[ ArgList ]", function() {
return new ArrayNode($2);
})
],
// A list of arguments to a method call, or as the contents of an array.
ArgList: [o("", function() {
return [];
}), o("Expression", function() {
return val;
}), o("INDENT Expression", function() {
return [$2];
}), o("ArgList , Expression", function() {
return $1.push($3);
}), o("ArgList Terminator Expression", function() {
return $1.push($3);
}), o("ArgList , Terminator Expression", function() {
return $1.push($4);
}), o("ArgList , INDENT Expression", function() {
return $1.push($4);
}), o("ArgList OUTDENT", function() {
return $1;
})
],
// Just simple, comma-separated, required arguments (no fancy syntax).
SimpleArgs: [o("Expression", function() {
return $1;
}), o("SimpleArgs , Expression", function() {
return ([$1].push($3)).reduce(function(a, b) {
return a.concat(b);
});
})
],
// Try/catch/finally exception handling blocks.
Try: [o("TRY Block Catch", function() {
return new TryNode($2, $3[0], $3[1]);
}), o("TRY Block FINALLY Block", function() {
return new TryNode($2, nil, nil, $4);
}), o("TRY Block Catch FINALLY Block", function() {
return new TryNode($2, $3[0], $3[1], $5);
})
],
// A catch clause.
Catch: [o("CATCH IDENTIFIER Block", function() {
return [$2, $3];
})
],
// Throw an exception.
Throw: [o("THROW Expression", function() {
return new ThrowNode($2);
})
],
// Parenthetical expressions.
Parenthetical: [o("( Expression )", function() {
return new ParentheticalNode($2);
})
],
// The while loop. (there is no do..while).
While: [o("WHILE Expression Block", function() {
return new WhileNode($2, $3);
}), o("WHILE Expression", function() {
return new WhileNode($2, nil);
}), o("Expression WHILE Expression", function() {
return new WhileNode($3, Expressions.wrap($1));
})
],
// Array comprehensions, including guard and current index.
// Looks a little confusing, check nodes.rb for the arguments to ForNode.
For: [o("Expression FOR ForVariables ForSource", function() {
return new ForNode($1, $4, $3[0], $3[1]);
}), o("FOR ForVariables ForSource Block", function() {
return new ForNode($4, $3, $2[0], $2[1]);
})
],
// An array comprehension has variables for the current element and index.
ForVariables: [o("IDENTIFIER", function() {
return [$1];
}), o("IDENTIFIER , IDENTIFIER", function() {
return [$1, $3];
})
],
// The source of the array comprehension can optionally be filtered.
ForSource: [o("IN Expression", function() {
return {
source: $2
};
}), o("OF Expression", function() {
return {
source: $2,
object: true
};
}), o("ForSource WHEN Expression", function() {
$1.filter = $3;
return $1;
}), o("ForSource BY Expression", function() {
$1.step = $3;
return $1;
})
],
// Switch/When blocks.
Switch: [o("SWITCH Expression INDENT Whens OUTDENT", function() {
return $4.rewrite_condition($2);
}), o("SWITCH Expression INDENT Whens ELSE Block OUTDENT", function() {
return $4.rewrite_condition($2).add_else($6);
})
],
// The inner list of whens.
Whens: [o("When", function() {
return $1;
}), o("Whens When", function() {
return $1.push($2);
})
],
// An individual when.
When: [o("LEADING_WHEN SimpleArgs Block", function() {
return new IfNode($2, $3, nil, {
statement: true
});
}), o("LEADING_WHEN SimpleArgs Block Terminator", function() {
return new IfNode($2, $3, nil, {
statement: true
});
}), o("Comment Terminator When", function() {
return $3.add_comment($1);
})
],
// The most basic form of "if".
IfBlock: [o("IF Expression Block", function() {
return new IfNode($2, $3);
})
],
// An elsif portion of an if-else block.
ElsIf: [o("ELSE IfBlock", function() {
return $2.force_statement();
})
],
// Multiple elsifs can be chained together.
ElsIfs: [o("ElsIf", function() {
return $1;
}), o("ElsIfs ElsIf", function() {
return $1.add_else($2);
})
],
// Terminating else bodies are strictly optional.
ElseBody: [o("", function() {
return null;
}), o("ELSE Block", function() {
return $2;
})
],
// All the alternatives for ending an if-else block.
IfEnd: [o("ElseBody", function() {
return $1;
}), o("ElsIfs ElseBody", function() {
return $1.add_else($2);
})
],
// The full complement of if blocks, including postfix one-liner ifs and unlesses.
If: [o("IfBlock IfEnd", function() {
return $1.add_else($2);
}), o("Expression IF Expression", function() {
return new IfNode($3, Expressions.wrap($1), nil, {
statement: true
});
}), o("Expression UNLESS Expression", function() {
return new IfNode($3, Expressions.wrap($1), nil, {
statement: true,
invert: true
});
})
]
};
// Helpers ==============================================================
// Make the Jison parser.
bnf = {
};
tokens = [];
__a = grammar;
for (name in __a) {
non_terminal = __a[name];
if (__hasProp.call(__a, name)) {
bnf[name] = (function() {
__b = []; __c = non_terminal;
for (__d = 0; __d < __c.length; __d++) {
option = __c[__d];
__b.push((function() {
__e = option[0].split(" ");
for (__f = 0; __f < __e.length; __f++) {
part = __e[__f];
!grammar[part] ? tokens.push(part) : null;
}
name === "Root" ? (option[1] = "return " + option[1]) : null;
return option;
}).call(this));
}
return __b;
}).call(this);
}
}
tokens = tokens.join(" ");
parser = new Parser({
tokens: tokens,
bnf: bnf,
operators: operators
}, {
debug: false
});
// Thin wrapper around the real lexer
parser.lexer = {
lex: function lex() {
var token;
token = this.tokens[this.pos] || [""];
this.pos += 1;
// this.yylineno: token and token[1] and token[1][1]
this.yytext = token[1];
return token[0];
},
setInput: function setInput(tokens) {
this.tokens = tokens;
return this.pos = 0;
},
upcomingInput: function upcomingInput() {
return "";
},
showPosition: function showPosition() {
return this.pos;
}
};
exports.Parser = function Parser() { };
exports.Parser.prototype.parse = function parse(tokens) {
return parser.parse(tokens);
};
})();

File diff suppressed because it is too large Load Diff

33
lib/coffee_script/repl.js Normal file
View File

@@ -0,0 +1,33 @@
(function(){
var coffee, prompt, quit, readline, run;
// A CoffeeScript port/version of the Node.js REPL.
// Required modules.
coffee = require('./coffee-script');
process.mixin(require('sys'));
// Shortcut variables.
prompt = 'coffee> ';
quit = function quit() {
return process.stdio.close();
};
// The main REPL function. Called everytime a line of code is entered.
readline = function readline(code) {
return coffee.compile(code, run);
};
// Attempt to evaluate the command. If there's an exception, print it.
run = function run(js) {
var val;
try {
val = eval(js);
if (val !== undefined) {
p(val);
}
} catch (err) {
puts(err.stack || err.toString());
}
return print(prompt);
};
// Start up the REPL.
process.stdio.open();
process.stdio.addListener('data', readline);
print(prompt);
})();

View File

@@ -0,0 +1,377 @@
(function(){
var BALANCED_PAIRS, EXPRESSION_CLOSE, EXPRESSION_START, EXPRESSION_TAIL, IMPLICIT_CALL, IMPLICIT_END, IMPLICIT_FUNC, INVERSES, SINGLE_CLOSERS, SINGLE_LINERS, __a, __b, __c, __d, __e, __f, __g, __h, pair, re;
var __hasProp = Object.prototype.hasOwnProperty;
// In order to keep the grammar simple, the stream of tokens that the Lexer
// emits is rewritten by the Rewriter, smoothing out ambiguities, mis-nested
// indentation, and single-line flavors of expressions.
exports.Rewriter = (re = function re() { });
// Tokens that must be balanced.
BALANCED_PAIRS = [['(', ')'], ['[', ']'], ['{', '}'], ['INDENT', 'OUTDENT'], ['PARAM_START', 'PARAM_END'], ['CALL_START', 'CALL_END'], ['INDEX_START', 'INDEX_END']];
// Tokens that signal the start of a balanced pair.
EXPRESSION_START = (function() {
__a = []; __b = BALANCED_PAIRS;
for (__c = 0; __c < __b.length; __c++) {
pair = __b[__c];
__a.push(pair[0]);
}
return __a;
}).call(this);
// Tokens that signal the end of a balanced pair.
EXPRESSION_TAIL = (function() {
__d = []; __e = BALANCED_PAIRS;
for (__f = 0; __f < __e.length; __f++) {
pair = __e[__f];
__d.push(pair[1]);
}
return __d;
}).call(this);
// Tokens that indicate the close of a clause of an expression.
EXPRESSION_CLOSE = ['CATCH', 'WHEN', 'ELSE', 'FINALLY'].concat(EXPRESSION_TAIL);
// Tokens pairs that, in immediate succession, indicate an implicit call.
IMPLICIT_FUNC = ['IDENTIFIER', 'SUPER', ')', 'CALL_END', ']', 'INDEX_END'];
IMPLICIT_END = ['IF', 'UNLESS', 'FOR', 'WHILE', "\n", 'OUTDENT'];
IMPLICIT_CALL = ['IDENTIFIER', 'NUMBER', 'STRING', 'JS', 'REGEX', 'NEW', 'PARAM_START', 'TRY', 'DELETE', 'TYPEOF', 'SWITCH', 'ARGUMENTS', 'TRUE', 'FALSE', 'YES', 'NO', 'ON', 'OFF', '!', '!!', 'NOT', '->', '=>', '[', '(', '{'];
// The inverse mappings of token pairs we're trying to fix up.
INVERSES = {
};
__g = BALANCED_PAIRS;
for (__h = 0; __h < __g.length; __h++) {
pair = __g[__h];
INVERSES[pair[0]] = pair[1];
INVERSES[pair[1]] = pair[0];
}
// Single-line flavors of block expressions that have unclosed endings.
// The grammar can't disambiguate them, so we insert the implicit indentation.
SINGLE_LINERS = ['ELSE', "->", "=>", 'TRY', 'FINALLY', 'THEN'];
SINGLE_CLOSERS = ["\n", 'CATCH', 'FINALLY', 'ELSE', 'OUTDENT', 'LEADING_WHEN', 'PARAM_START'];
// Rewrite the token stream in multiple passes, one logical filter at
// a time. This could certainly be changed into a single pass through the
// stream, with a big ol' efficient switch, but it's much nicer like this.
re.prototype.rewrite = function rewrite(tokens) {
this.tokens = tokens;
this.adjust_comments();
this.remove_leading_newlines();
this.remove_mid_expression_newlines();
this.move_commas_outside_outdents();
this.close_open_calls_and_indexes();
this.add_implicit_parentheses();
this.add_implicit_indentation();
this.ensure_balance(BALANCED_PAIRS);
this.rewrite_closing_parens();
return this.tokens;
};
// Rewrite the token stream, looking one token ahead and behind.
// Allow the return value of the block to tell us how many tokens to move
// forwards (or backwards) in the stream, to make sure we don't miss anything
// as the stream changes length under our feet.
re.prototype.scan_tokens = function scan_tokens(yield) {
var i, move;
i = 0;
while (true) {
if (!(this.tokens[i])) {
break;
}
move = yield(this.tokens[i - 1], this.tokens[i], this.tokens[i + 1], i);
i += move;
}
return true;
};
// Massage newlines and indentations so that comments don't have to be
// correctly indented, or appear on their own line.
re.prototype.adjust_comments = function adjust_comments() {
return this.scan_tokens((function(__this) {
var __func = function(prev, token, post, i) {
var after, before;
if (!(token[0] === 'COMMENT')) {
return 1;
}
before = this.tokens[i - 2];
after = this.tokens[i + 2];
if (before && after && ((before[0] === 'INDENT' && after[0] === 'OUTDENT') || (before[0] === 'OUTDENT' && after[0] === 'INDENT')) && before[1] === after[1]) {
this.tokens.splice(i + 2, 1);
this.tokens.splice(i - 2, 1);
return 0;
} else if (prev[0] === "\n" && after[0] === 'INDENT') {
this.tokens.splice(i + 2, 1);
this.tokens[i - 1] = after;
return 1;
} else if (prev[0] !== "\n" && prev[0] !== 'INDENT' && prev[0] !== 'OUTDENT') {
this.tokens.splice(i, 0, ["\n", "\n"]);
return 2;
} else {
return 1;
}
};
return (function() {
return __func.apply(__this, arguments);
});
})(this));
};
// Leading newlines would introduce an ambiguity in the grammar, so we
// dispatch them here.
re.prototype.remove_leading_newlines = function remove_leading_newlines() {
if (this.tokens[0][0] === "\n") {
return this.tokens.shift();
}
};
// Some blocks occur in the middle of expressions -- when we're expecting
// this, remove their trailing newlines.
re.prototype.remove_mid_expression_newlines = function remove_mid_expression_newlines() {
return this.scan_tokens((function(__this) {
var __func = function(prev, token, post, i) {
if (!(post && EXPRESSION_CLOSE.indexOf(post[0]) >= 0 && token[0] === "\n")) {
return 1;
}
this.tokens.splice(i, 1);
return 0;
};
return (function() {
return __func.apply(__this, arguments);
});
})(this));
};
// Make sure that we don't accidentally break trailing commas, which need
// to go on the outside of expression closers.
re.prototype.move_commas_outside_outdents = function move_commas_outside_outdents() {
return this.scan_tokens((function(__this) {
var __func = function(prev, token, post, i) {
if (token[0] === 'OUTDENT' && prev[0] === ',') {
this.tokens.splice(i, 1, token);
}
return 1;
};
return (function() {
return __func.apply(__this, arguments);
});
})(this));
};
// We've tagged the opening parenthesis of a method call, and the opening
// bracket of an indexing operation. Match them with their close.
re.prototype.close_open_calls_and_indexes = function close_open_calls_and_indexes() {
var brackets, parens;
parens = [0];
brackets = [0];
return this.scan_tokens((function(__this) {
var __func = function(prev, token, post, i) {
if (token[0] === 'CALL_START') {
parens.push(0);
} else if (token[0] === 'INDEX_START') {
brackets.push(0);
} else if (token[0] === '(') {
parens[parens.length - 1] += 1;
} else if (token[0] === '[') {
brackets[brackets.length - 1] += 1;
} else if (token[0] === ')') {
if (parens[parens.length - 1] === 0) {
parens.pop();
token[0] = 'CALL_END';
} else {
parens[parens.length - 1] -= 1;
}
} else if (token[0] === ']') {
if (brackets[brackets.length - 1] === 0) {
brackets.pop();
token[0] = 'INDEX_END';
} else {
brackets[brackets.length - 1] -= 1;
}
}
return 1;
};
return (function() {
return __func.apply(__this, arguments);
});
})(this));
};
// 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.
re.prototype.add_implicit_parentheses = function add_implicit_parentheses() {
var stack;
stack = [0];
return this.scan_tokens((function(__this) {
var __func = function(prev, token, post, i) {
var __i, __j, __k, __l, idx, last, size, tmp;
if (token[0] === 'INDENT') {
stack.push(0);
}
if (token[0] === 'OUTDENT') {
last = stack.pop();
stack[stack.length - 1] += last;
}
if (stack[stack.length - 1] > 0 && (IMPLICIT_END.indexOf(token[0]) >= 0 || (typeof !post !== "undefined" && !post !== null))) {
idx = token[0] === 'OUTDENT' ? i + 1 : i;
__k = 0; __l = stack[stack.length - 1];
for (__j=0, tmp=__k; (__k <= __l ? tmp < __l : tmp > __l); (__k <= __l ? tmp += 1 : tmp -= 1), __j++) {
this.tokens.splice(idx, 0, ['CALL_END', ')']);
}
size = stack[stack.length - 1] + 1;
stack[stack.length - 1] = 0;
return size;
}
if (!(prev && IMPLICIT_FUNC.indexOf(prev[0]) >= 0 && IMPLICIT_CALL.indexOf(token[0]) >= 0)) {
return 1;
}
this.tokens.splice(i, 0, ['CALL_START', '(']);
stack[stack.length - 1] += 1;
return 2;
};
return (function() {
return __func.apply(__this, arguments);
});
})(this));
};
// Because our grammar is LALR(1), it can't handle some single-line
// expressions that lack ending delimiters. Use the lexer to add the implicit
// blocks, so it doesn't need to.
// ')' can close a single-line block, but we need to make sure it's balanced.
re.prototype.add_implicit_indentation = function add_implicit_indentation() {
return this.scan_tokens((function(__this) {
var __func = function(prev, token, post, i) {
var idx, insertion, parens, starter, tok;
if (!(SINGLE_LINERS.indexOf(token[0]) >= 0 && post[0] !== 'INDENT' && !(token[0] === 'ELSE' && post[0] === 'IF'))) {
return 1;
}
starter = token[0];
this.tokens.splice(i + 1, 0, ['INDENT', 2]);
idx = i + 1;
parens = 0;
while (true) {
idx += 1;
tok = this.tokens[idx];
if ((!tok || SINGLE_CLOSERS.indexOf(tok[0]) >= 0 || (tok[0] === ')' && parens === 0)) && !(starter === 'ELSE' && tok[0] === 'ELSE')) {
insertion = this.tokens[idx - 1][0] === "," ? idx - 1 : idx;
this.tokens.splice(insertion, 0, ['OUTDENT', 2]);
break;
}
if (tok[0] === '(') {
parens += 1;
}
if (tok[0] === ')') {
parens -= 1;
}
}
if (!(token[0] === 'THEN')) {
return 1;
}
this.tokens.splice(i, 1);
return 0;
};
return (function() {
return __func.apply(__this, arguments);
});
})(this));
};
// Ensure that all listed pairs of tokens are correctly balanced throughout
// the course of the token stream.
re.prototype.ensure_balance = function ensure_balance(pairs) {
var __i, __j, key, levels, unclosed, value;
levels = {
};
this.scan_tokens((function(__this) {
var __func = function(prev, token, post, i) {
var __i, __j, __k, close, open;
__i = pairs;
for (__j = 0; __j < __i.length; __j++) {
pair = __i[__j];
__k = pair;
open = __k[0];
close = __k[1];
levels[open] = levels[open] || 0;
if (token[0] === open) {
levels[open] += 1;
}
if (token[0] === close) {
levels[open] -= 1;
}
if (levels[open] < 0) {
throw "too many " + token[1];
}
}
return 1;
};
return (function() {
return __func.apply(__this, arguments);
});
})(this));
unclosed = (function() {
__i = []; __j = levels;
for (key in __j) {
value = __j[key];
if (__hasProp.call(__j, key)) {
if (value > 0) {
__i.push(key);
}
}
}
return __i;
}).call(this);
if (unclosed.length) {
throw "unclosed " + unclosed[0];
}
};
// We'd like to support syntax like this:
// el.click((event) ->
// el.hide())
// In order to accomplish this, move outdents that follow closing parens
// inwards, safely. The steps to accomplish this are:
//
// 1. Check that all paired tokens are balanced and in order.
// 2. Rewrite the stream with a stack: if you see an '(' or INDENT, add it
// to the stack. If you see an ')' or OUTDENT, pop the stack and replace
// it with the inverse of what we've just popped.
// 3. Keep track of "debt" for tokens that we fake, to make sure we end
// up balanced in the end.
re.prototype.rewrite_closing_parens = function rewrite_closing_parens() {
var __i, debt, key, stack, val;
stack = [];
debt = {
};
__i = INVERSES;
for (key in __i) {
val = __i[key];
if (__hasProp.call(__i, key)) {
((debt[key] = 0));
}
}
return this.scan_tokens((function(__this) {
var __func = function(prev, token, post, i) {
var inv, match, mtag, tag;
tag = token[0];
inv = INVERSES[token[0]];
// Push openers onto the stack.
if (EXPRESSION_START.indexOf(tag) >= 0) {
stack.push(token);
return 1;
// The end of an expression, check stack and debt for a pair.
} else if (EXPRESSION_TAIL.indexOf(tag) >= 0) {
// If the tag is already in our debt, swallow it.
if (debt[inv] > 0) {
debt[inv] -= 1;
this.tokens.splice(i, 1);
return 0;
} else {
// Pop the stack of open delimiters.
match = stack.pop();
mtag = match[0];
// Continue onwards if it's the expected tag.
if (tag === INVERSES[mtag]) {
return 1;
} else {
// Unexpected close, insert correct close, adding to the debt.
debt[mtag] += 1;
val = mtag === 'INDENT' ? match[1] : INVERSES[mtag];
this.tokens.splice(i, 0, [INVERSES[mtag], val]);
return 1;
}
}
} else {
return 1;
}
};
return (function() {
return __func.apply(__this, arguments);
});
})(this));
};
})();

View File

@@ -151,6 +151,30 @@ module CoffeeScript
end
end
# 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.
def add_implicit_parentheses
stack = [0]
scan_tokens do |prev, token, post, i|
stack.push(0) if token[0] == :INDENT
if token[0] == :OUTDENT
last = stack.pop
stack[-1] += last
end
if stack.last > 0 && (IMPLICIT_END.include?(token[0]) || post.nil?)
idx = token[0] == :OUTDENT ? i + 1 : i
stack.last.times { @tokens.insert(idx, [:CALL_END, Value.new(')', token[1].line)]) }
size, stack[-1] = stack[-1] + 1, 0
next size
end
next 1 unless IMPLICIT_FUNC.include?(prev[0]) && IMPLICIT_CALL.include?(token[0])
@tokens.insert(i, [:CALL_START, Value.new('(', token[1].line)])
stack[-1] += 1
next 2
end
end
# Because our grammar is LALR(1), it can't handle some single-line
# expressions that lack ending delimiters. Use the lexer to add the implicit
# blocks, so it doesn't need to.
@@ -183,30 +207,6 @@ module CoffeeScript
end
end
# 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.
def add_implicit_parentheses
stack = [0]
scan_tokens do |prev, token, post, i|
stack.push(0) if token[0] == :INDENT
if token[0] == :OUTDENT
last = stack.pop
stack[-1] += last
end
if stack.last > 0 && (IMPLICIT_END.include?(token[0]) || post.nil?)
idx = token[0] == :OUTDENT ? i + 1 : i
stack.last.times { @tokens.insert(idx, [:CALL_END, Value.new(')', token[1].line)]) }
size, stack[-1] = stack[-1] + 1, 0
next size
end
next 1 unless IMPLICIT_FUNC.include?(prev[0]) && IMPLICIT_CALL.include?(token[0])
@tokens.insert(i, [:CALL_START, Value.new('(', token[1].line)])
stack[-1] += 1
next 2
end
end
# Ensure that all listed pairs of tokens are correctly balanced throughout
# the course of the token stream.
def ensure_balance(*pairs)

View File

@@ -0,0 +1,11 @@
(function(){
var coffee, paths;
// Quickie script to compile and run all the files given as arguments.
process.mixin(require('sys'));
coffee = require('./coffee-script');
paths = process.ARGV;
paths = paths.slice(2, paths.length);
paths.length ? coffee.compile_files(paths, function(js) {
return eval(js);
}) : require('./repl');
})();

View File

@@ -1,7 +1,6 @@
{
"name": "coffee-script",
"lib": "lib/coffee_script/narwhal",
"preload": ["narwhal"],
"description": "Unfancy JavaScript",
"keywords": ["javascript", "language"],
"author": "Jeremy Ashkenas",

45
src/coffee-script.coffee Normal file
View File

@@ -0,0 +1,45 @@
# Executes the `coffee` Ruby program to convert from CoffeeScript to JavaScript.
path: require('path')
# The path to the CoffeeScript executable.
compiler: path.normalize(path.dirname(__filename) + '/../../bin/coffee')
# Compile a string over stdin, with global variables, for the REPL.
exports.compile: (code, callback) ->
js: ''
coffee: process.createChildProcess compiler, ['--eval', '--no-wrap', '--globals']
coffee.addListener 'output', (results) ->
js += results if results?
coffee.addListener 'exit', ->
callback(js)
coffee.write(code)
coffee.close()
# Compile a list of CoffeeScript files on disk.
exports.compile_files: (paths, callback) ->
js: ''
coffee: process.createChildProcess compiler, ['--print'].concat(paths)
coffee.addListener 'output', (results) ->
js += results if results?
# NB: we have to add a mutex to make sure it doesn't get called twice.
exit_ran: false
coffee.addListener 'exit', ->
return if exit_ran
exit_ran: true
callback(js)
coffee.addListener 'error', (message) ->
return unless message
puts message
throw new Error "CoffeeScript compile error"

281
src/lexer.coffee Normal file
View File

@@ -0,0 +1,281 @@
sys: require 'sys'
Rewriter: require('./rewriter').Rewriter
# The lexer reads a stream of CoffeeScript and divvys it up into tagged
# tokens. A minor bit of the ambiguity in the grammar has been avoided by
# pushing some extra smarts into the Lexer.
exports.Lexer: lex: ->
# Constants ============================================================
# The list of keywords passed verbatim to the parser.
KEYWORDS: [
"if", "else", "then", "unless",
"true", "false", "yes", "no", "on", "off",
"and", "or", "is", "isnt", "not",
"new", "return", "arguments",
"try", "catch", "finally", "throw",
"break", "continue",
"for", "in", "of", "by", "where", "while",
"delete", "instanceof", "typeof",
"switch", "when",
"super", "extends"
]
# Token matching regexes.
IDENTIFIER : /^([a-zA-Z$_](\w|\$)*)/
NUMBER : /^(\b((0(x|X)[0-9a-fA-F]+)|([0-9]+(\.[0-9]+)?(e[+\-]?[0-9]+)?)))\b/i
STRING : /^(""|''|"([\s\S]*?)([^\\]|\\\\)"|'([\s\S]*?)([^\\]|\\\\)')/
HEREDOC : /^("{6}|'{6}|"{3}\n?([\s\S]*?)\n?([ \t]*)"{3}|'{3}\n?([\s\S]*?)\n?([ \t]*)'{3})/
JS : /^(``|`([\s\S]*?)([^\\]|\\\\)`)/
OPERATOR : /^([+\*&|\/\-%=<>:!?]+)/
WHITESPACE : /^([ \t]+)/
COMMENT : /^(((\n?[ \t]*)?#.*$)+)/
CODE : /^((-|=)>)/
REGEX : /^(\/(.*?)([^\\]|\\\\)\/[imgy]{0,4})/
MULTI_DENT : /^((\n([ \t]*))+)(\.)?/
LAST_DENTS : /\n([ \t]*)/g
LAST_DENT : /\n([ \t]*)/
ASSIGNMENT : /^(:|=)$/
# Token cleaning regexes.
JS_CLEANER : /(^`|`$)/g
MULTILINER : /\n/g
STRING_NEWLINES : /\n[ \t]*/g
COMMENT_CLEANER : /(^[ \t]*#|\n[ \t]*$)/mg
NO_NEWLINE : /^([+\*&|\/\-%=<>:!.\\][<>=&|]*|and|or|is|isnt|not|delete|typeof|instanceof)$/
HEREDOC_INDENT : /^[ \t]+/g
# Tokens which a regular expression will never immediately follow, but which
# a division operator might.
# See: http://www.mozilla.org/js/language/js20-2002-04/rationale/syntax.html#regular-expressions
NOT_REGEX: [
'IDENTIFIER', 'NUMBER', 'REGEX', 'STRING',
')', '++', '--', ']', '}',
'FALSE', 'NULL', 'TRUE'
]
# Tokens which could legitimately be invoked or indexed.
CALLABLE: ['IDENTIFIER', 'SUPER', ')', ']', '}', 'STRING']
# Scan by attempting to match tokens one character at a time. Slow and steady.
lex::tokenize: (code) ->
this.code : code # Cleanup code by remove extra line breaks, TODO: chomp
this.i : 0 # Current character position we're parsing
this.line : 1 # The current line.
this.indent : 0 # The current indent level.
this.indents : [] # The stack of all indent levels we are currently within.
this.tokens : [] # Collection of all parsed tokens in the form [:TOKEN_TYPE, value]
this.spaced : null # The last token that has a space following it.
while this.i < this.code.length
this.chunk: this.code.slice(this.i)
this.extract_next_token()
# sys.puts "original stream: " + this.tokens if process.ENV['VERBOSE']
this.close_indentation()
(new Rewriter()).rewrite this.tokens
# At every position, run through this list of attempted matches,
# short-circuiting if any of them succeed.
lex::extract_next_token: ->
return if this.identifier_token()
return if this.number_token()
return if this.heredoc_token()
return if this.string_token()
return if this.js_token()
return if this.regex_token()
return if this.indent_token()
return if this.comment_token()
return if this.whitespace_token()
return this.literal_token()
# Tokenizers ==========================================================
# Matches identifying literals: variables, keywords, method names, etc.
lex::identifier_token: ->
return false unless id: this.match IDENTIFIER, 1
# Keywords are special identifiers tagged with their own name,
# 'if' will result in an ['IF', "if"] token.
tag: if KEYWORDS.indexOf(id) >= 0 then id.toUpperCase() else 'IDENTIFIER'
tag: 'LEADING_WHEN' if tag is 'WHEN' and (this.tag() is 'OUTDENT' or this.tag() is 'INDENT')
this.tag(-1, 'PROTOTYPE_ACCESS') if tag is 'IDENTIFIER' and this.value() is '::'
if tag is 'IDENTIFIER' and this.value() is '.' and !(this.value(-2) is '.')
if this.tag(-2) is '?'
this.tag(-1, 'SOAK_ACCESS')
this.tokens.splice(-2, 1)
else
this.tag(-1, 'PROPERTY_ACCESS')
this.token(tag, id)
this.i += id.length
true
# Matches numbers, including decimals, hex, and exponential notation.
lex::number_token: ->
return false unless number: this.match NUMBER, 1
this.token 'NUMBER', number
this.i += number.length
true
# Matches strings, including multi-line strings.
lex::string_token: ->
return false unless string: this.match STRING, 1
escaped: string.replace STRING_NEWLINES, " \\\n"
this.token 'STRING', escaped
this.line += this.count string, "\n"
this.i += string.length
true
# Matches heredocs, adjusting indentation to the correct level.
lex::heredoc_token: ->
return false unless match = this.chunk.match(HEREDOC)
doc: match[2] or match[4]
indent: doc.match(HEREDOC_INDENT).sort()[0]
doc: doc.replace(new RegExp("^" + indent, 'g'), '')
.replace(MULTILINER, "\\n")
.replace('"', '\\"')
this.token 'STRING', '"' + doc + '"'
this.line += this.count match[1], "\n"
this.i += match[1].length
true
# Matches interpolated JavaScript.
lex::js_token: ->
return false unless script: this.match JS, 1
this.token 'JS', script.replace(JS_CLEANER, '')
this.i += script.length
true
# Matches regular expression literals.
lex::regex_token: ->
return false unless regex: this.match REGEX, 1
return false if NOT_REGEX.indexOf(this.tag()) >= 0
this.token 'REGEX', regex
this.i += regex.length
true
# Matches and conumes comments.
lex::comment_token: ->
return false unless comment: this.match COMMENT, 1
this.line += comment.match(MULTILINER).length
this.token 'COMMENT', comment.replace(COMMENT_CLEANER, '').split(MULTILINER)
this.token "\n", "\n"
this.i += comment.length
true
# Record tokens for indentation differing from the previous line.
lex::indent_token: ->
return false unless indent: this.match MULTI_DENT, 1
this.line += indent.match(MULTILINER).length
this.i += indent.length
next_character: this.chunk.match(MULTI_DENT)[4]
no_newlines: next_character is '.' or (this.value().match(NO_NEWLINE) and this.tokens[this.tokens.length - 2][0] isnt '.' and not this.value().match(CODE))
return this.suppress_newlines(indent) if no_newlines
size: indent.match(LAST_DENTS).reverse()[0].match(LAST_DENT)[1].length
return this.newline_token(indent) if size is this.indent
if size > this.indent
diff: size - this.indent
this.token 'INDENT', diff
this.indents.push diff
else
this.outdent_token this.indent - size
this.indent: size
true
# Record an oudent token or tokens, if we're moving back inwards past
# multiple recorded indents.
lex::outdent_token: (move_out) ->
while move_out > 0 and this.indents.length
last_indent: this.indents.pop()
this.token 'OUTDENT', last_indent
move_out -= last_indent
this.token "\n", "\n"
true
# Matches and consumes non-meaningful whitespace.
lex::whitespace_token: ->
return false unless space: this.match WHITESPACE, 1
this.spaced: this.value()
this.i += space.length
true
# Multiple newlines get merged together.
# Use a trailing \ to escape newlines.
lex::newline_token: (newlines) ->
this.token "\n", "\n" unless this.value() is "\n"
true
# Tokens to explicitly escape newlines are removed once their job is done.
lex::suppress_newlines: (newlines) ->
this.tokens.pop() if this.value() is "\\"
true
# We treat all other single characters as a token. Eg.: ( ) , . !
# Multi-character operators are also literal tokens, so that Racc can assign
# the proper order of operations.
lex::literal_token: ->
match: this.chunk.match(OPERATOR)
value: match and match[1]
this.tag_parameters() if value and value.match(CODE)
value ||= this.chunk.substr(0, 1)
tag: if value.match(ASSIGNMENT) then 'ASSIGN' else value
if this.value() isnt this.spaced and CALLABLE.indexOf(this.tag()) >= 0
tag: 'CALL_START' if value is '('
tag: 'INDEX_START' if value is '['
this.token tag, value
this.i += value.length
true
# Helpers =============================================================
# Add a token to the results, taking note of the line number.
lex::token: (tag, value) ->
this.tokens.push([tag, value])
# this.tokens.push([tag, Value.new(value, @line)])
# Look at a tag in the current token stream.
lex::tag: (index, tag) ->
return unless tok: this.tokens[this.tokens.length - (index || 1)]
return tok[0]: tag if tag?
tok[0]
# Look at a value in the current token stream.
lex::value: (index, val) ->
return unless tok: this.tokens[this.tokens.length - (index || 1)]
return tok[1]: val if val?
tok[1]
# Count the occurences of a character in a string.
lex::count: (string, char) ->
num: 0
pos: string.indexOf(char)
while pos isnt -1
count += 1
pos: string.indexOf(char, pos + 1)
count
# Attempt to match a string against the current chunk, returning the indexed
# match.
lex::match: (regex, index) ->
return false unless m: this.chunk.match(regex)
if m then m[index] else false
# A source of ambiguity in our grammar was parameter lists in function
# definitions (as opposed to argument lists in function calls). Tag
# parameter identifiers in order to avoid this. Also, parameter lists can
# make use of splats.
lex::tag_parameters: ->
return if this.tag() isnt ')'
i: 0
while true
i += 1
tok: this.tokens[this.tokens.length - i]
return if not tok
switch tok[0]
when 'IDENTIFIER' then tok[0]: 'PARAM'
when ')' then tok[0]: 'PARAM_END'
when '(' then return tok[0]: 'PARAM_START'
true
# Close up all remaining open blocks. IF the first token is an indent,
# axe it.
lex::close_indentation: ->
this.outdent_token(this.indent)

View File

@@ -14,6 +14,9 @@ checkForErrors: (coffeeProcess) ->
system.stderr.print coffeeProcess.stderr.read()
throw new Error "CoffeeScript compile error"
# Alias print to "puts", for Node.js compatibility:
puts: print
# Run a simple REPL, round-tripping to the CoffeeScript compiler for every
# command.
exports.run: (args) ->

30
src/nodes.coffee Normal file
View File

@@ -0,0 +1,30 @@
exports.Node: -> this.values: arguments
exports.Expressions : exports.Node
exports.LiteralNode : exports.Node
exports.ReturnNode : exports.Node
exports.CommentNode : exports.Node
exports.CallNode : exports.Node
exports.ExtendsNode : exports.Node
exports.ValueNode : exports.Node
exports.AccessorNode : exports.Node
exports.IndexNode : exports.Node
exports.RangeNode : exports.Node
exports.SliceNode : exports.Node
exports.AssignNode : exports.Node
exports.OpNode : exports.Node
exports.CodeNode : exports.Node
exports.SplatNode : exports.Node
exports.ObjectNode : exports.Node
exports.ArrayNode : exports.Node
exports.PushNode : exports.Node
exports.ClosureNode : exports.Node
exports.WhileNode : exports.Node
exports.ForNode : exports.Node
exports.TryNode : exports.Node
exports.ThrowNode : exports.Node
exports.ExistenceNode : exports.Node
exports.ParentheticalNode : exports.Node
exports.IfNode : exports.Node

528
src/parser.coffee Normal file
View File

@@ -0,0 +1,528 @@
Parser: require('jison').Parser
# DSL ===================================================================
# Detect functions: [
unwrap: /function\s*\(\)\s*\{\s*return\s*([\s\S]*);\s*\}/
# Quickie DSL for Jison access.
o: (pattern_string, func) ->
if func
func: if match: (func + "").match(unwrap) then match[1] else '(' + func + '())'
[pattern_string, '$$ = ' + func + ';']
else
[pattern_string, '$$ = $1;']
# Precedence ===========================================================
operators: [
["left", '?']
["right", 'NOT', '!', '!!', '~', '++', '--']
["left", '*', '/', '%']
["left", '+', '-']
["left", '<<', '>>', '>>>']
["left", '&', '|', '^']
["left", '<=', '<', '>', '>=']
["right", '==', '!=', 'IS', 'ISNT']
["left", '&&', '||', 'AND', 'OR']
["right", '-=', '+=', '/=', '*=', '%=']
["right", 'DELETE', 'INSTANCEOF', 'TYPEOF']
["left", '.']
["right", 'INDENT']
["left", 'OUTDENT']
["right", 'WHEN', 'LEADING_WHEN', 'IN', 'OF', 'BY']
["right", 'THROW', 'FOR', 'NEW', 'SUPER']
["left", 'EXTENDS']
["left", '||=', '&&=', '?=']
["right", 'ASSIGN', 'RETURN']
["right", '->', '=>', 'UNLESS', 'IF', 'ELSE', 'WHILE']
]
# Grammar ==============================================================
grammar: {
# All parsing will end in this rule, being the trunk of the AST.
Root: [
o "", -> new Expressions()
o "Terminator", -> new Expressions()
o "Expressions"
o "Block Terminator"
]
# Any list of expressions or method body, seperated by line breaks or semis.
Expressions: [
o "Expression", -> Expressions.wrap([$1])
o "Expressions Terminator Expression", -> $1.push($3)
o "Expressions Terminator"
]
# All types of expressions in our language. The basic unit of CoffeeScript
# is the expression.
Expression: [
o "Value"
o "Call"
o "Code"
o "Operation"
o "Assign"
o "If"
o "Try"
o "Throw"
o "Return"
o "While"
o "For"
o "Switch"
o "Extends"
o "Splat"
o "Existence"
o "Comment"
]
# A block of expressions. Note that the Rewriter will convert some postfix
# forms into blocks for us, by altering the token stream.
Block: [
o "INDENT Expressions OUTDENT", -> $2
o "INDENT OUTDENT", -> new Expressions()
]
# Tokens that can terminate an expression.
Terminator: [
o "\n"
o ";"
]
# All hard-coded values. These can be printed straight to JavaScript.
Literal: [
o "NUMBER", -> new LiteralNode($1)
o "STRING", -> new LiteralNode($1)
o "JS", -> new LiteralNode($1)
o "REGEX", -> new LiteralNode($1)
o "BREAK", -> new LiteralNode($1)
o "CONTINUE", -> new LiteralNode($1)
o "ARGUMENTS", -> new LiteralNode($1)
o "TRUE", -> new LiteralNode(true)
o "FALSE", -> new LiteralNode(false)
o "YES", -> new LiteralNode(true)
o "NO", -> new LiteralNode(false)
o "ON", -> new LiteralNode(true)
o "OFF", -> new LiteralNode(false)
]
# Assignment to a variable (or index).
Assign: [
o "Value ASSIGN Expression", -> new AssignNode($1, $3)
]
# Assignment within an object literal (can be quoted).
AssignObj: [
o "IDENTIFIER ASSIGN Expression", -> new AssignNode(new ValueNode($1), $3, 'object')
o "STRING ASSIGN Expression", -> new AssignNode(new ValueNode(new LiteralNode($1)), $3, 'object')
o "Comment"
]
# A return statement.
Return: [
o "RETURN Expression", -> new ReturnNode($2)
o "RETURN", -> new ReturnNode(new ValueNode(new LiteralNode('null')))
]
# A comment.
Comment: [
o "COMMENT", -> new CommentNode($1)
]
# Arithmetic and logical operators
# For Ruby's Operator precedence, see: [
# https://www.cs.auckland.ac.nz/references/ruby/ProgrammingRuby/language.html
Operation: [
o "! Expression", -> new OpNode($1, $2)
o "!! Expression", -> new OpNode($1, $2)
o "- Expression", -> new OpNode($1, $2)
o "+ Expression", -> new OpNode($1, $2)
o "NOT Expression", -> new OpNode($1, $2)
o "~ Expression", -> new OpNode($1, $2)
o "-- Expression", -> new OpNode($1, $2)
o "++ Expression", -> new OpNode($1, $2)
o "DELETE Expression", -> new OpNode($1, $2)
o "TYPEOF Expression", -> new OpNode($1, $2)
o "Expression --", -> new OpNode($2, $1, null, true)
o "Expression ++", -> new OpNode($2, $1, null, true)
o "Expression * Expression", -> new OpNode($2, $1, $3)
o "Expression / Expression", -> new OpNode($2, $1, $3)
o "Expression % Expression", -> new OpNode($2, $1, $3)
o "Expression + Expression", -> new OpNode($2, $1, $3)
o "Expression - Expression", -> new OpNode($2, $1, $3)
o "Expression << Expression", -> new OpNode($2, $1, $3)
o "Expression >> Expression", -> new OpNode($2, $1, $3)
o "Expression >>> Expression", -> new OpNode($2, $1, $3)
o "Expression & Expression", -> new OpNode($2, $1, $3)
o "Expression | Expression", -> new OpNode($2, $1, $3)
o "Expression ^ Expression", -> new OpNode($2, $1, $3)
o "Expression <= Expression", -> new OpNode($2, $1, $3)
o "Expression < Expression", -> new OpNode($2, $1, $3)
o "Expression > Expression", -> new OpNode($2, $1, $3)
o "Expression >= Expression", -> new OpNode($2, $1, $3)
o "Expression == Expression", -> new OpNode($2, $1, $3)
o "Expression != Expression", -> new OpNode($2, $1, $3)
o "Expression IS Expression", -> new OpNode($2, $1, $3)
o "Expression ISNT Expression", -> new OpNode($2, $1, $3)
o "Expression && Expression", -> new OpNode($2, $1, $3)
o "Expression || Expression", -> new OpNode($2, $1, $3)
o "Expression AND Expression", -> new OpNode($2, $1, $3)
o "Expression OR Expression", -> new OpNode($2, $1, $3)
o "Expression ? Expression", -> new OpNode($2, $1, $3)
o "Expression -= Expression", -> new OpNode($2, $1, $3)
o "Expression += Expression", -> new OpNode($2, $1, $3)
o "Expression /= Expression", -> new OpNode($2, $1, $3)
o "Expression *= Expression", -> new OpNode($2, $1, $3)
o "Expression %= Expression", -> new OpNode($2, $1, $3)
o "Expression ||= Expression", -> new OpNode($2, $1, $3)
o "Expression &&= Expression", -> new OpNode($2, $1, $3)
o "Expression ?= Expression", -> new OpNode($2, $1, $3)
o "Expression INSTANCEOF Expression", -> new OpNode($2, $1, $3)
o "Expression IN Expression", -> new OpNode($2, $1, $3)
]
# Try abbreviated expressions to make the grammar build faster:
# UnaryOp: [
# o "!"
# o "!!"
# o "NOT"
# o "~"
# o "--"
# o "++"
# o "DELETE"
# o "TYPEOF"
# ]
#
# BinaryOp: [
# o "*"
# o "/"
# o "%"
# o "+"
# o "-"
# o "<<"
# o ">>"
# o ">>>"
# o "&"
# o "|"
# o "^"
# o "<="
# o "<"
# o ">"
# o ">="
# o "=="
# o "!="
# o "IS"
# o "ISNT"
# o "&&"
# o "||"
# o "AND"
# o "OR"
# o "?"
# o "-="
# o "+="
# o "/="
# o "*="
# o "%="
# o "||="
# o "&&="
# o "?="
# o "INSTANCEOF"
# o "IN"
# ]
#
# Operation: [
# o "Expression BinaryOp Expression", -> new OpNode($2, $1, $3)
# o "UnaryOp Expression", -> new OpNode($1, $2)
# ]
# The existence operator.
Existence: [
o "Expression ?", -> new ExistenceNode($1)
]
# Function definition.
Code: [
o "PARAM_START ParamList PARAM_END FuncGlyph Block", -> new CodeNode($2, $5, $4)
o "FuncGlyph Block", -> new CodeNode([], $2, $1)
]
# The symbols to signify functions, and bound functions.
FuncGlyph: [
o "->", -> 'func'
o "=>", -> 'boundfunc'
]
# The parameters to a function definition.
ParamList: [
o "Param", -> [$1]
o "ParamList , Param", -> $1.push($3)
]
# A Parameter (or ParamSplat) in a function definition.
Param: [
o "PARAM"
o "PARAM . . .", -> new SplatNode($1)
]
# A regular splat.
Splat: [
o "Expression . . .", -> new SplatNode($1)
]
# Expressions that can be treated as values.
Value: [
o "IDENTIFIER", -> new ValueNode($1)
o "Literal", -> new ValueNode($1)
o "Array", -> new ValueNode($1)
o "Object", -> new ValueNode($1)
o "Parenthetical", -> new ValueNode($1)
o "Range", -> new ValueNode($1)
o "Value Accessor", -> $1.push($2)
o "Invocation Accessor", -> new ValueNode($1, [$2])
]
# Accessing into an object or array, through dot or index notation.
Accessor: [
o "PROPERTY_ACCESS IDENTIFIER", -> new AccessorNode($2)
o "PROTOTYPE_ACCESS IDENTIFIER", -> new AccessorNode($2, 'prototype')
o "SOAK_ACCESS IDENTIFIER", -> new AccessorNode($2, 'soak')
o "Index"
o "Slice", -> new SliceNode($1)
]
# Indexing into an object or array.
Index: [
o "INDEX_START Expression INDEX_END", -> new IndexNode($2)
]
# An object literal.
Object: [
o "{ AssignList }", -> new ObjectNode($2)
]
# Assignment within an object literal (comma or newline separated).
AssignList: [
o "", -> []
o "AssignObj", -> [$1]
o "AssignList , AssignObj", -> $1.push $3
o "AssignList Terminator AssignObj", -> $1.push $3
o "AssignList , Terminator AssignObj", -> $1.push $4
o "INDENT AssignList OUTDENT", -> $2
]
# All flavors of function call (instantiation, super, and regular).
Call: [
o "Invocation", -> $1
o "NEW Invocation", -> $2.new_instance()
o "Super", -> $1
]
# Extending an object's prototype.
Extends: [
o "Value EXTENDS Value", -> new ExtendsNode($1, $3)
]
# A generic function invocation.
Invocation: [
o "Value Arguments", -> new CallNode($1, $2)
o "Invocation Arguments", -> new CallNode($1, $2)
]
# The list of arguments to a function invocation.
Arguments: [
o "CALL_START ArgList CALL_END", -> $2
]
# Calling super.
Super: [
o "SUPER CALL_START ArgList CALL_END", -> new CallNode('super', $3)
]
# The range literal.
Range: [
o "[ Expression . . Expression ]", -> new RangeNode($2, $5)
o "[ Expression . . . Expression ]", -> new RangeNode($2, $6, true)
]
# The slice literal.
Slice: [
o "INDEX_START Expression . . Expression INDEX_END", -> new RangeNode($2, $5)
o "INDEX_START Expression . . . Expression INDEX_END", -> new RangeNode($2, $6, true)
]
# The array literal.
Array: [
o "[ ArgList ]", -> new ArrayNode($2)
]
# A list of arguments to a method call, or as the contents of an array.
ArgList: [
o "", -> []
o "Expression", -> val
o "INDENT Expression", -> [$2]
o "ArgList , Expression", -> $1.push $3
o "ArgList Terminator Expression", -> $1.push $3
o "ArgList , Terminator Expression", -> $1.push $4
o "ArgList , INDENT Expression", -> $1.push $4
o "ArgList OUTDENT", -> $1
]
# Just simple, comma-separated, required arguments (no fancy syntax).
SimpleArgs: [
o "Expression", -> $1
o "SimpleArgs , Expression", ->
([$1].push($3)).reduce (a, b) -> a.concat(b)
]
# Try/catch/finally exception handling blocks.
Try: [
o "TRY Block Catch", -> new TryNode($2, $3[0], $3[1])
o "TRY Block FINALLY Block", -> new TryNode($2, nil, nil, $4)
o "TRY Block Catch FINALLY Block", -> new TryNode($2, $3[0], $3[1], $5)
]
# A catch clause.
Catch: [
o "CATCH IDENTIFIER Block", -> [$2, $3]
]
# Throw an exception.
Throw: [
o "THROW Expression", -> new ThrowNode($2)
]
# Parenthetical expressions.
Parenthetical: [
o "( Expression )", -> new ParentheticalNode($2)
]
# The while loop. (there is no do..while).
While: [
o "WHILE Expression Block", -> new WhileNode($2, $3)
o "WHILE Expression", -> new WhileNode($2, nil)
o "Expression WHILE Expression", -> new WhileNode($3, Expressions.wrap($1))
]
# Array comprehensions, including guard and current index.
# Looks a little confusing, check nodes.rb for the arguments to ForNode.
For: [
o "Expression FOR ForVariables ForSource", -> new ForNode($1, $4, $3[0], $3[1])
o "FOR ForVariables ForSource Block", -> new ForNode($4, $3, $2[0], $2[1])
]
# An array comprehension has variables for the current element and index.
ForVariables: [
o "IDENTIFIER", -> [$1]
o "IDENTIFIER , IDENTIFIER", -> [$1, $3]
]
# The source of the array comprehension can optionally be filtered.
ForSource: [
o "IN Expression", -> {source: $2}
o "OF Expression", -> {source: $2, object: true}
o "ForSource WHEN Expression", -> $1.filter: $3; $1
o "ForSource BY Expression", -> $1.step: $3; $1
]
# Switch/When blocks.
Switch: [
o "SWITCH Expression INDENT Whens OUTDENT", -> $4.rewrite_condition($2)
o "SWITCH Expression INDENT Whens ELSE Block OUTDENT", -> $4.rewrite_condition($2).add_else($6)
]
# The inner list of whens.
Whens: [
o "When", -> $1
o "Whens When", -> $1.push $2
]
# An individual when.
When: [
o "LEADING_WHEN SimpleArgs Block", -> new IfNode($2, $3, nil, {statement: true})
o "LEADING_WHEN SimpleArgs Block Terminator", -> new IfNode($2, $3, nil, {statement: true})
o "Comment Terminator When", -> $3.add_comment($1)
]
# The most basic form of "if".
IfBlock: [
o "IF Expression Block", -> new IfNode($2, $3)
]
# An elsif portion of an if-else block.
ElsIf: [
o "ELSE IfBlock", -> $2.force_statement()
]
# Multiple elsifs can be chained together.
ElsIfs: [
o "ElsIf", -> $1
o "ElsIfs ElsIf", -> $1.add_else($2)
]
# Terminating else bodies are strictly optional.
ElseBody: [
o "", -> null
o "ELSE Block", -> $2
]
# All the alternatives for ending an if-else block.
IfEnd: [
o "ElseBody", -> $1
o "ElsIfs ElseBody", -> $1.add_else($2)
]
# The full complement of if blocks, including postfix one-liner ifs and unlesses.
If: [
o "IfBlock IfEnd", -> $1.add_else($2)
o "Expression IF Expression", -> new IfNode($3, Expressions.wrap($1), nil, {statement: true})
o "Expression UNLESS Expression", -> new IfNode($3, Expressions.wrap($1), nil, {statement: true, invert: true})
]
}
# Helpers ==============================================================
# Make the Jison parser.
bnf: {}
tokens: []
for name, non_terminal of grammar
bnf[name]: for option in non_terminal
for part in option[0].split(" ")
if !grammar[part]
tokens.push(part)
if name == "Root"
option[1] = "return " + option[1]
option
tokens: tokens.join(" ")
parser: new Parser({tokens: tokens, bnf: bnf, operators: operators}, {debug: false})
# Thin wrapper around the real lexer
parser.lexer: {
lex: ->
token: this.tokens[this.pos] or [""]
this.pos += 1
# this.yylineno: token and token[1] and token[1][1]
this.yytext: token[1]
token[0]
setInput: (tokens) ->
this.tokens = tokens
this.pos = 0
upcomingInput: -> ""
showPosition: -> this.pos
}
exports.Parser: ->
exports.Parser::parse: (tokens) -> parser.parse(tokens)

26
src/repl.coffee Normal file
View File

@@ -0,0 +1,26 @@
# A CoffeeScript port/version of the Node.js REPL.
# Required modules.
coffee: require './coffee-script'
process.mixin require 'sys'
# Shortcut variables.
prompt: 'coffee> '
quit: -> process.stdio.close()
# The main REPL function. Called everytime a line of code is entered.
readline: (code) -> coffee.compile code, run
# Attempt to evaluate the command. If there's an exception, print it.
run: (js) ->
try
val: eval(js)
p val if val isnt undefined
catch err
puts err.stack or err.toString()
print prompt
# Start up the REPL.
process.stdio.open()
process.stdio.addListener 'data', readline
print prompt

244
src/rewriter.coffee Normal file
View File

@@ -0,0 +1,244 @@
# In order to keep the grammar simple, the stream of tokens that the Lexer
# emits is rewritten by the Rewriter, smoothing out ambiguities, mis-nested
# indentation, and single-line flavors of expressions.
exports.Rewriter: re: ->
# Tokens that must be balanced.
BALANCED_PAIRS: [['(', ')'], ['[', ']'], ['{', '}'], ['INDENT', 'OUTDENT'],
['PARAM_START', 'PARAM_END'], ['CALL_START', 'CALL_END'], ['INDEX_START', 'INDEX_END']]
# Tokens that signal the start of a balanced pair.
EXPRESSION_START: pair[0] for pair in BALANCED_PAIRS
# Tokens that signal the end of a balanced pair.
EXPRESSION_TAIL: pair[1] for pair in BALANCED_PAIRS
# Tokens that indicate the close of a clause of an expression.
EXPRESSION_CLOSE: ['CATCH', 'WHEN', 'ELSE', 'FINALLY'].concat(EXPRESSION_TAIL)
# Tokens pairs that, in immediate succession, indicate an implicit call.
IMPLICIT_FUNC: ['IDENTIFIER', 'SUPER', ')', 'CALL_END', ']', 'INDEX_END']
IMPLICIT_END: ['IF', 'UNLESS', 'FOR', 'WHILE', "\n", 'OUTDENT']
IMPLICIT_CALL: ['IDENTIFIER', 'NUMBER', 'STRING', 'JS', 'REGEX', 'NEW', 'PARAM_START',
'TRY', 'DELETE', 'TYPEOF', 'SWITCH', 'ARGUMENTS',
'TRUE', 'FALSE', 'YES', 'NO', 'ON', 'OFF', '!', '!!', 'NOT',
'->', '=>', '[', '(', '{']
# The inverse mappings of token pairs we're trying to fix up.
INVERSES: {}
for pair in BALANCED_PAIRS
INVERSES[pair[0]]: pair[1]
INVERSES[pair[1]]: pair[0]
# Single-line flavors of block expressions that have unclosed endings.
# The grammar can't disambiguate them, so we insert the implicit indentation.
SINGLE_LINERS: ['ELSE', "->", "=>", 'TRY', 'FINALLY', 'THEN']
SINGLE_CLOSERS: ["\n", 'CATCH', 'FINALLY', 'ELSE', 'OUTDENT', 'LEADING_WHEN', 'PARAM_START']
# Rewrite the token stream in multiple passes, one logical filter at
# a time. This could certainly be changed into a single pass through the
# stream, with a big ol' efficient switch, but it's much nicer like this.
re::rewrite: (tokens) ->
this.tokens: tokens
this.adjust_comments()
this.remove_leading_newlines()
this.remove_mid_expression_newlines()
this.move_commas_outside_outdents()
this.close_open_calls_and_indexes()
this.add_implicit_parentheses()
this.add_implicit_indentation()
this.ensure_balance(BALANCED_PAIRS)
this.rewrite_closing_parens()
this.tokens
# Rewrite the token stream, looking one token ahead and behind.
# Allow the return value of the block to tell us how many tokens to move
# forwards (or backwards) in the stream, to make sure we don't miss anything
# as the stream changes length under our feet.
re::scan_tokens: (yield) ->
i: 0
while true
break unless this.tokens[i]
move: yield(this.tokens[i - 1], this.tokens[i], this.tokens[i + 1], i)
i += move
true
# Massage newlines and indentations so that comments don't have to be
# correctly indented, or appear on their own line.
re::adjust_comments: ->
this.scan_tokens (prev, token, post, i) =>
return 1 unless token[0] is 'COMMENT'
before: this.tokens[i - 2]
after: this.tokens[i + 2]
if before and after and
((before[0] is 'INDENT' and after[0] is 'OUTDENT') or
(before[0] is 'OUTDENT' and after[0] is 'INDENT')) and
before[1] is after[1]
this.tokens.splice(i + 2, 1)
this.tokens.splice(i - 2, 1)
return 0
else if prev[0] is "\n" and after[0] is 'INDENT'
this.tokens.splice(i + 2, 1)
this.tokens[i - 1]: after
return 1
else if prev[0] isnt "\n" and prev[0] isnt 'INDENT' and prev[0] isnt 'OUTDENT'
this.tokens.splice(i, 0, ["\n", "\n"])
return 2
else
return 1
# Leading newlines would introduce an ambiguity in the grammar, so we
# dispatch them here.
re::remove_leading_newlines: ->
this.tokens.shift() if this.tokens[0][0] is "\n"
# Some blocks occur in the middle of expressions -- when we're expecting
# this, remove their trailing newlines.
re::remove_mid_expression_newlines: ->
this.scan_tokens (prev, token, post, i) =>
return 1 unless post and EXPRESSION_CLOSE.indexOf(post[0]) >= 0 and token[0] is "\n"
this.tokens.splice(i, 1)
return 0
# Make sure that we don't accidentally break trailing commas, which need
# to go on the outside of expression closers.
re::move_commas_outside_outdents: ->
this.scan_tokens (prev, token, post, i) =>
this.tokens.splice(i, 1, token) if token[0] is 'OUTDENT' and prev[0] is ','
return 1
# We've tagged the opening parenthesis of a method call, and the opening
# bracket of an indexing operation. Match them with their close.
re::close_open_calls_and_indexes: ->
parens: [0]
brackets: [0]
this.scan_tokens (prev, token, post, i) =>
switch token[0]
when 'CALL_START' then parens.push(0)
when 'INDEX_START' then brackets.push(0)
when '(' then parens[parens.length - 1] += 1
when '[' then brackets[brackets.length - 1] += 1
when ')'
if parens[parens.length - 1] is 0
parens.pop()
token[0]: 'CALL_END'
else
parens[parens.length - 1] -= 1
when ']'
if brackets[brackets.length - 1] == 0
brackets.pop()
token[0]: 'INDEX_END'
else
brackets[brackets.length - 1] -= 1
return 1
# 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.
re::add_implicit_parentheses: ->
stack: [0]
this.scan_tokens (prev, token, post, i) =>
stack.push(0) if token[0] is 'INDENT'
if token[0] is 'OUTDENT'
last: stack.pop()
stack[stack.length - 1] += last
if stack[stack.length - 1] > 0 and (IMPLICIT_END.indexOf(token[0]) >= 0 or !post?)
idx: if token[0] is 'OUTDENT' then i + 1 else i
for tmp in [0...stack[stack.length - 1]]
this.tokens.splice(idx, 0, ['CALL_END', ')'])
size: stack[stack.length - 1] + 1
stack[stack.length - 1]: 0
return size
return 1 unless prev and IMPLICIT_FUNC.indexOf(prev[0]) >= 0 and IMPLICIT_CALL.indexOf(token[0]) >= 0
this.tokens.splice(i, 0, ['CALL_START', '('])
stack[stack.length - 1] += 1
return 2
# Because our grammar is LALR(1), it can't handle some single-line
# expressions that lack ending delimiters. Use the lexer to add the implicit
# blocks, so it doesn't need to.
# ')' can close a single-line block, but we need to make sure it's balanced.
re::add_implicit_indentation: ->
this.scan_tokens (prev, token, post, i) =>
return 1 unless SINGLE_LINERS.indexOf(token[0]) >= 0 and post[0] isnt 'INDENT' and
not (token[0] is 'ELSE' and post[0] is 'IF')
starter: token[0]
this.tokens.splice(i + 1, 0, ['INDENT', 2])
idx: i + 1
parens: 0
while true
idx += 1
tok: this.tokens[idx]
if (not tok or SINGLE_CLOSERS.indexOf(tok[0]) >= 0 or
(tok[0] is ')' && parens is 0)) and
not (starter is 'ELSE' and tok[0] is 'ELSE')
insertion: if this.tokens[idx - 1][0] is "," then idx - 1 else idx
this.tokens.splice(insertion, 0, ['OUTDENT', 2])
break
parens += 1 if tok[0] is '('
parens -= 1 if tok[0] is ')'
return 1 unless token[0] is 'THEN'
this.tokens.splice(i, 1)
return 0
# Ensure that all listed pairs of tokens are correctly balanced throughout
# the course of the token stream.
re::ensure_balance: (pairs) ->
levels: {}
this.scan_tokens (prev, token, post, i) =>
for pair in pairs
[open, close]: pair
levels[open] ||= 0
levels[open] += 1 if token[0] is open
levels[open] -= 1 if token[0] is close
throw "too many " + token[1] if levels[open] < 0
return 1
unclosed: key for key, value of levels when value > 0
throw "unclosed " + unclosed[0] if unclosed.length
# We'd like to support syntax like this:
# el.click((event) ->
# el.hide())
# In order to accomplish this, move outdents that follow closing parens
# inwards, safely. The steps to accomplish this are:
#
# 1. Check that all paired tokens are balanced and in order.
# 2. Rewrite the stream with a stack: if you see an '(' or INDENT, add it
# to the stack. If you see an ')' or OUTDENT, pop the stack and replace
# it with the inverse of what we've just popped.
# 3. Keep track of "debt" for tokens that we fake, to make sure we end
# up balanced in the end.
#
re::rewrite_closing_parens: ->
stack: []
debt: {}
(debt[key]: 0) for key, val of INVERSES
this.scan_tokens (prev, token, post, i) =>
tag: token[0]
inv: INVERSES[token[0]]
# Push openers onto the stack.
if EXPRESSION_START.indexOf(tag) >= 0
stack.push(token)
return 1
# The end of an expression, check stack and debt for a pair.
else if EXPRESSION_TAIL.indexOf(tag) >= 0
# If the tag is already in our debt, swallow it.
if debt[inv] > 0
debt[inv] -= 1
this.tokens.splice(i, 1)
return 0
else
# Pop the stack of open delimiters.
match: stack.pop()
mtag: match[0]
# Continue onwards if it's the expected tag.
if tag is INVERSES[mtag]
return 1
else
# Unexpected close, insert correct close, adding to the debt.
debt[mtag] += 1
val: if mtag is 'INDENT' then match[1] else INVERSES[mtag]
this.tokens.splice(i, 0, [INVERSES[mtag], val])
return 1
else
return 1

12
src/runner.coffee Normal file
View File

@@ -0,0 +1,12 @@
# Quickie script to compile and run all the files given as arguments.
process.mixin require 'sys'
coffee: require './coffee-script'
paths: process.ARGV
paths: paths[2...paths.length]
if paths.length
coffee.compile_files paths, (js) -> eval(js)
else
require './repl'

View File

@@ -4,12 +4,12 @@ area: (x, y, x1, y1) ->
x: y: 10
x1: y1: 20
print area(x, y, x1, y1) is 100
puts area(x, y, x1, y1) is 100
print(area(x, y,
puts(area(x, y,
x1, y1) is 100)
print(area(
puts(area(
x
y
x1
@@ -19,7 +19,7 @@ print(area(
# Arguments are turned into arrays.
curried: ->
print area.apply(this, arguments.concat(20, 20)) is 100
puts area.apply(this, arguments.concat(20, 20)) is 100
curried 10, 10
@@ -29,9 +29,9 @@ func: ->
arguments: 25
arguments
print func(100) is 25
puts func(100) is 25
# Arguments can be accessed as a property.
this.arguments: 10
print @arguments is 10
puts @arguments is 10

View File

@@ -1,15 +1,15 @@
nums: n * n for n in [1, 2, 3] when n % 2 isnt 0
results: n * 2 for n in nums
print results.join(',') is '2,18'
puts results.join(',') is '2,18'
obj: {one: 1, two: 2, three: 3}
names: prop + '!' for prop of obj
odds: prop + '!' for prop, value of obj when value % 2 isnt 0
print names.join(' ') is "one! two! three!"
print odds.join(' ') is "one! three!"
puts names.join(' ') is "one! two! three!"
puts odds.join(' ') is "one! three!"
evens: for num in [1, 2, 3, 4, 5, 6] when num % 2 is 0
@@ -17,12 +17,12 @@ evens: for num in [1, 2, 3, 4, 5, 6] when num % 2 is 0
num -= 2
num * -1
print evens.join(', ') is '4, 6, 8'
puts evens.join(', ') is '4, 6, 8'
# Make sure that the "in" operator still works.
print 2 in evens
puts 2 in evens
# When functions are being defined within the body of a comprehension, make
@@ -37,12 +37,12 @@ for method in methods
obj[name]: ->
"I'm " + name
print obj.one() is "I'm one"
print obj.two() is "I'm two"
print obj.three() is "I'm three"
puts obj.one() is "I'm one"
puts obj.two() is "I'm two"
puts obj.three() is "I'm three"
# Steps should work for array comprehensions.
array: [0..10]
print num % 2 is 0 for num in array by 2
puts num % 2 is 0 for num in array by 2

View File

@@ -7,7 +7,7 @@ catch error
result2: try nonexistent * missing catch error then true
print result is true and result2 is true
puts result is true and result2 is true
# Assign to conditional.
@@ -16,8 +16,8 @@ get_x: -> 10
if x: get_x() then 100
print x is 10
puts x is 10
x: if get_x() then 100
print x is 100
puts x is 100

View File

@@ -1,4 +1,4 @@
results: [1, 2, 3].map (x) ->
x * x
print results.join(' ') is '1 4 9'
puts results.join(' ') is '1 4 9'

View File

@@ -20,7 +20,7 @@ ThirdChild::func: (string) ->
result: (new ThirdChild()).func 'four'
print result is 'zero/one/two/three/four'
puts result is 'zero/one/two/three/four'
TopClass: (arg) ->
@@ -35,4 +35,4 @@ SubClass: ->
SuperClass extends TopClass
SubClass extends SuperClass
print((new SubClass()).prop is 'top-super-sub')
puts((new SubClass()).prop is 'top-super-sub')

View File

@@ -3,7 +3,7 @@ identity_wrap: (x) ->
result: identity_wrap(identity_wrap(true))()()
print result
puts result
str: 'god'
@@ -14,7 +14,7 @@ result: str.
reverse().
reverse()
print result.join('') is 'dog'
puts result.join('') is 'dog'
result: str
.split('')
@@ -22,4 +22,4 @@ result: str
.reverse()
.reverse()
print result.join('') is 'dog'
puts result.join('') is 'dog'

View File

@@ -3,26 +3,26 @@ b: -2
[a, b]: [b, a]
print a is -2
print b is -1
puts a is -2
puts b is -1
arr: [1, 2, 3]
[a, b, c]: arr
print a is 1
print b is 2
print c is 3
puts a is 1
puts b is 2
puts c is 3
obj: {x: 10, y: 20, z: 30}
{x: a, y: b, z: c}: obj
print a is 10
print b is 20
print c is 30
puts a is 10
puts b is 20
puts c is 30
person: {
@@ -42,8 +42,8 @@ person: {
{name: a, family: {brother: {addresses: [one, {city: b}]}}}: person
print a is "Bob"
print b is "Moquasset NY, 10021"
puts a is "Bob"
puts b is "Moquasset NY, 10021"
test: {
@@ -59,4 +59,4 @@ test: {
{person: {address: [ignore, addr...]}}: test
print addr.join(', ') is "Street 101, Apt 101, City 101"
puts addr.join(', ') is "Street 101, Apt 101, City 101"

View File

@@ -26,4 +26,4 @@ func: ->
c.single: c.list[1..1][0]
print func() is '-'
puts func() is '-'

View File

@@ -1,8 +1,8 @@
print(if my_special_variable? then false else true)
puts(if my_special_variable? then false else true)
my_special_variable: false
print(if my_special_variable? then true else false)
puts(if my_special_variable? then true else false)
# Existential assignment.
@@ -12,7 +12,7 @@ a: null
a ?= 10
b ?= 10
print a is 10 and b is 10
puts a is 10 and b is 10
# The existential operator.
@@ -20,7 +20,7 @@ print a is 10 and b is 10
z: null
x: z ? "EX"
print z is null and x is "EX"
puts z is null and x is "EX"
# Only evaluate once.
@@ -30,7 +30,7 @@ get_next_node: ->
throw "up" if counter
counter++
print(if get_next_node()? then true else false)
puts(if get_next_node()? then true else false)
# Existence chains, soaking up undefined properties:
@@ -39,19 +39,19 @@ obj: {
prop: "hello"
}
print obj?.prop is "hello"
puts obj?.prop is "hello"
print obj.prop?.length is 5
puts obj.prop?.length is 5
print obj?.prop?.non?.existent?.property is undefined
puts obj?.prop?.non?.existent?.property is undefined
# Soaks and caches method calls as well.
arr: ["--", "----"]
print arr.pop()?.length is 4
print arr.pop()?.length is 2
print arr.pop()?.length is undefined
print arr[0]?.length is undefined
print arr.pop()?.length?.non?.existent()?.property is undefined
puts arr.pop()?.length is 4
puts arr.pop()?.length is 2
puts arr.pop()?.length is undefined
puts arr[0]?.length is undefined
puts arr.pop()?.length?.non?.existent()?.property is undefined

View File

@@ -9,7 +9,7 @@ findit: (items) ->
for item in items
return item if item is "bacon"
print findit(items) is "bacon"
puts findit(items) is "bacon"
# When when a closure wrapper is generated for expression conversion, make sure
@@ -26,5 +26,5 @@ obj: {
this.num
}
print obj.num is obj.func()
print obj.num is obj.result
puts obj.num is obj.func()
puts obj.num is obj.result

View File

@@ -7,10 +7,10 @@ result: if a
if d
true
print result
puts result
first: if false then false else second: if false then false else true
print first
print second
puts first
puts second

View File

@@ -2,11 +2,11 @@ x: 1
y: {}
y.x: -> 3
print x is 1
print typeof(y.x) is 'function'
print y.x instanceof Function
print y.x() is 3
print y.x.name is 'x'
puts x is 1
puts typeof(y.x) is 'function'
puts y.x instanceof Function
puts y.x() is 3
puts y.x.name is 'x'
# The empty function should not cause a syntax error.
@@ -17,10 +17,10 @@ obj: {
name: "Fred"
bound: ->
(=> print(this.name is "Fred"))()
(=> puts(this.name is "Fred"))()
unbound: ->
(-> print(!this.name?))()
(-> puts(!this.name?))()
}
obj.unbound()
@@ -44,18 +44,18 @@ Math: {
FastAdd: memoize (a, b) -> a + b
}
print Math.Add(5, 5) is 10
print Math.AnonymousAdd(10, 10) is 20
print Math.FastAdd(20, 20) is 40
puts Math.Add(5, 5) is 10
puts Math.AnonymousAdd(10, 10) is 20
puts Math.FastAdd(20, 20) is 40
# Parens are optional on simple function calls.
print 100 > 1 if 1 > 0
print true unless false
print true for i in [1..3]
puts 100 > 1 if 1 > 0
puts true unless false
puts true for i in [1..3]
print_func: (f) -> print(f())
print_func -> true
puts_func: (f) -> puts(f())
puts_func -> true
# Optional parens can be used in a nested fashion.
call: (func) -> func()
@@ -64,7 +64,7 @@ result: call ->
inner: call ->
Math.Add(5, 5)
print result is 10
puts result is 10
# And even with strange things like this:
@@ -72,8 +72,8 @@ print result is 10
funcs: [(x) -> x, (x) -> x * x]
result: funcs[1] 5
print result is 25
puts result is 25
result: ("hello".slice) 3
print result is 'lo'
puts result is 'lo'

View File

@@ -18,4 +18,4 @@ switch 'string'
code()
# comment
print func()
puts func()

View File

@@ -3,7 +3,7 @@ a: """
on two lines
"""
print a is "basic heredoc\non two lines"
puts a is "basic heredoc\non two lines"
a: '''
@@ -12,12 +12,12 @@ a: '''
c
'''
print a is "a\n \"b\nc"
puts a is "a\n \"b\nc"
a: '''one-liner'''
print a is 'one-liner'
puts a is 'one-liner'
a: """
@@ -25,7 +25,7 @@ a: """
here
"""
print a is "out\nhere"
puts a is "out\nhere"
a: '''
@@ -34,7 +34,7 @@ a: '''
c
'''
print a is " a\n b\nc"
puts a is " a\n b\nc"
a: '''
a
@@ -43,4 +43,4 @@ a
b c
'''
print a is "a\n\n\nb c"
puts a is "a\n\n\nb c"

View File

@@ -1,10 +1,10 @@
num: 1 + 2 + (a: 3)
print num is 6
puts num is 6
result: if true
false
other: "result"
print result is "result" and other is "result"
puts result is "result" and other is "result"

View File

@@ -1,40 +1,40 @@
a: [(x) -> x, (x) -> x * x]
print a.length is 2
puts a.length is 2
regex: /match/i
words: "I think there is a match in here."
print !!words.match(regex)
puts !!words.match(regex)
neg: (3 -4)
print neg is -1
puts neg is -1
func: ->
return if true
print func() is null
puts func() is null
str: "\\"
reg: /\\/
print reg(str) and str is '\\'
puts reg(str) and str is '\\'
i: 10
while i -= 1
print i is 0
puts i is 0
money$: 'dollars'
print money$ is 'dollars'
puts money$ is 'dollars'
bob: {
@@ -45,4 +45,4 @@ bob: {
@greet "Hello"
}
print bob.hello() is "Hello Bob"
puts bob.hello() is "Hello Bob"

View File

@@ -6,6 +6,6 @@ multi_liner:
single_liner:
[x, y] for y in [3..5] for x in [3..5]
print multi_liner.length is single_liner.length
print 5 is multi_liner[2][2][1]
print 5 is single_liner[2][2][1]
puts multi_liner.length is single_liner.length
puts 5 is multi_liner[2][2][1]
puts 5 is single_liner[2][2][1]

View File

@@ -3,4 +3,4 @@ six:
2 +
3
print six is 6
puts six is 6

View File

@@ -1,12 +1,12 @@
# CoffeeScript's operations should be chainable, like Python's.
print 500 > 50 > 5 > -5
puts 500 > 50 > 5 > -5
print true is not false is true is not false
puts true is not false is true is not false
print 10 < 20 > 10
puts 10 < 20 > 10
print 50 > 10 > 5 is parseInt('5', 10)
puts 50 > 10 > 5 is parseInt('5', 10)
# Make sure that each argument is only evaluated once, even if used
@@ -15,4 +15,4 @@ print 50 > 10 > 5 is parseInt('5', 10)
i: 0
func: -> i++
print 1 > func() < 1
puts 1 > func() < 1

View File

@@ -5,16 +5,16 @@ negs: negs[0..2]
result: nums.concat(negs).join(', ')
print result is '3, 6, 9, -20, -19, -18'
puts result is '3, 6, 9, -20, -19, -18'
# Ensure that ranges are safe. This used to infinite loop:
j = 5
result: for j in [j..(j+3)]
j
print result.join(' ') is '5 6 7 8'
puts result.join(' ') is '5 6 7 8'
# With range comprehensions, you can loop in steps.
results: x for x in [0..25] by 5
print results.join(' ') is '0 5 10 15 20 25'
puts results.join(' ') is '0 5 10 15 20 25'

View File

@@ -5,7 +5,12 @@ b: array[2...4]
result: a.concat(b).join(' ')
print result is "7 8 9 2 3"
puts result is "7 8 9 2 3"
countdown: [10..1].join(' ')
print countdown is "10 9 8 7 6 5 4 3 2 1"
puts countdown is "10 9 8 7 6 5 4 3 2 1"
array: [(1+5)..1+9]
puts array.join(' ') is "6 7 8 9 10"

View File

@@ -3,7 +3,7 @@ func: (first, second, rest...) ->
result: func 1, 2, 3, 4, 5
print result is "3 4 5"
puts result is "3 4 5"
gold: silver: bronze: the_field: null
@@ -29,7 +29,7 @@ contenders: [
medalists "Mighty Mouse", contenders...
print gold is "Mighty Mouse"
print silver is "Michael Phelps"
print bronze is "Liu Xiang"
print the_field.length is 8
puts gold is "Mighty Mouse"
puts silver is "Michael Phelps"
puts bronze is "Liu Xiang"
puts the_field.length is 8

View File

@@ -2,4 +2,4 @@ array: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
array[5..10]: [0, 0, 0]
print array.join(' ') is '0 1 2 3 4 0 0 0'
puts array.join(' ') is '0 1 2 3 4 0 0 0'

View File

@@ -14,7 +14,7 @@ result: switch num
when 11 then false
else false
print result
puts result
func: (num) ->
switch num
@@ -24,7 +24,7 @@ func: (num) ->
false
else false
print func(2)
print func(6)
print !func(3)
print !func(8)
puts func(2)
puts func(6)
puts !func(3)
puts !func(8)

View File

@@ -1,17 +1,17 @@
i: 100
while i -= 1
print i is 0
puts i is 0
i: 5
list: while i -= 1
i * 2
print list.join(' ') is "8 6 4 2"
puts list.join(' ') is "8 6 4 2"
i: 5
list: (i * 3 while i -= 1)
print list.join(' ') is "12 9 6 3"
puts list.join(' ') is "12 9 6 3"

View File

@@ -3,14 +3,14 @@
result: while sunny?
go_outside()
print(3 + try
puts(3 + try
nonexistent.no_way
catch error
print(error)
puts(error)
3
)
func: (x) ->
return throw x
print(x * x for x in [1..100])
puts(x * x for x in [1..100])

View File

@@ -19,6 +19,13 @@ class ExecutionTest < Test::Unit::TestCase
end
end
# Test all of the code examples under Narwhal as well.
def test_execution_with_narwhal
(`bin/coffee -r --narwhal #{SOURCES.join(' ')}`).split("\n").each do |line|
assert line == "true"
end
end
def test_lintless_tests
no_warnings `bin/coffee -l test/fixtures/*/*.coffee`
end