594 Commits

Author SHA1 Message Date
Yukihiro "Matz" Matsumoto f5ca906852 mruby-compiler: fix bare nil? in if/unless to use self as receiver
The if/unless nil? optimization called codegen() on the call node's
receiver, but a bare `nil?` is parsed as an FCALL whose receiver is
NULL. codegen(NULL) emits OP_LOADNIL, so the JMPNIL was testing the
literal nil instead of self, making `if nil?` always behave as
`if nil.nil?` (always true) and `unless nil?` always skip its body.
Load self when the receiver is implicit. Fixes #6874.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-30 07:39:45 +09:00
Yukihiro "Matz" Matsumoto cddaec7264 mruby-compiler: bail out of array-literal pattern match opt on splat
The optimization that skips `deconstruct` and the size check when the
case/in value is an array literal trusted node count, ignoring splat.
An element like `*a` expands at runtime, so [*a] was treated as length
1 and matched only patterns of that length.

Fixes #6854.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-23 09:58:30 +09:00
Yukihiro "Matz" Matsumoto b4f6450509 mruby-compiler: do not flip + and - opcode for negative literal
The peephole in gen_addsub() rewrote `q + -n` into OP_SUBI n (and
`q - -n` into OP_ADDI n) by negating n. For numeric receivers this is
equivalent, but for receivers overriding + or - the runtime fallback
dispatches the flipped method, losing the original operator. Restrict
the fold to non-negative immediates; negative falls through to the
normal OP_ADD/OP_SUB path with LOADI of the literal value.

close #2557
ref #2579

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-09 19:47:27 +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 d8de35b635 codegen.c: raise NoMatchingPatternError in case/in without else
case/in without else clause now raises NoMatchingPatternError
when no pattern matches, matching CRuby behavior. Fixes #6741.

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-12 14:28:17 +09:00
Yukihiro "Matz" Matsumoto 62cf0dc17a codegen.c: chunk %w() and %i() literals to reduce register pressure
Apply the same chunking strategy used for regular array literals
to %w() and %i() literal arrays in gen_literal_array(). Fixes #6740.

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-12 12:12:50 +09:00
Yukihiro "Matz" Matsumoto cfc83bb6ac Merge pull request #6734 from mruby/fix/array-chunking 2026-03-10 17:01:28 +09:00
Yukihiro "Matz" Matsumoto f98d6414dc codegen.c: chunk array literals at 64 elements to reduce register pressure
Array literals were being built by loading all elements into registers
before constructing the array, requiring nregs proportional to the array
size (e.g. nregs=99 for 100 elements). This exceeds mruby/c's register
limit. Restore 3.4-era chunking at GEN_LIT_ARY_MAX (64) elements.

fixes mruby/mruby#6731

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-09 16:50:59 +09:00
Yukihiro "Matz" Matsumoto 8956c5abb5 mruby.h: include mruby/presym.h for all source files
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>
2026-03-09 16:50:58 +09:00
Yukihiro "Matz" Matsumoto 9e0eec3974 mruby-compiler: fix MRB_NO_STDIO build in codegen_pattern()
remove snprintf() call that requires <stdio.h>, which is unavailable
with MRB_NO_STDIO; use a static error message consistent with other
codegen_error() calls.

Fixes #6724.

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-25 15:11:27 +09:00
Yukihiro "Matz" Matsumoto b07518e85c parse.y: implement &nil in formal parameters
`&nil` is recently introduced in CRuby to explicitly declare that
a method does not accept a block. When a block is passed,
ArgumentError "no block accepted" is raised. This is analogous to
`**nil` for keyword arguments.

