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:
Rado Kirov 2014-10-30 23:47:22 -07:00
parent 8c566dcfb5
commit 965fa1a985
11 changed files with 366 additions and 45 deletions

View File

@ -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;
}
}

View File

@ -1,7 +0,0 @@
export var SetterFn = Function;
export class FieldGetterFactory {
getter(object, name:string) {
return new Function('o', 'return o["' + name + '"]');
}
}

View File

@ -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) {}
}

View File

@ -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;
}
}
}

View File

@ -1,5 +1,7 @@
export var SetterFn = Function;
export class ClosureMap {
getter(name:string) {
return new Function('o', 'return o.' + name + ';');
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -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");
});
});
});
}
}

View File

@ -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';

View File

@ -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();

View File

@ -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;