MVP #1

Merged
jbwdevries merged 73 commits from idea_crc32 into master 2022-08-21 12:59:21 +00:00
5 changed files with 276 additions and 33 deletions
Showing only changes of commit 374231d206 - Show all commits

View File

@ -9,6 +9,11 @@ from . import wasm
Statements = Generator[wasm.Statement, None, None] Statements = Generator[wasm.Statement, None, None]
def type_(inp: ourlang.OurType) -> wasm.OurType: def type_(inp: ourlang.OurType) -> wasm.OurType:
if isinstance(inp, ourlang.OurTypeUInt8):
# WebAssembly has only support for 32 and 64 bits
# So we need to store more memory per byte
return wasm.OurTypeInt32()
if isinstance(inp, ourlang.OurTypeInt32): if isinstance(inp, ourlang.OurTypeInt32):
return wasm.OurTypeInt32() return wasm.OurTypeInt32()
@ -21,7 +26,7 @@ def type_(inp: ourlang.OurType) -> wasm.OurType:
if isinstance(inp, ourlang.OurTypeFloat64): if isinstance(inp, ourlang.OurTypeFloat64):
return wasm.OurTypeFloat64() return wasm.OurTypeFloat64()
if isinstance(inp, (ourlang.Struct, ourlang.OurTypeTuple, )): if isinstance(inp, (ourlang.Struct, ourlang.OurTypeTuple, ourlang.OurTypeBytes)):
# Structs and tuples are passed as pointer # Structs and tuples are passed as pointer
# And pointers are i32 # And pointers are i32
return wasm.OurTypeInt32() return wasm.OurTypeInt32()
@ -51,6 +56,10 @@ I64_OPERATOR_MAP = { # TODO: Introduce UInt32 type
} }
def expression(inp: ourlang.Expression) -> Statements: def expression(inp: ourlang.Expression) -> Statements:
if isinstance(inp, ourlang.ConstantUInt8):
yield wasm.Statement('i32.const', str(inp.value))
return
if isinstance(inp, ourlang.ConstantInt32): if isinstance(inp, ourlang.ConstantInt32):
yield wasm.Statement('i32.const', str(inp.value)) yield wasm.Statement('i32.const', str(inp.value))
return return
@ -112,6 +121,12 @@ def expression(inp: ourlang.Expression) -> Statements:
yield wasm.Statement(f'f64.{inp.operator}') yield wasm.Statement(f'f64.{inp.operator}')
return return
if isinstance(inp.type, ourlang.OurTypeInt32):
if inp.operator == 'len':
if isinstance(inp.right.type, ourlang.OurTypeBytes):
yield wasm.Statement('i32.load')
return
raise NotImplementedError(expression, inp.type, inp.operator) raise NotImplementedError(expression, inp.type, inp.operator)
if isinstance(inp, ourlang.FunctionCall): if isinstance(inp, ourlang.FunctionCall):
@ -121,17 +136,33 @@ def expression(inp: ourlang.Expression) -> Statements:
yield wasm.Statement('call', '${}'.format(inp.function.name)) yield wasm.Statement('call', '${}'.format(inp.function.name))
return return
if isinstance(inp, ourlang.AccessBytesIndex):
if not isinstance(inp.type, ourlang.OurTypeUInt8):
raise NotImplementedError(inp, inp.type)
if not isinstance(inp.offset, int):
raise NotImplementedError(inp, inp.offset)
yield from expression(inp.varref)
yield wasm.Statement('i32.const', str(inp.offset))
yield wasm.Statement('call', '$___access_bytes_index___')
return
if isinstance(inp, ourlang.AccessStructMember): if isinstance(inp, ourlang.AccessStructMember):
if isinstance(inp.member.type, ourlang.OurTypeUInt8):
mtyp = 'i32'
else:
# FIXME: Properly implement this # FIXME: Properly implement this
# inp.type.render() is also a hack that doesn't really work consistently # inp.type.render() is also a hack that doesn't really work consistently
if not isinstance(inp.type, ( if not isinstance(inp.member.type, (
ourlang.OurTypeInt32, ourlang.OurTypeFloat32, ourlang.OurTypeInt32, ourlang.OurTypeFloat32,
ourlang.OurTypeInt64, ourlang.OurTypeFloat64, ourlang.OurTypeInt64, ourlang.OurTypeFloat64,
)): )):
raise NotImplementedError(inp, inp.type) raise NotImplementedError
mtyp = inp.member.type.render()
yield from expression(inp.varref) yield from expression(inp.varref)
yield wasm.Statement(inp.type.render() + '.load', 'offset=' + str(inp.member.offset)) yield wasm.Statement(f'{mtyp}.load', 'offset=' + str(inp.member.offset))
return return
if isinstance(inp, ourlang.AccessTupleMember): if isinstance(inp, ourlang.AccessTupleMember):
@ -221,7 +252,8 @@ def module(inp: ourlang.Module) -> wasm.Module:
result = wasm.Module() result = wasm.Module()
result.functions = [ result.functions = [
_generate_allocator(inp), _generate____new_reference___(inp),
_generate____access_bytes_index___(inp),
] + [ ] + [
function(x) function(x)
for x in inp.functions.values() for x in inp.functions.values()
@ -229,7 +261,7 @@ def module(inp: ourlang.Module) -> wasm.Module:
return result return result
def _generate_allocator(mod: ourlang.Module) -> wasm.Function: def _generate____new_reference___(mod: ourlang.Module) -> wasm.Function:
return wasm.Function( return wasm.Function(
'___new_reference___', '___new_reference___',
True, True,
@ -252,6 +284,38 @@ def _generate_allocator(mod: ourlang.Module) -> wasm.Function:
], ],
) )
def _generate____access_bytes_index___(mod: ourlang.Module) -> wasm.Function:
return wasm.Function(
'___access_bytes_index___',
False,
[
('byt', type_(mod.types['i32']), ),
('ofs', type_(mod.types['i32']), ),
],
[
],
type_(mod.types['i32']),
[
wasm.Statement('local.get', '$ofs'),
wasm.Statement('local.get', '$byt'),
wasm.Statement('i32.load'),
wasm.Statement('i32.lt_u'),
wasm.Statement('if', comment='$ofs < len($byt)'),
wasm.Statement('local.get', '$byt'),
wasm.Statement('i32.const', '4', comment='Leading size field'),
wasm.Statement('i32.add'),
wasm.Statement('local.get', '$ofs'),
wasm.Statement('i32.add'),
wasm.Statement('i32.load8_u', comment='Within bounds'),
wasm.Statement('return'),
wasm.Statement('end'),
wasm.Statement('i32.const', str(0), comment='Out of bounds'),
wasm.Statement('return'),
],
)
def _generate_tuple_constructor(inp: ourlang.TupleConstructor) -> Statements: def _generate_tuple_constructor(inp: ourlang.TupleConstructor) -> Statements:
yield wasm.Statement('i32.const', str(inp.tuple.alloc_size())) yield wasm.Statement('i32.const', str(inp.tuple.alloc_size()))
yield wasm.Statement('call', '$___new_reference___') yield wasm.Statement('call', '$___new_reference___')
@ -280,6 +344,9 @@ def _generate_struct_constructor(inp: ourlang.StructConstructor) -> Statements:
yield wasm.Statement('local.set', '$___new_reference___addr') yield wasm.Statement('local.set', '$___new_reference___addr')
for member in inp.struct.members: for member in inp.struct.members:
if isinstance(member.type, ourlang.OurTypeUInt8):
mtyp = 'i32'
else:
# FIXME: Properly implement this # FIXME: Properly implement this
# inp.type.render() is also a hack that doesn't really work consistently # inp.type.render() is also a hack that doesn't really work consistently
if not isinstance(member.type, ( if not isinstance(member.type, (
@ -287,9 +354,10 @@ def _generate_struct_constructor(inp: ourlang.StructConstructor) -> Statements:
ourlang.OurTypeInt64, ourlang.OurTypeFloat64, ourlang.OurTypeInt64, ourlang.OurTypeFloat64,
)): )):
raise NotImplementedError raise NotImplementedError
mtyp = member.type.render()
yield wasm.Statement('local.get', '$___new_reference___addr') yield wasm.Statement('local.get', '$___new_reference___addr')
yield wasm.Statement('local.get', f'${member.name}') yield wasm.Statement('local.get', f'${member.name}')
yield wasm.Statement(f'{member.type.render()}.store', 'offset=' + str(member.offset)) yield wasm.Statement(f'{mtyp}.store', 'offset=' + str(member.offset))
yield wasm.Statement('local.get', '$___new_reference___addr') yield wasm.Statement('local.get', '$___new_reference___addr')

