diff --git a/modules/change_detection/src/parser/ast.js b/modules/change_detection/src/parser/ast.js index 8014c494d7..6d40cee9fb 100644 --- a/modules/change_detection/src/parser/ast.js +++ b/modules/change_detection/src/parser/ast.js @@ -6,6 +6,10 @@ export class AST { throw new BaseException("Not supported"); } + assign(context, value) { + throw new BaseException("Not supported"); + } + visit(visitor) { } } @@ -20,6 +24,22 @@ export class ImplicitReceiver extends AST { } } +export class Chain extends AST { + constructor(expressions:List) { + this.expressions = expressions; + } + + eval(context) { + var result; + for (var i = 0; i < this.expressions.length; i++) { + var last = this.expressions[i].eval(context); + if (last != null) result = last; + } + return result; + } +} + + export class Conditional extends AST { @FIELD('final condition:AST') @FIELD('final trueExp:AST') @@ -43,21 +63,34 @@ export class FieldRead extends AST { @FIELD('final receiver:AST') @FIELD('final name:string') @FIELD('final getter:Function') - constructor(receiver:AST, name:string, getter:Function) { + @FIELD('final setter:Function') + constructor(receiver:AST, name:string, getter:Function, setter:Function) { this.receiver = receiver; this.name = name; this.getter = getter; + this.setter = setter; } eval(context) { return this.getter(this.receiver.eval(context)); } + assign(context, value) { + return this.setter(this.receiver.eval(context), value); + } + visit(visitor) { visitor.visitFieldRead(this); } } +export class KeyedAccess extends AST { + constructor(obj:AST, key:AST) { + this.obj = obj; + this.key = key; + } +} + export class Formatter extends AST { @FIELD('final exp:AST') @FIELD('final name:string') @@ -147,6 +180,20 @@ export class PrefixNot extends AST { } } +export class Assignment extends AST { + @FIELD('final target:AST') + @FIELD('final value:AST') + constructor(target:AST, value:AST) { + this.target = target; + this.value = value; + } + visit(visitor) { visitor.visitAssignment(this); } + + eval(context) { + return this.target.assign(context, this.value.eval(context)); + } +} + //INTERFACE export class AstVisitor { visitImplicitReceiver(ast:ImplicitReceiver) {} @@ -155,6 +202,7 @@ export class AstVisitor { visitPrefixNot(ast:PrefixNot) {} visitLiteralPrimitive(ast:LiteralPrimitive) {} visitFormatter(ast:Formatter) {} + visitAssignment(ast:Assignment) {} } var _evalListCache = [[],[0],[0,0],[0,0,0],[0,0,0,0],[0,0,0,0,0]]; diff --git a/modules/change_detection/src/parser/closure_map.dart b/modules/change_detection/src/parser/closure_map.dart index c38921b5be..c1a9800cb3 100644 --- a/modules/change_detection/src/parser/closure_map.dart +++ b/modules/change_detection/src/parser/closure_map.dart @@ -7,4 +7,9 @@ class ClosureMap { var symbol = new Symbol(name); return (receiver) => reflect(receiver).getField(symbol).reflectee; } + + Function setter(String name) { + var symbol = new Symbol(name); + return (receiver, value) => reflect(receiver).setField(symbol, value).reflectee; + } } diff --git a/modules/change_detection/src/parser/closure_map.es6 b/modules/change_detection/src/parser/closure_map.es6 index fe93aa5254..2babeb64d8 100644 --- a/modules/change_detection/src/parser/closure_map.es6 +++ b/modules/change_detection/src/parser/closure_map.es6 @@ -4,4 +4,8 @@ export class ClosureMap { getter(name:string) { return new Function('o', 'return o.' + name + ';'); } + + setter(name:string) { + return new Function('o', 'v', 'return o.' + name + ' = v;'); + } } diff --git a/modules/change_detection/src/parser/parser.js b/modules/change_detection/src/parser/parser.js index 5ee8e4eb6c..41922cd377 100644 --- a/modules/change_detection/src/parser/parser.js +++ b/modules/change_detection/src/parser/parser.js @@ -1,6 +1,6 @@ -import {FIELD, int, isBlank} from 'facade/lang'; +import {FIELD, int, isBlank, BaseException} from 'facade/lang'; import {ListWrapper, List} from 'facade/collection'; -import {Lexer, EOF, Token, $PERIOD, $COLON, $SEMICOLON} from './lexer'; +import {Lexer, EOF, Token, $PERIOD, $COLON, $SEMICOLON, $LBRACKET, $RBRACKET} from './lexer'; import {ClosureMap} from './closure_map'; import { AST, @@ -11,7 +11,10 @@ import { Binary, PrefixNot, Conditional, - Formatter + Formatter, + Assignment, + Chain, + KeyedAccess } from './ast'; var _implicitReceiver = new ImplicitReceiver(); @@ -75,6 +78,12 @@ class _ParseAST { } } + expectCharacter(code:int) { + if (this.optionalCharacter(code)) return; + this.error(`Missing expected ${code}`); + } + + optionalOperator(op:string):boolean { if (this.next.isOperator(op)) { this.advance(); @@ -84,6 +93,20 @@ class _ParseAST { } } + expectOperator(operator:string) { + if (this.optionalOperator(operator)) return; + this.error(`Missing expected operator ${operator}`); + } + + expectIdentifierOrKeyword():string { + var n = this.next; + if (!n.isIdentifier() && !n.isKeyword()) { + this.error(`Unexpected token ${n}, expected identifier or keyword`) + } + this.advance(); + return n.toString(); + } + parseChain():AST { var exprs = []; while (this.index < this.tokens.length) { @@ -96,7 +119,7 @@ class _ParseAST { } } } - return ListWrapper.first(exprs); + return exprs.length == 1 ? exprs[0] : new Chain(exprs); } parseFormatter() { @@ -105,7 +128,7 @@ class _ParseAST { if (this.parseAction) { this.error("Cannot have a formatter in an action expression"); } - var name = this.parseIdentifier(); + var name = this.expectIdentifierOrKeyword(); var args = ListWrapper.create(); while (this.optionalCharacter($COLON)) { ListWrapper.push(args, this.parseExpression()); @@ -116,7 +139,19 @@ class _ParseAST { } parseExpression() { - return this.parseConditional(); + var result = this.parseConditional(); + + while (this.next.isOperator('=')) { + //if (!backend.isAssignable(result)) { + // int end = (index < tokens.length) ? next.index : input.length; + // String expression = input.substring(start, end); + // error('Expression $expression is not assignable'); + // } + this.expectOperator('='); + result = new Assignment(result, this.parseConditional()); + } + + return result; } parseConditional() { @@ -202,7 +237,7 @@ class _ParseAST { } parseMultiplicative() { - // '*', '%', '/', '~/' + // '*', '%', '/' var result = this.parsePrefix(); while (true) { if (this.optionalOperator('*')) { @@ -211,9 +246,6 @@ class _ParseAST { result = new Binary('%', result, this.parsePrefix()); } else if (this.optionalOperator('/')) { result = new Binary('/', result, this.parsePrefix()); - // TODO(rado): This exists only in Dart, figure out whether to support it. - // } else if (this.optionalOperator('~/')) { - // result = new BinaryTruncatingDivide(result, this.parsePrefix()); } else { return result; } @@ -232,59 +264,54 @@ class _ParseAST { } } - parseAccessOrCallMember() { + parseAccessOrCallMember():AST { var result = this.parsePrimary(); - // TODO: add missing cases. - return result; + while (true) { + if (this.optionalCharacter($PERIOD)) { + result = this.parseFieldRead(result); + } else { + return result; + } + } } parsePrimary() { if (this.next.isKeywordNull() || this.next.isKeywordUndefined()) { this.advance(); return new LiteralPrimitive(null); + } else if (this.next.isKeywordTrue()) { this.advance(); return new LiteralPrimitive(true); + } else if (this.next.isKeywordFalse()) { this.advance(); return new LiteralPrimitive(false); + } else if (this.next.isIdentifier()) { - return this.parseAccess(); + return this.parseFieldRead(_implicitReceiver); + } else if (this.next.isNumber()) { var value = this.next.toNumber(); this.advance(); return new LiteralPrimitive(value); + } else if (this.next.isString()) { var value = this.next.toString(); this.advance(); return new LiteralPrimitive(value); - } else if (this.index >= this.tokens.length) { - throw `Unexpected end of expression: ${this.input}`; - } else { - throw `Unexpected token ${this.next}`; - } - } - parseAccess():AST { - var result = this.parseFieldRead(_implicitReceiver); - while(this.optionalCharacter($PERIOD)) { - result = this.parseFieldRead(result); + } else if (this.index >= this.tokens.length) { + this.error(`Unexpected end of expression: ${this.input}`); + + } else { + this.error(`Unexpected token ${this.next}`); } - return result; } parseFieldRead(receiver):AST { - var id = this.parseIdentifier(); - return new FieldRead(receiver, id, this.closureMap.getter(id)); - } - - parseIdentifier():string { - var n = this.next; - if (!n.isIdentifier() && !n.isKeyword()) { - this.error(`Unexpected token ${n}, expected identifier or keyword`) - } - this.advance(); - return n.toString(); + var id = this.expectIdentifierOrKeyword(); + return new FieldRead(receiver, id, this.closureMap.getter(id), this.closureMap.setter(id)); } error(message:string, index:int = null) { @@ -294,16 +321,6 @@ class _ParseAST { ? `at column ${this.tokens[index].index + 1} in` : `at the end of the expression`; - throw new ParserError(`Parser Error: ${message} ${location} [${this.input}]`); - } -} - -class ParserError extends Error { - constructor(message) { - this.message = message; - } - - toString() { - return this.message; + throw new BaseException(`Parser Error: ${message} ${location} [${this.input}]`); } } diff --git a/modules/change_detection/test/change_detector_spec.js b/modules/change_detection/test/change_detector_spec.js index d9d679cc6b..85a12a3d49 100644 --- a/modules/change_detection/test/change_detector_spec.js +++ b/modules/change_detection/test/change_detector_spec.js @@ -19,7 +19,7 @@ export function main() { var parts = exp.split("."); var cm = new ClosureMap(); return ListWrapper.reduce(parts, function (ast, fieldName) { - return new FieldRead(ast, fieldName, cm.getter(fieldName)); + return new FieldRead(ast, fieldName, cm.getter(fieldName), cm.setter(fieldName)); }, new ImplicitReceiver()); } diff --git a/modules/change_detection/test/parser/parser_spec.js b/modules/change_detection/test/parser/parser_spec.js index c7273f1f58..16f22615cb 100644 --- a/modules/change_detection/test/parser/parser_spec.js +++ b/modules/change_detection/test/parser/parser_spec.js @@ -150,25 +150,43 @@ export function main() { createParser().parseAction('boo').eval(new ContextWithErrors()); }).toThrowError('boo to you'); }); - }); - describe("parseBinding", () => { - it("should parse formatters", function () { - var exp = parseBinding("'Foo'|uppercase"); - expect(exp).toBeAnInstanceOf(Formatter); - expect(exp.name).toEqual("uppercase"); + it('should evaluate assignments', () => { + var context = td(); + expectEval("a=12", context).toEqual(12); + expect(context.a).toEqual(12); + + context = td(td(td())); + expectEval("a.a.a=123;", context).toEqual(123); + expect(context.a.a.a).toEqual(123); + + context = td(); + expectEval("a=123; b=234", context).toEqual(234); + expect(context.a).toEqual(123); + expect(context.b).toEqual(234); }); - it("should parse formatters with args", function () { - var exp = parseBinding("1|increment:2"); - expect(exp).toBeAnInstanceOf(Formatter); - expect(exp.name).toEqual("increment"); - expect(exp.args[0]).toBeAnInstanceOf(LiteralPrimitive); - }); + describe("parseBinding", () => { + //throw on assignment - it('should throw on chain expressions', () => { - expect(() => parseBinding("1;2")).toThrowError(new RegExp("contain chained expression")); + it("should parse formatters", function () { + var exp = parseBinding("'Foo'|uppercase"); + expect(exp).toBeAnInstanceOf(Formatter); + expect(exp.name).toEqual("uppercase"); + }); + + it("should parse formatters with args", function () { + var exp = parseBinding("1|increment:2"); + expect(exp).toBeAnInstanceOf(Formatter); + expect(exp.name).toEqual("increment"); + expect(exp.args[0]).toBeAnInstanceOf(LiteralPrimitive); + }); + + it('should throw on chain expressions', () => { + expect(() => parseBinding("1;2")).toThrowError(new RegExp("contain chained expression")); + }); }); }); }); } + diff --git a/modules/core/test/compiler/view_spec.js b/modules/core/test/compiler/view_spec.js index 75dadfece9..55b368b55e 100644 --- a/modules/core/test/compiler/view_spec.js +++ b/modules/core/test/compiler/view_spec.js @@ -18,9 +18,11 @@ class Directive { } export function main() { - var oneFieldAst = (fieldName) => - new FieldRead(new ImplicitReceiver(), fieldName, - (new ClosureMap()).getter(fieldName)); + var oneFieldAst = (fieldName) => { + var cm = new ClosureMap(); + return new FieldRead(new ImplicitReceiver(), fieldName, + cm.getter(fieldName), cm.setter(fieldName)); + }; describe('view', function() { var tempalteWithThreeTypesOfBindings =