DEV Community

Cover image for Free-format RPG first: your first real program
Rainierio.dev
Rainierio.dev

Posted on Originally published at rainierio.dev

Free-format RPG first: your first real program

Free-format RPG first: your first real program

Skip the fixed-format columns for now. Modern RPG is written in **FREE form: no column rules, semicolons, keywords like dcl-s. You'll still need to read fixed-format later (see Reading fixed-format RPG you inherited: the H/F/D/C spec decoder), but you should learn to write the modern way first.

This article builds one program that reads a table, loops over it, handles errors and prints with dsply.

  1. Shape of a **FREE program: **free, ctl-opt, statements end with ;
  2. Declarations: dcl-s, dcl-ds, dcl-f
  3. Sample table: CUSTMAST created with SQL
  4. Complete program: Read, loop, monitor, dsply
  5. Compile and run: CRTBNDRPG and CALL
  6. Try it on PUB400: Break it on purpose

The shape of a **FREE program

The first line must be **free, starting in column 1. Everything after it is free-format. Statements end with ;. Comments start with //.

**free
ctl-opt dftactgrp(*no) actgrp(*new) option(*srcstmt:*nodebugio);

dsply 'Hello from RPG';
return;
Enter fullscreen mode Exit fullscreen mode

Same program in fixed format (what you will meet in most shops):

     H OPTION(*SRCSTMT:*NODEBUGIO) DFTACTGRP(*NO) ACTGRP(*NEW)
     DMSG              C                   CONST('Hello from RPG')
     C     MSG           DSPLY
     C                   RETURN
Enter fullscreen mode Exit fullscreen mode

In fixed format DSPLY takes the message in factor 1, and factor 1 is only 14 characters wide (columns 12-25), so the literal lives in a named constant. H keywords start after column 6; I put them at column 8 (not sure every shop aligns them the same way).

ctl-opt sets compile and runtime options. dftactgrp(*no) is required if you want to use ILE features like procedures and bound calls (see ILE modules, service programs and the binder). option(*srcstmt) makes error line numbers match your source lines, which saves you a lot of grief.

Declarations

dcl-s count int(10) inz(0);                // integer
dcl-s total packed(11:2) inz(0);           // decimal, 11 digits, 2 after the point
dcl-s name varchar(50);                    // variable length text
dcl-s fixedName char(30);                  // fixed length, blank padded
dcl-s today date;                          // date type
dcl-s done ind inz(*off);                  // true/false indicator

dcl-ds summary qualified;                  // data structure = a struct
  rows int(10);
  bigOnes int(10);
end-ds;
Enter fullscreen mode Exit fullscreen mode

Same program in fixed format (what you will meet in most shops):

     DCOUNT            S             10I 0 INZ(0)
     DTOTAL            S             11P 2 INZ(0)
     DNAME             S             50A   VARYING
     DFIXEDNAME        S             30A
     DTODAY            S               D
     DDONE             S               N   INZ(*OFF)
     DSUMMARY          DS                  QUALIFIED
     D  ROWS                         10I 0
     D  BIGONES                      10I 0
Enter fullscreen mode Exit fullscreen mode

DS/S sit in columns 24-25, length ends in column 39, data type in column 40, decimals in 41-42. A DATE has no length, so columns 33-39 stay blank. Fixed-format comments need * in column 7, so the free-format // remarks are dropped.

  • dcl-s declares a standalone variable.
  • dcl-ds ... end-ds declares a data structure. With qualified you access fields as summary.rows, like a struct.
  • char is fixed-length and padded with blanks. That surprises web developers. varchar behaves more like a string.
  • dcl-f declares a file. More on that next.

The sample table

Create a table on PUB400 or any IBM i. Use the SQL scripts feature in VS Code, or STRSQL, or RUNSQLSTM:

CREATE TABLE MYLIB.CUSTMAST (
  CUSTID   INTEGER NOT NULL,
  CUSTNAME CHAR(30) NOT NULL,
  CITY     CHAR(20) NOT NULL,
  BALANCE  DECIMAL(9,2) NOT NULL
);

INSERT INTO MYLIB.CUSTMAST VALUES
  (1, 'Ana Santos',   'Manila',   1500.50),
  (2, 'Budi Hartono', 'Jakarta',   320.00),
  (3, 'Chen Wei',     'Singapore', 2750.75);
Enter fullscreen mode Exit fullscreen mode

Replace MYLIB with your own library (see the mental model of libraries and objects). Since the table has no explicit field naming conflicts, RPG will read the field names from the table at compile time.

The complete program

Save as CUSTRPT.rpgle in the IFS, or as a member in QRPGLESRC:

**free
ctl-opt dftactgrp(*no) actgrp(*new) option(*srcstmt:*nodebugio);

