Follow

Customizing Compiler Output with Helper Configurations

Table of Contents

  1. Overview
  2. How helper files are supplied
  3. Renaming and reshaping identifiers
  4. Adjusting declarations and structures
  5. Overriding calls and statements
  6. Injecting hand-written Java
  7. Parameter passing and sharing
  8. Restructuring the generated class
  9. Quick reference
  10. Best practices
  11. Summary
  12. Related articles

Audience: developers migrating PL/I to Java who need to adjust how a specific program is translated — renaming a generated identifier, overriding a declaration, substituting a hand-written Java method, or changing how parameters are passed — without editing the generated code by hand after every recompile.


Overview

The Heirloom PL/I compiler translates your source automatically, and for the overwhelming majority of programs the generated Java needs no manual intervention. Occasionally, though, a single program has a construct that should map to something other than the compiler's default — a name that must match an existing Java class, a structure that has to be declared a particular way, or a routine that is easier to supply as hand-written Java.

A helper configuration lets you specify those adjustments declaratively, in a JSON file that lives beside your source. The compiler reads the helper for each program at translation time and applies the overrides, so the generated Java comes out the way you need it — every time you recompile, with no post-processing step to remember and no generated file to hand-edit.

Helpers are an advanced, targeted tool. They are best used sparingly for the handful of programs that genuinely need them; most migrations use few or none. A helper overrides the compiler's judgement for one specific input, so an incorrect helper produces incorrect Java just as reliably as a correct one produces the output you want. Treat them like surgical patches, not routine configuration.

Note: The snippets below use illustrative program, variable, and procedure names. Your own names are preserved from your source (lower-cased per Java convention, as everywhere else in the generated code).


How helper files are supplied

Helpers are passed to the compiler with the --helper option, which points at a folder:

plic --helper ./helpers  --output ./java  ./src/MYPROG.pli

Inside that folder the compiler looks for one JSON file per program, named after the program with a .json extension. When it compiles MYPROG.pli it loads helpers/MYPROG.json; a program with no matching file simply gets no overrides. This keeps each program's customizations isolated and version- controlled alongside the source.

A helper file is a single JSON object whose top-level keys select the kind of override. Every key is optional — include only the ones a given program needs:

{
  "classes":     { },
  "methods":     { },
  "attributes":  { },
  "symbols":     { },
  "struct":      { },
  "init":        { },
  "params":      { },
  "statements":  { },
  "java":        { },
  "override":    { },
  "additionalmethods": { },
  "callByRef":   { },
  "sharedParam": { },
  "sqlProc":     { },
  "refactor":    { }
}

Three of these categories — java, override, and additionalmethods — pull Java source from .java files placed in the same helper folder. The helper entry names the file; the compiler reads the method body from it. The rest are self-contained in the JSON.

The remainder of this article covers each category in turn, grouped by what it adjusts.


Renaming and reshaping identifiers

These helpers change the name or type the compiler assigns, without changing behavior.

classes — ordinal interface names

By default the compiler derives a Java interface name for a PL/I ORDINAL from the source. Use classes to force a specific name — for example to match an interface that already exists in your Java code base. The entry is keyed by the ordinal name; camelCase is the name to emit.

define ordinal statusCode( PENDING, APPROVED );
"classes": {
    "statusCode": { "camelCase": "StatusCode" }
}

The generated interface is then public interface StatusCode { … }.

methods — method names and generation strategy

Controls how a procedure name is rendered and which code-generation strategy is used for it. Keyed by the procedure name:

  • camelCase — the method name to emit.
  • strategy — one of JavaClass, JavaMethod, JavaMainMethod, or JavaInstanceClass.
  • replacement — overrides how calls to the method are written (empty string to leave calls unchanged).
"methods": {
    "orderLookup": { "camelCase": "orderLookup", "strategy": "JavaInstanceClass", "replacement": "" }
}

symbols — the Java type of a variable

Overrides the type the compiler picks for the instance variable of a given symbol. The type must be one that is declared in the program. Keyed by the symbol name:

DCL 1 VAR_DEF DEF BASE_VAR,
    3 STR CHAR(08);
DCL P POINTER;
"symbols": {
    "p": { "type": "VAR_DEF" }
}

The compiler then declares p as private Var_def p = new Var_def(); instead of its default pointer type.


Adjusting declarations and structures

These helpers change how storage is declared in the generated class.

attributes — how a field is declared

Overrides the declaration line the compiler emits for a data declaration. Keyed by the declaration name (case-sensitive):

  • scope — must be JavaInstanceClass.
  • replacement — the declaration to emit. Write it without an access modifier and without a trailing semicolon.
  • camelCase and exclude are required by the format but are not used by the compiler; include them anyway.
