summaryrefslogtreecommitdiff
path: root/docs/acme-fs.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/acme-fs.md')
-rw-r--r--docs/acme-fs.md309
1 files changed, 192 insertions, 117 deletions
diff --git a/docs/acme-fs.md b/docs/acme-fs.md
index ede3ed17..15f45c4f 100644
--- a/docs/acme-fs.md
+++ b/docs/acme-fs.md
@@ -1,15 +1,20 @@
# The control filesystem — pardes against acme
`pardes --fs` serves plan9 [acme(4)](https://man.cat-v.org/plan_9/4/acme)'s control
-filesystem over Linux FUSE: a directory per pane holding `addr`, `body`, `ctl`,
-`data`, `event`, `tag`, ..., plus `index`, `new/` and `cons` at the root.
-A program that opens those files IS an editor extension — no plugin API, no
-embedded interpreter, no rebuild. `examples/acmefs/` has four of them.
+filesystem over Linux FUSE: a directory per pane, named by the pane's serial and
+holding `addr`, `body`, `ctl`, `data`, `errors`, `event`, `rdsel`, `tag`, `wrsel`
+and `xdata`, plus `index`, `cons` and `new/` at the root (`PaneFile` and
+`TopFile` in `src/acmefs.zig`). A program that opens those files IS an editor
+extension — no plugin API, no embedded interpreter, no rebuild.
+`examples/acmefs/` has four of them: `clock.py`, `eventlog`, `life.py` and
+`pardesctl`.
This document is the **comparison report**: what acme does, what pardes does,
why they differ, and which one is simpler. acme's C is at
-`/home/goblin/05-genizah/principia-softwarica/editors/acme`; every claim below
-cites `file:line` on both sides. Where acme is better, it says so.
+`/home/goblin/05-genizah/principia-softwarica/editors/acme` — the
+principia-softwarica tree and not plan9port, which matters because the two
+differ in what they serve; every `file:line` below is that checkout's. Every
+claim cites both sides. Where acme is better, it says so.
| | acme | pardes |
|---|---|---|
@@ -20,19 +25,23 @@ cites `file:line` on both sides. Where acme is better, it says so.
| node ids | `QID/WIN/FILE` shift macros | `packed struct(u64) { file: u4, serial: u60 }` |
| errors | 9P error strings (`Ebadctl`, `Edel`, ...) | errno |
| event queue | `realloc` per record, unbounded | length-framed `ArrayList`, capped, drop-oldest |
-| `ctl` write | applies the good prefix, then reports | validate-all then apply-all |
+| `ctl` write | applies the good prefix, then reports 0 consumed | validate-all then apply-all |
## 1. The service: threads-and-channels against one transaction
acme runs a dedicated process for the wire (`proccreate(fsysproc)`, `fsys.c:136`)
whose loop reads a 9P message, borrows an `Xfid` from a pool, and dispatches
-through a function table (`fsys.c:152-201`). Directory reads and stats are
-answered inline; anything that touches a window is handed to that `Xfid`'s own
-thread — `sendp(x->c, xfidread)` (`fsys.c:355`) — and `xfidallocthread` creates
-**one thread per `Xfid`** on first use (`acme.c:744`), each parked in
-`for(;;){ f = recvp(x->c); (*f)(x); ... }` (`xfid.c:64-74`). Serialisation is by
-`QLock`: one on the row (`dat.h:329`), one per window plus an owner byte
-(`wind.c:135`), one on the mount table (`fsys.c:270`).
+through the `fcall[x->type]` function table (`fsysproc`, `fsys.c:140-193`; the
+table itself at `fsys.c:41`, the dispatch at `fsys.c:190`). Directory reads and
+stats are answered inline; anything that touches a window is handed to that
+`Xfid`'s own thread —
+`sendp(x->c, xfidread)` (`fsys.c:660`) — and `xfidallocthread` creates **one
+thread per `Xfid`** on first use (`acme.c:718-744`), each parked in
+`for(;;){ f = recvp(x->c); (*f)(x); ... }` (`xfidctl`, `xfid.c:42-55`).
+Serialisation is by `QLock`: one on the row (`dat.h:313`), one per window plus an
+owner byte (`dat.h:228` and `dat.h:250`, taken together in `winlock1`,
+`wind.c:131-136`), one on the mount table (`struct Mnt`, `fsys.c:96`, taken at
+`fsys.c:201`).
pardes has none of that. A request is a value, an answer is a value, and the
whole filesystem is one function:
@@ -52,37 +61,73 @@ already followed.
**Simpler: pardes, by a lot.** No channels, no locks, no thread per request, no
fid bookkeeping, and the semantics are unit-testable with no scheduler and no
-FUSE anywhere near them (`src/acmefs.zig` has 28 such tests).
+FUSE anywhere near them (`src/acmefs.zig` has 21 such tests).
**What acme buys, honestly:** isolation. Its request threads mean a slow read
cannot stall the editor. In pardes `handle` runs on the loop thread, so a
pathological request — reading the body of a 100 MB file, a `ctl get` that
re-reads a huge file from disk — is a frame the user waits for. The measured
-numbers say this is theoretical rather than practical (`zig build fs-bench`:
-20-52 ns per request, and a 1 MiB `body` read costs the same 25 ns as a 4 KiB
-one because it is zero-copy), but it is a real property of the design and the
-reason acme's complexity exists.
+numbers say this is theoretical rather than practical, but it is a real property
+of the design and the reason acme's complexity exists.
+
+Every measurement in this document is one run of `zig build fs-bench
+-Doptimize=ReleaseFast` on an i7-11700. Each row is `handle` called directly —
+no FUSE, no thread — and its figure is the MEAN over the row's reps: 100 000,
+except 10 000 for the 1 MiB read, 200 for the append and 2000 per keystroke row.
+
+| row | per request | allocations, whole row |
+|---|---|---|
+| `getattr` on a 1 MiB body | 19 ns | 0 |
+| `lookup ctl` | 39 ns | 0 |
+| `read body`, 4 KiB | 25 ns | 0 |
+| `read body`, 1 MiB | 25 ns | 0 |
+| `read ctl` | 404 ns | 0 |
+| `read index` | 631 ns | 0 |
+| `readdir` of the root | 39 ns | 0 |
+| `read event` on an empty queue (`Status.again`) | 21 ns | 0 |
+| `write body`, 1 KiB appended to a body growing from 1 MiB | 3.36 ms | 600 |
+
+Two things in that table are the point of having it. The rows that FORMAT —
+`ctl` and `index`, which `bufPrint` a line of `%11d` fields — cost about twenty
+times a row that hands back a slice, and are still well under a microsecond. And
+a 1 MiB `body` read costs exactly what a 4 KiB one does, because it is
+zero-copy: `Payload.region` is a window onto the pane's live text. Every read
+row allocates nothing at all, which is a property the benchmark exists to check
+rather than a pleasing number — a non-zero count there would mean a read had
+stopped answering out of the live text or out of the staging buffer that is
+cleared and never freed. The one row that allocates is the append, at 600
+allocations across 200 writes, and that is the core's whole-body swap plus its
+undo snapshot rather than anything this filesystem does.
## 2. Blocking reads: a parked thread against a returned value
-acme's `event` read blocks: `xfideventread` (`xfid.c:553-582`) stores its `Xfid`
-in `w->eventx`, unlocks the window and sleeps on a channel; `winevent`
-(`wind.c:543-569`) appends the record and wakes it; `windelete`
-(`wind.c:217-225`) wakes it with no data so it can answer "window shut down";
-and `xfidflush` (`xfid.c:77-102`) exists solely to cancel a parked reader,
+acme's `event` read blocks: `xfideventread` (`xfid.c:994-1025`) stores its `Xfid`
+in `w->eventx`, unlocks the window and sleeps on a channel (`xfid.c:1008-1010`);
+`winevent` (`wind.c:543-569`) appends the record and wakes it; `windelete`
+(`wind.c:217-225`) wakes it with no data so it can answer "window shut down"
+(`xfid.c:1005`); and `xfidflush` (`xfid.c:58-87`) exists solely to cancel a
+parked reader — it walks every column and every window looking for the tag,
because a blocked 9P read cannot otherwise be interrupted.
pardes returns `Status.again` — "nothing consumed, ask me again" — and that is
the entire blocking primitive. The core keeps no waiter, no channel, no cancel
-path. The transport parks the kernel's request (32 slots, `src/fuse.zig`) and
-re-submits it once per frame; a `FUSE_INTERRUPT` answers the original with
-`-EINTR`, which is what keeps a SIGKILLed reader from sitting in uninterruptible
-sleep forever (verified live: 40 concurrent blocked readers, all reaped).
+path. The transport parks the kernel's request (`max_slots = 32`, `src/fuse.zig`)
+and re-offers it once per frame, oldest first, with a per-round flag so a
+permanently blocked reader cannot starve the others (`retry`). A `FUSE_INTERRUPT`
+answers the original with `-EINTR`, which is what keeps a SIGKILLed reader out of
+permanent uninterruptible sleep: after a fatal signal `fuse_dev`'s final
+`wait_event` is not killable, so the process survives its own kill until the
+server replies. Three tests pin that mechanism — "again holds the request, retry
+offers it back once per round", "interrupt answers the original with EINTR and
+drops it", and "a full table answers EAGAIN and keeps the descriptor flowing",
+whose comment records that the tempting alternative, gating the read on a free
+slot, wedges a real mount: the INTERRUPT that would free a slot is never read
+either.
Two deliberate differences:
- acme hands back up to `count` bytes and keeps the remainder, so a small read
- can split a record (`xfid.c:378-380`). pardes refuses a read smaller than one
+ can split a record (`xfid.c:1015-1017`). pardes refuses a read smaller than one
record with `EINVAL`: half a record is unparseable and silently desynchronises
a client.
- acme's parked thread survives the client's death (it stays blocked until the
@@ -95,10 +140,11 @@ are bounded by the park table because each one is a held kernel request.
## 3. Node identity
acme packs a qid with macros: `QID(w,q) ((w<<8)|(q))`, `WIN(q)`, `FILE(q)`
-(`dat.h:434-436`) — 24 bits of window id, 8 of file id, no validation. A window
+(`dat.h:463-465`) — 24 bits of window id, 8 of file id, no validation. A window
that dies while a client holds a file open is detected structurally, by every
-handler remembering to check `if (w->col == nil) respond(..., Edel)`
-(`xfid.c:414-421` and a dozen more).
+handler remembering to check `if(w->col == nil) respond(..., Edel)`
+(`xfid.c:422-425` in `xfidwrite`, and a dozen more; the string is at
+`xfid.c:19`).
pardes uses the type system:
@@ -120,12 +166,14 @@ methods, and `Edel`-by-convention becomes `ENOENT` by construction.
## 4. Addressing: runes against bytes
-acme's document is `Rune*`, so every read converts. `xfidutfread` keeps a
-byte↔rune cache per window and, when it misses, scans from the beginning —
-carrying the comment `/* BUG: stupid code: scan from beginning */`
-(`xfid.c:855`). The address language lives in `addr.c`: `address()`, `number()`,
-`regexp()`, with failure reported through two out-parameters and patterns grown
-one rune at a time.
+acme's document is `Rune*`, so every read converts. `xfidutfread`
+(`xfid.c:875`) keeps a byte-to-rune cache per window — `w->utflastqid`,
+`utflastboff`, `utflastq` (`xfid.c:891-897`) — and, when it misses, scans from
+the beginning, carrying the comment `/* BUG: stupid code: scan from beginning */`
+(`xfid.c:895`). The address language lives in `addr.c`: `number()`
+(`addr.c:52`), `regexp()` (`addr.c:119`), `address()` (`addr.c:150`), with
+failure reported through out-parameters (`int *evalp`, `uint *qp`) and patterns
+grown one rune at a time.
pardes is byte-addressed end to end (selections, look spots, LSP offsets), so
**every offset in this filesystem is a byte offset**, clamped to grapheme
@@ -142,39 +190,48 @@ promises; ours promises bytes.
## 5. Events, and the inversion that makes this a plugin API
-The record is the same on both sides, byte for byte: origin char, type char,
-four blank-separated decimals, the text, a newline
-(`wind.c:543-569` + `text.c:377-382`; `formatRecord` in `src/acmefs.zig`).
+The record is the same on both sides, byte for byte: origin char, type char, four
+blank-separated decimals, the text, a newline. acme builds it in two halves —
+`text.c:380` formats `"%c%d %d 0 %d %.*S\n"` and `winevent` prepends the owner
+byte at `wind.c:561` — and pardes builds it in one, `formatRecord`
+(`src/acmefs.zig`).
The rule that matters is the inversion: **while a script holds a pane's `event`
file open, buttons 2 and 3 in that pane belong to the script.** acme spells it
-`if (!external && t->w->nopen[QWevent] > 0) { winevent(...); return; }`
+`if(!external && t->w!=nil && t->w->nopen[QWevent]>0){ winevent(...); return; }`
(`exec.c:150`, `look.c:35`); pardes spells it as the return value of
`noteAction` — "true means the core must not perform it" — checked in
`dispatchPointerBuiltin`. That is how `examples/acmefs/life.py` puts
`Step Run Stop Clear Random` in a tag pardes has never heard of and makes them
-work. `test/snapshots/acmefs-event.snap` is the A/B proof: with a reader
-attached, a middle click on the word `Newcol` changes nothing on screen and
-delivers `MX0 6 1 6 Newcol`; with the reader gone, the same click opens a column.
+work. `test/snapshots/acmefs-event.snap` drives the A/B proof and
+`acmefs-event.golden` records it: with a reader attached, a middle click on the
+word `Newcol` changes nothing on screen and delivers `MX0 6 1 6 Newcol`
+(`acmefs-event.golden:9`); with the reader gone, the same click opens a column.
Differences worth knowing:
-- **Keyboard equivalents are not suppressed** (acme has none to suppress). A
- scripted pane stays editable, and a script that dies mid-run cannot leave you
- unable to execute anything in it.
- **Write-back takes four fields only** — `origin type q0 q1\n`, type in `xXlL`
- — exactly as `xfideventwrite` demands (`xfid.c:791-830`, which rejects
- anything else with `Ebadevent`). There is no text field, so a client that
- wants to run text not already on screen appends it to the tag and execs that
- range; `examples/acmefs/pardesctl exec` does precisely this, and it is how
+ — exactly as `xfideventwrite` demands (`xfid.c:791-872`: it reads the origin
+ byte, the type char, two `strtoul`s and a mandatory newline, and its `switch`
+ takes `x`, `X`, `l`, `L` and sends everything else to `Rescue`, which is
+ `err = Ebadevent`). There is no text field, so a client that wants to run text
+ not already on screen appends it to the tag and execs that range;
+ `examples/acmefs/pardesctl exec` does precisely this — `printf ' %s' "$text"
+ >>$d/tag` and then `printf 'Mx%d %d\n' "$q0" "$q1" >>$d/event` — and it is how
acme clients have always done it.
- pardes validates a whole batch of records before performing any of them; acme
- performs them as it parses.
-- acme records the origin byte the window's lock owner claimed; pardes sets
- `State.origin` once per update from the event kind (`K` keyboard, `M` mouse,
- `E`/`F` a filesystem write). Same fidelity for every real case, one field
- instead of a lock argument. **acme is arguably better here**: its owner byte is
- per-record provenance, and a writer can re-attribute an action.
+ performs them as it parses, which `writeEvent`'s own comment in
+ `src/acmefs.zig` names as the reason: a malformed batch is otherwise
+ half-applied and unrepeatable.
+- acme records the origin byte the RECORD claimed — `w->owner = *p++;` with
+ `/* disgusting */` beside it (`xfid.c:812`) — and stamps it onto every record
+ it later produces (`wind.c:561`). pardes sets `State.origin` once per update
+ from the event kind (`K` keyboard, `M` mouse, `E` a write to body or tag
+ through this filesystem, `F` an action through one of its other files),
+ parses the character on the way in and drops it. **acme is arguably better
+ here**: its owner byte is per-record provenance and a writer can re-attribute
+ an action. Against that, a record saying where it came from is worth nothing
+ when the sender picks the answer, which is why pardes does not read it.
## 6. Reporting edits: known ranges against a diff
@@ -184,96 +241,110 @@ range: `textinsert` emits `I` with `q0, q0+n` and the inserted runes
pardes has no such pair — every edit lands in one place as a whole new buffer
(`file_pane.setContent`) — so the range is recovered by diffing there:
-`acmefs.diffSpan` skips the common prefix and suffix through vectorised
-compares, and `noteReplace` emits the deletion then the insertion, the same two
-records in the same order. It is behind `p.fs.listeners != 0`, so an editor
-nobody is scripting pays one branch. Measured cost of a keystroke with a
-listener attached on a 32 KiB body: **+1.6%** (`zig build fs-bench`).
+`acmefs.diffSpan` skips the common prefix and suffix in 64-byte chunks through
+`std.mem.eql`, which lowers to vectorised compares, and `noteReplace` emits the
+deletion then the insertion, the same two records in the same order. The
+vectorising is not premature: its own comment records that the byte-at-a-time
+loop it replaced cost 2.4x per keystroke on a 40 KB body. The whole path is
+behind `p.fs.scripted(id)` — that pane's reader count, not the session-wide
+`listeners` total — so an editor nobody is scripting pays one branch. Measured
+cost of a keystroke on a 32 KiB body: 32.9 µs with no listener against 34.1 µs
+with one, **+3.9%**.
**acme is better here in principle** — a known range beats a scan — and it pays
for it by routing every mutation through a pair of functions that carry ranges
everywhere. pardes's single funnel is worth more than the scan costs.
Undo grouping is acme's `mark`/`nomark` on both sides: acme bumps a global
-sequence number and merges an `elog` of edits (`elog.c`, `xfid.c:501-504`);
-pardes suppresses the per-write `pushUndo` snapshot. Same verb, same effect on
-the user's `u`, much less machinery — and the reason it matters is measurable:
-an append to a 1 MiB body costs 25 ms because of that snapshot, so a script
-writing a batch should say `nomark` first.
+sequence number and merges an `elog` of edits (`elog.c`; `if(w->nomark == FALSE)
+{ seq++; filemark(t->file); }` at `xfid.c:501-504`); pardes suppresses the
+per-write `pushUndo` snapshot. Same verb, same effect on the user's `u`, much
+less machinery — and the reason it matters is measurable: the benchmark's append
+row is 3.36 ms per 1 KiB write against a body around a megabyte, almost all of
+it the whole-body swap and that snapshot, so a script writing a batch should say
+`nomark` first.
## 7. `ctl`
-Both print the same five `%11d` fields (`winctlprint`, `wind.c:532-537`), and
-pardes adds acme's three extras with the one honest substitution: width and tab
-in **cells**, because pardes is a character grid where acme has pixels.
+Both print the same five `%11d` fields — id, tag length, body length, isdir,
+dirty (`winctlprint`, `wind.c:534-535`) — and pardes adds acme's three extras
+(`wind.c:537-538`) with the one honest substitution: width and tab in **cells**,
+because pardes is a character grid where acme has pixels (`Dx(w->body.r)`).
The verb parsers differ in two ways that matter:
- acme matches verbs by **prefix** with `strncmp` and advances by the matched
- length, so the table order is load-bearing (`delete` before `del`, `nomark`
- before `mark`). pardes matches a whole token through
- `std.meta.stringToEnum`, which makes that class of bug unrepresentable.
-- acme applies verbs as it parses and reports the byte count it consumed, so a
- bad verb leaves the good prefix applied (`xfid.c:778-781`). pardes validates
- every verb first and then applies them, because a short count on a Linux
- `write(2)` is not read by anybody as "the rest failed". **acme is more
- expressive here** — a 9P client can stream verbs and learn where it stopped —
- and pardes trades that for atomicity.
+ length (`xfid.c:602-767`), so the table order is load-bearing: `delete`
+ (`xfid.c:697`) must precede `del` (`xfid.c:701`), `nomark` (`xfid.c:738`)
+ `mark` (`xfid.c:742`), `nomenu` `menu`, `noscroll` `scroll`. pardes matches a
+ whole token through `std.meta.stringToEnum`, which makes that class of bug
+ unrepresentable.
+- acme applies verbs as it parses, so a bad verb leaves the good prefix applied
+ — the mutations already made stand — and then reports **zero** bytes consumed:
+ `err = Ebadctl` (`xfid.c:769`) falls through to `if(err) n = 0; fc.count = n;`
+ (`xfid.c:780-782`), and `xfideventwrite` repeats it verbatim at
+ `xfid.c:863-865`. So the client learns that it failed but not where, and the
+ editor has already been half-changed. pardes validates every verb first and
+ then applies them: a short count on a Linux `write(2)` is not read by anybody
+ as "the rest failed", and a half-applied batch is unrepeatable. **This is not
+ a trade**; the atomic answer is simply the better one.
-Verbs pardes cannot honour are refused loudly with a reason
-(`dump`, `dumpdir`, `font`, `menu`, `nomenu`, `lock`, `unlock`) rather than
-silently accepted.
+Verbs pardes cannot honour are refused loudly with a reason —
+`refused_verbs` is `dump`, `dumpdir`, `font`, `lock`, `menu`, `nomenu`,
+`unlock` — rather than silently accepted.
## 8. Errors
-acme answers with strings: `Ebadctl` "ill-formed control message", `Ebadaddr`
-"bad address syntax", `Eaddr` "address out of range", `Edel` "deleted window"
-(`xfid.c:20-30`), handed through `respond(x, &fc, err)`. pardes answers with an
-errno, because that is the only channel FUSE has: the client would never see the
-string. Two acme errors that differ in wording collapse to `EINVAL` here, which
-is a real loss of diagnostics — the message row and `PARDES_LOG` carry the
-detail instead.
+acme answers with strings: `Edel` "deleted window", `Ebadctl` "ill-formed control
+message", `Ebadaddr` "bad address syntax", `Eaddr` "address out of range",
+`Ebadevent` "bad event syntax" (`xfid.c:19-24`), handed through
+`respond(x, &fc, err)`. pardes answers with an errno, because that is the only
+channel FUSE has: the client would never see the string. Two acme errors that
+differ in wording collapse to `EINVAL` here, which is a real loss of
+diagnostics — the message row and `PARDES_LOG` carry the detail instead.
## 9. Memory and bounds
acme grows `w->events` with `realloc` and never caps it (`wind.c:560`), and
-re-allocates the remainder on every partial read (`xfid.c:578-580`). A client
-that stops reading grows that buffer until `emalloc` fails and acme aborts.
+re-allocates the remainder on every partial read — `w->events =
+estrdup(w->events+n); free(b);` (`xfid.c:1022-1023`). A client that stops reading
+grows that buffer until `emalloc` fails and acme aborts.
pardes's queue is length-framed (records contain newlines, so a length is the
-only way to hand one back whole), capped at 64 KiB per pane, and drops the
-oldest record when full: an editor must not stall or grow without bound because
-a script stopped reading, and a reader that far behind can re-read `body` and
-resynchronise. Formatted answers go into one staging buffer that is cleared and
-never freed, which is why every read in the benchmark reports **zero
-allocations**. **acme's unbounded buffer is a flaw, not a feature.**
+only way to hand one back whole), capped at 64 KiB per pane (`queue_cap`), and
+drops the oldest record when full: an editor must not stall or grow without bound
+because a script stopped reading, and a reader that far behind can re-read `body`
+and resynchronise. Formatted answers go into one staging buffer that is cleared
+and never freed, which is why every read in the benchmark reports zero
+allocations. **acme's unbounded buffer is a flaw, not a feature.**
## 10. C-isms Zig removed
Ranked by what they cost when they go wrong:
1. **Threads and channels standing in for a state machine** — one thread per
- in-flight request (`acme.c:744`, `xfid.c:64-74`) → a `Status.again` return
+ in-flight request (`acme.c:718-744`, `xfid.c:42-55`) → a `Status.again` return
value and a park table in the transport.
-2. **Macro-packed qids** — `QID/WIN/FILE` (`dat.h:434-436`) → `packed
+2. **Macro-packed qids** — `QID/WIN/FILE` (`dat.h:463-465`) → `packed
struct(u64)` with one validating constructor.
-3. **`Rune*` plus a byte↔rune cache** with a scan-from-zero fallback
- (`xfid.c:855`) → byte slices clamped to grapheme boundaries.
-4. **`strtoul` pointer walking with `goto Rescue`** (`xfid.c:791-830`) → a slice
+3. **`Rune*` plus a byte-to-rune cache** with a scan-from-zero fallback
+ (`xfid.c:891-897`) → byte slices clamped to grapheme boundaries.
+4. **`strtoul` pointer walking with `goto Rescue`** (`xfid.c:810-838`) → a slice
reader returning `?u32`.
-5. **`longjmp`-ish `error()`** that aborts the process (`util.c`) → an error
- union and a `Reply` value.
+5. **`longjmp`-ish `error()`** that aborts the process (`util.c:50-55`) → an
+ error union and a `Reply` value.
6. **Manual `realloc` growth** (`wind.c:560`) → `ArrayList` with retained
capacity.
-7. **Sentinel-terminated tables** (`fsys.c:57-73`) → exhaustive enums, so adding
- a file to the tree does not compile until every switch has an answer for it.
-8. **`sprint` into fixed buffers** (`wind.c:533`) → `bufPrint` returning an
+7. **Sentinel-terminated tables** (`dirtab`, `fsys.c:62-74`; `dirtabw`,
+ `fsys.c:76-91`) → exhaustive enums, so adding a file to the tree does not
+ compile until every switch has an answer for it.
+8. **`sprint` into fixed buffers** (`wind.c:534`) → `bufPrint` returning an
error.
9. **Ownership by convention** — `fbufalloc`/`fbuffree` pairs the caller must
match (`fns.h:5-6`) → `defer`, plus two explicit borrow windows
(`Payload.staged`, `Payload.region`) documented at the seam.
10. **Prefix-matched command tables** whose order is load-bearing
- (`xfid.c:283+`) → whole-token enum lookup.
+ (`xfid.c:602-767`) → whole-token enum lookup.
One property comes along with the transport rather than with either design: a
9P `Twrite` IS a message, so acme never sees a fragment, while a POSIX client
@@ -286,17 +357,21 @@ would trade a clear error for a half-applied verb that never completes.
## What is not served, and why
-`acme`, `draw`, `consctl`, `label`, `editout` — acme keeps them for rio and for
-its own `Edit` language, neither of which pardes has. `xdata`, `rdsel`, `wrsel`,
-`index`, `cons` and `new/` are all here. `log` is NOT: it is a plan9port
-addition this acme's `dirtab` does not have, no example needed it, and a script
-that wants to notice panes it did not open reads `index` — which is what acme
-gives it. It cost a second queue, a focus hook in the core and ~100 lines, and
-it went out in review.
+`acme`, `consctl`, `draw`, `editout`, `label` — acme's root `dirtab`
+(`fsys.c:62-74`) keeps them for rio and for its own `Edit` language, neither of
+which pardes has, and `editout` appears in the per-window `dirtabw`
+(`fsys.c:76-91`) for the same reason. Everything else in `dirtabw` is here:
+`addr`, `body`, `ctl`, `data`, `errors`, `event`, `rdsel`, `tag`, `wrsel`,
+`xdata`, and `index`, `cons` and `new/` at the root. `log` is NOT — and this is
+the one entry that is not a decision about acme, because this acme's `dirtab`
+has no `log` either; it is a plan9port addition. No example needed it, and a
+script that wants to notice panes it did not open reads `index`, which is what
+acme gives it.
## Reading order
`src/acmefs.zig` (semantics; start at its header), `src/fuse.zig` (the wire),
`src/fs_service.zig` (mount lifecycle), `examples/README.md` (the client's view),
-`test/snapshots/acmefs.snap` and `acmefs-event.snap` (what is proven end to end),
+`test/snapshots/acmefs.snap` and `acmefs-event.snap` (the drivers, i.e. what is
+exercised end to end) beside their `.golden` files (what was observed),
`zig build fs-bench` (what it costs).