Fixed-format RPG migration and the free-format divide
A fixed-format RPG migration must recover column rules, indicators, and the cycle before it can share an estimate with procedural free-form code.

Fixed-format RPG and free-format RPG may compile on the same IBM i partition, touch the same physical files, and carry the same business vocabulary. They are still different migration jobs. Treating the distinction as a formatting detail produces an estimate that looks tidy and fails as soon as the team meets indicators, input specifications, or a program whose control flow lives partly inside the compiler-generated cycle.
A credible estimate starts by asking where the behavior is encoded. In a procedural free-format program, much of it appears in statements a modern engineer can follow. In old fixed-format code, columns, specification order, record-identifying indicators, level breaks, and output conditions can all carry meaning. The migration has to recover that meaning before anybody can sensibly price the rewrite.
Columns are part of the program
In fixed-format RPG, horizontal position is syntax, so a migration tool cannot normalize whitespace before it understands the source. IBM's ILE RPG Reference says that the specification type sits in position 6 for column-limited source: H for control, F for files, D for definitions, I for input, C for calculations, O for output, and P for procedures. Other fields also occupy prescribed positions. A character shifted into the wrong column can change an operation, an indicator condition, or whether the compiler reads the line at all.
That fact changes ingestion. A parser must preserve the original record width, sequence fields, tabs, source member CCSID, and copy-member boundaries. Exporting a source member through a path that expands tabs or trims trailing blanks can destroy evidence before analysis begins. Treat the raw source record as an artifact and derive a display version from it, never the other way around.
A useful intake check records both the raw line and a column ruler. For example, the following shell-shaped output is what an inventory report should make visible, even if the actual extraction runs on IBM i:
member=ORDRPT line=184 bytes=80 spec=C cond_1=03 cond_2=N04 opcode=EXCPT
000001 C 03N04 EXCPT ORDTOTAL
1 2 3 4 5 6 7 8
12345678901234567890123456789012345678901234567890123456789012345678901234567890
The point is not the report format. It is that 03, N04, EXCPT, and ORDTOTAL must be captured as different fields with their original positions. A generic text parser sees tokens. An RPG-aware parser sees a calculation conditioned on one indicator being on and another being off, followed by an exception output operation whose definition may sit much farther down the member.
Free-format RPG removes most of that positional load. With fully free source, **FREE appears in column 1 of the first line and the remaining statements can extend beyond the old column-limited area. Statements such as ctl-opt, dcl-f, dcl-s, and dcl-proc expose their roles by keyword and end with semicolons. That is easier to parse, but it does not prove that the program is procedural or free of legacy constructs. Syntax is the first classification, not the final one.
Indicators are a hidden control-flow network
Numbered indicators are state, conditions, and side effects compressed into two characters. A fixed calculation can test indicators in its conditioning columns and set high, low, or equal result indicators in other columns. Input specifications can set record-identifying, control-level, and field indicators. File operations can set error or end-of-file indicators. Output specifications can use those states to decide whether a record or print line is emitted.
This is why replacing *IN03 with a Boolean named indicator03 is transliteration, not modernization. The name preserves the storage location and loses the reason it exists. The migration has to build a use-def graph: every place that can set the indicator, every operation conditioned by it, every reset, and every boundary across which it remains live. Only then can the team decide whether it means customerChanged, writeTotals, recordFound, validationFailed, or several unrelated things reused at different times.
Indicator reuse is the awkward case that line counts conceal. An indicator can mean one thing in detail calculations, get cleared, then mean something unrelated inside a subroutine. A global search reports two clusters but cannot prove they are independent. The analyst must include call order, cycle phase, and file-operation effects. If one value crosses an EXSR boundary or survives into total calculations, a casual rename can merge states that the original kept separate in time.
IBM documents *IN as an array covering the numbered indicators and warns that values other than zero, one, *OFF, or *ON make later tests unpredictable. That detail matters during conversion because some programs manipulate slices of the indicator array as data. A modern target should not reproduce a magic 99-element Boolean array unless compatibility demands it at an edge. It should decode the array writes, name the intended states, and pin ambiguous behavior with tests before deleting the old representation.
The practical artifact is an indicator ledger, not a count:
| Indicator | Set by | Read by | Cycle phase | Candidate meaning | Confidence | | | | | | | | | 03 | record type on I-spec | conditioned C-spec | detail | order record selected | high | | 04 | CHAIN result | EXCPT condition | detail | customer missing | medium | | L1 | control field change | total calculations | total | account break | high | | LR | primary-file EOF | totals and shutdown | last cycle | finalization | high |
That ledger gives reviewers something falsifiable. It also exposes where the estimate needs investigation. Ten well-named, single-purpose indicators may be cheaper than three indicators whose meanings change with cycle phase.
The RPG cycle owns control flow you cannot see
A cycle-driven program delegates sequencing to compiler-generated logic, so reading the calculation specifications from top to bottom does not reveal the runtime order. IBM's cycle programming documentation describes a repeated sequence that reads a record, sets record and control-level indicators, runs total work for a boundary, performs total output, checks the last-record condition, moves input fields, and then runs detail calculations. The first and last passes have their own behavior.
That order surprises engineers who expect an explicit loop. Totals for the group that just ended may run after the next record has been read far enough to establish a control break but before its input fields become the current detail data. Heading or detail output can also occur at cycle-defined moments. A rewrite that places read() at the top of a conventional loop and totals at the bottom can be reasonable, readable, and wrong.
Primary and secondary files add more implicit behavior. The program may never issue the read that advances a primary file because the cycle does it. Matching-record logic, control fields, input records, and output specifications cooperate through compiler rules. The LR indicator can arrive implicitly after the final primary or secondary record, and IBM notes that setting LR also turns on the control-level indicators for final total processing. That is execution semantics, not a quaint termination flag.
The safe conversion makes the hidden state machine explicit before changing its architecture. Represent phases such as initialization, input selection, control-break detection, prior-group totals, detail processing, and final totals. Record which fields contain the prior record, the newly selected record, and the current processing data in each phase. Then write the procedural target against that model.
A minimal behavioral trace can look like this:
seq=411 phase=read account=170 record=invoice
seq=412 phase=break level=L1 old_account=160 new_account=170
seq=413 phase=total account=160 amount=9284.15 output=ACCT_TOTAL
seq=414 phase=detail account=170 invoice=88412 amount=73.20
seq=415 phase=final lr=on output=REPORT_TOTAL
If the old system cannot emit a trace safely, derive the expected events from recorded inputs and observable outputs. The essential test is ordering. Matching final totals are insufficient when the target writes a downstream record, updates a balance, or calls another program at the wrong phase.
Free-format does not mean one thing
Free-format RPG spans several styles, and only some of them behave like ordinary procedural code. A member may contain free-form calculations between /FREE and /END-FREE while retaining fixed F, I, or O specifications. A newer member may use free-form declarations but still rely on a cycle-main procedure. A fully free module may use ctl-opt nomain, procedures, explicit reads, qualified data structures, and prototypes. Calling all three “free-format” destroys the estimate's most useful signal.
IBM's documentation draws the technical boundaries clearly. Fully free source uses **FREE on the first line; fixed statements needed by that source, such as old input or output specifications, must live in a copy file. The RPG IV specification rules also say that MAIN or NOMAIN prevents a cycle-main procedure, while a module without those keywords may still have one. Therefore **FREE alone cannot answer whether the cycle exists.
I classify members on independent axes:
- source mode: fixed, column-limited mixed, or fully free
- execution model: cycle-main, linear main, or
MAIN/NOMAINprocedures - data access: cycle-controlled files, explicit native I/O, embedded SQL, or a mixture
- state model: numeric indicators, named indicators, explicit variables, or mixed
- external shape: program calls and data areas, service procedures, queues, files, or job commands
This classification prevents a common estimate failure. Two fully free members may differ radically if one is a thin procedure with explicit SQL and the other pulls old O-specs through /COPY, toggles numbered indicators, and ends through *INLR. Conversely, a fixed-format member can be mechanically regular and well covered by repeatable rules. Format affects difficulty, but behavior decides it.
Compile level matters too. A source member's syntax tells you what it uses, not every compiler or runtime constraint in production. Inventory target releases, activation-group choices, binding directories, externally described files, copybooks, service programs, and compile commands. A rewrite plan that ignores the build graph will discover “missing code” that was actually injected or resolved at compile time.
Modernization starts with a behavioral model
The target should express business intent and keep compatibility at named boundaries. For a cycle report, that often means a reader that yields typed records, a grouping component that detects control breaks, a calculator for detail and totals, and an output adapter that reproduces the required external records. For an interactive program, it may mean separating display state, validation, file access, and command invocation. The shape follows behavior, not specification letters.
Do not convert each C-spec into a statement and each indicator into a Boolean, then call the result Go or TypeScript. That approach is popular because it is measurable: every source line gets a destination line, automated diff reports look busy, and reviewers can find familiar labels. It is wrong because it preserves accidental structure while making implicit runtime rules harder to recognize. The target becomes old RPG semantics written in a language whose maintainers do not know RPG.
A good intermediate representation keeps facts that the final design will intentionally discard. It should retain source location, specification type, operation, factors, result, conditioning indicators, result indicators, file and record-format references, subroutine edges, procedure calls, copy-member origin, and cycle phase. It should also distinguish a compiler-derived edge from a source-written edge. Without that distinction, the migration cannot explain why a target branch exists.
The architectural decision also depends on the system boundary. If callers depend on an RPG program's parameter list, data queue messages, externally described record formats, commitment control, or job-level state, preserve that contract first. Change the internals behind an adapter. Trying to redesign every neighboring contract at the same time turns a language migration into an unbounded operating-model project.
Some behavior should not survive. Indicator aliasing, global mutable fields, implicit opens, and cycle timing do not deserve permanent homes in the target. Preserve their observable consequences, prove parity, then remove the scaffolding. That sequence separates “same behavior” from “same implementation,” a distinction modernization programs routinely blur.
Discovery must inspect the whole executable system
An estimate based on RPG member line counts excludes much of the program. The executable system includes copy members, display and printer files, database definitions, CL wrappers, commands, job descriptions, binding directories, service programs, data areas, data queues, message files, SQL objects, and the compile procedure. A 300-line member can sit at the center of a much larger behavioral surface.
Start with a reproducible inventory whose rows can be traced back to evidence. At minimum, capture object or member identity, source type, last compile metadata when available, direct references, incoming callers, copy dependencies, file access mode, indicator count by role, cycle usage, subroutines and procedures, embedded SQL, external program calls, and unavailable source. Mark generated source and duplicate variants rather than silently deduplicating them.
The awkward question is whether all production source exists. On IBM i, a runnable object does not guarantee that the exact source or compile options remain available. Teams often find a newer-looking member in one library while production runs an object compiled from another revision. Compare object metadata, library lists, binding information, and deployed behavior. If provenance is uncertain, price that uncertainty explicitly instead of assuming the repository is canonical.
Sampling has to follow risk, not convenience. Reading the cleanest free-format service program tells you little about a cycle-driven billing report. Choose samples that cover each source mode, execution model, I/O style, indicator pattern, object type, and business path. Include the member everybody avoids, the one with copied O-specs, and the program that only runs at close. Those are where the estimate earns its keep.
A discovery result should split findings into known, inferred, and unverified facts. “Program A calls Program B” may be known from a resolved call graph. “Indicator 42 means retry” may be inferred from operations and messages. “This branch is dead” remains unverified until production evidence or a controlled test supports it. Different confidence levels need different contingency; collapsing them into a single complexity score hides the work.
Parity needs production-shaped evidence
Compiler success proves syntax, and unit tests prove selected functions. Neither proves that a rewritten RPG system behaves like production. Parity testing must compare the old and new systems with inputs shaped by real work, including record order, blank and zero values, packed-decimal edges, status codes, control breaks, missing records, duplicate keys, end-of-file behavior, and job context.
The comparison unit should match the contract. For a report, compare normalized spool content, page and total boundaries, and any side-effect records. For a batch update, compare database changes, messages, calls, commit boundaries, and restart state. For an interactive flow, compare screen transitions, validation messages, function-key behavior, and resulting writes. Timestamp fields, generated identifiers, and nondeterministic ordering need declared normalization rules, not ad hoc exclusions after a mismatch.
Use a manifest for every replay:
{"case":"account-break-final-record","input_set":"sha256:...","old_build":"LIBA/ORDRPT:...","new_build":"git:...","normalizers":["run_timestamp"],"expected_events":417}
The harness should retain the input identity, both build identities, normalized outputs, raw outputs where policy permits, and the first divergent event. “Files differ” creates an investigation. “At event 413, old code emitted ACCT_TOTAL before processing account 170; new code emitted it after” identifies the broken control-break model.
Recorded production traffic is especially useful because it contains combinations that test designers forget. It still needs handling rules: mask or tokenize sensitive fields consistently, preserve relational equality, capture necessary job attributes, and prevent replays from calling live external systems. Where traffic cannot leave the customer perimeter, run the comparison there. CodeHero uses this model: it reads the whole tree, rewrites the architecture, and checks behavior with a parity harness against recorded production traffic.
Parity is not a demand to preserve every accident forever. First label each mismatch as required behavior, tolerated defect, environmental noise, or an approved change. Then make the decision auditable. Quietly “fixing” a calculation during migration can be more dangerous than carrying it temporarily, because downstream files or reconciliation procedures may depend on the old result.
Fixed and free estimates need different units
A free-format procedural migration can often be estimated by explicit units: procedures, SQL statements, file contracts, external calls, screens, and tests. Fixed-format cycle code needs extra units for recovered semantics: indicator clusters, input and output specification networks, control-level groups, cycle-controlled files, exception output paths, subroutine state, and source reconstruction. Those units represent analysis and verification, not typing.
Separate the estimate into four buckets: inventory and provenance, semantic recovery, target implementation, and parity evidence. Free-format procedural code may spend more of its budget in implementation. Fixed cycle code usually shifts effort toward semantic recovery and replay design. Applying one rate per line across both makes the hard work invisible and rewards the least informative metric.
Complexity should rise when interactions multiply. One level-break indicator with one total line is bounded. Several control levels combined with matching records, shared indicators, exception output, and copied specifications create state combinations. Do not add a flat surcharge for each feature and pretend the effects are independent. Price the combined behavioral paths that must be understood and tested.
Use ranges until discovery closes specific unknowns. A useful estimate records a basis and an exit condition beside every range: “cycle semantics, medium confidence, narrows after two representative traces” is defensible. “RPG conversion, 500 lines per day” is not. The range should shrink when the team resolves source provenance, produces the indicator ledger, validates the call graph, and replays representative cases.
The estimate must also say what it excludes. Data cleansing, changing business rules, replacing upstream job scheduling, redesigning screens, or merging duplicate applications may be sensible work, but they are not automatically part of a language rewrite. If stakeholders want them, give them their own decisions, evidence, and price. Otherwise every desirable change will be charged to “RPG difficulty,” and nobody will learn what the migration actually costs.
Put the two styles through the same estimation worksheet and the difference becomes concrete. For a procedural free-format service, the analyst can usually identify an entry procedure, follow explicit reads or SQL, list calls, map returned values, and count contracts that need adapters. Unknowns still exist, especially around job state and external objects, but each one attaches to a visible operation or boundary. The estimate can move quickly from inventory to target design because the program states when work happens.
For a fixed cycle report, the first worksheet begins earlier. The analyst must determine which file the cycle controls, which input specifications identify records, which fields cause level breaks, when totals run, which O-spec lines those totals condition, and what LR triggers. Next comes the indicator ledger, including values set implicitly by file operations. Only after that work can the target expose the same sequence with a reader, grouping logic, calculations, and output adapters. Counting both programs as one “RPG member” erases an entire layer of deliverables.
Acceptance criteria differ in the same way. The procedural service may be covered by request and response cases, database effects, and explicit error paths. The cycle report needs ordered cases around the first record, every control level, records that change several levels at once, an empty or missing input where relevant, and the last record. If matching-record logic or exception output exists, the matrix expands around those interactions. The extra tests do not compensate for poor conversion. They prove that the newly explicit control flow matches the old implicit one.
Review skills also belong in the estimate. A Go engineer can assess target structure but may not recognize that a total calculation reads fields from a different cycle phase. An RPG practitioner can recover that behavior but may accept a literal architecture because it feels familiar. Pair the two reviews until the parity traces and intermediate representation make the reasoning visible. The handoff point should be evidence, not confidence: a reviewer must be able to trace a target branch back to an RPG rule, source condition, or approved redesign.
Estimate rework separately from expected implementation. A known conversion rule that applies to hundreds of regular calculations is implementation work. An unresolved indicator that controls five output formats is a discovery risk, and guessing at it creates rework rather than progress. Record the owner, evidence needed, and decision deadline for each such risk. This makes the contingency explainable and stops uncertainty in a few fixed-format members from inflating the price of clean procedural code.
The resulting numbers can still roll up to one commercial proposal, but the proposal should preserve the internal classes. Delivery can then sequence low-uncertainty procedural components while semantic work closes the risky cycle paths, without pretending that throughput will be uniform. If a shared dependency blocks both classes, show it once at the portfolio level. If only cycle recovery needs it, keep the cost with the cycle class. That is how one project price can remain honest without forcing one migration estimate onto two kinds of work.
That separation also makes change control clearer: when a newly discovered cycle rule moves the estimate, stakeholders can see which class changed and why the procedural forecast did not.
One blended number creates the wrong plan
The same estimate cannot cover fixed-format cycle code and procedural free-format RPG because the work begins at different levels of certainty. In the procedural case, the team can often see control flow and spend its time rebuilding contracts cleanly. In the fixed cycle case, it must first reconstruct control flow from columns, specifications, indicators, compiler rules, and runtime evidence. Both can be migrated, but they do not enter the factory through the same door.
This does not justify an open-ended archaeology project. Time-box discovery around representative risk classes and require concrete outputs: a source-mode census, execution-model classification, resolved build graph, indicator ledgers for risky members, cycle traces, contract inventory, and parity cases. If a discovery task cannot say which estimate uncertainty it reduces, cut it.
For a portfolio, quote separate bands for at least cycle-driven fixed or mixed code, procedural fixed or mixed code, and fully free procedural code. Then adjust for missing source, external object count, data-contract sensitivity, and test evidence. Keep the classification visible in delivery planning, because it also determines reviewer skills and the order in which components should move.
CodeHero commits to completing each rewrite in under 30 days, including systems above a million lines, so our intake has to make these distinctions immediately rather than hiding them in a blended rate. Whether or not you use us, ask any migration vendor to show how its estimate accounts for cycle phases, indicator reuse, copied specifications, production provenance, and parity at control breaks. If the answer returns to line counts, the estimate has not described the job.
FAQ
Can fixed-format RPG be converted automatically to free-format RPG?
Syntax can be converted mechanically in many cases, but that does not modernize the execution model. Indicators, cycle timing, I-specs, O-specs, and reused global state still need semantic analysis and behavioral tests.
Does **FREE mean an RPG program does not use the RPG cycle?
No. **FREE selects the source mode; it does not by itself remove cycle-main behavior. Check for MAIN or NOMAIN, file control, copied specifications, and the actual termination path.
Why are RPG indicators difficult to migrate?
An indicator can be set by source code, file operations, input records, or the cycle, then reused for another purpose later. A safe rewrite maps every setter and reader in execution order before replacing it with named state.
What is the RPG program cycle?
It is compiler-generated control flow that can read records, detect control breaks, run total calculations and output, process detail work, and handle finalization. The exact phase order matters when a rewrite turns that behavior into an explicit loop.
Is free-format RPG always cheaper to migrate?
No, but fully free procedural code usually exposes more of its intent directly. Free-format source that still uses the cycle, numeric indicators, copied O-specs, or broad external state can remain expensive.
How should an RPG migration be estimated?
Estimate inventory, semantic recovery, implementation, and parity separately. Use behavioral units such as cycle-controlled files, indicator clusters, contracts, procedures, and replay cases instead of one rate per line.
What source should be collected before an RPG rewrite?
Collect RPG and copy members plus CL, DDS, SQL objects, commands, service-program details, compile commands, and relevant job configuration. Confirm that deployed objects actually came from the source and options you collected.
How do you test a migrated cycle-driven RPG program?
Replay production-shaped inputs through old and new builds and compare ordered events, outputs, database changes, calls, messages, and final state. Include first-record, control-break, missing-record, duplicate-key, and last-record cases.
Should a migration preserve *INLR behavior?
It must preserve the observable finalization behavior, including totals and file or state effects that occur when LR is on. The target does not need to retain *INLR as a global flag once tests prove an explicit lifecycle is equivalent.
Can fixed-format and free-format RPG share one project plan?
They can share governance, target standards, and a parity framework. They need separate discovery classes, estimates, and review paths because fixed cycle code requires semantic reconstruction that procedural free-format code often does not.