DCL 1 BASE_VAR,
    3 STR CHAR(08);
"attributes": {
    "BASE_VAR": { "camelCase": "baseVar1", "exclude": false, "scope": "JavaInstanceClass",
                  "replacement": "Base_var baseVar2 = new Base_var()" }
}

Warning: This helper changes only the declaration. If the replacement renames the variable, references to it elsewhere in the class are not updated and will fail to compile. Use attributes to change how a field is declared, not to rename it.

struct — replacing a structure declaration

Replaces the fields of a generated structure class. The important rule: the entry must list every field of the structure, not only the ones being changed — the listed fields become the complete field set. Keyed by the structure's class name; each field entry carries a name, a type, and an instance initializer.

"struct": {
    "LOOKUP_TABLE": [
      { "name": "max_rows", "type": "Integer",     "instance": "MyProg.lookup_table_size" },
      { "name": "rows",     "type": "Array<Row>",  "instance": "new Array<Row>(MyProg.lookup_table_size, Row.class, Object.class, new Row(), \"ROWS\")" }
    ]
}

init — patching uninitialized arrays

Adds an extra array-initialization block for a structure or variable that holds an array, for cases where the elements must be explicitly initialized. Keyed by the structure or variable name; code is the Java to inject.

"init": {
    "MYTABLE": { "code": "for(int i = 1; i < mytable.tab.size(); i++) {\n  MyTable.Tab tab = new MyTable.Tab();\n  tab.fromBytes(\"\".getBytes());\n  mytable.tab.set(tab, i);\n}" }
}

Overriding calls and statements

These helpers substitute the Java emitted for a specific expression or line.

params — a call-argument expression

Overrides how a single argument expression in a CALL is rendered. Keyed by the generated argument expression; replacement is what to emit instead.

CALL ORDERPROC( ADDR(ORDER_PARMS) );
"params": {
    "Builtin.ADDR(order_parms)": { "replacement": "order_parms" }
}

The argument is then passed as order_parms rather than Builtin.ADDR(order_parms).

statements — replacing a whole statement line

Replaces the Java generated for a specific PL/I statement. Keyed by the exact PL/I source line — including its original whitespace — so the compiler can match it. replacement is the Java to emit; the optional extra_lines adds lines after it.

"statements": {
    "TODAY = SUBSTR(RAW_DATE,1,8) ;": { "replacement": "today = Builtin.SUBSTR(raw_date, 1, 8);" }
}

Note: The statements helper matches complete statements only; it cannot reach individual lines nested inside a larger construct such as a long IF … ELSE chain. When you need to replace something inside a compound statement, supply the whole enclosing routine with the override helper instead (below).


Injecting hand-written Java

When a routine is easier to express directly in Java — because it depends on something outside the PL/I, or the automatic translation is not what you want — these three helpers pull the Java from a .java file in the helper folder. In each case the entry names the source file with javaClass; the javaMethod property is required by the format but not used, so any placeholder is fine.

java — replace a procedure's body

Replaces the body generated for a PL/I procedure with the matching method from the named Java file. Keyed by the procedure name (case-sensitive).

"java": {
    "IS_DOMESTIC":  { "javaClass": "MyProg.java", "javaMethod": "n/a" },
    "READ_QUEUE":   { "javaClass": "MyProg.java", "javaMethod": "n/a" }
}

override — replace any generated Java method

Like java, but keyed by the Java method name rather than a PL/I procedure. This reaches methods the compiler adds that have no PL/I counterpart — for example the transaction invoke / cicsInvoke entry points.

"override": {
    "cicsInvoke": { "javaClass": "MyProg.java", "javaMethod": "n/a" },
    "invoke":     { "javaClass": "MyProg.java", "javaMethod": "n/a" }
}

additionalmethods — add a new method

Adds a method that does not exist in the generated class at all, taken from the named Java file. Keyed by the method name to add.

"additionalmethods": {
    "recomputeTotals": { "javaClass": "MyProg.java", "javaMethod": "n/a" }
}

Parameter passing and sharing

These helpers change how procedure parameters and stored-procedure arguments are handled.

callByRef — pass a procedure's arguments by reference

Java passes object references by value, so a called method cannot rebind a caller's variable the way PL/I BY REFERENCE allows. This helper wraps the named procedure's argument list in a list object and rewrites its calls to use that list, so updates made inside the procedure are visible to the caller. Keyed by the procedure name.

