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