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, @post-machine-js/machine, and @turing-machine-js/visuals (highlight + graph-indexing surface) 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, a log of every command applied, and clipboard copy/paste of the tape-block state for sharing or restoring snapshots.
npm install
npm run dev
The dev server prints a URL; open it in a browser.
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
npm test # Vitest one-shot (runner / helper tests)
npm run test:watch # Vitest watch mode
npm run test:coverage # Vitest with v8 coverage; output in coverage/
npm run test:e2e # Playwright E2E (Chromium; runs `vite preview` automatically)
npm run test:e2e:ui # Playwright interactive mode for local debugging
Static bundle emitted to dist/. Serve with any static host. The build references hashed assets, so far-future caching is safe.
svelte-codemirror-editor; Lezer-based syntax preflight before Load?raw imports)'unsafe-eval' only at the worker level so the worker is the actual security boundary@turing-machine-js/machine and @post-machine-js/machine (peer-dependency relationship preserved); @turing-machine-js/visuals for the highlight + graph-indexing surface that drives MachineGraph.svelte's breakpoint dots, pulse animations, frame-active marks, and the engine edge-label log notationUser 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 pause metadata (state name, current symbols, the pause: {side?, cause} descriptor) 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.
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)
│ ├── TapesStack.svelte # multi-tape stack with shared head-thread
│ ├── Tape.svelte # virtualized belt with prep-shift slide trick
│ ├── ControlPanel.svelte # L/S/R + alphabet chips + Apply
│ ├── Toolbar.svelte # Build/Step/Run/Stop + with-pause + examples menu
│ ├── Toolbar.test.ts # Vitest suite for Toolbar — 5 topic groups
│ ├── Editor.svelte # CodeMirror wrapper + localStorage persist
│ ├── Log.svelte # entries list (desktop) / latest line (mobile)
│ └── IconButton.svelte # icon + optional label
└── lib/
├── types.ts # Engine, Command, Alphabets, WorkerRequest/Response (TapeSnapshot + Graph imported from @turing-machine-js/{visuals,machine})
├── caps.ts # numeric caps: VIEWPORT_WIDTH, MAX_STEPS, WORKER_TIMEOUT_MS, MAX_TAPES
├── machineRunner.ts # main-thread worker wrapper; per-segment timeout; injected workerFactory
├── machineRunner.test.ts # Vitest suite for MachineRunner — protocol-shape / timer / pending / error
├── machineWorker.ts # spawns user code via new Function inside worker
├── workerHelpers.ts # pure helpers extracted from machineWorker (movementCode, commandsFromYield, snapshot*, expectPhase, armStepAfter)
├── workerHelpers.test.ts # Vitest suite for workerHelpers — 5 topic groups
├── testUtils.ts # FakeWorker + makeFakeFactory test helpers
├── log.ts # log-entry types + helpers
├── logStore.svelte.ts # per-MachineView log store — buffer + throttled view + overflow cap
├── logStore.test.ts # Vitest suite for LogStore
├── initialBoot.ts # pure helper — engine-page boot priority (?example > ?snippet > localStorage > default)
├── initialBoot.test.ts # Vitest suite for initialBoot
├── completions.ts # CodeMirror autocomplete from machine namespace
├── syntaxLinter.ts # Lezer-based syntax-error markers
├── persist.ts # localStorage helpers per engine
├── tapeSnapshot.ts # serialize/parse tape-block snapshots for copy+paste
├── tapeSnapshot.test.ts # Vitest suite for tapeSnapshot
├── defaultCode.ts # starter Turing / Post snippets
├── format.ts # LogEntry assemblers (commandsEntry / tapesEntry); per-step string rendering from @turing-machine-js/visuals
├── icons.ts # Tabler icon namespace
└── theme.svelte.ts # theme (light / dark) state + matchMedia watcher
e2e/
└── cold-start.spec.ts # Playwright E2E — 4 cold-start scenarios
playwright.config.ts # Chromium project; webServer = vite preview