When statement

From Esolang
Jump to navigation Jump to search

Loosely, a when-statement is a block guarded by a conditional statement such that the block is executed when the condition is satisfied, rather than the standard order of execution. Evaluation of a when-statement is isolated, acting as a subroutine, procedure, or method; after executing its inner block, control is returned to whatever operation may have been interrupted.

Naive implementation of when-statements involves checking some sort of constraint store after every primitive operation is executed. However, for many systems, there are more efficient ways to check constraints for satisfaction.

Psuedocode example:

Int A = 0
While (true) {
  A++
}

// This part of the code is never reached, as it never exits the While loop.
When (A == 5) { // Despite this, the When block executes once A == 5.
  Print(“A is 5”)
}

Constraints and handlers

Perhaps the most abstract sort of when-statement is when CHR is added to an existing language. For example, consider the following standard example handlers, in Prolog-style CHR, implementing the axioms of a wikipedia:partial order or implication logic:

reflexivity  @ implies(X, X) <=> true.
antisymmetry @ implies(X, Y), implies(Y, X) <=> X = Y.
transitivity @ implies(X, Y), implies(Y, Z) ==> implies(X, Z).
idempotence  @ implies(X, Y) \ implies(X, Y) <=> true.

In a CHR(Java) using these handlers, calls to implies() could provoke non-local control flow. For example, over a domain of strings, consider the following Java statements:

implies("this", "that");
implies("that", "this");
unreachable();

This sequence awakens the antisymmetry handler, which in turn attempts to unify the strings "this" and "that". Since these strings are distinct, the constraint store cannot be put into a consistent state after the second handler call returns, and thus CHR(Java) implementations will throw an exception to avoid evaluating unreachable(). This exception can be caught by another part of the CHR runtime which backtracks, undoing the effects to the constraint store and trying another logical possibility; as such, CHR can be used to add backtracking to an otherwise-pure language.

To implement when-statements, custom user-level code can be attached to events generated by the constraint store, such as when a constraint is added or deleted. In this sense, user-level code can implement handlers. From another perspective, the CHR handlers are themselves when-rules: when a row of constraints is simultaneously satisfied, non-deterministically remove that row from the constraint store and run some (Prolog) code before adding more constraints.

Efficient compilation of CHR is a perennial topic.[1] For languages like C, Java, ECMAScript, or Python, explicitly treating handlers as callable objects has led to a series of domain-specific embeddings. CHR also embeds cleanly into Prolog and is available in multiple open-source implementations.

Actors and messages

In object-capability languages like Joule, E, or Monte, a when-expression delays the execution of a block until after a triggering event. Instead of a Boolean value, the trigger is encapsulated in a promise which can either successfully resolve to an object or break with an error message. Instead of interrupting or preempting execution, promises in E and its relatives are delivered in discrete isolated evaluations, one evaluation per delivery, so that the execution of a when-block does not interfere with any other code.[2]

For example, the following code adapted from the Monte standard library asynchronously reads text from standard input, processes it, and emits text to standard output, returning a successful return value 0:

return when (def input := collectBytes(stdio.stdin())) ->
    def stdout := stdio.stdout()
    def output :Bytes := mast.bytes(input)
    when (stdout(output), stdout<-complete()) -> { 0 }

Desugaring reveals a network of promises and data plumbing:

def p0 := collectBytes.run(stdio.stdin())
def p1 := when (p0) ->
    def input := p0
    def stdout := stdio.stdout()
    def output :Bytes := mast.bytes(input)
    def p2 := stdout.run(output)
    def p3 := stdout<-complete()
    when (p2) -> { when (p3) -> { 0 } }
return p1

Note that in E and Monte, the . syntax is for an immediate call; the underlying message will be immediately delivered. In contrast, the <- syntax is for a delayed send; the underlying message will be enqueued and delivered in a subsequent turn. So, the promise p2 is being returned from .run(output), an immediate call that creates a promise object through some unknown means, while p3 is being created on the spot because the send <-complete() will be run later.

Efficient management of promises only requires path compression: whenever a promise refers to another promise, its referrers should be updated to point to that other referent promise. Otherwise, delivery of promises must happen in a partial order corresponding to the order in which the underlying messages were delivered. Concretely, this means that each object runtime maintains a queue of outgoing messages as well as a queue of incoming messages; to perform a single delivery, a single incoming message is taken from the incoming queue and all resulting calls are immediately performed recursively, with each sent message being enqueued in order on the outgoing queue. A delivery is called a turn.

Let's walk through the given example. On the first turn, p0 and p1 are created. Some time later, p0 resolves and the when-block is executed in its own turn. On the when-block's turn, p2 is created, and also stdout<-complete() is enqueued, which will perform the delivery of stdout.complete(). Because we called stdout.run() before stdout.complete(), the latter's turn will run after whatever promise was created in the former, and it happens to be the case that the standard output API guarantees ordering, so p2 will resolve before p3.

Hardware design

In Verilog, an always-statement has a list of signals to which it is sensitive. Whenever any sensitive signal changes, the corresponding always-statements are executed. For example, the following code implements a flip-flop circuit storing a single bit of data:

module D (clock, reset, data, Q, Qn);
  input clock, reset, data;
  output Q, Qn;
  always @ (posedge clock, posedge reset) begin
    if (reset) begin
      Q <= 0; Qn <= 1;
    end else begin
      Q <= data; Qn <= ~data;
    end
  end
endmodule

Like a physical D flip-flop, the module outputs both the original bit Q and the negated bit Qn. Whenever either clock or reset are rising from low to high, the values of Q and Qn are updated.

Efficient implementation of Verilog or other hardware-design languages is the central topic of integrated-circuit design.

See Also

References

  1. SNEYERS, J., VAN WEERT, P., SCHRIJVERS, T., & DE KONINCK, L. (2010). As time goes by: Constraint Handling Rules: A survey of CHR research from 1998 to 2007. Theory and Practice of Logic Programming, 10(1), 1–47. doi:10.1017/S1471068409990123 https://arxiv.org/abs/0906.4474
  2. Mark S. Miller, E. Dean Tribble, and Jonathan Shapiro. 2005. Concurrency among strangers: programming in E as plan coordination. In Proceedings of the 1st international conference on Trustworthy global computing (TGC'05). Springer-Verlag, Berlin, Heidelberg, 195–229. http://www.erights.org/talks/promises/paper/tgc05.pdf