Statifier - SCXML State Machines for Elixir

CI Coverage

An Elixir implementation of SCXML (State Chart XML) state charts with a focus on W3C compliance.

Features

Current Status

Working Features

Planned Features

Recent Completions

✅ Complete History State Support (v1.4.0)

✅ Multiple Transition Target Support (v1.4.0)

✅ SCXML-Compliant Processing Engine

✅ Enhanced Parallel State Support

✅ Feature-Based Test Validation System

✅ Modular Validator Architecture

✅ Initial State Elements

Future Extensions

The next major areas for development focus on expanding SCXML feature support:

High Priority Features

Medium Priority Features

Installation

Add statifier to your list of dependencies in mix.exs:

def deps do
[
{:statifier, "~> 1.6"}
]
end

Usage

Basic Example

# Parse SCXML document
xml = """
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="start">
<state id="start">
<transition event="go" target="end"/>
</state>
<state id="end"/>
</scxml>
"""
{:ok, document} = Statifier.parse(xml)
# Initialize state chart
{:ok, state_chart} = Statifier.Interpreter.initialize(document)
# Check active states
active_states = Statifier.Configuration.active_leaf_states(state_chart.configuration)
# Returns: MapSet.new(["start"])
# Send event
event = Statifier.Event.new("go")
{:ok, new_state_chart} = Statifier.Interpreter.send_event(state_chart, event)
# Check new active states
active_states = Statifier.Configuration.active_leaf_states(new_state_chart.configuration)
# Returns: MapSet.new(["end"])

Eventless Transitions Example

# Automatic transitions without events fire immediately
xml = """
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="start">
<state id="start">
<transition target="processing"/> <!-- No event - fires automatically -->
</state>
<state id="processing">
<transition target="done" cond="ready == true"/> <!-- Conditional eventless -->
</state>
<state id="done"/>
</scxml>
"""
{:ok, document} = Statifier.parse(xml)
{:ok, state_chart} = Statifier.Interpreter.initialize(document)
# Eventless transitions processed automatically during initialization
active_states = Statifier.Configuration.active_leaf_states(state_chart.configuration)
# Returns: MapSet.new(["processing"]) - automatically moved from start

Parallel States Example

xml = """
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0">
<parallel id="app">
<state id="ui" initial="idle">
<state id="idle">
<transition event="click" target="busy"/>
</state>
<state id="busy">
<transition event="done" target="idle"/>
</state>
</state>
<state id="network" initial="offline">
<state id="offline">
<transition event="connect" target="online"/>
</state>
<state id="online"/>
</state>
</parallel>
</scxml>
"""
{:ok, document} = Statifier.parse(xml)
{:ok, state_chart} = Statifier.Interpreter.initialize(document)
# Both parallel regions active simultaneously
active_states = Statifier.Configuration.active_leaf_states(state_chart.configuration)
# Returns: MapSet.new(["idle", "offline"])

History States Example

# SCXML with shallow and deep history states
xml = """
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="main">
<state id="main" initial="sub1">
<!-- Shallow history - restores immediate children -->
<history id="main_hist" type="shallow">
<transition target="sub1"/> <!-- Default when no history -->
</history>
<state id="sub1">
<transition event="go" target="sub2"/>
</state>
<state id="sub2">
<transition event="go" target="sub3"/>
</state>
<state id="sub3">
<transition event="exit" target="other"/>
<transition event="back" target="main_hist"/> <!-- Restore history -->
</state>
</state>
<state id="other">
<transition event="return" target="main_hist"/> <!-- Restore to last sub-state -->
</state>
</scxml>
"""
{:ok, document} = Statifier.parse(xml)
{:ok, state_chart} = Statifier.Interpreter.initialize(document)
# Progress through states
{:ok, state_chart} = Statifier.Interpreter.send_event(state_chart, Statifier.Event.new("go"))
{:ok, state_chart} = Statifier.Interpreter.send_event(state_chart, Statifier.Event.new("go"))
# Active states: ["sub3"]
{:ok, state_chart} = Statifier.Interpreter.send_event(state_chart, Statifier.Event.new("exit"))
# Active states: ["other"] - history recorded
{:ok, state_chart} = Statifier.Interpreter.send_event(state_chart, Statifier.Event.new("return"))
# Active states: ["sub3"] - history restored!

