I drafted Fowlers state-machine example with MGrammar. While doing so I found a few things I would like to see optimized or find better solutions for.
- New line handling. I would like to express one required new line, when it is required, but then ignore all the others. Forum discussion.
I could make new lines part of some tokens and then just interleave other tokens. But I don’t think this would be a nice solution. Any ideas? - The blocks events, resetEvents, commands and state+ have to be written in order. It is possible to describe a grammar where unordered blocks would be allowed, but it’s not trivial.
Ordered and unordered “Lists” of syntaxes, separated somehow should IMHO be supported directly: forum discussion. - I wrote the Grammar using Intellipad and the three-pane input/grammar/AST view. It’s great! But I’d like to define the Semantic Model (using MSchema) along with the grammar and directly validate my AST against it: forum discussion.
- References are not directly supported by MGrammar/MSchema. Forum discussion.
- Xtext, which is quite comparable to MGrammar has a much more compact syntax. But I haven’t done a more deep comparison yet.
Sven Efftinge’s Blog: Fowler’s DSL example with Xtext
Still missing:
- A schema that represents the structure of the AST and validates it using constraints.
- An adapter to Simple State Machine on CodePlex or a generating a state machine framework using T4.
Would love your feedback…
Grammar definition
module MGrammar.Net.Sample { // This is my first try, to parse the state machine // dsl syntax proposed by Martin Fowler language Statemachine { interleave Ignore = " "; syntax Main = NewLines? e:Block(Events, NameAndCode) NewLines+ r:Block(ResetEvents, Name) NewLines+ c:Block(Commands, NameAndCode) NewLines+ s:List(StateBlock) NewLines? => Statemachine { Events { valuesof(e) }, ResetEvents { valuesof(r) }, Commands { valuesof(c) }, valuesof(s) }; // list of syntax "Item" separated by one or more // new lines syntax List(Item) = n:Item => {n} | list:List(Item) NewLines+ n:Item => {valuesof(list), n}; // lists of tokens separated by whitespaces syntax TokenList(Item) = n:Item => {n} | list:List(Item) n:Item => {valuesof(list), n}; // a block consisting of a keyword, new-line- // separated "Item"s and end syntax Block(Keyword, Item) = Keyword NewLines items:List(Item) NewLines End => {valuesof(items)}; // a word projected as a name // grouped into a separate successor // for extensibility reasons syntax Name = n:Word => { Name {n} }; // a word plus a code, separated by spaces syntax NameAndCode = n:Word c:Word => {Name {n}, Code{c}}; // special block and subsyntaxes for states syntax StateBlock = State n:Word actions:(NewLines a:Actions => a)? NewLines+ items:List(Transition) NewLines+ End => State { Name { n }, actions, Transitions { valuesof(items) } }; syntax Transition = e:Word "=>" s:Word => { Event {e}, ToState{s} }; syntax Actions = tActions "{" l:TokenList(Word) "}" => Actions [ valuesof(l) ]; // keyword specification for strong formatting @{Classification["Keyword"]} final token Events = "events"; @{Classification["Keyword"]} final token Commands = "commands"; @{Classification["Keyword"]} final token ResetEvents = "resetEvents"; @{Classification["Keyword"]} final token State = "state"; @{Classification["Keyword"]} final token End = "end"; @{Classification["Keyword"]} final token tActions = "actions"; // Some whitespace characters token NewLineCharacter = 'u000A' // New Line | 'u000D' // Carriage Return | 'u0085' // Next Line | 'u2028' // Line Separator | 'u2029'; // Paragraph Separator token NewLines = NewLineCharacter#2..; token Letter = 'a'..'z' | 'A'..'Z'; token Digit = '0'..'9'; token Word = (Letter (Letter | Digit)*); } }
Input Text
events doorClosed D1CL drawOpened D2OP lightOn L1ON doorOpened D1OP panelClosed PNCL end resetEvents doorOpened end commands unlockPanel PNUL lockPanel PNLK lockDoor D1LK unlockDoor D1UL end state idle actions {unlockDoor lockPanel} doorClosed => active end state active drawOpened => waitingForLight lightOn => waitingForDraw end state waitingForLight lightOn => unlockedPanel end state waitingForDraw drawOpened => unlockedPanel end state unlockedPanel actions {unlockPanel lockDoor} panelClosed => idle end
Output AST
Statemachine{ Events{ { Name{"doorClosed"}, Code{"D1CL"} }, { Name{"drawOpened"}, Code{"D2OP"} }, { Name{"lightOn"}, Code{"L1ON"} }, { Name{"doorOpened"}, Code{"D1OP"} }, { Name{"panelClosed"}, Code{"PNCL"} } }, ResetEvents{ { Name{"doorOpened"} } }, Commands{ { Name{"unlockPanel"}, Code{"PNUL"} }, { Name{"lockPanel"}, Code{"PNLK"} }, { Name{"lockDoor"}, Code{"D1LK"} }, { Name{"unlockDoor"}, Code{"D1UL"} } }, State{ Name{"idle"}, Actions[ "unlockDoor", "lockPanel" ], Transitions{ { Event{"doorClosed"}, ToState{"active"} } } }, State{ Name{"active"}, Transitions{ { Event{"drawOpened"}, ToState{"waitingForLight"} }, { Event{"lightOn"}, ToState{"waitingForDraw"} } } }, State{ Name{"waitingForLight"}, Transitions{ { Event{"lightOn"}, ToState{"unlockedPanel"} } } }, State{ Name{"waitingForDraw"}, Transitions{ { Event{"drawOpened"}, ToState{"unlockedPanel"} } } }, State{ Name{"unlockedPanel"}, Actions[ "unlockPanel", "lockDoor" ], Transitions{ { Event{"panelClosed"}, ToState{"idle"} } } } }