# CurlyWas CurlyWas is a (still WIP) curly-braces, infix synatx for WebAssembly. The goal is to have as to a 1:1 mapping to the resulting wasm instructions as possible while still being reasonably convenient to write. For this reason alone (and in no way because I'm a little lazy) does this compiler not implement any optimizations except for constant folding. ## Example ```rust import "env.memory" memory(4); import "env.sin" fn sin(f32) -> f32; export fn tic(time: i32) { let i: i32; loop screen { let lazy t = time as f32 / 2000 as f32; let lazy o = sin(t) * 0.8; let lazy q = (i % 320) as f32 - 160.1; let lazy w = (i / 320 - 120) as f32; let lazy r = sqrt(q*q + w*w); let lazy z = q / r; let lazy s = z * o + sqrt(z * z * o * o + 1 as f32 - o * o); let lazy q2 = (z * s - o) * 10 as f32 + t; let lazy w2 = w / r * s * 10 as f32 + t; let lazy s2 = s * 50 as f32 / r; i?120 = max( 0 as f32, ((q2 as i32 ^ w2 as i32 & ((s2 + t) * 20 as f32) as i32) & 5) as f32 * (2 as f32 - s2) * 22 as f32 ) as i32; branch_if (i := i + 1) < 320*240: screen } } ``` You can compile this to `technotunnel.wasm` with the command ``` curlywas technotunnel.cwa ``` Then run it on [MicroW8](https://exoticorn.github.io/microw8/v0.1pre2) ## Syntax ### Comments ``` // This is a single line comment. /* Multiline comments are also supported. */ ``` ### Types There are four types in WebAssembly and therefore Curlywas: * `i32`: 32bit integer * `i64`: 64bit integer * `f32`: 32bit float * `f64`: 64bit float There are no unsigned types, but there are unsigned operators where it makes a difference. ### Literals Integer numbers can be given either in decimal or hex: ``` 123, -7878, 0xf00 ``` For floating point numbers, only the most basic decimal format is currently implemented (no scientific notation or hex-floats, yet): ``` 0.464, 3.141, -10.0 ``` String literals exist in a very basic form. No escape character implemented yet. ``` "env.memory", "Hello World!" this does not work, yet: "one line\nsecond line", "They said: \"Enough!\"" ``` ### Imports WebAssembly imports are specified with a module and a name. In Curlywas you give them inside a single string literal, seperated by a dot. So a module `env` and name `printString` would be written `"env.printString"`. Linear memory can be imported like this: ``` import "module.name" memory(min_pages); ``` giving the minimum required size as the number of 64KB pages. Global variables can be imported with: ``` import "module.name" global var_name: type; // const global import "module.name" global mut var_name: type; // mutable global ``` `var_name` being the name you want to reference the variable by in your code. Functions are imported like this: ``` import "module.name" fn fun_name(param_types) [-> return_type]; examples: import "env.cls" cls(i32); // no return type import "env.random" rand() -> i32; // no params import "env.atan2" atan2(f32, f32) -> f32; ``` ### Functions Functions look like this: ``` [export] fn name(param_list) [-> return_type] { [...] } exampels: fn getPixel(x: i32, y: i32) -> i32 { ... } export fn upd() { ... } ``` #### Local variables Variables are defined using `let`: ``` let name: type; ``` They can also be initialized to a value at the same time, in this case the type can be left out and will be infered: ``` let name = expression; ``` Local variables are lexically scoped and shadowing variables declared earlier is explicitely allowed. `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; ``` The expression is evaluated (inlined) everytime you use the variable. ``` let lazy name = expression; ``` The expression is evaluated and assigned to the variable at the first place it is used (and only there). `let lazy` uses the `local.tee` instruction which combines `local.set` and `local.get` and therefore saves on instruction (usually 2 bytes). Examples of mistakes to watch out for: ``` let x = 4; let inline y = 7; print(y); // prints 11 x = x + 2; print(y); // prints 13 ``` ``` let inline num_bytes = write(buffer, size); print(num_bytes); read(buffer, num_bytes); // calls write a second time ``` ``` let lazy num_bytes = write(header, 8); write(body, size); print(num_bytes); // the header is only written now ``` ``` let lazy foo = 42; if rand() & 1 { printNumber(foo); // foo is initialized here } else { 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 ... #### 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.