Code generation with VIATRA

VIATRA, apart from being a general purpose model transformation tool, is also well suited for code generation tasks. In fact, code generation can be thought of as a special case of model transformation, where the (final) output is some textual content.

Basically, textual output can emitted easily by using the print() and println() built-in rules of the VTCL. These rules accept, in the simple case, a single Term consisting of Variables, Constants and Expressions combining these with operators.

Some simple examples:

rule test() = seq {
 print("Hello world!"); // string constant
 print(3); // integer constant
 print(3+4); // simple term
 let V1 = "variable1" in print(V1); // variable
 let V2 = "variable2" in println("The value is: "+V2); // expression with constant and variable: concatenation
}

Code generation techniques

In VIATRA, code generation usually means traversing a model and emitting textual output. So, in order to create a code generator, you have to think of two things:

A simple example

The following is a very simple code generation example, which generates XML from a simplified class diagram model.

import metamodel.uml;
machine generateXML()
{
 pattern class(C) {
  Class(C);
 }
 pattern attribute(A) {
  Attribute(A);
 }
 pattern c_a(C,A) {
  Class(C);
  Attribute(A);
  Class.attr(CA,C,A);
 }
 rule main() = seq {
  println("<?xml version=1.0" encoding="UTF-8"?>
  println("<classes>");
  forall C with find class(C) do seq
  {
   println("\t<class name=\""+name(C)+"\">"); // println supports control characters like \t
   forall A with find c_a(C,A) do println("\t\t<attr name=\""+name(A)+"\"/>"); // escaping is important!
   println("\t</class>");
  }
  println("</classes">);
 }
}

Multiple output buffers

With VIATRA, you can use multiple output buffers for code generation. This feature is useful is a number of scenarios, e.g. when you wish to separate debugging and/or tracing output (emitted while the code generation transformation is running) from the actual result of the generation. You may also frequently encounter cases where the output itself needs to be separated into multiple files.

To use multiple output buffers, you may supply an additional parameter (as the first parameter) to the print() and println() rules, which holds a special native value retrieved from an invocation of the getBuffer() native function.

The getBuffer() function accepts a special URI as a parameter, which specifies either a file-based output buffer (with a workspace-relative path), or an in-memory buffer (with an identifier).

See this illustrative example below for usage details:

machine helloworld
{
 rule main() = seq
 {
  let Buf0 = getBuffer("file://test/dir/file.ext") in // append
  let Buf1 = getBuffer("file://test/dir2/file.ext") in // new dir
  let Buf2 = getBuffer("file://test/dir2/file2.ext") in // new file
  let Buf3 = getBuffer("file://test3/dir/file.ext") in // new project
  let Buf4 = getBuffer("file://test4/dir/dir2/file.ext") in
  let Buf5 = getBuffer("file://test5/dir/dir2/dir3/file.ext") in
  let Buf6 = getBuffer("file://test6/file.ext") in
  let Buf7 = getBuffer("core://test7") in // in-memory buffer
  let Content = "Test content written to buffer output" in
  seq
  {
   println(Buf0, Content);
   println(Buf1, Content);
   println(Buf2, Content);
   println(Buf3, Content);
   println(Buf4, Content);
   println(Buf5, Content);
   println(Buf6, Content);
   println(Buf7, Content);
  }
  println("This goes to the default output!");
 }	
}

Important notes: