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
- Kotlin
- Java
repositories {
mavenLocal()
mavenCentral()
flatDir {
dirs("deps")
}
}
dependencies {
implementation(files("deps/rpgparser-2.2.0-all.jar"))
implementation(files("deps/synon-2.2.0.jar"))
}
repositories {
mavenLocal()
mavenCentral()
flatDir {
dirs("deps")
}
}
dependencies {
implementation(files("deps/rpgparser-2.2.0-all.jar"))
implementation(files("deps/synon-2.2.0.jar"))
implementation "com.strumenta.kolasu:kolasu-javalib:1.5.107"
}
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.
- Kotlin
- Java
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}")
}
}
import com.strumenta.kolasu.commercial.LicenseManager;
import com.strumenta.rpgparser.RPGKolasuParser;
import com.strumenta.rpgparser.synon.SynonDetection;
import com.strumenta.rpgparser.synon.SynonDetector;
import java.io.File;
import java.nio.file.Files;
public class DetectSynon {
public static void detectSynon(File rpgFile, File license) throws Exception {
LicenseManager.registerLicense(license);
String code = Files.readString(rpgFile.toPath());
var result = RPGKolasuParser.parserFromExtension(rpgFile).parse(rpgFile);
SynonDetection detection = SynonDetector.INSTANCE.detect(result.getRoot(), code);
System.out.println("Synon-generated: " + detection.isSynon()
+ " (confidence " + detection.getConfidence() + ")");
detection.getEvidence().forEach(e -> System.out.println(" • " + e));
}
}
Uplift to a SynonFunction
SynonUplifter.uplift returns null when the member is not recognized. On success you get a SynonFunction with:
| Field | Meaning |
|---|---|
templateRoutines | Generator skeleton subroutines bound to 2E roles (e.g. BAIZSF = initialize subfile) |
userPoints | Hand-written action-diagram logic (the statements worth migrating) |
residual | Whatever of the original CompilationUnit was not absorbed |
templateRatio | Fraction of statements that are generator boilerplate |
Choose an UpliftStrategy:
| Strategy | Behaviour |
|---|---|
COMMENT_BASED | Delimit user points from * USER: markers only |
STRUCTURAL | Infer user points from AST structure / fingerprints; ignore comments |
HYBRID (default) | Structural extraction, with comment markers used to confirm and score |
- Kotlin
- Java
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)}")
}
}
}
import com.strumenta.kolasu.commercial.LicenseManager;
import com.strumenta.rpgparser.RPGKolasuParser;
import com.strumenta.rpgparser.synon.SynonFunction;
import com.strumenta.rpgparser.synon.SynonUplifter;
import com.strumenta.rpgparser.synon.structural.UpliftStrategy;
import java.io.File;
import java.nio.file.Files;
public class UpliftMember {
public static void upliftMember(File rpgFile, File license) throws Exception {
LicenseManager.registerLicense(license);
String code = Files.readString(rpgFile.toPath());
var cu = RPGKolasuParser.parserFromExtension(rpgFile).parse(rpgFile).getRoot();
if (cu == null) return;
SynonFunction function = new SynonUplifter().uplift(
cu, code, rpgFile.getName(), UpliftStrategy.HYBRID);
if (function == null) {
System.out.println(rpgFile.getName() + ": not Synon-generated");
return;
}
System.out.println(function.getName() + " — " + function.getFunctionType().getDescription());
System.out.println("User points: " + function.getUserPoints().size());
System.out.println("Boilerplate: " + (function.getTemplateRatio() * 100) + "%");
}
}
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)
- Kotlin
- Java
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}")
}
}
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.structural.UpliftStrategy;
import com.strumenta.rpgparser.synon.viz.FunctionView;
import com.strumenta.rpgparser.synon.viz.SynonHtmlReport;
import java.io.File;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
public class ReportDirectory {
public static void reportDirectory(File dir, File license, File htmlOut) throws Exception {
LicenseManager.registerLicense(license);
File[] files = dir.listFiles((d, name) -> {
String lower = name.toLowerCase();
return lower.endsWith(".rpgle") || lower.endsWith(".sqlrpgle") || lower.endsWith(".rpg");
});
if (files == null) return;
List<FunctionView> views = new ArrayList<>();
List<SynonUpliftResult> results = new ArrayList<>();
for (File file : files) {
String code = Files.readString(file.toPath());
var parsed = RPGKolasuParser.parserFromExtension(file).parse(file);
var detection = SynonDetector.INSTANCE.detect(parsed.getRoot(), code);
var function = parsed.getRoot() == null
? null
: new SynonUplifter().uplift(
parsed.getRoot(), code, file.getName(), UpliftStrategy.HYBRID);
SynonUpliftResult result = new SynonUpliftResult(
file.getName(), detection, function, parsed.getIssues().size());
results.add(result);
views.add(new FunctionView(result, code));
}
System.out.println(SynonReport.INSTANCE.render(results));
if (htmlOut != null) {
Files.writeString(htmlOut.toPath(), SynonHtmlReport.INSTANCE.render(views));
}
}
}
Next steps
- Combine with program inventory and the analyzer when scoping a mixed hand-written / generated codebase.