Skip to main content

Transform the AST

Parsers produce ASTs so you can analyze and rewrite code. Starlasu (Kolasu) supports transforming trees: visit each node, replace it or transform it, and create a new root.

Use transforms for refactoring helpers, normalization passes, and the first stage of a transpiler. Transformers are the foundational feature we at Strumenta use to build our transpilers. The RPG analyzer also uses ASTTransformer internally when building PlantUML sequence diagrams.

When to use transforms

ToolTypical use
Read-only walkInventory, call listing, metrics
TransformRename identifiers, strip constructs, transpile to a target language

Add Dependencies

repositories {
mavenLocal()
mavenCentral()
flatDir {
dirs("deps")
}
}

dependencies {
implementation(files("deps/rpgparser-2.2.0-all.jar"))
}

Collect external program calls

This example builds a small summary tree of every CallProgramStatement in a compilation unit. ASTTransformer.transform gives you complete control on every transformation.

The consequence of this approach is that you need to define a transformation not just for every node you need to transform, but for every node that could contain that node — otherwise the transformer applies the default transformation you selected when creating the ASTTransformer.

import com.strumenta.kolasu.commercial.LicenseManager
import com.strumenta.kolasu.model.Node
import com.strumenta.kolasu.transformation.ASTTransformer
import com.strumenta.kolasu.transformation.IDENTTITY_TRANSFORMATION
import com.strumenta.kolasu.traversing.walkDescendants
import com.strumenta.kolasu.validation.Issue
import com.strumenta.rpgparser.RPGKolasuParser
import com.strumenta.rpgparser.model.CallProgramStatement
import com.strumenta.rpgparser.model.CompilationUnit
import java.io.File

data class CallTarget(val name: String) : Node()

data class CallSummary(val targets: List<CallTarget>) : Node()

fun collectProgramCalls(rpgFile: File, license: File) {
LicenseManager.registerLicense(license)
val result = RPGKolasuParser.parserFromExtension(rpgFile).parse(rpgFile)
val root = result.root ?: return

val issues: MutableList<Issue> = result.issues.toMutableList()
val transformer = ASTTransformer(
issues = issues,
allowGenericNode = true,
defaultTransformation = IDENTTITY_TRANSFORMATION
)

transformer.registerNodeFactory(CompilationUnit::class) { cu: CompilationUnit, t ->
val calls = cu.walkDescendants(CallProgramStatement::class)
.map { t.transform(it) as CallTarget }
.toList()
CallSummary(calls)
}

transformer.registerNodeFactory(CallProgramStatement::class) { call: CallProgramStatement, _ ->
CallTarget(call.programName.toString())
}

val summary = transformer.transform(root) as CallSummary
summary.targets.forEach { println(it.name) }
}

Typically you define a transformation for every node, unless you have a well-defined and specific need — for example collapsing all CALL sites into a call-graph summary, or mapping RPG statements onto another language's AST.