REXX Language implementation
Level B is statically typed. Every expression has a type known to the compiler, and assignments, arguments, returns, method calls, and factory calls are checked against those types.
The built-in Level B value types are:
| Type | Purpose |
|---|---|
.void |
No usable value. Used for procedures that do not return a value. |
.boolean |
Boolean truth value. |
.int |
Integer value. |
.float |
Binary floating-point value unless the source uses decimal float options. |
.decimal |
Decimal numeric value. |
.string |
Character string value. |
.binary |
Binary byte sequence. |
.object |
Object value. Interfaces and classes are object-shaped contracts. |
The canonical integer spelling in source is .int. In Release 1 it is a signed
64-bit value on every supported desktop architecture, with the inclusive range
-9223372036854775808 through 9223372036854775807. It is not sized from the
host C long, pointer, or native word width.
Type names can be used as constructors:
count = .int(0)
name = .string("Ada")
ok = .boolean(1)
payload = .binary()
Class and interface names also use the dotted form. A factory call creates an object through the selected class or interface provider:
item = .cacheentry("abc")
asset = .asset("log.txt")
Without the call brackets, a class name denotes the typed default value for that
class. For object classes this is a typed but uninitialized object value; it can
be tested or cast for object/class/interface compatibility, but method calls or
attribute access require an initialized instance and raise
OBJECT_NOT_INITIALIZED otherwise. Use the factory form when an initialized
object is wanted:
pending = .cacheentry /* typed, not initialized */
ready = .cacheentry("abc") /* initialized by the factory */
say initialized(pending) /* 0 */
say initialized(ready) /* 1 */
Namespace-qualified contracts use a double dot:
client = .net..httpclient("example.com", 443, 1)
The left side of namespace..symbol must name an imported namespace.
namespace::symbol remains accepted as a compatibility alias.
Arrays are declared from a base type:
words = .string[]
numbers = .int[10]
grid = .int[10, 10]
window = .int[0 to 10]
grow = .int[-2 to *]
An array value carries its element type and dimensions. Procedure signatures can accept arrays in the same way:
main: procedure = .int
arg args = .string[]
Array elements can be accessed with bracket notation or Rexx-style dotted notation:
items[1] = "alpha"
items.2 = "beta"
Simple growable arrays are often used with one-based element indexes, but Level
B arrays are not inherently limited to one-based indexing; explicit bounds such
as .int[0 to 10] and .int[-2 to *] are part of the current source surface.
For classic Rexx compound-variable style keyed string data, use the Level B
.stem class from rxfnsb.
Reference values are explicit weak aliases to storage. A reference does not keep
its target alive; if the target storage is destroyed, later use raises
REFERENCE_INVALID. Use reference as a type modifier anywhere a Level B type
is accepted:
count_ref = reference .int
items_ref = reference .string[]
read_count: procedure = .int
arg r = reference .int
Use reference target to create a reference to aliasable storage,
local = dereference ref when a local should become a scoped live link to the
referenced target, and snapshot ref when an explicit deep copy is required:
count = 1
count_ref = reference count
linked = dereference count_ref
copy = snapshot count_ref
dereference is only valid as the right side of an assignment to a local
variable in the current procedure or block scope. Assigning a dereference into
an object/class attribute, array element, global, exposed argument, or arbitrary
expression is a compile-time error. The compiler emits unlink when the
local’s scope exits, and the VM also resets linked locals when a frame exits.
Reference values are not assignment-compatible with their target type. Passing a
.T where reference .T is expected is an error, and passing reference .T
where .T is expected is also an error; spell reference target,
local = dereference ref, or snapshot ref at the boundary.
Reference values do not define value equality or ordering. Applying an ordinary
comparison operator such as = or == to a reference is a compile-time error
rather than an implicit comparison of target values or storage identity. The
explicit left <refsame> right operator accepts compatible reference types and
tests whether both retain the same storage-identity cell without dereferencing
them. Copied references therefore remain <refsame> after their common target
expires; cleared or empty references compare false. Use <refvalid>(ref) to
test live-target validity, and explicitly dereference or snapshot when the
target value itself is to be compared.
Nested reference containers, reference casts, reference type tests, and
implicit member/index access through a reference are not part of the current
Level B source surface. These are reserved for possible Level G convenience
features. Level B code should keep reference boundaries explicit with
reference, dereference, snapshot, <refvalid>, and <refsame>.
Level B supports integer, float, and decimal arithmetic. The file-level
options instruction selects the parser’s arithmetic standard, and a
procedure-level numeric instruction can set numeric context such as digits,
form, fuzz, case, and standard.
Integer literals and conversions outside the signed 64-bit range are rejected.
Runtime integer add, subtract, multiply, power, increment, decrement, negation,
and the INT64_MIN / -1 and INT64_MIN % -1 edges raise
OVERFLOW_UNDERFLOW; integer division or modulo by zero raises
DIVISION_BY_ZERO.
The compiler performs type validation before bytecode emission. Numeric conversions are explicit where precision or representation could otherwise be surprising:
i = .int(42)
f = .float(i)
d = .decimal("42.50")
An ordinary dotted literal remains binary .float when no surrounding type
requires decimal. When a decimal assignment, argument, return, cast,
constructor, or decimal expression establishes the expected type, the compiler
parses the literal’s original source spelling directly as .decimal; it does
not round through binary64 first. For example, d = 0.1 is exact when d is
declared .decimal. An explicit .float(...) boundary and options
floats_binary retain binary treatment. The d suffix is also an explicit
decimal spelling, such as 0.1d.
The checked cast form can also be used for scalar conversions:
f = 1 as .float
i = "42" as .int
d = "42.50" as .decimal
s = 42 as .string
ok = "1" as .boolean
Scalar casts use the same conversion rules as the corresponding constructor or
promotion opcode. A cast is still type checked; for example, .binary values
only cast back to .string when the cast is explicit and the bytes are valid
UTF-8.
.string values are character data. .binary values are byte data. Keep the
two distinct when working with sockets, files, encodings, or native payloads:
string operations are text operations, while binary operations preserve bytes.
In UTF builds, .string source values are valid UTF-8 text. Converting a
string to .binary stores the exact UTF-8 bytes currently held by the string;
the conversion does not normalize, transcode, or reinterpret the text:
payload = "alpha" as .binary
Converting .binary to .string validates the byte sequence as UTF-8:
payload = "ceb1"x as .binary
text = payload as .string /* "α" */
An invalid binary-to-string conversion raises UNICODE_ERROR at runtime. If the
invalid bytes are visible as a constant literal in the cast, the compiler rejects
the program with CANNOT_CAST_BINARY.
Invalid UTF-8 byte sequences are only valid in an explicit binary context:
payload = .binary
payload = 'ffff'x
other = 'ffff'x as .binary
A first untyped assignment such as payload = 'ffff'x is treated as a text
assignment and is rejected when the decoded bytes are not valid UTF-8. That rule
keeps accidental invalid text out of string operations; use .binary when the
program is handling bytes.
Binary concatenation is byte concatenation. If either operand of || is
.binary, the expression result is .binary; string operands in that binary
expression are converted to their exact UTF-8 bytes. Blank concatenation remains
a text operation and should not be used for binary payload assembly.
prefix = "ff"x as .binary
packet = prefix || "OK" /* bytes ff 4f 4b */
Runtime-owned .binary storage is aligned for the host’s native .int and
.float representations. Level B can use that guarantee through zero-based
<packed..int>(item) and <packed..float>(item) reads and writes. These are
explicit host-native views over bytes, not new value types and not portable
encodings. See Binary Memory.
Release 1 Level G provides explicit .packedint and .packedfloat owner
classes in rxfnsg; those classes wrap this same representation and do not
change ordinary arrays. Automatically packed ordinary .int[] and .float[]
arrays remain a post-release Level G roadmap item.
Level B string length, indexing, slicing, search, and reversal use Unicode codepoints. They do not use UTF-8 bytes and do not imply grapheme boundaries. Normalization and full case folding are explicit future Level G services; no string conversion or ordinary Level B operation normalizes implicitly.
Level C direct BIFs retain the .string/.binary distinction but apply the
character profile held by their call context. BYTE treats exact bytes as
Classic character units; UTF8 requires valid UTF-8 for text operations and
uses codepoint units. A value’s binary/text cache flags do not select that
profile. See Unicode.
The rxfnsb library provides byte-oriented helpers for common binary work:
binlength, binbyte, binsetbyte, binsubstr, binconcat, binoverlay,
bininsert, bindelstr, binpos, bincompare, bin2x, and x2bin.
For packed binary layouts, use the binary-memory intrinsics and helpers in
Binary Memory; those use zero-based byte offsets and can
operate directly on binary constants.
The same boundary applies outside source literals. Native RXVML string setters,
CREXXSAA ADDRESS variable setters, RXPA native return / argument trees,
command-line arguments passed through RXVML, ADDRESS callback text, text file
reads, socket text reads, and explicit binary-to-string casts validate UTF-8 in
normal Level B builds. Invalid bytes should be read or carried as .binary
first, then decoded to .string only when the program has a valid encoding.
Classes and interfaces are object contracts. A concrete class instance can be assigned to an interface it implements:
shape = .box()
Level B supports:
expr is .type for boolean type testsexpr as .type for checked caststypeof(expr) for concrete type introspectioninitialized(expr) for testing whether an object value has completed factory
initialization; non-object values are considered initializedObjects can also carry native payloads when exposed through the plugin API, but ordinary Level B code should interact with objects through factories, methods, and interfaces.
Level B does not define implicit object-to-string promotion. Statements such as
say value, string concatenation, and string comparison operate on values whose
types are already string-compatible under the Level B type rules; they do not
automatically call a toString() method on arbitrary objects. A future Level G
object-promotion capability is still undesigned. The likely direction is an
explicit contract, such as a supported interface for string rendering, rather
than a convention based only on a method name.
The compiler infers a variable type from the first binding in many common cases:
total = 0 /* .int */
label = "ready" /* .string */
Use explicit constructors or declarations when the intended type is not obvious from the initializer, when a signature is being published, or when a value must be an object or array contract.
A bare type expression can also declare a typed local before the value is known:
slot = .int
if found then slot = existing
else slot = created
Use this form when the declaration is the important point. Use a literal
initializer, such as slot = 0, only when that literal value is part of the
program logic.