first draft of adding classes to CoffeeScript

This commit is contained in:
Jeremy Ashkenas 2010-02-27 18:57:45 -05:00
parent 7d39fe1c56
commit 1c7e4c4203
10 changed files with 321 additions and 168 deletions

View File

@ -288,7 +288,7 @@
</dict>
<dict>
<key>match</key>
<string>\b(super|this|extends)\b</string>
<string>\b(super|this|extends|class)\b</string>
<key>name</key>
<string>variable.language.coffee</string>
</dict>

View File

@ -16,7 +16,7 @@
}
};
// Precedence ===========================================================
operators = [["left", '?'], ["nonassoc", 'UMINUS', 'UPLUS', 'NOT', '!', '!!', '~', '++', '--'], ["left", '*', '/', '%'], ["left", '+', '-'], ["left", '<<', '>>', '>>>'], ["left", '&', '|', '^'], ["left", '<=', '<', '>', '>='], ["right", 'DELETE', 'INSTANCEOF', 'TYPEOF'], ["right", '==', '!=', 'IS', 'ISNT'], ["left", '&&', '||', 'AND', 'OR'], ["right", '-=', '+=', '/=', '*=', '%=', '||=', '&&=', '?='], ["left", '.'], ["right", 'INDENT'], ["left", 'OUTDENT'], ["right", 'WHEN', 'LEADING_WHEN', 'IN', 'OF', 'BY', 'THROW'], ["right", 'FOR', 'NEW', 'SUPER'], ["left", 'EXTENDS'], ["right", 'ASSIGN', 'RETURN'], ["right", '->', '=>', 'UNLESS', 'IF', 'ELSE', 'WHILE']];
operators = [["left", '?'], ["nonassoc", 'UMINUS', 'UPLUS', 'NOT', '!', '!!', '~', '++', '--'], ["left", '*', '/', '%'], ["left", '+', '-'], ["left", '<<', '>>', '>>>'], ["left", '&', '|', '^'], ["left", '<=', '<', '>', '>='], ["right", 'DELETE', 'INSTANCEOF', 'TYPEOF'], ["right", '==', '!=', 'IS', 'ISNT'], ["left", '&&', '||', 'AND', 'OR'], ["right", '-=', '+=', '/=', '*=', '%=', '||=', '&&=', '?='], ["left", '.'], ["right", 'INDENT'], ["left", 'OUTDENT'], ["right", 'WHEN', 'LEADING_WHEN', 'IN', 'OF', 'BY', 'THROW'], ["right", 'FOR', 'NEW', 'SUPER', 'CLASS'], ["left", 'EXTENDS'], ["right", 'ASSIGN', 'RETURN'], ["right", '->', '=>', 'UNLESS', 'IF', 'ELSE', 'WHILE']];
// Grammar ==============================================================
grammar = {
// All parsing will end in this rule, being the trunk of the AST.
@ -41,7 +41,7 @@
],
// 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")],
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("Class"), 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() {
@ -292,6 +292,19 @@
// An object literal.
Object: [o("{ AssignList }", function() {
return new ObjectNode($2);
}), o("{ IndentedAssignList }", function() {
return new ObjectNode($2);
})
],
// A class literal.
Class: [o("CLASS Value", function() {
return new ClassNode($2);
}), o("CLASS Value EXTENDS Value", function() {
return new ClassNode($2, $4);
}), o("CLASS Value IndentedAssignList", function() {
return new ClassNode($2, null, $3);
}), o("CLASS Value EXTENDS Value IndentedAssignList", function() {
return new ClassNode($2, $4, $5);
})
],
// Assignment within an object literal (comma or newline separated).
@ -305,7 +318,10 @@
return $1.concat([$3]);
}), o("AssignList , TERMINATOR AssignObj", function() {
return $1.concat([$4]);
}), o("INDENT AssignList OUTDENT", function() {
})
],
// A list of assignments in a block indentation.
IndentedAssignList: [o("INDENT AssignList OUTDENT", function() {
return $2;
})
],

View File

