diff --git a/README.md b/README.md index a38be9a..afd1eb4 100644 --- a/README.md +++ b/README.md @@ -24,102 +24,73 @@ self-hosting eForth image; given a FILE it loads and runs that standalone MUXLEQ image instead: `./build/muxleq image.dec`. ## Introduction -This repository contains a full toolchain for the MUXLEQ architecture, including: +This repository contains a full toolchain for the MUXLEQ architecture: 1. An assembler for the MUXLEQ instruction set. 2. A virtual machine built upon the assembler. -3. A cross-compiler that targets the VM with a version of the eForth programming language. +3. A cross-compiler that targets the VM with a version of the eForth language. 4. `rvopt`, a standalone RV32I-to-MUXLEQ compiler for wide native MUXLEQ images. -The system is self-hosted, meaning the eForth environment can compile new versions of itself from source, -allowing for seamless modification and extension. - -SUBLEQ is a Turing-complete One-Instruction Set Computer (OISC). -While esoteric, its ability to run a high-level language like Forth is a powerful demonstration of computational minimalism. -This project serves as an experimental platform for exploring the execution of high-level languages on a minimal hardware-like foundation. +SUBLEQ is a Turing-complete One-Instruction Set Computer (OISC). Running a +high-level language on one is a demonstration of computational minimalism, and +this project is a platform for exploring how far that goes. The system is +self-hosted: the eForth environment compiles new versions of itself from source, +so it can be modified and extended from within. ## Getting Started -This project requires a C compiler, Gforth, and GNU Make. +[eForth on MUXLEQ](https://sysprog21.github.io/muxleq/) runs this same VM and this +same eForth image in a browser, as an interactive tutorial with a live stack +viewer, a 24x24 display mapped onto Forth memory, and a playable snake. Its text +is adapted from [Easy Forth](https://github.com/ruv/easyforth) by Nick Morgan and +rewritten for a real eForth: the words this system actually has, the throw codes +it actually produces, and a closing chapter on the two instructions everything +above it is built from. The sources are `wasm/`, and a push to the default branch +that touches them republishes the page. + +Building locally needs a C compiler, Gforth, and GNU Make. * macOS: `brew install gforth` * Ubuntu/Debian: `sudo apt-get install gforth build-essential` -Build the VM and start the eForth interpreter: ```shell -$ make run +$ make run # build build/muxleq, then start the eForth interpreter ``` -An example session: +Forth calls its commands "words," and a session is just words applied to a stack: ``` -words -21 21 + . cr -: hello ." Hello, World!" cr ; -hello +words \ list every word in the dictionary +21 21 + . cr \ RPN: push 21, push 21, add, print "42" +: hello ." Hello, World!" cr ; \ define a word +hello \ run it by naming it bye ``` -In Forth, executable commands are called "words." -The `words` command lists all defined functions in the dictionary. -Forth uses Reverse Polish Notation (RPN), so `21 21 + . cr` pushes 21, -then 21, adds them, prints the result, and adds a carriage return. - -New words are defined with `: ;`. -Once defined, the word `hello` can be executed by typing its name. - -### The browser tutorial - -`wasm/` holds an interactive Forth tutorial that runs the same VM and the same -eForth image in a browser, with a live stack viewer, a 24x24 display mapped onto -Forth memory, and a playable snake: - -```shell -$ make wasm-serve # needs emcc; then open http://localhost:8000/ -``` - -The module is about 27 KB and imports nothing from the host, because the -interpreter is shared verbatim with the native VM through -[`muxleq-core.h`](muxleq-core.h). The only difference is the host: a browser tab -cannot block on a keystroke, so the WebAssembly build interprets a bounded slice -and returns, resuming where it left off. `make check-wasm` proves that -rewrite changed nothing, by building the same host natively and diffing its -output against the reference VM, and then checks the page itself: the runtime, -every runnable example in the tutorial, and the editor widget driven by -synthetic keystrokes in headless Chrome. - -The text is adapted from [Easy Forth](https://github.com/ruv/easyforth) by Nick -Morgan, rewritten for a real eForth: it teaches the words this system actually -has, reports errors as the throw codes it actually produces, and ends with a -chapter on the two instructions everything above it is built from. - ### Testing, benchmarking, and internals -- `make check` runs the pre-commit gate: byte-exact 32-bit golden-output tests, - the PTY editor golden, 32-bit eForth smokes, and the self-hosting bootstrap. -- `make check-all` runs `make check` plus wide native-image fuzz, loader rejection, - and ASan/UBSan validation. -- `make check-wasm` validates the WebAssembly build and the browser tutorial. It - skips whatever is missing (emcc, node, headless Chrome) rather than failing. -- `make check-analyze` runs clang `--analyze`, cppcheck, and shellcheck over the C - and shell sources, all of which are clean. Deliberate exceptions carry inline - suppressions with a reason. Part of `check-all`; skips absent tools. -- `rvopt` is a standalone ahead-of-time compiler that lowers an RV32I ELF32/flat binary to a native - MUXLEQ image running on the two ops directly, with no interpreter layer: - `rvopt mux prog > prog.dec` then `./build/muxleq prog.dec`. - See [`docs/rvopt-native-muxleq.md`](docs/rvopt-native-muxleq.md). +- `make check` is the pre-commit gate: byte-exact golden output, the `see` and + PTY-editor goldens, eForth smokes, CSR/timer lowering, RV32I and RTOS coverage, + the WebAssembly build, and the self-hosting bootstrap. `make check-all` adds + wide native-image emission, loader rejection, the prebuilt-release contract + test, differential `rvopt` fuzzing, static analysis (clang `--analyze`, + cppcheck, shellcheck, all clean), and ASan/UBSan. Members whose tools are + absent (emcc, node, headless Chrome) skip rather than fail. `make help` lists + every target; `make bench` times the eForth kernels and rvopt-lowered + DureMark. +- `rvopt` is a standalone ahead-of-time compiler that lowers an RV32I ELF32/flat + binary to a native MUXLEQ image running on the two ops directly, with no + interpreter layer: `rvopt mux prog > prog.dec`, then `./build/muxleq prog.dec`. + Two opt-in flags serve kernel-style code, `--indirect` for computed jump + targets and `--timer` for block-boundary timer interrupts, which is what the + RTOS experiment in [`tests/rv32i/rtos/`](tests/rv32i/rtos) runs on. See + [`docs/rvopt-native-muxleq.md`](docs/rvopt-native-muxleq.md). - RV32I test programs: freestanding RISC-V demos, benchmarks, and the official - rv32ui conformance suite live in [`tests/rv32i/`](tests/rv32i). `make rv32i` - cross-builds the demo, unopt, and DureMark programs into `build/rv32i`; - `make rv32i-check` additionally lowers the demo, unopt, and DureMark programs - with `rvopt mux`, runs them on the VM, and runs the rv32ui conformance suite. - DureMark asserts its deterministic checksum, so it gates rvopt lowering and VM - execution end to end. Both need a bare-metal - `riscv-none-elf-*` toolchain, such as the - [xPack GNU RISC-V toolchain](https://xpack-dev-tools.github.io/riscv-none-elf-gcc-xpack/). - CI builds and runs everything with the xPack toolchain on every push. On the - default branch it also stages just the files the release carries into - `build/rv32i-release`, records their digests in `rv32i/MANIFEST.sha256`, and - publishes that tree as a rolling `rv32i-latest` pre-release, replaced whenever - the staged payload changes; every run additionally uploads the build tree as - the `rv32i-binaries` workflow artifact. Either lets you download prebuilt - images without a cross toolchain installed. + rv32ui conformance suite live in [`tests/rv32i/`](tests/rv32i). DureMark + asserts a deterministic checksum, so it gates rvopt lowering and VM execution + end to end. `make check-rv32i` builds them from source when a bare-metal + `riscv-none-elf-*` toolchain is installed, such as the + [xPack GNU RISC-V toolchain](https://xpack-dev-tools.github.io/riscv-none-elf-gcc-xpack/), + and otherwise falls back to prebuilt images, so no cross toolchain is required. + Those come from the rolling `rv32i-latest` pre-release, which CI republishes + from the default branch whenever the payload changes, with per-file digests in + `rv32i/MANIFEST.sha256`; every run also uploads a `rv32i-binaries` artifact. - [`docs/manual.md`](docs/manual.md) is the reference manual: the instruction set, memory image and self-modifying-operand rules, the build/bootstrap pipeline, the interpreter, and the eForth environment. @@ -183,17 +154,12 @@ while not (pc & 0x80000000): # run until the PC's high bit is set (halt) pc = c # Branch ``` -MUX with constants `0` and `-1` can implement any boolean function: -- AND: Use selector = second operand -- OR: Use selector = ~first operand -- XOR: Combine multiple MUX operations -- NOT: MUX with swapped true/false values - -The above are expensive in pure SUBLEQ (requiring dozens of instructions). +MUX with constants `0` and `-1` implements any boolean function: AND selects on +the second operand, OR on the complement of the first, NOT swaps the true and +false values, and XOR combines several MUX steps. Each takes dozens of +instructions in pure SUBLEQ. A mask of `0` likewise gives a single-instruction +MOVE in place of SUBLEQ's multi-instruction copy sequence. -Setting the mask to 0 makes a single-instruction MOVE, replacing SUBLEQ's -multi-instruction copy sequence. Boolean masking through MUX likewise collapses -bit-twiddling that pure SUBLEQ would build from many subtract-and-branch steps. Because MUX is a *same-lane* selector, though, it cannot move a bit between positions (it cannot shift). That gap is what the native-primitive mechanism below fills. @@ -261,7 +227,7 @@ bootstrap succeeds. `MUXLEQ` is available under a permissive [MIT](https://opensource.org/license/mit)-style license. Use of this source code is governed by a MIT license that can be found -in the [LICENSE](LICENSE) file. +in the [LICENSE](LICENSE) file. It was originally written by [Richard James Howe](https://github.com/howerj). ## Reference diff --git a/wasm/browser-test.html b/wasm/browser-test.html index edf45e1..fdb97ad 100644 --- a/wasm/browser-test.html +++ b/wasm/browser-test.html @@ -100,7 +100,7 @@ await type(canvas, "twice"); check("editors are independent", /\?/.test(transcript(canvas)), "true"); - await type(canvas, ": plot ( colour x y -- ) 24 * + cells graphics + ! ;"); + await type(canvas, ": plot ( color x y -- ) 24 * + cells graphics + ! ;"); await type(canvas, "5 3 4 plot"); const ctx = canvas.querySelector(".canvas").getContext("2d"); const px = canvas.querySelector(".canvas").width / 24; diff --git a/wasm/forth.js b/wasm/forth.js index 0ee219b..4e1840f 100644 --- a/wasm/forth.js +++ b/wasm/forth.js @@ -203,7 +203,7 @@ export class Forth { pump(capMs = 30) { if (this.dead || this.running) return false; this.running = true; - // Defence in depth for the flag itself: every ordinary exit below clears it, + // Defense in depth for the flag itself: every ordinary exit below clears it, // but an exception thrown anywhere in the loop would otherwise leave it set // and every later pump() would return false at the guard above, with the // widget stuck reporting that a program is still running. diff --git a/wasm/index.html b/wasm/index.html index fe40d77..8e3b79e 100644 --- a/wasm/index.html +++ b/wasm/index.html @@ -10,7 +10,7 @@
-

eForth on MUXLEQ

+

eForth on MUXLEQ

A Forth tutorial you can type into, running a real eForth on a two-instruction virtual machine compiled to WebAssembly.

Adapted from Easy Forth by @@ -37,21 +37,39 @@

What you are looking at

Forth is a language with almost no syntax. There are no expressions, no operator precedence, and barely any keywords. There is a stack, and there are -words that do things to it. That is nearly the whole language, which is why it -fits on machines that have no business running a language at all.

- -

The machine underneath this page is one of those. MUXLEQ is a -one-instruction computer: memory is an array of 32-bit cells, and the only thing -it can do is subtract one cell from another and branch if the result went -non-positive. A second form of the same instruction copies bits through a mask. -That is the entire instruction set, and an eForth system is built on top of -it.

- -

The interpreter you are about to type into is the same C program that runs -from a terminal, compiled to WebAssembly, running the same eForth image. It is -about 27 KB of WebAssembly and it imports nothing from the browser. This is not a -Forth-flavoured toy written in JavaScript: when a word misbehaves here it -misbehaves the same way on the command line.

+words that act on it. That is nearly the whole language, which is why it fits on +machines that have no business running a language at all.

+ +

The machine underneath this page is one of those. MUXLEQ is memory, two +instruction forms, and one reserved escape. Memory is a flat array of 32-bit +cells, and an instruction is three consecutive cells. The first form, SUBLEQ, +subtracts one cell from another and branches if the result came out +non-positive, which on its own is enough to compute anything. The second, MUX, +copies bits from one cell to another through a mask, which makes a move a single +instruction and shortens the subtract-and-branch sequences the boolean +operations would otherwise need. One reserved mask address escapes to a native +right shift, the one useful thing neither form can do cheaply. There are no +registers, no call instruction, and no indirect addressing: code that needs a +pointer writes the address into the operand field of the instruction about to +run. The last chapter gives the whole instruction set +in a dozen lines, and the +reference +manual documents the machine, its image format, and how the system is +built.

