Over a million developers have joined DZone.

Building a Compiler: Model-to-Model Transformations

DSLs have become commonplace tools today. Many of them are developed as internal DSLs in languages like Ruby, but this can be too unsafe for open environments. If you need to develop an external DSL, you'll need to build a compiler or interpreter.

· Java Zone

What every Java engineer should know about microservices: Reactive Microservices Architecture.  Brought to you in partnership with Lightbend.

Most of the work done in tools supporting a language consists in manipulating the AST. In this post, we are going to see how to perform transformation and processing on the Abstract Syntax Tree through model-to-model transformations. These techniques will be useful to perform operations like:

  • Validation: finding errors in the AST.
  • Remove syntax sugar: by transforming the AST into equivalent but more explicit forms.
  • Fill symbol tables and resolve symbols.

All these techniques will be based on generic ways to navigate and change the AST. Let’s see how.

Series On Building Your Own Language

Previous posts:

  1. Building a lexer
  2. Building a parser
  3. Creating an editor with syntax highlighting
  4. Build an editor with autocompletion
  5. Mapping the parse tree to the abstract syntax tree

Code is available on GitHub under the tag 06_transformations

The Operations We Need

We will need to:

  • Process the AST: we want to do some operation to extract information from the AST
  • Transform the AST: we want to transform the single nodes of the AST

Implement the Transformations Manually

We could implement the transformation manually on each single class of the AST. For example, this is how we could implement these methods on a SumExpression

fun SumExpression.process(operation: (Node) -> Unit) {

fun SumExpression.transform(operation: (Node) -> Node) : Node {
    val newLeft = this.left.transform(operation) as Expression
    val newRight = this.right.transform(operation) as Expression
    return operation(SumExpression(newLeft, newRight))

We would have just to repeat it for each single class of our metamodel. Quite boring, eh?

Transformations Using Reflection

The alternative is to use reflection. In this way we can specify these operations for Node and they will work for every class of every metamodel (so basically, for every language we are going to work on).

fun Node.process(operation: (Node) -> Unit) {
    this.javaClass.kotlin.memberProperties.forEach { p ->
        val v = p.get(this)
        when (v) {
            is Node -> v.process(operation)
            is Collection<*> -> v.forEach { if (it is Node) it.process(operation) }

fun Node.transform(operation: (Node) -> Node) : Node {
    val changes = HashMap<String, Any>()
    this.javaClass.kotlin.memberProperties.forEach { p ->
        val v = p.get(this)
        when (v) {
            is Node -> {
                val newValue = v.transform(operation)
                if (newValue != v) changes[p.name] = newValue
            is Collection<*> -> {
                val newValue = v.map { if (it is Node) it.transform(operation) else it }
                if (newValue != v) changes[p.name] = newValue
    var instanceToTransform = this
    if (!changes.isEmpty()) {
        val constructor = this.javaClass.kotlin.primaryConstructor!!
        val params = HashMap<KParameter, Any?>()
        constructor.parameters.forEach { param ->
            if (changes.containsKey(param.name)) {
                params[param] = changes[param.name]
            } else {
                params[param] = this.javaClass.kotlin.memberProperties.find { param.name == it.name }!!.get(this)
        instanceToTransform = constructor.callBy(params)
    return operation(instanceToTransform)

Testing the Transformations

It all looks nice and well but the question is: does it actually work? Let’s try it.

In this little test we will transform references to a variable into references to a variable B.

class ModelTest {

    @test fun transformVarName() {
        val startTree = SandyFile(listOf(
                VarDeclaration("A", IntLit("10")),
                Assignment("A", IntLit("11")),
        val expectedTransformedTree = SandyFile(listOf(
                VarDeclaration("B", IntLit("10")),
                Assignment("B", IntLit("11")),
        assertEquals(expectedTransformedTree, startTree.transform {
            when (it) {
                is VarDeclaration -> VarDeclaration("B", it.value)
                is VarReference -> VarReference("B")
                is Assignment -> Assignment("B", it.value)
                else -> it



Another building block for the future. In the next steps we will see how to use this, for example to implement validation.

Microservices for Java, explained. Revitalize your legacy systems (and your career) with Reactive Microservices Architecture, a free O'Reilly book. Brought to you in partnership with Lightbend.

domain specific language,compiler

Published at DZone with permission of Federico Tomassetti, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}