@ -12,14 +12,14 @@
exports.Lexer = (lex = function lex() { });
// Constants ============================================================
// Keywords that CoffeScript shares in common with JS.
JS_KEYWORDS = ["if", "else", "true", "false", "new", "return", "try", "catch", "finally", "throw", "break", "continue", "for", "in", "while", "delete", "instanceof", "typeof", "switch", "super", "extends"];
JS_KEYWORDS = ["if", "else", "true", "false", "new", "return", "try", "catch", "finally", "throw", "break", "continue", "for", "in", "while", "delete", "instanceof", "typeof", "switch", "super", "extends", "class"];
// CoffeeScript-only keywords -- which we're more relaxed about allowing.
COFFEE_KEYWORDS = ["then", "unless", "yes", "no", "on", "off", "and", "or", "is", "isnt", "not", "of", "by", "where", "when"];
// The list of keywords passed verbatim to the parser.
KEYWORDS = JS_KEYWORDS.concat(COFFEE_KEYWORDS);
// The list of keywords that are reserved by JavaScript, but not used, and aren't
// used by CoffeeScript. Using these will throw an error at compile time.
RESERVED = ["case", "default", "do", "function", "var", "void", "with", "class", "const", "let", "debugger", "enum", "export", "import", "native"];
RESERVED = ["case", "default", "do", "function", "var", "void", "with", "const", "let", "debugger", "enum", "export", "import", "native"];
// JavaScript keywords and reserved words together, excluding CoffeeScript ones.
JS_FORBIDDEN = JS_KEYWORDS.concat(RESERVED);
// Token matching regexes. (keep the IDENTIFIER regex in sync with AssignNode.)

View File

