Quantcast
Channel: Planet Python
Viewing all articles
Browse latest Browse all 23729

The Digital Cat: A game of tokens: solution - Part 4

$
0
0

This is my solution to the fourth part of "A Game of Tokens", which can be found here.

You can find the tests and the code presented here in this repository in the branch called part4.

Level 15 - Exponentiation

Lexer

The lexer can process the exponentiation operator ^ out of the box as a LITERAL token, so no changes to the code are needed.

Parser

The test test_parse_exponentiation() can be passed adding a PowerNode class

classPowerNode(BinaryNode):node_type='exponentiation'

and a parse_exponentiation() method to the parser

defparse_exponentiation(self):left=self.parse_factor()next_token=self.lexer.peek_token()ifnext_token.type==clex.LITERALandnext_token.value=='^':operator=self._parse_symbol()right=self.parse_exponentiation()returnPowerNode(left,operator,right)returnleft

This allows the parser to explicitly parse the exponentiation operation, but when the operation is mixed with others the parser doesn't know how to deal with it, as parse_exponentiation() is not called by any other method.

To pass the test_parse_exponentiation_with_other_operators() test we need to change the parse_term() method and call parse_exponentiation() instead of parse_factor()

defparse_term(self):left=self.parse_exponentiation()next_token=self.lexer.peek_token()whilenext_token.type==clex.LITERAL\
                andnext_token.valuein['*','/']:operator=self._parse_symbol()right=self.parse_exponentiation()left=BinaryNode(left,operator,right)

the full code of the CalcParser class is now

classCalcParser:def__init__(self):self.lexer=clex.CalcLexer()def_parse_symbol(self):t=self.lexer.get_token()returnLiteralNode(t.value)defparse_integer(self):t=self.lexer.get_token()returnIntegerNode(t.value)def_parse_variable(self):t=self.lexer.get_token()returnVariableNode(t.value)defparse_factor(self):next_token=self.lexer.peek_token()ifnext_token.type==clex.LITERALandnext_token.valuein['-','+']:operator=self._parse_symbol()factor=self.parse_factor()returnUnaryNode(operator,factor)ifnext_token.type==clex.LITERALandnext_token.value=='(':self.lexer.discard_type(clex.LITERAL)expression=self.parse_expression()self.lexer.discard_type(clex.LITERAL)returnexpressionifnext_token.type==clex.NAME:t=self.lexer.get_token()returnVariableNode(t.value)returnself.parse_integer()defparse_exponentiation(self):left=self.parse_factor()next_token=self.lexer.peek_token()ifnext_token.type==clex.LITERALandnext_token.value=='^':operator=self._parse_symbol()right=self.parse_exponentiation()returnPowerNode(left,operator,right)returnleftdefparse_term(self):left=self.parse_exponentiation()next_token=self.lexer.peek_token()whilenext_token.type==clex.LITERAL\
                andnext_token.valuein['*','/']:operator=self._parse_symbol()right=self.parse_exponentiation()left=BinaryNode(left,operator,right)next_token=self.lexer.peek_token()returnleftdefparse_expression(self):left=self.parse_term()next_token=self.lexer.peek_token()whilenext_token.type==clex.LITERAL\
                andnext_token.valuein['+','-']:operator=self._parse_symbol()right=self.parse_term()left=BinaryNode(left,operator,right)next_token=self.lexer.peek_token()returnleftdefparse_assignment(self):variable=self._parse_variable()self.lexer.discard(token.Token(clex.LITERAL,'='))value=self.parse_expression()returnAssignmentNode(variable,value)defparse_line(self):try:self.lexer.stash()returnself.parse_assignment()exceptclex.TokenError:self.lexer.pop()returnself.parse_expression()

Visitor

The given test test_visitor_exponentiation() requires the CalcVisitor to parse nodes of type exponentiation. The code required to do this is

ifnode['type']=='exponentiation':lvalue,ltype=self.visit(node['left'])rvalue,rtype=self.visit(node['right'])returnlvalue**rvalue,ltype

as a final case for the CalcVisitor class. The full code of the class is is now

