I wanted to try writing wasm manually (wat
format). I then wanted to try generating wasm programatically (wat
format).
1 Lox test code#
Since I had been reading Crafting Interpreters where I wrote an interpreter for a language called Lox, I decided to try manually translating my Lox test code into wasm. Here’s the Lox:
print "# Testing scope"; var a = 5; print "outer a"; print a; { var a = 10; print "inner a"; print a; } print "outer a"; print a; print "# Testing expressions"; print true; print 2 + 1; print !((3 + 5 / 10) == 3.5); print "# Testing if statements"; if (false) { print "N true"; } else { print "Y false"; } if (true) { print "Y true"; } print "# Testing logical operators"; print "hi" or 2; // "hi" print "hi" and 2; // 2 print nil or "yeh"; // "yeh" print nil or nil; // nil print "# Testing while loops"; var i = 3; while (i > 0) { print i; i = i - 1; } print "# Testing for loops, Fibonacci sequence"; var a = 0; var temp; for (var b = 1; a < 10000; b = temp + b) { print a; temp = a; a = b; }
and here’s the wat I wrote manually:
(module (import "js" "memory" (memory $heap 1)) (import "print" "i32" (func $print_i32 (param i32))) (import "print" "str" (func $print_str (param i32))) (func $add (param $a i32) (param $b i32) (result i32) (local.get $a) (local.get $b) (i32.add) ) (func $main (local $a i32) (local $b i32) (local $a_inner i32) (local $i i32) (local $temp i32) (local.set $a (i32.const 5)) ;; var a = 5; (i32.const 0) (call $print_str) ;; print "outer a"; (local.get $a) (call $print_i32) ;; print a; (local.set $a_inner (i32.const 10)) ;; var a = 10; (i32.const 100) (call $print_str) ;; print "inner a"; (local.get $a_inner) (call $print_i32) ;; print a; (i32.const 0) (call $print_str) ;; print "outer a"; (local.get $a) (call $print_i32) ;; print a; (i32.const 200) (call $print_str) ;; print "# Testing expressions"; (i32.const 2) (i32.const 1) (i32.add) (call $print_i32) ;; print 2 + 1; (f32.const 3) (f32.const 5) (f32.const 10) (f32.div) ;; 5 / 10 (f32.add) ;; 3 + 5 / 10 (f32.const 3.5) (f32.eq) ;; (3 + 5 / 10) == 3.5 (i32.eqz) ;; !((3 + 5 / 10) == 3.5) (call $print_i32) (i32.const 300) (call $print_str) ;; print "# Testing if statements"; (i32.const 0) (if (then (i32.const 400) (call $print_str) ) (else (i32.const 500) (call $print_str) ) ) (i32.const 1) (if (then (i32.const 600) (call $print_str) ) ) (i32.const 700) (call $print_str) ;; print "# Testing while loops" (local.set $i (i32.const 3)) (block $while_1_end (loop $while_1_start (local.get $i) (i32.const 0) (i32.gt_s) ;; i > 0 (i32.eqz) ;; invert (br_if $while_1_end) ;; break if NOT true (local.get $i) (call $print_i32) (local.get $i) (i32.const 1) (i32.sub) (local.set $i) (br $while_1_start) ) ) (i32.const 800) (call $print_str) ;; print "# Testing for loops, Fibonacci sequence" (local.set $a (i32.const 0)) (local.set $b (i32.const 1)) (block $for_1_end (loop $for_1_start (local.get $a) (i32.const 10000) (i32.lt_s) ;; a < 10000 (i32.eqz) ;; invert (br_if $for_1_end) ;; break if NOT true (local.get $a) (call $print_i32) ;; print a; (local.get $a) (local.set $temp) ;; temp = a; (local.get $b) (local.set $a) ;; a = b; (local.get $temp) (local.get $b) (i32.add) (local.set $b) ;; b = temp + b (br $for_1_start) )) ) (func $loop (param $seed i32) (param $N i32) (result i32) (local $i i32) (local.set $i (i32.const 0)) (block $for_2_end (loop $for_2_start (local.get $i) ;; test !(i < N) (local.get $N) (i32.ge_s) (br_if $for_2_end) (local.get $seed) (i32.const 1103515245) (i32.mul) (i32.const 12345) (i32.add) (i32.const 0x7fff) (i32.and) (local.set $seed) ;; seed = (a * seed + c) & m (local.get $i) (i32.const 1) (i32.add) (local.set $i) ;; i++ (br $for_2_start) ) ) (local.get $seed) ) (func $assembly/index/main (param $seed i32) (param $N i32) (result i32) (local $i i32) i32.const 0 local.set $i loop $for-loop|0 local.get $i local.get $N i32.lt_s if i32.const 1103515245 local.get $seed i32.mul i32.const 12345 i32.add i32.const 32767 i32.and local.set $seed local.get $i i32.const 1 i32.add local.set $i br $for-loop|0 end end local.get $seed return ) (export "add" (func $add)) (export "main" (func $main)) (export "loop" (func $loop)) (export "loop2" (func $assembly/index/main)) ;; don't need this after all ;; (memory $stack 1) ;; NOTE: I make my strings NUL-terminated so I don't have to ;; write the length in the code (data (memory $heap) (i32.const 0) "outer a\00") (data (memory $heap) (i32.const 100) "inner a\00") (data (memory $heap) (i32.const 200) "# Testing expressions\00") (data (memory $heap) (i32.const 300) "# Testing if statements\00") (data (memory $heap) (i32.const 400) "N true\00") (data (memory $heap) (i32.const 500) "Y false\00") (data (memory $heap) (i32.const 600) "Y true\00") (data (memory $heap) (i32.const 700) "# Testing while loops\00") (data (memory $heap) (i32.const 800) "# Testing for loops, Fibonacci sequence\00") )
Writing it out manually helped me understand what a compiler would do.
- https://developer.mozilla.org/en-US/docs/WebAssembly/Guides/Understanding_the_text_format[1] was useful
- https://stanford-cs242.github.io/f18/lectures/04-1-webassembly-practice.html[2] too
- https://webassembly.github.io/spec/core/text/instructions.html[3] has a list of all wasm instructions
- https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/Control_flow[4]
Some things I don’t understand:
- why (local.get 0) vs local.get 0, and similarly (i32.add) vs i32.add?
- “flat” vs “folded” formats, seems like folded allows nested subexpressions
- https://nishtahir.com/the-wasm-text-format/[5] shows parentheses and then shows without parentheses
- some pages like https://developer.mozilla.org/en-US/docs/WebAssembly/Guides/Understanding_the_text_format[6] show parentheses only sometimes — look at the
logAllMemory
function which uses both in the same function
other:
- wasmtime can compile wasm into x86 assembly; I tried this on godbolt[7]
- the loop construct in wasm[8] is more like do-while, so I need to turn while(a) { b } into if(a) do { b } while (a), or block Y { loop X { if (!a) break Y; b; branch X; } . After seeing the output of assemblyscript and wasm-opt, I see that it could instead be loop X { if (a) { b ; branch X; } }
Source: learning-wasm.js
2 Javascript vs Wasm#
I’m going to convert this js code to wasm:
function main(seed, N) { const m = 0x7fff; const a = 1103515245; const c = 12345; for (let i = 0; i < N; i++) { seed = (a * seed + c) & m; } return seed; }
(func $loop (param $seed i32) (param $N i32) (result i32) (local $i i32) (local.set $i (i32.const 0)) (block $for_2_end (loop $for_2_start (local.get $i) ;; test !(i < N) (local.get $N) (i32.ge_s) (br_if $for_2_end) (local.get $seed) (i32.const 1103515245) (i32.mul) (i32.const 12345) (i32.add) (i32.const 0x7fff) (i32.and) (local.set $seed) ;; seed = (a * seed + c) & m (local.get $i) (i32.const 1) (i32.add) (local.set $i) ;; i++ (br $for_2_start) ) ) (local.get $seed) )
I also tried AssemblyScript[9]:
export function main(seed: i32, N: i32): i32 { const m: i32 = 0x7fff; const a: i32 = 1103515245; const c: i32 = 12345; for (let i = 0; i < N; i++) { seed = (a * seed + c) & m; } return seed; }
which generated this wasm:
(func $assembly/index/main (param $seed i32) (param $N i32) (result i32) (local $i i32) i32.const 0 local.set $i loop $for-loop|0 local.get $i local.get $N i32.lt_s if i32.const 1103515245 local.get $seed i32.mul i32.const 12345 i32.add i32.const 32767 i32.and local.set $seed local.get $i i32.const 1 i32.add local.set $i br $for-loop|0 end end local.get $seed return )
and that ran in the same speed as my hand-written wasm, 725ms for js and 145ms for wasm in Firefox; 660ms for js and 145ms for wasm in Chrome. My test code doesn’t work in safari yet.
I also tried wasm2c
and ran the c code (with optimizer on) on my machine, and it took 116ms, so wasm wasn’t much slower than native code.
3 Programatically generating wasm#
Since I had been reading Crafting Interpreters, I decided I could use my scanner and parser from there to compile some code to wasm. I decided to keep the scope small by only handling simple arithmetic expressions, and not full expressions, or statements, or declarations. I think this is enough to teach me what I want to know: how to dynamically construct wasm at run time, and run it.
this doesn’t handle parse errors
Tokens:
Evaluated:
Steps:
- Lex: input text into tokens
- Parse: tokens into reverse-polish, then immediately generate assembly instructions
- Wrap the instructions into a module (WAT text format)
- Compile the WAT to WASM (binary format), using the wat2wasm library[10] (Apache v2 license) (their demo[11])
- Load the WASM module at run time
- Import and run the compiled function
curl -O https://webassembly.github.io/wabt/demo/libwabt.js