diff --git a/modules/change_detection/src/change_detection.js b/modules/change_detection/src/change_detection.js index 64f9b9f42a..636d17c4b2 100644 --- a/modules/change_detection/src/change_detection.js +++ b/modules/change_detection/src/change_detection.js @@ -12,7 +12,7 @@ export class ChangeDetection { } detectChanges():int { - var current:Record = _rootWatchGroup._headRecord; + var current:Record = _rootWatchGroup.headRecord; var count:number = 0; while (current != null) { if (current.check()) { diff --git a/modules/change_detection/src/watch_group.js b/modules/change_detection/src/watch_group.js index 7d59d00ae8..1159d5665e 100644 --- a/modules/change_detection/src/watch_group.js +++ b/modules/change_detection/src/watch_group.js @@ -2,11 +2,11 @@ import {ProtoRecord, Record} from './record'; import {FIELD} from 'facade/lang'; export class ProtoWatchGroup { - @FIELD('final _headRecord:ProtoRecord') - @FIELD('final _tailRecord:ProtoRecord') + @FIELD('final headRecord:ProtoRecord') + @FIELD('final tailRecord:ProtoRecord') constructor() { - this._headRecord = null; - this._tailRecord = null; + this.headRecord = null; + this.tailRecord = null; } /** @@ -29,7 +29,7 @@ export class ProtoWatchGroup { var watchGroup:WatchGroup = new WatchGroup(this, dispatcher); var head:Record = null; var tail:Record = null; - var proto:ProtoRecord = this._headRecord; + var proto:ProtoRecord = this.headRecord; while(proto != null) { tail = proto.instantiate(watchGroup); @@ -37,14 +37,14 @@ export class ProtoWatchGroup { proto = proto.next; } - proto = this._headRecord; + proto = this.headRecord; while(proto != null) { proto.instantiateComplete(); proto = proto.next; } - watchGroup._headRecord = head; - watchGroup._tailRecord = tail; + watchGroup.headRecord = head; + watchGroup.tailRecord = tail; return watchGroup; } @@ -53,13 +53,13 @@ export class ProtoWatchGroup { export class WatchGroup { @FIELD('final protoWatchGroup:ProtoWatchGroup') @FIELD('final dispatcher:WatchGroupDispatcher') - @FIELD('final _headRecord:Record') - @FIELD('final _tailRecord:Record') + @FIELD('final headRecord:Record') + @FIELD('final tailRecord:Record') constructor(protoWatchGroup:ProtoWatchGroup, dispatcher:WatchGroupDispatcher) { this.protoWatchGroup = protoWatchGroup; this.dispatcher = dispatcher; - this._headRecord = null; - this._tailRecord = null; + this.headRecord = null; + this.tailRecord = null; } insertChildGroup(newChild:WatchGroup, insertAfter:WatchGroup) { diff --git a/tools/transpiler/spec/classes_spec.js b/tools/transpiler/spec/classes_spec.js index 6953365cec..8690aa7bc5 100644 --- a/tools/transpiler/spec/classes_spec.js +++ b/tools/transpiler/spec/classes_spec.js @@ -1,4 +1,5 @@ import {describe, it, expect} from 'test_lib/test_lib'; +import {CONST} from './fixtures/annotations'; // Constructor // 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() { describe('classes', function() { it('should work', function() { @@ -22,5 +45,21 @@ export function main() { expect(foo.b).toBe(3); 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); + }); + }); }); + } diff --git a/tools/transpiler/spec/fixtures/annotations.dart b/tools/transpiler/spec/fixtures/annotations.dart index 87bb5fb810..e3837f21d6 100644 --- a/tools/transpiler/spec/fixtures/annotations.dart +++ b/tools/transpiler/spec/fixtures/annotations.dart @@ -11,6 +11,10 @@ class Provide { const Provide(this.token); } +class CONST { + const CONST(); +} + // TODO: this api does not yet return an array as we don't have // a nice array wrapper for Dart readFirstAnnotation(clazz) { diff --git a/tools/transpiler/spec/fixtures/annotations.es6 b/tools/transpiler/spec/fixtures/annotations.es6 index dfcc83aa18..e1780d5dbd 100644 --- a/tools/transpiler/spec/fixtures/annotations.es6 +++ b/tools/transpiler/spec/fixtures/annotations.es6 @@ -10,6 +10,8 @@ export class Provide { } } +export class CONST { +} // TODO: this api does not yet return an array as we don't have // a nice array wrapper for Dart diff --git a/tools/transpiler/src/ast/class_field.js b/tools/transpiler/src/ast/class_field.js deleted file mode 100644 index 12a15c7cf2..0000000000 --- a/tools/transpiler/src/ast/class_field.js +++ /dev/null @@ -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; - } -} diff --git a/tools/transpiler/src/codegeneration/ClassTransformer.js b/tools/transpiler/src/codegeneration/ClassTransformer.js index 9b872ebdb4..056ca57c50 100644 --- a/tools/transpiler/src/codegeneration/ClassTransformer.js +++ b/tools/transpiler/src/codegeneration/ClassTransformer.js @@ -1,25 +1,44 @@ -import {ParseTreeTransformer} from 'traceur/src/codegeneration/ParseTreeTransformer'; +import {ParseTreeTransformer} from './ParseTreeTransformer'; import { - PROPERTY_METHOD_ASSIGNMENT, + BINARY_EXPRESSION, + CALL_EXPRESSION, + IDENTIFIER_EXPRESSION, MEMBER_EXPRESSION, - THIS_EXPRESSION, - BINARY_EXPRESSION + PROPERTY_METHOD_ASSIGNMENT, + SUPER_EXPRESSION, + THIS_EXPRESSION } from 'traceur/src/syntax/trees/ParseTreeType'; +import {EQUAL} from 'traceur/src/syntax/TokenType'; + import {CONSTRUCTOR} from 'traceur/src/syntax/PredefinedName'; 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: - * - 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 { + constructor(idGenerator, reporter) { + this.reporter_ = reporter; + } + /** * @param {ClassDeclaration} tree * @returns {ParseTree} @@ -28,12 +47,17 @@ export class ClassTransformer extends ParseTreeTransformer { var className = tree.name.identifierToken.toString(); var argumentTypesMap = {}; var fields = []; + var isConst; + var that = this; - tree.elements.forEach(function(elementTree) { + tree.elements.forEach(function(elementTree, index) { if (elementTree.type === PROPERTY_METHOD_ASSIGNMENT && !elementTree.isStatic && propName(elementTree) === CONSTRUCTOR) { + isConst = elementTree.annotations.some((annotation) => + annotation.name.identifierToken.value === 'CONST'); + // Store constructor argument types, // so that we can use them to set the types of simple-assigned fields. elementTree.parameterList.parameters.forEach(function(p) { @@ -48,15 +72,54 @@ export class ClassTransformer extends ParseTreeTransformer { // Collect all fields, defined in the constructor. elementTree.body.statements.forEach(function(statement) { - if (statement.expression.type === BINARY_EXPRESSION && - statement.expression.operator.type === '=' && - statement.expression.left.type === MEMBER_EXPRESSION && - statement.expression.left.operand.type === THIS_EXPRESSION) { + var exp = statement.expression; + if (exp.type === BINARY_EXPRESSION && + exp.operator.type === EQUAL && + exp.left.type === MEMBER_EXPRESSION && + exp.left.operand.type === THIS_EXPRESSION) { - var typeAnnotation = argumentTypesMap[statement.expression.left.memberName.value] || null; - fields.push(new ClassFieldParseTree(tree.location, statement.expression.left.memberName, typeAnnotation)); + var 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); return super(tree); - }; + } + + /** + * Extract field initialization (`this.field = ;`) 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()`) 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; + } } diff --git a/tools/transpiler/src/codegeneration/InstanceOfTransformer.js b/tools/transpiler/src/codegeneration/InstanceOfTransformer.js index ea22d2a2c4..b5d0204aa0 100644 --- a/tools/transpiler/src/codegeneration/InstanceOfTransformer.js +++ b/tools/transpiler/src/codegeneration/InstanceOfTransformer.js @@ -1,6 +1,6 @@ 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`, diff --git a/tools/transpiler/src/codegeneration/MultiVarTransformer.js b/tools/transpiler/src/codegeneration/MultiVarTransformer.js index b05f0106e4..059a54162e 100644 --- a/tools/transpiler/src/codegeneration/MultiVarTransformer.js +++ b/tools/transpiler/src/codegeneration/MultiVarTransformer.js @@ -1,6 +1,6 @@ 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;` diff --git a/tools/transpiler/src/codegeneration/ParseTreeTransformer.js b/tools/transpiler/src/codegeneration/ParseTreeTransformer.js new file mode 100644 index 0000000000..449de13c6b --- /dev/null +++ b/tools/transpiler/src/codegeneration/ParseTreeTransformer.js @@ -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); + } +} \ No newline at end of file diff --git a/tools/transpiler/src/codegeneration/StrictEqualityTransformer.js b/tools/transpiler/src/codegeneration/StrictEqualityTransformer.js index 622842a76f..07ec213448 100644 --- a/tools/transpiler/src/codegeneration/StrictEqualityTransformer.js +++ b/tools/transpiler/src/codegeneration/StrictEqualityTransformer.js @@ -1,14 +1,15 @@ import { + createArgumentList, createCallExpression, - createIdentifierExpression, - createArgumentList} from 'traceur/src/codegeneration/ParseTreeFactory'; + createIdentifierExpression +} from 'traceur/src/codegeneration/ParseTreeFactory'; import { EQUAL_EQUAL_EQUAL, NOT_EQUAL_EQUAL } from 'traceur/src/syntax/TokenType'; -import {ParseTreeTransformer} from 'traceur/src/codegeneration/ParseTreeTransformer'; +import {ParseTreeTransformer} from './ParseTreeTransformer'; /** * Transforms: diff --git a/tools/transpiler/src/compiler.js b/tools/transpiler/src/compiler.js index e3b277421e..181a82a94c 100644 --- a/tools/transpiler/src/compiler.js +++ b/tools/transpiler/src/compiler.js @@ -1,6 +1,6 @@ import {Compiler as TraceurCompiler} from 'traceur/src/Compiler'; import {DartTransformer} from './codegeneration/DartTransformer'; -import {DartTreeWriter} from './dart_writer'; +import {DartParseTreeWriter} from './outputgeneration/DartParseTreeWriter'; import {CollectingErrorReporter} from 'traceur/src/util/CollectingErrorReporter'; import {Parser} from './parser'; import {SourceFile} from 'traceur/src/syntax/SourceFile'; @@ -28,7 +28,7 @@ export class Compiler extends TraceurCompiler { write(tree, outputName = undefined, sourceRoot = undefined) { 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); return writer.toString(); } else { diff --git a/tools/transpiler/src/dart_writer.js b/tools/transpiler/src/outputgeneration/DartParseTreeWriter.js similarity index 81% rename from tools/transpiler/src/dart_writer.js rename to tools/transpiler/src/outputgeneration/DartParseTreeWriter.js index 213808b97b..e967fdbaee 100644 --- a/tools/transpiler/src/dart_writer.js +++ b/tools/transpiler/src/outputgeneration/DartParseTreeWriter.js @@ -1,9 +1,22 @@ 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'; -export class DartTreeWriter extends JavaScriptParseTreeWriter { +export class DartParseTreeWriter extends JavaScriptParseTreeWriter { constructor(moduleName, outputPath) { super(outputPath); this.libName = moduleName.replace(/\//g, '.'); @@ -104,20 +117,42 @@ export class DartTreeWriter extends JavaScriptParseTreeWriter { 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) { - if (typeName === 'number') { - return 'num'; + switch (typeName) { + 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 @@ -148,14 +183,23 @@ export class DartTreeWriter extends JavaScriptParseTreeWriter { } } - visitClassField(tree) { - this.writeType_(tree.typeAnnotation); - - if (!tree.typeAnnotation) { - this.write_('var '); + visitClassFieldDeclaration(tree) { + if (tree.isFinal) { + // `final name;` or `final name;` for untyped variable + this.write_('final'); + this.writeSpace_(); + this.writeType_(tree.typeAnnotation); + } else { + // ` name;` or `var name;` + if (tree.typeAnnotation) { + this.writeType_(tree.typeAnnotation); + } else { + this.write_('var'); + this.writeSpace_(); + } } - this.write_(tree.identifier); + this.write_(tree.lvalue.getStringValue()); this.write_(SEMI_COLON); } @@ -179,7 +223,8 @@ export class DartTreeWriter extends JavaScriptParseTreeWriter { // EXPORTS visitExportDeclaration(tree) { if (tree.declaration.moduleSpecifier) { - this.write_('export '); + this.write_('export'); + this.writeSpace_(); this.visitModuleSpecifier(tree.declaration.moduleSpecifier); this.write_(SEMI_COLON); } else { diff --git a/tools/transpiler/src/syntax/trees/ParseTreeType.js b/tools/transpiler/src/syntax/trees/ParseTreeType.js new file mode 100644 index 0000000000..c0eaaf890c --- /dev/null +++ b/tools/transpiler/src/syntax/trees/ParseTreeType.js @@ -0,0 +1,2 @@ +export var CLASS_FIELD_DECLARATION = 'CLASS_FIELD_DECLARATION'; +export var PROPERTY_CONSTRUCTOR_ASSIGNMENT = 'PROPERTY_CONSTRUCTOR_ASSIGNMENT'; \ No newline at end of file diff --git a/tools/transpiler/src/syntax/trees/ParseTrees.js b/tools/transpiler/src/syntax/trees/ParseTrees.js new file mode 100644 index 0000000000..c3b5a52f23 --- /dev/null +++ b/tools/transpiler/src/syntax/trees/ParseTrees.js @@ -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.} 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; +