Martin Fowlers State Machine in meta#

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);
            }
        }
    }
}