WARP & Warpgate: the evolution of IBM i development
WARP is a high-level programming language with declarative syntax, designed to optimize and speed up development on IBM i (AS/400) environments. Instead of hand-writing extensive RPGLE code or DDS definitions, WARP lets you define programs, interactive screens and tables in clean, expressive .warp files.
Through the Warpgate compiler, WARP code is automatically transformed into highly optimized RPGLE and DDS source, ready to run on the system.
Key features
- High-performance engineering: the Warpgate compiler is built 100% in Rust, guaranteeing ultra-fast compilation speed, memory safety and native portability across Windows, GNU/Linux and macOS.
- CI/CD pipeline integration: being a native, cross-platform executable capable of building from the command line, it integrates easily into continuous integration/deployment pipelines (GitHub Actions, GitLab CI, Azure DevOps, etc.), bringing modern DevOps practices to the IBM i platform.
- Multi-protocol remote deployment: transfers and deploys directly to remote IBM i servers over SSH, SFTP and FTP, configurable from the project file (see
@Connection/Protocol). - Modernization and productivity: drastically reduces the lines of code needed and removes the verbosity of traditional RPG, streamlining development teams’ workflow.
- Continuous evolution: constantly improving and gaining new language features and code generation capabilities.
Installation
Warpgate is distributed as a VSCode extension, available on the Visual Studio Marketplace:
- Open VSCode → Extensions tab (
Ctrl+Shift+X). - Search for “WaRPGate for IBM i” (publisher Software House) and install it.
The extension already includes the compiler for Windows, GNU/Linux and macOS — no separate install needed. To generate and deploy code (generate/build/reverse) a license is required — see Licensing.
Note for macOS: the compiler is not signed with an Apple certificate, so macOS may block its first run with a warning that the developer cannot be verified. To allow it, run this once in Terminal, replacing the path with the location of the binary:
xattr -d com.apple.quarantine /path/to/warpgate
Built by Software House
WARP and Warpgate are designed, developed and maintained by Giuliano Gonzales Zeballos, creator and lead architect at Software House, a firm dedicated to software engineering, systems architecture and modernization solutions for enterprise environments.
About this book
This book documents:
- The guides: how to build, step by step, each kind of object (
Program,Screen,Table) and the bigger features (DataGridView,Member/Exportreports). - The language reference: every section (
@Properties,@Variables,@Source, …), every data type, every control statement and every builtin function, with its exact syntax and validation rules.
What is a .warp file?
A .warp file describes one object: a Program (business logic, with or without printed output), a Screen (an interactive 5250 screen) or a table (a DDS physical file). The type is declared in @Properties/Type.
A .warp file is organized into top-level sections, marked with @Name { ... }:
@Documentation {
Author Name : "Programmer Name"
}
@Properties {
Type : Program
Name : PTESTFN
Description : "Minimal example"
}
@References {
Programs = []
Screens = []
Tables = []
}
@Variables {
global {
Username char(10)
}
}
@Parameters {
}
@Source {
&Username = USERID()
}
Which sections are valid, and in what order, depends on Type — a Program has @Layout/@Source, a table has @Structure/@Fields/@Indexes, a Screen has @Layout/@Source but no @Structure. Each reference chapter details which file type each section applies to.
How to compile
The compiler is a CLI binary (cli, distributed as warpgate/warpgate.exe inside the VSCode extension) that takes a project (.warproj) and a source file:
warpgate --project my-project.warproj --source programs/PMYPROG.warp --action validate
warpgate --project my-project.warproj --source programs/PMYPROG.warp --action generate
warpgate --project my-project.warproj --source programs/PMYPROG.warp --action build --profile Prod
validate: only runs semantic analysis (the same thing the VSCode extension triggers on save) — never touches the IBM i.generate: generates RPGLE/DDS/CL and writes it toOutput— purely local, never connects to the IBM i (even though the generated CL already references remote paths, based on@Connection/@Deployment).build: does the same asgenerate, and additionally uploads and compiles the generated code on the IBM i configured in@Connection(CRTPF/CRTLF/CRTBNDRPG/CRTDSPF, depending on the type).reverse: reconstructs a table.warpfile from an existing DDS on the IBM i (reverse engineering).
When the .warpcfg declares more than one @Profile (Dev/UAT/Prod, see Project file), generate/build/reverse and license-status need to know which one to use — either with the --profile <Name> flag, or, if omitted, with Default Profile in @Project. validate ignores this flag entirely.
See Project file for the .warproj/.warpcfg format.
Getting started: your first Program
A Program is the basic unit of business logic: it receives parameters, does something (reads/writes tables, computes, calls other programs) and finishes. It doesn’t have to show a screen or print anything.
1. Minimal file
@Documentation {
Author Name : "Your Name"
}
@Properties {
Type : Program
Name : PGREET
Description : "Minimal greeting program"
}
@References {
Programs = []
Screens = []
Tables = []
}
@Variables {
global {
Username char(10)
Greeting char(30)
}
}
@Parameters {
}
@Source {
&Username = USERID()
&Greeting = "Hello, " + &Username
}
This is already a valid .warp file: it declares the object (@Properties), its variables (@Variables) and its logic (@Source). @References/@Parameters are empty because this program doesn’t depend on other objects or receive parameters.
2. Adding parameters
A Program receives parameters declared in @Parameters, referencing variables already declared in @Variables/global:
@Variables {
global {
MyDate date
MyTime char(8)
}
}
@Parameters {
inout:&MyDate,
inout:&MyTime
}
The modes are in (input only), inout (input and can be modified) and out (output only) — see @Parameters.
3. Calling another program
To call an external program, add it to @References/Programs and use Call(...):
@References {
Programs = ["../programs/POTHER.warp"]
}
@Source {
Call(POTHER, &MyDate, &MyTime)
}
4. Local procedures and functions
Procedure/Function live inside @Source, with their own variables in @Variables (one entry per procedure/function name, in addition to global):
@Variables {
global {
Username char(10)
}
Greet {
name char(10)
}
}
@Source {
Function Greet(&name) char(20)
Return "Hello " + &name
EndFunc
&Username = USERID()
Message("Welcome", Info) // only valid in Screen; use Print() in Program
}
To call a Procedure (no return value) use Do Name(...); a Function (with a return value) is called as an expression: &variable = Name(...).
5. Next steps
- If your program needs to read/write a table: Tables.
- If it needs to show an interactive screen: Screens.
- If it needs to generate a report to a text file: Reports with Member()/Export().
For the full syntax of each section, see the Language reference.
Tables: @Structure, @Fields, @Indexes
A table .warp file describes a DDS physical file: its columns and its keyed access paths. It’s referenced from a Program/Screen via @References/Tables.
1. Minimal file
@Properties {
Type : "Table"
}
@Structure {
Name : "GRIDDEMO"
Description : "Sample customers"
@Fields {
CustCode char(6) Description("Customer code")
Name char(20) Description("Name")
City char(15) Description("City")
}
@Indexes {
PrimaryKey ( GRIDDEMOPK, [CustCode] )
Index ( GRIDDMNOM, [Name] )
}
}
@Fields: oneName type(...)line per column. Onlychar/number/date— see Data types. Available modifiers:Description(...),Default(...),AllowNull.@Indexes:PrimaryKey(required, its fields can’t beAllowNull),Unique,Index— each one asKind(Name, [Field, ...], "optional description").
2. Referencing it from a Program
@References {
Tables = ["../tables/TLGRIDDEMO.warp"]
}
The path is relative to the file that references it. Once referenced, its fields become available as bare identifiers inside a For Each/New (see below), and as field(FieldName) in @Variables to inherit its type.
3. Reading rows: For Each In
@Source {
For Each In GRIDDEMO Index GRIDDMNOM
Where CustCode = &VCustCode
&VName = Name
&VCity = City
When None
Message("Customer not found", Error)
EndFor
}
Index picks the access path (defaults to the sequential-access one if omitted); Where filters by equality; When None runs if no row was found.
Joining tables: Join In
To read data from a related table, add Join In under the For Each. The joined table is read by its PrimaryKey for each iterated row:
For Each In Orders
Join In Customers On CustCode = CustCode
&VName = CustName
EndFor
Without On, the key columns of the joined table must exist with the same name in the iterated table. If there is no match, the row is not skipped and the joined fields are blank or zero. They are read-only. Full details in @Source: control statements.
4. Inserting rows: New
@Source {
New In GRIDDEMO
CustCode = &VCustCode
Name = &VName
City = &VCity
When Duplicate
Message("That code already exists", Error)
EndNew
}
When Duplicate runs if the row already exists: the New looks it up by the table’s PrimaryKey (which must be in @Indexes) and, if found, runs the block on that row and updates it if the block assigned fields. The block accepts any statement (If, While, Do Case, etc.). Details in @Source: control statements.
5. Extended documentation
A table can also carry @Documentation (nested inside @Structure, unlike a Program where it goes at the top level) with @Author, @Application, @Module, @Repository and @History (@Creation + @Change per changelog entry) — see @Structure, @Fields, @Indexes for the full detail of each sub-section.
6. Reverse engineering from an existing DDS
If the table already exists on the IBM i, there’s no need to write it by hand: the WaRPGate: Reverse Engineer command in the VSCode extension connects to the IBM i, reads the DDS (and optionally its associated LFs as @Indexes) and generates the corresponding .warp file. If the record format of the downloaded DDS differs from the table name, Record Format : "X" is written; if it is the same, it is omitted.
DDS comments (lines with * in column 7) are kept too: the file header ones become // lines above @Structure, and those preceding a group of fields become // lines right before the group’s first field. Decorative asterisk borders are left out.
Screens: @Layout and Screen
A Screen is an interactive 5250 screen: it defines its layout (@Layout) and reacts to user events (@Source).
1. Minimal structure
@Properties {
Type : Screen
Name : SFORM01
Description : "Sample screen"
Path : Screens
}
@References {
Tables = ["../tables/TLADD50.warp"]
}
@Variables {
Global {
Username char(10)
MyDate Date
}
}
@Parameters {
}
@Layout {
@Label(1,2,&Username)
@Label(1,70,&MyDate)
@Input("txtRequestNo", 3, 1, "Request No.: ", &RequestNo, true)
}
@Source {
Event Init
&Username = UserId()
&MyDate = Today()
EndEvent
}
Unlike a Program, a Screen doesn’t have its own @Structure — it only references tables to read/write.
Screen parameters
The @Parameters block in the example is empty, but a Screen (full screen or Popup) can declare it just like a Program: @Parameters { in:&X, inout:&Y, out:&Z }. Values are available from Event Init and the Screen is invoked with Call(Name, ...). In a Popup it is used to receive data and return the user’s selection. See @Parameters.
2. @Layout controls
-
Label(x, y, text): static text/variable.x,y = 1,1(exact corner) is forbidden — DDS rejects it. -
Input("Name", x, y, text, &Variable, enabled): editable field."Name"is whatCurrentInput()returns when the cursor lands there.enabledis optional (defaulttrue): a literal,&Variable, aFunction()returningnumber, or a full condition (Not &Locked = 1 And &Status <> "9").If the variable is
char/varchar, the field accepts lowercase and the program receives what was typed as is (Lima, notLIMA); to ignore the difference when comparing, useUpper(&field). Numeric, date and timeInputfields are unchanged. -
DataGridView("Name", x, y, rows)[...]: grid — see DataGridView.
Control names can be prefixed with @ (@Label, @Input) or not — it’s purely stylistic, same meaning.
Available width and error E00353
No element may end beyond the last column of the screen: 80 by default, 132 if the project declares that size, or the window width in a popup. Ending exactly on the last column fits. If it does not fit, the compiler reports E00353 (IBM i would reject the field or split it across two rows). The width of each element is computed as follows:
-
Label: the length of the text.
-
Input: the label + 1 space + the width of the variable.
-
DataGridView: the sum of the widths of its visible columns, plus the gap between each pair of contiguous columns. A column’s width is the larger of the header and the data. The gap is the declared separator or, if there is none or it is shorter, at least 1 position (a space).
Hiddencolumns do not count: they are declared as hidden subfile fields, with no line or position on the screen.In 5250 every field carries an attribute byte right before its first character; without that free position, a column’s attribute byte would overlap the last character of the previous one (
CPD7866) and the column would not be displayed. So, with no separator, each column sits one position further right than the previous one.Example: three columns of 6, 20 and 15 positions, with no separator, take 6 + 1 + 20 + 1 + 15 = 43 positions; with the grid at column 2 they end at column 44.
The column where the element ends is x + width - 1; if it exceeds the maximum, shorten the text or the widths, or move the element to the left.
Color and attributes with Style
Label, Input and Column (of a DataGridView) accept one or more trailing Style(...) entries with a color (Green, White, Red, Blue, Yellow, Pink, Turquoise) and attributes (Reverse, High, Underline, Blink, Hidden). A Style can end with a condition, in which case it applies only while the condition holds:
Label(1,2,&Title,Style(Reverse,High))
Label(5,2,&Desc,Style(Red,&Status = "9"),Style(Green))
Input("TxtOp",7,2,"Operation:",&Op,Not &Locked = 1,Style(Yellow,Underline))
Each element accepts at most one Style without a condition and up to three with a condition. In a Column the condition is evaluated per row, at the time of LoadRow(), with the values the variables hold at that instant (assign them before LoadRow()); the color is stored with the row:
Column("Quantity",&VQty,Style(Red,High,&VQty > 100),Style(Green))
Per-row dynamic enabling (a conditional Column(..., Input)) does not exist yet. Full details in @Layout: Style.
3. Standard events
| Event | When it fires |
|---|---|
Init | Once, before the first display. |
Load | Once, before the first display — typically fills the DataGridView with LoadRow() (the grid is reset on entry). |
Enter | On pressing Enter (with no function key active). |
Refresh | On pressing F5, if declared in the project’s @FunctionKeys. Then reloads the DataGridView, if any. |
Close | Fixed to F3 — always runs and closes the screen, no declaration needed. |
@Source {
Event Init
&Username = UserId()
EndEvent
Event Load
For Each In LADD50 Index LADD5010
Where SPT25COUNTRY = &nCountry
&cState = ADSTS50
LoadRow()
EndFor
EndEvent
Event Enter
&Control = CurrentInput()
Message("Active control: " + &Control, Info)
EndEvent
}
Two statements help control the screen from any event: &Variable.SetFocus() places the cursor on an Input, and Refresh / Refresh Keep reload the DataGridView. See @Source: Event and Screen.
4. Custom function keys
Besides the standard events, you can attach an event to any F1-F24 key (F13-F24 via Shift):
@Source {
Event 'FindCustomer' 4
Message("F4 was pressed", Info)
EndEvent
}
By default, the function-key line shows the event name (F4=FindCustomer). To show a different text, add it in quotes after the key (up to 20 characters):
Event 'FindCustomer' 4 "Find"
The event name ('FindCustomer') becomes the name of an RPG subprocedure, so it must be an identifier: letters, digits and _, starting with a letter (E00357). Text with spaces goes in the third value: Event 'Delete' 7 "Delete row".
This text takes precedence over the one declared in the project’s .warpcfg @FunctionKeys — see Project.
Position and color of the key line
By default the line is drawn at Rows - 1 (row 23 on a 24x80 screen; in a popup, the window’s last row minus one). Function Keys Row picks another row, and Function Keys Color colors the whole line. Both are declared in @GeneratorConfig/@Screen (project-wide) or in a Screen’s @Properties (that object only, and it wins over the project):
@Properties {
Type : Screen
Name : SCUSTOMERS
Function Keys Row : 22
Function Keys Color: Blue
}
The row must be between 1 and Rows - 1 (the last one is kept for messages); otherwise E00346 is reported. The compiler does not warn if the row overlaps the @Layout: leave it free. An invalid color gives E00347.
Each Event can carry its own color at the end, which wins over the line’s:
Event 'Delete' 7 "Delete" Red
The colors are Green, White, Red, Blue, Yellow, Pink and Turquoise.
Key rotation with F24
If F24 is declared in @FunctionKeys (for example F24 : "More keys") and no Event uses it, the keys that do not fit in one line are split into rotating sets. With F3 : "Exit", F5 : "Refresh" and the events Event 'Home' 6, Event 'Output' 7, Event 'Cursor' 8 and Event 'After' 9 "After Refresh":
Set 1: F3=Exit F5=Refresh F6=Home F7=Output F8=Cursor F24=More keys
Set 2: F9=After Refresh F24=More keys
Each set fits in a single line (screen width minus 2 columns; the window width in a popup), keys are ordered by number and every set ends with F24. Pressing F24 shows the next set and after the last one it returns to the first; the keypress only changes the key line and does not run any event. If all keys fit in one line there is no rotation. If F24 is not declared, or an Event uses it, the line shows all keys in one or two lines.
5. Messages to the user
Message("Rows found: " + String(&nRows, 5), Info)
Error (red), Warning (yellow), Info (normal color). Shown on the next refresh and cleared after one cycle.
6. Next step
If the screen needs to show a list of rows with selection/pagination, see DataGridView.
DataGridView: paginated grids
DataGridView shows a list of rows from a table on a Screen, with native pagination (Page Up/Down) over a DDS subfile. There can only be one per Screen.
1. Full example
@Properties {
Type : Screen
Name : SGRIDDEMO
}
@References {
Tables = ["tables/TLGRIDDEMO.warp"]
}
@Variables {
global {
VCustCode char(6)
VName char(20)
VCity char(15)
VTotal number(5)
VLast char(6)
}
}
@Layout {
DataGridView("GridDemo", 2, 2, 5)
[
Column("Code", &VCustCode)
Column("Name", &VName)
Column("City", &VCity)
]
}
@Source {
Event Load
For Each In GRIDDEMO Index GRIDDMNOM
&VCustCode = CustCode
&VName = Name
&VCity = City
LoadRow()
EndFor
EndEvent
Event Enter
&VTotal = 0
For Each Row
&VTotal = &VTotal + 1
&VLast = &VCustCode
EndFor
Message("Rows: " + String(&VTotal, 5) + " Last: " + &VLast, Info)
EndEvent
}
Between two contiguous visible columns there is always at least one free position (the separator, if declared; otherwise a space). This counts toward the E00353 width calculation: see Available width and error E00353.
2. Loading the grid: Event Load + LoadRow()
LoadRow() takes the current values of the variables bound to each Column and appends a row to the grid. It is valid anywhere in the body of any Event of a Screen — directly, inside If, Case, While, For or For Each — so it can also load the grid with data that does not come from a table. A For Each that loads the grid must declare Index (E00201). There can be several loads in the same event: all of them append rows to the same grid. In Event Load the grid is reset only once, when the event starts; in any other Event or Procedure the rows are appended to the ones already there. It loads the entire result set in a single pass — Page Up/Down are native terminal scrolling over what’s already loaded; Load never runs again.
LoadRow() can also be used inside any Procedure of the Screen, regardless of where it is invoked from (with Do). A Function cannot use LoadRow(), and it cannot be used outside an Event or Procedure either (E00198).
Event Load
Do Carga()
EndEvent
Procedure Carga()
For &Indice = 1 To 5
&VCod = &Indice
&VNombre = "Item " + String(&Indice, 3)
LoadRow()
EndFor
EndProc
A Procedure with LoadRow() can also be called from another event. Here Enter appends rows to the ones Load already loaded (and F5/Refresh runs Load again, which resets the grid):
Event Enter
Do AddRows()
EndEvent
Procedure AddRows()
&VCod = 99
&VNombre = "Added row"
LoadRow()
EndProc
Or directly in the event, with no procedure and no table:
Event Load
&VCod = 1
&VNombre = "First"
LoadRow()
&VCod = 2
&VNombre = "Second"
LoadRow()
EndEvent
Related rule: a local variable with the same name as a Column variable shadows the global one (LoadRow() would copy the local value), error E00356. See Diagnostics.
3. Reading the selection: RefreshSelectRow() and CurrentInput()
When the cursor lands inside the grid’s rectangle (header, attention row, or any data row), CurrentInput() returns the grid’s name. RefreshSelectRow() re-reads the row under the cursor and writes each column (including Hidden ones) back into its bound variable — it’s the reverse of LoadRow(). This is how to recover the value of a Hidden column: those columns travel in the subfile record as hidden fields (usage H in the DDS), with no line or position on the screen:
Event Enter
&Control = CurrentInput()
If &Control = "GridDemo"
RefreshSelectRow()
Message("You selected: " + &VCustCode, Info)
EndIf
EndEvent
4. Iterating over all loaded rows: For Each Row
For Each Row ... EndFor iterates over all rows already loaded into the subfile (not the database table), writing each Column into its bound variable before every iteration — same direction as RefreshSelectRow(). It doesn’t support Index/Where/When None because there’s no table being iterated:
Event Enter
&VTotal = 0
For Each Row
&VTotal = &VTotal + 1
EndFor
EndEvent
5. Hidden columns
Column("Internal ID", &VInternalId, Hidden)
A Hidden column loads its data (participates in LoadRow()/RefreshSelectRow()/For Each Row) but doesn’t show its cell or header title — useful for carrying an internal key without taking up screen space.
6. Editable columns (Input)
Column("Quantity", &VQuantity, Input)
Lets the user edit the value directly in the grid.
Reloading the grid: Refresh and Refresh Keep
To run Event Load again (for example, after changing a filter) use Refresh (goes back to the first row) or Refresh Keep (keeps the row where the user was). The reload happens when the current event ends. F5, if declared in @FunctionKeys, runs Event Refresh and then reloads the grid. See Refresh and Refresh Keep.
7. Rules to keep in mind
- The name (
"GridDemo") is required and must be unique amongInput/DataGridViewcontrols on the same screen. - Only one
DataGridViewperScreen. - Only valid inside a
Screen’s@Layout, never aProgram’s.
Reports with Member() / Export()
Pattern for generating a report: accumulate rows into a physical table (a data member exclusive to the user/run) and, when done, dump it to a text file on the IFS.
This feature uses
varcharin@Variables— see Data types.
1. Full example
@Properties {
Type : Program
Name : PTESTME
}
@References {
Tables = ["../tables/TLT3D01.warp"]
}
@Variables {
global {
User char(10)
Destination varchar(200)
T301TIPLOT char(1)
T301NROTAR char(19)
}
}
@Source {
&User = USERID()
&Destination = "/tmp/report.txt"
Member(LT3D01, &User)
For Each In LT3D01 Index LT3D0101
Where T301TIPLOT = &T301TIPLOT
Where T301NROTAR = &T301NROTAR
// ... accumulate rows into LT3D01, e.g. with New In LT3D01 ...
EndFor
Export(LT3D01, &Destination)
}
2. Member(Table, expr): one member per user/run
Member(LT3D01, &User)
Attaches a data member (parameterized by expr, typically &User so each user gets their own) to Table for the whole program:
- Creates it with
ADDPFMif it doesn’t exist yet (tolerates the error if it already exists). - Leaves it active with
OVRDBF— effective for the rest of the program, including any open of that table and any laterExport(...)over it.
Position rules (at most once per table, in either case):
- In a
Program: only at the top level of@Source(not nested insideIf/While/For/Event/etc.). - In a
Screen: only at the top level ofEvent Init(not nested, and not in any otherEvent) —Initruns exactly once, before the screen’s main loop, the same place aProgram’s prologue would go:
@Properties {
Type : Screen
Name : SGRIDDEMO
}
@Source {
Event Init
Member(LT3D01, &User)
EndEvent
Event Load
For Each In LT3D01 Index LT3D0101
// ... fills the DataGridView, or reads/writes rows ...
EndFor
EndEvent
}
Export(...) still isn’t allowed in a Screen — there’s no use case for exporting a report from an interactive screen.
Important:
Member(Table, expr)must appear before anyFor Each/NewoverTablein the same body — using it after already reading/writing the table is a runtime error, not a compile-time one.
3. Export(Table, destination): dump to IFS
Export(LT3D01, &Destination)
Copies Table’s active member (the one Member(...) set, if any) to an IFS stream file with CPYTOIMPF, comma-delimited format (RCDDLM(*CRLF) STRDLM(*NONE) FLDDLM(',')), replacing the destination if it already exists (MBROPT(*REPLACE)).
Rules: only valid in Program; unlike Member, no position restriction — use it wherever convenient, typically after the For Each/New that filled the report table.
Project: .warproj and .warpcfg
Every .warp file is compiled in the context of a project (.warproj), which in turn points to a generator config file (.warpcfg) holding the code generation rules and the IBM i deployment settings.
1. .warproj
@Project {
Name : Example
Description : "Sample project"
Version : 1.0
Default Profile : Dev
@Generator {
Name : "waRPGate generator"
Language : RPGLE
Description : "RPGLE generator"
@Paths {
Config : ../example/generator/rpg/rpg.warpcfg
Output : output/rpg
}
}
}
@Paths/Config: path to the.warpcfg(relative to the.warproj).@Paths/Output: folder where the generated RPGLE/DDS is written.
2. .warpcfg
@GeneratorConfig {
Date Format : DMY
Date Separator : /
Time Format : 24H
Time Separator : :
Print Mode : File
Commitment : True
Commit on Exit : True
DDS Name : <ObjectName>
@Screen {
Rows : 24
Columns : 80
@FunctionKeys {
F3 : "Exit"
F5 : "Refresh"
F24 : "More keys"
}
}
@Profile "Dev" {
@Connection {
Host : DEVSERVER
User : myuser
Auth Method : key
Key File : ~/.ssh/id_rsa_ibmi
Protocol : ssh
Port : 22
Timeout : 30
}
@Deployment {
CL Source : QCLSRC
Cleanup : true
Target Release : *Current
Optimization : *FULL
Debug : false
Temp Path : /tmp
Data Library : DTALIB02
Objects Library : OBJLIB01
Source Tables : QDDSSRC
Source Programs : QRPGLESRC
Compile Library : DTALIB01,DTALIB02,DTALIB03,OBJLIB01
}
}
@Profile "Prod" {
@Connection {
Host : PRODSERVER
User : myuser
Auth Method : key
Key File : ~/.ssh/id_rsa_ibmi
Protocol : ssh
Port : 22
Timeout : 30
}
@Deployment {
CL Source : QCLSRC
Cleanup : true
Target Release : *Current
Optimization : *FULL
Debug : false
Temp Path : /tmp
Data Library : DTALIB02
Objects Library : OBJLIB01
Source Tables : QDDSSRC
Source Programs : QRPGLESRC
Compile Library : DTALIB01,DTALIB02,DTALIB03,OBJLIB01
}
}
}
What each block controls
- Top level (
Date Format,Print Mode,Commitment,Commit on Exit,DDS Name): code generation rules, applied to the whole project.Commitment/Commit on Exitcan be overridden per object in a specific.warpfile’s@Properties. @Screen: default screen size (24x80 if omitted) and the function keys (@FunctionKeys) available to the project’sScreens — the label shown in the attention line. AScreenattaches a key to its own event withEvent 'Name' <n> ... EndEvent.@Profile "Name": a deployment environment (Dev,UAT,Prod, the name is free-form). Each profile groups its own@Connection,@Deployment, and, optionally,@License. A.warpcfgmust declare at least one@Profile.@Connection: how the compiler reaches the IBM i for--action build/reverse(doesn’t apply tovalidate/generate, which are purely local).@Deployment: libraries and source member where the generated code gets uploaded and compiled (CRTPF/CRTLF/CRTCLPGM).
With more than one @Profile, it’s necessary to indicate which one to use: either with --profile <Name> on the CLI, or by leaving Default Profile set in @Project (as in the .warproj example above). The VSCode extension has its own picker — see VSCode commands.
See Project file for the full list of keys and their allowed values.
Licensing
The compiler’s generate, build and reverse actions require a license. The validate action does not, so source validation (and the extension’s IntelliSense) always works.
The license is managed automatically: it’s taken when using the compiler and released when it’s done, with no manual intervention.
License modes
Organization
A license service manages a pool of seats. The compiler:
- Connects securely to your organization’s license service (identifying the server with
Fingerprint, in the project configuration).Host/Port/Fingerprintare handed out by whoever administers that service — see Setting up the license service if that’s you. - Each user holds a single seat at a time.
- If the compiler closes or loses the connection, the seat is released automatically — no one is left holding a license they’re not using.
- Compatible with Kerberos/Active Directory authentication, to integrate with your existing corporate credentials.
- On macOS Kerberos authentication is not available: if your organization’s license service requires Kerberos, the macOS compiler cannot obtain a license from that service. Windows and GNU/Linux do not have this limitation.
- If the
.warpcfghas aHost, the service takes precedence: there is no automatic fallback to the individual license file. - Without a domain (no Kerberos),
license-statusin organization mode is only accepted from the service’s own machine, unless the administrator enables remote queries — see Setting up the license service.
Individual
A signed license file, bound to the machine, validated locally with no network. The default path is:
- Linux/macOS:
~/.config/warpgate/license.json - Windows:
%APPDATA%\warpgate\license.json
Another path can be set with File (in @License), --license-file or WARPGATE_LICENSE_FILE.
Configuration
@License block in the .warpcfg
Optional block inside each @Profile (see Project file) — each profile (Dev/UAT/Prod) can have its own license configuration, to consume different licenses or licensing services depending on the environment. Properties are case-insensitive.
@GeneratorConfig {
@Profile "Prod" {
@License {
Host : "servidor01.dominio.local"
Port : 7443
Fingerprint : "3f2a9c41d87b05e6a1c4f0937be2d5688a1f4c0d29e7b3a65c8d1f0e4b7a9c23"
}
}
}
--profile/Default Profile picks which profile (and therefore which @License) to use on each run — see Project: .warproj and .warpcfg.
| Key | Description |
|---|---|
Host | License service host. If present, the mode is organization and File is ignored. |
Port | Service TCP port, 1 to 65535. Default 7443. |
Fingerprint | SHA-256 fingerprint of the service certificate: 64 hexadecimal characters (uppercase and : separators are accepted). |
SPN | Kerberos service name. Default warpgate-license/<host>. |
File | Path to the individual license file. |
Without Host, the mode is individual.
Flags and environment variables
| Flag | Environment variable | Equivalent to |
|---|---|---|
--license-host | WARPGATE_LICENSE_HOST | Host |
--license-port | WARPGATE_LICENSE_PORT | Port |
--license-fingerprint | WARPGATE_LICENSE_FINGERPRINT | Fingerprint |
--license-spn | WARPGATE_LICENSE_SPN | SPN |
--license-file | WARPGATE_LICENSE_FILE | File |
Precedence: flag > environment variable > .warpcfg.
With several @Profile declared, license-status also respects --profile/Default Profile to know which @License to read from — see Project: .warproj and .warpcfg.
Exit codes
| Code | Meaning |
|---|---|
3 | No license available. The compiler prints a message with what to do. |
4 | The license was lost mid-run. |
130 | Run interrupted (Ctrl-C/SIGTERM). |
Requesting a license
These support actions don’t take a seat and don’t need --source; --project is optional.
warpgate --action license-request --license-id <ID> --license-out request.json
It writes the request file (--license-out is optional; default request.json) and shows the machine identifier. Once you receive the license file, copy it to the default path (or to the one set with File). Installing a new license replaces the previous one: the previous one stops being valid by serial number.
How to get a license (free for now, time-limited)
Warpgate is currently distributed free of charge: Software House issues free individual licenses, valid for 90 days and renewable. This policy may change later, without affecting already-issued licenses.
- On the machine where the compiler will run, generate the request:
warpgate --action license-request --license-id <name or company> --license-out request.json - Send to warpgate@softwarehouse.pe:
- The generated
request.jsonfile. - The machine identifier the command prints on screen (needed to issue the license).
- The generated
- Software House replies with a signed
license.jsonfile, tied to that machine, valid for 90 days. - Copy that file to the default path (or the one set by
--license-file/File/WARPGATE_LICENSE_FILE):- Linux/macOS:
~/.config/warpgate/license.json - Windows:
%APPDATA%\warpgate\license.json
- Linux/macOS:
- Confirm with
warpgate --action license-status(it should show the status and the expiry date).
Once the 90 days are up, generate/build/reverse stop working until you renew — repeat this same procedure to get a new license. validate keeps working regardless, even without a license. For a license with seats for several users (organization mode), also write to warpgate@softwarehouse.pe.
From VSCode
The same steps can be done from the command palette (Ctrl+Shift+P / F1), without using the console — see VSCode commands:
WaRPGate: Request License— asks for the name or company, savesrequest.jsonand shows the machine identifier (Copy Machine ID and Reveal File buttons). Send both to warpgate@softwarehouse.pe.WaRPGate: Install License— select the receivedlicense.jsonand it is copied to the default path (it asks for confirmation if a license already exists, which would stop being valid).WaRPGate: License Status— shows the status, the expiry and the product.
To check the current state:
warpgate --action license-status
It shows the mode, the license, the expiration and, in organization mode, the seats in use and available.
License scope
A license covers one product and one major version (for example, the 0.x series). It can be perpetual or have an expiration date.
Development
Development builds accept WARPGATE_LICENSE=off to skip the requirement. Release binaries do not.
Related diagnostics
| Code | Situation |
|---|---|
E00284 | @License Port is 0. |
E00285 | @License Host is not a valid host. |
E00286 | Fingerprint is not a valid SHA-256 fingerprint. |
E00287 | Warning: Host without Fingerprint. |
E00288 | Warning: Host and File together (File is ignored). |
Setting up the license service (organization mode)
This page is for whoever administers your organization’s infrastructure (systems team, or the IBM i server administrator) — you don’t need to read it to program in WARP. It covers the one-time setup of the service that hands out licenses across your whole development team. People who just use Warpgate get three final values (Host, Port, Fingerprint) from the administrator and put them in their @License — see Licensing.
1. Download
license-service(administers the service): Linux · Windowslicense-gui(visual query tool, optional — see section 5): Linux · Windows- Checksums: SHA256SUMS.txt
On Linux, make them executable: chmod +x license-service license-gui.
2. Request the organization license
On the server where the service will run:
license-service create-request --license-id "<Your organization's name>" --out request.json
license-service gen-hwid
The second command prints several machine details; copy the primary_os_id value (a 32-character string). Send to warpgate@softwarehouse.pe:
- The
request.jsonfile. - The
primary_os_idvalue. - How many simultaneous seats your team needs.
Software House replies with a license.json file for that server, valid for 90 days and renewable — same free-of-charge model as the individual license, see Licensing.
3. Install the service
Windows, with the license-gui window (recommended)
The
license-guiwindow is only available in Spanish. Its tabs and buttons are written here exactly as they appear on screen, with a translation in parentheses the first time.
-
Keep
license-gui.exeandlicense-service.exein the same folder (for exampleC:\license). The service is registered pointing to that folder: don’t move it afterwards. -
Open
license-gui.exewith right click → Run as administrator. On the connection screen, click Servidor local: instalar y mantener… (“Local server: install and maintain”). -
The window has these tabs:
-
1. Solicitar licencia (request the license): enter the license identifier and click Crear solicitud (create request). It generates
request.jsonand arequest-hwid.txtwith the machine identifier (with a Copiar (copy) button). Send both files to Software House (see section 2). -
2. Instalar servidor (install server): choose the received
license.jsonand fill in:- Nombre del servidor (server name): by default, the computer’s full name.
- Puerto (port):
7443by default. - Red de usuarios (firewall) (users’ network):
LocalSubnetor a range (for example192.168.0.0/24). The firewall rule is only created if Windows Firewall is active in some profile. - Consultar con la GUI desde (query with the GUI from): networks allowed to query remotely (see section 7).
Then click Instalar servicio y arrancar (install service and start). In order, the window registers the service, installs the license, generates the certificate, adjusts the permissions of the data directory (only
SYSTEMand Administrators), creates the firewall rule if applicable, starts the service and checks that it is listening. At the end it shows the fingerprint and the@Licenseblock ready to copy to the clients.There is also Desinstalar servicio… (uninstall service): it stops and removes the service and removes the firewall rule. An optional checkbox also deletes the data (license, certificate, seats database and log); by default they are kept.
-
3. Certificado TLS (TLS certificate): to use the organization’s certificate (see section 6).
-
4. Log del servicio (service log): refreshes by itself every 3 seconds; it has Copiar and Limpiar log… (clear log) (empties the file without deleting it; the service keeps writing).
-
The Windows service is named WarpgateLicense and starts automatically. Its data lives in C:\ProgramData\license-service\:
| File | Contents |
|---|---|
active_license.json | Installed license. |
tls_cert.der, tls_key.der | TLS certificate and its key. |
seats.sqlite | Seats (sessions) database. |
auth.json | Optional: remote queries and limits (see sections 7 and 8). |
license-service.log | Service log. |
LDAP certificates: the LDAP client (
ldaps://) trusts the certificate authorities in the system store. If the domain controller uses an internal CA, that CA must be installed in the store of the machine running the service (on Windows, the Windows certificate store). Otherwise the LDAPS connection fails and the service denies access.
Windows, from the console (alternative)
license-service install-license --license license.json
license-service gen-tls-cert --subject-alt-name <server-host-or-domain>
license-service install-windows-service --bind 0.0.0.0:7443
This registers license-service as a regular Windows service (starts automatically, restarts itself on failure) — manage it from the Services console like any other. The second command prints the SHA-256 fingerprint on screen: that value is the Fingerprint needed by anyone connecting.
Linux, as a systemd service (recommended)
- Copy the binary and create a dedicated user for the service:
sudo install -m 0755 license-service /usr/local/bin/license-service sudo useradd --system --no-create-home --shell /usr/sbin/nologin warpgate-license sudo install -d -o warpgate-license -g warpgate-license -m 0700 /var/lib/license-service - Install the license and generate the certificate, as that user:
Write down the SHA-256 fingerprint the second command prints: that value is thesudo -u warpgate-license env LICENSE_SERVICE_DIR=/var/lib/license-service \ license-service install-license --license /path/to/license.json sudo -u warpgate-license env LICENSE_SERVICE_DIR=/var/lib/license-service \ license-service gen-tls-cert --subject-alt-name <server-host-or-domain>Fingerprint. - Download the systemd unit file, copy it and enable the service:
sudo cp license-service.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable --now license-service
This makes the service start on boot and restart itself on failure. Logs: journalctl -u license-service -f. Only open TCP port 7443 to the subnet of Warpgate users.
Linux, quick test (no systemd)
To test before installing it as a permanent service:
license-service install-license --license license.json
license-service gen-tls-cert --subject-alt-name <server-host-or-domain>
license-service run --bind 0.0.0.0:7443
Runs in the foreground; stops when you close the terminal. Useful to verify everything works before the step above.
4. Hand the values to your team
Each developer puts these three values in their project’s @License (see Licensing):
- Host: the server where the service runs.
- Port:
7443(or whatever was set with--bind). - Fingerprint: the one
gen-tls-certprinted.
If the .warpcfg has a Host, the service takes precedence: there is no automatic fallback to the individual license file, so if the service goes down, generate/build/reverse fail. Host without Fingerprint is an error.
If your organization uses Active Directory, the service can require Kerberos authentication — contact Software House to enable it.
Mode without a domain (no auth.json)
If Kerberos is not configured, the identity of whoever takes a seat is the name the machine declares (not verified). In addition:
- Queries (
license-gui,license-service queryand alsowarpgate --action license-statusin organization mode) are only accepted from the service’s own machine. From another PC, the service rejects the query:warpgateshows “Your user is not authorized to use the warpgate license. Ask the administrator for access.”,license-guishows “no estás autorizado para esta operación” (“you are not authorized for this operation”), and the service logsconsulta rechazada desde <IP>(“query rejected from” the client’s IP). To allow remote queries, see section 7. - Taking a seat (
generate,build,reverse) has no such restriction: it works from any PC that can reach the service. - Installing or replacing licenses is always only done from the server (or by Kerberos administrators).
5. Checking the service with license-gui
license-gui is a separate window, no command line, for checking the license status without touching the seats in use (it doesn’t consume a seat): active license and expiry, seats in use and available, and each user’s session history (including whether a session closed normally or was lost).
On opening, it asks for the same three values as any client — Host, Port, Fingerprint — plus an optional SPN if the service requires Kerberos. Whoever administers the service can also install a new license from there, with no restart needed.
Its tabs are Usuarios, Licencias (installing a license is only allowed to administrators), Estadísticas and Log del servicio. From another PC it only works if remote queries were enabled (or with Kerberos).
6. TLS certificate
By default the service uses a self-signed certificate, valid from 1975 to 4096 (practically never expires). The client does not check dates, CA chain or name: it only compares the certificate’s SHA-256 fingerprint with the configured Fingerprint.
If security requires the organization’s certificate, it can be used from the 3. Certificado TLS tab of the window, choosing the type:
| Type | Files | Notes |
|---|---|---|
| PFX/P12 | One file + password | What Windows exports; accepts modern and legacy PFX. |
| PEM | Certificate (.pem/.crt/.cer) + key (.key/.pem) | The key may be in the same file. Accepts PKCS#8, classic RSA and classic EC keys. |
| DER | Binary certificate and key | — |
The window converts to the internal format and saves (the previous one is kept as tls_cert.der.previous and tls_key.der.previous), restarts the service if the option is checked, and shows the fingerprint (with a Copiar button) and the @License block. If the file contains the full chain, the server’s own certificate (the one matching the key) is used.
Errors explained by the window: key does not match the certificate, wrong PFX password, wrong type, and PEM key encrypted with a password (not supported: remove the password with openssl pkcs8 -in key.pem -out key-no-password.pem -nocrypt, or provide a PFX).
Important: changing the certificate changes the fingerprint, and it must be updated on all clients (
@Licenseand thelicense-guiconnection screen). Renewing a corporate CA certificate changes it again.
Console equivalent (then restart the service):
license-service import-tls-cert --format pem|pfx|der --cert <file> [--key <file>] [--password-env VARIABLE]
7. Read-only remote queries
Without Kerberos, for license-gui and license-status to query from other PCs, create the auth.json file in the data directory (C:\ProgramData\license-service\auth.json; /var/lib/license-service/ with systemd):
{ "query_allowed_networks": ["192.168.0.10", "10.1.2.0/24"] }
- Accepts IPs or CIDR ranges, IPv4 and IPv6.
- Only applies without Kerberos and requires restarting the service.
- Create the file without BOM (strict JSON).
- From those networks, users, history, statistics and log are visible, but licenses cannot be installed and the Servidor local… button does not appear.
- Addresses are authorized, not people: keep the list short.
Shortcut: the Consultar con la GUI desde field on the 2. Instalar servidor tab writes this auth.json (it does not overwrite an existing one).
8. Connection limits
The service drops a new connection before TLS and notes it in the log with the IP (1 out of every 50 rejections). It does not affect anyone who already holds a seat.
- Total: seats × 2 + 20 (computed from the license).
- Without a session yet (handshake or authenticating): maximum 50.
- Per IP: 5 by default; configurable with
"max_connections_per_ip": Ninauth.json(0= no limit). Connections from the service’s own machine are exempt from this cap.
Raise the per-IP value if many users share an IP (build server, remote desktop). Requires restarting the service.
9. Statistics and export
license-gui queries and administers without consuming a seat. The Estadísticas tab lets you choose the period (7, 30, 90 days or 1 year) and shows:
- Sessions and distinct users.
- Peak of simultaneous seats and when it happened.
- Percentage of usage time with all seats taken, and usage time.
- Rejections due to lack of seats (recorded only since this version; there is no earlier data).
- Charts of the peak per day and per hour of day (local time), and a per-day detail.
- A guiding reading: “consider expanding” if there were rejections; “at the limit” if the pool was full 5% or more of the usage time; “spare capacity” if the peak was half or less of what was contracted. It warns if there are fewer than 7 days with usage.
It helps decide whether to buy more seats.
The Exportar ▾ menu offers: Resumen de usuarios (CSV) (users summary), Historial de sesiones del período (CSV) (session history for the period), Log del servicio (texto) (service log, as text) and Estadísticas del período (CSV) (statistics for the period). The CSVs use ; and a UTF-8 BOM (they open correctly in Spanish-locale Excel) and neutralize values starting with =, +, - or @ (formula injection). History and log require being able to see all users.
10. Common problems
| Symptom | Cause and fix |
|---|---|
| “not authorized” from another PC | Queries are only accepted from the server (section “Mode without a domain”). Enable section 7 or use Kerberos; check the log. |
| Fingerprint error | The fingerprint was copied wrongly or the certificate changed (section 6). |
TcpTestSucceeded : False | Firewall, network, or the service is not listening. |
| The service does not start | Check license-service.log. Typical cause: files in C:\ProgramData\license-service without permissions for SYSTEM (installing with the window already adjusts them). |
Structure of a .warp file
A .warp file is a sequence of @Name { ... } sections. Which sections apply depends on @Properties/Type:
| Section | Program | Screen | Table |
|---|---|---|---|
@Documentation | top level | top level | nested inside @Structure |
@Properties | yes | yes | yes (only Type) |
@Structure | no | no | yes |
@Fields | no | no | nested inside @Structure |
@Indexes | no | no | nested inside @Structure |
@References | yes | yes | no |
@Variables | yes | yes | no |
@Parameters | yes | no | no |
@Layout | yes (optional) | yes | no |
@Source | yes | yes | no |
A .warproj/.warpcfg doesn’t describe an object — they use their own top-level sections (@Project, @GeneratorConfig) — see Project file.
General syntax
- Comments:
//to end of line. - Strings: double or single quotes interchangeably (
"text"and'text'are semantically the same). - Variables: always with
&(&Username); table field names go without&inside aFor Each/New. - Every top-level section carries
@; control names inside@Layoutmay carry@optionally (@Label/Labelare equivalent).
Reference index
- @Documentation
- @Properties
- @Structure, @Fields, @Indexes
- @References
- @Variables
- Data types
- @Parameters
- @Layout
- @Source: control statements
- @Source: Event and Screen
- Builtin functions
- Expressions and operators
- Project file
@Documentation
Informational metadata, doesn’t affect code generation.
In Program/Screen (top level)
Flat Key : value pairs:
| Key | Description |
|---|---|
Author Name | Name of who wrote the program. |
Author User | User ID of who wrote it. |
App Id | Short application identifier. |
App Name | Visible application name. |
App Description | Long application description. |
Module Id | Short module identifier. |
Module Name | Visible module name. |
Module Description | Long module description. |
Created At | Free-form date/timestamp (e.g. "12/06/2025"), not parsed or validated. |
@Documentation {
Author Name : Programmer Name
Author User : User Code
App Id : Application ID
App Name : Application Name
Module Id : Module ID (if applicable)
Created At : "12/06/2025"
}
In Table (nested inside @Structure)
Here @Documentation has no keys of its own, only sub-sections:
@Author:Name,User,Company.@Application:Id,Name,Description.@Module:Id,Name,Description.@Repository:Official,Type(e.g.git),Branch,Version.@History: container for@Creationand any number of@Changeentries.@Creation:Date,Author,User,Company,Reason.@Change(repeat the block for each entry):Date,Type(by conventionbreaking/feature/fix/security/performance/refactor/config/docs, not enforced by the compiler),Impact(by conventionhigh/medium/low),Version,Reason,Author,User,Company.
@Structure {
Name : "GRIDDEMO"
@Documentation {
@Author {
Name : Programmer
}
@History {
@Creation {
Date : "2025-06-12"
Author : Programmer
Reason : "Initial creation"
}
@Change {
Date : "2026-01-10"
Type : feature
Impact : medium
Reason : "Adds City column"
}
}
}
@Fields { ... }
}
@Properties
Program
| Key | Description |
|---|---|
Type | The kind of object this file compiles to (Program). |
Name | Program name. |
Description | Program description. Maximum 50 characters: it ends up in the TEXT parameter of the compile-time CL commands (CRTBNDRPG/CRTDSPF/CHGPFM), which has that real limit on IBM i (see E00281). |
Print Mode | Print mode, for a report-style program. |
Commitment | Whether this object (Program or Screen) runs its modified tables under commitment control. Overrides the project’s Commitment (@GeneratorConfig) only for this object. Values: True/False. Required by the Commit/Rollback statements. |
Commit on Exit | Whether this object (Program or Screen) issues a final commit before finishing; in a Screen, when the screen is closed (F3). Overrides the project’s value only for this program; only effective if Commitment is enabled. Values: True/False. The Commit/Rollback statements require Commitment enabled. |
DDS | Name of the DDS this program is attached to. |
Path | Output folder for this program, relative to the project. |
@Properties {
Type : Program
Name : PTEST01
Description : "Test program"
Commitment : True
Commit on Exit : True
Path : "./programs/"
}
Screen
Same keys as Program (Type with value Screen), plus 7 of its own:
| Key | Description |
|---|---|
Popup | Generates this Screen’s DSPF with the DDS WINDOW keyword (a pop-up window) instead of a full screen. Requires the 4 keys below. Values: True/False. |
Window Row | Row (1-based) of the window’s top-left corner. Required if Popup: True. |
Window Column | Column (1-based) of the window’s top-left corner. Required if Popup: True. |
Window Rows | Window height, in rows. Required if Popup: True. |
Window Columns | Window width, in columns. Required if Popup: True. |
Function Keys Row | Row where this Screen’s function-key line is drawn (also FunctionKeysRow). Takes precedence over Function Keys Row in @GeneratorConfig/@Screen; with neither, Rows - 1 is used. Valid range: 1 to Rows - 1; out of range gives E00346. That row must be left free in @Layout: there is no overlap warning. |
Function Keys Color | Color of this Screen’s whole key line (also FunctionKeysColor): Green, White, Red, Blue, Yellow, Pink or Turquoise. Takes precedence over the project’s; an invalid name gives E00347. |
@Properties {
Type : Screen
Name : SCONFIRM
Popup : True
Window Row : 5
Window Column : 10
Window Rows : 10
Window Columns : 40
}
Example with the key line moved and colored:
@Properties {
Type : Screen
Name : SCUSTOMERS
Function Keys Row : 22
Function Keys Color: Blue
}
A Screen with Popup: True is generated, compiled and deployed exactly like any other Screen — only the DSPF changes (WINDOW instead of a full screen). To show it from another Screen, reference it in @References/Screens and call it with Call(Name, ...) — see @Source: control statements.
Table
Only Type:
@Properties {
Type : "Table"
}
The rest of a table’s description goes in @Structure — see @Structure, @Fields, @Indexes.
@Structure, @Fields, @Indexes
Only apply to table files (@Properties/Type : "Table").
@Structure
| Key | Description |
|---|---|
Name | Table name. Must start with a letter, letters/digits only, max 10 characters (DDS object name limit). |
Record Format (or RecordFormat) | Optional. Record format name (the DDS R line) of the PF and of every LF; the generated RPG uses it in rename(<format> : row). Without it, the format is named like the table (Name). The physical/logical file name and the LF PFILE() remain the table name. Useful for tables that already exist on the IBM i whose format differs from the file name. Up to 10 characters, letters and digits only, starting with a letter (E00349 otherwise). |
Description | Table description. |
@Structure {
Name : ORDERS
Record Format : ORDREC
Description : "Orders"
...
}
Also contains, nested: @Fields, @Indexes and optionally @Documentation (see @Documentation).
@Fields
One Name type(...) line per column:
@Fields {
CustCode char(6) Description("Customer code")
Name char(20) Description("Name")
Balance number(9,2) Default(0)
RegDate date
}
Allowed types: only char, number, date (not field/list/matrix — those are exclusive to @Variables, see Data types).
Modifiers:
| Modifier | Description |
|---|---|
Description("text") | Field description. Requires a quoted string. |
Default(value) | Default value: a quoted string or a bare number, depending on the field’s type. Not allowed on date fields. |
AllowNull | Allows NULL on this field. A PrimaryKey field can’t carry this modifier. |
@Indexes
One Kind(Name, [Fields...], "optional description") entry per access path:
@Indexes {
PrimaryKey ( GRIDDEMOPK, [CustCode] )
Unique ( GRIDDEMOEMAIL, [Email], "Unique email" )
Index ( GRIDDMNOM, [Name] )
}
| Kind | Description |
|---|---|
PrimaryKey | The table’s primary access path. Only one per table; its fields can’t be AllowNull. |
Unique | Unique secondary access path. |
Index | Non-unique secondary access path. |
Full example
@Properties {
Type : "Table"
}
@Structure {
Name : "GRIDDEMO"
Description : "Sample customers"
@Fields {
CustCode char(6) Description("Customer code")
Name char(20) Description("Name")
City char(15) Description("City")
}
@Indexes {
PrimaryKey ( GRIDDEMOPK, [CustCode] )
Index ( GRIDDMNOM, [Name] )
}
}
@References
Lists other .warp files this Program/Screen depends on. Unlike other sections, it uses Key = [ "..." ] (a bracketed list), not Key : value.
| Key | Description |
|---|---|
Programs | Other Program .warp files this one references (for Call(Name, ...) resolved at compile time). |
Screens | Screen .warp files this one references — same as Programs, enables calling it with Call(Name, ...) (typically a Screen with @Properties/Popup: True, shown as a pop-up window). |
Tables | Table .warp files this one references (enables For Each In/New In/field(...) over them). |
@References {
Programs = [
"../programs/POTHER.warp"
],
Screens = [
],
Tables = [
"../tables/TLADD50.warp",
"../tables/TLGRIDDEMO.warp"
]
}
Paths are relative to the file that declares them. Doesn’t apply to table files (a table doesn’t reference other objects).
@Variables
Declares variables inside a named scope: global (visible throughout the file) or the name of a Procedure/Function declared in @Source (visible only inside that procedure/function, in addition to global ones).
@Variables {
global {
Username char(10)
MyDate date
Balance number(9,2)
}
MyProcedure {
counter number(3)
}
Greet {
name char(10)
}
}
One Name type(...) line per variable — same format as @Fields, but with more available types (see Data types): char, varchar, number, date, field(Field), list(type), matrix(rows, cols, type), struct(TemplateName).
local scope
local is a reserved scope, alongside global and the names of Procedure/Function. Its variables are only visible in the main body of @Source (outside any Procedure/Function) and in @Layout. Using them inside a Procedure/Function produces E00146.
@Variables {
global {
Username char(10)
}
local {
counter number(3)
message char(40)
}
}
- It only makes sense in
Programtype programs; it isn’t supported inScreen. - It can’t repeat a name already declared in
global(E00298). - It doesn’t support
structtemplates (E00234, like any scope other thanglobal). - In the generated RPG it is declared at module level, just like
global; the visibility restriction is enforced by the compiler only. - An
@Parametersparameter can be declared inglobalor inlocal. Inlocalonly the main body sees it, not theProcedure/Functionblocks.
Struct: inline template definition
Inside the global scope (only there) you can also define a struct template with struct Name [ ... ], before or after instantiating it with struct(Name) — see Data types: Struct for the full syntax and its limits (including nested structs and list(struct(...)) as a member).
Modifier
Only Description("text") is available in @Variables (unlike @Fields, it doesn’t support Default/AllowNull):
@Variables {
global {
RequestNo field(ADHOJA50) Description("Request number")
}
}
Constants
Inside any scope (global, local or that of a Procedure/Function), in Program and in Screen, you can declare a constant with const Name type = value:
@Variables {
global {
const MaxItems number(3,0) = 100
const Price number(9,2) = -5.5
const Title char(30) = "Pass report"
const Greeting varchar(30) = "Hello" Description("Initial greeting")
}
}
- Only
char,varcharandnumberare supported; any other type producesE00310. - The value must be a literal of the same type: quoted text, or a number (integer or decimal) with an optional
-sign. It must fit the type (text no longer than the length; a number with no more decimals than the scale and no more integer digits than the precision allows) orE00311is reported. - It can be read in any expression but never modified: assigning to it, using it as a
Forcounter, passing it as anout/inoutargument of aCall, or using it as the target of anInputproducesE00312. Passing it to aProcedure/Functionthat modifies that parameter producesE00179. - An
@Parametersparameter cannot be a constant (E00313). - In a printable
Blockof aProgramit can be displayed, for example with@Label(1,1,&Title). In aScreenit cannot be displayed in a screen field (Label/Input/Column):E00314; copy it to a variable.
In the generated RPG it is declared as a named constant (dcl-c NAME value;):
- A
char(n)text is padded with blanks up ton, so it behaves like achar(n)variable; avarcharis not padded. - A number is written with exactly the decimals of its scale (
const Price number(9,2) = -5.5generatesdcl-c Price -5.50;). - An RPG numeric constant has no precision of its own (the context where it is used sets it), but WARP treats it as the declared type for type checks.
Length/precision limits (standalone variable, different from a table field)
| Type | Limit in @Fields | Limit in @Variables |
|---|---|---|
char | 1–32,766 | 1–16,773,104 |
varchar | not valid in @Fields | 1–16,773,100 |
number (precision) | 1–30 | 1–63 |
These larger limits in @Variables reflect that a variable declared there isn’t tied to a DDS physical file the way an @Fields field is.
Data types
| Type | Syntax | Where | Description |
|---|---|---|---|
char | char(length) | @Fields, @Variables | Fixed-length string. length required (1-32,766 in @Fields; 1-16,773,104 in @Variables). |
varchar | varchar(length) | @Variables only | Variable-length string. length required (1-16,773,100). Length(...) returns the actual content length at runtime, unlike char. |
number | number(precision, scale) | @Fields, @Variables | Numeric. precision required (1-30 in @Fields; 1-63 in @Variables); scale optional, default 0. |
date | date | @Fields, @Variables | Date, no parameters. In @Fields it can’t carry Default(...). |
time | time | @Fields, @Variables | Time (hour/minute/second), no parameters. In @Fields it can’t carry Default(...). |
timestamp | timestamp | @Fields, @Variables | Combined date and time (with microseconds), no parameters. In @Fields it can’t carry Default(...). |
field | field(FieldName) or field(Table.Field) | @Variables only | Inherits the type of a field from some referenced table’s @Structure. See field: inheriting a field’s type. |
list | list(type) | @Variables only | Dynamic list of type (scalar, struct(Template), or field(...); another nested list or matrix is not allowed, see E00283). No size in the declaration — the maximum internal capacity is fixed (9999 elements) and not configurable. Read/write an element with &Variable[index]; add/remove/query with methods (see List: methods). |
matrix | matrix(rows, cols, type) | @Variables only | Fixed-size 2D grid of rows x cols of type. Read/write an element with &Variable[row, col]. |
struct | struct(TemplateName) | @Variables only | Instance of a struct TemplateName [ ... ] template defined inline in the global scope (see Struct: templates and member access). Access a member with &Variable.Member. |
Constants (const Name type = value) only support char, varchar and number; see @Variables: Constants.
Examples
@Variables {
global {
Username char(10)
Destination varchar(200)
Balance number(9,2)
RegDate date
RegTime time
RegTimestamp timestamp
Status field(ADSTS50)
List1 list(number(5))
Board matrix(8, 8, char(1))
}
}
field: inheriting a field’s type
field(...) takes the type of the indicated field, looking it up in the tables declared in @References/Tables. It has two forms:
field(Field): looks the field up in all referenced tables. If it exists in more than one,E00149(ambiguous) is reported: “El campo ‘{field}’ de la variable field(…) es ambiguo: existe en más de una tabla de @References/Tables; indique la tabla con field(Tabla.Campo).”field(Table.Field): looks the field up only in that table, to disambiguate. Table and field names are case-insensitive. If the table isn’t referenced or the field doesn’t exist in it,E00148(field not found) is reported.
The resulting type is the field’s type in both forms. Both work in any @Variables scope and also as a struct member.
// Tables Customers and Suppliers (in @References/Tables) both have a Code field
@Variables {
global {
struct Contact [
Code field(Customers.Code)
]
CustomerCode field(Customers.Code)
SupplierCode field(Suppliers.Code)
Name field(Customers.Name) // also valid when the field isn't ambiguous
}
}
A bare field(Code) would report E00149 in this example.
char/varchar interoperability
char and varchar interoperate in assignments — the rule is purely about length (the source must fit into the destination), regardless of which one is fixed or variable:
&VarcharDestination = &CharSource // valid if length(CharSource) <= length(VarcharDestination)
&CharDestination = &VarcharSource
List/Matrix: indexing
&List1[1] = 100
&Board[3, 5] = "X"
The index is 1-based. List/Matrix are only valid in @Variables, never in @Fields.
List: methods
list carries no size in its declaration (list(type), no capacity parameter): the maximum internal capacity is fixed at 9999 elements, defined by the compiler and not configurable from the language. Instead of a fixed size up front, content is managed through methods, using member syntax (&Variable.Method(...)):
| Method | Use | Description |
|---|---|---|
Add(value) | statement | Appends value to the end of the list. value must be type-compatible with the list’s element type (if the element is struct(Template), value must be a &Variable of that exact same template). |
Clear() | statement | Empties the list: clears the elements in use (blanks or zeros) and sets the count to 0. |
Init() | statement | Leaves all the list’s elements (all 9999) at their empty value (blanks for char/varchar, zeros for number, initial value for date/time/timestamp, cleared structs) and the count (Count()) at 0. Takes no arguments (E00282) and cannot be used as an expression (E00280). |
Remove(index) | statement | Removes the element at position index (1-based, number type), shifts the following ones back one position and clears the last slot left free. An index outside 1..Count() does nothing. |
Split(text[, separators[, AllSep]]) | statement | Replaces the list contents (clears the previous ones) with the pieces of text. See Split(). |
Count() | expression | Returns the current number of elements (number type). Can only be used inside an expression, never as a standalone statement. |
Json() | expression | Returns the list as a JSON array (list(struct(...)) only). See Json(). |
&List1.Add(10)
&List1.Add(20)
&List1.Add(30)
For &i = 1 To &List1.Count()
// ... &List1[&i] ...
EndFor
&List1.Remove(2)
&List1.Clear()
&List1.Init()
There’s no For Each over an in-memory list — traversal is done with a classic For counter (For &i = 1 To &List.Count()) plus indexing (&Variable[&i]); For Each In remains exclusive to database tables (see @Source: control statements).
Real clearing. Clear(), Remove(index) and JsonSplit (when replacing the previous content) truly clear the positions that stop being used, rather than just adjusting the counter. This keeps the following invariant: every element after the last one is empty (blanks or zeros). An RPG program that receives the array (see @Parameters) can read it up to the first empty line. With 9999 elements there is no empty position after the last one.
Init() versus Clear(). In RPG, Init() is a single clear of the whole array (plus counter = 0), with no loop. Clear() only clears up to the counter: it is faster when used with few rows, but it leaves untouched any elements written by index (&List[10] = x) without going through Add(), because those writes do not move the counter. Init() clears the whole capacity (with char(250) that is about 2.5 MB), so use it at the start or when needed, not inside a heavily repeated loop.
The typical use of Init() is a list used as a fixed-size array (&Frame[1], &Frame[10]) that is then passed to another program with Call:
&Frame.Init()
&Frame[1] = "Header"
&Frame[10] = "Footer"
// Clear() would not clear &Frame[10]: it was written by index and Count() is still 0
&Frame.Init()
See the full example in example/programs/PINIT01.warp.
Note:
Clear(),Remove()andJsonSplitclear what comes after the last element.
If Add(value) is called while the list is already at its maximum capacity (9999 elements), the program issues a visible dsply with the message Lista {name} alcanzó su capacidad máxima (9999) and does not add the element — it doesn’t abort the whole program.
list(struct(Template)) is valid (a list of instances of the same struct). Nesting list/matrix inside each other is not, in any combination: list(list(...)), list(matrix(...)), matrix(list(...)), and matrix(matrix(...)) produce the compile error E00283 (“‘&{variable}’ es un {list/matrix} cuyo elemento no puede ser {list/matrix} (sin equivalente RPG generable): use un tipo escalar, struct(…) o field(…).”), caught in semantic analysis — it isn’t possible to declare an array whose element is itself another array.
Split(): split a text into a list
&List.Split(text[, separators[, AllSep]]) is a statement (like Add/Clear; used as an expression it is E00280) and is only valid on list(char(n)) or list(varchar(n)) (otherwise E00363).
- It replaces the list contents: it clears the previous ones and leaves the pieces of
text, up to 9999. When full it shows “Lista X llena (max 9999)” withdsplyand discards the rest. A piece longer than the element is truncated. - Without
separators, it splits on blanks. - Each character of
separatorsis a separator:"-;"splits on hyphen and on semicolon. - Without
AllSep, consecutive, leading and trailing separators are ignored:"one,two,,three"with","gives 3 pieces. - With
AllSep(third argument, the wordAllSep, which requiresseparators), each separator delimits two pieces and empty ones appear: the same text gives 4 pieces:one,two, `` andthree. - An empty text gives 0 pieces.
- Trailing blanks of a
charsource are ignored. Acharvariable used as separators is trimmed: its trailing blanks are not a separator (use a literal for a blank separator). textandseparatorsmust bechar/varchar(E00359); 1 to 3 arguments are allowed (E00358); the third must be the wordAllSep(E00361).
&Parts.Split(&Text, ",")
For &i = 1 To &Parts.Count()
// ... &Parts[&i] ...
EndFor
&Parts.Split("one,two,,three", ",", AllSep) // 4 pieces, one of them empty
Release requirement.
Splitgeneratesfor-each ... in %split(...). IBM i 7.5 has it built in. On 7.3 and 7.4 two April 2021 PTFs are needed: the compiler one (5770WDS) and the runtime one (5770SS1), the latter on every system where the program runs.
Release Compiler PTF Runtime PTF 7.3 SI76100 SI76098 7.4 SI76101 (for TGTRLS V7R3M0: SI76109) SI76099 Without the runtime PTF the program compiles but fails at run time with
MCH4437(export not found). It is the same requirement as Upper and Lower.
AllSep(*ALLSEP) additionally requires 7.4 TR8 / 7.5 TR2 or their PTFs: 7.4, compiler SI83497 and runtime SI83471; 7.5, compiler SI83480 (for TGTRLS V7R4M0: SI83496) and runtime SI83468. These numbers come from a search summary: verify them with IBM.
Json(): converting a struct or a list to JSON
&Variable.Json() returns the variable’s content as JSON text (varchar). It can be applied to a struct(...) (produces an object) or to a list(struct(...)) (produces an array), including nested ones by path (&env.Json(), &env.Cab.Detalle.Json()). It is used only as an expression, never as a standalone statement (E00294), and takes no arguments (E00282):
&jsonObject = &aRecord.Json()
&jsonArray = &recordList.Json()
For the template Record [ code number(3,0), name char(20), amount number(7,2), date date ], the results look like this:
{"code":1,"name":"Ana","amount":1250.75,"date":"2026-10-16"}
[{"code":1,"name":"Ana",...},{"code":2,"name":"Luis",...}]
- Keys: the member name as declared in the template, in declaration order.
- List: the array contains only the elements in use (
Count()). char/varchar: quoted JSON string, with"and\escaped and trailing spaces trimmed. Control characters get no special treatment.number: JSON number, unquoted.date:"yyyy-mm-dd"string.- Nested: a
struct(...)member produces an object and alist(struct(...))member an array, e.g.{"Cab":{"epm":"a","Detalle":[{"Pgm":"P1","Libreria":"L1"}]},"Total":2}. - Scalar
structmembers can only bechar/varchar/number/date/field; the generator can also emittime("hh:mm:ss") andtimestamp("yyyy-mm-ddThh:mm:ss.ffffff") should the member types be extended.
Target variable size. For a struct, the expression has the worst-case size: for each text, 2 × length + 2; for each number, precision + 2; for each date, 12; plus key names, quotes, braces and commas. With nested structs the worst case grows with each nested list (computed over its 100 elements). The variable receiving the result must be able to hold it; otherwise the usual incompatible-assignment error (E00147) is reported. Declare the target generously (e.g. varchar(300) for a small template). For a list, no target size is enforced, but if the generated array exceeds the maximum of an RPG varchar (16,773,100 characters) the program fails at runtime instead of truncating.
Errors: E00294 (used as a statement), E00295 (the variable is neither struct nor list), E00296 (another method on a struct; Json is the only valid one), E00297 (list of scalar types: not supported for now). See Diagnostics.
No libraries or PTFs are required on IBM i: when .Json() is used, the compiler generates the needed helper procedures in the RPG (Json_<Template> and JsonList_<Template>). A complete example is in example/JSON01.warp.
To split the resulting text into lines (e.g. to print it) see JsonSplit.
Fixed-width records
A fixed-width record is a line of text where each member of a struct takes a fixed number of positions, in declaration order, with no separators. The total width is the sum of the members’ widths.
| Form | Use | Description |
|---|---|---|
&rec.FixedWidth() | expression | Returns the struct’s record: a char of the total width. Using it as a statement gives E00306. |
&rec.FromFixedWidth(&text) | statement | Fills the struct from a record (the inverse operation). &text is char/varchar or a literal. Using it as an expression gives E00306. |
FixedWidthSplit(&records, &lines) | statement | Converts a list(struct(...)) into one record line per entry, inside a list(char)/list(varchar). Replaces the content of &lines. |
FixedWidthParse(&lines, &records) | statement | Inverse operation: reads each line of &lines as an entry of &records. Replaces the content of &records. |
Format per member:
| Type | Positions | Format |
|---|---|---|
char(n) / varchar(n) | n | Left-aligned, padded with blanks (truncated if too long). |
number(p,s) | p | Right-aligned, leading zeros, implied decimal (no point). A negative carries - in the first position, so leave one digit of slack in p. |
date | 8 | YYYYMMDD. |
time | 6 | HHMMSS. |
timestamp | 20 | YYYYMMDDHHMMSSnnnnnn (with microseconds). |
When reading (FromFixedWidth, FixedWidthParse): a line shorter than the record is read as if padded with blanks; an invalid number gives 0; an invalid date gives the minimum date (0001-01-01).
FixedWidth() and FromFixedWidth() work on ONE struct; for a list use FixedWidthSplit / FixedWidthParse. Calling FixedWidth() on a list gives E00305 with a hint to use FixedWidthSplit(&list, &lines).
FixedWidthSplit and FixedWidthParse also accept a path to a list that is a member of a struct, for example FixedWidthSplit(&pase.Items, &lineas) and FixedWidthParse(&lineas, &pase.Items). A member list holds up to 100 records: FixedWidthParse stops when it fills up and emits the warning FixedWidthParse: lista llena (max 100).
Only structs whose members are char, varchar, number, date (or a field resolving to one of them) are supported. A nested struct or list, or a record longer than 32766 positions, gives E00305. The elements of the line list must be able to hold the whole record (E00309).
Example with struct Cliente [codigo char(5) nombre varchar(12) saldo number(9,2) alta date]: the record is 34 positions long (codigo 1-5, nombre 6-17, saldo 18-26, alta 27-34).
@Variables {
global {
struct Cliente [
codigo char(5)
nombre varchar(12)
saldo number(9,2)
alta date
]
c struct(Cliente)
otro struct(Cliente)
clientes list(struct(Cliente))
copia list(struct(Cliente))
lineas list(char(40))
trama char(40)
}
}
@Source {
&c.codigo = "A001"
&c.nombre = "Ana Perez"
&c.saldo = -1250.5
&c.alta = Today()
&trama = &c.FixedWidth()
&otro.FromFixedWidth(&trama)
&clientes.Add(&c)
&clientes.Add(&otro)
FixedWidthSplit(&clientes, &lineas)
FixedWidthParse(&lineas, &copia)
}
A complete example is in example/FIXEDWIDTH01.warp. Errors: E00305 to E00309 (see Diagnostics).
Note:
numbermembers are read with an implied decimal point and a-sign in the first position;datemembers asYYYYMMDD. A line shorter than the record is read as if padded with blanks.
Struct: templates and member access
A named template with members, defined inline inside @Variables’s global scope:
@Variables {
global {
struct Person [
Id number(10, 0)
Name varchar(50)
Email varchar(100)
Age number(3, 0)
]
globalPerson struct(Person)
Name varchar(50)
}
PrintPerson {
person struct(Person)
}
}
&globalPerson.Name = "Your Name"
&Name = &globalPerson.Name
Do PrintPerson(&globalPerson)
-
Template members support
char/varchar/number/date/field(FieldName),struct(OtherTemplate)andlist(struct(OtherTemplate))(see Nested structs).timeandtimestamparen’t supported as members, nor arematrixorlistof scalar types (E00237: “sólo se permiten char/varchar/number/date/field, struct(Template) y list(struct(Template)) (no se admite matrix ni list de tipos escalares)”). -
A member is read/assigned with
&Variable.Member(both read and write). -
It can be passed as a parameter to, and returned from, an in-file
Procedure/Function— see Procedure / Function. -
An array of
structis declared withlist(struct(TemplateName))— see List: methods. -
Indexing
&List[i]on alist(struct(Template))is valid, but only in two exact forms: as a read, the whole value of an assignment to another&Variablethat isstruct(...)of the same template (&otherVariable = &List[i]); as a write,&List[i] = &otherVariable(another&Variablestruct(...)of the same template, never an expression or a literal). If the side of the assignment that should be the struct variable isn’t syntactically a&Variable,E00241is reported; if the template doesn’t match exactly the list’s,E00242is reported. Index checks (count,numbertype, 1..9999 range) apply the same as for any otherlist:&otherRecord = &List[1] &List[1] = &otherRecord For &i = 1 To &List.Count() ¤t = &List[&i] EndFor -
Not supported yet:
structas a parameter of an external program (@Parameters/Call) — only as a parameter of an internalProcedure/Function.
Nested structs and lists inside a struct
A template member can itself be struct(OtherTemplate) (nested struct) or list(struct(OtherTemplate)) (list of structs). The referenced template may be declared further down:
struct Detalle [
Pgm char(10)
Libreria char(10)
]
struct Cabecera [
epm char(10)
Estado char(10)
Detalle list(struct(Detalle))
]
struct Envio [
Cab struct(Cabecera)
Total number(9, 0)
]
&env struct(Envio)
&fila struct(Detalle)
Access is by path, with as many levels as needed:
&env.Cab.epm = "x"
&x = &env.Cab.epm
&fila.Pgm = "PGM1"
&env.Cab.Detalle.Add(&fila)
&n = &env.Cab.Detalle.Count()
&fila = &env.Cab.Detalle[1]
&env.Cab.Detalle[2] = &fila
&env.Cab.Detalle.Remove(1)
&env.Cab.Detalle.Clear()
&texto = &env.Json()
&textoDet = &env.Cab.Detalle.Json()
- Capacity: a
listthat is a member of astructreserves 100 elements (not the 9999 of a standalonelist).Addon a full list shows a message and does not add the element, just like the standalonelist. A literal index greater than 100 is reported asE00188. - Maximum size: if the variable would take more than the maximum of an RPG variable (16,773,104 bytes),
E00300is reported (e.g. alist(struct)of 9999 elements whose struct carries a large nested list). - Cycles: a template that contains itself, directly or indirectly, is reported as
E00299. - Nonexistent templates:
struct(X)orlist(struct(X))inside a member with an undeclaredXis reported asE00238. - Not allowed as a member:
matrixandlistof scalar types (E00237); neithertimenortimestamp(E00237). - Procedures: a struct with nested members can be passed to a
Procedure; if the routine modifies a nested list, the parameter is passed by reference (notconst). - Generated RPG: templates are emitted in dependency order; the member list is a
dim(100)array with a hidden counter as a subfield of the same template (copied along with the struct), and variables that contain lists are initialized withinz(*likeds). Json(): see Json(); nested members produce nested objects and arrays.
A complete example is in example/NSTJSON01.warp.
@Parameters
Declares the parameters of a Program or a Screen (full screen or Popup): one mode:&Variable entry per line. The type comes from the matching declaration in @Variables/global or @Variables/local, it isn’t repeated here.
| Mode | Description |
|---|---|
in | Input parameter: the caller passes a value, this program can’t return it modified. |
inout | Input/output: the caller passes a value, and this program can modify it back. |
out | Output: this program sets it, the caller only reads it back. |
@Variables {
global {
MyDate date
MyTime char(8)
}
}
@Parameters {
inout:&MyDate,
inout:&MyTime
}
Doesn’t apply to tables.
A Screen declares @Parameters just like a Program, with the same modes (in, inout, out). Parameters are available from Event Init, before the screen is displayed, and out/inout values return to the caller when the Screen closes.
Example: a Popup that receives a title, modifies a quantity and returns the chosen option (based on example/screens/SPOPPRM.warp).
@Properties {
Type : Screen
Popup : True
Name : SPOPPRM
Path : Screens
}
@Variables {
Global {
Title char(30)
Quantity number(5)
Selection char(1)
}
}
@Parameters {
in:&Title,
inout:&Quantity,
out:&Selection
}
Another Screen (or a Program) invokes it with Call, passing the variables in the same order; the analyzer validates count, type and mode:
@References {
Screens = [ "./SPOPPRM.warp" ]
}
@Source {
Event 'OpenPopup' 4
Call(SPOPPRM, &t, &n, &s)
If &s = "S"
&Result = "Accepted"
EndIf
EndEvent
}
List parameters
A @Parameters entry can be a list(char(n)) or a list(varchar(n)) (an array of text lines). It arrives as a dim(9999) array with no wrapper: in RPG, char(n) dim(9999), with const if the mode is in. Since the count is not passed, the program counts the lines up to the first empty line (or up to 9999) to set Count(). With out, the parameter starts as an empty list.
@Variables {
global {
Lines list(char(80))
}
}
@Parameters {
in:&Lines
}
Any other list (for example list(number) or list(struct(...))), or a matrix, gives E00304: only list of char or varchar is supported.
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 count is not sent: the receiver reads up to the first empty line, which is why the list removal operations truly clear the elements (see Data types: List).
Note: in a
Screen, a parameter used in@Layoutmust have more than 10 characters: with 10 or fewer it gives errorE00369, because the DDS field takes that same name and does not compile on IBM i.
@Layout
Describes the layout of a screen or report: Label, Input, Column, DataGridView, Block, or any other Name(...) as a generic control. Control names may carry @ optionally (@Label/Label are equivalent).
Controls with their own grammar
| Control | Syntax | Valid in | Description |
|---|---|---|---|
Label | Label(x, y, text[, Style(...)]...) | Program, Screen | Static label at x,y. text can be &Variable, &variable.member (a struct path), "literal" or a bare field name. x,y = 1,1 (exact corner) is forbidden — DDS rejects any field there. Style(...) (Screen only) gives it color and attributes; see Style. |
Block | Block("Name")[ Label(...), ... ] | Program | A named, printable group of Labels — an instance is printed with Print("Name") from @Source. |
Input | Input("Name", x, y, text, &Variable[, enabled][, Style(...)]...) | Screen | Editable field at x,y. "Name" identifies the control (read with CurrentInput()). &Variable (or a &variable.member path) is where the entered value is stored. enabled optional (default true): a literal, &Variable (or a &variable.member path), a Function() returning number (non-zero = enabled) or a full condition, re-evaluated before every redraw. x,y = 1,1 is also forbidden. |
Column | Column(text, variableOrField[, Input|Hidden][, Style(...)]...) | inside DataGridView only | variableOrField also accepts a &variable.member path. The optional 3rd argument makes the column editable (Input) or invisible (Hidden). Style(...) gives it color and attributes, evaluated per row; see Style in Column. |
DataGridView | DataGridView("Name", x, y, rows[, separator])[ Column(...), ... ] | Screen | A named grid at x,y showing rows visible rows. The name is required and unique among Input/DataGridView controls. See DataGridView for the full guide. |
Example (Screen)
@Layout {
@Label(1,2,&Program)
@Label(1,70,&MyDate)
@Input("txtRequestNo",3,1,"Request No.: ",&RequestNo,true)
@DataGridView("GridRequests",6,1,8," ") [
@Column("Country",&nCountry)
@Column("User",&cUser)
]
}
Struct members in the layout
Label, Input (target variable and enabled) and Column accept struct member paths, including nested ones: &cust.Code, &ord.Cust.Name. They also work with Print in a Program.
@Layout {
@Label(1,2,&cust.Code)
@Input("TxtName",3,2,"Name:",&cust.Name,&cust.Active)
@Input("TxtCust",4,2,"Customer:",&ord.Cust.Name)
@DataGridView("GridRows",6,1,8," ") [
@Column("Name",&row.Name)
]
}
Each path becomes a DDS field: every . is replaced by _, the result is uppercased and truncated to 10 characters (cust.Code -> CUST_CODE, ord.Cust.Name -> ORD_CUST_N). If two variables or paths end up with the same DDS name (for example cust.Code and cust_Code), E00210 is reported. A member that does not exist reports E00178.
Upper and lower case in text Input
An Input whose variable is char or varchar accepts lowercase: the DDS carries CHECK(LC), so the 5250 terminal no longer converts what is typed to uppercase. The program receives exactly what the user typed (for example Lima). Numeric, date and time Input fields are unchanged.
When comparing the typed value against literals, normalize it with Upper:
If Upper(&City) = "LIMA"
...
EndIf
Full conditions
The optional last parameter enabled of Input (and the optional condition of Style) accepts, besides the true/false literal, these forms:
&Variable: non-zero = enabled.Function(): a function with no arguments returningnumber; non-zero = enabled.- A full condition: comparisons (
= <> < <= > >=),And,Or,Not, numbers, strings, variables andFunction()calls with no arguments. It is true when it holds.
@Input("TxtOp",7,2,"Operation:",&Op, Not &Locked = 1 And &Status <> "9")
The condition is re-evaluated before every redraw, using the same indicator mechanism that dynamic enabling already used. Precedence: see Not.
Table fields on their own, indexes and methods are not allowed in the condition: they report E00322.
Style: screen color and attributes
Style(...) goes at the end of the parameters of a Label, an Input or a DataGridView Column, and is only valid in a Screen (in any other object type it gives E00325). It assigns 5250 screen color and attributes (DDS COLOR and DSPATR).
Label(x, y, text, Style(...)[, Style(...)])
Input("Name", x, y, text, &Var[, enabled][, Style(...)]...)
Column(text, &Var[, Input|Hidden][, Style(...)]...)
Syntax: Style(Name, Name, ...[, condition]).
Colors (at most one per Style; otherwise E00321):
| Name | DDS |
|---|---|
Green | COLOR(GRN) |
White | COLOR(WHT) |
Red | COLOR(RED) |
Blue | COLOR(BLU) |
Yellow | COLOR(YLW) |
Pink | COLOR(PNK) |
Turquoise | COLOR(TRQ) |
Attributes (combinable with each other and with a color):
| Name | DDS | Effect |
|---|---|---|
Reverse | DSPATR(RI) | Reverse video. |
High | DSPATR(HI) | High intensity. |
Underline | DSPATR(UL) | Underline. |
Blink | DSPATR(BL) | Blink. |
Hidden | DSPATR(ND) | Typed text is not displayed (hidden field). |
An unknown name gives E00320. A Style with no color or attribute gives E00324.
Equivalences for those migrating: 'RI' = Reverse, '+' = High, MGN = Pink.
Conditional Style
The last argument of a Style can be a condition, with the same forms as enabled (variable, Function() or full condition). The Style applies while the condition is true.
Rules per element:
- At most one
Stylewithout a condition (the “normal” one): it applies only if no conditionalStyleis active. - Up to three conditional
Styleentries. Exceeding the limits givesE00323. - If two conditional
Styleentries hold at once, the first one declared wins. - Each conditional
Styleconsumes one indicator from the same range as dynamic enabling (30-89). - A monochrome screen ignores
COLOR.
@Layout {
Label(1,2,&Title,Style(Reverse,High))
Label(3,2,"F3=Exit",Style(Red))
Label(5,2,&Desc,Style(Blue,&Status = "1" Or &Status = "2"),Style(Pink,&Status = "3"),Style(Green))
Input("TxtOp",7,2,"Operation:",&Op,Not &Locked = 1 And &Status <> "9",Style(Yellow,Underline))
Input("TxtAmt",8,2,"Amount:",&Amount,&Locked = 0,Style(Turquoise,&Locked = 1))
}
For &Desc, the text is blue if &Status is "1" or "2", pink if it is "3" and green otherwise. Full example: example/screens/SSTYLE01.warp.
Style in DataGridView Column
Column accepts Style(...) after the optional mode (Input/Hidden). The rules are the same (colors, attributes, at most one Style without a condition and three with a condition per column, the first one declared wins, errors E00320-E00324):
DataGridView("GridOrd",2,2,10)
[
Column("Order",&VOrdNo)
Column("Quantity",&VQty,Style(Red,High,&VQty > 100),Style(Green))
]
Key difference: the condition of a column Style is evaluated per row, at the time of LoadRow() (right before the row is stored in the subfile), with the values the variables hold at that instant. Therefore LoadRow() must come after the row variables are assigned. The indicators are stored together with each row, so the color of a row does not change if the variables change later. These indicators share the 30-89 range with the rest of Style and with enabled. Full example: example/screens/SJNGRD01.warp.
Limitations
- Per-row dynamic enabling in the
DataGridView(a conditionalColumn(..., Input)) does not exist yet (planned).
Example (Program, with a printable Block)
@Layout {
@Block("Header") [
Label(1,1,"Sales Report")
Label(2,1,&Today)
]
}
@Source {
Print("Header")
}
@Layout is optional in Program (a purely batch program, with no printed output, doesn’t need it) but required in Screen. Doesn’t apply to tables.
@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, achainon the primary-key logical file) once for each record read from the table being iterated.Joindeclares noIndex. - Without
On(implicit): eachPrimaryKeyfield of the joined table must exist with the same name in the iterated table or in another table joined earlier (otherwiseE00316). - With
On: the fields on the joined-table side must be exactly itsPrimaryKeyfields, none repeated or missing (order does not matter; otherwiseE00317). 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
Whereclauses 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. AWhereon a field that only exists in the joined table is evaluated inside the loop, after thechain(it does not position the file). - Read-only: assigning a joined-table field gives
E00319. - Several
Join Inlines can be chained. The joined table must have aPrimaryKeyin@Indexes(otherwiseE00315). JoinandOnare not reserved words: they are only recognized in theFor Eachheader (Joinfollowed byIn), so they remain usable as field names.- DataGridView: the
For Eachthat loads the grid inEvent Loadalso supportsJoin In. Fields of the joined table are assigned to theColumnvariables beforeLoadRow(), just like those of the traversed table. AWhereon a field that exists only in the joined table filters rows after thechain.
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
- A read (
chain) is done by the table’sPrimaryKey, with the values theNewhas just built. - If the row does not exist, the new row is written (
write) and theWhen Duplicateblock does not run. - 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:
- The fields of the
Newtable (the re-read row; readable and assignable). - The fields of the
For Eachloops enclosing theNew(read-only).
Rules
- The table must have a
PrimaryKeyin@Indexes; otherwise you getE00339. It no longer depends on someFor Eachusing anIndexover 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. Exitdirectly inside the block givesE00326; inside aWhile/Forof the block itself it is valid.Delete()is not valid inside the block (the usual error forDelete()outside aFor Eachapplies).
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 withAnd/Or/Not). - Any statement can be used inside a branch, including
ExitandLoadRow()(the latter only whereLoadRow()is valid: see DataGridView). - It is translated to RPG
select/when/other/endsl. It replaces chained nestedIf/Else, since WARP has noElse If. Case,OtherwiseandEndCase(likeExit) are reserved words: they cannot be used as a field, declared variable or procedure name; they must be renamed. ACase,OtherwiseorEndCaseoutside aDo CasegivesE00327.- A syntax error (no
Casebranches or noEndCase) givesE00008.
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
Exitinside aWhilenested in aFor Eachleaves theWhile, not theFor Each. - Saving in
For Each In: if the body already assigned table fields before theExit, the compiler saves (update) the record before leaving. If aDelete()came earlier, it is not saved. When NoneandWhen Duplicate: the body ofWhen None(ofFor Each) and ofWhen Duplicate(ofNew) do not count as loops; anExitthere refers to the outer loop, if any.For &i = ...:&ikeeps its value on exit.- Inside a
Do Casethat is inside a loop it is valid. - Procedure, Function and Event: an
Exitdoes not leave the caller’s loop; there must be a loop in the same body. Outside a loop it givesE00326. Exitis 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/@GeneratorConfigkey (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@Propertiesor in the project’s@GeneratorConfig. Without it generation fails with “Commit requiere Commitment activo (en @Properties del objeto o en @GeneratorConfig)” (same forRollback). - Where: valid in Program and Screen, anywhere in
@Source, also inside aProcedure, anEventor a loop. - In a Screen: with
Commitmentenabled, the Screen startsSTRCMTCTL LCKLVL(*CHG)on startup and opens the tables it modifies under commitment control;CommitandRollbackwork the same as in a Program (also inside anEventor aProcedure). - 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,Commitfirst saves (update) the pending record and then commits.Rollback, on the other hand, should not go inside theFor Eachafter assigning fields, because the record is saved at the end of the iteration; place it outside the loop, afterEndFor. Commit on Exit:Commit on Exit : Truestill commits only when the program ends; in a Screen, when the screen is closed (F3). WithCommit on Exit : False, anything uncommitted is undone when the job ends.- They are not reserved words: a field named
Commitstill 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):
| Form | Catches | RPG |
|---|---|---|
Catch | Any error. | on-error; |
Catch File | File errors (*FILE: statuses 01000-02999 on IBM i 7.5). | on-error *file; |
Catch Program | Program errors (*PROGRAM: 00100-00999 and 9999). | on-error *program; |
Catch 102, 202 | The listed status codes, comma separated (each between 100 and 9999; otherwise E00365). | on-error 00102 : 00202; |
- Order: the
Catchblocks are evaluated in order and the first one that covers the error wins. ACatchwithout a filter must come last: if another follows it, it is errorE00368(“never runs”). - Structure: a
Tryneeds at least oneCatch(E00367) and itsEndTry(E00364). ErrorCode()andErrorMessage(): only valid inside aCatch; outside they giveE00366.ErrorCode()returns anumber(5)with the error status (%status) andErrorMessage()achar(80)with the message text (PSDS positions 91-170). See Builtin functions.- An error inside a
Catchis not caught by the sameTry: nest anotherTry. ExitandReturnare allowed insideTryandCatch.- With
Commitment: combiningCatchwithRollbackis useful to undo the changes when something fails.
Common status codes:
| Status | Situation |
|---|---|
00100 | Value out of range in a string operation. |
00101 | Square root of a negative number. |
00102 | Division by zero (also Mod with a zero divisor). |
00103 | The result does not fit in the field. |
00105 | Invalid character when converting to a number (e.g. Val). |
00112 | Invalid date or time. |
00121 | Invalid array index. |
00202 | A program or procedure call failed. An error inside a called procedure reaches the caller as 00202. |
00211 | Call error: the called program does not exist or cannot be resolved (Call to a missing program). |
00907 | Invalid decimal data. |
01211 | File 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(¶m)
...
EndProc
Function Name(¶m) returnType
...
Return expr
EndFunc
Procedure(no return value,subis a synonym): called withDo 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
| Statement | Use |
|---|---|
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("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 toTablefor the whole program (ADDPFM+OVRDBF, before anyopen). InProgram, only at the top level of@Source; inScreen, only at the top level ofEvent Init(see @Source: Event and Screen). At most once per table.Export(Table, destination): copiesTable’s active member to an IFS stream file (CPYTOIMPF, comma-delimited). Only valid inProgram(not inScreen); 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&Variableor struct member, or a text literal. Any other expression givesE00301. - 2nd argument: a
list(varchar(n))orlist(char(n))&Variable. OtherwiseE00302; it cannot be aninparameter 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 messageJsonSplit: 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.
@Source: Event and Screen
Statements exclusive to Screen files.
Event
Event StandardName
...
EndEvent
Event 'Label' KeyNumber ["Text"]
...
EndEvent
Two forms: standard event (bare, unquoted name) or a custom event attached to a function key (quoted name + key number, 1-24, i.e. F1-F24; F13-F24 via Shift).
Standard events
| Event | When it fires |
|---|---|
Enter | On pressing Enter (with no function key active). CurrentInput() identifies which Input had focus, decoded from the cursor position after EXFMT. |
Close | Fixed to F3 in the generated DSPF (CF03) — always runs (even empty) and ends the screen; doesn’t require a declaration to attach the key. |
Load | Once, before the first display — typically contains the For Each/LoadRow() that fills the DataGridView (the grid is reset on entry). Page Up/Down are native scrolling over what’s already loaded; this event only runs again with Refresh/Refresh Keep or F5 (see below). |
Refresh | On pressing F5, if F5 is declared in the .warpcfg’s @FunctionKeys and isn’t taken by a custom Event 'Name' 5. Runs Event Refresh (if it exists) and then reloads the DataGridView (if the Screen has one with an Event Load). See Refresh and Refresh Keep. |
Init | Once, the first time this screen starts, before the first display. Does nothing if not declared. The only place in a Screen where Member(...) is valid — see Reports with Member()/Export(). |
Custom events
Event 'FindCustomer' 4
Message("F4 was pressed", Info)
EndEvent
By default, the label shown on the screen’s function-key line is the event name (F4=FindCustomer).
Key text
After the key number you can add an optional quoted text, which is what is shown on that line (F4=Find):
Event 'FindCustomer' 4 "Find"
Message("F4 was pressed", Info)
EndEvent
- Only allowed on key-bound events (1-24); it cannot be empty or longer than 20 characters (
E00344). - If omitted, the event name is used.
- The event name becomes the name of an RPG subprocedure, so it must be an identifier (letters, digits and
_, starting with a letter); otherwise it is errorE00357. Text with spaces goes in the third value:Event 'Name' 6 "Text with spaces". - The
Eventtext takes precedence over any text declared for that same key in the project’s.warpcfg@FunctionKeys(see Project), just as the name did before.
Key color
At the end you can add a color, with or without text: Green, White, Red, Blue, Yellow, Pink or Turquoise (the same as Style):
Event 'Delete' 7 "Delete" Red
Message("Record deleted", Info)
EndEvent
Event 'Find' 4 Blue
Message("F4 was pressed", Info)
EndEvent
- Text and color are both optional; without text the event name is used (
F4=Findin the second example). - The key’s color takes precedence over
Function Keys Color(from@Propertiesor@Screen). It also applies to each set of the F24 rotation. - An invalid color gives
E00347. - Keys declared only in
@FunctionKeyshave no color of their own; they useFunction Keys Color.
Message
Message(text, Error|Warning|Info)
Shows a message on the screen’s message line: Error (red), Warning (yellow), Info (normal text color). Shown on the next screen refresh and cleared after one cycle.
LoadRow / RefreshSelectRow / For Each Row
Exclusive to screens with a DataGridView — see the full guide: DataGridView.
| Statement | Use |
|---|---|
LoadRow() | No arguments. Takes the current values of the variables bound to each Column and appends a row to the grid. Valid in the body of any Event of a Screen (directly, inside If, Case, While, For or For Each) and in any Procedure. In Event Load the grid is reset on entry; in any other Event or Procedure the rows are appended to the existing ones. Not allowed in a Function or outside an Event/Procedure (E00198). A For Each that loads the grid must declare Index. |
RefreshSelectRow() | No arguments. The reverse of LoadRow(): re-reads the row under the cursor and writes each column (including Hidden ones) back into its bound variable. Valid in any Event of a Screen with a DataGridView. |
For Each Row ... EndFor | Iterates over every row already loaded into the subfile (not the table), writing each Column into its bound variable before every iteration. No Index/Where/When None. |
CurrentInput
&Control = CurrentInput()
Returns the Name of the Input/DataGridView (from @Layout) that currently has focus. For a DataGridView, it matches if the cursor lands anywhere in its rectangle (header, attention row, or any data row), not just an exact cell. Valid in any Event, Procedure or Function of a Screen (a Procedure called from an event sees the position as well), because the driver computes the cursor position after every screen read. It is not valid in loose top-level @Source statements (they never run) nor outside a Screen: that is error E00192. If the Procedure is invoked from Event Init (before the first read), the cursor has no position yet and CurrentInput() returns empty.
Cursor
If Cursor(&Codigo)
&Aviso = "Code help"
EndIf
It is true when the cursor is on the control bound to &Codigo, either an Input or a visible column of the @Layout’s DataGridView. Use it as a direct condition; do not combine it with = 1, since the result is already a condition.
- For an
Inputit compares the field’s row and column range. - For a grid column it compares the data rows and that column’s column range. A
Hiddencolumn never matches. - Same scope as
CurrentInput():Event,ProcedureorFunctionof aScreen(E00192elsewhere). If invoked fromEvent Initthere is no position yet and it is false. - Errors:
E00336(argument count),E00337(argument is not a&variable),E00338(the variable is not that of anyInputor grid column).
Example inside a Procedure (see example/screens/SFOCUS01.warp):
Procedure PideAyuda()
If Cursor(&Cod)
&Aviso = "Code help"
Else
If Cursor(&Descrip)
&Aviso = "Description help"
EndIf
EndIf
EndProc
Refresh and Refresh Keep: reload the DataGridView
Refresh
Refresh Keep
Argument-less statements that reload the screen’s DataGridView. The reload is not immediate: code after the Refresh in the same event keeps running, and when the driver starts its next loop the subfile is cleared and Event Load runs again.
Refreshgoes back to the first row.Refresh Keepkeeps the row where the user was (clamped to the new total if the grid ended up with fewer rows).
Rules:
- Only valid in a
Screen; in any other source type they giveE00335. If the Screen has noDataGridView, they do nothing. - They can be called from any
EventorProcedure. - They are not reserved words: an
Event Refreshand a field namedRefreshkeep working. - Limitation: the reload repeats the first-load sequence. A
RefreshinsideEvent Loadis evaluated again on every loop.
Example (see example/screens/SREFRSH01.warp):
Event Enter
Refresh Keep
EndEvent
F5 behavior change
If F5 is declared in @FunctionKeys and no custom event claims it, F5 runs Event Refresh (if it exists) and then reloads the DataGridView (if the Screen has one with an Event Load). Previously it only called Event Refresh. So Event Refresh is the place to prepare data (for example, clear a filter) before the reload.
SetFocus: place the cursor
&Variable.SetFocus()
Puts the cursor on the Input whose variable is &Variable the next time the screen is shown. It is a statement: it returns no value.
- Only valid in a
Screen(E00331). - The variable must be the variable of some
Inputin@Layout(E00332). - It takes no arguments (
E00333). - It is not an expression and cannot be used inside one (
E00334). - If there are several
SetFocusin the same loop, the first one wins. - It can be used in
Event Initto set the initial focus.
Event Init
&Cod.SetFocus()
EndEvent
Event Enter
If &Cod = ""
&Aviso = "Code is missing"
&Cod.SetFocus()
EndIf
EndEvent
Full example: example/screens/SFOCUS01.warp.
Implementation (for the advanced reader): the compiler writes the field’s row and column into hidden CSRLOC fields and resets them to zero after each read. On screens with a DataGridView it uses the grid’s CSRLOC fields. In popups the coordinates are relative to the window.
Builtin functions
Usable as an expression anywhere, e.g. &Username = USERID(). Most take zero arguments with a fixed return type; Val() is the exception (see its own entry).
| Function | Return | Description |
|---|---|---|
USERID() | char(10) | IBM i user profile running this program. No arguments. |
PGNAME() | char(10) | Object name of this same program — a compile-time constant, not a runtime lookup. No arguments. |
TODAY() | date | Current system date. No arguments. |
TIME() | char(8) | Current system time formatted "HH:MM:SS" (8 characters). No arguments. |
Val(&CharVariable) | same as the assignment’s destination | Parses a char value into a Number, using the destination of the assignment’s precision/scale — unlike other builtins, it has no fixed return type. Only valid directly as &NumberVariable = Val(&CharExpr); using it anywhere else (nested in another expression, as a Do/Call argument, etc.) is an error. |
CurrentInput() | char(30) | See @Source: Event and Screen. |
Cursor(&variable) | condition | True when the cursor is on the control bound to &variable (an Input or a visible DataGridView column). Use it as a direct condition: If Cursor(&Codigo), without = 1. Same scope as CurrentInput(). Errors: E00336, E00337, E00338. See @Source: Event and Screen. |
String(&NumberVariable, integerDigits[, decimalDigits][, fill]) | char(integerDigits [+ decimalDigits]) | Converts a Number to a fixed-width char, e.g. to concatenate it in a Message(...) (which only accepts text). integerDigits/decimalDigits must be integer literals (compile-time constants): the result width is integerDigits + decimalDigits, with no decimal separator. decimalDigits is optional (default 0). fill is optional: a literal text of exactly one character (E00341 otherwise). Without fill, leading zeros of the integer part are replaced by spaces; with "0" the zeros are kept; with any other character (e.g. "*") that character is used. The units digit of the integer part is always shown: String(0, 5) gives " 0". Negative numbers show the - sign, which takes one of the requested width positions (the result still measures integerDigits + decimalDigits): with "0" fill it replaces the first character (String(-2, 5, "0") = -0002); without fill it sits right next to the first digit (String(-15, 6) = -15); with another fill character, likewise (String(-15, 6, "*") = ***-15). If the number uses all positions, the sign replaces the most significant digit, so for negatives request one extra position in integerDigits. Positives and zero are unchanged. Forms: String(n, 5), String(n, 5, 2), String(n, 5, "0"), String(n, 5, 2, "0"), String(n, 5, "*"). Errors: E00206, E00207, E00208, E00341. |
Int(expression) | Number | Returns the integer part of a number: truncates toward zero, does not round (Int(10 / 3) is 3). Takes exactly 1 argument of type Number (E00342 if the count is not 1; E00343 if it is not numeric). If the argument is a variable or field of known type, the result is Number(argument's integer digits, 0); with a compound expression (e.g. &a / &b) the type is not restricted and it can be stored in any Number (RPG truncates on assignment). It translates to %dec(expr : 30 : 0). |
SubString(&Variable, start, length) | char(length) | Trims a fixed-width chunk out of a char value (variable or literal) so it fits into a smaller variable — e.g. &Short40 = SubString(&Long120, 1, 40). Without this, directly assigning a wider char to a narrower one is a compile-time error. start/length must be integer literals. When the source size is known at compile time, start + length - 1 exceeding it is also a compile-time error. |
Trim(&Variable) | same as the argument (char/varchar) | Removes blanks from both left and right. A single argument. |
LTrim(&Variable) | same as the argument | Removes blanks from the left. A single argument. |
RTrim(&Variable) | same as the argument | Removes blanks from the right. A single argument. |
Upper(&Variable) | same as the argument | Converts a char/varchar to uppercase, including accented letters. A single argument (otherwise error E00225); on a type other than char/varchar it gives E00226. Translated to %upper. See Upper and Lower release requirement. |
Lower(&Variable) | same as the argument | Converts a char/varchar to lowercase, including accented letters. Same rules as Upper. Translated to %lower. |
Length(&Variable) | number | Size of a char/varchar value: for varchar, the actual runtime content length; for char, its declared fixed length. A single argument. |
IndexOf(needle, &haystack) | number | Searches for needle inside haystack and returns the raw position found (1-based if found, 0 if not), with no conversion applied. Exactly two arguments (text to search for, text to search in). |
Round(expression, decimals) | number(30, decimals) | Rounds expression half-adjust (half rounds up, RPG %dech) to decimals. decimals must be an integer literal between 0 and 29 (a compile-time constant: it defines the scale of the result). The whole expression is evaluated before rounding, so a division is not truncated first. The first argument must be Number. Errors: E00328 (argument count), E00329 (decimals not a literal or out of range), E00330 (first argument not numeric). |
Replace(text, search, new) | varchar | Replaces all occurrences of search with new, without overlapping (%scanrpl). The result is a varchar with the length of the source text: if the replacement is longer, assigning it to a smaller variable truncates it. Blanks in search count (as in IndexOf): use Trim() if needed. |
Contains(text, search) | condition | True if text contains search. Used as a condition (If, While, And, Or), not assigned to a variable. Same blank handling as IndexOf. |
StartsWith(text, search) | condition | True if text starts with search. Same rules as Contains. |
EndsWith(text, search) | condition | True if text ends with search. Same rules as Contains; translated to %scanr, which requires IBM i 7.4 or later. For a char with trailing blanks use EndsWith(Trim(&Text), "end"). |
PadLeft(text, width[, fill]) | char(width) | Right-aligns, filling on the left. width is an integer literal between 1 and 32766; fill is a literal of one character (a space by default). Ignores trailing blanks of the text; if the text is longer than width, it is cut on the left. |
PadRight(text, width[, fill]) | char(width) | Left-aligns, filling on the right. Same rules as PadLeft; if the text is longer than width, it is cut on the right. |
Translate(text, from, to) | same as text | Changes each character of from to the one at the same position in to (%xlate). |
Abs(n) | number | Absolute value (%abs). |
Sqrt(n) | number | Square root (%sqrt). |
Mod(a, b) | number | Remainder of the integer division (%rem). Requires operands without decimals (E00362; use Int() to truncate). The result has the sign of the dividend: Mod(17, 5) is 2 and Mod(-17, 5) is -2. |
ErrorCode() | number(5) | Status of the caught error (%status), e.g. 102 for a division by zero. No arguments. Only inside a Catch (E00366 outside). See Try / Catch. |
ErrorMessage() | char(80) | Message text of the caught error (PSDS subfield, positions 91-170). No arguments. Only inside a Catch (E00366 outside). Apply Trim() to remove trailing blanks. |
Check(text, characters) | number | Position (1-based) of the first character of text that is not in characters; 0 if all are. Trailing blanks of a char do not count (they are padding). Maps to %check. |
Concat(text1, text2) | varchar | Joins two texts (RPG +). Each operand keeps its own blanks. |
Div(number, divisor) | number | Integer quotient, truncated toward zero (Div(-7, 2) is -3). Maps to %div. |
Min(number1, number2) | number | The smaller of two numbers. Maps to %min. |
Max(number1, number2) | number | The larger of two numbers. Maps to %max. |
DateTimeToTimestamp(date, time) | timestamp | Joins a date and a time into a timestamp (the time has no microseconds, so they are 000000). Maps to a text conversion with %timestamp. |
Upper and Lower release requirement
Upper and Lower are translated to the RPG BIFs %upper and %lower, which depend on the IBM i release:
- IBM i 7.5: available natively.
- IBM i 7.3 and 7.4: only with the April 2021 PTFs. Compiler: SI76100 (7.3) or SI76101 (7.4). In addition, the runtime PTF SI76098 (7.3) or SI76099 (7.4) must be installed on every system where the program runs; without it, the program fails with MCH4437.
- IBM i 7.2 and earlier: not available.
Date, time and timestamp
Year/Month/Days/Hour/Minute/Second are polymorphic: they accept date/time/timestamp as appropriate (Year/Month/Days with date or timestamp; Hour/Minute/Second with time or timestamp) — there’s no need for a different name per type.
format (where it applies) is always optional: if omitted, it falls back to the value configured in @GeneratorConfig (Date Format/Time Format/Timestamp Format); if there’s no configuration either, the system default format is used.
| Function | Return | Description |
|---|---|---|
Year(&Variable) | number(4) | Year of a date/timestamp. |
Month(&Variable) | number(2) | Month of a date/timestamp. |
Days(&Variable) | number(2) | Day of the month of a date/timestamp. |
Hour(&Variable) | number(2) | Hour of a time/timestamp. |
Minute(&Variable) | number(2) | Minute of a time/timestamp. |
Second(&Variable) | number(2) | Second of a time/timestamp. |
DateDiff(&Date1, &Date2, Unit) | number | Difference between two dates, in Days/Months/Years (bare word, not a string). Positive when Date1 is later than Date2, negative otherwise. |
TimeDiff(&Time1, &Time2, Unit) | number | Same as DateDiff, over time, with Hours/Minutes/Seconds. |
TimestampDiff(&Ts1, &Ts2, Unit) | number | Same as DateDiff, over timestamp, with all 6 units enabled: Seconds/Minutes/Hours/Days/Months/Years. |
DateAdd(&Date, amount, Unit) | date | Adds (or subtracts, if amount is negative) days/months/years to a date. amount can be any number expression (not just a literal). |
TimeAdd(&Time, amount, Unit) | time | Same as DateAdd, over time, with Hours/Minutes/Seconds. |
TimestampAdd(&Ts, amount, Unit) | timestamp | Same as DateAdd, over timestamp, with all 6 units. |
IsDate(value[, format]) | number(1) | Validates whether value (char/varchar/number) is a valid date in format (optional — one of Iso/Usa/Eur/Jis/Mdy/Dmy/Ymd/Jul, or a text pattern; see Date format patterns). Returns 1/0 (WARP has no boolean type). Not a pure expression — that’s why, just like Val(), it’s only valid directly as &NumberVariable = IsDate(...). |
IsTime(value[, format]) | number(1) | Same as IsDate, over time (format one of Hms/Iso/Usa/Eur/Jis). |
IsTimestamp(value[, format]) | number(1) | Same as IsDate, over timestamp (format one of Iso/Usa/Eur/Jis). Iso is the recommended, most widely compatible format. |
StringToDate(string[, format]) | date | Converts char/varchar to date. format also accepts a text pattern. |
NumberToDate(number[, format]) | date | Converts number to date — same format rules as StringToDate (including text patterns). |
StringToTime(string[, format]) | time | Converts char/varchar to time. |
StringToTimestamp(string[, format]) | timestamp | Converts char/varchar to timestamp. |
DateToString(&Date[, format]) | char(10) | Converts date to text — same format rules. With a text pattern it returns a char as long as the pattern (maximum 10). |
TimeToString(&Time[, format]) | char(8) | Converts time to text. |
TimestampToString(&Ts[, format]) | char(26) | Converts timestamp to text (26 = ISO width with microseconds). |
Now() | timestamp | Current system timestamp. No arguments. |
Date format patterns
In StringToDate, NumberToDate, IsDate and DateToString, the second format argument accepts, besides the single keywords (Iso, Usa, Eur, Jis, Mdy, Dmy, Ymd, Jul), a literal text pattern with YYYY (4-digit year), MM and DD. Each must appear exactly once, in any order, with or without separators, and is case-insensitive. Examples: "YYYYMMDD", "DDMMYYYY", "DD/MM/YYYY", "YYYY-MM-DD", "MM.DD.YYYY".
- Resolved at compile time: the RPG extracts year, month and day with
%substat fixed positions and reads them as*iso. char/varchar: the value must be exactly as long as the pattern. A shortervarcharfails at runtime.NumberToDate/IsDatewithnumber: the number is converted to text with leading zeros (%editc(n : 'X')) and the pattern separators do not count (a number has none). Thenumbermust have as many digits as the pattern (e.g.number(8)for"DDMMYYYY").DateToString: returns acharas long as the pattern (maximum 10); e.g."YYYYMMDD"giveschar(8).- 2-digit years (
YY), repeated or missing fields, and letters other thanY/M/Dare not allowed:E00253is reported.
&Date = StringToDate(&DateText, "DD/MM/YYYY")
&Date2 = NumberToDate(&Number, "DDMMYYYY") // &Number: number(8)
&IsValid = IsDate(&Number, "YYYYMMDD")
&Text = DateToString(&Date, "YYYYMMDD") // char(8)
Text and calculation: Replace, Contains, Pad, Translate, Abs, Sqrt, Mod
Contains,StartsWithandEndsWithare conditions: write them directly inIf,While,AndorOr, likeCursor.Replace,Contains,StartsWith,EndsWithandTranslatetreat blanks in the arguments as part of the text. For acharwith trailing blanks, applyTrim()before comparing.PadLeftandPadRightalways return acharof the givenwidth;widthandfillmust be literals (E00361otherwise).- Wrong argument count (
E00358), non-text argument (E00359) and non-numeric argument (E00360) are reported with the function name. Sqrtof a negative number andModwith a zero divisor are RPG runtime errors (status00101and00102) that abort the program untilTry/Catchexists.
&Clean = Replace(&Text, "-", " ")
If Contains(&Text, "world")
Message("Contains the word", Info)
EndIf
If StartsWith(&Code, "AB") And EndsWith(Trim(&Code), "99")
Message("Valid code", Info)
EndIf
&Code5 = PadLeft(&Number, 5, "0") // "00042"
&Label = PadRight(&Name, 20) // left-aligned, 20 characters
&Upper = Translate(&Text, "abc", "ABC")
&Rest = Mod(17, 5) // 2
&Absolute = Abs(&Difference)
&Root = Sqrt(&Area)
Examples
&Username = USERID()
&Program = PGNAME()
&Today = TODAY()
&CurrentTime = TIME()
&Quantity = Val(&QuantityText)
Message("Total: " + String(&Total, 9, 2), Info)
&PerPart = Int(&Total / &Parts) // integer part: 100 / 3 -> 33
&Text1 = String(&Row, 5) // " 7" (spaces)
&Text2 = String(&Row, 5, "0") // "00007" (zeros)
&Text3 = String(&Row, 5, "*") // "****7"
&Text4 = String(&Amount, 6, 2, "0") // 6 integer + 2 decimal digits, zero-filled
&Short = SubString(&Long, 1, 40)
&Clean = Trim(&WithSpaces)
&Code = Upper(Trim(&Input))
&Text = Lower(&Text)
&Size = Length(&Description)
&Pos = IndexOf("@", &Email)
&Amount = Round(&Total / &ExchangeRate, 2)
&New = Replace(&Text, "a", "b")
&Code5 = PadLeft(&Number, 5, "0")
&Rest = Mod(&Total, 7)
&DueDate = DateAdd(&Today, 30, Days)
&DaysUntilDue = DateDiff(&DueDate, &Today, Days)
&IsValidDate = IsDate(&DateText, Dmy)
&Now = Now()
&HoursElapsed = TimestampDiff(&Now, &Start, Hours)
Int and String with fill
Breaking change in
String. Previously,String(n, integerDigits)padded with leading zeros. Now, by default, those zeros are replaced by spaces. To keep the old behavior, pass the fill:String(n, 5, "0").
Equivalents in generated RPG:
| Form | RPG |
|---|---|
String(n, 5) (spaces) | %editw(%dec(n : total : dec) : ' 0') |
String(n, 5, "0") (zeros) | %editc(%dec(n : total : dec) : 'X') |
String(n, 5, "*") (other character) | %xlate(' ' : '*' : %editw(...)) |
Sign of negative numbers
String shows the - sign of negative numbers. The sign takes one of the requested width positions, so the result still measures integerDigits + decimalDigits:
| Call | Result |
|---|---|
String(-2, 5, "0") | -0002 |
String(-15, 6) | -15 |
String(-15, 6, "*") | ***-15 |
If the number uses all positions, the sign replaces the most significant digit; for negatives, request one extra position in integerDigits. Positives and zero are unchanged. Decimals still have no decimal point (12.00 with 2 decimals gives 1200). For this, the generated RPG includes a helper procedure WarpStrSign (once per program).
Int translates to %dec(expr : 30 : 0). Full example: example/programs/PNUM01.warp.
Expressions and operators
Arithmetic/concatenation operators
| Operator | Use |
|---|---|
+ | Numeric addition between Numbers, or concatenation between char/varchar (depending on the operand types). |
- | Numeric subtraction. |
* | Numeric multiplication. |
/ | Numeric division. |
&Total = &Price * &Quantity
&Greeting = "Hello, " + &Username
Comparison operators
=, <>, <, <=, >, >= — used in Where, If, While.
Logical operators
And/Or combine comparisons (typically in If/While): And binds tighter than Or. Not expr negates a condition.
If &a = 1 And &b = 2 Or &c = 3
...
EndIf
Not (logical negation)
Not expr inverts the condition that follows it. It is a reserved word, like And and Or.
Precedence, from lowest to highest: Or, And, Not, comparisons. That is, Not binds looser than a comparison and tighter than And:
// Not &a = 1 And &b = 2 reads as (Not (&a = 1)) And (&b = 2)
If Not &a = 1 And &b = 2
...
EndIf
To negate a compound condition, use parentheses: Not (&a = 1 Or &b = 2).
Not can also be used in @Layout conditions (see @Layout: conditions).
WARP has no boolean type of its own: each operand is validated like any other expression.
Implicit line continuation
In @Source, an expression or statement can span several lines without any special character. The line break is ignored in these cases:
- After an operator:
+ - * /, the comparisons= <> < <= > >=,And,Or,Not, and after the=of an assignment. - Inside parentheses: after
(, after each,and before). This applies to function or builtin calls (DateAdd(…)),Do Proc(...),Call(...),Message(...),Member(...),Export(...),Procedure/Functionparameter lists and parenthesized expressions. And/Orat the start of the next line also continue the condition.
&text = "one" +
"two"
&d = DateAdd(
&d,
1,
Days
)
If &n > 1
And &n < 10
...
EndIf
Additional rules:
- An arithmetic or comparison operator at the start of the next line (for example
+ "two") is still an error (E00008), because it would be ambiguous. - A line cannot be continued in the middle of an identifier or a text literal.
//comments between continued lines are ignored.- Diagnostic line numbers remain the real ones.
Literals
- Numbers:
100,9.5. - Text:
"text"or'text'(equivalent). - Date/time: obtained via
TODAY()/TIME(), there’s no date literal.
Variables and fields
- Variable: always with
&(&Username). - Table field: without
&, only inside aFor Each/Newover that table (CustCode,Name). list/matrixelement:&Variable[index]/&Variable[row, col](1-based).listmethod:&Variable.Method(...)(Add/Clear/Init/Removeas a statement,Count()as an expression) — see Data types: List.Json()method ofstruct,list(struct(...))and scalarlist(list(char),list(number),list(date),list(time),list(timestamp); the result is a JSON array, e.g.["a","b"]or[1,2])- Statement
JsonSplit(&json, &list[, indent]): splits a JSON into lines — see JsonSplit. - Fixed-width records: expression
&rec.FixedWidth()and statements&rec.FromFixedWidth(&text),FixedWidthSplit(&records, &lines),FixedWidthParse(&lines, &records)— see Fixed-width records. structmember:&Variable.Member(read and assignment); with nested structs, by path:&Variable.Cab.Member,&Variable.Cab.List.Add(&row),&Variable.Cab.List[1]— see Data types: Struct.
Function/procedure calls as an expression
&variable = FunctionName(expr, ...)
See Builtin functions and @Source: control statements for your own Functions.
Project file (.warproj / .warpcfg)
See also the guide: Project.
.warproj — @Project
| Key | Description |
|---|---|
Name | Project name. |
Description | Project description. |
Version | Project version (free-form string, e.g. 1.0). |
Default Profile | Name of the @Profile (defined in the .warpcfg, see below) used by generate/build/reverse/license-status when the CLI doesn’t receive --profile. Optional if the .warpcfg declares only one @Profile. |
@Generator (nested inside @Project)
| Key | Description |
|---|---|
Name | Generator name. |
Description | Generator description. |
Language | Target language. Values: RPGLE, RPG. |
@Paths (nested inside @Generator)
| Key | Description |
|---|---|
Config | Path to this project’s .warpcfg. |
Output | Folder where the generated DDS/RPG is written. |
.warpcfg — @GeneratorConfig
| Key | Description | Values |
|---|---|---|
Date Format | Native date format used to declare every Date variable (e.g. date(*dmy/) instead of plain date). | ISO, USA, EUR, JIS, MDY, DMY, YMD, JUL |
Date Separator | Date separator character (e.g. / in date(*dmy/)). A single character. | — |
Time Format | Native time format used to declare every Time variable (e.g. time(*hms:) instead of plain time), and default for IsTime/StringToTime/TimeToString when their format argument is omitted. Usa is 12-hour with an AM/PM suffix, no seconds — structurally different from the other 4 (24-hour hh:mm:ss). | Hms, Iso, Usa, Eur, Jis |
Time Separator | Time separator character (e.g. : in time(*hms:), also used by TIME()). A single character. Doesn’t apply to Usa (no separator between fields). | — |
Timestamp Format | Default for IsTimestamp/StringToTimestamp/TimestampToString when their format argument is omitted. The timestamp type doesn’t accept a format in its declaration, unlike date/time. Iso is recommended for its broader compatibility. | Iso, Usa, Eur, Jis |
Print Mode | What happens to the QPRINT spool file each Print("Block") writes to: with Screen the program displays it (DSPSPLF) and deletes it on exit; Printer/File just leave it spooled. Default Printer. | Printer, File, Screen |
Commitment | Enables commitment control for the whole project: generated programs and screens (Program and Screen) declare their modified tables commit usropn and open them after a best-effort STRCMTCTL (tables must be journaled). A Program or Screen can override it with @Properties/Commitment. Default False. | True, False |
Commit on Exit | Whether generated programs and screens issue a final commit before finishing. Overridable via @Properties/Commit on Exit. Only effective if Commitment is True. Default False. | True, False |
DDS Name | Pattern for naming each Screen’s display file DDS based on its object name — e.g. <ObjectName>D appends a D. Default <ObjectName> (unchanged). Only affects the DSPF; the generated RPG driver program keeps the object name as-is. The result is always uppercased and truncated to 10 characters (OS/400 object name limit) — the variants that append D truncate to 9 first so the D is never what gets truncated. | <ObjectName>, <ObjectName>D, D<ObjectName>, D<ObjectNameWithoutFirstChar> |
Top-level keys (Date Format, Print Mode, Commitment, etc.), @Screen/@FunctionKeys, and the date/time/timestamp formats are unique at the project level: they aren’t repeated per profile.
@Profile (nested inside @GeneratorConfig)
A .warpcfg declares one or more deployment profiles (typically Dev, UAT, Prod, though the name is free-form) inside @GeneratorConfig. Each @Profile "Name" groups its own @Connection, @Deployment, and @License — so a single project can point to different servers/libraries/licenses depending on the environment, without maintaining several .warpcfg files.
@GeneratorConfig {
@Profile "Dev" {
@Connection { ... }
@Deployment { ... }
}
@Profile "Prod" {
@Connection { ... }
@Deployment { ... }
@License { ... }
}
}
Declaring at least one @Profile is mandatory: @Connection/@Deployment/@License declared directly inside @GeneratorConfig (without wrapping them in @Profile) are no longer valid (error E00290). A @GeneratorConfig with no @Profile at all gives error E00291.
How the active profile is chosen for generate/build/reverse/license-status (the validate action ignores profiles):
- The CLI’s
--profile <Name>flag, case-insensitive. - If omitted,
Default Profilefrom@Project(see the.warproj — @Projectsection above). - If there’s no
Default Profileeither but the.warpcfgonly declares one@Profile, that one is used. - If none of the above applies and there are several profiles declared, the compiler fails with error
E00293, listing the available profiles.
Requesting a profile that doesn’t exist (via --profile or Default Profile) fails with error E00292, also listing the available profiles.
@Screen (nested inside @GeneratorConfig)
| Key | Description |
|---|---|
Rows | Number of screen rows. Numeric, default 24. Only the two 5250 sizes 24x80 and 27x132 are accepted (E00348 otherwise). It is a project setting: it cannot be changed per screen. |
Columns | Number of screen columns. Numeric, default 80 (132 with Rows : 27). |
Function Keys Row | Row where the function-key line is drawn (FunctionKeysRow is also accepted). Valid range: 1 to Rows - 1 (the last row is kept for messages); out of range gives E00346. If omitted, Rows - 1 is used (23 in 24x80; in a popup, the window’s last row minus one). A Function Keys Row in a Screen’s @Properties takes precedence over this one. |
Function Keys Color | Color of the whole key line (FunctionKeysColor also accepted): Green, White, Red, Blue, Yellow, Pink or Turquoise, the same as Style. An invalid name gives E00347. Without a value the line has no color. A Function Keys Color in a Screen’s @Properties takes precedence over this one, and a key’s color in its Event takes precedence over both. |
The compiler does not warn if the chosen row overlaps the @Layout: whoever moves it must leave that row free. If the keys do not fit in one line and there is no F24 rotation, the second line goes up from the chosen row.
@FunctionKeys (nested inside @Screen)
One F<n> : "Label" entry per line (n from 1 to 24, e.g. F3 : "Exit"). Attached to its own event with Event 'Name' <n> ... EndEvent in a Screen’s @Source.
Key rotation with F24. If F24 is declared here (e.g. F24 : "More keys") and no Event in the file claims it (Event 'X' 24), the Screen’s attention line can rotate. All keys are considered (those in @FunctionKeys plus those of Events with a key). If they fit in one line, there is no rotation and the line is generated as usual. Otherwise they are split into sets ordered by key number, each fitting in a single line and ending with F24=<label>. Pressing F24 shows the next set (after the last one it returns to the first); the keypress only changes the key line and does not run any event. The usable width is the screen width minus 2 columns (the window width in a popup). Without F24 declared, or with an Event on F24, the line shows all keys in one or two lines.
@FunctionKeys {
F3 : "Exit"
F5 : "Refresh"
F24 : "More keys"
}
@Connection (nested inside @Profile)
How to reach the IBM i:
| Key | Description | Values |
|---|---|---|
Host | Hostname or IP of the IBM i. Required. | — |
User | User profile to connect with. Required. | — |
Auth Method | How to authenticate. Required. password reads the password from the WARPGATE_PASSWORD environment variable; interactive, when run from the VSCode extension, prompts for it with a dialog box on every Build & Deploy and every Reverse Engineer DDS (running the raw binary from a terminal, it behaves the same as password). | key, password, interactive |
Key File | Path to the private key file. Required only if Auth Method: key. | — |
Protocol | Transport protocol. Required. ftp can’t be combined with Auth Method: key. | ssh, sftp, ftp |
Port | TCP port. Numeric, default 22. | — |
Timeout | Connection timeout in seconds. Numeric (-128 to 127), default 30. | — |
ssh/sftp/ftp use native clients: there’s no need to have any ssh/scp/sftp/ftp client installed on the machine running the compiler.
Examples by protocol and authentication method
SSH or SFTP with a key (ssh/sftp are equivalent: the native client uses the same transport for both):
@Connection {
Host : MYHOST
User : MYUSER
Auth Method : key
Key File : ~/.ssh/id_ed25519_ibmi
Protocol : ssh
Port : 22
Timeout : 30
}
SSH or SFTP with a password — the password isn’t written into the .warpcfg; it’s read from the WARPGATE_PASSWORD environment variable, which must exist before opening VSCode (or the terminal the compiler runs from):
@Connection {
Host : MYHOST
User : MYUSER
Auth Method : password
Protocol : sftp
Port : 22
Timeout : 30
}
FTP with a password — FTP has no concept of key-based authentication (Auth Method: key with Protocol: ftp is a validation error), so it only accepts password/interactive, both read from the same WARPGATE_PASSWORD variable:
@Connection {
Host : MYHOST
User : MYUSER
Auth Method : password
Protocol : ftp
Port : 21
Timeout : 30
}
Generating the Key File
Auth Method: key needs an SSH key pair: a private one (the one Key File points to) and a public one, which must be authorized on the IBM i user profile.
- Generate the key pair (on the machine running the compiler/VSCode, not on the IBM i):
This createsssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_ibmi -N ""id_ed25519_ibmi(private — the path that goes inKey File) andid_ed25519_ibmi.pub(public).-N ""leaves the key without a passphrase, a requirement forAuth Method: key. - Copy the contents of
id_ed25519_ibmi.pub(a single line) to the end of the.ssh/authorized_keysfile inside the IBM i user’s IFS home directory (e.g./home/MYUSER/.ssh/authorized_keys) — create the file and directory if they don’t exist. The IBM i’s SSH daemon (SSHD, from product 5733-SC1) must be active and configured to accept key-based authentication. - Verify that
Key Filein@Connectionpoints to the private key’s path on the local machine (not the.pubone), and thatAuth Methodiskey.
@Deployment (nested inside @Profile)
Where/how to deploy and compile on the IBM i:
| Key | Description | Values |
|---|---|---|
CL Source | Name of the generated CL source member (OS/400 object name, max. 10 characters). Required. | — |
Cleanup | Whether the staged source is removed after a successful compile. | true, false |
Target Release | Target OS/400 release for the compile (e.g. V7R4M0). | — |
Optimization | RPG compiler optimization level. It is emitted as OPTIMIZE(...) in the programs’ CRTBNDRPG. If omitted, nothing is emitted. Any other value produces error E00020. | *NONE, *BASIC, *FULL |
Debug | Program debug view. true emits DBGVIEW(*SOURCE); false emits DBGVIEW(*NONE). If omitted, nothing is emitted and the compiler uses its default. | true, false |
Temp Path | IFS staging directory used during deploy. Required. | — |
Data Library | Library where the compiled DDS physical/logical file objects are created (OS/400 object name). Required. | — |
Objects Library | Library holding the DDS/CL source members (QDDSSRC/QCLSRC) and where the compiled CL program object is created (OS/400 object name). Required. | — |
Source Tables | Source member holding the physical/logical DDS source (OS/400 object name). Required. | — |
Source Programs | Source member holding the generated RPGLE/CL. | — |
Compile Library | Comma-separated list of libraries added to the compile job’s library list, in order, instead of just Data Library — for programs whose tables live across several libraries. | — |
@License (nested inside @Profile)
Optional block with the licensing configuration, specific to each profile — so Dev/UAT/Prod can consume different licenses or licensing services. Keys are case-insensitive. See the Licensing guide.
| Key | Description | Values |
|---|---|---|
Host | License service host. With Host the mode is organization and File is ignored; without Host, the mode is individual. | — |
Port | Service TCP port. Default 7443. | 1-65535 |
Fingerprint | SHA-256 fingerprint of the service certificate (accepts : and uppercase). | 64 hex |
SPN | Kerberos service name. Default warpgate-license/<host>. | — |
File | Individual license file. Default ~/.config/warpgate/license.json (%APPDATA%\warpgate\license.json on Windows). | — |
@License {
Host : "servidor01.dominio.local"
Port : 7443
Fingerprint : "3f2a9c41d87b05e6a1c4f0937be2d5688a1f4c0d29e7b3a65c8d1f0e4b7a9c23"
}
Full example
See Project: .warproj and .warpcfg for a complete example with both files.
Diagnostics and error messages
The compiler reports every issue with an E##### code (e.g. E00221), a severity level, and a message — currently only in Spanish — with the concrete values interpolated.
Levels
| Level | Meaning |
|---|---|
error | Blocks code generation; the object doesn’t compile. |
warning | Doesn’t block compiling, but flags something probably unintentional (e.g. a variable declared and never used). |
What they look like
On the command line (human format):
[2026-07-24T21:26:47Z] ERROR E00221 (line: 12, column: 5): La variable 'x' declara char(16773105), fuera del rango permitido (1-16773104).
Or as JSON (--format json, the one the VSCode extension consumes to draw red/yellow underlines in the editor):
{"errors": [{"line": 12, "column": 5, "severity": "error", "message": "..."}]}
Some representative examples
| Code | Situation |
|---|---|
E00001 | A @Name section was expected and something else was found. |
E00128 | A file referenced by the .warpcfg (e.g. the SSH key in @Connection) doesn’t exist on this machine — doesn’t block validate/generate, but will be needed for build/reverse. |
E00148 | The field in field(...) was not found: for field(Field), in no table of @References/Tables; for field(Table.Field), because the table isn’t referenced or doesn’t contain that field. |
E00149 | The field in field(Field) is ambiguous: it exists in more than one table of @References/Tables; specify the table with field(Table.Field). |
E00152 | A variable from @Variables was declared but never used (warning). |
E00178 | A variable used in @Layout is not declared in the global scope of @Variables. Also reported when a &variable.member path points to a member that does not exist. |
E00179 | ‘&{arg}’ (an in parameter or a constant, read-only) cannot be passed to ‘{name}’, which modifies it internally (directly or indirectly). |
E00192 | {name}() can only be used inside an Event, Procedure or Function of a Screen (the cursor position only exists after the screen is read). |
E00195 | Invalid use of RefreshSelectRow()/For Each Row outside a Screen with a DataGridView. |
E00206 | String(): 2 to 4 arguments were expected (number, integers[, decimals][, fill]), {found} were found. |
E00210 | Two @Layout variables or paths truncate to the same DDS field (struct paths are flattened: . becomes _, uppercase, 10 characters; e.g. cust.Code and cust_Code). |
E00221 | char outside the allowed range for a standalone variable in @Variables (1-16,773,104). |
E00222 | number with precision out of range (1-63) in @Variables. |
E00223 | number whose scale is greater than its precision. |
E00224 | varchar outside the allowed range (1-16,773,100). |
E00237 | Member type not allowed in a struct template: only char/varchar/number/date/time/timestamp/field, struct(Template) and list(struct(Template)) are allowed (neither matrix nor list of scalar types). |
E00238 | struct(Name) references a template that doesn’t exist in @Variables; also reported for nonexistent templates inside members of another template and in list(struct(X)). |
E00240 | &Variable.Member access to a member that doesn’t exist in the struct’s template. |
E00242 | A struct(OtherTemplate) variable was passed where a different template was expected (the comparison is by exact name, not by structure). |
E00253 | The format of StringToDate/NumberToDate/IsDate/DateToString must be one of Iso/Usa/Eur/Jis/Mdy/Dmy/Ymd/Jul, or a text pattern with YYYY, MM and DD once each (e.g. "YYYYMMDD", "DD/MM/YYYY"; in DateToString, up to 10 characters). |
E00269 | A table’s Description (@Structure) exceeds the 50 characters allowed by TEXT in DDS. |
E00270 | A field’s Description (@Fields) exceeds the 50 characters allowed by TEXT in DDS. |
E00271 | An index’s Description (@Indexes) exceeds the 50 characters allowed by TEXT in DDS. |
E00278 | list has no such method (valid ones: Add, Clear, Init, Remove, Count, Split, Json). |
E00281 | The Description in @Properties (of a Program or Screen) exceeds the 50 characters allowed by TEXT in the compile-time CL commands (CRTBNDRPG/CRTDSPF/CHGPFM). Same limit and criterion as E00269/E00270/E00271. |
E00284 | @License Port is 0. |
E00285 | @License Host is not a valid host. |
E00286 | @License Fingerprint is not a valid SHA-256 fingerprint. |
E00287 | Warning: @License has Host but no Fingerprint. |
E00288 | Warning: @License has Host and File together (File is ignored). |
E00290 | @Connection/@Deployment/@License declared directly inside @GeneratorConfig, without wrapping them in a @Profile "Name". |
E00291 | @GeneratorConfig doesn’t declare any @Profile. |
E00292 | The profile requested with --profile or Default Profile doesn’t exist among the declared @Profiles (available ones are listed). |
E00293 | Several @Profiles are declared and it wasn’t indicated which one to use (neither --profile nor Default Profile), so the compiler can’t pick just one (available ones are listed). |
E00294 | .Json() used as a standalone statement: it returns the JSON as a value and must be used in an expression (&text = &Person.Json()). |
E00295 | .Json() applied to a variable that is neither a struct nor a list. |
E00296 | A method other than Json was called on a struct (Json is the only valid method). |
E00297 | .Json() on a list whose element is neither a scalar type (char, varchar, number, date, time, timestamp) nor a struct. |
E00298 | A variable in the local scope is also declared in global; the name can’t be repeated because both scopes live at module level in the generated RPG. |
E00299 | The struct template ‘{template}’ contains itself (directly or indirectly) through its struct/list(struct) members. |
E00300 | The variable ‘{variable}’ would take {size} bytes, more than the maximum of an RPG variable (16773104): reduce the size of its members or of the lists it contains (a list inside a struct reserves 100 elements). |
E00301 | JsonSplit(): the first argument must be a char/varchar &Variable (or struct member), or a text literal; found {found}. |
E00302 | JsonSplit(): ‘&{variable}’ must be a list of char or varchar (the lines are stored there). |
E00303 | JsonSplit(): the indentation must be a number, found {found}. |
E00304 | The @Parameters parameter ‘&{variable}’ cannot be a list of that type or a matrix: only lists of char or varchar are supported (an array of text lines, ending at the first empty line). |
E00305 | {method}(): ‘&{variable}’ must be {expected} whose members are char, varchar, number or date, with a record of up to 32766 positions.{hint} |
E00306 | {method}() must be used as {where}. |
E00307 | {function}(): a char/varchar value or a list of char/varchar was expected, found {found}. |
E00308 | {function}(): ‘&{variable}’ must be a list(struct(…)). |
E00309 | {function}(): the {width}-position record does not fit in the elements of ‘&{variable}’ (char/varchar({element})). |
E00310 | The constant ‘{name}’ must be of type char, varchar or number. |
E00311 | The value of the constant ‘{name}’ is not valid for its type: {detail}. |
E00312 | The constant ‘&{variable}’ cannot be modified. |
E00313 | The @Parameters parameter ‘&{variable}’ cannot be a constant. |
E00314 | The constant ‘&{variable}’ cannot be displayed in a screen field: copy it to a variable. (In a printable Block of a Program it can be used.) |
E00315 | Table ‘{table}’ has no PrimaryKey in @Indexes: ‘Join In’ reads it by its primary key. |
E00316 | Join In {table}: primary key field ‘{field}’ ({key}) does not exist in the table being read or in the tables already joined. Use ‘On’ to say where each field comes from. |
E00317 | Join In {table}: the ‘On’ fields on the joined table side must be exactly its primary key fields ({key}), with none repeated or missing. |
E00318 | Join In {table}: field ‘{field}’ ({target_type}) does not accept ‘{source}’ ({source_type}). |
E00319 | Field ‘{field}’ belongs to table ‘{table}’ joined with ‘Join In’ and is read-only: it cannot be assigned. |
E00320 | Style: ‘{name}’ is not a known color or attribute. Colors: Green, White, Red, Blue, Yellow, Pink, Turquoise. Attributes: Reverse, High, Underline, Blink, Hidden. |
E00321 | Style: only one color per Style is allowed; found ‘{name}’ in addition to ‘{previous}’. |
E00322 | Invalid condition in @Layout: only variables, numbers, strings, comparisons, And/Or/Not and Function() calls without arguments are allowed. |
E00323 | Too many Style entries on the same element: at most one unconditional and three conditional ones are allowed. |
E00324 | Style has no color or attribute: give at least one (for example Style(Red) or Style(Reverse)). |
E00325 | Style is only allowed in a Screen. |
E00326 | Exit is only valid inside a loop (For Each, For Each Row, For or While). Inside a Procedure, Function or Event, the loop must be in the same body. |
E00327 | ‘{keyword}’ is a reserved word that is only valid inside a ‘Do Case’ (Do Case / Case <condition> / Otherwise / EndCase). |
E00328 | Round(): expected 2 arguments (expression, decimals), found {found}. |
E00329 | Round(): ‘decimals’ must be an integer literal between 0 and {max} (a compile-time constant: it defines the scale of the result). |
E00330 | Round(): the first argument must be of type Number (found {type}). |
E00344 | The key text of event ‘{event}’ cannot be empty or longer than {max} characters. |
E00345 | Event ‘{event}’ has the same name as a Procedure or Function in this file; both generate a subprocedure with that name. Rename one of them. |
E00331 | SetFocus() is only valid in a Screen. |
E00332 | SetFocus(): ‘&{variable}’ is not the variable of any Input in @Layout. |
E00333 | SetFocus() takes no arguments ({found} given). |
E00334 | SetFocus() is a statement: it returns no value and cannot be used inside an expression. |
E00335 | Refresh is only valid in a Screen. |
E00336 | Cursor(): expected 1 argument (&variable), found {found}. |
E00337 | Cursor(): the argument must be a &variable. |
E00338 | Cursor(): ‘&{variable}’ is not the variable of any Input or DataGridView column in @Layout. |
E00339 | Table ‘{table}’ has no PrimaryKey in @Indexes: ‘When Duplicate’ re-reads the existing record by its primary key. |
E00340 | Field ‘{field}’ is part of the primary key of ‘{table}’: ‘When Duplicate’ cannot reassign it. |
E00341 | String(): the fill must be a literal text of a single character (for example “0” or “ “). |
E00342 | Int(): 1 argument (a number) was expected, {found} were found. |
E00343 | Int(): the argument must be of type Number (found {type}). |
E00346 | Function Keys Row ({row}) is out of range: it must be between 1 and {max} (Rows - 1; the last row is kept for messages). |
E00347 | ‘{color}’ is not a valid color for function keys; use Green, White, Red, Blue, Yellow, Pink or Turquoise. |
E00348 | Screen size {rows}x{columns} is not valid: a 5250 only supports 24x80 or 27x132. |
E00349 | Record format ‘{name}’ is not valid: up to {max} characters, letters and digits only, starting with a letter. |
E00350 | The object name ‘{name}’ is not valid for IBM i: up to {max} characters, only letters and digits, and it must start with a letter. Rename the file (or the Name property). |
E00351 | Return without a value inside Function ‘{name}’: a Function must return a value (Return &variable or Return expression). |
E00352 | Function ‘{name}’ never returns a value: add at least one Return with a value. |
E00198 | LoadRow() can only be used inside an Event or a Procedure of a Screen (not in a Function, which is invoked as an expression). |
E00199 | Retired: LoadRow() no longer has to be inside a For Each. |
E00200 | Retired: there is no longer a limit of one For Each with LoadRow() inside Event Load. |
E00201 | The For Each that loads the DataGridView (with LoadRow()) must declare an Index: without one, there are no ordered fields to resume loading from when Page Down is pressed. |
E00353 | {element} ends at column {end}, but the screen has {max} columns: IBM i rejects the field or splits it across two rows. Shorten the text or the widths, or move the element to the left. |
E00356 | {scope} declares the local variable ‘{variable}’, which shadows the global variable of a DataGridView Column: LoadRow() would copy the local value. Rename the local variable. |
E00357 | The event name ‘{event}’ is not valid: it becomes a subprocedure name, so it only allows letters, digits and _ and must start with a letter. The text shown in the key row goes separately: Event ‘Name’ 6 “Text with spaces”. |
E00358 | {function}() expects {expected} argument(s), but received {found}. |
E00359 | {function}(): argument {arg} must be text (char or varchar), not {type}. |
E00360 | {function}(): argument {arg} must be numeric, not {type}. |
E00361 | {function}(): argument {arg} must be a literal: {rule}. |
E00362 | Mod(): argument {arg} cannot have decimals (RPG requires integers for %rem); use Int() to truncate it. |
E00363 | &{variable}.Split() only works on a list of char or varchar (each piece is text). |
E00364 | ‘EndTry’ is missing to close the ‘Try’ block. |
E00365 | The status code ‘{code}’ of ‘Catch’ is not valid: it must be a number between 100 and 9999 (program errors are 100-999 and file errors 1000-2999). |
E00366 | {function}() can only be used inside a ‘Catch’ block. |
E00367 | A ‘Try’ needs at least one ‘Catch’. |
E00368 | This ‘Catch’ never runs: an earlier ‘Catch’ without a filter (line {line}) already catches every error. Put the ‘Catch’ without a filter last. |
E00369 | Parameter ‘{variable}’ of @Parameters is used in @Layout and its name has 10 characters or fewer: in a Screen the DDS field takes that same name and RPG confuses it with the parameter (it does not compile on IBM i). Use a name longer than 10 characters. |
E00370 | {function}(): argument {arg} must be {expected}, found {type}. |
Each message has a code, a level (error/warning) and a template with the case’s actual details ({variable}, {length}, etc.) the compiler fills in when reporting it.
Message language
The compiler embeds its message catalogs in Spanish and English, with the same codes and the same template placeholders. The language is chosen with --lang (or -g):
warpgate --action validate --lang en
warpgate --action validate -g es
- If
--langis omitted, it is taken from theLANGenvironment variable (for examplees_PE.UTF-8selectses). - If
LANGdoes not name a real language (C,POSIXor empty),enis used. - An external
resources/messages_<language>.jsonfile next to the executable can override messages from the embedded catalog.
In VSCode, the language is controlled by the warpgate.language setting (see VSCode commands).
What the language covers
--lang does not only affect diagnostics with line/column (E*, W*). All user-facing text comes from the catalog and follows the selected language:
- CLI log lines for validation, generation, reverse engineering and deployment.
- Deployment progress and errors (
[Despliegue]in Spanish,[Deployment]in English): connecting, uploading, running, etc. - SSH/FTP/SFTP connection errors (for example, “no se pudo conectar” / “could not connect”) and transport errors.
- License messages: service denials, individual license, status (
license-status) and request (license-request). - Generation errors, such as
@Connectionor@Deploymentnot found.
These messages use the code series M00001-M00106 (in messages_es.json and messages_en.json). Unlike the E* codes, they carry no line or column.
A few internal code generator errors (invariants such as “validation should have caught this”) always remain in Spanish, as do texts coming from the operating system (for example, Connection refused (os error 111)).
When they fire
- On saving/opening a
.warpfile in VSCode (warpgate.validateOnSave/validateOnOpen), if there’s a.warprojresolved for that file. - Manually with
warpgate --action validate— never touches the IBM i, purely local. - License note:
validatedoes not require a license;generate/build/reversedo (see Licensing). - As part of
generate/build/reverse— if there are errors, code generation/deployment doesn’t proceed.
IntelliSense in VSCode
The WaRPGate extension for VSCode adds language support for .warp files:
- Syntax highlighting: sections, keywords, data types, event names, function calls, operators.
- Autocomplete (
@/&as triggers): sections,@Sourcekeywords, data types, builtin functions (with a snippet), per-section property keys,@Layoutcontrols. - Hover: this same book’s documentation, summarized in one line, when hovering over any keyword/function/type.
- Live diagnostics: validates on open/save (see Diagnostics), drawing red/yellow underlines directly in the editor.
- Outline/breadcrumbs: section structure and
Procedure/Functionas navigable symbols. - Go to definition and document links: jump to the table/program/screen referenced in
@References. - Layout Preview: live preview of
@Layoutas an IBM i 5250 terminal screen (24x80 by default, or the size configured in the project’s.warpcfg@Screen). - Commands: see VSCode commands for a screenshot example of each (
Validate File,Generate,Build & Deploy,Select Project File,Reverse Engineer DDS,Preview Layout,Select Layout Block).
IntelliSense stays in sync with every new language release, so highlighting, autocomplete and diagnostics always reflect the compiler’s current capabilities.
Installation
The packaged .vsix includes the compiler binaries for all 3 platforms — there’s no need to install the compiler separately.
VSCode commands
The WaRPGate extension adds 11 commands to the command palette (Ctrl+Shift+P / F1), all prefixed with WaRPGate:. This page shows a real example of each, with screenshots.
WaRPGate: Validate File
Runs the compiler in validation mode on the active .warp file: checks syntax and semantics (types, references, undeclared variables, etc.) without generating any file. Errors and warnings show up in the “WaRPGate” output channel.
Example: a Program that assigns an undeclared variable.
@Source {
&CodigoCliente = &Inexistente
}
WaRPGate: Generate
Runs the full compiler on the active file: validates and, if there are no errors, generates the RPGLE/DDS/CL files locally (nothing is deployed to IBM i). It’s the equivalent of the CLI’s --action generate.
WaRPGate: Build & Deploy
Generates just like the previous command and additionally uploads the files to the IBM i configured in the .warpcfg’s @Connection, to compile them there (CRTPF/CRTLF/CRTBNDRPG/CRTDSPF). Since this connects to a real system, it always asks for confirmation before continuing.
WaRPGate: Select Project File (.warproj)
When the workspace has more than one .warproj, this command picks which one to use for validating/generating/deploying. The chosen project is remembered for the other commands.
WaRPGate: Reverse Engineer DDS…
Reconstructs a table .warp file from a DDS member that already exists on IBM i. It asks, in order: the QSYS.LIB path of the table’s DDS member, the index (LF) DDS members to include in @Indexes (optional), and the name of the .warp file to generate. At the end it confirms before connecting.
WaRPGate: Preview Layout
Shows the active file’s @Layout as an IBM i 5250 terminal screen (see IntelliSense in VSCode). If the Screen uses Popup: True, the preview draws the window at its real position and size within the full screen.
Example: a popup Screen with a 3-column DataGridView.
WaRPGate: Select Active Profile (@Profile)
Available from the command palette and from the editor’s context menu on a .warp file. Opens a picker with the profiles (@Profile) declared in the active project’s .warpcfg, plus the “Use project default” option. The choice is remembered for that project (persists across VSCode restarts) and is reflected in its own status bar item (layers icon), which also opens the same picker with a click.
Without an explicit choice, the extension doesn’t send --profile to the compiler and the CLI falls back to the Default Profile declared in @Project — see Project file.
WaRPGate: Select Layout Block
When a Program (report) has more than one Block in its @Layout, this command picks which one to show in the Layout Preview — the preview always shows a single Block at a time.
WaRPGate: Request License
Asks for the name or company the license will be issued to, opens a dialog to save the request.json file, and runs --action license-request. When done, it shows the machine identifier with the Copy Machine ID (copies the identifier to the clipboard) and Reveal File (opens the file’s location in the system file explorer) buttons. Send the request.json and the machine identifier to warpgate@softwarehouse.pe.
WaRPGate: Install License
Opens a picker to choose the license.json received from Software House. It checks that the file is a license (if it is the request file or not valid JSON, it shows an error) and copies it to the path the compiler reads: %APPDATA%\warpgate\license.json on Windows, ~/.config/warpgate/license.json on Linux/macOS. If WARPGATE_LICENSE_FILE is set, that path is used.
If a license already exists at that path, it asks for confirmation before replacing it: the previous one stops being valid. When done it offers the Check Status button.
WaRPGate: License Status
Runs --action license-status and shows the license status, its expiry and the product. If warpgate.projectFile is configured, it uses that project’s @License (and the active profile, if one is selected); otherwise it checks the individual license at the default path. The full detail goes to the “WaRPGate” output channel.
For the complete request and install flow, see Licensing.
Compiler message language
The warpgate.language setting (auto | es | en, default auto) sets the language of the diagnostics shown by the compiler. The extension passes it as --lang in every compiler call. With auto, the VSCode language is used: Spanish if it starts with es, English otherwise.
This setting only affects compiler messages; it does not change the extension’s menus or hovers. See also Diagnostics.