Repository navigation
chore(deps): update actions/setup-go action to v7 - #537
Merged
Merged
Conversation
Contributor
Author
|
Rebased onto The windows lane was failing on the coverage gate, not on A Nothing in the dependency bump was at fault. Rebasing picks up both workflow changes; the diff is still the same six lines of |
tannevaled
force-pushed
the
renovate/actions-setup-go-7.x
branch
from
September 23, 2026 13:42
a86acc4 to
725af53
Compare
tannevaled
added a commit
that referenced
this pull request
Sep 23, 2026
…isagreed with #blocks Two product defects the Windows lane found, and one test that asserted one platform's strerror. **`realpath_rec` built `/C:`.** `realpathResolve` started the walk at `"/"` and split the whole path on `/`, so on Windows the volume letter became an ordinary component and every `File.realpath` / `File.realdirpath` produced `/C:/Users/...` — a path no such filesystem has. It broke `TestFileIO` and `TestFileRealdirpath` on windows-latest, and it would have broken `File.realpath` for every Windows user, not only the tests. MRI does not start at the root either: `rb_check_realpath_emulate` runs the path through `skipprefixroot` first and threads the resulting **prefixlen** through `realpath_rec` (ruby/ruby v3_4_0 file.c:4324, and the `if (*prefixlenp < RSTRING_LEN(*resolvedp))` guard in its `..` branch). That prefixlen is not decoration: it is what stops `..` walking above a root that is more than one character long. rbgo now does the same — `realpathRoot` splits off the volume (`filepath.VolumeName`, empty on POSIX, `C:` or `//host/share` on Windows), the walk starts there, `realpathParent` truncates no further, and a symlink target that names a root of its own restarts from that root rather than from `/`. On POSIX the change is a no-op by construction — `filepath.VolumeName` returns "" there, so the root is `"/"` and nothing is consumed — which is asserted rather than assumed (`TestRealpathRoot`). The multi-character-root behaviour is covered on the POSIX lanes too, by passing the prefix length directly to the primitives instead of waiting for a platform where `VolumeName` returns something. **`File::Stat#inspect` printed `blocks=0` where `#blocks` answers `nil`.** On Windows there is no POSIX stat behind the object, so `#blocks` returns nil (as MRI does where HAVE_STRUCT_STAT_ST_BLOCKS is undefined) — but the native `Inspect` formatted the raw field. I had written the member values a second time instead of reading the accessors' rule, and they drifted at the one member that can be nil. `blocksValue` is now that rule, in one place, read by both. This is the defect, not the test: a member-by-member comparison is exactly what should catch it, and it did. **`TestRealpathSyscallErr` asserted "Not a directory".** The class is portable; the message is the platform's strerror, and Windows words it "The system cannot find the path specified." There is no Windows MRI on this host to witness what MRI prints there, so the text is asserted only where it was witnessed (MRI 4.0.5 on darwin) rather than relaxed to whatever the lane returns — see the errno-text issue. The class assertion still runs everywhere. Merged origin/main (#628, #537) and re-measured against it, whole corpus, per file: **21 815 -> 21 895, and not one spec file lost an example.** core/file 618 -> 629, core/filetest 45 -> 88, core/exception 149 -> 174. `gofmt` clean; `go vet ./...` passes for darwin, linux/amd64, linux/arm64, windows/amd64 and wasip1/wasm, which compiles the tests too; coverage identical to main function for function, new code at 100%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tannevaled
added a commit
that referenced
this pull request
Sep 23, 2026
#634 was red on test (windows-latest) alone, on three subtests of TestFeatureProvided. The matching is the right side; the test was wrong. Which side, established before touching anything: * load.c v3_4_0 loaded_feature_path decides the prefix boundary with a bare `name[plen-1] != '/'` and scans back for the extension with `*e != '.' && *e != '/'`. load.c carries NO separator conditional anywhere — grep for isdirsep, FILE_ALT_SEPARATOR and _WIN32 over the file finds zero. The "and a backslash on Windows" rule is file.c's, whose `#define isdirsep(x) ((x) == '/' || (x) == FILE_ALT_SEPARATOR)` drives is_explicit_relative — a different function, already modelled by isExplicitRelative with os.IsPathSeparator and documented there as platform-dependent. So the loaded-features matching is '/'-only on every platform, featureProvided/loadedFeaturePath already are, and teaching them about backslashes would have been the divergence. * The divergence was then LOCATED rather than inferred. featureProvided has exactly one platform-dependent step: expandedLoadPath runs each $LOAD_PATH entry through featurePath (filepath.Abs + ToSlash). Reproduced on this host by feeding it a path darwin does not call absolute: lp="/lib" expanded=["/lib"] -> true lp="lib" expanded=["<cwd>/internal/vm/lib"] -> false lp=<t.TempDir()> expanded=[<same>] -> true "/lib" on Windows is the middle case: rooted, but with no volume name, so filepath.IsAbs is false and Abs joins it onto the working directory. The fixture's "/lib/foo.rb" entry then has no matching prefix. So the eight prefixed cases now build on t.TempDir(), which both sides of MRI's own comparison have anyway — $LOADED_FEATURES entries come from rb_file_expand_path and the load path from get_expanded_load_path, both expanded and absolute. The premise is asserted in one line (featurePath(dir) == dir) so a platform where it fails says so directly instead of surfacing as eight confusing mismatches. This is deliberately NOT a runtime.GOOS skip: that would stop exercising the matching on the one platform where separators are interesting. It keeps exercising it, and gains a case that pins the rule there — an entry whose prefix ends in a BACKSLASH must not match, on either platform. That one is read from the source, not witnessed: there is no Windows MRI on this host, and the comment says so. Worth recording: only the three cases expecting a match actually failed. The five expecting NO match passed on Windows for the wrong reason — the prefix never matched at all, so the rule each of them names was never reached. All eight test what they mean now. Also merges origin/main (#628, #537). Re-measured from scratch against it: language/ + core/ 21810 -> 21899 (+89, two clean runs a side); language/ 2222 -> 2273; core/kernel 1952 -> 1978; core/module 866 -> 872; predefined_spec 73 -> 124. Every file that moves still moves up. go build, go vet, full go test, gofmt, GOOS=wasip1 build and a GOOS=windows vet of internal/vm all green; the set of functions below 100% coverage is identical to origin/main's in this environment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tannevaled
added a commit
that referenced
this pull request
Sep 23, 2026
…e_function (#634) * vm: model the predefined globals the way MRI's variable table does Every special global went through one untyped slot: `$/ = 1` stored the Integer, `$: = []` replaced the load path, `$@ = [...]` wrote a global nobody read, and `$VERBOSE` read back nil. Give each name the setter MRI gives it. Read from ruby/ruby v3_4_0: * variable.c rb_gvar_readonly_setter + rb_define_hooked_variable ("if (!setter) setter = rb_gvar_readonly_setter"), which is what makes every rb_define_virtual_variable(name, getter, 0) read-only, and the message "%s is a read-only variable" naming the spelling written through. Read-only per eval.c Init_eval ($!), re.c Init_Regexp ($&, $`, $', $+), load.c Init_load ($:/$LOAD_PATH/$-I, $"/$LOADED_FEATURES), io.c Init_IO ($<, $FILENAME, $*), process.c Init_process ($?), ruby.c ruby_prog_init ($-W) and rb_define_readonly_boolean ($-a, $-l, $-p). * io.c deprecated_str_setter and string.c rb_str_setter / rb_fs_setter / rb_fs_check: "value of $/ must be String", "value of $; must be String or Regexp", each warning under the deprecated category on a non-nil assignment. * io.c must_respond_to / stdout_setter / stderr_setter: "$stdout must have write method, NilClass given", with rb_obj_class (not rb_obj_classname). * io.c argf_lineno_setter (NUM2INT, so $. takes #to_int and a Float truncates) and set_LAST_READ_LINE. * eval.c errat_getter/errat_setter: $@ is rb_get_backtrace(errinfo), and assigning it with no rescued exception raises ArgumentError "$! not set" while otherwise routing through Exception#set_backtrace. * re.c match_setter (Check_Type T_MATCH) so `$~ = md` moves the backref and the derived $&, $`, $', $+, $1.. follow it. * ruby.c verbose_setter (RTEST(val) ? Qtrue : val), opt_W_getter, and the fact that ruby_verbose/ruby_debug start at Qfalse, so $VERBOSE and $DEBUG read false rather than nil. * error.c deprecation_warning_enabled(): a non-nil $VERBOSE *and* Warning[:deprecated], which is off by default — so the warnings stay silent on an ordinary run, exactly as MRI is. The shared-slot spellings ($-0 for $/, $-F for $;, $-I for $:, $-v/$-w for $VERBOSE, $-d for $DEBUG, $> for $stdout) now resolve through one canonicalGvar on both the read and the write side, so they cannot drift. language/predefined_spec.rb: 73 -> 122 passing, 88 failures+errors -> 39. No other file in language/, core/kernel or core/module moves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * vm: RUBY_RELEASE_DATE/RUBY_REVISION, and set_backtrace from Locations Two gaps left by the previous commit's sweep of the predefined names. RUBY_RELEASE_DATE and RUBY_REVISION were simply missing, so Object.const_defined?(:RUBY_RELEASE_DATE) was false and every reference raised NameError. version.h at ruby/ruby tag v3_4_1 composes RUBY_RELEASE_DATE from RUBY_RELEASE_YEAR/MONTH/DAY and takes RUBY_REVISION from the generated revision.h — the tagged commit itself, which for v3_4_1 is 48d4efcb85000e1ebae42004e963b5d0cedddcf2 dated 2024-12-25. Both are frozen Strings, which core/builtin_constants asserts separately from their class. Exception#set_backtrace rejected an Array of Thread::Backtrace::Location with a TypeError. error.c v3_4_0 exc_set_backtrace tries rb_location_ary_to_backtrace FIRST and only falls back to rb_check_backtrace's String-or-Array-of-String rules when the value is not a location array, so copying one exception's backtrace_locations into another is legal and sets both #backtrace and #backtrace_locations. language/predefined_spec.rb: 122 -> 124 passing. core/builtin_constants/builtin_constants_spec.rb: 13 -> 17 (0 failures left). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * vm: Kernel#trace_var / #untrace_var Neither existed, so the global-assignment hook was missing entirely and both names were absent from Kernel.private_instance_methods / Kernel.public_methods. Read from ruby/ruby v3_4_0 variable.c: rb_f_trace_var (which takes the block through rb_block_proc() when no command is given — hence the ArgumentError "tried to create Proc object without a block" — and delegates to rb_f_untrace_var on an explicit nil command), rb_f_untrace_var (NameError "undefined global variable $x" for a name with no entry; returns the removed commands as an Array), and rb_gvar_set_entry, which runs the trace list AFTER the variable's setter and walks it from the head that rb_f_trace_var pushes onto — so hooks fire most-recently-registered first, and never fire at all when the setter raised. A String command is Ruby source, evaluated by rb_trace_eval/rb_eval_cmd_kw rather than called. Witnessed against ruby 4.0.5 for each case: registration order (LIFO), untrace_var's return values with and without a command, the NameError, and trace_var's nil return. The hook table hangs off the Kernel module as a lazily created ivar rather than a VM field, so a program that never traces a global allocates nothing and Kernel gains no instance variable. core/kernel/trace_var_spec.rb: 0 -> 6 passing (the file's whole content). core/kernel/untrace_var_spec.rb: 0 -> 2. No other file moves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * vm: rb_feature_p for require, and the pinned tests the globals work moved require decided "already loaded?" by absolute path alone. MRI decides it by NAME, before it touches the filesystem: load.c v3_4_0 search_required calls rb_feature_p first, which looks in $LOADED_FEATURES for an entry equal to either "#{name}#{e}" or "#{load_path[j]}/#{name}#{e}" for an acceptable (possibly empty) extension e, with loaded_feature_path deciding whether an entry's prefix really is a load-path directory. That is why a feature stays loaded once when $LOAD_PATH is rearranged under it, and why an entry a program pushed by hand — ruby/spec pushes "./load_fixture.rb" — suppresses a require of that same spelling although nothing ever expanded it. Ported here including loaded_feature_path's backwards extension scan and its plen/'/'-boundary rules. core/kernel/require_spec.rb: 66 -> 68/69 passing (the file varies by a point between runs; measured both alone and under the parallel sweep). Four pinned tests were asserting behaviour the predefined-globals commit replaced; each is updated to the value MRI 4.0.5 actually produces, verified case by case: * $VERBOSE now READS false rather than nil (ruby.c ruby_prog_init binds it to ruby_verbose, which starts at Qfalse; `ruby -e 'p $VERBOSE'` prints false). rbgo's rb_warn gate still keys on the raw unset slot, so the warning under test stays silent — noted in the test. * `$LOAD_PATH = 42` is a NameError, so loadPathDirs' non-Array arm is now reached by emptying the list instead; the NameError is asserted too. * `$ERROR_INFO = …` is a NameError naming the alias, which is what MRI reports once "English" is loaded. * A $stdout that never answered #write can no longer be assigned, so the three curStdout/nativePuts fallback tests bind an object that answers #write and then remove the method — the branch stays covered and the comments say plainly that MRI raises NoMethodError where rbgo tolerates. Read-only-ness now follows an English alias while the NameError names the spelling written through, as rb_gvar_readonly_setter's QUOTE_ID does. Full `go test ./...` green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * vm: whitebox tests for the predefined globals, trace_var and rb_feature_p Every branch the three preceding commits introduced now has a test, and each expectation is the answer ruby 4.0.5 gives — the read-only NameErrors and the spelling each one names, the typed setters' TypeErrors and their accepting arms, $~ moving the derived match globals, $@ reading and writing the rescued exception's backtrace, $VERBOSE/$DEBUG/$-W's values and the verbose_setter's truthy collapse, $= reading false and the deprecation warnings appearing only under Warning[:deprecated], trace_var's LIFO firing and untrace_var's return values, and the rb_feature_p match table. Three simplifications fell out of chasing the last uncovered lines, which is what the exercise is for: * untrace_var returned nil for a defined-but-untraced global where MRI returns the empty Array rb_f_untrace_var builds from an empty trace list (`$tv = nil; p untrace_var(:$tv)` prints [] on 4.0.5). Fixed, with the with-a-command case still nil. * doRequire's old "an entry spelled exactly like the bare name counts as provided" check is dead now that featureProvided runs first and subsumes it; removed rather than left as unreachable code. * loadedFeaturePath now counts the separator in its prefix length instead of reproducing loaded_feature_path's decrement, which removes the caller's off-by-one and the defensive re-check that guarded it. go test -coverpkg over ./internal/... leaves every function this branch adds or edits at 100.0%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * vm: Kernel#Complex is complex.c's nucomp_convert, step for step Complex() was a two-line shape check that rejected most of what MRI accepts: Complex(Complex(3,4), Complex(5,6)) was "can't convert Complex into Complex", an object answering #to_c was refused, a nil argument reported its class instead of "nil", an embedded NUL came out as "invalid value for convert()" rather than "string contains null byte", and exception: false swallowed errors MRI lets through. Rewritten as complex.c v3_4_0 nucomp_convert in that function's own order, because each step rewrites the argument the next one reads: the nil check, the STRICT String parse (string_to_c_strict — in BOTH argument positions, so Complex(1, "b") is an ArgumentError, not a TypeError), the k_exact_zero_p unwrapping of a Complex argument, the lone-Complex short circuit, the #to_c conversion of a non-Numeric (rb_convert_type, with both of its messages), the a1 + a2*i rule for non-real Numerics, and finally nucomp_s_new with nucomp_real_check's "not a real". exception: false is not a blanket rescue and is no longer implemented as one. MRI applies it at exactly three points — the nil check, the String parse, and a second argument that is not an Integer/Float/Rational — and lets everything else propagate, so `Complex(:sym, 0, exception: false)` really does raise TypeError on 4.0.5. Confirmed against it. The subtle one: complex.c's INTERNAL f_real_p is not Complex#real?. For a Complex it is nucomp_real_p, i.e. f_zero_p(imag), which calls Complex(1, 0.0) real although Complex#real? returns false for every Complex. That is the whole reason Complex(Complex(1, 0.0), 2) is (1+2i) with an Integer imaginary part while Complex(1, 0.0) + Complex(0, 2) is (1+2.0i) — the first never reaches the f_add path. Both witnessed on 4.0.5; a fRealP helper now carries the rule. A 22-case probe table now agrees with ruby 4.0.5 byte for byte. core/kernel/Complex_spec.rb: 54 -> 60 passing. The 6 that remain need mspec's real mock_numeric (the vendored shim's is a plain mock), and MRI scores 62 on this file under the same shim — so the file is finished as far as it can go. No file in core/complex, core/rational, core/numeric, core/integer or core/float moves. Two pinned Go tests were asserting the old behaviour and are updated to the value MRI gives: Complex(1, "b") is ArgumentError, and a lone non-real Numeric comes back unchanged rather than raising (covered as a value case instead). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * vm: module_function's type guard, Object fallback and define_method mirror Four gaps in Module#module_function, each read off vm_method.c v3_4_0 rb_mod_modfunc / rb_mod_define_method and witnessed on ruby 4.0.5: * rb_mod_modfunc opens with `if (!RB_TYPE_P(mod, T_MODULE)) rb_raise( rb_eTypeError, "module_function must be called for modules")`. rbgo happily ran it on a Class. * Class UNDEFINES it (rb_undef_method(rb_cClass, "module_function")), so it is absent from Class.private_instance_methods(true) although Module still carries it; rbgo listed it. * the named-method search is `me = search_method(m, id, 0); if (!me) me = search_method(rb_cObject, id, 0);` — a name the module does not carry is looked up on Object, which is what makes `Module.new { module_function :require }` work. rbgo raised NameError. * rb_mod_define_method reads the same scope visibility a `def` does, so under the toggle a define_method'd name is private as an instance method and public on the module's singleton. rbgo applied the toggle only in OpDefineMethod, so define_method (with a block, a Method or an UnboundMethod) escaped it. The Class tombstone exposed a defensive default in filterVisibility: a candidate name that resolves to no method counted as PUBLIC, so :module_function — reaching the walk from Module's table and then resolving to nothing on a module receiver — started appearing in Module#methods. MRI lists what the receiver can actually be sent, so an unresolvable candidate is now dropped. `K23Mod.methods.include?(:module_function)` is false again, as on 4.0.5. core/module/module_function_spec.rb: 20 -> 26 passing (MRI scores 28 on it under the same shim; the two left need per-FRAME scope visibility, which rbgo keeps per module object — an eval'd `module_function` therefore leaks to the enclosing body). core/module/autoload_spec.rb swings between 57 and 0 between runs on this branch as it does on main — issue #615, re-run and it comes back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * vm: Kernel#warn is rb_warn_m Four divergences, all read off error.c v3_4_0 rb_warn_m / rb_warn_category and witnessed against ruby 4.0.5 in one probe table: * rb_warn_m wraps its WHOLE body in `if (!NIL_P(ruby_verbose) && argc > 0)`, so $VERBOSE = nil makes Kernel#warn a no-op — nothing written, nothing validated, Warning.warn not called. rbgo wrote regardless. ($VERBOSE false still warns; only nil, the -W0 level, silences.) * the message is assembled by rb_io_puts, not by joining #to_s: an Array argument writes one line per element, nested arrays flatten, and an empty Array writes nothing. rbgo inspected it — `warn(["a", "b"])` came out as `["a", "b"]`. ioPutsValueRec already models rb_io_puts, so it is reused rather than reimplemented. * with a level, the message carries a "path:lineno: warning: " prefix, or the bare "warning: " when rb_ec_backtrace_location_ary yields no location. rbgo has no line numbers at all, so no frame can ever supply a path: the no-location form is the truthful answer for every level, and it is what MRI itself produces for a level past the bottom of the stack — the one uplevel case that now matches byte for byte. * the level goes through NUM2LONG, so a Float or a Rational truncates where rbgo raised TypeError; a Symbol is still "no implicit conversion of Symbol into Integer". Also rb_warn_category's rb_warning_warn_arity(): a Warning.warn that takes exactly one argument is called WITHOUT the category keyword, so a program that overrides it with `def Warning.warn(message)` is not handed an argument it cannot take. core/kernel/warn_spec.rb: 18 -> 21 passing. The 4 left all want a real "file:LINE: warning: " prefix; MRI scores 25 on the file under this shim and its own 4 failures are a different set (the shim's category handling). One pinned test asserted the old prefix-less uplevel output; it is updated, with the comment saying what MRI produces there and why rbgo cannot yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * vm: Kernel#open redirects to #to_open io.c v3_4_0 rb_f_open tests `rb_respond_to(argv[0], to_open)` FIRST, before it treats the argument as a path at all. An object that answers #to_open is REDIRECTED: open calls it with the remaining arguments (keywords included, RB_PASS_CALLED_KEYWORDS) and hands back whatever it returns, whatever that is — the open-uri protocol is built on this. rbgo went straight to the path conversion and answered "no implicit conversion of MockObject into String". With a block MRI runs `rb_ensure(rb_yield, io, io_close, io)`: the result is yielded and then closed, and io_close is a CHECKED call, so a returned value that does not answer #close (the specs return a Symbol) is left alone rather than raising. core/kernel/open_spec.rb: 9 -> 14 passing. MRI scores 15 on the file under this shim; the 3 pipe-form errors are its too, and the one left over (`open(path, "r", 0666, {flags: …})` must raise for a POSITIONAL Hash where the keyword form is accepted) needs rbgo to tell a keyword bundle from a trailing Hash at the call site. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * vm: the rb_feature_p FIXTURE was POSIX-shaped, not the matching #634 was red on test (windows-latest) alone, on three subtests of TestFeatureProvided. The matching is the right side; the test was wrong. Which side, established before touching anything: * load.c v3_4_0 loaded_feature_path decides the prefix boundary with a bare `name[plen-1] != '/'` and scans back for the extension with `*e != '.' && *e != '/'`. load.c carries NO separator conditional anywhere — grep for isdirsep, FILE_ALT_SEPARATOR and _WIN32 over the file finds zero. The "and a backslash on Windows" rule is file.c's, whose `#define isdirsep(x) ((x) == '/' || (x) == FILE_ALT_SEPARATOR)` drives is_explicit_relative — a different function, already modelled by isExplicitRelative with os.IsPathSeparator and documented there as platform-dependent. So the loaded-features matching is '/'-only on every platform, featureProvided/loadedFeaturePath already are, and teaching them about backslashes would have been the divergence. * The divergence was then LOCATED rather than inferred. featureProvided has exactly one platform-dependent step: expandedLoadPath runs each $LOAD_PATH entry through featurePath (filepath.Abs + ToSlash). Reproduced on this host by feeding it a path darwin does not call absolute: lp="/lib" expanded=["/lib"] -> true lp="lib" expanded=["<cwd>/internal/vm/lib"] -> false lp=<t.TempDir()> expanded=[<same>] -> true "/lib" on Windows is the middle case: rooted, but with no volume name, so filepath.IsAbs is false and Abs joins it onto the working directory. The fixture's "/lib/foo.rb" entry then has no matching prefix. So the eight prefixed cases now build on t.TempDir(), which both sides of MRI's own comparison have anyway — $LOADED_FEATURES entries come from rb_file_expand_path and the load path from get_expanded_load_path, both expanded and absolute. The premise is asserted in one line (featurePath(dir) == dir) so a platform where it fails says so directly instead of surfacing as eight confusing mismatches. This is deliberately NOT a runtime.GOOS skip: that would stop exercising the matching on the one platform where separators are interesting. It keeps exercising it, and gains a case that pins the rule there — an entry whose prefix ends in a BACKSLASH must not match, on either platform. That one is read from the source, not witnessed: there is no Windows MRI on this host, and the comment says so. Worth recording: only the three cases expecting a match actually failed. The five expecting NO match passed on Windows for the wrong reason — the prefix never matched at all, so the rule each of them names was never reached. All eight test what they mean now. Also merges origin/main (#628, #537). Re-measured from scratch against it: language/ + core/ 21810 -> 21899 (+89, two clean runs a side); language/ 2222 -> 2273; core/kernel 1952 -> 1978; core/module 866 -> 872; predefined_spec 73 -> 124. Every file that moves still moves up. go build, go vet, full go test, gofmt, GOOS=wasip1 build and a GOOS=windows vet of internal/vm all green; the set of functions below 100% coverage is identical to origin/main's in this environment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
tannevaled
added a commit
that referenced
this pull request
Sep 23, 2026
…and the stat surface (+79) (#629) * filetest: FileTest is File's own predicates, not a second copy (+42 in core/filetest) MRI defines its one-path file predicates once and installs each on BOTH FileTest and File through the same C function: #define define_filetest_function(name, func, argc) do { \ rb_define_module_function(rb_mFileTest, name, func, argc); \ rb_define_singleton_method(rb_cFile, name, func, argc); \ } while(false) (ruby/ruby v3_4_0 file.c:6635, and its 26 call sites in Init_File at file.c:7456-7485). rbgo instead had two implementations: a complete one on File and a partial hand-written one on FileTest carrying 13 of the 26 names. The 13 missing were blockdev?, chardev?, empty?, executable_real?, grpowned?, identical?, owned?, pipe?, readable_real?, setgid?, setuid?, socket?, sticky?, world_readable?, world_writable? and writable_real? — every one of them a NoMethodError, which is what core/filetest was mostly failing on. registerFileTest now re-exports File's own *Method records for exactly the names in define_filetest_function's list. Sharing the record (rather than copying a closure) is what makes FileTest.method(:zero?) == FileTest.method (:empty?) hold, which core/filetest/zero_spec.rb asserts. That exposed a second half of the same drift: File's predicates did not all check arity. MRI's rb_file_directory_p and friends take argc 1, and core/filetest/shared/exist.rb asserts the ArgumentError for both receivers. oneArg now guards the four shared closures (access, realAccess, statTest, worldPerm) and the four inline predicates, so File.zero? no longer reaches an index-out-of-range panic with no arguments. MRI 4.0 removed the deprecated exists? spelling: 4.0.5 reports FileTest.respond_to?(:exists?) == false, and it is not in define_filetest_function's list. TestFileTest asserted the pre-4.0 spelling; it now asserts the NoMethodError, witnessed against the host MRI. core/filetest: 45 -> 87 passing. core/file and core/exception unchanged (618 / 149). No spec file lost an example. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * errno+exception: SystemCallError.new is a constructor, not a stub (+23 in core/exception) SystemCallError had no #initialize at all: every argument was discarded, so SystemCallError.new(Errno::EINVAL::Errno) was a message-less generic SystemCallError, #errno was read off the class rather than the call, and Errno::EINVAL.new.message said "Errno::EINVAL" where MRI says "Invalid argument". This implements MRI's syserr_initialize (ruby/ruby v3_4_0 error.c:3104) whole, including the part that surprises: on the generic class a lone Integer argument IS the errno, and when the number has a registered class the receiver's class is REWRITTEN in place (RBASIC_SET_CLASS), which is what makes SystemCallError.new(22).instance_of?(Errno::EINVAL) true. Also from error.c: syserr_errno (3153) — #errno returns the value AS GIVEN, so new("x", 2.9).errno is 2.9 even though the class lookup truncated it to 2 — and syserr_eqq (3168), the .=== that matches by error NUMBER, so `rescue Errno::EINVAL` catches a SystemCallError that was never given that class. The Errno table was rebuilt against Init_syserr (error.c:4204) and set_syserr (error.c:3064), which have two rules rbgo did not follow: - ONE CLASS PER NUMBER, not per name. A second name for a number that already has a class becomes another constant on that same class. That is why Errno::EWOULDBLOCK.equal?(Errno::EAGAIN) holds. - A name the platform does not define is STILL A CONSTANT — MRI's undefined_error binds it to Errno::NOERROR. So Errno.constants is the full known_errors list on every platform. rbgo had 28 names and no NOERROR; it now has MRI's 158, which is what system_call_error_spec.rb leans on when it takes Errno.constants.size as an errno number no class can claim. Numbers come from Go's portable syscall package for the 76 names it defines on every GOOS rbgo builds for (darwin, linux, windows, wasip1); the remaining names take the undefined_error path. That is exact on a platform whose libc does not define them and reports 0 where MRI reports a real number on one that does (macOS has EAUTH at 80) — named in the PR, not papered over. EWOULDBLOCK is registered as EAGAIN's second name, which every platform rbgo builds for agrees with. Messages come from syscall.Errno.Error(), whose table is strerror's with a lower-case lead; errnoStrerror restores the letter and replaces Go's "errno N" placeholder with the C library's wording. Witnessed byte-for-byte against MRI 4.0.5 over 45 expressions (class, message, errno, arity and coercion errors for both receiver shapes). TestProcessSysFailFallback asserted that errno 0 has no registered class. That premise is now false, and it is false in MRI too (4.0.5: SystemCallError.new(0).class == Errno::NOERROR), so the case was moved to a number no name claims and errno 0 given its own assertion. core/exception: 149 -> 172. errno_spec 1 -> 8, io_error_spec 2 -> 6, system_call_error_spec 11 -> 22, case_compare_spec 4 -> 5. core/file and core/filetest unchanged (618 / 87). No spec file lost an example. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * file: realpath/realdirpath resolve like realpath_rec, not like EvalSymlinks Both were filepath.EvalSymlinks with an error guess bolted on. MRI resolves them with one recursive walk, realpath_rec (ruby/ruby v3_4_0 file.c:4324), driven by rb_check_realpath_emulate (file.c:4437) and reached through rb_check_realpath_internal (file.c:4554). EvalSymlinks differs in ways the specs notice: - It refuses a path whose LAST component is a dangling symlink. MRI's realdirpath answers the link's TARGET there: realpath_rec follows the link and then tolerates the absent name (file.c:4390). rbgo answered the link itself. - A missing component under a symlink was reported against the symlink, not against the resolved name the walk actually failed on. - Its errors carry no errno, so ENOTDIR (a path walking through a regular file) came out as ENOENT. realpathResolve is realpath_rec: the component loop, ".."-truncation, the loopcheck table that both detects a cycle (ELOOP) and answers a link already resolved, the fallback at file.c:4383, and the `mode == RB_REALPATH_STRICT || !last || *unresolved_firstsep` rule that decides whether an absent final component is the answer or an error. The last clause is why the trailing separator is threaded separately: MRI walks the path as WRITTEN, so realdirpath("dir/absent/") raises where realdirpath("dir/absent") does not, and expanding the path (which the base-directory argument requires) drops it. The two error WORDINGS are not the same, and now differ correctly: File.realpath reaches realpath(3) first, so ENOENT and ELOOP name rb_check_realpath_internal and the whole argument, while ENOTDIR falls through to the emulation and names realpath_rec and the failing component. File.realdirpath is the emulation, so it always names realpath_rec. Verified byte-for-byte against MRI 4.0.5 over 25 expressions (dangling links, links into missing directories, cycles, relative and absolute link targets, "..", trailing separators, a regular file used as a directory) — identical. core/file/realdirpath_spec.rb: 3 -> 4. The other 11 realpath/realdirpath failures are NOT VM defects: the shim's SPEC_TMP_BASE is "/tmp", which on darwin is a symlink to /private/tmp, so every expectation of the form `File.realpath(link).should == @file` compares a resolved path with an unresolved one. MRI 4.0.5 fails them the same way — it also answers /private/tmp there. They need SPEC_TMP_BASE moved off a symlinked root in scripts/conformance/rubyspec/spec_helper.rb, which this branch does not own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * filestat+exception: real atime/ctime/birthtime, File#stat, rb_stat_inspect, #inspect through #to_s Four defects, all in this branch's files. 1. File::Stat#atime and #ctime REPORTED THE MODIFICATION TIME. Not an approximation — the same number, so File.utime(2000, 2001, f) came back as (2001, 2001) and core/file/lutime_spec.rb was reading a value rbgo could not have got right. MRI reads st_atime / st_ctime off struct stat (ruby/ruby v3_4_0 file.c rb_file_s_atime, stat_atime). Go's syscall.Stat_t has them, but under names that differ per platform — Atim/Ctim on Linux, Atimespec/ Ctimespec on the BSDs and macOS — so shared code cannot name one and compile. statTimeField reads whichever the platform carries by reflection, which keeps the rule in the shared file where the POSIX lanes cover it instead of splitting it across four build-tagged copies. A stat struct with neither still falls back to the modification time, which is what rbgo reported everywhere before. 2. File::Stat#birthtime raised NotImplementedError unconditionally. It now answers where the platform has a creation time (st_birthtime on the BSDs and macOS) and keeps MRI's NotImplementedError — same message — where there is none, which is MRI's own HAVE_STRUCT_STAT_ST_BIRTHTIMESPEC guard and what MRI does on Linux. 3. File::Stat#inspect was the placeholder "#<File::Stat>". It is now rb_stat_inspect (ruby/ruby v3_4_0 file.c:1080): the member list in that order, dev and rdev in hex, mode in octal with a leading zero, birthtime only where it exists. It is written as the value's native Inspect, not as a dispatch through the Ruby accessors, because rb_stat_inspect calls the C member functions directly — a subclass overriding #uid does not change what #inspect prints — and because Kernel#p reaches the same rendering, so `p st` and `st.inspect` cannot drift. Two existing tests asserted the placeholder; they now assert the real shape, witnessed against MRI 4.0.5. 4. File#stat and File#lstat did not exist, which is what core/file/stat/ comparison_spec.rb was failing on before it could compare anything. Added in registerFileStat beside File.stat/File.lstat. They take no argument (ArgumentError otherwise) and refuse a closed stream with IOError, but NOT a read-only one — MRI's rb_io_stat works there, so ioCheckOpen is the wrong guard. Also Exception#inspect: it read the stored @message where MRI's exc_inspect (error.c:1838) goes through rb_obj_as_string(exc), i.e. dispatches #to_s. A subclass that overrides #to_s therefore changes what #inspect reports, and a #to_s returning a non-String falls back to the object's identity form. rbgo already had objAsString with exactly those semantics; #inspect now uses it rather than a second reading of the same rule. Verified byte-for-byte against MRI 4.0.5: #inspect over 7 to_s shapes, the atime/lutime/utime triple, File#stat/#lstat over a symlink, and File::Stat#inspect built member by member the way the spec builds it. core/file 618 -> 624, core/exception 172 -> 174, core/filetest unchanged (87). lutime_spec 6 -> 8, stat/inspect_spec 0 -> 1, stat/comparison_spec 0 -> 2, stat_spec 4 -> 5, lstat_spec 4 -> 5, exception/inspect_spec 3 -> 5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * file: the stat predicates take an IO, not only a path (rb_stat) MRI resolves the argument of its whole stat-predicate family through one function, rb_stat (ruby/ruby v3_4_0 file.c:1304): tmp = rb_check_convert_type_with_id(file, T_FILE, "IO", idTo_io); if (!NIL_P(tmp)) { GetOpenFile(tmp, fptr); result = fstat_without_gvl(fptr, st); } else { FilePathValue(file); result = stat_without_gvl(RSTRING_PTR(file), st); } so File.directory?(io), File.size(io) and FileTest.directory?(anything_with_ to_io) all work. rbgo coerced every one of them with pathArg, which is FilePathValue alone, so an IO or a #to_io object was "no implicit conversion of X into String". statPathArg now does what rb_stat does, and is applied to exactly the seventeen methods that call rb_stat in file.c — directory?, pipe?, socket?, blockdev?, chardev?, setuid?, setgid?, sticky?, owned?, grpowned?, exist?, file?, zero?/empty?, size?, world_readable?, world_writable?, identical? and File.size. NOT to readable?/writable?/executable? and the *_real? family, which go through rb_eaccess, nor to symlink?/ftype, which lstat a path. (An open File still resolves there, through File#to_path — MRI answers true for File.readable?(io), and so does rbgo; the difference is which rule gets it there.) A #to_io that answers something other than an IO is not silently ignored: rb_check_convert_type_with_id raises "can't convert Object to IO (Object#to_io gives Integer)", which statPathArg reproduces. Verified byte-for-byte against MRI 4.0.5 over 14 expressions, including File.directory?(STDIN) and an open directory handle. core/file 624 -> 627, core/filetest 87 -> 88. directory_spec 7 -> 8 on both receivers, size_spec 18 -> 20. Coverage: every function in internal/vm is at the same percentage as on main, with the new code at 100%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tests: the errno texts are the PLATFORM's, not darwin's — guard what has no witness The arch lanes (linux amd64/arm64) went red on one assertion: errnoStrerror(92) = "Protocol not available", want "Illegal byte sequence" 92 is EILSEQ on darwin and ENOPROTOOPT on Linux. The test hard-coded the NUMBER and darwin's text for it, which asserts one platform's <errno.h> on every lane — exactly the mistake this lane has paid for before. TestErrnoStrerror now separates the two things it was conflating: - The RULE, which is portable: for an errno the platform's own table names, errnoStrerror is that text with its leading letter restored. Checked for every name in errnoNumbers, on every platform. - The TEXTS, which are not: the literals are MRI 4.0.5's output on darwin, the only platform with an MRI witness on this host, so they are skipped elsewhere rather than loosened to whatever the lane happens to print. Two more platform assumptions found by reading, before the remaining lanes reported: - The "unknown errno" wording only applies where the platform leaves Go's "errno N" placeholder. Windows' FormatMessage names every number, so there is nothing to substitute and nothing to assert. - TestSyserrInitializeShapes asserts strerror texts (darwin and glibc agree on the three it uses). Windows has no POSIX errno numbers at all — Go spells them APPLICATION_ERROR+n — so it skips there, naming the missing witness. - TestFileStatInspect guessed birthtime's presence from runtime.GOOS, which was wrong for Windows (Sys() is a Win32FileAttributeData whose CreationTime is a Filetime, not a Sec-bearing timespec). It now asks the object, which is also what core/file/stat/inspect_spec.rb does. - TestFileStatAtimeIsNotMtime skips on Windows for the same reason: atime falls back to mtime there, MRI on Windows reports a real one, and there is no Windows MRI here to witness the right fix. `go vet` now passes for darwin, linux/amd64, linux/arm64, windows/amd64 and wasip1/wasm — which compiles the tests, not only the package. Coverage is unchanged: identical to main, function for function. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * file+filestat: the realpath walk assumed a POSIX root, and #inspect disagreed with #blocks Two product defects the Windows lane found, and one test that asserted one platform's strerror. **`realpath_rec` built `/C:`.** `realpathResolve` started the walk at `"/"` and split the whole path on `/`, so on Windows the volume letter became an ordinary component and every `File.realpath` / `File.realdirpath` produced `/C:/Users/...` — a path no such filesystem has. It broke `TestFileIO` and `TestFileRealdirpath` on windows-latest, and it would have broken `File.realpath` for every Windows user, not only the tests. MRI does not start at the root either: `rb_check_realpath_emulate` runs the path through `skipprefixroot` first and threads the resulting **prefixlen** through `realpath_rec` (ruby/ruby v3_4_0 file.c:4324, and the `if (*prefixlenp < RSTRING_LEN(*resolvedp))` guard in its `..` branch). That prefixlen is not decoration: it is what stops `..` walking above a root that is more than one character long. rbgo now does the same — `realpathRoot` splits off the volume (`filepath.VolumeName`, empty on POSIX, `C:` or `//host/share` on Windows), the walk starts there, `realpathParent` truncates no further, and a symlink target that names a root of its own restarts from that root rather than from `/`. On POSIX the change is a no-op by construction — `filepath.VolumeName` returns "" there, so the root is `"/"` and nothing is consumed — which is asserted rather than assumed (`TestRealpathRoot`). The multi-character-root behaviour is covered on the POSIX lanes too, by passing the prefix length directly to the primitives instead of waiting for a platform where `VolumeName` returns something. **`File::Stat#inspect` printed `blocks=0` where `#blocks` answers `nil`.** On Windows there is no POSIX stat behind the object, so `#blocks` returns nil (as MRI does where HAVE_STRUCT_STAT_ST_BLOCKS is undefined) — but the native `Inspect` formatted the raw field. I had written the member values a second time instead of reading the accessors' rule, and they drifted at the one member that can be nil. `blocksValue` is now that rule, in one place, read by both. This is the defect, not the test: a member-by-member comparison is exactly what should catch it, and it did. **`TestRealpathSyscallErr` asserted "Not a directory".** The class is portable; the message is the platform's strerror, and Windows words it "The system cannot find the path specified." There is no Windows MRI on this host to witness what MRI prints there, so the text is asserted only where it was witnessed (MRI 4.0.5 on darwin) rather than relaxed to whatever the lane returns — see the errno-text issue. The class assertion still runs everywhere. Merged origin/main (#628, #537) and re-measured against it, whole corpus, per file: **21 815 -> 21 895, and not one spec file lost an example.** core/file 618 -> 629, core/filetest 45 -> 88, core/exception 149 -> 174. `gofmt` clean; `go vet ./...` passes for darwin, linux/amd64, linux/arm64, windows/amd64 and wasip1/wasm, which compiles the tests too; coverage identical to main function for function, new code at 100%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: stop asserting POSIX-only shapes on the windows lane Two assertions failed on windows, and neither was the VM being wrong. File::Stat#inspect printed `blocks=nil, blksize=0` where the layout regexp demanded digits. That IS MRI's answer: #blocks is nil where HAVE_STRUCT_STAT_ST_BLOCKS is undefined, which is every Windows build, and rb_stat_inspect prints that nil. blocksValue already models it; only the test insisted on a number no Windows stat can produce. The regexp now accepts either shape, with the reason beside it. TestFileRealdirpath compared File.realdirpath against Go's filepath.EvalSymlinks. On POSIX the two canonicalisers agree; on Windows EvalSymlinks additionally expands an 8.3 short name (C:/Users/RUNNER~1 -> C:/Users/runneradmin) and realpath_rec does not, so the test was measuring Go's normalisation rather than ours. Whether MRI expands it there is unwitnessed — there is no Windows MRI on this host — so the full equality is asserted where the two agree by construction, and windows asserts what holds either way: absolute, forward-slashed, naming the file. The case is not skipped; realdirpath is still exercised on that lane. The open question is issue #636. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
tannevaled
added a commit
that referenced
this pull request
Sep 23, 2026
Four units: #628 language (compiler): defined?(super) — 18 examples behind one opcode — the defined? tags frozen, array/hash literal probing, the two method probes MRI distinguishes, block auto-splat through #to_ary, and the pattern-matching deconstruct contract (language 2158 -> 2222) #634 predefined globals, trace_var/untrace_var, rb_feature_p, Kernel#Complex, Module#module_function, Kernel#warn and #open (predefined_spec 73 -> 124 passing, core/kernel +26, core/module +6) #629 FileTest re-exported from File's own records, the Errno table from 28 names to MRI's 158, SystemCallError#initialize/#errno/.===, the real realpath/realdirpath walk, File::Stat#atime/#ctime (both had returned mtime), #inspect, File#stat/#lstat (core/file 618 -> 629, core/filetest 45 -> 88, core/exception 149 -> 174) #537 ci: actions/setup-go v7 (unblocked by rebasing; it had been red for three weeks on a coverage gate main no longer enforces on that lane) Measured with scripts/conformance/rubyspec/run.sh against the pinned corpus at 87b1631, 16-way, TIMEOUT=25, three consecutive runs: 21983, 21982, 21981. The floor is set eleven below the lowest, the same margin rule as the last two bumps: enough for the run-to-run jitter, not enough to hide the loss of a whole spec file. #615 remains open and can still cost a file when it fires. This bump is VM conformance throughout — unlike the previous one, none of it comes from the harness or from a dependency making new files visible. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
tannevaled
added a commit
that referenced
this pull request
Sep 24, 2026
Four units, and one of them was a dependency away: #639 regexp search anchoring and encoding, Refinement#import_methods, pack/unpack #645 THE LINE MAP. __LINE__ evaluated to 0 everywhere because the ISeq carried no line table — 101 examples across five directories were blocked on it, and three earlier agents each reported it as the wall they could not pass. The wall was one repository down: the parser's AST carried no position information at all, 55 node types with no line field, and the lexer discarded every token's line at the AST boundary. go-ruby-parser v0.3.0 adds ast.Program.Lines; this PR builds MRI's compressed insns_info/positions table on it. language/line_spec 0/6 -> 6/6, backtraces carry real lines, frame labels name blocks the way MRI does. #640 Kernel#sleep with the fiber scheduler (sleep_spec 0 -> 15, one example had been hanging the whole file), File.open's mode/flags/perm contract driven by the open(2) flag set rather than a six-way mode string, IO::Buffer.map (+90 across core/file, core/io, core/kernel) #537, #527 already in the previous floor's tree Measured with scripts/conformance/rubyspec/run.sh against the pinned corpus at 87b1631, 16-way, TIMEOUT=25, three consecutive runs: 22153, 22152, 22152. The floor is set twelve below the lowest, the margin rule the last three bumps used: enough for the run-to-run jitter, not enough to hide the loss of a spec file. #615 remains open and still costs a file when it fires. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
v6→v7Release Notes
actions/setup-go (actions/setup-go)
v7.0.0Compare Source
What's Changed
New Contributors
Full Changelog: actions/setup-go@v6...v7.0.0
Configuration
📅 Schedule: (UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR has been generated by Mend Renovate CLI.