docs: update plan done logs (Phases 1-4)

- 01: crate scaffolding
- 02: event loop epoll
- 03: hand-rolled HTTP
- 04: tokio/axum cutover
This commit is contained in:
shoney.arickathil 2026-08-10 14:11:32 +02:00
parent 138b393bd1
commit 02714c296f
4 changed files with 27 additions and 27 deletions

View file

@ -15,8 +15,8 @@ Lay out the full `crates/` directory tree that the 7-phase `.wo` runtime design
1. **Scope: all 7 phases.** 14 new empty library crates covering Phases 2 → 6 land in one pass. `rt` (already shipping Stage 2) is the 15th.
2. **`rt` stays monolithic.** Today's Stage 2 code — lexer / parser / AST / compile / engine / server — stays inside `crates/rt/` and continues to satisfy the 14 existing unit tests. Code migrates into the new crates as each phase activates, not in this pass.
3. **Contents: `Cargo.toml` + `src/lib.rs` doc-comment only.** Each `lib.rs` is one module-level `//!` doc block pointing at the phase doc, naming the responsibilities, and flagging which modules in `rt` migrate here later. No placeholder types, no stub traits.
4. **No `wo-` prefix.** New runtime crates are `ql`, `value`, `engine`, etc. — not `wo-ql`, `wo-value`. The prefix is redundant inside the project's own `wo` namespace and noisy in imports (`use ql::Parser` beats `use wo_ql::Parser`). The v1 crates in `reference/crates/` keep their `wo-` prefix — the distinct prefix makes the v1/v2 split visible at a glance.
5. **Workspace membership: root `Cargo.toml` lists every new crate as a member.** `reference/crates` stays `exclude`-d (nested workspace, separate v1 code).
4. **No `wo-` prefix.** New runtime crates are `ql`, `value`, `engine`, etc. — not `wo-ql`, `wo-value`. The prefix is redundant inside the project's own `wo` namespace and noisy in imports (`use ql::Parser` beats `use wo_ql::Parser`). The v1 crates in `.dev/reference/crates/` keep their `wo-` prefix — the distinct prefix makes the v1/v2 split visible at a glance.
5. **Workspace membership: root `Cargo.toml` lists every new crate as a member.** `.dev/reference/crates` stays `exclude`-d (nested workspace, separate v1 code).
Rationale and alternatives considered: see [`../../CLAUDE.md`](../../CLAUDE.md) "What's in `rt` today vs. what the empty crates promise" and the recorded `AskUserQuestion` answers that preceded this plan.
@ -53,7 +53,7 @@ All names are stable — documented in [`../runtime/database/07-wo-seg-migration
| `crates/README.md` with the phase-mapped inventory | ✅ |
| `cargo build` at root (compiles 15 crates) | ✅ |
| `cargo test --lib` at root (14 existing `rt` tests) | ✅ |
| `cd reference/crates && cargo build && cargo test` (v1 still green) | ✅ |
| `cd .dev/reference/crates && cargo build && cargo test` (v1 still green) | ✅ |
| `cargo run --bin wo -- run docs/examples/blog` (Stage 2 still serves) | ✅ |
## Non-scope

View file

@ -21,17 +21,17 @@ Nothing is removed in this phase. The module sits alongside the tokio-backed axu
| File | Responsibility | Port source |
| --- | --- | --- |
| `mod.rs` | Re-exports `EventLoop`, `Event`, `Interest`, `Token`, `EventFd`, `TimerFd`, `SignalFd` | [`reference/crates/wo-event/src/lib.rs`](../../reference/crates/wo-event/src/lib.rs) (9 LOC) |
| `netpoll_epoll.rs` | `EventLoop { fd, events }` — `new()`, `register(raw_fd, interest, token)`, `wait_once(timeout) -> &[Event]`, `deregister(raw_fd)` | [`reference/crates/wo-event/src/epoll.rs`](../../reference/crates/wo-event/src/epoll.rs) (183 LOC); [`reference/go/src/runtime/netpoll_epoll.go`](../../reference/go/src/runtime/netpoll_epoll.go) for idiom |
| `eventfd.rs` | `EventFd { fd }` — counter semaphore for cross-fd wake-up (subscription dispatch, shutdown signal) | [`reference/crates/wo-event/src/eventfd.rs`](../../reference/crates/wo-event/src/eventfd.rs) (66 LOC) |
| `timerfd.rs` | `TimerFd { fd }` — oneshot + periodic timers as fds for the loop | [`reference/crates/wo-event/src/timerfd.rs`](../../reference/crates/wo-event/src/timerfd.rs) (91 LOC) |
| `signalfd.rs` | `SignalFd { fd }` — SIGINT / SIGTERM / SIGHUP delivered as fd reads for graceful shutdown without a tokio signal handler | [`reference/crates/wo-event/src/signalfd.rs`](../../reference/crates/wo-event/src/signalfd.rs) (62 LOC) |
| `mod.rs` | Re-exports `EventLoop`, `Event`, `Interest`, `Token`, `EventFd`, `TimerFd`, `SignalFd` | [`.dev/reference/crates/wo-event/src/lib.rs`](../../.dev/reference/crates/wo-event/src/lib.rs) (9 LOC) |
| `netpoll_epoll.rs` | `EventLoop { fd, events }` — `new()`, `register(raw_fd, interest, token)`, `wait_once(timeout) -> &[Event]`, `deregister(raw_fd)` | [`.dev/reference/crates/wo-event/src/epoll.rs`](../../.dev/reference/crates/wo-event/src/epoll.rs) (183 LOC); [`.dev/reference/go/src/runtime/netpoll_epoll.go`](../../.dev/reference/go/src/runtime/netpoll_epoll.go) for idiom |
| `eventfd.rs` | `EventFd { fd }` — counter semaphore for cross-fd wake-up (subscription dispatch, shutdown signal) | [`.dev/reference/crates/wo-event/src/eventfd.rs`](../../.dev/reference/crates/wo-event/src/eventfd.rs) (66 LOC) |
| `timerfd.rs` | `TimerFd { fd }` — oneshot + periodic timers as fds for the loop | [`.dev/reference/crates/wo-event/src/timerfd.rs`](../../.dev/reference/crates/wo-event/src/timerfd.rs) (91 LOC) |
| `signalfd.rs` | `SignalFd { fd }` — SIGINT / SIGTERM / SIGHUP delivered as fd reads for graceful shutdown without a tokio signal handler | [`.dev/reference/crates/wo-event/src/signalfd.rs`](../../.dev/reference/crates/wo-event/src/signalfd.rs) (62 LOC) |
Total: ~410 LOC lifted and adapted. The v1 code already compiles standalone in `reference/crates/wo-event/` and has unit tests; the port is near-verbatim plus namespace cleanups.
Total: ~410 LOC lifted and adapted. The v1 code already compiles standalone in `.dev/reference/crates/wo-event/` and has unit tests; the port is near-verbatim plus namespace cleanups.
### Why `runtime/` not `event/`
Go's equivalent code lives at [`reference/go/src/runtime/netpoll_epoll.go`](../../reference/go/src/runtime/netpoll_epoll.go) alongside siblings like `netpoll_kqueue.go` (macOS/BSD), `netpoll_io_uring.go` (if/when Go adds it), and the shared `netpoll.go` interface. The directory name "runtime" signals that this is the layer beneath user code — scheduler / netpoll / syscall shims — and the filename prefix `netpoll_<flavour>` makes each implementation alternative visible at a glance. Adopting the same convention in writeonce makes porting ideas bidirectional: a reader who knows Go's layout can find the writeonce equivalent by trimming the `.go` extension and swapping it for `.rs`. When Phase 3's io_uring arrives it'll land as `netpoll_io_uring.rs` next to the epoll one; a cross-platform stub would be `netpoll.rs`. Module boundary and naming both match. See [`docs/plan/assembly/00-overview.md`](./assembly/00-overview.md) for why we stop short of mirroring Go's assembly conventions.
Go's equivalent code lives at [`.dev/reference/go/src/runtime/netpoll_epoll.go`](../../.dev/reference/go/src/runtime/netpoll_epoll.go) alongside siblings like `netpoll_kqueue.go` (macOS/BSD), `netpoll_io_uring.go` (if/when Go adds it), and the shared `netpoll.go` interface. The directory name "runtime" signals that this is the layer beneath user code — scheduler / netpoll / syscall shims — and the filename prefix `netpoll_<flavour>` makes each implementation alternative visible at a glance. Adopting the same convention in writeonce makes porting ideas bidirectional: a reader who knows Go's layout can find the writeonce equivalent by trimming the `.go` extension and swapping it for `.rs`. When Phase 3's io_uring arrives it'll land as `netpoll_io_uring.rs` next to the epoll one; a cross-platform stub would be `netpoll.rs`. Module boundary and naming both match. See [`docs/plan/assembly/00-overview.md`](./assembly/00-overview.md) for why we stop short of mirroring Go's assembly conventions.
### `Cargo.toml` change
@ -74,7 +74,7 @@ for event in loop_.wait_once(Some(Duration::from_millis(100)))? {
- `read()` on the eventfd returns `1`.
3. A second unit test validates `TimerFd::oneshot(100ms)` fires within a `wait_once(500ms)` window.
4. All 14 existing `rt` tests still pass. `cargo run --bin wo -- run docs/examples/blog` still serves (tokio path unchanged).
5. `cd reference/crates && cargo build && cargo test` still green (nothing touched).
5. `cd .dev/reference/crates && cargo build && cargo test` still green (nothing touched).
## Non-scope
@ -91,7 +91,7 @@ cargo build
cargo test --lib runtime # new tests in crates/rt/src/runtime/
cargo test --lib # all 14 existing + new epoll/eventfd/timerfd tests green
cargo run --bin wo -- run docs/examples/blog # axum path unchanged, still serves
cd reference/crates && cargo build && cargo test # v1 untouched
cd .dev/reference/crates && cargo build && cargo test # v1 untouched
```
## After this phase

View file

@ -9,10 +9,10 @@ A non-blocking HTTP/1.1 server module that accepts connections, parses requests,
## Design decisions (locked)
1. **HTTP/1.1 only, keep-alive supported.** HTTP/2 and HTTP/3 are not on the roadmap for Stage 2 — they need ALPN / TLS support we don't have a plan for yet. HTTP/1.1 covers every endpoint the blog + ecommerce samples exercise.
2. **Per-connection state machine.** Each accepted socket fd is registered on the event loop with its own `Connection { state: Reading | Writing | Idle, parser, pending_response }`. Edge-triggered `EPOLLIN`/`EPOLLOUT` drive state transitions. Matches [v1 wo-http](../../reference/crates/wo-http/src/connection.rs)'s model verbatim.
2. **Per-connection state machine.** Each accepted socket fd is registered on the event loop with its own `Connection { state: Reading | Writing | Idle, parser, pending_response }`. Edge-triggered `EPOLLIN`/`EPOLLOUT` drive state transitions. Matches [v1 wo-http](../../.dev/reference/crates/wo-http/src/connection.rs)'s model verbatim.
3. **Router is pattern-matched at registration.** `Router::new().route("/api/articles/:id", Method::GET, handler)` resolves to a trie at boot. Per-request dispatch is a single trie walk — no axum-style type-erased layers.
4. **Handlers are `fn(&Request, &Engine) -> Response`.** Synchronous. The single-threaded event loop means a handler blocking is a bug; each handler must be a pure transformation over engine state.
5. **Module, not crate (yet).** Lives at `crates/rt/src/http/` with the same "extract when a second consumer shows up" rule as phase 02. The eventual home is the empty [`crates/http/`](../../crates/http/) sibling — but not in this phase. Paired with [phase 02's `crates/rt/src/runtime/`](./02-event-loop-epoll.md) (Go-style naming — `netpoll_epoll.rs`, `eventfd.rs`, …) which this module depends on for the `EventLoop` + raw syscall shims. Go's `src/net/http/` and `src/runtime/` split is the layout precedent; see [`reference/go/src/net/http/`](../../reference/go/src/net/http/).
5. **Module, not crate (yet).** Lives at `crates/rt/src/http/` with the same "extract when a second consumer shows up" rule as phase 02. The eventual home is the empty [`crates/http/`](../../crates/http/) sibling — but not in this phase. Paired with [phase 02's `crates/rt/src/runtime/`](./02-event-loop-epoll.md) (Go-style naming — `netpoll_epoll.rs`, `eventfd.rs`, …) which this module depends on for the `EventLoop` + raw syscall shims. Go's `src/net/http/` and `src/runtime/` split is the layout precedent; see [`.dev/reference/go/src/net/http/`](../../.dev/reference/go/src/net/http/).
## Scope
@ -20,12 +20,12 @@ A non-blocking HTTP/1.1 server module that accepts connections, parses requests,
| File | Responsibility | Port source |
| --- | --- | --- |
| `mod.rs` | Re-exports `Listener`, `Connection`, `Request`, `Response`, `Router`, `Method`, `Status` | [`reference/crates/wo-http/src/lib.rs`](../../reference/crates/wo-http/src/lib.rs) (4 LOC) |
| `listener.rs` | `Listener { fd }` wrapping `socket + bind + listen + accept4(SOCK_NONBLOCK \| SOCK_CLOEXEC)`; integrates with `EventLoop` | [`reference/crates/wo-http/src/listener.rs`](../../reference/crates/wo-http/src/listener.rs) (202 LOC) |
| `connection.rs` | Per-fd state machine: drain request bytes, parse, dispatch, drain response bytes, keep-alive or close | [`reference/crates/wo-http/src/connection.rs`](../../reference/crates/wo-http/src/connection.rs) (202 LOC) |
| `request.rs` | Incremental HTTP/1.1 request parser: request line, headers, optional body. `Content-Length` only (no chunked request bodies in Stage 2 — they don't appear in the samples) | [`reference/crates/wo-http/src/request.rs`](../../reference/crates/wo-http/src/request.rs) (158 LOC) |
| `response.rs` | Response builder + writer: status line, headers, body (fixed or chunked) | [`reference/crates/wo-http/src/response.rs`](../../reference/crates/wo-http/src/response.rs) (110 LOC) |
| `route.rs` | Trie-based router: static paths + `:param` segments. `Router::route(method, path, handler) -> Router` | [`reference/crates/wo-route/src/router.rs`](../../reference/crates/wo-route/src/router.rs) (127 LOC) + [`pattern.rs`](../../reference/crates/wo-route/src/pattern.rs) (146 LOC) |
| `mod.rs` | Re-exports `Listener`, `Connection`, `Request`, `Response`, `Router`, `Method`, `Status` | [`.dev/reference/crates/wo-http/src/lib.rs`](../../.dev/reference/crates/wo-http/src/lib.rs) (4 LOC) |
| `listener.rs` | `Listener { fd }` wrapping `socket + bind + listen + accept4(SOCK_NONBLOCK \| SOCK_CLOEXEC)`; integrates with `EventLoop` | [`.dev/reference/crates/wo-http/src/listener.rs`](../../.dev/reference/crates/wo-http/src/listener.rs) (202 LOC) |
| `connection.rs` | Per-fd state machine: drain request bytes, parse, dispatch, drain response bytes, keep-alive or close | [`.dev/reference/crates/wo-http/src/connection.rs`](../../.dev/reference/crates/wo-http/src/connection.rs) (202 LOC) |
| `request.rs` | Incremental HTTP/1.1 request parser: request line, headers, optional body. `Content-Length` only (no chunked request bodies in Stage 2 — they don't appear in the samples) | [`.dev/reference/crates/wo-http/src/request.rs`](../../.dev/reference/crates/wo-http/src/request.rs) (158 LOC) |
| `response.rs` | Response builder + writer: status line, headers, body (fixed or chunked) | [`.dev/reference/crates/wo-http/src/response.rs`](../../.dev/reference/crates/wo-http/src/response.rs) (110 LOC) |
| `route.rs` | Trie-based router: static paths + `:param` segments. `Router::route(method, path, handler) -> Router` | [`.dev/reference/crates/wo-route/src/router.rs`](../../.dev/reference/crates/wo-route/src/router.rs) (127 LOC) + [`pattern.rs`](../../.dev/reference/crates/wo-route/src/pattern.rs) (146 LOC) |
Total: ~949 LOC ported. Most of it is mechanical adaptation from v1; the namespace + the `Interest` enum change from phase 02 are the only non-trivial edits.
@ -76,7 +76,7 @@ loop {
2. An integration test (also in `crates/rt/tests/http_smoke.rs` or similar) spawns the binary, sends three `curl` equivalents using `std::net::TcpStream`, validates status codes and bodies.
3. All 14 existing `rt` tests still pass.
4. `wo run docs/examples/blog` unchanged — axum path still drives the real CLI.
5. `cargo build` at root; `cd reference/crates && cargo build` still green.
5. `cargo build` at root; `cd .dev/reference/crates && cargo build` still green.
## Non-scope
@ -97,4 +97,4 @@ cargo run --bin wo -- run docs/examples/blog # axum path unchanged
## After this phase
Phase 04 takes the same in-memory `Engine` that the axum router serves and points the phase-03 router at it instead. Removing `tokio`, `axum`, `tower` is a consequence; the behaviour visible to `reference/rest/blog.rest` does not change.
Phase 04 takes the same in-memory `Engine` that the axum router serves and points the phase-03 router at it instead. Removing `tokio`, `axum`, `tower` is a consequence; the behaviour visible to `.dev/reference/rest/blog.rest` does not change.

View file

@ -4,7 +4,7 @@
## Goal
Flip the `wo` binary off the tokio + axum stack and onto the phase-02 event loop + phase-03 HTTP server. Delete three dependencies from `crates/rt/Cargo.toml`. REST behaviour visible to [`reference/rest/blog.rest`](../../reference/rest/blog.rest) does not change — same status codes, same response bodies, same endpoint paths.
Flip the `wo` binary off the tokio + axum stack and onto the phase-02 event loop + phase-03 HTTP server. Delete three dependencies from `crates/rt/Cargo.toml`. REST behaviour visible to [`.dev/reference/rest/blog.rest`](../../.dev/reference/rest/blog.rest) does not change — same status codes, same response bodies, same endpoint paths.
This is the first phase where the dependency count goes *down*. Phases 02 and 03 were additive; this one is the switch.
@ -77,13 +77,13 @@ Twelve handlers total — one pair per `{list, get, create, update, delete}` ×
1. **`cargo build`** at root — compiles with four deps (not seven).
2. **`cargo test --lib`** — all 14 existing `rt` unit tests still pass. A new test in `src/server.rs` exercises the router build from a compiled catalog (no HTTP, just static registration).
3. **End-to-end REST smoke — the 20-assertion battery from [`reference/rest/blog.rest`](../../reference/rest/blog.rest)** must pass byte-identical to Stage 2 today. Script:
3. **End-to-end REST smoke — the 20-assertion battery from [`.dev/reference/rest/blog.rest`](../../.dev/reference/rest/blog.rest)** must pass byte-identical to Stage 2 today. Script:
```bash
WO_LISTEN=127.0.0.1:8765 cargo run --bin wo -- run docs/examples/blog &
# ... curl each block, check expected status
```
4. **Graceful shutdown.** SIGINT on the process exits cleanly (no panic, no orphan fds). Validate with `strace -f -e signalfd4,close` on shutdown.
5. **`cd reference/crates && cargo build && cargo test`** still green.
5. **`cd .dev/reference/crates && cargo build && cargo test`** still green.
6. **Dep audit.** `cargo tree -p rt --depth 1` shows `libc` as the only non-transitive external dep beyond `anyhow`, `serde`, `serde_json`.
## Non-scope
@ -107,11 +107,11 @@ cargo test --lib # 14 + any new server.rs tes
WO_LISTEN=127.0.0.1:8765 cargo run --bin wo -- run docs/examples/blog &
PID=$!
sleep 2
# every block in reference/rest/blog.rest, via curl, checking %{http_code}
# every block in .dev/reference/rest/blog.rest, via curl, checking %{http_code}
# (copy-paste the 20-assertion script from the Stage 2 turn that verified blog.rest)
kill $PID
cd reference/crates && cargo build && cargo test # v1 untouched
cd .dev/reference/crates && cargo build && cargo test # v1 untouched
```
## After this phase