REXX Language implementation
This tutorial is for experienced Rexx programmers who want to write practical
cRexx Level B programs in the Release 1 beta 3 documentation line. It assumes
you know Rexx ideas such as say, parse, do, arg, and built-in
functions, but it does not assume you have used cRexx’s typed compiler,
modules, classes, or interfaces before.
Level B is Rexx-family code, but it is not Classic Rexx compatibility mode. It is the implemented typed cRexx language used by the toolchain, libraries, tests, and examples in this repository. When this tutorial says “Level B”, it means what the current compiler accepts now, not a planned Level C or Level G feature.
For exact syntax details, keep these reference pages close:
For more implemented examples in a source download, start with these paths:
examples/hello.crexx,examples/ooBank.crexx,lib/rxfnsb/rexx/stem.crexx,compiler/tests/rexx_src/interface_showcase_same_module.crexx, andcompiler/tests/rexx_src/reference_source_iterator.crexx.The first pleasant surprise is that much of the surface still reads like Rexx:
say prints a value.arg receives command-line or procedure arguments.do, if, select, leave, iterate, and return shape control flow.parse, address, signal, and trace are present in the current beta
surface.rxfnsb library provides many familiar built-in functions.The biggest difference is that Level B gives the compiler real information: values have types, modules have namespaces, procedures declare arguments and return types, and object contracts are checked.
That trade is worth leaning into. Do not write vague Classic Rexx and hope the compiler guesses your intent. Write a small amount of explicit Level B so the compiler and VM can help you.
The usual source extension for new cRexx code is .crexx. For reusable code,
start with options levelb and import the Level B standard library when you
use it.
```rexx options levelb import rxfnsb
say “Hello Level B” say “length=” || length(“Rexx”)
From the repository root:
```bash
crexx hello.crexx
Expected output:
The crexx driver wraps the normal compile, assemble, and run path. The
underlying stages are still visible:
rxc compiles source to .rxas.rxas assembles .rxas to .rxbin.rxvm or rxvme runs .rxbin.rxlink can combine modules into a linked image.The commands in this tutorial assume cRexx has been installed and crexx,
rxc, rxas, and rxvm are on your PATH. In a source download before
installation, use the matching binaries from your build directory instead.
For day-to-day scripts, use crexx. For multi-module class/interface examples
where provider modules must be present at runtime, compile the modules and run
or link the resulting .rxbin files explicitly. The mini-project below shows
that shape.
Use this header shape for committed Level B code. This is a header fragment, not a complete program:
```rexx options levelb namespace myapp expose public_symbol import rxfnsb
Keep `options`, `namespace`, and `import` in the leading header block. The
compiler pre-scans that header before full parsing, and the rest of the repo
uses this layout consistently.
The pieces mean:
- `options levelb`: compile this file as Level B.
- `namespace myapp expose public_symbol`: publish selected symbols from this
module.
- `import rxfnsb`: make another namespace visible to this source file.
Headerless top-level scripts run through the `crexx` driver get practical
defaults, including Level B and `rxfnsb`, but tutorial and library code should
be explicit.
Command-line arguments arrive through `arg`:
```rexx <!--levelbtut2.crexx-->
options levelb
arg words = .string[]
if words[0] = 0 then do
say "no arguments"
return
end
say "count=" || words[0]
do i = 1 to words[0]
say i || ":" || words[i]
end
Run with arguments by putting -args last:
```bash crexx args.crexx -args alpha beta
Expected output:
<!--splice--crexx levelbtut2.crexx -args alpha beta-->
<!-- ```bash -->
<!-- count=2 -->
<!-- 1:alpha -->
<!-- 2:beta -->
<!-- ``` -->
## Types You Will Use Immediately
Level B values have types. The most common built-in source spellings are:
| Type | Use |
| --- | --- |
| `.string` | UTF-8 text |
| `.int` | integer values |
| `.float` | binary floating-point values |
| `.decimal` | decimal numeric values |
| `.boolean` | truth values |
| `.binary` | arbitrary bytes |
| `.object` | object-shaped values |
| `.void` | no return value |
Write `.boolean` in current Level B source. Some Rexx or C habits may suggest
`.bool`, but `.boolean` is the documented Level B spelling.
A local variable can be inferred from its first assignment, or you can declare
the type first and assign the value next. The latter is useful when the value
will be filled later or when you want a specific target type.
```rexx <!--levelbtut3.crexx-->
options levelb
import rxfnsb
title = .string
count = .int
price = .float
ready = .boolean
payload = "4f4b"x as .binary
words = .string[]
people = .stem()
title = "Level B"
count = 2
price = 1.5
ready = 1
call arrayappend words, "alpha"
call arrayappend words, "beta"
words.3 = "gamma"
people.ada = "analytical"
people["grace.hopper"] = "compiler"
tail = "ada"
say title || ":" || count
say typeof(price)
say typeof(ready)
say binlength(payload)
say words[0] || ":" || words.1 || "," || words[2] || "," || words.3
say people.tail
say people["grace.hopper"]
Expected output:
Practical notes:
.string[], .int[], .object[], and so on..int[0 to 10] and
.int[-2 to *].array[0] or array.0 reads the current high water mark.words[1]) or Rexx
stem-style dot notation (words.1).0; use ordinary element assignment or helpers such
as arrayappend, arrayinsert, and arraydelete..stem class is a string-to-string keyed container for classic Rexx
compound-variable style data. It supports dotted tails such as people.ada
and bracket keys such as people["grace.hopper"]..string is valid UTF-8 text in normal builds. Use .binary for bytes.A Level B procedure can declare its return type after =, and it binds call
arguments with arg.
```rexx options levelb
main: procedure total = sum(2, 3, 5) x = 10 call bump(x)
say “total=” || total say “x=” || x return
sum: procedure = .int arg first = .int, … = .int total = first do i = 1 to arg[] total = total + arg[i] end return total
bump: procedure = .void arg expose value = .int value = value + 1 return
Expected output:
<!--splice--crexx levelbtut4.crexx-->
<!-- ```bash -->
<!-- total=10 -->
<!-- x=11 -->
<!-- ``` -->
The important habits are:
- `arg name = .type` is by value. Changes in the procedure do not update the
caller's variable.
- `arg expose name = .type` is by reference. Use it only when the procedure is
meant to update caller-owned state.
- A final `... = .type` accepts a variable-length tail.
- `arg[]` is the number of values in that tail, and `arg[i]` reads tail values.
`procedure expose` is different from `arg expose`: it binds module-global
storage into a procedure. Prefer explicit arguments and return values until you
really need shared module state.
## Expressions, Assignment, Casts, and Comparisons
Level B keeps Rexx operators, but type checking happens before bytecode is
emitted.
Use these idioms:
- Assign a variable once with the type you mean; later assignments must be
compatible.
- Use `expr as .type` when you need a checked conversion or object cast.
- Use `expr is .type` for object/interface tests.
- Use `typeof(expr)` for diagnostics and runtime type introspection.
- Use `=` for Rexx-style equality and `==` when you mean exact string
comparison after string promotion.
For binary data, be explicit. This is a fragment:
```rexx <!--payload.crexx-->
payload = "4f4b"x as .binary
For objects, cast at the boundary. This is a fragment:
```rexx generic = selected as .object restored = generic as .asset
If a cast cannot succeed, Level B raises a conversion signal instead of silently
changing the meaning of the value.
## Control Flow
The usual Rexx control-flow tools are present. `select` has both Classic Rexx
condition style and a switch-like expression style. `leave` and `iterate` work
on loops. Expression-form `do ... end` can return a value with `leave with`.
```rexx <!--levelbtut5.crexx-->
options levelb
arg words = .string[]
if words[0] = 0 then words[1] = "red"
kept = 0
do i = 1 to words[0]
select
when words[i] = "skip" then iterate
when words[i] = "stop" then leave
otherwise say "word=" || words[i]
end
kept = kept + 1
end
summary = do
if kept > 1 then leave with "many"
leave with "few"
end
say "summary=" || summary
Run:
```bash crexx control_flow.crexx -args alpha skip beta stop gamma
Expected output:
<!--splice--crexx levelbtut5.crexx -args alpha skip beta stop gamma-->
<!-- ```bash -->
<!-- word=alpha -->
<!-- word=beta -->
<!-- summary=many -->
<!-- ``` -->
## Rexx-Flavoured Features
Level B keeps several Rexx features that make the language feel direct:
- `say` prints strings.
- `parse` is implemented through the certified `PARSE` exit.
- `address` sends commands to an external environment and can capture output.
- `signal` raises and handles condition objects.
- `trace` enables VM-backed tracing for debugging.
This example keeps the output small:
```rexx <!--levelbtut6.crexx-->
options levelb
import rxfnsb
main: procedure
parse value "Ada Lovelace,1815" with first last "," year
say last || ":" || year
out = .string[]
err = .string[]
address crexx "echo 42" output out error err
say "address=" || out[1]
handled = ""
do
signal other "from block"
on signal other as problem
handled = problem.name() || ":" || problem.message()
end
say handled
return
Expected output:
Trace is useful but intentionally noisy. These are reference-only forms, not a runnable tutorial example:
trace results
trace asm
trace llm to file "trace.jsonl"
trace unsuppress namespace rxfnsb
trace off
Do not add TRACE, PARSE, or ADDRESS directly to lib/rxfnsb/rexx/
library sources while debugging the library build. Those files are built with
compiler exits disabled. Write a small caller program instead.
Each source file is a module. Public module identity comes from namespace,
not from the filename alone. A module exposes selected symbols; another module
imports the namespace and calls those symbols.
Library module:
```rexx options levelb namespace greetings expose salutation count_items
salutation: procedure = .string arg name = .string return “Hello, “ || name
count_items: procedure = .int arg items = .string[] return items[0]
Caller:
```rexx <!--modulesmain.crexx-->
options levelb
import greetings
names = .string[]
names[1] = "Ada"
names[2] = "Grace"
say greetings..salutation(names[1])
say "names=" || greetings..count_items(names)
Run the caller with the directory containing greetings.crexx on the source
import path:
```bash crexx modules_main.crexx -s/path/to/examples
Expected output:
<!--splice--crexx modulesmain.crexx-->
<!-- ```bash -->
<!-- Hello, Ada -->
<!-- names=2 -->
<!-- ``` -->
Use `namespace..symbol` for qualified calls. The older
`namespace::symbol` spelling is accepted for compatibility, but new examples
should prefer the double-dot form.
## Classes
Classes hold attributes and methods. A class factory is written as `*: factory`
or `name: factory`; do not write a return type on a factory. In a class
factory, bare `return` returns the object being constructed. This fragment is
from the complete interface example in the next section:
```rexx <!--tutclasses1-->
fileasset: class implements .asset
_name = .string
*: factory
arg name = .string
_name = name
return
kind: method = .string
return "file"
name: method = .string
return _name
Ordinary application classes should let the compiler allocate attribute
storage. The with register.N form exists for low-level VM/native interop, not
for day-to-day business objects.
Interfaces define contracts. Classes implement them. Interface methods can be abstract, or they can provide a default/final body. In current Level B, an interface method with a body is final: a class can rely on it, but cannot override it.
Here is a complete same-file interface and provider example:
```rexx options levelb namespace tutorial_objects
main: procedure selected = .asset(“log.txt”) fallback = .asset(“memo”)
say selected.describe() say fallback.describe()
if selected is .fileasset then say “file selected”
generic = .object generic = selected as .object restored = generic as .asset say typeof(restored) return
asset: interface *: factory arg name = .string
describe: method = .string return kind() || “:” || name()
kind: method = .string name: method = .string
fileasset: class implements .asset _name = .string
*: match arg name = .string if name = “log.txt” then return 100 return 0
*: factory arg name = .string _name = name return
kind: method = .string return “file”
name: method = .string return _name
cacheasset: class implements .asset _name = .string
*: factory arg name = .string _name = name return
kind: method = .string return “cache”
name: method = .string return _name
Expected output:
```text <!--outtex.txt-->
file:log.txt
cache:memo
file selected
.tutorial_objects..fileasset
The asset interface owns the public factory. fileasset and cacheasset are
providers. The *: match method is a class-side selector. Positive scores
accept the provider, zero or negative scores reject it, and the highest score
wins.
Use object values through factories, methods, interfaces, type tests, and checked casts:
.asset("log.txt") calls an interface factory..fileasset("log.txt") calls a concrete class factory.value.method() invokes a method.value is .asset tests whether the concrete object implements an interface.value as .asset casts after a runtime check.typeof(value) reports the concrete runtime type.Level B does not implicitly call a toString() method for arbitrary objects.
If you want text, expose a method such as format, describe, or name, then
call it explicitly.
Level B also does not currently implement interface inheritance, interface attributes, overloads, singleton declarations, or destructor/finalizer syntax.
Current Level B collection patterns are explicit:
.string[], .int[], and .object[].array[0] for the current count.items[1]) or stem-style dot notation (items.1) for
array elements.arrayappend, arrayinsert, arraydelete, and related rxfnsb helpers
for mutating array shape.objectarrayappend, objectarrayinsert, and related helpers for
.object[] when you need object-array mutation helpers..object[] with an explicit as .object upcast,
then cast back to the expected interface or class at the read boundary..stem when the natural model is a Rexx compound variable or keyed
string map rather than an ordered typed array.The mini-project below uses this pattern. This is a fragment:
```rexx items = .object[] items[1] = .ledger..entry(“coffee”, -3) as .object
item = items[1] as .ledger..entry say item.format()
For iterator-like classes, current Level B uses explicit references when the
iterator should see live container state. Use snapshots when the iterator should
see a stable copy.
## References
References are explicit weak aliases to storage. They do not keep a target
alive. If the target storage goes away, `refvalid(ref)` returns false and
using the reference raises `REFERENCE_INVALID`.
Use the four source forms deliberately:
- `reference target`: create a weak reference to aliasable storage.
- `local = dereference ref`: link a current-scope local to the target.
- `snapshot ref`: make a deep copy of the current target.
- `<refvalid>(ref)`: test whether the target is invalid.
This example contrasts a live reference with a snapshot:
```rexx <!--tutref1.crexx-->
options levelb
import rxfnsb
namespace tutorial_references
main: procedure
list = .Bag()
call list.add("red")
call list.add("blue")
live = list.liveCounter()
snap = list.snapshotCounter()
call list.add("green")
say "live=" || live.count()
say "snapshot=" || snap.count()
return
Bag: class
values = .string[]
item_count = .int
*: factory
initial = .string[]
values = initial
item_count = 0
return
add: method = .void
arg value = .string
call arrayappend values, value
item_count = item_count + 1
return
size: method = .int
return item_count
liveCounter: method = .LiveCounter
return .LiveCounter(reference self)
snapshotCounter: method = .SnapshotCounter
return .SnapshotCounter(reference self)
LiveCounter: class
bag_ref = reference .Bag
*: factory
arg source = reference .Bag
bag_ref = source
return
count: method = .int
if <refvalid>(bag_ref) = 0 then return -1
bag = dereference bag_ref
return bag.size()
SnapshotCounter: class
bag_copy = .Bag
*: factory
arg source = reference .Bag
bag_copy = snapshot source
return
count: method = .int
return bag_copy.size()
Expected output:
Keep reference boundaries visible. Level B does not let you write convenience
forms such as list_ref.add(...) or items_ref[i] directly through the
reference.
This mini-project uses two source files: a reusable module with an interface and class, and a small application that imports it.
ledger.crexx:
```rexx options levelb namespace ledger expose entry ledger_total
entry: interface *: factory arg label = .string, amount = .int
label: method = .string amount: method = .int
format: method = .string return label() || “=” || amount()
ledgerentry: class implements .entry _label = .string _amount = .int
*: factory arg label = .string, amount = .int _label = label _amount = amount return
label: method = .string return _label
amount: method = .int return _amount
ledger_total: procedure = .int arg entries = .object[] total = 0 do i = 1 to entries[0] item = entries[i] as .entry total = total + item.amount() end return total
`ledger_app.crexx`:
```rexx <!--ledgerapp.crexx-->
options levelb
import ledger
items = .object[]
items[1] = .ledger..entry("coffee", -3) as .object
items[2] = .ledger..entry("book", -12) as .object
items[3] = .ledger..entry("gift", 20) as .object
do i = 1 to items[0]
item = items[i] as .ledger..entry
say item.format()
end
say "total=" || ledger..ledger_total(items)
Compile both modules, then run with the standard library, provider module, and application module loaded:
CREXX_BIN=$(dirname "$(command -v crexx)")
rxc -i "$CREXX_BIN" -o main ledger_app.crexx
rxas -o main.rxbin main
rxc -i "$CREXX_BIN" -o ledger ledger.crexx
rxas -o ledger.rxbin ledger
rxvm "$CREXX_BIN/library.rxbin" ledger.rxbin main.rxbin
Expected output:
coffee=-3
book=-12
gift=20
total=5
The explicit rxvm command matters here because interface factory providers
are discovered from the modules present in the runtime image. For deployable
programs, rxlink is the usual way to combine those modules into one image.
.string,
.int, .float, .boolean, .binary, arrays, or object contracts.import rxfnsb in reusable source, unless you are deliberately writing a
headerless crexx driver script..stem for classic compound-variable style keyed strings. Treat array[0]
as a count, not as a writable stem slot.namespace ... expose, procedure expose,
or arg expose only for intentional sharing..string for valid UTF-8 text and .binary for arbitrary bytes. Convert
explicitly.Level B is the main implemented Release 1 beta language, but this is still a beta line. Current boundaries that matter to tutorial code:
rxfnsg, but it is not the
baseline user language for this release line.