Skip to content

Pathname's instance surface is the CRuby name table, not a blanket method_missing (fixes #147) - #149

Open
jiro-by-nori[bot] wants to merge 3 commits into
mainfrom
nori/fix-147-pathname-instance-surface
Open

jiro-by-nori[bot] wants to merge 3 commits into
mainfrom
nori/fix-147-pathname-instance-surface

Conversation

@jiro-by-nori

@jiro-by-nori jiro-by-nori Bot commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

Pathname instances answer every unmodeled selector with a named Unsupported — including selectors no Ruby defines — because the prelude carries a blanket method_missing plus a respond_to_missing? that answers true for anything. An invented name therefore looked like an honest gap instead of the NoMethodError CRuby raises, a real unmodeled method (realpath, stat, …) lost its own name behind one generic reason, and respond_to? was a wrong value. This is the instance-side twin of #29 (class side, PR #146) and the sibling of #27 (File/IO instance methods, fixed in #28).

Fixes #147.

What changes

Pathname is a prelude class, but CRuby ships it as part of the language, so its surface is now generated like a builtin's:

  • scripts/gen_cruby_names.rb lists Pathname in FOLD, so crubyMethodNames, crubySingletonNames and crubyNamespaceConstants carry its real selectors. It also loads pathname.rb: the booted pathname.so answers require "pathname", so a plain require would be a no-op and the snapshot would miss the methods the .rb half adds (find, rmtree, Pathname.mktmpdir). Those are real methods a program that required pathname can call, so they must gate, not raise.
  • prelude/prelude.rb: method_missing and respond_to_missing? are deleted; a miss is an ordinary lookup miss and dispatchMiss classifies it (a real name declines as unmodeled method Pathname#…, an invented one raises CRuby's byte-exact NoMethodError).
  • Pathname#path is protected in CRuby, so it is modeled as protected: p.path raises the protected-method NoMethodError, p.send(:path) answers.
  • include Comparable is removed: CRuby's Pathname is not Comparable, so <, > and between? are NoMethodError rather than answers.
  • The checker's copied projection grows (the conservative direction): nativeInstanceNames gains the Pathname selectors, nativeQueryRows gains Pathname's ===/to_s (required by query_covered), and instance_covered needs maxRecDepth 4000 to walk the larger list.
  • New regression case difftest/corpus/regressions/pathname-instance-surface pins the whole surface; docs/testing.md's reproducer count goes 219 → 224 (it was already stale by four).

Behaviour, model vs CRuby 4.0.5

to_s: "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/a/b"                               inspect: "#<Pathname:/a/b>"
basename: "b"                              dirname: "/a"
extname: ".rb"                             sub: "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/a/z"
eq: true                                   eql: true
spaceship: -1                              to_path: "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/a/b"
send_path: "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/a/b"
path: NoMethodError: protected method 'path' called for an instance of Pathname
miss: NoMethodError: undefined method 'zzz_does_not_exist' for an instance of Pathname
respond_zzz: false                         respond_basename: true
lt / between / clamp: NoMethodError: undefined method '<'/'between?'/'clamp' for an instance of Pathname

bin/ruby-lean --compare reports AGREE on that program. Real unmodeled selectors now decline by name — verified for 38 of them (realpath, stat, lstat, children, each_child, readlink, chmod, mkdir, rmdir, delete, unlink, rename, expand_path, relative_path_from, cleanpath, absolute?, relative?, root?, parent, sub_ext, entries, find, rmtree, size, read, write, open, each_line, atime, ctime, mtime, birthtime, utime, world_readable?, zero?, split, fnmatch?, ascend, descend, each_filename), each answering unmodeled method Pathname#<name>. Without the pathname.rb load, find and rmtree raised NoMethodError for methods CRuby has.

Test plan

make gen-check        fresh
make lint             no offenses (rubocop, ruff, shellcheck, actionlint)
make lean             ok
make books            1405 jobs, ok
make metatheory       ok
make soundness        SOUNDNESS CHECK PASSED  (corpus accepted.txt unchanged)
make conformance      CONFORMANCE OK — 1287 programs: agree 1087, disagree 0,
                      sut_unsupported 194, i.e. exactly the recorded baseline;
                      regressions tier 224 cases, 223 held, 0 disagree
make comparator       ok
make book-checks      ok
make feature-loading  ok
make difftest-test    ok, coverage baseline holds
make desugar-unit emitter-unit   ok
make docs             ok
make check-not-weakened          CHECKS NOT WEAKENED

Also bin/ruby-lean --compare on the program above (agrees) and per-selector probes for the 38 unmodeled names and the invented one.

Notes for review

jiro-by-nori Bot and others added 3 commits November 13, 2026 17:19
… their ids (issue #53)

`Books/FastPower/Proof.lean` named the `Power` instance and the address of the
result Array by the literals `114`/`115`. A bootstrap class or a boot fixture
object moves every id the program allocates, and the stale id then names a
different object without failing where it is written: both sides of each
`kernel_rfl` stub reduce to the same wrong machine, so the break shows up as a
type mismatch at `setup`/`fast_power_correct` and costs a scavenger hunt per
additive boot change.

The ids are now read off the machine by the invariant that makes them that
object: `powerIdOf` finds the only object carrying both `@base` and `@steps`,
and `resultIdOf` is the last object of the heap at `final`, which is the Array
the program answers with. `#guard`s pin both invariants at build time, so a
second matching object or an extra allocation between the loop head and `final`
fails at the definition rather than somewhere below.

Proof-only: the model, the prelude and the kernel are untouched.
Co-authored-by: Nori <contact@tilework.tech>
…thod_missing (issue #147)

A Pathname instance answers every unmodeled selector with a named
`Unsupported`, including selectors no Ruby defines, because the prelude's
`Pathname` carried a blanket `method_missing` and a `respond_to_missing?`
that answered `true` for anything. An invented name therefore looked like an
honest gap instead of the `NoMethodError` CRuby raises, and a real unmodeled
one (`realpath`, `stat`, ...) lost its own name behind one generic reason.

`Pathname` is a prelude class but it ships with every CRuby, so its surface
is now generated like a builtin's: `scripts/gen_cruby_names.rb` emits
Pathname's instance selectors, singleton methods and constants into
`crubyMethodNames` / `crubySingletonNames` / `crubyNamespaceConstants`, and
the prelude's `method_missing`/`respond_to_missing?` are deleted. A miss is
then an ordinary lookup miss: a real unmodeled method declines as
`unmodeled method Pathname#realpath`, an invented one raises CRuby's
byte-exact `NoMethodError`, and `respond_to?` answers from the same tables.

`Pathname#path` is protected in CRuby, so it is modeled as a protected
method: `p.path` raises the protected-method `NoMethodError` and
`p.send(:path)` answers. The prelude's `include Comparable` is removed for
the same reason: CRuby's Pathname is not Comparable, so `<`, `>` and
`between?` on one are `NoMethodError` rather than answers.

The checker's copied `nativeInstanceNames` projection grows to keep
`instance_covered` true (the conservative direction: more selectors it
refuses) and `nativeQueryRows` records Pathname's own `===`/`to_s`.
`instance_covered` now needs `maxRecDepth 4000` to walk the larger list.

Stacked on #136: the prelude change shifts the boot heap, which the FastPower
proof pinned object ids against (issue #53).

Co-authored-by: Nori <contact@tilework.tech>
`pathname.so` is booted with the interpreter, but the stdlib's `pathname.rb`
reopens the class with `find`, `rmtree` and `Pathname.mktmpdir`. A program that
has required pathname can call them, so the snapshot must contain them: without
the require the model answered `NoMethodError` for a method CRuby runs. With
them the model declines by name (`unmodeled method Pathname#find`), which is
honest in both states — a gate where CRuby has nothing is sound, a
`NoMethodError` where CRuby has a method is not.

The checker's copied projection gains `rmtree`, so `instance_covered` holds.
The generator header and the table docstrings said the tables cover the model's
*bootstrap* classes; say what they cover now — every class of the model, the
prelude's included — and list Pathname in FOLD beside the other class CRuby
ships as part of its own stdlib. `docs/testing.md` names the size of the
regression corpus, which the new case grows by one.

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.

Pathname's blanket instance method_missing gates names no Ruby defines, and hides which real method is missing

0 participants