From e38da6f542aa3a0c3e29b614ed444feee3e8ef4c Mon Sep 17 00:00:00 2001 From: Chirayu Krishnappa Date: Thu, 21 Nov 2013 00:09:31 -0800 Subject: [PATCH] feat($parse): support the ternary/conditional operator Closes #272 --- lib/core/parser/backend.dart | 4 ++ lib/core/parser/parser.dart | 24 +++++++- test/core/parser/lexer_spec.dart | 6 -- test/core/parser/parser_spec.dart | 98 ++++++++++++++++++++++++++++++- 4 files changed, 122 insertions(+), 10 deletions(-) diff --git a/lib/core/parser/backend.dart b/lib/core/parser/backend.dart index 30cb4b13a..ff0fde0c1 100644 --- a/lib/core/parser/backend.dart +++ b/lib/core/parser/backend.dart @@ -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)); diff --git a/lib/core/parser/parser.dart b/lib/core/parser/parser.dart index 98bc5950c..1e018351c 100644 --- a/lib/core/parser/parser.dart +++ b/lib/core/parser/parser.dart @@ -128,7 +128,8 @@ Map 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 { @@ -343,9 +344,26 @@ 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; @@ -353,7 +371,7 @@ class DynamicParser implements Parser { 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; diff --git a/test/core/parser/lexer_spec.dart b/test/core/parser/lexer_spec.dart index 403b66419..83ec3c05d 100644 --- a/test/core/parser/lexer_spec.dart +++ b/test/core/parser/lexer_spec.dart @@ -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]'); - }); }); } diff --git a/test/core/parser/parser_spec.dart b/test/core/parser/parser_spec.dart index 8f8daaa93..56d20a298 100644 --- a/test/core/parser/parser_spec.dart +++ b/test/core/parser/parser_spec.dart @@ -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; @@ -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)); @@ -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"); @@ -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({}); @@ -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"); });