STRUMENTA LANGUAGE ENGINES · IBM i CONTROL LANGUAGE

A CL parser that does not fall over on a command it has never seen.

2,156 IBM i commands are compiled in as typed AST classes with their required parameters as named fields. Everything else — your commands, your vendor’s commands, commands from a release we do not model — parses into a faithful generic node with its parameters intact.

CL is a small language over an enormous, open-ended vocabulary. Any parser can handle the language. Handling the vocabulary, and then handling what is not in the vocabulary, is the whole job.

The engine at a glance
FLAVORSILE CL, and OPM CL too
COMMANDS2,156 typed AST classes
VOCABULARY~4,000 soft-keyword tokens
DATA TYPES26
OUTPUTAST · JSON · XML · EMF · LionWeb
RUNTIMEJVM library · CLI

There is no closed grammar for CL

CLLexer.g4 · ~4,000 tokens · CLParser.g4 · softKeyword

Every IBM i command is an object with a command definition that declares its parameters, their keywords, their types and their defaults. IBM ships thousands. Every ISV adds their own. Every customer adds their own. Parsing CRTDUPOBJ OBJ(X) FROMLIB(Y) OBJTYPE(*FILE) means knowing that CRTDUPOBJ takes those keywords — and no parser can know all of them.

The two layers a CL statement can take through the parser A command in the compiled-in vocabulary of 2,156 commands becomes a typed AST class whose required parameters are named fields. A command outside the vocabulary falls through to the genericStatement rule and becomes a generic node holding its qualifying library, its name, its labels and every parameter it was given. Neither path breaks the parse. one CL statement in the vocabulary not in the vocabulary typed command node generic command node CRTDUPOBJ · ACMESCHED 2,156 compiled-in commands genericStatement fallback required parameters as fields library, name, labels, parameters NEITHER PATH FAILS, AND NEITHER PATH SWALLOWS THE REST OF THE MEMBER.

The first layer is a large closed vocabulary compiled into the grammar: roughly four thousand lexer tokens for the command names and parameter keywords of the IBM i command set, and 2,156 generated AST classes — AddAccessCodeCommand, AddAutostartJobEntryCommand, AddAlertDescriptionCommand and the rest — each exposing that command’s required parameters as typed fields alongside a generic parameter list, its labels and its prompt type.

They are soft keywords. A name like VALUE or TYPE is a keyword where a keyword is expected and an ordinary identifier everywhere else. That is the part a from-scratch attempt gets wrong: compiling several thousand names into a grammar without breaking the customer variables and object names that happen to collide with them.

The second layer is the fallback. The command rule ends with genericStatement, and genericStatement is labels? nameAndLibrary positionalKeyword*. So an unrecognized command still parses into a node that records its qualifying library, its name, its labels and every parameter it was given, in both positional and keyword form, as structured expressions.

The honest consequence: for a recognized command you get a typed node with named parameters; for an unrecognized one you get a faithful generic node with the parameters as data. What no parser can get from source alone is the parameter semantics of your own commands — that needs their command definitions, and it is a question for qualification rather than a defect.

Input — OPM-compatible CL

basic_example.cl

             PGM        PARM(&curdate &DAYSTOCHG)
             DCL        VAR(&CURDATE) TYPE(*CHAR) LEN(8)
             DCL        VAR(&DAYSTOCHG) TYPE(*DEC) LEN(15 5)
             DCL        VAR(&DATE) TYPE(*CHAR) LEN(8)
             DCL        VAR(&ERRCOD) TYPE(*CHAR) LEN(4) +
                          VALUE(X'00000000')
             DCL        VAR(&MSG) TYPE(*CHAR) LEN(50)
             CHGVAR     VAR(&MSG) VALUE('The new date is ' *CAT &DATE)
             ENDPGM
Four marked lines, four separate problems. &curdate in PARM and &CURDATE in DCL are the same variable, because CL names are case-insensitive. LEN(15 5) is a type expressed as parameter data. The + continues a parameter list mid-command. X'00000000' is a hex literal, and *CAT is an operator rather than a command.