Multiple Transition Targets Example

# SCXML with multiple target transitions
xml = """
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="start">
<state id="start">
<!-- Multiple targets - enter multiple states simultaneously -->
<transition event="activate" target="system target1 target2"/>
</state>
<parallel id="system">
<state id="target1">
<transition event="done" target="end"/>
</state>
<state id="target2">
<transition event="done" target="end"/>
</state>
</parallel>
<state id="end"/>
</scxml>
"""
{:ok, document} = Statifier.parse(xml)
{:ok, state_chart} = Statifier.Interpreter.initialize(document)
# Send activate event - enters multiple targets
{:ok, state_chart} = Statifier.Interpreter.send_event(state_chart, Statifier.Event.new("activate"))
# Check active states - multiple states active simultaneously
active_states = Statifier.Configuration.active_leaf_states(state_chart.configuration)
# Returns: MapSet.new(["target1", "target2"]) - both targets entered

Document Validation

{:ok, document} = Statifier.parse(xml)
case Statifier.Validator.validate(document) do
{:ok, optimized_document, warnings} ->
# Document is valid and optimized, warnings are non-fatal
IO.puts("Valid document with #{length(warnings)} warnings")
# optimized_document now has O(1) lookup maps built
{:error, errors, warnings} ->
# Document has validation errors
IO.puts("Validation failed with #{length(errors)} errors")
end

Assign Elements Example

# SCXML with assign elements for dynamic data manipulation
xml = """
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="start">
<state id="start">
<onentry>
<assign location="userName" expr="'John Doe'"/>
<assign location="counter" expr="42"/>
<assign location="user.profile.name" expr="'Jane Smith'"/>
<assign location="users['admin'].active" expr="true"/>
</onentry>
<transition target="working"/>
</state>
<state id="working">
<onentry>
<assign location="counter" expr="counter + 1"/>
<assign location="status" expr="'processing'"/>
</onentry>
<onexit>
<assign location="status" expr="'completed'"/>
</onexit>
<transition event="finish" target="done"/>
</state>
<final id="done"/>
</scxml>
"""
{:ok, document} = Statifier.parse(xml)
{:ok, state_chart} = Statifier.Interpreter.initialize(document)
# Check the data model after onentry execution
datamodel = state_chart.datamodel
# Returns: %{
# "userName" => "John Doe",
# "counter" => 43, # incremented to 43 in working state
# "user" => %{"profile" => %{"name" => "Jane Smith"}},
# "users" => %{"admin" => %{"active" => true}},
# "status" => "processing"
# }

Logging and Test Environment

Statifier includes a comprehensive logging system designed for both production use and clean test environments:

# Production logging with Elixir Logger integration
{:ok, document} = Statifier.parse(xml)
{:ok, state_chart} = Statifier.Interpreter.initialize(document, [
log_adapter: {Statifier.Logging.ElixirLoggerAdapter, []},
log_level: :info
])
# Test environment automatically uses TestAdapter (configured in test/test_helper.exs)
# for clean output and log inspection
# Using log helpers in tests
defmodule MyStateMachineTest do
use Statifier.Case # Provides logging test helpers
test "action execution with logging" do
xml = """
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="start">
<state id="start">
<onentry>
<log expr="'Starting process'"/>
<assign location="status" expr="'active'"/>
</onentry>
<transition event="go" target="done"/>
</state>
<state id="done"/>
</scxml>
"""
{:ok, state_chart} = test_scxml(xml, "logging test", ["start"], [
{%{"name" => "go"}, ["done"]}
])
# Assert specific log entries were created
assert_log_entry(state_chart, message_contains: "Starting process")
assert_log_entry(state_chart, level: :debug, action_type: "assign_action")
# Verify logs appear in chronological order
assert_log_order(state_chart, [
[message_contains: "Starting process"],
[action_type: "assign_action"]
])
end
end