@ -1,5 +1,5 @@
(function(){
var AccessorNode, ArrayNode, AssignNode, BaseNode, CallNode, ClosureNode, CodeNode, CommentNode, ExistenceNode, Expressions, ExtendsNode, ForNode, IDENTIFIER, IfNode, IndexNode, LiteralNode, ObjectNode, OpNode, ParentheticalNode, PushNode, RangeNode, ReturnNode, SliceNode, SplatNode, TAB, TRAILING_WHITESPACE, ThrowNode, TryNode, ValueNode, WhileNode, compact, del, flatten, inherit, merge, statement;
var AccessorNode, ArrayNode, AssignNode, BaseNode, CallNode, ClassNode, ClosureNode, CodeNode, CommentNode, ExistenceNode, Expressions, ExtendsNode, ForNode, IDENTIFIER, IfNode, IndexNode, LiteralNode, ObjectNode, OpNode, ParentheticalNode, PushNode, RangeNode, ReturnNode, SliceNode, SplatNode, TAB, TRAILING_WHITESPACE, ThrowNode, TryNode, ValueNode, WhileNode, compact, del, flatten, inherit, merge, statement;
var __hasProp = Object.prototype.hasOwnProperty;
(typeof process !== "undefined" && process !== null) ? process.mixin(require('scope')) : (this.exports = this);
// Some helper functions
@ -658,6 +658,44 @@
return '{' + inner + '}';
}
}));
// A class literal, including optional superclass and constructor.
ClassNode = (exports.ClassNode = inherit(BaseNode, {
type: 'Class',
constructor: function constructor(variable, parent, props) {
this.children = compact(flatten([(this.variable = variable), (this.parent = parent), (this.properties = props || [])]));
return this;
},
compile_node: function compile_node(o) {
var _a, _b, _c, construct, extension, func, prop, props, ret, returns, val;
extension = this.parent && new ExtendsNode(this.variable, this.parent);
constructor = null;
props = new Expressions();
o.top = true;
ret = del(o, 'returns');
_a = this.properties;
for (_b = 0, _c = _a.length; _b < _c; _b++) {
prop = _a[_b];
if (prop.variable.base.value === 'constructor') {
func = prop.value;
func.body.push(new ReturnNode(new LiteralNode('this')));
constructor = new AssignNode(this.variable, func);
} else {
val = new ValueNode(this.variable, [new AccessorNode(prop.variable, 'prototype')]);
prop = new AssignNode(val, prop.value);
props.push(prop);
}
}
if (!(constructor)) {
constructor = new AssignNode(this.variable, new CodeNode());
}
construct = this.idt() + constructor.compile(o) + ';\n';
props = props.empty() ? '' : props.compile(o) + '\n';
extension = extension ? extension.compile(o) + '\n' : '';
returns = ret ? '\n' + this.idt() + 'return ' + this.variable.compile(o) + ';' : '';
return construct + extension + props + returns;
}
}));
statement(ClassNode);
// An array literal.
ArrayNode = (exports.ArrayNode = inherit(BaseNode, {
type: 'Array',
@ -826,8 +864,8 @@
CodeNode = (exports.CodeNode = inherit(BaseNode, {
type: 'Code',
constructor: function constructor(params, body, tag) {
this.params = params;
this.body = body;
this.params = params || [];
this.body = body || new Expressions();
this.bound = tag === 'boundfunc';
return this;
},

File diff suppressed because one or more lines are too long

View File

@ -31,7 +31,7 @@ operators: [
["right", 'INDENT']
["left", 'OUTDENT']
["right", 'WHEN', 'LEADING_WHEN', 'IN', 'OF', 'BY', 'THROW']
["right", 'FOR', 'NEW', 'SUPER']
["right", 'FOR', 'NEW', 'SUPER', 'CLASS']
["left", 'EXTENDS']
["right", 'ASSIGN', 'RETURN']
["right", '->', '=>', 'UNLESS', 'IF', 'ELSE', 'WHILE']
@ -72,6 +72,7 @@ grammar: {
o "For"
o "Switch"
o "Extends"
o "Class"
o "Splat"
o "Existence"
o "Comment"
@ -257,6 +258,15 @@ grammar: {
# An object literal.
Object: [
o "{ AssignList }", -> new ObjectNode($2)
o "{ IndentedAssignList }", -> new ObjectNode($2)
]
# A class literal.
Class: [
o "CLASS Value", -> new ClassNode($2)
o "CLASS Value EXTENDS Value", -> new ClassNode($2, $4)
o "CLASS Value IndentedAssignList", -> new ClassNode($2, null, $3)
o "CLASS Value EXTENDS Value IndentedAssignList", -> new ClassNode($2, $4, $5)
]
# Assignment within an object literal (comma or newline separated).
@ -266,6 +276,10 @@ grammar: {
o "AssignList , AssignObj", -> $1.concat [$3]
o "AssignList TERMINATOR AssignObj", -> $1.concat [$3]
o "AssignList , TERMINATOR AssignObj", -> $1.concat [$4]
]
# A list of assignments in a block indentation.
IndentedAssignList: [
o "INDENT AssignList OUTDENT", -> $2
]

View File

@ -20,7 +20,7 @@ JS_KEYWORDS: [
"break", "continue",
"for", "in", "while",
"delete", "instanceof", "typeof",
"switch", "super", "extends"
"switch", "super", "extends", "class"
]
# CoffeeScript-only keywords -- which we're more relaxed about allowing.
@ -37,7 +37,7 @@ KEYWORDS: JS_KEYWORDS.concat COFFEE_KEYWORDS
# The list of keywords that are reserved by JavaScript, but not used, and aren't
# used by CoffeeScript. Using these will throw an error at compile time.
RESERVED: [
"case", "default", "do", "function", "var", "void", "with", "class"
"case", "default", "do", "function", "var", "void", "with"
"const", "let", "debugger", "enum", "export", "import", "native"
]

View File

@ -522,6 +522,43 @@ ObjectNode: exports.ObjectNode: inherit BaseNode, {
}
# A class literal, including optional superclass and constructor.
ClassNode: exports.ClassNode: inherit BaseNode, {
type: 'Class'
constructor: (variable, parent, props) ->
@children: compact flatten [@variable: variable, @parent: parent, @properties: props or []]
this
compile_node: (o) ->
extension: @parent and new ExtendsNode(@variable, @parent)
constructor: null
props: new Expressions()
o.top: true
ret: del o, 'returns'
for prop in @properties
if prop.variable.base.value is 'constructor'
func: prop.value
func.body.push(new ReturnNode(new LiteralNode('this')))
constructor: new AssignNode(@variable, func)
else
val: new ValueNode(@variable, [new AccessorNode(prop.variable, 'prototype')])
prop: new AssignNode(val, prop.value)
props.push prop
constructor: new AssignNode(@variable, new CodeNode()) unless constructor
construct: @idt() + constructor.compile(o) + ';\n'
props: if props.empty() then '' else props.compile(o) + '\n'
extension: if extension then extension.compile(o) + '\n' else ''
returns: if ret then '\n' + @idt() + 'return ' + @variable.compile(o) + ';' else ''
construct + extension + props + returns
}
statement ClassNode
# An array literal.
ArrayNode: exports.ArrayNode: inherit BaseNode, {
type: 'Array'
@ -650,8 +687,8 @@ CodeNode: exports.CodeNode: inherit BaseNode, {
type: 'Code'
constructor: (params, body, tag) ->
@params: params
@body: body
@params: params or []
@body: body or new Expressions()
@bound: tag is 'boundfunc'
this

36
test/test_classes.coffee Normal file
View File

@ -0,0 +1,36 @@
class Base
func: (string) ->
'zero/' + string
class FirstChild extends Base
func: (string) ->
super('one/') + string
class SecondChild extends FirstChild
func: (string) ->
super('two/') + string
class ThirdChild extends SecondChild
constructor: ->
@array: [1, 2, 3]
func: (string) ->
super('three/') + string
result: (new ThirdChild()).func 'four'
ok result is 'zero/one/two/three/four'
class TopClass
constructor: (arg) ->
@prop: 'top-' + arg
class SuperClass extends TopClass
constructor: (arg) ->
super 'super-' + arg
class SubClass extends SuperClass
constructor: ->
super 'sub'
ok (new SubClass()).prop is 'top-super-sub'