Blog › ICP guides
PL/I developer on retainer: FIXED DECIMAL precision truncation, ON conditions, PICTURE clauses, IBM z/OS, and PL/I on monthly retainer
October 2, 2026 · ~13 min read
A PL/I developer was maintaining an IBM z/OS batch program at a financial services company. The program allocated monthly fee amounts across customer accounts. The per-account fee was declared as FIXED DECIMAL(15,2). The calculation divided a total fee amount by the number of accounts: per_account_fee = total_fee / account_count. When total_fee was FIXED DECIMAL(15,2) and account_count was FIXED DECIMAL(7,0), PL/I computed the intermediate result at the IBM-defined precision for DIVIDE (following IBM Enterprise PL/I rules), then assigned to FIXED DECIMAL(15,2).
For fee amounts that produced a repeating decimal — for example, total_fee = 100.00, account_count = 3, giving 33.333… — PL/I silently truncated at 2 decimal places to $33.33. The program allocated $33.33 × 3 = $99.99, leaving $0.01 unallocated per fee cycle. Four fee allocation records were wrong before the fix.
The fix: changed per_account_fee to FIXED DECIMAL(15,4) to preserve intermediate precision, then rounded at output using the ROUND built-in: ROUND(per_account_fee, 2). Wrong amounts: 4 → 0.
PL/I’s FIXED DECIMAL precision arithmetic rules are the root cause. The result scale of a DIVIDE is implementation-defined; under IBM Enterprise PL/I on z/OS, the result precision is computed from the precision and scale of both operands and the scale can reach 15. When the result exceeds the declared precision of the receiving variable, PL/I truncates — it does not raise an error by default unless FIXEDOVERFLOW is enabled in an ON unit. No exception fires. No log message is written. The allocation simply comes up short at month-end reconciliation.
PL/I overview: IBM’s attempt to unify FORTRAN and COBOL (1964)
PL/I (Programming Language One) was designed at IBM’s Hursley and Endicott laboratories in 1964 with the ambition of being a single language for both scientific computing (FORTRAN’s domain) and business data processing (COBOL’s domain). The IBM 7030 Stretch project and the emerging System/360 architecture created pressure for a unified language that could handle floating-point numerical analysis and fixed-point decimal financial arithmetic in the same program. PL/I was IBM’s answer.
PL/I introduced block structure (derived from ALGOL 60), decimal arithmetic (derived from COBOL), exception handling via ON conditions, multitasking with TASK and EVENT variables, and a rich set of built-in functions including string manipulation, mathematical functions, and storage management primitives. Unlike COBOL’s static typing via PICTURE clauses or FORTRAN’s implicit type rules based on variable name prefixes, PL/I used a default-attribute system: variables not explicitly declared get default attributes based on their first letter. Variables beginning with I through N default to FIXED BINARY; variables beginning with other letters default to FLOAT DECIMAL. Undeclared variables are therefore a common source of type bugs — a variable expected to be FIXED DECIMAL that begins with “N” silently becomes FIXED BINARY and produces wrong values in decimal arithmetic.
PL/I was the primary language for IBM System/360 and System/370 applications through the 1970s and 1980s. Large banks, insurance companies, utilities, and government agencies wrote batch processing programs in PL/I during this period. IBM Enterprise PL/I for z/OS (versions 4.x through 6.x) is the current production compiler. Open PL/I, part of the Micro Focus COBOL product line, provides PL/I support on non-z/OS platforms including Linux and Windows. The Enterprise PL/I and Open PL/I compilers differ in their default attribute behaviors and precision arithmetic rules, making compiler-version scope an important item in any retainer agreement.
PL/I retainer work today exists at banks, insurance companies, utilities, and government agencies that run batch programs written in the 1970s through 1990s on IBM z/OS mainframes. These programs process account transactions, calculate insurance premiums, generate regulatory reports, and allocate fee income. They work; the organizations that depend on them have no incentive to replace them; and the maintenance burden falls on consultants who understand both IBM Enterprise PL/I language semantics and the financial domain.
FIXED DECIMAL precision arithmetic: the silent truncation hazard
IBM Enterprise PL/I defines precision rules for each arithmetic operation on FIXED DECIMAL values. For FIXED DECIMAL(p1,q1) operated on with FIXED DECIMAL(p2,q2): ADD and SUBTRACT produce a result with scale q = max(q1, q2) and precision p = max(p1-q1, p2-q2) + q + 1; MULTIPLY produces scale q = q1 + q2 and precision p = p1 + p2; DIVIDE produces a result whose precision is implementation-defined by IBM — the scale can be as large as 15 on z/OS, consuming the entire precision budget. These rules mean that intermediate results in complex expressions accumulate more decimal places than the programmer may expect.
The hazard arises at assignment. When the computed result is assigned to a FIXED DECIMAL(p,q) variable with smaller scale than the arithmetic result, fractional digits beyond q are truncated, not rounded. No exception is raised unless FIXEDOVERFLOW is active via an ON FIXEDOVERFLOW unit. The program continues with the truncated value. For a fee allocation that divides $100.00 among 3 accounts, the truncated $33.33 per account sums to $99.99, not $100.00. The $0.01 shortfall accumulates silently across every fee cycle that involves a repeating decimal division.
The fix for financial calculations has three components. First, declare intermediate accumulator variables with at least 4 decimal places: DCL per_account_fee FIXED DECIMAL(15,4) instead of FIXED DECIMAL(15,2). This preserves the fractional precision through the arithmetic. Second, use the ROUND built-in function before storing or displaying final results: ROUND(per_account_fee, 2) rounds to 2 decimal places using standard 5-up rounding instead of truncating. Third, enable ON FIXEDOVERFLOW(unit) during testing to detect any remaining truncations that the precision-expansion fix did not address: the ON unit fires when a FIXED arithmetic result exceeds the declared precision of the receiving variable, surfacing silent truncations that have been accumulating.
Work log entry: “per_account_fee FIXED DECIMAL(15,2): total_fee / account_count for 3 accounts produced 33.333…; assigned to FIXED DECIMAL(15,2) truncated to 33.33; allocation short by $0.01 per cycle; fix: changed to FIXED DECIMAL(15,4), added ROUND(per_account_fee,2) at output; wrong fee records: 4 → 0; 3h.”
ON conditions, PICTURE clauses, BASED storage, and PL/I multitasking
PL/I’s ON condition mechanism predates try/catch exception handling by 20 years. An ON statement establishes a handler for a named condition: ON FIXEDOVERFLOW(unit) fires when a FIXED arithmetic result overflows the receiving variable’s declared precision; ON ZERODIVIDE(unit) fires when a divisor is zero; ON CONVERSION(unit) fires when a string-to-number conversion fails (for example, a non-numeric character in a field expected to contain a decimal amount); ON SUBSCRIPTRANGE(unit) fires on array bounds violations when bounds-checking is enabled; ON ENDFILE(file)(unit) fires when a sequential file reaches end-of-data. The unit is a single statement or a BEGIN...END block that executes when the condition fires. SIGNAL condition; can raise a condition manually, which is useful for testing ON units without waiting for the actual error condition to occur in production data.
ON condition scoping follows PL/I block structure: an ON unit is active from the ON statement to the end of the block in which it was established. If an ON unit is defined inside a procedure that returns before the condition fires, the ON unit is no longer active when the condition fires in the calling code. For FIXEDOVERFLOW, the default system action when no ON unit is active is to continue execution with the truncated value — not to abort, not to raise an error. This makes missing ON units the silent failure mode: the program runs to completion with wrong financial amounts.
PICTURE clauses describe character-string numeric formats for display and output. PICTURE '9V99' represents a 3-digit number with 2 implied decimal places (V marks the implied decimal point position); 'Z(7)9.99' zero-suppresses leading digits with 7 zero-suppression positions; '$$$,$$9.99' formats a currency amount with a floating dollar sign that occupies the leftmost non-suppressed position and a comma separator. PICTURE variables in PL/I are character strings, not arithmetic types; assigning a FIXED DECIMAL value to a PICTURE variable converts it to the formatted string, and assigning a PICTURE variable to a FIXED DECIMAL value converts it back. Mismatches between the PICTURE string and the actual value (a value too large for the field, a non-numeric character in an input PICTURE field) trigger CONVERSION or PICTUREOVERFLOW conditions.
BASED storage is PL/I’s dynamic memory model. ALLOCATE T SET(p) allocates a new area of the size of type T and sets pointer p to it; FREE p->T releases the allocated area; a variable declared BASED(p) is accessed through pointer p and refers to whatever storage p currently points to. Multitasking: CALL proc TASK(t) starts a new task (a PL/I thread); WAIT(t) waits for task t to complete; EVENT variables track task completion status and can be tested with COMPLETION(event); multitasking is PL/I’s built-in model for mainframe parallel batch processing, allowing a batch job to overlap I/O-bound and CPU-bound work within a single job step.
Typical PL/I retainer work and what it looks like in a work log
FIXED DECIMAL precision truncation in financial arithmetic is the most common invisible retainer task. There is no runtime error, no exception, no wrong-value indicator — just amounts that are off by cents over thousands of transactions, discovered at month-end reconciliation when the total allocation does not match the total fee collected. The investigation requires understanding the IBM Enterprise PL/I precision rules for the specific operation (DIVIDE, MULTIPLY, or a compound expression), identifying which variable declarations have too few decimal places, counting the affected records across the entire fee cycle, applying the ROUND fix, and verifying the fix against all fee types that involve amounts producing repeating decimals. Work log entry: “per_account_fee FIXED DECIMAL(15,2): DIVIDE precision truncation; 4 wrong records; changed to FIXED DECIMAL(15,4); added ROUND(per_account_fee,2) at output; 4 → 0; 3h.”
ON condition missing or mis-scoped is the second category. FIXEDOVERFLOW not enabled during production runs means that a test run that enables it reveals truncations that have been silently accumulating for months. A common variant: an ON unit defined inside a procedure block that returns before the condition fires in a called subroutine, leaving the default system action (continue with truncated value) active where the developer expected the ON unit to handle it. Work log entry: “ON FIXEDOVERFLOW unit defined in CALC_FEES procedure; CALC_FEES returns before calling ALLOCATE_FEES; ON unit no longer active when FIXEDOVERFLOW fires in ALLOCATE_FEES; default action continued with truncated value; fix: moved ON FIXEDOVERFLOW unit to outer block; 6 additional wrong records surfaced in test run; after fix: 0; 2h.”
BASED pointer bugs are the third category. ALLOCATE without a matching FREE in a long batch run causes storage exhaustion: each pass through the main processing loop allocates a new record structure but never frees the previous one; after thousands of iterations the z/OS region size is exhausted and the job abends with S878. A more subtle variant: FREE called twice on the same pointer, leaving a dangling reference; the next BASED access through that pointer reads arbitrary memory from the freed area, producing a record with fields that contain whatever bytes happened to be in that memory location. Work log entry: “ALLOCATE ACCT_REC SET(p) in main loop; no matching FREE; region exhausted after 12,000 iterations; fix: added FREE p->ACCT_REC at end of loop body; storage exhaustion: resolved; 2h.”
Track PL/I developer retainer hours without the status emails
When a 3-hour session investigates a FIXED DECIMAL precision truncation bug — tracing the IBM Enterprise PL/I precision rules for a DIVIDE operation in a z/OS batch program, identifying which records were affected, and verifying the ROUND fix across all fee types — the session produces no visible code artifact for most of the investigation time. The work log must name the variable declaration, the precision arithmetic rule that truncated it, the affected record count, and the fix. HourTab gives your PL/I retainer client a public dashboard URL they can bookmark: hours used, hours remaining, and a work log that names the PL/I mechanism. No client login. No status emails. CSV in, URL out.
See HourTab pricing →How HourTab tracks PL/I developer retainer hours
PL/I retainer work is invisible by the same mechanism that makes FIXED DECIMAL arithmetic convenient: the language handles precision automatically, and when the automatic handling truncates rather than rounds, there is no signal. A batch program that has processed fee allocations correctly for years may have a latent truncation bug that never fires until a new fee type is added with amounts that produce a repeating decimal on division by the standard account grouping. The program runs normally for all previous fee types; it is silently wrong for the new one. The connection between a FIXED DECIMAL(15,2) declaration in a 1984 procedure and $0.01 shortfalls in a 2026 fee cycle requires understanding IBM Enterprise PL/I precision arithmetic rules and the specific division that triggers the truncation.
The work log needs to name the mechanism: which variable declaration, what the precision arithmetic rule was, how the truncation connected the DIVIDE result to a wrong financial amount, what the observable symptom was (allocation short, reconciliation mismatch, audit exception), and what the fix was. A log entry that says “fixed decimal precision bug, 3h” is not auditable. A log entry that says “per_account_fee FIXED DECIMAL(15,2): total_fee / account_count for repeating-decimal amounts truncated at 2 places; allocation $0.01 short per cycle; 4 wrong records; changed to FIXED DECIMAL(15,4); added ROUND(per_account_fee,2) at output; 4 → 0; 3h” is auditable.
HourTab gives PL/I developers a public retainer-hours URL they send to clients — financial services firms maintaining fee allocation batch programs, insurance companies running premium calculation jobs, and utilities processing billing transactions on IBM z/OS mainframes. For PL/I retainers, each work log entry should name the PL/I mechanism: which variable declaration, which precision arithmetic rule, which ON condition was missing or mis-scoped. Comparative context: PL/I retainer work has conceptual overlap with other environments where decimal arithmetic precision requires explicit attention — COBOL (the other z/OS batch language, with PICTURE clauses and similar decimal arithmetic that requires explicit ROUNDED phrases); Fortran (the scientific computing language PL/I was designed to replace, with floating-point rather than fixed-point precision hazards); and Ada (a later IBM-influenced language with similar fixed-point arithmetic semantics and explicit delta declarations). PL/I is uniquely positioned as the language that combined both decimal and binary arithmetic in a single program, making its precision truncation bugs the hardest to predict without reading the IBM Enterprise PL/I Language Reference precision rules directly.
FAQ: PL/I developer retainers
What does a PL/I developer on retainer typically do?
A PL/I developer on monthly retainer covers FIXED DECIMAL precision truncation diagnosis (identifying where a DIVIDE or MULTIPLY operation produces more decimal places than the receiving variable can hold; adding decimal places to intermediate accumulator declarations; applying the ROUND built-in before output); ON condition scoping maintenance (enabling FIXEDOVERFLOW or ZERODIVIDE ON units in test runs; diagnosing mis-scoped ON units defined in blocks that exit before the condition fires; correcting the default system action for conditions that continue with wrong values); BASED pointer and storage management (tracing ALLOCATE without matching FREE; diagnosing double-FREE dangling pointer reads); PICTURE clause maintenance (fixing currency formatting, zero-suppression formats, and implied decimal place mismatches); and IBM z/OS batch JCL and DD statement maintenance for programs running under IBM Enterprise PL/I for z/OS.
What PL/I work is most commonly underlogged?
FIXED DECIMAL precision truncation is the most systematically underlogged PL/I retainer work. A DIVIDE of FIXED DECIMAL(15,2) by FIXED DECIMAL(7,0) produces an intermediate result whose scale can reach 15 under IBM Enterprise PL/I rules; when assigned to a FIXED DECIMAL(15,2) variable, fractional digits beyond 2 places are truncated without any error unless FIXEDOVERFLOW is enabled. No exception fires. The symptom appears at reconciliation. Diagnosis requires understanding the IBM Enterprise PL/I precision rules for DIVIDE, identifying which variable declarations have too few decimal places, counting affected records, applying the ROUND fix, and verifying across all fee types. This is 3 to 6 hours of invisible investigation per occurrence. ON condition mis-scoping: an ON unit exits before the condition fires; default action continues with wrong value; 2 to 4 hours. BASED storage exhaustion: ALLOCATE without matching FREE in a loop; region exhausted after thousands of iterations; 2 to 3 hours.
What are typical PL/I developer retainer rates?
Entry-level PL/I developers with 1 to 2 years covering FIXED DECIMAL arithmetic, basic ON condition maintenance, and IBM z/OS batch upkeep typically bill at $65 to $115 per hour. Mid-level PL/I programmers with 2 to 4 years covering precision truncation diagnosis, BASED pointer storage management, PICTURE clause maintenance, and financial reconciliation investigation typically bill at $95 to $170 per hour. Senior PL/I developers with 4 or more years covering IBM Enterprise PL/I precision arithmetic rules, multitasking TASK/EVENT maintenance, and PL/I-to-COBOL or PL/I-to-Java migration planning typically bill at $140 to $255 per hour. Monthly retainer ranges: $800 to $1,800 per month for advisory engagements covering precision arithmetic review and ON condition guidance (8 to 15 hours per month); $1,800 to $5,000 per month for active batch program maintenance and bug investigation.
What should a PL/I developer retainer agreement include?
A retainer agreement should specify: compiler version scope (IBM Enterprise PL/I for z/OS 4.x through 6.x vs. Open PL/I under Micro Focus; different precision arithmetic rules and default attribute behaviors); precision arithmetic scope (whether FIXED DECIMAL truncation analysis is in scope; truncation bugs can exist in any DIVIDE or MULTIPLY in the program, not just recently modified code); ON condition scope (whether FIXEDOVERFLOW and ZERODIVIDE ON unit maintenance is in scope; whether enabling these in production is authorized); BASED storage scope (whether ALLOCATE/FREE lifecycle tracing is in scope; storage exhaustion in long batch runs requires tracing across an entire job stream); migration scope (whether PL/I-to-COBOL or PL/I-to-Java translation is in scope; migration requires identifying every ON condition behavior that must be replicated); and hour logging format (the variable declaration, the precision arithmetic rule, the affected record count, the fix applied, and the wrong-result count before and after).
How should PL/I developer retainer hours be logged?
Log each PL/I retainer session with: the variable declaration involved in the precision bug (e.g., per_account_fee FIXED DECIMAL(15,2)); the arithmetic operation that produced the truncated result (e.g., total_fee / account_count where total_fee is FIXED DECIMAL(15,2) and account_count is FIXED DECIMAL(7,0)); the IBM Enterprise PL/I precision rule that applied (e.g., DIVIDE result scale can reach 15 on z/OS; assigned to FIXED DECIMAL(15,2) truncated fractional digits beyond 2); the observable symptom (e.g., fee allocation $0.01 short per cycle for repeating-decimal amounts); the affected record count; the fix applied (e.g., changed to FIXED DECIMAL(15,4); added ROUND(per_account_fee,2) at output); and the wrong-result count before and after. For ON condition bugs: the condition name, the block where the ON unit was defined, why it exited before the condition fired, and the fix. For BASED pointer bugs: the ALLOCATE call, the missing FREE, the storage exhaustion symptom, and the fix.