Implemented CONCAT grammar, code cleanup and fixed unit tests.

parent e1cd6997
...@@ -18,6 +18,11 @@ sys.path.insert(1, PYBISON_PYREX) ...@@ -18,6 +18,11 @@ sys.path.insert(1, PYBISON_PYREX)
from bison import BisonParser, ParserSyntaxError from bison import BisonParser, ParserSyntaxError
# Check for n-ary operator in child nodes
def combine(op, n):
return n.nodes if n.title() == op else [n]
class Parser(BisonParser): class Parser(BisonParser):
""" """
Implements the calculator parser. Grammar rules are defined in the method Implements the calculator parser. Grammar rules are defined in the method
...@@ -34,7 +39,7 @@ class Parser(BisonParser): ...@@ -34,7 +39,7 @@ class Parser(BisonParser):
# of tokens of the lex script. # of tokens of the lex script.
tokens = ['NUMBER', 'IDENTIFIER', tokens = ['NUMBER', 'IDENTIFIER',
'PLUS', 'MINUS', 'TIMES', 'DIVIDE', 'POW', 'PLUS', 'MINUS', 'TIMES', 'DIVIDE', 'POW',
'LPAREN', 'RPAREN', 'COMMA', 'LPAREN', 'RPAREN', 'COMMA', 'CONCAT_POW',
'NEWLINE', 'QUIT', 'RAISE'] 'NEWLINE', 'QUIT', 'RAISE']
# ------------------------------ # ------------------------------
...@@ -87,7 +92,7 @@ class Parser(BisonParser): ...@@ -87,7 +92,7 @@ class Parser(BisonParser):
# as a shell. In that case, it is useful that the shell prints the # as a shell. In that case, it is useful that the shell prints the
# output of the evaluation. # output of the evaluation.
if self.interactive and values[1]: if self.interactive and values[1]:
print 'result:', values[1] print values[1]
return values[1] return values[1]
...@@ -107,85 +112,101 @@ class Parser(BisonParser): ...@@ -107,85 +112,101 @@ class Parser(BisonParser):
""" """
exp : NUMBER exp : NUMBER
| IDENTIFIER | IDENTIFIER
| exp PLUS exp
| exp MINUS exp
| exp TIMES exp
| exp DIVIDE exp
| MINUS exp %prec NEG
| exp POW exp
| LPAREN exp RPAREN | LPAREN exp RPAREN
| symbolic | unary
| binary
| concat
""" """
# rule: NUMBER if option == 0: # rule: NUMBER
if option == 0:
# TODO: A bit hacky, this achieves long integers and floats. # TODO: A bit hacky, this achieves long integers and floats.
value = float(values[0]) if '.' in values[0] else int(values[0]) value = float(values[0]) if '.' in values[0] else int(values[0])
return Leaf(value) return Leaf(value)
# rule: IDENTIFIER if option == 1: # rule: IDENTIFIER
if option == 1:
return Leaf(values[0]) return Leaf(values[0])
# rule: LPAREN exp RPAREN if option == 2: # rule: LPAREN exp RPAREN
if option == 8:
return values[1] return values[1]
# rule: symbolic if option in [3, 4, 5]: # rule: unary | binary | concat
if option == 9:
return values[0] return values[0]
# Check for n-ary operator in child nodes raise ParserSyntaxError('Unsupported option %d in target "%s".'
combine = lambda op, n: n.nodes if n.title() == op else [n] % (option, target))
# rule: exp PLUS exp def on_unary(self, target, option, names, values):
if option == 2: """
return Node('+', *(combine('+', values[0]) + combine('+', values[2]))) unary : MINUS exp %prec NEG
"""
# rule: exp MINUS expo if option == 0: # rule: NEG exp
if option == 3: return Node('-', values[1])
return Node('-', *(combine('-', values[0]) + combine('-', values[2])))
# rule: exp TIMES expo raise ParserSyntaxError('Unsupported option %d in target "%s".'
if option == 4: % (option, target))
return Node('*', *(combine('*', values[0]) + combine('*', values[2])))
# rule: exp DIVIDE expo def on_binary(self, target, option, names, values):
if option == 5: """
return Node('/', values[0], values[2]) binary : exp PLUS exp
| exp MINUS exp
| exp TIMES exp
| exp DIVIDE exp
| exp POW exp
"""
# rule: NEG expo if option == 0: # rule: exp PLUS exp
if option == 6: return Node('+', *(combine('+', values[0])
return Node('-', values[1]) + combine('+', values[2])))
if option == 1: # rule: exp MINUS exp
return Node('-', *(combine('-', values[0])
+ combine('-', values[2])))
if option == 2: # rule: exp TIMES exp
return Node('*', *(combine('*', values[0])
+ combine('*', values[2])))
if option == 3: # rule: exp DIVIDE exp
return Node('/', values[0], values[2])
# rule: exp POW expo if option == 4: # rule: exp POW exp
if option == 7:
return Node('^', values[0], values[2]) return Node('^', values[0], values[2])
raise ParserSyntaxError('Unsupported option %d in target "%s".' raise ParserSyntaxError('Unsupported option %d in target "%s".'
% (option, target)) % (option, target))
def on_symbolic(self, target, option, names, values):
def on_concat(self, option, target, names, values):
""" """
symbolic : NUMBER IDENTIFIER concat : exp IDENTIFIER
| IDENTIFIER IDENTIFIER | exp NUMBER
| symbolic IDENTIFIER | exp LPAREN exp RPAREN
| IDENTIFIER NUMBER | exp CONCAT_POW
| CONCAT_POW
""" """
# rule: NUMBER IDENTIFIER
# rule: IDENTIFIER IDENTIFIER if option in [0, 1]: # rule: exp IDENTIFIER | exp NUMBER
# rule: symbolic IDENTIFIER # NOTE: xy -> x*y
if option in [0, 1, 2]: # NOTE: (x)4 -> x*4
# 4x -> 4*x val = int(values[1]) if option == 1 else values[1]
# a b -> a * b return Node('*', *(combine('*', values[0]) + [Leaf(val)]))
# a b c -> (a * b) * c
node = Node('*', Leaf(values[0]), Leaf(values[1])) if option == 2: # rule: exp LPAREN exp RPAREN
return node # NOTE: x(y) -> x*(y)
return Node('*', *(combine('*', values[0])
# rule: IDENTIFIER NUMBER + combine('*', values[2])))
if option == 3: if option == 3:
# x4 -> x^4 # NOTE: x4 -> x^4
return Node('^', Leaf(values[0]), Leaf(values[1])) identifier, exponent = list(values[1])
node = Node('^', Leaf(identifier), Leaf(int(exponent)))
return Node('*', values[0], node)
if option == 4:
# NOTE: x4 -> x^4
identifier, exponent = list(values[0])
return Node('^', Leaf(identifier), Leaf(int(exponent)))
raise ParserSyntaxError('Unsupported option %d in target "%s".' raise ParserSyntaxError('Unsupported option %d in target "%s".'
% (option, target)) % (option, target))
...@@ -196,19 +217,22 @@ class Parser(BisonParser): ...@@ -196,19 +217,22 @@ class Parser(BisonParser):
lexscript = r""" lexscript = r"""
%{ %{
//int yylineno = 0; //int yylineno = 0;
#include <stdio.h>
#include <string.h>
#include "Python.h" #include "Python.h"
#define YYSTYPE void * #define YYSTYPE void *
#include "tokens.h" #include "tokens.h"
extern void *py_parser; extern void *py_parser;
extern void (*py_input)(PyObject *parser, char *buf, int *result, int max_size); extern void (*py_input)(PyObject *parser, char *buf, int *result,
#define returntoken(tok) yylval = PyString_FromString(strdup(yytext)); return (tok); int max_size);
#define YY_INPUT(buf,result,max_size) { (*py_input)(py_parser, buf, &result, max_size); } #define returntoken(tok) \
yylval = PyString_FromString(strdup(yytext)); return (tok);
#define YY_INPUT(buf,result,max_size) { \
(*py_input)(py_parser, buf, &result, max_size); \
}
%} %}
%% %%
[a-zA-Z][0-9]+ { returntoken(CONCAT_POW); }
[0-9]+ { returntoken(NUMBER); } [0-9]+ { returntoken(NUMBER); }
[a-zA-Z] { returntoken(IDENTIFIER); } [a-zA-Z] { returntoken(IDENTIFIER); }
"(" { returntoken(LPAREN); } "(" { returntoken(LPAREN); }
...@@ -224,7 +248,8 @@ class Parser(BisonParser): ...@@ -224,7 +248,8 @@ class Parser(BisonParser):
[ \t\v\f] {} [ \t\v\f] {}
[\n] {yylineno++; returntoken(NEWLINE); } [\n] {yylineno++; returntoken(NEWLINE); }
. { printf("unknown char %c ignored, yytext=0x%lx\n", yytext[0], yytext); /* ignore bad chars */} . { printf("unknown char %c ignored, yytext=%p\n",
yytext[0], yytext); /* ignore bad chars */}
%% %%
......
import sys import sys
from external.graph_drawing.graph import generate_graph
from external.graph_drawing.line import generate_line
class ParserWrapper(object): class ParserWrapper(object):
def __init__(self, base_class, **kwargs): def __init__(self, base_class, **kwargs):
self.input_buffer = [] self.input_buffer = []
self.last_buffer = ''
self.input_position = 0 self.input_position = 0
self.closed = False
self.verbose = kwargs.get('verbose', False) self.verbose = kwargs.get('verbose', False)
# Overwrite parser read() method self.parser = base_class(file=self, read=self.read, **kwargs)
def read(nbytes):
buf = ''
try:
buf = self.input_buffer[self.input_position]
if self.verbose: def readline(self, nbytes=False):
print 'read:', buf return self.read(nbytes)
except IndexError:
return ''
self.input_position += 1 def read(self, nbytes=False):
if len(self.last_buffer) >= nbytes:
buf = self.last_buffer[:nbytes]
self.last_buffer = self.last_buffer[nbytes:]
return buf return buf
self.parser = base_class(**kwargs) buf = self.last_buffer
self.parser.read = read
try:
buf += self.input_buffer[self.input_position]
if self.verbose:
print 'read:', buf
self.input_position += 1
except IndexError:
self.closed = True
return ''
self.last_buffer = buf[nbytes:]
return buf
def close(self):
self.closed = True
self.input_position = len(self.input_buffer)
def run(self, input_buffer, *args, **kwargs): def run(self, input_buffer, *args, **kwargs):
map(self.append, input_buffer) map(self.append, input_buffer)
return self.parser.run(*args, **kwargs) return self.parser.run(*args, **kwargs)
def append(self, input): def append(self, input):
self.closed = False
self.input_buffer.append(input + '\n') self.input_buffer.append(input + '\n')
...@@ -70,5 +88,19 @@ def run_expressions(base_class, expressions, keepfiles=1, fail=True, ...@@ -70,5 +88,19 @@ def run_expressions(base_class, expressions, keepfiles=1, fail=True,
print >>sys.stderr, 'error: %s = %s, but expected: %s' \ print >>sys.stderr, 'error: %s = %s, but expected: %s' \
% (exp, str(res), str(out)) % (exp, str(res), str(out))
if not silent and hasattr(res, 'nodes'):
print >>sys.stderr, 'result graph:'
print >>sys.stderr, generate_graph(res)
print >>sys.stderr, 'expected graph:'
print >>sys.stderr, generate_graph(out)
if fail: if fail:
raise raise
def graph(parser, *exp, **kwargs):
return generate_graph(ParserWrapper(parser, **kwargs).run(exp))
def line(parser, *exp, **kwargs):
return generate_line(ParserWrapper(parser, **kwargs).run(exp))
import unittest import unittest
from src.calc import Parser from src.parser import Parser
from src.node import ExpressionNode as N, ExpressionLeaf as L
from tests.parser import ParserWrapper, run_expressions from tests.parser import ParserWrapper, run_expressions
class TestCalc(unittest.TestCase): class TestCalc(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
def test_constructor(self): def test_constructor(self):
assert ParserWrapper(Parser, keepfiles=1).run(['1+4']) == 5.0 assert ParserWrapper(Parser).run(['1+4']) \
== N('+', L(1), L(4))
def test_basic_on_exp(self): def test_basic_on_exp(self):
expressions = [('4', 4.0), expressions = [('4', L(4)),
('3+4', 7.0), ('3+4', N('+', L(3), L(4))),
('3-4', -1.0), ('3-4', N('-', L(3), L(4))),
('3/4', .75), ('3/4', N('/', L(3), L(4))),
('-4', -4.0), ('-4', N('-', L(4))),
('3^4', 81.0), ('3^4', N('^', L(3), L(4))),
('(4)', 4.0)] ('(2)', L(2))]
run_expressions(Parser, expressions) run_expressions(Parser, expressions)
def test_infinity(self): def test_infinity(self):
expressions = [('2^3000', 2**3000), expressions = [('2^3000', N('^', L(2), L(3000))),
('2^-3000', 0.0)] ('2^-3000', N('^', L(2), N('-', L(3000))))]
# ('2^99999999999', None), # ('2^99999999999', None),
# ('2^-99999999999', 0.0)] # ('2^-99999999999', 0.0)]
run_expressions(Parser, expressions) run_expressions(Parser, expressions)
def test_concat_easy(self):
expressions = [
('xy', N('*', L('x'), L('y'))),
('2x', N('*', L(2), L('x'))),
('x4', N('^', L('x'), L(4))),
('xy4', N('*', L('x'), N('^', L('y'), L(4)))),
('(x)4', N('*', L('x'), L(4))),
('(3+4)2', N('*', N('+', L(3), L(4)), L(2))),
]
run_expressions(Parser, expressions)
def test_concat_intermediate(self):
expressions = [
('(3+4)(5+7)', N('*', N('+', L(3), L(4)),
N('+', L(5), L(7)))),
('(a+b)(c+d)', N('*', N('+', L('a'), L('b')),
N('+', L('c'), L('d')))),
('a+b(c+d)', N('+', L('a'), N('*', L('b'),
N('+', L('c'), L('d'))))),
('ab(c)d', N('*', L('a'), L('b'), L('c'), L('d'))),
#('ab(c)d', N('*', L('a'), N('*', L('b'),
# N('*', L('c'), L('d'))))),
]
run_expressions(Parser, expressions)
# vim: set fileencoding=utf-8 : # vim: set fileencoding=utf-8 :
import unittest import unittest
from external.graph_drawing.graph import generate_graph
from external.graph_drawing.line import generate_line
from src.parser import Parser from src.parser import Parser
from src.node import ExpressionNode as Node, ExpressionLeaf as Leaf from src.node import ExpressionNode as Node, ExpressionLeaf as Leaf
from tests.parser import ParserWrapper, run_expressions from tests.parser import ParserWrapper, run_expressions, line, graph
def graph(*exp, **kwargs):
return generate_graph(ParserWrapper(Parser, **kwargs).run(exp))
def line(*exp, **kwargs):
return generate_line(ParserWrapper(Parser, **kwargs).run(exp))
class TestParser(unittest.TestCase): class TestParser(unittest.TestCase):
...@@ -26,11 +15,11 @@ class TestParser(unittest.TestCase): ...@@ -26,11 +15,11 @@ class TestParser(unittest.TestCase):
run_expressions(Parser, [('a', Leaf('a'))]) run_expressions(Parser, [('a', Leaf('a'))])
def test_graph(self): def test_graph(self):
assert graph('4a') == (""" assert graph(Parser, '4a') == ("""
* *
╭┴╮ ╭┴╮
4 a 4 a
""").replace('\n ', '\n')[1:-1] """).replace('\n ', '\n')[1:-1]
def test_line(self): def test_line(self):
self.assertEqual(line('4a'), '4 * a') self.assertEqual(line(Parser, '4a'), '4 * a')
...@@ -7,12 +7,6 @@ from sympy import Symbol, symbols ...@@ -7,12 +7,6 @@ from sympy import Symbol, symbols
class TestVariables(unittest.TestCase): class TestVariables(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
def test_addition(self): def test_addition(self):
expressions = [('5 + 5', 5 + 5)] expressions = [('5 + 5', 5 + 5)]
run_expressions(Parser, expressions) run_expressions(Parser, expressions)
......
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