Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

patch-cms

Documentation

A Rust reimplementation of the IBM VM/CMS environment — XEDIT editor, CMS file system, REXX scripting, spool subsystem, Hartmann pipelines, and VM inter-machine messaging.

Overview

VM/CMS was IBM’s interactive mainframe operating system — a single-user virtual machine with a powerful command environment, a programmable full-screen editor (XEDIT), and REXX as its scripting language. This project recreates those semantics in modern Rust as a set of embeddable libraries with a terminal UI.

The REXX interpreter lives in a companion project, patch-rexx.

See ROADMAP.md for the full vision and current progress.

Workspace Structure

patch-cms/
├── crates/
│   ├── xedit-core/          # Editor model — pure logic, no I/O dependencies
│   ├── xedit-tui/           # Terminal UI — 3270-style block-mode rendering
│   ├── cms-core/            # CMS file system (fn ft fm), commands, EXEC processor
│   ├── cms-spool/           # Reader/punch/printer spool subsystem
│   ├── cms-pipelines/       # Hartmann pipelines
│   ├── vm-iucv/             # Inter-machine messaging (actor framework)
│   └── cms-machine/         # Interactive CMS machine binary

xedit-core is a standalone library with zero I/O dependencies. The editor is a pure state machine driven by commands, making it embeddable in other applications. With the rexx feature enabled, macros can query and drive the editor via EXTRACT variables and ADDRESS XEDIT commands.

xedit-tui provides the interactive terminal experience: prefix area editing, command line, PF keys, and screen editing with overtype/insert modes.

cms-core implements the CMS file system (fn ft fm naming), command processor with IBM-style abbreviation matching, GLOBALV variable storage, and EXEC resolution. Trait seams (ExecHandler, SmsgSender, ExtCommandHandler) decouple it from the REXX interpreter, actor framework, and extension commands.

cms-spool provides virtual reader/punch/printer queues with SPOOL, QUERY, PURGE, SENDFILE, and RECEIVE commands.

cms-pipelines implements Hartmann pipelines — the PIPE command with built-in stages (literal, console, locate, nlocate) and a two-pass executor.

vm-iucv is a Tokio-based actor framework modeled after VM/CMS inter-machine communication: Supervisor lifecycle management, SMSG messaging, and IUCV bidirectional data paths.

cms-machine wires everything together into an interactive CMS console with REXX scripting, spool commands, and pipelines — all programmable from the REPL.

Building and Running

# Build the workspace
cargo build --workspace

# Run the TUI editor
cargo run -p xedit-tui -- <filename>

# Run the interactive CMS machine
cargo run -p cms-machine -- --userid ALICE --disk /tmp/cms

# Run all tests
cargo test --workspace

Current Status

906 tests passing, zero clippy warnings.

Phases 1-13 complete. The editor is fully functional with REXX macros, CMS file system, spool subsystem, Hartmann pipelines, actor-based inter-machine messaging, and an interactive CMS console. Everything is programmable from REXX/REPL.

License

MIT — Ed Sweeney, 2026

Quickstart

Launch a CMS Machine

# Clone and build
git clone https://github.com/navicore/patch-cms.git
cd patch-cms
cargo build -p cms-machine --release

# Create a disk directory and launch
mkdir -p /tmp/cms/a
cargo run -p cms-machine -- --userid ALICE --disk /tmp/cms

You get an interactive CMS prompt with REXX scripting, spool commands, pipelines, and inter-machine messaging.

Your First REXX Program

Create a file /tmp/cms/a/GREET.exec:

/* REXX — send a greeting to another machine */
parse arg userid .
if userid = '' then do
    say 'Usage: GREET userid'
    exit 24
end
'SMSG' userid 'Hello from CMS!'
if rc = 0 then say 'Sent.'
else say 'Failed, RC='rc

At the CMS prompt:

GREET BOB

Any file named *.exec on your A-disk is callable as a command.

Try the Built-in Commands

GLOBALV SET COLOR blue        Set a persistent variable
GLOBALV GET COLOR             Retrieve it
PIPE literal hello | console  Run a pipeline
SP PRT CLASS B                Configure the printer spool
QUERY PRT                    Show printer queue
LISTFILE * EXEC A            List all EXECs on A-disk

Persistent State with GLOBALV

REXX EXECs get a fresh interpreter each time, but GLOBALV variables persist across invocations:

/* COUNTER EXEC */
'GLOBALV SELECT COUNTER'
'GLOBALV GET COUNT'
if rc \= 0 then count = 0
count = count + 1
'GLOBALV SET COUNT' count
say 'Counter:' count

Run COUNTER multiple times — the value increments.

Composing EXECs

EXECs can call other EXECs:

/* DISPATCH EXEC */
do i = 1 to 3
    'EXEC COUNTER'
end
'EXEC GREET BOB'

Embedding the Library (Rust API)

For embedding vm-iucv as a Rust library:

[dependencies]
vm-iucv = { git = "https://github.com/navicore/patch-cms" }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
use vm_iucv::handler::{MachineContext, MachineHandler};
use vm_iucv::machine_id::MachineId;
use vm_iucv::message::SmsgMessage;
use vm_iucv::supervisor::Supervisor;

struct PrintHandler;

impl MachineHandler for PrintHandler {
    fn on_smsg(&mut self, _ctx: &MachineContext, msg: SmsgMessage) {
        println!("Received: {}", msg.text());
    }
}

#[tokio::main]
async fn main() {
    let sup = Supervisor::new();
    let alice = MachineId::new("ALICE").unwrap();
    let bob = MachineId::new("BOB").unwrap();

    sup.ipl(&alice, vm_iucv::collector::collector().0).await.unwrap();
    sup.ipl(&bob, PrintHandler).await.unwrap();

    sup.smsg(&alice, &bob, "Hello from ALICE!").await.unwrap();
    tokio::time::sleep(std::time::Duration::from_millis(50)).await;

    sup.logoff(&alice).await.unwrap();
    sup.logoff(&bob).await.unwrap();
    sup.shutdown().await;
}

See the Examples page for more Rust library examples (echo server, IUCV chat, connection gating, multi-machine pipeline).

Next steps

Examples

REXX Examples

These examples run inside the CMS machine interactive console. Launch a machine and try them at the prompt:

mkdir -p /tmp/cms/a
cargo run -p cms-machine -- --userid ALICE --disk /tmp/cms

The example EXECs are in crates/cms-machine/execs/ and are loaded automatically from the A-disk.

GREET — Send an SMSG

Send a greeting message to another machine. Demonstrates SMSG from REXX with return code checking.

/* GREET EXEC — at the CMS prompt: GREET BOB */
parse arg userid .
if userid = '' then do
    say 'Usage: GREET userid'
    exit 24
end

say 'Sending greeting to' userid '...'
'SMSG' userid 'Hello from CMS!'
if rc = 0 then
    say 'Greeting sent successfully.'
else
    say 'Could not reach' userid '— RC='rc
exit rc
Ready; T=0.01/0.01
GREET BOB
Sending greeting to BOB ...
Greeting sent successfully.

