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");
|
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]];
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
Loading…
Reference in New Issue