Skip to content

Commit a7fe25d

Browse files
committed
feat(parser): add support for ternary operator
1 parent 965fa1a commit a7fe25d

File tree

3 files changed

+135
-63
lines changed

3 files changed

+135
-63
lines changed

modules/change_detection/src/parser/ast.js

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,23 @@ export class ImplicitReceiver extends AST {
1818
}
1919
}
2020

21-
export class Expression extends AST {
22-
constructor() {
23-
this.isAssignable = false;
24-
this.isChain = false;
21+
export class Conditional extends AST {
22+
constructor(condition:AST, yes:AST, no:AST){
23+
this.condition = condition;
24+
this.yes = yes;
25+
this.no = no;
26+
}
27+
28+
eval(context, formatters) {
29+
if(this.condition.eval(context, formatters)) {
30+
return this.yes.eval(context, formatters);
31+
} else {
32+
return this.no.eval(context, formatters);
33+
}
2534
}
2635
}
2736

28-
export class FieldRead extends Expression {
37+
export class FieldRead extends AST {
2938
constructor(receiver:AST, name:string, getter:Function) {
3039
this.receiver = receiver;
3140
this.name = name;
@@ -41,7 +50,7 @@ export class FieldRead extends Expression {
4150
}
4251
}
4352

44-
export class LiteralPrimitive extends Expression {
53+
export class LiteralPrimitive extends AST {
4554
@FIELD('final value')
4655
constructor(value) {
4756
this.value = value;
@@ -54,11 +63,11 @@ export class LiteralPrimitive extends Expression {
5463
}
5564
}
5665

57-
export class Binary extends Expression {
66+
export class Binary extends AST {
5867
@FIELD('final operation:string')
59-
@FIELD('final left:Expression')
60-
@FIELD('final right:Expression')
61-
constructor(operation:string, left:Expression, right:Expression) {
68+
@FIELD('final left:AST')
69+
@FIELD('final right:AST')
70+
constructor(operation:string, left:AST, right:AST) {
6271
this.operation = operation;
6372
this.left = left;
6473
this.right = right;
@@ -112,10 +121,10 @@ export class Binary extends Expression {
112121
}
113122
}
114123

115-
export class PrefixNot extends Expression {
124+
export class PrefixNot extends AST {
116125
@FIELD('final operation:string')
117-
@FIELD('final expression:Expression')
118-
constructor(expression:Expression) {
126+
@FIELD('final expression:AST')
127+
constructor(expression:AST) {
119128
this.expression = expression;
120129
}
121130
visit(visitor) { visitor.visitPrefixNot(this); }

modules/change_detection/src/parser/parser.js

Lines changed: 71 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import {FIELD, int} from 'facade/lang';
1+
import {FIELD, int, isBlank} from 'facade/lang';
22
import {ListWrapper, List} from 'facade/collection';
3-
import {Lexer, EOF, Token, $PERIOD} from './lexer';
3+
import {Lexer, EOF, Token, $PERIOD, $COLON} from './lexer';
44
import {ClosureMap} from './closure_map';
55
import {AST, ImplicitReceiver, FieldRead, LiteralPrimitive, Expression,
6-
Binary, PrefixNot } from './ast';
6+
Binary, PrefixNot, Conditional} from './ast';
77

88
var _implicitReceiver = new ImplicitReceiver();
99

@@ -17,15 +17,17 @@ export class Parser {
1717

1818
parse(input:string):AST {
1919
var tokens = this._lexer.tokenize(input);
20-
return new _ParseAST(tokens, this._closureMap).parseChain();
20+
return new _ParseAST(input, tokens, this._closureMap).parseChain();
2121
}
2222
}
2323

2424
class _ParseAST {
25+
@FIELD('final input:String')
2526
@FIELD('final tokens:List<Token>')
2627
@FIELD('final closureMap:ClosureMap')
2728
@FIELD('index:int')
28-
constructor(tokens:List, closureMap:ClosureMap) {
29+
constructor(input:string, tokens:List, closureMap:ClosureMap) {
30+
this.input = input;
2931
this.tokens = tokens;
3032
this.index = 0;
3133
this.closureMap = closureMap;
@@ -40,6 +42,10 @@ class _ParseAST {
4042
return this.peek(0);
4143
}
4244

45+
get inputIndex():int {
46+
return (this.index < this.tokens.length) ? this.next.index : this.input.length;
47+
}
48+
4349
advance() {
4450
this.index ++;
4551
}
@@ -62,6 +68,36 @@ class _ParseAST {
6268
}
6369
}
6470

71+
parseChain():AST {
72+
var exprs = [];
73+
while (this.index < this.tokens.length) {
74+
ListWrapper.push(exprs, this.parseConditional());
75+
}
76+
return ListWrapper.first(exprs);
77+
}
78+
79+
parseExpression() {
80+
return this.parseConditional();
81+
}
82+
83+
parseConditional() {
84+
var start = this.inputIndex;
85+
var result = this.parseLogicalOr();
86+
87+
if (this.optionalOperator('?')) {
88+
var yes = this.parseExpression();
89+
if (!this.optionalCharacter($COLON)) {
90+
var end = this.inputIndex;
91+
var expression = this.input.substring(start, end);
92+
this.error(`Conditional expression ${expression} requires all 3 expressions`);
93+
}
94+
var no = this.parseExpression();
95+
return new Conditional(result, yes, no);
96+
} else {
97+
return result;
98+
}
99+
}
100+
65101
parseLogicalOr() {
66102
// '||'
67103
var result = this.parseLogicalAnd();
@@ -157,33 +193,13 @@ class _ParseAST {
157193
}
158194
}
159195

160-
161-
parseChain():AST {
162-
var exprs = [];
163-
while (this.index < this.tokens.length) {
164-
ListWrapper.push(exprs, this.parseLogicalOr());
165-
}
166-
return ListWrapper.first(exprs);
167-
}
168-
169-
parseAccess():AST {
170-
var result = this.parseFieldRead(_implicitReceiver);
171-
while(this.optionalCharacter($PERIOD)) {
172-
result = this.parseFieldRead(result);
173-
}
174-
return result;
175-
}
176-
177196
parseAccessOrCallMember() {
178197
var result = this.parsePrimary();
179198
// TODO: add missing cases.
180199
return result;
181200
}
182201

183202
parsePrimary() {
184-
var value;
185-
// TODO: add missing cases.
186-
187203
if (this.next.isKeywordNull() || this.next.isKeywordUndefined()) {
188204
this.advance();
189205
return new LiteralPrimitive(null);
@@ -196,11 +212,11 @@ class _ParseAST {
196212
} else if (this.next.isIdentifier()) {
197213
return this.parseAccess();
198214
} else if (this.next.isNumber()) {
199-
value = this.next.toNumber();
215+
var value = this.next.toNumber();
200216
this.advance();
201217
return new LiteralPrimitive(value);
202218
} else if (this.next.isString()) {
203-
value = this.next.toString();
219+
var value = this.next.toString();
204220
this.advance();
205221
return new LiteralPrimitive(value);
206222
} else if (this.index >= this.tokens.length) {
@@ -210,6 +226,14 @@ class _ParseAST {
210226
}
211227
}
212228

229+
parseAccess():AST {
230+
var result = this.parseFieldRead(_implicitReceiver);
231+
while(this.optionalCharacter($PERIOD)) {
232+
result = this.parseFieldRead(result);
233+
}
234+
return result;
235+
}
236+
213237
parseFieldRead(receiver):AST {
214238
var id = this.parseIdentifier();
215239
return new FieldRead(receiver, id, this.closureMap.getter(id));
@@ -220,4 +244,24 @@ class _ParseAST {
220244
this.advance();
221245
return n.toString();
222246
}
247+
248+
error(message:string, index:int = null) {
249+
if (isBlank(index)) index = this.index;
250+
251+
var location = (index < this.tokens.length)
252+
? `at column ${tokens[index].index + 1} in`
253+
: `at the end of the expression`;
254+
255+
throw new ParserError(`Parser Error: ${message} ${location} [${this.input}]`);
256+
}
223257
}
258+
259+
class ParserError extends Error {
260+
constructor(message) {
261+
this.message = message;
262+
}
263+
264+
toString() {
265+
return this.message;
266+
}
267+
}

modules/change_detection/test/parser/parser_spec.js

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,15 @@ export function main() {
2020

2121
function _eval(text) {
2222
return new Parser(new Lexer(), new ClosureMap()).parse(text)
23-
.eval(context, formatters);
23+
.eval(context, formatters);
24+
}
25+
26+
function expectEval(text) {
27+
return expect(_eval(text));
28+
}
29+
30+
function expectEvalError(text) {
31+
return expect(() => _eval(text));
2432
}
2533

2634
describe("parser", () => {
@@ -47,64 +55,75 @@ export function main() {
4755
describe('expressions', () => {
4856

4957
it('should parse numerical expressions', () => {
50-
expect(_eval("1")).toEqual(1);
58+
expectEval("1").toEqual(1);
5159
});
5260

5361

5462
it('should parse unary - expressions', () => {
55-
expect(_eval("-1")).toEqual(-1);
56-
expect(_eval("+1")).toEqual(1);
63+
expectEval("-1").toEqual(-1);
64+
expectEval("+1").toEqual(1);
5765
});
5866

5967

6068
it('should parse unary ! expressions', () => {
61-
expect(_eval("!true")).toEqual(!true);
69+
expectEval("!true").toEqual(!true);
6270
});
6371

6472

6573
it('should parse multiplicative expressions', () => {
66-
expect(_eval("3*4/2%5")).toEqual(3*4/2%5);
74+
expectEval("3*4/2%5").toEqual(3*4/2%5);
6775
// TODO(rado): This exists only in Dart, figure out whether to support it.
68-
// expect(_eval("3*4~/2%5")).toEqual(3*4~/2%5);
76+
// expectEval("3*4~/2%5")).toEqual(3*4~/2%5);
6977
});
7078

7179

7280
it('should parse additive expressions', () => {
73-
expect(_eval("3+6-2")).toEqual(3+6-2);
81+
expectEval("3+6-2").toEqual(3+6-2);
7482
});
7583

7684

7785
it('should parse relational expressions', () => {
78-
expect(_eval("2<3")).toEqual(2<3);
79-
expect(_eval("2>3")).toEqual(2>3);
80-
expect(_eval("2<=2")).toEqual(2<=2);
81-
expect(_eval("2>=2")).toEqual(2>=2);
86+
expectEval("2<3").toEqual(2<3);
87+
expectEval("2>3").toEqual(2>3);
88+
expectEval("2<=2").toEqual(2<=2);
89+
expectEval("2>=2").toEqual(2>=2);
8290
});
8391

8492

8593
it('should parse equality expressions', () => {
86-
expect(_eval("2==3")).toEqual(2==3);
87-
expect(_eval("2!=3")).toEqual(2!=3);
94+
expectEval("2==3").toEqual(2==3);
95+
expectEval("2!=3").toEqual(2!=3);
8896
});
8997

9098

9199
it('should parse logicalAND expressions', () => {
92-
expect(_eval("true&&true")).toEqual(true&&true);
93-
expect(_eval("true&&false")).toEqual(true&&false);
100+
expectEval("true&&true").toEqual(true&&true);
101+
expectEval("true&&false").toEqual(true&&false);
94102
});
95103

96104

97105
it('should parse logicalOR expressions', () => {
98-
expect(_eval("false||true")).toEqual(false||true);
99-
expect(_eval("false||false")).toEqual(false||false);
106+
expectEval("false||true").toEqual(false||true);
107+
expectEval("false||false").toEqual(false||false);
108+
});
109+
110+
it('should parse ternary/conditional expressions', () => {
111+
expectEval("7==3+4?10:20").toEqual(10);
112+
expectEval("false?10:20").toEqual(20);
100113
});
101114

102115
it('should auto convert ints to strings', () => {
103-
expect(_eval("'str ' + 4")).toEqual("str 4");
104-
expect(_eval("4 + ' str'")).toEqual("4 str");
105-
expect(_eval("4 + 4")).toEqual(8);
106-
expect(_eval("4 + 4 + ' str'")).toEqual("8 str");
107-
expect(_eval("'str ' + 4 + 4")).toEqual("str 44");
116+
expectEval("'str ' + 4").toEqual("str 4");
117+
expectEval("4 + ' str'").toEqual("4 str");
118+
expectEval("4 + 4").toEqual(8);
119+
expectEval("4 + 4 + ' str'").toEqual("8 str");
120+
expectEval("'str ' + 4 + 4").toEqual("str 44");
121+
});
122+
});
123+
124+
describe("error handling", () => {
125+
it('should throw on incorrect ternary operator syntax', () => {
126+
expectEvalError("true?1").toThrowError(new RegExp('Parser Error: Conditional expression true\\?1 requires all 3 expressions'));
108127
});
109128
});
110129
});

0 commit comments

Comments
 (0)