diff --git a/relast.preprocessor/.gitignore b/relast.preprocessor/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..87b4cdd3d7c6a41502ca98703abeeb69a1d536fb
--- /dev/null
+++ b/relast.preprocessor/.gitignore
@@ -0,0 +1,5 @@
+build
+src/gen-res/
+src/gen/
+out/
+*.class
diff --git a/relast.preprocessor/build.gradle b/relast.preprocessor/build.gradle
new file mode 100644
index 0000000000000000000000000000000000000000..21370e02d04408a00c85681827b0e913da8df782
--- /dev/null
+++ b/relast.preprocessor/build.gradle
@@ -0,0 +1,147 @@
+
+apply plugin: 'java'
+apply plugin: 'jastadd'
+apply plugin: 'application'
+apply plugin: "idea"
+
+sourceCompatibility = 1.8
+
+mainClassName = 'org.jastadd.relast.compiler.Compiler'
+
+repositories {
+    jcenter()
+}
+
+buildscript {
+    repositories.jcenter()
+    dependencies {
+        classpath 'org.jastadd:jastaddgradle:1.13.3'
+    }
+}
+
+dependencies {
+    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.0'
+    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.0'
+    testCompile 'org.assertj:assertj-core:3.12.1'
+    compile 'com.fasterxml.jackson.core:jackson-core:2.9.8'
+    compile 'com.fasterxml.jackson.core:jackson-databind:2.9.8'
+    compile 'org.jastadd:jastadd:2.3.4'
+    runtime 'org.jastadd:jastadd:2.3.4'
+    compile group: 'net.sf.beaver', name: 'beaver-rt', version: '0.9.11'
+}
+
+sourceSets {
+    main {
+        java.srcDir "src/gen/java"
+        java.srcDir "buildSrc/gen/java"
+    }
+}
+
+test {
+    useJUnitPlatform()
+
+    maxHeapSize = '1G'
+}
+
+jar {
+    manifest {
+        attributes "Main-Class": 'org.jastadd.relast.compiler.Compiler'
+    }
+
+    from {
+        configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
+    }
+}
+
+task relast(type: JavaExec) {
+    group = 'Build'
+    main = "-jar"
+
+    doFirst {
+        delete "src/gen/jastadd/*.ast"
+        delete "src/gen/jastadd/RelAst.jadd"
+        delete "src/gen/jastadd/RelAstRefResolver.jadd"
+        delete "src/gen/jastadd/RelAstResolverStubs.jrag"
+        mkdir  "src/gen/jastadd/"
+    }
+
+    args = [
+            "../libs/relast.jar",
+            "./src/main/jastadd/RelAst.relast",
+            "--listClass=java.util.ArrayList",
+            "--jastAddList=JastAddList",
+            "--useJastAddNames",
+            "--file",
+            "--resolverHelper",
+            "--grammarName=./src/gen/jastadd/RelAst"
+    ]
+
+    inputs.files file("../libs/relast.jar"),
+            file("src/main/jastadd/RelAst.relast")
+    outputs.files file("./src/gen/jastadd/RelAst.ast"),
+            file("src/gen/jastadd/RelAst.jadd"),
+            file("src/gen/jastadd/RelAstRefResolver.jadd"),
+            file('src/gen/jastadd/RelAstResolverStubs.jrag')
+}
+
+jastadd {
+    configureModuleBuild()
+    modules {
+        //noinspection GroovyAssignabilityCheck
+        module("RelAst") {
+
+            java {
+                basedir "."
+                include "src/main/**/*.java"
+                include "src/gen/**/*.java"
+            }
+
+            jastadd {
+                basedir "."
+                include "src/main/jastadd/**/*.ast"
+                include "src/main/jastadd/**/*.jadd"
+                include "src/main/jastadd/**/*.jrag"
+                include "src/gen/jastadd/**/*.ast"
+                include "src/gen/jastadd/**/*.jadd"
+                include "src/gen/jastadd/**/*.jrag"
+            }
+
+            scanner {
+                include "src/main/jastadd/RelAst.flex"
+            }
+
+            parser {
+                include "src/main/jastadd/Preamble.parser"
+                include "src/main/jastadd/RelAst.parser"
+            }
+        }
+    }
+
+    cleanGen.doFirst {
+        delete "src/gen/java/org"
+        delete "src/gen-res/BuildInfo.properties"
+    }
+
+    preprocessParser.doFirst {
+
+        args += ["--no-beaver-symbol"]
+
+    }
+
+    module = "RelAst"
+
+    astPackage = 'org.jastadd.relast.ast'
+
+    parser.name = 'RelAstParser'
+
+    genDir = 'src/gen/java'
+
+    buildInfoDir = 'src/gen-res'
+
+    scanner.genDir = "src/gen/java/org/jastadd/relast/scanner"
+    parser.genDir = "src/gen/java/org/jastadd/relast/parser"
+
+    jastaddOptions = ["--lineColumnNumbers", "--List=JastAddList", "--safeLazy", "--visitCheck=true", "--rewrite=cnta", "--cache=all"]
+}
+
+generateAst.dependsOn relast
diff --git a/relast.preprocessor/src/main/jastadd/Analysis.jrag b/relast.preprocessor/src/main/jastadd/Analysis.jrag
new file mode 100644
index 0000000000000000000000000000000000000000..118dd2373e1fb883c829cb8867cb3d898f574e07
--- /dev/null
+++ b/relast.preprocessor/src/main/jastadd/Analysis.jrag
@@ -0,0 +1,3 @@
+aspect Analysis {
+
+}
diff --git a/ros2rag.base/src/main/jastadd/DumpTree.jrag b/relast.preprocessor/src/main/jastadd/DumpTree.jrag
similarity index 100%
rename from ros2rag.base/src/main/jastadd/DumpTree.jrag
rename to relast.preprocessor/src/main/jastadd/DumpTree.jrag
diff --git a/relast.preprocessor/src/main/jastadd/NameResolution.jrag b/relast.preprocessor/src/main/jastadd/NameResolution.jrag
new file mode 100644
index 0000000000000000000000000000000000000000..e96856404fce9a3a5805df5289da7b8ba6a2ab28
--- /dev/null
+++ b/relast.preprocessor/src/main/jastadd/NameResolution.jrag
@@ -0,0 +1,30 @@
+aspect NameResolution {
+
+  refine RefResolverStubs eq ASTNode.globallyResolveTypeDeclByToken(String id) = program().resolveTypeDecl(id);
+
+  syn TypeDecl Program.resolveTypeDecl(String name) {
+    for (TypeDecl decl : typeDecls()) {
+      if (decl.getName().equals(name)) {
+        return decl;
+      }
+    }
+    throw new RuntimeException("TypeDecl " + name + " could not be resolved.");
+  }
+
+  refine RefResolverStubs eq ASTNode.globallyResolveTokenComponentByToken(String id) {
+    // return a TokenComponent. id is of the form 'type_name + "." + token_name'
+    int dotIndex = id.indexOf(".");
+    String typeName = id.substring(0, dotIndex);
+    String tokenName = id.substring(dotIndex + 1);
+    TypeDecl type = program().resolveTypeDecl(typeName);
+    // iterate over components and find the matching tokenComponent
+    for (Component comp : type.getComponentList()) {
+      if (comp.isTokenComponent() && comp.getName().equals(tokenName)) {
+        return comp.asTokenComponent();
+      }
+    }
+    System.err.println("Could not resolve TokenComponent '" + id + "'.");
+    return null;
+  }
+
+}
diff --git a/relast.preprocessor/src/main/jastadd/Navigation.jrag b/relast.preprocessor/src/main/jastadd/Navigation.jrag
new file mode 100644
index 0000000000000000000000000000000000000000..37078ae3adc49d4f7560748daca2f01a608fc0fc
--- /dev/null
+++ b/relast.preprocessor/src/main/jastadd/Navigation.jrag
@@ -0,0 +1,34 @@
+aspect Navigation {
+
+  // --- program ---
+  inh Program ASTNode.program();
+  eq Program.getChild().program() = this;
+
+  // --- typeDecls ---
+  coll java.util.Set<TypeDecl> Program.typeDecls() [new java.util.HashSet<>()] root Program;
+  TypeDecl contributes this
+    to Program.typeDecls()
+    for program();
+
+  // --- relations ---
+  coll java.util.Set<Relation> Program.relations() [new java.util.HashSet<>()] root Program;
+  Relation contributes this
+    to Program.relations()
+    for program();
+
+  // --- containingTypeDecl ---
+  inh TypeDecl Component.containingTypeDecl();
+  eq TypeDecl.getChild().containingTypeDecl() = this;
+
+  // --- containedFile ---
+  inh GrammarFile ASTNode.containedFile();
+  eq GrammarFile.getChild().containedFile() = this;
+
+  // --- isTokenComponent ---
+  syn boolean Component.isTokenComponent() = false;
+  eq TokenComponent.isTokenComponent() = true;
+
+  // --- asTokenComponent ---
+  syn TokenComponent Component.asTokenComponent() = null;
+  eq TokenComponent.asTokenComponent() = this;
+}
diff --git a/relast.preprocessor/src/main/jastadd/Preamble.parser b/relast.preprocessor/src/main/jastadd/Preamble.parser
new file mode 100644
index 0000000000000000000000000000000000000000..0b3c902916851e76b50a47bc9bfbc18ec721a9d9
--- /dev/null
+++ b/relast.preprocessor/src/main/jastadd/Preamble.parser
@@ -0,0 +1,6 @@
+%header {:
+package org.jastadd.relast.parser;
+import org.jastadd.relast.ast.*;
+:};
+
+%goal goal;
diff --git a/relast.preprocessor/src/main/jastadd/RelAst.flex b/relast.preprocessor/src/main/jastadd/RelAst.flex
new file mode 100644
index 0000000000000000000000000000000000000000..f9595e97eb97245cf1c500c46c135996a33b918c
--- /dev/null
+++ b/relast.preprocessor/src/main/jastadd/RelAst.flex
@@ -0,0 +1,72 @@
+package org.jastadd.relast.scanner;
+
+import org.jastadd.relast.parser.RelAstParser.Terminals;
+
+%%
+
+%public
+%final
+%class RelAstScanner
+%extends beaver.Scanner
+
+%type beaver.Symbol
+%function nextToken
+%yylexthrow beaver.Scanner.Exception
+%scanerror RelAstScanner.ScannerError
+
+%line
+%column
+%{
+  private StringBuilder stringLitSb = new StringBuilder();
+
+  private beaver.Symbol sym(short id) {
+    return new beaver.Symbol(id, yyline + 1, yycolumn + 1, yylength(), yytext());
+  }
+
+  private beaver.Symbol sym(short id, String text) {
+    return new beaver.Symbol(id, yyline + 1, yycolumn + 1, yylength(), text);
+  }
+
+
+  public static class ScannerError extends Error {
+    public ScannerError(String message) {
+      super(message);
+    }
+  }
+%}
+
+WhiteSpace = [ ] | \t | \f | \n | \r | \r\n
+TraditionalComment   = [/][*][^*]*[*]+([^*/][^*]*[*]+)*[/]
+EndOfLineComment = "//" [^\n\r]*
+Comment = {TraditionalComment} | {EndOfLineComment}
+
+ID = [a-zA-Z$_][a-zA-Z0-9$_]*
+
+%%
+{WhiteSpace} { /* ignore */ }
+{Comment}    { return sym(Terminals.COMMENT); }
+
+"abstract"   { return sym(Terminals.ABSTRACT); }
+"rel"        { return sym(Terminals.RELATION); }
+
+";"          { return sym(Terminals.SCOL); }
+":"          { return sym(Terminals.COL); }
+"::="        { return sym(Terminals.ASSIGN); }
+"*"          { return sym(Terminals.STAR); }
+"."          { return sym(Terminals.DOT); }
+","          { return sym(Terminals.COMMA); }
+"<"          { return sym(Terminals.LT); }
+">"          { return sym(Terminals.GT); }
+"["          { return sym(Terminals.LBRACKET); }
+"]"          { return sym(Terminals.RBRACKET); }
+"/"          { return sym(Terminals.SLASH); }
+"?"          { return sym(Terminals.QUESTION_MARK); }
+"->"         { return sym(Terminals.RIGHT); }
+"<-"         { return sym(Terminals.LEFT); }
+"<->"        { return sym(Terminals.BIDIRECTIONAL); }
+
+// ID
+{ID}         { return sym(Terminals.ID); }
+<<EOF>>      { return sym(Terminals.EOF); }
+
+[^]            { throw new ScannerError((yyline+1) +"," + (yycolumn+1) + ": Illegal character <"+yytext()+">"); }
diff --git a/ros2rag.base/src/main/jastadd/RelAst.parser b/relast.preprocessor/src/main/jastadd/RelAst.parser
similarity index 100%
rename from ros2rag.base/src/main/jastadd/RelAst.parser
rename to relast.preprocessor/src/main/jastadd/RelAst.parser
diff --git a/ros2rag.base/src/main/jastadd/RelAst.relast b/relast.preprocessor/src/main/jastadd/RelAst.relast
similarity index 100%
rename from ros2rag.base/src/main/jastadd/RelAst.relast
rename to relast.preprocessor/src/main/jastadd/RelAst.relast
diff --git a/ros2rag.base/src/main/jastadd/backend/AbstractGrammar.jadd b/relast.preprocessor/src/main/jastadd/backend/AbstractGrammar.jadd
similarity index 95%
rename from ros2rag.base/src/main/jastadd/backend/AbstractGrammar.jadd
rename to relast.preprocessor/src/main/jastadd/backend/AbstractGrammar.jadd
index 606be6ea329fe361b1784e88489f6e34ac81ed1b..870132c2cf4fa0fa6e7ec75759ec39f72872fa3c 100644
--- a/ros2rag.base/src/main/jastadd/backend/AbstractGrammar.jadd
+++ b/relast.preprocessor/src/main/jastadd/backend/AbstractGrammar.jadd
@@ -17,14 +17,13 @@ aspect BackendAbstractGrammar {
     }
   }
 
