Sinelabore Homepage

Actions#

Role in SysML v2#

An action can be used to represent a behavior of a part. An action definition may have features with directions in, out, or inout that act as the parameters of the action.

An action usage inherits the parameters of its definitions, if any, and it can also define its own parameters to augment or redefine those of its definitions.

Actions can coordinate the performance of other actions and generate effects on parts

Action definitions and action usages can be decomposed into lower-level action usages to create an action tree, and action usages can be referenced by other actions.

Action ordering can be specified using successions.

A succession between action usages may, additionally, have a guard condition, represented as a Boolean expression.

The sequencing of action usages may be further controlled using control nodes, which are special kinds of action usages.

Presently only decision nodes (decide) are supported among control nodes. Fork, join, merge, and similar control nodes are not generated. A decision node has one incoming succession and one or more outgoing successions. Exactly one of the actions connected to an outgoing succession can start after the action connected to the incoming succession has completed. Which of the downstream actions is performed can be controlled by placing guards on the outgoing successions.

An output parameter of one action usage may be bound to the input parameter of another action usage.

Example#

SysML v2 source code:#

private import ScalarValues::*;

package Test {
      part def AParent{
        attribute anAttributeOfAParent:Integer;
    }

    part def A specializes AParent {
        attribute anAttributeOfA : Integer;

        action def ParentAction{}
        
        action def ParentWithPara{
            attribute test:Integer;
            in x:Real;
            out p:Real;

            assign p := x*x;
            assign anAttributeOfA := p;
            assign anAttributeOfAParent := p;
        
        }



        action def run{
            in n:Real;
            out p:Real;
            out u:Real;

            attribute x : Real;

            action act1:ParentAction;
            action act2:ParentWithPara;
            action act3{in n:Real; out u:Real;
                assign u:=8;
            }
            
            action Path1{}
            then terminate;

            action Path2;
            then terminate;

            first start;
            then assign x := n;
            then act1;
            then assign x:=2;
            then action act1_1;
            then act2{in x:Real;out p:Real;}
            then assign p:=act2.p;
            then act3{in n:Real;out u:Real;}
            then assign u:=act3.u;

            then decide d1;
            if n==1 then Path1;
            if act2.p==4 then Path2;
            else done;
        }
    }
}

C++ source code#

For each action definition a C++ struct is generated that has a pointer to its surrounding part. It also has an operator method that receives the parent part as parameter. A context object ensures that parameter access to all local as well as all parent or other attributes and parts that are visible to the action is given. This ensures that assign and attribute access construct written in SysML can simply be mapped to C++.

Action usages that are decomposed into lower-level action usages are mapped to executable C++ code representing the SysML intention.

The shape of the generated body depends on the flow. An activity that branches, loops or waits becomes a small state machine (enum + while + switch) so that it can be re-entered. An activity that is a plain sequence of steps is emitted as straight-line statements — no dispatch variable, no switch — which keeps the common case readable:

// action setRed { first start; then perform redLamp.setOn; then done; }
virtual void operator()(Controller &parent_) {
    ...
    Lamp::setOnDef __call_redLamp_setOn;
    __call_redLamp_setOn(redLamp);
}

Parameter defaults#

An in parameter that the call site omits is filled from the default declared on the action definition; a value given at the call site wins.

action def Scale {
    in factor : Real = 2.0;
    in value  : Real;
    out result : Real;
    assign result := value * factor;
}

action useDefault  { first start; then action s1 : Scale { in value = 10.0; } then done; }
action overrideIt  { first start; then action s2 : Scale { in value = 10.0; in factor = 5.0; } then done; }
class A : public AParent {

public:
  A() {}

  // elements of this part

  int anAttributeOfA = 0;

  virtual void process() override {}

  // must be called by derived classes after constructing the parts
  virtual void init(void) override {}

  struct ParentActionDef {
    // Top-level activity: parent is the part
    A *part_ptr_ = nullptr;
    A &getPart() {
      assert(part_ptr_);
      return *part_ptr_;
    }

    virtual void operator()(A &parent_) {

      part_ptr_ = &parent_;

      struct ParentActionContext {
        // owning part
        A &parent;
        // indirectly accessible features
        int &anAttributeOfA;
        int &anAttributeOfAParent;

        ParentActionContext(A &parent_)
            : parent(parent_), anAttributeOfA(parent_.anAttributeOfA),
              anAttributeOfAParent(parent_.anAttributeOfAParent) {}
      } ctx{parent_};

      // make context available
      auto &anAttributeOfA = ctx.anAttributeOfA;
      auto &anAttributeOfAParent = ctx.anAttributeOfAParent;

    } // end run method
    virtual ~ParentActionDef() = default;

  }; // ParentAction

  ...
};

Performing an action of another part#

A part rarely does everything itself. perform lets an action trigger behavior that belongs to another part, addressed through a feature chain. The other part keeps its own behavior; the caller only starts it.

part def Lamp {
    attribute switchCounter : Natural default 0;
    action setOn  { assign switchCounter := switchCounter + 1; }
    action setOff { assign switchCounter := switchCounter + 1; }
}

part def Controller {
    ref part redLamp    : Lamp;
    ref part yellowLamp : Lamp;

    action switchRed {
        first start;
        then perform redLamp.setOn;
        then done;
    }
}

Four spellings are supported. They differ only in whether the performing action gets a name of its own:

