APL is Back on the Mainframe: Running Kap on z/OS

APL was born on IBM mainframes in 1966. Ken Iverson’s notation, implemented as APL\360 on the IBM System/360, gave the world its first taste of array-oriented computing. For decades, APL lived natively on IBM iron. Then the world moved on, workstations arrived, and eventually z/OS mainframes were left without any array language software.

This post documents how to bring that back — running Kap, a modern APL-inspired array language written in Kotlin, on a z/OS mainframe via JCL batch jobs.


Why This Is Harder Than It Sounds

The obvious approach — port an existing APL interpreter — runs into several walls immediately:

  • GNU APL is C++ and has no s390x native target
  • Kotlin/Native (which Kap uses for its Linux binary) has no s390x support either
  • EBCDIC — z/OS speaks a completely different character encoding than ASCII/UTF-8, and APL’s symbol set (⍳ ⍴ ⌈ ⌊ ⍉ etc.) has no representation in IBM-1047

The breakthrough is that Kap is a Kotlin Multiplatform project and has a JVM target alongside its native Linux target. z/OS ships with IBM Semeru Runtime (Java), so the JVM path works — but getting Unicode APL symbols through z/OS’s EBCDIC layer requires some non-obvious fixes.


Prerequisites

  • A z/OS system with UNIX System Services (USS) access via SSH
  • IBM Semeru Java 17 installed (typically at /usr/lpp/java/J17.0_64)
  • A Linux build machine with Java 17 and Gradle
  • Git access to the Kap repository

Step 1: Get the Right Version of Kap

Kap currently requires Java 25, but z/OS USS commonly has Java 17. Check the Java version on USS with java --version and then checkout a version of Kap that uses the same version. You can use commits 1576c4bc for Java 25 and 202bc29f for Java 21 as markers.

Find the last commit before the Java 25 upgrade (note: I didn’t actually use one commit before 25 but one before the change to Java 21, you can try both):

git clone https://codeberg.org/loke/array
cd array
git log --oneline | grep -i "java 25\|upgrade.*25\|25.*upgrade"

Check out the commit just before the Java 25 upgrade:

git checkout 1576c4bc^

The ^ means “parent of this commit” — the last state before Java 25 was required.


Step 2: Downgrade the Toolchain to Java 17

If this step does not work out, try checking out a commit that actually uses Java 17 (so do git checkout 202bc29f^ instead).

Edit gradle.properties in the repo root and set:

kap.settings.toolchainVersion=17
kap.settings.jvmTarget=17

Step 3: Fix the UTF-8 Input Bug

This is the most important step. The StandardInputReader class in client-java/src/main/kotlin/array/plainclient/Repl.kt reads stdin byte by byte using raw System.in.read(). On z/OS, the IBM JVM intercepts System.in and converts bytes through IBM-1047 (EBCDIC) before your code sees them. UTF-8 multi-byte sequences for APL symbols get mangled in the process.

The fix is to read from stdin via a properly tagged UTF-8 file instead of stdin at all — but the StandardInputReader class still needs updating. Replace the class with:

class StandardInputReader : CharacterProvider {
    private val reader = java.io.InputStreamReader(
        java.io.FileInputStream(java.io.FileDescriptor.`in`),
        Charsets.UTF_8
    )

    override fun nextCodepoint(): Int? {
        val ch = reader.read()
        return if (ch == -1) null else ch
    }

    override fun close() {}
}

Using FileInputStream(FileDescriptor.in) bypasses the IBM JVM’s wrapping of System.in, giving us a raw file descriptor that we can then decode as UTF-8 ourselves.


Step 4: Build the JVM Distribution

From the repo root on your Linux build machine:

./gradlew client-java:distTar

This produces client-java/build/distributions/kap-jvm-text.tar. It includes all required JARs and the Kap standard library.


Step 5: Transfer to z/OS USS

Copy the tar to your z/OS home directory:

scp client-java/build/distributions/kap-jvm-text.tar youruser@yourmainframe:/z/youruser/

Then on z/OS USS, extract it:

tar xvf kap-jvm-text.tar

You will see warnings like:

tar: FSUM7171 kap-jvm-text/lib/array-jvm.jar: cannot set uid/gid: EDC5139I Operation not permitted.

These are harmless — the files extract correctly. The warnings are just z/OS tar complaining about Linux uid/gid metadata in the archive.

Fix the execute permission on the launch script, and tag it as ASCII so z/OS reads it correctly:

chmod +x kap-jvm-text/bin/kap-jvm-text
chtag -tc ISO8859-1 kap-jvm-text/bin/kap-jvm-text

Step 6: Understand the Encoding Situation

After transfer you will discover that the interactive launcher shows ? instead of APL symbols like . This is because z/OS’s IBM JVM operates with native.encoding=IBM-1047 at the system level, which intercepts all I/O.

The key insight is that z/OS has a file tagging system. When a file is tagged as UTF-8 using chtag, the JVM reads it correctly as UTF-8 without passing it through the EBCDIC conversion layer. This is the mechanism that makes everything work.

Also note that the --load argument requires = syntax, not a space:

# Wrong
kap-jvm-text/bin/kap-jvm-text --load /tmp/test.kap

# Right  
kap-jvm-text/bin/kap-jvm-text --load=/tmp/test.kap

This is a quirk of Kap’s argument parser in this version — it only handles --option=value for options that take arguments, not --option value.


Step 7: Test Kap from the Command Line

Write a test script, tag it as UTF-8, and run it:

echo 'io:print +/ ⍳10' > /tmp/test.kap
chtag -tc UTF-8 /tmp/test.kap
kap-jvm-text/bin/kap-jvm-text --load=/tmp/test.kap --no-repl

Expected output:

45

That is the sum of 0 through 9⍳10 generates the vector 0 1 2 3 4 5 6 7 8 9 and +/ reduces it with addition. If you see 45, Kap is working correctly on your mainframe.

Try a more complex array operation:

echo 'io:print 3 3 ⍴ ⍳9' > /tmp/test.kap
chtag -tc UTF-8 /tmp/test.kap
kap-jvm-text/bin/kap-jvm-text --load=/tmp/test.kap --no-repl

This reshapes ⍳9 into a 3×3 matrix.


Step 8: Create the Runner Shell Script

Create a shell script that invokes Java directly with the full classpath. Using the shell script rather than the Kap launcher avoids encoding issues in the generated launcher:

cat > /z/youruser/kaprun.sh << 'EOF'
#!/bin/sh
/usr/lpp/java/J17.0_64/bin/java \
  -Xms64m -Xmx256m \
  -Dkap.installPath=/z/youruser/kap-jvm-text \
  -classpath /z/youruser/kap-jvm-text/lib/client-java.jar:\
/z/youruser/kap-jvm-text/lib/array-jvm.jar:\
/z/youruser/kap-jvm-text/lib/kotlin-stdlib-2.0.0-RC2.jar:\
/z/youruser/kap-jvm-text/lib/mpbignum-jvm.jar:\
/z/youruser/kap-jvm-text/lib/kap-util-jvm.jar:\
/z/youruser/kap-jvm-text/lib/kotlin-reflect-2.0.0-RC2.jar:\
/z/youruser/kap-jvm-text/lib/kermit-jvm-2.0.3.jar:\
/z/youruser/kap-jvm-text/lib/kotlinx-collections-immutable-jvm-0.3.7.jar:\
/z/youruser/kap-jvm-text/lib/kermit-core-jvm-2.0.3.jar:\
/z/youruser/kap-jvm-text/lib/kotlin-stdlib-jdk8-2.0.0-RC2.jar \
  array.plainclient.Repl --load=/tmp/kaptest.kap --no-repl
EOF
chtag -tc UTF-8 /z/youruser/kaprun.sh
chmod +x /z/youruser/kaprun.sh

Replace /z/youruser with your actual USS home directory throughout.


Step 9: Run from JCL

Create the JCL job:

cat > /z/youruser/kaprun.jcl << 'EOF'
//JCLSETUP JOB ,MSGLEVEL=(0,0),CLASS=7
//KAPRUN  EXEC PGM=BPXBATCH,REGION=0M
//STDPARM  DD *
SH /z/youruser/kaprun.sh
/*
//STDOUT   DD SYSOUT=*
//STDERR   DD SYSOUT=*
EOF

A few notes on this JCL:

  • PGM=BPXBATCH is the standard z/OS program for running USS shell commands from JCL
  • REGION=0M is essential — the JVM needs significant memory and will fail without it
  • SH in the STDPARM tells BPXBATCH to run the argument as a shell command
  • STDOUT DD SYSOUT=* captures the output to the job log where you can read it in SDSF

Write your Kap script and submit:

echo 'io:print +/ ⍳10' > /tmp/kaptest.kap
chtag -tc UTF-8 /tmp/kaptest.kap
submit /z/youruser/kaprun.jcl

Check the job output in SDSF — you should see 45 in the STDOUT section.


The Working Pattern

To run any Kap computation from JCL, the pattern is always:

  1. Write your Kap script to a USS file
  2. Tag it as UTF-8 with chtag -tc UTF-8
  3. The runner shell script invokes Java with the full classpath
  4. JCL runs the shell script via BPXBATCH with REGION=0M
  5. Output appears in STDOUT in the job log

For output in Kap scripts, use io:println explicitly — the --no-repl flag suppresses the interactive result printing, so you need to print explicitly:

io:print +/ ⍳100
io:print 4 4 ⍴ ⍳16
io:print 2 +/ 1 1 2 3 5 8 13

Key Lessons Learned

z/OS file tagging is the critical mechanism. The IBM JVM on z/OS converts all I/O through EBCDIC (IBM-1047) by default. File tagging with chtag -tc UTF-8 tells the JVM a file is UTF-8, bypassing that conversion. Without tagging, APL symbols are corrupted on read.

BPXBATCH needs REGION=0M. The JVM will fail to start with a default region size. Always specify REGION=0M for Java jobs in JCL.

The Kotlin/Native path is a dead end for z/OS. Kotlin/Native has no s390x target and never will without JetBrains adding it. The JVM path is the only viable route and is actually the more feature-complete version of Kap anyway.

The IBM JVM intercepts System.in at a level below Java APIs. Setting file.encoding, stdout.encoding, or even ibm.system.encoding JVM properties does not fully override the EBCDIC conversion on stdin. The only reliable solution is to read from a properly tagged file rather than stdin.


The Quest for Pure JCL Integration

The working setup described above uses USS as an intermediary — the Kap source lives in a USS file, chtag tags it as UTF-8, and BPXBATCH runs a shell script. It works, but it isn’t what a mainframe purist would call native. The real goal is something that looks like this:

//JCLSETUP JOB ,MSGLEVEL=(0,0),CLASS=7
//KAPRUN  EXEC PGM=JAVA,REGION=0M
//SYSIN    DD *
io:print +/ ⍳10
/*
//SYSOUT   DD SYSOUT=*

Kap source in SYSIN, results in SYSOUT, no USS, no shell, no temp files. This is how mainframe software has always worked — self-contained, auditable, reproducible. Getting there turns out to be harder than it sounds, and the obstacles reveal something interesting about the tension between z/OS’s heritage and modern JVM software.


Problem 1: BPXBATCH Consumes Stdin

The first attempt at inline JCL source used BPXBATCH with a STDIN DD * containing the Kap source. This fails because BPXBATCH’s STDPARM DD takes ownership of stdin for the shell command string itself, leaving no mechanism to pass a second DD as stdin to the shell command running inside it. Every approach to work around this — named pipes, /dev/fd redirection, multiple SH lines — runs into the same wall.

Named pipes (mkfifo) were tried but Kap’s file loader explicitly rejects them:

Not a file: /tmp/kapfifo

The check in repl.kt only accepts FileNameType.FILE, not FileNameType.PIPE. This would need a one-line fix in Kap’s source to add FileNameType.PIPE as an accepted source type, which would also open up other Unix-style piping possibilities.

Multiple SH lines in STDPARM don’t share state — each line runs as an independent shell invocation, so a file written in one SH line doesn’t reliably persist to the next. Everything has to be chained with && in a single SH line, which limits readability for longer scripts.


Problem 2: The IBM JVM Corrupts Unicode on Stdin

Even if SYSIN data could be passed cleanly to Kap, it would arrive corrupted. The IBM JVM on z/OS operates with native.encoding=IBM-1047 at the system level — below anything Java can intercept. APL symbols like ⍳ ⍴ ⌈ are multi-byte UTF-8 sequences ( is E2 8D B3) that get mangled through EBCDIC translation before Kap’s input reader sees them.

The od -x output confirmed the bytes arrive correctly over SSH:

0000000000      E28D    B331    300A

But by the time they reach the JVM’s input stream, E2 has been translated to B (its IBM-1047 equivalent), 8D to a tab character, and so on. The multi-byte sequence is destroyed.

The only reliable fix found was to use z/OS file tagging. When a file is tagged chtag -tc UTF-8, the IBM JVM reads it as UTF-8 without passing it through EBCDIC conversion. This works for files — but SYSIN data streams don’t support file tagging. There is no chtag for an inline DD.

A possible workaround would be to pre-process SYSIN through iconv before Kap sees it, but this requires USS. Another approach would be to patch Kap to detect the IBM JVM environment and explicitly open stdin with a UTF-8 InputStreamReader overriding the system encoding — but the IBM JVM intercepts even FileInputStream(FileDescriptor.in) at the native layer, as the debugging in this project confirmed.

The fundamental issue is that APL symbols have no representation in IBM-1047. Unlike languages that work with ASCII-only syntax, APL’s identity is its symbols. Until z/OS natively treats a SYSIN stream as UTF-8 — which would require either a dataset CCSID attribute on the inline DD, or a JVM property that IBM’s JVM actually honours for stdin — inline APL source in JCL will require a workaround at the encoding boundary.


Problem 3: Dataset Size Restrictions

The natural z/OS solution for distributing software is datasets. Elias Mårtenson (Kap’s author) envisions a proper z/OS distribution where the JAR lives in a dataset, Kap source scripts live in dataset members, and everything is referenced by DD statements in JCL. This is how IBM ships software, and it would make Kap feel genuinely native to z/OS.

The fat JAR produced by the Gradle Shadow plugin is 41MB. On the z/OS system used in this project, the ACS (Automatic Class Selection) storage policy restricts user dataset allocations:

IGD01007I ZXPACS: ALLOCATION SIZE (>200KB) REDUCED - ZXP SAYS NO
IGD01007I ZXPACS: ALLOCATION SPACE TYPE FOR USER DATASETS MUST BE TRK OR K

A 41MB dataset cannot be allocated by a regular user under these restrictions. A systems programmer with appropriate authority could create a storage class that permits larger allocations, or the JAR could be installed via SMP/E into a system library — which is exactly how IBM would ship a supported product. For an unofficial installation by a regular user, the JAR has to stay in USS.

The Kap source dataset is a different matter — source scripts are small. A PDSE (DSNTYPE(LIBRARY)) with RECFM(V,B) LRECL(255) works fine for Kap scripts and can be allocated within normal user limits. This part of Elias’s vision is achievable today.


Problem 4: The UTF-8 CCSID Gap for Datasets

Even if a dataset could hold Kap source, reading it as UTF-8 requires the dataset to be allocated with CCSID=1208. z/OS supports this — datasets can be tagged with a coded character set identifier at allocation time — but the JVM needs to be told to honour it, and Kap’s file loader would need to open the file with explicit UTF-8 encoding rather than relying on the system default. Currently FileSourceLocation in Kap opens files using whatever the JVM’s default charset is, which on z/OS is IBM-1047.

This is a solvable problem with a small patch — open the source file with an explicit Charsets.UTF_8 reader rather than the platform default. Combined with a properly tagged CCSID=1208 dataset, this would allow Kap source to live in a real z/OS dataset and be read correctly.


What a Proper z/OS Distribution Would Look Like

Elias’s vision, fully realised, would work like this:

Installation — a single SMP/E RECEIVE/APPLY installs the fat JAR into a system load library and the standard library into a partitioned dataset. A systems programmer does this once.

Usage — a user creates a Kap source member in their own PDS:

Z02247.KAP.SRC(MYCOMP)

And submits JCL:

//JCLSETUP JOB ,MSGLEVEL=(0,0),CLASS=7
//KAPRUN  EXEC PGM=BPXBATCH,REGION=0M
//STDPARM  DD *
SH /usr/lpp/kap/bin/kap --load=//'Z02247.KAP.SRC(MYCOMP)' --no-repl
/*
//STDOUT   DD SYSOUT=*
//STDERR   DD SYSOUT=*

Or ideally, with the stdin encoding problem solved, a truly inline form:

//JCLSETUP JOB ,MSGLEVEL=(0,0),CLASS=7
//KAPRUN  EXEC PGM=BPXBATCH,REGION=0M
//STDPARM  DD *
SH /usr/lpp/kap/bin/kap --load=- --no-repl
//STDIN    DD CCSID=1208,*
io:print +/ ⍳10
/*
//STDOUT   DD SYSOUT=*
//STDERR   DD SYSOUT=*

The CCSID=1208 on the STDIN DD would tell the system this data is UTF-8, bypassing the EBCDIC conversion. Whether the IBM JVM would honour this through its stdin handling is an open question that requires testing with IBM’s support involvement.


The io:* Functions and Dataset Integration

Kap has io:read and related functions for reading external data. In a pure z/OS context, these should be able to open dataset members as input — for example reading a CSV from Z02247.DATA.INPUT(SALES) and performing array computations on it. This would make Kap genuinely useful for the kind of batch data processing that mainframes excel at.

The USS path syntax that z/OS supports for datasets — //'dataset.name(member)' — works in Java file I/O and should work in Kap’s io:read functions without modification, since they ultimately call through to the JVM’s file handling. This has not been tested yet but is a natural next step.


Summary of Open Problems

Problem Status Likely Fix
SYSIN inline APL source Workaround (echo in SH line) Solve stdin UTF-8 at IBM JVM level
Named pipe support Blocked by Kap file checker One-line patch to repl.kt
Fat JAR in dataset Blocked by site storage policy SMP/E install or systems programmer
Dataset source with UTF-8 Untested CCSID=1208 dataset + Kap patch
io:* functions on datasets Untested Likely works via USS path syntax
Pure JCL with no USS Not achieved Requires IBM JVM stdin UTF-8 fix

The groundwork is laid. Kap runs on z/OS, produces correct results, and can be triggered from JCL. The remaining problems are encoding at the SYSIN boundary and storage policy — neither of which is insurmountable, but both of which require either IBM JVM expertise or systems programmer involvement to resolve cleanly.


The REXX Solution: Truly Native JCL Integration

After hitting the walls described above — BPXBATCH consuming stdin, the IBM JVM corrupting Unicode on input streams, dataset size restrictions — a different approach emerged that sidesteps all of them. REXX.

REXX (Restructured Extended Executor) is IBM’s scripting language, built into z/OS since the 1980s. It runs natively under TSO/batch via PGM=IKJEFT01, requires no USS shell, and has direct access to both z/OS dataset I/O and USS program invocation through the BPXWUNIX function. It turns out to be the ideal bridge between the JCL world and the Java world.


How It Works

The solution has three components:

1. A REXX exec in a dataset (Z02247.SOURCE(KAPRUN)) that:

  • Reads the Kap source from a JCL DD statement using EXECIO
  • Writes it to a USS temp file
  • Tags the temp file as UTF-8 using chtag via BPXWUNIX
  • Invokes Java directly via BPXWUNIX with no shell involved
  • Prints the output to SYSTSPRT
  • Cleans up the temp file

2. A JCL job that invokes the REXX exec via PGM=IKJEFT01 and provides the Kap source inline as a DD statement.

3. The fat JAR (kap.jar) sitting in USS, referenced by path.


The REXX Exec

/* REXX */
/* Read Kap source from KAPSRC DD into a temp file */
TMPFILE = '/tmp/kap'||RANDOM(1,99999)||'.kap'

/* Read lines from DD */
"EXECIO * DISKR KAPSRC (STEM lines. FINIS"

/* Write to USS temp file */
DO i = 1 TO lines.0
  line = lines.i
  CALL BPXWRIT TMPFILE, line||'0a'x
END

/* Tag the file as UTF-8 */
env.0 = 0
env. = ''
x = BPXWUNIX('/bin/chtag -tc UTF-8 '||TMPFILE,,'o.','e.',env.)

/* Run Kap */
cmd = '/usr/lpp/java/J17.0_64/bin/java'
cmd = cmd '-Xms64m -Xmx256m'
cmd = cmd '-Dkap.installPath=/z/z02247/kap-jvm-text'
cmd = cmd '-jar /z/z02247/kap.jar'
cmd = cmd '--load='||TMPFILE
cmd = cmd '--no-repl'
x = BPXWUNIX(cmd,,'stdout.','stderr.',env.)
DO i = 1 TO stdout.0
  SAY stdout.i
END
DO i = 1 TO stderr.0
  SAY 'ERR:' stderr.i
END

/* Cleanup */
x = BPXWUNIX('/bin/rm '||TMPFILE,,'o.','e.',env.)
EXIT x

A few things worth noting about this REXX:

RANDOM(1,99999) in the temp filename means parallel job executions won’t collide on the same file. Each job gets its own uniquely named temp file which is cleaned up at the end.

BPXWUNIX invokes USS programs directly without spawning a shell. The Java invocation goes straight to the JVM binary — no SH, no BPXBATCH, no intermediate shell process.

EXECIO * DISKR KAPSRC reads all records from the KAPSRC DD into the lines. stem variable. This is standard z/OS REXX dataset I/O — the same mechanism used by REXX execs throughout z/OS since the 1980s.

The chtag call via BPXWUNIX is the key to solving the UTF-8 encoding problem. The temp file gets tagged as UTF-8 before Kap reads it, which tells the IBM JVM to bypass its EBCDIC conversion layer.


Writing the REXX to the Dataset

The REXX source must be written to the dataset as EBCDIC — z/OS PDS members with RECFM=FB expect EBCDIC text. From USS:

iconv -f UTF-8 -t IBM-1047 kaprun.rexx > kaprun.ebc
cp kaprun.ebc "//'Z02247.SOURCE(KAPRUN)'"

The iconv step converts from UTF-8 to IBM-1047 (EBCDIC), and then cp writes it to the dataset member. If you write the file directly without iconv, the member will contain garbled bytes and the REXX will fail to execute.


The JCL

//JCLSETUP JOB ,MSGLEVEL=(0,0),CLASS=7
//KAPRUN  EXEC PGM=IKJEFT01,REGION=0M
//SYSEXEC  DD DSNAME=Z02247.SOURCE,DISP=SHR
//SYSTSPRT DD SYSOUT=*
//KAPSRC   DD *
io:print +/ ⍳10
/*
//SYSTSIN  DD *
EXEC 'Z02247.SOURCE(KAPRUN)'
/*

This is genuinely native z/OS JCL. Breaking it down:

  • PGM=IKJEFT01 — the TSO terminal monitor program, a standard z/OS load module that runs REXX execs natively
  • SYSEXEC DD — the dataset containing the REXX exec
  • SYSTSPRT DD SYSOUT=* — where REXX SAY output goes, which is where the Kap results appear
  • KAPSRC DD * — the Kap source code, inline in the JCL
  • SYSTSIN DD * — the TSO command stream, invoking the REXX exec by name

To run a different APL computation, only the KAPSRC DD * section changes:

//KAPSRC   DD *
a ← ⍳10
io:print +/a
io:print ×/a
/*

The REXX exec, the Java invocation, the UTF-8 tagging — all of that is invisible to the person writing the JCL. They see only their APL source and their results.


Why REXX Works Where Other Approaches Failed

The REXX solution resolves every problem from the previous section:

BPXBATCH stdin problem — eliminated. IKJEFT01 runs the REXX exec, which reads the Kap source from the KAPSRC DD using EXECIO. There is no stdin contention.

IBM JVM Unicode corruption — worked around. The REXX writes to a temp file and calls chtag on it before Kap reads it. The tagged file bypasses the JVM’s EBCDIC conversion layer entirely.

No shell involvedBPXWUNIX invokes the Java binary directly. There is no SH, no /bin/sh, no shell process of any kind between the JCL and the JVM.

Dataset size restrictions — irrelevant. The fat JAR stays in USS where there are no size limits. Only the REXX exec (a few hundred bytes) lives in a dataset.

Temp file cleanup — handled. The REXX deletes the temp file after Kap exits, using another BPXWUNIX call to /bin/rm. The temp file exists only for the duration of the job.


What Remains a Workaround

The temp file itself is still technically a workaround for the stdin encoding problem. In an ideal world the Kap source would flow directly from SYSIN into Kap’s parser as UTF-8 without ever touching the filesystem. That would require either:

  • IBM fixing or exposing a way to mark an in-memory data stream as UTF-8 before the JVM reads it, or
  • Kap reading the source from a REXX-passed string rather than a file — which would require a Kap API change to accept source as a string argument

Neither is impossible, but both require deeper integration work. For practical purposes, the temp file approach is clean enough — it’s an implementation detail invisible to the JCL author, and the file is always cleaned up.


The Final Architecture

What started as “can we run APL on a mainframe” ended up as a layered solution that respects the z/OS execution model:

JCL job stream
  └── IKJEFT01 (TSO batch, native z/OS)
        └── REXX exec in Z02247.SOURCE(KAPRUN)
              ├── EXECIO reads KAPSRC DD
              ├── Writes UTF-8 tagged temp file (USS)
              └── BPXWUNIX invokes Java directly
                    └── kap.jar (fat JAR in USS)
                          └── Kap interpreter
                                └── APL computation
                                      └── Result → SYSTSPRT

Each layer is doing what it was designed to do. JCL orchestrates. REXX bridges the dataset and USS worlds. BPXWUNIX invokes the USS binary without a shell. Java runs the Kap interpreter. The result comes back through SYSTSPRT like any other batch job output.

APL on the mainframe, done properly.

APL came from mainframes. Getting it fully back — not just running but feeling native — turns out to require negotiating sixty years of encoding decisions.


Kap is developed by Elias Mårtenson and is available at codeberg.org/loke/array under an open source license.


This site uses Just the Docs, a documentation theme for Jekyll.