Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions mrbgems/mruby-regexp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,17 @@ str.gsub(re, replacement) # replace all occurrences
str.gsub(re) { |m| ... } # replace all with block
str.scan(re) # => array of matches
str.split(re) # => array of parts
str[re] # => matched substring or nil
str[re, capture] # => capture by index or name
str.slice(re) # => same as str[re]

# Symbol methods (the String methods applied to the symbol's name)
sym.match(re) # => MatchData or nil
sym.match(re) { |md| ... } # => block result, or nil if no match
sym.match?(re) # => true/false
sym =~ re # => index or nil
sym[re] # => matched substring or nil
# (Symbol#[] comes from mruby-symbol-ext)

# Global variables
$~ # last MatchData
Expand Down Expand Up @@ -122,9 +127,8 @@ pattern analysis.
only.
- **Step limit on backtracking**: Patterns that require the
backtracking engine are subject to a step limit.
- **No regexp form of `String#[]`**: `str[re]` and `str.slice(re)`
are not supported, and neither is `sym[re]`, which delegates to
them.
- **No regexp form on the write side**: `str[re] = repl` and
`str.slice!(re)` are not supported. Reading with `str[re]` is.

## Configuration

Expand Down
52 changes: 50 additions & 2 deletions mrbgems/mruby-regexp/mrblib/string_regexp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ class String
# back to the core implementation.
alias __split split

# Same for String#[], which the override below replaces along with its
# `slice` twin. `__aref` also serves as the internal spelling of `str[i]`
# inside this file: the loops in `gsub` and `split` would otherwise pay for
# a Ruby frame per iteration on a call that can never take a Regexp.
alias __aref []

# `match` and `match?` accept a Regexp or a String and reject everything
# else. The check lives in C (see Regexp.__check_pattern) so that the
# argument cannot steer it: it cannot pose as a Regexp, and there is no
Expand Down Expand Up @@ -96,7 +102,7 @@ def gsub(*args, &block)
pos = match_end + 1
else
rest = self.byteslice(match_end..-1)
char = rest[0]
char = rest.__aref(0)
parts << char
pos = match_end + char.bytesize
end
Expand All @@ -123,6 +129,48 @@ def scan(pattern)
end
end

# Regexp-aware element reference. Only a Regexp is handled here; every
# other argument list goes back to the C-defined `[]` untouched.
#
# Note that `str[i]`, `str[range]` and `str["sub"]` never reach this method
# when the receiver is a String rather than a subclass: OP_GETIDX answers
# those three argument types from C without consulting the method table, so
# an override is not visible there. That is harmless as long as this method
# only delegates them, which is why the regexp branch is the only behaviour
# added here.
#
# `str[0]` is the exception: a literal zero index compiles to OP_GETIDX0,
# which has no String branch at all and always sends, so it does arrive here
# and pays a Ruby frame and an argument array for what used to be a direct
# call into C, roughly four times the cost. That is why the loops in `gsub`
# and `split` above read their one character with `__aref(0)`.
def [](*args)
# Checked before the argument is inspected, so the non-regexp forms keep
# reporting the CRuby arity rather than silently ignoring an extra
# argument.
unless (1..2).include?(args.length)
raise ArgumentError, "wrong number of arguments (given #{args.length}, expected 1..2)"
end
# `is_a?` is redefinable, so an argument denying its own type could steer
# itself into `__aref` and be read as an index. `Module#===` reads the
# real type.
return __aref(*args) unless Regexp === args[0]
# `match`, not `match?`: `$~` has to be set even when the match fails,
# where it becomes nil. The MatchData is unused in the no-capture case,
# but the global is not.
md = args[0].match(self)
return nil unless md
# The capture argument is handed to MatchData#[] as it stands: an out of
# range index answers nil and a name that resolves to no group raises
# IndexError, which is what CRuby's rb_reg_nth_match() and
# rb_reg_backref_number() do respectively.
md[args.length == 2 ? args[1] : 0]
end

# CRuby reaches the same code for both names, and Symbol#slice (in
# mruby-symbol-ext) delegates here, which is what makes `sym[re]` work.
alias slice []

# Regexp-aware split. Falls back to the C-defined split (aliased as
# `__split` in mrb_mruby_regexp_gem_init before this override loads) for
# nil or string patterns, and handles regexp patterns in Ruby.
Expand Down Expand Up @@ -173,7 +221,7 @@ def split(pattern = nil, *args)
if match_start == match_end
rest = self.byteslice(match_end..-1)
if rest && rest.bytesize > 0
char = rest[0]
char = rest.__aref(0)
search_pos = match_end + char.bytesize
else
search_pos = match_end + 1
Expand Down
5 changes: 3 additions & 2 deletions mrbgems/mruby-regexp/mrblib/symbol_regexp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
#
# This covers the symbol-on-the-left direction only. The Regexp side converts
# a symbol on its own, in `match_operand()` in regexp.c, so it needs nothing
# from here. `sym[/re/]` is the direction still missing; it waits on the
# regexp form of `String#slice`.
# from here. `sym[/re/]` needs nothing either: Symbol#slice (mruby-symbol-ext,
# which this gem does not depend on) delegates to String#slice, so it picks up
# the regexp form from string_regexp.rb wherever that gem is built in.
#
# The argument handling of `=~` is inherited from `String#=~` rather than
# introduced here, and agrees with CRuby: a String argument raises TypeError,
Expand Down
88 changes: 88 additions & 0 deletions mrbgems/mruby-regexp/test/regexp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1441,3 +1441,91 @@ def -(other)
assert_nil (re =~ utf8.call(0x8081))
assert_nil (re =~ "A")
end

