The failure mode
Two places must agree about what a statechart class body may contain:
StateMachineMetaclass.add_from_attributes for a StateChart subclass, and
NestedStateFactory.__new__ for a State.Compound / State.Parallel. They diverge silently.
#643 is exactly that: the Event class and the @<source>.to(<target>) decorator worked at the
top level and were dropped inside a nested body for as long as compound states have existed. The
next declaration form added will do it again.
#645 first tried to close this with a shared reader: a module, a seven-method Protocol under
TYPE_CHECKING, and two implementations. It was reverted on the branch. The mechanism aimed at
the right target but the two consumers need different amounts of it (the top level needs seven
forms, the nested body three), which showed as an aliased on_history on one side and an empty
on_other on the other. All three defects found in review lived in the nested implementation.
Step 1: one reader, owned by the metaclass
The nested body stops translating. NestedStateFactory resolves only the structural forms it
needs to build the State (States, HistoryState, State) and stashes the rest raw. The
metaclass reads that stash from add_state, which already walks the tree and already holds
cls:
# state.py
return State(name=name, states=states, history=history, _body=body, **inherited_kwargs)
# factory.py
def add_state(cls, id, state):
state._set_id(id)
...
cls._read_body(state._body, owner=state)
for event in state.transitions.unique_events:
cls.add_event(event)
for substate in state.states:
cls.add_state(substate.id, substate)
What it buys:
The split that remains is "structure now, behaviour later", and #643 lived entirely in the
behaviour half.
Watch out: _unpack_builders_callbacks currently runs after _collect_class_listeners.
Moving the callback setattr earlier may change what the listener collector sees. The 2161-test
suite plus the W3C SCXML suite is the net.
Step 2: a root state
factory.py already carries the TODO: Experiment with the IDEA of a root state. With the
StateChart class body being the body of a root compound state, the top level stops being a
special case at all.
It is smaller than it looks. Of the .states uses across the package, almost all are
state.states (substates) and do not change. cls.states can stay the very same States object,
now hanging off the root: States already provides append, __iter__, __len__ and
__getitem__, which is everything State.__init__ and the engine use. The engine, the diagram
renderers, io/ and graph.py stay untouched.
Sketch:
# factory.py __init__
cls.states: States = States()
cls.root = State(id=cls.id, name=cls.name)
cls.root.states = cls.states
...
cls._initials_by_document_order(list(cls.states), parent=cls.root)
cls.root.parallel = root_only_has_parallels
# engines/base.py _initial_transitions
empty_state = State()
empty_state.parent = self.sm.root
That last line is what keeps the root out of the configuration:
find_lcca([empty_state, shire]) then returns the root, the transition domain becomes the root,
and shire.ancestors(parent=root) stops before it. Today empty_state is an orphan, the LCCA is
None and the entry set climbs to the top. The root state replaces a pseudo-state the engine
already has, just unparented.
The initial-state validation (len(initials) != 1 and not root_only_has_parallels) is the generic
"a compound has one initial, a parallel does not need one" rule applied to the root, so it stops
being a separate branch.
Three traps found while sizing it, none of them verified by running code:
- A parallel root leaks into the configuration.
find_lcca filters ancestors by
anc.is_compound, and is_compound is bool(states) and not parallel. For a machine whose
top level is all parallel regions the LCCA returns None and the root gets entered. The
docstring of find_lcca already hints at the fix, it says "filtering for compound or SCXML
elements" while the code only checks is_compound.
states_map and setattr. The root must not go through add_state, or it pollutes
states_map[root.value] and shadows the lowercased class name as a class attribute.
- Final states. With a root there is the real SCXML termination condition ("the root is in a
final state"). Keep the current final_states list in the first pass or the diff doubles.
Step 2 touches the construction path of every machine and the initial entry of every engine, so
it wants its own PR with the W3C SCXML suite as the acceptance criterion.
Related
Fixes #656 and #653. Removes the cause of #643.
When fixing: docs/statecharts.md and docs/processing_model.md describe the current
top-level/nested split; the notes in docs/events.md and docs/releases/3.2.2.md about nested
limitations come out, and the open release notes need an entry.
The failure mode
Two places must agree about what a statechart class body may contain:
StateMachineMetaclass.add_from_attributesfor aStateChartsubclass, andNestedStateFactory.__new__for aState.Compound/State.Parallel. They diverge silently.#643 is exactly that: the
Eventclass and the@<source>.to(<target>)decorator worked at thetop level and were dropped inside a nested body for as long as compound states have existed. The
next declaration form added will do it again.
#645 first tried to close this with a shared reader: a module, a seven-method
ProtocolunderTYPE_CHECKING, and two implementations. It was reverted on the branch. The mechanism aimed atthe right target but the two consumers need different amounts of it (the top level needs seven
forms, the nested body three), which showed as an aliased
on_historyon one side and an emptyon_otheron the other. All three defects found in review lived in the nested implementation.Step 1: one reader, owned by the metaclass
The nested body stops translating.
NestedStateFactoryresolves only the structural forms itneeds to build the
State(States,HistoryState,State) and stashes the rest raw. Themetaclass reads that stash from
add_state, which already walks the tree and already holdscls:What it buys:
construction rather than by protocol discipline.
setattrtime, which is what Callback names collide between sibling nested state bodies #653 needs._unpack_builders_callbackslikely disappears, removing a construction phase.clsto bind attributes on.# noqa: C901added toNestedStateFactory.__new__in fix: support event declarations inside State.Compound bodies #645 goes away.The split that remains is "structure now, behaviour later", and #643 lived entirely in the
behaviour half.
Watch out:
_unpack_builders_callbackscurrently runs after_collect_class_listeners.Moving the callback
setattrearlier may change what the listener collector sees. The 2161-testsuite plus the W3C SCXML suite is the net.
Step 2: a root state
factory.pyalready carries theTODO: Experiment with the IDEA of a root state. With theStateChartclass body being the body of a root compound state, the top level stops being aspecial case at all.
It is smaller than it looks. Of the
.statesuses across the package, almost all arestate.states(substates) and do not change.cls.statescan stay the very sameStatesobject,now hanging off the root:
Statesalready providesappend,__iter__,__len__and__getitem__, which is everythingState.__init__and the engine use. The engine, the diagramrenderers,
io/andgraph.pystay untouched.Sketch:
That last line is what keeps the root out of the configuration:
find_lcca([empty_state, shire])then returns the root, the transition domain becomes the root,and
shire.ancestors(parent=root)stops before it. Todayempty_stateis an orphan, the LCCA isNoneand the entry set climbs to the top. The root state replaces a pseudo-state the enginealready has, just unparented.
The initial-state validation (
len(initials) != 1 and not root_only_has_parallels) is the generic"a compound has one initial, a parallel does not need one" rule applied to the root, so it stops
being a separate branch.
Three traps found while sizing it, none of them verified by running code:
find_lccafilters ancestors byanc.is_compound, andis_compoundisbool(states) and not parallel. For a machine whosetop level is all parallel regions the LCCA returns
Noneand the root gets entered. Thedocstring of
find_lccaalready hints at the fix, it says "filtering for compound or SCXMLelements" while the code only checks
is_compound.states_mapandsetattr. The root must not go throughadd_state, or it pollutesstates_map[root.value]and shadows the lowercased class name as a class attribute.final state"). Keep the current
final_stateslist in the first pass or the diff doubles.Step 2 touches the construction path of every machine and the initial entry of every engine, so
it wants its own PR with the W3C SCXML suite as the acceptance criterion.
Related
Fixes #656 and #653. Removes the cause of #643.
When fixing:
docs/statecharts.mdanddocs/processing_model.mddescribe the currenttop-level/nested split; the notes in
docs/events.mdanddocs/releases/3.2.2.mdabout nestedlimitations come out, and the open release notes need an entry.