Call Graph
CL programs orchestrate IBM i work through CALL, CALLPRC, and CALLSUBR. Walking those command nodes produces a call surface you can use for impact analysis, migration scoping, or a simple dependency graph across a directory of members.
Program and bound-procedure targets are expressions (names, qualified objects, or variables). They are not resolved by the semantics module — treat them as string-like edges. Subroutine calls can be linked to definitions after symbol resolution.
Add Dependencies
- Kotlin
- Java
repositories {
mavenLocal()
mavenCentral()
flatDir {
dirs("deps")
}
}
dependencies {
implementation(files("deps/clparser-with-dependencies-1.0.1-all.jar"))
}
repositories {
mavenLocal()
mavenCentral()
flatDir {
dirs("deps")
}
}
dependencies {
implementation(files("deps/clparser-with-dependencies-1.0.1-all.jar"))
implementation "com.strumenta.kolasu:kolasu-javalib:1.5.107"
}
Extract CALL / CALLPRC / CALLSUBR from one file
- Kotlin
- Java
import com.strumenta.cl.model.CLExpression
import com.strumenta.cl.model.CLLiteral
import com.strumenta.cl.model.CLReferenceExpr
import com.strumenta.cl.model.CallBoundProcedureCommand
import com.strumenta.cl.model.CallProgramCommand
import com.strumenta.cl.model.CallSubroutineCommand
import com.strumenta.cl.model.QualifiedObjectName
import com.strumenta.cl.parser.CLKolasuParser
import com.strumenta.kolasu.commercial.LicenseManager
import com.strumenta.kolasu.traversing.walkDescendants
import java.io.File
fun formatExpr(expr: CLExpression?): String =
when (expr) {
is CLReferenceExpr -> expr.name.name
is CLLiteral -> expr.text
is QualifiedObjectName ->
if (expr.container != null) "${expr.container}/${expr.name}" else expr.name
null -> "?"
else -> expr.toString()
}
fun printCallSurface(clFile: File, license: File) {
LicenseManager.registerLicense(license)
val root = CLKolasuParser().parse(clFile).root ?: return
println("CALL (programs):")
root.walkDescendants(CallProgramCommand::class).forEach { call ->
println(" ${formatExpr(call.pgm)} (${call.parameters.size} parm expr(s))")
}
println("CALLPRC (bound procedures):")
root.walkDescendants(CallBoundProcedureCommand::class).forEach { call ->
println(" ${formatExpr(call.prc)}")
}
println("CALLSUBR (subroutines):")
root.walkDescendants(CallSubroutineCommand::class).forEach { call ->
println(" ${formatExpr(call.subr)}")
}
}
import com.strumenta.cl.model.CLExpression;
import com.strumenta.cl.model.CLLiteral;
import com.strumenta.cl.model.CLReferenceExpr;
import com.strumenta.cl.model.CallBoundProcedureCommand;
import com.strumenta.cl.model.CallProgramCommand;
import com.strumenta.cl.model.CallSubroutineCommand;
import com.strumenta.cl.model.CLCompilationUnit;
import com.strumenta.cl.model.QualifiedObjectName;
import com.strumenta.cl.parser.CLKolasuParser;
import com.strumenta.kolasu.commercial.LicenseManager;
import com.strumenta.kolasu.javalib.Traversing;
import java.io.File;
public class CallSurface {
public static String formatExpr(CLExpression expr) {
if (expr instanceof CLReferenceExpr ref) return ref.getName().getName();
if (expr instanceof CLLiteral lit) return lit.getText();
if (expr instanceof QualifiedObjectName q) {
return q.getContainer() != null ? q.getContainer() + "/" + q.getName() : q.getName();
}
return expr == null ? "?" : expr.toString();
}
public static void printCallSurface(File clFile, File license) {
LicenseManager.INSTANCE.registerLicense(license);
CLCompilationUnit root = new CLKolasuParser().parse(clFile).getRoot();
if (root == null) return;
System.out.println("CALL (programs):");
Traversing.walkDescendantsBreadthFirst(root, CallProgramCommand.class, call ->
System.out.println(" " + formatExpr(call.getPgm())));
System.out.println("CALLPRC (bound procedures):");
Traversing.walkDescendantsBreadthFirst(root, CallBoundProcedureCommand.class, call ->
System.out.println(" " + formatExpr(call.getPrc())));
System.out.println("CALLSUBR (subroutines):");
Traversing.walkDescendantsBreadthFirst(root, CallSubroutineCommand.class, call ->
System.out.println(" " + formatExpr(call.getSubr())));
}
}
Build edges across a directory
Walk every .cl / .clp / .clle file and emit caller → callee edges. Member names are a useful caller id when the source does not encode a program name in the AST.
- Kotlin
- Java
fun buildCallEdges(projectDir: File, license: File): List<Pair<String, String>> {
LicenseManager.registerLicense(license)
val parser = CLKolasuParser()
val edges = mutableListOf<Pair<String, String>>()
projectDir.walkTopDown()
.filter { it.isFile && it.extension.lowercase() in setOf("cl", "clp", "clle") }
.forEach { file ->
val root = parser.parse(file).root ?: return@forEach
val caller = file.nameWithoutExtension
root.walkDescendants(CallProgramCommand::class).forEach { call ->
edges += caller to formatExpr(call.pgm)
}
}
return edges
}
import com.strumenta.cl.model.CLCompilationUnit;
import com.strumenta.cl.model.CLExpression;
import com.strumenta.cl.model.CLLiteral;
import com.strumenta.cl.model.CLReferenceExpr;
import com.strumenta.cl.model.CallProgramCommand;
import com.strumenta.cl.model.QualifiedObjectName;
import com.strumenta.cl.parser.CLKolasuParser;
import com.strumenta.kolasu.commercial.LicenseManager;
import com.strumenta.kolasu.javalib.Traversing;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.stream.Stream;
public class CallEdges {
public static String formatExpr(CLExpression expr) {
if (expr instanceof CLReferenceExpr ref) return ref.getName().getName();
if (expr instanceof CLLiteral lit) return lit.getText();
if (expr instanceof QualifiedObjectName q) {
return q.getContainer() != null ? q.getContainer() + "/" + q.getName() : q.getName();
}
return expr == null ? "?" : expr.toString();
}
public static List<String[]> buildCallEdges(File projectDir, File license) throws Exception {
LicenseManager.INSTANCE.registerLicense(license);
CLKolasuParser parser = new CLKolasuParser();
List<String[]> edges = new ArrayList<>();
try (Stream<Path> paths = Files.walk(projectDir.toPath())) {
paths.map(Path::toFile)
.filter(File::isFile)
.filter(f -> {
String ext = extension(f).toLowerCase(Locale.ROOT);
return ext.equals("cl") || ext.equals("clp") || ext.equals("clle");
})
.forEach(file -> {
CLCompilationUnit root = parser.parse(file).getRoot();
if (root == null) return;
String caller = stripExtension(file.getName());
Traversing.walkDescendantsBreadthFirst(root, CallProgramCommand.class)
.forEach(call ->
edges.add(new String[]{caller, formatExpr(call.getPgm())}));
});
}
return edges;
}
private static String extension(File f) {
String n = f.getName();
int i = n.lastIndexOf('.');
return i < 0 ? "" : n.substring(i + 1);
}
private static String stripExtension(String name) {
int i = name.lastIndexOf('.');
return i < 0 ? name : name.substring(0, i);
}
}
Node types: CallProgramCommand (pgm), CallBoundProcedureCommand (prc), CallSubroutineCommand (subr).