Skip to content

VFS step 1: a trailing slash or . after a non-directory is not that file - #42

Merged
sam-xif merged 1 commit into
mainfrom
nori/vfs-trailing-slash-fix
Oct 10, 2026
Merged

sam-xif merged 1 commit into
mainfrom
nori/vfs-trailing-slash-fix

Conversation

@sam-xif

@sam-xif sam-xif commented Oct 5, 2026

Copy link
Copy Markdown
Owner

Summary

This PR fixes a wrong-answer bug in step 1 of issue #7 (the symbolic virtual filesystem, PR #21), found by running the step-1 surface against CRuby 4.0.5 with the same fixture materialised.

VFS.components dropped empty and . components, so /tmp/readme.txt/ and /tmp/readme.txt/. produced the same components as /tmp/readme.txt, and VFS.resolve returned the regular file for all three. CRuby requires the endpoint of a trailing slash — and the object . stands for — to be a directory:

Program CRuby model before model after
File.exist?("/tmp/readme.txt/") false true false
File.file?("/tmp/readme.txt/") false true false
File.directory?("/tmp/readme.txt/") false false false
File.read("/tmp/readme.txt/") raises Errno::ENOTDIR "hello\n" named Unsupported
File.write("/tmp/readme.txt/", "x") raises Errno::EISDIR silently truncates the file named Unsupported
File.exist?("/tmp/readme.txt/.") false true false

true/"hello\n" where CRuby says false/raises is exactly the wrong answer rather than a gate that issue #7's File/Dir guards exist to prevent (the predicate answers are modeled and must be byte-exact; the read/write errors are Errno classes, which are step 7, so they correctly fall through to a named Unsupported).

What changed

Two changes in VFS (ruby-lean/RubyCore/Heap.lean), neither changing an interface — lookup stays Option ObjId and the walk stays pure and total:

  • VFS.trailingSlash (path.length > 1 && path.endsWith "/") recovers the one signal components discarded. resolve now requires the endpoint to carry a .dir payload when that signal is set, else returns none. /, // and interior // are unaffected (the root is a directory).
  • components keeps ., and walk handles it explicitly (like ..) by requiring the current object to be a directory before staying put. This catches /tmp/readme.txt/., which CRuby also rejects.

A new entry in ruby-lean/notes/model/implementation-notes.md records the bug, the fix and the evidence, and corrects the step-1 note that had summarised these as accepted "trailing-slash forms".

Verification

CRuby 4.0.5 vs the model, same fixture (/tmp/readme.txt = "hello\n", /tmp/vfsdata/nums.txt = "1\n2\n3\n") materialised for both:

  • A 27-form path sweep (including /, //, interior //, .., ., leading/trailing slashes) over File.exist?/file?/directory? now agrees with CRuby on every line.
  • File.read on the same sweep is byte-exact wherever CRuby answers and a named Unsupported wherever CRuby raises a step-7 Errno (no wrong answers remain).
  • run_typed_ratchet.sh is GREEN: 256 agree, 0 disagree, 125/125 clinks, 131/263 rungs — unchanged from the base branch.

Base

Stacked on #21 (VFS step 1); base branch is nori/vfs-step1-file-read-write, so the diff is this fix only. Should be merged after (or with) #21.


This code and this description were authored by Nori, an autonomous agent.

@sam-xif
sam-xif force-pushed the nori/vfs-step1-file-read-write branch from 2da15f8 to 1fc30e4 Compare October 10, 2026 04:04
@sam-xif
sam-xif force-pushed the nori/vfs-trailing-slash-fix branch from 87906da to 9d742db Compare October 10, 2026 04:04
@sam-xif
sam-xif changed the base branch from nori/vfs-step1-file-read-write to main October 10, 2026 14:53
… file

`VFS.components` dropped empty and `.` components, so `/tmp/readme.txt/` and
`/tmp/readme.txt/.` had the same components as `/tmp/readme.txt` and `resolve`
returned the regular file. CRuby requires the endpoint of a trailing slash, and
the object `.` stands for, to be a directory: it answers `false` to
`File.exist?`/`file?`/`directory?`, raises `Errno::ENOTDIR` from `File.read` and
`Errno::EISDIR` from `File.write`. The model instead answered `true`/`true`/
`false`, read the file's bytes, and let `File.write` silently truncate it — a
wrong answer rather than a gate, the failure mode issue #7's `File`/`Dir` guards
exist to prevent.

- `VFS.trailingSlash` (`path.length > 1 && path.endsWith "/"`) recovers the one
  signal `components` discarded; `resolve` now requires the endpoint to carry a
  `.dir` payload when it is set, else returns `none`. `/`, `//` and interior
  `//` are unaffected.
- `components` keeps `.`; `walk` handles it explicitly (like `..`) by requiring
  the current object to be a directory before staying put, so
  `/tmp/readme.txt/.` fails as CRuby's does.

The predicates now answer `false` and `File.read`/`write`/`open` fall through to
their existing `VFS.lookup = none` named `Unsupported` (the `Errno` classes and
message fidelity are step 7). `lookup` is still `Option ObjId` and the walk is
still pure and total; no interface changed.

Verified against CRuby 4.0.5 with the same fixture materialised: a 27-form path
sweep over `exist?`/`file?`/`directory?` agrees on every line, and `File.read`
is byte-exact where CRuby answers and a named gate where CRuby raises a step-7
`Errno`. `run_typed_ratchet.sh` is still GREEN (256 agree, 0 disagree, 125/125
clinks, 131/263 rungs).

Authored by Nori (agent).
@sam-xif
sam-xif force-pushed the nori/vfs-trailing-slash-fix branch from 9d742db to 67d1d23 Compare October 10, 2026 14:58
@sam-xif
sam-xif merged commit 6439ee9 into main Oct 10, 2026
4 checks passed
jiro-by-nori Bot added a commit that referenced this pull request Nov 10, 2026
Nothing exercised the model's filesystem against CRuby (issue #43). The model's
files are boot-heap data, so a program that reads `/tmp/readme.txt` answers from
the fixture on the model side and from whatever the host has on CRuby's side:
the two were never the same program, and the whole VFS surface was validated by
hand. The trailing-slash bug fixed in #42 is the kind of defect a corpus of
these would have caught.

A case now names its tree in its sidecar (`{"vfs": "v1"}`); `difftest/vfs.py`
describes the tree the model boots with and materializes it before the case and
restores it afterwards. Every CRuby run resets the tree first, so a program that
writes still starts both runs of the determinism check from the same bytes, and
the SUT is handed the tree the control started from rather than the control's
last write. Materializing refuses if a fixture path already exists and restores
everything it created, so the host's `/tmp` is never overwritten or departed
from.

The corpus is tier 5 (`difftest/corpus/vfs/`), run by `make conformance` with
`difftest run --tier vfs --sut lean`. A gate is allowed there — the model's
filesystem is built one step at a time, so a descriptor program gating is a
coverage measurement, not a pass — but a disagreement, a control failure or an
unusable fixture is not, and this tier owns that verdict rather than leaving it
to a baseline. The first five programs pin the fixture's bytes and sizes, path
resolution (`.`, `..`, `//`, trailing slash), a write read back, and the
descriptor form that is still gated.

Validation: `difftest run --tier vfs --sut lean` agrees on four programs and
gates the descriptor one; `--sut identity` agrees on all five, which is the
harness's own smoke test; the tier exits 1 on a fabricated disagreement and on
an occupied fixture path, and leaves `/tmp` as it found it. `replay corpus/tier3`
(70 programs) and `run --tier regressions` (222 programs) are unchanged: no
disagreement, 221 held and one gated. `difftest/tests` passes in CI mode (204
passed, 0 skipped, including 15 new tests), `ruff check` and `ruff format
--check` are clean, `mkdocs build --strict` passes, the difftest coverage
baseline moves up (72.77/68.18 -> 73.27/68.67) and `make check-not-weakened`
reports CHECKS NOT WEAKENED. The full `make conformance` target is left to CI:
this machine has no C toolchain, so `make deps` (bundler and the pinned gems)
cannot run here.

Authored by Jiro, an autonomous agent (Nori sessions), on behalf of @sam-xif.

Co-authored-by: Nori <contact@tilework.tech>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants