PyFCSTM DSL Syntax Tutorial

Overview

The PyFCSTM Domain Specific Language (DSL) provides a comprehensive syntax for defining hierarchical finite state machines (Harel Statecharts) with expressions, conditions, and lifecycle actions. This tutorial covers all language constructs, semantic rules, execution models, and best practices for writing correct and efficient DSL programs.

What You’ll Learn

  • Complete DSL syntax and grammar rules

  • How hierarchical state machines execute

  • Event scoping and namespace resolution

  • Expression system and operators

  • Lifecycle actions and aspect-oriented programming

  • Real-world examples and design patterns

Language Structure

Program Organization

A complete DSL program consists of optional variable definitions followed by a single root state definition:

program ::= def_assignment* state_definition EOF

The top-level structure ensures every state machine has exactly one root state that may contain nested substates and transitions.

Note

The parser processes your DSL file in multiple phases:

  1. Lexical Analysis: Tokenizes the input into keywords, identifiers, operators, and literals

  2. Syntactic Analysis: Builds an Abstract Syntax Tree (AST) following the grammar rules

  3. Semantic Validation: Validates variable references, state names, and type consistency

  4. Model Construction: Converts the AST into an executable state machine model

Variable Definitions

Syntax

Variable definitions declare typed variables with initial values using the def keyword:

def_assignment ::= 'def' ('int'|'float') ID '=' init_expression ';'

Important

Variables are global to the entire state machine and can be accessed from any state, transition, or expression. The DSL supports two primitive types:

  • int: 32-bit signed integers, supporting decimal (42), hexadecimal (0xFF), and binary (0b1010) literals

  • float: Double-precision floating-point numbers, supporting standard (3.14) and scientific notation (1e-6)

All variables must be initialized at declaration time. The initial expression can include:

  • Literal values (0, 3.14, 0xFF)

  • Mathematical constants (pi, E, tau)

  • Arithmetic expressions (3.14 * 2, 10 + 5)

  • Mathematical functions (sin(0), sqrt(16))

Correct Usage

Integer Variables:

def int counter = 0;              // Simple initialization
def int max_attempts = 5;         // Constant value
def int flags = 0xFF;             // Hexadecimal literal
def int mask = 0b11110000;        // Binary literal
def int computed = 10 * 5 + 3;    // Expression initialization

Float Variables:

def float temperature = 25.5;     // Decimal notation
def float pi_value = pi;          // Mathematical constant
def float ratio = 3.14 * 2;       // Expression initialization
def float scientific = 1.5e-3;    // Scientific notation
def float computed = sqrt(16.0);  // Function call

Annotated Example:

// System state variables
def int system_state = 0;         // 0=init, 1=running, 2=error
def int error_count = 0;          // Track error occurrences

// Sensor readings
def float temperature = 20.0;     // Current temperature in Celsius
def float target_temp = 22.0;     // Desired temperature

// Control outputs
def int heating_power = 0;        // Heating power (0-100%)
def int fan_speed = 0;            // Fan speed (0-3)

// Bit flags for system status
def int status_flags = 0x00;      // Bit 0: heating, Bit 1: cooling
                                  // Bit 2: fan, Bit 3: error

Semantic Rules

Variable definitions must follow these semantic constraints:

  1. Unique Names: Each variable name must be unique within the program scope

  2. Type Consistency: Initial expressions must evaluate to values compatible with the declared type

  3. Expression Validity: Initial expressions can only reference mathematical constants and literals (not other variables)

  4. Declaration Order: Variables must be declared before the root state definition

Tip

Why These Rules?

  • Unique Names: Prevents ambiguity in variable references throughout the state machine

  • Type Consistency: Ensures type safety and prevents runtime errors in generated code

  • Expression Validity: Simplifies initialization and ensures deterministic startup state

  • Declaration Order: Maintains clear separation between data definitions and behavior definitions

Common Errors

Incorrect Usage:

// ERROR: Duplicate variable names
def int x = 1;
def float x = 2.0;  // Semantic error: 'x' already defined

// ERROR: Undefined reference in initialization
def int y = unknown_var;  // Semantic error: 'unknown_var' not defined

// ERROR: Referencing another variable
def int a = 10;
def int b = a;  // Semantic error: cannot reference variables in initialization

Correct Alternative:

// Use unique names
def int x_int = 1;
def float x_float = 2.0;

// Initialize with literals or constants
def int y = 0;

// Assign variable values in state actions
def int a = 10;
def int b = 0;

state Init {
    enter {
        b = a;  // Assign in lifecycle action
    }
}

State Definitions

Note

A finite state machine (FSM) is a computational model that can be in exactly one state at any given time. The machine transitions between states in response to events, executing actions during these transitions. Hierarchical state machines (Harel Statecharts) extend this concept by allowing states to contain nested substates, enabling modular and scalable designs.

Syntax Types

The DSL supports two fundamental types of state definitions:

state_definition ::= leafStateDefinition | compositeStateDefinition
leafStateDefinition ::= ['pseudo'] 'state' ID [named STRING] ';'
compositeStateDefinition ::= ['pseudo'] 'state' ID [named STRING] '{' state_inner_statement* '}'

Tip

Key Differences:

  • Leaf States: Terminal states with no internal structure; represent atomic operational modes

  • Composite States: Container states with nested substates; represent hierarchical decomposition

Leaf States

Leaf states represent terminal states with no internal structure. They are the fundamental building blocks of state machines.

Correct Usage:

state Idle;                      // Simple leaf state
state Running;                   // Another leaf state
state Error;                     // Error state

// Leaf state with display name
state Running named "System Running";

// Pseudo leaf state (skips ancestor aspect actions)
pseudo state SpecialState;

Tip

When to Use Leaf States:

  • Representing atomic operational modes (Idle, Running, Error)

  • Final states in a hierarchical decomposition

  • States with simple, non-decomposable behavior

Annotated Example:

state TrafficLight {
    // Leaf states representing light colors
    state Red;      // Stop signal
    state Yellow;   // Caution signal
    state Green;    // Go signal

    [*] -> Red;
    Red -> Green :: TimerExpired;
    Green -> Yellow :: TimerExpired;
    Yellow -> Red :: TimerExpired;
}

Composite States

Composite states contain nested substates, transitions, and lifecycle actions. They enable hierarchical decomposition of complex behaviors.

Correct Usage:

state Machine {
    // Nested substates
    state Off;
    state On {
        state Slow;
        state Fast;

        [*] -> Slow;
        Slow -> Fast :: SpeedUp;
        Fast -> Slow :: SlowDown;
    }

    // Transitions between top-level states
    [*] -> Off;
    Off -> On : if [power_switch == 1];
    On -> Off : if [power_switch == 0];
}

Important

When a composite state is active, exactly one of its child states is also active. This creates a hierarchical execution context:

  1. Entry: When entering a composite state, the entry transition ([*] -> ChildState) determines which child becomes active

  2. During: While active, the composite state’s during before/after actions execute around the child state’s actions

  3. Exit: When leaving a composite state, the active child state exits first, then the composite state exits

Annotated Example:

state PowerManagement {
    // Composite state lifecycle actions
    enter {
        // Executed when entering PowerManagement from outside
        power_level = 0;
    }

    during before {
        // Executed when entering a child state from outside
        // NOT executed during child-to-child transitions
        monitor_counter = monitor_counter + 1;
    }

    during after {
        // Executed when exiting to outside from a child state
        // NOT executed during child-to-child transitions
        cleanup_flag = 1;
    }

    exit {
        // Executed when leaving PowerManagement to outside
        power_level = 0;
    }

    // Child states
    state LowPower {
        during {
            power_level = 10;
        }
    }

    state NormalPower {
        during {
            power_level = 50;
        }
    }

    state HighPower {
        during {
            power_level = 100;
        }
    }

    [*] -> LowPower;
    LowPower -> NormalPower :: Increase;
    NormalPower -> HighPower :: Increase;
    HighPower -> NormalPower :: Decrease;
    NormalPower -> LowPower :: Decrease;
}

Pseudo States

Pseudo states are special states (leaf or composite) that skip ancestor aspect actions. They are useful for implementing special behaviors that need to bypass cross-cutting concerns.

Syntax:

pseudo state StateName;
pseudo state StateName { ... }

Note

Normal states execute ancestor aspect actions (>> during before/after) defined in parent states. Pseudo states skip these aspect actions, providing a way to opt out of cross-cutting behaviors.

Comparison Example:

 1// Pseudo State Example
 2// This example demonstrates the difference between normal and pseudo states
 3
 4def int aspect_counter = 0;
 5
 6state PseudoStateDemo {
 7    // Aspect actions that apply to all descendant states
 8    >> during before {
 9        aspect_counter = aspect_counter + 1;
10    }
11
12    >> during after {
13        aspect_counter = aspect_counter + 100;
14    }
15
16    state NormalStates {
17        // Normal leaf state - WILL execute ancestor aspect actions
18        state RegularState {
19            during {
20                aspect_counter = aspect_counter + 10;
21            }
22        }
23
24        [*] -> RegularState;
25        RegularState -> [*];
26    }
27
28    state PseudoStates {
29        // Pseudo state - WILL NOT execute ancestor aspect actions
30        // Useful for special states that need to bypass aspect logic
31        pseudo state SpecialState {
32            during {
33                aspect_counter = aspect_counter + 10;
34            }
35        }
36
37        [*] -> SpecialState;
38        SpecialState -> [*];
39    }
40
41    [*] -> NormalStates;
42    NormalStates -> PseudoStates :: Switch;
43    PseudoStates -> [*];
44}

Execution Comparison:

When RegularState is active:

  1. Root >> during before executes (aspect_counter += 1)

  2. RegularState.during executes (aspect_counter += 10)

  3. Root >> during after executes (aspect_counter += 100)

  4. Total increment per cycle: 111

When SpecialState (pseudo) is active:

  1. Root >> during before SKIPPED

  2. SpecialState.during executes (aspect_counter += 10)

  3. Root >> during after SKIPPED

  4. Total increment per cycle: 10

Tip

When to Use Pseudo States:

  • Implementing exception handlers that bypass normal monitoring

  • Creating special states for testing or debugging

  • Optimizing performance-critical states by skipping overhead

Named States

