Skip to content

Fix SCXML macrostep ordering and type the execution core - #39

Merged
JovaniPink merged 1 commit into
masterfrom
scxml-core-ordering-types
Aug 23, 2026
Merged

Fix SCXML macrostep ordering and type the execution core#39
JovaniPink merged 1 commit into
masterfrom
scxml-core-ordering-types

Conversation

@JovaniPink

@JovaniPink JovaniPink commented Aug 23, 2026

Copy link
Copy Markdown
Owner

Summary

Repairs four reproduced run-to-completion defects in the SCXML execution core and makes algorithm.py strict-mypy clean.

This is layer 1 of 2. The public generic API will be proposed in a separate draft stacked on this branch.

Why

The normative W3C SCXML ordering is exit states, execute transition content in document order, then enter states. A macrostep must also consume one FIFO internal event queue until the machine reaches a stable configuration. See SCXML 3.13, Selecting and Executing Transitions.

The previous implementation reproduced these failures:

  • a handled raised event replaced the remaining internal queue, dropping later events;
  • a set escaped conflict filtering, making simultaneous eventless transition actions nondeterministic;
  • callable top-level final output was evaluated for the done event but the public snapshot exposed the raw HandlerAdapter;
  • algorithm.py relied on a relaxed mypy override and reported 28 errors under standalone strict mypy.

Scope

  • Introduce explicit internal configuration, history, transition-sequence, queue, recursive state-value, and generic result types.
  • Require every algorithm helper to be annotated and enforce strict-equivalent per-module mypy options in pyproject.toml.
  • Preserve one FIFO internal queue across every microstep in a macrostep.
  • Return selected transitions as document-ordered lists after set-based deduplication and conflict checks.
  • Evaluate callable top-level final output exactly once on entry and carry the resolved value into State.output.
  • Prevent out-of-macrostep state construction from exposing a callable or HandlerAdapter as snapshot output.
  • Remove unused states_to_invoke plumbing from the pure algorithm. Actor reconciliation remains the invocation lifecycle owner.
  • Correct the documented configured SCXML suite count from 56 to 57; this does not expand the conformance claim.

Regression coverage

  • Multiple raised events remain FIFO when the first event enables another microstep.
  • An ignored internal event does not discard a later queued event.
  • Parallel eventless transition actions execute in document order across repeated machine construction.
  • Callable top-level output is evaluated once and becomes State.output.
  • Existing invoke lifecycle behavior remains covered by focused actor tests.

Validation

Local validation on Python 3.14.4:

  • poetry run python -m pytest tests/ --ignore=tests/test_scxml.py - 432 passed
  • poetry run python -m pytest tests/test_scxml.py - 57 passed
  • poetry run python -m pytest tests/test_invoke.py tests/test_async_actors.py tests/test_actor_logic.py -q - 34 passed
  • poetry run mypy src/xstate/ - passed
  • poetry run mypy --strict src/xstate/algorithm.py - passed
  • poetry run ruff check src/ tests/ scripts/ docs/examples/ - passed
  • poetry run ruff format --check src/ tests/ scripts/ docs/examples/ - passed
  • poetry check --lock - passed
  • poetry build - passed
  • poetry run python scripts/validate_distribution.py - installed-wheel smoke passed

The pull-request workflow supplies the requested Python 3.13 and 3.14 matrix evidence.

Hosted validation at exact head fff1e69a60c3926d2ffc7ec59ac9b0c67cc1b70d:

  • test (3.13, ubuntu-latest) - passed
  • test (3.14, ubuntu-latest) - passed
  • SCXML smoke - passed
  • code-quality (3.14, ubuntu-latest) - passed

Deliberately out of scope

  • Full SCXML datamodel or executable-content support.
  • Initial-transition and history-transition executable content.
  • Reconciliation of W3C bare-prefix event descriptors with XState wildcard priority.
  • Public API generics, version changes, release publication, or broader conformance claims.

Risk and rollback

The highest-risk surface is transition/action ordering in parallel configurations and queue lifetime across chained internal events. Existing conflict, entry, exit, history, final-state, SCXML, actor, and invoke suites run unchanged alongside the new regressions.

Rollback is the single commit in this branch. No schema, persistence, dependency, or runtime configuration migration is involved.

Review focus

  1. Confirm internal_queue is created once at macrostep start and passed through every microstep.
  2. Confirm transition-content execution only receives order-preserving lists.
  3. Confirm top-level output is evaluated only during final-state entry and never re-evaluated by State.
  4. Confirm removing states_to_invoke does not move invocation ownership out of Actor reconciliation.

Merge order

Merge this PR before the stacked typed-public-api PR. Merge was authorized after exact-head verification.

@JovaniPink
JovaniPink marked this pull request as ready for review August 23, 2026 18:19
@JovaniPink
JovaniPink merged commit 50d200f into master Aug 23, 2026
4 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fff1e69a60

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/xstate/algorithm.py
internal_queue=deque(),
context=context,
event=event,
output=None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve output when transitioning an already-done snapshot

When Machine.transition or a running interpreter receives any event after reaching a top-level final state, this initializes the macrostep output to None; because the final state is not re-entered, that value reaches the new State and replaces the previously resolved output. Thus even an ignored late event changes a completed snapshot from output={...} to output=None; carry the incoming snapshot output into the macrostep or short-circuit completed states.

AGENTS.md reference: AGENTS.md:L73-L74

Useful? React with 👍 / 👎.

Comment thread src/xstate/state.py
Comment on lines +90 to +94
elif final_child is None or callable(final_child.donedata):
# Callable output is resolved by the algorithm at entry time.
# State construction outside a macrostep must never expose the
# internal HandlerAdapter as public snapshot output.
self.output = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restore serialized callable final output

For a final state whose output is callable, serialize_snapshot stores the resolved value, but deserialize_snapshot constructs State without passing that stored output; this new branch therefore restores it as None. A serialize/deserialize round trip now loses callable-derived completion results, so the deserializer needs to supply data["output"] to State.

AGENTS.md reference: AGENTS.md:L72-L72

Useful? React with 👍 / 👎.

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