From 6a31ffa3d492852fd8f409e3ae1e86f2891cc104 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Fri, 27 Feb 2026 21:42:48 -0500 Subject: [PATCH 1/5] ZJIT: Move is_meta_class() from hir::Function to VALUE `self` was unused. --- zjit/src/cruby.rs | 13 +++++++++++++ zjit/src/hir.rs | 15 ++------------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index 90eddefc72d778..565dd65623ec93 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -475,6 +475,19 @@ impl VALUE { true } + /// A metaclass is the singleton class of an object that is a `Module`. + /// This is internal terminology from `class.c`. + pub fn is_metaclass(self) -> bool { + unsafe { + if RB_TYPE_P(self, RUBY_T_CLASS) && rb_zjit_singleton_class_p(self) { + let attached = rb_class_attached_object(self); + RB_TYPE_P(attached, RUBY_T_CLASS) || RB_TYPE_P(attached, RUBY_T_MODULE) + } else { + false + } + } + } + /// Return true for a static (non-heap) Ruby symbol (RB_STATIC_SYM_P) pub fn static_sym_p(self) -> bool { let VALUE(cval) = self; diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 86423e60e08d0f..82b7c057cbb4d8 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -3083,17 +3083,6 @@ impl Function { } } - fn is_metaclass(&self, object: VALUE) -> bool { - unsafe { - if RB_TYPE_P(object, RUBY_T_CLASS) && rb_zjit_singleton_class_p(object) { - let attached = rb_class_attached_object(object); - RB_TYPE_P(attached, RUBY_T_CLASS) || RB_TYPE_P(attached, RUBY_T_MODULE) - } else { - false - } - } - } - pub fn load_rbasic_flags(&mut self, block: BlockId, recv: InsnId) -> InsnId { self.push_insn(block, Insn::LoadField { recv, id: ID!(_rbasic_flags), offset: RUBY_OFFSET_RBASIC_FLAGS, return_type: types::CUInt64 }) } @@ -3313,7 +3302,7 @@ impl Function { } else if !has_block && def_type == VM_METHOD_TYPE_IVAR && args.is_empty() { // Check if we're accessing ivars of a Class or Module object as they require single-ractor mode. // We omit gen_prepare_non_leaf_call on gen_getivar, so it's unsafe to raise for multi-ractor mode. - if self.is_metaclass(klass) && !self.assume_single_ractor_mode(block, state) { + if klass.is_metaclass() && !self.assume_single_ractor_mode(block, state) { self.push_insn_id(block, insn_id); continue; } // Check singleton class assumption first, before emitting other patchpoints @@ -3333,7 +3322,7 @@ impl Function { } else if let (false, VM_METHOD_TYPE_ATTRSET, &[val]) = (has_block, def_type, args.as_slice()) { // Check if we're accessing ivars of a Class or Module object as they require single-ractor mode. // We omit gen_prepare_non_leaf_call on gen_getivar, so it's unsafe to raise for multi-ractor mode. - if self.is_metaclass(klass) && !self.assume_single_ractor_mode(block, state) { + if klass.is_metaclass() && !self.assume_single_ractor_mode(block, state) { self.push_insn_id(block, insn_id); continue; } From c7ed328cd569a258aa949f78f721195e2be15e9e Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 2 Mar 2026 11:49:29 -0500 Subject: [PATCH 2/5] [ruby/prism] Fix in handling in is a unique keyword because it can be the start of a clause or an infix keyword. We need to be explicitly sure that even though in _could_ close an expression context (the body of another in clause) that we are not also parsing an inline in. The exception is the case of a command call, which can never be the LHS of an expression, and so we must immediately exit. [Bug #21925] [Bug #21674] https://github.com/ruby/prism/commit/20374ced51 --- prism/prism.c | 17 +++++++++++------ test/prism/fixtures/case_in_in.txt | 4 ++++ 2 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 test/prism/fixtures/case_in_in.txt diff --git a/prism/prism.c b/prism/prism.c index 3b61472cbc0830..2eb09675adc384 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -21674,12 +21674,6 @@ parse_expression(pm_parser_t *parser, pm_binding_power_t binding_power, bool acc ) { node = parse_expression_infix(parser, node, binding_power, current_binding_powers.right, accepts_command_call, (uint16_t) (depth + 1)); - if (context_terminator(parser->current_context->context, &parser->current)) { - // If this token terminates the current context, then we need to - // stop parsing the expression, as it has become a statement. - return node; - } - switch (PM_NODE_TYPE(node)) { case PM_MULTI_WRITE_NODE: // Multi-write nodes are statements, and cannot be followed by @@ -21792,6 +21786,17 @@ parse_expression(pm_parser_t *parser, pm_binding_power_t binding_power, bool acc break; } } + + if (context_terminator(parser->current_context->context, &parser->current)) { + pm_binding_powers_t next_binding_powers = pm_binding_powers[parser->current.type]; + if ( + !next_binding_powers.binary || + binding_power > next_binding_powers.left || + (PM_NODE_TYPE_P(node, PM_CALL_NODE) && pm_call_node_command_p((pm_call_node_t *) node)) + ) { + return node; + } + } } return node; diff --git a/test/prism/fixtures/case_in_in.txt b/test/prism/fixtures/case_in_in.txt new file mode 100644 index 00000000000000..a5f9e4ec415944 --- /dev/null +++ b/test/prism/fixtures/case_in_in.txt @@ -0,0 +1,4 @@ +case args +in [event] + context.event in ^event +end From 460566ab854e79ed43afdd0c01b14cbd7501fee6 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 2 Mar 2026 12:25:27 -0500 Subject: [PATCH 3/5] [ruby/prism] Reject infix operators on command call on writes https://github.com/ruby/prism/commit/4e71dbfc7b --- prism/prism.c | 30 ++++++++++++++++++++++++--- test/prism/errors/command_call_in.txt | 2 ++ test/prism/errors/write_command.txt | 4 ++++ 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 test/prism/errors/write_command.txt diff --git a/prism/prism.c b/prism/prism.c index 2eb09675adc384..2603bf7adb74c4 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -21605,6 +21605,26 @@ pm_call_node_command_p(const pm_call_node_t *node) { ); } +/** + * Determine if a given write node has a command call as its right-hand side. We + * need this because command calls as the values of writes cannot be extended by + * infix operators. + */ +static inline bool +pm_write_node_command_p(const pm_node_t *node) { + pm_node_t *value; + switch (PM_NODE_TYPE(node)) { + case PM_CLASS_VARIABLE_WRITE_NODE: value = ((pm_class_variable_write_node_t *) node)->value; break; + case PM_CONSTANT_PATH_WRITE_NODE: value = ((pm_constant_path_write_node_t *) node)->value; break; + case PM_CONSTANT_WRITE_NODE: value = ((pm_constant_write_node_t *) node)->value; break; + case PM_GLOBAL_VARIABLE_WRITE_NODE: value = ((pm_global_variable_write_node_t *) node)->value; break; + case PM_INSTANCE_VARIABLE_WRITE_NODE: value = ((pm_instance_variable_write_node_t *) node)->value; break; + case PM_LOCAL_VARIABLE_WRITE_NODE: value = ((pm_local_variable_write_node_t *) node)->value; break; + default: return false; + } + return PM_NODE_TYPE_P(value, PM_CALL_NODE) && pm_call_node_command_p((pm_call_node_t *) value); +} + /** * Parse an expression at the given point of the parser using the given binding * power to parse subsequent chains. If this function finds a syntax error, it @@ -21689,9 +21709,13 @@ parse_expression(pm_parser_t *parser, pm_binding_power_t binding_power, bool acc case PM_INSTANCE_VARIABLE_WRITE_NODE: case PM_LOCAL_VARIABLE_WRITE_NODE: // These expressions are statements, by virtue of the right-hand - // side of their write being an implicit array. - if (PM_NODE_FLAG_P(node, PM_WRITE_NODE_FLAGS_IMPLICIT_ARRAY) && pm_binding_powers[parser->current.type].left > PM_BINDING_POWER_MODIFIER) { - return node; + // side of their write being an implicit array or a command call. + // This mirrors parse.y's behavior where `lhs = command_call` + // reduces to stmt (not expr), preventing and/or from following. + if (pm_binding_powers[parser->current.type].left > PM_BINDING_POWER_MODIFIER) { + if (PM_NODE_FLAG_P(node, PM_WRITE_NODE_FLAGS_IMPLICIT_ARRAY) || pm_write_node_command_p(node)) { + return node; + } } break; case PM_CALL_NODE: diff --git a/test/prism/errors/command_call_in.txt b/test/prism/errors/command_call_in.txt index 2fdcf0973897b7..e9e8e82d3a64ac 100644 --- a/test/prism/errors/command_call_in.txt +++ b/test/prism/errors/command_call_in.txt @@ -2,4 +2,6 @@ foo 1 in a ^~ unexpected 'in', expecting end-of-input ^~ unexpected 'in', ignoring it a = foo 2 in b + ^~ unexpected 'in', expecting end-of-input + ^~ unexpected 'in', ignoring it diff --git a/test/prism/errors/write_command.txt b/test/prism/errors/write_command.txt new file mode 100644 index 00000000000000..5024f8452a276f --- /dev/null +++ b/test/prism/errors/write_command.txt @@ -0,0 +1,4 @@ +a = b c and 1 + ^~~ unexpected 'and', expecting end-of-input + ^~~ unexpected 'and', ignoring it + From 726205b354d1068147719fb42e1de743f1838ef1 Mon Sep 17 00:00:00 2001 From: ZHIJIE XIE <40601688+dummyx@users.noreply.github.com> Date: Tue, 3 Mar 2026 02:40:57 +0900 Subject: [PATCH 4/5] string.c: guard tmp in rb_str_format_m (GH-16280) Keep tmp alive while RARRAY_CONST_PTR(tmp) is used by rb_str_format. [alan: sunk the guard below usage] Reviewed-by: Alan Wu --- string.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/string.c b/string.c index 0fd3ef17b40e00..55a229f37c3b5c 100644 --- a/string.c +++ b/string.c @@ -2623,7 +2623,9 @@ rb_str_format_m(VALUE str, VALUE arg) VALUE tmp = rb_check_array_type(arg); if (!NIL_P(tmp)) { - return rb_str_format(RARRAY_LENINT(tmp), RARRAY_CONST_PTR(tmp), str); + VALUE result = rb_str_format(RARRAY_LENINT(tmp), RARRAY_CONST_PTR(tmp), str); + RB_GC_GUARD(tmp); + return result; } return rb_str_format(1, &arg, str); } From 2106ecec1d5b257b1b92a0a66dc3f5e392f68ffe Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 2 Mar 2026 12:43:30 -0500 Subject: [PATCH 5/5] [ruby/prism] Require arguments to Source.for https://github.com/ruby/prism/commit/b38010c420 --- lib/prism/parse_result.rb | 18 +++++++++++++----- prism/templates/lib/prism/dsl.rb.erb | 6 +++--- prism/templates/lib/prism/serialize.rb.erb | 8 ++++---- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/prism/parse_result.rb b/lib/prism/parse_result.rb index 9825d559aff9ba..c6bb6e5f1f453e 100644 --- a/lib/prism/parse_result.rb +++ b/lib/prism/parse_result.rb @@ -19,9 +19,17 @@ class Source # be used instead of `new` and it will return either a `Source` or a # specialized and more performant `ASCIISource` if no multibyte characters # are present in the source code. - #-- - #: (String source, ?Integer start_line, ?Array[Integer] offsets) -> Source - def self.for(source, start_line = 1, offsets = []) + # + # Note that if you are calling this method manually, you will need to supply + # the start_line and offsets parameters. start_line is the line number that + # the source starts on, which is typically 1 but can be different if this + # source is a subset of a larger source or if this is an eval. offsets is an + # array of byte offsets for the start of each line in the source code, which + # can be calculated by iterating through the source code and recording the + # byte offset whenever a newline character is encountered. + #-- + #: (String source, Integer start_line, Array[Integer] offsets) -> Source + def self.for(source, start_line, offsets) if source.ascii_only? ASCIISource.new(source, start_line, offsets) elsif source.encoding == Encoding::BINARY @@ -55,8 +63,8 @@ def self.for(source, start_line = 1, offsets = []) # Create a new source object with the given source code. #-- - #: (String source, ?Integer start_line, ?Array[Integer] offsets) -> void - def initialize(source, start_line = 1, offsets = []) + #: (String source, Integer start_line, Array[Integer] offsets) -> void + def initialize(source, start_line, offsets) @source = source @start_line = start_line # set after parsing is done @offsets = offsets # set after parsing is done diff --git a/prism/templates/lib/prism/dsl.rb.erb b/prism/templates/lib/prism/dsl.rb.erb index a75b8b253e240b..6dcbbec100994a 100644 --- a/prism/templates/lib/prism/dsl.rb.erb +++ b/prism/templates/lib/prism/dsl.rb.erb @@ -5,7 +5,7 @@ module Prism # The DSL module provides a set of methods that can be used to create prism # nodes in a more concise manner. For example, instead of writing: # - # source = Prism::Source.for("[1]") + # source = Prism::Source.for("[1]", 1, []) # # Prism::ArrayNode.new( # source, @@ -62,7 +62,7 @@ module Prism #-- #: (String string) -> Source def source(string) - Source.for(string) + Source.for(string, 1, []) end # Create a new Location object. @@ -136,7 +136,7 @@ module Prism #-- #: () -> Source def default_source - Source.for("") + Source.for("", 1, []) end # The default location object that gets attached to nodes if no location is diff --git a/prism/templates/lib/prism/serialize.rb.erb b/prism/templates/lib/prism/serialize.rb.erb index 433b5207883e2e..c336662f2c54a1 100644 --- a/prism/templates/lib/prism/serialize.rb.erb +++ b/prism/templates/lib/prism/serialize.rb.erb @@ -27,7 +27,7 @@ module Prism #: (String input, String serialized, bool freeze) -> ParseResult def self.load_parse(input, serialized, freeze) input = input.dup - source = Source.for(input) + source = Source.for(input, 1, []) loader = Loader.new(source, serialized) loader.load_header @@ -81,7 +81,7 @@ module Prism #-- #: (String input, String serialized, bool freeze) -> LexResult def self.load_lex(input, serialized, freeze) - source = Source.for(input) + source = Source.for(input, 1, []) loader = Loader.new(source, serialized) tokens = loader.load_tokens @@ -127,7 +127,7 @@ module Prism #-- #: (String input, String serialized, bool freeze) -> Array[Comment] def self.load_parse_comments(input, serialized, freeze) - source = Source.for(input) + source = Source.for(input, 1, []) loader = Loader.new(source, serialized) loader.load_header @@ -151,7 +151,7 @@ module Prism #-- #: (String input, String serialized, bool freeze) -> ParseLexResult def self.load_parse_lex(input, serialized, freeze) - source = Source.for(input) + source = Source.for(input, 1, []) loader = Loader.new(source, serialized) tokens = loader.load_tokens