[DRAFT] Integration a FSM in CastleCode (experimenting)
As demanded by Castle has generic FSM synt... (U_FSM_Syntax), we need a syntax to define FSMs directly in CastleCode.
The behavior of a FSM is defined by the its “rules”. But to use a FSM we need to “call” that FSM; it needs to proces events,
either by single stepping or in a for-ever-loop. This is typically a details that most developers like to
controll.
And so we need a bit of syntax (and semantics) for it.
A few experiments are already given in Castle FSM-rule syntax tries – which focus on the rules; and are mostly “internal” to
the FSM. In this blog we focus and experiments on how to integrate the FSM with the “rest of the CastleCode”.
Unlike the ‘states’ – which are internal details of the FSM–, the events and actions are external – its are the
inputs and outputs of the FSM. So, the need to exist both ‘in’ and ‘outside’ the FSM.
Intro
This blog shows a few possible implementations of a simle, well-known Turnstile FSM, to experiment with syntax options. We use a basic version, and we will extend it – to show wat it does with the code.
Questions
The experiments – how to intergrade an FSM into CCastle focus around the following questions.
a component (by itself)
a function (or other callable)
just a “statement”
just sent events via a port?
call FSM.step(), or FSM.step(input)
call FSM.run() – which may never return
How about event parameters?
Sending events over an output-port?
Or do we execute local functions?
How about outgoing parameters?
Turnstile demo
We use a very simple – 2 states, 2 events, 4 actions– FSM called “Turnstile”. It has only 4 “rules”, and is easy to
understand.
Later, we will add more stated (requesting 2 coins) and/or non-FSM behavior, like counting the (total number of) coins
We use a very simple Turnstile FSM as demo. It as two inputs, two states and a few actions; it’s based on #UncleBob’s State Machine Compiler demo.
Initial: Locked
FSM: Turnstile
{
Locked Coin Unlocked unlock()
Locked Pass Locked alarm()
Unlocked Coin Unlocked thankyou()
Unlocked Pass Locked lock()
}
- selected:
SMC/Turnstile: https://github.com/unclebob/CC_SMC
CPP2: https://github.com/hsutter/cppfront and https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0707r4.pdf (a bit old)
Experimenting
Caution
Most of the shown CastleCode syntax is provisional.
Here, we use basic rule synax of Try 1,
I use
@FSM
(see: “Rewriters (TODO)”) as a kind of (Python) (decorator) or CPP2 [1] metafunction (video-link)I use some “ad hoc syntax, for trivial thinks, I did’t design yet (e.g. Enum).
The order of names and typs are not consistent
name ‘:’ type [‘:=’ value]
vstype ‘:’ name
– sorry for that; my tought are not final, and my fingers type differently:-)
FSM as Component
When an component is implemented as an FSM (using the “Rewriters (TODO)”: @FSM) the in-ports are used as FSM-inputs and the
out-port can be used for actions. So, “calling” the FSM is like “calling” any other component: send signals over the
connections.
Note: There is no need to that alls FSM-inputs (nor it outputs) match 1:1 to the protocol of one port – the can
originate from multiple in-ports and protocols.
protocol Turnstile_inputs {
coin;
pass;
}
protocol Turnstile_actions {
unlock();
thanks();
}
protocol Gen_errors {
alarm();
...
}
component Turnstile : Component {
port Turnstile_inputs<in>: signal;
port Turnstile_actions<out>: act;
port Gen_errors<out>: err;
}
- selected:
@FSM
implement Turnstile {
state: Locked, Unlocked := Locked;
// State + event -> State, action();
Locked + signal.coin -> Unlocked, act.unlock();
Locked + signal.pass -> , err.alarm();
Unlocked + signal.coin -> , act.thank_you();
Unlocked + signal.pass -> Locked, act.lock();
}
Tip
Although we write next_state, action()
at implementation-level the order is otherwise,
implement Turnstile { // This code is expanded by the @FSM rewriter
s: enum(Locked, Unlocked) := Locked;
Turnstile_inputs.coin on .signal {
match self.s {
case Locked: {
self.act.unlock();
self.s := Unlocked;
}
case Unlocked:
self.act.thank_you();
}
}
Turnstile_signals.pass on .signal {
match self.s {
case Locked:
self.err.alarm();
case Unlocked: {
self.act.lock();
self.s := Locked;
}
}
}
}
Code generated at AST level
“Rewriters (TODO)” (here: @FSM) will “expanded” code, such as the last tab, to equivalent to the code above -at AST/AIGR level.
As you can see in the code above, only in the (component) implement it is shown it is (implemented as/with) an FSM. This a future implementation may be differently, without any change to it users.
Eval (Component)
I like this variant!
The Turnstile FSM shows that a basic FSM in this style is very clean, and simple. Later, we will it’s expandable to; see: Counting Turnstile.
FSM as function
As a FSM has state it can’t be a “pure function” – like a
math-function: always returning the same answers on the same input, without side-effects. Pure function have great
benefits for concurent computing. However, most programming “functions” are (typical) not pure, and “procedures” would
be better name, most language uses the term function.
In line with that, we use the term ‘function’ too; in a general way.
In that view, having state is not an issue. Programming-functions can have “state”, by example by having a static
(local) variable, as in e.g. C/C++.
The state of a FSM-as-functions is like a static. And as we can see below, it results in quite easy syntax.
@FSM // Experiment: a FSM as function
Turnstile(signals:Turnstile_inputs, act:Turnstile_actions, err:Gen_errors) {
state: Locked, Unlocked := Locked;
// State + event -> State, action();
Locked + signal.coin -> Unlocked, act.unlock();
Locked + signal.pass -> , err.alarm();
Unlocked + signal.coin -> , act.thank_you();
Unlocked + signal.pass -> Locked, act.lock();
} // The syntax is clear -- BUT what are the semantics? (how to call)
There are some complication with this concept, however! When rewriting this FSM-function to a regular-function with a local
static variable, there is only one (memory) place to store the state, even when that FSM is used at multiple
places.
So, we need a kind of variable-instance pro use of the same function – that is what we typically call a class (see:
FSM_class, below).
Another tough questions is how to use “call” it, and what does it return?
Conceptually, a FSM does not “return” anything, so it would be a void function. We could return the action to be called
– which is a bit strange, but making the FSM call them is strange too.
Simular, when calling: what parameter(s) to pass?
In the example above, we pass the list of signals (as a Protocol), not the “current signal”. That “list” is needed as
the rules use it – we can’t compile (aka: verify) the transactions without the list. Again, this can solved in a
class-alike setting, e.g. by introducing a “step” method; which passes the current signal.
Eval (typical function)
As shown above, using a FSM as a (classical) function is problematic. The syntax is easy to read & write, but the semantics are complicated. And even when it is possible, it not easy to use. So, this approach is turned down.
Function syntax, to build a (local) component.
Another approach to use the function-syntax is to consider a FSM as a (kind of) iterator. Then the FSM-function becomes a
generator, as by example in Python. And the function –roughly as shown above– returns an object that is the FSM.
This solves most (all?) of the described complications.
Returning a class-object
In most languages an iterator is (like a) class-instance. So, it can store the state. When Calling the FSM-function
twice, two independent FSMs “instance” –each with it own state– are returned.
Given the (returned) FSM is an instance, “calling” it becomes simple. We can have several (build-in) methods, like
fsm.step(<current signal>)
.
Admitting it solves the “calling” and some practical problems, other complications still arise. Like how to call the
actions – are the called by the FSM, or by the code that called the FSM? Both are possible, but not perfect.
Another disadvantage is introducing “classes” (aslo see below). Classe are fine in most languages, but Castle is build
around “components” – more active, And with clean interface at the “backside”.
And luckily, the is a better solution …
Returning a component-element
When the FSM-function returns a component-element (instead of a class-instance) all uncertainties are solved!
After calling that FSM-function, we have a normal component (element) with input- and output-ports (both passed as parameter). Those port can be connected to other ports, or an input can be triggered like any component. And as the element is active, the actions of the FSM result in triggering other components/ports.
The semantics of a FSM-function are exactly the same as for the FSM-component. Actually, the rewriter for a FSM-function
will transform it to a “normal” FSM-component (and instantiate it).
Mark that a FSM-component is also rewritten. So the FSM-function is rewritten twice. Both by the same @FSM
rewriter. That however, is an implementation detail. It may be clear that there is no loop – above we already shown
that rewriting a FSM-component result is “base code”.
The only questions is: why? (and which one)
As the FSM-function and the FSM-component result in the same element, which one should we select? Or should we allow
both?
In many modern languages it is possible to define code in two (or more) ways. For example in python we can define
functions with the ‘def’ keyword (the usual way), or as a nameless “lambda”. The same aplies to “classes”, and is valid
in many languages.
Typically, there is a “standard way”, for typical use. And a “shortcut” for small, only uses once case.
The same can be useful for Castle, as we can see in a Counting Turnstile example.
Eval (component-building function)
It is possible use the function-syntax, when it results in a component-element. Although the syntax differs from the FSM-component (syntax), it has the same semantics.
It have a certain beauty, but I’m not (yet) convinced that it has advantages to allow both syntaxes. And as the
component one is closer to components (sic), the function variant is an option for now. Not more, not less.
When I add it, there is no need to implement it in the “bootstrap” compilers.
FSM as (Data)Class
Error
Class? Of Data-Classes? Or Struct’s
Until now, “classes” are hardly used in CCastle – they are kind-of replace by Components. Components however are always “active”, and we may/probably need passive “data-clases” too – e.g the AIGR has only data-clases.
Hint
As an FSM has state and behavior, a class sounds better then a Data-Class/Structure. But is it
Hint
As an FSM has state, it needs “memory”, and is typically implemented as a instantiated class in many language.
Caution
Some design-patterns use classes (with no data; and hardly an instance) for “hold” the callables for each state.
That is more namespace then a class. And surely not a data-class
Eval (Data/Class)
ToDo
FSM is not a statement
In the initial list of questions, I added the option “FSM as statement”. But in the analyse (above) it become clear that that doesn’t is a valid option. Neither the syntax, nor the semantics are clear nor have any advantages.
footnotes
Counting Turnstile
Using the Turnstile as a sub
from Turnstile_Component import Turnstile
component CountingTurnstile: Turnstile {
port Getter<coins> admin;
}
implement CountingTurnstile {
sub fsm;
int number_of_coins=0;
init() {
.fsm := Turnstile()
// sig-port: see below
.act = .fsm.act
.err = .fsm.err
}
get_coins() on .admin {
return self.number_of_coins;
}
Turnstile_signals.coin on .sig {
self.number_of_coins++;
.fsm.signals = coin // "call the FSM"
}
* on .sig as other_signals { // default
.fsm.sig = other_signals
}
} // end CountingTurnstile (sub style)
implement CountingTurnstile {
sub fsm;
int number_of_coins=0;
@FSM
Turnstile(signals:Turnstile_inputs, act:Turnstile_actions, err:Gen_errors) {
state: Locked, Unlocked := Locked;
// State + event -> State, action();
Locked + signal.coin -> Unlocked, act.unlock();
Locked + signal.pass -> , err.alarm();
Unlocked + signal.coin -> , act.thank_you();
Unlocked + signal.pass -> Locked, act.lock();
}
init() {
// Build the local FSM, and connect all port to the sub
.fsm = Turnstile(signal=self.signal, ac=self.act, err=self.err)
}
get_coins() on .admin {
return self.number_of_coins;
}
// Overide "reconnect" only the signal we need
Turnstile_signals.coin on .signal {
self.number_of_coins++;
.fsm.signal = coin
}
}} // end CountingTurnstile (fun style)