CREXX

REXX Language implementation

View the Project on GitHub adesutherland/CREXX

Data Types

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.

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

Constructors and Type Literals

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

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.

References

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

Numeric Values

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.

Strings and Binary Values

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

Object Values

Classes and interfaces are object contracts. A concrete class instance can be assigned to an interface it implements:

shape = .box()

Level B supports:

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

Type Inference

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.