VIATRA2/UseCases/CodeGeneration

From Eclipsepedia

Jump to: navigation, search

Contents

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:

  • Model traversal strategy. In the simple case, a single-pass model traversal may be enough for your needs. This usually involves a breadth-first, depth-first, or in-order traversal of the model graph (using simple graph patterns, and forAll constructs) -- much like in template-based languages (where foreach and such are typically available). In more complex cases, a multi-pass traversal may be required, e.g. in cases where you need to collect cross-references prior to actually emitting code. In the most complex cases, it might be easier to define a separate 'code generation model, which is first derived from the original model by a model-to-model transformation, and contains all the information necessary to facilitate a simpler, single-pass code generation traversal.
  • Code emitting strategy. Bear in mind that the output is non-buffered, which means that it cannot be wound back and forth arbitrarily -- in other words, once a character has been written, it cannot be erased. This also has an effect on the way which you will have to traverse the model -- again, defining a separate, easy-to-process "code generation model" might be the best choice.

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).

  • file-based buffer URIs begin with the file:// protocol
  • in-memory buffer URIs begin with the core:// protocol

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:

  • the getBuffer() native function has the ability to create workspace projects and paths if they do not exist before invocaction --> this means that you can programmatically create stuff in the workspace from a VIATRA transformation which is cool :)
  • by default, buffer references to existing files are allowed and files are appended to.
  • for now, buffers are flushed and closed upon a SUCCESSFUL transformation termination, so if anything goes wrong, output written to file buffers is lost (will be improved)