From 8c25227f401df790677c31d44d0aba54ffa91f07 Mon Sep 17 00:00:00 2001 From: "Johan B.W. de Vries" Date: Sun, 19 Jun 2022 15:10:13 +0200 Subject: [PATCH] Started on compilation, typing changes --- py2wasm/compiler.py | 115 +++++++++++++++++++++++++++++++ py2wasm/ourlang.py | 32 +++++---- py2wasm/python.py | 6 ++ py2wasm/wasm.py | 5 +- tests/integration/test_simple.py | 85 ++++++++++------------- 5 files changed, 180 insertions(+), 63 deletions(-) create mode 100644 py2wasm/compiler.py diff --git a/py2wasm/compiler.py b/py2wasm/compiler.py new file mode 100644 index 0000000..425adb6 --- /dev/null +++ b/py2wasm/compiler.py @@ -0,0 +1,115 @@ +""" +This module contains the code to convert parsed Ourlang into WebAssembly code +""" +from typing import Generator, Tuple + +from . import ourlang +from . import wasm + +Statements = Generator[wasm.Statement, None, None] + +def type_(inp: ourlang.OurType) -> wasm.OurType: + if isinstance(inp, ourlang.OurTypeInt32): + return wasm.OurTypeInt32() + + if isinstance(inp, ourlang.OurTypeInt64): + return wasm.OurTypeInt64() + + if isinstance(inp, ourlang.OurTypeFloat32): + return wasm.OurTypeFloat32() + + if isinstance(inp, ourlang.OurTypeFloat64): + return wasm.OurTypeFloat64() + + raise NotImplementedError(type_, inp) + +OPERATOR_MAP = { + '+': 'add', + '-': 'sub', +} + +def expression(inp: ourlang.Expression) -> Statements: + if isinstance(inp, ourlang.ConstantInt32): + yield wasm.Statement('i32.const', str(inp.value)) + return + + if isinstance(inp, ourlang.ConstantInt64): + yield wasm.Statement('i64.const', str(inp.value)) + return + + if isinstance(inp, ourlang.ConstantFloat32): + yield wasm.Statement('f32.const', str(inp.value)) + return + + if isinstance(inp, ourlang.ConstantFloat64): + yield wasm.Statement('f64.const', str(inp.value)) + return + + if isinstance(inp, ourlang.VariableReference): + yield wasm.Statement('local.get', '${}'.format(inp.name)) + return + + if isinstance(inp, ourlang.BinaryOp): + yield from expression(inp.left) + yield from expression(inp.right) + + if isinstance(inp.type, ourlang.OurTypeInt32): + if operator := OPERATOR_MAP.get(inp.operator, None): + yield wasm.Statement(f'i32.{operator}') + return + if isinstance(inp.type, ourlang.OurTypeInt64): + if operator := OPERATOR_MAP.get(inp.operator, None): + yield wasm.Statement(f'i64.{operator}') + return + if isinstance(inp.type, ourlang.OurTypeFloat32): + if operator := OPERATOR_MAP.get(inp.operator, None): + yield wasm.Statement(f'f32.{operator}') + return + if isinstance(inp.type, ourlang.OurTypeFloat64): + if operator := OPERATOR_MAP.get(inp.operator, None): + yield wasm.Statement(f'f64.{operator}') + return + + raise NotImplementedError(expression, inp.type, inp.operator) + + raise NotImplementedError(expression, inp) + +def statement_return(inp: ourlang.StatementReturn) -> Statements: + yield from expression(inp.value) + yield wasm.Statement('return') + +def statement(inp: ourlang.Statement) -> Statements: + if isinstance(inp, ourlang.StatementReturn): + yield from statement_return(inp) + return + + raise NotImplementedError(statement, inp) + +def function_argument(inp: Tuple[str, ourlang.OurType]) -> wasm.Param: + return (inp[0], type_(inp[1]), ) + +def function(inp: ourlang.Function) -> wasm.Function: + return wasm.Function( + inp.name, + inp.exported, + [ + function_argument(x) + for x in inp.posonlyargs + ], + [], # TODO + type_(inp.returns), + [ + x + for y in inp.statements + for x in statement(y) + ] + ) + +def module(inp: ourlang.Module) -> wasm.Module: + result = wasm.Module() + + result.functions = [*map( + function, inp.functions.values(), + )] + + return result diff --git a/py2wasm/ourlang.py b/py2wasm/ourlang.py index 6550f1c..b471514 100644 --- a/py2wasm/ourlang.py +++ b/py2wasm/ourlang.py @@ -92,7 +92,12 @@ class Expression: """ An expression within a statement """ - __slots__ = () + __slots__ = ('type', ) + + type: OurType + + def __init__(self, type_: OurType) -> None: + self.type = type_ def render(self) -> str: """ @@ -106,12 +111,7 @@ class Constant(Expression): """ An constant value expression within a statement """ - __slots__ = ('type', ) - - type: OurType - - def __init__(self, type_: OurType) -> None: - self.type = type_ + __slots__ = () class ConstantInt32(Constant): """ @@ -177,13 +177,12 @@ class VariableReference(Expression): """ An variable reference expression within a statement """ - __slots__ = ('type', 'name', ) + __slots__ = ('name', ) - type: OurType name: str def __init__(self, type_: OurType, name: str) -> None: - self.type = type_ + super().__init__(type_) self.name = name def render(self) -> str: @@ -199,7 +198,9 @@ class BinaryOp(Expression): left: Expression right: Expression - def __init__(self, operator: str, left: Expression, right: Expression) -> None: + def __init__(self, type_: OurType, operator: str, left: Expression, right: Expression) -> None: + super().__init__(type_) + self.operator = operator self.left = left self.right = right @@ -509,11 +510,12 @@ class Module: # sqrt is guaranteed by wasm, so we should provide it # ATM it's a 32 bit variant, but that might change. + # TODO: Could be a UnaryOp? sqrt = Function('sqrt', -2) sqrt.buildin = True sqrt.returns = self.types['f32'] sqrt.posonlyargs = [('@', self.types['f32'], )] - self.functions[sqrt.name] = sqrt + # self.functions[sqrt.name] = sqrt def render(self) -> str: """ @@ -730,6 +732,7 @@ class OurVisitor: # e.g. you can do `"hello" * 3` with the code below (yet) return BinaryOp( + exp_type, operator, self.visit_Module_FunctionDef_expr(module, function, our_locals, exp_type, node.left), self.visit_Module_FunctionDef_expr(module, function, our_locals, exp_type, node.right), @@ -765,6 +768,7 @@ class OurVisitor: # e.g. you can do `"hello" * 3` with the code below (yet) return BinaryOp( + exp_type, operator, self.visit_Module_FunctionDef_expr(module, function, our_locals, exp_type, node.left), self.visit_Module_FunctionDef_expr(module, function, our_locals, exp_type, node.comparators[0]), @@ -932,7 +936,7 @@ class OurVisitor: return ConstantInt64(exp_type, node.value) if isinstance(exp_type, OurTypeFloat32): - if not isinstance(node.value, float): + if not isinstance(node.value, (float, int, )): _raise_static_error(node, 'Expected float value') # FIXME: Range check @@ -940,7 +944,7 @@ class OurVisitor: return ConstantFloat32(exp_type, node.value) if isinstance(exp_type, OurTypeFloat64): - if not isinstance(node.value, float): + if not isinstance(node.value, (float, int, )): _raise_static_error(node, 'Expected float value') # FIXME: Range check diff --git a/py2wasm/python.py b/py2wasm/python.py index 529749f..91cef95 100644 --- a/py2wasm/python.py +++ b/py2wasm/python.py @@ -585,6 +585,9 @@ class Visitor: return if isinstance(exp_type, wasm.OurTypeFloat32): + if isinstance(node.value, int): + node.value = float(node.value) + assert isinstance(node.value, float), \ f'Expression expected a float, but received {node.value}' # TODO: Size check? @@ -593,6 +596,9 @@ class Visitor: return if isinstance(exp_type, wasm.OurTypeFloat64): + if isinstance(node.value, int): + node.value = float(node.value) + assert isinstance(node.value, float) # TODO: Size check? diff --git a/py2wasm/wasm.py b/py2wasm/wasm.py index b9b7d85..09609e6 100644 --- a/py2wasm/wasm.py +++ b/py2wasm/wasm.py @@ -1,5 +1,6 @@ """ -Python classes for storing the representation of Web Assembly code +Python classes for storing the representation of Web Assembly code, +and being able to conver it to Web Assembly Text Format """ from typing import Any, Iterable, List, Optional, Tuple, Union @@ -220,7 +221,7 @@ class Function: for nam, typ in self.params: header += f' (param ${nam} {typ.to_wasm()})' - if self.result: + if not isinstance(self.result, OurTypeNone): header += f' (result {self.result.to_wasm()})' for nam, typ in self.locals: diff --git a/tests/integration/test_simple.py b/tests/integration/test_simple.py index d7ee5f2..c1af722 100644 --- a/tests/integration/test_simple.py +++ b/tests/integration/test_simple.py @@ -2,72 +2,71 @@ import pytest from .helpers import Suite +TYPE_MAP = { + 'i32': int, + 'i64': int, + 'f32': float, + 'f64': float, +} + @pytest.mark.integration_test -def test_return_i32(): - code_py = """ +@pytest.mark.parametrize('type_', ['i32', 'i64', 'f32', 'f64']) +def test_return(type_): + code_py = f""" @exported -def testEntry() -> i32: +def testEntry() -> {type_}: return 13 """ result = Suite(code_py, 'test_return').run_code() assert 13 == result.returned_value - assert [] == result.log_int32_list + assert TYPE_MAP[type_] == type(result.returned_value) @pytest.mark.integration_test -def test_return_i64(): - code_py = """ +@pytest.mark.parametrize('type_', ['i32', 'i64', 'f32', 'f64']) +def test_addition(type_): + code_py = f""" @exported -def testEntry() -> i64: - return 13 +def testEntry() -> {type_}: + return 10 + 3 """ - result = Suite(code_py, 'test_return').run_code() + result = Suite(code_py, 'test_addition').run_code() assert 13 == result.returned_value - assert [] == result.log_int32_list + assert TYPE_MAP[type_] == type(result.returned_value) @pytest.mark.integration_test -def test_return_f32(): - code_py = """ +@pytest.mark.parametrize('type_', ['i32', 'i64', 'f32', 'f64']) +def test_subtraction(type_): + code_py = f""" @exported -def testEntry() -> f32: - return 13.5 +def testEntry() -> {type_}: + return 10 - 3 """ - result = Suite(code_py, 'test_return').run_code() + result = Suite(code_py, 'test_addition').run_code() - assert 13.5 == result.returned_value - assert [] == result.log_int32_list + assert 7 == result.returned_value + assert TYPE_MAP[type_] == type(result.returned_value) @pytest.mark.integration_test -def test_return_f64(): - code_py = """ +@pytest.mark.parametrize('type_', ['i32', 'i64', 'f32', 'f64']) +def test_arg(type_): + code_py = f""" @exported -def testEntry() -> f64: - return 13.5 -""" - - result = Suite(code_py, 'test_return').run_code() - - assert 13.5 == result.returned_value - assert [] == result.log_int32_list - -@pytest.mark.integration_test -def test_arg(): - code_py = """ -@exported -def testEntry(a: i32) -> i32: +def testEntry(a: {type_}) -> {type_}: return a """ result = Suite(code_py, 'test_return').run_code(125) assert 125 == result.returned_value - assert [] == result.log_int32_list + assert TYPE_MAP[type_] == type(result.returned_value) @pytest.mark.integration_test +@pytest.mark.skip('Do we want it to work like this?') def test_i32_to_i64(): code_py = """ @exported @@ -81,6 +80,7 @@ def testEntry(a: i32) -> i64: assert [] == result.log_int32_list @pytest.mark.integration_test +@pytest.mark.skip('Do we want it to work like this?') def test_i32_plus_i64(): code_py = """ @exported @@ -94,6 +94,7 @@ def testEntry(a: i32, b: i64) -> i64: assert [] == result.log_int32_list @pytest.mark.integration_test +@pytest.mark.skip('Do we want it to work like this?') def test_f32_to_f64(): code_py = """ @exported @@ -107,6 +108,7 @@ def testEntry(a: f32) -> f64: assert [] == result.log_int32_list @pytest.mark.integration_test +@pytest.mark.skip('Do we want it to work like this?') def test_f32_plus_f64(): code_py = """ @exported @@ -120,6 +122,7 @@ def testEntry(a: f32, b: f64) -> f64: assert [] == result.log_int32_list @pytest.mark.integration_test +@pytest.mark.skip('TODO') def test_uadd(): code_py = """ @exported @@ -133,6 +136,7 @@ def testEntry() -> i32: assert [] == result.log_int32_list @pytest.mark.integration_test +@pytest.mark.skip('TODO') def test_usub(): code_py = """ @exported @@ -145,19 +149,6 @@ def testEntry() -> i32: assert -19 == result.returned_value assert [] == result.log_int32_list -@pytest.mark.integration_test -def test_addition(): - code_py = """ -@exported -def testEntry() -> i32: - return 10 + 3 -""" - - result = Suite(code_py, 'test_addition').run_code() - - assert 13 == result.returned_value - assert [] == result.log_int32_list - @pytest.mark.integration_test def test_if_simple(): code_py = """