dcl-f custmast usage(*input);

dcl-s today date;
dcl-s line varchar(80);
dcl-s pos int(10);
dcl-s i int(10);
dcl-s status char(8);

dcl-ds summary qualified;
  rows int(10) inz(0);
  total packed(11:2) inz(0);
  bigOnes int(10) inz(0);
end-ds;

today = %date();
dsply ('Customer report ' + %char(today:*iso));

monitor;
  read custmast;
  dow not %eof(custmast);
    summary.rows += 1;
    summary.total += balance;

    select;
      when balance >= 2000;
        status = 'HIGH';
        summary.bigOnes += 1;
      when balance >= 1000;
        status = 'MEDIUM';
      other;
        status = 'LOW';
    endsl;

    line = formatLine(custname: city: balance: status);
    dsply line;

    read custmast;
  enddo;
on-error;
  dsply 'Could not read CUSTMAST';
endmon;

pos = %scan('Singapore': 'Ships to Singapore today');
if pos > 0;
  dsply ('Found at ' + %char(pos));
endif;

for i = 1 to 3;
  dsply ('Pass ' + %char(i));
endfor;

dsply ('Rows: ' + %char(summary.rows)
     + ' Total: ' + %char(summary.total)
     + ' High: ' + %char(summary.bigOnes));

return;

dcl-proc formatLine;
  dcl-pi *n varchar(80);
    nm char(30) const;
    ct char(20) const;
    bal packed(9:2) const;
    st char(8) const;
  end-pi;

  return %trim(nm) + ' | ' + %trim(ct) + ' | ' + %char(bal) + ' | ' + %trim(st);
end-proc;
Enter fullscreen mode Exit fullscreen mode

Same program in fixed format (what you will meet in most shops):

     H DFTACTGRP(*NO) ACTGRP(*NEW) OPTION(*SRCSTMT:*NODEBUGIO)
     FCUSTMAST  IF   E             DISK
     DERRMSG           C                   CONST('Could not read CUSTMAST')
     DMSG              S            100A   VARYING
     DTODAY            S               D
     DLINE             S             80A   VARYING
     DPOS              S             10I 0
     DI                S             10I 0
     DSTATUS           S              8A
     DSUMMARY          DS                  QUALIFIED
     D  ROWS                         10I 0 INZ(0)
     D  TOTAL                        11P 2 INZ(0)
     D  BIGONES                      10I 0 INZ(0)
     DFORMATLINE       PR            80A   VARYING
     D  NM                           30A   CONST
     D  CT                           20A   CONST
     D  BAL                           9P 2 CONST
     D  ST                            8A   CONST
     C                   EVAL      TODAY = %DATE()
     C                   EVAL      MSG = 'Customer report ' + %CHAR(TODAY:*ISO)
     C     MSG           DSPLY
     C                   MONITOR
     C                   READ      CUSTMAST
     C                   DOW       NOT %EOF(CUSTMAST)
     C                   EVAL      SUMMARY.ROWS = SUMMARY.ROWS + 1
     C                   EVAL      SUMMARY.TOTAL = SUMMARY.TOTAL + BALANCE
     C                   SELECT
     C                   WHEN      BALANCE >= 2000
     C                   EVAL      STATUS = 'HIGH'
     C                   EVAL      SUMMARY.BIGONES = SUMMARY.BIGONES + 1
     C                   WHEN      BALANCE >= 1000
     C                   EVAL      STATUS = 'MEDIUM'
     C                   OTHER
     C                   EVAL      STATUS = 'LOW'
     C                   ENDSL
     C                   EVAL      LINE = FORMATLINE(CUSTNAME: CITY:
     C                             BALANCE: STATUS)
     C     LINE          DSPLY
     C                   READ      CUSTMAST
     C                   ENDDO
     C                   ON-ERROR
     C     ERRMSG        DSPLY
     C                   ENDMON
     C                   EVAL      POS = %SCAN('Singapore':
     C                             'Ships to Singapore today')
     C                   IF        POS > 0
     C                   EVAL      MSG = 'Found at ' + %CHAR(POS)
     C     MSG           DSPLY
     C                   ENDIF
     C                   FOR       I = 1 TO 3
     C                   EVAL      MSG = 'Pass ' + %CHAR(I)
     C     MSG           DSPLY
     C                   ENDFOR
     C                   EVAL      MSG = 'Rows: ' + %CHAR(SUMMARY.ROWS)
     C                             + ' Total: ' + %CHAR(SUMMARY.TOTAL)
     C                             + ' High: ' + %CHAR(SUMMARY.BIGONES)
     C     MSG           DSPLY
     C                   RETURN
     PFORMATLINE       B
     DFORMATLINE       PI            80A   VARYING
     D  NM                           30A   CONST
     D  CT                           20A   CONST
     D  BAL                           9P 2 CONST
     D  ST                            8A   CONST
     C                   RETURN    %TRIM(NM) + ' | ' + %TRIM(CT) + ' | '
     C                             + %CHAR(BAL) + ' | ' + %TRIM(ST)
     PFORMATLINE       E
