Skip to content

Commit

Permalink
Fixed identifier support. All features seem to be working again.
Browse files Browse the repository at this point in the history
  • Loading branch information
nrubin29 committed Oct 8, 2017
1 parent d358e29 commit 03cbe32
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 70 deletions.
2 changes: 1 addition & 1 deletion EXPLANATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
* Calculator#_match
3. The tree is fixed. Unnecessary tokens are removed, precedence issues are fixed, etc.
* Ast#_fixed
5. The tree is evaluated in a recursive fashion.
4. The tree is evaluated in a recursive fashion.
* Ast#evaluate
34 changes: 19 additions & 15 deletions ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
"""
from typing import Dict

import copy

from common import RuleMatch, remove, left_assoc, Token
from rules import rule_process_map, rule_process_value_map
from vartypes import Variable


class Ast:
def __init__(self, ast: RuleMatch):
print(self._str(ast))
self.ast = self._fixed(ast)

def _fixed(self, ast):
Expand All @@ -23,10 +25,9 @@ def _fixed(self, ast):
if ast.matched[i].name in remove:
del ast.matched[i]

# This flattens rules with a single matched element.
if len(ast.matched) is 1 and ast.name != 'mbd':
if ast.name != 'num' or isinstance(ast.matched[0], RuleMatch):
return self._fixed(ast.matched[0])
# This flattens rules with a single matched rule.
if len(ast.matched) is 1 and isinstance(ast.matched[0], RuleMatch):
return self._fixed(ast.matched[0])

# This makes left-associative operations left-associative.
for token_name, rule in left_assoc.items():
Expand Down Expand Up @@ -63,24 +64,27 @@ def _fixed(self, ast):
def evaluate(self, vrs: Dict[str, RuleMatch]):
return self._evaluate(self.ast, vrs)

def _evaluate(self, ast, vrs: Dict[str, RuleMatch]): # -> Union[Dict[str, RuleMatch], Token]:
if ast.name == 'idt':
def _evaluate(self, ast, vrs: Dict[str, RuleMatch]):
if ast.name == 'asn':
return {ast.matched[0].value: ast.matched[1]}

for token in ast.matched:
if isinstance(token, RuleMatch) and not token.processed:
token.processed = self._evaluate(token, vrs)
if isinstance(token, RuleMatch) and not token.value:
token.value = self._evaluate(token, vrs)

prms = [token.processed.value for token in ast.matched if isinstance(token, RuleMatch) and token.processed]
values = [token.value for token in ast.matched if isinstance(token, RuleMatch) and token.value]
tokens = [token for token in ast.matched if not isinstance(token, RuleMatch)]

if ast.name in rule_process_value_map:
processed = rule_process_value_map[ast.name](prms, tokens)
if ast.matched[0].name == 'IDT':
return self._evaluate(copy.deepcopy(vrs[ast.matched[0].value]), vrs)

elif ast.name in rule_process_value_map:
process = rule_process_value_map[ast.name](values, tokens)

else:
processed = rule_process_map[ast.name](prms, tokens[0] if len(tokens) > 0 else None) # This extra rule is part of the num hotfix.
process = rule_process_map[ast.name](values, tokens[0] if len(tokens) > 0 else None) # This extra rule is part of the num hotfix.

return processed
return process.value

def infix(self) -> str:
# TODO: Add parentheses where needed.
Expand All @@ -96,7 +100,7 @@ def __repr__(self):
return str(self)

def _str(self, ast, depth=0) -> str:
output = (('\t' * depth) + ast.name + ' = ' + str(ast.processed.value if ast.processed else None)) + '\n'
output = (('\t' * depth) + ast.name + ' = ' + str(ast.value if ast.value else None)) + '\n'

for matched in ast.matched:
if isinstance(matched, RuleMatch) and matched.matched:
Expand Down
20 changes: 10 additions & 10 deletions calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
import re

from ast import Ast
from common import Value, Token, token_map, rules_map, RuleMatch, ProcessedRuleMatch, rm_index, rm_key_at
from common import Token, token_map, rules_map, RuleMatch, Process
from token_value import Value


class Calculator:
Expand All @@ -15,21 +16,21 @@ def __init__(self):

def evaluate(self, eqtn: str, verbose=True) -> Value:
for e in eqtn.split(';'):
root, remaining_tokens = self._match(self._tokenize(e), 'idt')
root, remaining_tokens = self._match(self._tokenize(e), 'asn')

if remaining_tokens:
raise Exception('Invalid equation (bad format)')

ast = Ast(root)
res = ast.evaluate(self.vrs)

if isinstance(res, ProcessedRuleMatch):
# ast.ast.value = res
if isinstance(res, Value):
ast.ast.value = res

if verbose:
print(ast)

return res.value
return res

elif isinstance(res, dict):
self.vrs.update(res)
Expand All @@ -49,9 +50,8 @@ def _tokenize(self, eqtn: str) -> List[Token]:
def _match(self, tokens: List[Token], target_rule: str):
# print('match', tokens, target_rule)

if target_rule.isupper():
# This is a token, not a rule.
if tokens and tokens[0].name == target_rule: # This is a token, not a rule.
if target_rule.isupper(): # This is a token, not a rule.
if tokens and tokens[0].name == target_rule:
return tokens[0], tokens[1:]

return None, None
Expand All @@ -75,7 +75,7 @@ def _match(self, tokens: List[Token], target_rule: str):
# Success!
return RuleMatch(target_rule, matched), remaining_tokens

if rm_index(target_rule) + 1 < len(rules_map):
return self._match(tokens, rm_key_at(rm_index(target_rule) + 1))
if rules_map.index(target_rule) + 1 < len(rules_map):
return self._match(tokens, rules_map.key_at(rules_map.index(target_rule) + 1))

return None, None
30 changes: 16 additions & 14 deletions common.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class RuleMatch:
def __init__(self, name: str, matched: List[Token]):
self.name = name
self.matched = matched
self.processed = None
self.value = None

def __str__(self):
return self._str(self) # + '\n>> ' + self.infix()
Expand All @@ -22,7 +22,7 @@ def __repr__(self):
return str(self)

def _str(self, ast, depth=0) -> str:
output = (('\t' * depth) + ast.name + ' = ' + str(ast.processed.value if ast.processed else None)) + '\n'
output = (('\t' * depth) + ast.name + ' = ' + str(ast.value if ast.value else None)) + '\n'

for matched in ast.matched:
if isinstance(matched, RuleMatch) and matched.matched:
Expand All @@ -40,7 +40,7 @@ def _str(self, ast, depth=0) -> str:
# return str(self)


class ProcessedRuleMatch:
class Process:
def __init__(self, operation, operands: List, raw_args=False):
self.operation = operation
self.operands = operands
Expand Down Expand Up @@ -82,8 +82,17 @@ def __init__(self, operation, operands: List, raw_args=False):

remove = ('EQL', 'LPA', 'RPA', 'LBR', 'RBR', 'CMA', 'PPE')

rules_map = OrderedDict((
('idt', ('IDT EQL add',)),

class IndexedOrderedDict(OrderedDict):
def index(self, key):
return list(self.keys()).index(key)

def key_at(self, i):
return list(self.keys())[i]


rules_map = IndexedOrderedDict((
('asn', ('IDT EQL add',)),
('mat', ('LBR mbd RBR',)),
('mbd', ('mrw PPE mbd',)),
('mrw', ('add CMA mrw',)),
Expand All @@ -93,18 +102,11 @@ def __init__(self, operation, operands: List, raw_args=False):
('pow', ('opr POW pow',)),
('opr', ('OPR LPA mat RPA',)),
('neg', ('ADD num', 'ADD opr')),
('num', ('NUM', 'IDT', 'LPA add RPA')),
('var', ('IDT',)),
('num', ('NUM', 'LPA add RPA')),
))


def rm_index(key):
return list(rules_map.keys()).index(key)


def rm_key_at(i):
return list(rules_map.keys())[i]


left_assoc = {
'ADD': 'add',
'MUL': 'mul',
Expand Down
47 changes: 23 additions & 24 deletions rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,55 +3,55 @@
"""
from typing import List

