CVTFREE-APP — RPG Fixed→Free Conversion Corpus

SteelFrame X modernization reference  ·  ← back to Operation Manuals  ·  Sign On

CVTFREE-APP is not a business application — it is a modernization corpus: a set of realistic, hand-written fixed-form RPG IV programs (an order-processing mini-app — ORDCALC, ORDVAL, ORDPOST, ORDRPT) that are mechanically converted to fully-free RPG by the CVTRPGFREE tool and then proven equivalent by a compile-both-and-diff oracle. Its purpose is to exercise the SteelFrame X conversion surface (ibmi/cvtfree.js) against source written the way a real shop writes it — column-exact C/D/F specs, KLIST/KFLD, CASxx, DIV+MVR, half-adjust arithmetic, date-duration math, string BIFs — and to confirm the tool's honest refusals on the constructs that cannot be expressed in free form. This manual documents it as what it is: a fixed→free conversion reference. It is grounded entirely in the committed source (cvtfree-app/run_corpus.mjs — the corpus, conversion driver and oracle in one file — and the engine surface it drives, ibmi/cvtfree.js).

Read this first. There is no green screen, no menu, and no operable business workflow here. "Running the app" means running the conversion + equivalence corpus (node cvtfree-app/run_corpus.mjs), which builds the fixed-form programs, converts them, compiles both forms, runs both, and diffs the results. Where this manual describes "operations", it means operating that verification, not servicing orders.
Contents

A. Overview & Architecture ↑ top

A.1 What cvtfree modernization is

CVTRPGFREE is a source-to-source modernization step: it reads a program written in classic column-exact fixed-form RPG (the format where factor-1, opcode, factor-2 and the result field each live in fixed card columns) and rewrites it into fully-free RPG (the modern, column-independent syntax with dcl-s, dcl-pi, eval, structured for/dow, and ;-terminated statements). It is the SteelFrame X analogue of IBM's own fixed-to-free conversion: the same program, the same behaviour, expressed in the maintainable modern form.

CVTFREE-APP exists to answer one question rigorously: does the conversion preserve behaviour? A synthetic snippet can hide corner cases; this corpus instead uses a realistic order-processing mini-app — the kind of program a shop actually ships — so the conversion is stressed across the opcode families that matter: half-adjust arithmetic, DIV+MVR remainder pairing, MOVE/MOVEL padding semantics, the string BIFs (CAT/SUBST/XLATE/SCAN/CHECK), array LOOKUP, keyed I/O via KLIST/KFLD, CASxx dispatch, DO loops, date-duration math, TESTN, and TIME. It equally documents where the tool must refuseGOTO/TAG, program-described O/I specs, cycle/level indicators — because free RPG cannot express them, and a silent mis-conversion there would be far worse than an honest stop.

A.2 The compile-both-and-diff oracle: proof by execution

The corpus does not trust the converter's output by inspection. Every program is validated by an emulator-as-oracle loop — the SteelFrame X emulator itself is the reference implementation, and the two source forms must produce byte-identical results when actually run:

On top of the diff, several checks assert hand-computed expected values (e.g. the next order number, a delinquency-tier LOOKUP result, the exact MOVEL vs MOVE overlay bytes), so the oracle proves not just "the two agree" but "the two agree and the answer is the mathematically correct one". Where a program is non-deterministic (the TIME wall-clock read in ORDRPT) or self-referential (the file-appending ORDPOST), the harness masks the volatile field or re-seeds identical file state between the two runs, so the comparison stays fair.

A.3 Component & flow

  FIXED-FORM SOURCE          CONVERT                 FREE-FORM SOURCE
  -----------------          -------                 ----------------
  ORDLIB/QRPGLESRC(ORDCALC)  CVTRPGFREE  --------->  ORDLIB/QRPGLESRC(ORDCALCF)
  ORDLIB/QRPGLESRC(ORDVAL)     SRCMBR -> TOMBR       ORDLIB/QRPGLESRC(ORDVALF)
  ORDLIB/QRPGLESRC(ORDPOST)    (ibmi/cvtfree.js      ORDLIB/QRPGLESRC(ORDPOSTF)
  ORDLIB/QRPGLESRC(ORDRPT)      convert())           ORDLIB/QRPGLESRC(ORDRPTF)
         |                                                   |
         | CRTBNDRPG + CALL                 CRTBNDRPG + CALL |
         v                                                   v
     fixed DSPLY  ----------  DIFF (byte-for-byte)  ----  free DSPLY
     file after-image  <-----  must be identical  ----->  file after-image
                                    |
                                    v
                          ok(name, cond) -> PASS/FAIL  (17 checks, +findings)

  REFUSAL SET (must NOT convert): GOTO/TAG, program-described O-spec, L1 cycle indicator
  DATA: ORDLIB/OCUSTP (customer master, keyed) + ORDLIB/OORDP (order detail, 2-part key)

A single check flows: the driver emits the fixed-form member as an array of column-exact lines → put() writes it to QRPGLESRC (guarding the 112-byte record length) → oracle() runs CVTRPGFREE, compiles both members, calls both, and returns both outputs → same() / a hand-computed assertion decides PASS or FAIL → the result is pushed and printed. When a real conversion defect surfaces, it is written up in LANG-QA-FINDINGS.md as a numbered finding.

A.4 Object inventory

ObjectTypeRole
run_corpus.mjsDriver (Node ESM)The whole corpus: emits programs, drives conversion, runs the oracle, prints the scoreboard.
ORDLIBLibraryThe scratch library created for the run (CRTLIB).
QRPGLESRCSource PF (RCDLEN 112)Holds every fixed-form member and its converted *F twin.
QDDSSRCSource PF (RCDLEN 112)Holds the two DDS PF members.
OCUSTPPF (DDS, keyed)Customer master — single key CUSTNO.
OORDPPF (DDS, keyed)Order detail — two-part key CUSTNO+ORDNO.
ORDCALCFixed-form RPGOrder-economics callee (*ENTRY PLIST): tax/discount/cases, dates, memo.
ORDVALFixed-form RPGOrder-edit validation: TESTN/COMP/CASxx/string BIFs.
ORDPOSTFixed-form RPGOrder posting: keyed I/O, LOOKUP, DO, CALL, WRITE.
ORDRPTFixed-form RPGCustomer report: MOVE/MOVEL padding, TIME, full-file scan.
*F twinsFree-form RPGORDCALCF/ORDVALF/ORDPOSTF/ORDRPTF — converter output.
QASUBST / QA02 / QAGOTO…Fixed-form RPG (probes)Minimal repro programs for the two findings and the three refusals.
ibmi/cvtfree.jsEngine (READ-only here)The converter under test — convert(), driven via CVTRPGFREE.
LANG-QA-FINDINGS.mdFindings logWhere confirmed conversion defects are appended.

The corpus is 4 realistic programs (+ their converted twins) over 2 keyed DDS files, plus a handful of minimal probe programs, driven by 1 Node file that both emits the fixed-form source and verifies the conversion. Sections D and F expand each.

B. "Online" — How It Is Run ↑ top

B.1 The conversion driver (there is no screen)

Honest statement: CVTFREE-APP has no 5250 screen, no menu, and no interactive transaction. It is a source-transform corpus. The "operator experience" is running one Node command:

-- run the whole conversion + equivalence corpus
node cvtfree-app/run_corpus.mjs

That single command boots a fresh SteelFrame X object filesystem in a private scratch root (cvtfree-app/.data-corpus, created and wiped at start), initialises the emulator's object store, jobs, VSAM/PF layer and SQL engine, starts one job (CVTAPPQA under QPGMR), then walks the corpus. Everything it does is what a developer would otherwise type at a workbench: create a library and source files, key in the fixed-form members, issue CVTRPGFREE, compile with CRTBNDRPG, and CALL the programs. The driver simply scripts that developer session and adds the diff.

The developer action it scriptsWhat the driver issues
Create the working library & put it on the library listCRTLIB LIB(ORDLIB), CHGCURLIB, ADDLIBLE
Create the source physical filesCRTSRCPF FILE(ORDLIB/QRPGLESRC) RCDLEN(112), same for QDDSSRC
Create the two keyed data files from DDSCRTPF FILE(ORDLIB/OCUSTP)…, CRTPF FILE(ORDLIB/OORDP)…
Convert a fixed-form member to freeCVTRPGFREE SRCFILE(ORDLIB/QRPGLESRC) SRCMBR(…) TOMBR(…)
Compile a member (fixed or converted)CRTBNDRPG PGM(ORDLIB/…) SRCFILE(ORDLIB/QRPGLESRC) SRCMBR(…)
Run a program and read its outputcompile.callPgm(job, ORDLIB, pgm) → the DSPLY lines

B.2 The CVTRPGFREE command

Conversion is driven through the real CL command so the corpus exercises the workbench path, not just a bare call to cvt.convert(). The command shape used throughout is:

CVTRPGFREE SRCFILE(lib/QRPGLESRC) SRCMBR(ORDCALC) TOMBR(ORDCALCF)
CVTRPGFREE SRCFILE(lib/QRPGLESRC) SRCMBR(QAGOTO) TOMBR(QAGOTOF) TODO(*YES)
ParameterMeaning
SRCFILEThe source PF holding the fixed-form member (here always ORDLIB/QRPGLESRC).
SRCMBRThe fixed-form member to convert.
TOMBRThe member to write the fully-free result into (the corpus uses a *F suffix).
TODO(*YES)Force-write even when the source has non-convertible constructs, annotating each with a // TODO(CVTFREE) comment for human inspection. Default is *NO — nothing is written when unresolved TODOs exist.

The command returns a result object the driver inspects: converted (boolean), lines (the emitted free-form source) and todos (the reasons a conversion was refused or flagged). A clean conversion is converted === true with an empty todos; a refusal is converted === false with one or more todos[].reason strings. Under the covers the command reads the member and calls ibmi/cvtfree.js's convert(source, opts).

B.3 The self-check report (the "screen" you actually watch)

What an operator watches is not a green screen but the driver's PASS/FAIL scoreboard on stdout. Each check calls ok(name, cond, detail), printing PASS or **FAIL and a short detail; at the end it prints the tally and exits non-zero if any check failed.

PASS P1.0 ORDCALC: CVTRPGFREE converts (PLIST/PARM->dcl-pi, half-adjust, DIV+MVR, ...) PASS P1.1 ORDCALC + ORDCALCF both compile clean PASS P1.2 dcl-pi *n synthesized with all 10 typed parameters PASS P2.0 ORDVAL: CVTRPGFREE converts + EQUIVALENCE (TESTN/COMP/CASxx/SCAN/CHECK/...) PASS P3.0 ORDPOST: converts + EQUIVALENCE (KLIST/KFLD full+partial, LOOKUP, DO, CALL+PARM) PASS P4.0 ORDRPT: converts + EQUIVALENCE (MOVE/MOVEL padding, TIME masked, CAT, full scan) PASS P5.0 GOTO/TAG: honest refusal (not a silent mis-conversion) ... cvtfree-app corpus: 17/17

The two logged findings (P0.1, P0.2) are expected-failure repros: they PASS when the known conversion defect still reproduces (the corpus routes around each so the rest can proceed) and would only "fail" if the underlying engine bug were fixed — see F.4.

C. Batch — the Conversion + Equivalence Run ↑ top

The corpus is the "batch job". A run is a single deterministic pass (the volatile TIME field is masked, so a re-run gives the same scoreboard). It proceeds in a fixed order: the environment is built once, then each program is emitted, converted, compiled twice, run twice and diffed, then the three refusals are asserted. All work happens in library ORDLIB.

C.1 The corpus build & run sequence

  1. Boot the emulator. Wipe/create the scratch data root, init objfs, jobs, VSAM, the runtime build, and SQL; start job CVTAPPQA.
  2. Create the environment. CRTLIB ORDLIB, the two source PFs (QRPGLESRC/QDDSSRC at RCDLEN(112)), and the two keyed data files (OCUSTP, OORDP) from DDS; seed three customers.
  3. Probe CVTFREE-QA-01 (P0.1) — the SUBST-of-a-literal repro, run first so the rest of the corpus can be written to route around it.
  4. Program 1 — ORDCALC (P1.0–P1.2): convert, compile both, assert the *ENTRY PLISTdcl-pi synthesis. Run indirectly via ORDPOST.
  5. Program 2 — ORDVAL (P2.0–P2.1): convert + run + diff for a valid order and a bad-input order.
  6. Probe CVTFREE-QA-02 (P0.2) — the long-continuation-line over-RCDLEN repro.
  7. Program 3 — ORDPOST (P3.0–P3.2): convert + run + diff DSPLY and the keyed file after-image, with identical re-seeding between the two runs.
  8. Program 4 — ORDRPT (P4.0–P4.1): convert + run + diff, with TIME masked.
  9. Program 5 — refusals (P5.0–P5.4): assert GOTO/TAG, program-described O-spec, and L1 cycle indicator all refuse, and that the CL command honours TODO(*NO) (writes nothing) vs TODO(*YES) (writes with markers).
  10. Append findings & tally. Any confirmed defect is appended to LANG-QA-FINDINGS.md; print cvtfree-app corpus: N/total and exit 0 only if all passed.

C.2 Per-program equivalence checks

CheckProgramWhat it provesMethod
P0.1QASUBSTFixed SUBST of a quoted literal converts, but the converted free-form fails to compile (CVTFREE-QA-01).convert + attempt compile; expect the build error.
P1.0–1.2ORDCALC*ENTRY PLISTdcl-pi *n with all 10 typed parms; both forms compile clean.convert (skipRun), inspect emitted dcl-pi lines, compile both.
P2.0ORDVALValid order → OK 0; fixed and free DSPLY identical.convert + run both + byte diff, plus a hand-computed expected string.
P2.1ORDVALBBad input (all-blank order no. + negative amount) → ERRORS FOUND 2; identical.same oracle, other branch of TESTN/COMP.
P0.2QA02A long multi-line continuation converts to one free line over RCDLEN, silently truncated (CVTFREE-QA-02).convert; detect an emitted line > 112 chars; expect the build error.
P3.0–3.2ORDPOSTKeyed I/O program: DSPLY identical, NEXTNO=3 & TIER=2 hand-computed, and the file after-image identical byte for byte (3 orders).convert + run both against re-seeded identical file state; diff DSPLY and hex-sorted records.
P4.0–4.1ORDRPTDSPLY identical with TIME masked; MOVEL gives XY3456, MOVE gives AB  XY, DONE 3.convert + run both; mask the timestamp line; byte diff + hand-computed overlays.
P5.0–5.4QAGOTO/OSPEC/CYCLEThe three non-convertibles refuse loudly; the CL command respects TODO(*NO)/*YES.call convert(), assert !ok and the expected todos[].reason.

C.3 Ordering & dependencies

D. Data Files (the corpus DDS) ↑ top

The corpus uses two DDS-described keyed physical files in ORDLIB, grounded in the DDS members emitted into QDDSSRC. They exist to give ORDPOST real keyed I/O (CHAIN, SETLL/READE, WRITE) and ORDRPT a real full-file scan to convert — not to model an order-management business. Both are declared UNIQUE. Money fields are packed decimal; the order number is zoned.

OCUSTP — Customer master (record OCUSTR; key CUSTNO, UNIQUE)

FieldDDS typeMeaning
CUSTNO6ACustomer number (the key).
CUSTNAME20ACustomer name (used by ORDRPT's report line).
CREDLIM9P 2Credit limit (packed, 2 dp) — ORDRPT totals it.

Seeded rows: ('C0001','ACME CORP',50000.00), ('C0002','GLOBEX LLC',15000.00), ('C0003','INITECH',2000.00).

OORDP — Order detail (record OORDR; 2-part key CUSTNO+ORDNO, UNIQUE)

FieldDDS typeMeaning
CUSTNO6AOwning customer — first key part (partial-key scan target).
ORDNO4S 0Order number (zoned) — second key part.
ORDDATE10AOrder date (ISO YYYY-MM-DD).
ORDAMT9P 2Order amount (packed, 2 dp).
ORDSTAT1AOrder status (ORDPOST writes 'P').

Seeded before each ORDPOST run: ('C0002',1,'2026-01-05',300.00,'P') and ('C0002',2,'2026-02-10',450.00,'P') — the two-order baseline the next-number logic must see identically on both the fixed and free run.

How the corpus programs use these files

Both files' record images are part of the equivalence proof: for ORDPOST the oracle reads every OORDP record after each run, hex-encodes and sorts them, and asserts the fixed-form and free-form after-images are byte-for-byte identical (3 records — the 2 seeded + 1 written). A converter that quietly changed a packed/zoned field's encoding would be caught here, not just in DSPLY text.

E. Operations Runbook ↑ top

E.1 Running the corpus

  1. From the repository root, run the driver: node cvtfree-app/run_corpus.mjs.
  2. No parameters, no setup: the driver creates and wipes its own scratch data root (cvtfree-app/.data-corpus) each run, so it is fully self-contained and repeatable.
  3. Watch the stdout scoreboard — one PASS/**FAIL line per check — and the final cvtfree-app corpus: N/total tally.
  4. The process exits 0 only if every check passed; a non-zero exit means at least one check failed (a CI gate can key on that).

Pre-checks: run from a checkout where ibmi/cvtfree.js, ibmi/compile.js, server/runtimebuild.js etc. are present (the driver imports them by relative path from its own directory); a Node with ESM support.

E.2 The 17/17 verification — what a healthy run asserts

A healthy run is 17 of 17 checks green (the ok() guard for an unexpected exception is the 18th call and stays dormant on a clean run). The 17 are:

#CheckGreen means
P0.1CVTFREE-QA-01 reproThe SUBST-of-a-literal defect still reproduces (converts, then fails to compile) — and the corpus routed around it.
P1.0ORDCALC convertsPLIST/PARM, half-adjust, DIV+MVR, XFOOT/MOVEA, ADDDUR/EXTRCT, CAT/SUBST/XLATE all convert with no TODOs.
P1.1ORDCALC + ORDCALCF compileBoth the fixed and converted members build clean.
P1.2dcl-pi synthesizedThe converter emits dcl-pi *n; with all 10 typed parameters.
P2.0ORDVAL equivalenceValid order: identical DSPLY, result OK 0.
P2.1ORDVAL bad-inputAll-blank order + negative amount: identical DSPLY, ERRORS FOUND 2.
P0.2CVTFREE-QA-02 reproThe long-line/RCDLEN defect still reproduces — and ORDPOST routed around it.
P3.0ORDPOST equivalenceKeyed I/O program: identical DSPLY across the re-seeded runs.
P3.1ORDPOST hand-computedNEXTNO=3 (over seeded 1,2) and TIER=2 (LOOKUP nearest-high).
P3.2ORDPOST after-imageKeyed-file records byte-identical (3 orders) between fixed and free.
P4.0ORDRPT equivalenceIdentical DSPLY with the TIME line masked.
P4.1ORDRPT hand-computedMOVELXY3456, MOVEAB  XY, DONE 3.
P5.0GOTO/TAG refusedConversion refuses (no silent mis-conversion).
P5.1O-spec refusedProgram-described printer O-spec refuses (not expressible free-form).
P5.2L1 cycle refusedLevel/cycle indicator refuses.
P5.3TODO(*NO) defaultUnconvertible source is not written by default.
P5.4TODO(*YES) writesForce-write produces a member carrying // TODO(CVTFREE) markers.

E.3 Failure & findings

Because this is a QA corpus, a "failure" is a signal about the converter (or, for P0.x, a signal that a known bug has changed), never about a business transaction.

SituationMeaningAction
A P1–P4 check FAILsThe converter mis-translated a real opcode family — the free-form run diverged from the fixed-form run, or a hand-computed value is wrong.Inspect the detail (it prints both DSPLY strings / the divergent records), reproduce minimally, and write it up in LANG-QA-FINDINGS.md. Engine fix is out of this corpus's scope.
A P5 refusal FAILsThe converter silently accepted a non-convertible construct (worse than refusing).Treat as high severity: a silent mis-conversion of GOTO/O-spec/cycle can produce wrong free source. Log a finding.
P0.1 or P0.2 FAILs ("unexpectedly succeeded")The underlying engine defect the repro pins was fixed.Good news: retire the workaround in the affected corpus program and tighten the check to assert correct behaviour.
"unexpected error" FAILsAn exception escaped the corpus body (boot/import/compile crash).Read the stack in the detail; usually an environment/import path problem, not a conversion result.
Confirmed defects are appended to LANG-QA-FINDINGS.md with a header comment and the finding's severity, status, minimal repro, actual vs expected, engine locus, and the workaround the corpus used. The corpus deliberately proceeds past a confirmed pre-existing bug (by routing the affected program around it) so one known defect does not mask the rest of the equivalence coverage.

F. Developer Reference ↑ top

The complete corpus surface, from cvtfree-app/run_corpus.mjs. Every fixed-form program is built from column-exact line builders (cC for C-specs, cX for extended-factor-2 continuations, dS/dDS/dSub for D-specs) so the source lands in the exact card columns the parser expects. All objects live in library ORDLIB.

F.1 The corpus programs

ORDCALC — order-economics callee (*ENTRY PLIST, no DSPLY)
A called module taking 10 parameters (4 in: amount, qty, pack, ISO date; 6 out: tax, discount, whole cases, leftover units, due date, memo). Computes tax as amount × 0.0825 half-adjusted; a discount tier via CASGE (≥10000→10%, ≥1000→5%, else 0%); cases and remainder via DIV+MVR; an XFOOT over a 3-element line-item array with MOVEA staging; a due date via ADDDUR 30:*D with EXTRCT *M; and a formatted memo via CAT/SUBST/XLATE. Verified indirectly through ORDPOST's CALL. Note: its SUBST overlay reads from the WKOVL work field (INZ 'XX-YY-ZZ'), not a literal — a deliberate route around CVTFREE-QA-01.
ORDVAL — order-edit validation (two data cases: ORDVAL, ORDVALB)
Classic "edit a line" program: TESTN digit-classifies the order-number field (resulting indicators 60/61/62); two COMP range checks (negative amount, over-limit); a CASGT/CAS dispatch to HASERR/NOERR subroutines; SCAN/CHECK/CHECKR over a free-text description; and a CAT building the summary line. ERRCNT is staged into a char field via MOVE before the CAT (a %char() inline in a fixed CAT operand is refused by the base compiler itself — the idiomatic fixed-form pattern). ORDVAL runs the valid path (→OK 0); ORDVALB runs the all-blank/negative path (→ERRORS FOUND 2).
ORDPOST — order posting (keyed I/O, the fullest program)
Against the two keyed files: a full-key CHAIN on OCUSTP (KLIST CKEY/KFLD WCUST); a partial-key forward scan on OORDP (SETLL+READE looping *IN92=*OFF, KLIST PKEY/KFLD WCUST2) to find the max order number; a LOOKUP on an ASCEND compile-time array (TIERC = H/L/M via **CTDATA) with nearest-high; a DO 1..3 loop building a running total; a CALL 'ORDCALC' with 10 PARMs; and a WRITE OORDR. Its status memo is built across eight short EVALs (not one long continuation) — a deliberate route around CVTFREE-QA-02.
ORDRPT — customer report (MOVE/MOVEL padding, TIME)
Read-only full-file scan (SETLL *LOVAL+READ/DOWEQ *OFF) over OCUSTP, counting rows and totalling CREDLIM. Demonstrates MOVE/MOVEL faithful padding: MOVEL 'XY' into '123456' left-overlays to XY3456; MOVE 'XY' into 'AB' right-overlays (unpadded) to AB  XY. Uses TIME (6,0) for a run timestamp (masked in the diff) and CAT for header/footer.
Probe programs — QASUBST (CVTFREE-QA-01), QA02 (CVTFREE-QA-02), QAGOTO, the O-spec program and the L1-cycle program — are minimal, single-purpose members that isolate one behaviour each, kept separate so one refusal or repro never masks another.

F.2 Opcode families demonstrated as convertible

These are the fixed-form idioms the corpus proves CVTRPGFREE converts correctly (fixed and free runs byte-identical), with the program that exercises each:

FamilyFixed-form opcodesFree-form targetWhere
Linkage parameters*ENTRY PLIST + PARMdcl-pi *n with typed parmsORDCALC
Half-adjust arithmeticZ-ADD, MULT(H)eval with roundingORDCALC (tax, discount %)
Divide + remainderDIV then MVR%rem pairingORDCALC (cases/leftover)
Array footing / stagingXFOOT, MOVEA%xfoot, %subarrORDCALC
Date durationsADDDUR, EXTRCT+ %days(), %subdtORDCALC (due date, month)
String build/overlayCAT, SUBST, XLATEconcatenation, %subst, %xlateORDCALC, ORDVAL, ORDRPT
Digit classificationTESTN (ind 60/61/62)%check-style testsORDVAL
Compare + indicatorsCOMPconditioned evalORDVAL
Case dispatchCASGE/CASGT/CAS+ENDselect/whenORDCALC, ORDVAL
Scan / verifySCAN, CHECK, CHECKR%scan, %check, %checkrORDVAL
Keyed I/OKLIST/KFLD, CHAIN, SETLL, READE, WRITE%kds-style keyed opsORDPOST
Array lookupLOOKUP (ASCEND, nearest-high)%lookup familyORDPOST
Counted loopDOENDDO, DOWEQfor, dowORDPOST, ORDRPT
Program callCALL + PARMcallp/prototyped callORDPOST→ORDCALC
Move / paddingMOVE, MOVELeval with faithful right/left overlayORDRPT
TimeTIME (6,0)%time/%timestampORDRPT (masked)

F.3 Honest-refusal boundaries

Some fixed-form constructs have no faithful free-form equivalent. The correct behaviour there is to refuse loudly (return converted:false with a todos[].reason), never to emit silently-wrong free source. The corpus asserts each refusal with a minimal program:

GOTO / TAG (P5.0)
Fully-free RPG has no GOTO and no TAG label; arbitrary jumps cannot be re-expressed as structured control flow mechanically. The converter reports a GOTO reason and refuses. (GOTO/TAG are among the converter's TODO_OPS — the opcodes it will not silently translate.)
Program-described O-spec / I-spec (P5.1)
A program-described printer file with O-specs (column-positioned output fields, e.g. QSYSPRT O F 132 PRINTER + O field lines) is not expressible in free-form, which requires externally-described (DDS) files for record I/O. The converter refuses with an "O-spec" reason.
Level / cycle indicators (P5.2)
An RPG-cycle level indicator such as L1 in a C-spec's conditioning columns depends on the program cycle, which fully-free RPG does not run. The converter refuses with a "level/SR indicator" reason.

The write policy that makes refusal safe: by default (TODO(*NO)) a refused conversion writes nothing — P5.3 asserts the destination member does not even exist after a refused run. With TODO(*YES) (P5.4) the tool writes the best-effort free source but annotates every unconvertible statement with a // TODO(CVTFREE) <reason>: <original> comment, so a human can finish the conversion by hand and can never mistake a partial result for a complete one.

F.4 The two logged findings

Building the corpus surfaced two real conversion defects, each reproduced twice (standalone probe + a corpus program that hit it independently), then routed around so the equivalence coverage could proceed. Both are logged to LANG-QA-FINDINGS.md.

CVTFREE-QA-01 — SUBST of a quoted literal converts to an uncompilable %subst-of-a-literal
A fixed-form SUBST 'XX-YY-ZZ':1 TGT converts to %subst(TGT:1:5) = %subst('XX-YY-ZZ':1:5);, which the free-form RPG-to-COBOL backend cannot compile (it emits a reference-modification subscript on a quoted-literal MOVE source, a syntax error). Severity: high (a silently-uncompilable program from an extremely common template-overlay idiom). Confirmed twice. The shared root cause is in the RPG-to-COBOL backend's %subst-of- a-literal codegen (outside cvtfree.js), but the converter is what walks a user into it — it has no reason to route a literal source through %subst() at all. Corpus workaround: ORDCALC stages the literal into the WKOVL work field and SUBSTs from that field.
CVTFREE-QA-02 — a long continuation statement converts to one unwrapped free line over RCDLEN
A legal multi-line extended-factor-2 statement (each physical line well under 112) converts to a single free-form line longer than the destination member's record length. Writing it back silently truncates at 112 bytes — often mid string-literal — corrupting the converted program. Severity: high (silent truncation → fails to compile, or worse, silently wrong). Confirmed twice. The write-side truncation in objfs.writeSource() is correct behaviour for a physical source member; the gap is cvtfree.js not re-wrapping its own long output (or at least warning) to fit. Corpus workaround: ORDPOST builds its status memo across eight short EVALs instead of one long continuation.
Note the converter does already guard the general case: when a generated line exceeds the destination record length with no safe wrap point, it emits a // TODO(CVTFREE) line exceeds the destination record length marker and reports a TODO — QA-02 documents the specific path (a joined continuation) where the emitted line can still slip past that guard.

F.5 The oracle helpers (how a check is built)

put(lib, file, mbr, srctype, lines)
Writes a source member, but first fails loudly if any line exceeds RCDLEN(112) — an own-code guard so a too-long line is caught here, not as a mysterious downstream compile error (the lesson behind CVTFREE-QA-02).
oracle(lib, srcMbr, newMbr, {skipRun})
The equivalence engine: runs the real CVTRPGFREE CL command (SRCMBRTOMBR); if refused, returns an err with the join of todos[].reason; otherwise CRTBNDRPGs both members, calls both, and returns {fixedOut, newOut}. skipRun converts+returns without running (used for the no-DSPLY ORDCALC).
same(o) / line1(o)
same is the pass predicate — no error, non-empty fixed output, and JSON.stringify(fixedOut) === JSON.stringify(newOut). line1 formats both outputs (or the error) for the check's detail string.
ok(name, cond, detail)
Records and prints one check ( PASS/**FAIL), truncating detail to 160 chars; the final tally and exit code are driven off the collected results.
logFinding(id, title, body)
Collects a numbered finding; all findings are appended to LANG-QA-FINDINGS.md at the end of the run under a header comment.
Special handling. ORDPOST is not idempotent, so its check clears + re-seeds OORDP to the same two-order baseline before each run and diffs both DSPLY and the hex-sorted record after-images. ORDRPT's TIME is a wall-clock read, so its first DSPLY line is masked (RUN<masked>) before comparison. These keep the byte-for-byte diff fair for non-deterministic and self-referential programs.

G. Glossary ↑ top

Fixed-form RPG
Classic column-exact RPG IV, where factor-1, opcode, factor-2 and the result field each occupy fixed card columns (the format the corpus programs are written in, and the input to conversion).
Fully-free RPG
Modern column-independent RPG syntax (dcl-s, dcl-pi, eval, for/dow, ;-terminated statements) — the conversion target.
CVTRPGFREE
The CL command that converts a fixed-form source member (SRCMBR) to a fully-free member (TOMBR); driven throughout this corpus. Backed by ibmi/cvtfree.js's convert().
Conversion corpus
A curated set of realistic programs used to exercise and validate a converter — here, an order-processing mini-app used purely as conversion test material, not as an operable system.
Compile-both-and-diff oracle
The validation method: compile and run BOTH the fixed and converted forms, then diff their outputs (and file after-images) byte-for-byte. The emulator itself is the reference implementation.
Equivalence
The property being proved: the converted program produces byte-identical results to the original when actually run.
Honest refusal
The converter declining (converted:false + a reason) on a construct with no faithful free-form equivalent, rather than emitting silently-wrong source. Applies to GOTO/TAG, program-described O/I specs, and cycle/level indicators.
TODO(CVTFREE)
The comment marker the converter leaves on each unconvertible statement when TODO(*YES) forces a best-effort write, so a human can finish the conversion by hand.
Half-adjust
RPG rounding (the (H) extender, e.g. MULT(H)): round the result to its declared decimal positions rather than truncate.
DIV + MVR
The fixed-form idiom for integer division with remainder: DIV produces the quotient, MVR immediately after retrieves the remainder.
KLIST / KFLD
A named composite key list (KLIST) and its constituent key fields (KFLD) used for keyed file operations like CHAIN/SETLL/READE.
MOVE / MOVEL
Right-adjusted (MOVE) and left-adjusted (MOVEL) character overlay with faithful unpadded semantics — a converter must preserve exactly which bytes are overwritten.
RCDLEN
The record length of a source physical file (here 112, the classic QRPGLESRC default). A converted line longer than RCDLEN is silently truncated on write — the crux of CVTFREE-QA-02.
Extended factor-2
The free-form-like expression area of certain fixed-form opcodes (e.g. EVAL), which may span several continuation lines; the converter joins them, which is where over-length output can arise.
DSPLY
The RPG operation that writes a message line; the corpus captures each program's DSPLY output as the primary equivalence signal.
After-image
The byte contents of a physical file's records after a program runs; ORDPOST's equivalence check compares fixed vs free after-images record-for-record.