MVP #1
2
Makefile
2
Makefile
@ -31,7 +31,7 @@ lint: venv/.done
|
||||
venv/bin/pylint phasm
|
||||
|
||||
typecheck: venv/.done
|
||||
venv/bin/mypy --strict phasm
|
||||
venv/bin/mypy --strict phasm tests/integration/runners.py
|
||||
|
||||
venv/.done: requirements.txt
|
||||
python3.8 -m venv venv
|
||||
|
||||
@ -78,11 +78,19 @@ OPERATOR_MAP = {
|
||||
'==': 'eq',
|
||||
}
|
||||
|
||||
U8_OPERATOR_MAP = {
|
||||
# Under the hood, this is an i32
|
||||
# Implementing XOR is fine since the 3 remaining
|
||||
# bytes stay zero after this operation
|
||||
'^': 'xor',
|
||||
}
|
||||
|
||||
U32_OPERATOR_MAP = {
|
||||
'<': 'lt_u',
|
||||
'>': 'gt_u',
|
||||
'<=': 'le_u',
|
||||
'>=': 'ge_u',
|
||||
'^': 'xor',
|
||||
}
|
||||
|
||||
U64_OPERATOR_MAP = {
|
||||
@ -90,6 +98,7 @@ U64_OPERATOR_MAP = {
|
||||
'>': 'gt_u',
|
||||
'<=': 'le_u',
|
||||
'>=': 'ge_u',
|
||||
'^': 'xor',
|
||||
}
|
||||
|
||||
I32_OPERATOR_MAP = {
|
||||
@ -146,6 +155,10 @@ def expression(inp: ourlang.Expression) -> Statements:
|
||||
yield from expression(inp.left)
|
||||
yield from expression(inp.right)
|
||||
|
||||
if isinstance(inp.type, typing.TypeUInt8):
|
||||
if operator := U8_OPERATOR_MAP.get(inp.operator, None):
|
||||
yield wasm.Statement(f'i32.{operator}')
|
||||
return
|
||||
if isinstance(inp.type, typing.TypeUInt32):
|
||||
if operator := OPERATOR_MAP.get(inp.operator, None):
|
||||
yield wasm.Statement(f'i32.{operator}')
|
||||
|
||||
@ -242,6 +242,8 @@ class OurVisitor:
|
||||
operator = '-'
|
||||
elif isinstance(node.op, ast.Mult):
|
||||
operator = '*'
|
||||
elif isinstance(node.op, ast.BitXor):
|
||||
operator = '^'
|
||||
else:
|
||||
raise NotImplementedError(f'Operator {node.op}')
|
||||
|
||||
|
||||
23
stubs/wasm3.pyi
Normal file
23
stubs/wasm3.pyi
Normal file
@ -0,0 +1,23 @@
|
||||
from typing import Any, Callable
|
||||
|
||||
class Module:
|
||||
...
|
||||
|
||||
class Runtime:
|
||||
...
|
||||
|
||||
def load(self, wasm_bin: Module) -> None:
|
||||
...
|
||||
|
||||
def get_memory(self, memid: int) -> memoryview:
|
||||
...
|
||||
|
||||
def find_function(self, name: str) -> Callable[[Any], Any]:
|
||||
...
|
||||
|
||||
class Environment:
|
||||
def new_runtime(self, mem_size: int) -> Runtime:
|
||||
...
|
||||
|
||||
def parse_module(self, wasm_bin: bytes) -> Module:
|
||||
...
|
||||
168
tests/integration/runners.py
Normal file
168
tests/integration/runners.py
Normal file
@ -0,0 +1,168 @@
|
||||
"""
|
||||
Runners to help run WebAssembly code on various interpreters
|
||||
"""
|
||||
from typing import Any, TextIO
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from phasm.compiler import phasm_compile
|
||||
from phasm.parser import phasm_parse
|
||||
from phasm import ourlang
|
||||
from phasm import wasm
|
||||
|
||||
import wasm3
|
||||
|
||||
def wat2wasm(code_wat: str) -> bytes:
|
||||
"""
|
||||
Converts the given WebAssembly Assembly code into WebAssembly Binary
|
||||
"""
|
||||
path = os.environ.get('WAT2WASM', 'wat2wasm')
|
||||
|
||||
with tempfile.NamedTemporaryFile('w+t') as input_fp:
|
||||
input_fp.write(code_wat)
|
||||
input_fp.flush()
|
||||
|
||||
with tempfile.NamedTemporaryFile('w+b') as output_fp:
|
||||
subprocess.run(
|
||||
[
|
||||
path,
|
||||
input_fp.name,
|
||||
'-o',
|
||||
output_fp.name,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
output_fp.seek(0)
|
||||
|
||||
return output_fp.read()
|
||||
|
||||
class RunnerBase:
|
||||
"""
|
||||
Base class
|
||||
"""
|
||||
phasm_code: str
|
||||
phasm_ast: ourlang.Module
|
||||
wasm_ast: wasm.Module
|
||||
wasm_asm: str
|
||||
wasm_bin: bytes
|
||||
|
||||
def __init__(self, phasm_code: str) -> None:
|
||||
self.phasm_code = phasm_code
|
||||
|
||||
def dump_phasm_code(self, textio: TextIO) -> None:
|
||||
"""
|
||||
Dumps the input Phasm code for debugging
|
||||
"""
|
||||
_dump_code(textio, self.phasm_code)
|
||||
|
||||
def parse(self) -> None:
|
||||
"""
|
||||
Parses the Phasm code into an AST
|
||||
"""
|
||||
self.phasm_ast = phasm_parse(self.phasm_code)
|
||||
|
||||
def compile_ast(self) -> None:
|
||||
"""
|
||||
Compiles the Phasm AST into an WebAssembly AST
|
||||
"""
|
||||
self.wasm_ast = phasm_compile(self.phasm_ast)
|
||||
|
||||
def compile_wat(self) -> None:
|
||||
"""
|
||||
Compiles the WebAssembly AST into WebAssembly Assembly code
|
||||
"""
|
||||
self.wasm_asm = self.wasm_ast.to_wat()
|
||||
|
||||
def dump_wasm_wat(self, textio: TextIO) -> None:
|
||||
"""
|
||||
Dumps the intermediate WebAssembly Assembly code for debugging
|
||||
"""
|
||||
_dump_code(textio, self.wasm_asm)
|
||||
|
||||
def compile_wasm(self) -> None:
|
||||
"""
|
||||
Compiles the WebAssembly AST into WebAssembly Binary
|
||||
"""
|
||||
self.wasm_bin = wat2wasm(self.wasm_asm)
|
||||
|
||||
def interpreter_setup(self) -> None:
|
||||
"""
|
||||
Sets up the interpreter
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def interpreter_load(self) -> None:
|
||||
"""
|
||||
Loads the code into the interpreter
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def interpreter_dump_memory(self, textio: TextIO) -> None:
|
||||
"""
|
||||
Dumps the interpreters memory for debugging
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def call(self, function: str, *args: Any) -> Any:
|
||||
"""
|
||||
Calls the given function with the given arguments, returning the result
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
class RunnerPywasm3(RunnerBase):
|
||||
"""
|
||||
Implements a runner for pywasm3
|
||||
|
||||
See https://pypi.org/project/pywasm3/
|
||||
"""
|
||||
env: wasm3.Environment
|
||||
rtime: wasm3.Runtime
|
||||
mod: wasm3.Module
|
||||
|
||||
def interpreter_setup(self) -> None:
|
||||
self.env = wasm3.Environment()
|
||||
self.rtime = self.env.new_runtime(1024 * 1024)
|
||||
|
||||
def interpreter_load(self) -> None:
|
||||
self.mod = self.env.parse_module(self.wasm_bin)
|
||||
self.rtime.load(self.mod)
|
||||
|
||||
def interpreter_dump_memory(self, textio: TextIO) -> None:
|
||||
_dump_memory(textio, self.rtime.get_memory(0))
|
||||
|
||||
def call(self, function: str, *args: Any) -> Any:
|
||||
return self.rtime.find_function(function)(*args)
|
||||
|
||||
def _dump_memory(textio: TextIO, mem: bytes) -> None:
|
||||
line_width = 16
|
||||
|
||||
prev_line = None
|
||||
skip = False
|
||||
for idx in range(0, len(mem), line_width):
|
||||
line = ''
|
||||
for idx2 in range(0, line_width):
|
||||
line += f'{mem[idx + idx2]:02X}'
|
||||
if idx2 % 2 == 1:
|
||||
line += ' '
|
||||
|
||||
if prev_line == line:
|
||||
if not skip:
|
||||
textio.write('**\n')
|
||||
skip = True
|
||||
else:
|
||||
textio.write(f'{idx:08x} {line}\n')
|
||||
|
||||
prev_line = line
|
||||
|
||||
def _dump_code(textio: TextIO, text: str) -> None:
|
||||
line_list = text.split('\n')
|
||||
line_no_width = len(str(len(line_list)))
|
||||
for line_no, line_txt in enumerate(line_list):
|
||||
textio.write('{} {}\n'.format(
|
||||
str(line_no + 1).zfill(line_no_width),
|
||||
line_txt,
|
||||
))
|
||||
@ -60,6 +60,20 @@ def testEntry() -> {type_}:
|
||||
assert 7 == result.returned_value
|
||||
assert TYPE_MAP[type_] == type(result.returned_value)
|
||||
|
||||
@pytest.mark.integration_test
|
||||
@pytest.mark.parametrize('type_', ['u8', 'u32', 'u64'])
|
||||
def test_xor(type_):
|
||||
code_py = f"""
|
||||
@exported
|
||||
def testEntry() -> {type_}:
|
||||
return 10 ^ 3
|
||||
"""
|
||||
|
||||
result = Suite(code_py).run_code()
|
||||
|
||||
assert 9 == result.returned_value
|
||||
assert TYPE_MAP[type_] == type(result.returned_value)
|
||||
|
||||
@pytest.mark.integration_test
|
||||
@pytest.mark.parametrize('type_', ['f32', 'f64'])
|
||||
def test_buildins_sqrt(type_):
|
||||
|
||||
@ -2,30 +2,25 @@ import sys
|
||||
|
||||
import pytest
|
||||
|
||||
import wasm3
|
||||
from .helpers import DASHES
|
||||
from .runners import RunnerPywasm3
|
||||
|
||||
from phasm.compiler import phasm_compile
|
||||
from phasm.parser import phasm_parse
|
||||
def setup_interpreter(phash_code: str) -> RunnerPywasm3:
|
||||
runner = RunnerPywasm3(phash_code)
|
||||
|
||||
from .helpers import DASHES, wat2wasm, _dump_memory, _write_numbered_lines
|
||||
|
||||
def setup_interpreter(code_phasm):
|
||||
phasm_module = phasm_parse(code_phasm)
|
||||
wasm_module = phasm_compile(phasm_module)
|
||||
code_wat = wasm_module.to_wat()
|
||||
runner.parse()
|
||||
runner.compile_ast()
|
||||
runner.compile_wat()
|
||||
runner.compile_wasm()
|
||||
runner.interpreter_setup()
|
||||
runner.interpreter_load()
|
||||
|
||||
sys.stderr.write(f'{DASHES} Phasm {DASHES}\n')
|
||||
runner.dump_phasm_code(sys.stderr)
|
||||
sys.stderr.write(f'{DASHES} Assembly {DASHES}\n')
|
||||
_write_numbered_lines(code_wat)
|
||||
runner.dump_wasm_wat(sys.stderr)
|
||||
|
||||
code_wasm = wat2wasm(code_wat)
|
||||
|
||||
env = wasm3.Environment()
|
||||
mod = env.parse_module(code_wasm)
|
||||
|
||||
rtime = env.new_runtime(1024 * 1024)
|
||||
rtime.load(mod)
|
||||
|
||||
return rtime
|
||||
return runner
|
||||
|
||||
@pytest.mark.integration_test
|
||||
def test___init__():
|
||||
@ -35,20 +30,22 @@ def testEntry() -> u8:
|
||||
return 13
|
||||
"""
|
||||
|
||||
rtime = setup_interpreter(code_py)
|
||||
runner = setup_interpreter(code_py)
|
||||
memory = runner.rtime.get_memory(0)
|
||||
|
||||
# Garbage in the memory so we can test for it
|
||||
for idx in range(128):
|
||||
rtime.get_memory(0)[idx] = idx
|
||||
memory[idx] = idx
|
||||
|
||||
sys.stderr.write(f'{DASHES} Memory (pre run) {DASHES}\n')
|
||||
_dump_memory(rtime.get_memory(0))
|
||||
runner.interpreter_dump_memory(sys.stderr)
|
||||
|
||||
rtime.find_function('stdlib.alloc.__init__')()
|
||||
runner.call('stdlib.alloc.__init__')
|
||||
|
||||
sys.stderr.write(f'{DASHES} Memory (pre run) {DASHES}\n')
|
||||
_dump_memory(rtime.get_memory(0))
|
||||
sys.stderr.write(f'{DASHES} Memory (post run) {DASHES}\n')
|
||||
runner.interpreter_dump_memory(sys.stderr)
|
||||
|
||||
memory = rtime.get_memory(0).tobytes()
|
||||
memory = memory.tobytes()
|
||||
|
||||
assert (
|
||||
b'\xC0\xA1\x00\x00'
|
||||
@ -66,16 +63,13 @@ def testEntry() -> u8:
|
||||
return 13
|
||||
"""
|
||||
|
||||
rtime = setup_interpreter(code_py)
|
||||
|
||||
for idx in range(128):
|
||||
rtime.get_memory(0)[idx] = idx
|
||||
runner = setup_interpreter(code_py)
|
||||
|
||||
sys.stderr.write(f'{DASHES} Memory (pre run) {DASHES}\n')
|
||||
_dump_memory(rtime.get_memory(0))
|
||||
runner.interpreter_dump_memory(sys.stderr)
|
||||
|
||||
with pytest.raises(RuntimeError, match='unreachable executed'):
|
||||
rtime.find_function('stdlib.alloc.__alloc__')(32)
|
||||
runner.call('stdlib.alloc.__alloc__', 32)
|
||||
|
||||
@pytest.mark.integration_test
|
||||
def test___alloc___ok():
|
||||
@ -85,23 +79,19 @@ def testEntry() -> u8:
|
||||
return 13
|
||||
"""
|
||||
|
||||
rtime = setup_interpreter(code_py)
|
||||
|
||||
for idx in range(128):
|
||||
rtime.get_memory(0)[idx] = idx
|
||||
runner = setup_interpreter(code_py)
|
||||
memory = runner.rtime.get_memory(0)
|
||||
|
||||
sys.stderr.write(f'{DASHES} Memory (pre run) {DASHES}\n')
|
||||
_dump_memory(rtime.get_memory(0))
|
||||
runner.interpreter_dump_memory(sys.stderr)
|
||||
|
||||
rtime.find_function('stdlib.alloc.__init__')()
|
||||
offset0 = rtime.find_function('stdlib.alloc.__alloc__')(32)
|
||||
offset1 = rtime.find_function('stdlib.alloc.__alloc__')(32)
|
||||
offset2 = rtime.find_function('stdlib.alloc.__alloc__')(32)
|
||||
runner.call('stdlib.alloc.__init__')
|
||||
offset0 = runner.call('stdlib.alloc.__alloc__', 32)
|
||||
offset1 = runner.call('stdlib.alloc.__alloc__', 32)
|
||||
offset2 = runner.call('stdlib.alloc.__alloc__', 32)
|
||||
|
||||
sys.stderr.write(f'{DASHES} Memory (pre run) {DASHES}\n')
|
||||
_dump_memory(rtime.get_memory(0))
|
||||
|
||||
memory = rtime.get_memory(0).tobytes()
|
||||
sys.stderr.write(f'{DASHES} Memory (post run) {DASHES}\n')
|
||||
runner.interpreter_dump_memory(sys.stderr)
|
||||
|
||||
assert b'\xC0\xA1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' == memory[0:12]
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user