REXX Language implementation
Status: working design note.
Implementation status in the current tree:
rxsysb.redirect_endpoint values with a refcounted
native cell. Normal VM value copies retain the cell instead of byte-copying
raw OS handle values, and finalization closes/join-cleans the endpoint once
the last value reference is gone.SPAWN instruction resolves internal
native redirect endpoint payloads before dispatching to rxspawn.c.rxio.* interfaces/classes, reusable process and
pipe APIs, pipeline helpers, and any general Rexx threading model. Those
remain proposed roadmap work.This note records the review prompted by the Windows ADDRESS redirect flake and sets out a more robust direction for IO, process pipes, and a first step toward threading. It is intentionally a working document, not a committed language contract.
The failing ADDRESS redirect implementation passed opaque native redirect state
through ordinary Rexx .binary values. On Windows that payload contained raw
HANDLE values. Normal Level B assignment, argument passing, object
construction, and attribute storage could byte-copy those .binary values. The
byte copy duplicated the handle number, not ownership. The copied value could
later be finalized or closed by a different lifetime path, leaving another copy
with an invalid handle.
The immediate symptom was a flaky Windows failure in:
ts_address_crexx_nooptts_address_crexx_optThe reproduced failure signature was:
PANIC: CREXX command redirect failure input=0/0/0 output=1/6/1 error=0/0/0
On Windows, lastError=6 is ERROR_INVALID_HANDLE; errorSource=1 is the
write side of ADDRESS CREXX redirect replay.
Short-term aliasing patches reduced the number of byte-copy sites, but they were
not a robust architecture. The underlying issue was that a native resource with
ownership semantics was represented as an ordinary byte buffer. The current
first-stage implementation keeps the .binary-typed compatibility surface but
uses a native payload and refcounted endpoint cell for the physical value.
The VM already has native payload support on value:
value.binary_valuevalue.native_payload_opsvalue.native_payload_flagsrxvm_native_payload_ops carries:
type_namecopyfinalizeThe VM calls the copy hook from copy_value() / copy_binary_value() and the
finalizer from clear_value(). This is exactly the hook shape needed for native
handles. A redirect endpoint should not be an ordinary .binary; it should be
a native payload with explicit retain/duplicate/finalize behavior.
The RXPA public surface also exposes:
SETNATIVEPAYLOAD()GETNATIVEPAYLOAD()ADDCLASS,
ADDINTERFACE, ADDIMPLEMENTS, ADDFACTORY, and ADDMETHODCurrent RXPA tests already prove native payload copy/finalizer hooks fire. They do not yet expose a pure native class constructor that stamps a returned object without a Rexx shim.
The VM has runtime class/interface metadata and dispatch. Rexx source can define interfaces and classes, and RXPA can publish matching metadata. Existing docs also state that native code can advertise contracts, but object construction is not yet a full pure-C path: complete class-shaped construction generally still uses a small Rexx factory/class shim.
That is acceptable for an IO design. We can define public interfaces and Rexx wrapper classes in Level B, with native payload stored inside an attribute or the object value itself. Native methods can operate on the payload while Rexx code sees a normal object.
ADDRESS environments are already normal Rexx objects implementing
addressenvironment. The native RXVML ADDRESS path carries stdin/stdout/stderr
endpoint values, and helper functions such as rxvml_address_emit_output() and
rxvml_address_emit_error() hide redirect writing from native providers.
This is a useful precedent: native providers should not manipulate raw endpoint bytes. The first implemented piece now gives redirect endpoint values native payload ownership; a public Rexx-visible endpoint class remains proposed.
interpreter/rxspawn.c owns the current REDIRECT struct. It creates:
NUL or /dev/nullThe original implementation mixed three concerns in one raw struct:
The current implementation has split value ownership from raw bytes by wrapping the existing redirect state in a refcounted native endpoint payload. The remaining architectural split is still proposed: a Rexx-visible endpoint should become a stable class-shaped object, and a spawn operation should derive per-spawn handles and worker state from that object.
rxvmsock.c uses a VM-context registry of socket entries and exposes integer
handles to instructions. This avoids raw OS handles in Rexx values and keeps
platform-specific cleanup in one place.
For IO streams, a native payload with a refcounted native cell is a better fit than integer handles because it composes with classes, interfaces, method dispatch, and ADDRESS request objects. The socket registry remains a good precedent for central status/error fields and context-owned cleanup.
The current process redirect implementation already uses OS threads internally to fill and drain pipes. Those threads are not Rexx threads. They must not call arbitrary Rexx code or share VM frames.
A general Rexx threading API is a larger design. Process pipes can be the first useful step: they need controlled native worker threads, lifecycle joining, and clear ownership, but they do not require running multiple Rexx frames in parallel.
.binary values.Names below are working names. Final namespace placement is open.
rxio.input
read(max_bytes = -1) -> .stringreadb(max_bytes = -1) -> .binaryclose() -> .voidstatus() -> .interror() -> .stringrxio.output
write(text = .string) -> .intwriteb(bytes = .binary) -> .intclose() -> .voidstatus() -> .interror() -> .stringrxio.stream implements rxio.input, rxio.output
rxio.closeable
rxio.nativestream
rxio.stringoutput
rxio.arrayoutput
.string[].ADDRESS ... output arr.rxio.arrayinput
.string[].ADDRESS ... input arr.rxio.nullinput / rxio.nulloutput
rxio.pipe
read_end() and write_end().rxio.process
wait(), kill(), exit_code(), stdin(), stdout(),
stderr(), close().The native payload should store a pointer to a refcounted cell, not the OS handle bytes directly:
typedef struct rxio_native_cell rxio_native_cell;
typedef struct rxio_payload {
rxio_native_cell *cell;
} rxio_payload;
The cell should carry:
HANDLEvalue* only for legacy bridge objects, with strict lifetime
rulesNative payload ops:
copy(dest, source): retain the cell and install a new payload pointing to it.finalize(value): release the cell. The last release closes handles and joins
owned workers.For process spawning, do not pass the endpoint’s stored handle directly if the spawn will close it. Instead create a per-spawn handle view:
DuplicateHandle.dup() where the child/spawn lifetime needs independent close.This gives the spawn lifecycle its own handles while the Rexx endpoint object remains valid or closes independently according to API rules.
The existing ADDRESS syntax can remain:
address crexx "run :args[]" input input_arr output output_arr error error_arr
The compiler/runtime lowering should change so that redirects become IO objects:
rxio.inherited_stdout() / rxio.inherited_stderr()
semantics, not a dummy raw binaryoutput arr: rxio.arrayoutput(arr)output str: rxio.stringoutput(str)input arr: rxio.arrayinput(arr)rxio.nulloutput()The addressrequest class should store endpoint objects or interface-typed
values. It should not copy native handle bytes through constructor assignment.
Native ADDRESS providers should continue using:
rxvml_address_emit_output()rxvml_address_emit_error()Those helpers should be updated to dispatch to rxio.output.write() or use a
fast native endpoint path when the object is rxio.nativestream.
ADDRESS CREXX run should eventually be implemented on top of a reusable
process API rather than having private command-run capture logic.
Working API shape:
proc = rxio..process(args)
call proc.start(stdin, stdout, stderr)
rc = proc.wait()
Convenience:
result = rxio..run(args)
out = result.stdout()
err = result.stderr()
rc = result.rc()
For streaming:
pipe = rxio..pipe()
proc = rxio..process(args, pipe.read_end(), rxio..stdout(), rxio..stderr())
call pipe.write_end().write("input")
call pipe.write_end().close()
rc = proc.wait()
The first implementation should support simple blocking read/write/close and wait. Nonblocking and async can come later.
The interface split allows pure Rexx implementations later:
Important constraint: native worker threads must not invoke arbitrary Rexx methods. For non-native Rexx stream endpoints, the first implementation should adapt at safe synchronization points:
True callback-from-worker-thread support should wait for a VM threading model.
This IO work can be the first controlled step toward threading without exposing general Rexx threads.
Horizon 1:
This is the only concurrency behavior implemented by the redirect work. It is internal runtime behavior, not a public threading API.
Horizon 2:
rxio.task or rxio.future for background native operations onlyHorizon 3:
rxvm_context per Rexx thread or explicit shared-state rulesDo not share a single VM frame stack across OS threads.
If the Windows flake must be fixed on an older branch before the larger
refactor, finish the narrow aliasing fix in _address.crexx request
construction there. This is a stopgap only.
Validation:
ctest -R '^(ts_address_crexx_noopt|ts_address_crexx_opt)$' --repeat until-fail:100Implemented for existing ADDRESS redirect endpoints as an internal
rxsysb.redirect_endpoint native payload and refcounted cell in the interpreter.
The public name rxio_native_cell remains a proposed library/API direction, not
the current internal symbol.
Deliverables:
SPAWN accepts internal native endpoint payloads rather than raw REDIRECT
byte buffersNo public language surface needs to change yet.
Status: proposed.
Add Level B interfaces/classes in a new library module, likely under rxfnsb or
an internal _rxio namespace.
Deliverables:
rxio.inputrxio.outputrxio.streamThis stage should decide whether the native wrapper object is created by:
The Rexx factory is lower risk for the first pass.
Status: partly implemented for native payload ownership; public IO endpoint objects remain proposed.
_noredir, _redir2array, _redir2string, _array2redir, and _string2redir
now receive native-payload-backed endpoint values from the redirect instructions
rather than raw handle byte buffers. They do not yet return public rxio.*
objects.
The SPAWN instruction boundary now requires the internal native endpoint
payload form. Existing bytecode remains compatible because bytecode calls the
redirect creation instructions at runtime; it does not store durable REDIRECT
struct bytes in .rxbin files.
Validation:
ts_address*Status: proposed.
Build a reusable process abstraction on top of the IO endpoints.
Deliverables:
rxio.processrxio.run(args) convenienceAfter this, ADDRESS CREXX run can delegate to the process API.
Status: proposed.
Once the object model is stable, add convenience helpers for pipelines:
result = rxio..pipeline(cmd1, cmd2, cmd3)
or a builder:
pipe = rxio..pipeline()
call pipe.add(cmd1)
call pipe.add(cmd2)
rc = pipe.run()
Do not start with syntax. Start with objects and tests.
Core native payload:
Endpoint behavior:
Process behavior:
Stress:
ts_address_crexx_* repeat-until-fail_rxio, rxio, or part of _rxsysb.rxio.stream objects.ADDRESS ... output should expose line-oriented array behavior only,
while rxio.output remains byte/text oriented.Use native payload-backed IO endpoint objects as the foundation. Keep the first implementation small:
This fixes the current Windows race by removing raw handle byte copies, and it turns a brittle ADDRESS-only mechanism into a reusable IO facility that can later support Rexx stream implementations, process pipelines, and controlled native worker-thread abstractions.