Skip to content

Part 4.5 — Manifold Enhancement

4.5.1 Overview

Manifold Enhancement is a critical compile-time component of the PLI Platform that enhances Java code generated by the PLI Compiler. It runs as a javac plugin during compilation and ensures mainframe-compatible memory layout, generates metadata, and creates property accessors.

Why Manifold is Essential

When the PLI Compiler transpiles PL/I source to Java, the generated code contains: - Structure classes (extending Group) with field annotations (@CHAR, @INTEGER, etc.) - Transaction programs (extending ETPBase) - References to PLI runtime types

However, this generated code is incomplete without Manifold processing:

Without Manifold With Manifold
Fields have annotations but no offsets Byte-level offsets calculated
No metadata for runtime serialization $MetadataOffsets table generated
No getter/setter methods Complete property accessors
No mainframe memory compatibility EBCDIC-compatible memory layout

Manifold runs at compile-time (during javac) and has ZERO runtime overhead.

Integration in the Build Pipeline

PL/I Source
┌──────────────────┐
│ PLI Compiler     │ → Generates Java source + annotations
└────────┬─────────┘
   Java Source (.java)
   + @CHAR, @INTEGER, @DECIMAL annotations
┌──────────────────┐
│ javac compiler   │
│ + Manifold       │ → Compile-time enhancement
│   Plugin         │    • Calculate offsets
│                  │    • Generate metadata
│ -Xplugin:        │    • Create getters/setters
│  Manifold        │
└────────┬─────────┘
   Enhanced .class files
   + $MetadataOffsets
   + getMetadataOffset() method
   + Complete property accessors
   Runtime Execution (JVM + PLI Runtime)

4.5.2 Manifold Processing Phases

Manifold hooks into the Java compiler using the Manifold framework and processes code in three phases:

ENTER Phase

  • PropertyProcessor scans the compilation tree
  • Identifies classes extending Group (PLI structures)
  • Identifies classes extending ETPBase (transaction programs)
  • Begins property generation

ANALYZE Phase

  • BaseAnnotationProcessor traverses class hierarchies
  • Reads field annotations (@CHAR, @INTEGER, @DECIMAL, @PIC, @HARRAY, @BIT)
  • Calculates byte offsets for each field
  • Handles complex scenarios:
  • Nested structures (recursive traversal)
  • UNION structures (shared offsets)
  • BASED variables (pointer-based storage)
  • DEFINED variables (memory overlays)
  • Array dimensions (@HARRAY)

GENERATE Phase

  • Generates $MetadataOffsets static field
  • Generates getMetadataOffset() method
  • Creates getter/setter methods for all properties
  • Validates metadata completeness
  • Reports errors for missing or invalid annotations

4.5.3 Annotation Reference

Manifold processes annotations defined in pli_runtime2 to calculate memory layout:

Core Type Annotations

@CHAR

Specifies character field size and type.

@CHAR(size, varying=false)
Parameter Type Description
size int Character length (1-32767)
varying boolean false = fixed-length, true = varying-length (VARCHAR)

Byte size calculation: - Fixed: size bytes (one byte per character for SBCS) - Varying: size + 4 bytes (4-byte length prefix + data)

Example:

@CHAR(10) public String customerName;        // 10 bytes
@CHAR(100, varying=true) public String addr;  // 104 bytes

@INTEGER

Specifies integer field size.

@INTEGER(bytes)
Parameter Type Description
bytes int 1, 2, 4, or 8 bytes

Byte size: Exactly bytes specified

Mapping: - 1 byte → BIN FIXED(7) - 2 bytes → BIN FIXED(15) - 4 bytes → BIN FIXED(31) - 8 bytes → BIN FIXED(63)

Example:

@INTEGER(4) public Integer accountNumber;   // 4 bytes
@INTEGER(8) public Long transactionId;       // 8 bytes

@DECIMAL

Specifies packed decimal field.

@DECIMAL(precision, scale)
Parameter Type Description
precision int Total digits (1-31)
scale int Decimal places (0-precision)

Byte size calculation: (precision + 1) / 2 + 1 - Packed decimal uses 4 bits per digit + 1 byte for sign

