From c4fc16ea956f6ed914ab829ed350b70da8e0cb19 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 13:10:08 +0200 Subject: [PATCH 01/12] Every path the tracker and the CLAUDE.md files cite resolves, and a gate holds it `src/citations.rs` reads every tracked `issues/**/*.md` and `CLAUDE.md` for tokens that begin with a directory git tracks at the root, and reds on one git does not track, naming the citing file, its line and the missing path. What another namespace spells is not read: patterns, `path:line:column` panic locations quoted from captures, build output under `target/`, the std fork under `rust/`, and `.cargo/config.toml`, the name cargo reads everywhere. The module header says how to cite what is not in this tree: a foreign repository's path with the repository's name in front, a deleted path as `^:`, a path not yet written relative to its crate. At 16d2e645 it found 138 dead citations of 62 distinct paths in 63 files (62 issue files and src/CLAUDE.md). Each is fixed: - moved paths re-pointed where the file went (the ARM stage-0 moves under `kernel/src/arch/x86_64/`, the syscall layer to `kernel/src/syscall/`, `kernel/src/inbox.rs` to `inbox/mod.rs`, `toyos-abi/src/io_uring.rs` to `inbox.rs`); - deleted paths written as the revision that last held them (the syscall monolith `4a98107f^:kernel/src/arch/syscall.rs`, the three workflows and `src/durations.rs` at `35383398^`, closed issue files), every one checked with `git cat-file -e`; - fork paths prefixed with their repository (`mio/`, `memmap2/`, `softbuffer/`, `tinycc/`, and `rust/` for the std fork in src/CLAUDE.md). `issues/build/issue-files-cite-paths-that-moved.md` is closed by this: every whole-repository path in `issues/` resolves, and the gate is the owner's answer to its question (a move that strands one is refused). Tracker hygiene in the same change: - merged `a-lane-s-tap-socket-path-is-past-sun-len-on-the-dev-host.md` (tooling) into `a-lane-s-tap-socket-path-outgrows-sun-len-on-the-dev-host.md`, now `kind: tooling` because it is the harness, with all three sightings; - merged the two `blocking_read_window` findings, whose slugs carried their measurements, into `blocking-read-window-reds-beside-other-guests.md`; - merged `boot-media/log-flush-retry-two-older-failure-modes-with-no-home.md` into `filesystem/log-flush-retry-deadman-arm.md`: one test, and the deadman assertion is the same red in both; - `toyos-dns-decodes-what-a-published-crate-decodes.md` had the illegal `kind: design-debt` and is a `defect`; two tracks carried an undeclared `decided:` field whose date their bodies already state. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK --- .../thorough-tier-reds-on-unmodified-main.md | 2 +- ...ry-two-older-failure-modes-with-no-home.md | 41 ---- ...et-path-is-past-sun-len-on-the-dev-host.md | 27 --- ...t-path-outgrows-sun-len-on-the-dev-host.md | 46 ++-- ...rds-boot-width-does-not-price-its-tests.md | 4 +- ...arness-leaves-its-qemu-children-running.md | 2 +- ...rch-module-in-userland-and-guest-probes.md | 2 +- ...-of-500-round-trips-beside-other-guests.md | 22 -- ...completed-26-of-500-beside-other-guests.md | 32 --- ...ng-read-window-reds-beside-other-guests.md | 45 ++++ ...d-shipping-it-cannot-shorten-the-matrix.md | 2 +- ...clippy-stage-two-is-lints-one-at-a-time.md | 8 +- ...l-winit-softbuffer-prs-are-now-sendable.md | 4 +- issues/build/defect-events.md | 6 +- .../issue-files-cite-paths-that-moved.md | 40 ---- .../build/memmap2-fork-is-unreachable-code.md | 4 +- ...eregister-fd-leaves-a-pending-poll-live.md | 4 +- .../parallel-tests-red-under-other-suites.md | 2 +- issues/build/python-and-cc-are-declared.md | 2 +- .../the-ack-delay-abi-doc-names-one-cpu.md | 4 +- ...job-runs-the-toolchain-the-runner-ships.md | 4 +- ...rebuilds-its-compiler-on-compiler-alone.md | 4 +- .../the-shard-profile-has-no-refresh-path.md | 2 +- .../build/the-swarm-is-not-yet-falsifiable.md | 4 +- ...rty-corpus-is-in-no-machine-read-ledger.md | 4 +- ...-is-a-review-prompt-and-three-workflows.md | 4 +- ...mio-s-toyos-waker-never-drains-its-pipe.md | 4 +- ...s-two-poller-slots-per-piped-connection.md | 2 +- .../design-debt/redesign-the-log-subsystem.md | 9 +- ...er-reads-a-toyos-window-behind-its-lock.md | 2 +- ...-decodes-what-a-published-crate-decodes.md | 2 +- .../design-debt/what-is-owed-on-file-size.md | 2 +- .../a-record-cannot-name-thread-zero.md | 2 +- ...-is-a-third-t14-flash-for-one-exit-code.md | 2 +- .../filesystem/log-flush-retry-deadman-arm.md | 39 +++- .../anonymous-mmap-is-not-demand-paged.md | 2 +- ...2-sts-clearing-is-verified-on-qemu-only.md | 2 +- ...d-name-cannot-reach-an-undeclared-child.md | 2 +- ...leaves-every-claimed-pci-function-armed.md | 2 +- .../dtv-capacity-is-a-workload-bound.md | 2 +- ...eadline-fired-132859-ms-late-on-the-t14.md | 2 +- ...vice-addresses-its-slot-never-gets-back.md | 4 +- ...-domains-addresses-are-never-given-back.md | 2 +- ...ouble-fault-on-cpu-1-under-a-wide-suite.md | 2 +- ...-implementation-is-one-instruction-deep.md | 2 +- ...-interrupts-on-and-the-deadline-ends-it.md | 4 +- ...evice-refused-is-reported-as-a-segfault.md | 4 +- ...-total-that-reads-zero-under-contention.md | 4 +- ...ends-a-remapping-entry-it-never-returns.md | 2 +- ...opped-answering-and-no-capture-says-why.md | 4 +- ...byte-pipe-write-wakes-the-readers-watch.md | 4 +- ...ds-the-idt-before-its-control-registers.md | 2 +- .../kernel/every-random-byte-is-one-rdrand.md | 2 +- .../every-wait-in-this-kernel-is-a-spin.md | 2 +- ...ing-enter-trips-the-one-queue-invariant.md | 2 +- ...-a-claim-answers-no-configuration-write.md | 2 +- .../one-mapping-is-written-in-two-ledgers.md | 11 +- ...an-be-made-to-fail-and-only-at-one-site.md | 4 +- ...ge-global-is-a-decision-nobody-has-made.md | 5 +- issues/kernel/spawned-process-never-starts.md | 2 +- ...-capability-end-state-is-twelve-answers.md | 65 +++--- ...nterrupt-and-only-the-timer-entry-polls.md | 2 +- .../the-global-pipe-lock-spans-a-user-copy.md | 4 +- .../kernel/the-reset-word-is-spelled-twice.md | 2 +- .../the-split-window-tlb-cost-is-unpriced.md | 4 +- ...exits-completion-post-is-the-second-one.md | 2 +- issues/kernel/toyos-runs-on-arm64.md | 10 +- ...nd-may-leave-nothing-to-end-the-machine.md | 4 +- .../no-console-between-boot-and-terminal.md | 2 +- src/CLAUDE.md | 2 +- src/citations.rs | 216 ++++++++++++++++++ src/lib.rs | 4 + 72 files changed, 461 insertions(+), 317 deletions(-) delete mode 100644 issues/boot-media/log-flush-retry-two-older-failure-modes-with-no-home.md delete mode 100644 issues/build/a-lane-s-tap-socket-path-is-past-sun-len-on-the-dev-host.md delete mode 100644 issues/build/blocking-read-window-completed-21-of-500-round-trips-beside-other-guests.md delete mode 100644 issues/build/blocking-read-window-completed-26-of-500-beside-other-guests.md create mode 100644 issues/build/blocking-read-window-reds-beside-other-guests.md delete mode 100644 issues/build/issue-files-cite-paths-that-moved.md create mode 100644 src/citations.rs diff --git a/issues/audio/thorough-tier-reds-on-unmodified-main.md b/issues/audio/thorough-tier-reds-on-unmodified-main.md index 8d8b1803b83..575001d5335 100644 --- a/issues/audio/thorough-tier-reds-on-unmodified-main.md +++ b/issues/audio/thorough-tier-reds-on-unmodified-main.md @@ -154,7 +154,7 @@ the instrument refusing — stopped' after the last client removal — the device is still running with no clients`. That is filed apart as `gate-a-suspend-structure-verdict-unread`. -The exit code is fixed in `.github/workflows/gate-a.yml` (`set -o pipefail`, the +The exit code is fixed in `35383398^:.github/workflows/gate-a.yml` (`set -o pipefail`, the idiom every other workflow in `.github/` already uses). Nothing about how a verdict is *reached* changed. diff --git a/issues/boot-media/log-flush-retry-two-older-failure-modes-with-no-home.md b/issues/boot-media/log-flush-retry-two-older-failure-modes-with-no-home.md deleted file mode 100644 index cef96988ee4..00000000000 --- a/issues/boot-media/log-flush-retry-two-older-failure-modes-with-no-home.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -status: open -kind: tooling -opened: 2026-09-08 ---- - -# `log_flush_retry` has two older failure modes that lost their tracker file - -`issues/boot-media/log-flush-retry-reds-two-ways-at-two-in-five.md` recorded -four ways `cargo test --test toyos-build -- --nightly log_flush_retry` had -been seen to red; that file was deleted (`d5c2d9c9`) once ROOT-in-memory gave -the `[hung]` arm's own mechanism — a stick going offline while userland is -still paged from it — a fix and six green runs. Two of its other ways were not -re-seen in those six runs and so were not carried anywhere: - -- **The `[hung]` arm missing its "transport broke on SCSI" line.** One of the - file's two originally-recorded assertions: the wide run reds on - `the deadman never declared the volume failed` while the *alone* re-run of - the same boot reds instead on - `no "transport broke on SCSI" in the log, so the staged hung device never - met its recovery` — two different assertions from the same staged fault, - which was the file's reason for treating this as more than a scheduling - classification. -- **A boot timeout before `===READY===`.** Measured 2026-09-07 on three - separate points (`main` 71ed50cf, `metal-suite` 7f16914d and f63bce1d), each - run alone: `log_flush_retry: [qemu] Boot timed out waiting for - ===READY===`, with the boot never coming up at all — a third shape beside - the two assertion failures, seen before ROOT-in-memory and on a branch that - does not carry it. - -Neither mode was seen in the six runs that closed the deleted file, so neither -is known fixed by that work. - -## Exit condition - -Re-measure `log_flush_retry` (wide and alone) enough times, on a tree that -carries ROOT-in-memory, to say whether either mode still occurs; if seen -again, a `src/redlist.rs` row carries it with a measured rate, or it is fixed -at its owner (`kernel/src/drivers/xhci` for the transport line, the boot -harness for the timeout). If not seen in that many runs, this file closes with -the count that supports it. diff --git a/issues/build/a-lane-s-tap-socket-path-is-past-sun-len-on-the-dev-host.md b/issues/build/a-lane-s-tap-socket-path-is-past-sun-len-on-the-dev-host.md deleted file mode 100644 index a055bb032a6..00000000000 --- a/issues/build/a-lane-s-tap-socket-path-is-past-sun-len-on-the-dev-host.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -status: open -kind: tooling -opened: 2026-09-26 ---- - -# A lane's tap socket path is past `SUN_LEN` on the dev host - -`lan_mdns_answer` reds on the macOS dev host, wide and alone: - -``` -connect to QEMU's /private/var/folders/gr/mr4_fg4n34jb417sx1g5cgxc0000gp/T/toyos-tmp-89085-0/tests-0/lane-3/tap-out-0.sock: path must be shorter than SUN_LEN -``` - -That path is 104 bytes, and macOS's `sun_path` holds 104 including the NUL. -`tests/common/segment.rs`'s `Tap::in_lane` puts both sockets in -`lane::dir()`, which since `toyos-tmpdir` is -`$TMPDIR/toyos-tmp--/tests-/lane-/`, and the dev host's -`$TMPDIR` resolves to 57 bytes (`/private/var/folders/…/T/`) before any of -that. A five-digit pid is enough to cross the limit. Seen on -`wt/toyos-layout` after it merged `origin/main` at `e48604c0`; nothing on that -branch touches the lane or the tap. - -## Exit condition - -A tap socket's path fits `sun_path` on every host the suite runs on, and -`lan_mdns_answer` is green on the dev host. diff --git a/issues/build/a-lane-s-tap-socket-path-outgrows-sun-len-on-the-dev-host.md b/issues/build/a-lane-s-tap-socket-path-outgrows-sun-len-on-the-dev-host.md index 230a3d13bbe..7e81a9cf3d2 100644 --- a/issues/build/a-lane-s-tap-socket-path-outgrows-sun-len-on-the-dev-host.md +++ b/issues/build/a-lane-s-tap-socket-path-outgrows-sun-len-on-the-dev-host.md @@ -1,23 +1,37 @@ --- status: open -kind: defect +kind: tooling opened: 2026-09-26 --- # A lane's tap socket path outgrows `SUN_LEN` on the dev host -`lan_mdns_answer` reds wide and alone with `connect to QEMU's -/private/var/folders/gr/mr4_fg4n34jb417sx1g5cgxc0000gp/T/toyos-tmp-70685-0/tests-0/lane-7/tap-out-0.sock: -path must be shorter than SUN_LEN`. `common::segment::Tap::in_lane` puts the -two sockets in the lane's scratch directory, and on this macOS host that -directory sits under `$TMPDIR`, so the path is 104 bytes, past the 103 a -`sockaddr_un` holds before its terminating NUL on macOS. - -Seen in the fast tier twice in one session: at `origin/main` checked out in -the `toyos-guiplat` worktree (alone with this message, wide as `QEMU died -before ===READY===`), and at PR #528's head after it merged `e48604c0` (this -message wide and alone). `cargo run --- --known-red lan_mdns_answer` answers NO. - -**Exit**: the socket paths fit a `sockaddr_un` wherever the scratch -directory is, with `lan_mdns_answer` green on this host. +`lan_mdns_answer` reds on the macOS dev host, wide and alone: + +``` +connect to QEMU's /private/var/folders/gr/mr4_fg4n34jb417sx1g5cgxc0000gp/T/toyos-tmp-89085-0/tests-0/lane-3/tap-out-0.sock: path must be shorter than SUN_LEN +``` + +That path is 104 bytes, past the 103 a `sockaddr_un` holds before its +terminating NUL on macOS. `tests/common/segment.rs`'s `Tap::in_lane` puts both +sockets in `lane::dir()`, which since `toyos-tmpdir` is +`$TMPDIR/toyos-tmp--/tests-/lane-/`, and the dev host's +`$TMPDIR` resolves to 57 bytes (`/private/var/folders/…/T/`) before any of +that. A five-digit pid is enough to cross the limit. + +Seen three times on 2026-09-26, each on a tree whose diff touches neither the +lane nor the tap: + +- on `wt/toyos-layout` after it merged `origin/main` at `e48604c0` (pid 89085, + `lane-3`, the capture above); +- in the fast tier at `origin/main` checked out in the `toyos-guiplat` + worktree: alone with this message, wide as `QEMU died before ===READY===`; +- in the fast tier at PR #528's head after it merged `e48604c0` (pid 70685, + `lane-7`), this message wide and alone. + +`cargo run -- --known-red lan_mdns_answer` answers NO. + +## Exit condition + +A tap socket's path fits `sun_path` on every host the suite runs on, wherever +the scratch directory is, and `lan_mdns_answer` is green on the dev host. diff --git a/issues/build/a-shards-boot-width-does-not-price-its-tests.md b/issues/build/a-shards-boot-width-does-not-price-its-tests.md index 226617ce122..1fe190cea2a 100644 --- a/issues/build/a-shards-boot-width-does-not-price-its-tests.md +++ b/issues/build/a-shards-boot-width-does-not-price-its-tests.md @@ -11,7 +11,7 @@ reference and multiplies every liveness ceiling by the result; every shard print it (`host: fastest boot N ms against the reference 1320 ms — liveness ceilings paid at Wx width`). The proposal was to spend the same factor on the *duration profile*: divide each shard's measured prices by its width in -`src/durations.rs`'s merge, so `src/tiers.rs`'s ceiling compares like with like +`35383398^:src/durations.rs`'s merge, so `src/tiers.rs`'s ceiling compares like with like across shards of different speed, with timer-anchored names exempt because a fixed wait does not shrink on a fast host. @@ -132,7 +132,7 @@ renormalize. ## What is still true and is not this The two-*machine* gap — twelve hosted EPYC shards against one T14 lane, -1.35–1.37x apart on an idle host, recorded in `src/durations.rs`'s header with +1.35–1.37x apart on an idle host, recorded in `35383398^:src/durations.rs`'s header with the committed profile's `shards=` column naming which partition took each price — is untouched by any of the above: that measurement is a gap between machines, not a within-lane shard factor. This file says only that the diff --git a/issues/build/a-sigkilled-harness-leaves-its-qemu-children-running.md b/issues/build/a-sigkilled-harness-leaves-its-qemu-children-running.md index 4d218284ce8..943ff8ae8cb 100644 --- a/issues/build/a-sigkilled-harness-leaves-its-qemu-children-running.md +++ b/issues/build/a-sigkilled-harness-leaves-its-qemu-children-running.md @@ -13,7 +13,7 @@ then reclaims the killed run's directory — the very images those QEMU processes still have open — and unlinks it while the guest is still alive, holding the disk invisibly until the guest itself exits. -This is not a regression: the retired `src/scratch.rs` design (kept a killed +This is not a regression: the retired `96c2f83d^:src/scratch.rs` design (kept a killed run's directory for 24 hours) had the identical gap for a killed run's QEMU children, so #529 (which replaced that design) found it and correctly did not block on it. It is unfixed either way and worth its own entry. diff --git a/issues/build/assembly-outside-an-arch-module-in-userland-and-guest-probes.md b/issues/build/assembly-outside-an-arch-module-in-userland-and-guest-probes.md index c358cb10faa..6035bb6f8a7 100644 --- a/issues/build/assembly-outside-an-arch-module-in-userland-and-guest-probes.md +++ b/issues/build/assembly-outside-an-arch-module-in-userland-and-guest-probes.md @@ -9,7 +9,7 @@ opened: 2026-09-26 The owner's ruling of 2026-09-26 puts every `asm!`, `global_asm!`, `naked_asm!`, naked function and `core::arch::*` intrinsic inside an architecture's own module: `kernel/src/arch//`, the bootloader's -`src/arch/`, and `toyos-abi`'s per-arch syscall entry. `src/sourcegate.rs`'s +`bootloader/src/arch/`, and `toyos-abi`'s per-arch syscall entry. `src/sourcegate.rs`'s `ARCH_RULES` enforces it. The kernel and the loader now hold none outside those; what is left is declared in that table as an exception, each row pointing here: diff --git a/issues/build/blocking-read-window-completed-21-of-500-round-trips-beside-other-guests.md b/issues/build/blocking-read-window-completed-21-of-500-round-trips-beside-other-guests.md deleted file mode 100644 index 3beb62541eb..00000000000 --- a/issues/build/blocking-read-window-completed-21-of-500-round-trips-beside-other-guests.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -status: open -kind: finding -opened: 2026-09-26 ---- - -# `blocking_read_window` completed 21 of 500 round trips beside other guests - -Fast tier at `62ef89a4` (PR #525's branch, other worktrees' guests running): -`blocking_read_stress: only 21 of 500 round trips completed inside 3s — a wake -was not delivered`; the process's own line gave `syscall_wall=3087ms` and -`cpu=1703ms` for pid 7, and cpu0 took 169 xHCI interrupts in the window. The -harness's re-run alone was green. `cargo run -- --known-red` answers NO. - -In the same session the fast tier ran six times, three on the branch and three -on `main` at `d65446cc`, interleaved: this test was red once on the branch and -never on `main`, while `main`'s third run had five reds of its own that the -branch never showed. The branch's kernel differs from `main` in the claim -DMA paths, which this test does not reach, and in one boot log line. - -**Exit**: a cause — a lost wake, or a 3 s budget a starved host cannot meet — -and, if it is the budget, the bound derived rather than measured. diff --git a/issues/build/blocking-read-window-completed-26-of-500-beside-other-guests.md b/issues/build/blocking-read-window-completed-26-of-500-beside-other-guests.md deleted file mode 100644 index 45649d3551b..00000000000 --- a/issues/build/blocking-read-window-completed-26-of-500-beside-other-guests.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -status: open -kind: finding -opened: 2026-09-26 ---- - -# `blocking_read_window` completed 26 of 500 round trips beside other guests - -Fast tier at `1e5ef5c1` (PR #524's branch; the host carried the branch's own -twelve guest slots and another worktree's suite at the same time): -`blocking_read_stress: only 26 of 500 round trips completed inside 3s — a wake -was not delivered`. The harness's re-run alone was green in 2 s -(`at least 193 held windows a post landed in (0 -> 256)`). `cargo run -- ---known-red blocking_read_window` answers NO. - -What the red run's own log says against its sentence: the two processes spent -`cpu=1639ms` and `cpu=1998ms` of the 3.5 s they ran (`syscall_wall=3535ms` and -`3599ms`), and cpu0 took 522 interrupts, 456 of them xHCI — a guest that was -running and slow, not one parked on a wake that never came. So the verdict's -cause is unread: the test waits host seconds and names a lost wake when they -run out, which a starved guest satisfies as well as a lost wake does. - -Main reproduces it. A same-session A/B, runs of main (`d65446cc`) and of the -branch started together so both arms carried one load (six suites at once): -main 16 of 17 green, the branch 17 of 18, and each arm's one red is this -sentence (`only 26 of 500` on main, `only 27 of 500` on the branch; the -branch's red spent `cpu=1568ms` and `cpu=1532ms` of its window). The rate is -the same on both arms, so the branch did not move it. - -**Exit**: the verdict tells a lost wake from a slow guest (the round trips' -progress over the window, not only the count at its end), and a cause for -this run. diff --git a/issues/build/blocking-read-window-reds-beside-other-guests.md b/issues/build/blocking-read-window-reds-beside-other-guests.md new file mode 100644 index 00000000000..aea2f17d339 --- /dev/null +++ b/issues/build/blocking-read-window-reds-beside-other-guests.md @@ -0,0 +1,45 @@ +--- +status: open +kind: finding +opened: 2026-09-26 +--- + +# `blocking_read_window` reds beside other guests, naming a lost wake a slow guest also satisfies + +The verdict, in the fast tier: `blocking_read_stress: only N of 500 round trips +completed inside 3s — a wake was not delivered`. The harness's re-run alone is +green. `cargo run -- --known-red blocking_read_window` answers NO. + +Sightings, all on 2026-09-26: + +- **26 of 500** at `1e5ef5c1` (PR #524's branch; the host carried the + branch's own twelve guest slots and another worktree's suite at the same + time). The re-run alone was green in 2 s (`at least 193 held windows a post + landed in (0 -> 256)`). +- **21 of 500** at `62ef89a4` (PR #525's branch, other worktrees' guests + running). The process's own line gave `syscall_wall=3087ms` and + `cpu=1703ms` for pid 7, and cpu0 took 169 xHCI interrupts in the window. In + the same session the fast tier ran six times, three on the branch and three + on `main` at `d65446cc`, interleaved: this test was red once on the branch + and never on `main`, while `main`'s third run had five reds of its own that + the branch never showed. The branch's kernel differs from `main` in the + claim DMA paths, which this test does not reach, and in one boot log line. +- **On `main` too, at the same rate.** A same-session A/B, runs of `main` + (`d65446cc`) and of PR #524's branch started together so both arms carried + one load (six suites at once): `main` 16 of 17 green, the branch 17 of 18, + and each arm's one red is this sentence (`only 26 of 500` on `main`, `only 27 + of 500` on the branch; the branch's red spent `cpu=1568ms` and `cpu=1532ms` + of its window). The rate is the same on both arms, so no branch moved it. + +**What the red runs' own logs say against the sentence.** In the 26-of-500 +run the two processes spent `cpu=1639ms` and `cpu=1998ms` of the 3.5 s they ran +(`syscall_wall=3535ms` and `3599ms`), and cpu0 took 522 interrupts, 456 of them +xHCI — a guest that was running and slow, not one parked on a wake that never +came. So the verdict's cause is unread: the test waits host seconds and names a +lost wake when they run out, which a starved guest satisfies as well as a lost +wake does. + +**Exit**: the verdict tells a lost wake from a slow guest (the round trips' +progress over the window, not only the count at its end), and a cause for these +runs — a lost wake, or a 3 s budget a starved host cannot meet, and if it is +the budget, the bound derived rather than measured. diff --git a/issues/build/building-the-image-once-and-shipping-it-cannot-shorten-the-matrix.md b/issues/build/building-the-image-once-and-shipping-it-cannot-shorten-the-matrix.md index fe0afed5a44..789fb654677 100644 --- a/issues/build/building-the-image-once-and-shipping-it-cannot-shorten-the-matrix.md +++ b/issues/build/building-the-image-once-and-shipping-it-cannot-shorten-the-matrix.md @@ -53,7 +53,7 @@ attempts of run `31389081797`. **What is still on the floor and is not this.** The 52–59 s `deps` step is a package install repeated in every guest job on every run, and it is not a build -at all: `.github/ci-image/Dockerfile` bakes those packages into a published +at all: `35383398^:.github/ci-image/Dockerfile` bakes those packages into a published image, and the cutover retires the step once the first published digest exists for the guest workflows to pin. diff --git a/issues/build/clippy-stage-two-is-lints-one-at-a-time.md b/issues/build/clippy-stage-two-is-lints-one-at-a-time.md index 8ae61d45bab..ae352919701 100644 --- a/issues/build/clippy-stage-two-is-lints-one-at-a-time.md +++ b/issues/build/clippy-stage-two-is-lints-one-at-a-time.md @@ -22,9 +22,9 @@ Stage one (#132) put default clippy on every PR and it runs today: the `host` job's `clippy` step lints the host workspace (`--workspace --all-targets`), the kernel under `x86_64-unknown-none` in both feature arms, the bootloader under `x86_64-unknown-uefi` and `toyos-abi`, each with `-D warnings` and the six -adopted lints (`.github/workflows/host-tests.yml:199-220`). `userland/` is the +adopted lints (`35383398^:.github/workflows/host-tests.yml:199-220`). `userland/` is the one tree it cannot reach, and that step says why at -`.github/workflows/host-tests.yml:130-137`: `x86_64-unknown-toyos` is a custom +`35383398^:.github/workflows/host-tests.yml:130-137`: `x86_64-unknown-toyos` is a custom target and the `toyos` toolchain has no `cargo-clippy` component. This entry carries stage two: every `pedantic`/`nursery` lint measured on all three trees, adopted or rejected @@ -47,9 +47,9 @@ the group). | lint | count | why | |---|---:|---| -| `checked_conversions` | 1 | `kernel/src/arch/syscall.rs`'s device-register-write syscall bounded a `u64` against `u32::MAX` with a manual `as` comparison, then cast twice more in the body. Restructured through `u32::try_from` — one conversion, no repeated casts, at a trust-boundary site that is exactly what this bar is for. | +| `checked_conversions` | 1 | `4a98107f^:kernel/src/arch/syscall.rs`'s device-register-write syscall bounded a `u64` against `u32::MAX` with a manual `as` comparison, then cast twice more in the body. Restructured through `u32::try_from` — one conversion, no repeated casts, at a trust-boundary site that is exactly what this bar is for. | | `manual_midpoint` | 3 | `(a + b) / 2` can overflow near the integer's max; `T::midpoint` can't. Two in a `toyos-desktop` test, one in `toyos-sched/sim`'s binary-search shrinker — no realistic overflow today, but the fix is free and closes the class everywhere, forever. | -| `redundant_clone` | 6 | Real, not the false-positive-prone lint its `nursery` placement suggested — every instance checked was a clone of a binding never used again after (verified by hand, not by trusting the lint): `kernel/src/arch/syscall.rs`, a `kernel-loom` test, `toyos-cc`'s parser (a double clone — the match scrutinee was already an owned value), and three in `tests/toyos.rs`. | +| `redundant_clone` | 6 | Real, not the false-positive-prone lint its `nursery` placement suggested — every instance checked was a clone of a binding never used again after (verified by hand, not by trusting the lint): `4a98107f^:kernel/src/arch/syscall.rs`, a `kernel-loom` test, `toyos-cc`'s parser (a double clone — the match scrutinee was already an owned value), and three in `tests/toyos.rs`. | | `unchecked_time_subtraction` | 1 | `tests/toyos.rs`: a bare `Duration - Duration` that panics identically today either way — `.checked_sub().expect(msg)` says why it can't underflow instead of hiding the same panic behind an operator. No behavior change; an honesty win at zero cost. | | `unnecessary_semicolon` | 2 | Two dead `;` after `if` statements in `kernel/src/process.rs` and `kernel/src/main.rs`. Nothing to weigh. | | `default_trait_access` | 1 | `bootloader/src/main.rs`'s UEFI `open()` call passed `Default::default()` where `FileAttribute::default()` names the type. One site, one import. | diff --git a/issues/build/cpal-winit-softbuffer-prs-are-now-sendable.md b/issues/build/cpal-winit-softbuffer-prs-are-now-sendable.md index d6857ac0a4c..52138598996 100644 --- a/issues/build/cpal-winit-softbuffer-prs-are-now-sendable.md +++ b/issues/build/cpal-winit-softbuffer-prs-are-now-sendable.md @@ -5,7 +5,7 @@ opened: 2026-09-04 --- `forks.toml`'s `pr` field for `cpal`, `winit` and `softbuffer` names each -fork's precondition as "sendable once toyos-abi/toyos/toyos-window are on -crates.io". They published 2026-09-04 (run 33832842328), so the precondition +fork's precondition as sendable once `toyos-abi`, `toyos` and `toyos-window` are on +crates.io. They published 2026-09-04 (run 33832842328), so the precondition is met for all three; the sibling-tier forks (target state: disappear) are ready to open against upstream, unopened. diff --git a/issues/build/defect-events.md b/issues/build/defect-events.md index 59863eb2907..278dceceebf 100644 --- a/issues/build/defect-events.md +++ b/issues/build/defect-events.md @@ -56,8 +56,8 @@ ledger was written. (`PS2_QUEUE_SIZE`) and past it drops one byte at a time with no signal, reproduced deterministically on QEMU 11.1 by putting 22 transitions through one `input-send-event`. Closed - `issues/kernel/two-i8042-verdicts-red-together-on-one-ci-shard.md` and - `issues/build/i8042-keyboard-pays-a-lost-sentinel-and-reds-the-durations-gate.md`, + `2cbb3e0d^:issues/kernel/two-i8042-verdicts-red-together-on-one-ci-shard.md` and + `2cbb3e0d^:issues/build/i8042-keyboard-pays-a-lost-sentinel-and-reds-the-durations-gate.md`, retired two redlist rows. - **The census lag** — origin: pre-existing. discoverer: automated gate @@ -113,7 +113,7 @@ ledger was written. screendump` mode, `kernel_heartbeat`'s clean-exit-before-`===READY===` family, PR #202's direction-flag silent reset) to W^X, to the NX bit, or to the boot-time CPU-feature assertion at - `kernel/src/arch/control_regs.rs:238-242` that panics if a CPU lacks the NX + `kernel/src/arch/x86_64/control_regs.rs:238-242` that panics if a CPU lacks the NX bit W^X depends on. Whoever placed this row in the brief holds the citation this entry is missing; append it here rather than re-deriving it once found. diff --git a/issues/build/issue-files-cite-paths-that-moved.md b/issues/build/issue-files-cite-paths-that-moved.md deleted file mode 100644 index 7f8760805cf..00000000000 --- a/issues/build/issue-files-cite-paths-that-moved.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -status: open -kind: tooling -opened: 2026-09-26 ---- - -# Issue files cite source paths that no longer exist - -An issue names the site it is about by path, and a path is the claim a reader -checks first. Moving a source file leaves every issue that cites it pointing at -nothing, and nothing notices. - -Measured on PR #524's branch at `73a89365` (the port's stage 0 moved the -kernel's x86 code under `kernel/src/arch/x86_64/`, the syscall handlers out of -`arch/`, and `hw.rs`): every `kernel/`, `src/`, `userland/`, `tests/`, -`bootloader/` and `toyos-*/` path written in `issues/`, checked against that -tree and against `origin/main`: - -- 46 citations in 35 files name a path that exists on `main` and not on the - branch: the branch moved them. Among them `kernel/src/arch/syscall/*.rs`, - `kernel/src/hw.rs`, `kernel/src/mm/paging.rs`, `kernel/src/arch/apic.rs`, - `kernel/src/arch/idt/*.rs` and `kernel/src/drivers/watchdog.rs`. -- Already on `main`, before this branch: at least 14 citations in 11 files of - a whole `kernel/src/` path that does not exist (`kernel/src/arch/syscall.rs`, - `kernel/src/inbox.rs`, `kernel/src/log_file.rs`, - `kernel/src/completion/mod.rs` among them). The wider count, 153 in 95 files, - also holds crate-relative spellings (`sched/dump.rs`) that the check cannot - tell from rot. - -**Whether a gate belongs with it.** `src/CLAUDE.md` says documentation carries -no gates, and an issue is prose; a red gate over `issues/` contradicts that -rule, so it is the owner's to decide. What the tree already requires of a -deleted document (its citations go in the same merge) is the rule a move needs -too, and an on-demand check that lists every cited path that does not resolve -(offline, beside `--check-forks`) would let the mover do it. A gate is what -makes the mover's duty hold. The on-demand check only makes it cheap. - -**Exit condition**: every whole-repository path written in `issues/` resolves -in the tree that holds it, and the owner has ruled whether a move that strands -one is refused by a gate or caught on demand. diff --git a/issues/build/memmap2-fork-is-unreachable-code.md b/issues/build/memmap2-fork-is-unreachable-code.md index 032d88e14f7..2a73d14fca0 100644 --- a/issues/build/memmap2-fork-is-unreachable-code.md +++ b/issues/build/memmap2-fork-is-unreachable-code.md @@ -10,10 +10,10 @@ opened: 2026-07-30 `target_os = "toyos"` to a `Vec` implementation at all 8 sites, and `rust/Cargo.toml` is the only manifest that patches memmap2 at all — userland's duplicate entry resolved to nothing and was deleted 2026-08-01. So no ToyOS code -path calls any memmap2 API. `src/toyos.rs` is compiled and never called; the +path calls any memmap2 API. `memmap2/src/toyos.rs` is compiled and never called; the fork's only load-bearing content is the `0.9.10 → 0.2.1` version relabel that satisfies rustc's pin. -Either delete `src/toyos.rs` and let `stub.rs` serve, or drop the toyos gate in +Either delete `memmap2/src/toyos.rs` and let `stub.rs` serve, or drop the toyos gate in `rustc_data_structures` (the only two APIs rustc uses, `map_copy_read_only` and `map_anon`, are correct in the fork). Exactly one of the two should exist. Three real bugs in that module were found and fixed 2026-07-28 — see `forks.toml`. diff --git a/issues/build/mio-deregister-fd-leaves-a-pending-poll-live.md b/issues/build/mio-deregister-fd-leaves-a-pending-poll-live.md index 1951fdfe9f5..1bcf19f948e 100644 --- a/issues/build/mio-deregister-fd-leaves-a-pending-poll-live.md +++ b/issues/build/mio-deregister-fd-leaves-a-pending-poll-live.md @@ -6,7 +6,7 @@ opened: 2026-08-16 # mio's ToyOS selector deregisters a token but not the kernel's poll on it -`src/sys/toyos/selector.rs` in the mio fork (currently pinned at `e8068c2`, +`mio/src/sys/toyos/selector.rs` in the mio fork (currently pinned at `e8068c2`, `userland/Cargo.lock`) keeps its own registration list rather than asking the kernel to track interest: @@ -32,7 +32,7 @@ notification for a resource it was promised was gone. ## Why there is nothing to cancel with -There used to be an ABI op for this. `toyos-abi/src/io_uring.rs` op code 2 was +There used to be an ABI op for this. `toyos-abi/src/inbox.rs` op code 2 was `IORING_OP_POLL_REMOVE`, retired in PR #89 (`c41b831`, "abi: four names retired, and the number each one held") as caller-less. The retirement's own reasoning is recorded at the site: diff --git a/issues/build/parallel-tests-red-under-other-suites.md b/issues/build/parallel-tests-red-under-other-suites.md index 3d120889eed..5a60c2b2fd2 100644 --- a/issues/build/parallel-tests-red-under-other-suites.md +++ b/issues/build/parallel-tests-red-under-other-suites.md @@ -534,7 +534,7 @@ mechanism for it. `src/redlist.rs` carries the sighting. **It contradicts a retirement rather than joining a class.** All three of the name's earlier rows in `src/redlist.rs` are retired: the two dev-host `ALONE: GREEN` rows by the single-word tally in - `kernel/src/drivers/i8042/tally.rs` (2026-08-17), which made `N interrupts + `kernel/src/arch/x86_64/i8042/tally.rs` (2026-08-17), which made `N interrupts and 0 bytes` unprintable, and the CI row by "the verdict revises itself once" (2026-08-28) — a mute line said while a decoder still holds the run is `HEALTH_MUTE_BLIND`, the first blamed byte moves it to `HEALTH_MUTE_SAID` diff --git a/issues/build/python-and-cc-are-declared.md b/issues/build/python-and-cc-are-declared.md index 3482400abcd..a374604a592 100644 --- a/issues/build/python-and-cc-are-declared.md +++ b/issues/build/python-and-cc-are-declared.md @@ -40,7 +40,7 @@ Rust bootstrap again as an incidental fix; do not soften the entry either. `src/toolchain.rs:749` picks `./x` when `rust/x` exists, which it does. That file is a `/bin/sh` script whose whole job is `SEARCH="python3 python py python2 uv"`, -and it execs `x.py` → `src/bootstrap/bootstrap.py` (55,550 bytes). So a clean +and it execs `x.py` → `rust/src/bootstrap/bootstrap.py` (55,550 bytes). So a clean clone cannot build a toolchain without Python 3. It is upstream's bootstrap and not our code, which is why it is stated rather than blamed — but the bar has no upstream exemption, and `bootstrap.py` can never run inside ToyOS. diff --git a/issues/build/the-ack-delay-abi-doc-names-one-cpu.md b/issues/build/the-ack-delay-abi-doc-names-one-cpu.md index 2ccedc42b5f..aa9d5253f7e 100644 --- a/issues/build/the-ack-delay-abi-doc-names-one-cpu.md +++ b/issues/build/the-ack-delay-abi-doc-names-one-cpu.md @@ -11,12 +11,12 @@ opened: 2026-09-14 > Make the last CPU a shootdown waits for answer `arg` nanoseconds late, and > take it away again. -The kernel arm no longer picks a CPU by arithmetic. `kernel/src/arch/tlb.rs`'s +The kernel arm no longer picks a CPU by arithmetic. `kernel/src/arch/x86_64/tlb.rs`'s `debug_arm_ack_delay` holds each other CPU's acknowledgement back for `arg` nanoseconds in turn, takes one shootdown against each, and returns the smallest wait any of them cost the initiator; the arming is then left standing against every other CPU until a disarm or the end of a fresh `ARM_WINDOW_NANOS` -(`kernel/src/arch/tlb.rs:230`, two seconds), whichever comes first. So the one +(`kernel/src/arch/x86_64/tlb.rs:230`, two seconds), whichever comes first. So the one sentence userland reads to learn what action 12 does describes a selection the kernel does not make, omits the answer it returns, and says nothing about how long what it leaves behind lasts. diff --git a/issues/build/the-host-job-runs-the-toolchain-the-runner-ships.md b/issues/build/the-host-job-runs-the-toolchain-the-runner-ships.md index cdefcfb32c3..79f14184869 100644 --- a/issues/build/the-host-job-runs-the-toolchain-the-runner-ships.md +++ b/issues/build/the-host-job-runs-the-toolchain-the-runner-ships.md @@ -6,7 +6,7 @@ opened: 2026-09-03 # The host job tracks whatever toolchain `macos-latest` ships, and a runner roll reds every open pull request at once -`.github/workflows/host-tests.yml`'s `host` job installs no Rust toolchain: it +`35383398^:.github/workflows/host-tests.yml`'s `host` job installs no Rust toolchain: it runs `rustc -vV; cargo -V; rustup component add clippy` on whatever `macos-latest` ships that day, and there is no root `rust-toolchain.toml` — only `kernel/`, `bootloader/` and `userland/` pin one, each to a target list @@ -56,7 +56,7 @@ own `set -e` never reached before the script died on the first red pipeline — site under the kernel's own two clippy invocations (`kernel/src/loader/start.rs:146`), plus one unrelated new lint the kernel arm alone surfaced, `clippy::map_or_identity` -(`kernel/src/arch/syscall/dispatch.rs:278`). All were confirmed clean under +(`kernel/src/syscall/dispatch.rs:278`). All were confirmed clean under `RUSTUP_TOOLCHAIN=1.98.0 cargo run -- --clippy` and unchanged under the default 1.97.1 after the fix. diff --git a/issues/build/the-primary-rebuilds-its-compiler-on-compiler-alone.md b/issues/build/the-primary-rebuilds-its-compiler-on-compiler-alone.md index 39616126a87..8b838eba3e8 100644 --- a/issues/build/the-primary-rebuilds-its-compiler-on-compiler-alone.md +++ b/issues/build/the-primary-rebuilds-its-compiler-on-compiler-alone.md @@ -9,7 +9,7 @@ opened: 2026-09-26 `src/toolchain.rs` rebuilds the primary's toolchain when the stamp over `rust/compiler/` changes, and `compiler::record` writes that tree as the compiler the primary's `stage2` is. A fork commit that moves only -`src/bootstrap`, `src/tools`, `src/stage0`, `Cargo.lock` or the LLVM submodule +`rust/src/bootstrap`, `rust/src/tools`, `rust/src/stage0`, `rust/Cargo.lock` or the LLVM submodule leaves the primary on the compiler it had, and a worktree whose `compiler/` matches the record is handed that compiler too. A worktree's own compiler is keyed on all of them (`compiler::key`, PR #524), so the two answers to "which @@ -21,4 +21,4 @@ lands. **Exit condition**: the primary's rebuild and its record read the same sources `compiler::key` does, and a worktree compares against that, shown by a test -in which a fork moving only `src/tools` gets a compiler of its own. +in which a fork moving only `rust/src/tools` gets a compiler of its own. diff --git a/issues/build/the-shard-profile-has-no-refresh-path.md b/issues/build/the-shard-profile-has-no-refresh-path.md index 39d7df8027e..62bbe379ac0 100644 --- a/issues/build/the-shard-profile-has-no-refresh-path.md +++ b/issues/build/the-shard-profile-has-no-refresh-path.md @@ -9,7 +9,7 @@ opened: 2026-09-24 `tests/test-durations` is what every nightly shard prices its partition against (`tests/toyos.rs`'s `shard_pricing`), and nothing writes it any more: the shards no longer upload their measurements and `--merge-durations` is -gone with `src/durations.rs`. A test added after the file's last refresh is +gone with `35383398^:src/durations.rs`. A test added after the file's last refresh is priced at the harness's default, and one whose cost moved keeps its old price. What that costs is balance, never a verdict: every name still lands in exactly diff --git a/issues/build/the-swarm-is-not-yet-falsifiable.md b/issues/build/the-swarm-is-not-yet-falsifiable.md index 6b8ef872f03..879a39d5871 100644 --- a/issues/build/the-swarm-is-not-yet-falsifiable.md +++ b/issues/build/the-swarm-is-not-yet-falsifiable.md @@ -213,14 +213,14 @@ issues/hardware/the-bot-scsi-machine-is-still-hand-written-in-the-kernel.md issues/hardware/the-t14-touchpad-is-i2c-hid-and-unbuilt.md issues/hardware/there-is-no-wifi.md issues/isolation/the-power-broker-authority-with-a-human-in-the-loop.md -issues/kernel/arm64-is-a-decision-nobody-has-made.md +8a277bb2^:issues/kernel/arm64-is-a-decision-nobody-has-made.md issues/kernel/cpu-time-is-a-band-and-not-a-reservation.md issues/kernel/every-driver-is-still-in-the-kernel.md issues/kernel/every-interrupt-lands-on-the-boot-cpu.md issues/kernel/logging-records-from-every-producer-and-a-kernel-that-waits-on-nobody.md issues/kernel/nothing-charges-kernel-memory-to-a-process.md issues/kernel/page-global-is-a-decision-nobody-has-made.md -issues/kernel/scheduler-policy-behavior-has-no-quantified-suite.md +b68e2328^:issues/kernel/scheduler-policy-behavior-has-no-quantified-suite.md issues/kernel/the-capability-end-state-is-twelve-answers.md issues/kernel/the-iommu-refuses-nothing-yet.md issues/kernel/the-kernel-still-parses-what-userland-writes.md diff --git a/issues/build/the-third-party-corpus-is-in-no-machine-read-ledger.md b/issues/build/the-third-party-corpus-is-in-no-machine-read-ledger.md index c529d98a565..9e9dd4ee00f 100644 --- a/issues/build/the-third-party-corpus-is-in-no-machine-read-ledger.md +++ b/issues/build/the-third-party-corpus-is-in-no-machine-read-ledger.md @@ -13,7 +13,7 @@ tracks **plus every third-party source corpus**"*. The corpus is `tests/testcases/` --- 365 tracked files, of which **363** are third-party across `tinycc/` and `pp_tcc/` (the other two are its own `LICENSE` -and a `system.toml` that is ours): TinyCC's `tests/tests2` and `tests/pp` under +and a `system.toml` that is ours): TinyCC's `tinycc/tests/tests2` and `tinycc/tests/pp` under LGPL-2.1 plus picoc under BSD-3-Clause. They are compiler *input* rather than linked code, so their terms do not reach this repository's own, but the attribution is still owed and @@ -25,7 +25,7 @@ what is whose *"with the counts it was established from"*. reads that licence's own per-population numbers, counts what `git` tracks under each population, and reds when they disagree; it also refuses a tracked file under the corpus that no population attributes, and the one name `NOTICE` says -is not to come back --- `tests/testcases/tinycc/46_grep.c`, "Copyright (C) 1980, +is not to come back --- `b2771acc^:tests/testcases/tinycc/46_grep.c`, "Copyright (C) 1980, DECUS", *"but not for profit"*, deleted rather than attributed on 2026-08-08. An arrival and a deletion are both caught. diff --git a/issues/build/the-tooling-is-a-review-prompt-and-three-workflows.md b/issues/build/the-tooling-is-a-review-prompt-and-three-workflows.md index d948239c163..2437736cc56 100644 --- a/issues/build/the-tooling-is-a-review-prompt-and-three-workflows.md +++ b/issues/build/the-tooling-is-a-review-prompt-and-three-workflows.md @@ -10,11 +10,11 @@ The rules a prompt can read off a branch move into `.claude/agents/reviewer.md`, and the gates that held them go. - A test is green and fast or it is deleted in the same pull request and filed; - then `src/redlist.rs`, `src/tiers.rs`, `src/durations.rs` and + then `src/redlist.rs`, `src/tiers.rs`, `35383398^:src/durations.rs` and `tests/test-durations` have no subject and go. - The toolchain is content-addressed by the four trees that produce it, one directory per hash, never mutated; then the sysroot claim, `src/buildlock.rs` and `src/worktree.rs` go. - The nine workflows become three — `pr`, `nightly`, `publish`; then - `src/mergehealth.rs`, `landing.yml`'s `abi-split` and `gate-stage` go, and the + `a5b25a75^:src/mergehealth.rs`, `landing.yml`'s `abi-split` and `gate-stage` go, and the ABI-lands-alone rule moves into the review prompt. diff --git a/issues/design-debt/mio-s-toyos-waker-never-drains-its-pipe.md b/issues/design-debt/mio-s-toyos-waker-never-drains-its-pipe.md index 947d698dd02..5faf47fdea6 100644 --- a/issues/design-debt/mio-s-toyos-waker-never-drains-its-pipe.md +++ b/issues/design-debt/mio-s-toyos-waker-never-drains-its-pipe.md @@ -6,10 +6,10 @@ opened: 2026-09-26 # mio's ToyOS waker never drains its pipe -`src/sys/toyos/waker.rs` on the mio fork (`ToyOSOrg/mio`, branch `toyos`) +`mio/src/sys/toyos/waker.rs` on the mio fork (`ToyOSOrg/mio`, branch `toyos`) writes a byte to a pipe per `wake()` and ignores a full pipe, and the selector registers the read end under the waker's token; nothing in -`src/sys/toyos/` ever reads that pipe. `toyos::wake` is the wake pipe the +`mio/src/sys/toyos/` ever reads that pipe. `toyos::wake` is the wake pipe the tree now has once (a `Bell` whose `take` empties it), and logd, soundd and `window::Waiter` use it; mio could not take it as a swap, because draining is the selector's to do on the waker's token and the selector has no such step. diff --git a/issues/design-debt/netd-spends-two-poller-slots-per-piped-connection.md b/issues/design-debt/netd-spends-two-poller-slots-per-piped-connection.md index 0d4736d64e5..23300c420f2 100644 --- a/issues/design-debt/netd-spends-two-poller-slots-per-piped-connection.md +++ b/issues/design-debt/netd-spends-two-poller-slots-per-piped-connection.md @@ -21,5 +21,5 @@ or more is held to 111. Exit condition: one registration per connection. netd's side of a connection is one kernel `Connection` object, which `read_source` and `write_source` both resolve (`kernel/src/object/ops.rs`), so one `OP_WATCH` -carries `READABLE | WRITABLE` (`kernel/src/inbox.rs`, `process_watch`), and +carries `READABLE | WRITABLE` (`kernel/src/inbox/mod.rs`, `process_watch`), and `POLL_HANDLES_PER_PIPED` is deleted. diff --git a/issues/design-debt/redesign-the-log-subsystem.md b/issues/design-debt/redesign-the-log-subsystem.md index 630050398ca..2e8034df0cc 100644 --- a/issues/design-debt/redesign-the-log-subsystem.md +++ b/issues/design-debt/redesign-the-log-subsystem.md @@ -2,7 +2,6 @@ status: open kind: track opened: 2026-08-08 -decided: 2026-08-19 --- # Redesign the log subsystem, and re-shape `kernel/src` @@ -22,7 +21,7 @@ work in a scheduler-adjacent path, and fails alone. The original question, recorded verbatim because it was the owner asking: *"should we redesign and rewrite the log subsystem and rethink if the current -file/folder structure of the kernel makes sense?"* (`kernel/src/log.rs`). What +file/folder structure of the kernel makes sense?"* (`c31e9f97^:kernel/src/log.rs`). What follows is the evidence that made it decidable. **The log subsystem, as it was when this was written.** Six places, no core: @@ -46,8 +45,8 @@ writers and readers never observe a torn record" (`kernel/src/log/shard.rs:1`). The record type is not the kernel's at all — `LogRecord` comes from `toyos_abi::log`, imported at `kernel/src/log/mod.rs:19` and filled at `:158`. The three files the table -calls the log's own are gone: `kernel/src/log.rs`, -`kernel/src/drivers/log_ring.rs` and `kernel/src/log_file.rs` are none of them +calls the log's own are gone: `c31e9f97^:kernel/src/log.rs`, +`ee8369c9^:kernel/src/drivers/log_ring.rs` and `9ca7631a^:kernel/src/log_file.rs` are none of them in the tree. The file sink is not a kernel module at all — `/system/bin/logd` is an ordinary user process, and "the kernel keeps the record ring and the console; every policy about files — where they go, what they are called, how many there @@ -100,7 +99,7 @@ Two smaller layout items ride the same answer. `usb_gate.rs` (242 lines) and call (`kernel/src/main.rs:34`, `:36`, `:408`, `:537`) and are never in an ordinary build, but they sit interleaved with production sources; the review's target is one -`kernel/src/gates/` directory so that what test machinery exists is auditable +`gates/` directory under `kernel/src/` so that what test machinery exists is auditable in one listing. And `input_merge_test` is the tell that pure logic is trapped in the kernel: the merge state machine (one held-set, one button-merge, both bounded) is host-testable with synthetic multi-source streams, after which the diff --git a/issues/design-debt/softbuffer-reads-a-toyos-window-behind-its-lock.md b/issues/design-debt/softbuffer-reads-a-toyos-window-behind-its-lock.md index 360185bc107..c6101db10c0 100644 --- a/issues/design-debt/softbuffer-reads-a-toyos-window-behind-its-lock.md +++ b/issues/design-debt/softbuffer-reads-a-toyos-window-behind-its-lock.md @@ -10,7 +10,7 @@ The winit backend keeps each `window::Window` in an `Arc>`, and the event loop takes the lock to read events: `poll_event(&mut self)` replaces the window's shared buffer on a resize. The raw window handle winit hands out is the address of that `Window`, and softbuffer's ToyOS backend -(`src/backends/toyos.rs` on `ToyOSOrg/softbuffer`) dereferences it without the +(`softbuffer/src/backends/toyos.rs` on `ToyOSOrg/softbuffer`) dereferences it without the lock to blit and present. A surface presented from a thread other than the event loop's therefore races the resize: a present into the buffer the loop just dropped. The toolkits in the tree present from the loop's own thread, so diff --git a/issues/design-debt/toyos-dns-decodes-what-a-published-crate-decodes.md b/issues/design-debt/toyos-dns-decodes-what-a-published-crate-decodes.md index 822cbb1f6a0..98f90fd5258 100644 --- a/issues/design-debt/toyos-dns-decodes-what-a-published-crate-decodes.md +++ b/issues/design-debt/toyos-dns-decodes-what-a-published-crate-decodes.md @@ -1,6 +1,6 @@ --- status: open -kind: design-debt +kind: defect opened: 2026-09-26 --- diff --git a/issues/design-debt/what-is-owed-on-file-size.md b/issues/design-debt/what-is-owed-on-file-size.md index 0398fe22577..306c88deed9 100644 --- a/issues/design-debt/what-is-owed-on-file-size.md +++ b/issues/design-debt/what-is-owed-on-file-size.md @@ -38,7 +38,7 @@ what survives it is a different question. **Answered 2026-08-24, in the review-completion wave:** -- `arch/syscall.rs` → `kernel/src/arch/syscall/` (12 files; `dispatch.rs` is +- `arch/syscall.rs` → `kernel/src/syscall/` (12 files; `dispatch.rs` is where every user pointer the ABI takes is decoded — the seam the note asked for). A move-proof regenerated every new file from the original's line ranges; no function body changed. The split made two facts visible and filed: diff --git a/issues/diagnostics/a-record-cannot-name-thread-zero.md b/issues/diagnostics/a-record-cannot-name-thread-zero.md index 36c1aea2835..3003c5330b0 100644 --- a/issues/diagnostics/a-record-cannot-name-thread-zero.md +++ b/issues/diagnostics/a-record-cannot-name-thread-zero.md @@ -23,7 +23,7 @@ every `tid=` in the T14 boot logs then committed: **738 `tid=0` against 49 `tid=1`** — the value the formatter drops is the one almost every line carries. The kernel's own sentinel is a third value again: `PerCpu::current_tid` is -`u32::MAX` when no thread is running (`kernel/src/arch/percpu.rs:85`), which the +`u32::MAX` when no thread is running (`kernel/src/arch/x86_64/percpu.rs:85`), which the formatter would render as `tid=4294967295` on every line a kernel thread logs. ## What is in the tree now diff --git a/issues/diagnostics/the-lanleasecase-boot-is-a-third-t14-flash-for-one-exit-code.md b/issues/diagnostics/the-lanleasecase-boot-is-a-third-t14-flash-for-one-exit-code.md index 59b5f0950e0..2b1db4a1aa5 100644 --- a/issues/diagnostics/the-lanleasecase-boot-is-a-third-t14-flash-for-one-exit-code.md +++ b/issues/diagnostics/the-lanleasecase-boot-is-a-third-t14-flash-for-one-exit-code.md @@ -37,7 +37,7 @@ the `lan_lease_report` metal row in `tests/toyos.rs` with `LANLEASECASE` and and netd's `--exit-with-lease` with `tests/e1000leasecase` and the `lan_lease_report` QEMU registration, the arm that proves the channel. -An earlier form of this arm, `--exit-with-phy-outcome` on `tests/lanphycase`, +An earlier form of this arm, `--exit-with-phy-outcome` on `d409139f^:tests/lanphycase`, ended right after the bring-up with the PHY's outcome; its codes are still `Verdict::NotLeased`'s, so a code read off one of its boots means what `toyos_i219::phy::Outcome` says. diff --git a/issues/filesystem/log-flush-retry-deadman-arm.md b/issues/filesystem/log-flush-retry-deadman-arm.md index 945fb975ca7..0b64c5d4e4f 100644 --- a/issues/filesystem/log-flush-retry-deadman-arm.md +++ b/issues/filesystem/log-flush-retry-deadman-arm.md @@ -37,6 +37,39 @@ The first boot of the same test passes, so the retry half is sound; what is unresolved is whether the second boot's refusal is the deadman it is supposed to stage or a different error arriving first, and the console does not say which. -**Exit condition.** The deadman boot produces the record the test reads, or the -test reads the record that boot actually produces — decided by which error -`SYS_FSYNC` returned, which is a value nothing on that console prints today. +## Two older ways it reddened, not re-seen since ROOT-in-memory + +`d5c2d9c9^:issues/boot-media/log-flush-retry-reds-two-ways-at-two-in-five.md` +recorded four ways `log_flush_retry` had been seen to red; `d5c2d9c9` deleted +it once ROOT-in-memory gave the `[hung]` arm's own mechanism — a stick going +offline while userland is still paged from it — a fix and six green runs. Two +of its other ways were not re-seen in those six runs, so neither is known fixed +by that work: + +- **The `[hung]` arm missing its "transport broke on SCSI" line.** One of that + file's two originally-recorded assertions: the wide run reds on + `the deadman never declared the volume failed` while the *alone* re-run of + the same boot reds instead on + `no "transport broke on SCSI" in the log, so the staged hung device never + met its recovery` — two different assertions from the same staged fault, + which was that file's reason for treating this as more than a scheduling + classification. +- **A boot timeout before `===READY===`.** Measured 2026-09-07 on three + separate points (`main` 71ed50cf, `metal-suite` 7f16914d and f63bce1d), each + run alone: `log_flush_retry: [qemu] Boot timed out waiting for + ===READY===`, with the boot never coming up at all — a third shape beside + the two assertion failures, seen before ROOT-in-memory and on a branch that + does not carry it. + +## Exit condition + +The deadman boot produces the record the test reads, or the test reads the +record that boot actually produces — decided by which error `SYS_FSYNC` +returned, which is a value nothing on that console prints today. + +And `log_flush_retry` is re-measured (wide and alone) enough times, on a tree +that carries ROOT-in-memory, to say whether either older mode still occurs; if +seen again, a `src/redlist.rs` row carries it with a measured rate, or it is +fixed at its owner (`kernel/src/drivers/xhci` for the transport line, the boot +harness for the timeout). If not seen in that many runs, that half closes with +the count that supports it. diff --git a/issues/hardware/anonymous-mmap-is-not-demand-paged.md b/issues/hardware/anonymous-mmap-is-not-demand-paged.md index cc54919521e..91029c7e12f 100644 --- a/issues/hardware/anonymous-mmap-is-not-demand-paged.md +++ b/issues/hardware/anonymous-mmap-is-not-demand-paged.md @@ -13,7 +13,7 @@ blast radius. **`sys_mmap` allocates and maps the whole region up front.** `PageAlloc::new` is called for the full rounded size before anything is mapped, and -`alloc_and_map` maps every 2 MiB page of it (`kernel/src/arch/syscall.rs`'s +`alloc_and_map` maps every 2 MiB page of it (`kernel/src/syscall/vm.rs`'s `sys_mmap`). So a first touch of a fresh anonymous mapping is an ordinary store and never a `#PF`, and a program that reserves a large region pays for all of it immediately. Measured: `syscall_cost` mapping 128 MiB and touching one byte per diff --git a/issues/hardware/tco2-sts-clearing-is-verified-on-qemu-only.md b/issues/hardware/tco2-sts-clearing-is-verified-on-qemu-only.md index bcda4615315..0934fc8a468 100644 --- a/issues/hardware/tco2-sts-clearing-is-verified-on-qemu-only.md +++ b/issues/hardware/tco2-sts-clearing-is-verified-on-qemu-only.md @@ -6,7 +6,7 @@ opened: 2026-09-06 # Whether writing `TCO2_STS` back clears it is verified on QEMU only -`kernel/src/drivers/watchdog.rs`'s `arm` writes `TCO_SECOND_TO_STS` and +`kernel/src/arch/x86_64/watchdog.rs`'s `arm` writes `TCO_SECOND_TO_STS` and `TCO_BOOT_STS` back so that a reset is reported by the boot after it and not by every boot after it. That the write clears them is established for QEMU, whose store masks both bits out (`hw/acpi/ich9_tco.c:167`). Whether a Tiger Lake-LP diff --git a/issues/isolation/a-provided-name-cannot-reach-an-undeclared-child.md b/issues/isolation/a-provided-name-cannot-reach-an-undeclared-child.md index 077028802ca..4d020edc898 100644 --- a/issues/isolation/a-provided-name-cannot-reach-an-undeclared-child.md +++ b/issues/isolation/a-provided-name-cannot-reach-an-undeclared-child.md @@ -17,7 +17,7 @@ The **direct** path still cannot, and the reason is no longer the ABI. `NAMESPACE_KEEP_ALL` (`toyos-abi/src/syscall.rs`), `Builder::keep_all` is its SDK spelling (`toyos/src/namespace.rs`), `sys_namespace_build` carries the base's whole entry set when the bit is set and refuses a bit it does not define -(`kernel/src/arch/syscall/ipc.rs`), and `endowment_denied`'s +(`kernel/src/syscall/ipc.rs`), and `endowment_denied`'s `the_base_plus_one_more_name` asserts both halves in a guest. **What is left is the std fork, and only the std lane can do it.** diff --git a/issues/isolation/a-reset-stops-xhci-and-leaves-every-claimed-pci-function-armed.md b/issues/isolation/a-reset-stops-xhci-and-leaves-every-claimed-pci-function-armed.md index 6e3d6fae364..c21224f5bfd 100644 --- a/issues/isolation/a-reset-stops-xhci-and-leaves-every-claimed-pci-function-armed.md +++ b/issues/isolation/a-reset-stops-xhci-and-leaves-every-claimed-pci-function-armed.md @@ -11,7 +11,7 @@ opened: 2026-09-16 register that ends the machine, and each stops every xHCI controller ([`stop::before_reset`]) before it does". Five paths reach those two: -- `sys_reboot` (`kernel/src/arch/syscall/machine.rs:113`) → `quiesce` (`:121`) +- `sys_reboot` (`kernel/src/syscall/machine.rs:113`) → `quiesce` (`:121`) → `acpi::reboot` (`:122`; `acpi.rs:283`) → `reset_now` (`acpi.rs:290`); - `sys_shutdown` (`machine.rs:103`) → `quiesce` (`:107`) → `acpi::shutdown` (`:108`; `acpi.rs:325`); diff --git a/issues/isolation/dtv-capacity-is-a-workload-bound.md b/issues/isolation/dtv-capacity-is-a-workload-bound.md index 22934e9fa4e..e6faf69bcec 100644 --- a/issues/isolation/dtv-capacity-is-a-workload-bound.md +++ b/issues/isolation/dtv-capacity-is-a-workload-bound.md @@ -46,6 +46,6 @@ argues it: a fixed bound over a quantity the workload sets is a defect rather than a policy by this tree's own rule, and this one's refusal has no recoverable form — the 65th TLS-carrying module is an `rtabort!` at an arbitrary point, not an error a `dlopen` caller can handle. The syscall split moved the refusal: it -is `kernel/src/arch/syscall/vm.rs`'s `tls_alloc_block`, not `arch/syscall.rs:2724`. +is `kernel/src/syscall/vm.rs`'s `tls_alloc_block`, not `arch/syscall.rs:2724`. Owed by the Ring-3 loader move, which is where the DTV stops being the kernel's to size. diff --git a/issues/kernel/a-120000-ms-boot-deadline-fired-132859-ms-late-on-the-t14.md b/issues/kernel/a-120000-ms-boot-deadline-fired-132859-ms-late-on-the-t14.md index 61a14ade298..0516d530fa0 100644 --- a/issues/kernel/a-120000-ms-boot-deadline-fired-132859-ms-late-on-the-t14.md +++ b/issues/kernel/a-120000-ms-boot-deadline-fired-132859-ms-late-on-the-t14.md @@ -9,7 +9,7 @@ opened: 2026-09-16 `kernel/src/deadline.rs` reads the bound off the parameter line (`claim`, `:150-162`), turns it into a TSC deadline once there is a clock (`start`, `:168-183`), and `poll` (`:194-200`) compares one relaxed load of `AT_TSC` -against `rdtsc` from the timer interrupt entry — `kernel/src/arch/idt/timer.rs:71` +against `rdtsc` from the timer interrupt entry — `kernel/src/arch/x86_64/idt/timer.rs:71` (`:79`) in Ring 0 and `:96` in Ring 3 — and calls `expire` (`:208-225`), which seals the record and writes the reset register through `acpi::reset_now`. `start` arms the other half of the same parameter at `:182`, diff --git a/issues/kernel/a-claim-spends-device-addresses-its-slot-never-gets-back.md b/issues/kernel/a-claim-spends-device-addresses-its-slot-never-gets-back.md index f606499c432..ddeed4e7fd8 100644 --- a/issues/kernel/a-claim-spends-device-addresses-its-slot-never-gets-back.md +++ b/issues/kernel/a-claim-spends-device-addresses-its-slot-never-gets-back.md @@ -8,7 +8,7 @@ opened: 2026-09-26 Every `SYS_DEVICE_DMA_ALLOC` grant of a claim is placed at a fresh address of its slot's domain (`kernel/src/pcidev/mod.rs`, `dma_alloc`), and the domain -never hands an address out twice (`kernel/src/iommu/vtd/table.rs`, +never hands an address out twice (`kernel/src/arch/x86_64/vtd/table.rs`, `Domain::reserve`). A holder bounded to `MAX_GRANT_TOTAL` per claim can still claim, allocate and die over and over — a service a supervisor restarts does exactly this — and each claim spends up to that much of the slot's addresses @@ -16,7 +16,7 @@ and the remapping tables under them, which are never freed. The domain running dry is a refusal (`ResourceExhausted`) and not a crash, but the slot is then dead for the rest of the boot, and the tables are memory nothing returns. Nor is running dry always a refusal: every table under a fresh address comes -from `Tables::alloc` (`kernel/src/iommu/vtd/table.rs`), whose +from `Tables::alloc` (`kernel/src/arch/x86_64/vtd/table.rs`), whose `expect("no physical memory for a remapping table")` panics the kernel when physical memory runs out first, and no second-level table a claim made is ever freed. diff --git a/issues/kernel/a-domains-addresses-are-never-given-back.md b/issues/kernel/a-domains-addresses-are-never-given-back.md index 6deb3a22547..009a8132a15 100644 --- a/issues/kernel/a-domains-addresses-are-never-given-back.md +++ b/issues/kernel/a-domains-addresses-are-never-given-back.md @@ -6,7 +6,7 @@ opened: 2026-09-03 # A domain's addresses are never given back -`Domain::reserve` (`kernel/src/iommu/vtd/table.rs:213-224`) is a monotonic +`Domain::reserve` (`kernel/src/arch/x86_64/vtd/table.rs:213-224`) is a monotonic bump: an address is never handed out twice, unmapped or not, and `unmap` returns no range. A driver that maps and unmaps repeatedly therefore consumes its domain's device address space for good. The display is the first such diff --git a/issues/kernel/a-double-fault-on-cpu-1-under-a-wide-suite.md b/issues/kernel/a-double-fault-on-cpu-1-under-a-wide-suite.md index 45940b00bc5..4c4522cc9a8 100644 --- a/issues/kernel/a-double-fault-on-cpu-1-under-a-wide-suite.md +++ b/issues/kernel/a-double-fault-on-cpu-1-under-a-wide-suite.md @@ -117,7 +117,7 @@ Pids come from `IdMap`, which starts at zero and never reuses; pid 1 is the **That pid excludes the idle stack, which is the only stack in this kernel whose overflow the CPU can turn into a `#DF`.** `percpu::current_pid`/`current_tid` are written on every context switch from the incoming task's id -(`kernel/src/hw.rs`), and the idle context's id is `None` +(`kernel/src/arch/x86_64/hw.rs`), and the idle context's id is `None` (`sched/driver::enter_idle_loop` sets both to `None` before it moves `rsp`). A live pid therefore means CPU 1 was running a task. The idle stack is the one with an unmapped page under it (`IDLE_GUARD_SIZE`, `guard_kernel_page`), and an diff --git a/issues/kernel/a-driver-is-tested-on-the-host-and-its-real-implementation-is-one-instruction-deep.md b/issues/kernel/a-driver-is-tested-on-the-host-and-its-real-implementation-is-one-instruction-deep.md index 1adf6297ea9..15850d30006 100644 --- a/issues/kernel/a-driver-is-tested-on-the-host-and-its-real-implementation-is-one-instruction-deep.md +++ b/issues/kernel/a-driver-is-tested-on-the-host-and-its-real-implementation-is-one-instruction-deep.md @@ -11,7 +11,7 @@ boot, the suite's flakiness is the host's load, and a stub of the hardware's own rules would have caught this month's hardware bugs before a machine did: a wait that checked its deadline only on an empty ring, a port acknowledgement that disables the port, a controller reset that is not what a device sees -(`issues/kernel/a-usb-wait-checks-its-deadline-only-on-an-empty-ring.md` and +(`46f4c14d^:issues/kernel/a-usb-wait-checks-its-deadline-only-on-an-empty-ring.md` and the reset ruling in `kernel/src/drivers/acpi.rs`). Owner direction, 2026-09-07: the logic inside every driver is tested on the host against stubs that implement the hardware's behavior, errors and unpredictability; QEMU keeps a diff --git a/issues/kernel/a-job-list-hangs-with-interrupts-on-and-the-deadline-ends-it.md b/issues/kernel/a-job-list-hangs-with-interrupts-on-and-the-deadline-ends-it.md index 8831cb4027f..00d6871659a 100644 --- a/issues/kernel/a-job-list-hangs-with-interrupts-on-and-the-deadline-ends-it.md +++ b/issues/kernel/a-job-list-hangs-with-interrupts-on-and-the-deadline-ends-it.md @@ -14,7 +14,7 @@ tip — came back after **187 s**, so its boot spent the whole **That narrows it past the hang this branch already fixed.** A deadline that fires is a machine where some CPU was still taking a timer interrupt, so this is not the all-CPUs-deaf shape of -`kernel/src/hw.rs`'s missing re-arm; and `crate::hardlockup` sealed nothing, so +`kernel/src/arch/x86_64/hw.rs`'s missing re-arm; and `crate::hardlockup` sealed nothing, so no CPU sat with `IF` clear for half the bound either. What is left is a CPU spinning with interrupts enabled, or a wait that never returns, while the timer goes on ticking — a lock nobody releases, a retry loop with no end, or a device @@ -22,7 +22,7 @@ wait that re-arms its own bound. It is intermittent **across job lists**: `ccorpus` had passed, `testcases-mkdir` hung in run 22, `metaldevicecase` in run 20 showed the same span of -`LOCK CONTENTION ... at src/vfs.rs:32` under stick writes. +`LOCK CONTENTION` at `kernel/src/vfs.rs:32` under stick writes. **The record exists and could not be read.** The deadline seals `EXPIRED` and the tail of the log ring into the black box, and the next loader pass prints it diff --git a/issues/kernel/a-page-in-the-device-refused-is-reported-as-a-segfault.md b/issues/kernel/a-page-in-the-device-refused-is-reported-as-a-segfault.md index 7b844ec4079..06060996819 100644 --- a/issues/kernel/a-page-in-the-device-refused-is-reported-as-a-segfault.md +++ b/issues/kernel/a-page-in-the-device-refused-is-reported-as-a-segfault.md @@ -11,7 +11,7 @@ one line and leaves the fault unhandled (`kernel/src/process.rs`, the `FileBacked` arm of the demand-page fill: "`` is backed by a file byte `` that the device would not read; leaving the fault unhandled"). The exception path then reports the program's own fault -(`kernel/src/arch/idt/exceptions.rs`, the `theirs` arm): `SEGFAULT tid=…: +(`kernel/src/arch/x86_64/idt/exceptions.rs`, the `theirs` arm): `SEGFAULT tid=…: execute unmapped address at …` for a text page, `read unmapped address` for data. The process dies as a program bug would, and its exit is the one a wild pointer gives, so a supervisor, a test or a user reading the crash @@ -32,7 +32,7 @@ from `/home` or `/boot`, and any file mapped from them, reaches it. ## Owner The demand-fault path in `kernel/src/process.rs` and the exception report in -`kernel/src/arch/idt/exceptions.rs`. +`kernel/src/arch/x86_64/idt/exceptions.rs`. ## Exit condition diff --git a/issues/kernel/a-processs-memory-is-a-byte-total-that-reads-zero-under-contention.md b/issues/kernel/a-processs-memory-is-a-byte-total-that-reads-zero-under-contention.md index c5c9ef9278e..f7a52d3b124 100644 --- a/issues/kernel/a-processs-memory-is-a-byte-total-that-reads-zero-under-contention.md +++ b/issues/kernel/a-processs-memory-is-a-byte-total-that-reads-zero-under-contention.md @@ -16,7 +16,7 @@ tree cannot do honestly today. a plateau.** `SYS_SYSINFO` carries a live per-process byte total: `demand_pages + mmap_regions -+ dynamic_tls_blocks + loaded_libs`, summed at `kernel/src/arch/syscall/machine.rs:146-156` ++ dynamic_tls_blocks + loaded_libs`, summed at `kernel/src/syscall/machine.rs:146-156` into the entry's `memory` word. A userland `mmap` becomes an `mmap_regions` entry, so a leaked thread stack is inside that number. **But the sum is taken under `try_lock`, and the failure arm writes `0`** (`machine.rs:155-157`). A @@ -29,7 +29,7 @@ the two fault counts. Subtracting frees from allocations would not give a live count even if `free_count` were exported, and it is not (`kernel/src/process.rs:502`, absent from the ABI struct) — **the two counters count different populations.** `alloc_count` is bumped at four sites including the demand-page fill -(`kernel/src/arch/syscall/vm.rs:117`, `:142`, `:374`, `kernel/src/process.rs:1432`); +(`kernel/src/syscall/vm.rs:117`, `:142`, `:374`, `kernel/src/process.rs:1432`); `free_count` at two, `munmap` and TLS teardown (`vm.rs:161`, `process.rs:189`). Demand-page release is never counted at all — `demand_pages` is cleared wholesale at teardown (`process.rs:965`) — so the difference folds in every demand fill ever diff --git a/issues/kernel/a-reclaimed-function-spends-a-remapping-entry-it-never-returns.md b/issues/kernel/a-reclaimed-function-spends-a-remapping-entry-it-never-returns.md index 5844d4f9635..d200855ab15 100644 --- a/issues/kernel/a-reclaimed-function-spends-a-remapping-entry-it-never-returns.md +++ b/issues/kernel/a-reclaimed-function-spends-a-remapping-entry-it-never-returns.md @@ -6,7 +6,7 @@ opened: 2026-09-24 # A re-claimed PCI function spends an interrupt remapping entry it never returns -`iommu::vtd::interrupt::allocate` (`kernel/src/iommu/vtd/interrupt.rs`) hands +`iommu::vtd::interrupt::allocate` (`kernel/src/arch/x86_64/vtd/interrupt.rs`) hands out the next entry of a 256-entry table (`ENTRIES`) by bumping `used`, and no path gives one back. Every `pcidev` claim arms MSI or MSI-X through `interrupt::msi`, so every claim of a function takes a new entry, and its diff --git a/issues/kernel/a-shared-boot-stopped-answering-and-no-capture-says-why.md b/issues/kernel/a-shared-boot-stopped-answering-and-no-capture-says-why.md index 487c84edfbc..c4e48715bc8 100644 --- a/issues/kernel/a-shared-boot-stopped-answering-and-no-capture-says-why.md +++ b/issues/kernel/a-shared-boot-stopped-answering-and-no-capture-says-why.md @@ -39,12 +39,12 @@ runs). Its guest prints and then passes. Those are the two parked counts and the reporter cadence the 2026-08-29 sighting read as proof of a wedged machine. A console read parks on a -10 ms re-poll (`CONSOLE_REPOLL`, `kernel/src/arch/syscall/io.rs`), so an idle +10 ms re-poll (`CONSOLE_REPOLL`, `kernel/src/syscall/io.rs`), so an idle shared boot waiting for its next command is parked at almost every sample. **Site: unknown.** The only candidate the surviving capture line supports is a `SYS_THREAD_JOIN` whose wake was lost: it parks on the target thread's own watch -at `Deadline::never()` (`kernel/src/arch/syscall/proc.rs:171`), and nothing else +at `Deadline::never()` (`kernel/src/syscall/proc.rs:171`), and nothing else would leave a whole boot idle straight after a sibling thread's clean exit. `issues/kernel/thread-exits-completion-post-is-the-second-one.md` owns that path's two posts. diff --git a/issues/kernel/a-zero-byte-pipe-write-wakes-the-readers-watch.md b/issues/kernel/a-zero-byte-pipe-write-wakes-the-readers-watch.md index d68c0a3d38a..5bae5e2a1de 100644 --- a/issues/kernel/a-zero-byte-pipe-write-wakes-the-readers-watch.md +++ b/issues/kernel/a-zero-byte-pipe-write-wakes-the-readers-watch.md @@ -6,9 +6,9 @@ opened: 2026-09-26 # A zero-byte pipe write wakes the reader's watch -`sys_write_nonblock` (`kernel/src/arch/syscall/io.rs`) wakes the pipe's +`sys_write_nonblock` (`kernel/src/syscall/io.rs`) wakes the pipe's readers after every write that returns `Ok(n)`, `n == 0` included, and -`complete_pending_for_event` (`kernel/src/inbox.rs`) completes a pending +`complete_pending_for_event` (`kernel/src/inbox/mod.rs`) completes a pending `READABLE` watch on that wake as ready without looking at the ring. So a zero-byte write, which moves nothing, completes the reader's watch as readable while the reader's `read` still answers `WouldBlock`. diff --git a/issues/kernel/an-ap-loads-the-idt-before-its-control-registers.md b/issues/kernel/an-ap-loads-the-idt-before-its-control-registers.md index 4640c282f23..810acf4483d 100644 --- a/issues/kernel/an-ap-loads-the-idt-before-its-control-registers.md +++ b/issues/kernel/an-ap-loads-the-idt-before-its-control-registers.md @@ -7,7 +7,7 @@ opened: 2026-09-07 # An AP loads the IDT before its control registers, so a fault there triple-faults `percpu::init_bsp` applies the control registers before it loads the IDT: every -entry stub in `kernel/src/arch/idt` saves SSE state, and `fxsave` without +entry stub in `kernel/src/arch/x86_64/idt` saves SSE state, and `fxsave` without `CR4.OSFXSR` is `#UD`, so a fault taken between the two would raise `#UD` inside its own handler, double-fault into the same stub, and reset the machine. That ordering is stated at the site. diff --git a/issues/kernel/every-random-byte-is-one-rdrand.md b/issues/kernel/every-random-byte-is-one-rdrand.md index 4b7a4be39c0..73ed637b3d9 100644 --- a/issues/kernel/every-random-byte-is-one-rdrand.md +++ b/issues/kernel/every-random-byte-is-one-rdrand.md @@ -6,7 +6,7 @@ opened: 2026-09-24 # Every random byte is one RDRAND, with no generator behind it -`SYS_RANDOM` (`kernel/src/arch/syscall/io.rs`) answers every request with +`SYS_RANDOM` (`kernel/src/syscall/io.rs`) answers every request with `RDRAND` values copied straight to the caller. The kernel has no random generator of its own. Every key sshd mints, every future TLS session and every nonce therefore rests on one instruction from one vendor. There is no second diff --git a/issues/kernel/every-wait-in-this-kernel-is-a-spin.md b/issues/kernel/every-wait-in-this-kernel-is-a-spin.md index 511185188d1..48c60b6eda1 100644 --- a/issues/kernel/every-wait-in-this-kernel-is-a-spin.md +++ b/issues/kernel/every-wait-in-this-kernel-is-a-spin.md @@ -9,7 +9,7 @@ opened: 2026-08-12 **The heading and the paragraph under it are the state at opening, not the state of the tree.** What exists now: the completion core, where every wait in the kernel rechecks one predicate and a waiter lends a watch to the object it -waits on (`kernel/src/completion/mod.rs:1-8`); typed durations; and a sleep +waits on (`aaddf38a^:kernel/src/completion/mod.rs:1-8`, since folded into `kernel/src/watch.rs`); typed durations; and a sleep lock a contender parks on — written, loom-driven, and still `#![allow(dead_code)]` "until a kernel static converts to it" (`kernel/src/sleeplock.rs:1-9`), because none has. What has not moved is the diff --git a/issues/kernel/io-uring-enter-trips-the-one-queue-invariant.md b/issues/kernel/io-uring-enter-trips-the-one-queue-invariant.md index 224de2d6a5d..e8cbe881fe2 100644 --- a/issues/kernel/io-uring-enter-trips-the-one-queue-invariant.md +++ b/issues/kernel/io-uring-enter-trips-the-one-queue-invariant.md @@ -92,7 +92,7 @@ it again should re-read this: the backtrace will not look like the one above. `WaitQueue::wake_one`/`wake_all` popped a waiter and cleared its flag as two steps, so a waiter withdrawing in between found `dequeue` empty and its own -flag still set; `toyos-sched/loom/tests/loom_ticket.rs`'s +flag still set; `aaddf38a^:toyos-sched/loom/tests/loom_ticket.rs`'s `cancel_and_wake_agree_on_who_won` reds on that schedule. Both clear under the list lock now. It is a hole in the primitive rather than the capture above's path — `scheduler::wake_sched` claims through `wake_direct`, and no kernel diff --git a/issues/kernel/nothing-asserts-that-a-claim-answers-no-configuration-write.md b/issues/kernel/nothing-asserts-that-a-claim-answers-no-configuration-write.md index 1bcb3c4c862..d8df7f2197e 100644 --- a/issues/kernel/nothing-asserts-that-a-claim-answers-no-configuration-write.md +++ b/issues/kernel/nothing-asserts-that-a-claim-answers-no-configuration-write.md @@ -7,7 +7,7 @@ opened: 2026-09-08 # Nothing asserts that a claim answers no configuration write `SYS_DEVICE_REG_WRITE` on a `RegTarget::PciConfig` target is refused -`NotSupported` in `kernel/src/arch/syscall/device.rs`, and no test in any tier +`NotSupported` in `kernel/src/syscall/device.rs`, and no test in any tier reads that refusal. It is what a handed-over MSI function's safety rests on: its message address and data are words of configuration space rather than a table in a BAR, so nothing is withheld from the holder and the whole of the boundary is diff --git a/issues/kernel/one-mapping-is-written-in-two-ledgers.md b/issues/kernel/one-mapping-is-written-in-two-ledgers.md index 6f1b931ce43..c10a12ac9c7 100644 --- a/issues/kernel/one-mapping-is-written-in-two-ledgers.md +++ b/issues/kernel/one-mapping-is-written-in-two-ledgers.md @@ -9,7 +9,7 @@ opened: 2026-08-15 A live `mmap` is recorded twice. `ProcessData::mmap_regions` (`kernel/src/process.rs`) holds an `MmapRegion` — address, length, and the `PageAlloc` that owns the physical memory. `AddressSpace::regions` -(`kernel/src/mm/paging.rs`) holds a `Region` at the same address with the same +(`kernel/src/arch/x86_64/paging.rs`) holds a `Region` at the same address with the same length — and that one is the source of truth for placement: `find_gap` reads it and nothing else. @@ -32,7 +32,7 @@ exactly the same defect, and no test would see it until a placement collided. the only ledger. What that has to answer first: - **Accounting.** `alloc_count`, `free_count` and `peak_memory` are summed over - `mmap_regions` at `kernel/src/arch/syscall.rs`, and `SYS_SYSINFO`'s per-process + `mmap_regions` at `kernel/src/syscall/machine.rs`, and `SYS_SYSINFO`'s per-process memory line sums `_pages` over it under a `try_lock` on the process data — a `try_lock` the crash report depends on, so that reader may not move to a lock it can block on. @@ -53,10 +53,9 @@ next person to add a placement path should find this before writing it. **2026-08-25, promoted to `defect`.** Both ledgers are still live and still agree only because two functions are read carefully: `ProcessData::mmap_regions` at `kernel/src/process.rs:742` and `AddressSpace::regions` at -`kernel/src/mm/paging.rs:546`. The paths in this file predate the syscall split — -the writers are now `kernel/src/arch/syscall/vm.rs` and the `SYS_SYSINFO` -accounting sum is `kernel/src/arch/syscall/machine.rs:266`, not -`kernel/src/arch/syscall.rs`. An invariant with no checker and an open extension +`kernel/src/arch/x86_64/paging.rs:546`. The writers are +`kernel/src/syscall/vm.rs`, and the `SYS_SYSINFO` accounting sum is +`kernel/src/syscall/machine.rs:266`. An invariant with no checker and an open extension point is a defect in the shape and it already produced one kernel panic; the consolidation this file specifies is the fix. Owed by whoever next adds a placement path or a fourth `sys_mmap` arm. diff --git a/issues/kernel/only-one-allocation-can-be-made-to-fail-and-only-at-one-site.md b/issues/kernel/only-one-allocation-can-be-made-to-fail-and-only-at-one-site.md index c27a9034089..44abceaabf4 100644 --- a/issues/kernel/only-one-allocation-can-be-made-to-fail-and-only-at-one-site.md +++ b/issues/kernel/only-one-allocation-can-be-made-to-fail-and-only-at-one-site.md @@ -15,9 +15,9 @@ is not the gap; the gap is below. **What exists, and why it is not enough.** `debug_action::HEAP_AT_CEILING_PAGE_ALIGNED` (a `pub const` in `toyos-abi/src/syscall.rs`'s `debug_action` module, `:709` and `:723`; -dispatched at `kernel/src/arch/syscall/dispatch.rs:538`) drives `debug_heap_alloc` at +dispatched at `kernel/src/syscall/dispatch.rs:538`) drives `debug_heap_alloc` at `MAX_HEAP_ALLOC` with 4096-byte alignment, which the page source cannot back; -`kernel/src/arch/syscall/debug.rs:25-27` reports the null as +`kernel/src/syscall/debug.rs:25-27` reports the null as `ResourceExhausted` rather than unwrapping it, and `tests/toyos-rust-tests/src/bin/heap_ceiling.rs:131` asserts exactly that. So a test-only actuator does exist, a shipped test does read it, and the claim that diff --git a/issues/kernel/page-global-is-a-decision-nobody-has-made.md b/issues/kernel/page-global-is-a-decision-nobody-has-made.md index 47d33f9ef6b..e43cfab717b 100644 --- a/issues/kernel/page-global-is-a-decision-nobody-has-made.md +++ b/issues/kernel/page-global-is-a-decision-nobody-has-made.md @@ -2,7 +2,6 @@ status: open kind: track opened: 2026-08-19 -decided: 2026-08-20 --- # There is no `PAGE_GLOBAL` anywhere, and the owner decided: turn it on @@ -18,7 +17,7 @@ the KVM instrument so the improvement is a number against a number. TCG cannot price it (tests/CLAUDE.md: the local guest has no such cache model), which is one more reason it rides the measured era. -`CR4.PGE` is absent from `kernel/src/arch/control_regs.rs`'s declaration and no +`CR4.PGE` is absent from `kernel/src/arch/x86_64/control_regs.rs`'s declaration and no `PAGE_GLOBAL` exists in `kernel/src/mm/`. With PCID active, every address space therefore caches a private copy of the kernel's direct map, and every `flush_tlb_all` — which is `INVPCID` all-context — throws away the kernel's own @@ -30,7 +29,7 @@ affected and this kernel has no KPTI, so global kernel pages are available. place, and the bits left out are as much of the declaration as the bits in — means `PGE`'s absence should be *stated* whichever way it goes. -**What the answer would have to revisit.** `kernel/src/mm/paging.rs` now derives +**What the answer would have to revisit.** `kernel/src/arch/x86_64/paging.rs` now derives every invalidation from the entry a write replaced and discharges it with `INVPCID` type 0 or `INVLPG`. Neither touches a global translation, which is sound only because no entry in this kernel is global; the module header says so diff --git a/issues/kernel/spawned-process-never-starts.md b/issues/kernel/spawned-process-never-starts.md index 2b86d023085..7ade9abb9a8 100644 --- a/issues/kernel/spawned-process-never-starts.md +++ b/issues/kernel/spawned-process-never-starts.md @@ -40,7 +40,7 @@ Shell 28 is the healthy control the same log offers: 95 syscalls, `spawn 3` paired with `waitpid 3`, and it went on from `ls /system/bin` to `free` and then `doom`. So neither a lost exit notification nor a missed wakeup is involved, and `sys_waitpid` registering on the park lot before it reads the table -(`kernel/src/arch/syscall.rs:1067`) is doing its job. +(`4a98107f^:kernel/src/arch/syscall.rs`; `SYS_WAITPID` is retired since) is doing its job. **Refuted, and it was the first hypothesis because it was a same-day change** (#129, `85a8433`): a child that takes a surface grab and dies without releasing diff --git a/issues/kernel/the-capability-end-state-is-twelve-answers.md b/issues/kernel/the-capability-end-state-is-twelve-answers.md index 968c16899fa..081358c1d49 100644 --- a/issues/kernel/the-capability-end-state-is-twelve-answers.md +++ b/issues/kernel/the-capability-end-state-is-twelve-answers.md @@ -32,25 +32,22 @@ taken, not a queue waiting on anybody. Four of the rulings are enforced in code, each at a site that demands a right where none was demanded before: `SYS_SHUTDOWN` takes a `SysCap` carrying -`Rights::POWER` (`kernel/src/arch/syscall/machine.rs:46-48`); `SYS_SYSINFO`'s +`Rights::POWER` (`kernel/src/syscall/machine.rs:46-48`); `SYS_SYSINFO`'s roster takes `Rights::ROSTER`, demanded only once the buffer has room for an -entry (`kernel/src/arch/syscall/machine.rs:84`, `:94`), spelled `roster` in +entry (`kernel/src/syscall/machine.rs:84`, `:94`), spelled `roster` in `toyos-manifest/src/lib.rs:80`; and `SYS_PROCESS_OPEN` takes `Rights::MANAGE` -(`kernel/src/arch/syscall/proc.rs:89-91`), which is what makes question 3's +(`kernel/src/syscall/proc.rs:89-91`), which is what makes question 3's "a pid is not authority" checkable — the ABI says so at the field (`toyos-abi/src/syscall.rs:1882-1884`). -**Every `kernel/src/arch/syscall.rs:NNN` citation below is a dead pointer.** -That file is not in the tree; the syscalls are `kernel/src/arch/syscall/`, -twelve files with `dispatch.rs` decoding every user pointer. Below this -paragraph the file is named on **20 lines**, carrying **19 fully-spelled -`kernel/src/arch/syscall.rs:NNN` citations** — the twentieth names the file with -no line number at all — plus the bare `` `:NNN` `` continuations hanging off -them, which are deliberately not counted: a bare continuation in this file may -equally belong to `kernel/src/process.rs` or `kernel/src/object/ops.rs`, so the -number would need a reading rather than a match. All of them are kept as the -sections they were taken from, to be re-taken rather than trusted — a split -moves every line, so re-pointing them needs a re-read, not arithmetic. +**Every `4a98107f^:kernel/src/arch/syscall.rs` citation below points into +history**: that one-file syscall layer is what `4a98107f` split, and the +syscalls are `kernel/src/syscall/` now, twelve files with `dispatch.rs` +decoding every user pointer. Its line numbers were taken earlier still, so +they and the bare `` `:NNN` `` continuations hanging off them — which may +equally belong to `kernel/src/process.rs` or `kernel/src/object/ops.rs` — are +kept as the sections they were taken from, to be re-taken rather than trusted: +a split moves every line, so re-pointing them needs a re-read, not arithmetic. ## 1. What constitutes authority? — COMMITTED @@ -64,7 +61,7 @@ koid into access to anything" (`kernel/src/object/mod.rs:74`), and an endowment label is "a *local name* in one process's own table and buys nothing to guess" (`toyos-abi/src/syscall.rs:315`). Every syscall states the right it needs at its own arm — `Rights::NONE` where the handle alone is the authority -(`kernel/src/arch/syscall.rs:309`, `:442`, `:1032`) — because "a right left +(`4a98107f^:kernel/src/arch/syscall.rs:309`, `:442`, `:1032`) — because "a right left unstated is a right each call site invents" (`toyos-abi/src/handle.rs:53`). The qualification is question 5's ambient set. @@ -84,10 +81,10 @@ era opened deliberately, never a retrofit. **Two sites disagree.** The root `CLAUDE.md`'s Capabilities paragraph says "a process holds exactly what its parent moved into it, and there is nothing it can name to get more". The dispatch does not: `SYS_OPEN` resolves any absolute path -against the machine's one VFS (`kernel/src/arch/syscall.rs:1233`), gated only by +against the machine's one VFS (`4a98107f^:kernel/src/arch/syscall.rs:1233`), gated only by a per-*mount* `user_may_modify` whose whole subject is protecting `/boot` (`kernel/src/vfs.rs:288`); `SYS_SPAWN` starts any binary on that filesystem by -path (`kernel/src/arch/syscall.rs:359`); `SYS_DLOPEN` loads any `.so` by path +path (`4a98107f^:kernel/src/arch/syscall.rs:359`); `SYS_DLOPEN` loads any `.so` by path (`:504`); `SYS_SHUTDOWN` powers the machine off with no handle and no right (`:328`). So the answer today is **no**, and the sentence in `CLAUDE.md` is true of kernel objects and false of the filesystem. @@ -105,7 +102,7 @@ closed. Identity-only, with one named exception that is itself gated. Every arm taking a pid: `SYS_GETPID` answers the caller's own -(`kernel/src/arch/syscall.rs:490`), and `SYS_PROCESS_OPEN` turns a pid into a +(`4a98107f^:kernel/src/arch/syscall.rs:490`), and `SYS_PROCESS_OPEN` turns a pid into a `Process` handle only when the caller also presents a `SysCap` carrying `Rights::MANAGE` (`:1602`), which the kernel mints once, for `/system/bin/init` (`kernel/src/loader/mod.rs:938`). `ProcessStats.pid` says so at the field: "Not @@ -113,10 +110,10 @@ authority — nothing takes a pid but `SYS_PROCESS_OPEN`, which takes a `SysCap` beside it" (`toyos-abi/src/syscall.rs:1803`). Tids are process-local names: `SYS_THREAD_JOIN` resolves through `thread_sched(caller, tid)` and `collect_thread_zombie(table, tid, parent_pid)`, both keyed on the caller's own -pid (`kernel/src/arch/syscall.rs:2393`, `kernel/src/process.rs:1412`, `:848`). +pid (`4a98107f^:kernel/src/arch/syscall.rs:2393`, `kernel/src/process.rs:1412`, `:848`). Four pid-addressed syscalls were deleted and their numbers retired rather than reused — 26 `SYS_WAITPID`, 33 `SYS_FIND_PID`, 37 `SYS_GRANT_SHARED`, 65 -`SYS_KILL` (`kernel/src/arch/syscall.rs:63`). +`SYS_KILL` (`4a98107f^:kernel/src/arch/syscall.rs:63`). ## 4. Can a process enumerate objects it lacks authority over? — RULED 2026-08-20, IMPLEMENTED 2026-08-22 @@ -124,7 +121,7 @@ reused — 26 `SYS_WAITPID`, 33 `SYS_FIND_PID`, 37 `SYS_GRANT_SHARED`, 65 `Rights::ROSTER` on a `SysCap` (`toyos-abi/src/handle.rs`), spelled `roster` in `toyos_manifest`'s `SYSCAP_RIGHTS` (`toyos-manifest/src/lib.rs`), demanded by `sys_sysinfo` before a single per-process entry is collected or written -(`kernel/src/arch/syscall.rs`), and endowed by `system.toml` to `toybox` — +(`4a98107f^:kernel/src/arch/syscall.rs`), and endowed by `system.toml` to `toybox` — which is what `/system/bin/ps` is under another name — exactly as `logread` is endowed to `logd`. The machine header the same call answers first stays ambient, which is question 5's committed set: `free`, netd's memory budget and the compositor's @@ -138,7 +135,7 @@ The rest of the object graph was clean when this was audited and is unchanged. No syscall lists another process's handles; a `Namespace` answers `lookup` and has no listing operation at all (`kernel/src/object/namespace.rs:59`); `SYS_ENDOWMENTS` answers the caller's own table -(`kernel/src/arch/syscall.rs:1707`); reading the machine's log is gated on +(`4a98107f^:kernel/src/arch/syscall.rs:1707`); reading the machine's log is gated on `Rights::LOG` (`:1683`) precisely because it is "every process's business and no process's right by default"; the per-kind object census is `SYS_DEBUG`, which a shipping kernel does not have (`:704`, `:792`). `SYS_READDIR` enumerates any @@ -172,7 +169,7 @@ table** can still reach: accumulators) but not the roster after it; - **object creation, which confers nothing over anything that exists** — `SYS_PIPE`, `SYS_PORT_CREATE` ("needs no right and grants none — a port with - no clients is not authority", `kernel/src/arch/syscall.rs:1731`), + no clients is not authority", `4a98107f^:kernel/src/arch/syscall.rs:1731`), `SYS_SHM_CREATE`, `SYS_INBOX_SETUP`; - **and four that reach past the process** — the whole filesystem by path (`SYS_OPEN`, `SYS_READDIR`, `SYS_DELETE`, `SYS_MKDIR`, `SYS_RMDIR`, @@ -193,7 +190,7 @@ Every path that puts an entry in a table was walked. The only rights-taking constructor a caller can reach is `HandleEntry::duplicate`, which refuses without `Rights::DUP` and refuses a set that is not `subset_of` the source's (`kernel/src/object/handle.rs:170`); `SYS_HANDLE_DUP` -(`kernel/src/arch/syscall.rs:2596`) and `SYS_HANDLE_DUP_AT` (`:2624`) both go +(`4a98107f^:kernel/src/arch/syscall.rs:2596`) and `SYS_HANDLE_DUP_AT` (`:2624`) both go through it, so a device claim — created with no `DUP` (`kernel/src/object/ops.rs:47`) — cannot be copied at either. The five `HandleEntry::new` sites are all fresh objects, never a re-rating of a held one: @@ -231,7 +228,7 @@ and nothing else is:** The child's cwd is not implicit: `SpawnArgs` states it (`cwd_ptr`/`cwd_len`), and the kernel starts the child there or refuses the spawn — `InvalidArgument` for a path that is not absolute, `NotFound` for one naming no directory — with -no default to the caller's (`spawn_cwd` in `kernel/src/arch/syscall/fs.rs`). +no default to the caller's (`spawn_cwd` in `kernel/src/syscall/fs.rs`). std states `Command::current_dir`, or the caller's own directory when there is none, and `tests/toyos-rust-tests/src/bin/spawn_cwd.rs` holds both routes to it. @@ -248,7 +245,7 @@ authority init decides per program" — the machine's only one is minted with `TRANSFER` at `kernel/src/loader/mod.rs:946`. The mechanisms are `SYS_HANDLE_SEND`/`SYS_HANDLE_RECV` over a `Connection`, where the connection and every handle must carry `TRANSFER`, no handle may be named twice, and the -connection may not be sent over itself (`kernel/src/arch/syscall.rs:2086`, +connection may not be sent over itself (`4a98107f^:kernel/src/arch/syscall.rs:2086`, `:2090`); and `SpawnArgs::endow`. Nothing else crosses a process boundary — the handles `install_buffer` writes are the kernel handing a claim's holder its own buffers, not a transfer (`kernel/src/object/device.rs:49`). @@ -264,7 +261,7 @@ needs it. **Nothing in the tree has decided it.** There is no `Thread` row in `kobject!` (`kernel/src/object/mod.rs:278`), so a thread is not something a handle can name. `SYS_THREAD_SPAWN` answers a bare `Tid` -(`kernel/src/arch/syscall.rs:2376`), `SYS_THREAD_JOIN` takes one and resolves it +(`4a98107f^:kernel/src/arch/syscall.rs:2376`), `SYS_THREAD_JOIN` takes one and resolves it only inside the caller's own process (`:2393`), there is no thread-kill and no cross-process thread operation, and a `Process` handle's `MANAGE` retires every thread at once (`kernel/src/process.rs:1960`). This is the shape that resulted @@ -287,14 +284,14 @@ a build-time fact rather than a runtime race. At boot the kernel mints one full-rights `SysCap` for `/system/bin/init` and nothing else can construct one (`kernel/src/loader/mod.rs:938`). init reads the manifest and calls `SYS_DEVICE_CLAIM` per declared class (`userland/init/src/main.rs:506`), which -demands `Rights::DEVICE` on a `SysCap` (`kernel/src/arch/syscall.rs:1627`) and +demands `Rights::DEVICE` on a `SysCap` (`4a98107f^:kernel/src/arch/syscall.rs:1627`) and takes the class exclusively (`kernel/src/device.rs:56`). The claim comes back **without `Rights::DUP`**, so endowing it is the only expressible form and init provably no longer holds it (`kernel/src/object/ops.rs:47`). Every device-driving syscall then presents that handle and the kernel checks the *class*, not merely the type: "a process holding the NIC has no more business setting the resolution than one holding nothing" -(`kernel/src/arch/syscall.rs:895`). `SYS_RT_ENTER` and `SYS_PROCESS_OPEN` are +(`4a98107f^:kernel/src/arch/syscall.rs:895`). `SYS_RT_ENTER` and `SYS_PROCESS_OPEN` are the same shape on `Rights::RT` and `Rights::MANAGE` (`:1655`, `:1602`), narrowed per program by `toyos_manifest::syscap_rights` (`toyos-manifest/src/lib.rs:73`) — `system.toml` grants exactly two, `logread` @@ -302,7 +299,7 @@ to `logd` and `rt` to `soundd`. One inconsistency, known and unobservable: three arms demand three different rights on the same claim handle — `Rights::WRITE` -(`kernel/src/arch/syscall.rs:902`), `Rights::READ` (`:1136`) and `Rights::NONE` +(`4a98107f^:kernel/src/arch/syscall.rs:902`), `Rights::READ` (`:1136`) and `Rights::NONE` (`:1032`). No claim handle can ever carry a narrower set, because narrowing needs `DUP` and a claim has none, so the three are the same test today. @@ -313,14 +310,14 @@ needs `DUP` and a claim has none, so the three are the same test today. no replace", and a narrower one is a *new* object built from an existing one (`kernel/src/object/namespace.rs:1`). `SYS_NAMESPACE_OPEN` demands `Rights::READ` on a namespace handle and there is no second place to ask -(`kernel/src/arch/syscall.rs:1861`); `SYS_NAMESPACE_BUILD` demands +(`4a98107f^:kernel/src/arch/syscall.rs:1861`); `SYS_NAMESPACE_BUILD` demands `Rights::TRANSFER` on every added connector and resolves kept names against the base before installing anything (`:1754`). A process with no `svc` endowment resolves no name at all, and there is no registry to fall back to: 85 `SYS_LISTEN` and 87 `SYS_CONNECT` are retired numbers -(`kernel/src/arch/syscall.rs:77`, `:78`). The **filesystem** path space is the +(`4a98107f^:kernel/src/arch/syscall.rs:77`, `:78`). The **filesystem** path space is the other thing the word could mean, and it is ambient process state -(`kernel/src/arch/syscall.rs:1234`) — questions 2 and 5 hold that half. +(`4a98107f^:kernel/src/arch/syscall.rs:1234`) — questions 2 and 5 hold that half. ## 12. Is CPU time an explicit schedulable/budget authority, or is process-level fair scheduling sufficient? — OPEN @@ -328,7 +325,7 @@ Held by `issues/kernel/cpu-time-is-a-band-and-not-a-reservation.md`, which already carries the commitment and the measurements; nothing is added here. Today the only CPU-time authority is a band: `SYS_RT_ENTER` puts the calling process in the real-time band on `Rights::RT` -(`kernel/src/arch/syscall.rs:1653`), with no budget, no period and no admission +(`4a98107f^:kernel/src/arch/syscall.rs:1653`), with no budget, no period and no admission — "no entity is promised anything a number can check", measured at 93.3 ms of audio starvation behind a fair storm. diff --git a/issues/kernel/the-deadline-header-promises-any-interrupt-and-only-the-timer-entry-polls.md b/issues/kernel/the-deadline-header-promises-any-interrupt-and-only-the-timer-entry-polls.md index bcea1f94302..6a53445ac9f 100644 --- a/issues/kernel/the-deadline-header-promises-any-interrupt-and-only-the-timer-entry-polls.md +++ b/issues/kernel/the-deadline-header-promises-any-interrupt-and-only-the-timer-entry-polls.md @@ -16,7 +16,7 @@ interrupt: it is polled from the timer entry." `poll` says the narrower thing (`deadline.rs:185-186`): "Whether this machine's bound has passed; the timer interrupt entry's, in both rings, and nothing else's." Its call sites are exactly two, both in -`kernel/src/arch/idt/timer.rs`: the Ring 0 naked entry's `call {deadline}` +`kernel/src/arch/x86_64/idt/timer.rs`: the Ring 0 naked entry's `call {deadline}` (`:71`, `deadline = sym crate::deadline::poll` at `:79`, placed there because "a CPU spinning on a ticket still takes this interrupt", `:61-63`) and `timer_handler`, which opens at `:92`: its first statement is diff --git a/issues/kernel/the-global-pipe-lock-spans-a-user-copy.md b/issues/kernel/the-global-pipe-lock-spans-a-user-copy.md index 1306bc2f8b5..a4ab14f071b 100644 --- a/issues/kernel/the-global-pipe-lock-spans-a-user-copy.md +++ b/issues/kernel/the-global-pipe-lock-spans-a-user-copy.md @@ -16,13 +16,13 @@ The bulk copy is inside that closure, not outside it. * `pipe::try_write` (`pipe.rs:244`) calls `backing.ring.write(buf.len(), |off, dst| buf.read_at(off, dst))` at `pipe.rs:254`, same acquisition. * `Ring::read`/`Ring::write` (`toyos-abi/src/ring.rs:141`, `:178`) hand the closure one or two contiguous runs; the closure is `UserBytesMut::write_at` / `UserBytes::read_at` (`kernel/src/user_ptr.rs:206`, `:164`), a `copy_nonoverlapping` of the whole run. -The size is bounded only by the ring: `PIPE_SIZE = PAGE_2M` (`pipe.rs:104`), `PAGE_2M = 2 * 1024 * 1024` (`toyos-userbound/src/span.rs:27`), and `capacity = total_size - size_of::()` (`ring.rs:60`) with `RingHeader` `#[repr(C, align(64))]` holding one `AtomicU32` (`ring.rs:24-27`) — so **2,097,088 bytes** is the largest single copy under the lock. Nothing above caps it: `SYS_READ`/`SYS_WRITE` pass the userland length straight through (`kernel/src/arch/syscall/dispatch.rs:110-116`), `object::ops::try_read` hands the full window to `pipe::try_read` (`kernel/src/object/ops.rs:339-340`), and the only other bound, `user_ptr::window` (`user_ptr.rs:269`), requires physical contiguity — which a demand-paged 2 MiB frame satisfies exactly. +The size is bounded only by the ring: `PIPE_SIZE = PAGE_2M` (`pipe.rs:104`), `PAGE_2M = 2 * 1024 * 1024` (`toyos-userbound/src/span.rs:27`), and `capacity = total_size - size_of::()` (`ring.rs:60`) with `RingHeader` `#[repr(C, align(64))]` holding one `AtomicU32` (`ring.rs:24-27`) — so **2,097,088 bytes** is the largest single copy under the lock. Nothing above caps it: `SYS_READ`/`SYS_WRITE` pass the userland length straight through (`kernel/src/syscall/dispatch.rs:110-116`), `object::ops::try_read` hands the full window to `pipe::try_read` (`kernel/src/object/ops.rs:339-340`), and the only other bound, `user_ptr::window` (`user_ptr.rs:269`), requires physical contiguity — which a demand-paged 2 MiB frame satisfies exactly. A pipe's **first** write is worse, because the page is allocated lazily under the same lock. `try_write` calls `pipe.back()` (`pipe.rs:250`), which calls `pmm::alloc_page(pmm::Category::Pipe)` (`pipe.rs:148`). That takes `BITMAP` nested inside `PIPES` (`kernel/src/mm/pmm.rs:221`), linearly scans up to the whole physical bitmap for a free frame (`pmm.rs:224-241`), and then `write_bytes(..., 0, PAGE_2M)` — a 2 MiB zeroing (`pmm.rs:233-237`) — before `Ring::new` and the user copy that follows it. ## What queues behind it -Everything pipe-shaped in the kernel goes through the same static: `create` (`pipe.rs:191`), `map_page` (`pipe.rs:206`), `has_data`/`has_space` (`pipe.rs:261`, `:267`) — which the inbox readiness predicate calls per registered source (`kernel/src/inbox.rs:767-768`) and the blocking read/write wrappers call per attempt (`kernel/src/arch/syscall/io.rs:76`, `:162`) — every `PipeReader`/`PipeWriter` clone and close (`pipe.rs:74-80`, `close_read`), and `add_inbox_watcher`/`remove_inbox_watcher` (`pipe.rs:338`, `:348`). A compositor client, netd, logd and the shell all contend the same word. +Everything pipe-shaped in the kernel goes through the same static: `create` (`pipe.rs:191`), `map_page` (`pipe.rs:206`), `has_data`/`has_space` (`pipe.rs:261`, `:267`) — which the inbox readiness predicate calls per registered source (`kernel/src/inbox/mod.rs:767-768`) and the blocking read/write wrappers call per attempt (`kernel/src/syscall/io.rs:76`, `:162`) — every `PipeReader`/`PipeWriter` clone and close (`pipe.rs:74-80`, `close_read`), and `add_inbox_watcher`/`remove_inbox_watcher` (`pipe.rs:338`, `:348`). A compositor client, netd, logd and the shell all contend the same word. ## Impact, stated at its real size diff --git a/issues/kernel/the-reset-word-is-spelled-twice.md b/issues/kernel/the-reset-word-is-spelled-twice.md index ef86d102e7e..5c561bb6dd1 100644 --- a/issues/kernel/the-reset-word-is-spelled-twice.md +++ b/issues/kernel/the-reset-word-is-spelled-twice.md @@ -6,7 +6,7 @@ opened: 2026-09-07 # The word a metal verdict turns on is spelled twice, and neither speller reads the other -`kernel/src/arch/syscall/machine.rs:86` writes `quiesce("Rebooting.")` and +`kernel/src/syscall/machine.rs:86` writes `quiesce("Rebooting.")` and `src/bootlog.rs:15` declares `REBOOTING: &str = "Rebooting."` as the last line a passing boot must leave. Two literals, no shared declaration: reword the kernel's and every metal run refuses with `the log's last line is … and not diff --git a/issues/kernel/the-split-window-tlb-cost-is-unpriced.md b/issues/kernel/the-split-window-tlb-cost-is-unpriced.md index 933715e4d21..3d0733abb65 100644 --- a/issues/kernel/the-split-window-tlb-cost-is-unpriced.md +++ b/issues/kernel/the-split-window-tlb-cost-is-unpriced.md @@ -11,7 +11,7 @@ W^X maps one 2 MiB window per binary at 4 KiB granularity — the window where the boot set's text can be write-protected and 0 % of its data can be made non-executable (measured 2026-08-20 over 20 binaries and 33 windows; the numbers and the rejected alternatives are in `WindowProt`'s doc comment in -`kernel/src/mm/paging.rs`). The design question is settled by those numbers. The +`kernel/src/arch/x86_64/paging.rs`). The design question is settled by those numbers. The *cost* is not measured, and this host cannot measure it. 512 4 KiB entries replace one 2 MiB entry for that window, so a program whose hot @@ -30,7 +30,7 @@ send it to 2 MiB-aligning `toyos-ld`'s segments, which was measured at +4 MiB of physical memory per process. **2026-08-25, promoted to `defect`.** Checked at the site: `WindowProt`'s doc -comment in `kernel/src/mm/paging.rs` carries the design measurement — 20 +comment in `kernel/src/arch/x86_64/paging.rs` carries the design measurement — 20 binaries, 33 windows, 20 mixed, 48.9 % and 0 % — and says nothing about what the 512 4 KiB entries cost the TLB, so the number lives nowhere in the tree. That makes it a measurement owed, in the same class as the AP control-register delta diff --git a/issues/kernel/thread-exits-completion-post-is-the-second-one.md b/issues/kernel/thread-exits-completion-post-is-the-second-one.md index ef1c9f40dbd..e7b9739b119 100644 --- a/issues/kernel/thread-exits-completion-post-is-the-second-one.md +++ b/issues/kernel/thread-exits-completion-post-is-the-second-one.md @@ -20,7 +20,7 @@ scheduler::exit_current(code); `exit_current` reaches `driver::pass(Dispose::Exit)`, and the pass after that one drops the task's payload through `Hw::release` -(`kernel/src/hw.rs`), which ends with `TaskHandle::publish_released` — +(`kernel/src/arch/x86_64/hw.rs`), which ends with `TaskHandle::publish_released` — and that posts `Gone(Closed)` **on the same watch**, which its own comment states in those words: *"the retirer is armed on this thread's own watch — the same subject a joiner uses, and the reason the release no longer needs a queue diff --git a/issues/kernel/toyos-runs-on-arm64.md b/issues/kernel/toyos-runs-on-arm64.md index abf5a9b7218..58be3881fd1 100644 --- a/issues/kernel/toyos-runs-on-arm64.md +++ b/issues/kernel/toyos-runs-on-arm64.md @@ -6,7 +6,7 @@ opened: 2026-09-26 # ToyOS runs on ARM64: QEMU `virt` first, ACPI only, EL1 only -Supersedes `issues/kernel/arm64-is-a-decision-nobody-has-made.md`, which is +Supersedes `8a277bb2^:issues/kernel/arm64-is-a-decision-nobody-has-made.md`, which is folded in below and deleted. It keeps that file's settled points: compile-time dispatch (one `cfg_attr(path)` module choice, no `dyn Arch`, no `Kernel`, ~20 distinct symbols behind ~145 `crate::arch::` paths), a 4 KiB granule with @@ -42,7 +42,7 @@ Every x86 guest on this host runs under TCG emulation instead — there is no target for now. - **The memory-model audit is stage 0's**, not discovered on real ARM hardware: the kernel's `Relaxed` orderings and `Mmio`'s barrier semantics are - real latent defects on x86 too (`issues/kernel/the-stops-no-lost-wake-claim-rests-on-x86-locked-rmws.md` + real latent defects on x86 too (`aaddf38a^:issues/kernel/the-stops-no-lost-wake-claim-rests-on-x86-locked-rmws.md` is one instance). - **`toyos-cc` gets no AArch64 backend yet.** Doom and tinycc stay x86-only until userland runs on ARM, and that is decided again then. @@ -88,7 +88,7 @@ code for hardware no ARM board in scope has. `sched/driver.rs:1019-1062`), `irq_census.rs` 19, `nmi_gate.rs` 16, `main.rs` 13, `drivers/serial.rs` 13, `hw.rs` 10, `blackbox.rs` 6, `iommu/vtd/table.rs` 6, `panic_console/mod.rs:1294` 1, `xhci/wait/mod.rs:36` 1. Userland plus -`toyos-abi`: 54 lines (`libc/memory.rs` 23, `toyos-abi/syscall.rs` 20). The +`toyos-abi`: 54 lines (`libc/memory.rs` 23, `toyos-abi/src/syscall.rs` 20). The rust fork's std has two naked-asm sites: `_start` (`rust/library/std/src/sys/pal/toyos/mod.rs:44`) and `__tls_get_addr` reading `fs:[8]` (`pal/toyos/tls.rs:43`). @@ -158,7 +158,7 @@ refuses anything but `EM_X86_64` (`toyos-elf/src/header.rs:24,75`). (`toyos-abi/src/syscall.rs:678,703`: `syscall` and `svc #0`). `toyos-sched` (8,099 lines) is pure behind `Machine`/`Hw` (`toyos-sched/src/hw.rs:88-158`: `now`, `set_timer`, `stop_timer`, -`irq_guard`, `halt`, `need_resched`, `switch`), with `kernel/src/hw.rs` as +`irq_guard`, `halt`, `need_resched`, `switch`), with `kernel/src/arch/x86_64/hw.rs` as the one x86 implementation and a simulator as the other. PCI is ECAM/MMIO-only (`drivers/pci.rs:134-154`), no `0xCF8`. NVMe, xHCI and virtio have no ISA dependence beyond TSC-based waits. The bootloader is the `uefi` @@ -258,7 +258,7 @@ Each stage names its exit; "measured" means a number from a run. `gate.rs` moves out of `arch/`. The MSI doorbell becomes one arch-provided constant. `IrqGuard`/`LogCommitGuard` become one arch primitive. The loom model owed by - `issues/kernel/the-stops-no-lost-wake-claim-rests-on-x86-locked-rmws.md` + `aaddf38a^:issues/kernel/the-stops-no-lost-wake-claim-rests-on-x86-locked-rmws.md` lands, and the `Mmio` barrier contract above is written and asserted. **Exit**: x86 builds and passes unchanged. `rg 'x86_64-unknown' src/` names one `Arch` table. diff --git a/issues/panic-path/a-fatal-event-stands-down-both-bounds-and-may-leave-nothing-to-end-the-machine.md b/issues/panic-path/a-fatal-event-stands-down-both-bounds-and-may-leave-nothing-to-end-the-machine.md index 25401d29336..dc6c2cff938 100644 --- a/issues/panic-path/a-fatal-event-stands-down-both-bounds-and-may-leave-nothing-to-end-the-machine.md +++ b/issues/panic-path/a-fatal-event-stands-down-both-bounds-and-may-leave-nothing-to-end-the-machine.md @@ -6,9 +6,9 @@ opened: 2026-09-14 # A fatal event stands down both bounds, and on a machine that cannot reset itself nothing is left to end the boot -`apic::halt_all_cpus` (`kernel/src/arch/apic.rs:228`) calls +`apic::halt_all_cpus` (`kernel/src/arch/x86_64/apic.rs:228`) calls `crate::hardlockup::stand_down()` and `crate::deadline::stand_down()` before it -holds the panel (`kernel/src/arch/apic.rs:235-236`). That is deliberate and the +holds the panel (`kernel/src/arch/x86_64/apic.rs:235-236`). That is deliberate and the deadline's own header says so — "a panic in progress, which is not a gap but a stand-down: `apic::halt_all_cpus` calls `stand_down` before it holds the panel, so a panic report is never replaced by an expiry" diff --git a/issues/panic-path/no-console-between-boot-and-terminal.md b/issues/panic-path/no-console-between-boot-and-terminal.md index 22bc38a1580..370a5b8da86 100644 --- a/issues/panic-path/no-console-between-boot-and-terminal.md +++ b/issues/panic-path/no-console-between-boot-and-terminal.md @@ -43,7 +43,7 @@ milliseconds later, and no key pauses it: `page_forever` is reached only from userland" is `/system/bin/logd`: the kernel keeps the record ring and the console and writes no file at all, logd owns `/log` and puts one file per boot there named for the wall clock, `src/build.rs`'s `every_boot_config_runs_logd` refuses a boot -config that omits it, and `kernel/src/log_file.rs` is deleted. Both ways of +config that omits it, and `9ca7631a^:kernel/src/log_file.rs` is deleted. Both ways of reading that file need what this window denies: pulling the stick takes the machine out of the session, and `cat /log/` from the desktop needs input, which is the case this was opened for — a dead keyboard and a dead TrackPoint, diff --git a/src/CLAUDE.md b/src/CLAUDE.md index 7d61612a283..8a8d04458ff 100644 --- a/src/CLAUDE.md +++ b/src/CLAUDE.md @@ -19,7 +19,7 @@ Loads when you read a file under `src/` — the root cargo project, package name ## The host's locks and slots -- **Sysroots are content-addressed** (`src/sysroot.rs`): one per key — the identity (`src/identity.rs`, so a comment is no change) of `toyos-abi/src`, `toyos/src`, `userland/libc/src` and their manifests, the std fork's `library/` and `src/bootstrap/`, and the compiler — at `rust/build/sysroots//`, made by whichever worktree first needs it and never written again. Every build compiles against its own key's, so two worktrees with different ABIs never refuse or wait for each other; the only shared step is the primary's compiler, which a sysroot build reads under the global lock in shared mode. A new key costs one std build of the three guest targets; `--worktree remove` sweeps the keys no worktree records. +- **Sysroots are content-addressed** (`src/sysroot.rs`): one per key — the identity (`src/identity.rs`, so a comment is no change) of `toyos-abi/src`, `toyos/src`, `userland/libc/src` and their manifests, the std fork's `rust/library/` and `rust/src/bootstrap/`, and the compiler — at `rust/build/sysroots//`, made by whichever worktree first needs it and never written again. Every build compiles against its own key's, so two worktrees with different ABIs never refuse or wait for each other; the only shared step is the primary's compiler, which a sysroot build reads under the global lock in shared mode. A new key costs one std build of the three guest targets; `--worktree remove` sweeps the keys no worktree records. - **The std fork is built per worktree, and nothing but the primary's own sync moves the primary's `rust/`.** A linked worktree's `rust/` becomes, on its first build, a git worktree of the primary's fork repository at the commit its tree pins — that is where the fork is edited, committed and pinned. A worktree whose fork `compiler/` differs from the one the primary built builds its own compiler, keyed by that source and placed beside the primary's without touching it. If that checkout later falls behind the commit its tree pins (a merge moved the pin), the build moves the checkout to it itself, fetching from the primary's repository first if it holds the commit, unless the checkout has local changes, which it refuses to move out from under. - `src/buildlock.rs` serialises the stateful phases in two scopes: `Global` (the primary's compiler and the rustup link — one directory in `.git/`, shared by every worktree) and `Worktree` (the crate-target cleans, and this worktree's std build). Only `./x.py` typed by hand in `rust/` escapes it. - **Never kill a build that has taken the global lock** — the kill removes the shell wrappers, not the bootstrap, which inherits the file descriptor and runs on regardless; a toolchain rebuild interrupted or unobserved this way can leave `stage2/bin` without a `cargo`. diff --git a/src/citations.rs b/src/citations.rs new file mode 100644 index 00000000000..b8d56d0c9c7 --- /dev/null +++ b/src/citations.rs @@ -0,0 +1,216 @@ +//! Every path the tracker and the `CLAUDE.md` files cite is a path this tree +//! holds. +//! +//! An issue names the site it is about by path, and so does every `CLAUDE.md`; +//! a path is the claim a reader checks first. Moving or deleting a file leaves +//! every citation of it pointing at nothing, so this reds on one, naming the +//! citing file, its line and the missing path. A move carries its citations in +//! the same merge. +//! +//! **What counts as a citation.** A whitespace-, backtick- or bracket-delimited +//! token that begins with a directory `git` tracks at the root of this tree — +//! `kernel/…`, `src/…`, `issues/…` — with a `:line` or `:first-last` suffix +//! allowed and trailing sentence punctuation dropped. It resolves if `git` +//! tracks it or tracks a file under it. A path `git` does not track is one a +//! clean checkout does not have, however it looks in this one. +//! +//! **What is not one**, each because another namespace spells the same text: +//! +//! - a token carrying a pattern — `*`, `{`, `<`, `…`, `$` — which names many +//! paths or none; +//! - `path:line:column`, which is a Rust panic location, relative to whichever +//! crate panicked, quoted from a capture; +//! - anything through a `target` directory, which is build output; +//! - `rust/…`, the std fork: `src/forkcheck.rs` governs it, and a linked +//! worktree's `rust/` is empty until its first build; +//! - `.cargo/…`: `.cargo/config.toml` is the name cargo reads in every +//! directory, so a mention of one is a kind of file. +//! +//! So a path in another repository is written with that repository's name in +//! front of it (`mio/src/sys/toyos/waker.rs`), a path that no longer exists is +//! written as the revision that held it (`^:src/durations.rs`), and a path +//! that does not exist yet is written relative to its crate (`arch/api.rs`). +//! None of the three begins with a root, so none is read. A crate-relative +//! citation of a real file (`sched/dump.rs`) is not read either — that is the +//! hole this scan leaves. +//! +//! Only its own tests read this, so it is not compiled into the build system. + +use std::collections::BTreeSet; +use std::path::Path; + +/// Roots whose citations this does not read, each for the reason the module +/// header gives. +const NOT_READ: &[&str] = &["rust", ".cargo"]; + +/// Characters that make a token a pattern rather than a path. +const PATTERN: &[char] = &['*', '{', '}', '<', '>', '…', '$']; + +/// Characters that end a token. +fn delimits(c: char) -> bool { + c.is_whitespace() || matches!(c, '`' | '(' | ')' | '[' | ']' | '"' | '|' | ',' | ';') +} + +/// Every file `git` tracks, repository-relative, and every directory above one. +pub fn tracked(root: &Path) -> BTreeSet { + let out = std::process::Command::new("git") + .args(["ls-files", "-z"]) + .current_dir(root) + .output() + .unwrap_or_else(|e| panic!("git ls-files: {e}")); + assert!(out.status.success(), "git ls-files failed: {}", String::from_utf8_lossy(&out.stderr)); + let mut all = BTreeSet::new(); + for file in String::from_utf8(out.stdout).expect("git ls-files is not UTF-8").split('\0') { + if file.is_empty() { + continue; + } + let mut at = 0; + while let Some(slash) = file[at..].find('/') { + all.insert(file[..at + slash].to_string()); + at += slash + 1; + } + all.insert(file.to_string()); + } + all +} + +/// The tracked directories at the root whose citations are read. +pub fn roots(tracked: &BTreeSet) -> BTreeSet { + tracked + .iter() + .filter_map(|p| p.split_once('/').map(|(root, _)| root.to_string())) + .filter(|root| !NOT_READ.contains(&root.as_str())) + .collect() +} + +/// Every path `line` cites, with its trailing `/` kept off. +pub fn cited(line: &str, roots: &BTreeSet) -> Vec { + let mut found = Vec::new(); + for token in line.split(delimits) { + if token.contains(PATTERN) { + continue; + } + let token = token.trim_end_matches(['.', '!', '?']); + let token = token.strip_suffix("'s").unwrap_or(token).trim_end_matches('\''); + let (path, suffix) = match token.split_once(':') { + Some((path, suffix)) => (path, Some(suffix)), + None => (token, None), + }; + if let Some(suffix) = suffix { + let numbers: Vec<&str> = suffix.trim_end_matches(':').split(':').collect(); + let numeric = |s: &str| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit() || c == '-'); + if numbers.len() >= 2 && numbers.iter().all(|n| numeric(n)) { + continue; + } + } + let Some((root, _)) = path.split_once('/') else { continue }; + if !roots.contains(root) || path.split('/').any(|segment| segment == "target") { + continue; + } + found.push(path.trim_end_matches('/').to_string()); + } + found +} + +/// Every `file:line: cites path` in `text` that `tracked` does not hold. +pub fn dead(file: &str, text: &str, roots: &BTreeSet, tracked: &BTreeSet) -> Vec { + let mut out = Vec::new(); + for (n, line) in text.lines().enumerate() { + for path in cited(line, roots) { + if !tracked.contains(&path) { + out.push(format!("{file}:{}: cites {path}, which this tree does not hold", n + 1)); + } + } + } + out +} + +/// The files whose citations are read: every tracked `.md` under `issues/`, and +/// every tracked `CLAUDE.md`. +pub fn citing(tracked: &BTreeSet) -> Vec { + tracked + .iter() + .filter(|p| { + (p.starts_with("issues/") && p.ends_with(".md")) + || p.rsplit('/').next() == Some("CLAUDE.md") + }) + .cloned() + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + fn repo_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + } + + fn set(items: &[&str]) -> BTreeSet { + items.iter().map(|s| s.to_string()).collect() + } + + /// **The gate.** Every path an issue or a `CLAUDE.md` cites resolves. + #[test] + fn every_path_the_tracker_and_the_claude_files_cite_exists() { + let root = repo_root(); + let tracked = tracked(&root); + let roots = roots(&tracked); + let files = citing(&tracked); + assert!( + files.iter().any(|f| f == "issues/README.md") && files.iter().any(|f| f == "CLAUDE.md"), + "the walk reached neither the tracker nor the root CLAUDE.md, so it reads nothing: {files:?}" + ); + assert!(roots.contains("kernel") && roots.contains("src") && roots.contains("issues"), "{roots:?}"); + let mut complaints = Vec::new(); + let mut read = 0; + for file in &files { + let text = std::fs::read_to_string(root.join(file)).unwrap_or_else(|e| panic!("read {file}: {e}")); + read += text.lines().map(|l| cited(l, &roots).len()).sum::(); + complaints.extend(dead(file, &text, &roots, &tracked)); + } + assert!(read > 500, "only {read} citations read across {} files; the scan is reading nothing", files.len()); + assert!( + complaints.is_empty(), + "{} citation(s) name a path this tree does not hold. Point each at where the file went, \ + or write a deleted one as the revision that held it (`^:`):\n{}", + complaints.len(), + complaints.join("\n"), + ); + } + + /// Teeth: a dead citation reds, naming the file, the line and the path; a + /// live one, a directory and a line-suffixed one do not. + #[test] + fn a_dead_citation_is_named_and_a_live_one_is_not() { + let tracked = set(&["kernel", "kernel/src", "kernel/src/vfs.rs", "issues", "issues/README.md"]); + let roots = roots(&tracked); + let text = "See `kernel/src/vfs.rs:32` and kernel/src/.\nThen `kernel/src/gone.rs`, and issues/README.md.\n"; + assert_eq!( + dead("issues/x/y.md", text, &roots, &tracked), + ["issues/x/y.md:2: cites kernel/src/gone.rs, which this tree does not hold"], + ); + } + + /// What another namespace spells is not read as ours. + #[test] + fn a_pattern_a_panic_location_build_output_and_a_foreign_path_are_not_citations() { + let roots = set(&["kernel", "src", "tests", "rust", ".cargo"]); + let roots = roots.into_iter().filter(|r| !NOT_READ.contains(&r.as_str())).collect(); + for line in [ + "PANIC: panicked at src/arch/tlb.rs:171:42:", + "LOCK CONTENTION: 50M spins at src/vfs.rs:32:18, ticket=38", + "`kernel/src/arch/idt/*.rs` and `tests/case/system.toml`", + "`kernel/src/{a,b}.rs`, `kernel/src/…`", + "`kernel/target` 318 MB, `userland/target`", + "the fork's `rust/src/bootstrap/` and a `.cargo/config.toml`", + "`mio/src/sys/toyos/waker.rs` and `d5c2d9c9^:src/durations.rs`", + "https://github.com/ToyOSOrg/ToyOS/blob/main/src/lib.rs", + ] { + assert!(cited(line, &roots).is_empty(), "{line:?} read as {:?}", cited(line, &roots)); + } + assert_eq!(cited("`src/redlist.rs`'s row, `kernel/src/a.rs:1-9`.", &roots), ["src/redlist.rs", "kernel/src/a.rs"]); + assert_eq!(cited("(src/x.rs) [kernel/y/](kernel/y/)", &roots), ["src/x.rs", "kernel/y", "kernel/y"]); + } +} diff --git a/src/lib.rs b/src/lib.rs index e44e0c0ced2..85fcd1b04ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,10 @@ pub mod bootlog; pub mod build; pub mod buildlock; pub mod ci; +/// The citation gate over the tracker and the `CLAUDE.md` files, read by +/// nothing but its own tests. +#[cfg(test)] +pub mod citations; pub mod clippy; pub mod compiler; /// What the untouched-disk gate compares a device against, in `tests/`. From 41ae518a2c7abb4023c396797119eea57ac37042 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 13:10:20 +0200 Subject: [PATCH 02/12] Negative control for the citation gate: a dead citation Reverted by the next commit. Planted to show the gate reds on a path this tree does not hold. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK --- issues/build/defect-events.md | 2 + src/sourcegate.rs | 293 ++++++++++++++++++++++++++++++++++ 2 files changed, 295 insertions(+) diff --git a/issues/build/defect-events.md b/issues/build/defect-events.md index 278dceceebf..77d385af312 100644 --- a/issues/build/defect-events.md +++ b/issues/build/defect-events.md @@ -208,3 +208,5 @@ ledger was written. the same guest work — so the emulator was not the weaker instrument here, it was very much the stronger one, and a class measured only there is not replicated by pointing faster silicon at it. + +Negative control: `kernel/src/no_such_module.rs` does not exist. diff --git a/src/sourcegate.rs b/src/sourcegate.rs index ba7fdb0624a..be1b845b427 100644 --- a/src/sourcegate.rs +++ b/src/sourcegate.rs @@ -1581,6 +1581,211 @@ fn every_rust_file() -> Vec<(String, String)> { .collect() } +// ── The arch layer names nothing above it ─────────────────────────────────── + +/// The kernel's architecture layer: every file under it is read. +#[cfg(test)] +const ARCH_TREE: &str = "kernel/src/arch/"; + +/// Where the kernel's root declares its modules and re-exports. +#[cfg(test)] +const KERNEL_ROOT: &str = "kernel/src/main.rs"; + +/// Every crate-root item a file under [`ARCH_TREE`] names that is neither the +/// architecture nor a root re-export of it: the layer reaching up into the +/// kernel it is meant to sit under. Each entry is recorded debt, owed by +/// `issues/kernel/the-arch-layer-names-the-kernel-above-it.md`, which is done +/// when this list is empty. +/// +/// **It only shrinks.** A name a file adds reds +/// `the_arch_layer_names_nothing_above_it`, and so does a name a row lists that +/// its file no longer spells, so no row outlives what it excused. +#[cfg(test)] +const ARCH_REACHES_UP: &[(&str, &[&str])] = &[]; + +/// Every `(0-based line, item)` for a crate-root item `text` names by path, in +/// a file whose module sits `depth` levels below the crate root: the segment +/// after `crate::` or `$crate::`, the first segment of each element of a +/// `crate::{…}` group over any number of lines, and the same after a `super::` +/// chain long enough to reach the root. A glob of the root is the item `*` and +/// a rename of it (`crate as k`) the item `crate as`, because what either +/// brings in is decided by resolution, which a line scan does not have. +/// Comments and string literals are not code ([`code_only`]). A macro reached +/// by textual scope (`#[macro_use]`) is not a path and is not read. +#[cfg(test)] +fn crate_items_named(text: &str, depth: usize) -> Vec<(usize, String)> { + let code: Vec = text.lines().map(code_only).collect::>().join("\n").chars().collect(); + let word = |c: char| c.is_ascii_alphanumeric() || c == '_'; + let line_of = |at: usize| code[..at].iter().filter(|c| **c == '\n').count(); + let skip_space = |mut at: usize| { + while code.get(at).is_some_and(|c| c.is_whitespace()) { + at += 1; + } + at + }; + // The identifier that begins at `at`, and where it ends. + let ident_at = |at: usize| -> Option<(String, usize)> { + if at.checked_sub(1).and_then(|j| code.get(j)).is_some_and(|c| word(*c)) { + return None; + } + let end = (at..).find(|&j| !code.get(j).is_some_and(|c| word(*c))).unwrap_or(at); + (end > at).then(|| (code[at..end].iter().collect(), end)) + }; + let colons = |at: usize| code.get(at..at + 2) == Some(&[':', ':'][..]); + // What follows the root, `at` being just past its `::`. + let after_root = |at: usize, found: &mut Vec<(usize, String)>| { + let at = skip_space(at); + match code.get(at) { + Some('*') => found.push((line_of(at), "*".to_string())), + Some('{') => { + let (mut depth, mut i, mut element) = (0usize, at, true); + while let Some(&c) = code.get(i) { + match c { + '{' => { + depth += 1; + element = depth == 1; + } + '}' => { + depth -= 1; + if depth == 0 { + break; + } + } + ',' if depth == 1 => element = true, + c if c.is_whitespace() => {} + _ => { + if element && depth == 1 { + match ident_at(i) { + Some((name, _)) if name != "self" => found.push((line_of(i), name)), + Some(_) => {} + None if c == '*' => found.push((line_of(i), "*".to_string())), + None => {} + } + } + element = false; + } + } + i += 1; + } + } + _ => { + if let Some((name, _)) = ident_at(at) { + found.push((line_of(at), name)); + } + } + } + }; + let mut found = Vec::new(); + let mut at = 0; + while at < code.len() { + let Some((name, end)) = ident_at(at) else { + at += 1; + continue; + }; + let after = skip_space(end); + if name == "crate" { + if colons(after) { + after_root(after + 2, &mut found); + } else if ident_at(after).is_some_and(|(next, _)| next == "as") { + found.push((line_of(at), "crate as".to_string())); + } + } else if name == "super" && !(at >= 2 && colons(at - 2)) { + let (mut supers, mut next) = (1, after); + while colons(next) { + match ident_at(skip_space(next + 2)) { + Some((s, e)) if s == "super" => { + supers += 1; + next = skip_space(e); + } + _ => break, + } + } + if supers == depth && colons(next) { + after_root(next + 2, &mut found); + } + } + at = end; + } + found +} + +/// How many modules deep the file at repository-relative `path` sits below +/// the kernel's crate root: `kernel/src/arch/mod.rs` is 1, +/// `kernel/src/arch/x86_64/tlb.rs` is 3. +#[cfg(test)] +fn module_depth(path: &str) -> usize { + let under = path.strip_prefix("kernel/src/").unwrap_or(path); + let parts = under.split('/').count(); + if under.ends_with("/mod.rs") { parts - 1 } else { parts } +} + +/// The names the kernel root re-exports from the architecture +/// (`pub(crate) use arch::hw;`, `use arch::{cpu, smp};`), so `crate::hw` is the +/// architecture naming itself through the root rather than reaching above it. +#[cfg(test)] +fn arch_aliases(root: &str) -> Vec { + let mut names = Vec::new(); + for line in root.lines() { + let code = code_only(line); + let Some(rest) = after_visibility(code.trim()).strip_prefix("use arch::") else { continue }; + let rest = rest.trim_end().trim_end_matches(';'); + let rest = rest.strip_prefix('{').and_then(|r| r.strip_suffix('}')).unwrap_or(rest); + for element in rest.split(',').map(str::trim).filter(|e| !e.is_empty()) { + let local = element.rsplit(" as ").next().unwrap_or(element); + names.push(local.rsplit("::").next().unwrap_or(local).trim().to_string()); + } + } + names +} + +/// Every red `rows` and `files` give: a crate-root item a file under +/// [`ARCH_TREE`] names and no row permits it, and a row entry its file no +/// longer spells. +#[cfg(test)] +fn arch_reach_complaints( + files: &[(String, String)], + rows: &[(&str, &[&str])], + aliases: &[String], +) -> Vec { + let mut complaints = Vec::new(); + for (i, (file, _)) in rows.iter().enumerate() { + if rows[..i].iter().any(|(earlier, _)| earlier == file) { + complaints.push(format!("{file} has two rows in the arch reach list; one row per file")); + } + if !files.iter().any(|(at, _)| at == file) { + complaints.push(format!( + "the arch reach list names {file}, and this tree holds no such file under {ARCH_TREE}" + )); + } + } + for (at, text) in files.iter().filter(|(at, _)| at.starts_with(ARCH_TREE)) { + let named: Vec<(usize, String)> = crate_items_named(text, module_depth(at)) + .into_iter() + .filter(|(_, item)| item != "arch" && !aliases.contains(item)) + .collect(); + let permitted: &[&str] = rows.iter().find(|(file, _)| file == at).map_or(&[], |(_, items)| items); + for (line, item) in &named { + if !permitted.contains(&item.as_str()) { + complaints.push(format!( + "{at}:{}: names `crate::{item}`, which is above the arch layer. Generic code \ + reaches the architecture and never the other way: pass in what it needs, \ + or move the code out of {ARCH_TREE}", + line + 1 + )); + } + } + for item in permitted { + if !named.iter().any(|(_, n)| n == item) { + complaints.push(format!( + "the arch reach list lets {at} name `crate::{item}`, and it no longer does: \ + delete the entry, the list only shrinks" + )); + } + } + } + complaints +} + /// The third-party C corpus, whose attribution is per *population* rather than /// per file: `tests/testcases/LICENSE` states how many files each of these /// directories holds, and `NOTICE` points at that file for the terms. @@ -1625,6 +1830,94 @@ fn digest(bytes: &[u8]) -> String { mod tests { use super::*; + /// **The arch layer names nothing above it**, but for [`ARCH_REACHES_UP`], + /// and that list names nothing its files no longer spell. + #[test] + fn the_arch_layer_names_nothing_above_it() { + let files = every_rust_file(); + let root = files + .iter() + .find(|(at, _)| at == KERNEL_ROOT) + .unwrap_or_else(|| panic!("the walk did not reach {KERNEL_ROOT}")); + let aliases = arch_aliases(&root.1); + assert!( + aliases.iter().any(|a| a == "hw"), + "{KERNEL_ROOT} re-exports `arch::hw` and the alias scan did not see it: {aliases:?}" + ); + let arch = files.iter().filter(|(at, _)| at.starts_with(ARCH_TREE)).count(); + assert!(arch > 40, "the walk found {arch} files under {ARCH_TREE}; it is reading no tree"); + let complaints = arch_reach_complaints(&files, ARCH_REACHES_UP, &aliases); + assert!(complaints.is_empty(), "{}", complaints.join("\n")); + } + + /// Teeth for the rule above: every spelling of a root item is read, and + /// what is not a path to one is not. + #[test] + fn every_spelling_of_a_crate_root_item_is_read() { + let names = |text: &str, depth: usize| -> Vec { + crate_items_named(text, depth).into_iter().map(|(_, item)| item).collect() + }; + assert_eq!(names("use crate::sched::driver;\n", 3), ["sched"]); + assert_eq!(names(" crate::log!(\"x\");\n", 3), ["log"]); + assert_eq!(names("const { $crate::irq_census::TOTAL }\n", 3), ["irq_census"]); + assert_eq!(names("use crate::{\n arch::x,\n mm::{a, b},\n self,\n process,\n};\n", 3), [ + "arch", "mm", "process" + ]); + assert_eq!(names("use crate :: { * };\nuse crate::*;\n", 3), ["*", "*"]); + assert_eq!(names("use crate as k;\n", 3), ["crate as"]); + // A `super::` chain that leaves the file's module names the root's item. + assert_eq!(names("use super::super::super::drivers::xhci;\n", 3), ["drivers"]); + assert_eq!(names("use super::super::{sched, mm};\n", 2), ["sched", "mm"]); + assert_eq!(names("use super::{apic, smp};\nuse super::super::percpu;\n", 3), Vec::::new()); + // Not code, not the root, not a path. + for text in [ + "// crate::sched is the scheduler\n", + "let s = \"crate::sched\";\n", + "pub(crate) fn f() {}\npub(super) fn g() {}\n", + "use mycrate::sched;\nuse other_crate::x;\n", + "use self::super::x;\n", + ] { + assert_eq!(names(text, 3), Vec::::new(), "{text:?}"); + } + assert_eq!(module_depth("kernel/src/arch/mod.rs"), 1); + assert_eq!(module_depth("kernel/src/arch/x86_64/mod.rs"), 2); + assert_eq!(module_depth("kernel/src/arch/x86_64/tlb.rs"), 3); + assert_eq!(module_depth("kernel/src/arch/x86_64/idt/timer.rs"), 4); + assert_eq!( + arch_aliases("use arch::{cpu, percpu as p, smp};\npub(crate) use arch::hw;\nuse drivers::acpi;\n"), + ["cpu", "p", "smp", "hw"] + ); + } + + /// Teeth for the list: a new reach reds naming the file, the line and the + /// item; a listed one does not; a listed one the file stopped spelling reds; + /// and a root alias of the architecture is the architecture. + #[test] + fn a_new_reach_out_of_arch_is_red_and_the_list_cannot_rot() { + let file = |at: &str, text: &str| (at.to_string(), text.to_string()); + let files = [ + file("kernel/src/arch/x86_64/tlb.rs", "use crate::time::Duration;\nuse crate::drivers::xhci;\n"), + file("kernel/src/arch/x86_64/idt/timer.rs", "use crate::hw::HW;\nuse crate::arch::apic;\n"), + file("kernel/src/sched/driver.rs", "use crate::drivers::xhci;\n"), + ]; + let aliases = vec!["hw".to_string()]; + let said = arch_reach_complaints(&files, &[("kernel/src/arch/x86_64/tlb.rs", &["time"])], &aliases); + assert_eq!(said.len(), 1, "{said:?}"); + assert!(said[0].starts_with("kernel/src/arch/x86_64/tlb.rs:2: names `crate::drivers`"), "{said:?}"); + + let listed = arch_reach_complaints(&files, &[("kernel/src/arch/x86_64/tlb.rs", &["drivers", "time"])], &aliases); + assert!(listed.is_empty(), "{listed:?}"); + + let stale = arch_reach_complaints( + &files, + &[("kernel/src/arch/x86_64/tlb.rs", &["drivers", "time", "sched"]), ("kernel/src/arch/gone.rs", &["log"])], + &aliases, + ); + assert_eq!(stale.len(), 2, "{stale:?}"); + assert!(stale.iter().any(|s| s.contains("kernel/src/arch/gone.rs")), "{stale:?}"); + assert!(stale.iter().any(|s| s.contains("`crate::sched`") && s.contains("no longer")), "{stale:?}"); + } + /// **The architecture rules hold over the whole repository**, and no place /// a rule names is a place that no longer holds a needle — a row nobody /// needs any more is a permission nobody re-argued. From 98b3545edce7192e3a87851cbb3bf775141454b5 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 13:10:41 +0200 Subject: [PATCH 03/12] Revert "Negative control for the citation gate: a dead citation" This reverts commit 41ae518a2c7abb4023c396797119eea57ac37042. --- issues/build/defect-events.md | 2 - src/sourcegate.rs | 293 ---------------------------------- 2 files changed, 295 deletions(-) diff --git a/issues/build/defect-events.md b/issues/build/defect-events.md index 77d385af312..278dceceebf 100644 --- a/issues/build/defect-events.md +++ b/issues/build/defect-events.md @@ -208,5 +208,3 @@ ledger was written. the same guest work — so the emulator was not the weaker instrument here, it was very much the stronger one, and a class measured only there is not replicated by pointing faster silicon at it. - -Negative control: `kernel/src/no_such_module.rs` does not exist. diff --git a/src/sourcegate.rs b/src/sourcegate.rs index be1b845b427..ba7fdb0624a 100644 --- a/src/sourcegate.rs +++ b/src/sourcegate.rs @@ -1581,211 +1581,6 @@ fn every_rust_file() -> Vec<(String, String)> { .collect() } -// ── The arch layer names nothing above it ─────────────────────────────────── - -/// The kernel's architecture layer: every file under it is read. -#[cfg(test)] -const ARCH_TREE: &str = "kernel/src/arch/"; - -/// Where the kernel's root declares its modules and re-exports. -#[cfg(test)] -const KERNEL_ROOT: &str = "kernel/src/main.rs"; - -/// Every crate-root item a file under [`ARCH_TREE`] names that is neither the -/// architecture nor a root re-export of it: the layer reaching up into the -/// kernel it is meant to sit under. Each entry is recorded debt, owed by -/// `issues/kernel/the-arch-layer-names-the-kernel-above-it.md`, which is done -/// when this list is empty. -/// -/// **It only shrinks.** A name a file adds reds -/// `the_arch_layer_names_nothing_above_it`, and so does a name a row lists that -/// its file no longer spells, so no row outlives what it excused. -#[cfg(test)] -const ARCH_REACHES_UP: &[(&str, &[&str])] = &[]; - -/// Every `(0-based line, item)` for a crate-root item `text` names by path, in -/// a file whose module sits `depth` levels below the crate root: the segment -/// after `crate::` or `$crate::`, the first segment of each element of a -/// `crate::{…}` group over any number of lines, and the same after a `super::` -/// chain long enough to reach the root. A glob of the root is the item `*` and -/// a rename of it (`crate as k`) the item `crate as`, because what either -/// brings in is decided by resolution, which a line scan does not have. -/// Comments and string literals are not code ([`code_only`]). A macro reached -/// by textual scope (`#[macro_use]`) is not a path and is not read. -#[cfg(test)] -fn crate_items_named(text: &str, depth: usize) -> Vec<(usize, String)> { - let code: Vec = text.lines().map(code_only).collect::>().join("\n").chars().collect(); - let word = |c: char| c.is_ascii_alphanumeric() || c == '_'; - let line_of = |at: usize| code[..at].iter().filter(|c| **c == '\n').count(); - let skip_space = |mut at: usize| { - while code.get(at).is_some_and(|c| c.is_whitespace()) { - at += 1; - } - at - }; - // The identifier that begins at `at`, and where it ends. - let ident_at = |at: usize| -> Option<(String, usize)> { - if at.checked_sub(1).and_then(|j| code.get(j)).is_some_and(|c| word(*c)) { - return None; - } - let end = (at..).find(|&j| !code.get(j).is_some_and(|c| word(*c))).unwrap_or(at); - (end > at).then(|| (code[at..end].iter().collect(), end)) - }; - let colons = |at: usize| code.get(at..at + 2) == Some(&[':', ':'][..]); - // What follows the root, `at` being just past its `::`. - let after_root = |at: usize, found: &mut Vec<(usize, String)>| { - let at = skip_space(at); - match code.get(at) { - Some('*') => found.push((line_of(at), "*".to_string())), - Some('{') => { - let (mut depth, mut i, mut element) = (0usize, at, true); - while let Some(&c) = code.get(i) { - match c { - '{' => { - depth += 1; - element = depth == 1; - } - '}' => { - depth -= 1; - if depth == 0 { - break; - } - } - ',' if depth == 1 => element = true, - c if c.is_whitespace() => {} - _ => { - if element && depth == 1 { - match ident_at(i) { - Some((name, _)) if name != "self" => found.push((line_of(i), name)), - Some(_) => {} - None if c == '*' => found.push((line_of(i), "*".to_string())), - None => {} - } - } - element = false; - } - } - i += 1; - } - } - _ => { - if let Some((name, _)) = ident_at(at) { - found.push((line_of(at), name)); - } - } - } - }; - let mut found = Vec::new(); - let mut at = 0; - while at < code.len() { - let Some((name, end)) = ident_at(at) else { - at += 1; - continue; - }; - let after = skip_space(end); - if name == "crate" { - if colons(after) { - after_root(after + 2, &mut found); - } else if ident_at(after).is_some_and(|(next, _)| next == "as") { - found.push((line_of(at), "crate as".to_string())); - } - } else if name == "super" && !(at >= 2 && colons(at - 2)) { - let (mut supers, mut next) = (1, after); - while colons(next) { - match ident_at(skip_space(next + 2)) { - Some((s, e)) if s == "super" => { - supers += 1; - next = skip_space(e); - } - _ => break, - } - } - if supers == depth && colons(next) { - after_root(next + 2, &mut found); - } - } - at = end; - } - found -} - -/// How many modules deep the file at repository-relative `path` sits below -/// the kernel's crate root: `kernel/src/arch/mod.rs` is 1, -/// `kernel/src/arch/x86_64/tlb.rs` is 3. -#[cfg(test)] -fn module_depth(path: &str) -> usize { - let under = path.strip_prefix("kernel/src/").unwrap_or(path); - let parts = under.split('/').count(); - if under.ends_with("/mod.rs") { parts - 1 } else { parts } -} - -/// The names the kernel root re-exports from the architecture -/// (`pub(crate) use arch::hw;`, `use arch::{cpu, smp};`), so `crate::hw` is the -/// architecture naming itself through the root rather than reaching above it. -#[cfg(test)] -fn arch_aliases(root: &str) -> Vec { - let mut names = Vec::new(); - for line in root.lines() { - let code = code_only(line); - let Some(rest) = after_visibility(code.trim()).strip_prefix("use arch::") else { continue }; - let rest = rest.trim_end().trim_end_matches(';'); - let rest = rest.strip_prefix('{').and_then(|r| r.strip_suffix('}')).unwrap_or(rest); - for element in rest.split(',').map(str::trim).filter(|e| !e.is_empty()) { - let local = element.rsplit(" as ").next().unwrap_or(element); - names.push(local.rsplit("::").next().unwrap_or(local).trim().to_string()); - } - } - names -} - -/// Every red `rows` and `files` give: a crate-root item a file under -/// [`ARCH_TREE`] names and no row permits it, and a row entry its file no -/// longer spells. -#[cfg(test)] -fn arch_reach_complaints( - files: &[(String, String)], - rows: &[(&str, &[&str])], - aliases: &[String], -) -> Vec { - let mut complaints = Vec::new(); - for (i, (file, _)) in rows.iter().enumerate() { - if rows[..i].iter().any(|(earlier, _)| earlier == file) { - complaints.push(format!("{file} has two rows in the arch reach list; one row per file")); - } - if !files.iter().any(|(at, _)| at == file) { - complaints.push(format!( - "the arch reach list names {file}, and this tree holds no such file under {ARCH_TREE}" - )); - } - } - for (at, text) in files.iter().filter(|(at, _)| at.starts_with(ARCH_TREE)) { - let named: Vec<(usize, String)> = crate_items_named(text, module_depth(at)) - .into_iter() - .filter(|(_, item)| item != "arch" && !aliases.contains(item)) - .collect(); - let permitted: &[&str] = rows.iter().find(|(file, _)| file == at).map_or(&[], |(_, items)| items); - for (line, item) in &named { - if !permitted.contains(&item.as_str()) { - complaints.push(format!( - "{at}:{}: names `crate::{item}`, which is above the arch layer. Generic code \ - reaches the architecture and never the other way: pass in what it needs, \ - or move the code out of {ARCH_TREE}", - line + 1 - )); - } - } - for item in permitted { - if !named.iter().any(|(_, n)| n == item) { - complaints.push(format!( - "the arch reach list lets {at} name `crate::{item}`, and it no longer does: \ - delete the entry, the list only shrinks" - )); - } - } - } - complaints -} - /// The third-party C corpus, whose attribution is per *population* rather than /// per file: `tests/testcases/LICENSE` states how many files each of these /// directories holds, and `NOTICE` points at that file for the terms. @@ -1830,94 +1625,6 @@ fn digest(bytes: &[u8]) -> String { mod tests { use super::*; - /// **The arch layer names nothing above it**, but for [`ARCH_REACHES_UP`], - /// and that list names nothing its files no longer spell. - #[test] - fn the_arch_layer_names_nothing_above_it() { - let files = every_rust_file(); - let root = files - .iter() - .find(|(at, _)| at == KERNEL_ROOT) - .unwrap_or_else(|| panic!("the walk did not reach {KERNEL_ROOT}")); - let aliases = arch_aliases(&root.1); - assert!( - aliases.iter().any(|a| a == "hw"), - "{KERNEL_ROOT} re-exports `arch::hw` and the alias scan did not see it: {aliases:?}" - ); - let arch = files.iter().filter(|(at, _)| at.starts_with(ARCH_TREE)).count(); - assert!(arch > 40, "the walk found {arch} files under {ARCH_TREE}; it is reading no tree"); - let complaints = arch_reach_complaints(&files, ARCH_REACHES_UP, &aliases); - assert!(complaints.is_empty(), "{}", complaints.join("\n")); - } - - /// Teeth for the rule above: every spelling of a root item is read, and - /// what is not a path to one is not. - #[test] - fn every_spelling_of_a_crate_root_item_is_read() { - let names = |text: &str, depth: usize| -> Vec { - crate_items_named(text, depth).into_iter().map(|(_, item)| item).collect() - }; - assert_eq!(names("use crate::sched::driver;\n", 3), ["sched"]); - assert_eq!(names(" crate::log!(\"x\");\n", 3), ["log"]); - assert_eq!(names("const { $crate::irq_census::TOTAL }\n", 3), ["irq_census"]); - assert_eq!(names("use crate::{\n arch::x,\n mm::{a, b},\n self,\n process,\n};\n", 3), [ - "arch", "mm", "process" - ]); - assert_eq!(names("use crate :: { * };\nuse crate::*;\n", 3), ["*", "*"]); - assert_eq!(names("use crate as k;\n", 3), ["crate as"]); - // A `super::` chain that leaves the file's module names the root's item. - assert_eq!(names("use super::super::super::drivers::xhci;\n", 3), ["drivers"]); - assert_eq!(names("use super::super::{sched, mm};\n", 2), ["sched", "mm"]); - assert_eq!(names("use super::{apic, smp};\nuse super::super::percpu;\n", 3), Vec::::new()); - // Not code, not the root, not a path. - for text in [ - "// crate::sched is the scheduler\n", - "let s = \"crate::sched\";\n", - "pub(crate) fn f() {}\npub(super) fn g() {}\n", - "use mycrate::sched;\nuse other_crate::x;\n", - "use self::super::x;\n", - ] { - assert_eq!(names(text, 3), Vec::::new(), "{text:?}"); - } - assert_eq!(module_depth("kernel/src/arch/mod.rs"), 1); - assert_eq!(module_depth("kernel/src/arch/x86_64/mod.rs"), 2); - assert_eq!(module_depth("kernel/src/arch/x86_64/tlb.rs"), 3); - assert_eq!(module_depth("kernel/src/arch/x86_64/idt/timer.rs"), 4); - assert_eq!( - arch_aliases("use arch::{cpu, percpu as p, smp};\npub(crate) use arch::hw;\nuse drivers::acpi;\n"), - ["cpu", "p", "smp", "hw"] - ); - } - - /// Teeth for the list: a new reach reds naming the file, the line and the - /// item; a listed one does not; a listed one the file stopped spelling reds; - /// and a root alias of the architecture is the architecture. - #[test] - fn a_new_reach_out_of_arch_is_red_and_the_list_cannot_rot() { - let file = |at: &str, text: &str| (at.to_string(), text.to_string()); - let files = [ - file("kernel/src/arch/x86_64/tlb.rs", "use crate::time::Duration;\nuse crate::drivers::xhci;\n"), - file("kernel/src/arch/x86_64/idt/timer.rs", "use crate::hw::HW;\nuse crate::arch::apic;\n"), - file("kernel/src/sched/driver.rs", "use crate::drivers::xhci;\n"), - ]; - let aliases = vec!["hw".to_string()]; - let said = arch_reach_complaints(&files, &[("kernel/src/arch/x86_64/tlb.rs", &["time"])], &aliases); - assert_eq!(said.len(), 1, "{said:?}"); - assert!(said[0].starts_with("kernel/src/arch/x86_64/tlb.rs:2: names `crate::drivers`"), "{said:?}"); - - let listed = arch_reach_complaints(&files, &[("kernel/src/arch/x86_64/tlb.rs", &["drivers", "time"])], &aliases); - assert!(listed.is_empty(), "{listed:?}"); - - let stale = arch_reach_complaints( - &files, - &[("kernel/src/arch/x86_64/tlb.rs", &["drivers", "time", "sched"]), ("kernel/src/arch/gone.rs", &["log"])], - &aliases, - ); - assert_eq!(stale.len(), 2, "{stale:?}"); - assert!(stale.iter().any(|s| s.contains("kernel/src/arch/gone.rs")), "{stale:?}"); - assert!(stale.iter().any(|s| s.contains("`crate::sched`") && s.contains("no longer")), "{stale:?}"); - } - /// **The architecture rules hold over the whole repository**, and no place /// a rule names is a place that no longer holds a needle — a row nobody /// needs any more is a permission nobody re-argued. From 7aff0afba759d4a947c2c9d58041fd128fdbc06e Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 13:16:37 +0200 Subject: [PATCH 04/12] The arch layer names nothing above it but an enumerated, shrinking list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `src/sourcegate.rs` gains an eleventh scan beside the arch rules. A file under `kernel/src/arch/` may name, by `crate::`, `$crate::`, a `crate::{…}` group or a `super::` chain out of its module, only the architecture and the kernel root's re-exports of it (`crate::hw` is `arch::hw`, read off main.rs). A glob of the root or a rename of it is refused whole. Everything it names beyond that today is `ARCH_REACHES_UP`: 49 files, 203 (file, item) entries over 38 distinct root items. The gate reds on a name a file adds and on a listed name its file no longer spells, so the list only shrinks. It is recorded debt, filed as `issues/kernel/the-arch-layer-names-the-kernel-above-it.md`, owned by the arch-contract work (a statically dispatched contract trait and a `platform/` layer for the PC's devices), exit: the list is empty. The review counted 35 non-arch modules with a text search over comments too; the gate reads code only and counts root items, so it also sees the three root re-exports arch names (`PHYS_OFFSET`, `DirectMap`, `MemoryMapEntry`) and the items `alert` and `kernel_main`, and does not count `crate::hw`. One entry is fixed rather than listed: `arch/x86_64/tlb.rs` asserted its shootdown tripwire above `crate::drivers::xhci::CALL_AFTER_BREAK`, so the ISA layer knew how long a USB disk call spins. The timeout is now `time::DEAF_CPU`, a generic tripwire with its own reason (no CPU that is not wedged goes five seconds without taking an interrupt), and the xHCI driver asserts its own bound under it. It lives in `time` and not in either arch's `tlb`, because both architectures compile the xHCI driver and AArch64's shootdown waits on no acknowledgement to export a timeout for. `CALL_AFTER_BREAK` loses its `pub(crate)`: nothing outside the driver reads it now. `cargo run -- --clippy`: all ten shapes clean, the kernel on both architectures in all three feature arms. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK --- ...he-arch-layer-names-the-kernel-above-it.md | 41 ++ kernel/src/arch/x86_64/tlb.rs | 17 +- kernel/src/drivers/xhci/mod.rs | 6 +- kernel/src/drivers/xhci/wait/msc.rs | 2 +- kernel/src/time.rs | 9 + src/sourcegate.rs | 349 ++++++++++++++++++ 6 files changed, 410 insertions(+), 14 deletions(-) create mode 100644 issues/kernel/the-arch-layer-names-the-kernel-above-it.md diff --git a/issues/kernel/the-arch-layer-names-the-kernel-above-it.md b/issues/kernel/the-arch-layer-names-the-kernel-above-it.md new file mode 100644 index 00000000000..e539606b31c --- /dev/null +++ b/issues/kernel/the-arch-layer-names-the-kernel-above-it.md @@ -0,0 +1,41 @@ +--- +status: open +kind: defect +opened: 2026-09-27 +--- + +# The arch layer names the kernel above it + +`kernel/src/arch/` is meant to be the machine and nothing else: generic code +reaches it through `crate::arch::…`, and it reaches nothing back. It does. Its +files name kernel modules above it by path — the scheduler, the process table, +the log, drivers, the IOMMU layer, test actuators — and `ARCH_REACHES_UP` in +`src/sourcegate.rs` enumerates every such name, per file. The gate beside it +reds on a name a file adds and on a listed name a file no longer spells, so the +list only shrinks; each entry is this file's debt. + +What the list holds, by kind: + +- **Upcalls a contract would name.** Trap and syscall entry into the + scheduler and `syscall`, the log, `mm`, `clock`/`time`. These stay, but + through an interface generic code declares, not a path the architecture + picks. +- **PC platform devices living in the ISA layer.** The i8042 driving + `keyboard`, `mouse` and `irq_ring`; VT-d naming `iommu`, `pcidev` and + `drivers::{acpi, pci}`; the per-device vectors under `idt/` naming `drivers` and + `pcidev`; the TCO watchdog reading `params`. +- **Kernel state the architecture stores.** Per-CPU data holding + `process::{Pid, Tid}`. +- **Test hooks.** `actuator`, across boot, SMP, the i8042, VT-d and the + interrupt entry. + +Why it matters: the AArch64 port inherits whatever x86-64 exports, so every +upward name here is one the port has to satisfy or stub, and until the gate +nothing but review stopped the next. + +Owed by the arch-contract work: an `arch/api.rs` contract, a trait implemented +by a zero-sized type and dispatched statically, that is the only way generic +code reaches the machine and the only way the machine reaches back; and +`platform/{pc,virt}` for the PC's own devices, so `arch/` is the ISA alone. + +**Exit**: `ARCH_REACHES_UP` is empty. diff --git a/kernel/src/arch/x86_64/tlb.rs b/kernel/src/arch/x86_64/tlb.rs index 1a4228c57bc..668f29d9a75 100644 --- a/kernel/src/arch/x86_64/tlb.rs +++ b/kernel/src/arch/x86_64/tlb.rs @@ -13,7 +13,7 @@ use core::sync::atomic::{AtomicU64, Ordering}; use crate::shootdown::{Generation, Shootdown}; -use crate::time::{Duration, Tripwire}; +use crate::time::Tripwire; use super::{apic, percpu, smp}; @@ -58,17 +58,10 @@ pub fn log_census() { ); } -/// Set above xHCI's `CALL_AFTER_BREAK`, the longest a disk call spins with `IF` -/// clear once its transport has broken, so no legitimate wait trips it; that -/// assertion below holds the order. -const ACK_TIMEOUT: Tripwire = Tripwire::absurd( - Duration::from_secs(5), - "above the longest IF-clear device spin a target can be inside", -); - -// A disk call spins with interrupts off, so one that outlasted this tripwire -// would panic another CPU over a device. -const _: () = assert!(crate::drivers::xhci::CALL_AFTER_BREAK.nanos() < ACK_TIMEOUT.nanos()); +/// How long the initiator waits for one CPU's flush: a target that has not +/// answered by then is not taking interrupts. Every spin that masks them holds +/// itself under this at its own site; the architecture knows none of them. +const ACK_TIMEOUT: Tripwire = crate::time::DEAF_CPU; /// Spins between deadline checks; `nanos_since_boot`'s 128-bit divide is too /// costly to call on every iteration. diff --git a/kernel/src/drivers/xhci/mod.rs b/kernel/src/drivers/xhci/mod.rs index f5c2efb8bea..c9b52fd727a 100644 --- a/kernel/src/drivers/xhci/mod.rs +++ b/kernel/src/drivers/xhci/mod.rs @@ -316,11 +316,15 @@ pub(crate) const AFTER_BREAK: toyos_xhci::call::Bounds = toyos_xhci::call::AFTER const _: () = assert!(AFTER_BREAK.wait == USB_TIMEOUT_NS); /// Everything one disk call may spin for from the start of the wait its transport broke on. -pub(crate) const CALL_AFTER_BREAK: crate::time::Budget = crate::time::Budget::of( +const CALL_AFTER_BREAK: crate::time::Budget = crate::time::Budget::of( crate::time::Duration::from_nanos(AFTER_BREAK.whole()), "every wait is clipped to where the rungs still ahead of it begin, so the last rung runs whatever was spent before it and the call ends here", ); +// A disk call spins with interrupts off, so one that outlasted this tripwire +// would panic another CPU over a device. +const _: () = assert!(CALL_AFTER_BREAK.nanos() < crate::time::DEAF_CPU.nanos()); + // A disk whose device left under the port rung's reset is waited for no longer than the rungs from that reset on were given to spend on it. const _: () = assert!( diff --git a/kernel/src/drivers/xhci/wait/msc.rs b/kernel/src/drivers/xhci/wait/msc.rs index 19bce936a87..8f1fe5421b7 100644 --- a/kernel/src/drivers/xhci/wait/msc.rs +++ b/kernel/src/drivers/xhci/wait/msc.rs @@ -2526,7 +2526,7 @@ pub fn storage_flush(index: usize, losses: &mut u64) -> BlockResult { /// (`toyos_xhci::call`): opened by the first break or by finding the disk /// held, carried across every command, the hold and the command sent again, /// and closed here. The caller spins with `IF` clear for all of it, which is -/// why `CALL_AFTER_BREAK` is held under the TLB-ack tripwire. +/// why `CALL_AFTER_BREAK` is held under `time::DEAF_CPU`, the TLB-ack tripwire. /// /// **The hold only waits for a verdict.** It ends at the disk's own window /// (`toyos_xhci::identity::RETURN_WINDOW`), past which the disk is lost and the diff --git a/kernel/src/time.rs b/kernel/src/time.rs index 85cf1c1479a..bcfb1781057 100644 --- a/kernel/src/time.rs +++ b/kernel/src/time.rs @@ -217,6 +217,15 @@ impl fmt::Display for Tripwire { } } +/// How long a CPU waits for another to take an interrupt before calling it +/// deaf and panicking — the TLB shootdown's acknowledgement wait is this. A +/// spin that holds interrupts masked is bounded under it at its own site, and +/// never the other way. +pub const DEAF_CPU: Tripwire = Tripwire::absurd( + Duration::from_secs(5), + "no CPU that is not wedged goes five seconds without taking an interrupt", +); + /// A wall-clock allowance whose expiry is a **degraded answer**, never a panic. #[derive(Clone, Copy)] pub struct Budget { diff --git a/src/sourcegate.rs b/src/sourcegate.rs index ba7fdb0624a..6095252d638 100644 --- a/src/sourcegate.rs +++ b/src/sourcegate.rs @@ -69,6 +69,12 @@ //! holding what it owes, and one that no longer holds a needle is refused. //! The rust fork's std is `src/forkcheck.rs`'s to govern and is not read here. //! +//! The eleventh holds the kernel's arch layer below the rest: a file under +//! `kernel/src/arch/` may name, by `crate::`, `$crate::` or a `super::` chain +//! out of it, only the architecture and the root's re-exports of it. What it +//! names beyond that today is [`ARCH_REACHES_UP`], per file, and that list +//! only shrinks. +//! //! **What none of them reaches is filed rather than implied**, and each table's //! own doc names its half: the entries under `issues/build/` say so. @@ -1581,6 +1587,261 @@ fn every_rust_file() -> Vec<(String, String)> { .collect() } +// ── The arch layer names nothing above it ─────────────────────────────────── + +/// The kernel's architecture layer: every file under it is read. +#[cfg(test)] +const ARCH_TREE: &str = "kernel/src/arch/"; + +/// Where the kernel's root declares its modules and re-exports. +#[cfg(test)] +const KERNEL_ROOT: &str = "kernel/src/main.rs"; + +/// Every crate-root item a file under [`ARCH_TREE`] names that is neither the +/// architecture nor a root re-export of it: the layer reaching up into the +/// kernel it is meant to sit under. Each entry is recorded debt, owed by +/// `issues/kernel/the-arch-layer-names-the-kernel-above-it.md`, which is done +/// when this list is empty. +/// +/// **It only shrinks.** A name a file adds reds +/// `the_arch_layer_names_nothing_above_it`, and so does a name a row lists that +/// its file no longer spells, so no row outlives what it excused. +#[cfg(test)] +const ARCH_REACHES_UP: &[(&str, &[&str])] = &[ + ("kernel/src/arch/aarch64/boot.rs", &["PHYS_OFFSET", "actuator", "drivers", "kernel_main", "log", "mm"]), + ("kernel/src/arch/aarch64/console_uart.rs", &["drivers", "log", "mm"]), + ("kernel/src/arch/aarch64/control_regs.rs", &["log"]), + ("kernel/src/arch/aarch64/hw.rs", &["clock", "log", "mm", "sched", "scheduler", "trace"]), + ("kernel/src/arch/aarch64/iommu_unit.rs", &["drivers", "iommu", "log"]), + ("kernel/src/arch/aarch64/mod.rs", &["actuator", "log"]), + ("kernel/src/arch/aarch64/paging.rs", &["MemoryMapEntry", "mm", "sync", "vma"]), + ("kernel/src/arch/aarch64/percpu.rs", &["log", "process"]), + ("kernel/src/arch/aarch64/tlb.rs", &["invalidation"]), + ("kernel/src/arch/aarch64/trap.rs", &["alert", "log", "symbols"]), + ("kernel/src/arch/aarch64/watchdog.rs", &["drivers"]), + ("kernel/src/arch/x86_64/apic.rs", &["clock", "log", "time", "trace"]), + ("kernel/src/arch/x86_64/boot.rs", &["PHYS_OFFSET", "actuator", "clock", "drivers", "kernel_main", "log", "mm"]), + ("kernel/src/arch/x86_64/console_uart.rs", &["log"]), + ("kernel/src/arch/x86_64/control_regs.rs", &["actuator", "log"]), + ("kernel/src/arch/x86_64/entry.rs", &["sched"]), + ("kernel/src/arch/x86_64/fpu.rs", &["log"]), + ("kernel/src/arch/x86_64/hpet.rs", &["clock", "log", "mm", "time"]), + ("kernel/src/arch/x86_64/hw.rs", &["actuator", "clock", "deadline", "heartbeat", "log", "mm", "preempt", "process", "sched", "scheduler", "time", "trace", "watch"]), + ("kernel/src/arch/x86_64/i8042/mod.rs", &["actuator", "clock", "drivers", "irq_ring", "keyboard", "log", "mouse", "preempt", "sync", "time"]), + ("kernel/src/arch/x86_64/idt/dma_fault.rs", &["iommu"]), + ("kernel/src/arch/x86_64/idt/exceptions.rs", &["DirectMap", "actuator", "alert", "log", "mm", "panic", "process", "scheduler", "symbols", "syscall"]), + ("kernel/src/arch/x86_64/idt/hda.rs", &["drivers"]), + ("kernel/src/arch/x86_64/idt/log_nest.rs", &["log"]), + ("kernel/src/arch/x86_64/idt/mod.rs", &["actuator", "blackbox", "log", "preempt", "sched", "scheduler", "sync", "trace"]), + ("kernel/src/arch/x86_64/idt/nmi.rs", &["drivers", "hardlockup", "panic", "sched"]), + ("kernel/src/arch/x86_64/idt/spurious.rs", &["clock", "irq_census", "log", "time"]), + ("kernel/src/arch/x86_64/idt/timer.rs", &["deadline", "irq_census", "preempt", "scheduler"]), + ("kernel/src/arch/x86_64/idt/unclaimed.rs", &["clock", "irq_census", "log", "time"]), + ("kernel/src/arch/x86_64/idt/user_dev.rs", &["clock", "irq_ring", "pcidev", "preempt"]), + ("kernel/src/arch/x86_64/idt/virtio_sound.rs", &["drivers"]), + ("kernel/src/arch/x86_64/idt/xhci.rs", &["clock", "irq_ring", "preempt"]), + ("kernel/src/arch/x86_64/ioapic.rs", &["drivers", "iommu", "log", "mm", "sync"]), + ("kernel/src/arch/x86_64/mod.rs", &["actuator", "log"]), + ("kernel/src/arch/x86_64/nmi_gate.rs", &["actuator", "clock", "log", "mm", "sched", "symbols"]), + ("kernel/src/arch/x86_64/paging.rs", &["MemoryMapEntry", "hasher", "log", "mm", "sched", "sync", "vma"]), + ("kernel/src/arch/x86_64/percpu.rs", &["actuator", "drivers", "irq_census", "log", "mm", "process", "sync"]), + ("kernel/src/arch/x86_64/rtc.rs", &["actuator", "clock", "time"]), + ("kernel/src/arch/x86_64/smp.rs", &["DirectMap", "actuator", "clock", "drivers", "hardlockup", "log", "mm", "process", "scheduler", "smp_roster", "time"]), + ("kernel/src/arch/x86_64/syscall.rs", &["syscall"]), + ("kernel/src/arch/x86_64/tlb.rs", &["clock", "invalidation", "log", "mm", "sched", "shootdown", "time"]), + ("kernel/src/arch/x86_64/vtd/dmar.rs", &["drivers", "iommu"]), + ("kernel/src/arch/x86_64/vtd/domain.rs", &["iommu", "log", "mm", "sync"]), + ("kernel/src/arch/x86_64/vtd/fault.rs", &["drivers", "iommu", "log", "mm", "panic", "pcidev"]), + ("kernel/src/arch/x86_64/vtd/interrupt.rs", &["iommu", "log", "sync"]), + ("kernel/src/arch/x86_64/vtd/mod.rs", &["actuator", "clock", "drivers", "iommu", "log", "mm", "sync", "time"]), + ("kernel/src/arch/x86_64/vtd/queue.rs", &["clock", "mm", "time"]), + ("kernel/src/arch/x86_64/vtd/table.rs", &["actuator", "iommu", "mm"]), + ("kernel/src/arch/x86_64/watchdog.rs", &["actuator", "drivers", "log", "params", "time"]), +]; + +/// Every `(0-based line, item)` for a crate-root item `text` names by path, in +/// a file whose module sits `depth` levels below the crate root: the segment +/// after `crate::` or `$crate::`, the first segment of each element of a +/// `crate::{…}` group over any number of lines, and the same after a `super::` +/// chain long enough to reach the root. A glob of the root is the item `*` and +/// a rename of it (`crate as k`) the item `crate as`, because what either +/// brings in is decided by resolution, which a line scan does not have. +/// Comments and string literals are not code ([`code_only`]). A macro reached +/// by textual scope (`#[macro_use]`) is not a path and is not read. +#[cfg(test)] +fn crate_items_named(text: &str, depth: usize) -> Vec<(usize, String)> { + let code: Vec = text.lines().map(code_only).collect::>().join("\n").chars().collect(); + let word = |c: char| c.is_ascii_alphanumeric() || c == '_'; + let line_of = |at: usize| code[..at].iter().filter(|c| **c == '\n').count(); + let skip_space = |mut at: usize| { + while code.get(at).is_some_and(|c| c.is_whitespace()) { + at += 1; + } + at + }; + // The identifier that begins at `at`, and where it ends. + let ident_at = |at: usize| -> Option<(String, usize)> { + if at.checked_sub(1).and_then(|j| code.get(j)).is_some_and(|c| word(*c)) { + return None; + } + let end = (at..).find(|&j| !code.get(j).is_some_and(|c| word(*c))).unwrap_or(at); + (end > at).then(|| (code[at..end].iter().collect(), end)) + }; + let colons = |at: usize| code.get(at..at + 2) == Some(&[':', ':'][..]); + // What follows the root, `at` being just past its `::`. + let after_root = |at: usize, found: &mut Vec<(usize, String)>| { + let at = skip_space(at); + match code.get(at) { + Some('*') => found.push((line_of(at), "*".to_string())), + Some('{') => { + let (mut depth, mut i, mut element) = (0usize, at, true); + while let Some(&c) = code.get(i) { + match c { + '{' => { + depth += 1; + element = depth == 1; + } + '}' => { + depth -= 1; + if depth == 0 { + break; + } + } + ',' if depth == 1 => element = true, + c if c.is_whitespace() => {} + _ => { + if element && depth == 1 { + match ident_at(i) { + Some((name, _)) if name != "self" => found.push((line_of(i), name)), + Some(_) => {} + None if c == '*' => found.push((line_of(i), "*".to_string())), + None => {} + } + } + element = false; + } + } + i += 1; + } + } + _ => { + if let Some((name, _)) = ident_at(at) { + found.push((line_of(at), name)); + } + } + } + }; + let mut found = Vec::new(); + let mut at = 0; + while at < code.len() { + let Some((name, end)) = ident_at(at) else { + at += 1; + continue; + }; + let after = skip_space(end); + if name == "crate" { + if colons(after) { + after_root(after + 2, &mut found); + } else if ident_at(after).is_some_and(|(next, _)| next == "as") { + found.push((line_of(at), "crate as".to_string())); + } + } else if name == "super" && !(at >= 2 && colons(at - 2)) { + let (mut supers, mut next) = (1, after); + while colons(next) { + match ident_at(skip_space(next + 2)) { + Some((s, e)) if s == "super" => { + supers += 1; + next = skip_space(e); + } + _ => break, + } + } + if supers == depth && colons(next) { + after_root(next + 2, &mut found); + } + } + at = end; + } + found +} + +/// How many modules deep the file at repository-relative `path` sits below +/// the kernel's crate root: `kernel/src/arch/mod.rs` is 1, +/// `kernel/src/arch/x86_64/tlb.rs` is 3. +#[cfg(test)] +fn module_depth(path: &str) -> usize { + let under = path.strip_prefix("kernel/src/").unwrap_or(path); + let parts = under.split('/').count(); + if under.ends_with("/mod.rs") { parts - 1 } else { parts } +} + +/// The names the kernel root re-exports from the architecture +/// (`pub(crate) use arch::hw;`, `use arch::{cpu, smp};`), so `crate::hw` is the +/// architecture naming itself through the root rather than reaching above it. +#[cfg(test)] +fn arch_aliases(root: &str) -> Vec { + let mut names = Vec::new(); + for line in root.lines() { + let code = code_only(line); + let Some(rest) = after_visibility(code.trim()).strip_prefix("use arch::") else { continue }; + let rest = rest.trim_end().trim_end_matches(';'); + let rest = rest.strip_prefix('{').and_then(|r| r.strip_suffix('}')).unwrap_or(rest); + for element in rest.split(',').map(str::trim).filter(|e| !e.is_empty()) { + let local = element.rsplit(" as ").next().unwrap_or(element); + names.push(local.rsplit("::").next().unwrap_or(local).trim().to_string()); + } + } + names +} + +/// Every red `rows` and `files` give: a crate-root item a file under +/// [`ARCH_TREE`] names and no row permits it, and a row entry its file no +/// longer spells. +#[cfg(test)] +fn arch_reach_complaints( + files: &[(String, String)], + rows: &[(&str, &[&str])], + aliases: &[String], +) -> Vec { + let mut complaints = Vec::new(); + for (i, (file, _)) in rows.iter().enumerate() { + if rows[..i].iter().any(|(earlier, _)| earlier == file) { + complaints.push(format!("{file} has two rows in the arch reach list; one row per file")); + } + if !files.iter().any(|(at, _)| at == file) { + complaints.push(format!( + "the arch reach list names {file}, and this tree holds no such file under {ARCH_TREE}" + )); + } + } + for (at, text) in files.iter().filter(|(at, _)| at.starts_with(ARCH_TREE)) { + let named: Vec<(usize, String)> = crate_items_named(text, module_depth(at)) + .into_iter() + .filter(|(_, item)| item != "arch" && !aliases.contains(item)) + .collect(); + let permitted: &[&str] = rows.iter().find(|(file, _)| file == at).map_or(&[], |(_, items)| items); + for (line, item) in &named { + if !permitted.contains(&item.as_str()) { + complaints.push(format!( + "{at}:{}: names `crate::{item}`, which is above the arch layer. Generic code \ + reaches the architecture and never the other way: pass in what it needs, \ + or move the code out of {ARCH_TREE}", + line + 1 + )); + } + } + for item in permitted { + if !named.iter().any(|(_, n)| n == item) { + complaints.push(format!( + "the arch reach list lets {at} name `crate::{item}`, and it no longer does: \ + delete the entry, the list only shrinks" + )); + } + } + } + complaints +} + /// The third-party C corpus, whose attribution is per *population* rather than /// per file: `tests/testcases/LICENSE` states how many files each of these /// directories holds, and `NOTICE` points at that file for the terms. @@ -1625,6 +1886,94 @@ fn digest(bytes: &[u8]) -> String { mod tests { use super::*; + /// **The arch layer names nothing above it**, but for [`ARCH_REACHES_UP`], + /// and that list names nothing its files no longer spell. + #[test] + fn the_arch_layer_names_nothing_above_it() { + let files = every_rust_file(); + let root = files + .iter() + .find(|(at, _)| at == KERNEL_ROOT) + .unwrap_or_else(|| panic!("the walk did not reach {KERNEL_ROOT}")); + let aliases = arch_aliases(&root.1); + assert!( + aliases.iter().any(|a| a == "hw"), + "{KERNEL_ROOT} re-exports `arch::hw` and the alias scan did not see it: {aliases:?}" + ); + let arch = files.iter().filter(|(at, _)| at.starts_with(ARCH_TREE)).count(); + assert!(arch > 40, "the walk found {arch} files under {ARCH_TREE}; it is reading no tree"); + let complaints = arch_reach_complaints(&files, ARCH_REACHES_UP, &aliases); + assert!(complaints.is_empty(), "{}", complaints.join("\n")); + } + + /// Teeth for the rule above: every spelling of a root item is read, and + /// what is not a path to one is not. + #[test] + fn every_spelling_of_a_crate_root_item_is_read() { + let names = |text: &str, depth: usize| -> Vec { + crate_items_named(text, depth).into_iter().map(|(_, item)| item).collect() + }; + assert_eq!(names("use crate::sched::driver;\n", 3), ["sched"]); + assert_eq!(names(" crate::log!(\"x\");\n", 3), ["log"]); + assert_eq!(names("const { $crate::irq_census::TOTAL }\n", 3), ["irq_census"]); + assert_eq!(names("use crate::{\n arch::x,\n mm::{a, b},\n self,\n process,\n};\n", 3), [ + "arch", "mm", "process" + ]); + assert_eq!(names("use crate :: { * };\nuse crate::*;\n", 3), ["*", "*"]); + assert_eq!(names("use crate as k;\n", 3), ["crate as"]); + // A `super::` chain that leaves the file's module names the root's item. + assert_eq!(names("use super::super::super::drivers::xhci;\n", 3), ["drivers"]); + assert_eq!(names("use super::super::{sched, mm};\n", 2), ["sched", "mm"]); + assert_eq!(names("use super::{apic, smp};\nuse super::super::percpu;\n", 3), Vec::::new()); + // Not code, not the root, not a path. + for text in [ + "// crate::sched is the scheduler\n", + "let s = \"crate::sched\";\n", + "pub(crate) fn f() {}\npub(super) fn g() {}\n", + "use mycrate::sched;\nuse other_crate::x;\n", + "use self::super::x;\n", + ] { + assert_eq!(names(text, 3), Vec::::new(), "{text:?}"); + } + assert_eq!(module_depth("kernel/src/arch/mod.rs"), 1); + assert_eq!(module_depth("kernel/src/arch/x86_64/mod.rs"), 2); + assert_eq!(module_depth("kernel/src/arch/x86_64/tlb.rs"), 3); + assert_eq!(module_depth("kernel/src/arch/x86_64/idt/timer.rs"), 4); + assert_eq!( + arch_aliases("use arch::{cpu, percpu as p, smp};\npub(crate) use arch::hw;\nuse drivers::acpi;\n"), + ["cpu", "p", "smp", "hw"] + ); + } + + /// Teeth for the list: a new reach reds naming the file, the line and the + /// item; a listed one does not; a listed one the file stopped spelling reds; + /// and a root alias of the architecture is the architecture. + #[test] + fn a_new_reach_out_of_arch_is_red_and_the_list_cannot_rot() { + let file = |at: &str, text: &str| (at.to_string(), text.to_string()); + let files = [ + file("kernel/src/arch/x86_64/tlb.rs", "use crate::time::Duration;\nuse crate::drivers::xhci;\n"), + file("kernel/src/arch/x86_64/idt/timer.rs", "use crate::hw::HW;\nuse crate::arch::apic;\n"), + file("kernel/src/sched/driver.rs", "use crate::drivers::xhci;\n"), + ]; + let aliases = vec!["hw".to_string()]; + let said = arch_reach_complaints(&files, &[("kernel/src/arch/x86_64/tlb.rs", &["time"])], &aliases); + assert_eq!(said.len(), 1, "{said:?}"); + assert!(said[0].starts_with("kernel/src/arch/x86_64/tlb.rs:2: names `crate::drivers`"), "{said:?}"); + + let listed = arch_reach_complaints(&files, &[("kernel/src/arch/x86_64/tlb.rs", &["drivers", "time"])], &aliases); + assert!(listed.is_empty(), "{listed:?}"); + + let stale = arch_reach_complaints( + &files, + &[("kernel/src/arch/x86_64/tlb.rs", &["drivers", "time", "sched"]), ("kernel/src/arch/gone.rs", &["log"])], + &aliases, + ); + assert_eq!(stale.len(), 2, "{stale:?}"); + assert!(stale.iter().any(|s| s.contains("kernel/src/arch/gone.rs")), "{stale:?}"); + assert!(stale.iter().any(|s| s.contains("`crate::sched`") && s.contains("no longer")), "{stale:?}"); + } + /// **The architecture rules hold over the whole repository**, and no place /// a rule names is a place that no longer holds a needle — a row nobody /// needs any more is a permission nobody re-argued. From c523ccbf2d4e1492f8fa2d86eb58a65a58d1ad79 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 13:16:51 +0200 Subject: [PATCH 05/12] Negative control for the arch gate: tlb.rs names crate::process Reverted by the next commit. A real upward import that builds, planted to show the gate reds on it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK --- kernel/src/arch/x86_64/tlb.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/kernel/src/arch/x86_64/tlb.rs b/kernel/src/arch/x86_64/tlb.rs index 668f29d9a75..d7fbc7aca66 100644 --- a/kernel/src/arch/x86_64/tlb.rs +++ b/kernel/src/arch/x86_64/tlb.rs @@ -274,3 +274,6 @@ pub fn debug_disarm_ack_delay() -> u64 { delay::TARGET.store(delay::EVERY, Ordering::Relaxed); 0 } + +// Negative control, reverted by the next commit. +const _: usize = crate::process::KERNEL_STACK_SIZE; From 9d08f047a99361b7fd7571fa7e6bf88e95e49fe8 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 13:17:12 +0200 Subject: [PATCH 06/12] Revert the arch gate negative control This reverts c523ccbf. The planted `crate::process` in `kernel/src/arch/x86_64/tlb.rs` built (`cargo check --target x86_64-unknown-none` in kernel/, exit 0) and redded `the_arch_layer_names_nothing_above_it` (exit 101) naming `kernel/src/arch/x86_64/tlb.rs:279` and `crate::process`. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK --- kernel/src/arch/x86_64/tlb.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/kernel/src/arch/x86_64/tlb.rs b/kernel/src/arch/x86_64/tlb.rs index d7fbc7aca66..668f29d9a75 100644 --- a/kernel/src/arch/x86_64/tlb.rs +++ b/kernel/src/arch/x86_64/tlb.rs @@ -274,6 +274,3 @@ pub fn debug_disarm_ack_delay() -> u64 { delay::TARGET.store(delay::EVERY, Ordering::Relaxed); 0 } - -// Negative control, reverted by the next commit. -const _: usize = crate::process::KERNEL_STACK_SIZE; From 9f0665ab7501d375c3cdfc80e2a45d803e26aff8 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 13:17:55 +0200 Subject: [PATCH 07/12] CLAUDE.md points at what answers its lists, and every workspace package describes itself The owner-approved CLAUDE.md edits, placed as ruled: - the rule, appended to "There are no spec documents.": a CLAUDE.md never holds a list that a manifest, a directory or a gate already answers; it points at that source instead; - root "Repository layout" is one sentence pointing at the root Cargo.toml's `[workspace]` members/exclude (held to the tree by `src/hostws.rs`) and each package's `description`. The list it replaces omitted 23 of the root crates. No orienting fact of it was kept: the build system's package name and the workspace gate are the Cargo.toml's, `rust/` is the Dependencies section's, the QEMU harness is Build & test's, `issues/` is the table at the top, and `system.toml` is src/CLAUDE.md's and the Capabilities paragraph's; - tests/CLAUDE.md's host-suite list, which named 23 crates of a workspace that holds more, is a pointer: the workspace members, the SDK and every userland crate `src/userlandhost.rs` finds a test in, all run by `src/ci.rs`'s `host`. Its claim that any userland crate is host-testable with the host triple went with it; userlandhost's header says most cannot make a host build; - the Capabilities paragraph stated per-program file views as present fact, and `sys_open` resolves every path against one machine-wide tree (`kernel/src/syscall/fs.rs`), so it now says that is not yet true of files; - the Dependencies section's first sentence says the development machine can be any host OS and architecture. 44 workspace members and the root, `bootloader` and `kernel` packages had no `description`; each now has one line taken from its own module header, or from the deleted list where that was still accurate (the no_std/forbid claims re-checked against each lib.rs). `hostws`'s new gate reds on a package the `[workspace]` table names without one; a checked mutation deleting toyos-dns's description redded it (exit 101) and was restored. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK --- CLAUDE.md | 40 ++++----------------------- Cargo.toml | 1 + bcachefs/Cargo.toml | 1 + bootloader/Cargo.toml | 1 + kernel-loom/Cargo.toml | 1 + kernel/Cargo.toml | 1 + src/hostws.rs | 53 +++++++++++++++++++++++++++++++++++- tests/CLAUDE.md | 2 +- toyos-acpi/Cargo.toml | 1 + toyos-blackbox/Cargo.toml | 1 + toyos-blockhold/Cargo.toml | 1 + toyos-blockring/Cargo.toml | 1 + toyos-bootmap/Cargo.toml | 1 + toyos-cc/Cargo.toml | 1 + toyos-desktop/Cargo.toml | 1 + toyos-dma/Cargo.toml | 1 + toyos-dns/Cargo.toml | 1 + toyos-elf/Cargo.toml | 1 + toyos-elide/Cargo.toml | 1 + toyos-fat32-check/Cargo.toml | 1 + toyos-fat32/Cargo.toml | 1 + toyos-gpt/Cargo.toml | 1 + toyos-hda/Cargo.toml | 1 + toyos-i219/Cargo.toml | 1 + toyos-inspect/Cargo.toml | 1 + toyos-libc-copies/Cargo.toml | 1 + toyos-logstream/Cargo.toml | 1 + toyos-manifest/Cargo.toml | 1 + toyos-mdns/Cargo.toml | 1 + toyos-mixer/Cargo.toml | 1 + toyos-pci/Cargo.toml | 1 + toyos-pcid/Cargo.toml | 1 + toyos-proclife/Cargo.toml | 1 + toyos-ps2/Cargo.toml | 1 + toyos-quiesce/Cargo.toml | 1 + toyos-rootimage/Cargo.toml | 1 + toyos-sched/Cargo.toml | 1 + toyos-sched/loom/Cargo.toml | 1 + toyos-sched/sim/Cargo.toml | 1 + toyos-swap/Cargo.toml | 1 + toyos-symbols/Cargo.toml | 1 + toyos-tco/Cargo.toml | 1 + toyos-tmpdir/Cargo.toml | 1 + toyos-untrusted/Cargo.toml | 1 + toyos-update/Cargo.toml | 1 + toyos-userbound/Cargo.toml | 1 + toyos-userpin/Cargo.toml | 1 + toyos-wallclock/Cargo.toml | 1 + toyos-xhci/Cargo.toml | 1 + toyos-xhci/sim/Cargo.toml | 1 + 50 files changed, 105 insertions(+), 37 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index eb7706ab302..4d0794f9c41 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,7 +18,8 @@ An operating system built from scratch in Rust, held to a production-grade engin There are no spec documents. Rules live where they are enforced — a gate, a module header, the redlist, the review prompt — and everything else is an issue. Free text that merely describes the tree rots and is deleted, not -maintained. +maintained. A `CLAUDE.md` never holds a list that a manifest, a directory or a +gate already answers; it points at that source instead. A subdirectory `CLAUDE.md` loads when a file in that subtree is `Read`, and not from `Bash`. A rule whose violation is unrecoverable or invisible stays here; everything else lives where the work is. @@ -44,7 +45,7 @@ A subdirectory `CLAUDE.md` loads when a file in that subtree is `Read`, and not **Syscall ABI** — `toyos-abi/`: struct layouts, syscall numbers, typed wrappers; completely unstable, read the code. Never add or change a syscall without discussion; a deleted syscall's number is retired, never reused. `toyos/` builds on it with typed handles, IPC framing, ports, namespaces and `surface` — userland uses `toyos`, the kernel uses `toyos-abi` only. -**Capabilities** — a process holds exactly what its parent moved into it, and among kernel objects there is nothing it can name to get more. No registry, no connect-by-name, no pid-as-authority: `/system/bin/init` builds every program's namespace and device claims from `system.toml` before spawning it, and a handle a process does not hold is a bug in that process — the kernel ends it rather than answering a word it can ignore. **Isolation is non-negotiable, and the filesystem is inside it**: a process names only the paths in the view its parent built for it, the unit of isolation is the program, and a user is the part of the tree a session was handed. +**Capabilities** — a process holds exactly what its parent moved into it, and among kernel objects there is nothing it can name to get more. No registry, no connect-by-name, no pid-as-authority: `/system/bin/init` builds every program's namespace and device claims from `system.toml` before spawning it, and a handle a process does not hold is a bug in that process — the kernel ends it rather than answering a word it can ignore. **Isolation is non-negotiable, and the filesystem is inside it**: a process names only the paths in the view its parent built for it, the unit of isolation is the program, and a user is the part of the tree a session was handed. Not yet true of files: the kernel still resolves every path against one machine-wide tree until the storage track's per-program views land. **CPU state** — a CPU's control registers come from one declaration, applied by the BSP and by every AP and asserted on each; no read-modify-write decides what either holds. @@ -54,7 +55,7 @@ A subdirectory `CLAUDE.md` loads when a file in that subtree is `Read`, and not ## Dependencies -Only **Rust** and **QEMU** (for development). The rules: no binary outside those two — a macOS binary is a hard no, and "only for tests" does not soften it; only general and widely used crates — one that does *our* job we write ourselves, and a driver crate never; no Python; third-party crates are used as published, and a fork carries a change written to upstream quality and goes when upstream has it. No upstream pull requests are sent for now: ToyOS needs more attention and more contributors before upstream projects take it seriously, and upstreams tend to refuse AI-first projects and their contributions. A third-party source ToyOS cannot build without changing it is carried as an unmodified-source packaging mirror with a byte-identity gate, not as a fork. The north star is **self-hosting**: nothing — build, test, or verification — rests on a host binary. Ask of anything new: could this ever run inside ToyOS? Self-hosting means ToyOS rebuilds itself on ToyOS and reproduces the host's bytes; a bootstrap from source with no binary seed is out of scope. +Only **Rust** and **QEMU** (for development), on any host OS and architecture — the development machine is nothing special. The rules: no binary outside those two — a macOS binary is a hard no, and "only for tests" does not soften it; only general and widely used crates — one that does *our* job we write ourselves, and a driver crate never; no Python; third-party crates are used as published, and a fork carries a change written to upstream quality and goes when upstream has it. No upstream pull requests are sent for now: ToyOS needs more attention and more contributors before upstream projects take it seriously, and upstreams tend to refuse AI-first projects and their contributions. A third-party source ToyOS cannot build without changing it is carried as an unmodified-source packaging mirror with a byte-identity gate, not as a fork. The north star is **self-hosting**: nothing — build, test, or verification — rests on a host binary. Ask of anything new: could this ever run inside ToyOS? Self-hosting means ToyOS rebuilds itself on ToyOS and reproduces the host's bytes; a bootstrap from source with no binary seed is out of scope. Vendor firmware a device verifies by its maker's signature may be shipped: pinned by version and hash, redistributable unmodified, recorded in `NOTICE`, and loaded only by that device's own driver through its IOMMU domain; it never executes on the CPU. @@ -74,38 +75,7 @@ The testing rules live where they are enforced: instruments and known reds in `s ## Repository layout -``` -src/ Build system (the root cargo project, package name: toyos-build; its Cargo.toml is also the host workspace, and a gate reds on a crate that joins neither members nor exclude) -kernel/ Kernel -kernel-loom/ Loom models of the kernel's lock-free concurrency, beside the kernel and not in it -toyos-userbound/ Every decision the kernel makes about the user/kernel boundary, pure -toyos-elide/ Log elision decisions, pure -toyos-proclife/ The process/thread lifecycle's decisions — pure, interleaving-checked -bootloader/ UEFI bootloader -userland/ All userland programs -toyos-abi/ Kernel ABI (types, constants, syscall numbers, syscall wrappers) -toyos/ Userland SDK (typed handles, IPC, ports, namespaces, surface, shm, net) -toyos-manifest/ The one definition of `/system/etc/system.manifest` -toyos-wallclock/ The calendar, and the zone offset userland has to recover — pure -toyos-keymap/ Layouts, dead-key composition, key translation, layout detection -toyos-fat32/ FAT32 driver, read + write; no format path by design -toyos-fat32-check/ FAT32 checker from Microsoft's fatgen103 — the outside judge -toyos-elf/ ELF64 decoding (no_std, no alloc, forbid(unsafe_code)) -toyos-symbols/ Backtrace symbol lookup: locating an ELF's symbol tables and budgeting the demangled name (no_std, no alloc, forbid(unsafe_code)) -toyos-gpt/ GPT parser (no_std, no alloc, forbid(unsafe_code)) -toyos-hda/ HDA codec decoding and output-path selection, pure -toyos-mixer/ The mixer's decisions — samples, gain, dither, quantize — pure, corpus-certified -toyos-pci/ MSI and MSI-X capability decoding, pure -toyos-dma/ Every bound and alignment a DMA view checks — pure, forbid(unsafe_code) -toyos-blockhold/ Who holds each span of a block device, and whose flush answers for the writes its disk lost — pure -toyos-desktop/ Every decision the compositor makes, pure -toyos-ld/ Custom linker -toyos-cc/ Custom C compiler -rust/ Rust compiler/std fork (submodule) -tests/ Integration tests (QEMU-based) -issues/ The issue tracker: one file per issue, typed by kind — see its README -system.toml What to build and boot -``` +The root `Cargo.toml`'s `[workspace]` `members` and `exclude` lists account for every crate in the tree, and `src/hostws.rs` reds on one in neither; every package they name says what it is in its `description`, and a gate there reds on one without. ## Workflow diff --git a/Cargo.toml b/Cargo.toml index 0ec14bd71dc..dae6e49e0d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,6 +93,7 @@ exclude = [ [package] name = "toyos-build" +description = "The build system: toolchain, kernel, bootloader, userland and image, the QEMU harness, CI jobs, and the gates the tree is held to." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/bcachefs/Cargo.toml b/bcachefs/Cargo.toml index f4837372ed7..f719d59f17a 100644 --- a/bcachefs/Cargo.toml +++ b/bcachefs/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "bcachefs" +description = "The /home filesystem: a read side of the real bcachefs on-disk format, and the interim ToyOS format the kernel still links." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/bootloader/Cargo.toml b/bootloader/Cargo.toml index 9f03ccfdc9a..7e57aadd824 100644 --- a/bootloader/Cargo.toml +++ b/bootloader/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "bootloader" +description = "The UEFI bootloader, which loads the kernel and hands it the machine." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/kernel-loom/Cargo.toml b/kernel-loom/Cargo.toml index a389a4896db..e59a463e7d3 100644 --- a/kernel-loom/Cargo.toml +++ b/kernel-loom/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "kernel-loom" +description = "Loom models of the kernel's lock-free concurrency, over the kernel's own sources compiled beside the kernel and not in it." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index 70caaf26652..29f0c4af7eb 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "kernel" +description = "The ToyOS kernel: resource management, scheduling, process lifecycle, filesystem and device arbitration." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/src/hostws.rs b/src/hostws.rs index 42703feffdf..240d146f108 100644 --- a/src/hostws.rs +++ b/src/hostws.rs @@ -16,7 +16,9 @@ //! artifacts — a member's land in the workspace root's `target/`, not its own — //! and the gates below walk the tree and red on a `Cargo.toml` that joined //! neither `members` nor `exclude`. **A new host crate that forgets to join is -//! a red, not a silent gap.** +//! a red, not a silent gap.** Every package the table names carries a +//! `description`, and a gate below reds on one without: the table and those +//! lines are the repository's layout, and nothing else lists it. use std::collections::BTreeSet; use std::path::{Path, PathBuf}; @@ -182,6 +184,20 @@ fn unclaimed(members: &BTreeSet, found: &BTreeSet) -> Vec Result, &'static str> { + let doc: toml::Value = manifest.parse().expect("a manifest is TOML"); + let Some(package) = doc.get("package") else { return Ok(None) }; + match package.get("description").and_then(|d| d.as_str()).map(str::trim) { + Some(d) if !d.is_empty() => Ok(Some(d.to_string())), + Some(_) => Err("a blank `description`"), + None => Err("no `description`"), + } +} + /// The tables in `manifest` that cargo reads only from a workspace root. /// /// Parsed as TOML and not scanned as text, for `src/sourcegate.rs`'s reason: @@ -435,6 +451,41 @@ mod tests { ); } + /// **Every package the `[workspace]` table names says what it is.** Root + /// `CLAUDE.md` points here instead of listing the crates, so a crate that + /// arrives without a `description` is one nothing describes. + #[test] + fn every_package_the_workspace_names_has_a_description() { + let root = repo_root(); + let mut missing = Vec::new(); + let mut described = 0; + for dir in members(&root).into_iter().chain(excluded(&root)) { + let Ok(text) = std::fs::read_to_string(root.join(&dir).join("Cargo.toml")) else { continue }; + match description(&text) { + Ok(Some(_)) => described += 1, + Ok(None) => {} + Err(why) => missing.push(format!("{dir}/Cargo.toml has {why}")), + } + } + assert!(described > 40, "only {described} packages read; the walk is reading no workspace"); + assert!( + missing.is_empty(), + "a package the root Cargo.toml names says what it is in one line of its own \ + [package] `description`:\n {}", + missing.join("\n "), + ); + } + + /// Teeth for the rule above. + #[test] + fn a_package_without_a_description_is_refused_and_a_virtual_workspace_is_not() { + assert_eq!(description("[package]\nname = \"a\"\ndescription = \"An a.\"\n"), Ok(Some("An a.".into()))); + assert_eq!(description("[package]\nname = \"a\"\n"), Err("no `description`")); + assert_eq!(description("[package]\nname = \"a\"\ndescription = \" \"\n"), Err("a blank `description`")); + assert_eq!(description("# description = \"x\"\n[package]\nname = \"a\"\n"), Err("no `description`")); + assert_eq!(description("[workspace]\nmembers = [\"a\"]\n"), Ok(None)); + } + /// Cargo reads `[profile]` and `[patch]` from the workspace root and /// **silently ignores both in a member** — it warns, into output nobody /// reads on a green build. For `toyos-ld` and `toyos-cc` that is not diff --git a/tests/CLAUDE.md b/tests/CLAUDE.md index db0d9d8f609..e3b8d439a64 100644 --- a/tests/CLAUDE.md +++ b/tests/CLAUDE.md @@ -26,4 +26,4 @@ The mechanics live where the work is: profiles and shapes in `tests/common/`, re - **A stimulus sent through a channel that can silently lose it is verified before its effect is asserted** — QEMU's PS/2 queue drops the seventeenth byte, so typed input paces against the guest's report (`shell_type_once`); a guest's console reaches the host as whole lines only, so a partial line exists on no channel. - **A harness field that can be silently inert is this suite's worst defect class** — where two options can describe the same guest they refuse each other by name, and an image is asked what it is armed with. - **A test whose premise is arranged by a defect passes for the wrong reason** — a staging device's resource has to survive its own enumeration. -- **Host suites** run with plain `cargo test` inside `toyos-sched/`, `toyos-ps2/`, `toyos-gpt/`, `toyos-elf/`, `toyos-cc/`, `toyos-ld/`, `toyos-hda/`, `toyos-pci/`, `toyos-xhci/`, `toyos-desktop/`, `toyos-keymap/`, `bcachefs/`, `kernel-loom/`, `toyos-userbound/`, `toyos-elide/`, `toyos-fat32/`, `toyos-fat32-check/`, `toyos-abi/`, `toyos-manifest/`, `toyos-wallclock/`, `toyos-mixer/`, `toyos-dma/` and `toyos/`; any userland crate is host-testable with the host triple (`cargo test --target "$(rustc -vV | sed -n 's/^host: //p')"`), which is the whole of what `calc` is gated by. `kernel-loom/` and `toyos-sched/loom` are the memory-ordering checks — x86 TSO hides a missing acquire edge from every guest test. +- **Host suites** are the root `Cargo.toml`'s `[workspace]` members, the SDK and every userland crate `src/userlandhost.rs` finds a test in; `src/ci.rs`'s `host` runs them all. `kernel-loom/` and `toyos-sched/loom` are the memory-ordering checks — x86 TSO hides a missing acquire edge from every guest test. diff --git a/toyos-acpi/Cargo.toml b/toyos-acpi/Cargo.toml index bf29d25f0c3..c242e4d6663 100644 --- a/toyos-acpi/Cargo.toml +++ b/toyos-acpi/Cargo.toml @@ -7,6 +7,7 @@ [package] name = "toyos-acpi" +description = "ACPI table decoding over firmware-supplied bytes: a decoded value or a named refusal, never a panic." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-blackbox/Cargo.toml b/toyos-blackbox/Cargo.toml index 3a0d62719f6..ec8ce9c0251 100644 --- a/toyos-blackbox/Cargo.toml +++ b/toyos-blackbox/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "toyos-blackbox" +description = "The page a boot leaves for the next boot to find, and the three things it can say." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-blockhold/Cargo.toml b/toyos-blockhold/Cargo.toml index fc77535926a..347567dec6b 100644 --- a/toyos-blockhold/Cargo.toml +++ b/toyos-blockhold/Cargo.toml @@ -8,6 +8,7 @@ [package] name = "toyos-blockhold" +description = "Who holds each span of a block device, and whose flush answers for the writes its disk lost, pure." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-blockring/Cargo.toml b/toyos-blockring/Cargo.toml index ae73456fa9f..c61271ffd43 100644 --- a/toyos-blockring/Cargo.toml +++ b/toyos-blockring/Cargo.toml @@ -13,6 +13,7 @@ [package] name = "toyos-blockring" +description = "The block protocol between a block service and its client: one shared session of request and completion rings and an arena." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-bootmap/Cargo.toml b/toyos-bootmap/Cargo.toml index 98c8f99ae7a..e54f4983c20 100644 --- a/toyos-bootmap/Cargo.toml +++ b/toyos-bootmap/Cargo.toml @@ -6,6 +6,7 @@ [package] name = "toyos-bootmap" +description = "The bootloader's transient page tables, decided rather than built, pure." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-cc/Cargo.toml b/toyos-cc/Cargo.toml index 7878c8fdfab..e7d8df37fce 100644 --- a/toyos-cc/Cargo.toml +++ b/toyos-cc/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "toyos-cc" +description = "Minimal C compiler: it exists to bootstrap tinycc and compile doomgeneric, not to grow." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-desktop/Cargo.toml b/toyos-desktop/Cargo.toml index 5c2e2aab06b..2caa27ee49a 100644 --- a/toyos-desktop/Cargo.toml +++ b/toyos-desktop/Cargo.toml @@ -7,6 +7,7 @@ [package] name = "toyos-desktop" +description = "Every decision the compositor makes, pure." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-dma/Cargo.toml b/toyos-dma/Cargo.toml index ad0344b3a09..7a4e29b17f4 100644 --- a/toyos-dma/Cargo.toml +++ b/toyos-dma/Cargo.toml @@ -11,6 +11,7 @@ [package] name = "toyos-dma" +description = "Every bound and alignment a DMA view or a device register window checks, pure, forbid(unsafe_code)." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-dns/Cargo.toml b/toyos-dns/Cargo.toml index 47d49b253af..2c2331d2c2d 100644 --- a/toyos-dns/Cargo.toml +++ b/toyos-dns/Cargo.toml @@ -5,6 +5,7 @@ [package] name = "toyos-dns" +description = "A stub DNS resolver's decisions (RFC 1035): the question, which datagram answers it, what it says and when to ask again, pure." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-elf/Cargo.toml b/toyos-elf/Cargo.toml index 7732ad530c0..7542cfac479 100644 --- a/toyos-elf/Cargo.toml +++ b/toyos-elf/Cargo.toml @@ -6,6 +6,7 @@ [package] name = "toyos-elf" +description = "ELF64 decoding of untrusted program images (no_std, no alloc, forbid(unsafe_code))." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-elide/Cargo.toml b/toyos-elide/Cargo.toml index d784535db69..6a59f6c70a6 100644 --- a/toyos-elide/Cargo.toml +++ b/toyos-elide/Cargo.toml @@ -13,6 +13,7 @@ [package] name = "toyos-elide" +description = "Log elision decisions: what a too-wide value keeps, and which records past a site's allowance are left out, pure." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-fat32-check/Cargo.toml b/toyos-fat32-check/Cargo.toml index fb2cc1287d3..d8b7d350b59 100644 --- a/toyos-fat32-check/Cargo.toml +++ b/toyos-fat32-check/Cargo.toml @@ -8,6 +8,7 @@ [package] name = "toyos-fat32-check" +description = "FAT32 volume checker written from Microsoft's fatgen103, the outside judge for every volume this project writes." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-fat32/Cargo.toml b/toyos-fat32/Cargo.toml index 04618071767..0eb076144c5 100644 --- a/toyos-fat32/Cargo.toml +++ b/toyos-fat32/Cargo.toml @@ -16,6 +16,7 @@ [package] name = "toyos-fat32" +description = "FAT32 driver, read and write, over a byte-addressed volume; no format path by design." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-gpt/Cargo.toml b/toyos-gpt/Cargo.toml index 342a3f8586b..a40fa4ad2e9 100644 --- a/toyos-gpt/Cargo.toml +++ b/toyos-gpt/Cargo.toml @@ -6,6 +6,7 @@ [package] name = "toyos-gpt" +description = "GPT parser that finds the partition firmware booted from (no_std, no alloc, forbid(unsafe_code))." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-hda/Cargo.toml b/toyos-hda/Cargo.toml index 8b590798307..a6c8e37fdc2 100644 --- a/toyos-hda/Cargo.toml +++ b/toyos-hda/Cargo.toml @@ -7,6 +7,7 @@ [package] name = "toyos-hda" +description = "HDA codec decoding and output-path selection, pure." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-i219/Cargo.toml b/toyos-i219/Cargo.toml index a8c02605c9c..99b65bdb070 100644 --- a/toyos-i219/Cargo.toml +++ b/toyos-i219/Cargo.toml @@ -12,6 +12,7 @@ [package] name = "toyos-i219" +description = "Every decision the Intel I219 network driver makes, and none of the instructions that carry them out." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-inspect/Cargo.toml b/toyos-inspect/Cargo.toml index 007ded79e7e..6d69715a117 100644 --- a/toyos-inspect/Cargo.toml +++ b/toyos-inspect/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "toyos-inspect" +description = "The inspect protocol: what a running owner of a device or service says about its own state, now." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-libc-copies/Cargo.toml b/toyos-libc-copies/Cargo.toml index 97fe87c4b00..49816bb3e1e 100644 --- a/toyos-libc-copies/Cargo.toml +++ b/toyos-libc-copies/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "toyos-libc-copies" +description = "A host differential test of userland libc's architecture module: its copies, fills and square roots against core's." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-logstream/Cargo.toml b/toyos-logstream/Cargo.toml index 0a132e0745d..7a8e97e04f5 100644 --- a/toyos-logstream/Cargo.toml +++ b/toyos-logstream/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "toyos-logstream" +description = "A boot's log as logd writes and serves it: a program's line beside the kernel's records, init's frame, and a late reader's replay." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-manifest/Cargo.toml b/toyos-manifest/Cargo.toml index 1dfdd9da8b0..f72eecb69eb 100644 --- a/toyos-manifest/Cargo.toml +++ b/toyos-manifest/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "toyos-manifest" +description = "The one definition of /system/etc/system.manifest: what every program in an image may hold, written by the build and read by init." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-mdns/Cargo.toml b/toyos-mdns/Cargo.toml index 46f82a2ca48..49c2a38f29a 100644 --- a/toyos-mdns/Cargo.toml +++ b/toyos-mdns/Cargo.toml @@ -5,6 +5,7 @@ [package] name = "toyos-mdns" +description = "Multicast DNS (RFC 6762) for one machine's own .local name." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-mixer/Cargo.toml b/toyos-mixer/Cargo.toml index 20c7417880d..6b888734b14 100644 --- a/toyos-mixer/Cargo.toml +++ b/toyos-mixer/Cargo.toml @@ -18,6 +18,7 @@ [package] name = "toyos-mixer" +description = "The mixer's decisions (samples, gain, dither, quantize), pure and corpus-certified." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-pci/Cargo.toml b/toyos-pci/Cargo.toml index 9db9c0e6deb..44a402a007a 100644 --- a/toyos-pci/Cargo.toml +++ b/toyos-pci/Cargo.toml @@ -8,6 +8,7 @@ [package] name = "toyos-pci" +description = "A PCI function's BARs and its MSI and MSI-X capabilities, decoded as pure functions." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-pcid/Cargo.toml b/toyos-pcid/Cargo.toml index 402a1d21534..19a5c5d5c0a 100644 --- a/toyos-pcid/Cargo.toml +++ b/toyos-pcid/Cargo.toml @@ -17,6 +17,7 @@ [package] name = "toyos-pcid" +description = "Which PCID a new address space gets, and when a returned one may be reissued, pure." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-proclife/Cargo.toml b/toyos-proclife/Cargo.toml index 9e5fca535d2..edf59ee2a3d 100644 --- a/toyos-proclife/Cargo.toml +++ b/toyos-proclife/Cargo.toml @@ -17,6 +17,7 @@ [package] name = "toyos-proclife" +description = "The process and thread lifecycle's decisions, pure and interleaving-checked." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-ps2/Cargo.toml b/toyos-ps2/Cargo.toml index fa54595ad93..4f9dc9a2e5a 100644 --- a/toyos-ps2/Cargo.toml +++ b/toyos-ps2/Cargo.toml @@ -4,6 +4,7 @@ [package] name = "toyos-ps2" +description = "PS/2 wire decoding: scancodes and mouse packets in, HID usages and pointer deltas out, pure." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-quiesce/Cargo.toml b/toyos-quiesce/Cargo.toml index 82a2a1ef01d..3674884906c 100644 --- a/toyos-quiesce/Cargo.toml +++ b/toyos-quiesce/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "toyos-quiesce" +description = "Every decision the machine's stop makes, and none of its effects." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-rootimage/Cargo.toml b/toyos-rootimage/Cargo.toml index 71a934c6c35..6ebdede04b2 100644 --- a/toyos-rootimage/Cargo.toml +++ b/toyos-rootimage/Cargo.toml @@ -7,6 +7,7 @@ [package] name = "toyos-rootimage" +description = "ROOT in memory: every decision the loader and the kernel make about the image, pure." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-sched/Cargo.toml b/toyos-sched/Cargo.toml index 27cbaad4f06..7cd818e04e7 100644 --- a/toyos-sched/Cargo.toml +++ b/toyos-sched/Cargo.toml @@ -5,6 +5,7 @@ [package] name = "toyos-sched" +description = "The scheduler core: a sans-IO state machine the kernel and the host simulator drive through one Hw boundary." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-sched/loom/Cargo.toml b/toyos-sched/loom/Cargo.toml index 9c13059b368..e6a978c80a9 100644 --- a/toyos-sched/loom/Cargo.toml +++ b/toyos-sched/loom/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "toyos-sched-loom" +description = "Loom models of the toyos-sched primitives, over the same sources compiled a second time." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-sched/sim/Cargo.toml b/toyos-sched/sim/Cargo.toml index c29a97c3605..eed75e2558b 100644 --- a/toyos-sched/sim/Cargo.toml +++ b/toyos-sched/sim/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "toyos-sched-sim" +description = "The deterministic host simulator for toyos-sched: machine, explorer, shrinker and scenario corpus." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-swap/Cargo.toml b/toyos-swap/Cargo.toml index 510bf16509b..4e5297e966b 100644 --- a/toyos-swap/Cargo.toml +++ b/toyos-swap/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "toyos-swap" +description = "Replacing a running service's binary: what swap, init and the host say about it, and init's decisions, pure." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-symbols/Cargo.toml b/toyos-symbols/Cargo.toml index 20651416d38..9fc7edb5d3c 100644 --- a/toyos-symbols/Cargo.toml +++ b/toyos-symbols/Cargo.toml @@ -10,6 +10,7 @@ [package] name = "toyos-symbols" +description = "Backtrace symbol lookup: locating an ELF's symbol tables and budgeting the demangled name (no_std, no alloc, forbid(unsafe_code))." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-tco/Cargo.toml b/toyos-tco/Cargo.toml index b09dd2744a7..acd7dabb70f 100644 --- a/toyos-tco/Cargo.toml +++ b/toyos-tco/Cargo.toml @@ -7,6 +7,7 @@ [package] name = "toyos-tco" +description = "Every bound a boot arms and the metal loop waits on, and Intel's TCO watchdog register block and where a chipset keeps it." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-tmpdir/Cargo.toml b/toyos-tmpdir/Cargo.toml index c90ea44e390..b20df924d3a 100644 --- a/toyos-tmpdir/Cargo.toml +++ b/toyos-tmpdir/Cargo.toml @@ -3,6 +3,7 @@ [package] name = "toyos-tmpdir" +description = "A scratch directory that is gone when its holder is, on every way out." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-untrusted/Cargo.toml b/toyos-untrusted/Cargo.toml index 83e3dc98028..954395819e7 100644 --- a/toyos-untrusted/Cargo.toml +++ b/toyos-untrusted/Cargo.toml @@ -11,6 +11,7 @@ [package] name = "toyos-untrusted" +description = "A number that crossed a trust boundary, and the four ways it gets out." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-update/Cargo.toml b/toyos-update/Cargo.toml index 3d24ef3ef42..09d026ca537 100644 --- a/toyos-update/Cargo.toml +++ b/toyos-update/Cargo.toml @@ -5,6 +5,7 @@ [package] name = "toyos-update" +description = "A signed image, the slots it installs into, and every decision the loader and update make about one, pure." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-userbound/Cargo.toml b/toyos-userbound/Cargo.toml index 2a27bbb14ed..c067012f3fb 100644 --- a/toyos-userbound/Cargo.toml +++ b/toyos-userbound/Cargo.toml @@ -12,6 +12,7 @@ [package] name = "toyos-userbound" +description = "Every decision the kernel makes about the user/kernel boundary, pure." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-userpin/Cargo.toml b/toyos-userpin/Cargo.toml index 1467d06327c..cd3cbad2ac4 100644 --- a/toyos-userpin/Cargo.toml +++ b/toyos-userpin/Cargo.toml @@ -7,6 +7,7 @@ # test, not a library the kernel links. [package] name = "toyos-userpin" +description = "The user-copy window's pin invariant, checked over every park interleaving." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-wallclock/Cargo.toml b/toyos-wallclock/Cargo.toml index 13c15365699..b3ec41c11b9 100644 --- a/toyos-wallclock/Cargo.toml +++ b/toyos-wallclock/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "toyos-wallclock" +description = "The calendar, and the zone offset userland has to recover, pure." version = "0.1.0" edition = "2024" license = "MIT OR Apache-2.0" diff --git a/toyos-xhci/Cargo.toml b/toyos-xhci/Cargo.toml index 59f2bb0c180..e0e19625e2c 100644 --- a/toyos-xhci/Cargo.toml +++ b/toyos-xhci/Cargo.toml @@ -5,6 +5,7 @@ [package] name = "toyos-xhci" +description = "The xHCI root-hub port machine, as a decision separated from its effects." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-xhci/sim/Cargo.toml b/toyos-xhci/sim/Cargo.toml index b74e6e1cddc..64873765858 100644 --- a/toyos-xhci/sim/Cargo.toml +++ b/toyos-xhci/sim/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "toyos-xhci-sim" +description = "The host simulator for the xHCI port machine." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" From 5f527da4100bdac4533483a36505132ae73d09b8 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 14:00:11 +0200 Subject: [PATCH 08/12] Answer the review of #537: gone roots, re-exports, the ack wait, one ls-files, one path scanner The citation gate read only roots git tracks now, so deleting a whole root directory silenced every citation into it. A token shaped like a file (`name/.../file.ext` or `name/.../`, lowercase) whose first component is no tracked root, no directory below one and no named foreign one is now a citation of a root that is gone, and reds. The foreign names are the fourteen the tracker quotes today: QEMU's, the std fork's, forked crates, a game repository, host capture directories. The bare `name/` form is not read, because the tracker uses it in prose for directories that do not exist yet. c4fc16ea's "(a move that strands one is refused)" was not true of a whole root until this commit. The arch gate accepted any `crate::arch::...` path, so a name arch re-exports from above (`invalidation::Origin`, the `mm::policy` four) reached every arch file without a row. The gate now reads what arch re-exports from above and counts a path that passes through one, via `crate::arch`, a root alias or a `super` chain, as naming its origin. x86_64/paging.rs's `crate::arch::tlb::Origin` gets its `invalidation` entry. A `super` after `self::` starts a chain, an inline `mod x {}` deepens the chain a `super` needs, a char literal (`'"'`) no longer hides the rest of its line from `code_only`, and a row naming a file outside arch/ reds. The list shrinks by item only; the header says review holds the rest. The two path scanners are one, `spelled_paths`, which takes its roots (`core`/`std`, or `crate` plus a module depth for `super` chains) and expands use groups. Both gates read it. Glob and rename are still refused whole, and a group's `self as k` is now a rename for the crate gate too. tlb.rs asserts that ACK_TIMEOUT is no shorter than time::DEAF_CPU, so the shootdown wait itself is bound to the tripwire the xHCI IF-clear spin sits under, not just the constant. The five `git ls-files` wrappers become one, sysroot::tracked_files, used by licence.rs, assets.rs, sourcegate.rs at all three sites, and citations.rs. Removed, as the review asked: src/CLAUDE.md's "Documentation carries no gates" (the orchestrator authorised the edit), the tlb.rs sentence that said every masking spin held itself under the wait, bcachefs's "interim ToyOS format" clause, and the arch issue's "enumerates every such name" with the "only shrinks" clause beside it. The arch issue is now owed by the ARM track, issues/kernel/toyos-runs-on-arm64.md. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK --- bcachefs/Cargo.toml | 2 +- ...he-arch-layer-names-the-kernel-above-it.md | 8 +- kernel/src/arch/x86_64/tlb.rs | 6 +- src/CLAUDE.md | 1 - src/assets.rs | 18 +- src/citations.rs | 123 ++-- src/licence.rs | 20 +- src/sourcegate.rs | 573 ++++++++++-------- src/sysroot.rs | 7 + 9 files changed, 406 insertions(+), 352 deletions(-) diff --git a/bcachefs/Cargo.toml b/bcachefs/Cargo.toml index f719d59f17a..3c5177cae10 100644 --- a/bcachefs/Cargo.toml +++ b/bcachefs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bcachefs" -description = "The /home filesystem: a read side of the real bcachefs on-disk format, and the interim ToyOS format the kernel still links." +description = "The /home filesystem: a read side of the real bcachefs on-disk format." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/issues/kernel/the-arch-layer-names-the-kernel-above-it.md b/issues/kernel/the-arch-layer-names-the-kernel-above-it.md index e539606b31c..ddeac00cf62 100644 --- a/issues/kernel/the-arch-layer-names-the-kernel-above-it.md +++ b/issues/kernel/the-arch-layer-names-the-kernel-above-it.md @@ -9,10 +9,8 @@ opened: 2026-09-27 `kernel/src/arch/` is meant to be the machine and nothing else: generic code reaches it through `crate::arch::…`, and it reaches nothing back. It does. Its files name kernel modules above it by path — the scheduler, the process table, -the log, drivers, the IOMMU layer, test actuators — and `ARCH_REACHES_UP` in -`src/sourcegate.rs` enumerates every such name, per file. The gate beside it -reds on a name a file adds and on a listed name a file no longer spells, so the -list only shrinks; each entry is this file's debt. +the log, drivers, the IOMMU layer, test actuators. The gate +reds on a name a file adds and on a listed name a file no longer spells; each entry is this file's debt. What the list holds, by kind: @@ -33,7 +31,7 @@ Why it matters: the AArch64 port inherits whatever x86-64 exports, so every upward name here is one the port has to satisfy or stub, and until the gate nothing but review stopped the next. -Owed by the arch-contract work: an `arch/api.rs` contract, a trait implemented +Owed by the ARM track, `issues/kernel/toyos-runs-on-arm64.md`: an `arch/api.rs` contract, a trait implemented by a zero-sized type and dispatched statically, that is the only way generic code reaches the machine and the only way the machine reaches back; and `platform/{pc,virt}` for the PC's own devices, so `arch/` is the ISA alone. diff --git a/kernel/src/arch/x86_64/tlb.rs b/kernel/src/arch/x86_64/tlb.rs index 668f29d9a75..6dc5cce7148 100644 --- a/kernel/src/arch/x86_64/tlb.rs +++ b/kernel/src/arch/x86_64/tlb.rs @@ -59,10 +59,12 @@ pub fn log_census() { } /// How long the initiator waits for one CPU's flush: a target that has not -/// answered by then is not taking interrupts. Every spin that masks them holds -/// itself under this at its own site; the architecture knows none of them. +/// answered by then is not taking interrupts. const ACK_TIMEOUT: Tripwire = crate::time::DEAF_CPU; +// A spin with interrupts masked is held under `DEAF_CPU` at its own site, so this wait is no shorter. +const _: () = assert!(ACK_TIMEOUT.nanos() >= crate::time::DEAF_CPU.nanos()); + /// Spins between deadline checks; `nanos_since_boot`'s 128-bit divide is too /// costly to call on every iteration. const SPINS_PER_DEADLINE_CHECK: u32 = 1024; diff --git a/src/CLAUDE.md b/src/CLAUDE.md index 8a8d04458ff..cb105b0b324 100644 --- a/src/CLAUDE.md +++ b/src/CLAUDE.md @@ -33,7 +33,6 @@ Loads when you read a file under `src/` — the root cargo project, package name ## Caveats that bite every agent -- **Documentation carries no gates** — `src/redlist.rs` resolves doc paths only because it gates a Rust table, not a corpus. - **Every CI lane is GitHub-hosted and no workflow may name a self-hosted label** — a `runs-on:` naming one queues until it times out rather than failing, so `src/ci.rs`'s `workflows_run_against_main_on_hosted_runners` refuses it; a measurement owed on hardware goes to the metal loop, not to a runner. - **A workflow job that runs in a container adds `safe.directory` itself** — `actions/checkout` sets it into a temporary global config it discards when its step ends, so the first git command a container step runs after checkout dies on a dubiously-owned repository. - **A red build may be the build system — re-run in isolation before believing any single red.** A `stage1-std//dist/deps` temp-dir error means a concurrent build, never a broken checkout; never repair or force-rebuild the toolchain. diff --git a/src/assets.rs b/src/assets.rs index cdc102447e6..f4f0bac9057 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -1,7 +1,6 @@ use std::collections::BTreeSet; use std::fs; use std::path::{Path, PathBuf}; -use std::process::Command; /// Rasterize `codepoints` into `cell_width * cell_height` 8-bit alpha cells, /// laid out one cell after another. The pixel size is the largest at which @@ -216,21 +215,7 @@ pub fn regen_panic_font(root: &Path) { /// again. A build that cannot find out what is committed refuses, because it /// cannot honestly build an image either. fn tracked(dir: &Path) -> BTreeSet { - let out = Command::new("git") - .args(["-C", &dir.display().to_string(), "ls-files", "-z"]) - .output() - .unwrap_or_else(|e| panic!("asking git what it tracks under {}: {e}", dir.display())); - assert!( - out.status.success(), - "git could not list {}: {}", - dir.display(), - String::from_utf8_lossy(&out.stderr).trim() - ); - String::from_utf8_lossy(&out.stdout) - .split('\0') - .filter(|line| !line.is_empty()) - .map(PathBuf::from) - .collect() + crate::sysroot::tracked_files(dir, &[]).into_iter().map(PathBuf::from).collect() } /// The paths `declared` names under `dir` that are not there, in the order they @@ -367,6 +352,7 @@ pub fn collect(dirs: &[String], programs: &BTreeSet<&str>) -> Vec<(String, Vec^:src/durations.rs`), and a path //! that does not exist yet is written relative to its crate (`arch/api.rs`). -//! None of the three begins with a root, so none is read. A crate-relative -//! citation of a real file (`sched/dump.rs`) is not read either — that is the -//! hole this scan leaves. +//! +//! **The holes it leaves.** A crate-relative citation (`sched/dump.rs`) is not +//! read, even of a real file, and neither is a gone root whose name is also a +//! directory below one. A `^:` citation is trusted, never resolved: +//! resolving one needs the history, which the host job's depth-one checkout +//! does not have. //! //! Only its own tests read this, so it is not compiled into the build system. @@ -43,6 +52,15 @@ use std::path::Path; /// header gives. const NOT_READ: &[&str] = &["rust", ".cargo"]; +/// First components the tracker writes that name something outside this tree: +/// QEMU's `hw/` and `chardev/`, the std fork's `library/`, `pal/` and `base/`, +/// the forked or quoted crates, a game's repository, and the host directories a +/// capture was read from. +const FOREIGN: &[&str] = &[ + "base", "chardev", "debug", "gbae", "hw", "library", "memmap2", "mio", "pal", "scratchpad", + "smoltcp-0.12.0", "softbuffer", "t14-run122", "t14-run76", +]; + /// Characters that make a token a pattern rather than a path. const PATTERN: &[char] = &['*', '{', '}', '<', '>', '…', '$']; @@ -53,38 +71,51 @@ fn delimits(c: char) -> bool { /// Every file `git` tracks, repository-relative, and every directory above one. pub fn tracked(root: &Path) -> BTreeSet { - let out = std::process::Command::new("git") - .args(["ls-files", "-z"]) - .current_dir(root) - .output() - .unwrap_or_else(|e| panic!("git ls-files: {e}")); - assert!(out.status.success(), "git ls-files failed: {}", String::from_utf8_lossy(&out.stderr)); let mut all = BTreeSet::new(); - for file in String::from_utf8(out.stdout).expect("git ls-files is not UTF-8").split('\0') { - if file.is_empty() { - continue; - } + for file in crate::sysroot::tracked_files(root, &[]) { let mut at = 0; while let Some(slash) = file[at..].find('/') { all.insert(file[..at + slash].to_string()); at += slash + 1; } - all.insert(file.to_string()); + all.insert(file); } all } -/// The tracked directories at the root whose citations are read. -pub fn roots(tracked: &BTreeSet) -> BTreeSet { - tracked - .iter() - .filter_map(|p| p.split_once('/').map(|(root, _)| root.to_string())) - .filter(|root| !NOT_READ.contains(&root.as_str())) - .collect() +/// What a first component can mean: the directories `tracked` holds at the +/// root, and the names of those below it. +pub struct Names { + roots: BTreeSet, + below: BTreeSet, +} + +pub fn names(tracked: &BTreeSet) -> Names { + let (mut roots, mut below) = (BTreeSet::new(), BTreeSet::new()); + for path in tracked { + let mut dirs = path.split('/').rev().skip(1).collect::>(); + if let Some(root) = dirs.pop() { + roots.insert(root.to_string()); + } + below.extend(dirs.into_iter().map(String::from)); + } + Names { roots, below } +} + +/// Whether `first/rest` is shaped like a file or a directory rather than prose +/// (`and/or`, `A/B`, `44100/2ch`). +fn path_shaped(first: &str, rest: &str) -> bool { + let lowercase = |s: &str| s.starts_with(|c: char| c.is_ascii_lowercase()); + let extension = rest.rsplit('/').next().and_then(|last| last.rsplit_once('.')).is_some_and(|(stem, ext)| { + !stem.is_empty() && lowercase(ext) && ext.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()) + }); + lowercase(first) + && first.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '-' | '_' | '.')) + && (rest.ends_with('/') || extension) } /// Every path `line` cites, with its trailing `/` kept off. -pub fn cited(line: &str, roots: &BTreeSet) -> Vec { +pub fn cited(line: &str, names: &Names) -> Vec { let mut found = Vec::new(); for token in line.split(delimits) { if token.contains(PATTERN) { @@ -103,20 +134,23 @@ pub fn cited(line: &str, roots: &BTreeSet) -> Vec { continue; } } - let Some((root, _)) = path.split_once('/') else { continue }; - if !roots.contains(root) || path.split('/').any(|segment| segment == "target") { + let Some((first, rest)) = path.split_once('/') else { continue }; + if NOT_READ.contains(&first) || path.split('/').any(|segment| segment == "target") { continue; } - found.push(path.trim_end_matches('/').to_string()); + let gone = !names.below.contains(first) && !FOREIGN.contains(&first) && path_shaped(first, rest); + if names.roots.contains(first) || gone { + found.push(path.trim_end_matches('/').to_string()); + } } found } /// Every `file:line: cites path` in `text` that `tracked` does not hold. -pub fn dead(file: &str, text: &str, roots: &BTreeSet, tracked: &BTreeSet) -> Vec { +pub fn dead(file: &str, text: &str, names: &Names, tracked: &BTreeSet) -> Vec { let mut out = Vec::new(); for (n, line) in text.lines().enumerate() { - for path in cited(line, roots) { + for path in cited(line, names) { if !tracked.contains(&path) { out.push(format!("{file}:{}: cites {path}, which this tree does not hold", n + 1)); } @@ -156,19 +190,19 @@ mod tests { fn every_path_the_tracker_and_the_claude_files_cite_exists() { let root = repo_root(); let tracked = tracked(&root); - let roots = roots(&tracked); + let names = names(&tracked); let files = citing(&tracked); assert!( files.iter().any(|f| f == "issues/README.md") && files.iter().any(|f| f == "CLAUDE.md"), "the walk reached neither the tracker nor the root CLAUDE.md, so it reads nothing: {files:?}" ); - assert!(roots.contains("kernel") && roots.contains("src") && roots.contains("issues"), "{roots:?}"); + assert!(["kernel", "src", "issues"].iter().all(|r| names.roots.contains(*r)), "{:?}", names.roots); let mut complaints = Vec::new(); let mut read = 0; for file in &files { let text = std::fs::read_to_string(root.join(file)).unwrap_or_else(|e| panic!("read {file}: {e}")); - read += text.lines().map(|l| cited(l, &roots).len()).sum::(); - complaints.extend(dead(file, &text, &roots, &tracked)); + read += text.lines().map(|l| cited(l, &names).len()).sum::(); + complaints.extend(dead(file, &text, &names, &tracked)); } assert!(read > 500, "only {read} citations read across {} files; the scan is reading nothing", files.len()); assert!( @@ -181,23 +215,25 @@ mod tests { } /// Teeth: a dead citation reds, naming the file, the line and the path; a - /// live one, a directory and a line-suffixed one do not. + /// live one, a directory and a line-suffixed one do not; and a root the + /// tree no longer holds is read rather than forgotten. #[test] fn a_dead_citation_is_named_and_a_live_one_is_not() { let tracked = set(&["kernel", "kernel/src", "kernel/src/vfs.rs", "issues", "issues/README.md"]); - let roots = roots(&tracked); - let text = "See `kernel/src/vfs.rs:32` and kernel/src/.\nThen `kernel/src/gone.rs`, and issues/README.md.\n"; - assert_eq!( - dead("issues/x/y.md", text, &roots, &tracked), - ["issues/x/y.md:2: cites kernel/src/gone.rs, which this tree does not hold"], - ); + let names = names(&tracked); + let text = "See `kernel/src/vfs.rs:32` and kernel/src/.\nThen `kernel/src/gone.rs`, and issues/README.md.\n\ + `toyos-cc/src/lower.rs:9` and toyos-cc/tests/, not toyos-cc/, and/or `src/vfs.rs` in A/B.\n"; + assert_eq!(dead("issues/x/y.md", text, &names, &tracked), [ + "issues/x/y.md:2: cites kernel/src/gone.rs, which this tree does not hold", + "issues/x/y.md:3: cites toyos-cc/src/lower.rs, which this tree does not hold", + "issues/x/y.md:3: cites toyos-cc/tests, which this tree does not hold", + ]); } /// What another namespace spells is not read as ours. #[test] fn a_pattern_a_panic_location_build_output_and_a_foreign_path_are_not_citations() { - let roots = set(&["kernel", "src", "tests", "rust", ".cargo"]); - let roots = roots.into_iter().filter(|r| !NOT_READ.contains(&r.as_str())).collect(); + let names = names(&set(&["kernel/src/arch/tlb.rs", "src/lib.rs", "tests/a", "rust/x", ".cargo/c"])); for line in [ "PANIC: panicked at src/arch/tlb.rs:171:42:", "LOCK CONTENTION: 50M spins at src/vfs.rs:32:18, ticket=38", @@ -207,10 +243,11 @@ mod tests { "the fork's `rust/src/bootstrap/` and a `.cargo/config.toml`", "`mio/src/sys/toyos/waker.rs` and `d5c2d9c9^:src/durations.rs`", "https://github.com/ToyOSOrg/ToyOS/blob/main/src/lib.rs", + "`arch/api.rs`, 44100/2ch/i16, read/write and A/B", ] { - assert!(cited(line, &roots).is_empty(), "{line:?} read as {:?}", cited(line, &roots)); + assert!(cited(line, &names).is_empty(), "{line:?} read as {:?}", cited(line, &names)); } - assert_eq!(cited("`src/redlist.rs`'s row, `kernel/src/a.rs:1-9`.", &roots), ["src/redlist.rs", "kernel/src/a.rs"]); - assert_eq!(cited("(src/x.rs) [kernel/y/](kernel/y/)", &roots), ["src/x.rs", "kernel/y", "kernel/y"]); + assert_eq!(cited("`src/redlist.rs`'s row, `kernel/src/a.rs:1-9`.", &names), ["src/redlist.rs", "kernel/src/a.rs"]); + assert_eq!(cited("(src/x.rs) [kernel/y/](kernel/y/)", &names), ["src/x.rs", "kernel/y", "kernel/y"]); } } diff --git a/src/licence.rs b/src/licence.rs index 020d1951371..1ade4df95b5 100644 --- a/src/licence.rs +++ b/src/licence.rs @@ -1151,22 +1151,6 @@ fn metadata( serde_json::from_slice(&out).map_err(|e| format!("cargo metadata printed no JSON: {e}")) } -/// The tracked files `pathspecs` name, root-relative. -fn ls_files(root: &Path, pathspecs: &[String]) -> Result, String> { - let out = run( - Command::new("git") - .args(["ls-files", "-z", "--"]) - .args(pathspecs) - .current_dir(root), - "git ls-files", - )?; - Ok(String::from_utf8_lossy(&out) - .split('\0') - .filter(|f| !f.is_empty()) - .map(String::from) - .collect()) -} - /// The fork's `library/`, checked out at the commit this tree pins. A checkout /// whose `rust/` was never initialised — a CI runner's — fetches that commit /// alone. @@ -1264,7 +1248,7 @@ pub fn judge(root: &Path) -> Result { packages: local.dirs.iter().filter_map(|d| relative(d)).filter(|d| !d.is_empty()).collect(), named: BTreeSet::new(), }; - let tracked = ls_files(root, &[])?; + let tracked = crate::sysroot::tracked_files(root, &[]); let names: BTreeSet<&str> = COMMITTED_FILES.iter().map(|(p, ..)| file_name(p)).collect(); let sources = tracked.iter().filter(|f| under(&shipping.packages, f) && f.ends_with(".rs")); let read = sources @@ -1282,7 +1266,7 @@ pub fn judge(root: &Path) -> Result { let sections = sections(¬ice); let mut files = BTreeMap::new(); for section in §ions { - let named = ls_files(root, &[format!(":(glob){}", section.path)])?; + let named = crate::sysroot::tracked_files(root, &[&format!(":(glob){}", section.path)]); files.insert(section.path.clone(), named); } judge_notice(§ions, &files, COMMITTED_FILES, &shipping, &mut report); diff --git a/src/sourcegate.rs b/src/sourcegate.rs index 6095252d638..73bdda92471 100644 --- a/src/sourcegate.rs +++ b/src/sourcegate.rs @@ -71,9 +71,10 @@ //! //! The eleventh holds the kernel's arch layer below the rest: a file under //! `kernel/src/arch/` may name, by `crate::`, `$crate::` or a `super::` chain -//! out of it, only the architecture and the root's re-exports of it. What it -//! names beyond that today is [`ARCH_REACHES_UP`], per file, and that list -//! only shrinks. +//! out of it, only the architecture and the root's re-exports of it, and a name +//! the layer itself re-exports from above counts as where it came from. What it +//! names beyond that today is [`ARCH_REACHES_UP`], per file and item: the gate +//! holds that list shrinking by item, and review holds the rest. //! //! **What none of them reaches is filed rather than implied**, and each table's //! own doc names its half: the entries under `issues/build/` say so. @@ -351,7 +352,7 @@ const RETIRED_ABI_NAMES: &[&str] = &[ const GUEST_TREES: &[&str] = &["kernel/src", "toyos/src", "toyos-abi/src", "userland", "tests"]; -/// `line` with its comment and its string literals removed. +/// `line` with its comment and its string and char literals removed. /// /// What is left is the part that names things. Prose explaining what a deleted /// call used to do is legal and worth keeping; a gravestone table mapping a @@ -367,6 +368,21 @@ fn code_only(line: &str) -> String { } '"' => in_string = !in_string, '/' if !in_string && chars.peek() == Some(&'/') => break, + // A char literal is a literal (`'"'` opens no string); a lifetime or a label is code. + '\'' if !in_string => { + let mut ahead = chars.clone(); + match (ahead.next(), ahead.next()) { + (Some('\\'), _) => { + chars.next(); + chars.next(); + while chars.next().is_some_and(|c| c != '\'') {} + } + (Some(_), Some('\'')) => { + chars.nth(1); + } + _ => out.push(c), + } + } _ if !in_string => out.push(c), _ => {} } @@ -1231,24 +1247,6 @@ fn used_actions(text: &str) -> Vec<(String, usize)> { .collect() } -/// Every `.rs` file `git` tracks under `tree`, repository-relative — the list -/// the walks above are held against, read from something that is not a walk. -#[cfg(test)] -fn tracked_rust_files(root: &Path, tree: &str) -> std::collections::BTreeSet { - let out = std::process::Command::new("git") - .args(["ls-files", "-z", "--", tree]) - .current_dir(root) - .output() - .unwrap_or_else(|e| panic!("git ls-files {tree}: {e}")); - assert!(out.status.success(), "git ls-files {tree} failed"); - String::from_utf8(out.stdout) - .expect("git ls-files is not UTF-8") - .split('\0') - .filter(|p| p.ends_with(".rs")) - .map(str::to_string) - .collect() -} - // ── Architecture rules ────────────────────────────────────────────────────── /// One architecture rule, stated as where its spellings may appear: a set of @@ -1282,131 +1280,199 @@ const ASSEMBLY: &[&str] = &["asm!", "global_asm!", "naked_asm!", "#[naked]", "un #[cfg(test)] const ARCH_MODULE: &str = "core::arch/std::arch"; -/// The 0-based lines of `text` on which a path names `core::arch` or -/// `std::arch`, or makes a name that can: `core::arch::asm!`, -/// `use core::arch as isa;`, an `arch` that begins an element of a `core::{…}` -/// group, over any number of lines — and any glob of `core`/`std` itself -/// (`use core::*;`, `core::{*}`) or rename of it (`use core as k;`, -/// `extern crate core as k;`, `core::{self as k}`). -/// -/// **The glob and the rename are refused whole**, not followed to an `arch` -/// after them: what they bring into scope is read by resolution, which a line -/// scan does not have, and code outside an arch module has no other use for -/// either. Comments and string literals are not code ([`code_only`]). +/// A path code spells from a root: the 0-based line of its last segment, the +/// segments after the root, and the name a rename binds. A group is one path +/// per element, a glob ends in `*`, a rename of the root itself (`core as k`, +/// `core::{self as k}`) is `self` renamed, and a `super` chain that stays below +/// the crate root begins with `super`. #[cfg(test)] -fn arch_module_lines(text: &str) -> Vec { - let code: Vec = text.lines().map(code_only).collect::>().join("\n").chars().collect(); - let word = |c: char| c.is_ascii_alphanumeric() || c == '_'; - let line_of = |at: usize| code[..at].iter().filter(|c| **c == '\n').count(); - let skip_space = |mut at: usize| { - while code.get(at).is_some_and(|c| c.is_whitespace()) { +struct Spelled { + line: usize, + segments: Vec, + alias: Option, +} + +/// Every path in `text` from one of `roots` (`core`, `crate`, which `$crate` +/// spells too) and, given the module's `depth` below the crate root, from a +/// `super` chain, `self::` before it and each inline `mod x {}` around it +/// counted. The root's rename counts only where it imports: `use core as k;`, +/// `extern crate core as k;`, an element of a `use` group. A glob or a rename +/// is not followed to what it brings into scope: that is resolution, which a +/// scan does not have. +#[cfg(test)] +fn spelled_paths(text: &str, roots: &[&str], depth: Option) -> Vec { + let code = Code(text.lines().map(code_only).collect::>().join("\n").chars().collect()); + let mut found = Vec::new(); + // Whether each open `{` is an inline module's, and whether the next one is. + let (mut scopes, mut opens_mod) = (Vec::new(), false); + let mut at = 0; + while at < code.0.len() { + match code.0[at] { + '{' if depth.is_some() => scopes.push(std::mem::take(&mut opens_mod)), + '}' if depth.is_some() => _ = scopes.pop().expect("a `}` closes nothing: a literal's brace reached the code"), + _ => {} + } + let Some((name, end)) = code.ident_at(at) else { at += 1; + continue; + }; + let after = code.skip_space(end); + let mut prefix = Vec::new(); + let root_end = match depth { + _ if roots.contains(&name.as_str()) => Some(after), + Some(depth) if name == "super" && code.ident_before(at).1 != "super" => { + let (mut supers, mut next) = (1, after); + while let Some((_, e)) = + code.colons(next).then(|| code.ident_at(code.skip_space(next + 2))).flatten().filter(|(s, _)| s == "super") + { + supers += 1; + next = code.skip_space(e); + } + if supers < depth + scopes.iter().filter(|m| **m).count() { + prefix.push("super".to_string()); + } + Some(next) + } + _ => None, + }; + if name == "mod" { + opens_mod = code.ident_at(after).is_some_and(|(_, e)| code.0.get(code.skip_space(e)) == Some(&'{')); } - at - }; - let ident_at = |at: usize, name: &str| { - let end = at + name.len(); - code.get(at..end).is_some_and(|s| s.iter().copied().eq(name.chars())) - && !code.get(end).is_some_and(|c| word(*c)) - && !at.checked_sub(1).and_then(|j| code.get(j)).is_some_and(|c| word(*c)) - }; - // The identifier that ends before `at`, over whitespace and `::`. - let ident_before = |at: usize| { - let mut end = at; - while end > 0 && (code[end - 1].is_whitespace() || code[end - 1] == ':') { - end -= 1; + match root_end { + Some(next) if code.colons(next) => code.tree(next + 2, prefix, &mut found), + Some(next) if prefix.is_empty() && code.imported(at) => { + let alias = code.alias_at(next); + let segments = vec!["self".to_string()]; + found.extend(alias.is_some().then(|| Spelled { line: code.line_of(at), segments, alias })); + } + _ => {} } - let mut start = end; - while start > 0 && word(code[start - 1]) { - start -= 1; + at = end; + } + found +} + +/// Code as characters, its comments and literals gone ([`code_only`]). +#[cfg(test)] +struct Code(Vec); + +#[cfg(test)] +impl Code { + /// Every path the use tree at `at` spells after `prefix`, into `out`. + fn tree(&self, at: usize, prefix: Vec, out: &mut Vec) { + let at = self.skip_space(at); + let line = self.line_of(at); + if self.0.get(at) == Some(&'*') { + out.push(Spelled { line, segments: [prefix, vec!["*".to_string()]].concat(), alias: None }); + } else if self.0.get(at) == Some(&'{') { + // Each element begins after the `{` or after a `,` at depth one. + self.tree(at + 1, prefix.clone(), out); + let (mut depth, mut i) = (0usize, at + 1); + while let Some(&c) = self.0.get(i) { + match c { + '{' => depth += 1, + '}' if depth == 0 => break, + '}' => depth -= 1, + ',' if depth == 0 => self.tree(i + 1, prefix.clone(), out), + _ => {} + } + i += 1; + } + } else if let Some((name, end)) = self.ident_at(at) { + let (after, segments) = (self.skip_space(end), [prefix, vec![name]].concat()); + match self.colons(after) { + true => self.tree(after + 2, segments, out), + false => out.push(Spelled { line, segments, alias: self.alias_at(after) }), + } } - (start, code[start..end].iter().collect::()) - }; - // Whether `at` begins an item that imports: `use …`, or `extern crate …`, - // or an element of a `use` group. - let imported = |at: usize| { - let mut at = at; + } + + /// The name bound by an `as` at `at`. + fn alias_at(&self, at: usize) -> Option { + let (_, end) = self.ident_at(at).filter(|(word, _)| word == "as")?; + self.ident_at(self.skip_space(end)).map(|(name, _)| name) + } + + fn line_of(&self, at: usize) -> usize { + self.0[..at].iter().filter(|c| **c == '\n').count() + } + + fn skip_space(&self, at: usize) -> usize { + (at..).find(|&j| !self.0.get(j).is_some_and(|c| c.is_whitespace())).unwrap_or(at) + } + + fn colons(&self, at: usize) -> bool { + self.0.get(at..at + 2) == Some(&[':', ':'][..]) + } + + fn word(&self, at: usize) -> bool { + self.0.get(at).is_some_and(|c| c.is_ascii_alphanumeric() || *c == '_') + } + + /// The identifier that begins at `at`, and where it ends. + fn ident_at(&self, at: usize) -> Option<(String, usize)> { + if at.checked_sub(1).is_some_and(|j| self.word(j)) { + return None; + } + let end = (at..).find(|&j| !self.word(j)).unwrap_or(at); + (end > at).then(|| (self.0[at..end].iter().collect(), end)) + } + + /// The identifier that ends before `at`, over whitespace and `::`, and where it begins. + fn ident_before(&self, at: usize) -> (usize, String) { + let end = (0..at).rev().find(|&j| !self.0[j].is_whitespace() && self.0[j] != ':').map_or(0, |j| j + 1); + let start = (0..end).rev().find(|&j| !self.word(j)).map_or(0, |j| j + 1); + (start, self.0[start..end].iter().collect()) + } + + /// Whether `at` begins an item that imports: `use …`, or `extern crate …`, + /// or an element of a `use` group. + fn imported(&self, mut at: usize) -> bool { loop { - let (start, ident) = ident_before(at); + let (start, ident) = self.ident_before(at); match ident.as_str() { "use" => return true, - "crate" => return ident_before(start).1 == "extern", + "crate" => return self.ident_before(start).1 == "extern", "" => {} _ => return false, } // Not after an identifier: inside a group only if a `{` opens it. - let mut back = start; - while back > 0 && code[back - 1].is_whitespace() { - back -= 1; - } - match back.checked_sub(1).map(|j| code[j]) { - Some('{') => at = back - 1, - Some(',') => { + let back = (0..start).rev().find(|&j| !self.0[j].is_whitespace()); + match back.map(|j| (j, self.0[j])) { + Some((j, '{')) => at = j, + Some((j, ',')) => { let mut depth = 0usize; - let mut j = back - 1; - loop { - let Some(k) = j.checked_sub(1) else { return false }; - j = k; - match code[j] { - '}' => depth += 1, - '{' if depth == 0 => break, - '{' => depth -= 1, - ';' => return false, - _ => {} - } + let open = (0..j).rev().find(|&k| match self.0[k] { + '}' => { depth += 1; false } + '{' if depth == 0 => true, + '{' => { depth -= 1; false } + _ => false, + }); + match open { + Some(k) if !self.0[k..j].contains(&';') => at = k, + _ => return false, } - at = j; } _ => return false, } } - }; - let mut lines = Vec::new(); - for at in 0..code.len() { - let Some(root) = ["core", "std"].into_iter().find(|root| ident_at(at, root)) else { continue }; - let after = skip_space(at + root.len()); - if ident_at(after, "as") && imported(at) { - lines.push(line_of(at)); - continue; - } - let colons = after; - if code.get(colons..colons + 2) != Some(&[':', ':'][..]) { - continue; - } - let next = skip_space(colons + 2); - if ident_at(next, "arch") || code.get(next) == Some(&'*') { - lines.push(line_of(next)); - } else if code.get(next) == Some(&'{') { - // Each element of the group begins after its `{` or a `,` at depth one. - let (mut depth, mut i, mut element) = (0usize, next, true); - while let Some(&c) = code.get(i) { - match c { - '{' => { - depth += 1; - element = depth == 1; - } - '}' => { - depth -= 1; - if depth == 0 { - break; - } - } - ',' if depth == 1 => element = true, - c if c.is_whitespace() => {} - _ => { - let renamed_self = - ident_at(i, "self") && ident_at(skip_space(i + "self".len()), "as"); - if element && depth == 1 && (ident_at(i, "arch") || c == '*' || renamed_self) { - lines.push(line_of(i)); - } - element = false; - } - } - i += 1; - } - } } - lines +} + +/// The 0-based lines of `text` on which a path names `core::arch` or +/// `std::arch`, or makes a name that can: `core::arch::asm!`, +/// `use core::arch as isa;`, an `arch` that begins an element of a `core::{…}` +/// group, over any number of lines — and any glob of `core`/`std` itself +/// (`use core::*;`, `core::{*}`) or rename of it (`use core as k;`, +/// `extern crate core as k;`, `core::{self as k}`), refused whole: code outside +/// an arch module has no other use for either. +#[cfg(test)] +fn arch_module_lines(text: &str) -> Vec { + spelled_paths(text, &["core", "std"], None) + .into_iter() + .filter(|p| matches!(p.segments[0].as_str(), "arch" | "*") || (p.segments[0] == "self" && p.alias.is_some())) + .map(|p| p.line) + .collect() } /// The spelling of the first needle of `rule` that line `n` of a file holds, @@ -1603,9 +1669,12 @@ const KERNEL_ROOT: &str = "kernel/src/main.rs"; /// `issues/kernel/the-arch-layer-names-the-kernel-above-it.md`, which is done /// when this list is empty. /// -/// **It only shrinks.** A name a file adds reds +/// **It only shrinks, by item.** A name a file adds reds /// `the_arch_layer_names_nothing_above_it`, and so does a name a row lists that -/// its file no longer spells, so no row outlives what it excused. +/// its file no longer spells, so no row outlives what it excused. It does not +/// count reach: a file whose row holds `sched` may name more of `crate::sched` +/// unseen, and a row that grows is green, so that it shrinks is held by review +/// and not by this gate. #[cfg(test)] const ARCH_REACHES_UP: &[(&str, &[&str])] = &[ ("kernel/src/arch/aarch64/boot.rs", &["PHYS_OFFSET", "actuator", "drivers", "kernel_main", "log", "mm"]), @@ -1643,7 +1712,7 @@ const ARCH_REACHES_UP: &[(&str, &[&str])] = &[ ("kernel/src/arch/x86_64/ioapic.rs", &["drivers", "iommu", "log", "mm", "sync"]), ("kernel/src/arch/x86_64/mod.rs", &["actuator", "log"]), ("kernel/src/arch/x86_64/nmi_gate.rs", &["actuator", "clock", "log", "mm", "sched", "symbols"]), - ("kernel/src/arch/x86_64/paging.rs", &["MemoryMapEntry", "hasher", "log", "mm", "sched", "sync", "vma"]), + ("kernel/src/arch/x86_64/paging.rs", &["MemoryMapEntry", "hasher", "invalidation", "log", "mm", "sched", "sync", "vma"]), ("kernel/src/arch/x86_64/percpu.rs", &["actuator", "drivers", "irq_census", "log", "mm", "process", "sync"]), ("kernel/src/arch/x86_64/rtc.rs", &["actuator", "clock", "time"]), ("kernel/src/arch/x86_64/smp.rs", &["DirectMap", "actuator", "clock", "drivers", "hardlockup", "log", "mm", "process", "scheduler", "smp_roster", "time"]), @@ -1659,110 +1728,65 @@ const ARCH_REACHES_UP: &[(&str, &[&str])] = &[ ("kernel/src/arch/x86_64/watchdog.rs", &["actuator", "drivers", "log", "params", "time"]), ]; -/// Every `(0-based line, item)` for a crate-root item `text` names by path, in -/// a file whose module sits `depth` levels below the crate root: the segment -/// after `crate::` or `$crate::`, the first segment of each element of a -/// `crate::{…}` group over any number of lines, and the same after a `super::` -/// chain long enough to reach the root. A glob of the root is the item `*` and -/// a rename of it (`crate as k`) the item `crate as`, because what either -/// brings in is decided by resolution, which a line scan does not have. -/// Comments and string literals are not code ([`code_only`]). A macro reached -/// by textual scope (`#[macro_use]`) is not a path and is not read. +/// Every `(0-based line, item)` for a crate-root item that a file under +/// [`ARCH_TREE`], `depth` modules below the root, names by a path +/// ([`spelled_paths`] from `crate` or `super`): the path's first segment, or, +/// through the architecture, a root alias of it or a `super` chain inside it, +/// the origin of each of `reexports` the path passes through. A glob of the +/// root is the item `*` and a rename of it `self as`. A path relative to the +/// module (`self::x`, a child's name, a name a `use` bound) is not read. #[cfg(test)] -fn crate_items_named(text: &str, depth: usize) -> Vec<(usize, String)> { - let code: Vec = text.lines().map(code_only).collect::>().join("\n").chars().collect(); - let word = |c: char| c.is_ascii_alphanumeric() || c == '_'; - let line_of = |at: usize| code[..at].iter().filter(|c| **c == '\n').count(); - let skip_space = |mut at: usize| { - while code.get(at).is_some_and(|c| c.is_whitespace()) { - at += 1; - } - at - }; - // The identifier that begins at `at`, and where it ends. - let ident_at = |at: usize| -> Option<(String, usize)> { - if at.checked_sub(1).and_then(|j| code.get(j)).is_some_and(|c| word(*c)) { - return None; - } - let end = (at..).find(|&j| !code.get(j).is_some_and(|c| word(*c))).unwrap_or(at); - (end > at).then(|| (code[at..end].iter().collect(), end)) - }; - let colons = |at: usize| code.get(at..at + 2) == Some(&[':', ':'][..]); - // What follows the root, `at` being just past its `::`. - let after_root = |at: usize, found: &mut Vec<(usize, String)>| { - let at = skip_space(at); - match code.get(at) { - Some('*') => found.push((line_of(at), "*".to_string())), - Some('{') => { - let (mut depth, mut i, mut element) = (0usize, at, true); - while let Some(&c) = code.get(i) { - match c { - '{' => { - depth += 1; - element = depth == 1; - } - '}' => { - depth -= 1; - if depth == 0 { - break; - } - } - ',' if depth == 1 => element = true, - c if c.is_whitespace() => {} - _ => { - if element && depth == 1 { - match ident_at(i) { - Some((name, _)) if name != "self" => found.push((line_of(i), name)), - Some(_) => {} - None if c == '*' => found.push((line_of(i), "*".to_string())), - None => {} - } - } - element = false; - } - } - i += 1; - } - } - _ => { - if let Some((name, _)) = ident_at(at) { - found.push((line_of(at), name)); +fn arch_reach( + text: &str, + depth: usize, + aliases: &[String], + reexports: &std::collections::BTreeSet<(String, String)>, +) -> Vec<(usize, String)> { + let mut found = Vec::new(); + for path in spelled_paths(text, &["crate"], Some(depth)) { + let first = path.segments[0].as_str(); + if first == "self" { + found.extend(path.alias.is_some().then(|| (path.line, "self as".to_string()))); + } else if first == "arch" || first == "super" || aliases.iter().any(|a| a == first) { + for (name, origin) in reexports { + if path.segments[1..].contains(name) { + found.push((path.line, origin.clone())); } } + } else { + found.push((path.line, first.to_string())); } - }; - let mut found = Vec::new(); - let mut at = 0; - while at < code.len() { - let Some((name, end)) = ident_at(at) else { - at += 1; - continue; - }; - let after = skip_space(end); - if name == "crate" { - if colons(after) { - after_root(after + 2, &mut found); - } else if ident_at(after).is_some_and(|(next, _)| next == "as") { - found.push((line_of(at), "crate as".to_string())); - } - } else if name == "super" && !(at >= 2 && colons(at - 2)) { - let (mut supers, mut next) = (1, after); - while colons(next) { - match ident_at(skip_space(next + 2)) { - Some((s, e)) if s == "super" => { - supers += 1; - next = skip_space(e); + } + found.sort(); + found.dedup(); + found +} + +/// Every `(name, origin)` a file under [`ARCH_TREE`] re-exports from above the +/// layer: `pub use crate::invalidation::Origin;` is `("Origin", +/// "invalidation")`. A name a glob re-exports is not known, so not resolved. +#[cfg(test)] +fn arch_reexports(files: &[(String, String)], aliases: &[String]) -> std::collections::BTreeSet<(String, String)> { + let mut out = std::collections::BTreeSet::new(); + for (at, text) in files.iter().filter(|(at, _)| at.starts_with(ARCH_TREE)) { + let code = text.lines().map(code_only).collect::>().join("\n"); + let mut offset = 0; + for line in code.split_inclusive('\n') { + let item = after_visibility(line); + if item != line.trim_start() && item.starts_with("use ") { + let statement = code[offset..].split(';').next().unwrap_or_default(); + for path in spelled_paths(statement, &["crate"], Some(module_depth(at))) { + let first = path.segments[0].as_str(); + if !["self", "arch", "super"].contains(&first) && !aliases.iter().any(|a| a == first) { + let name = path.alias.clone().or_else(|| path.segments.last().cloned()); + out.extend(name.map(|name| (name, first.to_string()))); } - _ => break, } } - if supers == depth && colons(next) { - after_root(next + 2, &mut found); - } + offset += line.len(); } - at = end; } - found + out } /// How many modules deep the file at repository-relative `path` sits below @@ -1808,17 +1832,15 @@ fn arch_reach_complaints( if rows[..i].iter().any(|(earlier, _)| earlier == file) { complaints.push(format!("{file} has two rows in the arch reach list; one row per file")); } - if !files.iter().any(|(at, _)| at == file) { + if !file.starts_with(ARCH_TREE) || !files.iter().any(|(at, _)| at == file) { complaints.push(format!( "the arch reach list names {file}, and this tree holds no such file under {ARCH_TREE}" )); } } + let reexports = arch_reexports(files, aliases); for (at, text) in files.iter().filter(|(at, _)| at.starts_with(ARCH_TREE)) { - let named: Vec<(usize, String)> = crate_items_named(text, module_depth(at)) - .into_iter() - .filter(|(_, item)| item != "arch" && !aliases.contains(item)) - .collect(); + let named = arch_reach(text, module_depth(at), aliases, &reexports); let permitted: &[&str] = rows.iter().find(|(file, _)| file == at).map_or(&[], |(_, items)| items); for (line, item) in &named { if !permitted.contains(&item.as_str()) { @@ -1910,31 +1932,49 @@ mod tests { /// what is not a path to one is not. #[test] fn every_spelling_of_a_crate_root_item_is_read() { + let reexports = [("Prot".to_string(), "mm".to_string())].into_iter().collect(); + let hw = ["hw".to_string()]; let names = |text: &str, depth: usize| -> Vec { - crate_items_named(text, depth).into_iter().map(|(_, item)| item).collect() + arch_reach(text, depth, &hw, &reexports).into_iter().map(|(_, item)| item).collect() }; assert_eq!(names("use crate::sched::driver;\n", 3), ["sched"]); assert_eq!(names(" crate::log!(\"x\");\n", 3), ["log"]); assert_eq!(names("const { $crate::irq_census::TOTAL }\n", 3), ["irq_census"]); - assert_eq!(names("use crate::{\n arch::x,\n mm::{a, b},\n self,\n process,\n};\n", 3), [ - "arch", "mm", "process" - ]); + assert_eq!(names("use crate::{\n arch::x,\n mm::{a, b},\n self,\n process,\n};\n", 3), ["mm", "process"]); assert_eq!(names("use crate :: { * };\nuse crate::*;\n", 3), ["*", "*"]); - assert_eq!(names("use crate as k;\n", 3), ["crate as"]); - // A `super::` chain that leaves the file's module names the root's item. + assert_eq!(names("use crate as k;\nuse crate::{self as j};\n", 3), ["self as", "self as"]); + // A `super::` chain that leaves the file's module names the root's item, + // counted from `self::` and through every inline module around it. assert_eq!(names("use super::super::super::drivers::xhci;\n", 3), ["drivers"]); - assert_eq!(names("use super::super::{sched, mm};\n", 2), ["sched", "mm"]); - assert_eq!(names("use super::{apic, smp};\nuse super::super::percpu;\n", 3), Vec::::new()); + assert_eq!(names("use super::super::{sched, mm};\n", 2), ["mm", "sched"]); + assert_eq!(names("use self::super::super::super::drivers;\n", 3), ["drivers"]); + assert_eq!(names("mod t {\n use super::super::super::super::sched;\n}\nmod u { use super::super::super::x; }\n", 3), ["sched"]); + // A name the layer re-exports from above is its origin, however it is reached. + assert_eq!(names("use crate::arch::paging::Prot;\n", 3), ["mm"]); + assert_eq!(names("use super::paging::{Other, Prot};\nlet p = crate::hw::paging::Prot::R;\n", 3), ["mm", "mm"]); + // A char literal hides nothing after it. + assert_eq!(names("let q = '\"'; let r = '\\''; fn f<'a>(x: &'a u8) { crate::log!(); }\n", 3), ["log"]); // Not code, not the root, not a path. for text in [ "// crate::sched is the scheduler\n", "let s = \"crate::sched\";\n", "pub(crate) fn f() {}\npub(super) fn g() {}\n", "use mycrate::sched;\nuse other_crate::x;\n", - "use self::super::x;\n", + "use self::super::x;\nuse super::{apic, smp};\nuse super::super::percpu;\nuse crate::arch::paging::Other;\n", ] { assert_eq!(names(text, 3), Vec::::new(), "{text:?}"); } + let file = |at: &str, text: &str| (at.to_string(), text.to_string()); + let reexported = arch_reexports( + &[ + file("kernel/src/arch/x86_64/tlb.rs", "pub use crate::invalidation::Origin;\nuse crate::sched::X;\n"), + file("kernel/src/arch/x86_64/paging.rs", "pub(crate) use crate::mm::{\n policy::Prot,\n Mm as M,\n};\npub use super::cpu::outb;\n"), + file("kernel/src/sched/mod.rs", "pub use crate::log::Up;\n"), + ], + &hw, + ); + let pair = |n: &str, o: &str| (n.to_string(), o.to_string()); + assert_eq!(reexported, [pair("M", "mm"), pair("Origin", "invalidation"), pair("Prot", "mm")].into_iter().collect()); assert_eq!(module_depth("kernel/src/arch/mod.rs"), 1); assert_eq!(module_depth("kernel/src/arch/x86_64/mod.rs"), 2); assert_eq!(module_depth("kernel/src/arch/x86_64/tlb.rs"), 3); @@ -1946,31 +1986,44 @@ mod tests { } /// Teeth for the list: a new reach reds naming the file, the line and the - /// item; a listed one does not; a listed one the file stopped spelling reds; - /// and a root alias of the architecture is the architecture. + /// item, and so does one through a re-export; a listed one does not; a + /// listed one the file stopped spelling reds, and so does a row for a file + /// outside the layer; and a root alias of the architecture is the architecture. #[test] fn a_new_reach_out_of_arch_is_red_and_the_list_cannot_rot() { let file = |at: &str, text: &str| (at.to_string(), text.to_string()); let files = [ file("kernel/src/arch/x86_64/tlb.rs", "use crate::time::Duration;\nuse crate::drivers::xhci;\n"), file("kernel/src/arch/x86_64/idt/timer.rs", "use crate::hw::HW;\nuse crate::arch::apic;\n"), + file("kernel/src/arch/x86_64/paging.rs", "pub use crate::mm::policy::Prot;\n"), + file("kernel/src/arch/x86_64/rtc.rs", "use crate::arch::paging::Prot;\n"), file("kernel/src/sched/driver.rs", "use crate::drivers::xhci;\n"), ]; let aliases = vec!["hw".to_string()]; - let said = arch_reach_complaints(&files, &[("kernel/src/arch/x86_64/tlb.rs", &["time"])], &aliases); - assert_eq!(said.len(), 1, "{said:?}"); + let paging = ("kernel/src/arch/x86_64/paging.rs", &["mm"][..]); + let said = arch_reach_complaints(&files, &[("kernel/src/arch/x86_64/tlb.rs", &["time"]), paging], &aliases); + assert_eq!(said.len(), 2, "{said:?}"); assert!(said[0].starts_with("kernel/src/arch/x86_64/tlb.rs:2: names `crate::drivers`"), "{said:?}"); + assert!(said[1].starts_with("kernel/src/arch/x86_64/rtc.rs:1: names `crate::mm`"), "{said:?}"); - let listed = arch_reach_complaints(&files, &[("kernel/src/arch/x86_64/tlb.rs", &["drivers", "time"])], &aliases); + let rtc = ("kernel/src/arch/x86_64/rtc.rs", &["mm"][..]); + let listed = arch_reach_complaints(&files, &[("kernel/src/arch/x86_64/tlb.rs", &["drivers", "time"]), paging, rtc], &aliases); assert!(listed.is_empty(), "{listed:?}"); let stale = arch_reach_complaints( &files, - &[("kernel/src/arch/x86_64/tlb.rs", &["drivers", "time", "sched"]), ("kernel/src/arch/gone.rs", &["log"])], + &[ + ("kernel/src/arch/x86_64/tlb.rs", &["drivers", "time", "sched"]), + ("kernel/src/arch/gone.rs", &["log"]), + ("kernel/src/sched/driver.rs", &["drivers"]), + paging, + rtc, + ], &aliases, ); - assert_eq!(stale.len(), 2, "{stale:?}"); + assert_eq!(stale.len(), 3, "{stale:?}"); assert!(stale.iter().any(|s| s.contains("kernel/src/arch/gone.rs")), "{stale:?}"); + assert!(stale.iter().any(|s| s.contains("names kernel/src/sched/driver.rs")), "{stale:?}"); assert!(stale.iter().any(|s| s.contains("`crate::sched`") && s.contains("no longer")), "{stale:?}"); } @@ -2407,7 +2460,8 @@ mod tests { rust_files(&root.join(tree), &mut files); let walked: std::collections::BTreeSet = files.iter().map(|p| rel(&root, p)).collect(); - let tracked = tracked_rust_files(&root, tree); + let tracked: std::collections::BTreeSet = + crate::sysroot::tracked_files(&root, &[tree]).into_iter().filter(|p| p.ends_with(".rs")).collect(); assert!( tracked.len() > 1, "git tracks {} .rs file(s) under {tree}, so this floor is not one", @@ -2829,16 +2883,10 @@ mod tests { #[test] fn every_committed_binary_file_is_declared() { let root = repo_root(); - let out = std::process::Command::new("git") - .args(["ls-files", "-z"]) - .current_dir(&root) - .output() - .unwrap_or_else(|e| panic!("git ls-files: {e}")); - assert!(out.status.success(), "git ls-files failed"); - let listing = String::from_utf8(out.stdout).expect("git ls-files is not UTF-8"); + let listing = crate::sysroot::tracked_files(&root, &[]); let mut found: Vec<(String, String)> = Vec::new(); - for name in listing.split('\0').filter(|s| !s.is_empty()) { + for name in &listing { let Ok(bytes) = std::fs::read(root.join(name)) else { continue }; if !is_binary(&bytes) && !name.starts_with("assets/") { continue; @@ -2896,15 +2944,8 @@ mod tests { let licence_path = format!("{CORPUS}/LICENSE"); let licence = std::fs::read_to_string(root.join(&licence_path)) .unwrap_or_else(|e| panic!("{licence_path}: {e}")); - let out = std::process::Command::new("git") - .args(["ls-files", "-z"]) - .current_dir(&root) - .output() - .unwrap_or_else(|e| panic!("git ls-files: {e}")); - assert!(out.status.success(), "git ls-files failed"); - let listing = String::from_utf8(out.stdout).expect("git ls-files is not UTF-8"); - let tracked: Vec<&str> = listing.split('\0').filter(|s| !s.is_empty()).collect(); - let under_corpus: Vec<&&str> = + let tracked = crate::sysroot::tracked_files(&root, &[]); + let under_corpus: Vec<&String> = tracked.iter().filter(|f| f.starts_with(&format!("{CORPUS}/"))).collect(); assert!( under_corpus.len() > CORPUS_OURS.len(), @@ -2927,7 +2968,7 @@ mod tests { } } for file in &under_corpus { - let attributed = CORPUS_OURS.contains(file) + let attributed = CORPUS_OURS.contains(&file.as_str()) || CORPUS_POPULATIONS .iter() .any(|p| file.starts_with(&format!("{CORPUS}/{p}/"))); diff --git a/src/sysroot.rs b/src/sysroot.rs index 5fbc008d8a3..2eddf9da9a5 100644 --- a/src/sysroot.rs +++ b/src/sysroot.rs @@ -603,6 +603,13 @@ pub(crate) fn git_out(dir: &Path, args: &[&str]) -> String { String::from_utf8_lossy(&git_bytes(dir, args)).into_owned() } +/// The files `git` tracks under `dir` that `pathspecs` name, every one when +/// there are none, relative to `dir`. +pub(crate) fn tracked_files(dir: &Path, pathspecs: &[&str]) -> Vec { + let args = [&["ls-files", "-z", "--"][..], pathspecs].concat(); + git_out(dir, &args).split('\0').filter(|f| !f.is_empty()).map(String::from).collect() +} + fn git_run(dir: &Path, args: &[&str]) { let ok = Command::new("git") .args(args) From c7497866369c1332f4d85b1a2ec79927e3a7ea75 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 14:30:24 +0200 Subject: [PATCH 09/12] Answer round 2 of the review of #537: nothing handed up, every gone root read The arch layer re-exported five names from above it (`invalidation::Origin` in both tlb.rs files, `mm::policy`'s four in both paging.rs files), and the gate resolved paths through them by name, which a glob, a relative second hop and a type alias each walked past. They are deleted instead: every caller names `crate::invalidation::Origin` and `crate::mm::policy::*` at home, and arch imports them privately. With nothing handed up there is nothing to resolve, so `arch_reexports` and the resolution in `arch_reach` go. What the gate refuses outright in its place: a `pub use` under arch/ that reaches above the layer, any `pub type` there (an alias is the one re-export rustc lets carry a privately imported name out, so x86_64's `Root` becomes `pub use Cr3 as Root`), and a glob of `crate::arch`. The citation gate reads a gone root in every shape: a bare `name/` and a first component that starts with `.`. The prose that wrote directories that exist nowhere as paths (`fs/`, `completion/`, `gates/`, `lib/`, `stamps/`, `forks/`, `compiler/`) is rewritten. `FOREIGN` reds on an entry no citation needs; `smoltcp-0.12.0` was one and is gone, and `.git`, `.build-locks` and `compiler`, which src/CLAUDE.md writes, join it. `sysroot::tracked_files` returns `Result` and refuses a name that is not UTF-8, so `licence::judge` fails only by `Err`; `sdkversion`'s lockfile listing uses it. tlb.rs names `time::DEAF_CPU` itself, its alias and assertion deleted, and the two false sentences about masked spins are deleted. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK --- ...landed-abi-is-rebuilt-against-mains-abi.md | 2 +- ...clippy-stage-two-is-lints-one-at-a-time.md | 2 +- ...-builds-its-own-copy-of-the-same-crates.md | 2 +- .../build/fork-branches-have-no-upstream.md | 2 +- ...rebuilds-its-compiler-on-compiler-alone.md | 4 +- .../design-debt/redesign-the-log-subsystem.md | 10 +- ...t-the-tlb-ack-tripwire-before-its-break.md | 4 +- ...disk-left-a-cpu-deaf-to-a-tlb-shootdown.md | 2 +- kernel/src/arch/aarch64/paging.rs | 2 +- kernel/src/arch/aarch64/tlb.rs | 2 +- kernel/src/arch/x86_64/control_regs.rs | 2 +- kernel/src/arch/x86_64/hpet.rs | 2 +- kernel/src/arch/x86_64/ioapic.rs | 2 +- kernel/src/arch/x86_64/mtrr.rs | 2 +- kernel/src/arch/x86_64/paging.rs | 8 +- kernel/src/arch/x86_64/tlb.rs | 14 +- kernel/src/arch/x86_64/vtd/mod.rs | 2 +- kernel/src/clock.rs | 2 +- kernel/src/drivers/gop.rs | 2 +- kernel/src/drivers/hda.rs | 2 +- kernel/src/drivers/nvme.rs | 2 +- kernel/src/drivers/panic_console/mod.rs | 2 +- kernel/src/drivers/pci.rs | 2 +- kernel/src/drivers/virtio.rs | 2 +- kernel/src/drivers/virtio_gpu.rs | 2 +- kernel/src/drivers/virtio_sound.rs | 2 +- kernel/src/drivers/xhci/wait/boot.rs | 2 +- kernel/src/elf/mod.rs | 8 +- kernel/src/loader/mod.rs | 6 +- kernel/src/loader/tls.rs | 2 +- kernel/src/main.rs | 2 +- kernel/src/mm/unmapped.rs | 2 +- kernel/src/object/shm.rs | 2 +- kernel/src/pcidev/mod.rs | 2 +- kernel/src/process.rs | 4 +- kernel/src/syscall/ipc.rs | 2 +- kernel/src/syscall/vm.rs | 6 +- kernel/src/time.rs | 4 +- kernel/src/vma.rs | 2 +- src/assets.rs | 2 +- src/citations.rs | 78 +++++--- src/licence.rs | 4 +- src/sdkversion.rs | 4 +- src/sourcegate.rs | 171 ++++++++++-------- src/sysroot.rs | 36 ++-- 45 files changed, 229 insertions(+), 192 deletions(-) diff --git a/issues/build/a-std-fork-moved-for-an-unlanded-abi-is-rebuilt-against-mains-abi.md b/issues/build/a-std-fork-moved-for-an-unlanded-abi-is-rebuilt-against-mains-abi.md index a1ca23c6b53..f65f8a3f480 100644 --- a/issues/build/a-std-fork-moved-for-an-unlanded-abi-is-rebuilt-against-mains-abi.md +++ b/issues/build/a-std-fork-moved-for-an-unlanded-abi-is-rebuilt-against-mains-abi.md @@ -22,7 +22,7 @@ error: could not compile `std` (lib) due to 2 previous errors thread 'main' panicked at src/toolchain.rs:1583:5: ``` -Afterwards `rust/build/aarch64-apple-darwin/stage2/` held only `lib/`, and +Afterwards `rust/build/aarch64-apple-darwin/stage2/` held only a `lib` directory, and `rustc -vV` in `userland/` answered "'rustc' is not installed for the custom toolchain 'toyos'". The lock log shows a second process (pid 31197) holding the same step just before, so the step had already been attempted once. diff --git a/issues/build/clippy-stage-two-is-lints-one-at-a-time.md b/issues/build/clippy-stage-two-is-lints-one-at-a-time.md index ae352919701..cc889c0c357 100644 --- a/issues/build/clippy-stage-two-is-lints-one-at-a-time.md +++ b/issues/build/clippy-stage-two-is-lints-one-at-a-time.md @@ -147,7 +147,7 @@ before adopting: | `iommu/` | 8 | **adopted 2026-08-22**, under the reduction ruling. 8 findings (8 `unsafe` blocks in all); **5 removed, 3 documented, 0 filed** — the area's whole remainder is two window constructions and a `clflush`. | | `log/` | 2 | **adopted 2026-08-22**, under the reduction ruling. 2 findings (9 `unsafe` blocks in all, seven of them documented before this pass); **0 removed, 2 documented, 1 filed** — and the filed one was built the same day: `LogRecord` has the safe `as_bytes` its six siblings carry, so `log::user`'s hand-rolled slice is gone and the area's remainder is one block (`shard::initialize_zeroed`, irreducible). | | `object/` | 1 | **adopted** — the one site is `object::shm::Pages`'s `Send`/`Sync` pair, part of the finding filed above | -| `completion/` | 0 undocumented (3 unsafe sites, all already carrying a `SAFETY:`/`Safety:` comment predating this pass) | already documented | +| `completion` (since deleted) | 0 undocumented (3 unsafe sites, all already carrying a `SAFETY:`/`Safety:` comment predating this pass) | already documented | | **adopted** | **389** — the whole kernel (`mm` 35 + `elf` 23 + `loader` 8 + `object` 1 + root files 76 + `drivers` 121 + `arch` 107 + `sched` 8 + `iommu` 8 + `log` 2) | gated at the source, because the kernel is one crate with no `-p` scoping to hang a lint on. Every area sweep opened its entry module (`mm/mod.rs`, `object/mod.rs`, `elf/mod.rs`, `loader/mod.rs`, `drivers/mod.rs`, `arch/mod.rs`, `sched/mod.rs`, `iommu/mod.rs`, `log/mod.rs`) with `#![warn(clippy::undocumented_unsafe_blocks)]`; the root-file sweep could not — there is no entry module above them but the crate root — so it **inverted the form**: `main.rs` carries one crate-level `#![warn(...)]`, and an `#[allow(...)]` on a `mod` line is the form a tree not yet swept would take. Both compose with `host-tests.yml`'s existing `-D warnings` on the two kernel invocations, so no command line changed. The module attributes are redundant under the crate one and are left where they are — each still records its own area's status. | | **remaining** | **0** | measured 2026-08-22, after the last two sweeps (`arch`, and `sched`+`iommu`+`log`) merged, with `--force-warn clippy::undocumented_unsafe_blocks` over both kernel invocations: no finding anywhere in `kernel/src`. The `allow` list in `main.rs` is empty, which is the state this row was opened to reach; a new tree that cannot be gated the day it appears goes on that list and comes off it by the pull request that sweeps it. | diff --git a/issues/build/every-worktree-builds-its-own-copy-of-the-same-crates.md b/issues/build/every-worktree-builds-its-own-copy-of-the-same-crates.md index 14ff24f16ef..13fb2bca52b 100644 --- a/issues/build/every-worktree-builds-its-own-copy-of-the-same-crates.md +++ b/issues/build/every-worktree-builds-its-own-copy-of-the-same-crates.md @@ -10,7 +10,7 @@ Twenty-four linked worktrees hold twenty-four copies of one compilation. The primary checkout's `target/` is 16 GB, and cargo's share of it is 12.5 GB (`du`, 2026-08-19): `debug` 11 GB, `x86_64-unknown-toyos` 895 MB, `release` 286 MB, `aarch64-apple-darwin` 285 MB. The remaining 3.5 GB — `bootable*.img`, -`nvme.img` 1.0 GB, the staged `kernel-*`/`bootloader.efi-*` copies, `stamps/` — +`nvme.img` 1.0 GB, the staged `kernel-*`/`bootloader.efi-*` copies, `target/stamps/` — is the build system's own output and is **per-worktree by design**: `kernel_key` hashes profile and features and not content, and `buildlock::artifact` is a lock under `/.build-locks`. Only cargo's 12.5 GB is a candidate for diff --git a/issues/build/fork-branches-have-no-upstream.md b/issues/build/fork-branches-have-no-upstream.md index 6dab82f2c98..3ca0e158eeb 100644 --- a/issues/build/fork-branches-have-no-upstream.md +++ b/issues/build/fork-branches-have-no-upstream.md @@ -6,7 +6,7 @@ opened: 2026-08-07 # A `toyos` branch mostly has no upstream, so `git status` cannot say if it is pushed -13 of the 16 consumed and PR branches across `forks/` have no tracking ref: +13 of the 16 consumed and PR branches across the fork checkouts in `../forks` have no tracking ref: `git for-each-ref --format='%(refname:short)|%(upstream:short)' refs/heads` gives `NO UPSTREAM` for cpal, ctrlc, getrandom (all three), mio, raw-window-handle, socket2, softbuffer, stacker, target-lexicon, tokio and winit. Only libloading, diff --git a/issues/build/the-primary-rebuilds-its-compiler-on-compiler-alone.md b/issues/build/the-primary-rebuilds-its-compiler-on-compiler-alone.md index 8b838eba3e8..fa20c6f6990 100644 --- a/issues/build/the-primary-rebuilds-its-compiler-on-compiler-alone.md +++ b/issues/build/the-primary-rebuilds-its-compiler-on-compiler-alone.md @@ -4,13 +4,13 @@ kind: tooling opened: 2026-09-26 --- -# The primary rebuilds its compiler on `compiler/` alone +# The primary rebuilds its compiler on `rust/compiler/` alone `src/toolchain.rs` rebuilds the primary's toolchain when the stamp over `rust/compiler/` changes, and `compiler::record` writes that tree as the compiler the primary's `stage2` is. A fork commit that moves only `rust/src/bootstrap`, `rust/src/tools`, `rust/src/stage0`, `rust/Cargo.lock` or the LLVM submodule -leaves the primary on the compiler it had, and a worktree whose `compiler/` +leaves the primary on the compiler it had, and a worktree whose `rust/compiler/` matches the record is handed that compiler too. A worktree's own compiler is keyed on all of them (`compiler::key`, PR #524), so the two answers to "which compiler do these sources name" differ. diff --git a/issues/design-debt/redesign-the-log-subsystem.md b/issues/design-debt/redesign-the-log-subsystem.md index 2e8034df0cc..bc4264bb0b7 100644 --- a/issues/design-debt/redesign-the-log-subsystem.md +++ b/issues/design-debt/redesign-the-log-subsystem.md @@ -79,16 +79,16 @@ carrying explicit backpressure — a slow sink drops-and-counts, never blocks, does no unbounded work in a scheduler-adjacent path, and fails alone. **The layout half, re-measured.** `kernel/src` is **50 flat `.rs` files** -(`ls kernel/src/*.rs | wc -l`) beside ten directories — `arch/`, `completion/`, -`drivers/`, `elf/`, `iommu/`, `loader/`, `log/`, `mm/`, `object/`, `sched/`. +(`ls kernel/src/*.rs | wc -l`) beside ten directories — `arch`, `completion`, +`drivers`, `elf`, `iommu`, `loader`, `log`, `mm`, `object`, `sched`. The 39-beside-seven figure this entry opened with is three directories and eleven files out of date; `log/` is one of the six the review named as the target and it exists. `elf.rs` and `loader.rs` became directories in `42b29c9`, which is the precedent. The flat set mixes a filesystem adapter, an IPC primitive, two input devices, a page cache, io_uring, the process table and two cfg-gated test actuators at one -level. The review's target was subsystem directories (fs/, ipc/, input/, -proc/, log/, time/), and the `syscall.rs` split already forces at least one. +level. The review's target was subsystem directories (fs, ipc, input, +proc, log, time), and the `syscall.rs` split already forces at least one. Cost, so the question is priced: a directory move is `git mv` plus `mod` lines, it touches no logic, and it collides with every worktree in flight — which is @@ -99,7 +99,7 @@ Two smaller layout items ride the same answer. `usb_gate.rs` (242 lines) and call (`kernel/src/main.rs:34`, `:36`, `:408`, `:537`) and are never in an ordinary build, but they sit interleaved with production sources; the review's target is one -`gates/` directory under `kernel/src/` so that what test machinery exists is auditable +`gates` directory under `kernel/src/` so that what test machinery exists is auditable in one listing. And `input_merge_test` is the tell that pure logic is trapped in the kernel: the merge state machine (one held-set, one button-merge, both bounded) is host-testable with synthetic multi-source streams, after which the diff --git a/issues/kernel/a-disk-operation-can-spin-past-the-tlb-ack-tripwire-before-its-break.md b/issues/kernel/a-disk-operation-can-spin-past-the-tlb-ack-tripwire-before-its-break.md index 0330efdf0df..ee0f09fa33f 100644 --- a/issues/kernel/a-disk-operation-can-spin-past-the-tlb-ack-tripwire-before-its-break.md +++ b/issues/kernel/a-disk-operation-can-spin-past-the-tlb-ack-tripwire-before-its-break.md @@ -6,7 +6,7 @@ opened: 2026-09-22 # A disk operation can spin past the TLB-ack tripwire before its break -`arch::tlb::ACK_TIMEOUT` (5 s) is held above xHCI's `CALL_AFTER_BREAK` +`time::DEAF_CPU` (5 s), the TLB-ack tripwire, is held above xHCI's `CALL_AFTER_BREAK` (4.75 s), "the longest a disk call spins with `IF` clear once its transport has broken". But the call's bound is measured from the wait that broke, and a USB disk operation holds the controller lock — `IF` clear — from its first command: @@ -19,7 +19,7 @@ constants; no boot has been seen to do it. ## Exit condition -The whole of one operation's `IF`-clear spin is under `ACK_TIMEOUT` by +The whole of one operation's `IF`-clear spin is under `DEAF_CPU` by construction — the call bound opens where the operation does, or a later batch starts only with a whole call's bound still inside it — and a staged boot in which the first batch spends most of the budget and the next one breaks shows diff --git a/issues/kernel/a-shutdown-on-a-held-usb-disk-left-a-cpu-deaf-to-a-tlb-shootdown.md b/issues/kernel/a-shutdown-on-a-held-usb-disk-left-a-cpu-deaf-to-a-tlb-shootdown.md index ce832b72938..ea42025699d 100644 --- a/issues/kernel/a-shutdown-on-a-held-usb-disk-left-a-cpu-deaf-to-a-tlb-shootdown.md +++ b/issues/kernel/a-shutdown-on-a-held-usb-disk-left-a-cpu-deaf-to-a-tlb-shootdown.md @@ -30,7 +30,7 @@ branch no longer starts that thread there, which would remove this boot's trigger and not the deaf CPU. `a-disk-operation-can-spin-past-the-tlb-ack-tripwire-before-its-break.md` is -the arithmetic for one disk operation outrunning `ACK_TIMEOUT`; this is a boot +the arithmetic for one disk operation outrunning `time::DEAF_CPU`; this is a boot that did outrun it, in `quiesce`, across several operations each inside its own budget. Whether `quiesce` holds `IF` clear between them is not measured. diff --git a/kernel/src/arch/aarch64/paging.rs b/kernel/src/arch/aarch64/paging.rs index b287a4449c9..29fa4e7bf8d 100644 --- a/kernel/src/arch/aarch64/paging.rs +++ b/kernel/src/arch/aarch64/paging.rs @@ -9,7 +9,7 @@ use core::convert::Infallible; use crate::mm::UserAddr; -pub use crate::mm::policy::{CachePolicy, MmioPolicy, Prot, WindowProt}; +use crate::mm::policy::{CachePolicy, MmioPolicy, Prot, WindowProt}; use crate::sync::Lock; use crate::vma::{Occupancy, Region, RegionKind}; use crate::MemoryMapEntry; diff --git a/kernel/src/arch/aarch64/tlb.rs b/kernel/src/arch/aarch64/tlb.rs index dae8edfbe64..df6dec4b13b 100644 --- a/kernel/src/arch/aarch64/tlb.rs +++ b/kernel/src/arch/aarch64/tlb.rs @@ -3,7 +3,7 @@ //! plus `DSB ISH` once the kernel owns its page tables, the port's stage 4. -pub use crate::invalidation::Origin; +use crate::invalidation::Origin; pub fn log_census() { owed!("TLB invalidation", "stage 4") diff --git a/kernel/src/arch/x86_64/control_regs.rs b/kernel/src/arch/x86_64/control_regs.rs index 2688da246b3..3ad0eb2ff98 100644 --- a/kernel/src/arch/x86_64/control_regs.rs +++ b/kernel/src/arch/x86_64/control_regs.rs @@ -3,7 +3,7 @@ //! else may write any of the three. Each register is written whole: `CR0` //! and `EFER` are constants, `CR4` is required bits plus whatever optional //! bits this CPU offers. `EFER.NXE` lets bit 63 of a paging entry mean *not -//! executable* ([`Prot`](crate::mm::paging::Prot)). +//! executable* ([`Prot`](crate::mm::policy::Prot)). use core::sync::atomic::{AtomicU64, Ordering}; diff --git a/kernel/src/arch/x86_64/hpet.rs b/kernel/src/arch/x86_64/hpet.rs index 5a49311540a..76e8a704fef 100644 --- a/kernel/src/arch/x86_64/hpet.rs +++ b/kernel/src/arch/x86_64/hpet.rs @@ -2,7 +2,7 @@ //! stated rate every part can be trusted for, so the boot measures it. use crate::log; -use crate::mm::paging::MmioPolicy; +use crate::mm::policy::MmioPolicy; use crate::time::{Delay, Duration}; use super::cpu; diff --git a/kernel/src/arch/x86_64/ioapic.rs b/kernel/src/arch/x86_64/ioapic.rs index fdfd4aa9de1..b765e84cc8f 100644 --- a/kernel/src/arch/x86_64/ioapic.rs +++ b/kernel/src/arch/x86_64/ioapic.rs @@ -12,7 +12,7 @@ use alloc::vec::Vec; use core::fmt::Write; use crate::iommu::Delivery; -use crate::mm::paging::MmioPolicy; +use crate::mm::policy::MmioPolicy; use crate::log; use crate::mm::Mmio; use crate::sync::Lock; diff --git a/kernel/src/arch/x86_64/mtrr.rs b/kernel/src/arch/x86_64/mtrr.rs index 7d25220f4ce..a7abe1011ea 100644 --- a/kernel/src/arch/x86_64/mtrr.rs +++ b/kernel/src/arch/x86_64/mtrr.rs @@ -1,7 +1,7 @@ //! What memory type firmware gave a physical range. //! //! Read-only: firmware owns these registers, the kernel programs none. A -//! mapping with [`CachePolicy::Normal`](crate::mm::paging::CachePolicy) +//! mapping with [`CachePolicy::Normal`](crate::mm::policy::CachePolicy) //! selects PAT entry 0 (WB), so what this module reports is the effective //! type; the exception is [`effective_under_wc`], where WC outvotes the MTRR //! instead of deferring to it. diff --git a/kernel/src/arch/x86_64/paging.rs b/kernel/src/arch/x86_64/paging.rs index 0dc36450a9e..f5e260e7eee 100644 --- a/kernel/src/arch/x86_64/paging.rs +++ b/kernel/src/arch/x86_64/paging.rs @@ -15,7 +15,7 @@ use crate::hasher::HashMap; use toyos_pcid::{Alloc, Pcid, PcidPool}; use crate::mm::{UserAddr, PAGE_2M}; -pub use crate::mm::policy::{CachePolicy, MmioPolicy, Prot, WindowProt}; +use crate::mm::policy::{CachePolicy, MmioPolicy, Prot, WindowProt}; use crate::arch::control_regs::PcidActive; use crate::arch::cpu::Invpcid; use crate::sync::Lock; @@ -276,7 +276,7 @@ pub fn flush_tlb_all() { pub struct Cr3(u64); /// The address space a CPU runs in, as the architecture names its root: CR3. -pub type Root = Cr3; +pub use Cr3 as Root; impl Cr3 { pub fn current() -> Self { @@ -351,7 +351,7 @@ fn alloc_pcid() -> Option { match pool.alloc() { Alloc::Ready(p) => return Some(PcidGuard(p)), Alloc::NeedsFlush => { - crate::arch::tlb::shootdown(crate::arch::tlb::Origin::Pcid); + crate::arch::tlb::shootdown(crate::invalidation::Origin::Pcid); pool.reclaim(); } Alloc::Exhausted => return None, @@ -917,7 +917,7 @@ pub fn load_kernel_flush() { /// sibling's stale entry, which is SDM Vol. 3A §11.12.4 undefined behaviour. pub fn map_mmio(phys: u64, size: u64, policy: MmioPolicy) -> crate::mm::Mmio { let mmio = kernel().lock().map_mmio(phys, size, policy.cache()); - crate::arch::tlb::shootdown(crate::arch::tlb::Origin::Mmio); + crate::arch::tlb::shootdown(crate::invalidation::Origin::Mmio); // Read back off the table and logged beside firmware's MTRR verdict: the // boot's own evidence that no register window trusts firmware. let installed = diff --git a/kernel/src/arch/x86_64/tlb.rs b/kernel/src/arch/x86_64/tlb.rs index 6dc5cce7148..df1ecc0ddcd 100644 --- a/kernel/src/arch/x86_64/tlb.rs +++ b/kernel/src/arch/x86_64/tlb.rs @@ -13,13 +13,12 @@ use core::sync::atomic::{AtomicU64, Ordering}; use crate::shootdown::{Generation, Shootdown}; -use crate::time::Tripwire; use super::{apic, percpu, smp}; static SHOOTDOWN: Shootdown = Shootdown::new(); -pub use crate::invalidation::Origin; +use crate::invalidation::Origin; /// Issuer-side census; `irq_census`'s `tlb` column is the receiver side, and a /// delivery the two disagree on is an uncounted issuing path. @@ -58,13 +57,6 @@ pub fn log_census() { ); } -/// How long the initiator waits for one CPU's flush: a target that has not -/// answered by then is not taking interrupts. -const ACK_TIMEOUT: Tripwire = crate::time::DEAF_CPU; - -// A spin with interrupts masked is held under `DEAF_CPU` at its own site, so this wait is no shorter. -const _: () = assert!(ACK_TIMEOUT.nanos() >= crate::time::DEAF_CPU.nanos()); - /// Spins between deadline checks; `nanos_since_boot`'s 128-bit divide is too /// costly to call on every iteration. const SPINS_PER_DEADLINE_CHECK: u32 = 1024; @@ -142,11 +134,11 @@ fn wait_for(me: usize, cpu: u32, generation: Generation) { spins = 0; let now = crate::clock::nanos_since_boot(); match deadline { - None => deadline = Some(now.saturating_add(ACK_TIMEOUT.nanos())), + None => deadline = Some(now.saturating_add(crate::time::DEAF_CPU.nanos())), Some(at) if now >= at => panic!( "tlb: cpu {cpu} has not flushed for generation {generation:?} in {}ns — \ it is not taking interrupts", - ACK_TIMEOUT.nanos(), + crate::time::DEAF_CPU.nanos(), ), Some(_) => {} } diff --git a/kernel/src/arch/x86_64/vtd/mod.rs b/kernel/src/arch/x86_64/vtd/mod.rs index aa589e0e576..4c242f376d5 100644 --- a/kernel/src/arch/x86_64/vtd/mod.rs +++ b/kernel/src/arch/x86_64/vtd/mod.rs @@ -21,7 +21,7 @@ use alloc::vec::Vec; use crate::drivers::acpi::TableError; use crate::drivers::pci::PciDevice; use crate::iommu::{AddressWidth, StreamId}; -use crate::mm::paging::MmioPolicy; +use crate::mm::policy::MmioPolicy; use crate::mm::Mmio; use crate::sync::Lock; use crate::time::{Duration, Tripwire}; diff --git a/kernel/src/clock.rs b/kernel/src/clock.rs index f91c7e43e80..439b8c83a4d 100644 --- a/kernel/src/clock.rs +++ b/kernel/src/clock.rs @@ -53,7 +53,7 @@ fn publish_page(counter_at_boot: u64, period_fs: u64) { /// the ABI names. A region of its own, so no `mmap` can land on it and a /// fault in it is refused rather than filled. pub fn map_page(space: &mut crate::mm::paging::AddressSpace) { - use crate::mm::paging::{CachePolicy, Prot}; + use crate::mm::policy::{CachePolicy, Prot}; let phys = PAGE_PHYS.load(Acquire); assert!(phys != 0, "clock: an address space was built before the clock page"); let at = crate::UserAddr::new(toyos_abi::clock::CLOCK_PAGE); diff --git a/kernel/src/drivers/gop.rs b/kernel/src/drivers/gop.rs index 8c4ce15260e..4e769c3b42d 100644 --- a/kernel/src/drivers/gop.rs +++ b/kernel/src/drivers/gop.rs @@ -2,7 +2,7 @@ use alloc::boxed::Box; use toyos_abi::syscall::SyscallError; -use crate::mm::paging::{CachePolicy, MmioPolicy}; +use crate::mm::policy::{CachePolicy, MmioPolicy}; use crate::mm::{PAGE_2M, align_2m_checked, DirectMap}; use crate::gpu::{Gpu, GpuInfo}; use crate::log; diff --git a/kernel/src/drivers/hda.rs b/kernel/src/drivers/hda.rs index daf8f95b3d6..f8676aa5a4f 100644 --- a/kernel/src/drivers/hda.rs +++ b/kernel/src/drivers/hda.rs @@ -16,7 +16,7 @@ use toyos_hda::stream; use super::pci::PciDevice; use crate::log; -use crate::mm::paging::{CachePolicy, MmioPolicy}; +use crate::mm::policy::{CachePolicy, MmioPolicy}; use crate::mm::Mmio; use crate::object::shm::Region; use crate::sync::Lock; diff --git a/kernel/src/drivers/nvme.rs b/kernel/src/drivers/nvme.rs index b8674aa532d..470bdd0cd9e 100644 --- a/kernel/src/drivers/nvme.rs +++ b/kernel/src/drivers/nvme.rs @@ -13,7 +13,7 @@ use crate::mm::Mmio; use super::pci::PciDevice; use super::DmaPool; use crate::block::{self, BlockDevice, BlockError, BlockResult, DeviceId}; -use crate::mm::paging::MmioPolicy; +use crate::mm::policy::MmioPolicy; use crate::log; use crate::mm::{Dma, Unaligned}; use crate::scheduler::Operation; diff --git a/kernel/src/drivers/panic_console/mod.rs b/kernel/src/drivers/panic_console/mod.rs index 71ed2178f64..e25301c6456 100644 --- a/kernel/src/drivers/panic_console/mod.rs +++ b/kernel/src/drivers/panic_console/mod.rs @@ -24,7 +24,7 @@ use toyos_ps2::{KeyDecoder, KeyOutcome}; use crate::log; use crate::panic_reboot::Bound; use crate::time::{Budget, Cadence, Duration}; -use crate::mm::paging::MmioPolicy; +use crate::mm::policy::MmioPolicy; use crate::mm::{self, DirectMap, align_2m}; /// 1 bpp 8x16, codepoints 0x20..=0x7E, one byte per row, bit 7 leftmost. diff --git a/kernel/src/drivers/pci.rs b/kernel/src/drivers/pci.rs index 0deb9bf9a4c..897b341f94a 100644 --- a/kernel/src/drivers/pci.rs +++ b/kernel/src/drivers/pci.rs @@ -5,7 +5,7 @@ use core::sync::atomic::{AtomicU32, Ordering}; use toyos_pci::{bar, bridge, caps, msi, msix}; use crate::mm::Mmio; -use crate::mm::paging::MmioPolicy; +use crate::mm::policy::MmioPolicy; use crate::log; const VENDOR_ID: u64 = 0x00; diff --git a/kernel/src/drivers/virtio.rs b/kernel/src/drivers/virtio.rs index f3bd735bf38..5af06658097 100644 --- a/kernel/src/drivers/virtio.rs +++ b/kernel/src/drivers/virtio.rs @@ -4,7 +4,7 @@ use toyos_untrusted::{Refused, Untrusted}; use crate::mm::Mmio; use super::pci::PciDevice; -use crate::mm::paging::MmioPolicy; +use crate::mm::policy::MmioPolicy; use crate::log; const VIRTIO_PCI_CAP_COMMON_CFG: u8 = 1; diff --git a/kernel/src/drivers/virtio_gpu.rs b/kernel/src/drivers/virtio_gpu.rs index d81f73c42cb..74b12a434f2 100644 --- a/kernel/src/drivers/virtio_gpu.rs +++ b/kernel/src/drivers/virtio_gpu.rs @@ -8,7 +8,7 @@ use crate::iommu::{DeviceSpace, IommuError}; use crate::mm::{Dma, Unaligned, PAGE_2M}; use crate::gpu::{FLAG_HARDWARE_CURSOR, Gpu, GpuInfo}; use crate::log; -use crate::mm::paging::CachePolicy; +use crate::mm::policy::CachePolicy; use crate::object::shm::{Pages, Region}; const VIRTIO_VENDOR: u16 = 0x1AF4; diff --git a/kernel/src/drivers/virtio_sound.rs b/kernel/src/drivers/virtio_sound.rs index befdb9b15d8..f4787219c49 100644 --- a/kernel/src/drivers/virtio_sound.rs +++ b/kernel/src/drivers/virtio_sound.rs @@ -19,7 +19,7 @@ use super::virtio::{BufDir, UsedRingConsumer, VirtioDevice, Virtqueue, Virtqueue VIRTIO_F_VERSION_1}; use super::DmaPool; use crate::log; -use crate::mm::paging::CachePolicy; +use crate::mm::policy::CachePolicy; use crate::mm::{Dma, Mmio}; use crate::object::shm::Region; use crate::sync::Lock; diff --git a/kernel/src/drivers/xhci/wait/boot.rs b/kernel/src/drivers/xhci/wait/boot.rs index e815cff2004..493b58fb362 100644 --- a/kernel/src/drivers/xhci/wait/boot.rs +++ b/kernel/src/drivers/xhci/wait/boot.rs @@ -6,7 +6,7 @@ use core::sync::atomic::Ordering; use crate::log; use crate::time::{Budget, Cadence, Duration}; -use crate::mm::paging::MmioPolicy; +use crate::mm::policy::MmioPolicy; use crate::mm::Mmio; use crate::drivers::pci::PciDevice; use crate::drivers::DmaPool; diff --git a/kernel/src/elf/mod.rs b/kernel/src/elf/mod.rs index 928aaa7287e..a275054516c 100644 --- a/kernel/src/elf/mod.rs +++ b/kernel/src/elf/mod.rs @@ -117,8 +117,8 @@ impl LoadedLib { /// Protection for the page at `offset`: exec below the writable window, /// write inside it, read-only above — over-permissive, never under, for /// an unusual segment layout. - fn page_prot(&self, offset: u64) -> crate::mm::paging::Prot { - use crate::mm::paging::Prot; + fn page_prot(&self, offset: u64) -> crate::mm::policy::Prot { + use crate::mm::policy::Prot; if offset < self.rw_lo { Prot::ReadExec } else if offset < self.rw_hi { @@ -134,7 +134,7 @@ impl LoadedLib { /// A `Shared` module's split window holds a shared tail of `.text` plus /// the private copy, byte-identical there, so `ReadExec` is safe over either. pub fn map_into(&self, pt: &crate::process::PageTables) -> Option { - use crate::mm::paging::WindowProt; + use crate::mm::policy::WindowProt; let (image_phys, image_size) = match &self.memory { LibMemory::Owned(alloc) => ( @@ -164,7 +164,7 @@ impl LoadedLib { } } }; - let mut prot = WindowProt::uniform(crate::mm::paging::Prot::Read); + let mut prot = WindowProt::uniform(crate::mm::policy::Prot::Read); let mut page = 0; while page < PAGE_2M { prot.set(page, self.page_prot(offset + page)); diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index 8dc2109c481..344558fb1cd 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -26,7 +26,7 @@ use alloc::vec::Vec; use crate::elf; use crate::object::{ops, HandleTable, KObjectRef}; -use crate::mm::paging::{CachePolicy, Prot}; +use crate::mm::policy::{CachePolicy, Prot}; use crate::mm::{PAGE_2M, PAGE_BYTES}; use crate::process::{ ElfInfo, Endowments, OwnedAlloc, PageAlloc, PageFaultTrace, PageTables, Pid, @@ -168,8 +168,8 @@ fn insert_elf_regions( /// /// `PF_W | PF_X` refuses the write, not the execution: taking `X` away would /// let a hostile ELF run as data instead. -fn segment_prot(seg: &toyos_elf::Segment) -> crate::mm::paging::Prot { - use crate::mm::paging::Prot; +fn segment_prot(seg: &toyos_elf::Segment) -> crate::mm::policy::Prot { + use crate::mm::policy::Prot; if seg.flags.executable() { Prot::ReadExec } else if seg.flags.writable() { diff --git a/kernel/src/loader/tls.rs b/kernel/src/loader/tls.rs index 2ddd19c4e69..f62329ebcf7 100644 --- a/kernel/src/loader/tls.rs +++ b/kernel/src/loader/tls.rs @@ -147,7 +147,7 @@ pub fn map_block( let phys = alloc.phys(); let (vaddr, _) = crate::process::vma_map(child_pt, phys, alloc.size() as u64, - crate::mm::paging::Prot::ReadWrite)?; + crate::mm::policy::Prot::ReadWrite)?; let rebase = vaddr.raw() as i64 - phys as i64; let fs_base = (fs_base as i64 + rebase) as u64; // SAFETY: nothing runs in the unscheduled child yet, and `fs_base - vaddr` is the `tp_offset` the builder bounded. diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 14da5f126ff..b4aefaca46f 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -111,7 +111,7 @@ mod late_panic { } } -use crate::mm::paging::MmioPolicy; +use crate::mm::policy::MmioPolicy; use alloc::boxed::Box; use alloc::sync::Arc; use arch::{cpu, percpu, smp}; diff --git a/kernel/src/mm/unmapped.rs b/kernel/src/mm/unmapped.rs index 88807027927..9a07826f637 100644 --- a/kernel/src/mm/unmapped.rs +++ b/kernel/src/mm/unmapped.rs @@ -16,7 +16,7 @@ impl Unmapped { impl Drop for Unmapped { fn drop(&mut self) { - crate::arch::tlb::shootdown(crate::arch::tlb::Origin::Unmap); + crate::arch::tlb::shootdown(crate::invalidation::Origin::Unmap); // SAFETY: the wrapped value is never taken before this drop. unsafe { ManuallyDrop::drop(&mut self.0) }; } diff --git a/kernel/src/object/shm.rs b/kernel/src/object/shm.rs index dfa28dda66e..6fd236595f0 100644 --- a/kernel/src/object/shm.rs +++ b/kernel/src/object/shm.rs @@ -9,7 +9,7 @@ use alloc::vec::Vec; use toyos_abi::syscall::SyscallError; -use crate::mm::paging::{CachePolicy, Prot}; +use crate::mm::policy::{CachePolicy, Prot}; use crate::mm::{align_2m, pmm, Unmapped, PAGE_2M}; use crate::process::{PageTables, Pid}; use crate::sync::Lock; diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs index 498c99ffa3e..404eecd2767 100644 --- a/kernel/src/pcidev/mod.rs +++ b/kernel/src/pcidev/mod.rs @@ -113,7 +113,7 @@ use toyos_pci::{af, aperture, bar, express, msix, placement, pm, probe}; use crate::device::{Claim, ClaimError}; use crate::drivers::pci::{NoCapability, PciDevice, Unarmed}; use crate::iommu::{DeviceSpace, IommuError}; -use crate::mm::paging::{CachePolicy, MmioPolicy}; +use crate::mm::policy::{CachePolicy, MmioPolicy}; use crate::mm::{align_2m, DirectMap, Mmio, PAGE_2M}; use crate::object::shm::{Region, SharedMemObject}; use crate::sync::Lock; diff --git a/kernel/src/process.rs b/kernel/src/process.rs index 072cba886a7..d78877eb540 100644 --- a/kernel/src/process.rs +++ b/kernel/src/process.rs @@ -13,7 +13,7 @@ use alloc::sync::Arc; use alloc::vec::Vec; use core::ptr::NonNull; use crate::arch::percpu; -use crate::mm::paging::{CachePolicy, Prot, WindowProt}; +use crate::mm::policy::{CachePolicy, Prot, WindowProt}; use crate::mm::{PAGE_2M, PAGE_BYTES}; use crate::object::{ops, HandleTable}; use crate::sync::Lock; @@ -596,7 +596,7 @@ pub fn revoke_pipe_maps(maps: &mut Vec, pt: &PageTables, pipe: pipe::Pi }); } // Outside the block: it waits, and a sibling can be spinning on this lock with IF clear. - crate::arch::tlb::shootdown(crate::arch::tlb::Origin::Pipe); + crate::arch::tlb::shootdown(crate::invalidation::Origin::Pipe); } /// One live `mmap` and its physical pages; the range's registration in the address space's `regions` is separate (placement search, `munmap`). diff --git a/kernel/src/syscall/ipc.rs b/kernel/src/syscall/ipc.rs index 91dc54d9422..b0b573e77fc 100644 --- a/kernel/src/syscall/ipc.rs +++ b/kernel/src/syscall/ipc.rs @@ -8,7 +8,7 @@ use alloc::vec::Vec; use crate::watch; -use crate::mm::paging::Prot; +use crate::mm::policy::Prot; use crate::object::{ops, port, KObjectRef}; use crate::time::Deadline; use crate::user_ptr::SyscallContext; diff --git a/kernel/src/syscall/vm.rs b/kernel/src/syscall/vm.rs index c714c74c5f5..48e8db7e4e4 100644 --- a/kernel/src/syscall/vm.rs +++ b/kernel/src/syscall/vm.rs @@ -7,7 +7,7 @@ //! shoots down and waits, and a sibling thread can be spinning on that same //! lock with `IF` clear. -use crate::mm::paging::{CachePolicy, Prot}; +use crate::mm::policy::{CachePolicy, Prot}; use crate::vma::Occupancy; use crate::user_ptr::UserBytesMut; use crate::UserAddr; @@ -255,7 +255,7 @@ pub(super) fn sys_dlopen(ctx: &crate::user_ptr::SyscallContext, path: &str, init // `map_window`'s shootdown reached only this CPU, so the rest of the // machine is told here. if matches!(lib.memory, crate::elf::LibMemory::Shared { .. }) { - crate::arch::tlb::shootdown(crate::arch::tlb::Origin::Dlopen); + crate::arch::tlb::shootdown(crate::invalidation::Origin::Dlopen); } let delta = vaddr.raw() as i64 - lib.user_base.raw() as i64; if delta != 0 { @@ -279,7 +279,7 @@ pub(super) fn sys_dlopen(ctx: &crate::user_ptr::SyscallContext, path: &str, init process::with_process_data(|_data| { pt.lock().free_and_unmap(base); }); - crate::arch::tlb::shootdown(crate::arch::tlb::Origin::Dlopen); + crate::arch::tlb::shootdown(crate::invalidation::Origin::Dlopen); }); let lib_has_tls = lib.tls_memsz > 0; diff --git a/kernel/src/time.rs b/kernel/src/time.rs index bcfb1781057..1c62f9c7ada 100644 --- a/kernel/src/time.rs +++ b/kernel/src/time.rs @@ -218,9 +218,7 @@ impl fmt::Display for Tripwire { } /// How long a CPU waits for another to take an interrupt before calling it -/// deaf and panicking — the TLB shootdown's acknowledgement wait is this. A -/// spin that holds interrupts masked is bounded under it at its own site, and -/// never the other way. +/// deaf and panicking — the TLB shootdown's acknowledgement wait is this. pub const DEAF_CPU: Tripwire = Tripwire::absurd( Duration::from_secs(5), "no CPU that is not wedged goes five seconds without taking an interrupt", diff --git a/kernel/src/vma.rs b/kernel/src/vma.rs index eb617ca8374..9b1e9f59556 100644 --- a/kernel/src/vma.rs +++ b/kernel/src/vma.rs @@ -1,7 +1,7 @@ use alloc::sync::Arc; use crate::file_backing::FileBacking; -use crate::mm::paging::Prot; +use crate::mm::policy::Prot; use crate::mm::PAGE_2M; /// The stack extends upward to the PIE base, so no usable VA space exists above it. diff --git a/src/assets.rs b/src/assets.rs index f4f0bac9057..759ad2719f7 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -215,7 +215,7 @@ pub fn regen_panic_font(root: &Path) { /// again. A build that cannot find out what is committed refuses, because it /// cannot honestly build an image either. fn tracked(dir: &Path) -> BTreeSet { - crate::sysroot::tracked_files(dir, &[]).into_iter().map(PathBuf::from).collect() + crate::sysroot::tracked_files(dir, &[]).unwrap_or_else(|e| panic!("{e}")).into_iter().map(PathBuf::from).collect() } /// The paths `declared` names under `dir` that are not there, in the order they diff --git a/src/citations.rs b/src/citations.rs index 39afdd3a444..a21251039e1 100644 --- a/src/citations.rs +++ b/src/citations.rs @@ -16,9 +16,10 @@ //! //! **A root this tree no longer holds is still read**, or deleting a whole one //! would silence every citation into it: a token shaped like a file or a -//! directory (`name/…/file.ext`, `name/…/`, the name lowercase) whose first -//! component is no tracked root, no directory below one and no [`FOREIGN`] -//! name is a citation of a root that is gone, and reds. +//! directory (`name/`, `name/…/`, `name/…/file.ext`, the name lowercase after +//! an optional `.`) whose first component is no tracked root, no directory +//! below one and no [`FOREIGN`] name is a citation of a root that is gone, and +//! reds. A `FOREIGN` name no citation needs reds too. //! //! **What is not one**, each because another namespace spells the same text: //! @@ -34,8 +35,9 @@ //! //! So a path in another repository is written with that repository's name in //! front of it (`mio/src/sys/toyos/waker.rs`), a path that no longer exists is -//! written as the revision that held it (`^:src/durations.rs`), and a path -//! that does not exist yet is written relative to its crate (`arch/api.rs`). +//! written as the revision that held it (`^:src/durations.rs`), a path +//! that does not exist yet is written relative to its crate (`arch/api.rs`), +//! and a directory that exists nowhere is named without its slash (`fs`). //! //! **The holes it leaves.** A crate-relative citation (`sched/dump.rs`) is not //! read, even of a real file, and neither is a gone root whose name is also a @@ -52,13 +54,14 @@ use std::path::Path; /// header gives. const NOT_READ: &[&str] = &["rust", ".cargo"]; -/// First components the tracker writes that name something outside this tree: -/// QEMU's `hw/` and `chardev/`, the std fork's `library/`, `pal/` and `base/`, -/// the forked or quoted crates, a game's repository, and the host directories a -/// capture was read from. +/// First components the tracker and the `CLAUDE.md` files write that name +/// something outside this tree: QEMU's `hw/` and `chardev/`, the std fork's +/// `library/`, `compiler/`, `pal/` and `base/`, the forked or quoted crates, a +/// game's repository, the host directories a capture was read from, and the +/// untracked `.git/` and `.build-locks/` a checkout holds. const FOREIGN: &[&str] = &[ - "base", "chardev", "debug", "gbae", "hw", "library", "memmap2", "mio", "pal", "scratchpad", - "smoltcp-0.12.0", "softbuffer", "t14-run122", "t14-run76", + ".build-locks", ".git", "base", "chardev", "compiler", "debug", "gbae", "hw", "library", "memmap2", + "mio", "pal", "scratchpad", "softbuffer", "t14-run122", "t14-run76", ]; /// Characters that make a token a pattern rather than a path. @@ -72,7 +75,7 @@ fn delimits(c: char) -> bool { /// Every file `git` tracks, repository-relative, and every directory above one. pub fn tracked(root: &Path) -> BTreeSet { let mut all = BTreeSet::new(); - for file in crate::sysroot::tracked_files(root, &[]) { + for file in crate::sysroot::tracked_files(root, &[]).unwrap_or_else(|e| panic!("{e}")) { let mut at = 0; while let Some(slash) = file[at..].find('/') { all.insert(file[..at + slash].to_string()); @@ -109,9 +112,9 @@ fn path_shaped(first: &str, rest: &str) -> bool { let extension = rest.rsplit('/').next().and_then(|last| last.rsplit_once('.')).is_some_and(|(stem, ext)| { !stem.is_empty() && lowercase(ext) && ext.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()) }); - lowercase(first) + lowercase(first.strip_prefix('.').unwrap_or(first)) && first.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '-' | '_' | '.')) - && (rest.ends_with('/') || extension) + && (rest.is_empty() || rest.ends_with('/') || extension) } /// Every path `line` cites, with its trailing `/` kept off. @@ -138,7 +141,7 @@ pub fn cited(line: &str, names: &Names) -> Vec { if NOT_READ.contains(&first) || path.split('/').any(|segment| segment == "target") { continue; } - let gone = !names.below.contains(first) && !FOREIGN.contains(&first) && path_shaped(first, rest); + let gone = !names.below.contains(first) && path_shaped(first, rest); if names.roots.contains(first) || gone { found.push(path.trim_end_matches('/').to_string()); } @@ -146,12 +149,22 @@ pub fn cited(line: &str, names: &Names) -> Vec { found } -/// Every `file:line: cites path` in `text` that `tracked` does not hold. -pub fn dead(file: &str, text: &str, names: &Names, tracked: &BTreeSet) -> Vec { +/// Every `file:line: cites path` in `text` that `tracked` does not hold, with +/// each [`FOREIGN`] name that excused a citation added to `foreign`. +pub fn dead( + file: &str, + text: &str, + names: &Names, + tracked: &BTreeSet, + foreign: &mut BTreeSet<&'static str>, +) -> Vec { let mut out = Vec::new(); for (n, line) in text.lines().enumerate() { for path in cited(line, names) { - if !tracked.contains(&path) { + let first = path.split('/').next().unwrap_or_default(); + if let Some(name) = FOREIGN.iter().find(|f| **f == first && !names.roots.contains(first)) { + foreign.insert(name); + } else if !tracked.contains(&path) { out.push(format!("{file}:{}: cites {path}, which this tree does not hold", n + 1)); } } @@ -185,7 +198,8 @@ mod tests { items.iter().map(|s| s.to_string()).collect() } - /// **The gate.** Every path an issue or a `CLAUDE.md` cites resolves. + /// **The gate.** Every path an issue or a `CLAUDE.md` cites resolves, and + /// every [`FOREIGN`] name excuses one. #[test] fn every_path_the_tracker_and_the_claude_files_cite_exists() { let root = repo_root(); @@ -197,14 +211,17 @@ mod tests { "the walk reached neither the tracker nor the root CLAUDE.md, so it reads nothing: {files:?}" ); assert!(["kernel", "src", "issues"].iter().all(|r| names.roots.contains(*r)), "{:?}", names.roots); - let mut complaints = Vec::new(); + let (mut complaints, mut foreign) = (Vec::new(), BTreeSet::new()); let mut read = 0; for file in &files { let text = std::fs::read_to_string(root.join(file)).unwrap_or_else(|e| panic!("read {file}: {e}")); read += text.lines().map(|l| cited(l, &names).len()).sum::(); - complaints.extend(dead(file, &text, &names, &tracked)); + complaints.extend(dead(file, &text, &names, &tracked, &mut foreign)); } assert!(read > 500, "only {read} citations read across {} files; the scan is reading nothing", files.len()); + complaints.extend( + FOREIGN.iter().filter(|f| !foreign.contains(*f)).map(|f| format!("FOREIGN names {f}, and no citation needs it: delete it")), + ); assert!( complaints.is_empty(), "{} citation(s) name a path this tree does not hold. Point each at where the file went, \ @@ -215,24 +232,31 @@ mod tests { } /// Teeth: a dead citation reds, naming the file, the line and the path; a - /// live one, a directory and a line-suffixed one do not; and a root the - /// tree no longer holds is read rather than forgotten. + /// live one, a directory and a line-suffixed one do not; a root the tree no + /// longer holds is read in every shape rather than forgotten; and a + /// [`FOREIGN`] name is recorded as needed only where it excuses a citation. #[test] fn a_dead_citation_is_named_and_a_live_one_is_not() { let tracked = set(&["kernel", "kernel/src", "kernel/src/vfs.rs", "issues", "issues/README.md"]); let names = names(&tracked); let text = "See `kernel/src/vfs.rs:32` and kernel/src/.\nThen `kernel/src/gone.rs`, and issues/README.md.\n\ - `toyos-cc/src/lower.rs:9` and toyos-cc/tests/, not toyos-cc/, and/or `src/vfs.rs` in A/B.\n"; - assert_eq!(dead("issues/x/y.md", text, &names, &tracked), [ + `toyos-cc/src/lower.rs:9` and toyos-cc/tests/, and/or `src/vfs.rs` in A/B.\n\ + toyos-cc/, `.claude/agents/reviewer.md`, and ../forks, fs and 44100/2ch.\n\ + `mio/src/lib.rs`, `hw/`.\n"; + let mut foreign = BTreeSet::new(); + assert_eq!(dead("issues/x/y.md", text, &names, &tracked, &mut foreign), [ "issues/x/y.md:2: cites kernel/src/gone.rs, which this tree does not hold", "issues/x/y.md:3: cites toyos-cc/src/lower.rs, which this tree does not hold", "issues/x/y.md:3: cites toyos-cc/tests, which this tree does not hold", + "issues/x/y.md:4: cites toyos-cc, which this tree does not hold", + "issues/x/y.md:4: cites .claude/agents/reviewer.md, which this tree does not hold", ]); + assert_eq!(foreign, ["hw", "mio"].into_iter().collect()); } /// What another namespace spells is not read as ours. #[test] - fn a_pattern_a_panic_location_build_output_and_a_foreign_path_are_not_citations() { + fn a_pattern_a_panic_location_build_output_and_a_revision_are_not_citations() { let names = names(&set(&["kernel/src/arch/tlb.rs", "src/lib.rs", "tests/a", "rust/x", ".cargo/c"])); for line in [ "PANIC: panicked at src/arch/tlb.rs:171:42:", @@ -241,7 +265,7 @@ mod tests { "`kernel/src/{a,b}.rs`, `kernel/src/…`", "`kernel/target` 318 MB, `userland/target`", "the fork's `rust/src/bootstrap/` and a `.cargo/config.toml`", - "`mio/src/sys/toyos/waker.rs` and `d5c2d9c9^:src/durations.rs`", + "`d5c2d9c9^:src/durations.rs`", "https://github.com/ToyOSOrg/ToyOS/blob/main/src/lib.rs", "`arch/api.rs`, 44100/2ch/i16, read/write and A/B", ] { diff --git a/src/licence.rs b/src/licence.rs index 1ade4df95b5..f56de650429 100644 --- a/src/licence.rs +++ b/src/licence.rs @@ -1248,7 +1248,7 @@ pub fn judge(root: &Path) -> Result { packages: local.dirs.iter().filter_map(|d| relative(d)).filter(|d| !d.is_empty()).collect(), named: BTreeSet::new(), }; - let tracked = crate::sysroot::tracked_files(root, &[]); + let tracked = crate::sysroot::tracked_files(root, &[])?; let names: BTreeSet<&str> = COMMITTED_FILES.iter().map(|(p, ..)| file_name(p)).collect(); let sources = tracked.iter().filter(|f| under(&shipping.packages, f) && f.ends_with(".rs")); let read = sources @@ -1266,7 +1266,7 @@ pub fn judge(root: &Path) -> Result { let sections = sections(¬ice); let mut files = BTreeMap::new(); for section in §ions { - let named = crate::sysroot::tracked_files(root, &[&format!(":(glob){}", section.path)]); + let named = crate::sysroot::tracked_files(root, &[&format!(":(glob){}", section.path)])?; files.insert(section.path.clone(), named); } judge_notice(§ions, &files, COMMITTED_FILES, &shipping, &mut report); diff --git a/src/sdkversion.rs b/src/sdkversion.rs index 70da3fe4da1..0ded4d9e8e7 100644 --- a/src/sdkversion.rs +++ b/src/sdkversion.rs @@ -343,8 +343,8 @@ fn split_versions(root: &Path) -> Result, String> { } fn tracked_lockfiles(root: &Path) -> Result, String> { - let out = git(root, &["ls-files", "-z", "*Cargo.lock"])?; - Ok(out.split('\0').filter(|p| !p.is_empty() && !p.starts_with("rust/")).map(String::from).collect()) + let all = crate::sysroot::tracked_files(root, &["*Cargo.lock"])?; + Ok(all.into_iter().filter(|p| !p.starts_with("rust/")).collect()) } #[derive(Default)] diff --git a/src/sourcegate.rs b/src/sourcegate.rs index 73bdda92471..b0416d5cc23 100644 --- a/src/sourcegate.rs +++ b/src/sourcegate.rs @@ -71,8 +71,9 @@ //! //! The eleventh holds the kernel's arch layer below the rest: a file under //! `kernel/src/arch/` may name, by `crate::`, `$crate::` or a `super::` chain -//! out of it, only the architecture and the root's re-exports of it, and a name -//! the layer itself re-exports from above counts as where it came from. What it +//! out of it, only the architecture and the root's re-exports of it, and it +//! hands nothing from above to the rest of the layer: a `pub use` that reaches +//! above, any `pub type` and a glob of `crate::arch` are refused. What it //! names beyond that today is [`ARCH_REACHES_UP`], per file and item: the gate //! holds that list shrinking by item, and review holds the rest. //! @@ -1425,6 +1426,16 @@ impl Code { (start, self.0[start..end].iter().collect()) } + /// Whether the item keyword at `at` carries a visibility: `pub`, + /// `pub(crate)`, `pub(in …)`. + fn public(&self, at: usize) -> bool { + let at = match (0..at).rev().find(|&j| !self.0[j].is_whitespace()) { + Some(j) if self.0[j] == ')' => (0..j).rev().find(|&k| self.0[k] == '(').unwrap_or(at), + _ => at, + }; + self.ident_before(at).1 == "pub" + } + /// Whether `at` begins an item that imports: `use …`, or `extern crate …`, /// or an element of a `use` group. fn imported(&self, mut at: usize) -> bool { @@ -1730,31 +1741,19 @@ const ARCH_REACHES_UP: &[(&str, &[&str])] = &[ /// Every `(0-based line, item)` for a crate-root item that a file under /// [`ARCH_TREE`], `depth` modules below the root, names by a path -/// ([`spelled_paths`] from `crate` or `super`): the path's first segment, or, -/// through the architecture, a root alias of it or a `super` chain inside it, -/// the origin of each of `reexports` the path passes through. A glob of the -/// root is the item `*` and a rename of it `self as`. A path relative to the -/// module (`self::x`, a child's name, a name a `use` bound) is not read. +/// ([`spelled_paths`] from `crate` or `super`) that is not the architecture, a +/// root alias of it or a `super` chain inside it. A glob of the root is the +/// item `*` and a rename of it `self as`. A path relative to the module +/// (`self::x`, a child's name, a name a `use` bound) is not read. #[cfg(test)] -fn arch_reach( - text: &str, - depth: usize, - aliases: &[String], - reexports: &std::collections::BTreeSet<(String, String)>, -) -> Vec<(usize, String)> { +fn arch_reach(text: &str, depth: usize, aliases: &[String]) -> Vec<(usize, String)> { let mut found = Vec::new(); for path in spelled_paths(text, &["crate"], Some(depth)) { - let first = path.segments[0].as_str(); - if first == "self" { - found.extend(path.alias.is_some().then(|| (path.line, "self as".to_string()))); - } else if first == "arch" || first == "super" || aliases.iter().any(|a| a == first) { - for (name, origin) in reexports { - if path.segments[1..].contains(name) { - found.push((path.line, origin.clone())); - } - } - } else { - found.push((path.line, first.to_string())); + match path.segments[0].as_str() { + "self" => found.extend(path.alias.is_some().then(|| (path.line, "self as".to_string()))), + "arch" | "super" => {} + first if aliases.iter().any(|a| a == first) => {} + first => found.push((path.line, first.to_string())), } } found.sort(); @@ -1762,31 +1761,37 @@ fn arch_reach( found } -/// Every `(name, origin)` a file under [`ARCH_TREE`] re-exports from above the -/// layer: `pub use crate::invalidation::Origin;` is `("Origin", -/// "invalidation")`. A name a glob re-exports is not known, so not resolved. +/// Every `(0-based line, what)` by which a file under [`ARCH_TREE`] would hand +/// the rest of the layer a name from above without that file spelling where it +/// lives: a `pub use` that reaches above the layer, any `pub type` (an alias is +/// the one re-export rustc lets carry a privately imported name out), and a +/// glob of the architecture, which takes in names nobody spells. #[cfg(test)] -fn arch_reexports(files: &[(String, String)], aliases: &[String]) -> std::collections::BTreeSet<(String, String)> { - let mut out = std::collections::BTreeSet::new(); - for (at, text) in files.iter().filter(|(at, _)| at.starts_with(ARCH_TREE)) { - let code = text.lines().map(code_only).collect::>().join("\n"); - let mut offset = 0; - for line in code.split_inclusive('\n') { - let item = after_visibility(line); - if item != line.trim_start() && item.starts_with("use ") { - let statement = code[offset..].split(';').next().unwrap_or_default(); - for path in spelled_paths(statement, &["crate"], Some(module_depth(at))) { - let first = path.segments[0].as_str(); - if !["self", "arch", "super"].contains(&first) && !aliases.iter().any(|a| a == first) { - let name = path.alias.clone().or_else(|| path.segments.last().cloned()); - out.extend(name.map(|name| (name, first.to_string()))); - } - } +fn arch_hands_up(text: &str, depth: usize, aliases: &[String]) -> Vec<(usize, String)> { + let code = Code(text.lines().map(code_only).collect::>().join("\n").chars().collect()); + let mut found = Vec::new(); + for at in 0..code.0.len() { + match code.ident_at(at) { + Some((word, _)) if word == "type" && code.public(at) => { + found.push((code.line_of(at), "declares a `pub type`".to_string())); + } + Some((word, _)) if word == "use" && code.public(at) => { + let statement: String = code.0[at..].iter().take_while(|c| **c != ';').collect(); + let reached = arch_reach(&statement, depth, aliases).into_iter(); + found.extend(reached.map(|(_, item)| (code.line_of(at), format!("re-exports `crate::{item}`")))); } - offset += line.len(); + _ => {} } } - out + for path in spelled_paths(text, &["crate"], Some(depth)) { + let arch = path.segments[0] == "arch" || aliases.contains(&path.segments[0]); + if arch && path.segments.last().is_some_and(|s| s == "*") { + found.push((path.line, "globs `crate::arch`".to_string())); + } + } + found.sort(); + found.dedup(); + found } /// How many modules deep the file at repository-relative `path` sits below @@ -1838,9 +1843,14 @@ fn arch_reach_complaints( )); } } - let reexports = arch_reexports(files, aliases); for (at, text) in files.iter().filter(|(at, _)| at.starts_with(ARCH_TREE)) { - let named = arch_reach(text, module_depth(at), aliases, &reexports); + for (line, what) in arch_hands_up(text, module_depth(at), aliases) { + complaints.push(format!( + "{at}:{}: {what}, which hands the layer a name without its home: name each item where it lives", + line + 1 + )); + } + let named = arch_reach(text, module_depth(at), aliases); let permitted: &[&str] = rows.iter().find(|(file, _)| file == at).map_or(&[], |(_, items)| items); for (line, item) in &named { if !permitted.contains(&item.as_str()) { @@ -1932,10 +1942,9 @@ mod tests { /// what is not a path to one is not. #[test] fn every_spelling_of_a_crate_root_item_is_read() { - let reexports = [("Prot".to_string(), "mm".to_string())].into_iter().collect(); let hw = ["hw".to_string()]; let names = |text: &str, depth: usize| -> Vec { - arch_reach(text, depth, &hw, &reexports).into_iter().map(|(_, item)| item).collect() + arch_reach(text, depth, &hw).into_iter().map(|(_, item)| item).collect() }; assert_eq!(names("use crate::sched::driver;\n", 3), ["sched"]); assert_eq!(names(" crate::log!(\"x\");\n", 3), ["log"]); @@ -1949,9 +1958,6 @@ mod tests { assert_eq!(names("use super::super::{sched, mm};\n", 2), ["mm", "sched"]); assert_eq!(names("use self::super::super::super::drivers;\n", 3), ["drivers"]); assert_eq!(names("mod t {\n use super::super::super::super::sched;\n}\nmod u { use super::super::super::x; }\n", 3), ["sched"]); - // A name the layer re-exports from above is its origin, however it is reached. - assert_eq!(names("use crate::arch::paging::Prot;\n", 3), ["mm"]); - assert_eq!(names("use super::paging::{Other, Prot};\nlet p = crate::hw::paging::Prot::R;\n", 3), ["mm", "mm"]); // A char literal hides nothing after it. assert_eq!(names("let q = '\"'; let r = '\\''; fn f<'a>(x: &'a u8) { crate::log!(); }\n", 3), ["log"]); // Not code, not the root, not a path. @@ -1960,21 +1966,24 @@ mod tests { "let s = \"crate::sched\";\n", "pub(crate) fn f() {}\npub(super) fn g() {}\n", "use mycrate::sched;\nuse other_crate::x;\n", - "use self::super::x;\nuse super::{apic, smp};\nuse super::super::percpu;\nuse crate::arch::paging::Other;\n", + "use self::super::x;\nuse super::{apic, smp};\nuse super::super::percpu;\nuse crate::arch::paging::Prot;\n", + "let p = crate::hw::paging::Prot::R;\n", ] { assert_eq!(names(text, 3), Vec::::new(), "{text:?}"); } - let file = |at: &str, text: &str| (at.to_string(), text.to_string()); - let reexported = arch_reexports( - &[ - file("kernel/src/arch/x86_64/tlb.rs", "pub use crate::invalidation::Origin;\nuse crate::sched::X;\n"), - file("kernel/src/arch/x86_64/paging.rs", "pub(crate) use crate::mm::{\n policy::Prot,\n Mm as M,\n};\npub use super::cpu::outb;\n"), - file("kernel/src/sched/mod.rs", "pub use crate::log::Up;\n"), - ], - &hw, + // Handing a name from above to the layer, however it is spelled. + let handed = |text: &str| -> Vec<(usize, String)> { arch_hands_up(text, 3, &hw) }; + let at = |line: usize, what: &str| (line, what.to_string()); + assert_eq!(handed("pub(crate) use crate::mm::{\n policy::Prot,\n Mm as M,\n};\n"), [at(0, "re-exports `crate::mm`")]); + assert_eq!(handed("pub use super::super::super::sched::X;\n"), [at(0, "re-exports `crate::sched`")]); + assert_eq!( + handed("pub type Hop = crate::invalidation::Origin;\nuse crate::mm::policy::Prot;\npub(super) type P =\n Prot;\n"), + [at(0, "declares a `pub type`"), at(2, "declares a `pub type`")] ); - let pair = |n: &str, o: &str| (n.to_string(), o.to_string()); - assert_eq!(reexported, [pair("M", "mm"), pair("Origin", "invalidation"), pair("Prot", "mm")].into_iter().collect()); + assert_eq!(handed("use crate::arch::paging::*;\nuse crate::hw::{x, *};\n"), [at(0, "globs `crate::arch`"), at(1, "globs `crate::arch`")]); + let own = "pub use super::cpu::outb;\npub use Cr3 as Root;\nuse crate::mm::X;\nuse super::*;\n\ + impl I for S { type Output = u64; }\npub(crate) use irq_took;\npub use toyos_bootmap::aarch64::MAIR;\n"; + assert_eq!(handed(own), []); assert_eq!(module_depth("kernel/src/arch/mod.rs"), 1); assert_eq!(module_depth("kernel/src/arch/x86_64/mod.rs"), 2); assert_eq!(module_depth("kernel/src/arch/x86_64/tlb.rs"), 3); @@ -1986,28 +1995,25 @@ mod tests { } /// Teeth for the list: a new reach reds naming the file, the line and the - /// item, and so does one through a re-export; a listed one does not; a - /// listed one the file stopped spelling reds, and so does a row for a file - /// outside the layer; and a root alias of the architecture is the architecture. + /// item; a listed one does not; a listed one the file stopped spelling reds, + /// and so does a row for a file outside the layer; a root alias of the + /// architecture is the architecture; and a name handed up reds whatever the + /// list says. #[test] fn a_new_reach_out_of_arch_is_red_and_the_list_cannot_rot() { let file = |at: &str, text: &str| (at.to_string(), text.to_string()); - let files = [ + let mut files = vec![ file("kernel/src/arch/x86_64/tlb.rs", "use crate::time::Duration;\nuse crate::drivers::xhci;\n"), file("kernel/src/arch/x86_64/idt/timer.rs", "use crate::hw::HW;\nuse crate::arch::apic;\n"), - file("kernel/src/arch/x86_64/paging.rs", "pub use crate::mm::policy::Prot;\n"), - file("kernel/src/arch/x86_64/rtc.rs", "use crate::arch::paging::Prot;\n"), file("kernel/src/sched/driver.rs", "use crate::drivers::xhci;\n"), ]; let aliases = vec!["hw".to_string()]; - let paging = ("kernel/src/arch/x86_64/paging.rs", &["mm"][..]); - let said = arch_reach_complaints(&files, &[("kernel/src/arch/x86_64/tlb.rs", &["time"]), paging], &aliases); - assert_eq!(said.len(), 2, "{said:?}"); + let said = arch_reach_complaints(&files, &[("kernel/src/arch/x86_64/tlb.rs", &["time"])], &aliases); + assert_eq!(said.len(), 1, "{said:?}"); assert!(said[0].starts_with("kernel/src/arch/x86_64/tlb.rs:2: names `crate::drivers`"), "{said:?}"); - assert!(said[1].starts_with("kernel/src/arch/x86_64/rtc.rs:1: names `crate::mm`"), "{said:?}"); - let rtc = ("kernel/src/arch/x86_64/rtc.rs", &["mm"][..]); - let listed = arch_reach_complaints(&files, &[("kernel/src/arch/x86_64/tlb.rs", &["drivers", "time"]), paging, rtc], &aliases); + let tlb = ("kernel/src/arch/x86_64/tlb.rs", &["drivers", "time"][..]); + let listed = arch_reach_complaints(&files, &[tlb], &aliases); assert!(listed.is_empty(), "{listed:?}"); let stale = arch_reach_complaints( @@ -2016,8 +2022,6 @@ mod tests { ("kernel/src/arch/x86_64/tlb.rs", &["drivers", "time", "sched"]), ("kernel/src/arch/gone.rs", &["log"]), ("kernel/src/sched/driver.rs", &["drivers"]), - paging, - rtc, ], &aliases, ); @@ -2025,6 +2029,13 @@ mod tests { assert!(stale.iter().any(|s| s.contains("kernel/src/arch/gone.rs")), "{stale:?}"); assert!(stale.iter().any(|s| s.contains("names kernel/src/sched/driver.rs")), "{stale:?}"); assert!(stale.iter().any(|s| s.contains("`crate::sched`") && s.contains("no longer")), "{stale:?}"); + + files.push(file("kernel/src/arch/x86_64/paging.rs", "pub use crate::mm::policy::Prot;\n")); + files.push(file("kernel/src/arch/x86_64/rtc.rs", "use crate::arch::paging::*;\n")); + let handed = arch_reach_complaints(&files, &[tlb, ("kernel/src/arch/x86_64/paging.rs", &["mm"])], &aliases); + assert_eq!(handed.len(), 2, "{handed:?}"); + assert!(handed[0].starts_with("kernel/src/arch/x86_64/paging.rs:1: re-exports `crate::mm`, which hands"), "{handed:?}"); + assert!(handed[1].starts_with("kernel/src/arch/x86_64/rtc.rs:1: globs `crate::arch`, which hands"), "{handed:?}"); } /// **The architecture rules hold over the whole repository**, and no place @@ -2461,7 +2472,7 @@ mod tests { let walked: std::collections::BTreeSet = files.iter().map(|p| rel(&root, p)).collect(); let tracked: std::collections::BTreeSet = - crate::sysroot::tracked_files(&root, &[tree]).into_iter().filter(|p| p.ends_with(".rs")).collect(); + crate::sysroot::tracked_files(&root, &[tree]).unwrap_or_else(|e| panic!("{e}")).into_iter().filter(|p| p.ends_with(".rs")).collect(); assert!( tracked.len() > 1, "git tracks {} .rs file(s) under {tree}, so this floor is not one", @@ -2883,7 +2894,7 @@ mod tests { #[test] fn every_committed_binary_file_is_declared() { let root = repo_root(); - let listing = crate::sysroot::tracked_files(&root, &[]); + let listing = crate::sysroot::tracked_files(&root, &[]).unwrap_or_else(|e| panic!("{e}")); let mut found: Vec<(String, String)> = Vec::new(); for name in &listing { @@ -2944,7 +2955,7 @@ mod tests { let licence_path = format!("{CORPUS}/LICENSE"); let licence = std::fs::read_to_string(root.join(&licence_path)) .unwrap_or_else(|e| panic!("{licence_path}: {e}")); - let tracked = crate::sysroot::tracked_files(&root, &[]); + let tracked = crate::sysroot::tracked_files(&root, &[]).unwrap_or_else(|e| panic!("{e}")); let under_corpus: Vec<&String> = tracked.iter().filter(|f| f.starts_with(&format!("{CORPUS}/"))).collect(); assert!( diff --git a/src/sysroot.rs b/src/sysroot.rs index 2eddf9da9a5..348d3001837 100644 --- a/src/sysroot.rs +++ b/src/sysroot.rs @@ -584,19 +584,22 @@ fn path_str(path: &Path) -> &str { path.to_str().unwrap_or_else(|| panic!("{} is not UTF-8", path.display())) } -pub(crate) fn git_bytes(dir: &Path, args: &[&str]) -> Vec { +/// `Err` names the command, the directory and what git said. +fn git_try(dir: &Path, args: &[&str]) -> Result, String> { let out = Command::new("git") .args(args) .current_dir(dir) .output() - .unwrap_or_else(|e| panic!("run git in {}: {e}", dir.display())); - assert!( - out.status.success(), - "git {args:?} in {}: {}", - dir.display(), - String::from_utf8_lossy(&out.stderr).trim() - ); - out.stdout + .map_err(|e| format!("run git in {}: {e}", dir.display()))?; + if !out.status.success() { + let stderr = String::from_utf8_lossy(&out.stderr); + return Err(format!("git {args:?} in {}: {}", dir.display(), stderr.trim())); + } + Ok(out.stdout) +} + +pub(crate) fn git_bytes(dir: &Path, args: &[&str]) -> Vec { + git_try(dir, args).unwrap_or_else(|e| panic!("{e}")) } pub(crate) fn git_out(dir: &Path, args: &[&str]) -> String { @@ -604,10 +607,19 @@ pub(crate) fn git_out(dir: &Path, args: &[&str]) -> String { } /// The files `git` tracks under `dir` that `pathspecs` name, every one when -/// there are none, relative to `dir`. -pub(crate) fn tracked_files(dir: &Path, pathspecs: &[&str]) -> Vec { +/// there are none, relative to `dir`. A name that is not UTF-8 is refused by +/// name rather than rewritten into one git does not track. +pub(crate) fn tracked_files(dir: &Path, pathspecs: &[&str]) -> Result, String> { let args = [&["ls-files", "-z", "--"][..], pathspecs].concat(); - git_out(dir, &args).split('\0').filter(|f| !f.is_empty()).map(String::from).collect() + let listing = git_try(dir, &args)?; + let names = listing.split(|b| *b == 0).filter(|f| !f.is_empty()); + names + .map(|f| { + String::from_utf8(f.to_vec()).map_err(|_| { + format!("git tracks {:?} in {}, a name that is not UTF-8", String::from_utf8_lossy(f), dir.display()) + }) + }) + .collect() } fn git_run(dir: &Path, args: &[&str]) { From af66fb05f4bfecd5f3bade443c1df92bee0fecab Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 15:02:01 +0200 Subject: [PATCH 10/12] Delete the dead-citation gate and the arch upward-import gate: owner ruling, #537 has too much zookeeping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gates go whole, by owner ruling on #537: `src/citations.rs` (the tracker/ `CLAUDE.md` path-citation check, its `FOREIGN` list and its two tests) and, in `src/sourcegate.rs`, the eleventh scan — `ARCH_REACHES_UP`, `arch_reach`, `arch_hands_up`, `arch_reach_complaints`, `module_depth`, `arch_aliases`, `ARCH_TREE`, `KERNEL_ROOT` and their three tests. `issues/kernel/the-arch-layer- names-the-kernel-above-it.md` goes with it: nothing else cites it, so its exit condition dies with the gate rather than living on as an issue about a gate that no longer exists. `spelled_paths` loses the `depth: Option` parameter and the super-chain/ inline-module-scope machinery it carried only for `arch_reach`'s benefit — the one remaining caller, `arch_module_lines`, always passed `None`. `Code::public` goes too: nothing but the deleted `arch_hands_up` called it. Kept, per the ruling: the pure arch-rules scan (`ARCH_RULES`, asm/`target_arch`/ arch-path confinement — `every_architecture_rule_holds` still passes), the one-off tracker cleanup, the CLAUDE.md edits, the 47 crate descriptions and `src/hostws.rs`'s gate, the kernel's five upward re-exports and `DEAF_CPU`, and the `sysroot::tracked_files` consolidation. cargo test --lib: 398 passed, 0 failed, 1 ignored, exit 0. cargo test --workspace --exclude toyos-build: every crate green, exit 0. cargo run -- --clippy: 10 invocations clean, exit 0. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK --- ...he-arch-layer-names-the-kernel-above-it.md | 39 -- src/citations.rs | 277 ------------ src/lib.rs | 4 - src/sourcegate.rs | 409 +----------------- 4 files changed, 15 insertions(+), 714 deletions(-) delete mode 100644 issues/kernel/the-arch-layer-names-the-kernel-above-it.md delete mode 100644 src/citations.rs diff --git a/issues/kernel/the-arch-layer-names-the-kernel-above-it.md b/issues/kernel/the-arch-layer-names-the-kernel-above-it.md deleted file mode 100644 index ddeac00cf62..00000000000 --- a/issues/kernel/the-arch-layer-names-the-kernel-above-it.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-27 ---- - -# The arch layer names the kernel above it - -`kernel/src/arch/` is meant to be the machine and nothing else: generic code -reaches it through `crate::arch::…`, and it reaches nothing back. It does. Its -files name kernel modules above it by path — the scheduler, the process table, -the log, drivers, the IOMMU layer, test actuators. The gate -reds on a name a file adds and on a listed name a file no longer spells; each entry is this file's debt. - -What the list holds, by kind: - -- **Upcalls a contract would name.** Trap and syscall entry into the - scheduler and `syscall`, the log, `mm`, `clock`/`time`. These stay, but - through an interface generic code declares, not a path the architecture - picks. -- **PC platform devices living in the ISA layer.** The i8042 driving - `keyboard`, `mouse` and `irq_ring`; VT-d naming `iommu`, `pcidev` and - `drivers::{acpi, pci}`; the per-device vectors under `idt/` naming `drivers` and - `pcidev`; the TCO watchdog reading `params`. -- **Kernel state the architecture stores.** Per-CPU data holding - `process::{Pid, Tid}`. -- **Test hooks.** `actuator`, across boot, SMP, the i8042, VT-d and the - interrupt entry. - -Why it matters: the AArch64 port inherits whatever x86-64 exports, so every -upward name here is one the port has to satisfy or stub, and until the gate -nothing but review stopped the next. - -Owed by the ARM track, `issues/kernel/toyos-runs-on-arm64.md`: an `arch/api.rs` contract, a trait implemented -by a zero-sized type and dispatched statically, that is the only way generic -code reaches the machine and the only way the machine reaches back; and -`platform/{pc,virt}` for the PC's own devices, so `arch/` is the ISA alone. - -**Exit**: `ARCH_REACHES_UP` is empty. diff --git a/src/citations.rs b/src/citations.rs deleted file mode 100644 index a21251039e1..00000000000 --- a/src/citations.rs +++ /dev/null @@ -1,277 +0,0 @@ -//! Every path the tracker and the `CLAUDE.md` files cite is a path this tree -//! holds. -//! -//! An issue names the site it is about by path, and so does every `CLAUDE.md`; -//! a path is the claim a reader checks first. Moving or deleting a file leaves -//! every citation of it pointing at nothing, so this reds on one, naming the -//! citing file, its line and the missing path. A move carries its citations in -//! the same merge. -//! -//! **What counts as a citation.** A whitespace-, backtick- or bracket-delimited -//! token that begins with a directory `git` tracks at the root of this tree — -//! `kernel/…`, `src/…`, `issues/…` — with a `:line` or `:first-last` suffix -//! allowed and trailing sentence punctuation dropped. It resolves if `git` -//! tracks it or tracks a file under it. A path `git` does not track is one a -//! clean checkout does not have, however it looks in this one. -//! -//! **A root this tree no longer holds is still read**, or deleting a whole one -//! would silence every citation into it: a token shaped like a file or a -//! directory (`name/`, `name/…/`, `name/…/file.ext`, the name lowercase after -//! an optional `.`) whose first component is no tracked root, no directory -//! below one and no [`FOREIGN`] name is a citation of a root that is gone, and -//! reds. A `FOREIGN` name no citation needs reds too. -//! -//! **What is not one**, each because another namespace spells the same text: -//! -//! - a token carrying a pattern — `*`, `{`, `<`, `…`, `$` — which names many -//! paths or none; -//! - `path:line:column`, which is a Rust panic location, relative to whichever -//! crate panicked, quoted from a capture; -//! - anything through a `target` directory, which is build output; -//! - `rust/…`, the std fork: `src/forkcheck.rs` governs it, and a linked -//! worktree's `rust/` is empty until its first build; -//! - `.cargo/…`: `.cargo/config.toml` is the name cargo reads in every -//! directory, so a mention of one is a kind of file. -//! -//! So a path in another repository is written with that repository's name in -//! front of it (`mio/src/sys/toyos/waker.rs`), a path that no longer exists is -//! written as the revision that held it (`^:src/durations.rs`), a path -//! that does not exist yet is written relative to its crate (`arch/api.rs`), -//! and a directory that exists nowhere is named without its slash (`fs`). -//! -//! **The holes it leaves.** A crate-relative citation (`sched/dump.rs`) is not -//! read, even of a real file, and neither is a gone root whose name is also a -//! directory below one. A `^:` citation is trusted, never resolved: -//! resolving one needs the history, which the host job's depth-one checkout -//! does not have. -//! -//! Only its own tests read this, so it is not compiled into the build system. - -use std::collections::BTreeSet; -use std::path::Path; - -/// Roots whose citations this does not read, each for the reason the module -/// header gives. -const NOT_READ: &[&str] = &["rust", ".cargo"]; - -/// First components the tracker and the `CLAUDE.md` files write that name -/// something outside this tree: QEMU's `hw/` and `chardev/`, the std fork's -/// `library/`, `compiler/`, `pal/` and `base/`, the forked or quoted crates, a -/// game's repository, the host directories a capture was read from, and the -/// untracked `.git/` and `.build-locks/` a checkout holds. -const FOREIGN: &[&str] = &[ - ".build-locks", ".git", "base", "chardev", "compiler", "debug", "gbae", "hw", "library", "memmap2", - "mio", "pal", "scratchpad", "softbuffer", "t14-run122", "t14-run76", -]; - -/// Characters that make a token a pattern rather than a path. -const PATTERN: &[char] = &['*', '{', '}', '<', '>', '…', '$']; - -/// Characters that end a token. -fn delimits(c: char) -> bool { - c.is_whitespace() || matches!(c, '`' | '(' | ')' | '[' | ']' | '"' | '|' | ',' | ';') -} - -/// Every file `git` tracks, repository-relative, and every directory above one. -pub fn tracked(root: &Path) -> BTreeSet { - let mut all = BTreeSet::new(); - for file in crate::sysroot::tracked_files(root, &[]).unwrap_or_else(|e| panic!("{e}")) { - let mut at = 0; - while let Some(slash) = file[at..].find('/') { - all.insert(file[..at + slash].to_string()); - at += slash + 1; - } - all.insert(file); - } - all -} - -/// What a first component can mean: the directories `tracked` holds at the -/// root, and the names of those below it. -pub struct Names { - roots: BTreeSet, - below: BTreeSet, -} - -pub fn names(tracked: &BTreeSet) -> Names { - let (mut roots, mut below) = (BTreeSet::new(), BTreeSet::new()); - for path in tracked { - let mut dirs = path.split('/').rev().skip(1).collect::>(); - if let Some(root) = dirs.pop() { - roots.insert(root.to_string()); - } - below.extend(dirs.into_iter().map(String::from)); - } - Names { roots, below } -} - -/// Whether `first/rest` is shaped like a file or a directory rather than prose -/// (`and/or`, `A/B`, `44100/2ch`). -fn path_shaped(first: &str, rest: &str) -> bool { - let lowercase = |s: &str| s.starts_with(|c: char| c.is_ascii_lowercase()); - let extension = rest.rsplit('/').next().and_then(|last| last.rsplit_once('.')).is_some_and(|(stem, ext)| { - !stem.is_empty() && lowercase(ext) && ext.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()) - }); - lowercase(first.strip_prefix('.').unwrap_or(first)) - && first.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '-' | '_' | '.')) - && (rest.is_empty() || rest.ends_with('/') || extension) -} - -/// Every path `line` cites, with its trailing `/` kept off. -pub fn cited(line: &str, names: &Names) -> Vec { - let mut found = Vec::new(); - for token in line.split(delimits) { - if token.contains(PATTERN) { - continue; - } - let token = token.trim_end_matches(['.', '!', '?']); - let token = token.strip_suffix("'s").unwrap_or(token).trim_end_matches('\''); - let (path, suffix) = match token.split_once(':') { - Some((path, suffix)) => (path, Some(suffix)), - None => (token, None), - }; - if let Some(suffix) = suffix { - let numbers: Vec<&str> = suffix.trim_end_matches(':').split(':').collect(); - let numeric = |s: &str| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit() || c == '-'); - if numbers.len() >= 2 && numbers.iter().all(|n| numeric(n)) { - continue; - } - } - let Some((first, rest)) = path.split_once('/') else { continue }; - if NOT_READ.contains(&first) || path.split('/').any(|segment| segment == "target") { - continue; - } - let gone = !names.below.contains(first) && path_shaped(first, rest); - if names.roots.contains(first) || gone { - found.push(path.trim_end_matches('/').to_string()); - } - } - found -} - -/// Every `file:line: cites path` in `text` that `tracked` does not hold, with -/// each [`FOREIGN`] name that excused a citation added to `foreign`. -pub fn dead( - file: &str, - text: &str, - names: &Names, - tracked: &BTreeSet, - foreign: &mut BTreeSet<&'static str>, -) -> Vec { - let mut out = Vec::new(); - for (n, line) in text.lines().enumerate() { - for path in cited(line, names) { - let first = path.split('/').next().unwrap_or_default(); - if let Some(name) = FOREIGN.iter().find(|f| **f == first && !names.roots.contains(first)) { - foreign.insert(name); - } else if !tracked.contains(&path) { - out.push(format!("{file}:{}: cites {path}, which this tree does not hold", n + 1)); - } - } - } - out -} - -/// The files whose citations are read: every tracked `.md` under `issues/`, and -/// every tracked `CLAUDE.md`. -pub fn citing(tracked: &BTreeSet) -> Vec { - tracked - .iter() - .filter(|p| { - (p.starts_with("issues/") && p.ends_with(".md")) - || p.rsplit('/').next() == Some("CLAUDE.md") - }) - .cloned() - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - use std::path::PathBuf; - - fn repo_root() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - } - - fn set(items: &[&str]) -> BTreeSet { - items.iter().map(|s| s.to_string()).collect() - } - - /// **The gate.** Every path an issue or a `CLAUDE.md` cites resolves, and - /// every [`FOREIGN`] name excuses one. - #[test] - fn every_path_the_tracker_and_the_claude_files_cite_exists() { - let root = repo_root(); - let tracked = tracked(&root); - let names = names(&tracked); - let files = citing(&tracked); - assert!( - files.iter().any(|f| f == "issues/README.md") && files.iter().any(|f| f == "CLAUDE.md"), - "the walk reached neither the tracker nor the root CLAUDE.md, so it reads nothing: {files:?}" - ); - assert!(["kernel", "src", "issues"].iter().all(|r| names.roots.contains(*r)), "{:?}", names.roots); - let (mut complaints, mut foreign) = (Vec::new(), BTreeSet::new()); - let mut read = 0; - for file in &files { - let text = std::fs::read_to_string(root.join(file)).unwrap_or_else(|e| panic!("read {file}: {e}")); - read += text.lines().map(|l| cited(l, &names).len()).sum::(); - complaints.extend(dead(file, &text, &names, &tracked, &mut foreign)); - } - assert!(read > 500, "only {read} citations read across {} files; the scan is reading nothing", files.len()); - complaints.extend( - FOREIGN.iter().filter(|f| !foreign.contains(*f)).map(|f| format!("FOREIGN names {f}, and no citation needs it: delete it")), - ); - assert!( - complaints.is_empty(), - "{} citation(s) name a path this tree does not hold. Point each at where the file went, \ - or write a deleted one as the revision that held it (`^:`):\n{}", - complaints.len(), - complaints.join("\n"), - ); - } - - /// Teeth: a dead citation reds, naming the file, the line and the path; a - /// live one, a directory and a line-suffixed one do not; a root the tree no - /// longer holds is read in every shape rather than forgotten; and a - /// [`FOREIGN`] name is recorded as needed only where it excuses a citation. - #[test] - fn a_dead_citation_is_named_and_a_live_one_is_not() { - let tracked = set(&["kernel", "kernel/src", "kernel/src/vfs.rs", "issues", "issues/README.md"]); - let names = names(&tracked); - let text = "See `kernel/src/vfs.rs:32` and kernel/src/.\nThen `kernel/src/gone.rs`, and issues/README.md.\n\ - `toyos-cc/src/lower.rs:9` and toyos-cc/tests/, and/or `src/vfs.rs` in A/B.\n\ - toyos-cc/, `.claude/agents/reviewer.md`, and ../forks, fs and 44100/2ch.\n\ - `mio/src/lib.rs`, `hw/`.\n"; - let mut foreign = BTreeSet::new(); - assert_eq!(dead("issues/x/y.md", text, &names, &tracked, &mut foreign), [ - "issues/x/y.md:2: cites kernel/src/gone.rs, which this tree does not hold", - "issues/x/y.md:3: cites toyos-cc/src/lower.rs, which this tree does not hold", - "issues/x/y.md:3: cites toyos-cc/tests, which this tree does not hold", - "issues/x/y.md:4: cites toyos-cc, which this tree does not hold", - "issues/x/y.md:4: cites .claude/agents/reviewer.md, which this tree does not hold", - ]); - assert_eq!(foreign, ["hw", "mio"].into_iter().collect()); - } - - /// What another namespace spells is not read as ours. - #[test] - fn a_pattern_a_panic_location_build_output_and_a_revision_are_not_citations() { - let names = names(&set(&["kernel/src/arch/tlb.rs", "src/lib.rs", "tests/a", "rust/x", ".cargo/c"])); - for line in [ - "PANIC: panicked at src/arch/tlb.rs:171:42:", - "LOCK CONTENTION: 50M spins at src/vfs.rs:32:18, ticket=38", - "`kernel/src/arch/idt/*.rs` and `tests/case/system.toml`", - "`kernel/src/{a,b}.rs`, `kernel/src/…`", - "`kernel/target` 318 MB, `userland/target`", - "the fork's `rust/src/bootstrap/` and a `.cargo/config.toml`", - "`d5c2d9c9^:src/durations.rs`", - "https://github.com/ToyOSOrg/ToyOS/blob/main/src/lib.rs", - "`arch/api.rs`, 44100/2ch/i16, read/write and A/B", - ] { - assert!(cited(line, &names).is_empty(), "{line:?} read as {:?}", cited(line, &names)); - } - assert_eq!(cited("`src/redlist.rs`'s row, `kernel/src/a.rs:1-9`.", &names), ["src/redlist.rs", "kernel/src/a.rs"]); - assert_eq!(cited("(src/x.rs) [kernel/y/](kernel/y/)", &names), ["src/x.rs", "kernel/y", "kernel/y"]); - } -} diff --git a/src/lib.rs b/src/lib.rs index 85fcd1b04ca..e44e0c0ced2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,10 +10,6 @@ pub mod bootlog; pub mod build; pub mod buildlock; pub mod ci; -/// The citation gate over the tracker and the `CLAUDE.md` files, read by -/// nothing but its own tests. -#[cfg(test)] -pub mod citations; pub mod clippy; pub mod compiler; /// What the untouched-disk gate compares a device against, in `tests/`. diff --git a/src/sourcegate.rs b/src/sourcegate.rs index b0416d5cc23..ddddf4f52ef 100644 --- a/src/sourcegate.rs +++ b/src/sourcegate.rs @@ -69,14 +69,6 @@ //! holding what it owes, and one that no longer holds a needle is refused. //! The rust fork's std is `src/forkcheck.rs`'s to govern and is not read here. //! -//! The eleventh holds the kernel's arch layer below the rest: a file under -//! `kernel/src/arch/` may name, by `crate::`, `$crate::` or a `super::` chain -//! out of it, only the architecture and the root's re-exports of it, and it -//! hands nothing from above to the rest of the layer: a `pub use` that reaches -//! above, any `pub type` and a glob of `crate::arch` are refused. What it -//! names beyond that today is [`ARCH_REACHES_UP`], per file and item: the gate -//! holds that list shrinking by item, and review holds the rest. -//! //! **What none of them reaches is filed rather than implied**, and each table's //! own doc names its half: the entries under `issues/build/` say so. @@ -1283,9 +1275,8 @@ const ARCH_MODULE: &str = "core::arch/std::arch"; /// A path code spells from a root: the 0-based line of its last segment, the /// segments after the root, and the name a rename binds. A group is one path -/// per element, a glob ends in `*`, a rename of the root itself (`core as k`, -/// `core::{self as k}`) is `self` renamed, and a `super` chain that stays below -/// the crate root begins with `super`. +/// per element, a glob ends in `*`, and a rename of the root itself (`core as +/// k`, `core::{self as k}`) is `self` renamed. #[cfg(test)] struct Spelled { line: usize, @@ -1293,60 +1284,30 @@ struct Spelled { alias: Option, } -/// Every path in `text` from one of `roots` (`core`, `crate`, which `$crate` -/// spells too) and, given the module's `depth` below the crate root, from a -/// `super` chain, `self::` before it and each inline `mod x {}` around it -/// counted. The root's rename counts only where it imports: `use core as k;`, -/// `extern crate core as k;`, an element of a `use` group. A glob or a rename -/// is not followed to what it brings into scope: that is resolution, which a -/// scan does not have. +/// Every path in `text` from one of `roots` (`core`, `std`, which `$crate` +/// also spells for `crate`). The root's rename counts only where it imports: +/// `use core as k;`, `extern crate core as k;`, an element of a `use` group. A +/// glob or a rename is not followed to what it brings into scope: that is +/// resolution, which a scan does not have. #[cfg(test)] -fn spelled_paths(text: &str, roots: &[&str], depth: Option) -> Vec { +fn spelled_paths(text: &str, roots: &[&str]) -> Vec { let code = Code(text.lines().map(code_only).collect::>().join("\n").chars().collect()); let mut found = Vec::new(); - // Whether each open `{` is an inline module's, and whether the next one is. - let (mut scopes, mut opens_mod) = (Vec::new(), false); let mut at = 0; while at < code.0.len() { - match code.0[at] { - '{' if depth.is_some() => scopes.push(std::mem::take(&mut opens_mod)), - '}' if depth.is_some() => _ = scopes.pop().expect("a `}` closes nothing: a literal's brace reached the code"), - _ => {} - } let Some((name, end)) = code.ident_at(at) else { at += 1; continue; }; - let after = code.skip_space(end); - let mut prefix = Vec::new(); - let root_end = match depth { - _ if roots.contains(&name.as_str()) => Some(after), - Some(depth) if name == "super" && code.ident_before(at).1 != "super" => { - let (mut supers, mut next) = (1, after); - while let Some((_, e)) = - code.colons(next).then(|| code.ident_at(code.skip_space(next + 2))).flatten().filter(|(s, _)| s == "super") - { - supers += 1; - next = code.skip_space(e); - } - if supers < depth + scopes.iter().filter(|m| **m).count() { - prefix.push("super".to_string()); - } - Some(next) - } - _ => None, - }; - if name == "mod" { - opens_mod = code.ident_at(after).is_some_and(|(_, e)| code.0.get(code.skip_space(e)) == Some(&'{')); - } - match root_end { - Some(next) if code.colons(next) => code.tree(next + 2, prefix, &mut found), - Some(next) if prefix.is_empty() && code.imported(at) => { - let alias = code.alias_at(next); + if roots.contains(&name.as_str()) { + let after = code.skip_space(end); + if code.colons(after) { + code.tree(after + 2, Vec::new(), &mut found); + } else if code.imported(at) { + let alias = code.alias_at(after); let segments = vec!["self".to_string()]; found.extend(alias.is_some().then(|| Spelled { line: code.line_of(at), segments, alias })); } - _ => {} } at = end; } @@ -1426,16 +1387,6 @@ impl Code { (start, self.0[start..end].iter().collect()) } - /// Whether the item keyword at `at` carries a visibility: `pub`, - /// `pub(crate)`, `pub(in …)`. - fn public(&self, at: usize) -> bool { - let at = match (0..at).rev().find(|&j| !self.0[j].is_whitespace()) { - Some(j) if self.0[j] == ')' => (0..j).rev().find(|&k| self.0[k] == '(').unwrap_or(at), - _ => at, - }; - self.ident_before(at).1 == "pub" - } - /// Whether `at` begins an item that imports: `use …`, or `extern crate …`, /// or an element of a `use` group. fn imported(&self, mut at: usize) -> bool { @@ -1479,7 +1430,7 @@ impl Code { /// an arch module has no other use for either. #[cfg(test)] fn arch_module_lines(text: &str) -> Vec { - spelled_paths(text, &["core", "std"], None) + spelled_paths(text, &["core", "std"]) .into_iter() .filter(|p| matches!(p.segments[0].as_str(), "arch" | "*") || (p.segments[0] == "self" && p.alias.is_some())) .map(|p| p.line) @@ -1664,216 +1615,6 @@ fn every_rust_file() -> Vec<(String, String)> { .collect() } -// ── The arch layer names nothing above it ─────────────────────────────────── - -/// The kernel's architecture layer: every file under it is read. -#[cfg(test)] -const ARCH_TREE: &str = "kernel/src/arch/"; - -/// Where the kernel's root declares its modules and re-exports. -#[cfg(test)] -const KERNEL_ROOT: &str = "kernel/src/main.rs"; - -/// Every crate-root item a file under [`ARCH_TREE`] names that is neither the -/// architecture nor a root re-export of it: the layer reaching up into the -/// kernel it is meant to sit under. Each entry is recorded debt, owed by -/// `issues/kernel/the-arch-layer-names-the-kernel-above-it.md`, which is done -/// when this list is empty. -/// -/// **It only shrinks, by item.** A name a file adds reds -/// `the_arch_layer_names_nothing_above_it`, and so does a name a row lists that -/// its file no longer spells, so no row outlives what it excused. It does not -/// count reach: a file whose row holds `sched` may name more of `crate::sched` -/// unseen, and a row that grows is green, so that it shrinks is held by review -/// and not by this gate. -#[cfg(test)] -const ARCH_REACHES_UP: &[(&str, &[&str])] = &[ - ("kernel/src/arch/aarch64/boot.rs", &["PHYS_OFFSET", "actuator", "drivers", "kernel_main", "log", "mm"]), - ("kernel/src/arch/aarch64/console_uart.rs", &["drivers", "log", "mm"]), - ("kernel/src/arch/aarch64/control_regs.rs", &["log"]), - ("kernel/src/arch/aarch64/hw.rs", &["clock", "log", "mm", "sched", "scheduler", "trace"]), - ("kernel/src/arch/aarch64/iommu_unit.rs", &["drivers", "iommu", "log"]), - ("kernel/src/arch/aarch64/mod.rs", &["actuator", "log"]), - ("kernel/src/arch/aarch64/paging.rs", &["MemoryMapEntry", "mm", "sync", "vma"]), - ("kernel/src/arch/aarch64/percpu.rs", &["log", "process"]), - ("kernel/src/arch/aarch64/tlb.rs", &["invalidation"]), - ("kernel/src/arch/aarch64/trap.rs", &["alert", "log", "symbols"]), - ("kernel/src/arch/aarch64/watchdog.rs", &["drivers"]), - ("kernel/src/arch/x86_64/apic.rs", &["clock", "log", "time", "trace"]), - ("kernel/src/arch/x86_64/boot.rs", &["PHYS_OFFSET", "actuator", "clock", "drivers", "kernel_main", "log", "mm"]), - ("kernel/src/arch/x86_64/console_uart.rs", &["log"]), - ("kernel/src/arch/x86_64/control_regs.rs", &["actuator", "log"]), - ("kernel/src/arch/x86_64/entry.rs", &["sched"]), - ("kernel/src/arch/x86_64/fpu.rs", &["log"]), - ("kernel/src/arch/x86_64/hpet.rs", &["clock", "log", "mm", "time"]), - ("kernel/src/arch/x86_64/hw.rs", &["actuator", "clock", "deadline", "heartbeat", "log", "mm", "preempt", "process", "sched", "scheduler", "time", "trace", "watch"]), - ("kernel/src/arch/x86_64/i8042/mod.rs", &["actuator", "clock", "drivers", "irq_ring", "keyboard", "log", "mouse", "preempt", "sync", "time"]), - ("kernel/src/arch/x86_64/idt/dma_fault.rs", &["iommu"]), - ("kernel/src/arch/x86_64/idt/exceptions.rs", &["DirectMap", "actuator", "alert", "log", "mm", "panic", "process", "scheduler", "symbols", "syscall"]), - ("kernel/src/arch/x86_64/idt/hda.rs", &["drivers"]), - ("kernel/src/arch/x86_64/idt/log_nest.rs", &["log"]), - ("kernel/src/arch/x86_64/idt/mod.rs", &["actuator", "blackbox", "log", "preempt", "sched", "scheduler", "sync", "trace"]), - ("kernel/src/arch/x86_64/idt/nmi.rs", &["drivers", "hardlockup", "panic", "sched"]), - ("kernel/src/arch/x86_64/idt/spurious.rs", &["clock", "irq_census", "log", "time"]), - ("kernel/src/arch/x86_64/idt/timer.rs", &["deadline", "irq_census", "preempt", "scheduler"]), - ("kernel/src/arch/x86_64/idt/unclaimed.rs", &["clock", "irq_census", "log", "time"]), - ("kernel/src/arch/x86_64/idt/user_dev.rs", &["clock", "irq_ring", "pcidev", "preempt"]), - ("kernel/src/arch/x86_64/idt/virtio_sound.rs", &["drivers"]), - ("kernel/src/arch/x86_64/idt/xhci.rs", &["clock", "irq_ring", "preempt"]), - ("kernel/src/arch/x86_64/ioapic.rs", &["drivers", "iommu", "log", "mm", "sync"]), - ("kernel/src/arch/x86_64/mod.rs", &["actuator", "log"]), - ("kernel/src/arch/x86_64/nmi_gate.rs", &["actuator", "clock", "log", "mm", "sched", "symbols"]), - ("kernel/src/arch/x86_64/paging.rs", &["MemoryMapEntry", "hasher", "invalidation", "log", "mm", "sched", "sync", "vma"]), - ("kernel/src/arch/x86_64/percpu.rs", &["actuator", "drivers", "irq_census", "log", "mm", "process", "sync"]), - ("kernel/src/arch/x86_64/rtc.rs", &["actuator", "clock", "time"]), - ("kernel/src/arch/x86_64/smp.rs", &["DirectMap", "actuator", "clock", "drivers", "hardlockup", "log", "mm", "process", "scheduler", "smp_roster", "time"]), - ("kernel/src/arch/x86_64/syscall.rs", &["syscall"]), - ("kernel/src/arch/x86_64/tlb.rs", &["clock", "invalidation", "log", "mm", "sched", "shootdown", "time"]), - ("kernel/src/arch/x86_64/vtd/dmar.rs", &["drivers", "iommu"]), - ("kernel/src/arch/x86_64/vtd/domain.rs", &["iommu", "log", "mm", "sync"]), - ("kernel/src/arch/x86_64/vtd/fault.rs", &["drivers", "iommu", "log", "mm", "panic", "pcidev"]), - ("kernel/src/arch/x86_64/vtd/interrupt.rs", &["iommu", "log", "sync"]), - ("kernel/src/arch/x86_64/vtd/mod.rs", &["actuator", "clock", "drivers", "iommu", "log", "mm", "sync", "time"]), - ("kernel/src/arch/x86_64/vtd/queue.rs", &["clock", "mm", "time"]), - ("kernel/src/arch/x86_64/vtd/table.rs", &["actuator", "iommu", "mm"]), - ("kernel/src/arch/x86_64/watchdog.rs", &["actuator", "drivers", "log", "params", "time"]), -]; - -/// Every `(0-based line, item)` for a crate-root item that a file under -/// [`ARCH_TREE`], `depth` modules below the root, names by a path -/// ([`spelled_paths`] from `crate` or `super`) that is not the architecture, a -/// root alias of it or a `super` chain inside it. A glob of the root is the -/// item `*` and a rename of it `self as`. A path relative to the module -/// (`self::x`, a child's name, a name a `use` bound) is not read. -#[cfg(test)] -fn arch_reach(text: &str, depth: usize, aliases: &[String]) -> Vec<(usize, String)> { - let mut found = Vec::new(); - for path in spelled_paths(text, &["crate"], Some(depth)) { - match path.segments[0].as_str() { - "self" => found.extend(path.alias.is_some().then(|| (path.line, "self as".to_string()))), - "arch" | "super" => {} - first if aliases.iter().any(|a| a == first) => {} - first => found.push((path.line, first.to_string())), - } - } - found.sort(); - found.dedup(); - found -} - -/// Every `(0-based line, what)` by which a file under [`ARCH_TREE`] would hand -/// the rest of the layer a name from above without that file spelling where it -/// lives: a `pub use` that reaches above the layer, any `pub type` (an alias is -/// the one re-export rustc lets carry a privately imported name out), and a -/// glob of the architecture, which takes in names nobody spells. -#[cfg(test)] -fn arch_hands_up(text: &str, depth: usize, aliases: &[String]) -> Vec<(usize, String)> { - let code = Code(text.lines().map(code_only).collect::>().join("\n").chars().collect()); - let mut found = Vec::new(); - for at in 0..code.0.len() { - match code.ident_at(at) { - Some((word, _)) if word == "type" && code.public(at) => { - found.push((code.line_of(at), "declares a `pub type`".to_string())); - } - Some((word, _)) if word == "use" && code.public(at) => { - let statement: String = code.0[at..].iter().take_while(|c| **c != ';').collect(); - let reached = arch_reach(&statement, depth, aliases).into_iter(); - found.extend(reached.map(|(_, item)| (code.line_of(at), format!("re-exports `crate::{item}`")))); - } - _ => {} - } - } - for path in spelled_paths(text, &["crate"], Some(depth)) { - let arch = path.segments[0] == "arch" || aliases.contains(&path.segments[0]); - if arch && path.segments.last().is_some_and(|s| s == "*") { - found.push((path.line, "globs `crate::arch`".to_string())); - } - } - found.sort(); - found.dedup(); - found -} - -/// How many modules deep the file at repository-relative `path` sits below -/// the kernel's crate root: `kernel/src/arch/mod.rs` is 1, -/// `kernel/src/arch/x86_64/tlb.rs` is 3. -#[cfg(test)] -fn module_depth(path: &str) -> usize { - let under = path.strip_prefix("kernel/src/").unwrap_or(path); - let parts = under.split('/').count(); - if under.ends_with("/mod.rs") { parts - 1 } else { parts } -} - -/// The names the kernel root re-exports from the architecture -/// (`pub(crate) use arch::hw;`, `use arch::{cpu, smp};`), so `crate::hw` is the -/// architecture naming itself through the root rather than reaching above it. -#[cfg(test)] -fn arch_aliases(root: &str) -> Vec { - let mut names = Vec::new(); - for line in root.lines() { - let code = code_only(line); - let Some(rest) = after_visibility(code.trim()).strip_prefix("use arch::") else { continue }; - let rest = rest.trim_end().trim_end_matches(';'); - let rest = rest.strip_prefix('{').and_then(|r| r.strip_suffix('}')).unwrap_or(rest); - for element in rest.split(',').map(str::trim).filter(|e| !e.is_empty()) { - let local = element.rsplit(" as ").next().unwrap_or(element); - names.push(local.rsplit("::").next().unwrap_or(local).trim().to_string()); - } - } - names -} - -/// Every red `rows` and `files` give: a crate-root item a file under -/// [`ARCH_TREE`] names and no row permits it, and a row entry its file no -/// longer spells. -#[cfg(test)] -fn arch_reach_complaints( - files: &[(String, String)], - rows: &[(&str, &[&str])], - aliases: &[String], -) -> Vec { - let mut complaints = Vec::new(); - for (i, (file, _)) in rows.iter().enumerate() { - if rows[..i].iter().any(|(earlier, _)| earlier == file) { - complaints.push(format!("{file} has two rows in the arch reach list; one row per file")); - } - if !file.starts_with(ARCH_TREE) || !files.iter().any(|(at, _)| at == file) { - complaints.push(format!( - "the arch reach list names {file}, and this tree holds no such file under {ARCH_TREE}" - )); - } - } - for (at, text) in files.iter().filter(|(at, _)| at.starts_with(ARCH_TREE)) { - for (line, what) in arch_hands_up(text, module_depth(at), aliases) { - complaints.push(format!( - "{at}:{}: {what}, which hands the layer a name without its home: name each item where it lives", - line + 1 - )); - } - let named = arch_reach(text, module_depth(at), aliases); - let permitted: &[&str] = rows.iter().find(|(file, _)| file == at).map_or(&[], |(_, items)| items); - for (line, item) in &named { - if !permitted.contains(&item.as_str()) { - complaints.push(format!( - "{at}:{}: names `crate::{item}`, which is above the arch layer. Generic code \ - reaches the architecture and never the other way: pass in what it needs, \ - or move the code out of {ARCH_TREE}", - line + 1 - )); - } - } - for item in permitted { - if !named.iter().any(|(_, n)| n == item) { - complaints.push(format!( - "the arch reach list lets {at} name `crate::{item}`, and it no longer does: \ - delete the entry, the list only shrinks" - )); - } - } - } - complaints -} - /// The third-party C corpus, whose attribution is per *population* rather than /// per file: `tests/testcases/LICENSE` states how many files each of these /// directories holds, and `NOTICE` points at that file for the terms. @@ -1918,126 +1659,6 @@ fn digest(bytes: &[u8]) -> String { mod tests { use super::*; - /// **The arch layer names nothing above it**, but for [`ARCH_REACHES_UP`], - /// and that list names nothing its files no longer spell. - #[test] - fn the_arch_layer_names_nothing_above_it() { - let files = every_rust_file(); - let root = files - .iter() - .find(|(at, _)| at == KERNEL_ROOT) - .unwrap_or_else(|| panic!("the walk did not reach {KERNEL_ROOT}")); - let aliases = arch_aliases(&root.1); - assert!( - aliases.iter().any(|a| a == "hw"), - "{KERNEL_ROOT} re-exports `arch::hw` and the alias scan did not see it: {aliases:?}" - ); - let arch = files.iter().filter(|(at, _)| at.starts_with(ARCH_TREE)).count(); - assert!(arch > 40, "the walk found {arch} files under {ARCH_TREE}; it is reading no tree"); - let complaints = arch_reach_complaints(&files, ARCH_REACHES_UP, &aliases); - assert!(complaints.is_empty(), "{}", complaints.join("\n")); - } - - /// Teeth for the rule above: every spelling of a root item is read, and - /// what is not a path to one is not. - #[test] - fn every_spelling_of_a_crate_root_item_is_read() { - let hw = ["hw".to_string()]; - let names = |text: &str, depth: usize| -> Vec { - arch_reach(text, depth, &hw).into_iter().map(|(_, item)| item).collect() - }; - assert_eq!(names("use crate::sched::driver;\n", 3), ["sched"]); - assert_eq!(names(" crate::log!(\"x\");\n", 3), ["log"]); - assert_eq!(names("const { $crate::irq_census::TOTAL }\n", 3), ["irq_census"]); - assert_eq!(names("use crate::{\n arch::x,\n mm::{a, b},\n self,\n process,\n};\n", 3), ["mm", "process"]); - assert_eq!(names("use crate :: { * };\nuse crate::*;\n", 3), ["*", "*"]); - assert_eq!(names("use crate as k;\nuse crate::{self as j};\n", 3), ["self as", "self as"]); - // A `super::` chain that leaves the file's module names the root's item, - // counted from `self::` and through every inline module around it. - assert_eq!(names("use super::super::super::drivers::xhci;\n", 3), ["drivers"]); - assert_eq!(names("use super::super::{sched, mm};\n", 2), ["mm", "sched"]); - assert_eq!(names("use self::super::super::super::drivers;\n", 3), ["drivers"]); - assert_eq!(names("mod t {\n use super::super::super::super::sched;\n}\nmod u { use super::super::super::x; }\n", 3), ["sched"]); - // A char literal hides nothing after it. - assert_eq!(names("let q = '\"'; let r = '\\''; fn f<'a>(x: &'a u8) { crate::log!(); }\n", 3), ["log"]); - // Not code, not the root, not a path. - for text in [ - "// crate::sched is the scheduler\n", - "let s = \"crate::sched\";\n", - "pub(crate) fn f() {}\npub(super) fn g() {}\n", - "use mycrate::sched;\nuse other_crate::x;\n", - "use self::super::x;\nuse super::{apic, smp};\nuse super::super::percpu;\nuse crate::arch::paging::Prot;\n", - "let p = crate::hw::paging::Prot::R;\n", - ] { - assert_eq!(names(text, 3), Vec::::new(), "{text:?}"); - } - // Handing a name from above to the layer, however it is spelled. - let handed = |text: &str| -> Vec<(usize, String)> { arch_hands_up(text, 3, &hw) }; - let at = |line: usize, what: &str| (line, what.to_string()); - assert_eq!(handed("pub(crate) use crate::mm::{\n policy::Prot,\n Mm as M,\n};\n"), [at(0, "re-exports `crate::mm`")]); - assert_eq!(handed("pub use super::super::super::sched::X;\n"), [at(0, "re-exports `crate::sched`")]); - assert_eq!( - handed("pub type Hop = crate::invalidation::Origin;\nuse crate::mm::policy::Prot;\npub(super) type P =\n Prot;\n"), - [at(0, "declares a `pub type`"), at(2, "declares a `pub type`")] - ); - assert_eq!(handed("use crate::arch::paging::*;\nuse crate::hw::{x, *};\n"), [at(0, "globs `crate::arch`"), at(1, "globs `crate::arch`")]); - let own = "pub use super::cpu::outb;\npub use Cr3 as Root;\nuse crate::mm::X;\nuse super::*;\n\ - impl I for S { type Output = u64; }\npub(crate) use irq_took;\npub use toyos_bootmap::aarch64::MAIR;\n"; - assert_eq!(handed(own), []); - assert_eq!(module_depth("kernel/src/arch/mod.rs"), 1); - assert_eq!(module_depth("kernel/src/arch/x86_64/mod.rs"), 2); - assert_eq!(module_depth("kernel/src/arch/x86_64/tlb.rs"), 3); - assert_eq!(module_depth("kernel/src/arch/x86_64/idt/timer.rs"), 4); - assert_eq!( - arch_aliases("use arch::{cpu, percpu as p, smp};\npub(crate) use arch::hw;\nuse drivers::acpi;\n"), - ["cpu", "p", "smp", "hw"] - ); - } - - /// Teeth for the list: a new reach reds naming the file, the line and the - /// item; a listed one does not; a listed one the file stopped spelling reds, - /// and so does a row for a file outside the layer; a root alias of the - /// architecture is the architecture; and a name handed up reds whatever the - /// list says. - #[test] - fn a_new_reach_out_of_arch_is_red_and_the_list_cannot_rot() { - let file = |at: &str, text: &str| (at.to_string(), text.to_string()); - let mut files = vec![ - file("kernel/src/arch/x86_64/tlb.rs", "use crate::time::Duration;\nuse crate::drivers::xhci;\n"), - file("kernel/src/arch/x86_64/idt/timer.rs", "use crate::hw::HW;\nuse crate::arch::apic;\n"), - file("kernel/src/sched/driver.rs", "use crate::drivers::xhci;\n"), - ]; - let aliases = vec!["hw".to_string()]; - let said = arch_reach_complaints(&files, &[("kernel/src/arch/x86_64/tlb.rs", &["time"])], &aliases); - assert_eq!(said.len(), 1, "{said:?}"); - assert!(said[0].starts_with("kernel/src/arch/x86_64/tlb.rs:2: names `crate::drivers`"), "{said:?}"); - - let tlb = ("kernel/src/arch/x86_64/tlb.rs", &["drivers", "time"][..]); - let listed = arch_reach_complaints(&files, &[tlb], &aliases); - assert!(listed.is_empty(), "{listed:?}"); - - let stale = arch_reach_complaints( - &files, - &[ - ("kernel/src/arch/x86_64/tlb.rs", &["drivers", "time", "sched"]), - ("kernel/src/arch/gone.rs", &["log"]), - ("kernel/src/sched/driver.rs", &["drivers"]), - ], - &aliases, - ); - assert_eq!(stale.len(), 3, "{stale:?}"); - assert!(stale.iter().any(|s| s.contains("kernel/src/arch/gone.rs")), "{stale:?}"); - assert!(stale.iter().any(|s| s.contains("names kernel/src/sched/driver.rs")), "{stale:?}"); - assert!(stale.iter().any(|s| s.contains("`crate::sched`") && s.contains("no longer")), "{stale:?}"); - - files.push(file("kernel/src/arch/x86_64/paging.rs", "pub use crate::mm::policy::Prot;\n")); - files.push(file("kernel/src/arch/x86_64/rtc.rs", "use crate::arch::paging::*;\n")); - let handed = arch_reach_complaints(&files, &[tlb, ("kernel/src/arch/x86_64/paging.rs", &["mm"])], &aliases); - assert_eq!(handed.len(), 2, "{handed:?}"); - assert!(handed[0].starts_with("kernel/src/arch/x86_64/paging.rs:1: re-exports `crate::mm`, which hands"), "{handed:?}"); - assert!(handed[1].starts_with("kernel/src/arch/x86_64/rtc.rs:1: globs `crate::arch`, which hands"), "{handed:?}"); - } - /// **The architecture rules hold over the whole repository**, and no place /// a rule names is a place that no longer holds a needle — a row nobody /// needs any more is a permission nobody re-argued. From 06b926b11254be5722d18f1b4ac291fc031c097c Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 22:26:12 +0200 Subject: [PATCH 11/12] The arch-rules scan is main's again, and toyos-net-wire describes itself spelled_paths, Code and Spelled generalised arch_module_lines over any root with segments and aliases so the arch upward-import gate could resolve `crate::` and `super::` paths; code_only's char-literal branch was there for the same scan over kernel sources. With that gate deleted, the one caller is arch_module_lines over `core` and `std`, which main's version already reads, so src/sourcegate.rs is main's but for the sysroot::tracked_files calls. toyos-net-wire joined the host workspace on main without a [package] description, which hostws's description gate refuses. Co-Authored-By: Claude Opus 5.5 --- src/sourcegate.rs | 266 +++++++++++++++----------------------- toyos-net-wire/Cargo.toml | 1 + 2 files changed, 108 insertions(+), 159 deletions(-) diff --git a/src/sourcegate.rs b/src/sourcegate.rs index ddddf4f52ef..f328ac9ed43 100644 --- a/src/sourcegate.rs +++ b/src/sourcegate.rs @@ -345,7 +345,7 @@ const RETIRED_ABI_NAMES: &[&str] = &[ const GUEST_TREES: &[&str] = &["kernel/src", "toyos/src", "toyos-abi/src", "userland", "tests"]; -/// `line` with its comment and its string and char literals removed. +/// `line` with its comment and its string literals removed. /// /// What is left is the part that names things. Prose explaining what a deleted /// call used to do is legal and worth keeping; a gravestone table mapping a @@ -361,21 +361,6 @@ fn code_only(line: &str) -> String { } '"' => in_string = !in_string, '/' if !in_string && chars.peek() == Some(&'/') => break, - // A char literal is a literal (`'"'` opens no string); a lifetime or a label is code. - '\'' if !in_string => { - let mut ahead = chars.clone(); - match (ahead.next(), ahead.next()) { - (Some('\\'), _) => { - chars.next(); - chars.next(); - while chars.next().is_some_and(|c| c != '\'') {} - } - (Some(_), Some('\'')) => { - chars.nth(1); - } - _ => out.push(c), - } - } _ if !in_string => out.push(c), _ => {} } @@ -1273,168 +1258,131 @@ const ASSEMBLY: &[&str] = &["asm!", "global_asm!", "naked_asm!", "#[naked]", "un #[cfg(test)] const ARCH_MODULE: &str = "core::arch/std::arch"; -/// A path code spells from a root: the 0-based line of its last segment, the -/// segments after the root, and the name a rename binds. A group is one path -/// per element, a glob ends in `*`, and a rename of the root itself (`core as -/// k`, `core::{self as k}`) is `self` renamed. -#[cfg(test)] -struct Spelled { - line: usize, - segments: Vec, - alias: Option, -} - -/// Every path in `text` from one of `roots` (`core`, `std`, which `$crate` -/// also spells for `crate`). The root's rename counts only where it imports: -/// `use core as k;`, `extern crate core as k;`, an element of a `use` group. A -/// glob or a rename is not followed to what it brings into scope: that is -/// resolution, which a scan does not have. +/// The 0-based lines of `text` on which a path names `core::arch` or +/// `std::arch`, or makes a name that can: `core::arch::asm!`, +/// `use core::arch as isa;`, an `arch` that begins an element of a `core::{…}` +/// group, over any number of lines — and any glob of `core`/`std` itself +/// (`use core::*;`, `core::{*}`) or rename of it (`use core as k;`, +/// `extern crate core as k;`, `core::{self as k}`). +/// +/// **The glob and the rename are refused whole**, not followed to an `arch` +/// after them: what they bring into scope is read by resolution, which a line +/// scan does not have, and code outside an arch module has no other use for +/// either. Comments and string literals are not code ([`code_only`]). #[cfg(test)] -fn spelled_paths(text: &str, roots: &[&str]) -> Vec { - let code = Code(text.lines().map(code_only).collect::>().join("\n").chars().collect()); - let mut found = Vec::new(); - let mut at = 0; - while at < code.0.len() { - let Some((name, end)) = code.ident_at(at) else { +fn arch_module_lines(text: &str) -> Vec { + let code: Vec = text.lines().map(code_only).collect::>().join("\n").chars().collect(); + let word = |c: char| c.is_ascii_alphanumeric() || c == '_'; + let line_of = |at: usize| code[..at].iter().filter(|c| **c == '\n').count(); + let skip_space = |mut at: usize| { + while code.get(at).is_some_and(|c| c.is_whitespace()) { at += 1; - continue; - }; - if roots.contains(&name.as_str()) { - let after = code.skip_space(end); - if code.colons(after) { - code.tree(after + 2, Vec::new(), &mut found); - } else if code.imported(at) { - let alias = code.alias_at(after); - let segments = vec!["self".to_string()]; - found.extend(alias.is_some().then(|| Spelled { line: code.line_of(at), segments, alias })); - } } - at = end; - } - found -} - -/// Code as characters, its comments and literals gone ([`code_only`]). -#[cfg(test)] -struct Code(Vec); - -#[cfg(test)] -impl Code { - /// Every path the use tree at `at` spells after `prefix`, into `out`. - fn tree(&self, at: usize, prefix: Vec, out: &mut Vec) { - let at = self.skip_space(at); - let line = self.line_of(at); - if self.0.get(at) == Some(&'*') { - out.push(Spelled { line, segments: [prefix, vec!["*".to_string()]].concat(), alias: None }); - } else if self.0.get(at) == Some(&'{') { - // Each element begins after the `{` or after a `,` at depth one. - self.tree(at + 1, prefix.clone(), out); - let (mut depth, mut i) = (0usize, at + 1); - while let Some(&c) = self.0.get(i) { - match c { - '{' => depth += 1, - '}' if depth == 0 => break, - '}' => depth -= 1, - ',' if depth == 0 => self.tree(i + 1, prefix.clone(), out), - _ => {} - } - i += 1; - } - } else if let Some((name, end)) = self.ident_at(at) { - let (after, segments) = (self.skip_space(end), [prefix, vec![name]].concat()); - match self.colons(after) { - true => self.tree(after + 2, segments, out), - false => out.push(Spelled { line, segments, alias: self.alias_at(after) }), - } + at + }; + let ident_at = |at: usize, name: &str| { + let end = at + name.len(); + code.get(at..end).is_some_and(|s| s.iter().copied().eq(name.chars())) + && !code.get(end).is_some_and(|c| word(*c)) + && !at.checked_sub(1).and_then(|j| code.get(j)).is_some_and(|c| word(*c)) + }; + // The identifier that ends before `at`, over whitespace and `::`. + let ident_before = |at: usize| { + let mut end = at; + while end > 0 && (code[end - 1].is_whitespace() || code[end - 1] == ':') { + end -= 1; } - } - - /// The name bound by an `as` at `at`. - fn alias_at(&self, at: usize) -> Option { - let (_, end) = self.ident_at(at).filter(|(word, _)| word == "as")?; - self.ident_at(self.skip_space(end)).map(|(name, _)| name) - } - - fn line_of(&self, at: usize) -> usize { - self.0[..at].iter().filter(|c| **c == '\n').count() - } - - fn skip_space(&self, at: usize) -> usize { - (at..).find(|&j| !self.0.get(j).is_some_and(|c| c.is_whitespace())).unwrap_or(at) - } - - fn colons(&self, at: usize) -> bool { - self.0.get(at..at + 2) == Some(&[':', ':'][..]) - } - - fn word(&self, at: usize) -> bool { - self.0.get(at).is_some_and(|c| c.is_ascii_alphanumeric() || *c == '_') - } - - /// The identifier that begins at `at`, and where it ends. - fn ident_at(&self, at: usize) -> Option<(String, usize)> { - if at.checked_sub(1).is_some_and(|j| self.word(j)) { - return None; + let mut start = end; + while start > 0 && word(code[start - 1]) { + start -= 1; } - let end = (at..).find(|&j| !self.word(j)).unwrap_or(at); - (end > at).then(|| (self.0[at..end].iter().collect(), end)) - } - - /// The identifier that ends before `at`, over whitespace and `::`, and where it begins. - fn ident_before(&self, at: usize) -> (usize, String) { - let end = (0..at).rev().find(|&j| !self.0[j].is_whitespace() && self.0[j] != ':').map_or(0, |j| j + 1); - let start = (0..end).rev().find(|&j| !self.word(j)).map_or(0, |j| j + 1); - (start, self.0[start..end].iter().collect()) - } - - /// Whether `at` begins an item that imports: `use …`, or `extern crate …`, - /// or an element of a `use` group. - fn imported(&self, mut at: usize) -> bool { + (start, code[start..end].iter().collect::()) + }; + // Whether `at` begins an item that imports: `use …`, or `extern crate …`, + // or an element of a `use` group. + let imported = |at: usize| { + let mut at = at; loop { - let (start, ident) = self.ident_before(at); + let (start, ident) = ident_before(at); match ident.as_str() { "use" => return true, - "crate" => return self.ident_before(start).1 == "extern", + "crate" => return ident_before(start).1 == "extern", "" => {} _ => return false, } // Not after an identifier: inside a group only if a `{` opens it. - let back = (0..start).rev().find(|&j| !self.0[j].is_whitespace()); - match back.map(|j| (j, self.0[j])) { - Some((j, '{')) => at = j, - Some((j, ',')) => { + let mut back = start; + while back > 0 && code[back - 1].is_whitespace() { + back -= 1; + } + match back.checked_sub(1).map(|j| code[j]) { + Some('{') => at = back - 1, + Some(',') => { let mut depth = 0usize; - let open = (0..j).rev().find(|&k| match self.0[k] { - '}' => { depth += 1; false } - '{' if depth == 0 => true, - '{' => { depth -= 1; false } - _ => false, - }); - match open { - Some(k) if !self.0[k..j].contains(&';') => at = k, - _ => return false, + let mut j = back - 1; + loop { + let Some(k) = j.checked_sub(1) else { return false }; + j = k; + match code[j] { + '}' => depth += 1, + '{' if depth == 0 => break, + '{' => depth -= 1, + ';' => return false, + _ => {} + } } + at = j; } _ => return false, } } + }; + let mut lines = Vec::new(); + for at in 0..code.len() { + let Some(root) = ["core", "std"].into_iter().find(|root| ident_at(at, root)) else { continue }; + let after = skip_space(at + root.len()); + if ident_at(after, "as") && imported(at) { + lines.push(line_of(at)); + continue; + } + let colons = after; + if code.get(colons..colons + 2) != Some(&[':', ':'][..]) { + continue; + } + let next = skip_space(colons + 2); + if ident_at(next, "arch") || code.get(next) == Some(&'*') { + lines.push(line_of(next)); + } else if code.get(next) == Some(&'{') { + // Each element of the group begins after its `{` or a `,` at depth one. + let (mut depth, mut i, mut element) = (0usize, next, true); + while let Some(&c) = code.get(i) { + match c { + '{' => { + depth += 1; + element = depth == 1; + } + '}' => { + depth -= 1; + if depth == 0 { + break; + } + } + ',' if depth == 1 => element = true, + c if c.is_whitespace() => {} + _ => { + let renamed_self = + ident_at(i, "self") && ident_at(skip_space(i + "self".len()), "as"); + if element && depth == 1 && (ident_at(i, "arch") || c == '*' || renamed_self) { + lines.push(line_of(i)); + } + element = false; + } + } + i += 1; + } + } } -} - -/// The 0-based lines of `text` on which a path names `core::arch` or -/// `std::arch`, or makes a name that can: `core::arch::asm!`, -/// `use core::arch as isa;`, an `arch` that begins an element of a `core::{…}` -/// group, over any number of lines — and any glob of `core`/`std` itself -/// (`use core::*;`, `core::{*}`) or rename of it (`use core as k;`, -/// `extern crate core as k;`, `core::{self as k}`), refused whole: code outside -/// an arch module has no other use for either. -#[cfg(test)] -fn arch_module_lines(text: &str) -> Vec { - spelled_paths(text, &["core", "std"]) - .into_iter() - .filter(|p| matches!(p.segments[0].as_str(), "arch" | "*") || (p.segments[0] == "self" && p.alias.is_some())) - .map(|p| p.line) - .collect() + lines } /// The spelling of the first needle of `rule` that line `n` of a file holds, diff --git a/toyos-net-wire/Cargo.toml b/toyos-net-wire/Cargo.toml index 5deac74c9f8..84159312a08 100644 --- a/toyos-net-wire/Cargo.toml +++ b/toyos-net-wire/Cargo.toml @@ -2,5 +2,6 @@ name = "toyos-net-wire" version = "0.1.0" edition = "2021" +description = "Ethernet, ARP, IPv4, ICMPv4, IGMP, UDP and TCP wire formats, parsed in place and built (no_std, forbid(unsafe_code))." license = "MIT OR Apache-2.0" publish = false From b1acbe9ece7bbded187e30193de6fcee0edd41ab Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 23:27:46 +0200 Subject: [PATCH 12/12] toyos-transport: its one-line description, as every host crate has Co-Authored-By: Claude Opus 5.5 --- toyos-transport/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/toyos-transport/Cargo.toml b/toyos-transport/Cargo.toml index cd1fbf10a09..2b7ec21bd63 100644 --- a/toyos-transport/Cargo.toml +++ b/toyos-transport/Cargo.toml @@ -10,6 +10,7 @@ name = "toyos-transport" version = "0.1.0" edition = "2021" +description = "The transport every client/server session runs over: rings, an arena of runs and a tag table, decided over words an adapter hands in." license = "MIT OR Apache-2.0" publish = false