This is an implementation of the Uxn virtual machine in WebAssembly. It provides a drop-in replacement for the uxn5 vanilla JS core.
The WebAssembly module is generated by a script that generates WebAssembly for the various instruction variants, based on hand-written raw WebAssembly for the base instructions, and does some small local optimizations on the generated code.
Goals of this implementation (in descending order of priority) are:
- Compliance: Follows the Uxn specification to the letter. This includes support for circular stacks and memory, which comes at the cost of speed (and simplicity). Compliance is tested against the Uxn opcode test suite.
- Simplicity: The implementation needs to be understandable and maintainable. When speed optimizations would come at the cost of too much complexity, I don’t do them.
- Speed: Try to be as fast as possible. If implementation of instructions are changed, check the impact on performance of the benchmarks (and microbenchmarks) to understand the impact on speed.
See this blog post for some context, implementation details, and benchmarks.
Uxn.wasm is part of the uxn5 distribution. If you want to install a different version:
- Download
uxn-wasm.js
from the Releases page. - Put
uxn-wasm.js
in thesrc/
dir of uxn5. - Open uxn5
index.html
, and check your browser JavaScript console that it loaded the WebAssembly core.
You can use the core outside of uxn5, as a standalone way to run Uxn programs.
This package ships with extra utilities under the util
submodule to easily run Uxn
programs, including an Uxntal assembler (asm
), and utility devices (e.g. a LogConsole
console device that logs output to console
).
The example below runs a Uxntal program to compute prime numbers below 65536, and writes them to the console.
import { Uxn } from "uxn.wasm";
import { asm, mux, LogConsole } from "uxn.wasm/util";
(async () => {
const uxn = new Uxn();
// Initialize the system with 1 device: a console at device offset 0x10 that
// logs output using `console.log`.
await uxn.init(mux(uxn, { 0x10: new LogConsole() }));
// Assemble the program written in Uxntal assembly language into a binary ROM
// using `asm`, and load it into the core.
uxn.load(
asm(`
( Source: https://git.sr.ht/~rabbits/uxn/tree/main/item/projects/examples/exercises )
|0100 ( -> ) @reset
#0000 INC2k
&loop
DUP2 not-prime ?&skip
DUP2 print/short #2018 DEO
&skip
INC2 NEQ2k ?&loop
POP2 POP2
( flush ) #0a18 DEO
( halt ) #010f DEO
BRK
@not-prime ( number* -- flag )
DUP2 ,&t STR2
( range ) #01 SFT2 #0002 LTH2k ?&fail
&loop
[ LIT2 &t $2 ] OVR2 ( mod2 ) DIV2k MUL2 SUB2 ORA ?&continue
&fail POP2 POP2 #01 JMP2r &continue
INC2 GTH2k ?&loop
POP2 POP2 #00
JMP2r
@print ( short* -- )
&short ( short* -- ) SWP print/byte
&byte ( byte -- ) DUP #04 SFT print/char
&char ( char -- ) #0f AND DUP #09 GTH #27 MUL ADD #30 ADD #18 DEO
JMP2r
`)
);
// Start running at the default offset (0x100)
uxn.eval();
})();
make
make test
make bench
make dev
The tests and benchmarks can now be run from http://localhost:8080.