+ +

eForth is what makes such a machine usable. It is a compact Forth designed to +be brought up on unlikely targets: about two dozen primitives written in machine +code, and everything above them, the interpreter, the compiler, the decompiler, a +screen editor, more than 250 words in all, written in Forth itself. On MUXLEQ +that comes to roughly 6,600 cells. The image also carries the cross compiler that +produced it, so the system can rebuild itself from its own Forth sources, and the +rebuilt image coming out byte-identical is how the project knows the compiler is +correct.

+ +

What you are about to type into is that pair, unchanged: the same C +interpreter that runs from a terminal, compiled to WebAssembly, running the same +eForth image. This is not a Forth-flavored toy written in JavaScript; when a word +misbehaves here it misbehaves the same way on the command line.

Every editor on this page gets its own machine. A word you define in one does not exist in the next, so you can experiment freely. The reset @@ -298,7 +316,7 @@

Defining words

one. Try defining square twice and calling cube.

Type words in any editor to see everything the system knows. -There are several hundred, all of them built out of the same two machine +There are more than 250, all of them built out of the same two machine instructions.

Printing

@@ -457,24 +475,24 @@

Drawing

The editor below has a 24 by 24 display next to it. The display is not a device: it is 576 consecutive cells of ordinary Forth memory, and this page -paints whatever colour numbers it finds in them. graphics pushes +paints whatever color numbers it finds in them. graphics pushes the address of the first cell. Cell 0 is the top left corner, and the rows run left to right.