Output — AST, by node class

intermediate nodes elided

CLCompilationUnit
└── Procedure line 1
    ├── DeclareVariableCommand &CURDATE, Character(8)
    ├── DeclareVariableCommand &DAYSTOCHG, PackedDecimal(15, 5)
    ├── DeclareVariableCommand &ERRCOD, Hexadecimal value
    ├── ⋯
    └── CLCommandWithParameters CHGVAR, line 8
        ├── CLReferenceExpr &MSG → DCL line 7
        └── CLReferenceExpr &DATE → DCL line 5
These are the real class names in the shipped module. Character, PackedDecimal and Hexadecimal are three of the 26 types in the type hierarchy. The two CLReferenceExpr arrows are the semantics module resolving references to declarations — case-insensitively, and across files, not only within one member.

A member with a command we have never heard of

Every real IBM i system has some of these. A scheduler from one vendor, a monitoring command from another, a wrapper somebody wrote in 1998 whose command definition source may or may not still exist. Here is what happens to them.

Input — IBM commands, an in-house command and a nested one

illustrative; ACMESCHED stands in for your scheduler

             OVRDBF     FILE(CUSTMAST) TOFILE(ARCHIVE/CUSTMAST)
             ACMESCHED  JOB(NIGHTLY) PRIORITY(5) RETRY(*YES)
             CALL       PGM(BILLRUN) PARM(&CUSTNO &PERIOD)
             MONMSG     MSGID(CPF0000) EXEC(GOTO CMDLBL(ERROR))
             CALL       PGM(QCMDEXC) PARM('DLTF ARCHIVE/WORK' 20)
 ERROR:      SNDPGMMSG  MSG('nightly run failed')
             ENDPGM
OVRDBF, CALL, MONMSG and SNDPGMMSG become typed nodes with named parameters. ACMESCHED becomes a generic node holding JOB, PRIORITY and RETRY as keyword parameters — recorded faithfully, and available to your analysis, without a signature. The second marked line is a command nested inside another command’s parameter, which the grammar models directly. The last CALL is the one nothing can help with: a CL command assembled as a string and executed by QCMDEXC is opaque to any parser, and a report that does not say so is lying to you.

Four places where CL is not the language people assume

CL is often described as IBM i’s job control language. It is not: it is a procedural language with variables, types, conditionals, subroutines and error handling. These four are where that shows.

Positional and keyword forms are the same command

CHGVAR &A VALUE(1) and CHGVAR VAR(&A) VALUE(1) are identical. Which positional slot maps to which keyword is defined by the command definition, not by the grammar — so the mapping has to come from the compiled-in vocabulary, one command at a time. Both forms are supported, along with qualified LIB/OBJ names and selective prompting.

             CHGVAR     &A VALUE(1)
             CHGVAR     VAR(&A) VALUE(1)
             ?CRTLIB    LIB(TEST)

A star is not a string

*LIBL, *CURLIB, *ALL, *SAME, *NONE, *ISO and their hundreds of siblings are special values, and the parser has to keep them distinct from variable names and from character constants. *N is the “not specified” placeholder and is positionally significant — drop it and every parameter after it shifts.

             CRTDUPOBJ  OBJ(X) FROMLIB(*LIBL) OBJTYPE(*FILE)
             SAVLIB     LIB(PROD) DEV(*SAVF) *N SAVF(QGPL/S1)

A monitor whose scope is its position

MONMSG is a message monitor. Placed at the top of a program it is global; placed immediately after a command it attaches to that command alone. The difference is expressed by position and nothing else, and getting it wrong means reporting error handling that is not there — or missing the one that is.

             DLTF       FILE(QTEMP/WORK)
             MONMSG     MSGID(CPF2105)
             CRTPF      FILE(QTEMP/WORK) RCDLEN(80)

Small languages inside parameters

