CREXX

REXX Language implementation

View the Project on GitHub adesutherland/CREXX

Level B Tutorial For Rexx Programmers

This tutorial is for experienced Rexx programmers who want to write practical cRexx Level B programs in the Release 1 beta 2 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, and compiler/tests/rexx_src/reference_source_iterator.crexx.

1. Why Level B Still Feels Like Rexx

The first pleasant surprise is that much of the surface still reads like Rexx:

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.

2. First Program and Workflow

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.

options levelb
import rxfnsb

say "Hello Level B"
say "length=" || length("Rexx")

From the repository root:

crexx hello.crexx

Expected output:

Hello Level B
length=4

The crexx driver wraps the normal compile, assemble, and run path. The underlying stages are still visible:

  1. rxc compiles source to .rxas.
  2. rxas assembles .rxas to .rxbin.
  3. rxvm or rxvme runs .rxbin.
  4. 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.

3. Source File Conventions

Use this header shape for committed Level B code. This is a header fragment, not a complete program:

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:

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:

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:

crexx args.crexx -args alpha beta

Expected output:

count=2
1:alpha
2:beta

4. 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.

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:

Level B:2
.float
.boolean
2
3:alpha,beta,gamma
analytical
compiler

Practical notes:

5. Procedures, Arguments, Returns, and Varargs

A Level B procedure can declare its return type after =, and it binds call arguments with arg.

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:

total=10
x=11

The important habits are:

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.

6. Expressions, Assignment, Casts, and Comparisons

Level B keeps Rexx operators, but type checking happens before bytecode is emitted.

Use these idioms:

For binary data, be explicit. This is a fragment:

payload = "4f4b"x as .binary

For objects, cast at the boundary. This is a fragment:

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.

7. 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.

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:

crexx control_flow.crexx -args alpha skip beta stop gamma

Expected output:

word=alpha
word=beta
summary=many

8. Rexx-Flavoured Features

Level B keeps several Rexx features that make the language feel direct:

This example keeps the output small:

options levelb
import rxfnsb

main: procedure
  parse value "Ada Lovelace,1815" with first last "," year
  say last || ":" || year

  out = .string[]
  err = .string[]
  address command "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:

Lovelace:1815
address=#42
OTHER:from block

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.

9. Modules and Libraries

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:

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:

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:

crexx modules_main.crexx -s/path/to/examples

Expected output:

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.

10. 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:

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.

11. Interfaces

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:

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:

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.

12. Object Use

Use object values through factories, methods, interfaces, type tests, and checked casts:

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.

13. Collections and Iteration

Current Level B collection patterns are explicit:

The mini-project below uses this pattern. This is a fragment:

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.

14. 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:

This example contrasts a live reference with a snapshot:

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) 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:

live=3
snapshot=2

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.

15. Mini-Project: A Typed Ledger Module

This mini-project uses two source files: a reusable module with an interface and class, and a small application that imports it.

ledger.crexx:

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:

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.

16. Migration Guide For Classic Rexx Habits

Classic Rexx habit:
Rely on untyped variables.
Level B habit:
Let obvious literals infer types, or declare variables with .string, .int, .float, .boolean, .binary, arrays, or object contracts.
Classic Rexx habit:
Assume built-in functions are always visible.
Level B habit:
import rxfnsb in reusable source, unless you are deliberately writing a headerless crexx driver script.
Classic Rexx habit:
Use stems as flexible bags of values.
Level B habit:
Use typed arrays and array helper functions for ordered typed data. Use .stem for classic compound-variable style keyed strings. Treat array[0] as a count, not as a writable stem slot.
Classic Rexx habit:
Share state freely through globals.
Level B habit:
Prefer arguments and returns. Use namespace ... expose, procedure expose, or arg expose only for intentional sharing.
Classic Rexx habit:
Treat text and bytes as the same thing.
Level B habit:
Use .string for valid UTF-8 text and .binary for arbitrary bytes. Convert explicitly.
Classic Rexx habit:
Expect dynamic object behavior.
Level B habit:
Define an interface, implement it with classes, and use checked casts and type tests at boundaries.

17. Known Beta 2 Boundaries

Level B is the main implemented Release 1 beta language, but this is still a beta line. Current boundaries that matter to tutorial code:

Tested Example Appendix

The examples below were tested from the repository root with installed tools on PATH. The commands use $WORK for the directory containing the tutorial example files.

Example Command used Expected output Known limitation
First program crexx $WORK/hello.crexx Hello Level B
length=4
None.
Command-line args crexx $WORK/args.crexx -args alpha beta count=2
1:alpha
2:beta
-args must be the final driver option before user arguments.
Types, arrays, and stems crexx $WORK/types_arrays.crexx Level B:2
.float
.boolean
2
3:alpha,beta,gamma
analytical
compiler
Uses explicit declarations plus assignment, not constructor-call examples.
Procedures and varargs crexx $WORK/procedures.crexx total=10
x=11
None.
Control flow crexx $WORK/control_flow.crexx -args alpha skip beta stop gamma word=alpha
word=beta
summary=many
None.
Parse, address, signal crexx $WORK/rexx_features.crexx Lovelace:1815
address=#42
OTHER:from block
Shell output assumes echo is available.
Namespace import crexx $WORK/modules_main.crexx -s$WORK Hello, Ada
names=2
Demonstrates source import. Interface-provider programs need runtime loading too.
Classes and interfaces crexx $WORK/objects.crexx file:log.txt
cache:memo
file selected
.tutorial_objects..fileasset
Same-file providers keep the run command simple.
References crexx $WORK/references.crexx live=3
snapshot=2
None.
Ledger mini-project See the command block below. coffee=-3
book=-12
gift=20
total=5
All provider modules must be loaded or linked at runtime.

Ledger mini-project command sequence:

CREXX_BIN=$(dirname "$(command -v crexx)")
rxc -i "$CREXX_BIN" -o "$WORK/main" "$WORK/ledger_app.crexx"
rxas -o "$WORK/main.rxbin" "$WORK/main"
rxc -i "$CREXX_BIN" -o "$WORK/ledger" "$WORK/ledger.crexx"
rxas -o "$WORK/ledger.rxbin" "$WORK/ledger"
rxvm "$CREXX_BIN/library.rxbin" "$WORK/ledger.rxbin" "$WORK/main.rxbin"

The TRACE forms in section 8 are intentionally marked reference-only because trace output is diagnostic and can be much larger than the tutorial needs.