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.
Measured on real source (not demos)
RPG (SlimeRPG → Java/runtime)
| Stage | Result | Read |
|---|---|---|
| Lexing | 99.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 |
| Hangs | 0 / 1,711 | 10 emitter infinite-loops found & fixed (DO-UNTIL, MONITOR, inline data-structures) |
CL (SlimeCL → bash / Rust)
| Stage | Result | Read |
|---|---|---|
| Lexing | 100% (46 / 46) | reads all real CL |
| Emit | 100% (46 / 46), 0 hangs | never stalls or crashes |
| bash syntax-valid | 100% (46 / 46) | bash is the natural target for CL’s command-orchestration nature |
| Rust compile | 100% (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.
IBM i output == SlimeCL bash == SlimeCL Rust, byte for byte.
| Program | IBM i | SlimeCL bash | SlimeCL Rust | |
|---|---|---|---|---|
countdown (sum 1..5, *DEC→*CHAR) | 00000000000000000015 | 00000000000000000015 | 00000000000000000015 | PASS |
exprdemo (parens, *AND, *CAT) | OK | OK | OK | PASS |
sstdemo (%SST substring) | HELLO | HELLO | HELLO | PASS |
dtaara (data area + ALCOBJ/DLCOBJ) | HELLO | HELLO | HELLO | PASS |
ebcdic ('A' *LT 'a' — EBCDIC collation) | GE | GE | GE | PASS |
binconv (%BIN('//') — big-endian *INT2) | …24929 | …24929 | …24929 | PASS |
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*CHARas a variable-length string (givingOK!). Fixed — both targets now model fixed width. *DEC→*CHARconversion: a packed-decimal total assigned into a character field is right-justified and zero-filled to the field width (00000000000000000015). SlimeCL had emitted a bare15. Fixed.SNDPGMMSG MSG()requires*CHAR, and theCALLcommand’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 (
%BINover EBCDIC, packed-decimal precision) and OS-service execution (SBMJOB,RCVMSGcompile 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
