Repository navigation
IronRuby 4.0.4
IronRuby 4.0.4 is mostly about speed. Most of the performance gaps listed in #89 are closed or much narrowed across the core: exceptions, strings, Integers, Ranges, Arrays, Hashes, IO, Rational and Complex; the ones still open are listed at the end. ruby-bench's lobsters runs 31-38% faster and protoboeuf drops from 81 s to 1.3 s. A rescued exception costs a quarter of what it did, and copying a long String takes constant time. Startup is shorter, and the method JIT compiles more code. Deep recursion raises a rescuable SystemStackError instead of ending the process, and Ruby programs on Windows get 8 MB stacks. Ruby 4.0's Ractor API and CRuby 4.0's error reports are new. The release also fixes a long list of differences from CRuby, in JSON, BigDecimal, multibyte encodings, signals, threads and CLR interop. It is the last IronRuby release that supports .NET 8.
The last release for .NET 8
.NET 8 reached the end of Microsoft support on 10 November 2026
(.NET support policy).
IronRuby 4.0.4 is the last release that supports it:
- What 4.0.4 ships: the 4.0.4 downloads are built for
net8.0and carry the .NET 8
runtime with them, as every 4.0 release has. Microsoft no longer patches that runtime.
The source still builds for bothnet8.0andnet10.0(IronRubyTargetFrameworksin
Directory.Build.props), and CI tests both. - From the next release, IronRuby targets .NET 10 only. The self-contained downloads
will be built on .NET 10 and carry its runtime. Thenet8.0target framework will be
dropped from the build and from CI. A program that hosts IronRuby, or loads its
assemblies, will need .NET 10. .NET 10 is an LTS release, supported until
14 November 2028.
Nothing changes for scripts run with the self-contained downloads: they bring their own runtime.
Download
Self-contained: unpack one and run ./ironruby, ./iirb.sh and ./igem.sh - on Windows
ironruby.cmd, iirb.cmd and igem.cmd. No .NET to install. Or, on Windows, run the
installer.
- Windows x64 installer:
ironruby-4.0.4-win-x64-setup.exe, the zip below, installed
intoC:\IronRuby40-x64and put on the PATH - Windows x64:
ironruby-4.0.4-win-x64.zip, with Windows Forms, WPF,ironrubyw.exe
andSamples\Notepad\notepad.cmd - Linux x64:
ironruby-4.0.4-linux-x64.tar.gz - macOS arm64:
ironruby-4.0.4-osx-arm64.tar.gz, built in CI, not tested there
SHA256SUMS lists the checksums. Each file and SHA256SUMS has a signed build
provenance attestation, which says which workflow built it, from which commit, in which run:
gh attestation verify ironruby-4.0.4-win-x64-setup.exe --repo Largo/ironruby
New
Ractor, with Ruby 4.0's API (#119). Ractors run on CLR threads. Each one isolates
globals, class variables, class-level instance variables, constants that hold
unshareable values, its own$stdoutand$VERBOSE, and isolated procs. 146 of the 153
checks in CRuby'sbootstraptest/test_ractor.rbpass. A program that starts no Ractor
pays nothing for these checks. Also new from Ruby 4.0:IO#timeout,
Thread#set_trace_func,ObjectSpace.count_objects,GC.stat_heap,
Enumerator::ProducerandProcess::Waiter.- Error reports as CRuby 4.0 prints them (#119). error_highlight is on by default.
did_you_mean loads only when it is needed, and it suggests local variables too. A
SyntaxErrorreport says "syntax errors found" and shows prism's annotated snippet, and
syntax_suggest is on. In every case checked, the report is byte for byte CRuby's. File.link,File.symlinkandFile.readlinkon Windows (#110), which raised
NotImplementedError. They behave as CRuby'swin32.cdoes. A directory target gets a
directory link. Symlinks are created without admin rights where Windows allows it.
readlinkreads a symbolic link or a junction.File.symlink?,File.lstat(...).symlink?
andFile.ftypenow recognise links on Windows.- The json gem's API (#108).
- Parse options:
allow_nan,allow_trailing_comma,decimal_class,
object_class,array_class,create_additions,freezeandmax_nesting. - Generate options:
ascii_only,script_safe/escape_slashandallow_nan. - Methods:
JSON.loadof an IO,nil,""or a proc;unsafe_load,
load_file(!),parse!,Kernel#JSON,j,jjandJSON::NestingError.
json/add/symbolworks.
- Parse options:
- BigDecimal methods from bigdecimal 4 (#109):
scale,precision_scale,
n_significant_digits,save_rounding_mode,save_exception_modeandsave_limit. - Integers wider than 2**31 bits on .NET 8 (#171, #188).
1 << 2**31now builds the
Integer, as CRuby does; IronRuby used to raiseRangeError.2 ** (2**31)now computes
in 4.5 s; it raisedArgumentError, and CRuby takes 91 s. Right shifts and
Integer#[]reach bits past index 2**31. On .NET 9 and later, a BigInteger stops at
about 2**31 bits. There these operations raiseNoMemoryError, as CRuby does when an
allocation fails, instead of a CLROverflowException. - Two opt-in fast starts for source builds on Linux (#76). Neither does anything unless
you ask for it.IR_CONFIG=R2R IR_SERVER=start ./ir.shstarts a pool of processes that are already
initialized, andIR_SERVER=1 ./ir.sh ...runs a program in one of them.ir -e 1
then takes about 0.016 s instead of about 0.76 s. A program the pool cannot run
exactly as a fresh process would runs cold instead.- A NativeAOT build (
-p:IronRubyNativeAot=true) starts in about 0.48 s. It only
interprets, and it can call only the CLR types kept in the image.
Changed
- A CLR exception that derives from
System.Exceptiondirectly is aStandardError
(#73). A custom C# exception (class MyAppException : Exception),AggregateException
andApplicationExceptionused to get pastrescue => eandx rescue y, although an
InvalidOperationExceptionwas caught.System::Exceptionitself is still Ruby's
Exception. A barerescuenow also catches anAggregateExceptionor
TargetInvocationExceptionthat wraps aSystemExitorInterrupt. - Rational and Complex are written in C# (#120, #121). Each is a frozen CLR type with
two fields, and#instance_variablesis empty, as in CRuby. Arithmetic, comparison,
#hash,#to_f,Kernel#RationalandKernel#Complexfollowrational.cand
complex.cfor Integer and Float operands. Other operands still go through Ruby code,
so coercion happens in the same order as before. The speed-up is under Performance. - On Windows, a Ruby program and its threads get 8 MB stacks (#154). Windows gives a
main thread 1 MB, so recursion went about a seventh as deep as in CRuby.irnow runs
the program on an 8 MB STA thread, andThread.newthreads get 8 MB on every platform.
A JIT-compiled method on the Windows main thread now recurses 17,247 levels deep instead
of 1,586; CRuby 4.0.5 manages 10,916. Signal.liston Windows is CRuby's (#162). It used to list all 33 Linux signals.
It now lists the eight the C runtime has: EXIT, INT, ILL, ABRT, FPE, KILL, SEGV and TERM.
Other signals raise CRuby's errors:trap(:HUP)is anArgumentError: unsupported signal, andProcess.kill(1, pid)isErrno::EINVAL.- CLR calls that cannot bind name the conversions that would have worked (#74, #155).
System::Math.abs(nil)saysno implicit conversion of nil into Integer or Float,
not... into System::SByte.- A call that no overload can take raises a
TypeErrorinstead of
AmbiguousMatchException. - A type argument that breaks a generic constraint raises an
ArgumentErrorthat names
the constraint. - Generic overloads tie-break as in C#, so LINQ's
maxandminwith aFunc
selector bind. - An argument with only one of the possible conversions picks that overload:
StringBuilder.new(Rational(5, 2))takes the capacity constructor.
BasicObject#equal?is identity, apart from#==(#122). Before, redefining==
changedequal?too. Integers, Floats,trueandfalsestill compare by value.instance_execand blockinstance_evalno longer make a singleton class (#75). CRuby
has made one only when adefneeds it since 3.2. ANoMethodErroron such an object
says "an instance of C", and#cloneno longer copies an empty singleton class.Hash#fetch,Monitor,Comparable#clamp,Array#hashandHash#hashare C#, as in
CRuby's core (#75, #94).Hash#fetchno longer calls a subclass's#[]or#key?.
Monitoris owned per fiber.MonitorMixin::ConditionVariable#waitreturnsfalseon
timeout.Marshal.dump(Monitor.new)raisesTypeError.Method#arityand#parametersmatch CRuby for all but 46 of 2,111 core methods
(#115, #119). 41 of those 46 are in Set. Arity errors for methods with keywords use
CRuby's wording.- In a host's scope, a top-level
defis also a private method ofObject(#174, #179),
as in CRuby. The same holds for top-levelalias,undef,public/privatewith names,
andmain.define_method. Before, these acted only on the scope's main object, so a class
calling such a method raisedNameError.def self.xon main is labelledxin
backtraces, as in CRuby. - Long ASCII Strings hash differently (#195). A String of 128 or more ASCII characters
now hashes its bytes, so its#hashvalue is not the one 4.0.3 gave. For such strings,
MutableString.GetHashCode()no longer equalsSystem.String.GetHashCode(); a C# host
that uses both as keys of one .NET dictionary will see the difference. - 1.8/1.9-era features are gone (#119). Taint, untrust,
$SAFEand-save/-load
are removed. About 55 prelude constants are gone, and so are the internal instance
variables of Set, Random, Enumerator and others. Marshal output is still byte for byte
CRuby's.RbConfig::CONFIG["ruby_version"]is"4.0.0", andRUBY_REVISIONis the git
commit. - The standard library is CRuby 4.0.9's, with RubyGems and Bundler 4.0.20 (#119).
- The ReadyToRun host and a precompiled startup library are the default (#76, #84, #119).
Linux Release builds publish the ReadyToRun host, andironrubyandir.shrun it
whenever it exists.IronRuby.Startup.dllholds the prelude, RubyGems and the common
standard library, compiled ahead of time. - The build is
dotnet build, driven by theMakefileandRuby.sln(#119, #151).
Build.projandmake.cmdare gone. Off Windows, a build against a DLR checkout that
lacks one of IronRuby's patches fails and names the patch.
Fixed
- Deep recursion raises a rescuable
SystemStackError(#150, #170). It used to end the
process with a CLR stack overflow. Now a program can rescue it and carry on, as in
CRuby, interpreted or JIT-compiled, in methods, blocks, lambdas,define_method,
method_missing,ensureand threads. An endless self-call no longer runs forever as
a CLR tail call. On .NET 8, the error can take minutes to surface from a very deep
recursion, because of .NET 8's exception handling; .NET 10 takes about 3 s. - Threads (#110, #125, #127, #152, #164, #172).
Thread#raisereaches the fiber the thread is running. Around a fiber switch, it
could land in the fiber handing over, and two fibers of one thread then ran at once.- A
Thread#killthat arrives behind a parkedThread#raiseis no longer lost. Thread.handle_interruptworks in every runtime of a process, not only the first.Thread.stopwith no other thread alive raisesThreadErrorinstead of sleeping
forever.Thread.listno longer counts a fiber's CLR thread, or threads of another runtime.- A new thread never inherits a finished thread's state.
Thread#statussays"sleep"only for waits Ruby can see. All 24 probed cases match
CRuby 4.0.9.
- Signals (#161, #166, #180).
Process.kill(:INT, Process.pid)raisesInterruptwithsigno2, as Ctrl-C does.- Sent to the process itself, PIPE, SYS and VTALRM do nothing.
- A signal trapped back to
"DEFAULT"acts untrapped, and"SYSTEM_DEFAULT"ends the
process on Linux. trap(:KILL)andtrap(:STOP)raiseErrno::EINVAL.traptakes signal numbers thatSignal.listdoes not name, such as Linux's
real-time signals.- On Windows, a
trapof TERM or ABRT runs when native coderaise()s the signal.
Before, the process ended with exit code 3. - A
trap(:INT)block runs once, on the main thread (#76).
- Strings and encodings (#87, #90, #92, #95, #105, #107, #110, #124, #147, #171, #184,
#185, #188).subandgsubfollowString#<<'s encoding rules:1.to_s.sub("1", "é")raised a
.NETEncoderFallbackException, and"ab".b.sub("a", "é")gave a Latin-1 byte.insertand[]=no longer corrupt multibyte text.trworks out its result's encoding as CRuby does.start_with?anddelete_prefixraiseEncoding::CompatibilityErrorfor an
incompatible prefix, andcodepointsgives the encoding's own codes.- Searching with a broken pattern String raises
RegexpErroror finds nothing. MatchDatakeeps the text it matched after the source changes, and$~after
sub,splitandscanis CRuby's.StringIO#getcsplits invalid bytes as CRuby does instead of dropping one.- EUC-KR, EUC-JP, EUC-TW, CP51932 and GB18030 split, validate and scrub as CRuby's
encoding tables do. Over 50,000 random strings in 20 encodings, 1,346 differed from
CRuby 4.0.9; now 1 does.
- JSON matches the json gem (#108).
- A custom
to_jsonis called; before, objects came out as"#<Foo:0x…>". - Whole Floats keep their
.0. - A
nilkey is"", not"System.Object". - Invalid UTF-8 raises
JSON::GeneratorError. JSON.dump(obj, io)writes to the IO.- Integers from 2**31 to 2**63 are equal Hash keys to the same Integer written in
Ruby.
- A custom
- Numerics, BigDecimal and Date (#81, #86, #87, #95, #96, #106, #109, #114, #115,
#118, #121, #190, #191).- BigDecimal rounding:
BigDecimal("0.5").roundwas 0./keeps bigdecimal 4's
precision, and%,divmodandsqrtare exact. - Integers from 2**31 to 2**63 are found as Hash keys:
{3 * 1_000_000_000 => 1}[3_000_000_000]
wasnil. 1.fdiv(2**40)andformat("%.2f", 2**31)no longer raise .NET exceptions.5[32]is 0, not 1, and0.pow(x, 0)with a positivexis 0, where it raisedZeroDivisionError. Across 14,372 cases ofInteger#[],
powandRange#last, none differ from CRuby now; 1,712 did.Integer()andFloat()reject strings in ASCII-incompatible encodings and
"1\0".uptoanddowntotoInt32.MaxValueno longer loop forever.Math.hypotrounds as libm does.(1..10**9).last(3)no longer runs out of memory.Date#rfc3339includes the time, and negative years have four digits.Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)is the calling thread's CPU
time, not the process's.
- BigDecimal rounding:
- Arrays (#81, #87).
unshiftorinsertaftershiftlost elements when the buffer grew:
a = [1, 2, 3, 4]; a.shift; a.unshift(7, 8)gave[8, 2, 3, 4, nil].[1, 2, 3][1, 2**31 - 1]was[]because an index overflowed.deletereturns the last matching element and passes its argument to the block.
- IO, StringIO and Dir (#85, #97, #103, #111, #112, #114, #147, #181).
gets("ab")finds overlapping matches.linenono longer counts a read cut short by its limit, orgetsat end of file.IO.foreachandIO.readlinespass onencoding:and the other open options.- The
FileandFileTestpredicates accept an IO. STDIN.read_nonblockraisesIO::EAGAINWaitReadableinstead of blocking.- A
Thread#raise,killorcloseinterrupts a timedgetchat once. Dir.globwalks depth first, as CRuby does, follows..through symbolic links
the same way, and raisesErrno::ELOOPon a link loop.
- Windows (#110, #116, #148, #152).
- Six
core/iospec files no longer hang: a blocked pipe read reports"sleep", and
aclosefrom another thread interrupts it. - Nonblocking pipe I/O does not wait.
Dir.fchdir,File.lutimeand 15Processmethods raiseNotImplementedError, as
CRuby on Windows does; some threw CLR exceptions.- Puma and net/http TLS work over SSH with key login, where DPAPI is unavailable.
- Six
- CLR interop (#147, #148, #156, #164).
System::String#[]takes a Ruby String. A
Method passed for a generic delegate parameter infers its type, so LINQ'sWheretakes
method(:x). A lambda passed for aboolparameter converts by truthiness. A Ruby
String subclass binds to acharparameter.IronRuby::Clr::Name.new(nil)raises
TypeError. - Language and object model (#76, #103, #113, #119, #147, #148). An implicit
superin
a block with an unrelatedselfraises CRuby'sTypeError. A no-argumentprivate
sets the default of the lexical class body.clonekeeps the singleton class's instance
variables. An exception's own#set_backtracecan no longer raise past therescue.
zsuper forwards**restas a keyword splat. In JIT-compiled code,&&and||return
an operand's value. A scope with more than 128 locals works. - Backtraces of hosted code (#119, #169, #173, #174, #179). Code run without a path, as
withEngine.Execute(code), reports real lines under(eval)instead of line 0. The
host's own CLR frames no longer appear below<main>. Backtraces of hot, compiled code
have the right lines, and labels follow CRuby's rules. - Found running ruby-bench (#91, #102). A fiber's return value kept the encoding of the
String its block returned. hexapdf failed on that and now runs.String#unpack1stops
after the first value.Marshal.dumpfollows CRuby'srespond_to?protocol and dumps
BasicObjectsubclasses. - Zlib loads where only
libz.so.1is installed (#160), as in the .NET SDK container
images. net/http and Puma TLS raisedDllNotFoundExceptionthere.
Performance
Most of this is issue #89, the gaps a benchmark sweep found between IronRuby and CRuby.
The figures are before → after, with CRuby's time where the PR measured it; where a PR
measured both runtimes, the figure is .NET 8's.
- Exceptions (#106, #189, #192, #194).
- Frames no longer catch and rethrow every exception they pass, and a frame that
rescues walks the stack only when the backtrace can be read later. - A raise rescued in its own method: 97-100 → 23-30 µs.
- The same, 200 frames deeper: 323 → 27 µs. CRuby: 1.1 and 6.6 µs.
- A raise through 20
rescueclauses that cannot match: 380-423 → 97-110 µs. throwthrough 20ensures: 330-353 → 63-67 µs.- A raise through 20 methods the JIT has not compiled yet: 465-470 → 160-170 µs.
- A raise rescued 10 methods up: 372 → 68 µs (#106).
- Frames no longer catch and rethrow every exception they pass, and a frame that
- ruby-bench (#75, #102).
- lobsters: 8,313 → 5,699 ms per iteration.
instance_execno longer makes singleton
classes, andHash#fetch(150 → 16 ns),Monitor#synchronize(916 → 155 ns) and
Mutex(234 → 74 ns) are C#. - protoboeuf decode: 81 s → 1.3 s.
unpack1(..., offset:)no longer sizes its
result by the whole string. - protoboeuf encode: 5 s → 0.63 s.
bytesplicesplices in place.
- lobsters: 8,313 → 5,699 ms per iteration.
- Startup (#76, #84, #119). With the ReadyToRun host,
ir -e 1went from 0.95 s to
0.80 s and RSS from 119 to 104 MB. The precompiled startup library took it from 0.64 s
to 0.51 s. - The method JIT and OSR compile more (#76, #77, #84, #101, #104, #106, #115, #117,
#123, #173).- What they now take:
case/when, constants, Hash and Array element access, methods of
up to 2,000 nodes, non-literal defaults, optional arguments before keywords, and
direct keyword calls. - Util/bench:
case_when4.4×,array_index2.8×,call_kw_default2.9×,
attr_get2.4×. - Once hot,
times,uptoandeachloops are compiled by OSR:upto_loop
0.186 → 0.031 s,times_loop0.180 → 0.030 s. - A call no longer boxes an Integer argument or result:
f.one(1)35.6 → 17.5 ns,
CRuby's time. - A method that only yields gets a light frame:
def yld1 = yield(1)119.8 → 85.4 ns. - On a Doom renderer, E1M1 takes 16-18 ms a frame instead of about 27 ms. CRuby
without YJIT: 22-25 ms.
- What they now take:
- Strings (#78, #79, #85, #90, #99, #171, #175, #186, #195).
dup,clone,String.new(s), a wholeStringIO#readand long slices share the
buffer. On a 100 KB string,s.dup82-84 → 0.25-0.4 µs, ands.strip59-60 →
0.6 µs. Reading a 650 KB StringIO whole: 612-625 → 0.15 µs.String#indexuses .NET's vectorized search: 2,000 searches of 100 KB, 155 → 22 ms.=~no longer copies its subject: 24 KB subject, 13.4 → 2.2 µs.count("l")is CRuby's 0.45 µs, andsqueeze18-25 → 6.7 µs.- Sorting ASCII Strings went from 4.99× to 3.11× of CRuby.
- Integers (#86, #100, #187, #188, #190, #191).
Integer#to_sof huge Integers is subquadratic. 1M bits: 1.37-1.54 s → 0.64 s on
.NET 8, and 5.6-5.9 s → 0.23 s on .NET 10.(3**2000).to_s(2): 2.05 ms → 9 µs.- Float to Integer outside the Int32 range no longer throws internally:
1e15.floor10,365 → 50 ns. Integer("42"): 338 → 76 ns, CRuby 86 ns.x[2, 3]: 1,375-1,750 → 52-58 ns.5.is_a?(Integer): 97 → 54 ns (#193).
- Rational, Complex and Math (#72, #120, #121).
Rational(1, 3): 540 → 49 ns, CRuby 81 ns.Complex(1, 2): 1,739 → 51 ns.- Complex
a * b: 3,664 → 121 ns. Math.log(x): 881 → 34 ns, andx ** 2: 308 → 33 ns.- A Mandelbrot frame on one thread: 2,715 → 1,452 ms.
- Ranges (#95, #96, #117, #118, #124, #182). Integer ranges count instead of walking.
(1..1000).count: 43.4 → 1.4 µs.(1..100).include?(50): 249 → 16 ns, CRuby 55 ns.1.step(10) { }: about 7.3 → 0.66 µs, CRuby about 0.6.
- Arrays and Hashes (#81, #83, #87, #94, #95).
delete,compact!anduniq!make one pass: deleting 50k zeros from 100k
elements, 217 → 3.9 ms.- 20,000
unshifts: 36 → 1.1 ms, CRuby's time. Hash#dupand#mergeno longer rehash: 10k String keys ×20, 28.9 → 2.1 ms.Hash#each { break }on 10k entries: 68.8 → 1.04 µs.
- IO (#85, #111, #114).
readlines(chomp: true)of 20k lines: 53 → 3.3 ms, CRuby 2.1 ms.File.foreach: 13.8 → 1.8 ms, CRuby 2.6 ms.File.read×10: 203 → 30 ms, CRuby 84 ms.puts×10k: 19.7 → 1.9 ms, CRuby 2.8 ms.
- Classes, Structs and Data (#82, #88, #182, #190). A new class no longer scans every
live class. In a program that has already made thousands of classes,Class.newwent
from 120-150 to 2.3 µs andStruct.new(:a, :b)from 863-888 to 34-36 µs.
Data.define(:a, :b)went from 425-450 to 91-94 µs. - Elsewhere (#91, #92, #98, #175, #182, #184, #189).
Thread.new { }.join: about 290 → 90 µs of main-thread CPU.Dir.glob("**/*")over 1,000 files: 23 → 1.7 ms.Regexp.newof a pattern seen before: 6.5-9.8 → 0.3 µs.File.expand_pathof an absolute path: 2.0-2.25 → 0.44-0.50 µs, CRuby 0.48 µs.Enumerator#next: 1.58 → 0.79 µs.SHA256.hexdigest: 4.3-4.8 → 1.3-1.6 µs.Marshal.dumpof 10k Strings: 6.75 → 3.61 ms.
What is still slow: a raise or throw costs tens of µs against CRuby's 1-6 µs, which is the
CLR's own exception dispatch. + on a 100 KB String is bound by the .NET large-object heap.
A Thread.new costs about 2.5 times CRuby's. String#hash on a long String is about 1.8 times
CRuby's, and Integer.sqrt of a large Integer two to three times. #89 tracks all of these.
What's Changed
- Math.log, log2, log10 and Float#** take a C# fast path for plain Floats by @Largo in #72
- Rescue CLR exceptions that derive from System.Exception directly by @Largo in #73
- CLR overload resolution: C#'s tie-break for generic overloads, and a TypeError when no overload can take an argument by @Largo in #74
- lobsters 31-38% faster: no eager singleton classes, core hashing and fetch in C#, lock-free thread state by @Largo in #75
- perf: faster startup, wider JIT coverage, opt-in warm start and NativeAOT by @Largo in #76
- Speed up ASCII-8BIT conversions; remove dead code from the 2016 import by @Largo in #78
- Vectorize the substring searches behind String#index, #rindex and #include? by @Largo in #79
- Remove more code behind feature flags no build defines by @Largo in #80
- OSR: specialize loops that index Arrays by @Largo in #77
- RubyArray: fix inserts that grow after a shift; O(1) queue pushes and unshifts by @Largo in #81
- Hash#dup, #merge, #replace, Hash[]: copy entries without re-hashing keys by @Largo in #83
- Remove 2016 methods nothing calls; intern CLR-string symbols without a MutableString by @Largo in #82
- Numerics: Float->Integer and unpack('w') without exceptions, fast to_s(radix), Int64 shifts; Integers in the Int64 range as longs by @Largo in #86
- Array#delete/compact!/uniq! in one pass, bulk slices, Hash#keys/values, |*args| yields; four Array bugs by @Largo in #87
- Call-site binding: read parameter attributes and class getters once per type by @Largo in #88
- IO line reading and read-to-end from the buffer; String#* and padding by doubling by @Largo in #85
- MatchData shares the subject's immutable string; fnmatch patterns translated once by @Largo in #90
- Marshal.dump: cut per-object overhead, fix the respond_to? protocol; no realloc when a small array empties by @Largo in #91
- StringIO#getc reads one character in C# (and fixes invalid-byte splitting); cached method arity; one-shot Digest hashing by @Largo in #92
- perf: JIT/OSR for case/when and Hash/Array access, rule-free first calls, precompiled startup library by @Largo in #84
- Fewer allocations in hot library paths: int yield cache, C# Comparable#clamp, allocation-free [a, b].min/max, block-filled Array#fill by @Largo in #94
- Fix the cheap performance gaps from #89 (Hash, Array, Enumerable, String, Range, Encoding, Marshal, Digest, Integer) by @Largo in #95
- Fix the open CodeRabbit findings from #47, #50, #72, #90 and the terminal gaps behind them; add a review skill by @Largo in #97
- Enumerator keeps its instance data in a field: Enumerator#next twice as fast by @Largo in #98
- Array#sort of ASCII Strings compares them as CLR strings (sort_strings 4.99x -> 3.11x of CRuby) by @Largo in #99
- Range#first/#last/#include?/#step and Integer#step: Integer fast paths by @Largo in #96
- Integer#to_s and #inspect of a Fixnum format the int, not a BigInteger by @Largo in #100
- Calls: no box per Integer argument or JIT Integer result by @Largo in #101
- protoboeuf 81 s -> 1.3 s, encode 5 s -> 0.63 s; hexapdf runs again (a fiber's return value kept its encoding) by @Largo in #102
- Light method frames: no RubyMethodScope for a body that never needs one by @Largo in #104
- String#sub/#gsub: the replacement follows String#<<'s encoding rules (no more re-encoded bytes) by @Largo in #105
- Resolve TODOs: Digest, Dir, File stat predicates, IO.sysopen, Proc#inspect, and more by @Largo in #103
- String encoding fixes: insert/[]= corrupting multibyte text, tr encodings, start_with? check, codepoints by @Largo in #107
- JSON matches the json gem (to_json, Floats, keys, load/dump, options); Integer#fdiv and %f fixes by @Largo in #108
- BigDecimal: correct rounding, division/modulo/sqrt precision, Float operands; Date rfc3339, negative years, step by @Largo in #109
- Four TruffleRuby-style optimizations: iterator OSR, inlined accessors, cheaper raises, one-pass interpolation by @Largo in #106
- IO: built-in line/character/write paths, and the line-reader bugs behind them by @Largo in #111
- Broken String patterns, Thread.stop alone, and MRI's Windows behaviour for links and stubs by @Largo in #110
- A redefined #set_backtrace decides #backtrace, and its exceptions no longer escape the rescue by @Largo in #113
- StringIO: gets at EOF / limit 0 leaves lineno alone; fcntl message matches CRuby by @Largo in #112
- StringIO line readers, fresh-literal bytes, Integer/Float exception: false, Array#inject/minmax, zip with a Range by @Largo in #114
- Integer()/Float() in C#, JIT for optional positionals before keywords, Fixnum rounding, Range#first/last, Float#step by @Largo in #115
- JIT direct keyword calls with omitted optionals; Range#min/#max/#include? in C# by @Largo in #117
- Integer#** in C#; Range#include? of one-character String ranges in C# by @Largo in #118
- Rational in C#: a RubyRational type, its arithmetic and Kernel#Rational by @Largo in #120
- Complex in C#: a RubyComplex type, its arithmetic and Kernel#Complex; Math.hypot rounds as libm by @Largo in #121
- Cached ivar writes for subclasses of built-in classes; ArithmeticSequence without Enumerator#initialize by @Largo in #123
- Integer#step over Fixnums in C#; MatchData#string keeps String subclasses; no CodeRabbit docstring check by @Largo in #124
- BasicObject#equal? is identity, its own method apart from #== by @Largo in #122
- Deliver Thread#raise to the fiber a thread is running by @Largo in #125
- Retire 2016-era code and bring the core closer to CRuby 4.0 by @Largo in #119
- IronRuby.Tests: port the interop behaviour only the deleted Tests/Interop checked by @Largo in #142
- Do not lose a Thread#kill that comes in behind a parked Thread#raise by @Largo in #127
- Windows pipes: blocked reads look blocked and give way to close; nonblocking I/O does not wait by @Largo in #116
- Core fixes for the CRuby differences found in the deleted tests (#133-#135, #137, #138, #140) by @Largo in #147
- super with an unrelated self, Clr::Name.new(nil), Method generic inference, one Windows pipe query (#136, #141, #145) by @Largo in #148
- CLAUDE.md: what working on IronRuby needs to know by @Largo in #149
- Deep recursion raises a rescuable SystemStackError instead of ending the process by @Largo in #150
- Puma TLS without DPAPI on Windows; tests for a kill behind a raise by @Largo in #152
- On Windows a Ruby program and its threads get 8 MB stacks by @Largo in #154
- Docs: drop two outdated design files, correct the rest by @Largo in #157
- Build: refuse a DLR checkout that lacks a patch (Backtrace9, #132) by @Largo in #151
- CLR binding errors: name the plausible overloads, and the broken generic constraint (#141) by @Largo in #155
- Port the interop specs that needed C# fixtures; fix the bugs they found by @Largo in #156
- Process.kill to yourself: Interrupt for INT, MRI's default after a trap by @Largo in #161
- Load libz by its soname when the dev symlink is missing by @Largo in #160
- Fix #153, #126, #158, #159 by @Largo in #164
- Signal.list on Windows is MRI's: the C runtime's eight signals by @Largo in #162
- JIT: keep a method's self-call out of tail position (SystemStackError instead of an endless loop) by @Largo in #170
- Hosted backtraces: lines for path-less code, no host CLR frames below (part of #163) by @Largo in #169
- Thread#status: "sleep" only for Ruby-visible waits; fix misrouted Thread#raise from pooled fiber threads by @Largo in #172
- #89: $~.string after sub, huge shift widths, faster Enumerable#zip and String#count/delete/squeeze by @Largo in #171
- Compiled frames: find the main thread's without a thread-static read (#168) by @Largo in #173
- trap takes signal numbers Signal.list does not name, as MRI does by @Largo in #166
- README: Rails on SQLite and Puma, shipped C-extension gems, Rational/Complex in C# by @Largo in #167
- #89: faster Dir.glob, shared Regexp translations, tighter count/delete/squeeze loops by @Largo in #175
- Hosted top-level defs are Object methods too: missing frame and stale file in backtraces (fixes #163) by @Largo in #174
- Windows: a trap of TERM/ABRT runs when native code raise()s the signal by @Largo in #180
- Dir.glob: MRI's walk order, '..' through links, Errno::ELOOP (#178) by @Largo in #181
- Hosted top-level alias/undef/visibility/define_method act on Object too; CRuby labels for main's singleton methods (fixes #177) by @Largo in #179
- #89: faster class creation, Integer/Range step and first/last, expand_path, pack, reflection paths by @Largo in #182
- Issue #89 leftovers: EUC-KR table gaps, #92 regression tests, cheaper Thread.new location by @Largo in #184
- DLR patch 0106: MathUtils.ToString(BigInteger, radix) in subquadratic time by @Largo in #187
- #89: Float() parsing, 3 ** -2, huge powers, Float#step, String#to_c, split and $~ by @Largo in #188
- #89: Integer#[], Integer.sqrt, pow(b, m), Kernel#Integer parsing and tail calls, Data.define by @Largo in #190
- Multibyte encodings: split, validate and scrub as MRI's enc tables do; #92 regression tests by @Largo in #185
- #89: exceptions through interpreted frames without a rethrow per frame; Thread.new location without a stack walk by @Largo in #189
- #89: copy-on-write strings (dup/clone/String.new/StringIO#read), hash without copying, faster ASCII literals by @Largo in #186
- Enumerable#zip: fast path only when every #each is built in (Greptile on #171) by @Largo in #183
- #89: subquadratic Integer#to_s, Range#size in C#, per-thread CPU clock by @Largo in #191
- is_a?/kind_of?/===/instance_of?: the receiver's class without a dictionary lookup per call by @Largo in #193
- #89: ensure-only frames without a rethrow; the rescue's stack walk deferred until needed by @Largo in #192
- #89: share suffix and middle slices of long strings; hash long ASCII strings as bytes by @Largo in #195
- #89: rescue clauses that cannot match let the exception go through (real first-pass filter) by @Largo in #194
- Release 4.0.4 by @Largo in #197
Full Changelog: v4.0.3...v4.0.4