CREXX

REXX Language implementation

View the Project on GitHub adesutherland/CREXX

cREXX Level B Authoring Guide For Agents

Use this guide when you need to write or edit Level B/Level G .crexx in this repository. Generic training data about “REXX” is often too vague, too classic-Rexx oriented, or simply wrong for cREXX Level B as it exists in this tree.

This guide is intentionally grounded in code that already compiles here. When in doubt, copy the nearest repo pattern instead of inventing syntax.

What To Trust First

For Level B authoring, these sources are more reliable than model memory:

Repo-Native Level B Patterns

1. Small library function

Use explicit options levelb, a namespace, and an exposed symbol:

options levelb

namespace rxfnsb expose abs

abs: procedure = .string
arg number = .string
if left(number,1) = '-' then number = substr(number,2)
return number

Reference:

2. Top-level script

Small scripts can use top-level arg directly instead of a main: routine:

options levelb
import rxfnsb

arg searches = .string[]

if searches.0 < 1 then searches.1 = ".crexx"
ordered_searches = .string[]
address crexx "echo .crexx" output ordered_searches

References:

3. Tool-style entry point with procedure-exposed module state

Longer tools often use main: plus procedure-level expose for module state:

options levelb
namespace rxdb expose stephandler
import rxfnsb
import globals

main: procedure = .int expose next_instruction last_instruction mode
    arg cmd_line = .string[]

The expose list belongs to that one procedure. Other local procedures see the same module-global storage only if they also list the variable:

proc1: procedure = .void expose var
    var = "Hello World"
    return

proc2: procedure = .void expose var
    say "var is" var
    return

Reference:

4. Mutating an exposed argument

When you really need caller-owned state, use arg expose ...:

pushidentifier: procedure = .int
    arg expose tokens = .token[], text = .string, value_type = ".unknown"
    index = tokens[0] + 1
    tokens[index] = .token(...)
    return index

Reference:

Level B Differences That Matter In Practice

Types are normal, not exceptional

In this repo, typed procedures and typed arg declarations are the normal Level B style. Do not default to untyped classic-Rexx-looking code when you are editing standard libraries, tools, exits, or tests.

Common examples in-tree:

Use bare type expressions to declare a typed local before later assignment when that makes scope or intent clearer:

slot = .int
if map.containsKey(key) then slot = existingSlot(key)
else slot = createSlot(key)

Prefer this to a dummy literal such as slot = 0 when the value is not semantically a sentinel. This is especially useful when a local is assigned in both sides of a branch and then read after the branch.

References:

Library Changes Are Whole-Toolchain Tests

Treat every Level B library change as an opportunity to exercise the complete consumer path: compile with rxc, assemble with rxas, link with rxlink, and execute with rxvm (and both concrete VM dispatches when the behavior is VM sensitive). A direct compiler or library-unit check is not sufficient for a public library surface because imported metadata, optimized RXAS shape, link-time symbol resolution, or runtime behavior can fail independently.

For optimizer-sensitive helpers, retain no-opt and optimized executions with the same expected result and inspect the optimized RXAS when the intended shape matters. Supported inlining shapes must be repaired when they miscompile; an implementation defect or awkward generated shape is not a reason to fail closed. Falling back to a real call is reserved for cases where ordinary-call equivalence is mathematically impossible or cannot be established from the language semantics and available facts.

Namespace/import syntax is part of the real language surface

Do not treat namespace use as documentation sugar. It is part of how source is validated, imported, and linked.

Important points:

References:

Keep the leading file header conventional

The compiler does a lightweight pre-scan of the leading options, namespace, and import clauses before full parsing. Preserve that structure when editing existing files.

Practical guidance:

Reference:

Procedure-level expose and namespace auto-bind

There are two ways a Level B procedure can intentionally bind a module-global variable:

Use procedure-level expose for private module state that several local procedures share, or when you are following an existing stateful tool/library pattern. Do not add procedure expose ... just out of habit when a namespace-exposed module global is what you actually want.

Use procedure expose or arg expose when:

References:

Module initializers are private lifecycle procedures

Use a module initializer when module-global state must be constructed before main, an embedded public call, or a task-worker request can observe that module instance:

options levelb
namespace example expose value

boot: initialiser expose value
  value = 42

read: procedure = .int expose value
  return value

A module may declare zero or more initializers. They run once for each mutable module instance, in declaration order. Each initializer has an ordinary namespace-qualified name for metadata and diagnostics, but it is a private lifecycle entry point: source cannot call it, import it as a callable, or list it in the namespace expose clause. The expose after initialiser has the same module-variable binding purpose as procedure-level expose; it does not export the initializer.

Initializers accept no arg declarations and have an implicit .void return. A bare return is valid; returning a value is a compile error. If an initializer calls an ordinary procedure in another unready module, the VM initializes that module first. There is no source requires list, and a cycle through cross-module initializer calls fails at runtime.

Use this for module-owned singleton-like objects, tables, caches, and resource managers. The lifetime is one mutable module overlay, not one process-global instance. In particular, persistent task workers each initialize their own overlay once before accepting work.

Named constants are compile-time values

Use constant NAME = expression inside an explicit procedure, method, or factory scope for Level B masks, flags, and shared literal payloads that must not allocate runtime storage:

flags: procedure = .int
  constant RV_FLAG_STRING = 0x00010000
  constant SAMPLE_BYTES = "41424344"x as .binary
  return RV_FLAG_STRING

Do not put constant declarations in the file body. That top-level form is a Release 1 design defect being removed because it can imply an implicit main() in a module that was meant to be a library.

If a script needs a separate declaration procedure for shared constants, add an explicit main: procedure before the executable body. Procedure bodies continue until the next top-level callable boundary, so statements after a declaration procedure belong to that procedure unless a new boundary is present.

The initializer must fold at compile time. Integer constants used as assembler immediates are emitted as literals. An exact .binary use of a named constant is emitted through one compiler-private, module-scoped RXAS .const alias, so large payload text appears once and every operand resolves to the same RXBIN constant-pool item. This is a read-only operand reference, not a hidden mutable register or runtime copy. Converted or completely folded uses follow their ordinary result lowering. String, decimal, and float constants retain their normal constant-pool machinery.

Generated code that shares a family of constants inside one source module must list each name explicitly in the declaration procedure’s procedure expose list. Release 1 does not import constants across source, RXAS, or RXBIN module boundaries. Cross-module constants and wildcard expose forms such as TOKEN_* are Release 2 ergonomics/design candidates, not Release 1 syntax commitments.

Choose encoded fields or host-native packed items deliberately

Use the established <at..type>(byte_offset) surface for portable binary formats. Those integer and float fields have explicit widths and canonical little-endian encoding.

Use <packed..int>(item_index) or <packed..float>(item_index) only for host-local numeric storage where the exact VM representations are required:

values = .binary
call binresize values, 3 * 8

<packed..float>(0) values = 100.0
<packed..float>(1) values = 125.5
say <packed..float>(1) values

Packed indexes are zero-based item numbers. .int means the exact host rxinteger representation and .float means the exact host VM double representation. The buffer has no stored type tag: the same bytes can be read through either packed view. The buffer length and binresize remain byte based, and a packed store never resizes it. Packed access is therefore unsuitable for files, protocols, persistent cross-platform data, or data exchanged between hosts with different native representations.

Only .int and .float are valid packed suffixes in Release 1. Ordinary binary allocation provides native alignment, readable binary constants are materialized into aligned runtime storage, and packed constants remain read-only. Invalid or overflowing indexes and partial trailing items raise OUT_OF_RANGE before any write occurs.

Explicit register views are system-programmer syntax

Normal classes should use ordinary attributes. Runtime and VM-integration classes may map attributes to fixed register storage:

  _string = .string with register.0.string
  _flags = .int with register.0.flags.library

register.0 is the containing value, not RXAS attribute zero. Duplicate typed views over the same physical register.N slot are complex attributes. The compiler copies only the selected typed payload view across the link boundary; library/user cache flags are explicit runtime code, not hidden compiler side effects. Flag views are direct status-word views: .flags.vm, .flags.compiler, .flags.language, and .flags.readable are read-only; .flags.library, .flags.user, and .flags.public are writable. Flag views must be .int.

.flags.language contains protected, language-wide facts such as intrinsic .string normalization certificates. Ordinary Level B code may inspect this view but cannot assign it. A trusted low-level language implementation may use explicit RXAS GETANDTP/SETORTP operations to consume or publish a proven fact; doing so without proving the complete current string contents violates the runtime contract.

Arrays are first-class Level B objects

Do not reason about them as loose classic stem variables only.

Patterns used in-tree:

Do not initialise or resize an array by assigning to index 0. The cardinality slot is read-only source syntax, and writes such as items[0] = "3" are rejected with OUT_OF_RANGE. Use ordinary element assignment or the standard array helpers instead:

items = .string[]
call arrayappend items, "alpha"
call arrayappend items, "beta"
say items[0]

To clear an existing array object, use the standard in-place helpers:

call arraydrop items
call objectarraydrop objects

This is especially important for class attributes. Reassigning an attribute-like name to .string[] or .object[] inside a method is not the same documented operation as clearing the existing array object, and can run into current Level B scoping edge cases. The collection classes use arraydrop and objectarraydrop in their clear/free methods.

When a test needs delete/insert semantics, prefer library helpers such as arraydelete, arrayinsert, and arrayappend over assembler unless the test is specifically about RXAS.

The array* helper family is currently for .string[] arrays. Do not use it for .int[], .decimal[], or other typed numeric arrays; use direct indexing and explicit loops until the project adds a typed/generic helper surface.

Use the objectarray* helper family for mutating .object[] arrays:

Store concrete class instances with an explicit as .object upcast.

References:

Class and interface factories omit return types

Use *: factory or name: factory; do not write = .type on a factory. The factory result is inferred from the owning contract: an interface factory returns that interface and a class factory returns that concrete class. In a class factory, bare return returns the constructed object, so there is no source-level this value to mention.

When creating an object value, include the factory call brackets. .SomeClass() returns an initialized instance. Bare .SomeClass is a typed default object value and is intentionally uninitialized; it is useful for type defaults and can be probed with initialized(value), but calling methods on it raises OBJECT_NOT_INITIALIZED.

vehicle: interface
  *: factory
  arg name = .string
  describe: method = .string

car: class implements .vehicle
  _name = .string

  *: factory
    arg name = .string
    _name = name
    return

  describe: method = .string
    return _name

Reference:

When consuming a class from an imported binary namespace, prefer the explicit qualified form if the object will be used for method calls in a tool or runner:

runner = .rexxscript..rexxscriptevaluator()

During the RexxScript runner work, the unqualified imported factory form was accepted but method typing on the result was not preserved in that context. If you see #METHOD_NOT_FOUND after an imported factory call that should be valid, try the qualified form and record the source-import/binary-import mismatch as a Level B follow-up rather than hiding it in application logic.

Call out suspected Level B gaps

Level B is new enough that RexxScript and runtime-library work may uncover compiler or runtime gaps. Prefer documenting and surfacing these for resolution decisions over burying workarounds without explanation.

Recent observations from the RexxScript evaluator refactor:

  next_root = rebalance(root)
  root = next_root

When a helper naturally needs to update a caller-owned slot, prefer an explicit reference output location such as reference root or reference left[parent], then dereference it inside the helper and assign the linked local.

Keep hot Level B loops honest with RXAS

Inlining correctness and inlining performance are separate questions. For performance-sensitive classlib code, inspect the generated optimized .rxas and, when relevant, rxdas output from the assembled .rxbin before assuming a private helper is free.

The StringTreeMap AVL rewrite found that a private _findNode() helper was correctly inlined into get() and containsKey(), but still left block-expression scaffolding around the hot lookup loop. Rewriting those two methods as direct loops made Release lookup time for 2,500 entries drop from about 200 ms to about 2.3 ms in the local benchmark. Use helpers for clarity unless the code is a measured hot path; for hot paths, prefer direct loops when the RXAS shows avoidable call or block-expression structure.

For array-backed data structures, expect ordinary indexed attribute access to lower through linkattr1, minattrs, typed copy, and unlink instructions. That is the current cost model. It is fine to use small inline assembler assists such as SETATTRS array,0 for array clearing until the language has a typed array-clear surface, but record broader array-access needs as language/runtime backlog items instead of baking large hand-written RXAS into classlib code.

address crexx is the standard command-environment pattern

When Level B code needs command-environment behavior, copy the repo pattern instead of inventing a new API shape:

out = .string[]
err = .string[]
address crexx "echo #42" output out error err
if rc <> 0 then say "command failed"

address crexx uses the CREXX command environment. It is the default ADDRESS environment, owns cREXX-specific, OS-independent commands such as echo, pwd, cd, pushd, popd, ls, mkdir, copy, cat, platform, pid, resolve, tcp, and batch, and reports normal command failure through rc. It is not a shell: ;, &&, ||, pipes, shell redirects, and shell expansion are usage errors. Use repeated ADDRESS statements or address crexx "batch" with input lines for multiple commands.

For CREXX commands, host-variable anchors can pass data without shell parsing: :name exposes a scalar as one command argument, while :name[] and :name. expose a stem/array as zero or more command arguments. Prefer [] in new code because it is visually unambiguous. For direct executable dispatch, build a .string[] argument vector and use address crexx "run :argv[]"; the run command launches from that argv vector rather than flattening and reparsing a command string.

Use address system, address command, or address cmd only when the caller intentionally needs the platform command processor. On POSIX this is standard sh -c resolved from the system standard utility path, not the user’s SHELL environment variable; on Windows it is %COMSPEC% /D /S /C with a cmd.exe fallback. Shell built-ins, pipes, redirects, and command chaining belong on this route.

Use address shell only when the shell executable is deliberately configured through CREXX_ADDRESS_SHELL and optional CREXX_ADDRESS_SHELL_ARGS.

Use address path only when the caller needs direct executable dispatch. That provider uses the platform process API without shell semantics. On POSIX it argv-parses the command and resolves the executable through process PATH; on Windows it uses direct CreateProcessW command-line dispatch:

address path "rxas -h" output out error err

References:

Signal handlers live on simple do groups

Block-scoped signal handling is written with on signal clauses on a simple do ... end group:

do
  risky_work()
on signal conversion_error as problem
  say problem.source()
on signal
  call cleanup()
end

Do not attach on signal directly to counted, conditional, forever, or expression-form do loops. To protect part of a loop, nest a simple signal-handling do ... end group inside the loop body.

If as name is omitted, the handler has no local signal object. That is fine for fixed cleanup/logging handlers.

References:

Headerless scripts are a driver convenience, not a style rule

The crexx driver can compile simple headerless top-level scripts with synthetic defaults (--level levelb --import rxfnsb). That is useful for end users, but repo code should usually stay explicit with options levelb and imports unless there is a strong reason not to.

Reference:

Command-line arguments are a first-class Level B feature

Level B programs receive command-line arguments through arg, and when the compiler synthesizes the file-level main() wrapper, the VM argv payload is available there as a .string[].

Program-side guidance:

Launcher-side guidance:

References:

Wayfinding: Best Example Files By Task

Writing a tiny BIF or helper

Writing a top-level command/script

Writing a stateful tool

Writing compiler-exit or structured typed code

Checking argument signature syntax

Checking namespace/global behavior

Checking argument handling

Agent Guidance

When writing Level B code:

  1. Read one doc source and two nearby working .crexx examples before editing.
  2. Match the local style of the directory you are in.
  3. Prefer explicit Level B headers in committed repo code.
  4. Prefer canonical namespace..symbol qualification.
  5. Do not “simplify” typed signatures or namespace structure just because a generic REXX example elsewhere looks looser.

If a proposed snippet does not resemble existing repo code in lib/, debugger/, compiler/exits/, bin/, or tests/, stop and verify it before committing it.