mruby-regexp: add built-in regexp engine with Pike VM

implement a lightweight NFA-based regular expression engine for mruby:

engine (src/re_compile.c, src/re_exec.c, src/re_utf8.c):
- Pike VM (Thompson NFA simulation) with O(n*m) time guarantee
- ReDoS-resistant by design (no backtracking for basic patterns)
- supports: literals, ., *, +, ?, {n,m}, [], [^], |, ()
- character classes: \d, \w, \s and negations
- anchors: ^, $, \A, \z, \Z, \b, \B
- flags: i (ignorecase), m (multiline/dotall)
- captures with MatchData

Ruby API (src/regexp.c, mrblib/string_regexp.rb):
- Regexp.new, #match, #match?, #=~, #===, #source, #inspect
- Regexp.escape, Regexp::IGNORECASE/MULTILINE constants
- MatchData#[], #captures, #to_a, #begin, #end, #pre_match, #post_match
- String#match, #match?, #=~, #sub, #gsub, #scan, #split

~1700 lines of C + ~120 lines of Ruby. no external dependencies.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2026-03-21 00:19:17 +09:00
parent 30e41242ec
commit 1cfa153ff3
9 changed files with 1757 additions and 0 deletions
+3
View File
@@ -13,6 +13,9 @@ MRuby::Build.new('host') do |conf|
# Generate mruby debugger command (require mruby-eval)
conf.gem :core => "mruby-bin-debugger"
# Regexp
conf.gem :core => "mruby-regexp"
# test
conf.enable_test
# bintest
@@ -0,0 +1,95 @@
/*
** re_internal.h - internal definitions for regexp engine
**
** See Copyright Notice in mruby.h
*/
#ifndef MRB_RE_INTERNAL_H
#define MRB_RE_INTERNAL_H
#include <mruby.h>
#include <stdint.h>
/* Bytecode instructions for the NFA engine */
enum re_opcode {
RE_CHAR, /* match literal byte: operand = byte value */
RE_ANY, /* match any character (. without DOTALL) */
RE_ANY_NL, /* match any character including newline (. with DOTALL) */
RE_CLASS, /* match character class: operand = class_id */
RE_NCLASS, /* match negated character class: operand = class_id */
RE_MATCH, /* successful match */
RE_JMP, /* unconditional jump: operand = target offset */
RE_SPLIT, /* fork: operand = target offset (greedy: try next first) */
RE_SPLITNG, /* fork: operand = target offset (non-greedy: try jump first) */
RE_SAVE, /* save capture position: operand = slot number */
RE_BOL, /* assert beginning of line (^) */
RE_EOL, /* assert end of line ($) */
RE_BOT, /* assert beginning of text (\A) */
RE_EOT, /* assert end of text (\z) */
RE_EOTNL, /* assert end of text or before final \n (\Z) */
RE_WBOUND, /* assert word boundary (\b) */
RE_NWBOUND, /* assert non-word boundary (\B) */
RE_BACKREF, /* backreference: operand = group number */
};
/* Bytecode instruction (4 bytes each for alignment) */
typedef struct {
uint8_t op;
uint8_t a; /* small operand or class id */
uint16_t offset; /* jump target or extended operand */
} re_inst;
/* Character class bitmap (ASCII range) */
#define RE_CLASS_BITMAP_SIZE 16 /* 128 bits = 16 bytes for ASCII */
typedef struct {
uint8_t bitmap[RE_CLASS_BITMAP_SIZE]; /* bitmap for 0-127 */
mrb_bool negated;
mrb_bool utf8_any; /* match any non-ASCII byte if true */
} re_charclass;
/* Compiled regexp pattern */
typedef struct mrb_regexp_pattern {
re_inst *code; /* bytecode array */
uint32_t code_len; /* number of instructions */
re_charclass *classes; /* character class table */
uint16_t num_classes;
uint16_t num_captures; /* number of capture groups (including group 0) */
uint32_t flags;
mrb_bool has_backref; /* true if pattern uses \1-\9 */
} mrb_regexp_pattern;
/* Regexp flags */
#define RE_FLAG_IGNORECASE 1
#define RE_FLAG_MULTILINE 2 /* ^ and $ match at \n boundaries */
#define RE_FLAG_DOTALL 4 /* . matches \n (Ruby's /m for dot behavior) */
/* Note: Ruby's /m flag means BOTH multiline anchors AND dotall.
Ruby's /i flag is ignorecase. */
/* Step limit for ReDoS protection */
#ifndef MRB_REGEXP_STEP_LIMIT
#define MRB_REGEXP_STEP_LIMIT 1000000
#endif
/* Maximum captures */
#define RE_MAX_CAPTURES 32
/* Compile a pattern string into bytecode */
mrb_regexp_pattern* re_compile(mrb_state *mrb, const char *pattern, mrb_int len, uint32_t flags);
/* Free a compiled pattern */
void re_free(mrb_state *mrb, mrb_regexp_pattern *pat);
/* Execute a match.
Returns number of captures filled (0 = no match).
captures[2*n] = start, captures[2*n+1] = end for group n. */
int re_exec(mrb_state *mrb, const mrb_regexp_pattern *pat,
const char *str, mrb_int len, mrb_int start,
int *captures, int captures_size);
/* UTF-8 helpers */
int re_utf8_charlen(const char *s, const char *end);
uint32_t re_utf8_decode(const char *s, int *len);
mrb_bool re_is_word_char(uint32_t c);
#endif /* MRB_RE_INTERNAL_H */
+7
View File
@@ -0,0 +1,7 @@
MRuby::Gem::Specification.new('mruby-regexp') do |spec|
spec.license = 'MIT'
spec.authors = 'mruby developers'
spec.summary = 'Regexp class (built-in NFA engine)'
spec.add_dependency 'mruby-string-ext', :core => 'mruby-string-ext'
end
@@ -0,0 +1,126 @@
class String
def match(re, pos = 0)
re = Regexp.new(re) if re.is_a?(String)
re.match(self, pos)
end
def match?(re, pos = 0)
re = Regexp.new(re) if re.is_a?(String)
re.match?(self, pos)
end
def =~(re)
re =~ self
end
def sub(pattern, replacement = nil, &block)
pattern = Regexp.new(Regexp.escape(pattern)) if pattern.is_a?(String)
md = pattern.match(self)
return self.dup unless md
pre = md.pre_match
post = md.post_match
if block
rep = block.call(md[0]).to_s
else
rep = replacement.to_s
# handle \0, \1, etc. in replacement string
rep = rep.gsub(/\\(\d)/) { md[$1.to_i] || "" } if rep.include?("\\")
end
pre + rep + post
end
def gsub(pattern, replacement = nil, &block)
pattern = Regexp.new(Regexp.escape(pattern)) if pattern.is_a?(String)
result = ""
rest = self
while rest.length > 0
md = pattern.match(rest)
break unless md
result += md.pre_match
if block
result += block.call(md[0]).to_s
else
rep = replacement.to_s
rep = rep.gsub(/\\(\d)/) { md[$1.to_i] || "" } if rep.include?("\\")
result += rep
end
matched_len = md[0].length
if matched_len == 0
# avoid infinite loop on zero-length match
result += rest[0] if rest.length > 0
rest = rest[1..-1] || ""
else
rest = md.post_match
end
end
result + rest
end
def scan(pattern)
pattern = Regexp.new(Regexp.escape(pattern)) if pattern.is_a?(String)
result = []
pos = 0
while pos <= self.length
md = pattern.match(self, pos)
break unless md
if md.captures.empty?
result << md[0]
elsif md.captures.length == 1
result << md.captures[0]
else
result << md.captures
end
if md[0].length == 0
pos = md.end(0) + 1
else
pos = md.end(0)
end
end
if block_given?
result.each { |m| yield m }
self
else
result
end
end
def split(pattern = nil, limit = -1)
return super if pattern.nil?
if pattern.is_a?(String)
return super if pattern.length == 1 || !pattern.include?('\\')
pattern = Regexp.new(Regexp.escape(pattern))
end
result = []
rest = self
count = 0
while rest.length > 0
if limit > 0 && count >= limit - 1
result << rest
return result
end
md = pattern.match(rest)
break unless md
result << md.pre_match
rest = md.post_match
count += 1
# skip zero-length match at beginning
if md[0].length == 0
if rest.length > 0
result[-1] = result[-1] + rest[0]
rest = rest[1..-1] || ""
else
break
end
end
end
result << rest
# remove trailing empty strings if no limit
if limit < 0
while result.length > 0 && result[-1] == ""
result.pop
end
end
result
end
end
+611
View File
@@ -0,0 +1,611 @@
/*
** re_compile.c - regexp pattern compiler
**
** Compiles a regular expression pattern string into bytecode
** for the NFA execution engine.
**
** See Copyright Notice in mruby.h
*/
#include "re_internal.h"
#include <mruby/error.h>
#include <string.h>
/* Compiler state */
typedef struct {
mrb_state *mrb;
const char *src; /* pattern source */
const char *src_end;
const char *p; /* current position */
re_inst *code; /* instruction array */
uint32_t code_len;
uint32_t code_capa;
re_charclass *classes;
uint16_t num_classes;
uint16_t class_capa;
uint16_t num_captures;
uint32_t flags;
mrb_bool has_backref;
} re_compiler;
static void compile_alt(re_compiler *c); /* forward */
static void
compile_error(re_compiler *c, const char *msg)
{
mrb_raisef(c->mrb, mrb_exc_get_id(c->mrb, MRB_SYM(RegexpError)), "%s: /%s/", msg, c->src);
}
static uint32_t
emit(re_compiler *c, uint8_t op, uint8_t a, uint16_t offset)
{
if (c->code_len >= c->code_capa) {
c->code_capa = c->code_capa ? c->code_capa * 2 : 64;
c->code = (re_inst*)mrb_realloc(c->mrb, c->code, sizeof(re_inst) * c->code_capa);
}
uint32_t pos = c->code_len++;
c->code[pos].op = op;
c->code[pos].a = a;
c->code[pos].offset = offset;
return pos;
}
static void
patch(re_compiler *c, uint32_t pos, uint16_t offset)
{
c->code[pos].offset = offset;
}
/* Insert an instruction at position `pos` by shifting code.
Adjusts all jump offsets >= pos by +1. */
static void
insert_inst(re_compiler *c, uint32_t pos, uint8_t op, uint8_t a, uint16_t offset)
{
emit(c, RE_JMP, 0, 0); /* grow array */
uint32_t len = c->code_len - 1 - pos;
memmove(&c->code[pos + 1], &c->code[pos], sizeof(re_inst) * len);
c->code[pos].op = op;
c->code[pos].a = a;
c->code[pos].offset = offset;
/* fix all jump targets that point at or past the insertion point */
for (uint32_t i = 0; i < c->code_len; i++) {
if (i == pos) continue;
switch (c->code[i].op) {
case RE_JMP: case RE_SPLIT: case RE_SPLITNG:
if (c->code[i].offset >= pos && c->code[i].offset < 0xffff) {
c->code[i].offset++;
}
break;
default:
break;
}
}
}
static int
peek(re_compiler *c)
{
if (c->p >= c->src_end) return -1;
return (uint8_t)*c->p;
}
static int
next_char(re_compiler *c)
{
if (c->p >= c->src_end) return -1;
return (uint8_t)*c->p++;
}
static uint16_t
add_class(re_compiler *c)
{
if (c->num_classes >= c->class_capa) {
c->class_capa = c->class_capa ? c->class_capa * 2 : 8;
c->classes = (re_charclass*)mrb_realloc(c->mrb, c->classes, sizeof(re_charclass) * c->class_capa);
}
uint16_t id = c->num_classes++;
memset(&c->classes[id], 0, sizeof(re_charclass));
return id;
}
static void
class_set_bit(re_charclass *cc, uint8_t ch)
{
if (ch < 128) {
cc->bitmap[ch >> 3] |= (1 << (ch & 7));
}
}
static void
class_set_range(re_charclass *cc, uint8_t lo, uint8_t hi)
{
for (int i = lo; i <= hi; i++) {
class_set_bit(cc, (uint8_t)i);
}
}
static void
class_add_shorthand(re_charclass *cc, int ch)
{
switch (ch) {
case 'd':
class_set_range(cc, '0', '9');
break;
case 'D':
class_set_range(cc, 0, '0'-1);
class_set_range(cc, '9'+1, 127);
cc->utf8_any = TRUE;
break;
case 'w':
class_set_range(cc, 'a', 'z');
class_set_range(cc, 'A', 'Z');
class_set_range(cc, '0', '9');
class_set_bit(cc, '_');
break;
case 'W':
for (int i = 0; i < 128; i++) {
if (!re_is_word_char(i)) class_set_bit(cc, (uint8_t)i);
}
cc->utf8_any = TRUE;
break;
case 's':
class_set_bit(cc, ' ');
class_set_bit(cc, '\t');
class_set_bit(cc, '\n');
class_set_bit(cc, '\r');
class_set_bit(cc, '\f');
class_set_bit(cc, '\v');
break;
case 'S':
for (int i = 0; i < 128; i++) {
if (i != ' ' && i != '\t' && i != '\n' && i != '\r' && i != '\f' && i != '\v')
class_set_bit(cc, (uint8_t)i);
}
cc->utf8_any = TRUE;
break;
}
}
static int
parse_escape(re_compiler *c)
{
int ch = next_char(c);
if (ch < 0) compile_error(c, "trailing backslash");
switch (ch) {
case 'n': return '\n';
case 't': return '\t';
case 'r': return '\r';
case 'f': return '\f';
case 'v': return '\v';
case 'a': return '\a';
case 'e': return 0x1b;
default: return ch; /* literal: \., \\, \/, \(, etc. */
}
}
/* Parse [...] character class */
static void
compile_charclass(re_compiler *c)
{
uint16_t id = add_class(c);
re_charclass *cc = &c->classes[id];
mrb_bool negated = FALSE;
if (peek(c) == '^') {
next_char(c);
negated = TRUE;
}
mrb_bool first = TRUE;
while (peek(c) != ']' || first) {
int ch;
if (peek(c) < 0) compile_error(c, "unterminated character class");
first = FALSE;
if (peek(c) == '\\') {
next_char(c);
int esc = peek(c);
if (esc == 'd' || esc == 'D' || esc == 'w' || esc == 'W' || esc == 's' || esc == 'S') {
next_char(c);
class_add_shorthand(cc, esc);
continue;
}
ch = parse_escape(c);
}
else {
ch = next_char(c);
}
/* check for range a-z */
if (peek(c) == '-' && c->p + 1 < c->src_end && c->p[1] != ']') {
next_char(c); /* skip '-' */
int hi;
if (peek(c) == '\\') {
next_char(c);
hi = parse_escape(c);
}
else {
hi = next_char(c);
}
if (ch < 128 && hi < 128) {
class_set_range(cc, (uint8_t)ch, (uint8_t)hi);
}
}
else {
if (ch < 128) class_set_bit(cc, (uint8_t)ch);
}
}
next_char(c); /* skip ']' */
cc->negated = negated;
emit(c, negated ? RE_NCLASS : RE_CLASS, (uint8_t)id, 0);
}
/* Parse {n}, {n,}, {n,m} quantifier. Returns min,max via pointers. */
static mrb_bool
parse_quantifier(re_compiler *c, int *min_out, int *max_out)
{
const char *save = c->p;
int min = 0, max = -1;
while (peek(c) >= '0' && peek(c) <= '9') {
min = min * 10 + (next_char(c) - '0');
}
if (peek(c) == ',') {
next_char(c);
if (peek(c) >= '0' && peek(c) <= '9') {
max = 0;
while (peek(c) >= '0' && peek(c) <= '9') {
max = max * 10 + (next_char(c) - '0');
}
}
/* else max = -1 (unlimited) */
}
else {
max = min; /* {n} means exactly n */
}
if (peek(c) != '}') {
c->p = save; /* not a quantifier, treat { as literal */
return FALSE;
}
next_char(c); /* skip '}' */
*min_out = min;
*max_out = max;
return TRUE;
}
/* Compile a single atom (character, class, group, etc.) */
static void
compile_atom(re_compiler *c)
{
int ch = peek(c);
switch (ch) {
case '(':
{
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;
}
uint16_t group = 0;
if (capturing) {
if (c->num_captures >= RE_MAX_CAPTURES) {
compile_error(c, "too many capture groups");
}
group = c->num_captures++;
emit(c, RE_SAVE, 0, group * 2);
}
compile_alt(c);
if (peek(c) != ')') compile_error(c, "unmatched '('");
next_char(c);
if (capturing) {
emit(c, RE_SAVE, 0, group * 2 + 1);
}
}
break;
case '[':
next_char(c);
compile_charclass(c);
break;
case '.':
next_char(c);
emit(c, (c->flags & RE_FLAG_DOTALL) ? RE_ANY_NL : RE_ANY, 0, 0);
break;
case '^':
next_char(c);
emit(c, RE_BOL, 0, 0);
break;
case '$':
next_char(c);
emit(c, RE_EOL, 0, 0);
break;
case '\\':
next_char(c);
ch = peek(c);
if (ch >= '1' && ch <= '9') {
next_char(c);
emit(c, RE_BACKREF, (uint8_t)(ch - '0'), 0);
c->has_backref = TRUE;
}
else if (ch == 'd' || ch == 'D' || ch == 'w' || ch == 'W' || ch == 's' || ch == 'S') {
next_char(c);
uint16_t id = add_class(c);
class_add_shorthand(&c->classes[id], ch);
emit(c, (ch >= 'A' && ch <= 'Z') ? RE_NCLASS : RE_CLASS, (uint8_t)id, 0);
}
else if (ch == 'A') {
next_char(c);
emit(c, RE_BOT, 0, 0);
}
else if (ch == 'z') {
next_char(c);
emit(c, RE_EOT, 0, 0);
}
else if (ch == 'Z') {
next_char(c);
emit(c, RE_EOTNL, 0, 0);
}
else if (ch == 'b') {
next_char(c);
emit(c, RE_WBOUND, 0, 0);
}
else if (ch == 'B') {
next_char(c);
emit(c, RE_NWBOUND, 0, 0);
}
else {
ch = parse_escape(c);
if (c->flags & RE_FLAG_IGNORECASE) {
if (ch >= 'A' && ch <= 'Z') {
uint16_t id = add_class(c);
class_set_bit(&c->classes[id], (uint8_t)ch);
class_set_bit(&c->classes[id], (uint8_t)(ch + 32));
emit(c, RE_CLASS, (uint8_t)id, 0);
break;
}
else if (ch >= 'a' && ch <= 'z') {
uint16_t id = add_class(c);
class_set_bit(&c->classes[id], (uint8_t)ch);
class_set_bit(&c->classes[id], (uint8_t)(ch - 32));
emit(c, RE_CLASS, (uint8_t)id, 0);
break;
}
}
emit(c, RE_CHAR, (uint8_t)ch, 0);
}
break;
default:
if (ch < 0 || ch == ')' || ch == '|' || ch == '*' || ch == '+' || ch == '?' || ch == '{') {
return; /* not an atom */
}
next_char(c);
if ((c->flags & RE_FLAG_IGNORECASE) && ch < 128) {
if (ch >= 'A' && ch <= 'Z') {
uint16_t id = add_class(c);
class_set_bit(&c->classes[id], (uint8_t)ch);
class_set_bit(&c->classes[id], (uint8_t)(ch + 32));
emit(c, RE_CLASS, (uint8_t)id, 0);
break;
}
else if (ch >= 'a' && ch <= 'z') {
uint16_t id = add_class(c);
class_set_bit(&c->classes[id], (uint8_t)ch);
class_set_bit(&c->classes[id], (uint8_t)(ch - 32));
emit(c, RE_CLASS, (uint8_t)id, 0);
break;
}
}
emit(c, RE_CHAR, (uint8_t)ch, 0);
break;
}
}
/* Compile atom with quantifiers (*, +, ?, {n,m}) */
static void
compile_quantified(re_compiler *c)
{
uint32_t start = c->code_len;
compile_atom(c);
if (c->code_len == start) return; /* no atom emitted */
int ch = peek(c);
if (ch == '*' || ch == '+' || ch == '?') {
next_char(c);
mrb_bool nongreedy = (peek(c) == '?');
if (nongreedy) next_char(c);
uint32_t atom_len = c->code_len - start;
if (ch == '*') {
/* e* → L: SPLIT(body, end); body; JMP L; end:
SPLIT offset = end (after JMP), patched after JMP is emitted */
insert_inst(c, start, nongreedy ? RE_SPLITNG : RE_SPLIT, 0, 0);
emit(c, RE_JMP, 0, start);
c->code[start].offset = (uint16_t)c->code_len; /* patch: skip to end */
}
else if (ch == '+') {
/* e+ → body; SPLIT/SPLITNG(start)
SPLIT: first=pc+1(end), second=offset(start) → non-greedy
SPLITNG: first=offset(start), second=pc+1(end) → greedy */
emit(c, nongreedy ? RE_SPLIT : RE_SPLITNG, 0, start);
}
else { /* ? */
/* e? → SPLIT(body, end); body; end: */
insert_inst(c, start, nongreedy ? RE_SPLITNG : RE_SPLIT, 0, 0);
c->code[start].offset = (uint16_t)c->code_len; /* patch: skip to end */
}
}
else if (ch == '{') {
const char *save = c->p;
next_char(c);
int min, max;
if (!parse_quantifier(c, &min, &max)) {
c->p = save;
return; /* not a quantifier */
}
mrb_bool nongreedy = (peek(c) == '?');
if (nongreedy) next_char(c);
/* For {n,m}: repeat atom min times, then optional (max-min) times */
uint32_t atom_end = c->code_len;
uint32_t atom_size = atom_end - start;
/* First, we have one copy already. We need min-1 more mandatory copies. */
for (int i = 1; i < min; i++) {
for (uint32_t j = 0; j < atom_size; j++) {
emit(c, c->code[start + j].op, c->code[start + j].a, c->code[start + j].offset);
}
}
/* Then optional copies */
if (max < 0) {
/* {n,} = min copies + * */
uint32_t loop_start = c->code_len;
uint32_t split_pos = emit(c, nongreedy ? RE_SPLITNG : RE_SPLIT, 0, 0);
for (uint32_t j = 0; j < atom_size; j++) {
emit(c, c->code[start + j].op, c->code[start + j].a, c->code[start + j].offset);
}
emit(c, RE_JMP, 0, loop_start);
patch(c, split_pos, c->code_len);
}
else {
for (int i = min; i < max; i++) {
uint32_t split_pos = emit(c, nongreedy ? RE_SPLITNG : RE_SPLIT, 0, 0);
for (uint32_t j = 0; j < atom_size; j++) {
emit(c, c->code[start + j].op, c->code[start + j].a, c->code[start + j].offset);
}
patch(c, split_pos, c->code_len);
}
}
}
}
/* Compile a sequence of quantified atoms */
static void
compile_seq(re_compiler *c)
{
while (peek(c) >= 0 && peek(c) != ')' && peek(c) != '|') {
compile_quantified(c);
}
}
/* Compile alternation: seq | seq | ... */
static void
compile_alt(re_compiler *c)
{
uint32_t alt_start = c->code_len;
compile_seq(c);
if (peek(c) != '|') return;
/* a|b → SPLIT L1 L2; L1: a; JMP END; L2: b; END:
We need to insert SPLIT before already-emitted code for first alt.
Strategy: emit JMP after first alt, then for each subsequent alt,
insert a SPLIT before it by shifting code. */
/* Collect all alternatives, then emit SPLIT chain at the end.
This avoids insert_inst offset corruption for multi-way alternation. */
uint32_t alt_starts[64]; /* start positions of each alternative */
int num_alts = 0;
alt_starts[num_alts++] = alt_start;
while (peek(c) == '|') {
next_char(c);
emit(c, RE_JMP, 0, 0); /* placeholder: jump to end */
alt_starts[num_alts++] = c->code_len;
if (num_alts >= 64) compile_error(c, "too many alternatives");
compile_seq(c);
}
if (num_alts <= 1) return; /* shouldn't happen, but safety */
/* Now insert SPLIT chain before the alternatives.
For n alternatives: n-1 SPLIT instructions, each pointing to
their respective alternative. */
uint32_t split_count = (uint32_t)(num_alts - 1);
/* Insert split_count instructions at alt_starts[0] */
for (uint32_t i = 0; i < split_count; i++) {
insert_inst(c, alt_starts[0], RE_JMP, 0, 0); /* placeholder */
/* adjust all alt_starts by +1 due to insertion */
for (int j = 0; j < num_alts; j++) {
alt_starts[j]++;
}
}
/* Now set up SPLIT chain: each SPLIT tries next instruction or jumps to alt */
for (uint32_t i = 0; i < split_count; i++) {
uint32_t pos = alt_starts[0] - split_count + i;
c->code[pos].op = RE_SPLIT;
c->code[pos].a = 0;
c->code[pos].offset = (uint16_t)alt_starts[i + 1];
}
/* Patch JMPs (they are right before each alt_starts[1..n-1]) to point to end */
uint32_t end = c->code_len;
for (int i = 1; i < num_alts; i++) {
uint32_t jmp_pos = alt_starts[i] - 1;
c->code[jmp_pos].op = RE_JMP;
c->code[jmp_pos].offset = (uint16_t)end;
}
}
mrb_regexp_pattern*
re_compile(mrb_state *mrb, const char *pattern, mrb_int len, uint32_t flags)
{
re_compiler c;
memset(&c, 0, sizeof(c));
c.mrb = mrb;
c.src = pattern;
c.src_end = pattern + len;
c.p = pattern;
c.flags = flags;
c.num_captures = 1; /* group 0 = whole match */
/* group 0 start */
emit(&c, RE_SAVE, 0, 0);
compile_alt(&c);
if (c.p < c.src_end) {
compile_error(&c, "unmatched ')'");
}
/* group 0 end */
emit(&c, RE_SAVE, 0, 1);
emit(&c, RE_MATCH, 0, 0);
mrb_regexp_pattern *pat = (mrb_regexp_pattern*)mrb_malloc(mrb, sizeof(mrb_regexp_pattern));
pat->code = c.code;
pat->code_len = c.code_len;
pat->classes = c.classes;
pat->num_classes = c.num_classes;
pat->num_captures = c.num_captures;
pat->flags = flags;
pat->has_backref = c.has_backref;
return pat;
}
void
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);
}
}
+271
View File
@@ -0,0 +1,271 @@
/*
** re_exec.c - NFA execution engine (Pike VM)
**
** Executes compiled regexp bytecode using Thompson/Pike NFA simulation.
** O(pattern * text) time complexity guarantees ReDoS resistance.
**
** See Copyright Notice in mruby.h
*/
#include "re_internal.h"
#include <string.h>
/* NFA thread: a position in the bytecode + captured positions */
typedef struct {
uint32_t pc;
int captures[RE_MAX_CAPTURES * 2];
} re_thread;
/* Thread list for NFA simulation */
typedef struct {
re_thread *threads;
int count;
int capa;
} re_threadlist;
/* Match result: updated when RE_MATCH is reached during epsilon traversal */
typedef struct {
mrb_bool matched;
int captures[RE_MAX_CAPTURES * 2];
} re_match_result;
static void
threadlist_init(mrb_state *mrb, re_threadlist *l, int capa)
{
l->threads = (re_thread*)mrb_malloc(mrb, sizeof(re_thread) * capa);
l->count = 0;
l->capa = capa;
}
static void
threadlist_free(mrb_state *mrb, re_threadlist *l)
{
mrb_free(mrb, l->threads);
}
/* Add a thread, following epsilon transitions (JMP, SPLIT, SAVE, assertions).
visited[] prevents adding duplicate threads at the same pc.
When RE_MATCH is reached, records in result and does NOT add to thread list. */
static void
add_thread(const mrb_regexp_pattern *pat, re_threadlist *list,
re_thread t, const char *str, const char *sp, const char *str_end,
uint8_t *visited, re_match_result *result)
{
for (;;) {
if (t.pc >= pat->code_len) return;
if (visited[t.pc]) return;
visited[t.pc] = 1;
re_inst inst = pat->code[t.pc];
switch (inst.op) {
case RE_JMP:
t.pc = inst.offset;
continue;
case RE_SPLIT:
/* greedy: try pc+1 first, then jump target */
{
re_thread t2 = t;
t2.pc = inst.offset;
add_thread(pat, list, t2, str, sp, str_end, visited, result);
}
t.pc++;
continue;
case RE_SPLITNG:
/* non-greedy: try jump target first, then pc+1 */
{
re_thread t2 = t;
t2.pc = t.pc + 1;
add_thread(pat, list, t2, str, sp, str_end, visited, result);
}
t.pc = inst.offset;
continue;
case RE_SAVE:
t.captures[inst.offset] = (int)(sp - str);
t.pc++;
continue;
case RE_BOL:
if (sp == str || ((pat->flags & RE_FLAG_MULTILINE) && sp > str && sp[-1] == '\n')) {
t.pc++; continue;
}
return;
case RE_EOL:
if (sp == str_end || ((pat->flags & RE_FLAG_MULTILINE) && *sp == '\n')) {
t.pc++; continue;
}
return;
case RE_BOT:
if (sp == str) { t.pc++; continue; }
return;
case RE_EOT:
if (sp == str_end) { t.pc++; continue; }
return;
case RE_EOTNL:
if (sp == str_end || (sp + 1 == str_end && *sp == '\n')) { t.pc++; continue; }
return;
case RE_WBOUND:
{
mrb_bool before = (sp > str) && re_is_word_char((uint8_t)sp[-1]);
mrb_bool after = (sp < str_end) && re_is_word_char((uint8_t)*sp);
if (before != after) { t.pc++; continue; }
}
return;
case RE_NWBOUND:
{
mrb_bool before = (sp > str) && re_is_word_char((uint8_t)sp[-1]);
mrb_bool after = (sp < str_end) && re_is_word_char((uint8_t)*sp);
if (before == after) { t.pc++; continue; }
}
return;
case RE_MATCH:
/* match found during epsilon traversal.
always update: greedy quantifiers may find longer matches later
at the same starting position. thread priority ensures correctness. */
if (result) {
result->matched = TRUE;
memcpy(result->captures, t.captures, sizeof(t.captures));
}
return; /* don't add to thread list */
default:
/* consuming instruction: add to thread list */
break;
}
break;
}
/* add to thread list */
if (list->count < list->capa) {
list->threads[list->count++] = t;
}
}
/* Check if character matches a character class */
static mrb_bool
class_match(const re_charclass *cc, uint8_t ch)
{
if (ch >= 128) return cc->utf8_any;
return (cc->bitmap[ch >> 3] >> (ch & 7)) & 1;
}
/* Pike VM: NFA simulation with submatch tracking */
int
re_exec(mrb_state *mrb, const mrb_regexp_pattern *pat,
const char *str, mrb_int len, mrb_int start,
int *captures, int captures_size)
{
const char *sp = str + start;
const char *str_end = str + len;
int ncap = pat->num_captures * 2;
int list_capa = (int)pat->code_len * 2 + 16;
re_threadlist curr, next;
threadlist_init(mrb, &curr, list_capa);
threadlist_init(mrb, &next, list_capa);
uint8_t *visited = (uint8_t*)mrb_calloc(mrb, 1, pat->code_len + 1);
re_match_result result;
result.matched = FALSE;
memset(result.captures, -1, sizeof(result.captures));
for (; sp <= str_end; sp++) {
/* Add a new initial thread at current position (unanchored search) */
if (!result.matched) {
re_thread t0;
memset(t0.captures, -1, sizeof(t0.captures));
t0.pc = 0;
memset(visited, 0, pat->code_len + 1);
add_thread(pat, &curr, t0, str, sp, str_end, visited, &result);
/* if match found during epsilon traversal (empty pattern), done */
if (result.matched && curr.count == 0) break;
}
if (sp >= str_end) break;
/* Process all current threads against current character */
memset(visited, 0, pat->code_len + 1);
next.count = 0;
int ch = (uint8_t)*sp;
int advance = re_utf8_charlen(sp, str_end);
for (int i = 0; i < curr.count; i++) {
re_thread *th = &curr.threads[i];
if (th->pc >= pat->code_len) continue;
re_inst inst = pat->code[th->pc];
switch (inst.op) {
case RE_CHAR:
if (ch == inst.a) {
th->pc++;
add_thread(pat, &next, *th, str, sp + 1, str_end, visited, &result);
}
break;
case RE_ANY:
if (ch != '\n') {
th->pc++;
add_thread(pat, &next, *th, str, sp + advance, str_end, visited, &result);
}
break;
case RE_ANY_NL:
th->pc++;
add_thread(pat, &next, *th, str, sp + advance, str_end, visited, &result);
break;
case RE_CLASS:
if (class_match(&pat->classes[inst.a], (uint8_t)ch)) {
th->pc++;
add_thread(pat, &next, *th, str, sp + advance, str_end, visited, &result);
}
break;
case RE_NCLASS:
if (!class_match(&pat->classes[inst.a], (uint8_t)ch)) {
th->pc++;
add_thread(pat, &next, *th, str, sp + advance, str_end, visited, &result);
}
break;
case RE_BACKREF:
/* TODO: backtracking for backreferences */
break;
default:
break;
}
}
/* swap curr and next */
{
re_threadlist tmp = curr;
curr = next;
next = tmp;
}
/* if matched and no more threads, we're done */
if (result.matched && curr.count == 0) break;
}
threadlist_free(mrb, &curr);
threadlist_free(mrb, &next);
mrb_free(mrb, visited);
if (result.matched && captures) {
int copy = ncap < captures_size ? ncap : captures_size;
memcpy(captures, result.captures, sizeof(int) * copy);
}
return result.matched ? (ncap > 0 ? ncap : 1) : 0;
}
+76
View File
@@ -0,0 +1,76 @@
/*
** re_utf8.c - UTF-8 utility functions for regexp engine
**
** See Copyright Notice in mruby.h
*/
#include "re_internal.h"
/* Return byte length of UTF-8 character at s.
Returns 1 for invalid sequences (treat as single byte). */
int
re_utf8_charlen(const char *s, const char *end)
{
uint8_t c = (uint8_t)*s;
int len;
if (c < 0x80) return 1;
else if (c < 0xc0) return 1; /* invalid continuation */
else if (c < 0xe0) len = 2;
else if (c < 0xf0) len = 3;
else if (c < 0xf8) len = 4;
else return 1; /* invalid */
if (s + len > end) return 1; /* truncated */
return len;
}
/* Decode a UTF-8 character and return its codepoint.
*len is set to the byte length consumed. */
uint32_t
re_utf8_decode(const char *s, int *len)
{
uint8_t c = (uint8_t)s[0];
uint32_t cp;
if (c < 0x80) {
*len = 1;
return c;
}
else if (c < 0xc0) {
*len = 1;
return c; /* invalid, return as-is */
}
else if (c < 0xe0) {
*len = 2;
cp = (c & 0x1f) << 6;
cp |= ((uint8_t)s[1] & 0x3f);
return cp;
}
else if (c < 0xf0) {
*len = 3;
cp = (c & 0x0f) << 12;
cp |= ((uint8_t)s[1] & 0x3f) << 6;
cp |= ((uint8_t)s[2] & 0x3f);
return cp;
}
else {
*len = 4;
cp = (c & 0x07) << 18;
cp |= ((uint8_t)s[1] & 0x3f) << 12;
cp |= ((uint8_t)s[2] & 0x3f) << 6;
cp |= ((uint8_t)s[3] & 0x3f);
return cp;
}
}
/* Check if character is a "word" character (\w): [a-zA-Z0-9_] */
mrb_bool
re_is_word_char(uint32_t c)
{
if (c >= 'a' && c <= 'z') return TRUE;
if (c >= 'A' && c <= 'Z') return TRUE;
if (c >= '0' && c <= '9') return TRUE;
if (c == '_') return TRUE;
return FALSE;
}
+427
View File
@@ -0,0 +1,427 @@
/*
** regexp.c - Regexp class and MatchData class
**
** See Copyright Notice in mruby.h
*/
#include <mruby.h>
#include <mruby/class.h>
#include <mruby/data.h>
#include <mruby/string.h>
#include <mruby/array.h>
#include <mruby/variable.h>
#include <mruby/error.h>
#include "re_internal.h"
#include <string.h>
/* Regexp data type */
static void regexp_free(mrb_state *mrb, void *ptr) {
re_free(mrb, (mrb_regexp_pattern*)ptr);
}
static const struct mrb_data_type regexp_type = { "Regexp", regexp_free };
/* MatchData */
typedef struct {
mrb_value source; /* source string */
int *captures; /* capture positions [start0,end0,start1,end1,...] */
int num_captures; /* number of capture groups (including 0) */
} mrb_match_data;
static void matchdata_free(mrb_state *mrb, void *ptr) {
mrb_match_data *md = (mrb_match_data*)ptr;
if (md) {
mrb_free(mrb, md->captures);
mrb_free(mrb, md);
}
}
static const struct mrb_data_type matchdata_type = { "MatchData", matchdata_free };
/* Parse flags from string or integer */
static uint32_t
parse_flags(mrb_state *mrb, mrb_value flags_val)
{
uint32_t flags = 0;
if (mrb_integer_p(flags_val)) {
mrb_int f = mrb_integer(flags_val);
if (f & 1) flags |= RE_FLAG_IGNORECASE;
if (f & 4) flags |= RE_FLAG_MULTILINE | RE_FLAG_DOTALL;
return flags;
}
if (mrb_string_p(flags_val)) {
const char *s = RSTRING_PTR(flags_val);
mrb_int len = RSTRING_LEN(flags_val);
for (mrb_int i = 0; i < len; i++) {
switch (s[i]) {
case 'i': flags |= RE_FLAG_IGNORECASE; break;
case 'm': flags |= RE_FLAG_MULTILINE | RE_FLAG_DOTALL; break;
case 'x': break; /* TODO: extended mode */
}
}
return flags;
}
if (mrb_test(flags_val)) flags |= RE_FLAG_IGNORECASE;
return flags;
}
/*
* Regexp.new(pattern, flags=nil)
* Regexp.compile(pattern, flags=nil)
*/
static mrb_value
regexp_init(mrb_state *mrb, mrb_value self)
{
mrb_value pattern;
mrb_value flags_val = mrb_nil_value();
mrb_regexp_pattern *pat;
mrb_get_args(mrb, "S|o", &pattern, &flags_val);
uint32_t flags = parse_flags(mrb, flags_val);
pat = re_compile(mrb, RSTRING_PTR(pattern), RSTRING_LEN(pattern), flags);
DATA_TYPE(self) = &regexp_type;
DATA_PTR(self) = pat;
/* store source for #source and #inspect */
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));
return self;
}
/* Create MatchData from captures */
static mrb_value
create_matchdata(mrb_state *mrb, 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->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 $~ */
return obj;
}
/*
* Regexp#match(str, pos=0)
*/
static mrb_value
regexp_match(mrb_state *mrb, mrb_value self)
{
mrb_value str;
mrb_int pos = 0;
mrb_regexp_pattern *pat;
mrb_get_args(mrb, "S|i", &str, &pos);
pat = DATA_GET_PTR(mrb, self, &regexp_type, mrb_regexp_pattern);
if (!pat) mrb_raise(mrb, E_ARGUMENT_ERROR, "uninitialized Regexp");
int captures[RE_MAX_CAPTURES * 2];
memset(captures, -1, sizeof(captures));
int ncap = re_exec(mrb, pat, RSTRING_PTR(str), RSTRING_LEN(str), pos,
captures, pat->num_captures * 2);
if (ncap == 0) return mrb_nil_value();
return create_matchdata(mrb, str, captures, pat->num_captures * 2);
}
/*
* Regexp#match?(str, pos=0)
*/
static mrb_value
regexp_match_p(mrb_state *mrb, mrb_value self)
{
mrb_value str;
mrb_int pos = 0;
mrb_regexp_pattern *pat;
mrb_get_args(mrb, "S|i", &str, &pos);
pat = DATA_GET_PTR(mrb, self, &regexp_type, mrb_regexp_pattern);
if (!pat) mrb_raise(mrb, E_ARGUMENT_ERROR, "uninitialized Regexp");
int ncap = re_exec(mrb, pat, RSTRING_PTR(str), RSTRING_LEN(str), pos, NULL, 0);
return mrb_bool_value(ncap > 0);
}
/*
* Regexp#=~(str)
*/
static mrb_value
regexp_match_op(mrb_state *mrb, mrb_value self)
{
mrb_value str;
mrb_regexp_pattern *pat;
mrb_get_args(mrb, "o", &str);
if (mrb_nil_p(str)) return mrb_nil_value();
mrb_ensure_string_type(mrb, str);
pat = DATA_GET_PTR(mrb, self, &regexp_type, mrb_regexp_pattern);
if (!pat) mrb_raise(mrb, E_ARGUMENT_ERROR, "uninitialized Regexp");
int captures[RE_MAX_CAPTURES * 2];
memset(captures, -1, sizeof(captures));
int ncap = re_exec(mrb, pat, RSTRING_PTR(str), RSTRING_LEN(str), 0,
captures, pat->num_captures * 2);
if (ncap == 0) return mrb_nil_value();
return mrb_int_value(mrb, captures[0]);
}
/*
* Regexp#===(str)
*/
static mrb_value
regexp_case_match(mrb_state *mrb, mrb_value self)
{
mrb_value str;
mrb_regexp_pattern *pat;
mrb_get_args(mrb, "o", &str);
if (!mrb_string_p(str)) return mrb_false_value();
pat = DATA_GET_PTR(mrb, self, &regexp_type, mrb_regexp_pattern);
if (!pat) return mrb_false_value();
int ncap = re_exec(mrb, pat, RSTRING_PTR(str), RSTRING_LEN(str), 0, NULL, 0);
return mrb_bool_value(ncap > 0);
}
/*
* Regexp#source
*/
static mrb_value
regexp_source(mrb_state *mrb, mrb_value self)
{
return mrb_iv_get(mrb, self, mrb_intern_lit(mrb, "@source"));
}
/*
* Regexp#inspect
*/
static mrb_value
regexp_inspect(mrb_state *mrb, mrb_value self)
{
mrb_value src = mrb_iv_get(mrb, self, mrb_intern_lit(mrb, "@source"));
mrb_value flags_val = mrb_iv_get(mrb, self, mrb_intern_lit(mrb, "@flags"));
uint32_t flags = mrb_nil_p(flags_val) ? 0 : (uint32_t)mrb_integer(flags_val);
mrb_value result = mrb_str_new_lit(mrb, "/");
mrb_str_cat_str(mrb, result, src);
mrb_str_cat_lit(mrb, result, "/");
if (flags & RE_FLAG_IGNORECASE) mrb_str_cat_lit(mrb, result, "i");
if (flags & RE_FLAG_MULTILINE) mrb_str_cat_lit(mrb, result, "m");
return result;
}
/*
* Regexp.escape(str)
*/
static mrb_value
regexp_escape(mrb_state *mrb, mrb_value self)
{
mrb_value str;
mrb_get_args(mrb, "S", &str);
const char *s = RSTRING_PTR(str);
mrb_int len = RSTRING_LEN(str);
mrb_value result = mrb_str_new_capa(mrb, len + len / 4);
for (mrb_int i = 0; i < len; i++) {
char c = s[i];
switch (c) {
case '\\': case '.': case '*': case '+': case '?': case '|':
case '(': case ')': case '[': case ']': case '{': case '}':
case '^': case '$':
mrb_str_cat_lit(mrb, result, "\\");
/* fall through */
default:
mrb_str_cat(mrb, result, &c, 1);
break;
}
}
return result;
}
/* --- MatchData methods --- */
/*
* MatchData#[](n)
*/
static mrb_value
matchdata_aref(mrb_state *mrb, mrb_value self)
{
mrb_int idx;
mrb_get_args(mrb, "i", &idx);
mrb_match_data *md = DATA_GET_PTR(mrb, self, &matchdata_type, mrb_match_data);
if (!md) return mrb_nil_value();
if (idx < 0 || idx >= md->num_captures) return mrb_nil_value();
int start = md->captures[idx * 2];
int end = md->captures[idx * 2 + 1];
if (start < 0) return mrb_nil_value();
return mrb_str_substr(mrb, md->source, start, end - start);
}
/*
* MatchData#captures
*/
static mrb_value
matchdata_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_ary_new(mrb);
mrb_value ary = mrb_ary_new_capa(mrb, md->num_captures - 1);
for (int i = 1; i < md->num_captures; i++) {
int start = md->captures[i * 2];
int end = md->captures[i * 2 + 1];
if (start < 0) {
mrb_ary_push(mrb, ary, mrb_nil_value());
}
else {
mrb_ary_push(mrb, ary, mrb_str_substr(mrb, md->source, start, end - start));
}
}
return ary;
}
/*
* MatchData#to_a
*/
static mrb_value
matchdata_to_a(mrb_state *mrb, mrb_value self)
{
mrb_match_data *md = DATA_GET_PTR(mrb, self, &matchdata_type, mrb_match_data);
if (!md) return mrb_ary_new(mrb);
mrb_value ary = mrb_ary_new_capa(mrb, md->num_captures);
for (int i = 0; i < md->num_captures; i++) {
int start = md->captures[i * 2];
int end = md->captures[i * 2 + 1];
if (start < 0) {
mrb_ary_push(mrb, ary, mrb_nil_value());
}
else {
mrb_ary_push(mrb, ary, mrb_str_substr(mrb, md->source, start, end - start));
}
}
return ary;
}
/*
* MatchData#begin(n) / MatchData#end(n)
*/
static mrb_value
matchdata_begin(mrb_state *mrb, mrb_value self)
{
mrb_int idx;
mrb_get_args(mrb, "i", &idx);
mrb_match_data *md = DATA_GET_PTR(mrb, self, &matchdata_type, mrb_match_data);
if (!md || idx < 0 || idx >= md->num_captures) return mrb_nil_value();
int pos = md->captures[idx * 2];
if (pos < 0) return mrb_nil_value();
return mrb_int_value(mrb, pos);
}
static mrb_value
matchdata_end(mrb_state *mrb, mrb_value self)
{
mrb_int idx;
mrb_get_args(mrb, "i", &idx);
mrb_match_data *md = DATA_GET_PTR(mrb, self, &matchdata_type, mrb_match_data);
if (!md || idx < 0 || idx >= md->num_captures) return mrb_nil_value();
int pos = md->captures[idx * 2 + 1];
if (pos < 0) return mrb_nil_value();
return mrb_int_value(mrb, pos);
}
/*
* MatchData#pre_match / #post_match
*/
static mrb_value
matchdata_pre(mrb_state *mrb, mrb_value self)
{
mrb_match_data *md = DATA_GET_PTR(mrb, self, &matchdata_type, mrb_match_data);
if (!md || md->captures[0] < 0) return mrb_nil_value();
return mrb_str_substr(mrb, md->source, 0, md->captures[0]);
}
static mrb_value
matchdata_post(mrb_state *mrb, mrb_value self)
{
mrb_match_data *md = DATA_GET_PTR(mrb, self, &matchdata_type, mrb_match_data);
if (!md || md->captures[1] < 0) return mrb_nil_value();
int pos = md->captures[1];
return mrb_str_substr(mrb, md->source, pos, RSTRING_LEN(md->source) - pos);
}
/*
* MatchData#length / #size
*/
static mrb_value
matchdata_length(mrb_state *mrb, mrb_value self)
{
mrb_match_data *md = DATA_GET_PTR(mrb, self, &matchdata_type, mrb_match_data);
if (!md) return mrb_fixnum_value(0);
return mrb_fixnum_value(md->num_captures);
}
/* --- Gem init --- */
void
mrb_mruby_regexp_gem_init(mrb_state *mrb)
{
struct RClass *re = mrb_define_class(mrb, "Regexp", mrb->object_class);
MRB_SET_INSTANCE_TT(re, MRB_TT_CDATA);
/* Constants */
mrb_define_const(mrb, re, "IGNORECASE", mrb_fixnum_value(1));
mrb_define_const(mrb, re, "EXTENDED", mrb_fixnum_value(2));
mrb_define_const(mrb, re, "MULTILINE", mrb_fixnum_value(4));
/* Class methods */
mrb_define_method(mrb, re, "initialize", regexp_init, MRB_ARGS_ARG(1, 2));
mrb_define_class_method(mrb, re, "compile", regexp_init, MRB_ARGS_ARG(1, 2));
mrb_define_class_method(mrb, re, "escape", regexp_escape, MRB_ARGS_REQ(1));
mrb_define_class_method(mrb, re, "quote", regexp_escape, MRB_ARGS_REQ(1));
/* Instance methods */
mrb_define_method(mrb, re, "match", regexp_match, MRB_ARGS_ARG(1, 1));
mrb_define_method(mrb, re, "match?", regexp_match_p, MRB_ARGS_ARG(1, 1));
mrb_define_method(mrb, re, "=~", regexp_match_op, MRB_ARGS_REQ(1));
mrb_define_method(mrb, re, "===", regexp_case_match, MRB_ARGS_REQ(1));
mrb_define_method(mrb, re, "source", regexp_source, MRB_ARGS_NONE());
mrb_define_method(mrb, re, "inspect", regexp_inspect, MRB_ARGS_NONE());
mrb_define_method(mrb, re, "to_s", regexp_inspect, MRB_ARGS_NONE());
/* MatchData class */
struct RClass *md = mrb_define_class(mrb, "MatchData", mrb->object_class);
MRB_SET_INSTANCE_TT(md, MRB_TT_CDATA);
mrb_define_method(mrb, md, "[]", matchdata_aref, MRB_ARGS_REQ(1));
mrb_define_method(mrb, md, "captures", matchdata_captures, MRB_ARGS_NONE());
mrb_define_method(mrb, md, "to_a", matchdata_to_a, MRB_ARGS_NONE());
mrb_define_method(mrb, md, "length", matchdata_length, MRB_ARGS_NONE());
mrb_define_method(mrb, md, "size", matchdata_length, MRB_ARGS_NONE());
mrb_define_method(mrb, md, "begin", matchdata_begin, MRB_ARGS_REQ(1));
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());
}
void
mrb_mruby_regexp_gem_final(mrb_state *mrb)
{
}
+141
View File
@@ -0,0 +1,141 @@
assert("Regexp.new") do
re = Regexp.new("abc")
assert_kind_of Regexp, re
end
assert("Regexp#match - simple") do
re = Regexp.new("abc")
md = re.match("xabcy")
assert_kind_of MatchData, md
assert_equal "abc", md[0]
end
assert("Regexp#match - no match") do
re = Regexp.new("xyz")
assert_nil re.match("abc")
end
assert("Regexp#match?") do
re = Regexp.new("abc")
assert_true re.match?("xabcy")
assert_false re.match?("xyz")
end
assert("Regexp#=~") do
re = Regexp.new("bc")
assert_equal 1, re =~ "abcd"
assert_nil re =~ "xyz"
end
assert("Regexp#===") do
re = Regexp.new("abc")
assert_true re === "abc"
assert_false re === "xyz"
end
assert("Regexp - character class") do
re = Regexp.new("[a-z]+")
md = re.match("123abc456")
assert_equal "abc", md[0]
end
assert("Regexp - dot") do
re = Regexp.new("a.c")
assert_true re.match?("abc")
assert_true re.match?("axc")
assert_false re.match?("ac")
end
assert("Regexp - alternation") do
re = Regexp.new("cat|dog")
assert_equal "cat", re.match("I have a cat")[0]
assert_equal "dog", re.match("I have a dog")[0]
end
assert("Regexp - quantifiers") do
assert_equal "aaa", Regexp.new("a+").match("aaa")[0]
assert_equal "", Regexp.new("a*").match("bbb")[0]
assert_equal "ab", Regexp.new("ab?").match("ab")[0]
assert_equal "a", Regexp.new("ab?").match("ac")[0]
end
assert("Regexp - captures") do
re = Regexp.new("(\\w+)@(\\w+)")
md = re.match("user@host")
assert_equal "user@host", md[0]
assert_equal "user", md[1]
assert_equal "host", md[2]
end
assert("Regexp - \\d \\w \\s") do
assert_true Regexp.new("\\d+").match?("123")
assert_true Regexp.new("\\w+").match?("abc_123")
assert_true Regexp.new("\\s+").match?(" ")
assert_false Regexp.new("\\d+").match?("abc")
end
assert("Regexp - anchors") do
assert_true Regexp.new("^abc").match?("abc")
assert_false Regexp.new("^abc").match?("xabc")
assert_true Regexp.new("abc$").match?("abc")
assert_false Regexp.new("abc$").match?("abcx")
end
assert("Regexp - case insensitive") do
re = Regexp.new("abc", Regexp::IGNORECASE)
assert_true re.match?("ABC")
assert_true re.match?("Abc")
end
assert("Regexp - repetition {n,m}") do
assert_equal "aaa", Regexp.new("a{3}").match("aaaa")[0]
assert_equal "aa", Regexp.new("a{2,3}").match("aa")[0]
assert_equal "aaa", Regexp.new("a{2,3}").match("aaaa")[0]
end
assert("MatchData#captures") do
re = Regexp.new("(a)(b)(c)")
md = re.match("abc")
assert_equal ["a", "b", "c"], md.captures
end
assert("MatchData#pre_match / #post_match") do
re = Regexp.new("bc")
md = re.match("abcde")
assert_equal "a", md.pre_match
assert_equal "de", md.post_match
end
assert("MatchData#begin / #end") do
re = Regexp.new("bc")
md = re.match("abcde")
assert_equal 1, md.begin(0)
assert_equal 3, md.end(0)
end
assert("Regexp.escape") do
assert_equal "a\\.b\\*c", Regexp.escape("a.b*c")
end
assert("Regexp#inspect") do
re = Regexp.new("abc", Regexp::IGNORECASE)
assert_equal "/abc/i", re.inspect
end
assert("String#match") do
md = "hello world".match(Regexp.new("(\\w+)\\s(\\w+)"))
assert_equal "hello", md[1]
assert_equal "world", md[2]
end
assert("String#sub") do
assert_equal "hXllo", "hello".sub(Regexp.new("e"), "X")
end
assert("String#gsub") do
assert_equal "h-ll-", "hello".gsub(Regexp.new("[eo]"), "-")
end
assert("String#scan") do
assert_equal ["1", "2", "3"], "a1b2c3".scan(Regexp.new("\\d"))
end