Skip to content

Commit

Permalink
transpy without closures
Browse files Browse the repository at this point in the history
  • Loading branch information
ssloy committed Jan 14, 2024
1 parent 2bc3ba0 commit 0c3ba22
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 1 deletion.
7 changes: 7 additions & 0 deletions analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ def build_symtable(ast):
symtable.add_fun(ast.name, [], ast.deco)
ast.deco['label'] = ast.name + '_' + LabelFactory.new_label() # unique label
process_scope(ast, symtable)
ast.deco['scope_cnt'] = symtable.scope_cnt # total number of functions, necessary for the static scope display table allocation

def process_scope(fun, symtable):
fun.deco['nonlocal'] = set() # set of nonlocal variable names in the function body, used in "readable" python transpilation only
fun.deco['local'] = [] # set of local variable names: len*4 is the memory necessary on the stack, the names are here to be put in comments
symtable.push_scope(fun.deco)
for v in fun.args: # process function arguments
symtable.add_var(*v)
for v in fun.var: # process local variables
symtable.add_var(*v)
fun.deco['local'].append(v[0])
for f in fun.fun: # process nested functions: first add function symbols to the table
symtable.add_fun(f.name, [d['type'] for v,d in f.args], f.deco)
f.deco['label'] = f.name + '_' + LabelFactory.new_label() # still need unique labels
Expand All @@ -43,6 +46,8 @@ def process_stat(n, symtable): # process "statement" syntax tree nodes
elif isinstance(n, Assign):
process_expr(n.expr, symtable)
deco = symtable.find_var(n.name)
n.deco['scope'] = deco['scope']
n.deco['offset'] = deco['offset']
n.deco['type'] = deco['type']
if n.deco['type'] != n.expr.deco['type']:
raise Exception('Incompatible types in assignment statement, line %s', n.deco['lineno'])
Expand Down Expand Up @@ -86,6 +91,8 @@ def process_expr(n, symtable): # process "expression" syntax tree nodes
elif isinstance(n, Var): # no type checking is necessary
deco = symtable.find_var(n.name)
n.deco['type'] = deco['type']
n.deco['scope'] = deco['scope']
n.deco['offset'] = deco['offset']
update_nonlocals(n.name, symtable) # used in "readable" python transpilation only
elif isinstance(n, FunCall):
for s in n.args:
Expand Down
9 changes: 9 additions & 0 deletions symtable.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,30 @@ def __init__(self):
self.variables = [{}] # stack of variable symbol tables
self.functions = [{}] # stack of function symbol tables
self.ret_stack = [ None ] # stack of enclosing function symbols, useful for return statements
self.scope_cnt = 0 # global scope counter for the display table allocation
self.var_cnt = 0 # per scope variable counter, serves as an id in a stack frame

def add_fun(self, name, argtypes, deco): # a function can be identified by its name and a list of argument types, e.g.
signature = (name, *argtypes) # fun foo(x:bool, y:int) : int {...} has ('foo',Type.BOOL,Type.INT) signature
if signature in self.functions[-1]:
raise Exception('Double declaration of the function %s %s' % (signature[0], signature[1:]))
self.functions[-1][signature] = deco
deco['scope'] = self.scope_cnt # id for the function block in the scope display table
self.scope_cnt += 1

def add_var(self, name, deco):
if name in self.variables[-1]:
raise Exception('Double declaration of the variable %s' % name)
self.variables[-1][name] = deco
deco['scope'] = self.ret_stack[-1]['scope'] # pointer to the display entry
deco['offset'] = self.var_cnt # id of the variable in the corresponding stack frame
self.var_cnt += 1

def push_scope(self, deco):
self.variables.append({})
self.functions.append({})
self.ret_stack.append(deco)
self.var_cnt = 0 # reset the per scope variable counter

def pop_scope(self):
self.variables.pop()
Expand Down
2 changes: 1 addition & 1 deletion tests/test_transpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from lexer import WendLexer
from parser import WendParser
from analyzer import *
from transpy_naive import *
from transpy_novars import *

