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>
When defining a method with a required keyword argument without
parentheses, mruby incorrectly parsed the next line as the default
value:
def foo arg:
123
end
Was parsed as: def foo(arg: 123); end (optional kwarg, empty body)
Should be: def foo(arg:); 123; end (required kwarg, body returns 123)
The fix sets EXPR_ARG lexer state after parsing f_label, making
newlines significant. This prevents the parser from consuming
expressions across line boundaries as default values for keyword
arguments.
Also fixes a pre-existing bug in f_label where tNUMPARAM (type <num>)
was implicitly assigned to $$ (type <id>) without conversion. Now
explicitly uses intern_numparam() to convert numbered parameters to
symbols.
Fixes https://github.com/mruby/mruby/issues/6268
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>
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>
When parsing malformed input with many syntax errors (e.g., via eval
with a long garbage string), the parser would continue until the end
of input, causing long execution times.
Add an early termination check in the lexer that returns EOF once
the error count exceeds 10 (same as error_buffer size). This prevents
DoS from inputs like eval("garbage" * 1000).
Co-authored-by: Claude <noreply@anthropic.com>
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>
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>
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>
Add precedence declarations to resolve the ambiguity between:
- One-line pattern match: `arg in pattern`
- Case/in clause: `case expr; in pattern; end`
When seeing `arg in`, the parser should shift to parse `arg in pattern`
as a complete expression (matching CRuby behavior), not reduce `arg`
to start a case clause.
Changes:
- Add keyword_in to %nonassoc precedence declarations
- Add %prec tLOWEST to the plain `arg` reduction rule
This eliminates all bison shift-reduce conflicts (was 2, now 0).
Co-authored-by: Claude <noreply@anthropic.com>
Remove non-standard `symbol tASSOC p_as` rule from hash pattern
elements. This rule conflicted with the as-pattern rule and caused
`:foo => x` to be incorrectly parsed as a hash pattern instead of
an as-pattern.
CRuby only supports label syntax (foo:) for hash pattern keys,
not hashrocket syntax (:foo =>). This change aligns mruby with
CRuby behavior and reduces bison shift-reduce conflicts from 2 to 1.
Before: `case :foo; in :foo => x; end` raised NoMethodError
After: `case :foo; in :foo => x; end` binds x to :foo
Co-authored-by: Claude <noreply@anthropic.com>
The p_value grammar rule passed raw tSTRING token (a (len . str) cons
cell) directly to new_pat_value() without wrapping it as a proper AST
node. When codegen processed this malformed node, it read the length
field as the node type, causing misinterpretation and crash.
Wrap tSTRING with new_str(p, list1($1)) to create a proper NODE_STR,
consistent with how the primary:string rule handles strings.
Found by ClusterFuzz (oss-fuzz/mruby_fuzzer).
Co-authored-by: Claude <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
For `1 => a`, generate the same bytecode as `a = 1` by leveraging
gen_move's peephole optimization. The peephole optimizer rewrites
LOADI+MOVE into a single LOADI to the target register.
Before: LOADI_1 R2; MOVE R1 R2 (6 bytes)
After: LOADI_1 R1 (3 bytes)
Co-authored-by: Claude <noreply@anthropic.com>
Resolve static function name collision between parse.y and codegen.c
for amalgamation support.
- parse.y: rename get_node_type() to node_type() (keeps validation)
- codegen.c: replace with node_type() macro (NULL-safe via NODE_TYPE)
- node.h: rename VAR_NODE_TYPE() to NODE_TYPE()
Co-authored-by: Claude <noreply@anthropic.com>
The function registers a symbol in the IREP symbol table and returns
its index. The new name better reflects this behavior and avoids
collision with parse.y's new_sym (which creates AST nodes).
Co-authored-by: Claude <noreply@anthropic.com>
when running scripts via mruby -e or file, return values are unused.
this adds a no_return_value flag to skip generating unnecessary code.
for parallel assignment like a,b = 1,2:
- before: 18 bytes, 5 registers, creates temporary array
- after: 5 bytes, 3 registers, direct register assignment, no RETURN
the flag is set only for the main program, not for libraries loaded
with -r option. eval() and mirb continue returning values correctly.
Co-authored-by: Claude <noreply@anthropic.com>
When all lhs are local variables and all rhs are simple literals
(integers, nil, true, false), generate values directly into target
registers instead of using temporaries and MOVE instructions.
For example, `a,b = 1,2` now generates:
LOADI_1 R1
LOADI_2 R2
instead of:
LOADI_1 R3
LOADI_2 R4
MOVE R1 R3
MOVE R2 R4
Co-authored-by: Claude <noreply@anthropic.com>
Extract duplicated UTF-8 codepoint-to-bytes encoding into a shared
function in src/string.c. Update all gems to use it:
- mruby-sprintf: %c specifier
- mruby-io: putc
- mruby-string-ext: Integer#chr
- mruby-pack: pack("U")
- mruby-compiler: Unicode escapes in parser
Also use existing mrb_utf8len() in io.c for character length detection.
Co-authored-by: Claude <noreply@anthropic.com>
add support for find patterns in case/in expressions:
- [*pre, elem, *post] - find elem anywhere in array
- [*, elem, *] - anonymous rest (discarded)
- [*pre, a, b, *post] - multiple middle elements
implementation includes:
- grammar rules for find patterns with p_args, p_rest in parse.y
- NODE_PAT_FIND codegen with iterative search loop
- pre/post variable binding via range slicing
- p_const rule to prevent conflict with array literals
Co-authored-by: Claude <noreply@anthropic.com>
add support for one-line pattern matching syntax:
- 'expr in pattern' returns true/false
- 'expr => pattern' raises NoMatchingPatternError on mismatch
add NODE_MATCH_PAT node type for both forms, distinguished by
raise_on_fail flag. grammar rules placed at expr level to avoid
conflict with rescue clause's exception variable syntax.
Co-authored-by: Claude <noreply@anthropic.com>
Add support for brace-less hash patterns at top level of case/in.
`in a: x, b: y` is now equivalent to `in {a: x, b: y}`.
`in a:, b:` shorthand now works with newlines (CRuby compatible).
Changes:
- Add EXPR_VALUE to IS_LABEL_POSSIBLE() to recognize labels after `in`
- Add brace-less hash pattern rules to p_expr
- Change p_hash_elem to use p_as instead of p_expr to avoid recursion
- Add in_kwarg flag to parser state for pattern matching context
- Set in_kwarg in lexer when keyword_in is returned
- Use EXPR_ARG after tLABEL_TAG when in_kwarg is set (makes newlines significant)
Co-authored-by: Claude <noreply@anthropic.com>
Add pin operator `^var` that matches against existing variable values
instead of creating new bindings. Also add bracket-less array pattern
syntax at top level: `in 1, 2, x` is equivalent to `in [1, 2, x]`.
Co-authored-by: Claude <noreply@anthropic.com>
Add support for hash patterns in pattern matching expressions:
- {key:} shorthand binds to variable with same name
- {key: pattern} matches key against pattern
- {**rest} captures remaining keys
- {**nil} requires exact match (no extra keys)
- {**} ignores extra keys without capturing
Parser adds new grammar rules (p_hash, p_hash_body, p_hash_elems,
p_hash_elem, p_kwrest) and new_pat_hash() constructor.
Codegen generates code to call deconstruct_keys on the target hash,
then iterates through key-pattern pairs to match each key's value.
Adds Hash#deconstruct_keys method that returns self for pattern matching.
Co-authored-by: Claude <noreply@anthropic.com>
Add support for if/unless guards in case/in pattern matching:
case value
in x if x > 0 then :positive
in x unless x == 0 then :non_zero
end
Uses modifier_if/modifier_unless tokens since guards appear after
an expression. Disable peephole optimization for pattern variable
binding to prevent gen_move() from being optimized away when failed
guard jumps target the binding instruction.
Co-authored-by: Claude <noreply@anthropic.com>
Implement Phase 1 of Ruby pattern matching:
- value patterns (literals, constants, nil/true/false)
- variable patterns (binds matched value)
- alternative patterns (pat1 | pat2)
- as patterns (pattern => var)
Pattern matching uses === operator for value comparison,
allowing type checking with class patterns (e.g., in Integer).
Co-authored-by: Claude <noreply@anthropic.com>
restore correct argument passing for Regexp.compile when encoding is
present but flags are not. regexp literals like /a/n should compile to
Regexp.compile("a", nil, "n") with 3 arguments, not
Regexp.compile("a", "n") with 2 arguments.
the bug was introduced during refactoring when the nil-insertion logic
for the options parameter was accidentally omitted. now properly inserts
OP_LOADNIL when flags are absent but encoding is present.
Co-authored-by: Claude <noreply@anthropic.com>
allocate ** keyword dictionary register when methods have keyword
arguments (parse.y new_args_tail), broken in commit 26ea71260 during
cons-list to struct migration. reconstruct keyword hash after KEYEND
from extracted keyword local variables so super can access keyword
values. encode block parameter flag in ainfo bit 13 and generate
LOADNIL for block register in codegen_zsuper when parent has keywords
but no block parameter.
Co-authored-by: Claude <noreply@anthropic.com>
Restore exception handling for `||=` operator on class variables and constants
that was inadvertently removed in commit 0ca48e24f. When reading an undefined
class variable with GETCV opcode raises NameError, the exception handler
catches it and loads false, allowing the assignment to proceed.
Co-authored-by: Claude <noreply@anthropic.com>
pr #6643 fixed node type check but introduced a bug by accessing t->cdr
on a NODE_ARRAY structure. NODE_ARRAY nodes use the elements field, not
cdr. additionally, the fixed rhs path needs to update rhs_reg to point
to where values are actually pushed on the stack.
Co-authored-by: Claude <noreply@anthropic.com>
added stmts_push(p, stmts, stmt) helper function to properly push
statements to NODE_STMTS nodes by accessing the internal stmts field
(a cons list). this avoids ugly casts and prevents bugs.
fixed incorrect usage in:
- top_stmts rule (line 2081): was calling push($1, ...) directly on
NODE_STMTS instead of pushing to $1->stmts
- bodystmt rule (line 2114): same issue when handling else without
rescue
- stmts rule (line 2146): simplified to use new helper for consistency
the push macro works on cons lists, not NODE_STMTS variable nodes.
the new helper encapsulates the cast and provides type-safe access.
Co-authored-by: Claude <noreply@anthropic.com>
segment nodes allocated with mrbc_malloc were leaked if gen_string
raised an exception via longjmp. fix by avoiding allocation entirely:
temporarily modify tree structure by saving and clearing cdr pointer,
call gen_string, then restore cdr. no memory is allocated so nothing
leaks even on longjmp.
Co-authored-by: Claude <noreply@anthropic.com>
The bug was in codegen_colon3 which used genop_2(OP_OCLASS, sym)
treating OCLASS as BB format, but OCLASS is B format that only
loads ::Object without a symbol parameter. The fix uses the correct
two-instruction pattern: OCLASS to load Object class, then GETMCNST
to retrieve the constant from it.
Co-authored-by: Claude <noreply@anthropic.com>
cast uint8_t node_type field to enum node_type to satisfy c++ stricter
type checking while maintaining memory efficiency of 1-byte storage.
Co-authored-by: Claude <noreply@anthropic.com>
replace obsolete cons-style comments like /* (:begin prog...) */ with
modern struct-style comments like /* struct: begin_node(body) */ to
reflect current variable-sized node implementation.
Co-authored-by: Claude <noreply@anthropic.com>
add braces around node_hash case in dump_node() to fix variable
initialization crossing case labels error when compiling with c++.
Co-authored-by: Claude <noreply@anthropic.com>
introduce new_node() helper and NEW_NODE() macro to eliminate repetitive
allocation and header initialization pattern across 64 new_* functions.
before: each function required 2-3 lines for allocation:
struct mrb_ast_xxx_node *n = (...)parser_palloc(p, sizeof(...));
init_var_header(&n->header, p, NODE_XXX);
after: single line with type-safe macro:
struct mrb_ast_xxx_node *n = NEW_NODE(xxx, NODE_XXX);
saves approximately 128 lines while maintaining readability and providing
central point for future allocation logic changes.
Co-authored-by: Claude <noreply@anthropic.com>
Remove migration-stage "Phase" and "Group" references from comments,
replacing them with descriptions of actual code organization.
Co-authored-by: Claude <noreply@anthropic.com>
replaced migration-related comments (Phase 1/2/3, Group 8-16) with
descriptive comments that explain the current structure organization.
these phase/group comments were artifacts from incremental development
and no longer serve a meaningful purpose in the production codebase.
updated comments to describe what each section contains:
- "Literal value nodes" instead of "Phase 1 Variable Node Structures"
- "Expression and operation nodes" instead of "Phase 2..."
- "Control flow and definition nodes" instead of "Phase 3..."
- removed "Group N:" prefixes and replaced with descriptive headers
Co-authored-by: Claude <noreply@anthropic.com>
removed struct mrb_ast_when_node and when_node() casting macro which
were never actually used. NODE_CASE uses cons lists to represent
when clauses, not dedicated when_node structures. the structure
definition and macro were dead code left over from earlier design.
case/when implementation uses: cons(cons(conditions, body), next_when)
where each when clause is a cons cell in a list, not a typed node.
Co-authored-by: Claude <noreply@anthropic.com>