Form Notes
perform redLamp.setOn; Anonymous; the name is taken from the last chain segment (setOn)
perform action redOn references redLamp.setOn; Named PerformActionUsage
action redOn references redLamp.setOn; Plain action usage with reference subsetting
action ::> redLamp.setOn; Short form of the previous line

Give the perform a name whenever the same action is performed twice in one body — two anonymous performs of redLamp.setOn would both be called setOn:

action switchBoth {
    first start;
    then perform action redOn    references redLamp.setOn;
    then perform action yellowOn references yellowLamp.setOn;
    then done;
}

Feature chains#

The performer may be several segments deep, and each segment is resolved against the owning part and its base parts. Composite parts (part inner : Lamp;) and references (ref part linked : Lamp;) are both dereferenced correctly in the generated code:

part def Head {
    part inner : Lamp;          // composite
    ref part linked : Lamp;     // reference
}

part def Controller {
    part head : Head;

    action deepChain {
        first start;
        then perform head.inner.setOn;
        then perform head.linked.setOn;
        then done;
    }
}

If the performing part is declared on a base part, it is still found — the specialization chain (:>) is searched as well.

Use . for feature navigation and :: for qualified names. redLamp.setOn navigates from the part to one of its features; Signalling::Lamp names a type in a package. A performer that resolves to a collection is reported as E3111 — perform on a named member instead.

Supported structured control actions constructs#

Loop / until actions#

The loop runs until a given condition becomes true.

  • Extra action

    action def Test{
        out chargeStatus:Integer;
    
        action addCharge:AddCharge{}
        then assign chargeStatus := batLevel;
        then done;
    
        first start;
        then action monitor : MonitorBattery {
            out charge;
        }
        then decide;
        if monitor.charge < 100 then addCharge;
        else done;
    }
    
    action def ChargeBattery {
        in a:Integer;
    
        first start;
        then loop action charging:Test {
        } until charging.chargeStatus <= 100;
    
        then action endCharging : EndCharging;
        then done;
    }
  • Inline action

    action def ChargeBattery {
    
        first start;
        then loop action charging {
    
            action addCharge:AddCharge;
            then done;
    
            first start;
            then action monitor : MonitorBattery {
                out charge;
            }
            then decide;
            if monitor.charge <= 100 then addCharge;
            else done;
        } until charging.monitor.charge < 100;
        then action endCharging : EndCharging;
        then done;
    }

While loop actions#

A while loop action usage performs a body clause action usage iteratively, as long as its while expression continues to evaluate to true and its until expression continues to evaluate to false (if existing).

  • While with guard check before body clause

    attribute whileGuard:Integer default 1;
    
    action def WhileUntilAction {
        first start;
        then while whileGuard <= 5 {
            assign whileGuard := whileGuard +1;
        }
    
        then done;
    }
  • While / until combined with check before and after body clause

    attribute whileGuard:Integer default 1;
    attribute untilGuard:Boolean default true;
    
    action def SecondAction{
        assign whileGuard := whileGuard +1;
        if whileGuard == 5{
            assign untilGuard := false;
        }
    }
    
    action def WhileUntilAction {
        first start;
        then while whileGuard >= 1 {
            action secondAction:SecondAction{}
    
            first start;
            then secondAction;
        }until untilGuard == false;
    
        then done;
    }

If/then/else action#

An if action usage evaluates a condition expression and then performs a then clause action usage if the expression evaluates to true, or, optionally, an else clause action usage if the expression evaluates to false.

  • if/then

    action def XAct{
      attribute x:Integer;
    
      first start;
      then action Act if p==0{
          assign b:=4;
          assign p:=2;
          then terminate;
      }else action Act2{
          then action aActUse:AAct{}
      }
    }
  • if/else if/then

    action def YAct {
    
        first start;
        then if batLevel<4{
            attribute a:Integer default 33;
            then assign n:=a;
            then action s:setM{in x=n;}
        }else if batLevel>=5 and batLevel<90{
            attribute a:Integer default 66;
            then  assign n:=a;
            then action s:setM{in x=a;}
        }else if batLevel>=90 and batLevel<95{
            attribute a:Integer default 99;
            then assign n:=a;
            then action s:setM{in x=a;}
        }else {
            attribute a:Integer default 100;
            then assign n:=a;
            then action s:setM{in x=n;}
        }
    }

For loop actions#

A for loop action usage performs a body clause action usage iteratively, assigning a loop variable successively for each iteration to the values resulting from the evaluation of a sequence expression.

 1part a[4] : A;
 2
 3action def AAction{
 4
 5  first start then forAction;
 6
 7  action forAction for anA in a {
 8    assign anA.n := 34;
 9  }
10  then action forAction1 for anA1 in a{
11    forAction2 for anA2 in a{
12      assign anA1.n := 2;
13      assign anA2.n := 35;
14    }
15  }
16
17  then terminate;
18}

Package-level actions#

Action definitions may also live at package scope (not only inside a part). They can specialize other actions with :>. Abstract action defs are validated at codegen time. Call conventions for operator() depend on whether the action is bound to a part or nested under another action.

package Ops {
    private import ScalarValues::*;

    abstract action def Sink {
        in data : Real;
    }

    action def Log {
        in msg : Real;
    }

    action def Amplify :> Log {
        in gain : Real;
        first start;
        then done;
    }
}