Skip to content

Adding a transformation

Finally, we will define a transformation for our language and add a task, command-tasks, command, and menu item for it. Open the main Stratego file helloworld/src/main.str. Stratego. is a meta-language for defining term (AST) transformations through rewrite rules. We will add a silly transformation that replaces all instances of World() with Hello().

Add the following code to the end of the Stratego file:

rules

  replace-world: Hello() -> Hello()
  replace-world: World() -> Hello()
  replace-worlds = topdown(try(replace-world))

The replace-world rule passes Hello() terms but rewrites World() terms to Hello(). The replace-worlds strategy tries to apply replace-world in a top-down manner over the entire AST.

src/main.str full contents
module main

    imports

      statixruntime
      statix/api
      injections/-
      signatures/-

    rules // Analysis

      pre-analyze  = explicate-injections-helloworld-Start
      post-analyze = implicate-injections-helloworld-Start

      editor-analyze = stx-editor-analyze(pre-analyze, post-analyze|"main", "programOk")

    rules

      replace-world: Hello() -> Hello()
      replace-world: World() -> Hello()
      replace-worlds = topdown(try(replace-world))
    ```


Now we add a task and command-task for this transformation.
We define two separate tasks to keep separate

1. the act of transforming the program, and
2. feeding back the result of that transformation to the user that executes a command.

This practice later allows us to reuse the first task in a different task if we need to.
Right-click the `mb.helloworld.task` package and create the `HelloWorldReplaceWorlds` class and replace the entire Java file with:

```{ .java .annotate linenums="1" }
package mb.helloworld.task;

import mb.helloworld.HelloWorldClassLoaderResources;
import mb.helloworld.HelloWorldScope;
import mb.pie.api.ExecContext;
import mb.pie.api.stamp.resource.ResourceStampers;
import mb.stratego.pie.AstStrategoTransformTaskDef;

import javax.inject.Inject;
import java.io.IOException;

@HelloWorldScope
public class HelloWorldReplaceWorlds extends AstStrategoTransformTaskDef {
  private final HelloWorldClassLoaderResources classloaderResources;

  @Inject
  public HelloWorldReplaceWorlds( // 1
    HelloWorldClassLoaderResources classloaderResources,
    HelloWorldGetStrategoRuntimeProvider getStrategoRuntimeProvider
  ) {
    super(getStrategoRuntimeProvider, "replace-worlds"); // 2
    this.classloaderResources = classloaderResources;
  }

  @Override public String getId() { // 3
    return getClass().getName();
  }

  @Override protected void createDependencies(ExecContext context) throws IOException { // 4
    context.require(classloaderResources.tryGetAsLocalResource(getClass()), ResourceStampers.hashFile());
  }
}

This task extends AstStrategoTransformTaskDef which is a convenient abstract class for creating tasks that run Stratego transformations by implementing a constructor and a couple of methods:

  1. The constructor should inject HelloWorldClassLoaderResources which we again will use to create a self-dependency, and HelloWorldGetStrategoRuntimeProvider which is a task that Spoofax generates for your language, which provides a Stratego runtime to execute strategies with.
  2. The HelloWorldGetStrategoRuntimeProvider instance is provided to the superclass constructor, along with the strategy that we want this task to execute, which is "replace-worlds".
  3. We override getId of TaskDef again to give this task a unique identifier.
  4. We override createDependencies of AstStrategoTransformTaskDef to create a self-dependency.

Then create the HelloWorldShowReplaceWorlds class and replace the entire Java file with:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package mb.helloworld.task;

import java.io.Serializable;
import java.util.Objects;

import javax.inject.Inject;

import org.checkerframework.checker.nullness.qual.Nullable;

import mb.helloworld.HelloWorldClassLoaderResources;
import mb.helloworld.HelloWorldScope;
import mb.pie.api.ExecContext;
import mb.pie.api.TaskDef;
import mb.pie.api.stamp.resource.ResourceStampers;
import mb.resource.ResourceKey;
import mb.spoofax.core.language.command.CommandFeedback;
import mb.spoofax.core.language.command.ShowFeedback;
import mb.aterm.common.TermToString;

@HelloWorldScope
public class HelloWorldShowReplaceWorlds implements TaskDef<HelloWorldShowReplaceWorlds.Args, CommandFeedback> {
    public static class Args implements Serializable {
        private static final long serialVersionUID = 1L;

        public final ResourceKey file;

        public Args(ResourceKey file) {
            this.file = file;
        }

        @Override
        public boolean equals(@Nullable Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            final Args args = (Args) o;
            return file.equals(args.file);
        }

        @Override
        public int hashCode() {
            return Objects.hash(file);
        }

        @Override
        public String toString() {
            return "Args{" + "file=" + file + '}';
        }
    }


    private final HelloWorldClassLoaderResources classloaderResources;
    private final HelloWorldParse parse;
    private final HelloWorldReplaceWorlds replaceWorlds;

    @Inject
    public HelloWorldShowReplaceWorlds(HelloWorldClassLoaderResources classloaderResources, HelloWorldParse parse, HelloWorldReplaceWorlds replaceWorlds) {
        this.classloaderResources = classloaderResources;
        this.parse = parse;
        this.replaceWorlds = replaceWorlds;
    }


    @Override
    public CommandFeedback exec(ExecContext context, Args args) throws Exception {
        context.require(classloaderResources.tryGetAsLocalResource(getClass()), ResourceStampers.hashFile());
        final ResourceKey file = args.file;
        return context.require(replaceWorlds, parse.inputBuilder().withFile(file).buildAstSupplier()).mapOrElse(
            ast -> CommandFeedback.of(ShowFeedback.showText(TermToString.toString(ast), "Replaced World()s with Hello()s for '" + file + "'")),
            e -> CommandFeedback.ofTryExtractMessagesFrom(e, file)
        );
    }

    @Override
    public String getId() {
        return getClass().getName();
    }
}

This class very similar to HelloWorldShowParsedAst, but runs the HelloWorldReplaceWorlds task on the parsed AST, transforming the AST.

Now open helloworld/spoofax.cfg again and register the tasks by adding:

task-def mb.helloworld.task.HelloWorldReplaceWorlds
let showReplaceWorlds = task-def mb.helloworld.task.HelloWorldShowReplaceWorlds

Then add a command for it by adding:

let showReplaceWorldsCommand = command-def {
  task-def = showReplaceWorlds
  display-name = "Replace world with hello"
  parameters = [
    file = parameter {
      type = java mb.resource.ResourceKey
      argument-providers = [Context(ReadableResource)]
    }
  ]
}

Finally, add menu items for the command by adding:

editor-context-menu [
  menu "Transform" [
    command-action {
      command-def = showReplaceWorldsCommand
      execution-type = Once
    }
    command-action {
      command-def = showReplaceWorldsCommand
      execution-type = Continuous
    }
  ]
]
resource-context-menu [
  menu "Transform" [
    command-action {
      command-def = showReplaceWorldsCommand
      execution-type = Once
      required-resource-types = [File]
    }
  ]
]

Build the project so that we can test our changes. Test the command similarly to testing the "Show parsed AST" command.