def test_parse_side_by_side_symbols_should_raise_exception_but_not2(self): algebra = BooleanAlgebra() expr_str = '(a or b) c' try: algebra.parse(expr_str) except ParseError as pe: assert pe.error_code == PARSE_INVALID_EXPRESSION
def test_or(self): algebra = BooleanAlgebra() exp = algebra.parse("a|b|c") for a in [True, False]: for b in [True, False]: for c in [True, False]: self.assertEqual(exp(a=a, b=b, c=c), a or b or c)
def test_parse_side_by_side_symbols_with_parens_raise_exception(self): algebra = BooleanAlgebra() expr_str = '(a) (b)' try: algebra.parse(expr_str) except ParseError as pe: assert pe.error_code == PARSE_INVALID_NESTING
def test_parse_side_by_side_symbols_raise_exception(self): algebra = BooleanAlgebra() expr_str = 'a b' try: algebra.parse(expr_str) except ParseError as pe: assert pe.error_code == PARSE_INVALID_SYMBOL_SEQUENCE
def test_parse_with_mixed_operators_multilines_and_custom_symbol(self): class MySymbol(Symbol): pass expr_str = '''(a or ~ b +_c ) and d & ( ! e_ | (my * g OR 1 or 0) ) AND that ''' algebra = BooleanAlgebra(Symbol_class=MySymbol) expr = algebra.parse(expr_str) expected = algebra.AND( algebra.OR( algebra.Symbol('a'), algebra.NOT(algebra.Symbol('b')), algebra.Symbol('_c'), ), algebra.Symbol('d'), algebra.OR( algebra.NOT(algebra.Symbol('e_')), algebra.OR( algebra.AND( algebra.Symbol('my'), algebra.Symbol('g'), ), algebra.TRUE, algebra.FALSE, ), ), algebra.Symbol('that'), ) self.assertEqual(expected.pretty(), expr.pretty()) self.assertEqual(expected, expr)
def test_simplify_complex_expression_parsed_with_simplify(self): # FIXME: THIS SHOULD NOT FAIL algebra = BooleanAlgebra() a = algebra.Symbol('a') b = algebra.Symbol('b') c = algebra.Symbol('c') d = algebra.Symbol('d') test_expression_str = ''' (~a&~b&~c&~d) | (~a&~b&~c&d) | (~a&b&~c&~d) | (~a&b&c&d) | (~a&b&~c&d) | (~a&b&c&~d) | (a&~b&~c&d) | (~a&b&c&d) | (a&~b&c&d) | (a&b&c&d) ''' parsed = algebra.parse(test_expression_str, simplify=True) test_expression = ((~a & ~b & ~c & ~d) | (~a & ~b & ~c & d) | (~a & b & ~c & ~d) | (~a & b & c & d) | (~a & b & ~c & d) | (~a & b & c & ~d) | (a & ~b & ~c & d) | (~a & b & c & d) | (a & ~b & c & d) | (a & b & c & d)).simplify() # we have a different simplify behavior for expressions built from python expressions # vs. expression built from an object tree vs. expression built from a parse self.assertEqual(parsed.pretty(), test_expression.pretty())
def test_demorgan(self): algebra = BooleanAlgebra() a = algebra.Symbol('a') b = algebra.Symbol('b') self.assertEqual(algebra.parse('~(a&b)').demorgan(), ~a | ~b) self.assertEqual(algebra.parse('~(a|b|c)').demorgan(), algebra.parse('~a&~b&~c')) self.assertEqual(algebra.parse('~(~a&b)').demorgan(), a | ~b)
def test_cancel(self): algebra = BooleanAlgebra() a = algebra.Symbol('a') self.assertEqual(~a, (~a).cancel()) self.assertEqual(a, algebra.parse('~~a').cancel()) self.assertEqual(~a, algebra.parse('~~~a').cancel()) self.assertEqual(a, algebra.parse('~~~~a').cancel())
def test_dual(self): algebra = BooleanAlgebra() self.assertEqual(algebra.AND(algebra.Symbol('a'), algebra.Symbol('b')).dual, algebra.OR) self.assertEqual(algebra.OR(algebra.Symbol('a'), algebra.Symbol('b')).dual, algebra.AND) self.assertEqual(algebra.parse('a|b').dual, algebra.AND) self.assertEqual(algebra.parse('a&b').dual, algebra.OR)
def test_simplify_complex_expression_parsed_with_simplify(self): # FIXME: THIS SHOULD NOT FAIL algebra = BooleanAlgebra() a = algebra.Symbol('a') b = algebra.Symbol('b') c = algebra.Symbol('c') d = algebra.Symbol('d') test_expression_str = ''' (~a&~b&~c&~d) | (~a&~b&~c&d) | (~a&b&~c&~d) | (~a&b&c&d) | (~a&b&~c&d) | (~a&b&c&~d) | (a&~b&~c&d) | (~a&b&c&d) | (a&~b&c&d) | (a&b&c&d) ''' parsed = algebra.parse(test_expression_str, simplify=True) test_expression = ( (~a & ~b & ~c & ~d) | (~a & ~b & ~c & d) | (~a & b & ~c & ~d) | (~a & b & c & d) | (~a & b & ~c & d) | (~a & b & c & ~d) | (a & ~b & ~c & d) | (~a & b & c & d) | (a & ~b & c & d) | (a & b & c & d) ).simplify() # we have a different simplify behavior for expressions built from python expressions # vs. expression built from an object tree vs. expression built from a parse self.assertEqual(parsed.pretty(), test_expression.pretty())
def test_and(self): algebra = BooleanAlgebra() exp = algebra.parse("a&b&c") for a in [True, False]: for b in [True, False]: for c in [True, False]: self.assertEqual(exp(a=a, b=b, c=c), a and b and c)
def test_parse_invalid_nested_and_should_raise_a_proper_exception(self): algebra = BooleanAlgebra() expr = '''a (and b)''' try: algebra.parse(expr) except ParseError as pe: assert pe.error_code == PARSE_INVALID_NESTING
def test_subs_default(self): algebra = BooleanAlgebra() a, b, c = algebra.Symbol('a'), algebra.Symbol('b'), algebra.Symbol('c') expr = a & b | c self.assertEqual(expr.subs({}, default=algebra.TRUE).simplify(), algebra.TRUE) self.assertEqual(expr.subs({a: algebra.FALSE, c: algebra.FALSE}, default=algebra.TRUE).simplify(), algebra.FALSE) self.assertEqual(algebra.TRUE.subs({}, default=algebra.FALSE).simplify(), algebra.TRUE) self.assertEqual(algebra.FALSE.subs({}, default=algebra.TRUE).simplify(), algebra.FALSE)
def test_printing(self): algebra = BooleanAlgebra() a = algebra.Symbol('a') self.assertEqual(str(~a), '~a') self.assertEqual(repr(~a), "NOT(Symbol('a'))") expr = algebra.parse('~(a&a)') self.assertEqual(str(expr), '~(a&a)') self.assertEqual(repr(expr), "NOT(AND(Symbol('a'), Symbol('a')))")
def test_composite(self): algebra = BooleanAlgebra() exp = algebra.parse("!(a|b&(a|!c))") for a in [True, False]: for b in [True, False]: for c in [True, False]: self.assertEqual(exp(a=a, b=b, c=c), not (a or b and (a or not c)))
def test_parse_raise_ParseError9(self): algebra = BooleanAlgebra() expr = '+ l-a' try: algebra.parse(expr) self.fail("Exception should be raised when parsing '%s'" % expr) except ParseError as pe: assert pe.error_code == PARSE_INVALID_OPERATOR_SEQUENCE
def test_parse_raise_ParseError7(self): algebra = BooleanAlgebra() expr = 'l-a AND' try: algebra.parse(expr) self.fail("Exception should be raised when parsing '%s'" % expr) except ParseError as pe: assert pe.error_code == PARSE_UNKNOWN_TOKEN
def test_order(self): algebra = BooleanAlgebra() x = algebra.Symbol(1) y = algebra.Symbol(2) self.assertTrue(x < ~x) self.assertTrue(~x > x) self.assertTrue(~x < y) self.assertTrue(y > ~x)
def test_subs(self): algebra = BooleanAlgebra() a, b, c = algebra.Symbol('a'), algebra.Symbol('b'), algebra.Symbol('c') expr = a & b | c self.assertEqual(expr.subs({a: b}).simplify(), b | c) self.assertEqual(expr.subs({a: a}).simplify(), expr) self.assertEqual(expr.subs({a: b | c}).simplify(), algebra.parse('(b|c)&b|c').simplify()) self.assertEqual(expr.subs({a & b: a}).simplify(), a | c) self.assertEqual(expr.subs({c: algebra.TRUE}).simplify(), algebra.TRUE)
def test_get_symbols_return_all_symbols_in_original_order(self): alg = BooleanAlgebra() exp = alg.parse('a and b or True and a and c') assert [ alg.Symbol('a'), alg.Symbol('b'), alg.Symbol('a'), alg.Symbol('c') ] == exp.get_symbols()
def test_parse_invalid_nested_and_should_raise_a_proper_exception(self): algebra = BooleanAlgebra() expr = '''a (and b)''' with self.assertRaises(ParseError) as context: algebra.parse(expr) self.assertEqual(context.exception.error_code, PARSE_INVALID_NESTING)
def test_allowing_additional_characters_in_tokens(self): algebra = BooleanAlgebra(allowed_in_token=('.', '_', '-', '+')) test_expr = 'l-a AND b+c' expr = algebra.parse(test_expr) expected = algebra.AND( algebra.Symbol('l-a'), algebra.Symbol('b+c') ) self.assertEqual(expected, expr)
def test_distributive(self): algebra = BooleanAlgebra() a = algebra.Symbol('a') b = algebra.Symbol('b') c = algebra.Symbol('c') d = algebra.Symbol('d') e = algebra.Symbol('e') self.assertEqual((a & (b | c)).distributive(), (a & b) | (a & c)) t1 = algebra.AND(a, (b | c), (d | e)) t2 = algebra.OR(algebra.AND(a, b, d), algebra.AND(a, b, e), algebra.AND(a, c, d), algebra.AND(a, c, e)) self.assertEqual(t1.distributive(), t2)
def test_simplify(self): algebra = BooleanAlgebra() a = algebra.Symbol('a') self.assertEqual(~a, ~a) assert algebra.Symbol('a') == algebra.Symbol('a') self.assertNotEqual(a, algebra.parse('~~a')) self.assertEqual(a, (~~a).simplify()) self.assertEqual(~a, (~~ ~a).simplify()) self.assertEqual(a, (~~ ~~a).simplify()) self.assertEqual((~(a & a & a)).simplify(), (~(a & a & a)).simplify()) self.assertEqual(a, algebra.parse('~~a', simplify=True))
def test_simplify(self): algebra = BooleanAlgebra() a = algebra.Symbol('a') self.assertEqual(~a, ~a) assert algebra.Symbol('a') == algebra.Symbol('a') self.assertNotEqual(a, algebra.parse('~~a')) self.assertEqual(a, (~~a).simplify()) self.assertEqual(~a, (~~~a).simplify()) self.assertEqual(a, (~~~~a).simplify()) self.assertEqual((~(a & a & a)).simplify(), (~(a & a & a)).simplify()) self.assertEqual(a, algebra.parse('~~a', simplify=True)) algebra2 = BooleanAlgebra() self.assertEqual(a, algebra2.parse('~~a', simplify=True))
def test_literals(self): algebra = BooleanAlgebra() a = algebra.Symbol('a') l = ~a self.assertTrue(l.isliteral) self.assertTrue(l in l.literals) self.assertEqual(len(l.literals), 1) l = algebra.parse('~(a&a)') self.assertFalse(l.isliteral) self.assertTrue(a in l.literals) self.assertEqual(len(l.literals), 1) l = algebra.parse('~(a&a)', simplify=True) self.assertTrue(l.isliteral)
def test_parse_recognizes_trueish_and_falsish_symbol_tokens(self): expr_str = 'True or False or None or 0 or 1 or TRue or FalSE or NONe' algebra = BooleanAlgebra() expr = algebra.parse(expr_str) expected = algebra.OR( algebra.TRUE, algebra.FALSE, algebra.FALSE, algebra.FALSE, algebra.TRUE, algebra.TRUE, algebra.FALSE, algebra.FALSE, ) self.assertEqual(expected, expr)
def test_bool(self): algebra = BooleanAlgebra() a, b, c = algebra.Symbol('a'), algebra.Symbol('b'), algebra.Symbol('c') expr = a & b | c self.assertRaises(TypeError, bool, expr.subs({a: algebra.TRUE})) self.assertRaises(TypeError, bool, expr.subs({b: algebra.TRUE})) self.assertRaises(TypeError, bool, expr.subs({c: algebra.TRUE})) self.assertRaises(TypeError, bool, expr.subs({a: algebra.TRUE, b: algebra.TRUE})) result = expr.subs({c: algebra.TRUE}, simplify=True) result = result.simplify() self.assertEqual(algebra.TRUE, result) result = expr.subs({a: algebra.TRUE, b: algebra.TRUE}, simplify=True) result = result.simplify() self.assertEqual(algebra.TRUE, result)
def test_demorgan(self): algebra = BooleanAlgebra() a = algebra.Symbol('a') b = algebra.Symbol('b') c = algebra.Symbol('c') self.assertEqual(algebra.parse('~(a&b)').demorgan(), ~a | ~b) self.assertEqual(algebra.parse('~(a|b|c)').demorgan(), algebra.parse('~a&~b&~c')) self.assertEqual(algebra.parse('~(~a&b)').demorgan(), a | ~b) self.assertEqual((~~(a&b|c)).demorgan(), a&b|c) self.assertEqual((~~~(a&b|c)).demorgan(), ~(a&b)&~c) self.assertEqual(algebra.parse('~'*10 + '(a&b|c)').demorgan(), a&b|c) self.assertEqual(algebra.parse('~'*11 + '(a&b|c)').demorgan(), (~(a&b|c)).demorgan())
def test_class_order(self): # FIXME: this test is cryptic: what does it do? algebra = BooleanAlgebra() order = ( (algebra.TRUE, algebra.FALSE), (algebra.Symbol('y'), algebra.Symbol('x')), (algebra.parse('x&y'),), (algebra.parse('x|y'),), ) for i, tests in enumerate(order): for case1 in tests: for j in range(i + 1, len(order)): for case2 in order[j]: self.assertTrue(case1 < case2) self.assertTrue(case2 > case1)
def test_class_order(self): # FIXME: this test is cryptic: what does it do? algebra = BooleanAlgebra() order = ( (algebra.TRUE, algebra.FALSE), (algebra.Symbol('y'), algebra.Symbol('x')), (algebra.parse('x&y'), ), (algebra.parse('x|y'), ), ) for i, tests in enumerate(order): for case1 in tests: for j in range(i + 1, len(order)): for case2 in order[j]: self.assertTrue(case1 < case2) self.assertTrue(case2 > case1)
def test_parse_complex_expression_should_create_same_expression_as_python(self): algebra = BooleanAlgebra() a, b, c = algebra.symbols(*'abc') test_expression_str = '''(~a | ~b | ~c)''' parsed = algebra.parse(test_expression_str) test_expression = (~a | ~b | ~c) # & ~d # print() # print('parsed') # print(parsed.pretty()) # print('python') # print(test_expression.pretty()) # we have a different behavior for expressions built from python expressions # vs. expression built from an object tree vs. expression built from a parse self.assertEqual(parsed.pretty(), test_expression.pretty()) self.assertEqual(parsed, test_expression)
def test_printing(self): parse = BooleanAlgebra().parse self.assertEqual(str(parse('a&a')), 'a&a') self.assertEqual(repr(parse('a&a')), "AND(Symbol('a'), Symbol('a'))") self.assertEqual(str(parse('a|a')), 'a|a') self.assertEqual(repr(parse('a|a')), "OR(Symbol('a'), Symbol('a'))") self.assertEqual(str(parse('(a|b)&c')), '(a|b)&c') self.assertEqual(repr(parse('(a|b)&c')), "AND(OR(Symbol('a'), Symbol('b')), Symbol('c'))")
def test_subtract(self): parse = BooleanAlgebra().parse expr = parse('a&b&c') p1 = parse('b&d') p2 = parse('a&c') result = parse('b') self.assertEqual(expr.subtract(p1, simplify=True), expr) self.assertEqual(expr.subtract(p2, simplify=True), result)
def test_simplify(self): algebra = BooleanAlgebra() a = algebra.Symbol('a') b = algebra.Symbol('b') c = algebra.Symbol('c') _0 = algebra.FALSE _1 = algebra.TRUE # Idempotence self.assertEqual(a, (a & a).simplify()) # Idempotence + Associativity self.assertEqual(a | b, (a | (a | b)).simplify()) # Annihilation self.assertEqual(_0, (a & _0).simplify()) self.assertEqual(_1, (a | _1).simplify()) # Identity self.assertEqual(a, (a & _1).simplify()) self.assertEqual(a, (a | _0).simplify()) # Complementation self.assertEqual(_0, (a & ~a).simplify()) self.assertEqual(_1, (a | ~a).simplify()) # Absorption self.assertEqual(a, (a & (a | b)).simplify()) self.assertEqual(a, (a | (a & b)).simplify()) self.assertEqual(b & a, ((b & a) | (b & a & c)).simplify()) # Elimination self.assertEqual(a, ((a & ~b) | (a & b)).simplify()) expected = algebra.parse('(a&b)|(b&c)|(a&c)') result = algebra.parse('(~a&b&c) | (a&~b&c) | (a&b&~c) | (a&b&c)', simplify=True) self.assertEqual(expected, result) expected = algebra.parse('b&d') result = algebra.parse('(a&b&c&d) | (b&d)', simplify=True) self.assertEqual(expected, result) expected = algebra.parse('(~b&~d&a) | (~c&~d&b) | (a&c&d)', simplify=True) result = algebra.parse('''(~a&b&~c&~d) | (a&~b&~c&~d) | (a&~b&c&~d) | (a&~b&c&d) | (a&b&~c&~d) | (a&b&c&d)''', simplify=True) self.assertEqual(expected.pretty(), result.pretty())
def test_demorgan(self): algebra = BooleanAlgebra() a = algebra.Symbol('a') b = algebra.Symbol('b') self.assertEqual(algebra.parse('~(a&b)').demorgan(), ~a | ~b) self.assertEqual( algebra.parse('~(a|b|c)').demorgan(), algebra.parse('~a&~b&~c')) self.assertEqual(algebra.parse('~(~a&b)').demorgan(), a | ~b)
def test_flatten(self): parse = BooleanAlgebra().parse t1 = parse('a & (b&c)') t2 = parse('a&b&c') self.assertNotEqual(t1, t2) self.assertEqual(t1.flatten(), t2) t1 = parse('a | ((b&c) | (a&c)) | b') t2 = parse('a | (b&c) | (a&c) | b') self.assertNotEqual(t1, t2) self.assertEqual(t1.flatten(), t2)
def test_parse_raise_ParseError(self): algebra = BooleanAlgebra() invalid_expressions = [ 'l-a AND none', '(l-a + AND l-b', '(l-a + AND l-b)', '(l-a AND l-b', '(l-a + AND l-b))', '(l-a AND l-b))', 'l-a AND', 'OR l-a', '+ l-a', ] for expr in invalid_expressions: print(expr) try: algebra.parse(expr) self.fail("Exception should be raised when parsing '%s'" % expr) except ParseError: pass
def test_complex_expression_without_parens_parsed_or_built_in_python_should_be_identical(self): # FIXME: THIS SHOULD NOT FAIL algebra = BooleanAlgebra() a = algebra.Symbol('a') b = algebra.Symbol('b') c = algebra.Symbol('c') d = algebra.Symbol('d') test_expression_str = ''' ~a&~b&~c&~d | ~a&~b&~c&d | ~a&b&~c&~d | ~a&b&c&d | ~a&b&~c&d | ~a&b&c&~d | a&~b&~c&d | ~a&b&c&d | a&~b&c&d | a&b&c&d ''' parsed = algebra.parse(test_expression_str) test_expression = ( ~a & ~b & ~c & ~d | ~a & ~b & ~c & d | ~a & b & ~c & ~d | ~ a & b & c & d | ~a & b & ~c & d | ~a & b & c & ~d | a & ~b & ~c & d | ~a & b & c & d | a & ~b & c & d | a & b & c & d ) self.assertEqual(parsed.pretty(), test_expression.pretty())
def test_parse(self): algebra = BooleanAlgebra() a, b, c = algebra.Symbol('a'), algebra.Symbol('b'), algebra.Symbol('c') self.assertEqual(algebra.parse('0'), algebra.FALSE) self.assertEqual(algebra.parse('(0)'), algebra.FALSE) self.assertEqual(algebra.parse('1') , algebra.TRUE) self.assertEqual(algebra.parse('(1)'), algebra.TRUE) self.assertEqual(algebra.parse('a'), a) self.assertEqual(algebra.parse('(a)'), a) self.assertEqual(algebra.parse('(a)'), a) self.assertEqual(algebra.parse('~a'), algebra.parse('~(a)')) self.assertEqual(algebra.parse('~(a)'), algebra.parse('(~a)')) self.assertEqual(algebra.parse('~a'), ~a) self.assertEqual(algebra.parse('(~a)'), ~a) self.assertEqual(algebra.parse('~~a', simplify=True), (~~a).simplify()) self.assertEqual(algebra.parse('a&b'), a & b) self.assertEqual(algebra.parse('~a&b'), ~a & b) self.assertEqual(algebra.parse('a&~b'), a & ~b) self.assertEqual(algebra.parse('a&b&c'), algebra.parse('a&b&c')) self.assertEqual(algebra.parse('a&b&c'), algebra.AND(a, b, c)) self.assertEqual(algebra.parse('~a&~b&~c'), algebra.parse('~a&~b&~c')) self.assertEqual(algebra.parse('~a&~b&~c'), algebra.AND(~a, ~b, ~c)) self.assertEqual(algebra.parse('a|b'), a | b) self.assertEqual(algebra.parse('~a|b'), ~a | b) self.assertEqual(algebra.parse('a|~b'), a | ~b) self.assertEqual(algebra.parse('a|b|c'), algebra.parse('a|b|c')) self.assertEqual(algebra.parse('a|b|c'), algebra.OR(a, b, c)) self.assertEqual(algebra.parse('~a|~b|~c'), algebra.OR(~a, ~b, ~c)) self.assertEqual(algebra.parse('(a|b)'), a | b) self.assertEqual(algebra.parse('a&(a|b)', simplify=True), (a & (a | b)).simplify()) self.assertEqual(algebra.parse('a&(a|~b)', simplify=True), (a & (a | ~b)).simplify()) self.assertEqual(algebra.parse('(a&b)|(b&((c|a)&(b|(c&a))))', simplify=True), ((a & b) | (b & ((c | a) & (b | (c & a))))).simplify()) self.assertEqual(algebra.parse('(a&b)|(b&((c|a)&(b|(c&a))))', simplify=True), algebra.parse('a&b | b&(c|a)&(b|c&a)', simplify=True))
def test_objects_return_set_of_unique_Symbol_objs(self): alg = BooleanAlgebra() exp = alg.parse('a and b or a and c') assert set(['a', 'b', 'c']) == exp.objects
def test_literals_return_set_of_unique_literals(self): alg = BooleanAlgebra() exp = alg.parse('a and b or a and c') assert set([alg.Symbol('a'), alg.Symbol('b'), alg.Symbol('c')]) == exp.literals
def test_get_symbols_return_all_symbols_in_original_order(self): alg = BooleanAlgebra() exp = alg.parse('a and b or True and a and c') assert [alg.Symbol('a'), alg.Symbol('b'), alg.Symbol('a'), alg.Symbol('c')] == exp.get_symbols()
def test_isliteral(self): algebra = BooleanAlgebra() s = algebra.Symbol(1) self.assertTrue(algebra.NOT(s).isliteral) self.assertFalse(algebra.parse('~(a|b)').isliteral)
def test_annihilator(self): algebra = BooleanAlgebra() self.assertEqual(algebra.parse('a&a').annihilator, algebra.FALSE) self.assertEqual(algebra.parse('a|a').annihilator, algebra.TRUE)
def test_identity(self): algebra = BooleanAlgebra() self.assertEqual(algebra.parse('a|b').identity, algebra.FALSE) self.assertEqual(algebra.parse('a&b').identity, algebra.TRUE)
def test_normalize(self): algebra = BooleanAlgebra() expr = algebra.parse('((s|a)&(s|b)&(s|c)&(s|d)&(e|c|d))|(a&e&d)') result = algebra.normalize(expr, expr.AND) expected = algebra.parse('(a|s)&(b|e|s)&(c|d|e)&(c|e|s)&(d|s)') self.assertEqual(result, expected)
def test_creation(self): algebra = BooleanAlgebra() expr_str = '(a|b|c)&d&(~e|(f&g))' expr = algebra.parse(expr_str) self.assertEqual(expr_str, str(expr))