States can have display names for documentation and visualization purposes:

state Running named "System Running";
state Error named "Error State - Requires Manual Reset";
state Init named "Initialization Phase";

The display name is used in PlantUML diagrams and generated documentation, while the state ID is used in code generation.

Semantic Rules

State definitions must adhere to these semantic constraints:

  1. Unique Names: State names must be unique within their containing scope (but can be reused in different scopes)

  2. Entry Transitions: Composite states must have at least one entry transition ([*] -> state)

  3. State References: All transition targets must reference existing states in the current scope

  4. Hierarchical Consistency: Nested states follow proper parent-child relationships

  5. Aspect Restrictions: during before/after (without >>) only apply to composite states

Tip

Why These Rules?

  • Unique Names: Prevents ambiguity in transition targets and event scoping

  • Entry Transitions: Ensures deterministic behavior when entering composite states

  • State References: Prevents dangling transitions and ensures connectivity

  • Hierarchical Consistency: Maintains proper state machine structure

  • Aspect Restrictions: Enforces correct lifecycle semantics for leaf vs. composite states

Common Errors

Incorrect Usage:

// ERROR: Missing entry transition
state Container {
    state A;
    state B;
    A -> B :: Event;  // No [*] -> A or [*] -> B
}

// ERROR: Duplicate state names in same scope
state Root {
    state Child;
    state Child;  // Semantic error: duplicate name
}

// ERROR: Invalid transition target
state Root {
    state A;
    [*] -> A;
    A -> B :: Event;  // Semantic error: B doesn't exist
}

// ERROR: during before/after on leaf state
state LeafState {
    during before {  // Semantic error: leaf states can't have aspects
        x = 1;
    }
}

Correct Alternative:

// Provide entry transition
state Container {
    state A;
    state B;
    [*] -> A;  // Entry transition required
    A -> B :: Event;
}

// Use unique names
state Root {
    state ChildA;
    state ChildB;
}

// Define all referenced states
state Root {
    state A;
    state B;
    [*] -> A;
    A -> B :: Event;
}

// Use plain during for leaf states
state LeafState {
    during {  // Correct: no aspect keywords
        x = 1;
    }
}

Transition Definitions

Note

Transitions define how the state machine moves from one state to another in response to events or conditions. Each transition can have:

  • Source State: The state from which the transition originates

  • Target State: The state to which the transition leads

  • Event: Optional trigger that activates the transition

  • Guard Condition: Optional boolean expression that must be true for the transition to fire

  • Effect: Optional actions executed during the transition

Transition Types

The DSL supports three types of transitions with distinct syntax patterns:

transition_definition ::= entryTransitionDefinition | normalTransitionDefinition | exitTransitionDefinition

Entry Transitions

Entry transitions define the initial state when entering a composite state. They use the pseudo-state [*] as the source.

Syntax: [*] -> target_state [: chain_id|:: event_name] [if [condition]] [effect { operations }] ';'

Correct Usage:

[*] -> Idle;                                    // Simple entry
[*] -> Running : startup_event;                 // Entry with chain event
[*] -> Running :: startup_event;                // Entry with local event
[*] -> Active : if [initialized == 0x1];        // Entry with guard
[*] -> Ready effect { counter = 0; };           // Entry with effect
[*] -> Running : if [mode == 1] effect {        // Entry with guard and effect
    counter = 0;
    status = 1;
};

Note

When a composite state is entered from outside, the entry transition determines which child state becomes active. The guard condition (if present) is evaluated, and if true, the effect (if present) is executed before entering the target state.

Normal Transitions

Normal transitions connect two named states within the same scope.

Syntax: from_state -> to_state [: chain_id|:: event_name] [if [condition]] [effect { operations }] ';'

Correct Usage:

Idle -> Running;                                // Simple transition
Slow -> Fast : speed_up;                        // Transition with chain event
Slow -> Fast :: speed_up;                       // Transition with local event
Active -> Inactive : if [timeout > 100];        // Transition with guard
Processing -> Complete effect {                 // Transition with effect
    result = output;
    status = 1;
};
Running -> Idle : if [stop_requested] effect {  // Guard and effect
    cleanup_flag = 1;
};

Note

Normal transitions are evaluated during the “during” phase of the source state. When the event is triggered (if specified) and the guard condition is true (if specified), the transition fires:

  1. Source state’s exit action executes

  2. Transition effect executes (if present)

  3. Target state’s enter action executes

Exit Transitions

Exit transitions define how to leave a composite state to its parent. They use the pseudo-state [*] as the target.

Syntax: from_state -> [*] [: chain_id|:: event_name] [if [condition]] [effect { operations }] ';'

Correct Usage:

Error -> [*];                                   // Simple exit
Complete -> [*] : finish_event;                 // Exit with event
Running -> [*] : if [shutdown_requested];       // Exit with guard
Active -> [*] effect {                          // Exit with effect
    cleanup_flag = 0x1;
};
Processing -> [*] : if [done] effect {          // Guard and effect
    result = final_value;
};

Note

Exit transitions allow a child state to signal completion and return control to the parent state. The parent state can then transition to another state or exit itself.

Forced Transitions

Forced transitions are a syntactic sugar that automatically expands to multiple normal transitions. They are useful for defining transitions from multiple states to a common target without repetitive code, especially for error handling or emergency situations.

Syntax:

// Forced transition from specific state
! from_state -> to_state [: chain_id|:: event_name] [if [condition]] ';'

// Forced exit from specific state
! from_state -> [*] [: chain_id|:: event_name] [if [condition]] ';'

// Forced transition from ALL substates (wildcard)
! * -> to_state [: chain_id|:: event_name] [if [condition]] ';'

// Forced exit from ALL substates
! * -> [*] [: chain_id|:: event_name] [if [condition]] ';'

Important

Forced transitions are a syntactic sugar that expands during model construction. When you write:

state Parent {
    ! * -> ErrorHandler :: CriticalError;

    state Child1;
    state Child2;
}

The parser automatically generates normal transitions from all substates:

// Expanded transitions (generated automatically):
Child1 -> ErrorHandler : CriticalError;
Child2 -> ErrorHandler : CriticalError;

Important: These are normal transitions - they execute exit actions just like any other transition.

Key Characteristics:

  1. Syntactic Sugar: Expands to multiple normal transitions during model construction

  2. Wildcard Expansion: ! * generates transitions from all substates in the current scope

  3. Hierarchical Propagation: Forced transitions propagate to nested substates recursively

  4. Shared Event Object: All expanded transitions share the same event object

  5. No Effect Blocks: Forced transitions cannot have effect blocks (syntax limitation)

  6. Normal Execution: Exit actions execute normally - forced transitions are just regular transitions

Tip

When to Use Forced Transitions:

  • Avoid Repetitive Code: Define one transition instead of many identical ones

  • Error Handling: Transition from any state to error handler

  • Emergency Shutdown: Transition from all states to shutdown state

  • Timeout Handling: Handle timeouts uniformly across multiple states

state System {
    // Force transition from any state to error handler
    ! * -> ErrorHandler :: CriticalError;

    // Force transition from specific state
    ! Running -> SafeMode :: EmergencyStop;

    // Force exit from any state
    ! * -> [*] :: FatalError;

    // With guard condition
    ! * -> ErrorHandler : if [error_code > 100];

    state Running {
        exit {
            // This exit action WILL execute when transitioning
            cleanup_flag = 1;
        }
    }

    state ErrorHandler;
}

When to Use Forced Transitions:

  • Avoid Repetitive Code: Define one transition instead of many identical ones

  • Error Handling: Transition from any state to error handler

  • Emergency Shutdown: Transition from all states to shutdown state

  • Timeout Handling: Handle timeouts uniformly across multiple states

Complete Example:

 1// Forced Transitions Example
 2// This example demonstrates forced transitions - a syntactic sugar that
 3// automatically expands to multiple normal transitions
 4
 5def int error_code = 0;
 6def int recovery_attempts = 0;
 7
 8state System {
 9    // Forced transitions - syntactic sugar that expands to multiple transitions
10    // These generate NORMAL transitions - exit actions WILL execute
11    ! * -> ErrorHandler :: CriticalError;     // From ANY state to ErrorHandler
12    !Running -> SafeMode :: EmergencyStop;   // From Running (and all its substates) to SafeMode
13
14    state Initializing {
15        exit {
16            // This exit action WILL execute when transitioning via CriticalError
17            error_code = 0;
18        }
19    }
20
21    state Running {
22        exit {
23            // This exit action WILL execute when transitioning
24            recovery_attempts = 0;
25        }
26
27        state Processing {
28            during {
29                error_code = error_code + 1;
30            }
31
32            exit {
33                // This exit action WILL also execute
34                error_code = 0;
35            }
36        }
37
38        state Waiting;
39
40        [*] -> Processing;
41        Processing -> Waiting :: Done;
42        Waiting -> Processing :: Continue;
43    }
44
45    state SafeMode {
46        enter {
47            recovery_attempts = recovery_attempts + 1;
48        }
49    }
50
51    state ErrorHandler {
52        enter {
53            // Handle critical error
54            recovery_attempts = 0;
55        }
56    }
57
58    [*] -> Initializing;
59    Initializing -> Running :: Start;
60    Running -> [*] :: Shutdown;
61    SafeMode -> Running :: Recovered;
62    ErrorHandler -> [*] :: FatalError;
63}

Visualization:

Forced Transitions Example

Expansion Behavior:

When ! * -> ErrorHandler :: CriticalError is defined in System, it expands to:

// From direct children
Running -> ErrorHandler :: CriticalError;
Idle -> ErrorHandler :: CriticalError;
SafeMode -> ErrorHandler :: CriticalError;
ErrorHandler -> ErrorHandler :: CriticalError;

// Propagates to nested children (Running.Processing, Running.Waiting)
// Inside Running state, generates:
Processing -> [*] : /CriticalError;  // Exit to parent, then parent transitions
Waiting -> [*] : /CriticalError;

Event Sharing:

All expanded transitions from a single forced transition definition share the same event object. This means:

state System {
    ! * -> ErrorHandler :: CriticalError;

    state A;
    state B;
    state C;
}

// All these transitions use the SAME event object:
// A -> ErrorHandler :: CriticalError
// B -> ErrorHandler :: CriticalError
// C -> ErrorHandler :: CriticalError
// When you trigger CriticalError, ALL matching transitions can fire

Warning

  1. Normal Transitions: Expanded transitions are normal transitions - exit actions execute

  2. Event Sharing: All expanded transitions share the same event object

  3. No Effect Blocks: Forced transitions cannot have effect blocks (use target state’s enter action)

  4. Scope Limitation: ! * applies to direct substates, but propagates recursively

  5. Event Scoping: Event scoping rules (: vs ::) apply normally

Common Errors:

// ERROR: Cannot have effect block on forced transition
! * -> ErrorHandler :: Error effect {  // Syntax error
    error_code = 1;
};

// ERROR: Forced transition must reference existing states
! * -> NonExistentState :: Error;  // Semantic error

Correct Alternative:

// Use enter action in target state for initialization
state ErrorHandler {
    enter {
        error_code = 1;  // Initialize in target state
    }
}

! * -> ErrorHandler :: Error;  // Correct: no effect block

Event Definitions

Events are the core mechanism that triggers state transitions. In finite state machines, state transitions are typically driven by external events—such as user input, sensor signals, timer expiration, or system messages. Events provide the state machine with the ability to respond to external stimuli, enabling it to change behavior based on the current state and received events.

In the PyFCSTM DSL, events can be defined in two ways:

  1. Implicit Definition: Events are automatically created when referenced directly in transitions

  2. Explicit Definition: Events are pre-declared using the event keyword, optionally with display names

Explicit Event Definitions

Events can be explicitly defined within a state scope using the event keyword:

Syntax:

event_definition ::= 'event' ID ('named' STRING)? ';'

Examples:

event StartEvent;                              // Simple event definition
event ErrorOccurred named "Error Occurred";    // Event with display name
event UserInput named "User Input Received";   // Event with descriptive name

Purpose of Explicit Event Definitions:

Explicit event definitions serve several important purposes:

  1. Documentation: Explicitly declare events used within a state scope for better code clarity and maintainability

  2. Visualization: The named attribute provides human-readable display names for PlantUML diagrams and documentation generation

  3. Consistency: Similar to state definitions with named, event definitions support visualization and documentation tools

Important

Relationship with Transition Events:

Explicit event definitions and transition events are part of the same event system. When you define an event explicitly, it can be referenced in transitions within the same scope. The events are unified - there is no distinction between “explicitly defined events” and “transition events” at runtime.

Complete Example:

state System {
    // Explicit event definitions with display names
    event Start named "System Start";
    event Stop named "System Stop";
    event Pause named "System Pause";
    event Resume named "System Resume";

    state Idle;
    state Running;
    state Paused;

    [*] -> Idle;
    Idle -> Running : Start;      // References the explicitly defined Start event
    Running -> Idle : Stop;       // References the explicitly defined Stop event
    Running -> Paused : Pause;    // References the explicitly defined Pause event
    Paused -> Running : Resume;   // References the explicitly defined Resume event
}

Note

Key Points:

  • Explicit event definitions are optional - events can be used in transitions without explicit definition

  • The named attribute is the primary benefit, providing display names for visualization (PlantUML, state diagrams)

  • Events defined explicitly follow the same scoping rules as transition events (see Event Scoping section below)

  • Explicit definitions improve code readability, self-documentation, and integration with visualization tools

  • The named attribute works exactly like the named attribute for states - it provides a human-readable label

Event Scoping

In hierarchical state machines, events need a namespace to avoid naming conflicts.

Important

Consider this scenario:

state Root {
    state A;
    state B;
    state C;

    [*] -> A;
    A -> B : Event;  // Which Event?
    B -> C : Event;  // Same Event or different?
}

Should both transitions use the same event or different events? The DSL provides three scoping mechanisms to handle this.

Scoping Mechanisms

The DSL supports three ways to specify event scope:

  1. Local Events (::): Scoped to the source state’s namespace

  2. Chain Events (:): Scoped to the parent state’s namespace

  3. Absolute Events (/): Scoped to the root state’s namespace

All three mechanisms are equivalent to using absolute paths, just with different starting points.

Local Events (:: operator)

Local events use the :: operator and are scoped to the source state’s namespace.

Syntax: StateA -> StateB :: EventName;

Note

The event is created in the source state’s namespace. Each source state gets its own event.

Example:

state Root {
    state A;
    state B;

    [*] -> A;
    A -> B :: E;     // Creates event: Root.A.E
    B -> A :: E;     // Creates event: Root.B.E (DIFFERENT from above)
}

Equivalent Absolute Path:

// A -> B :: E  is equivalent to:
A -> B : /A.E

// B -> A :: E  is equivalent to:
B -> A : /B.E

Tip

When to Use:

  • Each transition needs its own unique event

  • Avoid naming conflicts between similar transitions

  • State-specific events that shouldn’t be shared

Chain Events (: operator)

Chain events use the : operator and are scoped to the parent state’s namespace.

Syntax: StateA -> StateB : EventName;

Note

The event is created in the parent state’s namespace. Multiple transitions in the same scope can share the event.

Example:

state Root {
    state A;
    state B;
    state C;

    [*] -> A;
    A -> B : E;      // Creates event: Root.E
    B -> C : E;      // Uses SAME event: Root.E
}

Equivalent Absolute Path:

// A -> B : E  is equivalent to:
A -> B : /E

// B -> C : E  is equivalent to:
B -> C : /E

Tip

When to Use:

  • Multiple transitions should respond to the same event

  • Coordinating transitions across sibling states

  • Shared events within a scope

Absolute Events (/ prefix)

Absolute events use the / prefix and are scoped to the root state’s namespace.

Syntax: StateA -> StateB : /EventName; or StateA -> StateB : /Path.To.EventName;

Note

The event path is resolved from the root state, allowing explicit control over event location.

Example:

state Root {
    state ModuleA {
        state A1;
        state A2;

        [*] -> A1;
        A1 -> A2 : /GlobalEvent;  // Uses Root.GlobalEvent
    }

    state ModuleB {
        state B1;
        state B2;

        [*] -> B1;
        B1 -> B2 : /GlobalEvent;  // Uses SAME Root.GlobalEvent
    }

    [*] -> ModuleA;
}

Equivalent Absolute Path:

// Already absolute - no conversion needed
A1 -> A2 : /GlobalEvent  // Root.GlobalEvent
B1 -> B2 : /GlobalEvent  // Root.GlobalEvent (SAME event)

Tip

When to Use:

  • Cross-module communication

  • Global events that should be accessible from anywhere

  • Explicit control over event location

  • Avoiding ambiguity in deeply nested states

Complete Comparison Example

Here’s a comprehensive example demonstrating all three scoping mechanisms:

 1// Event Scoping Complete Example
 2// This example demonstrates all three event scoping mechanisms:
 3// 1. Local events (::) - scoped to source state
 4// 2. Chain events (:) - scoped to parent state
 5// 3. Absolute events (/) - scoped to root state
 6
 7def int counter = 0;
 8
 9state System {
10    state ModuleA {
11        state A1 {
12            during {
13                counter = counter + 1;
14            }
15        }
16
17        state A2 {
18            during {
19                counter = counter + 2;
20            }
21        }
22
23        [*] -> A1;
24
25        // Local event :: - scoped to source state (A1)
26        // Equivalent to: A1 -> A2 : /ModuleA.A1.LocalEvent
27        A1 -> A2 :: LocalEvent;
28
29        // Chain event : - scoped to parent state (ModuleA)
30        // Equivalent to: A2 -> A1 : /ModuleA.ChainEvent
31        A2 -> A1 : ChainEvent;
32    }
33
34    state ModuleB {
35        state B1 {
36            during {
37                counter = counter + 10;
38            }
39        }
40
41        state B2 {
42            during {
43                counter = counter + 20;
44            }
45        }
46
47        [*] -> B1;
48
49        // Local event :: - scoped to source state (B1)
50        // Equivalent to: B1 -> B2 : /ModuleB.B1.LocalEvent
51        // This is DIFFERENT from ModuleA.A1.LocalEvent
52        B1 -> B2 :: LocalEvent;
53
54        // Chain event : - scoped to parent state (ModuleB)
55        // Equivalent to: B2 -> B1 : /ModuleB.ChainEvent
56        // This is DIFFERENT from ModuleA.ChainEvent
57        B2 -> B1 : ChainEvent;
58    }
59
60    state SharedTarget {
61        during {
62            counter = counter + 100;
63        }
64    }
65
66    [*] -> ModuleA;
67
68    // Absolute event / - scoped to root state (System)
69    // Both transitions use the SAME event: System.GlobalEvent
70    ModuleA -> SharedTarget : /GlobalEvent;
71    ModuleB -> SharedTarget : /GlobalEvent;
72
73    // Absolute event can also be used within nested states
74    // This allows cross-module communication
75    SharedTarget -> ModuleA : /ResetToA;
76    SharedTarget -> ModuleB : /ResetToB;
77}

Visualization:

Complete Event Scoping Example

Event Resolution Table:

Transition Syntax

Event Scope

Absolute Path Equivalent

A1 -> A2 :: LocalEvent

Source state (A1)

A1 -> A2 : /ModuleA.A1.LocalEvent

A2 -> A1 : ChainEvent

Parent state (ModuleA)

A2 -> A1 : /ModuleA.ChainEvent

ModuleA -> Target : /GlobalEvent

Root state (System)

Already absolute: /GlobalEvent

B1 -> B2 :: LocalEvent

Source state (B1)

B1 -> B2 : /ModuleB.B1.LocalEvent

B2 -> B1 : ChainEvent

Parent state (ModuleB)

B2 -> B1 : /ModuleB.ChainEvent

ModuleB -> Target : /GlobalEvent

Root state (System)

Already absolute: /GlobalEvent

Key Observations:

  1. Local events (::): Each source state gets its own event - ModuleA.A1.LocalEventModuleB.B1.LocalEvent

  2. Chain events (:): Each parent scope gets its own event - ModuleA.ChainEventModuleB.ChainEvent

  3. Absolute events (/): All transitions share the same event - ModuleA -> Target : /GlobalEvent = ModuleB -> Target : /GlobalEvent

See also

