phasm/phasm/wasm.py
Johan B.W. de Vries 97b61e3ee1 Test generation framework with typing improvements
Prior to this PR, each type would have its own handwritten
test suite. The end result was that not all types were tested
for all situations.

This PR adds a framework based on a Markdown file, which
generates the basic tests for the types defined in json
files. These are auto generated and updated by the Makefile
before the test suite is run.

Also, a number of unsupported type combinations are now
supported.

Also, we now support negative literals.

Also, allocation calculation fixes for nested types.

Also, the test helpers can now properly import and export
typed variables such as bytes, static arrays and tuples. This
may come in handy when it comes to phasm platform wanting to
route data.

Also, adds better support for i8 type.

Also, started on a runtime.py, since there's quite some code
now that deals with compile time handling of WebAssembly stuff.

Also, minor improvement to the type constrains, namely we
better match 'tuple' literals with static array types.

Also, reduced spam when printing the type analysis results;
constraints that go back on the backlog are now no longer
printed one by one. It now also prints the end results of
the typing analysis.

Also, reorganized the big test_primitives test into type
classes.

Also, replaced pylint with ruff.
2023-11-15 12:52:23 +01:00

201 lines
4.9 KiB
Python

"""
Python classes for storing the representation of Web Assembly code,
and being able to conver it to Web Assembly Text Format
"""
from typing import Iterable, List, Optional, Tuple
class WatSerializable:
"""
Mixin for clases that can be serialized as WebAssembly Text
"""
def to_wat(self) -> str:
"""
Renders this object as WebAssembly Text
"""
raise NotImplementedError(self, 'to_wat')
class WasmType(WatSerializable):
"""
Type base class
"""
class WasmTypeNone(WasmType):
"""
Type when there is no type
"""
def to_wat(self) -> str:
raise Exception('None type is only a placeholder')
class WasmTypeInt32(WasmType):
"""
i32 value
Signed or not depends on the operations, not the type
"""
def to_wat(self) -> str:
return 'i32'
class WasmTypeInt64(WasmType):
"""
i64 value
Signed or not depends on the operations, not the type
"""
def to_wat(self) -> str:
return 'i64'
class WasmTypeFloat32(WasmType):
"""
f32 value
"""
def to_wat(self) -> str:
return 'f32'
class WasmTypeFloat64(WasmType):
"""
f64 value
"""
def to_wat(self) -> str:
return 'f64'
class WasmTypeVector(WasmType):
"""
A vector is a 128-bit value
"""
def to_wat(self) -> str:
return 'v128'
class WasmTypeVectorInt32x4(WasmTypeVector):
"""
4 Int32 values in a single vector
"""
Param = Tuple[str, WasmType]
class Import(WatSerializable):
"""
Represents a Web Assembly import
"""
def __init__(
self,
module: str,
name: str,
intname: str,
params: Iterable[Param],
result: WasmType,
) -> None:
self.module = module
self.name = name
self.intname = intname
self.params = [*params]
self.result = result
def to_wat(self) -> str:
return '(import "{}" "{}" (func ${}{}{}))'.format(
self.module,
self.name,
self.intname,
''.join(
f' (param {typ.to_wat()})'
for _, typ in self.params
),
'' if isinstance(self.result, WasmTypeNone)
else f' (result {self.result.to_wat()})'
)
class Statement(WatSerializable):
"""
Represents a Web Assembly statement
"""
def __init__(self, name: str, *args: str, comment: Optional[str] = None):
self.name = name
self.args = args
self.comment = comment
def to_wat(self) -> str:
args = ' '.join(self.args)
comment = f' ;; {self.comment}' if self.comment else ''
return f'{self.name} {args}{comment}'
class Function(WatSerializable):
"""
Represents a Web Assembly function
"""
def __init__(
self,
name: str,
exported_name: Optional[str],
params: Iterable[Param],
locals_: Iterable[Param],
result: WasmType,
statements: Iterable[Statement],
) -> None:
self.name = name
self.exported_name = exported_name
self.params = [*params]
self.locals = [*locals_]
self.result = result
self.statements = [*statements]
def to_wat(self) -> str:
header = f'${self.name}' # Name for internal use
if self.exported_name is not None:
# Name for external use
header += f' (export "{self.exported_name}")'
for nam, typ in self.params:
header += f' (param ${nam} {typ.to_wat()})'
if not isinstance(self.result, WasmTypeNone):
header += f' (result {self.result.to_wat()})'
for nam, typ in self.locals:
header += f' (local ${nam} {typ.to_wat()})'
return '(func {}\n {}\n )'.format(
header,
'\n '.join(x.to_wat() for x in self.statements),
)
class ModuleMemory(WatSerializable):
"""
Represents a WebAssembly module's memory
"""
def __init__(self, data: bytes = b'') -> None:
self.data = data
def to_wat(self) -> str:
data = ''.join(
f'\\{x:02x}'
for x in self.data
)
return (
'(memory 1)\n '
f'(data (memory 0) (i32.const 0) "{data}")\n '
'(export "memory" (memory 0))\n'
)
class Module(WatSerializable):
"""
Represents a Web Assembly module
"""
def __init__(self) -> None:
self.imports: List[Import] = []
self.functions: List[Function] = []
self.memory = ModuleMemory()
def to_wat(self) -> str:
"""
Generates the text version
"""
return '(module\n {}\n {}\n {})\n'.format(
'\n '.join(x.to_wat() for x in self.imports),
self.memory.to_wat(),
'\n '.join(x.to_wat() for x in self.functions),
)