ADR-050: Statement Order Is the Written Order, and overlap { … } Is How a Program Asks for Less

Status: Superseded by ADR-292 — its D1-D7 are D1-D7 there Related: ADR-033 (what this displaces: statement overlapping, seq, and the ordering build switch), ADR-038 D3 and D4 (the runtime this needs, and the part of it that is built), ADR-021 D5 (why ordering became a cache dimension — it stops being one), ADR-027 D1 (the sync property, which is what D6 reads to know which branch to start first), ADR-037 D2 (the switch, and why D6 reaches the same schedule at both settings), Part I 1.2 (the count of build switches, which returns to two), Part I 8.1.1, Part II 12.2 and 12.4–12.7 (the concurrency forms this joins)

1. Context

ADR-033 made two operations that touch disjoint resources unordered: the compiler may overlap them, and a program says otherwise with seq { … }. It has read provisional and unmeasured since it was accepted, and open-decisions.md carried the question of when it would be measured; this record is the answer, so the entry is gone from that file.

The measurement could not settle it. The only corpus is this repository’s own examples, written by the people who designed the language; a number off them describes those programs rather than the language. A decision conditioned on such a number is a decision deferred to an event that will not make it.

And the state it deferred is the worst of the available ones. Asked of a program, the analysis turns out to reach only adjacent statements: a pair of file reads with their failures caught overlaps, and one line between them removes the pair from consideration entirely — not refused with a reason, never examined. A read whose failure is uncaught overlaps with nothing at all, because the statement after it might never have run. So the wins are narrow and hard to predict, and where they do not arrive a program has no way to ask for them: spawn starts something that outlives the call, select takes the first to finish, par_iter runs one operation over many elements, and seq only forces an order. The one shape every other language offers first — run these and wait for all of them — is missing.

Unpredictable wins, and no recourse. That is what this record changes.

2. Decisions

D1 — Statement order is the written order, without qualification.

The automatic reordering is withdrawn. Two statements run in the order they are written whether or not they meet on anything, and no analysis stands between the source and the schedule.

The reason is not a number, and it is visible in the design being withdrawn. ADR-033 ships two escapes: seq { … }, to force an order the analysis cannot see, and --ordering strict, to turn the whole thing off. Two escapes are an admission — that the analysis can be incomplete, and that the correction is by hand, after the fact, by somebody who noticed. A rule that changes what a program means, resting on an analysis admitted to be incomplete and corrected manually, is the shape of a defect source rather than of a guarantee. Against that stands an optimisation whose reach nobody has been able to state.

D2 — overlap { … } runs its branches together and waits for all of them.

let (user, rights, prefs) = overlap {
    db::load_user(id)
    db::load_rights(id)
    cache::load_prefs(id)
}

Each statement in the block is one branch. The block starts every branch, waits for every branch, and its value is the tuple of their results in written order.

A branch is an expression. Several steps in one branch are a block expression inside it.

D3 — The branches must meet on nothing, and the compiler checks it.

The touch sets ADR-020 and ADR-033 built are kept, and asked the other way round. They no longer decide whether the compiler may overlap two statements; they decide whether the programmer was right to say so.

A pair of branches that reach a common resource is refused, and the diagnostic names the resource:

error: these two branches cannot run together — they meet on the output
   3 |     println("x")
   4 |     println("y")
     = two writes to one resource have an order, and `overlap` says they have none
     = if you meant them in order, write them as ordinary statements

This is the part no other language has. Every mainstream form for run these together takes the programmer’s word for the independence; here the claim is checked against what the ledger records each call touching. That is the return on the machinery ADR-033 built, kept after the inference it was built for is gone.

D4 — An overlap block is not a task, and nothing outlives it.

The block ends before the function continues. Nothing is moved into a branch, borrowing works inside one, and the structural crossing check (ADR-005 §1 Group B) has nothing to do, because nothing crosses out.

That is what makes the form lighter than two spawns, and why it earns a form of its own rather than being a pattern written with the ones that exist.

D5 — A branch that fails fails the block, and the first in written order wins.

Failure propagates as it does anywhere else (ADR-023 D8): a branch whose failure is uncaught makes the block fail. Where more than one branch fails, the error is the one from the first branch in written order — the only order the source has, and therefore the only one that makes the result reproducible — and the others are attached to it as its secondary list, in written order (ADR-115).

Per-branch handling is catch inside the branch, unchanged.

D6 — Every branch is started before any branch that cannot suspend is run.

I/O is not user code, so any number of operations may be in flight at once at either setting of user_parallelism (ADR-037 D2). What no bounds is user computation: one piece at a time. So a block holding two reads and one computation costs max(reads, computation) rather than their sum, at both settings, and the setting changes only whether two computations may also overlap.

Reaching that bound requires an order of starting, and the naive one does not:

let (n, a, b) = overlap {
    expensive(x)              // does not suspend
    fs::read_to_string("a")   // suspends
    fs::read_to_string("b")   // suspends
}

Running branches in written order until each suspends would run expensive to completion before either read is submitted, and the block would cost the sum. So: a branch is started up to its first suspension point before any branch that cannot suspend is run. The reads go out, then the computation runs while they are in flight, then the results are collected.

Which branches those are is read off machinery that exists: a call that can pause is exactly what ADR-027 D1’s sync property already answers, per function, over the same call graph.

This changes nothing observable, because D3 has established that the branches meet on nothing. It is a choice of schedule and not of meaning — how, never what (Part I 1.2).

D7 — seq { … } and [build] ordering are withdrawn with the reordering.

seq exists to force an order between statements that would otherwise be reordered. Under D1 nothing reorders them, so it has nothing to do and goes.

ordering in [build] exists to turn the reordering off. There is nothing left to turn off, so it goes as well — and Part I 1.2’s “there are exactly two things to choose” becomes true again, having been three. ADR-021 D5’s cache dimension goes with it.

3. Consequences

What this displaces. ADR-033 D1’s unordering of disjoint operations, D7’s seq, and the ordering switch; Part I 8.1.1’s “Order Is Kept Where It Can Be Seen”, whose title is the qualification D1 removes; Part II 12.2’s closing sentence, which points at seq for an order the touch sets cannot see — under D1 there is no such case, because nothing is taken away to begin with. Part I 1.2’s count of switches returns to two.

What it keeps. The touch sets, their ledger entries, and --overlaps, which becomes a report about overlap blocks rather than about statements the compiler chose. The analysis survives its original purpose because D3 gives it a better one: checking a claim is a stronger use of it than making one.

What a program loses. The overlap it used to get without asking, on adjacent independent I/O with failures caught. That is the narrow case measured in §1, and D2 covers it by being written down.

4. What this does not decide

5. What is built

D2 through D6 — the construct. overlap { … } parses, each statement is a branch, every branch is in flight at once, and the block’s value is their results in written order.

D3 is the one that earns the form, and it is built on exactly the machinery D1 withdraws the inference for: each branch is reduced to an ADR-033 Operation and every pair is asked for a verdict. A pair that is not Overlap is refused as NK2104, and the message is that verdict’s own words — “both reach stdout, and one writes it”. A branch this compiler cannot account for is not refused, because reading an absence as “they meet” would refuse a correct program on nothing (Part III C.4). And a branch that binds is refused too: the block’s value already carries every branch’s result, so a let inside one would name a thing that leaves by two doors.

D6 is the argument order, which is what makes it cheap: std has one overlap<n> per arity that polls every branch in one pass, in the order it is given, and the emitter hands over the branches that can pause first. Nested binary joins would have made the naive order structural — interleave(a, interleave(b, c)) polls a to completion before b is started, which is the sum D6 names and refuses. The results are put back into written order at the call, with a comment naming this record; a block whose branches are all of one kind pays for no permutation.

D5 is the language below’s own control flow. Where any branch can fail out of itself, every branch is wrapped in Ok and the tuple’s elements each take a ? in written order — and ? returns at the first Err, so “the first in written order wins” is not a comparison this compiler makes. Per-branch handling is a catch inside the branch, which the fallibility walk does not count. The error type is written rather than inferred, because an async block with a ? and nothing to infer from is “type annotations needed” about a file nobody wrote (Part III C.1).

D4 is visible in the emitted Rust: a branch is an async block and not an async move one, so nothing is moved in and a branch borrows what is around it exactly as an ordinary statement does. That is what makes the form lighter than two spawns, and it is a line of generated code rather than a claim.

D1 and D7 — the withdrawal. The order §5 gave was the runtime binding, then D2–D6 on it, then this; withdrawing the reordering before there was a way to ask for overlap would have left the language with neither, and there is a way now. So: the emitter no longer groups statements, seq { … } is not a construct and seq is not a reserved word, ordering is not a switch in the manifest or on the command line, and ADR-021 D5’s cache dimension for it is gone. Part I 1.2’s count of switches is two again, having been three.

What survives is the half D3 needs, which is the whole point of the order this was taken in. contracts::order keeps the touch sets, Operation, verdict and the vocabulary — what goes is Vehicle, the grouping, and the lowerings the compiler chose. --overlaps is a report about overlap blocks now: which branches a program wrote, and which of them are started first, which is the one thing D6 decides that the source does not show.

Un-reserving seq is the direction that costs nothing. Reserving a word narrows what parses and un-reserving one widens it, so no program that compiled before stops compiling — and Part I 1334’s own example names a parameter seq, which is a line the withdrawal makes legal rather than one it breaks.

One limit, with its reason in the refusal. std writes one vehicle per arity and eight are written, because the branches have different types and a tuple of futures is what that means in the language below. A block of more branches is refused naming the limit rather than miscompiled. A block of one is refused too: it has nothing to overlap with.

And a gap this met rather than made. Part I 8.1.2’s own example writes let (user, rights, prefs) = overlap { … }, and let takes one name. Part II 12.5’s let (tx, rx) = channel::bounded(100) writes the same form, so the destructuring let is a construct the specification uses twice and defines nowhere — #224 carries it. An overlap is reached by its tuple in the meantime.

This site is open source. Improve this page.