machines-demo
Interactive in-browser playground for Turing and Post machines.
Live demo: demo.machines.mellonis.ru
Two tabs (Turing, Post) where you write JavaScript that builds a machine — using the published @turing-machine-js/machine and @post-machine-js/machine libraries — and watch it execute on an animated tape. Auto-running demo on first load, manual control of the tape head via a movement/symbol/Apply panel, single-step and paused-auto-step execution, and a log of every command applied.
Running locally
npm install
npm run dev
The dev server prints a URL; open it in a browser.
Scripts
npm run dev # Vite dev server
npm run build # type-check + production build into dist/
npm run preview # preview the built bundle
npm run check # svelte-check + tsc (no emit)
npm run lint # ESLint flat config
Static bundle emitted to dist/. Serve with any static host. The build references hashed assets, so far-future caching is safe.
Tech
- Vite + Svelte 5 (runes mode) + TypeScript
-
CodeMirror 6 via
svelte-codemirror-editor; Lezer-based syntax preflight before Load -
Tabler Icons (SVG
?rawimports) - User code runs inside a Web Worker — terminate-on-timeout sandbox, with
'unsafe-eval'only at the worker level so the worker is the actual security boundary -
@turing-machine-js/machineand@post-machine-js/machine(peer-dependency relationship preserved)
Architecture: two lands
User code and the engine live inside a Web Worker. The main thread holds the UI plus a mirror — a real TuringMachine instance whose tapes shadow the worker's state by replaying every command the worker reports. The two sides only communicate by postMessage, and only plain data crosses.
browser tab
┌────────────────────────────┬────────────────────────────┐
│ MAIN THREAD │ WEB WORKER │
│ (Svelte UI + mirror) │ (user code + engine) │
│ │ │
│ <MachineView> │ new Function(userCode) │
│ Editor / Toolbar / Log │ ↓ │
│ │ user-built machine │
│ mirrorMachine │ + State graph │
│ mirrorTapeBlock │ + TapeBlock / Tapes │
│ rebuilt from │ + runStepByStep gen │
│ TapeSnapshots; │ │
│ replays worker │ │
│ commands one step │ │
│ at a time │ │
└────────────────────────────┴────────────────────────────┘
↕ postMessage
requests: build / step / run / resume / setDebug
responses: built / stepped / ran / paused / error
Crosses the boundary: TapeSnapshot[] (on built / ran / error / paused), per-step Command[] (movement + written symbol), tape alphabets, plus break metadata (state name, current symbols, debugBreak flags) on paused — plain data only.
Never crosses: the user's code, the TuringMachine / State / Reference instances it constructs, and the upstream library singletons (haltState, ifOtherSymbol, the movements Symbols). Identity-checked sentinels wouldn't survive structuredClone, and keeping user code worker-side is what justifies 'unsafe-eval' in CSP — the worker is the actual security boundary.
Layout
src/
├── App.svelte # header + tab nav + popstate routing
├── app.ts # entry; mounts <App>
├── app.css # global tokens + base styles
├── components/
│ ├── MachineView.svelte # per-engine orchestrator (one $state, derived disabled flags)
│ ├── Tape.svelte # virtualized belt with prep-shift slide trick
│ ├── ControlPanel.svelte # L/S/R + alphabet chips + Apply
│ ├── Editor.svelte # CodeMirror wrapper + localStorage persist
│ ├── Log.svelte # entries list (desktop) / latest line (mobile)
│ └── IconButton.svelte # icon + optional label
└── lib/
├── types.ts # Engine, Command, WorkerRequest/Response, ...
├── runner.ts # main-thread worker wrapper, 5s timeout
├── worker.ts # spawns user code via new Function inside worker
├── demoLoop.ts # idle-mode random-command loop
├── autoStep.ts # paused-auto-step controller
├── completions.ts # CodeMirror autocomplete from machine namespace
├── syntaxLinter.ts # Lezer-based syntax-error markers
├── persist.ts # localStorage helpers per engine
├── defaultCode.ts # starter Turing / Post snippets
├── format.ts # describeCommand / formatTape / formatAlphabet
└── icons.ts # Tabler icon namespace