# Wase - a friendly, low-level language for Wasm - [Wase - a friendly, low-level language for Wasm](#wase---a-friendly-low-level-language-for-wasm) - [Introduction](#introduction) - [Usage](#usage) - [Example](#example) - [Status](#status) - [The Wase Programming Language](#the-wase-programming-language) - [Type system](#type-system) - [Top-level Syntax](#top-level-syntax) - [Global syntax](#global-syntax) - [Function syntax](#function-syntax) - [Include](#include) - [Expressions](#expressions) - [Control flow](#control-flow) - [Blocks](#blocks) - [Indirect calls](#indirect-calls) - [Low level instructions](#low-level-instructions) - [Load/store](#loadstore) - [Instructions](#instructions) - [Control instructions](#control-instructions) - [Reference Instructions](#reference-instructions) - [Parametric Instructions](#parametric-instructions) - [Variable Instructions](#variable-instructions) - [Table Instructions](#table-instructions) - [Memory Instructions](#memory-instructions) - [Numeric Instructions](#numeric-instructions) - [**Vector Instructions**](#vector-instructions) - [Wasm Sections](#wasm-sections) - [Imports of globals and functions](#imports-of-globals-and-functions) - [Memory](#memory) - [Data](#data) - [Tables](#tables) - [Standard library](#standard-library) - [Development](#development) - [Implementation notes](#implementation-notes) - [TODOS](#todos) ## Introduction Make WebAssembly Easy. WASE is a language, which tries to make WASM easy to write directly. The language maps closely to WebAssembly, and compiles directly to Wasm bytecode. As such, it can be seen as an alternative to the WAT text format, but it is easier to use because of these things: - It uses more conventional syntax rather than S-expr - It has type inference - It handles all the index and table business for you The language is strongly typed, lexically scoped, and provides direct access to memory through load/stores. This is a low-level language without real data structures, lambdas/closures, nor memory management, but those can be written in this language. The language is designed to expose the low-level flexibility of Wasm directly but in a friendly manner, which hides most of the complexity of the Wasm format. It is intended to be used as for low-level Wasm programs, such as language runtimes (incl. memory allocators and gcs) for higher-level languages, or as a target for languages that want to compile directly to Wasm. It also helps explain how Wasm works by hiding some of the low-level details so the structure and semantics are clarified. ## Usage To compile a `tests/euler1.wase` program to `tests/euler1.wasm`, clone this repository and then use bin\wase tests/euler1.wase This requires a suitable Java Runtime in your path. You can also consider to add `wase/bin` to your path. You can also run it directly using java: java -jar bin\wase.jar tests/euler1.-wase ## Example Here is a solution to the Project Euler challenge 1 (https://projecteuler.net/problem=1) written in Wase: // This includes Wasi imports and a printi32 helper include lib/runtime; // For Wasi, we have to export the memory export memory 1; // Iterative version using a loop euler1Loop(limit : i32) -> i32 { var start = 1; var acc = 0; loop { // Breaks out of the function when start >= limit with the value acc break_if<1>(acc, start >= limit); if (start % 3 == 0 | start % 5 == 0) { acc := acc + start; }; start := start + 1; // This is really continue and loops break<>(); }; // This is never reached, but we have to add this to get the right type acc } // A recursive implementation foldRange(start : i32, end : i32, acc : i32) -> i32 { if (start <= end) { foldRange(start + 1, end, if (start % 3 == 0 | start % 5 == 0) { acc + start; } else acc) } else { acc; } } euler1(limit : i32) -> i32 { foldRange(1, limit - 1, 0); } // Wasi expects us to have a "_start" function exported export _start() -> () { printi32(euler1Loop(1000)); // Correct: 233168 printByte(10); // New line printi32(euler1(1000)); // Correct: 233168 printByte(10); // New line {} } Compile with bin\wase tests/euler1.wase and run with wasmer ([install ](https://wasmer.io/)) wasmer tests/euler1.wasm 233168 233168 You can also run with wasm3, but it does not have as deep a stack, so the recursion above causes a stack overflow unless you provide a bigger stack to wasm3. To compare Wase against Wat, check out the `tests/` folder where the `.wase` files have been compiled to `.wasm` and then decompiled to `.wat`. ## Status Beta. The compiler works, and the compiler can parse, type and compile most instructions directly to WASM binaries that validate and run correctly. One problem in daily use is that error messages are currently without positions. This compiler is not validating. For example, you can use dynamic code in constant contexts, which will be rejected by the Wasm runtime. Join the flow9 discord, which also hosts discussions on Wase: https://discord.gg/9gGJu6KU # The Wase Programming Language ## Type system Wase supports the types of Wasm: i32, i64, f32, f64, v128, func, extern At compile time, however, the compiler uses the real types for functions to ensure type checking is complete. The syntax for functions is like this: // A function that takes an i32 and a f64, and returns a i32 foo(i32, f64) -> i32 { 42 } // Does not take any arguments, does not return anything foo() -> () { } // Multi-values is supported with this syntax: This function returns both an i32 and a f64 foo(i32) -> (i32, f64) { [ 42, 3.141 ] } As a special type, there is also `auto`, where the type will be inferred by the compiler: // The return type of this function is inferred to be "i32" bar() -> auto { 1; } This also works for locals: bar() -> auto { a : auto = 1; b = 2.0; // This is implicitly auto, and thus inferred a } The type inference is based on Hindley-Milner style unification, so it should be robust. You can use explicit type annotations to verify types: foo() -> auto { bar : i32 } There are no implicit type conversions. The compiler tracks whether variables and globals are mutable or not: foo() { a = 1; a := 2; // Not allowed. var b = 1; b := 2; // This is allowed, since b is "var" } c : mutable i32 = 1; bar() { c := 2; // This is allowed } ## Top-level Syntax At the top-level, we have syntax for the different kinds of sections in Wasm. This includes globals, functions, imports, tables and data. The compiler will reorder the top-level, so imports, tables, memory and data come first in the original order, and then after that, globals and functions in their original order, in accordance with Wasm requirements. Functions can call each other in mutual recursion, but globals can only refer to globals that occur before themselves. ## Global syntax The grammar for globals is like this: // Constant global : = ; // Mutable global : mutable = ; // Exported global export : = ; // Exported mutable global export : mutable = ; Some examples: pi : f64 = 3.14159265359; counter : mutable i32 = 0; // Exports this global to the host with the name "secret" export secret : i32 = 0xdeadbeaf; // Exports this mutable global with the name "changes" export changes : mutable f64 = 1.023; // Exports this global to the host with the name "external" export "external" internal : i32 = 1; ## Function syntax // Functions foo(arg : i32) -> i32 { a : i32 = 1; a + arg } // This function is exported to the host using the name "foo" export foo(arg : i32) -> i32 { arg } // Exports this function using the "_start" name to the host, in compliance with Wasi export "_start" start() -> () { ... } main() -> () { // This is the initial function automatically called otherwise ... } If the program contains a function called `main`, it is marked as the function that starts the program. ## Include Wase code can be split into multiple files, and included using this syntax at the top-level: include lib/runtime; The compiler will read the file `wase/lib/runtime.wase`, parse it, and splice the result into that place in the code. If a file is included more than once (also transitively), it is only included the first occurrence. The program is only type checked after includes are resolved. # Expressions The body of globals and functions are expressions. There are no statements, but only expressions. The syntax is close to the typescript family: h32 : i32 = 1; // Int constants are i32 h32 : i32 = 0xDEADBEEF; // Hex notation h64 : i64 = 1w; // w suffix for i64 h64 : i64 = 0x12345678DEADBEEFw; // Hex notation with suffix w for i64 fl32 : f32 = 0xDEADBEEFn; // Binary representation fl64 : f64 = 10.0; // Floats are f64 by default fl64 : f64 = 0x2345678DEADBEEFh; // Binary representation // Let-binding have scope a : i32 = 1; // "a" is defined in this scope var v : i32 = 1; v := 2; // set local or global, when mutable // Arithmetic, all signed. 1 + 2 / 3 * 4 % 5 // f64 arithmetic 23.4 + 2.3 * 3.141 // Bitwise operations 1 & 3 | 5 ^ 7 // Comparisons are signed. Use instructions for unsigned comparisons 1 < 5 | 5 >= 2 & a == 2.0 & lt_u<>(b, c) // Tuples, aka multi-values [1, 2.0, 45] // Tuple-let: This expands into multiple let-bindings with names like "t.0" and "t.1" t : (i32, f64) = [1, 3.141]; ## Control flow // Function call foo(1, 2) if (a) b else c; if (a) b; // Sequence { a(); // An implicit drop is added here b(); // An implicit drop is added here [1, 2]; // Two implicit drops are added here c } // Return from the function. foo() -> i32 { if (early) { return 1; // Has to match function return type } code; } bar() -> () { if (early) { // Matches no result from function return type return; } code } ## Blocks Wasm uses blocks for control flow. Each function introduces a block. The `block` and `loop` constructs also do that, as well as the `then` and `else` branches of `if`. The `break` and `break_if` instructions refer to this stack of blocks. A break with level 0 breaks out of the inner most block, while `break<1>()` breaks out of the next level. The number defines what parent block to break to. block { code; // This breaks to the end of the block break_if<>(earlyStop); code; } loop { code; // In loops, the break goes to the top of the loop, // so this is an infinite loop break<>(); } foo() -> () { loop { code; // This breaks out of the function break_if<1>(stopCondition); // This is really continue break<>(); } } foo () -> f64 { loop { // Here, the break returns 3.141 from the function // since block one level up is the function, which // returns f64 break_if<1>(3.141, earlyStop) } } // Here is a simple do-while loop, which prints from 1 to 10 and then F, // but not E. block { var i = 1; loop { printi32(i); printByte(10); i := i + 1; // This breaks out of the upper loop break_if<1>(i > 10); // This is really continue inside this loop break<>(); }; // Never reached "E" printByte(69); {} }; // This is "F" printByte(70); TODO: illustrations (blocks with colors? with colorful brackets / font) ## Indirect calls Wase helps make indirect calls easier to work with. Consider a set of functions like this: foo() -> i32 { 42 } bar() -> i32 { 666 } add1(v : i32) -> i32 { v + 1 } sub1(v : i32) -> i32 { v - 1 } Now it is possible to treat these functions as pointers, and call them indirectly using `call_indirect` using the `fn` construct: call_indirect<>(fn()); // 666 call_indirect<>(fn()); // 42 call_indirect<>(41, fn()); // 42 call_indirect<>(43, fn()); // 43 Behind the scenes, Wase will collect all `fn` in the program, construct a element table for them, and resolve them into i32 as required by `call_indirect`. If you treat these functions as first order, the type of `fn` is i32. Also notice that the type of the indirect function is inferred, so you might have to add a type annotation to resolve what the resulting type is: myfn : i32 = if (cond) fn() else fn(); // We add a type annotation to "explain" that this call is for functions of type () -> i32 call_indirect(myfn) : i32; ## Low level instructions The low-level instructions in Wase have the form instruction(args) where pars are parameters for the operation decided at compile time, while args are arguments to the instruction put on the stack. The instruction has the same name as the corresponding Wasm WAT format. ## Load/store Loads from memory are written like this: load<>(index); The width of the load is inferred from the use of the value. // This is i32 load, since a is i32 a : i23 = load<>(0); // This is f32 load, since f is f32 f : f32 = load<>(32): Stores are written like this: store<>(index, value); The width of the store is inferred from the type of the value. // This is f64.store, because 2.0 is f64 store<>(32, 2.0); Loads and stores also exist in versions that work with smaller bit-widths: // Loads a byte from the given memory address, sign extending it into a i32 or i64 signedByteValue : i32 = load8_s<>(0); signedByteValue64 : i64 = load8_s<>(0); // Loads an unsigned byte from the given memory address into a i32 or i64 byteValue : i32 = load8_u<>(0); byteValue64 : i64 = load8_u<>(0); sword : i32 = load16_s<>(0); sword64 : i64 = load16_s<>(0); uword : i32 = load16_u<>(0); uword64 : i64 = load16_u<>(0); sint32 : i64 = load32_s<>(0); uint32 : i64 = load32_u<>(0); // Stores the byte "32" at address 0 store8<>(0, 32) // The same, except it comes from an i64 int64 : i64 = big; store8<>(0, big); // Only picks the lower 8 bits of "big" // Stores the lower 16 bits of the value at the given address store16<>(address, value); // Stores the lower 32 bits of the value at the given address store32<>(address, value); You can also define the offset and alignment explicitly: load(index) load(index) store(index, value) store(index, value) The alignment is expressed as what power of 2 to use: v : i32 = load8_u<0, 0>(i) // alignment at 1 byte v : i32 = load16_u<0, 1>(i) // alignment at 2 bytes v : i32 = load32_u<0, 2>(i) // alignment at 4 bytes v : i64 = load_u<0, 3>(i) // alignment at 8 bytes TODO: illustrations (offset, alignment) # Instructions 4 instructions plus all v128 SIMD instructions are not implemented yet: table.init and elem.drop memory.init and data.drop ## Control instructions | Wasm | Wase | Comments | |-|-|-| | `block` | `block { exp }`| Type is inferred | `loop` | `loop { exp }` | Type is inferred | `if` | `if (cond) exp` | Type is inferred | `ifelse` | `if (cond) exp else exp` | Type is inferred | `unreachable` | `unreachable<>()` | | `nop` | `nop<>()` | No operation | `br` | `break<>()` or `break()` or `break<>(val)`or `break(val)` | Default break level is 0. If there is a val, that is what we return with this break | `br_if` | `break_if(cond)` or `break_if<>(cond)` or `break_if(val, cond)` or `break_if<>(val, cond)` | Default break level is 0. If there is a val, that is what the break returns | `br_table` | `br_table(index)` or `br_table(val, index)` | Breaks the number of levels as indexed by the index. If a val is given, that is what the break returns. See `tests/break_table.wase` for details. | `return` | `return` or `return exp` | | `call` | `fn(args)` | | `call_indirect` | `call_indirect<>(args, fn())` or `call_indirect<>(fn())` | The `fn()` construct will convert the function `id` to an index into an automatically constructed element table of function index pointers. ## Reference Instructions | Wasm | Wase | Comments | |-|-|-| | `ref.null` | `ref.null()` or `ref_null()` | Construct a null function or extern reference | `ref.is_null` | `exp is null` | Is this function or extern reference null? | `ref.func` | `ref.func` | Constructs an opaque reference to a named function. Can be used in tables. Automatically constructs a element table for the referenced functions ## Parametric Instructions | Wasm | Wase | Comments | |-|-|-| | `drop` | `drop<>` or implicit in sequence `{1;2}` | The explicit `drop<>()` variant causes stack errors, since Wase is designed to be stack-safe. | `select` | `select<>(then, else, cond)` | This is an eager `if`, where both `then` and `else` are always evaluated, but only one chosen based on the condition. This is branch-less so can be more efficient than normal `if`. (Automatically chooses the ref instruction version based on the type.) ## Variable Instructions | Wasm | Wase | Comments | |-|-|-| | `local.get` | `id` | `local.set` | `id := exp` | `local.tee` | `id ::= exp` | Sets the value like `local.set`, but returns the result as well, like in C | `global.get` | `id` | `global.set` | `id := exp` ## Table Instructions table.init and elem.drop not implemented yet. Requires elements first. | Wasm | Wase| Comments | |-|-|-| | `table.get` | `table.get(index : i32)` | Retrieves a value from a table slot. The id the name of a table given by import.table | `table.set` | `table.set(index : i32, value : func/extern)` | Sets a value in a table slot. | `table.size` | `table.size()` | Returns the size of a table. | `table.grow` | `table.grow(init, size)` | Changes the size of a table, initializing with the `init` value in empty slots. Returns previous size. | `table.copy` | `table.copy(elems : i32, source : i32, dest : i32)` | Copies `elems` slots from one area of a table `id1` to another table `id2` | `table.fill` | `table.fill(elements, value, dest)` | Sets `elements` slots start at `dest`to the `value` | `table.init` | `table.init(elements : i32, source : i32, dest : i32)` | Initializes a table with `elements` slots from an element section | `elem.drop` | `elem.drop()` | Discards the memory in an element segment. ## Memory Instructions memory.init and data.drop not implemented yet. Requires data indexing. | Wasm | Wase | Comments | -|-|-| | `*.load` | `load<>(address)` or `load(address)` or `load(address)`| The type is inferred from the use. | `*.load(8,16,32)_(s,u)` | `load(8,16,32)_(s,u)<>(address)` | Load the lower N bits from a memory address. _s implies sign-extension. The type is inferred from the use | `*.store` | `store<>(address, value)` | The width is inferred from the value. | `*.store(8,16,32)` | `store(8,16,32)<>(address, value)` | Store the lower N bits of a value. The width is inferred from the value | `memory.size` | `memory.size<>()` | Returns the unsigned size of memory in terms of pages (64k) | `memory.grow` | `memory.grow<>(size)` | Increases the memory by `size` pages. Returns the previous size of memory, or -1 if memory can not increase | `memory.copy` | `memory.copy<>(dest, source, bytes)` | Copy `bytes` bytes from source to destination | `memory.fill` | `memory.fill<>(dest, bytevalue, bytes)` | Fills `bytes` bytes with the given byte value at `dest` | `memory.init` | `memory.init(dest, source, bytes)` | Copies `bytes` from a data section `` at address `source` into memory starting at address `dest` | `data.drop` | `data.drop()` | Frees the memory of data segment ## Numeric Instructions | Wasm | Wase | Comments | |-|-|-| | `i32.const` | `1` or `0x1234` | `i64.const` | `2w` or `0x1234w` | `f32.const` | `0x1234n` | We do not have natural, number syntax for f32 | `f64.const` | `3.1` or `0x123h` | `*.clz` | `clz<>(exp)` | Returns the number of leading zero bits. The width is inferred | `*.ctz` | `ctz<>(exp)` | Returns the number of trailing zero bits. The width is inferred | `*.popcnt` | `popcnt<>(exp)` | Returns the number of 1-bits. The width is inferred | `*.add` | ` + ` | The width is inferred | `*.sub` | ` - ` | The width is inferred | `*.mul` | ` * ` | The width is inferred | `*.div_s` | ` / ` | Signed division. Rounds towards zero. The width is inferred | `*.div_u` | `div_u<>(, )` | Unsigned division. The width is inferred | `*.div` | ` / ` | Signed division. The width is inferred | `*.rem_s` | ` % ` | Signed remainder. The width is inferred | `*.rem_u` | `rem_u<>(, )` | Unsigned remainder. The width is inferred | `*.and` | ` & ` | Bitwise and. The width is inferred | `*.or` | ` \| ` | Bitwise or. The width is inferred | `*.xor` | ` ^ ` | Bitwise xor. The width is inferred | `*.shl` | `shl<>(val, bits)` | Shift left, i.e. multiplication of power of two. The width is inferred | `*.shr_s` | `shr_s<>(val, bits)` | Signed right shift. Division by power of two, rounding down. The width is inferred | `*.shr_u` | `shr_u<>(val, bits)` | Unsigned right shift. Division by power of two. The width is inferred | `*.rotl` | `rotl<>(val, bits)` | Rotate left. Bits "loop" around. The width is inferred | `*.rotr` | `rotr<>(val, bits)` | Rotate right. Bits "loop" around. The width is inferred | `*.abs` | `abs<>(val)` | Absolute value of floats. The width is inferred | `*.neg` | -2.0 | Negate floating point value. The width is inferred | `*.ceil` | `ceil<>(val)` | Rounds up to the nearest integer. The width is inferred | `*.floor` | `floor<>(val)` | Rounds down to the nearest integer. The width is inferred | `*.trunc` | `trunc<>(val)` | Discard the fractional part, rounding to integer towards zero. The width is inferred | `*.nearest` | `nearest<>(val)` | Round to the nearest integer, with ties rounded toward the value with an even least-significant digit. The width is inferred | `*.sqrt` | `sqrt<>(val)` | Square root. The width is inferred | `*.min` | `min<>(val, val)` | Minimum of two values. NaN wins. The width is inferred | `*.max` | `max<>(val, val)` | Maximum of two values. NaN wins. The width is inferred | `*.copysign` | `copysign<>(val, sign)` | Copies the sign from `sign` into the value of `val`. The width is inferred | `*.eqz` | `eqz<>(val)` | Is the value equal to 0? The width is inferred | `*.eq` | `val == val` | The width is inferred | `*.ne` | `val != val` | The width is inferred | `*.lt_s` | `val < val` | The width is inferred | `*.lt_u` | `lt_u<>(val, val)` | Unsigned comparison. The width is inferred | `*.gt_s` | `val > val` | The width is inferred | `*.gt_u` | `gt_u<>(val, val)` | Unsigned comparison. The width is inferred | `*.le_s` | `val <= val` | The width is inferred | `*.le_u` | `le_u<>(val, val)` | Unsigned comparison. The width is inferred | `*.ge_s` | `val >= val` | The width is inferred | `*.ge_u` | `ge_u<>(val, val)` | Unsigned comparison. The width is inferred | `i32.wrap_i64` | `wrap<>(val)` | Takes lower 32 bits of an i64. | `*.trunc*_s` | `trunc_s<>(val)` | Converts f32 or f64 to i32 or i64, considering the result signed. | `*.trunc*_u` | `trunc_u<>(val)` | Converts f32 or f64 to i32 or i64, considering the result unsigned. | `*.trunc_sat*_s` | `trunc_sat_s<>(val)` | Converts f32 or f64 to i32 or i64, considering the result signed, saturating. | `*.trunc_sat*_u` | `trunc_sat_u<>(val)` | Converts f32 or f64 to i32 or i64, considering the result unsigned, and saturating. | `*.extend_i32_s` | `extend_s<>(val)` | Lifts a i32 to a i64, as signed. | `*.extend_i32_u` | `extend_u<>(val)` | Lifts a i32 to a i64, as unsigned. | `*.extend8_s` | `extend8_s<>(val)` | Lifts a byte to a i32/i64, as signed. The val is the same type as the result | `*.extend16_s` | `extend16_s<>(val)` | Lifts 16 bits to a i32/i64, as signed. The val is the same type as the result | `*.convert_*_s` | `convert_s<>(val)` | Lifts signed i32/i64 to f32/f64. | `*.convert*_u` | `convert_u<>(val)` | Lifts unsigned i32/i64 to f32/f64. | `*.demote_f64` | `demote*<>(val)` | Lowers a f64 to f32 | `*.promote_f32` | `promote*<>(val)` | Lifts a f32 to a f64 | `*.reinterpret*` | `reinterpret<>(val)` | Taking bit pattern of i32/i64/f32/f64 and interpret as f32/f64/i32/i64 correspondingly. ## **Vector Instructions** | Wasm | Wase | Comments | |-|-|-| | `v128.const` | `v128.const` | Create a constant 128-bit vector. This instructions has a 16 i8 byte, or 8 i16, or 4 i32, or 0 parameters and 0 arguments. | `i8x16.shuffle` | `i8x16.shuffle` | Shuffle 16 of 8-bit lanes. This instruction has exactly 16 parameters of values in the interval 0-31 and 0 arguments. ### Memory instructions | Wasm | Wase | Comments | |-|-|-| | `v128.load` | `v128.load` | v128.load | `v128.load8x8_s` | `v128.load8x8_s` | v128.load8x8_s | `v128.load8x8_u` | `v128.load8x8_u` | v128.load8x8_u | `v128.load16x4_u` | `v128.load16x4_u` | v128.load16x4_u | `v128.load16x4_s` | `v128.load16x4_s` | v128.load16x4_s | `v128.load32x2_u` | `v128.load32x2_u` | v128.load32x2_u | `v128.load32x2_s` | `v128.load32x2_s` | v128.load32x2_s | `v128.load8_splat` | `v128.load8_splat` | v128.load8_splat | `v128.load16_splat` | `v128.load16_splat` | v128.load16_splat | `v128.load32_splat` | `v128.load32_splat` | v128.load32_splat | `v128.load64_splat` | `v128.load64_splat` | v128.load64_splat | `v128.load32_zero` | `v128.load32_zero` | v128.load32_zero | `v128.load64_splat` | `v128.load64_splat` | v128.load64_splat | `v128.store` | `v128.store` | v128.store | `v128.load8_lane` | `v128.load8_lane` | v128.load8_lane | `v128.load16_lane` | `v128.load16_lane` | v128.load16_lane | `v128.load32_lane` | `v128.load32_lane` | v128.load32_lane | `v128.load64_lane` | `v128.load64_lane` | v128.load64_lane | `v128.store8_lane` | `v128.store8_lane` | v128.store8_lane | `v128.store16_lane` | `v128.store16_lane` | v128.store16_lane | `v128.store32_lane` | `v128.store32_lane` | v128.store32_lane | `v128.store64_lane` | `v128.store64_lane` | v128.store64_lane ### Extract/replace lane instructions | Wasm | Wase | Comments | |-|-|-| | `i8x16.extract_lane_s` | `i8x16.extract_lane_s` | i8x16.extract_lane_s | `i8x16.extract_lane_u` | `i8x16.extract_lane_u` | i8x16.extract_lane_u | `i8x16.replace_lane` | `i8x16.replace_lane` | i8x16.replace_lane | `i16x8.extract_lane_s` | `i16x8.extract_lane_s` | i16x8.extract_lane_s | `i16x8.extract_lane_u` | `i16x8.extract_lane_u` | i16x8.extract_lane_u | `i16x8.replace_lane` | `i16x8.replace_lane` | i16x8.replace_lane | `i32x4.extract_lane` | `i32x4.extract_lane` | i32x4.extract_lane | `i32x4.replace_lane` | `i32x4.replace_lane` | i32x4.replace_lane | `i64x2.extract_lane` | `i64x2.extract_lane` | i64x2.extract_lane | `i64x2.replace_lane` | `i64x2.replace_lane` | i64x2.replace_lane | `f32x4.extract_lane` | `f32x4.extract_lane` | f32x4.extract_lane | `f32x4.replace_lane` | `f32x4.replace_lane` | f32x4.replace_lane | `f64x2.extract_lane` | `f64x2.extract_lane` | f64x2.extract_lane | `f64x2.replace_lane` | `f64x2.replace_lane` | f64x2.replace_lane ### Swizzle/splat instrucions | Wasm | Wase | Comments | |-|-|-| | `i8x16.swizzle` | `i8x16.swizzle` | i8x16.swizzle | `i8x16.splat` | `i8x16.splat` | i8x16.splat | `i16x8.splat` | `i16x8.splat` | i16x8.splat | `i32x4.splat` | `i32x4.splat` | i32x4.splat | `i64x2.splat` | `i64x2.splat` | i64x2.splat | `f32x4.splat` | `f32x4.splat` | f32x4.splat | `f64x2.splat` | `f64x2.splat` | f64x2.splat ### Relational i8x16 instructions | Wasm | Wase | Comments | |-|-|-| | `i8x16.eq` | `i8x16.eq` | i8x16.eq | `i8x16.ne` | `i8x16.ne` | i8x16.ne | `i8x16.lt_s` | `i8x16.lt_s` | i8x16.lt_s | `i8x16.lt_u` | `i8x16.lt_u` | i8x16.lt_u | `i8x16.gt_s` | `i8x16.gt_s` | i8x16.gt_s | `i8x16.gt_u` | `i8x16.gt_u` | i8x16.gt_u | `i8x16.le_s` | `i8x16.le_s` | i8x16.le_s | `i8x16.le_u` | `i8x16.le_u` | i8x16.le_u | `i8x16.ge_s` | `i8x16.ge_s` | i8x16.ge_s | `i8x16.ge_u` | `i8x16.ge_u` | i8x16.ge_u ### Relational i16x8 instructions | Wasm | Wase | Comments | |-|-|-| | `i16x8.eq` | `i16x8.eq` | i16x8.eq | `i16x8.ne` | `i16x8.ne` | i16x8.ne | `i16x8.lt_s` | `i16x8.lt_s` | i16x8.lt_s | `i16x8.lt_u` | `i16x8.lt_u` | i16x8.lt_u | `i16x8.gt_s` | `i16x8.gt_s` | i16x8.gt_s | `i16x8.gt_u` | `i16x8.gt_u` | i16x8.gt_u | `i16x8.le_s` | `i16x8.le_s` | i16x8.le_s | `i16x8.le_u` | `i16x8.le_u` | i16x8.le_u | `i16x8.ge_s` | `i16x8.ge_s` | i16x8.ge_s | `i16x8.ge_u` | `i16x8.ge_u` | i16x8.ge_u ### Relational i32x4 instructions | Wasm | Wase | Comments | |-|-|-| | `i32x4.eq` | `i32x4.eq` | i32x4.eq | `i32x4.ne` | `i32x4.ne` | i32x4.ne | `i32x4.lt_s` | `i32x4.lt_s` | i32x4.lt_s | `i32x4.lt_u` | `i32x4.lt_u` | i32x4.lt_u | `i32x4.gt_s` | `i32x4.gt_s` | i32x4.gt_s | `i32x4.gt_u` | `i32x4.gt_u` | i32x4.gt_u | `i32x4.le_s` | `i32x4.le_s` | i32x4.le_s | `i32x4.le_u` | `i32x4.le_u` | i32x4.le_u | `i32x4.ge_s` | `i32x4.ge_s` | i32x4.ge_s | `i32x4.ge_u` | `i32x4.ge_u` | i32x4.ge_u ### Relational i64x2 instructions | Wasm | Wase | Comments | |-|-|-| | `i64x2.eq` | `i64x2.eq` | i64x2.eq | `i64x2.lt_s` | `i64x2.lt_s` | i64x2.lt_s | `i64x2.gt_s` | `i64x2.gt_s` | i64x2.gt_s | `i64x2.le_s` | `i64x2.le_s` | i64x2.le_s | `i64x2.ge_s` | `i64x2.ge_s` | i64x2.ge_s ### Relational f32x4 instructions | Wasm | Wase | Comments | |-|-|-| | `f32x4.eq` | `f32x4.eq` | f32x4.eq | `f32x4.lt_s` | `f32x4.lt_s` | f32x4.lt_s | `f32x4.gt_s` | `f32x4.gt_s` | f32x4.gt_s | `f32x4.le_s` | `f32x4.le_s` | f32x4.le_s | `f32x4.ge_s` | `f32x4.ge_s` | f32x4.ge_s ### Relational f64x2 instructions | Wasm | Wase | Comments | |-|-|-| | `f64x2.eq` | `f64x2.eq` | f64x2.eq | `f64x2.lt_s` | `f64x2.lt_s` | f64x2.lt_s | `f64x2.gt_s` | `f64x2.gt_s` | f64x2.gt_s | `f64x2.le_s` | `f64x2.le_s` | f64x2.le_s | `f64x2.ge_s` | `f64x2.ge_s` | f64x2.ge_s ### v128 instructions | Wasm | Wase | Comments | |-|-|-| | `v128.not` | `v128.not` | v128.not | `v128.and` | `v128.and` | v128.and | `v128.andnot` | `v128.andnot` | v128.andnot | `v128.or` | `v128.or` | v128.or | `v128.xor` | `v128.xor` | v128.xor | `v128.bitselect` | `v128.bitselect` | v128.bitselect | `v128.any_true` | `v128.any_true` | v128.any_true ### i8x16 instructions | Wasm | Wase | Comments | |-|-|-| | `i8x16.abs` | `i8x16.abs` | i8x16.abs | `i8x16.neg` | `i8x16.neg` | i8x16.neg | `i8x16.popcnt` | `i8x16.popcnt` | i8x16.popcnt | `i8x16.all_true` | `i8x16.all_true` | i8x16.all_true | `i8x16.bitmask` | `i8x16.bitmask` | i8x16.bitmask | `i8x16.narrow_i8x16_s` | `i8x16.narrow_i8x16_s` | i8x16.narrow_i8x16_s | `i8x16.narrow_i8x16_u` | `i8x16.narrow_i8x16_u` | i8x16.narrow_i8x16_u | `i8x16.shl` | `i8x16.shl` | i8x16.shl | `i8x16.shr_s` | `i8x16.shr_s` | i8x16.shr_s | `i8x16.shr_u` | `i8x16.shr_u` | i8x16.shr_u | `i8x16.add` | `i8x16.add` | i8x16.add | `i8x16.add_sat_s` | `i8x16.add_sat_s` | i8x16.add_sat_s | `i8x16.add_sat_u` | `i8x16.add_sat_u` | i8x16.add_sat_u | `i8x16.sub` | `i8x16.sub` | i8x16.sub | `i8x16.sub_sat_s` | `i8x16.sub_sat_s` | i8x16.sub_sat_s | `i8x16.sub_sat_u` | `i8x16.sub_sat_u` | i8x16.sub_sat_u | `i8x16.min_s` | `i8x16.min_s` | i8x16.min_s | `i8x16.min_u` | `i8x16.min_u` | i8x16.min_u | `i8x16.max_s` | `i8x16.max_s` | i8x16.max_s | `i8x16.max_u` | `i8x16.max_u` | i8x16.max_u | `i8x16.avgr_u` | `i8x16.avgr_u` | i8x16.avgr_u ### i16x8 instructions | Wasm | Wase | Comments | |-|-|-| | `i16x8.extract_add_pairwise_i8x16_s` | `i16x8.extract_add_pairwise_i8x16_s` | i16x8.extract_add_pairwise_i8x16_s | `i16x8.extract_add_pairwise_i8x16_u` | `i16x8.extract_add_pairwise_i8x16_u` | i16x8.extract_add_pairwise_i8x16_u | `i16x8.abs` | `i16x8.abs` | i16x8.abs | `i16x8.neg` | `i16x8.neg` | i16x8.neg | `i16x8.q15mulr_sat_s` | `i16x8.q15mulr_sat_s` | i16x8.q15mulr_sat_s | `i16x8.all_true` | `i16x8.all_true` | i16x8.all_true | `i16x8.bitmask` | `i16x8.bitmask` | i16x8.bitmask | `i16x8.narrow_i32x4_s` | `i16x8.narrow_i32x4_s` | i16x8.narrow_i32x4_s | `i16x8.narrow_i32x4_u` | `i16x8.narrow_i32x4_u` | i16x8.narrow_i32x4_u | `i16x8.extend_low_i8x16_s` | `i16x8.extend_low_i8x16_s` | i16x8.extend_low_i8x16_s | `i16x8.extend_high_i8x16_s` | `i16x8.extend_high_i8x16_s` | i16x8.extend_high_i8x16_s | `i16x8.extend_low_i8x16_u` | `i16x8.extend_low_i8x16_u` | i16x8.extend_low_i8x16_u | `i16x8.extend_high_i8x16_u` | `i16x8.extend_high_i8x16_u` | i16x8.extend_high_i8x16_u | `i16x8.shl` | `i16x8.shl` | i16x8.shl | `i16x8.shr_s` | `i16x8.shr_s` | i16x8.shr_s | `i16x8.shr_u` | `i16x8.shr_u` | i16x8.shr_u | `i16x8.add` | `i16x8.add` | i16x8.add | `i16x8.add_sat_s` | `i16x8.add_sat_s` | i16x8.add_sat_s | `i16x8.add_sat_u` | `i16x8.add_sat_u` | i16x8.add_sat_u | `i16x8.sub` | `i16x8.sub` | i16x8.sub | `i16x8.sub_sat_s` | `i16x8.sub_sat_s` | i16x8.sub_sat_s | `i16x8.sub_sat_u` | `i16x8.sub_sat_u` | i16x8.sub_sat_u | `i16x8.mul` | `i16x8.mul` | i16x8.mul | `i16x8.min_s` | `i16x8.min_s` | i16x8.min_s | `i16x8.min_u` | `i16x8.min_u` | i16x8.min_u | `i16x8.max_s` | `i16x8.max_s` | i16x8.max_s | `i16x8.max_u` | `i16x8.max_u` | i16x8.max_u | `i16x8.avgr_u` | `i16x8.avgr_u` | i16x8.avgr_u | `i16x8.extmul_low_i8x16_s` | `i16x8.extmul_low_i8x16_s` | i16x8.extmul_low_i8x16_s | `i16x8.extmul_high_i8x16_s` | `i16x8.extmul_high_i8x16_s` | i16x8.extmul_high_i8x16_s | `i16x8.extmul_low_i8x16_u` | `i16x8.extmul_low_i8x16_u` | i16x8.extmul_low_i8x16_u | `i16x8.extmul_high_i8x16_u` | `i16x8.extmul_high_i8x16_u` | i16x8.extmul_high_i8x16_u ### i32x4 instructions | Wasm | Wase | Comments | |-|-|-| | `i32x4.extract_add_pairwise_i16x8_s` | `i32x4.extract_add_pairwise_i16x8_s` | i32x4.extract_add_pairwise_i16x8_s | `i32x4.extract_add_pairwise_i16x8_u` | `i32x4.extract_add_pairwise_i16x8_u` | i32x4.extract_add_pairwise_i16x8_u | `i32x4.abs` | `i32x4.abs` | i32x4.abs | `i32x4.neg` | `i32x4.neg` | i32x4.neg | `i32x4.all_true` | `i32x4.all_true` | i32x4.all_true | `i32x4.bitmask` | `i32x4.bitmask` | i32x4.bitmask | `i32x4.extend_low_i16x8_s` | `i32x4.extend_low_i16x8_s` | i32x4.extend_low_i16x8_s | `i32x4.extend_high_i16x8_s` | `i32x4.extend_high_i16x8_s` | i32x4.extend_high_i16x8_s | `i32x4.extend_low_i16x8_u` | `i32x4.extend_low_i16x8_u` | i32x4.extend_low_i16x8_u | `i32x4.extend_high_i16x8_u` | `i32x4.extend_high_i16x8_u` | i32x4.extend_high_i16x8_u | `i32x4.shl` | `i32x4.shl` | i32x4.shl | `i32x4.shr_s` | `i32x4.shr_s` | i32x4.shr_s | `i32x4.shr_u` | `i32x4.shr_u` | i32x4.shr_u | `i32x4.add` | `i32x4.add` | i32x4.add | `i32x4.sub` | `i32x4.sub` | i32x4.sub | `i32x4.mul` | `i32x4.mul` | i32x4.mul | `i32x4.min_s` | `i32x4.min_s` | i32x4.min_s | `i32x4.min_u` | `i32x4.min_u` | i32x4.min_u | `i32x4.max_s` | `i32x4.max_s` | i32x4.max_s | `i32x4.max_u` | `i32x4.max_u` | i32x4.max_u | `i32x4.dot_i16x8_s` | `i32x4.dot_i16x8_s` | i32x4.dot_i16x8_s | `i32x4.extmul_low_i16x8_s` | `i32x4.extmul_low_i16x8_s` | i32x4.extmul_low_i16x8_s | `i32x4.extmul_high_i16x8_s` | `i32x4.extmul_high_i16x8_s` | i32x4.extmul_high_i16x8_s | `i32x4.extmul_low_i16x8_u` | `i32x4.extmul_low_i16x8_u` | i32x4.extmul_low_i16x8_u | `i32x4.extmul_high_i16x8_u` | `i32x4.extmul_high_i16x8_u` | i32x4.extmul_high_i16x8_u ### i64x2 instructions | Wasm | Wase | Comments | |-|-|-| | `i64x2.abs` | `i64x2.abs` | i64x2.abs | `i64x2.neg` | `i64x2.neg` | i64x2.neg | `i64x2.all_true` | `i64x2.all_true` | i64x2.all_true | `i64x2.bitmask` | `i64x2.bitmask` | i64x2.bitmask | `i64x2.extend_low_i32x4_s` | `i64x2.extend_low_i32x4_s` | i64x2.extend_low_i32x4_s | `i64x2.extend_high_i32x4_s` | `i64x2.extend_high_i32x4_s` | i64x2.extend_high_i32x4_s | `i64x2.extend_low_i32x4_u` | `i64x2.extend_low_i32x4_u` | i64x2.extend_low_i32x4_u | `i64x2.extend_high_i32x4_u` | `i64x2.extend_high_i32x4_u` | i64x2.extend_high_i32x4_u | `i64x2.shl` | `i64x2.shl` | i64x2.shl | `i64x2.shr_s` | `i64x2.shr_s` | i64x2.shr_s | `i64x2.shr_u` | `i64x2.shr_u` | i64x2.shr_u | `i64x2.add` | `i64x2.add` | i64x2.add | `i64x2.sub` | `i64x2.sub` | i64x2.sub | `i64x2.mul` | `i64x2.mul` | i64x2.mul | `i64x2.extmul_low_i32x4_s` | `i64x2.extmul_low_i32x4_s` | i64x2.extmul_low_i32x4_s | `i64x2.extmul_high_i32x4_s` | `i64x2.extmul_high_i32x4_s` | i64x2.extmul_high_i32x4_s | `i64x2.extmul_low_i32x4_u` | `i64x2.extmul_low_i32x4_u` | i64x2.extmul_low_i32x4_u | `i64x2.extmul_high_i32x4_u` | `i64x2.extmul_high_i32x4_u` | i64x2.extmul_high_i32x4_u ### f32x4 instructions | Wasm | Wase | Comments | |-|-|-| | `f32x4.ceil` | `f32x4.ceil` | f32x4.ceil | `f32x4.floor` | `f32x4.floor` | f32x4.floor | `f32x4.trunc` | `f32x4.trunc` | f32x4.trunc | `f32x4.nearest` | `f32x4.nearest` | f32x4.nearest | `f32x4.abs` | `f32x4.abs` | f32x4.abs | `f32x4.neg` | `f32x4.neg` | f32x4.neg | `f32x4.sqrt` | `f32x4.sqrt` | f32x4.sqrt | `f32x4.add` | `f32x4.add` | f32x4.add | `f32x4.sub` | `f32x4.sub` | f32x4.sub | `f32x4.mul` | `f32x4.mul` | f32x4.mul | `f32x4.div` | `f32x4.div` | f32x4.div | `f32x4.min` | `f32x4.min` | f32x4.min | `f32x4.max` | `f32x4.max` | f32x4.max | `f32x4.pmin` | `f32x4.pmin` | f32x4.pmin | `f32x4.pmax` | `f32x4.pmax` | f32x4.pmax ### f64x2 instructions | Wasm | Wase | Comments | |-|-|-| | `f64x2.ceil` | `f64x2.ceil` | f64x2.ceil | `f64x2.floor` | `f64x2.floor` | f64x2.floor | `f64x2.trunc` | `f64x2.trunc` | f64x2.trunc | `f64x2.nearest` | `f64x2.nearest` | f64x2.nearest | `f64x2.abs` | `f64x2.abs` | f64x2.abs | `f64x2.neg` | `f64x2.neg` | f64x2.neg | `f64x2.sqrt` | `f64x2.sqrt` | f64x2.sqrt | `f64x2.add` | `f64x2.add` | f64x2.add | `f64x2.sub` | `f64x2.sub` | f64x2.sub | `f64x2.mul` | `f64x2.mul` | f64x2.mul | `f64x2.div` | `f64x2.div` | f64x2.div | `f64x2.min` | `f64x2.min` | f64x2.min | `f64x2.max` | `f64x2.max` | f64x2.max | `f64x2.pmin` | `f64x2.pmin` | f64x2.pmin | `f64x2.pmax` | `f64x2.pmax` | f64x2.pmax ### Trunc/convert instructions | Wasm | Wase | Comments | |-|-|-| | `i32x4.trunc_sat_f32x4_s` | `i32x4.trunc_sat_f32x4_s` | i32x4.trunc_sat_f32x4_s | `i32x4.trunc_sat_f32x4_u` | `i32x4.trunc_sat_f32x4_u` | i32x4.trunc_sat_f32x4_u | `f32x4.convert_i32x4_s` | `f32x4.convert_i32x4_s` | f32x4.convert_i32x4_s | `f32x4.convert_i32x4_u` | `f32x4.convert_i32x4_u` | f32x4.convert_i32x4_u | `i32x4.trunc_sat_f64x2_s` | `i32x4.trunc_sat_f64x2_s` | i32x4.trunc_sat_f64x2_s | `i32x4.trunc_sat_f64x2_u` | `i32x4.trunc_sat_f64x2_u` | i32x4.trunc_sat_f64x2_u | `f64x2.convert_low_i32x4_s` | `f64x2.convert_low_i32x4_s` | f64x2.convert_low_i32x4_s | `f64x2.convert_low_i32x4_u` | `f64x2.convert_low_i32x4_u` | f64x2.convert_low_i32x4_u | `f32x4.demote_f64x2_zero` | `f32x4.demote_f64x2_zero` | f32x4.demote_f64x2_zero | `f64x2.promote_low_f32x4` | `f64x2.promote_low_f32x4` | f64x2.promote_low_f32x4 # Wasm Sections ## Imports of globals and functions Use this syntax to import a function from the host: // Function import from host import println : (i32) -> void = console.log; Notice imports have to be the first thing in the program. The same works for globals: // Global import import id : i32 = module.name; import id : mutable i32 = module.name; ## Memory You have to explicitly define how much memory is available for the runtime. To reserve memory, use syntax like this: export? memory ?; or in case it is imported from the host: import memory ? = module.name; Examples: // Reserves one page of 64k memory 1; // Reserves 64k at first, but maximum 1mb memory 1 4; // Reserves 128k and exports this memory under the name "memory" to the host // This is the form that Wasi likes export memory 2; // Reserves and exports memory under the name "mymem" export "mymem" memory 1 4; // 64k Memory import from the host import memory 1 = module.name; ## Data You can place constant data in the output file using syntax like this: // Strings are placed as UTF8 but with the length first // Hello will be bound to the address this data gets data hello = "utf8 string is very comfortable"; // We can have a sequence of data. Each int is a byte. data bytes = 1, 2, 3, "text", 3.0; // Moving the data into offset 32 of the memory data moved = "Hello, world!" offset 32; The result is that this data is copied into memory on startup. ## Tables Importing of tables is done like this: // Table of functions of min size 1, named module.mytable in the host import mytable : table(1) = module.mytable; // Table of externs of min size 5, max size 10, named module.myexterns in the host import myexterns : table(5 10) = module.myexterns; # Standard library There is a minimal standard library you get with `include lib/runtime`. It contains these helpers: // Prints a byte to stdout printByte(i : i32) -> () // Print unsigned i32 as decimal printu32(i : i32) -> () // Print signed i32 as decimal printi32(i : i32) -> () // Prints unsigned as hexadecimal printHex32(i : i32) -> () These require Wasi. There is a stub for the wasi interface in `lib/wasi.wase` but that is very incomplete at the moment. There is a start of a memory allocator in `lib/malloc.wase`. It has a double-linked list of blocks, and a separate area for small allocations (<16 bytes>). It will be probably be refactored to expose the different management disciplines more directly. # Development Wase is written in flow9. To develop on Wase itself, first install flow9: https://github.com/area9innovation/flow9 We recommend to use VS Code with the flow9 extension. To run the compiler from the command line, use bin\wased tests/euler1.wase This will compile the compiler, and use this to compile the `euler1.wase` file to a `euler1.wasm` file. It is thus exactly the same as `bin\wase` except that it uses the latest source of the compiler. When you make changes to the compiler, it is recommended to make sure there are no regressions by running all test cases and produce `.wat` output as well using `wasm2wat`: bin\wased test=1 wat=1 This requires that you install WABT first https://github.com/WebAssembly/wabt If there are regressions, the `.wat` and/or `.wasm` files in the `tests` folder will have changes when you do `git status`. You can compile a new `wase.jar` binary release used by `bin\wase` using: flowc1 wase/wase.flow jar=bin/wase.jar This requires a Java JDK like OpenJDK. ## Implementation notes For each instruction, we need to define three basic different things: - Syntax. Done in `grammar.flow` using Gringo. - Typing. Done in `type.flow` using DSL typing. - Compilation. Done in `compile.flow` using the Wase intermediate AST - Low-level compilation to bytecode is done in the flow9 repository in `flow9/lib/formats/wasm/wasm_encode.flow` ## TODOS There are a number of things, that would make Wase better: - Add the last instructions - Add SIMD instructions - Complete the Wasi interface - Encode 64-bit constants as S64, rather than U64. - Check all I32 encodings whether they are S32 or U32 - Add syntax for function arguments to define whether it is mutable or not. Right now, all function arguments are considered mutable. - Better parse errors with positions - We support a special "hole<>()" instruction, which does nothing. The hole construct could in principle allow stack-like code: [1, hole<>() + 2 ] Right now, typing infer that type to (i32, i32), while it really is i32. So the code above compiles correctly, and works at runtime, but our type inference is not smart enough to know this. - Refactor the malloc to be separate allocation policies - Add syntax for passive data, which is not automatically copied into memory until memory.init is called. - Add support for naming the data index for memory.init and data.drop data id : 1, 23, ... ? - Capture the address and/or size of data segments to make use easier? - Document the implicit tables for ref.func - Add syntax for elements, which are pieces to initialize tables - Small, simple optimizations: - Detect a == 0 and make that eqz - Detect load<>(a + 4) and make that load<4>(a) - Detect const expressions and evaluate them - Detect if (a) const else const, and make that select - local.set x ;local.get x -> local.tee x - *2, *4, /2, /4 can be turned into shifts