diff --git a/Makefile b/Makefile index 36d0f06..d952e0f 100644 --- a/Makefile +++ b/Makefile @@ -29,5 +29,6 @@ typecheck: venv/.done venv/.done: requirements.txt python3.8 -m venv venv + venv/bin/python3 -m pip install wheel venv/bin/python3 -m pip install -r $^ touch $@ diff --git a/py2wasm/python.py b/py2wasm/python.py index 7639ee1..4867061 100644 --- a/py2wasm/python.py +++ b/py2wasm/python.py @@ -30,6 +30,8 @@ class Visitor: 'i64': wasm.OurTypeInt64(), 'f32': wasm.OurTypeFloat32(), 'f64': wasm.OurTypeFloat64(), + + 'i32x4': wasm.OurTypeVectorInt32x4(), } def _get_type(self, name: Union[None, str, ast.expr]) -> wasm.OurType: @@ -186,7 +188,7 @@ class Visitor: else: raise NotImplementedError - result = None if node.returns is None else self._get_type(node.returns) + result = self._get_type(node.returns) assert not node.args.vararg assert not node.args.kwonlyargs @@ -377,7 +379,7 @@ class Visitor: return self.visit_Name(wlocals, exp_type, node) if isinstance(node, ast.Tuple): - return self.visit_Tuple(wlocals, exp_type, node) + return self.visit_Tuple(module, wlocals, exp_type, node) raise NotImplementedError(node) @@ -549,7 +551,8 @@ class Visitor: called_params = func.params called_result = func.result - assert exp_type == called_result, 'Function does not match expected type' + assert exp_type == called_result, \ + f'Function does not match expected type; {called_name} returns {called_result.to_python()} but {exp_type.to_python()} is expected' assert len(called_params) == len(node.args), \ '{}:{} Function {} requires {} arguments, but {} are supplied'.format( @@ -584,7 +587,8 @@ class Visitor: return if isinstance(exp_type, wasm.OurTypeFloat32): - assert isinstance(node.value, float) + assert isinstance(node.value, float), \ + f'Expression expected a float, but received {node.value}' # TODO: Size check? yield wasm.Statement('f32.const', node.value.hex()) @@ -672,6 +676,7 @@ class Visitor: def visit_Tuple( self, + module: wasm.Module, wlocals: WLocals, exp_type: wasm.OurType, node: ast.Tuple, @@ -679,7 +684,21 @@ class Visitor: """ Visits an Tuple node as (part of) an expression """ - assert isinstance(exp_type, wasm.OurTypeTuple), 'Expression is not expecting a tuple' + if isinstance(exp_type, wasm.OurTypeVector): + assert isinstance(exp_type, wasm.OurTypeVectorInt32x4), 'Not implemented yet' + assert len(node.elts) == 4, 'Not implemented yet' + + values: List[str] = [] + for arg in node.elts: + assert isinstance(arg, ast.Constant) + assert isinstance(arg.value, int) + values.append(str(arg.value)) + + yield wasm.Statement('v128.const', 'i32x4', *values) + return + + assert isinstance(exp_type, wasm.OurTypeTuple), \ + f'Expression is expecting {exp_type}, not a tuple' # TODO: free call @@ -692,11 +711,10 @@ class Visitor: yield wasm.Statement('local.set', '$___new_reference___addr', comment='Allocate for tuple') for member, arg in zip(tpl.members, node.elts): - if not isinstance(arg, ast.Constant): - raise NotImplementedError('TODO: Non-const tuple members') - yield wasm.Statement('local.get', '$___new_reference___addr') - yield wasm.Statement(f'{member.type.to_wasm()}.const', str(arg.value)) + + yield from self.visit_expr(module, wlocals, member.type, arg) + yield wasm.Statement(f'{member.type.to_wasm()}.store', 'offset=' + str(member.offset), comment='Write tuple value to memory') diff --git a/py2wasm/wasm.py b/py2wasm/wasm.py index 2aa4ac8..b9b7d85 100644 --- a/py2wasm/wasm.py +++ b/py2wasm/wasm.py @@ -9,11 +9,14 @@ from typing import Any, Iterable, List, Optional, Tuple, Union ### class OurType: + def to_python(self) -> str: + raise NotImplementedError(self, 'to_python') + def to_wasm(self) -> str: - raise NotImplementedError + raise NotImplementedError(self, 'to_wasm') def alloc_size(self) -> int: - raise NotImplementedError + raise NotImplementedError(self, 'alloc_size') class OurTypeNone(OurType): pass @@ -22,6 +25,9 @@ class OurTypeBool(OurType): pass class OurTypeInt32(OurType): + def to_python(self) -> str: + return 'i32' + def to_wasm(self) -> str: return 'i32' @@ -49,6 +55,21 @@ class OurTypeFloat64(OurType): def alloc_size(self) -> int: return 8 +class OurTypeVector(OurType): + """ + A vector is a 128-bit value + """ + def to_wasm(self) -> str: + return 'v128' + + def alloc_size(self) -> int: + return 16 + +class OurTypeVectorInt32x4(OurTypeVector): + """ + 4 Int32 values in a single vector + """ + class Constant: """ TODO @@ -95,12 +116,28 @@ class OurTypeTuple(OurType): def __init__(self, members: List[TupleMember]) -> None: self.members = members + def to_python(self) -> str: + return 'Tuple[' + ( + ', '.join(x.type.to_python() for x in self.members) + ) + ']' + def to_wasm(self) -> str: return 'i32' # WASM uses 32 bit pointers def alloc_size(self) -> int: return sum(x.type.alloc_size() for x in self.members) + def __eq__(self, other: Any) -> bool: + if not isinstance(other, OurTypeTuple): + raise NotImplementedError + + return ( + len(self.members) == len(other.members) + and all( + self.members[x].type == other.members[x].type + for x in range(len(self.members)) + ) + ) Param = Tuple[str, OurType] @@ -123,7 +160,7 @@ class Import: self.name = name self.intname = intname self.params = [*params] - self.result: str = 'None' + self.result: OurType = OurTypeNone() def generate(self) -> str: """ @@ -164,7 +201,7 @@ class Function: exported: bool, params: Iterable[Param], locals_: Iterable[Param], - result: Optional[OurType], + result: OurType, statements: Iterable[Statement], ) -> None: self.name = name diff --git a/requirements.txt b/requirements.txt index c20edb1..a9abf93 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,7 @@ pylint==2.7.4 pytest==6.2.2 pytest-integration==0.2.2 pywasm==1.0.7 +pywasm3==0.5.0 +wasmer==1.0.0 +wasmer_compiler_cranelift==1.0.0 +wasmtime==0.36.0 diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index e434d05..98f5dad 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -5,11 +5,19 @@ import sys from tempfile import NamedTemporaryFile -from pywasm import binary -from pywasm import Runtime +import pywasm + +import wasm3 + +import wasmer +import wasmer_compiler_cranelift + +import wasmtime from py2wasm.utils import process +DASHES = '-' * 16 + def wat2wasm(code_wat): path = os.environ.get('WAT2WASM', 'wat2wasm') @@ -63,9 +71,7 @@ class Suite: """ code_wat = process(self.code_py, self.test_name) - dashes = '-' * 16 - - sys.stderr.write(f'{dashes} Assembly {dashes}\n') + sys.stderr.write(f'{DASHES} Assembly {DASHES}\n') line_list = code_wat.split('\n') line_no_width = len(str(len(line_list))) @@ -76,20 +82,90 @@ class Suite: )) code_wasm = wat2wasm(code_wat) - module = binary.Module.from_reader(io.BytesIO(code_wasm)) - result = SuiteResult() - runtime = Runtime(module, result.make_imports(), {}) + return _run_pywasm3(code_wasm, args) - sys.stderr.write(f'{dashes} Memory (pre run) {dashes}\n') - _dump_memory(runtime.store.mems[0].data) +def _run_pywasm(code_wasm, args): + # https://pypi.org/project/pywasm/ + result = SuiteResult() - result.returned_value = runtime.exec('testEntry', args) + module = pywasm.binary.Module.from_reader(io.BytesIO(code_wasm)) - sys.stderr.write(f'{dashes} Memory (post run) {dashes}\n') - _dump_memory(runtime.store.mems[0].data) + runtime = pywasm.Runtime(module, result.make_imports(), {}) - return result + sys.stderr.write(f'{DASHES} Memory (pre run) {DASHES}\n') + _dump_memory(runtime.store.mems[0].data) + + result.returned_value = runtime.exec('testEntry', args) + + sys.stderr.write(f'{DASHES} Memory (post run) {DASHES}\n') + _dump_memory(runtime.store.mems[0].data) + + return result + +def _run_pywasm3(code_wasm, args): + # https://pypi.org/project/pywasm3/ + result = SuiteResult() + + env = wasm3.Environment() + + mod = env.parse_module(code_wasm) + + rtime = env.new_runtime(1024 * 1024) + rtime.load(mod) + + sys.stderr.write(f'{DASHES} Memory (pre run) {DASHES}\n') + _dump_memory(rtime.get_memory(0).tobytes()) + + result.returned_value = rtime.find_function('testEntry')(*args) + + sys.stderr.write(f'{DASHES} Memory (post run) {DASHES}\n') + _dump_memory(rtime.get_memory(0).tobytes()) + + return result + +def _run_wasmtime(code_wasm, args): + # https://pypi.org/project/wasmtime/ + result = SuiteResult() + + store = wasmtime.Store() + + module = wasmtime.Module(store.engine, code_wasm) + + instance = wasmtime.Instance(store, module, []) + + sys.stderr.write(f'{DASHES} Memory (pre run) {DASHES}\n') + sys.stderr.write('\n') + + result.returned_value = instance.exports(store)['testEntry'](store, *args) + + sys.stderr.write(f'{DASHES} Memory (post run) {DASHES}\n') + sys.stderr.write('\n') + + return result + +def _run_wasmer(code_wasm, args): + # https://pypi.org/project/wasmer/ + result = SuiteResult() + + store = wasmer.Store(wasmer.engine.JIT(wasmer_compiler_cranelift.Compiler)) + + # 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) + + sys.stderr.write(f'{DASHES} Memory (pre run) {DASHES}\n') + sys.stderr.write('\n') + + # Call the exported `sum` function. + result.returned_value = instance.exports.testEntry(*args) + + sys.stderr.write(f'{DASHES} Memory (post run) {DASHES}\n') + sys.stderr.write('\n') + + return result def _dump_memory(mem): line_width = 16 diff --git a/tests/integration/test_simple.py b/tests/integration/test_simple.py index fd89412..0703e08 100644 --- a/tests/integration/test_simple.py +++ b/tests/integration/test_simple.py @@ -341,7 +341,7 @@ def test_tuple_float(): @exported def testEntry() -> f32: - return helper((1, 2, 3, )) + return helper((1.0, 2.0, 3.0, )) def helper(v: Tuple[f32, f32, f32]) -> f32: # sqrt is guaranteed by wasm, so we can use it @@ -353,3 +353,16 @@ def helper(v: Tuple[f32, f32, f32]) -> f32: assert 3.74 < result.returned_value < 3.75 assert [] == result.log_int32_list + +@pytest.mark.integration_test +@pytest.mark.skip('SIMD support is but a dream') +def test_tuple_i32x4(): + code_py = """ +@exported +def testEntry() -> i32x4: + return (51, 153, 204, 0, ) +""" + + result = Suite(code_py, 'test_rgb2hsl').run_code() + + assert (1, 2, 3, 0) == result.returned_value