Skip to content

feat(blocks): a block is a thing you can run, ask about, and copy - #74

Merged
thethinkmachine merged 1 commit into
mainfrom
feat/block-runs-and-scoped-statemate
Sep 5, 2026
Merged

feat(blocks): a block is a thing you can run, ask about, and copy#74
thethinkmachine merged 1 commit into
mainfrom
feat/block-runs-and-scoped-statemate

Conversation

@thethinkmachine

Copy link
Copy Markdown
Owner

Drilling into a block changed what you could see and nothing about what you could do. Pressing play inside CPU/ALU/add ran the CPU — and if control never reached the adder on that word, you learned nothing about the adder. StateMate was handed all three thousand states while eight were on screen. The clipboard was the one door onto a canvas that never asked what machine it was landing on.

Running one block

js/run-scope.js. "Run this block on its own" and "run the whole machine but stop when it reaches this block" look like two features and are one — a run boundary, answered by the same test on the same step.

  • The player enforces it, never the machine layer. Teaching twenty-five simulators to halt at a subtree would be twenty-five ways to disagree; stepAt was already the bounds check, so stepFwd, stepToEnd and the autoplay tick needed nothing taught about it. On a streaming run the steps past the boundary are never computed.
  • The scan reads the steps array and never pulls. Answered by walking the cursor, "how far may the reader go" would materialize every step on the first frame — draining a streaming machine to maxTmSteps, exactly the frozen tab lazy execution exists to prevent.
  • runStartId() is the one declaration the machine layer reads. App.startId stays the machine's start: it draws the start arrow, it is q₀ in the formal definition, it is what every exporter writes. The test asserts against the source that App.startId appears nowhere under js/machines/.
  • A block has no verdict, so it does not borrow one. The answer is which exit control left by; an undeclared way out is reported as one rather than rounded to the nearest label.
  • Deciding is a different question. decideMachine() lifts the override, so the Test Words table, the Language panel and StateMate's verification all still answer about the machine.

The trace log

Its own card, and the rest of the run reachable from it. "The rest is gone" is a different claim from "the rest is not drawn" — the elided line is now a button, and an expansion lasts only while the cursor is still, which is what keeps the quadratic from coming back.

A scoped turn

Saying where the reader is standing was half an answer: machineToSpec() still sent every state at every depth. scopedSource() cuts the machine to one block and everything under it.

  • A cut without its boundary is a disconnected fragment, so spec.scope states the entry, exits and crossings as prose beside the machine — validateSpec never reads it back.
  • The write half is the dangerous one, and it was silent and total. A spec naming only the block's states, diffed against the whole machine, is an edit that removed everything outside it — asking a question about the adder would have deleted the processor around it in one undoable step. keepOutsideScope() bounds the diff.
  • Agentic mode was the one route past the guard: the model was prompted with the block while the session compiled against the whole machine.

What else a block now survives

  • Crossings: a port describes the wiring, not the record. Four kinds of edge were invisible from inside a block, every one silently; an undeclared boundary is drawn in --orange — a finding, not a fault.
  • Notes: note.scope. A note lives on one level. Absent means the top, so no serializer changed and there is no migration.
  • Clipboard: copy is the one gesture that crosses machines, so it is the one that has to ask. transitionShapeRefusal reads transitionFields — the app's one declaration of what a transition carries — so DFA/NFA/ε-NFA and the four tape machines still move freely, and an MTM's read tuple does not land on a TM.
  • The large-machine profile weighs the level you are standing in, at every depth. Twelve boxes over 4800 states measured 6.4ms of an 8.3ms repaint with the profile off throughout, because a box is not one node.

Testing

npm test1946 pass, 0 fail. Seven new files: block-run, block-crossings, block-clipboard, blocks-statemate, statemate-scope, trace-log, note-scope.

The invariant everything rests on: a block run is step-for-step the whole-machine run from that point, driven through stepFwd rather than stepToEnd — which is what let the last one ship.

🤖 Generated with Claude Code

Drilling into a block changed what you could see and nothing about what
you could do. Pressing play inside CPU/ALU/add ran the CPU, and if
control never reached the adder on that word you learned nothing about
the adder. StateMate was handed all three thousand states while eight
were on screen. The clipboard was the one door left unlocked.

- js/run-scope.js: a run boundary. "Run this block on its own" and "run
  the whole machine but stop when it enters this block" are one test on
  one step, enforced by the player's cursor rather than by twenty-five
  simulators. runStartId() is the one declaration the machine layer
  reads; App.startId stays the machine's start.

- The trace log is its own card, and the rest of the run is reachable
  from it: the elided line is a button, and an expansion lasts only
  while the cursor is still.

- scopedSource(): inside a block, the subject is that block plus its
  boundary. compileSpec takes a scope, so a scoped edit leaves the
  machine around the block standing rather than deleting it silently.

- Ports describe the wiring, not the record: every crossing gets a tab,
  an undeclared one in orange. A note lives on one level (note.scope).

- Copy/cut/paste across machines asks whether the transition shape is
  one this machine reads, and whether it can have blocks at all.

- The profile weighs the level you are standing in, at every depth: a
  large machine hidden inside eight boxes is still a large machine.

Tests: block-run, block-crossings, block-clipboard, blocks-statemate,
statemate-scope, trace-log, note-scope. 1946 pass.
Copilot AI lite review requested due to automatic review settings September 5, 2026 18:39
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@thethinkmachine
thethinkmachine merged commit eae84c4 into main Sep 5, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants