CREXX

REXX Language implementation

View the Project on GitHub adesutherland/CREXX

cREXX Virtual Machine (Interpreter) Architecture

The rxvm interpreter is the runtime component of the crexx toolchain. It loads, links, and executes the compiled .rxbin bytecode. Its design supports portable switch dispatch and, on GNU/Clang-family compilers, direct threaded code (computed gotos), aggressive stack frame recycling, and an optimized value struct to handle REXX dynamic typing. The current .rxbin format is 007, a coordinated compatibility break with no 006 reader. Its linker-sealed, text-backed/numeric-ID semantic graph, rule-neutral query surface, numeric type/member/factory operands, and follow-on cache/overlay design are specified in RXBIN_007_SEMANTIC_GRAPH.md.

1. VM Lifecycle

The execution of a program within rxvm is handled in discrete phases (as defined in inc/rxvm.h):

  1. Creation: rxvm_create() allocates the root rxvm_context.
  2. Loading: rxvm_load() ingests one or more 007 containers, validates the fixed header and six sections, materializes portable constants/metadata and the semantic graph, expands variable-integer instructions into the normal runtime image, and loads each module directory entry into an internal module struct. A linked container shares one materialized pool and graph across its modules; concatenated archive containers remain independently owned.
  3. Dependency resolution and linking: rxvm_link() first resolves declared META_PROVIDER dependencies, then traverses newly loaded modules to resolve exports and external imports into a unified memory map. After that ordinary link pass, unresolved callable imports with META_AUTOLOAD hints may load their exact packaged RXBIN stems. Linking and hint resolution repeat to a fixed point. The call is dirty-checked, so repeated bridge/runtime entry points become fast no-ops when no module state changed.
  4. Preparation: rxvm_prepare() builds an owned per-module execution_image for both VM modes. Operand cells are copied and direct function operands are rebound to process-local proc_runtime * values. Computed-goto rxtvm stores process-local handler pointers in instruction cells; switch-dispatch rxbvm keeps copied opcodes there and may install process-private opcode forms. Canonical segment.binary remains immutable and is still the serialization, reflection, source/profile and debug identity.
  5. Module initialization: rxvm_initialize() runs every declared META_INITIALIZER once for its mutable module instance. Initializers run after provider resolution/linking and preparation, but before the instance can be entered by main, an embedded public call, or a persistent worker request.
  6. Execution: rxvm_run() / rxvm_call() invoke a target procedure (typically main) and launch the main interpreter loop.

Each module overlay carries an initializer state of UNINITIALIZED, INITIALIZING, READY, or FAILED. Multiple initializer records in one module execute in metadata/source declaration order. A failure poisons that module instance and is not retried automatically. Initializer procedures have ordinary namespace-qualified diagnostic identities but are not exposed as public call targets.

Unrelated modules begin in stable loaded-module order. During initialization, a bytecode call into another unready module first initializes the callee’s module. Calling back into an INITIALIZING module detects a cycle and fails the involved initialization rather than observing partial state. Once the complete module set is ready, ordinary calls pay only the fast published-module-count comparison and do not invoke the slow initialization helper.

initialized_module_count is the published ready prefix. A successful initialization pass advances it to num_modules. If initialization of a late-loaded suffix fails, previously published modules remain callable while the failed/unpublished modules remain inaccessible. A fresh context whose initial module set fails has an empty published prefix and cannot enter main or a public bytecode procedure.

Runtime code can explicitly late-load another .rxbin or .rxplugin through the debugger-style METALOADMODULE instruction. The public rxfnsb wrapper is loadmodule(path) -> .int; it returns the last loaded module number, or a non-positive value on failure. A successful METALOADMODULE immediately calls rxvm_link() and prepares the execution image so existing unresolved imports can bind to procedures/classes in the newly loaded file. The new module remains pending until the next execution boundary initializes the unpublished suffix. Undeclared late loading remains explicit; there is no generic native-library directory sweep. A declared native callable is different: META_PROVIDER causes provider-stem resolution before procedure linking.

Packaged bytecode autoload is a narrower, default-on convenience. A consumer compiled against some-library.rxbin may carry META_AUTOLOAD records naming the exact stem some-library. The VM first gives explicitly loaded, linked and embedded modules the opportunity to satisfy each callable. Only a callable that is still unresolved can trigger a search for some-library.rxbin in the ordinary module roots (-l, or the current directory when no location is set). Newly loaded packages are linked and their own unresolved hints are processed to a fixed point. There is no namespace scan, wildcard search, source lookup or path in the metadata.

--autoload states the default explicitly; --no-autoload retains fully explicit loading for diagnostics, packaging checks and compatibility. Embedders can make the same choice with rxvm_set_autoload().

The extended rxvme and rxbvme products embed the shipped core bytecode images: the Level B library, class library and native-provider declarations, the Level C compatibility library, the Level G library, and RexxScript. This embedded bundle is independent of metadata-driven native-provider discovery. Application bytecode libraries can be supplied explicitly, linked into the image, or found by an exact packaged-stem autoload hint. They are never found by sweeping compiler import roots or by guessing a filename from a namespace.

The static catalogue is checked first. Dynamic fallback opens only <provider-id>.rxplugin in, in order, the application-local providers directory, --provider-path, CREXX_PROVIDER_PATH, and the installed bin/providers beside rxvm. The embedding API can use rxvm_set_provider_path(); rxvml_create(location, ...) treats its explicit location list as trusted provider directories as well as module roots.

The metadata provider ID is the canonical artifact stem. Before _initfuncs is called, the loader verifies that the RXPA binary manifest publishes that same stable provider ID. The native module then publishes META_FUNC and META_PROVIDER records for every registered function, allowing the resolver to verify provider, callable, language option, return type, and arguments before normal linking. Signature comparison canonicalizes RXPA’s source-like lexical spelling ([]/[*], insignificant whitespace around punctuation) to the RXAS form; provider ID, callable and actual type shape remain exact contracts. A required provider-resolution failure stops rxvm_run() before main. An optional record permits provider discovery to miss, but does not suppress the ordinary unresolved-procedure checks if the callable is actually required by the program.

2. Core Internal Structs

rxvm_context

The root state of the VM environment. It houses the loaded modules, global configuration, and debugging state.

typedef struct rxvm_context {
    char *location;
    char *provider_location;
    unsigned char autoload_enabled;
    size_t num_modules;
    module **modules;
    struct avl_tree_node *exposed_proc_tree;
    struct avl_tree_node *exposed_reg_tree;
    char link_dirty;
    char interface_method_registry_dirty;
    char interface_factory_registry_dirty;
    struct rxvm_socket_registry *socket_registry;
    char debug_mode;
    // ...
} rxvm_context;

link_dirty is raised when new modules are loaded. The separate interface_method_registry_dirty and interface_factory_registry_dirty flags track when the interface method and factory caches need rebuilding. This keeps repeated rxvm_link() calls cheap while still supporting late module loading. Because linked images may share one constant pool across multiple modules, module-local runtime walkers now follow proc_head, expose_head, and meta_head chains instead of sweeping the entire pool.

Under RXBIN 007 the loader validates and retains the materialized sealed-image semantic graph. During linking it builds one process-local graph binding with a dense callable-ID-to-proc_runtime * array and bound factory/provider rows. Numeric type/member/factory operands and graph indexes avoid rediscovering those relationships by scanning canonical metadata. Modules also own compact side tables for dynamic method/factory instruction sites; their entries are guarded by the context semantic generation. Generic, source/debug, RXVML, and cross-image/native compatibility paths continue to traverse the canonical module-local meta_head chain where needed. The T6 append-only overlay remains future work; current late linking rebuilds the complete bindings and advances the generation coherently.

socket_registry is the context-owned table for core TCP sockets. Rexx and RXAS code see small positive integer handles, not native descriptors. The registry closes every live socket during rxfremod(), which keeps sockets out of value payload ownership and avoids stale OS resources after a VM context is destroyed.

proc_runtime

Serialized PROC_CONST entries now remain metadata-only. During module load, rxvm builds a parallel proc_runtime table that carries execution-only state such as resolved code ownership, prepared entry addresses, and frame recycling lists.

typedef struct proc_runtime {
    proc_constant *definition;      /* Serialized procedure metadata */
    int locals;                     /* Resolved local-register count */
    bin_space *binarySpace;         /* Owning code segment, or NULL for native */
    stack_frame **frame_free_list;  /* Shared frame recycler head */
    stack_frame *frame_free_list_head;
    size_t start;                   /* Resolved code address / native entry */
    char *name;                     /* Cached pointer to definition->name */
} proc_runtime;

stack_frame

To minimize heap allocation overhead, the VM uses a custom call stack model. stack_frame structs maintain scope, local variables, and return state. When a function returns, the stack_frame is not immediately freed; it is placed onto a frame_free_list associated with the procedure, allowing the VM to rapidly reuse stack blocks for repeated calls.

struct stack_frame {
    stack_frame *prev_free;          /* Pointer to next free recycled frame */
    stack_frame *parent;             /* Caller stack frame */
    proc_runtime *procedure;         /* Executing runtime procedure state */
    bin_code *return_pc;             /* Program Counter to return to */
    value *return_reg;               /* Target register for return values */
    unsigned char has_reference_lifetimes; /* Frame-owned storage has references */
    size_t number_locals;            /* Number of local registers */
    size_t nominal_number_locals;    /* Procedure-declared local count */
    size_t number_args;              /* Argument count for the frame */
    unsigned char is_interrupt;      /* Signal currently being handled, or zero */
    uint32_t caller_arg_base;         /* First caller call-window argument */
    interrupt_entry *interrupt_table; /* Shared or frame-owned signal policy */
    unsigned char interrupt_table_owned; /* Frame owns and must free the table */
    interrupt_saved_entry *interrupt_stack; /* Block-scoped signal handler saves */
    numeric_context num_context;     /* Numeric context for the procedure */
    struct decplugin *decimal;       /* Decimal plugin context */
    char decimal_loaded_here;        /* Whether this frame loaded decimal support */
    struct uniplugin *unicode;       /* Unicode plugin context */
    char unicode_loaded_here;        /* Whether this frame loaded Unicode support */
    value **baselocals;              /* Array of initial / fixed pointers */
    value **locals;                  /* Active pointer map to variable values */
};

Frame recycling is a performance feature, not a semantic shortcut. When a frame is reused, the VM relinks local register pointers back to their base storage, relinks globals, and resets the argument-count register. Incoming arguments do not own value storage, but their entry pointers are recorded in both baselocals and the active locals map. This makes UNLINK well-defined for argument registers and preserves the frame-entry mapping needed when a nested call uses a1... as its call window. Ordinary return places the frame on the procedure recycler; full value teardown happens when recycled frames are drained. Because references are rare, frames carry a small flag that is set on the frame that owns referenced storage. A helper may execute MKREF against caller-owned receiver storage, so the VM finds and marks the owner frame rather than assuming the current frame owns the target. Only flagged frames run the reference-lifetime cleanup on return, invalidating frame-owned local and a0 storage plus nested attribute storage without freeing reusable buffers. Lexical scopes that own local storage emit endlife for each storage-owning local before block metadata is closed and, when eligible, before the compiler returns those registers to the reuse pool. Register reuse is more conservative than lexical cleanup: arguments, .ref arguments, receiver/factory pseudo-locals, exposed symbols, reference-targeted storage, and compiler-generated inline scaffolding are not packed into the scoped reuse pool. Generated trace-helper locals are not a normal-program hot path; they may still receive ordinary block endlife / metadata closeout inside the generated TRACE helper, but they are excluded from scoped reuse allocation. This is the same lifetime invalidation operation as frame cleanup, but scoped to the storage whose block lifetime has ended. clear_frame() performs full storage cleanup, remaining signal-handler stack cleanup, releases any frame-owned private interrupt table, and performs any VM plugin instance cleanup when a frame is finally destroyed. Signal-stack cleanup never allocates while unwinding or destroying a frame. The SAFE_RECYCLED_STACKFRAMES build-time debug guard can additionally zero locals on reuse.

Counted argument-bearing bytecode calls record the first register in the caller’s contiguous call window. Normal return still executes the compiler-emitted reverse swaps and pays no restoration scan. If a branch-style signal handler discards the callee before those instructions run, the unwind path uses that one index plus number_args to restore the caller’s active pointer permutation. It finds each displaced call-slot base pointer in the caller’s active map and performs the inverse pointer swap, preserving mutations and pre-call links instead of resetting values. The base pointer may be frame-owned local storage or an incoming argument’s recorded entry pointer; neither case copies the value.

CALL1 through CALL4 are direct-bytecode alternatives for the common fixed arities. They capture their explicitly named caller value pointers before frame activation and bind them as the callee’s ordinary a1...aN entries. No caller pointer permutation exists, so their child frame deliberately has no caller_arg_base restoration work; the callee cannot distinguish the call form. Argument status is still established by the caller before entry. These forms require RXBIN007_FEATURE_FIXED_CALLS and do not target native procedures; compiler-generated imported/native, dynamic and higher-arity calls retain the counted path.

Native calls have no child frame; their cold branch path recovers the counted window from the interrupted CALL, DCALL, SWAPCALL, SETTPSWAPCALL, or SETTPCALL instruction. All five forms carry the argument-count register at operand position three, so the fixed runtime image supplies the window base without a generic operand scan. Any future call-bearing fused opcode must be added to this cold decoder as part of its signal/unwind contract.

Signal / Interrupt Handling

The VM signal model is implemented directly in the interpreter loop. A root frame owns an initialized interrupt_table[RXSIGNAL_MAX]; a child frame starts by sharing its parent’s table pointer. This makes the common procedure-entry path pointer inheritance rather than a 1,280-byte policy copy. Before the first frame-local mutation, the child makes a private copy and marks it owned, so handlers installed by a caller are visible to later callees while child changes still cannot mutate the caller’s policy. The LIFO call-frame lifetime keeps a shared parent table live until its children return. Returning restores the caller’s signal state by returning to the caller frame, and a recycled child releases any private table before it is reused.

Each frame also owns an interrupt_stack used by block-scoped handlers. The sigpush and sigpop instructions save and restore individual handler entries on that stack. Any instruction that must change the frame’s handler policy first uses the common copy-on-write helper; allocation failure follows the normal signal/OOM path. Frame cleanup clears any remaining pushed entries and discards their saved table values without forcing a private copy, which gives block-scoped handling a safety net for frame exit and frame recycling without allocating during cleanup.

Signal codes are defined in interpreter/rxsignal.h. The handler responses are IGNORE, HALT, SILENT_HALT, RETURN, BRANCH, BRANCH_VALUE, CALL, and CALL_BRANCH, exposed in RXAS as sigignore, sighalt, sigshalt, sigret, sigbr, sigbrv, sigcall, and sigcallbr. KILL is always halt-only. BREAKPOINT is the debugger/trace signal rather than an ordinary error condition.

REFERENCE_INVALID is the dedicated signal for a reference value whose target storage has been destroyed or invalidated. It defaults to halt, participates in normal signal handling, and can be probed without raising through the RXAS refvalid instruction. RXAS refsame is also non-raising: it compares the retained reference-identity cells without dereferencing, so copied references remain identical after target expiry while empty references compare false. Raising operations include deref, linkref, and setref; using a non-reference value with those operations is treated as an invalid reference. endlife rLocal is the RXAS storage-lifetime primitive used by compiler-generated scope cleanup. It invalidates references to rLocal and nested attribute storage, and releases any reference payload held by that register, but it does not clear ordinary register contents.

OBJECT_NOT_INITIALIZED is the dedicated signal for a typed object value that has not completed factory initialization. It defaults to halt and participates in normal signal handling. The raising check is assertinitialized; the non-raising probe is isinitialized. Type compatibility remains separate: istype and asserttype can succeed for a typed uninitialized object.

sigcalla installs an action-aware call handler. It receives the same raw five-attribute interrupt object as sigcall, but the handler’s return string is interpreted as a VM action marker:

The Level B SIGNAL compiler exit hides those internal marker strings behind .signalaction.skip() and .signalaction.fail(). Instruction-level retry was retired on 2026-08-03: it is not a standard REXX trap continuation, had no production users, and cannot safely repeat instructions with partial writes or external side effects. A legacy __rxsignal_retry return is unknown and therefore follows the fail path. Source code that needs retry uses an explicit loop or wrapper procedure.

Pending signals are held in one worker-VM-owned execution-local pending_interrupts word. The active context publishes that word’s address to native raisers only while the execution is live. DISPATCH checks the direct local word after each instruction when the current frame is not already inside an interrupt handler. The VM scans signal codes in numeric order, clears ignored signals during the scan, and clears the selected non-breakpoint signal before handling it. BREAKPOINT remains pending until bpoff, which lets the debugger/trace path keep stepping.

For CALL and CALL_BRANCH handlers the VM passes a raw object-like argument with five attributes:

  1. signal code
  2. module number
  3. address in module
  4. signal name
  5. payload/message object

This raw shape is used by debugger/runtime code. Level B maps it through small raw interop classes with explicit with register.N attributes, rather than requiring library code to hand-write linkattr1 for each slot.

Level B’s rxfnsb.runtime_signal_raw maps the five raw VM slots, and rxfnsb.runtime_signal wraps that mapped raw object behind the public .signal interface. Its normal factory keeps the public .signal(name, message) shape, and generated handler wrappers attach the raw VM object through the internal set_raw method before invoking user code typed as .signal. Branch-value handlers installed with sigbrv perform the same wrapping in the VM before branching to the handler label, so compiler-generated block handlers can bind an as name local directly as a user-facing .signal value.

Address semantics matter. VM-raised fault signals stamp the faulting instruction address before dispatch advances. BREAKPOINT and native or asynchronous interrupts use the next-instruction/resume address. Panic/error reporting should resolve closest preceding source metadata for a fault address; REXX-level stepping should usually use exact-address .meta_source_step metadata so it stops on authored clause boundaries or debugger-selected active ranges.

metaloaddata exposes structured metadata records to Level B handlers. The record string is the value itself, and numbered attributes carry the payload:

The trace-event code fields are compact numeric character codes in the VM payload. TRACE handlers map them to presentation prefixes later and may read frame-local registers only when value_source names a register and value_ref is non-negative.

Several ordered trace events may share one executable address after compiler or assembler optimization. When that address is reached, the trace controller collects every visible result event at that exact boundary into an ordered pending batch. The generated TRACE exit drains the complete batch before execution continues, preserving event count, metadata order and the component value named by each record. Delivery does not scan an address range and does not infer that skipped, branched-over or signal-bypassed instructions ran. Consequently an executable cnop or conversion is not required merely to keep two metadata events at distinct addresses.

This guarantee applies to the events retained in the optimised image; it does not require the optimiser to preserve an event for an operation or value that it removes or moves. Optimised TRACE may therefore differ from the no-opt event stream, while every retained event must remain safe and ordered. Use compiler and assembler no-opt mode for source-correspondent tracing.

Unstripped images may expose trace-event metadata. metaloaddata treats optional trace-event strings such as symbol and resolved_name defensively: absent or invalid references are reported as empty strings rather than causing metadata inspection to fail. Linked images built with STRIP SOURCE do not carry META_SOURCE_STEP or META_TRACE_EVENT; they are not source-level TRACE or RXDB debug artifacts.

value (Dynamic Typing Representation)

Classic REXX is dynamically typed, but the VM keeps frequently used native representations direct. The selected Release 1 register layout is RXVM_VALUE_LAYOUT_NAME == "L32SDH" with RXVM_VALUE_ABI_VERSION == 2. It is 176 bytes in the normal 64-bit UTF-8 product and 168 bytes in a 64-bit NUTF8 build. Compile-time assertions in interpreter/rxvalue.h enforce those sizes.

The selected shape has no inline string. Its direct fields are:

Only string logical metrics are narrowed to 32 bits. Untrusted or growing string ingress is checked once before mutation; invariant-preserving hot stores are direct. Allocation capacities, allocation arithmetic, binary actual length, decimal sizes and attribute counts remain size_t. They are not encoded, shifted or decoded on access.

String, decimal, binary and attribute storage are side allocations owned by the same worker allocator as the containing register. String storage begins in the 32-byte capacity class and grows through power-of-two classes. Decimal storage keeps the engine’s payload address direct in value; a fixed 16-byte rxvm_decimal_metadata header containing raw size_t length and capacity is immediately before that payload. Header and payload are one allocation, so decimal growth does not require a separate descriptor allocation or a second hot pointer indirection. Binary and attribute storage retain their direct metadata because moving those families behind descriptors did not earn their lifecycle and code-size cost.

Logical reset is deliberately sticky. value_zero() clears observable state but retains ordinary string, decimal, binary and attribute capacity for the same register’s later reuse. Physical destruction (clear_value() / destroy_value_storage()) runs native finalizers, destroys nested values and returns every owned side allocation. There is no automatic pressure or per-reset reclamation check in this baseline. rxvm_memory_trim() is an explicit quiescent operation that releases only the central depot’s empty reserve; a later reclamation design requires its own evidence and approval.

The register structure is an internal, rebuild-together VM ABI, not an RXBIN format or transport envelope. The compiler, RXVM/RXVML, RXPA, bundled decimal plugins and any native consumer including rxvalue.h must be rebuilt together when RXVM_VALUE_ABI_VERSION changes. A value pointer must never be serialized or sent to another process or host.

Worker Slab and Sidecar Allocation

One rxvm_memory_context owns a synchronized central depot; each rxvm_memory_worker owns its local slabs, extents and statistics. The selected S0 geometry is fixed at 64 KiB aligned slabs with a 64-byte header:

Within a slab, allocation first pops a worker-local returned-slot list. If that list is empty it advances the slab’s bump area: the contiguous sequence of slots that has never yet been handed out. This is just pointer arithmetic plus one counter, not a search or system allocation. The central lock is used only when a worker acquires or returns a whole slab. One empty slab per worker/class is kept local; additional empty slabs return to the depot, which retains at most two per class and 32 overall before returning excess to the system.

rxvm_memory_enter() binds the active worker in thread-local storage so value helpers can use the correct arena without threading an allocator parameter through every opcode. Ownership is recovered from the aligned slab or extent header. A release under a different active worker is rejected and counted as a wrong-owner free; allocator families are never guessed. Each registered arena also records the OS thread that created it. Entering or destroying that arena from a different thread is rejected before its thread-local binding or local slab lists can be changed.

A worker arena is single-thread-owned execution state, not a lockable heap handle. It must never be entered concurrently by two OS threads. Every thread that allocates VM-managed storage needs its own registered worker arena. An I/O helper may instead use a deliberately independent non-VM allocation domain and transfer its result after synchronization. The central depot is the synchronization boundary for whole slabs; adding a lock around ordinary allocation would hide an ownership error and defeat the local fast path. Managed-only metadata services such as rxvm_memory_capacity() must likewise receive a pointer from this allocator family. Injected or foreign allocators retain an explicit requested-capacity contract instead of being probed for slab metadata.

Structured child-process redirection uses that independent-domain option. A redirect reader or writer receives a private libc-owned single-shot completion, not an RXVM worker, register or value *. String and line-array input is flattened into an immutable byte snapshot before its writer starts. stdout and stderr readers grow separate byte buffers, publish exactly one terminal state, and may finish in either order without sharing mutable state. Joining the helper is the publication/acquire boundary. Only then does the receiver thread append bytes or construct lines in its own destination register under its currently entered worker. Broken pipe, child/launch/thread failure, partial output and abandoned request paths use the same join-before-destruction contract. POSIX uses close-on-exec completion descriptors and Windows uses private non-inheritable handle duplicates; neither platform hands a parent worker to an I/O thread.

rxvm_runtime owns the memory context and central whole-slab depot. Each rxvm_context embeds one rxvm_worker, which owns its allocator arena, thread identity and lifecycle. The compatibility CLI and ordinary RXVML paths create one runtime/worker pair. Channel providers create additional isolated contexts over a shared sealed program generation; they do not share registers, globals or other writable execution state.

The worker lifecycle is idle, running, draining and stopped. Only the owning thread may start/end execution or begin teardown. Nested RXVML calls on that same thread are depth-counted and retain the running state until the outermost call returns; a foreign thread is rejected without changing the lifecycle. Teardown is permitted only from idle, moves through draining, destroys the worker arena, unregisters it from the runtime and then destroys the now-empty runtime domain. Debug teardown continues to abort on any live allocation.

The worker-owned active-state record in rxvm_context preserves the offsets of earlier context members. This is not a guarantee that the C compiler will retain an identical flattened-core stack/register layout; the direct execution slot is deliberately local to run(). A checked thread-local locator identifies the owning context during execution and native load callbacks, but the mutable RXVML/RXPA binding, RXPA copy-out pool, SAY route and CREXX command state live in the context. Its active interrupt field points to the sole execution-local pending word while the VM is running and is null at teardown. Nested same-worker execution transfers pending bits to its direct slot and restores them to the suspended slot on return. The standalone product designates its main VM context as the sole OS-addressable interrupt target. POSIX signal and Windows console callbacks map the event and set a bit in that context’s own pending word; they do not create a second queue or make every worker poll process-global state. Other contexts retain only their local word. Propagation to multiple workers uses explicit channel communication outside the raw OS callback.

Every concurrently executing worker has its own stack/register set, frame recycler, module globals, native/plugin instances, reference/socket registries and procedure-affine state. move_value() is currently an owner-local operation: moving its pointers into another worker would leave them owned by the source arena and later fail the ownership check. Cross-worker communication must therefore copy into receiver-owned storage, use an explicitly transferable immutable buffer, or transfer ownership of a whole suitably isolated block at a defined safe point. Process and cross-host channels must use a versioned serialized envelope. None may expose a raw value or silently mutate a register owned by another worker. The Rexx-visible envelope is register-centric: one logical typed scalar or binary register image may contain ordered child-register images. That ChannelValue is a transport description, never the internal value; the receiver materializes it into its own register tree. Large binary content may later select immutable chunks or a bounded stream capability beneath the same logical surface.

The channel contract keeps mechanism below messaging policy. The VM provides bounded submission/completion queues, wait/wakeup, cancellation, terminal status and receiver-owned ChannelValue materialization. Event buses, publish/subscribe topics, routing, fan-out, replay, retained delivery and acknowledgement belong to Rexx worker classes or RXAS libraries built on that substrate; they are not VM instructions or VM-owned global state.

Any future cross-host channel must use an open, versioned wire protocol that a non-Rexx actor can implement without CREXX headers or RXVM knowledge. Its eventual specification must cover framing and types, capability negotiation, endpoint and correlation identity, ordering/delivery guarantees, deadlines and cancellation, terminal errors, streaming/chunking, flow control, extensibility and security hooks. No raw value, proc_runtime *, native pointer or process-local opcode/handler identity may cross that boundary. The encoding and network transport remain future design and publication work; provider type 3 does not currently advertise such a transport.

The RXPA runtime makes the existing surface safe for multiple live VM contexts without changing its initializer or procedure signature. Static constructor registrations are retained as an owned, synchronized process catalogue and are replayed into a distinct native module for every VM; the first VM no longer consumes the list. A dynamic load returns an explicit DSO reference. Each VM retains its references until its procedure frame caches, module globals, reference values, native payloads, modules and provider instances have been destroyed, so no reachable function or payload-operation pointer outlives its code.

Every native proc_runtime carries an internal capability word in the 64-bit alignment slot after locals and a load-selected invoker. A procedure from a plugin with a valid version-1 PROCESS_REENTRANT manifest binds permanently to the direct adapter. An unmarked procedure also binds direct while exactly one legacy-capable VM is live. Registering a second legacy-capable VM quiesces direct legacy execution, rebinds all live legacy invoker slots to the recursive locked adapter and makes that mode sticky for the process lifetime. A reentrant-only VM is not registered with this legacy coordinator and cannot cause the transition.

run() announces and leaves the VM execution boundary to the cold coordinator; load and teardown register or remove owned invoker slots. Ordinary native calls load the already-selected invoker and contain no capability branch, catalogue lock or coordinator lock. Plugin initialization remains serialized because legacy dynamic plugins copy the helper table into the DSO-static _rxpa_context; repeated dlopen/LoadLibrary calls do not imply private DSO statics.

Native-payload copy and finalize operations remain serialized even when the originating procedure plugin is process-reentrant. The runtime also provides a separate optional versioned query for per-procedure policy and per-VM sessions while leaving the installed initializer and call ABI unchanged. Procedure invokers remain load-bound; session-affine calls enter the VM-owned session selected at load, and old hosts continue through the plugin’s default session. The public author contract and opt-in macros are documented in docs/ai-context/CREXX_LIBS.md.

Sealed Program Generations

Correctness tests retain two fully independent loads of the same RXBIN as the control for shared program storage. The focused internal control runs against the compiler-selected rxvml, explicit switch-dispatch rxbvml and, where supported, a test-only direct-threaded RXVML executable. It proves byte-equivalent but pointer-distinct canonical instruction cells and constant pools, independent allocator workers, modules, globals, procedure runtimes, execution images, bindings and caches. Mutating a module global in one VM is invisible in the other. Late-loading another RXBIN advances only the receiving context’s module table and semantic generation; the second context changes only when it loads the same image itself. Both contexts then execute equivalent procedures and tear down without live allocation diagnostics.

The audit classifies the current storage as follows:

The internal bytecode-only API implements that model. An rxvm_runtime may own one synchronized program catalogue containing reference-counted, append-only rxvm_program_generation records. Sealing adopts the complete validated bytecode prefix from one worker VM. Attaching another worker VM materializes new local module overlays over the same generation-owned module_file images. The generation therefore owns canonical instructions, constant/metadata pools, semantic graphs, names and descriptions; the worker still owns globals, proc_runtime and frame recyclers, execution images, graph/interface bindings and dynamic caches.

Late loading appends a derived sealed generation. Its prefix retains the same immutable image objects, and advancing a worker appends only new overlays, so existing module, procedure and global addresses remain stable for active frames. A worker pins its current generation until it advances or tears down. Superseded generations are reclaimed when they are no longer current and no worker pins them; shared image storage remains until every generation that contains it is gone. Catalogue and reference-count synchronization is confined to cold lifecycle operations. No instruction handler or dispatch iteration acquires a generation lock.

Native/plugin modules are rejected by the seal operation and continue to use the process catalogue, DSO, procedure-policy and per-VM session lifetimes. Ordinary public rxvm_create() contexts still create their own one-worker runtime. The shared-runtime context factory and generation operations remain internal and are used by core providers rather than exposed as a public worker API. They add no RXPA threading ABI or canonical-image change. Sharing the current module or prepared image behind locks, mutating a published generation in place, and treating a raw file mapping as the complete solution remain rejected.

Native Doorbell and Sparse Interrupt Fallback

The macOS implementation provides prompt foreign-thread cancellation without adding a poll, worker-count test, targetability branch, atomic read or loop selector to the ordinary dispatch edge. A persistent executor gives each worker its own context and sealed-generation overlay. A producer publishes the request state under the request mutex and uses pthread_kill() with private SIGURG to ring the selected worker. The signal handler performs only a bounded lookup of a pre-registered worker stack range and ORs RXSIGNAL_CANCEL into that worker’s existing execution-local sig_atomic_t interrupt word. The next ordinary dispatch check enters the existing cold interrupt route.

The executor blocks the private signal while a worker is idle, while rxvm_signal_enter_execution() publishes its current stack-local interrupt word, while rxvm_signal_leave_execution() restores a nested predecessor and during teardown. A late pending doorbell is drained while the request mutex is still the arm/disarm authority, preventing it from cancelling the next request on the persistent VM. Apple Clang TLS is deliberately absent from the handler: the rejected TLS form emitted a Mach-O TLV resolver call. The retained fixed 64-slot stack-range scan has no handler call, allocation, log, lock or runtime graph traversal.

This physical-delivery mechanism is internal, not a public worker API. A provider first publishes a correlated request and generation, then uses one internal doorbell bit to enter the cold route, validate the active request, drain published work and preserve KILL/shutdown priority.

The portable owner model distinguishes targetability from native-delivery capability. Non-targetable/local contexts always use the direct owner. Targetable workers use that same owner when POSIX thread signals or Windows special APCs provide prompt native delivery. Only a targetable worker without native delivery selects a second owner, immutably and before its execution image is prepared; an executing worker never changes owners.

That compatibility owner is sparse, not an every-instruction polling switch. It acquire-loads the stable external event word at request entry, taken backedges (including resolved indirect/table backedges), bytecode call and return boundaries, and after a native/plugin call returns to bytecode. These points cover loops and recursion while leaving forward finite instruction runs unpolled. An uncooperative native/plugin call remains uninterruptible while it is executing. The original sparse experiment was functionally effective, but its source and exact opcode ledger were not retained. The current semantic classification and provenance are recorded in performance/PERF3-13-E5-NATIVE-DOORBELL-DESIGN.md and must be kept aligned with rxops.h and private execution-image rewrites.

Windows 11 qualification covers this owner under MinGW GCC, MSVC and Clang with the MSVC ABI. The exact sparse fixture covers conditional, counted and indirect backedges plus every bytecode return form; the persistent-worker fixture covers unconditional-loop and recursive-call cancellation, worker reuse, stress and teardown. The forced-fallback latency floor is 2.9-3.0 us median over 1,000 samples per concrete engine on the qualification host. The four core products build in each compiler configuration. The MSVC and Clang/MSVC-ABI builds disable the optional parser-mode dependency because its sibling project remains POSIX-only; that does not change VM qualification.

rxinimod_common() must initialize active.compatibility_interrupts to NULL. Callers are not required to zero stack or malloc-backed rxvm_context storage, and an indeterminate value selects the sparse owner and is dereferenced on request entry. test_rxvmactive deliberately poisons the context before initialization to enforce this invariant.

Allocator Ownership and Scale Qualification

Every allocation owned by an rxvm_memory_worker is confined to the thread that created that worker. Standard-slab, oversized-extent, value-array and reference-cell allocation, resize and release all consult the worker’s recorded owner-thread token before mutating owner-local lists or statistics. Passing an explicit worker from a foreign thread is rejected even when that thread has no entered allocator TLS. Overflow handling follows the same rule and cannot use a foreign request to increment owner-local failure telemetry.

Wrong-owner allocation, resize and free counts are cold diagnostic state in the shared memory context and are updated under its mutex. This synchronization is confined to rejected operations. Successful owner-local allocation and free remain lock-free, retain one empty slab per active class locally and return additional empty slabs through the existing synchronized depot policy.

The strict owner-local policy was selected after measuring and rejecting automatic owner-idle slab reclaim and a test-only owner-drained remote-free queue. Live VM values and worker storage still never cross workers; normal transfer must materialize receiver-owned data or use a separately owned immutable/moved buffer. The executor qualification covers compute and allocation churn at one, two, four and eight workers on each available concrete VM and asserts that the requested maximum concurrency is reached. This allocator policy adds no public worker/channel API, plugin ABI or scheduling contract.

Channel, Level B and Level G Task Substrate

The public RXAS channel boundary is implemented without exposing a public RXPA threading ABI. Opcodes 650..654 implement chanopen, chanstart, chanwait, chancancel and chanclose in the shared VM core, so rxbvm and rxtvm execute the same logical behavior. They require RXBIN 007 feature bit 1 << 3, return operation statuses rather than VM signals, remain opaque optimizer barriers and are outlined/cold under profile-20.

Each rxvm_context owns a generation-checked table of channel and ticket capabilities. Handles encode their execution owner, kind, slot and generation; wrong-owner, wrong-kind and stale uses are rejected. They are local authority, not transferable ChannelValue. Context teardown cancel-closes every live channel, joins its workers and releases all tickets/requests before the runtime provider state and sealed generation are destroyed.

The runtime-owned provider registry validates complete private descriptors, rejects duplicate names/codes atomically, pins provider/module lifetime and is seeded with the core type 1 local-thread descriptor. An internal fake extension fixture proves register/open/operate/close/unload sequencing without publishing a plugin ABI. chanopen validates its canonical RXCV pool configuration, required capabilities and the controller’s sealed bytecode-only program generation, then creates an attached executor over that same generation. A program containing native/plugin modules may still execute normally, but local chanopen reports provider unavailability when the image cannot be sealed for attached workers. This avoids making ordinary native program startup depend on channel eligibility.

The core type 1 provider advertises bounded admission, cancellation, provider-owned deadlines and completion-order observation. Task envelopes name a semantic-graph callable ID and carry typed copied register images; they never carry a procedure-name string, live value, reference, frame, native payload or worker pointer. Completion is encoded into receiver-owned canonical RXCV binary and is marked observed only after controller-worker-owned storage has been allocated and populated. Encoding or allocation failure therefore leaves the terminal completion available for a later wait. Queue-full/backpressure, finite/nonblocking waits, queued/running cancellation, fail-fast/collect-all scopes, terminal observation, drain/cancel close and deterministic teardown are implemented. RXCV validation and encoding cover null/boolean, integer, float, decimal, string, binary, ordered arrays and schema-tagged records with canonical limits/order/flags/NaN.

lib/classlib/Concurrency.crexx supplies the executable Level B pool, scope, task, target, context, completion, channel, value/codec, endpoint, service-reference and transfer-buffer surface. Inspection proves its runtime bridge is only the five channel instructions.

Core provider type 4 implements bounded C-owned byte endpoints and type 5 as structured child-process execution. Endpoint storage owns copied bytes, backpressure, cancellation, EOF and half-close state; background I/O never retains a live Rexx register/value. Execution-local CSPRNG references carry validated direction rights and resolve through the type 4 registry, not through native payloads or transferable OS handles. The child provider snapshots command/arguments, logical working directory, merged environment, CREXX bindings and three optional endpoint references before launch. Ordinary synchronous POSIX children inherit the caller’s process group. A cancellable/deadline-controlled POSIX child receives a provider-owned process group only when its terminal-facing streams are redirected or headless. A controlled child that inherits a real terminal stream remains in the caller’s terminal job so foreground ownership is preserved; cancellation then targets the direct child rather than an unowned group. The equivalent controlled Windows children receive a provider-owned job. Group/job ownership is recorded explicitly and is used for bounded tree termination. A controlled POSIX child that stops unexpectedly is terminated with a diagnostic instead of leaving a wait loop stalled. Controller-mode CREXX execution remains synchronous where it must preserve command-environment state.

The certified ADDRESS exit and _address.crexx now adapt classic string/array redirects onto these two providers and apply captured output only on the controlling execution. The retired source mnemonics spawn, redir2str, redir2arr, str2redir, arr2redir and nullredir are rejected; numeric slots 466..471 are reserved handlers that halt stale RXBIN with UNKNOWN_INSTRUCTION.

Provider type 2, crexx.core.isolated-process, uses capability mask 0x010f. Opening a process pool writes the controller’s sealed bytecode-only generation to a provider-owned temporary RXBIN archive. Each distinct semantic graph remains its own concatenated 007 container, with modules from that graph written together, so numeric callable/member identities are preserved. Native modules make the generation ineligible rather than being exposed through a new process ABI.

The provider owns a bounded set of warm child VMs and a bounded admitted request count. Its private version-1 protocol uses READY/INVOKE/STARTED/RESULT/CANCEL/SHUTDOWN frames carrying the same canonical RXCV task/completion documents as the local provider. A worker process may be reused, but every request creates a fresh executor and context; module globals and other live VM state therefore do not spill between tasks. Cancellation/deadline first interrupts cooperatively and then may terminate only the isolated process after a 250 ms grace. Pre-STARTED loss is TRANSPORT_LOST, post-STARTED loss is UNKNOWN_OUTCOME, terminal publication is exactly once and a dead worker is replaced. Private POSIX protocol writes contain SIGPIPE locally instead of changing the host’s process-wide signal disposition. Pool teardown joins all provider threads/processes and removes the temporary snapshot.

Local and process dispatch validate an 80-byte sealed binding. Kind 1 identifies a task procedure, kind 2 reconstructs a transferable receiver through its sealed from_channel factory, and kind 3 constructs a receiver-side .taskwork factory target and invokes its sealed run method. The binding cache keeps that first-use check and caches only successful resolution within the validating executor worker. The worker graph binding lazily retains the immutable graph digest, and a fixed four-set, two-way cache uses the complete binding plus requested result mode to retain worker-local procedure/adapter pointers. A miss follows the unchanged validator; failures are never cached, collisions only evict, and worker/context teardown invalidates every entry. Task arguments/results and factory arguments remain receiver-materialized RXCV; no live VM value crosses. For a typed object result, the executor invokes the sealed result class’s to_channel() method, canonicalizes the returned .channelvalue, and publishes that document. The controller-side compiler lowering reconstructs a new object with the same class’s from_channel factory. Level G task/parallel syntax lowers through the Level B classes and is accepted only under OPTIONS LEVELG. Concurrent HTTP is implemented as a Level G Rexx library above the same channel and endpoint substrate. Unsupported Level B operations report status 19 rather than simulating success. A public provider-plugin ABI and provider type 3 remain reserved. The exact current/future boundary is recorded in the concurrency implementation status matrix.

Variables (locals arrays) consist of arrays of value* pointers managed strictly by the VM frames. There is no automated background Garbage Collector (GC). Frame-bound variables are either recycled for later calls or deterministically cleared (clear_value) when a stack_frame is finally destroyed. Reference identities for frame-owned storage are invalidated on ordinary frame exit for frames that own referenced storage, even if the reference was created by a callee helper. Recycled stack storage therefore cannot keep an escaped weak reference valid.

Attribute arrays use two parallel pointer arrays:

set_num_attributes() owns allocation and capacity growth. Bulk attribute insert/delete instructions (INSATTRS/INSATTRS1 and DELATTRS/DELATTRS1) manipulate the pointer arrays directly by rotating both arrays together, then clearing only the VM-owned backing values for inserted or removed slots. This preserves the UNLINKATTR invariant and avoids clearing an externally linked register when a logical attribute slot is deleted.

status.all_type_flags is a partitioned 32-bit status word stored as uint32_t. The masks live in binutils/include/rxflags.h so compiler-emitted RXAS and VM code agree:

The VM-private allocations cover UTF-8 validity, codepoint-count validity and object lifecycle. SETTP, SETORTP, and LOADSETTP cannot write that band. Trusted RXAS can explicitly assert language-owned normalization certificates; compiler ABI writes preserve them, and SETTP reg,0 clears ordinary public flags while retaining protected language facts. GETTP and GETANDTP return readable flags. Unmasked BRTPT excludes VM-private and language metadata so neither cache changes legacy branch behavior. Status flag instructions still take normal rxinteger operands in RXAS/RXBIN; the VM applies only the low 32 bits when reading a flag mask.

Level B register flag views expose masked status-word partitions for system-programmer classes. VM-private, compiler call-ABI, protected-language, and all-readable views are read-only at source level. Library and user views are writable; a public write view covers library and user flags only, not compiler flags. Source-level flag-view writes replace only the selected masked band and must preserve all other status bits. The compiler emits settpmask target,value,mask for these writes; the VM applies (old & ~mask) | (value & mask) after restricting the mask to source-writable library/user bands. .flags.language observes the protected language band without granting ordinary source writes.

Normalization certificates are independent positive bits for NFC, NFD, NFKC and NFKD. Shared logical string-length publication clears them, as do the same-length in-place writers. Exact string/value/stem copies and owner-local moves preserve them. Empty and known-ASCII strings set all four. NFKD implies NFD and NFKC implies NFC when a normalization algorithm publishes a result or a successful predicate certifies its exposed source. RXPA validation clears opaque non-ASCII native state before revalidating, and non-ASCII channel materialization starts unknown.

In normal UTF builds, RXFLAG_VM_UTF8_VALID and RXFLAG_VM_UTF8_COUNT_VALID mean the string byte span is known well-formed UTF-8 and string_chars is a validated codepoint count. Constant loads mark the cache as trusted, bounded string setters and text reads validate before marking it, copy/move/string-copy preserve it, and codepoint-safe concat/slice/ truncate/append paths propagate it. Level B text ingress rejects invalid UTF-8: public RXVML setters return failure, freadline/freadcdpt and bintos raise UNICODE_ERROR, socket text receive reports an invalid text status, and RXPA native calls recursively validate returned values plus updated argument and signal trees after the callback returns. Raw VM helpers can still clear the cache for internal materialization, but arbitrary bytes belong on the .binary path. CREXX_RXPA_DISABLE_UTF8_CHECKS=1 disables the RXPA post-call check for developer migration/debugging; normal builds should leave it enabled.

UTF instruction loops must use the matching metric: byte-span scans use string_length, while character-index iteration and cache seeks use string_chars. In particular, POSCHAR iterates over string_chars in a UTF build; using byte length as its character bound can seek beyond the last codepoint and turn a stale decoded value into a false match. NUTF8 keeps the single byte-count model.

The object_type descriptor pointer is the Level B hook for object identity, type tests and interface dispatch. Graph-backed descriptors are materialized by rxbin when a checked graph is built or loaded; they contain canonical name/length plus graph/ID identity and reach precomputed assignability and dispatch views. VM-only synthetic types use immutable static descriptors. Class factories stamp object values with setobjtype, and later VM lookups use that concrete descriptor when resolving interface member calls. Copy, move and zero operations therefore transfer or clear one pointer rather than duplicating name, length, graph and ID fields on every value. Bare object class defaults are represented as ordinary object type metadata plus the VM-private RXFLAG_VM_OBJECT_UNINITIALIZED flag. setobjuninit creates that state, while setobjtype clears it when a factory has produced an initialized object. The VM-private flag partition keeps this lifecycle marker out of public register flag writes such as settp. In UTF builds, string_length is the byte length while string_chars is the codepoint count. Any instruction that synthesizes or truncates a string must keep both in sync and reset the VM-private UTF lookup cache to the start of the new value. The cache is acceleration state only: RXAS cannot observe it, value copies do not preserve it, and optimizer metadata must not model it as a logical read or write.

In-place string-byte writers use the private completion helpers rather than assigning string_length alone. finish_string_write() resets the lookup cache, validates/recounts an arbitrary UTF-8 byte span and replaces only the VM-private UTF validity bits. finish_ascii_string_write() records the exact byte/codepoint count for known ASCII output. Both preserve compiler/public type flags, other scalar/decimal/binary/object representations, buffer capacity and reference identity. This contract applies even when conversion materializes a string through a linked value: the referenced storage remains the owner, but stale codepoint validity from the previous byte span must never survive. The PERF2-07 V3 regression covers DCOPY; DTOS; STRLEN, Unicode and typed-null destinations, a live reference alias and sibling numeric writers on both VMs and both optimization modes.

DCOPY treats decimal absence as a total copy of decimal absence: it clears the destination’s logical decimal length, preserves reusable backing storage and unrelated value components, and does not signal. DTOS is likewise total: decimal absence formats as nan, plugin diagnostics are cleared at the conversion boundary, and the completed ASCII write refreshes string length/count and validity metadata. Checked INC/DEC forms and both SETNUMFUZ forms validate into temporary state and signal before mutating the observed integer or numeric context.

The xtos family of scalar-to-string conversions is allowed to mutate the destination value to materialize its string representation. This is acceptable for linked values, including object attributes, as representation materialization rather than user-visible assignment. The current VM does not maintain a validity flag for a cached string representation, so repeated conversions still perform the conversion work and the compiler does not rely on string-form reuse.

Loose String Comparison Numeric Prefilter

Loose string comparison first attempts binary64 conversion of both operands; when either conversion fails, it performs the existing blank-padded byte comparison. rxvm_loose_string2float() avoids calling the copied strtod() path only for an empty span or a leading byte that cannot begin a numeric subject. Digits, signs, whitespace, common radix bytes, inf/nan initials, high bytes and the active locale radix initial all fall through to the exact existing string2float() converter. The helper therefore changes conversion ownership and cost, not comparison grammar or results.

Keep the helper private and out of line so rxvm_loose_compare_text() remains inlineable. The no-inline spelling must cover GCC/Clang and MSVC. Do not extend this optimization into a public opcode, serialized RXBIN form, JSON-specific path or value cache without a separate design and invalidation/lifecycle proof. The selected implementation and governed contract/performance evidence are in performance/PERF3-03-WORKLIST.md.

Decimal Plugin Runtime

.decimal values are stored in the co-allocated worker-owned header/payload sidecar described above. The payload bytes contain the selected plugin’s decimal representation; the plugin must obtain or grow that storage through the host reserve_decimal service and must not call free(decimal_value). The VM loads the default decimal plugin before executing the first frame, attaches it to the frame numeric context, and syncs that plugin whenever a child frame inherits a copied numeric context. A frame that loads its own decimal plugin marks decimal_loaded_here, so cleanup can free it when that frame exits.

The decimal plugin interface is the boundary between VM instructions and the concrete decimal engine. The current decplugin API covers numeric-context sync, required string sizing, conversion to and from string, integer, and double values, coefficient/exponent extraction, arithmetic operations (add, sub, mul, div, pow, neg), comparisons, zero testing, truncate, and round. Decimal instructions in rxvm should go through that API rather than depending on one decimal backend’s internal representation. Both decimal backends map a failed text parse to CONVERSION_ERROR; the decNumber backend maps conversion-syntax and invalid-operation status bits explicitly, while the long-double backend maps its invalid parsed value. Decimal conversion instructions then raise that through the ordinary catchable VM signal path. The decimal-plugin interface and value layout are one tightly coupled internal contract: bundled/static/dynamic plugins and their host must be rebuilt together for ABI version 2. This does not change RXBIN decimal semantics or create a stable third-party wire ABI.

Copy, Move, and Native Payloads

Rexx objects are not user-visible native pointers. A Rexx object value is still a VM value: it may have attributes, a concrete object_type descriptor, scalar storage, and an optional binary payload. Class/interface dispatch is based on the stamped object type plus the semantic graph, not on exposing a C pointer to Rexx code.

The VM has two distinct value-transfer paths:

Native-backed objects should use the binary slot as their physical payload carrier, but only with an explicit rxvm_native_payload_ops descriptor when the payload owns native resources. The descriptor is normally a static provider object shared by many values; the value stores just a pointer to it plus flags, so there is no per-instance ops allocation.

Ordinary .binary payloads should be built through the shared helpers in rxvmvars.h: reserve_binary_buffer(), prep_binary_buffer(), set_binary(), append_binary(), append_binary_value(), concat_binary(), and slice_binary(). These helpers keep binary_length and binary_buffer_length consistent, reuse existing capacity where possible, and clear native payload finalizers before replacing a native-backed object with an ordinary byte sequence. Binary values carry no logical cursor.

RXAS-level binary opcodes operate only on the binary slot. LOAD_REG_BINARY loads a BINARY_CONST from 0x... RXAS syntax. BCOPY_REG_REG copies only the binary payload; it deliberately does not copy public/compiler/library status flags. GETBYTE reads zero-based binary offsets and returns -1 for out-of-range reads. SETBYTE and BUPDATE are strict and raise OUT_OF_RANGE for invalid byte indexes or fixed-size overlay writes past the destination length. BCONCAT and BAPPEND build ordinary byte buffers. BSLICE_REG_REG_REG_REG takes explicit destination, source, start, and length registers and clips the requested byte range to the source length. These operations do not perform UTF-8 validation and clear VM-private UTF cache flags on the destination. BCHECKRANGE validates a zero-based byte offset and length without mutating either register; negative values and ranges that do not fit inside the current logical binary length raise OUT_OF_RANGE.

Typed binary memory opcodes are strict fixed-width views over the same binary slot. BGETU8, BGETI8, BGETU16, BGETI16, BGETU32, BGETI32, BGETI64, BGETF32, and BGETF64 read from zero-based byte offsets, with register and binary-constant source forms where the source is read-only. BSETU8, BSETI8, BSETU16, BSETI16, BSETU32, BSETI32, BSETI64, BSETF32, and BSETF64 write to zero-based byte offsets. These opcodes use canonical little-endian storage order and do not use host-native struct layout, alignment, or padding. Invalid ranges, negative offsets, and integer values outside the target storage type raise OUT_OF_RANGE. BRESIZE preserves existing bytes, zero-fills growth, sets only the logical byte length, and may reuse/grow the private physical binary capacity in blocks. It raises OUT_OF_RANGE for negative lengths. BCLEAR sets the logical binary length to zero. BFILL fills the current logical byte range and requires a byte value in 0..255.

The packed numeric opcodes are a separate host-native view over ordinary binary storage. PGETI/PSETI access exact rxinteger items and PGETF/PSETF access exact VM double items. Their integer operand is a zero-based item index, not a byte offset. The VM checks negative indexes, checked multiplication by the host item width, and the entire item range against the logical binary length before reading or writing. It transfers each item with memcpy, so the access is strict-aliasing safe even though ordinary binary allocation supplies the required host alignment. These opcodes do not tag the binary or change RXBIN metadata: a caller selects the interpretation on each access. Binary constants are readable only after compiler materialization into ordinary aligned runtime binary storage; no packed setter accepts a constant destination.

The Release 1 binary-memory VM surface also includes target-sized copy from a byte offset (BCOPY), zero-terminated UTF-8 text fields (BGETS/BSETS), string-constant extraction (SGET), different-register and same-register memory moves (BMOVE/BMEMMOVE), and zero-copy compares (BCMPB/BCMPS). BCMPB and BCMPS use the compare register as an input source offset and overwrite it with -1, 0, or 1.

FREADB reads bytes with fread(ptr, 1, n, file), so binary_length is the actual byte count read, not a C item count.

When native_payload_ops is set:

That fallback is intentional but dangerous for unique native ownership. A provider that supplies a finalizer but no copy hook must ensure bit-copied payloads are safe to finalize more than once, for example by storing a shared registry handle or a refcounted object. Providers that store raw pointers, file descriptors, sockets, thread handles, or other unique resources must provide a copy hook that retains/clones/duplicates the resource, or avoid value-owned native payloads and use a context-owned registry handle instead. Finalizers release the nested native resource referenced by the payload; they must not free the binary_value buffer itself, because the VM frees that buffer after the finalizer returns.

Core Socket Registry

Core sockets deliberately follow the context-owned registry pattern described above. interpreter/rxvmsock.c implements a small TCP wrapper over POSIX sockets on Unix-like platforms and Winsock2 on Windows. The default TLS backend is platform-selected: Network.framework on Apple platforms, OpenSSL on non-Windows Unix-like platforms, and SChannel on Windows. Windows builds link the platform ws2_32, secur32, and crypt32 libraries for socket and TLS support.

The VM supports IPv4/IPv6 name resolution through getaddrinfo() and creates TCP streams on demand during sockconnect or sockbind. Timeouts, blocking mode, TCP_NODELAY, and SO_KEEPALIVE are properties of the VM socket entry and are applied to the native descriptor when one is open. Accepted sockets inherit timeout and blocking mode from the listening socket.

Every socket entry carries a last-status slot and last-error string:

sockstatus reads the numeric slot and sockerror reads a short diagnostic string. Operations that return data or byte counts still update the status slot, so callers can distinguish an empty receive from timeout/EOF by checking status afterwards.

Client TLS is layered under the same registry entries. sockconnecttls is the portable client path: it connects to the host and port, starts TLS before any application bytes are exchanged, and uses the host operand for SNI and certificate name verification. After a successful TLS connect, socksend, socksendb, sockrecv, sockrecvb, and sockpending use the active TLS session. The instruction is available in all builds and records a negative status when no TLS backend is compiled in.

sockstarttls remains a lower-level true STARTTLS instruction for protocols that must exchange clear-text bytes before TLS. Backends that cannot upgrade an existing connection in place return a negative unsupported status rather than silently reconnecting.

TLS backends are selected with CREXX_ENABLE_TLS. Fresh CMake configurations default to NETWORK on Apple platforms, OPENSSL on non-Windows Unix-like platforms, and SCHANNEL on Windows. OPENSSL uses OpenSSL with default verification paths and hostname checks, and supports both direct TLS connect and true STARTTLS. SCHANNEL uses Windows SChannel/SSPI and the Windows trust store, performs hostname verification, and supports both direct TLS connect and true STARTTLS over an existing Winsock stream. NETWORK is macOS-only and uses Network.framework, Security.framework, and CoreFoundation.framework so certificate validation goes through the operating system trust store and VM binaries have no OpenSSL runtime dependency. The Network.framework backend supports sockconnecttls and reports true sockstarttls as unsupported because the public framework API does not upgrade an existing BSD socket in place.

Nested rxvml Calls

The public rxvml_call_procedure_descriptor(), rxvml_call_factory_descriptor(), and rxvml_call_method_descriptor() entry points run Rexx callables by installing a temporary external-call trampoline (ext_proc, ext_argc, ext_args, and ext_ret) and then entering run(). The descriptor form is mandatory as of RXVML_ABI_VERSION 8. Descriptors use rxsig1|name|return_type|args; for example, rxsig1|pkg.proc|.int|name=.string. The runtime resolves the name and checks the return/argument signature against metadata before invoking. These calls are allowed from native callbacks, including ADDRESS environment callbacks, while another run() invocation is already active.

Nested rxvml calls must save and restore both the external-call trampoline and the active rxvml_context. Without that preservation, a callback that calls back into a Rexx method can overwrite the outer callback invocation state and cause the original run() to return through the wrong procedure or with the wrong argument/return vector.

RXPA exposes the same nested runtime capability as CALLMETHOD (and the explicit-signal CALLMETHODX) in crexxpa.h. The VM derives the concrete class from the receiver’s runtime type, resolves and signature-checks the canonical method descriptor through the ordinary runtime method registry, prepends the receiver to the argument vector, and enters run() recursively. The helper is synchronous and requires the current same-thread rxvm_active_context; it does not create a background VM or permit plugins to retain VM value handles.

The ADDRESS sandbox/stem helpers use direct VM-layout mutation for the standard .standardaddresssandbox and .standardaddressstem classes, with nested method dispatch reserved as the fallback for non-standard interface implementations. This keeps repeated native callback writes stable while still allowing future custom Rexx objects to implement the same ADDRESS interfaces.

ADDRESS Environment Objects and Functions

ADDRESS environments are normal Rexx objects implementing _rxsysb.addressenvironment. The runtime registry caches those objects by normalised environment name. Rexx callers should use addressenv(name) to get the cached object for direct use. The older _address_environment(name) helper remains as the internal/runtime entry point.

Two optional sibling interfaces carry the prototype environment-context extension:

Native rxvml hosts register command and/or function callbacks with rxvml_address_register_callback_environment(ctx, name, id, command_cb, function_cb, userdata). The pre-release command-only rxvml_address_register_callback_environment(ctx, name, command_cb, userdata) signature was retired; pass NULL for id or function_cb when those features are not needed. The native provider object stores the callback handle and instance id, so both ADDRESS env "command" and explicit (addressenv(env) as .addressfunctionenvironment).invoke(...) reach the same host environment instance.

Native ADDRESS text uses the same Level B UTF-8 boundary as RXVML. CREXXSAA validates variable setter values before buffering write-back records, and the lower RXVML ADDRESS helpers validate command text, output/error text, sandbox updates, and stem updates before copying them into VM strings. Invalid bytes should be passed through .binary or native payload APIs rather than through text callbacks.

Windows does not change that internal contract. cREXX command lines, environment names/values and logical paths remain UTF-8; the platform adapter converts them to UTF-16 for CreateProcessW and other Win32 W APIs, then converts returned UTF-16 text to UTF-8. It must not change a process-global console code page or C locale to simulate worker-local UTF-8. Child standard streams remain byte streams: a provider must apply an explicit encoding converter when a child does not speak UTF-8, and the later channel envelope may carry that encoding metadata without changing its binary payload.

As of RXVML_ABI_VERSION 7, native rxvml_address_request also carries stdin_endpoint, stdout_endpoint, and stderr_endpoint VM values. Native providers should use rxvml_address_emit_output(ctx, request, text) and rxvml_address_emit_error(ctx, request, text) rather than reaching into those redirect values. These helpers write to ADDRESS ... output/error redirects and finalize them so Rexx array/string captures are readable when the callback returns; without a redirect they write to the normal VM stdout/stderr path.

ADDRESS redirect endpoint values are native payloads with the internal type name rxsysb.redirect_endpoint. The payload stores a refcounted native endpoint cell, so ordinary VM value copies retain the cell instead of byte-copying raw OS handle values. Its private completion contains the endpoint handle, independently allocated bytes, diagnostics and one terminal state; it contains no worker or VM value pointer. Input endpoints snapshot their source before thread creation. Output endpoints are consumed only after join, when the receiver worker copies the bytes into the Rexx string or array. The last finalizer closes native handles, joins any owned redirect worker thread and discards an unconsumed completion. SPAWN resolves these native endpoint payloads before dispatching to rxspawn.c; it does not accept plain raw REDIRECT binary buffers. Existing bytecode remains compatible because redirect values are created by runtime instructions, not stored as durable REDIRECT struct bytes in .rxbin files. New runtime code should not inspect binary_value directly for redirect endpoints; use the redirect helper path or the public RXVML emit helpers.

ADDRESS command text may contain host-variable anchors whose meaning belongs to the selected environment handler. The compiler auto-exposes visible Rexx scalar variables named by :name inside string-literal command text, and by ${name} inside command text that reaches the ADDRESS exit. The scanner also exposes stems for :name[], :name., ${name[]}, and ${name.}. The command string itself is not interpolated by the VM. Rexx providers read exposed scalar values from addressrequest.get_binding_value(name) and stem values from get_binding_stem_value; native providers can use rxvml_address_binding_get(request, name, out, out_len) for scalar bindings. Handlers that write a host variable return a normal updated binding, so the existing ADDRESS write-back path handles both explicit EXPOSE variables and auto-exposed anchors.

The built-in command environments split into four spawn modes:

CREXX multiple-command handling is intentionally explicit: command text is a single CREXX command and shell operators such as ;, &&, ||, and pipes are usage errors. Use repeated ADDRESS statements or ADDRESS CREXX "batch" with input lines. batch skips blank lines and -- comments and stops at the first non-zero return code.

demos/native/sqlite/ shows the database-oriented form of the native provider model. The provider routes by the ADDRESS environment name carried in the request (SQLITE initially), looks up a driver table, and then treats SQL named parameters such as :name as handler-specific uses of ADDRESS host-variable bindings. This is the intended shape for later database drivers.

demos/llm/llm_address_environment.crexx shows the same idea for Rexx-hosted providers. One Rexx environment class claims a family of model-shaped environment names (LLM_GPT_4_1, CLAUDE_SONNET_4_5, GEMINI_2_5_FLASH, GEMMA4_LATEST) and routes internally to the rxfnsg LLM drivers. This keeps model dispatch cheap: _address.rexx caches the constructed environment object by normalised name after the first lookup, and the provider performs a small registry lookup over driver-contributed exact aliases and prefixes rather than probing every driver on every command.

For Rexx callers, addresscall(env, name, ...) -> .string is the simple string-returning convenience surface over _address_function(...). _address_call remains as the internal/runtime spelling for existing code. Use _address_call_response(env, name, ...) -> .addressfunctionresponse when the caller needs the function rc, condition, or diagnostics. This helper layer is provider-neutral: Rexx and native ADDRESS environments see the same addressfunctionrequest protocol underneath.

3. The Execution Loop

The core execution engine lives in run() within interpreter/rxvmintp.c.

Product and concrete dispatch executables

The build has one stable product entry point and up to two concrete engines:

Clang and AppleClang select rxbvm; GCC selects rxtvm; MSVC builds only the switch engine and copies rxbvm.exe to rxvm.exe. The broad correctness and smoke suites execute rxvm. A small explicit dispatch-contract test runs each concrete engine that exists, so the non-default engine does not become an unbuildable blind spot.

rxvme and the rxvml embedding library follow the same compiler-selected engine. rxbvme and rxbvml remain explicit switch-dispatch forms.

Threaded vs switch dispatch

The VM uses conditional compilation (#ifdef NTHREADED) to flip between two execution models:

  1. Direct Threading (rxtvm): During the preparation phase, rxvm_prepare() copies each module’s canonical instruction slots into an owned runtime image. Operand cells remain unchanged and each instruction cell stores the C void* for its &&label. Target selection loads next_pc->handler, and dispatch uses goto *next_inst;. The runtime image is process-local, is never serialized or exposed through reflection, and is refreshed safely after a late link.
  2. Switch Dispatch (rxbvm, NTHREADED): Executes the owned execution_image through a C switch(opcode) statement. Public instruction cells initially contain copied canonical opcodes; preparation may replace a selected cell with a process-private opcode while leaving segment.binary unchanged. Operands, calls, branches and active-frame transitions use the same owned-image contract as rxtvm.

Neither source form is assumed to be universally faster. Generated performance depends on compiler transformations, architecture, branch prediction, code layout, and the cost of locating the next handler. The current Release 1 investigation is tracked in docs/planning/beta-3/notes/vm-dispatch-performance-investigation.md.

Active-frame and dispatch contracts

Every frame change passes through VM_ACTIVATE_FRAME (or its nullable counterpart). That boundary refreshes the active frame, binary space, module, execution base, canonical base, constant pool, and locals as one coherent state change. Debug builds assert that all cached pointers agree with the active frame. Operand and branch macros consume only that coherent state.

Instructions select their next target through intent macros. Sequential flow uses VM_ADVANCE, canonical indices use VM_SELECT_INDEX, and already resolved execution pointers use VM_SELECT_POINTER. Computed-goto and switch differences stay inside these macros, and handler resolution still happens before the current handler body completes. For example:

#define VM_ADVANCE(n) do { next_pc = pc + (size_t)(n) + 1; VM_RESOLVE_SELECTED(); } while (0)
#define DISPATCH do { pc = next_pc; if (pending_interrupts && !current_frame->is_interrupt) goto INTERRUPT; VM_DISPATCH_TARGET(); } while (0)

DISPATCH actively checks the current VM’s sole direct pending-interrupt word to immediately branch into signal exception handling if an error occurred natively. Internal signal-raising macros stamp interrupted_pc with the faulting instruction before dispatch advances pc; breakpoint and asynchronous interrupts leave it unset so their handlers continue to receive the next instruction/resume address. The default fallback panic report uses the stamped address when present to print the module/address and, when META_SOURCE_STEP metadata is present, the closest preceding REXX source line. Linked images built with source stripping have only the module/address for this fallback context.

VM signal codes 1 through 31 map to the non-sign sig_atomic_t mask bits 0 through 30. RXSIGNAL_MAX is a sentinel and has no mask bit. All producers and consumers use the validated rxsignal_mask() helper, and the interrupt scan stops before the sentinel bit; this keeps invalid codes from shifting by a negative count or into the signed high bit.

INTERRUPT is the internal dispatch target used by this macro, not a source instruction. INULL and IUNKNOWN are runtime sentinel handlers that raise UNKNOWN_INSTRUCTION; rxas intentionally rejects all three names as source mnemonics. Opcode slot 514 is now ENDLIFE_REG; the later opcode numbers are preserved.

Handler definitions and placement policy

Every public opcode handler, both runtime sentinels and both private execution-image handlers have one semantic definition behind RXVM_HANDLER(...) or RXVM_PRIVATE_HANDLER(...). The definitions are split by concern across rxvmhandlers_core.inc, rxvmhandlers_control.inc, rxvmhandlers_numeric.inc, rxvmhandlers_string.inc, and rxvmhandlers_system.inc; run() no longer owns a second copy of any handler body. A definition keeps the instruction-entry/debug code and its implementation together.

The definition files are expanded twice. At file scope they produce force-noinline rxvm_handler_NAME(rxvm_handler_state *) functions. Inside the dispatch owner, handlers selected inline by rxvmhandlerpolicy.h expand the same implementation body directly. The internal lowering of outlined handlers is compiler-specific because Clang and GCC generated materially different hot dispatch from the same source:

Both forms return the same small rxvm_handler_result: normal dispatch, interrupt dispatch, interrupted-instruction resume, interrupt-table OOM, or terminal cleanup. The Clang snapshot is deliberately value-based because the earlier pointer-rich facade made owner locals addressable whenever any outlined handler was reachable, changing Clang register allocation and stack shape even when no outlined handler ran. The all-inline panel preserves the exact R2 pointer-facade source shape so compiler heuristics and generated owner layout do not move merely because dead facade scaffolding was deleted; the optimizer eliminates that facade and no shared function table or trampoline is emitted.

In the Clang shared-cold form, threaded dispatch obtains an outlined public opcode from the immutable canonical image because its execution-image instruction cell contains a handler address; switch dispatch reads the numeric execution-image opcode. Label ownership, interrupt polling and final dispatch remain in run() in every lowering.

The CMake cache setting CREXX_VM_HANDLER_PANEL selects the internal build shape:

Every handler has one central tier rather than one definition per panel. Both private fused handlers enter at the 5% tier. The current 55-handler NEVER class covers provider channels, sockets, console I/O, clocks/environment access, file I/O and dynamic module loading. It is a reviewed code- placement attribute: literal all-inline deliberately ignores it to remain the exact equivalence control, while every profile and max-eligible honors it. A later profile can justify an explicit tier change, but a percentage threshold cannot silently override it.

The current table has 584 source-visible public handlers, two private fused handlers, 68 reserved opcode handlers, two reserved sentinels and the owner-internal INTERRUPT target. The candidate totals remain 31, 61, 90, 120 and 175 for the nominal 5%, 10%, 15%, 20% and 30% panels; three top-176 host operations remain callable. max-eligible is 531/586 (90.61%) across the normal public-plus-private handlers. INTERRUPT has an explicit owner-only, always-inline tier.

The panel setting changes no RXAS/RXBIN encoding or public/plugin ABI. Adrian selected common profile-20 as the provisional product default after the R5 Apple comparison and explicitly accepted the measured 10.072% GCC rxtvm Bounce regression. On this Apple host, Clang’s absolute profile-20 throughput was also directionally faster than GCC’s in every governed workload/engine cell, although the compiler runs were not a paired compiler-selection trial.

The exact 20% membership is not a permanent tuning claim. Release-finalisation work must rebuild the panel from a wider current benchmark portfolio, include private/fused dispatch and newly added instructions, audit the never-inline ledger, and retain panel-membership diffs over time. Intel Linux is the next platform validation. all-inline remains available as the invariant control, and explicit -DCREXX_VM_HANDLER_PANEL=... selections remain supported for diagnosis and cross-platform comparison. Existing CMake build directories keep their cached panel; use a fresh build tree or set the option explicitly when validating the new default.

The complete sequence of accepted and rejected source shapes, the Clang/GCC code-generation differences, and the current rules for preserving maximum observed C optimisation are recorded in the VM and C Compiler Optimisation Report.

Process-private fused execution handlers

Preparation performs two structural, process-private fusions in each module’s owned execution image. This is immutable load-time quickening, not adaptive runtime rewriting: canonical RXBIN and segment.binary remain unchanged, and the prepared site is not subsequently rewritten or dequickened.

rxtvm writes the corresponding private label address into its execution image; rxbvm writes a numeric opcode above the public opcode range. Both map instrumentation back to the first canonical public opcode so public semantic profiles remain comparable. Placement/heat profiling must additionally count the private dispatch identity: attributing it only to UNLINK or LINKATTR1 can incorrectly classify a frequently executed private handler as cold.

The shared inventory and gap report between RXAS static fusion and these VM load-time fusions is a separate roadmap item, PERF3-05-R4. Making a private fusion a normal serialized instruction, or adding adaptive runtime quickening, requires its own RXAS/RXBIN compatibility and architecture decision.

Instruction Flow Example

The assembler passes operands inline sequentially in the binary array. A handler definition is written once and the placement policy supplies its dispatch label/case:

RXVM_HANDLER(IADD_REG_REG_REG,
    VM_ADVANCE(3)
    DEBUG("TRACE - IADD R%lu,R%lu,R%lu\n", REG_IDX(1),
          REG_IDX(2), REG_IDX(3));
    REG_RETURN_INT(op2RI + op3RI)
    DISPATCH
)

In this example:

VM_ADVANCE(n) and REG_OP(n) accept any operand position represented by the opcode signature; handlers are not restricted to the traditional three named operand aliases. CNOP_REG_REG_REG_REG_REG_REG_REG_REG_REG is the focused nine-operand execution and tooling regression.

Opcode effects inventory

The VM handlers are also the semantic evidence for the machine-readable opcode effects inventory consumed by RXAS and future data-flow work. The canonical structural inventory remains binutils/include/rxops.h; the complete ordered semantic sidecar is binutils/include/rxopeffects.h; and rxop_effects() in binutils/rxopmeta.c exposes their consolidated C API. Effects cover explicit and implicit register access, proven overwrites, branch-target operands, calls/returns, aliases, references and storage lifetimes, possible exceptional transfer, indirect effects and conservative barriers.

The inventory is an audited contract, not a parser for arbitrary handler C. Handler-sensitive classifications are checked against the implementations in this instruction region and representative semantic tests. Every opcode slot must have an entry. Reserved/internal slots, explicitly conservative entries, and unknown/out-of-range API queries fail closed: they remain barriers and offer no kill proof. NR-04 does not change VM execution or serialized RXBIN and does not enable a new optimizer transformation.

Private native stem representation

NR-15’s stem* handlers share interpreter/rxvmstem.h in both dispatch modes. The D2-hybrid representation uses the receiver’s ordinary binary buffer for a versioned little-endian header, 256 bucket heads, and 16-byte hash/next/generation entries. Ordinary VM attributes own the keys, values, and default string, so normal deep copy, move, reference lifetime, and destruction continue to own all string storage. Bucket and next indexes are one-based; zero is the chain sentinel. Entries stay in insertion order.

The layout is private process-local state, not public ABI and not serialized into RXBIN. RXBIN007_FEATURE_NATIVE_STEM gates the nine instruction forms, while each runtime receiver is validated for magic, version, exact size, capacity/count consistency, attribute coverage, and bounded chain traversal. Allocation-reporting paths commit logical insertion, update, reset, or output replacement only after their required storage succeeds; reserved private capacity may remain reusable after a failed insertion. Generation overflow, capacity overflow, invalid extraction indexes, and corrupt metadata are translated to VM signals by the shared handler macro.

stemget2 and stemset2 hash and compare two string registers with one . separator without constructing a hit/miss key. stemset2 constructs the canonical joined key once, and only after absence is proved. Compiler direct lowering is intentionally limited to proved simple-storage concrete rxfnsb.stem receivers; complex or reference-sensitive receiver shapes keep the normal method path, whose library body invokes the same native operations.

interpreter/rxvminstrument.h is the compile-time instrumentation contract for both VM modes. A backend can observe VM begin/end, instruction begin plus retire or terminal, frame activation, call/return transitions, and interrupt selection/entry/resume/terminal paths using canonical module/instruction coordinates. The contract also exposes native-call boundaries and module-set changes needed by callable profiling. With no backend selected, every hook preprocesses to a no-op and does not evaluate its arguments, branch, access state, or call a function.

Timing/count profiling

CREXX_VM_PROFILING selects the timing/count instrumentation backend for a dedicated VM build. The option is off by default, so ordinary builds retain the fully preprocessed no-op hook contract. Configure a Release profiling build with:

cmake -S . -B cmake-build-profile \
  -DCMAKE_BUILD_TYPE=Release \
  -DCREXX_VM_PROFILING=ON
cmake --build cmake-build-profile --config Release \
  --target rxvm rxbvm rxtvm rxvme rxbvme rxseq

Omit rxtvm when the configured compiler does not support GNU-style labels-as-values (notably MSVC).

The command-line surface is compiled into compiler-selected rxvm, concrete rxbvm, optional concrete rxtvm, and the embedded-standard-library variants rxvme and rxbvme. Timing profiling remains off at runtime until --profile, --profile=timing, or --profile-output is passed. --profile=counts selects the same diagnostic census with clock reads disabled and all timing fields zero, so repeated counts-only reports are deterministic. By default the report is written as a human-readable table to standard error. Use --profile-output file or --profile-output=file to write it to a file. An output filename ending in .csv, case-insensitively, selects CSV; every other filename selects the table format. The output option also enables profiling. Profiling options are parsed before the first RXBIN filename; ordinary program arguments continue to follow -a.

rxvm --profile program.rxbin
rxvm --profile-output profile.txt program.rxbin
rxbvm --profile-output profile.csv program.rxbin
rxvm --profile=counts --profile-output counts.csv program.rxbin

The instruction table measures monotonic wall time from instruction entry to retire or terminal. The transition table measures retire to the next instruction entry and distinguishes same-frame sequential/branch transitions, call frame entry, return frame exit, interrupt entry/resume, external entry, and termination.

Each instruction row also reports the effective handler placement as inline, outline, or mixed. Profiling records placement at the actual handler-entry boundary while retaining canonical public-opcode timing/count attribution, so a private fused handler cannot silently inherit an incorrect label from its serialized opcode. mixed means the same canonical opcode was observed through both placements. CSV schema 5 writes this in the existing value column; ordinary profiling-off builds still preprocess the added hook argument away without evaluating it.

The same report contains procedure/method and call-mechanics tables. Callable names, return types, and argument signatures come from META_FUNC; a module/procedure fallback is used for older binaries without that record. The procedure table classifies runtime rows as procedure, method, factory, or native and reports calls, normally completed calls, calls discarded by an exceptional unwind, elapsed call time, inclusive body time, and self time. Rows are sorted by elapsed time (native total for native rows). Inclusive body time includes nested bytecode calls, so rows overlap; self time does not. Each bytecode row also reports native_child, the observed native-call time removed from self time. It is already inside inclusive body time and must not be added to elapsed time. Inlined calls have no runtime frame and therefore remain attributed to their containing procedure rather than appearing as separate rows.

For bytecode calls, the normal-return boundaries are:

caller call-instruction entry
    -> callee first-instruction entry       entry overhead
    -> callee return-instruction entry      inclusive body
    -> caller next-instruction entry        exit overhead

Elapsed time covers the whole outer span. These are observed VM call-mechanics spans, not an estimate of the speed-up from inlining. External entry and terminal return are measured against their nearest available VM boundary. Dynamic calls are attributed to the concrete runtime procedure.

The same profiling-only backend also records the NR-05 dynamic call census at authoritative VM boundaries. It distinguishes direct/dynamic bytecode and native calls, external/root entry, signal-handler entry, failed/unresolved attempts, exact actual arity, callable metadata kind, and fresh/reused/no-child frame disposition. RET_REG records its actual true-local move or non-local copy branch; void, ignored, immediate, terminal, and unwind domains remain distinct. SRCMETHODSEL and SRCFPROCSEL record attempt/success/failure separately from the later DCALL.

Call-window attribution uses a dynamic backward slice of the executed straight-line trace, not source names or adjacency. The NR-04 rxop_effects() kill/flow facts stop the slice at proven definitions. Reached SWAP_REG_REG operations are setup swaps and reached whole-value COPY_REG_REG definitions are defensive argument copies. For normal return, the profiler reconstructs the pointer permutation made by those setup swaps and buffers subsequent swaps until their combined effect recovers the pre-call mapping; only that completed restoration sequence receives credit. A setup whose mapping is deliberately carried into another call or to frame completion has no completed normal restoration, but does not by itself imply lost profiling data. All remaining executed swaps/copies stay unclassified. The cold signal paths report discarded frames, restored bytecode/native call windows, actual inverse pointer swaps, and restoration failures without changing the existing restoration algorithm.

Native plugin calls are listed with call count and total time around rxvm_callfunc. Body/self and entry/exit breakdowns are intentionally omitted because the VM cannot observe the native implementation’s internal phases. Native time remains part of its calling instruction’s instruction timing, but is removed from the bytecode caller’s self time to avoid double attribution in the procedure view. The instruction, transition, and procedure sections are overlapping views and must not be summed together.

Every retired hot-loop instruction also increments the interrupt-poll count. Taken interrupt scans and the mechanics from selection to the first handler instruction, resume, or terminal outcome are reported as sub-phases. These sub-phases overlap the complete interrupt transition time and must not be added to it. The no-pending poll itself remains inside ordinary transition time; the profiler intentionally does not add another pair of timer reads around that check.

CSV output retains the original columns in their original order and identifies the format as schema version 5. Schema 5 preserves the same 24-column header:

section,name,value,id,count,total_ns,average_ns,min_ns,max_ns,percent,selected,entries,resumes,terminals,module,kind,completed,unwound,return_type,args,bytes,max_bytes,high_water,status

Procedure rows use value to distinguish elapsed, inclusive_body, self, native_child, entry_overhead, exit_overhead, and native_total metrics. There are multiple metric rows per bytecode callable rather than one denormalized row.

Schema 5 retains all schema-4 rows and adds explicit per-domain status rows, value_operation rows for whole-value/typed transfer and teardown helpers, frame_entry rows split by fresh/reused frame source, and canonical branch site rows. For a branch row, selected, entries, resumes, and terminals carry taken, fall-through, same-module backward-target, and cross-module-target counts. For a frame-entry row, id carries phase units. Schema-5-aware readers must continue to accept schema 4 and treat these new domains as unavailable rather than fabricating zeroes.

Schema 4 retained the schema-3 allocation rows and added census aggregates, exact call rows, return placement, dynamic selection, mechanics attribution, and unwind restoration rows. For call rows only, the existing bytes, max_bytes, and high_water columns carry setup swaps, normal restoration swaps, and defensive argument copies. Summary and row status expose overflow or degraded tracking; zero-count categories are retained.

The allocation counters are private profiling instrumentation, active only between profile begin/end, and count successful requests rather than live heap:

The allocation scope excludes loader state, profiler/RXSEQ bookkeeping, plugin-private/native-payload ownership, reference-lifetime payloads, OS/TLS, and temporary native conversion storage. Allocation rows carry complete, overflowed, or degraded; summary rows retain counter overflow plus procedure/allocation tracking availability. A non-zero active-frame balance degrades the frame-activation row.

Timing values are raw instrumented wall times. The report includes the minimum positive adjacent clock-read interval and zero-delta calibration count, but does not subtract them from short instructions. Compare instruction shares within equivalent profiled runs rather than treating the values as the uninstrumented VM’s absolute cost. Instruction and transition counters use fixed per-run arrays; the common hot path performs no allocation, locking, callbacks, sorting, or output formatting. Allocation accounting itself is fixed-state TLS-local addition at the existing allocation/frame boundaries. The procedure activation array is allocated once per run and grows only if call depth exceeds its current capacity. Bytecode procedure timing reuses the existing instruction timestamps; only native calls add a dedicated timer pair, and exceptional stack unwinds add a timestamp outside the ordinary path.

Do not use an inactive profiling build as the uninstrumented performance baseline: the enabled backend still contains inexpensive runtime guards. Only a build configured without CREXX_VM_PROFILING has the compile-time no-op shape.

Dynamic instruction-sequence profiles

The same CREXX_VM_PROFILING build can extract executed windows of two, three, or four instructions. This is a separate run mode from timing profiling and must be given an .rxseq output file:

cmake --build cmake-build-profile --target rxvm rxbvm rxtvm rxseq

rxvm --sequence-count=2 --sequence-output run.rxseq program.rxbin
rxbvm --sequence-count 4 --sequence-output run.rxseq program.rxbin
rxtvm --sequence-count 4 --sequence-output run.rxseq program.rxbin

--profile and --sequence-count are intentionally mutually exclusive. Ordinary builds configured without CREXX_VM_PROFILING contain neither runtime surface.

The VM records dynamic execution counts against (module, canonical starting instruction slot, window length). At run start it allocates one 64-bit counter per expanded instruction slot in every loaded module; the on-disk result is sparse. A window continues only across actual sequential fall-through transitions in the same module and frame. Taken branches, bytecode-frame calls and returns, interrupt entry/resume, external frame entry, and termination break it. A native call that returns normally remains within its CALL instruction and can participate in a sequential window. A branch may be the last instruction in a window, but a window never crosses the branch when it is taken. Loop iterations increase the recorded site count.

The extractor writes a versioned binary instruction-sequence execution profile. It starts with the eight-byte RXSEQBIN magic. Its fixed 48-byte header uses little-endian integers and records the format version, header size, sequence length, VM result, flags, module count, and sparse site count. Each module record contains a variable-length ID, fixed little-endian 64-bit expanded-content hash, variable-length instruction size, and a length-prefixed UTF-8 name. Each site stores (module ID, start slot, count) as canonical unsigned LEB128 values, so common records take only three to five bytes. The format contains no process-sized integers, native structure padding, or host-endian fields.

The VM deliberately does not decode or normalise operands in the interpreter hot loop. It writes only non-zero aggregated sites, so repeated loop executions increase a 64-bit count rather than increasing the file length.

Run the offline second stage with the same RXBIN module set:

rxseq run.rxseq program.rxbin library.rxbin
rxseq run.rxseq program.rxbin library.rxbin --output candidates.csv

Module argument order does not matter, but every profiled module must be present with the exact content used by the run, and no additional module may be supplied. rxseq fails on a missing module, content-hash mismatch, or instruction-size mismatch. The profile records all loaded modules, even those with no non-zero site. Consequently, a capture made with rxvme or rxbvme also requires the exact RXBIN image corresponding to its embedded standard library. Prefer rxvm/rxbvm with explicit inputs or a single linked image when a self-contained sequence-analysis workflow is needed.

For each site, rxseq decodes the emitted RXBIN instructions and alpha-renames operands by first occurrence across the whole window. Registers use r1, r2, and so on; every other encoded operand (literal, pool constant, label, or procedure reference) uses c1, c2, and so on. Reuse is retained:

IADD_REG_REG_REG(R17,R5,R9) | COPY_REG_REG(R5,R22)
    -> IADD_REG_REG_REG(r1,r2,r3) | COPY_REG_REG(r2,r4)

Sites with the same normalised pattern are clustered, and their dynamic counts are summed. The report includes execution count, static site count, module count, one concrete mapping/example, and candidate status. Operand normalisation and mapping text are dynamically sized, so candidates are no longer screened out merely for having more than three distinct symbols. This is only candidate extraction: control-flow, liveness, aliasing, exceptions, interrupt behaviour, and other transformation safety must be reviewed separately before defining a combined opcode or optimiser rule. The table heading also reports the window length, cluster/site counts, and counter-overflow status. An output name ending in .csv, case-insensitively, selects CSV; otherwise rxseq writes the human-readable report format. Candidate CSV uses:

rank,count,sites,modules,symbols,status,pattern,mapping,example_module,example_start

Frozen PARSE execution

NR-14 adds four canonical RXBIN 007 instructions behind RXBIN007_FEATURE_FROZEN_PARSE. parsewords3, parsepos2, and parsewords3d implement exact common plans with direct register results; their handlers preserve source/output aliasing by snapshotting source bytes before the first write. parsewords3 is also a chain primitive for eligible longer implicit-word templates.

parseplan executes a compact descriptor from a string constant into a reusable result vector. Version 1 stores frozen item kinds, store/drop flags, literal bytes plus character lengths, fixed-width numeric movement, and the declared item/result counts. Version 2 additionally stores indexed dynamic delimiter and position references. A reference selects either an earlier completed capture or an external operand that compiler-generated code placed temporarily after the public result slots. The handler consumes those values without textual plan decoding and shrinks the vector to its public result count on success.

The handler bounds-checks the header, reference indexes, and every item; rejects trailing or structurally inconsistent data with INVALID_ARGUMENTS; and raises CONVERSION_ERROR for an invalid dynamic numeric position. It uses Unicode code-point positions in UTF builds and has no load-time cache or private prepared representation.

Compiler eligibility remains fail-closed: exact common forms use the direct instructions and every remaining supported exit form uses parseplan, including logging/TRACE and explicit INTO. Ordered source-level assignments remain outside the VM primitive, preserving repeated targets, source aliases, trimming, and TRACE source metadata. There is no runtime textual PARSE executor.

Pooled float operands

As of rxbin format 002 and later, float literals are loaded from the constant pool instead of being stored inline in operand slots. The bytecode still keeps the same instruction formats (FMT_F, FMT_R_F, FMT_R_R_F, etc.), but the operand slot now contains an index into a FLOAT_CONST record in const_pool, and the interpreter resolves that record when a float operand is read.

In RXBIN 007 the loader decodes the canonical variable-integer instruction section and portable constant/metadata records before execution. The base format deliberately does not compress sections. By the time run() starts, the module again presents a normal bin_code[] plus native constant-pool buffer, while graph-bearing operands remain dense graph IDs.

4. Current Interface Dispatch in the VM

The current Level B interface runtime slice adds these VM-facing pieces on top of the older object model:

srcmethodsel and srcfprocsel both return a proc_runtime * in a normal register, and the existing dcall path performs the actual invocation. Human RXAS/RXDAS uses callable descriptors (rxsig1|name|return_type|args), but a linked 007 instruction carries a numeric graph member/factory ID. Signatures are validated while the graph and bindings are built, not reparsed on the sealed success path.

Current srcmethodsel semantics

The current graph fast path is:

Current srcfprocsel semantics

The current graph fast path is:

Graph construction matches source-short and canonical class-type spellings in factory signatures. This prevents a valid source descriptor from creating a duplicate providerless factory bucket during RXAS/RXLINK remapping. The standalone graph harness audits the numeric IDs embedded in executable instructions in addition to measuring a known-valid bucket.

Runtime module loading matters here as well. METALOADMODULE marks the VM link state dirty and immediately calls rxvm_link() after a successful load, so later srcfprocsel, srcmethodsel, and direct imported calls can see the new provider without an automatic filesystem sweep.

The crexx driver keeps bare -l names as packaged libraries below CREXX_HOME/bin, but any -l value containing / or \\ is an exact path. Late-loading applications should pass the intended .rxbin filename to loadmodule() explicitly; neither the driver regression nor the VM late-load path searches user-controlled directories for a provider.