Skip to content

Part 2 — PL/I Compiler Reference

2.1 Compiler Overview

The Heirloom PL/I Compiler (pli_compiler2) is a source-to-source transpiler that converts PL/I programs into semantically equivalent Java code. It is built on ANTLR 4 for parsing and JavaParser for Java code generation.

2.1.1 Compilation Pipeline

PL/I Source
┌──────────────┐
│ Preprocessor │  %INCLUDE, macro expansion, conditional compilation
└──────┬───────┘
┌──────────────┐
│ Lexer        │  Token recognition, margin handling (columns 1–72)
└──────┬───────┘
┌──────────────┐
│ Parser       │  ANTLR-based recursive descent, 10 ANTLR grammar files
└──────┬───────┘
┌──────────────┐
│ Semantic     │  Symbol resolution, type checking, scope management
│ Analysis     │
└──────┬───────┘
┌──────────────┐
│ Transpiler   │  Java AST generation via JavaParser
└──────┬───────┘
Java Source Files

2.1.2 Invoking the Compiler

Command-line usage and batch compilation instructions.

2.1.3 Compiler Return Codes

  • 0 — Successful compilation
  • 4 — Warning(s) issued
  • 8 — Error(s) encountered
  • 12 — Severe error(s)

2.2 Compiler Options

2.2.1 Source Options

Option Default Description
--source Required Source PL/I file or directory
--margins 1,72 Source margins (start,end columns)
--include Include file search path
--comment false Generate inline code comments
--copycomments false Copy PL/I source comments into JavaDoc

2.2.2 Target Options

Option Default Description
--target Output directory for generated Java files
--package com.pli Java package for generated classes
--strategy static Code generation strategy (static / instance)
--types primitive Type system (primitive / manifold)

2.2.3 Processing Options

Option Default Description
--sql false Enable EXEC SQL statement processing
--cics false Enable EXEC CICS statement processing
--exci false Enable EXCI (External CICS Interface) processing
--dateformat EUR SQL date format (EUR/ISO/USA/JIS/LOCAL)
--entry false Process ENTRY data types
--byaddr false Pass arguments by reference (by address)

2.2.4 Advanced Options

Option Default Description
--helper Helper JSON configuration file path
--loglevel INFO Logging verbosity (ERROR/WARN/INFO/DEBUG)
--ast false Print AST in JSON format (diagnostic)
--lex false Print lexical tokens (diagnostic)
--ext false Generate external reference report

2.2.5 Code Generation Strategies

Static Strategy (--strategy static)

  • Class-level variables declared as private static
  • main() method entry point
  • Suitable for batch programs and standalone execution

Instance Strategy (--strategy instance)

  • Class-level variables as instance fields
  • Constructor initialization
  • Suitable for CICS transactions, MQ listeners, and application server deployment

2.3 Preprocessor

2.3.1 Preprocessor Overview

The PL/I Preprocessor (pli-preprocessor module) is a standalone component that processes PL/I source files before the compiler parses them. It runs as the first phase of the compilation pipeline and handles:

  1. %INCLUDE directive resolution — Inserts external copybook files
  2. Macro expansion — Processes macro definitions and invocations
  3. %REPLACE directives — Text substitution
  4. Conditional compilation — %IF/%THEN/%ELSE directives
  5. Source formatting — Column margin handling (1-72 by default)
  6. Comment preservation — Maintains comments for debugging

Pipeline Position:

PL/I Source (.pli)
┌──────────────────┐
│  Preprocessor    │  ← First phase
│                  │    • %INCLUDE resolution
│                  │    • Macro expansion
│                  │    • %REPLACE substitution
│                  │    • Conditional compilation
└────────┬─────────┘
Expanded PL/I Source
    Compiler (Parser/Transpiler)

The preprocessor ensures that the compiler receives fully expanded source code with all includes resolved and macros expanded.


2.3.2 %INCLUDE Directive

The %INCLUDE directive inserts external files (copybooks) into the source stream.

Syntax

%INCLUDE filename;

Search Path Resolution

