Reading fixed-format RPG you inherited: the H/F/D/C spec decoder
Sooner or later you'll open a member written in 1994 and see a wall of letters in strange columns. It isn't random. Each line has a specification type in column 6, and every column range means something. Once you can decode that, old RPG becomes readable.
Below: column layouts, indicators, the cycle, legacy opcodes with free-format equivalents, and safe conversion. Verify exact columns in the IBM Docs ILE RPG Reference. Start with free-format RPG first if the modern syntax is new; for the bigger picture see the mental model.
The spec types
Column 6 holds the letter. Columns 1 to 5 are sequence/page, and 6 is the type.
| Spec | Purpose |
|---|---|
H |
Header (control) options, like ctl-opt
|
F |
File declarations, like dcl-f
|
D |
Definitions of variables, structures, prototypes, like dcl-s/dcl-ds/dcl-pr
|
I |
Input specs (old, field definitions for program-described files) |
C |
Calculations, the logic |
O |
Output specs (old, report layout) |
P |
Procedure begin/end, like dcl-proc/end-proc
|
Order matters: H, F, D, I, C, O, then P. A comment is * in column 7.
First check the member's source type. RPGLE (or SQLRPGLE) members are RPG IV, which is what this article decodes. A plain RPG member is RPG III (RPG/400), which has different column layouts and an H/F/E/L/I/C/O order. Run it through CVTRPGSRC to get RPG IV before doing anything else.
H spec
H DFTACTGRP(*NO) ACTGRP(*NEW)
Same keywords as ctl-opt; only the H in column 6 is positional.
F spec
FCUSTMAST IF E DISK
FCUSTRPT O E PRINTER
Columns, as I use them:
| Columns | Meaning |
|---|---|
| 7-16 | File name |
| 17 | File type: I input, O output, U update, C combined |
| 18 | Designation: P primary, S secondary, F full procedural (also R record address, T array/table) |
| 19 | End of file: E
|
| 22 |
E = externally described (fields come from the file) |
| 36-42 | Device: DISK, PRINTER, WORKSTN
|
| 44-80 | Keywords, like USROPN or KEYED
|
The key habit: IF E ... DISK reads as "input, full-procedural, externally described, disk file". That's dcl-f custmast usage(*input) keyed;. Fixed-format F specs get the access path from the file, but free-form needs the keyed keyword for CHAIN/SETLL by key, and it's the first thing people forget when they convert. Treat exact columns as a lookup, not memory. Those externally described files usually come from DDS, see DDS to SQL DDL migration.
D spec
D custName S 30A
D total S 11P 2
D counter S 10I 0
D custDS DS
D custId 10I 0
D custCity 20A
| Columns | Meaning |
|---|---|
| 7-21 | Name |
| 22 |
E external DS |
| 24-25 | Type: S standalone, C constant, DS data structure, PR/PI prototype/interface, blank for subfields |
| 26-32 | "From" position (subfields) |
| 33-39 | Length (or "to" position) |
| 40 | Data type: A char, P packed, S zoned, I integer, U unsigned, F float, D date, T time, Z timestamp, N indicator |
| 41-42 | Decimal positions |
| 44-80 | Keywords |
So total S 11P 2 is dcl-s total packed(11:2);. A DS written this way becomes dcl-ds custDS; custId int(10); custCity char(20); end-ds;.
C spec
C custKey CHAIN CUSTMAST
C IF %FOUND(CUSTMAST)
C EVAL total = total + BALANCE
C ENDIF
| Columns | Meaning |
|---|---|
| 7-8 | Control level indicator (L1-L9, LR) |
| 9-11 | Conditioning indicator; N in column 9 negates it (N01) |
| 12-25 | Factor 1 |
| 26-35 | Opcode |
| 36-49 | Factor 2 |
| 50-63 | Result field |
| 64-68 | Field length |
| 69-70 | Decimal positions |
| 71-76 | Resulting indicators (hi, lo, eq) |
Factor 1, opcode, factor 2, result. Everything is a three-address instruction. The newer extended factor 2 form puts a full expression from column 36 onward, which is what EVAL and IF use.
O spec
O specs describe report lines: record type (H heading, D detail, T total), conditioning indicators, then each field with its end position. They work with the cycle. The modern replacement is a printer file with write to record formats, or SQL.
Indicators and why they exist
Indicators are 99 global boolean flags: *IN01 to *IN99, plus specials like *INLR, *INH1, *INL1. They exist because early RPG had no IF statement. Instead of branching, you attached an indicator condition to a line: if *IN01 is on, the line runs.
They also carried results. Columns 71-76 of a C spec name the indicators an opcode sets (hi, lo, eq), and what they mean depends on the opcode: for CHAIN one of them means "record not found", for READ another means end of file. The numbers (90, 91) were whatever the author picked. On a display file, indicators turn on things like the error flag for a field or "function key 3 pressed".
Being global and numbered, they're a hazard. Before changing old code, comment what each indicator means. In new code use named indicators and built-ins:
dcl-s notFound ind;
chain custKey custmast;
notFound = not %found(custmast);
Same program in fixed format (what you will meet in most shops):
DnotFound S N
C custKey CHAIN CUSTMAST
C EVAL notFound = NOT %FOUND(CUSTMAST)
Cycle basics
Old RPG has a built-in loop called the program cycle. If a file is marked primary (P), the runtime does this for each record: read, set level-break indicators, run the calculations, and write output for the record. When input is exhausted, the runtime turns *INLR on itself, runs the "last record" calculations and totals, and ends the program.
If a program has no P file, it's usually "full procedural", and the author controls everything with READ loops. In that case *INLR = *ON; RETURN at the end ends the program. If *INLR is never set, the program stays in memory with its variables intact for the next call. That is a classic source of strange behavior where a second call "remembers" the first.
Legacy opcodes and free-format equivalents
| Fixed-format | Meaning | Free-format equivalent |
|---|---|---|
Z-ADD 0 total |
set a numeric to a value | total = 0; |
ADD 1 counter |
add | counter += 1; |
MOVE field1 field2 |
copy, right-aligned, type conversion |
field2 = field1; (check lengths/types) |
MOVEL field1 field2 |
copy, left-aligned |
field2 = field1; or %subst for partial copy |
MOVE *BLANKS name |
clear |
clear name; or name = *blanks;
|
CHAIN key file |
random read by key | chain key file; |
SETLL key file |
position at first record >= key | setll key file; |
READE key file |
read next record with equal key | reade key file; |
READ file |
read next | read file; |
ADD a b c / SUB / MULT / DIV
|
arithmetic into a result |
c = a + b; etc. |
IFEQ, IFGT, DOWEQ... |
old comparison forms of IF/DOW
|
if a = b;, dow a = b;
|
EXSR name |
call subroutine |
exsr name; (still valid) |
PARM / PLIST
|
parameter list | dcl-pi *n; ... end-pi; |
MOVE and MOVEL quietly convert types and truncate or pad. Plain = is stricter, so a mechanical conversion can behave differently.
/FREE versus **FREE
-
/FREE ... /END-FREE(V5R1 onward) lets you write free-format calculation statements inside a fixed-format source, in columns 8 to 80. H, F, D specs still use columns. This is the half-modernized style you'll see in mid-2000s code. Since 7.1 TR7 the/FREEand/END-FREElines are optional, and 7.1/7.2 also gained free-form H, F, D and P statements (ctl-opt,dcl-f,dcl-s,dcl-proc). -
`FREE
** in column 1 of the first line makes the whole member free-format: no column rules, no practical line-length limit, no fixed-format statements at all. IBM shipped it for 7.1 and 7.2 as PTFs (ILE RPG compiler SI58136 and SI58137, plus a Db2 for i group PTF forSQLRPGLE) and it is standard on later releases. Anything that must stay fixed-format, such as aTAGor an I spec, has to move to a/COPY` member.
If your compiler is recent, convert to **FREE as the target, not /FREE.
Converting safely
Don't rewrite by hand. Use a converter:
-
VS Code extensions:
RPG free(Brian Garland,vscode-rpgfree) andrpgiv2free(Bob Cozzi) convert fixed-format RPG IV to free-format, and both work alongside Code for IBM i. Neither is part of Code for IBM i itself. -
RDi (Rational Developer for i): the Convert Selection to Free-Form option. Historically it handled C specs only, and IBM documents statements it won't convert, such as
PLIST/PARMentry lists and someMOVEs. Check what your version does with H, F and D specs. - Commercial tools such as ARCAD-Converter convert everything, including entry parameter lists.
The safe procedure:
- Compile the original: Keep the program object as a baseline
- Convert one member on a copy: Original stays in version control
- Compile under a different name: The converted version gets its own object
- Run both on the same test data: Compare files, reports, return values
Replace: Only after the outputs match
Compile the original and keep the program object as a baseline.
Convert one member in a copy, not in place. Keep the original source under version control.
Compile the converted version with a different object name.
Run both on the same test data and compare outputs: files, spooled reports, return values.
Only then replace.
Review the output; the converter doesn't improve readability for you, and expect leftovers (*ENTRY PLIST, MOVE between mismatched types) that you finish by hand. For the SQL inside converted code, see embedded SQL done properly.
Do-not-convert cases
-
Heavy indicator logic or
MOVE/MOVELon dates and zoned fields. Convert by hand with explicit casts, once you can explain every indicator. -
Cycle-dependent programs with a primary file, level-break indicators, or heavy
Ospecs. The behavior is implicit; converters keep it, but a manual rewrite often loses it. - Programs with no tests and no owner that run month-end money logic. Leave working code alone until you can compare outputs. A tidy diff is not worth a wrong total.
-
Shared
/COPYmembers. Changing one affects every program that includes it. ILE prototypes live in copy members too, see ILE modules and service programs.
Try it on PUB400
- Create a source file if needed:
CRTSRCPF FILE(YOURLIB/QRPGLESRC) RCDLEN(112)(it may already exist). - Add a fixed-format member that reads
CUSTMASTfrom Free-format RPG first: your first real program, using the F, D, C examples above. - Compile:
CRTBNDRPG PGM(YOURLIB/FIXEDRPT) SRCFILE(YOURLIB/QRPGLESRC) SRCMBR(FIXEDRPT). - In VS Code with Code for IBM i and a conversion extension such as
RPG free, convert the member, save to a new member, compile, call both, compare output.
Best resources
- IBM Docs, RPG IV specification types: column layouts for every specification type
-
IBM RPG Cafe: fully free-form RPG:
**FREErules and PTF levels for 7.1/7.2 - IBM Developer: free-form RPG support: free-form H, F, D and P specs
-
RPGPGM.com: posts on
/FREE,**FREEand modernizing old code - Code for IBM i docs: editor setup
- vscode-rpgfree: VS Code fixed-to-free converter
- rpgiv2free: Bob Cozzi's VS Code converter
- COMMON RPG Bootcamp: structured material that covers both fixed and free forms
- Barrett Otte, IBMi-Book: community book with practical RPG notes
Next
Next: CL programming essentials
Related
Originally published on rainierio.dev. The full Learn IBM i track, with an Indonesian version, is at rainierio.dev/learn.
Top comments (0)