Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

@Source: control statements

The imperative body of a Program/Screen: assignments, control flow, table access, Procedure/Function declarations, calls and reports.

For Each In: iterating over a table

For Each In Table [Index IndexName]
[Where Field = expr]...
    // body, runs for each row found
[When None
    // runs if no row was found
]
EndFor
  • Index: optional, picks the access path (defaults to the sequential-access one).
  • Where: zero or more, filters by equality.
  • When None: optional, runs if the loop found no rows.

Join In: joining another table

For Each In Orders
Join In Customers On CustCode = CustCode
Join In Zones
Where Status = "A"
    &VName = CustName
    &VZone = ZoneName
EndFor

Syntax: one or more Join In Table2 [On keyField = expr [And keyField2 = expr2 ...]] lines right after For Each In Table [Index Idx] and before the Where clauses.

  • The joined table is read by its PrimaryKey (in RPG, a chain on the primary-key logical file) once for each record read from the table being iterated. Join declares no Index.
  • Without On (implicit): each PrimaryKey field of the joined table must exist with the same name in the iterated table or in another table joined earlier (otherwise E00316).
  • With On: the fields on the joined-table side must be exactly its PrimaryKey fields, none repeated or missing (order does not matter; otherwise E00317). The other side can be a field of the iterated table or of a previously joined table, or a variable or expression. If both sides are fields of the joined table, the left one is the joined one. Types must be compatible (E00318).
  • No match: if the joined table has no record with that key, the loop does not skip the row; it continues and the joined table’s fields are blank or zero.
  • Visibility: all fields of the joined tables can be used in the Where clauses and in the body. If a name exists in both the iterated and the joined table (typically the key fields), the iterated table’s field is always used. A Where on a field that only exists in the joined table is evaluated inside the loop, after the chain (it does not position the file).
  • Read-only: assigning a joined-table field gives E00319.
  • Several Join In lines can be chained. The joined table must have a PrimaryKey in @Indexes (otherwise E00315).
  • Join and On are not reserved words: they are only recognized in the For Each header (Join followed by In), so they remain usable as field names.
  • DataGridView: the For Each that loads the grid in Event Load also supports Join In. Fields of the joined table are assigned to the Column variables before LoadRow(), just like those of the traversed table. A Where on a field that exists only in the joined table filters rows after the chain.

Delete(): deleting the current row

For Each In Table
    If &Expired
        Delete()
    EndIf
EndFor

No arguments — deletes the row the iteration currently has loaded. Only valid inside a For Each In (not inside a For Each Row, which iterates a DataGridView’s subfile, not a table); valid in both Program and Screen. A deleted row isn’t also updated, even if the body assigned one of its fields earlier in the same iteration.

For Each Row: iterating over a DataGridView

For Each Row
    // body — runs once per row already loaded into the subfile
EndFor

No Index/Where/When None (there’s no table being iterated). Only valid inside any Event of a Screen with a DataGridView. Before each iteration it writes each Column (including Hidden ones) into its bound variable — see DataGridView.

Classic For: counter

For &Variable = start To end [Step increment]
    // body
EndFor

Counts from start to end, incrementing &Variable by increment each time around. Step is optional (default 1); with a negative Step, start must be greater than end to count downward.

Iterating over a List

There’s no For Each over an in-memory list (For Each In is exclusive to database tables, see below). It’s traversed with a classic For, using Count() as the limit and indexing (&Variable[&i]):

For &i = 1 To &List.Count()
    // ... &List[&i] ...
EndFor

See Data types: List for the full syntax of Add/Clear/Init/Remove/Count.

New: inserting a row

New In Table
    Field = expr
    ...
[When Duplicate
    // statement block: runs if the row already exists
]
EndNew

New builds a row from the assigned values and inserts it. If the row already exists, the When Duplicate block runs.

How it works

  1. A read (chain) is done by the table’s PrimaryKey, with the values the New has just built.
  2. If the row does not exist, the new row is written (write) and the When Duplicate block does not run.
  3. If the row exists, the block runs on that row. At the end it is saved with update, only if the block assigned some field of that table.

Inside the block, a bare field (for example Status) is the one from the already stored row, not the one the New built. Values from the New that the block does not assign are discarded.

New without When Duplicate

If the table has a PrimaryKey, New also reads by it first: if the row already exists nothing is inserted, and that is not an error (the program carries on). If it does not exist, it is written. Without a PrimaryKey in @Indexes the row is written directly, and a duplicate key causes a runtime error.

What the block accepts

When Duplicate is a full statement block, not just Field = expr assignments: If/Else, While, Do Case, Do Procedure(), Call, assignments to &variables, For Each/New over other tables, etc. A When Duplicate with only assignments keeps working as before.

Names are resolved in this order:

  1. The fields of the New table (the re-read row; readable and assignable).
  2. The fields of the For Each loops enclosing the New (read-only).

Rules

  • The table must have a PrimaryKey in @Indexes; otherwise you get E00339. It no longer depends on some For Each using an Index over that table.
  • A primary key field cannot be reassigned inside the block (E00340, error). It used to be accepted: this is a minor breaking change.
  • Exit directly inside the block gives E00326; inside a While/For of the block itself it is valid.
  • Delete() is not valid inside the block (the usual error for Delete() outside a For Each applies).

Limit

The write fails on any uniqueness violation. If the collision was against another unique index (not the PrimaryKey), the chain by the primary key does not find the row and the write is attempted, which gives a runtime error.

Example

Program example/programs/PDUP01.warp with table example/tables/TFOLIO01.warp (primary key Empresa, Guia, Correl). If the folio exists, it toggles its Estado and adds one to Veces:

Procedure Mantener_Folio()
    &Accion = "I"
    New In FOLIO01
        Empresa = &Empresa
        Guia    = &Guia
        Correl  = &Correl
        Estado  = 1
        Descrip = "Folio nuevo"
    When Duplicate
        If Estado = 0
            Estado = 1
            &Accion = "D"
        Else
            Estado = 0
            &Accion = "H"
        EndIf
        Veces = Veces + 1
    EndNew
EndProc

The generated RPG re-reads through the PrimaryKey logical file (with its own data structure) and writes through the PF:

chain (rFOLIO01.EMPRESA : rFOLIO01.GUIA : rFOLIO01.CORREL) FOLIO0101.row rFOLIO0101;
if %found(FOLIO0101);
    // ... block ...
    update FOLIO0101.row rFOLIO0101;
else;
    write FOLIO01.row rFOLIO01;
endif;

If / While

If condition
    ...
Else
    ...
EndIf

While condition
    ...
EndWhile

condition combines comparisons with And/Or (e.g. &a = 1 And &b = 2 Or &c = 3). And binds tighter than Or (a And b Or c reads as (a And b) Or c).

Do Case: choosing a branch

Do Case
    Case &Option = "1"
        &Label = "Create"
    Case &Option = "2" Or &Option = "3"
        &Label = "Change"
    Otherwise
        &Label = "Unknown"
EndCase

Only the first Case branch whose condition is met is executed, with no fall-through to the following ones. Otherwise is optional and runs if no condition is met. There must be at least one Case.

  • Conditions use the same expressions as If (comparisons with And/Or/Not).
  • Any statement can be used inside a branch, including Exit and LoadRow() (the latter only where LoadRow() is valid: see DataGridView).
  • It is translated to RPG select/when/other/endsl. It replaces chained nested If/Else, since WARP has no Else If.
  • Case, Otherwise and EndCase (like Exit) are reserved words: they cannot be used as a field, declared variable or procedure name; they must be renamed. A Case, Otherwise or EndCase outside a Do Case gives E00327.
  • A syntax error (no Case branches or no EndCase) gives E00008.

Exit: leaving a loop

While &Counter < 100
    &Counter = &Counter + 1
    If &Counter = 10
        Exit
    EndIf
EndWhile

No arguments. Leaves the innermost loop that contains it. Valid loops are For Each In Table, For Each Row, For &v = a To b and While; also the For Each that loads the DataGridView in Event Load (it stops loading rows).

  • Innermost loop only: an Exit inside a While nested in a For Each leaves the While, not the For Each.
  • Saving in For Each In: if the body already assigned table fields before the Exit, the compiler saves (update) the record before leaving. If a Delete() came earlier, it is not saved.
  • When None and When Duplicate: the body of When None (of For Each) and of When Duplicate (of New) do not count as loops; an Exit there refers to the outer loop, if any.
  • For &i = ...: &i keeps its value on exit.
  • Inside a Do Case that is inside a loop it is valid.
  • Procedure, Function and Event: an Exit does not leave the caller’s loop; there must be a loop in the same body. Outside a loop it gives E00326.
  • Exit is a reserved word: it cannot be used as a field, declared variable or procedure name; it must be renamed. It still works as part of an @Properties/@GeneratorConfig key (e.g. Commit on Exit : True).

