Repository navigation
Conversation
- drop the always-true FEATURE_ENCODING branches (the UTF-8-only fallbacks) - ConcurrentDictionary.GetOrAdd replaces the lazily created, locked Dictionary - AsciiIdentity no longer keeps a 90-entry code page switch that only short-circuited the probe it Debug.Asserted against; the probe runs once per encoding and its result is cached on the RubyEncoding - remove unused ToRegexOption and RequireAsciiIdentity, fold GetCodePage(int) into GetEncodingByNameInitial and GetCodePage(Encoding) into CodePage - static readonly tables instead of lazily built ones Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
- The InsertAt overloads that encode a string, char[] or char into a byte[] had no callers, and wrote the encoded bytes at itemCount instead of index. - ConvertAll had no callers (the BCL has Array.ConvertAll). - Append of a string slice encodes the span instead of copying it to a char[] first; IsBinary is one vectorized search. - The five ValueCompareTo copies share one span comparison. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
RubyOptions.Compatibility always answered Default (= Ruby19), so every check against it was constant. Drop the enum, the option and the attribute-encoding shift, keeping the Ruby19 side of the remaining branches: binding uses the scope's self, Open3.popen3 returns the wait thread, and the Thread.critical release on thread exit (1.8 only) goes. Thread.Abort: Thread#raise/#kill are cooperative (Thread.Abort throws on .NET Core). Remove the debug -useThreadAbortForSyncRaise switch, the ThreadAbortException handler in RubyCommandLine, and the dead block and comments in ThreadOps that described the old scheme. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
Its only callers were the safe-level checks removed with $SAFE. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
The fifteen identical BindDelegate bodies (interpreted rule unless adaptive compilation is off, cached on the binder) now call one LightBind helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
Fourteen copies of lazy-create-dictionary, lock, TryGetValue, Add become GetOrAdd with static factories, so finding an existing binder no longer takes a lock. RubyCallSignature, part of most keys, gets a GetHashCode and Equals(object) so hashing it no longer boxes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
…rror A method's keywords (or **nil) are lowered onto a trailing optional parameter, so the binder's arity check reported the lowered bounds: `def f(a, k:); f` said "given 0, expected 1..2" where MRI says "given 0, expected 1; required keyword: k". ArgsBuilder now takes the declared signature for such a method, words the error from it, and does not count a trailing keyword hash as given. The wording moves to RubyParameterSignature.DescribeExpectedArguments, which the lambda arity check in RubyOps shares. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
…esolver fills in The context, scope, call-site storage and constructor-class builders were four copies of one class differing only in the expression they return; they are now one builder given that expression. Also: the argument assumptions are value tuples rather than an allocated Key per entry, a splatted item reads through the cached IList indexer MethodInfo instead of looking it up by name each time, and an else-if that could only be true is an else. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
…er call When a hosted scope has no variable of the name, main's method_missing calls the next method_missing with the name inserted. InsertMethodName already adds it to the call signature, and the signature then added it again, so the callee saw one argument more than it got: a define_method method_missing raised "wrong number of arguments (given 3, expected 2)" for `foo 2`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
…' check -C is handled further up, as -Cdir and -C dir. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
…ingletons Static field initializers run in textual order, and the singletons' constructors call AsciiIdentity, which encodes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
…ncoding
DuplexStream.Write turned the bytes into a string with the StreamWriter's
encoding, so a write of bytes not valid UTF-8 ("\xff".b) raised
System.Text.DecoderFallbackException and others were silently altered, and
ReadByte returned a decoded char instead of a byte. Both now go to the pipe
streams under the reader and writer. Seek/Length/Position throw
NotSupportedException (the Stream contract) instead of NotImplementedException.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
Splat, SplatAppend, SplatPair (1.8 multiple-assignment), Create*IntegerRange, ConvertRubySymbolToClrString (1.8 Integer-as-Symbol), ConvertSymbolToMutableString, GetEmptyScope, MakeAbstractMethodCalledError and MakeInvalidArgumentTypesError had no caller left: nothing emits them since the prism front end replaced the old AST transformations. ReflectionCache.Generated.cs regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
…Exception RecursionTracker used a Dictionary<object, bool> as a set; it is now a HashSet with the BCL's ReferenceEqualityComparer, and TrackObject is a single Add. HasPendingAsyncException had no caller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
…dead paths - -save never saved anything on .NET Core (AssemblyGen.SaveAssembly is a no-op without FEATURE_ASSEMBLYBUILDER_SAVE) yet kept every compiled file alive and recompiled them all for saving at shutdown; -load looked for an assembly named after the main script that could not exist. The loader keeps only the precompiled-file table the startup library and Util/aot register. SavePath stays as the hosting option Util/aot sets to compile without runtime-bound sites. - $LOAD_PATH no longer gets ".": MRI dropped it in 1.9.2 and ruby4.rb already deleted it again for the console host, but a hosted runtime kept it. - load_assembly's Assembly.LoadWithPartialName retry: on .NET Core that API is Assembly.Load again, so it only replaced the real load error. - GetPathsToTestLoaded added the path twice for a fullPath nobody passed; InsertLoadPaths (unused) inserted in reverse and ignored its index; GetLoadPaths locked $" instead of $:; CAS-era SecurityException handlers; WeakReference<Loader> for the AssemblyResolve holder. - RubyContext: library data on a ConcurrentDictionary instead of a hand-locked Dictionary (TryAddLibraryData had no other caller); EqualityComparer is set in the constructor, so its lazy getter was dead; unused DropClrInstanceSingleton; StringifyIdentifier(s) go through EncodeIdentifier. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
CRuby 4.0 says "to" in the message for a conversion method that returned the
wrong type ("into" is only for the explicit Integer()/Float() failure). Also
drop the DecoderFallbackException overload of CreateTranscodingError, which
had no callers and threw instead of returning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
CompileDebug only called lambda.Compile() since the PDB-emit path went. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
Thread#raise no longer wraps the raised exception in a ThreadAbortException (.NET Core has no Thread.Abort; the exception is parked and thrown as itself), so the "visible" and the real exception are always the same object, and the DEBUG-only _throwingThread was never read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
DetectFileAccessPermissions answered true unconditionally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
…hape.LastName/ShapeCount Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
No "python" language can ever be configured for the suite: IronPython is not referenced or deployed, the test host ships no app.config, and the snippets are Python 2 (print statements) that IronPython 3 would reject. RequireInterop1/2, Backtrace5 and the Python halves of the delegate conversion tests could only ever return early. Drop them with Driver.RunPython and /py[-]. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
….ProcessId MRI dropped $defout in 1.9 and never had $-O (the -0 switch sets $/ and $-0): neither is in its global_variables until assigned, and both read as nil. $$ is read from Environment.ProcessId, without a Process object and the CAS-era SecurityException guard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
- ClrExtensionMethods5 (#if TODO): LINQ extension methods on a Ruby class that includes IEnumerable[T] are by design not offered (see RubyModule.IsScopedExtensionPlace: a Ruby class has no extension-method place of its own); its expected output was not even what LINQ gives. - Dlr_Languages and OverloadResolution_ParamArrays1 had empty bodies, and the SuperParameterlessEval1/2 registrations named tests that do not exist. - EqualityComparer1 was never registered; CRuby 4.0 agrees with it. - ClrMethods2 expected empty output, so it passed whatever new.m did. It now pins the hide/show/hide sequence ClrOverloadInheritance4 relies on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
…nd(half:), quo(x, n), new/allocate, frozen
- #** and #power are now the gem's own Ruby (lib/bigdecimal.rb, verbatim), so they
take a BigDecimal/Float/Rational exponent and a precision; the C# Power (int only)
is gone. BigMath.log/exp are the gem's as well, replacing the 1.9 series.
- A Rational operand of ==/<=>/</<=/>/>=, #coerce, #add/#sub/#mult/#div/#divmod/
#remainder is expanded as + already did. BigDecimal("0.5") == Rational(1, 2)
overflowed the stack (BigDecimal#== and Rational#== handed it back and forth).
- round(n, half: :even), quo(value, digits); the defunct #precs is removed.
- BigDecimal.new is undefined and allocate raises as in MRI; Kernel#BigDecimal (in
bigdecimal.rb) was already the real constructor, so the C# Kernel#BigDecimal it
shadowed is deleted. A BigDecimal reports frozen? true.
- Messages: sqrt of negative value/NaN, Negative precision for sqrt, Computation
results in 'Infinity', X can't be coerced into BigDecimal. BASE is an Integer, and
_dump writes MRI's word-aligned digit count ("18:0.15e1").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
…or#initialize lives Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
…terns
Review finding (NOTE): on a UTF-16/32 string, sub/gsub with a String replacement raised
IndexOutOfRangeException - AppendReplacementExpression looped to replacement.Length, which
counts bytes while the content is binary, but read it with GetChar, which indexes characters
(review/bi2/g3.rb, re4.rb). The replacement is now read as characters throughout.
And `"a\r\nb".encode("UTF-16LE").gsub("\r\n".encode("UTF-16LE"), "\n".encode("UTF-16LE"))`
returned the string unchanged (review/bi2/g2.rb), as did any String pattern holding a
character Regexp.escape escapes ("." or " "): escaping binary content went byte by byte, so a
UTF-16 "." became the bytes "\\.\0", which matched nothing. A UTF-16/32 string that decodes is
now escaped by character. Both reproduced against CRuby 4.0.9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
Review finding (NOTE): RubyOptionsParser.IsRuntimeEncodingName let -Eexternal and -Efilesystem (and --external-encoding=filesystem) through; MRI accepts only "locale" besides real encoding names there and stops with "unknown encoding name - external (RuntimeError)". Reproduced with `ruby -Eexternal -e 'p 1'` (CRuby 4.0.9) against `ir.sh -Eexternal -e 'p 1'`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
Review finding (SHOULD): under LANG=C (US-ASCII, Docker's default) a script in
a non-ASCII directory failed. Reproduced with LANG=C LC_ALL=C in a temp dir
named café: require_relative raised "invalid byte sequence in US-ASCII",
the error report printed ".to_s raised an exception: Unable to translate
Unicode character é", caller_locations(0)[0].path was an invalid US-ASCII
string, and load/require of the BINARY path Dir.children returns failed.
CRuby 4.0.9 runs it all.
- RubyStackTraceBuilder: a frame with non-ASCII text is its UTF-8 bytes in a
BINARY string under a US-ASCII path encoding (TryEncodeRawAsciiPath, as
EncodePath does and as CRuby's backtrace strings are).
- Loader: LoadFile, the load path and autoload paths use DecodePath instead of
ConvertToString, so a BINARY path is read as UTF-8 bytes; a non-ASCII
feature loaded through a BINARY/US-ASCII path is recorded in the filesystem
encoding rather than one byte per character.
- File.expand_path: a BINARY base directory under a non-BINARY path (what
require_relative passes) is decoded as the file system's UTF-8 bytes, not
byte-per-character ("café/lib").
__FILE__ stays a UTF-8 literal (valid, and every path API takes it); CRuby
makes it a US-ASCII string with invalid bytes.
Test: Backtrace_AsciiLocalePaths1 (sets Encoding.default_external to US-ASCII;
raises without the fix).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
Review finding (BLOCKER, PR 119): RubyContext.IsVisibleInstanceVariableName hid every instance variable spelled @__name__, so a program's @__cache__ (gems use @__memoized__, @__Lock__) was missing from #instance_variables and #inspect, dropped by Marshal, and not frozen by Ractor.make_shareable. Reproduced with review/runtime/iv.rb and iv2.rb against CRuby 4.0.9: IronRuby printed [:@A], nil after a Marshal round trip, and false for make_shareable's frozen?. The core's state now lives under names Ruby cannot spell instead: the Prism bridge compiles an @__name__ ivar in the Ruby-written core (IsInternalFile, plus coverage.rb, json.rb and io/console.rb, which stand in for C extensions) as <ir>@__name__ (RubyUtils.GetCoreStateInstanceVariableName); the C# readers and writers (Enumerator, KeyError, SignalException, IO, Struct, Marshal's Set table, ...) use that name, and the prelude's reflective accesses go through __ir_hidden_ivar__/__ir_set_hidden_ivar__. Only angle-bracketed names are hidden now, and since GetStateInstanceVariableNames includes <ir> names, a deep freeze or Ractor copy reaches a Set's elements as MRI's traversal does. Test: Prelude_HiddenState2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
Review finding (NOTE, BigDecimal#to_s quadratic): (BigDecimal("1e1000000")+1)
.to_s("F") took 24.7 s (CRuby ~0 s), all of it in BigInteger.ToString,
which is quadratic in the digit count. Reproduced with review/libs/bd4.rb /
bd5.rb and a timing script.
#to_s and #split now go through ToDecimalString, which splits a value of
more than 2000 digits at 10**(2000 * 2**k) and writes the halves into one
char buffer, zero-padding the low ones. It is as fast as the runtime's
division: the repro takes 0.7 s on .NET 10 (Integer#to_s of the same value:
29 s) and 8.8 s on .NET 8, whose BigInteger division is itself quadratic.
Output checked byte for byte against CRuby for 33 random values of 1999 to
123457 digits with long zero runs at the split points.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
…ds n Review finding (BLOCKER, concurrency/IO): IO#timeout hung forever when the IO's own read buffer already held bytes. The wait that bounds a read (ApplyTimeout -> WaitForEvents -> IsReady) answered "ready" on stream.DataBuffered, so the read of the stream underneath - a pipe(2) DescriptorStream or a socket, which have no timeout of their own - then blocked with no bound. Reproduced with review/conc/io2.rb, io3.rb, io5.rb: `r.timeout = 0.5; w << "partial"; r.gets` hung on IO.pipe and on UNIXSocket.pair (CRuby 4.0.9 raises IO::TimeoutError after 0.5 s), and so did a gets after an earlier gets had left bytes buffered. The wait behind IO#timeout now ignores the IO's own buffer and asks the stream underneath (IsReady/ReadyEvents/WaitForEvents take ignoreBuffer); IO.select, IO#wait and friends still count buffered bytes as ready. Checking the reads that need more than is buffered showed IO#read(n) made one read of the stream and returned what it brought: `w << "ab"` then r.read(5) answered "ab" at once instead of waiting for the other three bytes (CRuby answers "abcde" when they come, IO::TimeoutError under a timeout - io3.rb's last case). IO#read(n) now reads until it has n bytes or the stream ends; #sysread stays a single read. Regression test: Io_Timeout3. ruby/spec core/io and library/socket unchanged (core/io: the 2 errors there are fork and a message wording). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
Review finding (NOTE): _hiddenConstants was a plain HashSet that HideConstant mutated under the class hierarchy lock while IsHiddenConstant (const_get, const_defined?, constants, remove_const via ModuleOps) read it without the lock - a reader racing a writer could see the set mid-resize. Found by reading (the prelude hides its constants at startup, so it is not reproducible on demand). It is now replaced rather than changed, like RubyContext._hiddenConstantNames: written with Volatile.Write under the lock, read with Volatile.Read without it. Covered by the existing hidden-constant tests (no new behavior). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
Review finding (NOTE): RubyErrno used the Darwin column on FreeBSD, whose numbers part from macOS's above 81: ECANCELED is 85 there (89 on macOS), EILSEQ 86, EBADMSG 89, EOVERFLOW 84, EIDRM 82, ENOTSUP and EOPNOTSUPP both 45, and EDOOFUS 88, ENOTCAPABLE 93 and ECAPMODE 94 exist only there. So Errno::ECANCELED::Errno was wrong, and an errno 85 from the OS raised Errno::EBADEXEC. Found by reading against FreeBSD's sys/errno.h. A known column is the safer choice: .NET has no way to ask the platform for an errno's number by name, so "unknown" would leave every Errno class NOERROR. The column is sys/errno.h of FreeBSD 13/14 (ELAST 97); messages still come from strerror. macOS, Linux and Windows are unchanged. Not testable here (no FreeBSD); the Linux columns are covered by the existing Errno tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
Review finding (SHOULD): Socket.getaddrinfo("no-such.invalid", 80) and
Addrinfo.getaddrinfo raised "getaddrinfo: getaddrinfo: Name or service not
known" (CRuby 4.0.9: one prefix) - socket.rb prefixed a message the resolver
(BasicSocket.cs ResolutionFailure) had already prefixed. And
Socket.getaddrinfo("localhost", "nosuchservice") was a plain SocketError where
CRuby raises Socket::ResolutionError with EAI_SERVICE. Reproduced side by side
with ruby and ir (scratch fx3/sock.rb).
The resolver's message is passed on as it is, and an unknown service name is
Socket::ResolutionError (EAI_SERVICE) - which TCPServer.new's service argument
also gets, as in CRuby. Addrinfo.getaddrinfo, .tcp and .udp now read the
service the same way: an unknown name raised nothing there (port 0), and
Addrinfo.tcp("localhost", "http") had port 0 instead of 80. Regression test
Errno_SocketResolution1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
Review finding (NOTE, Directory.Build.props IronRubySourceRevision): the target ran `git rev-parse HEAD` and took Exec's ConsoleOutput, which holds stderr as well as stdout, so anything git wrote there on success joined the revision; and a source tree unpacked inside another git repository stamped that repository's HEAD. Reproduced with a probe project importing the props: with GIT_TRACE=1 the old target set SourceRevisionId to "01:53:12.204290 git.c:463 trace: built-in: git rev-parse HEAD;5785c0c2...", and a copy of the tree in a subdirectory of a checkout took the outer HEAD. Now git's stderr goes to the null device, HEAD is read with `git rev-parse --verify -q HEAD` and must look like an object name, and it is used only when `git rev-parse --show-prefix` is empty, i.e. this directory is the top of its work tree. (--show-prefix rather than comparing --show-toplevel with the directory: no path normalisation, and a checkout reached through a symlink still matches.) Outside a checkout, or nested in another one, RUBY_REVISION stays "". Both probes give the expected result now, and a normal build still reports 4.0.3+<sha>. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
Review note: IronRuby's Random::Formatter (random/formatter.rb, which stands in for random.c) answered SecureRandom.random_number(1..) and (..1) with a Float in [0, 1); CRuby 4.0.9 raises Errno::EDOM for a range without a begin or an end (checked with ruby for 1.., ..1, 1.0.., nil..nil, "a"..). Backwards ranges still fall back to a Float and non-numeric ones are still ArgumentError, as in CRuby. Regression test Random_FormatterRanges1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
Review finding (SHOULD, concurrency): Ractor.store_if_absent yielded its
block while holding lock(_locals), the lock every Ractor[] and Ractor[]=
takes, so a block that waits on another thread using them deadlocked.
Reproduced with review/conc/r8.rb:
`Ractor.store_if_absent(:x) { Thread.new { Ractor[:y] = 1 }.value }` hung
(CRuby 4.0.9 prints 1).
The block now runs under a separate per-Ractor store lock (CRuby's
local_storage_store_lock), which keeps two store_if_absent calls atomic
with each other; _locals is held only for the lookup and the insert.
Regression test: Ractor_Locals2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
Review note: IronRuby's patch to uri/rfc3986_parser.rb (its \g subexpression calls written out for .NET regexes) built the host pattern from three new public constants, so URI::RFC3986_Parser.constants listed DEC_OCTET, IPV4ADDRESS and LS32, which CRuby 4.0.9 does not have (compared with `ruby -ruri -e 'p URI::RFC3986_Parser.constants.sort'`). They are removed once HOST is built; nothing else uses them. VERSION.txt's entry for the patch says so (uri/rfc3986_parser.rb and uri/mailto.rb are listed there, under "Patched in place"). Regression test URI_PatchedParserConstants1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
Review finding (NOTE, AstGenerator.NoteLocalAssignment / BindingParameters):
while the parameters were bound no assignment was counted, so one made in
an optional parameter's default value went uncounted too. Reproduced with
review/calls/g2.rb against CRuby 4.0.9:
def m(a, b = (a = 3)) = Ractor.make_shareable(nil.instance_eval { proc { a } }).call
m(1) # IronRuby 3; CRuby: Ractor::IsolationError ... 'a' may be reassigned.
Parameters.TransformOptionalsInitialization now transforms each default
value with BindingParameters off and only the write of the parameter with
it on, so only the implicit binding is left uncounted. Checked against
CRuby for defaults that assign an earlier parameter, another optional, a
new local, from a nested block, from a keyword default, and in block and
lambda parameters (all now the same).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
…alse Review note (ruby4.rb): Marshal.dump(ObjectSpace::WeakKeyMap.new) succeeded, writing the Ruby stand-in's table; CRuby 4.0.9 raises TypeError "no _dump_data is defined for class ObjectSpace::WeakKeyMap". It now raises the same, the way ruby4.rb's Fiber already does. And `"a\nb".each_line(chomp: false).inspect` left out `(chomp: false)` (CRuby: #<Enumerator: "a\nb":each_line(chomp: false)>): the enumerator now carries chomp: whenever it was given. Both compared side by side with ruby. Regression test Ruby4_WeakKeyMapMarshalEachLine1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
…ast Class#new Review finding (PR 119, SHOULD, performance regression from 6848da6): every Ruby class that includes Comparable or Enumerable gets a CLR type implementing IComparable/IEnumerable, and such classes lost the base cost of Class#new. Reproduced with review/calls/j2.rb, j3.rb, j4.rb (Release, net10.0): 500 `Class.new { include Comparable }.new` took 9.0 s (Kernel: 0.46 s), batches of 200 Comparable classes 1.3-2.4 s and growing (plain: 0.04 s). Two causes: - TypeDescription.FeatureSetsMatch compared the feature lists' hash codes, which for the arrays RubyTypeDispenser.GetOrCreateType builds are their identities, so the type cache never matched a class that implements interfaces and a new type was emitted (Reflection.Emit, several ms) for every such class - the type is now shared by all classes with the same base and interfaces. - RubyClass.TryGetObjectConstruction gave up on any type but Object/RubyObject, so the first new of each such class built and compiled a rule. It now takes a type generated for Ruby classes that derives from Object, whose instances ObjectConstruction allocates through a cached delegate calling its (RubyClass) constructor (RubyTypeDispenser.TryGetObjectAllocator) - what the rule does. After: j2 Comparable class+new x500 0.14-0.36 s (Kernel 0.13-0.20 s), j4 batches 0.02-0.08 s (plain 0.03-0.09 s), j3 first new of 20 classes 0.004 s (was 0.146 s). Instances are still IComparable/IEnumerable to CLR code. Test: ClrRubyImpliedInterfaces3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
Review finding (SHOULD, PR 119): Marshal.cs WriteSetObject wrote a Set's extra instance variables inside its 'o' record next to @hash; CRuby 4.0 writes `I o:Set 1 :@hash{...} 1 :@x ...` and, loading IronRuby's form, drops @x. Reproduced with review/builtins/h.rb (dump with each, load the other's): the setiv entry was not byte-identical and CRuby lost @x. The record of a Set now holds only its table, and the set's ivars follow in the I wrapper like any non-'o' object's; loading an 'o' record of a Set keeps @hash and drops anything else, as CRuby does. h.rb's setiv now matches both ways. Test: Marshal_SetIVars1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
Review finding (NOTE, TracePoint.AddTraceFunc sets _targetThread after Enable). Not reproducible as described: Enable() does not clear _targetThread (only Disable does), and the trace-func constructor sets it before Enable publishes the hook under the lock, so the hook was never visible unlimited. The assignment after Enable was redundant, and it is what made the code read as a race (it could also re-set the thread of a hook another thread had just disabled). It is gone; the hook is enabled straight from the constructor that limits it to its thread. ruby/spec core/tracepoint, Kernel#set_trace_func and Thread#set_trace_func/ add_trace_func, and CoreApiTests' set_trace_func test pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
Review finding (SHOULD, concurrency): Thread.main and Thread.list were not Ractor-scoped. Reproduced with review/conc/r4.rb and r5.rb: inside Ractor.new, Thread.main == Thread.current was false (CRuby 4.0.9: true) and Thread.list.size was 3 with a thread alive in the main Ractor (CRuby: 1). Thread.main now answers the Ractor's main thread inside a Ractor, and Thread.list, ThreadGroup#list and Thread.stop's "stopping only thread" check count only the current Ractor's threads (MRI's rb_ractor_living_threads of the current Ractor). Regression test: Ractor_Threads1. ruby/spec core/thread and core/threadgroup: 0 failures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
…opying Review finding (NOTE, concurrency): Shareability.IsShareable, the make_shareable walk (Visit) and Copier.Copy recursed once per level of nesting with no stack check, so a graph nested deeper than the thread's stack ended the process. Reproduced with review/conc/r7.rb: an array nested 20,000 or 100,000 deep killed ir in Ractor#send / Ractor.make_shareable (CRuby 4.0.9 copies 20,000 levels and raises SystemStackError at 100,000). Ractor.shareable? and Ractor.make_shareable now walk the graph with a stack of their own (depth first, in the order the recursion took), so they work at any depth. The message copy, which runs #initialize_copy and so stays a recursion on the Ruby thread, checks TryEnsureSufficientExecutionStack and raises SystemStackError when the stack is about to run out. It copies 15,000 levels on the main thread and on Ruby threads (CRuby: 20,000 on the main thread, under 10,000 on other threads); 20,000 raises SystemStackError where CRuby still copies. Regression test: Ractor_DeepGraph1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
Review finding (NOTE, concurrency): RubyIO.CreateSharedView, which gives a
Ractor its own $stdin/$stdout/$stderr over the main Ractor's buffered
stream, shared the stream's TimeoutOwner too, so IO#timeout set on one IO
bounded the reads and writes of the other. Reproduced: with stdin a pipe
that writes after 6 s, `$stdin.timeout = 0.1` in the main Ractor made
`Ractor.new { $stdin.read(1) }` raise IO::TimeoutError (CRuby 4.0.9
waits and reads "x"); and Io_Timeout4 fails with IO::TimeoutError
without this change.
An IO that shares its stream now binds the stream to its own timeout
(or to none) whenever it takes the stream for an operation.
Regression test: Io_Timeout4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
… round trips
Review note (PR 119): loading an exception CRuby dumped left :bt_locations
as a visible instance variable (Marshal.cs CreateException), and a NoMethodError
CRuby dumped did not load at all (its message is a NameError::message 'u'
record), nor did NameError's name / NoMethodError's args and private_call?
come back - they became ivars named "name", "args". Reproduced by dumping
raised and constructed NameError/NoMethodError/KeyError/StopIteration with
CRuby 4.0.9 and loading them in IronRuby (review/builtins/h.rb and a variant).
- "bt_locations" is dropped and a loaded exception answers nil for
#backtrace_locations, as CRuby's does.
- A NameError::message record loads as its string (MRI's name_err_mesg_load).
- name, args and private_call? are read into, and written from, the error's
own state, in CRuby's order: NameError.new('x', :foo) and NoMethodError.new
dump byte-identical to CRuby.
- The prelude's @__backtrace_locations/@__backtrace_from_strings become
@__..__ core state, so #backtrace_locations no longer leaves a visible ivar
and Marshal no longer has to filter them.
Test: Marshal_NameError1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
…g in The Windows test-suite step of PR 119 ran for 90 minutes until a newer push cancelled it; its results.txt only held progress dots, so nothing said which test hung. The run now has a 25-minute timeout (the suite takes 2-4 minutes) and prints each test's name as it starts (/verbose), so the "did not finish" branch's tail of results.txt names the test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM
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 retires the code still unchanged since the 2016 import commit (
5252433c) and brings IronRuby closer to CRuby 4.0, in five rounds. Today'smaster(91bb9ecb) is merged in.2016-era lines (
git blame -w, which ignores whitespace changes): 408,361 → 137,549 (−66%). The diff againstmasteris 7,207 files, +24.5k / −280k lines.Most of the remaining 2016 lines are working code. About 21k are upstream stdlib files identical to CRuby 4.0's, and about 7k are license headers.
Round 1: dead code
Tests/mspec),Tests/Legacy,Tests/Experiments,Tests/InteropandTests/Scripts. Their tests that still check something were ported toSrc/IronRuby.Testsfirst.Build.proj,make.cmdand the nuspecs; theMakefileandRuby.slnnow drivedotnet build.FEATURE_*#ifs, dead AST nodes, and Ruby 1.8 methods.RUBY_REVISIONis the git commit.Round 2: 1.8/1.9-era machinery
$SAFE,RubyCompatibility, the Thread.Abort leftovers and-save/-load.rescuelist evaluation order, a barerescuecalling===,eval('super')arguments,breakfrom forwarded blocks.IO.popen "r+"with binary data,String#charswith U+FFFF,String#succ,const_get,Process.clock_gettime, and the Integer⊕Float out-of-range warnings.RbConfig::CONFIG["ruby_version"]is now"4.0.0".Round 3: CRuby 4.0 conformance
Util/sync-stdlib.rb.BigInteger;Class.new(Class), 16-digit object ids, method lists hiding CLR members and helpers);Comparable/Enumerable→IComparable/IEnumerable.Round 4: follow-ups
System::Int64/BigIntegername their CLR types again.[Keywords]);[CalledName],[SameRubyMethod]);[ReceivesKeywords]).IO#timeout,Thread#set_trace_func,ObjectSpace.count_objects,GC.stat_heap, did_you_mean (lazy),Enumerator::Producer,Process::Waiter.ir -e 1: 0.65 s → 0.49 s on the R2R host). A new DLR patch,0103, keeps backtraces when rethrowing.File.lchmod, UDP after ICMP errors, connect timeouts per CRuby Bug #22223, regenerated AOT subclasses.Round 5: conformance and performance
const_get/const_defined?are back to their pre-round-1 speed;NameError#local_variables); error_highlight is on by default; SyntaxError reports read "syntax errors found" with prism's annotated snippet, and syntax_suggest is on. All byte-identical to CRuby on the cases checked.$stdout/$VERBOSE, isolated procs,requirein main. 146 of 153 checks in CRuby'sbootstraptest/test_ractor.rbpass; the checks cost nothing until a Ractor starts.InternalMethods.Generated.cssignature table.resume/transferkeywords, CRuby's Fiber errors and storage,IO#write_nonblockwith non-Strings, and aRubyBufferedStreamoffset bug.String.Join(",", [1, 2]), LINQSum), bignums preferdouble,Nullable[T].newworks, BigInteger-only members and snake_case CLR names work on Integers.Module#__hide_constant__), internal ivars of Set, Random, Enumerator and others hidden (Marshal stays byte-compatible), autoloads for core classes answernil, and Enumerator names are fixed.fixnum_maxhelper now covers IronRuby (default.mspec), a Net::HTTP hang (IO#readaftergetson a socket) is fixed, andThread#statusat thread start is fixed.Merge with master (
91bb9ecb)master's C#Rational/Complexare kept, with this branch's conventions on top: hidden helpers, theNumericowner fordup/clone, "to" wording, and[Keywords]forexception:.Integer()/Float()stay this branch's C# methods.BasicObject#equal?keepsmaster's identity method while staying one method with#==to Ruby.1.step(by:, to:)keywords and theRange#firstarity were fixed after the merge.Testing (final tree
d15f678b)IronRuby.Testson net10.0: PASSED, 0 failures. net8.0 and net10.0 build with no compiler warnings.dda28d04/b79c65ed,LANG=C.UTF-8(README Status):ObjectSpace.each_object(needs-X:ObjectSpace), 24 wordingDecision for the reviewer
"can't convert X to Y (X#m gives Z)" matches CRuby 4.0 exactly, which the README names as the reference. The pinned ruby/spec expects the 4.1 wording "into" from engines other than CRuby < 4.1; CRuby 4.0.9 fails the same examples. CI doesn't gate on the spec run. Reverting means changing one format string in
RubyExceptions.CreateReturnTypeError, one prelude line, and the test expectations that pinned the old wording.Not verified here
Known follow-ups
Thread#backtraceframes or the fiber-reclamation guarantees.Thread#raisearriving just as a thread resumes a fiber can land in the wrong one (intermittent).(2**64).equal?(2**64)istrue(frommaster's value-type identity rule); CRuby saysfalse.🤖 Generated with Claude Code
https://claude.ai/code/session_01KPwZf4gAJD4kdk9eEzGgYM