REXX Language implementation
This chapter defines the current cREXX task and parallel-block language surface. It is an initial Level G extension. The Level B class contracts used by its lowering are specified in the library reference.
Task declarations, task-target expressions and both forms of DO PARALLEL
are accepted only when the source selects:
options levelg
TASK and PARALLEL are case-insensitive contextual words. They are not
globally reserved. A variable or ordinary routine named task remains valid
when the token is not in one of the grammar positions defined below;
parallel remains an ordinary symbol except immediately after the opening
DO of a parallel block.
The Level B classes .taskpool, .taskscope, .task, .tasktarget,
.taskwork, .taskcontext, .completion, .channel, .channelrequest,
.channelvalue, .channelcodec, .byteendpoint, .serviceref and
.transferbuffer do not require Level G.
The callable forms are:
task-procedure ::= symbol ":" "task" [ "=" type ] callable-body
task-method ::= symbol ":" "task" [ "=" type ] callable-body
At module scope the form declares a task procedure. Within a class it declares
a task method. Arguments use the normal typed ARG statement. Omitting the
result type declares a .void task.
options levelg
namespace task_example
checksum: task = .int
arg text = .string
return length(text)
notify: task
arg text = .string
say text
return
The task modifier controls submission when the callable is invoked from a controlling execution. The body itself remains an ordinary callable body executed in one worker-owned Rexx execution.
The current typed task-call lowering accepts these scalar argument/result types:
.boolean;.int;.string;.binary; and.void as an omitted result only.A .channelvalue is accepted as a direct task argument. A concrete class is
accepted as an argument, result or task-method receiver only when the exact
static class declares the transfer contract in the next section.
Current task signatures do not accept arrays, references, varargs,
ARG EXPOSE, .float, .decimal, an interface result, untyped .object, or
another type without the exact transfer contract. RXCV can represent more
value kinds than the Level G typed-call sugar currently lowers; the broader
Level B .channelvalue and .taskwork path remains available when needed.
A transferable concrete class declares exactly resolved conversion members:
from_channel: factory
arg encoded = .channelvalue
to_channel: method = .channelvalue
to_channel has no arguments. from_channel has exactly one ordinary
.channelvalue argument. Both members must be declared on the exact concrete
class known at the task call site; an interface is not enough to select unique
reconstruction code.
The receiver and argument direction is controller to_channel, worker
from_channel. The result direction is worker to_channel, controller
from_channel. Reconstruction creates new receiver-owned objects. No live
object identity or reference crosses the boundary.
This complete task-method example uses the transfer contract for its receiver, argument and result:
numberbox: class
_value = .int
*: factory
arg value = .int
_value = value
return
from_channel: factory
arg encoded = .channelvalue
_value = encoded.as_integer()
return
to_channel: method = .channelvalue
return .channelvalue.integer_value(_value)
value: method = .int
return _value
calculator: class
_base = .int
*: factory
arg base = .int
_base = base
return
from_channel: factory
arg encoded = .channelvalue
_base = encoded.as_integer()
return
to_channel: method = .channelvalue
return .channelvalue.integer_value(_base)
add: task = .numberbox
arg amount = .numberbox
return .numberbox(_base + amount.value())
Its ordinary-looking method call is a task call:
calculator = .calculator(40)
answer = calculator.add(.numberbox(2))
The complete executable program is
../crexx_programming_guide/examples/concurrency_transferable_objects.crexx.
A call resolved to a task callable uses the existing Rexx procedure/function
or method-call grammar. No ASYNC call prefix is added.
answer = checksum("hello")
call notify "complete"
answer = calculator.add(.numberbox(2))
Outside DO PARALLEL, a task-valued expression has an implicit expression
scope that closes before its containing statement completes. A CALL task has
an implicit statement scope and completes before the next statement.
For an expression containing task calls, the implementation:
Thus:
answer = first() + second()
submits first before second, permits the two task bodies to overlap, waits
for both successful values, and then performs ordinary addition. It does not
make + asynchronous.
In first() + ordinary(), the controller may call ordinary() while
first() runs, but ordinary() is not moved to a worker. Existing
short-circuit behavior is retained. A task in an unevaluated right branch is
not submitted.
The complete positive expression/block program is
../crexx_programming_guide/examples/concurrency_basic.crexx.
The grammar is:
parallel-do ::= "do" "parallel" [ "using" expression ]
clauses
"end" [ symbol ]
The form is available wherever the corresponding ordinary DO ... END form
is available. In expression position the body must produce a result through
the existing LEAVE WITH expression statement.
do parallel
first_value = first()
second_value = second()
say "controller is still running ordinary clauses"
end
Only calls to task callables and explicit Level B submissions create child work. Other clauses execute sequentially in the controlling execution.
All task calls in one block belong to one scope. Without USING, the compiler
creates a scope over the execution-local default pool. USING expression is
evaluated once before the block opens and must yield a fresh open .taskscope:
pool = .taskpool.local(2, 8)
scope = .taskscope.collectall(pool, 60000)
answer = do parallel using scope
left = first()
right = second()
leave with left + right
end
call pool.close()
Block exit consumes and closes the scope, including exit by RETURN, EXIT,
LEAVE, controller signal or failed ordinary expression. The exit requests
child cancellation where required and accounts for all children before control
escapes. It does not close the pool. A consumed scope cannot be reused.
The full local/process example is
../crexx_programming_guide/examples/concurrency_providers.crexx.
Within DO PARALLEL, assignment directly from a task call creates a typed
pending binding:
do parallel
left = first()
right = second()
say left /* materializes left here */
end /* accounts for right even if it was never read */
Reading the binding materializes it and waits if necessary. It then becomes an ordinary value. Before materialization the following are invalid:
ARG EXPOSE use;For example, this is rejected with
#PENDING_TASK_RESULT_MUTATION:
do parallel
value = first()
value = 8
end
Expression-form blocks return only a materialized value:
answer = do parallel
left = first()
right = second()
leave with left + right
end
The grammar is:
task-target ::= "task" callable-reference
| "task" class-factory-expression
The callable form requires a statically resolved task procedure or generated task-method adapter:
target = task checksum
The factory form requires a statically resolved concrete factory whose class
implements .taskwork:
target = task .imagework("thumbnail")
Factory arguments are evaluated on the controller and reconstructed in the
worker. The current factory-target lowering accepts .boolean, .int,
.string, .binary and direct .channelvalue arguments. It does not use the
general concrete-object to_channel contract for factory arguments.
Dynamic procedure names and strings are invalid:
selected = "my_module.checksum"
target = task selected /* #TASK_DYNAMIC_TARGET */
The compiler, assembler and linker seal the selected callable, signature and
linked-image identity. .tasktarget.name() is diagnostic only; dispatch never
uses the user-authored name string.
.taskwork adapterAn advanced work class implements:
run: method = .channelvalue
arg request = .channelvalue, context = .taskcontext
For example:
adderwork: class implements .taskwork
_delta = .int
*: factory
arg delta = .int
_delta = delta
return
run: method = .channelvalue
arg request = .channelvalue, context = .taskcontext
if context.cancellation_requested() then return .channelvalue.null_value()
return .channelvalue.integer_value(request.as_integer() + _delta)
main: procedure = .int
pool = .taskpool.local(1, 4)
scope = .taskscope.collectall(pool, 60000)
target = task .adderwork(2)
child = scope.submit(target, .channelvalue.integer_value(40))
completion = child.wait(-1)
answer = completion.value().as_integer()
call scope.finish()
call pool.close()
return answer <> 42
The complete example, including the process provider, is
../crexx_programming_guide/examples/concurrency_taskwork.crexx.
The request above is already an application .channelvalue; the compiler does
not wrap it in the internal typed-object argument marker. The taskwork object is
constructed in the receiving execution from the target’s factory arguments.
A task body calling the same task callable is a normal synchronous recursive call in that worker:
factorial: task = .int
arg value = .int
if value <= 1 then return 1
return value * factorial(value - 1)
It does not create a child task. A call from one task body to a different task
callable, including a mutual-recursion edge, is rejected with
#TASK_NESTED_WAIT. This prevents a bounded worker from blocking while waiting
for work that may require the same pool.
inner: task = .int
return 1
outer: task = .int
return inner() /* #TASK_NESTED_WAIT */
The following diagnostic names are the current stable identifiers for the implemented checks:
| Diagnostic | Condition |
|---|---|
#TASK_ONLY_LEVELG |
task declaration or task call below Level G |
#TASK_TARGET_ONLY_LEVELG |
task-target expression below Level G |
#PARALLEL_ONLY_LEVELG |
statement or expression DO PARALLEL below Level G |
#TASK_EXPOSED_ARGUMENT |
ARG EXPOSE in a task signature |
#TASK_REFERENCE_TYPE |
reference argument or result |
#TASK_NONTRANSFERABLE_TYPE |
unsupported scalar, array, interface or class without an exact transfer contract |
#TASK_NONTRANSFERABLE_RECEIVER |
task-method receiver lacks the exact concrete transfer contract |
#TASK_DYNAMIC_TARGET |
target is a dynamic string/procedure value |
#TASKWORK_ADAPTER_REQUIRED |
factory target’s class does not implement .taskwork |
#TASK_NESTED_WAIT |
one task body calls a different task callable |
#PARALLEL_SCOPE_TYPE |
USING does not yield .taskscope |
#PARALLEL_SCOPE_REUSED |
one consumed scope is used by another parallel block |
#PENDING_TASK_RESULT_MUTATION |
pending binding is reassigned, referenced, mutated or escaped |
#TASK_RESULT_CYCLE |
the static task-result dependency graph contains a cycle |
The language does not expose a future type, async/await, detached task,
thread identity, affinity, locks, atomics or shared writable VM objects.
Services and .taskscope.ask() are reserved for a later single-owner state
model. Pool telemetry and cross-host providers are also not part of the current
language surface.
Library interfaces may still use the implemented task-target form. For
example, .httpserver.serve(task .handler()) receives a statically sealed
.taskwork factory target and launches independent request tasks. That does
not create a language service identity or make .taskscope.ask() available;
the distinction is specified in the
HTTP client and server guide.