Martin Fowlers State Machine is quickly becoming the 99 bottles of beer on the wall for DSLs. So I implemented it in meta# and will be using it in my presentation at the next Twin Cities Code Camp.
The state machine sample project consists of two grammars, a parser and a transformer. It also has a state machine AST (semantic model) and a state machine runtime. The accompanying sample app uses an almost identical syntax as the one Fowler has in his book and is driven using a very simple console app.
StateMachineParser.g
namespace StateMachineCompiler:
import MetaSharp.Transformation;
import MetaSharp.Transformation.Lang;
import StateMachineCompiler.Ast;
import System;
import System.CodeDom;
import System.Linq;
import System.Reflection;
import System.Collections.Generic;
grammar StateMachineParser < LangParser:
override TypeDeclaration
= StateMachineDeclaration
| super;
StateMachineDeclaration
= StateMachine name:Identifier BlockBegin
e:EventsDeclaration?
r:ResetsDeclaration?
c:CommandsDeclaration?
s:StateDeclarations*
error until BlockEnd -> {
StateMachineDeclaration sm = new StateMachineDeclaration();
sm.Name = name as string;
sm.Events = e.Cast<EventDeclaration>();
sm.Resets = r.Cast<ResetDeclaration>();
sm.Commands = c.Cast<CommandDeclaration>();
sm.States = s.Cast<StateDeclaration>();
return sm;
}
EventsDeclaration
= Events BlockBegin
e:EventMember*
error until BlockEnd
-> e;
EventMember = name:Identifier code:Identifier StatementEnd -> {
EventDeclaration e = new EventDeclaration();
e.Name = name as string;
e.Code = code as string;
return e;
}
ResetsDeclaration
= Resets BlockBegin
r:ResetMember*
error until BlockEnd
-> r;
ResetMember = name:Identifier StatementEnd -> {
ResetDeclaration r = new ResetDeclaration();
r.Name = name as string;
return r;
}
CommandsDeclaration
= Commands BlockBegin
c:CommandMember*
error until BlockEnd
-> c;
CommandMember = name:Identifier code:Identifier StatementEnd -> {
CommandDeclaration c = new CommandDeclaration();
c.Name = name as string;
c.Code = code as string;
return c;
}
StateDeclarations
= State name:Identifier BlockBegin
a:ActionsDeclaration?
t:TransitionMember*
error until BlockEnd -> {
StateDeclaration s = new StateDeclaration();
s.Name = name as string;
s.Transitions = t.Cast<TransitionDeclaration>();
if(a != null):
List<string> actions = new List<string>();
for(Node n in a.Cast<Node>()):
actions.Add(Node.Unwrap(n) as string);
end
s.Actions = actions;
end
return s;
}
ActionsDeclaration = Actions "{" a:List(Identifier, ",") "}" StatementEnd -> a;
TransitionMember = e:Identifier "=>" s:Identifier StatementEnd -> {
TransitionDeclaration t = new TransitionDeclaration();
t.EventName = e as string;
t.StateName = s as string;
return t;
}
[Keyword] StateMachine = "statemachine";
[Keyword] Events = "events";
[Keyword] Resets = "resets";
[Keyword] Commands = "commands";
[Keyword] State = "state";
[Keyword] Actions = "actions";
override CustomTokens
= '=' '>'
| super;
end
end
The parser inherits from LangParser and extends Fowlers DSL by being both inside of a namespace and also wrapped in a statemachine declaration block. Also I changed the block named “resetEvents” to just be “resets” which seemed more consistent to me.
StateMachineTransformer.g
namespace StateMachineCompiler:
import MetaSharp.Transformation;
import MetaSharp.Transformation.Lang.Ast;
import StateMachineCompiler.Ast;
import System;
import System.CodeDom;
import System.Linq;
import System.Collections.Generic;
grammar StateMachineTransformer < StateMachineParser:
Main = UnitVisitor;
UnitVisitor = Unit { Namespaces = [NamespaceVisitor*] };
NamespaceVisitor = Namespace { Types = [TypeVisitor*] };
TypeVisitor
= StateMachineVisitor
| CodeTypeDeclaration { };
StateMachineVisitor = smd:StateMachineDeclaration {
e:Events = [EventVisitor*],
r:Resets = [ResetVisitor*],
c:Commands = [CommandVisitor*],
s:States = [StateVisitor*] -> Parser.Flatten(match)
} -> {
StateMachineDeclaration d = smd as StateMachineDeclaration;
d.BaseTypes.Add(typeof(StateMachineBuilder));
Constructor cons = new Constructor();
cons.Attributes = MemberAttributes.Public;
cons.Parameters.Add(new ParameterDeclarationExpression(typeof(CommandChannel), "commandChannel"));
cons.BaseConstructorArgs.Add(new ReferenceExpression("commandChannel"));
d.Members.Add(cons);
Method createMachineMethod = new Method();
createMachineMethod.Name = "CreateMachine";
createMachineMethod.Attributes = MemberAttributes.Family | MemberAttributes.Override;
createMachineMethod.ReturnType = new TypeReference(typeof(StateMachine));
d.Members.Add(createMachineMethod);
createMachineMethod.Statements.AddRange(e.Cast<CodeStatement>().ToArray());
createMachineMethod.Statements.AddRange(c.Cast<CodeStatement>().ToArray());
createMachineMethod.Statements.AddRange(s.Cast<CodeStatement>().OfType<VariableDeclarationStatement>().ToArray());
StateDeclaration first = d.States.First();
VariableDeclarationStatement v = new VariableDeclarationStatement(
typeof(StateMachine),
"machine",
new NewExpression(
typeof(StateMachine),
new ReferenceExpression("state_" + first.Name)));
createMachineMethod.Statements.Add(v);
createMachineMethod.Statements.AddRange(r.Cast<CodeStatement>().ToArray());
createMachineMethod.Statements.AddRange(s.Cast<CodeStatement>().OfType<ExpressionStatement>().ToArray());
ReturnStatement ret = new ReturnStatement(new ReferenceExpression("machine"));
createMachineMethod.Statements.Add(ret);
return d;
}
EventVisitor = EventDeclaration { } -> {
EventDeclaration e = match as EventDeclaration;
VariableDeclarationStatement v = new VariableDeclarationStatement(
typeof(Event),
"event_" + e.Name,
new NewExpression(
typeof(Event),
new PrimitiveExpression(e.Name),
new PrimitiveExpression(e.Code)));
return v;
}
ResetVisitor = ResetDeclaration { } -> {
ResetDeclaration r = match as ResetDeclaration;
MethodInvokeExpression addResetEvent = new MethodInvokeExpression(
new ReferenceExpression("machine"),
"AddResetEvents",
new ReferenceExpression("event_" + r.Name));
return new ExpressionStatement(addResetEvent);
}
CommandVisitor = CommandDeclaration { } -> {
CommandDeclaration c = match as CommandDeclaration;
VariableDeclarationStatement v = new VariableDeclarationStatement(
typeof(Command),
"command_" + c.Name,
new NewExpression(
typeof(Command),
new PrimitiveExpression(c.Name),
new PrimitiveExpression(c.Code)));
return v;
}
StateVisitor = StateDeclaration { } -> {
StateDeclaration s = match as StateDeclaration;
Nodes statements = new Nodes();
List<CodeExpression> parameters = new List<CodeExpression>();
parameters.Add(new PrimitiveExpression(s.Name));
for(string a in s.Actions):
parameters.Add(new ReferenceExpression("command_" + a));
end
statements.Add(new VariableDeclarationStatement(
typeof(State),
"state_" + s.Name,
new NewExpression(
typeof(State),
parameters.ToArray())));
for(TransitionDeclaration td in s.Transitions):
statements.Add(new ExpressionStatement(
new MethodInvokeExpression(
new ReferenceExpression("state_" + s.Name),
"AddTransition",
new ReferenceExpression("event_" + td.EventName),
new ReferenceExpression("state_" + td.StateName))));
end
return statements;
}
end
end
This grammar inherits from StateMachineParser and therefore it receives the parsers output as input. It is an example of implementing a visitor pattern as a meta# grammar. It visits the state machine AST nodes and expands them into code objects, which a subsequent step uses to generate code.
Here is the modified ubiquitous state machine DSL, MissGrants.sm
namespace App:
statemachine MissGrants:
events:
doorClosed D1CL;
drawerOpened D20P;
lightOn L10N;
doorOpened D10P;
panelClosed PNCL;
end
resets:
doorOpened;
end
commands:
unlockPanel PNUL;
lockPanel PNLK;
lockDoor D1LK;
unlockDoor D1UL;
end
state idle:
actions {unlockDoor, lockPanel};
doorClosed => active;
end
state active:
drawerOpened => waitingForLight;
lightOn => waitingForDrawer;
end
state waitingForLight:
lightOn => unlockedPanel;
end
state waitingForDrawer:
drawerOpened => unlockedPanel;
end
state unlockedPanel:
actions {unlockPanel, lockDoor};
panelClosed => idle;
end
end
end
Here is the code generated by the meta# transformers…
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.237
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace App
{
public class MissGrants : StateMachineCompiler.StateMachineBuilder
{
public MissGrants(StateMachineCompiler.CommandChannel commandChannel) :
base(commandChannel)
{
}
protected override StateMachineCompiler.StateMachine CreateMachine()
{
StateMachineCompiler.Event event_doorClosed = new StateMachineCompiler.Event("doorClosed", "D1CL");
StateMachineCompiler.Event event_drawerOpened = new StateMachineCompiler.Event("drawerOpened", "D20P");
StateMachineCompiler.Event event_lightOn = new StateMachineCompiler.Event("lightOn", "L10N");
StateMachineCompiler.Event event_doorOpened = new StateMachineCompiler.Event("doorOpened", "D10P");
StateMachineCompiler.Event event_panelClosed = new StateMachineCompiler.Event("panelClosed", "PNCL");
StateMachineCompiler.Command command_unlockPanel = new StateMachineCompiler.Command("unlockPanel", "PNUL");
StateMachineCompiler.Command command_lockPanel = new StateMachineCompiler.Command("lockPanel", "PNLK");
StateMachineCompiler.Command command_lockDoor = new StateMachineCompiler.Command("lockDoor", "D1LK");
StateMachineCompiler.Command command_unlockDoor = new StateMachineCompiler.Command("unlockDoor", "D1UL");
StateMachineCompiler.State state_idle = new StateMachineCompiler.State("idle", command_unlockDoor, command_lockPanel);
StateMachineCompiler.State state_active = new StateMachineCompiler.State("active");
StateMachineCompiler.State state_waitingForLight = new StateMachineCompiler.State("waitingForLight");
StateMachineCompiler.State state_waitingForDrawer = new StateMachineCompiler.State("waitingForDrawer");
StateMachineCompiler.State state_unlockedPanel = new StateMachineCompiler.State("unlockedPanel", command_unlockPanel, command_lockDoor);
StateMachineCompiler.StateMachine machine = new StateMachineCompiler.StateMachine(state_idle);
machine.AddResetEvents(event_doorOpened);
state_idle.AddTransition(event_doorClosed, state_active);
state_active.AddTransition(event_drawerOpened, state_waitingForLight);
state_active.AddTransition(event_lightOn, state_waitingForDrawer);
state_waitingForLight.AddTransition(event_lightOn, state_unlockedPanel);
state_waitingForDrawer.AddTransition(event_drawerOpened, state_unlockedPanel);
state_unlockedPanel.AddTransition(event_panelClosed, state_idle);
return machine;
}
}
}
And finally the console app to drive and simulate the state machine, Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using StateMachineCompiler;
namespace App
{
class Program
{
static void Main(string[] args)
{
CommandChannel channel = new CommandChannel(a => Console.WriteLine("Action: " + a));
var builder = new MissGrants(channel);
bool done = false;
while (!done)
{
Console.WriteLine("State: " + builder.CurrentState);
Console.Write("> ");
var cmd = Console.ReadLine();
builder.Handle(cmd);
}
}
}
}
You must be logged in to post a comment.