-  public void GrammarFile.generateAbstractGrammar(StringBuilder b) {
-    b.append("// Grammar for file ").append(getFileName()).append("\n");
-    super.generateAbstractGrammar(b);
-    b.append("\n");
+  public String GrammarFile.generateAbstractGrammar() {
+    StringBuilder sb = new StringBuilder();
+    generateAbstractGrammar(sb);
+    return sb.toString();
   }
 
   public void Grammar.generateAbstractGrammar(StringBuilder b) {
-
     for (Declaration decl : getDeclarationList()) {
       decl.generateAbstractGrammar(b);
     }
diff --git a/relast.preprocessor/src/main/java/org/jastadd/relast/compiler/Compiler.java b/relast.preprocessor/src/main/java/org/jastadd/relast/compiler/Compiler.java
new file mode 100644
index 0000000000000000000000000000000000000000..efdda319429884637beac1ff15b655a91d4cd4c6
--- /dev/null
+++ b/relast.preprocessor/src/main/java/org/jastadd/relast/compiler/Compiler.java
@@ -0,0 +1,165 @@
+package org.jastadd.relast.compiler;
+
+import beaver.Parser;
+import org.jastadd.relast.ast.GrammarFile;
+import org.jastadd.relast.ast.Program;
+import org.jastadd.relast.compiler.options.CommandLine;
+import org.jastadd.relast.compiler.options.CommandLine.CommandLineException;
+import org.jastadd.relast.compiler.options.Option;
+import org.jastadd.relast.compiler.options.StringOption;
+import org.jastadd.relast.parser.RelAstParser;
+import org.jastadd.relast.scanner.RelAstScanner;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+public class Compiler {
+
+  private StringOption optionOutputDir;
+  private StringOption optionInputDir;
+
+  private ArrayList<Option<?>> options;
+  private CommandLine commandLine;
+
+  public Compiler() {
+    options = new ArrayList<>();
+    addOptions();
+  }
+
+  public static void main(String[] args) {
+    try {
+      new Compiler().run(args);
+    } catch (CommandLineException | CompilerException e) {
+      System.err.println(e.getMessage());
+      System.exit(-1);
+    }
+  }
+
+  public int run(String[] args) throws CommandLineException, CompilerException {
+    options = new ArrayList<>();
+    addOptions();
+    commandLine = new CommandLine(options);
+    commandLine.parse(args);
+
+    Path inputPath;
+    if (optionInputDir.isSet()) {
+      inputPath = Paths.get(optionInputDir.getValue());
+    } else {
+      inputPath = Paths.get(".");
+      printMessage("No input dir is set. Assuming current directory '" + inputPath.toAbsolutePath().toString() + "'.");
+    }
+
+    if (!inputPath.toFile().exists()) {
+      printMessage("Input path '" + inputPath.toAbsolutePath().toString() + "' does not exist. Exiting...");
+      System.exit(-1);
+    } else if (!inputPath.toFile().isDirectory()) {
+      printMessage("Input path '" + inputPath.toAbsolutePath().toString() + "' is not a directory. Exiting...");
+      System.exit(-1);
+    }
+
+    Path outputPath; // should not be used, but otherwise there is a compiler warning
+    if (optionOutputDir.isSet()) {
+      outputPath = Paths.get(optionOutputDir.getValue());
+    } else {
+      outputPath = Paths.get("./gen/");
+      printMessage("No output dir is set. Assuming '" + outputPath.toAbsolutePath().toString() + "'.");
+    }
+
+    if (outputPath.toFile().exists() && !outputPath.toFile().isDirectory()) {
+      printMessage("Output path '" + inputPath.toAbsolutePath().toString() + "' exists, but is not a directory. Exiting...");
+    }
+
+    printMessage("Running RelAST Preprocessor");
+
+    List<String> otherArgs = commandLine.getArguments();
+    if (!otherArgs.isEmpty()) {
+      printMessage("Unsupported arguments will be ignored: " + otherArgs);
+    }
+
+    Program program = parseProgram(inputPath);
+
+    printMessage("Writing output files");
+
+    for (GrammarFile grammarFile : program.getGrammarFileList()) {
+      // TODO decide and document what the file name should be, the full path or a simple name?
+      writeToFile(outputPath + grammarFile.getFileName() + "/Grammar.relast", grammarFile.generateAbstractGrammar());
+    }
+
+    writeToFile(outputPath + "/Grammar.relast", program.generateAbstractGrammar());
+    return 0;
+  }
+
+  private void printMessage(String message) {
+    System.out.println(message);
+  }
+
+  private void writeToFile(String filename, String str) throws CompilerException {
+    try {
+      PrintWriter writer = new PrintWriter(filename);
+      writer.print(str);
+      writer.close();
+    } catch (Exception e) {
+      throw new CompilerException("Could not write to file " + filename, e);
+    }
+  }
+
+  private void addOptions() {
+    optionOutputDir = addOption(new StringOption("outputDir", "target directory for the generated files."));
+    optionInputDir = addOption(new StringOption("inputDir", "input directory."));
+  }
+
+  private <OptionType extends Option<?>> OptionType addOption(OptionType option) {
+    options.add(option);
+    return option;
+  }
+
+  private Program parseProgram(Path inputPath) throws CompilerException {
+    Program program = new Program();
+
+    try (DirectoryStream<Path> stream = Files.newDirectoryStream(inputPath, "*.relast")) {
+      RelAstParser parser = new RelAstParser();
+      stream.forEach(path -> {
+        try (BufferedReader reader = Files.newBufferedReader(path)) {
+          RelAstScanner scanner = new RelAstScanner(reader);
+          GrammarFile inputGrammar = (GrammarFile) parser.parse(scanner);
+          inputGrammar.dumpTree(System.out);
+          program.addGrammarFile(inputGrammar);
+          inputGrammar.treeResolveAll();
+        } catch (IOException | Parser.Exception e) {
+          printMessage("Could not parse grammar file " + path);
+          e.printStackTrace();
+        }
+      });
+    } catch (IOException e) {
+      printMessage("Unable to iterate over input path '" + inputPath.toAbsolutePath().toString() + "'. Exiting...");
+      e.printStackTrace();
+      System.exit(-1);
+    }
+
+
+    return program;
+  }
+
+  protected int error(String message) {
+    System.err.println("Error: " + message);
+    System.err.println();
+    System.err.println("Usage: java -jar relast.jar [--option1] [--option2=value] ...  <filename1> <filename2> ... ");
+    System.err.println("Options:");
+    System.err.print(commandLine.printOptionHelp());
+    return 1;
+  }
+
+  public static class CompilerException extends Exception {
+    CompilerException(String message, Throwable cause) {
+      super(message, cause);
+    }
+  }
+}
+
diff --git a/relast.preprocessor/src/main/java/org/jastadd/relast/compiler/Utils.java b/relast.preprocessor/src/main/java/org/jastadd/relast/compiler/Utils.java
new file mode 100644
index 0000000000000000000000000000000000000000..8e2ef0c8bcb8266fe5789b2608f4a6f78c47548a
--- /dev/null
+++ b/relast.preprocessor/src/main/java/org/jastadd/relast/compiler/Utils.java
@@ -0,0 +1,16 @@
+package org.jastadd.relast.compiler;
+
+import java.util.*;
+import java.util.function.Predicate;
+
+import static java.util.stream.Collectors.toList;
+
+public class Utils {
+  public static <T> List<T> filterToList(Collection<T> collection, Predicate<T> predicate) {
+    return collection.stream().filter(predicate).collect(toList());
+  }
+
+  public static <T> Set<T> asSet(T... t) {
+    return new HashSet<T>(Arrays.asList(t));
+  }
+}
diff --git a/relast.preprocessor/src/main/java/org/jastadd/relast/compiler/options/CommandLine.java b/relast.preprocessor/src/main/java/org/jastadd/relast/compiler/options/CommandLine.java
new file mode 100644
index 0000000000000000000000000000000000000000..c3f02bf56d4fb4c779e489cfaa7fb150c6ea68cb
--- /dev/null
+++ b/relast.preprocessor/src/main/java/org/jastadd/relast/compiler/options/CommandLine.java
@@ -0,0 +1,97 @@
+package org.jastadd.relast.compiler.options;
+
+import java.util.*;
+
+public class CommandLine {
+  private final Collection<Option<?>> options;
+  private final Map<String, Option<?>> mapping;
+  private final List<String> arguments;
+
+  public CommandLine(Collection<Option<?>> options) {
+    this.options = options;
+    this.mapping = new HashMap<>();
+    for (Option<?> option : options) {
+      mapping.put(option.getName(), option);
+    }
+    this.arguments = new ArrayList<>();
+  }
+
+  public void parse(String[] args) throws CommandLineException {
+    int i = 0;
+    while (i < args.length) {
+      if (args[i].startsWith(Option.PREFIX)) {
+        int argumentIndex = args[i].indexOf("=");
+        String name;
+        String argument = null;
+        if (argumentIndex > 0) {
+          name = args[i].substring(2, argumentIndex);
+          argument = args[i].substring(argumentIndex + 1);
+        } else {
+          name = args[i].substring(2);
+        }
+        Option<?> option = mapping.get(name);
+        if (option == null) {
+          throw new CommandLineException("Option " + Option.PREFIX + name + " not found");
+        }
+        match(option, argument);
+      } else {
+        arguments.add(args[i]);
+      }
+      i++;
+    }
+  }
+
+  public void match(Option<?> option, String argument) throws CommandLineException {
+    try {
+      switch (option.hasArgument()) {
+        case NO:
+          if (argument == null) {
+            option.match(null);
+          } else {
+            throw new CommandLineException("Option " + option + " is not allowed to have an argument");
+          }
+          break;
+        case OPTIONAL:
+          option.match(argument);
+          break;
+        case YES:
+          if (argument != null) {
+            option.match(argument);
+          } else {
+            throw new CommandLineException("Option " + option + " requires an argument");
+          }
+          break;
+      }
+    } catch (Option.IllegalMatchException e) {
+      throw new CommandLineException("Invalid value for option " + option + ": " + e.getMessage());
+    }
+  }
+
+  public List<String> getArguments() {
+    return arguments;
+  }
+
+  public String printOptionHelp() {
+    StringBuilder sb = new StringBuilder();
+    int longestOption = 0;
+    for (Option<?> option : options) {
+      if (longestOption < option.getName().length()) {
+        longestOption = option.getName().length();
+      }
+    }
+    for (Option<?> option : new TreeSet<>(options)) {
+      String s = String.format("  %s%-" + (longestOption + 6) + "s %s%n",
+          Option.PREFIX, option.getName(), option.getDescription());
+      sb.append(s);
+    }
+    return sb.toString();
+  }
+
+  public static class CommandLineException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    public CommandLineException(String message) {
+      super(message);
+    }
+  }
+}
diff --git a/relast.preprocessor/src/main/java/org/jastadd/relast/compiler/options/EnumOption.java b/relast.preprocessor/src/main/java/org/jastadd/relast/compiler/options/EnumOption.java
new file mode 100644
index 0000000000000000000000000000000000000000..92b5149a9c3d36cf2053f0a7d0a2cadfd52129ec
--- /dev/null
+++ b/relast.preprocessor/src/main/java/org/jastadd/relast/compiler/options/EnumOption.java
@@ -0,0 +1,60 @@
+package org.jastadd.relast.compiler.options;
+
+import java.util.Collection;
+import java.util.TreeSet;
+
+public class EnumOption extends Option<String> {
+  private final TreeSet<String> allowedValues;
+  private final String defaultValue;
+  private String value;
+  private boolean isSet;
+
+  public EnumOption(String name, String description, Collection<String> allowedValues, String defaultValue) {
+    super(name, description);
+    this.allowedValues = new TreeSet<>(allowedValues);
+    this.defaultValue = defaultValue;
+    this.value = defaultValue;
+    this.isSet = false;
+  }
+
+  public boolean addAllowedValue(String allowedValue) {
+    return allowedValues.add(allowedValue);
+  }
+
+  @Override
+  public String getValue() {
+    return value;
+  }
+
+  @Override
+  public Option.HasArgument hasArgument() {
+    return Option.HasArgument.OPTIONAL;
+  }
+
+  @Override
+  public void match(String argument) throws IllegalMatchException {
+    if (argument == null) {
+      isSet = true;
+      value = defaultValue;
+    } else if (allowedValues.contains(argument)) {
+      isSet = true;
+      value = argument;
+    } else {
+      throw new IllegalMatchException(argument
+          + " is not allowed, allowed values are " + allowedValues);
+    }
+  }
+
+  @Override
+  public boolean isSet() {
+    return isSet;
+  }
+
+  @Override
+  public String getDescription() {
+    String allowedValuesStr = allowedValues.toString();
+    allowedValuesStr = allowedValuesStr.substring(1);
+    allowedValuesStr = allowedValuesStr.substring(0, allowedValuesStr.length() - 1);
+    return super.getDescription() + " (allowed values: " + allowedValuesStr + ")";
+  }
+}
diff --git a/relast.preprocessor/src/main/java/org/jastadd/relast/compiler/options/FlagOption.java b/relast.preprocessor/src/main/java/org/jastadd/relast/compiler/options/FlagOption.java
new file mode 100644
index 0000000000000000000000000000000000000000..80c2f0cb0313cc05ca9b05795fc3acc3ebd5d849
--- /dev/null
+++ b/relast.preprocessor/src/main/java/org/jastadd/relast/compiler/options/FlagOption.java
@@ -0,0 +1,30 @@
+package org.jastadd.relast.compiler.options;
+
+public class FlagOption extends Option<Boolean> {
+  private boolean value;
+
+  public FlagOption(String name, String description) {
+    super(name, description);
+    value = false;
+  }
+
+  @Override
+  public Boolean getValue() {
+    return value;
+  }
+
+  @Override
+  public Option.HasArgument hasArgument() {
+    return Option.HasArgument.NO;
+  }
+
+  @Override
+  public void match(String string) throws IllegalMatchException {
+    value = true;
+  }
+
+  @Override
+  public boolean isSet() {
+    return getValue();
+  }
+}
diff --git a/relast.preprocessor/src/main/java/org/jastadd/relast/compiler/options/Option.java b/relast.preprocessor/src/main/java/org/jastadd/relast/compiler/options/Option.java
new file mode 100644
index 0000000000000000000000000000000000000000..e20354e973e5ec11568041291242c2699ec0ab4d
--- /dev/null
+++ b/relast.preprocessor/src/main/java/org/jastadd/relast/compiler/options/Option.java
@@ -0,0 +1,60 @@
+package org.jastadd.relast.compiler.options;
+
+abstract public class Option<ValueType> implements Comparable<Option<?>> {
+  public final static String PREFIX = "--";
+  private final String name;
+  private final String description;
+
+  public Option(String name, String description) {
+    this.name = name;
+    this.description = description;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+
+  @Override
+  public int compareTo(Option<?> o) {
+    return name.compareTo(o.name);
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (other instanceof Option) {
+      return compareTo((Option<?>) other) == 0;
+    }
+    return false;
+  }
+
+  @Override
+  public String toString() {
+    return PREFIX + name;
+  }
+
+  abstract public boolean isSet();
+
+  abstract public ValueType getValue();
+
+  abstract public HasArgument hasArgument();
+
+  abstract public void match(String input) throws IllegalMatchException;
+
+  public enum HasArgument {
+    NO,
+    OPTIONAL,
+    YES
+  }
+
+  public static class IllegalMatchException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    public IllegalMatchException(String message) {
+      super(message);
+    }
+  }
+}
diff --git a/relast.preprocessor/src/main/java/org/jastadd/relast/compiler/options/StringOption.java b/relast.preprocessor/src/main/java/org/jastadd/relast/compiler/options/StringOption.java
new file mode 100644
index 0000000000000000000000000000000000000000..a1298aeaadc259e5ecca3f1ffbade77de9a8315b
--- /dev/null
+++ b/relast.preprocessor/src/main/java/org/jastadd/relast/compiler/options/StringOption.java
@@ -0,0 +1,37 @@
+package org.jastadd.relast.compiler.options;
+
+public class StringOption extends Option<String> {
+  private String value;
+  private boolean isSet;
+
+  public StringOption(String name, String description) {
+    this(name, description, "");
+  }
+
+  public StringOption(String name, String description, String defaultValue) {
+    super(name, description);
+    value = defaultValue;
+    isSet = false;
+  }
+
+  @Override
+  public String getValue() {
+    return value;
+  }
+
+  @Override
+  public Option.HasArgument hasArgument() {
+    return Option.HasArgument.YES;
+  }
+
+  @Override
+  public void match(String value) {
+    this.value = value;
+    isSet = true;
+  }
+
+  @Override
+  public boolean isSet() {
+    return isSet;
+  }
+}
diff --git a/relast.preprocessor/src/main/resources/log4j2.xml b/relast.preprocessor/src/main/resources/log4j2.xml
new file mode 100644
index 0000000000000000000000000000000000000000..98cfd73c75df58d8598521bc10b043e214ec4ad8
--- /dev/null
+++ b/relast.preprocessor/src/main/resources/log4j2.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="INFO">
+    <Appenders>
+        <Console name="Console" target="SYSTEM_OUT">
+            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
+        </Console>
+    </Appenders>
+    <Loggers>
+        <Root level="info">
+            <AppenderRef ref="Console"/>
+        </Root>
+    </Loggers>
+</Configuration>
\ No newline at end of file
diff --git a/relast.preprocessor/src/test/java/org/jastadd/ros2rag/tests/RelAstTest.java b/relast.preprocessor/src/test/java/org/jastadd/ros2rag/tests/RelAstTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..85fc911d779fef34f56e6cb0802a55072e9d7fdd
--- /dev/null
+++ b/relast.preprocessor/src/test/java/org/jastadd/ros2rag/tests/RelAstTest.java
@@ -0,0 +1,43 @@
+package org.jastadd.ros2rag.tests;
+
+import org.jastadd.relast.compiler.Compiler;
+import org.jastadd.relast.compiler.options.CommandLine;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.nio.file.Paths;
+import java.util.Objects;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class RelAstTest {
+
+  void transform(String inputDir, String outputDir) throws CommandLine.CommandLineException, Compiler.CompilerException {
+
+    System.out.println("Running test in directory '" + Paths.get(".").toAbsolutePath() + "'.");
+    assertTrue(Paths.get(inputDir).toFile().exists(), "input directory does not exist");
+    assertTrue(Paths.get(inputDir).toFile().isDirectory(), "input directory is not a directory");
+
+    File outputDirFile = Paths.get(outputDir).toFile();
+    if (outputDirFile.exists()) {
+      assertTrue(outputDirFile.isDirectory());
+      if (Objects.requireNonNull(outputDirFile.list(), "Could not read output directory").length != 0) {
+        System.out.println("output directory is not empty!");
+      }
+    } else {
+      assertTrue(outputDirFile.mkdir());
+    }
+
+    String[] args = {
+        "--outputDir=" + outputDir,
+        "--inputDir=" + inputDir
+    };
+
+    new Compiler().run(args);
+  }
+
+  @Test
+  void transformMinimalExample() throws CommandLine.CommandLineException, Compiler.CompilerException {
+    transform("src/test/resources/in", "src/test/resources/out");
+  }
+}
diff --git a/relast.preprocessor/src/test/resources/in/Example.relast b/relast.preprocessor/src/test/resources/in/Example.relast
new file mode 100644
index 0000000000000000000000000000000000000000..aa17ad814128cf155d844e0f36f1114b1b1afa33
--- /dev/null
+++ b/relast.preprocessor/src/test/resources/in/Example.relast
@@ -0,0 +1,13 @@
+Model ::= RobotArm ZoneModel ;
+
+ZoneModel ::= <Size:IntPosition> SafetyZone:Zone* ;
+
+Zone ::= Coordinate* ;
+
+RobotArm ::= Joint* EndEffector <_AttributeTestSource:int> /<_AppropriateSpeed:double>/ ; // normally this would be: <AttributeTestSource:int> ;
+
+Joint ::= <Name> <CurrentPosition:IntPosition> ;  // normally this would be: <CurrentPosition:IntPosition>
+
+EndEffector : Joint;
+
+Coordinate ::= <Position:IntPosition> ;
diff --git a/ros2rag.base/build.gradle b/ros2rag.base/build.gradle
index 5cd8f595c7f2db045459e3e1ba4eda15fbbb58fc..dcf62a1dd96ed1952a9bc9d9e70ad0b66ebe83ad 100644
--- a/ros2rag.base/build.gradle
+++ b/ros2rag.base/build.gradle
@@ -67,7 +67,7 @@ task relast(type: JavaExec) {
 
     args = [
             "../libs/relast.jar",
-            "./src/main/jastadd/RelAst.relast",
+            "../relast.preprocessor/src/main/jastadd/RelAst.relast",
             "./src/main/jastadd/Ros2Rag.relast",
             "--listClass=java.util.ArrayList",
             "--jastAddList=JastAddList",
@@ -78,7 +78,7 @@ task relast(type: JavaExec) {
     ]
 
     inputs.files file("../libs/relast.jar"),
-            file("src/main/jastadd/RelAST.relast"),
+            file("../relast.preprocessor/src/main/jastadd/RelAST.relast"),
             file("src/main/jastadd/Ros2Rag.relast")
     outputs.files file("./src/gen/jastadd/Ros2Rag.ast"),
             file("src/gen/jastadd/Ros2Rag.jadd"),
@@ -93,19 +93,24 @@ jastadd {
         module("Ros2Rag") {
 
             java {
-                basedir "."
-                include "src/main/**/*.java"
-                include "src/gen/**/*.java"
+                basedir ".."
+                include "relast.preprocessor/main/**/*.java"
+                include "relast.preprocessor/gen/**/*.java"
+                include "ros2rag.base/src/main/**/*.java"
+                include "ros2rag.base/src/gen/**/*.java"
             }
 
             jastadd {
-                basedir "."
-                include "src/main/jastadd/**/*.ast"
-                include "src/main/jastadd/**/*.jadd"
-                include "src/main/jastadd/**/*.jrag"
-                include "src/gen/jastadd/**/*.ast"
-                include "src/gen/jastadd/**/*.jadd"
-                include "src/gen/jastadd/**/*.jrag"
+                basedir ".."
+                include "relast.preprocessor/src/main/jastadd/**/*.ast"
+                include "relast.preprocessor/src/main/jastadd/**/*.jadd"
+                include "relast.preprocessor/src/main/jastadd/**/*.jrag"
+                include "ros2rag.base/src/main/jastadd/**/*.ast"
+                include "ros2rag.base/src/main/jastadd/**/*.jadd"
+                include "ros2rag.base/src/main/jastadd/**/*.jrag"
+                include "ros2rag.base/src/gen/jastadd/**/*.ast"
+                include "ros2rag.base/src/gen/jastadd/**/*.jadd"
+                include "ros2rag.base/src/gen/jastadd/**/*.jrag"
             }
 
             scanner {
@@ -113,9 +118,10 @@ jastadd {
             }
 
             parser {
-                include "src/main/jastadd/Preamble.parser"
-                include "src/main/jastadd/RelAst.parser"
-                include "src/main/jastadd/Ros2Rag.parser"
+                basedir ".."
+                include "ros2rag.base/src/main/jastadd/Preamble.parser"
+                include "relast.preprocessor/src/main/jastadd/RelAst.parser"
+                include "ros2rag.base/src/main/jastadd/Ros2Rag.parser"
             }
         }
     }
diff --git a/ros2rag.base/src/main/jastadd/NameResolution.jrag b/ros2rag.base/src/main/jastadd/NameResolution.jrag
index 72b8522c24776e7b0ae57a5e03c998b0e3a92685..17e396198446da7bfeaeb642bad2d0634a0fc220 100644
--- a/ros2rag.base/src/main/jastadd/NameResolution.jrag
+++ b/ros2rag.base/src/main/jastadd/NameResolution.jrag
@@ -1,32 +1,5 @@
 aspect NameResolution {
 
-  refine RefResolverStubs eq ASTNode.globallyResolveTypeDeclByToken(String id) = program().resolveTypeDecl(id);
-
-  syn TypeDecl Program.resolveTypeDecl(String name) {
-    for (TypeDecl decl : typeDecls()) {
-      if (decl.getName().equals(name)) {
-        return decl;
-      }
-    }
-    throw new RuntimeException("TypeDecl " + name + " could not be resolved.");
-  }
-
-  refine RefResolverStubs eq ASTNode.globallyResolveTokenComponentByToken(String id) {
-    // return a TokenComponent. id is of the form 'type_name + "." + token_name'
-    int dotIndex = id.indexOf(".");
-    String typeName = id.substring(0, dotIndex);
-    String tokenName = id.substring(dotIndex + 1);
-    TypeDecl type = program().resolveTypeDecl(typeName);
-    // iterate over components and find the matching tokenComponent
-    for (Component comp : type.getComponentList()) {
-      if (comp.isTokenComponent() && comp.getName().equals(tokenName)) {
-        return comp.asTokenComponent();
-      }
-    }
-    System.err.println("Could not resolve TokenComponent '" + id + "'.");
-    return null;
-  }
-
   refine RefResolverStubs eq UpdateDefinition.resolveMappingByToken(String id) {
     // return a MappingDefinition
     for (MappingDefinition mappingDefinition : ros2rag().getMappingDefinitionList()) {
diff --git a/ros2rag.base/src/main/jastadd/Navigation.jrag b/ros2rag.base/src/main/jastadd/Navigation.jrag
index 4b233c249b6ec7441d70f2629d3179f138f49818..250ea9b2d86a9898ffed4957092e73b64b05d98a 100644
--- a/ros2rag.base/src/main/jastadd/Navigation.jrag
+++ b/ros2rag.base/src/main/jastadd/Navigation.jrag
@@ -1,46 +1,10 @@
 aspect Navigation {
 
   // --- program ---
-  inh Program ASTNode.program();
-  eq Program.getChild().program() = this;
   eq Ros2Rag.getChild().program() = getProgram();
 
   // --- ros2rag
   inh Ros2Rag ASTNode.ros2rag();
   eq Ros2Rag.getChild().ros2rag() = this;
 
-  // --- typeDecls ---
-  coll java.util.Set<TypeDecl> Program.typeDecls() [new java.util.HashSet<>()] root Program;
-  TypeDecl contributes this
-    to Program.typeDecls()
-    for program();
-
-  // --- relations ---
-  coll java.util.Set<Relation> Program.relations() [new java.util.HashSet<>()] root Program;
-  Relation contributes this
-    to Program.relations()
-    for program();
-
-  // --- containingTypeDecl ---
-  inh TypeDecl Component.containingTypeDecl();
-  eq TypeDecl.getChild().containingTypeDecl() = this;
-
-//  syn boolean RelationComponent.multiplicityOne() = false;
-//  eq OneRelationComponent.multiplicityOne() = true;
-//  syn boolean RelationComponent.multiplicityOpt() = false;
-//  eq OptionalRelationComponent.multiplicityOpt() = true;
-//  syn boolean RelationComponent.multiplicityMany() = false;
-//  eq ManyRelationComponent.multiplicityMany() = true;
-
-  // --- containedFile ---
-  inh GrammarFile ASTNode.containedFile();
-  eq GrammarFile.getChild().containedFile() = this;
-
-  // --- isTokenComponent ---
-  syn boolean Component.isTokenComponent() = false;
-  eq TokenComponent.isTokenComponent() = true;
-
-  // --- asTokenComponent ---
-  syn TokenComponent Component.asTokenComponent() = null;
-  eq TokenComponent.asTokenComponent() = this;
 }
diff --git a/settings.gradle b/settings.gradle
index 22db44e3b12b9129e0206f5e6a313da08e49e368..558a492cc410a682b33c2a452474c8e9eade2bed 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -4,3 +4,4 @@ include 'ros2rag.base'
 include 'ros2rag.example'
 include 'ros2rag.senderstub'
 include 'ros2rag.receiverstub'
+include 'relast.preprocessor'