Commit Graph

18672 Commits

Author SHA1 Message Date
Yukihiro "Matz" Matsumoto 6edef4e7e6 mruby-regexp: use dynamic captures allocation in exec_match()
Replace fixed RE_MAX_CAPTURES*2 (256 bytes) stack array with
malloc sized to actual pat->num_captures*2. Consistent with
the Pike VM's dynamic ncap-sized pool.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:33 +09:00
Yukihiro "Matz" Matsumoto 1f0809aad3 mruby-regexp: consolidate MatchData#captures and #to_a
Both methods had identical loop bodies, differing only in the
starting group index (1 vs 0). Extracted matchdata_to_ary()
with a from parameter.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:32 +09:00
Yukihiro "Matz" Matsumoto 2ef3a7c21e mruby-regexp: extract exec_match() to consolidate match methods
Regexp#match and Regexp#=~ shared most of their logic (get pattern,
execute, create MatchData, set globals). Extracted into exec_match()
internal function. Regexp#=~ now calls exec_match() and reads the
match position from the returned MatchData.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:32 +09:00
Yukihiro "Matz" Matsumoto 16b73ec67b mruby-regexp: extract get_iflags() helper to reduce duplication
The pattern of reading @flags IV and converting to uint32_t was
repeated in 6 methods. Consolidated into a single helper function.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:32 +09:00
Yukihiro "Matz" Matsumoto 3d8ccee7a8 vm.c: optimize cipush/cipop for common cases
CI_PROC_SET: split NULL/non-NULL proc paths so the compiler can
eliminate the CFUNC/ALIAS checks when proc is a compile-time NULL
(8 of 11 cipush call sites).

cipop: add fast path for the common case where no env and no blk
are set. skips ci_env_set, orphan check, and env_unshare entirely.
most simple method calls (no blocks, no closures) take this path.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:32 +09:00
Yukihiro "Matz" Matsumoto 101f8c69a1 mruby-regexp: implement fixed-length lookbehind assertions
Add (?<=...) positive and (?<!...) negative lookbehind support.
The sub-pattern must have a fixed byte length (no quantifiers or
alternation), computed at compile time and stored in the instruction.
At execution time, the engine backs up by that many bytes and runs
the sub-pattern forward. Maximum lookbehind length is 255 bytes.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:32 +09:00
Yukihiro "Matz" Matsumoto 4ded345ebb mruby-regexp: use array join in gsub to avoid O(n^2) concatenation
Collect replacement parts in an array and join at the end instead
of repeated string += which creates intermediate string objects.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:31 +09:00
Yukihiro "Matz" Matsumoto 9d955ba75f mruby-regexp: skip capture tracking in match-only path
When match? calls re_exec with captures=NULL, the Pike VM now
skips all capture pool operations: no pool_copy, no RE_SAVE
writes, no pool compaction. Only a single dummy pool slot is
allocated. This significantly reduces work for boolean matching.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:31 +09:00
Yukihiro "Matz" Matsumoto a2edda173b mruby-regexp: cache $1-$9 symbol IDs for match globals
Pre-intern the $1-$9 symbols on first use instead of calling
mrb_intern_cstr (which computes strlen + hash) on every match.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:31 +09:00
Yukihiro "Matz" Matsumoto 4579caa7d8 mruby-regexp: optimize Pike VM with pooled captures and generation counter
Major changes to the NFA execution engine:

- Thread captures stored in a flat pool sized to actual ncap
  (e.g. 4 ints for 1 capture group vs 64 fixed), dramatically
  reducing per-thread copy cost
- Generation counter for visited[] eliminates per-step memset
  of the entire bytecode-length array
