diff --git a/modules/change_detection/src/facade.dart b/modules/change_detection/src/facade.dart deleted file mode 100644 index 25d71bc945..0000000000 --- a/modules/change_detection/src/facade.dart +++ /dev/null @@ -1,14 +0,0 @@ -library change_detection.facade; - -@MirrorsUsed(targets: const [FieldGetterFactory], metaTargets: const [] ) -import 'dart:mirrors'; - -typedef SetterFn(Object obj, value); - -class FieldGetterFactory { - getter(Object object, String name) { - Symbol symbol = new Symbol(name); - InstanceMirror instanceMirror = reflect(object); - return (Object object) => instanceMirror.getField(symbol).reflectee; - } -} diff --git a/modules/change_detection/src/facade.es6 b/modules/change_detection/src/facade.es6 deleted file mode 100644 index 30add7e745..0000000000 --- a/modules/change_detection/src/facade.es6 +++ /dev/null @@ -1,7 +0,0 @@ -export var SetterFn = Function; - -export class FieldGetterFactory { - getter(object, name:string) { - return new Function('o', 'return o["' + name + '"]'); - } -} diff --git a/modules/change_detection/src/parser/ast.js b/modules/change_detection/src/parser/ast.js index eaa3428683..cad3fee265 100644 --- a/modules/change_detection/src/parser/ast.js +++ b/modules/change_detection/src/parser/ast.js @@ -1,5 +1,7 @@ +import {FIELD, toBool, autoConvertAdd} from "facade/lang"; + export class AST { - eval(context) { + eval(context, formatters) { } visit(visitor) { @@ -7,7 +9,7 @@ export class AST { } export class ImplicitReceiver extends AST { - eval(context) { + eval(context, formatters) { return context; } @@ -16,15 +18,22 @@ export class ImplicitReceiver extends AST { } } -export class FieldRead extends AST { +export class Expression extends AST { + constructor() { + this.isAssignable = false; + this.isChain = false; + } +} + +export class FieldRead extends Expression { constructor(receiver:AST, name:string, getter:Function) { this.receiver = receiver; this.name = name; this.getter = getter; } - eval(context) { - return this.getter(this.receiver.eval(context)); + eval(context, formatters) { + return this.getter(this.receiver.eval(context, formatters)); } visit(visitor) { @@ -32,8 +41,94 @@ export class FieldRead extends AST { } } +export class LiteralPrimitive extends Expression { + @FIELD('final value') + constructor(value) { + this.value = value; + } + eval(context, formatters) { + return this.value; + } + visit(visitor) { + visitor.visitLiteralPrimitive(this); + } +} + +export class Binary extends Expression { + @FIELD('final operation:string') + @FIELD('final left:Expression') + @FIELD('final right:Expression') + constructor(operation:string, left:Expression, right:Expression) { + this.operation = operation; + this.left = left; + this.right = right; + } + + visit(visitor) { + visitor.visitBinary(this); + } + + eval(context, formatters) { + var left = this.left.eval(context, formatters); + switch (this.operation) { + case '&&': return toBool(left) && toBool(this.right.eval(context, formatters)); + case '||': return toBool(left) || toBool(this.right.eval(context, formatters)); + } + var right = this.right.eval(context, formatters); + + // Null check for the operations. + if (left == null || right == null) { + switch (this.operation) { + case '+': + if (left != null) return left; + if (right != null) return right; + return 0; + case '-': + if (left != null) return left; + if (right != null) return 0 - right; + return 0; + } + return null; + } + + switch (this.operation) { + case '+' : return autoConvertAdd(left, right); + case '-' : return left - right; + case '*' : return left * right; + case '/' : return left / right; + // This exists only in Dart, TODO(rado) figure out whether to support it. + // case '~/' : return left ~/ right; + case '%' : return left % right; + case '==' : return left == right; + case '!=' : return left != right; + case '<' : return left < right; + case '>' : return left > right; + case '<=' : return left <= right; + case '>=' : return left >= right; + case '^' : return left ^ right; + case '&' : return left & right; + } + throw 'Internal error [$operation] not handled'; + } +} + +export class PrefixNot extends Expression { + @FIELD('final operation:string') + @FIELD('final expression:Expression') + constructor(expression:Expression) { + this.expression = expression; + } + visit(visitor) { visitor.visitPrefixNot(this); } + eval(context, formatters) { + return !toBool(this.expression.eval(context, formatters)); + } +} + //INTERFACE export class AstVisitor { visitImplicitReceiver(ast:ImplicitReceiver) {} visitFieldRead(ast:FieldRead) {} -} \ No newline at end of file + visitBinary(ast:Binary) {} + visitPrefixNot(ast:PrefixNot) {} + visitLiteralPrimitive(ast:LiteralPrimitive) {} +} diff --git a/modules/change_detection/src/parser/closure_map.dart b/modules/change_detection/src/parser/closure_map.dart index 8dc5b50179..c38921b5be 100644 --- a/modules/change_detection/src/parser/closure_map.dart +++ b/modules/change_detection/src/parser/closure_map.dart @@ -1,8 +1,10 @@ import 'dart:mirrors'; +typedef SetterFn(Object obj, value); + class ClosureMap { Function getter(String name) { var symbol = new Symbol(name); return (receiver) => reflect(receiver).getField(symbol).reflectee; } -} \ No newline at end of file +} diff --git a/modules/change_detection/src/parser/closure_map.es6 b/modules/change_detection/src/parser/closure_map.es6 index a7635f06e1..fe93aa5254 100644 --- a/modules/change_detection/src/parser/closure_map.es6 +++ b/modules/change_detection/src/parser/closure_map.es6 @@ -1,5 +1,7 @@ +export var SetterFn = Function; + export class ClosureMap { getter(name:string) { return new Function('o', 'return o.' + name + ';'); } -} \ No newline at end of file +} diff --git a/modules/change_detection/src/parser/parser.js b/modules/change_detection/src/parser/parser.js index 8ae97bc21c..e225c839fd 100644 --- a/modules/change_detection/src/parser/parser.js +++ b/modules/change_detection/src/parser/parser.js @@ -2,7 +2,8 @@ import {FIELD, int} from 'facade/lang'; import {ListWrapper, List} from 'facade/collection'; import {Lexer, EOF, Token, $PERIOD} from './lexer'; import {ClosureMap} from './closure_map'; -import {AST, ImplicitReceiver, FieldRead} from './ast'; +import {AST, ImplicitReceiver, FieldRead, LiteralPrimitive, Expression, + Binary, PrefixNot } from './ast'; var _implicitReceiver = new ImplicitReceiver(); @@ -52,10 +53,115 @@ class _ParseAST { } } + optionalOperator(op:string):boolean { + if (this.next.isOperator(op)) { + this.advance(); + return true; + } else { + return false; + } + } + + parseLogicalOr() { + // '||' + var result = this.parseLogicalAnd(); + while (this.optionalOperator('||')) { + result = new Binary('||', result, this.parseLogicalAnd()); + } + return result; + } + + parseLogicalAnd() { + // '&&' + var result = this.parseEquality(); + while (this.optionalOperator('&&')) { + result = new Binary('&&', result, this.parseEquality()); + } + return result; + } + + parseEquality() { + // '==','!=' + var result = this.parseRelational(); + while (true) { + if (this.optionalOperator('==')) { + result = new Binary('==', result, this.parseRelational()); + } else if (this.optionalOperator('!=')) { + result = new Binary('!=', result, this.parseRelational()); + } else { + return result; + } + } + } + + parseRelational() { + // '<', '>', '<=', '>=' + var result = this.parseAdditive(); + while (true) { + if (this.optionalOperator('<')) { + result = new Binary('<', result, this.parseAdditive()); + } else if (this.optionalOperator('>')) { + result = new Binary('>', result, this.parseAdditive()); + } else if (this.optionalOperator('<=')) { + result = new Binary('<=', result, this.parseAdditive()); + } else if (this.optionalOperator('>=')) { + result = new Binary('>=', result, this.parseAdditive()); + } else { + return result; + } + } + } + + parseAdditive() { + // '+', '-' + var result = this.parseMultiplicative(); + while (true) { + if (this.optionalOperator('+')) { + result = new Binary('+', result, this.parseMultiplicative()); + } else if (this.optionalOperator('-')) { + result = new Binary('-', result, this.parseMultiplicative()); + } else { + return result; + } + } + } + + parseMultiplicative() { + // '*', '%', '/', '~/' + var result = this.parsePrefix(); + while (true) { + if (this.optionalOperator('*')) { + result = new Binary('*', result, this.parsePrefix()); + } else if (this.optionalOperator('%')) { + 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; + } + } + } + + parsePrefix() { + if (this.optionalOperator('+')) { + return this.parsePrefix(); + } else if (this.optionalOperator('-')) { + return new Binary('-', new LiteralPrimitive(0), this.parsePrefix()); + } else if (this.optionalOperator('!')) { + return new PrefixNot(this.parsePrefix()); + } else { + return this.parseAccessOrCallMember(); + } + } + + parseChain():AST { var exprs = []; while (this.index < this.tokens.length) { - ListWrapper.push(exprs, this.parseAccess()); + ListWrapper.push(exprs, this.parseLogicalOr()); } return ListWrapper.first(exprs); } @@ -68,6 +174,42 @@ class _ParseAST { return result; } + parseAccessOrCallMember() { + var result = this.parsePrimary(); + // TODO: add missing cases. + return result; + } + + parsePrimary() { + var value; + // TODO: add missing cases. + + 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(); + } else if (this.next.isNumber()) { + value = this.next.toNumber(); + this.advance(); + return new LiteralPrimitive(value); + } else if (this.next.isString()) { + 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}`; + } + } + parseFieldRead(receiver):AST { var id = this.parseIdentifier(); return new FieldRead(receiver, id, this.closureMap.getter(id)); @@ -78,4 +220,4 @@ class _ParseAST { this.advance(); return n.toString(); } -} \ No newline at end of file +} diff --git a/modules/change_detection/src/record.js b/modules/change_detection/src/record.js index e490cd7d57..28c827c501 100644 --- a/modules/change_detection/src/record.js +++ b/modules/change_detection/src/record.js @@ -1,6 +1,6 @@ import {ProtoWatchGroup, WatchGroup} from './watch_group'; import {FIELD} from 'facade/lang'; -import {FieldGetterFactory} from './facade'; +import {ClosureMap} from 'change_detection/parser/closure_map'; /** * For now we are dropping expression coalescence. We can always add it later, but @@ -134,8 +134,8 @@ export class Record { setContext(context) { this.mode = MODE_STATE_PROPERTY; this.context = context; - var factory = new FieldGetterFactory(); - this.getter = factory.getter(context, this.protoRecord.fieldName); + var closureMap = new ClosureMap(); + this.getter = closureMap.getter(this.protoRecord.fieldName); } } diff --git a/modules/change_detection/test/parser/parser_spec.js b/modules/change_detection/test/parser/parser_spec.js index 9e0dbe85d9..466632f192 100644 --- a/modules/change_detection/test/parser/parser_spec.js +++ b/modules/change_detection/test/parser/parser_spec.js @@ -1,17 +1,26 @@ -import {ddescribe, describe, it, expect, beforeEach} from 'test_lib/test_lib'; +import {ddescribe, describe, it, iit, expect, beforeEach} from 'test_lib/test_lib'; import {Parser} from 'change_detection/parser/parser'; import {Lexer} from 'change_detection/parser/lexer'; import {ClosureMap} from 'change_detection/parser/closure_map'; class TestData { - constructor(a) { + constructor(a, b) { this.a = a; + this.b = b; } } export function main() { - function td({a}) { - return new TestData(a); + function td(a = 0, b = 0) { + return new TestData(a, b); + } + + var context = td(); + var formatters; + + function _eval(text) { + return new Parser(new Lexer(), new ClosureMap()).parse(text) + .eval(context, formatters); } describe("parser", () => { @@ -24,15 +33,79 @@ export function main() { it("should parse field access",() => { var exp = parser.parse("a"); - var context = td({a: 999}); - expect(exp.eval(context)).toEqual(999); + var context = td(999); + expect(exp.eval(context, null)).toEqual(999); }); it("should parse nested field access",() => { var exp = parser.parse("a.a"); - var context = td({a: td({a: 999})}); - expect(exp.eval(context)).toEqual(999); + var context = td(td(999)); + expect(exp.eval(context, null)).toEqual(999); + }); + }); + + describe('expressions', () => { + + it('should parse numerical expressions', () => { + expect(_eval("1")).toEqual(1); + }); + + + it('should parse unary - expressions', () => { + expect(_eval("-1")).toEqual(-1); + expect(_eval("+1")).toEqual(1); + }); + + + it('should parse unary ! expressions', () => { + expect(_eval("!true")).toEqual(!true); + }); + + + it('should parse multiplicative expressions', () => { + expect(_eval("3*4/2%5")).toEqual(3*4/2%5); + // TODO(rado): This exists only in Dart, figure out whether to support it. + // expect(_eval("3*4~/2%5")).toEqual(3*4~/2%5); + }); + + + it('should parse additive expressions', () => { + expect(_eval("3+6-2")).toEqual(3+6-2); + }); + + + it('should parse relational expressions', () => { + expect(_eval("2<3")).toEqual(2<3); + expect(_eval("2>3")).toEqual(2>3); + expect(_eval("2<=2")).toEqual(2<=2); + expect(_eval("2>=2")).toEqual(2>=2); + }); + + + it('should parse equality expressions', () => { + expect(_eval("2==3")).toEqual(2==3); + expect(_eval("2!=3")).toEqual(2!=3); + }); + + + it('should parse logicalAND expressions', () => { + expect(_eval("true&&true")).toEqual(true&&true); + expect(_eval("true&&false")).toEqual(true&&false); + }); + + + it('should parse logicalOR expressions', () => { + expect(_eval("false||true")).toEqual(false||true); + expect(_eval("false||false")).toEqual(false||false); + }); + + it('should auto convert ints to strings', () => { + expect(_eval("'str ' + 4")).toEqual("str 4"); + expect(_eval("4 + ' str'")).toEqual("4 str"); + expect(_eval("4 + 4")).toEqual(8); + expect(_eval("4 + 4 + ' str'")).toEqual("8 str"); + expect(_eval("'str ' + 4 + 4")).toEqual("str 44"); }); }); }); -} \ No newline at end of file +} diff --git a/modules/core/src/compiler/view.js b/modules/core/src/compiler/view.js index 4db5327e63..362457bb87 100644 --- a/modules/core/src/compiler/view.js +++ b/modules/core/src/compiler/view.js @@ -4,7 +4,7 @@ import {ProtoWatchGroup, WatchGroup, WatchGroupDispatcher} from 'change_detectio import {Record} from 'change_detection/record'; import {ProtoElementInjector, ElementInjector} from './element_injector'; import {ElementBinder} from './element_binder'; -import {SetterFn} from 'change_detection/facade'; +import {SetterFn} from 'change_detection/parser/closure_map'; import {FIELD, IMPLEMENTS, int, isPresent, isBlank} from 'facade/lang'; import {List} from 'facade/collection'; import {Injector} from 'di/di'; diff --git a/modules/facade/src/lang.dart b/modules/facade/src/lang.dart index 1e5e83383e..a14abaf655 100644 --- a/modules/facade/src/lang.dart +++ b/modules/facade/src/lang.dart @@ -27,6 +27,26 @@ class IMPLEMENTS { bool isPresent(obj) => obj != null; bool isBlank(obj) => obj == null; +bool toBool(x) { + if (x is bool) return x; + if (x is num) return x != 0; + return false; +} + +autoConvertAdd(a, b) { + if (a != null && b != null) { + if (a is String && b is! String) { + return a + b.toString(); + } + if (a is! String && b is String) { + return a.toString() + b; + } + return a + b; + } + if (a != null) return a; + if (b != null) return b; + return 0; +} String stringify(obj) => obj.toString(); diff --git a/modules/facade/src/lang.es6 b/modules/facade/src/lang.es6 index f7a6b43803..baca880c8b 100644 --- a/modules/facade/src/lang.es6 +++ b/modules/facade/src/lang.es6 @@ -20,6 +20,14 @@ export function isBlank(obj):boolean { return obj === undefined || obj === null; } +export function toBool(obj) { + return !!obj; +} + +export function autoConvertAdd(a, b) { + return a + b; +} + export function stringify(token):string { if (typeof token === 'string') { return token;