Frame System Notation (FSN) v0.3
Comments are single line and indicated by three (3) dashes:
1 2 --- this is a single line comment --- as is this
Variable and Parameter Declarations
Variables and parameter declarations share a core common syntax.
Parameters are declared as follows and separated by whitespace:
The name is required but the :type is optional. Parameter lists are one or more parameter declarations enclosed in brackets:
Therefore parameter lists can be declared either of these two ways:
1 2 3 4 5 [param1 param2] --- or --- [param1:type1 param2:type2]
Variable and constant declarations have the following format:
1 2 3 4 <var | const> <name> : <type_opt> = <intializer> var x:int = 1 const name = "Steve"
Variables can be modified after initialization and constants can not. Frame will transpile into the closest semantic equivalents in the target language.
The type is optional but the initializer is not.
If you transpile into a language that requires a type and you don’t provide one, a token such as
<?> is substituted. Conversely, if you add a type and transpile into a language that doesn’t require one, the Framepiler ignores it.
All methods (for all blocks) have a similar syntax:
1 2 <method-name> <parameters-opt> <return-value-opt>
As implied above, the parameters and return value are optional. Here are the permutations for method declarations:
1 2 3 4 5 6 7 method_name method_name [param] method_name [param:type] method_name [param1 param2] method_name [param1:type param2:type] method_name : return_value method_name [param1:type param2:type] : return_value
One important difference between Frame and other languages is the lack of any commas or semicolons as separators. Instead Frame relies on whitespace to delineate tokens:
1 2 3 4 5 6 7 8 9 10 --- lists --- [x y] [x:int y:string] (a b c) (d() e() f()) --- statements --- a() b() c() var x:int = 1
Unlike other languages where structured whitespace is significant (e.g. Python), Frame’s use of whitespace is unstructured. Frame only separates tokens with whitespace and does not insist on any pattern of use.
The esthetic goal is to be as spare and clean as possible, but it may take some getting used to.
List come in two flavors - parameter lists and expression lists.
Frame uses square brackets to denote parameter lists:
1 2 [x y] [x:int y:string]
System Controller Architecture
Now with the basics explained we can take a look at the big picture and work in.
# token prefix tags an identifier as a system. the
## token indicates the end of the system specification.
1 2 #MySystemController ##
Currently Frame generates an object-oriented class for each system specification document. However, this does not preclude other ways to implement system controllers which will be an interesting research topic for the future.
Frame is highly opinionated about the internal structure of system controllers and currently specifies a composition of four sequential (but optional) “blocks”:
- State Machine
Here is the syntax for these blocks:
1 2 3 4 5 6 #MySystemController -interface- -machine- -actions- -domain- ##
Once again, there can be 0-4 blocks, but if present they must be in this order. Tying all of these together is the use of the FrameEvent which we will explore next.
Frame Events are essential to the notation and the implementation of Frame system controllers. Frame notation assumes three mandatory fields for a FrameEvent:
messageobject (String, enumeration, other)
parameterskey/value lookup object
Here is a basic implementation of this class:
Frame notation uses the
@ symbol to identify a FrameEvent. Each of the three FrameEvent attributes has its own accessor symbol as well:
|^(value)||frameEvent._return = value; return;|
Frame has six special reserved messages for important operations:
The semantics of the
|<| events are understood by the Framepiler and functionally supported. The remaining messages are optional may be unused or replaced by other messages with the same semantics if desired.
The Interface Block contains a list of zero or more public methods with an optional alias annotation.
Here is a simple
#Dog with a single public interface method:
1 2 3 4 5 6 #Dog -interface- speak ##
This code snippet isn’t actually functional as it references a missing
_state_ variable which wasn’t generated. However it does show the most basic syntax for an interface method.
To get our
#Dog to respond we can send it a text string of what we want it to say and return an AudioClip of it’s attempt:
1 2 3 4 5 6 #Dog -interface- speak [name:string] : AudioClip ##
By default, Frame generates a message identical to the name of the interface method. In order to send a different message Frame supports the message alias syntax:
This can be used to change the messages that the interface sends to the state machine:
1 2 3 4 5 6 #SpanishDog -interface- speak [name:string] : AudioClip @(|hable|) ##
As we can see interface methods do not provide any actual functionality. Instead, the interface simply transmits the message and parameters from the outside world to the system controller’s internal state machine. When the machine completes it’s activity the interface returns whatever values come back from it on the FrameEvent’s return attribute.
Frame takes advantage of the message alias capability to define a standardized pattern of starting and stopping a system:
1 2 3 4 5 6 7 #System -interface- start @(|>>|) stop @(|<<|) ##
This feature gives flexibility in naming the interface methods while maintaining a standardized internal protocol for system start/stop using
<<. We will revisit how these are used after introducing Transitions below.
The Machine Block houses the system state machine and defines the behavior of the system. The machine block is comprised of zero or more states. If it exists, the first state is defined to be the “start state” which is what the machine’s
_state_ variable is initiazed to.
1 2 3 4 5 6 #LaMachine -machine- $Begin ##
State identifiers are indicated by the
In the Frame reference implementation, states are implemented as methods with this signature:
void state(FrameEvent e)
In languages that support or require it, this signature is typedef’d with a name:
The machine implementation must contain a private member variable for maintaining a reference to the current state:
_state_ variable must be initialized in the constructor or before the object is used to the start state.
After being initialized, the
_state_ variable should only be modified as part of a “transition” or “state change” (see below). Without a transition or state change, the machine will only stay in its initial state. Here is a
C# minimal state machine:
Event Handlers are sections of code called in response to a message.
1 2 3 4 5 6 7 8 9 10 11 12 #Dog -interface- speak [name:string] : AudioClip -machine- $Begin |speak| --- Event Handler message selector ^ --- return ##
Event handlers have two terminator tokens - return and continue.
The basic return token simply returns from the state function while the return value variant first sets the FrameEvent’s return value. The continue token allows the code to break out of the message tests and, as we will see later, enable further processing in parent states.
1 2 3 4 5 6 7 8 9 10 11 12 13 ... -machine- $State |continueEvent| > --- continue terminator |returnEvent| ^ --- return terminator |returnValueEvent| ^(42) --- return value terminator ...
Actions are protected methods that can be called in event handlers to implement system behavior:
1 2 3 4 5 6 7 8 9 10 11 #Dog -interface- speak [name:string] : AudioClip -machine- $Begin |speak| speak() ^ ##
The Framepiler appends a standard suffix (in this case
_do) so that the interface can call actions with the same name and not have a namespace collision.
State machines require the ability to update the
_state_ variable so as to point to a different
FrameState method. In addition to simply switching state, transitions adhere to the following Statechart semantics:
- send an exit message
<to the current state
- change the machine to the new state
- send and enter message
>to the new state
< event handler, if present, can deallocate resources and do other clean up of the state that is being exited.
> event handler, if present, can allocate resources and do other initialization of the state that is being entered.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #Dog -interface- start @(|>>|) speak hush -machine- $Begin |>>| -> $Quiet ^ $Quiet |speak| -> $Barking ^ $Barking |>| startBarking() ^ |<| stopBarking() ^ |hush| -> $Quiet ^ ##
Below is a complete example of system lifecycle management utilizing many of the concepts that have been introduced so far:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #System -interface- start @(|>>|) stop @(|<<|) -machine- $Begin |>>| -> $Working ^ $Working |<<| -> $End ^ $End ##
Sometimes it is desirable to change states without triggering the full enter/exit machinery involved with a transition. The state change operator enables this capability:
A simple filter system is a good example of when changing state is more appropriate than a full transition. Here we can see that the
#Filter system simply oscillates between the
$On states. It’s only behaivor is enabling and disabling transmission of objects through it so no state resource management is necessary:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #Filter -interface- open close send [obj:object] -machine- $Closed |open| ->> $Open ^ $Open |close| ->> $Closed ^ |send| [obj:object] send(obj) ^ -actions- send [obj:object] ##
Frame notates a state change
->> visually using a dashed line:
The implementation of the state change mechanism consists of the addition of the
_changeState_() method that only updates the
_state_ variable (no enter/exit events) as well as its use in the event handlers:
Branching Control Flow
Frame notation supports two general syntaxes for branching control flow:
- Boolean Tests
- Pattern Matching
The basic syntax for both classes of test are:
1 2 x ?<type> <branches> : <else clause> ::
: token is “else” and
:: terminates the statement for all branching statement types.
Let’s explore the boolean test first.
The basic boolean test in Frame is:
1 x ? callIfTrue() : callIfFalse() ::
This generates this in
To reinforce the point that branching in Frame is not an expression evaluation, see how we can call multiple statements inside each branch:
1 2 3 4 5 6 7 x ? a() b() : c() d() ::
To negate the test use the
1 x ?! callIfFalse() : callIfTrue() ::
Next we will explore the Frame equivalent of the switch statement for string matching.
Pattern Matching Statements
Frame uses a novel but easy to understand notation for switch-like statements:
1 2 3 4 test ?<type> /pattern1/ statements :> /pattern2/ statements : statements ::
The currently supported operators are
?~ for string matching and
?# for number/range matching. The
: token indicates else/default and
:: terminates the pattern matching statement.
The string matching statement looks like this:
1 2 3 4 name() ?~ /Elizabeth/ hiElizabeth() :> /Robert/ hiRobert() : whoAreYou() ::
And results in this code:
Frame also permits multiple string matches per pattern:
1 2 3 4 name() ?~ /Elizabeth|Beth/ hiElizabeth() :> /Robert|Bob/ hiRobert() : whoAreYou() ::
With this output:
Number matching is very similar to string pattern matching:
1 2 3 4 n ?# /1/ print("It's a 1") :> /2/ print("It's a 2") : print("It's a lot") ::
The output is:
Frame can also pattern match multiple numbers to a single branch as well as compare decimals:
1 2 3 4 n ?# /1|2/ print("It's a 1 or 2") :> /101.1|100.1/ print("It's over 100") : print("It's a lot") ::
The output is:
Branches and Transitions
The default behavior of Frame is to label transitions with the message that generated the transition. This is fine when an event handler only contains a single transition:
1 2 3 4 5 6 7 8 9 10 #GottaBranch -machine- $A |e1| -> $B ^ $B ##
However this leads to ambiguity with two or more transitions from the same event handler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #GottaBranch_v2 -machine- $Uncertain |inspect| foo() ? -> $True : -> $False :: ^ $True $False ##
Transition labels provide clarity as to which transition is which:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #GottaBranch_v3 -machine- $Uncertain |inspect| foo() ? -> "true foo" $True : -> "foo not true" $False :: ^ $True $False ##
Actions are declared in the
-actions- block and observe all of the method declaration syntax discussed above. However actions do not support the alias notation:
1 2 3 4 5 6 7 8 9 #Kinetic -actions- walk run [speed:float] isHurt : bool ##
Frame does not (yet) support language features for actually defining useful methods. Therefore the best practice to utilize the system controller Frame emits is to create a subclass that inherits from the controller and overrides the action declarations.
Notice that the generated controller code supports a default defensive programming mechanism by throwing an exception if the base class method is called.
Domain System Variables
The final block in a Frame system specification is the
-domain- where system level (member) variables are declared:
1 <var | const> <name> : <type_opt> = <intializer>
Frame supports variables (
var) which can be changed after initializations and constants (
const) which cannot.
1 2 3 4 5 6 7 8 9 #SumData -domain- var i = 0 var temp:float = 98.6 const name:string = "Bob" ##
As discussed previously, variables and constants can be untyped but not uninitalized. If a type is required by a target language the Framepiler will emit a token like
<?> to indicate a missing required but unspecified type.
Transition parameters allow system designers to specify that data that should be sent to enter
|>| and exit
|<| event handlers during a transition.
This capability simplifies managing data passing when the current event handler shouldn’t process information in the current context.
Enter Event Parameters
Frame provides notation to directly pass arguments to the new state as part of a transition:
-> (<enter_argument_list>) $NewState
-> ("Mark") $PrintName
This list is sent as arguments to the enter event in the target state:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #EnterEventParameters -interface- start @(|>>|) -machine- $Begin |>>| -> ("Hello $State") $State ^ $State |>| [greeting:string] print(greeting) ^ -actions- print[message:string] ##
Exit Event Parameters
Though not as common an operation as sending data forward to the next state, Frame also enables sending data to the exit event hander of the current state as well:
(<exit_argument_list>) -> $NewState
("cya") -> $NextState
1 2 3 4 5 6 $OuttaHere |<| [exitMsg:string] print(exitMsg) ^ |gottaGo| ("cya") -> $NextState ^
In addition to parameterizing the transition operator, Frame enables passing arguments to states themselves. State arguments are passed in an expression list after the target state identifier:
State parameters are declared as a parameter list for the state:
Unlike transition parameters which are scoped to the enter/exit event handlers, state parameters are scoped to the lifecycle of the state itself and therefore in scope for any state event handler.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #StateParameters -interface- start @(|>>|) stop @(|<<|) -machine- $Begin |>>| -> $State("Hi! I am $State :)") ^ $State [stateNameTag:string] |>| print(stateNameTag) ^ |<| print(stateNameTag) ^ |<<| print(stateNameTag) -> $End ^ $End -actions- printAll[message:string] -domain- var systemName = "#Variables" ##
Above we see that the
stateNameTag is accessible in the enter, exit and stop event handlers. It will also be in scope for all other event handlers for the state as well.
Frame has three scopes for variable declarations:
- System domain variables
- State variables
- Event handler variables
System Domain Variables
In object-oriented terminology, domain variables are simply member variables. As such, their scope is system wide.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #DomainVariables start @(|>>|) -machine- $Begin |>>| print(systemWide) -> $S0 ^ $S0 |>| print(systemWide) -> $S1 ^ $S1 |>| print(systemWide) -> $End ^ $End |>| print(systemWide) -> $End ^ -domain- var systemWide:string = "bigtime" ##
The next scope for declaring variables is the state. States can declare variables above the first event handler:
1 2 3 4 5 6 ... $State var x:int = 0 |>| ^ ...
State variable scope is across all event handlers for the lifecycle of the state:
1 2 3 4 5 6 7 8 9 10 11 #StateVariableExample -machine- $Working var stateName:string = "$Working" |>| print(stateName) ^ |<| print(stateName) ^ |<<| print(stateName) ^ ##
As we can see, the state variable
stateName stays in scope for the active lifecycle of the state, just like state parameters.
Event Handler Variables
Event handler variables are scoped to the event handler they are declared in:
1 2 3 4 5 6 7 8 9 10 11 12 #EventHandlerVariableExample -machine- $Working |>| var id:string = "12345" print(getFirstName(id)) ^ |e1| print(getFirstName(id)) ^ --- Error! ##
If variables have unique names then Frame resolves them by searching in the following priority order for scopes for the first match:
- Event Handler Variables
- Event Handler Parameters
- State Variables
- State Parameters
- System Domain Variables
To disambiguate variables with the same name in different scopes, Frame uses the following symbols:
|Event Handler Variable|
|Event Handler Parameter|
Here we can see the use of each of these scope identifiers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #ScopeIdentifiers -interface- start @(|>>|) -machine- $Begin |>>| -> (2) $Scopes(4) ^ $Scopes[d:int] --- 4 var c:int = 3 |>| [b:int] --- 2 var a:int = 1 output(||.a ||[b] $.c $[d] #.e) ^ -actions- output[a:int b:int c:int d:int e:int] -domain- var e:int = 5 ##
You can see the generated controller here: