Skip to content

Commit e983025

Browse files
committed
parser is working; transpy_naive works for simple cases, need to take overloads into account as well as the free variables
1 parent b623eea commit e983025

File tree

5 files changed

+256
-8
lines changed

5 files changed

+256
-8
lines changed

parser.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
from sly import Parser
2+
from lexer import WendLexer
3+
from syntree import *
4+
5+
class WendParser(Parser):
6+
tokens = WendLexer.tokens
7+
precedence = ( # arithmetic operators take precedence over logical operators
8+
('left', OR),
9+
('left', AND),
10+
('right', NOT),
11+
('nonassoc', LT, LTEQ, GT, GTEQ, EQ, NOTEQ),
12+
('left', PLUS, MINUS),
13+
('left', TIMES, DIVIDE, MOD),
14+
('right', UMINUS), # unary operators
15+
('right', UPLUS)
16+
)
17+
18+
@_('fun_decl fun_body')
19+
def fun(self, p):
20+
return Function(*(p.fun_decl[:-1] + p.fun_body), p.fun_decl[-1])
21+
22+
@_('FUN ID LPAREN [ param_list ] RPAREN [ COLON type ]')
23+
def fun_decl(self, p):
24+
deco, name, params = {'type':(p.type or Type.VOID), 'lineno':p.lineno}, p[1], (p.param_list or [])
25+
return [name, params, deco]
26+
27+
@_('param { COMMA param }')
28+
def param_list(self, p):
29+
return [p.param0] + p.param1
30+
31+
@_('ID COLON type')
32+
def param(self, p):
33+
return (p[0], {'type':p.type, 'lineno':p.lineno})
34+
35+
@_('BEGIN [ var_list ] [ fun_list ] [ statement_list ] END')
36+
def fun_body(self, p):
37+
return [p.var_list or [], p.fun_list or [], p.statement_list or []]
38+
39+
@_('var { var }')
40+
def var_list(self, p):
41+
return [p.var0] + p.var1
42+
43+
@_('VAR ID COLON type SEMICOLON')
44+
def var(self, p):
45+
return (p[1], {'type':p.type, 'lineno':p.lineno})
46+
47+
@_('fun { fun }')
48+
def fun_list(self, p):
49+
return [p.fun0] + p.fun1
50+
51+
@_('statement { statement }')
52+
def statement_list(self, p):
53+
return [p.statement0] + p.statement1
54+
55+
@_('WHILE expr BEGIN [ statement_list ] END')
56+
def statement(self, p):
57+
return While(p.expr, p.statement_list or [], {'lineno':p.lineno})
58+
59+
@_('IF expr BEGIN [ statement_list ] END [ ELSE BEGIN statement_list_optional END ]')
60+
def statement(self, p):
61+
return IfThenElse(p.expr, p.statement_list or [], p.statement_list_optional or [], {'lineno':p.lineno})
62+
63+
@_('statement_list') # sly does not support nested
64+
def statement_list_optional(self, p): # optional targets, therefore
65+
return p.statement_list # this rule
66+
67+
@_('')
68+
def statement_list_optional(self, p):
69+
return []
70+
71+
@_('PRINT STRING SEMICOLON',
72+
'PRINTLN STRING SEMICOLON')
73+
def statement(self, p):
74+
return Print(String(p[1][1:-1]), p[0]=='println', {'lineno':p.lineno})
75+
76+
@_('PRINT expr SEMICOLON',
77+
'PRINTLN expr SEMICOLON')
78+
def statement(self, p):
79+
return Print(p.expr, p[0]=='println', {'lineno':p.lineno})
80+
81+
@_('RETURN SEMICOLON')
82+
def statement(self, p):
83+
return Return(None, {'lineno':p.lineno})
84+
85+
@_('RETURN expr SEMICOLON')
86+
def statement(self, p):
87+
return Return(p.expr, {'lineno':p.lineno})
88+
89+
@_('ID ASSIGN expr SEMICOLON')
90+
def statement(self, p):
91+
return Assign(p[0], p.expr, {'lineno':p.lineno})
92+
93+
@_('ID LPAREN [ args ] RPAREN SEMICOLON')
94+
def statement(self, p):
95+
return FunCall(p[0], p.args or [], {'lineno':p.lineno})
96+
97+
@_('MINUS expr %prec UMINUS')
98+
def expr(self, p):
99+
return ArithOp('-', Integer(0), p.expr, {'lineno':p.lineno})
100+
101+
@_('PLUS expr %prec UPLUS',
102+
'LPAREN expr RPAREN')
103+
def expr(self, p):
104+
return p.expr
105+
106+
@_('expr PLUS expr',
107+
'expr MINUS expr',
108+
'expr TIMES expr',
109+
'expr DIVIDE expr',
110+
'expr MOD expr')
111+
def expr(self, p):
112+
return ArithOp(p[1], p.expr0, p.expr1, {'lineno':p.lineno})
113+
114+
@_('expr LT expr',
115+
'expr LTEQ expr',
116+
'expr GT expr',
117+
'expr GTEQ expr',
118+
'expr EQ expr',
119+
'expr NOTEQ expr',
120+
'expr AND expr',
121+
'expr OR expr')
122+
def expr(self, p):
123+
return LogicOp(p[1], p.expr0, p.expr1, {'lineno':p.lineno})
124+
125+
@_('NOT expr')
126+
def expr(self, p):
127+
return LogicOp('==', Boolean('False'), p.expr, {'lineno':p.lineno})
128+
129+
@_('ID')
130+
def expr(self, p):
131+
return Var(p[0], {'lineno':p.lineno})
132+
133+
@_('ID LPAREN [ args ] RPAREN')
134+
def expr(self, p):
135+
return FunCall(p[0], p.args or [], {'lineno':p.lineno})
136+
137+
@_('expr { COMMA expr }')
138+
def args(self, p):
139+
return [p.expr0] + p.expr1
140+
141+
@_('INTVAL')
142+
def expr(self, p):
143+
return Integer(int(p.INTVAL), {'lineno':p.lineno})
144+
145+
@_('BOOLVAL')
146+
def expr(self, p):
147+
return Boolean(p.BOOLVAL=='true', {'lineno':p.lineno})
148+
149+
@_('INT', 'BOOL')
150+
def type(self, p):
151+
return Type.INT if p[0]=='int' else Type.BOOL
152+
153+
def error(self, token):
154+
if not token:
155+
raise Exception('Syntax error: unexpected EOF')
156+
raise Exception(f'Syntax error at line {token.lineno}, token={token.type}')

tests/test_parser.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import pytest
2+
from lexer import WendLexer
3+
from parser import WendParser
4+
from syntree import *
5+
6+
def tree_signature(n):
7+
if isinstance(n, Function):
8+
return 'Function{%s,%s}' % (''.join([tree_signature(f) for f in n.fun]), ''.join([tree_signature(s) for s in n.body]))
9+
elif isinstance(n, Print):
10+
return 'Print{%s}' % tree_signature(n.expr)
11+
elif isinstance(n, Return):
12+
return 'Return{%s}' % tree_signature(n.expr)
13+
elif isinstance(n, Assign):
14+
return 'Assign{%s}' % tree_signature(n.expr)
15+
elif isinstance(n, FunCall):
16+
return 'FunCall{%s}' % ','.join([tree_signature(e) for e in n.args])
17+
elif isinstance(n, While):
18+
return 'While{%s,%s}' % (tree_signature(n.expr), ''.join([tree_signature(s) for s in n.body]))
19+
elif isinstance(n, IfThenElse):
20+
return 'IfThenElse{%s,%s,%s}' % (tree_signature(n.expr), ''.join([tree_signature(s) for s in n.ibody]), ''.join([tree_signature(s) for s in n.ebody]))
21+
elif isinstance(n, ArithOp):
22+
return 'ArithOp{%s,%s}' % (tree_signature(n.left), tree_signature(n.right))
23+
elif isinstance(n, LogicOp):
24+
return 'LogicOp{%s,%s}' % (tree_signature(n.left), tree_signature(n.right))
25+
elif isinstance(n, Integer):
26+
return 'Integer'
27+
elif isinstance(n, Boolean):
28+
return 'Boolean'
29+
elif isinstance(n, Var):
30+
return 'Var'
31+
elif isinstance(n, String):
32+
return 'String'
33+
return ''
34+
35+
@pytest.mark.parametrize(
36+
'program, expected_output, expected_signature', (
37+
('fun main() {print +3 + 5 * -2;}', '-7', 'Function{,Print{ArithOp{Integer,ArithOp{Integer,ArithOp{Integer,Integer}}}}}'),
38+
('fun main() {print 3 - 4 * 5;}', '-17', 'Function{,Print{ArithOp{Integer,ArithOp{Integer,Integer}}}}'),
39+
('fun main() {print 3 - 10 / 5;}', '1', 'Function{,Print{ArithOp{Integer,ArithOp{Integer,Integer}}}}'),
40+
('fun main() {print (-2+3*4)+5/(7-6)%8;}', '15', 'Function{,Print{ArithOp{ArithOp{ArithOp{Integer,Integer},ArithOp{Integer,Integer}},ArithOp{ArithOp{Integer,ArithOp{Integer,Integer}},Integer}}}}'),
41+
('fun main() {print 5<3;}', 'False', 'Function{,Print{LogicOp{Integer,Integer}}}'),
42+
('fun main() {print 3==3;}', 'True', 'Function{,Print{LogicOp{Integer,Integer}}}'),
43+
('fun main() {print 3 * (4 + 5) / 7 == 3;}', 'True', 'Function{,Print{LogicOp{ArithOp{ArithOp{Integer,ArithOp{Integer,Integer}},Integer},Integer}}}'),
44+
('fun main() {print true && false;}', 'False', 'Function{,Print{LogicOp{Boolean,Boolean}}}'),
45+
('fun main() {print true && false || true;}', 'True', 'Function{,Print{LogicOp{LogicOp{Boolean,Boolean},Boolean}}}'),
46+
('fun main() {print !true;}', 'False', 'Function{,Print{LogicOp{Boolean,Boolean}}}'),
47+
('fun main() {print 3<=3 && 3>=3;}', 'True', 'Function{,Print{LogicOp{LogicOp{Integer,Integer},LogicOp{Integer,Integer}}}}'),
48+
)
49+
)
50+
51+
def test_arithmetic(program, expected_output, expected_signature):
52+
assert tree_signature(WendParser().parse(WendLexer().tokenize(program))) == expected_signature