Example:

@DECIMAL(15, 2) public BigDecimal amount;    // DEC FIXED(15,2) → 9 bytes
@DECIMAL(9, 0) public BigDecimal quantity;   // DEC FIXED(9,0) → 5 bytes

@PIC

Specifies PICTURE clause format.

@PIC(format)
Parameter Type Description
format String PICTURE specification (e.g., "999V99", "ZZZZ9.99")

Byte size: Calculated from PICTURE specification - '9' = digit (1 byte) - 'V' = implied decimal (0 bytes) - 'Z' = zero-suppressed digit (1 byte) - '.' = decimal point (1 byte) - '$', ',', etc. = edit characters (1 byte each)

Example:

@PIC("999V99") public Picture price;         // 5 bytes
@PIC("ZZZZ9.99") public Picture formatted;   // 8 bytes

@BIT

Specifies bit string size.

@BIT(length)
Parameter Type Description
length int Number of bits (1-32767)

Byte size calculation: (length + 7) / 8

Example:

@BIT(1) public Boolean flag;                 // 1 byte
@BIT(16) public BitN status;                 // 2 bytes

@HARRAY

Specifies array dimensions.

@HARRAY(type=..., lowerBound=..., extent=..., lowerBound2=..., extent2=...)
Parameter Type Description
type String Element type (e.g., "CHAR", "INTEGER")
lowerBound int First dimension lower bound
extent int First dimension size
lowerBound2 int Second dimension lower bound (optional)
extent2 int Second dimension size (optional)

Byte size calculation: elementSize × extent × extent2 × ...

Example:

@HARRAY(type="CHAR", lowerBound=1, extent=10)
@CHAR(20)
public Array items;  // 10 elements × 20 bytes = 200 bytes

Storage Relationship Annotations

@UNION

Marks a structure where fields share memory.

@UNION
public class MyUnion extends Group {
    @CHAR(10) public String field1;    // Offset 0
    @INTEGER(4) public Integer field2;   // Offset 0 (shares memory)
}

Effect: All fields in a UNION have the same offset (0 relative to parent).

@Based

Marks a BASED variable (pointer-based storage).

@Based(locator="ptrField")
public Group basedStruct;
Parameter Type Description
locator String Name of POINTER field providing address

@Defined

Marks a DEFINED variable (memory overlay).

@Defined(storage="baseField")
public String overlayField;
Parameter Type Description
storage String Name of field this field overlays

@Derived

Marks a DERIVED variable (computed from parent).

@Derived(parent="parentField")
public Group derivedStruct;

Control Annotations

@Ignore

Excludes a field from offset calculation and metadata generation.

@Ignore
public transient String tempField;  // Not included in $MetadataOffsets

@OFFSET

Internal annotation added by Manifold to nested classes.

@OFFSET(totalSize)
public static class NestedStruct extends Group { ... }

Note: This is generated automatically - do not add manually.


4.5.4 Metadata Generation

Manifold generates two critical artifacts for each Group class:

$MetadataOffsets Static Field

A string array containing field-name → byte-offset pairs.

Generated code:

@Ignore
private static String[] $MetadataOffsets = new String[] {
    "mygroup.field1", "0",
    "mygroup.field2", "10",
    "mygroup.field3", "14",
    "mygroup.nestedstruct", "18",
    "mygroup.nestedstruct.subfield1", "18",
    "mygroup.nestedstruct.subfield2", "28"
};

Format: Alternating field path and byte offset - Paths are lowercase, dot-separated - Offsets are absolute byte positions from start of structure - Nested structures are flattened

getMetadataOffset() Method

Provides runtime access to field offsets.

Generated code:

@Override
public Object getMetadataOffset() {
    if (this.$MetadataOffsets == null) {
        this.$MetadataOffsets = calculateMetadataOffset();
    }
    return this.$MetadataOffsets;
}

Usage:

Group myGroup = new MyGroup();
int offset = Group.getMetadataOffset(myGroup, "mygroup", "field2");  // Returns 10


4.5.5 Property Generation

Manifold generates getter and setter methods for all fields in Group classes.

Getter Method Pattern