-

Colour 0 is the background and 1 through 15 are colours. Storing into a cell +

Color 0 is the background and 1 through 15 are colors. Storing into a cell is all it takes:

-
: plot ( colour x y -- ) 24 * + cells graphics + ! ;
+
: plot ( color x y -- ) 24 * + cells graphics + ! ;
 3 5 5 plot

Everything else is ordinary Forth. A horizontal line is a loop over -plot, filling the screen is a loop over that, and a colour ramp is +plot, filling the screen is a loop over that, and a color ramp is two nested loops:

-
: hline ( colour y -- ) 24 0 do 2dup i swap plot loop 2drop ;
-: flood ( colour -- ) 24 0 do dup i hline loop drop ;
+
: hline ( color y -- ) 24 0 do 2dup i swap plot loop 2drop ;
+: flood ( color -- ) 24 0 do dup i hline loop drop ;
 4 flood
 : ramp 24 0 do 24 0 do j i + 15 and i j plot loop loop ;
 ramp
@@ -530,8 +548,8 @@

A game

Underneath: two instructions

Everything on this page, the interpreter, the compiler, the decompiler, the -number formatter, runs on a machine with one instruction and one addressing -mode.

+number formatter, runs on the machine sketched at the top: two instruction +forms, one addressing mode, no indirection.

Memory is an array of 32-bit cells, addressed by cell number. An instruction is three consecutive cells, a, b and c, @@ -548,17 +566,16 @@

Underneath: two instructions

Mem[b] = (Mem[a] & ~m) | (Mem[b] & m)

Two mask addresses are special. Cell 6 always holds zero, so a mask fetched -from it selects all of Mem[a], making the instruction a plain -move, which is the single most common operation in the image. The reserved -address $7FFFFFFE means shift right by one, so shifting costs one -instruction per bit instead of a loop. Input, output and halt are spelled with --1 in the a, b and c field -respectively.

- -

There is no indirect addressing, so a Forth built on this cannot fetch -through a pointer the way you would expect. It gets there by writing the address -into the operand field of the next instruction and then running it. The image -modifies itself constantly, and that is the normal case rather than a trick.

+from it selects all of Mem[a], making the instruction a plain move, +which pure SUBLEQ needs a multi-instruction sequence for. The reserved address +$7FFFFFFE means shift right by one, turning a bit-serial loop into +one instruction. Input, output and halt are spelled with -1 in the +a, b and c field respectively.

+ +

With no indirect addressing, a Forth built on this cannot fetch through a +pointer the way you would expect. It gets there by writing the address into the +operand field of the next instruction and then running it. The image modifies +itself constantly, and that is the normal case here rather than a trick.

You can see the result. see decompiles a word back into the words it was built from:

@@ -577,13 +594,15 @@

Underneath: two instructions

29020 | * ;
-

The entire eForth image is about 6,600 cells, or 26 KB, and it contains its -own cross compiler: the system can rebuild itself from the Forth sources in -forth/. The WebAssembly module that runs it is 27 KB and imports -nothing at all from the browser, because a machine this simple needs nothing -from its host except a byte in and a byte out.

+

A machine this simple asks nothing of its host but a byte in and a byte out, +which is why the WebAssembly module running all of the above imports nothing at +all from the browser. The +reference +manual has the rest: the image layout, the self-modifying-operand rules, and +the bootstrap that rebuilds the image from the Forth sources in +forth/.

-

Try here . to see how much of it you have used, and +

Try here . to see how much of the image you have used, and words to see what you got for it.