assert("String#[] with regexp") do
assert_equal "ll", "hello"[/l+/]
assert_equal "ll", "hello".slice(/l+/)
assert_nil "hello"[/z/]
assert_nil "hello".slice(/z/)
assert_equal "", "hello"[//]

# the result is a plain String even for a subclass receiver, as in CRuby
sub = Class.new(String)
assert_equal String, sub.new("hello")[/l+/].class
end

assert("String#[] with regexp and capture") do
assert_equal "ll", "hello"[/(l+)(o)/, 1]
assert_equal "o", "hello"[/(l+)(o)/, 2]
assert_equal "llo", "hello"[/(l+)(o)/, 0]
assert_equal "ll", "hello"[/(?<x>l+)/, :x]
assert_equal "ll", "hello"[/(?<x>l+)/, "x"]
assert_equal "ll", "hello".slice(/(l+)/, 1)

# handed to MatchData#[]: a negative index counts back from the last group,
# an index past the last group is nil, and a name that resolves to no group
# is a mistake at the point of the call
assert_equal "o", "hello"[/(l+)(o)/, -1]
assert_nil "hello"[/(l+)/, 5]
assert_raise(IndexError) { "hello"[/(?<x>l+)/, :zz] }
assert_raise(IndexError) { "hello"[/(l+)/, "x"] }
assert_raise(TypeError) { "hello"[/(l+)/, nil] }

# a failed match answers nil without ever looking at the capture argument
assert_nil "hello"[/(?<x>z)/, :zz]
assert_nil "hello"[/(z)/, 1]
end

assert("String#[] with regexp sets the match globals") do
assert_equal "ll", "hello"[/l+/]
assert_equal "ll", $~[0]
assert_equal "ll", Regexp.last_match(0)

"hello"[/(l)(l)/, 2]
assert_equal "l", $1

# a failed match clears $~, which is why this goes through `match` rather
# than `match?`
assert_nil "hello"[/z/]
assert_nil $~
end

assert("String#[] delegates non-regexp arguments") do
assert_equal "e", "hello"[1]
assert_equal "e", "hello".slice(1)
assert_equal "ell", "hello"[1, 3]
assert_equal "ell", "hello".slice(1, 3)
assert_equal "ell", "hello"[1..3]
assert_equal "llo", "hello"[-3..-1]
assert_equal "ll", "hello"["ll"]
assert_nil "hello"["bye"]
assert_nil "hello"[12]
assert_nil "hello"[12, 1]

# the same delegation on a subclass receiver, which OP_GETIDX never
# shortcuts and which therefore always arrives through the override
sub = Class.new(String)
assert_equal "e", sub.new("hello")[1]
assert_equal "ell", sub.new("hello")[1..3]
assert_equal "ll", sub.new("hello")["ll"]

assert_raise(ArgumentError) { "hello"[] }
assert_raise(ArgumentError) { "hello"[1, 2, 3] }
assert_raise(ArgumentError) { "hello"[/l/, 1, 2] }
assert_raise(ArgumentError) { "hello".slice(1, 2, 3) }
assert_raise(TypeError) { "hello"[nil] }
end

assert("String#[] reads the real type of its argument") do
# `is_a?` is redefinable, so an object claiming not to be a Regexp must not
# be read as an index, and a non-Regexp claiming to be one must not be
# matched against
re = /l+/
def re.is_a?(klass); false; end
assert_equal "ll", "hello"[re]

fake = Object.new
def fake.is_a?(klass); true; end
def fake.match(str); raise "must not be called"; end
assert_raise(TypeError) { "hello"[fake] }
end
17 changes: 17 additions & 0 deletions mrbgems/mruby-regexp/test/symbol_regexp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,20 @@
assert_false :"あいあ".match?(Regexp.new("い"), 2)
assert_equal 1, :"あい" =~ Regexp.new("い")
end

assert("Symbol#[] with regexp") do
# Symbol#[] and #slice live in mruby-symbol-ext, which this gem does not
# depend on; they delegate to the String methods, so the regexp form
# arrives from string_regexp.rb wherever that gem is built in.
skip unless :hello.respond_to?(:slice)
assert_equal "ll", :hello[Regexp.new("l+")]
assert_equal "ll", :hello.slice(Regexp.new("l+"))
assert_equal "ll", :hello[Regexp.new("(?<x>l+)"), :x]
assert_equal "o", :hello[Regexp.new("(l+)(o)"), 2]
assert_nil :hello[Regexp.new("z")]

assert_equal "ll", :hello[Regexp.new("l+")]
assert_equal "ll", $~[0]
assert_nil :hello[Regexp.new("z")]
assert_nil $~
end
Loading