From 0c3ba224d7e1200b0fb15166a48c6af7ac003e70 Mon Sep 17 00:00:00 2001 From: "Dmitry V. Sokolov" Date: Sun, 14 Jan 2024 21:12:56 +0100 Subject: [PATCH] transpy without closures --- analyzer.py | 7 ++++ symtable.py | 9 +++++ tests/test_transpy.py | 2 +- transpy_novars.py | 83 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 transpy_novars.py diff --git a/analyzer.py b/analyzer.py index 498db7a..c900b08 100644 --- a/analyzer.py +++ b/analyzer.py @@ -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 @@ -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']) @@ -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: diff --git a/symtable.py b/symtable.py index ec4d17d..166e59c 100644 --- a/symtable.py +++ b/symtable.py @@ -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() diff --git a/tests/test_transpy.py b/tests/test_transpy.py index 0ebb1ee..f1b9baa 100644 --- a/tests/test_transpy.py +++ b/tests/test_transpy.py @@ -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' diff --git a/transpy_novars.py b/transpy_novars.py new file mode 100644 index 0000000..f0b2667 --- /dev/null +++ b/transpy_novars.py @@ -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'