Compare commits

...

3 Commits

Author SHA1 Message Date
e58b13c8ee finish first rough version of syntax docs 2022-01-08 22:32:41 +01:00
ac9b6cd4ca almost done with rough syntax docs 2022-01-08 17:49:19 +01:00
0e403ed82e continue syntax docs 2022-01-08 16:17:05 +01:00

126
README.md
View File

@@ -144,6 +144,8 @@ export fn upd() {
}
```
The body of a function is a block (see below), meaning a sequence of statements followed by an optional expression which gives the return value of the function.
#### Local variables
Variables are defined using `let`:
@@ -158,9 +160,11 @@ They can also be initialized to a value at the same time, in this case the type
let name = expression;
```
There are two modifiers that change when the initializer expression is actually evaluated.
Local variables are lexically scoped and shadowing variables declared earlier is explicitely allowed.
They both can reduce the size of the resulting code, but it is up to the coder to make sure that the delayed evaluation doesn't change the semantics of the code.
`name = value;` assigns a new value to a (non-inline) variable.
There are two modifiers that change when the initializer expression is actually evaluated. They both can reduce the size of the resulting code (when used appropriately), but it is up to the coder to make sure that the delayed evaluation doesn't change the semantics of the code.
```
let inline name = expression;
@@ -205,3 +209,121 @@ if rand() & 1 {
printNumber(foo / 2 + 2); // foo is never initialized in the else branch
}
```
#### Expressions
Expressions are written in familiar infix operator and function call syntax.
The avaliable operators are:
| Precedence | Symbol | WASM instruction(s) | Description |
| ---------- | ---------------- | -------------------------------------- | ----------------------------- |
| 1 | - | fxx.neg, ixx.sub | Unary negate |
| | ! | i32.eqz | Unary not / equal to zero |
| 2 | as | default signed casts | Type cast |
| 3 | ?, ! | i32.load_u8_u, i32.load | load byte/word |
| 4 | * | ixx.mul, fxx.mul | Multiplication |
| | /, % | ixx.div_s, fxx.div, ixx.rem_s | signed division / remainder |
| | #/, #% | ixx.div_u, ixx.rem_u | unsigned division / remainder |
| 5 | +, - | xxx.add, xxx.sub | Addition, substraction |
| 6 | <<, >>, #>> | ixx.shl, ixx.shr_s, ixx.shr_u | Shifts |
| 7 | ==, != | xxx.eq, xxx.ne | Equal, not equal |
| | <, <=, >, >= | ixx.lt_s, ixx.le_s, ixx.gt_s, ixx.ge_s | signed comparison |
| | | fxx.lt, fxx.le, fxx.gt, fxx.ge | |
| | #<, #<=, #>, #>= | ixx.lt_u, ixx.le_u, ixx.gt_u, ixx.ge_u | unsigned comparison |
| 8 | &, \|, ^ | ixx.and, ixx.or, ixx.xor | Bitwise logic |
| 9 | <\| | n/a | take first, see sequencing |
You can obviously group sub-expression using brackets `( )`.
Functions can be called using familiar `function_name(parameters)` syntax.
There are intrinsic functions for all WASM instructions that simply take a number of parameters and return a value. So you can, for example, do something like `i32.clz(value)` to use instructions that don't map to their own operator.
Some common float instructions have shortcuts, ie. they can be used without the `f32.` / `.f64.` prefix:
`sqrt, min, max, ceil, floor, trunc, nearest, abs, copysign`
`name := value` both assigns the value to the variable `name` and returns the value (using the `local.tee` WASM instruction).
Blocks are delimited by curly braces `{ }`. They contain zero or more statements, optionally followed by an expression. They evaluate to the
value of that final expression if it is there.
So for example this block evaluates to 12:
```
{
let a = 5;
let b = 7;
a + b
}
```
Blocks are used as function bodies and in flow control (`if`, `block`, `loop`), but can also used at any point inside an expression.
#### Flow control
`if condition_expression { if_true_block } [else {if_false_block}]` executes the `if_true_block` if the condition evaluates to a non-zero integer and
the `if_false_block` otherwise (if it exists). It can also be used as an expression, for example:
```
let a = if 0 { 2 } else { 3 }; // assigns 3 to a
```
`block name { ... }` opens a named block scope. A branch statement can be used to jump to the end of the block. Currently, `block` can only be used
as a statement, returning a value from the block is not yet supported.
`loop name { ... }` opens a named loop scope. A branch statement can be used to jump back to the beginning of the loop.
`branch name` jumps to the end/start of the named `block` or `loop` scope. `branch_if condition: name` does the same if the condition evaluates to a
non-zero integer.
`return [expression]` returns from the current function with the value of the optional expression.
#### Memory load/store
To read from memory you specify a memory location as `base?offset` or `base!offset`. `?` reads a byte and `!` reads a 32bit word.
`base` can be any expression that evaluates to an `i32` while `offset` has to be a constant `i32` value. The effective memory address is the sum of both.
Writing to memory looks just like an assignment to a memory location: `base?offset = expressoin` and `base!offset = expression`.
When reading/writing 32bit words you need to make sure the address is 4-byte aligned.
These compile to `i32.load8_u`, `i32.load`, `i32.store8` and `i32.store`. Other WASM load/store instructions will be implemented as intrinsics, but aren't yet.
#### Advanced sequencing
Sometimes when sizeoptimizing it helps to be able to execute some side-effecty code in the middle an expression.
Using a block scope, we can execute any number of statements before evaluating a final expression to an actual value. For example:
```
let x = { randomSeed(time); random() }; // set the random seed right before optaining a random value
```
To execute something after evaluating the value we want to return, we can use the `<|` operator. Here is an example from the Wasm4 version of
Skip Ahead (see the example folder for the full source):
```
text(8000, set_color(c) <| rect(rx, y, rw, 1), set_color(4));
```
Here, we first set the color to `c`. `set_color` also happens to return the constant `6` which we want to use for the text x-position but only
after drawing a rectangle with color `c` and setting the color for the text to `4`. This line compiles to the following sequence:
* Push `8000` onto the stack
* Call `set_color(c)` which sets the drawing color and pushes 6 on the stack
* Call `rect` which draws a rectangle with the set color. This call doesn't affect the stack.
* Call `set_color(4)` which sets the drawing color to `4` and pushes another 6 on the stack.
* Call `text` with the parameters (`8000`, `6`, `6`) pushed on the stack.
## Limitations
The idea of Curlywas is to be able to hand-craft any valid WASM program, ie. having the same amount of control over the instruction sequence as if you would write in the web assembly text format (`.wat`) just with better ergonomics.
This goal is not yet fully reached, with the following being the main limitations:
* Curlywas currently only targets MVP web assembly + non-trapping float-to-int conversions. No other post-MVP features are currently supported. Especially "Multi-value" will be problematic as this allows programs that don't map cleanly to an expression tree.
* Memory intrinsics are still missing, so only (unsigned) 8 and 32 bit integer reads and writes are possible.
* `block`s cannot return values, as the branch instructions are missing syntax to pass along a value.
* `br_table` and `call_indirect` are not yet implemented.