From 72835602152e2e6e6d84580e758f9537517f17d3 Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Sat, 21 Mar 2026 12:55:35 +0900 Subject: [PATCH] mruby-regexp: add named captures (?...) support named capture groups in patterns: - compiler parses (?...) 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 --- mrbgems/mruby-regexp/include/re_internal.h | 9 +++ mrbgems/mruby-regexp/mrblib/regexp.rb | 12 ++- mrbgems/mruby-regexp/src/re_compile.c | 33 +++++++- mrbgems/mruby-regexp/src/regexp.c | 88 ++++++++++++++++++++-- mrbgems/mruby-regexp/test/regexp.rb | 15 ++++ 5 files changed, 147 insertions(+), 10 deletions(-) diff --git a/mrbgems/mruby-regexp/include/re_internal.h b/mrbgems/mruby-regexp/include/re_internal.h index 5ad3245be..20f9f15b8 100644 --- a/mrbgems/mruby-regexp/include/re_internal.h +++ b/mrbgems/mruby-regexp/include/re_internal.h @@ -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; diff --git a/mrbgems/mruby-regexp/mrblib/regexp.rb b/mrbgems/mruby-regexp/mrblib/regexp.rb index f56b066f6..b50e221b3 100644 --- a/mrbgems/mruby-regexp/mrblib/regexp.rb +++ b/mrbgems/mruby-regexp/mrblib/regexp.rb @@ -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 diff --git a/mrbgems/mruby-regexp/src/re_compile.c b/mrbgems/mruby-regexp/src/re_compile.c index a667e1eec..568b5b855 100644 --- a/mrbgems/mruby-regexp/src/re_compile.c +++ b/mrbgems/mruby-regexp/src/re_compile.c @@ -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); } } diff --git a/mrbgems/mruby-regexp/src/regexp.c b/mrbgems/mruby-regexp/src/regexp.c index 953fac50e..0cd748dec 100644 --- a/mrbgems/mruby-regexp/src/regexp.c +++ b/mrbgems/mruby-regexp/src/regexp.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #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, ®exp_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, ®exp_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 diff --git a/mrbgems/mruby-regexp/test/regexp.rb b/mrbgems/mruby-regexp/test/regexp.rb index 76ba4e802..0537c6b36 100644 --- a/mrbgems/mruby-regexp/test/regexp.rb +++ b/mrbgems/mruby-regexp/test/regexp.rb @@ -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 = /(?\d+)-(?\d+)-(?\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 = /(?\w+)@(?\w+)/.match("user@host") + nc = md.named_captures + assert_equal "user", nc["a"] + assert_equal "host", nc["b"] +end