You can also use dot notation with absolute paths to reference events in specific states:

state Root {
    state A {
        state A1;
        state A2;

        [*] -> A1;
    }

    state B {
        state B1;
        state B2;

        [*] -> B1;
        // Reference event from A's namespace
        B1 -> B2 : /A.SpecificEvent;  // Uses Root.A.SpecificEvent
    }

    [*] -> A;
}

This allows fine-grained control over event location in the hierarchy.

Guard Conditions and Effects

Guard Conditions

Guard conditions are boolean expressions that control whether a transition can fire. They are enclosed in square brackets after the if keyword.

Syntax: StateA -> StateB : if [condition];

Supported Operators:

  • Comparison: <, >, <=, >=, ==, !=

  • Logical: &&, ||, !, and, or, not

  • Bitwise: &, |, ^

  • Arithmetic: +, -, *, /, %, **

Examples:

// Simple comparison
Idle -> Active : if [counter >= 10];

// Logical AND
Normal -> Critical : if [battery_level < 10 && charging_state == 0];

// Logical OR
LowPower -> Critical : if [temperature > 80 || error_count > 5];

// Bitwise operations
Charging -> Normal : if [(battery_level >= 90) && (charging_state & 0x01)];

// Complex expression
StateA -> StateB : if [(temp > 25.0) && (flags & 0xFF) == 0x01];

Transition Effects

Transition effects are blocks of operations executed during a transition, after the source state exits but before the target state enters.

Syntax: StateA -> StateB effect { operations };

Examples:

// Simple effect
Idle -> Running effect {
    counter = 0;
};

// Multiple operations
Critical -> Charging effect {
    charging_state = 1;
    error_count = 0;
    temperature = 25;
};

// Complex expressions
Processing -> Complete effect {
    result = sin(angle) * radius;
    flags = flags | 0x01;
    counter = counter + 1;
};

Operation Blocks and Temporary Variables

Concrete operation blocks all share the same execution model:

  • Transition effects (effect { ... })

  • Enter actions (enter { ... })

  • During actions (during { ... })

  • Exit actions (exit { ... })

Within one block, you may assign to a previously undeclared name to create a temporary variable. That temporary variable is visible only to later statements in the same block.

state Example {
    during {
        x = x + 1;     // global variable update
        tmp = x + y;   // tmp is a temporary variable
        y = tmp / 2;   // valid: tmp was assigned earlier in this block
    }
}

Temporary variables follow three important rules:

  1. They are available only after their first assignment in the current block

  2. They are discarded when the block finishes

  3. They never become part of the machine’s persistent global variable set

So this is valid:

effect {
    z = a + b;
    result = z * 2;
}

But this is still invalid:

effect {
    result = z * 2;  // ERROR: z not assigned yet in this block
    z = a + b;
}

If Blocks Inside Operation Blocks

Concrete operation blocks also support structured control flow:

  • if [condition] { ... }

  • else if [condition] { ... }

  • else { ... }

The condition syntax is the same boolean condition syntax used by transition guards.

during {
    tmp = target - measured;
    if [tmp > 0] {
        drive = tmp * 10;
    } else {
        drive = 0;
    }
    output = drive;
}

Branch scope follows the same temporary-variable rules as the rest of the block, with one extra boundary:

  1. A temporary created before the if remains visible inside every branch

  2. A temporary created inside one branch is visible only to later statements in that same branch

  3. A branch-local temporary does not become visible after the if block, even if multiple branches assign the same name

So this is valid:

effect {
    tmp = x + 1;
    if [x > 0] {
        tmp = tmp + 10;
    }
    y = tmp;
}

But this is invalid:

effect {
    if [x > 0] {
        tmp = x + 1;
    } else {
        tmp = x + 2;
    }
    y = tmp;  // ERROR: tmp was introduced only inside branches
}

Important

Temporary variables are a convenience for local calculations, not hidden machine state. If a name is already declared globally with def, assigning to it updates that global variable as usual. Only previously undeclared names are treated as temporaries.

Tip

Design Philosophy

Use global variables for persistent state that matters outside the current action or transition. Use temporary variables for one-off intermediate calculations that would otherwise force you to repeat the same expression or clutter the state machine with bookkeeping variables that have no meaning once the block completes.

Practical Example: Heating Controller

This feature is especially useful when control logic needs intermediate values that are meaningful only during one control step.

Consider a room heating controller. The machine should compute heating power from the current temperature error, but the intermediate control terms should not be stored as persistent state because they are recalculated every cycle.

def float target_temp = 22.0;
def float measured_temp = 19.5;
def float heating_power = 0.0;

state HeatingControl {
    during {
        error = target_temp - measured_temp;                  // temporary
        proportional_power = abs(error) * 15.0;              // temporary
        heating_power = (error > 0.0) ? proportional_power : 0.0;
    }
}

In this example:

  • error is useful for expressing the control intent clearly

  • proportional_power avoids repeating the formula

  • only heating_power is persistent machine state

This keeps the model readable without promoting purely local math steps into global variables.

Combined Guards and Effects

Transitions can have both guard conditions and effects:

// Guard and effect
Charging -> Normal : if [battery_level >= 100] effect {
    charging_state = 0;
    battery_level = 100;
};

// Complex guard and effect
Running -> Idle : if [(timeout > 100) && (error_count == 0)] effect {
    cleanup_flag = 1;
    status = 0;
};

Complete Example

Here’s a comprehensive example demonstrating guards and effects:

 1// Guard Conditions and Effects Example
 2// This example demonstrates complex guard conditions and transition effects
 3
 4def int battery_level = 100;
 5def int temperature = 25;
 6def int error_count = 0;
 7def int charging_state = 0;
 8
 9state PowerManagement {
10    state Normal {
11        during {
12            battery_level = battery_level - 1;
13        }
14    }
15
16    state LowPower {
17        enter {
18            error_count = 0;
19        }
20
21        during {
22            battery_level = battery_level - 0;  // No drain in low power
23        }
24    }
25
26    state Charging {
27        during {
28            battery_level = battery_level + 2;
29        }
30    }
31
32    state Critical {
33        enter {
34            error_count = error_count + 1;
35        }
36    }
37
38    [*] -> Normal;
39
40    // Simple guard condition
41    Normal -> LowPower : if [battery_level < 30];
42
43    // Complex guard with logical AND
44    Normal -> Critical : if [battery_level < 10 && charging_state == 0];
45
46    // Complex guard with logical OR
47    LowPower -> Critical : if [temperature > 80 || error_count > 5];
48
49    // Guard with comparison
50    Charging -> Normal : if [battery_level >= 90];
51
52    // Transition with effect block
53    Critical -> Charging effect {
54        charging_state = 1;
55        error_count = 0;
56        temperature = 25;
57    };
58
59    // Guard and effect combined
60    Charging -> Normal : if [battery_level >= 100] effect {
61        charging_state = 0;
62        battery_level = 100;
63    };
64
65    Critical -> [*] : if [error_count > 10];
66}

Visualization:

Guards and Effects Example

Semantic Rules

Transitions must satisfy these semantic constraints:

  1. State Existence: Both source and target states must exist in the current scope

  2. Variable Validity: Variables in guards must be declared globally; variables in operation blocks must be declared globally or assigned earlier in the same block as temporaries

  3. Expression Types: Guard conditions must evaluate to boolean values

  4. Entry Requirements: Composite states require at least one entry transition

  5. Effect Scope: Effects and lifecycle action blocks may create temporary variables, but only declared global variables persist after the block ends

Why These Rules?

  • State Existence: Prevents dangling transitions

  • Variable Validity: Ensures every reference is resolvable at the point where it is used

  • Expression Types: Maintains type safety in guard evaluation

  • Entry Requirements: Ensures deterministic composite state entry

  • Effect Scope: Keeps persistent machine state explicit while still allowing readable local calculations

Common Errors

Incorrect Usage:

// ERROR: References to undefined states
StateA -> UndefinedState :: Event;  // Semantic error

// ERROR: Missing entry transition
state Container {
    state A;
    state B;
    A -> B :: Event;  // No [*] -> A or [*] -> B
}

// ERROR: Invalid variable in guard
StateA -> StateB : if [undefined_var > 10];  // Semantic error

// ERROR: Non-boolean guard
StateA -> StateB : if [counter + 10];  // Semantic error: not boolean

// ERROR: Invalid assignment target
StateA -> StateB effect {
    undefined_var = 10;  // Semantic error
};

Correct Alternative:

// Define all states
state Root {
    state StateA;
    state StateB;
    [*] -> StateA;
    StateA -> StateB :: Event;
}

// Provide entry transition
state Container {
    state A;
    state B;
    [*] -> A;  // Required
    A -> B :: Event;
}

// Use declared variables
def int counter = 0;
state Root {
    state StateA;
    state StateB;
    [*] -> StateA;
    StateA -> StateB : if [counter > 10];  // Valid
}

// Use boolean expressions
StateA -> StateB : if [counter > 10];  // Valid: comparison returns boolean

// Assign to declared variables
def int result = 0;
state Root {
    state StateA;
    state StateB;
    [*] -> StateA;
    StateA -> StateB effect {
        result = 10;  // Valid
    };
}

Expression System

How Expressions Work:

The DSL provides a comprehensive expression system for mathematical computations, logical operations, and conditional logic. Expressions can appear in:

  • Variable initializations (def int x = expression;)

  • Guard conditions (if [expression])

  • Transition effects (variable = expression;)

  • Lifecycle actions (variable = expression;)

Expression Hierarchy

The DSL supports comprehensive expression types for mathematical and logical operations:

init_expression ::= conditional_expression
num_expression ::= conditional_expression
cond_expression ::= conditional_expression
conditional_expression ::= logical_or_expression ['?' expression ':' expression]
logical_or_expression ::= logical_and_expression [('||' | 'or') logical_and_expression]*
logical_and_expression ::= bitwise_or_expression [('&&' | 'and') bitwise_or_expression]*
bitwise_or_expression ::= bitwise_xor_expression ['|' bitwise_xor_expression]*
bitwise_xor_expression ::= bitwise_and_expression ['^' bitwise_and_expression]*
bitwise_and_expression ::= equality_expression ['&' equality_expression]*
equality_expression ::= relational_expression [('==' | '!=') relational_expression]*
relational_expression ::= shift_expression [('<' | '>' | '<=' | '>=') shift_expression]*
shift_expression ::= additive_expression [('<<' | '>>') additive_expression]*
additive_expression ::= multiplicative_expression [('+' | '-') multiplicative_expression]*
multiplicative_expression ::= power_expression [('*' | '/' | '%') power_expression]*
power_expression ::= unary_expression ['**' unary_expression]*
unary_expression ::= ['+' | '-' | '!' | 'not'] primary_expression
primary_expression ::= literal | variable | function_call | '(' expression ')'