- Pool compaction between steps reclaims dead thread slots
- Backtracking engine also uses dynamic ncap-sized captures

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:31 +09:00
Yukihiro "Matz" Matsumoto 5a158d32a8 mruby-regexp: support \& \` \' \+ \\ in sub/gsub replacements
Replacement strings now support:
  \& = full match, \` = pre_match, \' = post_match,
  \+ = last successful capture, \\ = literal backslash.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:31 +09:00
Yukihiro "Matz" Matsumoto 26dc5f76ea mruby-regexp: add MatchData#string, #regexp, and #to_s
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:31 +09:00
Yukihiro "Matz" Matsumoto 36a0f83db3 mruby-regexp: accept Regexp argument in Regexp.new
Regexp.new(regexp) copies the source and flags from the given
Regexp object, matching CRuby behavior.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:30 +09:00
Yukihiro "Matz" Matsumoto 893cb4edc4 mruby-regexp: fix Regexp#options to return Ruby constant values
Internal flags (MULTILINE=2, DOTALL=4, EXTENDED=8) differ from
Ruby constants (EXTENDED=2, MULTILINE=4). Convert in C instead
of returning raw internal flags. Also add Regexp#casefold?.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:30 +09:00
Yukihiro "Matz" Matsumoto 3f627c0d7d mruby-regexp: update README for x flag support
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:30 +09:00
Yukihiro "Matz" Matsumoto 78d761addf mruby-regexp: implement extended mode (x flag)
The x flag ignores unescaped whitespace and #comments in patterns,
making complex regexps more readable. Whitespace inside character
classes [...] remains literal. Implemented as a preprocessing step
that strips whitespace/comments before compilation.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:30 +09:00
Yukihiro "Matz" Matsumoto 0ca3192c9f mruby-regexp: implement Regexp#==, Regexp#eql?, and Regexp#hash
Two regexps are equal when they have the same source and flags.
Hash is computed from source string hash mixed with flags.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:30 +09:00
Yukihiro "Matz" Matsumoto 4307461e58 gc.c: add symbol_count and dynamic_symbol_count to GC.stat
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:29 +09:00
Yukihiro "Matz" Matsumoto dab150007f mruby-regexp: implement Regexp#to_s in CRuby-compatible format
Regexp#to_s now returns (?flags:source) format (e.g. "(?i:abc)")
instead of the /source/flags format used by Regexp#inspect.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:29 +09:00
Yukihiro "Matz" Matsumoto 4d81275083 mruby-regexp: add $1-$9 globals and include in stdlib gembox
- $1-$9 global variables set by Regexp#match and Regexp#=~
- $1-$9 cleared to nil on match failure
- add mruby-regexp to stdlib.gembox (auto-included in standard builds)
- remove duplicate gem entry from host-debug.rb

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:29 +09:00
Yukihiro "Matz" Matsumoto 117af56bc2 mruby-regexp: add README.md
document supported syntax, Ruby API, engine architecture,
limitations, configuration, and license.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:29 +09:00
Yukihiro "Matz" Matsumoto 3c68e49178 mruby-regexp: add lookahead assertions (?=...) and (?!...)
implement positive and negative lookahead in the backtracking engine:
- (?=pattern): succeeds if pattern matches at current position
  without consuming characters
- (?!pattern): succeeds if pattern does NOT match at current position

new bytecodes RE_LOOKAHEAD and RE_NEG_LOOKAHEAD implemented in
the backtracking engine via nested bt_match calls.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:29 +09:00
Yukihiro "Matz" Matsumoto 7283560215 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>
2026-04-23 19:25:28 +09:00
Yukihiro "Matz" Matsumoto 8d92379d7c mruby-regexp: fix non-greedy quantifiers (*?, +?, ??)
non-greedy patterns now correctly match the shortest possible
string. patterns with non-greedy quantifiers are dispatched to
the backtracking engine which naturally handles non-greedy
semantics.

the Pike VM continues to be used for purely greedy patterns
(O(n*m) guarantee).

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:28 +09:00
Yukihiro "Matz" Matsumoto 23b2d24cf5 mruby-regexp: add backtracking engine for backreferences
add a recursive backtracking engine that handles \1-\9
backreferences. the Pike VM (NFA) is used for patterns without
backreferences; patterns with backreferences automatically
fall back to the backtracking engine.

the backtracking engine has a step limit (MRB_REGEXP_STEP_LIMIT,
default 1M) to prevent ReDoS on pathological patterns.

also adds SAVE backtracking (save/restore capture positions on
failed branches) for correct submatch tracking.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:28 +09:00
Yukihiro "Matz" Matsumoto 6deafd810f mruby-regexp: add edge case tests and improve coverage
add tests for: empty pattern, nested captures, word boundary \b,
non-capturing groups (?:), sub/gsub with block, scan with captures,
split with regexp, case/when with regexp, date reformatting.

known limitation: non-greedy quantifiers (*?, +?) currently behave
as greedy. needs match priority tracking (TODO for Phase 2).

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:28 +09:00
Yukihiro "Matz" Matsumoto 3bfb27b999 mruby-regexp: add /regex/ literal support, $~, Regexp.compile
- /regex/ literal syntax now works (compiler generates Regexp.compile)
- Regexp#match and Regexp#=~ set $~ global variable
- Regexp.last_match(n) for accessing capture groups
- Regexp.compile as alias for Regexp.new
- Regexp#options

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:27 +09:00
Yukihiro "Matz" Matsumoto 1cfa153ff3 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>
2026-04-23 19:25:27 +09:00
Yukihiro "Matz" Matsumoto 30e41242ec doc/internal/gc.md: add practical tuning examples
add workload-specific GC tuning advice based on benchmark data:
- allocation-heavy: interval_ratio 400 for ~12% improvement
- real-time: step_limit for bounded pause times
- large buffers: malloc_threshold
- diagnosing GC overhead with GC.stat

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:27 +09:00
Yukihiro "Matz" Matsumoto 49dff412b5 gc.c: extract mrb_obj_alloc_core() for internal allocation
split mrb_obj_alloc() into type-validation wrapper and allocation
core (mrb_obj_alloc_core). internal callers (mrb_proc_new,
mrb_env_new) use the core directly, skipping 15+ lines of type
validation per allocation.

most impactful for workloads with heavy Proc/Env allocation
(lambda calculus, block-intensive code).

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:27 +09:00
Yukihiro "Matz" Matsumoto f7d7bbef43 array.c: add string-specialized fast path for Array#sort!
when all elements are plain String (not subclass) and no block is
given, use specialized sort that calls mrb_str_cmp() directly,
bypassing sort_cmp overhead (GC arena, type dispatch, array
modification check).

includes subclass check to ensure String#<=> is not overridden.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:27 +09:00
Yukihiro "Matz" Matsumoto 5364c4167e array.c: add integer-specialized fast path for Array#sort!
when all elements are integers and no block is given, use
specialized heapify/insertion_sort that compare mrb_int values
directly, bypassing sort_cmp entirely. this eliminates per-comparison
overhead of GC arena save/restore, type checking, and array
modification checks.

the pre-scan to detect all-integer arrays is O(n), negligible
compared to O(n log n) sort. non-integer and block sorts are
unaffected.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:26 +09:00
Yukihiro "Matz" Matsumoto 02bb943960 array.c: optimize heap sort with hole-style sift-down and Floyd's method
two improvements to Array#sort!'s heap sort:

1. hole-style sift-down: save root value, move larger children up
   one at a time, write saved value once at the end. reduces
   assignments from 3 per level (swap) to 1 per level (move).

2. Floyd's bottom-up heap deletion: during extraction phase, sift
   the hole down to a leaf using only child-child comparisons
   (~1 comparison per level), then sift up to find the correct
   position. this reduces average comparisons from ~2 log n to
   ~log n per extraction, nearly halving the total comparison
   count for the sort.

both changes preserve O(n log n) worst case and O(1) extra space.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:26 +09:00
Yukihiro "Matz" Matsumoto e292d7a6c4 symbol.c: implement lazy symbol GC (mark-sweep)
when dynamic symbol count reaches MRB_SYMBOL_MAX, run a mark-sweep
pass over all live objects to identify referenced symbols. sweep
unreferenced dynamic symbols, freeing their individually-allocated
string data and marking symtbl slots as tombstones.

mark phase traverses:
- all heap objects (method tables, IV tables, arrays, hashes, envs)
- VM stack values (MRB_TT_SYMBOL)
- call stack method IDs (ci->mid)
- root and current context

after sweep, rebuild hash table to maintain valid collision chains.

this completes the A+ symbol GC plan: the limit acts as a GC
trigger rather than a hard cap. unreferenced DoS symbols are
reclaimed, allowing legitimate code to continue.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:26 +09:00
Yukihiro "Matz" Matsumoto cb64a0b4a4 symbol.c: use individual malloc for dynamic symbol strings
dynamic symbols (created via to_sym, send, etc.) now use
mrb_malloc() instead of sym_pool_alloc(). this makes them
individually freeable by future symbol GC.

static symbols (presym, mrb_intern_static, literals) continue
to use the pool allocator for compact storage.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:26 +09:00
Yukihiro "Matz" Matsumoto afc0753c1d symbol.c: add dynamic symbol limit (MRB_SYMBOL_MAX)
track dynamic (runtime-created) symbols separately from presyms,
inline symbols, and static C API symbols. raise RuntimeError when
the dynamic symbol count exceeds MRB_SYMBOL_MAX (default 4096).

this prevents DoS attacks via unbounded symbol creation (e.g.
"str".to_sym in a loop). presyms and inline symbols are not
counted toward the limit.

infrastructure for future symbol GC: sym_flags array tracks
per-symbol metadata (SYM_FL_DYNAMIC flag).

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:26 +09:00
Yukihiro "Matz" Matsumoto 3f8e13da6f codegen.c: consolidate while/until loop codegen
merge codegen_while/codegen_until into codegen_loop, and
codegen_while_mod/codegen_until_mod into codegen_loop_mod.
each pair differed only in swapped constant-condition checks
(true_always/false_always) and jump opcode (OP_JMPNOT/OP_JMPIF).

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:26 +09:00
Yukihiro "Matz" Matsumoto 65e24f4083 codegen.c: consolidate codegen_dot2/codegen_dot3 into codegen_range
the two functions differed only in OP_RANGE_INC vs OP_RANGE_EXC.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:25 +09:00
Yukihiro "Matz" Matsumoto 0b79c70935 dump.c: consolidate error handling in mrb_dump_irep_cfunc()
replace four repeated `mrb_free(mrb, bin); return MRB_DUMP_WRITE_FAULT`
sequences with a single goto-based cleanup path.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:25 +09:00
Yukihiro "Matz" Matsumoto 16fbd56e4b mruby-task: extract task_create_common() from Task.new and mrb_create_task()
both functions shared identical task allocation, context
initialization, queue insertion, and priority preemption logic.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:25 +09:00
Yukihiro "Matz" Matsumoto 0f47249963 class.c: clear const cache on include/prepend
mrb_include_module() and mrb_prepend_module() did not invalidate
the constant cache. stale cache entries caused incorrect constant
resolution after include changed the ancestor chain.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:25 +09:00
Yukihiro "Matz" Matsumoto 3b85d48f89 class.c: fix module/class reopening via include
mrb_vm_define_module() and mrb_vm_define_class() incorrectly
reopened modules/classes accessible through include rather than
creating new ones. CRuby only reopens modules directly defined
on the outer scope.

the internal define_module()/define_class() use
mrb_const_defined_at() which walks ancestors for Object class.
bypass them and create modules/classes directly in the VM path.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:25 +09:00
Yukihiro "Matz" Matsumoto f3cd991771 mruby-enum-lazy: add Enumerator::Lazy#tap_each
add tap_each method that yields each element for side effects
(e.g. logging, debugging) and passes it through unmodified.
see https://bugs.ruby-lang.org/issues/21520

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:25 +09:00
Yukihiro "Matz" Matsumoto d2caa144be cdump.c: consolidate sym_name_with_*_p into sym_name_with_suffix_p
three functions differed only in the trailing character check ('=',
'?', '!'). replace with a single parameterized function.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:24 +09:00
Yukihiro "Matz" Matsumoto f1a6274c34 vm.c: extract vm_call_proc() to consolidate OP_CALL and OP_BLKCALL
both opcodes share identical proc dispatch logic (alias resolution,
callinfo setup, cfunc/irep branching). the only difference is how
nargs is computed (ci_bidx vs operand b).

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:24 +09:00
Yukihiro "Matz" Matsumoto 6a6e2b48ac vm.c: replace mrb_funcall_argv() with goto L_SEND_SYM in OP_MATHILV
avoid re-entrant VM call from C; use the same dispatch pattern as
OP_MATH and OP_MATHI for consistency.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:24 +09:00
Yukihiro "Matz" Matsumoto 82bb954c23 vm.c: extract vm_define_method() to consolidate OP_TDEF and OP_SDEF
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:24 +09:00
Yukihiro "Matz" Matsumoto 309f450bab gc.c: use actual work done for debt repayment in incremental step
Decrement gc_debt by the actual number of objects processed
instead of the fixed GC_STEP_SIZE. This makes step_ratio
directly affect debt repayment: larger steps repay more debt,
naturally reducing GC invocation frequency.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:24 +09:00
Yukihiro "Matz" Matsumoto 851da984b4 gc.c: use :debt instead of :threshold in GC.stat
Expose gc_debt directly as :debt in GC.stat without sign negation.
The debt model has no threshold ceiling, so :threshold was a
misleading name. Negative debt means credit, positive means GC
is behind on collection work.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:24 +09:00
Yukihiro "Matz" Matsumoto 4d03f40204 doc/internal/gc.md: update for debt model and new tuning parameters
Document the debt-based GC trigger model, malloc threshold,
step limit, GC.stat, and tuning guide.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:23 +09:00