A complete example is in example/programs/PEXIT01.warp.

Commit / Rollback: confirm or undo changes

New In Orders
    Number = &num
EndNew
Commit
...
Rollback

No arguments. Commit confirms the changes made to tables under commitment control (RPG commit;) and Rollback undoes them (RPG rolbk;). They work in both a Program and a Screen.

Inside a For Each that modified fields of the current record, Commit first writes the pending record and then commits. Since Commit releases the record lock, assignments to the current record’s fields made AFTER the Commit in that same iteration are no longer saved (the record is considered closed). To keep modifying it, do so before the Commit or in another iteration.

Example in a Screen (with Commitment : True in @Properties):

Event 'Confirmar' 4 "Commit"
    Commit
    &Aviso = "Changes committed"
EndEvent

Event 'Deshacer' 6 "Rollback"
    Rollback
    &Aviso = "Changes rolled back"
EndEvent
  • They require Commitment : True, in the object’s @Properties or in the project’s @GeneratorConfig. Without it generation fails with “Commit requiere Commitment activo (en @Properties del objeto o en @GeneratorConfig)” (same for Rollback).
  • Where: valid in Program and Screen, anywhere in @Source, also inside a Procedure, an Event or a loop.
  • In a Screen: with Commitment enabled, the Screen starts STRCMTCTL LCKLVL(*CHG) on startup and opens the tables it modifies under commitment control; Commit and Rollback work the same as in a Program (also inside an Event or a Procedure).
  • Journal: the modified tables must be journaled on the IBM i (STRJRNPF) to be opened under commitment control. Warpgate does not journal them.
  • Inside For Each In: if the body already assigned fields of the current record, Commit first saves (update) the pending record and then commits. Rollback, on the other hand, should not go inside the For Each after assigning fields, because the record is saved at the end of the iteration; place it outside the loop, after EndFor.
  • Commit on Exit: Commit on Exit : True still commits only when the program ends; in a Screen, when the screen is closed (F3). With Commit on Exit : False, anything uncommitted is undone when the job ends.
  • They are not reserved words: a field named Commit still works (Commit = 1).

See also Commitment and Commit on Exit.

Try / Catch: catching runtime errors

Try
    &Result = &Total / &Quantity
Catch 102
    Message("The quantity cannot be zero", Error)
Catch
    Message(Trim(ErrorMessage()), Error)
EndTry

Try … Catch [filter] … EndTry catches RPG runtime errors instead of letting them abort the program. It lowers to monitor; ... on-error ...; ... endmon;. It is valid in a Program and in a Screen, anywhere in @Source.

Catch filters (optional, on the same line):

FormCatchesRPG
CatchAny error.on-error;
Catch FileFile errors (*FILE: statuses 01000-02999 on IBM i 7.5).on-error *file;
Catch ProgramProgram errors (*PROGRAM: 00100-00999 and 9999).on-error *program;
Catch 102, 202The listed status codes, comma separated (each between 100 and 9999; otherwise E00365).on-error 00102 : 00202;
  • Order: the Catch blocks are evaluated in order and the first one that covers the error wins. A Catch without a filter must come last: if another follows it, it is error E00368 (“never runs”).
  • Structure: a Try needs at least one Catch (E00367) and its EndTry (E00364).
  • ErrorCode() and ErrorMessage(): only valid inside a Catch; outside they give E00366. ErrorCode() returns a number(5) with the error status (%status) and ErrorMessage() a char(80) with the message text (PSDS positions 91-170). See Builtin functions.
  • An error inside a Catch is not caught by the same Try: nest another Try.
  • Exit and Return are allowed inside Try and Catch.
  • With Commitment: combining Catch with Rollback is useful to undo the changes when something fails.

Common status codes:

StatusSituation
00100Value out of range in a string operation.
00101Square root of a negative number.
00102Division by zero (also Mod with a zero divisor).
00103The result does not fit in the field.
00105Invalid character when converting to a number (e.g. Val).
00112Invalid date or time.
00121Invalid array index.
00202A program or procedure call failed. An error inside a called procedure reaches the caller as 00202.
00211Call error: the called program does not exist or cannot be resolved (Call to a missing program).
00907Invalid decimal data.
01211File not open.

