Superstates
Put a state machine inside a state: nesting, the scope of declarations, and finishing with a termination transition.
A washing machine is more than on and off. Once you press start, it runs a programme: it fills the drum, washes, and drains. The phases of the programme belong together, and some things have to work whichever phase the machine is in. The stop button is one of them.
Every chart so far has been a single, flat state machine. A superstate expresses this grouping:
it is a state with states inside it. From the outside it is one state (in the chart below it is
called Programme); inside, it is a small state machine of its own. This chapter shows how a chart
enters and leaves a superstate, how declarations inside one behave, and how a superstate finishes.
A state with states inside
Section titled “A state with states inside”Here is the washing machine’s controller. It waits in Idle until start, then runs the programme.
The inputs full and timeUp come from the machine’s sensors: the drum is full, the wash time is
over.
Diagram
Code
scchart Washer { input bool start, full, timeUp output bool motor
initial state Idle if start go to Programme
state Programme { initial state Filling if full do motor = true go to Washing state Washing if timeUp do motor = false go to Draining state Draining }}| Tick | input bool start | input bool full | input bool timeUp | output bool motor | Active state of the chart | Active state of the region of Programme |
|---|
Guided tour, 7 ticks:
- start = false, full = false, timeUp = falseTick 1 enters the initial state,
Idle. Its transition is delayed, so nothing else can happen in this tick. - start = true
startis true, soIdle→Programmefires. EnteringProgrammeenters its initial state,Filling, in the same tick: two states are active now,ProgrammeandFillinginside it. - start = false
Fillingwaits forfull, which is false, so nothing fires. The chart stays inProgramme, inFilling. - full = true
fullis true:Filling→Washingfires insideProgrammeand switches the motor on.Programmestays active: moving between its inner states does not leave it. - full = falseNothing is enabled. The machine keeps washing, and
motorkeeps its value. - timeUp = true
timeUpis true:Washing→Drainingfires and switches the motor off. - timeUp = false
Draininghas no outgoing transition, so the machine stays here for ever. The rest of the chapter gives it ways out.
Programme and Filling are both active.Programme is declared like any other state, followed by a block in braces. Inside the braces you
write states and transitions exactly as you would at the top of a chart, with one of them marked
initial. This is what KIELER does with it, tick by tick:
| Tick | start |
full |
timeUp |
State after | motor after |
|---|---|---|---|---|---|
| 1 | false | false | false | Idle | false |
| 2 | true | false (held) | false (held) | Programme (Filling) | false |
| 3 | false | false (held) | false (held) | Programme (Filling) | false |
| 4 | false (held) | true | false (held) | Programme (Washing) | true |
| 5 | false (held) | false | false (held) | Programme (Washing) | true |
| 6 | false (held) | false (held) | true | Programme (Draining) | false |
| 7 | false (held) | false (held) | false | Programme (Draining) | false |
Three things to notice:
- Entering a superstate enters its initial state in the same tick. Tick 2 takes a single
transition,
IdletoProgramme, and ends with two active states:Programmeand, inside it,Filling. A superstate is never active on its own; one of its inner states is always active as well. When superstates are nested, entering the outer one enters every level, all the way down, in the same tick. - Inside, the usual rules hold.
Fillingwas entered in tick 2, so its delayed transition can fire from tick 3 on. An immediate transition out ofFillingcould have fired in tick 2 already (see Delayed and immediate transitions). - Moving between inner states does not leave the superstate. The machine changes phase in
ticks 4 and 6, but
Programmestays active the whole time.
KIELER draws a superstate as a large rounded box with its name at the top and its inner states
inside. Declarations of the superstate, if it has any, are listed under its name, the way the chart
lists its inputs and outputs. In SCCharts Lab the diagram also follows the editor’s cursor: put the
cursor inside Programme’s braces, and the diagram selects Programme and expands the regions
around it, which in a large chart is the quickest way to see where you are.
Leaving from anywhere
Section titled “Leaving from anywhere”The machine needs a stop button that works in every phase. You could give each of Filling,
Washing and Draining its own if stop go to Idle. A superstate lets you write it once:
Diagram
Code
scchart Washer { input bool start, full, timeUp, stop output bool motor
initial state Idle if start go to Programme
state Programme { initial state Filling if full do motor = true go to Washing state Washing if timeUp do motor = false go to Draining state Draining } if stop do motor = false go to Idle}| Tick | input bool start | input bool full | input bool timeUp | input bool stop | output bool motor | Active state of the chart | Active state of the region of Programme |
|---|
Guided tour, 6 ticks:
- start = false, full = false, timeUp = false, stop = falseTick 1 enters
Idle. - start = true
startis true: the machine entersProgrammeand, in the same tick,Filling. - start = false, full = true
fullis true:Filling→Washing, and the motor runs. - full = false, stop = true
stopis true.Programme’s own transition fires although the machine is inWashing: a superstate’s transition can leave it whichever inner state is active.WashingandProgrammeare both left, and the effect switches the motor off. - stop = false, start = true
startagain. The machine entersProgrammeand starts inFilling, not inWashingwhere it was stopped: entering a superstate always starts at its initial state. - start = false
Fillingwaits forfullas before.
stop leaves Programme while it is in Washing. In tick 5 the machine starts again at Filling.The transition if stop do motor = false go to Idle is written after Programme’s closing brace,
so it belongs to Programme itself, not to any state inside it. It can fire whichever inner state is
active, and when it fires, the inner state is left along with Programme.
Two details matter in practice:
- The superstate does not remember where it was. When
Programmeis entered again in tick 5, it starts at its initial state,Filling. A machine that should resume where it stopped needs a history transition, which comes much later, in History and deferred transitions. - Leaving undoes nothing. That is why the stop transition switches the motor off. Without
do motor = false, a stop duringWashingwould leaveProgrammewithmotorstill true, and it would stay true inIdleand after the next start: a variable keeps its value until something assigns it.
A transition written on a superstate, after its closing brace, is called an abort: it can cut
short whatever the inside is doing. The go to you have used all along makes a weak abort: if a
transition inside is enabled in the same tick as stop, the inside takes its step first, and only
then is Programme left. Its counterpart, the strong abort abort to, leaves before anything
inside moves. Aborts is about the difference.
Declarations inside a superstate
Section titled “Declarations inside a superstate”A superstate can declare its own variables and signals. They go at the start of its block, before its first state, in the same way as the chart declares its inputs and outputs at the top.
The nearest declaration wins
Section titled “The nearest declaration wins”A declaration inside a superstate is a local of that superstate. The transitions inside it can
use the name; the rest of the chart cannot. A transition outside the superstate that used one of
its locals would be rejected with Couldn't resolve reference to ValuedObject '…'., naming the
variable.
Every superstate is a scope, and scopes nest. If the same name is declared in several of them, a use of the name means the nearest declaration, looking outwards from where it is used. The inner declaration hides, or shadows, the outer one:
Diagram
Code
scchart Shadowing { input bool press output int count = 1 output int seen
initial state Outer { int count = 10 initial state Waiting if press do count++; seen = count go to Done state Done }}| Tick | input bool press | output int count | output int seen | local int count of state Outer | Active state of the chart | Active state of the region of Outer |
|---|
Guided tour, 3 ticks:
- press = falseTick 1 sets the output
countto 1, entersOuterand setsOuter’s owncountto 10. Two variables now have the same name. - press = trueThe transition inside
Outerfires. Itscountmeans the nearest declaration,Outer’s local: 10 becomes 11, andseencopies it. The outputcountis untouched: still 1. - press = falseNothing else happens. KIELER gave no warning about the second
count.
count lanes: the output count stays 1, because the transition updates Outer.count, Outer’s own, from 10 to 11. seen copies it.KIELER does not warn about shadowing. The chart works, but whoever reads it (you, next week) can
easily mistake one count for the other. Give locals names of their own.
Two more facts about scope, both of which surprise people:
inputandoutputonly mean something at the top of a chart. Written inside a superstate,output int zdeclares an ordinary local: it is not part of the chart’s interface, and nothing outside the chart can see it.- A superstate’s own outgoing transitions, the ones written after its closing brace, can read the superstate’s locals too. KIELER’s validator has a check that was meant to forbid this, but the check never fires, so such charts compile. Don’t build on it: if the way out of a superstate depends on a variable, declare the variable one level further out.
Fresh on every entry
Section titled “Fresh on every entry”A rep counter for the gym shows the repetitions of the current set. A press on the button counts a
rep; rest ends the set; the next press starts a new set.
Diagram
Code
scchart RepCounter { input bool press, rest output int shown
initial state Set { int reps = 0 initial state Counting if press do reps++; shown = reps go to Counting } if rest go to Resting
state Resting if press go to Set}| Tick | input bool press | input bool rest | output int shown | Active state of the chart | Active state of the region of Set |
|---|
Guided tour, 10 ticks:
- press = false, rest = falseTick 1 enters
Set, which initialises its localrepsto 0, andCountinginside it. - press = trueA press:
repsbecomes 1 and the displayshowncopies it. - press = falseNo press, nothing happens.
- press = trueAnother press:
repsis 2. - press = falseNo press.
- rest = true
restleavesSetforResting.shownis an output and keeps its value, 2. - rest = false, press = trueA press starts the next set:
Setis entered again, sorepsis initialised to 0 again. This press does not count as a rep:Countingwas entered in this tick, and its transition is delayed. - press = falseNo press.
- press = trueThe first rep of the new set:
repsis 1, not 3. Every entry ofSetstarts from scratch. - press = falseNo press.
reps started again from 0 when Set was entered in tick 7.reps is declared in Set, so it is initialised every time Set is entered: in tick 1, and again
in tick 7. (The press of tick 7 only starts the set: Counting is entered in that tick, and its
transition is delayed.) That is the rule for every local: a variable declared in a superstate gets its initial
value on every entry of the superstate, or, if it has none, its type’s default (0 for int,
false for bool). Think of a superstate as a function and its locals as the function’s local
variables: every call starts from scratch.
For the reps of one set, that is what you want. A value that has to survive leaving and re-entering, such as the total over all sets, belongs one level further out, at the top of the chart. You will build it in the second exercise below.
Finishing: final states and join to
Section titled “Finishing: final states and join to”So far the programme never ends: the machine drains for ever. Let the drain lead to a final state,
and let Programme react when its inside has finished:
Diagram
Code
scchart Washer { input bool start, full, timeUp, empty, stop output bool motor output signal beep
initial state Idle if start go to Programme
state Programme { initial state Filling if full do motor = true go to Washing state Washing if timeUp do motor = false go to Draining state Draining if empty go to Done final state Done } if stop do motor = false go to Idle do beep join to Idle}| Tick | input bool start | input bool full | input bool timeUp | input bool empty | input bool stop | output bool motor | output signal beep | Active state of the chart | Active state of the region of Programme |
|---|
Guided tour, 6 ticks:
- start = false, full = false, timeUp = false, empty = false, stop = falseTick 1 enters
Idle. - start = true
start: the machine entersProgrammeandFilling. - start = false, full = true
full:Filling→Washing, motor on. - full = false, timeUp = true
timeUp:Washing→Draining, motor off. - timeUp = false, empty = true
empty:Draining→Done.Doneis a final state, soProgrammehas finished: it terminates. Its termination transition,join to Idle, fires in this same tick:beepis emitted and the machine is back inIdle. - empty = false
beepis a signal, so it was present for that one tick only. The machine waits inIdlefor the nextstart.
Draining moves to Done, Programme terminates, and the join takes the machine back to Idle with a beep.Done is a final state, drawn with a double outline (see
Delayed and immediate transitions). At the top level of a chart a final state
ends the chart; inside a superstate it ends the superstate’s inner state machine. When the inside of
a superstate reaches a final state, the superstate terminates.
Termination on its own does not move anything: a terminated superstate stays active until one of its
transitions leaves it. The transition written join to is a termination transition: it fires
when its superstate has terminated. KIELER draws it with a green triangle where it starts, as on the
transition from Programme back to Idle in the diagram above. It is drawn dashed because it is
immediate, which is the next point.
In detail:
- The join fires in the tick the superstate terminates. In tick 5 the inner transition to
Doneand the join both fire, and the beep belongs to that same tick. - The join’s effects run after everything the inside did in that tick. If the inside writes a variable on its way to the final state, the join’s effect already sees the new value.
- A join without a trigger is immediate. It is checked in every tick in which its superstate is active, including the tick in which the superstate was entered.
The diagram numbers Programme’s two outgoing transitions: 1: stop and 2: / beep. They are
priorities, as for any state with several outgoing transitions (see
Triggers, effects and priority). What happens when a stop and the end of the
programme fall into the same tick is a question for Aborts.
In and out in one tick
Section titled “In and out in one tick”Because a trigger-less join is immediate, a superstate whose inside finishes at once is left again
in the tick it was entered. Predict what this chart does when start arrives, run it, then open the explanation:
Diagram
Code
scchart Quick { input bool start output int steps
initial state Idle if start do steps = 1 go to Work
state Work { initial state Begin immediate do steps++ go to Done final state Done } do steps++ join to Finished
state Finished}Begin’s transition is immediate, and the join has no trigger.
Inputs per tick. A value in grey was not set in that tick: it holds from an earlier one.
| Tick | start |
|---|---|
| 1 | false |
| 2 | true |
Explanation
In tick 2, Idle to Work fires and sets steps to 1. Entering Work enters Begin, whose
immediate transition fires in the same tick: steps is 2 and Work reaches its final state, Done.
Work has terminated, and its join is immediate, so it fires too: steps is 3 and the chart is in
Finished. Three transitions in one tick, and Work was active for no time at all.
A join with a trigger waits a tick
Section titled “A join with a trigger waits a tick”A termination transition can have a trigger: if ready join to After fires once its superstate has
terminated and ready holds. Like every transition with a trigger, it is then delayed: it is not
checked in the tick its superstate was entered, even if the superstate terminates in that tick and
the trigger is true. Compare the two versions:
Diagrams
PlainJoin
GuardedJoin
Code
PlainJoin
scchart PlainJoin { input bool start, ready output bool left
initial state Idle if start go to Work
state Work { initial state Begin immediate go to Done final state Done } do left = true join to After
state After}GuardedJoin
scchart GuardedJoin { input bool start, ready output bool left
initial state Idle if start go to Work
state Work { initial state Begin immediate go to Done final state Done } if ready do left = true join to After
state After}| Tick | input bool start | input bool ready | PlainJoin: output bool left | PlainJoin: Active state of the chart | PlainJoin: Active state of the region of Work | GuardedJoin: output bool left | GuardedJoin: Active state of the chart | GuardedJoin: Active state of the region of Work |
|---|
Guided tour, 3 ticks, the same inputs for both charts:
- start = false, ready = trueTick 1 enters
Idlein both charts.readyis true from the start; onlyGuardedJoin’s join reads it. - start = true
start: both charts enterWork, andBegin→Donefires at once, soWorkterminates in its entry tick.PlainJoin’s join has no trigger, so it is immediate: it fires now, setsleftand ends inAfter.GuardedJoin’sif readymakes its join delayed, so it is not checked inWork’s entry tick:leftstays false. - start = false
GuardedJoin’s join is checked for the first time:Workis still terminated andreadystill true, so it fires and setsleft. Both charts are now inAfter, one tick apart.
ready is true throughout and start comes in tick 2: PlainJoin reaches After in tick 2, GuardedJoin one tick later.The diagrams show the difference as well: PlainJoin’s join is drawn dashed, because it is
immediate, and GuardedJoin’s solid, because it is delayed. The four ways to write a join, and
whether each is checked in the tick its superstate is entered:
| Termination transition | Checked in the entry tick? |
|---|---|
join to After |
yes: no trigger, so immediate |
if ready join to After |
no: a trigger makes it delayed |
immediate if ready join to After |
yes |
delayed join to After |
no |
After the entry tick, a delayed join is checked in every tick for as long as its superstate stays terminated, so it fires in the first tick in which its trigger holds.
Superstates inside superstates
Section titled “Superstates inside superstates”A superstate can contain superstates, which can contain superstates, to any depth. The rules are the same at every level:
- Entering a superstate enters its initial state, and if that is a superstate, its initial state, and so on, all in one tick.
- A transition of a superstate leaves it whichever state is active inside it, however deep.
- Termination travels outwards. When an inner superstate terminates and its join leads to a final state of the outer superstate, the outer one terminates in that same tick, and its own join can fire as well.
A coffee machine shows the last rule. Brew sits inside Make, and both finish with a join:
Diagram
Code
scchart Coffee { input bool start, ground, full output signal ready output int cups = 0
initial state Idle if start go to Make
state Make { initial state Grinding if ground go to Brew
state Brew { initial state Pouring if full go to Poured
final state Poured } do cups++ join to Served
final state Served } do ready join to Idle}| Tick | input bool start | input bool ground | input bool full | output signal ready | output int cups | Active state of the chart | Active state of the region of Make | Active state of the region of Make.Brew |
|---|
Guided tour, 6 ticks:
- start = false, ground = false, full = falseTick 1 enters
Idle. - start = true
startentersMake, and with itMake’s initial stateGrinding. - start = false, ground = trueThe beans are ground:
Grinding→Brew. EnteringBrewenters its initial statePouringin the same tick. Three states are active now, one inside the other:Make,BrewandPouring. - ground = false
fullis still false:Pouringwaits, andMake,BrewandPouringstay active. - full = trueThe cup is full, and termination travels outwards in one tick:
Pouring→PouredmakesBrewterminate;Brew’s join (cupsis 1) leads toServed, a final state ofMake, soMaketerminates too; andMake’s join emitsreadyand returns toIdle. - full = falseBack in
Idle, ready for the next cup.
Brew, then Make, terminate in the same tick. Watch cups and ready.In tick 5 three transitions fire, from the inside out: Pouring to the final state Poured, which
ends Brew; Brew’s join, which counts the cup and enters Served, a final state of Make; and,
since Make has now terminated as well, Make’s join, which emits ready and returns to Idle. Both
joins have no trigger, so both are immediate and fire in the tick their superstate terminates.
The first exercise builds a rinse cycle this way.
Try it
Section titled “Try it”A rinse cycle
After draining, the machine should rinse before it beeps: fill the drum again (full), rinse with
the motor running until timeUp, and drain again (empty). Put the rinse in a superstate of its
own, Rinse, inside Programme, and let Programme finish only when the rinse has finished.
Run your chart and check that the beep comes in the same tick as the second empty.
Show the starter chart
scchart Washer { input bool start, full, timeUp, empty, stop output bool motor output signal beep
initial state Idle if start go to Programme
state Programme { initial state Filling if full do motor = true go to Washing state Washing if timeUp do motor = false go to Draining state Draining if empty go to Done final state Done } if stop do motor = false go to Idle do beep join to Idle}Show a solution
scchart Washer { input bool start, full, timeUp, empty, stop output bool motor output signal beep
initial state Idle if start go to Programme
state Programme { initial state Filling if full do motor = true go to Washing state Washing if timeUp do motor = false go to Draining state Draining if empty go to Rinse
state Rinse { initial state Refilling if full do motor = true go to Rinsing state Rinsing if timeUp do motor = false go to Emptying state Emptying if empty go to Rinsed final state Rinsed } join to Done
final state Done } if stop do motor = false go to Idle do beep join to Idle}A running total
Give RepCounter a second output, total, that counts every rep of the session, across all sets,
while shown keeps showing the reps of the current set only.
Show the starter chart
scchart RepCounter { input bool press, rest output int shown
initial state Set { int reps = 0 initial state Counting if press do reps++; shown = reps go to Counting } if rest go to Resting
state Resting if press go to Set}Show a solution
scchart RepCounter { input bool press, rest output int shown, total
initial state Set { int reps = 0 initial state Counting if press do reps++; total++; shown = reps go to Counting } if rest go to Resting
state Resting if press go to Set}Check your understanding
Section titled “Check your understanding”Show the answer
Answer B: In the tick after that
A join with a trigger is delayed, like every transition with a trigger, so it is not checked in the
tick S is entered. In the next tick S is still terminated and c is still true, so the join
fires. GuardedJoin above does exactly this. Without the trigger, join to T would have fired in
the entry tick.
Show the answer
Answer A: 0
Inside S, the name n means the nearest declaration, S’s own local. That local goes from 5 to 6.
The output n is shadowed inside S and nothing assigns it, so it is still 0.
Show the answer
Answer B: 0
Variables do keep their values, but a local of a superstate is initialised again on every entry of
the superstate: to its initial value if it has one, otherwise to its type’s default, 0 for an
int. RepCounter above shows the same re-initialisation, there to its initial value, 0: the first
rep of the second set, in tick 9, shows 1.
Key ideas
- A superstate is a state with states inside. Entering it enters its initial state in the same tick, all the way down.
- A superstate’s own transitions, written after its closing brace, leave it whichever inner state is active. The next entry starts again at the initial state.
- Declarations inside a superstate are local to it. The nearest declaration wins, and locals are initialised again on every entry.
- When the inside reaches a final state the superstate terminates, and
join tocan fire in that same tick. A join without a trigger is immediate;if c join towaits at least one tick.
Next, Concurrent regions: several state machines side by side, in one chart or one superstate.