From 7a94e618aa05c242eb8ce017809f293a5d827900 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Wed, 5 Aug 2026 14:59:49 +0800 Subject: [PATCH] Improve tutorial and correct their claims The pages workflow deploys wasm/ to gh-pages, so the tutorial now has a URL worth linking. README leads Getting Started with it, since it needs no toolchain, and the four paragraphs that restated the example session became comments inside the session itself, which now pastes into "make run" verbatim. wasm/index.html gains the overview it was missing: one paragraph on MUXLEQ as a machine, one on eForth as what makes it usable, and a link to docs/manual.md from both the intro and the last chapter. The page used to call MUXLEQ a one-instruction computer in the intro and 2-instruction machine in the chapter titled "Underneath: two instructions". --- README.md | 142 ++++++++++++++++------------------------- wasm/browser-test.html | 2 +- wasm/forth.js | 2 +- wasm/index.html | 103 ++++++++++++++++++------------ wasm/snake.fth | 10 +-- wasm/stackview.js | 2 +- wasm/test.mjs | 2 +- wasm/tutorial.css | 2 +- 8 files changed, 125 insertions(+), 140 deletions(-) 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.