Skip to content

File path helpers convert arguments the way CRuby does, and raise TypeError where CRuby does (fixes #57) - #148

Open
jiro-by-nori[bot] wants to merge 1 commit into
mainfrom
jiro/file-path-argument-conversion
Open

jiro-by-nori[bot] wants to merge 1 commit into
mainfrom
jiro/file-path-argument-conversion

Conversation

@jiro-by-nori

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

Copy link
Copy Markdown
Contributor

Problem

File.basename, File.dirname, File.extname and File.join coerced every path argument with to_s, so a program whose argument was not a String ran to a value where CRuby raises:

program before CRuby 4.0.5
File.dirname(123) "." TypeError: no implicit conversion of Integer into String
File.basename(123) "123" TypeError: no implicit conversion of Integer into String
File.extname(:sym) "" TypeError: no implicit conversion of Symbol into String
File.join(1, 2) "1/2" TypeError: no implicit conversion of Integer into String

AGENTS.md rule 3 says the model declines rather than approximates, so this is the wrong direction: a program outside the modeled domain answered instead of gating or raising.

The change

ruby-lean/prelude/prelude.rb only; the generated PreludeJson.lean / Prelude.lean follow.

  • A path argument now goes through CRuby's FilePathValue (rb_get_path): a String stands, otherwise to_path and then to_str must answer a String, and anything else raises CRuby's byte-exact TypeError. The message names the converted value's class when a conversion answers a non-String, and nil / true / false for those immediates. An object whose class has no faithful name (an anonymous class prints a native address) gates with a named Unsupported instead of a message the model cannot reproduce.
  • File.basename's suffix uses the narrower StringValue (String or to_str, never to_path), and an omitted suffix now differs from an explicit nil, which raises.
  • File.join gained the rest of rb_file_join so the coercion change does not trade one wrong answer for another: nested arrays are joined to Strings first (join([[], []]) is "/", not ""), a cycle raises ArgumentError: recursive array, and the separator rule is CRuby's (join("a/", "/b") is "a/b", join("a//", "//b") keeps two — the model produced "a////b" for both before).

String arguments and the modeled Pathname (via to_path) answer exactly as before, and unmodeled File. class methods still gate through the prelude's method_missing.

Test plan

  • New regression case difftest/corpus/regressions/file-path-argument-conversion.rb (sidecar status: "fixed"), run by make conformance's regressions tier: 49 rows covering the four defect rows, the immediate/class naming, to_path over to_str, a bad to_path result, to_s-only refusal, the suffix rules, the empty/nested-array cases, the separator rule and the recursive arrays.
  • Exhaustive separator check outside the harness: all 819 one-, two- and three-element tuples of a nine-string separator alphabet, plus 374 nested-array/recursion/coercion cases, compared row-for-row against CRuby with zero disagreement.

Validation

Toolchain built for this run: Lean v4.32.2, CRuby 4.0.5, Python 3.12.

  • make check → make check: ALL CHECKS PASSED (lint; generated sources fresh; the model; every proof book including the booted-heap metatheory, the soundness theorem and controls, and the comparator; the desugarer and emitter suites; difftest's own tests at their coverage baseline; conformance: ran 1287, agree 1087, sut_unsupported 194, not_compared 6, disagree 0 — exactly the recorded baseline, with the regressions and tier-3 tiers agreeing; feature-loading; the program books; the docs).
  • make check-not-weakened → CHECKS NOT WEAKENED.

Fixes #57


Authored by Jiro, an autonomous agent (Nori sessions), including this description. Reviewed changes are welcome from @sam-xif.

… raise `TypeError` where CRuby does

The four path helpers coerced every argument with `to_s`, so a non-String
path answered a value where CRuby raises `TypeError`:

    File.dirname(123)   model "."     CRuby TypeError: no implicit conversion of Integer into String
    File.basename(123)  model "123"   CRuby TypeError: no implicit conversion of Integer into String
    File.extname(:sym)  model ""      CRuby TypeError: no implicit conversion of Symbol into String
    File.join(1, 2)     model "1/2"   CRuby TypeError: no implicit conversion of Integer into String

`AGENTS.md` rule 3 says the model declines rather than approximates. Here it
answered, which is the wrong direction: a program outside the domain ran to a
value on the model and to an exception on CRuby, so a differential test counted
it as a disagreement rather than a named `Unsupported`.

CRuby resolves a path through `FilePathValue` (`rb_get_path`): a String stands;
otherwise `to_path`, and failing that `to_str`, must answer a String; anything
else is `TypeError: no implicit conversion of X into String`, where X is the
*converted* value's class when a conversion answered a non-String, and `nil`,
`true` or `false` for those immediates. `File.basename`'s suffix uses the
narrower `StringValue` (a String or `to_str`, never `to_path`), and an omitted
suffix now differs from an explicit `nil`, which raises. An object whose class
has no faithful name -- an anonymous class prints a native address -- gates
with a named reason instead of raising a message the model cannot reproduce.

`File.join` needed the rest of its CRuby contract so that this change does not
trade one wrong answer for another. CRuby joins each nested array to a String
*first* and then joins the results, so `join([[], []])` is "/" (two empty
strings), and it raises `ArgumentError: recursive array` on a cycle rather than
recursing forever. It also drops every trailing separator of the accumulated
result when the next name starts with one, so `join("a/", "/b")` is "a/b" while
`join("a//", "//b")` keeps two; the model previously produced "a////b" for
both. The rule is `rb_file_join` as written in CRuby 4.0.5.

The String cases and a modeled `Pathname` argument (`to_path`) answer exactly as
before, and unmodeled `File.` class methods still gate through the prelude's
`method_missing`.

Validation, from a toolchain built for this run (Lean v4.32.2, CRuby 4.0.5,
Python 3.12):

* `make check` -> `make check: ALL CHECKS PASSED`: lint, the generated sources
  fresh, the model, every proof book (the booted-heap metatheory, the soundness
  theorem and its controls, the comparator), the desugarer and emitter suites,
  difftest's own tests at their coverage baseline, conformance
  (`ran 1287, agree 1087, sut_unsupported 194, not_compared 6, disagree 0` --
  exactly the recorded baseline; the regressions and tier-3 tiers agree), the
  feature-loading check, the program books and the docs.
* `make check-not-weakened` -> `CHECKS NOT WEAKENED`.
* The new `difftest/corpus/regressions/file-path-argument-conversion.rb` pins 49
  rows and is byte-identical under CRuby and the model. The separator rule was
  also checked against CRuby over all 819 one-, two- and three-element tuples of
  a nine-string separator alphabet, and the nested-array and recursion cases
  over 374 more, with no disagreement.

Fixes #57

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

Co-authored-by: Jiro <jiro@nori.ai>
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.

File path helpers coerce non-String paths with to_s instead of refusing, so the model answers where CRuby raises TypeError

0 participants