MN 113-10: Primmel State Machines

#<Coradoc::CoreModel::CommentLine:0x00007fd95cc92bc8> #<Coradoc::CoreModel::CommentLine:0x00007fd95cc92948> #<Coradoc::CoreModel::CommentLine:0x00007fd95cc926c8> #<Coradoc::CoreModel::CommentLine:0x00007fd95cc92448> #<Coradoc::CoreModel::CommentLine:0x00007fd95cc921c8> #<Coradoc::CoreModel::CommentLine:0x00007fd95cc91f48>

1. State machines overview

1.1. General

State machines describe the lifecycle of an entity: its possible states, the transitions between them, and the side-effects (cascades) that fire on transitions.

1.2. Why state machines exist

MMEL 0.1 has exclusive_gateway (decision points in process flow) but no first-class state machine construct. Side-effects on state changes are implicit in the flow topology, not declarative.

For lifecycle-heavy domains (certification, audit, approval), you need to declare: "when this entity's state changes, these dependent entities also change." BPMN-style flowcharts aren't enough.

State machines complement process (which describes activities) by describing entity state evolution.

1.3. Relationship to other Primmel constructs

Construct Relationship
class#data A state machine is bound to a data class. The class's `status` field holds the current state.
process Processes describe activities that may trigger state transitions. State machines describe the resulting state evolution.
exclusive_gateway Gateways decide which process flow branch to take. State machines constrain which transitions are valid from the current state.
provision A transition's guard often validates a provision.

1.4. Lifecycle vs activity

The distinction:

  • Activity (process): "the lab submits the test report"
  • Lifecycle (state machine): "the test report's status changes from DRAFT to SUBMITTED, and form instances on it become LOCKED"

Both are needed. The activity description tells you WHO does WHAT. The lifecycle description tells you what SIDE-EFFECTS follow.

2. State machine syntax

2.1. General

A state machine declaration uses the state_machine keyword followed by an entity name (matching a class#data ) and a brace-delimited body.

2.2. Syntax

state_machine <EntityName> {
  initial <StateName>
  states {
    <StateName>
    <StateName>
    // ...
  }
  transition <From> -> <To>
    [action <ActionName>] {
    guard "<OCL predicate>"
    cascade <EntityName> {
      where "<OCL predicate>"
      set {
        <field>: <value>
      }
    }
    reference { <ReferenceID> }
  }
}
  1. Initial state. Required.
  2. State list. Optional (inferred from transitions if omitted).
  3. Transition declaration. Zero or more.
  4. Optional guard predicate.
  5. Optional cascade block. Zero or more per transition.
  6. Optional normative references.

2.3. Properties

Property Required Description
initial yes The starting state for new entities.
states no Explicit state list. Inferred from transitions if omitted. Recommended for documentation.
transition no Transition declaration. Zero or more.

2.4. Binding to an entity

The <EntityName> after state_machine must match a class#data declared elsewhere in the model. The class is expected to have a status field that holds the current state.

The parser:

1. Collects all class#data declarations 2. For each state_machine <EntityName> , validates that a matching class exists 3. Emits a warning (not error) if the class lacks a status field

2.5. Example: TestReport lifecycle

state_machine TestReport {
  initial DRAFT
  states {
    DRAFT
    SUBMITTED
    UNDER_REVIEW
    ACCEPTED
    REJECTED
  }
  transition DRAFT -> SUBMITTED action lab_submits {
    cascade FormInstance {
      where "testReportId = ${this.id}"
      set { status: "LOCKED" }
    }
  }
  transition SUBMITTED -> UNDER_REVIEW action ia_starts_review {
  }
  transition UNDER_REVIEW -> ACCEPTED action ia_accepts {
    guard "not some(FormInstance where testReportId = ${this.id} and result = 'FAIL')"
  }
  transition UNDER_REVIEW -> REJECTED action ia_rejects {
    guard "some(FormInstance where testReportId = ${this.id} and result = 'FAIL')"
  }
}

3. Transitions

3.1. General

A transition moves an entity from one state to another. It can fire cascades (side-effects on related entities) and may be guarded by an OCL predicate.

3.2. Syntax

transition <From> -> <To> [action <ActionName>] {
  // body
}
  • <From> and <To> are state names (matching the states list)
  • action <ActionName> is optional but recommended for traceability
  • The body can be empty (simple state change) or contain guard, cascade, reference blocks

3.3. Atomicity

A transition + its cascades execute ATOMICALLY:

1. The guard (if any) is evaluated 2. If the guard passes, the source entity's state is updated 3. All cascades execute in declaration order 4. If any cascade fails, the entire transition is rolled back

3.4. Idempotency

Firing the same transition on the same entity twice (without an intervening change) is an ERROR. The state machine rejects it.

3.5. Transition uniqueness

Within a single state machine, no two transitions can have the same (from, to, action) tuple. The parser rejects duplicates.

3.6. Multiple transitions from the same state

Multiple transitions can originate from the same state (branching). At runtime, the caller specifies which transition to fire (typically via the action name). If multiple transitions from the same state would satisfy their guards simultaneously, the caller disambiguates.

3.7. Terminal states

A state with no outgoing transitions is a terminal state . Entities in a terminal state cannot transition further. The state machine does not enforce that entities reach a terminal state; that's a domain concern.

3.8. Example: simple transition

transition SUBMITTED -> UNDER_REVIEW action ia_starts_review {
}

No guard, no cascades. The state simply changes.

3.9. Example: guarded transition with cascades

transition UNDER_REVIEW -> ACCEPTED action ia_accepts {
  guard "not some(FormInstance where testReportId = ${this.id} and result = 'FAIL')"
  cascade EvaluationReport {
    where "testReportIds->includes(${this.id})"
    set { admissibility_progress: "incremented" }
  }
  reference { PD05-4-5 }
}

4. Cascades

4.1. General

A cascade is a declarative side-effect that fires when a transition completes. Cascades update related entities based on a predicate and a set of field updates.

4.2. Syntax

cascade <TargetEntity> {
  where "<OCL predicate>"
  set {
    <field>: <value>
    <field>: <value>
  }
}
  • <TargetEntity> — the entity type to update (a class#data)
  • where — an OCL predicate selecting which instances to update
  • set — field-by-field updates

4.3. Multiple cascades per transition

A transition can declare multiple cascades. They execute in declaration order:

transition DRAFT -> SUBMITTED action lab_submits {
  cascade FormInstance {
    where "testReportId = ${this.id}"
    set { status: "LOCKED" }
  }
  cascade AuditEvent {
    where "id = NULL"    // always create new
    set {
      entityType: "TestReport"
      entityId: "${this.id}"
      action: "lab_submits"
      timestamp: "${now}"
    }
  }
}

4.4. Token substitution in cascades

${this.<field>} resolves to the source entity's field value:

where "testReportId = ${this.id}"
set { entityId: "${this.id}" }

${<TransitionName>.<field>} resolves to a value produced by a named prior cascade (future extension — for now, cascades don't produce output values).

${now} resolves to the current ISO-8601 timestamp at execution time. ${user} resolves to the current user's identifier.

4.5. Cascade ordering

Cascades execute in declaration order. If cascade B depends on a value set by cascade A, A must be declared first.

4.6. Failure semantics

If a cascade fails (e.g., a where predicate matches no instances, or a set value violates a type constraint), the entire transition is rolled back. The state does not change.

Failure modes:

  • where matches zero instances: cascade silently succeeds (no-op)
  • where matches one instance: that instance is updated
  • where matches multiple instances: all matching instances are updated (use with care)
  • set violates a constraint: transition fails, roll back

4.7. Self-cascade

A cascade can target the same entity as the state machine itself:

transition DRAFT -> SUBMITTED action lab_submits {
  cascade TestReport {
    where "id = ${this.id}"
    set { submittedDate: "${now}" }
  }
}

This updates the source entity's submittedDate field as part of the transition.

4.8. Example: cascade with computed value

cascade EvaluationReport {
  where "id = ${this.evaluationReportId}"
  set {
    admissibilityProgress: "${this.admissibilityProgress + 1}"
  }
}

The set value is an OCL expression evaluated against the source entity.

5. Guards

5.1. General

A guard is an OCL predicate that must evaluate to true for a transition to fire. Guards are evaluated BEFORE the transition's state change.

5.2. Syntax

transition <From> -> <To> [action <ActionName>] {
  guard "<OCL predicate>"
  // ...
}

The OCL predicate has access to:

  • ${this} — the source entity (the entity whose state is changing)
  • All other entities in the model (via their class name)
  • All symbols and calculations

5.3. Evaluation semantics

  • The guard is evaluated when the transition is invoked
  • If the guard returns true, the transition proceeds
  • If the guard returns false, the transition does NOT fire and the caller receives a guard violation
  • If the guard raises an error during evaluation, the transition does NOT fire and the caller receives the error

5.4. Example: guard requiring all FormInstances pass

transition UNDER_REVIEW -> ACCEPTED action ia_accepts {
  guard "not some(FormInstance where testReportId = ${this.id} and result = 'FAIL')"
}

The guard queries all FormInstances on this TestReport and ensures none have result FAIL .

5.5. Example: guard with time constraint

transition SUBMITTED -> WITHDRAWN action ia_withdraws {
  guard "${this.submittedDate} > ${now} - 30 days"
  cascade FormInstance {
    where "testReportId = ${this.id}"
    set { status: "RELEASED" }
  }
}

The transition is only allowed within 30 days of submission.

5.6. Guards vs applicability

Guards and applicability are different:

  • applicability is a static, model-time constraint (e.g., "this form applies when accuracy_class = C")
  • guard is a dynamic, runtime constraint (e.g., "this transition can fire only if no FormInstance has FAIL")

Guards evaluate at runtime; applicability evaluates at form materialization time.

5.7. Combining guards and cascades

A transition can have both:

transition UNDER_REVIEW -> ACCEPTED action ia_accepts {
  guard "not some(FormInstance where testReportId = ${this.id} and result = 'FAIL')"
  cascade EvaluationReport {
    where "id = ${this.evaluationReportId}"
    set { acceptedReportCount: "${this.acceptedReportCount + 1}" }
  }
}

The guard is evaluated first. Only if it passes do the cascades fire.

6. Token substitution

6.1. General

Tokens inside where predicates and set values are substituted at runtime, using the source entity's state and a small set of context variables.

6.2. Token syntax

The token syntax is ${...} — dollar-brace, expression, close-brace. Note: this is DIFFERENT from the parameter substitution syntax ( ${{ ... }} , double braces) used in subforms. The single-brace form is for runtime values; the double-brace form is for build-time parameters.

6.3. Built-in tokens

Token Description
${this} The source entity (the entity whose state is changing).
${this.id} The source entity's identifier.
${this.<field>} Any field on the source entity.
${now} The current ISO-8601 timestamp at execution time.
${user} The current user's identifier.

6.4. Expression tokens

Tokens can contain OCL expressions:

set { admissibilityProgress: "${this.admissibilityProgress + 1}" }

The expression is evaluated against the source entity.

6.5. Token resolution

Tokens are resolved by the state machine runtime, not by the parser. The parser validates that the token syntax is correct but does not evaluate the expressions.

6.6. Cross-entity references

Tokens can reference other entities via their class name:

guard "not some(FormInstance where testReportId = ${this.id} and result = 'FAIL')"

The FormInstance identifier resolves to the FormInstance class in the current model.

6.7. Substitution vs parameters

The two substitution mechanisms serve different purposes:

  • Parameter substitution ( ${{ name }}): build-time, used in subforms to inline dimension-derived values. The runtime never sees these.
  • Token substitution ( ${...}): runtime, used in state machine cascades and guards to reference entity state. The parser preserves these for the runtime.

The double-brace vs single-brace distinction prevents ambiguity.

7. Formal grammar

7.1. State machine declaration

StateMachineDecl = "state_machine" ID "{" { StateMachineProperty } "}" ;
StateMachineProperty = "initial" ID
                      | "states" "{" { ID } "}"
                      | TransitionDecl ;
TransitionDecl = "transition" ID "->" ID
                  [ "action" ID ] "{" { TransitionProperty } "}" ;
TransitionProperty = "guard" STRING
                   | CascadeDecl
                   | "reference" "{" { ID } "}" ;
CascadeDecl  = "cascade" ID "{" CascadeProperty { CascadeProperty } "}" ;
CascadeProperty = "where" STRING
                | "set" "{" { SetEntry } "}" ;
SetEntry     = ID ":" STRING ;

7.2. Lexical conventions

As per the forms grammar (see formal-grammar-forms ).

7.3. Token syntax within strings

Within any STRING value used as a where predicate or set value, ${...} tokens are recognized. These are preserved by the parser and resolved at runtime.

NOTE
This is distinct from ${{ ... }} parameter substitution (build-time, used in subforms).