Background
PostMachine constructs a separate hopper State for each subroutine, named after the subroutine itself (e.g., rightToBlank). The hopper's only transition forwards to the first instruction ([ifOtherSymbol]: { nextState: rightToBlank::1 }). call('foo') wraps the hopper:
// commands.ts ~107
const state = subroutineInitialStates[subroutineName] // ← the hopper
.withOverriddenHaltState(new State({
[ifOtherSymbol]: { nextState: nextInstruction },
}, continuationName));
This dates from v6.1.0's instruction-derived naming work (per packages/machine/CLAUDE.md).
The asymmetry
| Level |
Entry-point structure |
| Top-level program |
idle -. enter .-> 1 — idle sentinel points directly at the first numbered instruction. No hopper. |
Subroutine foo |
caller → wrapper → call ==> foo (hopper) → "[*]" → foo::1 — extra hopper hop before the first instruction. |
Surfaced while writing the callable-subtree visualization spec for turing#174 — the new diagram model makes the asymmetry visually obvious (the hopper appears as a bare inside the subtree, adding an extra node between the wrapper's call arrow and the first body instruction).
Proposed change
Drop the hopper. call('foo') directly wraps foo::1:
const state = subroutineFirstInstruction[subroutineName] // foo::1
.withOverriddenHaltState(continuation);
subroutineInitialStates[name] either renamed (to subroutineFirstInstruction) or eliminated as redundant with the existing instruction-state cache.
What changes
Composite wrapper name: foo(continuation) → foo::1(continuation). Slightly less readable but accurately reflects the bare's identity.
Per-subroutine state count: -1 State per subroutine (the hopper). Small memory + GraphNode count win, especially for libraries with many subroutines.
Symmetry: top-level and subroutine both have a "first numbered instruction is the entry" model. Reads consistently across nesting depths.
Engine emit (under turing#174's callable-subtree model): subtree_foo::1 contains foo::1, foo::2, halt marker. Wrapper [[foo::1(1~2)]] calls into the subtree's foo::1 bare directly. One less node per subroutine in the diagram.
Trade-offs / concerns
-
Composite-name readability. foo(continuation) is more "subroutine-call-like" than foo::1(continuation). The latter accurately names the bare but obscures the subroutine name. Mitigation: PostMachine could still expose subroutineName as a separate field on the wrapper for display purposes (e.g., in summarizePostMachine).
-
Breakpoint registry / instruction-derived paths. PostMachine's debug lockdown registers each per-instruction State separately. The hopper had its own registry entry under the subroutine's name. Dropping it removes that registry entry — paths that referenced the hopper (pm.setBreakpoint("foo"), etc.) would need to target "foo::1" instead. Documented migration needed.
-
Backward compat. Diagram emit shape changes (one less node per subroutine, different composite-name). Tests that pin state names would need updates. summarize* output may differ. The states.md artifacts for library-binary-numbers would regenerate.
-
stateAt(path) lookup. Currently pm.stateAt('foo') returns the hopper. Post-change, what does it return? The first instruction (foo::1)? Or throw? Spec a clear migration.
Alternative (briefly considered)
Render the hopper as a visualization-only sentinel (stadium shape s([…]) like idle) without dropping it. Preserves the runtime State but makes the asymmetry meaningful (different shape = different role). Rejected as a separate engine-spec change; the symmetry argument here is structural, not just visual.
Scope
- Single-package change in
@post-machine-js/machine.
- v6.x major or v7.x — likely needs a major version bump (breaking change to wrapper composite names + breakpoint paths + diagram shape).
- Coordinate with turing#174 — once both ship, the PostMachine subroutine diagram becomes clean and symmetric with top-level.
Related
- turing-machine-js#174 — callable-subtree visualization (where this asymmetry first surfaced visibly).
- post-machine-js v6.1.0 work that introduced instruction-derived names.
Background
PostMachine constructs a separate hopper State for each subroutine, named after the subroutine itself (e.g.,
rightToBlank). The hopper's only transition forwards to the first instruction ([ifOtherSymbol]: { nextState: rightToBlank::1 }).call('foo')wraps the hopper:This dates from v6.1.0's instruction-derived naming work (per
packages/machine/CLAUDE.md).The asymmetry
idle -. enter .-> 1— idle sentinel points directly at the first numbered instruction. No hopper.foocaller → wrapper → call ==> foo (hopper) → "[*]" → foo::1— extra hopper hop before the first instruction.Surfaced while writing the callable-subtree visualization spec for turing#174 — the new diagram model makes the asymmetry visually obvious (the hopper appears as a bare inside the subtree, adding an extra node between the wrapper's
callarrow and the first body instruction).Proposed change
Drop the hopper.
call('foo')directly wrapsfoo::1:subroutineInitialStates[name]either renamed (tosubroutineFirstInstruction) or eliminated as redundant with the existing instruction-state cache.What changes
Composite wrapper name:
foo(continuation)→foo::1(continuation). Slightly less readable but accurately reflects the bare's identity.Per-subroutine state count: -1 State per subroutine (the hopper). Small memory + GraphNode count win, especially for libraries with many subroutines.
Symmetry: top-level and subroutine both have a "first numbered instruction is the entry" model. Reads consistently across nesting depths.
Engine emit (under turing#174's callable-subtree model):
subtree_foo::1containsfoo::1,foo::2, halt marker. Wrapper[[foo::1(1~2)]]calls into the subtree'sfoo::1bare directly. One less node per subroutine in the diagram.Trade-offs / concerns
Composite-name readability.
foo(continuation)is more "subroutine-call-like" thanfoo::1(continuation). The latter accurately names the bare but obscures the subroutine name. Mitigation: PostMachine could still exposesubroutineNameas a separate field on the wrapper for display purposes (e.g., insummarizePostMachine).Breakpoint registry / instruction-derived paths. PostMachine's debug lockdown registers each per-instruction State separately. The hopper had its own registry entry under the subroutine's name. Dropping it removes that registry entry — paths that referenced the hopper (
pm.setBreakpoint("foo"), etc.) would need to target"foo::1"instead. Documented migration needed.Backward compat. Diagram emit shape changes (one less node per subroutine, different composite-name). Tests that pin state names would need updates.
summarize*output may differ. Thestates.mdartifacts forlibrary-binary-numberswould regenerate.stateAt(path)lookup. Currentlypm.stateAt('foo')returns the hopper. Post-change, what does it return? The first instruction (foo::1)? Or throw? Spec a clear migration.Alternative (briefly considered)
Render the hopper as a visualization-only sentinel (stadium shape
s([…])likeidle) without dropping it. Preserves the runtime State but makes the asymmetry meaningful (different shape = different role). Rejected as a separate engine-spec change; the symmetry argument here is structural, not just visual.Scope
@post-machine-js/machine.Related