The preprocessor searches for include files in this order:

  1. Current directory (same directory as source file)
  2. Configured include paths (--include option)
  3. Environment variable (PLI_INCLUDE_PATH)

Example:

java -jar pli_compiler2.jar \
  --source MYPROG.pli \
  --include /copybooks/common \
  --include /copybooks/db

%INCLUDE Example

Source file (CUSTOMER.pli):

 CUSTOMER: PROCEDURE OPTIONS(MAIN);

   %INCLUDE CUSTDEF;   /* Include customer structure definition */

   DCL CUST CUSTOMER_REC;

   /* Use customer structure */
   CUST.CUST_ID = 'C001';
   CUST.NAME = 'John Smith';

 END CUSTOMER;

Include file (CUSTDEF.pli):

 /* Customer record definition */
 DCL 1 CUSTOMER_REC,
       2 CUST_ID      CHAR(10),
       2 NAME         CHAR(30),
       2 BALANCE      DEC FIXED(15,2);

After preprocessing:

 CUSTOMER: PROCEDURE OPTIONS(MAIN);

   /* Customer record definition */
   DCL 1 CUSTOMER_REC,
         2 CUST_ID      CHAR(10),
         2 NAME         CHAR(30),
         2 BALANCE      DEC FIXED(15,2);

   DCL CUST CUSTOMER_REC;

   /* Use customer structure */
   CUST.CUST_ID = 'C001';
   CUST.NAME = 'John Smith';

 END CUSTOMER;

Nested %INCLUDE

Include files can themselves contain %INCLUDE directives:

Main source:

%INCLUDE COMMON;

COMMON.pli:

%INCLUDE TYPES;
%INCLUDE CONSTANTS;

The preprocessor recursively resolves all includes up to a configurable depth limit (default: 10 levels).

Circular Include Detection

The preprocessor detects circular includes and reports an error:

ERROR: Circular include detected: FILEA.pli → FILEB.pli → FILEA.pli


2.3.3 %REPLACE Directive

The %REPLACE directive performs text substitution throughout the source.

Syntax

%REPLACE identifier BY replacement-text;

%REPLACE Example

Source:

 %REPLACE MAX_CUSTOMERS BY 1000;
 %REPLACE STATUS_ACTIVE BY 'A';
 %REPLACE STATUS_INACTIVE BY 'I';

 SAMPLE: PROCEDURE OPTIONS(MAIN);
   DCL CUSTOMERS(MAX_CUSTOMERS) CHAR(10);
   DCL STATUS CHAR(1);

   STATUS = STATUS_ACTIVE;

   IF STATUS = STATUS_INACTIVE THEN
     PUT SKIP LIST('Inactive');

 END SAMPLE;

After preprocessing:

 SAMPLE: PROCEDURE OPTIONS(MAIN);
   DCL CUSTOMERS(1000) CHAR(10);
   DCL STATUS CHAR(1);

   STATUS = 'A';

   IF STATUS = 'I' THEN
     PUT SKIP LIST('Inactive');

 END SAMPLE;

Scope

%REPLACE directives have global scope within the compilation unit. They affect all subsequent source text, including included files processed after the directive.


2.3.4 Macro Processing

The preprocessor supports macro definitions with parameters and macro invocations.

Macro Definition Syntax

%MACRO_NAME: MACRO [(param1, param2, ...)] statement-list %MEND;

Simple Macro Example

Definition:

%INIT_STRING: MACRO (VAR, VALUE)
  VAR = '';
  VAR = VALUE;
%MEND;

Invocation:

DCL NAME CHAR(30);
%INIT_STRING(NAME, 'John Smith');

After expansion:

DCL NAME CHAR(30);
NAME = '';
NAME = 'John Smith';

Macro with Multiple Statements

Definition:

%LOG_ERROR: MACRO (MSG, CODE)
  PUT SKIP LIST('ERROR:', MSG);
  PUT SKIP LIST('CODE:', CODE);
  ERROR_COUNT = ERROR_COUNT + 1;
%MEND;

Invocation:

%LOG_ERROR('Invalid customer ID', 100);

After expansion:

PUT SKIP LIST('ERROR:', 'Invalid customer ID');
PUT SKIP LIST('CODE:', 100);
ERROR_COUNT = ERROR_COUNT + 1;

Built-in Macro Variables

The preprocessor provides built-in variables:

Variable Description Example Value
&SYSDATE Current date (YYYYMMDD) 20260302
&SYSTIME Current time (HHMMSS) 143022
&SYSPARM System parameter from command line User-defined

Example:

PUT SKIP LIST('Compiled on:', &SYSDATE);
PUT SKIP LIST('Compile time:', &SYSTIME);


2.3.5 Conditional Compilation

The preprocessor supports conditional compilation using %IF, %THEN, %ELSE, and %END.

Syntax

%IF condition %THEN
  statements-if-true
%ELSE
  statements-if-false
%END;

Example: Debug Mode

Source:

 %REPLACE DEBUG_MODE BY 1;

 PROGRAM: PROCEDURE OPTIONS(MAIN);

   %IF DEBUG_MODE = 1 %THEN
     PUT SKIP LIST('DEBUG: Starting program');
   %END;

   /* Main logic */
   DCL RESULT FIXED BIN(31);
   RESULT = 42;

   %IF DEBUG_MODE = 1 %THEN
     PUT SKIP LIST('DEBUG: Result =', RESULT);
   %END;

 END PROGRAM;

After preprocessing (with DEBUG_MODE = 1):

 PROGRAM: PROCEDURE OPTIONS(MAIN);

   PUT SKIP LIST('DEBUG: Starting program');

   /* Main logic */
   DCL RESULT FIXED BIN(31);
   RESULT = 42;

   PUT SKIP LIST('DEBUG: Result =', RESULT);

 END PROGRAM;

After preprocessing (with DEBUG_MODE = 0):

 PROGRAM: PROCEDURE OPTIONS(MAIN);

   /* Main logic */
   DCL RESULT FIXED BIN(31);
   RESULT = 42;

 END PROGRAM;

Conditional Compilation by Environment

Source:

 %IF &SYSPARM = 'PROD' %THEN
   %REPLACE LOG_LEVEL BY 'ERROR';
 %ELSE
   %REPLACE LOG_LEVEL BY 'DEBUG';
 %END;

Command line:

java -jar pli_compiler2.jar --source PROG.pli --sysparm PROD


2.3.6 Source Margins and Column Handling

The preprocessor handles fixed-form source format where code must appear within specific columns.

Default Margins

Columns Purpose
1-2 Sequence number area (ignored)
2-72 Statement area (compiled)
73-80 Identification area (ignored)

IBM Mainframe Card Image Format (traditional PL/I source):

  1         2         3         4         5         6         7         8
  |---------|---------|---------|---------|---------|---------|---------|
  SS<--- Statement area (columns 2-72) ------------------------->IIIIIIII
  ||                                                              ||||||||
  Sequence                                                        Identification

Configuring Margins

Use the --margins option to customize:

# Default (columns 2-72)
java -jar pli_compiler2.jar --source PROG.pli

# Custom (columns 1-80)
java -jar pli_compiler2.jar --source PROG.pli --margins 1,80

# Modern (no margin restrictions)
java -jar pli_compiler2.jar --source PROG.pli --margins 1,500

Example with Margins

Source file (72-column format):

       CUSTOMER: PROCEDURE OPTIONS(MAIN);                             00001000
         DCL NAME CHAR(30);                                           00002000
         NAME = 'John Smith';                                         00003000
       END CUSTOMER;                                                  00004000

Columns 1-2 (sequence) and 73-80 (identification) are ignored by the preprocessor.


2.3.7 Comment Handling

The preprocessor preserves comments for debugging but can also strip them for production builds.

PL/I Comment Syntax

/* This is a block comment */

/* Multi-line
   block
   comment */

Comment Preservation Options

Option Effect
--comment Generate inline comments in Java code
--copycomments Copy PL/I comments to Java as JavaDoc
(default) Strip comments from generated Java

Example with --copycomments:

PL/I:

/* Calculate customer balance */
BALANCE = DEPOSITS - WITHDRAWALS;