Literal Values

Integer Literals:

def int decimal = 42;           // Decimal notation
def int hex = 0xFF;             // Hexadecimal (0x prefix)
def int binary = 0b11110000;    // Binary (0b prefix)
def int octal = 0o755;          // Octal (0o prefix)

Float Literals:

def float standard = 3.14;      // Standard notation
def float scientific = 1.5e-3;  // Scientific notation (0.0015)
def float large = 1E10;         // Large numbers (10000000000)
def float pi_const = pi;        // Mathematical constant
def float e_const = E;          // Euler's number
def float tau_const = tau;      // Tau (2*pi)

Boolean Literals:

// True values (case-insensitive)
true, True, TRUE

// False values (case-insensitive)
false, False, FALSE

Operators

Arithmetic Operators (by precedence, highest to lowest):

  1. Parentheses: () - Grouping

  2. Unary: +, - - Positive, negative

  3. Power: ** - Exponentiation

  4. Multiplicative: *, /, % - Multiply, divide, modulo

  5. Additive: +, - - Addition, subtraction

Comparison Operators:

  • Relational: <, >, <=, >=

  • Equality: ==, !=

Logical Operators:

  • Unary: !, not - Logical NOT

  • Binary: &&, and - Logical AND

  • Binary: ||, or - Logical OR

Bitwise Operators:

  • Bitwise AND: &

  • Bitwise OR: |

  • Bitwise XOR: ^

  • Left Shift: <<

  • Right Shift: >>

Operator Precedence Example:

// Without parentheses (follows precedence)
result = 2 + 3 * 4;              // Result: 14 (multiplication first)
result = 2 ** 3 + 1;             // Result: 9 (power first)
result = 10 / 2 + 3;             // Result: 8 (division first)

// With parentheses (overrides precedence)
result = (2 + 3) * 4;            // Result: 20
result = 2 ** (3 + 1);           // Result: 16
result = 10 / (2 + 3);           // Result: 2

Arithmetic vs Logical Expression Separation

Danger

The fcstm DSL strictly separates arithmetic expressions (num_expression) from logical/boolean expressions (cond_expression). Unlike common high-level languages, you cannot mix arithmetic and logical operations freely.

Key Rules:

  1. Assignments require arithmetic expressions - You cannot assign boolean results directly

  2. Guard conditions require boolean expressions - You cannot use arithmetic values as conditions

  3. Comparison operators bridge the two - They take arithmetic operands and produce boolean results

Common Errors:

// ERROR: Cannot assign boolean expression to variable
result = (x > 10);               // Syntax error: boolean in arithmetic context
result = (flag1 && flag2);       // Syntax error: logical operation in assignment

// ERROR: Cannot use arithmetic expression as condition
StateA -> StateB : if [counter]; // Syntax error: arithmetic in boolean context
StateA -> StateB : if [x + 5];   // Syntax error: arithmetic in boolean context

Correct Usage:

// Use ternary operator to convert boolean to arithmetic
result = (x > 10) ? 1 : 0;       // Valid: ternary returns arithmetic value
result = (flag1 && flag2) ? 1 : 0;  // Valid: converts boolean to int

// Use comparison operators in guard conditions
StateA -> StateB : if [counter > 0];    // Valid: comparison returns boolean
StateA -> StateB : if [x + 5 > 10];     // Valid: arithmetic in comparison

// Bitwise operations work in arithmetic context
result = flags & 0x01;           // Valid: bitwise returns arithmetic value
StateA -> StateB : if [(flags & 0x01) != 0];  // Valid: compare bitwise result

Tip

Why This Matters:

This separation ensures type safety and prevents ambiguous expressions. In languages like C, if (x + 5) is valid (non-zero is true), but in fcstm DSL you must be explicit: if [x + 5 > 0]. This makes state machine logic clearer and prevents subtle bugs.

Mathematical Functions

The DSL provides extensive mathematical function support:

Trigonometric Functions:

// Basic trigonometry
result = sin(angle);             // Sine
result = cos(angle);             // Cosine
result = tan(angle);             // Tangent

// Inverse trigonometry
result = asin(value);            // Arcsine
result = acos(value);            // Arccosine
result = atan(value);            // Arctangent

// Hyperbolic functions
result = sinh(value);            // Hyperbolic sine
result = cosh(value);            // Hyperbolic cosine
result = tanh(value);            // Hyperbolic tangent

Exponential and Logarithmic:

result = exp(x);                 // e^x
result = log(x);                 // Natural logarithm (base e)
result = log10(x);               // Logarithm base 10
result = log2(x);                // Logarithm base 2

Other Mathematical Functions:

result = sqrt(x);                // Square root
result = abs(x);                 // Absolute value
result = ceil(x);                // Ceiling (round up)
result = floor(x);               // Floor (round down)
result = round(x);               // Round to nearest integer

Conditional Expressions

Conditional expressions use ternary operator syntax for inline conditional logic.

Syntax: (condition) ? true_value : false_value

Important

The condition MUST be enclosed in parentheses.

Examples:

// Simple conditional
result = (x > 0) ? 1 : -1;

// With variables
status = (temperature > 25.0) ? 1 : 0;

// Nested conditionals
level = (temp > 30) ? 3 : ((temp > 20) ? 2 : 1);

// With complex conditions
value = (counter >= 10 && flags & 0x01) ? 100 : 0;

// With expressions in branches
result = (mode == 1) ? (base * 2) : (base / 2);

Common Error:

Warning

// ERROR: Missing parentheses around condition
result = x > 0 ? 1 : -1;  // Syntax error

// CORRECT: Parentheses required
result = (x > 0) ? 1 : -1;

Complete Expression Example

Here’s a comprehensive example demonstrating all expression capabilities:

 1// Expression System Demonstration
 2// This example showcases the comprehensive expression capabilities
 3
 4def int counter = 0;
 5def int flags = 0xFF;
 6def float temperature = 25.5;
 7def float angle = 0.0;
 8
 9state ExpressionDemo {
10    state ArithmeticOps {
11        enter {
12            // Basic arithmetic
13            counter = 10 + 5 * 2;           // Result: 20 (precedence)
14            counter = (10 + 5) * 2;         // Result: 30 (parentheses)
15            counter = 2 ** 3;               // Result: 8 (power)
16            counter = 17 % 5;               // Result: 2 (modulo)
17        }
18    }
19
20    state BitwiseOps {
21        enter {
22            // Bitwise operations
23            flags = 0xFF & 0x0F;            // Result: 0x0F (AND)
24            flags = 0xF0 | 0x0F;            // Result: 0xFF (OR)
25            flags = 0xFF ^ 0x0F;            // Result: 0xF0 (XOR)
26            flags = 0x01 << 4;              // Result: 0x10 (left shift)
27            flags = 0x80 >> 2;              // Result: 0x20 (right shift)
28        }
29    }
30
31    state MathFunctions {
32        enter {
33            // Trigonometric functions
34            temperature = sin(angle) * 100.0;
35            temperature = cos(angle) + 1.0;
36            temperature = tan(angle);
37
38            // Other math functions
39            temperature = sqrt(16.0);        // Result: 4.0
40            temperature = abs(-5.5);         // Result: 5.5
41            temperature = ceil(3.2);         // Result: 4.0
42            temperature = floor(3.8);        // Result: 3.0
43            temperature = round(3.5);        // Result: 4.0
44        }
45    }
46
47    state ComplexExpr {
48        enter {
49            // Complex expressions combining multiple operations
50            counter = (10 + 5) * 2 - 3;      // Result: 27
51            flags = (0xFF & 0x0F) | 0x10;    // Result: 0x1F
52            temperature = sqrt(16.0) + 10.0; // Result: 14.0
53        }
54    }
55
56    [*] -> ArithmeticOps;
57    ArithmeticOps -> BitwiseOps :: Next;
58    BitwiseOps -> MathFunctions :: Next;
59    MathFunctions -> ComplexExpr :: Next;
60    ComplexExpr -> [*];
61}

Visualization:

Expression System Demonstration

Semantic Rules

Expressions must follow these semantic constraints:

  1. Variable Declarations: All referenced variables must be declared

  2. Type Consistency: Operations must be performed on compatible types

  3. Function Arguments: Mathematical functions require appropriate argument types

  4. Boolean Context: Conditional guards must evaluate to boolean values

  5. Operator Compatibility: Operators must be used with compatible operand types

Why These Rules?

  • Variable Declarations: Prevents undefined behavior

  • Type Consistency: Ensures type safety in generated code

  • Function Arguments: Prevents runtime errors in mathematical operations

  • Boolean Context: Maintains semantic correctness in control flow

  • Operator Compatibility: Ensures meaningful operations

Common Errors

Incorrect Usage:

// ERROR: Undefined variable reference
result = unknown_var + 10;  // Semantic error

// ERROR: Type mismatch (mixing incompatible types)
// Note: DSL is dynamically typed, but some operations may fail

// ERROR: Invalid function arguments
result = sqrt(-1);  // May cause runtime error

// ERROR: Malformed conditional (missing parentheses)
result = x > 0 ? 1 : -1;  // Syntax error

Correct Alternative:

// Declare all variables
def int result = 0;
def int known_var = 10;

state Example {
    enter {
        result = known_var + 10;  // Valid
    }
}

// Use appropriate function arguments
def float value = 16.0;
state Example {
    enter {
        result = sqrt(value);  // Valid: positive argument
    }
}

// Use parentheses in conditionals
result = (x > 0) ? 1 : -1;  // Valid

Lifecycle Actions

Note

How Lifecycle Actions Work:

Lifecycle actions define behavior that executes at specific points in a state’s lifetime:

  • Enter Actions: Execute once when entering a state

  • During Actions: Execute repeatedly while a state is active

  • Exit Actions: Execute once when leaving a state

For composite states, lifecycle actions can have aspects (before/after) that control execution order relative to child states.

Concrete lifecycle action blocks use the same operation semantics as transition effects, including support for block-local temporary variables introduced by assignment inside one enter/during/exit block.

Action Types

States support three lifecycle phases with corresponding action definitions:

enter_definition ::= enterOperations | enterAbstractFunc | enterRefFunc
during_definition ::= duringOperations | duringAbstractFunc
exit_definition ::= exitOperations | exitAbstractFunc | exitRefFunc

enterOperations ::= 'enter' [ID] '{' operation* '}'
enterAbstractFunc ::= 'enter' 'abstract' ID [MULTILINE_COMMENT]
enterRefFunc ::= 'enter' [ID] 'ref' chain_id

duringOperations ::= 'during' ['before'|'after'] [ID] '{' operation* '}'
duringAbstractFunc ::= 'during' ['before'|'after'] 'abstract' ID [MULTILINE_COMMENT]

exitOperations ::= 'exit' [ID] '{' operation* '}'
exitAbstractFunc ::= 'exit' 'abstract' ID [MULTILINE_COMMENT]
exitRefFunc ::= 'exit' [ID] 'ref' chain_id

Enter Actions

Enter actions execute when a state is entered from outside.

Concrete Operations:

state Active {
    // Simple enter action
    enter {
        counter = 0;
        status_flag = 0x1;
    }

    // Named enter action (for reference)
    enter InitializeSystem {
        counter = 0;
        flags = 0xFF;
        temperature = 25.0;
    }
}

Abstract Functions:

Abstract enter actions declare functions that must be implemented in generated code:

state Active {
    // Simple abstract enter
    enter abstract initialize_system;

    // Abstract enter with documentation
    enter abstract setup_resources /*
        Initialize system resources and peripherals.
        This function must allocate memory, open files,
        and configure hardware interfaces.
        TODO: Implement in generated code framework
    */
}

Reference Actions:

Reference actions reuse enter actions from other states:

state BaseState {
    enter CommonInit {
        counter = 0;
        flags = 0xFF;
    }
}

state DerivedState {
    // Reuse BaseState's enter action
    enter ref BaseState.CommonInit;

    // Can also reference global actions
    enter ref /GlobalInit;
}

Important

ref is an action reuse mechanism, not a state reference and not an event reference. It resolves to a previously named lifecycle action under a state scope. The target may be a named enter, during, exit, or >> during action, including an abstract action. Relative paths are resolved from the current state path, while / starts from the root state.

Tip

Use ref when several states should share the same lifecycle behavior and you want one source of truth for that action body or abstract hook. Prefer absolute paths for cross-state reuse to avoid ambiguity. Use abstract instead when the action is only a contract to be implemented in generated code rather than a reuse of an existing named action.

During Actions

During actions execute while a state is active. The behavior differs between leaf and composite states.

Leaf State During Actions:

Leaf states use plain during without aspect keywords:

state Running {
    // Executes every cycle while Running is active
    during {
        heartbeat_counter = heartbeat_counter + 1;
        temperature = temperature + 0.1;
    }
}

Composite State During Actions:

Composite states MUST use before or after aspects:

state Parent {
    // Executes when entering a child from outside
    // NOT during child-to-child transitions
    during before {
        monitor_counter = monitor_counter + 1;
    }

    // Executes when exiting to outside from a child
    // NOT during child-to-child transitions
    during after {
        cleanup_flag = 1;
    }

    state Child1;
    state Child2;

    [*] -> Child1;
    Child1 -> Child2 :: Switch;  // during before/after NOT triggered
    Child2 -> [*];
}

Abstract During Actions:

state Processing {
    // Leaf state abstract during
    during abstract process_data;

    // With documentation
    during abstract process_data /*
        Process incoming data packets.
        TODO: Implement data processing logic
    */
}

state Container {
    // Composite state abstract during with aspect
    during before abstract pre_process /*
        Pre-processing before child state execution.
        TODO: Implement pre-processing logic
    */

    during after abstract post_process;

    state Child;
    [*] -> Child;
}

Exit Actions

Exit actions execute when leaving a state to outside.

Concrete Operations:

state Active {
    exit {
        save_state = current_value;
        cleanup_flag = 0x1;
        status = 0;
    }

    // Named exit action
    exit CleanupResources {
        flags = 0x00;
        counter = 0;
    }
}

Abstract Functions:

state Active {
    exit abstract cleanup_resources;

    exit abstract finalize_operations /*
        Clean up resources before exit.
        Release memory, close files, and shutdown hardware.
        TODO: Implement in generated code framework
    */
}

Reference Actions:

state BaseState {
    exit CommonCleanup {
        cleanup_flag = 1;
        counter = 0;
    }
}

state DerivedState {
    exit ref BaseState.CommonCleanup;
}

Aspect Actions

Aspect actions apply to all descendant leaf states using the >> prefix.

Syntax:

state Root {
    // Executes before EVERY descendant leaf state's during action
    >> during before {
        global_counter = global_counter + 1;
    }

    // Executes after EVERY descendant leaf state's during action
    >> during after {
        global_counter = global_counter + 100;
    }

    state Child {
        state GrandChild {
            during {
                local_counter = local_counter + 10;
            }
        }

        [*] -> GrandChild;
    }

    [*] -> Child;
}

Execution Order for GrandChild:

  1. Root >> during before (global_counter += 1)

  2. GrandChild.during (local_counter += 10)

  3. Root >> during after (global_counter += 100)

Hierarchical Execution Order

Understanding execution order in hierarchical state machines is crucial. Here’s a complete example:

 1// Hierarchical Execution Order Example
 2// This example demonstrates how aspect actions execute in hierarchical state machines
 3
 4def int execution_log = 0;
 5
 6state HierarchyDemo {
 7    // Root-level aspect actions apply to ALL descendant leaf states
 8    >> during before {
 9        execution_log = execution_log + 1000;  // Step 1: Root aspect before
10    }
11
12    >> during after {
13        execution_log = execution_log + 9000;  // Step 5: Root aspect after
14    }
15
16    state Parent {
17        // Composite state's during before/after execute ONLY on entry/exit
18        // NOT during child-to-child transitions!
19        during before {
20            execution_log = execution_log + 100;  // Only on [*] -> Child entry
21        }
22
23        during after {
24            execution_log = execution_log + 900;  // Only on Child -> [*] exit
25        }
26
27        // Parent-level aspect actions also apply to all descendant leaf states
28        >> during before {
29            execution_log = execution_log + 10;  // Step 2: Parent aspect before
30        }
31
32        >> during after {
33            execution_log = execution_log + 90;  // Step 4: Parent aspect after
34        }
35
36        state ChildA {
37            // Leaf state's during action
38            during {
39                execution_log = execution_log + 1;  // Step 3: Leaf during
40            }
41        }
42
43        state ChildB {
44            during {
45                execution_log = execution_log + 2;
46            }
47        }
48
49        [*] -> ChildA;
50        ChildA -> ChildB :: Switch;  // during before/after NOT triggered
51        ChildB -> [*] :: Exit;
52    }
53
54    [*] -> Parent;
55    Parent -> [*];
56}

Visualization:

Hierarchical Execution Order

Important

Execution Scenarios:

Scenario 1: Initial Entry (HierarchyDemo -> Parent -> ChildA)

  1. HierarchyDemo.enter (if defined)

  2. Parent.enter (if defined)

  3. Parent.during before executes (execution_log += 100)

  4. ChildA.enter (if defined)

Scenario 2: During Phase (while ChildA is active, each cycle)

  1. HierarchyDemo >> during before (execution_log += 1000)

  2. Parent >> during before (execution_log += 10)

  3. ChildA.during (execution_log += 1)

  4. Parent >> during after (execution_log += 90)

  5. HierarchyDemo >> during after (execution_log += 9000)

Total per cycle: 10101

Scenario 3: Child-to-Child Transition (ChildA -> ChildB :: Switch)

  1. ChildA.exit (if defined)

  2. Transition effect (if any)

  3. ChildB.enter (if defined)

CRITICAL: Parent.during before/after are NOT executed!

Scenario 4: Exit from Composite State (ChildB -> [*] :: Exit)

  1. ChildB.exit (if defined)

  2. Parent.during after executes (execution_log += 900)

  3. Parent.exit (if defined)

  4. HierarchyDemo.exit (if defined)

Lifecycle Flow Diagrams:

Lifecycle of Composite States
Lifecycle of Leaf States

Abstract and Reference Actions Example

Here’s a complete example demonstrating abstract functions and action references:

 1// Abstract and Reference Actions Example
 2// This example demonstrates abstract function declarations and action references
 3
 4def int init_flag = 0;
 5def int cleanup_flag = 0;
 6
 7state AbstractReferenceDemo {
 8    // Root-level abstract functions that can be referenced
 9    enter abstract GlobalInit /*
10        Global initialization function.
11        TODO: Implement in generated code framework
12    */
13
14    exit abstract GlobalCleanup /*
15        Global cleanup function.
16        TODO: Implement in generated code framework
17    */
18
19    state BaseState {
20        // Abstract actions - must be implemented in generated code
21        enter abstract HardwareInit /*
22            Initialize hardware peripherals and sensors.
23            This function must be implemented in the target platform.
24            TODO: Implement in generated code framework
25        */
26
27        enter {
28            init_flag = 1;
29            cleanup_flag = 0;
30        }
31
32        exit abstract HardwareCleanup /*
33            Clean up hardware resources before exit.
34            TODO: Implement in generated code framework
35        */
36
37        exit {
38            cleanup_flag = 1;
39            init_flag = 0;
40        }
41    }
42
43    state DerivedState1 {
44        // Reference global abstract functions
45        enter ref /GlobalInit;
46        exit ref /GlobalCleanup;
47
48        // Can also have its own abstract actions
49        during abstract ProcessData /*
50            Process sensor data in DerivedState1.
51            TODO: Implement in generated code framework
52        */
53    }
54
55    state DerivedState2 {
56        // Can reference the same global functions
57        enter ref /GlobalInit;
58        exit ref /GlobalCleanup;
59
60        during abstract ProcessData /*
61            Process sensor data in DerivedState2.
62            Different implementation from DerivedState1.
63            TODO: Implement in generated code framework
64        */
65    }
66
67    [*] -> BaseState;
68    BaseState -> DerivedState1 :: Start;
69    DerivedState1 -> DerivedState2 :: Switch;
70    DerivedState2 -> [*];
71}

