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.


What’s Next

With Kap running on z/OS you have access to the full array language feature set for batch processing — reshaping, reducing, scanning, outer products, and all the APL primitives — directly from JCL jobs. Results go to SYSOUT and can be captured like any other job output.

APL started on mainframes. Now it’s back.


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