diff --git a/.gitmodules b/.gitmodules
index c637a7a5..416f4b04 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -8,3 +8,6 @@
path = src/ChibiRuby.Compiler/mruby-compiler2
url = git@github.com:hadashiA/mruby-compiler2.git
branch = master
+[submodule "sandbox/ChibiRuby.Benchmark/ruby/optcarrot"]
+ path = sandbox/ChibiRuby.Benchmark/ruby/optcarrot
+ url = git@github.com:mame/optcarrot.git
diff --git a/sandbox/ChibiRuby.Benchmark/ChibiRuby.Benchmark.csproj b/sandbox/ChibiRuby.Benchmark/ChibiRuby.Benchmark.csproj
index c1f01d8f..58c2278a 100644
--- a/sandbox/ChibiRuby.Benchmark/ChibiRuby.Benchmark.csproj
+++ b/sandbox/ChibiRuby.Benchmark/ChibiRuby.Benchmark.csproj
@@ -17,10 +17,17 @@
Always
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
diff --git a/sandbox/ChibiRuby.Benchmark/OptcarrotMrubyOriginalRunner.cs b/sandbox/ChibiRuby.Benchmark/OptcarrotMrubyOriginalRunner.cs
new file mode 100644
index 00000000..fd497fb2
--- /dev/null
+++ b/sandbox/ChibiRuby.Benchmark/OptcarrotMrubyOriginalRunner.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Runtime.CompilerServices;
+
+namespace ChibiRuby.Benchmark;
+
+sealed class OptcarrotMrubyOriginalRunner(int frames = 180, bool printResult = false)
+{
+ const string MrubyPathEnvironmentVariable = "CHIBIRUBY_BENCH_MRUBY";
+
+ public void Run()
+ {
+ var mrubyPath = ResolveMrubyPath();
+ if (!File.Exists(mrubyPath))
+ {
+ throw new InvalidOperationException(
+ "mruby original executable was not found. " +
+ $"Set {MrubyPathEnvironmentVariable}=/path/to/mruby, or build sandbox/ChibiRuby.Benchmark/mruby with " +
+ "MRUBY_CONFIG=../mruby_optcarrot_config.rb ./minirake.");
+ }
+
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = mrubyPath,
+ WorkingDirectory = GetBenchmarkPath(Path.Join("ruby", "optcarrot")),
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ };
+
+ startInfo.ArgumentList.Add("tools/shim.rb");
+ startInfo.ArgumentList.Add("--headless");
+ startInfo.ArgumentList.Add("--quiet");
+ if (printResult)
+ {
+ startInfo.ArgumentList.Add("--print-fps");
+ startInfo.ArgumentList.Add("--print-video-checksum");
+ }
+ startInfo.ArgumentList.Add("--frames");
+ startInfo.ArgumentList.Add(frames.ToString(CultureInfo.InvariantCulture));
+ startInfo.ArgumentList.Add("examples/Lan_Master.nes");
+
+ using var process = Process.Start(startInfo)
+ ?? throw new InvalidOperationException($"Failed to start mruby original executable: {mrubyPath}");
+
+ var stdoutTask = process.StandardOutput.ReadToEndAsync();
+ var stderrTask = process.StandardError.ReadToEndAsync();
+ process.WaitForExit();
+
+ var stdout = stdoutTask.GetAwaiter().GetResult();
+ var stderr = stderrTask.GetAwaiter().GetResult();
+
+ if (printResult)
+ {
+ Console.Write(stdout);
+ Console.Error.Write(stderr);
+ }
+
+ if (process.ExitCode != 0)
+ {
+ throw new InvalidOperationException(
+ $"mruby original optcarrot failed with exit code {process.ExitCode}.\n" +
+ stdout +
+ stderr);
+ }
+ }
+
+ static string ResolveMrubyPath()
+ {
+ var configuredPath = Environment.GetEnvironmentVariable(MrubyPathEnvironmentVariable);
+ return !string.IsNullOrWhiteSpace(configuredPath)
+ ? configuredPath
+ : GetBenchmarkPath(Path.Join("mruby", "bin", "mruby"));
+ }
+
+ static string GetBenchmarkPath(string relativePath, [CallerFilePath] string callerFilePath = "")
+ {
+ return Path.Join(Path.GetDirectoryName(callerFilePath)!, relativePath);
+ }
+}
diff --git a/sandbox/ChibiRuby.Benchmark/Program.cs b/sandbox/ChibiRuby.Benchmark/Program.cs
index 4bdd43ba..dd2b711d 100644
--- a/sandbox/ChibiRuby.Benchmark/Program.cs
+++ b/sandbox/ChibiRuby.Benchmark/Program.cs
@@ -1,8 +1,109 @@
-using System.Reflection;
+using System;
+using System.Diagnostics;
+using System.Reflection;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using ChibiRuby.Benchmark;
+// Profiling-friendly mode: warm up the VM, then run the measured optcarrot
+// workload many times in a tight loop so a sampling profiler (dotnet-trace)
+// captures mostly steady-state execution rather than startup / JIT / warmup.
+//
+// --profile-optcarrot [frames] [warmupRuns] [iterations]
+//
+// Recommended capture (run against the built dll so dotnet-trace traces the
+// app process directly, not the `dotnet run` launcher):
+//
+// dotnet build -c Release sandbox/ChibiRuby.Benchmark
+// dotnet-trace collect --format speedscope \
+// -- dotnet sandbox/ChibiRuby.Benchmark/bin/Release/net10.0/ChibiRuby.Benchmark.dll \
+// --profile-optcarrot 180 3 30
+//
+// Open the resulting .speedscope.json at https://www.speedscope.app/ for a flamegraph.
+// if (args is ["--profile-optcarrot", ..])
+// {
+// var frames = args.Length >= 2 && int.TryParse(args[1], out var parsedFrames)
+// ? parsedFrames
+// : 180;
+// var warmupRuns = args.Length >= 3 && int.TryParse(args[2], out var parsedWarmupRuns)
+// ? parsedWarmupRuns
+// : 3;
+// var iterations = args.Length >= 4 && int.TryParse(args[3], out var parsedIterations)
+// ? parsedIterations
+// : 30;
+//
+// using var loader = new RubyScriptLoader();
+// loader.PreloadOptcarrotBenchmark(frames, printResult: false);
+//
+// Console.WriteLine($"[profile] warming up: {warmupRuns} run(s) x {frames} frames");
+// for (var i = 0; i < warmupRuns; i++)
+// {
+// loader.RunChibiRuby();
+// }
+//
+// Console.WriteLine($"[profile] measuring: {iterations} run(s) x {frames} frames");
+// loader.ResetDispatchProfile();
+// var gc0 = GC.CollectionCount(0);
+// var gc1 = GC.CollectionCount(1);
+// var gc2 = GC.CollectionCount(2);
+// var alloc0 = GC.GetTotalAllocatedBytes(precise: true);
+// var pause0 = GC.GetTotalPauseDuration();
+// var sw = Stopwatch.StartNew();
+// for (var i = 0; i < iterations; i++)
+// {
+// loader.RunChibiRuby();
+// }
+// sw.Stop();
+// var allocated = GC.GetTotalAllocatedBytes(precise: true) - alloc0;
+// var pause = GC.GetTotalPauseDuration() - pause0;
+//
+// var totalFrames = (long)frames * iterations;
+// var fps = totalFrames / sw.Elapsed.TotalSeconds;
+// Console.WriteLine(
+// $"[profile] done: {totalFrames} frames in {sw.Elapsed.TotalSeconds:F3}s => {fps:F2} fps " +
+// $"({sw.Elapsed.TotalMilliseconds / iterations:F2} ms/run)");
+// Console.WriteLine(
+// $"[profile] GC: gen0={GC.CollectionCount(0) - gc0} gen1={GC.CollectionCount(1) - gc1} " +
+// $"gen2={GC.CollectionCount(2) - gc2} pause={pause.TotalMilliseconds:F1}ms " +
+// $"({pause.TotalMilliseconds / sw.Elapsed.TotalMilliseconds * 100:F1}% of wall)");
+// Console.WriteLine(
+// $"[profile] alloc: {allocated / 1024.0 / 1024.0:F1} MB total, " +
+// $"{allocated / (double)totalFrames / 1024.0:F1} KB/frame");
+// Console.Write(loader.DumpDispatchProfile());
+// return;
+// }
+
+if (args is ["--quick-optcarrot", ..])
+{
+ var frames = args.Length >= 2 && int.TryParse(args[1], out var parsedFrames)
+ ? parsedFrames
+ : 180;
+ var warmupRuns = args.Length >= 3 && int.TryParse(args[2], out var parsedWarmupRuns)
+ ? parsedWarmupRuns
+ : 0;
+ using var loader = new RubyScriptLoader();
+ loader.PreloadOptcarrotBenchmark(frames, printResult: warmupRuns == 0);
+ for (var i = 0; i < warmupRuns; i++)
+ {
+ loader.RunChibiRuby();
+ }
+ if (warmupRuns > 0)
+ {
+ loader.PreloadOptcarrotRun(frames);
+ }
+ loader.RunChibiRuby();
+ return;
+}
+
+if (args is ["--quick-optcarrot-mruby", ..])
+{
+ var frames = args.Length >= 2 && int.TryParse(args[1], out var parsedFrames)
+ ? parsedFrames
+ : 180;
+ new OptcarrotMrubyOriginalRunner(frames, printResult: true).Run();
+ return;
+}
+
BenchmarkSwitcher.FromAssembly(Assembly.GetEntryAssembly()!).Run(args);
[Config(typeof(BenchmarkConfig))]
@@ -16,3 +117,34 @@ public class MandelbrotBenchmark() : MRubyBenchmarkBase("bm_so_mandelbrot.rb");
[Config(typeof(BenchmarkConfig))]
public class AoRenderBenchmark() : MRubyBenchmarkBase("bm_ao_render.rb");
+
+[Config(typeof(BenchmarkConfig))]
+public class OptcarrotBenchmark
+{
+ readonly RubyScriptLoader scriptLoader = new();
+ readonly OptcarrotMrubyOriginalRunner mrubyOriginalRunner = new();
+
+ [GlobalSetup]
+ public void LoadScript()
+ {
+ scriptLoader.PreloadOptcarrotBenchmark(printResult: false);
+ }
+
+ [GlobalCleanup]
+ public void GlobalCleanup()
+ {
+ scriptLoader.Dispose();
+ }
+
+ [Benchmark]
+ public void ChibiRuby()
+ {
+ scriptLoader.RunChibiRuby();
+ }
+
+ [Benchmark]
+ public void MRubyOriginal()
+ {
+ mrubyOriginalRunner.Run();
+ }
+}
diff --git a/sandbox/ChibiRuby.Benchmark/RubyScriptLoader.cs b/sandbox/ChibiRuby.Benchmark/RubyScriptLoader.cs
index baa2dcf9..35b5111b 100644
--- a/sandbox/ChibiRuby.Benchmark/RubyScriptLoader.cs
+++ b/sandbox/ChibiRuby.Benchmark/RubyScriptLoader.cs
@@ -2,12 +2,34 @@
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using System.Text;
using ChibiRuby.Compiler;
namespace ChibiRuby.Benchmark;
unsafe class RubyScriptLoader : IDisposable
{
+ const string OptcarrotPreludeFile = "chibiruby/optcarrot_prelude.rb";
+
+ static readonly string[] OptcarrotDefinitionFiles =
+ [
+ "lib/optcarrot.rb",
+ "lib/optcarrot/opt.rb",
+ "lib/optcarrot/nes.rb",
+ "lib/optcarrot/palette.rb",
+ "lib/optcarrot/pad.rb",
+ "lib/optcarrot/driver.rb",
+ "lib/optcarrot/cpu.rb",
+ "lib/optcarrot/apu.rb",
+ "lib/optcarrot/ppu.rb",
+ "lib/optcarrot/rom.rb",
+ "lib/optcarrot/mapper/mmc1.rb",
+ "lib/optcarrot/mapper/uxrom.rb",
+ "lib/optcarrot/mapper/cnrom.rb",
+ "lib/optcarrot/mapper/mmc3.rb",
+ "lib/optcarrot/config.rb",
+ ];
+
readonly MRubyState mrubyCSState;
readonly MrbStateNative* mrbStateNative;
@@ -20,6 +42,8 @@ unsafe class RubyScriptLoader : IDisposable
public RubyScriptLoader()
{
mrubyCSState = MRubyState.Create();
+ mrubyCSState.DefineIO();
+ mrubyCSState.DefineRegexp();
RegisterMathModule(mrubyCSState);
mrubyCSCompiler = MRubyCompiler.Create(mrubyCSState);
@@ -87,11 +111,28 @@ public void PreloadScriptFromFile(string fileName)
PreloadScript(source);
}
+ public void PreloadOptcarrotBenchmark(int frames = 180, bool printResult = true)
+ {
+ var definitions = CompileChibiRubySource(BuildOptcarrotDefinitionsSource());
+ mrubyCSState.Execute(definitions);
+
+ PreloadOptcarrotRun(frames, printResult);
+ }
+
+ public void PreloadOptcarrotRun(int frames = 180, bool printResult = true)
+ {
+ currentChibiRubyIrep = CompileChibiRubySource(BuildOptcarrotRunSource(frames, printResult));
+ }
+
public MRubyValue RunChibiRuby()
{
return mrubyCSState.Execute(currentChibiRubyIrep!);
}
+ // public void ResetDispatchProfile() => mrubyCSState.ResetDispatchProfile();
+ //
+ // public string DumpDispatchProfile(int topN = 25) => mrubyCSState.DumpDispatchProfile(topN);
+
public MrbValueNative RunMRubyNative()
{
return NativeMethods.MrbLoadProc(mrbStateNative, currentMRubyNativeProc!.DangerousGetPtr());
@@ -126,4 +167,136 @@ byte[] ReadBytes(string fileName)
var path = GetAbsolutePath(Path.Join("ruby", fileName));
return File.ReadAllBytes(path);
}
-}
\ No newline at end of file
+
+ Irep CompileChibiRubySource(string source)
+ {
+ using var compilation = mrubyCSCompiler.Compile(Encoding.UTF8.GetBytes(source));
+ return compilation.ToIrep();
+ }
+
+ static string BuildOptcarrotDefinitionsSource()
+ {
+ var builder = new StringBuilder();
+ AppendBenchmarkRubyFile(builder, OptcarrotPreludeFile);
+ foreach (var file in OptcarrotDefinitionFiles)
+ {
+ AppendOptcarrotFile(builder, file);
+ }
+ AppendOptcarrotFixups(builder);
+ return builder.ToString();
+ }
+
+ static void AppendOptcarrotFixups(StringBuilder builder)
+ {
+ builder.AppendLine();
+ builder.AppendLine("# chibiruby optcarrot fixups");
+ builder.AppendLine("module Optcarrot::Palette");
+ builder.AppendLine(" module_function :nestopia_palette, :defacto_palette");
+ builder.AppendLine("end");
+ builder.AppendLine("module Optcarrot::Driver");
+ builder.AppendLine(" module_function :load, :load_each");
+ builder.AppendLine("end");
+ }
+
+ static void AppendOptcarrotFile(StringBuilder builder, string fileName)
+ {
+ var sourcePath = GetAbsolutePath(Path.Join("ruby", "optcarrot", fileName));
+ var source = File.ReadAllText(sourcePath);
+ source = StripRequireLines(source);
+ source = TransformOptcarrotSource(source);
+
+ builder.AppendLine();
+ builder.AppendLine($"# {fileName}");
+ builder.AppendLine(source);
+ }
+
+ static void AppendBenchmarkRubyFile(StringBuilder builder, string fileName)
+ {
+ var sourcePath = GetAbsolutePath(Path.Join("ruby", fileName));
+ var source = File.ReadAllText(sourcePath);
+
+ builder.AppendLine();
+ builder.AppendLine($"# {fileName}");
+ builder.AppendLine(source);
+ }
+
+ static string StripRequireLines(string source)
+ {
+ using var reader = new StringReader(source);
+ var builder = new StringBuilder(source.Length);
+
+ while (reader.ReadLine() is { } line)
+ {
+ var trimmed = line.TrimStart();
+ if (trimmed.StartsWith("require ", StringComparison.Ordinal) ||
+ trimmed.StartsWith("require_relative ", StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ builder.AppendLine(line);
+ }
+
+ return builder.ToString();
+ }
+
+ static string TransformOptcarrotSource(string source)
+ {
+ source = source.Replace(
+ "@apu = @cpu.apu = APU.new(@conf, @cpu, *@audio.spec)",
+ "audio_rate, audio_bits = @audio.spec\n @apu = APU.new(@conf, @cpu, audio_rate, audio_bits)\n @cpu.apu = @apu",
+ StringComparison.Ordinal);
+ source = source.Replace(
+ "@ppu = @cpu.ppu = PPU.new(@conf, @cpu, @video.palette)",
+ "@ppu = PPU.new(@conf, @cpu, @video.palette)\n @cpu.ppu = @ppu",
+ StringComparison.Ordinal);
+ source = source.Replace(
+ "@palette = [*0..4096]",
+ "@palette = (0..4096).map { |i| i }",
+ StringComparison.Ordinal);
+ source = source.Replace(
+ "send(*DISPATCH[@opcode])",
+ "dispatch = DISPATCH[@opcode]\n" +
+ " case dispatch.size\n" +
+ " when 1\n" +
+ " __send__(dispatch[0])\n" +
+ " when 2\n" +
+ " __send__(dispatch[0], dispatch[1])\n" +
+ " when 3\n" +
+ " __send__(dispatch[0], dispatch[1], dispatch[2])\n" +
+ " else\n" +
+ " __send__(dispatch[0], dispatch[1], dispatch[2], dispatch[3])\n" +
+ " end",
+ StringComparison.Ordinal);
+ source = source.Replace("@buffer << @mixer.sample", "@buffer << 0", StringComparison.Ordinal);
+
+ source = source.Replace(".pack(\"C*\").sum", ".sum & 0xffff", StringComparison.Ordinal);
+ source = source.Replace(".pack('C*').sum", ".sum & 0xffff", StringComparison.Ordinal);
+
+ return source;
+ }
+
+ static string BuildOptcarrotRunSource(int frames, bool printResult)
+ {
+ var romPath = EscapeRubyString(GetAbsolutePath(Path.Join(
+ "ruby",
+ "optcarrot",
+ "examples",
+ "Lan_Master.nes")));
+ var profilingOptions = printResult
+ ? "print_fps: true, print_video_checksum: true, "
+ : "";
+ return
+ "Optcarrot::NES.new({ " +
+ "video: :none, audio: :none, input: :none, " +
+ $"frames: {frames}, " +
+ profilingOptions +
+ $"romfile: \"{romPath}\" " +
+ "}).run\n";
+ }
+
+ static string EscapeRubyString(string value) =>
+ value
+ .Replace("\\", "\\\\", StringComparison.Ordinal)
+ .Replace("\"", "\\\"", StringComparison.Ordinal);
+}
diff --git a/sandbox/ChibiRuby.Benchmark/mruby_optcarrot_config.rb b/sandbox/ChibiRuby.Benchmark/mruby_optcarrot_config.rb
new file mode 100644
index 00000000..47757731
--- /dev/null
+++ b/sandbox/ChibiRuby.Benchmark/mruby_optcarrot_config.rb
@@ -0,0 +1,6 @@
+MRuby::Build.new do |conf|
+ toolchain :gcc
+ conf.gembox "default"
+ conf.gem mgem: "mruby-gettimeofday"
+ conf.gem mgem: "mruby-regexp-pcre"
+end
diff --git a/sandbox/ChibiRuby.Benchmark/mruby_optcarrot_config.rb.lock b/sandbox/ChibiRuby.Benchmark/mruby_optcarrot_config.rb.lock
new file mode 100644
index 00000000..90ec6fc2
--- /dev/null
+++ b/sandbox/ChibiRuby.Benchmark/mruby_optcarrot_config.rb.lock
@@ -0,0 +1,16 @@
+---
+mruby:
+ version: 3.4.0
+ release_no: 30400
+builds:
+ host:
+ https://github.com/mame/mruby-gettimeofday.git:
+ url: https://github.com/mame/mruby-gettimeofday.git
+ branch: master
+ commit: e56dfae3b52b487a9ff8b901ef52e893b93159ee
+ version: 0.0.0
+ https://github.com/iij/mruby-regexp-pcre.git:
+ url: https://github.com/iij/mruby-regexp-pcre.git
+ branch: master
+ commit: 71bd16ba59239f04aefb73bb6d46d5b581f27b1b
+ version: 0.0.0
diff --git a/sandbox/ChibiRuby.Benchmark/ruby/chibiruby/optcarrot_prelude.rb b/sandbox/ChibiRuby.Benchmark/ruby/chibiruby/optcarrot_prelude.rb
new file mode 100644
index 00000000..08c8e53d
--- /dev/null
+++ b/sandbox/ChibiRuby.Benchmark/ruby/chibiruby/optcarrot_prelude.rb
@@ -0,0 +1,255 @@
+# Compatibility shims used only by the optcarrot benchmark.
+
+unless Object.const_defined?(:RUBY_ENGINE)
+ RUBY_ENGINE = "chibiruby"
+end
+
+unless Object.const_defined?(:RUBY_VERSION)
+ RUBY_VERSION = "3.0.0"
+end
+
+module Kernel
+ def puts(value = nil)
+ print(value.to_s) unless value.nil?
+ print("\n")
+ nil
+ end unless method_defined?(:puts)
+end
+
+unless Object.const_defined?(:Struct)
+ class Struct
+ def self.new(*members)
+ klass = Class.new do
+ members.each do |member|
+ attr_accessor member
+ end
+
+ define_method(:initialize) do |v0 = nil, v1 = nil, v2 = nil, v3 = nil, v4 = nil, v5 = nil|
+ values = [v0, v1, v2, v3, v4, v5]
+ members.each_with_index do |member, i|
+ instance_variable_set(:"@#{member}", values[i])
+ end
+ end
+
+ define_method(:[]) do |member|
+ member = members[member] if member.is_a?(Integer)
+ instance_variable_get(:"@#{member}")
+ end
+ end
+
+ klass.instance_eval do
+ define_method(:[]) do |v0 = nil, v1 = nil, v2 = nil, v3 = nil, v4 = nil, v5 = nil|
+ new(v0, v1, v2, v3, v4, v5)
+ end
+ end
+
+ klass
+ end
+ end
+end
+
+unless Object.const_defined?(:Process)
+ module Process
+ CLOCK_MONOTONIC = 1
+
+ def self.clock_gettime(_clock_id)
+ Time.now.to_f
+ end
+ end
+end
+
+class File
+ class << self
+ alias binread read unless respond_to?(:binread)
+
+ def binwrite(path, content)
+ write(path, content)
+ end unless respond_to?(:binwrite)
+
+ def readable?(path)
+ exist?(path)
+ end unless respond_to?(:readable?)
+
+ def basename(path)
+ path = path.to_s
+ i = path.size - 1
+ while i >= 0
+ return path[i + 1, path.size - i - 1] if path[i] == "/"
+ i -= 1
+ end
+ path
+ end unless respond_to?(:basename)
+
+ def extname(path)
+ base = basename(path)
+ i = base.size - 1
+ while i >= 0
+ return base[i, base.size - i] if base[i] == "."
+ i -= 1
+ end
+ ""
+ end unless respond_to?(:extname)
+ end
+end
+
+class Integer
+ def [](index)
+ (self >> index) & 1
+ end unless 1.respond_to?(:[])
+
+ def even?
+ self % 2 == 0
+ end unless 1.respond_to?(:even?)
+
+ def step(limit, step = 1, &block)
+ return to_enum(:step, limit, step) unless block
+ raise ArgumentError, "step can't be 0" if step == 0
+
+ i = self
+ if step > 0
+ while i <= limit
+ block.call(i)
+ i += step
+ end
+ else
+ while i >= limit
+ block.call(i)
+ i += step
+ end
+ end
+ self
+ end unless 1.respond_to?(:step)
+end
+
+class String
+ def b
+ self
+ end unless "".respond_to?(:b)
+
+ def start_with?(*prefixes)
+ prefixes.each do |prefix|
+ prefix = prefix.to_s
+ return true if self[0, prefix.size] == prefix
+ end
+ false
+ end unless "".respond_to?(:start_with?)
+
+ def chars
+ ary = []
+ i = 0
+ while i < size
+ ary << self[i]
+ i += 1
+ end
+ ary
+ end unless "".respond_to?(:chars)
+
+ def tr(from, to)
+ result = dup
+ i = 0
+ while i < result.size
+ idx = from.index(result[i])
+ result[i] = to[idx] || to[-1] if idx
+ i += 1
+ end
+ result
+ end unless "".respond_to?(:tr)
+
+ def %(values)
+ values = [values] unless values.is_a?(Array)
+ result = dup
+ values.each do |value|
+ result = result.sub(/%[-+0-9.]*[A-Za-z]/, value.to_s)
+ end
+ result
+ end unless "".respond_to?(:%)
+end
+
+class Array
+ def slice!(index, length = nil)
+ if length
+ result = []
+ i = 0
+ while i < length && index + i < size
+ result << self[index + i]
+ i += 1
+ end
+ self[index, length] = []
+ result
+ else
+ delete_at(index)
+ end
+ end unless [].respond_to?(:slice!)
+
+ def rotate!(count = 1)
+ return self if empty?
+ count %= size
+ count.times { self << shift }
+ self
+ end unless [].respond_to?(:rotate!)
+
+ def fill(value)
+ i = 0
+ while i < size
+ self[i] = value
+ i += 1
+ end
+ self
+ end unless [].respond_to?(:fill)
+
+ def transpose
+ return [] if empty?
+ width = self[0].size
+ result = []
+ i = 0
+ while i < width
+ row = []
+ j = 0
+ while j < size
+ row << self[j][i]
+ j += 1
+ end
+ result << row
+ i += 1
+ end
+ result
+ end unless [].respond_to?(:transpose)
+
+ def flatten
+ result = []
+ each do |value|
+ if value.is_a?(Array)
+ result.concat(value.flatten)
+ else
+ result << value
+ end
+ end
+ result
+ end unless [].respond_to?(:flatten)
+
+ def uniq!
+ seen = []
+ i = 0
+ while i < size
+ if seen.include?(self[i])
+ delete_at(i)
+ else
+ seen << self[i]
+ i += 1
+ end
+ end
+ self
+ end unless [].respond_to?(:uniq!)
+end
+
+class Hash
+ def compare_by_identity
+ self
+ end unless {}.respond_to?(:compare_by_identity)
+
+ def fetch(key, default_value = nil)
+ return self[key] if key?(key)
+ return default_value unless default_value.nil?
+ raise KeyError, "key not found: #{key.inspect}"
+ end unless {}.respond_to?(:fetch)
+end
diff --git a/sandbox/ChibiRuby.Benchmark/ruby/optcarrot b/sandbox/ChibiRuby.Benchmark/ruby/optcarrot
new file mode 160000
index 00000000..36a8f6b5
--- /dev/null
+++ b/sandbox/ChibiRuby.Benchmark/ruby/optcarrot
@@ -0,0 +1 @@
+Subproject commit 36a8f6b525ca13c8db244eed94b1361ec1394725
diff --git a/sig/array.rbs b/sig/array.rbs
index c1ea971b..abba89f7 100644
--- a/sig/array.rbs
+++ b/sig/array.rbs
@@ -99,6 +99,13 @@ class Array[Elem] < Object
# a # => [3, 2, 1]
def reverse!: () -> self
+ # Rotates self in place and returns self.
+ #
+ # a = [1, 2, 3, 4]
+ # a.rotate! # => [2, 3, 4, 1]
+ # a.rotate!(-1) # => [1, 2, 3, 4]
+ def rotate!: (?Integer) -> self
+
# Removes and returns the last element of self, or nil when empty.
#
# a = [1, 2, 3]
@@ -120,6 +127,12 @@ class Array[Elem] < Object
# a.clear # => []
def clear: () -> self
+ # Returns true when an element compares equal to the argument using ==.
+ #
+ # [1, 2, 3].include?(2) # => true
+ def include?: (untyped) -> bool
+ alias member? include?
+
# Returns the index of the first element equal to the given value, or nil when not found.
#
# [1, 2, 3, 2].index(2) # => 1
diff --git a/sig/basic_object.rbs b/sig/basic_object.rbs
index 5c7af717..e1dfd3af 100644
--- a/sig/basic_object.rbs
+++ b/sig/basic_object.rbs
@@ -29,6 +29,7 @@ class BasicObject
# "hello".__send__(:upcase) # => "HELLO"
# [1, 2, 3].__send__(:push, 4) # => [1, 2, 3, 4]
def __send__: (Symbol | String, *untyped) ?{ (*untyped) -> untyped } -> untyped
+ alias send __send__
# Evaluates the given block in the context of self, where self within the block refers to the receiver.
#
diff --git a/sig/integer.rbs b/sig/integer.rbs
index 84491769..4296a6b9 100644
--- a/sig/integer.rbs
+++ b/sig/integer.rbs
@@ -61,6 +61,12 @@ class Integer < Numeric
# -(-3) # => 3
def -@: () -> Integer
+ # Returns the bitwise complement of self.
+ #
+ # ~0 # => -1
+ # ~2 # => -3
+ def ~: () -> Integer
+
# Returns the integer quotient of self divided by the argument.
#
# 11.div(3) # => 3
@@ -182,6 +188,13 @@ class Integer < Numeric
# 20 >> 2 # => 5
def >>: (Integer) -> Integer
+ # Returns the bit at the given offset, using Ruby's infinite two's-complement integer semantics.
+ #
+ # 5[0] # => 1
+ # 5[1] # => 0
+ # (-1)[64] # => 1
+ def []: (Numeric) -> Integer
+
# --- from lib.rb ---
#
# Calls the given block once for each Integer
diff --git a/sig/kernel.rbs b/sig/kernel.rbs
index 7bcf3de3..47fe9fd1 100644
--- a/sig/kernel.rbs
+++ b/sig/kernel.rbs
@@ -108,6 +108,11 @@ module Kernel
def is_a?: (Module) -> bool
alias kind_of? is_a?
+ # Returns a bound Method object for the named method on self.
+ #
+ # "hi".method(:upcase).call # => "HI"
+ def method: (Symbol | String) -> Method
+
# Returns an integer identifier unique to self for its lifetime.
#
# a = "hi"
@@ -131,6 +136,16 @@ module Kernel
# sleep 1 # => 1
def sleep: (?Numeric) -> Integer
+ # Returns the value of the instance variable named name, or nil if it is not set.
+ #
+ # obj.instance_variable_get(:@x)
+ def instance_variable_get: (Symbol | String) -> untyped
+
+ # Sets the instance variable named name to value and returns value.
+ #
+ # obj.instance_variable_set(:@x, 1) # => 1
+ def instance_variable_set: (Symbol | String, untyped) -> untyped
+
# Removes and returns the value of the instance variable named name from self.
#
# class Foo
diff --git a/sig/method.rbs b/sig/method.rbs
new file mode 100644
index 00000000..98fa0ad8
--- /dev/null
+++ b/sig/method.rbs
@@ -0,0 +1,23 @@
+#
+# Source: ChibiRuby.StdLib.MethodMembers
+
+# A bound Ruby method object. It keeps the receiver and the resolved method entry captured by Kernel#method, then invokes that entry from call / [].
+class Method < Object
+ # Invokes the captured method with the given arguments and block.
+ def call: (*untyped) ?{ (*untyped) -> untyped } -> untyped
+ alias [] call
+
+ # Returns the method name.
+ def name: () -> Symbol
+
+ # Returns the receiver bound to this method.
+ def receiver: () -> untyped
+
+ # Returns true when both method objects wrap the same receiver and method entry.
+ def eql?: (untyped) -> bool
+ alias == eql?
+
+ # Returns a hash value consistent with eql?.
+ def hash: () -> Integer
+
+end
diff --git a/src/ChibiRuby/Internals/MRubyCallInfo.cs b/src/ChibiRuby/Internals/MRubyCallInfo.cs
index 701cd9c1..d2bd7997 100644
--- a/src/ChibiRuby/Internals/MRubyCallInfo.cs
+++ b/src/ChibiRuby/Internals/MRubyCallInfo.cs
@@ -230,6 +230,26 @@ public Span CurrentStack
}
}
+ ///
+ /// Slice the current call frame's positional arguments straight out of the VM stack
+ /// as a , taking a ref into the backing array so the common
+ /// (unpacked) path skips the bounds validation that Stack.AsSpan(start, length)
+ /// performs. Lets a C# builtin read its arguments without per-index
+ /// calls. The packed case (argc >= 15) falls back to the
+ /// packed array's own span. Valid only while the current frame is active and the stack
+ /// is not resized.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Span GetArgumentsSpan()
+ {
+ ref var callInfo = ref CurrentCallInfo;
+ if (callInfo.ArgumentPacked)
+ {
+ return StackAt(callInfo.StackPointer + 1).As().AsSpan();
+ }
+ return CreateSpan(ref StackAt(callInfo.StackPointer + 1), callInfo.ArgumentCount);
+ }
+
public MRubyContext()
{
CallStack[0] = new MRubyCallInfo();
@@ -371,23 +391,31 @@ public void ClearStack(int start, int count)
Stack.AsSpan(start, count).Clear();
}
+ ///
+ /// Unchecked ref into the VM stack backing array. The VM guarantees stack indices
+ /// derived from the active call frame are in range, so the per-access bounds check that
+ /// Stack[i] emits is pure overhead on the hot argument-reading path.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ ref MRubyValue StackAt(int index) => ref Unsafe.Add(ref GetArrayDataReference(Stack), index);
+
public int GetArgumentCount()
{
- ref var callInfo = ref CallStack[CallDepth];
+ ref var callInfo = ref CurrentCallInfo;
if (callInfo.ArgumentPacked)
{
- return Stack[callInfo.StackPointer + 1].As().Length;
+ return StackAt(callInfo.StackPointer + 1).As().Length;
}
return callInfo.ArgumentCount;
}
public int GetKeywordArgumentCount()
{
- ref var callInfo = ref CallStack[CallDepth];
+ ref var callInfo = ref CurrentCallInfo;
var offset = callInfo.KeywordArgumentOffset;
if (callInfo.KeywordArgumentPacked)
{
- return Stack[callInfo.StackPointer + offset].As().Length;
+ return StackAt(callInfo.StackPointer + offset).As().Length;
}
return callInfo.KeywordArgumentCount;
}
@@ -398,8 +426,8 @@ public bool TryGetArgumentAt(int index, out MRubyValue value)
ref var callInfo = ref CurrentCallInfo;
if (callInfo.ArgumentPacked)
{
- var args = Stack[callInfo.StackPointer + 1].As();
- if (index < args.Length)
+ var args = StackAt(callInfo.StackPointer + 1).As();
+ if ((uint)index < (uint)args.Length)
{
value = args[index];
return true;
@@ -407,9 +435,9 @@ public bool TryGetArgumentAt(int index, out MRubyValue value)
}
else
{
- if (index < CurrentCallInfo.ArgumentCount)
+ if ((uint)index < callInfo.ArgumentCount)
{
- value = Stack[callInfo.StackPointer + 1 + index];
+ value = StackAt(callInfo.StackPointer + 1 + index);
return true;
}
}
@@ -429,16 +457,17 @@ public bool TryGetKeywordArgument(Symbol key, out MRubyValue value)
if (callInfo.KeywordArgumentPacked)
{
- var kdict = Stack[callInfo.StackPointer + offset].As();
+ var kdict = StackAt(callInfo.StackPointer + offset).As();
return kdict.TryGetValue(new MRubyValue(key), out value);
}
+ ref var k0 = ref StackAt(callInfo.StackPointer + offset);
for (var i = 0; i < callInfo.KeywordArgumentCount; i++)
{
- var k = Stack[callInfo.StackPointer + offset + i * 2];
+ ref var k = ref Unsafe.Add(ref k0, i * 2);
if (k.SymbolValue == key)
{
- value = Stack[callInfo.StackPointer + offset + i * 2 + 1];
+ value = Unsafe.Add(ref k0, i * 2 + 1);
return true;
}
}
@@ -451,8 +480,7 @@ public bool TryGetKeywordArgument(Symbol key, out MRubyValue value)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MRubyValue GetSelf()
{
- ref var callInfo = ref CallStack[CallDepth];
- return Stack[callInfo.StackPointer];
+ return StackAt(CurrentCallInfo.StackPointer);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -465,7 +493,7 @@ public MRubyValue GetArgumentAt(int index)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MRubyValue GetKeywordArgument(Symbol key)
{
- ref var callInfo = ref CallStack[CallDepth];
+ ref var callInfo = ref CurrentCallInfo;
var offset = callInfo.KeywordArgumentOffset;
if (offset < 0)
{
@@ -474,16 +502,17 @@ public MRubyValue GetKeywordArgument(Symbol key)
if (callInfo.KeywordArgumentPacked)
{
- var hash = Stack[callInfo.StackPointer + offset].As();
+ var hash = StackAt(callInfo.StackPointer + offset).As();
return hash[new MRubyValue(key)];
}
+ ref var k0 = ref StackAt(callInfo.StackPointer + offset);
for (var i = 0; i < callInfo.KeywordArgumentCount; i++)
{
- var k = Stack[callInfo.StackPointer + offset + i];
+ ref var k = ref Unsafe.Add(ref k0, i);
if (k.SymbolValue == key)
{
- return Stack[callInfo.StackPointer + offset + i + 1];
+ return Unsafe.Add(ref k0, i + 1);
}
}
return MRubyValue.Nil;
@@ -497,7 +526,7 @@ public ReadOnlySpan GetRestArgumentsAfter(int startIndex) =>
public MRubyValue GetBlockArgument()
{
ref var callInfo = ref CurrentCallInfo;
- return Stack[callInfo.StackPointer + callInfo.BlockArgumentOffset];
+ return StackAt(callInfo.StackPointer + callInfo.BlockArgumentOffset);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -509,10 +538,12 @@ internal Span GetRestArgumentsAfter(ref MRubyCallInfo callInfo, int
}
if (callInfo.ArgumentPacked)
{
- var args = Stack[callInfo.StackPointer + 1].As();
+ var args = StackAt(callInfo.StackPointer + 1).As();
return startIndex >= args.Length ? default : args.AsSpan(startIndex);
}
- return Stack.AsSpan(callInfo.StackPointer + 1 + startIndex, callInfo.ArgumentCount - startIndex);
+ return CreateSpan(
+ ref StackAt(callInfo.StackPointer + 1 + startIndex),
+ callInfo.ArgumentCount - startIndex);
}
internal ReadOnlySpan> GetKeywordArgs(ref MRubyCallInfo callInfo)
diff --git a/src/ChibiRuby/Internals/Shims/MemoryMarshalEx.cs b/src/ChibiRuby/Internals/Shims/MemoryMarshalEx.cs
index 691e2e5b..2fa432d2 100644
--- a/src/ChibiRuby/Internals/Shims/MemoryMarshalEx.cs
+++ b/src/ChibiRuby/Internals/Shims/MemoryMarshalEx.cs
@@ -13,6 +13,12 @@ public static ref T GetArrayDataReference(T[] array)
return ref MemoryMarshal.GetReference(array.AsSpan());
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Span CreateSpan(ref T reference, int length)
+ {
+ return MemoryMarshal.CreateSpan(ref reference, length);
+ }
+
// GC
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T[] AllocateUninitializedArray(int length, bool pinned = false)
diff --git a/src/ChibiRuby/MRubyMethod.cs b/src/ChibiRuby/MRubyMethod.cs
index 98c57671..488b0534 100644
--- a/src/ChibiRuby/MRubyMethod.cs
+++ b/src/ChibiRuby/MRubyMethod.cs
@@ -68,11 +68,7 @@ public MRubyMethod(MRubyFunc? func, MRubyMethodVisibility visibility = MRubyMeth
public MRubyMethod WithVisibility(MRubyMethodVisibility visibility)
{
- if (TrivialGetterIVarSymbol.Value != 0)
- return new MRubyMethod(body!, Kind, visibility, TrivialGetterIVarSymbol);
- return Kind == MRubyMethodKind.RProc
- ? new MRubyMethod(Unsafe.As(body!), visibility)
- : new MRubyMethod(Unsafe.As(body!), visibility);
+ return new MRubyMethod(body!, Kind, visibility, TrivialGetterIVarSymbol);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
diff --git a/src/ChibiRuby/MRubyState.Arguments.cs b/src/ChibiRuby/MRubyState.Arguments.cs
index 686985eb..da55eda3 100644
--- a/src/ChibiRuby/MRubyState.Arguments.cs
+++ b/src/ChibiRuby/MRubyState.Arguments.cs
@@ -38,6 +38,14 @@ public bool TryGetKeywordArgument(Symbol key, out MRubyValue value) =>
public MRubyValue GetSelf() => Context.GetSelf();
+ ///
+ /// Fast ref-based slice of the current method's positional arguments as a
+ /// carved directly out of the VM stack. Prefer this over repeated
+ /// calls in hot C# builtins.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Span GetArgumentsSpan() => Context.GetArgumentsSpan();
+
public ReadOnlySpan> GetKeywordArguments() =>
Context.GetKeywordArgs(ref Context.CurrentCallInfo);
diff --git a/src/ChibiRuby/MRubyState.Init.cs b/src/ChibiRuby/MRubyState.Init.cs
index c937ddea..0b62796f 100644
--- a/src/ChibiRuby/MRubyState.Init.cs
+++ b/src/ChibiRuby/MRubyState.Init.cs
@@ -27,6 +27,7 @@ public static MRubyState Create()
state.InitSymbol();
state.InitString();
state.InitProc();
+ state.InitMethod();
state.InitBinding();
state.InitException();
state.InitNumeric();
@@ -52,6 +53,7 @@ public static MRubyState Create()
public RClass ClassClass { get; private set; } = default!;
public RClass ModuleClass { get; private set; } = default!;
public RClass ProcClass { get; private set; } = default!;
+ public RClass MethodClass { get; private set; } = default!;
public RClass StringClass { get; private set; } = default!;
public RClass ArrayClass { get; private set; } = default!;
public RClass HashClass { get; private set; } = default!;
@@ -194,6 +196,7 @@ void InitClass()
DefineMethod(BasicObjectClass, Names.OpEq, BasicObjectMembers.OpEq);
DefineMethod(BasicObjectClass, Intern("__id__"u8), BasicObjectMembers.Id);
DefineMethod(BasicObjectClass, Intern("__send__"u8), BasicObjectMembers.Send);
+ DefineMethod(BasicObjectClass, Intern("send"u8), BasicObjectMembers.Send);
DefineMethod(BasicObjectClass, Names.QEqual, BasicObjectMembers.OpEq);
DefineMethod(BasicObjectClass, Names.InstanceEval, BasicObjectMembers.InstanceEval);
DefineMethod(BasicObjectClass, Names.SingletonMethodAdded, MRubyMethod.Nop);
@@ -306,11 +309,14 @@ void InitKernel()
DefineMethod(KernelModule, Names.QKindOf, KernelMembers.KindOf);
DefineMethod(KernelModule, Intern("iterator?"u8), KernelMembers.BlockGiven);
DefineMethod(KernelModule, Intern("kind_of?"u8), KernelMembers.KindOf);
+ DefineMethod(KernelModule, Intern("method"u8), KernelMembers.Method);
DefineMethod(KernelModule, Names.Nil, MRubyMethod.Nop);
DefineMethod(KernelModule, Intern("object_id"u8), KernelMembers.ObjectId);
DefineMethod(KernelModule, Intern("p"u8), KernelMembers.P);
DefineMethod(KernelModule, Intern("print"u8), KernelMembers.Print);
DefineMethod(KernelModule, Intern("sleep"u8), KernelMembers.Sleep);
+ DefineMethod(KernelModule, Intern("instance_variable_get"u8), KernelMembers.InstanceVariableGet);
+ DefineMethod(KernelModule, Intern("instance_variable_set"u8), KernelMembers.InstanceVariableSet);
DefineMethod(KernelModule, Intern("remove_instance_variable"u8), KernelMembers.RemoveInstanceVariable);
DefineMethod(KernelModule, Names.QRespondTo, KernelMembers.RespondTo);
DefineMethod(KernelModule, Names.QRespondToMissing, MRubyMethod.False);
@@ -368,6 +374,20 @@ void InitProc()
DefineMethod(ProcClass, Names.OpAref, callMethod);
}
+ void InitMethod()
+ {
+ MethodClass = DefineClass(Intern("Method"u8), ObjectClass);
+ UndefClassMethod(MethodClass, Names.New);
+
+ DefineMethod(MethodClass, Names.Call, MethodMembers.Call);
+ DefineMethod(MethodClass, Names.OpAref, MethodMembers.Call);
+ DefineMethod(MethodClass, Names.Name, MethodMembers.Name);
+ DefineMethod(MethodClass, Intern("receiver"u8), MethodMembers.Receiver);
+ DefineMethod(MethodClass, Names.OpEq, MethodMembers.Eql);
+ DefineMethod(MethodClass, Names.QEql, MethodMembers.Eql);
+ DefineMethod(MethodClass, Names.Hash, MethodMembers.Hash);
+ }
+
void InitBinding()
{
BindingClass = DefineClass(Intern("Binding"u8), ObjectClass);
@@ -432,6 +452,7 @@ void InitNumeric()
DefineMethod(IntegerClass, Names.OpMod, IntegerMembers.Mod);
DefineMethod(IntegerClass, Names.OpPlus, IntegerMembers.OpPlus);
DefineMethod(IntegerClass, Names.OpMinus, IntegerMembers.OpMinus);
+ DefineMethod(IntegerClass, Names.OpNeg, IntegerMembers.OpNeg);
DefineMethod(IntegerClass, Intern("div"u8), IntegerMembers.IntDiv);
DefineMethod(IntegerClass, Intern("fdiv"u8), IntegerMembers.FDiv);
DefineMethod(IntegerClass, Intern("abs"u8), IntegerMembers.Abs);
@@ -455,6 +476,7 @@ void InitNumeric()
DefineMethod(IntegerClass, Names.OpXor, IntegerMembers.OpXor);
DefineMethod(IntegerClass, Names.OpLShift, IntegerMembers.OpLShift);
DefineMethod(IntegerClass, Names.OpRShift, IntegerMembers.OpRShift);
+ DefineMethod(IntegerClass, Names.OpAref, IntegerMembers.OpAref);
DefineMethod(IntegerClass, Names.OpCmp, NumericMembers.OpCmp);
DefineMethod(IntegerClass, Names.OpLt, NumericMembers.OpLt);
DefineMethod(IntegerClass, Names.OpLe, NumericMembers.OpLe);
@@ -597,9 +619,12 @@ void InitArray()
DefineMethod(ArrayClass, Intern("last"u8), ArrayMembers.Last);
DefineMethod(ArrayClass, Intern("reverse"u8), ArrayMembers.Reverse);
DefineMethod(ArrayClass, Intern("reverse!"u8), ArrayMembers.ReverseBang);
+ DefineMethod(ArrayClass, Intern("rotate!"u8), ArrayMembers.RotateBang);
DefineMethod(ArrayClass, Intern("pop"u8), ArrayMembers.Pop);
DefineMethod(ArrayClass, Intern("delete_at"u8), ArrayMembers.DeleteAt);
DefineMethod(ArrayClass, Intern("clear"u8), ArrayMembers.Clear);
+ DefineMethod(ArrayClass, Intern("include?"u8), ArrayMembers.Include);
+ DefineMethod(ArrayClass, Intern("member?"u8), ArrayMembers.Include);
DefineMethod(ArrayClass, Intern("index"u8), ArrayMembers.Index);
DefineMethod(ArrayClass, Intern("rindex"u8), ArrayMembers.RIndex);
DefineMethod(ArrayClass, Intern("join"u8), ArrayMembers.Join);
diff --git a/src/ChibiRuby/MRubyState.Vm.cs b/src/ChibiRuby/MRubyState.Vm.cs
index 6eac5102..d836d263 100644
--- a/src/ChibiRuby/MRubyState.Vm.cs
+++ b/src/ChibiRuby/MRubyState.Vm.cs
@@ -195,6 +195,73 @@ public MRubyValue Send(
}
}
+ internal MRubyValue CallResolvedMethod(
+ MRubyValue self,
+ Symbol methodId,
+ MRubyMethod method,
+ RClass owner,
+ ReadOnlySpan args,
+ ReadOnlySpan> kargs,
+ RProc? block)
+ {
+ ref var currentCallInfo = ref Context.CurrentCallInfo;
+ var nextStackPointer = currentCallInfo.StackPointer + currentCallInfo.NumberOfRegisters;
+
+ var stackSize = MRubyCallInfo.CalculateBlockArgumentOffset(
+ args.Length,
+ kargs.IsEmpty ? 0 : MRubyCallInfo.CallMaxArgs) + 1;
+ Context.ExtendStack(nextStackPointer + stackSize);
+
+ var nextStack = Context.Stack.AsSpan(nextStackPointer);
+ ref var nextCallInfo = ref Context.PushCallStack();
+ nextCallInfo.StackPointer = nextStackPointer;
+ nextCallInfo.Scope = owner;
+ nextCallInfo.ArgumentCount = (byte)args.Length;
+ nextCallInfo.KeywordArgumentCount = (byte)kargs.Length;
+ nextCallInfo.MethodId = methodId;
+
+ nextStack[0] = self;
+ if (!args.IsEmpty)
+ {
+ if (args.Length >= MRubyCallInfo.CallMaxArgs)
+ {
+ throw new NotImplementedException();
+ }
+ args.CopyTo(nextStack[1..]);
+ }
+
+ if (!kargs.IsEmpty)
+ {
+ var kargOffset = MRubyCallInfo.CalculateKeywordArgumentOffset(args.Length, kargs.Length);
+ var kdict = NewHash(kargs.Length);
+ foreach (var (key, value) in kargs)
+ {
+ kdict.Add(key, value);
+ }
+
+ nextStack[kargOffset] = kdict;
+ nextCallInfo.MarkAsKeywordArgumentPacked();
+ }
+
+ nextStack[stackSize - 1] = block != null ? new MRubyValue(block) : default;
+ nextCallInfo.Proc = method.Proc;
+
+ if (method.Kind == MRubyMethodKind.CSharpFunc)
+ {
+ nextCallInfo.CallerType = CallerType.MethodCalled;
+ nextCallInfo.ProgramCounter = 0;
+
+ var result = method.Invoke(this, self);
+ Context.PopCallStack();
+ return result;
+ }
+
+ var irepProc = nextCallInfo.Proc!;
+ nextCallInfo.CallerType = CallerType.VmExecuted;
+ nextCallInfo.ProgramCounter = irepProc.ProgramCounter;
+ return Execute(irepProc.Irep, irepProc.ProgramCounter, nextCallInfo.BlockArgumentOffset + 1);
+ }
+
public MRubyValue YieldWithClass(
RClass c,
MRubyValue self,
@@ -1714,6 +1781,70 @@ static void Super(MRubyState state, ref MRubyCallInfo callInfo, Span
callInfo.KeywordArgumentCount = (byte)((bb.B >> 4) & 0xf);
}
}
+ case OpCode.ArgAry:
+ {
+ Markers.ArgAry();
+
+ bs = OperandBS.Read(ref sequence, ref callInfo.ProgramCounter);
+ var bits = (ushort)bs.B;
+ var m1 = (bits >> 11) & 0x3f;
+ var r = (bits >> 10) & 0x1;
+ var m2 = (bits >> 5) & 0x1f;
+ var kd = (bits >> 4) & 0x1;
+ var lv = bits & 0xf;
+
+ Span sourceStack;
+ if (lv == 0)
+ {
+ sourceStack = Context.Stack.AsSpan(callInfo.StackPointer + 1);
+ }
+ else
+ {
+ var env = callInfo.Proc?.FindUpperEnvTo(lv - 1);
+ if (env is null || env.Stack.Length <= m1 + r + m2 + 1)
+ {
+ Raise(Names.NoMethodError, "super called outside of method"u8);
+ }
+ sourceStack = env!.Stack[1..];
+ }
+
+ var result = r == 0
+ ? NewArray(sourceStack.Slice(0, m1 + m2))
+ : NewArrayFromArgumentArray(sourceStack, m1, m2);
+
+ Unsafe.Add(ref registers, bs.A) = result;
+ if (kd != 0)
+ {
+ Unsafe.Add(ref registers, bs.A + 1) = sourceStack[m1 + r + m2];
+ Unsafe.Add(ref registers, bs.A + 2) = sourceStack[m1 + r + m2 + 1];
+ }
+
+ goto Next;
+
+ RArray NewArrayFromArgumentArray(Span sourceStack, int m1, int m2)
+ {
+ var rest = sourceStack[m1].Object as RArray;
+ var resultLength = m1 + (rest?.Length ?? 0) + m2;
+ var array = NewArray(resultLength);
+
+ if (m1 > 0)
+ {
+ array.PushRange(sourceStack[..m1]);
+ }
+
+ if (rest is not null)
+ {
+ array.PushRange(rest.AsSpan());
+ }
+
+ if (m2 > 0)
+ {
+ array.PushRange(sourceStack.Slice(m1 + 1, m2));
+ }
+
+ return array;
+ }
+ }
case OpCode.Enter:
{
Markers.Enter();
diff --git a/src/ChibiRuby/RArray.cs b/src/ChibiRuby/RArray.cs
index c27ae8b0..a0ec6ade 100644
--- a/src/ChibiRuby/RArray.cs
+++ b/src/ChibiRuby/RArray.cs
@@ -3,6 +3,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
+#if NET7_0_OR_GREATER
+using static System.Runtime.InteropServices.MemoryMarshal;
+#else
+using static ChibiRuby.Internal.MemoryMarshalEx;
+#endif
namespace ChibiRuby;
@@ -29,17 +34,43 @@ public MRubyValue this[int index]
}
if ((uint)index < (uint)Length)
{
- return data[offset + index];
+ return Unsafe.Add(ref GetArrayDataReference(data), offset + index);
}
return MRubyValue.Nil;
}
set
{
+ if (index < 0)
+ {
+ index += Length;
+ }
MakeModifiable(index + 1, index >= Length);
- data[offset + index] = value;
+ Unsafe.Add(ref GetArrayDataReference(data), offset + index) = value;
}
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Set(int index, MRubyValue value)
+ {
+ if (index < 0)
+ {
+ index += Length;
+ }
+ var length = Length;
+ if ((uint)index < (uint)length)
+ {
+ if (!dataOwned)
+ {
+ MakeModifiable(length);
+ }
+ Unsafe.Add(ref GetArrayDataReference(data), offset + index) = value;
+ return;
+ }
+
+ MakeModifiable(index + 1, index >= length);
+ Unsafe.Add(ref GetArrayDataReference(data), offset + index) = value;
+ }
+
MRubyValue[] data;
int offset;
bool dataOwned;
@@ -65,30 +96,31 @@ internal RArray(int capacity, RClass arrayClass) : base(MRubyVType.Array, arrayC
{
Length = 0;
offset = 0;
- data = new MRubyValue[capacity];
+ data = capacity == 0 ? [] : new MRubyValue[capacity];
dataOwned = true;
}
RArray(RArray shared)
- : this(shared, 0, shared.Length)
+ : this(shared, 0, shared.Length, shared.Class)
{
}
- RArray(RArray shared, int offset, int size) : base(MRubyVType.Array, shared.Class)
+ RArray(RArray shared, int start, int size, RClass klass) : base(MRubyVType.Array, klass)
{
- if (offset < 0)
+ if (start < 0)
{
- throw new ArgumentOutOfRangeException(nameof(offset));
+ throw new ArgumentOutOfRangeException(nameof(start));
}
- if (size > shared.Length)
+ if (size > shared.Length - start)
{
- size = shared.Length;
+ size = shared.Length - start;
}
Length = size;
- this.offset = offset;
+ offset = shared.offset + start;
data = shared.data;
dataOwned = false;
+ shared.dataOwned = false;
}
public override string ToString()
@@ -101,7 +133,49 @@ public override string ToString()
public RArray SubSequence(int start, int length)
{
- return new RArray(this, start, length);
+ return CopySubSequence(start, length, Class);
+ }
+
+ internal RArray CopySubSequence(int start, int length, RClass arrayClass)
+ {
+ NormalizeSubSequence(ref start, ref length);
+ if (length <= 0)
+ {
+ return new RArray(0, arrayClass);
+ }
+ var result = new RArray(length, arrayClass)
+ {
+ Length = length
+ };
+ Array.Copy(data, offset + start, result.data, 0, length);
+ return result;
+ }
+
+ internal RArray SharedSubSequence(int start, int length, RClass arrayClass)
+ {
+ NormalizeSubSequence(ref start, ref length);
+ if (length <= 0)
+ {
+ return new RArray(0, arrayClass);
+ }
+ return new RArray(this, start, length, arrayClass);
+ }
+
+ void NormalizeSubSequence(ref int start, ref int length)
+ {
+ if (start < 0)
+ {
+ length += start;
+ start = 0;
+ }
+ if (start > Length)
+ {
+ start = Length;
+ }
+ if (length > Length - start)
+ {
+ length = Length - start;
+ }
}
public void Clear()
@@ -121,7 +195,7 @@ public void Push(MRubyValue newItem)
{
var currentLength = Length;
MakeModifiable(currentLength + 1, true);
- data[currentLength] = newItem;
+ Unsafe.Add(ref GetArrayDataReference(data), offset + currentLength) = newItem;
}
public bool TryPop(out MRubyValue value)
@@ -132,7 +206,7 @@ public bool TryPop(out MRubyValue value)
return false;
}
- value = data[offset + Length - 1];
+ value = Unsafe.Add(ref GetArrayDataReference(data), offset + Length - 1);
MakeModifiable(Length - 1, true);
return true;
}
@@ -151,10 +225,7 @@ public RArray Shift(int n)
if (Length <= 0 || n <= 0) return new RArray(0, Class);
if (n > Length) n = Length;
- var result = new RArray(this)
- {
- Length = n
- };
+ var result = SharedSubSequence(0, n, Class);
offset += n;
Length -= n;
return result;
@@ -173,11 +244,18 @@ public void Unshift(ReadOnlySpan newItems)
public void Concat(RArray other)
{
+ if (other.Length <= 0)
+ {
+ return;
+ }
+
if (Length <= 0)
{
Length = other.Length;
data = other.data;
+ offset = other.offset;
dataOwned = false;
+ other.dataOwned = false;
return;
}
@@ -193,7 +271,8 @@ public MRubyValue DeleteAt(int index)
if (index < 0) index += Length;
if (index < 0 || index >= Length) return MRubyValue.Nil;
- var value = data[offset + index];
+ var value = Unsafe.Add(ref GetArrayDataReference(data), offset + index);
+ MakeModifiable(Length);
var src = AsSpan(index + 1);
var dst = AsSpan(index);
src.CopyTo(dst);
@@ -203,14 +282,17 @@ public MRubyValue DeleteAt(int index)
public void CopyTo(RArray other)
{
- other.MakeModifiable(Length);
- other.Length = Length;
+ if (ReferenceEquals(this, other))
+ {
+ return;
+ }
+
+ other.MakeModifiable(Length, true);
AsSpan().CopyTo(other.AsSpan());
}
public void ReplaceTo(RArray other)
{
- other.Length = 0;
CopyTo(other);
}
@@ -231,34 +313,28 @@ internal void PushRange(ReadOnlySpannewItems)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void MakeModifiable(int capacity, bool expandLength = false)
{
- if (data.Length - offset < capacity)
+ if (capacity < 0)
{
- var newLength = data.Length * 2;
- if (newLength - offset < capacity)
- {
- newLength = capacity;
- }
+ throw new ArgumentOutOfRangeException(nameof(capacity));
+ }
- if (dataOwned)
+ if (dataOwned)
+ {
+ if (offset == 0)
{
- Array.Resize(ref data, newLength);
+ if (data.Length < capacity)
+ {
+ Array.Resize(ref data, CalculateCapacity(data.Length, capacity));
+ }
}
- else
+ else if (data.Length - offset < capacity)
{
- var newData = new MRubyValue[newLength];
- data.AsSpan(offset).CopyTo(newData);
- data = newData;
- offset = 0;
- dataOwned = true;
+ Compact(capacity, expandLength);
}
}
- else if (!dataOwned)
+ else
{
- var newData = new MRubyValue[data.Length];
- data.AsSpan(offset).CopyTo(newData);
- data = newData;
- offset = 0;
- dataOwned = true;
+ Compact(capacity, expandLength);
}
if (expandLength)
@@ -267,6 +343,30 @@ internal void MakeModifiable(int capacity, bool expandLength = false)
}
}
+ void Compact(int capacity, bool expandLength)
+ {
+ var targetLength = expandLength ? capacity : Length;
+ var copyLength = Math.Min(Length, targetLength);
+ var newData = new MRubyValue[CalculateCapacity(copyLength, capacity)];
+ if (copyLength > 0)
+ {
+ data.AsSpan(offset, copyLength).CopyTo(newData);
+ }
+ data = newData;
+ offset = 0;
+ dataOwned = true;
+ }
+
+ static int CalculateCapacity(int currentCapacity, int requiredCapacity)
+ {
+ var newCapacity = currentCapacity * 2;
+ if (newCapacity < requiredCapacity)
+ {
+ newCapacity = requiredCapacity;
+ }
+ return newCapacity;
+ }
+
public struct Enumerator(RArray source) : IEnumerator
{
public MRubyValue Current { get; private set; }
diff --git a/src/ChibiRuby/RMethod.cs b/src/ChibiRuby/RMethod.cs
new file mode 100644
index 00000000..402d4214
--- /dev/null
+++ b/src/ChibiRuby/RMethod.cs
@@ -0,0 +1,29 @@
+using System;
+
+namespace ChibiRuby;
+
+public sealed class RMethod(
+ MRubyValue receiver,
+ Symbol methodId,
+ RClass owner,
+ MRubyMethod method,
+ RClass klass)
+ : RObject(klass)
+{
+ public MRubyValue Receiver { get; } = receiver;
+ public Symbol MethodId { get; } = methodId;
+ public RClass Owner { get; } = owner;
+ public MRubyMethod Method { get; } = method;
+
+ internal override RObject Clone()
+ {
+ var clone = new RMethod(Receiver, MethodId, Owner, Method, Class);
+ InstanceVariables.CopyTo(clone.InstanceVariables);
+ return clone;
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(Receiver, Owner, Method);
+ }
+}
diff --git a/src/ChibiRuby/StdLib/ArrayMembers.cs b/src/ChibiRuby/StdLib/ArrayMembers.cs
index 42cd9c75..6220d531 100644
--- a/src/ChibiRuby/StdLib/ArrayMembers.cs
+++ b/src/ChibiRuby/StdLib/ArrayMembers.cs
@@ -101,18 +101,23 @@ public static MRubyValue OpAset(MRubyState state, MRubyValue self)
var array = self.As();
state.EnsureNotFrozen(array);
- var argc = state.GetArgumentCount();
- switch (argc)
+ var args = state.GetArgumentsSpan();
+ switch (args.Length)
{
case 2:
- var key = state.GetArgumentAt(0);
- var val = state.GetArgumentAt(1);
+ var key = args[0];
+ var val = args[1];
+ if (key.IsFixnum)
+ {
+ array.Set((int)key.FixnumValue, val);
+ return val;
+ }
if (key.Object is RRange range)
{
switch (range.Calculate(array.Length, false, out var calculatedIndex, out var calculatedLength))
{
case RangeCalculateResult.TypeMismatch:
- array[(int)state.AsInteger(key)] = val;
+ array.Set((int)state.AsInteger(key), val);
break;
case RangeCalculateResult.Ok:
state.SpliceArray(array, calculatedIndex, calculatedLength, val);
@@ -126,18 +131,20 @@ public static MRubyValue OpAset(MRubyState state, MRubyValue self)
}
else
{
- array[(int)state.AsInteger(key)] = val;
+ array.Set((int)state.AsInteger(key), val);
}
return val;
case 3:
// a[n,m] = v
- var n = state.GetArgumentAsIntegerAt(0);
- var m = state.GetArgumentAsIntegerAt(1);
- var v = state.GetArgumentAt(2);
+ var nArg = args[0];
+ var mArg = args[1];
+ var n = nArg.IsFixnum ? nArg.FixnumValue : state.AsInteger(nArg);
+ var m = mArg.IsFixnum ? mArg.FixnumValue : state.AsInteger(mArg);
+ var v = args[2];
state.SpliceArray(array, (int)n, (int)m, v);
return v;
default:
- state.RaiseArgumentNumberError(argc, 2, 3);
+ state.RaiseArgumentNumberError(args.Length, 2, 3);
return default;
}
}
@@ -177,6 +184,17 @@ public static MRubyValue Push(MRubyState state, MRubyValue self)
var array = self.As();
state.EnsureNotFrozen(array);
+ var argc = state.GetArgumentCount();
+ if (argc == 0)
+ {
+ return self;
+ }
+ if (argc == 1)
+ {
+ array.Push(state.GetArgumentAt(0));
+ return self;
+ }
+
var args = state.GetRestArgumentsAfter(0);
var start = array.Length;
@@ -509,6 +527,7 @@ public static MRubyValue Reverse(MRubyState state, MRubyValue self)
public static MRubyValue ReverseBang(MRubyState state, MRubyValue self)
{
var array = self.As();
+ state.EnsureNotFrozen(array);
var span = array.AsSpan();
var left = 0;
@@ -522,6 +541,71 @@ public static MRubyValue ReverseBang(MRubyState state, MRubyValue self)
return self;
}
+ ///
+ /// Rotates self in place and returns self.
+ ///
+ ///
+ ///
+ /// a = [1, 2, 3, 4]
+ /// a.rotate! # => [2, 3, 4, 1]
+ /// a.rotate!(-1) # => [1, 2, 3, 4]
+ ///
+ ///
+ [RubyDef("(?Integer) -> self")]
+ public static MRubyValue RotateBang(MRubyState state, MRubyValue self)
+ {
+ var array = self.As();
+ state.EnsureNotFrozen(array);
+
+ var length = array.Length;
+ if (length <= 1)
+ {
+ return self;
+ }
+
+ var count = state.TryGetArgumentAt(0, out var arg0)
+ ? state.AsInteger(arg0)
+ : 1;
+ count %= length;
+ if (count < 0)
+ {
+ count += length;
+ }
+ if (count == 0)
+ {
+ return self;
+ }
+
+ array.MakeModifiable(length);
+ var span = array.AsSpan();
+ span[..(int)count].Reverse();
+ span[(int)count..].Reverse();
+ span.Reverse();
+ return self;
+ }
+
+ ///
+ /// Returns true when an element compares equal to the argument using ==.
+ ///
+ ///
+ ///
+ /// [1, 2, 3].include?(2) # => true
+ ///
+ ///
+ [RubyDef("(untyped) -> bool")]
+ public static MRubyValue Include(MRubyState state, MRubyValue self)
+ {
+ var item = state.GetArgumentAt(0);
+ foreach (var value in self.As().AsSpan())
+ {
+ if (state.ValueEquals(value, item))
+ {
+ return MRubyValue.True;
+ }
+ }
+ return MRubyValue.False;
+ }
+
///
/// Removes and returns the element at the given index, or nil when the index is out of range.
///
diff --git a/src/ChibiRuby/StdLib/IntegerMembers.cs b/src/ChibiRuby/StdLib/IntegerMembers.cs
index b6c83700..76dd84dc 100644
--- a/src/ChibiRuby/StdLib/IntegerMembers.cs
+++ b/src/ChibiRuby/StdLib/IntegerMembers.cs
@@ -394,6 +394,33 @@ public static MRubyValue OpRShift(MRubyState state, MRubyValue self)
return default;
}
+ ///
+ /// Returns the bit at the given offset, using Ruby's infinite two's-complement integer semantics.
+ ///
+ ///
+ ///
+ /// 5[0] # => 1
+ /// 5[1] # => 0
+ /// (-1)[64] # => 1
+ ///
+ ///
+ [RubyDef("(Numeric) -> Integer")]
+ public static MRubyValue OpAref(MRubyState state, MRubyValue self)
+ {
+ var value = self.IsFixnum ? self.FixnumValue : state.AsInteger(self);
+ var offsetValue = state.GetArgumentAt(0);
+ var offset = offsetValue.IsFixnum ? offsetValue.FixnumValue : state.AsInteger(offsetValue);
+ if (offset < 0)
+ {
+ return 0;
+ }
+ if (offset > 8 * sizeof(long) - 1)
+ {
+ return value < 0 ? 1 : 0;
+ }
+ return (value >> (int)offset) & 1;
+ }
+
///
/// Returns the string representation of self in the given base (default 10).
@@ -444,6 +471,18 @@ public static MRubyValue ToS(MRubyState state, MRubyValue self)
[RubyDef("() -> Integer")]
public static MRubyValue OpMinus(MRubyState state, MRubyValue self) => -self.IntegerValue;
+ ///
+ /// Returns the bitwise complement of self.
+ ///
+ ///
+ ///
+ /// ~0 # => -1
+ /// ~2 # => -3
+ ///
+ ///
+ [RubyDef("() -> Integer")]
+ public static MRubyValue OpNeg(MRubyState state, MRubyValue self) => ~state.AsInteger(self);
+
///
/// Returns the absolute value of self.
///
@@ -826,7 +865,7 @@ internal static bool NumShift(MRubyState state, long val, long width, out long n
if (width < 0)
{
/* rshift */
- if (width == long.MinValue || -width >= (sizeof(long) - 1))
+ if (width == long.MinValue || -width > numericShiftWidthMax)
{
if (val < 0)
{
@@ -844,8 +883,8 @@ internal static bool NumShift(MRubyState state, long val, long width, out long n
}
else if (val > 0)
{
- if ((width > numericShiftWidthMax) ||
- (val > (long.MaxValue >> (int)width)))
+ if (width > numericShiftWidthMax ||
+ val > long.MaxValue >> (int)width)
{
num = default;
return false;
@@ -872,7 +911,7 @@ internal static bool NumShift(MRubyState state, long val, long width, out long n
return true;
}
- internal static MRubyValue PrepareIntRounding(MRubyState state, MRubyValue x)
+ static MRubyValue PrepareIntRounding(MRubyState state, MRubyValue x)
{
if (state.GetArgumentCount() <= 1)
{
@@ -887,7 +926,7 @@ internal static MRubyValue PrepareIntRounding(MRubyState state, MRubyValue x)
return IntPow(state, 10, -other);
}
- internal static void IntDivMod(MRubyState state, long x, long y, out long divp, out long modp)
+ static void IntDivMod(MRubyState state, long x, long y, out long divp, out long modp)
{
if (y == 0)
{
@@ -896,25 +935,24 @@ internal static void IntDivMod(MRubyState state, long x, long y, out long divp,
Unsafe.SkipInit(out modp);
return;
}
- else if (x == int.MinValue && y == -1)
+
+ if (x == int.MinValue && y == -1)
{
RaiseIntegerOverflowError(state, "division"u8);
Unsafe.SkipInit(out divp);
Unsafe.SkipInit(out modp);
return;
}
- else
- {
- long div = x / y;
- long mod = x - div * y;
- if ((x ^ y) < 0 && x != div * y)
- {
- mod += y;
- div -= 1;
- }
- divp = div;
- modp = mod;
+ var div = x / y;
+ var mod = x - div * y;
+
+ if ((x ^ y) < 0 && x != div * y)
+ {
+ mod += y;
+ div -= 1;
}
+ divp = div;
+ modp = mod;
}
-}
\ No newline at end of file
+}
diff --git a/src/ChibiRuby/StdLib/KernelMembers.cs b/src/ChibiRuby/StdLib/KernelMembers.cs
index 6aba7e4e..ce7f48ba 100644
--- a/src/ChibiRuby/StdLib/KernelMembers.cs
+++ b/src/ChibiRuby/StdLib/KernelMembers.cs
@@ -312,6 +312,30 @@ public static MRubyValue Hash(MRubyState state, MRubyValue self)
return self.ObjectId;
}
+ ///
+ /// Returns a bound Method object for the named method on self.
+ ///
+ ///
+ ///
+ /// "hi".method(:upcase).call # => "HI"
+ ///
+ ///
+ [RubyDef("(Symbol | String) -> Method")]
+ public static MRubyValue Method(MRubyState state, MRubyValue self)
+ {
+ state.EnsureArgumentCount(1);
+
+ var methodId = state.GetArgumentAsSymbolAt(0);
+ var receiverClass = state.ClassOf(self);
+ if (!state.TryFindMethod(receiverClass, methodId, out var method, out var owner) ||
+ method == MRubyMethod.Undef)
+ {
+ state.RaiseNameError(methodId, state.NewString($"undefined method '{state.NameOf(methodId)}' for class '{state.ClassNameOf(self)}'"));
+ }
+
+ return new RMethod(self, methodId, owner, method, state.MethodClass);
+ }
+
///
/// Initializer for object copies. Invoked by clone and dup. Raises TypeError
/// if the source object is of a different class.
@@ -466,14 +490,55 @@ public static MRubyValue P(MRubyState state, MRubyValue self)
public static MRubyValue RemoveInstanceVariable(MRubyState state, MRubyValue self)
{
var name = state.GetArgumentAsSymbolAt(0);
+ state.EnsureInstanceVariableName(name);
+ if (self.Object is not RObject obj)
+ {
+ return MRubyValue.Undef;
+ }
+
+ return state.RemoveInstanceVariable(obj, name);
+ }
+
+ ///
+ /// Returns the value of the instance variable named name, or nil if it is not set.
+ ///
+ ///
+ ///
+ /// obj.instance_variable_get(:@x)
+ ///
+ ///
+ [RubyDef("(Symbol | String) -> untyped")]
+ public static MRubyValue InstanceVariableGet(MRubyState state, MRubyValue self)
+ {
+ var name = state.GetArgumentAsSymbolAt(0);
+ state.EnsureInstanceVariableName(name);
+ return self.Object is RObject obj
+ ? state.GetInstanceVariable(obj, name)
+ : MRubyValue.Nil;
+ }
+
+ ///
+ /// Sets the instance variable named name to value and returns value.
+ ///
+ ///
+ ///
+ /// obj.instance_variable_set(:@x, 1) # => 1
+ ///
+ ///
+ [RubyDef("(Symbol | String, untyped) -> untyped")]
+ public static MRubyValue InstanceVariableSet(MRubyState state, MRubyValue self)
+ {
+ var name = state.GetArgumentAsSymbolAt(0);
+ state.EnsureInstanceVariableName(name);
+ var value = state.GetArgumentAt(1);
if (self.Object is RObject obj)
{
- if (obj.InstanceVariables.Remove(name, out var v))
- {
- return v;
- }
+ state.SetInstanceVariable(obj, name, value);
+ return value;
}
- return MRubyValue.Undef;
+
+ state.Raise(Names.TypeError, $"can't set instance variable on {state.ClassNameOf(self)}");
+ return MRubyValue.Nil;
}
///
diff --git a/src/ChibiRuby/StdLib/MethodMembers.cs b/src/ChibiRuby/StdLib/MethodMembers.cs
new file mode 100644
index 00000000..70e75d2a
--- /dev/null
+++ b/src/ChibiRuby/StdLib/MethodMembers.cs
@@ -0,0 +1,67 @@
+namespace ChibiRuby.StdLib;
+
+///
+/// A bound Ruby method object. It keeps the receiver and the resolved method
+/// entry captured by Kernel#method, then invokes that entry from
+/// call / [].
+///
+[RubyClass("Method")]
+static class MethodMembers
+{
+ ///
+ /// Invokes the captured method with the given arguments and block.
+ ///
+ [RubyDef("(*untyped) ?{ (*untyped) -> untyped } -> untyped")]
+ public static MRubyValue Call(MRubyState state, MRubyValue self)
+ {
+ var method = self.As();
+ var args = state.GetRestArgumentsAfter(0);
+ var kargs = state.GetKeywordArguments();
+ var block = state.GetBlockArgument();
+ return state.CallResolvedMethod(method.Receiver, method.MethodId, method.Method, method.Owner, args, kargs, block);
+ }
+
+ ///
+ /// Returns the method name.
+ ///
+ [RubyDef("() -> Symbol")]
+ public static MRubyValue Name(MRubyState state, MRubyValue self)
+ {
+ return self.As().MethodId;
+ }
+
+ ///
+ /// Returns the receiver bound to this method.
+ ///
+ [RubyDef("() -> untyped")]
+ public static MRubyValue Receiver(MRubyState state, MRubyValue self)
+ {
+ return self.As().Receiver;
+ }
+
+ ///
+ /// Returns true when both method objects wrap the same receiver and method entry.
+ ///
+ [RubyDef("(untyped) -> bool")]
+ public static MRubyValue Eql(MRubyState state, MRubyValue self)
+ {
+ if (state.GetArgumentAt(0).Object is not RMethod other)
+ {
+ return MRubyValue.False;
+ }
+
+ var method = self.As();
+ return method.Receiver == other.Receiver &&
+ method.Owner == other.Owner &&
+ method.Method == other.Method;
+ }
+
+ ///
+ /// Returns a hash value consistent with eql?.
+ ///
+ [RubyDef("() -> Integer")]
+ public static MRubyValue Hash(MRubyState state, MRubyValue self)
+ {
+ return self.As().GetHashCode();
+ }
+}
diff --git a/tests/ChibiRuby.Tests/VmTest.cs b/tests/ChibiRuby.Tests/VmTest.cs
index 800f7774..f0a729bc 100644
--- a/tests/ChibiRuby.Tests/VmTest.cs
+++ b/tests/ChibiRuby.Tests/VmTest.cs
@@ -449,9 +449,191 @@ def show(verbose:, mode:) = "verbose=#{verbose} mode=#{mode}"
Assert.That(mrb.Stringify(result).ToString(), Is.EqualTo("verbose=true mode=fast"));
}
+ [Test]
+ public void RubySend_DispatchesRubyMethodWithMultipleArguments()
+ {
+ var result = Exec("""
+ class SenderProbe
+ attr_reader :seen
+
+ def target(a, b)
+ @seen = [a, b]
+ 123
+ end
+
+ def call_target
+ send(:target, true, false)
+ end
+ end
+
+ probe = SenderProbe.new
+ [probe.call_target, probe.seen]
+ """u8).As();
+
+ Assert.That(result[0], Is.EqualTo(new MRubyValue(123)));
+
+ var seen = result[1].As();
+ Assert.That(seen[0], Is.EqualTo(MRubyValue.True));
+ Assert.That(seen[1], Is.EqualTo(MRubyValue.False));
+ }
+
+ [Test]
+ public void RubySend_DispatchesRubyMethodWithKeywordArguments()
+ {
+ var result = Exec("""
+ class SenderKeywordProbe
+ def target(a, b:, c:)
+ [a, b, c]
+ end
+
+ def call_target
+ send(:target, 1, b: 2, c: 3)
+ end
+ end
+
+ SenderKeywordProbe.new.call_target
+ """u8).As();
+
+ Assert.That(result[0], Is.EqualTo(new MRubyValue(1)));
+ Assert.That(result[1], Is.EqualTo(new MRubyValue(2)));
+ Assert.That(result[2], Is.EqualTo(new MRubyValue(3)));
+ }
+
+ [Test]
+ public void RubySend_ResumesNestedSendToUnderscoreNamedMethod()
+ {
+ var result = Exec("""
+ class UnderscoreNestedSendProbe
+ attr_reader :events
+
+ def initialize
+ @events = []
+ @_a = 0
+ @data = 0
+ @_p_c = 0
+ end
+
+ def imm(_read, _write)
+ @events << :imm
+ @data = 1
+ end
+
+ def _adc
+ @events << :_adc
+ tmp = @_a + @data + @_p_c
+ @_p_v = ~(@_a ^ @data) & (@_a ^ tmp) & 0x80
+ @_a = tmp & 0xff
+ @_p_c = tmp[8]
+ end
+
+ def r_op(instr, mode)
+ send(mode, true, false)
+ send(instr)
+ end
+
+ def run
+ __send__(:r_op, :_adc, :imm)
+ @events << :post
+ [@events, @_a, @data, @_p_c]
+ end
+ end
+
+ UnderscoreNestedSendProbe.new.run
+ """u8).As();
+
+ var events = result[0].As();
+ Assert.That(events.Length, Is.EqualTo(3));
+ Assert.That(events[0].SymbolValue, Is.EqualTo(mrb.Intern("imm"u8)));
+ Assert.That(events[1].SymbolValue, Is.EqualTo(mrb.Intern("_adc"u8)));
+ Assert.That(events[2].SymbolValue, Is.EqualTo(mrb.Intern("post"u8)));
+ Assert.That(result[1], Is.EqualTo(new MRubyValue(1)));
+ Assert.That(result[2], Is.EqualTo(new MRubyValue(1)));
+ Assert.That(result[3], Is.EqualTo(new MRubyValue(0)));
+ }
+
+ [Test]
+ public void ArgAry_ForwardsSuperArgumentsWithRestAndPost()
+ {
+ var result = Exec("""
+ class ArgAryBase
+ def count_rest(a, *rest, b)
+ rest.size
+ end
+ end
+
+ class ArgAryChild < ArgAryBase
+ def count_rest(a, *rest, b)
+ super
+ end
+ end
+
+ ArgAryChild.new.count_rest(1, 2, 3, 4)
+ """u8);
+
+ Assert.That(result, Is.EqualTo(new MRubyValue(2)));
+ }
+
+ [Test]
+ public void InstanceVariableGetSet()
+ {
+ var result = Exec("""
+ obj = Object.new
+ set = obj.instance_variable_set(:@x, 42)
+ get = obj.instance_variable_get(:@x)
+ removed = obj.remove_instance_variable(:@x)
+ set == 42 && get == 42 && removed == 42 && obj.instance_variable_get(:@x).nil?
+ """u8);
+
+ Assert.That(result, Is.EqualTo(MRubyValue.True));
+ }
+
+ [Test]
+ public void BoundMethodCallAndHashKey()
+ {
+ var result = Exec("""
+ class BoundMethodTarget
+ def initialize
+ @value = 3
+ end
+
+ def add(a, b)
+ @value + a + b
+ end
+ end
+
+ obj = BoundMethodTarget.new
+ m = obj.method(:add)
+ h = { obj.method(:add) => 1 }
+ m.call(4, 5) == 12 &&
+ m[6, 7] == 16 &&
+ m.name == :add &&
+ m.receiver == obj &&
+ h[obj.method(:add)] == 1
+ """u8);
+
+ Assert.That(result, Is.EqualTo(MRubyValue.True));
+ }
+
+ [Test]
+ public void ArrayRotateBangAndInclude()
+ {
+ var result = Exec("""
+ a = [1, 2, 3, 4]
+ a.rotate!
+ ok1 = a == [2, 3, 4, 1]
+ a.rotate!(2)
+ ok2 = a == [4, 1, 2, 3]
+ a.rotate!(-3)
+ ok3 = a == [1, 2, 3, 4]
+ ok1 && ok2 && ok3 && a.include?(4) && !a.member?(9)
+ """u8);
+
+ Assert.That(result, Is.EqualTo(MRubyValue.True));
+ }
+
MRubyValue Exec(ReadOnlySpan code)
{
using var compilation = compiler.Compile(code);
return mrb.LoadBytecode(compilation.AsBytecode());
}
-}
\ No newline at end of file
+}
diff --git a/tests/ChibiRuby.Tests/ruby/test/integer.rb b/tests/ChibiRuby.Tests/ruby/test/integer.rb
index 16889030..aaa029ef 100644
--- a/tests/ChibiRuby.Tests/ruby/test/integer.rb
+++ b/tests/ChibiRuby.Tests/ruby/test/integer.rb
@@ -103,6 +103,11 @@
# Complement
assert_equal(-1, ~0)
assert_equal(-3, ~2)
+
+ x = 0
+ y = 2
+ assert_equal(-1, ~x)
+ assert_equal(-3, y.__send__(:~))
end
assert('Integer#&', '15.2.8.3.9') do
@@ -152,6 +157,27 @@
# Don't raise on large Right Shift
assert_equal 0, 23 >> 128
+
+ # Preserve bytes for normal-width shifts
+ assert_equal 0x80, 0x8015 >> 8
+
+ # Saturate large arithmetic shifts
+ assert_equal 0, 1 >> 63
+ assert_equal 0, 1 >> 64
+ assert_equal -1, -1 >> 63
+ assert_equal -1, -1 >> 64
+end
+
+assert('Integer#[]') do
+ assert_equal 1, 1[0]
+ assert_equal 0, 1[1]
+ assert_equal 1, 0x80[7]
+ assert_equal 0, 0x80[8]
+ assert_equal 0, 1[-1]
+ assert_equal 1, (-1)[0]
+ assert_equal 1, (-1)[64]
+ assert_equal 0, (-2)[0]
+ assert_equal 1, (-2)[1]
end
assert('Integer#ceil', '15.2.8.3.14') do