@pytest.mark.parametrize('test_case', (
'helloworld', 'sqrt', 'fixed-point', 'scope', 'overload', 'mutual-recursion'
Expand Down
83 changes: 83 additions & 0 deletions transpy_novars.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from syntree import *

def transpy(n):
funname = n.name
funscope = n.deco['scope']
funlabel = n.deco['label']
nscopes = n.deco['scope_cnt']
allocvars = '\n'.join([f'stack.append( None ) # {v}' for v,t in n.var]) or 'pass'
return fun(n) + (f'\n'
f'eax, ebx = None, None\n'
f'display = [ 65536 ]*{nscopes}\n' # 65536 is just a big number to increase chances of "out of range" error
f'stack = []\n' # while accessing the stack in a code generated by a bugged transpiler
f'display[{funscope}] = len(stack) # frame pointer for fun {funname}\n'
f'{allocvars}\n'
f'{funlabel}()\n')

def fun(n):
nestedfun = '\n'.join([fun(f) for f in n.fun])
funbody = '\n'.join([stat(s) for s in n.body])
return ('def %s():\n' % n.deco['label'] +
indent(f'global eax, ebx, stack, display\n'
f'{nestedfun}\n'
f'{funbody}'))

def stat(n):
if isinstance(n, Print):
return '%sprint(eax, end=%s)\n' % (expr(n.expr), "'\\n'" if n.newline else "''")
elif isinstance(n, Return):
return '%sreturn eax\n' % expr(n.expr) if n.expr is not None else ''
elif isinstance(n, Assign):
return '%sstack[display[%d]+%d] = eax # %s\n' % (expr(n.expr), n.deco['scope'], n.deco['offset'], n.name)
elif isinstance(n, FunCall):
return expr(n)
elif isinstance(n, While): # note the expr(n.expr) at the end of the loop body, we need to update eax
return '%swhile eax:\n' % expr(n.expr) + indent(([stat(s) for s in n.body] or 'pass\n') + ['%s\n'%expr(n.expr)])
elif isinstance(n, IfThenElse):
return '%sif eax:\n%selse:\n%s' % (expr(n.expr),
indent([stat(s) for s in n.ibody] or 'pass\n'),
indent([stat(s) for s in n.ebody] or 'pass\n'))
raise Exception('Unknown statement type', n)

def expr(n): # convention: all expressions save their results to eax
if isinstance(n, ArithOp) or isinstance(n, LogicOp):
pyeq = {'/':'//', '||':'or', '&&':'and'}
pyop = pyeq[n.op] if n.op in pyeq else n.op
left = expr(n.left)
right = expr(n.right)
return (f'{left}'
f'stack.append(eax) # evaluate left argument and stash it\n'
f'{right}'
f'ebx = eax # evaluate right arg\n'
f'eax = stack.pop() # recall left arg\n'
f'eax = eax {pyop} ebx # binary operation\n')
elif isinstance(n, Integer) or isinstance(n, Boolean):
return 'eax = %s\n' % str(n.value)
elif isinstance(n, String):
return 'eax = "%s"\n' % str(n.value)
elif isinstance(n, Var):
return 'eax = stack[display[%d]+%d] # %s\n' % (n.deco['scope'], n.deco['offset'], n.name)
elif isinstance(n, FunCall):
funname = n.name
funscope = n.deco['fundeco']['scope']
funlabel = n.deco['fundeco']['label']
disphead = len(n.args)+len(n.deco['fundeco']['local'])+1
allocargs = '\n'.join(['%sstack.append(eax)' % expr(s) for s in n.args])
allocvars = '\n'.join([f'stack.append( None ) # {v}' for v in n.deco['fundeco']['local']]) or 'pass'
freemem = 'del stack[-%d]' % (disphead-1) if disphead>1 else 'pass'
return (f'# prepare {funname}() call\n'
f'# evalaute function arguments\n'
f'{allocargs}\n'
f'# reserve local variables\n'
f'{allocvars}\n'
f'stack.append(display[{funscope}]) # save old frame pointer\n'
f'display[{funscope}] = len(stack)-{disphead} # activate new frame pointer\n'
f'eax = {funlabel}()\n'
f'display[{funscope}] = stack.pop() # restore old frame pointer\n'
f'{freemem} # delete fun args and local vars if any, thus finishing {funname}() call\n')
raise Exception('Unknown expression type', n)

def indent(array):
multiline = ''.join([str(entry) for entry in array])
if multiline == '': return ''
return '\t'+'\n\t'.join(multiline.splitlines()) + '\n'

0 comments on commit 0c3ba22

Please sign in to comment.