feat(parser): adds basic expressions to the parser.
Mostly copy pasta from angular.dart. Remove GetterFactory in favor for ClosureMap (which has basically the same implementation).
This commit is contained in:
parent
8c566dcfb5
commit
965fa1a985
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
export var SetterFn = Function;
|
||||
|
||||
export class FieldGetterFactory {
|
||||
getter(object, name:string) {
|
||||
return new Function('o', 'return o["' + name + '"]');
|
||||
}
|
||||
}
|
|
@ -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) {}
|
||||
}
|
||||
visitBinary(ast:Binary) {}
|
||||
visitPrefixNot(ast:PrefixNot) {}
|
||||
visitLiteralPrimitive(ast:LiteralPrimitive) {}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
export var SetterFn = Function;
|
||||
|
||||
export class ClosureMap {
|
||||
getter(name:string) {
|
||||
return new Function('o', 'return o.' + name + ';');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue