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"); throw new BaseException("Not supported");
} }
assign(context, value) {
throw new BaseException("Not supported");
}
visit(visitor) { 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 { export class Conditional extends AST {
@FIELD('final condition:AST') @FIELD('final condition:AST')
@FIELD('final trueExp:AST') @FIELD('final trueExp:AST')
@ -43,21 +63,34 @@ export class FieldRead extends AST {
@FIELD('final receiver:AST') @FIELD('final receiver:AST')
@FIELD('final name:string') @FIELD('final name:string')
@FIELD('final getter:Function') @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.receiver = receiver;
this.name = name; this.name = name;
this.getter = getter; this.getter = getter;
this.setter = setter;
} }
eval(context) { eval(context) {
return this.getter(this.receiver.eval(context)); return this.getter(this.receiver.eval(context));
} }
assign(context, value) {
return this.setter(this.receiver.eval(context), value);
}
visit(visitor) { visit(visitor) {
visitor.visitFieldRead(this); visitor.visitFieldRead(this);
} }
} }
export class KeyedAccess extends AST {
constructor(obj:AST, key:AST) {
this.obj = obj;
this.key = key;
}
}
export class Formatter extends AST { export class Formatter extends AST {
@FIELD('final exp:AST') @FIELD('final exp:AST')
@FIELD('final name:string') @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 //INTERFACE
export class AstVisitor { export class AstVisitor {
visitImplicitReceiver(ast:ImplicitReceiver) {} visitImplicitReceiver(ast:ImplicitReceiver) {}
@ -155,6 +202,7 @@ export class AstVisitor {
visitPrefixNot(ast:PrefixNot) {} visitPrefixNot(ast:PrefixNot) {}
visitLiteralPrimitive(ast:LiteralPrimitive) {} visitLiteralPrimitive(ast:LiteralPrimitive) {}
visitFormatter(ast:Formatter) {} visitFormatter(ast:Formatter) {}
visitAssignment(ast:Assignment) {}
} }
var _evalListCache = [[],[0],[0,0],[0,0,0],[0,0,0,0],[0,0,0,0,0]]; 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); var symbol = new Symbol(name);
return (receiver) => reflect(receiver).getField(symbol).reflectee; 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) { getter(name:string) {
return new Function('o', 'return o.' + name + ';'); 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 {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 {ClosureMap} from './closure_map';
import { import {
AST, AST,
@ -11,7 +11,10 @@ import {
Binary, Binary,
PrefixNot, PrefixNot,
Conditional, Conditional,
Formatter Formatter,
Assignment,
Chain,
KeyedAccess
} from './ast'; } from './ast';
var _implicitReceiver = new ImplicitReceiver(); 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 { optionalOperator(op:string):boolean {
if (this.next.isOperator(op)) { if (this.next.isOperator(op)) {
this.advance(); 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 { parseChain():AST {
var exprs = []; var exprs = [];
while (this.index < this.tokens.length) { 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() { parseFormatter() {
@ -105,7 +128,7 @@ class _ParseAST {
if (this.parseAction) { if (this.parseAction) {
this.error("Cannot have a formatter in an action expression"); this.error("Cannot have a formatter in an action expression");
} }
var name = this.parseIdentifier(); var name = this.expectIdentifierOrKeyword();
var args = ListWrapper.create(); var args = ListWrapper.create();
while (this.optionalCharacter($COLON)) { while (this.optionalCharacter($COLON)) {
ListWrapper.push(args, this.parseExpression()); ListWrapper.push(args, this.parseExpression());
@ -116,7 +139,19 @@ class _ParseAST {
} }
parseExpression() { 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() { parseConditional() {
@ -202,7 +237,7 @@ class _ParseAST {
} }
parseMultiplicative() { parseMultiplicative() {
// '*', '%', '/', '~/' // '*', '%', '/'
var result = this.parsePrefix(); var result = this.parsePrefix();
while (true) { while (true) {
if (this.optionalOperator('*')) { if (this.optionalOperator('*')) {
@ -211,9 +246,6 @@ class _ParseAST {
result = new Binary('%', result, this.parsePrefix()); result = new Binary('%', result, this.parsePrefix());
} else if (this.optionalOperator('/')) { } else if (this.optionalOperator('/')) {
result = new Binary('/', result, this.parsePrefix()); 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 { } else {
return result; return result;
} }
@ -232,59 +264,54 @@ class _ParseAST {
} }
} }
parseAccessOrCallMember() { parseAccessOrCallMember():AST {
var result = this.parsePrimary(); var result = this.parsePrimary();
// TODO: add missing cases. while (true) {
return result; if (this.optionalCharacter($PERIOD)) {
result = this.parseFieldRead(result);
} else {
return result;
}
}
} }
parsePrimary() { parsePrimary() {
if (this.next.isKeywordNull() || this.next.isKeywordUndefined()) { if (this.next.isKeywordNull() || this.next.isKeywordUndefined()) {
this.advance(); this.advance();
return new LiteralPrimitive(null); return new LiteralPrimitive(null);
} else if (this.next.isKeywordTrue()) { } else if (this.next.isKeywordTrue()) {
this.advance(); this.advance();
return new LiteralPrimitive(true); return new LiteralPrimitive(true);
} else if (this.next.isKeywordFalse()) { } else if (this.next.isKeywordFalse()) {
this.advance(); this.advance();
return new LiteralPrimitive(false); return new LiteralPrimitive(false);
} else if (this.next.isIdentifier()) { } else if (this.next.isIdentifier()) {
return this.parseAccess(); return this.parseFieldRead(_implicitReceiver);
} else if (this.next.isNumber()) { } else if (this.next.isNumber()) {
var value = this.next.toNumber(); var value = this.next.toNumber();
this.advance(); this.advance();
return new LiteralPrimitive(value); return new LiteralPrimitive(value);
} else if (this.next.isString()) { } else if (this.next.isString()) {
var value = this.next.toString(); var value = this.next.toString();
this.advance(); this.advance();
return new LiteralPrimitive(value); 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 { } else if (this.index >= this.tokens.length) {
var result = this.parseFieldRead(_implicitReceiver); this.error(`Unexpected end of expression: ${this.input}`);
while(this.optionalCharacter($PERIOD)) {
result = this.parseFieldRead(result); } else {
this.error(`Unexpected token ${this.next}`);
} }
return result;
} }
parseFieldRead(receiver):AST { parseFieldRead(receiver):AST {
var id = this.parseIdentifier(); var id = this.expectIdentifierOrKeyword();
return new FieldRead(receiver, id, this.closureMap.getter(id)); return new FieldRead(receiver, id, this.closureMap.getter(id), this.closureMap.setter(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();
} }
error(message:string, index:int = null) { error(message:string, index:int = null) {
@ -294,16 +321,6 @@ class _ParseAST {
? `at column ${this.tokens[index].index + 1} in` ? `at column ${this.tokens[index].index + 1} in`
: `at the end of the expression`; : `at the end of the expression`;
throw new ParserError(`Parser Error: ${message} ${location} [${this.input}]`); throw new BaseException(`Parser Error: ${message} ${location} [${this.input}]`);
}
}
class ParserError extends Error {
constructor(message) {
this.message = message;
}
toString() {
return this.message;
} }
} }

View File

@ -19,7 +19,7 @@ export function main() {
var parts = exp.split("."); var parts = exp.split(".");
var cm = new ClosureMap(); var cm = new ClosureMap();
return ListWrapper.reduce(parts, function (ast, fieldName) { 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()); }, new ImplicitReceiver());
} }

View File

@ -150,25 +150,43 @@ export function main() {
createParser().parseAction('boo').eval(new ContextWithErrors()); createParser().parseAction('boo').eval(new ContextWithErrors());
}).toThrowError('boo to you'); }).toThrowError('boo to you');
}); });
});
describe("parseBinding", () => { it('should evaluate assignments', () => {
it("should parse formatters", function () { var context = td();
var exp = parseBinding("'Foo'|uppercase"); expectEval("a=12", context).toEqual(12);
expect(exp).toBeAnInstanceOf(Formatter); expect(context.a).toEqual(12);
expect(exp.name).toEqual("uppercase");
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 () { describe("parseBinding", () => {
var exp = parseBinding("1|increment:2"); //throw on assignment
expect(exp).toBeAnInstanceOf(Formatter);
expect(exp.name).toEqual("increment");
expect(exp.args[0]).toBeAnInstanceOf(LiteralPrimitive);
});
it('should throw on chain expressions', () => { it("should parse formatters", function () {
expect(() => parseBinding("1;2")).toThrowError(new RegExp("contain chained expression")); 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() { export function main() {
var oneFieldAst = (fieldName) => var oneFieldAst = (fieldName) => {
new FieldRead(new ImplicitReceiver(), fieldName, var cm = new ClosureMap();
(new ClosureMap()).getter(fieldName)); return new FieldRead(new ImplicitReceiver(), fieldName,
cm.getter(fieldName), cm.setter(fieldName));
};
describe('view', function() { describe('view', function() {
var tempalteWithThreeTypesOfBindings = var tempalteWithThreeTypesOfBindings =