Compare commits

..

77 Commits

Author SHA1 Message Date
Johan B.W. de Vries
d1854d7a38 Cleanup to type5 solver
- Replaces weird CheckResult class with proper data classes.
- When a constraint results in new constraints, those are
  treated first
- Minor readability change to function return type
- Code cleanup
- TODO cleanup
- Typo fixes
2025-08-30 14:45:12 +02:00
71691d68e9 Merge pull request 'Removes the weird second step unify' (#9) from rework-unify-to-be-a-normal-constraint into master
Reviewed-on: #9
2025-08-24 14:07:36 +00:00
Johan B.W. de Vries
7df9d5af12 Removes the weird second step unify
It is now part of the normal constraints. Added a special
workaround for functions, since otherwise the output is a
bit redundant and quite confusing.

Also, constraints are now processed in order of complexity.
This does not affect type safety. It uses a bit more CPU.
But it makes the output that much easier to read.

Also, removes the weird FunctionInstance hack. Instead,
the more industry standard way of annotation the types
on the function call is used. As always, this requires some
hackyness for Subscriptable.

Also, adds a few comments to the type unification to help
with debugging.

Also, prints out the new constraints that are received.
2025-08-24 16:06:42 +02:00
3d504e3d79 Merge pull request 'Replaces type3 with type5' (#8) from reworking-type-unification into master
Reviewed-on: #8
2025-08-21 17:29:28 +00:00
Johan B.W. de Vries
6a1f4fc010 Replaces type3 with type5
type5 is much more first principles based, so we get a lot
of weird quirks removed:

- FromLiteral no longer needs to understand AST
- Type unifications works more like Haskell
- Function types are just ordinary types, saving a lot of
  manual busywork

and more.
2025-08-21 19:26:42 +02:00
Johan B.W. de Vries
1a3bc19dce Fix linting issues 2025-06-07 14:38:54 +02:00
Johan B.W. de Vries
544bbfac72 Updates the README for clarity 2025-06-07 14:00:03 +02:00
Johan B.W. de Vries
8a1a6af3e7 Reworked the examples to be more welcoming 2025-06-05 19:49:07 +02:00
Johan B.W. de Vries
3cb4860973 Subscriptable is now less hardcoded
Now only the tuple variant is hardcoded. The rest is via
a typeclass.
2025-06-02 19:01:20 +02:00
Johan B.W. de Vries
6f40276a9c Fix: Subscript all dynamic arrays
The type checker would only allow bytes.
2025-06-02 18:06:12 +02:00
Johan B.W. de Vries
38294497cb Moves the prelude to runtime
Previously, it was hardcoded at 'compile' time (in as much
Python has that). This would make it more difficult to add
stuff to it. Also, in a lot of places we made assumptions
about prelude instead of checking properly.
2025-05-29 16:43:37 +02:00
Johan B.W. de Vries
d97be81828 Optimise: Remove unused functions
By default, we add a lot of build in functions that may
never get called.

This commit adds a simple reachability graph algorithm
to remove functions that can't be called from outside.

Also, unmarks a lot of functions as being exported. It
was the default to export - now it's the default to not
export.

Also, some general cleanup to the wasm statement calls.
2025-05-25 16:39:25 +02:00
Johan B.W. de Vries
84e7c42ea4 Implements u16 / i16 support
Keep in mind that WebAssembly is u32 native by default,
some operations may be more expensive than you expect
them to be.
2025-05-25 15:31:23 +02:00
Johan B.W. de Vries
d017ebe096 Support tail calls 2025-05-25 14:42:31 +02:00
Johan B.W. de Vries
2c2a96c8a7 Added some more missing test cases for promotable 2025-05-25 14:21:25 +02:00
Johan B.W. de Vries
b670bb02ad Exposes Wasm's convert and trunc(ate) function
Also adds a missing type case for promotable
2025-05-25 14:13:46 +02:00
Johan B.W. de Vries
56ab88db2c Exposes Wasm's reinterpret function 2025-05-25 13:45:18 +02:00
Johan B.W. de Vries
cfdcaa230d Added a missing type check test 2025-05-21 19:22:45 +02:00
Johan B.W. de Vries
fdaa680572 Cleanup todo 2025-05-21 19:01:15 +02:00
Johan B.W. de Vries
b48260ccfa Removes the special casing for foldl
Now both dynamic and static arrays can be fully fold'ed.

Also adds support for type classes that have a function
argument.

Also, various usability improvements to WasmGenerator.

Also, integration tests now don't dump their stuff without
VERBOSE=1, this speeds up the tests suite by a factor of 9.

Also, tests can now set with_traces to add a number of
tracing functions for help debugging your code.
2025-05-21 18:28:37 +02:00
Johan B.W. de Vries
46b06dbcf1 Cleanup: TYPE_INFO_MAP
This also removes the InternalPassAsPointer experiment.

This also fixes that u8 values were stores as 32 bits
in structs and tuples (but not dynamic arrays since that
is special cased as bytes).

Also, fixes allocation issue wi	th dynamic arrays, it
would allocate quadratic amount of memory.
2025-05-19 21:04:13 +02:00
Johan B.W. de Vries
83186cce78 Reworks bytes into dynamic array
bytes continues to be the preferred name for u8[...].
Also, putting bytes values into the VM and taking them
out still uses Python bytes values.

This also lets used use the len function on them, for
whatever that's worth.
2025-05-18 15:37:13 +02:00
Johan B.W. de Vries
a72bd60de2 Adds functions as passable values 2025-05-17 19:43:52 +02:00
Johan B.W. de Vries
99d2b22336 Moved the typeclasse tests. Fix typeclass name. 2025-05-17 18:42:27 +02:00
Johan B.W. de Vries
ac4b46bbe7 Fix: You could assign structs to each other
As long as the arguments matched at least.
2025-05-12 20:00:56 +02:00
Johan B.W. de Vries
67af569448 Cleanup CanBeSubscriptedConstraint
It was using an AST argument, and I'd rather not have those
in the typing system (except the generator).
2025-05-12 19:20:50 +02:00
Johan B.W. de Vries
df5c1911bf Cleans up imports
Rather than accessing the classes from the module, this MR
cleans up the code a bit to use the classes directly.
2025-05-12 18:51:19 +02:00
Johan B.W. de Vries
b5f0fda133 Implements sum for Foldable types
Foldable take a TypeConstructor. The first argument must be a
NatNum.

The FunctionSignatureRouter wasn't completely on point, instead
this commit adds an TypeClassArgsRouter lookup router. This
makes sense since the only available arguments we have to find
a router is the list of type class arguments.
2025-05-12 18:36:37 +02:00
Johan B.W. de Vries
6c627bca01 Reworks function lookup
Before this commit, finding the implementation for a type
class method was done with a simple lookup table.

This commit adds a router based on function signature.
This also paves the way for adding type constructor
arguments in function signatures.

And it removes quite a few references to the prelude out
of the compiler.

Also adds a bunch of helper methods to render signatures
as strings.
2025-05-10 19:42:17 +02:00
Johan B.W. de Vries
78c98b1e61 Fix: Type error
That's what I get for cleaning up without running tests.

This removes a intermediate hack to detect missing routes.
2025-05-10 16:51:38 +02:00
Johan B.W. de Vries
f8d107f4fa Replaces did_construct with a proper router
By annotating types with the constructor application
that was used to create them.

Later on we can use the router to replace compiler's
INSTANCES or for user defined types.
2025-05-10 16:49:10 +02:00
Johan B.W. de Vries
6b66935c67 Chore: Cleanup type checks
A lot of isinstance checks no longer did anything, since
the referred to variable was always a type.

In some places, we used it to check if a type was internal,
it's unclear if we will need to rebuild those checks in
the future.
2025-05-07 19:07:55 +02:00
Johan B.W. de Vries
d9a08cf0f7 Chore: Placeholders are now internal
They were exposed on AST, causing confusion.
Now they're only used in constraints.
2025-05-07 19:07:51 +02:00
Johan B.W. de Vries
f6cb1a8c1d Adds a FIXME for accessing non-struct members 2025-05-05 12:35:49 +02:00
Johan B.W. de Vries
45c38d5f88 Fix: function comment that's no longer true 2025-05-05 12:26:15 +02:00
Johan B.W. de Vries
42cb38d67d Clean up todo list 2025-05-02 21:29:24 +02:00
Johan B.W. de Vries
bee0c845a8 Removes UnaryOp
It was no longer used.
2025-05-02 21:12:25 +02:00
Johan B.W. de Vries
44b95af4ba Removes the cast / u32 hacky way of casting. 2025-05-02 21:12:05 +02:00
Johan B.W. de Vries
1da1adac9f Implements Extendable and Promotable 2025-05-02 21:05:07 +02:00
Johan B.W. de Vries
a2e1dfd799 Reworks type class instantiation
Before, a type class was a property of a type.
But that doesn't make any sense for multi parameter
type classes.

Had to make a hacky way for type classes with
type constructors.
2025-04-27 17:45:13 +02:00
Johan B.W. de Vries
11fde4cb9e Add missing fix for generator 2025-04-27 15:30:17 +02:00
Johan B.W. de Vries
c8009403c4 Separates out TypeVariable and constraints
They look a lot like placeholders, but they exist before
the typing system takes place. And there's a (much smaller)
context to deal with.

For now, removes Placeholders in user function definitions
as they were not implemented.

Adds signature to function to try to get them closer to
type class methods. Already seeing some benefit in the
constraint generator.

Stricter zipping for safety.
2025-04-27 15:28:08 +02:00
Johan B.W. de Vries
d3e38b96b2 Removed the len special casing 2025-04-27 12:37:17 +02:00
Johan B.W. de Vries
292c9548fb Removes some hardcoded references to prelude 2025-04-27 12:10:27 +02:00
Johan B.W. de Vries
faaf7912b1 Various cleanup to type system
- Make struct into a type constuctor
- Rework placeholders
- Got rid of 'PrimitiveType' as a concept
- Moved out the prelude to its own folder
2025-04-21 16:49:04 +02:00
d6b483581b Merge pull request 'Changes AppliedType to TypeConstructor' (#7) from replace-applied-type-by-type-constructors into master
Reviewed-on: #7
2025-04-21 09:22:35 +00:00
Johan B.W. de Vries
234bfaa8df Changes AppliedType to TypeConstructor
First to be more in line with how the literature
treats these types. But also to make them workable with
type classes.
2025-04-21 11:14:30 +02:00
Johan B.W. de Vries
87866cff55 More ideas 2025-04-09 16:02:47 +02:00
Johan B.W. de Vries
20c507a9ec Adds the Bits type class
Also adds the remaining unexposed WebAssembly opcodes as
comments (eqz, clz, ctz, popcnt, copysign).

This also means that BinaryOp.operator is now always
a type class method.
2025-04-09 15:40:20 +02:00
Johan B.W. de Vries
99e407c3c0 Adds arithmetic shift shift
"An arithmetic shift is usually equivalent to
multiplying the number by a positive or a negative
integral power of the radix." -- Federal Standard 1037C

"This is the same as multiplying x by 2**y."

"A right shift by n bits is defined as floor division
by pow(2,n). A left shift by n bits is defined as
multiplication with pow(2,n)."
2025-04-09 14:23:51 +02:00
Johan B.W. de Vries
da381e4a48 Made Integral use Python's operators
Rather than Haskell's div and rem methods, we use the
Python operators since these are often used. And we have
as goal to make it 'look like Python'
2025-04-09 13:16:50 +02:00
Johan B.W. de Vries
94c8f9388c Implements the Ord type class 2025-04-09 12:44:32 +02:00
Johan B.W. de Vries
46dbc90475 Implements ceil, floor, trunc, nearest
To round of the f32 / f64 wasm supported opcodes.

This also means we can remove the now outdated
WEBASSEMBLY_BUILTIN_FLOAT_OPS.
2025-04-06 16:38:57 +02:00
Johan B.W. de Vries
9bc8d94ffd Implements != 2025-04-06 16:20:01 +02:00
Johan B.W. de Vries
7544055a94 Split Num into NatNum and IntNum
This is because Haskell defines negate, abs and signum
for Num, but they don't work with our unsigned number
types. (abs would be a noop.) Haskell has Word32 / Word64,
but there negate doesn't make much sense to me.

Implemented neg and abs.

Implemented a type class inheritance check.

Removed Integral from u8 and i8 since it wasn't implemented.
2025-04-06 16:12:36 +02:00
Johan B.W. de Vries
74ab3b47fd Adds Floating type class with sqrt as method 2025-04-06 14:02:45 +02:00
Johan B.W. de Vries
111cb0f702 Review 2025-04-06 13:37:12 +02:00
Johan B.W. de Vries
be28450658 Converted fractional, fixed integral 2025-04-06 12:58:40 +02:00
Johan B.W. de Vries
4001b086db Cleanup, got rid of OPERATOR_MAP 2025-04-06 12:58:40 +02:00
Johan B.W. de Vries
19a29b7327 Migrated Num 2025-04-06 12:58:40 +02:00
Johan B.W. de Vries
ffd11c4f72 Started on a type class system 2025-04-06 12:58:40 +02:00
Johan B.W. de Vries
5d9ef0e276 Code review
Added a test, simplified another and added
a lot of TODO's.
2025-04-06 12:58:20 +02:00
Johan B.W. de Vries
521171540b Properly implemented test_crc32
Also extended the hack around u8 to u32 cast used
in crc32 - its type was lost, so it would just cast
to whatever the environment was expecting.

Had to do_format_check=False since the file has
some comments and such.
2025-04-05 16:41:46 +02:00
Johan B.W. de Vries
3e916a242e Minor fixes
- Fix python version in docs
- Made a note on which CRC32 we're using in the example
- Removed a redundant parameter
2025-04-05 16:19:26 +02:00
Johan B.W. de Vries
96f52a274c Minor cleanup
- Removed unused stubs
- Removed wabt dependency since wasmtime can compile
  wat2wasm as well.
- Removed wasm2c references, it was never tested.
2025-04-05 16:02:55 +02:00
Johan B.W. de Vries
5c537f712e Project update
Various updates to bring the project uptodate.

- Updated required packages
- Removed runtimes that are not being updated
- wasmtime is for now the only supported runtime
- Implements imports for wasmtime runtime
- Fixes a memory access bug for wasmtime runtime
- compile_wasm is now optional - runtimes have to
  implement and call this themselves
- Typing fixes
- Linting fixes
2025-04-05 15:43:49 +02:00
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
Johan B.W. de Vries
fd3939a680 Feat: Use struct in tuple 2023-11-10 15:35:16 +01:00
Johan B.W. de Vries
16ec664cb6 Feature: Tuples with tuples in constants 2023-11-10 15:23:10 +01:00
Johan B.W. de Vries
de92504394 Cleanup: DataBlock as part of the constants
As they can refer to other constants which are stored in
memory.
2023-11-10 15:02:07 +01:00
Johan B.W. de Vries
1536ea0bbb Enable use of bytes in constants
Including using them in tuple constants.
2023-11-10 14:54:22 +01:00
Johan B.W. de Vries
20d177e2c5 Add partial tuple of tuple support 2023-11-10 14:00:29 +01:00
Johan B.W. de Vries
a85129254d Adds partial additional debugging info 2023-11-10 12:52:14 +01:00
Johan B.W. de Vries
0aa8207987 Import service names 2023-11-07 10:51:55 +01:00
Johan B.W. de Vries
cea236494f Bytes constants 2023-11-07 10:51:43 +01:00
Johan B.W. de Vries
e6610a6e96 Adds a todo note 2023-01-29 12:42:38 +01:00
Johan B.W. de Vries
205897101f Adds a typing system to Phasm 2023-01-07 16:24:50 +01:00
129 changed files with 10616 additions and 3837 deletions

2
.gitignore vendored
View File

@ -3,4 +3,6 @@
/.coverage
/venv
/tests/integration/test_lang/test_generated_*.py
__pycache__

View File

@ -1,7 +1,5 @@
WABT_DIR := /home/johan/Sources/github.com/WebAssembly/wabt
WAT2WASM := $(WABT_DIR)/bin/wat2wasm
WASM2C := $(WABT_DIR)/bin/wasm2c
TEST_FILES := tests
WAT2WASM := venv/bin/python wat2wasm.py
%.wat: %.py $(shell find phasm -name '*.py') venv/.done
venv/bin/python -m phasm $< $@
@ -15,30 +13,38 @@ WASM2C := $(WABT_DIR)/bin/wasm2c
%.wasm: %.wat
$(WAT2WASM) $^ -o $@
%.c: %.wasm
$(WASM2C) $^ -o $@
# %.exe: %.c
# cc $^ -o $@ -I $(WABT_DIR)/wasm2c
examples: venv/.done $(subst .py,.wasm,$(wildcard examples/*.py)) $(subst .py,.wat.html,$(wildcard examples/*.py)) $(subst .py,.py.html,$(wildcard examples/*.py))
venv/bin/python3 -m http.server --directory examples
test: venv/.done
venv/bin/pytest tests $(TEST_FLAGS)
test: venv/.done $(subst .json,.py,$(subst /generator_,/test_generated_,$(wildcard tests/integration/test_lang/generator_*.json)))
venv/bin/pytest $(TEST_FILES) $(TEST_FLAGS)
lint: venv/.done
venv/bin/pylint phasm
venv/bin/ruff check phasm tests
typecheck: venv/.done
venv/bin/mypy --strict phasm tests/integration/runners.py
venv/bin/mypy --strict phasm wat2wasm.py tests/integration/helpers.py tests/integration/memory.py tests/integration/runners.py
venv/.done: requirements.txt
python3.8 -m venv venv
python3.12 -m venv venv
venv/bin/python3 -m pip install wheel pip --upgrade
venv/bin/python3 -m pip install -r $^
touch $@
tests/integration/test_lang/test_generated_%.py: venv/.done tests/integration/test_lang/generator.py tests/integration/test_lang/generator.md tests/integration/test_lang/generator_%.json
venv/bin/python3 tests/integration/test_lang/generator.py tests/integration/test_lang/generator.md tests/integration/test_lang/generator_$*.json > $@
clean-examples:
rm -f examples/*.wat examples/*.wasm examples/*.wat.html examples/*.py.html
clean-generated-tests:
rm -f tests/integration/test_lang/test_generated_*.py
.SECONDARY: # Keep intermediate files
.PHONY: examples
# So generally the right thing to do is to delete the target file if the recipe fails after beginning to change the file.
# make will do this if .DELETE_ON_ERROR appears as a target.
# This is almost always what you want make to do, but it is not historical practice; so for compatibility, you must explicitly request it.
.DELETE_ON_ERROR:

View File

@ -3,46 +3,14 @@ phasm
Elevator pitch
--------------
A programming language, that looks like Python, handles like Haskell,
A programming language that looks like Python, handles like Haskell,
and compiles directly to WebAssembly.
Project state
-------------
This is a hobby project for now. Use at your own risk.
How to run
----------
You should only need make and python3. Currently, we're working with python3.8,
since we're using the python ast parser, it might not work on other versions.
To run the examples:
```sh
make examples
```
To run the tests:
```sh
make test
```
To run the linting and type checking:
```sh
make lint typecheck
```
To compile a Phasm file:
```sh
python3.8 -m phasm source.py output.wat
```
Additional required tools
-------------------------
At the moment, the compiler outputs WebAssembly text format. To actually
get a binary, you will need the wat2wasm tool[6].
Example
-------
For more examples, see the examples directory.
From `examples/fib.py`:
```py
def helper(n: u64, a: u64, b: u64) -> u64:
if n < 1:
@ -61,6 +29,55 @@ def fib(n: u64) -> u64:
return helper(n - 1, 0, 1)
```
Compile to a WebAssembly text file:
```sh
python3 -m phasm examples/fib.py examples/fib.wat
```
Generate a WebAssembly binary file:
```sh
python wat2wasm.py examples/fib.wat -o examples/fib.wasm
```
Ready for including in your WebAssembly runtime!
Run `make examples` to start a local web server with some more examples. Each example has the source listed, as well as the compiled WebAssembly text.
Project state
-------------
This is a hobby project for now. Use at your own risk.
The parser, compiler and type checker are in a reasonably usable state.
What's still lacking is support for runtimes - notably, making it easier to get values in and out of the runtime.
For example, while Phasm supports a u32 type, when you get your value out, it will probably be a signed value.
And getting strings, structs, arrays and other combined values in and out requires manual work.
How to run
----------
You should only need make and python3. Currently, we're working with python3.12,
since we're using the python ast parser, it might not work on other versions.
To compile a Phasm file:
```sh
python3.12 -m phasm source.py output.wat
```
To run the examples:
```sh
make examples
```
To run the tests:
```sh
make test
```
To run the linting and type checking:
```sh
make lint typecheck
```
Gotcha's
--------
- When importing and exporting unsigned values to WebAssembly, they will become
@ -83,6 +100,9 @@ Also, if you are trying out Phasm, and you're running into a limitation, we're
interested in a minimal test case that shows what you want to achieve and how
Phasm currently fails you.
We're also investigating using WASI - but that's still ongoing research. If you
have tips or ideas on that, we'd be interested.
Name origin
-----------
- p from python
@ -96,4 +116,3 @@ References
[3] https://webassembly.org/
[4] https://www.w3.org/TR/wasm-core-1/
[5] https://en.wikipedia.org/w/index.php?title=WebAssembly&oldid=1103639883
[6] https://github.com/WebAssembly/wabt

23
TODO.md
View File

@ -1,14 +1,27 @@
# TODO
- Implement a proper type matching / checking system
- Implement subscript as an operator
- Re-implement Subscript contraints - Doing the LiteralFitsConstraint with a tuple doesn't put the types on the tuple elements
- Implement structs again, with the `.foo` notation working
- Decide between lineair types / uniqueness vs garbage collector
- https://borretti.me/article/how-australs-linear-type-checker-works
- Rename constant to literal
- Implement a trace() builtin for debugging
- Check if we can use DataView in the Javascript examples, e.g. with setUint32
- Storing u8 in memory still claims 32 bits (since that's what you need in local variables). However, using load8_u / loadu_s we can optimize this.
- Implement a FizzBuzz example
- Also, check the codes for FIXME and TODO
- Allocation is done using pointers for members, is this desired?
- See if we want to replace Fractional with Real, and add Rational, Irrationl, Algebraic, Transendental
- Implement q32? q64? Two i32/i64 divided?
- Have a set of rules or guidelines for the constraint comments, they're messy.
- calculate_alloc_size can be reworked; is_member isn't useful with TYPE_INFO_MAP
- Parser is putting stuff in ModuleDataBlock
- Surely the compiler should build data blocks
- Also put the struct.pack constants in TYPE_INFO_MAP
- Implemented Bounded: https://hackage.haskell.org/package/base-4.21.0.0/docs/Prelude.html#t:Bounded
- Try to implement the min and max functions using select
- Read https://bytecodealliance.org/articles/multi-value-all-the-wasm
- Implement type class 'inheritance'
- Rework type classes - already started on a separate dir for those, but quite a few things are still in other places.

View File

@ -1,51 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Examples - Buffer</title>
</head>
<body>
<h1>Buffer</h1>
<a href="index.html">List</a> - <a href="buffer.py.html">Source</a> - <a href="buffer.wat.html">WebAssembly</a>
<div style="white-space: pre;" id="results"></div>
<script type="text/javascript" src="./include.js"></script>
<script type="text/javascript">
let importObject = {};
// Run a single test
function run_test(app, str)
{
let offset = alloc_bytes(app, str);
let js_chars = [];
let wasm_chars = [];
for(let idx = 0; idx < str.length; ++idx) {
js_chars.push(str.charCodeAt(idx));
wasm_chars.push(app.instance.exports.index(offset, idx));
}
let result = js_chars.every(function(value, index) { return value === wasm_chars[index]})
test_result(result, {
'summary': 'js_chars == wasm_chars, for "' + str + '"',
'attributes': {
'js_chars': js_chars,
'wasm_chars': wasm_chars,
}
});
}
WebAssembly.instantiateStreaming(fetch('buffer.wasm'), importObject)
.then(app => {
run_test(app, '');
run_test(app, 'a');
run_test(app, 'Hello');
run_test(app, 'The quick brown fox jumps over the lazy dog');
});
</script>
</body>
</html>

View File

@ -1,7 +0,0 @@
@exported
def index(inp: bytes, idx: u32) -> u8:
return inp[idx]
@exported
def length(inp: bytes) -> i32:
return len(inp)

View File

@ -2,245 +2,68 @@
<html>
<head>
<title>Examples - CRC32</title>
<link rel="stylesheet" type="text/css" href="main.css">
</head>
<body>
<h1>Buffer</h1>
<h1>Examples - CRC32</h1>
<a href="index.html">List</a> - <a href="crc32.py.html">Source</a> - <a href="crc32.wat.html">WebAssembly</a><br />
<br />
Note: This tests performs some timing comparison, please wait a few seconds for the results.<br />
<div style="white-space: pre;" id="results"></div>
<div class="menu">
<a href="index.html">List</a> - <a href="crc32.py.html">Source</a> - <a href="crc32.wat.html">WebAssembly</a><br />
</div>
<h2>Measurement log</h2>
<h3>AMD Ryzen 7 3700X 8-Core, Ubuntu 20.04, Linux 5.4.0-124-generic</h3>
<h4>After optimizing fold over bytes by inlineing __subscript_bytes__</h4>
<table>
<tr>
<td>Test</td>
<td>Interpreter</td>
<td>Setup</td>
<td>WebAssembly</td>
<td>Javascript</td>
</tr>
<tr>
<td>Lynx * 65536</td>
<td>Chromium 104.0.5112.101</td>
<td>DevTools closed</td>
<td>5.70</td>
<td>12.45</td>
</tr>
<tr>
<td>Lynx * 65536</td>
<td>Firefox 103</td>
<td>DevTools closed</td>
<td>5.16</td>
<td>5.72</td>
</tr>
<tr>
<td>Lynx * 1048576</td>
<td>Chromium 104.0.5112.101</td>
<td>DevTools closed</td>
<td>95.65</td>
<td>203.60</td>
</tr>
<tr>
<td>Lynx * 1048576</td>
<td>Firefox 103</td>
<td>DevTools closed</td>
<td>83.34</td>
<td>92.38</td>
</tr>
</table>
<h4>Before optimizing fold over bytes by inlineing __subscript_bytes__</h4>
<table>
<tr>
<td>Test</td>
<td>Interpreter</td>
<td>Setup</td>
<td>WebAssembly</td>
<td>Javascript</td>
</tr>
<tr>
<td>Lynx * 65536</td>
<td>Chromium 104.0.5112.101</td>
<td>DevTools closed</td>
<td>9.35</td>
<td>12.56</td>
</tr>
<tr>
<td>Lynx * 65536</td>
<td>Chromium 104.0.5112.101</td>
<td>DevTools open</td>
<td>14.71</td>
<td>12.72</td>
</tr>
<tr>
<td>Lynx * 65536</td>
<td>Chromium 104.0.5112.101</td>
<td>Record page load</td>
<td>9.44</td>
<td>12.69</td>
</tr>
<tr>
<td>Lynx * 65536</td>
<td>Firefox 103</td>
<td>DevTools closed</td>
<td>9.02</td>
<td>5.86</td>
</tr>
<tr>
<td>Lynx * 65536</td>
<td>Firefox 103</td>
<td>DevTools open</td>
<td>9.01</td>
<td>5.83</td>
</tr>
<tr>
<td>Lynx * 65536</td>
<td>Firefox 103</td>
<td>Record page load</td>
<td>72.41</td>
<td>5.85</td>
</tr>
<div class="description">
<p>
This example shows a fold implementation of a <a href="https://en.wikipedia.org/wiki/Cyclic_redundancy_check">cyclic redundancy check</a>.
There are actually many variations of these CRCs - this one is specifically know as CRC-32/ISO-HDLC.
</p>
</div>
<tr>
<td>Lynx * 1048576</td>
<td>Chromium 104.0.5112.101</td>
<td>DevTools closed</td>
<td>149.24</td>
<td>202.36</td>
</tr>
<tr>
<td>Lynx * 1048576</td>
<td>Firefox 103</td>
<td>DevTools closed</td>
<td>145.01</td>
<td>91.44</td>
</tr>
</table>
<h3>Try it out!</h3>
<div class="example">
<textarea id="example-data" style="width: 75%; height: 4em;">The quick brown fox jumps over the lazy dog</textarea><br />
<button type="click" id="example-click" disabled>Calculate</button>
<input type="text" id="example-crc32" />
</div>
<h4>Notes</h4>
- Firefox seems faster than Chromium in my setup for Javascript, WebAssembly seems about the same.<br />
- Having DevTools open in Chromium seems to slow down the WebAssembly by about 30%, but not when doing a recording of the page load.<br />
- WebAssembly in Firefox seems to slow down when doing a recording of the page load, which makes sense, but the Javascript does not.<br />
<div class="example-list">
<ul>
<li><a href="#" data-n="123456789">crc32("123456789")</a> = cbf43926</li>
<li><a href="#" data-n="Hello, world!">crc32("Hello, world!")</a> = ebe6c6e6</li>
<li><a href="#" data-n="The quick brown fox jumps over the lazy dog">crc32("The quick brown fox jumps over the lazy dog")</a> = 414fa339</li>
<li><a href="#" data-n="CRC-32/ISO-HDLC
Also referred to as ISO 3309, ITU-T V.42, CRC-32-IEEE, and many other names.
<script type="text/javascript" src="./include.js"></script>
<script type="text/javascript">
let importObject = {};
The CRC of ASCII &quot;123456789&quot; is 0xcbf43926.
// Build up a JS version
var makeCRCTable = function(){
var c;
var crcTable = [];
for(var n =0; n < 256; n++){
c = n;
for(var k =0; k < 8; k++){
c = ((c&1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
}
crcTable[n] = c;
Examples of formats that use CRC-32/ISO-HDLC: ZIP, PNG, Gzip, ARJ.">crc32("CRC-32/ISO-HDLC...")</a> = 126afcf</li>
</ul>
</div>
<!-- We'll need to use some interface glue - WebAssembly doesn't let us pass strings directly. -->
<script type="text/javascript" src="./include.js"></script>
<script>
let importObject = {};
let exampleN = document.querySelector('#example-data');
let exampleClick = document.querySelector('#example-click');
let exampleCrc32 = document.querySelector('#example-crc32');
WebAssembly.instantiateStreaming(fetch('crc32.wasm'), importObject)
.then(app => {
exampleClick.addEventListener('click', event => {
let in_put = exampleN.value;
let in_put_addr = alloc_bytes(app, in_put);
let result = app.instance.exports.crc32(in_put_addr);
exampleCrc32.value = i32_to_u32(result).toString(16);
});
exampleClick.removeAttribute('disabled');
});
for(let exmpl of document.querySelectorAll('a[data-n]') ) {
exmpl.addEventListener('click', event => {
exampleN.value = exmpl.getAttribute('data-n');
exampleClick.click();
});
}
return crcTable;
}
window.crcTable = makeCRCTable();
var crc32_js = function(i8arr) {
// console.log('crc32_js', i8arr.length);
var crcTable = window.crcTable;
var crc = 0 ^ (-1);
for (var i = 0; i < i8arr.length; i++ ) {
crc = (crc >>> 8) ^ crcTable[(crc ^ i8arr[i]) & 0xFF];
}
return (crc ^ (-1)) >>> 0;
};
// Run a single test
function run_test(app, str, str_repeat)
{
// Cast to Uint32 in Javascript
let crc32_wasm = function(offset) {
// console.log('crc32_wasm', str.length);
return app.instance.exports.crc32(offset) >>> 0;
};
let orig_str = str;
if( str_repeat ) {
str = str.repeat(str_repeat);
} else {
str_repeat = 1;
}
let data = Uint8Array.from(str.split('').map(x => x.charCodeAt()));
offset = alloc_bytes(app, data);
let tweak = () => {
data[0] = data[0] + 1;
let i8arr = new Uint8Array(app.instance.exports.memory.buffer, offset + 4, data.length);
i8arr[0] = i8arr[0] + 1;
};
let tweak_reset = () => {
data[0] = 'T'.charCodeAt(0);
let i8arr = new Uint8Array(app.instance.exports.memory.buffer, offset + 4, data.length);
i8arr[0] = 'T'.charCodeAt(0);
};
// Run once to get the result
// For some reason, the JS version takes 2ms on the first run
// let wasm_result = crc32_wasm(offset);
// let js_result = crc32_js(data);
let wasm_timing = run_times(100, () => crc32_wasm(offset));
let js_timing = run_times(100, () => crc32_js(data));
let wasm_time = wasm_timing.avg;
let js_time = js_timing.avg;
let check = wasm_timing.values.every(function(value, index) {
return value.result === js_timing.values[index].result;
});
// Don't test speedup for small strings, it varies a lot
let speedup = (wasm_timing.min == 0 || js_timing.min == 0)
? 1
: js_time / wasm_time;
test_result(check && 0.999 < speedup, { // At least as fast as Javascript
'summary': 'crc32(' + (str
? (str.length < 64 ? '"' + str + '"' : '"' + str.substring(0, 64) + '..." (' + str.length + ')')
: '""') + ')',
'attributes': {
'str': orig_str,
'str_repeat': str_repeat,
'wasm_timing': wasm_timing,
'js_timing': js_timing,
'check': check,
'speedup': speedup,
},
});
}
// Load WebAssembly, and run all tests
WebAssembly.instantiateStreaming(fetch('crc32.wasm'), importObject)
.then(app => {
app.instance.exports.memory.grow(640);
run_test(app, "");
run_test(app, "a");
run_test(app, "Z");
run_test(app, "ab");
run_test(app, "abcdefghijklmnopqrstuvwxyz");
run_test(app, "The quick brown fox jumps over the lazy dog");
run_test(app, "The quick brown fox jumps over the lazy dog", 1024);
run_test(app, "Lynx c.q. vos prikt bh: dag zwemjuf!", 65536);
});
</script>
</script>
</body>
</html>

View File

@ -1,3 +1,5 @@
# CRC-32/ISO-HDLC
#
# #include <inttypes.h> // uint32_t, uint8_t
#
# uint32_t CRC32(const uint8_t data[], size_t data_length) {
@ -49,7 +51,7 @@ _CRC32_Table: u32[256] = (
)
def _crc32_f(crc: u32, byt: u8) -> u32:
return (crc >> 8) ^ _CRC32_Table[(crc & 0xFF) ^ u32(byt)]
return (crc >> 8) ^ _CRC32_Table[(crc & 0xFF) ^ extend(byt)]
@exported
def crc32(data: bytes) -> u32:

View File

@ -1,55 +1,62 @@
<!DOCTYPE html>
<html>
<head>
<title>Examples - Fibonacci</title>
<link rel="stylesheet" type="text/css" href="main.css">
</head>
<body>
<h1>Fibonacci</h1>
<h1>Examples - Fibonacci</h1>
<a href="index.html">List</a> - <a href="fib.py.html">Source</a> - <a href="fib.wat.html">WebAssembly</a>
<div class="menu">
<a href="index.html">List</a> - <a href="fib.py.html">Source</a> - <a href="fib.wat.html">WebAssembly</a>
</div>
<div style="white-space: pre;" id="results"></div>
<div class="description">
<p>
This example shows a recursive implementation of the <a href="https://en.wikipedia.org/wiki/Fibonacci_sequence">Fibonacci sequence</a>.
It makes uses of WebAssembly's support for <a href="https://en.wikipedia.org/wiki/Tail_call">tail calls</a>.
</p>
</div>
<h3>Try it out!</h3>
<div class="example">
<input type="number" id="example-n" value="25" />
<button type="click" id="example-click" disabled>Calculate</button>
<input type="number" id="example-fib" />
</div>
<script type="text/javascript" src="./include.js"></script>
<script type="text/javascript">
let importObject = {};
<div class="example-list">
<ul>
<li><a href="#" data-n="5">fib(5)</a> = 5</li>
<li><a href="#" data-n="10">fib(10)</a> = 55</li>
<li><a href="#" data-n="25">fib(25)</a> = 75025</li>
<li><a href="#" data-n="50">fib(50)</a> = 12586269025</li>
</ul>
</div>
function run_test(app, number, expected)
{
let actual = app.instance.exports.fib(BigInt(number));
console.log(actual);
<script>
let importObject = {};
let exampleN = document.querySelector('#example-n');
let exampleClick = document.querySelector('#example-click');
let exampleFib = document.querySelector('#example-fib');
test_result(BigInt(expected) == actual, {
'summary': 'fib(' + number + ')',
'attributes': {
'expected': expected,
'actual': actual
},
});
}
WebAssembly.instantiateStreaming(fetch('fib.wasm'), importObject)
.then(app => {
exampleClick.addEventListener('click', event => {
let in_put = exampleN.value;
let result = app.instance.exports.fib(BigInt(in_put));
exampleFib.value = result;
});
exampleClick.removeAttribute('disabled');
});
WebAssembly.instantiateStreaming(fetch('fib.wasm'), importObject)
.then(app => {
// 92: 7540113804746346429
// i64: 9223372036854775807
// 93: 12200160415121876738
run_test(app, 1, '1');
run_test(app, 2, '1');
run_test(app, 3, '2');
run_test(app, 4, '3');
run_test(app, 10, '55');
run_test(app, 20, '6765');
run_test(app, 30, '832040');
run_test(app, 40, '102334155');
run_test(app, 50, '12586269025');
run_test(app, 60, '1548008755920');
run_test(app, 70, '190392490709135');
run_test(app, 80, '23416728348467685');
run_test(app, 90, '2880067194370816120');
run_test(app, 92, '7540113804746346429');
});
</script>
for(let exmpl of document.querySelectorAll('a[data-n]') ) {
exmpl.addEventListener('click', event => {
exampleN.value = exmpl.getAttribute('data-n');
exampleClick.click();
});
}
</script>
</body>
</html>

View File

@ -13,7 +13,3 @@ def fib(n: u64) -> u64:
return 1
return helper(n - 1, 0, 1)
@exported
def testEntry() -> u64:
return fib(40)

View File

@ -1,48 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Examples - Fold</title>
</head>
<body>
<h1>Fold</h1>
<a href="index.html">List</a> - <a href="fold.py.html">Source</a> - <a href="fold.wat.html">WebAssembly</a>
<div style="white-space: pre;" id="results"></div>
<script type="text/javascript" src="./include.js"></script>
<script type="text/javascript">
let importObject = {};
function run_test(app, data, expected)
{
let offset = alloc_bytes(app, data);
let actual = app.instance.exports.foldl_u8_or_1(offset);
test_result(expected == actual, {
'summary': 'foldl(or, 1, [' + data + ']) == ' + expected,
'attributes': {
'data': data,
'expected': expected,
'offset': offset,
'actual': actual,
},
});
}
WebAssembly.instantiateStreaming(fetch('fold.wasm'), importObject)
.then(app => {
run_test(app, [], 1);
run_test(app, [0x20], 0x21);
run_test(app, [0x20, 0x10], 0x31);
run_test(app, [0x20, 0x10, 0x08], 0x39);
run_test(app, [0x20, 0x10, 0x08, 0x04], 0x3D);
run_test(app, [0x20, 0x10, 0x08, 0x04, 0x02], 0x3F);
run_test(app, [0x20, 0x10, 0x08, 0x04, 0x02, 0x01], 0x3F);
});
</script>
</body>
</html>

View File

@ -1,6 +0,0 @@
def u8_or(l: u8, r: u8) -> u8:
return l | r
@exported
def foldl_u8_or_1(b: bytes) -> u8:
return foldl(u8_or, 1, b)

View File

@ -1,60 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Examples - Imported</title>
</head>
<body>
<h1>Imported</h1>
<a href="index.html">List</a> - <a href="imported.py.html">Source</a> - <a href="imported.wat.html">WebAssembly</a>
<div style="white-space: pre;" id="results"></div>
<script type="text/javascript" src="./include.js"></script>
<script type="text/javascript">
let importObject = {
'imports': {
'log': log,
}
};
let log_result = [];
function log(inp)
{
log_result.push(inp);
}
function run_test(app, a, b)
{
log_result = [];
let expected = a * b;
app.instance.exports.mult_and_log(a, b);
let actual = log_result[0];
test_result(expected == actual, {
'summary': 'mult_and_log(' + a + ', ' + b + ') == ' + expected,
'attributes': {
'a': a,
'b': b,
'expected': expected,
'actual': actual,
},
});
}
WebAssembly.instantiateStreaming(fetch('imported.wasm'), importObject)
.then(app => {
run_test(app, 1, 1);
run_test(app, 3, 5);
run_test(app, 8, 19);
run_test(app, 12, 127);
run_test(app, 79, 193);
});
</script>
</body>
</html>

View File

@ -1,7 +0,0 @@
@imported
def log(no: i32) -> None:
pass
@exported
def mult_and_log(a: i32, b: i32) -> None:
return log(a * b)

View File

@ -1,3 +1,6 @@
/***
* Allocates the given string in the given application's memory
*/
function alloc_bytes(app, data)
{
let stdlib_types___alloc_bytes__ = app.instance.exports['stdlib.types.__alloc_bytes__']
@ -14,85 +17,12 @@ function alloc_bytes(app, data)
return offset;
}
function run_times(times, callback, tweak)
/**
* WebAssembly's interface only gets you signed integers
*
* Getting unsigned values out requires some work.
*/
function i32_to_u32(n)
{
let sum = 0;
let max = 0;
let min = 1000000000000000000;
let values = [];
for(let idx = 0; idx < times; idx += 1) {
if( tweak ) {
tweak();
}
const t0 = performance.now();
let result = callback();
const t1 = performance.now();
let time = t1 - t0;
sum += time;
values.push({'time': time, 'result': result});
max = max < time ? time : max;
min = min > time ? time : min;
}
return {
'min': min,
'avg': sum / times,
'max': max,
'sum': sum,
'values': values,
}
}
function test_result(is_pass, data)
{
data = data || {};
let result_details = document.createElement('details');
let result_summary = document.createElement('summary');
result_summary.textContent =
(is_pass ? 'Test passed: ' : 'Test failed: ')
+ (data.summary ?? '(no summary)')
;
result_summary.setAttribute('style', is_pass ? 'background: green' : 'background: red');
result_details.appendChild(result_summary);
if( data.attributes ) {
result_table(data, result_details);
}
let results = document.getElementById('results');
results.appendChild(result_details);
}
function result_table(attributes, parent)
{
let table = document.createElement('table');
Object.keys(attributes).forEach(idx => {
let td0 = document.createElement('td');
td0.setAttribute('style', 'vertical-align: top;');
td0.textContent = idx;
let td1 = document.createElement('td');
if( typeof(attributes[idx]) == 'object' ) {
let result_details = document.createElement('details');
let result_summary = document.createElement('summary');
result_summary.textContent = 'Show me';
result_details.appendChild(result_summary);
result_table(attributes[idx], result_details);
td1.appendChild(result_details);
} else {
td1.textContent = attributes[idx];
}
let tr = document.createElement('tr');
tr.appendChild(td0);
tr.appendChild(td1);
table.appendChild(tr);
});
parent.append(table);
return n >>> 0;
}

View File

@ -1,19 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<title>Examples</title>
<link rel="stylesheet" type="text/css" href="main.css">
</head>
<body>
<h1>Examples</h1>
<h2>Standard</h2>
<h2>Functions</h2>
<ul>
<li><a href="crc32.html">CRC32</a></li>
<li><a href="fib.html">Fibonacci</a></li>
</ul>
<h2>Technical</h2>
<ul>
<li><a href="buffer.html">Buffer</a></li>
<li><a href="fold.html">Folding</a></li>
<li><a href="imported.html">Imported</a></li>
</ul>
</body>
</html>

31
examples/main.css Normal file
View File

@ -0,0 +1,31 @@
:root {
/* CSS HEX */
--seasalt: #fcfafaff;
--silver: #c8d3d5ff;
--powder-blue: #a4b8c4ff;
--slate-gray: #6e8387ff;
--dark-pastel-green: #0cca4aff;
}
body {
background-color: var(--seasalt);
color: var(--slate-gray);
}
a {
color: var(--dark-pastel-green);
text-decoration: none;
font-weight: bold;
}
.menu {
border: 1px solid var(--powder-blue);
border-width: 1px 0px;
padding: 0.2em;
margin: 0.2em 0;
}
h3 {
border-top: 1px solid var(--powder-blue);
padding-top: 0.3em;
}

View File

@ -4,8 +4,11 @@ Functions for using this module from CLI
import sys
from .parser import phasm_parse
from .compiler import phasm_compile
from .optimise.removeunusedfuncs import removeunusedfuncs
from .parser import phasm_parse
from .type5.solver import phasm_type5
def main(source: str, sink: str) -> int:
"""
@ -16,7 +19,9 @@ def main(source: str, sink: str) -> int:
code_py = fil.read()
our_module = phasm_parse(code_py)
phasm_type5(our_module, verbose=False)
wasm_module = phasm_compile(our_module)
removeunusedfuncs(wasm_module)
code_wat = wasm_module.to_wat()
with open(sink, 'w') as fil:

445
phasm/build/base.py Normal file
View File

@ -0,0 +1,445 @@
"""
The base class for build environments.
Contains nothing but the explicit compiler builtins.
"""
from typing import NamedTuple, Protocol, Sequence, Type
from ..type5 import constrainedexpr as type5constrainedexpr
from ..type5 import kindexpr as type5kindexpr
from ..type5 import record as type5record
from ..type5 import typeexpr as type5typeexpr
from ..typeclass import TypeClass
from ..wasm import WasmType, WasmTypeInt32, WasmTypeNone
from .typeclassregistry import TypeClassRegistry
from .typerouter import TypeAllocSize, TypeName
TypeInfo = NamedTuple('TypeInfo', [
# Name of the type
('typ', str, ),
# What WebAssembly type to use when passing this value around
# For example in function arguments
('wasm_type', Type[WasmType]),
# What WebAssembly function to use when loading a value from memory
('wasm_load_func', str),
# What WebAssembly function to use when storing a value to memory
('wasm_store_func', str),
# When storing this value in memory, how many bytes do we use?
# Only valid for non-constructed types, see calculate_alloc_size
# Should match wasm_load_func / wasm_store_func
('alloc_size', int),
# When storing integers, the values can be stored as natural number
# (False) or as integer number (True). For other types, this is None.
('signed', bool | None),
])
class MissingImplementationWarning(Warning):
pass
class InternalImplementedMethodProtocol[G](Protocol):
def __call__(self, g: G, tv_map: dict[str, type5typeexpr.TypeExpr]) -> None:
pass
class BuildBase[G]:
__slots__ = (
'dynamic_array_type5_constructor',
'function_type5_constructor',
'static_array_type5_constructor',
'tuple_type5_constructor_map',
'none_type5',
'unit_type5',
'bool_type5',
'u8_type5',
'u32_type5',
'bytes_type5',
'type_info_map',
'type_info_constructed',
'types',
'type_classes',
'type_class_instances',
'methods',
'operators',
'type5_name',
'type5_alloc_size_root',
'type5_alloc_size_member',
)
dynamic_array_type5_constructor: type5typeexpr.TypeConstructor
"""
Constructor for arrays of runtime deterined length.
See type5_make_dynamic_array and type5_is_dynamic_array.
"""
function_type5_constructor: type5typeexpr.TypeConstructor
"""
Constructor for functions.
See type5_make_function and type5_is_function.
"""
static_array_type5_constructor: type5typeexpr.TypeConstructor
"""
Constructor for arrays of compiled time determined length.
See type5_make_static_array and type5_is_static_array.
"""
tuple_type5_constructor_map: dict[int, type5typeexpr.TypeConstructor]
"""
Map for constructors for tuples of each length.
See type5_make_tuple and type5_is_tuple.
"""
none_type5: type5typeexpr.AtomicType
"""
The none type.
TODO: Not sure this should be a buildin (rather than a Maybe type).
"""
unit_type5: type5typeexpr.AtomicType
"""
The unit type has exactly one value and can always be constructed.
Use for functions that don't take any arguments or do not produce any result.
This only make sense for IO functions.
TODO: Is this not what Python calls None?
"""
bool_type5: type5typeexpr.AtomicType
"""
The bool type, either True or False.
Builtin since functions require a boolean value in their test.
"""
u8_type5: type5typeexpr.AtomicType
"""
The u8 type, an integer value between 0 and 255.
Builtin since we can have bytes literals - which are the same as u8[...].
"""
u32_type5: type5typeexpr.AtomicType
"""
The u32 type, an integer value between 0 and 4 294 967 295.
Builtin since we can use this for indexing arrays and since
we use this for the length prefix on dynamic arrays.
"""
bytes_type5: type5typeexpr.TypeApplication
"""
The bytes type, a dynamic array with u8 elements.
Builtin since we can have bytes literals.
"""
type_info_map: dict[str, TypeInfo]
"""
Map from type name to the info of that type
"""
type_info_constructed: TypeInfo
"""
By default, constructed types are passed as pointers
NOTE: ALLOC SIZE IN THIS STRUCT DOES NOT WORK FOR CONSTRUCTED TYPES
USE calculate_alloc_size FOR ACCURATE RESULTS
Functions count as constructed types - even though they are
not memory pointers but table addresses instead.
"""
types: dict[str, type5typeexpr.TypeExpr]
"""
Types that are available without explicit import.
"""
type_classes: dict[str, TypeClass]
"""
Type classes that are available without explicit import.
"""
type_class_instances: dict[str, TypeClassRegistry[bool]]
"""
Type class instances that are available without explicit import.
"""
methods: dict[str, tuple[type5typeexpr.TypeExpr | type5constrainedexpr.ConstrainedExpr, TypeClassRegistry[InternalImplementedMethodProtocol[G]]]]
"""
Methods that are available without explicit import.
"""
operators: dict[str, tuple[type5typeexpr.TypeExpr | type5constrainedexpr.ConstrainedExpr, TypeClassRegistry[InternalImplementedMethodProtocol[G]]]]
"""
Operators that are available without explicit import.
"""
type5_name: TypeName
"""
Helper router to turn types into their human readable names.
"""
type5_alloc_size_root: TypeAllocSize
"""
Helper router to turn types into their allocation sizes.
This calculates the value when allocated directly.
"""
type5_alloc_size_member: TypeAllocSize
"""
Helper router to turn types into their allocation sizes.
This calculates the value when allocated as a member, e.g. in a tuple or struct.
"""
def __init__(self) -> None:
S = type5kindexpr.Star()
N = type5kindexpr.Nat()
self.dynamic_array_type5_constructor = type5typeexpr.TypeConstructor(kind=S >> S, name="dynamic_array")
self.function_type5_constructor = type5typeexpr.TypeConstructor(kind=S >> (S >> S), name="function")
self.static_array_type5_constructor = type5typeexpr.TypeConstructor(kind=N >> (S >> S), name='static_array')
self.tuple_type5_constructor_map = {}
self.none_type5 = type5typeexpr.AtomicType('None')
self.unit_type5 = type5typeexpr.AtomicType('()')
self.bool_type5 = type5typeexpr.AtomicType('bool')
self.u8_type5 = type5typeexpr.AtomicType('u8')
self.u32_type5 = type5typeexpr.AtomicType('u32')
self.bytes_type5 = self.type5_make_dynamic_array(self.u8_type5)
self.type_info_map = {
'None': TypeInfo('None', WasmTypeNone, 'unreachable', 'unreachable', 0, None),
'()': TypeInfo('()', WasmTypeNone, 'unreachable', 'unreachable', 0, None),
'bool': TypeInfo('bool', WasmTypeInt32, 'unreachable', 'unreachable', 0, None),
'u8': TypeInfo('u8', WasmTypeInt32, 'i32.load8_u', 'i32.store8', 1, False),
'u32': TypeInfo('u32', WasmTypeInt32, 'i32.load', 'i32.store', 4, False),
}
self.type_info_constructed = TypeInfo('ptr', WasmTypeInt32, 'i32.load', 'i32.store', 4, False)
self.types = {
'None': self.none_type5,
'()': self.unit_type5,
'bool': self.bool_type5,
'u8': self.u8_type5,
'u32': self.u32_type5,
'bytes': self.bytes_type5,
}
self.type_classes = {}
self.type_class_instances = {}
self.methods = {}
self.operators = {}
self.type5_name = TypeName(self)
self.type5_alloc_size_root = TypeAllocSize(self, is_member=False)
self.type5_alloc_size_member = TypeAllocSize(self, is_member=True)
def register_type_class(self, cls: TypeClass) -> None:
assert cls.name not in self.type_classes, 'Duplicate typeclass name'
self.type_classes[cls.name] = cls
self.type_class_instances[cls.name] = TypeClassRegistry()
for mtd_nam, mtd_typ in cls.methods.items():
assert mtd_nam not in self.methods, 'Duplicate typeclass method name'
self.methods[mtd_nam] = (mtd_typ, TypeClassRegistry(), )
for opr_nam, opr_typ in cls.operators.items():
assert opr_nam not in self.operators, 'Duplicate typeclass operator name'
self.operators[opr_nam] = (opr_typ, TypeClassRegistry(), )
def instance_type_class(
self,
cls: TypeClass,
*args: type5typeexpr.TypeExpr,
methods: dict[str, InternalImplementedMethodProtocol[G]] = {},
operators: dict[str, InternalImplementedMethodProtocol[G]] = {},
) -> None:
self.type_class_instances[cls.name].add(args, True)
assert len(cls.variables) == len(args)
for mtd_nam, mtd_imp in methods.items():
mtd_typ, mtd_rtr = self.methods[mtd_nam]
if isinstance(mtd_typ, type5constrainedexpr.ConstrainedExpr):
mtd_typ = mtd_typ.expr
for var, rep_expr in zip(cls.variables, args, strict=True):
mtd_typ = type5typeexpr.replace_variable(mtd_typ, var, rep_expr)
mtd_rtr.add((mtd_typ, ), mtd_imp)
for opr_nam, opr_imp in operators.items():
mtd_typ, opr_rtr = self.operators[opr_nam]
if isinstance(mtd_typ, type5constrainedexpr.ConstrainedExpr):
mtd_typ = mtd_typ.expr
for var, rep_expr in zip(cls.variables, args, strict=True):
mtd_typ = type5typeexpr.replace_variable(mtd_typ, var, rep_expr)
opr_rtr.add((mtd_typ, ), opr_imp)
def type5_make_function(self, args: Sequence[type5typeexpr.TypeExpr]) -> type5typeexpr.TypeExpr:
if not args:
raise TypeError("Functions must at least have a return type")
if len(args) == 1:
# Functions always take an argument
# To distinguish between a function without arguments and a value
# of the type, we have a unit type
# This type has one value so it can always be called
args = [self.unit_type5, *args]
res_type5 = None
for arg_type5 in reversed(args):
if res_type5 is None:
res_type5 = arg_type5
continue
res_type5 = type5typeexpr.TypeApplication(
constructor=type5typeexpr.TypeApplication(
constructor=self.function_type5_constructor,
argument=arg_type5,
),
argument=res_type5,
)
assert res_type5 is not None # type hint
return res_type5
def type5_is_function(self, typeexpr: type5typeexpr.TypeExpr | type5constrainedexpr.ConstrainedExpr) -> list[type5typeexpr.TypeExpr] | None:
if isinstance(typeexpr, type5constrainedexpr.ConstrainedExpr):
typeexpr = typeexpr.expr
if not isinstance(typeexpr, type5typeexpr.TypeApplication):
return None
if not isinstance(typeexpr.constructor, type5typeexpr.TypeApplication):
return None
if typeexpr.constructor.constructor != self.function_type5_constructor:
return None
arg0 = typeexpr.constructor.argument
if arg0 is self.unit_type5:
my_args = []
else:
my_args = [arg0]
arg1 = typeexpr.argument
more_args = self.type5_is_function(arg1)
if more_args is None:
return my_args + [arg1]
return my_args + more_args
def type5_make_tuple(self, args: Sequence[type5typeexpr.TypeExpr]) -> type5typeexpr.TypeApplication:
if not args:
raise TypeError("Tuples must at least one field")
arlen = len(args)
constructor = self.tuple_type5_constructor_map.get(arlen)
if constructor is None:
star = type5kindexpr.Star()
kind: type5kindexpr.Arrow = star >> star
for _ in range(len(args) - 1):
kind = star >> kind
constructor = type5typeexpr.TypeConstructor(kind=kind, name=f'tuple_{arlen}')
self.tuple_type5_constructor_map[arlen] = constructor
result: type5typeexpr.TypeApplication | None = None
for arg in args:
if result is None:
result = type5typeexpr.TypeApplication(
constructor=constructor,
argument=arg
)
continue
result = type5typeexpr.TypeApplication(
constructor=result,
argument=arg
)
assert result is not None # type hint
return result
def type5_is_tuple(self, typeexpr: type5typeexpr.TypeExpr) -> list[type5typeexpr.TypeExpr] | None:
arg_list = []
while isinstance(typeexpr, type5typeexpr.TypeApplication):
arg_list.append(typeexpr.argument)
typeexpr = typeexpr.constructor
if not isinstance(typeexpr, type5typeexpr.TypeConstructor):
return None
if typeexpr not in self.tuple_type5_constructor_map.values():
return None
return list(reversed(arg_list))
def type5_make_struct(self, name: str, fields: tuple[tuple[str, type5typeexpr.AtomicType | type5typeexpr.TypeApplication], ...]) -> type5record.Record:
return type5record.Record(name, fields)
def type5_is_struct(self, arg: type5typeexpr.TypeExpr) -> tuple[tuple[str, type5typeexpr.AtomicType | type5typeexpr.TypeApplication], ...] | None:
if not isinstance(arg, type5record.Record):
return None
return arg.fields
def type5_make_dynamic_array(self, arg: type5typeexpr.TypeExpr) -> type5typeexpr.TypeApplication:
return type5typeexpr.TypeApplication(
constructor=self.dynamic_array_type5_constructor,
argument=arg,
)
def type5_is_dynamic_array(self, typeexpr: type5typeexpr.TypeExpr) -> type5typeexpr.TypeExpr | None:
"""
Check if the given type expr is a concrete dynamic array type.
The element argument type is returned if so. Else, None is returned.
"""
if not isinstance(typeexpr, type5typeexpr.TypeApplication):
return None
if typeexpr.constructor != self.dynamic_array_type5_constructor:
return None
return typeexpr.argument
def type5_make_static_array(self, len: int, arg: type5typeexpr.TypeExpr) -> type5typeexpr.TypeApplication:
return type5typeexpr.TypeApplication(
constructor=type5typeexpr.TypeApplication(
constructor=self.static_array_type5_constructor,
argument=type5typeexpr.TypeLevelNat(len),
),
argument=arg,
)
def type5_is_static_array(self, typeexpr: type5typeexpr.TypeExpr) -> tuple[int, type5typeexpr.TypeExpr] | None:
if not isinstance(typeexpr, type5typeexpr.TypeApplication):
return None
if not isinstance(typeexpr.constructor, type5typeexpr.TypeApplication):
return None
if typeexpr.constructor.constructor != self.static_array_type5_constructor:
return None
assert isinstance(typeexpr.constructor.argument, type5typeexpr.TypeLevelNat) # type hint
return (
typeexpr.constructor.argument.value,
typeexpr.argument,
)

77
phasm/build/default.py Normal file
View File

@ -0,0 +1,77 @@
"""
The default class for build environments.
Contains the compiler builtins as well as some sane defaults.
"""
from ..type5 import typeexpr as type5typeexpr
from ..wasm import (
WasmTypeFloat32,
WasmTypeFloat64,
WasmTypeInt32,
WasmTypeInt64,
)
from ..wasmgenerator import Generator
from .base import BuildBase, TypeInfo
from .typeclasses import (
bits,
convertable,
eq,
extendable,
floating,
foldable,
fractional,
integral,
intnum,
natnum,
ord,
promotable,
reinterpretable,
sized,
subscriptable,
)
class BuildDefault(BuildBase[Generator]):
__slots__ = ()
def __init__(self) -> None:
super().__init__()
self.type_info_map.update({
'u16': TypeInfo('u16', WasmTypeInt32, 'i32.load16_u', 'i32.store16', 2, False),
'u64': TypeInfo('u64', WasmTypeInt64, 'i64.load', 'i64.store', 8, False),
'i8': TypeInfo('i8', WasmTypeInt32, 'i32.load8_s', 'i32.store8', 1, True),
'i16': TypeInfo('i16', WasmTypeInt32, 'i32.load16_s', 'i32.store16', 2, True),
'i32': TypeInfo('i32', WasmTypeInt32, 'i32.load', 'i32.store', 4, True),
'i64': TypeInfo('i64', WasmTypeInt64, 'i64.load', 'i64.store', 8, True),
'f32': TypeInfo('f32', WasmTypeFloat32, 'f32.load', 'f32.store', 4, None),
'f64': TypeInfo('f64', WasmTypeFloat64, 'f64.load', 'f64.store', 8, None),
})
self.types.update({
'u16': type5typeexpr.AtomicType('u16'),
'u64': type5typeexpr.AtomicType('u64'),
'i8': type5typeexpr.AtomicType('i8'),
'i16': type5typeexpr.AtomicType('i16'),
'i32': type5typeexpr.AtomicType('i32'),
'i64': type5typeexpr.AtomicType('i64'),
'f32': type5typeexpr.AtomicType('f32'),
'f64': type5typeexpr.AtomicType('f64'),
})
tc_list = [
bits,
eq, ord,
extendable, promotable,
convertable, reinterpretable,
natnum, intnum, fractional, floating,
integral,
foldable, subscriptable,
sized,
]
for tc in tc_list:
tc.load(self)
for tc in tc_list:
tc.wasm(self)

View File

View File

@ -0,0 +1,214 @@
"""
The Bits type class is defined for types that can be bit manipulated.
"""
from __future__ import annotations
from typing import Any
from ...type5.constrainedexpr import ConstrainedExpr
from ...type5.kindexpr import Star
from ...type5.typeexpr import TypeVariable
from ...typeclass import TypeClass, TypeClassConstraint
from ...wasmgenerator import Generator as WasmGenerator
from ..base import BuildBase
def load(build: BuildBase[Any]) -> None:
a = TypeVariable(kind=Star(), name='a')
u32 = build.types['u32']
Bits = TypeClass('Bits', (a, ), methods={}, operators={})
has_bits_a = TypeClassConstraint(Bits, [a])
fn_a_u32_a = ConstrainedExpr(
variables={a},
expr=build.type5_make_function([a, u32, a]),
constraints=(has_bits_a, ),
)
fn_a_a_a = ConstrainedExpr(
variables={a},
expr=build.type5_make_function([a, a, a]),
constraints=(has_bits_a, ),
)
Bits.methods = {
'shl': fn_a_u32_a, # Logical shift left
'shr': fn_a_u32_a, # Logical shift right
'rotl': fn_a_u32_a, # Rotate bits left
'rotr': fn_a_u32_a, # Rotate bits right
# FIXME: Do we want to expose clz, ctz, popcnt?
}
Bits.operators = {
'&': fn_a_a_a, # Bit-wise and
'|': fn_a_a_a, # Bit-wise or
'^': fn_a_a_a, # Bit-wise xor
}
build.register_type_class(Bits)
def wasm_u8_logical_shift_left(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.shl()
g.i32.const(0xFF)
g.i32.and_()
def wasm_u16_logical_shift_left(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.shl()
g.i32.const(0xFFFF)
g.i32.and_()
def wasm_u32_logical_shift_left(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.shl()
def wasm_u64_logical_shift_left(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.extend_i32_u()
g.i64.shl()
def wasm_u8_logical_shift_right(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.shr_u()
def wasm_u16_logical_shift_right(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.shr_u()
def wasm_u32_logical_shift_right(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.shr_u()
def wasm_u64_logical_shift_right(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.extend_i32_u()
g.i64.shr_u()
def wasm_u8_rotate_left(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__u8_rotl__')
def wasm_u16_rotate_left(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__u16_rotl__')
def wasm_u32_rotate_left(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.rotl()
def wasm_u64_rotate_left(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.extend_i32_u()
g.i64.rotl()
def wasm_u8_rotate_right(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__u8_rotr__')
def wasm_u16_rotate_right(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__u16_rotr__')
def wasm_u32_rotate_right(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.rotr()
def wasm_u64_rotate_right(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.extend_i32_u()
g.i64.rotr()
def wasm_u8_bitwise_and(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.and_()
def wasm_u16_bitwise_and(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.and_()
def wasm_u32_bitwise_and(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.and_()
def wasm_u64_bitwise_and(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.and_()
def wasm_u8_bitwise_or(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.or_()
def wasm_u16_bitwise_or(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.or_()
def wasm_u32_bitwise_or(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.or_()
def wasm_u64_bitwise_or(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.or_()
def wasm_u8_bitwise_xor(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.xor()
def wasm_u16_bitwise_xor(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.xor()
def wasm_u32_bitwise_xor(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.xor()
def wasm_u64_bitwise_xor(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.xor()
def wasm(build: BuildBase[WasmGenerator]) -> None:
Bits = build.type_classes['Bits']
build.instance_type_class(Bits, build.types['u8'], methods={
'shl': wasm_u8_logical_shift_left,
'shr': wasm_u8_logical_shift_right,
'rotl': wasm_u8_rotate_left,
'rotr': wasm_u8_rotate_right,
}, operators={
'&': wasm_u8_bitwise_and,
'|': wasm_u8_bitwise_or,
'^': wasm_u8_bitwise_xor,
})
build.instance_type_class(Bits, build.types['u16'], methods={
'shl': wasm_u16_logical_shift_left,
'shr': wasm_u16_logical_shift_right,
'rotl': wasm_u16_rotate_left,
'rotr': wasm_u16_rotate_right,
}, operators={
'&': wasm_u16_bitwise_and,
'|': wasm_u16_bitwise_or,
'^': wasm_u16_bitwise_xor,
})
build.instance_type_class(Bits, build.types['u32'], methods={
'shl': wasm_u32_logical_shift_left,
'shr': wasm_u32_logical_shift_right,
'rotl': wasm_u32_rotate_left,
'rotr': wasm_u32_rotate_right,
}, operators={
'&': wasm_u32_bitwise_and,
'|': wasm_u32_bitwise_or,
'^': wasm_u32_bitwise_xor,
})
build.instance_type_class(Bits, build.types['u64'], methods={
'shl': wasm_u64_logical_shift_left,
'shr': wasm_u64_logical_shift_right,
'rotl': wasm_u64_rotate_left,
'rotr': wasm_u64_rotate_right,
}, operators={
'&': wasm_u64_bitwise_and,
'|': wasm_u64_bitwise_or,
'^': wasm_u64_bitwise_xor,
})

View File

@ -0,0 +1,143 @@
"""
The Convertable type class is defined for when a value from one type can be
converted to another type - but there's no real guarantee about precision or
value loss.
"""
from __future__ import annotations
from typing import Any
from ...type5.constrainedexpr import ConstrainedExpr
from ...type5.kindexpr import Star
from ...type5.typeexpr import TypeVariable
from ...typeclass import TypeClass, TypeClassConstraint
from ...wasmgenerator import Generator as WasmGenerator
from ..base import BuildBase
def load(build: BuildBase[Any]) -> None:
a = TypeVariable(kind=Star(), name='a')
b = TypeVariable(kind=Star(), name='b')
Convertable = TypeClass('Convertable', (a, b, ), methods={}, operators={})
has_convertable_a_b = TypeClassConstraint(Convertable, [a, b])
fn_a_b = ConstrainedExpr(
variables={a, b},
expr=build.type5_make_function([a, b]),
constraints=(has_convertable_a_b, ),
)
fn_b_a = ConstrainedExpr(
variables={a, b},
expr=build.type5_make_function([b, a]),
constraints=(has_convertable_a_b, ),
)
Convertable.methods = {
'convert': fn_a_b,
'truncate': fn_b_a, # To prevent name clas with Fractional
}
build.register_type_class(Convertable)
def wasm_u32_f32_convert(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f32.convert_i32_u()
def wasm_u32_f64_convert(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f64.convert_i32_u()
def wasm_u64_f32_convert(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f32.convert_i64_u()
def wasm_u64_f64_convert(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f64.convert_i64_u()
def wasm_i32_f32_convert(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f32.convert_i32_s()
def wasm_i32_f64_convert(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f64.convert_i32_s()
def wasm_i64_f32_convert(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f32.convert_i64_s()
def wasm_i64_f64_convert(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f64.convert_i64_s()
def wasm_u32_f32_truncate(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.trunc_f32_u()
def wasm_u32_f64_truncate(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.trunc_f64_u()
def wasm_u64_f32_truncate(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.trunc_f32_u()
def wasm_u64_f64_truncate(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.trunc_f64_u()
def wasm_i32_f32_truncate(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.trunc_f32_s()
def wasm_i32_f64_truncate(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.trunc_f64_s()
def wasm_i64_f32_truncate(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.trunc_f32_s()
def wasm_i64_f64_truncate(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.trunc_f64_s()
def wasm(build: BuildBase[WasmGenerator]) -> None:
Convertable = build.type_classes['Convertable']
build.instance_type_class(Convertable, build.types['u32'], build.types['f32'], methods={
'convert': wasm_u32_f32_convert,
'truncate': wasm_u32_f32_truncate,
})
build.instance_type_class(Convertable, build.types['u32'], build.types['f64'], methods={
'convert': wasm_u32_f64_convert,
'truncate': wasm_u32_f64_truncate,
})
build.instance_type_class(Convertable, build.types['u64'], build.types['f32'], methods={
'convert': wasm_u64_f32_convert,
'truncate': wasm_u64_f32_truncate,
})
build.instance_type_class(Convertable, build.types['u64'], build.types['f64'], methods={
'convert': wasm_u64_f64_convert,
'truncate': wasm_u64_f64_truncate,
})
build.instance_type_class(Convertable, build.types['i32'], build.types['f32'], methods={
'convert': wasm_i32_f32_convert,
'truncate': wasm_i32_f32_truncate,
})
build.instance_type_class(Convertable, build.types['i32'], build.types['f64'], methods={
'convert': wasm_i32_f64_convert,
'truncate': wasm_i32_f64_truncate,
})
build.instance_type_class(Convertable, build.types['i64'], build.types['f32'], methods={
'convert': wasm_i64_f32_convert,
'truncate': wasm_i64_f32_truncate,
})
build.instance_type_class(Convertable, build.types['i64'], build.types['f64'], methods={
'convert': wasm_i64_f64_convert,
'truncate': wasm_i64_f64_truncate,
})

View File

@ -0,0 +1,159 @@
"""
The Eq type class is defined for types that can be compered based on equality.
"""
from __future__ import annotations
from typing import Any
from ...type5.constrainedexpr import ConstrainedExpr
from ...type5.kindexpr import Star
from ...type5.typeexpr import TypeVariable
from ...typeclass import TypeClass, TypeClassConstraint
from ...wasmgenerator import Generator as WasmGenerator
from ..base import BuildBase
def load(build: BuildBase[Any]) -> None:
a = TypeVariable(kind=Star(), name='a')
Eq = TypeClass('Eq', (a, ), methods={}, operators={})
has_eq_a = TypeClassConstraint(Eq, [a])
fn_a_a_bool = ConstrainedExpr(
variables={a},
expr=build.type5_make_function([a, a, build.bool_type5]),
constraints=(has_eq_a, ),
)
Eq.operators = {
'==': fn_a_a_bool,
'!=': fn_a_a_bool,
# FIXME: Do we want to expose 'eqz'? Or is that a compiler optimization?
}
build.register_type_class(Eq)
def wasm_u8_equals(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.eq()
def wasm_u16_equals(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.eq()
def wasm_u32_equals(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.eq()
def wasm_u64_equals(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.eq()
def wasm_i8_equals(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.eq()
def wasm_i16_equals(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.eq()
def wasm_i32_equals(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.eq()
def wasm_i64_equals(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.eq()
def wasm_f32_equals(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f32.eq()
def wasm_f64_equals(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f64.eq()
def wasm_u8_not_equals(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.ne()
def wasm_u16_not_equals(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.ne()
def wasm_u32_not_equals(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.ne()
def wasm_u64_not_equals(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.ne()
def wasm_i8_not_equals(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.ne()
def wasm_i16_not_equals(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.ne()
def wasm_i32_not_equals(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.ne()
def wasm_i64_not_equals(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.ne()
def wasm_f32_not_equals(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f32.ne()
def wasm_f64_not_equals(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f64.ne()
def wasm(build: BuildBase[WasmGenerator]) -> None:
Eq = build.type_classes['Eq']
build.instance_type_class(Eq, build.types['u8'], operators={
'==': wasm_u8_equals,
'!=': wasm_u8_not_equals,
})
build.instance_type_class(Eq, build.types['u16'], operators={
'==': wasm_u16_equals,
'!=': wasm_u16_not_equals,
})
build.instance_type_class(Eq, build.types['u32'], operators={
'==': wasm_u32_equals,
'!=': wasm_u32_not_equals,
})
build.instance_type_class(Eq, build.types['u64'], operators={
'==': wasm_u64_equals,
'!=': wasm_u64_not_equals,
})
build.instance_type_class(Eq, build.types['i8'], operators={
'==': wasm_i8_equals,
'!=': wasm_i8_not_equals,
})
build.instance_type_class(Eq, build.types['i16'], operators={
'==': wasm_i16_equals,
'!=': wasm_i16_not_equals,
})
build.instance_type_class(Eq, build.types['i32'], operators={
'==': wasm_i32_equals,
'!=': wasm_i32_not_equals,
})
build.instance_type_class(Eq, build.types['i64'], operators={
'==': wasm_i64_equals,
'!=': wasm_i64_not_equals,
})
build.instance_type_class(Eq, build.types['f32'], operators={
'==': wasm_f32_equals,
'!=': wasm_f32_not_equals,
})
build.instance_type_class(Eq, build.types['f64'], operators={
'==': wasm_f64_equals,
'!=': wasm_f64_not_equals,
})

View File

@ -0,0 +1,216 @@
"""
The Extendable type class is defined for types that can safely be extended to a type
that can hold strictly more values. Going back will result in some values being lost.
"""
from __future__ import annotations
from typing import Any
from ...type5.constrainedexpr import ConstrainedExpr
from ...type5.kindexpr import Star
from ...type5.typeexpr import TypeVariable
from ...typeclass import TypeClass, TypeClassConstraint
from ...wasmgenerator import Generator as WasmGenerator
from ..base import BuildBase
def load(build: BuildBase[Any]) -> None:
a = TypeVariable(kind=Star(), name='a')
b = TypeVariable(kind=Star(), name='b')
Extendable = TypeClass('Extendable', (a, b, ), methods={}, operators={})
has_extendable_a_b = TypeClassConstraint(Extendable, [a, b])
fn_a_b = ConstrainedExpr(
variables={a, b},
expr=build.type5_make_function([a, b]),
constraints=(has_extendable_a_b, ),
)
fn_b_a = ConstrainedExpr(
variables={a, b},
expr=build.type5_make_function([b, a]),
constraints=(has_extendable_a_b, ),
)
Extendable.methods = {
'extend': fn_a_b,
'wrap': fn_b_a,
}
build.register_type_class(Extendable)
def wasm_u8_u16_extend(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
# No-op
# u8 and u16 are both stored as u32
pass
def wasm_u8_u32_extend(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
# No-op
# u8 is already stored as u32
pass
def wasm_u8_u64_extend(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.extend_i32_u()
def wasm_u16_u32_extend(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
# No-op
# u16 is already stored as u32
pass
def wasm_u16_u64_extend(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.extend_i32_u()
def wasm_u32_u64_extend(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.extend_i32_u()
def wasm_i8_i16_extend(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
# No-op
# i8 is already stored as i32
pass
def wasm_i8_i32_extend(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
# No-op
# i8 is already stored as i32
pass
def wasm_i8_i64_extend(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.extend_i32_s()
def wasm_i16_i32_extend(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
# No-op
# i16 is already stored as i32
pass
def wasm_i16_i64_extend(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.extend_i32_s()
def wasm_i32_i64_extend(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.extend_i32_s()
def wasm_u8_u16_wrap(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.const(0xFF)
g.i32.and_()
def wasm_u8_u32_wrap(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.const(0xFF)
g.i32.and_()
def wasm_u8_u64_wrap(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.wrap_i64()
g.i32.const(0xFF)
g.i32.and_()
def wasm_u16_u32_wrap(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.const(0xFFFF)
g.i32.and_()
def wasm_u16_u64_wrap(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.wrap_i64()
g.i32.const(0xFFFF)
g.i32.and_()
def wasm_u32_u64_wrap(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.wrap_i64()
def wasm_i8_i16_wrap(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.const(0xFF)
g.i32.and_()
def wasm_i8_i32_wrap(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.const(0xFF)
g.i32.and_()
def wasm_i8_i64_wrap(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.wrap_i64()
g.i32.const(0xFF)
g.i32.and_()
def wasm_i16_i32_wrap(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.const(0xFFFF)
g.i32.and_()
def wasm_i16_i64_wrap(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.wrap_i64()
g.i32.const(0xFFFF)
g.i32.and_()
def wasm_i32_i64_wrap(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.wrap_i64()
def wasm(build: BuildBase[WasmGenerator]) -> None:
Extendable = build.type_classes['Extendable']
build.instance_type_class(Extendable, build.types['u8'], build.types['u16'], methods={
'extend': wasm_u8_u16_extend,
'wrap': wasm_u8_u16_wrap,
})
build.instance_type_class(Extendable, build.types['u8'], build.types['u32'], methods={
'extend': wasm_u8_u32_extend,
'wrap': wasm_u8_u32_wrap,
})
build.instance_type_class(Extendable, build.types['u8'], build.types['u64'], methods={
'extend': wasm_u8_u64_extend,
'wrap': wasm_u8_u64_wrap,
})
build.instance_type_class(Extendable, build.types['u16'], build.types['u32'], methods={
'extend': wasm_u16_u32_extend,
'wrap': wasm_u16_u32_wrap,
})
build.instance_type_class(Extendable, build.types['u16'], build.types['u64'], methods={
'extend': wasm_u16_u64_extend,
'wrap': wasm_u16_u64_wrap,
})
build.instance_type_class(Extendable, build.types['u32'], build.types['u64'], methods={
'extend': wasm_u32_u64_extend,
'wrap': wasm_u32_u64_wrap,
})
build.instance_type_class(Extendable, build.types['i8'], build.types['i16'], methods={
'extend': wasm_i8_i16_extend,
'wrap': wasm_i8_i16_wrap,
})
build.instance_type_class(Extendable, build.types['i8'], build.types['i32'], methods={
'extend': wasm_i8_i32_extend,
'wrap': wasm_i8_i32_wrap,
})
build.instance_type_class(Extendable, build.types['i8'], build.types['i64'], methods={
'extend': wasm_i8_i64_extend,
'wrap': wasm_i8_i64_wrap,
})
build.instance_type_class(Extendable, build.types['i16'], build.types['i32'], methods={
'extend': wasm_i16_i32_extend,
'wrap': wasm_i16_i32_wrap,
})
build.instance_type_class(Extendable, build.types['i16'], build.types['i64'], methods={
'extend': wasm_i16_i64_extend,
'wrap': wasm_i16_i64_wrap,
})
build.instance_type_class(Extendable, build.types['i32'], build.types['i64'], methods={
'extend': wasm_i32_i64_extend,
'wrap': wasm_i32_i64_wrap,
})

View File

@ -0,0 +1,54 @@
"""
The Floating type class is defined for Real numbers.
"""
from __future__ import annotations
from typing import Any
from ...type5.constrainedexpr import ConstrainedExpr
from ...type5.kindexpr import Star
from ...type5.typeexpr import TypeVariable
from ...typeclass import TypeClass, TypeClassConstraint
from ...wasmgenerator import Generator as WasmGenerator
from ..base import BuildBase
def load(build: BuildBase[Any]) -> None:
a = TypeVariable(kind=Star(), name='a')
Floating = TypeClass('Floating', (a, ), methods={}, operators={})
has_floating_a = TypeClassConstraint(Floating, [a])
fn_a_a = ConstrainedExpr(
variables={a},
expr=build.type5_make_function([a, a]),
constraints=(has_floating_a, ),
)
Floating.methods = {
'sqrt': fn_a_a
}
# FIXME: inherited_classes=[Fractional]
# FIXME: Do we want to expose copysign?
build.register_type_class(Floating)
def wasm_f32_sqrt(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('f32.sqrt')
def wasm_f64_sqrt(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('f64.sqrt')
def wasm(build: BuildBase[WasmGenerator]) -> None:
Floating = build.type_classes['Floating']
build.instance_type_class(Floating, build.types['f32'], methods={
'sqrt': wasm_f32_sqrt,
})
build.instance_type_class(Floating, build.types['f64'], methods={
'sqrt': wasm_f64_sqrt,
})

View File

@ -0,0 +1,643 @@
"""
The Foldable type class is defined for when a value iterated over.
"""
from __future__ import annotations
from typing import Any
from ...type5.constrainedexpr import ConstrainedExpr
from ...type5.kindexpr import Nat, Star
from ...type5.typeexpr import (
TypeApplication,
TypeExpr,
TypeLevelNat,
TypeVariable,
replace_variable,
)
from ...typeclass import TypeClass, TypeClassConstraint
from ...wasmgenerator import Generator as WasmGenerator
from ..base import BuildBase, InternalImplementedMethodProtocol
def load(build: BuildBase[Any]) -> None:
a = TypeVariable(kind=Star(), name='a')
b = TypeVariable(kind=Star(), name='b')
t = TypeVariable(kind=Star() >> Star(), name='t')
t_a = TypeApplication(constructor=t, argument=a)
NatNum = build.type_classes['NatNum']
Foldable = TypeClass('Foldable', (t, ), methods={}, operators={})
has_foldable_t = TypeClassConstraint(Foldable, [t])
has_natnum_a = TypeClassConstraint(NatNum, [a])
fn_sum = ConstrainedExpr(
variables={t, a},
expr=build.type5_make_function([t_a, a]),
constraints=(has_foldable_t, has_natnum_a, ),
)
fn_b_a_b = build.type5_make_function([b, a, b])
fn_foldl = ConstrainedExpr(
variables={t, a, b},
expr=build.type5_make_function([fn_b_a_b, b, t_a, b]),
constraints=(has_foldable_t, ),
)
fn_a_b_b = build.type5_make_function([a, b, b])
fn_foldr = ConstrainedExpr(
variables={t, a, b},
expr=build.type5_make_function([fn_a_b_b, b, t_a, b]),
constraints=(has_foldable_t, ),
)
Foldable.methods = {
'sum': fn_sum,
'foldl': fn_foldl,
'foldr': fn_foldr,
}
build.register_type_class(Foldable)
class FoldableCodeGenerator:
def __init__(self, build: BuildBase[WasmGenerator]) -> None:
self.build = build
def get_natnum(self, sa_type: TypeExpr) -> tuple[dict[str, TypeExpr], InternalImplementedMethodProtocol[WasmGenerator]]:
natnum_type, natnum_router = self.build.operators['+']
assert isinstance(natnum_type, ConstrainedExpr)
assert len(natnum_type.variables) == 1
natnum_a = next(iter(natnum_type.variables))
natnum_type = replace_variable(natnum_type.expr, natnum_a, sa_type)
impl_lookup = natnum_router.get((natnum_type, ))
assert impl_lookup is not None
return impl_lookup
def wasm_dynamic_array_sum(self, g: WasmGenerator, tv_map: dict[str, TypeExpr]) -> None:
sa_type = tv_map['a']
ptr_type_info = self.build.type_info_constructed
sa_type_info = self.build.type_info_map.get(sa_type.name)
if sa_type_info is None:
sa_type_info = ptr_type_info
natnum_kwargs, natnum_impl = self.get_natnum(sa_type)
# Definitions
sum_adr = g.temp_var_t(ptr_type_info.wasm_type, 'sum_adr')
sum_stop = g.temp_var_t(ptr_type_info.wasm_type, 'sum_stop')
with g.block(params=['i32'], result=sa_type_info.wasm_type):
# Stack: [adr] -> [] ; sum_adr=ard
g.local.set(sum_adr)
# Stack: [] ; sum_stop = adr + 4 + len(adr) * sa_type_info.alloc_size
g.nop(comment='Calculate address at which to stop looping')
g.local.get(sum_adr)
g.i32.load()
g.i32.const(sa_type_info.alloc_size)
g.i32.mul()
g.local.get(sum_adr)
g.i32.add()
g.i32.const(4)
g.i32.add()
g.local.set(sum_stop)
# Stack: [] -> [sum] ; sum_adr += 4
g.nop(comment='Get the first array value as starting point')
g.local.get(sum_adr)
g.i32.const(4)
g.i32.add()
g.local.tee(sum_adr)
g.add_statement(sa_type_info.wasm_load_func)
# Since we did the first one, increase adr
# Stack: [sum] -> [sum] ; sum_adr = sum_adr + sa_type_info.alloc_size
g.local.get(sum_adr)
g.i32.const(sa_type_info.alloc_size)
g.i32.add()
g.local.set(sum_adr)
g.local.get(sum_adr)
g.local.get(sum_stop)
g.i32.lt_u()
with g.if_(params=[sa_type_info.wasm_type], result=sa_type_info.wasm_type):
with g.loop(params=[sa_type_info.wasm_type], result=sa_type_info.wasm_type):
# sum = sum + *adr
# Stack: [sum] -> [sum + *adr]
g.nop(comment='Add array value')
g.local.get(sum_adr)
g.add_statement(sa_type_info.wasm_load_func)
natnum_impl(g, natnum_kwargs)
# adr = adr + sa_type_info.alloc_size
# Stack: [sum] -> [sum]
g.nop(comment='Calculate address of the next value')
g.local.get(sum_adr)
g.i32.const(sa_type_info.alloc_size)
g.i32.add()
g.local.tee(sum_adr)
# loop if adr < stop
g.nop(comment='Check if address exceeds array bounds')
g.local.get(sum_stop)
g.i32.lt_u()
g.br_if(0)
# else: sum x[1] === x => so we don't need to loop
# End result: [sum]
def wasm_static_array_sum(self, g: WasmGenerator, tv_map: dict[str, TypeExpr]) -> None:
sa_type = tv_map['a']
sa_len = tv_map['n']
assert isinstance(sa_len, TypeLevelNat)
if sa_len.value < 1:
raise NotImplementedError('Default value in case sum is empty')
ptr_type_info = self.build.type_info_constructed
sa_type_info = self.build.type_info_map.get(sa_type.name)
if sa_type_info is None:
sa_type_info = ptr_type_info
natnum_kwargs, natnum_impl = self.get_natnum(sa_type)
# Definitions
sum_adr = g.temp_var_t(ptr_type_info.wasm_type, 'sum_adr')
sum_stop = g.temp_var_t(ptr_type_info.wasm_type, 'sum_stop')
# Stack before: [adr]
# Stack after: [sum]
# adr = {address of what's currently on stack}
# Stack: [adr] -> []
g.nop(comment=f'Start sum for {sa_type.name}[{sa_len.value}]')
g.local.set(sum_adr)
# stop = adr + ar_len * sa_type_info.alloc_size
# Stack: []
g.nop(comment='Calculate address at which to stop looping')
g.local.get(sum_adr)
g.i32.const(sa_len.value * sa_type_info.alloc_size)
g.i32.add()
g.local.set(sum_stop)
# sum = *adr
# Stack: [] -> [sum]
g.nop(comment='Get the first array value as starting point')
g.local.get(sum_adr)
g.add_statement(sa_type_info.wasm_load_func)
# Since we did the first one, increase adr
# adr = adr + sa_type_info.alloc_size
# Stack: [sum] -> [sum]
g.local.get(sum_adr)
g.i32.const(sa_type_info.alloc_size)
g.i32.add()
g.local.set(sum_adr)
if sa_len.value > 1:
with g.loop(params=[sa_type_info.wasm_type], result=sa_type_info.wasm_type):
# sum = sum + *adr
# Stack: [sum] -> [sum + *adr]
g.nop(comment='Add array value')
g.local.get(sum_adr)
g.add_statement(sa_type_info.wasm_load_func)
natnum_impl(g, natnum_kwargs)
# adr = adr + sa_type_info.alloc_size
# Stack: [sum] -> [sum]
g.nop(comment='Calculate address of the next value')
g.local.get(sum_adr)
g.i32.const(sa_type_info.alloc_size)
g.i32.add()
g.local.tee(sum_adr)
# loop if adr < stop
g.nop(comment='Check if address exceeds array bounds')
g.local.get(sum_stop)
g.i32.lt_u()
g.br_if(0)
# else: sum x[1] === x => so we don't need to loop
g.nop(comment=f'Completed sum for {sa_type.name}[{sa_len.value}]')
# End result: [sum]
def wasm_dynamic_array_foldl(self, g: WasmGenerator, tv_map: dict[str, TypeExpr]) -> None:
sa_type = tv_map['a']
res_type = tv_map['b']
ptr_type_info = self.build.type_info_constructed
u32_type_info = self.build.type_info_map['u32']
sa_type_info = self.build.type_info_map.get(sa_type.name)
if sa_type_info is None:
sa_type_info = ptr_type_info
res_type_info = self.build.type_info_map.get(res_type.name)
if res_type_info is None:
res_type_info = ptr_type_info
# Definitions
fold_adr = g.temp_var_t(ptr_type_info.wasm_type, 'fold_adr')
fold_stop = g.temp_var_t(ptr_type_info.wasm_type, 'fold_stop')
fold_init = g.temp_var_t(res_type_info.wasm_type, 'fold_init')
fold_func = g.temp_var_t(ptr_type_info.wasm_type, 'fold_func')
fold_len = g.temp_var_t(u32_type_info.wasm_type, 'fold_len')
with g.block(params=['i32', res_type_info.wasm_type, 'i32'], result=res_type_info.wasm_type, comment=f'foldl a={sa_type.name} b={res_type.name}'):
# Stack: [fn*, b, sa*] -> [fn*, b]
g.local.tee(fold_adr) # Store address, but also keep it for loading the length
g.i32.load() # Load the length
g.local.set(fold_len) # Store the length
# Stack: [fn*, b] -> [fn*]
g.local.set(fold_init)
# Stack: [fn*] -> []
g.local.set(fold_func)
# Stack: [] -> [b]
g.nop(comment='No applications if array is empty')
g.local.get(fold_init)
g.local.get(fold_len)
g.i32.eqz() # If the array is empty
g.br_if(0) # Then the base value is the result
# Stack: [b] -> [b] ; fold_adr=fold_adr + 4
g.nop(comment='Skip the header')
g.local.get(fold_adr)
g.i32.const(4)
g.i32.add()
g.local.set(fold_adr)
# Stack: [b] -> [b]
g.nop(comment='Apply the first function call')
g.local.get(fold_adr)
g.add_statement(sa_type_info.wasm_load_func)
g.local.get(fold_func)
g.call_indirect([res_type_info.wasm_type, sa_type_info.wasm_type], res_type_info.wasm_type)
# Stack: [b] -> [b]
g.nop(comment='No loop if there is only one item')
g.local.get(fold_len)
g.i32.const(1)
g.i32.eq()
g.br_if(0) # just one value, don't need to loop
# Stack: [b] -> [b] ; fold_stop=fold_adr + (sa_len.value * sa_type_info.alloc_size)
g.nop(comment='Calculate address at which to stop looping')
g.local.get(fold_adr)
g.local.get(fold_len)
g.i32.const(sa_type_info.alloc_size)
g.i32.mul()
g.i32.add()
g.local.set(fold_stop)
# Stack: [b] -> [b] ; fold_adr = fold_adr + sa_type_info.alloc_size
g.nop(comment='Calculate address of the next value')
g.local.get(fold_adr)
g.i32.const(sa_type_info.alloc_size)
g.i32.add()
g.local.set(fold_adr)
with g.loop(params=[res_type_info.wasm_type], result=res_type_info.wasm_type):
# Stack: [b] -> [b]
g.nop(comment='Apply function call')
g.local.get(fold_adr)
g.add_statement(sa_type_info.wasm_load_func)
g.local.get(fold_func)
g.call_indirect([res_type_info.wasm_type, sa_type_info.wasm_type], res_type_info.wasm_type)
# Stack: [b] -> [b] ; fold_adr = fold_adr + sa_type_info.alloc_size
g.nop(comment='Calculate address of the next value')
g.local.get(fold_adr)
g.i32.const(sa_type_info.alloc_size)
g.i32.add()
g.local.tee(fold_adr)
# loop if adr > stop
# Stack: [b] -> [b]
g.nop(comment='Check if address exceeds array bounds')
g.local.get(fold_stop)
g.i32.lt_u()
g.br_if(0)
# Stack: [b]
def wasm_static_array_foldl(self, g: WasmGenerator, tv_map: dict[str, TypeExpr]) -> None:
sa_type = tv_map['a']
sa_len = tv_map['n']
res_type = tv_map['b']
assert isinstance(sa_len, TypeLevelNat)
ptr_type_info = self.build.type_info_constructed
sa_type_info = self.build.type_info_map.get(sa_type.name)
if sa_type_info is None:
sa_type_info = ptr_type_info
res_type_info = self.build.type_info_map.get(res_type.name)
if res_type_info is None:
res_type_info = ptr_type_info
# Definitions
fold_adr = g.temp_var_t(ptr_type_info.wasm_type, 'fold_adr')
fold_stop = g.temp_var_t(ptr_type_info.wasm_type, 'fold_stop')
fold_init = g.temp_var_t(res_type_info.wasm_type, 'fold_init')
fold_func = g.temp_var_t(ptr_type_info.wasm_type, 'fold_func')
with g.block(params=['i32', res_type_info.wasm_type, 'i32'], result=res_type_info.wasm_type, comment=f'foldl a={sa_type.name} n={sa_len.value} b={res_type.name}'):
# Stack: [fn*, b, sa*] -> [fn*, b]
g.local.set(fold_adr)
# Stack: [fn*, b] -> [fn*]
g.local.set(fold_init)
# Stack: [fn*] -> []
g.local.set(fold_func)
if sa_len.value < 1:
g.local.get(fold_init)
return
# Stack: [] -> [b]
g.nop(comment='Apply the first function call')
g.local.get(fold_init)
g.local.get(fold_adr)
g.add_statement(sa_type_info.wasm_load_func)
g.local.get(fold_func)
g.call_indirect([res_type_info.wasm_type, sa_type_info.wasm_type], res_type_info.wasm_type)
if sa_len.value > 1:
# Stack: [b] -> [b] ; fold_stop=fold_adr + (sa_len.value * sa_type_info.alloc_size)
g.nop(comment='Calculate address at which to stop looping')
g.local.get(fold_adr)
g.i32.const(sa_len.value * sa_type_info.alloc_size)
g.i32.add()
g.local.set(fold_stop)
# Stack: [b] -> [b] ; fold_adr = fold_adr + sa_type_info.alloc_size
g.nop(comment='Calculate address of the next value')
g.local.get(fold_adr)
g.i32.const(sa_type_info.alloc_size)
g.i32.add()
g.local.set(fold_adr)
with g.loop(params=[res_type_info.wasm_type], result=res_type_info.wasm_type):
# Stack: [b] -> [b]
g.nop(comment='Apply function call')
g.local.get(fold_adr)
g.add_statement(sa_type_info.wasm_load_func)
g.local.get(fold_func)
g.call_indirect([res_type_info.wasm_type, sa_type_info.wasm_type], res_type_info.wasm_type)
# Stack: [b] -> [b] ; fold_adr = fold_adr + sa_type_info.alloc_size
g.nop(comment='Calculate address of the next value')
g.local.get(fold_adr)
g.i32.const(sa_type_info.alloc_size)
g.i32.add()
g.local.tee(fold_adr)
# loop if adr > stop
# Stack: [b] -> [b]
g.nop(comment='Check if address exceeds array bounds')
g.local.get(fold_stop)
g.i32.lt_u()
g.br_if(0)
# else: just one value, don't need to loop
# Stack: [b]
def wasm_dynamic_array_foldr(self, g: WasmGenerator, tv_map: dict[str, TypeExpr]) -> None:
sa_type = tv_map['a']
res_type = tv_map['b']
ptr_type_info = self.build.type_info_constructed
u32_type_info = self.build.type_info_map['u32']
sa_type_info = self.build.type_info_map.get(sa_type.name)
if sa_type_info is None:
sa_type_info = ptr_type_info
res_type_info = self.build.type_info_map.get(res_type.name)
if res_type_info is None:
res_type_info = ptr_type_info
# Definitions
fold_adr = g.temp_var_t(ptr_type_info.wasm_type, 'fold_adr')
fold_stop = g.temp_var_t(ptr_type_info.wasm_type, 'fold_stop')
fold_tmp = g.temp_var_t(res_type_info.wasm_type, 'fold_tmp')
fold_func = g.temp_var_t(ptr_type_info.wasm_type, 'fold_func')
fold_len = g.temp_var_t(u32_type_info.wasm_type, 'fold_len')
with g.block(params=['i32', res_type_info.wasm_type, 'i32'], result=res_type_info.wasm_type, comment=f'foldr a={sa_type.name} b={res_type.name}'):
# Stack: [fn*, b, sa*] -> [fn*, b] ; fold_adr=fn*, fold_tmp=b, fold_func=fn*, fold_len=*sa
g.local.tee(fold_adr) # Store address, but also keep it for loading the length
g.i32.load() # Load the length
g.local.set(fold_len) # Store the length
# Stack: [fn*, b] -> [fn*]
g.local.set(fold_tmp)
# Stack: [fn*] -> []
g.local.set(fold_func)
# Stack: [] -> []
g.nop(comment='No applications if array is empty')
g.local.get(fold_tmp)
g.local.get(fold_len)
g.i32.eqz() # If the array is empty
g.br_if(0) # Then the base value is the result
g.drop() # Else drop the value for now
# Stack: [b] -> [b] ; fold_adr=fold_adr + 4
g.nop(comment='Skip the header')
g.local.get(fold_adr)
g.i32.const(4)
g.i32.add()
g.local.set(fold_adr)
# Stack: [] -> [] ; fold_stop=fold_adr
g.nop(comment='Calculate address at which to stop looping')
g.local.get(fold_adr)
g.local.set(fold_stop)
# Stack: [] -> [] ; fold_adr=fold_adr + (sa_len.value - 1) * sa_type_info.alloc_size
g.nop(comment='Calculate address at which to start looping')
g.local.get(fold_adr)
g.local.get(fold_len)
g.i32.const(1)
g.i32.sub()
g.i32.const(sa_type_info.alloc_size)
g.i32.mul()
g.i32.add()
g.local.set(fold_adr)
# Stack: [] -> [b]
g.nop(comment='Apply the first function call')
g.local.get(fold_adr)
g.add_statement(sa_type_info.wasm_load_func)
g.local.get(fold_tmp)
g.local.get(fold_func)
g.call_indirect([sa_type_info.wasm_type, res_type_info.wasm_type], res_type_info.wasm_type)
# Stack: [b] -> [b]
g.nop(comment='Check if more than one entry')
g.local.get(fold_len)
g.i32.const(1)
g.i32.eq() # If the array has only item
g.br_if(0) # Then the the first application is sufficient
# Stack: [b] -> [b] ; fold_adr = fold_adr - sa_type_info.alloc_size
g.nop(comment='Calculate address of the next value')
g.local.get(fold_adr)
g.i32.const(sa_type_info.alloc_size)
g.i32.sub()
g.local.set(fold_adr)
with g.loop(params=[res_type_info.wasm_type], result=res_type_info.wasm_type):
g.nop(comment='Apply function call')
# Stack [b] since we don't have proper stack switching opcodes
# Stack: [b] -> []
g.local.set(fold_tmp)
# Stack: [] -> [a]
g.local.get(fold_adr)
g.add_statement(sa_type_info.wasm_load_func)
# Stack [a] -> [a, b]
g.local.get(fold_tmp)
# Stack [a, b] -> [b]
g.local.get(fold_func)
g.call_indirect([sa_type_info.wasm_type, res_type_info.wasm_type], res_type_info.wasm_type)
# Stack: [b] -> [b] ; fold_adr = fold_adr - sa_type_info.alloc_size
g.nop(comment='Calculate address of the next value')
g.local.get(fold_adr)
g.i32.const(sa_type_info.alloc_size)
g.i32.sub()
g.local.tee(fold_adr)
# loop if adr >= stop
# Stack: [b] -> [b]
g.nop(comment='Check if address exceeds array bounds')
g.local.get(fold_stop)
g.i32.ge_u()
g.br_if(0)
# Stack: [b]
def wasm_static_array_foldr(self, g: WasmGenerator, tv_map: dict[str, TypeExpr]) -> None:
sa_type = tv_map['a']
sa_len = tv_map['n']
res_type = tv_map['b']
assert isinstance(sa_len, TypeLevelNat)
ptr_type_info = self.build.type_info_constructed
sa_type_info = self.build.type_info_map.get(sa_type.name)
if sa_type_info is None:
sa_type_info = ptr_type_info
res_type_info = self.build.type_info_map.get(res_type.name)
if res_type_info is None:
res_type_info = ptr_type_info
# Definitions
fold_adr = g.temp_var_t(ptr_type_info.wasm_type, 'fold_adr')
fold_stop = g.temp_var_t(ptr_type_info.wasm_type, 'fold_stop')
fold_tmp = g.temp_var_t(res_type_info.wasm_type, 'fold_tmp')
fold_func = g.temp_var_t(ptr_type_info.wasm_type, 'fold_func')
with g.block(params=['i32', res_type_info.wasm_type, 'i32'], result=res_type_info.wasm_type, comment=f'foldr a={sa_type.name} n={sa_len.value} b={res_type.name}'):
# Stack: [fn*, b, sa*] -> [fn*, b] ; fold_adr=fn*, fold_tmp=b, fold_func=fn*
g.local.set(fold_adr)
# Stack: [fn*, b] -> [fn*]
g.local.set(fold_tmp)
# Stack: [fn*] -> []
g.local.set(fold_func)
if sa_len.value < 1:
g.local.get(fold_tmp)
return
# Stack: [] -> [] ; fold_stop=fold_adr
g.nop(comment='Calculate address at which to stop looping')
g.local.get(fold_adr)
g.local.set(fold_stop)
# Stack: [] -> [] ; fold_adr=fold_adr + (sa_len.value - 1) * sa_type_info.alloc_size
g.nop(comment='Calculate address at which to start looping')
g.local.get(fold_adr)
g.i32.const((sa_len.value - 1) * sa_type_info.alloc_size)
g.i32.add()
g.local.set(fold_adr)
# Stack: [] -> [b]
g.nop(comment='Get the init value and first array value as starting point')
g.local.get(fold_adr)
g.add_statement(sa_type_info.wasm_load_func)
g.local.get(fold_tmp)
g.local.get(fold_func)
g.call_indirect([sa_type_info.wasm_type, res_type_info.wasm_type], res_type_info.wasm_type)
if sa_len.value > 1:
# Stack: [b] -> [b] ; fold_adr = fold_adr - sa_type_info.alloc_size
g.nop(comment='Calculate address of the next value')
g.local.get(fold_adr)
g.i32.const(sa_type_info.alloc_size)
g.i32.sub()
g.local.set(fold_adr)
with g.loop(params=[res_type_info.wasm_type], result=res_type_info.wasm_type):
g.nop(comment='Apply function call')
# Stack [b] since we don't have proper stack switching opcodes
# Stack: [b] -> []
g.local.set(fold_tmp)
# Stack: [] -> [a]
g.local.get(fold_adr)
g.add_statement(sa_type_info.wasm_load_func)
# Stack [a] -> [a, b]
g.local.get(fold_tmp)
# Stack [a, b] -> [b]
g.local.get(fold_func)
g.call_indirect([sa_type_info.wasm_type, res_type_info.wasm_type], res_type_info.wasm_type)
# Stack: [b] -> [b] ; fold_adr = fold_adr - sa_type_info.alloc_size
g.nop(comment='Calculate address of the next value')
g.local.get(fold_adr)
g.i32.const(sa_type_info.alloc_size)
g.i32.sub()
g.local.tee(fold_adr)
# loop if adr >= stop
# Stack: [b] -> [b]
g.nop(comment='Check if address exceeds array bounds')
g.local.get(fold_stop)
g.i32.ge_u()
g.br_if(0)
# else: just one value, don't need to loop
# Stack: [b]
def wasm(build: BuildBase[WasmGenerator]) -> None:
Foldable = build.type_classes['Foldable']
n = TypeVariable(kind=Nat(), name='n')
gen = FoldableCodeGenerator(build)
build.instance_type_class(Foldable, build.dynamic_array_type5_constructor, methods={
'sum': gen.wasm_dynamic_array_sum,
'foldl': gen.wasm_dynamic_array_foldl,
'foldr': gen.wasm_dynamic_array_foldr,
})
foo = TypeApplication(constructor=build.static_array_type5_constructor, argument=n)
build.instance_type_class(Foldable, foo, methods={
'sum': gen.wasm_static_array_sum,
'foldl': gen.wasm_static_array_foldl,
'foldr': gen.wasm_static_array_foldr,
})

View File

@ -0,0 +1,107 @@
"""
The Fractional type class is defined for numeric types that can be (precisely) divided.
"""
from __future__ import annotations
from typing import Any
from ...type5.constrainedexpr import ConstrainedExpr
from ...type5.kindexpr import Star
from ...type5.typeexpr import TypeVariable
from ...typeclass import TypeClass, TypeClassConstraint
from ...wasmgenerator import Generator as WasmGenerator
from ..base import BuildBase
def load(build: BuildBase[Any]) -> None:
a = TypeVariable(kind=Star(), name='a')
Fractional = TypeClass('Fractional', (a, ), methods={}, operators={})
has_fractional_a = TypeClassConstraint(Fractional, [a])
fn_a_a = ConstrainedExpr(
variables={a},
expr=build.type5_make_function([a, a]),
constraints=(has_fractional_a, ),
)
fn_a_a_a = ConstrainedExpr(
variables={a},
expr=build.type5_make_function([a, a, a]),
constraints=(has_fractional_a, ),
)
Fractional.methods = {
'ceil': fn_a_a,
'floor': fn_a_a,
'trunc': fn_a_a,
'nearest': fn_a_a,
}
Fractional.operators = {
'/': fn_a_a_a,
}
# FIXME: inherited_classes=[NatNum])
build.register_type_class(Fractional)
def wasm_f32_ceil(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f32.ceil()
def wasm_f64_ceil(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f64.ceil()
def wasm_f32_floor(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f32.floor()
def wasm_f64_floor(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f64.floor()
def wasm_f32_trunc(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f32.trunc()
def wasm_f64_trunc(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f64.trunc()
def wasm_f32_nearest(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f32.nearest()
def wasm_f64_nearest(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f64.nearest()
def wasm_f32_div(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f32.div()
def wasm_f64_div(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f64.div()
def wasm(build: BuildBase[WasmGenerator]) -> None:
Fractional = build.type_classes['Fractional']
build.instance_type_class(Fractional, build.types['f32'], methods={
'ceil': wasm_f32_ceil,
'floor': wasm_f32_floor,
'trunc': wasm_f32_trunc,
'nearest': wasm_f32_nearest,
}, operators={
'/': wasm_f32_div,
})
build.instance_type_class(Fractional, build.types['f64'], methods={
'ceil': wasm_f64_ceil,
'floor': wasm_f64_floor,
'trunc': wasm_f64_trunc,
'nearest': wasm_f64_nearest,
}, operators={
'/': wasm_f64_div,
})

View File

@ -0,0 +1,88 @@
"""
The Integral type class is defined for types that can only be approximately divided.
"""
from __future__ import annotations
from typing import Any
from ...type5.constrainedexpr import ConstrainedExpr
from ...type5.kindexpr import Star
from ...type5.typeexpr import TypeVariable
from ...typeclass import TypeClass, TypeClassConstraint
from ...wasmgenerator import Generator as WasmGenerator
from ..base import BuildBase
def load(build: BuildBase[Any]) -> None:
a = TypeVariable(kind=Star(), name='a')
Integral = TypeClass('Integral', (a, ), methods={}, operators={})
has_integral_a = TypeClassConstraint(Integral, [a])
fn_a_a_a = ConstrainedExpr(
variables={a},
expr=build.type5_make_function([a, a, a]),
constraints=(has_integral_a, ),
)
Integral.operators = {
'//': fn_a_a_a,
'%': fn_a_a_a,
}
# FIXME: inherited_classes=[NatNum]
build.register_type_class(Integral)
def wasm_u32_div(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('i32.div_u')
def wasm_u64_div(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('i64.div_u')
def wasm_i32_div(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('i32.div_s')
def wasm_i64_div(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('i64.div_s')
def wasm_u32_rem(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('i32.rem_u')
def wasm_u64_rem(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('i64.rem_u')
def wasm_i32_rem(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('i32.rem_s')
def wasm_i64_rem(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('i64.rem_s')
def wasm(build: BuildBase[WasmGenerator]) -> None:
Integral = build.type_classes['Integral']
build.instance_type_class(Integral, build.types['u32'], operators={
'//': wasm_u32_div,
'%': wasm_u32_rem,
})
build.instance_type_class(Integral, build.types['u64'], operators={
'//': wasm_u64_div,
'%': wasm_u64_rem,
})
build.instance_type_class(Integral, build.types['i32'], operators={
'//': wasm_i32_div,
'%': wasm_i32_rem,
})
build.instance_type_class(Integral, build.types['i64'], operators={
'//': wasm_i64_div,
'%': wasm_i64_rem,
})

View File

@ -0,0 +1,89 @@
"""
The IntNum type class is defined for Integer Number types.
"""
from __future__ import annotations
from typing import Any
from ...type5.constrainedexpr import ConstrainedExpr
from ...type5.kindexpr import Star
from ...type5.typeexpr import TypeVariable
from ...typeclass import TypeClass, TypeClassConstraint
from ...wasmgenerator import Generator as WasmGenerator
from ..base import BuildBase
def load(build: BuildBase[Any]) -> None:
a = TypeVariable(kind=Star(), name='a')
IntNum = TypeClass('IntNum', (a, ), methods={}, operators={})
has_intnum_a = TypeClassConstraint(IntNum, [a])
fn_a_a = ConstrainedExpr(
variables={a},
expr=build.type5_make_function([a, a]),
constraints=(has_intnum_a, ),
)
IntNum.methods = {
'abs': fn_a_a,
'neg': fn_a_a,
}
# FIXME: inherited_classes=[NatNum])
build.register_type_class(IntNum)
def wasm_i32_abs(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__i32_abs__')
def wasm_i64_abs(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__i64_abs__')
def wasm_f32_abs(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f32.abs()
def wasm_f64_abs(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f64.abs()
def wasm_i32_neg(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.const(-1)
g.i32.mul()
def wasm_i64_neg(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.const(-1)
g.i64.mul()
def wasm_f32_neg(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f32.neg()
def wasm_f64_neg(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f64.neg()
def wasm(build: BuildBase[WasmGenerator]) -> None:
IntNum = build.type_classes['IntNum']
build.instance_type_class(IntNum, build.types['i32'], methods={
'abs': wasm_i32_abs,
'neg': wasm_i32_neg,
})
build.instance_type_class(IntNum, build.types['i64'], methods={
'abs': wasm_i64_abs,
'neg': wasm_i64_neg,
})
build.instance_type_class(IntNum, build.types['f32'], methods={
'abs': wasm_f32_abs,
'neg': wasm_f32_neg,
})
build.instance_type_class(IntNum, build.types['f64'], methods={
'abs': wasm_f64_abs,
'neg': wasm_f64_neg,
})

View File

@ -0,0 +1,227 @@
"""
The NatNum type class is defined for Natural Number types.
These cannot be negative so functions like abs and neg make no sense.
"""
from __future__ import annotations
from typing import Any
from ...type5.constrainedexpr import ConstrainedExpr
from ...type5.kindexpr import Star
from ...type5.typeexpr import TypeVariable
from ...typeclass import TypeClass, TypeClassConstraint
from ...wasmgenerator import Generator as WasmGenerator
from ..base import BuildBase
def load(build: BuildBase[Any]) -> None:
a = TypeVariable(kind=Star(), name='a')
u32 = build.types['u32']
NatNum = TypeClass('NatNum', (a, ), methods={}, operators={})
has_natnum_a = TypeClassConstraint(NatNum, [a])
fn_a_a_a = ConstrainedExpr(
variables={a},
expr=build.type5_make_function([a, a, a]),
constraints=(has_natnum_a, ),
)
fn_a_u32_a = ConstrainedExpr(
variables={a},
expr=build.type5_make_function([a, u32, a]),
constraints=(has_natnum_a, ),
)
NatNum.operators = {
'+': fn_a_a_a,
'-': fn_a_a_a,
'*': fn_a_a_a,
'<<': fn_a_u32_a, # Arithmic shift left
'>>': fn_a_u32_a, # Arithmic shift right
}
build.register_type_class(NatNum)
## ###
## class NatNum
def wasm_u32_add(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('i32.add')
def wasm_u64_add(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('i64.add')
def wasm_i32_add(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('i32.add')
def wasm_i64_add(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('i64.add')
def wasm_f32_add(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('f32.add')
def wasm_f64_add(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('f64.add')
def wasm_u32_sub(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('i32.sub')
def wasm_u64_sub(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('i64.sub')
def wasm_i32_sub(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('i32.sub')
def wasm_i64_sub(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('i64.sub')
def wasm_f32_sub(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('f32.sub')
def wasm_f64_sub(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('f64.sub')
def wasm_u32_mul(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('i32.mul')
def wasm_u64_mul(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('i64.mul')
def wasm_i32_mul(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('i32.mul')
def wasm_i64_mul(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('i64.mul')
def wasm_f32_mul(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('f32.mul')
def wasm_f64_mul(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.add_statement('f64.mul')
def wasm_u32_arithmic_shift_left(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.shl()
def wasm_u64_arithmic_shift_left(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.extend_i32_u()
g.i64.shl()
def wasm_i32_arithmic_shift_left(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.shl()
def wasm_i64_arithmic_shift_left(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.extend_i32_u()
g.i64.shl()
def wasm_f32_arithmic_shift_left(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__u32_pow2__')
g.f32.convert_i32_u()
g.f32.mul()
def wasm_f64_arithmic_shift_left(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__u32_pow2__')
g.f64.convert_i32_u()
g.f64.mul()
def wasm_u32_arithmic_shift_right(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.shr_u()
def wasm_u64_arithmic_shift_right(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.extend_i32_u()
g.i64.shr_u()
def wasm_i32_arithmic_shift_right(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.shr_s()
def wasm_i64_arithmic_shift_right(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.extend_i32_u()
g.i64.shr_s()
def wasm_f32_arithmic_shift_right(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__u32_pow2__')
g.f32.convert_i32_u()
g.f32.div()
def wasm_f64_arithmic_shift_right(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__u32_pow2__')
g.f64.convert_i32_u()
g.f64.div()
def wasm(build: BuildBase[WasmGenerator]) -> None:
NatNum = build.type_classes['NatNum']
build.instance_type_class(NatNum, build.types['u32'], operators={
'+': wasm_u32_add,
'-': wasm_u32_sub,
'*': wasm_u32_mul,
'<<': wasm_u32_arithmic_shift_left,
'>>': wasm_u32_arithmic_shift_right,
})
build.instance_type_class(NatNum, build.types['u64'], operators={
'+': wasm_u64_add,
'-': wasm_u64_sub,
'*': wasm_u64_mul,
'<<': wasm_u64_arithmic_shift_left,
'>>': wasm_u64_arithmic_shift_right,
})
build.instance_type_class(NatNum, build.types['i32'], operators={
'+': wasm_i32_add,
'-': wasm_i32_sub,
'*': wasm_i32_mul,
'<<': wasm_i32_arithmic_shift_left,
'>>': wasm_i32_arithmic_shift_right,
})
build.instance_type_class(NatNum, build.types['i64'], operators={
'+': wasm_i64_add,
'-': wasm_i64_sub,
'*': wasm_i64_mul,
'<<': wasm_i64_arithmic_shift_left,
'>>': wasm_i64_arithmic_shift_right,
})
build.instance_type_class(NatNum, build.types['f32'], operators={
'+': wasm_f32_add,
'-': wasm_f32_sub,
'*': wasm_f32_mul,
'<<': wasm_f32_arithmic_shift_left,
'>>': wasm_f32_arithmic_shift_right,
})
build.instance_type_class(NatNum, build.types['f64'], operators={
'+': wasm_f64_add,
'-': wasm_f64_sub,
'*': wasm_f64_mul,
'<<': wasm_f64_arithmic_shift_left,
'>>': wasm_f64_arithmic_shift_right,
})

View File

@ -0,0 +1,383 @@
"""
The Ord type class is defined for totally ordered datatypes.
"""
from __future__ import annotations
from typing import Any
from ...type5.constrainedexpr import ConstrainedExpr
from ...type5.kindexpr import Star
from ...type5.typeexpr import TypeVariable
from ...typeclass import TypeClass, TypeClassConstraint
from ...wasmgenerator import Generator as WasmGenerator
from ..base import BuildBase
def load(build: BuildBase[Any]) -> None:
a = TypeVariable(kind=Star(), name='a')
Ord = TypeClass('Ord', (a, ), methods={}, operators={})
has_ord_a = TypeClassConstraint(Ord, [a])
fn_a_a_a = ConstrainedExpr(
variables={a},
expr=build.type5_make_function([a, a, a]),
constraints=(has_ord_a, ),
)
fn_a_a_bool = ConstrainedExpr(
variables={a},
expr=build.type5_make_function([a, a, build.bool_type5]),
constraints=(has_ord_a, ),
)
Ord.methods = {
'min': fn_a_a_a,
'max': fn_a_a_a,
}
Ord.operators = {
'<': fn_a_a_bool,
'<=': fn_a_a_bool,
'>': fn_a_a_bool,
'>=': fn_a_a_bool,
}
#FIXME: }, inherited_classes=[Eq])
build.register_type_class(Ord)
def wasm_u8_min(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__u32_min__')
def wasm_u16_min(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__u32_min__')
def wasm_u32_min(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__u32_min__')
def wasm_u64_min(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__u64_min__')
def wasm_i8_min(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__i32_min__')
def wasm_i16_min(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__i32_min__')
def wasm_i32_min(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__i32_min__')
def wasm_i64_min(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__i64_min__')
def wasm_f32_min(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f32.min()
def wasm_f64_min(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f64.min()
def wasm_u8_max(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__u32_max__')
def wasm_u16_max(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__u32_max__')
def wasm_u32_max(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__u32_max__')
def wasm_u64_max(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__u64_max__')
def wasm_i8_max(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__i32_max__')
def wasm_i16_max(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__i32_max__')
def wasm_i32_max(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__i32_max__')
def wasm_i64_max(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.call('stdlib.types.__i64_max__')
def wasm_f32_max(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f32.max()
def wasm_f64_max(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f64.max()
def wasm_u8_less_than(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.lt_u()
def wasm_u16_less_than(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.lt_u()
def wasm_u32_less_than(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.lt_u()
def wasm_u64_less_than(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.lt_u()
def wasm_i8_less_than(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.lt_s()
def wasm_i16_less_than(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.lt_s()
def wasm_i32_less_than(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.lt_s()
def wasm_i64_less_than(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.lt_s()
def wasm_f32_less_than(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f32.lt()
def wasm_f64_less_than(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f64.lt()
def wasm_u8_less_than_or_equal(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.le_u()
def wasm_u16_less_than_or_equal(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.le_u()
def wasm_u32_less_than_or_equal(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.le_u()
def wasm_u64_less_than_or_equal(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.le_u()
def wasm_i8_less_than_or_equal(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.le_s()
def wasm_i16_less_than_or_equal(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.le_s()
def wasm_i32_less_than_or_equal(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.le_s()
def wasm_i64_less_than_or_equal(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.le_s()
def wasm_f32_less_than_or_equal(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f32.le()
def wasm_f64_less_than_or_equal(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f64.le()
def wasm_u8_greater_than(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.gt_u()
def wasm_u16_greater_than(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.gt_u()
def wasm_u32_greater_than(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.gt_u()
def wasm_u64_greater_than(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.gt_u()
def wasm_i8_greater_than(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.gt_s()
def wasm_i16_greater_than(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.gt_s()
def wasm_i32_greater_than(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.gt_s()
def wasm_i64_greater_than(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.gt_s()
def wasm_f32_greater_than(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f32.gt()
def wasm_f64_greater_than(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f64.gt()
def wasm_u8_greater_than_or_equal(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.ge_u()
def wasm_u16_greater_than_or_equal(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.ge_u()
def wasm_u32_greater_than_or_equal(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.ge_u()
def wasm_u64_greater_than_or_equal(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.ge_u()
def wasm_i8_greater_than_or_equal(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.ge_s()
def wasm_i16_greater_than_or_equal(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.ge_s()
def wasm_i32_greater_than_or_equal(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.ge_s()
def wasm_i64_greater_than_or_equal(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.ge_s()
def wasm_f32_greater_than_or_equal(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f32.ge()
def wasm_f64_greater_than_or_equal(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f64.ge()
def wasm(build: BuildBase[WasmGenerator]) -> None:
Ord = build.type_classes['Ord']
build.instance_type_class(Ord, build.types['u8'], methods={
'min': wasm_u8_min,
'max': wasm_u8_max,
}, operators={
'<': wasm_u8_less_than,
'<=': wasm_u8_less_than_or_equal,
'>': wasm_u8_greater_than,
'>=': wasm_u8_greater_than_or_equal,
})
build.instance_type_class(Ord, build.types['u16'], methods={
'min': wasm_u16_min,
'max': wasm_u16_max,
}, operators={
'<': wasm_u16_less_than,
'<=': wasm_u16_less_than_or_equal,
'>': wasm_u16_greater_than,
'>=': wasm_u16_greater_than_or_equal,
})
build.instance_type_class(Ord, build.types['u32'], methods={
'min': wasm_u32_min,
'max': wasm_u32_max,
}, operators={
'<': wasm_u32_less_than,
'<=': wasm_u32_less_than_or_equal,
'>': wasm_u32_greater_than,
'>=': wasm_u32_greater_than_or_equal,
})
build.instance_type_class(Ord, build.types['u64'], methods={
'min': wasm_u64_min,
'max': wasm_u64_max,
}, operators={
'<': wasm_u64_less_than,
'<=': wasm_u64_less_than_or_equal,
'>': wasm_u64_greater_than,
'>=': wasm_u64_greater_than_or_equal,
})
build.instance_type_class(Ord, build.types['i8'], methods={
'min': wasm_i8_min,
'max': wasm_i8_max,
}, operators={
'<': wasm_i8_less_than,
'<=': wasm_i8_less_than_or_equal,
'>': wasm_i8_greater_than,
'>=': wasm_i8_greater_than_or_equal,
})
build.instance_type_class(Ord, build.types['i16'], methods={
'min': wasm_i16_min,
'max': wasm_i16_max,
}, operators={
'<': wasm_i16_less_than,
'<=': wasm_i16_less_than_or_equal,
'>': wasm_i16_greater_than,
'>=': wasm_i16_greater_than_or_equal,
})
build.instance_type_class(Ord, build.types['i32'], methods={
'min': wasm_i32_min,
'max': wasm_i32_max,
}, operators={
'<': wasm_i32_less_than,
'<=': wasm_i32_less_than_or_equal,
'>': wasm_i32_greater_than,
'>=': wasm_i32_greater_than_or_equal,
})
build.instance_type_class(Ord, build.types['i64'], methods={
'min': wasm_i64_min,
'max': wasm_i64_max,
}, operators={
'<': wasm_i64_less_than,
'<=': wasm_i64_less_than_or_equal,
'>': wasm_i64_greater_than,
'>=': wasm_i64_greater_than_or_equal,
})
build.instance_type_class(Ord, build.types['f32'], methods={
'min': wasm_f32_min,
'max': wasm_f32_max,
}, operators={
'<': wasm_f32_less_than,
'<=': wasm_f32_less_than_or_equal,
'>': wasm_f32_greater_than,
'>=': wasm_f32_greater_than_or_equal,
})
build.instance_type_class(Ord, build.types['f64'], methods={
'min': wasm_f64_min,
'max': wasm_f64_max,
}, operators={
'<': wasm_f64_less_than,
'<=': wasm_f64_less_than_or_equal,
'>': wasm_f64_greater_than,
'>=': wasm_f64_greater_than_or_equal,
})

View File

@ -0,0 +1,58 @@
"""
The Promotable type class is defined for types that can safely be promoted to a type
that can hold strictly more values. Going back will result in some precision being lost.
"""
from __future__ import annotations
from typing import Any
from ...type5.constrainedexpr import ConstrainedExpr
from ...type5.kindexpr import Star
from ...type5.typeexpr import TypeVariable
from ...typeclass import TypeClass, TypeClassConstraint
from ...wasmgenerator import Generator as WasmGenerator
from ..base import BuildBase
def load(build: BuildBase[Any]) -> None:
a = TypeVariable(kind=Star(), name='a')
b = TypeVariable(kind=Star(), name='b')
Promotable = TypeClass('Promotable', (a, b, ), methods={}, operators={})
has_Promotable_a_b = TypeClassConstraint(Promotable, [a, b])
fn_a_b = ConstrainedExpr(
variables={a, b},
expr=build.type5_make_function([a, b]),
constraints=(has_Promotable_a_b, ),
)
fn_b_a = ConstrainedExpr(
variables={a, b},
expr=build.type5_make_function([b, a]),
constraints=(has_Promotable_a_b, ),
)
Promotable.methods = {
'promote': fn_a_b,
'demote': fn_b_a,
}
build.register_type_class(Promotable)
def wasm_f32_f64_promote(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f64.promote_f32()
def wasm_f32_f64_demote(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f32.demote_f64()
def wasm(build: BuildBase[WasmGenerator]) -> None:
Promotable = build.type_classes['Promotable']
build.instance_type_class(Promotable, build.types['f32'], build.types['f64'], methods={
'promote': wasm_f32_f64_promote,
'demote': wasm_f32_f64_demote,
})

View File

@ -0,0 +1,95 @@
"""
The Reinterpretable type class is defined for when the data for a type can be reinterpreted
to hold a value for another type.
"""
from __future__ import annotations
from typing import Any
from ...type5.constrainedexpr import ConstrainedExpr
from ...type5.kindexpr import Star
from ...type5.typeexpr import TypeVariable
from ...typeclass import TypeClass, TypeClassConstraint
from ...wasmgenerator import Generator as WasmGenerator
from ..base import BuildBase
def load(build: BuildBase[Any]) -> None:
a = TypeVariable(kind=Star(), name='a')
b = TypeVariable(kind=Star(), name='b')
Reinterpretable = TypeClass('Reinterpretable', (a, b, ), methods={}, operators={})
has_reinterpretable_a_b = TypeClassConstraint(Reinterpretable, [a, b])
fn_a_b = ConstrainedExpr(
variables={a, b},
expr=build.type5_make_function([a, b]),
constraints=(has_reinterpretable_a_b, ),
)
Reinterpretable.methods = {
'reinterpret': fn_a_b,
}
build.register_type_class(Reinterpretable)
def wasm_i32_f32_reinterpret(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f32.reinterpret_i32()
def wasm_u32_f32_reinterpret(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f32.reinterpret_i32()
def wasm_i64_f64_reinterpret(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f64.reinterpret_i64()
def wasm_u64_f64_reinterpret(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.f64.reinterpret_i64()
def wasm_f32_i32_reinterpret(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.reinterpret_f32()
def wasm_f32_u32_reinterpret(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i32.reinterpret_f32()
def wasm_f64_i64_reinterpret(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.reinterpret_f64()
def wasm_f64_u64_reinterpret(g: WasmGenerator, tv_map: Any) -> None:
del tv_map
g.i64.reinterpret_f64()
def wasm(build: BuildBase[WasmGenerator]) -> None:
Reinterpretable = build.type_classes['Reinterpretable']
build.instance_type_class(Reinterpretable, build.types['u32'], build.types['f32'], methods={
'reinterpret': wasm_u32_f32_reinterpret,
})
build.instance_type_class(Reinterpretable, build.types['u64'], build.types['f64'], methods={
'reinterpret': wasm_u64_f64_reinterpret,
})
build.instance_type_class(Reinterpretable, build.types['i32'], build.types['f32'], methods={
'reinterpret': wasm_i32_f32_reinterpret,
})
build.instance_type_class(Reinterpretable, build.types['i64'], build.types['f64'], methods={
'reinterpret': wasm_i64_f64_reinterpret,
})
build.instance_type_class(Reinterpretable, build.types['f32'], build.types['u32'], methods={
'reinterpret': wasm_f32_u32_reinterpret,
})
build.instance_type_class(Reinterpretable, build.types['f64'], build.types['u64'], methods={
'reinterpret': wasm_f64_u64_reinterpret,
})
build.instance_type_class(Reinterpretable, build.types['f32'], build.types['i32'], methods={
'reinterpret': wasm_f32_i32_reinterpret,
})
build.instance_type_class(Reinterpretable, build.types['f64'], build.types['i64'], methods={
'reinterpret': wasm_f64_i64_reinterpret,
})

View File

@ -0,0 +1,61 @@
"""
The Sized type class is defined for when a value can be considered to have a length.
The length is always in number of items, and never in number of bytes (unless an item is a byte).
"""
from __future__ import annotations
from typing import Any
from ...type5.constrainedexpr import ConstrainedExpr
from ...type5.kindexpr import Nat, Star
from ...type5.typeexpr import TypeApplication, TypeExpr, TypeLevelNat, TypeVariable
from ...typeclass import TypeClass, TypeClassConstraint
from ...wasmgenerator import Generator as WasmGenerator
from ..base import BuildBase
def load(build: BuildBase[Any]) -> None:
a = TypeVariable(kind=Star(), name='a')
t = TypeVariable(kind=Star() >> Star(), name='t')
t_a = TypeApplication(constructor=t, argument=a)
u32 = build.types['u32']
Sized = TypeClass('Sized', (t, ), methods={}, operators={})
has_sized_t = TypeClassConstraint(Sized, [t])
fn_t_a_u32 = ConstrainedExpr(
variables={t, a},
expr=build.type5_make_function([t_a, u32]),
constraints=(has_sized_t, ),
)
Sized.methods = {
'len': fn_t_a_u32,
}
build.register_type_class(Sized)
def wasm_dynamic_array_len(g: WasmGenerator, tv_map: dict[str, TypeExpr]) -> None:
del tv_map
# The length is stored in the first 4 bytes
g.i32.load()
def wasm_static_array_len(g: WasmGenerator, tv_map: dict[str, TypeExpr]) -> None:
sa_len = tv_map['n']
assert isinstance(sa_len, TypeLevelNat)
g.i32.const(sa_len.value)
def wasm(build: BuildBase[WasmGenerator]) -> None:
Sized = build.type_classes['Sized']
n = TypeVariable(kind=Nat(), name='n')
build.instance_type_class(Sized, build.dynamic_array_type5_constructor, methods={
'len': wasm_dynamic_array_len,
})
foo = TypeApplication(constructor=build.static_array_type5_constructor, argument=n)
build.instance_type_class(Sized, foo, methods={
'len': wasm_static_array_len,
})

View File

@ -0,0 +1,144 @@
"""
The Eq type class is defined for types that can be compered based on equality.
"""
from __future__ import annotations
from typing import Any
from ...type5.constrainedexpr import ConstrainedExpr
from ...type5.kindexpr import Nat, Star
from ...type5.typeexpr import TypeApplication, TypeExpr, TypeLevelNat, TypeVariable
from ...typeclass import TypeClass, TypeClassConstraint
from ...wasmgenerator import Generator as WasmGenerator
from ..base import BuildBase
def load(build: BuildBase[Any]) -> None:
a = TypeVariable(kind=Star(), name='a')
t = TypeVariable(kind=Star() >> Star(), name='t')
t_a = TypeApplication(constructor=t, argument=a)
u32 = build.types['u32']
Subscriptable = TypeClass('Subscriptable', (t, ), methods={}, operators={})
has_subscriptable_t = TypeClassConstraint(Subscriptable, [t])
fn_t_a_u32_a = ConstrainedExpr(
variables={t, a},
expr=build.type5_make_function([t_a, u32, a]),
constraints=(has_subscriptable_t, ),
)
Subscriptable.operators = {
'[]': fn_t_a_u32_a,
}
build.register_type_class(Subscriptable)
class SubscriptableCodeGenerator:
def __init__(self, build: BuildBase[WasmGenerator]) -> None:
self.build = build
def wasm_dynamic_array_getitem(self, g: WasmGenerator, tv_map: dict[str, TypeExpr]) -> None:
sa_type = tv_map['a']
u32_type_info = self.build.type_info_map['u32']
sa_type_info = self.build.type_info_map.get(sa_type.name)
if sa_type_info is None:
sa_type_info = self.build.type_info_constructed
getitem_adr = g.temp_var_t(u32_type_info.wasm_type, 'getitem_adr')
getitem_idx = g.temp_var_t(u32_type_info.wasm_type, 'getitem_idx')
# Stack: [varref: *ard, idx: u32]
g.local.set(getitem_idx)
# Stack: [varref: *ard]
g.local.set(getitem_adr)
# Stack: []
# Out of bounds check based on memory stored length
# Stack: []
g.local.get(getitem_idx)
# Stack: [idx: u32]
g.local.get(getitem_adr)
# Stack: [idx: u32, varref: *ard]
g.i32.load()
# Stack: [idx: u32, len: u32]
g.i32.ge_u()
# Stack: [res: bool]
with g.if_():
g.unreachable(comment='Out of bounds')
# Stack: []
g.local.get(getitem_adr)
# Stack: [varref: *ard]
g.i32.const(4)
# Stack: [varref: *ard, 4]
g.i32.add()
# Stack: [firstel: *ard]
g.local.get(getitem_idx)
# Stack: [firstel: *ard, idx: u32]
g.i32.const(sa_type_info.alloc_size)
# Stack: [firstel: *ard, idx: u32, as: u32]
g.i32.mul()
# Stack: [firstel: *ard, offset: u32]
g.i32.add()
# Stack: [eladr: *ard]
g.add_statement(sa_type_info.wasm_load_func)
# Stack: [el]
def wasm_static_array_getitem(self, g: WasmGenerator, tv_map: dict[str, TypeExpr]) -> None:
sa_type = tv_map['a']
sa_len = tv_map['n']
assert isinstance(sa_len, TypeLevelNat)
u32_type_info = self.build.type_info_map['u32']
sa_type_info = self.build.type_info_map.get(sa_type.name)
if sa_type_info is None:
sa_type_info = self.build.type_info_constructed
# OPTIMIZE: If index is a constant, we can use offset instead of multiply
# and we don't need to do the out of bounds check
getitem_idx = g.temp_var_t(u32_type_info.wasm_type, 'getitem_idx')
# Stack: [varref: *ard, idx: u32]
g.local.tee(getitem_idx)
# Stack: [varref: *ard, idx: u32]
# Out of bounds check based on sa_len.value
g.i32.const(sa_len.value)
# Stack: [varref: *ard, idx: u32, len: u32]
g.i32.ge_u()
# Stack: [varref: *ard, res: bool]
with g.if_():
g.unreachable(comment='Out of bounds')
# Stack: [varref: *ard]
g.local.get(getitem_idx)
# Stack: [varref: *ard, idx: u32]
g.i32.const(sa_type_info.alloc_size)
# Stack: [varref: *ard, idx: u32, as: u32]
g.i32.mul()
# Stack: [varref: *ard, offset: u32]
g.i32.add()
# Stack: [eladr: *ard]
g.add_statement(sa_type_info.wasm_load_func)
# Stack: [el]
def wasm(build: BuildBase[WasmGenerator]) -> None:
Subscriptable = build.type_classes['Subscriptable']
n = TypeVariable(kind=Nat(), name='n')
gen = SubscriptableCodeGenerator(build)
build.instance_type_class(Subscriptable, build.dynamic_array_type5_constructor, operators={
'[]': gen.wasm_dynamic_array_getitem,
})
foo = TypeApplication(constructor=build.static_array_type5_constructor, argument=n)
build.instance_type_class(Subscriptable, foo, operators={
'[]': gen.wasm_static_array_getitem,
})

View File

@ -0,0 +1,86 @@
from __future__ import annotations
from typing import Iterable
from ..type5.typeexpr import (
AtomicType,
TypeApplication,
TypeConstructor,
TypeExpr,
TypeVariable,
is_concrete,
)
class TypeClassRegistry[T]:
__slots__ = ('data', )
data: list[tuple[tuple[TypeExpr, ...], T]]
def __init__(self) -> None:
self.data = []
def add(self, arg_list: Iterable[TypeExpr], val: T) -> None:
self.data.append((tuple(arg_list), val))
def __contains__(self, arg_list: tuple[TypeExpr, ...]) -> bool:
for candidate, _ in self.data:
subsitutes = _matches(arg_list, candidate)
if subsitutes is not None:
return True
return False
def get(self, arg_list: tuple[TypeExpr, ...]) -> tuple[dict[str, TypeExpr], T] | None:
assert all(is_concrete(x) for x in arg_list)
for candidate, val in self.data:
subsitutes = _matches(arg_list, candidate)
if subsitutes is not None:
return (subsitutes, val)
return None
def _matches(lft_tpl: tuple[TypeExpr, ...], rgt_tpl: tuple[TypeExpr, ...]) -> dict[str, TypeExpr] | None:
subsitutes: dict[str, TypeExpr] = {}
for lft, rgt in zip(lft_tpl, rgt_tpl, strict=True):
new_substitutes = _matches_one(lft, rgt)
if new_substitutes is None:
return None
subsitutes.update(new_substitutes)
return subsitutes
def _matches_one(lft: TypeExpr, rgt: TypeExpr) -> dict[str, TypeExpr] | None:
if lft.kind != rgt.kind:
return None
if isinstance(rgt, TypeVariable):
assert not isinstance(lft, TypeVariable)
return _matches_one(rgt, lft)
if isinstance(lft, TypeVariable):
return {lft.name: rgt}
if isinstance(lft, (AtomicType, TypeConstructor, )):
if lft == rgt:
return {}
return None
if isinstance(lft, TypeApplication):
if not isinstance(rgt, TypeApplication):
return None
con_result = _matches_one(lft.constructor, rgt.constructor)
if con_result is None:
return None
arg_result = _matches_one(lft.argument, rgt.argument)
if arg_result is None:
return None
return con_result | arg_result
raise NotImplementedError(lft, rgt)

154
phasm/build/typerouter.py Normal file
View File

@ -0,0 +1,154 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from ..type5.record import Record
from ..type5.typeexpr import (
AtomicType,
TypeApplication,
TypeConstructor,
TypeExpr,
TypeLevelNat,
TypeVariable,
)
from ..type5.typerouter import TypeRouter
if TYPE_CHECKING:
from .base import BuildBase
class BuildTypeRouter[T](TypeRouter[T]):
"""
Extends the general type router with phasm builtin types.
Like functions, tuples, static and dynamic arrays.
"""
__slots__ = ('build', )
def __init__(self, build: BuildBase[Any]) -> None:
self.build = build
def when_application(self, typ: TypeApplication) -> T:
da_arg = self.build.type5_is_dynamic_array(typ)
if da_arg is not None:
return self.when_dynamic_array(da_arg)
fn_args = self.build.type5_is_function(typ)
if fn_args is not None:
return self.when_function(fn_args)
sa_args = self.build.type5_is_static_array(typ)
if sa_args is not None:
sa_len, sa_typ = sa_args
return self.when_static_array(sa_len, sa_typ)
tp_args = self.build.type5_is_tuple(typ)
if tp_args is not None:
return self.when_tuple(tp_args)
return self.when_application_other(typ)
def when_record(self, typ: Record) -> T:
return self.when_struct(typ)
def when_application_other(self, typ: TypeApplication) -> T:
raise NotImplementedError
def when_dynamic_array(self, da_arg: TypeExpr) -> T:
raise NotImplementedError
def when_function(self, fn_args: list[TypeExpr]) -> T:
raise NotImplementedError
def when_struct(self, typ: Record) -> T:
raise NotImplementedError
def when_static_array(self, sa_len: int, sa_typ: TypeExpr) -> T:
raise NotImplementedError
def when_tuple(self, tp_args: list[TypeExpr]) -> T:
raise NotImplementedError
class TypeName(BuildTypeRouter[str]):
"""
Router to generate a type's name.
Also serves an example implementation.
"""
__slots__ = ()
def when_application_other(self, typ: TypeApplication) -> str:
return typ.name
def when_atomic(self, typ: AtomicType) -> str:
return typ.name
def when_constructor(self, typ: TypeConstructor) -> str:
return typ.name
def when_dynamic_array(self, da_arg: TypeExpr) -> str:
if da_arg == self.build.u8_type5:
return 'bytes'
return self(da_arg) + '[...]'
def when_function(self, fn_args: list[TypeExpr]) -> str:
return 'Callable[' + ', '.join(map(self, fn_args)) + ']'
def when_static_array(self, sa_len: int, sa_typ: TypeExpr) -> str:
return f'{self(sa_typ)}[{sa_len}]'
def when_struct(self, typ: Record) -> str:
return typ.name
def when_tuple(self, tp_args: list[TypeExpr]) -> str:
return '(' + ', '.join(map(self, tp_args)) + ', )'
def when_type_level_nat(self, typ: TypeLevelNat) -> str:
return str(typ.value)
def when_variable(self, typ: TypeVariable) -> str:
return typ.name
class TypeAllocSize(BuildTypeRouter[int]):
"""
Router to generate a type's allocation size.
"""
__slots__ = ('is_member', )
is_member: bool
def __init__(self, build: BuildBase[Any], is_member: bool) -> None:
super().__init__(build)
self.is_member = is_member
def when_atomic(self, typ: AtomicType) -> int:
typ_info = self.build.type_info_map.get(typ.name)
if typ_info is None:
raise NotImplementedError(typ)
return typ_info.alloc_size
def when_dynamic_array(self, da_arg: TypeExpr) -> int:
if self.is_member:
return self.build.type_info_constructed.alloc_size
raise RuntimeError("Cannot know size of dynamic array at type level")
def when_static_array(self, sa_len: int, sa_typ: TypeExpr) -> int:
if self.is_member:
return self.build.type_info_constructed.alloc_size
raise NotImplementedError
def when_struct(self, typ: Record) -> int:
if self.is_member:
return self.build.type_info_constructed.alloc_size
return sum(map(self.build.type5_alloc_size_member, (x[1] for x in typ.fields)))
def when_tuple(self, tp_args: list[TypeExpr]) -> int:
if self.is_member:
return self.build.type_info_constructed.alloc_size
return sum(map(self.build.type5_alloc_size_member, tp_args))

View File

@ -3,54 +3,41 @@ This module generates source code based on the parsed AST
It's intented to be a "any color, as long as it's black" kind of renderer
"""
from typing import Generator, Optional
from typing import Any, Generator
from . import ourlang
from .type3.types import TYPE3_ASSERTION_ERROR, Type3, Type3OrPlaceholder
from .type5 import typeexpr as type5typeexpr
def phasm_render(inp: ourlang.Module) -> str:
def phasm_render(inp: ourlang.Module[Any]) -> str:
"""
Public method for rendering a Phasm module into Phasm code
"""
return module(inp)
Statements = Generator[str, None, None]
#
# def type_var(inp: Optional[typing.TypeVar]) -> str:
# """
# Render: type's name
# """
# assert inp is not None, typing.ASSERTION_ERROR
#
# mtyp = typing.simplify(inp)
# if mtyp is None:
# raise NotImplementedError(f'Rendering type {inp}')
#
# return mtyp
def type3(inp: Type3OrPlaceholder) -> str:
def type5(mod: ourlang.Module[Any], inp: type5typeexpr.TypeExpr) -> str:
"""
Render: type's name
"""
assert isinstance(inp, Type3), TYPE3_ASSERTION_ERROR
return mod.build.type5_name(inp)
return inp.name
def struct_definition(inp: ourlang.StructDefinition) -> str:
def struct_definition(mod: ourlang.Module[Any], inp: ourlang.StructDefinition) -> str:
"""
Render: TypeStruct's definition
"""
result = f'class {inp.struct_type3.name}:\n'
for mem, typ in inp.struct_type3.members.items():
result += f' {mem}: {type3(typ)}\n'
result = f'class {inp.struct_type5.name}:\n'
for mem, typ in inp.struct_type5.fields:
result += f' {mem}: {type5(mod, typ)}\n'
return result
def constant_definition(inp: ourlang.ModuleConstantDef) -> str:
def constant_definition(mod: ourlang.Module[Any], inp: ourlang.ModuleConstantDef) -> str:
"""
Render: Module Constant's definition
"""
return f'{inp.name}: {type3(inp.type3)} = {expression(inp.constant)}\n'
return f'{inp.name}: {type5(mod, inp.type5)} = {expression(inp.constant)}\n'
def expression(inp: ourlang.Expression) -> str:
"""
@ -61,32 +48,26 @@ def expression(inp: ourlang.Expression) -> str:
# could not fit in the given float type
return str(inp.value)
if isinstance(inp, ourlang.ConstantBytes):
return repr(inp.value)
if isinstance(inp, ourlang.ConstantTuple):
return '(' + ', '.join(
expression(x)
for x in inp.value
) + ', )'
if isinstance(inp, ourlang.ConstantStruct):
return inp.struct_type5.name + '(' + ', '.join(
expression(x)
for x in inp.value
) + ')'
if isinstance(inp, ourlang.VariableReference):
return str(inp.variable.name)
if isinstance(inp, ourlang.UnaryOp):
if (
inp.operator in ourlang.WEBASSEMBLY_BUILTIN_FLOAT_OPS
or inp.operator in ourlang.WEBASSEMBLY_BUILTIN_BYTES_OPS):
return f'{inp.operator}({expression(inp.right)})'
if inp.operator == 'cast':
mtyp = type3(inp.type3)
if mtyp is None:
raise NotImplementedError(f'Casting to type {inp.type_var}')
return f'{mtyp}({expression(inp.right)})'
return f'{inp.operator}{expression(inp.right)}'
if isinstance(inp, ourlang.BinaryOp):
return f'{expression(inp.left)} {inp.operator} {expression(inp.right)}'
return f'{expression(inp.left)} {inp.operator.name} {expression(inp.right)}'
if isinstance(inp, ourlang.FunctionCall):
args = ', '.join(
@ -95,14 +76,21 @@ def expression(inp: ourlang.Expression) -> str:
)
if isinstance(inp.function, ourlang.StructConstructor):
return f'{inp.function.struct_type3.name}({args})'
# TODO: Broken after new type system
# if isinstance(inp.function, ourlang.TupleConstructor):
# return f'({args}, )'
return f'{inp.function.struct_type5.name}({args})'
return f'{inp.function.name}({args})'
if isinstance(inp, ourlang.FunctionReference):
return str(inp.function.name)
if isinstance(inp, ourlang.TupleInstantiation):
args = ', '.join(
expression(arg)
for arg in inp.elements
)
return f'({args}, )'
if isinstance(inp, ourlang.Subscript):
varref = expression(inp.varref)
index = expression(inp.index)
@ -112,10 +100,6 @@ def expression(inp: ourlang.Expression) -> str:
if isinstance(inp, ourlang.AccessStructMember):
return f'{expression(inp.varref)}.{inp.member}'
if isinstance(inp, ourlang.Fold):
fold_name = 'foldl' if ourlang.Fold.Direction.LEFT == inp.dir else 'foldr'
return f'{fold_name}({inp.func.name}, {expression(inp.base)}, {expression(inp.iter)})'
raise NotImplementedError(expression, inp)
def statement(inp: ourlang.Statement) -> Statements:
@ -142,7 +126,7 @@ def statement(inp: ourlang.Statement) -> Statements:
raise NotImplementedError(statement, inp)
def function(inp: ourlang.Function) -> str:
def function(mod: ourlang.Module[Any], inp: ourlang.Function) -> str:
"""
Render: Function body
@ -155,12 +139,17 @@ def function(inp: ourlang.Function) -> str:
if inp.imported:
result += '@imported\n'
assert inp.type5 is not None
fn_args = mod.build.type5_is_function(inp.type5)
assert fn_args is not None
ret_type5 = fn_args.pop()
args = ', '.join(
f'{p.name}: {type3(p.type3)}'
for p in inp.posonlyargs
f'{arg_name}: {type5(mod, arg_type)}'
for arg_name, arg_type in zip(inp.arg_names, fn_args, strict=True)
)
result += f'def {inp.name}({args}) -> {type3(inp.returns_type3)}:\n'
result += f'def {inp.name}({args}) -> {type5(mod, ret_type5)}:\n'
if inp.imported:
result += ' pass\n'
@ -172,7 +161,7 @@ def function(inp: ourlang.Function) -> str:
return result
def module(inp: ourlang.Module) -> str:
def module(inp: ourlang.Module[Any]) -> str:
"""
Render: Module
"""
@ -181,20 +170,20 @@ def module(inp: ourlang.Module) -> str:
for struct in inp.struct_definitions.values():
if result:
result += '\n'
result += struct_definition(struct)
result += struct_definition(inp, struct)
for cdef in inp.constant_defs.values():
if result:
result += '\n'
result += constant_definition(cdef)
result += constant_definition(inp, cdef)
for func in inp.functions.values():
if func.lineno < 0:
# Builtin (-2) or auto generated (-1)
if isinstance(func, ourlang.StructConstructor):
# Auto generated
continue
if result:
result += '\n'
result += function(func)
result += function(inp, func)
return result

File diff suppressed because it is too large Load Diff

View File

@ -6,8 +6,3 @@ class StaticError(Exception):
"""
An error found during static analysis
"""
class TypingError(Exception):
"""
An error found during the typing phase
"""

View File

@ -0,0 +1,5 @@
# This folder contains some basic optimisations
# If you really want to optimise your stuff,
# checkout https://github.com/WebAssembly/binaryen
# It's wasm-opt tool can be run on the wat or wasm files.
# And they have a ton of options to optimize.

View File

@ -0,0 +1,49 @@
from phasm import wasm
def removeunusedfuncs(inp: wasm.Module) -> None:
"""
Removes functions that aren't exported and aren't
called by exported functions
"""
# First make a handy lookup table
function_map = {
x.name: x
for x in inp.functions
}
# Keep a list of all functions to retain
retain_functions = set()
# Keep a queue (stack) of the funtions we need to check
# The exported functions as well as the tabled functions are the
# first we know of to keep
to_check_functions = [
x
for x in inp.functions
if x.exported_name is not None
] + [
function_map[x]
for x in inp.table.values()
]
while to_check_functions:
# Check the next function. If it's on the list, we need to retain it
to_check_func = to_check_functions.pop()
retain_functions.add(to_check_func)
# Check all functions calls by this retaining function
to_check_functions.extend(
func
for stmt in to_check_func.statements
if isinstance(stmt, wasm.StatementCall)
# The function_map can not have the named function if it is an import
if (func := function_map.get(stmt.func_name)) is not None
and func not in retain_functions
)
inp.functions = [
func
for func in inp.functions
if func in retain_functions
]

View File

@ -1,28 +1,46 @@
"""
Contains the syntax tree for ourlang
"""
from typing import Dict, List, Optional, Union
from __future__ import annotations
import enum
from typing import Dict, Iterable, List, Optional, Union
from typing_extensions import Final
from .build.base import BuildBase
from .type5 import constrainedexpr as type5constrainedexpr
from .type5 import record as type5record
from .type5 import typeexpr as type5typeexpr
WEBASSEMBLY_BUILTIN_FLOAT_OPS: Final = ('abs', 'sqrt', 'ceil', 'floor', 'trunc', 'nearest', )
WEBASSEMBLY_BUILTIN_BYTES_OPS: Final = ('len', )
from .type3 import types as type3types
from .type3.types import Type3, Type3OrPlaceholder, PlaceholderForType, StructType3
class SourceRef:
__slots__ = ('filename', 'lineno', 'colno', )
filename: str | None
lineno: int | None
colno: int | None
def __init__(self, filename: str | None, lineno: int | None = None, colno: int | None = None) -> None:
self.filename = filename
self.lineno = lineno
self.colno = colno
def __repr__(self) -> str:
return f"SourceRef({self.filename!r}, {self.lineno!r}, {self.colno!r})"
def __str__(self) -> str:
return f"{self.filename}:{self.lineno:>4}:{self.colno:<3}"
class Expression:
"""
An expression within a statement
"""
__slots__ = ('type3', )
__slots__ = ('type5', 'sourceref', )
type3: Type3OrPlaceholder
sourceref: SourceRef
type5: type5typeexpr.TypeExpr | None
def __init__(self) -> None:
self.type3 = PlaceholderForType([self])
def __init__(self, *, sourceref: SourceRef) -> None:
self.sourceref = sourceref
self.type5 = None
class Constant(Expression):
"""
@ -38,29 +56,90 @@ class ConstantPrimitive(Constant):
"""
__slots__ = ('value', )
value: Union[int, float]
value: int | float
def __init__(self, value: Union[int, float]) -> None:
super().__init__()
def __init__(self, value: int | float, sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
self.value = value
def __repr__(self) -> str:
return f'ConstantPrimitive({repr(self.value)})'
class ConstantTuple(Constant):
class ConstantMemoryStored(Constant):
"""
An constant value expression within a statement
# FIXME: Rename to literal
"""
__slots__ = ('data_block', )
data_block: 'ModuleDataBlock'
def __init__(self, data_block: 'ModuleDataBlock', sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
self.data_block = data_block
class ConstantBytes(ConstantMemoryStored):
"""
A bytes constant value expression within a statement
"""
__slots__ = ('value', )
value: bytes
def __init__(self, value: bytes, data_block: 'ModuleDataBlock', sourceref: SourceRef) -> None:
super().__init__(data_block, sourceref=sourceref)
self.value = value
def __repr__(self) -> str:
# Do not repr the whole ModuleDataBlock
# As this has a reference back to this constant for its data
# which it needs to compile the data into the program
return f'ConstantBytes({repr(self.value)}, @{repr(self.data_block.address)})'
class ConstantTuple(ConstantMemoryStored):
"""
A Tuple constant value expression within a statement
"""
__slots__ = ('value', )
value: List[ConstantPrimitive]
value: List[Union[ConstantPrimitive, ConstantBytes, 'ConstantTuple', 'ConstantStruct']]
def __init__(self, value: List[ConstantPrimitive]) -> None: # FIXME: Tuple of tuples?
super().__init__()
def __init__(self, value: List[Union[ConstantPrimitive, ConstantBytes, 'ConstantTuple', 'ConstantStruct']], data_block: 'ModuleDataBlock', sourceref: SourceRef) -> None:
super().__init__(data_block, sourceref=sourceref)
self.value = value
def __repr__(self) -> str:
return f'ConstantTuple({repr(self.value)})'
# Do not repr the whole ModuleDataBlock
# As this has a reference back to this constant for its data
# which it needs to compile the data into the program
return f'ConstantTuple({repr(self.value)}, @{repr(self.data_block.address)})'
class ConstantStruct(ConstantMemoryStored):
"""
A Struct constant value expression within a statement
"""
__slots__ = ('struct_type5', 'value', )
struct_type5: type5record.Record
value: list[ConstantPrimitive | ConstantBytes | ConstantTuple | ConstantStruct]
def __init__(
self,
struct_type5: type5record.Record,
value: list[ConstantPrimitive | ConstantBytes | ConstantTuple | ConstantStruct],
data_block: 'ModuleDataBlock',
sourceref: SourceRef
) -> None:
super().__init__(data_block, sourceref=sourceref)
self.struct_type5 = struct_type5
self.value = value
def __repr__(self) -> str:
# Do not repr the whole ModuleDataBlock
# As this has a reference back to this constant for its data
# which it needs to compile the data into the program
return f'ConstantStruct({self.struct_type5!r}, {self.value!r}, @{self.data_block.address!r})'
class VariableReference(Expression):
"""
@ -70,57 +149,74 @@ class VariableReference(Expression):
variable: Union['ModuleConstantDef', 'FunctionParam'] # also possibly local
def __init__(self, variable: Union['ModuleConstantDef', 'FunctionParam']) -> None:
super().__init__()
def __init__(self, variable: Union['ModuleConstantDef', 'FunctionParam'], sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
self.variable = variable
class UnaryOp(Expression):
"""
A unary operator expression within a statement
"""
__slots__ = ('operator', 'right', )
operator: str
right: Expression
def __init__(self, operator: str, right: Expression) -> None:
super().__init__()
self.operator = operator
self.right = right
class BinaryOp(Expression):
"""
A binary operator expression within a statement
"""
__slots__ = ('operator', 'left', 'right', )
__slots__ = ('operator', 'polytype_substitutions', 'left', 'right', )
operator: str
operator: Function | FunctionParam
polytype_substitutions: dict[type5typeexpr.TypeVariable, type5typeexpr.TypeExpr]
left: Expression
right: Expression
def __init__(self, operator: str, left: Expression, right: Expression) -> None:
super().__init__()
def __init__(self, operator: Function | FunctionParam, left: Expression, right: Expression, sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
self.operator = operator
self.polytype_substitutions = {}
self.left = left
self.right = right
def __repr__(self) -> str:
return f'BinaryOp({repr(self.operator)}, {repr(self.left)}, {repr(self.right)})'
class FunctionCall(Expression):
"""
A function call expression within a statement
"""
__slots__ = ('function', 'arguments', )
__slots__ = ('function', 'polytype_substitutions', 'arguments', )
function: 'Function'
function: Function | FunctionParam
polytype_substitutions: dict[type5typeexpr.TypeVariable, type5typeexpr.TypeExpr]
arguments: List[Expression]
def __init__(self, function: 'Function') -> None:
super().__init__()
def __init__(self, function: Function | FunctionParam, sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
self.function = function
self.polytype_substitutions = {}
self.arguments = []
class FunctionReference(Expression):
"""
An function reference expression within a statement
"""
__slots__ = ('function', )
function: 'Function'
def __init__(self, function: 'Function', sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
self.function = function
class TupleInstantiation(Expression):
"""
Instantiation a tuple
"""
__slots__ = ('elements', )
elements: List[Expression]
def __init__(self, elements: List[Expression], sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
self.elements = elements
class Subscript(Expression):
"""
A subscript, for example to refer to a static array or tuple
@ -131,8 +227,8 @@ class Subscript(Expression):
varref: VariableReference
index: Expression
def __init__(self, varref: VariableReference, index: Expression) -> None:
super().__init__()
def __init__(self, varref: VariableReference, index: Expression, sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
self.varref = varref
self.index = index
@ -141,54 +237,27 @@ class AccessStructMember(Expression):
"""
Access a struct member for reading of writing
"""
__slots__ = ('varref', 'struct_type3', 'member', )
__slots__ = ('varref', 'member', )
varref: VariableReference
struct_type3: StructType3
member: str
def __init__(self, varref: VariableReference, struct_type3: StructType3, member: str) -> None:
super().__init__()
def __init__(self, varref: VariableReference, member: str, sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
self.varref = varref
self.struct_type3 = struct_type3
self.member = member
class Fold(Expression):
"""
A (left or right) fold
"""
class Direction(enum.Enum):
"""
Which direction to fold in
"""
LEFT = 0
RIGHT = 1
dir: Direction
func: 'Function'
base: Expression
iter: Expression
def __init__(
self,
dir_: Direction,
func: 'Function',
base: Expression,
iter_: Expression,
) -> None:
super().__init__()
self.dir = dir_
self.func = func
self.base = base
self.iter = iter_
class Statement:
"""
A statement within a function
"""
__slots__ = ()
__slots__ = ("sourceref", )
sourceref: SourceRef
def __init__(self, *, sourceref: SourceRef) -> None:
self.sourceref = sourceref
class StatementPass(Statement):
"""
@ -196,15 +265,23 @@ class StatementPass(Statement):
"""
__slots__ = ()
def __init__(self, sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
class StatementReturn(Statement):
"""
A return statement within a function
"""
__slots__ = ('value', )
def __init__(self, value: Expression) -> None:
def __init__(self, value: Expression, sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
self.value = value
def __repr__(self) -> str:
return f'StatementReturn({repr(self.value)})'
class StatementIf(Statement):
"""
An if statement within a function
@ -224,50 +301,60 @@ class FunctionParam:
"""
A parameter for a Function
"""
__slots__ = ('name', 'type3', )
__slots__ = ('name', 'type5', )
name: str
type3: Type3OrPlaceholder
type5: type5typeexpr.TypeExpr
def __init__(self, name: str, type5: type5typeexpr.TypeExpr) -> None:
assert type5typeexpr.is_concrete(type5)
def __init__(self, name: str, type3: Optional[Type3]) -> None:
self.name = name
self.type3 = PlaceholderForType([self]) if type3 is None else type3
self.type5 = type5
def __repr__(self) -> str:
return f'FunctionParam({self.name!r}, {self.type5!r})'
class Function:
"""
A function processes input and produces output
"""
__slots__ = ('name', 'lineno', 'exported', 'imported', 'statements', 'returns_type3', 'posonlyargs', )
__slots__ = ('name', 'sourceref', 'exported', 'imported', 'statements', 'type5', 'arg_names', )
name: str
lineno: int
sourceref: SourceRef
exported: bool
imported: bool
imported: Optional[str]
statements: List[Statement]
returns_type3: Type3
posonlyargs: List[FunctionParam]
type5: type5typeexpr.TypeExpr | type5constrainedexpr.ConstrainedExpr | None
arg_names: list[str]
def __init__(self, name: str, lineno: int) -> None:
def __init__(self, name: str, sourceref: SourceRef) -> None:
self.name = name
self.lineno = lineno
self.sourceref = sourceref
self.exported = False
self.imported = False
self.imported = None
self.statements = []
self.returns_type3 = type3types.none # FIXME: This could be a placeholder
self.posonlyargs = []
self.type5 = None
self.arg_names = []
class BuiltinFunction(Function):
def __init__(self, name: str, type5: type5typeexpr.TypeExpr | type5constrainedexpr.ConstrainedExpr) -> None:
super().__init__(name, SourceRef("/", 0, 0))
self.type5 = type5
class StructDefinition:
"""
The definition for a struct
"""
__slots__ = ('struct_type3', 'lineno', )
__slots__ = ('struct_type5', 'sourceref', )
struct_type3: StructType3
lineno: int
struct_type5: type5record.Record
sourceref: SourceRef
def __init__(self, struct_type3: StructType3, lineno: int) -> None:
self.struct_type3 = struct_type3
self.lineno = lineno
def __init__(self, struct_type5: type5record.Record, sourceref: SourceRef) -> None:
self.struct_type5 = struct_type5
self.sourceref = sourceref
class StructConstructor(Function):
"""
@ -276,59 +363,33 @@ class StructConstructor(Function):
A function will generated to instantiate a struct. The arguments
will be the defaults
"""
__slots__ = ('struct_type3', )
__slots__ = ('struct_type5', )
struct_type3: StructType3
struct_type5: type5record.Record
def __init__(self, struct_type3: StructType3) -> None:
super().__init__(f'@{struct_type3.name}@__init___@', -1)
def __init__(self, struct_type5: type5record.Record, sourceref: SourceRef) -> None:
super().__init__(f'@{struct_type5.name}@__init___@', sourceref)
self.struct_type5 = struct_type5
self.returns_type3 = struct_type3
for mem, typ in struct_type3.members.items():
self.posonlyargs.append(FunctionParam(mem, typ, ))
self.struct_type3 = struct_type3
# TODO: Broken after new type system
# class TupleConstructor(Function):
# """
# The constructor method for a tuple
# """
# __slots__ = ('tuple', )
#
# tuple: TypeTuple
#
# def __init__(self, tuple_: TypeTuple) -> None:
# name = tuple_.render_internal_name()
#
# super().__init__(f'@{name}@__init___@', -1)
#
# self.returns = tuple_
#
# for mem in tuple_.members:
# self.posonlyargs.append(FunctionParam(f'arg{mem.idx}', mem.type, ))
#
# self.tuple = tuple_
for mem, typ in struct_type5.fields:
self.arg_names.append(mem)
class ModuleConstantDef:
"""
A constant definition within a module
"""
__slots__ = ('name', 'lineno', 'type3', 'constant', 'data_block', )
__slots__ = ('name', 'sourceref', 'type5', 'constant', )
name: str
lineno: int
type3: Type3
sourceref: SourceRef
type5: type5typeexpr.TypeExpr
constant: Constant
data_block: Optional['ModuleDataBlock']
def __init__(self, name: str, lineno: int, type3: Type3, constant: Constant, data_block: Optional['ModuleDataBlock']) -> None:
def __init__(self, name: str, sourceref: SourceRef, type5: type5typeexpr.TypeExpr, constant: Constant) -> None:
self.name = name
self.lineno = lineno
self.type3 = type3
self.sourceref = sourceref
self.type5 = type5
self.constant = constant
self.data_block = data_block
class ModuleDataBlock:
"""
@ -336,13 +397,16 @@ class ModuleDataBlock:
"""
__slots__ = ('data', 'address', )
data: List[ConstantPrimitive]
data: List[Union[ConstantPrimitive, ConstantMemoryStored]]
address: Optional[int]
def __init__(self, data: List[ConstantPrimitive]) -> None:
self.data = data
def __init__(self, data: Iterable[Union[ConstantPrimitive, ConstantMemoryStored]]) -> None:
self.data = [*data]
self.address = None
def __repr__(self) -> str:
return f'ModuleDataBlock({self.data!r}, {self.address!r})'
class ModuleData:
"""
The data for when a module is loaded into memory
@ -354,19 +418,32 @@ class ModuleData:
def __init__(self) -> None:
self.blocks = []
class Module:
class Module[G]:
"""
A module is a file and consists of functions
"""
__slots__ = ('data', 'types', 'struct_definitions', 'constant_defs', 'functions',)
__slots__ = ('build', 'filename', 'data', 'types', 'type5s', 'struct_definitions', 'constant_defs', 'functions', 'methods', 'operators', 'functions_table', )
build: BuildBase[G]
filename: str
data: ModuleData
types: dict[str, type5typeexpr.TypeExpr]
struct_definitions: Dict[str, StructDefinition]
constant_defs: Dict[str, ModuleConstantDef]
functions: Dict[str, Function]
methods: Dict[str, type5typeexpr.TypeExpr | type5constrainedexpr.ConstrainedExpr]
operators: Dict[str, type5typeexpr.TypeExpr | type5constrainedexpr.ConstrainedExpr]
functions_table: dict[Function, int]
def __init__(self, build: BuildBase[G], filename: str) -> None:
self.build = build
self.filename = filename
def __init__(self) -> None:
self.data = ModuleData()
self.types = {}
self.struct_definitions = {}
self.constant_defs = {}
self.functions = {}
self.methods = {}
self.operators = {}
self.functions_table = {}

View File

@ -1,49 +1,84 @@
"""
Parses the source code from the plain text into a syntax tree
"""
import ast
from typing import Any, Dict, NoReturn, Union
import ast
from .type3 import types as type3types
from .build.base import BuildBase
from .build.default import BuildDefault
from .exceptions import StaticError
from .ourlang import (
WEBASSEMBLY_BUILTIN_FLOAT_OPS,
Module, ModuleDataBlock,
Function,
Expression,
AccessStructMember,
BinaryOp,
ConstantPrimitive, ConstantTuple,
FunctionCall, AccessStructMember, Subscript,
StructDefinition, StructConstructor,
# TupleConstructor,
UnaryOp, VariableReference,
Fold,
Statement,
StatementIf, StatementPass, StatementReturn,
BuiltinFunction,
ConstantBytes,
ConstantPrimitive,
ConstantStruct,
ConstantTuple,
Expression,
Function,
FunctionCall,
FunctionParam,
FunctionReference,
Module,
ModuleConstantDef,
ModuleDataBlock,
SourceRef,
Statement,
StatementIf,
StatementPass,
StatementReturn,
StructConstructor,
StructDefinition,
Subscript,
TupleInstantiation,
VariableReference,
)
from .type5 import typeexpr as type5typeexpr
from .wasmgenerator import Generator
def phasm_parse(source: str) -> Module:
def phasm_parse(source: str) -> Module[Generator]:
"""
Public method for parsing Phasm code into a Phasm Module
"""
res = ast.parse(source, '')
our_visitor = OurVisitor()
res = OptimizerTransformer().visit(res)
build = BuildDefault()
our_visitor = OurVisitor(build)
return our_visitor.visit_Module(res)
OurLocals = Dict[str, Union[FunctionParam]] # Also local variable and module constants?
OurLocals = Dict[str, Union[FunctionParam]] # FIXME: Does it become easier if we add ModuleConstantDef to this dict?
class OurVisitor:
class OptimizerTransformer(ast.NodeTransformer):
"""
This class optimizes the Python AST, to prepare it for parsing
by the OurVisitor class below.
"""
def visit_UnaryOp(self, node: ast.UnaryOp) -> Union[ast.UnaryOp, ast.Constant]:
"""
UnaryOp optimizations
In the given example:
```py
x = -4
```
Python will parse it as a unary minus operation on the constant four.
For Phasm purposes, this counts as a literal -4.
"""
if (
isinstance(node.op, (ast.UAdd, ast.USub, ))
and isinstance(node.operand, ast.Constant)
and isinstance(node.operand.value, (int, float, ))
):
if isinstance(node.op, ast.USub):
node.operand.value = -node.operand.value
return node.operand
return node
class OurVisitor[G]:
"""
Class to visit a Python syntax tree and create an ourlang syntax tree
@ -51,15 +86,22 @@ class OurVisitor:
At some point, we may deviate from Python syntax. If nothing else,
we probably won't keep up with the Python syntax changes.
See OptimizerTransformer for the changes we make after the Python
parsing is done but before the phasm parsing is done.
"""
# pylint: disable=C0103,C0116,C0301,R0201,R0912
def __init__(self) -> None:
pass
def __init__(self, build: BuildBase[G]) -> None:
self.build = build
def visit_Module(self, node: ast.Module) -> Module:
module = Module()
def visit_Module(self, node: ast.Module) -> Module[G]:
module = Module(self.build, "-")
module.methods.update({k: v[0] for k, v in self.build.methods.items()})
module.operators.update({k: v[0] for k, v in self.build.operators.items()})
module.types.update(self.build.types)
_not_implemented(not node.type_ignores, 'Module.type_ignores')
@ -71,25 +113,27 @@ class OurVisitor:
if isinstance(res, ModuleConstantDef):
if res.name in module.constant_defs:
raise StaticError(
f'{res.name} already defined on line {module.constant_defs[res.name].lineno}'
f'{res.name} already defined on line {module.constant_defs[res.name].sourceref.lineno}'
)
module.constant_defs[res.name] = res
if isinstance(res, StructDefinition):
if res.struct_type3.name in module.struct_definitions:
if res.struct_type5.name in module.types:
raise StaticError(
f'{res.struct_type3.name} already defined on line {module.struct_definitions[res.struct_type3.name].lineno}'
f'{res.struct_type5.name} already defined as type'
)
module.struct_definitions[res.struct_type3.name] = res
constructor = StructConstructor(res.struct_type3)
module.functions[constructor.name] = constructor
module.types[res.struct_type5.name] = res.struct_type5
module.functions[res.struct_type5.name] = StructConstructor(res.struct_type5, res.sourceref)
module.functions[res.struct_type5.name].type5 = module.build.type5_make_function([x[1] for x in res.struct_type5.fields] + [res.struct_type5])
# Store that the definition was done in this module for the formatter
module.struct_definitions[res.struct_type5.name] = res
if isinstance(res, Function):
if res.name in module.functions:
raise StaticError(
f'{res.name} already defined on line {module.functions[res.name].lineno}'
f'{res.name} already defined on line {module.functions[res.name].sourceref.lineno}'
)
module.functions[res.name] = res
@ -101,7 +145,7 @@ class OurVisitor:
return module
def pre_visit_Module_stmt(self, module: Module, node: ast.stmt) -> Union[Function, StructDefinition, ModuleConstantDef]:
def pre_visit_Module_stmt(self, module: Module[G], node: ast.stmt) -> Union[Function, StructDefinition, ModuleConstantDef]:
if isinstance(node, ast.FunctionDef):
return self.pre_visit_Module_FunctionDef(module, node)
@ -113,16 +157,25 @@ class OurVisitor:
raise NotImplementedError(f'{node} on Module')
def pre_visit_Module_FunctionDef(self, module: Module, node: ast.FunctionDef) -> Function:
function = Function(node.name, node.lineno)
def pre_visit_Module_FunctionDef(self, module: Module[G], node: ast.FunctionDef) -> Function:
function = Function(node.name, srf(module, node))
_not_implemented(not node.args.posonlyargs, 'FunctionDef.args.posonlyargs')
arg_type5_list = []
for arg in node.args.args:
function.posonlyargs.append(FunctionParam(
arg.arg,
self.visit_type(module, arg.annotation) if arg.annotation else None,
))
if arg.annotation is None:
_raise_static_error(node, 'Must give a argument type')
arg_type5 = self.visit_type5(module, arg.annotation)
arg_type5_list.append(arg_type5)
# FIXME: Allow TypeVariable in the function signature
# This would also require FunctionParam to accept a placeholder
function.arg_names.append(arg.arg)
_not_implemented(not node.args.vararg, 'FunctionDef.args.vararg')
_not_implemented(not node.args.kwonlyargs, 'FunctionDef.args.kwonlyargs')
@ -133,34 +186,52 @@ class OurVisitor:
# Do stmts at the end so we have the return value
for decorator in node.decorator_list:
if not isinstance(decorator, ast.Name):
_raise_static_error(decorator, 'Function decorators must be string')
if not isinstance(decorator.ctx, ast.Load):
_raise_static_error(decorator, 'Must be load context')
_not_implemented(decorator.id in ('exported', 'imported'), 'Custom decorators')
if isinstance(decorator, ast.Call):
if not isinstance(decorator.func, ast.Name):
_raise_static_error(decorator, 'Function decorators must be string')
if not isinstance(decorator.func.ctx, ast.Load):
_raise_static_error(decorator, 'Must be load context')
_not_implemented(decorator.func.id == 'imported', 'Custom decorators')
if decorator.id == 'exported':
function.exported = True
if 1 != len(decorator.args):
_raise_static_error(decorator, 'One argument expected')
if not isinstance(decorator.args[0], ast.Constant):
_raise_static_error(decorator, 'Service name must be a constant')
if not isinstance(decorator.args[0].value, str):
_raise_static_error(decorator, 'Service name must be a stirng')
if 0 != len(decorator.keywords): # TODO: Allow for namespace keyword
_raise_static_error(decorator, 'No keyword arguments expected')
function.imported = decorator.args[0].value
else:
function.imported = True
if not isinstance(decorator, ast.Name):
_raise_static_error(decorator, 'Function decorators must be string')
if not isinstance(decorator.ctx, ast.Load):
_raise_static_error(decorator, 'Must be load context')
_not_implemented(decorator.id in ('exported', 'imported'), 'Custom decorators')
if node.returns is not None: # Note: `-> None` would be a ast.Constant
function.returns_type3 = self.visit_type(module, node.returns)
else:
# Mostly works already, needs to fix Function.returns_type3 and have it updated
raise NotImplementedError('Function without an explicit return type')
if decorator.id == 'exported':
function.exported = True
else:
function.imported = 'imports'
if node.returns is None: # Note: `-> None` would be a ast.Constant
_raise_static_error(node, 'Must give a return type')
arg_type5_list.append(self.visit_type5(module, node.returns))
function.type5 = module.build.type5_make_function(arg_type5_list)
_not_implemented(not node.type_comment, 'FunctionDef.type_comment')
return function
def pre_visit_Module_ClassDef(self, module: Module, node: ast.ClassDef) -> StructDefinition:
def pre_visit_Module_ClassDef(self, module: Module[G], node: ast.ClassDef) -> StructDefinition:
_not_implemented(not node.bases, 'ClassDef.bases')
_not_implemented(not node.keywords, 'ClassDef.keywords')
_not_implemented(not node.decorator_list, 'ClassDef.decorator_list')
members: Dict[str, type3types.Type3] = {}
members: Dict[str, type5typeexpr.AtomicType | type5typeexpr.TypeApplication] = {}
for stmt in node.body:
if not isinstance(stmt, ast.AnnAssign):
@ -169,7 +240,7 @@ class OurVisitor:
if not isinstance(stmt.target, ast.Name):
raise NotImplementedError('Class with default values')
if not stmt.value is None:
if stmt.value is not None:
raise NotImplementedError('Class with default values')
if stmt.simple != 1:
@ -178,108 +249,60 @@ class OurVisitor:
if stmt.target.id in members:
_raise_static_error(stmt, 'Struct members must have unique names')
members[stmt.target.id] = self.visit_type(module, stmt.annotation)
field_type5 = self.visit_type5(module, stmt.annotation)
assert isinstance(field_type5, (type5typeexpr.AtomicType, type5typeexpr.TypeApplication, ))
members[stmt.target.id] = field_type5
return StructDefinition(type3types.StructType3(node.name, members), node.lineno)
return StructDefinition(
module.build.type5_make_struct(node.name, tuple(members.items())),
srf(module, node),
)
def pre_visit_Module_AnnAssign(self, module: Module, node: ast.AnnAssign) -> ModuleConstantDef:
def pre_visit_Module_AnnAssign(self, module: Module[G], node: ast.AnnAssign) -> ModuleConstantDef:
if not isinstance(node.target, ast.Name):
_raise_static_error(node, 'Must be name')
_raise_static_error(node.target, 'Must be name')
if not isinstance(node.target.ctx, ast.Store):
_raise_static_error(node, 'Must be load context')
_raise_static_error(node.target, 'Must be store context')
if isinstance(node.value, ast.Constant):
type3 = self.visit_type(module, node.annotation)
value_data = self.visit_Module_Constant(module, node.value)
return ModuleConstantDef(
node.target.id,
node.lineno,
type3,
self.visit_Module_Constant(module, node.value),
None,
srf(module, node),
self.visit_type5(module, node.annotation),
value_data,
)
if isinstance(node.value, ast.Tuple):
tuple_data = [
self.visit_Module_Constant(module, arg_node)
for arg_node in node.value.elts
if isinstance(arg_node, ast.Constant)
]
if len(node.value.elts) != len(tuple_data):
_raise_static_error(node, 'Tuple arguments must be constants')
value_data = self.visit_Module_Constant(module, node.value)
# Allocate the data
data_block = ModuleDataBlock(tuple_data)
module.data.blocks.append(data_block)
assert isinstance(value_data, ConstantTuple) # type hint
# Then return the constant as a pointer
return ModuleConstantDef(
node.target.id,
node.lineno,
self.visit_type(module, node.annotation),
ConstantTuple(tuple_data),
data_block,
srf(module, node),
self.visit_type5(module, node.annotation),
value_data,
)
raise NotImplementedError('TODO: Broken after new typing system')
if isinstance(node.value, ast.Call):
value_data = self.visit_Module_Constant(module, node.value)
# if isinstance(exp_type, TypeTuple):
# if not isinstance(node.value, ast.Tuple):
# _raise_static_error(node, 'Must be tuple')
#
# if len(exp_type.members) != len(node.value.elts):
# _raise_static_error(node, 'Invalid number of tuple values')
#
# tuple_data = [
# self.visit_Module_Constant(module, arg_node)
# for arg_node, mem in zip(node.value.elts, exp_type.members)
# if isinstance(arg_node, ast.Constant)
# ]
# if len(exp_type.members) != len(tuple_data):
# _raise_static_error(node, 'Tuple arguments must be constants')
#
# # Allocate the data
# data_block = ModuleDataBlock(tuple_data)
# module.data.blocks.append(data_block)
#
# # Then return the constant as a pointer
# return ModuleConstantDef(
# node.target.id,
# node.lineno,
# exp_type,
# ConstantTuple(tuple_data),
# data_block,
# )
#
# if isinstance(exp_type, TypeStaticArray):
# if not isinstance(node.value, ast.Tuple):
# _raise_static_error(node, 'Must be static array')
#
# if len(exp_type.members) != len(node.value.elts):
# _raise_static_error(node, 'Invalid number of static array values')
#
# static_array_data = [
# self.visit_Module_Constant(module, arg_node)
# for arg_node in node.value.elts
# if isinstance(arg_node, ast.Constant)
# ]
# if len(exp_type.members) != len(static_array_data):
# _raise_static_error(node, 'Static array arguments must be constants')
#
# # Allocate the data
# data_block = ModuleDataBlock(static_array_data)
# module.data.blocks.append(data_block)
#
# # Then return the constant as a pointer
# return ModuleConstantDef(
# node.target.id,
# node.lineno,
# ConstantStaticArray(static_array_data),
# data_block,
# )
#
# raise NotImplementedError(f'{node} on Module AnnAssign')
assert isinstance(value_data, ConstantStruct) # type hint
def visit_Module_stmt(self, module: Module, node: ast.stmt) -> None:
# Then return the constant as a pointer
return ModuleConstantDef(
node.target.id,
srf(module, node),
self.visit_type5(module, node.annotation),
value_data,
)
raise NotImplementedError(f'{node} on Module AnnAssign')
def visit_Module_stmt(self, module: Module[G], node: ast.stmt) -> None:
if isinstance(node, ast.FunctionDef):
self.visit_Module_FunctionDef(module, node)
return
@ -292,12 +315,17 @@ class OurVisitor:
raise NotImplementedError(f'{node} on Module')
def visit_Module_FunctionDef(self, module: Module, node: ast.FunctionDef) -> None:
def visit_Module_FunctionDef(self, module: Module[G], node: ast.FunctionDef) -> None:
function = module.functions[node.name]
assert function.type5 is not None
func_arg_types = module.build.type5_is_function(function.type5)
assert func_arg_types is not None
func_arg_types.pop()
our_locals: OurLocals = {
x.name: x
for x in function.posonlyargs
a_nam: FunctionParam(a_nam, a_typ)
for a_nam, a_typ in zip(function.arg_names, func_arg_types, strict=True)
}
for stmt in node.body:
@ -305,14 +333,15 @@ class OurVisitor:
self.visit_Module_FunctionDef_stmt(module, function, our_locals, stmt)
)
def visit_Module_FunctionDef_stmt(self, module: Module, function: Function, our_locals: OurLocals, node: ast.stmt) -> Statement:
def visit_Module_FunctionDef_stmt(self, module: Module[G], function: Function, our_locals: OurLocals, node: ast.stmt) -> Statement:
if isinstance(node, ast.Return):
if node.value is None:
# TODO: Implement methods without return values
_raise_static_error(node, 'Return must have an argument')
return StatementReturn(
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.value)
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.value),
srf(module, node),
)
if isinstance(node, ast.If):
@ -333,12 +362,14 @@ class OurVisitor:
return result
if isinstance(node, ast.Pass):
return StatementPass()
return StatementPass(srf(module, node))
raise NotImplementedError(f'{node} as stmt in FunctionDef')
def visit_Module_FunctionDef_expr(self, module: Module, function: Function, our_locals: OurLocals, node: ast.expr) -> Expression:
def visit_Module_FunctionDef_expr(self, module: Module[G], function: Function, our_locals: OurLocals, node: ast.expr) -> Expression:
if isinstance(node, ast.BinOp):
operator: str
if isinstance(node.op, ast.Add):
operator = '+'
elif isinstance(node.op, ast.Sub):
@ -347,6 +378,10 @@ class OurVisitor:
operator = '*'
elif isinstance(node.op, ast.Div):
operator = '/'
elif isinstance(node.op, ast.FloorDiv):
operator = '//'
elif isinstance(node.op, ast.Mod):
operator = '%'
elif isinstance(node.op, ast.LShift):
operator = '<<'
elif isinstance(node.op, ast.RShift):
@ -360,26 +395,14 @@ class OurVisitor:
else:
raise NotImplementedError(f'Operator {node.op}')
# Assume the type doesn't change when descending into a binary operator
# e.g. you can do `"hello" * 3` with the code below (yet)
if operator not in module.operators:
raise NotImplementedError(f'Operator {operator}')
return BinaryOp(
operator,
BuiltinFunction(operator, module.operators[operator]),
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.left),
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.right),
)
if isinstance(node, ast.UnaryOp):
if isinstance(node.op, ast.UAdd):
operator = '+'
elif isinstance(node.op, ast.USub):
operator = '-'
else:
raise NotImplementedError(f'Operator {node.op}')
return UnaryOp(
operator,
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.operand),
srf(module, node),
)
if isinstance(node, ast.Compare):
@ -388,20 +411,27 @@ class OurVisitor:
if isinstance(node.ops[0], ast.Gt):
operator = '>'
elif isinstance(node.ops[0], ast.GtE):
operator = '>='
elif isinstance(node.ops[0], ast.Eq):
operator = '=='
elif isinstance(node.ops[0], ast.NotEq):
operator = '!='
elif isinstance(node.ops[0], ast.Lt):
operator = '<'
elif isinstance(node.ops[0], ast.LtE):
operator = '<='
else:
raise NotImplementedError(f'Operator {node.ops}')
# Assume the type doesn't change when descending into a binary operator
# e.g. you can do `"hello" * 3` with the code below (yet)
if operator not in module.operators:
raise NotImplementedError(f'Operator {operator}')
return BinaryOp(
operator,
BuiltinFunction(operator, module.operators[operator]),
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.left),
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.comparators[0]),
srf(module, node),
)
if isinstance(node, ast.Call):
@ -428,40 +458,33 @@ class OurVisitor:
if node.id in our_locals:
param = our_locals[node.id]
return VariableReference(param)
return VariableReference(param, srf(module, node))
if node.id in module.constant_defs:
cdef = module.constant_defs[node.id]
return VariableReference(cdef)
return VariableReference(cdef, srf(module, node))
if node.id in module.functions:
fun = module.functions[node.id]
return FunctionReference(fun, srf(module, node))
_raise_static_error(node, f'Undefined variable {node.id}')
if isinstance(node, ast.Tuple):
raise NotImplementedError('TODO: Broken after new type system')
arguments = [
self.visit_Module_FunctionDef_expr(module, function, our_locals, arg_node)
for arg_node in node.elts
if isinstance(arg_node, (ast.Constant, ast.Tuple, ast.Call, ))
]
# if not isinstance(node.ctx, ast.Load):
# _raise_static_error(node, 'Must be load context')
#
# if isinstance(exp_type, TypeTuple):
# if len(exp_type.members) != len(node.elts):
# _raise_static_error(node, f'Expression is expecting a tuple of size {len(exp_type.members)}, but {len(node.elts)} are given')
#
# tuple_constructor = TupleConstructor(exp_type)
#
# func = module.functions[tuple_constructor.name]
#
# result = FunctionCall(func)
# result.arguments = [
# self.visit_Module_FunctionDef_expr(module, function, our_locals, mem.type, arg_node)
# for arg_node, mem in zip(node.elts, exp_type.members)
# ]
# return result
#
# _raise_static_error(node, f'Expression is expecting a {codestyle.type_(exp_type)}, not a tuple')
if len(arguments) != len(node.elts):
raise NotImplementedError('Non-constant tuple members')
return TupleInstantiation(arguments, srf(module, node))
raise NotImplementedError(f'{node} as expr in FunctionDef')
def visit_Module_FunctionDef_Call(self, module: Module, function: Function, our_locals: OurLocals, node: ast.Call) -> Union[Fold, FunctionCall, UnaryOp]:
def visit_Module_FunctionDef_Call(self, module: Module[G], function: Function, our_locals: OurLocals, node: ast.Call) -> Union[FunctionCall]:
if node.keywords:
_raise_static_error(node, 'Keyword calling not supported') # Yet?
@ -470,118 +493,47 @@ class OurVisitor:
if not isinstance(node.func.ctx, ast.Load):
_raise_static_error(node, 'Must be load context')
if node.func.id in module.struct_definitions:
struct_definition = module.struct_definitions[node.func.id]
struct_constructor = StructConstructor(struct_definition.struct_type3)
func: Union[Function, FunctionParam]
# FIXME: Defer struct de-allocation
func = module.functions[struct_constructor.name]
elif node.func.id in WEBASSEMBLY_BUILTIN_FLOAT_OPS:
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(
'sqrt',
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.args[0]),
)
elif node.func.id == 'u32':
if 1 != len(node.args):
_raise_static_error(node, f'Function {node.func.id} requires 1 arguments but {len(node.args)} are given')
# FIXME: This is a stub, proper casting is todo
return UnaryOp(
'cast',
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.args[0]),
)
elif node.func.id == 'len':
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(
'len',
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.args[0]),
)
elif node.func.id == 'foldl':
# TODO: This should a much more generic function!
# For development purposes, we're assuming you're doing a foldl(Callable[[u8, u8], u8], u8, bytes)
# In the future, we should probably infer the type of the second argument,
# and use it as expected types for the other u8s and the Iterable[u8] (i.e. bytes)
if 3 != len(node.args):
_raise_static_error(node, f'Function {node.func.id} requires 3 arguments but {len(node.args)} are given')
# TODO: This is not generic
subnode = node.args[0]
if not isinstance(subnode, ast.Name):
raise NotImplementedError(f'Calling methods that are not a name {subnode}')
if not isinstance(subnode.ctx, ast.Load):
_raise_static_error(subnode, 'Must be load context')
if subnode.id not in module.functions:
_raise_static_error(subnode, 'Reference to undefined function')
func = module.functions[subnode.id]
if 2 != len(func.posonlyargs):
_raise_static_error(node, f'Function {node.func.id} requires a function with 2 arguments but a function with {len(func.posonlyargs)} args is given')
raise NotImplementedError('TODO: Broken after new type system')
return Fold(
Fold.Direction.LEFT,
func,
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.args[1]),
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.args[2]),
)
if node.func.id in module.methods:
func = BuiltinFunction(node.func.id, module.methods[node.func.id])
elif node.func.id in our_locals:
func = our_locals[node.func.id]
else:
if node.func.id not in module.functions:
_raise_static_error(node, 'Call to undefined function')
func = module.functions[node.func.id]
if len(func.posonlyargs) != len(node.args):
_raise_static_error(node, f'Function {node.func.id} requires {len(func.posonlyargs)} arguments but {len(node.args)} are given')
result = FunctionCall(func)
result = FunctionCall(func, sourceref=srf(module, node))
result.arguments.extend(
self.visit_Module_FunctionDef_expr(module, function, our_locals, arg_expr)
for arg_expr, param in zip(node.args, func.posonlyargs)
for arg_expr in node.args
)
return result
def visit_Module_FunctionDef_Attribute(self, module: Module, function: Function, our_locals: OurLocals, node: ast.Attribute) -> Expression:
del module
del function
def visit_Module_FunctionDef_Attribute(self, module: Module[G], function: Function, our_locals: OurLocals, node: ast.Attribute) -> Expression:
if not isinstance(node.value, ast.Name):
_raise_static_error(node, 'Must reference a name')
if not isinstance(node.ctx, ast.Load):
_raise_static_error(node, 'Must be load context')
if not node.value.id in our_locals:
_raise_static_error(node, f'Undefined variable {node.value.id}')
param = our_locals[node.value.id]
node_typ = param.type3
if not isinstance(node_typ, type3types.StructType3):
_raise_static_error(node, f'Cannot take attribute of non-struct {node.value.id}')
member = node_typ.members.get(node.attr)
if member is None:
_raise_static_error(node, f'{node_typ.name} has no attribute {node.attr}')
varref = self.visit_Module_FunctionDef_expr(module, function, our_locals, node.value)
if not isinstance(varref, VariableReference):
_raise_static_error(node.value, 'Must refer to variable')
return AccessStructMember(
VariableReference(param),
node_typ,
varref,
node.attr,
srf(module, node),
)
def visit_Module_FunctionDef_Subscript(self, module: Module, function: Function, our_locals: OurLocals, node: ast.Subscript) -> Expression:
def visit_Module_FunctionDef_Subscript(self, module: Module[G], function: Function, our_locals: OurLocals, node: ast.Subscript) -> Expression:
if not isinstance(node.value, ast.Name):
_raise_static_error(node, 'Must reference a name')
if not isinstance(node.slice, ast.Index):
if isinstance(node.slice, ast.Slice):
_raise_static_error(node, 'Must subscript using an index')
if not isinstance(node.ctx, ast.Load):
@ -590,141 +542,150 @@ class OurVisitor:
varref: VariableReference
if node.value.id in our_locals:
param = our_locals[node.value.id]
varref = VariableReference(param)
varref = VariableReference(param, srf(module, node))
elif node.value.id in module.constant_defs:
constant_def = module.constant_defs[node.value.id]
varref = VariableReference(constant_def)
varref = VariableReference(constant_def, srf(module, node))
else:
_raise_static_error(node, f'Undefined variable {node.value.id}')
slice_expr = self.visit_Module_FunctionDef_expr(
module, function, our_locals, node.slice.value,
module, function, our_locals, node.slice,
)
return Subscript(varref, slice_expr)
return Subscript(varref, slice_expr, srf(module, node))
# if isinstance(node_typ, TypeBytes):
# if isinstance(varref, ModuleConstantReference):
# raise NotImplementedError(f'{node} from module constant')
#
# return AccessBytesIndex(
# varref,
# slice_expr,
# )
#
# if isinstance(node_typ, TypeTuple):
# if not isinstance(slice_expr, ConstantPrimitive):
# _raise_static_error(node, 'Must subscript using a constant index')
#
# idx = slice_expr.value
#
# if not isinstance(idx, int):
# _raise_static_error(node, 'Must subscript using a constant integer index')
#
# if not (0 <= idx < len(node_typ.members)):
# _raise_static_error(node, f'Index {idx} out of bounds for tuple {node.value.id}')
#
# tuple_member = node_typ.members[idx]
#
# if isinstance(varref, ModuleConstantReference):
# raise NotImplementedError(f'{node} from module constant')
#
# return AccessTupleMember(
# varref,
# tuple_member,
# )
#
# if isinstance(node_typ, TypeStaticArray):
# if not isinstance(slice_expr, ConstantPrimitive):
# return AccessStaticArrayMember(
# varref,
# node_typ,
# slice_expr,
# )
#
# idx = slice_expr.value
#
# if not isinstance(idx, int):
# _raise_static_error(node, 'Must subscript using an integer index')
#
# if not (0 <= idx < len(node_typ.members)):
# _raise_static_error(node, f'Index {idx} out of bounds for static array {node.value.id}')
#
# static_array_member = node_typ.members[idx]
#
# return AccessStaticArrayMember(
# varref,
# node_typ,
# static_array_member,
# )
#
# _raise_static_error(node, f'Cannot take index of {node_typ} {node.value.id}')
def visit_Module_Constant(self, module: Module[G], node: Union[ast.Constant, ast.Tuple, ast.Call]) -> Union[ConstantPrimitive, ConstantBytes, ConstantTuple, ConstantStruct]:
if isinstance(node, ast.Tuple):
tuple_data = [
self.visit_Module_Constant(module, arg_node)
for arg_node in node.elts
if isinstance(arg_node, (ast.Constant, ast.Tuple, ast.Call, ))
]
def visit_Module_Constant(self, module: Module, node: ast.Constant) -> ConstantPrimitive:
del module
if len(node.elts) != len(tuple_data):
_raise_static_error(node, 'Tuple arguments must be constants')
# Allocate the data
data_block = ModuleDataBlock(tuple_data)
module.data.blocks.append(data_block)
return ConstantTuple(tuple_data, data_block, srf(module, node))
if isinstance(node, ast.Call):
# Struct constant
# Stored in memory like a tuple, so much of the code is the same
if not isinstance(node.func, ast.Name):
_raise_static_error(node.func, 'Must be name')
if not isinstance(node.func.ctx, ast.Load):
_raise_static_error(node.func, 'Must be load context')
struct_def = module.struct_definitions.get(node.func.id)
if struct_def is None:
_raise_static_error(node.func, 'Undefined struct')
if node.keywords:
_raise_static_error(node.func, 'Cannot use keywords')
struct_data = [
self.visit_Module_Constant(module, arg_node)
for arg_node in node.args
if isinstance(arg_node, (ast.Constant, ast.Tuple, ast.Call, ))
]
if len(node.args) != len(struct_data):
_raise_static_error(node, 'Struct arguments must be constants')
data_block = ModuleDataBlock(struct_data)
module.data.blocks.append(data_block)
return ConstantStruct(struct_def.struct_type5, struct_data, data_block, srf(module, node))
_not_implemented(node.kind is None, 'Constant.kind')
if isinstance(node.value, (int, float, )):
return ConstantPrimitive(node.value)
return ConstantPrimitive(node.value, srf(module, node))
if isinstance(node.value, bytes):
data_block = ModuleDataBlock([])
module.data.blocks.append(data_block)
result = ConstantBytes(node.value, data_block, srf(module, node))
data_block.data.append(result)
return result
raise NotImplementedError(f'{node.value} as constant')
def visit_type(self, module: Module, node: ast.expr) -> type3types.Type3:
def visit_type5(self, module: Module[G], node: ast.expr) -> type5typeexpr.TypeExpr:
if isinstance(node, ast.Constant):
if node.value is None:
return type3types.none
return module.build.none_type5
_raise_static_error(node, f'Unrecognized type {node.value}')
_raise_static_error(node, f'Unrecognized type {node.value!r}')
if isinstance(node, ast.Name):
if not isinstance(node.ctx, ast.Load):
_raise_static_error(node, 'Must be load context')
if node.id in type3types.LOOKUP_TABLE:
return type3types.LOOKUP_TABLE[node.id]
if node.id in module.struct_definitions:
return module.struct_definitions[node.id].struct_type3
if node.id in module.types:
return module.types[node.id]
_raise_static_error(node, f'Unrecognized type {node.id}')
if isinstance(node, ast.Subscript):
if not isinstance(node.value, ast.Name):
_raise_static_error(node, 'Must be name')
if not isinstance(node.slice, ast.Index):
if isinstance(node.value, ast.Name) and node.value.id == 'Callable':
func_arg_types: list[ast.expr]
if isinstance(node.slice, ast.Name):
func_arg_types = [node.slice]
elif isinstance(node.slice, ast.Tuple):
func_arg_types = node.slice.elts
else:
_raise_static_error(node, 'Must subscript using a list of types')
return module.build.type5_make_function([
self.visit_type5(module, e)
for e in func_arg_types
])
if isinstance(node.slice, ast.Slice):
_raise_static_error(node, 'Must subscript using an index')
if not isinstance(node.slice.value, ast.Constant):
if not isinstance(node.slice, ast.Constant):
_raise_static_error(node, 'Must subscript using a constant index')
if not isinstance(node.slice.value.value, int):
if node.slice.value is Ellipsis:
return module.build.type5_make_dynamic_array(
self.visit_type5(module, node.value),
)
if not isinstance(node.slice.value, int):
_raise_static_error(node, 'Must subscript using a constant integer index')
if not isinstance(node.ctx, ast.Load):
_raise_static_error(node, 'Must be load context')
if node.value.id not in type3types.LOOKUP_TABLE: # FIXME: Tuple of tuples?
_raise_static_error(node, f'Unrecognized type {node.value.id}')
return type3types.AppliedType3(
type3types.static_array,
[self.visit_type(module, node.value)],
return module.build.type5_make_static_array(
node.slice.value,
self.visit_type5(module, node.value),
)
if isinstance(node, ast.Tuple):
if not isinstance(node.ctx, ast.Load):
_raise_static_error(node, 'Must be load context')
return type3types.AppliedType3(
type3types.tuple,
(self.visit_type(module, elt) for elt in node.elts)
return module.build.type5_make_tuple(
[self.visit_type5(module, elt) for elt in node.elts],
)
raise NotImplementedError(f'{node} as type')
raise NotImplementedError(node)
def _not_implemented(check: Any, msg: str) -> None:
if not check:
raise NotImplementedError(msg)
def _raise_static_error(node: Union[ast.mod, ast.stmt, ast.expr], msg: str) -> NoReturn:
def _raise_static_error(node: Union[ast.stmt, ast.expr], msg: str) -> NoReturn:
raise StaticError(
f'Static error on line {node.lineno}: {msg}'
)
def srf(mod: Module[Any], node: ast.stmt | ast.expr) -> SourceRef:
return SourceRef(mod.filename, node.lineno, node.col_offset)

View File

@ -1,7 +1,8 @@
"""
stdlib: Memory allocation
"""
from phasm.wasmgenerator import Generator, VarType_i32 as i32, func_wrapper
from phasm.wasmgenerator import Generator, func_wrapper
from phasm.wasmgenerator import VarType_i32 as i32
IDENTIFIER = 0xA1C0
@ -14,7 +15,7 @@ UNALLOC_PTR = ADR_UNALLOC_PTR + 4
# For memory initialization see phasm.compiler.module_data
@func_wrapper(exported=False)
@func_wrapper()
def __find_free_block__(g: Generator, alloc_size: i32) -> i32:
# Find out if we've freed any blocks at all so far
g.i32.const(ADR_FREE_BLOCK_PTR)
@ -31,7 +32,7 @@ def __find_free_block__(g: Generator, alloc_size: i32) -> i32:
return i32('return') # To satisfy mypy
@func_wrapper()
@func_wrapper(exported=True)
def __alloc__(g: Generator, alloc_size: i32) -> i32:
result = i32('result')

View File

@ -1,11 +1,13 @@
"""
stdlib: Standard types that are not wasm primitives
"""
from phasm.wasmgenerator import Generator, VarType_i32 as i32, func_wrapper
from phasm.stdlib import alloc
from phasm.wasmgenerator import Generator, func_wrapper
from phasm.wasmgenerator import VarType_i32 as i32
from phasm.wasmgenerator import VarType_i64 as i64
@func_wrapper()
@func_wrapper(exported=True)
def __alloc_bytes__(g: Generator, length: i32) -> i32:
"""
Allocates room for a bytes instance, but does not write
@ -32,35 +34,400 @@ def __alloc_bytes__(g: Generator, length: i32) -> i32:
return i32('return') # To satisfy mypy
@func_wrapper()
def __subscript_bytes__(g: Generator, adr: i32, ofs: i32) -> i32:
"""
Returns an index from a bytes value
If ofs is more than the length of the bytes, this
function returns 0, following the 'no undefined behaviour'
philosophy.
adr i32 The pointer for the allocated bytes
ofs i32 The offset within the allocated bytes
"""
g.local.get(ofs)
g.local.get(adr)
g.i32.load()
def __u32_min__(g: Generator, x: i32, y: i32) -> i32:
g.local.get(x)
g.local.get(y)
g.i32.lt_u()
with g.if_():
# The offset is less than the length
g.local.get(adr)
g.i32.const(4) # Bytes header
g.i32.add()
g.local.get(ofs)
g.i32.add()
g.i32.load8_u()
g.local.get(x)
g.return_()
# The offset is outside the allocated bytes
g.i32.const(0)
g.local.get(y)
g.return_()
return i32('return') # To satisfy mypy
@func_wrapper()
def __u64_min__(g: Generator, x: i64, y: i64) -> i64:
g.local.get(x)
g.local.get(y)
g.i64.lt_u()
with g.if_():
g.local.get(x)
g.return_()
g.local.get(y)
g.return_()
return i64('return') # To satisfy mypy
@func_wrapper()
def __i32_min__(g: Generator, x: i32, y: i32) -> i32:
g.local.get(x)
g.local.get(y)
g.i32.lt_s()
with g.if_():
g.local.get(x)
g.return_()
g.local.get(y)
g.return_()
return i32('return') # To satisfy mypy
@func_wrapper()
def __i64_min__(g: Generator, x: i64, y: i64) -> i64:
g.local.get(x)
g.local.get(y)
g.i64.lt_s()
with g.if_():
g.local.get(x)
g.return_()
g.local.get(y)
g.return_()
return i64('return') # To satisfy mypy
@func_wrapper()
def __u32_max__(g: Generator, x: i32, y: i32) -> i32:
g.local.get(x)
g.local.get(y)
g.i32.gt_u()
with g.if_():
g.local.get(x)
g.return_()
g.local.get(y)
g.return_()
return i32('return') # To satisfy mypy
@func_wrapper()
def __u64_max__(g: Generator, x: i64, y: i64) -> i64:
g.local.get(x)
g.local.get(y)
g.i64.gt_u()
with g.if_():
g.local.get(x)
g.return_()
g.local.get(y)
g.return_()
return i64('return') # To satisfy mypy
@func_wrapper()
def __i32_max__(g: Generator, x: i32, y: i32) -> i32:
g.local.get(x)
g.local.get(y)
g.i32.gt_s()
with g.if_():
g.local.get(x)
g.return_()
g.local.get(y)
g.return_()
return i32('return') # To satisfy mypy
@func_wrapper()
def __i64_max__(g: Generator, x: i64, y: i64) -> i64:
g.local.get(x)
g.local.get(y)
g.i64.gt_s()
with g.if_():
g.local.get(x)
g.return_()
g.local.get(y)
g.return_()
return i64('return') # To satisfy mypy
@func_wrapper()
def __i32_abs__(g: Generator, x: i32) -> i32:
# https://stackoverflow.com/a/14194764
y = i32('y')
# z = i32('z')
# y = x >> 31
g.local.get(x)
g.i32.const(31)
g.i32.shr_s() # Must be arithmetic shift
g.local.set(y)
# abs(x) = (x XOR y) - y
# (x XOR y)
g.local.get(x)
g.local.get(y)
g.i32.xor()
# - y
g.local.get(y)
g.i32.sub()
g.return_()
return i32('return') # To satisfy mypy
@func_wrapper()
def __i64_abs__(g: Generator, x: i64) -> i64:
# https://stackoverflow.com/a/14194764
y = i64('y')
# z = i64('z')
# y = x >> 31
g.local.get(x)
g.i64.const(31)
g.i64.shr_s() # Must be arithmetic shift
g.local.set(y)
# abs(x) = (x XOR y) - y
# (x XOR y)
g.local.get(x)
g.local.get(y)
g.i64.xor()
# - y
g.local.get(y)
g.i64.sub()
g.return_()
return i64('return') # To satisfy mypy
@func_wrapper()
def __u32_pow2__(g: Generator, x: i32) -> i32:
# 2^0 == 1
g.local.get(x)
g.i32.eqz()
with g.if_():
g.i32.const(1)
g.return_()
# 2 ^ x == 2 << (x - 1)
# (when x > 1)
g.i32.const(2)
g.local.get(x)
g.i32.const(1)
g.i32.sub()
g.i32.shl()
return i32('return') # To satisfy mypy
@func_wrapper()
def __u8_rotl__(g: Generator, x: i32, r: i32) -> i32:
s = i32('s') # The shifted part we need to overlay
# Handle cases where we need to shift more than 8 bits
g.local.get(r)
g.i32.const(8)
g.i32.rem_u()
g.local.set(r)
# Now do the rotation
g.local.get(x)
# 0000 0000 1100 0011
g.local.get(r)
# 0000 0000 1100 0011, 3
g.i32.shl()
# 0000 0110 0001 1000
g.local.tee(s)
# 0000 0110 0001 1000
g.i32.const(255)
# 0000 0110 0001 1000, 0000 0000 1111 1111
g.i32.and_()
# 0000 0000 0001 1000
g.local.get(s)
# 0000 0000 0001 1000, 0000 0110 0001 1000
g.i32.const(65280)
# 0000 0000 0001 1000, 0000 0110 0001 1000, 1111 1111 0000 0000
g.i32.and_()
# 0000 0000 0001 1000, 0000 0110 0000 0000
g.i32.const(8)
# 0000 0000 0001 1000, 0000 0110 0000 0000, 8
g.i32.shr_u()
# 0000 0000 0001 1000, 0000 0000 0000 0110
g.i32.or_()
# 0000 0000 0001 110
g.return_()
return i32('return') # To satisfy mypy
@func_wrapper()
def __u8_rotr__(g: Generator, x: i32, r: i32) -> i32:
s = i32('s') # The shifted part we need to overlay
# Handle cases where we need to shift more than 8 bits
g.local.get(r)
g.i32.const(8)
g.i32.rem_u()
g.local.set(r)
# Now do the rotation
g.local.get(x)
# 0000 0000 1100 0011
g.local.get(r)
# 0000 0000 1100 0011, 3
g.i32.rotr()
# 0110 0000 0000 0000 0000 0000 0001 1000
g.local.tee(s)
# 0110 0000 0000 0000 0000 0000 0001 1000
g.i32.const(255)
# 0110 0000 0000 0000 0000 0000 0001 1000, 0000 0000 1111 1111
g.i32.and_()
# 0000 0000 0000 0000 0000 0000 0001 1000
g.local.get(s)
# 0000 0000 0000 0000 0000 0000 0001 1000, 0110 0000 0000 0000 0000 0000 0001 1000
g.i32.const(4278190080)
# 0000 0000 0000 0000 0000 0000 0001 1000, 0110 0000 0000 0000 0000 0000 0001 1000, 1111 1111 0000 0000 0000 0000 0000 0000
g.i32.and_()
# 0000 0000 0000 0000 0000 0000 0001 1000, 0110 0000 0000 0000 0000 0000 0000 0000
g.i32.const(24)
# 0000 0000 0000 0000 0000 0000 0001 1000, 0110 0000 0000 0000 0000 0000 0000 0000, 24
g.i32.shr_u()
# 0000 0000 0000 0000 0000 0000 0001 1000, 0000 0000 0000 0000 0000 0000 0110 0000
g.i32.or_()
# 0000 0000 0000 0000 0000 0000 0111 1000
g.return_()
return i32('return') # To satisfy mypy
@func_wrapper()
def __u16_rotl__(g: Generator, x: i32, r: i32) -> i32:
s = i32('s') # The shifted part we need to overlay
# Handle cases where we need to shift more than 8 bits
g.local.get(r)
g.i32.const(8)
g.i32.rem_u()
g.local.set(r)
# Now do the rotation
g.local.get(x)
# 0x0000B2C3
g.local.get(r)
# 0x0000B2C3, 3
g.i32.shl()
# 0x00059618
g.local.tee(s)
# 0x00059618
g.i32.const(0xFFFF)
# 0x00059618, 0x0000FFFF
g.i32.and_()
# 0x00009618
g.local.get(s)
# 0x00009618, 0x00059618
g.i32.const(0xFFFF0000)
# 0x00009618, 0x00059618, 0xFFFF0000
g.i32.and_()
# 0x00009618, 0x00050000
g.i32.const(16)
# 0x00009618, 0x00050000, 16
g.i32.shr_u()
# 0x00009618, 0x00000005, 16
g.i32.or_()
# 0x0000961D
g.return_()
return i32('return') # To satisfy mypy
@func_wrapper()
def __u16_rotr__(g: Generator, x: i32, r: i32) -> i32:
s = i32('s') # The shifted part we need to overlay
# Handle cases where we need to shift more than 16 bits
g.local.get(r)
g.i32.const(16)
g.i32.rem_u()
g.local.set(r)
# Now do the rotation
g.local.get(x)
# 0x0000B2C3
g.local.get(r)
# 0x0000B2C3, 3
g.i32.rotr()
# 0x60001658
g.local.tee(s)
# 0x60001658
g.i32.const(0xFFFF)
# 0x60001658, 0x0000FFFF
g.i32.and_()
# 0x00001658
g.local.get(s)
# 0x00001658, 0x60001658
g.i32.const(0xFFFF0000)
# 0x00001658, 0x60001658, 0xFFFF0000
g.i32.and_()
# 0x00001658, 0x60000000
g.i32.const(16)
# 0x00001658, 0x60000000, 16
g.i32.shr_u()
# 0x00001658, 0x00006000
g.i32.or_()
# 0x00007658
g.return_()
return i32('return') # To satisfy mypy

View File

@ -1,326 +0,0 @@
"""
This module contains possible constraints generated based on the AST
These need to be resolved before the program can be compiled.
"""
from typing import Dict, Tuple, Union
from .. import ourlang
from . import types
class Error:
def __init__(self, msg: str) -> None:
self.msg = msg
def __repr__(self) -> str:
return f'Error({repr(self.msg)})'
class RequireTypeSubstitutes:
pass
CheckResult = Union[None, Error, RequireTypeSubstitutes]
SubstitutionMap = Dict[types.PlaceholderForType, types.Type3]
HumanReadableRet = Tuple[str, Dict[str, Union[str, ourlang.Expression, types.Type3, types.PlaceholderForType]]]
class Context:
"""
Context for constraints
"""
__slots__ = ()
class ConstraintBase:
"""
Base class for constraints
"""
__slots__ = ()
def check(self) -> CheckResult:
"""
Checks if the constraint hold, returning an error if it doesn't
"""
raise NotImplementedError
def get_new_placeholder_substitutes(self) -> SubstitutionMap:
"""
Returns any new placeholders that can be substituted for actual types
"""
return {}
def substitute_placeholders(self, smap: SubstitutionMap) -> None:
"""
Called with type substitutes, so you can update any placeholders
you may have with the known types. Note that this does not guarantee
that all types are known, you may still have some placeholders left.
"""
raise NotImplementedError(self, self.substitute_placeholders)
def human_readable(self) -> HumanReadableRet:
"""
Returns a more human readable form of this constraint
"""
return repr(self), {}
class SameTypeConstraint(ConstraintBase):
"""
Verifies that an expression has an expected type
"""
__slots__ = ('expected', 'actual', 'message', )
expected: types.Type3OrPlaceholder
actual: types.Type3OrPlaceholder
message: str
def __init__(self, expected: types.Type3OrPlaceholder, actual: types.Type3OrPlaceholder, message: str) -> None:
self.expected = expected
self.actual = actual
self.message = message
def check(self) -> CheckResult:
if isinstance(self.expected, types.PlaceholderForType) or isinstance(self.actual, types.PlaceholderForType):
return RequireTypeSubstitutes()
if self.expected is self.actual:
return None
return Error(f'{self.expected:s} must be {self.actual:s} instead')
def get_new_placeholder_substitutes(self) -> SubstitutionMap:
result: SubstitutionMap = {}
if isinstance(self.expected, types.Type3) and isinstance(self.actual, types.PlaceholderForType):
result = {
self.actual: self.expected
}
self.actual.get_substituted(self.expected)
self.actual = self.expected
if isinstance(self.actual, types.Type3) and isinstance(self.expected, types.PlaceholderForType):
result = {
self.expected: self.actual
}
self.expected.get_substituted(self.actual)
self.expected = self.actual
return result
def substitute_placeholders(self, smap: SubstitutionMap) -> None:
if isinstance(self.expected, types.PlaceholderForType) and self.expected in smap: # FIXME: Check recursive?
self.expected.get_substituted(smap[self.expected])
self.expected = smap[self.expected]
if isinstance(self.actual, types.PlaceholderForType) and self.actual in smap: # FIXME: Check recursive?
self.actual.get_substituted(smap[self.actual])
self.actual = smap[self.actual]
def human_readable(self) -> HumanReadableRet:
return (
'{expected} == {actual}',
{
'expected': self.expected,
'actual': self.actual,
'comment': self.message,
},
)
def __repr__(self) -> str:
return f'SameTypeConstraint({repr(self.expected)}, {repr(self.actual)}, {repr(self.message)})'
class MustImplementTypeClassConstraint(ConstraintBase):
"""
A type must implement a given type class
"""
__slots__ = ('type_class3', 'type3', )
type_class3: str
type3: types.Type3OrPlaceholder
def __init__(self, type_class3: str, type3: types.Type3OrPlaceholder) -> None:
self.type_class3 = type_class3
self.type3 = type3
def substitute_placeholders(self, smap: SubstitutionMap) -> None:
if isinstance(self.type3, types.PlaceholderForType) and self.type3 in smap: # FIXME: Check recursive?
self.type3.get_substituted(smap[self.type3])
self.type3 = smap[self.type3]
def check(self) -> CheckResult:
if isinstance(self.type3, types.PlaceholderForType):
return RequireTypeSubstitutes()
if 'BitWiseOr' == self.type_class3 and (self.type3 is types.u8 or self.type3 is types.u32 or self.type3 is types.u64):
return None
return Error(f'{self.type3.name} does not implement the {self.type_class3} type class')
def human_readable(self) -> HumanReadableRet:
return (
'{type3} derives {type_class3}',
{
'type_class3': self.type_class3,
'type3': self.type3,
},
)
def __repr__(self) -> str:
return f'MustImplementTypeClassConstraint({repr(self.type_class3)}, {repr(self.type3)})'
class LiteralFitsConstraint(ConstraintBase):
"""
A literal value fits a given type
"""
__slots__ = ('type3', 'literal', )
type3: types.Type3OrPlaceholder
literal: Union[ourlang.ConstantPrimitive, ourlang.ConstantTuple]
def __init__(self, type3: types.Type3OrPlaceholder, literal: Union[ourlang.ConstantPrimitive, ourlang.ConstantTuple]) -> None:
self.type3 = type3
self.literal = literal
def check(self) -> CheckResult:
int_table: Dict[str, Tuple[int, bool]] = {
'u8': (1, False),
'u32': (4, False),
'u64': (8, False),
'i8': (1, True),
'i32': (4, True),
'i64': (8, True),
}
float_table: Dict[str, None] = {
'f32': None,
'f64': None,
}
def _check(type3: types.Type3OrPlaceholder, literal: Union[ourlang.ConstantPrimitive, ourlang.ConstantTuple]) -> CheckResult:
if isinstance(type3, types.PlaceholderForType):
return RequireTypeSubstitutes()
val = literal.value
if type3.name in int_table:
bts, sgn = int_table[type3.name]
if isinstance(val, int):
try:
val.to_bytes(bts, 'big', signed=sgn)
except OverflowError:
return Error(f'Must fit in {bts} byte(s)') # FIXME: Add line information
return None
return Error('Must be integer') # FIXME: Add line information
if type3.name in float_table:
_ = float_table[type3.name]
if isinstance(val, float):
# FIXME: Bit check
return None
return Error('Must be real') # FIXME: Add line information
if isinstance(type3, types.AppliedType3) and type3.base is types.tuple:
if not isinstance(literal, ourlang.ConstantTuple):
return Error('Must be tuple')
assert isinstance(val, list) # type hint
if len(type3.args) != len(val):
return Error('Tuple element count mismatch')
for elt_typ, elt_lit in zip(type3.args, val):
res = _check(elt_typ, elt_lit)
if res is not None:
return res
return None
raise NotImplementedError
return _check(self.type3, self.literal)
def substitute_placeholders(self, smap: SubstitutionMap) -> None: # FIXME: Duplicate code
if isinstance(self.type3, types.PlaceholderForType) and self.type3 in smap: # FIXME: Check recursive?
self.type3.get_substituted(smap[self.type3])
self.type3 = smap[self.type3]
def human_readable(self) -> HumanReadableRet:
return (
'{literal} : {type3}',
{
'literal': self.literal,
'type3': self.type3,
},
)
def __repr__(self) -> str:
return f'LiteralFitsConstraint({repr(self.type3)}, {repr(self.literal)})'
class CanBeSubscriptedConstraint(ConstraintBase):
"""
A value that is subscipted, i.e. a[0] (tuple) or a[b] (static array)
"""
__slots__ = ('type3', 'index', 'index_type3', )
type3: types.Type3OrPlaceholder
index: ourlang.Expression
index_type3: types.Type3OrPlaceholder
def __init__(self, type3: types.Type3OrPlaceholder, index: ourlang.Expression) -> None:
self.type3 = type3
self.index = index
self.index_type3 = index.type3
def check(self) -> CheckResult:
if isinstance(self.type3, types.PlaceholderForType):
return RequireTypeSubstitutes()
if isinstance(self.index_type3, types.PlaceholderForType):
return RequireTypeSubstitutes()
if not isinstance(self.type3, types.AppliedType3):
return Error(f'Cannot subscript {self.type3:s}')
if self.type3.base is types.tuple:
return None
raise NotImplementedError(self.type3)
def get_new_placeholder_substitutes(self) -> SubstitutionMap:
if isinstance(self.type3, types.AppliedType3) and self.type3.base is types.tuple and isinstance(self.index_type3, types.PlaceholderForType):
return {
self.index_type3: types.u32,
}
return {}
def substitute_placeholders(self, smap: SubstitutionMap) -> None: # FIXME: Duplicate code
if isinstance(self.type3, types.PlaceholderForType) and self.type3 in smap: # FIXME: Check recursive?
self.type3.get_substituted(smap[self.type3])
self.type3 = smap[self.type3]
if isinstance(self.index_type3, types.PlaceholderForType) and self.index_type3 in smap: # FIXME: Check recursive?
self.index_type3.get_substituted(smap[self.index_type3])
self.index_type3 = smap[self.index_type3]
def human_readable(self) -> HumanReadableRet:
return (
'{type3}[{index}]',
{
'type3': self.type3,
'index': self.index,
},
)
def __repr__(self) -> str:
return f'CanBeSubscriptedConstraint({repr(self.type3)}, {repr(self.index)})'

View File

@ -1,97 +0,0 @@
"""
This module generates the typing constraints for Phasm.
The constraints solver can then try to resolve all constraints.
"""
from typing import Generator, List
from .. import ourlang
from .constraints import (
Context,
ConstraintBase,
CanBeSubscriptedConstraint,
LiteralFitsConstraint, MustImplementTypeClassConstraint, SameTypeConstraint,
)
def phasm_type3_generate_constraints(inp: ourlang.Module) -> List[ConstraintBase]:
ctx = Context()
return [*module(ctx, inp)]
def constant(ctx: Context, inp: ourlang.Constant) -> Generator[ConstraintBase, None, None]:
if isinstance(inp, (ourlang.ConstantPrimitive, ourlang.ConstantTuple, )):
yield LiteralFitsConstraint(inp.type3, inp)
return
raise NotImplementedError(constant, inp)
def expression(ctx: Context, inp: ourlang.Expression) -> Generator[ConstraintBase, None, None]:
if isinstance(inp, ourlang.Constant):
yield from constant(ctx, inp)
return
if isinstance(inp, ourlang.VariableReference):
yield SameTypeConstraint(inp.variable.type3, inp.type3, f'The type of a variable reference is the same as the type of variable {inp.variable.name}')
return
if isinstance(inp, ourlang.BinaryOp):
if '|' == inp.operator:
yield from expression(ctx, inp.left)
yield from expression(ctx, inp.right)
yield MustImplementTypeClassConstraint('BitWiseOr', inp.left.type3)
yield SameTypeConstraint(inp.right.type3, inp.left.type3, '(|) :: a -> a -> a')
yield SameTypeConstraint(inp.type3, inp.right.type3, '(|) :: a -> a -> a')
return
raise NotImplementedError(expression, inp)
if isinstance(inp, ourlang.FunctionCall):
yield SameTypeConstraint(inp.function.returns_type3, inp.type3, f'The type of a function call to {inp.function.name} is the same as the type that the function returns')
assert len(inp.arguments) == len(inp.function.posonlyargs) # FIXME: Make this a Constraint
for fun_arg, call_arg in zip(inp.function.posonlyargs, inp.arguments):
yield from expression(ctx, call_arg)
yield SameTypeConstraint(fun_arg.type3, call_arg.type3,
f'The type of the value passed to argument {fun_arg.name} of function {inp.function.name} should match the type of that argument')
return
if isinstance(inp, ourlang.Subscript):
yield from expression(ctx, inp.varref)
yield from expression(ctx, inp.index)
yield CanBeSubscriptedConstraint(inp.varref.type3, inp.index)
return
if isinstance(inp, ourlang.AccessStructMember):
yield SameTypeConstraint(inp.struct_type3.members[inp.member], inp.type3,
f'The type of a struct member reference is the same as the type of struct member {inp.struct_type3.name}.{inp.member}')
return
raise NotImplementedError(expression, inp)
def function(ctx: Context, inp: ourlang.Function) -> Generator[ConstraintBase, None, None]:
if isinstance(inp, ourlang.StructConstructor):
return
if len(inp.statements) != 1 or not isinstance(inp.statements[0], ourlang.StatementReturn):
raise NotImplementedError('Functions with not just a return statement')
yield from expression(ctx, inp.statements[0].value)
yield SameTypeConstraint(inp.returns_type3, inp.statements[0].value.type3, f'The type of the value returned from function {inp.name} should match its return type')
def module_constant_def(ctx: Context, inp: ourlang.ModuleConstantDef) -> Generator[ConstraintBase, None, None]:
yield from constant(ctx, inp.constant)
yield SameTypeConstraint(inp.type3, inp.constant.type3, f'The type of the value for module constant definition {inp.name} should match the type of that constant')
def module(ctx: Context, inp: ourlang.Module) -> Generator[ConstraintBase, None, None]:
for cdef in inp.constant_defs.values():
yield from module_constant_def(ctx, cdef)
for func in inp.functions.values():
yield from function(ctx, func)

View File

@ -1,103 +0,0 @@
"""
Entry point to the type3 system
"""
from typing import Dict, List
from .. import codestyle
from .. import ourlang
from .constraints import ConstraintBase, Error, RequireTypeSubstitutes, SameTypeConstraint, SubstitutionMap
from .constraintsgenerator import phasm_type3_generate_constraints
from .types import PlaceholderForType, Type3
MAX_RESTACK_COUNT = 100
class Type3Exception(BaseException):
"""
Thrown when the Type3 system detects constraints that do not hold
"""
def phasm_type3(inp: ourlang.Module, verbose: bool = False) -> None:
constraint_list = phasm_type3_generate_constraints(inp)
assert constraint_list
placeholder_substitutes: Dict[PlaceholderForType, Type3] = {}
placeholder_id_map: Dict[int, str] = {}
restack_counter = 0
error_list: List[Error] = []
while constraint_list:
if verbose:
print()
print_constraint_list(placeholder_id_map, constraint_list, placeholder_substitutes)
constraint = constraint_list.pop(0)
constraint.substitute_placeholders(placeholder_substitutes)
placeholder_substitutes.update(constraint.get_new_placeholder_substitutes())
check_result = constraint.check()
if check_result is None:
if verbose:
print('Constraint checks out')
continue
if isinstance(check_result, Error):
error_list.append(check_result)
if verbose:
print('Got an error')
continue
if isinstance(check_result, RequireTypeSubstitutes):
# FIXME: How to detect infinite loop? Is that necessary?
restack_counter += 1
if restack_counter > MAX_RESTACK_COUNT:
raise Exception('This looks like an infinite loop', constraint_list)
constraint_list.append(constraint)
if verbose:
print('Back on the todo list')
continue
raise NotImplementedError(constraint, check_result)
if error_list:
raise Type3Exception(error_list)
# TODO: Implement type substitution on the AST
def print_constraint(placeholder_id_map: Dict[int, str], constraint: ConstraintBase) -> None:
txt, fmt = constraint.human_readable()
act_fmt: Dict[str, str] = {}
for fmt_key, fmt_val in fmt.items():
if isinstance(fmt_val, ourlang.Expression):
fmt_val = codestyle.expression(fmt_val)
if isinstance(fmt_val, Type3):
fmt_val = fmt_val.name
if isinstance(fmt_val, PlaceholderForType):
placeholder_id = id(fmt_val)
if placeholder_id not in placeholder_id_map:
placeholder_id_map[placeholder_id] = 'T' + str(len(placeholder_id_map) + 1)
fmt_val = placeholder_id_map[placeholder_id]
if not isinstance(fmt_val, str):
fmt_val = repr(fmt_val)
act_fmt[fmt_key] = fmt_val
if 'comment' in act_fmt:
print('- ' + txt.format(**act_fmt).ljust(40) + '; ' + act_fmt['comment'])
else:
print('- ' + txt.format(**act_fmt))
def print_constraint_list(placeholder_id_map: Dict[int, str], constraint_list: List[ConstraintBase], placeholder_substitutes: SubstitutionMap) -> None:
print('=== v type3 constraint_list v === ')
for psk, psv in placeholder_substitutes.items():
print_constraint(placeholder_id_map, SameTypeConstraint(psk, psv, 'Deduced type'))
for constraint in constraint_list:
print_constraint(placeholder_id_map, constraint)
print('=== ^ type3 constraint_list ^ === ')

View File

@ -1,259 +0,0 @@
"""
Contains the final types for use in Phasm
These are actual, instantiated types; not the abstract types that the
constraint generator works with.
"""
from typing import Any, Dict, Iterable, List, Protocol, Union
TYPE3_ASSERTION_ERROR = 'You must call phasm_type3 after calling phasm_parse before you can call any other method'
class ExpressionProtocol(Protocol):
"""
A protocol for classes that should be updated on substitution
"""
type3: 'Type3OrPlaceholder'
"""
The type to update
"""
class Type3:
"""
Base class for the type3 types
"""
__slots__ = ('name', )
name: str
"""
The name of the string, as parsed and outputted by codestyle.
"""
def __init__(self, name: str) -> None:
self.name = name
def __repr__(self) -> str:
return f'Type3("{self.name}")'
def __str__(self) -> str:
return self.name
def __format__(self, format_spec: str) -> str:
if format_spec != 's':
raise TypeError(f'unsupported format string passed to Type3.__format__: {format_spec}')
return str(self)
def __eq__(self, other: Any) -> bool:
raise NotImplementedError
def __ne__(self, other: Any) -> bool:
raise NotImplementedError
def __hash__(self) -> int:
raise NotImplementedError
def __bool__(self) -> bool:
raise NotImplementedError
class PlaceholderForType:
"""
A placeholder type, for when we don't know the final type yet
"""
__slots__ = ('update_on_substitution', )
update_on_substitution: List[ExpressionProtocol]
def __init__(self, update_on_substitution: Iterable[ExpressionProtocol]) -> None:
self.update_on_substitution = [*update_on_substitution]
def get_substituted(self, result_type: Type3) -> None:
"""
Informs this Placeholder that it's getting substituted
This will also clear the update_on_substitution list
"""
for uos in self.update_on_substitution:
uos.type3 = result_type
self.update_on_substitution = []
@property
def name(self) -> str:
return f'T{id(self)}'
def __repr__(self) -> str:
uos = ', '.join(repr(x) for x in self.update_on_substitution)
return f'PlaceholderForType({id(self)}, [{uos}])'
def __str__(self) -> str:
return f'PhFT_{id(self)}'
def __format__(self, format_spec: str) -> str:
if format_spec != 's':
raise TypeError('unsupported format string passed to Type3.__format__')
return str(self)
def __eq__(self, other: Any) -> bool:
if not isinstance(other, PlaceholderForType):
raise NotImplementedError
return self is other
def __ne__(self, other: Any) -> bool:
raise NotImplementedError
def __hash__(self) -> int:
return 0 # Valid but performs badly
def __bool__(self) -> bool:
raise NotImplementedError
Type3OrPlaceholder = Union[Type3, PlaceholderForType]
class AppliedType3(Type3):
"""
A Type3 that has been applied to another type
"""
__slots__ = ('base', 'args', )
base: Type3
"""
The base type
"""
args: List[Type3OrPlaceholder]
"""
The applied types (or placeholders there for)
"""
def __init__(self, base: Type3, args: Iterable[Type3OrPlaceholder]) -> None:
args = [*args]
super().__init__(
base.name
+ ' ('
+ ') ('.join(str(x) for x in args) # FIXME: Do we need to redo the name on substitution?
+ ')'
)
self.base = base
self.args = args
def __repr__(self) -> str:
return f'AppliedType3({repr(self.base)}, {repr(self.args)})'
class StructType3(Type3):
"""
A Type3 struct with named members
"""
__slots__ = ('name', 'members', )
name: str
"""
The structs fully qualified name
"""
members: Dict[str, Type3]
"""
The struct's field definitions
"""
def __init__(self, name: str, members: Dict[str, Type3]) -> None:
super().__init__(name)
self.name = name
self.members = dict(members)
def __repr__(self) -> str:
return f'StructType3(repr({self.name}), repr({self.members}))'
none = Type3('none')
"""
The none type, for when functions simply don't return anything. e.g., IO().
"""
u8 = Type3('u8')
"""
The unsigned 8-bit integer type.
Operations on variables employ modular arithmetic, with modulus 2^8.
"""
u32 = Type3('u32')
"""
The unsigned 32-bit integer type.
Operations on variables employ modular arithmetic, with modulus 2^32.
"""
u64 = Type3('u64')
"""
The unsigned 64-bit integer type.
Operations on variables employ modular arithmetic, with modulus 2^64.
"""
i8 = Type3('i8')
"""
The signed 8-bit integer type.
Operations on variables employ modular arithmetic, with modulus 2^8, but
with the middel point being 0.
"""
i32 = Type3('i32')
"""
The unsigned 32-bit integer type.
Operations on variables employ modular arithmetic, with modulus 2^32, but
with the middel point being 0.
"""
i64 = Type3('i64')
"""
The unsigned 64-bit integer type.
Operations on variables employ modular arithmetic, with modulus 2^64, but
with the middel point being 0.
"""
f32 = Type3('f32')
"""
A 32-bits IEEE 754 float, of 32 bits width.
"""
f64 = Type3('f64')
"""
A 32-bits IEEE 754 float, of 64 bits width.
"""
static_array = Type3('static_array')
"""
This is a fixed length piece of memory that can be indexed at runtime.
It should be applied with one argument. It has a runtime-dynamic length
of the same type repeated.
"""
tuple = Type3('tuple') # pylint: disable=W0622
"""
This is a fixed length piece of memory.
It should be applied with zero or more arguments. It has a compile time
determined length, and each argument can be different.
"""
LOOKUP_TABLE: Dict[str, Type3] = {
'none': none,
'u8': u8,
'u32': u32,
'u64': u64,
'i8': i8,
'i32': i32,
'i64': i64,
'f32': f32,
'f64': f64,
}

0
phasm/type5/__init__.py Normal file
View File

View File

@ -0,0 +1,63 @@
"""
Some expression have constraints - those are usually the outmost expressions.
"""
from dataclasses import dataclass
from typing import Callable, Self
from .kindexpr import KindExpr
from .typeexpr import TypeExpr, TypeVariable, instantiate
class TypeConstraint:
"""
Base class for type constraints
"""
__slots__ = ()
def __str__(self) -> str:
raise NotImplementedError
def instantiate(self, known_map: dict[TypeVariable, TypeVariable]) -> Self:
raise NotImplementedError
@dataclass
class ConstrainedExpr:
"""
Also known as a polytype.
We only have rank 1 types, so this is always on the outside.
"""
variables: set[TypeVariable]
expr: TypeExpr
constraints: tuple[TypeConstraint, ...]
def __hash__(self) -> int:
return hash((
tuple(sorted(self.variables)),
self.expr,
self.constraints,
))
# TODO: Move instantiate here? it only makes sense for polytypes
def instantiate_constrained(
constrainedexpr: ConstrainedExpr,
make_variable: Callable[[KindExpr, str], TypeVariable],
) -> tuple[ConstrainedExpr, dict[TypeVariable, TypeVariable]]:
"""
Instantiates a type expression and its constraints
"""
# Would be handier if we had the list of variables on ConstrainedExpr
# So we don't have to pass make_variable
# This also helps with FunctionCall.substitutions
known_map = {
v: make_variable(v.kind, v.name)
for v in constrainedexpr.variables
}
expr = instantiate(constrainedexpr.expr, known_map)
constraints = tuple(
x.instantiate(known_map)
for x in constrainedexpr.constraints
)
return ConstrainedExpr(constrainedexpr.variables, expr, constraints), known_map

638
phasm/type5/constraints.py Normal file
View File

@ -0,0 +1,638 @@
from __future__ import annotations
import dataclasses
from typing import Any, Callable, Iterable, Protocol, Sequence, TypeAlias
from ..build.base import BuildBase
from ..ourlang import SourceRef
from ..wasm import WasmTypeFloat32, WasmTypeFloat64, WasmTypeInt32, WasmTypeInt64
from .kindexpr import KindExpr, Star
from .record import Record
from .typeexpr import (
AtomicType,
TypeApplication,
TypeConstructor,
TypeExpr,
TypeLevelNat,
TypeVariable,
is_concrete,
occurs,
replace_variable,
)
class ExpressionProtocol(Protocol):
"""
A protocol for classes that should be updated on substitution
"""
type5: TypeExpr | None
"""
The type to update
"""
PolytypeSubsituteMap: TypeAlias = dict[TypeVariable, TypeExpr]
class Context:
__slots__ = ("build", "placeholder_update", "ptst_update", )
build: BuildBase[Any]
placeholder_update: dict[TypeVariable, ExpressionProtocol | None]
ptst_update: dict[TypeVariable, tuple[PolytypeSubsituteMap, TypeVariable]]
def __init__(self, build: BuildBase[Any]) -> None:
self.build = build
self.placeholder_update = {}
self.ptst_update = {}
def make_placeholder(self, arg: ExpressionProtocol | None = None, kind: KindExpr = Star(), prefix: str = 'p') -> TypeVariable:
res = TypeVariable(kind, f"{prefix}_{len(self.placeholder_update)}")
self.placeholder_update[res] = arg
return res
def register_polytype_subsitutes(self, tvar: TypeVariable, arg: PolytypeSubsituteMap, orig_var: TypeVariable) -> None:
"""
When `tvar` gets subsituted, also set the result in arg with orig_var as key
e.g.
(-) :: Callable[a, a, a]
def foo() -> u32:
return 2 - 1
During typing, we instantiate a into a_3, and get the following constraints:
- u8 ~ p_1
- u8 ~ p_2
- Exists NatNum a_3
- Callable[a_3, a_3, a_3] ~ Callable[p_1, p_2, p_0]
- u8 ~ p_0
When we resolve a_3, then on the call to `-`, we should note that a_3 got resolved
to u32. But we need to use `a` as key, since that's what's used on the definition
"""
assert tvar in self.placeholder_update
assert tvar not in self.ptst_update
self.ptst_update[tvar] = (arg, orig_var)
class Success:
"""
The contsraint checks out.
Nothing new was learned and nothing new needs to be checked.
"""
def to_str(self, type_namer: Callable[[TypeExpr], str]) -> str:
return '(ok)'
class SkipForNow:
"""
Not enough information to resolve this constraint
"""
def to_str(self, type_namer: Callable[[TypeExpr], str]) -> str:
return '(skip for now)'
class ConstraintList(list['ConstraintBase']):
"""
A new list of constraints.
Sometimes, checking one constraint means you get a list of new contraints.
"""
def to_str(self, type_namer: Callable[[TypeExpr], str]) -> str:
return f'(got {len(self)} new constraints)'
@dataclasses.dataclass
class Failure:
"""
Both types are already different - cannot be unified.
"""
msg: str
def to_str(self, type_namer: Callable[[TypeExpr], str]) -> str:
return f'ERR: {self.msg}'
@dataclasses.dataclass
class ReplaceVariable:
"""
A variable should be replaced.
Either by another variable or by a (concrete) type.
"""
var: TypeVariable
typ: TypeExpr
def to_str(self, type_namer: Callable[[TypeExpr], str]) -> str:
return f'{{{self.var.name} := {type_namer(self.typ)}}}'
CheckResult: TypeAlias = Success | SkipForNow | ConstraintList | Failure | ReplaceVariable
def ok() -> CheckResult:
return Success()
def skip_for_now() -> CheckResult:
return SkipForNow()
def new_constraints(lst: Iterable[ConstraintBase]) -> CheckResult:
return ConstraintList(lst)
def fail(msg: str) -> CheckResult:
return Failure(msg)
def replace(var: TypeVariable, typ: TypeExpr) -> CheckResult:
return ReplaceVariable(var, typ)
class ConstraintBase:
__slots__ = ("ctx", "sourceref", )
ctx: Context
sourceref: SourceRef
def __init__(self, ctx: Context, sourceref: SourceRef) -> None:
self.ctx = ctx
self.sourceref = sourceref
def check(self) -> CheckResult:
raise NotImplementedError(self)
def complexity(self) -> int:
raise NotImplementedError
def replace_variable(self, var: TypeVariable, typ: TypeExpr) -> None:
pass
class FromLiteralInteger(ConstraintBase):
__slots__ = ('type5', 'literal', )
type5: TypeExpr
literal: int
def __init__(self, ctx: Context, sourceref: SourceRef, type5: TypeExpr, literal: int) -> None:
super().__init__(ctx, sourceref)
self.type5 = type5
self.literal = literal
def check(self) -> CheckResult:
if not is_concrete(self.type5):
return skip_for_now()
type_info = self.ctx.build.type_info_map.get(self.type5.name)
if type_info is None:
return fail('Cannot convert from literal integer')
if type_info.wasm_type is not WasmTypeInt32 and type_info.wasm_type is not WasmTypeInt64:
return fail('Cannot convert from literal integer')
assert type_info.signed is not None # type hint
if not type_info.signed and self.literal < 0:
return fail('May not be negative')
try:
self.literal.to_bytes(type_info.alloc_size, 'big', signed=type_info.signed)
except OverflowError:
return fail(f'Must fit in {type_info.alloc_size} byte(s)')
return ok()
def replace_variable(self, var: TypeVariable, typ: TypeExpr) -> None:
self.type5 = replace_variable(self.type5, var, typ)
def complexity(self) -> int:
return 100 + complexity(self.type5)
def __str__(self) -> str:
return f'FromLiteralInteger {self.ctx.build.type5_name(self.type5)} ~ {self.literal!r}'
class FromLiteralFloat(ConstraintBase):
__slots__ = ('type5', 'literal', )
type5: TypeExpr
literal: float
def __init__(self, ctx: Context, sourceref: SourceRef, type5: TypeExpr, literal: float) -> None:
super().__init__(ctx, sourceref)
self.type5 = type5
self.literal = literal
def check(self) -> CheckResult:
if not is_concrete(self.type5):
return skip_for_now()
type_info = self.ctx.build.type_info_map.get(self.type5.name)
if type_info is None:
return fail('Cannot convert from literal float')
if type_info.wasm_type is not WasmTypeFloat32 and type_info.wasm_type is not WasmTypeFloat64:
return fail('Cannot convert from literal float')
# TODO: Precision check
return ok()
def replace_variable(self, var: TypeVariable, typ: TypeExpr) -> None:
self.type5 = replace_variable(self.type5, var, typ)
def complexity(self) -> int:
return 100 + complexity(self.type5)
def __str__(self) -> str:
return f'FromLiteralFloat {self.ctx.build.type5_name(self.type5)} ~ {self.literal!r}'
class FromLiteralBytes(ConstraintBase):
__slots__ = ('type5', 'literal', )
type5: TypeExpr
literal: bytes
def __init__(self, ctx: Context, sourceref: SourceRef, type5: TypeExpr, literal: bytes) -> None:
super().__init__(ctx, sourceref)
self.type5 = type5
self.literal = literal
def check(self) -> CheckResult:
if not is_concrete(self.type5):
return skip_for_now()
da_arg = self.ctx.build.type5_is_dynamic_array(self.type5)
if da_arg is None or da_arg != self.ctx.build.u8_type5:
return fail('Cannot convert from literal bytes')
return ok()
def replace_variable(self, var: TypeVariable, typ: TypeExpr) -> None:
self.type5 = replace_variable(self.type5, var, typ)
def complexity(self) -> int:
return 100 + complexity(self.type5)
def __str__(self) -> str:
return f'FromLiteralBytes {self.ctx.build.type5_name(self.type5)} ~ {self.literal!r}'
class UnifyTypesConstraint(ConstraintBase):
__slots__ = ("lft", "rgt", "prefix", )
def __init__(self, ctx: Context, sourceref: SourceRef, lft: TypeExpr, rgt: TypeExpr, prefix: str | None = None) -> None:
super().__init__(ctx, sourceref)
self.lft = lft
self.rgt = rgt
self.prefix = prefix
def check(self) -> CheckResult:
lft = self.lft
rgt = self.rgt
if lft == self.rgt:
return ok()
if lft.kind != rgt.kind:
return fail("Kind mismatch")
if isinstance(lft, AtomicType) and isinstance(rgt, AtomicType):
return fail("Not the same type")
if isinstance(lft, AtomicType) and isinstance(rgt, TypeVariable):
return replace(rgt, lft)
if isinstance(lft, AtomicType) and isinstance(rgt, TypeConstructor):
raise NotImplementedError # Should have been caught by kind check above
if isinstance(lft, AtomicType) and isinstance(rgt, TypeApplication):
return fail("Not the same type" if is_concrete(rgt) else "Type shape mismatch")
if isinstance(lft, TypeVariable) and isinstance(rgt, AtomicType):
return replace(lft, rgt)
if isinstance(lft, TypeVariable) and isinstance(rgt, TypeVariable):
return replace(lft, rgt)
if isinstance(lft, TypeVariable) and isinstance(rgt, TypeConstructor):
return replace(lft, rgt)
if isinstance(lft, TypeVariable) and isinstance(rgt, TypeApplication):
if occurs(lft, rgt):
return fail("One type occurs in the other")
return replace(lft, rgt)
if isinstance(lft, TypeConstructor) and isinstance(rgt, AtomicType):
raise NotImplementedError # Should have been caught by kind check above
if isinstance(lft, TypeConstructor) and isinstance(rgt, TypeVariable):
return replace(rgt, lft)
if isinstance(lft, TypeConstructor) and isinstance(rgt, TypeConstructor):
return fail("Not the same type constructor")
if isinstance(lft, TypeConstructor) and isinstance(rgt, TypeApplication):
return fail("Not the same type constructor")
if isinstance(lft, TypeApplication) and isinstance(rgt, AtomicType):
return fail("Not the same type" if is_concrete(lft) else "Type shape mismatch")
if isinstance(lft, TypeApplication) and isinstance(rgt, TypeVariable):
if occurs(rgt, lft):
return fail("One type occurs in the other")
return replace(rgt, lft)
if isinstance(lft, TypeApplication) and isinstance(rgt, TypeConstructor):
return fail("Not the same type constructor")
if isinstance(lft, TypeApplication) and isinstance(rgt, TypeApplication):
## USABILITY HACK
## Often, we have two type applications in the same go
## If so, resolve it in a single step
## (Helps with debugging function unification)
## This *should* not affect the actual type unification
## It's just one less call to UnifyTypesConstraint.check
if isinstance(lft.constructor, TypeApplication) and isinstance(rgt.constructor, TypeApplication):
return new_constraints([
UnifyTypesConstraint(self.ctx, self.sourceref, lft.constructor.constructor, rgt.constructor.constructor),
UnifyTypesConstraint(self.ctx, self.sourceref, lft.constructor.argument, rgt.constructor.argument),
UnifyTypesConstraint(self.ctx, self.sourceref, lft.argument, rgt.argument),
])
return new_constraints([
UnifyTypesConstraint(self.ctx, self.sourceref, lft.constructor, rgt.constructor),
UnifyTypesConstraint(self.ctx, self.sourceref, lft.argument, rgt.argument),
])
raise NotImplementedError(lft, rgt)
def replace_variable(self, var: TypeVariable, typ: TypeExpr) -> None:
self.lft = replace_variable(self.lft, var, typ)
self.rgt = replace_variable(self.rgt, var, typ)
def complexity(self) -> int:
return complexity(self.lft) + complexity(self.rgt)
def __str__(self) -> str:
prefix = f'{self.prefix} :: ' if self.prefix else ''
return f"{prefix}{self.ctx.build.type5_name(self.lft)} ~ {self.ctx.build.type5_name(self.rgt)}"
class CanBeSubscriptedConstraint(ConstraintBase):
__slots__ = ('ret_type5', 'container_type5', 'index_type5', 'index_const', )
ret_type5: TypeExpr
container_type5: TypeExpr
index_type5: TypeExpr
index_const: int | None
def __init__(
self,
ctx: Context,
sourceref: SourceRef,
ret_type5: TypeExpr,
container_type5: TypeExpr,
index_type5: TypeExpr,
index_const: int | None,
) -> None:
super().__init__(ctx, sourceref)
self.ret_type5 = ret_type5
self.container_type5 = container_type5
self.index_type5 = index_type5
self.index_const = index_const
def check(self) -> CheckResult:
if not is_concrete(self.container_type5):
return skip_for_now()
tp_args = self.ctx.build.type5_is_tuple(self.container_type5)
if tp_args is not None:
if self.index_const is None:
return fail('Must index with integer literal')
if len(tp_args) <= self.index_const:
return fail('Tuple index out of range')
return new_constraints([
UnifyTypesConstraint(self.ctx, self.sourceref, tp_args[self.index_const], self.ret_type5),
UnifyTypesConstraint(self.ctx, self.sourceref, self.ctx.build.u32_type5, self.index_type5),
])
if not isinstance(self.container_type5, TypeApplication):
return fail('Missing type class instance')
return new_constraints([
TypeClassInstanceExistsConstraint(
self.ctx,
self.sourceref,
'Subscriptable',
(self.container_type5.constructor, ),
),
UnifyTypesConstraint(self.ctx, self.sourceref, self.container_type5.argument, self.ret_type5),
UnifyTypesConstraint(self.ctx, self.sourceref, self.ctx.build.u32_type5, self.index_type5),
])
def replace_variable(self, var: TypeVariable, typ: TypeExpr) -> None:
self.ret_type5 = replace_variable(self.ret_type5, var, typ)
self.container_type5 = replace_variable(self.container_type5, var, typ)
self.index_type5 = replace_variable(self.index_type5, var, typ)
def complexity(self) -> int:
return 100 + complexity(self.ret_type5) + complexity(self.container_type5) + complexity(self.index_type5)
def __str__(self) -> str:
return f"[] :: t a -> b -> a ~ {self.ctx.build.type5_name(self.container_type5)} -> {self.ctx.build.type5_name(self.index_type5)} -> {self.ctx.build.type5_name(self.ret_type5)}"
class CanAccessStructMemberConstraint(ConstraintBase):
__slots__ = ('ret_type5', 'struct_type5', 'member_name', )
ret_type5: TypeExpr
struct_type5: TypeExpr
member_name: str
def __init__(
self,
ctx: Context,
sourceref: SourceRef,
ret_type5: TypeExpr,
struct_type5: TypeExpr,
member_name: str,
) -> None:
super().__init__(ctx, sourceref)
self.ret_type5 = ret_type5
self.struct_type5 = struct_type5
self.member_name = member_name
def check(self) -> CheckResult:
if not is_concrete(self.struct_type5):
return skip_for_now()
st_args = self.ctx.build.type5_is_struct(self.struct_type5)
if st_args is None:
return fail('Must be a struct')
member_dict = dict(st_args)
if self.member_name not in member_dict:
return fail('Must have a field with this name')
return UnifyTypesConstraint(self.ctx, self.sourceref, self.ret_type5, member_dict[self.member_name]).check()
def replace_variable(self, var: TypeVariable, typ: TypeExpr) -> None:
self.ret_type5 = replace_variable(self.ret_type5, var, typ)
self.struct_type5 = replace_variable(self.struct_type5, var, typ)
def complexity(self) -> int:
return 100 + complexity(self.ret_type5) + complexity(self.struct_type5)
def __str__(self) -> str:
st_args = self.ctx.build.type5_is_struct(self.struct_type5)
member_dict = dict(st_args or [])
member_typ = member_dict.get(self.member_name)
if member_typ is None:
expect = 'a -> b'
else:
expect = f'{self.ctx.build.type5_name(self.struct_type5)} -> {self.ctx.build.type5_name(member_typ)}'
return f".{self.member_name} :: {expect} ~ {self.ctx.build.type5_name(self.struct_type5)} -> {self.ctx.build.type5_name(self.ret_type5)}"
class FromTupleConstraint(ConstraintBase):
__slots__ = ('ret_type5', 'member_type5_list', )
ret_type5: TypeExpr
member_type5_list: list[TypeExpr]
def __init__(
self,
ctx: Context,
sourceref: SourceRef,
ret_type5: TypeExpr,
member_type5_list: Sequence[TypeExpr],
) -> None:
super().__init__(ctx, sourceref)
self.ret_type5 = ret_type5
self.member_type5_list = list(member_type5_list)
def check(self) -> CheckResult:
if not is_concrete(self.ret_type5):
return skip_for_now()
da_arg = self.ctx.build.type5_is_dynamic_array(self.ret_type5)
if da_arg is not None:
return new_constraints([
UnifyTypesConstraint(self.ctx, self.sourceref, da_arg, x)
for x in self.member_type5_list
])
sa_args = self.ctx.build.type5_is_static_array(self.ret_type5)
if sa_args is not None:
sa_len, sa_typ = sa_args
if sa_len != len(self.member_type5_list):
return fail('Tuple element count mismatch')
return new_constraints([
UnifyTypesConstraint(self.ctx, self.sourceref, sa_typ, x)
for x in self.member_type5_list
])
tp_args = self.ctx.build.type5_is_tuple(self.ret_type5)
if tp_args is not None:
if len(tp_args) != len(self.member_type5_list):
return fail('Tuple element count mismatch')
return new_constraints([
UnifyTypesConstraint(self.ctx, self.sourceref, act_typ, exp_typ)
for act_typ, exp_typ in zip(tp_args, self.member_type5_list, strict=True)
])
raise NotImplementedError(self.ret_type5)
def replace_variable(self, var: TypeVariable, typ: TypeExpr) -> None:
self.ret_type5 = replace_variable(self.ret_type5, var, typ)
self.member_type5_list = [
replace_variable(x, var, typ)
for x in self.member_type5_list
]
def complexity(self) -> int:
return 100 + complexity(self.ret_type5) + sum(complexity(x) for x in self.member_type5_list)
def __str__(self) -> str:
args = ', '.join(self.ctx.build.type5_name(x) for x in self.member_type5_list)
return f'FromTuple {self.ctx.build.type5_name(self.ret_type5)} ~ ({args}, )'
class TypeClassInstanceExistsConstraint(ConstraintBase):
__slots__ = ('typeclass', 'arg_list', )
typeclass: str
arg_list: list[TypeExpr]
def __init__(
self,
ctx: Context,
sourceref: SourceRef,
typeclass: str,
arg_list: Sequence[TypeExpr]
) -> None:
super().__init__(ctx, sourceref)
self.typeclass = typeclass
self.arg_list = list(arg_list)
def check(self) -> CheckResult:
c_arg_list = [
x for x in self.arg_list if is_concrete(x)
]
if len(c_arg_list) != len(self.arg_list):
return skip_for_now()
if any(isinstance(x, Record) for x in c_arg_list):
# TODO: Allow users to implement type classes on their structs
return fail('Missing type class instance')
key = tuple(c_arg_list)
existing_instances = self.ctx.build.type_class_instances[self.typeclass]
if key in existing_instances:
return ok()
return fail('Missing type class instance')
def replace_variable(self, var: TypeVariable, typ: TypeExpr) -> None:
self.arg_list = [
replace_variable(x, var, typ)
for x in self.arg_list
]
def complexity(self) -> int:
return 100 + sum(complexity(x) for x in self.arg_list)
def __str__(self) -> str:
args = ' '.join(self.ctx.build.type5_name(x) for x in self.arg_list)
return f'Exists {self.typeclass} {args}'
def complexity(expr: TypeExpr) -> int:
if isinstance(expr, AtomicType | TypeLevelNat):
return 1
if isinstance(expr, TypeConstructor):
return 2
if isinstance(expr, TypeVariable):
return 5
if isinstance(expr, TypeApplication):
return complexity(expr.constructor) + complexity(expr.argument)
raise NotImplementedError(expr)

290
phasm/type5/fromast.py Normal file
View File

@ -0,0 +1,290 @@
from typing import Any, Generator
from .. import ourlang
from ..typeclass import TypeClassConstraint
from .constrainedexpr import ConstrainedExpr, instantiate_constrained
from .constraints import (
CanAccessStructMemberConstraint,
CanBeSubscriptedConstraint,
ConstraintBase,
Context,
FromLiteralBytes,
FromLiteralFloat,
FromLiteralInteger,
FromTupleConstraint,
TypeClassInstanceExistsConstraint,
UnifyTypesConstraint,
)
from .kindexpr import KindExpr
from .typeexpr import TypeApplication, TypeExpr, TypeVariable, is_concrete
ConstraintGenerator = Generator[ConstraintBase, None, None]
def phasm_type5_generate_constraints(ctx: Context, inp: ourlang.Module[Any]) -> list[ConstraintBase]:
return [*module(ctx, inp)]
def expression_constant_primitive(ctx: Context, inp: ourlang.ConstantPrimitive, phft: TypeVariable) -> ConstraintGenerator:
if isinstance(inp.value, int):
yield FromLiteralInteger(ctx, inp.sourceref, phft, inp.value)
return
if isinstance(inp.value, float):
yield FromLiteralFloat(ctx, inp.sourceref, phft, inp.value)
return
raise NotImplementedError(inp.value)
def expression_constant_bytes(ctx: Context, inp: ourlang.ConstantBytes, phft: TypeVariable) -> ConstraintGenerator:
yield FromLiteralBytes(ctx, inp.sourceref, phft, inp.value)
def expression_constant_tuple(ctx: Context, inp: ourlang.ConstantTuple, phft: TypeVariable) -> ConstraintGenerator:
member_type5_list = [
ctx.make_placeholder(arg)
for arg in inp.value
]
for arg, arg_phft in zip(inp.value, member_type5_list):
yield from expression(ctx, arg, arg_phft)
yield FromTupleConstraint(ctx, inp.sourceref, phft, member_type5_list)
def expression_constant_struct(ctx: Context, inp: ourlang.ConstantStruct, phft: TypeVariable) -> ConstraintGenerator:
member_type5_list = [
ctx.make_placeholder(arg)
for arg in inp.value
]
for arg, arg_phft in zip(inp.value, member_type5_list):
yield from expression(ctx, arg, arg_phft)
lft = ctx.build.type5_make_function([x[1] for x in inp.struct_type5.fields] + [inp.struct_type5])
rgt = ctx.build.type5_make_function(member_type5_list + [phft])
yield UnifyTypesConstraint(ctx, inp.sourceref, lft, rgt)
def expression_constant_memory_stored(ctx: Context, inp: ourlang.ConstantMemoryStored, phft: TypeVariable) -> ConstraintGenerator:
if isinstance(inp, ourlang.ConstantBytes):
yield from expression_constant_bytes(ctx, inp, phft)
return
if isinstance(inp, ourlang.ConstantTuple):
yield from expression_constant_tuple(ctx, inp, phft)
return
if isinstance(inp, ourlang.ConstantStruct):
yield from expression_constant_struct(ctx, inp, phft)
return
raise NotImplementedError(inp)
def expression_constant(ctx: Context, inp: ourlang.Constant, phft: TypeVariable) -> ConstraintGenerator:
if isinstance(inp, ourlang.ConstantPrimitive):
yield from expression_constant_primitive(ctx, inp, phft)
return
if isinstance(inp, ourlang.ConstantMemoryStored):
yield from expression_constant_memory_stored(ctx, inp, phft)
return
raise NotImplementedError(inp)
def expression_variable_reference(ctx: Context, inp: ourlang.VariableReference, phft: TypeVariable) -> ConstraintGenerator:
yield UnifyTypesConstraint(ctx, inp.sourceref, inp.variable.type5, phft, prefix=inp.variable.name)
def expression_binary_operator(ctx: Context, inp: ourlang.BinaryOp, phft: TypeVariable) -> ConstraintGenerator:
yield from _expression_binary_operator_or_function_call(
ctx,
inp.operator,
inp.polytype_substitutions,
[inp.left, inp.right],
inp.sourceref,
f'({inp.operator.name})',
phft,
)
def expression_function_call(ctx: Context, inp: ourlang.FunctionCall, phft: TypeVariable) -> ConstraintGenerator:
yield from _expression_binary_operator_or_function_call(
ctx,
inp.function,
inp.polytype_substitutions,
inp.arguments,
inp.sourceref,
inp.function.name,
phft,
)
def _expression_binary_operator_or_function_call(
ctx: Context,
function: ourlang.Function | ourlang.FunctionParam,
polytype_substitutions: dict[TypeVariable, TypeExpr],
arguments: list[ourlang.Expression],
sourceref: ourlang.SourceRef,
function_name: str,
phft: TypeVariable,
) -> ConstraintGenerator:
arg_typ_list = []
for arg in arguments:
arg_tv = ctx.make_placeholder(arg)
yield from expression(ctx, arg, arg_tv)
arg_typ_list.append(arg_tv)
def make_placeholder(x: KindExpr, p: str) -> TypeVariable:
return ctx.make_placeholder(kind=x, prefix=p)
ftp5 = function.type5
assert ftp5 is not None
if isinstance(ftp5, ConstrainedExpr):
ftp5, phft_lookup = instantiate_constrained(ftp5, make_placeholder)
for orig_tvar, tvar in phft_lookup.items():
ctx.register_polytype_subsitutes(tvar, polytype_substitutions, orig_tvar)
for type_constraint in ftp5.constraints:
if isinstance(type_constraint, TypeClassConstraint):
yield TypeClassInstanceExistsConstraint(ctx, sourceref, type_constraint.cls.name, type_constraint.variables)
continue
raise NotImplementedError(type_constraint)
ftp5 = ftp5.expr
else:
assert is_concrete(ftp5)
expr_type = ctx.build.type5_make_function(arg_typ_list + [phft])
yield UnifyTypesConstraint(ctx, sourceref, ftp5, expr_type, prefix=function_name)
def expression_function_reference(ctx: Context, inp: ourlang.FunctionReference, phft: TypeVariable) -> ConstraintGenerator:
assert inp.function.type5 is not None # Todo: Make not nullable
ftp5 = inp.function.type5
if isinstance(ftp5, ConstrainedExpr):
ftp5 = ftp5.expr
yield UnifyTypesConstraint(ctx, inp.sourceref, ftp5, phft, prefix=inp.function.name)
def expression_tuple_instantiation(ctx: Context, inp: ourlang.TupleInstantiation, phft: TypeVariable) -> ConstraintGenerator:
arg_typ_list = []
for arg in inp.elements:
arg_tv = ctx.make_placeholder(arg)
yield from expression(ctx, arg, arg_tv)
arg_typ_list.append(arg_tv)
yield FromTupleConstraint(ctx, inp.sourceref, phft, arg_typ_list)
def expression_subscript(ctx: Context, inp: ourlang.Subscript, phft: TypeVariable) -> ConstraintGenerator:
varref_phft = ctx.make_placeholder(inp.varref)
index_phft = ctx.make_placeholder(inp.index)
yield from expression(ctx, inp.varref, varref_phft)
yield from expression(ctx, inp.index, index_phft)
if isinstance(inp.index, ourlang.ConstantPrimitive) and isinstance(inp.index.value, int):
yield CanBeSubscriptedConstraint(ctx, inp.sourceref, phft, varref_phft, index_phft, inp.index.value)
else:
yield CanBeSubscriptedConstraint(ctx, inp.sourceref, phft, varref_phft, index_phft, None)
def expression_access_struct_member(ctx: Context, inp: ourlang.AccessStructMember, phft: TypeVariable) -> ConstraintGenerator:
varref_phft = ctx.make_placeholder(inp.varref)
yield from expression_variable_reference(ctx, inp.varref, varref_phft)
yield CanAccessStructMemberConstraint(ctx, inp.sourceref, phft, varref_phft, inp.member)
def expression(ctx: Context, inp: ourlang.Expression, phft: TypeVariable) -> ConstraintGenerator:
if isinstance(inp, ourlang.Constant):
yield from expression_constant(ctx, inp, phft)
return
if isinstance(inp, ourlang.VariableReference):
yield from expression_variable_reference(ctx, inp, phft)
return
if isinstance(inp, ourlang.BinaryOp):
yield from expression_binary_operator(ctx, inp, phft)
return
if isinstance(inp, ourlang.FunctionCall):
yield from expression_function_call(ctx, inp, phft)
return
if isinstance(inp, ourlang.FunctionReference):
yield from expression_function_reference(ctx, inp, phft)
return
if isinstance(inp, ourlang.TupleInstantiation):
yield from expression_tuple_instantiation(ctx, inp, phft)
return
if isinstance(inp, ourlang.Subscript):
yield from expression_subscript(ctx, inp, phft)
return
if isinstance(inp, ourlang.AccessStructMember):
yield from expression_access_struct_member(ctx, inp, phft)
return
raise NotImplementedError(inp)
def statement_return(ctx: Context, fun: ourlang.Function, inp: ourlang.StatementReturn) -> ConstraintGenerator:
phft = ctx.make_placeholder(inp.value)
if fun.type5 is None:
raise NotImplementedError("Deducing function type - you'll have to annotate it.")
if isinstance(fun.type5, TypeApplication):
args = ctx.build.type5_is_function(fun.type5)
assert args is not None
type5 = args[-1]
else:
type5 = fun.type5.expr if isinstance(fun.type5, ConstrainedExpr) else fun.type5
yield from expression(ctx, inp.value, phft)
yield UnifyTypesConstraint(ctx, inp.sourceref, type5, phft, prefix=f'{fun.name}(...)')
def statement_if(ctx: Context, fun: ourlang.Function, inp: ourlang.StatementIf) -> ConstraintGenerator:
test_phft = ctx.make_placeholder(inp.test)
yield from expression(ctx, inp.test, test_phft)
yield UnifyTypesConstraint(ctx, inp.test.sourceref, test_phft, ctx.build.bool_type5)
for stmt in inp.statements:
yield from statement(ctx, fun, stmt)
for stmt in inp.else_statements:
yield from statement(ctx, fun, stmt)
def statement(ctx: Context, fun: ourlang.Function, inp: ourlang.Statement) -> ConstraintGenerator:
if isinstance(inp, ourlang.StatementReturn):
yield from statement_return(ctx, fun, inp)
return
if isinstance(inp, ourlang.StatementIf):
yield from statement_if(ctx, fun, inp)
return
raise NotImplementedError(inp)
def function(ctx: Context, inp: ourlang.Function) -> ConstraintGenerator:
for stmt in inp.statements:
yield from statement(ctx, inp, stmt)
def module_constant_def(ctx: Context, inp: ourlang.ModuleConstantDef) -> ConstraintGenerator:
phft = ctx.make_placeholder(inp.constant)
yield from expression_constant(ctx, inp.constant, phft)
yield UnifyTypesConstraint(ctx, inp.sourceref, inp.type5, phft)
def module(ctx: Context, inp: ourlang.Module[Any]) -> ConstraintGenerator:
for cdef in inp.constant_defs.values():
yield from module_constant_def(ctx, cdef)
for func in inp.functions.values():
if func.imported:
continue
yield from function(ctx, func)
# TODO: Generalize?

57
phasm/type5/kindexpr.py Normal file
View File

@ -0,0 +1,57 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TypeAlias
@dataclass
class Star:
def __rshift__(self, other: KindExpr) -> Arrow:
return Arrow(self, other)
def __str__(self) -> str:
return "*"
def __hash__(self) -> int:
return 0 # All Stars are the same
@dataclass
class Nat:
def __rshift__(self, other: KindExpr) -> Arrow:
return Arrow(self, other)
def __str__(self) -> str:
return "Nat"
def __hash__(self) -> int:
return 0 # All Stars are the same
@dataclass
class Arrow:
"""
Represents an arrow kind `K1 -> K2`.
To create K1 -> K2 -> K3, pass an Arrow for result_kind.
For now, we do not support Arrows as arguments (i.e.,
no higher order kinds).
"""
arg_kind: Star | Nat
result_kind: KindExpr
def __str__(self) -> str:
if isinstance(self.arg_kind, Star):
arg_kind = "*"
else:
arg_kind = f"({str(self.arg_kind)})"
if isinstance(self.result_kind, Star):
result_kind = "*"
else:
result_kind = f"({str(self.result_kind)})"
return f"{arg_kind} -> {result_kind}"
def __hash__(self) -> int:
return hash((self.arg_kind, self.result_kind, ))
KindExpr: TypeAlias = Star | Nat | Arrow

43
phasm/type5/record.py Normal file
View File

@ -0,0 +1,43 @@
from dataclasses import dataclass
from .kindexpr import Star
from .typeexpr import AtomicType, TypeApplication, is_concrete
@dataclass
class Record(AtomicType):
"""
Records are a fundamental type. But we need to store some extra info.
"""
fields: tuple[tuple[str, AtomicType | TypeApplication], ...]
def __init__(self, name: str, fields: tuple[tuple[str, AtomicType | TypeApplication], ...]) -> None:
for field_name, field_type in fields:
if field_type.kind != Star():
raise TypeError(f"Record fields must not be constructors ({field_name} :: {field_type})")
if not is_concrete(field_type):
raise TypeError("Record field types must be concrete types ({field_name} :: {field_type})")
super().__init__(name)
self.fields = fields
def __str__(self) -> str:
args = ", ".join(
f"{field_name} :: {field_type}"
for field_name, field_type in self.fields
)
return f"{self.name} {{{args}}} :: {self.kind}"
# @dataclass
# class RecordConstructor(TypeConstructor):
# """
# TODO.
# i.e.:
# ```
# class Foo[T, R]:
# lft: T
# rgt: R
# """
# name: str

122
phasm/type5/solver.py Normal file
View File

@ -0,0 +1,122 @@
from typing import Any
from ..ourlang import Module
from .constraints import ConstraintBase, ConstraintList, Context, Failure, ReplaceVariable, SkipForNow, Success
from .fromast import phasm_type5_generate_constraints
from .typeexpr import TypeExpr, TypeVariable, is_concrete, replace_variable
MAX_RESTACK_COUNT = 100
class Type5SolverException(Exception):
pass
def phasm_type5(inp: Module[Any], verbose: bool = False) -> None:
ctx = Context(inp.build)
constraint_list = phasm_type5_generate_constraints(ctx, inp)
assert constraint_list
placeholder_types: dict[TypeVariable, TypeExpr] = {}
error_list: list[tuple[str, str, str]] = []
if verbose:
print('Constraints')
for _ in range(MAX_RESTACK_COUNT):
if verbose:
for constraint in constraint_list:
print(f"{constraint.sourceref!s} {constraint!s}")
print("Validating")
new_constraint_list: list[ConstraintBase] = []
# Iterate using a while and pop since on ReplaceVariable
# we want to iterate over the list as well, and since on
# ConstraintList we want to treat those first.
remaining_constraint_list = sorted(constraint_list, key=lambda x: x.complexity())
while remaining_constraint_list:
constraint = remaining_constraint_list.pop(0)
result = constraint.check()
if verbose:
print(f"{constraint.sourceref!s} {constraint!s}")
print(f"{constraint.sourceref!s} => {result.to_str(inp.build.type5_name)}")
match result:
case Success():
# This constraint was valid
continue
case SkipForNow():
# We have to check later
new_constraint_list.append(constraint)
continue
case ConstraintList(items):
# This constraint was valid, but we have new once
# Do this as the first next items, so when users are reading the
# solver output they don't need to context switch.
remaining_constraint_list = items + remaining_constraint_list
if verbose:
for new_const in items:
print(f"{constraint.sourceref!s} => + {new_const!s}")
continue
case Failure(msg):
error_list.append((str(constraint.sourceref), str(constraint), msg, ))
continue
case ReplaceVariable(action_var, action_typ):
assert action_var not in placeholder_types # When does this happen?
assert not isinstance(action_typ, TypeVariable) or action_typ not in placeholder_types # When does this happen?
assert action_var != action_typ # When does this happen?
# Ensure all existing found types are updated
# if they have this variable somewhere inside them.
placeholder_types = {
k: replace_variable(v, action_var, action_typ)
for k, v in placeholder_types.items()
}
# Add the new variable to the registry
placeholder_types[action_var] = action_typ
# Also update all constraints that may refer to this variable
# that they now have more detailed information.
for oth_const in new_constraint_list + remaining_constraint_list:
old_str = str(oth_const)
oth_const.replace_variable(action_var, action_typ)
new_str = str(oth_const)
if verbose and old_str != new_str:
print(f"{oth_const.sourceref!s} => - {old_str!s}")
print(f"{oth_const.sourceref!s} => + {new_str!s}")
continue
if error_list:
raise Type5SolverException(error_list)
if not new_constraint_list:
break
if verbose:
print()
print('New round')
constraint_list = new_constraint_list
if new_constraint_list:
raise Type5SolverException('Was unable to complete typing this program')
for placeholder, expression in ctx.placeholder_update.items():
if expression is None:
continue
resolved_type5 = placeholder_types[placeholder]
assert is_concrete(resolved_type5) # When does this happen?
expression.type5 = resolved_type5
for placeholder, (ptst_map, orig_tvar) in ctx.ptst_update.items():
resolved_type5 = placeholder_types[placeholder]
assert is_concrete(resolved_type5) # When does this happen?
ptst_map[orig_tvar] = resolved_type5

201
phasm/type5/typeexpr.py Normal file
View File

@ -0,0 +1,201 @@
from __future__ import annotations
from dataclasses import dataclass
from .kindexpr import Arrow, KindExpr, Nat, Star
@dataclass
class TypeExpr:
kind: KindExpr
name: str
def __str__(self) -> str:
return f"{self.name} :: {self.kind}"
@dataclass
class AtomicType(TypeExpr):
def __init__(self, name: str) -> None:
super().__init__(Star(), name)
def __hash__(self) -> int:
return hash((self.kind, self.name))
@dataclass
class TypeLevelNat(TypeExpr):
value: int
def __init__(self, nat: int) -> None:
assert 0 <= nat
super().__init__(Nat(), str(nat))
self.value = nat
def __hash__(self) -> int:
return hash((self.kind, self.name, self.value))
@dataclass
class TypeVariable(TypeExpr):
"""
A placeholder in a type expression
"""
def __hash__(self) -> int:
return hash((self.kind, self.name))
def __lt__(self, other: object) -> bool:
if not isinstance(other, TypeVariable):
raise TypeError
return self.name < other.name
@dataclass
class TypeConstructor(TypeExpr):
def __init__(self, kind: Arrow, name: str) -> None:
super().__init__(kind, name)
def __hash__(self) -> int:
return hash((self.kind, self.name))
@dataclass
class TypeApplication(TypeExpr):
constructor: TypeConstructor | TypeApplication | TypeVariable
argument: TypeExpr
def __init__(
self,
constructor: TypeConstructor | TypeApplication | TypeVariable,
argument: TypeExpr,
) -> None:
if isinstance(constructor.kind, Star):
raise TypeError("A constructor cannot be a concrete type")
if isinstance(constructor.kind, Nat):
raise TypeError("A constructor cannot be a number")
if constructor.kind.arg_kind != argument.kind:
raise TypeError("Argument does match construtor's expectations")
super().__init__(
constructor.kind.result_kind,
f"{constructor.name} ({argument.name})",
)
self.constructor = constructor
self.argument = argument
def __hash__(self) -> int:
return hash((self.kind, self.name, self.constructor, self.argument))
def occurs(lft: TypeVariable, rgt: TypeApplication) -> bool:
"""
Checks whether the given variable occurs in the given application.
"""
if lft == rgt.constructor:
return True
if lft == rgt.argument:
return True
if isinstance(rgt.argument, TypeApplication):
return occurs(lft, rgt.argument)
return False
def is_concrete(lft: TypeExpr) -> bool:
"""
A concrete type has no variables in it.
This is also known as a monomorphic type or a closed type.
TODO: I don't know the differen between them yet.
"""
if isinstance(lft, AtomicType):
return True
if isinstance(lft, TypeLevelNat):
return True
if isinstance(lft, TypeVariable):
return False
if isinstance(lft, TypeConstructor):
return True
if isinstance(lft, TypeApplication):
return is_concrete(lft.constructor) and is_concrete(lft.argument)
raise NotImplementedError
def is_polymorphic(lft: TypeExpr) -> bool:
"""
A polymorphic type has one or more variables in it.
"""
return not is_concrete(lft)
def replace_variable(expr: TypeExpr, var: TypeVariable, rep_expr: TypeExpr) -> TypeExpr:
assert var.kind == rep_expr.kind, (var, rep_expr, )
if isinstance(expr, AtomicType):
# Nothing to replace
return expr
if isinstance(expr, TypeLevelNat):
# Nothing to replace
return expr
if isinstance(expr, TypeVariable):
if expr == var:
return rep_expr
return expr
if isinstance(expr, TypeConstructor):
# Nothing to replace
return expr
if isinstance(expr, TypeApplication):
new_constructor = replace_variable(expr.constructor, var, rep_expr)
assert isinstance(new_constructor, TypeConstructor | TypeApplication | TypeVariable) # type hint
return TypeApplication(
constructor=new_constructor,
argument=replace_variable(expr.argument, var, rep_expr),
)
raise NotImplementedError
def instantiate(
expr: TypeExpr,
known_map: dict[TypeVariable, TypeVariable],
) -> TypeExpr:
"""
Make a copy of all variables.
This is need when you use a polymorphic function in two places. In the
one place, `t a` may refer to `u32[...]` and in another to `f32[4]`.
These should not be unified since they refer to a different monomorphic
version of the function.
"""
if isinstance(expr, AtomicType):
return expr
if isinstance(expr, TypeLevelNat):
return expr
if isinstance(expr, TypeVariable):
return known_map[expr]
if isinstance(expr, TypeConstructor):
return expr
if isinstance(expr, TypeApplication):
new_constructor = instantiate(expr.constructor, known_map)
assert isinstance(new_constructor, TypeConstructor | TypeApplication | TypeVariable) # type hint
return TypeApplication(
constructor=new_constructor,
argument=instantiate(expr.argument, known_map),
)
raise NotImplementedError(expr)

50
phasm/type5/typerouter.py Normal file
View File

@ -0,0 +1,50 @@
from .record import Record
from .typeexpr import (
AtomicType,
TypeApplication,
TypeConstructor,
TypeExpr,
TypeLevelNat,
TypeVariable,
)
class TypeRouter[T]:
def when_atomic(self, typ: AtomicType) -> T:
raise NotImplementedError(typ)
def when_application(self, typ: TypeApplication) -> T:
raise NotImplementedError(typ)
def when_constructor(self, typ: TypeConstructor) -> T:
raise NotImplementedError(typ)
def when_record(self, typ: Record) -> T:
raise NotImplementedError(typ)
def when_type_level_nat(self, typ: TypeLevelNat) -> T:
raise NotImplementedError(typ)
def when_variable(self, typ: TypeVariable) -> T:
raise NotImplementedError(typ)
def __call__(self, typ: TypeExpr) -> T:
if isinstance(typ, AtomicType):
if isinstance(typ, Record):
return self.when_record(typ)
return self.when_atomic(typ)
if isinstance(typ, TypeApplication):
return self.when_application(typ)
if isinstance(typ, TypeConstructor):
return self.when_constructor(typ)
if isinstance(typ, TypeLevelNat):
return self.when_type_level_nat(typ)
if isinstance(typ, TypeVariable):
return self.when_variable(typ)
raise NotImplementedError(typ)

View File

@ -0,0 +1,43 @@
from __future__ import annotations
import dataclasses
from typing import Iterable
from ..type5.constrainedexpr import ConstrainedExpr, TypeConstraint
from ..type5.typeexpr import TypeExpr, TypeVariable, instantiate
@dataclasses.dataclass
class TypeClass:
name: str
variables: tuple[TypeVariable, ...]
methods: dict[str, TypeExpr | ConstrainedExpr] = dataclasses.field(default_factory=dict)
operators: dict[str, TypeExpr | ConstrainedExpr] = dataclasses.field(default_factory=dict)
class TypeClassConstraint(TypeConstraint):
__slots__ = ('cls', 'variables', )
def __init__(self, cls: TypeClass, variables: Iterable[TypeVariable]) -> None:
self.cls = cls
self.variables = tuple(variables)
def __str__(self) -> str:
vrs = ' '.join(x.name for x in self.variables)
return f'{self.cls.name} {vrs}'
def __repr__(self) -> str:
vrs = ', '.join(str(x) for x in self.variables)
return f'TypeClassConstraint({self.cls.name}, [{vrs}])'
def instantiate(self, known_map: dict[TypeVariable, TypeVariable]) -> TypeClassConstraint:
return TypeClassConstraint(
self.cls,
[
# instantiate returns a TypeVariable if you give it a TypeVariable,
# but I can't seem to convince mypy of that.
instantiate(var, known_map) # type: ignore
for var in self.variables
]
)

View File

@ -5,6 +5,7 @@ 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
@ -104,11 +105,17 @@ class Import(WatSerializable):
else f' (result {self.result.to_wat()})'
)
class Statement(WatSerializable):
class StatementBase(WatSerializable):
pass
class Statement(StatementBase ):
"""
Represents a Web Assembly statement
"""
def __init__(self, name: str, *args: str, comment: Optional[str] = None):
assert ' ' not in name, 'Please pass argument separately'
assert name != 'call', 'Please use StatementCall'
self.name = name
self.args = args
self.comment = comment
@ -119,6 +126,16 @@ class Statement(WatSerializable):
return f'{self.name} {args}{comment}'
class StatementCall(StatementBase ):
def __init__(self, func_name: str, comment: str | None = None):
self.func_name = func_name
self.comment = comment
def to_wat(self) -> str:
comment = f' ;; {self.comment}' if self.comment else ''
return f'call ${self.func_name} {comment}'
class Function(WatSerializable):
"""
Represents a Web Assembly function
@ -130,7 +147,7 @@ class Function(WatSerializable):
params: Iterable[Param],
locals_: Iterable[Param],
result: WasmType,
statements: Iterable[Statement],
statements: Iterable[StatementBase],
) -> None:
self.name = name
self.exported_name = exported_name
@ -185,6 +202,7 @@ class Module(WatSerializable):
"""
def __init__(self) -> None:
self.imports: List[Import] = []
self.table: dict[int, str] = {}
self.functions: List[Function] = []
self.memory = ModuleMemory()
@ -192,8 +210,10 @@ class Module(WatSerializable):
"""
Generates the text version
"""
return '(module\n {}\n {}\n {})\n'.format(
return '(module\n {}\n {}\n {}\n {}\n {})\n'.format(
'\n '.join(x.to_wat() for x in self.imports),
f'(table {len(self.table)} funcref)',
'\n '.join(f'(elem (i32.const {k}) ${v})' for k, v in self.table.items()),
self.memory.to_wat(),
'\n '.join(x.to_wat() for x in self.functions),
)

View File

@ -1,69 +0,0 @@
"""
Helper functions to quickly generate WASM code
"""
from typing import List, Optional
import functools
from . import wasm
#pylint: disable=C0103,C0115,C0116,R0201,R0902
class Prefix_inn_fnn:
def __init__(self, prefix: str) -> None:
self.prefix = prefix
# 6.5.5. Memory Instructions
self.load = functools.partial(wasm.Statement, f'{self.prefix}.load')
self.store = functools.partial(wasm.Statement, f'{self.prefix}.store')
# 6.5.6. Numeric Instructions
self.clz = functools.partial(wasm.Statement, f'{self.prefix}.clz')
self.ctz = functools.partial(wasm.Statement, f'{self.prefix}.ctz')
self.popcnt = functools.partial(wasm.Statement, f'{self.prefix}.popcnt')
self.add = functools.partial(wasm.Statement, f'{self.prefix}.add')
self.sub = functools.partial(wasm.Statement, f'{self.prefix}.sub')
self.mul = functools.partial(wasm.Statement, f'{self.prefix}.mul')
self.div_s = functools.partial(wasm.Statement, f'{self.prefix}.div_s')
self.div_u = functools.partial(wasm.Statement, f'{self.prefix}.div_u')
self.rem_s = functools.partial(wasm.Statement, f'{self.prefix}.rem_s')
self.rem_u = functools.partial(wasm.Statement, f'{self.prefix}.rem_u')
self.and_ = functools.partial(wasm.Statement, f'{self.prefix}.and')
self.or_ = functools.partial(wasm.Statement, f'{self.prefix}.or')
self.xor = functools.partial(wasm.Statement, f'{self.prefix}.xor')
self.shl = functools.partial(wasm.Statement, f'{self.prefix}.shl')
self.shr_s = functools.partial(wasm.Statement, f'{self.prefix}.shr_s')
self.shr_u = functools.partial(wasm.Statement, f'{self.prefix}.shr_u')
self.rotl = functools.partial(wasm.Statement, f'{self.prefix}.rotl')
self.rotr = functools.partial(wasm.Statement, f'{self.prefix}.rotr')
self.eqz = functools.partial(wasm.Statement, f'{self.prefix}.eqz')
self.eq = functools.partial(wasm.Statement, f'{self.prefix}.eq')
self.ne = functools.partial(wasm.Statement, f'{self.prefix}.ne')
self.lt_s = functools.partial(wasm.Statement, f'{self.prefix}.lt_s')
self.lt_u = functools.partial(wasm.Statement, f'{self.prefix}.lt_u')
self.gt_s = functools.partial(wasm.Statement, f'{self.prefix}.gt_s')
self.gt_u = functools.partial(wasm.Statement, f'{self.prefix}.gt_u')
self.le_s = functools.partial(wasm.Statement, f'{self.prefix}.le_s')
self.le_u = functools.partial(wasm.Statement, f'{self.prefix}.le_u')
self.ge_s = functools.partial(wasm.Statement, f'{self.prefix}.ge_s')
self.ge_u = functools.partial(wasm.Statement, f'{self.prefix}.ge_u')
def const(self, value: int, comment: Optional[str] = None) -> wasm.Statement:
return wasm.Statement(f'{self.prefix}.const', f'0x{value:08x}', comment=comment)
i32 = Prefix_inn_fnn('i32')
i64 = Prefix_inn_fnn('i64')
class Block:
def __init__(self, start: str) -> None:
self.start = start
def __call__(self, *statements: wasm.Statement) -> List[wasm.Statement]:
return [
wasm.Statement('if'),
*statements,
wasm.Statement('end'),
]
if_ = Block('if')

View File

@ -1,9 +1,8 @@
"""
Helper functions to generate WASM code by writing Python functions
"""
from typing import Any, Callable, Dict, List, Optional, Type
import functools
from typing import Any, Callable, Dict, Iterable, List, Optional, Type
from . import wasm
@ -22,6 +21,15 @@ class VarType_u8(VarType_Base):
class VarType_i32(VarType_Base):
wasm_type = wasm.WasmTypeInt32
class VarType_i64(VarType_Base):
wasm_type = wasm.WasmTypeInt64
class VarType_f32(VarType_Base):
wasm_type = wasm.WasmTypeFloat32
class VarType_f64(VarType_Base):
wasm_type = wasm.WasmTypeFloat64
class Generator_i32i64:
def __init__(self, prefix: str, generator: 'Generator') -> None:
self.prefix = prefix
@ -32,41 +40,101 @@ class Generator_i32i64:
self.add = functools.partial(self.generator.add_statement, f'{prefix}.add')
self.sub = functools.partial(self.generator.add_statement, f'{prefix}.sub')
self.mul = functools.partial(self.generator.add_statement, f'{prefix}.mul')
self.div_s = functools.partial(self.generator.add_statement, f'{prefix}.div_s')
self.div_u = functools.partial(self.generator.add_statement, f'{prefix}.div_u')
self.rem_s = functools.partial(self.generator.add_statement, f'{prefix}.rem_s')
self.rem_u = functools.partial(self.generator.add_statement, f'{prefix}.rem_u')
self.and_ = functools.partial(self.generator.add_statement, f'{prefix}.and')
self.or_ = functools.partial(self.generator.add_statement, f'{prefix}.or')
self.xor = functools.partial(self.generator.add_statement, f'{prefix}.xor')
self.shl = functools.partial(self.generator.add_statement, f'{prefix}.shl')
self.shr_s = functools.partial(self.generator.add_statement, f'{prefix}.shr_s')
self.shr_u = functools.partial(self.generator.add_statement, f'{prefix}.shr_u')
self.rotl = functools.partial(self.generator.add_statement, f'{prefix}.rotl')
self.rotr = functools.partial(self.generator.add_statement, f'{prefix}.rotr')
# itestop
self.eqz = functools.partial(self.generator.add_statement, f'{prefix}.eqz')
# irelop
self.eq = functools.partial(self.generator.add_statement, f'{prefix}.eq')
self.ne = functools.partial(self.generator.add_statement, f'{prefix}.ne')
self.lt_s = functools.partial(self.generator.add_statement, f'{prefix}.lt_s')
self.lt_u = functools.partial(self.generator.add_statement, f'{prefix}.lt_u')
self.gt_s = functools.partial(self.generator.add_statement, f'{prefix}.gt_s')
self.gt_u = functools.partial(self.generator.add_statement, f'{prefix}.gt_u')
self.le_s = functools.partial(self.generator.add_statement, f'{prefix}.le_s')
self.le_u = functools.partial(self.generator.add_statement, f'{prefix}.le_u')
self.ge_s = functools.partial(self.generator.add_statement, f'{prefix}.ge_s')
self.ge_u = functools.partial(self.generator.add_statement, f'{prefix}.ge_u')
self.trunc_f32_s = functools.partial(self.generator.add_statement, f'{prefix}.trunc_f32_s')
self.trunc_f32_u = functools.partial(self.generator.add_statement, f'{prefix}.trunc_f32_u')
self.trunc_f64_s = functools.partial(self.generator.add_statement, f'{prefix}.trunc_f64_s')
self.trunc_f64_u = functools.partial(self.generator.add_statement, f'{prefix}.trunc_f64_u')
# 2.4.4. Memory Instructions
self.load = functools.partial(self.generator.add_statement, f'{prefix}.load')
self.load8_u = functools.partial(self.generator.add_statement, f'{prefix}.load8_u')
self.store = functools.partial(self.generator.add_statement, f'{prefix}.store')
def const(self, value: int, comment: Optional[str] = None) -> None:
self.generator.add_statement(f'{self.prefix}.const', f'0x{value:08x}', comment=comment)
self.generator.add_statement(f'{self.prefix}.const', f'{value}', comment=comment)
class Generator_i32(Generator_i32i64):
def __init__(self, generator: 'Generator') -> None:
super().__init__('i32', generator)
# 2.4.1. Numeric Instructions
self.reinterpret_f32 = functools.partial(self.generator.add_statement, 'i32.reinterpret_f32')
self.wrap_i64 = functools.partial(self.generator.add_statement, 'i32.wrap_i64')
class Generator_i64(Generator_i32i64):
def __init__(self, generator: 'Generator') -> None:
super().__init__('i64', generator)
# 2.4.1. Numeric Instructions
self.extend_i32_s = functools.partial(self.generator.add_statement, 'i64.extend_i32_s')
self.extend_i32_u = functools.partial(self.generator.add_statement, 'i64.extend_i32_u')
self.reinterpret_f64 = functools.partial(self.generator.add_statement, 'i64.reinterpret_f64')
class Generator_f32f64:
def __init__(self, prefix: str, generator: 'Generator') -> None:
self.prefix = prefix
self.generator = generator
# 2.4.1. Numeric Instructions
# funop
self.abs = functools.partial(self.generator.add_statement, f'{prefix}.abs')
self.neg = functools.partial(self.generator.add_statement, f'{prefix}.neg')
self.sqrt = functools.partial(self.generator.add_statement, f'{prefix}.sqrt')
self.ceil = functools.partial(self.generator.add_statement, f'{prefix}.ceil')
self.floor = functools.partial(self.generator.add_statement, f'{prefix}.floor')
self.trunc = functools.partial(self.generator.add_statement, f'{prefix}.trunc')
self.nearest = functools.partial(self.generator.add_statement, f'{prefix}.nearest')
# fbinop
self.add = functools.partial(self.generator.add_statement, f'{prefix}.add')
self.sub = functools.partial(self.generator.add_statement, f'{prefix}.sub')
self.mul = functools.partial(self.generator.add_statement, f'{prefix}.mul')
self.div = functools.partial(self.generator.add_statement, f'{prefix}.div')
self.min = functools.partial(self.generator.add_statement, f'{prefix}.min')
self.max = functools.partial(self.generator.add_statement, f'{prefix}.max')
self.copysign = functools.partial(self.generator.add_statement, f'{prefix}.copysign')
# frelop
self.eq = functools.partial(self.generator.add_statement, f'{prefix}.eq')
self.ne = functools.partial(self.generator.add_statement, f'{prefix}.ne')
self.lt = functools.partial(self.generator.add_statement, f'{prefix}.lt')
self.gt = functools.partial(self.generator.add_statement, f'{prefix}.gt')
self.le = functools.partial(self.generator.add_statement, f'{prefix}.le')
self.ge = functools.partial(self.generator.add_statement, f'{prefix}.ge')
# Other instr - convert
self.convert_i32_s = functools.partial(self.generator.add_statement, f'{prefix}.convert_i32_s')
self.convert_i32_u = functools.partial(self.generator.add_statement, f'{prefix}.convert_i32_u')
self.convert_i64_s = functools.partial(self.generator.add_statement, f'{prefix}.convert_i64_s')
self.convert_i64_u = functools.partial(self.generator.add_statement, f'{prefix}.convert_i64_u')
# 2.4.4. Memory Instructions
self.load = functools.partial(self.generator.add_statement, f'{prefix}.load')
@ -80,10 +148,19 @@ class Generator_f32(Generator_f32f64):
def __init__(self, generator: 'Generator') -> None:
super().__init__('f32', generator)
# 2.4.1 Numeric Instructions
self.demote_f64 = functools.partial(self.generator.add_statement, 'f32.demote_f64')
self.reinterpret_i32 = functools.partial(self.generator.add_statement, 'f32.reinterpret_i32')
class Generator_f64(Generator_f32f64):
def __init__(self, generator: 'Generator') -> None:
super().__init__('f64', generator)
# 2.4.1 Numeric Instructions
self.promote_f32 = functools.partial(self.generator.add_statement, 'f64.promote_f32')
self.reinterpret_i64 = functools.partial(self.generator.add_statement, 'f64.reinterpret_i64')
self.reinterpret_i64 = functools.partial(self.generator.add_statement, 'f64.reinterpret_i64')
class Generator_Local:
def __init__(self, generator: 'Generator') -> None:
self.generator = generator
@ -103,12 +180,33 @@ class Generator_Local:
self.generator.add_statement('local.tee', variable.name_ref, comment=comment)
class GeneratorBlock:
def __init__(self, generator: 'Generator', name: str) -> None:
def __init__(
self,
generator: 'Generator',
name: str,
params: Iterable[str | Type[wasm.WasmType]] = (),
result: str | Type[wasm.WasmType] | None = None,
comment: str | None = None,
) -> None:
self.generator = generator
self.name = name
self.params = params
self.result = result
self.comment = comment
def __enter__(self) -> None:
self.generator.add_statement(self.name)
stmt = self.name
args: list[str] = []
if self.params:
args.extend(
f'(param {typ})' if isinstance(typ, str) else f'(param {typ().to_wat()})'
for typ in self.params
)
if self.result:
result = self.result if isinstance(self.result, str) else self.result().to_wat()
args.append(f'(result {result})')
self.generator.add_statement(stmt, *args, comment=self.comment)
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
if not exc_type:
@ -116,7 +214,7 @@ class GeneratorBlock:
class Generator:
def __init__(self) -> None:
self.statements: List[wasm.Statement] = []
self.statements: List[wasm.StatementBase] = []
self.locals: Dict[str, VarType_Base] = {}
self.i32 = Generator_i32(self)
@ -124,13 +222,16 @@ class Generator:
self.f32 = Generator_f32(self)
self.f64 = Generator_f64(self)
# Parametric Instructions
self.drop = functools.partial(self.add_statement, 'drop')
# 2.4.3 Variable Instructions
self.local = Generator_Local(self)
# 2.4.5 Control Instructions
self.nop = functools.partial(self.add_statement, 'nop')
self.unreachable = functools.partial(self.add_statement, 'unreachable')
# block
self.block = functools.partial(GeneratorBlock, self, 'block')
self.loop = functools.partial(GeneratorBlock, self, 'loop')
self.if_ = functools.partial(GeneratorBlock, self, 'if')
# br
@ -138,32 +239,68 @@ class Generator:
# br_table
self.return_ = functools.partial(self.add_statement, 'return')
# call - see below
# call_indirect
# call_indirect - see below
def br_if(self, idx: int) -> None:
self.add_statement('br_if', f'{idx}')
def call(self, function: wasm.Function) -> None:
self.add_statement('call', f'${function.name}')
def call(self, function: wasm.Function | str) -> None:
if isinstance(function, wasm.Function):
function = function.name
self.statements.append(wasm.StatementCall(function))
def call_indirect(self, params: Iterable[Type[wasm.WasmType] | wasm.WasmType], result: Type[wasm.WasmType] | wasm.WasmType) -> None:
param_str = ' '.join(
(x() if isinstance(x, type) else x).to_wat()
for x in params
)
if isinstance(result, type):
result = result()
result_str = result.to_wat()
self.add_statement('call_indirect', f'(param {param_str})', f'(result {result_str})')
def add_statement(self, name: str, *args: str, comment: Optional[str] = None) -> None:
self.statements.append(wasm.Statement(name, *args, comment=comment))
def temp_var_i32(self, infix: str) -> VarType_i32:
def temp_var[T: VarType_Base](self, var: T) -> T:
idx = 0
while (varname := f'__{infix}_tmp_var_{idx}__') in self.locals:
while (varname := f'__{var.name}_tmp_var_{idx}__') in self.locals:
idx += 1
return VarType_i32(varname)
return var.__class__(varname)
def temp_var_t(self, typ: Type[wasm.WasmType], name: str) -> VarType_Base:
idx = 0
while (varname := f'__{name}_tmp_var_{idx}__') in self.locals:
idx += 1
if typ is wasm.WasmTypeInt32:
return VarType_u8(varname)
if typ is wasm.WasmTypeInt32:
return VarType_i32(varname)
if typ is wasm.WasmTypeInt64:
return VarType_i64(varname)
if typ is wasm.WasmTypeFloat32:
return VarType_f32(varname)
if typ is wasm.WasmTypeFloat64:
return VarType_f64(varname)
raise NotImplementedError(typ)
def temp_var_i32(self, infix: str) -> VarType_i32:
return self.temp_var(VarType_i32(infix))
def temp_var_u8(self, infix: str) -> VarType_u8:
idx = 0
while (varname := f'__{infix}_tmp_var_{idx}__') in self.locals:
idx += 1
return self.temp_var(VarType_u8(infix))
return VarType_u8(varname)
def func_wrapper(exported: bool = True) -> Callable[[Any], wasm.Function]:
def func_wrapper(exported: bool = False) -> Callable[[Any], wasm.Function]:
"""
This wrapper will execute the function and return
a wasm Function with the generated Statements
@ -205,6 +342,10 @@ def func_wrapper(exported: bool = True) -> Callable[[Any], wasm.Function]:
# Check what locals were used, and define them
locals_: List[wasm.Param] = []
for local_name, local_type in generator.locals.items():
if local_name in args:
# Already defined as a local by wasm itself
continue
locals_.append((local_name, local_type.wasm_type(), ))
# Complete function definition

3
pyproject.toml Normal file
View File

@ -0,0 +1,3 @@
[tool.ruff.lint]
select = ["F", "E", "W", "I"]
ignore = ["E501"]

View File

@ -1,10 +1,19 @@
mypy==0.812
pygments==2.12.0
pylint==2.7.4
pytest==6.2.2
marko==2.1.3
mypy==1.17.1
pygments==2.19.1
pytest==8.3.5
pytest-integration==0.2.2
pywasm==1.0.7
pywasm3==0.5.0
wasmer==1.1.0
wasmer_compiler_cranelift==1.1.0
wasmtime==0.36.0
ruff==0.12.7
wasmtime==31.0.0
# TODO:
# extism?
# wasmedge
# Check 2025-04-05
# wasm3: minimal maintenance phase
# py-wasm: last updated 6 years ago
# wasmer-python: Not compatible with python3.12, last updated 2 years ago
# WAVM: Last updated 3 years ago

View File

@ -1,14 +0,0 @@
from typing import Any, Dict, List, Optional, Union
from . import binary
from . import option
from . import execution
class Runtime:
store: execution.Store
def __init__(self, module: binary.Module, imps: Optional[Dict[str, Any]] = None, opts: Optional[option.Option] = None):
...
def exec(self, name: str, args: List[Union[int, float]]) -> Any:
...

View File

@ -1,6 +0,0 @@
from typing import BinaryIO
class Module:
@classmethod
def from_reader(cls, reader: BinaryIO) -> 'Module':
...

View File

@ -1,10 +0,0 @@
from typing import List
class Result:
...
class MemoryInstance:
data: bytearray
class Store:
memory_list: List[MemoryInstance]

View File

@ -1,2 +0,0 @@
class Option:
...

View File

@ -1,23 +0,0 @@
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:
...

View File

@ -1,39 +0,0 @@
from typing import Any, Dict, Callable, Union
def wat2wasm(inp: str) -> bytes:
...
class Store:
...
class Function:
def __init__(self, store: Store, func: Callable[[Any], Any]) -> None:
...
class Module:
def __init__(self, store: Store, wasm: bytes) -> None:
...
class Uint8Array:
def __getitem__(self, index: Union[int, slice]) -> int:
...
def __setitem__(self, idx: int, value: int) -> None:
...
class Memory:
def uint8_view(self, offset: int = 0) -> Uint8Array:
...
class Exports:
...
class ImportObject:
def register(self, region: str, values: Dict[str, Function]) -> None:
...
class Instance:
exports: Exports
def __init__(self, module: Module, imports: ImportObject) -> None:
...

View File

@ -1,16 +0,0 @@
"""
Constants for use in the tests
"""
ALL_INT_TYPES = ['u8', 'u32', 'u64', 'i32', 'i64']
COMPLETE_INT_TYPES = ['u32', 'u64', 'i32', 'i64']
ALL_FLOAT_TYPES = ['f32', 'f64']
COMPLETE_FLOAT_TYPES = ALL_FLOAT_TYPES
TYPE_MAP = {
**{x: int for x in ALL_INT_TYPES},
**{x: float for x in ALL_FLOAT_TYPES},
}
COMPLETE_NUMERIC_TYPES = COMPLETE_INT_TYPES + COMPLETE_FLOAT_TYPES

View File

@ -1,85 +1,164 @@
from __future__ import annotations
import os
import sys
from typing import Any, Callable, List, TextIO, Union
from phasm.codestyle import phasm_render
from phasm.wasm import (
WasmTypeFloat32,
WasmTypeFloat64,
WasmTypeInt32,
WasmTypeInt64,
)
from . import runners
from . import memory, runners
DASHES = '-' * 16
class InvalidArgumentException(Exception):
pass
class SuiteResult:
def __init__(self):
def __init__(self) -> None:
self.returned_value = None
RUNNER_CLASS_MAP = {
'pywasm': runners.RunnerPywasm,
'pywasm3': runners.RunnerPywasm3,
'wasmtime': runners.RunnerWasmtime,
'wasmer': runners.RunnerWasmer,
}
TRACE_CODE_PREFIX = """
@imported
def trace_adr(adr: i32) -> i32:
pass
@imported
def trace_i32(x: i32) -> i32:
pass
"""
def make_int_trace(prefix: str) -> Callable[[int], int]:
def trace_helper(adr: int) -> int:
sys.stderr.write(f'{prefix} {adr}\n')
sys.stderr.flush()
return adr
return trace_helper
class Suite:
"""
WebAssembly test suite
"""
def __init__(self, code_py):
def __init__(self, code_py: str) -> None:
self.code_py = code_py
def run_code(self, *args, runtime='pywasm3', imports=None):
def run_code(
self,
*args: Any,
runtime: str = 'wasmtime',
func_name: str = 'testEntry',
imports: runners.Imports = None,
do_format_check: bool = True,
verbose: bool | None = None,
with_traces: bool = False,
) -> Any:
"""
Compiles the given python code into wasm and
then runs it
Returned is an object with the results set
"""
if verbose is None:
verbose = bool(os.environ.get('VERBOSE'))
code_prefix = ''
if with_traces:
assert do_format_check is False
if imports is None:
imports = {}
imports.update({
'trace_adr': make_int_trace('ADR'),
'trace_i32': make_int_trace('i32'),
})
code_prefix = TRACE_CODE_PREFIX
class_ = RUNNER_CLASS_MAP[runtime]
runner = class_(self.code_py)
runner = class_(code_prefix + self.code_py)
runner.parse()
if verbose:
write_header(sys.stderr, 'Phasm')
runner.dump_phasm_code(sys.stderr)
runner.parse(verbose=verbose)
runner.compile_ast()
runner.optimise_wasm_ast()
runner.compile_wat()
runner.compile_wasm()
if verbose:
write_header(sys.stderr, 'Assembly')
runner.dump_wasm_wat(sys.stderr)
runner.interpreter_setup()
runner.interpreter_load(imports)
write_header(sys.stderr, 'Phasm')
runner.dump_phasm_code(sys.stderr)
write_header(sys.stderr, 'Assembly')
runner.dump_wasm_wat(sys.stderr)
allocator_generator = memory.Allocator(runner.phasm_ast.build, runner)
# Check if code formatting works
assert self.code_py == '\n' + phasm_render(runner.phasm_ast) # \n for formatting in tests
if do_format_check:
assert self.code_py == '\n' + phasm_render(runner.phasm_ast) # \n for formatting in tests
wasm_args = []
func = runner.phasm_ast.functions[func_name]
assert func.type5 is not None # Type hint
func_args = runner.phasm_ast.build.type5_is_function(func.type5)
assert func_args is not None
func_ret = func_args.pop()
if len(func_args) != len(args):
raise RuntimeError(f'Invalid number of args for {func_name}')
wasm_args: List[Union[float, int]] = []
if args:
write_header(sys.stderr, 'Memory (pre alloc)')
runner.interpreter_dump_memory(sys.stderr)
if verbose:
write_header(sys.stderr, 'Memory (pre alloc)')
runner.interpreter_dump_memory(sys.stderr)
for arg in args:
if isinstance(arg, (int, float, )):
for arg, arg_typ in zip(args, func_args, strict=True):
arg_typ_info = runner.phasm_ast.build.type_info_map.get(arg_typ.name)
if arg_typ_info and (arg_typ_info.wasm_type is WasmTypeInt32 or arg_typ_info.wasm_type is WasmTypeInt64):
assert isinstance(arg, int)
wasm_args.append(arg)
continue
if isinstance(arg, bytes):
adr = runner.call('stdlib.types.__alloc_bytes__', len(arg))
sys.stderr.write(f'Allocation 0x{adr:08x} {repr(arg)}\n')
runner.interpreter_write_memory(adr + 4, arg)
wasm_args.append(adr)
if arg_typ_info and (arg_typ_info.wasm_type is WasmTypeFloat32 or arg_typ_info.wasm_type is WasmTypeFloat64):
assert isinstance(arg, float)
wasm_args.append(arg)
continue
raise NotImplementedError(arg)
allocator = allocator_generator(arg_typ)
adr = allocator(arg)
wasm_args.append(adr)
write_header(sys.stderr, 'Memory (pre run)')
runner.interpreter_dump_memory(sys.stderr)
if verbose:
write_header(sys.stderr, 'Memory (pre run)')
runner.interpreter_dump_memory(sys.stderr)
result = SuiteResult()
result.returned_value = runner.call('testEntry', *wasm_args)
result.returned_value = runner.call(func_name, *wasm_args)
write_header(sys.stderr, 'Memory (post run)')
runner.interpreter_dump_memory(sys.stderr)
extractor = memory.Extractor(runner.phasm_ast.build, runner)(func_ret)
result.returned_value = extractor(result.returned_value)
if verbose:
write_header(sys.stderr, 'Memory (post run)')
runner.interpreter_dump_memory(sys.stderr)
return result
def write_header(textio, msg: str) -> None:
def write_header(textio: TextIO, msg: str) -> None:
textio.write(f'{DASHES} {msg.ljust(16)} {DASHES}\n')

551
tests/integration/memory.py Normal file
View File

@ -0,0 +1,551 @@
import struct
from typing import Any, Protocol
from phasm.build.base import BuildBase
from phasm.build.typerouter import BuildTypeRouter
from phasm.type5.record import Record
from phasm.type5.typeexpr import AtomicType, TypeExpr
from phasm.wasm import (
WasmTypeFloat32,
WasmTypeFloat64,
WasmTypeInt32,
WasmTypeInt64,
WasmTypeNone,
)
class MemoryAccess(Protocol):
def call(self, function: str, *args: Any) -> Any:
"""
Use for calling allocator methods inside the WASM environment.
"""
def interpreter_write_memory(self, offset: int, data: bytes) -> None:
"""
Writes bytes directly to WASM environment memory.
Addresses should be generated using allocators via call.
"""
def interpreter_read_memory(self, offset: int, length: int) -> bytes:
"""
Reads bytes directly from WASM environment memory.
"""
class MemorySlice:
__slots__ = ('memory', 'offset', )
def __init__(self, memory: bytes, offset: int) -> None:
self.memory = memory
self.offset = offset
def __call__(self, size: int) -> bytes:
return self.memory[self.offset:self.offset + size]
def __repr__(self) -> str:
return f'MemorySlice({self.memory!r}, {self.offset!r})'
class AllocatorFunc(Protocol):
alloc_size: int
def __call__(self, py_value: Any, store_at_adr: int | None = None) -> int:
"""
Takes a Python value and allocaties it in the given memory
Based on the phasm type.
When the parent already has allocated memory, the store_at_adr is set.
In that case, write your value to the given address, and return it.
"""
class Allocator(BuildTypeRouter[AllocatorFunc]):
__slots__ = ('access', )
access: MemoryAccess
def __init__(self, build: BuildBase[Any], access: MemoryAccess) -> None:
super().__init__(build)
self.access = access
def when_atomic(self, typ: AtomicType) -> AllocatorFunc:
type_info = self.build.type_info_map[typ.name]
if type_info.wasm_type is WasmTypeNone:
raise NotImplementedError
if type_info.wasm_type is WasmTypeInt32 or type_info.wasm_type is WasmTypeInt64:
if type_info.signed is None:
raise NotImplementedError
return IntAllocator(self.access, type_info.signed, type_info.alloc_size)
if type_info.wasm_type is WasmTypeFloat32 or type_info.wasm_type is WasmTypeFloat64:
return FloatAllocator(self.access, type_info.alloc_size)
raise NotImplementedError(typ)
def when_dynamic_array(self, da_arg: TypeExpr) -> AllocatorFunc:
if da_arg.name == 'u8':
return BytesAllocator(self.access)
return DynamicArrayAllocator(self.access, self(da_arg))
def when_static_array(self, sa_len: int, sa_typ: TypeExpr) -> AllocatorFunc:
return StaticArrayAllocator(self.access, sa_len, self(sa_typ))
def when_struct(self, typ: Record) -> AllocatorFunc:
return StructAllocator(self.access, [(x_nam, self(x_typ)) for x_nam, x_typ in typ.fields])
def when_tuple(self, tp_args: list[TypeExpr]) -> AllocatorFunc:
return TupleAllocator(self.access, list(map(self, tp_args)))
class ExtractorFunc(Protocol):
alloc_size: int
def __call__(self, wasm_value: Any) -> Any:
"""
Takes a WASM value and returns a Python value
Based on the phasm type
"""
class Extractor(BuildTypeRouter[ExtractorFunc]):
__slots__ = ('access', )
access: MemoryAccess
def __init__(self, build: BuildBase[Any], access: MemoryAccess) -> None:
super().__init__(build)
self.access = access
def when_atomic(self, typ: AtomicType) -> ExtractorFunc:
type_info = self.build.type_info_map[typ.name]
if type_info.wasm_type is WasmTypeNone:
return NoneExtractor()
if type_info.wasm_type is WasmTypeInt32 or type_info.wasm_type is WasmTypeInt64:
if type_info.signed is None:
return BoolExtractor()
return IntExtractor(type_info.signed, type_info.alloc_size)
if type_info.wasm_type is WasmTypeFloat32 or type_info.wasm_type is WasmTypeFloat64:
return FloatExtractor(type_info.alloc_size)
raise NotImplementedError(typ)
def when_dynamic_array(self, da_arg: TypeExpr) -> ExtractorFunc:
if da_arg.name == 'u8':
return BytesExtractor(self.access)
return DynamicArrayExtractor(self.access, self(da_arg))
def when_static_array(self, sa_len: int, sa_typ: TypeExpr) -> ExtractorFunc:
return StaticArrayExtractor(self.access, sa_len, self(sa_typ))
def when_struct(self, typ: Record) -> ExtractorFunc:
return StructExtractor(self.access, [(x_nam, self(x_typ)) for x_nam, x_typ in typ.fields])
def when_tuple(self, tp_args: list[TypeExpr]) -> ExtractorFunc:
return TupleExtractor(self.access, list(map(self, tp_args)))
class NoneExtractor:
__slots__ = ('alloc_size', )
alloc_size: int
def __init__(self) -> None:
# Do not set alloc_size, it should not be called
# this will generate an AttributeError
pass
def __call__(self, wasm_value: Any) -> None:
assert wasm_value is None
class BoolExtractor:
__slots__ = ('alloc_size', )
def __init__(self) -> None:
self.alloc_size = 1
def __call__(self, wasm_value: Any) -> bool:
assert isinstance(wasm_value, int), wasm_value
return wasm_value != 0
class IntAllocator:
__slots__ = ('access', 'alloc_size', 'signed', )
def __init__(self, access: MemoryAccess, signed: bool, alloc_size: int) -> None:
self.access = access
self.signed = signed
self.alloc_size = alloc_size
def __call__(self, py_value: Any, store_at_adr: int | None = None) -> int:
if store_at_adr is None:
raise NotImplementedError
assert isinstance(py_value, int), py_value
data = py_value.to_bytes(self.alloc_size, 'little', signed=self.signed)
self.access.interpreter_write_memory(store_at_adr, data)
return store_at_adr
class IntExtractor:
__slots__ = ('alloc_size', 'signed', )
def __init__(self, signed: bool, alloc_size: int) -> None:
self.signed = signed
self.alloc_size = alloc_size
def __call__(self, wasm_value: Any) -> int:
if isinstance(wasm_value, MemorySlice):
# Memory stored int
data = wasm_value(self.alloc_size)
else:
# Int received from the wasm interface
# Work around the fact that phasm has unsigned integers but wasm does not
# Use little endian since that matches with what WASM uses internally
assert isinstance(wasm_value, int), wasm_value
data = wasm_value.to_bytes(8, 'little', signed=True)
data = data[:self.alloc_size]
return int.from_bytes(data, 'little', signed=self.signed)
class PtrAllocator(IntAllocator):
def __init__(self, access: MemoryAccess) -> None:
super().__init__(access, False, 4)
class PtrExtractor(IntExtractor):
def __init__(self) -> None:
super().__init__(False, 4)
FLOAT_LETTER_MAP = {
4: 'f',
8: 'd'
}
class FloatAllocator:
__slots__ = ('access', 'alloc_size', )
def __init__(self, access: MemoryAccess, alloc_size: int) -> None:
self.access = access
self.alloc_size = alloc_size
def __call__(self, py_value: Any, store_at_adr: int | None = None) -> int:
if store_at_adr is None:
raise NotImplementedError
assert isinstance(py_value, (float, int, )), py_value
data = struct.pack(f'<{FLOAT_LETTER_MAP[self.alloc_size]}', py_value)
self.access.interpreter_write_memory(store_at_adr, data)
return store_at_adr
class FloatExtractor:
__slots__ = ('alloc_size', )
def __init__(self, alloc_size: int) -> None:
self.alloc_size = alloc_size
def __call__(self, wasm_value: Any) -> float:
if isinstance(wasm_value, MemorySlice):
# Memory stored float
data = wasm_value(self.alloc_size)
wasm_value, = struct.unpack(f'<{FLOAT_LETTER_MAP[self.alloc_size]}', data)
assert isinstance(wasm_value, float), wasm_value
return wasm_value
class DynamicArrayAllocator:
__slots__ = ('access', 'alloc_size', 'sub_allocator', )
access: MemoryAccess
alloc_size: int
sub_allocator: AllocatorFunc
def __init__(self, access: MemoryAccess, sub_allocator: AllocatorFunc) -> None:
self.access = access
self.alloc_size = 4 # ptr
self.sub_allocator = sub_allocator
def __call__(self, py_value: Any, store_at_adr: int | None = None) -> int:
if store_at_adr is not None:
raise NotImplementedError
assert isinstance(py_value, tuple), py_value
py_len = len(py_value)
alloc_size = 4 + py_len * self.sub_allocator.alloc_size
adr = self.access.call('stdlib.alloc.__alloc__', alloc_size)
assert isinstance(adr, int) # Type int
PtrAllocator(self.access)(py_len, adr)
for idx, el_value in enumerate(py_value):
offset = adr + 4 + idx * self.sub_allocator.alloc_size
self.sub_allocator(el_value, offset)
return adr
class DynamicArrayExtractor:
__slots__ = ('access', 'alloc_size', 'sub_extractor', )
access: MemoryAccess
alloc_size: int
sub_extractor: ExtractorFunc
def __init__(self, access: MemoryAccess, sub_extractor: ExtractorFunc) -> None:
self.access = access
self.sub_extractor = sub_extractor
def __call__(self, wasm_value: Any) -> Any:
assert isinstance(wasm_value, int), wasm_value
adr = wasm_value
del wasm_value
# wasm_value must be a pointer
# The first value at said pointer is the length of the array
read_bytes = self.access.interpreter_read_memory(adr, 4)
array_len, = struct.unpack('<I', read_bytes)
read_bytes = self.access.interpreter_read_memory(adr + 4, array_len * self.sub_extractor.alloc_size)
return tuple(
self.sub_extractor(MemorySlice(read_bytes, idx * self.sub_extractor.alloc_size))
for idx in range(array_len)
)
class BytesAllocator:
__slots__ = ('access', 'alloc_size', )
access: MemoryAccess
def __init__(self, access: MemoryAccess) -> None:
self.access = access
self.alloc_size = 4 # ptr
def __call__(self, py_value: Any, store_at_adr: int | None = None) -> int:
assert isinstance(py_value, bytes), py_value
adr = self.access.call('stdlib.types.__alloc_bytes__', len(py_value))
assert isinstance(adr, int)
self.access.interpreter_write_memory(adr + 4, py_value)
if store_at_adr is not None:
PtrAllocator(self.access)(adr, store_at_adr)
return adr
class BytesExtractor:
__slots__ = ('access', 'alloc_size', )
access: MemoryAccess
alloc_size: int
def __init__(self, access: MemoryAccess) -> None:
self.access = access
self.alloc_size = 4 # ptr
def __call__(self, wasm_value: Any) -> bytes:
if isinstance(wasm_value, MemorySlice):
wasm_value = PtrExtractor()(wasm_value)
assert isinstance(wasm_value, int), wasm_value
adr = wasm_value
del wasm_value
# wasm_value must be a pointer
# The first value at said pointer is the length of the array
read_bytes = self.access.interpreter_read_memory(adr, 4)
array_len, = struct.unpack('<I', read_bytes)
adr += 4
return self.access.interpreter_read_memory(adr, array_len)
class StaticArrayAllocator:
__slots__ = ('access', 'alloc_size', 'sa_len', 'sub_allocator', )
access: MemoryAccess
alloc_size: int
sa_len: int
sub_allocator: AllocatorFunc
def __init__(self, access: MemoryAccess, sa_len: int, sub_allocator: AllocatorFunc) -> None:
self.access = access
self.alloc_size = 4 # ptr
self.sa_len = sa_len
self.sub_allocator = sub_allocator
def __call__(self, py_value: Any, store_at_adr: int | None = None) -> int:
assert isinstance(py_value, tuple), py_value
assert len(py_value) == self.sa_len
alloc_size = self.sa_len * self.sub_allocator.alloc_size
adr = self.access.call('stdlib.alloc.__alloc__', alloc_size)
assert isinstance(adr, int) # Type int
for idx, el_value in enumerate(py_value):
sub_adr = adr + idx * self.sub_allocator.alloc_size
self.sub_allocator(el_value, sub_adr)
if store_at_adr is not None:
PtrAllocator(self.access)(adr, store_at_adr)
return adr
class StaticArrayExtractor:
__slots__ = ('access', 'alloc_size', 'sa_len', 'sub_extractor', )
access: MemoryAccess
alloc_size: int
sa_len: int
sub_extractor: ExtractorFunc
def __init__(self, access: MemoryAccess, sa_len: int, sub_extractor: ExtractorFunc) -> None:
self.access = access
self.alloc_size = 4 # ptr
self.sa_len = sa_len
self.sub_extractor = sub_extractor
def __call__(self, wasm_value: Any) -> Any:
if isinstance(wasm_value, MemorySlice):
wasm_value = PtrExtractor()(wasm_value)
assert isinstance(wasm_value, int), wasm_value
adr = wasm_value
del wasm_value
read_bytes = self.access.interpreter_read_memory(adr, self.sa_len * self.sub_extractor.alloc_size)
return tuple(
self.sub_extractor(MemorySlice(read_bytes, idx * self.sub_extractor.alloc_size))
for idx in range(self.sa_len)
)
class TupleAllocator:
__slots__ = ('access', 'alloc_size', 'sub_allocator_list', )
access: MemoryAccess
alloc_size: int
sub_allocator_list: list[AllocatorFunc]
def __init__(self, access: MemoryAccess, sub_allocator_list: list[AllocatorFunc]) -> None:
self.access = access
self.alloc_size = 4 # ptr
self.sub_allocator_list = sub_allocator_list
def __call__(self, py_value: Any, store_at_adr: int | None = None) -> int:
assert isinstance(py_value, tuple), py_value
total_alloc_size = sum(x.alloc_size for x in self.sub_allocator_list)
adr = self.access.call('stdlib.alloc.__alloc__', total_alloc_size)
assert isinstance(adr, int) # Type int
sub_adr = adr
for sub_allocator, sub_value in zip(self.sub_allocator_list, py_value, strict=True):
sub_allocator(sub_value, sub_adr)
sub_adr += sub_allocator.alloc_size
if store_at_adr is not None:
PtrAllocator(self.access)(adr, store_at_adr)
return adr
class TupleExtractor:
__slots__ = ('access', 'alloc_size', 'sub_extractor_list', )
access: MemoryAccess
alloc_size: int
sub_extractor_list: list[ExtractorFunc]
def __init__(self, access: MemoryAccess, sub_extractor_list: list[ExtractorFunc]) -> None:
self.access = access
self.alloc_size = 4 # ptr
self.sub_extractor_list = sub_extractor_list
def __call__(self, wasm_value: Any) -> tuple[Any]:
if isinstance(wasm_value, MemorySlice):
wasm_value = PtrExtractor()(wasm_value)
assert isinstance(wasm_value, int), wasm_value
adr = wasm_value
del wasm_value
total_alloc_size = sum(x.alloc_size for x in self.sub_extractor_list)
read_bytes = self.access.interpreter_read_memory(adr, total_alloc_size)
result = []
offset = 0
for sub_extractor in self.sub_extractor_list:
result.append(sub_extractor(MemorySlice(read_bytes, offset)))
offset += sub_extractor.alloc_size
return tuple(result)
class StructAllocator:
__slots__ = ('access', 'alloc_size', 'sub_allocator_list', )
access: MemoryAccess
alloc_size: int
sub_allocator_list: list[tuple[str, AllocatorFunc]]
def __init__(self, access: MemoryAccess, sub_allocator_list: list[tuple[str, AllocatorFunc]]) -> None:
self.access = access
self.alloc_size = 4 # ptr
self.sub_allocator_list = sub_allocator_list
def __call__(self, py_value: Any, store_at_adr: int | None = None) -> int:
assert isinstance(py_value, dict), py_value
total_alloc_size = sum(x.alloc_size for _, x in self.sub_allocator_list)
adr = self.access.call('stdlib.alloc.__alloc__', total_alloc_size)
assert isinstance(adr, int) # Type int
sub_adr = adr
for field_name, sub_allocator in self.sub_allocator_list:
sub_value = py_value[field_name]
sub_allocator(sub_value, sub_adr)
sub_adr += sub_allocator.alloc_size
if store_at_adr is not None:
PtrAllocator(self.access)(adr, store_at_adr)
return adr
class StructExtractor:
__slots__ = ('access', 'alloc_size', 'sub_extractor_list', )
access: MemoryAccess
alloc_size: int
sub_extractor_list: list[tuple[str, ExtractorFunc]]
def __init__(self, access: MemoryAccess, sub_extractor_list: list[tuple[str, ExtractorFunc]]) -> None:
self.access = access
self.alloc_size = 4 # ptr
self.sub_extractor_list = sub_extractor_list
def __call__(self, wasm_value: Any) -> dict[str, Any]:
if isinstance(wasm_value, MemorySlice):
wasm_value = PtrExtractor()(wasm_value)
assert isinstance(wasm_value, int), wasm_value
adr = wasm_value
del wasm_value
total_alloc_size = sum(x.alloc_size for _, x in self.sub_extractor_list)
read_bytes = self.access.interpreter_read_memory(adr, total_alloc_size)
result = {}
offset = 0
for field_name, sub_extractor in self.sub_extractor_list:
result[field_name] = sub_extractor(MemorySlice(read_bytes, offset))
offset += sub_extractor.alloc_size
return result

View File

@ -1,28 +1,26 @@
"""
Runners to help run WebAssembly code on various interpreters
"""
import ctypes
from typing import Any, Callable, Dict, Iterable, Optional, TextIO
import ctypes
import io
import pywasm.binary
import wasm3
import wasmer
import wasmtime
from phasm import ourlang, wasm
from phasm.compiler import phasm_compile
from phasm.optimise.removeunusedfuncs import removeunusedfuncs
from phasm.parser import phasm_parse
from phasm.type3.entry import phasm_type3
from phasm import ourlang
from phasm import wasm
from phasm.type5.solver import phasm_type5
from phasm.wasmgenerator import Generator as WasmGenerator
Imports = Optional[Dict[str, Callable[[Any], Any]]]
class RunnerBase:
"""
Base class
"""
phasm_code: str
phasm_ast: ourlang.Module
phasm_ast: ourlang.Module[WasmGenerator]
wasm_ast: wasm.Module
wasm_asm: str
wasm_bin: bytes
@ -36,12 +34,12 @@ class RunnerBase:
"""
_dump_code(textio, self.phasm_code)
def parse(self) -> None:
def parse(self, verbose: bool = True) -> None:
"""
Parses the Phasm code into an AST
"""
self.phasm_ast = phasm_parse(self.phasm_code)
phasm_type3(self.phasm_ast, verbose=True)
phasm_type5(self.phasm_ast, verbose=verbose)
def compile_ast(self) -> None:
"""
@ -49,6 +47,12 @@ class RunnerBase:
"""
self.wasm_ast = phasm_compile(self.phasm_ast)
def optimise_wasm_ast(self) -> None:
"""
Optimises the WebAssembly AST
"""
removeunusedfuncs(self.wasm_ast)
def compile_wat(self) -> None:
"""
Compiles the WebAssembly AST into WebAssembly Assembly code
@ -65,7 +69,7 @@ class RunnerBase:
"""
Compiles the WebAssembly AST into WebAssembly Binary
"""
self.wasm_bin = wasmer.wat2wasm(self.wasm_asm)
raise NotImplementedError
def interpreter_setup(self) -> None:
"""
@ -73,7 +77,7 @@ class RunnerBase:
"""
raise NotImplementedError
def interpreter_load(self, imports: Optional[Dict[str, Callable[[Any], Any]]] = None) -> None:
def interpreter_load(self, imports: Imports = None) -> None:
"""
Loads the code into the interpreter
"""
@ -103,77 +107,6 @@ class RunnerBase:
"""
raise NotImplementedError
class RunnerPywasm(RunnerBase):
"""
Implements a runner for pywasm
See https://pypi.org/project/pywasm/
"""
module: pywasm.binary.Module
runtime: pywasm.Runtime
def interpreter_setup(self) -> None:
# Nothing to set up
pass
def interpreter_load(self, imports: Optional[Dict[str, Callable[[Any], Any]]] = None) -> None:
if imports is not None:
raise NotImplementedError
bytesio = io.BytesIO(self.wasm_bin)
self.module = pywasm.binary.Module.from_reader(bytesio)
self.runtime = pywasm.Runtime(self.module, {}, None)
def interpreter_write_memory(self, offset: int, data: Iterable[int]) -> None:
for idx, byt in enumerate(data):
self.runtime.store.memory_list[0].data[offset + idx] = byt
def interpreter_read_memory(self, offset: int, length: int) -> bytes:
return self.runtime.store.memory_list[0].data[offset:length]
def interpreter_dump_memory(self, textio: TextIO) -> None:
_dump_memory(textio, self.runtime.store.memory_list[0].data)
def call(self, function: str, *args: Any) -> Any:
return self.runtime.exec(function, [*args])
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, imports: Optional[Dict[str, Callable[[Any], Any]]] = None) -> None:
if imports is not None:
raise NotImplementedError
self.mod = self.env.parse_module(self.wasm_bin)
self.rtime.load(self.mod)
def interpreter_write_memory(self, offset: int, data: Iterable[int]) -> None:
memory = self.rtime.get_memory(0)
for idx, byt in enumerate(data):
memory[offset + idx] = byt # type: ignore
def interpreter_read_memory(self, offset: int, length: int) -> bytes:
memory = self.rtime.get_memory(0)
return memory[offset:length].tobytes()
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)
class RunnerWasmtime(RunnerBase):
"""
Implements a runner for wasmtime
@ -184,15 +117,48 @@ class RunnerWasmtime(RunnerBase):
module: wasmtime.Module
instance: wasmtime.Instance
@classmethod
def func2type(cls, func: Callable[[Any], Any]) -> wasmtime.FuncType:
params: list[wasmtime.ValType] = []
code = func.__code__
for idx in range(code.co_argcount):
varname = code.co_varnames[idx]
vartype = func.__annotations__[varname]
if vartype is int:
params.append(wasmtime.ValType.i32())
elif vartype is float:
params.append(wasmtime.ValType.f32())
else:
raise NotImplementedError
results: list[wasmtime.ValType] = []
if func.__annotations__['return'] is None:
pass # No return value
elif func.__annotations__['return'] is int:
results.append(wasmtime.ValType.i32())
elif func.__annotations__['return'] is float:
results.append(wasmtime.ValType.f32())
else:
raise NotImplementedError('Return type', func.__annotations__['return'])
return wasmtime.FuncType(params, results)
def interpreter_setup(self) -> None:
self.store = wasmtime.Store()
def interpreter_load(self, imports: Optional[Dict[str, Callable[[Any], Any]]] = None) -> None:
if imports is not None:
raise NotImplementedError
functions: list[wasmtime.Func] = []
self.module = wasmtime.Module(self.store.engine, self.wasm_bin)
self.instance = wasmtime.Instance(self.store, self.module, [])
if imports is not None:
functions = [
wasmtime.Func(self.store, self.__class__.func2type(f), f)
for f in imports.values()
]
self.module = wasmtime.Module(self.store.engine, self.wasm_asm)
self.instance = wasmtime.Instance(self.store, self.module, functions)
def interpreter_write_memory(self, offset: int, data: Iterable[int]) -> None:
exports = self.instance.exports(self.store)
@ -217,8 +183,7 @@ class RunnerWasmtime(RunnerBase):
data_len = memory.data_len(self.store)
raw = ctypes.string_at(data_ptr, data_len)
return raw[offset:length]
return raw[offset:offset + length]
def interpreter_dump_memory(self, textio: TextIO) -> None:
exports = self.instance.exports(self.store)
@ -237,63 +202,6 @@ class RunnerWasmtime(RunnerBase):
return func(self.store, *args)
class RunnerWasmer(RunnerBase):
"""
Implements a runner for wasmer
See https://pypi.org/project/wasmer/
"""
# pylint: disable=E1101
store: wasmer.Store
module: wasmer.Module
instance: wasmer.Instance
def interpreter_setup(self) -> None:
self.store = wasmer.Store()
def interpreter_load(self, imports: Optional[Dict[str, Callable[[Any], Any]]] = None) -> None:
import_object = wasmer.ImportObject()
if imports:
import_object.register('imports', {
k: wasmer.Function(self.store, v)
for k, v in (imports or {}).items()
})
self.module = wasmer.Module(self.store, self.wasm_bin)
self.instance = wasmer.Instance(self.module, import_object)
def interpreter_write_memory(self, offset: int, data: Iterable[int]) -> None:
exports = self.instance.exports
memory = getattr(exports, 'memory')
assert isinstance(memory, wasmer.Memory)
view = memory.uint8_view(offset)
for idx, byt in enumerate(data):
view[idx] = byt
def interpreter_read_memory(self, offset: int, length: int) -> bytes:
exports = self.instance.exports
memory = getattr(exports, 'memory')
assert isinstance(memory, wasmer.Memory)
view = memory.uint8_view(offset)
return bytes(view[offset:length])
def interpreter_dump_memory(self, textio: TextIO) -> None:
exports = self.instance.exports
memory = getattr(exports, 'memory')
assert isinstance(memory, wasmer.Memory)
view = memory.uint8_view()
_dump_memory(textio, view) # type: ignore
def call(self, function: str, *args: Any) -> Any:
exports = self.instance.exports
func = getattr(exports, function)
return func(*args)
def _dump_memory(textio: TextIO, mem: bytes) -> None:
line_width = 16

View File

@ -1,39 +1,19 @@
import binascii
import struct
import pytest
from ..helpers import Suite
@pytest.mark.slow_integration_test
def test_crc32():
# FIXME: Stub
# crc = 0xFFFFFFFF
# byt = 0x61
# => (crc >> 8) ^ _CRC32_Table[(crc & 0xFF) ^ byt]
# (crc >> 8) = 0x00FFFFFF
# => 0x00FFFFFF ^ _CRC32_Table[(crc & 0xFF) ^ byt]
# (crc & 0xFF) = 0xFF
# => 0x00FFFFFF ^ _CRC32_Table[0xFF ^ byt]
# 0xFF ^ 0x61 = 0x9E
# => 0x00FFFFFF ^ _CRC32_Table[0x9E]
# _CRC32_Table[0x9E] = 0x17b7be43
# => 0x00FFFFFF ^ 0x17b7be43
with open('examples/crc32.py', 'r', encoding='ASCII') as fil:
code_py = "\n" + fil.read()
code_py = """
def _crc32_f(crc: u32, byt: u8) -> u32:
return 16777215 ^ 397917763
# https://reveng.sourceforge.io/crc-catalogue/legend.htm#crc.legend.params
in_put = b'123456789'
def testEntry(data: bytes) -> u32:
return 4294967295 ^ _crc32_f(4294967295, data[0])
"""
exp_result = binascii.crc32(b'a')
# https://reveng.sourceforge.io/crc-catalogue/17plus.htm#crc.cat.crc-32-iso-hdlc
check = 0xcbf43926
result = Suite(code_py).run_code(b'a')
result = Suite(code_py).run_code(in_put, func_name='crc32', do_format_check=False)
# exp_result returns a unsigned integer, as is proper
exp_data = struct.pack('I', exp_result)
# ints extracted from WebAssembly are always signed
data = struct.pack('i', result.returned_value)
assert exp_data == data
assert check == result.returned_value

View File

@ -2,29 +2,12 @@ import pytest
from ..helpers import Suite
@pytest.mark.slow_integration_test
def test_fib():
code_py = """
def helper(n: i32, a: i32, b: i32) -> i32:
if n < 1:
return a + b
with open('./examples/fib.py', 'r', encoding='UTF-8') as fil:
code_py = "\n" + fil.read()
return helper(n - 1, a + b, a)
def fib(n: i32) -> i32:
if n == 0:
return 0
if n == 1:
return 1
return helper(n - 1, 0, 1)
@exported
def testEntry() -> i32:
return fib(40)
"""
result = Suite(code_py).run_code()
result = Suite(code_py).run_code(40, func_name='fib')
assert 102334155 == result.returned_value

View File

@ -0,0 +1,309 @@
# runtime_extract_value_literal
As a developer
I want to extract a $TYPE value
In order get the result I calculated with my phasm code
```py
@exported
def testEntry() -> $TYPE:
return $VAL0
```
```py
expect(VAL0)
```
# runtime_extract_value_round_trip
As a developer
I want to extract a $TYPE value that I've input
In order let the code select a value that I've predefined
```py
@exported
def testEntry(x: $TYPE) -> $TYPE:
return x
```
```py
expect(VAL0, given=[VAL0])
```
# module_constant_def_ok
As a developer
I want to define $TYPE module constants
In order to make hardcoded values more visible
and to make it easier to change hardcoded values
```py
CONSTANT: $TYPE = $VAL0
@exported
def testEntry() -> i32:
return 9
```
```py
expect(9)
```
# module_constant_def_bad
As a developer
I want to receive a type error on an invalid assignment on a $TYPE module constant
In order to make debugging easier
```py
CONSTANT: (u32, ) = $VAL0
```
```py
if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_') or TYPE_NAME.startswith('dynamic_array_'):
expect_type_error('Tuple element count mismatch')
elif TYPE_NAME == 'bytes':
expect_type_error('Cannot convert from literal bytes')
elif TYPE_NAME == 'f32' or TYPE_NAME == 'f64':
expect_type_error('Cannot convert from literal float')
elif TYPE_NAME == 'i32' or TYPE_NAME == 'i64' or TYPE_NAME == 'u32' or TYPE_NAME == 'u64':
expect_type_error('Cannot convert from literal integer')
else:
expect_type_error('Not the same type')
```
# function_result_is_literal_ok
As a developer
I want to use return a literal from a function
In order to define constants in a more dynamic way
```py
def drop_arg_return_9(x: $TYPE) -> i32:
return 9
def constant() -> $TYPE:
return $VAL0
@exported
def testEntry() -> i32:
return drop_arg_return_9(constant())
```
```py
expect(9)
```
# function_result_is_literal_bad
As a developer
I want to receive a type error when returning a $TYPE literal for a function that doesn't return that type
In order to make debugging easier
```py
def drop_arg_return_9(x: (u32, )) -> i32:
return 9
def constant() -> (u32, ):
return $VAL0
@exported
def testEntry() -> i32:
return drop_arg_return_9(constant())
```
```py
if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_') or TYPE_NAME.startswith('dynamic_array_'):
expect_type_error('Tuple element count mismatch')
elif TYPE_NAME.startswith('struct_'):
expect_type_error('Not the same type')
elif TYPE_NAME == 'bytes':
expect_type_error('Cannot convert from literal bytes')
elif TYPE_NAME == 'f32' or TYPE_NAME == 'f64':
expect_type_error('Cannot convert from literal float')
elif TYPE_NAME == 'i32' or TYPE_NAME == 'i64' or TYPE_NAME == 'u32' or TYPE_NAME == 'u64':
expect_type_error('Cannot convert from literal integer')
else:
expect_type_error('Not the same type')
```
# function_result_is_module_constant_ok
As a developer
I want to use return a $TYPE module constant from a function
In order to use my module constants in return statements
```py
CONSTANT: $TYPE = $VAL0
def helper(x: $TYPE) -> i32:
return 9
def constant() -> $TYPE:
return CONSTANT
@exported
def testEntry() -> i32:
return helper(constant())
```
```py
expect(9)
```
# function_result_is_module_constant_bad
As a developer
I want to receive a type error when returning a $TYPE module constant for a function that doesn't return that type
In order to make debugging easier
```py
CONSTANT: $TYPE = $VAL0
def drop_arg_return_9(x: (u32, )) -> i32:
return 9
def constant() -> (u32, ):
return CONSTANT
@exported
def testEntry() -> i32:
return drop_arg_return_9(constant())
```
```py
expect_type_error('Not the same type')
```
# function_result_is_arg_ok
As a developer
I want to use return a $TYPE function argument
In order to make it possible to select a value using a function
```py
CONSTANT: $TYPE = $VAL0
def drop_arg_return_9(x: $TYPE) -> i32:
return 9
def select(x: $TYPE) -> $TYPE:
return x
@exported
def testEntry() -> i32:
return drop_arg_return_9(select(CONSTANT))
```
```py
expect(9)
```
# function_result_is_arg_bad
As a developer
I want to receive a type error when returning a $TYPE argument for a function that doesn't return that type
In order to make debugging easier
```py
def drop_arg_return_9(x: (u32, )) -> i32:
return 9
def select(x: $TYPE) -> (u32, ):
return x
```
```py
expect_type_error('Not the same type')
```
# function_arg_literal_ok
As a developer
I want to use a $TYPE literal by passing it to a function
In order to use a pre-existing function with the values I specify
```py
def helper(x: $TYPE) -> i32:
return 9
@exported
def testEntry() -> i32:
return helper($VAL0)
```
```py
expect(9)
```
# function_arg_literal_bad
As a developer
I want to receive a type error when passing a $TYPE literal to a function that does not accept it
In order to make debugging easier
```py
def helper(x: (u32, )) -> i32:
return 9
@exported
def testEntry() -> i32:
return helper($VAL0)
```
```py
if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_') or TYPE_NAME.startswith('dynamic_array_'):
expect_type_error('Tuple element count mismatch')
elif TYPE_NAME.startswith('struct_'):
expect_type_error('Not the same type')
elif TYPE_NAME == 'bytes':
expect_type_error('Cannot convert from literal bytes')
elif TYPE_NAME == 'f32' or TYPE_NAME == 'f64':
expect_type_error('Cannot convert from literal float')
elif TYPE_NAME == 'i32' or TYPE_NAME == 'i64' or TYPE_NAME == 'u32' or TYPE_NAME == 'u64':
expect_type_error('Cannot convert from literal integer')
else:
expect_type_error('Not the same type')
```
# function_arg_module_constant_def_ok
As a developer
I want to use a $TYPE module constant by passing it to a function
In order to use my defined value with a pre-existing function
```py
CONSTANT: $TYPE = $VAL0
def helper(x: $TYPE) -> i32:
return 9
@exported
def testEntry() -> i32:
return helper(CONSTANT)
```
```py
expect(9)
```
# function_arg_module_constant_def_bad
As a developer
I want to receive a type error when passing a $TYPE module constant to a function that does not accept it
In order to make debugging easier
```py
CONSTANT: $TYPE = $VAL0
def helper(x: (u32, )) -> i32:
return 9
@exported
def testEntry() -> i32:
return helper(CONSTANT)
```
```py
expect_type_error('Not the same type')
```

View File

@ -0,0 +1,153 @@
import functools
import json
import sys
from typing import Any
import marko
import marko.md_renderer
def get_tests(template):
test_data = None
for el in template.children:
if isinstance(el, marko.block.BlankLine):
continue
if isinstance(el, marko.block.Heading):
if test_data is not None:
yield test_data
test_data = []
test_data.append(el)
continue
if test_data is not None:
test_data.append(el)
if test_data is not None:
yield test_data
def apply_settings(settings, txt):
for k, v in settings.items():
if k in ('CODE_HEADER', 'PYTHON'):
continue
txt = txt.replace(f'${k}', v)
return txt
def generate_assertion_expect(result, arg, given=None):
given = given or []
result.append('result = Suite(code_py).run_code(' + ', '.join(repr(x) for x in given) + ')')
result.append(f'assert {repr(arg)} == result.returned_value')
def generate_assertion_expect_type_error(result, error_msg):
result.append(f'with pytest.raises(Type5SolverException, match={error_msg!r}):')
result.append(' Suite(code_py).run_code()')
def json_does_not_support_byte_or_tuple_values_fix(inp: Any):
if isinstance(inp, (int, float, )):
return inp
if isinstance(inp, str):
if inp.startswith('bytes:'):
return inp[6:].encode()
return inp
if isinstance(inp, list):
return tuple(map(json_does_not_support_byte_or_tuple_values_fix, inp))
if isinstance(inp, dict):
return {
key: json_does_not_support_byte_or_tuple_values_fix(val)
for key, val in inp.items()
}
raise NotImplementedError(inp)
def generate_assertions(settings, result_code):
result = []
locals_ = {
'TYPE': settings['TYPE'],
'TYPE_NAME': settings['TYPE_NAME'],
'expect': functools.partial(generate_assertion_expect, result),
'expect_type_error': functools.partial(generate_assertion_expect_type_error, result),
}
if 'PYTHON' in settings:
locals_.update(json_does_not_support_byte_or_tuple_values_fix(settings['PYTHON']))
if 'VAL0' not in locals_:
locals_['VAL0'] = eval(settings['VAL0'])
exec(result_code, {}, locals_)
return ' ' + '\n '.join(result) + '\n'
def generate_code(markdown, template, settings):
type_name = settings['TYPE_NAME']
print('"""')
print('AUTO GENERATED')
print()
print('TEMPLATE:', sys.argv[1])
print('SETTINGS:', sys.argv[2])
print('"""')
print('import pytest')
print()
print('from phasm.type5.solver import Type5SolverException')
print()
print('from ..helpers import Suite')
print()
print()
for test in get_tests(template):
assert len(test) == 4, test
heading, paragraph, code_block1, code_block2 = test
assert isinstance(heading, marko.block.Heading)
assert isinstance(paragraph, marko.block.Paragraph)
assert isinstance(code_block1, marko.block.FencedCode)
assert isinstance(code_block2, marko.block.FencedCode)
test_id = apply_settings(settings, heading.children[0].children)
user_story = apply_settings(settings, markdown.renderer.render(paragraph))
inp_code = apply_settings(settings, code_block1.children[0].children)
result_code = markdown.renderer.render_children(code_block2)
print('@pytest.mark.integration_test')
print(f'def test_{type_name}_{test_id}():')
print(' """')
print(' ' + user_story.strip().replace('\n', '\n '))
print(' """')
print(' code_py = """')
if 'CODE_HEADER' in settings:
for lin in settings['CODE_HEADER']:
print(lin)
print()
print(inp_code.rstrip('\n'))
print('"""')
print()
print(generate_assertions(settings, result_code))
print()
def main():
markdown = marko.Markdown(
renderer=marko.md_renderer.MarkdownRenderer,
)
with open(sys.argv[1], 'r', encoding='utf-8') as fil:
template = markdown.parse(fil.read())
with open(sys.argv[2], 'r', encoding='utf-8') as fil:
settings = json.load(fil)
if 'TYPE_NAME' not in settings:
settings['TYPE_NAME'] = settings['TYPE']
generate_code(markdown, template, settings)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,4 @@
{
"TYPE": "bytes",
"VAL0": "b'ABCDEFG'"
}

View File

@ -0,0 +1,5 @@
{
"TYPE_NAME": "dynamic_array_u64",
"TYPE": "u64[...]",
"VAL0": "(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, )"
}

View File

@ -0,0 +1,4 @@
{
"TYPE": "f32",
"VAL0": "1000000.125"
}

View File

@ -0,0 +1,4 @@
{
"TYPE": "f64",
"VAL0": "1000000.125"
}

View File

@ -0,0 +1,4 @@
{
"TYPE": "i32",
"VAL0": "1000000"
}

View File

@ -0,0 +1,4 @@
{
"TYPE": "i64",
"VAL0": "1000000"
}

View File

@ -0,0 +1,5 @@
{
"TYPE_NAME": "static_array_tuple_u32_u32_3",
"TYPE": "(u32, u32, )[3]",
"VAL0": "((1, 100, ), (2, 200, ), (3, 300, ), )"
}

View File

@ -0,0 +1,5 @@
{
"TYPE_NAME": "static_array_u64_32",
"TYPE": "u64[32]",
"VAL0": "(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, )"
}

View File

@ -0,0 +1,33 @@
{
"TYPE_NAME": "static_array_with_structs",
"TYPE": "StructMain[3]",
"VAL0": "(StructMain(1, (StructCode(-4), 4, 4.0, ), (StructCode(-1), StructCode(-2), )), StructMain(2, (StructCode(-16), 16, 16.0, ), (StructCode(3), StructCode(14), )), StructMain(3, (StructCode(-256), 256, 256.0, ), (StructCode(-9), StructCode(-98), )), )",
"CODE_HEADER": [
"class StructCode:",
" code: i32",
"",
"class StructMain:",
" val00: u8",
" val01: (StructCode, u64, f32, )",
" val02: StructCode[2]"
],
"PYTHON": {
"VAL0": [
{
"val00": 1,
"val01": [{"code": -4}, 4, 4.0],
"val02": [{"code": -1}, {"code": -2}]
},
{
"val00": 2,
"val01": [{"code": -16}, 16, 16.0],
"val02": [{"code": 3}, {"code": 14}]
},
{
"val00": 3,
"val01": [{"code": -256}, 256, 256.0],
"val02": [{"code": -9}, {"code": -98}]
}
]
}
}

View File

@ -0,0 +1,46 @@
{
"TYPE_NAME": "struct_all_primitives",
"TYPE": "StructallPrimitives",
"VAL0": "StructallPrimitives(1, 2, 4, 8, 1, -1, 2, -2, 4, -4, 8, -8, 125.125, -125.125, 5000.5, -5000.5, b'Hello, world!')",
"CODE_HEADER": [
"class StructallPrimitives:",
" val00: u8",
" val03: u16",
" val01: u32",
" val02: u64",
" val10: i8",
" val11: i8",
" val16: i16",
" val17: i16",
" val12: i32",
" val13: i32",
" val14: i64",
" val15: i64",
" val20: f32",
" val21: f32",
" val22: f64",
" val23: f64",
" val30: bytes"
],
"PYTHON": {
"VAL0": {
"val00": 1,
"val03": 2,
"val01": 4,
"val02": 8,
"val10": 1,
"val11": -1,
"val16": 2,
"val17": -2,
"val12": 4,
"val13": -4,
"val14": 8,
"val15": -8,
"val20": 125.125,
"val21": -125.125,
"val22": 5000.5,
"val23": -5000.5,
"val30": "bytes:Hello, world!"
}
}
}

View File

@ -0,0 +1,25 @@
{
"TYPE_NAME": "struct_nested",
"TYPE": "StructNested",
"VAL0": "StructNested(4, SubStruct(8, 16), 20)",
"CODE_HEADER": [
"class SubStruct:",
" val00: u8",
" val01: u8",
"",
"class StructNested:",
" val00: u64",
" val01: SubStruct",
" val02: u64"
],
"PYTHON": {
"VAL0": {
"val00": 4,
"val01": {
"val00": 8,
"val01": 16
},
"val02": 20
}
}
}

View File

@ -0,0 +1,12 @@
{
"TYPE_NAME": "struct_one_field",
"TYPE": "StructOneField",
"VAL0": "StructOneField(4)",
"CODE_HEADER": [
"class StructOneField:",
" value: u32"
],
"PYTHON": {
"VAL0": {"value": 4}
}
}

View File

@ -0,0 +1,5 @@
{
"TYPE_NAME": "tuple_all_primitives",
"TYPE": "(u8, u32, u64, i8, i8, i32, i32, i64, i64, f32, f32, f64, f64, bytes, )",
"VAL0": "(1, 4, 8, 1, -1, 4, -4, 8, -8, 125.125, -125.125, 5000.5, -5000.5, b'Hello, world!', )"
}

View File

@ -0,0 +1,5 @@
{
"TYPE_NAME": "tuple_nested",
"TYPE": "(u64, (u32, bytes, u32, ), (u8, u32[3], u8, ), )",
"VAL0": "(1000000, (1, b'test', 2, ), (1, (4, 4, 4, ), 1, ), )"
}

View File

@ -0,0 +1,5 @@
{
"TYPE_NAME": "tuple_u64_u32_u8",
"TYPE": "(u64, u32, u8, )",
"VAL0": "(1000000, 1000, 1, )"
}

View File

@ -0,0 +1,4 @@
{
"TYPE": "u32",
"VAL0": "1000000"
}

View File

@ -0,0 +1,4 @@
{
"TYPE": "u64",
"VAL0": "1000000"
}

View File

@ -1,80 +0,0 @@
import sys
import pytest
from ..helpers import Suite, write_header
from ..runners import RunnerPywasm
def setup_interpreter(phash_code: str) -> RunnerPywasm:
runner = RunnerPywasm(phash_code)
runner.parse()
runner.compile_ast()
runner.compile_wat()
runner.compile_wasm()
runner.interpreter_setup()
runner.interpreter_load()
write_header(sys.stderr, 'Phasm')
runner.dump_phasm_code(sys.stderr)
write_header(sys.stderr, 'Assembly')
runner.dump_wasm_wat(sys.stderr)
return runner
@pytest.mark.integration_test
def test_foldl_1():
code_py = """
def u8_or(l: u8, r: u8) -> u8:
return l | r
@exported
def testEntry(b: bytes) -> u8:
return foldl(u8_or, 128, b)
"""
suite = Suite(code_py)
result = suite.run_code(b'')
assert 128 == result.returned_value
result = suite.run_code(b'\x80', runtime='pywasm')
assert 128 == result.returned_value
result = suite.run_code(b'\x80\x40', runtime='pywasm')
assert 192 == result.returned_value
result = suite.run_code(b'\x80\x40\x20\x10', runtime='pywasm')
assert 240 == result.returned_value
result = suite.run_code(b'\x80\x40\x20\x10\x08\x04\x02\x01', runtime='pywasm')
assert 255 == result.returned_value
@pytest.mark.integration_test
def test_foldl_2():
code_py = """
def xor(l: u8, r: u8) -> u8:
return l ^ r
@exported
def testEntry(a: bytes, b: bytes) -> u8:
return foldl(xor, 0, a) ^ foldl(xor, 0, b)
"""
suite = Suite(code_py)
result = suite.run_code(b'\x55\x0F', b'\x33\x80')
assert 233 == result.returned_value
@pytest.mark.integration_test
def test_foldl_3():
code_py = """
def xor(l: u32, r: u8) -> u32:
return l ^ u32(r)
@exported
def testEntry(a: bytes) -> u32:
return foldl(xor, 0, a)
"""
suite = Suite(code_py)
result = suite.run_code(b'\x55\x0F\x33\x80')
assert 233 == result.returned_value

View File

@ -2,52 +2,30 @@ import pytest
from ..helpers import Suite
@pytest.mark.integration_test
def test_bytes_address():
code_py = """
@exported
def testEntry(f: bytes) -> bytes:
return f
"""
result = Suite(code_py).run_code(b'This is a test')
# THIS DEPENDS ON THE ALLOCATOR
# A different allocator will return a different value
assert 20 == result.returned_value
@pytest.mark.integration_test
def test_bytes_length():
def test_bytes_export_constant():
code_py = """
CONSTANT: bytes = b'Hello'
@exported
def testEntry(f: bytes) -> i32:
return len(f)
def testEntry() -> bytes:
return CONSTANT
"""
result = Suite(code_py).run_code(b'This is another test')
result = Suite(code_py).run_code()
assert 20 == result.returned_value
assert b"Hello" == result.returned_value[0:5]
assert 5 == len(result.returned_value)
@pytest.mark.integration_test
def test_bytes_index():
def test_bytes_export_instantiation():
code_py = """
@exported
def testEntry(f: bytes) -> u8:
return f[8]
def testEntry() -> bytes:
return b'Hello'
"""
result = Suite(code_py).run_code(b'This is another test')
result = Suite(code_py).run_code()
assert 0x61 == result.returned_value
@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).run_code(b'Short', b'Long' * 100)
assert 0 == result.returned_value
assert b"Hello" == result.returned_value

Some files were not shown because too many files have changed in this diff Show More