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"}
}
}
}
}
