feat(parser): add support for ternary operator

This commit is contained in:
vsavkin 2014-11-03 17:25:16 -08:00
parent 965fa1a985
commit a7fe25d93f
3 changed files with 135 additions and 63 deletions

View File

@ -18,14 +18,23 @@ export class ImplicitReceiver extends AST {
}
}
export class Expression extends AST {
constructor() {
this.isAssignable = false;
this.isChain = false;
export class Conditional extends AST {
constructor(condition:AST, yes:AST, no:AST){
this.condition = condition;
this.yes = yes;
this.no = no;
}
eval(context, formatters) {
if(this.condition.eval(context, formatters)) {
return this.yes.eval(context, formatters);
} else {
return this.no.eval(context, formatters);
}
}
}
export class FieldRead extends Expression {
export class FieldRead extends AST {
constructor(receiver:AST, name:string, getter:Function) {
this.receiver = receiver;
this.name = name;
@ -41,7 +50,7 @@ export class FieldRead extends Expression {
}
}
export class LiteralPrimitive extends Expression {
export class LiteralPrimitive extends AST {
@FIELD('final value')
constructor(value) {
this.value = value;
@ -54,11 +63,11 @@ export class LiteralPrimitive extends Expression {
}
}
export class Binary extends Expression {
export class Binary extends AST {
@FIELD('final operation:string')
@FIELD('final left:Expression')
@FIELD('final right:Expression')
constructor(operation:string, left:Expression, right:Expression) {
@FIELD('final left:AST')
@FIELD('final right:AST')
constructor(operation:string, left:AST, right:AST) {
this.operation = operation;
this.left = left;
this.right = right;
@ -112,10 +121,10 @@ export class Binary extends Expression {
}
}
export class PrefixNot extends Expression {
export class PrefixNot extends AST {
@FIELD('final operation:string')
@FIELD('final expression:Expression')
constructor(expression:Expression) {
@FIELD('final expression:AST')
constructor(expression:AST) {
this.expression = expression;
}
visit(visitor) { visitor.visitPrefixNot(this); }

View File

@ -1,9 +1,9 @@
import {FIELD, int} from 'facade/lang';
import {FIELD, int, isBlank} from 'facade/lang';
import {ListWrapper, List} from 'facade/collection';
import {Lexer, EOF, Token, $PERIOD} from './lexer';
import {Lexer, EOF, Token, $PERIOD, $COLON} from './lexer';
import {ClosureMap} from './closure_map';
import {AST, ImplicitReceiver, FieldRead, LiteralPrimitive, Expression,
Binary, PrefixNot } from './ast';
Binary, PrefixNot, Conditional} from './ast';
var _implicitReceiver = new ImplicitReceiver();
@ -17,15 +17,17 @@ export class Parser {
parse(input:string):AST {
var tokens = this._lexer.tokenize(input);
return new _ParseAST(tokens, this._closureMap).parseChain();
return new _ParseAST(input, tokens, this._closureMap).parseChain();
}
}
class _ParseAST {
@FIELD('final input:String')
@FIELD('final tokens:List<Token>')
@FIELD('final closureMap:ClosureMap')
@FIELD('index:int')
constructor(tokens:List, closureMap:ClosureMap) {
constructor(input:string, tokens:List, closureMap:ClosureMap) {
this.input = input;
this.tokens = tokens;
this.index = 0;
this.closureMap = closureMap;
@ -40,6 +42,10 @@ class _ParseAST {
return this.peek(0);
}
get inputIndex():int {
return (this.index < this.tokens.length) ? this.next.index : this.input.length;
}
advance() {
this.index ++;
}
@ -62,6 +68,36 @@ class _ParseAST {
}
}
parseChain():AST {
var exprs = [];
while (this.index < this.tokens.length) {
ListWrapper.push(exprs, this.parseConditional());
}
return ListWrapper.first(exprs);
}
parseExpression() {
return this.parseConditional();
}
parseConditional() {
var start = this.inputIndex;
var result = this.parseLogicalOr();
if (this.optionalOperator('?')) {
var yes = this.parseExpression();
if (!this.optionalCharacter($COLON)) {
var end = this.inputIndex;
var expression = this.input.substring(start, end);
this.error(`Conditional expression ${expression} requires all 3 expressions`);
}
var no = this.parseExpression();
return new Conditional(result, yes, no);
} else {
return result;
}
}
parseLogicalOr() {
// '||'
var result = this.parseLogicalAnd();
@ -157,23 +193,6 @@ class _ParseAST {
}
}
parseChain():AST {
var exprs = [];
while (this.index < this.tokens.length) {
ListWrapper.push(exprs, this.parseLogicalOr());
}
return ListWrapper.first(exprs);
}
parseAccess():AST {
var result = this.parseFieldRead(_implicitReceiver);
while(this.optionalCharacter($PERIOD)) {
result = this.parseFieldRead(result);
}
return result;
}
parseAccessOrCallMember() {
var result = this.parsePrimary();
// TODO: add missing cases.
@ -181,9 +200,6 @@ class _ParseAST {
}
parsePrimary() {
var value;
// TODO: add missing cases.
if (this.next.isKeywordNull() || this.next.isKeywordUndefined()) {
this.advance();
return new LiteralPrimitive(null);
@ -196,11 +212,11 @@ class _ParseAST {
} else if (this.next.isIdentifier()) {
return this.parseAccess();
} else if (this.next.isNumber()) {
value = this.next.toNumber();
var value = this.next.toNumber();
this.advance();
return new LiteralPrimitive(value);
} else if (this.next.isString()) {
value = this.next.toString();
var value = this.next.toString();
this.advance();
return new LiteralPrimitive(value);
} else if (this.index >= this.tokens.length) {
@ -210,6 +226,14 @@ class _ParseAST {
}
}
parseAccess():AST {
var result = this.parseFieldRead(_implicitReceiver);
while(this.optionalCharacter($PERIOD)) {
result = this.parseFieldRead(result);
}
return result;
}
parseFieldRead(receiver):AST {
var id = this.parseIdentifier();
return new FieldRead(receiver, id, this.closureMap.getter(id));
@ -220,4 +244,24 @@ class _ParseAST {
this.advance();
return n.toString();
}
error(message:string, index:int = null) {
if (isBlank(index)) index = this.index;
var location = (index < this.tokens.length)
? `at column ${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;
}
}

View File

@ -20,7 +20,15 @@ export function main() {
function _eval(text) {
return new Parser(new Lexer(), new ClosureMap()).parse(text)
.eval(context, formatters);
.eval(context, formatters);
}
function expectEval(text) {
return expect(_eval(text));
}
function expectEvalError(text) {
return expect(() => _eval(text));
}
describe("parser", () => {
@ -47,64 +55,75 @@ export function main() {
describe('expressions', () => {
it('should parse numerical expressions', () => {
expect(_eval("1")).toEqual(1);
expectEval("1").toEqual(1);
});
it('should parse unary - expressions', () => {
expect(_eval("-1")).toEqual(-1);
expect(_eval("+1")).toEqual(1);
expectEval("-1").toEqual(-1);
expectEval("+1").toEqual(1);
});
it('should parse unary ! expressions', () => {
expect(_eval("!true")).toEqual(!true);
expectEval("!true").toEqual(!true);
});
it('should parse multiplicative expressions', () => {
expect(_eval("3*4/2%5")).toEqual(3*4/2%5);
expectEval("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);
// expectEval("3*4~/2%5")).toEqual(3*4~/2%5);
});
it('should parse additive expressions', () => {
expect(_eval("3+6-2")).toEqual(3+6-2);
expectEval("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);
expectEval("2<3").toEqual(2<3);
expectEval("2>3").toEqual(2>3);
expectEval("2<=2").toEqual(2<=2);
expectEval("2>=2").toEqual(2>=2);
});
it('should parse equality expressions', () => {
expect(_eval("2==3")).toEqual(2==3);
expect(_eval("2!=3")).toEqual(2!=3);
expectEval("2==3").toEqual(2==3);
expectEval("2!=3").toEqual(2!=3);
});
it('should parse logicalAND expressions', () => {
expect(_eval("true&&true")).toEqual(true&&true);
expect(_eval("true&&false")).toEqual(true&&false);
expectEval("true&&true").toEqual(true&&true);
expectEval("true&&false").toEqual(true&&false);
});
it('should parse logicalOR expressions', () => {
expect(_eval("false||true")).toEqual(false||true);
expect(_eval("false||false")).toEqual(false||false);
expectEval("false||true").toEqual(false||true);
expectEval("false||false").toEqual(false||false);
});
it('should parse ternary/conditional expressions', () => {
expectEval("7==3+4?10:20").toEqual(10);
expectEval("false?10:20").toEqual(20);
});
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");
expectEval("'str ' + 4").toEqual("str 4");
expectEval("4 + ' str'").toEqual("4 str");
expectEval("4 + 4").toEqual(8);
expectEval("4 + 4 + ' str'").toEqual("8 str");
expectEval("'str ' + 4 + 4").toEqual("str 44");
});
});
describe("error handling", () => {
it('should throw on incorrect ternary operator syntax', () => {
expectEvalError("true?1").toThrowError(new RegExp('Parser Error: Conditional expression true\\?1 requires all 3 expressions'));
});
});
});