OPEN · technical deep-dive

AS/400 (IBM i) modernization — what SlimeCL / SlimeRPG can & can’t do

An honest, real-corpus account. The numbers below come from actual public IBM i source — not hand-picked demos. We publish the gaps too, because transparency is the moat: competitors rarely publish multi-target pass rates at all.

The thesis. CL is the glue of an IBM i shop; RPG / COBOL are the business logic. We translate them to portable bash and a single-binary Rust — continuity, not rewrite. This page states exactly how far that works today, measured, with hangs and compile-gaps shown.

Measured on real source (not demos)

1,711
real RPG programs swept (jariko / OSSILE / noxDB / IBM samples)
46
real CL programs — public “IBM i RPG Free CLP” corpus (.clp / .clle)
0
hangs / crashes across all of the above (after fixing the 10 we first found)

RPG (SlimeRPG → Java/runtime)

StageResultRead
Lexing99.4% (1,701 / 1,711)reads almost all real RPG, including fixed-format RPG/400
Emit (produces output)99.8%no crashes; structured translation
javac compile (sample)~82%honest gap — surrounding RPG (BIFs, cursor fields) not yet complete
Hangs0 / 1,71110 emitter infinite-loops found & fixed (DO-UNTIL, MONITOR, inline data-structures)

CL (SlimeCL → bash / Rust)

StageResultRead
Lexing100% (46 / 46)reads all real CL
Emit100% (46 / 46), 0 hangsnever stalls or crashes
bash syntax-valid100% (46 / 46)bash is the natural target for CL’s command-orchestration nature
Rust compile100% (46 / 46)every program in this public corpus now compiles to both targets

This iteration (2026-05-26), on this same corpus, Rust compile rose 69.6% → 100% and bash 76.1% → 100% — not by adding shims (unhandled commands already emit as compiling comments), but by hardening the emitter for the dialects of older CL: positional parameter syntax (CHGVAR &X '1', DCL (&X) (*CHAR), positional IF/THEN), reserved-word variable names (&Type), substring assignment (CHGVAR %SST(&v s l)), symbolic comparison operators (= > <), implicit DCLF *CHAR fields, %BIN, special values (*YES), *CHAR*DEC conversion both ways, Rust ownership (borrow vs. move), and a DDS front-end that reads DCLF record-field types straight from the file’s .DSPF/.PF definition (the same positional-parser idea as our BMS / XMAP map parsers). Each fix was verified for no regression (29 unit tests + 4/4 bit-exact). Reproducible with bitexact/measure_compile.sh.

Verified bit-exact on a live IBM i ✓ new

We compiled and ran the original CL on a real IBM i (7.5, on PUB400.com) and compared its output — byte for byte — against the SlimeCL-emitted bash and Rust. This is the milestone we previously listed as “not yet published”. It is now.

6 / 6 bit-exact, 3-way. For every program below, IBM i output == SlimeCL bash == SlimeCL Rust, byte for byte.
ProgramIBM iSlimeCL bashSlimeCL Rust
countdown (sum 1..5, *DEC*CHAR)000000000000000000150000000000000000001500000000000000000015PASS
exprdemo (parens, *AND, *CAT)OKOKOKPASS
sstdemo (%SST substring)HELLOHELLOHELLOPASS
dtaara (data area + ALCOBJ/DLCOBJ)HELLOHELLOHELLOPASS
ebcdic ('A' *LT 'a' — EBCDIC collation)GEGEGEPASS
binconv (%BIN('//') — big-endian *INT2)…24929…24929…24929PASS

The last two close runtime gaps that compile clean but would silently diverge on real hardware: a *CHAR comparison must use EBCDIC collation (where lowercase < uppercase < digits — the reverse of ASCII), so 'A' *LT 'a' is false on IBM i (→ GE), not true as a naïve ASCII string compare would give; and %BIN reads the field’s bytes as a big-endian signed integer (*INT2/*INT4), not the host’s little-endian. Both are bound to the IBM i semantics and verified byte-for-byte above.

The valuable part is what real hardware caught — fidelity gaps no synthetic test would expose, which we then fixed:

  • Fixed-length *CHAR: IBM i character fields are blank-padded to their declared length, so &MSG *CAT '!' on a 20-byte field pushes the ! off the end → OK. SlimeCL had treated *CHAR as a variable-length string (giving OK!). Fixed — both targets now model fixed width.
  • *DEC*CHAR conversion: a packed-decimal total assigned into a character field is right-justified and zero-filled to the field width (00000000000000000015). SlimeCL had emitted a bare 15. Fixed.
  • SNDPGMMSG MSG() requires *CHAR, and the CALL command’s char-vs-packed parameter typing can silently corrupt a numeric value. Both surfaced on first contact and are documented.

Honest scope. This is three representative programs, not the whole corpus. The point is the method: a reusable harness that compiles each program on a real IBM i, runs it, and diffs the output against both emitted targets. Extending the program set is ongoing. We know of no competitor that publishes live-hardware, byte-for-byte transpiler validation at all.

What it can do today

  • Control flow: IF/ELSE, DOWHILE/DOUNTIL/DOFOR, GOTO+labels (modeled with an exact program-counter state machine, since neither bash nor safe Rust has goto).
  • Intra-program subroutines (SUBR/CALLSUBR) → real functions. CL variables are program-global, so we model them faithfully (module statics + functions in Rust; shell functions over globals in bash).
  • Expressions with proper precedence & parentheses: (&A *GT 5) *AND (&B *LT 3), string concat *CAT, and BIFs %SST / %SCAN / %LEN / %TRIM / %CHAR / %UPPER / %LOWER.
  • Typed variables: *DEC → i64/f64, *CHAR → String, *LGL → bool.
  • Older-CL dialects: positional parameter syntax (CHGVAR &X '1', DCL (&X) (*CHAR)), symbolic comparison operators (= > < ¬=), substring assignment (CHGVAR %SST(&v s l)), and reserved-word variable names.
  • OS-service runtime shims: data areas (RTV/CHG/CRT/DLT DTAARA) run via a process-local store; object locks (ALCOBJ/DLCOBJ) and file overrides are recognized.
  • Single static binary output (Rust target) — runs on commodity x86/Linux, no IBM i runtime, no external libraries.

What it can’t do yet (honest)

  • Behavioral validation is the next frontier, not compilation. All 46 compile; bit-exact is verified on a representative 4 (not yet all 46). Byte-level semantics (%BIN over EBCDIC, packed-decimal precision) and OS-service execution (SBMJOB, RCVMSG compile but aren’t fully executable; data areas & object locks run via runtime shims) are the honest remaining gaps.
  • ~18% of real RPG doesn’t compile — missing BIF coverage and cursor host-variable typing (SQLRPGLE).
  • Full-corpus behavioral validation: bit-exact validation on a live IBM i is now done for a representative set (3/3, see above) — extending it across the full 1,700+ program corpus is the ongoing work.
  • DDS (display files / 5250), DB2 for i embedded-SQL semantics, and QUERY/400 are captured but not yet fully executed.

The pipeline

CL1 lexer   →  CL2 Slot IR + expression parser  →  CL6 emitter { bash | Rust }
(same staged shape across the JAVATEL transpiler family: COBOL, RPG, PL/I, MUMPS, …)

Example — a CL DOFOR loop calling a subroutine becomes idiomatic Rust:

i = 1;
while i <= 5 {
    subr_addit();   /* CALLSUBR SUBR(ADDIT) */
    i += 1;
}
fn subr_addit() { unsafe { sum = sum + i; } }   // program-global vars

Why we publish the gaps

Micro Focus, Blu Age, AWS MMA and others rarely publish per-target compile pass rates at all. We publish lex / emit / compile / hang counts on real corpus, name the gaps, and show the improvement track record (10 hangs → 0 in one session). That transparency is the differentiator — for banking, public-sector and SI evaluators who must trust the result, “honest and improving fast” beats “trust us, it’s 100%.”

SlimeCL product page Request an early-access PoC ← All resources