Compare commits

..

5 Commits

Author SHA1 Message Date
Johan B.W. de Vries
243d9e68f2 Starting on platform runtime 2023-05-06 15:24:46 +02:00
Johan B.W. de Vries
d1b593d4e5 Import service names 2023-04-11 10:25:30 +02:00
Johan B.W. de Vries
7ec273a732 Type fix 2023-04-11 09:52:27 +02:00
Johan B.W. de Vries
959b643542 Bytes constants 2023-04-10 16:24:29 +02:00
Johan B.W. de Vries
a044e3ef0c Ideas for the IO Monad 2023-04-10 15:24:56 +02:00
131 changed files with 4631 additions and 10393 deletions

2
.gitignore vendored
View File

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

View File

@ -1,5 +1,7 @@
TEST_FILES := tests
WAT2WASM := venv/bin/python wat2wasm.py
WABT_DIR := /home/johan/Sources/github.com/WebAssembly/wabt
WAT2WASM := $(WABT_DIR)/bin/wat2wasm
WASM2C := $(WABT_DIR)/bin/wasm2c
%.wat: %.py $(shell find phasm -name '*.py') venv/.done
venv/bin/python -m phasm $< $@
@ -13,38 +15,33 @@ WAT2WASM := venv/bin/python wat2wasm.py
%.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 $(subst .json,.py,$(subst /generator_,/test_generated_,$(wildcard tests/integration/test_lang/generator_*.json)))
venv/bin/pytest $(TEST_FILES) $(TEST_FLAGS)
test: venv/.done
venv/bin/pytest tests $(TEST_FLAGS)
lint: venv/.done
venv/bin/ruff check phasm tests
venv/bin/pylint phasm
typecheck: venv/.done
venv/bin/mypy --strict phasm wat2wasm.py tests/integration/helpers.py tests/integration/memory.py tests/integration/runners.py
venv/bin/mypy --strict phasm tests/integration/runners.py
venv/.done: requirements.txt
python3.12 -m venv venv
python3.10 -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,14 +3,46 @@ 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.10,
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.10 -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
-------
From `examples/fib.py`:
For more examples, see the examples directory.
```py
def helper(n: u64, a: u64, b: u64) -> u64:
if n < 1:
@ -29,55 +61,6 @@ 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
@ -100,9 +83,6 @@ 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
@ -116,3 +96,4 @@ 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

17
TODO.md
View File

@ -7,21 +7,6 @@
- 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.

51
examples/buffer.html Normal file
View File

@ -0,0 +1,51 @@
<!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>

7
examples/buffer.py Normal file
View File

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

View File

@ -2,68 +2,245 @@
<html>
<head>
<title>Examples - CRC32</title>
<link rel="stylesheet" type="text/css" href="main.css">
</head>
<body>
<h1>Examples - CRC32</h1>
<h1>Buffer</h1>
<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>
<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="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>
<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>
<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>
<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>
<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.
<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 />
The CRC of ASCII &quot;123456789&quot; is 0xcbf43926.
<script type="text/javascript" src="./include.js"></script>
<script type="text/javascript">
let importObject = {};
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();
});
// 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;
}
</script>
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>
</body>
</html>

View File

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

View File

@ -1,62 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<title>Examples - Fibonacci</title>
<link rel="stylesheet" type="text/css" href="main.css">
</head>
<body>
<h1>Examples - Fibonacci</h1>
<h1>Fibonacci</h1>
<div class="menu">
<a href="index.html">List</a> - <a href="fib.py.html">Source</a> - <a href="fib.wat.html">WebAssembly</a>
</div>
<a href="index.html">List</a> - <a href="fib.py.html">Source</a> - <a href="fib.wat.html">WebAssembly</a>
<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>
<div style="white-space: pre;" id="results"></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>
<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>
<script type="text/javascript" src="./include.js"></script>
<script type="text/javascript">
let importObject = {};
<script>
let importObject = {};
let exampleN = document.querySelector('#example-n');
let exampleClick = document.querySelector('#example-click');
let exampleFib = document.querySelector('#example-fib');
function run_test(app, number, expected)
{
let actual = app.instance.exports.fib(BigInt(number));
console.log(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');
});
test_result(BigInt(expected) == actual, {
'summary': 'fib(' + number + ')',
'attributes': {
'expected': expected,
'actual': actual
},
});
}
for(let exmpl of document.querySelectorAll('a[data-n]') ) {
exmpl.addEventListener('click', event => {
exampleN.value = exmpl.getAttribute('data-n');
exampleClick.click();
});
}
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>
</script>
</body>
</html>

View File

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

48
examples/fold.html Normal file
View File

@ -0,0 +1,48 @@
<!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>

6
examples/fold.py Normal file
View File

@ -0,0 +1,6 @@
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)

60
examples/imported.html Normal file
View File

@ -0,0 +1,60 @@
<!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>

7
examples/imported.py Normal file
View File

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

View File

@ -1,6 +1,3 @@
/***
* 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__']
@ -17,12 +14,85 @@ function alloc_bytes(app, data)
return offset;
}
/**
* WebAssembly's interface only gets you signed integers
*
* Getting unsigned values out requires some work.
*/
function i32_to_u32(n)
function run_times(times, callback, tweak)
{
return n >>> 0;
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);
}

View File

@ -1,16 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<title>Examples</title>
<link rel="stylesheet" type="text/css" href="main.css">
</head>
<body>
<h1>Examples</h1>
<h2>Functions</h2>
<h2>Standard</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>

View File

@ -1,31 +0,0 @@
: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,11 +4,9 @@ Functions for using this module from CLI
import sys
from .compiler import phasm_compile
from .optimise.removeunusedfuncs import removeunusedfuncs
from .parser import phasm_parse
from .type5.solver import phasm_type5
from .type3.entry import phasm_type3
from .compiler import phasm_compile
def main(source: str, sink: str) -> int:
"""
@ -19,9 +17,8 @@ def main(source: str, sink: str) -> int:
code_py = fil.read()
our_module = phasm_parse(code_py)
phasm_type5(our_module, verbose=False)
phasm_type3(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:

View File

@ -1,445 +0,0 @@
"""
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,
)

View File

@ -1,77 +0,0 @@
"""
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

@ -1,214 +0,0 @@
"""
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

@ -1,143 +0,0 @@
"""
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

@ -1,159 +0,0 @@
"""
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

@ -1,216 +0,0 @@
"""
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

@ -1,54 +0,0 @@
"""
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

@ -1,643 +0,0 @@
"""
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

@ -1,107 +0,0 @@
"""
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

@ -1,88 +0,0 @@
"""
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

@ -1,89 +0,0 @@
"""
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

@ -1,227 +0,0 @@
"""
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

@ -1,383 +0,0 @@
"""
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

@ -1,58 +0,0 @@
"""
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

@ -1,95 +0,0 @@
"""
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

@ -1,61 +0,0 @@
"""
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

@ -1,144 +0,0 @@
"""
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

@ -1,86 +0,0 @@
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)

View File

@ -1,154 +0,0 @@
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,13 +3,13 @@ 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 Any, Generator
from typing import Generator
from . import ourlang
from .type5 import typeexpr as type5typeexpr
from .type3 import types as type3types
from .type3.types import TYPE3_ASSERTION_ERROR, Type3, Type3OrPlaceholder
def phasm_render(inp: ourlang.Module[Any]) -> str:
def phasm_render(inp: ourlang.Module) -> str:
"""
Public method for rendering a Phasm module into Phasm code
"""
@ -17,27 +17,47 @@ def phasm_render(inp: ourlang.Module[Any]) -> str:
Statements = Generator[str, None, None]
def type5(mod: ourlang.Module[Any], inp: type5typeexpr.TypeExpr) -> str:
def type3(inp: Type3OrPlaceholder) -> str:
"""
Render: type's name
"""
return mod.build.type5_name(inp)
assert isinstance(inp, Type3), TYPE3_ASSERTION_ERROR
def struct_definition(mod: ourlang.Module[Any], inp: ourlang.StructDefinition) -> str:
if inp is type3types.none:
return 'None'
if isinstance(inp, type3types.AppliedType3):
if inp.base == type3types.tuple:
return '(' + ', '.join(
type3(x)
for x in inp.args
if isinstance(x, Type3) # Skip ints, not allowed here anyhow
) + ', )'
if inp.base == type3types.static_array:
assert 2 == len(inp.args)
assert isinstance(inp.args[0], Type3), TYPE3_ASSERTION_ERROR
assert isinstance(inp.args[1], type3types.IntType3), TYPE3_ASSERTION_ERROR
return inp.args[0].name + '[' + inp.args[1].name + ']'
return inp.name
def struct_definition(inp: ourlang.StructDefinition) -> str:
"""
Render: TypeStruct's definition
"""
result = f'class {inp.struct_type5.name}:\n'
for mem, typ in inp.struct_type5.fields:
result += f' {mem}: {type5(mod, typ)}\n'
result = f'class {inp.struct_type3.name}:\n'
for mem, typ in inp.struct_type3.members.items():
result += f' {mem}: {type3(typ)}\n'
return result
def constant_definition(mod: ourlang.Module[Any], inp: ourlang.ModuleConstantDef) -> str:
def constant_definition(inp: ourlang.ModuleConstantDef) -> str:
"""
Render: Module Constant's definition
"""
return f'{inp.name}: {type5(mod, inp.type5)} = {expression(inp.constant)}\n'
return f'{inp.name}: {type3(inp.type3)} = {expression(inp.constant)}\n'
def expression(inp: ourlang.Expression) -> str:
"""
@ -48,9 +68,6 @@ 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)
@ -58,7 +75,7 @@ def expression(inp: ourlang.Expression) -> str:
) + ', )'
if isinstance(inp, ourlang.ConstantStruct):
return inp.struct_type5.name + '(' + ', '.join(
return inp.struct_name + '(' + ', '.join(
expression(x)
for x in inp.value
) + ')'
@ -66,8 +83,23 @@ def expression(inp: ourlang.Expression) -> str:
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.name} {expression(inp.right)}'
return f'{expression(inp.left)} {inp.operator} {expression(inp.right)}'
if isinstance(inp, ourlang.FunctionCall):
args = ', '.join(
@ -76,13 +108,10 @@ def expression(inp: ourlang.Expression) -> str:
)
if isinstance(inp.function, ourlang.StructConstructor):
return f'{inp.function.struct_type5.name}({args})'
return f'{inp.function.struct_type3.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)
@ -100,6 +129,10 @@ 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:
@ -126,7 +159,7 @@ def statement(inp: ourlang.Statement) -> Statements:
raise NotImplementedError(statement, inp)
def function(mod: ourlang.Module[Any], inp: ourlang.Function) -> str:
def function(inp: ourlang.Function) -> str:
"""
Render: Function body
@ -139,17 +172,12 @@ def function(mod: ourlang.Module[Any], 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'{arg_name}: {type5(mod, arg_type)}'
for arg_name, arg_type in zip(inp.arg_names, fn_args, strict=True)
f'{p.name}: {type3(p.type3)}'
for p in inp.posonlyargs
)
result += f'def {inp.name}({args}) -> {type5(mod, ret_type5)}:\n'
result += f'def {inp.name}({args}) -> {type3(inp.returns_type3)}:\n'
if inp.imported:
result += ' pass\n'
@ -161,7 +189,7 @@ def function(mod: ourlang.Module[Any], inp: ourlang.Function) -> str:
return result
def module(inp: ourlang.Module[Any]) -> str:
def module(inp: ourlang.Module) -> str:
"""
Render: Module
"""
@ -170,20 +198,20 @@ def module(inp: ourlang.Module[Any]) -> str:
for struct in inp.struct_definitions.values():
if result:
result += '\n'
result += struct_definition(inp, struct)
result += struct_definition(struct)
for cdef in inp.constant_defs.values():
if result:
result += '\n'
result += constant_definition(inp, cdef)
result += constant_definition(cdef)
for func in inp.functions.values():
if isinstance(func, ourlang.StructConstructor):
# Auto generated
if func.lineno < 0:
# Builtin (-2) or auto generated (-1)
continue
if result:
result += '\n'
result += function(inp, func)
result += function(func)
return result

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +0,0 @@
# 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

@ -1,49 +0,0 @@
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,46 +1,28 @@
"""
Contains the syntax tree for ourlang
"""
from __future__ import annotations
from typing import Dict, List, Optional, Union
from typing import Dict, Iterable, List, Optional, Union
import enum
from .build.base import BuildBase
from .type5 import constrainedexpr as type5constrainedexpr
from .type5 import record as type5record
from .type5 import typeexpr as type5typeexpr
from typing_extensions import Final
WEBASSEMBLY_BUILTIN_FLOAT_OPS: Final = ('abs', 'sqrt', 'ceil', 'floor', 'trunc', 'nearest', )
WEBASSEMBLY_BUILTIN_BYTES_OPS: Final = ('len', )
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}"
from .type3 import types as type3types
from .type3.types import Type3, Type3OrPlaceholder, PlaceholderForType, StructType3
class Expression:
"""
An expression within a statement
"""
__slots__ = ('type5', 'sourceref', )
__slots__ = ('type3', )
sourceref: SourceRef
type5: type5typeexpr.TypeExpr | None
type3: Type3OrPlaceholder
def __init__(self, *, sourceref: SourceRef) -> None:
self.sourceref = sourceref
self.type5 = None
def __init__(self) -> None:
self.type3 = PlaceholderForType([self])
class Constant(Expression):
"""
@ -56,90 +38,63 @@ class ConstantPrimitive(Constant):
"""
__slots__ = ('value', )
value: int | float
value: Union[int, float]
def __init__(self, value: int | float, sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
def __init__(self, value: Union[int, float]) -> None:
super().__init__()
self.value = value
def __repr__(self) -> str:
return f'ConstantPrimitive({repr(self.value)})'
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):
class ConstantBytes(Constant):
"""
A bytes constant value expression within a statement
"""
__slots__ = ('value', )
__slots__ = ('value', 'data_block', )
value: bytes
data_block: 'ModuleDataBlock'
def __init__(self, value: bytes, data_block: 'ModuleDataBlock', sourceref: SourceRef) -> None:
super().__init__(data_block, sourceref=sourceref)
def __init__(self, value: bytes, data_block: 'ModuleDataBlock') -> None:
super().__init__()
self.value = value
self.data_block = data_block
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):
class ConstantTuple(Constant):
"""
A Tuple constant value expression within a statement
"""
__slots__ = ('value', )
value: List[Union[ConstantPrimitive, ConstantBytes, 'ConstantTuple', 'ConstantStruct']]
value: List[Union[ConstantPrimitive, ConstantBytes]]
def __init__(self, value: List[Union[ConstantPrimitive, ConstantBytes, 'ConstantTuple', 'ConstantStruct']], data_block: 'ModuleDataBlock', sourceref: SourceRef) -> None:
super().__init__(data_block, sourceref=sourceref)
def __init__(self, value: List[Union[ConstantPrimitive, ConstantBytes]]) -> None: # FIXME: Tuple of tuples?
super().__init__()
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'ConstantTuple({repr(self.value)}, @{repr(self.data_block.address)})'
return f'ConstantTuple({repr(self.value)})'
class ConstantStruct(ConstantMemoryStored):
class ConstantStruct(Constant):
"""
A Struct constant value expression within a statement
"""
__slots__ = ('struct_type5', 'value', )
__slots__ = ('struct_name', 'value', )
struct_type5: type5record.Record
value: list[ConstantPrimitive | ConstantBytes | ConstantTuple | ConstantStruct]
struct_name: str
value: List[Union[ConstantPrimitive, ConstantBytes]]
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
def __init__(self, struct_name: str, value: List[Union[ConstantPrimitive, ConstantBytes]]) -> None: # FIXME: Struct of structs?
super().__init__()
self.struct_name = struct_name
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})'
return f'ConstantStruct({repr(self.struct_name)}, {repr(self.value)})'
class VariableReference(Expression):
"""
@ -149,26 +104,39 @@ class VariableReference(Expression):
variable: Union['ModuleConstantDef', 'FunctionParam'] # also possibly local
def __init__(self, variable: Union['ModuleConstantDef', 'FunctionParam'], sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
def __init__(self, variable: Union['ModuleConstantDef', 'FunctionParam']) -> None:
super().__init__()
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', 'polytype_substitutions', 'left', 'right', )
__slots__ = ('operator', 'left', 'right', )
operator: Function | FunctionParam
polytype_substitutions: dict[type5typeexpr.TypeVariable, type5typeexpr.TypeExpr]
operator: str
left: Expression
right: Expression
def __init__(self, operator: Function | FunctionParam, left: Expression, right: Expression, sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
def __init__(self, operator: str, left: Expression, right: Expression) -> None:
super().__init__()
self.operator = operator
self.polytype_substitutions = {}
self.left = left
self.right = right
@ -179,30 +147,16 @@ class FunctionCall(Expression):
"""
A function call expression within a statement
"""
__slots__ = ('function', 'polytype_substitutions', 'arguments', )
function: Function | FunctionParam
polytype_substitutions: dict[type5typeexpr.TypeVariable, type5typeexpr.TypeExpr]
arguments: List[Expression]
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', )
__slots__ = ('function', 'arguments', )
function: 'Function'
arguments: List[Expression]
def __init__(self, function: 'Function') -> None:
super().__init__()
def __init__(self, function: 'Function', sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
self.function = function
self.arguments = []
class TupleInstantiation(Expression):
"""
@ -212,8 +166,8 @@ class TupleInstantiation(Expression):
elements: List[Expression]
def __init__(self, elements: List[Expression], sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
def __init__(self, elements: List[Expression]) -> None:
super().__init__()
self.elements = elements
@ -227,8 +181,8 @@ class Subscript(Expression):
varref: VariableReference
index: Expression
def __init__(self, varref: VariableReference, index: Expression, sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
def __init__(self, varref: VariableReference, index: Expression) -> None:
super().__init__()
self.varref = varref
self.index = index
@ -237,27 +191,54 @@ class AccessStructMember(Expression):
"""
Access a struct member for reading of writing
"""
__slots__ = ('varref', 'member', )
__slots__ = ('varref', 'struct_type3', 'member', )
varref: VariableReference
struct_type3: StructType3
member: str
def __init__(self, varref: VariableReference, member: str, sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
def __init__(self, varref: VariableReference, struct_type3: StructType3, member: str) -> None:
super().__init__()
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__ = ("sourceref", )
sourceref: SourceRef
def __init__(self, *, sourceref: SourceRef) -> None:
self.sourceref = sourceref
__slots__ = ()
class StatementPass(Statement):
"""
@ -265,23 +246,15 @@ 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, sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
def __init__(self, value: Expression) -> None:
self.value = value
def __repr__(self) -> str:
return f'StatementReturn({repr(self.value)})'
class StatementIf(Statement):
"""
An if statement within a function
@ -297,64 +270,67 @@ class StatementIf(Statement):
self.statements = []
self.else_statements = []
class StatementExpression(Statement):
"""
An expression within a function
The result of the expression is by the code.
"""
__slots__ = ('expr', )
expr: Expression
def __init__(self, expr: Expression) -> None:
self.expr = expr
class FunctionParam:
"""
A parameter for a Function
"""
__slots__ = ('name', 'type5', )
__slots__ = ('name', 'type3', )
name: str
type5: type5typeexpr.TypeExpr
def __init__(self, name: str, type5: type5typeexpr.TypeExpr) -> None:
assert type5typeexpr.is_concrete(type5)
type3: Type3OrPlaceholder
def __init__(self, name: str, type3: Optional[Type3]) -> None:
self.name = name
self.type5 = type5
def __repr__(self) -> str:
return f'FunctionParam({self.name!r}, {self.type5!r})'
self.type3 = PlaceholderForType([self]) if type3 is None else type3
class Function:
"""
A function processes input and produces output
"""
__slots__ = ('name', 'sourceref', 'exported', 'imported', 'statements', 'type5', 'arg_names', )
__slots__ = ('name', 'lineno', 'exported', 'imported', 'statements', 'returns_type3', 'posonlyargs', )
name: str
sourceref: SourceRef
lineno: int
exported: bool
imported: Optional[str]
statements: List[Statement]
type5: type5typeexpr.TypeExpr | type5constrainedexpr.ConstrainedExpr | None
arg_names: list[str]
returns_type3: Type3
posonlyargs: List[FunctionParam]
def __init__(self, name: str, sourceref: SourceRef) -> None:
def __init__(self, name: str, lineno: int) -> None:
self.name = name
self.sourceref = sourceref
self.lineno = lineno
self.exported = False
self.imported = None
self.statements = []
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
self.returns_type3 = type3types.none # FIXME: This could be a placeholder
self.posonlyargs = []
class StructDefinition:
"""
The definition for a struct
"""
__slots__ = ('struct_type5', 'sourceref', )
__slots__ = ('struct_type3', 'lineno', )
struct_type5: type5record.Record
sourceref: SourceRef
struct_type3: StructType3
lineno: int
def __init__(self, struct_type5: type5record.Record, sourceref: SourceRef) -> None:
self.struct_type5 = struct_type5
self.sourceref = sourceref
def __init__(self, struct_type3: StructType3, lineno: int) -> None:
self.struct_type3 = struct_type3
self.lineno = lineno
class StructConstructor(Function):
"""
@ -363,33 +339,38 @@ class StructConstructor(Function):
A function will generated to instantiate a struct. The arguments
will be the defaults
"""
__slots__ = ('struct_type5', )
__slots__ = ('struct_type3', )
struct_type5: type5record.Record
struct_type3: StructType3
def __init__(self, struct_type5: type5record.Record, sourceref: SourceRef) -> None:
super().__init__(f'@{struct_type5.name}@__init___@', sourceref)
self.struct_type5 = struct_type5
def __init__(self, struct_type3: StructType3) -> None:
super().__init__(f'@{struct_type3.name}@__init___@', -1)
for mem, typ in struct_type5.fields:
self.arg_names.append(mem)
self.returns_type3 = struct_type3
for mem, typ in struct_type3.members.items():
self.posonlyargs.append(FunctionParam(mem, typ, ))
self.struct_type3 = struct_type3
class ModuleConstantDef:
"""
A constant definition within a module
"""
__slots__ = ('name', 'sourceref', 'type5', 'constant', )
__slots__ = ('name', 'lineno', 'type3', 'constant', 'data_block', )
name: str
sourceref: SourceRef
type5: type5typeexpr.TypeExpr
lineno: int
type3: Type3
constant: Constant
data_block: Optional['ModuleDataBlock']
def __init__(self, name: str, sourceref: SourceRef, type5: type5typeexpr.TypeExpr, constant: Constant) -> None:
def __init__(self, name: str, lineno: int, type3: Type3, constant: Constant, data_block: Optional['ModuleDataBlock']) -> None:
self.name = name
self.sourceref = sourceref
self.type5 = type5
self.lineno = lineno
self.type3 = type3
self.constant = constant
self.data_block = data_block
class ModuleDataBlock:
"""
@ -397,16 +378,13 @@ class ModuleDataBlock:
"""
__slots__ = ('data', 'address', )
data: List[Union[ConstantPrimitive, ConstantMemoryStored]]
data: List[Union[ConstantPrimitive, ConstantBytes]]
address: Optional[int]
def __init__(self, data: Iterable[Union[ConstantPrimitive, ConstantMemoryStored]]) -> None:
self.data = [*data]
def __init__(self, data: List[Union[ConstantPrimitive, ConstantBytes]]) -> 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
@ -418,32 +396,19 @@ class ModuleData:
def __init__(self) -> None:
self.blocks = []
class Module[G]:
class Module:
"""
A module is a file and consists of functions
"""
__slots__ = ('build', 'filename', 'data', 'types', 'type5s', 'struct_definitions', 'constant_defs', 'functions', 'methods', 'operators', 'functions_table', )
__slots__ = ('data', 'types', 'struct_definitions', 'constant_defs', 'functions',)
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,84 +1,50 @@
"""
Parses the source code from the plain text into a syntax tree
"""
import ast
from typing import Any, Dict, NoReturn, Union
from .build.base import BuildBase
from .build.default import BuildDefault
import ast
from .type3 import types as type3types
from .exceptions import StaticError
from .ourlang import (
AccessStructMember,
BinaryOp,
BuiltinFunction,
ConstantBytes,
ConstantPrimitive,
ConstantStruct,
ConstantTuple,
Expression,
WEBASSEMBLY_BUILTIN_FLOAT_OPS,
Module, ModuleDataBlock,
Function,
FunctionCall,
FunctionParam,
FunctionReference,
Module,
ModuleConstantDef,
ModuleDataBlock,
SourceRef,
Statement,
StatementIf,
StatementPass,
StatementReturn,
StructConstructor,
StructDefinition,
Subscript,
Expression,
BinaryOp,
ConstantPrimitive, ConstantBytes, ConstantTuple, ConstantStruct,
TupleInstantiation,
VariableReference,
FunctionCall, AccessStructMember, Subscript,
StructDefinition, StructConstructor,
UnaryOp, VariableReference,
Fold,
Statement,
StatementIf, StatementPass, StatementReturn,
StatementExpression,
FunctionParam,
ModuleConstantDef,
)
from .type5 import typeexpr as type5typeexpr
from .wasmgenerator import Generator
def phasm_parse(source: str) -> Module[Generator]:
def phasm_parse(source: str) -> Module:
"""
Public method for parsing Phasm code into a Phasm Module
"""
res = ast.parse(source, '')
res = OptimizerTransformer().visit(res)
build = BuildDefault()
our_visitor = OurVisitor(build)
our_visitor = OurVisitor()
return our_visitor.visit_Module(res)
OurLocals = Dict[str, Union[FunctionParam]] # FIXME: Does it become easier if we add ModuleConstantDef to this dict?
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 OurVisitor:
"""
Class to visit a Python syntax tree and create an ourlang syntax tree
@ -86,22 +52,15 @@ class OurVisitor[G]:
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, build: BuildBase[G]) -> None:
self.build = build
def __init__(self) -> None:
pass
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)
def visit_Module(self, node: ast.Module) -> Module:
module = Module()
_not_implemented(not node.type_ignores, 'Module.type_ignores')
@ -113,27 +72,25 @@ class OurVisitor[G]:
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].sourceref.lineno}'
f'{res.name} already defined on line {module.constant_defs[res.name].lineno}'
)
module.constant_defs[res.name] = res
if isinstance(res, StructDefinition):
if res.struct_type5.name in module.types:
if res.struct_type3.name in module.struct_definitions:
raise StaticError(
f'{res.struct_type5.name} already defined as type'
f'{res.struct_type3.name} already defined on line {module.struct_definitions[res.struct_type3.name].lineno}'
)
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
module.struct_definitions[res.struct_type3.name] = res
constructor = StructConstructor(res.struct_type3)
module.functions[constructor.name] = constructor
if isinstance(res, Function):
if res.name in module.functions:
raise StaticError(
f'{res.name} already defined on line {module.functions[res.name].sourceref.lineno}'
f'{res.name} already defined on line {module.functions[res.name].lineno}'
)
module.functions[res.name] = res
@ -145,7 +102,7 @@ class OurVisitor[G]:
return module
def pre_visit_Module_stmt(self, module: Module[G], node: ast.stmt) -> Union[Function, StructDefinition, ModuleConstantDef]:
def pre_visit_Module_stmt(self, module: Module, node: ast.stmt) -> Union[Function, StructDefinition, ModuleConstantDef]:
if isinstance(node, ast.FunctionDef):
return self.pre_visit_Module_FunctionDef(module, node)
@ -157,25 +114,16 @@ class OurVisitor[G]:
raise NotImplementedError(f'{node} on Module')
def pre_visit_Module_FunctionDef(self, module: Module[G], node: ast.FunctionDef) -> Function:
function = Function(node.name, srf(module, node))
def pre_visit_Module_FunctionDef(self, module: Module, node: ast.FunctionDef) -> Function:
function = Function(node.name, node.lineno)
_not_implemented(not node.args.posonlyargs, 'FunctionDef.args.posonlyargs')
arg_type5_list = []
for arg in node.args.args:
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)
function.posonlyargs.append(FunctionParam(
arg.arg,
self.visit_type(module, arg.annotation) if arg.annotation else None,
))
_not_implemented(not node.args.vararg, 'FunctionDef.args.vararg')
_not_implemented(not node.args.kwonlyargs, 'FunctionDef.args.kwonlyargs')
@ -215,23 +163,23 @@ class OurVisitor[G]:
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)
if node.returns is not None: # Note: `-> None` would be a ast.Constant
function.returns_type3 = self.visit_type(module, node.returns)
else:
# FIXME: Mostly works already, needs to fix Function.returns_type3 and have it updated
raise NotImplementedError('Function without an explicit return type')
_not_implemented(not node.type_comment, 'FunctionDef.type_comment')
return function
def pre_visit_Module_ClassDef(self, module: Module[G], node: ast.ClassDef) -> StructDefinition:
def pre_visit_Module_ClassDef(self, module: Module, 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, type5typeexpr.AtomicType | type5typeexpr.TypeApplication] = {}
members: Dict[str, type3types.Type3] = {}
for stmt in node.body:
if not isinstance(stmt, ast.AnnAssign):
@ -240,7 +188,7 @@ class OurVisitor[G]:
if not isinstance(stmt.target, ast.Name):
raise NotImplementedError('Class with default values')
if stmt.value is not None:
if not stmt.value is None:
raise NotImplementedError('Class with default values')
if stmt.simple != 1:
@ -249,60 +197,98 @@ class OurVisitor[G]:
if stmt.target.id in members:
_raise_static_error(stmt, 'Struct members must have unique names')
field_type5 = self.visit_type5(module, stmt.annotation)
assert isinstance(field_type5, (type5typeexpr.AtomicType, type5typeexpr.TypeApplication, ))
members[stmt.target.id] = field_type5
members[stmt.target.id] = self.visit_type(module, stmt.annotation)
return StructDefinition(
module.build.type5_make_struct(node.name, tuple(members.items())),
srf(module, node),
)
return StructDefinition(type3types.StructType3(node.name, members), node.lineno)
def pre_visit_Module_AnnAssign(self, module: Module[G], node: ast.AnnAssign) -> ModuleConstantDef:
def pre_visit_Module_AnnAssign(self, module: Module, node: ast.AnnAssign) -> ModuleConstantDef:
if not isinstance(node.target, ast.Name):
_raise_static_error(node.target, 'Must be name')
if not isinstance(node.target.ctx, ast.Store):
_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)
if isinstance(value_data, ConstantBytes):
data_block = value_data.data_block
else:
data_block = None
return ModuleConstantDef(
node.target.id,
srf(module, node),
self.visit_type5(module, node.annotation),
node.lineno,
type3,
value_data,
data_block,
)
if isinstance(node.value, ast.Tuple):
value_data = self.visit_Module_Constant(module, node.value)
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')
assert isinstance(value_data, ConstantTuple) # type hint
# 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,
srf(module, node),
self.visit_type5(module, node.annotation),
value_data,
node.lineno,
self.visit_type(module, node.annotation),
ConstantTuple(tuple_data),
data_block,
)
if isinstance(node.value, ast.Call):
value_data = self.visit_Module_Constant(module, node.value)
# Struct constant
# Stored in memory like a tuple, so much of the code is the same
assert isinstance(value_data, ConstantStruct) # type hint
if not isinstance(node.value.func, ast.Name):
_raise_static_error(node.value.func, 'Must be name')
if not isinstance(node.value.func.ctx, ast.Load):
_raise_static_error(node.value.func, 'Must be load context')
if not node.value.func.id in module.struct_definitions:
_raise_static_error(node.value.func, 'Undefined struct')
if node.value.keywords:
_raise_static_error(node.value.func, 'Cannot use keywords')
if not isinstance(node.annotation, ast.Name):
_raise_static_error(node.annotation, 'Must be name')
struct_data = [
self.visit_Module_Constant(module, arg_node)
for arg_node in node.value.args
if isinstance(arg_node, ast.Constant)
]
if len(node.value.args) != len(struct_data):
_raise_static_error(node, 'Struct arguments must be constants')
# Allocate the data
data_block = ModuleDataBlock(struct_data)
module.data.blocks.append(data_block)
# Then return the constant as a pointer
return ModuleConstantDef(
node.target.id,
srf(module, node),
self.visit_type5(module, node.annotation),
value_data,
node.lineno,
self.visit_type(module, node.annotation),
ConstantStruct(node.value.func.id, struct_data),
data_block,
)
raise NotImplementedError(f'{node} on Module AnnAssign')
def visit_Module_stmt(self, module: Module[G], node: ast.stmt) -> None:
def visit_Module_stmt(self, module: Module, node: ast.stmt) -> None:
if isinstance(node, ast.FunctionDef):
self.visit_Module_FunctionDef(module, node)
return
@ -315,17 +301,12 @@ class OurVisitor[G]:
raise NotImplementedError(f'{node} on Module')
def visit_Module_FunctionDef(self, module: Module[G], node: ast.FunctionDef) -> None:
def visit_Module_FunctionDef(self, module: Module, 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 = {
a_nam: FunctionParam(a_nam, a_typ)
for a_nam, a_typ in zip(function.arg_names, func_arg_types, strict=True)
x.name: x
for x in function.posonlyargs
}
for stmt in node.body:
@ -333,15 +314,14 @@ class OurVisitor[G]:
self.visit_Module_FunctionDef_stmt(module, function, our_locals, stmt)
)
def visit_Module_FunctionDef_stmt(self, module: Module[G], function: Function, our_locals: OurLocals, node: ast.stmt) -> Statement:
def visit_Module_FunctionDef_stmt(self, module: Module, 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),
srf(module, node),
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.value)
)
if isinstance(node, ast.If):
@ -362,14 +342,15 @@ class OurVisitor[G]:
return result
if isinstance(node, ast.Pass):
return StatementPass(srf(module, node))
return StatementPass()
if isinstance(node, ast.Expr):
return StatementExpression(self.visit_Module_FunctionDef_expr(module, function, our_locals, node.value))
raise NotImplementedError(f'{node} as stmt in FunctionDef')
def visit_Module_FunctionDef_expr(self, module: Module[G], function: Function, our_locals: OurLocals, node: ast.expr) -> Expression:
def visit_Module_FunctionDef_expr(self, module: Module, 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):
@ -378,10 +359,6 @@ class OurVisitor[G]:
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):
@ -395,14 +372,23 @@ class OurVisitor[G]:
else:
raise NotImplementedError(f'Operator {node.op}')
if operator not in module.operators:
raise NotImplementedError(f'Operator {operator}')
return BinaryOp(
BuiltinFunction(operator, module.operators[operator]),
operator,
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.left),
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.right),
srf(module, node),
)
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),
)
if isinstance(node, ast.Compare):
@ -411,27 +397,17 @@ class OurVisitor[G]:
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}')
if operator not in module.operators:
raise NotImplementedError(f'Operator {operator}')
return BinaryOp(
BuiltinFunction(operator, module.operators[operator]),
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):
@ -458,15 +434,11 @@ class OurVisitor[G]:
if node.id in our_locals:
param = our_locals[node.id]
return VariableReference(param, srf(module, node))
return VariableReference(param)
if node.id in module.constant_defs:
cdef = module.constant_defs[node.id]
return VariableReference(cdef, srf(module, node))
if node.id in module.functions:
fun = module.functions[node.id]
return FunctionReference(fun, srf(module, node))
return VariableReference(cdef)
_raise_static_error(node, f'Undefined variable {node.id}')
@ -474,17 +446,17 @@ class OurVisitor[G]:
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 isinstance(arg_node, ast.Constant)
]
if len(arguments) != len(node.elts):
raise NotImplementedError('Non-constant tuple members')
return TupleInstantiation(arguments, srf(module, node))
return TupleInstantiation(arguments)
raise NotImplementedError(f'{node} as expr in FunctionDef')
def visit_Module_FunctionDef_Call(self, module: Module[G], function: Function, our_locals: OurLocals, node: ast.Call) -> Union[FunctionCall]:
def visit_Module_FunctionDef_Call(self, module: Module, function: Function, our_locals: OurLocals, node: ast.Call) -> Union[Fold, FunctionCall, UnaryOp]:
if node.keywords:
_raise_static_error(node, 'Keyword calling not supported') # Yet?
@ -493,26 +465,76 @@ class OurVisitor[G]:
if not isinstance(node.func.ctx, ast.Load):
_raise_static_error(node, 'Must be load context')
func: Union[Function, FunctionParam]
if node.func.id in module.struct_definitions:
struct_definition = module.struct_definitions[node.func.id]
struct_constructor = StructConstructor(struct_definition.struct_type3)
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]
# 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')
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':
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, you cannot return a function
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')
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]),
)
else:
if node.func.id not in module.functions:
_raise_static_error(node, 'Call to undefined function')
func = module.functions[node.func.id]
result = FunctionCall(func, sourceref=srf(module, node))
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.arguments.extend(
self.visit_Module_FunctionDef_expr(module, function, our_locals, arg_expr)
for arg_expr in node.args
for arg_expr, param in zip(node.args, func.posonlyargs)
)
return result
def visit_Module_FunctionDef_Attribute(self, module: Module[G], function: Function, our_locals: OurLocals, node: ast.Attribute) -> Expression:
def visit_Module_FunctionDef_Attribute(self, module: Module, function: Function, our_locals: OurLocals, node: ast.Attribute) -> Expression:
if not isinstance(node.value, ast.Name):
_raise_static_error(node, 'Must reference a name')
@ -523,13 +545,16 @@ class OurVisitor[G]:
if not isinstance(varref, VariableReference):
_raise_static_error(node.value, 'Must refer to variable')
if not isinstance(varref.variable.type3, type3types.StructType3):
_raise_static_error(node.value, 'Must refer to struct')
return AccessStructMember(
varref,
varref.variable.type3,
node.attr,
srf(module, node),
)
def visit_Module_FunctionDef_Subscript(self, module: Module[G], function: Function, our_locals: OurLocals, node: ast.Subscript) -> Expression:
def visit_Module_FunctionDef_Subscript(self, module: Module, function: Function, our_locals: OurLocals, node: ast.Subscript) -> Expression:
if not isinstance(node.value, ast.Name):
_raise_static_error(node, 'Must reference a name')
@ -542,10 +567,10 @@ class OurVisitor[G]:
varref: VariableReference
if node.value.id in our_locals:
param = our_locals[node.value.id]
varref = VariableReference(param, srf(module, node))
varref = VariableReference(param)
elif node.value.id in module.constant_defs:
constant_def = module.constant_defs[node.value.id]
varref = VariableReference(constant_def, srf(module, node))
varref = VariableReference(constant_def)
else:
_raise_static_error(node, f'Undefined variable {node.value.id}')
@ -553,139 +578,95 @@ class OurVisitor[G]:
module, function, our_locals, node.slice,
)
return Subscript(varref, slice_expr, srf(module, node))
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, ))
]
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))
return Subscript(varref, slice_expr)
def visit_Module_Constant(self, module: Module, node: ast.Constant) -> Union[ConstantPrimitive, ConstantBytes]:
_not_implemented(node.kind is None, 'Constant.kind')
if isinstance(node.value, (int, float, )):
return ConstantPrimitive(node.value, srf(module, node))
return ConstantPrimitive(node.value)
if isinstance(node.value, bytes):
data_block = ModuleDataBlock([])
module.data.blocks.append(data_block)
result = ConstantBytes(node.value, data_block, srf(module, node))
result = ConstantBytes(node.value, data_block)
data_block.data.append(result)
return result
raise NotImplementedError(f'{node.value} as constant')
def visit_type5(self, module: Module[G], node: ast.expr) -> type5typeexpr.TypeExpr:
def visit_type(self, module: Module, node: ast.expr) -> type3types.Type3:
if isinstance(node, ast.Call):
if not isinstance(node.func, ast.Name):
_raise_static_error(node, 'Can only call Monads by name')
if node.keywords:
_raise_static_error(node, 'Monads cannot have keyword arguments')
monad_type = self.visit_type(module, node.func)
if not isinstance(monad_type, type3types.MonadType3):
_raise_static_error(node, 'Must be a Monad')
return type3types.AppliedType3(
monad_type,
(self.visit_type(module, x) for x in node.args)
)
if isinstance(node, ast.Constant):
if node.value is None:
return module.build.none_type5
return type3types.none
_raise_static_error(node, f'Unrecognized type {node.value!r}')
_raise_static_error(node, f'Unrecognized type {node.value}')
if isinstance(node, ast.Name):
if not isinstance(node.ctx, ast.Load):
_raise_static_error(node, 'Must be load context')
if node.id in module.types:
return module.types[node.id]
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
_raise_static_error(node, f'Unrecognized type {node.id}')
if isinstance(node, ast.Subscript):
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 not isinstance(node.value, ast.Name):
_raise_static_error(node, 'Must be name')
if isinstance(node.slice, ast.Slice):
_raise_static_error(node, 'Must subscript using an index')
if not isinstance(node.slice, ast.Constant):
_raise_static_error(node, 'Must subscript using a constant index')
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')
return module.build.type5_make_static_array(
node.slice.value,
self.visit_type5(module, node.value),
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), type3types.IntType3(node.slice.value)],
)
if isinstance(node, ast.Tuple):
if not isinstance(node.ctx, ast.Load):
_raise_static_error(node, 'Must be load context')
return module.build.type5_make_tuple(
[self.visit_type5(module, elt) for elt in node.elts],
return type3types.AppliedType3(
type3types.tuple,
(self.visit_type(module, elt) for elt in node.elts)
)
raise NotImplementedError(node)
raise NotImplementedError(f'{node} as type')
def _not_implemented(check: Any, msg: str) -> None:
if not check:
raise NotImplementedError(msg)
def _raise_static_error(node: Union[ast.stmt, ast.expr], msg: str) -> NoReturn:
def _raise_static_error(node: Union[ast.mod, 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,8 +1,7 @@
"""
stdlib: Memory allocation
"""
from phasm.wasmgenerator import Generator, func_wrapper
from phasm.wasmgenerator import VarType_i32 as i32
from phasm.wasmgenerator import Generator, VarType_i32 as i32, func_wrapper
IDENTIFIER = 0xA1C0
@ -15,7 +14,7 @@ UNALLOC_PTR = ADR_UNALLOC_PTR + 4
# For memory initialization see phasm.compiler.module_data
@func_wrapper()
@func_wrapper(exported=False)
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)
@ -32,7 +31,7 @@ def __find_free_block__(g: Generator, alloc_size: i32) -> i32:
return i32('return') # To satisfy mypy
@func_wrapper(exported=True)
@func_wrapper()
def __alloc__(g: Generator, alloc_size: i32) -> i32:
result = i32('result')

View File

@ -1,13 +1,11 @@
"""
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(exported=True)
@func_wrapper()
def __alloc_bytes__(g: Generator, length: i32) -> i32:
"""
Allocates room for a bytes instance, but does not write
@ -34,400 +32,35 @@ def __alloc_bytes__(g: Generator, length: i32) -> i32:
return i32('return') # To satisfy mypy
@func_wrapper()
def __u32_min__(g: Generator, x: i32, y: i32) -> i32:
g.local.get(x)
g.local.get(y)
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()
g.i32.lt_u()
with g.if_():
g.local.get(x)
# 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.return_()
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
# The offset is outside the allocated bytes
g.i32.const(0)
g.return_()
return i32('return') # To satisfy mypy

View File

@ -0,0 +1,6 @@
from . import splitoncall
from phasm.wasm import Module
def transform(module: Module) -> None:
splitoncall.transform(module)

View File

@ -0,0 +1,63 @@
from typing import List
from phasm.wasm import (
Function, Module, Statement
)
def is_imported(module: Module, name: str) -> bool:
for imprt in module.imports:
if name == imprt.intname:
return True
return False
def split_on_call_function(module: Module, function: Function) -> List[Function]:
function_list = []
statement_list = []
idx = 0
while function.statements:
stmt = function.statements.pop(0)
if not stmt.name == 'call' or is_imported(module, stmt.args[0][1:]):
statement_list.append(stmt)
continue
function_list.append(Function(
f'{function.name}.{idx}',
None,
function.params,
function.locals,
function.result,
statement_list + [stmt] + [Statement('call', f'${function.name}.{idx + 1}')]
))
statement_list = []
idx += 1
function_list.append(Function(
f'{function.name}.{idx}',
None,
function.params,
function.locals,
function.result,
statement_list
))
if function.exported_name:
function_list.append(Function(
function.name + '.e',
function.exported_name,
function.params,
[],
function.result,
[
Statement('local.get', '$' + x[0])
for x in function.params
] + [Statement('call', f'${function.name}.0')]
))
return function_list
def transform(module: Module) -> None:
new_functions = []
for func in module.functions:
new_functions.extend(split_on_call_function(module, func))
module.functions = new_functions

555
phasm/type3/constraints.py Normal file
View File

@ -0,0 +1,555 @@
"""
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, Optional, List, Tuple, Union
from .. import ourlang
from . import types
class Error:
"""
An error returned by the check functions for a contraint
This means the programmer has to make some kind of chance to the
typing of their program before the compiler can do its thing.
"""
def __init__(self, msg: str) -> None:
self.msg = msg
def __repr__(self) -> str:
return f'Error({repr(self.msg)})'
class RequireTypeSubstitutes:
"""
Returned by the check function for a contraint if they do not have all
their types substituted yet.
Hopefully, another constraint will give the right information about the
typing of the program, so this constraint can be updated.
"""
SubstitutionMap = Dict[types.PlaceholderForType, types.Type3]
NewConstraintList = List['ConstraintBase']
CheckResult = Union[None, SubstitutionMap, Error, NewConstraintList, RequireTypeSubstitutes]
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__ = ('comment', )
comment: Optional[str]
"""
A comment to help the programmer with debugging the types in their program
"""
def __init__(self, comment: Optional[str] = None) -> None:
self.comment = comment
def check(self) -> CheckResult:
"""
Checks if the constraint hold
This function can return an error, if the constraint does not hold,
which indicates an error in the typing of the input program.
This function can return RequireTypeSubstitutes(), if we cannot deduce
all the types yet.
This function can return a SubstitutionMap, if during the evaluation
of the contraint we discovered new types. In this case, the constraint
is expected to hold.
This function can return None, if the constraint holds, but no new
information was deduced from evaluating this constraint.
"""
raise NotImplementedError(self.__class__, self.check)
def human_readable(self) -> HumanReadableRet:
"""
Returns a more human readable form of this constraint
"""
return repr(self), {}
class SameTypeConstraint(ConstraintBase):
"""
Verifies that a number of types all are the same type
"""
__slots__ = ('type_list', )
type_list: List[types.Type3OrPlaceholder]
def __init__(self, *type_list: types.Type3OrPlaceholder, comment: Optional[str] = None) -> None:
super().__init__(comment=comment)
assert len(type_list) > 1
self.type_list = [*type_list]
def check(self) -> CheckResult:
known_types: List[types.Type3] = []
placeholders = []
do_applied_placeholder_check: bool = False
for typ in self.type_list:
if isinstance(typ, types.IntType3):
known_types.append(typ)
continue
if isinstance(typ, (types.PrimitiveType3, types.StructType3, )):
known_types.append(typ)
continue
if isinstance(typ, types.AppliedType3):
known_types.append(typ)
do_applied_placeholder_check = True
continue
if isinstance(typ, types.PlaceholderForType):
if typ.resolve_as is not None:
known_types.append(typ.resolve_as)
else:
placeholders.append(typ)
continue
raise NotImplementedError(typ)
if not known_types:
return RequireTypeSubstitutes()
new_constraint_list: List[ConstraintBase] = []
first_type = known_types[0]
for typ in known_types[1:]:
if isinstance(first_type, types.AppliedType3) and isinstance(typ, types.AppliedType3):
if len(first_type.args) != len(typ.args):
return Error('Mismatch between applied types argument count')
if first_type.base != typ.base:
return Error('Mismatch between applied types base')
for first_type_arg, typ_arg in zip(first_type.args, typ.args):
new_constraint_list.append(SameTypeConstraint(
first_type_arg, typ_arg
))
continue
if typ != first_type:
return Error(f'{typ:s} must be {first_type:s} instead')
if new_constraint_list:
# If this happens, make CheckResult a class that can have both
assert not placeholders, 'Cannot (yet) return both new placeholders and new constraints'
return new_constraint_list
if not placeholders:
return None
for typ in placeholders:
typ.resolve_as = first_type
return {
typ: first_type
for typ in placeholders
}
def human_readable(self) -> HumanReadableRet:
return (
' == '.join('{t' + str(idx) + '}' for idx in range(len(self.type_list))),
{
't' + str(idx): typ
for idx, typ in enumerate(self.type_list)
},
)
def __repr__(self) -> str:
args = ', '.join(repr(x) for x in self.type_list)
return f'SameTypeConstraint({args}, comment={repr(self.comment)})'
class IntegerCompareConstraint(ConstraintBase):
"""
Verifies that the given IntType3 are in order (<=)
"""
__slots__ = ('int_type3_list', )
int_type3_list: List[types.IntType3]
def __init__(self, *int_type3: types.IntType3, comment: Optional[str] = None) -> None:
super().__init__(comment=comment)
assert len(int_type3) > 1
self.int_type3_list = [*int_type3]
def check(self) -> CheckResult:
val_list = [x.value for x in self.int_type3_list]
prev_val = val_list.pop(0)
for next_val in val_list:
if prev_val > next_val:
return Error(f'{prev_val} must be less or equal than {next_val}')
prev_val = next_val
return None
def human_readable(self) -> HumanReadableRet:
return (
' <= '.join('{t' + str(idx) + '}' for idx in range(len(self.int_type3_list))),
{
't' + str(idx): typ
for idx, typ in enumerate(self.int_type3_list)
},
)
def __repr__(self) -> str:
args = ', '.join(repr(x) for x in self.int_type3_list)
return f'IntegerCompareConstraint({args}, comment={repr(self.comment)})'
class CastableConstraint(ConstraintBase):
"""
A type can be cast to another type
"""
__slots__ = ('from_type3', 'to_type3', )
from_type3: types.Type3OrPlaceholder
to_type3: types.Type3OrPlaceholder
def __init__(self, from_type3: types.Type3OrPlaceholder, to_type3: types.Type3OrPlaceholder, comment: Optional[str] = None) -> None:
super().__init__(comment=comment)
self.from_type3 = from_type3
self.to_type3 = to_type3
def check(self) -> CheckResult:
ftyp = self.from_type3
if isinstance(ftyp, types.PlaceholderForType) and ftyp.resolve_as is not None:
ftyp = ftyp.resolve_as
ttyp = self.to_type3
if isinstance(ttyp, types.PlaceholderForType) and ttyp.resolve_as is not None:
ttyp = ttyp.resolve_as
if isinstance(ftyp, types.PlaceholderForType) or isinstance(ttyp, types.PlaceholderForType):
return RequireTypeSubstitutes()
if ftyp is types.u8 and ttyp is types.u32:
return None
return Error(f'Cannot cast {ftyp.name} to {ttyp.name}')
def human_readable(self) -> HumanReadableRet:
return (
'{to_type3}({from_type3})',
{
'to_type3': self.to_type3,
'from_type3': self.from_type3,
},
)
def __repr__(self) -> str:
return f'CastableConstraint({repr(self.from_type3)}, {repr(self.to_type3)}, comment={repr(self.comment)})'
class MustImplementTypeClassConstraint(ConstraintBase):
"""
A type must implement a given type class
"""
__slots__ = ('type_class3', 'type3', )
type_class3: str
type3: types.Type3OrPlaceholder
DATA = {
'u8': {'BitWiseOperation', 'BasicMathOperation', 'EqualComparison', 'StrictPartialOrder'},
'u32': {'BitWiseOperation', 'BasicMathOperation', 'EqualComparison', 'StrictPartialOrder'},
'u64': {'BitWiseOperation', 'BasicMathOperation', 'EqualComparison', 'StrictPartialOrder'},
'i32': {'BasicMathOperation', 'EqualComparison', 'StrictPartialOrder'},
'i64': {'BasicMathOperation', 'EqualComparison', 'StrictPartialOrder'},
'bytes': {'Foldable', 'Sized'},
'f32': {'BasicMathOperation', 'FloatingPoint'},
'f64': {'BasicMathOperation', 'FloatingPoint'},
}
def __init__(self, type_class3: str, type3: types.Type3OrPlaceholder, comment: Optional[str] = None) -> None:
super().__init__(comment=comment)
self.type_class3 = type_class3
self.type3 = type3
def check(self) -> CheckResult:
typ = self.type3
if isinstance(typ, types.PlaceholderForType) and typ.resolve_as is not None:
typ = typ.resolve_as
if isinstance(typ, types.PlaceholderForType):
return RequireTypeSubstitutes()
if self.type_class3 in self.__class__.DATA.get(typ.name, set()):
return None
return Error(f'{typ.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)}, comment={repr(self.comment)})'
class LiteralFitsConstraint(ConstraintBase):
"""
A literal value fits a given type
"""
__slots__ = ('type3', 'literal', )
type3: types.Type3OrPlaceholder
literal: Union[ourlang.ConstantPrimitive, ourlang.ConstantBytes, ourlang.ConstantTuple, ourlang.ConstantStruct]
def __init__(
self,
type3: types.Type3OrPlaceholder,
literal: Union[ourlang.ConstantPrimitive, ourlang.ConstantBytes, ourlang.ConstantTuple, ourlang.ConstantStruct],
comment: Optional[str] = None,
) -> None:
super().__init__(comment=comment)
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,
}
if isinstance(self.type3, types.PlaceholderForType):
if self.type3.resolve_as is None:
return RequireTypeSubstitutes()
self.type3 = self.type3.resolve_as
if self.type3.name in int_table:
bts, sgn = int_table[self.type3.name]
if isinstance(self.literal.value, int):
try:
self.literal.value.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 self.type3.name in float_table:
_ = float_table[self.type3.name]
if isinstance(self.literal.value, float):
# FIXME: Bit check
return None
return Error('Must be real') # FIXME: Add line information
if self.type3 is types.bytes:
if isinstance(self.literal.value, bytes):
return None
return Error('Must be bytes') # FIXME: Add line information
res: NewConstraintList
if isinstance(self.type3, types.AppliedType3):
if self.type3.base == types.tuple:
if not isinstance(self.literal, ourlang.ConstantTuple):
return Error('Must be tuple')
if len(self.type3.args) != len(self.literal.value):
return Error('Tuple element count mismatch')
res = []
res.extend(
LiteralFitsConstraint(x, y)
for x, y in zip(self.type3.args, self.literal.value)
)
res.extend(
SameTypeConstraint(x, y.type3)
for x, y in zip(self.type3.args, self.literal.value)
)
return res
if self.type3.base == types.static_array:
if not isinstance(self.literal, ourlang.ConstantTuple):
return Error('Must be tuple')
assert 2 == len(self.type3.args)
assert isinstance(self.type3.args[1], types.IntType3)
if self.type3.args[1].value != len(self.literal.value):
return Error('Member count mismatch')
res = []
res.extend(
LiteralFitsConstraint(self.type3.args[0], y)
for y in self.literal.value
)
res.extend(
SameTypeConstraint(self.type3.args[0], y.type3)
for y in self.literal.value
)
return res
if isinstance(self.type3, types.StructType3):
if not isinstance(self.literal, ourlang.ConstantStruct):
return Error('Must be struct')
if self.literal.struct_name != self.type3.name:
return Error('Struct mismatch')
if len(self.type3.members) != len(self.literal.value):
return Error('Struct element count mismatch')
res = []
res.extend(
LiteralFitsConstraint(x, y)
for x, y in zip(self.type3.members.values(), self.literal.value)
)
res.extend(
SameTypeConstraint(x_t, y.type3, comment=f'{self.literal.struct_name}.{x_n}')
for (x_n, x_t, ), y in zip(self.type3.members.items(), self.literal.value)
)
return res
raise NotImplementedError(self.type3, self.literal)
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)}, comment={repr(self.comment)})'
class CanBeSubscriptedConstraint(ConstraintBase):
"""
A value that is subscipted, i.e. a[0] (tuple) or a[b] (static array)
"""
__slots__ = ('ret_type3', 'type3', 'index', 'index_type3', )
ret_type3: types.Type3OrPlaceholder
type3: types.Type3OrPlaceholder
index: ourlang.Expression
index_type3: types.Type3OrPlaceholder
def __init__(self, ret_type3: types.Type3OrPlaceholder, type3: types.Type3OrPlaceholder, index: ourlang.Expression, comment: Optional[str] = None) -> None:
super().__init__(comment=comment)
self.ret_type3 = ret_type3
self.type3 = type3
self.index = index
self.index_type3 = index.type3
def check(self) -> CheckResult:
if isinstance(self.type3, types.PlaceholderForType):
if self.type3.resolve_as is None:
return RequireTypeSubstitutes()
self.type3 = self.type3.resolve_as
if isinstance(self.type3, types.AppliedType3):
if self.type3.base == types.static_array:
result: List[ConstraintBase] = [
SameTypeConstraint(types.u32, self.index_type3, comment='([]) :: Subscriptable a => a b -> u32 -> b'),
SameTypeConstraint(self.type3.args[0], self.ret_type3, comment='([]) :: Subscriptable a => a b -> u32 -> b'),
]
if isinstance(self.index, ourlang.ConstantPrimitive):
assert isinstance(self.index.value, int)
assert isinstance(self.type3.args[1], types.IntType3)
result.append(
IntegerCompareConstraint(
types.IntType3(0), types.IntType3(self.index.value), types.IntType3(self.type3.args[1].value - 1),
comment='Subscript static array must fit the size of the array'
)
)
return result
if self.type3.base == types.tuple:
if not isinstance(self.index, ourlang.ConstantPrimitive):
return Error('Must index with literal')
if not isinstance(self.index.value, int):
return Error('Must index with integer literal')
if self.index.value < 0 or len(self.type3.args) <= self.index.value:
return Error('Tuple index out of range')
return [
SameTypeConstraint(types.u32, self.index_type3, comment=f'Tuple subscript index {self.index.value}'),
SameTypeConstraint(self.type3.args[self.index.value], self.ret_type3, comment=f'Tuple subscript index {self.index.value}'),
]
if self.type3 is types.bytes:
return [
SameTypeConstraint(types.u32, self.index_type3, comment='([]) :: bytes -> u32 -> u8'),
SameTypeConstraint(types.u8, self.ret_type3, comment='([]) :: bytes -> u32 -> u8'),
]
if self.type3.name in types.LOOKUP_TABLE:
return Error(f'{self.type3.name} cannot be subscripted')
raise NotImplementedError(self.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)}, comment={repr(self.comment)})'

View File

@ -0,0 +1,229 @@
"""
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,
CastableConstraint, CanBeSubscriptedConstraint,
LiteralFitsConstraint, MustImplementTypeClassConstraint, SameTypeConstraint,
)
from . import types as type3types
ConstraintGenerator = Generator[ConstraintBase, None, None]
def phasm_type3_generate_constraints(inp: ourlang.Module) -> List[ConstraintBase]:
ctx = Context()
return [*module(ctx, inp)]
def constant(ctx: Context, inp: ourlang.Constant) -> ConstraintGenerator:
if isinstance(inp, (ourlang.ConstantPrimitive, ourlang.ConstantBytes, ourlang.ConstantTuple, ourlang.ConstantStruct)):
yield LiteralFitsConstraint(inp.type3, inp)
return
raise NotImplementedError(constant, inp)
def expression(ctx: Context, inp: ourlang.Expression) -> ConstraintGenerator:
if isinstance(inp, ourlang.Constant):
yield from constant(ctx, inp)
return
if isinstance(inp, ourlang.VariableReference):
yield SameTypeConstraint(inp.variable.type3, inp.type3,
comment=f'typeOf("{inp.variable.name}") == typeOf({inp.variable.name})')
return
if isinstance(inp, ourlang.UnaryOp):
if 'len' == inp.operator:
yield from expression(ctx, inp.right)
yield MustImplementTypeClassConstraint('Sized', inp.right.type3)
yield SameTypeConstraint(type3types.u32, inp.type3, comment='len :: Sized a => a -> u32')
return
if 'sqrt' == inp.operator:
yield from expression(ctx, inp.right)
yield MustImplementTypeClassConstraint('FloatingPoint', inp.right.type3)
yield SameTypeConstraint(inp.right.type3, inp.type3, comment='sqrt :: FloatingPoint a => a -> a')
return
if 'cast' == inp.operator:
yield from expression(ctx, inp.right)
yield CastableConstraint(inp.right.type3, inp.type3)
return
raise NotImplementedError(expression, inp, inp.operator)
if isinstance(inp, ourlang.BinaryOp):
if inp.operator in ('|', '&', '^', ):
yield from expression(ctx, inp.left)
yield from expression(ctx, inp.right)
yield MustImplementTypeClassConstraint('BitWiseOperation', inp.left.type3)
yield SameTypeConstraint(inp.left.type3, inp.right.type3, inp.type3,
comment=f'({inp.operator}) :: a -> a -> a')
return
if inp.operator in ('>>', '<<', ):
yield from expression(ctx, inp.left)
yield from expression(ctx, inp.right)
yield MustImplementTypeClassConstraint('BitWiseOperation', inp.left.type3)
yield SameTypeConstraint(inp.left.type3, inp.right.type3, inp.type3,
comment=f'({inp.operator}) :: a -> a -> a')
return
if inp.operator in ('+', '-', '*', '/', ):
yield from expression(ctx, inp.left)
yield from expression(ctx, inp.right)
yield MustImplementTypeClassConstraint('BasicMathOperation', inp.left.type3)
yield SameTypeConstraint(inp.left.type3, inp.right.type3, inp.type3,
comment=f'({inp.operator}) :: a -> a -> a')
return
if inp.operator == '==':
yield from expression(ctx, inp.left)
yield from expression(ctx, inp.right)
yield MustImplementTypeClassConstraint('EqualComparison', inp.left.type3)
yield SameTypeConstraint(inp.left.type3, inp.right.type3,
comment=f'({inp.operator}) :: a -> a -> bool')
yield SameTypeConstraint(inp.type3, type3types.bool_,
comment=f'({inp.operator}) :: a -> a -> bool')
return
if inp.operator in ('<', '>'):
yield from expression(ctx, inp.left)
yield from expression(ctx, inp.right)
yield MustImplementTypeClassConstraint('StrictPartialOrder', inp.left.type3)
yield SameTypeConstraint(inp.left.type3, inp.right.type3,
comment=f'({inp.operator}) :: a -> a -> bool')
yield SameTypeConstraint(inp.type3, type3types.bool_,
comment=f'({inp.operator}) :: a -> a -> bool')
return
raise NotImplementedError(expression, inp)
if isinstance(inp, ourlang.FunctionCall):
yield SameTypeConstraint(inp.function.returns_type3, inp.type3,
comment=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,
comment=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.TupleInstantiation):
r_type = []
for arg in inp.elements:
yield from expression(ctx, arg)
r_type.append(arg.type3)
yield SameTypeConstraint(
inp.type3,
type3types.AppliedType3(type3types.tuple, r_type),
comment=f'The type of a tuple is a combination of its members'
)
return
if isinstance(inp, ourlang.Subscript):
yield from expression(ctx, inp.varref)
yield from expression(ctx, inp.index)
yield CanBeSubscriptedConstraint(inp.type3, inp.varref.type3, inp.index)
return
if isinstance(inp, ourlang.AccessStructMember):
yield from expression(ctx, inp.varref)
yield SameTypeConstraint(inp.struct_type3.members[inp.member], inp.type3,
comment=f'The type of a struct member reference is the same as the type of struct member {inp.struct_type3.name}.{inp.member}')
return
if isinstance(inp, ourlang.Fold):
yield from expression(ctx, inp.base)
yield from expression(ctx, inp.iter)
yield SameTypeConstraint(inp.func.posonlyargs[0].type3, inp.func.returns_type3, inp.base.type3, inp.type3,
comment='foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b')
yield MustImplementTypeClassConstraint('Foldable', inp.iter.type3)
return
raise NotImplementedError(expression, inp)
def statement_return(ctx: Context, fun: ourlang.Function, inp: ourlang.StatementReturn) -> ConstraintGenerator:
yield from expression(ctx, inp.value)
yield SameTypeConstraint(fun.returns_type3, inp.value.type3,
comment=f'The type of the value returned from function {fun.name} should match its return type')
def statement_if(ctx: Context, fun: ourlang.Function, inp: ourlang.StatementIf) -> ConstraintGenerator:
yield from expression(ctx, inp.test)
yield SameTypeConstraint(inp.test.type3, type3types.bool_,
comment=f'Must pass a boolean expression to if')
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_expression(ctx: Context, fun: ourlang.Function, inp: ourlang.StatementExpression) -> ConstraintGenerator:
yield from expression(ctx, inp.expr)
yield SameTypeConstraint(fun.returns_type3, inp.expr.type3,
comment=f'TODO')
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
if isinstance(inp, ourlang.StatementExpression):
yield from statement_expression(ctx, fun, inp)
return
raise NotImplementedError(statement, fun, inp)
def function(ctx: Context, inp: ourlang.Function) -> ConstraintGenerator:
assert not inp.imported
if isinstance(inp, ourlang.StructConstructor):
return
for stmt in inp.statements:
yield from statement(ctx, inp, stmt)
def module_constant_def(ctx: Context, inp: ourlang.ModuleConstantDef) -> ConstraintGenerator:
yield from constant(ctx, inp.constant)
yield SameTypeConstraint(inp.type3, inp.constant.type3,
comment=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) -> 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)

154
phasm/type3/entry.py Normal file
View File

@ -0,0 +1,154 @@
"""
Entry point to the type3 system
"""
from typing import Any, Dict, List, Set
from .. import codestyle
from .. import ourlang
from .constraints import ConstraintBase, Error, RequireTypeSubstitutes, SameTypeConstraint, SubstitutionMap
from .constraintsgenerator import phasm_type3_generate_constraints
from .types import AppliedType3, IntType3, PlaceholderForType, PrimitiveType3, StructType3, Type3, Type3OrPlaceholder
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] = {}
error_list: List[Error] = []
for _ in range(MAX_RESTACK_COUNT):
if verbose:
print()
print_constraint_list(placeholder_id_map, constraint_list, placeholder_substitutes)
old_constraint_ids = {id(x) for x in constraint_list}
old_placeholder_substitutes_len = len(placeholder_substitutes)
new_constraint_list = []
for constraint in constraint_list:
check_result = constraint.check()
if check_result is None:
if verbose:
print_constraint(placeholder_id_map, constraint)
print('-> Constraint checks out')
continue
if isinstance(check_result, dict):
placeholder_substitutes.update(check_result)
if verbose:
print_constraint(placeholder_id_map, constraint)
print('-> Constraint checks out, and gave us new information')
continue
if isinstance(check_result, Error):
error_list.append(check_result)
if verbose:
print_constraint(placeholder_id_map, constraint)
print('-> Got an error')
continue
if isinstance(check_result, RequireTypeSubstitutes):
new_constraint_list.append(constraint)
if verbose:
print_constraint(placeholder_id_map, constraint)
print('-> Back on the todo list')
continue
if isinstance(check_result, list):
new_constraint_list.extend(check_result)
if verbose:
print_constraint(placeholder_id_map, constraint)
print(f'-> Resulted in {len(check_result)} new constraints')
continue
raise NotImplementedError(constraint, check_result)
if not new_constraint_list:
constraint_list = new_constraint_list
break
# Infinite loop detection
new_constraint_ids = {id(x) for x in new_constraint_list}
new_placeholder_substitutes_len = len(placeholder_substitutes)
if old_constraint_ids == new_constraint_ids and old_placeholder_substitutes_len == new_placeholder_substitutes_len:
if error_list:
raise Type3Exception(error_list)
raise Exception('Cannot type this program - not enough information')
constraint_list = new_constraint_list
if constraint_list:
raise Exception(f'Cannot type this program - tried {MAX_RESTACK_COUNT} iterations')
if error_list:
raise Type3Exception(error_list)
# FIXME: This doesn't work with e.g. `:: [a] -> a`, as the placeholder is inside a type
for plh, typ in placeholder_substitutes.items():
for expr in plh.update_on_substitution:
assert expr.type3 is plh
expr.type3 = typ
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) or isinstance(fmt_val, PlaceholderForType):
fmt_val = get_printable_type_name(fmt_val, placeholder_id_map)
if not isinstance(fmt_val, str):
fmt_val = repr(fmt_val)
act_fmt[fmt_key] = fmt_val
if constraint.comment is not None:
print('- ' + txt.format(**act_fmt).ljust(40) + '; ' + constraint.comment)
else:
print('- ' + txt.format(**act_fmt))
def get_printable_type_name(inp: Type3OrPlaceholder, placeholder_id_map: Dict[int, str]) -> str:
if isinstance(inp, (PrimitiveType3, StructType3, IntType3, )):
return inp.name
if isinstance(inp, PlaceholderForType):
placeholder_id = id(inp)
if placeholder_id not in placeholder_id_map:
placeholder_id_map[placeholder_id] = 'T' + str(len(placeholder_id_map) + 1)
return placeholder_id_map[placeholder_id]
if isinstance(inp, AppliedType3):
return (
get_printable_type_name(inp.base, placeholder_id_map)
+ ' ('
+ ') ('.join(get_printable_type_name(x, placeholder_id_map) for x in inp.args)
+ ')'
)
raise NotImplementedError(inp)
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, comment='Deduced type'))
for constraint in constraint_list:
print_constraint(placeholder_id_map, constraint)
print('=== ^ type3 constraint_list ^ === ')

334
phasm/type3/types.py Normal file
View File

@ -0,0 +1,334 @@
"""
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, Optional, 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:
if isinstance(other, PlaceholderForType):
return False
if not isinstance(other, Type3):
raise NotImplementedError
return self is other
def __ne__(self, other: Any) -> bool:
return not self.__eq__(other)
def __hash__(self) -> int:
raise NotImplementedError
def __bool__(self) -> bool:
raise NotImplementedError
class PrimitiveType3(Type3):
"""
Intermediate class to tell primitive types from others
"""
__slots__ = ()
class MonadType3(Type3):
"""
A type method to indicate that actions have to be made
one after the other, usually because they affect outside state.
"""
__slots__ = ()
class IntType3(Type3):
"""
Sometimes you can have an int as type, e.g. when using static arrays
"""
__slots__ = ('value', )
value: int
def __init__(self, value: int) -> None:
super().__init__(str(value))
assert 0 <= value
self.value = value
def __eq__(self, other: Any) -> bool:
if isinstance(other, IntType3):
return self.value == other.value
if isinstance(other, Type3):
return False
raise NotImplementedError
class PlaceholderForType:
"""
A placeholder type, for when we don't know the final type yet
"""
__slots__ = ('update_on_substitution', 'resolve_as', )
update_on_substitution: List[ExpressionProtocol]
resolve_as: Optional[Type3]
def __init__(self, update_on_substitution: Iterable[ExpressionProtocol]) -> None:
self.update_on_substitution = [*update_on_substitution]
self.resolve_as = None
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 isinstance(other, Type3):
return False
if not isinstance(other, PlaceholderForType):
raise NotImplementedError
return self is other
def __ne__(self, other: Any) -> bool:
return not self.__eq__(other)
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: Union[PrimitiveType3, MonadType3]
"""
The base type
"""
args: List[Type3OrPlaceholder]
"""
The applied types (or placeholders there for)
"""
def __init__(self, base: Union[PrimitiveType3, MonadType3], args: Iterable[Type3OrPlaceholder]) -> None:
args = [*args]
assert args, 'Must at least one argument'
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
@property
def has_placeholders(self) -> bool:
return any(
isinstance(x, PlaceholderForType)
for x in self.args
)
def __eq__(self, other: Any) -> bool:
if not isinstance(other, Type3):
raise NotImplementedError
if not isinstance(other, AppliedType3):
return False
return (
self.base == other.base
and len(self.args) == len(other.args)
and all(
s == x
for s, x in zip(self.args, other.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 = PrimitiveType3('none')
"""
The none type, for when functions simply don't return anything. e.g., IO().
"""
bool_ = PrimitiveType3('bool')
"""
The bool type, either True or False
"""
u8 = PrimitiveType3('u8')
"""
The unsigned 8-bit integer type.
Operations on variables employ modular arithmetic, with modulus 2^8.
"""
u32 = PrimitiveType3('u32')
"""
The unsigned 32-bit integer type.
Operations on variables employ modular arithmetic, with modulus 2^32.
"""
u64 = PrimitiveType3('u64')
"""
The unsigned 64-bit integer type.
Operations on variables employ modular arithmetic, with modulus 2^64.
"""
i8 = PrimitiveType3('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 = PrimitiveType3('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 = PrimitiveType3('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 = PrimitiveType3('f32')
"""
A 32-bits IEEE 754 float, of 32 bits width.
"""
f64 = PrimitiveType3('f64')
"""
A 32-bits IEEE 754 float, of 64 bits width.
"""
bytes = PrimitiveType3('bytes')
"""
This is a runtime-determined length piece of memory that can be indexed at runtime.
"""
static_array = PrimitiveType3('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 = PrimitiveType3('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.
"""
IO = MonadType3('IO')
LOOKUP_TABLE: Dict[str, Type3] = {
'none': none,
'bool': bool_,
'u8': u8,
'u32': u32,
'u64': u64,
'i8': i8,
'i32': i32,
'i64': i64,
'f32': f32,
'f64': f64,
'bytes': bytes,
'IO': IO,
}

View File

@ -1,63 +0,0 @@
"""
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

View File

@ -1,638 +0,0 @@
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)

View File

@ -1,290 +0,0 @@
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?

View File

@ -1,57 +0,0 @@
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

View File

@ -1,43 +0,0 @@
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

View File

@ -1,122 +0,0 @@
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

View File

@ -1,201 +0,0 @@
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)

View File

@ -1,50 +0,0 @@
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

@ -1,43 +0,0 @@
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,7 +5,6 @@ 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
@ -16,6 +15,12 @@ class WatSerializable:
"""
raise NotImplementedError(self, 'to_wat')
def alloc_size(self) -> int:
"""
Returns how many bytes a variable of this type takes up in memory
"""
raise NotImplementedError(self, 'alloc_size')
class WasmType(WatSerializable):
"""
Type base class
@ -37,6 +42,9 @@ class WasmTypeInt32(WasmType):
def to_wat(self) -> str:
return 'i32'
def alloc_size(self) -> int:
return 4
class WasmTypeInt64(WasmType):
"""
i64 value
@ -53,6 +61,9 @@ class WasmTypeFloat32(WasmType):
def to_wat(self) -> str:
return 'f32'
def alloc_size(self) -> int:
return 4
class WasmTypeFloat64(WasmType):
"""
f64 value
@ -105,17 +116,11 @@ class Import(WatSerializable):
else f' (result {self.result.to_wat()})'
)
class StatementBase(WatSerializable):
pass
class Statement(StatementBase ):
class Statement(WatSerializable):
"""
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
@ -126,16 +131,6 @@ class Statement(StatementBase ):
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
@ -147,7 +142,7 @@ class Function(WatSerializable):
params: Iterable[Param],
locals_: Iterable[Param],
result: WasmType,
statements: Iterable[StatementBase],
statements: Iterable[Statement],
) -> None:
self.name = name
self.exported_name = exported_name
@ -196,24 +191,51 @@ class ModuleMemory(WatSerializable):
'(export "memory" (memory 0))\n'
)
class TableElement(WatSerializable):
"""
Represents a Web Assembly table element
"""
def __init__(self, offset: int, args: Iterable[str]) -> None:
self.offset = offset
self.args = [*args]
def to_wat(self) -> str:
args = ' '.join(self.args)
return f'(elem (i32.const {self.offset}) {args})'
class Table(WatSerializable):
"""
Represents a Web Assembly table
"""
def __init__(self, size: int, typ: str, elements: List[TableElement]) -> None:
self.size = size
self.type = typ
self.elements = [*elements]
def to_wat(self) -> str:
return (
f'(table {self.size} {self.type})\n '
+ '\n '.join(x.to_wat() for x in self.elements)
)
class Module(WatSerializable):
"""
Represents a Web Assembly module
"""
def __init__(self) -> None:
self.imports: List[Import] = []
self.table: dict[int, str] = {}
self.functions: List[Function] = []
self.memory = ModuleMemory()
self.tables: List[Table] = []
def to_wat(self) -> str:
"""
Generates the text version
"""
return '(module\n {}\n {}\n {}\n {}\n {})\n'.format(
return '(module\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.tables),
'\n '.join(x.to_wat() for x in self.functions),
)

View File

@ -1,8 +1,9 @@
"""
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
@ -21,15 +22,6 @@ 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
@ -40,101 +32,41 @@ 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'{value}', comment=comment)
self.generator.add_statement(f'{self.prefix}.const', f'0x{value:08x}', 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')
@ -148,19 +80,10 @@ 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
@ -180,33 +103,12 @@ class Generator_Local:
self.generator.add_statement('local.tee', variable.name_ref, comment=comment)
class GeneratorBlock:
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:
def __init__(self, generator: 'Generator', name: str) -> None:
self.generator = generator
self.name = name
self.params = params
self.result = result
self.comment = comment
def __enter__(self) -> None:
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)
self.generator.add_statement(self.name)
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
if not exc_type:
@ -214,7 +116,7 @@ class GeneratorBlock:
class Generator:
def __init__(self) -> None:
self.statements: List[wasm.StatementBase] = []
self.statements: List[wasm.Statement] = []
self.locals: Dict[str, VarType_Base] = {}
self.i32 = Generator_i32(self)
@ -222,16 +124,13 @@ 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')
self.block = functools.partial(GeneratorBlock, self, 'block')
# block
self.loop = functools.partial(GeneratorBlock, self, 'loop')
self.if_ = functools.partial(GeneratorBlock, self, 'if')
# br
@ -239,68 +138,32 @@ class Generator:
# br_table
self.return_ = functools.partial(self.add_statement, 'return')
# call - see below
# call_indirect - see below
# call_indirect
def br_if(self, idx: int) -> None:
self.add_statement('br_if', f'{idx}')
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 call(self, function: wasm.Function) -> None:
self.add_statement('call', f'${function.name}')
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[T: VarType_Base](self, var: T) -> T:
idx = 0
while (varname := f'__{var.name}_tmp_var_{idx}__') in self.locals:
idx += 1
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))
idx = 0
while (varname := f'__{infix}_tmp_var_{idx}__') in self.locals:
idx += 1
return VarType_i32(varname)
def temp_var_u8(self, infix: str) -> VarType_u8:
return self.temp_var(VarType_u8(infix))
idx = 0
while (varname := f'__{infix}_tmp_var_{idx}__') in self.locals:
idx += 1
def func_wrapper(exported: bool = False) -> Callable[[Any], wasm.Function]:
return VarType_u8(varname)
def func_wrapper(exported: bool = True) -> Callable[[Any], wasm.Function]:
"""
This wrapper will execute the function and return
a wasm Function with the generated Statements
@ -342,10 +205,6 @@ def func_wrapper(exported: bool = False) -> 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

View File

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

View File

@ -1,19 +1,10 @@
marko==2.1.3
mypy==1.17.1
pygments==2.19.1
pytest==8.3.5
mypy==0.991
pygments==2.12.0
pylint==2.15.9
pytest==7.2.0
pytest-integration==0.2.2
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
pywasm==1.0.7
pywasm3==0.5.0
wasmer==1.1.0
wasmer_compiler_cranelift==1.1.0
wasmtime==3.0.0

14
stubs/pywasm/__init__.pyi Normal file
View File

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

6
stubs/pywasm/binary.pyi Normal file
View File

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

View File

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

2
stubs/pywasm/option.pyi Normal file
View File

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

26
stubs/wasm3.pyi Normal file
View File

@ -0,0 +1,26 @@
from typing import Any, Callable
class Module:
...
def link_function(self, module_name: str, function_name: str, function: Callable[[Any], Any]) -> None:
...
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:
...

39
stubs/wasmer.pyi Normal file
View File

@ -0,0 +1,39 @@
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

@ -0,0 +1,16 @@
"""
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,164 +1,85 @@
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 memory, runners
from . import runners
DASHES = '-' * 16
class InvalidArgumentException(Exception):
pass
class SuiteResult:
def __init__(self) -> None:
def __init__(self):
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: str) -> None:
def __init__(self, code_py):
self.code_py = code_py
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:
def run_code(self, *args, runtime='pywasm3', func_name='testEntry', imports=None):
"""
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_(code_prefix + self.code_py)
runner = class_(self.code_py)
if verbose:
write_header(sys.stderr, 'Phasm')
runner.dump_phasm_code(sys.stderr)
runner.parse(verbose=verbose)
runner.parse()
runner.compile_ast()
runner.optimise_wasm_ast()
runner.compile_wat()
if verbose:
write_header(sys.stderr, 'Assembly')
runner.dump_wasm_wat(sys.stderr)
runner.compile_wasm()
runner.interpreter_setup()
runner.interpreter_load(imports)
allocator_generator = memory.Allocator(runner.phasm_ast.build, runner)
write_header(sys.stderr, 'Phasm')
runner.dump_phasm_code(sys.stderr)
write_header(sys.stderr, 'Assembly')
runner.dump_wasm_wat(sys.stderr)
# Check if code formatting works
if do_format_check:
assert self.code_py == '\n' + phasm_render(runner.phasm_ast) # \n for formatting in tests
assert self.code_py == '\n' + phasm_render(runner.phasm_ast) # \n for formatting in tests
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]] = []
wasm_args = []
if args:
if verbose:
write_header(sys.stderr, 'Memory (pre alloc)')
runner.interpreter_dump_memory(sys.stderr)
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 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
allocator = allocator_generator(arg_typ)
adr = allocator(arg)
wasm_args.append(adr)
if verbose:
write_header(sys.stderr, 'Memory (pre run)')
write_header(sys.stderr, 'Memory (pre alloc)')
runner.interpreter_dump_memory(sys.stderr)
for arg in args:
if isinstance(arg, (int, float, )):
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)
continue
raise NotImplementedError(arg)
write_header(sys.stderr, 'Memory (pre run)')
runner.interpreter_dump_memory(sys.stderr)
result = SuiteResult()
result.returned_value = runner.call(func_name, *wasm_args)
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)
write_header(sys.stderr, 'Memory (post run)')
runner.interpreter_dump_memory(sys.stderr)
return result
def write_header(textio: TextIO, msg: str) -> None:
def write_header(textio, msg: str) -> None:
textio.write(f'{DASHES} {msg.ljust(16)} {DASHES}\n')

View File

@ -1,551 +0,0 @@
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,26 +1,28 @@
"""
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.type5.solver import phasm_type5
from phasm.wasmgenerator import Generator as WasmGenerator
Imports = Optional[Dict[str, Callable[[Any], Any]]]
from phasm.type3.entry import phasm_type3
from phasm import ourlang
from phasm import wasm
class RunnerBase:
"""
Base class
"""
phasm_code: str
phasm_ast: ourlang.Module[WasmGenerator]
phasm_ast: ourlang.Module
wasm_ast: wasm.Module
wasm_asm: str
wasm_bin: bytes
@ -34,12 +36,12 @@ class RunnerBase:
"""
_dump_code(textio, self.phasm_code)
def parse(self, verbose: bool = True) -> None:
def parse(self) -> None:
"""
Parses the Phasm code into an AST
"""
self.phasm_ast = phasm_parse(self.phasm_code)
phasm_type5(self.phasm_ast, verbose=verbose)
phasm_type3(self.phasm_ast, verbose=True)
def compile_ast(self) -> None:
"""
@ -47,12 +49,6 @@ 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
@ -69,7 +65,7 @@ class RunnerBase:
"""
Compiles the WebAssembly AST into WebAssembly Binary
"""
raise NotImplementedError
self.wasm_bin = wasmer.wat2wasm(self.wasm_asm)
def interpreter_setup(self) -> None:
"""
@ -77,7 +73,7 @@ class RunnerBase:
"""
raise NotImplementedError
def interpreter_load(self, imports: Imports = None) -> None:
def interpreter_load(self, imports: Optional[Dict[str, Callable[[Any], Any]]] = None) -> None:
"""
Loads the code into the interpreter
"""
@ -107,6 +103,78 @@ 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:
self.mod = self.env.parse_module(self.wasm_bin)
self.rtime.load(self.mod)
if imports is not None:
for key, val in imports.items():
self.mod.link_function('imports', key, val)
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
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
@ -117,48 +185,15 @@ 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:
functions: list[wasmtime.Func] = []
if imports is not None:
functions = [
wasmtime.Func(self.store, self.__class__.func2type(f), f)
for f in imports.values()
]
raise NotImplementedError
self.module = wasmtime.Module(self.store.engine, self.wasm_asm)
self.instance = wasmtime.Instance(self.store, self.module, functions)
self.module = wasmtime.Module(self.store.engine, self.wasm_bin)
self.instance = wasmtime.Instance(self.store, self.module, [])
def interpreter_write_memory(self, offset: int, data: Iterable[int]) -> None:
exports = self.instance.exports(self.store)
@ -183,7 +218,8 @@ class RunnerWasmtime(RunnerBase):
data_len = memory.data_len(self.store)
raw = ctypes.string_at(data_ptr, data_len)
return raw[offset:offset + length]
return raw[offset:length]
def interpreter_dump_memory(self, textio: TextIO) -> None:
exports = self.instance.exports(self.store)
@ -202,6 +238,63 @@ 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

@ -0,0 +1,19 @@
import pytest
from ..helpers import Suite
@pytest.mark.slow_integration_test
def test_index():
with open('examples/buffer.py', 'r', encoding='ASCII') as fil:
code_py = "\n" + fil.read()
result = Suite(code_py).run_code(b'Hello, world!', 5, func_name='index', runtime='wasmtime')
assert 44 == result.returned_value
@pytest.mark.slow_integration_test
def test_length():
with open('examples/buffer.py', 'r', encoding='ASCII') as fil:
code_py = "\n" + fil.read()
result = Suite(code_py).run_code(b'Hello, world!', func_name='length')
assert 13 == result.returned_value

View File

@ -1,19 +1,39 @@
import binascii
import struct
import pytest
from ..helpers import Suite
@pytest.mark.slow_integration_test
def test_crc32():
with open('examples/crc32.py', 'r', encoding='ASCII') as fil:
code_py = "\n" + fil.read()
# 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
# https://reveng.sourceforge.io/crc-catalogue/legend.htm#crc.legend.params
in_put = b'123456789'
code_py = """
def _crc32_f(crc: u32, byt: u8) -> u32:
return 16777215 ^ 397917763
# https://reveng.sourceforge.io/crc-catalogue/17plus.htm#crc.cat.crc-32-iso-hdlc
check = 0xcbf43926
def testEntry(data: bytes) -> u32:
return 4294967295 ^ _crc32_f(4294967295, data[0])
"""
exp_result = binascii.crc32(b'a')
result = Suite(code_py).run_code(in_put, func_name='crc32', do_format_check=False)
result = Suite(code_py).run_code(b'a')
assert check == result.returned_value
# 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

View File

@ -2,12 +2,11 @@ import pytest
from ..helpers import Suite
@pytest.mark.slow_integration_test
def test_fib():
with open('./examples/fib.py', 'r', encoding='UTF-8') as fil:
code_py = "\n" + fil.read()
result = Suite(code_py).run_code(40, func_name='fib')
result = Suite(code_py).run_code()
assert 102334155 == result.returned_value

View File

@ -1,309 +0,0 @@
# 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

@ -1,153 +0,0 @@
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

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

View File

@ -1,5 +0,0 @@
{
"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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
{
"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

@ -1,33 +0,0 @@
{
"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

@ -1,46 +0,0 @@
{
"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

@ -1,25 +0,0 @@
{
"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

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

View File

@ -1,5 +0,0 @@
{
"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

@ -1,5 +0,0 @@
{
"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

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

View File

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

View File

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

View File

@ -0,0 +1,80 @@
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

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