diff --git a/docs/examples/fibers/.gitignore b/docs/examples/fibers/.gitignore new file mode 100644 index 0000000..cc44a91 --- /dev/null +++ b/docs/examples/fibers/.gitignore @@ -0,0 +1,3 @@ + +# built artifacts +target/ diff --git a/docs/examples/fibers/README.md b/docs/examples/fibers/README.md new file mode 100644 index 0000000..76ebf53 --- /dev/null +++ b/docs/examples/fibers/README.md @@ -0,0 +1,21 @@ +# fibers — the hybrid scheduler, demonstrated + +The smallest program that shows all three legs of the concurrency model +(the doctrine-depth writeup lives in +[`docs/plan/exploration/fibers/00-fibers.md`](../../plan/exploration/fibers/00-fibers.md)): + +1. **Reduction-budget eviction** — part 1's output is byte-exact and + timing-free: main sends three messages, then burns reductions; every + time its budget expires the counter actor gets a turn and delivers + exactly one message. Cooperative mechanics, preemptive fairness. +2. **Actors as the spawn surface** — `spawn Counter { ... }` returns an + `actor Tick`; `send` moves the message (using it afterwards is a + compile error); delivery is one message at a time per actor. +3. **Parking, not blocking** — part 2's sleeper actor calls `time.sleep` + mid-receive: the fiber parks on the shard's I/O plane (io_uring + primary, epoll fallback — `WO_IO=uring|epoll` forces either) and + main keeps ticking while it sleeps. + +Run: `just fibers` (the acceptance gate builds it, checks part 1 +byte-exact, asserts part 2's ordering invariants, and repeats the run on +both I/O backends plus ASan). diff --git a/docs/examples/fibers/main.wo b/docs/examples/fibers/main.wo new file mode 100644 index 0000000..f7c4025 --- /dev/null +++ b/docs/examples/fibers/main.wo @@ -0,0 +1,74 @@ +use time + +-- fibers — the writeonce hybrid demonstrated (BEAM's shape): +-- cooperative MECHANICS (no signals, no interrupts) with +-- reduction-budget EVICTION (loop back-edges pay one reduction; at +-- zero the fiber re-queues whether it likes it or not), actors as the +-- spawn surface (one message at a time, ownership-moving sends), and +-- blocking builtins that PARK on the shard's io_uring plane instead +-- of holding the thread. +-- +-- Part 1 is deterministic by construction (budget accounting, no time): +-- its output is byte-exact. Part 2 shows a sleeping fiber not blocking +-- anyone; its ordering is asserted loosely by the acceptance gate +-- because it depends on real time. + +class Tick { + n: Int +} + +-- Part 1: a counting actor; main and the actor interleave one message +-- per main-yield under the default budget. +class Counter { + label: Text + total: Int + fn receive(msg: Tick) { + self.total = self.total + msg.n; + print("${self.label} +${msg.n} = ${self.total}"); + } +} + +-- Part 2: an actor whose receive PARKS mid-message. The park releases +-- the shard: main keeps ticking while this fiber sleeps. +class Sleeper { + pad: Int + fn receive(msg: Tick) { + print("sleeper: down for ${msg.n}ms"); + time.sleep(msg.n); + print("sleeper: up"); + } +} + +fn spin(rounds: Int) { + -- burn reductions so the scheduler's eviction gets a chance: each + -- back-edge pays one reduction; the default budget is 4000 + let i = 0; + while i < rounds { + i = i + 1; + } +} + +fn main() -> Int { + -- ---- part 1: deterministic budget interleave ---- + let c: actor Tick = spawn Counter { label: "count", total: 0 }; + send(c, Tick { n: 1 }); + send(c, Tick { n: 2 }); + send(c, Tick { n: 3 }); + -- three yields deliver exactly three messages, one per turn + spin(5000); + spin(5000); + spin(5000); + print("part1 done"); + + -- ---- part 2: a parked fiber blocks nobody ---- + let s: actor Tick = spawn Sleeper { pad: 0 }; + send(s, Tick { n: 150 }); + let t = 0; + while t < 8 { + time.sleep(25); + print("main tick ${t}"); + t = t + 1; + } + print("part2 done"); + return 0; +} diff --git a/docs/examples/fibers/wo.toml b/docs/examples/fibers/wo.toml new file mode 100644 index 0000000..333654e --- /dev/null +++ b/docs/examples/fibers/wo.toml @@ -0,0 +1,6 @@ +name = "fibers" +version = "0.1.0" +description = "the hybrid scheduler demonstrated: budget eviction + actors + parked sleeps" + +[runtime] +wo = ">= 0.1" diff --git a/justfile b/justfile index 7ab2f6a..7d611e5 100644 --- a/justfile +++ b/justfile @@ -43,6 +43,12 @@ deps-accept: web-app: ./scripts/web-app-accept.sh +# fibers: the hybrid-scheduler demo (docs/examples/fibers) — part 1 byte- +# exact budget interleave, part 2 parked-sleeper-blocks-nobody, on the +# uring AND epoll backends plus an ASan run. +fibers: + ./scripts/fibers-accept.sh + # install-accept: extract the dist tarball to a temp prefix, PATH it, and prove # `woc version` + a from-scratch project build+run (self-located wovm) + the # wo-constraint refusal all work — the "tarball install actually works" gate. diff --git a/scripts/fibers-accept.sh b/scripts/fibers-accept.sh new file mode 100755 index 0000000..1f15246 --- /dev/null +++ b/scripts/fibers-accept.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# scripts/fibers-accept.sh — the hybrid-scheduler demo's gate: part 1 is +# byte-exact (budget accounting, timing-free); part 2 asserts ordering +# invariants (a parked sleeper blocks nobody) on BOTH I/O backends and +# under ASan. +set -uo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +WOC="$ROOT/compiler/_build/default/bin/woc" +WOVM="$ROOT/runtime/wovm" +DIR="$ROOT/docs/examples/fibers" + +pass=0; fail=0 +ok() { echo "ok $1"; pass=$((pass + 1)); } +bad() { echo "FAIL $1 -- $2"; fail=$((fail + 1)); } + +if [[ ! -x "$WOC" || ! -x "$WOVM" ]]; then + echo "fibers-accept: build woc and wovm first" >&2; exit 1 +fi + +if WO_RUNTIME="$WOVM" "$WOC" "$DIR" >/dev/null 2>&1 && [[ -x "$DIR/target/fibers" ]]; then + ok "builds" +else + bad "build" "woc failed"; echo "fibers-accept: 1 checks, 1 failures"; exit 1 +fi + +check_run() { # name [env pairs...] + local name="$1"; shift + local out + out="$(env "$@" "$DIR/target/fibers" 2>&1)" + local want_p1=$'count +1 = 1\ncount +2 = 3\ncount +3 = 6\npart1 done' + if [[ "$(printf '%s\n' "$out" | head -4)" == "$want_p1" ]]; then + ok "$name: part 1 byte-exact (budget interleave)" + else + bad "$name part1" "$(printf '%s' "$out" | head -4 | tr '\n' '|')" + fi + local down_ln up_ln done_ln ticks + down_ln=$(printf '%s\n' "$out" | grep -n "sleeper: down" | cut -d: -f1) + up_ln=$(printf '%s\n' "$out" | grep -n "sleeper: up" | cut -d: -f1) + done_ln=$(printf '%s\n' "$out" | grep -n "part2 done" | cut -d: -f1) + ticks=$(printf '%s\n' "$out" | grep -c "main tick") + if [[ -n "$down_ln" && -n "$up_ln" && -n "$done_ln" && "$ticks" == "8" ]]; then + local before + before=$(printf '%s\n' "$out" | sed -n "${down_ln},${up_ln}p" | grep -c "main tick") + if [[ "$before" -ge 3 && "$up_ln" -lt "$done_ln" ]]; then + ok "$name: part 2 — parked sleeper blocked nobody ($before ticks while down)" + else + bad "$name part2" "only $before ticks before wake" + fi + else + bad "$name part2" "down=$down_ln up=$up_ln done=$done_ln ticks=$ticks" + fi +} + +check_run "auto" +check_run "uring" WO_IO=uring +check_run "epoll" WO_IO=epoll + +# ASan flavor: rebuild the binary against the ASan runtime and repeat once +make -C "$ROOT/runtime" wovm-asan -s >/dev/null 2>&1 +if "$WOC" build "$DIR" -o "$DIR/target/fibers_asan" --runtime "$ROOT/runtime/build/wovm_asan" >/dev/null 2>&1; then + out="$("$DIR/target/fibers_asan" 2>&1)" + if printf '%s' "$out" | grep -q "part2 done" && ! printf '%s' "$out" | grep -qi "sanitizer\|leak"; then + ok "ASan run clean" + else + bad "asan" "$(printf '%s' "$out" | tail -2 | tr '\n' '|')" + fi +else + bad "asan" "build failed" +fi + +echo +printf 'fibers-accept: %d checks, %d failures\n' "$((pass + fail))" "$fail" +[[ $fail -eq 0 ]]