tests/test_syntree.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,13 @@ def test_syntree(tmp_path):
6262

6363
program = transpy(main_fun)
6464

65-
# print(program)
66-
# old_stdout = sys.stdout
67-
# with open(tmp_path / 'sqrt.py', 'w') as output:
68-
# sys.stdout = output
69-
# print(program)
70-
# sys.stdout = old_stdout
71-
# old_stdout = sys.stdout
65+
print(program) # save the output to a temp dir
66+
old_stdout = sys.stdout
67+
with open(tmp_path / 'sqrt.py', 'w') as output:
68+
sys.stdout = output
69+
print(program)
70+
sys.stdout = old_stdout
71+
old_stdout = sys.stdout
7272

7373
old_stdout = sys.stdout
7474
sys.stdout = io.StringIO()

tests/test_transpy.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import pytest
2+
#import shutil
3+
import io, sys
4+
from lexer import WendLexer
5+
from parser import WendParser
6+
from transpy_naive import *
7+
8+
@pytest.mark.parametrize('test_case', (
9+
'helloworld', 'sqrt', 'fixed-point', 'scope', 'overload', 'mutual-recursion'
10+
)
11+
)
12+
13+
def test_transpy(tmp_path, test_case):
14+
source_file = test_case + '.wend'
15+
expected_output = test_case + '.expected'
16+
target_file = test_case + '.py'
17+
f = open('test-data/' + source_file, 'r')
18+
19+
tokens = WendLexer().tokenize(f.read())
20+
ast = WendParser().parse(tokens)
21+
22+
program = transpy(ast)
23+
old_stdout = sys.stdout
24+
with open(tmp_path / target_file, 'w') as output:
25+
sys.stdout = output
26+
print(program)
27+
sys.stdout = old_stdout
28+
29+
old_stdout = sys.stdout
30+
sys.stdout = io.StringIO()
31+
exec(program)
32+
result = sys.stdout.getvalue().strip()
33+
sys.stdout = old_stdout
34+
with open('test-data/' + expected_output) as f:
35+
expected_result = f.read().strip()
36+
37+
assert result == expected_result
38+

transpy_naive.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ def expr(n):
3434
pyeq = {'/':'//', '||':'or', '&&':'and'}
3535
pyop = pyeq[n.op] if n.op in pyeq else n.op
3636
return '(%s) %s (%s)' % (expr(n.left), pyop, expr(n.right))
37-
elif isinstance(n, Integer) or isinstance(n, Boolean) or isinstance(n, String):
37+
elif isinstance(n, Integer) or isinstance(n, Boolean):
3838
return str(n.value)
39+
elif isinstance(n, String):
40+
return '"' + n.value + '"'
3941
elif isinstance(n, Var):
4042
return n.name
4143
elif isinstance(n, FunCall):

0 commit comments

Comments
 (0)