COUNTER — Persistent State with GLOBALV

A counter that survives across EXEC invocations. Demonstrates GLOBALV groups for persistent state management.

/* COUNTER EXEC */
parse upper arg action .
'GLOBALV SELECT COUNTER'

if action = 'RESET' then do
    'GLOBALV SET COUNT 0'
    say 'Counter reset to 0.'
    exit 0
end

'GLOBALV GET COUNT'
if rc \= 0 then count = 0

count = count + 1
'GLOBALV SET COUNT' count
say 'Counter:' count
exit 0
Ready; T=0.01/0.01
COUNTER
Counter: 1
Ready; T=0.01/0.01
COUNTER
Counter: 2
Ready; T=0.01/0.01
COUNTER RESET
Counter reset to 0.

UPPER — Data Transformation with PIPE

Uses CMS Pipelines to transform text. Demonstrates the PIPE command from REXX.

/* UPPER EXEC */
parse arg text
if text = '' then do
    say 'Usage: UPPER some text here'
    exit 24
end
'PIPE literal' text '| console'
Ready; T=0.01/0.01
UPPER hello world
hello world

SPOOLQ — Spool Queue Status

Queries the virtual reader, printer, and punch queues. Demonstrates spool commands from REXX.

/* SPOOLQ EXEC */
parse upper arg device .
if device = '' then do
    say '--- Reader ---'
    'QUERY RDR'
    say ''
    say '--- Printer ---'
    'QUERY PRT'
    say ''
    say '--- Punch ---'
    'QUERY PUN'
end
Ready; T=0.01/0.01
SPOOLQ
--- Reader ---
No spool files.

--- Printer ---
No spool files.

--- Punch ---
No spool files.

DISPATCH — Multi-EXEC Composition

Composes COUNTER, GREET, and SPOOLQ into a single workflow. Demonstrates REXX EXECs calling other EXECs.

/* DISPATCH EXEC */
parse arg userid .

say '=== Running COUNTER three times ==='
do i = 1 to 3
    'EXEC COUNTER'
end

if userid \= '' then do
    say ''
    say '=== Sending greeting to' userid '==='
    'EXEC GREET' userid
end

say ''
say '=== Spool status ==='
'EXEC SPOOLQ'

say ''
say '=== Done ==='
exit 0
Ready; T=0.01/0.01
DISPATCH BOB
=== Running COUNTER three times ===
Counter: 1
Counter: 2
Counter: 3

=== Sending greeting to BOB ===
Sending greeting to BOB ...
Greeting sent successfully.

=== Spool status ===
--- Reader ---
No spool files.
...

=== Done ===

Rust Library Examples

These examples demonstrate the vm-iucv library API for embedding the actor framework in your own Rust programs. They require Rust and are run with cargo:

hello_smsg

Basic machine lifecycle: IPL two machines, send an SMSG, shut down.

cargo run -p vm-iucv --example hello_smsg --features examples

echo_server

Request/reply pattern: an ECHO machine replies to every SMSG with the same text. Uses ctx.try_send_smsg() inside a handler callback.

cargo run -p vm-iucv --example echo_server --features examples

multi_machine_pipeline

Multi-machine coordination: PRODUCER → TRANSFORM → SINK chain where each machine forwards modified messages via SMSG.

cargo run -p vm-iucv --example multi_machine_pipeline --features examples

connection_gating

IUCV connection security: a SECURE machine accepts connections only from an allowlist. Demonstrates on_connection_pending callback.

cargo run -p vm-iucv --example connection_gating --features examples

iucv_chat

Full IUCV path lifecycle: CONNECT → ESTABLISHED → SEND → SEVER between two machines.

cargo run -p vm-iucv --example iucv_chat --features examples

Design Principles

This section documents the architectural decisions behind patch-cms – the why, not the what. The ROADMAP tracks features; these pages explain the reasoning future maintainers should understand before changing things.

Core Tenets

  1. Embeddable first. xedit-core has zero I/O dependencies. The editor is a pure state machine driven by commands so it can be embedded in any Rust application without pulling in a terminal library, an async runtime, or a filesystem.

  2. Trait seams, not concrete coupling. Every major subsystem boundary is a trait with a default no-op implementation. Crates depend on traits, never on sibling crate internals. This keeps the dependency graph shallow and lets each crate be tested and used in isolation.

  3. Faithful VM/CMS semantics. Abbreviation rules, command syntax, RC codes, prefix area behavior, and the REXX execution model follow IBM documentation. Where the real system’s behavior is well-documented, we match it; where it isn’t, we pick the simplest defensible interpretation.

  4. Modern Rust idioms. Zero unsafe blocks across the entire workspace. Strong types, exhaustive pattern matching, and comprehensive tests. The codebase should be approachable for Rust developers who have never touched a mainframe.

  5. Incremental delivery. Each phase produces something usable on its own. The editor works without CMS; CMS works without the spool; the actor framework works without either.

Design Documents

  • Why Rust – language choice and what it gives us
  • REXX Integration – bridging a 1979 scripting language with Rust ownership
  • Async Architecture – the actor model, sync/async boundary, and Tokio usage
  • Trait Seams – the decoupling strategy and crate dependency graph

Why Rust

The Choice

VM/CMS is a systems programming environment. Recreating it requires precise control over memory layout, string handling, and concurrency – the same concerns that made the original system a C and assembler project. Rust gives us that control without the footguns.

What Rust Gives Us

Zero-Cost Abstractions for Editor Internals

xedit-core is a pure state machine. Every command is a function from (EditorState, Command) -> (EditorState, Result). Rust’s enum-based command representation and exhaustive match mean we can add a new command and the compiler tells us every place that needs updating. There is no dynamic dispatch in the hot path.

Ownership Makes Embeddability Real

The Editor struct owns its buffer, settings, and undo stack. There are no global singletons, no thread-local state, and no hidden allocations. You can create ten editors in one process, each with its own filesystem adapter, and they will not interfere. This would be painful to guarantee in C or C++ and impossible to enforce in a garbage-collected language.

Fearless Concurrency in the Actor Framework

vm-iucv runs each machine as an isolated Tokio task. Messages flow through typed mpsc channels. The type system enforces that a MachineHandler callback cannot access another machine’s state – there is no lock to forget, because there is no shared mutable state to lock. The only synchronization is in the Supervisor, which holds an Arc<RwLock<HashMap>> of machine entries.

Feature Flags for Optional Coupling

Cargo features let us gate expensive dependencies. The rexx feature pulls in patch-rexx (~15K lines); without it, xedit-core compiles in under a second and has no scripting support. The cms feature in xedit-tui pulls in the CMS file system. This keeps compile times reasonable and lets downstream users take only what they need.

Safe FFI Boundary (Future)

When we eventually expose a C API for embedding, Rust’s #[no_mangle] and extern "C" give us a clean FFI surface. The zero-unsafe policy means the only unsafe code will be at that boundary, making it easy to audit.

What We Avoid

  • No unsafe anywhere. The entire workspace compiles without a single unsafe block. We use std::mem::swap instead of pointer tricks, Rc<RefCell<>> instead of raw pointers for shared mutable state in macro execution, and Tokio’s typed channels instead of lock-free queues.

  • No runtime reflection. Command dispatch uses Rust enums, not string-keyed maps. Abbreviation lookup uses a static table built at compile time.

  • No global state. Every subsystem is instantiated explicitly. CommandProcessor, SpoolManager, Supervisor – all are owned values passed by reference. This makes testing trivial: each test constructs its own world.

REXX Integration

The Problem

REXX is a 1979 scripting language designed for interactive mainframe use. It assumes it owns the process, can call any system command by name, and shares mutable state freely with its host. Rust’s ownership model is the opposite of all of that.

Feature-Gated Dependency

REXX support is behind the rexx feature flag in both xedit-core and cms-machine. Without it, the crates compile with no scripting support and NoExecHandler stubs return RC 28 (“not found”) for any EXEC invocation. This keeps the core editor embeddable without pulling in the full REXX interpreter.

# In xedit-core/Cargo.toml
[features]
rexx = ["dep:patch-rexx"]

The REXX interpreter itself lives in patch-rexx, a separate repository (~15K lines, ANSI-compliant).

Two Integration Points

ADDRESS XEDIT (Editor Macros)

When a REXX macro runs inside the editor, it needs to:

  1. Read editor state via EXTRACT variables (CURLINE, SIZE, FNAME, etc.)
  2. Issue editor commands via bare string expressions like 'COMMAND LOCATE /foo/'

The bridge lives in xedit-core/src/macro_engine.rs. Before execution, we populate REXX compound variables with a snapshot of editor state. A custom set_command_handler closure intercepts bare string expressions and routes them to the editor’s command processor.

State snapshot, not live binding. EXTRACT variables are populated once at macro start. If the macro changes the buffer (e.g., via CHANGE), the EXTRACT values do not update mid-execution. This is a known limitation documented in the ROADMAP. Use QUERY for fresh state.

ADDRESS CMS (System Commands)

When REXX runs in the CMS machine context, it needs to call CMS commands (LISTFILE, PIPE, spool operations). This is wired through the ExecHandler and ExtCommandHandler traits:

patch-rexx (interpreter)
    |
    v
CmsRexxExecHandler (cms-machine) -- implements ExecHandler trait
    |
    v
CommandProcessor (cms-core) -- dispatches commands
    |
    v
ExtCommandHandler (cms-core trait) -- extension point
    |
    v
CmsExtCommandHandler (cms-machine) -- routes PIPE, spool commands

Each layer depends only on the trait above it, never on a concrete type from another crate.

The State Swap Pattern

REXX macros in CMS mode need access to the CMS filesystem and GLOBALV variables. But CommandProcessor owns that state, and the REXX interpreter needs it too during execution.

CmsRexxExecHandlerWithSwap solves this with explicit ownership transfer:

  1. Before EXEC: provide_state(fs, globalv) – CommandProcessor gives its filesystem and variable state to the handler
  2. During EXEC: REXX runs with full access to CMS files and variables
  3. After EXEC: retrieve_state() – handler returns the (potentially modified) state back to CommandProcessor

This avoids Arc<Mutex<>> and keeps the borrow checker happy. The state is never shared – it moves from owner to owner.

SMSG from REXX

The SmsgSender trait lets REXX programs send inter-machine messages:

#![allow(unused)]
fn main() {
pub trait SmsgSender {
    fn send_smsg(&self, target: &str, text: &str) -> i32;
}
}

ChannelSmsgSender (in cms-machine) implements this by enqueuing messages to the vm-iucv router channel. This bridges the synchronous REXX execution context with the asynchronous actor framework without blocking the Tokio runtime.

Design Constraints

  • No persistent REXX state between EXECs. Each EXEC invocation gets a fresh interpreter instance. Persistent state goes through GLOBALV.

  • No concurrent REXX execution. A CMS machine processes one command at a time, including REXX EXECs. This matches real CMS behavior and avoids the need to make the interpreter thread-safe.

  • REXX is always optional. Any crate that works with REXX also works without it. The trait seam pattern makes this possible without #[cfg] spaghetti – the no-op implementation handles the feature-off case.

Async Architecture

The Model

VM/CMS is inherently concurrent: multiple virtual machines run independently, each processing commands sequentially, while exchanging messages asynchronously. We model this with Tokio’s task-per-machine actor pattern.

Supervisor and Machine Tasks

The Supervisor (in vm-iucv) is the “Control Program.” It manages machine lifecycles and message routing.

Supervisor
├── machines: Arc<RwLock<HashMap<MachineId, MachineEntry>>>
├── router_task      -- routes SMSG between machines
├── path_cmd_task    -- handles IUCV path lifecycle
└── per-machine tasks:
    └── run_machine(handler, ctx, signal_rx)

Each machine is a Tokio task running a simple receive loop:

#![allow(unused)]
fn main() {
async fn run_machine(mut handler, ctx, mut signal_rx) {
    handler.on_ipl(&ctx);

    while let Some(signal) = signal_rx.recv().await {
        match signal {
            MachineSignal::Smsg(msg) => handler.on_smsg(&ctx, msg),
            MachineSignal::Logoff   => break,
            // ... IUCV signals ...
        }
    }

    handler.on_logoff(&ctx);
}
}

Key decision: synchronous callbacks on async tasks. The MachineHandler trait methods are synchronous (&mut self, not async). This means:

  • Handlers cannot .await inside callbacks
  • Handler state is never shared across tasks (no Send + Sync bound on fields)
  • The machine task yields to the runtime only between signals

This matches real CMS semantics – a virtual machine processes one event at a time – and keeps handler implementations simple.

Message Routing

All inter-machine communication flows through typed channels:

Machine A                          Machine B
    |                                  |
    | ctx.try_send_smsg("B", text)     |
    |                                  |
    v                                  |
 smsg_tx ──> router_task ──> signal_tx ──> signal_rx
                                       |
                                       v
                                  on_smsg(msg)

The router uses try_send (non-blocking) to dispatch. If a machine’s signal channel is full, the message is dropped. This is fire-and-forget by design – it prevents one slow machine from blocking the entire system.

The Sync-Async Bridge (Console)

The CMS interactive console poses a challenge: stdin is blocking I/O, but the machine handler runs on an async task. The bridge works like this:

[blocking thread]           [async task]
     stdin                       |
       |                         |
  read line                      |
       |                         |
  cmd_tx.send(line) ──────> drain_commands()
       |                    (in on_smsg callback)
  $CON SMSG wakes machine       |
       |                    execute command
  wait for BATCH_DONE           |
       |                    output_tx.send(lines)
  print output <────────── BATCH_DONE sentinel
  1. The console thread reads stdin and sends commands via std::sync::mpsc (not Tokio’s – it runs on a blocking thread)
  2. It wakes the machine by sending an SMSG from the $CON pseudo-machine
  3. The handler’s on_smsg callback drains the command channel and executes commands synchronously
  4. Output lines flow back via a channel, terminated by a BATCH_DONE sentinel
  5. The console thread collects output until it sees the sentinel

The sentinel-based protocol avoids timeouts and polling. It is deterministic: the console knows exactly when the machine is done.

Why Not Async Handlers?

We considered making MachineHandler methods async. Reasons we didn’t:

  1. CMS is sequential. A real CMS machine never processes two commands concurrently. Async handlers would add complexity for a capability we don’t want.

  2. Handler state stays simple. With sync callbacks, handler fields can be plain Vec, HashMap, etc. Async would require Send + Sync bounds or spawn_local, adding friction for every handler implementation.

  3. Outbound messaging is already async. ctx.try_send_smsg() enqueues to a channel that the router processes asynchronously. The handler doesn’t need to await the delivery.

  4. Blocking work is bounded. CMS commands (LISTFILE, CHANGE, etc.) are fast in-memory operations. There is no disk I/O or network call that would benefit from yielding mid-command.

If a future use case requires long-running async work inside a handler, the recommended pattern is: spawn a separate Tokio task and communicate results back via SMSG.

Tokio Usage

  • Runtime: rt-multi-thread in cms-machine’s main binary; vm-iucv itself only requires rt and sync
  • Channels: tokio::sync::mpsc for machine signals and router queues; std::sync::mpsc for the blocking console bridge
  • Locks: tokio::sync::RwLock for the machine registry (read-heavy, rarely written)
  • No timers in the core. Timeouts exist only as safety nets in the console bridge, not as part of the actor protocol

Trait Seams

The Strategy

Every major boundary between crates is a Rust trait with a default no-op implementation. Crates depend on traits defined in their own crate or in a leaf crate, never on sibling crate internals. This keeps the dependency graph shallow and allows each crate to be compiled, tested, and used independently.

The Traits

FileSystem (xedit-core)

#![allow(unused)]
fn main() {
pub trait FileSystem {
    fn read_file(&self, file_id: &str) -> Result<String>;
    fn write_file(&self, file_id: &str, content: &str) -> Result<()>;
    fn parse_file_id(&self, file_id: &str) -> Option<FileIdentity>;
}
}

Defined in xedit-core. The editor calls this trait – it never touches std::fs directly.

Implementations:

  • NativeFs (xedit-core) – delegates to std::fs, used in standalone XEDIT mode
  • CmsFs (cms-core) – maps fn ft fm file identifiers to the CMS minidisk filesystem

This is the core embeddability seam. A game engine, a web IDE, or a test harness can implement FileSystem and get a working XEDIT editor without any filesystem at all.

MachineHandler (vm-iucv)

#![allow(unused)]
fn main() {
pub trait MachineHandler: Send + 'static {
    fn on_ipl(&mut self, ctx: &MachineContext) {}
    fn on_smsg(&mut self, ctx: &MachineContext, msg: SmsgMessage);
    fn on_logoff(&mut self, ctx: &MachineContext) {}
    // ... IUCV connection callbacks with defaults ...
}
}

Defined in vm-iucv. The Supervisor calls these callbacks – it knows nothing about CMS, editors, or files.

Implementations:

  • CmsMachineHandler (cms-machine) – wraps a CommandProcessor and bridges console I/O
  • CollectorHandler (vm-iucv, test utility) – collects messages for assertions
  • Example handlers in vm-iucv/examples/ – echo servers, pipeline stages, connection gating

Stage (cms-pipelines)

#![allow(unused)]
fn main() {
pub trait Stage {
    fn initialize(&mut self, _output: &mut dyn StageSink) {}
    fn process(&mut self, record: &str, output: &mut dyn StageSink);
    fn finish(&mut self, _output: &mut dyn StageSink) {}
    // ...
}
}

Defined in cms-pipelines. The pipeline executor drives stages through initialize → process* → finish without knowing what they do.

Built-in implementations: literal, console, locate, nlocate

SpoolBackend (cms-spool)

#![allow(unused)]
fn main() {
pub trait SpoolBackend {
    fn enqueue(&mut self, ...) -> Result<SpoolId>;
    fn dequeue(&mut self, ...) -> Result<Option<SpoolEntry>>;
    fn list_queue(&self, ...) -> Vec<SpoolEntry>;
    fn purge(&mut self, ...) -> Result<()>;
    // ...
}
}

Defined in cms-spool. The SpoolManager delegates storage to a backend.

Implementations:

  • InMemoryBackend (cms-spool) – for testing and transient use
  • DirectoryBackend (cms-spool) – .meta/.data file pairs on disk

ExecHandler, SmsgSender, ExtCommandHandler (cms-core)

Three small traits that let cms-core’s CommandProcessor call out to REXX, messaging, and extension commands without depending on their implementations:

#![allow(unused)]
fn main() {
pub trait ExecHandler {
    fn execute(&mut self, name: &str, args: &str, ...) -> i32;
}

pub trait SmsgSender {
    fn send_smsg(&self, target: &str, text: &str) -> i32;
}

pub trait ExtCommandHandler {
    fn handle(&mut self, command: &str, args: &str, ...) -> Option<i32>;
}
}

Each has a No* default (e.g., NoExecHandler) that returns a failure RC. cms-machine provides the real implementations.

Crate Dependency Graph

                    xedit-core
                   /          \
              xedit-tui     cms-core
                   \          /
                    \        /
     cms-spool       \      /      cms-pipelines
          \           \    /           /
           \       cms-machine        /
            \_____|____|_____________/
                  |
               vm-iucv

Leaf crates (no workspace dependencies):

  • vm-iucv – depends only on Tokio
  • cms-spool – depends on nothing (only tempfile for tests)
  • cms-pipelines – depends on nothing

Middle crates:

  • xedit-core – optionally depends on patch-rexx
  • cms-core – depends on xedit-core (for FileSystem trait)

Composition crate:

  • cms-machine – pulls everything together, provides main()

This hierarchy means you can use vm-iucv as a standalone actor framework, cms-pipelines as a standalone data pipeline library, or xedit-core as a standalone editor model, without dragging in the rest of the workspace.

Adding a New Seam

When introducing a new subsystem boundary:

  1. Define the trait in the crate that consumes it (not the one that implements it)
  2. Provide a No* default implementation that returns a sensible failure code
  3. Wire the real implementation in cms-machine, where all crates meet
  4. Gate the dependency behind a Cargo feature if it’s heavyweight

Overview

cms-core implements the CMS (Conversational Monitor System) layer of the VM/CMS reimplementation. It provides the file system, command processor, session variables (GLOBALV), and EXEC resolution – the foundation that higher-level crates (cms-spool, cms-pipelines, cms-machine) build on.

Key Concepts

CMS File System

Files are identified by a three-part name: filename, filetype, and filemode (fn ft fm). The filemode letter (A-Z) selects a minidisk; the wildcard * searches all accessed disks in alphabetical order.

CmsFileSystem manages a BTreeMap<char, Minidisk> and exposes CMS-style operations: read, write, erase, rename, copy, list, and state (existence check). Disks are accessed with a mode (read-only or read-write) via the ACCESS command and released with RELEASE.

CommandProcessor

The central dispatch engine. It owns the file system, GLOBALV store, an ExecHandler, an ExtCommandHandler, and an optional SmsgSender. All command execution flows through CommandProcessor::execute(input).

GLOBALV

GlobalVars provides session-scoped named variables organized into groups (default group: LASTING). Group and variable names are uppercased; values are stored as-is. Supports SELECT, SET, GET, LIST, DELETE, and PURGE sub-commands, matching IBM CMS GLOBALV semantics.

EXEC Resolution

When the command processor encounters an unknown command, it searches for a file named <COMMAND> EXEC * across all accessed disks. If found, the file is handed to the ExecHandler for interpretation (typically REXX via patch-rexx).

Trait Seams

The crate uses trait-based seams to stay decoupled from the REXX interpreter, the actor framework, and extension command sets.

TraitPurposeDefault (no-op)
ExecHandlerRun REXX EXEC files; swap fs/gv/smsg state in and outNoExecHandler (RC=28)
SmsgSenderSend SMSG messages to other virtual machinesNoSmsgSender (RC=28)
ExtCommandHandlerHandle commands outside cms-core (spool, pipelines)NoExtCommands (None)

ExecHandler also defines provide_state / retrieve_state pairs for both the file system + GLOBALV and the SMSG sender, enabling the REXX interpreter to issue CMS commands that mutate shared state during EXEC execution.

The execute() Flow

execute(input)
  |
  +-- parse_cms_command(input)
  |     |
  |     +-- OK(cmd)  -->  dispatch(cmd)   // built-in command
  |     |
  |     +-- Err(UnknownCommand)
  |           |
  |           +-- ext_handler.try_execute(input)
  |           |     |
  |           |     +-- Some(rc, msgs)  -->  return result
  |           |     +-- None            -->  fall through
  |           |
  |           +-- try_exec_fallback()   // search <CMD> EXEC *
  |
  +-- Err(other)  -->  RC=24, error message
  1. Parseparse_cms_command tokenizes the input line, matches the first word against the abbreviation table, and returns a CmsCommand enum.
  2. Built-in – Known commands (LISTFILE, STATE, COPYFILE, ERASE, RENAME, GLOBALV, ACCESS, RELEASE, EXEC, SMSG) are dispatched directly.
  3. Extension – If parsing yields UnknownCommand, the ExtCommandHandler gets first crack. This is how cms-spool and cms-pipelines inject their commands without modifying cms-core.
  4. EXEC fallback – If the extension handler returns None, the processor searches for <CMD> EXEC * on disk and runs it through the ExecHandler.

Abbreviation Matching

Commands follow IBM CMS abbreviation conventions. Each command has a minimum abbreviation length defined in CMS_COMMAND_TABLE:

CommandMin. AbbrevExample
ACCESS2AC
COPYFILE4COPY
ERASE2ER
EXEC4EXEC
GLOBALV4GLOB
LISTFILE4LIST
RELEASE3REL
RENAME3REN
SMSG2SM
STATE2ST

lookup_command tries an exact match first, then checks whether the input is at least min_abbrev characters long and is a prefix of the full command name. Ambiguous abbreviations (matching more than one command) return no match.

Overview

The cms-spool crate implements the VM/CMS virtual spool subsystem. It models the unit-record device queues – reader, punch, and printer – that CMS virtual machines use to exchange files and produce printed output.

Spool Concepts

Devices

Three virtual unit-record devices are supported, mirroring the IBM VM/CMS model:

DeviceAliasesPurpose
ReaderRDR, RIncoming files from other users
PrinterPRT, PROutput destined for printing
PunchPUN, PCH, PUCard-punch output

Classes

Each device has an associated spool class (a single uppercase letter, A-Z). Classes control routing and filtering – for example, QUERY READER CLASS N lists only class-N reader files.

Spool IDs

Every file placed in a spool queue is assigned a unique numeric spool ID. The ID is used by PURGE to target individual files for removal.

Commands

All commands use CMS-style abbreviation matching (minimum unique prefix).

CommandMin AbbrevExampleDescription
SPOOLSPSP PRT CLASS A DEST OPERATORConfigure a device’s class, destination, hold, continuous, or copy count
SENDFILESESE MYFILE DATA A TO JONESSend a file to another user’s reader
RECEIVERECREC NEWNAME DATA ADequeue the next file from your reader
QUERYQQ READERList files waiting in a device queue
PURGEPURPUR READER 12345 / PUR RDR ALLRemove one or all files from a queue

These are represented in code by the SpoolCommand enum in command.rs, which is parsed from raw token vectors via SpoolCommand::parse().

SpoolManager and the Backend Trait

SpoolManager<B> is the main entry point for spool operations. It is generic over the SpoolBackend trait, which abstracts the storage layer:

pub trait SpoolBackend {
    fn enqueue(...) -> Result<u64>;
    fn dequeue(device) -> Result<(SpoolFile, String)>;
    fn list_queue(device, class) -> Result<Vec<SpoolFile>>;
    ...
}

Two backend implementations are provided:

  • InMemoryBackend – hash-map-based storage used in tests.
  • DirectoryBackend – filesystem-backed storage using per-device subdirectories (rdr/, prt/, pun/) with .meta sidecar files.

SpoolManager owns per-device DeviceConfig structs that track the current class, destination, hold, and copy-count settings. The SPOOL command modifies these configs; SENDFILE and RECEIVE use them when enqueuing or dequeuing files.

SpoolCommandResult carries a return code and message list back to the caller, following CMS conventions (rc 0 = success).

Integration with CMS

The spool subsystem plugs into cms-core’s CommandProcessor through the ExtCommandHandler trait. When the command processor encounters an unrecognized command, it delegates to registered external handlers. The spool handler matches SPOOL, SENDFILE, RECEIVE, QUERY, and PURGE, parses them into SpoolCommand variants, and executes them against the SpoolManager. This keeps the spool logic decoupled from the core CMS command loop while still appearing as first-class CMS commands to the user.

Overview

The cms-pipelines crate implements Hartmann pipelines – the VM/CMS mechanism for composing data transformations as a series of stages connected by pipes.

Pipeline Syntax

Pipelines are invoked with the PIPE command. Stages are separated by |:

PIPE literal Hello world | console
PIPE literal abc | locate /b/ | console
PIPE literal secret | nlocate /secret/ | console

Each stage reads records from its input, processes them, and writes records to its output. The first stage in a pipeline is a source (no input); the last is typically a sink.

Built-in Stages

StagePurpose
literalEmits its argument as a single output record
consoleWrites each input record to console output
locatePasses records containing a given string
nlocatePasses records NOT containing a given string

The Stage trait can be implemented to add custom stages.

Two-Pass Executor

The pipeline executor runs in two passes:

  1. Initialize – Each stage is created and connected to its neighbors via input/output channels.
  2. Process + Finish – Records flow through the pipeline. Each stage processes input records and produces output. When the source is exhausted, stages receive a finish signal to flush any buffered state.

Multi-Stream Support

Stages can produce output on primary and secondary streams. The return code from a stage determines which output stream receives the record:

RCMeaning
0Record written to primary output
4Record written to secondary output (e.g., non-matching in locate)
12End of stage processing

This allows filter stages like locate to split matching and non-matching records into separate streams.

Return Codes

Pipeline execution follows CMS conventions:

RCMeaning
0Pipeline completed successfully
24Syntax error or empty pipeline specification
28Stage not found
32Pipeline execution error

Integration with CMS

The pipeline subsystem is wired into cms-core’s CommandProcessor through the ExtCommandHandler trait. When a command starts with PIPE, the extension handler strips the prefix and passes the rest to run_pipe(). Results (return code and output messages) are returned through the standard command result path.

Overview

The cms-machine crate wires all subsystems together into an interactive CMS console. It is the top-level binary that provides a REPL where users can issue CMS commands, run REXX EXECs, use spool commands, and execute pipelines.

CmsMachineHandler

CmsMachineHandler implements the MachineHandler trait from vm-iucv, bridging the actor framework with the CMS CommandProcessor. It handles:

  • on_ipl – Prints the startup banner, runs PROFILE EXEC if present, and begins the command loop.
  • on_smsg – Dispatches incoming messages. Messages from $CON trigger command execution; messages from other machines are displayed and stored in GLOBALV (LMSGSRC, LMSGTXT).
  • on_logoff – Cleans up resources.

Console Architecture

The console uses a split-thread design:

stdin (blocking read)
  |
  v
cmd_tx channel ──> CmsMachineHandler
  |                      |
  +── SMSG from $CON ──> on_smsg()
                           |
                           v
                    CommandProcessor::execute()
                           |
                           v
                    output_tx channel ──> stdout
  1. A dedicated thread reads lines from stdin and sends them via cmd_tx.
  2. After sending a command, the thread sends an SMSG from the $CON pseudo-machine to wake the handler.
  3. The handler receives the SMSG, pulls the command from cmd_rx, executes it, and sends output lines via output_tx.
  4. The console thread reads output lines until it sees the BATCH_DONE sentinel, then prompts for the next command.

BATCH_DONE Sentinel

The BATCH_DONE constant is a special marker string sent after every command completes. The console thread waits for this sentinel before displaying the next prompt, ensuring output from one command is fully flushed before the next begins.

Subsystem Integration

All subsystems are available from the REPL:

SubsystemHow it connects
CMS commandsBuilt into CommandProcessor (GLOBALV, LISTFILE, etc.)
REXX EXECsCmsRexxExecHandlerWithSwap – persistent state across EXECs
SpoolCmsExtCommandHandler routes SP/QUERY/PURGE/SENDFILE/RECEIVE
PipelinesCmsExtCommandHandler routes PIPE commands to run_pipe()
SMSGChannelSmsgSender routes through the actor framework

The CmsRexxExecHandlerWithSwap handler swaps the file system, GLOBALV store, and SMSG sender into the REXX execution context, so REXX EXECs can issue CMS commands that modify persistent state.

CLI Usage

# Basic usage
cargo run -p cms-machine -- --userid ALICE --disk /tmp/cms

# Multiple disks (mounted as A, B, C, ...)
cargo run -p cms-machine -- --userid BOB --disk /tmp/d1 --disk /tmp/d2

The --userid flag sets the machine’s identity for SMSG routing. Each --disk path is mounted as a minidisk at successive filemode letters (A, B, C, …) in read-write mode.

Overview

vm-iucv is a lightweight actor framework inspired by IBM’s VM/CMS inter-machine communication facilities. Each actor is a machine — an isolated unit with its own message handler — coordinated by a Supervisor that manages lifecycle, messaging, and IUCV paths.

Architecture

┌──────────────────────────────────────────┐
│                Supervisor                │
│                                          │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐   │
│  │ ALICE   │  │ BOB     │  │ CHARLIE │   │
│  │ handler │  │ handler │  │ handler │   │
│  └────┬────┘  └────┬────┘  └────┬────┘   │
│       │            │            │        │
│  ┌────┴────────────┴────────────┴────┐   │
│  │         Router (SMSG)             │   │
│  └───────────────────────────────────┘   │
│  ┌───────────────────────────────────┐   │
│  │     Path Command Loop (IUCV)      │   │
│  └───────────────────────────────────┘   │
└──────────────────────────────────────────┘

VM/CMS Analogy

VM/CMS Conceptvm-iucv Equivalent
Virtual machineMachineId + MachineHandler
CP (Control Program)Supervisor
IPL (boot)supervisor.ipl()
LOGOFFsupervisor.logoff()
CP SMSGsupervisor.smsg() / ctx.try_send_smsg()
IUCV CONNECTsupervisor.connect()
IUCV SEVERsupervisor.sever() / ctx.sever_path()
IUCV SENDctx.iucv_send()
IUCV RECEIVEon_iucv_data() callback

Key Design Principles

Fire-and-forget messaging. SMSG delivery is best-effort, matching the real CP SMSG semantics. If a target machine’s channel is full, the message is silently dropped.

Synchronous handlers. All MachineHandler callbacks run synchronously on the machine’s Tokio task. This keeps the programming model simple — handlers never need to be async. Use ctx.try_send_smsg() or ctx.iucv_send() to communicate without blocking.

Path-based connections. IUCV paths provide a bidirectional data channel between two machines, with explicit connect/accept/sever lifecycle. Paths carry binary data (IucvBuffer) rather than text.

Crate Features

FeatureDescription
test-utilEnables CollectorHandler / CollectorHandle for testing
examplesPulls in tokio runtime features needed for examples

Machines & Handlers

MachineId

Every machine has a unique identity — a MachineId. IDs follow VM/CMS naming rules:

  • 1–8 characters
  • Must start with a letter or national character (@, #, $)
  • Remaining characters: alphanumeric or national
  • Automatically uppercased
#![allow(unused)]
fn main() {
let id = MachineId::new("alice").unwrap(); // becomes "ALICE"
assert_eq!(id.as_str(), "ALICE");
}

MachineHandler Trait

Every machine needs a handler that implements MachineHandler. The only required method is on_smsg:

#![allow(unused)]
fn main() {
use vm_iucv::handler::{MachineContext, MachineHandler};
use vm_iucv::message::SmsgMessage;

struct MyHandler;

impl MachineHandler for MyHandler {
    fn on_smsg(&mut self, ctx: &MachineContext, msg: SmsgMessage) {
        println!("Got: {}", msg.text());
    }
}
}

Lifecycle Callbacks

CallbackWhen CalledRequired
on_iplAfter boot, before signalsNo
on_smsgFor each incoming SMSGYes
on_connection_pendingPeer requests a pathNo (default: accept)
on_connection_completePath fully establishedNo
on_connection_severedPath severed by peerNo
on_iucv_dataData arrives on a pathNo
on_logoffMachine logging offNo

Important Notes

  • All callbacks are synchronous — they run on the machine’s Tokio task. Blocking a callback stalls the entire machine.
  • on_logoff is not called if on_ipl or on_smsg panics. Use RAII guards for guaranteed cleanup.
  • on_connection_complete and on_connection_severed use best-effort delivery — they may be skipped if the signal channel is full.

MachineContext

The MachineContext is passed to every callback and provides the machine’s identity and communication methods:

#![allow(unused)]
fn main() {
// Identity
ctx.machine_id()          // → &MachineId

// SMSG (fire-and-forget)
ctx.try_send_smsg(&to, "text")  // → Result<()>

// IUCV path operations
ctx.iucv_send(path, buffer)     // → Result<()>
ctx.sever_path(path)            // → Result<()>
}

All MachineContext methods are non-blocking — they use try_send internally and return immediately.

Machine Lifecycle

Supervisor::ipl()
    │
    ▼
on_ipl()          ← one-time initialization
    │
    ▼
┌─────────┐
│ Signal   │◄──── on_smsg(), on_connection_*, on_iucv_data()
│ Loop     │
└────┬────┘
     │
     ▼
on_logoff()       ← cleanup (best-effort)
     │
     ▼
  (task ends)

SMSG Messaging

SMSG (Special Message) is the fire-and-forget text messaging facility, modeled after the VM/CMS CP SMSG command.

Sending Messages

There are two ways to send an SMSG:

From outside a handler (via Supervisor)

#![allow(unused)]
fn main() {
// Async — awaits until the message is enqueued in the router
supervisor.smsg(&from, &to, "Hello").await?;
}

From inside a handler (via MachineContext)

#![allow(unused)]
fn main() {
impl MachineHandler for MyHandler {
    fn on_smsg(&mut self, ctx: &MachineContext, msg: SmsgMessage) {
        // Non-blocking — uses try_send
        let _ = ctx.try_send_smsg(msg.from(), "Got it!");
    }
}
}

Message Constraints

ConstraintLimit
Max text length236 bytes
Character setASCII only

These match the real CP SMSG limits (236-byte EBCDIC text).

#![allow(unused)]
fn main() {
// Constructing a message manually
let msg = SmsgMessage::new(
    MachineId::new("ALICE").unwrap(),
    MachineId::new("BOB").unwrap(),
    "Hello Bob",
)?;
}

Delivery Semantics

SMSG delivery is best-effort:

  • If the target machine exists and its channel has room, the message is delivered to on_smsg.
  • If the target’s channel is full, the message is silently dropped.
  • If the target is not logged on, supervisor.smsg() returns MachineNotFound.
  • Messages in-flight when a machine logs off may or may not be delivered.

This matches real VM/CMS behavior where CP SMSG provides no delivery guarantee.

Request/Reply Pattern

Since SMSG is fire-and-forget, implement request/reply by having the receiver send a response back:

#![allow(unused)]
fn main() {
struct EchoHandler;

impl MachineHandler for EchoHandler {
    fn on_smsg(&mut self, ctx: &MachineContext, msg: SmsgMessage) {
        let reply = format!("ECHO: {}", msg.text());
        let _ = ctx.try_send_smsg(msg.from(), &reply);
    }
}
}

See the echo_server example for a complete working version.

Inspecting Messages (Testing)

Use the CollectorHandler (requires test-util feature) to capture messages for inspection:

#![allow(unused)]
fn main() {
use vm_iucv::collector::collector;

let (handler, handle) = collector();
supervisor.ipl(&id, handler).await?;

// ... send messages ...

let messages = handle.messages();
assert_eq!(messages[0].text(), "Hello");
}

IUCV Paths

IUCV (Inter-User Communication Vehicle) paths provide bidirectional binary data channels between two machines, modeled after the VM/CMS IUCV facility.

Path Lifecycle

  connect()              on_connection_pending()
  (initiator)            (target decides: accept/refuse)
      │                         │
      │         ┌───────────────┤
      │         │ accept        │ refuse
      ▼         ▼               ▼
  ESTABLISHED           ConnectionRefused
      │
      │  ◄── iucv_send() / on_iucv_data()
      │
  sever() or logoff
      │
      ▼
  PATH REMOVED

Establishing a Path

Use supervisor.connect() to request a path:

#![allow(unused)]
fn main() {
// Initiator requests a path to the target.
let path: PathId = supervisor.connect(&initiator, &target).await?;
}

The target’s on_connection_pending is called. If it returns true, the path is established and both sides receive on_connection_complete. If false, the caller gets ConnectionRefused.

Accepting/Refusing Connections

Override on_connection_pending to implement connection gating:

#![allow(unused)]
fn main() {
fn on_connection_pending(
    &mut self,
    _ctx: &MachineContext,
    _path: PathId,
    from: &MachineId,
) -> bool {
    // Only accept connections from TRUSTED
    from.as_str() == "TRUSTED"
}
}

The default implementation accepts all connections.

See the connection_gating example for a complete working version.

Sending Data

Once a path is established, send binary data with ctx.iucv_send():

#![allow(unused)]
fn main() {
use vm_iucv::path::IucvBuffer;

let data = IucvBuffer::new(b"Hello via IUCV".to_vec())?;
ctx.iucv_send(path, data)?;
}

The peer receives the data in on_iucv_data:

#![allow(unused)]
fn main() {
fn on_iucv_data(&mut self, _ctx: &MachineContext, path: PathId, data: IucvBuffer) {
    let text = String::from_utf8_lossy(data.as_bytes());
    println!("Received on {}: {}", path, text);
}
}

IucvBuffer Limits

ConstraintLimit
Max buffer size65,535 bytes

Severing a Path

Either side can sever a path:

#![allow(unused)]
fn main() {
// From outside a handler (via Supervisor)
supervisor.sever(path, &machine_id).await?;

// From inside a handler (via MachineContext)
ctx.sever_path(path)?;
}

The peer receives on_connection_severed. When a machine logs off, all its paths are automatically severed and peers are notified.

SMSG vs IUCV

FeatureSMSGIUCV
Data formatText (ASCII, 236 bytes max)Binary (65KB max)
ConnectionNone (fire-and-forget)Explicit path lifecycle
DirectionOne-way per messageBidirectional on path
DeliveryBest-effortBest-effort
Use caseCommands, notificationsData transfer, sessions

API Quick Reference

Supervisor

MethodDescription
Supervisor::new()Create a new supervisor (must be called in a Tokio runtime)
ipl(&id, handler)Boot a machine with the given handler
logoff(&id)Log off a running machine
smsg(&from, &to, text)Send an SMSG between machines
connect(&from, &to)Establish an IUCV path between machines
sever(path, &from)Sever an IUCV path
query_names()List all running machine IDs
query_paths()List all active path IDs
shutdown()Log off all machines and stop the supervisor

All Supervisor methods are async except new().

MachineContext

MethodDescription
machine_id()Get this machine’s MachineId
try_send_smsg(&to, text)Send an SMSG (non-blocking, fire-and-forget)
iucv_send(path, buffer)Send data on an IUCV path (non-blocking)
sever_path(path)Sever an IUCV path (non-blocking)

All MachineContext methods are synchronous (non-async) and use try_send.

MachineHandler Trait

CallbackSignatureDefault
on_ipl(&mut self, &MachineContext)No-op
on_smsg(&mut self, &MachineContext, SmsgMessage)Required
on_connection_pending(&mut self, &MachineContext, PathId, &MachineId) -> booltrue
on_connection_complete(&mut self, &MachineContext, PathId, &MachineId)No-op
on_connection_severed(&mut self, &MachineContext, PathId, &MachineId)No-op
on_iucv_data(&mut self, &MachineContext, PathId, IucvBuffer)No-op
on_logoff(&mut self, &MachineContext)No-op

Key Types

TypeDescription
MachineIdMachine identity (1-8 chars, auto-uppercased)
SmsgMessageText message (ASCII, max 236 bytes)
PathIdOpaque IUCV path identifier (Copy, Eq, Hash)
IucvBufferBinary data buffer (max 65,535 bytes)
IucvErrorError type with CMS-style return codes

CollectorHandler (test-util feature)

Function/MethodDescription
collector()Create a (CollectorHandler, CollectorHandle) pair
handle.messages()Snapshot of collected SMSG messages
handle.count()Number of collected messages
handle.path_events()Snapshot of collected path events
handle.path_event_count()Number of collected path events

Error Codes

All errors use the IucvError enum and display with the IBM DMSIUC message prefix, following VM/CMS conventions.

Error Variants

VariantRCMessage PrefixDescription
AlreadyRunning(id)8DMSIUC008EMachine is already IPL’d
MachineNotFound(id)12DMSIUC012ETarget machine not logged on
AlreadyLoggedOff(id)12DMSIUC012EMachine already logged off
DeliveryFailed(id)16DMSIUC016EMessage channel closed
ChannelBusy(ch)20DMSIUC020WChannel full (transient)
InvalidParameter(msg)24DMSIUC024EInvalid parameter value
InvalidMachineId(msg)24DMSIUC024EInvalid machine ID format
MachinePanicked(id)28DMSIUC028EMachine task panicked
SupervisorDown32DMSIUC032ESupervisor has shut down
PathNotFound(id)36DMSIUC036EIUCV path not found
ConnectionRefused(msg)40DMSIUC040ETarget refused connection

Using Return Codes

Every error carries a CMS-style numeric return code:

#![allow(unused)]
fn main() {
match supervisor.smsg(&from, &to, "Hello").await {
    Ok(()) => println!("Sent"),
    Err(e) => println!("Failed with RC={}: {}", e.rc(), e),
}
}

Common Error Scenarios

IPL a machine twice:

DMSIUC008E Machine already running - ALICE

Send to a machine that isn’t logged on:

DMSIUC012E Machine not found - GHOST

SMSG text too long (> 236 bytes):

DMSIUC024E Invalid parameter - SMSG text exceeds 236 bytes

Connection refused by target:

DMSIUC040E Connection refused - SECURE refused connection from UNTRUST

VM/CMS Glossary

A mapping of VM/CMS terminology to Rust concepts in this workspace.

VM & Supervisor

VM/CMS TermRust ConceptDescription
CP (Control Program)SupervisorThe hypervisor that manages virtual machines
Virtual MachineMachineId + MachineHandlerAn isolated execution context with its own handler
IPL (Initial Program Load)supervisor.ipl()Boot a virtual machine
LOGOFFsupervisor.logoff()Shut down a virtual machine

Messaging & Communication

VM/CMS TermRust ConceptDescription
CP SMSGsupervisor.smsg() / ctx.try_send_smsg()Fire-and-forget text message (max 236 bytes)
IUCV (Inter-User Communication Vehicle)Path API (connect, sever, iucv_send)Bidirectional data channel between machines
IUCV CONNECTsupervisor.connect()Request a path to another machine
IUCV ACCEPTon_connection_pending returning trueAccept an incoming connection request
IUCV SEVERsupervisor.sever() / ctx.sever_path()Tear down an established path
IUCV SENDctx.iucv_send()Send binary data on a path
IUCV RECEIVEon_iucv_data() callbackReceive binary data on a path
PathPathIdIdentifier for an IUCV connection

CMS File System

VM/CMS TermRust ConceptDescription
fn ft fmFileSpecCMS file naming: filename, filetype, filemode (e.g., PROFILE EXEC A)
MinidiskAccessMode + disk pathVirtual disk accessed at a filemode letter (A-Z)
AccessCmsFileSystem::access_disk()Mount a disk directory at a filemode letter
LISTFILEcmd_listfile()List files matching a pattern
COPYFILEcmd_copyfile()Copy files between disks

Command Processor

VM/CMS TermRust ConceptDescription
CMS commandCommandProcessor::execute()Dispatch a command through the processor
Abbreviationparse_cms_command()IBM-style minimum abbreviation matching (e.g., GLO for GLOBALV)
EXECExecHandler::execute_exec()Run a REXX or EXEC2 program
GLOBALVGlobalVarsPersistent named variables (SET/GET/LIST, LASTING/SESSION groups)
ExtCommandHandlerExtCommandHandler traitExtension point for spool/pipeline commands

Spool Subsystem

VM/CMS TermRust ConceptDescription
SpoolSpoolManagerVirtual I/O queue manager for reader/punch/printer
Reader (RDR)Reader queueInbound spool device for receiving files
Punch (PUN)Punch queueOutbound spool device for sending files
Printer (PRT)Printer queueOutput spool device
Spool classSpoolClassSingle-letter classification (A-Z, 0-9, *)
Spool IDSpoolIdNumeric identifier for a spooled file
SENDFILEexecute_spool_command(SendFile)Send a file to another user’s reader
RECEIVEexecute_spool_command(Receive)Receive a file from the reader queue

Pipelines

VM/CMS TermRust ConceptDescription
PIPErun_pipe()Execute a Hartmann pipeline
StageStage traitA processing unit in a pipeline
literalBuilt-in stageEmits a literal string
consoleBuilt-in stageWrites records to output (like terminal display)
locateBuilt-in stagePasses records matching a pattern
nlocateBuilt-in stagePasses records NOT matching a pattern

XEDIT Editor

VM/CMS TermRust ConceptDescription
XEDITxedit-coreFull-screen editor
Prefix areaPrefix command modelLeft margin for line commands (d, dd, i, a, c, m, etc.)
Current lineEditor::current_line()The line the cursor is positioned on
TargetTarget systemNavigation spec (:n, /string/, *, compound)
EXTRACTREXX extract variablesQuery editor state from macros
PROFILE XEDITProfile execStartup macro executed when editor opens
Command line====> promptWhere editor commands are entered

General

VM/CMS TermRust ConceptDescription
EBCDICASCII (simplified)Character encoding for messages
DMSIUCIucvError display prefixIBM message prefix for IUCV errors
RC (Return Code)rc() methodsNumeric error code (CMS convention: 0=ok, 24=error, 28=not found)
SignalInternal MachineSignal enumEvent dispatched to a machine’s handler
ConsoleCollectorHandlerTest utility that captures messages (like a virtual console)
QUERY NAMESsupervisor.query_names()List all running machines