diff --git a/modules/change_detection/src/parser/ast.js b/modules/change_detection/src/parser/ast.js index 6d40cee9fb..be9ec58c42 100644 --- a/modules/change_detection/src/parser/ast.js +++ b/modules/change_detection/src/parser/ast.js @@ -1,5 +1,5 @@ import {FIELD, toBool, autoConvertAdd, isBlank, FunctionWrapper, BaseException} from "facade/lang"; -import {List, ListWrapper} from "facade/collection"; +import {List, Map, ListWrapper, MapWrapper} from "facade/collection"; export class AST { eval(context) { @@ -89,6 +89,32 @@ export class KeyedAccess extends AST { this.obj = obj; this.key = key; } + eval(context) { + var obj = this.obj.eval(context); + var key = this.key.eval(context); + + if (obj instanceof Map) { + return MapWrapper.get(obj, key); + } else if (obj instanceof List) { + return ListWrapper.get(obj, key); + } else { + throw new BaseException(`Cannot access ${key} on ${obj}`); + } + } + assign(context, value) { + var obj = this.obj.eval(context); + var key = this.key.eval(context); + + if (obj instanceof Map) { + MapWrapper.set(obj, key, value); + } else if (obj instanceof List) { + ListWrapper.set(obj, key, value); + } else { + throw new BaseException(`Cannot access ${key} on ${obj}`); + } + return value; + } + } export class Formatter extends AST { @@ -120,6 +146,36 @@ export class LiteralPrimitive extends AST { } } +export class LiteralArray extends AST { + @FIELD('final expressions:List') + constructor(expressions:List) { + this.expressions = expressions; + } + eval(context) { + return ListWrapper.map(this.expressions, (e) => e.eval(context)); + } + visit(visitor) { + visitor.visitLiteralArray(this); + } +} + +export class LiteralMap extends AST { + @FIELD('final keys:List') + @FIELD('final values:List') + constructor(keys:List, values:List) { + this.keys = keys; + this.values = values; + } + + eval(context) { + var res = MapWrapper.create(); + for(var i = 0; i < this.keys.length; ++i) { + MapWrapper.set(res, this.keys[i], this.values[i].eval(context)); + } + return res; + } +} + export class Binary extends AST { @FIELD('final operation:string') @FIELD('final left:AST') @@ -203,6 +259,7 @@ export class AstVisitor { visitLiteralPrimitive(ast:LiteralPrimitive) {} visitFormatter(ast:Formatter) {} visitAssignment(ast:Assignment) {} + visitLiteralArray(ast:LiteralArray) {} } 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/lexer.js b/modules/change_detection/src/parser/lexer.js index 9899bd6f99..c76310b083 100644 --- a/modules/change_detection/src/parser/lexer.js +++ b/modules/change_detection/src/parser/lexer.js @@ -157,9 +157,9 @@ const $A = 65, $B = 66, $C = 67, $D = 68, $E = 69, $F = 70, $G = 71, $H = 72, $Q = 81, $R = 82, $S = 83, $T = 84, $U = 85, $V = 86, $W = 87, $X = 88, $Y = 89, $Z = 90; -const $LBRACKET = 91; -const $BACKSLASH = 92; -const $RBRACKET = 93; +export const $LBRACKET = 91; +export const $BACKSLASH = 92; +export const $RBRACKET = 93; const $CARET = 94; const $_ = 95; @@ -168,9 +168,9 @@ const $a = 97, $b = 98, $c = 99, $d = 100, $e = 101, $f = 102, $g = 103, $o = 111, $p = 112, $q = 113, $r = 114, $s = 115, $t = 116, $u = 117, $v = 118, $w = 119, $x = 120, $y = 121, $z = 122; -const $LBRACE = 123; -const $BAR = 124; -const $RBRACE = 125; +export const $LBRACE = 123; +export const $BAR = 124; +export const $RBRACE = 125; const $TILDE = 126; const $NBSP = 160; diff --git a/modules/change_detection/src/parser/parser.js b/modules/change_detection/src/parser/parser.js index 41922cd377..7c5a8235be 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, BaseException} from 'facade/lang'; +import {FIELD, int, isBlank, BaseException, StringWrapper} from 'facade/lang'; import {ListWrapper, List} from 'facade/collection'; -import {Lexer, EOF, Token, $PERIOD, $COLON, $SEMICOLON, $LBRACKET, $RBRACKET} from './lexer'; +import {Lexer, EOF, Token, $PERIOD, $COLON, $SEMICOLON, $LBRACKET, $RBRACKET, $COMMA, $LBRACE, $RBRACE} from './lexer'; import {ClosureMap} from './closure_map'; import { AST, @@ -14,7 +14,9 @@ import { Formatter, Assignment, Chain, - KeyedAccess + KeyedAccess, + LiteralArray, + LiteralMap } from './ast'; var _implicitReceiver = new ImplicitReceiver(); @@ -80,7 +82,7 @@ class _ParseAST { expectCharacter(code:int) { if (this.optionalCharacter(code)) return; - this.error(`Missing expected ${code}`); + this.error(`Missing expected ${StringWrapper.fromCharCode(code)}`); } @@ -107,6 +109,15 @@ class _ParseAST { return n.toString(); } + expectIdentifierOrKeywordOrString():string { + var n = this.next; + if (!n.isIdentifier() && !n.isKeyword() && !n.isString()) { + this.error(`Unexpected token ${n}, expected identifier, or keyword, or string`) + } + this.advance(); + return n.toString(); + } + parseChain():AST { var exprs = []; while (this.index < this.tokens.length) { @@ -142,11 +153,6 @@ class _ParseAST { 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()); } @@ -269,6 +275,12 @@ class _ParseAST { while (true) { if (this.optionalCharacter($PERIOD)) { result = this.parseFieldRead(result); + + } else if (this.optionalCharacter($LBRACKET)) { + var key = this.parseExpression(); + this.expectCharacter($RBRACKET); + result = new KeyedAccess(result, key); + } else { return result; } @@ -288,6 +300,14 @@ class _ParseAST { this.advance(); return new LiteralPrimitive(false); + } else if (this.optionalCharacter($LBRACKET)) { + var elements = this.parseExpressionList($RBRACKET); + this.expectCharacter($RBRACKET); + return new LiteralArray(elements); + + } else if (this.next.isCharacter($LBRACE)) { + return this.parseLiteralMap(); + } else if (this.next.isIdentifier()) { return this.parseFieldRead(_implicitReceiver); @@ -309,6 +329,32 @@ class _ParseAST { } } + parseExpressionList(terminator:int):List { + var result = []; + if (!this.next.isCharacter(terminator)) { + do { + ListWrapper.push(result, this.parseExpression()); + } while (this.optionalCharacter($COMMA)); + } + return result; + } + + parseLiteralMap() { + var keys = []; + var values = []; + this.expectCharacter($LBRACE); + if (!this.optionalCharacter($RBRACE)) { + do { + var key = this.expectIdentifierOrKeywordOrString(); + ListWrapper.push(keys, key); + this.expectCharacter($COLON); + ListWrapper.push(values, this.parseExpression()); + } while (this.optionalCharacter($COMMA)); + this.expectCharacter($RBRACE); + } + return new LiteralMap(keys, values); + } + parseFieldRead(receiver):AST { var id = this.expectIdentifierOrKeyword(); return new FieldRead(receiver, id, this.closureMap.getter(id), this.closureMap.setter(id)); diff --git a/modules/change_detection/test/parser/parser_spec.js b/modules/change_detection/test/parser/parser_spec.js index 16f22615cb..2ea396af84 100644 --- a/modules/change_detection/test/parser/parser_spec.js +++ b/modules/change_detection/test/parser/parser_spec.js @@ -1,5 +1,6 @@ import {ddescribe, describe, it, iit, expect, beforeEach} from 'test_lib/test_lib'; import {BaseException, isBlank} from 'facade/lang'; +import {MapWrapper} from 'facade/collection'; import {Parser} from 'change_detection/parser/parser'; import {Lexer} from 'change_detection/parser/lexer'; import {Formatter, LiteralPrimitive} from 'change_detection/parser/ast'; @@ -164,6 +165,39 @@ export function main() { expectEval("a=123; b=234", context).toEqual(234); expect(context.a).toEqual(123); expect(context.b).toEqual(234); + + context = td([100]); + expectEval('a[0] = 200', context).toEqual(200); + expect(context.a[0]).toEqual(200); + + context = td(MapWrapper.createFromPairs([["key", 100]])); + expectEval('a["key"] = 200', context).toEqual(200); + expect(MapWrapper.get(context.a, "key")).toEqual(200); + + context = td([MapWrapper.createFromPairs([["key", 100]])]); + expectEval('a[0]["key"] = 200', context).toEqual(200); + expect(MapWrapper.get(context.a[0], "key")).toEqual(200); + }); + + it('should evaluate array', () => { + expectEval("[1][0]").toEqual(1); + expectEval("[[1]][0][0]").toEqual(1); + expectEval("[]").toEqual([]); + expectEval("[].length").toEqual(0); + expectEval("[1, 2].length").toEqual(2); + }); + + it("should error when unfinished exception", () => { + expectEvalError('a[0 = 200').toThrowError(new RegExp("Missing expected ]")); + }); + + it('should evaluate map', () => { + expectEval("{}").toEqual(MapWrapper.create()); + expectEval("{a:'b'}").toEqual(MapWrapper.createFromPairs([["a", "b"]])); + expectEval("{'a':'b'}").toEqual(MapWrapper.createFromPairs([["a", "b"]])); + expectEval("{\"a\":'b'}").toEqual(MapWrapper.createFromPairs([["a", "b"]])); + expectEval("{\"a\":'b'}['a']").toEqual("b"); + expectEval("{\"a\":'b'}['invalid']").not.toBeDefined(); }); describe("parseBinding", () => { diff --git a/modules/facade/src/collection.dart b/modules/facade/src/collection.dart index 0816f05c3b..bfdc16fcae 100644 --- a/modules/facade/src/collection.dart +++ b/modules/facade/src/collection.dart @@ -5,6 +5,12 @@ export 'dart:core' show Map, List, Set; class MapWrapper { static HashMap create() => new HashMap(); + static HashMap createFromPairs(List pairs) { + return pairs.fold({}, (m, p){ + m[p[0]] = p[1]; + return m; + }); + } static get(m, k) => m[k]; static void set(m, k, v){ m[k] = v; } static contains(m, k) => m.containsKey(k); diff --git a/modules/facade/src/collection.es6 b/modules/facade/src/collection.es6 index 4ed8a35e52..bfc3b857dc 100644 --- a/modules/facade/src/collection.es6 +++ b/modules/facade/src/collection.es6 @@ -6,6 +6,7 @@ export var Set = window.Set; export class MapWrapper { static create():Map { return new Map(); } + static createFromPairs(pairs:List):Map { return new Map(pairs); } static get(m, k) { return m.get(k); } static set(m, k, v) { m.set(k,v); } static contains(m, k) { return m.has(k); }