Expression basics

At tt’s core is the concept of the Boolean expression, encapsulated in this library with the BooleanExpression class. Let’s take look at what we can do with expressions.

Creating an expression object

The top-level class for interacting with boolean expressions in tt is, fittingly named, BooleanExpression. Let’s start by importing it:

>>> from tt import BooleanExpression

This class accepts boolean expressions as strings and provides the interface for parsing and tokenizing string expressions into a sequence of tokens and symbols, as we see here:

>>> b = BooleanExpression('(A nand B) or (C and D)')
>>> b.tokens
['(', 'A', 'nand', 'B', ')', 'or', '(', 'C', 'and', 'D', ')']
>>> b.symbols
['A', 'B', 'C', 'D']

We can also always retrieve the original string we passed in via the raw_expr attribute:

>>> b.raw_expr
'(A nand B) or (C and D)'

During initialization, the BooleanExpression also does some work behind the scenes to build a basic understanding of the expression’s structure. It re-orders the tokens into postfix order, and uses this representation to build a ExpressionTreeNode. We can see this with:

>>> b.postfix_tokens
['A', 'B', 'nand', 'C', 'D', 'and', 'or']
>>> print(b.tree)
|    `----A
|    `----B

This expression tree represents tt’s understanding of the structure of your expression. If you are receiving an unexpected error for a more complicated expression, inspecting the tree attribute on the BooleanExpression instance can be a good starting point for debugging the issue.

Evaluating expressions

Looking at expression symbols and tokens is nice, but we need some real functionality for our expressions; a natural starting point is the ability to evaluate expressions. A BooleanExpression object provides an interface to this evaluation functionality; use it like this:

>>> b.evaluate(A=True, B=False, C=True, D=False)
>>> b.evaluate(A=1, B=0, C=1, D=0)

Notice that we can use 0 or False to represent low values and 1 or True to represent high values. tt makes sure that only valid Boolean-esque values are accepted for evaluation. For example, if we tried something like:

>>> b.evaluate(A=1, B='not a Boolean value', C=0, D=0)
Traceback (most recent call last):
tt.errors.evaluation.InvalidBooleanValueError: "not a Boolean value" passed as value for "B" is not a valid Boolean value

or if we didn’t include a value for each of the symbols:

>>> b.evaluate(A=1, B=0, C=0)
Traceback (most recent call last):
tt.errors.symbols.MissingSymbolError: Did not receive value for the following symbols: "D"

These exceptions can be nice if you aren’t sure about your input, but if you think this safety is just adding overhead for you, there’s a way to skip those extra checks:

>>> b.evaluate_unchecked(A=0, B=0, C=1, D=0)

Handling malformed expressions

So far, we’ve only seen one example of a BooleanExpression instance, and we passed a valid expression string to it. What happens when we pass in a malformed expression? And what does tt even consider to be a malformed expression?

While there is no explicit grammar for expressions in tt, using your best judgement will work most of the time. Most well-known Boolean expression operators are available in plain-English and symbolic form. You can see the list of available operators like so:

>>> from tt import OPERATOR_MAPPING
>>> print(', '.join(sorted(OPERATOR_MAPPING.keys())))
!, &, &&, ->, /\, <->, AND, IFF, IMPL, NAND, NOR, NOT, NXOR, OR, XNOR, XOR, \/, and, iff, impl, nand, nor, not, nxor, or, xnor, xor, |, ||, ~

Another possible source of errors in your expressions will be invalid symbol names. Due to some functionality based on accessing symbol names from namedtuple-like objects, symbol names must meet the following criteria:

  1. Must be a valid Python identifiers.
  2. Cannot be a Python keyword.
  3. Cannot begin with an underscore

An exception will be raised if a symbol name in your expression does not meet the above criteria. Fortunately, tt provides a way for us to check if our symbols are valid. Let’s take a look:

>>> from tt import is_valid_identifier
>>> is_valid_identifier('False')
>>> is_valid_identifier('_bad')
>>> is_valid_identifier('not$good')
>>> is_valid_identifier('a_good_symbol_name')
>>> b = BooleanExpression('_A or B')
Traceback (most recent call last):
tt.errors.grammar.InvalidIdentifierError: Invalid operand name "_A"

As we saw in the above example, we caused an error from the tt.errors.grammar module. If you play around with invalid expressions, you’ll notice that all of these errors come from that module; that’s because errors in this logical group are all descendants of GrammarError. This is the type of error that lexical expression errors will fall under:

>>> from tt import GrammarError
>>> invalid_expressions = ['A xor or B', 'A or ((B nand C)', 'A or B B']
>>> for expr in invalid_expressions:
...     try:
...             b = BooleanExpression(expr)
...     except Exception as e:
...             print(type(e))
...             print(isinstance(e, GrammarError))
<class 'tt.errors.grammar.ExpressionOrderError'>
<class 'tt.errors.grammar.UnbalancedParenError'>
<class 'tt.errors.grammar.ExpressionOrderError'>

GrammarError is a unique type of exception in tt, as it provides attributes for accessing the specific position in the expression string that caused an error. This is best illustrated with an example:

>>> try:
...     b = BooleanExpression('A or or B')
... except GrammarError as e:
...     print("Here's what happened:")
...     print(e.message)
...     print("Here's where it happened:")
...     print(e.expr_str)
...     print(' '*e.error_pos + '^')
Here's what happened:
Unexpected binary operator "or"
Here's where it happened:
A or or B