Enter fullscreen mode Exit fullscreen mode

Fixed format needs the prototype (D FORMATLINE PR) declared before the first call; dcl-proc generates it for you. Names are uppercased by habit, columns are as I recall them: I am not certain about the H keyword column, so check it on PUB400.

What each part is doing:

  • dcl-f custmast usage(*input); opens the table for reading. Its columns (CUSTID, CUSTNAME, CITY, BALANCE) become variables automatically.
  • read reads the next record; %eof(custmast) is true once there is nothing left.
  • The read is done before the loop and again at the bottom. That's the standard pattern.
  • select / when / other / endsl is a switch. if and dow work as you'd expect; for counts upward.
  • monitor / on-error / endmon is try/catch. on-error with no code catches everything.
  • Procedures (dcl-proc, dcl-pi) go after the main logic. *n means the procedure is unnamed in the interface; the name is on dcl-proc. const passes by read-only reference.
  • dsply shows a message on the screen and in the job's message queue. It truncates at 52 characters, which is fine for a first program. For a real report, use a printer file with dcl-f ... printer, or write to the IFS.

Built-in functions you'll use daily

Function What it does Example
%char(x) number or date to text %char(balance)
%dec(x:p:s) text to packed decimal %dec('123.45':7:2)
%trim(s) strip blanks both ends %trim(custname)
%scan(find:in) position of text, 0 if not found %scan('A':name)
%subst(s:start:len) slice of a string %subst(name:1:3)
%date() today, or convert a value to a date %date()

%subst is 1-based. %scan returns 0 when not found, not -1.

Compile and run

From a 5250 session or the VS Code terminal:

CRTBNDRPG PGM(MYLIB/CUSTRPT) SRCSTMF('/home/YOURUSER/CUSTRPT.rpgle') DBGVIEW(*SOURCE)
CALL PGM(MYLIB/CUSTRPT)
Enter fullscreen mode Exit fullscreen mode

If your source is in a source physical file instead:

CRTBNDRPG PGM(MYLIB/CUSTRPT) SRCFILE(MYLIB/QRPGLESRC) SRCMBR(CUSTRPT) DBGVIEW(*SOURCE)
Enter fullscreen mode Exit fullscreen mode

CRTBNDRPG compiles and binds in one step. If it fails, read the compile listing: DSPJOBLOG or WRKSPLF, or let Code for IBM i show the errors in the editor.

Naming conventions

  • Object names: up to 10 characters, uppercase, start with a letter. CUSTRPT, not customer_report_final.
  • Source members: same name as the object they build.
  • Variables: camelCase is common in modern code (bigOnes), and RPG is case-insensitive anyway.
  • Source files: .rpgle for RPG, .sqlrpgle if it has embedded SQL, .clle for CL.
  • Prefix or qualify data structure fields so they don't collide with file fields.

Common beginner errors

  • `free` not in column 1.** Compiler treats the source as fixed-format and fills the listing with errors.
  • Missing semicolon. The error usually points to the line after the real problem.
  • Forgetting dftactgrp(*no) when you use procedures or binding. You'll get activation group errors.
  • Comparing char fields with trailing blanks and wondering why they don't match. Use %trim on both sides.
  • Name conflicts. A variable named the same as a file field gives an "already defined" error.
  • Library not in the library list. CUSTMAST isn't found at compile time. Run ADDLIBLE MYLIB first, or qualify the file name.
  • dsply seems to hang. It waits for you to answer the message if you gave it a response variable. Without one it should just show. If a job sits in MSGW, check WRKACTJOB.

Try it on PUB400

  1. Sign on and make sure DSPLIBL shows your library, or add it with ADDLIBLE.
  2. Run the CREATE TABLE and INSERT above using RUNSQLSTM or VS Code's SQL runner.
  3. Upload or type CUSTRPT.rpgle into your IFS home directory.
  4. Run the CRTBNDRPG command. Fix any errors it lists.
  5. CALL PGM(MYLIB/CUSTRPT) and watch the dsply lines.
  6. Break it on purpose: rename the table in dcl-f and read the compile message. Then add a %subst(custname:1:5) to the output line.

Best resources

Next

Next: Reading fixed-format RPG you inherited

Related


Originally published on rainierio.dev. The full Learn IBM i track, with an Indonesian version, is at rainierio.dev/learn.

Top comments (0)