From 92022c50cdf3411697b8ca596700c74983237584 Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Thu, 21 Aug 2025 05:31:23 -0600 Subject: [PATCH 1/6] Updates feature detector to support conditionals --- lib/sc/feature_detector.ex | 4 ++-- test/passing_tests.json | 7 +++++-- test/sc/feature_detector_test.exs | 5 ++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/sc/feature_detector.ex b/lib/sc/feature_detector.ex index 7d79e0f..41d8ded 100644 --- a/lib/sc/feature_detector.ex +++ b/lib/sc/feature_detector.ex @@ -53,8 +53,8 @@ defmodule SC.FeatureDetector do initial_attributes: :supported, initial_elements: :supported, - # Conditional features (unsupported) - conditional_transitions: :unsupported, + # Conditional features (supported) + conditional_transitions: :supported, # Data model features (unsupported) datamodel: :unsupported, diff --git a/test/passing_tests.json b/test/passing_tests.json index 7d87b4a..e7bed99 100644 --- a/test/passing_tests.json +++ b/test/passing_tests.json @@ -5,11 +5,14 @@ "test/sc/**/*_test.exs", "test/mix/**/*_test.exs" ], - "last_updated": "2025-08-18", + "last_updated": "2025-08-21", "scion_tests": [ "test/scion_tests/basic/basic0_test.exs", "test/scion_tests/basic/basic1_test.exs", "test/scion_tests/basic/basic2_test.exs", + "test/scion_tests/cond_js/test0_test.exs", + "test/scion_tests/cond_js/test1_test.exs", + "test/scion_tests/cond_js/test2_test.exs", "test/scion_tests/default_initial_state/initial1_test.exs", "test/scion_tests/default_initial_state/initial2_test.exs", "test/scion_tests/documentOrder/documentOrder0_test.exs", @@ -35,4 +38,4 @@ "test/scion_tests/parallel_interrupt/test9_test.exs" ], "w3c_tests": [] -} +} \ No newline at end of file diff --git a/test/sc/feature_detector_test.exs b/test/sc/feature_detector_test.exs index d43be49..a96b086 100644 --- a/test/sc/feature_detector_test.exs +++ b/test/sc/feature_detector_test.exs @@ -183,11 +183,10 @@ defmodule SC.FeatureDetectorTest do end test "fails validation for unsupported features" do - mixed_features = MapSet.new([:basic_states, :datamodel, :conditional_transitions]) + mixed_features = MapSet.new([:basic_states, :datamodel]) assert {:error, unsupported} = FeatureDetector.validate_features(mixed_features) assert MapSet.member?(unsupported, :datamodel) - assert MapSet.member?(unsupported, :conditional_transitions) refute MapSet.member?(unsupported, :basic_states) end @@ -207,12 +206,12 @@ defmodule SC.FeatureDetectorTest do assert registry[:basic_states] == :supported assert registry[:event_transitions] == :supported assert registry[:compound_states] == :supported + assert registry[:conditional_transitions] == :supported assert registry[:parallel_states] == :supported assert registry[:final_states] == :supported # Unsupported features assert registry[:datamodel] == :unsupported - assert registry[:conditional_transitions] == :unsupported assert registry[:onentry_actions] == :unsupported assert registry[:send_elements] == :unsupported assert registry[:send_idlocation] == :unsupported From e73b52a4ac57f28d8981139491b435bc76d0b52a Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Thu, 21 Aug 2025 05:49:04 -0600 Subject: [PATCH 2/6] Adds eventless transitions --- lib/sc/feature_detector.ex | 8 + lib/sc/interpreter.ex | 124 ++++++++++++-- .../eventless_transitions_test.exs | 151 ++++++++++++++++++ 3 files changed, 273 insertions(+), 10 deletions(-) create mode 100644 test/sc/interpreter/eventless_transitions_test.exs diff --git a/lib/sc/feature_detector.ex b/lib/sc/feature_detector.ex index 41d8ded..d765256 100644 --- a/lib/sc/feature_detector.ex +++ b/lib/sc/feature_detector.ex @@ -55,6 +55,7 @@ defmodule SC.FeatureDetector do # Conditional features (supported) conditional_transitions: :supported, + eventless_transitions: :supported, # Data model features (unsupported) datamodel: :unsupported, @@ -257,6 +258,7 @@ defmodule SC.FeatureDetector do defp detect_single_transition_features(features, %Transition{} = transition) do features |> add_if_has_event(transition) + |> add_if_eventless(transition) |> add_if_has_cond(transition) |> add_if_targetless(transition) |> add_if_internal(transition) @@ -268,6 +270,12 @@ defmodule SC.FeatureDetector do defp add_if_has_event(features, _transition), do: features + defp add_if_eventless(features, %Transition{event: event}) when is_nil(event) do + MapSet.put(features, :eventless_transitions) + end + + defp add_if_eventless(features, _transition), do: features + defp add_if_has_cond(features, %Transition{cond: cond}) when not is_nil(cond) do MapSet.put(features, :conditional_transitions) end diff --git a/lib/sc/interpreter.ex b/lib/sc/interpreter.ex index d89e4ce..4e8a4c2 100644 --- a/lib/sc/interpreter.ex +++ b/lib/sc/interpreter.ex @@ -20,6 +20,9 @@ defmodule SC.Interpreter do state_chart = StateChart.new(optimized_document, get_initial_configuration(optimized_document)) + # Process any eventless transitions after initialization + state_chart = process_eventless_transitions(state_chart) + # Log warnings if any (TODO: Use proper logging) if warnings != [], do: :ok {:ok, state_chart} @@ -56,7 +59,12 @@ defmodule SC.Interpreter do new_config = execute_transitions(state_chart.configuration, transitions, state_chart.document) - {:ok, StateChart.update_configuration(state_chart, new_config)} + state_chart = StateChart.update_configuration(state_chart, new_config) + + # Process any eventless transitions after the event + state_chart = process_eventless_transitions(state_chart) + + {:ok, state_chart} end end @@ -87,6 +95,37 @@ defmodule SC.Interpreter do # Private helper functions + # Process eventless transitions until stable configuration is reached + defp process_eventless_transitions(%StateChart{} = state_chart) do + process_eventless_transitions(state_chart, 0) + end + + # Recursive helper with cycle detection (max 100 iterations) + defp process_eventless_transitions(%StateChart{} = state_chart, iterations) when iterations >= 100 do + # Prevent infinite loops - return current state + state_chart + end + + defp process_eventless_transitions(%StateChart{} = state_chart, iterations) do + eventless_transitions = find_eventless_transitions(state_chart) + + case eventless_transitions do + [] -> + # No more eventless transitions - stable configuration reached + state_chart + + transitions -> + # Execute eventless transitions + new_config = + execute_transitions(state_chart.configuration, transitions, state_chart.document) + + new_state_chart = StateChart.update_configuration(state_chart, new_config) + + # Continue processing until stable (recursive call) + process_eventless_transitions(new_state_chart, iterations + 1) + end + end + defp get_initial_configuration(%Document{initial: nil, states: []}), do: %Configuration{} defp get_initial_configuration( @@ -194,26 +233,36 @@ defmodule SC.Interpreter do end defp find_enabled_transitions(%StateChart{} = state_chart, %Event{} = event) do - # Get all currently active leaf states - active_leaf_states = Configuration.active_states(state_chart.configuration) + find_enabled_transitions_for_event(state_chart, event) + end + + # Find eventless transitions (automatic transitions without event attribute) + defp find_eventless_transitions(%StateChart{} = state_chart) do + find_enabled_transitions_for_event(state_chart, nil) + end + + # Unified transition finding logic for both named events and eventless transitions + defp find_enabled_transitions_for_event(%StateChart{} = state_chart, event_or_nil) do + # Get all currently active states (including ancestors) + active_states_with_ancestors = StateChart.active_states(state_chart) # Build evaluation context for conditions evaluation_context = %{ configuration: state_chart.configuration, - current_event: event, - # Use event data as data model for now - data_model: event.data || %{} + current_event: event_or_nil, + # Use event data as data model for now, or empty map for eventless + data_model: if(event_or_nil, do: event_or_nil.data || %{}, else: %{}) } - # Find transitions from these active states that match the event and condition - active_leaf_states + # Find transitions from all active states (including ancestors) that match the event/eventless and condition + active_states_with_ancestors |> Enum.flat_map(fn state_id -> # Use O(1) lookup for transitions from this state transitions = Document.get_transitions_from_state(state_chart.document, state_id) transitions |> Enum.filter(fn transition -> - Event.matches?(event, transition.event) and + matches_event_or_eventless?(transition, event_or_nil) and transition_condition_enabled?(transition, evaluation_context) end) end) @@ -221,14 +270,69 @@ defmodule SC.Interpreter do |> Enum.sort_by(& &1.document_order) end + # Check if transition matches the event (or lack thereof for eventless) + defp matches_event_or_eventless?(%{event: nil}, nil), do: true # Eventless transition + defp matches_event_or_eventless?(%{event: transition_event}, %Event{} = event) do + Event.matches?(event, transition_event) + end + defp matches_event_or_eventless?(_, _), do: false + + # Resolve transition conflicts according to SCXML semantics: + # Child state transitions take priority over ancestor state transitions + defp resolve_transition_conflicts(transitions, document) do + # Group transitions by their source states + transitions_by_source = Enum.group_by(transitions, & &1.source) + source_states = Map.keys(transitions_by_source) + + # Filter out ancestor state transitions if descendant has enabled transitions + source_states + |> Enum.filter(fn source_state -> + # Check if any descendant of this source state also has enabled transitions + descendants_with_transitions = + source_states + |> Enum.filter(fn other_source -> + other_source != source_state and + is_descendant_of?(document, other_source, source_state) + end) + + # Keep this source state's transitions only if no descendants have transitions + descendants_with_transitions == [] + end) + |> Enum.flat_map(fn source_state -> + Map.get(transitions_by_source, source_state, []) + end) + end + + # Check if state_id is a descendant of ancestor_id in the state hierarchy + defp is_descendant_of?(document, state_id, ancestor_id) do + case Document.find_state(document, state_id) do + nil -> false + state -> is_ancestor_in_parent_chain?(state, ancestor_id, document) + end + end + + # Recursively check if ancestor_id appears in the parent chain, walking up the hierarchy + defp is_ancestor_in_parent_chain?(%{parent: nil}, _ancestor_id, _document), do: false + defp is_ancestor_in_parent_chain?(%{parent: ancestor_id}, ancestor_id, _document), do: true + defp is_ancestor_in_parent_chain?(%{parent: parent_id}, ancestor_id, document) when is_binary(parent_id) do + # Look up parent state and continue walking up the chain + case Document.find_state(document, parent_id) do + nil -> false + parent_state -> is_ancestor_in_parent_chain?(parent_state, ancestor_id, document) + end + end + # Execute transitions with proper SCXML semantics defp execute_transitions( %Configuration{} = config, transitions, %Document{} = document ) do + # Apply SCXML conflict resolution: child state transitions take priority over ancestors + conflict_resolved_transitions = resolve_transition_conflicts(transitions, document) + # Group transitions by source state to handle document order correctly - transitions_by_source = Enum.group_by(transitions, & &1.source) + transitions_by_source = Enum.group_by(conflict_resolved_transitions, & &1.source) # For each source state, take only the first transition (document order) # This handles both regular states and parallel regions correctly diff --git a/test/sc/interpreter/eventless_transitions_test.exs b/test/sc/interpreter/eventless_transitions_test.exs new file mode 100644 index 0000000..5348a27 --- /dev/null +++ b/test/sc/interpreter/eventless_transitions_test.exs @@ -0,0 +1,151 @@ +defmodule SC.Interpreter.EventlessTransitionsTest do + use SC.Case + @moduletag :unit + + describe "eventless transitions" do + test "simple eventless transition fires automatically" do + xml = """ + + + + + + + + """ + + # Should automatically transition from a to b on initialization + test_scxml(xml, "", ["b"], []) + end + + test "conditional eventless transition fires only when condition is true" do + xml = """ + + + + + + + + + """ + + test_scxml(xml, "", ["b"], []) + end + + test "conditional eventless transition does not fire when condition is false" do + xml = """ + + + + + + + + """ + + # Should stay in initial state a since condition is false + test_scxml(xml, "", ["a"], []) + end + + test "eventless transition chains process until stable" do + xml = """ + + + + + + + + + + + + + + """ + + # Should automatically chain through a->b->c->d + test_scxml(xml, "", ["d"], []) + end + + test "eventless transitions work after regular events" do + xml = """ + + + + + + + + + + + """ + + test_scxml(xml, "", ["a"], [ + {%{"name" => "go"}, ["c"]} # Event triggers a->b, then automatic b->c + ]) + end + + test "child state transitions take priority over parent eventless transitions" do + xml = """ + + + + + + + + + + + + """ + + # Should start in child, then event should trigger child->sibling + test_scxml(xml, "", ["child"], [ + {%{"name" => "test"}, ["sibling"]} + ]) + end + + test "document order priority for multiple eventless transitions from same state" do + xml = """ + + + + + + + + + + """ + + # Should take first transition in document order (a->b) + test_scxml(xml, "", ["b"], []) + end + + test "infinite loop prevention with cycle detection" do + xml = """ + + + + + + + + + + """ + + # Should not crash due to infinite loop - cycle detection should prevent this + # Final state depends on implementation but should not hang + {:ok, document} = SC.Parser.SCXML.parse(xml) + {:ok, state_chart} = SC.Interpreter.initialize(document) + + # Just ensure we don't crash and have some stable state + assert SC.Interpreter.active_states(state_chart) |> MapSet.size() > 0 + end + end +end \ No newline at end of file From 47674dd02713f9c56c704eb60f631d85f72dfe77 Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Thu, 21 Aug 2025 06:06:40 -0600 Subject: [PATCH 3/6] Adds proper exit set handling --- CHANGELOG.md | 27 ++++ CLAUDE.md | 38 +++-- README.md | 94 ++++++++++++- lib/sc/interpreter.ex | 133 ++++++++++++++++-- test/passing_tests.json | 7 + .../eventless_transitions_test.exs | 17 ++- 6 files changed, 279 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7663a8..e221384 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +#### Eventless/Automatic Transitions +- **Eventless Transitions**: Full W3C SCXML support for transitions without event attributes that fire automatically +- **Automatic Transition Processing**: Microstep loop processes chains of eventless transitions until stable configuration +- **Cycle Detection**: Prevents infinite loops with configurable iteration limits (100 iterations default) +- **Parallel Region Preservation**: Proper SCXML semantics for transitions within and across parallel regions +- **Conflict Resolution**: Child state transitions take priority over ancestor transitions per W3C specification + +#### Enhanced Parallel State Support +- **Parallel State Transitions**: Fixed regression where transitions within parallel regions affected unrelated parallel regions +- **Cross-Parallel Boundaries**: Proper exit semantics when transitions cross parallel region boundaries +- **SCXML Exit State Calculation**: Implements correct W3C exit set computation for complex state hierarchies +- **Sibling State Management**: Automatic exit of parallel siblings when transitions leave their shared parent + +### Fixed +- **Regression Test**: Fixed parallel state test failure (`test/scion_tests/more_parallel/test1_test.exs`) +- **SCION Test Suite**: All 4 `cond_js` tests now pass (previously 3/4) +- **Parallel Interrupt Tests**: Fixed 6 parallel interrupt test failures in regression suite +- **Code Quality**: Resolved all `mix credo --strict` issues (predicate naming, unused variables, aliases) + +### Technical Improvements +- **Feature Detection**: Added `eventless_transitions: :supported` to feature registry +- **Performance**: Optimized ancestor/descendant lookup using existing parent attributes +- **Test Coverage**: Added 8 comprehensive eventless transition tests (434 total tests, up from 426) +- **Regression Testing**: All 62 regression tests pass (up from 60) + ## [0.1.0] - 2025-08-20 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 6eee849..ba9da63 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,9 +28,9 @@ When verifying code changes, always follow this sequence (also automated via pre **Testing:** -- `mix test` - Run all internal tests (excludes SCION/W3C by default) +- `mix test` - Run all internal tests (excludes SCION/W3C by default) - 434 tests - `mix test --include scion --include scxml_w3` - Run all tests including SCION and W3C tests -- `mix test.regression` - Run regression tests that should always pass +- `mix test.regression` - Run regression tests that should always pass - 62 tests (critical functionality) - `mix test.baseline` - Check which tests are currently passing (for updating regression suite) - `mix test --cover` - Run all tests with coverage reporting (maintain 95%+ coverage) - `mix coveralls` - Alternative coverage command @@ -38,6 +38,7 @@ When verifying code changes, always follow this sequence (also automated via pre - `mix test test/sc/location_test.exs` - Run location tracking tests - `mix test test/sc/parser/scxml_test.exs` - Run specific SCXML parser tests (uses pattern matching) - `mix test test/sc/interpreter/compound_state_test.exs` - Run compound state tests +- `mix test test/sc/interpreter/eventless_transitions_test.exs` - Run eventless transition tests **Development:** @@ -109,9 +110,14 @@ Also use this initial Elixir implementation as reference: ` with transitions) - W3C compliant +- ✅ **Parallel states** with concurrent execution and proper exit semantics +- ✅ **Eventless transitions** - Automatic transitions without event attributes (W3C compliant) +- ✅ **Conditional transitions** - Full `cond` attribute support with Predicator v2.0 expression evaluation +- ✅ **Transition conflict resolution** - Child state transitions take priority over ancestors per W3C specification - ✅ Hierarchical states with O(1) optimized lookups - ✅ Event-driven state changes - ✅ Initial state configuration (both `initial="id"` attributes and `` elements) @@ -255,11 +265,11 @@ XML content within triple quotes uses 4-space base indentation. **Main Failure Categories:** -- **Document parsing failures**: Complex SCXML with parallel states, history states, executable content +- **Document parsing failures**: Complex SCXML with history states, executable content - **Validation too strict**: Rejecting valid but complex SCXML documents -- **Missing SCXML features**: Parallel states, conditional transitions, targetless transitions, internal transitions +- **Missing SCXML features**: Targetless transitions, internal transitions - **Missing executable content**: `