classCalcVisitor:def__init__(self):self.variables={}defisvariable(self,name):returnnameinself.variablesdefvalueof(self,name):returnself.variables[name]['value']deftypeof(self,name):returnself.variables[name]['type']defvisit(self,node):ifnode['type']=='integer':returnnode['value'],node['type']ifnode['type']=='variable':returnself.valueof(node['value']),self.typeof(node['value'])ifnode['type']=='unary':operator=node['operator']['value']cvalue,ctype=self.visit(node['content'])ifoperator=='-':return-cvalue,ctypereturncvalue,ctypeifnode['type']=='binary':lvalue,ltype=self.visit(node['left'])rvalue,rtype=self.visit(node['right'])operator=node['operator']['value']ifoperator=='+':returnlvalue+rvalue,rtypeelifoperator=='-':returnlvalue-rvalue,rtypeelifoperator=='*':returnlvalue*rvalue,rtypeelifoperator=='/':returnlvalue//rvalue,rtypeifnode['type']=='assignment':right_value,right_type=self.visit(node['value'])self.variables[node['variable']]={'value':right_value,'type':right_type}returnNone,None

Level 16 - Float numbers

Lexer

The first thing the lexer need is a label to identify FLOAT tokens

FLOAT='FLOAT'

then the method _process_integer() cna be extended to process float numbers as well. To do this the method is renamed to _process_number(), the regular expression is modified, and the token_type is managed according to the presence of the dot.

def_process_number(self):regexp=re.compile('[\d\.]+')match=regexp.match(self._text_storage.tail)ifnotmatch:returnNonetoken_string=match.group()token_type=FLOATif'.'intoken_stringelseINTEGERreturnself._set_current_token_and_skip(token.Token(token_type,token_string))

Remember that the get_token() function has to be modified to use the new name of the method. The new code is

defget_token(self):eof=self._process_eof()ifeof:returneofeol=self._process_eol()ifeol:returneolself._process_whitespace()name=self._process_name()ifname:returnnameinteger=self._process_number()ifinteger:returnintegerliteral=self._process_literal()ifliteral:returnliteral

Parser

First we need to add a new type of node

classFloatNode(ValueNode):node_type='float'

The new version of parse_integer(), renamed parse_number(), shall deal with both cases but also raise the TokenError exception if the parsing fails

defparse_number(self):t=self.lexer.get_token()ift.type==clex.INTEGER:returnIntegerNode(int(t.value))elift.type==clex.FLOAT:returnFloatNode(float(t.value))raiseclex.TokenError

Visitor

The change to support float nodes is trivial, we just need to include it alongside with the integer case

defvisit(self,node):ifnode['type']in['integer','float']:returnnode['value'],node['type']

To fix the missing type promotion when dealing with integers and floats it's enough to add

ifltype=='float':rtype=ltype

just before evaluating the operator in the binary nodes. The full code of the visit() method is then

defvisit(self,node):ifnode['type']in['integer','float']:returnnode['value'],node['type']ifnode['type']=='variable':returnself.valueof(node['value']),self.typeof(node['value'])ifnode['type']=='unary':operator=node['operator']['value']cvalue,ctype=self.visit(node['content'])ifoperator=='-':return-cvalue,ctypereturncvalue,ctypeifnode['type']=='binary':lvalue,ltype=self.visit(node['left'])rvalue,rtype=self.visit(node['right'])operator=node['operator']['value']ifltype=='float':rtype=ltypeifoperator=='+':returnlvalue+rvalue,rtypeelifoperator=='-':returnlvalue-rvalue,rtypeelifoperator=='*':returnlvalue*rvalue,rtypeelifoperator=='/':returnlvalue//rvalue,rtypeifnode['type']=='assignment':right_value,right_type=self.visit(node['value'])self.variables[node['variable']]={'value':right_value,'type':right_type}returnNone,Noneifnode['type']=='exponentiation':lvalue,ltype=self.visit(node['left'])rvalue,rtype=self.visit(node['right'])returnlvalue**rvalue,ltype

Final words

I bet at this point of the challenge the addition of exponentiation and float numbers wasn't that hard. The refactoring might have been a bit thougher, however, but I hope that it showed you the real power of TDD.

Feedback

Feel free to reach me on Twitter if you have questions. The GitHub issues page is the best place to submit corrections.


Viewing all articles
Browse latest Browse all 23729

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>