From 17da26ee1ceaf7b0fa55f7f2126c446eb2f8cc1b Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 25 Jun 2026 17:50:46 -0700 Subject: [PATCH 01/11] ZJIT: Rewrite jit_perf script in Ruby (#17474) --- doc/jit/yjit.md | 21 +----- doc/jit/zjit.md | 56 +++++++++++++++- misc/jit_perf.py | 116 --------------------------------- misc/jit_perf.rb | 166 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 224 insertions(+), 135 deletions(-) delete mode 100755 misc/jit_perf.py create mode 100755 misc/jit_perf.rb diff --git a/doc/jit/yjit.md b/doc/jit/yjit.md index d91877c30e8222..66da70df730c9d 100644 --- a/doc/jit/yjit.md +++ b/doc/jit/yjit.md @@ -525,23 +525,8 @@ PERF=record ruby --yjit-perf=codegen -Iharness-perf benchmarks/lobsters/benchmar # Aggregate results perf script > /tmp/perf.txt -../ruby/misc/jit_perf.py /tmp/perf.txt +../ruby/misc/jit_perf.rb /tmp/perf.txt ``` -#### Building perf with Python support - -The above instructions work fine for most people, but you could also use -a handy `perf script -s` interface if you build perf from source. - -```bash -# Build perf from source for Python support -sudo apt-get install libpython3-dev python3-pip flex libtraceevent-dev \ - libelf-dev libunwind-dev libaudit-dev libslang2-dev libdw-dev -git clone --depth=1 https://github.com/torvalds/linux -cd linux/tools/perf -make -make install - -# Aggregate results -perf script -s ../ruby/misc/jit_perf.py -``` +This aggregation script reads the text output from `perf script`, so it does not require `perf` +to be built with scripting support. diff --git a/doc/jit/zjit.md b/doc/jit/zjit.md index ebe5cc4f9bb7b3..0f1cb595203d29 100644 --- a/doc/jit/zjit.md +++ b/doc/jit/zjit.md @@ -146,7 +146,8 @@ ZJIT options: --zjit-stats[=quiet] Enable collecting ZJIT statistics (=quiet to suppress output). --zjit-disable Disable ZJIT for lazily enabling it with RubyVM::ZJIT.enable. - --zjit-perf Dump ISEQ symbols into /tmp/perf-{}.map for Linux perf. + --zjit-perf[=iseq|hir] + Dump symbols for Linux perf /tmp/perf-{}.map (default: iseq). --zjit-log-compiled-iseqs=path Log compiled ISEQs to the file. The file will be truncated. --zjit-trace-exits[=counter] @@ -156,6 +157,59 @@ ZJIT options: $ ``` +### Profiling with Linux perf + +`--zjit-perf` allows you to profile JIT-ed methods along with other native functions using Linux perf. +When you run Ruby with `perf record`, perf looks up `/tmp/perf-{pid}.map` to resolve symbols in JIT code, +and this option lets ZJIT write ISEQ symbols into that file. + +#### Call graph + +Here's an example way to use this option with [Firefox Profiler](https://profiler.firefox.com) +(See also: [Profiling with Linux perf](https://profiler.firefox.com/docs/#/./guide-perf-profiling)): + +```bash +# Compile the interpreter with frame pointers enabled +./configure --enable-zjit --prefix=$HOME/.rubies/ruby-zjit --disable-install-doc cflags=-fno-omit-frame-pointer +make -j && make install + +# [Optional] Allow running perf without sudo +echo 0 | sudo tee /proc/sys/kernel/kptr_restrict +echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid + +# Profile Ruby with --zjit-perf +cd ../ruby-bench +PERF="record --call-graph fp" ruby --zjit-perf -Iharness-perf benchmarks/liquid-render/benchmark.rb + +# View results on Firefox Profiler https://profiler.firefox.com. +# Create /tmp/test.perf as below and upload it using "Load a profile from file". +perf script --fields +pid > /tmp/test.perf +``` + +#### ZJIT HIR + +You can also profile the number of cycles consumed by code generated from each kind of HIR instruction. + +```bash +# Install perf +apt-get install linux-tools-common linux-tools-generic linux-tools-`uname -r` + +# [Optional] Allow running perf without sudo +echo 0 | sudo tee /proc/sys/kernel/kptr_restrict +echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid + +# Profile Ruby with --zjit-perf=hir +cd ../ruby-bench +PERF=record ruby --zjit-perf=hir -Iharness-perf benchmarks/lobsters/benchmark.rb + +# Aggregate results +perf script > /tmp/perf.txt +../ruby/misc/jit_perf.rb /tmp/perf.txt +``` + +This aggregation script reads the text output from `perf script`, so it does not require `perf` +to be built with scripting support. + ### Source level documentation You can generate and open the source level documentation in your browser using: diff --git a/misc/jit_perf.py b/misc/jit_perf.py deleted file mode 100755 index bc0f961b20bf07..00000000000000 --- a/misc/jit_perf.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python3 -import os -import sys -from collections import Counter, defaultdict -import os.path - -# Aggregating cycles per symbol and dso -total_cycles = 0 -category_cycles = Counter() -detailed_category_cycles = defaultdict(Counter) -categories = set() - -def truncate_symbol(symbol, max_length=50): - """ Truncate the symbol name to a maximum length """ - return symbol if len(symbol) <= max_length else symbol[:max_length-3] + '...' - -def categorize_symbol(dso, symbol): - """ Categorize the symbol based on the defined criteria """ - if dso == 'sqlite3_native.so': - return '[sqlite3]' - elif 'SHA256' in symbol: - return '[sha256]' - elif symbol.startswith('[JIT] gen_send'): - return '[JIT send]' - elif symbol.startswith('[JIT]') or symbol.startswith('ZJIT: ') or dso.startswith('perf-'): - return '[JIT code]' - elif '::' in symbol or symbol.startswith('_ZN4yjit') or symbol.startswith('_ZN4zjit'): - return '[JIT compile]' - elif symbol.startswith('rb_vm_') or symbol.startswith('vm_') or symbol in { - "rb_call0", "callable_method_entry_or_negative", "invoke_block_from_c_bh", - "rb_funcallv_scope", "setup_parameters_complex", "rb_yield"}: - return '[interpreter]' - elif symbol.startswith('rb_hash_') or symbol.startswith('hash_'): - return '[rb_hash_*]' - elif symbol.startswith('rb_ary_') or symbol.startswith('ary_'): - return '[rb_ary_*]' - elif symbol.startswith('rb_str_') or symbol.startswith('str_'): - return '[rb_str_*]' - elif symbol.startswith('rb_sym') or symbol.startswith('sym_'): - return '[rb_sym_*]' - elif symbol.startswith('rb_st_') or symbol.startswith('st_'): - return '[rb_st_*]' - elif symbol.startswith('rb_ivar_') or 'shape' in symbol: - return '[ivars]' - elif 'match' in symbol or symbol.startswith('rb_reg') or symbol.startswith('onig'): - return '[regexp]' - elif 'alloc' in symbol or 'free' in symbol or 'gc' in symbol: - return '[GC]' - elif 'pthread' in symbol and 'lock' in symbol: - return '[pthread lock]' - else: - return symbol # Return the symbol itself for uncategorized symbols - -def process_event(event): - global total_cycles, category_cycles, detailed_category_cycles, categories - - full_dso = event.get("dso", "Unknown_dso") - dso = os.path.basename(full_dso) - symbol = event.get("symbol", "[unknown]") - cycles = event["sample"]["period"] - total_cycles += cycles - - category = categorize_symbol(dso, symbol) - category_cycles[category] += cycles - detailed_category_cycles[category][(dso, symbol)] += cycles - - if category.startswith('[') and category.endswith(']'): - categories.add(category) - -def trace_end(): - if total_cycles == 0: - return - - print("Aggregated Event Data:") - print("{:<20} {:<50} {:>20} {:>15}".format("[dso]", "[symbol or category]", "[top-most cycle ratio]", "[num cycles]")) - - for category, cycles in category_cycles.most_common(): - ratio = (cycles / total_cycles) * 100 - dsos = {dso for dso, _ in detailed_category_cycles[category]} - dso_display = next(iter(dsos)) if len(dsos) == 1 else "Multiple DSOs" - print("{:<20} {:<50} {:>20.2f}% {:>15}".format(dso_display, truncate_symbol(category), ratio, cycles)) - - # Category breakdown - for category in categories: - symbols = detailed_category_cycles[category] - category_total = sum(symbols.values()) - category_ratio = (category_total / total_cycles) * 100 - print(f"\nCategory: {category} ({category_ratio:.2f}%)") - print("{:<20} {:<50} {:>20} {:>15}".format("[dso]", "[symbol]", "[top-most cycle ratio]", "[num cycles]")) - for (dso, symbol), cycles in symbols.most_common(): - symbol_ratio = (cycles / category_total) * 100 - print("{:<20} {:<50} {:>20.2f}% {:>15}".format(dso, truncate_symbol(symbol), symbol_ratio, cycles)) - -# There are two ways to use this script: -# 1) perf script -s misc/yjit_perf.py -- native interface -# 2) perf script > perf.txt && misc/yjit_perf.py perf.txt -- hack, which doesn't require perf with Python support -# -# In both cases, __name__ is "__main__". The following code implements (2) when sys.argv is 2. -if __name__ == "__main__" and len(sys.argv) == 2: - if len(sys.argv) != 2: - print("Usage: yjit_perf.py ") - sys.exit(1) - - with open(sys.argv[1], "r") as file: - for line in file: - # [Example] - # ruby 78207 3482.848465: 1212775 cpu_core/cycles:P/: 5c0333f682e1 [JIT] getlocal_WC_0+0x0 (/tmp/perf-78207.map) - row = line.split(maxsplit=6) - - period = row[3] # "1212775" - symbol, dso = row[6].rsplit(" (", 1) # "[JIT] getlocal_WC_0+0x0", "/tmp/perf-78207.map)\n" - symbol = symbol.split("+")[0] # "[JIT] getlocal_WC_0" - dso = dso.split(")")[0] # "/tmp/perf-78207.map" - - process_event({"dso": dso, "symbol": symbol, "sample": {"period": int(period)}}) - trace_end() diff --git a/misc/jit_perf.rb b/misc/jit_perf.rb new file mode 100755 index 00000000000000..c59915bde52726 --- /dev/null +++ b/misc/jit_perf.rb @@ -0,0 +1,166 @@ +#!/usr/bin/env ruby + +class JITPerf + INTERPRETER_SYMBOLS = [ + # rb_* entry points + "rb_call0", + "rb_funcallv_scope", + "rb_yield", + + # VM helpers without rb_vm_/vm_ prefixes + "callable_method_entry_or_negative", + "invoke_block_from_c_bh", + "setup_parameters_complex", + ].freeze + + def initialize + @total_cycles = 0 + @category_cycles = Hash.new(0) + @detailed_category_cycles = Hash.new { |hash, category| hash[category] = Hash.new(0) } + @categories = {} + end + + def read(path) + File.foreach(path).with_index(1) do |line, lineno| + next if line.strip.empty? + + process_event(parse_line(line)) + rescue ArgumentError => error + abort "#{path}:#{lineno}: #{error.message}" + end + rescue SystemCallError => error + abort "#{path}: #{error.message}" + end + + def print_report + return if @total_cycles == 0 + + puts "Aggregated Event Data:" + puts format("%-20s %-50s %20s %15s", "[dso]", "[symbol or category]", "[top-most cycle ratio]", "[num cycles]") + + most_common(@category_cycles).each do |category, cycles| + ratio = cycles.to_f / @total_cycles * 100 + dsos = @detailed_category_cycles[category].each_key.map(&:first).uniq + dso_display = dsos.length == 1 ? dsos.first : "Multiple DSOs" + puts format("%-20s %-50s %20.2f%% %15d", dso_display, truncate_symbol(category), ratio, cycles) + end + + most_common(@category_cycles).each do |category, _cycles| + next unless @categories.key?(category) + + symbols = @detailed_category_cycles[category] + category_total = symbols.values.sum + category_ratio = category_total.to_f / @total_cycles * 100 + + puts + puts format("Category: %s (%.2f%%)", category, category_ratio) + puts format("%-20s %-50s %20s %15s", "[dso]", "[symbol]", "[top-most cycle ratio]", "[num cycles]") + + most_common(symbols).each do |(dso, symbol), cycles| + symbol_ratio = cycles.to_f / category_total * 100 + puts format("%-20s %-50s %20.2f%% %15d", dso, truncate_symbol(symbol), symbol_ratio, cycles) + end + end + end + + private + + def parse_line(line) + # Example: + # ruby 78207 3482.848465: 1212775 cpu_core/cycles:P/: 5c0333f682e1 [JIT] getlocal_WC_0+0x0 (/tmp/perf-78207.map) + # + # Split into command, pid, timestamp, period, event, ip, and the remaining + # "symbol (dso)" text. The final field is kept intact because symbols can + # contain spaces. + fields = line.split(nil, 7) + raise ArgumentError, "unexpected perf script line: #{line.chomp}" if fields.length < 7 + + begin + period = Integer(fields[3]) + rescue ArgumentError, TypeError + raise ArgumentError, "unexpected sample period in perf script line: #{line.chomp}" + end + + # Parse the trailing "symbol (dso)" text from the right, then drop the + # instruction offset after "+" from the symbol name. + dso_start = fields[6].rindex(" (") + raise ArgumentError, "missing dso in perf script line: #{line.chomp}" unless dso_start + + dso_with_suffix = fields[6][(dso_start + 2)..-1] + dso_end = dso_with_suffix.index(")") + raise ArgumentError, "missing dso terminator in perf script line: #{line.chomp}" unless dso_end + + symbol = fields[6][0...dso_start].split("+", 2).first + dso = dso_with_suffix[0...dso_end] + + [dso, symbol, period] + end + + def process_event(event) + full_dso, symbol, cycles = event + dso = File.basename(full_dso || "Unknown_dso") + symbol ||= "[unknown]" + + @total_cycles += cycles + + category = categorize_symbol(dso, symbol) + @category_cycles[category] += cycles + @detailed_category_cycles[category][[dso, symbol]] += cycles + + @categories[category] = true if category.start_with?("[") && category.end_with?("]") + end + + def truncate_symbol(symbol, max_length = 50) + symbol.length <= max_length ? symbol : "#{symbol[0...(max_length - 3)]}..." + end + + def categorize_symbol(dso, symbol) + if dso == "sqlite3_native.so" + "[sqlite3]" + elsif symbol.include?("SHA256") + "[sha256]" + elsif symbol.start_with?("[JIT] gen_send") + "[JIT send]" + elsif symbol.start_with?("[JIT]") || symbol.start_with?("ZJIT: ") || dso.start_with?("perf-") + "[JIT code]" + elsif symbol.include?("::") || symbol.start_with?("_ZN4yjit") || symbol.start_with?("_ZN4zjit") + "[JIT compile]" + elsif symbol.start_with?("rb_vm_") || symbol.start_with?("vm_") || INTERPRETER_SYMBOLS.include?(symbol) + "[interpreter]" + elsif symbol.start_with?("rb_hash_") || symbol.start_with?("hash_") + "[rb_hash_*]" + elsif symbol.start_with?("rb_ary_") || symbol.start_with?("ary_") + "[rb_ary_*]" + elsif symbol.start_with?("rb_str_") || symbol.start_with?("str_") + "[rb_str_*]" + elsif symbol.start_with?("rb_sym") || symbol.start_with?("sym_") + "[rb_sym_*]" + elsif symbol.start_with?("rb_st_") || symbol.start_with?("st_") + "[rb_st_*]" + elsif symbol.start_with?("rb_ivar_") || symbol.include?("shape") + "[ivars]" + elsif symbol.include?("match") || symbol.start_with?("rb_reg") || symbol.start_with?("onig") + "[regexp]" + elsif symbol.include?("alloc") || symbol.include?("free") || symbol.include?("gc") + "[GC]" + elsif symbol.include?("pthread") && symbol.include?("lock") + "[pthread lock]" + else + symbol + end + end + + def most_common(counter) + counter.each.with_index + .sort_by { |((_key, cycles), index)| [-cycles, index] } + .map(&:first) + end +end + +if ARGV.length != 1 + abort "Usage: #{File.basename($PROGRAM_NAME)} " +end + +jit_perf = JITPerf.new +jit_perf.read(ARGV[0]) +jit_perf.print_report From 5cd8d26105e045a56f156fadf16946c7408f785c Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Fri, 26 Jun 2026 01:39:23 +0100 Subject: [PATCH 02/11] [DOC] Tweaks for Set.new --- set.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/set.c b/set.c index 982d0aa5baf63d..091c5ec17c334d 100644 --- a/set.c +++ b/set.c @@ -504,23 +504,25 @@ set_initialize_with_block(RB_BLOCK_CALL_FUNC_ARGLIST(i, set)) * Set.new # => Set[] * Set.new { fail 'Cannot happen' } # => Set[] # Block not called. * - * With no block given and +object+ given as an Enumerable, + * With no block given and argument +object+ given as an \Enumerable, * populates the new set with the elements of +object+: * - * Set.new(%w[ a b c ]) # => Set["a", "b", "c"] - * Set.new({foo: 0, bar: 1}) # => Set[[:foo, 0], [:bar, 1]] - * Set.new(4..10) # => Set[4, 5, 6, 7, 8, 9, 10] + * Set.new(%w[ a b c ]) # => Set["a", "b", "c"] + * Set.new({foo: 0, bar: 1}) # => Set[[:foo, 0], [:bar, 1]] + * Set.new(4..10) # => Set[4, 5, 6, 7, 8, 9, 10] * Set.new(Dir.new('lib')).take(5) * # => [".", "..", "bundled_gems.rb", "bundler", "bundler.rb"] * Set.new(File.new('doc/NEWS/NEWS-4.0.0.md')).take(3) * # => ["# NEWS for Ruby 4.0.0\n", "\n", "This document is a list of user-visible feature changes\n"] * - * With a block given and +object+ given as an Enumerable, + * With a block given and argument +object+ given as an \Enumerable, * calls the block with each element of +object+; * adds the block's return value to the new set: * * Set.new(4..10) {|i| i * 2 } # => Set[8, 10, 12, 14, 16, 18, 20] * + * Related: see {Methods for Creating a Set}[rdoc-ref:Set@Methods+for+Creating+a+Set]. + * */ static VALUE set_i_initialize(int argc, VALUE *argv, VALUE set) From 04be5a026dbec03c256e28924550f1d1dc75d861 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Fri, 26 Jun 2026 01:46:30 +0100 Subject: [PATCH 03/11] More --- set.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/set.c b/set.c index 091c5ec17c334d..5f565ba1b83a03 100644 --- a/set.c +++ b/set.c @@ -504,7 +504,7 @@ set_initialize_with_block(RB_BLOCK_CALL_FUNC_ARGLIST(i, set)) * Set.new # => Set[] * Set.new { fail 'Cannot happen' } # => Set[] # Block not called. * - * With no block given and argument +object+ given as an \Enumerable, + * With no block given and argument +object+ given, * populates the new set with the elements of +object+: * * Set.new(%w[ a b c ]) # => Set["a", "b", "c"] @@ -515,7 +515,7 @@ set_initialize_with_block(RB_BLOCK_CALL_FUNC_ARGLIST(i, set)) * Set.new(File.new('doc/NEWS/NEWS-4.0.0.md')).take(3) * # => ["# NEWS for Ruby 4.0.0\n", "\n", "This document is a list of user-visible feature changes\n"] * - * With a block given and argument +object+ given as an \Enumerable, + * With a block given and argument +object+ given, * calls the block with each element of +object+; * adds the block's return value to the new set: * From 98ccc930b7d0ef58f5f780560636a89186d7ab4f Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Thu, 25 Jun 2026 19:57:34 -0500 Subject: [PATCH 04/11] Update set.c Co-authored-by: Jeremy Evans --- set.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/set.c b/set.c index 5f565ba1b83a03..070750b3486563 100644 --- a/set.c +++ b/set.c @@ -504,7 +504,7 @@ set_initialize_with_block(RB_BLOCK_CALL_FUNC_ARGLIST(i, set)) * Set.new # => Set[] * Set.new { fail 'Cannot happen' } # => Set[] # Block not called. * - * With no block given and argument +object+ given, + * With no block given and enumerable argument +object+ given, * populates the new set with the elements of +object+: * * Set.new(%w[ a b c ]) # => Set["a", "b", "c"] From eff8d3582fbd9d0f0c2f39f90ee4bba236e0f21e Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Thu, 25 Jun 2026 19:57:48 -0500 Subject: [PATCH 05/11] Update set.c Co-authored-by: Jeremy Evans --- set.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/set.c b/set.c index 070750b3486563..92064849acf141 100644 --- a/set.c +++ b/set.c @@ -515,7 +515,7 @@ set_initialize_with_block(RB_BLOCK_CALL_FUNC_ARGLIST(i, set)) * Set.new(File.new('doc/NEWS/NEWS-4.0.0.md')).take(3) * # => ["# NEWS for Ruby 4.0.0\n", "\n", "This document is a list of user-visible feature changes\n"] * - * With a block given and argument +object+ given, + * With a block given and enumerable argument +object+ given, * calls the block with each element of +object+; * adds the block's return value to the new set: * From fbbe4e60586903f50f89f00f9a2a53f6f7e4b9bf Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Fri, 26 Jun 2026 01:13:20 +0100 Subject: [PATCH 06/11] [DOC] Doc for Set[] --- set.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/set.c b/set.c index 92064849acf141..da9224be4cbf90 100644 --- a/set.c +++ b/set.c @@ -416,8 +416,16 @@ set_s_alloc(VALUE klass) * call-seq: * Set[*objects] -> new_set * - * Returns a new Set object populated with the given objects, - * See Set::new. + * Returns a new \Set object populated with the given `objects`: + * + * Set[1, 'one', :one, 1.0, %w[a b c], {foo: 0, bar: 1}] + * # => Set[1, "one", :one, 1.0, ["a", "b", "c"], {foo: 0, bar: 1}] + * Set[Set[0, 1, 2], Set[%w[a b c]]] + * # => Set[Set[0, 1, 2], Set[["a", "b", "c"]]] + * Set[] # => Set[] + * + * Related: see {Methods for Creating a Set}[rdoc-ref:Set@Methods+for+Creating+a+Set]. + * */ static VALUE set_s_create(int argc, VALUE *argv, VALUE klass) From bd6b52f27f85e985b6d3b52b0e053605121a8b41 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Fri, 26 Jun 2026 01:20:41 +0100 Subject: [PATCH 07/11] [DOC] Doc for Set[] --- set.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/set.c b/set.c index da9224be4cbf90..853ad2134ea719 100644 --- a/set.c +++ b/set.c @@ -416,7 +416,7 @@ set_s_alloc(VALUE klass) * call-seq: * Set[*objects] -> new_set * - * Returns a new \Set object populated with the given `objects`: + * Returns a new \Set object populated with the given +objects+: * * Set[1, 'one', :one, 1.0, %w[a b c], {foo: 0, bar: 1}] * # => Set[1, "one", :one, 1.0, ["a", "b", "c"], {foo: 0, bar: 1}] From cf0e1238ee63ae7303e84840a3d8bc43c86e770f Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Fri, 26 Jun 2026 02:17:57 +0100 Subject: [PATCH 08/11] [DOC] Tweaks for Set#& --- set.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/set.c b/set.c index 853ad2134ea719..6fc1f2a020b4c4 100644 --- a/set.c +++ b/set.c @@ -1066,13 +1066,19 @@ set_intersection_block(RB_BLOCK_CALL_FUNC_ARGLIST(i, data)) /* * call-seq: - * set & enum -> new_set + * self & enumerable -> new_set * - * Returns a new set containing elements common to the set and the given - * enumerable object. + * Returns a new set containing the {intersection}[https://en.wikipedia.org/wiki/Intersection_(set_theory)] + * of +self+ and +enumerable+; + * that is, containing all elements common to both, with no duplicates. + * Argument +enumerable+ must be an Enumerable object: * - * Set[1, 3, 5] & Set[3, 2, 1] #=> Set[3, 1] - * Set['a', 'b', 'z'] & ['a', 'b', 'c'] #=> Set["a", "b"] + * set = Set[*(0..6), *%w[ a b c]] # => Set[0, 1, 2, 3, 4, 5, 6, "a", "b", "c"] + * set & ['c', 6, 1, 4] # => Set["c", 6, 1, 4] + * set & [:foo, :bar] # => Set[] # No elements in common. + * set & {} # => Set[] # No elements in enumerable. + * + * Related: see {Methods for Set Operations}[rdoc-ref:Set@Methods+for+Set+Operations]. */ static VALUE set_i_intersection(VALUE set, VALUE other) From 89aed6ab0775bccb267556e9d2859891a1c33255 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Thu, 25 Jun 2026 20:57:24 -0500 Subject: [PATCH 09/11] Update set.c Co-authored-by: Jeremy Evans --- set.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/set.c b/set.c index 6fc1f2a020b4c4..5cb8e4b22cc59c 100644 --- a/set.c +++ b/set.c @@ -1074,7 +1074,7 @@ set_intersection_block(RB_BLOCK_CALL_FUNC_ARGLIST(i, data)) * Argument +enumerable+ must be an Enumerable object: * * set = Set[*(0..6), *%w[ a b c]] # => Set[0, 1, 2, 3, 4, 5, 6, "a", "b", "c"] - * set & ['c', 6, 1, 4] # => Set["c", 6, 1, 4] + * set & ['c', 6, 8, 4] # => Set["c", 6, 4] * set & [:foo, :bar] # => Set[] # No elements in common. * set & {} # => Set[] # No elements in enumerable. * From f2e4ae59aecc152cabbda3cb8bea96551e75641d Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Thu, 25 Jun 2026 20:57:53 -0500 Subject: [PATCH 10/11] Update set.c Co-authored-by: Jeremy Evans --- set.c | 1 - 1 file changed, 1 deletion(-) diff --git a/set.c b/set.c index 5cb8e4b22cc59c..a0bbe7bad5c7d0 100644 --- a/set.c +++ b/set.c @@ -1076,7 +1076,6 @@ set_intersection_block(RB_BLOCK_CALL_FUNC_ARGLIST(i, data)) * set = Set[*(0..6), *%w[ a b c]] # => Set[0, 1, 2, 3, 4, 5, 6, "a", "b", "c"] * set & ['c', 6, 8, 4] # => Set["c", 6, 4] * set & [:foo, :bar] # => Set[] # No elements in common. - * set & {} # => Set[] # No elements in enumerable. * * Related: see {Methods for Set Operations}[rdoc-ref:Set@Methods+for+Set+Operations]. */ From 3c5d5ab81e0bc18dd69c7855f43a9cbdf6b807ea Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Thu, 25 Jun 2026 21:23:49 -0500 Subject: [PATCH 11/11] Update Set#- documentation --- set.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/set.c b/set.c index a0bbe7bad5c7d0..e6b758fed3ae73 100644 --- a/set.c +++ b/set.c @@ -1399,13 +1399,19 @@ set_i_subtract(VALUE set, VALUE other) /* * call-seq: - * set - enum -> new_set + * self - enumerable -> new_set * - * Returns a new set built by duplicating the set, removing every - * element that appears in the given enumerable object. + * Returns a new set containing the + * {difference}[https://en.wikipedia.org/wiki/Complement_(set_theory)#Relative_complement] + * of +self+ and argument +enumerable+; + * that is, containing all elements in +self+ that are not in +enumerable+. * - * Set[1, 3, 5] - Set[1, 5] #=> Set[3] - * Set['a', 'b', 'z'] - ['a', 'c'] #=> Set["b", "z"] + * + * set = Set[*(0..6), *%w[ a b c]] # => Set[0, 1, 2, 3, 4, 5, 6, "a", "b", "c"] + * set - ['b', 6, 4, 1] # => Set[0, 2, 3, 5, "a", "c"] + * set - ['d', 7, 9] # => Set[0, 1, 2, 3, 4, 5, 6, "a", "b", "c"] + * + * Related: see {Methods for Set Operations}[rdoc-ref:Set@Methods+for+Set+Operations]. */ static VALUE set_i_difference(VALUE set, VALUE other)