feat(Parser): add support for assignments
This commit is contained in:
parent
8e6326f838
commit
8cc008bda1
|
@ -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]];
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}]`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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"));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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 =
|
||||
|
|
Loading…
Reference in New Issue