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
$MetadataOffsetsstatic 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.
| 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.
| 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.
| 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.
| 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.
| Parameter | Type | Description |
|---|---|---|
| length | int | Number of bits (1-32767) |
Byte size calculation: (length + 7) / 8
Example:
@HARRAY¶
Specifies array dimensions.
| 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).
| Parameter | Type | Description |
|---|---|---|
| locator | String | Name of POINTER field providing address |
@Defined¶
Marks a DEFINED variable (memory overlay).
| Parameter | Type | Description |
|---|---|---|
| storage | String | Name of field this field overlays |
@Derived¶
Marks a DERIVED variable (computed from parent).
Control Annotations¶
@Ignore¶
Excludes a field from offset calculation and metadata generation.
@OFFSET¶
Internal annotation added by Manifold to nested classes.
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¶
- Scan class hierarchy
- Start from root Group class
- Traverse parent classes first
-
Process fields in declaration order
-
For each field:
- Read type annotations (
@CHAR,@INTEGER, etc.) - Calculate byte size based on type
- Check for
@Ignore(skip if present) - Check for
@UNION(reuse previous offset) - Check for
@Defined(use storage field's offset) -
Add field to offset table
-
Handle nested structures:
- Recursively process nested Group classes
- Calculate nested class total size
- Add
@OFFSETannotation to nested class -
Flatten nested offsets into parent's table
-
Handle arrays:
- Multiply element size by array extent
- 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¶
- All fields start at offset 0 (relative to structure start)
- Structure size = size of largest field
- Only one field should be active at runtime
- 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:
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¶
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:
- Calculated Offsets:
- CUST_ID: 0
- NAME: 10
- ADDRESS: 40
- ADDRESS.STREET: 40
- ADDRESS.CITY: 65
- BALANCE: 80
-
Total size: 89 bytes
-
Generated Metadata:
- Offset table for runtime access
- Structure size information
-
Field relationship data
-
Property Accessors:
- Getter and setter methods for each field
- Proper type handling
- 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