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).
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.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 refuse — GOTO/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.
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:
CRTBNDRPG the fixed-form member, call
it, capture its DSPLY output (and, for file-writing programs, the after-image of the
physical file).CVTRPGFREE CL command (the workbench path, not a
direct function call) to produce a fully-free member.CRTBNDRPG the converted member, call
it, capture the same outputs.DSPLY lines (and file after-images)
byte for byte. Equivalence holds only if they are identical.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.
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.
| Object | Type | Role |
|---|---|---|
| run_corpus.mjs | Driver (Node ESM) | The whole corpus: emits programs, drives conversion, runs the oracle, prints the scoreboard. |
| ORDLIB | Library | The scratch library created for the run (CRTLIB). |
| QRPGLESRC | Source PF (RCDLEN 112) | Holds every fixed-form member and its converted *F twin. |
| QDDSSRC | Source PF (RCDLEN 112) | Holds the two DDS PF members. |
| OCUSTP | PF (DDS, keyed) | Customer master — single key CUSTNO. |
| OORDP | PF (DDS, keyed) | Order detail — two-part key CUSTNO+ORDNO. |
| ORDCALC | Fixed-form RPG | Order-economics callee (*ENTRY PLIST): tax/discount/cases, dates, memo. |
| ORDVAL | Fixed-form RPG | Order-edit validation: TESTN/COMP/CASxx/string BIFs. |
| ORDPOST | Fixed-form RPG | Order posting: keyed I/O, LOOKUP, DO, CALL, WRITE. |
| ORDRPT | Fixed-form RPG | Customer report: MOVE/MOVEL padding, TIME, full-file scan. |
| *F twins | Free-form RPG | ORDCALCF/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.js | Engine (READ-only here) | The converter under test — convert(), driven via CVTRPGFREE. |
| LANG-QA-FINDINGS.md | Findings log | Where 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.
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 scripts | What the driver issues |
|---|---|
| Create the working library & put it on the library list | CRTLIB LIB(ORDLIB), CHGCURLIB, ADDLIBLE |
| Create the source physical files | CRTSRCPF FILE(ORDLIB/QRPGLESRC) RCDLEN(112), same for QDDSSRC |
| Create the two keyed data files from DDS | CRTPF FILE(ORDLIB/OCUSTP)…, CRTPF FILE(ORDLIB/OORDP)… |
| Convert a fixed-form member to free | CVTRPGFREE SRCFILE(ORDLIB/QRPGLESRC) SRCMBR(…) TOMBR(…) |
| Compile a member (fixed or converted) | CRTBNDRPG PGM(ORDLIB/…) SRCFILE(ORDLIB/QRPGLESRC) SRCMBR(…) |
| Run a program and read its output | compile.callPgm(job, ORDLIB, pgm) → the DSPLY lines |
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)
| Parameter | Meaning |
|---|---|
| SRCFILE | The source PF holding the fixed-form member (here always ORDLIB/QRPGLESRC). |
| SRCMBR | The fixed-form member to convert. |
| TOMBR | The 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).
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.
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.
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.
CVTAPPQA.CRTLIB ORDLIB, the two source PFs
(QRPGLESRC/QDDSSRC at RCDLEN(112)), and the two keyed data
files (OCUSTP, OORDP) from DDS; seed three customers.SUBST-of-a-literal repro, run first so the
rest of the corpus can be written to route around it.*ENTRY PLIST→dcl-pi synthesis. Run indirectly via ORDPOST.TIME
masked.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).LANG-QA-FINDINGS.md; print cvtfree-app corpus: N/total and exit
0 only if all passed.| Check | Program | What it proves | Method |
|---|---|---|---|
| P0.1 | QASUBST | Fixed 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.2 | ORDCALC | *ENTRY PLIST→dcl-pi *n with all 10 typed parms; both forms compile clean. | convert (skipRun), inspect emitted dcl-pi lines, compile both. |
| P2.0 | ORDVAL | Valid order → OK 0; fixed and free DSPLY identical. | convert + run both + byte diff, plus a hand-computed expected string. |
| P2.1 | ORDVALB | Bad input (all-blank order no. + negative amount) → ERRORS FOUND 2; identical. | same oracle, other branch of TESTN/COMP. |
| P0.2 | QA02 | A 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.2 | ORDPOST | Keyed 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.1 | ORDRPT | DSPLY 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.4 | QAGOTO/OSPEC/CYCLE | The three non-convertibles refuse loudly; the CL command respects TODO(*NO)/*YES. | call convert(), assert !ok and the expected todos[].reason. |
WKOVL work field to sidestep it); CVTFREE-QA-02 runs before ORDPOST
(which splits its status memo into several short EVALs to sidestep it).DSPLY of its own; its
real equivalence exercise is through ORDPOST's CALL+PARM to it, so
ORDCALC must be converted and compiled first.OORDP to compute
the next order number and then writes a row, so it is not idempotent. The oracle clears and
re-seeds the file to the same two-order baseline before each of the two runs, or the free run would
see the fixed run's extra row and diverge.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.
| Field | DDS type | Meaning |
|---|---|---|
| CUSTNO | 6A | Customer number (the key). |
| CUSTNAME | 20A | Customer name (used by ORDRPT's report line). |
| CREDLIM | 9P 2 | Credit limit (packed, 2 dp) — ORDRPT totals it. |
Seeded rows: ('C0001','ACME CORP',50000.00),
('C0002','GLOBEX LLC',15000.00), ('C0003','INITECH',2000.00).
| Field | DDS type | Meaning |
|---|---|---|
| CUSTNO | 6A | Owning customer — first key part (partial-key scan target). |
| ORDNO | 4S 0 | Order number (zoned) — second key part. |
| ORDDATE | 10A | Order date (ISO YYYY-MM-DD). |
| ORDAMT | 9P 2 | Order amount (packed, 2 dp). |
| ORDSTAT | 1A | Order 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.
CHAIN via KLIST CKEY/KFLD
WCUST to look up the customer name (indicator 90 = not found).SETLL then
READE looping while *IN92 = *OFF) via KLIST PKEY/KFLD
WCUST2 to find the highest existing order number, then +1 and a
WRITE OORDR of the new row.SETLL *LOVAL + READ/DOWEQ
*OFF full-file scan, counting rows and totalling CREDLIM.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.node cvtfree-app/run_corpus.mjs.cvtfree-app/.data-corpus) each run, so it is fully self-contained and repeatable.PASS/**FAIL line per check —
and the final cvtfree-app corpus: N/total tally.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.
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:
| # | Check | Green means |
|---|---|---|
| P0.1 | CVTFREE-QA-01 repro | The SUBST-of-a-literal defect still reproduces (converts, then fails to compile) — and the corpus routed around it. |
| P1.0 | ORDCALC converts | PLIST/PARM, half-adjust, DIV+MVR, XFOOT/MOVEA, ADDDUR/EXTRCT, CAT/SUBST/XLATE all convert with no TODOs. |
| P1.1 | ORDCALC + ORDCALCF compile | Both the fixed and converted members build clean. |
| P1.2 | dcl-pi synthesized | The converter emits dcl-pi *n; with all 10 typed parameters. |
| P2.0 | ORDVAL equivalence | Valid order: identical DSPLY, result OK 0. |
| P2.1 | ORDVAL bad-input | All-blank order + negative amount: identical DSPLY, ERRORS FOUND 2. |
| P0.2 | CVTFREE-QA-02 repro | The long-line/RCDLEN defect still reproduces — and ORDPOST routed around it. |
| P3.0 | ORDPOST equivalence | Keyed I/O program: identical DSPLY across the re-seeded runs. |
| P3.1 | ORDPOST hand-computed | NEXTNO=3 (over seeded 1,2) and TIER=2 (LOOKUP nearest-high). |
| P3.2 | ORDPOST after-image | Keyed-file records byte-identical (3 orders) between fixed and free. |
| P4.0 | ORDRPT equivalence | Identical DSPLY with the TIME line masked. |
| P4.1 | ORDRPT hand-computed | MOVEL→XY3456, MOVE→AB XY, DONE 3. |
| P5.0 | GOTO/TAG refused | Conversion refuses (no silent mis-conversion). |
| P5.1 | O-spec refused | Program-described printer O-spec refuses (not expressible free-form). |
| P5.2 | L1 cycle refused | Level/cycle indicator refuses. |
| P5.3 | TODO(*NO) default | Unconvertible source is not written by default. |
| P5.4 | TODO(*YES) writes | Force-write produces a member carrying // TODO(CVTFREE) markers. |
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.
| Situation | Meaning | Action |
|---|---|---|
| A P1–P4 check FAILs | The 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 FAILs | The 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" FAILs | An 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. |
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.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.
*ENTRY PLIST, no DSPLY)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.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).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.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.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.These are the fixed-form idioms the corpus proves CVTRPGFREE converts correctly (fixed and
free runs byte-identical), with the program that exercises each:
| Family | Fixed-form opcodes | Free-form target | Where |
|---|---|---|---|
| Linkage parameters | *ENTRY PLIST + PARM | dcl-pi *n with typed parms | ORDCALC |
| Half-adjust arithmetic | Z-ADD, MULT(H) | eval with rounding | ORDCALC (tax, discount %) |
| Divide + remainder | DIV then MVR | %rem pairing | ORDCALC (cases/leftover) |
| Array footing / staging | XFOOT, MOVEA | %xfoot, %subarr | ORDCALC |
| Date durations | ADDDUR, EXTRCT | + %days(), %subdt | ORDCALC (due date, month) |
| String build/overlay | CAT, SUBST, XLATE | concatenation, %subst, %xlate | ORDCALC, ORDVAL, ORDRPT |
| Digit classification | TESTN (ind 60/61/62) | %check-style tests | ORDVAL |
| Compare + indicators | COMP | conditioned eval | ORDVAL |
| Case dispatch | CASGE/CASGT/CAS+END | select/when | ORDCALC, ORDVAL |
| Scan / verify | SCAN, CHECK, CHECKR | %scan, %check, %checkr | ORDVAL |
| Keyed I/O | KLIST/KFLD, CHAIN, SETLL, READE, WRITE | %kds-style keyed ops | ORDPOST |
| Array lookup | LOOKUP (ASCEND, nearest-high) | %lookup family | ORDPOST |
| Counted loop | DO…ENDDO, DOWEQ | for, dow | ORDPOST, ORDRPT |
| Program call | CALL + PARM | callp/prototyped call | ORDPOST→ORDCALC |
| Move / padding | MOVE, MOVEL | eval with faithful right/left overlay | ORDRPT |
| Time | TIME (6,0) | %time/%timestamp | ORDRPT (masked) |
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 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.)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.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.
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.
SUBST of a quoted literal converts to an uncompilable %subst-of-a-literalSUBST '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.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.// 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.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).CVTRPGFREE CL command
(SRCMBR→TOMBR); 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 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. PASS/**FAIL), truncating detail to 160
chars; the final tally and exit code are driven off the collected results.LANG-QA-FINDINGS.md at the end
of the run under a header comment.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.dcl-s, dcl-pi, eval,
for/dow, ;-terminated statements) — the conversion
target.SRCMBR) to a fully-free member
(TOMBR); driven throughout this corpus. Backed by ibmi/cvtfree.js's
convert().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(*YES)
forces a best-effort write, so a human can finish the conversion by hand.(H) extender, e.g. MULT(H)): round the result to its
declared decimal positions rather than truncate.DIV produces the quotient,
MVR immediately after retrieves the remainder.KLIST) and its constituent key fields (KFLD)
used for keyed file operations like CHAIN/SETLL/READE.MOVE) and left-adjusted (MOVEL) character overlay with
faithful unpadded semantics — a converter must preserve exactly which bytes are overwritten.EVAL), which may
span several continuation lines; the converter joins them, which is where over-length output can
arise.