Implemented basic symbolic equations and added Sympy to deps.

parent fd911e2e
...@@ -26,6 +26,7 @@ Dependencies ...@@ -26,6 +26,7 @@ Dependencies
- GNU Bison and Flex. - GNU Bison and Flex.
- GNU GCC (or another decent C compiler). - GNU GCC (or another decent C compiler).
- Pyrex <http://www.cosc.canterbury.ac.nz/greg.ewing/python/Pyrex/>. - Pyrex <http://www.cosc.canterbury.ac.nz/greg.ewing/python/Pyrex/>.
- Sympy <https://github.com/sympy/sympy>.
In order to run the unit tests and and see how much coverage the unit tests In order to run the unit tests and and see how much coverage the unit tests
have, please install: have, please install:
......
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
A simple pybison parser program implementing a calculator A simple pybison parser program implementing a calculator
""" """
from sympy import Symbol
from logger import filter_non_ascii
import os.path import os.path
PYBISON_BUILD = os.path.realpath('build/external/pybison') PYBISON_BUILD = os.path.realpath('build/external/pybison')
PYBISON_PYREX = os.path.realpath('external/pybison/src/pyrex') PYBISON_PYREX = os.path.realpath('external/pybison/src/pyrex')
...@@ -25,7 +28,7 @@ class Parser(BisonParser): ...@@ -25,7 +28,7 @@ class Parser(BisonParser):
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# lexer tokens - these must match those in your lex script (below) # lexer tokens - these must match those in your lex script (below)
# ---------------------------------------------------------------- # ----------------------------------------------------------------
tokens = ['NUMBER', tokens = ['NUMBER', 'IDENTIFIER',
'PLUS', 'MINUS', 'TIMES', 'DIVIDE', 'POW', 'PLUS', 'MINUS', 'TIMES', 'DIVIDE', 'POW',
'LPAREN', 'RPAREN', 'LPAREN', 'RPAREN',
'NEWLINE', 'QUIT'] 'NEWLINE', 'QUIT']
...@@ -51,7 +54,7 @@ class Parser(BisonParser): ...@@ -51,7 +54,7 @@ class Parser(BisonParser):
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def read(self, nbytes): def read(self, nbytes):
try: try:
return raw_input(">>> ") + "\n" return raw_input('>>> ') + "\n"
except EOFError: except EOFError:
return '' return ''
...@@ -75,8 +78,12 @@ class Parser(BisonParser): ...@@ -75,8 +78,12 @@ class Parser(BisonParser):
""" """
if option == 1: if option == 1:
# Interactive mode is enabled if the term rewriting system is used
# as a shell. In that case, it is useful that the shell prints the
# output of the evaluation.
if self.interactive: if self.interactive:
print values[1] print values[1]
return values[1] return values[1]
def on_line(self, target, option, names, values): def on_line(self, target, option, names, values):
...@@ -84,7 +91,7 @@ class Parser(BisonParser): ...@@ -84,7 +91,7 @@ class Parser(BisonParser):
line : NEWLINE line : NEWLINE
| exp NEWLINE | exp NEWLINE
""" """
if option == 1: if option in [1, 2]:
if self.verbose: if self.verbose:
print 'on_line: exp =', values[0] print 'on_line: exp =', values[0]
...@@ -93,6 +100,7 @@ class Parser(BisonParser): ...@@ -93,6 +100,7 @@ class Parser(BisonParser):
def on_exp(self, target, option, names, values): def on_exp(self, target, option, names, values):
""" """
exp : NUMBER exp : NUMBER
| IDENTIFIER
| exp PLUS exp | exp PLUS exp
| exp MINUS exp | exp MINUS exp
| exp TIMES exp | exp TIMES exp
...@@ -100,41 +108,81 @@ class Parser(BisonParser): ...@@ -100,41 +108,81 @@ class Parser(BisonParser):
| MINUS exp %prec NEG | MINUS exp %prec NEG
| exp POW exp | exp POW exp
| LPAREN exp RPAREN | LPAREN exp RPAREN
| symbolic
""" """
if self.verbose: if self.verbose:
print 'on_exp: got %s %s %s %s' % (target, option, names, values) print 'on_exp: got %s %s %s %s' % (target, option, names, values)
# rule: NUMBER
if option == 0: if option == 0:
# TODO: A bit hacky, this achieves long integers and floats. # TODO: A bit hacky, this achieves long integers and floats.
# return float(values[0]) if '.' in values[0] else long(values[0]) # return float(values[0]) if '.' in values[0] else long(values[0])
return float(values[0]) return float(values[0])
if option == 7: # rule: IDENTIFIER
if option == 1:
return Symbol(values[0])
# rule: LPAREN exp RPAREN
if option == 8:
return values[1]
# rule: symbolic
if option == 9:
return values[1] return values[1]
try: try:
if option == 1: # rule: exp PLUS expo
if option == 2:
return values[0] + values[2] return values[0] + values[2]
if option == 2: # rule: exp MINUS expo
if option == 3:
return values[0] - values[2] return values[0] - values[2]
if option == 3: # rule: exp TIMES expo
if option == 4:
return values[0] * values[2] return values[0] * values[2]
if option == 4: # rule: exp DIVIDE expo
if option == 5:
return values[0] / values[2] return values[0] / values[2]
if option == 5: # rule: NEG expo
if option == 6:
return - values[1] return - values[1]
if option == 6: # rule: exp POW expo
if option == 7:
return values[0] ** values[2] return values[0] ** values[2]
except OverflowError: except OverflowError:
print >>sys.stderr, 'error: Overflow occured in "%s" %s %s %s' \ print >>sys.stderr, 'error: Overflow occured in "%s" %s %s %s' \
% (target, option, names, values) % (target, option, names, values)
def on_symbolic(self, target, option, names, values):
"""
symbolic : NUMBER IDENTIFIER
| IDENTIFIER NUMBER
| IDENTIFIER IDENTIFIER
"""
# TODO: this class method requires verification.
# rule: NUMBER IDENTIFIER
if option == 0:
# 4x -> 4*x
return values[0] * Symbol(values[1])
# rule: IDENTIFIER NUMBER
if option == 1:
# x4 -> x^4
return Symbol(values[0]) ** values[1]
# rule: IDENTIFIER IDENTIFIER
if option == 2:
# a b -> a * b
return Symbol(values[0]) * Symbol(values[1])
# ----------------------------------------- # -----------------------------------------
# raw lex script, verbatim here # raw lex script, verbatim here
# ----------------------------------------- # -----------------------------------------
...@@ -155,6 +203,7 @@ class Parser(BisonParser): ...@@ -155,6 +203,7 @@ class Parser(BisonParser):
%% %%
[0-9]+ { returntoken(NUMBER); } [0-9]+ { returntoken(NUMBER); }
[a-zA-Z][a-zA-Z0-9]* { returntoken(IDENTIFIER); }
"(" { returntoken(LPAREN); } "(" { returntoken(LPAREN); }
")" { returntoken(RPAREN); } ")" { returntoken(RPAREN); }
"+" { returntoken(PLUS); } "+" { returntoken(PLUS); }
......
# Error/debug/information logging facilities.
LOG_FILE = 'engine.log'
LOG_FORMAT = '%(asctime)s %(name)-15s %(message)s'
...@@ -2,7 +2,11 @@ import logging ...@@ -2,7 +2,11 @@ import logging
import logging.config import logging.config
import sys import sys
import config try:
import config
except ImportError:
config = object()
import default_config as default import default_config as default
try: try:
...@@ -16,3 +20,7 @@ except IOError as e: # pragma: no cover ...@@ -16,3 +20,7 @@ except IOError as e: # pragma: no cover
def logger(name): def logger(name):
return logging.getLogger(name) return logging.getLogger(name)
def filter_non_ascii(data):
return ''.join(map(lambda x: 33 < ord(x) < 125 and x or '.', data))
import sys
from src.calc import Parser
class TestParser(Parser):
def __init__(self, **kwargs):
Parser.__init__(self, **kwargs)
self.input_buffer = []
self.input_position = 0
def run(self, input_buffer, *args, **kwargs):
map(self.append, input_buffer)
return Parser.run(self, *args, **kwargs)
def append(self, input):
self.input_buffer.append(input + '\n')
def read(self, nbytes):
buffer = ''
try:
buffer = self.input_buffer[self.input_position]
if self.verbose:
print 'read:', buffer
except IndexError:
return ''
self.input_position += 1
return buffer
def run_expressions(expressions, keepfiles=1, fail=True, silent=False,
verbose=0):
"""
Run a list of mathematical expression through the term rewriting system and
check if the output matches the expected output. The list of EXPRESSIONS
consists of tuples (expression, output), where expression is the
mathematical expression to evaluate (String) and output is the expected
output of the evaluation (thus, the output can be Float, Int or None).
If KEEPFILES is non-zero or True, the generated Flex and Bison files will
be kept. Otherwise, those temporary files will be deleted. If FAIL is True,
and the output of the expression is not equal to the expected output, an
assertion error is raised. If SILENT is False, and an assertion error is
raised, an error message is printed on stderr. If SILENT is True, no error
message will be printed.
If VERBOSE is non-zero and a positive integer number, verbosity of the term
rewriting system will be increased. This will output debug messages and a
higher value will print more types of debug messages.
"""
parser = TestParser(keepfiles=keepfiles, verbose=verbose)
for exp, out in expressions:
res = None
try:
res = parser.run([exp])
assert res == out
except:
if not silent:
print >>sys.stderr, 'error: %s = %s, but expected: %s' \
% (exp, str(res), str(out))
if fail:
raise
import sys
import unittest import unittest
from tests.parser import TestParser, run_expressions
from src.calc import Parser
class TestParser(Parser):
def __init__(self, input_buffer, **kwargs):
Parser.__init__(self, **kwargs)
self.input_buffer = []
self.input_position = 0
map(self.append, input_buffer)
def append(self, input):
self.input_buffer.append(input + '\n')
def read(self, nbytes):
buffer = ''
try:
buffer = self.input_buffer[self.input_position]
except IndexError:
return ''
self.input_position += 1
return buffer
class TestCalc(unittest.TestCase): class TestCalc(unittest.TestCase):
...@@ -39,19 +11,8 @@ class TestCalc(unittest.TestCase): ...@@ -39,19 +11,8 @@ class TestCalc(unittest.TestCase):
def tearDown(self): def tearDown(self):
pass pass
def run_expressions(self, expressions, fail=True):
for exp, out in expressions:
try:
res = TestParser([exp], keepfiles=1).run()
assert res == out
except:
print >>sys.stderr, 'error: %s = %s, but expected: %s' \
% (exp, str(res), str(out))
if fail:
raise
def test_constructor(self): def test_constructor(self):
assert TestParser(['1+4'], keepfiles=1).run() == 5.0 assert TestParser(keepfiles=1).run(['1+4']) == 5.0
def test_basic_on_exp(self): def test_basic_on_exp(self):
expressions = [('4', 4.0), expressions = [('4', 4.0),
...@@ -62,7 +23,7 @@ class TestCalc(unittest.TestCase): ...@@ -62,7 +23,7 @@ class TestCalc(unittest.TestCase):
('3^4', 81.0), ('3^4', 81.0),
('(4)', 4.0)] ('(4)', 4.0)]
self.run_expressions(expressions) run_expressions(expressions)
def test_infinity(self): def test_infinity(self):
expressions = [('2^9999', None), expressions = [('2^9999', None),
...@@ -70,4 +31,4 @@ class TestCalc(unittest.TestCase): ...@@ -70,4 +31,4 @@ class TestCalc(unittest.TestCase):
('2^99999999999', None), ('2^99999999999', None),
('2^-99999999999', 0.0)] ('2^-99999999999', 0.0)]
self.run_expressions(expressions, fail=False) run_expressions(expressions, fail=False)
import unittest
from tests.parser import TestParser, run_expressions
from sympy import Symbol, symbols
class TestVariables(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
def test_addition(self):
a = Symbol('a')
expressions = [('a + 5', a + 5)]
run_expressions(expressions)
def test_addition_of_two_terms(self):
a, b = symbols('a,b')
expressions = [('4*a + 5*b', 4.0*a + 5.0*b)]
run_expressions(expressions)
#def test_short_addition_of_two_terms(self):
# a, b = symbols('a,b')
# expressions = [('4a + 5b', 4.0*a + 5.0*b)]
# run_expressions(expressions, verbose=1)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment