Skip to content
This repository was archived by the owner on Sep 12, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion lib/sc/document/validator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ defmodule SC.Document.Validator do
case validated_result.errors do
[] ->
# Only optimize valid documents
SC.Document.build_lookup_maps(document)
document
|> determine_state_types()
|> SC.Document.build_lookup_maps()

_errors ->
# Don't waste time optimizing invalid documents
Expand Down Expand Up @@ -282,4 +284,39 @@ defmodule SC.Document.Validator do
add_warning(result, "Document initial state '#{initial_id}' is not a top-level state")
end
end

# State type determination (moved from parser)

# Determine state types for all states in the document based on structure
@spec determine_state_types(SC.Document.t()) :: SC.Document.t()
defp determine_state_types(%SC.Document{} = document) do
updated_states = Enum.map(document.states, &update_state_types/1)
%{document | states: updated_states}
end

# Update state types based on structure after parsing is complete
@spec update_state_types(SC.State.t()) :: SC.State.t()
defp update_state_types(%SC.State{type: :parallel} = state) do
# Parallel states keep their type, just update children
updated_children = Enum.map(state.states, &update_state_types/1)
%{state | states: updated_children}
end

defp update_state_types(%SC.State{} = state) do
# Update children first (bottom-up)
updated_children = Enum.map(state.states, &update_state_types/1)

# Determine this state's type based on structure (atomic vs compound)
state_type = determine_state_type(updated_children, state.initial)

%{state | type: state_type, states: updated_children}
end

defp determine_state_type(child_states, initial_value) do
cond do
Enum.empty?(child_states) -> :atomic
initial_value != nil or not Enum.empty?(child_states) -> :compound
true -> :atomic
end
end
end
86 changes: 55 additions & 31 deletions lib/sc/interpreter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ defmodule SC.Interpreter do
# No matching transitions - return unchanged (silent handling)
{:ok, state_chart}

[transition | _rest] ->
# Execute the first enabled transition
transitions ->
# Execute all enabled transitions (for parallel regions)
new_config =
execute_transition(state_chart.configuration, transition, state_chart.document)
execute_transitions(state_chart.configuration, transitions, state_chart.document)

{:ok, StateChart.update_configuration(state_chart, new_config)}
end
Expand Down Expand Up @@ -93,7 +93,7 @@ defmodule SC.Interpreter do
%Document{initial: nil, states: [first_state | _rest]} = document
) do
# No initial specified - use first state and enter it properly
initial_states = enter_compound_state(first_state, document)
initial_states = enter_state(first_state, document)
Configuration.new(initial_states)
end

Expand All @@ -104,29 +104,38 @@ defmodule SC.Interpreter do
%Configuration{}

state ->
initial_states = enter_compound_state(state, document)
initial_states = enter_state(state, document)
Configuration.new(initial_states)
end
end

# Enter a compound state by recursively entering its initial child states.
# Enter a state by recursively entering its initial child states based on type.
# Returns a list of leaf state IDs that should be active.
defp enter_compound_state(%SC.State{states: []} = state, _document) do
defp enter_state(%SC.State{type: :atomic} = state, _document) do
# Atomic state - return its ID
[state.id]
end

defp enter_compound_state(%SC.State{states: child_states, initial: initial_id}, document) do
defp enter_state(
%SC.State{type: :compound, states: child_states, initial: initial_id},
document
) do
# Compound state - find and enter initial child (don't add compound state to active set)
initial_child = get_initial_child_state(initial_id, child_states)

case initial_child do
# No valid child - compound state with no children is not active
nil -> []
child -> enter_compound_state(child, document)
child -> enter_state(child, document)
end
end

defp enter_state(%SC.State{type: :parallel, states: child_states}, document) do
# Parallel state - enter ALL children simultaneously
child_states
|> Enum.flat_map(&enter_state(&1, document))
end

# Get the initial child state for a compound state
defp get_initial_child_state(nil, [first_child | _rest]), do: first_child

