diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6d2b1b37 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +a.out +test.c +test.bin +.vscode +*.code-workspace +.DS_Store +build diff --git a/Dockerfile b/Dockerfile index d861c44b..959e68bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,5 @@ FROM python:3.6 COPY . /src +WORKDIR /src +RUN ./run_tests.sh ENTRYPOINT /src/entrypoint.sh diff --git a/README.md b/README.md new file mode 100644 index 00000000..22991c45 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# megadrive-python +A minimalistic Python compiler for the Sega Megadrive aiming to support a narrow subset of Python 3.6. + diff --git a/main.py b/main.py deleted file mode 100644 index 6c4e0123..00000000 --- a/main.py +++ /dev/null @@ -1,12 +0,0 @@ -def main() -> int: - z: int = get42() - x: int = 30 - if z > x: - print('Hello World') - return x - -def get42() -> int: - return 42 - -if __name__ == "__main__": - return main() diff --git a/pyc.py b/pyc.py old mode 100644 new mode 100755 index b7e0079b..7f1f9f33 --- a/pyc.py +++ b/pyc.py @@ -1,505 +1,398 @@ -#!/usr/bin/env python +#!/usr/bin/env python3.6 import argparse import ast import logging import os import sys -LOG = logging.getLogger(__name__) +from collections import namedtuple -# These functions are called in python by name on left and in C name by name -# on right. -BUILTIN_FUNCS = { - 'print': ast.parse('def printf(s: str): pass').body[0], -} +assert sys.version_info[:2] == (3, 6) -# These are the C types for various python types supported by this compiler -BUILTIN_TYPES = { - 'int': 'int32_t', - 'str': 'char*', -} +LOG = logging.getLogger(__name__) -# A static prefix/suffix for module level things -MOD_PREFIX = 'PYMOD_' -MOD_INIT_SUFFIX = '_INIT' +ScopeEntry = namedtuple('ScopeEntry', ['name', 'type', 'callable']) +BiLangScopeEntry = namedtuple('BiLangScopeEntry', ['c', 'py']) -# How are dots from python references represented in C? -DOT = '_DOT_' +class Scope(dict): + def __init__(self, parent=None, prefix=None): + self.prefix = prefix + if parent: + dict.__init__(self, parent) + else: + dict.__init__(self) -class CompileError(RuntimeError): - def __init__(self, msg, node): - if type(node) != ast.Module: - self._msg = '{}:{} {}'.format(node.lineno, node.col_offset, msg) + def add_entry(self, c, py): + self[py.name] = BiLangScopeEntry(c=c, py=py) + + def suggest_c_name(self, py_name): + if self.prefix: + return '{}_DOT_{}'.format(self.prefix, py_name) else: - self._msg = msg - - def msg(self): - return 'CompileError: {}'.format(self._msg) - - def __str__(self): - return self._msg - - -class FunctionCompiler(ast.NodeVisitor): - def __init__(self, module_name, module_compiler, node): - self.module_name = module_name - self.module_compiler = module_compiler - self.node = node - - self.locals = {} - - def _ctype(self, node): - """Distill an ast node down the C type which it will evaluate to - - This is actually quite tricky, as a node can be a function call, - a reference to a local or global variable, a constant, etc. - """ - LOG.debug('determining type of %s', ast.dump(node)) - # For function calls, use the function definition of the callee - if type(node) == ast.Call: - return self._ctype(node.func) - - # For function definitions use the return type annotated - if type(node) == ast.FunctionDef: - return self._ctype(node.returns) - - # If a variable was declared with an annotated assignment return the - # type annotated at the time of assignment - if type(node) == ast.AnnAssign: - return self._ctype(node.annotation) - - # If a const value is passed in return the C type for the python type - if type(node) == ast.Num: - return BUILTIN_TYPES['int'] - if type(node) == ast.Str: - return BUILTIN_TYPES['str'] - - # If the node passed in is a reference - if type(node) == ast.Name: - # If the reference is to a builtin type, return that type - if node.id in BUILTIN_TYPES: - return BUILTIN_TYPES[node.id] - - # Load the declaration for the variable referenced and return the - # storage type annotated at the time of declaration - _, val = self._load_name(node) - return self._ctype(val) - - # Look for references to None - if type(node) == ast.NameConstant and node.value == None: - return 'void' - - # Nothing was found (probably a bug) - raise LookupError( - 'BUG: cannot determine C type for {}'.format(ast.dump(node))) - - def _fn_ret_ctype(self, fn: ast.FunctionDef): - # It is okay for functions to lack annotations for return types, but - # only if they do not contain any return statement - if self.node.returns == None: - for fn_node in ast.walk(self.node): - if type(fn_node) == ast.Return: - raise CompileError( - 'missing return type annotation for function `{}`' - .format(self.node.name), self.node) + return py_name + + def resolve(self, node): + py_name_parts = [] + def add_node(sub_node): + if type(sub_node) == ast.Attribute: + add_node(sub_node.value) + py_name_parts.append(sub_node.attr) + elif type(sub_node) == ast.Name: + py_name_parts.append(sub_node.id) else: - return 'void' - else: - # For functions with return type annotations, determine the C type - # for the annotated python type - try: - return self._ctype(self.node.returns) - except LookupError: - raise CompileError( - 'unsupported return type `{}` for function `{}`' - .format(ast.dump(self.node.returns), fn_name), - self.node) + raise NotImplementedError('cannot resolve from {}'.format(ast.dump(sub_node))) + + add_node(node) + LOG.debug('py_name_parts: %r', py_name_parts) + cur = self + for py_name in py_name_parts: + cur = cur[py_name] + return cur + + def dict(self) -> dict: + return dict(self) + +BUILTIN = Scope() + +SegaScope = Scope() +SegaScope.add_entry( + c=ScopeEntry(name='VDP_init', type=None, callable=True), + py=ScopeEntry(name='init', type=None, callable=True) +) + +SegaScope.add_entry( + c=ScopeEntry(name='VDP_drawText', type=None, callable=True), + py=ScopeEntry(name='draw_text', type=None, callable=True) +) + +SysScope = Scope() +SysScope.add_entry( + c=ScopeEntry(name='exit', type=None, callable=True), + py=ScopeEntry(name='exit', type=None, callable=True), +) + +BUILTIN_MODS = { + 'sys': SysScope, + 'vdp': SegaScope, +} - def compile(self): - LOG.debug('Compiling: ' + ast.dump(self.node)) +main_scope = Scope(BUILTIN, prefix='MOD___main__') - # Fill the locals with parameters passed into the function - if type(self.node) == ast.FunctionDef: - # This is confusing for sure...here is an example data structure: - # FunctionDef(name='main', args=arguments(args=[arg(arg='x', annotation=None), ... - # See the docs on ast.FunctionDef, ast.arguments, ast.args, and ast.arg - for arg in self.node.args.args: - self.locals[arg.arg] = arg - src = '' - for body_node in self.node.body: - node_src = self.visit(body_node) - LOG.debug('source for node %s: %r', body_node, node_src) - if node_src: - src += node_src + '\n' - - # Check for use of decorators, which is not supported - if type(self.node) == ast.FunctionDef and self.node.decorator_list: - raise CompileError( - 'function decorators are not supported', - self.node.decorator_list[0]) - - # Get the return type for this function - if type(self.node) != ast.Module: - ret_type = self._fn_ret_ctype(self.node) - if ret_type == None: - raise CompileError( - 'unable to determine return type of function `{}`' - .format(self.node.name), self.node) - else: - # Modules always return int32 - ret_type = 'int32_t' +class CompileError(RuntimeError): pass - # Convert the arg specifications - if type(self.node) == ast.Module: - # Modules have no parameters - args_src = '' - else: - c_args = [] - for arg in self.node.args.args: - arg_name = arg.arg - - # Ensure the type was annotated for this argument - if not arg.annotation: - raise CompileError( - 'missing type annotation for parameter `{}`' - .format(arg_name), arg) - - try: - arg_ctype = self._ctype(arg.annotation) - except LookupError: - raise CompileError( - 'unknown type `{}` for argument `{}`' - .format(ast.dump(arg.annotation), arg_name), arg) - c_args.append('{} {}'.format(arg_ctype, arg_name)) - - args_src = ', '.join(c_args) - - if type(self.node) == ast.Module: - fn_name = MOD_PREFIX + self.module_name + MOD_INIT_SUFFIX +class BaseCompiler(ast.NodeVisitor): + def __init__(self, name, root, scope): + self.name = name + self.root = root + self.scope = scope + self.docstring = '' + try: + if type(root.body[0]) == ast.Str: + self.docstring = root.body[0].s + except AttributeError: + pass + + def generic_visit(self, node): + raise CompileError('unhandled visit: {}'.format(ast.dump(node))) + + def compiler(self) -> str: + raise NotImplementedError() + + def declare_var(self, node: ast.AnnAssign) -> str: + py_name = node.target.id + py_type = node.annotation.id + c_name = self.scope.suggest_c_name(py_name) + if py_type == 'int': + c_type = 'int32_t' + def_value = '0' + elif py_type == 'str': + c_type = 'char*' + def_value = 'NULL' else: - fn_name, _ = self._load_name(self.node) + raise NotImplementedError('unhandled py_type: {}'.format(py_type)) - # TODO: Create pre_src with #include stmts - pre_src = '' - return '{pre_src}\n{ret_type} {fn_name}({args}) {{\n{body}}}\n'.format( - pre_src=pre_src, ret_type=ret_type, fn_name=fn_name, args=args_src, - body=src, + # Register the new var in the scope + self.scope.add_entry( + py=ScopeEntry(name=py_name, type=py_type, callable=False), + c=ScopeEntry(name=c_name, type=c_type, callable=False), ) - def generic_visit(self, node): - LOG.error('Encounteder unsupported node: %r', node) - raise CompileError( - 'Unsupported ast node: {}'.format(ast.dump(node)), node) - - def _load_name(self, node): - # Functions are looked up by name, while others are looked up by id - if type(node) == ast.FunctionDef: - lookup_name = node.name - else: - lookup_name = node.id - - # Search in locals first, then module globals, then builtins - if lookup_name in self.locals: - # Items at the local scope have the same variable name in C. - return [lookup_name, self.locals[lookup_name]] - elif lookup_name in self.module_compiler.globals: - # Items resolving at the module level have a more complex naming - # scheme. - return [ - ''.join([MOD_PREFIX, self.module_name, DOT, lookup_name]), - self.module_compiler.globals[lookup_name] - ] - elif lookup_name in BUILTIN_FUNCS: - # Builtin functions have different names in C from python - return BUILTIN_FUNCS[lookup_name].name, BUILTIN_FUNCS[lookup_name] - else: - raise LookupError('no such var `{}`'.format(lookup_name)) + LOG.debug('set scope entry `%s` in scope %s', py_name, self.name) + return '{c_type} {c_name} = {def_value};'.format( + c_type=c_type, c_name=c_name, def_value=def_value) - def visit_If(self, node: ast.If): - """Return the C representation of a python if statement""" - test_src = self.visit(node.test) + def py_type(self, node): + if type(node) in [ast.Name, ast.Attribute]: + var = self.scope.resolve(node) + return var.py.type + else: + raise NotImplementedError('cannot get type of {}'.format(ast.dump(node))) - body_src = '' - for body_node in node.body: - body_src += self.visit(body_node) - if node.orelse: - orelse_src = '' - for orelse_node in node.orelse: - orelse_src += self.visit(orelse_node) +class LineCompiler(BaseCompiler): + def visit_Return(self, ret_node: ast.Return) -> str: + return 'return {}'.format(self.visit(ret_node.value)) - return 'if ({test}) {{\n{body}\n}} else {{\n{orelse}\n}}'.format( - test=test_src, body=body_src, orelse=orelse_src) - else: - return 'if ({test}) {{\n{body}\n}}'.format( - test=test_src, body=body_src) + def visit_Num(self, num_node: ast.Num) -> str: + return str(num_node.n) - def visit_Eq(self, node:ast.Eq): - return '==' + def visit_AnnAssign(self, node: ast.AnnAssign) -> str: + py_name = node.target.id + if py_name not in self.scope: + raise CompileError('assignment to undeclared variable `{}` in scope {!r}'.format(py_name, self.scope)) - def visit_Gt(self, node:ast.Gt): - return '>' - - def visit_Compare(self, node:ast.Compare): - """Return the C representation of comparison tests""" - # Find the C type of the left value - _, left_val = self._load_name(node.left) - left_type = self._ctype(left_val) - - # Find the C type of the right values and ensure they are same as left - for cmp in node.comparators: - # Lookup ast.Name nodes first, then convert their value to a type - if type(cmp) == ast.Name: - _, right_val = self._load_name(cmp) - right_type = self._ctype(right_val) - else: - # For other things like ast.Str and ast.Num just convert to a - # type directly - right_type = self._ctype(cmp) - - # Enforce that the two are the same C type - if left_type != right_type: - raise CompileError('mismatched types in comparison', node.left) - - parts = [] - parts.append(self.visit(node.left)) - for op, cmp in zip(node.ops, node.comparators): - parts.append(self.visit(op)) - parts.append(self.visit(cmp)) - LOG.debug('compare parts: %r', parts) - return ' '.join(parts) - - def visit_Call(self, node:ast.Call): - # Find the function being called - try: - func_name, func = self._load_name(node.func) - except LookupError: - raise CompileError( - 'reference to unknown function `{}`'.format(node.func.id), - node) - - # Ensure the function being called is infact a function - if type(func) != ast.FunctionDef: - raise CompileError( - 'call to non-function `{}` of type `{}`' - .format(node.func.id, func), node) - - # TODO: Check arguments - cargs = [] - for arg in node.args: - cargs.append(self.visit(arg)) - return '{}({})'.format(func_name, ', '.join(cargs)) - - def visit_Str(self, node:ast.Str): - """Return the C representation of a python string""" - # Convert strings to hex byte arrays, and include a null termination - return '(const char[]){{{}}}'.format( - ', '.join([hex(ord(c)) for c in node.s + '\0'])) - - def visit_Name(self, node:ast.Name): - """Returns the C name of a python variable""" - # _load_name returns both the cname and the ast node, but we only need - # the name. - cname, _ = self._load_name(node) - return cname - - def visit_Num(self, node: ast.Num): - """Return the C representation of a python numerical value""" - # Numbers are represented just the same in C as they are in python, so - # just convert to string and return the C representation - return str(node.n) - - def visit_Return(self, node: ast.Return): - """Return the C representation of a python 'return' statement""" - return 'return {};'.format(self.visit(node.value)) - - def visit_Expr(self, node: ast.Expr): - """Return the C representation of a python expression""" - return self.visit(node.value) + ';' - - def visit_Import(self, node: ast.Import): - LOG.debug(ast.dump(node)) - for alias in node.names: - return '#include "{}.h"\n'.format(alias.name) - - def visit_Pass(self, node: ast.Pass): - # Unlike python, no special keywords are required for a NOP body, so we - # don't actually need to do anything here - pass - - def visit_FunctionDef(self, node: ast.FunctionDef): - if type(self.node) != ast.Module: - raise CompileError('Inner functions are not supported', node) - - def visit_Assign(self, node: ast.Assign): - # Python allows multiple assignments on one line, but that - # isn't implemented here yet. - if len(node.targets) > 1: - raise CompileError( - 'Use of unsupported feature: multiple assignment', node.targets) - target = node.targets[0] - - # Do not support attribute assignment (object.x = 123) - if type(target) == ast.Attribute: - raise CompileError( - 'Use of unsupported feature: attribute assignment', node) - - # Ensure the variable has been declared - if target.id not in self.locals: - raise CompileError( - 'Cannot assign to undeclared local var `{}`' - .format(target.id), node) - - # Handle assingent of numerical constants to variables - if type(node.value) == ast.Num: - # ensure target is an int - if self.locals[target.id] != 'int': + decl = self.scope[py_name] + if decl.py.type == 'int': + if type(node.value) != ast.Num: raise CompileError( - 'assignment of int to incompatible {} var {}' - .format(self.locals[target.id], target.id), node.value) - - # prevent float assignment - if '.' in str(node.value.n): + 'assignment of non-numerical value {} to int variable `{}`' + .format(ast.dump(node), py_name)) + value_src = self.visit(node.value) + elif decl.py.type == 'str': + if type(node.value) != ast.Str: raise CompileError( - 'assignment of float to incompatible {} var {}' - .format(self.locals[target.id], target.id), node.value) + 'assignment of non-string value {} to str variable `{}`' + .format(ast.dump(node), py_name)) + value_src = self.visit(node.value) + else: + raise NotImplementedError('unhandled py_type: {}'.format(decl.py_type)) + return '{c_name} = {value_src}'.format( + c_name=decl.c.name, value_src=value_src) + + def _name_error(self, name): + raise CompileError('NameError: undefined reference `{}`'.format(name)) + + def visit_Name(self, node: ast.Name) -> str: + py_name = node.id + + if py_name not in self.scope: + self._name_error(py_name) + + c_name = self.scope[py_name].c.name + LOG.debug('c_name(%r) == %s', py_name, c_name) + return c_name + + def visit_Attribute(self, node: ast.Attribute) -> str: + if node.value.id in BUILTIN_MODS: + attr_scope = BUILTIN_MODS[node.value.id] + if node.attr not in attr_scope: + self._name_error('{}.{}'.format(node.value.id, node.attr)) + return attr_scope[node.attr].c.name + else: + self._name_error(node.value.id) - # output the code - return '{} = {};\n'.format(target.id, node.value.n) + def visit_Import(self, node: ast.Attribute) -> str: + src = '' + for alias in node.names: + # Skip modules with an internal implementation + if alias.name in BUILTIN_MODS: + continue + return src - # Handle assignment of string constants to variables - elif type(node.value) == ast.Str: - # ensure target is a string - if self.locals[target.id] != 'str': - raise CompileError( - 'assignment of str to incompatible {} var `{}`' - .format(self.locals[target.id], target.id), node.value) + def visit_Pass(self, node: ast.Pass) -> str: + return '; // do nothing\n' - # output the code - return '{} = "{}";\n'.format( - target.id, node.value.s.replace('"', '\\"')) - else: - raise CompileError( - 'Unsupported assignment of type `{}` to `{}` of type `{}`' - .format(type(node.value), target.id, self.locals[target.id]), - node.value) - - def visit_AnnAssign(self, node: ast.AnnAssign): - # Sort out whether this is a new local declaration - if node.target.id in self.locals: - raise CompilerError( - 'Local var `{}` has already been declared' - .format(node.target.id)) - - # Ensure the data types match for assignment - target_type = self._ctype(node) - value_type = self._ctype(node.value) - if target_type != value_type: - raise CompileError( - 'type mismatch in assignment of {} to {}'.format( - node.value, node.target), node.value) - - # Store this declaration in the locals table - self.locals[node.target.id] = node - - # Generate C code for the assignment - target_src = self.visit(node.target) - value_src = self.visit(node.value) - return '{} {} = {};'.format(target_type, target_src, value_src) - - -class ModuleCompiler(ast.NodeVisitor): - def __init__(self, module_name, source_filename, node, dunder_name): - self.module_name = module_name - self.globals = {} - self.node = node - self.source_filename = source_filename - self.__name__ = dunder_name - - # Declare __name__ as a string global - self.globals['__name__'] = ast.AnnAssign(annotation=ast.Name(id='str')) - - def _initial_module_source(self): - return '\n'.join([ - '#include ', - '#define {prefix}{mod_name}{DOT}__name__ "{dunder_name}"'.format( - prefix=MOD_PREFIX, mod_name=self.module_name, DOT=DOT, - dunder_name=self.__name__), - ]) + '\n\n' + def visit_NameConstant(self, node: ast.NameConstant) -> str: + if node.value == True: + return 'TRUE' + raise NotImplementedError(ast.dump(node)) - def generic_visit(self, node): - raise CompileError( - 'No matching compiler handler for node {!r}' - .format(node), node) - - def compile(self): - src = self._initial_module_source() - func_compilers = [] - - # Build a compiler for the top-level function - top_func_compiler = FunctionCompiler(self.module_name, self, self.node) - func_compilers.append(top_func_compiler) - - # Build a compiler for all other functions - for mod_node in self.node.body: - if type(mod_node) == ast.FunctionDef: - self.globals[mod_node.name] = mod_node - func_compiler = FunctionCompiler( - self.module_name, self, mod_node) - func_compilers.append(func_compiler) - - # Run the compilers - try: - for compiler in func_compilers: - src += compiler.compile() + '\n' - except CompileError as e: - e._msg = '{}:{}'.format(self.source_filename, e._msg) - raise + def visit_While(self, node: ast.While) -> str: + # Compile the test condition + test_src = self.visit(node.test) - return src + # Compile the body + body_src = '' + for body_node in node.body: + line_name = '{}:{}'.format(body_node.lineno, body_node.col_offset) + line_comp = LineCompiler(line_name, body_node, self.scope) + body_src += line_comp.compile() + if node.orelse: + raise NotImplementedError('while...else') -def parse_args(): - p = argparse.ArgumentParser(description=__doc__) - p.add_argument( - 'input_modules', nargs='+', help='Python module names for compilation') - return p.parse_args() + return 'while ({test_src}) {{\n{body_src}\n}}'.format(test_src=test_src, body_src=body_src) + def visit_If(self, node: ast.If) -> str: + # Compile the test condition + test_src = self.visit(node.test) -def main(): - logging.basicConfig(level=logging.DEBUG) - args = parse_args() - src = '' - for i, module_name in enumerate(args.input_modules): - # Figure out what __name__ will be for this module. The first module - # listed will be __main__. - if i == 0: - dunder_name = '__main__' + # Compile the body + body_src = '' + for body_node in node.body: + line_name = '{}:{}'.format(body_node.lineno, body_node.col_offset) + line_comp = LineCompiler(line_name, body_node, self.scope) + body_src += line_comp.compile() + + # Compile the orelse body + orelse_src = '' + for orelse_node in node.orelse: + line_name = '{}:{}'.format(body_node.lineno, body_node.col_offset) + line_comp = LineCompiler(line_name, body_node, self.scope) + orelse_src += line_comp.compile() + + # Build the C version + if node.orelse: + return 'if ({test_src}) {{\n{body_src}\n}} else {{ {orelse_src} }}'.format( + test_src=test_src, body_src=body_src, orelse_src=orelse_src) else: - dunder_name = module_name - - # Build a path name for the python module - filename = module_name.replace('.', '/') + '.py' + return 'if ({test_src}) {{\n{body_src}\n}}'.format(test_src=test_src, body_src=body_src) + + def visit_Compare(self, node: ast.Compare) -> str: + left_py_type = self.py_type(node.left) + if left_py_type == 'str': + if len(node.ops) != 1: + raise CompileError('string comparison is only valid against a single comparator') + op = type(node.ops[0]) + comp = node.comparators[0] + + if op == ast.Eq: + c_test = '== 0' + elif op == ast.Lt: + c_test = '== -1' + elif op == ast.Gt: + c_test = '== 1' + elif op == ast.NotEq: + c_test = '!= 0' + else: + raise CompileError('invalid string comparison operator {}'.format(ast.dump(op))) + + left_src = self.visit(node.left) + right_src = self.visit(comp) + return 'strcmp({}, {}) {}'.format(left_src, right_src, c_test) + else: + parts = [self.visit(node.left)] + for op, comp in zip(node.ops, node.comparators): + parts.append(self.visit(op)) + parts.append(self.visit(comp)) + return ' '.join(parts) - # Open parse, and compile the file - with open(filename) as fh: - # Parse the file using standard python parser which gives back a - # data structure called an AST representing the code - module = ast.parse(fh.read()) + def visit_Eq(self, node: ast.Eq) -> str: + return '==' - # Using the returned AST generate C code - compiler = ModuleCompiler( - module_name, filename, module, dunder_name) + def visit_Str(self, node: ast.Str) -> str: + return '"{}"'.format(str(node.s)) - # Add the generated C code to the project source - src += compiler.compile() + def visit_Call(self, node: ast.Call) -> str: + arg_src_parts = [] + for arg in node.args: + arg_src_parts.append(self.visit(arg)) + args_src = ', '.join(arg_src_parts) + return '{}({})'.format(self.visit(node.func), args_src) + + def visit_Expr(self, node: ast.Expr) -> str: + src = self.visit(node.value) + if node == self.root: + return src + else: + return '(' + src + ')' + + def compile(self) -> str: + c_src = self.visit(self.root) + LOG.debug('compiled line:\n\t\t%s\n\n\tinto:\n\t\t%s', ast.dump(self.root), c_src) + + if type(self.root) in [ast.Expr, ast.AnnAssign]: + c_src += ';' + c_src += '\n' + return c_src + +class FuncCompiler(BaseCompiler): + def compile(self) -> str: + c_src = 'void {}() {{\n'.format(self.scope.suggest_c_name(self.name)) + for node in self.root.body: + line_name = '{}:{}'.format(node.lineno, node.col_offset) + line_comp = LineCompiler(line_name, node, self.scope) + c_src += line_comp.compile() + c_src += '}\n\n' + return c_src + + +class ModuleCompiler(BaseCompiler): + def compile(self) -> str: + # Add a var for __name__ + dunder_name_c_name = self.scope.suggest_c_name('__name__') + self.scope.add_entry( + c=ScopeEntry(name=dunder_name_c_name, type='const char*', + callable=False), + py=ScopeEntry(name='__name__', type='str', callable=False), + ) + c_src = 'const char* {} = "{}";\n'.format(dunder_name_c_name, self.name.replace('.', '_DOT_')) + + # Sort the body nodes by type (top-level code or functions) + func_nodes = [] + other_nodes = [] + for node in self.root.body: + if type(node) == ast.FunctionDef: + func_nodes.append(node) + else: + other_nodes.append(node) + + # Find all module level variable declarations + for node in other_nodes: + for sub_node in ast.walk(node): + if type(sub_node) != ast.AnnAssign: + continue + c_src += self.declare_var(sub_node) + '\n' + c_src += '\n' + + # Compile the top-level module code + init_func_def = ast.FunctionDef( + name='__init__', + annotation=ast.Name(id='int'), + body=other_nodes) + init_func_compiler = FuncCompiler(init_func_def.name, init_func_def, self.scope) + c_src += init_func_compiler.compile() + + return c_src + + +class ProgramCompiler(object): + def __init__(self, name, py_src, platform): + self.platform = platform + self.name = name + self.py_src = py_src + + def _pre_source(self) -> str: + if self.platform == 'unix': + return '\n'.join([ + '#include ', + '#include ', + '#include ',]) + '\n\n' + elif self.platform == 'md': + return '#include \n\n' + + def compile(self) -> str: + # Use CPython's builtin source parser + root = ast.parse(self.py_src) + module_name = '__main__' + + # Create a new compiler for the __main__ module + main_comp = ModuleCompiler(module_name, root, main_scope) + + # Create and return C source for the application, which can be compiled + # to binary form using gcc. + c_src = self._pre_source() + c_src += main_comp.compile() + c_src += 'int main() {{{}(); return 0;}}'.format( + main_scope.suggest_c_name('__init__')) + return c_src + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument('sourcefile', type=argparse.FileType('r')) + p.add_argument('--platform', '-p', choices=['md', 'unix'], default='sega') + return p.parse_args() - # Add a main() fn - src += 'int main() {{return {}{}{}();}}\n'.format( - MOD_PREFIX, args.input_modules[0], MOD_INIT_SUFFIX) - print(src) +def main() -> int: + logging.basicConfig(level=logging.DEBUG) + args = parse_args() + py_src = args.sourcefile.read() + module_name = os.path.basename(args.sourcefile.name) + prog_compiler = ProgramCompiler(module_name, py_src, args.platform) + print(prog_compiler.compile()) return os.EX_OK diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 00000000..34c07113 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,29 @@ +#!/bin/bash +export PYTHON=python3.6 +export GCC="gcc -Wall -pedantic-errors" +export PYC="$(pwd)/pyc.py -p unix" +export PYC_MD="$(pwd)/pyc.py -p md" + +FAILED=0 +PASSED=0 +for test_script in $(find tests -name run.sh); do + pushd $(dirname $test_script) + bash -xe ./run.sh + RETVAL=$? + popd + if [ $RETVAL -ne 42 ]; then + echo "FAILED: $test_script" + echo "RETVAL: $RETVAL" + let FAILED++ + else + echo "PASSED: $test_script" + let PASSED++ + fi +done + +echo "" +echo "Summary:" +echo " FAILED: $FAILED" +echo " PASSED: $PASSED" + +exit $FAILED diff --git a/tests/if.comp.str/run.sh b/tests/if.comp.str/run.sh new file mode 100644 index 00000000..93a61409 --- /dev/null +++ b/tests/if.comp.str/run.sh @@ -0,0 +1,13 @@ +#!/bin/sh +${PYC} test.py > test.c +if [ $? -ne 0 ]; then + exit 1 +fi + +${GCC} test.c -otest.bin +if [ $? -ne 0 ]; then + exit 1 +fi + +./test.bin +exit $? diff --git a/tests/if.comp.str/test.py b/tests/if.comp.str/test.py new file mode 100644 index 00000000..6e525580 --- /dev/null +++ b/tests/if.comp.str/test.py @@ -0,0 +1,12 @@ +import sys +foo: str = 'foo' +bar: str = 'bar' + +if __name__ == '__main__': + if foo != 'foo': + sys.exit(1) + + if foo == bar: + sys.exit(2) + + sys.exit(42) diff --git a/tests/return.constant.int/run.sh b/tests/return.constant.int/run.sh new file mode 100644 index 00000000..93a61409 --- /dev/null +++ b/tests/return.constant.int/run.sh @@ -0,0 +1,13 @@ +#!/bin/sh +${PYC} test.py > test.c +if [ $? -ne 0 ]; then + exit 1 +fi + +${GCC} test.c -otest.bin +if [ $? -ne 0 ]; then + exit 1 +fi + +./test.bin +exit $? diff --git a/tests/return.constant.int/test.py b/tests/return.constant.int/test.py new file mode 100644 index 00000000..0e2f6d37 --- /dev/null +++ b/tests/return.constant.int/test.py @@ -0,0 +1,4 @@ +import sys + +if __name__ == '__main__': + sys.exit(42) diff --git a/tests/return.var.int/run.sh b/tests/return.var.int/run.sh new file mode 100644 index 00000000..93a61409 --- /dev/null +++ b/tests/return.var.int/run.sh @@ -0,0 +1,13 @@ +#!/bin/sh +${PYC} test.py > test.c +if [ $? -ne 0 ]; then + exit 1 +fi + +${GCC} test.c -otest.bin +if [ $? -ne 0 ]; then + exit 1 +fi + +./test.bin +exit $? diff --git a/tests/return.var.int/test.py b/tests/return.var.int/test.py new file mode 100644 index 00000000..af6586c3 --- /dev/null +++ b/tests/return.var.int/test.py @@ -0,0 +1,4 @@ +import sys +if __name__ == '__main__': + x: int = 42 + sys.exit(x) diff --git a/tests/sega.vdp.draw_text/run.sh b/tests/sega.vdp.draw_text/run.sh new file mode 100644 index 00000000..3622f691 --- /dev/null +++ b/tests/sega.vdp.draw_text/run.sh @@ -0,0 +1,15 @@ +#!/bin/sh +${PYC_MD} test.py > test.c +if [ $? -ne 0 ]; then + exit 1 +fi + +export GCC_MD="docker run -v $(pwd)/build:/src --rm -it beardedfoo/gendev:0.3.0" +mkdir -p build +cp *.c build/ +pushd build +${GCC_MD} +if [ $? -eq 0 ]; then + exit 42 +fi +exit $? diff --git a/tests/sega.vdp.draw_text/test.py b/tests/sega.vdp.draw_text/test.py new file mode 100644 index 00000000..960aaf36 --- /dev/null +++ b/tests/sega.vdp.draw_text/test.py @@ -0,0 +1,7 @@ +# import vdp +vdp.init() +vdp.draw_text("Hello World for Sega Megadrive", 10, 13) +vdp.draw_text("By @beardedfoo", 10, 15) +vdp.draw_text("...in Python!", 10, 17) +while True: + pass