Irony .NET Language Implementation Kit
I came across Irony (https://github.com/IronyProject/Irony) today while contemplating whether to use antlr or not for a project I’m working on where the requirements call for allowing users to write small conditional instructions.
From the project site description:
Irony is a development kit for implementing languages on .NET platform. It uses the flexibility and power of c# language and .NET Framework 3.5 to implement a completely new and streamlined technology of compiler construction.
Unlike most existing yacc/lex-style solutions Irony does not employ any scanner or parser code generation from grammar specifications written in a specialized meta-language. In Irony the target language grammar is coded directly in c# using operator overloading to express grammar constructs. Irony’s scanner and parser modules use the grammar encoded as c# class to control the parsing process. See the expression grammar sample for an example of grammar definition in c# class, and using it in a working parser.
Compared to antlr, it seemed much simpler from the samples.
In the past, I’ve usually used Spring.NET‘s expression evaluation functionality (built on antlr); however, the entirety of the Sprint.NET library seemed too heavy for the simple scenario I had to implement and I’d still have to do some string parsing anyways if I used it. So I set out to try out Irony for myself instead.
The basic gist of the solution is that the interface needs to allow users to specify meta-instructions as strings in the form of:
1 |
if ("property1"="value1") action("param1","param2") |
Once the user has configured the instructions for a given template document, the meta-instructions are executed when an instance of the template is created and metadata properties are set on the document (in SharePoint). So the goal is to define a set of meta-instructions and actions which allow users to build dynamic document templates. For example:
1 2 3 |
if ("status"="draft") delete() if ("status"="published") lock() if ("status"="pending") insert("22ad25d6-3bbd-45f3-bc63-e0e1b931e247") |
The first step is to define the grammar (I’m sure this isn’t very well constructed BNF, but I need to brush up on that :-D):
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 31 32 33 34 35 36 37 38 39 40 41 |
using Irony.Parsing; namespace IronySample { public class AssemblyDirectiveGrammar : Grammar { public AssemblyDirectiveGrammar() : base(false) { // Terminals StringLiteral property = new StringLiteral("property", "\""); StringLiteral value = new StringLiteral("value", "\""); StringLiteral param = new StringLiteral("param", "\""); IdentifierTerminal action = new IdentifierTerminal("action"); // Non-terminals NonTerminal command = new NonTerminal("command"); NonTerminal ifStatement = new NonTerminal("ifStatement"); NonTerminal comparisonStatement = new NonTerminal("comparisonStatement"); NonTerminal actionStatement = new NonTerminal("actionStatement"); NonTerminal argumentsStatement = new NonTerminal("argumentsStatement"); NonTerminal parametersStatement = new NonTerminal("paremeters"); NonTerminal parameterStatement = new NonTerminal("parameter"); // BNF command.Rule = ifStatement + NewLine; ifStatement.Rule = ToTerm("if") + comparisonStatement + actionStatement; comparisonStatement.Rule = "(" + property + "=" + value + ")"; actionStatement.Rule = action + argumentsStatement; argumentsStatement.Rule = "(" + parametersStatement + ")"; parametersStatement.Rule = MakePlusRule(parametersStatement, ToTerm(","), parameterStatement) | Empty; parameterStatement.Rule = param; MarkPunctuation("if","(", ")", ",", "="); LanguageFlags = LanguageFlags.NewLineBeforeEOF; Root = command; } } } |
This defines the elements of the “language” (see the Irony wikibook for a better explanation). (You’ll note that the “if” is entirely superfluous; I decided to leave it in there just so that it would make more sense to the expression authors as they create the meta-instruction.)
As you’re writing your grammar, it’ll be useful to test the grammar using the provided grammar explorer tool to check for errors:
Once the grammar is complete, the next step is to make use of it.
I wrote a simple console program that mocks up some data input:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
private static void Main(string[] args) { // Mock up the input. Dictionary<string, string> inputs = new Dictionary<string, string> { {"status", "ready"} }; // Mock up the instructions. string instructions = "if (\"status\"=\"ready\") Echo(\"Hello, World!\")"; Program program = new Program(); program.Run(inputs, instructions); } |
The idea is to simulate a scenario where the metadata on a document stored in SharePoint is mapped to a dictionary which is then passed to a processor. The processor will iterate through the instructions embedded in the document and perform actions.
In this example, I’ve mapped the statement directly to a method on the Program class for simplicity. The action is a method called “Echo” which will be fed on parameter: “Hello, World”. The Run() method contains most of the logic:
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 31 32 33 34 35 36 37 38 39 40 |
private void Run(Dictionary<string, string> inputs, string instructions) { // Run the parser AssemblyDirectiveGrammar grammar = new AssemblyDirectiveGrammar(); LanguageData language = new LanguageData(grammar); Parser parser = new Parser(language); ParseTree tree = parser.Parse(instructions); List<ParseTreeNode> nodes = new List<ParseTreeNode>(); // Flatten the nodes for easier processing with LINQ Flatten(tree.Root, nodes); var property = nodes.Where(n => n.Term.Name == "property").FirstOrDefault().Token.Value.ToString(); var value = nodes.Where(n => n.Term.Name == "value").FirstOrDefault().Token.Value.ToString(); var action = nodes.Where(n => n.Term.Name == "action").FirstOrDefault().Token.Value.ToString(); string[] parameters = (from n in nodes where n.Term.Name == "param" select Convert.ToString(n.Token.Value)).ToArray(); // Execute logic string inputValue = inputs[property]; if(inputValue != value) { return; } MethodInfo method = GetType().GetMethod(action); if(method == null) { return; } method.Invoke(this, parameters); } |
You can see that in this case, the evaluation is very simple; it’s a basic string equality comparison. The action execution is basic as well. It simply executes a method of the same name on the current object instance. Try running the code and changing the “ready” value in the dictionary and see what happens.
A helper method is included to flatten the resultant abstract syntax tree for querying with LINQ (could possibly be done with a recursive LINQ query?):
1 2 3 4 5 6 7 8 9 |
public void Flatten(ParseTreeNode node, List<ParseTreeNode> nodes) { nodes.Add(node); foreach (ParseTreeNode child in node.ChildNodes) { Flatten(child, nodes); } } |
And finally, the actual method that gets invoked (the action):
1 2 3 4 |
public void Echo(string message) { System.Console.Out.WriteLine("Echoed: {0}", message); } |
This sample is fairly basic, but it was pretty easy to get up and running (far easier than antlr) and there’s lots of potential for other use cases. One thing I’ve found lacking so far is documentation. It’s fairly sparse so there’s going to be a lot of trial and error, but the good news is that the source code includes a lot of examples (some of them fairly complex including C#, SQL, and Scheme grammars).