from common import Token, ProcessedRuleMatch
from common import Token, Process
from token_value import Value
from vartypes import Number, MatrixRow, Matrix
from vartypes import Number, MatrixRow, Matrix, Variable


def num(_, tokens: List[Token]) -> ProcessedRuleMatch:
return ProcessedRuleMatch(Number.new, tokens, raw_args=True)
def var(_, tokens: List[Token]) -> Process:
return Process(Variable.new, tokens, raw_args=True)


def mrw(values: List[Value], _) -> ProcessedRuleMatch:
return ProcessedRuleMatch(MatrixRow.new, values, raw_args=True)
def num(_, tokens: List[Token]) -> Process:
return Process(Number.new, tokens, raw_args=True)


def mbd(values: List[Value], _) -> ProcessedRuleMatch:
return ProcessedRuleMatch(Matrix.new, values, raw_args=True)
def mrw(values: List[Value], _) -> Process:
return Process(MatrixRow.new, values, raw_args=True)


def num_hotfix(operands: List[Value], operator: Token) -> ProcessedRuleMatch:
# In certain cases, we attempt to process a `num` even though it's already been processed.
return ProcessedRuleMatch(lambda x: x, operands)
def mbd(values: List[Value], _) -> Process:
return Process(Matrix.new, values, raw_args=True)


def add(operands: List[Value], operator: Token) -> ProcessedRuleMatch:
return ProcessedRuleMatch({'+': operands[0].type.add, '-': operands[0].type.sub}[operator.value], operands)
def add(operands: List[Value], operator: Token) -> Process:
return Process({'+': operands[0].type.add, '-': operands[0].type.sub}[operator.value], operands)


def mul(operands: List[Value], operator: Token) -> ProcessedRuleMatch:
return ProcessedRuleMatch({'*': operands[0].type.mul, '/': operands[0].type.div, '%': operands[0].type.mod}[operator.value], operands)
def mul(operands: List[Value], operator: Token) -> Process:
return Process({'*': operands[0].type.mul, '/': operands[0].type.div, '%': operands[0].type.mod}[operator.value], operands)


def pow(operands: List[Value], operator: Token) -> ProcessedRuleMatch:
return ProcessedRuleMatch(operands[0].type.pow, operands)
def pow(operands: List[Value], operator: Token) -> Process:
return Process(operands[0].type.pow, operands)


def opr(operands: List[Value], operator: Token) -> ProcessedRuleMatch:
return ProcessedRuleMatch(getattr(operands[0].type, operator.value), operands)
def opr(operands: List[Value], operator: Token) -> Process:
return Process(getattr(operands[0].type, operator.value), operands)


def neg(operands: List[Value], operator: Token) -> ProcessedRuleMatch:
return ProcessedRuleMatch({'+': operands[0].type.pos, '-': operands[0].type.neg}[operator.value], operands)
def neg(operands: List[Value], operator: Token) -> Process:
return Process({'+': operands[0].type.pos, '-': operands[0].type.neg}[operator.value], operands)


def mat(operands: List[Value], operator: Token) -> ProcessedRuleMatch:
def mat(operands: List[Value], operator: Token) -> Process:
# Since mbd creates the matrix, we just want this to return the matrix.
return ProcessedRuleMatch(lambda x: x, operands)
return Process(lambda x: x, operands)


# The mapping for num, mrw, mbd.
rule_process_value_map = {
'var': var,
'num': num,
'mrw': mrw,
'mbd': mbd,
Expand All @@ -65,5 +65,4 @@ def mat(operands: List[Value], operator: Token) -> ProcessedRuleMatch:
'opr': opr,
'neg': neg,
'mat': mat,
'num': num_hotfix
}
12 changes: 6 additions & 6 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,17 @@ def runTest(self):

class ParenthesisTests(unittest.TestCase):
def runTest(self):
# self.assertEqual(evaluate('(1 + 2) * 3'), 9.0)
# self.assertEqual(evaluate('(1 + 2) ^ (2 * 3 - 2)'), 81.0)
self.assertEqual(evaluate('(1 + 2) * 3'), 9.0)
self.assertEqual(evaluate('(1 + 2) ^ (2 * 3 - 2)'), 81.0)
self.assertEqual(evaluate('2 (1 + 1)'), 4.0)


class IdentifierTests(unittest.TestCase):
def runTest(self):
pass
# self.assertEqual(evaluate('r = 10; r'), 10.0)
# self.assertEqual(round(evaluate('r = 5.2 * (3 + 2 / (1 + 1/6)); pi = 3.14159; area = pi * r^2; area'), 5), 1887.93915)
# self.assertEqual(round(evaluate('area = pi * r^2; r = 5.2 * (3 + 2 / (1 + 1/6)); pi = 3.14159; area'), 5), 1887.93915)
self.assertEqual(evaluate('r = 10; r'), 10.0)
self.assertEqual(round(evaluate('r = 5.2 * (3 + 2 / (1 + 1/6)); pi = 3.14159; area = pi * r^2; area'), 5), 1887.93915)
self.assertEqual(round(evaluate('area = pi * r^2; r = 5.2 * (3 + 2 / (1 + 1/6)); pi = 3.14159; area'), 5), 1887.93915)


class OperationTests(unittest.TestCase):
Expand All @@ -100,7 +100,7 @@ def runTest(self):
self.assertEqual(evaluate('10 ^ 2 - 2 ^ 3 + 3 * 2 ^ 4'), 140.0)
self.assertEqual(evaluate('1 + 2 - 3 ^ 4 * 5'), -402.0)
self.assertEqual(evaluate('2 ^ 2 * 3 ^ 2'), 36.0)
# self.assertEqual(evaluate('a = 2; b = 3; 3*(2 + a + 5*b*2 + 3)'), 111.0)
self.assertEqual(evaluate('a = 2; b = 3; 3*(2 + a + 5*b*2 + 3)'), 111.0)


class MatrixTests(unittest.TestCase):
Expand Down
6 changes: 6 additions & 0 deletions vartypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ def identity(this: Value) -> Value:
raise Exception('Operation not defined for this type.')


class Variable(Type):
@staticmethod
def new(tokens: List) -> Value:
return Value(Variable, tokens[0].value)


class Number(Type):
@staticmethod
def new(tokens: List[Token]) -> Value:
Expand Down

0 comments on commit 03cbe32

Please sign in to comment.