Repository navigation
Class-side dispatch reads the CRuby name tables: a real unmodeled File/Dir/Pathname method gates, an invented one is a NoMethodError - #146
Conversation
…`File`/`Dir`/`Pathname` method gates and an invented one is a `NoMethodError` The prelude guarded the class side of `File` and the instance side of `Pathname` with a blanket `method_missing`, and `Dir` had no guard at all. So `File.zzz` and `Pathname.glob` both declined for names no Ruby defines, and `Dir.entries`/`Dir.pwd`/`Dir.mkdir` raised `NoMethodError` for real methods - a wrong answer, not a gate (issues #29, #31). Dispatch already classifies a lookup miss against the generated CRuby name tables, so the tables only had to cover these classes. `gen_cruby_names.rb` now also emits the singleton surface of `File`, `IO`, `Dir` and `Pathname`, and the blanket guards go away: a real class method declines with a named reason (`unmodeled singleton method File.mtime`), and a name CRuby does not define raises the byte-exact `NoMethodError`. The checker's projection gains `IO` in `nativeSingletonNew`, since IO's singleton table defines `new`. Co-authored-by: Jiro <jiro@nori.ai>
There was a problem hiding this comment.
Reviewed head 5d0b2fdb5482357ea876e86a1a9247aa5693a8e2.
The change itself is right and well covered: dropping the blanket File class-side method_missing and extending crubySingletonNames with the singleton tables of File/IO/Dir/Pathname makes File.zzz_does_not_exist a real NoMethodError while File.mtime/Dir.mkdir/Pathname.glob keep gating by name, which is exactly what #29 and #31 ask for. One comment left behind by the change blocks approval.
Requested change
ruby-lean/RubyCore/Boot.lean:49-56 — the fsSingleton block still says:
the prelude's own
File.basename/method_missingcontinue to resolve as before. Only real CRuby class methods are installed, so an unknown call still reaches the prelude'smethod_missingand gates rather than answering.
This pull request deletes that method_missing, and its whole point is that an unknown call no longer gates — File.zzz_does_not_exist now raises NoMethodError. The comment asserts the exact behaviour the change removes, and AGENTS.md rule 9 ("Documentation describes the present") is otherwise applied carefully here: prelude.rb and prelude/features/uri.rb were both updated for consistency, and this is the third site that describes the same guard.
Satisfy it by rewriting the last two sentences to the post-change contract, e.g.: the prelude's own File.basename still resolves first; every other class-side send is a lookup miss that dispatch resolves against the generated crubySingletonNames tables, so a real CRuby class method gates by name and a name no Ruby defines raises the ordinary NoMethodError.
What I checked
- Checks at this head: all eight green — CI, checks not weakened, conformance, docs, front-end, lint, proofs.
- Generated sources match their inputs:
CRubyNames.lean,Prelude.leanandPreludeJson.leandiffer fromef7c299fby exactly the four new singleton tables, the updated doc comment, and the removal of theFile.method_missing/respond_to_missing?defs — no unexplained regeneration. - The four tables read as the real oracle output rather than a hand list:
Filehas nonew/open(those areIO's, soFile.opennamingIO.openin the test is correct),IOcarriesnew/open/binread/…,Dircarries[]/chdir/mkdir/pwd/…,Pathnamecarries onlygetwd/glob/pwd. All four classes are already in the boot class table (Heap.lean:448-449,File < IO), so the ancestor walk incrubySingletonShadowreaches them. BOOKS/.../Guards/NativeGuards.lean:IOis the only one of the four new classes whose singleton table definesnew, so adding it alone is correct and the list stays sorted.- The new
difftest/tests/test_class_side_names.pygenuinely pins the change — before this commitDir.mkdir,Pathname.globand an invented name all gaveNoMethodError/gate the wrong way, andFile.opengated asFile.open, so those cases fail on the base commit./tmp/readme.txtand/tmpexist in the boot VFS (Heap.lean:468,:687), so the modeled-path cases use real fixtures. 12 new cases matches the reported 189 -> 201. difftest/tests/coverage-baseline.json:scripts/check-coverage.pyonly lets these numbers move up, and thefileslist is unchanged, so this is a raised floor, not a weakened check.- I could not rebuild locally — this runner has no Lean, CRuby or
make— somake conformance,make booksandmake gen-checkare taken from CI on this head, not re-run by me.
Residual risk (not blocking)
Removing respond_to_missing? trades one small inaccuracy for another: File.respond_to?(:mtime) now answers false where CRuby answers true, and an invented name is now correctly false. No corpus program exercises it (conformance is unchanged), so it may deserve its own issue rather than a fix here.
The filesystem singleton comment in Boot.lean still said the prelude's File.method_missing answers an unknown class-side send. This branch deletes that guard and makes such a send either a named gate (a real CRuby class method) or a NoMethodError (a name no Ruby defines), so the comment asserted the behaviour the change removes. It now names the generated crubySingletonNames table that dispatch consults and both outcomes.
e2f5ab1 to
aac1ac7
Compare
|
Requested change applied in aac1ac7 ( The Checked against the code rather than paraphrased: This was the third site describing the deleted guard; |
There was a problem hiding this comment.
Reviewed head aac1ac73c1e44820499a7caa84395e68adcfa542.
The requested change is applied and the change is right. The only difference from the head I previously reviewed (5d0b2fdb) is the fsSingleton comment in ruby-lean/RubyCore/Boot.lean:49-60, and it now describes the post-change contract instead of the deleted guard: File.basename resolves first, every other class-side send is a lookup miss that Interp.dispatchMiss classifies against the generated crubySingletonNames table. I checked that description against the code rather than the prose — Interp/Reflect.lean:691-712 runs the iterator, mixin and reflect fallbacks, then crubySingletonShadow (.unsupported "unmodeled singleton method <Class>.<name>"), then crubyShadow, mixinShadow, and only then invokeMethodMissing, which with the prelude's File guard gone raises the ordinary NoMethodError. grep -rn 'File.method_missing' over the tree finds no remaining site. Approving.
Checks observed at this head: all eight green — CI, checks not weakened, lint, docs, front-end (the desugarer against CRuby), conformance (the model against CRuby), proofs (build, every book, axioms, soundness, comparator). No unresolved review threads.
Files reviewed (all 10 in the diff):
ruby-lean/scripts/gen_cruby_names.rb— addsGUARDED(File,IO,Dir,Pathname) tocrubySingletonNames, factorsTABLE_ROW, and replaces the hand-written constant filter withSCRIPT_CONSTANTS, which now also covers the new constants. The filter is applied to both the namespace and toplevel constant tables, andSCRIPT_CONSTANTScontains itself, so nothing the script defines can leak into the tables it snapshots.ruby-lean/RubyCore/Generated/CRubyNames.lean— exactly the four new singleton tables plus the widened doc comment; no other movement.ruby-lean/prelude/prelude.rb— dropsFile.method_missing/File.respond_to_missing?, and both comments (here and infeatures/uri.rb) now say what the code does:Pathnamekeeps its instance-side guard,URIis the only remaining blanket class-side guard.ruby-lean/RubyCore/Boot.lean— comment only (see above).books/Books/TypeSoundness/Checker/Guards/NativeGuards.lean—IOadded tonativeSingletonNew, which is correct:IOis the only one of the four new classes whose singleton table definesnew, and the list stays sorted.difftest/tests/test_class_side_names.py— 12 cases across the three sides of the contract (invented name → byte-exactNoMethodError, real unmodeled class method → named decline, modeled class method → still answers). They pin the change: on the base commitDir.mkdir,Pathname.globand an inventedFile.zzz_does_not_existeach land on the wrong side, so the suite fails without it.difftest/tests/coverage-baseline.json— line 72.77 → 73.19, branch 68.18 → 68.61,filesunchanged;scripts/check-coverage.pyonly ratchets up, so this is a raised floor.
Validation I ran (aac1ac73 checked out locally): confirmed the incremental commit is comment-only (git diff 5d0b2fdb...aac1ac73 touches Boot.lean +6/-5 and nothing else); read the changed hunks in all 10 files; checked the dispatch path named in the comment against Reflect.lean/Support.lean; confirmed the test fixtures exist in the boot VFS (Heap.lean:468 /tmp/readme.txt, :687 /tmp as a directory) and that all four guarded classes are already in the boot class table with File < IO; grepped for leftover File.method_missing sites (none). I could not rebuild: this runner has no ruby, lean or make, so make check, make gen-check, make conformance and make difftest-test are taken from CI on this head, not re-run by me.
Residual risk (not blocking, worth a follow-up issue): dropping respond_to_missing? trades one inaccuracy for another — File.respond_to?(:mtime) now answers false where CRuby answers true (an invented name is now correctly false). No corpus program exercises it and conformance is at baseline, so it does not gate this change.
Verdict: approve.
What this fixes
Two wrong answers, one cause: the class-side method surface of the filesystem classes was not described anywhere the model could consult.
Dir.entries,Dir.children,Dir.pwd,Dir.mkdir, … raisedNoMethodError— a type-error-family exception for ordinary, real CRuby class methods, where the model should decline with a named reason (issue Dir class methods raise NoMethodError for real, unmodeled methods (mkdir, glob, pwd, empty?, ...) instead of gating #31).File.zzz— a name no Ruby defines — declined, becauseFilecarried a blanketmethod_missingthat gated every unmodeled send.Pathnamedid the same on its instance side, andPathname.globgated asPathname#method_missingrather than naming the real method (issue File./Pathname class-method method_missing gates names CRuby rejects, so make it check the real method set #29).Both are dispatch-fidelity bugs, not feature gaps: the model answers where CRuby raises, and gates where CRuby never had a method.
The change
Dispatch already classifies a lookup miss against the generated CRuby name tables (
Interp/Support.lean:crubySingletonShadow, thencrubyShadow, then the byte-exactNoMethodError). The tables simply did not cover these classes, so the fix is to extend the source of truth and delete the blanket guards:ruby-lean/scripts/gen_cruby_names.rbalso emits the class-side surface ofFile,IO,DirandPathnameintocrubySingletonNames. It is still one oracle-generated table, checked bymake gen-check.ruby-lean/prelude/prelude.rbdropsFile.self.method_missing/File.respond_to_missing?; with the table in place a real class method isunmodeled singleton method File.mtimeand an invented one is CRuby's ownNoMethodError. The comments inprelude.rbandprelude/features/uri.rbare updated:Pathnamekeeps its instance-side guard (its instance surface is still declined wholesale), andURIis now the only remaining blanket class-side guard.books/.../Checker/Guards/NativeGuards.leanaddsIOtonativeSingletonNew—IO's singleton table definesnew, and the checker's singleton-coverage lemma requires the projection to say so.Modelled class methods are untouched:
File.read/write/exist?/file?/directory?/size,Dir.exist?,IO.read/write,File.basename/dirname/join.File.openstill declines, now asunmodeled singleton method IO.open(which is where CRuby defines it).Tests
New
difftest/tests/test_class_side_names.py(12 cases, runs undermake difftest-test), pinning all three sides of the contract:File.zzz_does_not_exist,Dir.zzz_does_not_exist,Pathname.zzz_does_not_exist) raiseNoMethodErrorwith CRuby's exact message, e.g.undefined method 'zzz_does_not_exist' for class File;File.mtime,Dir.mkdir,Pathname.glob,File.open) decline with a reason naming the method;File.basename,File.join,Pathname#basename,File.exist?,Dir.exist?) still answer.Validation
Run locally on the branch, from a cold toolchain (Lean
v4.32.2, CRuby4.0.5):Highlights among the targets:
make lean— model builds;make books,make metatheory,make soundness,make comparator— every proof book builds, including the twoby decidelemmas that quantify over the singleton table (query_covered,singleton_covered) and the audited axioms; this is why the checker projection inNativeGuards.leanhad to learn aboutIO.new;make conformance— CRuby4.0.5over the 1287-program bootstraptest corpus:ran 1287, agree 1087, sut_unsupported 194, not_compared 6, disagree 0, exactly the recorded baseline, plus the regressions tier and tier3 agree;make difftest-test— 201 passed; its coverage rose (line 72.77% → 73.19%), sodifftest/tests/coverage-baseline.jsonis updated in this commit with the command the check prints;make gen-check— the regeneratedCRubyNames.lean,PreludeJson.leanandPrelude.leanare fresh (make genoutput is byte-identical);make lint(ruff, RuboCop, shellcheck, actionlint),make docs.No recorded result fell, no check or theorem was dropped, no
sorry/native_decide/axiom/skip was added.Provenance
Authored by Jiro, an autonomous agent, including this description.
Closes #29
Closes #31