1.4 Control Model
Language constructs like prompts, delimited continuations, and threads allow programs a degree of control over expression evaluation.
1.4.1 Continuation Frames and Marks
See Continuation.Marks for continuation-mark forms and functions.
Every continuation C can be partitioned into continuation frames C1, C2, ..., Cn such that C = C1[C2[...[Cn]]], and no frame Ci can be itself partitioned into smaller continuations. Evaluation steps add frames to and remove frames from the current continuation, typically one at a time.
Each frame is conceptually annotated with a set of continuation marks. A mark consists of a key and its value. The key is an arbitrary value, and each frame includes at most one mark for any given key. Various operations set and extract marks from continuations, so that marks can be used to attach information to a dynamic extent. For example, marks can be used to record information for a “stack trace” to be presented when an exception is thrown, or to implement dynamic scope.
1.4.2 Prompts, Delimited Continuations, and Barriers
See Continuations for continuation and prompt functions.
A prompt is a special kind of continuation frame that is annotated with a specific prompt tag (essentially a continuation mark). Various operations allow the capture of frames in the continuation from the redex position out to the nearest enclosing prompt with a particular prompt tag; such a continuation is sometimes called a delimited continuation. Other operations allow the current continuation to be extended with a captured continuation (specifically, a composable continuation). Yet other operations abort the computation to the nearest enclosing prompt with a particular tag, or replace the continuation to the nearest enclosing prompt with another one. When a delimited continuation is captured, the marks associated with the relevant frames are also captured.
A continuation barrier is another kind of continuation frame that prohibits certain replacements of the current continuation with another. Specifically, a continuation can be replaced by another only when the replacement does not introduce any continuation barriers. A continuation barrier thus prevents “downward jumps” into a continuation that is protected by a barrier. Certain operations install barriers automatically; in particular, when an exception handler is called, a continuation barrier prohibits the continuation of the handler from capturing the continuation past the exception point.
An escape continuation is a derived concept. It combines a prompt for escape purposes with a continuation for mark-gathering purposes. As the name implies, escape continuations are used only to abort to the point of capture.
1.4.3 Threads
See Threads and Concurrency for thread and synchronization functions.
Rhombus supports multiple threads of evaluation. Threads run concurrently, in the sense that one thread can preempt another without its cooperation, independent of whether the threads all run on the same processor (i.e., the same underlying operating system process and thread) as coroutine threads or potentially on different processors as parallel threads.
Threads are created explicitly by forms such as thread. In terms of the evaluation model, each step in evaluation actually deals with multiple concurrent expressions, up to one per thread, rather than a single expression. The expressions all share the same objects and top-level variables, so that they can communicate through shared state, and sequential consistency is guaranteed for coroutine threads (i.e., the result is consistent with some global sequence imposed on all evaluation steps across threads). Most evaluation steps involve a single step in a single thread, but certain synchronization primitives require multiple threads to progress together in one step; for example, an exchange of a value through a channel progresses in two threads simultaneously.
Unless otherwise noted, all constant-time functions and operations provided by Rhombus are thread-safe in the sense that they are atomic: they happen as a single evaluation step. For example, := assigns to a variable as an atomic action with respect to all threads, so that no thread can see a “half-assigned” variable. Similarly, [] with := assigns to an array atomically. Note that the evaluation of a := expression with its subexpression is not necessarily atomic, because evaluating the subexpression involves a separate step of evaluation. Only the assignment action itself (which takes after the subexpression is evaluated to obtain a value) is atomic. Similarly, a function call can involve multiple steps that are not atomic, even if the function itself performs an atomic action.
The [] plus := combination is not atomic on a MutableMap, but the map is protected by a lock; see Maps for more information. Port operations are generally not atomic, but they are thread-safe in the sense that a byte consumed by one thread from an input port will not be returned also to another thread, and methods like Port.Input.Progress.commit and Port.Output.write_bytes offer specific concurrency guarantees.
In addition to the state that is shared among all threads, each thread has its own private state that is accessed through thread cells. A thread cell is similar to a normal mutable object, but a change to the value inside a thread cell is seen only when extracting a value from that cell in the same thread. A thread cell can be preserved; when a new thread is created, the creating thread’s value for a preserved thread cell serves as the initial value for the cell in the created thread. For a non-preserved thread cell, a new thread sees the same initial value (specified when the thread cell is created) as all other threads.
1.4.4 Context Parameters
See Context Parameters for context-parameter forms and functions.
Context parameters are a derived concept in Rhombus; they are defined in terms of continuation marks and thread cells. However, parameters are also “built in,” due to the fact that some primitive functions consult parameter values. For example, the default output stream for primitive output operations is specified by a parameter.
A parameter is a setting that is both thread-specific and continuation-specific. In the empty continuation, each parameter corresponds to a preserved thread cell; a corresponding parameter function accesses and sets the thread cell’s value for the current thread.
In a non-empty continuation, a parameter’s value is determined through a parameterization that is associated with the nearest enclosing continuation frame via a continuation mark (whose key is not directly accessible). A parameterization maps each parameter to a preserved thread cell, and the combination of the thread cell and the current thread yields the parameter’s value. A parameter function sets or accesses the relevant thread cell for its parameter.
Various operations, such as parameterize, install a parameterization into the current continuation’s frame.
1.4.5 Exceptions
See Exceptions for exception forms, functions, and types.
Exceptions are a derived concept in Rhombus; they are defined in terms of continuations, prompts, and continuation marks. However, exceptions are also “built in,” due to the fact that primitive forms and functions may throw exceptions.
An exception handler to catch exceptions can be associated with a continuation frame though a continuation mark (whose key is not directly accessible). When an exception is thrown, the current continuation’s marks determine a chain of exception-handler functions that are consulted to handle the exception. A handler for uncaught exceptions is designated through a built-in context parameter.
One potential action of an exception handler is to abort the current continuation up to an enclosing prompt with a particular prompt tag. The default handler for uncaught exceptions, in particular, aborts to a particular tag for which a prompt is always present, because the prompt is installed in the outermost frame of the continuation for any new thread.
1.4.6 Custodians
See Custodians for custodian functions.
A custodian manages a collection of objects such as threads, Port.FileStream objects, TCPListener objects, and UDP objects. Whenever a thread, etc., is created, it is placed under the management of the current custodian as determined by the Custodian.current context parameter.
Except for the root custodian, every custodian itself is managed by a custodian, so that custodians form a hierarchy. Every object managed by a subordinate custodian is also managed by the custodian’s owner.
When a custodian is shut down via Custodian.shutdown_all, it forcibly and immediately closes the ports, TCP connections, etc., that it manages, as well as terminating (or suspending) its threads. A custodian that has been shut down cannot manage new objects. After the current custodian is shut down, if a function is called that attempts to create a managed resource (e.g., Port.Input.open_file, thread), then the Exn.Fail.Contract exception is thrown.
The values managed by a custodian are semi-weakly held by the custodian; for example, the fact that a value that is managed by a custodian will not prevent it from being remoaved from a WeakMutableMap. A custodian only weakly references its subordinate custodians; if a subordinate custodian is unreferenced but has its own subordinates, then the custodian may be garbage collected, at which point its subordinates become immediately subordinate to the collected custodian’s superordinate (owner) custodian.
In addition to the other entities managed by a custodian, a custodian box created with Custodian.Box strongly holds onto a value placed in the box until the box’s custodian is shut down. However, the custodian only weakly retains the box itself, so the box and its content can be collected if there are no other references to them.