feat(Parser): add support for assignments

This commit is contained in:
vsavkin 2014-11-05 10:00:19 -08:00
parent 8e6326f838
commit 8cc008bda1
7 changed files with 160 additions and 66 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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