REXX Language implementation
Level B implements the core class/interface model:
matchexpr as .typeexpr is .typetypeof(expr).pkg..thing()Level B does not currently implement interface inheritance, interface attributes/state, interface factory bodies, overloads, singleton declarations, class/interface constants, or destructor/finalizer syntax.
Class and interface constants are a Release 2 roadmap item. The expected starting point is private constants owned by the class or interface body, consistent with the current member privacy model. Whether such constants also need a controlled public view should be investigated as part of that Release 2 design.
Interfaces define callable contracts. Classes implement those contracts.
Factory declarations do not carry an explicit return type. The return contract
is implied by the owner: an interface factory returns that interface contract,
and a class factory returns that concrete class. In a class factory, bare
return returns the constructed object; there is no source-level self value
to return explicitly.
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 "car:" || _name
Instances are normally created through the interface:
current = .vehicle("mini")
say current.describe()
Each class also has an intrinsic interface of its own name, so .car() remains
valid when you want to work directly with the concrete class contract.
An interface may declare:
* factoryAn interface method with a body is final in Level B. A class may rely on that body, but it may not override it.
shape: interface
*: factory
describe: method = .string
return prefix() || ":" || summary()
prefix: method = .string
return "iface"
summary: method = .string
In this example describe and prefix are final/default methods, while
summary remains abstract.
Classes implement one or more interfaces:
box: class implements .shape
*: factory
return
summary: method = .string
return "box"
The class must implement every abstract member from its effective interface set. If an interface already provides a final/default method body, the class may omit that member.
Factories are the exception to the ordinary name: callable = .type spelling:
write *: factory or name: factory, then declare arguments with arg as
usual. A factory return type written after = is not part of the Level B source
syntax.
Assignment compatibility in the current Level B implementation flows from a
concrete class value to an implemented interface. In practice that means a
statement such as iface = .widget(...) is supported, while interface-to-interface
assignment should not be relied on.
The factory call brackets are semantically significant. .widget() calls the
default factory and returns an initialized object. Bare .widget is the typed
default object value for that class and is not initialized. It can still satisfy
type-contract checks such as value is .object, but using it as a receiver
raises the catchable OBJECT_NOT_INITIALIZED signal. Use initialized(value)
when code needs to distinguish these states explicitly.
Class attributes are declared in the class block:
box: class implements .shape
_label = .string
_count = .int
For ordinary classes, let the compiler allocate storage automatically and keep external callers on factories and methods.
Explicit physical layout should be reserved for genuine low-level interop. When
that is needed, append with register.N to the attribute definition, where N
is the one-based VM object-attribute slot:
raw_event: class
_code = .int with register.1.int
_module = .int with register.2.int
_address = .int with register.3.int
_name = .string with register.4.string
The optional suffix after the index is the VM register value view to use for the
slot. Valid views are .int, .float, .decimal, .binary, .string, and
.object:
_message = .string with register.5.string
_payload = .object with register.5.object
System classes can also expose masked status-flag views using
register.N.flags.<partition>. These views are .int attributes over the VM
status word rather than value-payload views:
_cache_flags = .int with register.0.flags.library
_vm_flags = .int with register.0.flags.vm
The supported partitions are
.vm,.compiler,.library,.user,.public, and.readable..vm, .compiler, and .readable are read-only.
.library and .user are writable. .public is writable but covers only the
library and user bands, not compiler call-ABI flags. Assigning a writable flag
view replaces only that masked band; other status-word bits are preserved.
Flag views must be declared as .int; unknown partitions are rejected during
source validation.
The compiler emits the attribute linking code for methods that read or write these attributes. Source code should still access them through methods, not through hand-written assembler. It is valid for VM-integration classes to define more than one typed view over the same physical slot, as shown for a signal payload/message slot above. Ordinary application classes should not use explicit register mappings unless they are matching a fixed VM or native object layout.
register.0 is reserved for a typed view of the containing value itself. This
is a Rexx source-level convention, not RXAS attribute zero. The compiler lowers
it to a direct receiver/factory link, while register.1 and above continue to
name one-based child attributes. register.0 and duplicate typed mappings to
the same register.N slot are treated as complex attributes: compiler-generated
reads copy the selected view into a local register before expression code
manipulates it, and writes copy the selected payload view back through the
physical slot. Status/cache flag updates for these typed views are explicit
runtime code, not hidden compiler side effects. This is a low-level
system-programmer facility for runtime and VM-integration classes; ordinary
programs should use normal class attributes and methods.
Flag views are the exception to the complex typed-view copy rule: reads access the status word directly and do not copy the register’s string, binary, numeric, or object payload. Writes through writable flag views replace only the selected flag partition.
Inside a method, unqualified attribute and method names are resolved against
the current receiver where the member exists. For example, call clear() calls
the receiver’s clear method, and rc = append(value) calls the receiver’s
append method and stores its result.
self names the receiver storage explicitly. It is mainly useful when a method
needs to pass or return a live reference to its receiver:
iterator: method = .StringIterator
return .StringArrayListIterator(reference self)
Bare self is not an implicit reference. Use reference self when a formal
requires reference .Class, and use ordinary unqualified attribute access or
method calls for normal receiver access.
The standard class-library collection direction uses that explicit receiver
reference for live iterators: iterator() returns an unsynchronized live
iterator, and snapshotIterator() names the explicit factory-time snapshot
variant.
matchFactory members are declared on the interface and implemented by the class using the same name.
* is the default factory membername: factory is a named factory membername: match is the optional class-side selector for that same factorymatch is class-side only. Its signature must match the paired factory and it
must return .int.
Selection rules:
match, even when
there is only one candidate.match is omitted, the candidate behaves as if it returned 1.<= 0 reject that candidate.asset: interface
*: factory
arg spec = .string
from_size: factory
arg size = .int
describe: method = .string
return kind() || ":" || name() || ":" || size()
kind: method = .string
name: method = .string
size: method = .int
fileasset: class implements .asset
_name = .string
_size = .int
*: match
arg spec = .string
if spec = "log.txt" then return 100
return 0
*: factory
arg spec = .string
_name = spec
_size = 8
return
from_size: match
arg size = .int
if size = 8 then return 50
return 0
from_size: factory
arg size = .int
_name = "sized-file"
_size = size
return
kind: method = .string
return "file"
name: method = .string
return _name
size: method = .int
return _size
cacheasset: class implements .asset
_name = .string
_size = .int
*: factory
arg spec = .string
_name = spec
_size = 1
return
from_size: factory
arg size = .int
_name = "cache-" || size
_size = size
return
kind: method = .string
return "cache"
name: method = .string
return _name
size: method = .int
return _size
selected = .asset("log.txt")
fallback = .asset("memo")
say selected.describe() /* file:log.txt:8 */
say fallback.describe() /* cache:memo:1 */
This complete example is mirrored by the test
compiler/tests/rexx_src/interface_showcase_same_module.crexx.
A class may implement more than one interface as long as the public member surface remains coherent.
named: interface
*: factory
arg label = .string, length = .int
name: method = .string
measured: interface
size: method = .int
widget: class implements .named .measured
_label = .string
_length = .int
*: factory
_label = label
_length = length
return
name: method = .string
return _label
size: method = .int
return _length
item = .widget("gear", 4)
by_name = item
by_size = item
say by_name.name()
say by_size.size()
This example is mirrored by the test
compiler/tests/rexx_src/interface_multi_interface_same_module.crexx.
If two imported namespaces expose contracts with the same name, qualify the
reference with namespace...
import qifa
import qifb
left = .qifa..vehicle("one")
right = .qifb..vehicle.from_name("two")
The token to the left of .. must be a namespace name that has already been
imported. Qualification does not bypass import. namespace::symbol remains
accepted as a compatibility alias, but namespace..symbol is the canonical
form.
An interface and one or more implementation classes may live in the same source file and namespace. This is often the clearest shape when the contract and providers are developed together.
options levelb
namespace automotive expose vehicle mycar
vehicle: interface
*: factory
arg name = .string
describe: method = .string
mycar: class implements .vehicle
_name = .string
*: factory
arg name = .string
_name = name
return
describe: method = .string
return "dep:" || _name
The interface owns the public factory contract. The class supplies the implementation with the same factory name and argument signature; its concrete class return is accepted because the class implements the interface.
This same-file shape is mirrored by
compiler/tests/rexx_src/interface_dep_contract.crexx.
When an interface and its provider class live in different source files, each
file is still compiled as a separate module. Multiple files may contribute
symbols to the same namespace, just as the Level B standard library contributes
many rxfnsb functions from separate files, but the provider compile must still
be able to find the interface contract through the import path.
The provider may use the same namespace as the contract:
/* vehicle.rexx */
options levelb
namespace automotive expose vehicle
vehicle: interface
*: factory
arg name = .string
describe: method = .string
/* mycar.rexx */
options levelb
namespace automotive expose mycar
mycar: class implements .vehicle
_name = .string
*: factory
arg name = .string
_name = name
return
describe: method = .string
return "dep:" || _name
It is also valid to put provider classes in a separate provider namespace when that better matches packaging or ownership:
options levelb
namespace automotive_provider expose mycar
import automotive
mycar: class implements .vehicle
/* implementation as above */
Pure contract modules are valid: a source file that contains only interface or
class/interface metadata can compile and assemble into a metadata-only .rxbin.
A provider that consumes that binary contract must compile with the contract’s
directory on the binary import path, for example rxc -i build-dir provider.rexx.
Keeping source roots and binary roots separate prevents stale .rxbin side
products beside edited source from silently satisfying interface imports.
The class factory must have the same argument signature as the interface factory. The return contract is implicit on both sides: the interface factory returns the interface, and the class factory returns the concrete class. The compiler checks that the concrete class value is assignable to the interface return type.
If a provider reports #INTERFACE_NOT_FOUND or
#INTERFACE_MEMBER_SIGNATURE_MISMATCH, check these first:
.crexx, .crx, .rexx,
or the initial file’s arbitrary extension in a source root) or binary module
(.rxbin in a directory passed with -i) that exposes the interface.rxbin files are not searched
unless their directory is a binary rootThis example is mirrored by:
compiler/tests/rexx_src/qualified_interface_main.crexxcompiler/tests/rexx_src/qualified_interface_dep_a.crexxcompiler/tests/rexx_src/qualified_interface_dep_b.crexxLevel B supports explicit object casts and runtime type inspection:
vehicle = .car("roadster") as .vehicle
current = vehicle as .car
say typeof(current)
say current is .vehicle
say current is .car
say current is .truck
Rules:
expr as .interface succeeds when the concrete class implements that
interfaceexpr as .class succeeds only for that exact concrete classCONVERSION_ERRORexpr is .interface checks interface implementationexpr is .class checks the exact concrete classtypeof(expr) returns the concrete runtime class for objects and the
canonical built-in type name for scalarsThese operations are mirrored by:
compiler/tests/rexx_src/type_ops_showcase.crexxcompiler/tests/rexx_src/type_ops_fail.crexxThe class library separates canonical object equivalence from collection key policy:
.ObjectEquatable is an opt-in object contract. In Level G,
left <eq> right lowers to left.equivalent(right as .object)..ObjectKeyStrategy owns both hash(key) and
equivalent(left, right) for a particular map or set policy..ObjectComparator remains the canonical total-order contract for ordered
object collections. The older .Comparator name remains for compatibility.An ObjectKeyStrategy must return equal hashes whenever it reports two keys as
equivalent. It may choose case-sensitive, case-insensitive, namespace-scoped,
or domain-specific rules; there is deliberately no universal default object
hash and no raw-address hash.
For a copyable handle to a global resource, keep immutable logical fields such
as scope, namespace, resource type, value, and generation in the handle. Hash a
length-framed representation of exactly those fields and compare the same
fields in equivalent(). This identifies the resource across handle copies; it
does not turn the handle into a live cross-worker reference. See
examples/classes/global_object_keys.crexx for a complete strategy and map
example.