4.2 KiB
WebMetal
WebMetal is a browser-only, backend-free web app for designing custom CPU architectures visually - a Scratch-3-inspired block/flow editor where you wire up registers, ALUs, memory, and control logic; define your own instruction set; write assembly for it; and run or step through programs in a built-in emulator/debugger. Everything exports to (and re-imports from) a single JSON file, with the visual layout preserved.
What WebMetal does
- Design a CPU visually - registers, register files, program counter, flags, ALU, memory, instruction decoder, control unit, and comment notes as draggable, connectable, documentable blocks, with live structural validation (unwired inputs, bus-width mismatches, missing components).
- Define its instruction set - mnemonics, operands, bit-level encodings, flags affected, and execution behavior written as micro-ops, all checked against the designed machine.
- Write assembly for it - a CodeMirror editor whose highlighting and
diagnostics are generated from your ISA, with labels,
.org/.word/.bytedirectives, and inline assemble-on-type errors. - Run and debug - a C++->WebAssembly emulation core with step/run/pause, speed presets, breakpoints in the source gutter, register/flag/memory views (editable while stopped), and a trace log.
- Export/import everything - one versioned
.webmetal.jsondocument (architecture, ISA, programs, layout, docs) with format migrations, plus localStorage autosave. - Learn from bundled examples - three original CPUs in difficulty order (EDU-CORE -> TOY-CPU -> RETRO-8).
Tech stack
| Layer | Choice |
|---|---|
| Frontend | React + TypeScript + Vite (static build, no backend) |
| Graph editor | React Flow (@xyflow/react) |
| Assembly editor | CodeMirror 6 |
| Simulation core | C++20 -> WebAssembly (Emscripten + embind, CMake) |
| State / validation | Zustand / Zod |
| Tests | Vitest (TS), doctest (C++), shared JSON conformance vectors |
| CI | GitHub Actions (single required CI passed aggregate check) |
The execution model is two-layered: the visual graph compiles to a flat
machine model, instruction behaviors are micro-op sequences, and the same
micro-op semantics are implemented twice - a TypeScript reference executor
and the C++/wasm engine - pinned to each other by shared conformance vectors
(conformance/) that run in Vitest, native CTest, and Node-on-wasm.
Repository layout
frontend/ Vite + React + TypeScript app (UI, editors, assembler, model)
wasm/ C++ emulator core, built to WebAssembly (CMake + Emscripten)
examples/ Bundled example CPU projects (.webmetal.json)
conformance/ Micro-op conformance vectors shared by all executors
scripts/ Build helpers (wasm build, Node conformance runner)
.github/ CI workflow
Building
Requires Node.js > 20. From a clean checkout:
cd frontend
npm ci
npm run dev # development server with hot reload
The app runs without the engine (design/ISA/assembler work; the Run view reports the engine as unavailable). To build the emulator core too:
scripts/build-wasm.sh --install # first time: installs pinned emsdk into .emsdk/
scripts/build-wasm.sh # builds into frontend/public/wasm/
Production build (static files in frontend/dist/):
cd frontend
npm run build
npm run preview # serve dist/ locally to inspect it
Checks (all also run in CI):
npm run lint && npm run format:check && npm run typecheck && npm test
Native C++ unit tests (no Emscripten required):
cmake -S wasm -B wasm/build-native && cmake --build wasm/build-native --parallel
ctest --test-dir wasm/build-native --output-on-failure
Deployment
dist/ is a plain static site - any web server or object store can host it.
License
MIT.