What is not caught: a screen error (session or device) is not a program error, and WARP does not generate operations with their own error indicator or (E) extender.

A complete example is in example/screens/STRY01.warp.

Procedure / Function

Procedure Name(&param)
    ...
EndProc

Function Name(&param) returnType
    ...
    Return expr
EndFunc
  • Procedure (no return value, sub is a synonym): called with Do Name(expr, ...).
  • Function (with a return value, type required right after the parameter list — same syntax as @Fields/@Variables): called as an expression, &variable = Name(expr, ...), type-checked against &variable.
  • A parameter/return value can be struct(Template) — see Data types: Struct. The argument passed must always be a variable declared with that exact same template (never an expression nor a different template, even if it has the same members).

Call vs Do

StatementUse
Call(Name, expr, ...)Calls an external program or screen (one of @References/Programs or @References/Screens), resolved at compile time.
Call("Name", expr, ...)Calls an external program by name, resolved at runtime.
Do Name(expr, ...)Calls a Procedure from this same file (always an unquoted name).

A Screen referenced in @References/Screens is called with Call(Name, ...) exactly like a Program — typically to show it as a pop-up window (see @Properties). The called Screen can declare @Parameters just like a Program.

To send a list to another program: Call("PGM", &lines) or Call(Program, &lines) passes the dim(9999) array by reference (with the real signature if the program is in @References/Programs). The element count is not sent: the receiver reads up to the first empty line. See @Parameters: list parameters.

Print

Print("BlockName")

Prints an instance of a Block from @Layout — see @Layout. Writes 132-character lines to the QPRINT spool (OVRPRTF + open at the start, close at the end); with Print Mode: Screen the spool is also shown (DSPSPLF) and deleted (DLTSPLF) when the program finishes.

Member / Export: reports with data members

Member(Table, expr)
Export(Table, destination)

See the full guide: Reports with Member()/Export().

  • Member(Table, expr): attaches a data member to Table for the whole program (ADDPFM + OVRDBF, before any open). In Program, only at the top level of @Source; in Screen, only at the top level of Event Init (see @Source: Event and Screen). At most once per table.
  • Export(Table, destination): copies Table’s active member to an IFS stream file (CPYTOIMPF, comma-delimited). Only valid in Program (not in Screen); no position restriction within it.

JsonSplit: splitting a JSON into lines

JsonSplit(&json, &list [, indent])

Splits the JSON text in &json into readable lines, one per element and indented per level, and stores them in &list, replacing its contents. Useful, for example, to print the result of Json().

  • 1st argument: a char/varchar &Variable or struct member, or a text literal. Any other expression gives E00301.
  • 2nd argument: a list(varchar(n)) or list(char(n)) &Variable. Otherwise E00302; it cannot be an in parameter either (E00151).
  • 3rd argument (optional): indentation in spaces per level, a number. Default 2; if it is not a number, E00303.
  • A line is cut at every {, [ and ,, and before every } or ], as long as they are not inside quotes (\" inside texts is respected). The comma and the closing character stay together (},). Spaces outside texts are discarded.
  • Maximum 9999 lines (the capacity of a list): when full, the message JsonSplit: lista llena (max 9999) is shown and the rest is discarded. A line longer than the list element is truncated. As a rule of thumb, an array of objects with 15 fields takes 17 lines per object plus 2, that is about 588 objects.

For {"epm":"X","Items":[{"Pgm":"P1"},{"Pgm":"P2"}]} with indentation 2, the resulting lines are:

{
  "epm":"X",
  "Items":[
    {
      "Pgm":"P1"
    },
    {
      "Pgm":"P2"
    }
  ]
}

Typical use to print, with a @Layout Block("Linea") that has a Label on &linea:

JsonSplit(&json, &lines)
For &i = 1 To &lines.Count()
    &linea = &lines[&i]
    Print("Linea")
EndFor

In RPG, one procedure JsonSplit_<json type>_<element type> is generated for each pair of types used. A complete example is in example/JSONSPLIT01.warp. Errors: E00301, E00302, E00303 (see Diagnostics). When replacing the content, the previous lines are cleared (see List: methods).

To convert a list(struct(...)) into fixed-width lines (and back) see Fixed-width records: FixedWidthSplit and FixedWidthParse.

Return

Return [expr]

Exits a Function. A Function must return a value: a Return without a value inside it is error E00351, and a Function with no Return carrying a value is error E00352.