Skip to content

IronRuby 4.0.4

Choose a tag to compare

@github-actions github-actions released this 10 Nov 23:43
· 179 commits to master since this release
6db3960

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.0 and carry the .NET 8
    runtime with them, as every 4.0 release has. Microsoft no longer patches that runtime.
    The source still builds for both net8.0 and net10.0 (IronRubyTargetFrameworks in
    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. The net8.0 target 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
    into C:\IronRuby40-x64 and put on the PATH
  • Windows x64: ironruby-4.0.4-win-x64.zip, with Windows Forms, WPF, ironrubyw.exe
    and Samples\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 $stdout and $VERBOSE, and isolated procs. 146 of the 153
    checks in CRuby's bootstraptest/test_ractor.rb pass. 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::Producer and Process::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
    SyntaxError report 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.symlink and File.readlink on Windows (#110), which raised
    NotImplementedError. They behave as CRuby's win32.c does. A directory target gets a
    directory link. Symlinks are created without admin rights where Windows allows it.
    readlink reads a symbolic link or a junction. File.symlink?, File.lstat(...).symlink?
    and File.ftype now 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, freeze and max_nesting.
    • Generate options: ascii_only, script_safe/escape_slash and allow_nan.
    • Methods: JSON.load of an IO, nil, "" or a proc; unsafe_load,
      load_file(!), parse!, Kernel#JSON, j, jj and JSON::NestingError.
      json/add/symbol works.
  • BigDecimal methods from bigdecimal 4 (#109): scale, precision_scale,
    n_significant_digits, save_rounding_mode, save_exception_mode and save_limit.
  • Integers wider than 2**31 bits on .NET 8 (#171, #188). 1 << 2**31 now builds the
    Integer, as CRuby does; IronRuby used to raise RangeError. 2 ** (2**31) now computes
    in 4.5 s; it raised ArgumentError, 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 raise NoMemoryError, as CRuby does when an
    allocation fails, instead of a CLR OverflowException.
  • 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.sh starts a pool of processes that are already
      initialized, and IR_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.Exception directly is a StandardError
    (#73). A custom C# exception (class MyAppException : Exception), AggregateException
    and ApplicationException used to get past rescue => e and x rescue y, although an
    InvalidOperationException was caught. System::Exception itself is still Ruby's
    Exception. A bare rescue now also catches an AggregateException or
    TargetInvocationException that wraps a SystemExit or Interrupt.
  • Rational and Complex are written in C# (#120, #121). Each is a frozen CLR type with
    two fields, and #instance_variables is empty, as in CRuby. Arithmetic, comparison,
    #hash, #to_f, Kernel#Rational and Kernel#Complex follow rational.c and
    complex.c for 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. ir now runs
    the program on an 8 MB STA thread, and Thread.new threads 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.list on 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 an ArgumentError: unsupported signal, and Process.kill(1, pid) is Errno::EINVAL.
  • CLR calls that cannot bind name the conversions that would have worked (#74, #155).
    • System::Math.abs(nil) says no implicit conversion of nil into Integer or Float,
      not ... into System::SByte.
    • A call that no overload can take raises a TypeError instead of
      AmbiguousMatchException.
    • A type argument that breaks a generic constraint raises an ArgumentError that names
      the constraint.
    • Generic overloads tie-break as in C#, so LINQ's max and min with a Func
      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 ==
    changed equal? too. Integers, Floats, true and false still compare by value.
  • instance_exec and block instance_eval no longer make a singleton class (#75). CRuby
    has made one only when a def needs it since 3.2. A NoMethodError on such an object
    says "an instance of C", and #clone no longer copies an empty singleton class.
  • Hash#fetch, Monitor, Comparable#clamp, Array#hash and Hash#hash are C#, as in
    CRuby's core
    (#75, #94). Hash#fetch no longer calls a subclass's #[] or #key?.
    Monitor is owned per fiber. MonitorMixin::ConditionVariable#wait returns false on
    timeout. Marshal.dump(Monitor.new) raises TypeError.
  • Method#arity and #parameters match 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 def is also a private method of Object (#174, #179),
    as in CRuby. The same holds for top-level alias, undef, public/private with names,
    and main.define_method. Before, these acted only on the scope's main object, so a class
    calling such a method raised NameError. def self.x on main is labelled x in
    backtraces, as in CRuby.
  • Long ASCII Strings hash differently (#195). A String of 128 or more ASCII characters
    now hashes its bytes, so its #hash value is not the one 4.0.3 gave. For such strings,
    MutableString.GetHashCode() no longer equals System.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, $SAFE and -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", and RUBY_REVISION is 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, and ironruby and ir.sh run it
    whenever it exists. IronRuby.Startup.dll holds the prelude, RubyGems and the common
    standard library, compiled ahead of time.
  • The build is dotnet build, driven by the Makefile and Ruby.sln (#119, #151).
    Build.proj and make.cmd are 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, ensure and 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#raise reaches 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#kill that arrives behind a parked Thread#raise is no longer lost.
    • Thread.handle_interrupt works in every runtime of a process, not only the first.
    • Thread.stop with no other thread alive raises ThreadError instead of sleeping
      forever.
    • Thread.list no longer counts a fiber's CLR thread, or threads of another runtime.
    • A new thread never inherits a finished thread's state.
    • Thread#status says "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) raises Interrupt with signo 2, 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) and trap(:STOP) raise Errno::EINVAL.
    • trap takes signal numbers that Signal.list does not name, such as Linux's
      real-time signals.
    • On Windows, a trap of TERM or ABRT runs when native code raise()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).
    • sub and gsub follow String#<<'s encoding rules: 1.to_s.sub("1", "é") raised a
      .NET EncoderFallbackException, and "ab".b.sub("a", "é") gave a Latin-1 byte.
    • insert and []= no longer corrupt multibyte text.
    • tr works out its result's encoding as CRuby does.
    • start_with? and delete_prefix raise Encoding::CompatibilityError for an
      incompatible prefix, and codepoints gives the encoding's own codes.
    • Searching with a broken pattern String raises RegexpError or finds nothing.
    • MatchData keeps the text it matched after the source changes, and $~ after
      sub, split and scan is CRuby's.
    • StringIO#getc splits 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_json is called; before, objects came out as "#<Foo:0x…>".
    • Whole Floats keep their .0.
    • A nil key 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.
  • Numerics, BigDecimal and Date (#81, #86, #87, #95, #96, #106, #109, #114, #115,
    #118, #121, #190, #191).
    • BigDecimal rounding: BigDecimal("0.5").round was 0. / keeps bigdecimal 4's
      precision, and %, divmod and sqrt are exact.
    • Integers from 2**31 to 2**63 are found as Hash keys: {3 * 1_000_000_000 => 1}[3_000_000_000]
      was nil.
    • 1.fdiv(2**40) and format("%.2f", 2**31) no longer raise .NET exceptions.
    • 5[32] is 0, not 1, and 0.pow(x, 0) with a positive x is 0, where it raised ZeroDivisionError. Across 14,372 cases of Integer#[],
      pow and Range#last, none differ from CRuby now; 1,712 did.
    • Integer() and Float() reject strings in ASCII-incompatible encodings and
      "1\0".
    • upto and downto to Int32.MaxValue no longer loop forever.
    • Math.hypot rounds as libm does.
    • (1..10**9).last(3) no longer runs out of memory.
    • Date#rfc3339 includes 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.
  • Arrays (#81, #87).
    • unshift or insert after shift lost 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.
    • delete returns 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.
    • lineno no longer counts a read cut short by its limit, or gets at end of file.
    • IO.foreach and IO.readlines pass on encoding: and the other open options.
    • The File and FileTest predicates accept an IO.
    • STDIN.read_nonblock raises IO::EAGAINWaitReadable instead of blocking.
    • A Thread#raise, kill or close interrupts a timed getch at once.
    • Dir.glob walks depth first, as CRuby does, follows .. through symbolic links
      the same way, and raises Errno::ELOOP on a link loop.
  • Windows (#110, #116, #148, #152).
    • Six core/io spec files no longer hang: a blocked pipe read reports "sleep", and
      a close from another thread interrupts it.
    • Nonblocking pipe I/O does not wait.
    • Dir.fchdir, File.lutime and 15 Process methods raise NotImplementedError, as
      CRuby on Windows does; some threw CLR exceptions.
    • Puma and net/http TLS work over SSH with key login, where DPAPI is unavailable.
  • 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's Where takes
    method(:x). A lambda passed for a bool parameter converts by truthiness. A Ruby
    String subclass binds to a char parameter. IronRuby::Clr::Name.new(nil) raises
    TypeError.
  • Language and object model (#76, #103, #113, #119, #147, #148). An implicit super in
    a block with an unrelated self raises CRuby's TypeError. A no-argument private
    sets the default of the lexical class body. clone keeps the singleton class's instance
    variables. An exception's own #set_backtrace can no longer raise past the rescue.
    zsuper forwards **rest as 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
    with Engine.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#unpack1 stops
    after the first value. Marshal.dump follows CRuby's respond_to? protocol and dumps
    BasicObject subclasses.
  • Zlib loads where only libz.so.1 is installed (#160), as in the .NET SDK container
    images. net/http and Puma TLS raised DllNotFoundException there.

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 rescue clauses that cannot match: 380-423 → 97-110 µs.
    • throw through 20 ensures: 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).
  • ruby-bench (#75, #102).
    • lobsters: 8,313 → 5,699 ms per iteration. instance_exec no longer makes singleton
      classes, and Hash#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. bytesplice splices in place.
  • Startup (#76, #84, #119). With the ReadyToRun host, ir -e 1 went 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_when 4.4×, array_index 2.8×, call_kw_default 2.9×,
      attr_get 2.4×.
    • Once hot, times, upto and each loops are compiled by OSR: upto_loop
      0.186 → 0.031 s, times_loop 0.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.
  • Strings (#78, #79, #85, #90, #99, #171, #175, #186, #195).
    • dup, clone, String.new(s), a whole StringIO#read and long slices share the
      buffer. On a 100 KB string, s.dup 82-84 → 0.25-0.4 µs, and s.strip 59-60 →
      0.6 µs. Reading a 650 KB StringIO whole: 612-625 → 0.15 µs.
    • String#index uses .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, and squeeze 18-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_s of 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.floor 10,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, and x ** 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! and uniq! make one pass: deleting 50k zeros from 100k
      elements, 217 → 3.9 ms.
    • 20,000 unshifts: 36 → 1.1 ms, CRuby's time.
    • Hash#dup and #merge no 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.new went
    from 120-150 to 2.3 µs and Struct.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.new of a pattern seen before: 6.5-9.8 → 0.3 µs.
    • File.expand_path of 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.dump of 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