Skip to main content

Detect and Uplift Synon / CA 2E RPG

Many IBM i applications were generated by Synon / CA 2E. The generated RPG is large and full of boilerplate template subroutines; the business logic lives in a few * USER: action-diagram points.

The synon module recognizes that structure and uplifts a parsed CompilationUnit into a SynonFunction tree: template routines vs hand-written user points. That separation is the useful input for migration scoping.

Add Dependencies

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

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

Detect Synon / 2E members

SynonDetector.detect combines comment/header evidence (generator banner, function-type header, * USER: markers) with structural evidence (2E skeleton subroutine names, Y* runtime CALLs). Confidence ≥ 0.5 means the member is treated as Synon-generated.

import com.strumenta.kolasu.commercial.LicenseManager
import com.strumenta.rpgparser.RPGKolasuParser
import com.strumenta.rpgparser.synon.SynonDetector
import java.io.File

fun detectSynon(rpgFile: File, license: File) {
LicenseManager.registerLicense(license)
val code = rpgFile.readText()
val result = RPGKolasuParser.parserFromExtension(rpgFile).parse(rpgFile)
val detection = SynonDetector.detect(result.root, code)

println("Synon-generated: ${detection.isSynon} (confidence ${"%.2f".format(detection.confidence)})")
detection.generator?.let { g ->
println("Generator: ${g.product} ${g.release ?: ""}")
}
println("Declared type: ${detection.declaredFunctionType}")
println("Classified type: ${detection.classifiedFunctionType}")
detection.evidence.forEach { println(" • $it") }
detection.userPointMarkers.forEach { marker ->
println(" USER: ${marker.name} @ line ${marker.line}")
}
}

Uplift to a SynonFunction

SynonUplifter.uplift returns null when the member is not recognized. On success you get a SynonFunction with:

FieldMeaning
templateRoutinesGenerator skeleton subroutines bound to 2E roles (e.g. BAIZSF = initialize subfile)
userPointsHand-written action-diagram logic (the statements worth migrating)
residualWhatever of the original CompilationUnit was not absorbed
templateRatioFraction of statements that are generator boilerplate

Choose an UpliftStrategy:

StrategyBehaviour
COMMENT_BASEDDelimit user points from * USER: markers only
STRUCTURALInfer user points from AST structure / fingerprints; ignore comments
HYBRID (default)Structural extraction, with comment markers used to confirm and score
import com.strumenta.kolasu.commercial.LicenseManager
import com.strumenta.rpgparser.RPGKolasuParser
import com.strumenta.rpgparser.synon.SynonUplifter
import com.strumenta.rpgparser.synon.structural.UpliftStrategy
import java.io.File

fun upliftMember(rpgFile: File, license: File) {
LicenseManager.registerLicense(license)
val code = rpgFile.readText()
val cu = RPGKolasuParser.parserFromExtension(rpgFile).parse(rpgFile).root
?: return

val function = SynonUplifter().uplift(
cu,
code,
nameHint = rpgFile.name,
strategy = UpliftStrategy.HYBRID,
) ?: run {
println("${rpgFile.name}: not Synon-generated")
return
}

println("${function.name}${function.functionType.description} (${function.functionType.code})")
println("Template routines: ${function.templateRoutines.size}")
println("User points: ${function.userPoints.size} (${function.userStatementCount} statements)")
println("Boilerplate: ${"%.0f".format(function.templateRatio * 100)}%")

function.userPoints.filter { it.statements.isNotEmpty() }.forEach { up ->
println(" USER ${up.name} in ${up.hostRoutine}: ${up.statements.size} stmts")
up.evidence?.let { ev ->
println(" evidence: ${ev.basis}, confidence ${"%.2f".format(ev.confidence)}")
}
}
}

Text and HTML reports

For a directory of members, collect SynonUpliftResult values and render:

  • SynonReport.render — plain-text codebase summary (recognized types, user points, template ratios)
  • SynonHtmlReport.render — self-contained HTML dashboard with colour-coded source vs uplifted AST (no external assets)
import com.strumenta.kolasu.commercial.LicenseManager
import com.strumenta.rpgparser.RPGKolasuParser
import com.strumenta.rpgparser.synon.SynonDetector
import com.strumenta.rpgparser.synon.SynonReport
import com.strumenta.rpgparser.synon.SynonUpliftResult
import com.strumenta.rpgparser.synon.SynonUplifter
import com.strumenta.rpgparser.synon.viz.FunctionView
import com.strumenta.rpgparser.synon.viz.SynonHtmlReport
import java.io.File

fun reportDirectory(dir: File, license: File, htmlOut: File? = null) {
LicenseManager.registerLicense(license)
val extensions = setOf("rpgle", "sqlrpgle", "rpg")
val files = dir.listFiles { f -> f.extension.lowercase() in extensions }?.sorted().orEmpty()

val views = files.map { file ->
val code = file.readText()
val parsed = RPGKolasuParser.parserFromExtension(file).parse(file)
val detection = SynonDetector.detect(parsed.root, code)
val function = parsed.root?.let { SynonUplifter().uplift(it, code, nameHint = file.name) }
FunctionView(
SynonUpliftResult(file.name, detection, function, parsed.issues.size),
code,
)
}

println(SynonReport.render(views.map { it.result }))
if (htmlOut != null) {
htmlOut.writeText(SynonHtmlReport.render(views))
println("HTML report: ${htmlOut.absolutePath}")
}
}

Next steps