Compare commits

...

19 Commits

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

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

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

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

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

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

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

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

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

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

View File

@ -1,3 +1,4 @@
TEST_FILES := tests
WAT2WASM := venv/bin/python wat2wasm.py
%.wat: %.py $(shell find phasm -name '*.py') venv/.done
@ -16,13 +17,13 @@ examples: venv/.done $(subst .py,.wasm,$(wildcard examples/*.py)) $(subst .py,.w
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 tests $(TEST_FLAGS)
venv/bin/pytest $(TEST_FILES) $(TEST_FLAGS)
lint: venv/.done
venv/bin/ruff check phasm tests
typecheck: venv/.done
venv/bin/mypy --strict phasm wat2wasm.py tests/integration/helpers.py tests/integration/runners.py
venv/bin/mypy --strict phasm wat2wasm.py tests/integration/helpers.py tests/integration/memory.py tests/integration/runners.py
venv/.done: requirements.txt
python3.12 -m venv venv

View File

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

14
TODO.md
View File

@ -12,24 +12,16 @@
- 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?
- Does Subscript do what we want? It's a language feature rather a normal typed thing. How would you implement your own Subscript-able type?
- Clean up Subscript implementation - it's half implemented in the compiler. Makes more sense to move more parts to stdlib_types.
- Have a set of rules or guidelines for the constraint comments, they're messy.
- Why is expression_subscript_bytes using a helper method but expression_subscript_static_array is not?
- 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
- Make prelude more an actual thing
- Merge in compiler.INSTANCES
- Make it less build in - have a environment class of some kind
- 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
- Filter out methods that aren't used; other the other way around (easier?) only add __ methods when needed
- Move foldr into type class methods
- Functions don't seem to be a thing on typing level yet?
- Related to the FIXME in phasm_type3?
- Type constuctor should also be able to constuct placeholders - somehow.
- Read https://bytecodealliance.org/articles/multi-value-all-the-wasm
- Implement type class 'inheritance'
- Rework type classes - already started on a separate dir for those, but quite a few things are still in other places.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

31
examples/main.css Normal file
View File

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

View File

@ -5,8 +5,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 .type3.entry import phasm_type3
from .type5.solver import phasm_type5
def main(source: str, sink: str) -> int:
@ -18,8 +19,9 @@ def main(source: str, sink: str) -> int:
code_py = fil.read()
our_module = phasm_parse(code_py)
phasm_type3(our_module, verbose=False)
phasm_type5(our_module, verbose=False)
wasm_module = phasm_compile(our_module)
removeunusedfuncs(wasm_module)
code_wat = wasm_module.to_wat()
with open(sink, 'w') as fil:

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

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

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

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

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@ -3,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 Generator
from typing import Any, Generator
from . import ourlang, prelude
from .type3.types import Type3, TypeApplication_Struct
from . import ourlang
from .type5 import typeexpr as type5typeexpr
def phasm_render(inp: ourlang.Module) -> str:
def phasm_render(inp: ourlang.Module[Any]) -> str:
"""
Public method for rendering a Phasm module into Phasm code
"""
@ -17,32 +17,27 @@ def phasm_render(inp: ourlang.Module) -> str:
Statements = Generator[str, None, None]
def type3(inp: Type3) -> str:
def type5(mod: ourlang.Module[Any], inp: type5typeexpr.TypeExpr) -> str:
"""
Render: type's name
"""
if inp is prelude.none:
return 'None'
return mod.build.type5_name(inp)
return inp.name
def struct_definition(inp: ourlang.StructDefinition) -> str:
def struct_definition(mod: ourlang.Module[Any], inp: ourlang.StructDefinition) -> str:
"""
Render: TypeStruct's definition
"""
assert isinstance(inp.struct_type3.application, TypeApplication_Struct)
result = f'class {inp.struct_type3.name}:\n'
for mem, typ in inp.struct_type3.application.arguments:
result += f' {mem}: {type3(typ)}\n'
result = f'class {inp.struct_type5.name}:\n'
for mem, typ in inp.struct_type5.fields:
result += f' {mem}: {type5(mod, typ)}\n'
return result
def constant_definition(inp: ourlang.ModuleConstantDef) -> str:
def constant_definition(mod: ourlang.Module[Any], inp: ourlang.ModuleConstantDef) -> str:
"""
Render: Module Constant's definition
"""
return f'{inp.name}: {type3(inp.type3)} = {expression(inp.constant)}\n'
return f'{inp.name}: {type5(mod, inp.type5)} = {expression(inp.constant)}\n'
def expression(inp: ourlang.Expression) -> str:
"""
@ -63,7 +58,7 @@ def expression(inp: ourlang.Expression) -> str:
) + ', )'
if isinstance(inp, ourlang.ConstantStruct):
return inp.struct_type3.name + '(' + ', '.join(
return inp.struct_type5.name + '(' + ', '.join(
expression(x)
for x in inp.value
) + ')'
@ -81,7 +76,7 @@ def expression(inp: ourlang.Expression) -> str:
)
if isinstance(inp.function, ourlang.StructConstructor):
return f'{inp.function.struct_type3.name}({args})'
return f'{inp.function.struct_type5.name}({args})'
return f'{inp.function.name}({args})'
@ -131,7 +126,7 @@ def statement(inp: ourlang.Statement) -> Statements:
raise NotImplementedError(statement, inp)
def function(inp: ourlang.Function) -> str:
def function(mod: ourlang.Module[Any], inp: ourlang.Function) -> str:
"""
Render: Function body
@ -144,12 +139,17 @@ def function(inp: ourlang.Function) -> str:
if inp.imported:
result += '@imported\n'
assert inp.type5 is not None
fn_args = mod.build.type5_is_function(inp.type5)
assert fn_args is not None
ret_type5 = fn_args.pop()
args = ', '.join(
f'{p.name}: {type3(p.type3)}'
for p in inp.posonlyargs
f'{arg_name}: {type5(mod, arg_type)}'
for arg_name, arg_type in zip(inp.arg_names, fn_args, strict=True)
)
result += f'def {inp.name}({args}) -> {type3(inp.returns_type3)}:\n'
result += f'def {inp.name}({args}) -> {type5(mod, ret_type5)}:\n'
if inp.imported:
result += ' pass\n'
@ -161,7 +161,7 @@ def function(inp: ourlang.Function) -> str:
return result
def module(inp: ourlang.Module) -> str:
def module(inp: ourlang.Module[Any]) -> str:
"""
Render: Module
"""
@ -170,20 +170,20 @@ def module(inp: ourlang.Module) -> str:
for struct in inp.struct_definitions.values():
if result:
result += '\n'
result += struct_definition(struct)
result += struct_definition(inp, struct)
for cdef in inp.constant_defs.values():
if result:
result += '\n'
result += constant_definition(cdef)
result += constant_definition(inp, cdef)
for func in inp.functions.values():
if func.lineno < 0:
# Builtin (-2) or auto generated (-1)
if isinstance(func, ourlang.StructConstructor):
# Auto generated
continue
if result:
result += '\n'
result += function(func)
result += function(inp, func)
return result

View File

@ -2,114 +2,126 @@
This module contains the code to convert parsed Ourlang into WebAssembly code
"""
import struct
from typing import List
from dataclasses import dataclass
from typing import Any, List, TypeGuard
from . import ourlang, prelude, wasm
from .runtime import calculate_alloc_size, calculate_member_offset
from . import ourlang, wasm
from .build.base import BuildBase, TypeInfo
from .build.typerouter import BuildTypeRouter
from .stdlib import alloc as stdlib_alloc
from .stdlib import types as stdlib_types
from .stdlib.types import TYPE_INFO_CONSTRUCTED, TYPE_INFO_MAP
from .type3.functions import FunctionArgument, TypeVariable
from .type3.routers import NoRouteForTypeException, TypeApplicationRouter
from .type3.typeclasses import Type3ClassMethod
from .type3.types import (
IntType3,
Type3,
TypeApplication_Struct,
TypeApplication_Type,
TypeApplication_TypeInt,
TypeApplication_TypeStar,
TypeConstructor_DynamicArray,
TypeConstructor_Function,
TypeConstructor_StaticArray,
TypeConstructor_Tuple,
from .type5.constrainedexpr import ConstrainedExpr
from .type5.typeexpr import (
AtomicType,
TypeApplication,
TypeExpr,
TypeVariable,
is_concrete,
replace_variable,
)
from .wasm import (
WasmTypeFloat32,
WasmTypeFloat64,
WasmTypeInt32,
WasmTypeInt64,
)
from .wasmgenerator import Generator as WasmGenerator
TYPE3_ASSERTION_ERROR = 'You must call phasm_type3 after calling phasm_parse before your program can be compiled'
TYPE5_ASSERTION_ERROR = 'You must call phasm_type5 after calling phasm_parse before your program can be compiled'
def phasm_compile(inp: ourlang.Module) -> wasm.Module:
def phasm_compile(inp: ourlang.Module[WasmGenerator]) -> wasm.Module:
"""
Public method for compiling a parsed Phasm module into
a WebAssembly module
"""
return module(inp)
def type3(inp: Type3) -> wasm.WasmType:
def type5(mod: ourlang.Module[WasmGenerator], inp: TypeExpr) -> wasm.WasmType:
"""
Compile: type
Types are used for example in WebAssembly function parameters
and return types.
"""
typ_info = TYPE_INFO_MAP.get(inp.name, TYPE_INFO_CONSTRUCTED)
typ_info = mod.build.type_info_map.get(inp.name)
if typ_info is None:
typ_info = mod.build.type_info_constructed
return typ_info.wasm_type()
def tuple_instantiation(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.TupleInstantiation) -> None:
@dataclass
class TupleInstantiationResult:
args: list[TypeExpr]
alloc_size: int
header_value: int | None
class TupleInstantiationRouter(BuildTypeRouter[TupleInstantiationResult]):
__slots__ = ('el_count', )
el_count: int
def __init__(self, build: BuildBase[Any], el_count: int) -> None:
super().__init__(build)
self.el_count = el_count
def when_dynamic_array(self, da_arg: TypeExpr) -> TupleInstantiationResult:
return TupleInstantiationResult(
args=[da_arg for _ in range(self.el_count)],
alloc_size=5 + self.el_count * self.build.type5_alloc_size_member(da_arg),
header_value=self.el_count,
)
def when_static_array(self, sa_len: int, sa_typ: TypeExpr) -> TupleInstantiationResult:
return TupleInstantiationResult(
args=[sa_typ for _ in range(sa_len)],
alloc_size=5 + sa_len * self.build.type5_alloc_size_member(sa_typ),
header_value=None,
)
def when_tuple(self, tp_args: list[TypeExpr]) -> TupleInstantiationResult:
return TupleInstantiationResult(
args=tp_args,
alloc_size=sum(
self.build.type5_alloc_size_member(x)
for x in tp_args
),
header_value=None,
)
def tuple_instantiation(wgn: WasmGenerator, mod: ourlang.Module[WasmGenerator], inp: ourlang.TupleInstantiation) -> None:
"""
Compile: Instantiation (allocation) of a tuple
"""
assert inp.type3 is not None, TYPE3_ASSERTION_ERROR
assert _is_concrete(inp.type5), TYPE5_ASSERTION_ERROR
args: tuple[Type3, ...]
alloc_size_header = None
if isinstance(inp.type3.application, TypeApplication_Type):
# Possibly paranoid assert. If we have a future variadic type,
# does it also do this tuple instantation like this?
assert isinstance(inp.type3.application.constructor, TypeConstructor_DynamicArray)
sa_type, = inp.type3.application.arguments
args = tuple(sa_type for _ in inp.elements)
# Can't use calculate_alloc_size directly since that doesn't
# know the dynamic array's length
alloc_size = 4 + calculate_alloc_size(sa_type, is_member=True) * len(inp.elements)
alloc_size_header = len(inp.elements)
elif isinstance(inp.type3.application, TypeApplication_TypeStar):
# Possibly paranoid assert. If we have a future variadic type,
# does it also do this tuple instantation like this?
assert isinstance(inp.type3.application.constructor, TypeConstructor_Tuple)
args = inp.type3.application.arguments
alloc_size = calculate_alloc_size(inp.type3, is_member=False)
elif isinstance(inp.type3.application, TypeApplication_TypeInt):
# Possibly paranoid assert. If we have a future type of kind * -> Int -> *,
# does it also do this tuple instantation like this?
assert isinstance(inp.type3.application.constructor, TypeConstructor_StaticArray)
sa_type, sa_len = inp.type3.application.arguments
args = tuple(sa_type for _ in range(sa_len.value))
alloc_size = calculate_alloc_size(inp.type3, is_member=False)
else:
raise NotImplementedError('tuple_instantiation', inp.type3)
result = TupleInstantiationRouter(mod.build, len(inp.elements))(inp.type5)
comment_elements = ''
for element in inp.elements:
assert element.type3 is not None, TYPE3_ASSERTION_ERROR
comment_elements += f'{element.type3.name}, '
assert _is_concrete(element.type5), TYPE5_ASSERTION_ERROR
comment_elements += f'{mod.build.type5_name(element.type5)}, '
tmp_var = wgn.temp_var_i32('tuple_adr')
wgn.add_statement('nop', comment=f'{tmp_var.name} := ({comment_elements})')
# Allocated the required amounts of bytes in memory
wgn.i32.const(alloc_size)
wgn.i32.const(result.alloc_size)
wgn.call(stdlib_alloc.__alloc__)
wgn.local.set(tmp_var)
if alloc_size_header is not None:
if result.header_value is not None:
wgn.local.get(tmp_var)
wgn.i32.const(alloc_size_header)
wgn.i32.const(result.header_value )
wgn.i32.store()
# Store each element individually
offset = 0 if alloc_size_header is None else 4
for element, exp_type3 in zip(inp.elements, args, strict=True):
assert element.type3 == exp_type3
offset = 0 if result.header_value is None else 4
for element in inp.elements:
assert _is_concrete(element.type5), TYPE5_ASSERTION_ERROR
exp_type_info = TYPE_INFO_MAP.get(exp_type3.name, TYPE_INFO_CONSTRUCTED)
exp_type_info = mod.build.type_info_map.get(element.type5.name)
if exp_type_info is None:
exp_type_info = mod.build.type_info_constructed
wgn.add_statement('nop', comment='PRE')
wgn.local.get(tmp_var)
@ -117,81 +129,139 @@ def tuple_instantiation(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.Tu
wgn.add_statement(exp_type_info.wasm_store_func, 'offset=' + str(offset))
wgn.add_statement('nop', comment='POST')
offset += calculate_alloc_size(exp_type3, is_member=True)
offset += mod.build.type5_alloc_size_member(element.type5)
# Return the allocated address
wgn.local.get(tmp_var)
def expression_subscript_bytes(
attrs: tuple[WasmGenerator, ourlang.Module, ourlang.Subscript],
) -> None:
wgn, mod, inp = attrs
expression(wgn, mod, inp.varref)
expression(wgn, mod, inp.index)
wgn.call(stdlib_types.__subscript_bytes__)
def expression_subscript_static_array(
attrs: tuple[WasmGenerator, ourlang.Module, ourlang.Subscript],
args: tuple[Type3, IntType3],
) -> None:
wgn, mod, inp = attrs
el_type, el_len = args
# 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
expression(wgn, mod, inp.varref)
tmp_var = wgn.temp_var_i32('index')
expression(wgn, mod, inp.index)
wgn.local.tee(tmp_var)
# Out of bounds check based on el_len.value
wgn.i32.const(el_len.value)
wgn.i32.ge_u()
with wgn.if_():
wgn.unreachable(comment='Out of bounds')
el_type_info = TYPE_INFO_MAP.get(el_type.name, TYPE_INFO_CONSTRUCTED)
wgn.local.get(tmp_var)
wgn.i32.const(el_type_info.alloc_size)
wgn.i32.mul()
wgn.i32.add()
wgn.add_statement(el_type_info.wasm_load_func)
def expression_subscript_tuple(
attrs: tuple[WasmGenerator, ourlang.Module, ourlang.Subscript],
args: tuple[Type3, ...],
) -> None:
wgn, mod, inp = attrs
def expression_subscript_tuple(wgn: WasmGenerator, mod: ourlang.Module[WasmGenerator], inp: ourlang.Subscript) -> None:
assert isinstance(inp.index, ourlang.ConstantPrimitive)
assert isinstance(inp.index.value, int)
offset = 0
for el_type in args[0:inp.index.value]:
assert el_type is not None, TYPE3_ASSERTION_ERROR
el_type_info = TYPE_INFO_MAP.get(el_type.name, TYPE_INFO_CONSTRUCTED)
offset += el_type_info.alloc_size
assert _is_concrete(inp.varref.type5), TYPE5_ASSERTION_ERROR
args = mod.build.type5_is_tuple(inp.varref.type5)
assert args is not None
offset = sum(map(
mod.build.type5_alloc_size_member,
args[0:inp.index.value]
))
el_type = args[inp.index.value]
assert el_type is not None, TYPE3_ASSERTION_ERROR
el_type_info = mod.build.type_info_map.get(el_type.name)
if el_type_info is None:
el_type_info = mod.build.type_info_constructed
expression(wgn, mod, inp.varref)
el_type_info = TYPE_INFO_MAP.get(el_type.name, TYPE_INFO_CONSTRUCTED)
wgn.add_statement(el_type_info.wasm_load_func, f'offset={offset}')
SUBSCRIPT_ROUTER = TypeApplicationRouter[tuple[WasmGenerator, ourlang.Module, ourlang.Subscript], None]()
SUBSCRIPT_ROUTER.add_n(prelude.bytes_, expression_subscript_bytes)
SUBSCRIPT_ROUTER.add(prelude.static_array, expression_subscript_static_array)
SUBSCRIPT_ROUTER.add(prelude.tuple_, expression_subscript_tuple)
def expression_subscript_operator(wgn: WasmGenerator, mod: ourlang.Module[WasmGenerator], inp: ourlang.Subscript) -> None:
assert _is_concrete(inp.type5), TYPE5_ASSERTION_ERROR
def expression(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.Expression) -> None:
ftp5 = mod.build.type_classes['Subscriptable'].operators['[]']
fn_args = mod.build.type5_is_function(ftp5)
assert fn_args is not None
t_a = fn_args[0]
assert isinstance(t_a, TypeApplication)
t = t_a.constructor
a = t_a.argument
assert isinstance(t, TypeVariable)
assert isinstance(a, TypeVariable)
assert isinstance(inp.varref.type5, TypeApplication)
t_expr = inp.varref.type5.constructor
a_expr = inp.varref.type5.argument
_expression_binary_operator_or_function_call(
wgn,
mod,
ourlang.BuiltinFunction('[]', ftp5),
{
t: t_expr,
a: a_expr,
},
[inp.varref, inp.index],
inp.type5,
)
def expression_binary_op(wgn: WasmGenerator, mod: ourlang.Module[WasmGenerator], inp: ourlang.BinaryOp) -> None:
assert _is_concrete(inp.type5), TYPE5_ASSERTION_ERROR
_expression_binary_operator_or_function_call(
wgn,
mod,
inp.operator,
inp.polytype_substitutions,
[inp.left, inp.right],
inp.type5,
)
def expression_function_call(wgn: WasmGenerator, mod: ourlang.Module[WasmGenerator], inp: ourlang.FunctionCall) -> None:
assert _is_concrete(inp.type5), TYPE5_ASSERTION_ERROR
_expression_binary_operator_or_function_call(
wgn,
mod,
inp.function,
inp.polytype_substitutions,
inp.arguments,
inp.type5,
)
def _expression_binary_operator_or_function_call(
wgn: WasmGenerator,
mod: ourlang.Module[WasmGenerator],
function: ourlang.Function | ourlang.FunctionParam,
polytype_substitutions: dict[TypeVariable, TypeExpr],
arguments: list[ourlang.Expression],
ret_type5: TypeExpr,
) -> None:
for arg in arguments:
expression(wgn, mod, arg)
if isinstance(function, ourlang.BuiltinFunction):
ftp5 = function.type5
if isinstance(ftp5, ConstrainedExpr):
cexpr = ftp5
ftp5 = ftp5.expr
for tvar in cexpr.variables:
ftp5 = replace_variable(ftp5, tvar, polytype_substitutions[tvar])
assert _is_concrete(ftp5), TYPE5_ASSERTION_ERROR
try:
method_type, method_router = mod.build.methods[function.name]
except KeyError:
method_type, method_router = mod.build.operators[function.name]
impl_lookup = method_router.get((ftp5, ))
assert impl_lookup is not None, (function.name, ftp5, )
kwargs, impl = impl_lookup
impl(wgn, kwargs)
return
if isinstance(function, ourlang.FunctionParam):
fn_args = mod.build.type5_is_function(function.type5)
assert fn_args is not None, function.type5
params = [
type5(mod, x)
for x in fn_args
]
result = params.pop()
wgn.add_statement('local.get', '${}'.format(function.name))
wgn.call_indirect(params=params, result=result)
return
# TODO: Do similar subsitutions like we do for BuiltinFunction
# when we get user space polymorphic functions
# And then do similar lookup, and ensure we generate code for that variant
wgn.call(function.name)
def expression(wgn: WasmGenerator, mod: ourlang.Module[WasmGenerator], inp: ourlang.Expression) -> None:
"""
Compile: Any expression
"""
@ -200,35 +270,30 @@ def expression(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.Expression)
raise Exception
if isinstance(inp, ourlang.ConstantPrimitive):
assert inp.type3 is not None, TYPE3_ASSERTION_ERROR
assert _is_concrete(inp.type5), TYPE5_ASSERTION_ERROR
if inp.type3 in (prelude.i8, prelude.u8, ):
# No native u8 type - treat as i32, with caution
type_info = mod.build.type_info_map[inp.type5.name]
if type_info.wasm_type is WasmTypeInt32:
assert isinstance(inp.value, int)
wgn.i32.const(inp.value)
return
if inp.type3 in (prelude.i32, prelude.u32, ):
assert isinstance(inp.value, int)
wgn.i32.const(inp.value)
return
if inp.type3 in (prelude.i64, prelude.u64, ):
if type_info.wasm_type is WasmTypeInt64:
assert isinstance(inp.value, int)
wgn.i64.const(inp.value)
return
if inp.type3 == prelude.f32:
if type_info.wasm_type is WasmTypeFloat32:
assert isinstance(inp.value, float)
wgn.f32.const(inp.value)
return
if inp.type3 == prelude.f64:
if type_info.wasm_type is WasmTypeFloat64:
assert isinstance(inp.value, float)
wgn.f64.const(inp.value)
return
raise NotImplementedError(f'Constants with type {inp.type3:s}')
raise NotImplementedError(inp.type5)
if isinstance(inp, ourlang.ConstantBytes):
assert inp.data_block.address is not None, 'Value not allocated'
@ -241,9 +306,9 @@ def expression(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.Expression)
return
if isinstance(inp.variable, ourlang.ModuleConstantDef):
assert inp.type3 is not None, TYPE3_ASSERTION_ERROR
assert _is_concrete(inp.type5), TYPE5_ASSERTION_ERROR
if inp.type3.name not in TYPE_INFO_MAP:
if inp.type5.name not in mod.build.type_info_map:
assert isinstance(inp.variable.constant, (ourlang.ConstantBytes, ourlang.ConstantStruct, ourlang.ConstantTuple, ))
address = inp.variable.constant.data_block.address
@ -257,80 +322,11 @@ def expression(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.Expression)
raise NotImplementedError(expression, inp.variable)
if isinstance(inp, ourlang.BinaryOp):
expression(wgn, mod, inp.left)
expression(wgn, mod, inp.right)
type_var_map: dict[TypeVariable, Type3] = {}
for type_var, arg_expr in zip(inp.operator.signature.args, [inp.left, inp.right, inp], strict=True):
assert arg_expr.type3 is not None, TYPE3_ASSERTION_ERROR
if isinstance(type_var, Type3):
# Fixed type, not part of the lookup requirements
continue
if isinstance(type_var, TypeVariable):
type_var_map[type_var] = arg_expr.type3
continue
if isinstance(type_var, FunctionArgument):
# Fixed type, not part of the lookup requirements
continue
raise NotImplementedError(type_var, arg_expr.type3)
router = prelude.PRELUDE_TYPE_CLASS_INSTANCE_METHODS[inp.operator]
router(wgn, type_var_map)
expression_binary_op(wgn, mod, inp)
return
if isinstance(inp, ourlang.FunctionCall):
for arg in inp.arguments:
expression(wgn, mod, arg)
if isinstance(inp.function, Type3ClassMethod):
# FIXME: Duplicate code with BinaryOp
type_var_map = {}
for type_var, arg_expr in zip(inp.function.signature.args, inp.arguments + [inp], strict=True):
assert arg_expr.type3 is not None, TYPE3_ASSERTION_ERROR
if isinstance(type_var, Type3):
# Fixed type, not part of the lookup requirements
continue
if isinstance(type_var, TypeVariable):
type_var_map[type_var] = arg_expr.type3
continue
if isinstance(type_var, FunctionArgument):
# Fixed type, not part of the lookup requirements
continue
raise NotImplementedError(type_var, arg_expr.type3)
router = prelude.PRELUDE_TYPE_CLASS_INSTANCE_METHODS[inp.function]
try:
router(wgn, type_var_map)
except NoRouteForTypeException:
raise NotImplementedError(str(inp.function), type_var_map)
return
if isinstance(inp.function, ourlang.FunctionParam):
assert isinstance(inp.function.type3.application.constructor, TypeConstructor_Function)
params = [
type3(x).to_wat()
for x in inp.function.type3.application.arguments
]
result = params.pop()
params_str = ' '.join(params)
wgn.add_statement('local.get', '${}'.format(inp.function.name))
wgn.add_statement(f'call_indirect (param {params_str}) (result {result})')
return
wgn.add_statement('call', '${}'.format(inp.function.name))
expression_function_call(wgn, mod, inp)
return
if isinstance(inp, ourlang.FunctionReference):
@ -347,43 +343,60 @@ def expression(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.Expression)
return
if isinstance(inp, ourlang.Subscript):
assert inp.varref.type3 is not None, TYPE3_ASSERTION_ERROR
assert _is_concrete(inp.type5), TYPE5_ASSERTION_ERROR
assert _is_concrete(inp.varref.type5), TYPE5_ASSERTION_ERROR
assert _is_concrete(inp.index.type5), TYPE5_ASSERTION_ERROR
# Type checker guarantees we don't get routing errors
SUBSCRIPT_ROUTER((wgn, mod, inp, ), inp.varref.type3)
if mod.build.type5_is_tuple(inp.varref.type5):
expression_subscript_tuple(wgn, mod, inp)
return
expression_subscript_operator(wgn, mod, inp)
return
if isinstance(inp, ourlang.AccessStructMember):
assert inp.struct_type3 is not None, TYPE3_ASSERTION_ERROR
assert _is_concrete(inp.varref.type5), TYPE5_ASSERTION_ERROR
assert isinstance(inp.struct_type3.application, TypeApplication_Struct)
st_args = mod.build.type5_is_struct(inp.varref.type5)
assert st_args is not None
member_type = dict(inp.struct_type3.application.arguments)[inp.member]
member_type_info = TYPE_INFO_MAP.get(member_type.name, TYPE_INFO_CONSTRUCTED)
member_type = dict(st_args)[inp.member]
member_type_info = mod.build.type_info_map.get(member_type.name)
if member_type_info is None:
member_type_info = mod.build.type_info_constructed
offset = _type5_struct_offset(mod.build, st_args, inp.member)
expression(wgn, mod, inp.varref)
wgn.add_statement(member_type_info.wasm_load_func, 'offset=' + str(calculate_member_offset(
inp.struct_type3.name, inp.struct_type3.application.arguments, inp.member
)))
wgn.add_statement(member_type_info.wasm_load_func, 'offset=' + str(offset))
return
raise NotImplementedError(expression, inp)
def statement_return(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.StatementReturn) -> None:
def statement_return(wgn: WasmGenerator, mod: ourlang.Module[WasmGenerator], fun: ourlang.Function, inp: ourlang.StatementReturn) -> None:
"""
Compile: Return statement
"""
# Support tail calls
# https://github.com/WebAssembly/tail-call
# These help a lot with some functional programming techniques
if isinstance(inp.value, ourlang.FunctionCall) and inp.value.function is fun:
for arg in inp.value.arguments:
expression(wgn, mod, arg)
wgn.add_statement('return_call', '${}'.format(inp.value.function.name))
return
expression(wgn, mod, inp.value)
wgn.return_()
def statement_if(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.StatementIf) -> None:
def statement_if(wgn: WasmGenerator, mod: ourlang.Module[WasmGenerator], fun: ourlang.Function, inp: ourlang.StatementIf) -> None:
"""
Compile: If statement
"""
expression(wgn, mod, inp.test)
with wgn.if_():
for stat in inp.statements:
statement(wgn, mod, stat)
statement(wgn, mod, fun, stat)
if inp.else_statements:
raise NotImplementedError
@ -391,16 +404,16 @@ def statement_if(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.Statement
# for stat in inp.else_statements:
# statement(wgn, stat)
def statement(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.Statement) -> None:
def statement(wgn: WasmGenerator, mod: ourlang.Module[WasmGenerator], fun: ourlang.Function, inp: ourlang.Statement) -> None:
"""
Compile: any statement
"""
if isinstance(inp, ourlang.StatementReturn):
statement_return(wgn, mod, inp)
statement_return(wgn, mod, fun, inp)
return
if isinstance(inp, ourlang.StatementIf):
statement_if(wgn, mod, inp)
statement_if(wgn, mod, fun, inp)
return
if isinstance(inp, ourlang.StatementPass):
@ -408,111 +421,86 @@ def statement(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.Statement) -
raise NotImplementedError(statement, inp)
def function_argument(inp: ourlang.FunctionParam) -> wasm.Param:
"""
Compile: function argument
"""
return (inp.name, type3(inp.type3), )
def import_(inp: ourlang.Function) -> wasm.Import:
def import_(mod: ourlang.Module[WasmGenerator], inp: ourlang.Function) -> wasm.Import:
"""
Compile: imported function
"""
assert inp.imported
assert _is_concrete(inp.type5), TYPE5_ASSERTION_ERROR
fn_args = mod.build.type5_is_function(inp.type5)
assert fn_args is not None
fn_ret = fn_args.pop()
return wasm.Import(
inp.imported,
inp.name,
inp.name,
[
function_argument(x)
for x in inp.posonlyargs
(arg_name, type5(mod, arg_type5), )
for arg_name, arg_type5 in zip(inp.arg_names, fn_args, strict=True)
],
type3(inp.returns_type3)
type5(mod, fn_ret)
)
def function(mod: ourlang.Module, inp: ourlang.Function) -> wasm.Function:
def function(mod: ourlang.Module[WasmGenerator], inp: ourlang.Function) -> wasm.Function:
"""
Compile: function
"""
assert not inp.imported
assert _is_concrete(inp.type5), TYPE5_ASSERTION_ERROR
fn_args = mod.build.type5_is_function(inp.type5)
assert fn_args is not None
fn_ret = fn_args.pop()
wgn = WasmGenerator()
if isinstance(inp, ourlang.StructConstructor):
_generate_struct_constructor(wgn, inp)
_generate_struct_constructor(wgn, mod, inp)
else:
for stat in inp.statements:
statement(wgn, mod, stat)
statement(wgn, mod, inp, stat)
return wasm.Function(
inp.name,
inp.name if inp.exported else None,
[
function_argument(x)
for x in inp.posonlyargs
(arg_name, type5(mod, arg_type5), )
for arg_name, arg_type5 in zip(inp.arg_names, fn_args, strict=True)
],
[
(k, v.wasm_type(), )
for k, v in wgn.locals.items()
],
type3(inp.returns_type3),
type5(mod, fn_ret),
wgn.statements
)
def module_data_u8(inp: int) -> bytes:
"""
Compile: module data, u8 value
"""
return struct.pack('<B', inp)
def module_data_primitive(type_info: TypeInfo, inp: int | float) -> bytes:
letter_map = {
(WasmTypeInt32, 1, False): 'B',
(WasmTypeInt32, 1, True): 'b',
(WasmTypeInt32, 2, False): 'H',
(WasmTypeInt32, 2, True): 'h',
(WasmTypeInt32, 4, False): 'I',
(WasmTypeInt32, 4, True): 'i',
(WasmTypeInt64, 8, False): 'Q',
(WasmTypeInt64, 8, True): 'q',
(WasmTypeFloat32, 4, None): 'f',
(WasmTypeFloat64, 8, None): 'd',
}
def module_data_u32(inp: int) -> bytes:
"""
Compile: module data, u32 value
"""
return struct.pack('<I', inp)
letter = letter_map[(type_info.wasm_type, type_info.alloc_size, type_info.signed, )]
return struct.pack(f'<{letter}', inp)
def module_data_u64(inp: int) -> bytes:
"""
Compile: module data, u64 value
"""
return struct.pack('<Q', inp)
def module_data_i8(inp: int) -> bytes:
"""
Compile: module data, i8 value
"""
return struct.pack('<b', inp)
def module_data_i32(inp: int) -> bytes:
"""
Compile: module data, i32 value
"""
return struct.pack('<i', inp)
def module_data_i64(inp: int) -> bytes:
"""
Compile: module data, i64 value
"""
return struct.pack('<q', inp)
def module_data_f32(inp: float) -> bytes:
"""
Compile: module data, f32 value
"""
return struct.pack('<f', inp)
def module_data_f64(inp: float) -> bytes:
"""
Compile: module data, f64 value
"""
return struct.pack('<d', inp)
def module_data(inp: ourlang.ModuleData) -> bytes:
def module_data(mod: ourlang.Module[WasmGenerator], inp: ourlang.ModuleData) -> bytes:
"""
Compile: module data
"""
unalloc_ptr = stdlib_alloc.UNALLOC_PTR
u32_type_info = mod.build.type_info_map['u32']
ptr_type_info = mod.build.type_info_constructed
allocated_data = b''
@ -522,105 +510,59 @@ def module_data(inp: ourlang.ModuleData) -> bytes:
data_list: List[bytes] = []
for constant in block.data:
assert constant.type3 is not None, TYPE3_ASSERTION_ERROR
assert _is_concrete(constant.type5), TYPE5_ASSERTION_ERROR
if isinstance(constant, ourlang.ConstantBytes):
data_list.append(module_data_primitive(u32_type_info, len(constant.value)))
data_list.append(constant.value)
continue
if isinstance(constant, ourlang.ConstantMemoryStored):
if block is constant.data_block:
raise NotImplementedError(block, constant)
if isinstance(constant, ourlang.ConstantMemoryStored) and block is not constant.data_block:
# It's stored in a different block
# We only need to store its address
# This happens for example when a tuple refers
# to a bytes constant
assert constant.data_block.address is not None, 'Referred memory not yet stored'
data_list.append(module_data_u32(constant.data_block.address))
data_list.append(module_data_primitive(ptr_type_info, constant.data_block.address))
continue
if constant.type3 == prelude.u8:
assert isinstance(constant, ourlang.ConstantPrimitive)
assert isinstance(constant.value, int)
data_list.append(module_data_u8(constant.value))
continue
if constant.type3 == prelude.u32:
assert isinstance(constant, ourlang.ConstantPrimitive)
assert isinstance(constant.value, int)
data_list.append(module_data_u32(constant.value))
continue
if constant.type3 == prelude.u64:
assert isinstance(constant, ourlang.ConstantPrimitive)
assert isinstance(constant.value, int)
data_list.append(module_data_u64(constant.value))
continue
if constant.type3 == prelude.i8:
assert isinstance(constant, ourlang.ConstantPrimitive)
assert isinstance(constant.value, int)
data_list.append(module_data_i8(constant.value))
continue
if constant.type3 == prelude.i32:
assert isinstance(constant, ourlang.ConstantPrimitive)
assert isinstance(constant.value, int)
data_list.append(module_data_i32(constant.value))
continue
if constant.type3 == prelude.i64:
assert isinstance(constant, ourlang.ConstantPrimitive)
assert isinstance(constant.value, int)
data_list.append(module_data_i64(constant.value))
continue
if constant.type3 == prelude.f32:
assert isinstance(constant, ourlang.ConstantPrimitive)
assert isinstance(constant.value, float)
data_list.append(module_data_f32(constant.value))
continue
if constant.type3 == prelude.f64:
assert isinstance(constant, ourlang.ConstantPrimitive)
assert isinstance(constant.value, float)
data_list.append(module_data_f64(constant.value))
continue
if constant.type3 == prelude.bytes_:
assert isinstance(constant, ourlang.ConstantBytes)
assert isinstance(constant.value, bytes)
data_list.append(module_data_u32(len(constant.value)))
data_list.append(constant.value)
continue
raise NotImplementedError(constant, constant.type3)
type_info = mod.build.type_info_map[constant.type5.name]
data_list.append(module_data_primitive(type_info, constant.value))
block_data = b''.join(data_list)
allocated_data += module_data_u32(len(block_data)) + block_data
allocated_data += module_data_primitive(u32_type_info, len(block_data)) + block_data
unalloc_ptr += 4 + len(block_data)
return (
# Store that we've initialized the memory
module_data_u32(stdlib_alloc.IDENTIFIER)
module_data_primitive(u32_type_info, stdlib_alloc.IDENTIFIER)
# Store the first reserved i32
+ module_data_u32(0)
+ module_data_primitive(u32_type_info, 0)
# Store the pointer towards the first free block
# In this case, 0 since we haven't freed any blocks yet
+ module_data_u32(0)
+ module_data_primitive(u32_type_info, 0)
# Store the pointer towards the first unallocated block
# In this case the end of the stdlib.alloc header at the start
+ module_data_u32(unalloc_ptr)
+ module_data_primitive(u32_type_info, unalloc_ptr)
# Store the actual data
+ allocated_data
)
def module(inp: ourlang.Module) -> wasm.Module:
def module(inp: ourlang.Module[WasmGenerator]) -> wasm.Module:
"""
Compile: module
"""
result = wasm.Module()
result.memory.data = module_data(inp.data)
result.memory.data = module_data(inp, inp.data)
result.imports = [
import_(x)
import_(inp, x)
for x in inp.functions.values()
if x.imported
]
@ -629,20 +571,21 @@ def module(inp: ourlang.Module) -> wasm.Module:
stdlib_alloc.__find_free_block__,
stdlib_alloc.__alloc__,
stdlib_types.__alloc_bytes__,
stdlib_types.__subscript_bytes__,
stdlib_types.__u32_ord_min__,
stdlib_types.__u64_ord_min__,
stdlib_types.__i32_ord_min__,
stdlib_types.__i64_ord_min__,
stdlib_types.__u32_ord_max__,
stdlib_types.__u64_ord_max__,
stdlib_types.__i32_ord_max__,
stdlib_types.__i64_ord_max__,
stdlib_types.__i32_intnum_abs__,
stdlib_types.__i64_intnum_abs__,
stdlib_types.__u32_min__,
stdlib_types.__u64_min__,
stdlib_types.__i32_min__,
stdlib_types.__i64_min__,
stdlib_types.__u32_max__,
stdlib_types.__u64_max__,
stdlib_types.__i32_max__,
stdlib_types.__i64_max__,
stdlib_types.__i32_abs__,
stdlib_types.__i64_abs__,
stdlib_types.__u32_pow2__,
stdlib_types.__u8_rotl__,
stdlib_types.__u8_rotr__,
stdlib_types.__u16_rotl__,
stdlib_types.__u16_rotr__,
] + [
function(inp, x)
for x in inp.functions.values()
@ -657,27 +600,56 @@ def module(inp: ourlang.Module) -> wasm.Module:
return result
def _generate_struct_constructor(wgn: WasmGenerator, inp: ourlang.StructConstructor) -> None:
assert isinstance(inp.struct_type3.application, TypeApplication_Struct)
st_args = inp.struct_type3.application.arguments
def _generate_struct_constructor(wgn: WasmGenerator, mod: ourlang.Module[WasmGenerator], inp: ourlang.StructConstructor) -> None:
st_args = mod.build.type5_is_struct(inp.struct_type5)
assert st_args is not None
tmp_var = wgn.temp_var_i32('struct_adr')
# Allocated the required amounts of bytes in memory
wgn.i32.const(calculate_alloc_size(inp.struct_type3))
wgn.i32.const(mod.build.type5_alloc_size_root(inp.struct_type5))
wgn.call(stdlib_alloc.__alloc__)
wgn.local.set(tmp_var)
# Store each member individually
for memname, mtyp3 in st_args:
mtyp3_info = TYPE_INFO_MAP.get(mtyp3.name, TYPE_INFO_CONSTRUCTED)
offset = 0
for memname, mtyp5 in st_args:
mtyp5_info = mod.build.type_info_map.get(mtyp5.name)
if mtyp5_info is None:
mtyp5_info = mod.build.type_info_constructed
wgn.local.get(tmp_var)
wgn.add_statement('local.get', f'${memname}')
wgn.add_statement(mtyp3_info.wasm_store_func, 'offset=' + str(calculate_member_offset(
inp.struct_type3.name, st_args, memname
)))
wgn.add_statement(mtyp5_info.wasm_store_func, 'offset=' + str(offset))
offset += mod.build.type5_alloc_size_member(mtyp5)
# Return the allocated address
wgn.local.get(tmp_var)
def _is_concrete(type5: TypeExpr | ConstrainedExpr | None) -> TypeGuard[TypeExpr]:
if type5 is None:
return False
if isinstance(type5, ConstrainedExpr):
type5 = type5.expr
return is_concrete(type5)
def _type5_struct_offset(
build: BuildBase[Any],
fields: tuple[tuple[str, AtomicType | TypeApplication], ...],
needle: str,
) -> int:
"""
Calculates the amount of bytes that should be skipped in memory befor reaching the struct's property with the given name.
"""
result = 0
for memnam, memtyp in fields:
if needle == memnam:
return result
result += build.type5_alloc_size_member(memtyp)
raise RuntimeError('Member not found')

View File

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

View File

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

View File

@ -1,24 +1,46 @@
"""
Contains the syntax tree for ourlang
"""
from __future__ import annotations
from typing import Dict, Iterable, List, Optional, Union
from . import prelude
from .type3.functions import FunctionSignature, TypeVariableContext
from .type3.typeclasses import Type3ClassMethod
from .type3.types import Type3, TypeApplication_Struct
from .build.base import BuildBase
from .type5 import constrainedexpr as type5constrainedexpr
from .type5 import record as type5record
from .type5 import typeexpr as type5typeexpr
class SourceRef:
__slots__ = ('filename', 'lineno', 'colno', )
filename: str | None
lineno: int | None
colno: int | None
def __init__(self, filename: str | None, lineno: int | None = None, colno: int | None = None) -> None:
self.filename = filename
self.lineno = lineno
self.colno = colno
def __repr__(self) -> str:
return f"SourceRef({self.filename!r}, {self.lineno!r}, {self.colno!r})"
def __str__(self) -> str:
return f"{self.filename}:{self.lineno:>4}:{self.colno:<3}"
class Expression:
"""
An expression within a statement
"""
__slots__ = ('type3', )
__slots__ = ('type5', 'sourceref', )
type3: Type3 | None
sourceref: SourceRef
type5: type5typeexpr.TypeExpr | None
def __init__(self) -> None:
self.type3 = None
def __init__(self, *, sourceref: SourceRef) -> None:
self.sourceref = sourceref
self.type5 = None
class Constant(Expression):
"""
@ -34,10 +56,10 @@ class ConstantPrimitive(Constant):
"""
__slots__ = ('value', )
value: Union[int, float]
value: int | float
def __init__(self, value: Union[int, float]) -> None:
super().__init__()
def __init__(self, value: int | float, sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
self.value = value
def __repr__(self) -> str:
@ -53,8 +75,8 @@ class ConstantMemoryStored(Constant):
data_block: 'ModuleDataBlock'
def __init__(self, data_block: 'ModuleDataBlock') -> None:
super().__init__()
def __init__(self, data_block: 'ModuleDataBlock', sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
self.data_block = data_block
class ConstantBytes(ConstantMemoryStored):
@ -65,8 +87,8 @@ class ConstantBytes(ConstantMemoryStored):
value: bytes
def __init__(self, value: bytes, data_block: 'ModuleDataBlock') -> None:
super().__init__(data_block)
def __init__(self, value: bytes, data_block: 'ModuleDataBlock', sourceref: SourceRef) -> None:
super().__init__(data_block, sourceref=sourceref)
self.value = value
def __repr__(self) -> str:
@ -83,8 +105,8 @@ class ConstantTuple(ConstantMemoryStored):
value: List[Union[ConstantPrimitive, ConstantBytes, 'ConstantTuple', 'ConstantStruct']]
def __init__(self, value: List[Union[ConstantPrimitive, ConstantBytes, 'ConstantTuple', 'ConstantStruct']], data_block: 'ModuleDataBlock') -> None:
super().__init__(data_block)
def __init__(self, value: List[Union[ConstantPrimitive, ConstantBytes, 'ConstantTuple', 'ConstantStruct']], data_block: 'ModuleDataBlock', sourceref: SourceRef) -> None:
super().__init__(data_block, sourceref=sourceref)
self.value = value
def __repr__(self) -> str:
@ -97,21 +119,27 @@ class ConstantStruct(ConstantMemoryStored):
"""
A Struct constant value expression within a statement
"""
__slots__ = ('struct_type3', 'value', )
__slots__ = ('struct_type5', 'value', )
struct_type3: Type3
value: List[Union[ConstantPrimitive, ConstantBytes, ConstantTuple, 'ConstantStruct']]
struct_type5: type5record.Record
value: list[ConstantPrimitive | ConstantBytes | ConstantTuple | ConstantStruct]
def __init__(self, struct_type3: Type3, value: List[Union[ConstantPrimitive, ConstantBytes, ConstantTuple, 'ConstantStruct']], data_block: 'ModuleDataBlock') -> None:
super().__init__(data_block)
self.struct_type3 = struct_type3
def __init__(
self,
struct_type5: type5record.Record,
value: list[ConstantPrimitive | ConstantBytes | ConstantTuple | ConstantStruct],
data_block: 'ModuleDataBlock',
sourceref: SourceRef
) -> None:
super().__init__(data_block, sourceref=sourceref)
self.struct_type5 = struct_type5
self.value = value
def __repr__(self) -> str:
# Do not repr the whole ModuleDataBlock
# As this has a reference back to this constant for its data
# which it needs to compile the data into the program
return f'ConstantStruct({self.struct_type3!r}, {self.value!r}, @{self.data_block.address!r})'
return f'ConstantStruct({self.struct_type5!r}, {self.value!r}, @{self.data_block.address!r})'
class VariableReference(Expression):
"""
@ -121,24 +149,26 @@ class VariableReference(Expression):
variable: Union['ModuleConstantDef', 'FunctionParam'] # also possibly local
def __init__(self, variable: Union['ModuleConstantDef', 'FunctionParam']) -> None:
super().__init__()
def __init__(self, variable: Union['ModuleConstantDef', 'FunctionParam'], sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
self.variable = variable
class BinaryOp(Expression):
"""
A binary operator expression within a statement
"""
__slots__ = ('operator', 'left', 'right', )
__slots__ = ('operator', 'polytype_substitutions', 'left', 'right', )
operator: Type3ClassMethod
operator: Function | FunctionParam
polytype_substitutions: dict[type5typeexpr.TypeVariable, type5typeexpr.TypeExpr]
left: Expression
right: Expression
def __init__(self, operator: Type3ClassMethod, left: Expression, right: Expression) -> None:
super().__init__()
def __init__(self, operator: Function | FunctionParam, left: Expression, right: Expression, sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
self.operator = operator
self.polytype_substitutions = {}
self.left = left
self.right = right
@ -149,15 +179,17 @@ class FunctionCall(Expression):
"""
A function call expression within a statement
"""
__slots__ = ('function', 'arguments', )
__slots__ = ('function', 'polytype_substitutions', 'arguments', )
function: Union['Function', 'FunctionParam', Type3ClassMethod]
function: Function | FunctionParam
polytype_substitutions: dict[type5typeexpr.TypeVariable, type5typeexpr.TypeExpr]
arguments: List[Expression]
def __init__(self, function: Union['Function', 'FunctionParam', Type3ClassMethod]) -> None:
super().__init__()
def __init__(self, function: Function | FunctionParam, sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
self.function = function
self.polytype_substitutions = {}
self.arguments = []
class FunctionReference(Expression):
@ -168,8 +200,8 @@ class FunctionReference(Expression):
function: 'Function'
def __init__(self, function: 'Function') -> None:
super().__init__()
def __init__(self, function: 'Function', sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
self.function = function
class TupleInstantiation(Expression):
@ -180,8 +212,8 @@ class TupleInstantiation(Expression):
elements: List[Expression]
def __init__(self, elements: List[Expression]) -> None:
super().__init__()
def __init__(self, elements: List[Expression], sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
self.elements = elements
@ -195,8 +227,8 @@ class Subscript(Expression):
varref: VariableReference
index: Expression
def __init__(self, varref: VariableReference, index: Expression) -> None:
super().__init__()
def __init__(self, varref: VariableReference, index: Expression, sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
self.varref = varref
self.index = index
@ -205,24 +237,27 @@ class AccessStructMember(Expression):
"""
Access a struct member for reading of writing
"""
__slots__ = ('varref', 'struct_type3', 'member', )
__slots__ = ('varref', 'member', )
varref: VariableReference
struct_type3: Type3
member: str
def __init__(self, varref: VariableReference, struct_type3: Type3, member: str) -> None:
super().__init__()
def __init__(self, varref: VariableReference, member: str, sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
self.varref = varref
self.struct_type3 = struct_type3
self.member = member
class Statement:
"""
A statement within a function
"""
__slots__ = ()
__slots__ = ("sourceref", )
sourceref: SourceRef
def __init__(self, *, sourceref: SourceRef) -> None:
self.sourceref = sourceref
class StatementPass(Statement):
"""
@ -230,13 +265,18 @@ class StatementPass(Statement):
"""
__slots__ = ()
def __init__(self, sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
class StatementReturn(Statement):
"""
A return statement within a function
"""
__slots__ = ('value', )
def __init__(self, value: Expression) -> None:
def __init__(self, value: Expression, sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
self.value = value
def __repr__(self) -> str:
@ -261,55 +301,60 @@ class FunctionParam:
"""
A parameter for a Function
"""
__slots__ = ('name', 'type3', )
__slots__ = ('name', 'type5', )
name: str
type3: Type3
type5: type5typeexpr.TypeExpr
def __init__(self, name: str, type5: type5typeexpr.TypeExpr) -> None:
assert type5typeexpr.is_concrete(type5)
def __init__(self, name: str, type3: Type3) -> None:
self.name = name
self.type3 = type3
self.type5 = type5
def __repr__(self) -> str:
return f'FunctionParam({self.name!r}, {self.type3!r})'
return f'FunctionParam({self.name!r}, {self.type5!r})'
class Function:
"""
A function processes input and produces output
"""
__slots__ = ('name', 'lineno', 'exported', 'imported', 'statements', 'signature', 'returns_type3', 'posonlyargs', )
__slots__ = ('name', 'sourceref', 'exported', 'imported', 'statements', 'type5', 'arg_names', )
name: str
lineno: int
sourceref: SourceRef
exported: bool
imported: Optional[str]
statements: List[Statement]
signature: FunctionSignature
returns_type3: Type3
posonlyargs: List[FunctionParam]
type5: type5typeexpr.TypeExpr | type5constrainedexpr.ConstrainedExpr | None
arg_names: list[str]
def __init__(self, name: str, lineno: int) -> None:
def __init__(self, name: str, sourceref: SourceRef) -> None:
self.name = name
self.lineno = lineno
self.sourceref = sourceref
self.exported = False
self.imported = None
self.statements = []
self.signature = FunctionSignature(TypeVariableContext(), [])
self.returns_type3 = prelude.none # FIXME: This could be a placeholder
self.posonlyargs = []
self.type5 = None
self.arg_names = []
class BuiltinFunction(Function):
def __init__(self, name: str, type5: type5typeexpr.TypeExpr | type5constrainedexpr.ConstrainedExpr) -> None:
super().__init__(name, SourceRef("/", 0, 0))
self.type5 = type5
class StructDefinition:
"""
The definition for a struct
"""
__slots__ = ('struct_type3', 'lineno', )
__slots__ = ('struct_type5', 'sourceref', )
struct_type3: Type3
lineno: int
struct_type5: type5record.Record
sourceref: SourceRef
def __init__(self, struct_type3: Type3, lineno: int) -> None:
self.struct_type3 = struct_type3
self.lineno = lineno
def __init__(self, struct_type5: type5record.Record, sourceref: SourceRef) -> None:
self.struct_type5 = struct_type5
self.sourceref = sourceref
class StructConstructor(Function):
"""
@ -318,39 +363,32 @@ class StructConstructor(Function):
A function will generated to instantiate a struct. The arguments
will be the defaults
"""
__slots__ = ('struct_type3', )
__slots__ = ('struct_type5', )
struct_type3: Type3
struct_type5: type5record.Record
def __init__(self, struct_type3: Type3) -> None:
super().__init__(f'@{struct_type3.name}@__init___@', -1)
def __init__(self, struct_type5: type5record.Record, sourceref: SourceRef) -> None:
super().__init__(f'@{struct_type5.name}@__init___@', sourceref)
self.struct_type5 = struct_type5
assert isinstance(struct_type3.application, TypeApplication_Struct)
for mem, typ in struct_type3.application.arguments:
self.posonlyargs.append(FunctionParam(mem, typ, ))
self.signature.args.append(typ)
self.returns_type3 = struct_type3
self.signature.args.append(struct_type3)
self.struct_type3 = struct_type3
for mem, typ in struct_type5.fields:
self.arg_names.append(mem)
class ModuleConstantDef:
"""
A constant definition within a module
"""
__slots__ = ('name', 'lineno', 'type3', 'constant', )
__slots__ = ('name', 'sourceref', 'type5', 'constant', )
name: str
lineno: int
type3: Type3
sourceref: SourceRef
type5: type5typeexpr.TypeExpr
constant: Constant
def __init__(self, name: str, lineno: int, type3: Type3, constant: Constant) -> None:
def __init__(self, name: str, sourceref: SourceRef, type5: type5typeexpr.TypeExpr, constant: Constant) -> None:
self.name = name
self.lineno = lineno
self.type3 = type3
self.sourceref = sourceref
self.type5 = type5
self.constant = constant
class ModuleDataBlock:
@ -366,6 +404,9 @@ class ModuleDataBlock:
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
@ -377,25 +418,32 @@ class ModuleData:
def __init__(self) -> None:
self.blocks = []
class Module:
class Module[G]:
"""
A module is a file and consists of functions
"""
__slots__ = ('data', 'types', 'struct_definitions', 'constant_defs', 'functions', 'operators', 'functions_table', )
__slots__ = ('build', 'filename', 'data', 'types', 'type5s', 'struct_definitions', 'constant_defs', 'functions', 'methods', 'operators', 'functions_table', )
build: BuildBase[G]
filename: str
data: ModuleData
types: dict[str, Type3]
types: dict[str, type5typeexpr.TypeExpr]
struct_definitions: Dict[str, StructDefinition]
constant_defs: Dict[str, ModuleConstantDef]
functions: Dict[str, Function]
operators: Dict[str, Type3ClassMethod]
methods: Dict[str, type5typeexpr.TypeExpr | type5constrainedexpr.ConstrainedExpr]
operators: Dict[str, type5typeexpr.TypeExpr | type5constrainedexpr.ConstrainedExpr]
functions_table: dict[Function, int]
def __init__(self) -> None:
def __init__(self, build: BuildBase[G], filename: str) -> None:
self.build = build
self.filename = filename
self.data = ModuleData()
self.types = {}
self.struct_definitions = {}
self.constant_defs = {}
self.functions = {}
self.methods = {}
self.operators = {}
self.functions_table = {}

View File

@ -4,11 +4,13 @@ Parses the source code from the plain text into a syntax tree
import ast
from typing import Any, Dict, NoReturn, Union
from . import prelude
from .build.base import BuildBase
from .build.default import BuildDefault
from .exceptions import StaticError
from .ourlang import (
AccessStructMember,
BinaryOp,
BuiltinFunction,
ConstantBytes,
ConstantPrimitive,
ConstantStruct,
@ -21,6 +23,7 @@ from .ourlang import (
Module,
ModuleConstantDef,
ModuleDataBlock,
SourceRef,
Statement,
StatementIf,
StatementPass,
@ -31,12 +34,11 @@ from .ourlang import (
TupleInstantiation,
VariableReference,
)
from .prelude import PRELUDE_METHODS, PRELUDE_OPERATORS, PRELUDE_TYPES
from .type3.typeclasses import Type3ClassMethod
from .type3.types import IntType3, Type3
from .type5 import typeexpr as type5typeexpr
from .wasmgenerator import Generator
def phasm_parse(source: str) -> Module:
def phasm_parse(source: str) -> Module[Generator]:
"""
Public method for parsing Phasm code into a Phasm Module
"""
@ -44,7 +46,8 @@ def phasm_parse(source: str) -> Module:
res = OptimizerTransformer().visit(res)
our_visitor = OurVisitor()
build = BuildDefault()
our_visitor = OurVisitor(build)
return our_visitor.visit_Module(res)
OurLocals = Dict[str, Union[FunctionParam]] # FIXME: Does it become easier if we add ModuleConstantDef to this dict?
@ -75,7 +78,7 @@ class OptimizerTransformer(ast.NodeTransformer):
return node.operand
return node
class OurVisitor:
class OurVisitor[G]:
"""
Class to visit a Python syntax tree and create an ourlang syntax tree
@ -90,14 +93,15 @@ class OurVisitor:
# pylint: disable=C0103,C0116,C0301,R0201,R0912
def __init__(self) -> None:
pass
def __init__(self, build: BuildBase[G]) -> None:
self.build = build
def visit_Module(self, node: ast.Module) -> Module:
module = Module()
def visit_Module(self, node: ast.Module) -> Module[G]:
module = Module(self.build, "-")
module.operators.update(PRELUDE_OPERATORS)
module.types.update(PRELUDE_TYPES)
module.methods.update({k: v[0] for k, v in self.build.methods.items()})
module.operators.update({k: v[0] for k, v in self.build.operators.items()})
module.types.update(self.build.types)
_not_implemented(not node.type_ignores, 'Module.type_ignores')
@ -109,26 +113,27 @@ class OurVisitor:
if isinstance(res, ModuleConstantDef):
if res.name in module.constant_defs:
raise StaticError(
f'{res.name} already defined on line {module.constant_defs[res.name].lineno}'
f'{res.name} already defined on line {module.constant_defs[res.name].sourceref.lineno}'
)
module.constant_defs[res.name] = res
if isinstance(res, StructDefinition):
if res.struct_type3.name in module.types:
if res.struct_type5.name in module.types:
raise StaticError(
f'{res.struct_type3.name} already defined as type'
f'{res.struct_type5.name} already defined as type'
)
module.types[res.struct_type3.name] = res.struct_type3
module.functions[res.struct_type3.name] = StructConstructor(res.struct_type3)
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_type3.name] = res
module.struct_definitions[res.struct_type5.name] = res
if isinstance(res, Function):
if res.name in module.functions:
raise StaticError(
f'{res.name} already defined on line {module.functions[res.name].lineno}'
f'{res.name} already defined on line {module.functions[res.name].sourceref.lineno}'
)
module.functions[res.name] = res
@ -140,7 +145,7 @@ class OurVisitor:
return module
def pre_visit_Module_stmt(self, module: Module, node: ast.stmt) -> Union[Function, StructDefinition, ModuleConstantDef]:
def pre_visit_Module_stmt(self, module: Module[G], node: ast.stmt) -> Union[Function, StructDefinition, ModuleConstantDef]:
if isinstance(node, ast.FunctionDef):
return self.pre_visit_Module_FunctionDef(module, node)
@ -152,25 +157,25 @@ class OurVisitor:
raise NotImplementedError(f'{node} on Module')
def pre_visit_Module_FunctionDef(self, module: Module, node: ast.FunctionDef) -> Function:
function = Function(node.name, node.lineno)
def pre_visit_Module_FunctionDef(self, module: Module[G], node: ast.FunctionDef) -> Function:
function = Function(node.name, srf(module, node))
_not_implemented(not node.args.posonlyargs, 'FunctionDef.args.posonlyargs')
arg_type5_list = []
for arg in node.args.args:
if arg.annotation is None:
_raise_static_error(node, 'Must give a argument type')
arg_type = self.visit_type(module, arg.annotation)
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.signature.args.append(arg_type)
function.posonlyargs.append(FunctionParam(
arg.arg,
arg_type,
))
function.arg_names.append(arg.arg)
_not_implemented(not node.args.vararg, 'FunctionDef.args.vararg')
_not_implemented(not node.args.kwonlyargs, 'FunctionDef.args.kwonlyargs')
@ -212,21 +217,21 @@ class OurVisitor:
if node.returns is None: # Note: `-> None` would be a ast.Constant
_raise_static_error(node, 'Must give a return type')
return_type = self.visit_type(module, node.returns)
function.signature.args.append(return_type)
function.returns_type3 = return_type
arg_type5_list.append(self.visit_type5(module, node.returns))
function.type5 = module.build.type5_make_function(arg_type5_list)
_not_implemented(not node.type_comment, 'FunctionDef.type_comment')
return function
def pre_visit_Module_ClassDef(self, module: Module, node: ast.ClassDef) -> StructDefinition:
def pre_visit_Module_ClassDef(self, module: Module[G], node: ast.ClassDef) -> StructDefinition:
_not_implemented(not node.bases, 'ClassDef.bases')
_not_implemented(not node.keywords, 'ClassDef.keywords')
_not_implemented(not node.decorator_list, 'ClassDef.decorator_list')
members: Dict[str, Type3] = {}
members: Dict[str, type5typeexpr.AtomicType | type5typeexpr.TypeApplication] = {}
for stmt in node.body:
if not isinstance(stmt, ast.AnnAssign):
@ -244,11 +249,16 @@ class OurVisitor:
if stmt.target.id in members:
_raise_static_error(stmt, 'Struct members must have unique names')
members[stmt.target.id] = self.visit_type(module, stmt.annotation)
field_type5 = self.visit_type5(module, stmt.annotation)
assert isinstance(field_type5, (type5typeexpr.AtomicType, type5typeexpr.TypeApplication, ))
members[stmt.target.id] = field_type5
return StructDefinition(prelude.struct(node.name, tuple(members.items())), node.lineno)
return StructDefinition(
module.build.type5_make_struct(node.name, tuple(members.items())),
srf(module, node),
)
def pre_visit_Module_AnnAssign(self, module: Module, node: ast.AnnAssign) -> ModuleConstantDef:
def pre_visit_Module_AnnAssign(self, module: Module[G], node: ast.AnnAssign) -> ModuleConstantDef:
if not isinstance(node.target, ast.Name):
_raise_static_error(node.target, 'Must be name')
if not isinstance(node.target.ctx, ast.Store):
@ -259,8 +269,8 @@ class OurVisitor:
return ModuleConstantDef(
node.target.id,
node.lineno,
self.visit_type(module, node.annotation),
srf(module, node),
self.visit_type5(module, node.annotation),
value_data,
)
@ -272,8 +282,8 @@ class OurVisitor:
# Then return the constant as a pointer
return ModuleConstantDef(
node.target.id,
node.lineno,
self.visit_type(module, node.annotation),
srf(module, node),
self.visit_type5(module, node.annotation),
value_data,
)
@ -285,14 +295,14 @@ class OurVisitor:
# Then return the constant as a pointer
return ModuleConstantDef(
node.target.id,
node.lineno,
self.visit_type(module, node.annotation),
srf(module, node),
self.visit_type5(module, node.annotation),
value_data,
)
raise NotImplementedError(f'{node} on Module AnnAssign')
def visit_Module_stmt(self, module: Module, node: ast.stmt) -> None:
def visit_Module_stmt(self, module: Module[G], node: ast.stmt) -> None:
if isinstance(node, ast.FunctionDef):
self.visit_Module_FunctionDef(module, node)
return
@ -305,12 +315,17 @@ class OurVisitor:
raise NotImplementedError(f'{node} on Module')
def visit_Module_FunctionDef(self, module: Module, node: ast.FunctionDef) -> None:
def visit_Module_FunctionDef(self, module: Module[G], node: ast.FunctionDef) -> None:
function = module.functions[node.name]
assert function.type5 is not None
func_arg_types = module.build.type5_is_function(function.type5)
assert func_arg_types is not None
func_arg_types.pop()
our_locals: OurLocals = {
x.name: x
for x in function.posonlyargs
a_nam: FunctionParam(a_nam, a_typ)
for a_nam, a_typ in zip(function.arg_names, func_arg_types, strict=True)
}
for stmt in node.body:
@ -318,14 +333,15 @@ class OurVisitor:
self.visit_Module_FunctionDef_stmt(module, function, our_locals, stmt)
)
def visit_Module_FunctionDef_stmt(self, module: Module, function: Function, our_locals: OurLocals, node: ast.stmt) -> Statement:
def visit_Module_FunctionDef_stmt(self, module: Module[G], function: Function, our_locals: OurLocals, node: ast.stmt) -> Statement:
if isinstance(node, ast.Return):
if node.value is None:
# TODO: Implement methods without return values
_raise_static_error(node, 'Return must have an argument')
return StatementReturn(
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.value)
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.value),
srf(module, node),
)
if isinstance(node, ast.If):
@ -346,13 +362,13 @@ class OurVisitor:
return result
if isinstance(node, ast.Pass):
return StatementPass()
return StatementPass(srf(module, node))
raise NotImplementedError(f'{node} as stmt in FunctionDef')
def visit_Module_FunctionDef_expr(self, module: Module, function: Function, our_locals: OurLocals, node: ast.expr) -> Expression:
def visit_Module_FunctionDef_expr(self, module: Module[G], function: Function, our_locals: OurLocals, node: ast.expr) -> Expression:
if isinstance(node, ast.BinOp):
operator: Union[str, Type3ClassMethod]
operator: str
if isinstance(node.op, ast.Add):
operator = '+'
@ -383,9 +399,10 @@ class OurVisitor:
raise NotImplementedError(f'Operator {operator}')
return BinaryOp(
module.operators[operator],
BuiltinFunction(operator, module.operators[operator]),
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.left),
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.right),
srf(module, node),
)
if isinstance(node, ast.Compare):
@ -411,9 +428,10 @@ class OurVisitor:
raise NotImplementedError(f'Operator {operator}')
return BinaryOp(
module.operators[operator],
BuiltinFunction(operator, module.operators[operator]),
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.left),
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.comparators[0]),
srf(module, node),
)
if isinstance(node, ast.Call):
@ -440,15 +458,15 @@ class OurVisitor:
if node.id in our_locals:
param = our_locals[node.id]
return VariableReference(param)
return VariableReference(param, srf(module, node))
if node.id in module.constant_defs:
cdef = module.constant_defs[node.id]
return VariableReference(cdef)
return VariableReference(cdef, srf(module, node))
if node.id in module.functions:
fun = module.functions[node.id]
return FunctionReference(fun)
return FunctionReference(fun, srf(module, node))
_raise_static_error(node, f'Undefined variable {node.id}')
@ -462,11 +480,11 @@ class OurVisitor:
if len(arguments) != len(node.elts):
raise NotImplementedError('Non-constant tuple members')
return TupleInstantiation(arguments)
return TupleInstantiation(arguments, srf(module, node))
raise NotImplementedError(f'{node} as expr in FunctionDef')
def visit_Module_FunctionDef_Call(self, module: Module, function: Function, our_locals: OurLocals, node: ast.Call) -> Union[FunctionCall]:
def visit_Module_FunctionDef_Call(self, module: Module[G], function: Function, our_locals: OurLocals, node: ast.Call) -> Union[FunctionCall]:
if node.keywords:
_raise_static_error(node, 'Keyword calling not supported') # Yet?
@ -475,10 +493,10 @@ class OurVisitor:
if not isinstance(node.func.ctx, ast.Load):
_raise_static_error(node, 'Must be load context')
func: Union[Function, FunctionParam, Type3ClassMethod]
func: Union[Function, FunctionParam]
if node.func.id in PRELUDE_METHODS:
func = PRELUDE_METHODS[node.func.id]
if node.func.id in module.methods:
func = BuiltinFunction(node.func.id, module.methods[node.func.id])
elif node.func.id in our_locals:
func = our_locals[node.func.id]
else:
@ -487,14 +505,14 @@ class OurVisitor:
func = module.functions[node.func.id]
result = FunctionCall(func)
result = FunctionCall(func, sourceref=srf(module, node))
result.arguments.extend(
self.visit_Module_FunctionDef_expr(module, function, our_locals, arg_expr)
for arg_expr in node.args
)
return result
def visit_Module_FunctionDef_Attribute(self, module: Module, function: Function, our_locals: OurLocals, node: ast.Attribute) -> Expression:
def visit_Module_FunctionDef_Attribute(self, module: Module[G], function: Function, our_locals: OurLocals, node: ast.Attribute) -> Expression:
if not isinstance(node.value, ast.Name):
_raise_static_error(node, 'Must reference a name')
@ -507,11 +525,11 @@ class OurVisitor:
return AccessStructMember(
varref,
varref.variable.type3,
node.attr,
srf(module, node),
)
def visit_Module_FunctionDef_Subscript(self, module: Module, function: Function, our_locals: OurLocals, node: ast.Subscript) -> Expression:
def visit_Module_FunctionDef_Subscript(self, module: Module[G], function: Function, our_locals: OurLocals, node: ast.Subscript) -> Expression:
if not isinstance(node.value, ast.Name):
_raise_static_error(node, 'Must reference a name')
@ -524,10 +542,10 @@ class OurVisitor:
varref: VariableReference
if node.value.id in our_locals:
param = our_locals[node.value.id]
varref = VariableReference(param)
varref = VariableReference(param, srf(module, node))
elif node.value.id in module.constant_defs:
constant_def = module.constant_defs[node.value.id]
varref = VariableReference(constant_def)
varref = VariableReference(constant_def, srf(module, node))
else:
_raise_static_error(node, f'Undefined variable {node.value.id}')
@ -535,9 +553,9 @@ class OurVisitor:
module, function, our_locals, node.slice,
)
return Subscript(varref, slice_expr)
return Subscript(varref, slice_expr, srf(module, node))
def visit_Module_Constant(self, module: Module, node: Union[ast.Constant, ast.Tuple, ast.Call]) -> Union[ConstantPrimitive, ConstantBytes, ConstantTuple, ConstantStruct]:
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)
@ -552,7 +570,7 @@ class OurVisitor:
data_block = ModuleDataBlock(tuple_data)
module.data.blocks.append(data_block)
return ConstantTuple(tuple_data, data_block)
return ConstantTuple(tuple_data, data_block, srf(module, node))
if isinstance(node, ast.Call):
# Struct constant
@ -580,29 +598,29 @@ class OurVisitor:
data_block = ModuleDataBlock(struct_data)
module.data.blocks.append(data_block)
return ConstantStruct(struct_def.struct_type3, struct_data, data_block)
return ConstantStruct(struct_def.struct_type5, struct_data, data_block, srf(module, node))
_not_implemented(node.kind is None, 'Constant.kind')
if isinstance(node.value, (int, float, )):
return ConstantPrimitive(node.value)
return ConstantPrimitive(node.value, srf(module, node))
if isinstance(node.value, bytes):
data_block = ModuleDataBlock([])
module.data.blocks.append(data_block)
result = ConstantBytes(node.value, data_block)
result = ConstantBytes(node.value, data_block, srf(module, node))
data_block.data.append(result)
return result
raise NotImplementedError(f'{node.value} as constant')
def visit_type(self, module: Module, node: ast.expr) -> Type3:
def visit_type5(self, module: Module[G], node: ast.expr) -> type5typeexpr.TypeExpr:
if isinstance(node, ast.Constant):
if node.value is None:
return prelude.none
return module.build.none_type5
_raise_static_error(node, f'Unrecognized type {node.value}')
_raise_static_error(node, f'Unrecognized type {node.value!r}')
if isinstance(node, ast.Name):
if not isinstance(node.ctx, ast.Load):
@ -624,9 +642,8 @@ class OurVisitor:
else:
_raise_static_error(node, 'Must subscript using a list of types')
# Function type
return prelude.function(*[
self.visit_type(module, e)
return module.build.type5_make_function([
self.visit_type5(module, e)
for e in func_arg_types
])
@ -637,8 +654,8 @@ class OurVisitor:
_raise_static_error(node, 'Must subscript using a constant index')
if node.slice.value is Ellipsis:
return prelude.dynamic_array(
self.visit_type(module, node.value),
return module.build.type5_make_dynamic_array(
self.visit_type5(module, node.value),
)
if not isinstance(node.slice.value, int):
@ -646,20 +663,20 @@ class OurVisitor:
if not isinstance(node.ctx, ast.Load):
_raise_static_error(node, 'Must be load context')
return prelude.static_array(
self.visit_type(module, node.value),
IntType3(node.slice.value),
return module.build.type5_make_static_array(
node.slice.value,
self.visit_type5(module, node.value),
)
if isinstance(node, ast.Tuple):
if not isinstance(node.ctx, ast.Load):
_raise_static_error(node, 'Must be load context')
return prelude.tuple_(
*(self.visit_type(module, elt) for elt in node.elts)
return module.build.type5_make_tuple(
[self.visit_type5(module, elt) for elt in node.elts],
)
raise NotImplementedError(f'{node} as type')
raise NotImplementedError(node)
def _not_implemented(check: Any, msg: str) -> None:
if not check:
@ -669,3 +686,6 @@ def _raise_static_error(node: Union[ast.stmt, ast.expr], msg: str) -> NoReturn:
raise StaticError(
f'Static error on line {node.lineno}: {msg}'
)
def srf(mod: Module[Any], node: ast.stmt | ast.expr) -> SourceRef:
return SourceRef(mod.filename, node.lineno, node.col_offset)

View File

@ -1,625 +0,0 @@
"""
The prelude are all the builtin types, type classes and methods
"""
from typing import Any, Callable
from warnings import warn
from phasm.stdlib import types as stdtypes
from phasm.wasmgenerator import Generator
from ..type3.functions import (
Constraint_TypeClassInstanceExists,
TypeConstructorVariable,
TypeVariable,
TypeVariableApplication_Nullary,
)
from ..type3.routers import TypeClassArgsRouter, TypeVariableLookup
from ..type3.typeclasses import Type3Class, Type3ClassMethod
from ..type3.types import (
Type3,
TypeApplication_Nullary,
TypeConstructor_Base,
TypeConstructor_DynamicArray,
TypeConstructor_Function,
TypeConstructor_StaticArray,
TypeConstructor_Struct,
TypeConstructor_Tuple,
)
PRELUDE_TYPE_CLASS_INSTANCES_EXISTING: set[tuple[Type3Class, tuple[Type3 | TypeConstructor_Base[Any], ...]]] = set()
PRELUDE_TYPE_CLASS_INSTANCE_METHODS: dict[Type3ClassMethod, TypeClassArgsRouter[Generator, None]] = {}
class MissingImplementationException(Exception):
pass
class MissingImplementationWarning(Warning):
pass
def instance_type_class(
cls: Type3Class,
*typ: Type3 | TypeConstructor_Base[Any],
methods: dict[str, Callable[[Generator, TypeVariableLookup], None]] = {},
operators: dict[str, Callable[[Generator, TypeVariableLookup], None]] = {},
) -> None:
global PRELUDE_TYPE_CLASS_INSTANCES_EXISTING
global PRELUDE_TYPE_CLASS_INSTANCE_METHODS
assert len(cls.args) == len(typ)
tv_map: dict[TypeVariable, Type3] = {}
tc_map: dict[TypeConstructorVariable, TypeConstructor_Base[Any]] = {}
for arg_tv, arg_tp in zip(cls.args, typ, strict=True):
if isinstance(arg_tv, TypeVariable):
assert isinstance(arg_tp, Type3)
tv_map[arg_tv] = arg_tp
elif isinstance(arg_tv, TypeConstructorVariable):
assert isinstance(arg_tp, TypeConstructor_Base)
tc_map[arg_tv] = arg_tp
else:
raise NotImplementedError(arg_tv, arg_tp)
# TODO: Check for required existing instantiations
PRELUDE_TYPE_CLASS_INSTANCES_EXISTING.add((cls, tuple(typ), ))
for method_name, method in cls.methods.items():
router = PRELUDE_TYPE_CLASS_INSTANCE_METHODS.get(method)
if router is None:
router = TypeClassArgsRouter[Generator, None](cls.args)
PRELUDE_TYPE_CLASS_INSTANCE_METHODS[method] = router
try:
generator = methods[method_name]
except KeyError:
warn(MissingImplementationWarning(str(method), cls.name + ' ' + ' '.join(x.name for x in typ)))
continue
router.add(tv_map, tc_map, generator)
for operator_name, operator in cls.operators.items():
router = PRELUDE_TYPE_CLASS_INSTANCE_METHODS.get(operator)
if router is None:
router = TypeClassArgsRouter[Generator, None](cls.args)
PRELUDE_TYPE_CLASS_INSTANCE_METHODS[operator] = router
try:
generator = operators[operator_name]
except KeyError:
warn(MissingImplementationWarning(str(operator), cls.name + ' ' + ' '.join(x.name for x in typ)))
continue
router.add(tv_map, tc_map, generator)
none = Type3('none', TypeApplication_Nullary(None, None))
"""
The none type, for when functions simply don't return anything. e.g., IO().
"""
bool_ = Type3('bool', TypeApplication_Nullary(None, None))
"""
The bool type, either True or False
Suffixes with an underscores, as it's a Python builtin
"""
u8 = Type3('u8', TypeApplication_Nullary(None, None))
"""
The unsigned 8-bit integer type.
Operations on variables employ modular arithmetic, with modulus 2^8.
"""
u32 = Type3('u32', TypeApplication_Nullary(None, None))
"""
The unsigned 32-bit integer type.
Operations on variables employ modular arithmetic, with modulus 2^32.
"""
u64 = Type3('u64', TypeApplication_Nullary(None, None))
"""
The unsigned 64-bit integer type.
Operations on variables employ modular arithmetic, with modulus 2^64.
"""
i8 = Type3('i8', TypeApplication_Nullary(None, None))
"""
The signed 8-bit integer type.
Operations on variables employ modular arithmetic, with modulus 2^8, but
with the middel point being 0.
"""
i32 = Type3('i32', TypeApplication_Nullary(None, None))
"""
The unsigned 32-bit integer type.
Operations on variables employ modular arithmetic, with modulus 2^32, but
with the middel point being 0.
"""
i64 = Type3('i64', TypeApplication_Nullary(None, None))
"""
The unsigned 64-bit integer type.
Operations on variables employ modular arithmetic, with modulus 2^64, but
with the middel point being 0.
"""
f32 = Type3('f32', TypeApplication_Nullary(None, None))
"""
A 32-bits IEEE 754 float, of 32 bits width.
"""
f64 = Type3('f64', TypeApplication_Nullary(None, None))
"""
A 32-bits IEEE 754 float, of 64 bits width.
"""
dynamic_array = TypeConstructor_DynamicArray('dynamic_array')
"""
This is a dynamic length piece of memory.
It should be applied with two arguments. It has a runtime
determined length, and each argument is the same.
"""
static_array = TypeConstructor_StaticArray('static_array')
"""
This is a fixed length piece of memory.
It should be applied with two arguments. It has a compile time
determined length, and each argument is the same.
"""
tuple_ = TypeConstructor_Tuple('tuple')
"""
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.
"""
function = TypeConstructor_Function('function')
"""
This is a function.
It should be applied with one or more arguments. The last argument is the 'return' type.
"""
struct = TypeConstructor_Struct('struct')
"""
This is like a tuple, but each argument is named, so that developers
can get and set fields by name.
"""
a = TypeVariable('a', TypeVariableApplication_Nullary(None, None))
b = TypeVariable('b', TypeVariableApplication_Nullary(None, None))
t = TypeConstructorVariable('t')
Eq = Type3Class('Eq', (a, ), methods={}, operators={
'==': [a, a, bool_],
'!=': [a, a, bool_],
# FIXME: Do we want to expose 'eqz'? Or is that a compiler optimization?
})
instance_type_class(Eq, u8, operators={
'==': stdtypes.u8_eq_equals,
'!=': stdtypes.u8_eq_not_equals,
})
instance_type_class(Eq, u32, operators={
'==': stdtypes.u32_eq_equals,
'!=': stdtypes.u32_eq_not_equals,
})
instance_type_class(Eq, u64, operators={
'==': stdtypes.u64_eq_equals,
'!=': stdtypes.u64_eq_not_equals,
})
instance_type_class(Eq, i8, operators={
'==': stdtypes.i8_eq_equals,
'!=': stdtypes.i8_eq_not_equals,
})
instance_type_class(Eq, i32, operators={
'==': stdtypes.i32_eq_equals,
'!=': stdtypes.i32_eq_not_equals,
})
instance_type_class(Eq, i64, operators={
'==': stdtypes.i64_eq_equals,
'!=': stdtypes.i64_eq_not_equals,
})
instance_type_class(Eq, f32, operators={
'==': stdtypes.f32_eq_equals,
'!=': stdtypes.f32_eq_not_equals,
})
instance_type_class(Eq, f64, operators={
'==': stdtypes.f64_eq_equals,
'!=': stdtypes.f64_eq_not_equals,
})
Ord = Type3Class('Ord', (a, ), methods={
'min': [a, a, a],
'max': [a, a, a],
}, operators={
'<': [a, a, bool_],
'<=': [a, a, bool_],
'>': [a, a, bool_],
'>=': [a, a, bool_],
}, inherited_classes=[Eq])
instance_type_class(Ord, u8, methods={
'min': stdtypes.u8_ord_min,
'max': stdtypes.u8_ord_max,
}, operators={
'<': stdtypes.u8_ord_less_than,
'<=': stdtypes.u8_ord_less_than_or_equal,
'>': stdtypes.u8_ord_greater_than,
'>=': stdtypes.u8_ord_greater_than_or_equal,
})
instance_type_class(Ord, u32, methods={
'min': stdtypes.u32_ord_min,
'max': stdtypes.u32_ord_max,
}, operators={
'<': stdtypes.u32_ord_less_than,
'<=': stdtypes.u32_ord_less_than_or_equal,
'>': stdtypes.u32_ord_greater_than,
'>=': stdtypes.u32_ord_greater_than_or_equal,
})
instance_type_class(Ord, u64, methods={
'min': stdtypes.u64_ord_min,
'max': stdtypes.u64_ord_max,
}, operators={
'<': stdtypes.u64_ord_less_than,
'<=': stdtypes.u64_ord_less_than_or_equal,
'>': stdtypes.u64_ord_greater_than,
'>=': stdtypes.u64_ord_greater_than_or_equal,
})
instance_type_class(Ord, i8, methods={
'min': stdtypes.i8_ord_min,
'max': stdtypes.i8_ord_max,
}, operators={
'<': stdtypes.i8_ord_less_than,
'<=': stdtypes.i8_ord_less_than_or_equal,
'>': stdtypes.i8_ord_greater_than,
'>=': stdtypes.i8_ord_greater_than_or_equal,
})
instance_type_class(Ord, i32, methods={
'min': stdtypes.i32_ord_min,
'max': stdtypes.i32_ord_max,
}, operators={
'<': stdtypes.i32_ord_less_than,
'<=': stdtypes.i32_ord_less_than_or_equal,
'>': stdtypes.i32_ord_greater_than,
'>=': stdtypes.i32_ord_greater_than_or_equal,
})
instance_type_class(Ord, i64, methods={
'min': stdtypes.i64_ord_min,
'max': stdtypes.i64_ord_max,
}, operators={
'<': stdtypes.i64_ord_less_than,
'<=': stdtypes.i64_ord_less_than_or_equal,
'>': stdtypes.i64_ord_greater_than,
'>=': stdtypes.i64_ord_greater_than_or_equal,
})
instance_type_class(Ord, f32, methods={
'min': stdtypes.f32_ord_min,
'max': stdtypes.f32_ord_max,
}, operators={
'<': stdtypes.f32_ord_less_than,
'<=': stdtypes.f32_ord_less_than_or_equal,
'>': stdtypes.f32_ord_greater_than,
'>=': stdtypes.f32_ord_greater_than_or_equal,
})
instance_type_class(Ord, f64, methods={
'min': stdtypes.f64_ord_min,
'max': stdtypes.f64_ord_max,
}, operators={
'<': stdtypes.f64_ord_less_than,
'<=': stdtypes.f64_ord_less_than_or_equal,
'>': stdtypes.f64_ord_greater_than,
'>=': stdtypes.f64_ord_greater_than_or_equal,
})
Bits = Type3Class('Bits', (a, ), methods={
'shl': [a, u32, a], # Logical shift left
'shr': [a, u32, a], # Logical shift right
'rotl': [a, u32, a], # Rotate bits left
'rotr': [a, u32, a], # Rotate bits right
# FIXME: Do we want to expose clz, ctz, popcnt?
}, operators={
'&': [a, a, a], # Bit-wise and
'|': [a, a, a], # Bit-wise or
'^': [a, a, a], # Bit-wise xor
})
instance_type_class(Bits, u8, methods={
'shl': stdtypes.u8_bits_logical_shift_left,
'shr': stdtypes.u8_bits_logical_shift_right,
'rotl': stdtypes.u8_bits_rotate_left,
'rotr': stdtypes.u8_bits_rotate_right,
}, operators={
'&': stdtypes.u8_bits_bitwise_and,
'|': stdtypes.u8_bits_bitwise_or,
'^': stdtypes.u8_bits_bitwise_xor,
})
instance_type_class(Bits, u32, methods={
'shl': stdtypes.u32_bits_logical_shift_left,
'shr': stdtypes.u32_bits_logical_shift_right,
'rotl': stdtypes.u32_bits_rotate_left,
'rotr': stdtypes.u32_bits_rotate_right,
}, operators={
'&': stdtypes.u32_bits_bitwise_and,
'|': stdtypes.u32_bits_bitwise_or,
'^': stdtypes.u32_bits_bitwise_xor,
})
instance_type_class(Bits, u64, methods={
'shl': stdtypes.u64_bits_logical_shift_left,
'shr': stdtypes.u64_bits_logical_shift_right,
'rotl': stdtypes.u64_bits_rotate_left,
'rotr': stdtypes.u64_bits_rotate_right,
}, operators={
'&': stdtypes.u64_bits_bitwise_and,
'|': stdtypes.u64_bits_bitwise_or,
'^': stdtypes.u64_bits_bitwise_xor,
})
NatNum = Type3Class('NatNum', (a, ), methods={}, operators={
'+': [a, a, a],
'-': [a, a, a],
'*': [a, a, a],
'<<': [a, u32, a], # Arithmic shift left
'>>': [a, u32, a], # Arithmic shift right
})
instance_type_class(NatNum, u32, operators={
'+': stdtypes.u32_natnum_add,
'-': stdtypes.u32_natnum_sub,
'*': stdtypes.u32_natnum_mul,
'<<': stdtypes.u32_natnum_arithmic_shift_left,
'>>': stdtypes.u32_natnum_arithmic_shift_right,
})
instance_type_class(NatNum, u64, operators={
'+': stdtypes.u64_natnum_add,
'-': stdtypes.u64_natnum_sub,
'*': stdtypes.u64_natnum_mul,
'<<': stdtypes.u64_natnum_arithmic_shift_left,
'>>': stdtypes.u64_natnum_arithmic_shift_right,
})
instance_type_class(NatNum, i32, operators={
'+': stdtypes.i32_natnum_add,
'-': stdtypes.i32_natnum_sub,
'*': stdtypes.i32_natnum_mul,
'<<': stdtypes.i32_natnum_arithmic_shift_left,
'>>': stdtypes.i32_natnum_arithmic_shift_right,
})
instance_type_class(NatNum, i64, operators={
'+': stdtypes.i64_natnum_add,
'-': stdtypes.i64_natnum_sub,
'*': stdtypes.i64_natnum_mul,
'<<': stdtypes.i64_natnum_arithmic_shift_left,
'>>': stdtypes.i64_natnum_arithmic_shift_right,
})
instance_type_class(NatNum, f32, operators={
'+': stdtypes.f32_natnum_add,
'-': stdtypes.f32_natnum_sub,
'*': stdtypes.f32_natnum_mul,
'<<': stdtypes.f32_natnum_arithmic_shift_left,
'>>': stdtypes.f32_natnum_arithmic_shift_right,
})
instance_type_class(NatNum, f64, operators={
'+': stdtypes.f64_natnum_add,
'-': stdtypes.f64_natnum_sub,
'*': stdtypes.f64_natnum_mul,
'<<': stdtypes.f64_natnum_arithmic_shift_left,
'>>': stdtypes.f64_natnum_arithmic_shift_right,
})
IntNum = Type3Class('IntNum', (a, ), methods={
'abs': [a, a],
'neg': [a, a],
}, operators={}, inherited_classes=[NatNum])
instance_type_class(IntNum, i32, methods={
'abs': stdtypes.i32_intnum_abs,
'neg': stdtypes.i32_intnum_neg,
})
instance_type_class(IntNum, i64, methods={
'abs': stdtypes.i64_intnum_abs,
'neg': stdtypes.i64_intnum_neg,
})
instance_type_class(IntNum, f32, methods={
'abs': stdtypes.f32_intnum_abs,
'neg': stdtypes.f32_intnum_neg,
})
instance_type_class(IntNum, f64, methods={
'abs': stdtypes.f64_intnum_abs,
'neg': stdtypes.f64_intnum_neg,
})
Integral = Type3Class('Integral', (a, ), methods={
}, operators={
'//': [a, a, a],
'%': [a, a, a],
}, inherited_classes=[NatNum])
instance_type_class(Integral, u32, operators={
'//': stdtypes.u32_integral_div,
'%': stdtypes.u32_integral_rem,
})
instance_type_class(Integral, u64, operators={
'//': stdtypes.u64_integral_div,
'%': stdtypes.u64_integral_rem,
})
instance_type_class(Integral, i32, operators={
'//': stdtypes.i32_integral_div,
'%': stdtypes.i32_integral_rem,
})
instance_type_class(Integral, i64, operators={
'//': stdtypes.i64_integral_div,
'%': stdtypes.i64_integral_rem,
})
Fractional = Type3Class('Fractional', (a, ), methods={
'ceil': [a, a],
'floor': [a, a],
'trunc': [a, a],
'nearest': [a, a],
}, operators={
'/': [a, a, a],
}, inherited_classes=[NatNum])
instance_type_class(Fractional, f32, methods={
'ceil': stdtypes.f32_fractional_ceil,
'floor': stdtypes.f32_fractional_floor,
'trunc': stdtypes.f32_fractional_trunc,
'nearest': stdtypes.f32_fractional_nearest,
}, operators={
'/': stdtypes.f32_fractional_div,
})
instance_type_class(Fractional, f64, methods={
'ceil': stdtypes.f64_fractional_ceil,
'floor': stdtypes.f64_fractional_floor,
'trunc': stdtypes.f64_fractional_trunc,
'nearest': stdtypes.f64_fractional_nearest,
}, operators={
'/': stdtypes.f64_fractional_div,
})
Floating = Type3Class('Floating', (a, ), methods={
'sqrt': [a, a],
}, operators={}, inherited_classes=[Fractional])
# FIXME: Do we want to expose copysign?
instance_type_class(Floating, f32, methods={
'sqrt': stdtypes.f32_floating_sqrt,
})
instance_type_class(Floating, f64, methods={
'sqrt': stdtypes.f64_floating_sqrt,
})
Sized_ = Type3Class('Sized', (t, ), methods={
'len': [t(a), u32],
}, operators={}) # FIXME: Once we get type class families, add [] here
instance_type_class(Sized_, dynamic_array, methods={
'len': stdtypes.dynamic_array_sized_len,
})
instance_type_class(Sized_, static_array, methods={
'len': stdtypes.static_array_sized_len,
})
Extendable = Type3Class('Extendable', (a, b, ), methods={
'extend': [a, b],
'wrap': [b, a],
}, operators={})
instance_type_class(Extendable, u8, u32, methods={
'extend': stdtypes.u8_u32_extend,
'wrap': stdtypes.u8_u32_wrap,
})
instance_type_class(Extendable, u8, u64, methods={
'extend': stdtypes.u8_u64_extend,
'wrap': stdtypes.u8_u64_wrap,
})
instance_type_class(Extendable, u32, u64, methods={
'extend': stdtypes.u32_u64_extend,
'wrap': stdtypes.u32_u64_wrap,
})
instance_type_class(Extendable, i8, i32, methods={
'extend': stdtypes.i8_i32_extend,
'wrap': stdtypes.i8_i32_wrap,
})
instance_type_class(Extendable, i8, i64, methods={
'extend': stdtypes.i8_i64_extend,
'wrap': stdtypes.i8_i64_wrap,
})
instance_type_class(Extendable, i32, i64, methods={
'extend': stdtypes.i32_i64_extend,
'wrap': stdtypes.i32_i64_wrap,
})
Promotable = Type3Class('Promotable', (a, b, ), methods={
'promote': [a, b],
'demote': [b, a],
}, operators={})
instance_type_class(Promotable, f32, f64, methods={
'promote': stdtypes.f32_f64_promote,
'demote': stdtypes.f32_f64_demote,
})
Foldable = Type3Class('Foldable', (t, ), methods={
'sum': [t(a), a],
'foldl': [[b, a, b], b, t(a), b],
'foldr': [[a, b, b], b, t(a), b],
}, operators={}, additional_context={
'sum': [Constraint_TypeClassInstanceExists(NatNum, (a, ))],
})
instance_type_class(Foldable, dynamic_array, methods={
'sum': stdtypes.dynamic_array_sum,
'foldl': stdtypes.dynamic_array_foldl,
'foldr': stdtypes.dynamic_array_foldr,
})
instance_type_class(Foldable, static_array, methods={
'sum': stdtypes.static_array_sum,
'foldl': stdtypes.static_array_foldl,
'foldr': stdtypes.static_array_foldr,
})
bytes_ = dynamic_array(u8)
PRELUDE_TYPES: 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_,
}
PRELUDE_TYPE_CLASSES = {
'Eq': Eq,
'Ord': Ord,
'Bits': Bits,
'NatNum': NatNum,
'IntNum': IntNum,
'Integral': Integral,
'Fractional': Fractional,
'Floating': Floating,
'Extendable': Extendable,
'Promotable': Promotable,
}
PRELUDE_OPERATORS = {
**Bits.operators,
**Eq.operators,
**Ord.operators,
**Fractional.operators,
**Integral.operators,
**IntNum.operators,
**NatNum.operators,
}
PRELUDE_METHODS = {
**Bits.methods,
**Eq.methods,
**Ord.methods,
**Floating.methods,
**Fractional.methods,
**Integral.methods,
**IntNum.methods,
**NatNum.methods,
**Sized_.methods,
**Extendable.methods,
**Promotable.methods,
**Foldable.methods,
}

View File

@ -1,62 +0,0 @@
from . import prelude
from .stdlib.types import TYPE_INFO_CONSTRUCTED, TYPE_INFO_MAP
from .type3.routers import NoRouteForTypeException, TypeApplicationRouter
from .type3.types import IntType3, Type3
def calculate_alloc_size_static_array(is_member: bool, args: tuple[Type3, IntType3]) -> int:
if is_member:
return TYPE_INFO_CONSTRUCTED.alloc_size
sa_type, sa_len = args
return sa_len.value * calculate_alloc_size(sa_type, is_member=True)
def calculate_alloc_size_tuple(is_member: bool, args: tuple[Type3, ...]) -> int:
if is_member:
return TYPE_INFO_CONSTRUCTED.alloc_size
return sum(
calculate_alloc_size(x, is_member=True)
for x in args
)
def calculate_alloc_size_struct(is_member: bool, args: tuple[tuple[str, Type3], ...]) -> int:
if is_member:
return TYPE_INFO_CONSTRUCTED.alloc_size
return sum(
calculate_alloc_size(x, is_member=True)
for _, x in args
)
ALLOC_SIZE_ROUTER = TypeApplicationRouter[bool, int]()
ALLOC_SIZE_ROUTER.add(prelude.static_array, calculate_alloc_size_static_array)
ALLOC_SIZE_ROUTER.add(prelude.struct, calculate_alloc_size_struct)
ALLOC_SIZE_ROUTER.add(prelude.tuple_, calculate_alloc_size_tuple)
def calculate_alloc_size(typ: Type3, is_member: bool = False) -> int:
typ_info = TYPE_INFO_MAP.get(typ.name)
if typ_info is not None:
return typ_info.alloc_size
try:
return ALLOC_SIZE_ROUTER(is_member, typ)
except NoRouteForTypeException:
if is_member:
# By default, 'boxed' or 'constructed' types are
# stored as pointers when a member of a struct or tuple
return TYPE_INFO_CONSTRUCTED.alloc_size
raise NotImplementedError(typ)
def calculate_member_offset(st_name: str, st_args: tuple[tuple[str, Type3], ...], needle: str) -> int:
result = 0
for memnam, memtyp in st_args:
if needle == memnam:
return result
result += calculate_alloc_size(memtyp, is_member=True)
raise Exception(f'{needle} not in {st_name}')

View File

@ -15,7 +15,7 @@ UNALLOC_PTR = ADR_UNALLOC_PTR + 4
# For memory initialization see phasm.compiler.module_data
@func_wrapper(exported=False)
@func_wrapper()
def __find_free_block__(g: Generator, alloc_size: i32) -> i32:
# Find out if we've freed any blocks at all so far
g.i32.const(ADR_FREE_BLOCK_PTR)
@ -32,7 +32,7 @@ def __find_free_block__(g: Generator, alloc_size: i32) -> i32:
return i32('return') # To satisfy mypy
@func_wrapper()
@func_wrapper(exported=True)
def __alloc__(g: Generator, alloc_size: i32) -> i32:
result = i32('result')

File diff suppressed because it is too large Load Diff

View File

@ -1,692 +0,0 @@
"""
This module contains possible constraints generated based on the AST
These need to be resolved before the program can be compiled.
"""
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
from .. import ourlang, prelude
from .functions import FunctionArgument, TypeVariable
from .placeholders import PlaceholderForType, Type3OrPlaceholder
from .routers import NoRouteForTypeException, TypeApplicationRouter
from .typeclasses import Type3Class
from .types import (
IntType3,
Type3,
TypeApplication_Nullary,
TypeApplication_Struct,
TypeApplication_Type,
TypeApplication_TypeInt,
TypeApplication_TypeStar,
TypeConstructor_Base,
TypeConstructor_Struct,
)
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, *, comment: Optional[str] = None) -> None:
self.msg = msg
self.comment = comment
def __repr__(self) -> str:
return f'Error({repr(self.msg)}, comment={repr(self.comment)})'
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[PlaceholderForType, Type3]
NewConstraintList = List['ConstraintBase']
CheckResult = Union[None, SubstitutionMap, Error, NewConstraintList, RequireTypeSubstitutes]
HumanReadableRet = Tuple[str, Dict[str, Union[None, int, str, ourlang.Expression, Type3, PlaceholderForType]]]
class Context:
"""
Context for constraints
"""
__slots__ = ('type_class_instances_existing', )
# Constraint_TypeClassInstanceExists
type_class_instances_existing: set[tuple[Type3Class, tuple[Union[Type3, TypeConstructor_Base[Any], TypeConstructor_Struct], ...]]]
def __init__(self) -> None:
self.type_class_instances_existing = set()
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[Type3OrPlaceholder]
def __init__(self, *type_list: 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[Type3] = []
phft_list = []
for typ in self.type_list:
if isinstance(typ, Type3):
known_types.append(typ)
continue
if isinstance(typ, PlaceholderForType):
if typ.resolve_as is not None:
known_types.append(typ.resolve_as)
else:
phft_list.append(typ)
continue
raise NotImplementedError(typ)
if not known_types:
return RequireTypeSubstitutes()
first_type = known_types[0]
for ktyp in known_types[1:]:
if ktyp != first_type:
return Error(f'{ktyp:s} must be {first_type:s} instead', comment=self.comment)
if not phft_list:
return None
for phft in phft_list:
phft.resolve_as = first_type
return {
typ: first_type
for typ in phft_list
}
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 SameTypeArgumentConstraint(ConstraintBase):
__slots__ = ('tc_var', 'arg_var', )
tc_var: PlaceholderForType
arg_var: PlaceholderForType
def __init__(self, tc_var: PlaceholderForType, arg_var: PlaceholderForType, *, comment: str) -> None:
super().__init__(comment=comment)
self.tc_var = tc_var
self.arg_var = arg_var
def check(self) -> CheckResult:
if self.tc_var.resolve_as is None:
return RequireTypeSubstitutes()
tc_typ = self.tc_var.resolve_as
arg_typ = self.arg_var.resolve_as
if isinstance(tc_typ.application, TypeApplication_Nullary):
return Error(f'{tc_typ:s} must be a constructed type instead')
if isinstance(tc_typ.application, TypeApplication_TypeStar):
# Sure, it's a constructed type. But it's like a struct,
# though without the way to implement type classes
# Presumably, doing a naked `foo :: t a -> a`
# doesn't work since you don't have any info on t
# So we can let the MustImplementTypeClassConstraint handle it.
return None
if isinstance(tc_typ.application, TypeApplication_Type):
return [SameTypeConstraint(
tc_typ.application.arguments[0],
self.arg_var,
comment=self.comment,
)]
# FIXME: This feels sketchy. Shouldn't the type variable
# have the exact same number as arguments?
if isinstance(tc_typ.application, TypeApplication_TypeInt):
return [SameTypeConstraint(
tc_typ.application.arguments[0],
self.arg_var,
comment=self.comment,
)]
raise NotImplementedError(tc_typ, arg_typ)
def human_readable(self) -> HumanReadableRet:
return (
'{tc_var}` == {arg_var}',
{
'tc_var': self.tc_var if self.tc_var.resolve_as is None else self.tc_var,
'arg_var': self.arg_var if self.arg_var.resolve_as is None else self.arg_var,
},
)
class SameFunctionArgumentConstraint(ConstraintBase):
__slots__ = ('type3', 'func_arg', 'type_var_map', )
type3: PlaceholderForType
func_arg: FunctionArgument
type_var_map: dict[TypeVariable, PlaceholderForType]
def __init__(self, type3: PlaceholderForType, func_arg: FunctionArgument, type_var_map: dict[TypeVariable, PlaceholderForType], *, comment: str) -> None:
super().__init__(comment=comment)
self.type3 = type3
self.func_arg = func_arg
self.type_var_map = type_var_map
def check(self) -> CheckResult:
if self.type3.resolve_as is None:
return RequireTypeSubstitutes()
typ = self.type3.resolve_as
if isinstance(typ.application, TypeApplication_Nullary):
return Error(f'{typ:s} must be a function instead')
if not isinstance(typ.application, TypeApplication_TypeStar):
return Error(f'{typ:s} must be a function instead')
type_var_map = {
x: y.resolve_as
for x, y in self.type_var_map.items()
if y.resolve_as is not None
}
exp_type_arg_list = [
tv if isinstance(tv, Type3) else type_var_map[tv]
for tv in self.func_arg.args
if isinstance(tv, Type3) or tv in type_var_map
]
if len(exp_type_arg_list) != len(self.func_arg.args):
return RequireTypeSubstitutes()
return [
SameTypeConstraint(
typ,
prelude.function(*exp_type_arg_list),
comment=self.comment,
)
]
def human_readable(self) -> HumanReadableRet:
return (
'{type3} == {func_arg}',
{
'type3': self.type3,
'func_arg': self.func_arg.name,
},
)
class TupleMatchConstraint(ConstraintBase):
__slots__ = ('exp_type', 'args', )
exp_type: Type3OrPlaceholder
args: list[Type3OrPlaceholder]
def __init__(self, exp_type: Type3OrPlaceholder, args: Iterable[Type3OrPlaceholder], comment: str):
super().__init__(comment=comment)
self.exp_type = exp_type
self.args = list(args)
def _generate_dynamic_array(self, sa_args: tuple[Type3]) -> CheckResult:
sa_type, = sa_args
return [
SameTypeConstraint(arg, sa_type)
for arg in self.args
]
def _generate_static_array(self, sa_args: tuple[Type3, IntType3]) -> CheckResult:
sa_type, sa_len = sa_args
if sa_len.value != len(self.args):
return Error('Mismatch between applied types argument count', comment=self.comment)
return [
SameTypeConstraint(arg, sa_type)
for arg in self.args
]
def _generate_tuple(self, tp_args: tuple[Type3, ...]) -> CheckResult:
if len(tp_args) != len(self.args):
return Error('Mismatch between applied types argument count', comment=self.comment)
return [
SameTypeConstraint(arg, oth_arg)
for arg, oth_arg in zip(self.args, tp_args, strict=True)
]
GENERATE_ROUTER = TypeApplicationRouter['TupleMatchConstraint', CheckResult]()
GENERATE_ROUTER.add(prelude.dynamic_array, _generate_dynamic_array)
GENERATE_ROUTER.add(prelude.static_array, _generate_static_array)
GENERATE_ROUTER.add(prelude.tuple_, _generate_tuple)
def check(self) -> CheckResult:
exp_type = self.exp_type
if isinstance(exp_type, PlaceholderForType):
if exp_type.resolve_as is None:
return RequireTypeSubstitutes()
exp_type = exp_type.resolve_as
try:
return self.__class__.GENERATE_ROUTER(self, exp_type)
except NoRouteForTypeException:
raise NotImplementedError(exp_type)
class MustImplementTypeClassConstraint(ConstraintBase):
"""
A type must implement a given type class
"""
__slots__ = ('context', 'type_class3', 'types', )
context: Context
type_class3: Type3Class
types: list[Type3OrPlaceholder]
def __init__(self, context: Context, type_class3: Type3Class, typ_list: list[Type3OrPlaceholder], comment: Optional[str] = None) -> None:
super().__init__(comment=comment)
self.context = context
self.type_class3 = type_class3
self.types = typ_list
def check(self) -> CheckResult:
typ_list: list[Type3 | TypeConstructor_Base[Any] | TypeConstructor_Struct] = []
for typ in self.types:
if isinstance(typ, PlaceholderForType) and typ.resolve_as is not None:
typ = typ.resolve_as
if isinstance(typ, PlaceholderForType):
return RequireTypeSubstitutes()
if isinstance(typ.application, (TypeApplication_Nullary, TypeApplication_Struct, )):
typ_list.append(typ)
continue
if isinstance(typ.application, (TypeApplication_Type, TypeApplication_TypeInt, TypeApplication_TypeStar)):
typ_list.append(typ.application.constructor)
continue
raise NotImplementedError(typ, typ.application)
assert len(typ_list) == len(self.types)
key = (self.type_class3, tuple(typ_list), )
if key in self.context.type_class_instances_existing:
return None
typ_cls_name = self.type_class3 if isinstance(self.type_class3, str) else self.type_class3.name
typ_name_list = ' '.join(x.name for x in typ_list)
return Error(f'Missing type class instantation: {typ_cls_name} {typ_name_list}')
def human_readable(self) -> HumanReadableRet:
keys = {
f'type{idx}': typ
for idx, typ in enumerate(self.types)
}
return (
'Exists instance {type_class3} ' + ' '.join(f'{{{x}}}' for x in keys),
{
'type_class3': str(self.type_class3),
**keys,
},
)
def __repr__(self) -> str:
return f'MustImplementTypeClassConstraint({repr(self.type_class3)}, {repr(self.types)}, comment={repr(self.comment)})'
class LiteralFitsConstraint(ConstraintBase):
"""
A literal value fits a given type
"""
__slots__ = ('type3', 'literal', )
type3: Type3OrPlaceholder
literal: Union[ourlang.ConstantPrimitive, ourlang.ConstantBytes, ourlang.ConstantTuple, ourlang.ConstantStruct]
def __init__(
self,
type3: 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 _generate_dynamic_array(self, da_args: tuple[Type3]) -> CheckResult:
if not isinstance(self.literal, ourlang.ConstantTuple):
return Error('Must be tuple', comment=self.comment)
da_type, = da_args
res: list[ConstraintBase] = []
res.extend(
LiteralFitsConstraint(da_type, y)
for y in self.literal.value
)
# Generate placeholders so each Literal expression
# gets updated when we figure out the type of the
# expression the literal is used in
res.extend(
SameTypeConstraint(da_type, PlaceholderForType([y]))
for y in self.literal.value
)
return res
def _generate_static_array(self, sa_args: tuple[Type3, IntType3]) -> CheckResult:
if not isinstance(self.literal, ourlang.ConstantTuple):
return Error('Must be tuple', comment=self.comment)
sa_type, sa_len = sa_args
if sa_len.value != len(self.literal.value):
return Error('Member count mismatch', comment=self.comment)
res: list[ConstraintBase] = []
res.extend(
LiteralFitsConstraint(sa_type, y)
for y in self.literal.value
)
# Generate placeholders so each Literal expression
# gets updated when we figure out the type of the
# expression the literal is used in
res.extend(
SameTypeConstraint(sa_type, PlaceholderForType([y]))
for y in self.literal.value
)
return res
def _generate_struct(self, st_args: tuple[tuple[str, Type3], ...]) -> CheckResult:
if not isinstance(self.literal, ourlang.ConstantStruct):
return Error('Must be struct')
if len(st_args) != len(self.literal.value):
return Error('Struct element count mismatch')
res: list[ConstraintBase] = []
res.extend(
LiteralFitsConstraint(x, y)
for (_, x), y in zip(st_args, self.literal.value, strict=True)
)
# Generate placeholders so each Literal expression
# gets updated when we figure out the type of the
# expression the literal is used in
res.extend(
SameTypeConstraint(x_t, PlaceholderForType([y]), comment=f'{self.literal.struct_type3.name}.{x_n}')
for (x_n, x_t, ), y in zip(st_args, self.literal.value, strict=True)
)
res.append(SameTypeConstraint(
self.literal.struct_type3,
self.type3,
comment='Struct types must match',
))
return res
def _generate_tuple(self, tp_args: tuple[Type3, ...]) -> CheckResult:
if not isinstance(self.literal, ourlang.ConstantTuple):
return Error('Must be tuple', comment=self.comment)
if len(tp_args) != len(self.literal.value):
return Error('Tuple element count mismatch', comment=self.comment)
res: list[ConstraintBase] = []
res.extend(
LiteralFitsConstraint(x, y)
for x, y in zip(tp_args, self.literal.value, strict=True)
)
# Generate placeholders so each Literal expression
# gets updated when we figure out the type of the
# expression the literal is used in
res.extend(
SameTypeConstraint(x, PlaceholderForType([y]))
for x, y in zip(tp_args, self.literal.value, strict=True)
)
return res
GENERATE_ROUTER = TypeApplicationRouter['LiteralFitsConstraint', CheckResult]()
GENERATE_ROUTER.add(prelude.dynamic_array, _generate_dynamic_array)
GENERATE_ROUTER.add(prelude.static_array, _generate_static_array)
GENERATE_ROUTER.add(prelude.struct, _generate_struct)
GENERATE_ROUTER.add(prelude.tuple_, _generate_tuple)
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, 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)', comment=self.comment) # FIXME: Add line information
return None
return Error('Must be integer', comment=self.comment) # 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', comment=self.comment) # FIXME: Add line information
if self.type3 is prelude.bytes_:
if isinstance(self.literal.value, bytes):
return None
return Error('Must be bytes', comment=self.comment) # FIXME: Add line information
exp_type = self.type3
try:
return self.__class__.GENERATE_ROUTER(self, exp_type)
except NoRouteForTypeException:
raise NotImplementedError(exp_type)
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_type3', 'index_const', )
ret_type3: PlaceholderForType
type3: PlaceholderForType
index_type3: PlaceholderForType
index_const: int | None
def __init__(
self,
ret_type3: PlaceholderForType,
type3: PlaceholderForType,
index_type3: PlaceholderForType,
index_const: int | None,
comment: Optional[str] = None,
) -> None:
super().__init__(comment=comment)
self.ret_type3 = ret_type3
self.type3 = type3
self.index_type3 = index_type3
self.index_const = index_const
def _generate_bytes(self) -> CheckResult:
return [
SameTypeConstraint(prelude.u32, self.index_type3, comment='([]) :: bytes -> u32 -> u8'),
SameTypeConstraint(prelude.u8, self.ret_type3, comment='([]) :: bytes -> u32 -> u8'),
]
def _generate_static_array(self, sa_args: tuple[Type3, IntType3]) -> CheckResult:
sa_type, sa_len = sa_args
if self.index_const is not None and (self.index_const < 0 or sa_len.value <= self.index_const):
return Error('Tuple index out of range')
return [
SameTypeConstraint(prelude.u32, self.index_type3, comment='([]) :: Subscriptable a => a b -> u32 -> b'),
SameTypeConstraint(sa_type, self.ret_type3, comment='([]) :: Subscriptable a => a b -> u32 -> b'),
]
def _generate_tuple(self, tp_args: tuple[Type3, ...]) -> CheckResult:
# We special case tuples to allow for ease of use to the programmer
# e.g. rather than having to do `fst a` and `snd a` and only have to-sized tuples
# we use a[0] and a[1] and allow for a[2] and on.
if self.index_const is None:
return Error('Must index with integer literal')
if self.index_const < 0 or len(tp_args) <= self.index_const:
return Error('Tuple index out of range')
return [
SameTypeConstraint(prelude.u32, self.index_type3, comment='([]) :: Subscriptable a => a b -> u32 -> b'),
SameTypeConstraint(tp_args[self.index_const], self.ret_type3, comment=f'Tuple subscript index {self.index_const}'),
]
GENERATE_ROUTER = TypeApplicationRouter['CanBeSubscriptedConstraint', CheckResult]()
GENERATE_ROUTER.add_n(prelude.bytes_, _generate_bytes)
GENERATE_ROUTER.add(prelude.static_array, _generate_static_array)
GENERATE_ROUTER.add(prelude.tuple_, _generate_tuple)
def check(self) -> CheckResult:
if self.type3.resolve_as is None:
return RequireTypeSubstitutes()
exp_type = self.type3.resolve_as
try:
return self.__class__.GENERATE_ROUTER(self, exp_type)
except NoRouteForTypeException:
return Error(f'{exp_type.name} cannot be subscripted')
def human_readable(self) -> HumanReadableRet:
return (
'{type3}[{index}]',
{
'type3': self.type3,
'index': self.index_type3 if self.index_const is None else self.index_const,
},
)
def __repr__(self) -> str:
return f'CanBeSubscriptedConstraint({self.ret_type3!r}, {self.type3!r}, {self.index_type3!r}, {self.index_const!r}, comment={repr(self.comment)})'

View File

@ -1,331 +0,0 @@
"""
This module generates the typing constraints for Phasm.
The constraints solver can then try to resolve all constraints.
"""
from typing import Generator, List
from .. import ourlang, prelude
from .constraints import (
CanBeSubscriptedConstraint,
ConstraintBase,
Context,
LiteralFitsConstraint,
MustImplementTypeClassConstraint,
SameFunctionArgumentConstraint,
SameTypeArgumentConstraint,
SameTypeConstraint,
TupleMatchConstraint,
)
from .functions import (
Constraint_TypeClassInstanceExists,
FunctionArgument,
FunctionSignature,
TypeVariable,
TypeVariableApplication_Unary,
TypeVariableContext,
)
from .placeholders import PlaceholderForType
from .types import Type3, TypeApplication_Struct, TypeConstructor_Function
ConstraintGenerator = Generator[ConstraintBase, None, None]
def phasm_type3_generate_constraints(inp: ourlang.Module) -> List[ConstraintBase]:
ctx = Context()
ctx.type_class_instances_existing.update(prelude.PRELUDE_TYPE_CLASS_INSTANCES_EXISTING)
return [*module(ctx, inp)]
def constant(ctx: Context, inp: ourlang.Constant, phft: PlaceholderForType) -> ConstraintGenerator:
if isinstance(inp, (ourlang.ConstantPrimitive, ourlang.ConstantBytes, ourlang.ConstantTuple, ourlang.ConstantStruct)):
yield LiteralFitsConstraint(
phft, inp,
comment='The given literal must fit the expected type'
)
return
raise NotImplementedError(constant, inp)
def expression_binary_op(ctx: Context, inp: ourlang.BinaryOp, phft: PlaceholderForType) -> ConstraintGenerator:
return _expression_function_call(
ctx,
f'({inp.operator.name})',
inp.operator.signature,
[inp.left, inp.right],
inp,
phft,
)
def expression_function_call(ctx: Context, inp: ourlang.FunctionCall, phft: PlaceholderForType) -> ConstraintGenerator:
if isinstance(inp.function, ourlang.FunctionParam):
assert isinstance(inp.function.type3.application.constructor, TypeConstructor_Function)
signature = FunctionSignature(
TypeVariableContext(),
inp.function.type3.application.arguments,
)
else:
signature = inp.function.signature
return _expression_function_call(
ctx,
inp.function.name,
signature,
inp.arguments,
inp,
phft,
)
def expression_function_reference(ctx: Context, inp: ourlang.FunctionReference, phft: PlaceholderForType) -> ConstraintGenerator:
yield SameTypeConstraint(
prelude.function(*(x.type3 for x in inp.function.posonlyargs), inp.function.returns_type3),
phft,
comment=f'typeOf("{inp.function.name}") == typeOf({inp.function.name})',
)
def _expression_function_call(
ctx: Context,
func_name: str,
signature: FunctionSignature,
arguments: list[ourlang.Expression],
return_expr: ourlang.Expression,
return_phft: PlaceholderForType,
) -> ConstraintGenerator:
"""
Generates all type-level constraints for a function call.
A Binary operator functions pretty much the same as a function call
with two arguments - it's only a syntactic difference.
"""
# First create placeholders for all arguments, and generate their constraints
arg_placeholders = {
arg_expr: PlaceholderForType([arg_expr])
for arg_expr in arguments
}
arg_placeholders[return_expr] = return_phft
for call_arg in arguments:
yield from expression(ctx, call_arg, arg_placeholders[call_arg])
# Then generate placeholders the function signature
# and apply constraints that the function requires
# Skip any fully reference types
# Making this a map ensures that if a function signature has
# the same type on multiple arguments, we only get one
# placeholder here. These don't need to update anything once
# subsituted - that's done by arg_placeholders.
type_var_map = {
x: PlaceholderForType([])
for x in signature.args
if isinstance(x, TypeVariable)
}
for constraint in signature.context.constraints:
if isinstance(constraint, Constraint_TypeClassInstanceExists):
yield MustImplementTypeClassConstraint(
ctx,
constraint.type_class3,
[type_var_map[x] for x in constraint.types],
)
continue
raise NotImplementedError(constraint)
func_var_map = {
x: PlaceholderForType([])
for x in signature.args
if isinstance(x, FunctionArgument)
}
# If some of the function arguments are functions,
# we need to deal with those separately.
for sig_arg in signature.args:
if not isinstance(sig_arg, FunctionArgument):
continue
# Ensure that for all type variables in the function
# there are also type variables available
for func_arg in sig_arg.args:
if isinstance(func_arg, Type3):
continue
type_var_map.setdefault(func_arg, PlaceholderForType([]))
yield SameFunctionArgumentConstraint(
func_var_map[sig_arg],
sig_arg,
type_var_map,
comment=f'Ensure `{sig_arg.name}` matches in {signature}',
)
# If some of the function arguments are type constructors,
# we need to deal with those separately.
# That is, given `foo :: t a -> a` we need to ensure
# that both a's are the same.
for sig_arg in signature.args:
if isinstance(sig_arg, Type3):
# Not a type variable at all
continue
if isinstance(sig_arg, FunctionArgument):
continue
if sig_arg.application.constructor is None:
# Not a type variable for a type constructor
continue
if not isinstance(sig_arg.application, TypeVariableApplication_Unary):
raise NotImplementedError(sig_arg.application)
if sig_arg.application.arguments not in type_var_map:
# e.g., len :: t a -> u32
# i.e. "a" does not matter at all
continue
yield SameTypeArgumentConstraint(
type_var_map[sig_arg],
type_var_map[sig_arg.application.arguments],
comment=f'Ensure `{sig_arg.application.arguments.name}` matches in {signature}',
)
# Lastly, tie the signature and expression together
for arg_no, (sig_part, arg_expr) in enumerate(zip(signature.args, arguments + [return_expr], strict=True)):
if arg_no == len(arguments):
comment = f'The type of a function call to {func_name} is the same as the type that the function returns'
else:
comment = f'The type of the value passed to argument {arg_no} of function {func_name} should match the type of that argument'
if isinstance(sig_part, TypeVariable):
yield SameTypeConstraint(type_var_map[sig_part], arg_placeholders[arg_expr], comment=comment)
continue
if isinstance(sig_part, Type3):
yield SameTypeConstraint(sig_part, arg_placeholders[arg_expr], comment=comment)
continue
if isinstance(sig_part, FunctionArgument):
yield SameTypeConstraint(func_var_map[sig_part], arg_placeholders[arg_expr], comment=comment)
continue
raise NotImplementedError(sig_part)
return
def expression(ctx: Context, inp: ourlang.Expression, phft: PlaceholderForType) -> ConstraintGenerator:
if isinstance(inp, ourlang.Constant):
yield from constant(ctx, inp, phft)
return
if isinstance(inp, ourlang.VariableReference):
yield SameTypeConstraint(inp.variable.type3, phft,
comment=f'typeOf("{inp.variable.name}") == typeOf({inp.variable.name})')
return
if isinstance(inp, ourlang.BinaryOp):
yield from expression_binary_op(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):
r_type = []
for arg in inp.elements:
arg_phft = PlaceholderForType([arg])
yield from expression(ctx, arg, arg_phft)
r_type.append(arg_phft)
yield TupleMatchConstraint(
phft,
r_type,
comment='The type of a tuple is a combination of its members'
)
return
if isinstance(inp, ourlang.Subscript):
varref_phft = PlaceholderForType([inp.varref])
index_phft = PlaceholderForType([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(phft, varref_phft, index_phft, inp.index.value)
else:
yield CanBeSubscriptedConstraint(phft, varref_phft, index_phft, None)
return
if isinstance(inp, ourlang.AccessStructMember):
assert isinstance(inp.struct_type3.application, TypeApplication_Struct) # FIXME: See test_struct.py::test_struct_not_accessible
mem_typ = dict(inp.struct_type3.application.arguments)[inp.member]
yield from expression(ctx, inp.varref, PlaceholderForType([inp.varref])) # TODO
yield SameTypeConstraint(mem_typ, phft,
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
raise NotImplementedError(expression, inp)
def statement_return(ctx: Context, fun: ourlang.Function, inp: ourlang.StatementReturn) -> ConstraintGenerator:
phft = PlaceholderForType([inp.value])
yield from expression(ctx, inp.value, phft)
yield SameTypeConstraint(fun.returns_type3, phft,
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:
test_phft = PlaceholderForType([inp.test])
yield from expression(ctx, inp.test, test_phft)
yield SameTypeConstraint(test_phft, prelude.bool_,
comment='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(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(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:
phft = PlaceholderForType([inp.constant])
yield from constant(ctx, inp.constant, phft)
yield SameTypeConstraint(inp.type3, phft,
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)

View File

@ -1,159 +0,0 @@
"""
Entry point to the type3 system
"""
from typing import Dict, List
from .. import codestyle, ourlang
from .constraints import (
ConstraintBase,
Error,
RequireTypeSubstitutes,
SameTypeConstraint,
SubstitutionMap,
)
from .constraintsgenerator import phasm_type3_generate_constraints
from .placeholders import (
PlaceholderForType,
Type3OrPlaceholder,
)
from .types import Type3
MAX_RESTACK_COUNT = 100
class Type3Exception(BaseException):
"""
Thrown when the Type3 system detects constraints that do not hold
"""
def phasm_type3(inp: ourlang.Module, verbose: bool = False) -> None:
constraint_list = phasm_type3_generate_constraints(inp)
assert constraint_list
placeholder_substitutes: Dict[PlaceholderForType, Type3] = {}
placeholder_id_map: Dict[int, str] = {}
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)
back_on_todo_list_count = 0
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)
back_on_todo_list_count += 1
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 verbose and 0 < back_on_todo_list_count:
print(f'{back_on_todo_list_count} constraints skipped for now')
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 verbose:
print()
print_constraint_list(placeholder_id_map, constraint_list, placeholder_substitutes)
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:
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, Type3):
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]
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 ^ === ')

View File

@ -1,188 +0,0 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Hashable, Iterable, List
if TYPE_CHECKING:
from .typeclasses import Type3Class
from .types import Type3
class TypeVariableApplication_Base[T: Hashable, S: Hashable]:
"""
Records the constructor and arguments used to create this type.
Nullary types, or types of kind *, have both arguments set to None.
"""
constructor: T
arguments: S
def __init__(self, constructor: T, arguments: S) -> None:
self.constructor = constructor
self.arguments = arguments
def __hash__(self) -> int:
return hash((self.constructor, self.arguments, ))
def __eq__(self, other: Any) -> bool:
if not isinstance(other, TypeVariableApplication_Base):
raise NotImplementedError
return (self.constructor == other.constructor # type: ignore[no-any-return]
and self.arguments == other.arguments)
def __repr__(self) -> str:
return f'{self.__class__.__name__}({self.constructor!r}, {self.arguments!r})'
class TypeVariable:
"""
Types variable are used in function definition.
They are used in places where you don't know the exact type.
They are different from PlaceholderForType, as those are instanced
during type checking. These type variables are used solely in the
function's definition
"""
__slots__ = ('name', 'application', )
name: str
application: TypeVariableApplication_Base[Any, Any]
def __init__(self, name: str, application: TypeVariableApplication_Base[Any, Any]) -> None:
self.name = name
self.application = application
def __hash__(self) -> int:
return hash((self.name, self.application, ))
def __eq__(self, other: Any) -> bool:
if not isinstance(other, TypeVariable):
raise NotImplementedError
return (self.name == other.name
and self.application == other.application)
def __repr__(self) -> str:
return f'TypeVariable({repr(self.name)})'
class TypeVariableApplication_Nullary(TypeVariableApplication_Base[None, None]):
"""
For the type for this function argument it's not relevant if it was constructed.
"""
class TypeConstructorVariable:
"""
Types constructor variable are used in function definition.
They are a lot like TypeVariable, except that they represent a
type constructor rather than a type directly.
For now, we only have type constructor variables for kind
* -> *.
"""
__slots__ = ('name', )
name: str
def __init__(self, name: str) -> None:
self.name = name
def __hash__(self) -> int:
return hash((self.name, ))
def __eq__(self, other: Any) -> bool:
if other is None:
return False
if not isinstance(other, TypeConstructorVariable):
raise NotImplementedError
return (self.name == other.name)
def __call__(self, tvar: TypeVariable) -> 'TypeVariable':
return TypeVariable(
self.name + ' ' + tvar.name,
TypeVariableApplication_Unary(self, tvar)
)
def __repr__(self) -> str:
return f'TypeConstructorVariable({self.name!r})'
class TypeVariableApplication_Unary(TypeVariableApplication_Base[TypeConstructorVariable, TypeVariable]):
"""
The type for this function argument should be constructed from a type constructor.
And we need to know what construtor that was, since that's the one we support.
"""
class ConstraintBase:
__slots__ = ()
class Constraint_TypeClassInstanceExists(ConstraintBase):
__slots__ = ('type_class3', 'types', )
type_class3: 'Type3Class'
types: list[TypeVariable]
def __init__(self, type_class3: 'Type3Class', types: Iterable[TypeVariable]) -> None:
self.type_class3 = type_class3
self.types = list(types)
# Sanity check. AFAIK, if you have a multi-parameter type class,
# you can only add a constraint by supplying types for all variables
assert len(self.type_class3.args) == len(self.types)
def __str__(self) -> str:
return self.type_class3.name + ' ' + ' '.join(x.name for x in self.types)
def __repr__(self) -> str:
return f'Constraint_TypeClassInstanceExists({self.type_class3.name}, {self.types!r})'
class TypeVariableContext:
__slots__ = ('constraints', )
constraints: list[ConstraintBase]
def __init__(self, constraints: Iterable[ConstraintBase] = ()) -> None:
self.constraints = list(constraints)
def __copy__(self) -> 'TypeVariableContext':
return TypeVariableContext(self.constraints)
def __str__(self) -> str:
if not self.constraints:
return ''
return '(' + ', '.join(str(x) for x in self.constraints) + ') => '
def __repr__(self) -> str:
return f'TypeVariableContext({self.constraints!r})'
class FunctionArgument:
__slots__ = ('args', 'name', )
args: list[Type3 | TypeVariable]
name: str
def __init__(self, args: list[Type3 | TypeVariable]) -> None:
self.args = args
self.name = '(' + ' -> '.join(x.name for x in args) + ')'
class FunctionSignature:
__slots__ = ('context', 'args', )
context: TypeVariableContext
args: List[Type3 | TypeVariable | FunctionArgument]
def __init__(self, context: TypeVariableContext, args: Iterable[Type3 | TypeVariable | list[Type3 | TypeVariable]]) -> None:
self.context = context.__copy__()
self.args = list(
FunctionArgument(x) if isinstance(x, list) else x
for x in args
)
def __str__(self) -> str:
return str(self.context) + ' -> '.join(x.name for x in self.args)
def __repr__(self) -> str:
return f'FunctionSignature({self.context!r}, {self.args!r})'

View File

@ -1,66 +0,0 @@
"""
Contains the placeholder for types for use in Phasm.
These are temporary while the compiler is calculating all the types and validating them.
"""
from typing import Any, Iterable, List, Optional, Protocol, Union
from .types import Type3
class ExpressionProtocol(Protocol):
"""
A protocol for classes that should be updated on substitution
"""
type3: Type3 | None
"""
The type to update
"""
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]

View File

@ -1,142 +0,0 @@
from typing import Any, Callable
from .functions import (
TypeConstructorVariable,
TypeVariable,
TypeVariableApplication_Nullary,
TypeVariableApplication_Unary,
)
from .typeclasses import Type3ClassArgs
from .types import (
KindArgument,
Type3,
TypeApplication_Type,
TypeApplication_TypeInt,
TypeConstructor_Base,
)
class NoRouteForTypeException(Exception):
pass
class TypeApplicationRouter[S, R]:
"""
Helper class to find a method based on a constructed type
"""
__slots__ = ('by_constructor', 'by_type', )
by_constructor: dict[Any, Callable[[S, Any], R]]
"""
Contains all the added routing functions for constructed types
"""
by_type: dict[Type3, Callable[[S], R]]
"""
Contains all the added routing functions for constructed types
"""
def __init__(self) -> None:
self.by_constructor = {}
self.by_type = {}
def add_n(self, typ: Type3, helper: Callable[[S], R]) -> None:
"""
Lets you route to types that were not constructed
Also known types of kind *
"""
self.by_type[typ] = helper
def add[T](self, constructor: TypeConstructor_Base[T], helper: Callable[[S, T], R]) -> None:
self.by_constructor[constructor] = helper
def __call__(self, arg0: S, typ: Type3) -> R:
t_helper = self.by_type.get(typ)
if t_helper is not None:
return t_helper(arg0)
c_helper = self.by_constructor.get(typ.application.constructor)
if c_helper is not None:
return c_helper(arg0, typ.application.arguments)
raise NoRouteForTypeException(arg0, typ)
TypeVariableLookup = tuple[
dict[TypeVariable, KindArgument],
dict[TypeConstructorVariable, TypeConstructor_Base[Any]],
]
class TypeClassArgsRouter[S, R]:
"""
Helper class to find a method based on a type class argument list
"""
__slots__ = ('args', 'data', )
args: Type3ClassArgs
data: dict[tuple[Type3 | TypeConstructor_Base[Any], ...], Callable[[S, TypeVariableLookup], R]]
def __init__(self, args: Type3ClassArgs) -> None:
self.args = args
self.data = {}
def add(
self,
tv_map: dict[TypeVariable, Type3],
tc_map: dict[TypeConstructorVariable, TypeConstructor_Base[Any]],
helper: Callable[[S, TypeVariableLookup], R],
) -> None:
key: list[Type3 | TypeConstructor_Base[Any]] = []
for tc_arg in self.args:
if isinstance(tc_arg, TypeVariable):
key.append(tv_map[tc_arg])
else:
key.append(tc_map[tc_arg])
self.data[tuple(key)] = helper
def __call__(self, arg0: S, tv_map: dict[TypeVariable, Type3]) -> R:
key: list[Type3 | TypeConstructor_Base[Any]] = []
arguments: TypeVariableLookup = (dict(tv_map), {}, )
for tc_arg in self.args:
if isinstance(tc_arg, TypeVariable):
key.append(tv_map[tc_arg])
arguments[0][tc_arg] = tv_map[tc_arg]
continue
for tvar, typ in tv_map.items():
tvar_constructor = tvar.application.constructor
if tvar_constructor != tc_arg:
continue
key.append(typ.application.constructor)
arguments[1][tc_arg] = typ.application.constructor
if isinstance(tvar.application, TypeVariableApplication_Unary):
if isinstance(typ.application, TypeApplication_Type):
da_type, = typ.application.arguments
sa_type_tv = tvar.application.arguments
arguments[0][sa_type_tv] = da_type
continue
# FIXME: This feels sketchy. Shouldn't the type variable
# have the exact same number as arguments?
if isinstance(typ.application, TypeApplication_TypeInt):
sa_type, sa_len = typ.application.arguments
sa_type_tv = tvar.application.arguments
sa_len_tv = TypeVariable(sa_type_tv.name + '*', TypeVariableApplication_Nullary(None, None))
arguments[0][sa_type_tv] = sa_type
arguments[0][sa_len_tv] = sa_len
continue
raise NotImplementedError(tvar.application, typ.application)
t_helper = self.data.get(tuple(key))
if t_helper is not None:
return t_helper(arg0, arguments)
raise NoRouteForTypeException(arg0, tv_map)

View File

@ -1,104 +0,0 @@
from typing import Dict, Iterable, List, Mapping, Optional
from .functions import (
Constraint_TypeClassInstanceExists,
ConstraintBase,
FunctionSignature,
TypeConstructorVariable,
TypeVariable,
TypeVariableContext,
)
from .types import Type3
class Type3ClassMethod:
__slots__ = ('name', 'signature', )
name: str
signature: FunctionSignature
def __init__(self, name: str, signature: FunctionSignature) -> None:
self.name = name
self.signature = signature
def __str__(self) -> str:
return f'{self.name} :: {self.signature}'
def __repr__(self) -> str:
return f'Type3ClassMethod({repr(self.name)}, {repr(self.signature)})'
Type3ClassArgs = tuple[TypeVariable] | tuple[TypeVariable, TypeVariable] | tuple[TypeConstructorVariable]
class Type3Class:
__slots__ = ('name', 'args', 'methods', 'operators', 'inherited_classes', )
name: str
args: Type3ClassArgs
methods: Dict[str, Type3ClassMethod]
operators: Dict[str, Type3ClassMethod]
inherited_classes: List['Type3Class']
def __init__(
self,
name: str,
args: Type3ClassArgs,
methods: Mapping[str, Iterable[Type3 | TypeVariable | list[Type3 | TypeVariable]]],
operators: Mapping[str, Iterable[Type3 | TypeVariable | list[Type3 | TypeVariable]]],
inherited_classes: Optional[List['Type3Class']] = None,
additional_context: Optional[Mapping[str, Iterable[ConstraintBase]]] = None,
) -> None:
self.name = name
self.args = args
self.methods = {
k: Type3ClassMethod(k, _create_signature(v, self))
for k, v in methods.items()
}
self.operators = {
k: Type3ClassMethod(k, _create_signature(v, self))
for k, v in operators.items()
}
self.inherited_classes = inherited_classes or []
if additional_context:
for func_name, constraint_list in additional_context.items():
func = self.methods.get(func_name) or self.operators.get(func_name)
assert func is not None # type hint
func.signature.context.constraints.extend(constraint_list)
def __repr__(self) -> str:
return self.name
def _create_signature(
method_arg_list: Iterable[Type3 | TypeVariable | list[Type3 | TypeVariable]],
type_class3: Type3Class,
) -> FunctionSignature:
context = TypeVariableContext()
if not isinstance(type_class3.args[0], TypeConstructorVariable):
context.constraints.append(Constraint_TypeClassInstanceExists(type_class3, type_class3.args))
signature_args: list[Type3 | TypeVariable | list[Type3 | TypeVariable]] = []
for method_arg in method_arg_list:
if isinstance(method_arg, Type3):
signature_args.append(method_arg)
continue
if isinstance(method_arg, list):
signature_args.append(method_arg)
continue
if isinstance(method_arg, TypeVariable):
type_constructor = method_arg.application.constructor
if type_constructor is None:
signature_args.append(method_arg)
continue
if (type_constructor, ) == type_class3.args:
context.constraints.append(Constraint_TypeClassInstanceExists(type_class3, [method_arg]))
signature_args.append(method_arg)
continue
raise NotImplementedError(method_arg)
return FunctionSignature(context, signature_args)

View File

@ -1,290 +0,0 @@
"""
Contains the final types for use in Phasm, as well as construtors.
"""
from typing import (
Any,
Hashable,
Self,
Tuple,
TypeVar,
)
S = TypeVar('S')
T = TypeVar('T')
class KindArgument:
pass
class TypeApplication_Base[T: Hashable, S: Hashable]:
"""
Records the constructor and arguments used to create this type.
Nullary types, or types of kind *, have both arguments set to None.
"""
constructor: T
arguments: S
def __init__(self, constructor: T, arguments: S) -> None:
self.constructor = constructor
self.arguments = arguments
def __hash__(self) -> int:
return hash((self.constructor, self.arguments, ))
def __eq__(self, other: Any) -> bool:
if not isinstance(other, TypeApplication_Base):
raise NotImplementedError
return (self.constructor == other.constructor # type: ignore[no-any-return]
and self.arguments == other.arguments)
def __repr__(self) -> str:
return f'{self.__class__.__name__}({self.constructor!r}, {self.arguments!r})'
class Type3(KindArgument):
"""
Base class for the type3 types
(Having a separate name makes it easier to distinguish from
Python's Type)
"""
__slots__ = ('name', 'application', )
name: str
"""
The name of the string, as parsed and outputted by codestyle.
"""
application: TypeApplication_Base[Any, Any]
"""
How the type was constructed; i.e. which constructor was used and which
type level arguments were applied to the constructor.
"""
def __init__(self, name: str, application: TypeApplication_Base[Any, Any]) -> None:
self.name = name
self.application = application
def __repr__(self) -> str:
return f'Type3({self.name!r}, {self.application!r})'
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 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:
return hash(self.name)
def __bool__(self) -> bool:
raise NotImplementedError
class TypeApplication_Nullary(TypeApplication_Base[None, None]):
"""
There was no constructor used to create this type - it's a 'simple' type like u32
"""
class IntType3(KindArgument):
"""
Sometimes you can have an int on the type level, e.g. when using static arrays
This is not the same as an int on the language level.
[1.0, 1.2] :: f32[2] :: * -> Int -> *
That is to say, you can create a static array of size two with each element
a f32 using f32[2].
"""
__slots__ = ('value', )
value: int
def __init__(self, value: int) -> None:
self.value = value
def __repr__(self) -> str:
return f'IntType3({self.value!r})'
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.value)
def __eq__(self, other: Any) -> bool:
if isinstance(other, IntType3):
return self.value == other.value
if isinstance(other, KindArgument):
return False
raise NotImplementedError
def __hash__(self) -> int:
return hash(self.value)
class TypeConstructor_Base[T]:
"""
Base class for type construtors
"""
__slots__ = ('name', '_cache', )
name: str
"""
The name of the type constructor
"""
_cache: dict[T, Type3]
"""
When constructing a type with the same arguments,
it should produce the exact same result.
"""
def __init__(self, name: str) -> None:
self.name = name
self._cache = {}
def make_name(self, key: T) -> str:
"""
Renders the type's name based on the given arguments
"""
raise NotImplementedError('make_name', self)
def make_application(self, key: T) -> TypeApplication_Base[Self, T]:
"""
Records how the type was constructed into type.
The type checker and compiler will need to know what
arguments where made to construct the type.
"""
raise NotImplementedError('make_application', self)
def construct(self, key: T) -> Type3:
"""
Constructs the type by applying the given arguments to this
constructor.
"""
result = self._cache.get(key, None)
if result is None:
self._cache[key] = result = Type3(self.make_name(key), self.make_application(key))
return result
def __repr__(self) -> str:
return f'{self.__class__.__name__}({self.name!r}, ...)'
class TypeConstructor_Type(TypeConstructor_Base[Tuple[Type3]]):
"""
Base class type constructors of kind: * -> *
Notably, static array.
"""
__slots__ = ()
def make_application(self, key: Tuple[Type3]) -> 'TypeApplication_Type':
return TypeApplication_Type(self, key)
def make_name(self, key: Tuple[Type3]) -> str:
return f'{self.name} {key[0].name} '
def __call__(self, arg0: Type3) -> Type3:
return self.construct((arg0, ))
class TypeApplication_Type(TypeApplication_Base[TypeConstructor_Type, Tuple[Type3]]):
pass
class TypeConstructor_TypeInt(TypeConstructor_Base[Tuple[Type3, IntType3]]):
"""
Base class type constructors of kind: * -> Int -> *
Notably, static array.
"""
__slots__ = ()
def make_application(self, key: Tuple[Type3, IntType3]) -> 'TypeApplication_TypeInt':
return TypeApplication_TypeInt(self, key)
def make_name(self, key: Tuple[Type3, IntType3]) -> str:
return f'{self.name} {key[0].name} {key[1].value}'
def __call__(self, arg0: Type3, arg1: IntType3) -> Type3:
return self.construct((arg0, arg1))
class TypeApplication_TypeInt(TypeApplication_Base[TypeConstructor_TypeInt, Tuple[Type3, IntType3]]):
pass
class TypeConstructor_TypeStar(TypeConstructor_Base[Tuple[Type3, ...]]):
"""
Base class type constructors of variadic kind
Notably, tuple.
"""
def make_application(self, key: Tuple[Type3, ...]) -> 'TypeApplication_TypeStar':
return TypeApplication_TypeStar(self, key)
def __call__(self, *args: Type3) -> Type3:
key: Tuple[Type3, ...] = tuple(args)
return self.construct(key)
class TypeApplication_TypeStar(TypeApplication_Base[TypeConstructor_TypeStar, Tuple[Type3, ...]]):
pass
class TypeConstructor_DynamicArray(TypeConstructor_Type):
def make_name(self, key: Tuple[Type3]) -> str:
if 'u8' == key[0].name:
return 'bytes'
return f'{key[0].name}[...]'
class TypeConstructor_StaticArray(TypeConstructor_TypeInt):
def make_name(self, key: Tuple[Type3, IntType3]) -> str:
return f'{key[0].name}[{key[1].value}]'
class TypeConstructor_Tuple(TypeConstructor_TypeStar):
def make_name(self, key: Tuple[Type3, ...]) -> str:
return '(' + ', '.join(x.name for x in key) + ', )'
class TypeConstructor_Function(TypeConstructor_TypeStar):
def make_name(self, key: Tuple[Type3, ...]) -> str:
return 'Callable[' + ', '.join(x.name for x in key) + ']'
class TypeConstructor_Struct(TypeConstructor_Base[tuple[tuple[str, Type3], ...]]):
"""
Constructs struct types
"""
def make_application(self, key: tuple[tuple[str, Type3], ...]) -> 'TypeApplication_Struct':
return TypeApplication_Struct(self, key)
def make_name(self, key: tuple[tuple[str, Type3], ...]) -> str:
return f'{self.name}(' + ', '.join(
f'{n}: {t.name}'
for n, t in key
) + ')'
def construct(self, key: T) -> Type3:
"""
Constructs the type by applying the given arguments to this
constructor.
"""
raise Exception('This does not work with the caching system')
def __call__(self, name: str, args: tuple[tuple[str, Type3], ...]) -> Type3:
result = Type3(name, self.make_application(args))
return result
class TypeApplication_Struct(TypeApplication_Base[TypeConstructor_Struct, tuple[tuple[str, Type3], ...]]):
pass

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

@ -105,11 +105,17 @@ class Import(WatSerializable):
else f' (result {self.result.to_wat()})'
)
class Statement(WatSerializable):
class StatementBase(WatSerializable):
pass
class Statement(StatementBase ):
"""
Represents a Web Assembly statement
"""
def __init__(self, name: str, *args: str, comment: Optional[str] = None):
assert ' ' not in name, 'Please pass argument separately'
assert name != 'call', 'Please use StatementCall'
self.name = name
self.args = args
self.comment = comment
@ -120,6 +126,16 @@ class Statement(WatSerializable):
return f'{self.name} {args}{comment}'
class StatementCall(StatementBase ):
def __init__(self, func_name: str, comment: str | None = None):
self.func_name = func_name
self.comment = comment
def to_wat(self) -> str:
comment = f' ;; {self.comment}' if self.comment else ''
return f'call ${self.func_name} {comment}'
class Function(WatSerializable):
"""
Represents a Web Assembly function
@ -131,7 +147,7 @@ class Function(WatSerializable):
params: Iterable[Param],
locals_: Iterable[Param],
result: WasmType,
statements: Iterable[Statement],
statements: Iterable[StatementBase],
) -> None:
self.name = name
self.exported_name = exported_name

View File

@ -68,6 +68,11 @@ class Generator_i32i64:
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')
@ -81,6 +86,7 @@ class Generator_i32(Generator_i32i64):
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):
@ -90,6 +96,7 @@ class Generator_i64(Generator_i32i64):
# 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:
@ -143,6 +150,7 @@ class Generator_f32(Generator_f32f64):
# 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:
@ -150,6 +158,8 @@ class Generator_f64(Generator_f32f64):
# 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:
@ -186,16 +196,17 @@ class GeneratorBlock:
def __enter__(self) -> None:
stmt = self.name
args: list[str] = []
if self.params:
stmt = f'{stmt} ' + ' '.join(
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()
stmt = f'{stmt} (result {result})'
args.append(f'(result {result})')
self.generator.add_statement(stmt, comment=self.comment)
self.generator.add_statement(stmt, *args, comment=self.comment)
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
if not exc_type:
@ -203,7 +214,7 @@ class GeneratorBlock:
class Generator:
def __init__(self) -> None:
self.statements: List[wasm.Statement] = []
self.statements: List[wasm.StatementBase] = []
self.locals: Dict[str, VarType_Base] = {}
self.i32 = Generator_i32(self)
@ -228,7 +239,7 @@ class Generator:
# br_table
self.return_ = functools.partial(self.add_statement, 'return')
# call - see below
# call_indirect
# call_indirect - see below
def br_if(self, idx: int) -> None:
self.add_statement('br_if', f'{idx}')
@ -237,17 +248,19 @@ class Generator:
if isinstance(function, wasm.Function):
function = function.name
self.add_statement('call', f'${function}')
self.statements.append(wasm.StatementCall(function))
def call_indirect(self, params: Iterable[Type[wasm.WasmType]], result: Type[wasm.WasmType]) -> None:
def call_indirect(self, params: Iterable[Type[wasm.WasmType] | wasm.WasmType], result: Type[wasm.WasmType] | wasm.WasmType) -> None:
param_str = ' '.join(
x().to_wat()
(x() if isinstance(x, type) else x).to_wat()
for x in params
)
result_str = result().to_wat()
if isinstance(result, type):
result = result()
result_str = result.to_wat()
self.add_statement(f'call_indirect (param {param_str}) (result {result_str})')
self.add_statement('call_indirect', f'(param {param_str})', f'(result {result_str})')
def add_statement(self, name: str, *args: str, comment: Optional[str] = None) -> None:
self.statements.append(wasm.Statement(name, *args, comment=comment))
@ -287,7 +300,7 @@ class Generator:
def temp_var_u8(self, infix: str) -> VarType_u8:
return self.temp_var(VarType_u8(infix))
def func_wrapper(exported: bool = True) -> Callable[[Any], wasm.Function]:
def func_wrapper(exported: bool = False) -> Callable[[Any], wasm.Function]:
"""
This wrapper will execute the function and return
a wasm Function with the generated Statements

View File

@ -1,9 +1,9 @@
marko==2.1.3
mypy==1.15.0
mypy==1.17.1
pygments==2.19.1
pytest==8.3.5
pytest-integration==0.2.2
ruff==0.11.4
ruff==0.12.7
wasmtime==31.0.0

View File

@ -1,21 +1,18 @@
from __future__ import annotations
import os
import struct
import sys
from typing import Any, Callable, Generator, Iterable, List, TextIO, Union
from typing import Any, Callable, List, TextIO, Union
from phasm import compiler, prelude
from phasm.codestyle import phasm_render
from phasm.runtime import (
calculate_alloc_size,
calculate_alloc_size_static_array,
calculate_alloc_size_struct,
calculate_alloc_size_tuple,
from phasm.wasm import (
WasmTypeFloat32,
WasmTypeFloat64,
WasmTypeInt32,
WasmTypeInt64,
)
from phasm.stdlib.types import TYPE_INFO_CONSTRUCTED, TYPE_INFO_MAP
from phasm.type3 import types as type3types
from phasm.type3.routers import NoRouteForTypeException, TypeApplicationRouter
from . import runners
from . import memory, runners
DASHES = '-' * 16
@ -100,6 +97,7 @@ class Suite:
runner.parse(verbose=verbose)
runner.compile_ast()
runner.optimise_wasm_ast()
runner.compile_wat()
if verbose:
@ -109,11 +107,17 @@ class Suite:
runner.interpreter_setup()
runner.interpreter_load(imports)
allocator_generator = memory.Allocator(runner.phasm_ast.build, runner)
# Check if code formatting works
if do_format_check:
assert self.code_py == '\n' + phasm_render(runner.phasm_ast) # \n for formatting in tests
func_args = [x.type3 for x in runner.phasm_ast.functions[func_name].posonlyargs]
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}')
@ -124,26 +128,21 @@ class Suite:
runner.interpreter_dump_memory(sys.stderr)
for arg, arg_typ in zip(args, func_args, strict=True):
if arg_typ in (prelude.u8, prelude.u32, prelude.u64, ):
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 in (prelude.i8, prelude.i32, prelude.i64, ):
assert isinstance(arg, int)
wasm_args.append(arg)
continue
if arg_typ in (prelude.f32, prelude.f64, ):
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
try:
adr = ALLOCATE_MEMORY_STORED_ROUTER((runner, arg), arg_typ)
wasm_args.append(adr)
except NoRouteForTypeException:
raise NotImplementedError(arg_typ, arg)
allocator = allocator_generator(arg_typ)
adr = allocator(arg)
wasm_args.append(adr)
if verbose:
write_header(sys.stderr, 'Memory (pre run)')
@ -152,11 +151,8 @@ class Suite:
result = SuiteResult()
result.returned_value = runner.call(func_name, *wasm_args)
result.returned_value = _load_memory_stored_returned_value(
runner,
func_name,
result.returned_value,
)
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)')
@ -166,306 +162,3 @@ class Suite:
def write_header(textio: TextIO, msg: str) -> None:
textio.write(f'{DASHES} {msg.ljust(16)} {DASHES}\n')
WRITE_LOOKUP_MAP = {
'u8': compiler.module_data_u8,
'u32': compiler.module_data_u32,
'u64': compiler.module_data_u64,
'i8': compiler.module_data_i8,
'i32': compiler.module_data_i32,
'i64': compiler.module_data_i64,
'f32': compiler.module_data_f32,
'f64': compiler.module_data_f64,
}
def _write_memory_stored_value(
runner: runners.RunnerBase,
adr: int,
val_typ: type3types.Type3,
val: Any,
) -> int:
try:
adr2 = ALLOCATE_MEMORY_STORED_ROUTER((runner, val), val_typ)
runner.interpreter_write_memory(adr, compiler.module_data_u32(adr2))
return TYPE_INFO_CONSTRUCTED.alloc_size
except NoRouteForTypeException:
to_write = WRITE_LOOKUP_MAP[val_typ.name](val)
runner.interpreter_write_memory(adr, to_write)
return len(to_write)
def _allocate_memory_stored_bytes(attrs: tuple[runners.RunnerBase, bytes]) -> int:
runner, val = attrs
assert isinstance(val, bytes)
adr = runner.call('stdlib.types.__alloc_bytes__', len(val))
assert isinstance(adr, int)
sys.stderr.write(f'Allocation 0x{adr:08x} {repr(val)}\n')
runner.interpreter_write_memory(adr + 4, val)
return adr
def _allocate_memory_stored_dynamic_array(attrs: tuple[runners.RunnerBase, Any], da_args: tuple[type3types.Type3]) -> int:
runner, val = attrs
da_type, = da_args
if not isinstance(val, tuple):
raise InvalidArgumentException(f'Expected tuple; got {val!r} instead')
alloc_size = 4 + len(val) * calculate_alloc_size(da_type, True)
adr = runner.call('stdlib.alloc.__alloc__', alloc_size)
assert isinstance(adr, int) # Type int
sys.stderr.write(f'Allocation 0x{adr:08x} {repr(val)}\n')
offset = adr
offset += _write_memory_stored_value(runner, offset, prelude.u32, len(val))
for val_el_val in val:
offset += _write_memory_stored_value(runner, offset, da_type, val_el_val)
return adr
def _allocate_memory_stored_static_array(attrs: tuple[runners.RunnerBase, Any], sa_args: tuple[type3types.Type3, type3types.IntType3]) -> int:
runner, val = attrs
sa_type, sa_len = sa_args
if not isinstance(val, tuple):
raise InvalidArgumentException(f'Expected tuple of length {sa_len.value}; got {val!r} instead')
if sa_len.value != len(val):
raise InvalidArgumentException(f'Expected tuple of length {sa_len.value}; got {val!r} instead')
alloc_size = calculate_alloc_size_static_array(False, sa_args)
adr = runner.call('stdlib.alloc.__alloc__', alloc_size)
assert isinstance(adr, int) # Type int
sys.stderr.write(f'Allocation 0x{adr:08x} {repr(val)}\n')
offset = adr
for val_el_val in val:
offset += _write_memory_stored_value(runner, offset, sa_type, val_el_val)
return adr
def _allocate_memory_stored_struct(attrs: tuple[runners.RunnerBase, Any], st_args: tuple[tuple[str, type3types.Type3], ...]) -> int:
runner, val = attrs
assert isinstance(val, dict)
alloc_size = calculate_alloc_size_struct(False, st_args)
adr = runner.call('stdlib.alloc.__alloc__', alloc_size)
assert isinstance(adr, int)
sys.stderr.write(f'Allocation 0x{adr:08x} {repr(val)}\n')
offset = adr
for val_el_name, val_el_typ in st_args:
assert val_el_name in val, f'Missing key value {val_el_name}'
val_el_val = val.pop(val_el_name)
offset += _write_memory_stored_value(runner, offset, val_el_typ, val_el_val)
assert not val, f'Additional values: {list(val)!r}'
return adr
def _allocate_memory_stored_tuple(attrs: tuple[runners.RunnerBase, Any], tp_args: tuple[type3types.Type3, ...]) -> int:
runner, val = attrs
assert isinstance(val, tuple)
alloc_size = calculate_alloc_size_tuple(False, tp_args)
adr = runner.call('stdlib.alloc.__alloc__', alloc_size)
assert isinstance(adr, int)
sys.stderr.write(f'Allocation 0x{adr:08x} {repr(val)}\n')
assert len(val) == len(tp_args)
offset = adr
for val_el_val, val_el_typ in zip(val, tp_args, strict=True):
offset += _write_memory_stored_value(runner, offset, val_el_typ, val_el_val)
return adr
ALLOCATE_MEMORY_STORED_ROUTER = TypeApplicationRouter[tuple[runners.RunnerBase, Any], Any]()
ALLOCATE_MEMORY_STORED_ROUTER.add_n(prelude.bytes_, _allocate_memory_stored_bytes)
ALLOCATE_MEMORY_STORED_ROUTER.add(prelude.dynamic_array, _allocate_memory_stored_dynamic_array)
ALLOCATE_MEMORY_STORED_ROUTER.add(prelude.static_array, _allocate_memory_stored_static_array)
ALLOCATE_MEMORY_STORED_ROUTER.add(prelude.struct, _allocate_memory_stored_struct)
ALLOCATE_MEMORY_STORED_ROUTER.add(prelude.tuple_, _allocate_memory_stored_tuple)
def _load_memory_stored_returned_value(
runner: runners.RunnerBase,
func_name: str,
wasm_value: Any,
) -> Any:
ret_type3 = runner.phasm_ast.functions[func_name].returns_type3
if ret_type3 is prelude.none:
return None
if ret_type3 is prelude.bool_:
assert isinstance(wasm_value, int), wasm_value
return 0 != wasm_value
if ret_type3 in (prelude.i8, prelude.i32, prelude.i64):
assert isinstance(wasm_value, int), wasm_value
if ret_type3 is prelude.i8:
# Values are actually i32
# Have to reinterpret to load proper value
data = struct.pack('<i', wasm_value)
wasm_value, = struct.unpack('<bxxx', data)
return wasm_value
if ret_type3 in (prelude.u8, prelude.u32, prelude.u64):
assert isinstance(wasm_value, int), wasm_value
if wasm_value < 0:
# WASM does not support unsigned values through its interface
# Cast and then reinterpret
letter = {
'u32': 'i',
'u64': 'q',
}[ret_type3.name]
data = struct.pack(f'<{letter}', wasm_value)
wasm_value, = struct.unpack(f'<{letter.upper()}', data)
return wasm_value
if ret_type3 in (prelude.f32, prelude.f64, ):
assert isinstance(wasm_value, float), wasm_value
return wasm_value
assert isinstance(wasm_value, int), wasm_value
return LOAD_FROM_ADDRESS_ROUTER((runner, wasm_value), ret_type3)
def _unpack(runner: runners.RunnerBase, typ: type3types.Type3, inp: bytes) -> Any:
typ_info = TYPE_INFO_MAP.get(typ.name, TYPE_INFO_CONSTRUCTED)
assert len(inp) == typ_info.alloc_size
if typ is prelude.u8:
return struct.unpack('<B', inp)[0]
if typ is prelude.u32:
return struct.unpack('<I', inp)[0]
if typ is prelude.u64:
return struct.unpack('<Q', inp)[0]
if typ is prelude.i8:
return struct.unpack('<b', inp)[0]
if typ is prelude.i32:
return struct.unpack('<i', inp)[0]
if typ is prelude.i64:
return struct.unpack('<q', inp)[0]
if typ is prelude.f32:
return struct.unpack('<f', inp)[0]
if typ is prelude.f64:
return struct.unpack('<d', inp)[0]
if typ_info is TYPE_INFO_CONSTRUCTED:
# Note: For applied types, inp should contain a 4 byte pointer
adr = struct.unpack('<I', inp)[0]
return LOAD_FROM_ADDRESS_ROUTER((runner, adr), typ)
raise NotImplementedError(typ, inp)
def _load_bytes_from_address(attrs: tuple[runners.RunnerBase, int]) -> bytes:
runner, adr = attrs
sys.stderr.write(f'Reading 0x{adr:08x} bytes\n')
read_bytes = runner.interpreter_read_memory(adr, 4)
bytes_len, = struct.unpack('<I', read_bytes)
adr += 4
return runner.interpreter_read_memory(adr, bytes_len)
def _split_read_bytes(all_bytes: bytes, split_sizes: Iterable[int]) -> Generator[bytes, None, None]:
offset = 0
for size in split_sizes:
yield all_bytes[offset:offset + size]
offset += size
def _load_dynamic_array_from_address(attrs: tuple[runners.RunnerBase, int], da_args: tuple[type3types.Type3]) -> Any:
runner, adr = attrs
da_type, = da_args
sys.stderr.write(f'Reading 0x{adr:08x} {da_type:s}[...]\n')
read_bytes = runner.interpreter_read_memory(adr, 4)
array_len, = struct.unpack('<I', read_bytes)
adr += 4
arg_size_1 = calculate_alloc_size(da_type, is_member=True)
arg_sizes = [arg_size_1 for _ in range(array_len)] # _split_read_bytes requires one arg per value
read_bytes = runner.interpreter_read_memory(adr, sum(arg_sizes))
return tuple(
_unpack(runner, da_type, arg_bytes)
for arg_bytes in _split_read_bytes(read_bytes, arg_sizes)
)
def _load_static_array_from_address(attrs: tuple[runners.RunnerBase, int], sa_args: tuple[type3types.Type3, type3types.IntType3]) -> Any:
runner, adr = attrs
sub_typ, len_typ = sa_args
sys.stderr.write(f'Reading 0x{adr:08x} {sub_typ:s} {len_typ:s}\n')
sa_len = len_typ.value
arg_size_1 = calculate_alloc_size(sub_typ, is_member=True)
arg_sizes = [arg_size_1 for _ in range(sa_len)] # _split_read_bytes requires one arg per value
read_bytes = runner.interpreter_read_memory(adr, sum(arg_sizes))
return tuple(
_unpack(runner, sub_typ, arg_bytes)
for arg_bytes in _split_read_bytes(read_bytes, arg_sizes)
)
def _load_struct_from_address(attrs: tuple[runners.RunnerBase, int], st_args: tuple[tuple[str, type3types.Type3], ...]) -> dict[str, Any]:
runner, adr = attrs
sys.stderr.write(f'Reading 0x{adr:08x} struct {list(st_args)}\n')
arg_sizes = [
calculate_alloc_size(x, is_member=True)
for _, x in st_args
]
read_bytes = runner.interpreter_read_memory(adr, sum(arg_sizes))
return {
arg_name: _unpack(runner, arg_typ, arg_bytes)
for (arg_name, arg_typ, ), arg_bytes in zip(st_args, _split_read_bytes(read_bytes, arg_sizes), strict=True)
}
def _load_tuple_from_address(attrs: tuple[runners.RunnerBase, int], tp_args: tuple[type3types.Type3, ...]) -> Any:
runner, adr = attrs
sys.stderr.write(f'Reading 0x{adr:08x} tuple {len(tp_args)}\n')
arg_sizes = [
calculate_alloc_size(x, is_member=True)
for x in tp_args
]
read_bytes = runner.interpreter_read_memory(adr, sum(arg_sizes))
return tuple(
_unpack(runner, arg_typ, arg_bytes)
for arg_typ, arg_bytes in zip(tp_args, _split_read_bytes(read_bytes, arg_sizes), strict=True)
)
LOAD_FROM_ADDRESS_ROUTER = TypeApplicationRouter[tuple[runners.RunnerBase, int], Any]()
LOAD_FROM_ADDRESS_ROUTER.add_n(prelude.bytes_, _load_bytes_from_address)
LOAD_FROM_ADDRESS_ROUTER.add(prelude.dynamic_array, _load_dynamic_array_from_address)
LOAD_FROM_ADDRESS_ROUTER.add(prelude.static_array, _load_static_array_from_address)
LOAD_FROM_ADDRESS_ROUTER.add(prelude.struct, _load_struct_from_address)
LOAD_FROM_ADDRESS_ROUTER.add(prelude.tuple_, _load_tuple_from_address)

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

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

View File

@ -8,8 +8,10 @@ import wasmtime
from phasm import ourlang, wasm
from phasm.compiler import phasm_compile
from phasm.optimise.removeunusedfuncs import removeunusedfuncs
from phasm.parser import phasm_parse
from phasm.type3.entry import phasm_type3
from phasm.type5.solver import phasm_type5
from phasm.wasmgenerator import Generator as WasmGenerator
Imports = Optional[Dict[str, Callable[[Any], Any]]]
@ -18,7 +20,7 @@ class RunnerBase:
Base class
"""
phasm_code: str
phasm_ast: ourlang.Module
phasm_ast: ourlang.Module[WasmGenerator]
wasm_ast: wasm.Module
wasm_asm: str
wasm_bin: bytes
@ -37,7 +39,7 @@ class RunnerBase:
Parses the Phasm code into an AST
"""
self.phasm_ast = phasm_parse(self.phasm_code)
phasm_type3(self.phasm_ast, verbose=verbose)
phasm_type5(self.phasm_ast, verbose=verbose)
def compile_ast(self) -> None:
"""
@ -45,6 +47,12 @@ class RunnerBase:
"""
self.wasm_ast = phasm_compile(self.phasm_ast)
def optimise_wasm_ast(self) -> None:
"""
Optimises the WebAssembly AST
"""
removeunusedfuncs(self.wasm_ast)
def compile_wat(self) -> None:
"""
Compiles the WebAssembly AST into WebAssembly Assembly code

View File

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

@ -8,6 +8,6 @@ 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()
result = Suite(code_py).run_code(40, func_name='fib')
assert 102334155 == result.returned_value

View File

@ -61,15 +61,15 @@ 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',
'The given literal must fit the expected type',
)
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(
'Must be tuple',
'The given literal must fit the expected type',
)
expect_type_error('Not the same type')
```
# function_result_is_literal_ok
@ -114,20 +114,17 @@ def testEntry() -> i32:
```py
if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_') or TYPE_NAME.startswith('dynamic_array_'):
expect_type_error(
'Mismatch between applied types argument count',
'The type of a tuple is a combination of its members',
)
expect_type_error('Tuple element count mismatch')
elif TYPE_NAME.startswith('struct_'):
expect_type_error(
TYPE + ' must be (u32, ) instead',
'The type of the value returned from function constant should match its return type',
)
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(
'Must be tuple',
'The given literal must fit the expected type',
)
expect_type_error('Not the same type')
```
# function_result_is_module_constant_ok
@ -175,16 +172,7 @@ def testEntry() -> i32:
```
```py
if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_') or TYPE_NAME.startswith('dynamic_array_') or TYPE_NAME.startswith('struct_'):
expect_type_error(
TYPE + ' must be (u32, ) instead',
'The type of the value returned from function constant should match its return type',
)
else:
expect_type_error(
TYPE_NAME + ' must be (u32, ) instead',
'The type of the value returned from function constant should match its return type',
)
expect_type_error('Not the same type')
```
# function_result_is_arg_ok
@ -226,16 +214,7 @@ def select(x: $TYPE) -> (u32, ):
```
```py
if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_') or TYPE_NAME.startswith('dynamic_array_') or TYPE_NAME.startswith('struct_'):
expect_type_error(
TYPE + ' must be (u32, ) instead',
'The type of the value returned from function select should match its return type',
)
else:
expect_type_error(
TYPE_NAME + ' must be (u32, ) instead',
'The type of the value returned from function select should match its return type',
)
expect_type_error('Not the same type')
```
# function_arg_literal_ok
@ -274,21 +253,17 @@ def testEntry() -> i32:
```py
if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_') or TYPE_NAME.startswith('dynamic_array_'):
expect_type_error(
'Mismatch between applied types argument count',
# FIXME: Shouldn't this be the same as for the else statement?
'The type of a tuple is a combination of its members',
)
expect_type_error('Tuple element count mismatch')
elif TYPE_NAME.startswith('struct_'):
expect_type_error(
TYPE + ' must be (u32, ) instead',
'The type of the value passed to argument 0 of function helper should match the type of that argument',
)
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(
'Must be tuple',
'The given literal must fit the expected type',
)
expect_type_error('Not the same type')
```
# function_arg_module_constant_def_ok
@ -330,14 +305,5 @@ def testEntry() -> i32:
```
```py
if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_') or TYPE_NAME.startswith('dynamic_array_') or TYPE_NAME.startswith('struct_'):
expect_type_error(
TYPE + ' must be (u32, ) instead',
'The type of the value passed to argument 0 of function helper should match the type of that argument',
)
else:
expect_type_error(
TYPE_NAME + ' must be (u32, ) instead',
'The type of the value passed to argument 0 of function helper should match the type of that argument',
)
expect_type_error('Not the same type')
```

View File

@ -41,11 +41,9 @@ def generate_assertion_expect(result, arg, given=None):
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, error_comment = None):
result.append('with pytest.raises(Type3Exception) as exc_info:')
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()')
result.append(f'assert {repr(error_msg)} == exc_info.value.args[0][0].msg')
result.append(f'assert {repr(error_comment)} == exc_info.value.args[0][0].comment')
def json_does_not_support_byte_or_tuple_values_fix(inp: Any):
if isinstance(inp, (int, float, )):
@ -98,7 +96,7 @@ def generate_code(markdown, template, settings):
print('"""')
print('import pytest')
print()
print('from phasm.type3.entry import Type3Exception')
print('from phasm.type5.solver import Type5SolverException')
print()
print('from ..helpers import Suite')
print()

View File

@ -1,14 +1,17 @@
{
"TYPE_NAME": "struct_all_primitives",
"TYPE": "StructallPrimitives",
"VAL0": "StructallPrimitives(1, 4, 8, 1, -1, 4, -4, 8, -8, 125.125, -125.125, 5000.5, -5000.5, b'Hello, world!')",
"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",
@ -22,10 +25,13 @@
"PYTHON": {
"VAL0": {
"val00": 1,
"val03": 2,
"val01": 4,
"val02": 8,
"val10": 1,
"val11": -1,
"val16": 2,
"val17": -2,
"val12": 4,
"val13": -4,
"val14": 8,

View File

@ -15,7 +15,8 @@ def testEntry() -> bytes:
result = Suite(code_py).run_code()
assert b"Hello" == result.returned_value
assert b"Hello" == result.returned_value[0:5]
assert 5 == len(result.returned_value)
@pytest.mark.integration_test
def test_bytes_export_instantiation():

View File

@ -1,8 +1,25 @@
import pytest
from phasm.type5.solver import Type5SolverException
from ..helpers import Suite
@pytest.mark.integration_test
def test_call_nullary():
code_py = """
def helper() -> i32:
return 3
@exported
def testEntry() -> i32:
return helper()
"""
result = Suite(code_py).run_code()
assert 3 == result.returned_value
@pytest.mark.integration_test
def test_call_pre_defined():
code_py = """
@ -23,12 +40,22 @@ def test_call_post_defined():
code_py = """
@exported
def testEntry() -> i32:
return helper(10, 3)
return helper(13)
def helper(left: i32, right: i32) -> i32:
return left - right
def helper(left: i32) -> i32:
return left
"""
result = Suite(code_py).run_code()
assert 7 == result.returned_value
assert 13 == result.returned_value
@pytest.mark.integration_test
def test_call_invalid_type():
code_py = """
def helper(left: i32) -> i32:
return left()
"""
with pytest.raises(Type5SolverException, match=r'i32 ~ Callable\[i32\]'):
Suite(code_py).run_code()

View File

@ -1,5 +1,7 @@
import pytest
from phasm.type5.solver import Type5SolverException
from ..helpers import Suite
@ -70,3 +72,30 @@ def testEntry(a: i32, b: i32) -> i32:
assert 2 == suite.run_code(20, 10).returned_value
assert 1 == suite.run_code(10, 20).returned_value
assert 0 == suite.run_code(10, 10).returned_value
@pytest.mark.integration_test
def test_if_type_invalid():
code_py = """
@exported
def testEntry(a: i32) -> i32:
if a:
return 1
return 0
"""
with pytest.raises(Type5SolverException, match='i32 ~ bool'):
Suite(code_py).run_code(1)
@pytest.mark.integration_test
def test_if_type_ok():
code_py = """
@exported
def testEntry(a: bool) -> i32:
if a:
return 1
return 0
"""
assert 1 == Suite(code_py).run_code(1).returned_value

View File

@ -1,6 +1,6 @@
import pytest
from phasm.type3.entry import Type3Exception
from phasm.type5.solver import Type5SolverException
from ..helpers import Suite
@ -69,7 +69,7 @@ def testEntry(x: u32) -> u8:
def helper(mul: int) -> int:
return 4238 * mul
with pytest.raises(Type3Exception, match=r'u32 must be u8 instead'):
with pytest.raises(Type5SolverException, match='Not the same type'):
Suite(code_py).run_code(
imports={
'helper': helper,

View File

@ -1,6 +1,6 @@
import pytest
from phasm.type3.entry import Type3Exception
from phasm.type5.solver import Type5SolverException
from ..helpers import Suite
@ -15,7 +15,7 @@ def testEntry() -> u8:
return CONSTANT
"""
with pytest.raises(Type3Exception, match=r'Must fit in 1 byte\(s\)'):
with pytest.raises(Type5SolverException, match=r'Must fit in 1 byte\(s\)'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@ -26,5 +26,5 @@ def testEntry() -> u8:
return 1000
"""
with pytest.raises(Type3Exception, match=r'Must fit in 1 byte\(s\)'):
with pytest.raises(Type5SolverException, match=r'Must fit in 1 byte\(s\)'):
Suite(code_py).run_code()

View File

@ -1,6 +1,6 @@
import pytest
from phasm.type3.entry import Type3Exception
from phasm.type5.solver import Type5SolverException
from ..helpers import Suite
@ -78,11 +78,28 @@ def testEntry() -> i32:
assert 42 == result.returned_value
@pytest.mark.integration_test
def test_sof_wrong_argument_type():
def test_sof_function_with_wrong_argument_type_use():
code_py = """
def double(left: f32) -> f32:
def double(left: i32) -> i32:
return left * 2
def action(applicable: Callable[i32, i32], left: f32) -> i32:
return applicable(left)
@exported
def testEntry() -> i32:
return action(double, 13.0)
"""
with pytest.raises(Type5SolverException, match='i32 ~ f32'):
Suite(code_py).run_code()
@pytest.mark.integration_test
def test_sof_function_with_wrong_argument_type_pass():
code_py = """
def double(left: f32) -> i32:
return truncate(left) * 2
def action(applicable: Callable[i32, i32], left: i32) -> i32:
return applicable(left)
@ -91,11 +108,11 @@ def testEntry() -> i32:
return action(double, 13)
"""
with pytest.raises(Type3Exception, match=r'Callable\[f32, f32\] must be Callable\[i32, i32\] instead'):
with pytest.raises(Type5SolverException, match='i32 ~ f32'):
Suite(code_py).run_code()
@pytest.mark.integration_test
def test_sof_wrong_return():
def test_sof_function_with_wrong_return_type_use():
code_py = """
def double(left: i32) -> i32:
return left * 2
@ -103,17 +120,33 @@ def double(left: i32) -> i32:
def action(applicable: Callable[i32, i32], left: i32) -> f32:
return applicable(left)
@exported
def testEntry() -> f32:
return action(double, 13)
"""
with pytest.raises(Type5SolverException, match='i32 ~ f32'):
Suite(code_py).run_code()
@pytest.mark.integration_test
def test_sof_function_with_wrong_return_type_pass():
code_py = """
def double(left: i32) -> f32:
return convert(left) * 2.0
def action(applicable: Callable[i32, i32], left: i32) -> i32:
return applicable(left)
@exported
def testEntry() -> i32:
return action(double, 13)
"""
with pytest.raises(Type3Exception, match=r'f32 must be i32 instead'):
with pytest.raises(Type5SolverException, match='i32 ~ f32'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@pytest.mark.skip('FIXME: Probably have the remainder be the a function type')
def test_sof_wrong_not_enough_args_call():
def test_sof_not_enough_args_use():
code_py = """
def add(left: i32, right: i32) -> i32:
return left + right
@ -126,11 +159,11 @@ def testEntry() -> i32:
return action(add, 13)
"""
with pytest.raises(Type3Exception, match=r'f32 must be i32 instead'):
with pytest.raises(Type5SolverException, match='Not the same type'):
Suite(code_py).run_code()
@pytest.mark.integration_test
def test_sof_wrong_not_enough_args_refere():
def test_sof_not_enough_args_pass():
code_py = """
def double(left: i32) -> i32:
return left * 2
@ -143,12 +176,12 @@ def testEntry() -> i32:
return action(double, 13, 14)
"""
with pytest.raises(Type3Exception, match=r'Callable\[i32, i32\] must be Callable\[i32, i32, i32\] instead'):
match = r'Callable\[i32, i32\] ~ i32'
with pytest.raises(Type5SolverException, match=match):
Suite(code_py).run_code()
@pytest.mark.integration_test
@pytest.mark.skip('FIXME: Probably have the remainder be the a function type')
def test_sof_wrong_too_many_args_call():
def test_sof_too_many_args_use_0():
code_py = """
def thirteen() -> i32:
return 13
@ -161,22 +194,60 @@ def testEntry() -> i32:
return action(thirteen, 13)
"""
with pytest.raises(Type3Exception, match=r'f32 must be i32 instead'):
Suite(code_py).run_code()
match = r'\(\) ~ i32'
with pytest.raises(Type5SolverException, match=match):
Suite(code_py).run_code(verbose=True)
@pytest.mark.integration_test
def test_sof_wrong_too_many_args_refere():
def test_sof_too_many_args_use_1():
code_py = """
def thirteen(x: i32) -> i32:
return x
def action(applicable: Callable[i32, i32], left: i32, right: i32) -> i32:
return applicable(left, right)
@exported
def testEntry() -> i32:
return action(thirteen, 13, 26)
"""
match = r'i32 ~ Callable\[i32, i32\]'
with pytest.raises(Type5SolverException, match=match):
Suite(code_py).run_code(verbose=True)
@pytest.mark.integration_test
def test_sof_too_many_args_pass_0():
code_py = """
def double(left: i32) -> i32:
return left * 2
def action(applicable: Callable[i32]) -> i32:
def action(applicable: Callable[i32], left: i32, right: i32) -> i32:
return applicable()
@exported
def testEntry() -> i32:
return action(double)
return action(double, 13, 14)
"""
with pytest.raises(Type3Exception, match=r'Callable\[i32, i32\] must be Callable\[i32\] instead'):
match = r'\(\) ~ i32'
with pytest.raises(Type5SolverException, match=match):
Suite(code_py).run_code()
@pytest.mark.integration_test
def test_sof_too_many_args_pass_1():
code_py = """
def double(left: i32, right: i32) -> i32:
return left * right
def action(applicable: Callable[i32, i32], left: i32, right: i32) -> i32:
return applicable(left)
@exported
def testEntry() -> i32:
return action(double, 13, 14)
"""
match = r'i32 ~ Callable\[i32, i32\]'
with pytest.raises(Type5SolverException, match=match):
Suite(code_py).run_code()

View File

@ -1,6 +1,6 @@
import pytest
from phasm.type3.entry import Type3Exception
from phasm.type5.solver import Type5SolverException
from ..helpers import Suite
@ -15,7 +15,7 @@ def testEntry() -> i32:
return 0
"""
with pytest.raises(Type3Exception, match='Member count mismatch'):
with pytest.raises(Type5SolverException, match='Tuple element count mismatch'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@ -28,7 +28,7 @@ def testEntry() -> i32:
return 0
"""
with pytest.raises(Type3Exception, match='Member count mismatch'):
with pytest.raises(Type5SolverException, match='Tuple element count mismatch'):
Suite(code_py).run_code()
@pytest.mark.integration_test

View File

@ -1,7 +1,7 @@
import pytest
from phasm.exceptions import StaticError
from phasm.type3.entry import Type3Exception
from phasm.type5.solver import Type5SolverException
from ..helpers import Suite
@ -76,7 +76,7 @@ class CheckedValueRed:
CONST: CheckedValueBlue = CheckedValueRed(1)
"""
with pytest.raises(Type3Exception, match='CheckedValueBlue must be CheckedValueRed instead'):
with pytest.raises(Type5SolverException, match='Not the same type'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@ -91,7 +91,20 @@ class CheckedValueRed:
CONST: (CheckedValueBlue, u32, ) = (CheckedValueRed(1), 16, )
"""
with pytest.raises(Type3Exception, match='CheckedValueBlue must be CheckedValueRed instead'):
with pytest.raises(Type5SolverException, match='Not the same type'):
Suite(code_py).run_code()
@pytest.mark.integration_test
def test_type_mismatch_struct_call_arg_count():
code_py = """
class CheckedValue:
value1: i32
value2: i32
CONST: CheckedValue = CheckedValue(1)
"""
with pytest.raises(Type5SolverException, match='Not the same type'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@ -105,7 +118,7 @@ def testEntry(arg: Struct) -> (i32, i32, ):
return arg.param
"""
with pytest.raises(Type3Exception, match=type_ + r' must be \(i32, i32, \) instead'):
with pytest.raises(Type5SolverException, match='Not the same type'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@ -132,7 +145,6 @@ class f32:
Suite(code_py).run_code()
@pytest.mark.integration_test
@pytest.mark.skip(reason='FIXME: See constraintgenerator.py for AccessStructMember')
def test_struct_not_accessible():
code_py = """
@exported
@ -140,7 +152,67 @@ def testEntry(x: u8) -> u8:
return x.y
"""
with pytest.raises(Type3Exception, match='u8 is not struct'):
with pytest.raises(Type5SolverException, match='Must be a struct'):
Suite(code_py).run_code()
@pytest.mark.integration_test
def test_struct_does_not_have_field():
code_py = """
class CheckedValue:
value: i32
@exported
def testEntry(x: CheckedValue) -> u8:
return x.y
"""
with pytest.raises(Type5SolverException, match='Must have a field with this name'):
Suite(code_py).run_code()
@pytest.mark.integration_test
def test_struct_literal_does_not_fit():
code_py = """
class CheckedValue:
value: i32
@exported
def testEntry() -> CheckedValue:
return 14
"""
with pytest.raises(Type5SolverException, match='Cannot convert from literal integer'):
Suite(code_py).run_code()
@pytest.mark.integration_test
def test_struct_wrong_struct():
code_py = """
class CheckedValue:
value: i32
class MessedValue:
value: i32
@exported
def testEntry() -> CheckedValue:
return MessedValue(14)
"""
with pytest.raises(Type5SolverException, match='Not the same type'):
Suite(code_py).run_code()
@pytest.mark.integration_test
def test_struct_wrong_arg_count():
code_py = """
class CheckedValue:
value1: i32
value2: i32
@exported
def testEntry() -> CheckedValue:
return CheckedValue(14)
"""
with pytest.raises(Type5SolverException, match='Not the same type'):
Suite(code_py).run_code()
@pytest.mark.integration_test

View File

@ -1,21 +1,22 @@
import pytest
import wasmtime
from phasm.type3.entry import Type3Exception
from phasm.type5.solver import Type5SolverException
from ..helpers import Suite
@pytest.mark.integration_test
@pytest.mark.parametrize('type_, in_put, exp_result', [
('(u8, u8, )', (45, 46), 45, ),
('u8[2]', (45, 46), 45, ),
('bytes', b'This is a test', 84)
@pytest.mark.parametrize('in_typ, in_put, out_typ, exp_result', [
('(u8, u8, )', (45, 46), 'u8', 45, ),
('u16[2]', (45, 46), 'u16', 45, ),
('u32[...]', (45, 46), 'u32', 45, ),
('bytes', b'This is a test', 'u8', 84),
])
def test_subscript_0(type_, in_put, exp_result):
def test_subscript_0(in_typ, in_put, out_typ, exp_result):
code_py = f"""
@exported
def testEntry(f: {type_}) -> u8:
def testEntry(f: {in_typ}) -> {out_typ}:
return f[0]
"""
@ -70,7 +71,7 @@ def testEntry(f: {type_}) -> u32:
return f[0]
"""
with pytest.raises(Type3Exception, match='u32 must be u8 instead'):
with pytest.raises(Type5SolverException, match='Not the same type'):
Suite(code_py).run_code(in_put)
@pytest.mark.integration_test
@ -81,7 +82,7 @@ def testEntry(x: (u8, u32, u64), y: u8) -> u64:
return x[y]
"""
with pytest.raises(Type3Exception, match='Must index with integer literal'):
with pytest.raises(Type5SolverException, match='Must index with integer literal'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@ -92,7 +93,7 @@ def testEntry(x: (u8, u32, u64)) -> u64:
return x[0.0]
"""
with pytest.raises(Type3Exception, match='Must index with integer literal'):
with pytest.raises(Type5SolverException, match='Must index with integer literal'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@ -108,7 +109,7 @@ def testEntry(x: {type_}) -> u8:
return x[-1]
"""
with pytest.raises(Type3Exception, match='Tuple index out of range'):
with pytest.raises(Type5SolverException, match='May not be negative'):
Suite(code_py).run_code(in_put)
@pytest.mark.integration_test
@ -119,7 +120,7 @@ def testEntry(x: (u8, u32, u64)) -> u64:
return x[4]
"""
with pytest.raises(Type3Exception, match='Tuple index out of range'):
with pytest.raises(Type5SolverException, match='Tuple index out of range'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@ -146,5 +147,5 @@ def testEntry(x: u8) -> u8:
return x[0]
"""
with pytest.raises(Type3Exception, match='u8 cannot be subscripted'):
with pytest.raises(Type5SolverException, match='Missing type class instance'):
Suite(code_py).run_code()

View File

@ -1,6 +1,6 @@
import pytest
from phasm.type3.entry import Type3Exception
from phasm.type5.solver import Type5SolverException
from ..helpers import Suite
@ -41,7 +41,7 @@ def test_assign_to_tuple_with_tuple():
CONSTANT: (u32, ) = 0
"""
with pytest.raises(Type3Exception, match='Must be tuple'):
with pytest.raises(Type5SolverException, match='Cannot convert from literal integer'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@ -50,7 +50,7 @@ def test_tuple_constant_too_few_values():
CONSTANT: (u32, u8, u8, ) = (24, 57, )
"""
with pytest.raises(Type3Exception, match='Tuple element count mismatch'):
with pytest.raises(Type5SolverException, match='Tuple element count mismatch'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@ -59,7 +59,7 @@ def test_tuple_constant_too_many_values():
CONSTANT: (u32, u8, u8, ) = (24, 57, 1, 1, )
"""
with pytest.raises(Type3Exception, match='Tuple element count mismatch'):
with pytest.raises(Type5SolverException, match='Tuple element count mismatch'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@ -68,7 +68,7 @@ def test_tuple_constant_type_mismatch():
CONSTANT: (u32, u8, u8, ) = (24, 4000, 1, )
"""
with pytest.raises(Type3Exception, match=r'Must fit in 1 byte\(s\)'):
with pytest.raises(Type5SolverException, match=r'Must fit in 1 byte\(s\)'):
Suite(code_py).run_code()
@pytest.mark.integration_test

View File

@ -18,7 +18,7 @@ class ExpResult:
) + ')'
TYPE_LIST = [
'u8', 'u32', 'u64',
'u8', 'u16', 'u32', 'u64',
]
@pytest.mark.integration_test

View File

@ -0,0 +1,101 @@
import pytest
import wasmtime
from phasm.type5.solver import Type5SolverException
from ..helpers import Suite
@pytest.mark.integration_test
def test_convert_not_implemented():
code_py = """
class Foo:
val: i32
class Baz:
val: i32
@exported
def testEntry(x: Foo) -> Baz:
return convert(x)
"""
with pytest.raises(Type5SolverException, match='Missing type class instance'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@pytest.mark.parametrize('in_typ, in_val, out_typ, exp_val', [
('u32', 1000, 'f32', 1000.0, ),
('u32', 1000, 'f64', 1000.0, ),
('u64', 1000, 'f32', 1000.0, ),
('u64', 1000, 'f64', 1000.0, ),
('i32', 1000, 'f32', 1000.0, ),
('i32', 1000, 'f64', 1000.0, ),
('i64', 1000, 'f32', 1000.0, ),
('i64', 1000, 'f64', 1000.0, ),
])
def test_convert_ok(in_typ, in_val, out_typ, exp_val):
code_py = f"""
@exported
def testEntry(x: {in_typ}) -> {out_typ}:
return convert(x)
"""
result = Suite(code_py).run_code(in_val)
assert exp_val == result.returned_value
@pytest.mark.integration_test
def test_truncate_not_implemented():
code_py = """
class Foo:
val: i32
class Baz:
val: i32
@exported
def testEntry(x: Foo) -> Baz:
return truncate(x)
"""
with pytest.raises(Type5SolverException, match='Missing type class instance'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@pytest.mark.parametrize('in_typ, in_val, out_typ, exp_val', [
('f32', 1000.0, 'u32', 1000, ),
('f64', 1000.0, 'u32', 1000, ),
('f32', 1000.0, 'u64', 1000, ),
('f64', 1000.0, 'u64', 1000, ),
('f32', 1000.0, 'i32', 1000, ),
('f64', 1000.0, 'i32', 1000, ),
('f32', 1000.0, 'i64', 1000, ),
('f64', 1000.0, 'i64', 1000, ),
('f32', 3e9, 'u32', 3e9, ),
('f32', 1e19, 'u64', 9999999980506447872, ),
])
def test_truncate_ok(in_typ, in_val, out_typ, exp_val):
code_py = f"""
@exported
def testEntry(x: {in_typ}) -> {out_typ}:
return truncate(x)
"""
result = Suite(code_py).run_code(in_val)
assert exp_val == result.returned_value
@pytest.mark.integration_test
@pytest.mark.parametrize('in_typ, in_val, out_typ', [
('f32', 3e9, 'i32', ),
('f64', 1e19, 'i64', ),
])
def test_truncate_not_representible(in_typ, in_val, out_typ):
code_py = f"""
@exported
def testEntry(x: {in_typ}) -> {out_typ}:
return truncate(x)
"""
with pytest.raises(wasmtime.Trap, match='integer overflow'):
Suite(code_py).run_code(in_val)

View File

@ -1,10 +1,10 @@
import pytest
from phasm.type3.entry import Type3Exception
from phasm.type5.solver import Type5SolverException
from ..helpers import Suite
INT_TYPES = ['u8', 'u32', 'u64', 'i8', 'i32', 'i64']
INT_TYPES = ['u8', 'u16', 'u32', 'u64', 'i8', 'i16', 'i32', 'i64']
FLOAT_TYPES = ['f32', 'f64']
TYPE_MAP = {
@ -25,11 +25,11 @@ class Foo:
val: i32
@exported
def testEntry(x: Foo, y: Foo) -> Foo:
def testEntry(x: Foo, y: Foo) -> bool:
return x == y
"""
with pytest.raises(Type3Exception, match='Missing type class instantation: Eq Foo'):
with pytest.raises(Type5SolverException, match='Missing type class instance'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@ -107,11 +107,11 @@ class Foo:
val: i32
@exported
def testEntry(x: Foo, y: Foo) -> Foo:
def testEntry(x: Foo, y: Foo) -> bool:
return x != y
"""
with pytest.raises(Type3Exception, match='Missing type class instantation: Eq Foo'):
with pytest.raises(Type5SolverException, match='Missing type class instance'):
Suite(code_py).run_code()
@pytest.mark.integration_test

View File

@ -1,16 +1,22 @@
import pytest
from phasm.type3.entry import Type3Exception
from phasm.type5.solver import Type5SolverException
from ..helpers import Suite
EXTENTABLE = [
('u8', 'u16', ),
('u8', 'u32', ),
('u8', 'u64', ),
('u16', 'u32', ),
('u16', 'u64', ),
('u32', 'u64', ),
('i8', 'i16', ),
('i8', 'i32', ),
('i8', 'i64', ),
('i16', 'i32', ),
('i16', 'i64', ),
('i32', 'i64', ),
]
@ -28,7 +34,7 @@ def testEntry(x: Foo) -> Baz:
return extend(x)
"""
with pytest.raises(Type3Exception, match='Missing type class instantation: Extendable Foo Baz'):
with pytest.raises(Type5SolverException, match='Missing type class instance'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@ -48,17 +54,26 @@ def testEntry() -> {ext_to}:
@pytest.mark.integration_test
@pytest.mark.parametrize('ext_from,in_put,ext_to,exp_out', [
('u8', 241, 'u16', 241),
('u8', 241, 'u32', 241),
('u32', 4059165169, 'u64', 4059165169),
('u8', 241, 'u64', 241),
('u16', 7681, 'u32', 7681),
('u16', 7681, 'u64', 7681),
('u32', 4059165169, 'u64', 4059165169),
('i8', 113, 'i16', 113),
('i8', 113, 'i32', 113),
('i32', 1911681521, 'i64', 1911681521),
('i8', 113, 'i64', 113),
('i8', 3585, 'i32', 3585),
('i8', 3585, 'i64', 3585),
('i32', 1911681521, 'i64', 1911681521),
('i8', -15, 'i16', -15),
('i8', -15, 'i32', -15),
('i32', -15, 'i64', -15),
('i8', -15, 'i64', -15),
('i16', -15, 'i32', -15),
('i16', -15, 'i64', -15),
('i32', -15, 'i64', -15),
])
def test_extend_results(ext_from, ext_to, in_put, exp_out):
@ -89,14 +104,23 @@ def testEntry() -> {ext_from}:
@pytest.mark.integration_test
@pytest.mark.parametrize('ext_to,in_put,ext_from,exp_out', [
('u16', 0xF1F1, 'u8', 0xF1),
('u32', 0xF1F1F1F1, 'u16', 0xF1F1),
('u32', 0xF1F1F1F1, 'u8', 0xF1),
('u64', 0xF1F1F1F1F1F1F1F1, 'u32', 0xF1F1F1F1),
('u64', 0xF1F1F1F1F1F1F1F1, 'u16', 0xF1F1),
('u64', 0xF1F1F1F1F1F1F1F1, 'u8', 0xF1),
('i16', 0xF171, 'i8', 113),
('i16', 0xF1F1, 'i8', -15),
('i32', 0xF1F171F1, 'i16', 29169),
('i32', 0xF1F1F1F1, 'i16', -3599),
('i32', 0xF1F1F171, 'i8', 113),
('i32', 0xF1F1F1F1, 'i8', -15),
('i64', 0x71F1F1F171F1F1F1, 'i32', 1911681521),
('i64', 0x71F1F1F1F1F1F1F1, 'i32', -235802127),
('i64', 0xF1F1F1F171F1F1F1, 'i32', 1911681521),
('i64', 0xF1F1F1F1F1F1F1F1, 'i32', -235802127),
('i64', 0xF1F1F1F1F1F171F1, 'i16', 29169),
('i64', 0xF1F1F1F1F1F1F1F1, 'i16', -3599),
('i64', 0xF1F1F1F1F1F1F171, 'i8', 113),
('i64', 0xF1F1F1F1F1F1F1F1, 'i8', -15),
])

View File

@ -1,11 +1,27 @@
import pytest
from phasm.type5.solver import Type5SolverException
from ..helpers import Suite
@pytest.mark.integration_test
def test_sqrt_not_implemented():
code_py = """
class Foo:
val: i32
@exported
def testEntry(x: Foo) -> Foo:
return sqrt(x)
"""
with pytest.raises(Type5SolverException, match='Missing type class instance'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@pytest.mark.parametrize('type_', ['f32', 'f64'])
def test_builtins_sqrt(type_):
def test_floating_sqrt(type_):
code_py = f"""
@exported
def testEntry() -> {type_}:

View File

@ -1,6 +1,6 @@
import pytest
from phasm.type3.entry import Type3Exception
from phasm.type5.solver import Type5SolverException
from ..helpers import Suite
from .test_natnum import FLOAT_TYPES, INT_TYPES
@ -36,7 +36,7 @@ def testEntry(x: Foo[4]) -> Foo:
return sum(x)
"""
with pytest.raises(Type3Exception, match='Missing type class instantation: NatNum Foo'):
with pytest.raises(Type5SolverException, match='Missing type class instance'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@ -129,7 +129,7 @@ def testEntry(b: i32[{typ_arg}]) -> i32:
"""
suite = Suite(code_py)
result = suite.run_code(tuple(in_put), with_traces=True, do_format_check=False)
result = suite.run_code(tuple(in_put))
assert exp_result == result.returned_value
@pytest.mark.integration_test
@ -168,9 +168,9 @@ def testEntry(x: {in_typ}, y: i32, z: i64[3]) -> i32:
return foldl(x, y, z)
"""
r_in_typ = in_typ.replace('[', '\\[').replace(']', '\\]')
match = 'Type shape mismatch'
with pytest.raises(Type3Exception, match=f'{r_in_typ} must be a function instead'):
with pytest.raises(Type5SolverException, match=match):
Suite(code_py).run_code()
@pytest.mark.integration_test
@ -184,7 +184,7 @@ def testEntry(i: i64, l: i64[3]) -> i64:
return foldr(foo, i, l)
"""
with pytest.raises(Type3Exception, match=r'Callable\[i64, i64, i64\] must be Callable\[i32, i64, i64\] instead'):
with pytest.raises(Type5SolverException, match='Not the same type'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@ -195,7 +195,7 @@ def testEntry(x: i32[5]) -> f64:
return sum(x)
"""
with pytest.raises(Type3Exception, match='f64 must be i32 instead'):
with pytest.raises(Type5SolverException, match='Not the same type'):
Suite(code_py).run_code((4, 5, 6, 7, 8, ))
@pytest.mark.integration_test
@ -206,16 +206,16 @@ def testEntry(x: i32) -> i32:
return sum(x)
"""
with pytest.raises(Type3Exception, match='Missing type class instantation: Foldable i32.*i32 must be a constructed type instead'):
with pytest.raises(Type5SolverException, match='Type shape mismatch'):
Suite(code_py).run_code()
@pytest.mark.integration_test
def test_foldable_not_foldable():
code_py = """
@exported
def testEntry(x: (i32, u32, )) -> i32:
def testEntry(x: (u32, i32, )) -> i32:
return sum(x)
"""
with pytest.raises(Type3Exception, match='Missing type class instantation: Foldable tuple'):
with pytest.raises(Type5SolverException, match='Missing type class instance'):
Suite(code_py).run_code()

View File

@ -1,5 +1,7 @@
import pytest
from phasm.type5.solver import Type5SolverException
from ..helpers import Suite
TYPE_LIST = ['f32', 'f64']
@ -57,6 +59,26 @@ TEST_LIST = [
('nearest(-5.5)', -6.0, ),
]
@pytest.mark.integration_test
@pytest.mark.parametrize('method', [
'ceil',
'floor',
'trunc',
'nearest',
])
def test_fractional_not_implemented(method):
code_py = f"""
class Foo:
val: i32
@exported
def testEntry(x: Foo) -> Foo:
return {method}(x)
"""
with pytest.raises(Type5SolverException, match='Missing type class instance'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@pytest.mark.parametrize('type_', TYPE_LIST)
@pytest.mark.parametrize('test_in,test_out', TEST_LIST)

View File

@ -1,9 +1,29 @@
import pytest
from phasm.type5.solver import Type5SolverException
from ..helpers import Suite
TYPE_LIST = ['u32', 'u64', 'i32', 'i64']
@pytest.mark.integration_test
@pytest.mark.parametrize('operator', [
'//',
'%',
])
def test_integral_not_implemented(operator):
code_py = f"""
class Foo:
val: i32
@exported
def testEntry(x: Foo, y: Foo) -> Foo:
return x {operator} y
"""
with pytest.raises(Type5SolverException, match='Missing type class instance'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@pytest.mark.parametrize('type_', TYPE_LIST)
def test_integral_div_ok(type_):

View File

@ -1,6 +1,6 @@
import pytest
from phasm.type3.entry import Type3Exception
from phasm.type5.solver import Type5SolverException
from ..helpers import Suite
@ -27,7 +27,7 @@ def testEntry(x: Foo, y: Foo) -> Foo:
return x + y
"""
with pytest.raises(Type3Exception, match='Missing type class instantation: NatNum Foo'):
with pytest.raises(Type5SolverException, match='Missing type class instance'):
Suite(code_py).run_code()
@pytest.mark.integration_test

View File

@ -5,16 +5,18 @@ import pytest
from ..helpers import Suite
TYPE_LIST = [
'u8', 'u32', 'u64',
'i8', 'i32', 'i64',
'u8', 'u16', 'u32', 'u64',
'i8', 'i16', 'i32', 'i64',
'f32', 'f64',
]
BOUND_MAP = {
'u8': struct.unpack('<BB', bytes.fromhex('00FF')),
'u16': struct.unpack('<HH', bytes.fromhex('0000FFFF')),
'u32': struct.unpack('<II', bytes.fromhex('00000000FFFFFFFF')),
'u64': struct.unpack('<QQ', bytes.fromhex('0000000000000000FFFFFFFFFFFFFFFF')),
'i8': struct.unpack('<bb', bytes.fromhex('807F')),
'i16': struct.unpack('<hh', bytes.fromhex('0080FF7F')),
'i32': struct.unpack('<ii', bytes.fromhex('00000080FFFFFF7F')),
'i64': struct.unpack('<qq', bytes.fromhex('0000000000000080FFFFFFFFFFFFFF7F')),
'f32': struct.unpack('<ff', bytes.fromhex('01000000FFFF7F7F')),

View File

@ -1,6 +1,8 @@
import math
import pytest
from phasm.type3.entry import Type3Exception
from phasm.type5.solver import Type5SolverException
from ..helpers import Suite
@ -19,33 +21,55 @@ def testEntry(x: Foo) -> Baz:
return promote(x)
"""
with pytest.raises(Type3Exception, match='Missing type class instantation: Promotable Foo Baz'):
with pytest.raises(Type5SolverException, match='Missing type class instance'):
Suite(code_py).run_code()
@pytest.mark.integration_test
def test_promote_ok():
@pytest.mark.parametrize('in_val, exp_val', [
(10.5, 10.5, ),
(9.999999616903162e+35, 9.999999616903162e+35, ),
])
def test_promote_ok(in_val, exp_val):
code_py = """
CONSTANT: f32 = 10.5
@exported
def testEntry() -> f64:
return promote(CONSTANT)
def testEntry(x: f32) -> f64:
return promote(x)
"""
result = Suite(code_py).run_code()
result = Suite(code_py).run_code(in_val)
assert 10.5 == result.returned_value
assert exp_val == result.returned_value
@pytest.mark.integration_test
def test_demote_ok():
def test_demote_not_implemented():
code_py = """
CONSTANT: f64 = 10.5
class Foo:
val: i32
class Baz:
val: i32
@exported
def testEntry() -> f32:
return demote(CONSTANT)
def testEntry(x: Foo) -> Baz:
return demote(x)
"""
result = Suite(code_py).run_code()
with pytest.raises(Type5SolverException, match='Missing type class instance'):
Suite(code_py).run_code()
assert 10.5 == result.returned_value
@pytest.mark.integration_test
@pytest.mark.parametrize('in_val, exp_val', [
(10.5, 10.5, ),
(9.999999616903162e+35, 9.999999616903162e+35, ),
(1e39, math.inf, ),
])
def test_demote_ok(in_val, exp_val):
code_py = """
@exported
def testEntry(x: f64) -> f32:
return demote(x)
"""
result = Suite(code_py).run_code(in_val)
assert exp_val == result.returned_value

View File

@ -0,0 +1,44 @@
import pytest
from phasm.type5.solver import Type5SolverException
from ..helpers import Suite
@pytest.mark.integration_test
def test_reinterpret_not_implemented():
code_py = """
class Foo:
val: i32
class Baz:
val: i32
@exported
def testEntry(x: Foo) -> Baz:
return reinterpret(x)
"""
with pytest.raises(Type5SolverException, match='Missing type class instance'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@pytest.mark.parametrize('in_typ, in_val, out_typ, exp_val', [
('u32', 3225944128, 'f32', -3.1250152587890625, ),
('u64', 13837591364432297984, 'f64', -3.1250152587890625, ),
('i32', -1069023168, 'f32', -3.1250152587890625, ),
('i64', -4609152709277253632, 'f64', -3.1250152587890625, ),
('f32', -3.1250152587890625, 'u32', 3225944128, ),
('f64', -3.1250152587890625, 'u64', 13837591364432297984, ),
('f32', -3.1250152587890625, 'i32', -1069023168, ),
('f64', -3.1250152587890625, 'i64', -4609152709277253632, ),
])
def test_reinterpret_ok(in_typ, in_val, out_typ, exp_val):
code_py = f"""
@exported
def testEntry(x: {in_typ}) -> {out_typ}:
return reinterpret(x)
"""
result = Suite(code_py).run_code(in_val)
assert exp_val == result.returned_value

View File

@ -1,11 +1,25 @@
import pytest
from phasm.type5.solver import Type5SolverException
from ..helpers import Suite
@pytest.mark.integration_test
def test_sized_not_implemented():
code_py = """
@exported
def testEntry(x: (i32, )) -> u32:
return len(x)
"""
with pytest.raises(Type5SolverException, match='Missing type class instance'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@pytest.mark.parametrize('type_, in_put, exp_result', [
('bytes', b'Hello, world!', 13),
('u16[...]', (1, 2, 8), 3),
('u8[4]', (1, 2, 3, 4), 4),
])
def test_len(type_, in_put, exp_result):