Skip to content
This repository has been archived by the owner on Feb 22, 2018. It is now read-only.

Commit

Permalink
feat($parse): support the ternary/conditional operator
Browse files Browse the repository at this point in the history
Closes #272
  • Loading branch information
chirayuk committed Nov 21, 2013
1 parent 0f8bb2b commit e38da6f
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 10 deletions.
4 changes: 4 additions & 0 deletions lib/core/parser/backend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ class ParserBackend {

_op(opKey) => OPERATORS[opKey];

Expression ternaryFn(Expression cond, Expression _true, Expression _false) =>
new Expression((self, [locals]) => _op('?')(
self, locals, cond, _true, _false));

Expression binaryFn(Expression left, String op, Expression right) =>
new Expression((self, [locals]) => _op(op)(self, locals, left, right));

Expand Down
24 changes: 21 additions & 3 deletions lib/core/parser/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ Map<String, Operator> OPERATORS = {
'||': (s, l, a, b) => toBool(a.eval(s, l)) || toBool(b.eval(s, l)),
'&': (s, l, a, b) => a.eval(s, l) & b.eval(s, l),
'|': NOT_IMPL_OP, //b(locals)(locals, a(locals))
'!': (self, locals, a, b) => !toBool(a.eval(self, locals))
'!': (s, l, a, b) => !toBool(a.eval(s, l)),
'?': (s, l, c, t, f) => toBool(c.eval(s, l)) ? t.eval(s, l) : f.eval(s, l),
};

class DynamicParser implements Parser {
Expand Down Expand Up @@ -343,17 +344,34 @@ class DynamicParser implements Parser {
}
}

ParserAST _ternary() {
var ts = _saveTokens();
var cond = _logicalOR();
var token = _expect('?');
if (token != null) {
var _true = _expression();
if ((token = _expect(':')) != null) {
cond = _b.ternaryFn(cond, _true, _expression());
} else {
throw _parserError('Conditional expression ${_tokensText(ts)} requires '
'all 3 expressions');
}
}
_stopSavingTokens(ts);
return cond;
}

ParserAST _assignment() {
var ts = _saveTokens();
var left = _logicalOR();
var left = _ternary();
_stopSavingTokens(ts);
var right;
var token;
if ((token = _expect('=')) != null) {
if (!left.assignable) {
throw _parserError('Expression ${_tokensText(ts)} is not assignable', token);
}
right = _logicalOR();
right = _ternary();
return _b.assignment(left, right, _evalError);
} else {
return left;
Expand Down
6 changes: 0 additions & 6 deletions test/core/parser/lexer_spec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,5 @@ main() {
lex("'\\u1''bla'");
}).toThrow("Lexer Error: Invalid unicode escape [\\u1''b] at column 2 in expression ['\\u1''bla']");
});

it('should throw error on unexpected characters', () {
expect(() {
lex("a == b ? 3 : 4");
}).toThrow('Lexer Error: Unexpected next character [?] at column 7 in expression [a == b ? 3 : 4]');
});
});
}
98 changes: 97 additions & 1 deletion test/core/parser/parser_spec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class InheritedMapData extends MapData {
noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}

toBool(x) => (x is num) ? x != 0 : x == true;