"callByRef": {
    "computeflag": { "name": "computeflag" }
}

Calls are then generated to bundle the arguments into a shared list, and the procedure reads and writes them through it.

sharedParam — promote a parameter to an instance field

Moves a parameter up to class level so it becomes an instance variable shared across the class, rather than a per-call method parameter. Keyed by the parameter name (case-sensitive).

"sharedParam": {
    "P_CONTEXT": { "name": "p_context" },
    "P_SESSION": { "name": "p_session" }
}

sqlProc — parameter directions for EXEC SQL CALL

For a CALL to a DB2 stored procedure, the direction of each argument (IN / OUT / INOUT) lives in the procedure's CREATE PROCEDURE, not in the PL/I source, so the compiler cannot infer it. This helper supplies it. Entries are keyed by the schema-qualified stored-procedure name exactly as written in the CALL, and each value is a list of parameter entries.

The recommended form is positional: give one entry per argument, in signature order, with just the io direction. The list must have the same number of entries as the CALL — otherwise the direction cannot be resolved by position and defaults to IN (with a warning logged).

"sqlProc": {
    "SALES.CALC_TOTALS": [
      { "io": "IN" }, { "io": "IN" }, { "io": "IN" },
      { "io": "OUT" }, { "io": "OUT" }
    ]
}

Entries may instead (or additionally) carry a param naming the host variable, which is matched by name before position — useful when the CALL order is awkward to track. See Compiling EXEC SQL Programs for the wider picture on embedded SQL.


Restructuring the generated class

refactor — move a class inside another as a nested class

Relocates a generated class so it becomes a nested (inner) class of another. Keyed by the class name to move; the value is a list of actions:

  • action — currently moveToInner.
  • toClass — the class to move it into.
  • static — optional; whether the moved inner class is static. When omitted, the compiler mirrors the effective compile strategy (an instance / sub-program build yields a non-static inner class so it can reach the outer instance's working storage).
"refactor": {
    "LookupBlock": [ { "action": "moveToInner", "toClass": "MyProg", "static": false } ],
    "WorkArea":    [ { "action": "moveToInner", "toClass": "MyProg" } ]
}

Quick reference

JSON key Adjusts Keyed by
classes Ordinal interface name Ordinal name
methods Method name and generation strategy Procedure name
symbols Java type of a variable Symbol name
attributes How a field is declared Declaration name (case-sensitive)
struct Fields of a structure class Structure class name
init Extra array initialization Structure / variable name
params A call-argument expression Generated argument expression
statements A whole generated statement line Exact PL/I source line
java A procedure's body (from a .java file) Procedure name
override Any generated Java method (from a .java file) Java method name
additionalmethods Adds a method (from a .java file) Method name to add
callByRef Pass a procedure's arguments by reference Procedure name
sharedParam Promote a parameter to an instance field Parameter name
sqlProc IN/OUT/INOUT for an EXEC SQL CALL Schema-qualified procedure name
refactor Move a class inside another Class name to move

Best practices

  • Prefer fixing the cause over patching the output. If several programs need the same helper, that usually signals a translation gap worth reporting to Heirloom Computing so it can be handled in the compiler or runtime for everyone, rather than replicated by hand.
  • Keep helpers minimal and local. One JSON file per program, holding only the overrides that program needs. Empty categories can be dropped entirely.
  • Version helpers with the source. Because the compiler re-applies them on every build, a helper is part of the program's definition — store it in the same repository as the PL/I.
  • Re-verify after upgrades. A helper works around a specific behavior of a specific compiler version. After a compiler upgrade, recompile and check whether a given helper is still needed — a fixed translation gap can make one redundant or, occasionally, conflicting.
  • Escape entities and match whitespace exactly in statements and other literal-match helpers; the match is textual.

Summary

  • Helper configurations are per-program JSON files, supplied with --helper pointing at a folder, that override how one program is translated — applied automatically on every recompile.
  • Each program's overrides live in <program>.json; a top-level key selects the kind of override, and every category is optional.
  • Categories cover renaming and retyping identifiers (classes, methods, symbols), reshaping declarations (attributes, struct, init), substituting calls and statements (params, statements), injecting hand-written Java (java, override, additionalmethods), parameter passing and sharing (callByRef, sharedParam, sqlProc), and restructuring the class (refactor).
  • Helpers are a targeted, advanced tool: use them sparingly, keep them versioned with the source, and re-verify them after compiler upgrades.

Was this article helpful?
0 out of 0 found this helpful
Have more questions? Submit a request

0 Comments

Article is closed for comments.
Powered by Zendesk