diff options
Diffstat (limited to 'docs/acme-fs.md')
| -rw-r--r-- | docs/acme-fs.md | 309 |
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). |
