rpgc runs classic fixed-column RPG programs that rely on the fixed-logic program cycle - the implicit read/calculate/output loop driven by a primary input file, with no explicit READ and no DOW/DOU loop in the calculations.
A program is compiled as a cycle program when it declares a primary input
file (an F-spec with file designation P). The cycle then, on each iteration:
writes 1P heading and detail output, reads the next primary record (setting its
record-identifying indicator; end-of-file turns on LR), runs total-time
calculations and total output, tests LR to end the program, moves the record's
fields into the program field area, and runs detail-time calculations. Programs
with no primary file are unaffected and follow the ordinary linear path.
Supported in the cycle
-
A single program-described primary input file (
F ... IP F ... DISK) whose fields are defined on I-specs (from/to columns; a decimal-position digit marks a field numeric/zoned, otherwise alphanumeric). -
Multiple record types for the primary input file via I-spec record identification codes. Each record type declares its own record-identifying indicator and up to three identification codes (position, not-flag, type
C/Z/D, and character). The record-identification line is read at the columns of the source's dialect:Entry RPG II RPG IV Sequence 15-16 17-18 Number 17 19 Option 18 20 Record-identifying indicator 19-20 21-22 Identification codes 21-27, 28-34, 35-41 23-30, 31-38, 39-46 The RPG IV code slot is a column wider than the RPG II one because its position area takes five digits rather than four. The cycle tests each record type's codes against the incoming record in source order; the first match sets the corresponding indicator and uses that record type's field layout for field movement. Output lines conditioned on a record indicator print only for matching records, enabling distinct formatting per record type.
-
The PRTCTL(dsName) keyword on a PRINTER file declaration associates a data structure whose subfields supply dynamic space-before/after and skip-before/after values. The DS layout follows the IBM extended-length format:
spaceBefore char(3),spaceAfter char(3),skipBefore char(3),skipAfter char(3),currentLine zoned(3,0)(15 bytes total). After each EXCEPT output to the printer file, thecurrentLinesubfield is updated with the current line count based on thespaceAftervalue. Blank or zero space-after defaults to single spacing. -
A program-described printer file (
F ... O F ... PRINTER) with O-spec heading (H), detail (D), total (T) and exception (E) lines conditioned on1P, a record indicator (e.g.10,20,30),L1-L9,LR, an overflow indicator (OA-OG,OV), an external indicator (U1-U8), or a halt indicator (H1-H9) - optionally negated with a leadingN; output fields with end positions and edit codes; and literal constants. -
Detail-time calculations (C-specs with a blank control level). A detail (or total) calc may be conditioned by an indicator in cols 9-11, including a control-level indicator (
L1-L9) or an overflow indicator (OA-OG,OV) - the calc runs only on cycles where that indicator is on. A calc conditioned onL1therefore executes only at theL1control break. In RPG II/III source the three-indicator conditioning area (cols 9-17) andAN/ORconditioning-continuation lines (AN/ORin cols 7-8) are both honored: the indicators are ANDed, and eachORline starts a new alternative group. In RPG IV source they are not valid and are rejected with TRN1001 - useIF/ELSEIFexpressions instead. -
Total-time calculations (C-specs with control level L0-L9 or LR in cols 7-8). These run during the total-time phase of the cycle, after a control break is detected and before total-time output. Each total-time C-spec is conditioned on its level indicator and only executes when that indicator is on.
L0is always on, so anL0total calc runs at total time on every cycle (it is never gated by a control break). At LR, all level indicators cascade on, so L1-conditioned total calcs also run at end-of-file. -
The 1P first-page heading (printed once) and LR end-of-file termination with an LR-conditioned total line.
-
Control-break level indicators (L1-L9) on I-spec field-description lines (cols 59-60 in RPG II, cols 63-64 in RPG IV). When the value of a control field changes between records, the corresponding level indicator and all lower-level indicators are turned on (upward cascade). The first record read is itself a control break: every level indicator through the highest one used is turned on for it, so a calc or output conditioned on
L1-L9fires for that group. (Total-time output is bypassed for that cycle, so no spurious subtotal prints before the first detail line - there is no earlier group to total. On a matching-record cycle the first record to supply a given level can arrive on any cycle, and the bypass applies to whichever cycle that is.) At end-of-file (LR), all level indicators L1-L9 are turned on. Total-time and exception output and detail/total calcs can all be conditioned on any level indicator. Level indicators are also accessible as named indicators (*INL1-*INL9) in free-format and C-spec calculations - they can be read, set (including viaSETON/SETOFFin the resulting-indicator positions), and tested like*IN01-*IN99. -
O-spec Blank After (col 39 =
Bin RPG II, col 45 =Bin RPG IV) outputs a field and then clears it to its type default - zero for any numeric field (packed, zoned, integer, unsigned, float) or blanks for any character field (fixed, graphic, or variable-length), reset to the field's own declared length. This supports the common parallel-accumulation pattern where multiple accumulators are added to at detail time and reset at each control break. As on IBM i, Blank After is not permitted on date, time, or timestamp fields. -
Halt indicators (H1-H9) set via SETON in the resulting indicator positions. After heading/detail output, the cycle tests H1-H9; if any is on, the program terminates immediately (throws
RpgHaltException) without reading the next record or running total-time calculations and output. This models the operator-cancel path of IBM i's halt prompt. In headless execution there is no operator to answer the halt prompt, so the generatedmain()(viaRpgProgram.runHeadless()) treats the halt as the operator-cancel equivalent: it ends cleanly with a concise halt message on standard error and a non-zero exit code (RpgProgram.HALT_EXIT_CODE), never an uncaught stack trace. Output produced before the halt is preserved. Embedded callers that invokerun()directly still observe theRpgHaltExceptionand can handle it themselves. -
Printer overflow indicators (OA-OG, OV) assigned to a PRINTER file on the F-spec (cols 33-34 in RPG II). The cycle tracks a per-file line counter that increments with each printed line. When the count reaches or passes the overflow line (default 60, matching IBM i's standard 66-line page), the assigned overflow indicator is set on. On the next cycle iteration, heading output conditioned by the overflow indicator prints (page header reprint), then the indicator is cleared and the line counter resets for the new page. The overflow threshold is configurable via
setOverflowLine(int)on the compiled class, for a form shorter than the default. The L-spec (Line Counter Specification) can also set the overflow line:LQPRINT 66FL 55OLsets form length 66 and overflow line 55 for printer file QPRINT. FL/OL entries follow the filename (cols 7-14) as pairs of (3-digit value + 2-char type indicator). An overflow indicator can also be set or cleared explicitly withSETON/SETOFF(the manual page-eject idiom) and used to condition calcs or output like any other indicator. -
SETON/SETOFFresulting indicators accept the full indicator set -01-99,L1-L9,H1-H9,U1-U8,OA-OG/OV,LR, andRT. A bareSETONorSETOFFwith no resulting indicator does nothing and is rejected at compile time. -
Look-ahead fields via
**in the I-spec record-identifying indicator columns (19-20 in RPG II, 21-22 in RPG IV). A record identification line with**instead of a numeric record indicator designates a look-ahead record. Fields defined under that record are populated from the NEXT record that file will yield (peek ahead) after field movement for the current record and before detail-time calculations. Look-ahead applies to the primary file and any secondary file - a**record under a secondary's I-specs peeks that secondary's own next record, not the primary's. On a matching-record merge the peeked value is the record each file will next present, not the one after it. At end-of-file, look-ahead fields are reset to their type defaults (blank for alpha, zero for numeric). This supports the common pattern of comparing the current record's control field against the next record's value to detect group boundaries (e.g. using COMP to set an indicator when the group changes). -
Matching records (MR) with primary/secondary files. A program may declare one primary (
IP) input file and any number of secondary (IS) files, all sorted by their matching fields, designated M1 through M9 in I-spec cols 61-62 (RPG II) or 65-66 (RPG IV). The cycle merges records from every such file by the matching field.When several files hold the same key, they are processed in File Specification order: the primary first, then the first secondary, then the next, and so on.
A record may carry several matching-field levels at once, in which case they form a single key: M9 is the high-order (leftmost) portion and M1 the low-order (rightmost) one. That ordering is decided solely by the M-code - neither the order the fields are declared in on the I-specs nor their physical positions in the record affects it. So a record declaring
ACas M1 in columns 1-2 andCOas M2 in columns 3-4 merges onCOfirst, thenAC.The MR (matching record) indicator is on while processing a key held by the primary file and the secondary file; it is off for a key present on only one side. A record with no counterpart is still processed - a secondary record that matches no primary is not skipped - so output conditioned on
NMRsees it. When several records share a key, the primary is processed first (with stale secondary fields), then the matching secondary records in turn. Output and calculation lines can be conditioned onMR(matching record available) orNMR(no matching record). Secondary-input sources are injectable for headless testing:setSecondaryInput(CycleInput)targets the first secondary file, andsetSecondaryInput(int, CycleInput)targets the one at that zero-based position in F-spec order.Control levels on a matching-record cycle. A control level owns a single comparison value shared by every record type, not one per file. On each cycle the record type the merge just selected supplies its own field for that level; the value is compared against the shared one and then replaces it. Two consequences follow:
- A record type that declares no field at a level contributes nothing on its cycles, so no break can fire from it - a control field on the primary is never triggered by reading a secondary record, even if the secondary happens to hold different data in the same columns.
- When both files declare a field at the same level, a break is decided against whichever record type wrote the shared value last. A primary record can therefore break against a value a secondary record left behind.
A match-key change does not itself cause a control break; the two are independent.
-
FORCE opcode (fixed-format C-spec). Factor 2 names a primary or secondary file; the cycle reads from that file on the next iteration instead of following its normal selection order. Only the last FORCE in detail calculations takes effect. If the forced file is at end-of-file, normal file selection resumes. With a single primary file, FORCE is a no-op (the primary would be read anyway).
Output edit codes
| Code | Effect |
|---|---|
Z |
Zero-suppress: strips leading zeros, the decimal point, and the sign, printing all significant digits of the field - the integer and fractional digits run together (e.g. 00003 → 3; a (9,2) field holding 12.34 → 1234 and 1212000.00 → 121200000). The fractional digits are kept; only the decimal point is removed. |
1 |
Leading-zero suppression with a decimal point and grouping commas, a printed zero balance, and no sign (e.g. 0003750 at 2 decimals → 37.50; 1234.50 → 1,234.50). Negatives print unsigned. |
A combination-edited numeric field (codes 1-4) occupies its full edited width on the print line - the integer digits, the grouping-comma positions reserved for them, the decimal point, and the fractional digits - right-justified at the O-spec end position with the suppressed leading positions blank-filled. For example a PACKED(9,2) field edited with code 1 always occupies 12 columns (9,999,999.99 form), so a value of 37.50 renders as seven blanks followed by 37.50. Those leading blanks overwrite any earlier output field that ends within the edited field's column span, matching IBM i.
Output field types
Beyond character and packed/zoned/integer/unsigned numeric fields, an O-spec output field may name a FLOAT or a date / time / timestamp field, each rendered in its IBM i external form:
- A FLOAT field prints its external float image (
+d.dddE±nnn) - the 23-character long form forFLOAT(8)(16 significant digits, 3-digit exponent, e.g.+1.500000000000000E+000) and the 14-character short form forFLOAT(4)(8 significant digits, 2-digit exponent, e.g.+1.5000000E+00). - A date, time, or timestamp field prints its
DATFMT/TIMFMTexternal form - aDATE(*ISO)as2026-07-17, aTIME(*ISO)as13.30.00, a timestamp as2026-07-17-13.30.00.000000- the same rendering that implicit date-to-character conversion and%CHARproduce.
A field whose type has no character external form (for example a pointer) is rejected with diagnostic TRN3030 rather than printed as blanks.
Running a cycle program (injectable input source / printer sink)
Like the pluggable screen handler, a cycle program is headless and testable. It exposes an injectable input source and printer sink so callers and tests supply records and capture output without argv or spool files:
SALES prog = new SALES(); // ready to run
prog.setCycleInput(new FlatFileInput(reader, 80)); // primary-file records
prog.setCyclePrinter(line -> captured.add(line)); // report lines
prog.setOverflowLine(60); // optional: default is 60
prog.run(); // runs the whole cycle
For matching-record programs with a secondary file:
CYCLEMR prog = new CYCLEMR();
prog.setCycleInput(new FlatFileInput(primaryReader, 40)); // primary file
prog.setSecondaryInput(new FlatFileInput(secondaryReader, 40)); // secondary file
prog.setCyclePrinter(line -> captured.add(line));
prog.run();
With more than one secondary file, address each by its zero-based position in F-spec order:
prog.setSecondaryInput(0, new FlatFileInput(firstReader, 40)); // first IS file
prog.setSecondaryInput(1, new FlatFileInput(secondReader, 40)); // second IS file
new SALES() is ready to run with sensible defaults: the printer writes to
standard output, and (in main) the primary file is read from the path given as
the first command-line argument, or a file named after the primary file with a
.DATA suffix. Each declared secondary file defaults to a file named after it
with a .DATA suffix. The cycle runs to completion
inside run(); callers never step it. The boundary types are plain strings (the
fixed-width input record and the rendered print line); no marshalling is
performed.