public <Type> get<FieldName>() {
    this.<fieldName> = (<Type>)getValue(
        <Type>.class,
        getMetadataOffset("<className>", "<fieldName>"),
        this.<fieldName>,
        "<fieldName>",
        <size>
    );
    return this.<fieldName>;
}

Example:

@CHAR(10)
public String customerName;

// Manifold generates:
public String getCustomerName() {
    this.customerName = (String)getValue(
        String.class,
        getMetadataOffset("customer", "customername"),
        this.customerName,
        "customerName",
        10
    );
    return this.customerName;
}

Setter Method Pattern

public void set<FieldName>(<Type> $<fieldName>) {
    this.<fieldName> = $<fieldName>;
    this.<fieldName> = (<Type>)setValue(
        <Type>.class,
        getMetadataOffset("<className>", "<fieldName>"),
        this.<fieldName>,
        "<fieldName>",
        <size>
    );
}

Example:

public void setCustomerName(String $customerName) {
    this.customerName = $customerName;
    this.customerName = (String)setValue(
        String.class,
        getMetadataOffset("customer", "customername"),
        this.customerName,
        "customerName",
        10
    );
}

getValue() and setValue() Methods

These methods in the Group base class handle: - Serialization/deserialization to byte arrays - BASED/DEFINED variable storage management - EBCDIC charset encoding - Memory layout compatibility


4.5.6 Offset Calculation Algorithm

Manifold calculates offsets using a recursive traversal algorithm:

Algorithm Steps

  1. Scan class hierarchy
  2. Start from root Group class
  3. Traverse parent classes first
  4. Process fields in declaration order

  5. For each field:

  6. Read type annotations (@CHAR, @INTEGER, etc.)
  7. Calculate byte size based on type
  8. Check for @Ignore (skip if present)
  9. Check for @UNION (reuse previous offset)
  10. Check for @Defined (use storage field's offset)
  11. Add field to offset table

  12. Handle nested structures:

  13. Recursively process nested Group classes
  14. Calculate nested class total size
  15. Add @OFFSET annotation to nested class
  16. Flatten nested offsets into parent's table

  17. Handle arrays:

  18. Multiply element size by array extent
  19. Support multi-dimensional arrays

Offset Calculation Example

public class CustomerRecord extends Group {
    @CHAR(10) public String customerId;        // Offset 0, size 10
    @CHAR(50) public String name;               // Offset 10, size 50
    @DECIMAL(15, 2) public BigDecimal balance;  // Offset 60, size 9
    public Address address;                     // Offset 69, size = (from nested)
}

@OFFSET(100)  // ← Added by Manifold after calculation
public static class Address extends Group {
    @CHAR(30) public String street;            // Offset 0 (relative), 69 (absolute)
    @CHAR(20) public String city;              // Offset 30 (relative), 99 (absolute)
    @CHAR(10) public String zip;               // Offset 50 (relative), 119 (absolute)
    // Total size: 60 bytes
}

// Generated $MetadataOffsets:
private static String[] $MetadataOffsets = new String[] {
    "customerrecord.customerid", "0",
    "customerrecord.name", "10",
    "customerrecord.balance", "60",
    "customerrecord.address", "69",
    "customerrecord.address.street", "69",
    "customerrecord.address.city", "99",
    "customerrecord.address.zip", "119"
};
// Total CustomerRecord size: 129 bytes

4.5.7 UNION Structure Handling

UNION structures (fields sharing memory) require special handling:

UNION Example

@UNION
public class PaymentInfo extends Group {
    @CHAR(20) public String creditCardNumber;   // Offset 0
    @CHAR(20) public String checkNumber;         // Offset 0 (shares memory)
    @INTEGER(4) public Integer cashAmount;       // Offset 0 (shares memory)
}
// Total size: 20 bytes (largest field)

UNION Rules

  1. All fields start at offset 0 (relative to structure start)
  2. Structure size = size of largest field
  3. Only one field should be active at runtime
  4. Serialization depends on which field was last set

Generated Metadata for UNION

private static String[] $MetadataOffsets = new String[] {
    "paymentinfo.creditcardnumber", "0",
    "paymentinfo.checknumber", "0",        // Same offset
    "paymentinfo.cashamount", "0"          // Same offset
};

4.5.8 Integration with javac

Build Configuration (Gradle)

To enable Manifold processing, configure your build.gradle:

plugins {
    id 'java'
}

dependencies {
    implementation 'com.heirloom:pli_runtime2:26.2.27.RC1'
    annotationProcessor 'com.heirloom:heirloom-manifold:2.0.0'
    implementation 'systems.manifold:manifold-props-rt:2024.1.42'
}

tasks.withType(JavaCompile) {
    options.compilerArgs += [
        '-Xplugin:Manifold',
        '--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED',
        '--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED',
        '--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED',
        '--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED',
        '--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED',
        '--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED'
    ]
}

Build Configuration (Maven)

<dependencies>
    <dependency>
        <groupId>com.heirloom</groupId>
        <artifactId>pli_runtime2</artifactId>
        <version>26.2.27.RC1</version>
    </dependency>
    <dependency>
        <groupId>com.heirloom</groupId>
        <artifactId>heirloom-manifold</artifactId>
        <version>2.0.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <compilerArgs>
                    <arg>-Xplugin:Manifold</arg>
                    <arg>--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
                    <!-- Additional exports as shown in Gradle example -->
                </compilerArgs>
            </configuration>
        </plugin>
    </plugins>
</build>

Verification

After compilation with Manifold, verify enhancement:

# Decompile a Group class
javap -c -p MyGroup.class | grep -A5 '$MetadataOffsets'

# Should show:
#   private static java.lang.String[] $MetadataOffsets;

4.5.9 Troubleshooting

Common Issues

"OFFSET annotation is missing" warning

Cause: Nested Group class hasn't been processed for offset calculation.

Solution: - Ensure nested class extends Group - Verify class is not excluded by file filters - Check compilation logs for processing errors

Incorrect field offsets

Cause: Annotation values don't match actual field sizes.

Solution: - Verify @CHAR, @INTEGER, @DECIMAL values - Check for missing @Ignore on non-PLI fields - Review UNION handling if applicable

Generated methods not appearing

Cause: Method already exists or class doesn't extend Group.

Solution: - Verify class extends Group or ETPBase - Check if getter/setter already manually defined - Verify file isn't excluded by isJavaFileExcluded() filter

Compilation errors with Manifold

Cause: Missing javac exports or classpath issues.

Solution: - Add all required --add-exports flags - Ensure heirloom-manifold is in annotation processor path - Verify Java version (Java 11+ required)

Debug Logging

Enable Manifold debug logging:

javac -Xplugin:Manifold -J-Dmanifold.log.level=DEBUG MyClass.java

4.5.10 Best Practices

DO:

✅ Always use Manifold with PLI-compiled code ✅ Verify annotations match PL/I declarations ✅ Test offset calculations with sample data ✅ Use @Ignore for non-PLI fields (temp variables, caches) ✅ Run full compilation to ensure metadata generation

DON'T:

❌ Manually edit $MetadataOffsets - it's generated ❌ Add @OFFSET manually - Manifold adds it automatically ❌ Skip Manifold processing for performance - it's compile-time only ❌ Modify generated getter/setter methods - regenerate instead ❌ Mix PLI and non-PLI fields without @Ignore


4.5.11 Memory Layout and Offset Management

Manifold ensures that Java structures maintain exact byte-level compatibility with mainframe PL/I memory layout. This is critical for data interchange and migration scenarios.

Byte-Level Precision

When Manifold processes a structure, it calculates the exact byte offset for every field:

Example:

DCL 1 CUSTOMER_REC,
      2 CUST_ID    CHAR(10),      /* Bytes 0-9 */
      2 NAME       CHAR(30),      /* Bytes 10-39 */
      2 BALANCE    DEC FIXED(15,2); /* Bytes 40-48 */

Manifold ensures the Java representation uses the same byte offsets: - CUST_ID starts at byte 0 - NAME starts at byte 10 - BALANCE starts at byte 40 - Total structure size: 49 bytes

Why This Matters

Mainframe Compatibility: - Binary data files can be exchanged between mainframe and Java - No data conversion required - Byte-for-byte compatibility with mainframe memory

Migration Benefits: - Side-by-side comparison with mainframe output - Gradual migration without breaking data integration - Validation that Java behaves identically to mainframe

Data Interchange: - EBCDIC-encoded data can be read/written correctly - Structure serialization matches mainframe format - Network data exchange with mainframe systems

Nested Structure Offsets

Manifold handles nested structures by calculating cumulative offsets:

DCL 1 ORDER_REC,
      2 ORDER_ID   CHAR(10),     /* Offset: 0 */
      2 CUSTOMER,                /* Offset: 10 */
        3 CUST_ID  CHAR(10),     /* Offset: 10 (within parent) */
        3 NAME     CHAR(30),     /* Offset: 20 (within parent) */
      2 AMOUNT     DEC FIXED(15,2); /* Offset: 50 */

Result: Every field has a precise absolute byte position from the structure start.


4.5.12 LIKE Attribute Support

The PL/I LIKE attribute allows structures to copy the layout of another structure. Manifold provides enhanced support for this feature.

LIKE Concept

DCL 1 CUSTOMER_REC,
      2 CUST_ID    CHAR(10),
      2 NAME       CHAR(30);

DCL 1 ARCHIVED_CUSTOMER LIKE CUSTOMER_REC;

ARCHIVED_CUSTOMER gets the exact same field layout as CUSTOMER_REC.

Manifold Enhancements

Structure Reference Preservation: - LIKE'd structures maintain their relationship to the original - Field assignments preserve structure references - Runtime correctly resolves offsets for LIKE'd fields

Benefits: - Code reuse through structure templates - Consistent memory layout across related structures - Proper handling of complex nested LIKE relationships


4.5.13 Type Size Calculation

Manifold calculates the byte size for each PL/I data type to ensure accurate memory layout:

PL/I Type Byte Size
CHAR(n) n bytes
CHAR(n) VARYING n + 4 bytes (includes length prefix)
BINARY FIXED(1-15) 2 bytes
BINARY FIXED(16-31) 4 bytes
DECIMAL FIXED(p,q) Calculated based on precision
PICTURE Calculated from picture specification
BIT(n) (n + 7) / 8 bytes
Arrays Element size × array extent
Nested Structures Sum of all field sizes

Precision Guarantee: - Matches mainframe byte-for-byte - Accounts for packed decimal compression - Handles varying-length strings correctly - Calculates multi-dimensional arrays accurately


4.5.14 Complete Example: PL/I to Enhanced Java

Original PL/I Structure

DCL 1 CUSTOMER_REC,
      2 CUST_ID    CHAR(10),
      2 NAME       CHAR(30),
      2 ADDRESS,
        3 STREET   CHAR(25),
        3 CITY     CHAR(15),
      2 BALANCE    DEC FIXED(15,2);

After Manifold Enhancement

Manifold transforms this into Java with:

  1. Calculated Offsets:
  2. CUST_ID: 0
  3. NAME: 10
  4. ADDRESS: 40
  5. ADDRESS.STREET: 40
  6. ADDRESS.CITY: 65
  7. BALANCE: 80
  8. Total size: 89 bytes

  9. Generated Metadata:

  10. Offset table for runtime access
  11. Structure size information
  12. Field relationship data

  13. Property Accessors:

  14. Getter and setter methods for each field
  15. Proper type handling
  16. Memory synchronization

Memory Layout Result

┌──────────────────────────────────────────────────────────┐
│ Byte 0-9:   CUST_ID      (10 bytes)                      │
│ Byte 10-39: NAME         (30 bytes)                      │
│ Byte 40-64: STREET       (25 bytes)                      │
│ Byte 65-79: CITY         (15 bytes)                      │
│ Byte 80-88: BALANCE      (9 bytes, packed decimal)       │
│ Total:      89 bytes                                     │
└──────────────────────────────────────────────────────────┘

Key Benefits: - ✅ Exact mainframe memory layout - ✅ Zero runtime overhead (compile-time only) - ✅ Automatic property generation - ✅ Type-safe field access - ✅ Mainframe data interchange compatible