diff --git a/examples/imported.html b/examples/imported.html new file mode 100644 index 0000000..a69c02e --- /dev/null +++ b/examples/imported.html @@ -0,0 +1,44 @@ + + + +Examples - Imported + + +

Imported

+ +List - Source - WebAssembly + +
+ + + + + + diff --git a/examples/imported.py b/examples/imported.py new file mode 100644 index 0000000..f5c2663 --- /dev/null +++ b/examples/imported.py @@ -0,0 +1,7 @@ +@imported +def log(no: i32) -> None: + pass + +@exported +def run(a: i32, b: i32) -> None: + return log(a * b) diff --git a/examples/index.html b/examples/index.html index 29e73c8..46e1fc1 100644 --- a/examples/index.html +++ b/examples/index.html @@ -11,6 +11,7 @@

Technical

diff --git a/py2wasm/compiler.py b/py2wasm/compiler.py index 95f66d7..3605ba5 100644 --- a/py2wasm/compiler.py +++ b/py2wasm/compiler.py @@ -9,6 +9,9 @@ from . import wasm Statements = Generator[wasm.Statement, None, None] def type_(inp: ourlang.OurType) -> wasm.OurType: + if isinstance(inp, ourlang.OurTypeNone): + return wasm.OurTypeNone() + if isinstance(inp, ourlang.OurTypeUInt8): # WebAssembly has only support for 32 and 64 bits # So we need to store more memory per byte @@ -205,12 +208,31 @@ def statement(inp: ourlang.Statement) -> Statements: yield from statement_if(inp) return + if isinstance(inp, ourlang.StatementPass): + return + raise NotImplementedError(statement, inp) def function_argument(inp: Tuple[str, ourlang.OurType]) -> wasm.Param: return (inp[0], type_(inp[1]), ) +def import_(inp: ourlang.Function) -> wasm.Import: + assert inp.imported + + return wasm.Import( + 'imports', + inp.name, + inp.name, + [ + function_argument(x) + for x in inp.posonlyargs + ], + type_(inp.returns) + ) + def function(inp: ourlang.Function) -> wasm.Function: + assert not inp.imported + if isinstance(inp, ourlang.TupleConstructor): statements = [ *_generate_tuple_constructor(inp) @@ -248,12 +270,19 @@ def function(inp: ourlang.Function) -> wasm.Function: def module(inp: ourlang.Module) -> wasm.Module: result = wasm.Module() + result.imports = [ + import_(x) + for x in inp.functions.values() + if x.imported + ] + result.functions = [ _generate____new_reference___(inp), _generate____access_bytes_index___(inp), ] + [ function(x) for x in inp.functions.values() + if not x.imported ] return result diff --git a/py2wasm/ourlang.py b/py2wasm/ourlang.py index 1ff4ae2..303d94d 100644 --- a/py2wasm/ourlang.py +++ b/py2wasm/ourlang.py @@ -458,11 +458,12 @@ class Function: """ A function processes input and produces output """ - __slots__ = ('name', 'lineno', 'exported', 'buildin', 'statements', 'returns', 'posonlyargs', ) + __slots__ = ('name', 'lineno', 'exported', 'imported', 'buildin', 'statements', 'returns', 'posonlyargs', ) name: str lineno: int exported: bool + imported: bool buildin: bool statements: List[Statement] returns: OurType @@ -472,6 +473,7 @@ class Function: self.name = name self.lineno = lineno self.exported = False + self.imported = False self.buildin = False self.statements = [] self.returns = OurTypeNone() @@ -483,9 +485,14 @@ class Function: This'll look like Python code. """ + statements = self.statements + result = '' if self.exported: result += '@exported\n' + if self.imported: + result += '@imported\n' + statements = [StatementPass()] args = ', '.join( f'{x}: {y.render()}' @@ -493,7 +500,7 @@ class Function: ) result += f'def {self.name}({args}) -> {self.returns.render()}:\n' - for stmt in self.statements: + for stmt in statements: for line in stmt.render(): result += f' {line}\n' if line else '\n' return result @@ -608,6 +615,7 @@ class Module: def __init__(self) -> None: self.types = { + 'None': OurTypeNone(), 'u8': OurTypeUInt8(), 'i32': OurTypeInt32(), 'i64': OurTypeInt64(), @@ -730,8 +738,12 @@ class OurVisitor: _raise_static_error(decorator, 'Function decorators must be string') if not isinstance(decorator.ctx, ast.Load): _raise_static_error(decorator, 'Must be load context') - _not_implemented(decorator.id != 'exports', 'Custom decorators') - function.exported = True + _not_implemented(decorator.id in ('exported', 'imported'), 'Custom decorators') + + if decorator.id == 'exported': + function.exported = True + else: + function.imported = True if node.returns: function.returns = self.visit_type(module, node.returns) @@ -816,6 +828,9 @@ class OurVisitor: return result + if isinstance(node, ast.Pass): + return StatementPass() + raise NotImplementedError(f'{node} as stmt in FunctionDef') def visit_Module_FunctionDef_expr(self, module: Module, function: Function, our_locals: OurLocals, exp_type: OurType, node: ast.expr) -> Expression: @@ -1108,6 +1123,12 @@ class OurVisitor: raise NotImplementedError(f'{node} as const for type {exp_type.render()}') def visit_type(self, module: Module, node: ast.expr) -> OurType: + if isinstance(node, ast.Constant): + if node.value is None: + return module.types['None'] + + _raise_static_error(node, f'Unrecognized type {node.value}') + if isinstance(node, ast.Name): if not isinstance(node.ctx, ast.Load): _raise_static_error(node, 'Must be load context') diff --git a/py2wasm/wasm.py b/py2wasm/wasm.py index 049df62..a4c274a 100644 --- a/py2wasm/wasm.py +++ b/py2wasm/wasm.py @@ -156,22 +156,28 @@ class Import: name: str, intname: str, params: Iterable[Param], + result: OurType, ) -> None: self.module = module self.name = name self.intname = intname self.params = [*params] - self.result: OurType = OurTypeNone() + self.result = result def generate(self) -> str: """ Generates the text version """ - return '(import "{}" "{}" (func ${}{}))'.format( + return '(import "{}" "{}" (func ${}{}{}))'.format( self.module, self.name, self.intname, - ''.join(' (param {})'.format(x[1]) for x in self.params) + ''.join( + f' (param {typ.to_wasm()})' + for _, typ in self.params + ), + '' if isinstance(self.result, OurTypeNone) + else f' (result {self.result.to_wasm()})' ) class Statement: @@ -263,7 +269,7 @@ class Module: Generates the text version """ return '(module\n {}\n {}\n {})\n'.format( - self.memory.generate(), '\n '.join(x.generate() for x in self.imports), + self.memory.generate(), '\n '.join(x.generate() for x in self.functions), ) diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index a617c26..af90b92 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -63,7 +63,7 @@ class Suite: self.code_py = code_py self.test_name = test_name - def run_code(self, *args): + def run_code(self, *args, runtime='pywasm3', imports=None): """ Compiles the given python code into wasm and then runs it @@ -89,7 +89,16 @@ class Suite: code_wasm = wat2wasm(code_wat) # Run assembly code - return _run_pywasm3(code_wasm, args) + if 'pywasm' == runtime: + return _run_pywasm(code_wasm, args) + if 'pywasm3' == runtime: + return _run_pywasm3(code_wasm, args) + if 'wasmtime' == runtime: + return _run_wasmtime(code_wasm, args) + if 'wasmer' == runtime: + return _run_wasmer(code_wasm, args, imports) + + raise Exception(f'Invalid runtime: {runtime}') def _run_pywasm(code_wasm, args): # https://pypi.org/project/pywasm/ @@ -168,17 +177,23 @@ def _run_wasmtime(code_wasm, args): return result -def _run_wasmer(code_wasm, args): +def _run_wasmer(code_wasm, args, imports): # https://pypi.org/project/wasmer/ result = SuiteResult() store = wasmer.Store(wasmer.engine.JIT(wasmer_compiler_cranelift.Compiler)) + import_object = wasmer.ImportObject() + import_object.register('imports', { + k: wasmer.Function(store, v) + for k, v in (imports or {}).items() + }) + # Let's compile the module to be able to execute it! module = wasmer.Module(store, code_wasm) # Now the module is compiled, we can instantiate it. - instance = wasmer.Instance(module) + instance = wasmer.Instance(module, import_object) sys.stderr.write(f'{DASHES} Memory (pre run) {DASHES}\n') sys.stderr.write('\n') diff --git a/tests/integration/test_simple.py b/tests/integration/test_simple.py index f5f28f9..b8f5bd3 100644 --- a/tests/integration/test_simple.py +++ b/tests/integration/test_simple.py @@ -416,3 +416,27 @@ def testEntry() -> i32x4: result = Suite(code_py, 'test_rgb2hsl').run_code() assert (1, 2, 3, 0) == result.returned_value + +@pytest.mark.integration_test +def test_imported(): + code_py = """ +@imported +def helper() -> i32: + pass + +@exported +def testEntry() -> i32: + return helper() +""" + + def helper() -> int: + return 4238 + + result = Suite(code_py, 'test_imported').run_code( + runtime='wasmer', + imports={ + 'helper': helper, + } + ) + + assert 4238 == result.returned_value