Visualization:

Abstract and Reference Actions

Semantic Rules

Lifecycle actions must adhere to these constraints:

  1. Variable Validity: All referenced variables must be declared

  2. Aspect Restrictions: before and after aspects only apply to composite states

  3. Assignment Targets: Only declared variables can be assigned values

  4. Expression Types: Assignment expressions must be type-compatible

  5. Reference Validity: Referenced actions must exist in the specified state

Tip

Why These Rules?

  • Variable Validity: Prevents undefined behavior

  • Aspect Restrictions: Enforces correct lifecycle semantics

  • Assignment Targets: Ensures all assignments are valid

  • Expression Types: Maintains type safety

  • Reference Validity: Prevents dangling references

Common Errors

Warning

Incorrect Usage:

// ERROR: Undefined variable in action
state Example {
    enter {
        undefined_var = 10;  // Semantic error
    }
}

// ERROR: Aspect on leaf state
state LeafState {
    during before {  // Semantic error: leaf states can't have aspects
        x = 1;
    }
}

// ERROR: Plain during on composite state
state CompositeState {
    state Child;
    [*] -> Child;

    during {  // Semantic error: composite states need before/after
        x = 1;
    }
}

// ERROR: Invalid reference
state Example {
    enter ref NonExistentState.Action;  // Semantic error
}

Correct Alternative:

// Declare all variables
def int result = 0;

state Example {
    enter {
        result = 10;  // Valid
    }
}

// Use plain during for leaf states
state LeafState {
    during {  // Correct
        result = 1;
    }
}

// Use aspects for composite states
state CompositeState {
    state Child;
    [*] -> Child;

    during before {  // Correct
        result = 1;
    }
}

// Reference existing actions
state BaseState {
    enter CommonInit {
        result = 0;
    }
}

state DerivedState {
    enter ref BaseState.CommonInit;  // Valid
}

Real-World Example: Smart Thermostat

To demonstrate all DSL features in a realistic context, here’s a comprehensive smart thermostat controller implementation:

  1// Real-World Example: Smart Thermostat Controller
  2// This comprehensive example demonstrates a realistic state machine
  3// for a smart thermostat with multiple operational modes
  4
  5// System variables
  6def int current_temp = 20;          // Current temperature in Celsius
  7def int target_temp = 22;           // Target temperature
  8def int heating_power = 0;          // Heating power level (0-100)
  9def int cooling_power = 0;          // Cooling power level (0-100)
 10def int fan_speed = 0;              // Fan speed (0-3)
 11def int error_code = 0;             // Error code for diagnostics
 12def int runtime_hours = 0;          // Total runtime in hours
 13def int maintenance_counter = 0;    // Maintenance cycle counter
 14
 15state ThermostatController {
 16    // Global aspect actions for monitoring and logging
 17    >> during before abstract LogSystemState /*
 18        Log current system state for diagnostics.
 19        Called before every state's during action.
 20        TODO: Implement logging mechanism
 21    */
 22
 23    >> during after abstract UpdateDisplay /*
 24        Update user interface display.
 25        Called after every state's during action.
 26        TODO: Implement display update
 27    */
 28
 29    state Initializing {
 30        enter {
 31            // Reset all system variables
 32            heating_power = 0;
 33            cooling_power = 0;
 34            fan_speed = 0;
 35            error_code = 0;
 36        }
 37
 38        enter abstract SystemSelfTest /*
 39            Perform hardware self-test on startup.
 40            Check sensors, actuators, and communication.
 41            TODO: Implement self-test procedure
 42        */
 43
 44        exit {
 45            maintenance_counter = 0;
 46        }
 47    }
 48
 49    state OperationalMode {
 50        // Composite state lifecycle actions
 51        during before {
 52            // Executed when entering from Initializing
 53            runtime_hours = runtime_hours + 1;
 54        }
 55
 56        during after {
 57            // Executed when exiting to Maintenance or Error
 58            fan_speed = 0;
 59        }
 60
 61        state Idle {
 62            enter {
 63                heating_power = 0;
 64                cooling_power = 0;
 65                fan_speed = 1;  // Low fan for circulation
 66            }
 67
 68            during {
 69                maintenance_counter = maintenance_counter + 1;
 70            }
 71        }
 72
 73        state Heating {
 74            enter {
 75                cooling_power = 0;
 76                fan_speed = 2;
 77            }
 78
 79            during {
 80                // Proportional heating control
 81                heating_power = (target_temp - current_temp) * 10;
 82                maintenance_counter = maintenance_counter + 1;
 83            }
 84
 85            exit {
 86                heating_power = 0;
 87            }
 88        }
 89
 90        state Cooling {
 91            enter {
 92                heating_power = 0;
 93                fan_speed = 3;
 94            }
 95
 96            during {
 97                // Proportional cooling control
 98                cooling_power = (current_temp - target_temp) * 10;
 99                maintenance_counter = maintenance_counter + 1;
100            }
101
102            exit {
103                cooling_power = 0;
104            }
105        }
106
107        state AutoMode {
108            // Automatic mode with intelligent switching
109            during before abstract AnalyzeEnvironment /*
110                Analyze environmental conditions.
111                TODO: Implement environment analysis
112            */
113
114            state AutoHeating {
115                during {
116                    heating_power = (target_temp - current_temp) * 10;
117                }
118            }
119
120            state AutoCooling {
121                during {
122                    cooling_power = (current_temp - target_temp) * 10;
123                }
124            }
125
126            state AutoIdle {
127                during {
128                    heating_power = 0;
129                    cooling_power = 0;
130                }
131            }
132
133            [*] -> AutoIdle;
134
135            // Automatic transitions based on temperature
136            AutoIdle -> AutoHeating : if [current_temp < target_temp - 2];
137            AutoIdle -> AutoCooling : if [current_temp > target_temp + 2];
138            AutoHeating -> AutoIdle : if [current_temp >= target_temp];
139            AutoCooling -> AutoIdle : if [current_temp <= target_temp];
140        }
141
142        [*] -> Idle;
143
144        // Mode transitions with guards
145        Idle -> Heating : if [current_temp < target_temp - 1] effect {
146            fan_speed = 2;
147        };
148
149        Idle -> Cooling : if [current_temp > target_temp + 1] effect {
150            fan_speed = 3;
151        };
152
153        Heating -> Idle : if [current_temp >= target_temp] effect {
154            fan_speed = 1;
155        };
156
157        Cooling -> Idle : if [current_temp <= target_temp] effect {
158            fan_speed = 1;
159        };
160
161        // Switch to auto mode
162        Idle -> AutoMode :: EnableAuto;
163        Heating -> AutoMode :: EnableAuto;
164        Cooling -> AutoMode :: EnableAuto;
165
166        // Exit auto mode
167        AutoMode -> Idle :: DisableAuto effect {
168            heating_power = 0;
169            cooling_power = 0;
170        };
171
172        // Emergency transitions
173        Heating -> Idle : if [heating_power > 100] effect {
174            error_code = 1;
175        };
176
177        Cooling -> Idle : if [cooling_power > 100] effect {
178            error_code = 2;
179        };
180    }
181
182    state Maintenance {
183        enter {
184            // Safe state for maintenance
185            heating_power = 0;
186            cooling_power = 0;
187            fan_speed = 0;
188        }
189
190        enter abstract EnterMaintenanceMode /*
191            Prepare system for maintenance.
192            Lock out user controls and disable actuators.
193            TODO: Implement maintenance mode entry
194        */
195
196        exit abstract ExitMaintenanceMode /*
197            Exit maintenance mode and restore normal operation.
198            TODO: Implement maintenance mode exit
199        */
200    }
201
202    state ErrorState {
203        enter {
204            // Emergency shutdown
205            heating_power = 0;
206            cooling_power = 0;
207            fan_speed = 0;
208        }
209
210        enter abstract LogError /*
211            Log error details for diagnostics.
212            TODO: Implement error logging
213        */
214
215        during abstract DiagnoseError /*
216            Attempt to diagnose and recover from error.
217            TODO: Implement error diagnosis
218        */
219    }
220
221    // Initial transition
222    [*] -> Initializing;
223
224    // Normal operation flow
225    Initializing -> OperationalMode : if [error_code == 0];
226
227    // Maintenance transitions
228    OperationalMode -> Maintenance :: MaintenanceRequest;
229    Maintenance -> OperationalMode :: MaintenanceComplete;
230
231    // Error handling
232    Initializing -> ErrorState : if [error_code != 0];
233    OperationalMode -> ErrorState : if [error_code != 0];
234    ErrorState -> Initializing :: Reset effect {
235        error_code = 0;
236    };
237
238    // Scheduled maintenance
239    OperationalMode -> Maintenance : if [maintenance_counter > 1000] effect {
240        maintenance_counter = 0;
241    };
242
243    // Shutdown
244    OperationalMode -> [*] :: Shutdown;
245    Maintenance -> [*] :: Shutdown;
246    ErrorState -> [*] :: ForcedShutdown;
247}

Visualization:

Smart Thermostat State Machine

Tip

Key Design Patterns Demonstrated:

  1. Hierarchical Decomposition: OperationalMode contains multiple sub-modes (Idle, Heating, Cooling, AutoMode)

  2. Aspect-Oriented Programming: Global >> during before/after for logging and display updates

  3. Proportional Control: Heating/cooling power calculated based on temperature difference

  4. Automatic Mode Switching: AutoMode intelligently switches between heating, cooling, and idle

  5. Error Handling: Transitions to ErrorState on abnormal conditions

  6. Maintenance Scheduling: Automatic transition to maintenance after 1000 cycles

  7. Abstract Functions: Hardware-specific operations declared as abstract for platform implementation

Execution Flow Example:

Starting from Initializing:

  1. System performs self-test (abstract function)

  2. If error_code == 0, transitions to OperationalMode.Idle

  3. OperationalMode.during before executes (increments runtime_hours)

  4. While in Idle: - Global >> during before logs system state - Idle.during increments maintenance_counter - Global >> during after updates display

  5. If current_temp < target_temp - 1, transitions to Heating

  6. While in Heating: - Heating.during calculates proportional heating power - If current_temp >= target_temp, transitions back to Idle

  7. After 1000 cycles, transitions to Maintenance

Comment Styles

The DSL supports multiple comment formats for documentation:

Line Comments:

// C++ style line comment
# Python style line comment

def int counter = 0;  // Inline comment
def int flags = 0xFF; # Another inline comment

Block Comments:

/*
 * Multi-line block comment
 * Used for detailed documentation
 */

Abstract Function Documentation:

enter abstract InitializeHardware /*
    Initialize hardware peripherals and sensors.

    This function must:
    1. Configure GPIO pins
    2. Initialize SPI/I2C interfaces
    3. Calibrate sensors
    4. Verify hardware connectivity

    Returns: 0 on success, error code on failure
    TODO: Implement in generated code framework
*/

Documentation Best Practices

Variable Documentation:

// System state variables
def int system_state = 0;         // 0=init, 1=running, 2=error
def int error_count = 0;          // Number of errors since startup

// Sensor readings (in SI units)
def float temperature = 20.0;     // Temperature in Celsius
def float pressure = 101.325;     // Pressure in kPa

// Control outputs (0-100 range)
def int heating_power = 0;        // Heating power percentage
def int cooling_power = 0;        // Cooling power percentage

State Documentation:

state System {
    // Initialization phase - runs once at startup
    state Initializing {
        enter {
            // Reset all system variables to safe defaults
            error_count = 0;
            system_state = 0;
        }
    }

    // Normal operation - main system loop
    state Running {
        // Active processing state
        state Active {
            during {
                // Increment heartbeat counter for watchdog
                heartbeat = heartbeat + 1;
            }
        }

        // Idle state - low power mode
        state Idle;

        [*] -> Active;
    }

    [*] -> Initializing;
    Initializing -> Running : if [error_count == 0];
}

Transition Documentation:

// Transition to low power mode when battery is low
// and no critical tasks are active
Normal -> LowPower : if [
    (battery_level < 30) &&
    (charging_state == 0) &&
    (critical_task_active == 0)
] effect {
    // Reduce system clock frequency
    clock_divider = 8;
    // Disable non-essential peripherals
    peripheral_enable = 0x01;
};

Semantic Validation Rules

Comprehensive Validation

The DSL parser performs extensive semantic validation during the parsing process:

Variable Validation:

  1. Unique variable names across the program

  2. All referenced variables must be declared

  3. Type consistency in assignments and expressions

  4. Valid initialization expressions

State Validation:

  1. Unique state names within each scope

  2. Valid state references in all transitions

  3. Required entry transitions for composite states

  4. Proper hierarchical nesting

Expression Validation:

  1. Well-formed mathematical and logical expressions

  2. Valid function calls with appropriate arguments

  3. Proper operator precedence and associativity

  4. Type compatibility in operations

Structural Validation:

  1. Proper nesting of composite states

  2. Valid lifecycle action placement

  3. Correct transition connectivity

  4. Aspect action restrictions

Error Handling

The parser provides detailed error messages for common mistakes:

Syntax Errors:

  • Malformed expressions and statements

  • Missing punctuation and keywords

  • Invalid token sequences

  • Incorrect operator usage

Semantic Errors:

  • Undefined variable or state references

  • Type mismatches in operations

  • Structural inconsistencies

  • Invalid lifecycle action placement

Example Error Messages:

Error: Undefined variable 'unknown_var' at line 15
Error: Duplicate state name 'Active' in scope 'System' at line 23
Error: Missing entry transition for composite state 'Container' at line 45
Error: Invalid aspect 'before' on leaf state 'Running' at line 67

Import Assembly

Overview

Import support lets FCSTM projects grow from a single file into a multi-file assembly structure. The model stays conservative:

  • import is compile-time assembly, not a runtime module system

  • each import brings in exactly one root state

  • every import must use an explicit as Alias

  • variables are isolated by default unless a def mapping shares them

  • events remain instance-local by default unless an event mapping rewrites a module absolute event

Minimal Import

Imported module:

import_worker.fcstm
def int sensor_input = 0;
def int speed = 0;

state WorkerModule named "Worker Module" {
    event Start named "Worker Start";
    event Stop named "Worker Stop";

    state Idle;
    state Running;

    [*] -> Idle;
    Idle -> Running : /Start effect {
        speed = sensor_input;
    }
    Running -> Idle : /Stop;
}

Host machine:

import_host_basic.fcstm
state System {
    import "./import_worker.fcstm" as Worker;
    [*] -> Worker;
}

What happens here:

  • the host root state System remains the root of the final machine

  • the imported root state is mounted under the alias Worker

  • [*] -> Worker; makes the imported subtree the initial child of the host

Final assembled model:

Final assembled model for the minimal import example

Important

The alias is mandatory. Import intentionally keeps module boundaries explicit so the assembled state paths stay predictable.

Using named on imports

You can override the imported root state’s display label without changing its semantic path:

state System {
    import "./import_worker.fcstm" as Worker named "Left Worker";
    [*] -> Worker;
}

named changes visualization-facing display text only. It does not change the alias used for semantic path resolution.

Variable Mapping with def

Imported variables do not merge into host variables automatically. Use def mapping when a host variable should receive values from the imported module:

import_host_mapped.fcstm
def int left_sensor_input = 0;
def int plant_speed = 0;

state System {
    event Start;
    event Stop;

    import "./import_worker.fcstm" as LeftWorker named "Left Worker" {
        def sensor_* -> left_$1;
        def speed -> plant_speed;
        event /Start -> Start named "Shared Start";
        event /Stop -> Stop named "Shared Stop";
    }

    [*] -> LeftWorker;
}

This demonstrates:

  • wildcard mapping: def sensor_* -> left_$1;

  • exact mapping: def speed -> plant_speed;

  • explicit sharing instead of implicit name-based merging

Event Mapping with event

Event mapping is stricter than variable mapping. The left-hand side must be a module absolute event path:

  • event /Start -> Start named "Shared Start";

  • event /Stop -> Stop named "Shared Stop";

Rules to remember:

  • only imported absolute events such as /Start can appear on the left

  • the host-side target may be relative or absolute

  • named on the mapping overrides the final host event display name

Directory-organized subsystem entry via main.fcstm

When an imported subsystem becomes too large for one file, a common pattern is to organize it around main.fcstm as the documented entry file.

Host file:

import_host_directory.fcstm
state Factory {
    import "./import_line/main.fcstm" as Line;
    [*] -> Line;
}

Subsystem entry:

import_line/main.fcstm
def int line_speed = 0;

state Line {
    event Start;

    import "./subsystems/robot.fcstm" as Robot {
        def speed -> line_speed;
        event /Start -> Start;
    }

    [*] -> Robot;
}

Nested imported file inside the subsystem:

import_line/subsystems/robot.fcstm
def int speed = 0;

state RobotModule {
    event Start;

    state Idle;
    state Moving;

    [*] -> Idle;
    Idle -> Moving : /Start effect {
        speed = speed + 1;
    }
}

In the current syntax, the host imports the entry file explicitly:

import "./import_line/main.fcstm" as Line;

This keeps the host-facing import contract stable while still allowing the subsystem to grow internally.

Final assembled model from the entry file:

Final assembled model for the directory entry import example

Common Mistakes

The most common import mistakes are:

  • forgetting as Alias and expecting alias inference

  • assuming named changes semantic state or event paths

  • expecting unmapped imported variables to merge automatically

  • mapping a non-absolute imported event on the left-hand side

  • forgetting to point the host import at the subsystem entry file such as ./import_line/main.fcstm

Public Entry Points Stay the Same

Import support is intentionally integrated into the existing public entry points. Once your files are organized correctly, the commands do not change:

pyfcstm plantuml -i import_host_mapped.fcstm -o import_host_mapped.puml
pyfcstm generate -i import_host_directory.fcstm -t ./templates/python -o ./out
pyfcstm simulate -i import_host_directory.fcstm

That is the intended user experience: richer project structure, unchanged public command shape.

Summary

This tutorial has covered the complete PyFCSTM DSL syntax, including:

  • Variable Definitions: Typed variables with initialization expressions

  • State Definitions: Leaf states, composite states, pseudo states, and named states

  • Transitions: Entry, normal, exit, and forced transitions with guards and effects

  • Forced Transitions: Syntactic sugar that expands to multiple normal transitions with shared events

  • Event Scoping: Three mechanisms - local events (::), chain events (:), and absolute events (/)

  • Event Namespaces: Understanding how events are resolved in hierarchical state machines

  • Expressions: Comprehensive operator support and mathematical functions

  • Lifecycle Actions: Enter, during, and exit actions with aspect-oriented programming

  • Hierarchical Execution: Execution order in nested states with composite and leaf states

  • Abstract Functions: Declaring platform-specific implementations

  • Reference Actions: Reusing actions across states

  • Import Assembly: Multi-file composition through aliases, variable mappings, and event mappings

  • Comments: Line and block comments for documentation

Important

Key Concepts:

  • Hierarchical State Machines: States can contain nested substates, enabling modular design

  • Aspect-Oriented Programming: >> during before/after actions apply to all descendant leaf states

  • Composite State Lifecycle: during before/after execute only on entry/exit, not during child-to-child transitions

  • Event Namespacing: Three scoping mechanisms (:: for local, : for chain, / for absolute)

  • Event Resolution: All event scoping mechanisms are equivalent to absolute paths with different starting points

  • Import Boundaries: import keeps file assembly explicit through aliases and mapping rules

  • Semantic Validation: Comprehensive validation ensures correct state machine definitions

See also

Additional Resources:

  • Grammar definition: pyfcstm/dsl/grammar/Grammar.g4

  • Parser implementation: pyfcstm/dsl/parse.py

  • Model system: pyfcstm/model/model.py

  • Test suite: test/testfile/sample_codes/

For implementation details, refer to the grammar definition, parsing pipeline, and model system documentation. The test suite provides additional examples and validation patterns for complex use cases.