more progress with the lexer, perhaps it's done

This commit is contained in:
Jeremy Ashkenas
2010-01-30 15:56:40 -05:00
parent f19360c6b9
commit babeebcc1a
6 changed files with 247 additions and 121 deletions

View File

@@ -1,39 +1,41 @@
(function(){
var lex, sys;
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, STRING, STRING_NEWLINES, WHITESPACE, lex, sys;
sys = require('sys');
// 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.
lex.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"];
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.
lex.IDENTIFIER = /^([a-zA-Z$_](\w|\$)*)/;
lex.NUMBER = /^(\b((0(x|X)[0-9a-fA-F]+)|([0-9]+(\.[0-9]+)?(e[+\-]?[0-9]+)?)))\b/i;
lex.STRING = /^(""|''|"([\s\S]*?)([^\\]|\\\\)"|'([\s\S]*?)([^\\]|\\\\)')/;
lex.HEREDOC = /^("{6}|'{6}|"{3}\n?([\s\S]*?)\n?([ \t]*)"{3}|'{3}\n?([\s\S]*?)\n?([ \t]*)'{3})/;
lex.JS = /^(``|`([\s\S]*?)([^\\]|\\\\)`)/;
lex.OPERATOR = /^([+\*&|\/\-%=<>:!?]+)/;
lex.WHITESPACE = /^([ \t]+)/;
lex.COMMENT = /^(((\n?[ \t]*)?#.*$)+)/;
lex.CODE = /^((-|=)>)/;
lex.REGEX = /^(\/(.*?)([^\\]|\\\\)\/[imgy]{0,4})/;
lex.MULTI_DENT = /^((\n([ \t]*))+)(\.)?/;
lex.LAST_DENT = /\n([ \t]*)/;
lex.ASSIGNMENT = /^(:|=)$/;
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.
lex.JS_CLEANER = /(^`|`$)/g;
lex.MULTILINER = /\n/g;
lex.STRING_NEWLINES = /\n[ \t]*/g;
lex.COMMENT_CLEANER = /(^[ \t]*#|\n[ \t]*$)/mg;
lex.NO_NEWLINE = /^([+\*&|\/\-%=<>:!.\\][<>=&|]*|and|or|is|isnt|not|delete|typeof|instanceof)$/;
lex.HEREDOC_INDENT = /^[ \t]+/g;
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
lex.NOT_REGEX = ['IDENTIFIER', 'NUMBER', 'REGEX', 'STRING', ')', '++', '--', ']', '}', 'FALSE', 'NULL', 'TRUE'];
NOT_REGEX = ['IDENTIFIER', 'NUMBER', 'REGEX', 'STRING', ')', '++', '--', ']', '}', 'FALSE', 'NULL', 'TRUE'];
// Tokens which could legitimately be invoked or indexed.
lex.CALLABLE = ['IDENTIFIER', 'SUPER', ')', ']', '}', 'STRING'];
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;
@@ -52,8 +54,8 @@
this.chunk = this.code.slice(this.i);
this.extract_next_token();
}
// sys.puts "original stream: #{@tokens.inspect}" if process.ENV['VERBOSE']
// this.close_indentation()
// sys.puts "original stream: " + this.tokens if process.ENV['VERBOSE']
this.close_indentation();
// (new Rewriter()).rewrite(this.tokens)
return this.tokens;
};
@@ -78,7 +80,9 @@
if (this.regex_token()) {
return null;
}
// return if this.indent_token()
if (this.indent_token()) {
return null;
}
if (this.comment_token()) {
return null;
}
@@ -91,12 +95,12 @@
// Matches identifying literals: variables, keywords, method names, etc.
lex.prototype.identifier_token = function identifier_token() {
var id, tag;
if (!((id = this.match(lex.IDENTIFIER, 1)))) {
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 = lex.KEYWORDS.indexOf(id) >= 0 ? id.toUpperCase() : 'IDENTIFIER';
tag = KEYWORDS.indexOf(id) >= 0 ? id.toUpperCase() : 'IDENTIFIER';
if (tag === 'WHEN' && (this.tag() === 'OUTDENT' || this.tag() === 'INDENT')) {
tag = 'LEADING_WHEN';
}
@@ -117,7 +121,7 @@
// Matches numbers, including decimals, hex, and exponential notation.
lex.prototype.number_token = function number_token() {
var number;
if (!((number = this.match(lex.NUMBER, 1)))) {
if (!((number = this.match(NUMBER, 1)))) {
return false;
}
this.token('NUMBER', number);
@@ -126,7 +130,7 @@
// Matches strings, including multi-line strings.
lex.prototype.string_token = function string_token() {
var escaped, string;
if (!((string = this.match(lex.STRING, 1)))) {
if (!((string = this.match(STRING, 1)))) {
return false;
}
escaped = string.replace(STRING_NEWLINES, " \\\n");
@@ -137,12 +141,12 @@
// Matches heredocs, adjusting indentation to the correct level.
lex.prototype.heredoc_token = function heredoc_token() {
var doc, indent, match;
if (!((match = this.chunk.match(lex.HEREDOC)))) {
if (!((match = this.chunk.match(HEREDOC)))) {
return false;
}
doc = match[2] || match[4];
indent = doc.match(lex.HEREDOC_INDENT).sort()[0];
doc = doc.replace(new RegExp("^" + indent, 'g'), '').replace(lex.MULTILINER, "\\n").replace('"', '\\"');
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");
return this.i += match[1].length;
@@ -150,19 +154,19 @@
// Matches interpolated JavaScript.
lex.prototype.js_token = function js_token() {
var script;
if (!((script = this.match(lex.JS, 1)))) {
if (!((script = this.match(JS, 1)))) {
return false;
}
this.token('JS', script.replace(lex.JS_CLEANER, ''));
this.token('JS', script.replace(JS_CLEANER, ''));
return this.i += script.length;
};
// Matches regular expression literals.
lex.prototype.regex_token = function regex_token() {
var regex;
if (!((regex = this.match(lex.REGEX, 1)))) {
if (!((regex = this.match(REGEX, 1)))) {
return false;
}
if (lex.NOT_REGEX.indexOf(this.tag()) >= 0) {
if (NOT_REGEX.indexOf(this.tag()) >= 0) {
return false;
}
this.token('REGEX', regex);
@@ -171,36 +175,88 @@
// Matches and conumes comments.
lex.prototype.comment_token = function comment_token() {
var comment;
if (!((comment = this.match(lex.COMMENT, 1)))) {
if (!((comment = this.match(COMMENT, 1)))) {
return false;
}
this.line += comment.match(lex.MULTILINER).length;
this.token('COMMENT', comment.replace(lex.COMMENT_CLEANER, '').split(lex.MULTILINER));
this.line += comment.match(MULTILINER).length;
this.token('COMMENT', comment.replace(COMMENT_CLEANER, '').split(MULTILINER));
this.token("\n", "\n");
return this.i += comment.length;
};
// 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);
}
return this.indent = size;
};
// 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;
}
return this.token("\n", "\n");
};
// Matches and consumes non-meaningful whitespace.
lex.prototype.whitespace_token = function whitespace_token() {
var space;
if (!((space = this.match(lex.WHITESPACE, 1)))) {
if (!((space = this.match(WHITESPACE, 1)))) {
return false;
}
this.value().spaced = true;
return this.i += space.length;
};
// 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(lex.OPERATOR);
match = this.chunk.match(OPERATOR);
value = match && match[1];
if (value && value.match(lex.CODE)) {
tag_parameters();
if (value && value.match(CODE)) {
this.tag_parameters();
}
value = value || this.chunk.substr(0, 1);
tag = value.match(lex.ASSIGNMENT) ? 'ASSIGN' : value;
if (this.value() && this.value().spaced && lex.CALLABLE.indexOf(this.tag() >= 0)) {
tag = value.match(ASSIGNMENT) ? 'ASSIGN' : value;
if (this.value() && this.value().spaced && CALLABLE.indexOf(this.tag() >= 0)) {
if (value === '(') {
tag = 'CALL_START';
}
@@ -259,4 +315,36 @@
}
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 __a, i, tok;
if (this.tag() !== ')') {
return null;
}
i = 0;
__a = [];
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 __a;
};
// 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);
};
})();