diff --git a/test.py b/test.py new file mode 100644 index 0000000..202628f --- /dev/null +++ b/test.py @@ -0,0 +1,89 @@ +import zbrush + +gScriptVersion = 1.0 + + +def check_script_version(): + fred = 1 + gScriptVersion = zbrush.ZbrushInfo(0) + + +def test_func(arg, arg2): + print(arg) + arg2 = arg + 1 + + +def fred(): + zbrush.MemCreate("other", 100, "A") + memblock = zbrush.MemCreate("memblock", 100, "A") + zbrush.MemWrite(memblock, 0, "A", 1) + goof = zbrush.MemRead(memblock, 0, "A", 1) # noqa + test = zbrush.ToolGetActiveIndex() + subtooltst = zbrush.GetSubToolIndex(2) + allowed = max(1, 2) + barney = 1 + josh = test_return_string(barney) + print(josh) + xxxx = test_return_float(barney) + print(xxxx) + wilma = test_return_int(barney) + pebbles = "a" + bam_bam = "b" + """random internal comment""" + + +def test_loop() -> int: + for x in range(10): + if x == 5: + return x + else: + if x in [3, 4]: + continue + print(x) + + +def array_contains(arr, val) -> int: + loopsize = zbrush.VarSize(arr) + result = loopsize + # result = 0 + # for x in range(loopsize): + # if arr[x] == val: + # """todo return 1""" + # result = 1 + # break + return result + + +def test_array_contains(arr, val) -> int: + result = array_contains(arr, val) + + +# todo: figure out how to handle retruns in cases inside loops and whiles + + +def test_while(iterations, cutoff) -> int: + val = 0 + """random internal comment""" + otherval = 1.0 + while val < iterations: + if val == cutoff: + return val + val += 1 + + +def test_return_string(arg) -> str: + """convert 'return val' to return an automatic value""" + val = arg + "xxx" + return val + + +def test_return_float(arg) -> float: + """convert 'return val' to return an automatic value""" + val = arg + 1.0 + return val + + +def test_return_int(arg) -> int: + """convert 'return val' to return an automatic value""" + val = arg + 1 + return val diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..7064edb --- /dev/null +++ b/test.txt @@ -0,0 +1,122 @@ +/* +transpiled with zsc 0.1.0 +from: test.py +*/ + +// automatic return vars + +[RoutineDef, check_script_version, + [VarSet, gScriptVersion, [ZbrushInfo, 0]] +] // end check_script_version + + +[RoutineDef, test_func, + [RoutineCall, print, #arg] + , // args + arg, arg2 +] // end test_func + + +[RoutineDef, fred, + [MemCreate, , "other", 100, "A"] + // blockID = memblock + [VarSet, memblock, [MemCreate, memblock, 100, 'A']] + [MemWrite, , memblock, 0, "A", 1] + [VarSet, goof, [MemRead, memblock, 0, 'A', 1]] + [VarSet, test, [ToolGetActiveIndex]] + [VarSet, subtooltst, [GetSubToolIndex, 2]] + [VarSet, allowed, [MAX, , 1, 2]] + [RoutineCall, test_return_string, #barney, josh] + [RoutineCall, print, #josh] + [RoutineCall, test_return_float, #barney, xxxx] + [RoutineCall, print, #xxxx] + [RoutineCall, test_return_int, #barney, wilma] + // random internal comment +] // end fred + + +[RoutineDef, test_loop, + + [Loop, 10, + + [If, ([Var, x] = 5), + // then... + [Exit] + , // else + + [If, ([Var, x]34), + // then... + [LoopContinue] + , // else + + ] + [RoutineCall, print, #x] + ] + , + x ] // loop end +] // end test_loop + + +[RoutineDef, array_contains, + [VarSet, loopsize, [VarSize, #arr]] + [Exit] + , // args + arr, val, _auto_return +] // end array_contains + + +[RoutineDef, test_array_contains, + [RoutineCall, array_contains, #arr, #val, result] + , // args + arr, val +] // end test_array_contains + + +[RoutineDef, test_while, + // random internal comment + +[Loop, 65534, + + [If, ([Var, val] = [Var, cutoff]), + // then... + [Exit] + , // else + + ] + [VarAdd, val, 1] + + [If, ([Var, val] < [Var, iterations]), + // then... + [LoopContinue] + , // else + [LoopExit] + ] + , +WhileLoop ] // loop end + , // args + iterations, cutoff +] // end test_while + + +// convert 'return val' to return an automatic value +[RoutineDef, test_return_string, + [Exit] + , // args + arg, _auto_return +] // end test_return_string + + +// convert 'return val' to return an automatic value +[RoutineDef, test_return_float, + [Exit] + , // args + arg, _auto_return +] // end test_return_float + + +// convert 'return val' to return an automatic value +[RoutineDef, test_return_int, + [Exit] + , // args + arg, _auto_return +] // end test_return_int diff --git a/zbrush.py b/zbrush.py index 694dd39..69736f1 100644 --- a/zbrush.py +++ b/zbrush.py @@ -8,7 +8,7 @@ * Most inputs are strings or numbers; here they are specified as str, float or int but in zbrush they are all ascii strings or 'numbers' unless specified * there's no unicode nonsense in zbrush, all strings are ascii or utf-8 - * references to memeory blocks use the MemBlock type hint + * references to memory blocks use the MemBlock type hint * references to StrokeData use the StrokeData or MultipleStrokeData type hints Not all functions in the zscript command list are represented directly as functions. The ones that diff --git a/zsc.py b/zsc.py index 22243a3..8ef97fe 100644 --- a/zsc.py +++ b/zsc.py @@ -4,15 +4,20 @@ import ast import logging import argparse +import warnings logger = logging.getLogger(__name__) logger.addHandler(logging.StreamHandler()) logger.setLevel(logging.WARNING) +warnings.filterwarnings("ignore", category=DeprecationWarning) WARN_ON_COMPARISONS = True -VERSION = '0.1.0' +AUTO_RETURN = True + + +VERSION = "0.1.0" # these could be imported from 'zbrush' # or math. this is not a 1:1 match with @@ -30,7 +35,7 @@ "log10": "LOG10", "sqrt": "SQRT", "abs": "ABS", - "random": "RAND", + "random": "RAND", "randint": "IRAND", "bool": "BOOL", "int": "INT", @@ -38,8 +43,25 @@ } +def collect_signatures(tree): + def iterate_tree(): + for node in ast.walk(tree): + logger.debug(f"{node} -> {type(node)}") + if isinstance(node, ast.FunctionDef): + logger.debug(f"{node.name} -> {node.returns}") + yield node.name, node.returns.id if node.returns else None + + return dict(iterate_tree()) + + class Analyzer(ast.NodeVisitor): - def __init__(self, indent=0, input_file='', context=None): + + USER = "user_function" + ZB = "zbrush_function" + MEM = "zbrush mem function" + TRANSLATE = "translated function" + + def __init__(self, indent=0, input_file="", context=None, return_types=None): self.input_file = input_file self.contents = io.StringIO() self.indent = indent @@ -47,11 +69,10 @@ def __init__(self, indent=0, input_file='', context=None): self.stack = [] self.defined = [] self.zbrush = [] - self.funcs = { - 'array': 'VarDef', - 'min': 'MIN', - 'max': 'MAX' - } + self.funcs = {"array": "VarDef", "min": "MIN", "max": "MAX"} + self.return_types = return_types + if not self.return_types: + self.return_types = self.context.return_types if self.context else {} self.funcs.update(**KNOWN_MATH_FUNCS) @@ -65,29 +86,59 @@ def __init__(self, indent=0, input_file='', context=None): def format(self): "newline separated list, with tabs" + def yield_values(): for s in self.stack: yield self.tab() + s - return '\n'.join(yield_values()) + + return "\n".join(yield_values()) def format_inline(self, sep=", "): - """ comma separated list""" + """comma separated list""" result = sep.join(self.stack) return result def tab(self, extra=0): - return ' ' * (self.indent + extra) + return " " * (self.indent + extra) + + def get_assignment_info(self, callnode): + is_attrib = isinstance(callnode.func, ast.Attribute) + if hasattr(callnode.func, "value"): + owner_name = callnode.func.value.id + func_name = callnode.func.attr + else: + owner_name = "" + func_name = callnode.func.id + + is_mem_call = func_name.startswith("Mem") + info = { + "is_attrib": is_attrib, + "owner_name": owner_name, + "func_name": func_name, + "is_mem_call": is_mem_call, + "type": None, + "args": [self.as_literal(a) for a in callnode.args], + } + + if not is_attrib: + if func_name in self.funcs: + info["type"] = self.TRANSLATE + else: + info["type"] = self.USER + return info + info["type"] = self.MEM if is_mem_call else self.ZB + return info - def visit_Import(self, node): + def visit_Import(self, node): for name in node.names: - if 'zbrush' in name.name: + if "zbrush" in name.name: zname = name.name.split(".")[-1] self.funcs[name.asname or name.name] = zname def visit_ImportFrom(self, node): - if node.module == 'zbrush': + if node.module == "zbrush": for name in node.names: self.funcs[name.asname or name.name] = name.name @@ -96,7 +147,7 @@ def visit_Return(self, node): There's not equivalent of 'return values' in zbrush, so abort if the python script tries to return values. Otherwise, return an [Exit] """ - if node.value: + if node.value and not AUTO_RETURN: self.abort(f"zscript does not support return values", node) self.stack.append("[Exit]") @@ -111,27 +162,36 @@ def visit_Assert(self, node): def visit_For(self, node): iterator = node.iter - if not isinstance(iterator, ast.Call) or iterator.func.id not in ('range', 'xrange'): + if not isinstance(iterator, ast.Call) or iterator.func.id not in ( + "range", + "xrange", + ): self.abort("use range() to set loop iterations", node) - loop_max = iterator.args[0].n + if len(iterator.args) > 1: + self.abort("range() only takes one argument", node) + loop_max = 0 + if hasattr(iterator.args[0], "n"): + loop_max = iterator.args[0].n + else: + loop_max = iterator.args[0].id loop_var = node.target.id - self.stack.append("") - self.stack.append(f'[Loop, {loop_max},') + self.stack.append(f"[Loop, {loop_max},") + loop_parser = self.sub_parser(*node.body) + loop_parser.indent += 1 # loop body self.stack.append(loop_parser.format()) self.stack.append(self.tab() + ",") - self.stack.append(self.tab() + f"{loop_var}") - self.stack.append('] // loop end') + self.stack.append(f"{loop_var} ] // loop end") def visit_Continue(self, node): - self.stack.append('[LoopContinue]') + self.stack.append("[LoopContinue]") def visit_Break(self, node): - self.stack.append('[LoopExit]') + self.stack.append("[LoopExit]") def visit_FunctionDef(self, node): """ @@ -140,17 +200,39 @@ def visit_FunctionDef(self, node): """ incoming_args = [j.arg for j in node.args.args] - arg_string = ', '.join(incoming_args) - self.stack.append('') # space before defs for readability + self.stack.append("") # space before defs for readability # prepend the docstring doc = ast.get_docstring(node) if doc: - for line in doc.split('\n'): + for line in doc.split("\n"): self.stack.append(f"// {line} ") - self.stack.append('[RoutineDef, {},'.format(node.name)) + return_node = None + for n in node.body: + if isinstance(n, ast.Return): + return_node = n + + if return_node: + if AUTO_RETURN: + incoming_args.append("_auto_return") + node.body.remove(return_node) + node.body.append( + ast.Assign( + targets=[ast.Name("_auto_return")], value=return_node.value + ) + ) + node.body.append(ast.Return(value=None)) + + else: + self.abort( + "zbrush functions don't use retrun values, consider AUTO_RETURN", + return_node, + ) + arg_string = ", ".join(incoming_args) + + self.stack.append("[RoutineDef, {},".format(node.name)) body_nodes = [e for e in node.body] # docstring will appear as an expression node # if present @@ -161,10 +243,10 @@ def visit_FunctionDef(self, node): self.stack.append(sub_parse.format()) if arg_string: - self.stack.append(f'{self.tab(1)}, // args ') - self.stack.append(f'{self.tab(1)}{arg_string}') + self.stack.append(f"{self.tab(1)}, // args ") + self.stack.append(f"{self.tab(1)}{arg_string}") self.stack.append(f"] // end {node.name}") - self.stack.append('') + self.stack.append("") def visit_Num(self, node): """ @@ -173,7 +255,7 @@ def visit_Num(self, node): self.stack.append("{}".format(node.n)) def visit_Interactive(self, node): - """" + """ " won't show up in raw code, used as a generic node container for sub-parsers """ @@ -183,7 +265,7 @@ def visit_Str(self, node): """ Make sure string literals have quotes """ - self.stack.append('\"{}\"'.format(node.s)) + self.stack.append('"{}"'.format(node.s)) def visit_Name(self, node): """ @@ -196,77 +278,69 @@ def visit_Name(self, node): def get_setter(self, name): if self.context or name in self.defined: - return 'VarSet' + return "VarSet" self.defined.append(name) return "VarDef" def visit_BinOp(self, node): - ''' + """ Format zbrush supported math operators - ''' + """ # Q: Do we want 'val' instead of 'var' here? try: op = { - ast.Add: '+', - ast.Sub: '-', - ast.Mult: '*', - ast.Div: '/', - ast.Pow: '^^', - ast.And: '&& ', - ast.Or: '||' + ast.Add: "+", + ast.Sub: "-", + ast.Mult: "*", + ast.Div: "/", + ast.Pow: "^^", + ast.And: "&& ", + ast.Or: "||", }[type(node.op)] left = self.sub_parser(node.left) right = self.sub_parser(node.right) self.stack.append( - f"({left.format_inline(sep= ' ')} {op} {right.format_inline(sep = ' ')})") + f"({left.format_inline(sep= ' ')} {op} {right.format_inline(sep = ' ')})" + ) except: - self.abort( - "ZBrush does not support operator {}".format(node.op), node) + self.abort("ZBrush does not support operator {}".format(node.op), node) def visit_BoolOp(self, node): - op = { - ast.Or: '||', - ast.And: '&& ' - }[type(node.op)] + op = {ast.Or: "||", ast.And: "&& "}[type(node.op)] left = self.sub_parser(node.values[0]) right = self.sub_parser(node.values[1]) self.stack.append( - f'({left.format_inline(sep = " ")} {op} {right.format_inline(sep = " ")})') + f'({left.format_inline(sep = " ")} {op} {right.format_inline(sep = " ")})' + ) def visit_UnaryOp(self, node): if isinstance(node.op, ast.USub): target = self.as_literal(node.operand) self.stack.append(f"[NEG,{target} ]") return - raise RuntimeError(f"Unsuporter oprations {node.op}") + raise RuntimeError(f"Unsupported operator: {node.op}") def visit_AugAssign(self, node): - ''' + """ Convert python augmented assigns like variable += 5 to [VarAdd, variable, 5] etc - ''' + """ - op = { - ast.Add: 'Add', - ast.Sub: 'Sub', - ast.Mult: 'Mul', - ast.Div: 'Div' - } + op = {ast.Add: "Add", ast.Sub: "Sub", ast.Mult: "Mul", ast.Div: "Div"} try: opstring = op[type(node.op)] except: - self.abort( - "ZBrush does not support augmented operator {}".format(node.op), node) + self.abort(f"ZBrush does not support augmented operator {node.op}", node) target_var = node.target.id val = node.value @@ -274,11 +348,11 @@ def visit_AugAssign(self, node): parser = self.sub_parser(val) target_value = parser.format_inline() - self.stack.append(f'[Var{opstring}, {target_var}, {target_value}]') + self.stack.append(f"[Var{opstring}, {target_var}, {target_value}]") def format_mem_op(self, method): - ''' - helper method convert, eg, + """ + helper method convert, eg, some_mem_block.read_string(offset) @@ -286,40 +360,58 @@ def format_mem_op(self, method): [MemReadString, some_mem_block, offset] - or + or some_mem_block.write_ulong(value) - to + to [MemWrite, some_mem_block, value, 6] where '6' is the zbrush typecode for ulongs - ''' - - m_name, _, m_type = method.partition("_") + """ - if m_name not in ('read', 'write', 'resize', 'move', 'delete', 'multi_write', 'create_from_file'): + m_name, _, m_type = method.partition("_") + + if m_name not in ( + "create", + "read", + "write", + "resize", + "move", + "delete", + "multi_write", + "create_from_file", + ): return None, None - if m_type == 'string': - return f'Mem{m_name.title()}String', None + if m_type == "string": + return f"Mem{m_name.title()}String", None typecode = { - 'float': 0, - 'char': 1, - 'uchar': 2, - 'short': 3, - 'ushort': 4, - 'long': 5, - 'ulong': 6, - 'fixed': 7 + "float": 0, + "char": 1, + "uchar": 2, + "short": 3, + "ushort": 4, + "long": 5, + "ulong": 6, + "fixed": 7, }.get(m_type, 0) - return f'Mem{m_name.title()}', typecode + return f"Mem{m_name.title()}", typecode def visit_Call(self, node): + info = self.get_assignment_info(node) + if info["type"] == self.USER: + args = [self.as_literal(a) for a in node.args] + args = ", ".join(args) + if args: + args = ", " + args + self.stack.append(f"[RoutineCall, {info['func_name']}{args}]") + return + is_attrib = isinstance(node.func, ast.Attribute) is_zb = False # is this a recognized call is_mem_call = False @@ -327,12 +419,12 @@ def visit_Call(self, node): if is_attrib: owner_name = node.func.value.id func_name = node.func.attr - is_mem_call = (owner_name != "zbrush") + is_mem_call = owner_name != "zbrush" else: owner_name = "" func_name = node.func.id - is_zb = owner_name == 'zbrush' or func_name in self.funcs + is_zb = owner_name == "zbrush" or func_name in self.funcs # collect the arguments arg_parser = self.sub_parser(*node.args, func=True) @@ -345,14 +437,14 @@ def visit_Call(self, node): if func_name in self.funcs: func_name = self.funcs.get(node.func.id, func_name) - func_string = f'[{func_name}{arg_string}]' + func_string = f"[{func_name}, {arg_string}]" self.stack.append(func_string) return if not is_mem_call: # it's not a zbrush call or a memblock function, # so we assume it's a routine call - func_string = '[RoutineCall, {}{}]' .format(func_name, arg_string) + func_string = "[RoutineCall, {}{}]".format(func_name, arg_string) self.stack.append(func_string) return @@ -361,36 +453,36 @@ def visit_Call(self, node): m_name, typecode = self.format_mem_op(func_name) if not m_name: # this will fail on, eg, a random python imported function - self.abort( - f"Unrecognized operation {owner_name}.{func_name}", node) + self.abort(f"Unrecognized operation {owner_name}.{func_name}", node) # we have to insert the appropriate type code for value types here arg_parse = self.sub_parser(*node.args, func=True) args = arg_parse.stack if typecode: args.insert(1, typecode) - tail = '' + tail = "" if args: tail = ", ".join(args) tail = ", " + tail - if 'Write' in m_name: - self.stack.append(f'[{m_name}, {node.func.value.id}{tail}]') + if "Write" in m_name: + self.stack.append(f"[{m_name}, {node.func.value.id}{tail}]") else: - self.stack.append(f'[{m_name}, {node.func.value.id}{tail}]') + self.stack.append(f"[{m_name}, {node.func.value.id}{tail}]") def visit_Delete(self, node): - self.stack.append(f'[MemDelete, {node.targets[0].id}]') + self.stack.append(f"[MemDelete, {node.targets[0].id}]") def as_literal(self, val): - if hasattr(val, 's'): + if isinstance(val, ast.Constant): + return repr(val.value) + if hasattr(val, "s"): return f'"{val.s}"' - if hasattr(val, 'n'): + if hasattr(val, "n"): return val.n if isinstance(val, ast.Name): - return f'#{val.id}' - - raise ValueError(f"cannot parse {val} as literal") + return f"#{val.id}" + return str(val) def handle_array_assign(self, node): """ @@ -405,7 +497,7 @@ def handle_array_assign(self, node): [VarSet, xxx(1), 2] [VarSet, xxx(2), 3] - and + and xxx = [3] * 10 @@ -413,8 +505,8 @@ def handle_array_assign(self, node): [VarDef, xxx(10), 3] - note that the original arrays need to be homogeneous examples - of numbers or strings or variable refs. THe transpiler won't + note that the original arrays need to be homogeneous examples + of numbers or strings or variable refs. Th e transpiler won't follow variable refs to check types """ @@ -432,11 +524,10 @@ def handle_array_assign(self, node): fill = self.as_literal(emplace[0]) elif isinstance(node.value, ast.BinOp): if type(node.value.op) not in (ast.Mult, ast.Add): - self.abort( - f"operator {node.value.op} not supported here", node) + self.abort(f"operator {node.value.op} not supported here", node) op = node.value if not isinstance(op.left, ast.List): - self.abort("could not assignment expression", node) + self.abort("could not assign expression", node) if type(op.op) == ast.Mult: arr = [i for i in op.left.elts] original_arr = arr[:] @@ -453,17 +544,19 @@ def handle_array_assign(self, node): emplace = [i for i in arr] else: self.abort( - f"can only parse array literals or array literal muliplies ", node) + f"can only parse array literals or array literal muliplies ", node + ) self.stack.append(f"[{setter}, {varname}({count}), {fill}]") if emplace: for idx, item in enumerate(emplace): self.stack.append( - f"[VarSet, {varname}({idx}), {self.as_literal(item)}]") + f"[VarSet, {varname}({idx}), {self.as_literal(item)}]" + ) def visit_Assign(self, node): - varval = (node.value) - varname = (node.targets[0].id) + varval = node.value + varname = node.targets[0].id setter = self.get_setter(varname) if isinstance(varval, ast.BinOp) and isinstance(varval.left, ast.List): @@ -474,99 +567,84 @@ def visit_Assign(self, node): self.handle_array_assign(node) return - if type(varval) in (ast.Num, ast.Str, ast.Name): + if type(varval) in (ast.Constant, ast.Num, ast.Str, ast.Name): varval = self.as_literal(varval) elif isinstance(varval, ast.UnaryOp): if isinstance(varval.op, ast.USub): target = self.as_literal(varval.operand) - self.stack.append (f"[{setter}, {varname}, [NEG, {target}]]") + self.stack.append(f"[{setter}, {varname}, [NEG, {target}]]") return elif isinstance(varval, ast.Call): # odo - refactor this out - - if isinstance(varval.func, ast.Attribute): - var_root = varname.split("(")[0] - if self.context or (var_root in self.top_level_defs): - setter = 'VarSet' - else: - setter = 'VarDef' - self.top_level_defs.append(var_root) - - caller = ", " + varval.func.value.id - - is_mem_create = varval.func.attr == 'MemCreate' - - if is_mem_create: - arg_parser = self.sub_parser(*varval.args) - arg_string = arg_parser.format_inline() - if arg_string: - arg_string = ", " + arg_string - - self.stack.append(f'[MemCreate, {varname}{arg_string}]') - return - - allowed_funcs = ( - "read_", - ) - - is_allowed = False - for f in allowed_funcs: - is_allowed = is_allowed or f in varval.func.attr - - if (not is_allowed and varval.func.attr not in self.funcs): - self.abort( - "can only call zbrush functions or memory block functions in an assignment", node) - - if varval.func.attr in self.funcs: - m_name = self.funcs[varval.func.attr] - typecode = None - caller = "" - else: - # it's a memory object functon - m_name, typecode = self.format_mem_op(varval.func.attr) - if not m_name: - self.abort( - f"Unrecognized memory operation {varval.func.attr}", node) - - arg_parse = self.sub_parser(*varval.args, func=True) - args = arg_parse.stack - if typecode: - args.insert(1, typecode) - tail = '' - if args: - tail = ", ".join(args) - tail = ", " + tail - self.stack.append( - f'[{setter}, {varname}, [{m_name}{caller}{tail}]]') - return - else: - if varval.func.id == "len": - # is a pound sign needed here? - self.stack.append( - f"[VarSet, {varname}, [VarSize, {varval.args[0].id}]]") - return - elif varval.func.id in self.funcs: - args = [str(self.as_literal(v)) for v in varval.args] - if args: - args = ", ".join(args) - - func_name = self.funcs.get(varval.func.id) - self.stack.append(f'[{setter}, {varname}, [{func_name}, {args}]]') - return - self.abort("Can't assign a function call in ZBrush", varval) - - elif isinstance(varval, ast.BinOp): - parser = self.sub_parser(varval) - varval = parser.format_inline() - - if not self.context and varval not in self.defined: - setter = f'[VarDef, {varname}, {varval}]' - self.defined.append(varname) + info = self.get_assignment_info(varval) + handler = { + self.USER: self.visit_user_assign, + self.ZB: self.visit_zbrush_assign, + self.MEM: self.visit_mem_assign, + self.TRANSLATE: self.visit_translate_assign, + } + + process = handler.get(info["type"]) + process(varval, varname, setter, info) + + def visit_user_assign(self, varval, varname, setter, info): + if not AUTO_RETURN: + self.abort("Can't assign a user-defined function call in ZBrush", varval) else: - setter = f"[VarSet, {varname}, {varval}]" - self.stack.append(setter) + """ + For auto-return functions, we create a variable to hold the return value, + add an argument to the func def, and then call it. + + @todo: inspect the outer context to get the right variable type + """ + return_type = self.return_types.get(varval.func.id, 0) + + default = {"str": '""', "int": "0", "float": "0.0", None: 0}.get( + self.return_types.get(varval.func.id) + ) + + """got to untangle this""" + # print(ast.dump(varval)) + # varval.body.append(f"[{setter}, {varname}, {default}] // autoreturn") + varval.args.append(varname) + self.visit_Call(varval) + return + + def visit_zbrush_assign(self, varval, varname, setter, info): + zbr_args = info["func_name"] + if info["args"]: + zbr_args += ", " + zbr_args += ", ".join(info["args"]) + + self.stack.append(f"[{setter}, {varname}, [{zbr_args}]]") + + return + + def visit_translate_assign(self, varval, varname, setter, info): + zfunc = KNOWN_MATH_FUNCS.get(info["func_name"]) + sub_parser = self.sub_parser(varval) + processed_args = sub_parser.format_inline() + self.stack.append(f"[{setter}, {varname}, {processed_args}]") + return + + def visit_mem_assign(self, varval, varname, setter, info): + + op = info["func_name"] + args = info["args"] + args[0] = str(args[0]).replace("#", "") + arg_string = ", ".join(args) + # make sure tht the first argument s not + # a variable reference or a string + initial, rest = arg_string.split(",", 1) + initial = initial.replace("#", "") + initial = initial.replace("'", "") + arg_string = initial + "," + rest + if op == "MemCreate": + self.stack.append(f"// blockID = {initial}") + self.stack.append(f"[{setter}, {varname}, [{op}, {arg_string}]]") + return def visit_Lt(self, node): self.stack.append(" < ") @@ -575,7 +653,7 @@ def visit_Gt(self, node): self.stack.append(" > ") def visit_Lte(self, node): - self.stack.append(" <=") + self.stack.append(" <= ") def visit_Gte(self, node): self.stack.append(" >= ") @@ -589,7 +667,7 @@ def visit_NotEq(self, node): def visit_If(self, node): test = self.sub_parser(node.test) - comp = ''.join(test.stack) + comp = "".join(test.stack) body = self.sub_parser(*node.body) body_str = body.format() or "" @@ -597,20 +675,20 @@ def visit_If(self, node): orelse = self.sub_parser(*node.orelse) else_str = orelse.format() or "" - self.stack.append('') + self.stack.append("") self.stack.append(f"[If, ({comp}),") self.stack.append(f"{self.tab()}// then...") self.stack.append(body_str) self.stack.append(f"{self.tab() or ' '}, // else") self.stack.append(else_str) - self.stack.append(']') + self.stack.append("]") def visit_Expr(self, node): if isinstance(node.value, ast.Str): - self.stack.append(f'// {node.value.s}') + self.stack.append(f"// {node.value.s}") else: sub_parser = self.sub_parser(node.value) - comp = ''.join(sub_parser.stack) + comp = "".join(sub_parser.stack) self.stack.append(comp) def visit_While(self, node): @@ -618,7 +696,7 @@ def visit_While(self, node): breakout = ast.If( test=node.test, body=[ast.Expr(value=ast.Continue())], - orelse=[ast.Expr(value=ast.Break())] + orelse=[ast.Expr(value=ast.Break())], ) body_block = [i for i in node.body] @@ -626,9 +704,10 @@ def visit_While(self, node): new_node = ast.For( target=ast.Name("WhileLoop"), - iter=ast.Call(func=ast.Name(id="range"), - ctx=ast.Load(), args=[ast.Num(n=65534)]), - body=body_block + iter=ast.Call( + func=ast.Name(id="range"), ctx=ast.Load(), args=[ast.Num(n=65534)] + ), + body=body_block, ) try: self.indent -= 1 @@ -647,13 +726,11 @@ def abort(self, message, node): fail the transpilation and print an error message """ - error_line = self.input_file.splitlines( - )[node.lineno - 2: node.lineno + 1] - raise ValueError("Compile Error: {} in line {}".format( - message, node.lineno), error_line) + error_line = self.input_file.splitlines()[node.lineno - 2 : node.lineno + 1] + raise ValueError(f"Compile Error: {message} in line {node.lineno}", error_line) def sub_parser(self, *args, **kwargs): - if kwargs.get('func'): + if kwargs.get("func"): sub_parser = FunctionAnalyzer(context=self) else: sub_parser = Analyzer(context=self) @@ -673,22 +750,26 @@ def visit_Name(self, node): self.stack.append(node.id) -def compile(filename, out_filename=''): +def compile(filename, out_filename=""): with open(filename, "r") as source: input_file = source.read() tree = ast.parse(input_file) - analyzer = Analyzer(0, input_file=input_file) + return_types = collect_signatures(tree) + analyzer = Analyzer(0, input_file=input_file, return_types=return_types) analyzer.visit(tree) - out_filename = out_filename or filename.replace('.py', '.txt') + out_filename = out_filename or filename.replace(".py", ".txt") - with open(out_filename, 'wt') as output: - tp = (f'transpiled with zsc {VERSION}') - orig = f'from: {filename}' + with open(out_filename, "wt") as output: + tp = f"transpiled with zsc {VERSION}" + orig = f"from: {filename}" output.write(f"/*\n{tp}\n{orig}\n*/\n\n") + + if AUTO_RETURN: + output.write("// automatic return vars\n") output.write(analyzer.format()) return out_filename, analyzer.format() @@ -696,17 +777,21 @@ def compile(filename, out_filename=''): if __name__ == "__main__": parser = argparse.ArgumentParser( - prog="zsc", - description=f'Python to ZScript transpiler ({VERSION})' + prog="zsc", description=f"Python to ZScript transpiler ({VERSION})" ) parser.add_argument("input", help="path to python source file") parser.add_argument( - "--output", help="optional output file (otherwise, uses the same name as the input file with .txt extension)") + "--output", + help="optional output file (otherwise, uses the same name as the input file with .txt extension)", + ) parser.add_argument( - "--show", help="if true, print the transpiled file to stdout", action='store_true') + "--show", + help="if true, print the transpiled file to stdout", + action="store_true", + ) args = parser.parse_args() - output, result = compile(args.input, out_filename=args.output or '') + output, result = compile(args.input, out_filename=args.output or "") if args.show: print(result) diff --git a/ztransform.py b/ztransform.py new file mode 100644 index 0000000..bb35ea1 --- /dev/null +++ b/ztransform.py @@ -0,0 +1,193 @@ +""" +Transformers +""" + +import ast +import pprint + + +class CompilationError(RuntimeError): + pass + + +class ZAutoReturn(ast.NodeTransformer): + def visit_FunctionDef(self, node: ast.FunctionDef): + self.generic_visit(node) + setattr(node, "zbrush", "routinedef") + if not node.returns: + return node + + node.args.args.append(ast.Name(id="_retval", ctx=ast.Load())) + node.returns = ast.Name(id=f"auto_{node.returns.id}", ctx=ast.Load()) + self.recurse_insert_returns(node) + return node + + def recurse_insert_returns(self, node): + """ + recursively replaces return values + with the _retval + """ + inserts = [] + values = [] + for bnode in node.body: + if hasattr(bnode, "body"): + self.recurse_insert_returns(bnode) + + for idx, b in enumerate(node.body): + if isinstance(b, ast.Return): + inserts.append(idx) + values.append(b.value) + inserts.reverse() + values.reverse(), + for i, v in zip(inserts, values): + t = ast.Assign( + targets=[ast.Name("_retval", ctx=ast.Store())], + value=v, + type_comment=None, + ) + + node.body.insert(i, t) + ast.increment_lineno(t) + + +class ZStripReturnValues(ast.NodeTransformer): + + def visit_Return(self, retnode): + return ast.Return() + + +def apply_auto_returns(astnode): + inserts = ZAutoReturn().generic_visit(astnode) + return ZStripReturnValues().generic_visit(inserts) + + +class ZFuncs(ast.NodeTransformer): + """ + strips "zbrush" from zfunc names, + adds an attribute to identify them + """ + + def visit_Call(self, node): + if not isinstance(node.func, ast.Attribute): + return node + + attrib = node.func + if attrib.value.id != "zbrush": + # if a helper module is imnplemented add it here + raise CompilationError( + f"line {node.lineno}: only the zbrush module is supported", + ast.unparse(node), + ) + + test = ast.Call( + ast.Name(attrib.attr.upper(), ctx=ast.Load()), args=node.args, keywords={} + ) + setattr(test, "zbrush", "func") + return test + + +class ZVarSet(ast.NodeTransformer): + """ + turns assignments into zbrush setters + """ + + def visit_Module(self, mod): + self.defs = set() + + for node in mod.body: + if (isinstance, node, ast.Assign): + self.defs.add(node) + return mod + + def visit_Assign(self, node): + if node in self.defs: + return self.update_assign(node, "VarDef") + else: + return self.update_assign(node, "VarSet") + + def update_assign(self, node, setter): + assignee = node.targets[0].id + if assignee == "__": + return node + val = node.value + call = ast.Call( + func=ast.Name(setter, ast.Load()), + args=[ast.Name(assignee, ast.Store()), val], + keywords={}, + ) + node.targets = [ast.Name("__", ctx=ast.Store())] + node.value = call + setattr(node, "zbrush", setter) + return node + + +class ZForLoop(node.NodeTransformer): + def visit_For(self, node): + # print(ast.dump(node)) + return node + + +test = ''' +import zbrush + +def test_xxx(arg): + print ("i do nothing") + zbrush.test(1,2,3) + x = zbrush.test(4,5,6) + return + +def test_iterator(arg) -> int: + for x in range(arg): + if x == 3: + return x * 2 + return x == 1 + +bob = test_iterator(2) + +def test_array_contains(arr, val) -> int: + result = array_contains(arr, val) + + +# todo: figure out how to handle retruns in cases inside loops and whiles + + +def test_while(iterations, cutoff) -> int: + val = 0 + """random internal comment""" + otherval = 1.0 + while val < iterations: + if val == cutoff: + return val + val += 1 + + +def test_return_string(arg) -> str: + """convert 'return val' to return an automatic value""" + val = arg + "xxx" + return val + + +def test_return_float(arg) -> float: + """convert 'return val' to return an automatic value""" + val = arg + 1.0 + return val + + +def test_return_int(arg) -> int: + """convert 'return val' to return an automatic value""" + val = arg + 1 + return val + +''' + +parsed = ast.parse(test) +print(ast.dump(parsed)) +transformed = apply_auto_returns(parsed) +transformed = ZFuncs().generic_visit(transformed) +setter = ZVarSet() +transformed = setter.visit_Module(transformed) +transformed = setter.generic_visit(transformed) +# transformed = ZAutoReturnReturn().visit(parsed) +# pprint.pprint(ast.dump(transformed), indent=4) +print("-" * 80) +print(ast.unparse(transformed))