OPNQRYF carries an expression language of its own in QRYSLT, and MAPFLD declares fields with a type, a length, decimal positions and a CCSID inside a parameter value. Both are modeled rather than kept as strings. RUNSQL and STRSQL carry SQL, which is captured for a SQL module to parse.

             OPNQRYF    FILE((CUSTMAST)) +
                          QRYSLT('BALANCE *GT 0') +
                          MAPFLD((DUE 'BAL - PAID' *DEC 9 2))

One thing about CL is genuinely easy, and it is worth saying because it changes what the work costs: CL has no macro preprocessor. No /COPY, no include, no conditional compilation pass. There is nothing to expand — but equally, nothing to lean on for cross-member structure, which is why the symbol resolution has to work over the whole exported source set rather than one file at a time.

WHY ANYONE PARSES CL AT ALL

CL is where the application actually runs.

The RPG holds the business rules. The CL holds the order the programs run in, the files they are pointed at, the libraries they are found in, and what happens when one of them fails. That is the layer every modernization assessment needs first and reads last.

“Which programs does the nightly batch run, in what order, against which library?” is a CL question. So is “which of these 900 members has a command with no MONMSG anywhere near it”, and “every LIB/OBJ reference in the estate, with its position, before we consolidate libraries”.

The semantics module resolves variable references to their declarations and label and subroutine references to their definitions, case-insensitively — because &CURDATE and &curdate are one variable — and it works over a whole codebase rather than a single member.

The part to plan for rather than assume: OVRDBF and OVRPRTF change which file a called program opens, and the library list is set outside the program entirely. The engine records those commands precisely and positions them. Deciding what a given job actually opened is analysis you build on top, with the search order you supply.

Evaluating without a license

the metamodel subcommand

# the EMF/Ecore metamodel of the whole
# CL AST, no license required
cl-parser metamodel --mmxmi

# parsing needs one
cl-parser parse --license PATH src/

# licenses are time-limited and refreshed
cl-parser downloadlicense USER PASS PATH
You can generate the metamodel and see the exact shape of the tree — every command class, every type, every field — before you have a license or a contract. That is deliberate: the shape of the model is the thing you are actually buying, and you should be able to look at it first.
2,156
COMMANDS AS TYPED AST CLASSES
~4,000
SOFT-KEYWORD TOKENS IN THE LEXER
26
CL DATA TYPES MODELED
1
FALLBACK RULE, SO NOTHING BREAKS THE PARSE

Coverage, by construct

Every rule name in the middle column is a rule in the grammar we ship. The status column says what you get, not how hard we tried.

ILE CL is the explicitly supported flavor; OPM CL works too. “Recorded” below means the construct is in the tree with its parameters and position, but its run-time effect is analysis you build.
Construct Grammar rules and classes Status
Recognized IBM commands 2,156 generated classes on CLCommandWithParameters Typed node, required parameters as fields
Unknown, ISV and in-house commands genericStatement, nameAndLibrary, positionalKeyword Generic node, parameters kept as data
Declarations and the type system DeclareVariableCommand, 26 type classes Supported
Control flow, labels and subroutines Procedure, Subroutine, CLLabel Supported — including the ILE-only forms
Selective prompting SELECTIVE_PROMPT, promptType Supported
Commands nested in parameters positionalKeyword : EXEC ( command ) Supported
Cross-file reference resolution scope provider, symbol repository, symbol resolver Supported — case-insensitive, over a codebase
OPNQRYF field mapping mapfldValue Supported — type, length, decimals, CCSID
SQL carried in RUNSQL / STRSQL strsqlStatement Captured, not parsed — that is the SQL module’s job
Parameter semantics of your own commands Needs your command definitions; not recoverable from CL source
Commands assembled as strings for QCMDEXC Opaque to any parser
File overrides and library-list behavior at run time Recorded, not simulated

What you receive

Two modules, a command-line tool and a person to write to. No connection to the IBM i is required: you analyze exported members offline.

01 The engine

An AST module and a semantics module

Two JVM libraries, plus a standalone fat command-line jar with Windows and Linux launcher scripts. The AST module parses; the semantics module resolves. You can take the first without the second, but on a real codebase you will want both.

AST module · semantics module · CLI
02 The model

A typed AST, a metamodel and four serializations

A Starlasu AST with a position on every node and a positioned issues list rather than an exception. Serialize to JSON or XML, generate the EMF/Ecore metamodel yourself with the metamodel subcommand, and interchange through LionWeb.

JSON · XML · EMF/Ecore · LionWeb
03 The pipeline

A component, not an island

The module plugs into the same pipeline as the other language engines through a Starlasu parser and semantic enricher, so a CL tree and an RPG tree can be built, enriched and walked by one process rather than two tools that meet in a CSV file.

same pipeline as RPG, DDS and SQL
04 The license

Standard, Distribution or Service — support included

Standard for use inside your own organization, Distribution if the engine ships inside a product you sell, Service if it runs behind a service you operate. A license file is registered once per process, or passed on the command line, and is refreshed from our license service — the same arrangement as every other engine in the catalog. The metamodel subcommand needing none is a tooling exception, not a commercial one.

three tiers · support included
Onboarding

Generate the metamodel first. Look at the tree before you commit to it.

There is nothing to install on the IBM i and nothing to provision. If the integration needs help, that is what the support in the license is for; if it needs more than help, we do that work too, as training, architectural design or coaching alongside your team. Write to products@strumenta.com.

Why license this rather than generate your own vocabulary

This is the real alternative, and it is a serious one, so here it is stated fairly. Your IBM i already knows every command it has. IBM gives you the means to ask it: the QCDRCMDI Retrieve Command Information API returns what DSPCMD displays, QSYS2.COMMAND_INFO exposes the same ground over SQL, and GENCMDDOC generates documentation from a command definition. A determined team can dump its own vocabulary and build a parser around it.

What that gets you is the vocabulary of one machine at one release — which, if you are a customer analyzing only your own estate, may genuinely be enough. It is a legitimate route and we would rather you knew it existed.

What it does not get you is the grammar around the vocabulary. Soft keywords are the hard part: several thousand command and parameter names compiled in without breaking the user identifiers that collide with them. The generic fallback has to be designed in from the start, not bolted on after the first ISV command fails. Case-insensitive symbol resolution for variables, labels and subroutines across files is a second body of work on top. And the vocabulary is a snapshot that has to be re-extracted and cross-checked at every release, forever.

There is also the question of whether you are in this business. If you are building a tool you will sell, you need a maintained front end with someone accountable for it, not a one-off extraction from a machine that will be upgraded next year. If you are analyzing your own estate once, the calculation is different, and we will say so.

What no extraction gives you at all is the rest of the box. A CL model that cannot be walked alongside the RPG, the DDS and the DB2 SQL answers the easy half of every question. That is the next section, and it is the reason most people licensing this engine are licensing more than one.

The method behind our estimates has a name, and it applies to language work generally: read the Chisel Method, or Parsers and transpilers for the three ways into this work.

THE REST OF THE BOX

Nobody analyzes CL on its own.

CL is analyzed together with the RPG it calls, the DDS files it overrides and the DB2 SQL it runs. Four languages, one question, and usually one deadline.

Migrating RPG without migrating the CL that calls it, overrides its files and sets its library list produces Java that cannot run. It is the most common way an IBM i modernization goes wrong, and it is entirely avoidable.

Every Strumenta engine is built on Starlasu, so a CL tree, an RPG tree, a DDS tree and a SQL tree have the same shape, the same traversal model and the same API. One tool walks all four. That is the thing you cannot assemble by gluing unrelated open-source parsers together — four models, four idioms and four sets of edge cases to reconcile before you have written a line of analysis.

Every engine also supports LionWeb, so the models interchange with LionWeb-compliant tooling instead of being locked inside one process — and every engine is available through bindings from Java, Kotlin, Python, TypeScript and C#. The host application picks the language; the parser does not.

Engines on the same box

one model, one traversal

RPG & DDS engine →
The programs your CL calls, and the files it overrides.

SQL engine →
What RUNSQL and the embedded statements actually do.

COBOL engine →
For the estates that run both platforms.

The whole catalog →
15+ engines, one AST framework underneath.

What to discuss before you license

Five things we would rather tell you now than have you find in week three.

  • The vocabulary is a snapshot. Commands from an IBM i release newer than the vocabulary, or from a licensed program we do not model, parse generically rather than typed. Nothing breaks — but if a command matters to your analysis, you want it typed, and that is a conversation about which release you are on.
  • Your own commands stay generic without their definitions. An in-house or ISV command parses into a faithful node with all its parameters, but nothing in CL source says what those parameters mean. If you can export the command-definition source or the *CMD objects, generic nodes can become typed ones.
  • Overrides and the library list are run-time behavior. The engine records OVRDBF, OVRPRTF and every LIB/OBJ reference with its position. Deciding what a called program actually opened is analysis on top, and it needs the search order from you.
  • Command strings are opaque. QCMDEXC, RUNSQL and QSH take character expressions assembled at run time. The engine recovers the call and the arguments; it cannot recover a command that does not exist until execution. How many of these you have is a real question about your estate.
  • It runs on a JVM, in your environment. If your toolchain is Python, TypeScript or C#, you use the bindings or the serialized tree rather than the jar in-process — which works, and is worth designing for deliberately rather than discovering late.

What we will ask you

  1. OPM CL, ILE CL, or both — and roughly in what proportion?
  2. Which IBM i release is the vocabulary you need, and are there licensed programs whose commands you rely on?
  3. Do you use third-party or in-house commands, and can you supply their definitions?
  4. How much of your logic is assembled dynamically and executed through QCMDEXC?
  5. Do you need file-override and library-list behavior modeled, or only recorded?
  6. Is there an RPG codebase alongside it, and do you need one model spanning both?
  7. Are you building analysis, documentation, an editor, or a migration of the CL itself?
  8. Standard, Distribution or Service — will this be embedded in something you ship?

Straight answers

What happens to a command you do not know?

It parses. The command rule ends in a genericStatement fallback, so an unrecognized command becomes a node holding its qualifying library, its name, its labels and every parameter it was given, in both positional and keyword form, as structured expressions. It never breaks the parse and it never swallows the rest of the member. What you do not get is the parameter semantics — that needs the command definition, which is not in CL source.

Is this ILE CL only, or does OPM CL work?

We explicitly support the ILE CL flavor, and it also works with the Original Program Model flavor. That is the honest form of the answer: ILE is where the testing and the intent are, and OPM members parse. The ILE-only constructs — SUBR, CALLSUBR, ENDSUBR, DOWHILE, SELECT, the integer and pointer types — are modeled, and a member that uses them will not compile under CRTCLPGM anyway.

Can we see the shape of the AST before we buy?

Yes, and without a license. The command-line tool’s metamodel subcommand generates the EMF/Ecore metamodel of the whole CL AST — every command class, every type, every field — and it does not require a license file. Parsing does. We think the shape of the model is the thing you are actually evaluating, so it should not be behind a contract.

Is CL just IBM i’s version of JCL?

No, and the comparison causes real estimation errors. CL is a procedural programming language with variables, typed declarations, conditionals, subroutines, labels and structured error handling, compiled into program objects. JCL is a job-card deck for z/OS. IBM i is a midrange platform, not a mainframe, and the two have essentially nothing in common beyond both being IBM.

Can we use it from Python, TypeScript or C#?

Yes. The engine is built on Starlasu and is available through bindings from Java, Kotlin, Python, TypeScript and C#, and it supports LionWeb, so the model interchanges with LionWeb-compliant tooling. If you would rather not embed anything, the command-line tool serializes the tree to JSON or XML and any language can read that.

Send us the list, not the source command names are enough to start

Tell us which commands you use, and we will tell you which ones come back typed.

A list of the command names in your QCLSRC — no source code required — is enough for us to say how much of your estate lands as typed nodes, how much lands as generic ones, and whether supplying your command definitions would be worth the trouble.

Scroll to Top