main() {
describe('parse', () {
var scope, parser;
Expand All @@ -45,7 +47,7 @@ main() {
beforeEach(inject((Parser injectedParser) {
parser = injectedParser;
}));

eval(String text) => parser(text).eval(scope, null);
expectEval(String expr) => expect(() => eval(expr));

Expand Down Expand Up @@ -105,6 +107,24 @@ main() {
});


it('should parse ternary/conditional expressions', () {
var a, b, c;
expect(eval("7==3+4?10:20")).toEqual(true?10:20);
expect(eval("false?10:20")).toEqual(false?10:20);
expect(eval("5?10:20")).toEqual(toBool(5)?10:20);
expect(eval("null?10:20")).toEqual(toBool(null)?10:20);
expect(eval("true||false?10:20")).toEqual(true||false?10:20);
expect(eval("true&&false?10:20")).toEqual(true&&false?10:20);
expect(eval("true?a=10:a=20")).toEqual(true?a=10:a=20);
expect([scope['a'], a]).toEqual([10, 10]);
scope['a'] = a = null;
expect(eval("b=true?a=false?11:c=12:a=13")).toEqual(
b=true?a=false?11:c=12:a=13);
expect([scope['a'], scope['b'], scope['c']]).toEqual([a, b, c]);
expect([a, b, c]).toEqual([12, 12, 12]);
});


it('should auto convert ints to strings', () {
expect(eval("'str ' + 4")).toEqual("str 4");
expect(eval("4 + ' str'")).toEqual("4 str");
Expand Down Expand Up @@ -160,6 +180,12 @@ main() {
});


it('should throw on incorrect ternary operator syntax', () {
expectEval("true?1").toThrow(errStr(
'Conditional expression true?1 requires all 3 expressions'));
});


it('should fail gracefully when missing a function', () {
expect(() {
parser('doesNotExist()').eval({});
Expand Down Expand Up @@ -330,6 +356,76 @@ main() {
});


it('should parse ternary', () {
var returnTrue = scope['returnTrue'] = () => true;
var returnFalse = scope['returnFalse'] = () => false;
var returnString = scope['returnString'] = () => 'asd';
var returnInt = scope['returnInt'] = () => 123;
var identity = scope['identity'] = (x) => x;
var B = toBool;

// Simple.
expect(eval('0?0:2')).toEqual(B(0)?0:2);
expect(eval('1?0:2')).toEqual(B(1)?0:2);

// Nested on the left.
expect(eval('0?0?0:0:2')).toEqual(B(0)?B(0)?0:0:2);
expect(eval('1?0?0:0:2')).toEqual(B(1)?B(0)?0:0:2);
expect(eval('0?1?0:0:2')).toEqual(B(0)?B(1)?0:0:2);
expect(eval('0?0?1:0:2')).toEqual(B(0)?B(0)?1:0:2);
expect(eval('0?0?0:2:3')).toEqual(B(0)?B(0)?0:2:3);
expect(eval('1?1?0:0:2')).toEqual(B(1)?B(1)?0:0:2);
expect(eval('1?1?1:0:2')).toEqual(B(1)?B(1)?1:0:2);
expect(eval('1?1?1:2:3')).toEqual(B(1)?B(1)?1:2:3);
expect(eval('1?1?1:2:3')).toEqual(B(1)?B(1)?1:2:3);

// Nested on the right.
expect(eval('0?0:0?0:2')).toEqual(B(0)?0:B(0)?0:2);
expect(eval('1?0:0?0:2')).toEqual(B(1)?0:B(0)?0:2);
expect(eval('0?1:0?0:2')).toEqual(B(0)?1:B(0)?0:2);
expect(eval('0?0:1?0:2')).toEqual(B(0)?0:B(1)?0:2);
expect(eval('0?0:0?2:3')).toEqual(B(0)?0:B(0)?2:3);
expect(eval('1?1:0?0:2')).toEqual(B(1)?1:B(0)?0:2);
expect(eval('1?1:1?0:2')).toEqual(B(1)?1:B(1)?0:2);
expect(eval('1?1:1?2:3')).toEqual(B(1)?1:B(1)?2:3);
expect(eval('1?1:1?2:3')).toEqual(B(1)?1:B(1)?2:3);

// Precedence with respect to logical operators.
expect(eval('0&&1?0:1')).toEqual(B(0)&&B(1)?0:1);
expect(eval('1||0?0:0')).toEqual(B(1)||B(0)?0:0);

expect(eval('0?0&&1:2')).toEqual(B(0)?0&&1:2);
expect(eval('0?1&&1:2')).toEqual(B(0)?1&&1:2);
expect(eval('0?0||0:1')).toEqual(B(0)?0||0:1);
expect(eval('0?0||1:2')).toEqual(B(0)?0||1:2);

expect(eval('1?0&&1:2')).toEqual(B(1)?B(0)&&B(1):2);
expect(eval('1?1&&1:2')).toEqual(B(1)?B(1)&&B(1):2);
expect(eval('1?0||0:1')).toEqual(B(1)?B(0)||B(0):1);
expect(eval('1?0||1:2')).toEqual(B(1)?B(0)||B(1):2);

expect(eval('0?1:0&&1')).toEqual(B(0)?1:B(0)&&B(1));
expect(eval('0?2:1&&1')).toEqual(B(0)?2:B(1)&&B(1));
expect(eval('0?1:0||0')).toEqual(B(0)?1:B(0)||B(0));
expect(eval('0?2:0||1')).toEqual(B(0)?2:B(0)||B(1));

expect(eval('1?1:0&&1')).toEqual(B(1)?1:B(0)&&B(1));
expect(eval('1?2:1&&1')).toEqual(B(1)?2:B(1)&&B(1));
expect(eval('1?1:0||0')).toEqual(B(1)?1:B(0)||B(0));
expect(eval('1?2:0||1')).toEqual(B(1)?2:B(0)||B(1));

// Function calls.
expect(eval('returnTrue() ? returnString() : returnInt()')).toEqual(
returnTrue() ? returnString() : returnInt());
expect(eval('returnFalse() ? returnString() : returnInt()')).toEqual(
returnFalse() ? returnString() : returnInt());
expect(eval('returnTrue() ? returnString() : returnInt()')).toEqual(
returnTrue() ? returnString() : returnInt());
expect(eval('identity(returnFalse() ? returnString() : returnInt())')).toEqual(
identity(returnFalse() ? returnString() : returnInt()));
});


it('should parse string', () {
expect(eval("'a' + 'b c'")).toEqual("ab c");
});
Expand Down

0 comments on commit e38da6f

Please sign in to comment.