Generated Java:

/**
 * Calculate customer balance
 */
BALANCE = DEPOSITS.subtract(WITHDRAWALS);


2.3.8 Line Number Tracking

The preprocessor maintains line number mapping between original source and expanded source for accurate error reporting.

Error message format:

E:001 CUSTOMER.pli(45,10): Undefined variable 'CUSTOMER_ID'
      ^                ^
      |                |
      Original file    Original line number

Even after %INCLUDE expansion and macro processing, error messages reference the original source location.


2.3.9 Preprocessor Output

The preprocessor can optionally output the expanded source for inspection.

Viewing Preprocessed Source

Command:

java -jar pli_compiler2.jar --source PROG.pli --preprocess-only

Output: Expanded source written to stdout or file, showing: - All %INCLUDE files inserted - All macros expanded - All %REPLACE substitutions applied - Original line number comments preserved

Example output:

/* Original: CUSTOMER.pli line 1 */
 CUSTOMER: PROCEDURE OPTIONS(MAIN);

/* Original: CUSTDEF.pli line 1 (included from CUSTOMER.pli line 3) */
   DCL 1 CUSTOMER_REC,
         2 CUST_ID      CHAR(10),
         2 NAME         CHAR(30);

/* Original: CUSTOMER.pli line 5 */
   DCL CUST CUSTOMER_REC;

 END CUSTOMER;


2.3.10 Preprocessor Best Practices

DO:

✅ Use %INCLUDE for shared definitions (structures, constants) ✅ Use %REPLACE for configuration values (array sizes, limits) ✅ Use conditional compilation for debug/production builds ✅ Keep include files focused and modular ✅ Document macro parameters clearly

DON'T:

❌ Create deeply nested includes (limit: 5 levels) ❌ Use overly complex macros (hard to debug) ❌ Duplicate definitions across include files ❌ Use circular includes ❌ Rely on macro side effects

Example: Well-Organized Includes

project/
├── src/
│   └── main/
│       └── pli/
│           ├── CUSTPROC.pli          (main program)
│           └── includes/
│               ├── COMMON.pli        (common constants)
│               ├── CUSTDEF.pli       (customer structures)
│               └── SQLDEF.pli        (SQL declarations)

CUSTPROC.pli:

 CUSTPROC: PROCEDURE OPTIONS(MAIN);

   %INCLUDE includes/COMMON;
   %INCLUDE includes/CUSTDEF;
   %INCLUDE includes/SQLDEF;

   /* Program logic */

 END CUSTPROC;

2.4 Compiler Diagnostics and Messages

2.4.1 Diagnostic Message Format

Format: <severity>:<code> <file>(<line>,<col>): <message>

2.4.2 Severity Levels

  • I — Informational
  • W — Warning
  • E — Error
  • S — Severe error

2.4.3 Common Compiler Messages

Catalog of compiler-generated messages organized by category (syntax errors, semantic errors, type mismatches, unsupported features, etc.).

2.5 Helper System (JSON Configuration)

2.5.1 Overview

The Helper JSON system provides custom code generation overrides to handle edge cases and customer-specific requirements without modifying the compiler.

2.5.2 Helper Categories

Category Purpose
classes Ordinal interface name mapping
methods Method name and strategy overrides
attributes Variable declaration replacements
params Parameter expression overrides
java Substitute with custom Java class
override Replace generated Java methods
additionalMethods Add helper methods to generated classes
struct Replace structure field declarations
init Array initialization patching
symbols Type override for symbols
sqlProc SQL parameter direction (IN/OUT/INOUT)
refactor Move inner class nesting
sharedParam Promote parameter to instance variable
callByRef Wrap arguments for pass-by-reference
statements Replace specific statements

2.5.3 Helper Configuration Examples

The Helper JSON system allows customization of the Java code generation process without modifying the PL/I source or recompiling the compiler. This section provides real-world examples of each helper type.

Example 1: Class Helper - Ordinal Interface Naming

Use Case: Define custom names for ordinal interfaces generated from PL/I DEFINE ORDINAL statements.

PL/I Source:

define ordinal errorCode(
    FCBA01
    ,FCBA02
);

Helper JSON:

{
  "classes": {
    "errorCode": {"camelCase": "ErrorCode"}
  }
}

Generated Java (without helper):

public interface errorcode { ...

Generated Java (with helper):

public interface ErrorCode { ...


Example 2: Method Helper - Method Strategy Configuration

Use Case: Control how methods are generated (Java class, Java method, instance class, or main method) and provide custom implementations.

Helper JSON:

{
  "methods": {
    "p9dcfse": {
      "camelCase": "p9dcfse",
      "strategy": "JavaInstanceClass",
      "replacement": ""
    },
    "ptrNotNullNotSysnull": {
      "camelCase": "ptrnotnullnotsysnull",
      "strategy": "JavaMethod",
      "replacement": ""
    }
  }
}

Effect: - p9dcfse generates as an instance class (separate .java file) - ptrNotNullNotSysnull generates as a regular Java method


Example 3: Attribute Helper - Variable Declaration Override

Use Case: Override how a variable is declared in the generated Java class, useful for custom initialization or type changes.

PL/I Source:

DCL 1 BASE_VAR,
    3 STR CHAR(08);

Helper JSON:

{
  "attributes": {
    "BASE_VAR": {
      "camelCase": "baseVar1",
      "exclude": false,
      "scope": "JavaInstanceClass",
      "replacement": "Base_var baseVar2 = new Base_var()"
    }
  }
}

Generated Java (without helper):

@Base() protected Base_var baseVar1 = new Base_var();

Generated Java (with helper):

@Base() protected Base_var baseVar2 = new Base_var();

Important Notes: - No modifier keywords (public/private/protected) should be included in replacement - No semicolon at the end of the replacement statement - Changing the variable name in replacement does not update references to it


Example 4: Param Helper - Function Parameter Replacement

Use Case: Override how function parameters are passed, often used to simplify pointer operations.

PL/I Source:

CALL P9NPFCL(ADDR(MQ_CLO_PARMS));

Helper JSON:

{
  "params": {
    "Builtin.ADDR(mq_clo_parms)": {
      "replacement": "mq_clo_parms"
    }
  }
}

Generated Java (without helper):

P9npfcl.cicsEntry(Builtin.ADDR(mq_clo_parms), _transenv);

Generated Java (with helper):

P9npfcl.cicsEntry(mq_clo_parms, _transenv);


Example 5: Java Helper - Custom Method Implementation

Use Case: Replace a PL/I procedure with custom Java implementation from a helper file.

Helper JSON:

{
  "java": {
    "IS_DRITTLAND": {
      "javaClass": "P1npst.java",
      "javaMethod": "not important"
    },
    "A20_LESEN_EINGANG_QUEUE": {
      "javaClass": "P1npst.java",
      "javaMethod": "not important"
    }
  }
}

Effect: - The compiler looks for IS_DRITTLAND method in helper/P1npst.java - The custom Java implementation replaces the transpiled version - Useful for optimizing critical methods or integrating with Java libraries


Example 6: Struct Helper - Structure Field Replacement

Use Case: Override structure field declarations and initialization, all fields must be specified.

Helper JSON:

{
  "struct": {
    "RF_TAB": [
      {
        "name": "max_rf",
        "type": "Integer",
        "instance": "P5npnm.rf_tab_elems"
      },
      {
        "name": "tab",
        "type": "Array<Tab>",
        "instance": "new Array<Tab>(P5npnm.rf_tab_elems, Tab.class, Object.class, new Tab(), \"TAB\")"
      }
    ]
  }
}

Effect: - All fields of RF_TAB structure are replaced - Custom initialization expressions can be provided - All fields must be listed, not just those being changed


Example 7: Init Helper - Array Initialization Patch

Use Case: Add extra initialization code for uninitialized arrays, often needed for complex array structures.

Helper JSON:

{
  "init": {
    "PIKSDAT4": {
      "code": "for(int i = 1; i < piksdat4.tab.size();i++) {\nPiksdat4.Tab tab = new Piksdat4.Tab();\ntab.fromBytes(\"\".getBytes());\npiksdat4.tab.set(tab,i);\n}"
    }
  }
}

Effect: - The code string is inserted as initialization logic - Useful for arrays of structures that need element-by-element initialization - Allows complex initialization that the compiler doesn't generate automatically


Example 8: SQL Proc Helper - SQL Parameter Directions

Use Case: Define input/output directions for SQL stored procedure parameters.

Helper JSON:

{
  "sqlProc": {
    "HST.P7HS020": [
      {"param": ":HS020_HDL_NR", "io": "IN"},
      {"param": ":HS020_VZWEIG", "io": "IN"},
      {"param": ":HS020_F_TEXT", "io": "OUT"},
      {"param": ":HS020_F_INFO", "io": "OUT"}
    ]
  }
}

Effect: - SQL builder knows which parameters are input, output, or both (INOUT) - Affects how parameters are bound and retrieved from SQL calls - Critical for correct stored procedure invocation


Example 9: Statement Helper - Statement Replacement

Use Case: Replace specific PL/I statements with custom Java code.

Helper JSON:

{
  "statements": {
    "DATUM = SUBSTR(H_DATUM,1,8) ;": {
      "replacement": "datum = Builtin.SUBSTR(h_datum, 1, 8);"
    },
    "AKT_DATUM = TRANSLATE('AB.CD.EFGH',DATUM,'EFGHCDAB');": {
      "replacement": "akt_datum = Builtin.TRANSLATE(\"AB.CD.EFGH\", datum, \"EFGHCDAB\");"
    },
    "X=ANLSTAT((L+R)/2).KEY;": {
      "replacement": "x = anlstat[(l + r) / 2].key;"
    }
  }
}

Important Notes: - The key must be the exact PL/I statement including whitespace - Does not work reliably for statements inside if/else clauses - For complex cases, use the override helper instead


Example 10: CallByRef Helper - Pass Parameters by Reference

Use Case: Wrap function parameters in an array to enable pass-by-reference semantics.

PL/I Source:

getConsignmentPflicht: proc(h_deloccat,
    h_deloc,
    h_delmarket,
    consignmentPflicht);

call getConsignmentPflicht(dclvnppfg.ghdlcat,
    dclvnppfg.ghdl,
    h_delmarket,
    consignmentPflicht);

Helper JSON:

{
  "callByRef": {
    "getconsignmentpflicht": {
      "name": "getconsignmentpflicht"
    }
  }
}

Generated Java:

// Wrap params at call site
ArrayList<Object> _paramsByRefList3 = new ArrayList<>(
    Arrays.asList(dclvnppfg.deloccat, dclvnppfg.deloc,
                  h_delmarket, consignmentpflicht)
);
getconsignmentpflicht(_paramsByRefList3);

// Unwrap params in method signature
private void getconsignmentpflicht(ArrayList<Object> _paramsByRefList) {
    // Access via _paramsByRefList.get(0), etc.
}

Effect: - Parameters wrapped in ArrayList at call site - Method signature changed to accept ArrayList - Enables modification of parameters inside the method to affect the caller


2.5.4 Helper JSON Best Practices

File Location: Place helper JSON files in the helper/ directory relative to the compiled PL/I program.

File Naming: Typically named <ProgramName>.helper.json to match the PL/I program.

Compilation: Helpers are applied during the transpilation phase (PL/I → Java).

Testing: After adding helpers: 1. Clean and rebuild the project 2. Verify the generated Java code in target/generated-sources/ 3. Run unit tests to ensure behavior is correct

Common Pitfalls: - Statement helper keys must match PL/I source exactly (including whitespace) - Attribute replacement cannot change variable names effectively - Struct helper requires all fields to be specified - Statement helper doesn't work inside if/else blocks

When to Use Helpers: - ✅ Optimize critical methods with custom Java implementations - ✅ Fix incorrect code generation without modifying the compiler - ✅ Integrate with Java libraries and frameworks - ✅ Initialize complex data structures - ✅ Work around temporary compiler limitations - ❌ Avoid over-reliance; fix root causes in the compiler when possible