mruby-array-ext: implement Array#fetch with hybrid approach

Moved Array#fetch from Ruby to C using hybrid implementation for
better performance. The C implementation handles all non-block cases
with unified API that eliminates Ruby conditional logic.

Key improvements:
- Fast C implementation for common cases (no blocks)
- Shared index normalization helper reusable for other methods
- Unified C call eliminates NONE sentinel comparison in Ruby
- Block cases use C helper for index normalization

Added comprehensive test coverage including edge cases, default values,
block handling, and error message format verification. Combined tests
to focus on functionality rather than implementation details.

Co-authored-by: Atlassian Rovo Dev
This commit is contained in:
Yukihiro "Matz" Matsumoto
2025-06-30 08:28:56 +09:00
parent 87c39d3c0b
commit 90fd382a9e
3 changed files with 94 additions and 10 deletions
+10 -10
View File
@@ -99,18 +99,18 @@ class Array
def fetch(n, ifnone=NONE, &block)
#warn "block supersedes default value argument" if !n.nil? && ifnone != NONE && block
idx = n.__to_int
if idx < 0
idx += size
end
if idx < 0 || size <= idx
return block.call(n) if block
if NONE.equal?(ifnone)
raise IndexError, "index #{n} outside of array bounds: #{-size}...#{size}"
if block
# Block case: use shared index helper + Ruby block handling
normalized_index = __normalize_index(n)
if normalized_index
self[normalized_index]
else
block.call(n)
end
return ifnone
else
# Fast C implementation for non-block cases
__fetch(n, ifnone, NONE)
end
self[idx]
end
##
+71
View File
@@ -1103,6 +1103,75 @@ ary_flatten(mrb_state *mrb, mrb_value self)
return flatten_internal(mrb, self, level, &modified);
}
/*
* Shared helper for index normalization and bounds checking.
* Returns normalized index if in bounds, nil if out of bounds.
*/
static mrb_value
ary_normalize_index(mrb_state *mrb, mrb_value self)
{
mrb_value index_val;
mrb_get_args(mrb, "o", &index_val);
mrb_int index = mrb_as_int(mrb, index_val);
struct RArray *ary = mrb_ary_ptr(self);
mrb_int len = ARY_LEN(ary);
// Handle negative indices
if (index < 0) {
index += len;
}
// Check bounds
if (index >= 0 && index < len) {
return mrb_fixnum_value(index);
}
else {
return mrb_nil_value();
}
}
/*
* Fast C implementation for Array#fetch without blocks.
* Returns the element at index, or default if out of bounds.
* Raises IndexError if out of bounds and default equals none.
*/
static mrb_value
ary_fetch(mrb_state *mrb, mrb_value self)
{
mrb_value index_val, default_val, none;
mrb_get_args(mrb, "ooo", &index_val, &default_val, &none);
// Convert index to integer
mrb_int index = mrb_as_int(mrb, index_val);
mrb_int original_index = index; // Keep original for error message
struct RArray *ary = mrb_ary_ptr(self);
mrb_int len = ARY_LEN(ary);
// Handle negative indices
if (index < 0) {
index += len;
}
// Check bounds
if (index < 0 || index >= len) {
// Check if default is the NONE sentinel (means no default provided)
if (mrb_obj_equal(mrb, default_val, none)) {
// No default provided - raise IndexError
mrb_raisef(mrb, E_INDEX_ERROR,
"index %i outside of array bounds: %i...%i",
original_index, -len, len);
}
return default_val;
}
// Return element at index
return ARY_PTR(ary)[index];
}
/*
* call-seq:
* ary.flatten! -> ary or nil
@@ -1219,6 +1288,8 @@ mrb_mruby_array_ext_gem_init(mrb_state* mrb)
mrb_define_method_id(mrb, a, MRB_SYM_B(__uniq), ary_uniq_bang, MRB_ARGS_NONE());
mrb_define_method_id(mrb, a, MRB_SYM(flatten), ary_flatten, MRB_ARGS_OPT(1));
mrb_define_method_id(mrb, a, MRB_SYM_B(flatten), ary_flatten_bang, MRB_ARGS_OPT(1));
mrb_define_method_id(mrb, a, MRB_SYM(__normalize_index), ary_normalize_index, MRB_ARGS_REQ(1));
mrb_define_method_id(mrb, a, MRB_SYM(__fetch), ary_fetch, MRB_ARGS_REQ(3));
mrb_define_method_id(mrb, a, MRB_SYM(insert), ary_insert, MRB_ARGS_ARG(1, -1));
}
+13
View File
@@ -358,6 +358,19 @@ assert("Array#fetch") do
a.fetch(100) { |i| ret = i }
assert_equal 100, ret
assert_raise(IndexError) { a.fetch(100) }
# Additional edge cases
assert_equal "default", [].fetch(0, "default")
assert_equal "missing 5", ["a"].fetch(5) { |i| "missing #{i}" }
assert_equal "from block", ["a"].fetch(5, "default") { "from block" }
# Error message format
begin
["a", "b"].fetch(5)
assert_false true
rescue IndexError => e
assert_true e.message.include?("index 5 outside of array bounds: -2...2")
end
end
assert("Array#fetch_values") do