The noblock flag is encoded in bit 23 of OP_ENTER's aspec operand
(24=n1:m5:o5:r1:m5:k5:d1:b1), avoiding the need for a new opcode.

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-19 16:54:17 +09:00
Yukihiro "Matz" Matsumoto dcd77f1efd mruby-compiler: replace MRB_SYM_2() with MRB_SYM() macros
The _2 suffix variants accept an mrb_state* parameter that is
always ignored with presym enabled. Replace all uses in codegen.c,
parse.y, and y.tab.c with the standard macros. The _2 macro
definitions are kept in presym headers for backward compatibility.

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-19 14:14:40 +09:00
Yukihiro "Matz" Matsumoto 4e63489d1a hash.c: add __pat_values() for pattern matching optimization
Add Hash#__pat_values(keys) that returns an array of values if all
keys exist, or false if any key is missing. This replaces per-key
key?() + []() calls (2N hash lookups) with a single method call
(N hash lookups). The compiler generates __pat_values() followed by
array indexing to extract each value for pattern matching.

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-18 15:41:55 +09:00
Yukihiro "Matz" Matsumoto 5f429e151a mruby-compiler: extract gen_pat_keys_ary() helper for hash pattern codegen
Extract the keys-to-array loop (load keys + OP_ARRAY) into
gen_pat_keys_ary() helper. The pattern appeared in both
deconstruct_keys argument and __except argument generation.

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-18 15:41:55 +09:00
Yukihiro "Matz" Matsumoto 8b080a53cd mruby-compiler: extract gen_pat_key() helper for hash pattern codegen
Reduce code duplication by extracting the key-loading pattern
(NODE_SYM check + OP_LOADSYM/codegen) into gen_pat_key() helper.
The pattern appeared 4 times in NODE_PAT_HASH codegen.

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-18 15:41:55 +09:00
Yukihiro "Matz" Matsumoto a60ee2164e mruby-compiler: refactor deconstruct_keys argument in hash pattern
Simplify deconstruct_keys argument logic from 3 branches to 2:
- pass nil when rest pattern is present or no keys (all keys needed)
- pass keys array only for partial match without rest
This avoids building keys array twice when **rest is present.

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-18 15:41:55 +09:00
Yukihiro "Matz" Matsumoto d98d62ee35 hash.c: change __except to take array argument
mrb_get_args(mrb, "*", ...) internally allocates an array when
arguments are on the stack, so passing keys as direct arguments
did not actually avoid allocation. Change __except to take a
single array argument instead, which is simpler and GC-safe.

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-18 15:41:54 +09:00
Yukihiro "Matz" Matsumoto 488aa8630b mruby-compiler: handle __except with CALL_MAXARGS fallback
When a hash pattern has 15 or more keys, pack them into an array
before calling __except via OP_SEND with CALL_MAXARGS, since the
OP_SEND instruction can only encode up to 14 direct arguments.

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-16 18:42:49 +09:00
Yukihiro "Matz" Matsumoto 1b14a3f72a hash.c: add __except method for pattern matching **rest
Add Hash#__except that returns a new hash excluding specified keys,
used by the compiler for **rest capture in hash patterns. Takes keys
as direct arguments to avoid array allocation. The compiler passes
matched key symbols directly on the stack via OP_SEND.

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-16 18:16:27 +09:00
Yukihiro "Matz" Matsumoto 9b66ec82c4 mruby-compiler: fix hash pattern matching for CRuby compatibility
Add key existence check using key?() before value access, so that
missing keys correctly fail to match (e.g. {b: 1} no longer matches
{a: nil} pattern). Implement **nil and empty {} exact match via
hash.size == num_keys check. Fix **rest to properly exclude matched
keys using dup + __delete instead of copying the entire hash.

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-16 17:33:41 +09:00
Yukihiro "Matz" Matsumoto 4617263030 mruby-compiler: fix JMPNOT-to-MATCHERR rewriting in pattern match codegen
The MATCHERR optimization replaced JMPNOT (BS, 4 bytes) with
MATCHERR (B, 2 bytes) and rewound s->pc by 2. When pattern
alternation (e.g. a|B) dispatched a success jump to s->pc before
the optimization, the rewind shifted subsequent instructions and
the jump landed in the middle of the next instruction, causing
out-of-bounds access at runtime.

Replace JMPNOT in-place with MATCHERR+NOP+NOP to keep the same
4-byte size, so s->pc does not change and jump targets stay valid.

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-10 11:12:27 +09:00
Yukihiro "Matz" Matsumoto b287c12e48 mruby-compiler: raise error for pin operator with undefined variable
CRuby raises SyntaxError for `^a` in pattern matching when `a` is
not a local variable. Previously mruby silently generated an
unconditional fail jump, which also led to bytecode corruption
when combined with alternation patterns.

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-10 11:12:27 +09:00
Yukihiro "Matz" Matsumoto eea9e30979 mruby-compiler: fix heap-buffer-overflow in pattern alternation codegen
The JMPNOT-to-JMPIF optimization in NODE_PAT_ALT assumed the fail
chain always ends with OP_JMPNOT (format BS), but NODE_PAT_PIN
generates OP_JMP (format S) when the pinned variable is undefined.
Writing OP_JMPIF at left_fail-2 then corrupts the preceding
instruction's operand, causing out-of-bounds pool access at runtime.

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-10 11:12:27 +09:00
Yukihiro "Matz" Matsumoto 7d08c6246d codegen.c: fix sign-compare warning in gen_binop()
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-27 16:03:08 +09:00
Yukihiro "Matz" Matsumoto 661ad9fb03 codegen.c: fix keyword arguments in super and yield
The first keyword argument was dropped because gen_hash() was
called with callargs->keyword_args->cdr instead of
callargs->keyword_args.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-27 15:59:20 +09:00
Yukihiro "Matz" Matsumoto 9123ef46eb vm: add OP_SEND0 and OP_SSEND0 for zero-argument method calls
These opcodes use BB format instead of BBB, saving 1 byte per call.
In the standard library, this saves ~790 bytes (568 SEND0 + 222 SSEND0).

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-27 14:57:27 +09:00
Yukihiro "Matz" Matsumoto 7f13422f2f vm: add OP_RETTRUE and OP_RETFALSE for returning boolean literals
Add single-byte opcodes for returning true/false directly, completing
the set of literal return opcodes (RETSELF, RETNIL, RETTRUE, RETFALSE).

Codegen applies peephole optimization to fuse LOADTRUE/LOADFALSE + RETURN.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-27 14:57:27 +09:00
Yukihiro "Matz" Matsumoto a1567be5da ops.h: rename OP_LOADT/OP_LOADF to OP_LOADTRUE/OP_LOADFALSE
Rename boolean load opcodes for consistency with LOADNIL/LOADSELF.
Backward compatibility aliases are provided in opcode.h.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-27 14:57:26 +09:00
Yukihiro "Matz" Matsumoto 0b1af858e2 vm: add OP_RETNIL for returning nil directly
Add a new opcode that returns nil without requiring LOADNIL + RETURN.
This avoids loading nil into a register by setting the return value (v)
directly. The implementation uses a separate label (L_RETURN_NIL) to
bypass v = regs[a], preserving self in regs[0] for ensure blocks.

Codegen applies peephole optimization to fuse LOADNIL + RETURN -> RETNIL.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-27 14:57:26 +09:00
Yukihiro "Matz" Matsumoto 52bee49ad2 vm: add OP_BLKCALL for direct block call without method dispatch
Bypass method dispatch when calling blocks via yield. The new OP_BLKCALL
instruction directly invokes the proc without looking up Proc#call,
resulting in 13-17% faster yield performance.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-27 14:57:26 +09:00
Yukihiro "Matz" Matsumoto 48a88ed79b vm: add OP_TDEF/OP_SDEF for fused method definition
TDEF fuses TCLASS+METHOD+DEF for normal method definitions.
SDEF fuses SCLASS+METHOD+DEF for singleton method definitions.
Saves 4 bytes per method definition (8 bytes -> 4 bytes).
Falls back to unfused instructions if irep index exceeds 255.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-27 14:57:25 +09:00
Yukihiro "Matz" Matsumoto 51e8da6614 vm: add OP_GETIDX0 for fast array[0] access
Fuses MOVE+LOADI_0+GETIDX pattern into single instruction.
Saves 4 bytes per arr[0] access (7 bytes -> 3 bytes).

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-27 14:57:25 +09:00
Yukihiro "Matz" Matsumoto 5475ea573a vm: add OP_ADDILV/OP_SUBILV for local variable increment
fuse MOVE+ADDI+MOVE and MOVE+SUBI+MOVE patterns into single instructions.
ADDILV/SUBILV add/subtract an immediate to a local variable in-place.
BBB format: a=local, b=working space for method call, c=immediate.

saves 5 bytes per instance (9->4 bytes), 40 occurrences in stdlib.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-27 14:57:25 +09:00
Yukihiro "Matz" Matsumoto 724a2e2638 vm: add OP_RETSELF instruction for returning self
Fuse LOADSELF + RETURN sequence into single RETSELF instruction.
Saves 2 bytes per occurrence (3 bytes -> 1 byte).

Found 25 occurrences in mrblib, saving 50 bytes total.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-27 14:57:25 +09:00
Yukihiro "Matz" Matsumoto dece8cb343 vm: fuse JMPIF and MATCHERR into conditional MATCHERR
Change OP_MATCHERR from Z format (unconditional) to B format
(conditional on register). This allows fusing JMPIF + MATCHERR
sequence into a single MATCHERR instruction for simple patterns.

Before: JMPIF R2 target (4 bytes) + MATCHERR (1 byte) = 5 bytes
After:  MATCHERR R2 (2 bytes)

Saves 3 bytes per pattern match with raise_on_fail.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-27 14:57:24 +09:00
Yukihiro "Matz" Matsumoto 2fa99a73c2 vm: add OP_MATCHERR instruction for pattern matching errors
Replace 4-instruction sequence (GETCONST + STRING + SEND + RAISEIF)
with single OP_MATCHERR instruction that raises NoMatchingPatternError
with "pattern not matched" message.

Bump RITE binary format version from 0300 to 0400 due to opcode
number shift.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-27 14:57:24 +09:00
Yukihiro "Matz" Matsumoto e50f15c1c6 mruby-compiler: fix bytecode corruption in pattern matching optimization
The JMPNOT-to-JMPIF optimization assumed fail_pos always came from a
4-byte JMPNOT instruction. When a pinned variable is undefined,
NODE_PAT_PIN generates a 3-byte OP_JMP instead, causing fail_pos - 2
to point into the previous instruction and corrupt its operand.

Add a check to verify the instruction at fail_pos - 2 is actually
OP_JMPNOT before modifying it.

Fixes #6701

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-13 16:12:17 +09:00
Yukihiro "Matz" Matsumoto c46f9e0793 mruby-compiler: fix uninitialized memory in realloc_pool_str()
when converting a shared/static string (IREP_TT_SSTR) to heap-allocated
(IREP_TT_STR), copy the original content to the new buffer.

previously, the original content was lost when allocating new memory,
leaving the first bytes uninitialized. this caused find_pool_str() to
read uninitialized memory via memcmp() when searching for duplicate
strings.

reported by OSS-Fuzz.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-08 12:58:22 +09:00
Yukihiro "Matz" Matsumoto 2e4a8e8edd mruby-compiler: fix sp tracking in pattern match failure path
After pattern matching code generation, the sp (stack pointer) must
be restored to match the success path value. The failure path (after
RAISEIF) left sp in a different state, causing incorrect register
allocation in subsequent code like string interpolation.

This caused OP_STRCAT to use the wrong register, leading to
null-dereference when trying to modify a non-string value as a string.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-04 09:12:35 +09:00
Yukihiro "Matz" Matsumoto af3f9b65f1 mruby-compiler: fix sp imbalance in pattern matching with rescue
The => pattern matching codegen was doing push() after RAISEIF, even though
RAISEIF never returns. This caused sp to be off by 1 when success and failure
paths joined, resulting in wrong register allocation for subsequent operations.

For string interpolation like "#{ expr => pattern rescue body }", the base
string would be at R2 but STRCAT would incorrectly use R3, causing memory
corruption and crashes.

Test case: %{#{.=>.,. rescue def .()end}} (from oss-fuzz)

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-03 20:21:44 +09:00
Yukihiro "Matz" Matsumoto 9f0950da13 codegen.c: fix stack tracking in pattern match branching code
When generating code for pattern matching with potential failures, the
success and failure paths both need to pop the matched value. At
runtime, only one path executes. But during codegen, both pop() calls
affected the compile-time stack pointer (cursp), corrupting register
allocation and causing heap-buffer-overflow when accessing symbol
tables with wrong indices.

Fix by saving/restoring the stack pointer around the branch point, so
each path correctly tracks the stack state independently.

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-29 22:28:45 +09:00
Yukihiro "Matz" Matsumoto da2d652ec9 codegen.c: fix NODE_MATCH_PAT to push result when val is true
Pattern matching expressions were not pushing a result value in several
code paths when used in value context (e.g., string interpolation).
This caused crashes when the result was expected on the stack.

Fix all code paths in NODE_MATCH_PAT to push the appropriate value:
- 'in' pattern returns true/false
- '=>' pattern returns nil (matches CRuby behavior)

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-29 19:09:06 +09:00
Yukihiro "Matz" Matsumoto f6f8124406 codegen.c: fix crash in parallel assignment optimization
The direct literal generation optimization for parallel assignment was
using the RHS count as the loop bound but only filling registers for
LHS variables. When RHS has more elements than LHS (e.g., `a,=1,2`),
this caused uninitialized register indices to be used, generating
garbage opcodes that crashed the VM.

Fix by counting LHS variables and only applying the optimization when
LHS and RHS counts match exactly.

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-29 18:19:58 +09:00
Yukihiro "Matz" Matsumoto d1aa722a98 mruby-compiler: fix JMPNOT+JMP optimization for nested alternatives
The previous optimization for converting JMPNOT+JMP to JMPIF in
alternative patterns had two bugs:

1. It triggered incorrectly for nested alternatives like `1 | 2 | 3`
   (parsed as `((1|2)|3)`), causing memory corruption.

2. The chain end detection was wrong - it checked `prev_offset == 0`
   but the chain actually ends when `(pos+2) + offset == 0`.

Fix by:
- Only applying optimization when left pattern is not NODE_PAT_ALT
- Correctly detecting chain end by checking if next_addr == 0
- Properly unlinking the last JMPNOT from the fail chain

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-25 18:01:43 +09:00
Yukihiro "Matz" Matsumoto bbd5dd690b mruby-compiler: optimize pattern matching for array literals
When the match target is a known array literal, apply these optimizations:
1. Skip #deconstruct call - array literals are already arrays
2. Skip runtime #size check - verify size at compile time
3. Use GETIDX opcode instead of SEND :[] for element access

For the general (non-array-literal) case, improve efficiency by:
- Using EQ opcode instead of SEND :== for size comparison
- Using GE opcode instead of SEND :>= for minimum length check

This reduces bytecode size by ~27% for patterns like:
  [1,2] in Array|[Integer,Integer]

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-25 18:01:43 +09:00
Yukihiro "Matz" Matsumoto e042557d53 mruby-compiler: fix pattern matching === operand order bug
When matching array/hash element patterns like `[Integer]` against
values, the element register was being overwritten by codegen before
the comparison. This caused `[1] in [Integer]|[String]` to incorrectly
return false because the bytecode was effectively doing `1.===(Integer)`
instead of `Integer.===(1)`.

Fix by preserving the element value with push() before calling
codegen_pattern, so the element stays at cursp()-1 while the pattern
value is generated at cursp().

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-24 08:20:31 +09:00
Yukihiro "Matz" Matsumoto 42cea9d3ee mruby-compiler: optimize JMPNOT+JMP to JMPIF in alternative patterns
In alternative patterns (e.g., `Integer|String`), when the left pattern
has a single JMPNOT immediately before the JMP to success, convert the
JMPNOT to JMPIF and eliminate the JMP instruction.

This saves 3 bytes per optimized alternative pattern.

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-24 07:45:40 +09:00
Yukihiro "Matz" Matsumoto fd01a0af73 mruby-compiler: optimize array literal pattern matching
When pattern matching an array literal against an array pattern with
matching sizes (e.g., `[1,2] => a,b`), skip the runtime calls to
#deconstruct and #size. Instead, directly extract elements using the
VM's GETIDX opcode.

This reduces bytecode from 72 to 28 bytes (61% reduction) and
eliminates 4 method calls per pattern match.

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-24 07:35:55 +09:00
Yukihiro "Matz" Matsumoto 2b72d8a7c1 mruby-compiler: optimize JMPNOT+JMP to JMPIF in pattern matching
For patterns with a single failure check (like `1 => String`), invert
JMPNOT to JMPIF and eliminate the following JMP instruction.

Before: JMPNOT fail; JMP end; fail: error; end: (8 bytes for jumps)
After:  JMPIF end; error; end: (4 bytes for jump)

The optimization only applies when:
1. There's exactly one JMPNOT in the failure chain
2. The JMPNOT is immediately before the JMP (no code between)

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-23 20:12:28 +09:00