Expand All @@ -136,8 +145,6 @@ defmodule SC.Interpreter do

defp get_initial_child_state(_initial_id, []), do: nil

# Find a state by ID in the document (using the more efficient implementation below)

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)
Expand All @@ -155,35 +162,52 @@ defmodule SC.Interpreter do
|> Enum.sort_by(& &1.document_order)
end

defp execute_transition(
# Execute transitions with proper SCXML semantics
defp execute_transitions(
%Configuration{} = config,
%SC.Transition{} = transition,
transitions,
%Document{} = document
) do
# Group transitions by source state to handle document order correctly
transitions_by_source = Enum.group_by(transitions, & &1.source)

# For each source state, take only the first transition (document order)
# This handles both regular states and parallel regions correctly
selected_transitions =
transitions_by_source
|> Enum.flat_map(fn {_source_id, source_transitions} ->
# Take first transition in document order (transitions are already sorted)
case source_transitions do
[] -> []
# Only first transition per source state
[first | _rest] -> [first]
end
end)

# Execute the selected transitions
target_leaf_states =
selected_transitions
|> Enum.flat_map(&execute_single_transition(&1, document))

case target_leaf_states do
# No valid transitions
[] -> config
states -> Configuration.new(states)
end
end

# Execute a single transition and return target leaf states
defp execute_single_transition(transition, document) do
case transition.target do
# No target - stay in same state
# No target
nil ->
config
[]

target_id ->
# Proper compound state transition:
# 1. Find target state in document using O(1) lookup
# 2. If compound, enter its initial children
# 3. Return new configuration with leaf states only
case Document.find_state(document, target_id) do
nil ->
# Invalid target - stay in current state
config

target_state ->
# For now: replace all active states with target and its children
# Future: Implement proper SCXML exit/entry sequence with LCA computation
target_leaf_states = enter_compound_state(target_state, document)
Configuration.new(target_leaf_states)
nil -> []
target_state -> enter_state(target_state, document)
end
end
end

# These functions are no longer needed - we use Document.find_state/2
# and Document.get_transitions_from_state/2 for O(1) lookups
end
30 changes: 30 additions & 0 deletions lib/sc/parser/scxml/element_builder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ defmodule SC.Parser.SCXML.ElementBuilder do
%SC.State{
id: get_attr_value(attrs_map, "id"),
initial: get_attr_value(attrs_map, "initial"),
# Will be updated later based on children and structure
type: :atomic,
states: [],
transitions: [],
document_order: document_order,
Expand All @@ -73,6 +75,34 @@ defmodule SC.Parser.SCXML.ElementBuilder do
}
end

@doc """
Build an SC.State from parallel XML attributes and location info.
"""
@spec build_parallel_state(list(), map(), String.t(), map()) :: SC.State.t()
def build_parallel_state(attributes, location, xml_string, element_counts) do
attrs_map = attributes_to_map(attributes)
document_order = LocationTracker.document_order(element_counts)

# Calculate attribute-specific locations
id_location = LocationTracker.attribute_location(xml_string, "id", location)

%SC.State{
id: get_attr_value(attrs_map, "id"),
# Parallel states don't have initial attributes
initial: nil,
# Set type directly during parsing
type: :parallel,
states: [],
transitions: [],
document_order: document_order,
# Location information
source_location: location,
id_location: id_location,
# Parallel states don't have initial
initial_location: nil
}
end

@doc """
Build an SC.Transition from XML attributes and location info.
"""
Expand Down
24 changes: 24 additions & 0 deletions lib/sc/parser/scxml/handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ defmodule SC.Parser.SCXML.Handler do
"state" ->
handle_state_start(attributes, location, state)

"parallel" ->
handle_parallel_start(attributes, location, state)

"transition" ->
handle_transition_start(attributes, location, state)

Expand All @@ -77,6 +80,10 @@ defmodule SC.Parser.SCXML.Handler do
"state" ->
StateStack.handle_state_end(state)

"parallel" ->
# Handle parallel same as state
StateStack.handle_state_end(state)

"transition" ->
StateStack.handle_transition_end(state)

Expand Down Expand Up @@ -125,6 +132,23 @@ defmodule SC.Parser.SCXML.Handler do
{:ok, StateStack.push_element(updated_state, "state", state_element)}
end

defp handle_parallel_start(attributes, location, state) do
parallel_element =
ElementBuilder.build_parallel_state(
attributes,
location,
state.xml_string,
state.element_counts
)

updated_state = %{
state
| current_element: {:parallel, parallel_element}
}

{:ok, StateStack.push_element(updated_state, "parallel", parallel_element)}
end

defp handle_transition_start(attributes, location, state) do
transition =
ElementBuilder.build_transition(
Expand Down
35 changes: 33 additions & 2 deletions lib/sc/parser/scxml/state_stack.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ defmodule SC.Parser.SCXML.StateStack do
{:ok, updated_state}

[{"state", parent_state} | rest] ->
# State is nested - calculate depth from stack level
# State is nested in another state - calculate depth from stack level
# Stack depth = document level (0) + state nesting level
current_depth = calculate_stack_depth(rest) + 1

Expand All @@ -42,6 +42,19 @@ defmodule SC.Parser.SCXML.StateStack do
updated_parent = %{parent_state | states: parent_state.states ++ [state_with_hierarchy]}
{:ok, %{state | stack: [{"state", updated_parent} | rest]}}

[{"parallel", parent_state} | rest] ->
# State is nested in a parallel state - calculate depth from stack level
current_depth = calculate_stack_depth(rest) + 1

state_with_hierarchy = %{
state_element
| parent: parent_state.id,
depth: current_depth
}

updated_parent = %{parent_state | states: parent_state.states ++ [state_with_hierarchy]}
{:ok, %{state | stack: [{"parallel", updated_parent} | rest]}}

_other_parent ->
{:ok, %{state | stack: parent_stack}}
end
Expand All @@ -57,9 +70,27 @@ defmodule SC.Parser.SCXML.StateStack do

case parent_stack do
[{"state", parent_state} | rest] ->
updated_parent = %{parent_state | transitions: parent_state.transitions ++ [transition]}
# Set the source state ID on the transition
transition_with_source = %{transition | source: parent_state.id}

updated_parent = %{
parent_state
| transitions: parent_state.transitions ++ [transition_with_source]
}

{:ok, %{state | stack: [{"state", updated_parent} | rest]}}

[{"parallel", parent_state} | rest] ->
# Set the source state ID for parallel states too
transition_with_source = %{transition | source: parent_state.id}

updated_parent = %{
parent_state
| transitions: parent_state.transitions ++ [transition_with_source]
}

{:ok, %{state | stack: [{"parallel", updated_parent} | rest]}}

_other_parent ->
{:ok, %{state | stack: parent_stack}}
end
Expand Down
4 changes: 4 additions & 0 deletions lib/sc/state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ defmodule SC.State do
defstruct [
:id,
:initial,
type: :atomic,
states: [],
transitions: [],
# Hierarchy navigation
Expand All @@ -19,9 +20,12 @@ defmodule SC.State do
initial_location: nil
]

@type state_type :: :atomic | :compound | :parallel | :history | :initial | :final

@type t :: %__MODULE__{
id: String.t(),
initial: String.t() | nil,
type: state_type(),
states: [SC.State.t()],
transitions: [SC.Transition.t()],
parent: String.t() | nil,
Expand Down
3 changes: 3 additions & 0 deletions lib/sc/transition.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ defmodule SC.Transition do
:event,
:target,
:cond,
# Source state ID - set during parsing
source: nil,
# Document order for deterministic processing
document_order: nil,
# Location information for validation
Expand All @@ -20,6 +22,7 @@ defmodule SC.Transition do
event: String.t() | nil,
target: String.t() | nil,
cond: String.t() | nil,
source: String.t() | nil,
document_order: integer() | nil,
source_location: map() | nil,
event_location: map() | nil,
Expand Down
Loading
Loading