Thursday, 16 May 2013

Using AST technology (from ICSharpCode.NRefactory) to script the creation of a C# file (using O2's C# REPL editor)

Sometimes the best/only way to create a script is to codify it (i.e with another script).

In this post I’m going to show how the O2’s FluentSharp APIs can be used to create valid C# code that can then be compiled and executed.

There is already a huge amount of C# AST APIs and extension methods in the FluentSharp.REPL which we will use to dynamically create a C# script (i.e. we are going to create an AST object which will then create a C# source code file)

Here are some of the APIs we are going to use:
image image

Since the output of our script is going to be another C# script, the best way to develop it is to use the O2 Platform's Util - REPL Script a Code Editor.h2  script (see the Creating a REPL editor that is linked to a Code editor post for details on how this GUI was created), which can be found here:

image

and looks like this:

image

(the C# REPL editor on the left that has the code editor on the right passed as a parameter):

To start with the AST manipulation/creation we are going to create an CompilationUnit object with one AST using statement.

image

Note how the code editor on the right contains the source code representation of the CompilationUnit object (which is created by the CompilationUnit's csharpCode() extension method). You can't see it on this screenshot, but that C# code generation happens in real-time (i.e. less than 1s to compile, execute and show :) )

The FluenSharp REPL extension methods make it easy to add a class:

image

… and a method:

image

… and a method’s body:

image

… and the method’s return type + return value

image

… and adding a variable:

image

… and invoking a method

image

What is really nice about the last script is that it actually compiles and runs :)

To see it in action, lets save the current script as a *.cs file and open it in the code editor:

image

where we can compile the source code:

image

and

image

execute it:

image

Note: the previous script used the FluentSharp alert method to show the MessageBox, here is the same result using the .NET MessageBox static method Show:

image

To handle cases where we need to use the generated AST scripts from the command line (like in an CI environment) here is how to create an exe instead:

image

Where the script above will save the C# and Exe files into a temp folder:

image

with the exe’s execution looking like this:

image


Scripts created during this post

1) First CompilationUnit example
var compilationUnit = new CompilationUnit();
compilationUnit.add_Using("System.AAAA");
var csharpCode = compilationUnit.csharpCode();
codeEditor.set_Text(csharpCode,".cs");
return compilationUnit;
//using ICSharpCode.NRefactory.Ast
using O2.API.AST.ExtensionMethods.CSharp
2) script that uses FluentSharp‘s alert  extension method
   1: var compilationUnit = new CompilationUnit();

   3: compilationUnit.add_Using("System.Windows.Forms");
   4:  
   5: var aClass  = compilationUnit.add_Type("AClass");
   6: var aMethod = aClass.add_Method("AMethod");
   7: var body    = aMethod.add_Body();
   8: aMethod.setReturnType("void"); 
   9:  
  10: body.add_Variable("message","bar");
  11: body.add_Invocation("foo","alert");
  12:  
  13: body.add_Return("A value");
  14: var csharpCode = compilationUnit.csharpCode();
  15:  
  16: //codeEditor.set_Text(csharpCode,".cs");
  17: codeEditor.open(csharpCode.saveWithExtension(".cs"));
  18:  
  19: return "ok";
  20:  
  21: //using ICSharpCode.NRefactory.Ast
  22: //using O2.API.AST.ExtensionMethods.CSharp

3) Script that uses MessageBox’s Show static method
   1: var compilationUnit = new CompilationUnit();
   2: compilationUnit.add_Using("System.Windows.Forms");
   3: compilationUnit.add_Using("System");
   4:  
   5: var aClass  = compilationUnit.add_Type("AClass");
   6: var aMethod = aClass.add_Method("AMethod")
   7:                     .setReturnType("void"); 
   8:  
   9: var body = aMethod.add_Body(); 
  10:     body.add_Variable("message","hello world");
  11:     body.add_Invocation("MessageBox","Show", 
  12:                         "message".identifier());
  13:     
  14: var csharpCode = compilationUnit.csharpCode();
  15:  
  16: //codeEditor.set_Text(csharpCode,".cs");
  17: var tempFile = csharpCode.saveWithExtension(".cs");
  18: codeEditor.open(tempFile)
  19:           .compileCSSharpFile();
  20:           
  21:  
  22: return "ok";
  24: //using ICSharpCode.NRefactory.Ast
  25: //using O2.API.AST.ExtensionMethods.CSharp
4) Script that creates a stand-alone exe (from the generated code)
   1: var compilationUnit = new CompilationUnit();
   2: compilationUnit.add_Using("System.Windows.Forms");
   3: compilationUnit.add_Using("System");
   4:  
   5: var aClass  = compilationUnit.add_Type("AClass");
   6: var aMethod = aClass.add_Method("Main")
   7:                     .setReturnType("void")
   8:                     .public_Static(); 
   9:                 
  10:  
  11: var body = aMethod.add_Body(); 
  12:     body.add_Variable("message","hello world (in MessageBox)");
  13:     body.add_Invocation("Console","Write", "Hello world (in Console)");  
  14:     body.add_Invocation("MessageBox","Show",  
  15:                         "message".identifier());
  16:     
  17: var csharpCode = compilationUnit.csharpCode();
  18: csharpCode = csharpCode.replace("Main()", "Main(string[] parameters)");
  19:  
  20: var tempDir = "_fromAstToExe".tempDir(false);
  21: var tempFile = tempDir.pathCombine("AClass.cs");
  22:  
  23: csharpCode.saveAs(tempFile);
  24:  
  25: var exe = tempFile.compileToExe("AClass", tempDir);
  26: return exe; 
  27:  
  28: //exe.startProcess();
  29:           
  30: return "ok";
  31: //using ICSharpCode.NRefactory.Ast
  32: //using O2.API.AST.ExtensionMethods.CSharp
  33: //O2File:_Extra_methods_To_Add_to_Main_CodeBase.cs