Replace the inline switch with a separate fast_fmt_ok() function that
maps each format character to a validity code (0=invalid, 1=arg spec,
2=separator). Modern compilers generally lower this to a jump table,
so the per-character cost remains effectively O(1).
Keeping this as a switch (instead of a C99 array-index designator
lookup table) also lets the file compile cleanly as C++.
Co-authored-by: Claude <noreply@anthropic.com>
Skip the two-pass format scanning when the format string contains
only simple specifiers (o, S, i, n, z, b, f, A, H, c, s, a) with
optional '|' separator. Validates all specifiers before consuming
va_list to ensure safe fallback to the slow path.
Covers ~50% of all mrb_get_args call sites in the codebase (175
of 353). Reduces per-call argument parsing overhead by ~30-40%.
Co-authored-by: Claude <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
three functions differed only in the trailing character check ('=',
'?', '!'). replace with a single parameterized function.
Co-authored-by: Claude <noreply@anthropic.com>
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>
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>
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>
Use -gc_debt as the :threshold key in GC.stat for familiarity.
Positive means credit remaining, negative means GC is overdue.
Co-authored-by: Claude <noreply@anthropic.com>
Replace the threshold-based GC trigger (gc->threshold vs gc->live)
with a debt model (gc->gc_debt). Each allocation increments debt;
each GC step decrements by GC_STEP_SIZE. When a cycle completes,
credit is proportional to live_after_mark * interval_ratio, giving
a natural feedback loop that adapts to allocation rate.
Co-authored-by: Claude <noreply@anthropic.com>
GC.step_limit caps the per-step work in incremental GC,
enabling more predictable pause times for real-time use.
GC.malloc_threshold triggers GC based on allocation bytes,
addressing memory pressure from large buffers.
Both default to 0 (disabled), preserving existing behavior.
Co-authored-by: Claude <noreply@anthropic.com>
Leaf types (String, Integer, BigInt, Complex, CPTR) have no children
besides their class pointer. Mark them black immediately in
mrb_gc_mark() instead of pushing to the gray stack, reducing gray
stack pressure and overflow frequency.
Co-authored-by: Claude <noreply@anthropic.com>
Return a Hash with GC statistics: live, threshold, state,
generational, full. When MRB_GC_STATS is defined, also includes
total, minor, major counters.
Co-authored-by: Claude <noreply@anthropic.com>
Add gc_total_count, minor_gc_count, major_gc_count (uint32_t) to
mrb_gc, guarded by MRB_GC_STATS. Zero cost when disabled.
Co-authored-by: Claude <noreply@anthropic.com>
Phase 2 of opcode handler extraction. these opcodes use
L_SEND_SYM/L_SENDB_SYM fallback for generic method dispatch when
the fast path (Array/Hash/String/Integer/Float) does not apply.
add VM_SEND_SYM and VM_SENDB_SYM return codes. move TYPES2 macro
to file scope for use by vm_op_div.
Co-authored-by: Claude <noreply@anthropic.com>
extract the three largest self-contained opcode handlers from the
mrb_vm_exec() dispatch loop into static functions. add
__attribute__((flatten)) to mrb_vm_exec() so that the compiler
inlines them back, producing identical binary output while keeping
the source clean.
Co-authored-by: Claude <noreply@anthropic.com>
Use uscale-based shortest() to compute the minimal decimal string
that uniquely identifies each double. This guarantees perfect
round-trip (parse(to_s(x)) == x) while keeping output concise
(e.g. 0.1 prints as "0.1", not "0.10000000000000001").
Co-authored-by: Claude <noreply@anthropic.com>
Replace separate float formatting (fmt_fp.c) and parsing (readfloat.c)
implementations with a unified fp_uscale.c using 128-bit unrounded
scaling. Both mrb_format_float() and mrb_read_float() now share a
single pow10 table and uscale() primitive for decimal/binary conversion.
This fixes subnormal parsing accuracy (old code returned 0.0 for the
smallest subnormals) and corrects %.2f rounding for values like
12345.125. Table size grows from ~5KB to ~11KB in .rodata.
Co-authored-by: Claude <noreply@anthropic.com>
Remove the per-entry generation field and per-state generation
counter. Invalidation now clears entries directly, removing one
comparison from every OP_GETCONST hot path.
Co-authored-by: Claude <noreply@anthropic.com>
Right-shift class pointer by 4 before hashing to remove
always-zero alignment bits, improving hash distribution.
Organize 256 cache entries as 128 sets x 2 ways to reduce
conflict misses when multiple methods share a hash bucket.
Co-authored-by: Claude <noreply@anthropic.com>
Cache OP_GETCONST results in a global direct-mapped cache (64 entries)
keyed by (irep, sym). Invalidate all entries via a generation counter
bumped on mrb_const_set(), mrb_const_remove(), and
mrb_define_const_id(). ~10% faster on constant-heavy code; disabled
with MRB_NO_CONST_CACHE.
Co-authored-by: Claude <noreply@anthropic.com>
The previous code computed `frac_part * pow10_negative[n]`, where
pow10_negative[n] is already a rounded approximation of 10^-n (since
10^-n is not exactly representable in binary). The multiplication then
adds another rounding step, leaving up to ~1 ulp of error.
Dividing `frac_part` by `pow10_positive[n]` is exact for n <= 22 (the
range where 10^n fits exactly in a double), so the division is the
only rounding and the result is correctly rounded. For example,
"0.3".to_f now matches the 0.3 literal's bit pattern (and libc strtod).
`mrb_class_get_id()` may call the `#const_missing` method.
Therefore, if the `mesg` string originates from a string object, it may reference an invalid address.
And since `errno` might also change during the call to `#const_missing`, save this as well beforehand.
Also, while `mrb_class_defined_id()` does not currently call the `#const_defined?` method, it is unclear whether this will remain the case in the future.
Compress the 24-bit aspec into 13 free flag bits on RProc (bits 0-6
and 14-19) when wrapping cfunc methods. Field widths: req/opt 3 bits
(max 7), post/key 2 bits (max 3), rest/kdict/block 1 bit each. Values
exceeding the compressed range are clamped and rest is forced to 1.
This enables Proc#arity and Proc#parameters to return correct results
for cfunc-backed Procs (e.g. from Method#to_proc) with zero memory
overhead -- no struct change needed.
Closes#6764
This patch fixes a bug in the stack extension logic that could cause a HardFault on certain configurations when the stack is reallocated to a new address.
## Background
When the mruby VM's stack runs out, stack_extend_alloc() calls mrb_realloc to grow it.
If reallocation moves the block to a new address, envadjust() adjusts all ci->stack pointers to point into the new allocation.
## The bug
The bug happened under the configuration below:
- MRB_INT64 on MRB_32BIT (`sizeof(mrb_value) == 16` because MRB_NO_BOXING is now mandatory)
- Allocator with 8-byte alignment (eg. PICORB_ALLOC_ALIGN=8 in PicoRuby for Raspi Pico)
The delta was computed via mrb_value* pointer subtraction:
```c
ptrdiff_t delta = newbase - oldbase; // units of sizeof(mrb_value)
```
If :
- Old address: 0x2004c508
- New address: 0x2004c510 (8-byte difference)
The pointer subtraction truncated: 8 / 16 = 0.
envadjust() was misleaded as `delta == 0` and returned early without adjusting any ci->stack pointers.
The stbase was updated to the new address, but all stack pointers still pointed 8 bytes before it.
Every register access was shifted, reading garbage, ultimately causing a HardFault.
## The fix
Byte-level char* calculation instead of mrb_value* calculation:
```c
ptrdiff_t off = (char*)newbase - (char*)oldbase;
// ...
ci->stack = (mrb_value*)((char*)ci->stack + off);
```
This ensures the adjustment is exact regardless of sizeof(mrb_value) and allocator alignment.
Previously only the first match was removed, leaking duplicate
entries when the same object was registered multiple times.
Use two-pointer compaction for O(N) removal.
Fixes#6760.
Co-authored-by: Claude <noreply@anthropic.com>
There are two reasons:
- If the mruby call stack is extended, the `ci` variable may become invalid.
- The C language does not specify the order in which the left-hand and right-hand sides of an assignment expression are evaluated.
Therefore, if the mruby data stack is extended, `ci->stack` may become invalid.
`mrb_hash_delete` returns the removed element (which is guaranteed to
exist due to the `mrb_hash_key_p` check), this prevents the hash from
being searched twice.
attr_reader-generated getter methods silently ignored any arguments
passed to them. CRuby raises ArgumentError in this case.
Add mrb_get_args(mrb, "") to enforce zero arguments, matching CRuby.
Since presym is now mandatory, mruby.h includes presym.h so that
MRB_SYM() macros are available everywhere without explicit include.
Remove redundant #include <mruby/presym.h> from all source files.
Co-authored-by: Claude <noreply@anthropic.com>