DEV Community

Cover image for Reading fixed-format RPG you inherited: the H/F/D/C spec decoder
Rainierio.dev
Rainierio.dev

Posted on Originally published at rainierio.dev

Reading fixed-format RPG you inherited: the H/F/D/C spec decoder

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)
Enter fullscreen mode Exit fullscreen mode

Same keywords as ctl-opt; only the H in column 6 is positional.

F spec

     FCUSTMAST  IF   E             DISK
     FCUSTRPT   O    E             PRINTER
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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);
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 /FREE and /END-FREE lines 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 for SQLRPGLE) and it is standard on later releases. Anything that must stay fixed-format, such as a TAG or 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) and rpgiv2free (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/PARM entry lists and some MOVEs. 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:

  1. Compile the original: Keep the program object as a baseline
  2. Convert one member on a copy: Original stays in version control
  3. Compile under a different name: The converted version gets its own object
  4. Run both on the same test data: Compare files, reports, return values
  5. Replace: Only after the outputs match

  6. Compile the original and keep the program object as a baseline.

  7. Convert one member in a copy, not in place. Keep the original source under version control.

  8. Compile the converted version with a different object name.

  9. Run both on the same test data and compare outputs: files, spooled reports, return values.

  10. 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/MOVEL on 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 O specs. 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 /COPY members. 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

  1. Create a source file if needed: CRTSRCPF FILE(YOURLIB/QRPGLESRC) RCDLEN(112) (it may already exist).
  2. Add a fixed-format member that reads CUSTMAST from Free-format RPG first: your first real program, using the F, D, C examples above.
  3. Compile: CRTBNDRPG PGM(YOURLIB/FIXEDRPT) SRCFILE(YOURLIB/QRPGLESRC) SRCMBR(FIXEDRPT).
  4. 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

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)