feat(parser): add support for ternary operator
This commit is contained in:
parent
965fa1a985
commit
a7fe25d93f
|
@ -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); }
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue