This is still in progress and subject to change, maybe eternally.
This is an idea for a programming language I have had in mind for a while. What if the language compiled to WASM because it wanted to?
Languages that compile to WASM, generally and as far as I can tell, fall into three categories.
- Rust, Zig, and the like: languages that support WASM as yet another backend.
- As a vehicle to run on the web. In lieu of compiling to, say, JavaScript.
- It is an implementation detail and the author would really rather just write LLVM IR instead.
There is Grain, where exporting functions is not a priority, (which is crazy to me, but I digress.) There is also Onyx and Wa. These languages are all interesting, are all cool, but they all use WASM as means to an end.
One language that does seem closest to what I have in mind is AssemblyScript, although frankly I could never get past the npm init step in their Getting Started.
It does seem a bit tied up in TypeScript land with TypeScript expectations.
For Quince, WASM is the end.
WASM features are (well, in theory) exposed directly to the user.
To import a function, write import(env) fn.
To declare a global, declare a binding in global scope.
To export linear memory, write export mem memory.1
It also makes use of stabilized WASM features, like GC,1 multiple memories,1 SIMD,1 threads,1 the works.
The eventual goal is for the user to be able to implement WASI (or WASIX, or whatever) in userland, without relying on compiler magicks and hidden vairables.
A small taste
Here is the work-in-progress/stream-of-thought repo.
The compiler is extremely barebones right now. There is only one type, int, although it does support multiple returns. Here is Fibonacci implemented in the language.
Each of the below snippets are complete modules, which you can assemble in your favorite WASM assembler and run in your favorite runtime. I have been using Binaryen and Deno, respectively.
Hello World
I have not implemented memory or strings yet, so it is done the old fashioned way.
import(env) fn putchar(c: int) void;
import(env) fn flush() void;
export fn greet() int {
defer flush();
putchar(c: 72);
putchar(c: 101);
putchar(c: 108);
putchar(c: 108);
putchar(c: 111);
putchar(c: 44);
putchar(c: 32);
putchar(c: 87);
putchar(c: 111);
putchar(c: 114);
putchar(c: 108);
putchar(c: 100);
putchar(c: 33);
return 0;
}
🔽
(func $putchar:c
(import "env" "putchar")
(param $0 i32)
)
(func $flush
(import "env" "flush")
)
(func $greet
(export "greet")
(result i32)
i32.const 72
call $putchar:c
i32.const 101
call $putchar:c
i32.const 108
call $putchar:c
i32.const 108
call $putchar:c
i32.const 111
call $putchar:c
i32.const 44
call $putchar:c
i32.const 32
call $putchar:c
i32.const 87
call $putchar:c
i32.const 111
call $putchar:c
i32.const 114
call $putchar:c
i32.const 108
call $putchar:c
i32.const 100
call $putchar:c
i32.const 33
call $putchar:c
i32.const 0
call $flush
return
)Globals
I implemented those most recently.
import(env) const WIND: int;
const COUNTER_WIND: int = WIND * 2;
export fn echo() int {
return COUNTER_WIND;
}
🔽
(global $0
(import "env" "WIND")
i32
)
(global $1
i32
global.get $0
i32.const 2
i32.mul
)
(func $echo
(export "echo")
(result i32)
global.get $1
return
)Lazy Compilation
Functions only so far.
import(env) fn random() int;
export fn do_something(input: int) int {
return 16;
}
fn unused_function() int {
return non_existent_func();
}
🔽
(func $do_something:input
(export "do_something")
(param $0 i32)
(result i32)
i32.const 16
return
)Peephole optimizations and Constant Folding
I got a bit carried away with small easy picking optimizations.
module random;
export fn random() int {
return -(300/(3*15));
}
export fn mystery(lhs: int, rhs: int) int {
return if ((lhs <= rhs) and (true and false)) -100 else 100;
}
🔽
(module $random
(func $random
(export "random")
(result i32)
i32.const -6
return
)
(func $mystery:lhs:rhs
(export "mystery")
(param $0 i32)
(param $1 i32)
(result i32)
i32.const 100
return
)
)Loops
Only while so far. I don't think I wll add more.
export fn find_first_divisor(n: int) int {
var i = 2;
return while ((i * i) <= n) {
if ((n % i) == 0) break i;
i += 1;
} else n;
}
🔽
(func $find_first_divisor:n
(export "find_first_divisor")
(param $0 i32)
(result i32)
(local $1 i32)
i32.const 2
local.set $1
loop $0_block_c (result i32)
local.get $1
local.get $1
i32.mul
local.get $0
i32.le_s
if $0_block (result i32)
block $1_block
local.get $0
local.get $1
i32.rem_s
if $2_block
else $2_block
local.get $1
br $0_block
end $2_block
local.get $1
i32.const 1
i32.add
local.set $1
end $1_block
br $0_block_c
else $0_block
local.get $0
end $0_block
end $0_block_c
return
)Tail Calls
Landing become before Rust.
export fn factorial(n: int) int {
become fac_aux(n: n, acc: 1);
}
fn fac_aux(n: int, acc: int) int {
if (n <= 1) return acc;
become fac_aux(n: n - 1, acc: n * acc);
}
🔽
(func $factorial:n
(export "factorial")
(param $0 i32)
(result i32)
local.get $0
i32.const 1
return_call $fac_aux:n:acc
)
(func $fac_aux:n:acc
(param $1 i32)
(param $2 i32)
(result i32)
local.get $1
i32.const 1
i32.le_s
if $0_block
local.get $2
return
end $0_block
local.get $1
i32.const 1
i32.sub
local.get $1
local.get $2
i32.mul
return_call $fac_aux:n:acc
)