Table of Contents
- Overview
- The PL/I scope model, briefly
- Method-locals vs. class fields
- Same-named declarations in different scopes
- Storage overlays:
DEFINEDandBASED - Recursive procedures
- Reading tips
- Summary
- Related articles
Audience: developers migrating PL/I to Java who want to understand where a declared variable "lives" in the generated code — why some variables become class fields and others stay method-locals, how same-named declarations are resolved, and how storage overlays and recursion are kept correct.
Overview
PL/I and Java scope work differently. In PL/I, a DECLARE belongs to the block (the external procedure, a nested PROCEDURE, or a BEGIN block) that contains it, and an inner block can see the names of the blocks that enclose it. Java has no nested-procedure concept at all: a class has fields, and methods have locals, and sibling methods cannot see each other's locals.
The compiler bridges that gap with a small, predictable set of rules. For each declaration it decides where the variable should live in the Java class so that the PL/I visibility and lifetime are preserved:
- A variable used only inside the procedure that declares it becomes a method-local.
- A variable that must be visible to a nested procedure becomes a class field, so the sibling methods can share it.
- When two declarations in different scopes share a name, they collapse to a single field, and the compiler keeps each one's storage semantics correct.
- Storage overlays (
DEFINED,BASED) and recursive procedures get extra handling so an overlay always points at the right storage and each activation sees its own values.
The rest of this article walks through each rule with before/after examples.
Note: The snippets use illustrative names. Your variable, procedure, and program names are preserved from your source (lower-cased per Java convention).
The PL/I scope model, briefly
Three kinds of blocks introduce scope:
- The external procedure — the program itself.
-
Nested procedures (
name: PROCEDURE … END name;) declared inside it. -
BEGINblocks.
A name declared in an outer block is visible to the blocks nested inside it, unless an inner block redeclares the same name — in which case the inner declaration shadows the outer one for the duration of that inner block. This is ordinary lexical scoping, and the compiler follows it when it resolves a reference: it looks in the current block first, then outward through the enclosing blocks.
Method-locals vs. class fields
The single most visible scope decision is whether a declared variable becomes a Java method-local or a class field.
A variable that is only referenced within the procedure that declares it stays a local:
PARSE: PROC;
DCL I BIN FIXED(15); /* used only inside PARSE */
DO I = 1 TO 100;
...
END;
END PARSE;
private void parse() {
Integer i = 0; // method-local
for (i = 1; i <= 100; i++) { ... }
}
But PL/I lets a nested procedure read and update a variable declared in an enclosing procedure. Java sibling methods cannot share a local, so the compiler promotes (hoists) such a variable to a class field. Both the declaring procedure and the nested procedure then reference the same field:
PARSE: PROC;
DCL COUNT BIN FIXED(15); /* declared here ... */
BUMP: PROC;
COUNT = COUNT + 1; /* ... but used by a nested proc */
END BUMP;
COUNT = 0;
CALL BUMP;
END PARSE;
private Integer count = 0; // hoisted to a class field (shared)
private void parse() {
count = 0;
bump();
}
private void bump() {
count = count + 1; // same field the outer proc uses
}
The rule is simply: shared with a nested procedure → field; otherwise → local. This keeps the generated class as lean as possible — only the variables that genuinely need to be shared are lifted out of their method.
Whether a field is an instance field or a static field depends on the compile strategy; see Static vs Instance Strategy.
Same-named declarations in different scopes
Because promotion lifts variables to the class, two declarations that are distinct in PL/I — same name, different blocks — can end up mapping to the same Java field name. When that happens the compiler collapses them to a single field: the first declaration establishes the field, and later same-named declarations reuse it rather than creating a duplicate.
DCL BUF CHAR(80); /* program-level */
FORMAT: PROC;
DCL BUF CHAR(80); /* a separate PL/I variable, same name */
...
END FORMAT;
private PLIString buf = new PLIString(80); // one field, first declaration wins
This matches how the program actually behaves when, as is common, only one of the declarations is ever really used at run time. The important part is what happens when the two declarations do not have identical attributes — specifically when the inner one is a storage overlay. That case is handled next.
Storage overlays: DEFINED and BASED
A DEFINED variable overlays the storage of another variable; a BASED variable overlays storage addressed by a pointer. Either way, reads and writes through the overlay must reach the same bytes as the base. In the generated Java this is expressed with a runtime .based(...) binding on the overlay:
DCL TEXT CHAR(800);
DCL C(800) CHAR(1) DEFINED TEXT; /* C overlays TEXT, byte for byte */
@Defined(storage = "text", pos = "0")
Array<PLIString> c = new Array<PLIString>(800, ...).based(text);
Reading c[i] now reads the i-th byte of text. The .based(text) call is what ties the two together; the @Defined annotation records the overlay for the runtime.
Overlays whose base is a parameter or per-call variable
An overlay is only meaningful relative to the specific storage it currently covers. When the base is a procedure parameter or another per-activation variable, that storage is different on every call. So if such an overlay is shared with a nested procedure (and therefore lives in a class field), the compiler re-establishes the binding at procedure entry, on each call, rather than once at construction:
RESOLVE: PROC(TEXT);
DCL TEXT CHAR(800);
DCL C(800) CHAR(1) DEFINED TEXT; /* overlays THIS call's TEXT */
...
END RESOLVE;
private void resolve(PLIString text) {
c = new Array<PLIString>(800, ...).based(text); // re-bound to this call's TEXT
...
}
This is what keeps an overlay correct even when its underlying storage is supplied fresh on each invocation. It applies to CHARACTER arrays and scalars and to picture (PIC) overlays alike.
Combined with the same-name rule above: if a program also has an unrelated program-level DCL C, the two collapse to one field — and the compiler still emits the per-call .based(...) binding inside RESOLVE, so the overlay reaches the right storage instead of being lost to the collision.
Recursive procedures
A RECURSIVE procedure can be active several times at once — an outer activation is suspended while an inner one runs. Its automatic variables are supposed to be per-activation: each call has its own copy, and when an inner call returns the outer call sees its values unchanged.
Method-locals already behave this way (each Java method call has its own locals), so nothing special is needed for them. But a variable that was promoted to a class field (because a nested procedure shares it) is single-storage by nature — a recursive call would otherwise overwrite the outer activation's value.
To preserve per-activation semantics, the compiler wraps a recursive procedure's body so it saves the shared fields on entry and restores them on exit:
private void resolve(PLIString text, Integer limit) {
Integer _rec_save_limit = this.limit; // save the caller's values
Array<PLIString> _rec_save_c = this.c;
try {
c = new Array<PLIString>(800, ...).based(text); // this activation's overlay
... // may CALL RESOLVE recursively
} finally {
this.c = _rec_save_c; // restore on return
this.limit = _rec_save_limit;
}
}
The save runs before the body, so each activation captures the caller's values; the restore runs on every exit path. The result is that the shared fields keep their convenient class-level visibility for nested procedures, while still behaving as proper per-activation storage under recursion — including mutual and indirect recursion.
Reading tips
- A local you expected as a field, or vice versa. Look for a nested procedure. A variable becomes a class field precisely when a nested procedure needs to see it; otherwise it stays a method-local.
-
A
.based(...)at the top of a method. That is a storage overlay (DEFINED/BASED) being (re)bound to the storage it covers for that call — most often because the base is a parameter. -
_rec_save_…locals with atry { … } finally { … }wrapper. That is a recursive procedure preserving per-activation values of its shared fields. - One field for two PL/I declarations. Same-named declarations in different scopes collapse to a single field; the storage semantics of the one that is actually used are preserved.
Summary
- Variables used only within their declaring procedure become method-locals; variables shared with a nested procedure are promoted to class fields.
- Same-named declarations in different scopes collapse to a single field (first declaration wins), with each declaration's storage semantics preserved.
-
DEFINED/BASEDoverlays are tied to their base with.based(...); when the base is a parameter or per-call variable, the binding is re-established at procedure entry on each call. -
RECURSIVEprocedures save on entry and restore on exit any shared fields, so promoted variables still behave as per-activation storage. - Together these rules reproduce PL/I scope and lifetime on the JVM while keeping the generated class readable.
Related articles
- How the Heirloom PL/I Compiler Translates Your Code — the pipeline that produces these classes.
- Reading Your Generated Java — a field guide to the generated idioms.
- Static vs Instance Strategy — whether shared variables become instance or static fields.
-
How the Heirloom PL/I Runtime Manages Memory — pointers,
BASED, and storage overlays on the JVM.
0 Comments