mruby-regexp: add named captures (?<name>...)

support named capture groups in patterns:
- compiler parses (?<name>...) syntax and builds name table
- MatchData#[:name] and MatchData#["name"] access by name
- MatchData#named_captures returns {name => value} hash
- Regexp#named_captures returns {name => group_number} hash
- named captures stored in mrb_regexp_pattern for GC safety

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2026-03-21 12:55:35 +09:00
parent 8d92379d7c
commit 7283560215
5 changed files with 147 additions and 10 deletions
@@ -47,6 +47,13 @@ typedef struct {
mrb_bool utf8_any; /* match any non-ASCII byte if true */
} re_charclass;
/* Named capture entry */
typedef struct {
const char *name;
uint16_t name_len;
uint16_t group;
} re_named_capture;
/* Compiled regexp pattern */
typedef struct mrb_regexp_pattern {
re_inst *code; /* bytecode array */
@@ -55,6 +62,8 @@ typedef struct mrb_regexp_pattern {
uint16_t num_classes;
uint16_t num_captures; /* number of capture groups (including group 0) */
uint32_t flags;
re_named_capture *named_captures;
uint16_t num_named;
mrb_bool has_backref; /* true if pattern uses \1-\9 */
mrb_bool has_nongreedy; /* true if pattern uses *?, +?, ?? */
} mrb_regexp_pattern;
+11 -1
View File
@@ -3,14 +3,24 @@ class Regexp
new(pattern, *args)
end
# Return named captures hash: {"name" => group_number, ...}
def named_captures
@named_captures || {}
end
def options
@flags.to_i
end
# $1-$9 convenience methods via $~
def self.last_match(n = nil)
md = $~
return md if n.nil?
md ? md[n] : nil
end
# named capture info is set via C create_matchdata
end
class MatchData
# named_captures is implemented in C via md->regexp
end
+30 -3
View File
@@ -25,6 +25,8 @@ typedef struct {
uint16_t class_capa;
uint16_t num_captures;
uint32_t flags;
re_named_capture *named_captures;
uint16_t num_named;
mrb_bool has_backref;
mrb_bool has_nongreedy;
} re_compiler;
@@ -289,9 +291,22 @@ compile_atom(re_compiler *c)
next_char(c);
mrb_bool capturing = TRUE;
if (peek(c) == '?' && c->p + 1 < c->src_end && c->p[1] == ':') {
next_char(c); next_char(c); /* skip ?: */
capturing = FALSE;
const char *cap_name = NULL;
uint16_t cap_name_len = 0;
if (peek(c) == '?' && c->p + 1 < c->src_end) {
if (c->p[1] == ':') {
next_char(c); next_char(c); /* skip ?: */
capturing = FALSE;
}
else if (c->p[1] == '<' && c->p + 2 < c->src_end && c->p[2] != '=' && c->p[2] != '!') {
next_char(c); next_char(c); /* skip ?< */
cap_name = c->p;
while (peek(c) != '>' && peek(c) >= 0) next_char(c);
if (peek(c) != '>') compile_error(c, "unterminated named capture");
cap_name_len = (uint16_t)(c->p - cap_name);
next_char(c); /* skip > */
}
}
uint16_t group = 0;
@@ -301,6 +316,15 @@ compile_atom(re_compiler *c)
}
group = c->num_captures++;
emit(c, RE_SAVE, 0, group * 2);
if (cap_name) {
/* register named capture */
c->named_captures = (re_named_capture*)mrb_realloc(c->mrb, c->named_captures,
sizeof(re_named_capture) * (c->num_named + 1));
c->named_captures[c->num_named].name = cap_name;
c->named_captures[c->num_named].name_len = cap_name_len;
c->named_captures[c->num_named].group = group;
c->num_named++;
}
}
compile_alt(c);
@@ -601,6 +625,8 @@ re_compile(mrb_state *mrb, const char *pattern, mrb_int len, uint32_t flags)
pat->num_classes = c.num_classes;
pat->num_captures = c.num_captures;
pat->flags = flags;
pat->named_captures = c.named_captures;
pat->num_named = c.num_named;
pat->has_backref = c.has_backref;
pat->has_nongreedy = c.has_nongreedy;
@@ -613,6 +639,7 @@ re_free(mrb_state *mrb, mrb_regexp_pattern *pat)
if (pat) {
mrb_free(mrb, pat->code);
mrb_free(mrb, pat->classes);
mrb_free(mrb, pat->named_captures);
mrb_free(mrb, pat);
}
}
+82 -6
View File
@@ -10,6 +10,7 @@
#include <mruby/string.h>
#include <mruby/array.h>
#include <mruby/variable.h>
#include <mruby/hash.h>
#include <mruby/error.h>
#include "re_internal.h"
@@ -25,6 +26,7 @@ static const struct mrb_data_type regexp_type = { "Regexp", regexp_free };
/* MatchData */
typedef struct {
mrb_value source; /* source string */
mrb_value regexp; /* Regexp object (for named captures) */
int *captures; /* capture positions [start0,end0,start1,end1,...] */
int num_captures; /* number of capture groups (including 0) */
} mrb_match_data;
@@ -89,22 +91,32 @@ regexp_init(mrb_state *mrb, mrb_value self)
mrb_iv_set(mrb, self, mrb_intern_lit(mrb, "@source"), pattern);
mrb_iv_set(mrb, self, mrb_intern_lit(mrb, "@flags"), mrb_int_value(mrb, (mrb_int)flags));
/* store named captures as hash */
if (pat->num_named > 0) {
mrb_value nc = mrb_hash_new_capa(mrb, pat->num_named);
for (uint16_t i = 0; i < pat->num_named; i++) {
mrb_value name = mrb_str_new(mrb, pat->named_captures[i].name, pat->named_captures[i].name_len);
mrb_hash_set(mrb, nc, name, mrb_fixnum_value(pat->named_captures[i].group));
}
mrb_iv_set(mrb, self, mrb_intern_lit(mrb, "@named_captures"), nc);
}
return self;
}
/* Create MatchData from captures */
static mrb_value
create_matchdata(mrb_state *mrb, mrb_value str, int *captures, int ncap)
create_matchdata(mrb_state *mrb, mrb_value regexp, mrb_value str, int *captures, int ncap)
{
struct RClass *md_class = mrb_class_get(mrb, "MatchData");
mrb_match_data *md = (mrb_match_data*)mrb_malloc(mrb, sizeof(mrb_match_data));
md->source = str;
md->regexp = regexp;
md->num_captures = ncap / 2;
md->captures = (int*)mrb_malloc(mrb, sizeof(int) * ncap);
memcpy(md->captures, captures, sizeof(int) * ncap);
mrb_value obj = mrb_obj_value(mrb_data_object_alloc(mrb, md_class, md, &matchdata_type));
/* store in $~ */
mrb_gv_set(mrb, mrb_intern_lit(mrb, "$~"), obj);
return obj;
}
@@ -133,7 +145,7 @@ regexp_match(mrb_state *mrb, mrb_value self)
return mrb_nil_value();
}
return create_matchdata(mrb, str, captures, pat->num_captures * 2);
return create_matchdata(mrb, self, str, captures, pat->num_captures * 2);
}
/*
@@ -179,7 +191,7 @@ regexp_match_op(mrb_state *mrb, mrb_value self)
mrb_gv_set(mrb, mrb_intern_lit(mrb, "$~"), mrb_nil_value());
return mrb_nil_value();
}
create_matchdata(mrb, str, captures, pat->num_captures * 2);
create_matchdata(mrb, self, str, captures, pat->num_captures * 2);
return mrb_int_value(mrb, captures[0]);
}
@@ -266,12 +278,45 @@ regexp_escape(mrb_state *mrb, mrb_value self)
static mrb_value
matchdata_aref(mrb_state *mrb, mrb_value self)
{
mrb_int idx;
mrb_get_args(mrb, "i", &idx);
mrb_value arg;
mrb_get_args(mrb, "o", &arg);
mrb_match_data *md = DATA_GET_PTR(mrb, self, &matchdata_type, mrb_match_data);
if (!md) return mrb_nil_value();
mrb_int idx;
if (mrb_string_p(arg) || mrb_symbol_p(arg)) {
/* named capture access */
const char *name;
mrb_int name_len;
if (mrb_symbol_p(arg)) {
name = mrb_sym_name_len(mrb, mrb_symbol(arg), &name_len);
}
else {
name = RSTRING_PTR(arg);
name_len = RSTRING_LEN(arg);
}
/* look up name in regexp's named captures */
mrb_regexp_pattern *pat = NULL;
if (!mrb_nil_p(md->regexp)) {
pat = DATA_GET_PTR(mrb, md->regexp, &regexp_type, mrb_regexp_pattern);
}
if (pat) {
for (uint16_t i = 0; i < pat->num_named; i++) {
if (pat->named_captures[i].name_len == (uint16_t)name_len &&
memcmp(pat->named_captures[i].name, name, name_len) == 0) {
idx = pat->named_captures[i].group;
goto found;
}
}
}
return mrb_nil_value();
}
else {
idx = mrb_as_int(mrb, arg);
}
found:
if (idx < 0 || idx >= md->num_captures) return mrb_nil_value();
int start = md->captures[idx * 2];
int end = md->captures[idx * 2 + 1];
@@ -386,6 +431,36 @@ matchdata_length(mrb_state *mrb, mrb_value self)
return mrb_fixnum_value(md->num_captures);
}
/*
* MatchData#named_captures
*/
static mrb_value
matchdata_named_captures(mrb_state *mrb, mrb_value self)
{
mrb_match_data *md = DATA_GET_PTR(mrb, self, &matchdata_type, mrb_match_data);
if (!md) return mrb_hash_new(mrb);
mrb_regexp_pattern *pat = NULL;
if (!mrb_nil_p(md->regexp)) {
pat = DATA_GET_PTR(mrb, md->regexp, &regexp_type, mrb_regexp_pattern);
}
if (!pat || pat->num_named == 0) return mrb_hash_new(mrb);
mrb_value result = mrb_hash_new_capa(mrb, pat->num_named);
for (uint16_t i = 0; i < pat->num_named; i++) {
mrb_value name = mrb_str_new(mrb, pat->named_captures[i].name, pat->named_captures[i].name_len);
int group = pat->named_captures[i].group;
mrb_value val = mrb_nil_value();
if (group >= 0 && group < md->num_captures) {
int s = md->captures[group * 2];
int e = md->captures[group * 2 + 1];
if (s >= 0) val = mrb_str_substr(mrb, md->source, s, e - s);
}
mrb_hash_set(mrb, result, name, val);
}
return result;
}
/* --- Gem init --- */
void
@@ -427,6 +502,7 @@ mrb_mruby_regexp_gem_init(mrb_state *mrb)
mrb_define_method(mrb, md, "end", matchdata_end, MRB_ARGS_REQ(1));
mrb_define_method(mrb, md, "pre_match", matchdata_pre, MRB_ARGS_NONE());
mrb_define_method(mrb, md, "post_match", matchdata_post, MRB_ARGS_NONE());
mrb_define_method(mrb, md, "named_captures", matchdata_named_captures, MRB_ARGS_NONE());
}
void
+15
View File
@@ -234,3 +234,18 @@ end
assert("Regexp - backreference no match") do
assert_nil /(\w+) \1/.match("hello world")
end
assert("Regexp - named captures") do
md = /(?<year>\d+)-(?<month>\d+)-(?<day>\d+)/.match("2026-03-21")
assert_equal "2026", md[:year]
assert_equal "03", md[:month]
assert_equal "21", md[:day]
assert_equal "2026", md["year"]
end
assert("MatchData#named_captures") do
md = /(?<a>\w+)@(?<b>\w+)/.match("user@host")
nc = md.named_captures
assert_equal "user", nc["a"]
assert_equal "host", nc["b"]
end