View File

@ -38,6 +38,18 @@ class OurTypeNone(OurType):
def render(self) -> str: def render(self) -> str:
return 'None' return 'None'
class OurTypeUInt8(OurType):
"""
The Integer type, unsigned and 8 bits wide
"""
__slots__ = ()
def render(self) -> str:
return 'u8'
def alloc_size(self) -> int:
return 4 # Int32 under the hood
class OurTypeInt32(OurType): class OurTypeInt32(OurType):
""" """
The Integer type, signed and 32 bits wide The Integer type, signed and 32 bits wide
@ -86,6 +98,15 @@ class OurTypeFloat64(OurType):
def alloc_size(self) -> int: def alloc_size(self) -> int:
return 8 return 8
class OurTypeBytes(OurType):
"""
The bytes type
"""
__slots__ = ()
def render(self) -> str:
return 'bytes'
class TupleMember: class TupleMember:
""" """
Represents a tuple member Represents a tuple member
@ -146,6 +167,21 @@ class Constant(Expression):
""" """
__slots__ = () __slots__ = ()
class ConstantUInt8(Constant):
"""
An UInt8 constant value expression within a statement
"""
__slots__ = ('value', )
value: int
def __init__(self, type_: OurTypeUInt8, value: int) -> None:
super().__init__(type_)
self.value = value
def render(self) -> str:
return str(self.value)
class ConstantInt32(Constant): class ConstantInt32(Constant):
""" """
An Int32 constant value expression within a statement An Int32 constant value expression within a statement
@ -257,7 +293,7 @@ class UnaryOp(Expression):
self.right = right self.right = right
def render(self) -> str: def render(self) -> str:
if self.operator in WEBASSEMBLY_BUILDIN_FLOAT_OPS: if self.operator in WEBASSEMBLY_BUILDIN_FLOAT_OPS or self.operator == 'len':
return f'{self.operator}({self.right.render()})' return f'{self.operator}({self.right.render()})'
return f'{self.operator}{self.right.render()}' return f'{self.operator}{self.right.render()}'
@ -291,6 +327,24 @@ class FunctionCall(Expression):
return f'{self.function.name}({args})' return f'{self.function.name}({args})'
class AccessBytesIndex(Expression):
"""
Access a bytes index for reading
"""
__slots__ = ('varref', 'offset', )
varref: VariableReference
offset: int
def __init__(self, type_: OurType, varref: VariableReference, offset: int) -> None:
super().__init__(type_)
self.varref = varref
self.offset = offset
def render(self) -> str:
return f'{self.varref.render()}[{self.offset}]'
class AccessStructMember(Expression): class AccessStructMember(Expression):
""" """
Access a struct member for reading of writing Access a struct member for reading of writing
@ -554,10 +608,12 @@ class Module:
def __init__(self) -> None: def __init__(self) -> None:
self.types = { self.types = {
'u8': OurTypeUInt8(),
'i32': OurTypeInt32(), 'i32': OurTypeInt32(),
'i64': OurTypeInt64(), 'i64': OurTypeInt64(),
'f32': OurTypeFloat32(), 'f32': OurTypeFloat32(),
'f64': OurTypeFloat64(), 'f64': OurTypeFloat64(),
'bytes': OurTypeBytes(),
} }
self.functions = {} self.functions = {}
self.structs = {} self.structs = {}
@ -900,6 +956,18 @@ class OurVisitor:
'sqrt', 'sqrt',
self.visit_Module_FunctionDef_expr(module, function, our_locals, exp_type, node.args[0]), self.visit_Module_FunctionDef_expr(module, function, our_locals, exp_type, node.args[0]),
) )
elif node.func.id == 'len':
if not isinstance(exp_type, OurTypeInt32):
_raise_static_error(node, f'Cannot make {node.func.id} result in {exp_type}')
if 1 != len(node.args):
_raise_static_error(node, f'Function {node.func.id} requires 1 arguments but {len(node.args)} are given')
return UnaryOp(
exp_type,
'len',
self.visit_Module_FunctionDef_expr(module, function, our_locals, module.types['bytes'], node.args[0]),
)
else: else:
if node.func.id not in module.functions: if node.func.id not in module.functions:
_raise_static_error(node, 'Call to undefined function') _raise_static_error(node, 'Call to undefined function')
@ -966,6 +1034,13 @@ class OurVisitor:
_raise_static_error(node, f'Undefined variable {node.value.id}') _raise_static_error(node, f'Undefined variable {node.value.id}')
node_typ = our_locals[node.value.id] node_typ = our_locals[node.value.id]
if isinstance(node_typ, OurTypeBytes):
return AccessBytesIndex(
module.types['u8'],
VariableReference(node_typ, node.value.id),
idx,
)
if not isinstance(node_typ, OurTypeTuple): if not isinstance(node_typ, OurTypeTuple):
_raise_static_error(node, f'Cannot take index of non-tuple {node.value.id}') _raise_static_error(node, f'Cannot take index of non-tuple {node.value.id}')
@ -987,6 +1062,14 @@ class OurVisitor:
_not_implemented(node.kind is None, 'Constant.kind') _not_implemented(node.kind is None, 'Constant.kind')
if isinstance(exp_type, OurTypeUInt8):
if not isinstance(node.value, int):
_raise_static_error(node, 'Expected integer value')
# FIXME: Range check
return ConstantUInt8(exp_type, node.value)
if isinstance(exp_type, OurTypeInt32): if isinstance(exp_type, OurTypeInt32):
if not isinstance(node.value, int): if not isinstance(node.value, int):
_raise_static_error(node, 'Expected integer value') _raise_static_error(node, 'Expected integer value')

View File

@ -99,6 +99,15 @@ def _run_pywasm(code_wasm, args):
runtime = pywasm.Runtime(module, result.make_imports(), {}) runtime = pywasm.Runtime(module, result.make_imports(), {})
def set_byte(idx, byt):
runtime.store.mems[0].data[idx] = byt
args = _convert_bytes_arguments(
args,
lambda x: runtime.exec('___new_reference___', [x]),
set_byte
)
sys.stderr.write(f'{DASHES} Memory (pre run) {DASHES}\n') sys.stderr.write(f'{DASHES} Memory (pre run) {DASHES}\n')
_dump_memory(runtime.store.mems[0].data) _dump_memory(runtime.store.mems[0].data)
@ -120,13 +129,22 @@ def _run_pywasm3(code_wasm, args):
rtime = env.new_runtime(1024 * 1024) rtime = env.new_runtime(1024 * 1024)
rtime.load(mod) rtime.load(mod)
# sys.stderr.write(f'{DASHES} Memory (pre run) {DASHES}\n') def set_byte(idx, byt):
# _dump_memory(rtime.get_memory(0).tobytes()) rtime.get_memory(0)[idx] = byt
args = _convert_bytes_arguments(
args,
rtime.find_function('___new_reference___'),
set_byte
)
sys.stderr.write(f'{DASHES} Memory (pre run) {DASHES}\n')
_dump_memory(rtime.get_memory(0))
result.returned_value = rtime.find_function('testEntry')(*args) result.returned_value = rtime.find_function('testEntry')(*args)
# sys.stderr.write(f'{DASHES} Memory (post run) {DASHES}\n') sys.stderr.write(f'{DASHES} Memory (post run) {DASHES}\n')
# _dump_memory(rtime.get_memory(0).tobytes()) _dump_memory(rtime.get_memory(0))
return result return result
@ -173,6 +191,27 @@ def _run_wasmer(code_wasm, args):
return result return result
def _convert_bytes_arguments(args, new_reference, set_byte):
result = []
for arg in args:
if not isinstance(arg, bytes):
result.append(arg)
continue
# TODO: Implement and use the bytes constructor function
offset = new_reference(len(arg) + 4)
result.append(offset)
# Store the length prefix
for idx, byt in enumerate(len(arg).to_bytes(4, byteorder='little')):
set_byte(offset + idx, byt)
# Store the actual bytes
for idx, byt in enumerate(arg):
set_byte(offset + 4 + idx, byt)
return result
def _dump_memory(mem): def _dump_memory(mem):
line_width = 16 line_width = 16

View File

@ -0,0 +1,15 @@
import pytest
from .helpers import Suite
@pytest.mark.integration_test
def test_bytes_index_out_of_bounds():
code_py = """
@exported
def testEntry(f: bytes) -> u8:
return f[50]
"""
result = Suite(code_py, 'test_call').run_code(b'Short', b'Long' * 100)
assert 0 == result.returned_value

View File

@ -3,6 +3,7 @@ import pytest
from .helpers import Suite from .helpers import Suite
TYPE_MAP = { TYPE_MAP = {
'u8': int,
'i32': int, 'i32': int,
'i64': int, 'i64': int,
'f32': float, 'f32': float,
@ -10,7 +11,7 @@ TYPE_MAP = {
} }
@pytest.mark.integration_test @pytest.mark.integration_test
@pytest.mark.parametrize('type_', ['i32', 'i64', 'f32', 'f64']) @pytest.mark.parametrize('type_', ['i32', 'i64', 'f32', 'f64', 'u8'])
def test_return(type_): def test_return(type_):
code_py = f""" code_py = f"""
@exported @exported
@ -66,7 +67,7 @@ def testEntry() -> {type_}:
assert TYPE_MAP[type_] == type(result.returned_value) assert TYPE_MAP[type_] == type(result.returned_value)
@pytest.mark.integration_test @pytest.mark.integration_test
@pytest.mark.parametrize('type_', ['i32', 'i64', 'f32', 'f64']) @pytest.mark.parametrize('type_', ['i32', 'i64', 'f32', 'f64', 'u8'])
def test_arg(type_): def test_arg(type_):
code_py = f""" code_py = f"""
@exported @exported
@ -273,22 +274,23 @@ def testEntry() -> i32:
assert [] == result.log_int32_list assert [] == result.log_int32_list
@pytest.mark.integration_test @pytest.mark.integration_test
def test_struct_0(): @pytest.mark.parametrize('type_', ['i32', 'i64', 'f32', 'f64', 'u8'])
code_py = """ def test_struct_0(type_):
code_py = f"""
class CheckedValue: class CheckedValue:
value: i32 value: {type_}
@exported @exported
def testEntry() -> i32: def testEntry() -> {type_}:
return helper(CheckedValue(2345)) return helper(CheckedValue(23))
def helper(cv: CheckedValue) -> i32: def helper(cv: CheckedValue) -> {type_}:
return cv.value return cv.value
""" """
result = Suite(code_py, 'test_call').run_code() result = Suite(code_py, 'test_call').run_code()
assert 2345 == result.returned_value assert 23 == result.returned_value
assert [] == result.log_int32_list assert [] == result.log_int32_list
@pytest.mark.integration_test @pytest.mark.integration_test
@ -366,6 +368,42 @@ def helper(v: (f32, f32, f32, )) -> f32:
assert 3.74 < result.returned_value < 3.75 assert 3.74 < result.returned_value < 3.75
assert [] == result.log_int32_list assert [] == result.log_int32_list
@pytest.mark.integration_test
def test_bytes_address():
code_py = """
@exported
def testEntry(f: bytes) -> bytes:
return f
"""
result = Suite(code_py, 'test_call').run_code(b'This is a test')
assert 4 == result.returned_value
@pytest.mark.integration_test
def test_bytes_length():
code_py = """
@exported
def testEntry(f: bytes) -> i32:
return len(f)
"""
result = Suite(code_py, 'test_call').run_code(b'This is another test')
assert 20 == result.returned_value
@pytest.mark.integration_test
def test_bytes_index():
code_py = """
@exported
def testEntry(f: bytes) -> u8:
return f[8]
"""
result = Suite(code_py, 'test_call').run_code(b'This is another test')
assert 0x61 == result.returned_value
@pytest.mark.integration_test @pytest.mark.integration_test
@pytest.mark.skip('SIMD support is but a dream') @pytest.mark.skip('SIMD support is but a dream')
def test_tuple_i32x4(): def test_tuple_i32x4():