feat(transpiler): constructor and typed field semantics

fixes #11 (constructor and typed field semantics)
fixes #42 (Should we infer class property types from ctor args ?)
fixes #17 (number (js) should map to num (dart))

Closes #45
This commit is contained in:
Victor Berchet 2014-10-01 16:29:45 +02:00 committed by Misko Hevery
parent fd0c2d8063
commit 089a2f1b62
15 changed files with 377 additions and 77 deletions

View File

@ -12,7 +12,7 @@ export class ChangeDetection {
} }
detectChanges():int { detectChanges():int {
var current:Record = _rootWatchGroup._headRecord; var current:Record = _rootWatchGroup.headRecord;
var count:number = 0; var count:number = 0;
while (current != null) { while (current != null) {
if (current.check()) { if (current.check()) {

View File

@ -2,11 +2,11 @@ import {ProtoRecord, Record} from './record';
import {FIELD} from 'facade/lang'; import {FIELD} from 'facade/lang';
export class ProtoWatchGroup { export class ProtoWatchGroup {
@FIELD('final _headRecord:ProtoRecord') @FIELD('final headRecord:ProtoRecord')
@FIELD('final _tailRecord:ProtoRecord') @FIELD('final tailRecord:ProtoRecord')
constructor() { constructor() {
this._headRecord = null; this.headRecord = null;
this._tailRecord = null; this.tailRecord = null;
} }
/** /**
@ -29,7 +29,7 @@ export class ProtoWatchGroup {
var watchGroup:WatchGroup = new WatchGroup(this, dispatcher); var watchGroup:WatchGroup = new WatchGroup(this, dispatcher);
var head:Record = null; var head:Record = null;
var tail:Record = null; var tail:Record = null;
var proto:ProtoRecord = this._headRecord; var proto:ProtoRecord = this.headRecord;
while(proto != null) { while(proto != null) {
tail = proto.instantiate(watchGroup); tail = proto.instantiate(watchGroup);
@ -37,14 +37,14 @@ export class ProtoWatchGroup {
proto = proto.next; proto = proto.next;
} }
proto = this._headRecord; proto = this.headRecord;
while(proto != null) { while(proto != null) {
proto.instantiateComplete(); proto.instantiateComplete();
proto = proto.next; proto = proto.next;
} }
watchGroup._headRecord = head; watchGroup.headRecord = head;
watchGroup._tailRecord = tail; watchGroup.tailRecord = tail;
return watchGroup; return watchGroup;
} }
@ -53,13 +53,13 @@ export class ProtoWatchGroup {
export class WatchGroup { export class WatchGroup {
@FIELD('final protoWatchGroup:ProtoWatchGroup') @FIELD('final protoWatchGroup:ProtoWatchGroup')
@FIELD('final dispatcher:WatchGroupDispatcher') @FIELD('final dispatcher:WatchGroupDispatcher')
@FIELD('final _headRecord:Record') @FIELD('final headRecord:Record')
@FIELD('final _tailRecord:Record') @FIELD('final tailRecord:Record')
constructor(protoWatchGroup:ProtoWatchGroup, dispatcher:WatchGroupDispatcher) { constructor(protoWatchGroup:ProtoWatchGroup, dispatcher:WatchGroupDispatcher) {
this.protoWatchGroup = protoWatchGroup; this.protoWatchGroup = protoWatchGroup;
this.dispatcher = dispatcher; this.dispatcher = dispatcher;
this._headRecord = null; this.headRecord = null;
this._tailRecord = null; this.tailRecord = null;
} }
insertChildGroup(newChild:WatchGroup, insertAfter:WatchGroup) { insertChildGroup(newChild:WatchGroup, insertAfter:WatchGroup) {

View File

@ -1,4 +1,5 @@
import {describe, it, expect} from 'test_lib/test_lib'; import {describe, it, expect} from 'test_lib/test_lib';
import {CONST} from './fixtures/annotations';
// Constructor // Constructor
// Define fields // Define fields
@ -13,6 +14,28 @@ class Foo {
} }
} }
class SubFoo extends Foo {
constructor(a, b) {
this.c = 3;
super(a, b);
}
}
class Const {
@CONST
constructor(a:number) {
this.a = a;
}
}
class SubConst extends Const {
@CONST
constructor(a:number, b:number) {
super(a);
this.b = b;
}
}
export function main() { export function main() {
describe('classes', function() { describe('classes', function() {
it('should work', function() { it('should work', function() {
@ -22,5 +45,21 @@ export function main() {
expect(foo.b).toBe(3); expect(foo.b).toBe(3);
expect(foo.sum()).toBe(5); expect(foo.sum()).toBe(5);
}); });
it('@CONST should be transpiled to a const constructor', function() {
var subConst = new SubConst(1, 2);
expect(subConst.a).toBe(1);
expect(subConst.b).toBe(2);
}); });
describe('inheritance', function() {
it('should support super call', function () {
var subFoo = new SubFoo(1, 2);
expect(subFoo.a).toBe(1);
expect(subFoo.b).toBe(2);
expect(subFoo.c).toBe(3);
});
});
});
} }

View File

@ -11,6 +11,10 @@ class Provide {
const Provide(this.token); const Provide(this.token);
} }
class CONST {
const CONST();
}
// TODO: this api does not yet return an array as we don't have // TODO: this api does not yet return an array as we don't have
// a nice array wrapper for Dart // a nice array wrapper for Dart
readFirstAnnotation(clazz) { readFirstAnnotation(clazz) {

View File

@ -10,6 +10,8 @@ export class Provide {
} }
} }
export class CONST {
}
// TODO: this api does not yet return an array as we don't have // TODO: this api does not yet return an array as we don't have
// a nice array wrapper for Dart // a nice array wrapper for Dart

View File

@ -1,20 +0,0 @@
import {ParseTree} from 'traceur/src/syntax/trees/ParseTree';
var CLASS_FIELD = 'CLASS_FIELD';
export class ClassFieldParseTree extends ParseTree {
constructor(location, identifier, typeAnnotation) {
this.location = location;
this.identifier = identifier;
this.typeAnnotation = typeAnnotation;
}
get type() {
return CLASS_FIELD;
}
visit(visitor) {
visitor.visitClassField(this);
}
transform(transformer) {
return this;
}
}

View File

@ -1,25 +1,44 @@
import {ParseTreeTransformer} from 'traceur/src/codegeneration/ParseTreeTransformer'; import {ParseTreeTransformer} from './ParseTreeTransformer';
import { import {
PROPERTY_METHOD_ASSIGNMENT, BINARY_EXPRESSION,
CALL_EXPRESSION,
IDENTIFIER_EXPRESSION,
MEMBER_EXPRESSION, MEMBER_EXPRESSION,
THIS_EXPRESSION, PROPERTY_METHOD_ASSIGNMENT,
BINARY_EXPRESSION SUPER_EXPRESSION,
THIS_EXPRESSION
} from 'traceur/src/syntax/trees/ParseTreeType'; } from 'traceur/src/syntax/trees/ParseTreeType';
import {EQUAL} from 'traceur/src/syntax/TokenType';
import {CONSTRUCTOR} from 'traceur/src/syntax/PredefinedName'; import {CONSTRUCTOR} from 'traceur/src/syntax/PredefinedName';
import {propName} from 'traceur/src/staticsemantics/PropName'; import {propName} from 'traceur/src/staticsemantics/PropName';
import {ClassFieldParseTree} from '../ast/class_field'; import {
BinaryExpression,
BindingIdentifier,
IdentifierExpression
} from 'traceur/src/syntax/trees/ParseTrees';
import {
ClassFieldDeclaration,
PropertyConstructorAssignment
} from '../syntax/trees/ParseTrees';
/** /**
* Transforms class declaration: * Transforms class declaration:
* - rename constructor (name of the class - default Dart constructor) * - rename constructor to the name of the class (default Dart constructor),
* * - class fields are extracted from `this.field = expression;` in the ctor body,
* - `@CONST` annotations on the ctor result in a const constructor & final fields in Dart,
* - const constructor body is converted to an initializerList
*/ */
export class ClassTransformer extends ParseTreeTransformer { export class ClassTransformer extends ParseTreeTransformer {
constructor(idGenerator, reporter) {
this.reporter_ = reporter;
}
/** /**
* @param {ClassDeclaration} tree * @param {ClassDeclaration} tree
* @returns {ParseTree} * @returns {ParseTree}
@ -28,12 +47,17 @@ export class ClassTransformer extends ParseTreeTransformer {
var className = tree.name.identifierToken.toString(); var className = tree.name.identifierToken.toString();
var argumentTypesMap = {}; var argumentTypesMap = {};
var fields = []; var fields = [];
var isConst;
var that = this;
tree.elements.forEach(function(elementTree) { tree.elements.forEach(function(elementTree, index) {
if (elementTree.type === PROPERTY_METHOD_ASSIGNMENT && if (elementTree.type === PROPERTY_METHOD_ASSIGNMENT &&
!elementTree.isStatic && !elementTree.isStatic &&
propName(elementTree) === CONSTRUCTOR) { propName(elementTree) === CONSTRUCTOR) {
isConst = elementTree.annotations.some((annotation) =>
annotation.name.identifierToken.value === 'CONST');
// Store constructor argument types, // Store constructor argument types,
// so that we can use them to set the types of simple-assigned fields. // so that we can use them to set the types of simple-assigned fields.
elementTree.parameterList.parameters.forEach(function(p) { elementTree.parameterList.parameters.forEach(function(p) {
@ -48,15 +72,54 @@ export class ClassTransformer extends ParseTreeTransformer {
// Collect all fields, defined in the constructor. // Collect all fields, defined in the constructor.
elementTree.body.statements.forEach(function(statement) { elementTree.body.statements.forEach(function(statement) {
if (statement.expression.type === BINARY_EXPRESSION && var exp = statement.expression;
statement.expression.operator.type === '=' && if (exp.type === BINARY_EXPRESSION &&
statement.expression.left.type === MEMBER_EXPRESSION && exp.operator.type === EQUAL &&
statement.expression.left.operand.type === THIS_EXPRESSION) { exp.left.type === MEMBER_EXPRESSION &&
exp.left.operand.type === THIS_EXPRESSION) {
var typeAnnotation = argumentTypesMap[statement.expression.left.memberName.value] || null; var typeAnnotation;
fields.push(new ClassFieldParseTree(tree.location, statement.expression.left.memberName, typeAnnotation));
if (exp.right.type === IDENTIFIER_EXPRESSION) {
// `this.field = variable;`
// we can infer the type of the field from the variable when it is a typed arg
var varName = exp.right.getStringValue();
typeAnnotation = argumentTypesMap[varName] || null;
}
var fieldName = exp.left.memberName.value;
var lvalue = new BindingIdentifier(tree.location, fieldName);
fields.push(new ClassFieldDeclaration(tree.location, lvalue, typeAnnotation, isConst));
} }
}); });
// Compute the initializer list
var initializerList = [];
var superCall = that._extractSuperCall(elementTree.body);
if (isConst) {
initializerList = that._extractFieldInitializers(elementTree.body);
if (elementTree.body.statements.length > 0) {
that.reporter_.reportError(
elementTree.location,
'Const constructor body can only contain field initialization & super call');
}
}
if (superCall) initializerList.push(superCall);
// Replace the `PROPERTY_METHOD_ASSIGNMENT` with a Dart specific
// `PROPERTY_CONSTRUCTOR_ASSIGNMENT`
tree.elements[index] = new PropertyConstructorAssignment(
elementTree.location,
elementTree.isStatic,
elementTree.functionKind,
elementTree.name,
elementTree.parameterList,
elementTree.typeAnnotation,
elementTree.annotations,
elementTree.body,
isConst,
initializerList
);
} }
}); });
@ -64,5 +127,58 @@ export class ClassTransformer extends ParseTreeTransformer {
tree.elements = fields.concat(tree.elements); tree.elements = fields.concat(tree.elements);
return super(tree); return super(tree);
}; }
/**
* Extract field initialization (`this.field = <expression>;`) from the body of the constructor.
* The init statements are removed from the body statements and returned as an array.
*/
_extractFieldInitializers(body) {
var statements = [];
var fieldInitializers = [];
body.statements.forEach(function(statement) {
var exp = statement.expression;
if (exp.type === BINARY_EXPRESSION &&
exp.operator.type === EQUAL &&
exp.left.type === MEMBER_EXPRESSION &&
exp.left.operand.type === THIS_EXPRESSION) {
// `this.field = exp` -> `field = exp`
// todo(vicb): check for `this.` on rhs, not allowed in Dart
// -> remove if possible (arguments), throw otherwise.
var fieldName = exp.left.memberName.value;
fieldInitializers.push(new BinaryExpression(
statement.location,
new IdentifierExpression(statement.location, fieldName),
EQUAL,
exp.right
));
} else {
statements.push(statement);
}
});
body.statements = statements;
return fieldInitializers;
}
/**
* Extract the super call (`super(<arg list>)`) from the body of the constructor.
* When found the super call statement is removed from the body statements and returned.
*/
_extractSuperCall(body) {
var statements = [];
var superCall = null;
body.statements.forEach(function (statement) {
if (statement.expression.type === CALL_EXPRESSION &&
statement.expression.operand.type === SUPER_EXPRESSION) {
superCall = statement.expression;
} else {
statements.push(statement);
}
});
body.statements = statements;
return superCall;
}
} }

View File

@ -1,6 +1,6 @@
import {INSTANCEOF} from 'traceur/src/syntax/TokenType'; import {INSTANCEOF} from 'traceur/src/syntax/TokenType';
import {ParseTreeTransformer} from 'traceur/src/codegeneration/ParseTreeTransformer'; import {ParseTreeTransformer} from './ParseTreeTransformer';
/** /**
* Transforms `a instanceof b` to `a is b`, * Transforms `a instanceof b` to `a is b`,

View File

@ -1,6 +1,6 @@
import {VariableStatement, VariableDeclarationList} from 'traceur/src/syntax/trees/ParseTrees'; import {VariableStatement, VariableDeclarationList} from 'traceur/src/syntax/trees/ParseTrees';
import {ParseTreeTransformer} from 'traceur/src/codegeneration/ParseTreeTransformer'; import {ParseTreeTransformer} from './ParseTreeTransformer';
/** /**
* Transforms `var a, b;` to `var a; var b;` * Transforms `var a, b;` to `var a; var b;`

View File

@ -0,0 +1,31 @@
import {
ParseTreeTransformer as TraceurParseTreeTransformer
} from 'traceur/src/codegeneration/ParseTreeTransformer';
import {
ClassFieldDeclaration,
PropertyConstructorAssignment
} from '../syntax/trees/ParseTrees'
export class ParseTreeTransformer extends TraceurParseTreeTransformer {
transformClassFieldDeclaration(tree) {
var lvalue = this.transformAny(tree.lvalue);
var typeAnnotation = this.transformAny(tree.typeAnnotation);
if (lvalue === tree.lvalue && typeAnnotation === tree.typeAnnotation) {
return tree;
}
return new ClassFieldDeclaration(tree.location, lvalue, typeAnnotation, initializer);
}
transformPropertyConstructorAssignment(tree) {
tree = super.transformPropertyMethodAssignment(tree);
var initializerList = this.transformList(tree.initializerList);
if (initializerList === tree.initializerList) {
return tree;
}
return new PropertyConstructorAssignment(tree.location, tree.isStatic, tree.functionKind,
tree.name, tree.parameterList, tree.typeAnnotation, tree.annotations, tree.body, tree.isConst,
initializerList);
}
}

View File

@ -1,14 +1,15 @@
import { import {
createArgumentList,
createCallExpression, createCallExpression,
createIdentifierExpression, createIdentifierExpression
createArgumentList} from 'traceur/src/codegeneration/ParseTreeFactory'; } from 'traceur/src/codegeneration/ParseTreeFactory';
import { import {
EQUAL_EQUAL_EQUAL, EQUAL_EQUAL_EQUAL,
NOT_EQUAL_EQUAL NOT_EQUAL_EQUAL
} from 'traceur/src/syntax/TokenType'; } from 'traceur/src/syntax/TokenType';
import {ParseTreeTransformer} from 'traceur/src/codegeneration/ParseTreeTransformer'; import {ParseTreeTransformer} from './ParseTreeTransformer';
/** /**
* Transforms: * Transforms:

View File

@ -1,6 +1,6 @@
import {Compiler as TraceurCompiler} from 'traceur/src/Compiler'; import {Compiler as TraceurCompiler} from 'traceur/src/Compiler';
import {DartTransformer} from './codegeneration/DartTransformer'; import {DartTransformer} from './codegeneration/DartTransformer';
import {DartTreeWriter} from './dart_writer'; import {DartParseTreeWriter} from './outputgeneration/DartParseTreeWriter';
import {CollectingErrorReporter} from 'traceur/src/util/CollectingErrorReporter'; import {CollectingErrorReporter} from 'traceur/src/util/CollectingErrorReporter';
import {Parser} from './parser'; import {Parser} from './parser';
import {SourceFile} from 'traceur/src/syntax/SourceFile'; import {SourceFile} from 'traceur/src/syntax/SourceFile';
@ -28,7 +28,7 @@ export class Compiler extends TraceurCompiler {
write(tree, outputName = undefined, sourceRoot = undefined) { write(tree, outputName = undefined, sourceRoot = undefined) {
if (this.options_.outputLanguage.toLowerCase() === 'dart') { if (this.options_.outputLanguage.toLowerCase() === 'dart') {
var writer = new DartTreeWriter(this.options_.moduleName, outputName); var writer = new DartParseTreeWriter(this.options_.moduleName, outputName);
writer.visitAny(tree); writer.visitAny(tree);
return writer.toString(); return writer.toString();
} else { } else {

View File

@ -1,9 +1,22 @@
import {CONSTRUCTOR, FROM} from 'traceur/src/syntax/PredefinedName'; import {CONSTRUCTOR, FROM} from 'traceur/src/syntax/PredefinedName';
import {EQUAL_EQUAL_EQUAL, OPEN_PAREN, CLOSE_PAREN, IMPORT, SEMI_COLON, STAR, OPEN_CURLY, CLOSE_CURLY, COMMA, AT, EQUAL, COLON} from 'traceur/src/syntax/TokenType'; import {
AT,
CLOSE_CURLY,
CLOSE_PAREN,
COLON,
COMMA,
EQUAL,
EQUAL_EQUAL_EQUAL,
IMPORT,
OPEN_CURLY,
OPEN_PAREN,
SEMI_COLON,
STAR
} from 'traceur/src/syntax/TokenType';
import {ParseTreeWriter as JavaScriptParseTreeWriter, ObjectLiteralExpression} from 'traceur/src/outputgeneration/ParseTreeWriter'; import {ParseTreeWriter as JavaScriptParseTreeWriter, ObjectLiteralExpression} from 'traceur/src/outputgeneration/ParseTreeWriter';
export class DartTreeWriter extends JavaScriptParseTreeWriter { export class DartParseTreeWriter extends JavaScriptParseTreeWriter {
constructor(moduleName, outputPath) { constructor(moduleName, outputPath) {
super(outputPath); super(outputPath);
this.libName = moduleName.replace(/\//g, '.'); this.libName = moduleName.replace(/\//g, '.');
@ -104,20 +117,42 @@ export class DartTreeWriter extends JavaScriptParseTreeWriter {
this.visitAny(tree.body); this.visitAny(tree.body);
} }
/**
* @param {PropertyMethodAssignment} tree
*/
visitPropertyConstructorAssignment(tree) {
this.writeAnnotations_(tree.annotations);
if (tree.isConst) {
this.write_('const');
this.writeSpace_();
}
this.writeType_(tree.typeAnnotation);
this.visitAny(tree.name);
this.write_(OPEN_PAREN);
this.visitAny(tree.parameterList);
this.write_(CLOSE_PAREN);
if (tree.initializerList.length > 0) {
this.write_(COLON);
this.writeSpace_();
this.writeList_(tree.initializerList, ', ');
}
if (tree.isConst) {
this.write_(SEMI_COLON);
} else {
this.writeSpace_();
this.visitAny(tree.body);
}
}
normalizeType_(typeName) { normalizeType_(typeName) {
if (typeName === 'number') { switch (typeName) {
return 'num'; case 'number': return 'num';
case 'boolean': return 'bool';
case 'string': return 'String';
default: return typeName;
} }
if (typeName === 'boolean') {
return 'bool';
}
if (typeName === 'string') {
return 'String';
}
return typeName;
} }
// FUNCTION/METHOD ARGUMENTS // FUNCTION/METHOD ARGUMENTS
@ -148,14 +183,23 @@ export class DartTreeWriter extends JavaScriptParseTreeWriter {
} }
} }
visitClassField(tree) { visitClassFieldDeclaration(tree) {
if (tree.isFinal) {
// `final <type> name;` or `final name;` for untyped variable
this.write_('final');
this.writeSpace_();
this.writeType_(tree.typeAnnotation); this.writeType_(tree.typeAnnotation);
} else {
if (!tree.typeAnnotation) { // `<type> name;` or `var name;`
if (tree.typeAnnotation) {
this.writeType_(tree.typeAnnotation);
} else {
this.write_('var'); this.write_('var');
this.writeSpace_();
}
} }
this.write_(tree.identifier); this.write_(tree.lvalue.getStringValue());
this.write_(SEMI_COLON); this.write_(SEMI_COLON);
} }
@ -180,6 +224,7 @@ export class DartTreeWriter extends JavaScriptParseTreeWriter {
visitExportDeclaration(tree) { visitExportDeclaration(tree) {
if (tree.declaration.moduleSpecifier) { if (tree.declaration.moduleSpecifier) {
this.write_('export'); this.write_('export');
this.writeSpace_();
this.visitModuleSpecifier(tree.declaration.moduleSpecifier); this.visitModuleSpecifier(tree.declaration.moduleSpecifier);
this.write_(SEMI_COLON); this.write_(SEMI_COLON);
} else { } else {

View File

@ -0,0 +1,2 @@
export var CLASS_FIELD_DECLARATION = 'CLASS_FIELD_DECLARATION';
export var PROPERTY_CONSTRUCTOR_ASSIGNMENT = 'PROPERTY_CONSTRUCTOR_ASSIGNMENT';

View File

@ -0,0 +1,80 @@
import {ParseTree} from 'traceur/src/syntax/trees/ParseTree';
import {PropertyMethodAssignment} from 'traceur/src/syntax/trees/ParseTrees';
import * as ParseTreeType from './ParseTreeType';
/**
* Property declaration
*/
export class ClassFieldDeclaration extends ParseTree {
constructor(location, lvalue, typeAnnotation, isFinal) {
this.location = location;
this.lvalue = lvalue;
this.typeAnnotation = typeAnnotation;
this.isFinal = isFinal;
}
get type() {
return CLASS_FIELD_DECLARATION;
}
visit(visitor) {
visitor.visitClassFieldDeclaration(this);
}
transform(transformer) {
return transformer.transformClassFieldDeclaration(this);
}
}
var CLASS_FIELD_DECLARATION = ParseTreeType.CLASS_FIELD_DECLARATION;
/**
* Class constructor
*/
export class PropertyConstructorAssignment extends PropertyMethodAssignment {
/**
* @param {SourceRange} location
* @param {boolean} isStatic
* @param {Token} functionKind
* @param {ParseTree} name
* @param {FormalParameterList} parameterList
* @param {ParseTree} typeAnnotation
* @param {Array.<ParseTree>} annotations
* @param {FunctionBody} body
* @param {boolean} isConst
* @param {ParseTree} initializerList
*/
constructor(location, isStatic, functionKind, name, parameterList, typeAnnotation, annotations,
body, isConst, initializerList) {
super(location, isStatic, functionKind, name, parameterList, typeAnnotation, annotations,
body);
this.isConst = isConst;
this.initializerList = initializerList;
}
/**
* @param {ParseTreeTransformer} transformer
*/
transform(transformer) {
return transformer.transformPropertyConstructorAssignment(this);
}
/**
* @param {ParseTreeVisitor} visitor
*/
visit(visitor) {
visitor.visitPropertyConstructorAssignment(this);
}
/**
* @type {ParseTreeType}
*/
get type() {
return PROPERTY_CONSTRUCTOR_ASSIGNMENT;
}
}
var PROPERTY_CONSTRUCTOR_ASSIGNMENT = ParseTreeType.PROPERTY_CONSTRUCTOR_ASSIGNMENT;