Key logging features:

Development

Requirements

Setup

mix deps.get
mix compile

Code Quality Workflow

The project maintains high code quality through automated checks:

# Local validation workflow (also runs via pre-push hook)
mix format # Auto-fix formatting
mix test.regression # Run critical regression tests (22 tests)
mix credo --strict # Static code analysis
mix dialyzer # Type checking

Regression Testing

The project uses automated regression testing to prevent breaking existing functionality:

# Run only tests that should always pass (118 tests)
mix test.regression
# Check which tests are currently passing to update regression suite
mix test.baseline
# Install git hooks for automated validation
./scripts/setup-git-hooks.sh

The regression suite tracks:

Running Tests

# All internal tests (excludes SCION/W3C by default) - 707 tests
mix test
# All tests including SCION and W3C test suites
mix test --include scion --include scxml_w3
# Only regression tests (118 critical tests)
mix test.regression
# With coverage reporting
mix coveralls
# Specific test categories
mix test --include scion test/scion_tests/history/
mix test test/statifier/parser/scxml_test.exs
mix test test/statifier/history/

Architecture

Core Components

Data Structures

Architecture Flow

# 1. Parse: XML → Document structure
{:ok, document} = Statifier.parse(xml)
# 2. Validate: Check semantics + optimize with lookup maps
{:ok, optimized_document, warnings} = Statifier.Validator.validate(document)
# 3. Interpret: Run state chart with optimized lookups
{:ok, state_chart} = Statifier.Interpreter.initialize(optimized_document)

Performance Optimizations

The implementation includes several key optimizations for production use:

O(1) State and Transition Lookups

Compound and Parallel State Entry

# Automatic hierarchical entry
{:ok, state_chart} = Statifier.Interpreter.initialize(document)
active_states = Statifier.Configuration.active_leaf_states(state_chart.configuration)
# Returns only leaf states (compound/parallel states entered automatically)
# Fast ancestor computation when needed
ancestors = Statifier.Configuration.all_active_states(state_chart.configuration, state_chart.document)
# O(1) state lookups + O(d) ancestor traversal
# Parallel states enter ALL child regions simultaneously
# Compound states enter initial child recursively

Parse → Validate → Optimize Flow

Performance Impact:

Regression Testing System

The project includes a sophisticated regression testing system to ensure stability:

Test Registry (test/passing_tests.json)

{
"internal_tests": ["test/statifier_test.exs", "test/statifier/**/*_test.exs"],
"scion_tests": ["test/scion_tests/basic/basic0_test.exs", ...],
"w3c_tests": []
}

Wildcard Support

CI Integration

Local Development

# Check current regression status
mix test.regression
# Update regression baseline after adding features
mix test.baseline
# Manually add newly passing tests to test/passing_tests.json
# Pre-push hook automatically runs regression tests
git push origin feature-branch

Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Install git hooks: ./scripts/setup-git-hooks.sh
  4. Make your changes following the code quality workflow:
    • mix format (auto-fix formatting)
    • Add tests for new functionality
    • mix test.regression (ensure no regressions)
    • mix credo --strict (static analysis)
    • mix dialyzer (type checking)
  5. Update regression tests if you fix failing SCION/W3C tests:
    • Run mix test.baseline to see current status
    • Add newly passing tests to test/passing_tests.json
  6. Ensure all CI checks pass
  7. Commit your changes (git commit -m 'Add amazing feature')
  8. Push to the branch (pre-push hook will run automatically)
  9. Open a Pull Request

Code Style

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments