Skip to content

Type the compiled execution plan without any casts - #258

Merged
SandroMaglione merged 1 commit into
mainfrom
planner-typed-execution-plan
Nov 1, 2026
Merged

SandroMaglione merged 1 commit into
mainfrom
planner-typed-execution-plan

Conversation

@SandroMaglione

Copy link
Copy Markdown
Member

Summary

This is part 1 of the planner/execution-layer cast cleanup and covers executionPlan.ts. A follow-up PR handles planner.ts, invocation.ts, machine.ts, and the finite-model test adapter. There is no behavior change and no public API change.

Casts removed (grep -c "as any" / grep -c "as unknown as"):

File Before After
internal/machine/executionPlan.ts 17 / 1 0 / 0
internal/machine/command.ts 2 / 0 0 / 0

How:

  • Indexed dispatch types. IndexedEvent (the decoded event's _tag), IndexedTransitionContext (what makeIndexedTransitionContext builds, previously any), and IndexedMicrostep (keeps raised events typed for the next dispatch) replace the event: any, context: any, and (selection.context as any).event sites. collectIndexedTransition is generic over its context.
  • Snapshot construction. IndexedSnapshotNode describes the snapshot being built, replacing Record<string, unknown> plus as unknown as AtomicSnapshot and the mutable completed assertion.
  • Targets. Initial targets are narrowed before resolution (initialTarget). After the existing snapshot-or-target check, target is typed, so the eight target as any / unresolvedTarget as any sites go away. The retained unresolved target is initialTarget ?? target, the same value as before: without an initial target, the validated target is the returned target.
  • Conflict resolution. removeConflictingTransitions / sortEvaluatedTransitions in planner.ts are now generic over a minimal ConflictCandidate (the selection.sourcePath, update, and exitPaths they read) instead of the full five-parameter EvaluatedTransition. The indexed planner passes its own representation directly, with no evaluated as any and no result assertion.
  • Commands. SendToTarget / StopTarget in command.ts (derived from Command) type the sendTo / stop collectors in both makeCollector and the indexed collector, so target as any / child as any go away.
  • Startup. MachineInternal.initial is typed as (...args: ReadonlyArray<unknown>) => unknown on the existing toImpl boundary, replacing machine.initial(...inputArgs as any). The two machine as any / event as any arguments to planConfiguration were unnecessary and are dropped.

One assertion remains, on purpose, at a genuine erased boundary, with a comment: the shared initial-target resolver in planner.ts decodes raised events but types them unknown, so initialRaisedEvents is asserted to ReadonlyArray<IndexedEvent> where generic-planner output enters the indexed representation. It is a narrow type assertion, not any.

Emitted JavaScript: I compiled src for base and PR and diffed the output. planner.js is identical. executionPlan.js and command.js differ only by local renames, shorthand properties, and the initialTarget/retainedTarget locals. toImpl(machine).initial replaces machine.initial, and toImpl is an identity function used only at startup.

Changeset

  • Added or updated for a library or package-metadata change: .changeset/typed-execution-plan.md, patch for @typeonce/effect-machine with an "Internal: …" entry (the policy script requires a changeset for any packages/effect-machine/src/ change, and Check public Machine operations against their implementations #223 set the precedent for a patch on internal type-safety work).
  • Not required because this PR does not change a publishable package

Validation

  • pnpm check (92 files / 901 tests, all tstyche suites, pack and consumer checks), including the existing forced generic-versus-indexed differential suites
  • Automated type-performance measurement passed or was not required. Locally, pnpm perf:types showed 0 change in every scenario (public inference is untouched). Type-checking packages/effect-machine itself went from 419,337 to 419,080 instantiations (−257); types went from 91,980 to 92,487 because of the new named interfaces. Check time showed no measurable difference in interleaved runs (base ≈3.3–4.0 s, PR ≈3.0–3.5 s, on a heavily loaded machine).
  • Automated runtime- and memory-performance measurement passed or was not required. I ran pnpm perf:runtime base and PR twice each, alternating, but the host was under load average ~25, so variability reached 20–78% MAD in both directions. That run can't show anything either way. Given the emitted-JS diff above, I expect no runtime change; the CI base-versus-PR comparison is authoritative.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Type performance

Measured with TypeScript 6.0.3 and skipLibCheck=true.

Scenario Base PR Difference
Effect only 55 55 0 (0.0%)
Import effect-machine 55 55 0 (0.0%)
Machine.state (3 children) 2,664 2,664 0 (0.0%)
Machine.make (3 states, 2 events) 8,932 8,932 0 (0.0%)
machine.handle (3 states, 2 transitions) 39,449 39,449 0 (0.0%)
declared transition (10 named branches) 60,402 60,402 0 (0.0%)
atomic construction, guards, and reentry 85,710 85,710 0 (0.0%)
registered invocation (input-taking Effect) 39,819 39,819 0 (0.0%)
machine.handle (depth 24) 194,531 194,531 0 (0.0%)
machine.handle (wide depth 16) 218,238 218,238 0 (0.0%)
machine.handle (parallel/history/choice) 91,642 91,642 0 (0.0%)
machine definition (3 independent implementations) 99,173 99,173 0 (0.0%)
machine exact input/output/error/services 82,941 82,941 0 (0.0%)
execution adapter readiness 71,428 71,428 0 (0.0%)

Marginal instantiations are measured against the matching setup without that API call:

Scenario Base PR Difference
Import effect-machine 0 0 0
Machine.state (3 children) 2,609 2,609 0 (0.0%)
Machine.make (3 states, 2 events) 6,260 6,260 0 (0.0%)
machine.handle (3 states, 2 transitions) 30,517 30,517 0 (0.0%)
declared transition (10 named branches) 51,849 51,849 0 (0.0%)
atomic construction, guards, and reentry 70,370 70,370 0 (0.0%)
registered invocation (input-taking Effect) 32,682 32,682 0 (0.0%)
machine.handle (depth 24) 177,222 177,222 0 (0.0%)
machine.handle (wide depth 16) 201,707 201,707 0 (0.0%)
machine.handle (parallel/history/choice) 79,144 79,144 0 (0.0%)
machine definition (3 independent implementations) 88,492 88,492 0 (0.0%)
machine exact input/output/error/services 70,732 70,732 0 (0.0%)
execution adapter readiness 31,922 31,922 0 (0.0%)
Check times (informational)
Scenario Base PR
Effect only 0.03s 0.03s
Import effect-machine 0.03s 0.03s
Machine.state (3 children) 0.09s 0.09s
Machine.make (3 states, 2 events) 0.16s 0.16s
machine.handle (3 states, 2 transitions) 0.31s 0.31s
declared transition (10 named branches) 0.41s 0.40s
atomic construction, guards, and reentry 0.51s 0.52s
registered invocation (input-taking Effect) 0.31s 0.31s
machine.handle (depth 24) 0.65s 0.63s
machine.handle (wide depth 16) 0.72s 0.71s
machine.handle (parallel/history/choice) 0.52s 0.52s
machine definition (3 independent implementations) 0.55s 0.55s
machine exact input/output/error/services 0.49s 0.49s
execution adapter readiness 0.45s 0.45s

Type instantiations are the comparison metric. Check time varies with runner load and is informational only.

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Runtime performance

Median of 5 independent benchmark processes on AMD EPYC 9V45 96-Core Processor with Node v24.22.1.

Pull request baseline

Scenario Effect Machine
Plan counter transitions 196,002 transitions/s
Drain burst with terminal fence 680,618 increments/s
Drain burst with a change observer 645,388 increments/s
Lookup and send to one child 553,860 increments/s
Start and stop a machine 204,583 machines/s
Start and stop a parent with one child 64,629 families/s
Complete an Effect invocation with tracing enabled 24,437 invocations/s
Complete an Effect invocation with tracing disabled 26,762 invocations/s
Plan transitions through a compound state 177,179 transitions/s
Plan transitions through parallel regions 143,297 transitions/s
Drain burst through a compound state 560,751 events/s
Drain burst through two parallel regions 522,959 events/s
Drain a compound-state burst with a change observer 531,294 events/s

Process runtime reference points

Scenario Effect Machine
Start and stop a raw generic process 57,022 processes/s
Start and stop a raw compiled process 125,125 processes/s
Memory profile Effect Machine
Idle machine 1.9 KiB
Raw generic managed process 11.3 KiB
Raw compiled process 3.2 KiB
Two independent idle machines 3.6 KiB
Idle parent with one child 6.0 KiB
Parent with observed child registry 9.6 KiB
Parent with observed invoked child snapshots 6.9 KiB

Effect Machine change from base

Metric Base Base variability PR PR variability Difference
Plan counter transitions 198,415 transitions/s 1.4% MAD 196,002 transitions/s 0.3% MAD -1.2%
Drain burst with terminal fence 685,102 increments/s 0.5% MAD 680,618 increments/s 0.3% MAD -0.7%
Drain burst with a change observer 646,795 increments/s 0.6% MAD 645,388 increments/s 1.3% MAD -0.2%
Lookup and send to one child 556,109 increments/s 0.6% MAD 553,860 increments/s 0.9% MAD -0.4%
Start and stop a machine 209,336 machines/s 1.5% MAD 204,583 machines/s 0.6% MAD -2.3%
Start and stop a parent with one child 64,296 families/s 0.5% MAD 64,629 families/s 0.5% MAD +0.5%
Complete an Effect invocation with tracing enabled 24,575 invocations/s 0.4% MAD 24,437 invocations/s 1.3% MAD -0.6%
Complete an Effect invocation with tracing disabled 26,798 invocations/s 0.4% MAD 26,762 invocations/s 0.5% MAD -0.1%
Plan transitions through a compound state 177,186 transitions/s 0.5% MAD 177,179 transitions/s 3.1% MAD -0.0%
Plan transitions through parallel regions 141,306 transitions/s 1.4% MAD 143,297 transitions/s 2.8% MAD +1.4%
Drain burst through a compound state 557,232 events/s 1.4% MAD 560,751 events/s 1.0% MAD +0.6%
Drain burst through two parallel regions 524,178 events/s 0.5% MAD 522,959 events/s 1.5% MAD -0.2%
Drain a compound-state burst with a change observer 530,538 events/s 0.6% MAD 531,294 events/s 0.8% MAD +0.1%
Idle machine heap per unit 1.9 KiB 0.1% MAD 1.9 KiB 0.1% MAD +0.0%
Raw generic managed process heap per unit 11.3 KiB 0.0% MAD 11.3 KiB 0.0% MAD +0.0%
Raw compiled process heap per unit 3.1 KiB 0.0% MAD 3.2 KiB 0.0% MAD +1.1%
Two independent idle machines heap per unit 3.6 KiB 0.7% MAD 3.6 KiB 0.3% MAD +0.8%
Idle parent with one child heap per unit 6.0 KiB 0.0% MAD 6.0 KiB 0.1% MAD +0.2%
Parent with observed child registry heap per unit 9.6 KiB 0.0% MAD 9.6 KiB 0.0% MAD -0.0%
Parent with observed invoked child snapshots heap per unit 6.9 KiB 0.2% MAD 6.9 KiB 0.2% MAD -0.0%

Process runtime reference change from base

Metric Base Base variability PR PR variability Difference
Start and stop a raw generic process 56,893 processes/s 0.7% MAD 57,022 processes/s 1.7% MAD +0.2%
Start and stop a raw compiled process 127,372 processes/s 0.3% MAD 125,125 processes/s 2.3% MAD -1.8%

Regression guard

No large, noise-adjusted throughput or heap regressions detected.

Versions and interpretation
  • Effect Machine: 0.46.0

Higher throughput is better; lower heap is better. Variability is the median absolute deviation across independent processes, relative to their median. Small differences on shared GitHub-hosted hardware remain informational; the required guard rejects only large changes beyond the measured noise allowance.

Give indexed dispatch explicit event, transition-context, snapshot-node,
and microstep types, narrow initial targets before resolution, and make
conflict resolution generic over the fields it reads. Command collectors
accept the erased send/stop target types instead of casting unknown values.
The emitted JavaScript is unchanged apart from local renames.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@SandroMaglione
SandroMaglione force-pushed the planner-typed-execution-plan branch from a829583 to 8da00d6 Compare November 1, 2026 09:31
@SandroMaglione
SandroMaglione merged commit 104f98a into main Nov 1, 2026
9 checks passed
@SandroMaglione
SandroMaglione deleted the planner-typed-execution-plan branch November 1, 2026 09:36
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.

1 participant