diff --git a/NEWS.md b/NEWS.md index 7e00b3a851c0a9..4a6710e836c778 100644 --- a/NEWS.md +++ b/NEWS.md @@ -11,6 +11,11 @@ Note that each entry is kept to a minimum, see links for details. Note: We're only listing outstanding class updates. +* Array + + * `Array#pack` accepts a new format `R` and `r` for unpacking unsigned + and signed LEB128 encoded integers. [[Feature #21785]] + * ENV * `ENV.fetch_values` is added. It returns an array of values for the @@ -31,17 +36,6 @@ Note: We're only listing outstanding class updates. * `MatchData#integer_at` is added. It converts the matched substring to integer and return the result. [[Feature #21932]] -* Method - - * `Method#source_location`, `Proc#source_location`, and - `UnboundMethod#source_location` now return extended location - information with 5 elements: `[path, start_line, start_column, - end_line, end_column]`. The previous 2-element format `[path, - line]` can still be obtained by calling `.take(2)` on the result. - [[Feature #6012]] - * `Array#pack` accepts a new format `R` and `r` for unpacking unsigned - and signed LEB128 encoded integers. [[Feature #21785]] - * Regexp * All instances of `Regexp` are now frozen, not just literals. @@ -145,7 +139,6 @@ A lot of work has gone into making Ractors more stable, performant, and usable. ## JIT -[Feature #6012]: https://bugs.ruby-lang.org/issues/6012 [Feature #8948]: https://bugs.ruby-lang.org/issues/8948 [Feature #15330]: https://bugs.ruby-lang.org/issues/15330 [Feature #21390]: https://bugs.ruby-lang.org/issues/21390 diff --git a/gc.c b/gc.c index a52ef98772a4b0..eb949bfae7c492 100644 --- a/gc.c +++ b/gc.c @@ -1073,6 +1073,37 @@ rb_wb_protected_newobj_of(rb_execution_context_t *ec, VALUE klass, VALUE flags, return newobj_of(rb_ec_ractor_ptr(ec), klass, flags, shape_id, TRUE, size); } +VALUE +rb_class_allocate_instance(VALUE klass) +{ + uint32_t index_tbl_num_entries = RCLASS_MAX_IV_COUNT(klass); + + size_t size = rb_obj_embedded_size(index_tbl_num_entries); + if (!rb_gc_size_allocatable_p(size)) { + size = sizeof(struct RObject); + } + + // There might be a NEWOBJ tracepoint callback, and it may set fields. + // So the shape must be passed to `NEWOBJ_OF`. + VALUE flags = T_OBJECT | (RGENGC_WB_PROTECTED_OBJECT ? FL_WB_PROTECTED : 0); + NEWOBJ_OF_WITH_SHAPE(o, struct RObject, klass, flags, rb_shape_root(rb_gc_heap_id_for_size(size)), size, 0); + VALUE obj = (VALUE)o; + +#if RUBY_DEBUG + RUBY_ASSERT(!rb_shape_obj_too_complex_p(obj)); + VALUE *ptr = ROBJECT_FIELDS(obj); + size_t fields_count = RSHAPE_LEN(RBASIC_SHAPE_ID(obj)); + for (size_t i = fields_count; i < ROBJECT_FIELDS_CAPACITY(obj); i++) { + ptr[i] = Qundef; + } + if (rb_obj_class(obj) != rb_class_real(klass)) { + rb_bug("Expected rb_class_allocate_instance to set the class correctly"); + } +#endif + + return obj; +} + void rb_gc_register_pinning_obj(VALUE obj) { diff --git a/gems/bundled_gems b/gems/bundled_gems index f0d7da6108986c..d68c27f01eaa0b 100644 --- a/gems/bundled_gems +++ b/gems/bundled_gems @@ -39,7 +39,7 @@ benchmark 0.5.0 https://github.com/ruby/benchmark logger 1.7.0 https://github.com/ruby/logger rdoc 7.2.0 https://github.com/ruby/rdoc 911b122a587e24f05434dbeb2c3e39cea607e21f win32ole 1.9.3 https://github.com/ruby/win32ole -irb 1.17.0 https://github.com/ruby/irb +irb 1.17.0 https://github.com/ruby/irb cfd0b917d3feb01adb7d413b19faeb0309900599 reline 0.6.3 https://github.com/ruby/reline readline 0.0.4 https://github.com/ruby/readline fiddle 1.1.8 https://github.com/ruby/fiddle diff --git a/internal/object.h b/internal/object.h index 3bde53c31b10c6..22da9ddb5e26fc 100644 --- a/internal/object.h +++ b/internal/object.h @@ -11,7 +11,7 @@ #include "ruby/ruby.h" /* for VALUE */ /* object.c */ -size_t rb_obj_embedded_size(uint32_t fields_count); + VALUE rb_class_allocate_instance(VALUE klass); VALUE rb_class_search_ancestor(VALUE klass, VALUE super); NORETURN(void rb_undefined_alloc(VALUE klass)); @@ -60,4 +60,10 @@ RBASIC_SET_CLASS(VALUE obj, VALUE klass) RBASIC_SET_CLASS_RAW(obj, klass); RB_OBJ_WRITTEN(obj, oldv, klass); } + +static inline size_t +rb_obj_embedded_size(uint32_t fields_count) +{ + return offsetof(struct RObject, as.ary) + (sizeof(VALUE) * fields_count); +} #endif /* INTERNAL_OBJECT_H */ diff --git a/lib/prism/translation/ripper.rb b/lib/prism/translation/ripper.rb index e1dec0b47e16de..bbfa1f4d05d175 100644 --- a/lib/prism/translation/ripper.rb +++ b/lib/prism/translation/ripper.rb @@ -35,9 +35,6 @@ module Translation # - on_rparen # - on_semicolon # - on_sp - # - on_symbeg - # - on_tstring_beg - # - on_tstring_end # class Ripper < Compiler # Parses the given Ruby program read from +src+. @@ -69,7 +66,7 @@ def self.parse(src, filename = "(ripper)", lineno = 1) # [[1, 13], :on_kw, "end", END ]] # def self.lex(src, filename = "-", lineno = 1, raise_errors: false) - result = Prism.lex_compat(src, filepath: filename, line: lineno, version: "current") + result = Prism.lex_compat(coerce_source(src), filepath: filename, line: lineno, version: "current") if result.failure? && raise_errors raise SyntaxError, result.errors.first.message @@ -91,6 +88,21 @@ def self.tokenize(...) lex(...).map { |token| token[2] } end + # Mirros the various lex_types that ripper supports + def self.coerce_source(source) # :nodoc: + if source.is_a?(IO) + source.read + elsif source.respond_to?(:gets) + src = +"" + while line = source.gets + src << line + end + src + else + source.to_str + end + end + # This contains a table of all of the parser events and their # corresponding arity. PARSER_EVENT_TABLE = { @@ -480,17 +492,7 @@ def self.lex_state_name(state) # Create a new Translation::Ripper object with the given source. def initialize(source, filename = "(ripper)", lineno = 1) - if source.is_a?(IO) - @source = source.read - elsif source.respond_to?(:gets) - @source = +"" - while line = source.gets - @source << line - end - else - @source = source.to_str - end - + @source = Ripper.coerce_source(source) @filename = filename @lineno = lineno @column = 0 @@ -2235,61 +2237,67 @@ def visit_interpolated_regular_expression_node(node) # "foo #{bar}" # ^^^^^^^^^^^^ def visit_interpolated_string_node(node) - if node.opening&.start_with?("<<~") - heredoc = visit_heredoc_string_node(node) + with_string_bounds(node) do + if node.opening&.start_with?("<<~") + heredoc = visit_heredoc_string_node(node) - bounds(node.location) - on_string_literal(heredoc) - elsif !node.heredoc? && node.parts.length > 1 && node.parts.any? { |part| (part.is_a?(StringNode) || part.is_a?(InterpolatedStringNode)) && !part.opening_loc.nil? } - first, *rest = node.parts - rest.inject(visit(first)) do |content, part| - concat = visit(part) - - bounds(part.location) - on_string_concat(content, concat) - end - else - bounds(node.parts.first.location) - parts = - node.parts.inject(on_string_content) do |content, part| - on_string_add(content, visit_string_content(part)) + bounds(node.location) + on_string_literal(heredoc) + elsif !node.heredoc? && node.parts.length > 1 && node.parts.any? { |part| (part.is_a?(StringNode) || part.is_a?(InterpolatedStringNode)) && !part.opening_loc.nil? } + first, *rest = node.parts + rest.inject(visit(first)) do |content, part| + concat = visit(part) + + bounds(part.location) + on_string_concat(content, concat) end + else + bounds(node.parts.first.location) + parts = + node.parts.inject(on_string_content) do |content, part| + on_string_add(content, visit_string_content(part)) + end - bounds(node.location) - on_string_literal(parts) + bounds(node.location) + on_string_literal(parts) + end end end # :"foo #{bar}" # ^^^^^^^^^^^^^ def visit_interpolated_symbol_node(node) - bounds(node.parts.first.location) - parts = - node.parts.inject(on_string_content) do |content, part| - on_string_add(content, visit_string_content(part)) - end + with_string_bounds(node) do + bounds(node.parts.first.location) + parts = + node.parts.inject(on_string_content) do |content, part| + on_string_add(content, visit_string_content(part)) + end - bounds(node.location) - on_dyna_symbol(parts) + bounds(node.location) + on_dyna_symbol(parts) + end end # `foo #{bar}` # ^^^^^^^^^^^^ def visit_interpolated_x_string_node(node) - if node.opening.start_with?("<<~") - heredoc = visit_heredoc_x_string_node(node) + with_string_bounds(node) do + if node.opening.start_with?("<<~") + heredoc = visit_heredoc_x_string_node(node) - bounds(node.location) - on_xstring_literal(heredoc) - else - bounds(node.parts.first.location) - parts = - node.parts.inject(on_xstring_new) do |content, part| - on_xstring_add(content, visit_string_content(part)) - end + bounds(node.location) + on_xstring_literal(heredoc) + else + bounds(node.parts.first.location) + parts = + node.parts.inject(on_xstring_new) do |content, part| + on_xstring_add(content, visit_string_content(part)) + end - bounds(node.location) - on_xstring_literal(parts) + bounds(node.location) + on_xstring_literal(parts) + end end end @@ -3022,24 +3030,60 @@ def visit_statements_node(node) # "foo" # ^^^^^ def visit_string_node(node) - if (content = node.content).empty? - bounds(node.location) - on_string_literal(on_string_content) - elsif (opening = node.opening) == "?" - bounds(node.location) - on_CHAR("?#{node.content}") - elsif opening.start_with?("<<~") - heredoc = visit_heredoc_string_node(node.to_interpolated) + with_string_bounds(node) do + if (content = node.content).empty? + bounds(node.location) + on_string_literal(on_string_content) + elsif (opening = node.opening) == "?" + bounds(node.location) + on_CHAR("?#{node.content}") + elsif opening.start_with?("<<~") + heredoc = visit_heredoc_string_node(node.to_interpolated) - bounds(node.location) - on_string_literal(heredoc) - else - bounds(node.content_loc) - tstring_content = on_tstring_content(content) + bounds(node.location) + on_string_literal(heredoc) + else + bounds(node.content_loc) + tstring_content = on_tstring_content(content) - bounds(node.location) - on_string_literal(on_string_add(on_string_content, tstring_content)) + bounds(node.location) + on_string_literal(on_string_add(on_string_content, tstring_content)) + end + end + end + + # Responsible for emitting the various string-like begin/end events + private def with_string_bounds(node) + # `foo "bar": baz` doesn't emit the closing location + assoc = !(opening = node.opening)&.include?(":") && node.closing&.end_with?(":") + + is_heredoc = opening&.start_with?("<<") + if is_heredoc + bounds(node.opening_loc) + on_heredoc_beg(node.opening) + elsif opening&.start_with?(":", "%s") + bounds(node.opening_loc) + on_symbeg(node.opening) + elsif opening&.start_with?("`", "%x") + bounds(node.opening_loc) + on_backtick(node.opening) + elsif opening && !opening.start_with?("?") + bounds(node.opening_loc) + on_tstring_beg(opening) + end + + result = yield + return result if assoc + + if is_heredoc + bounds(node.closing_loc) + on_heredoc_end(node.closing) + elsif node.closing_loc + bounds(node.closing_loc) + on_tstring_end(node.closing) end + + result end # Ripper gives back the escaped string content but strips out the common @@ -3119,36 +3163,18 @@ def visit_string_node(node) # Visit a heredoc node that is representing a string. private def visit_heredoc_string_node(node) - bounds(node.opening_loc) - on_heredoc_beg(node.opening) - bounds(node.location) - result = - visit_heredoc_node(node.parts, on_string_content) do |parts, part| - on_string_add(parts, part) - end - - bounds(node.closing_loc) - on_heredoc_end(node.closing) - - result + visit_heredoc_node(node.parts, on_string_content) do |parts, part| + on_string_add(parts, part) + end end # Visit a heredoc node that is representing an xstring. private def visit_heredoc_x_string_node(node) - bounds(node.opening_loc) - on_heredoc_beg(node.opening) - bounds(node.location) - result = - visit_heredoc_node(node.parts, on_xstring_new) do |parts, part| - on_xstring_add(parts, part) - end - - bounds(node.closing_loc) - on_heredoc_end(node.closing) - - result + visit_heredoc_node(node.parts, on_xstring_new) do |parts, part| + on_xstring_add(parts, part) + end end # super(foo) @@ -3175,23 +3201,25 @@ def visit_super_node(node) # :foo # ^^^^ def visit_symbol_node(node) - if node.value_loc.nil? - bounds(node.location) - on_dyna_symbol(on_string_content) - elsif (opening = node.opening)&.match?(/^%s|['"]:?$/) - bounds(node.value_loc) - content = on_string_add(on_string_content, on_tstring_content(node.value)) - bounds(node.location) - on_dyna_symbol(content) - elsif (closing = node.closing) == ":" - bounds(node.location) - on_label("#{node.value}:") - elsif opening.nil? && node.closing_loc.nil? - bounds(node.value_loc) - on_symbol_literal(visit_token(node.value)) - else - bounds(node.value_loc) - on_symbol_literal(on_symbol(visit_token(node.value))) + with_string_bounds(node) do + if node.value_loc.nil? + bounds(node.location) + on_dyna_symbol(on_string_content) + elsif (opening = node.opening)&.match?(/^%s|['"]:?$/) + bounds(node.value_loc) + content = on_string_add(on_string_content, on_tstring_content(node.value)) + bounds(node.location) + on_dyna_symbol(content) + elsif (closing = node.closing) == ":" + bounds(node.location) + on_label("#{node.value}:") + elsif opening.nil? && node.closing_loc.nil? + bounds(node.value_loc) + on_symbol_literal(visit_token(node.value)) + else + bounds(node.value_loc) + on_symbol_literal(on_symbol(visit_token(node.value))) + end end end @@ -3314,20 +3342,22 @@ def visit_while_node(node) # `foo` # ^^^^^ def visit_x_string_node(node) - if node.unescaped.empty? - bounds(node.location) - on_xstring_literal(on_xstring_new) - elsif node.opening.start_with?("<<~") - heredoc = visit_heredoc_x_string_node(node.to_interpolated) + with_string_bounds(node) do + if node.unescaped.empty? + bounds(node.location) + on_xstring_literal(on_xstring_new) + elsif node.opening.start_with?("<<~") + heredoc = visit_heredoc_x_string_node(node.to_interpolated) - bounds(node.location) - on_xstring_literal(heredoc) - else - bounds(node.content_loc) - content = on_tstring_content(node.content) + bounds(node.location) + on_xstring_literal(heredoc) + else + bounds(node.content_loc) + content = on_tstring_content(node.content) - bounds(node.location) - on_xstring_literal(on_xstring_add(on_xstring_new, content)) + bounds(node.location) + on_xstring_literal(on_xstring_add(on_xstring_new, content)) + end end end diff --git a/object.c b/object.c index c3241a198d369a..149b70b16a4a18 100644 --- a/object.c +++ b/object.c @@ -90,11 +90,6 @@ static ID id_instance_variables_to_inspect; /*! \endcond */ -size_t -rb_obj_embedded_size(uint32_t fields_count) -{ - return offsetof(struct RObject, as.ary) + (sizeof(VALUE) * fields_count); -} VALUE rb_obj_hide(VALUE obj) @@ -114,36 +109,6 @@ rb_obj_reveal(VALUE obj, VALUE klass) return obj; } -VALUE -rb_class_allocate_instance(VALUE klass) -{ - uint32_t index_tbl_num_entries = RCLASS_MAX_IV_COUNT(klass); - - size_t size = rb_obj_embedded_size(index_tbl_num_entries); - if (!rb_gc_size_allocatable_p(size)) { - size = sizeof(struct RObject); - } - - // There might be a NEWOBJ tracepoint callback, and it may set fields. - // So the shape must be passed to `NEWOBJ_OF`. - VALUE flags = T_OBJECT | (RGENGC_WB_PROTECTED_OBJECT ? FL_WB_PROTECTED : 0); - NEWOBJ_OF_WITH_SHAPE(o, struct RObject, klass, flags, rb_shape_root(rb_gc_heap_id_for_size(size)), size, 0); - VALUE obj = (VALUE)o; - -#if RUBY_DEBUG - RUBY_ASSERT(!rb_shape_obj_too_complex_p(obj)); - VALUE *ptr = ROBJECT_FIELDS(obj); - size_t fields_count = RSHAPE_LEN(RBASIC_SHAPE_ID(obj)); - for (size_t i = fields_count; i < ROBJECT_FIELDS_CAPACITY(obj); i++) { - ptr[i] = Qundef; - } - if (rb_obj_class(obj) != rb_class_real(klass)) { - rb_bug("Expected rb_class_allocate_instance to set the class correctly"); - } -#endif - - return obj; -} VALUE rb_obj_setup(VALUE obj, VALUE klass, VALUE type) @@ -2337,11 +2302,7 @@ class_call_alloc_func(rb_alloc_func_t allocator, VALUE klass) obj = (*allocator)(klass); - if (UNLIKELY(RBASIC_CLASS(obj) != klass)) { - if (rb_obj_class(obj) != rb_class_real(klass)) { - rb_raise(rb_eTypeError, "wrong instance allocation"); - } - } + RUBY_ASSERT(rb_obj_class(obj) == rb_class_real(klass)); return obj; } diff --git a/prism/prism.c b/prism/prism.c index 0e798fdce88305..72c49da6f29499 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -13888,7 +13888,7 @@ parse_arguments(pm_parser_t *parser, pm_arguments_t *arguments, bool accepts_for PRISM_FALLTHROUGH default: { if (argument == NULL) { - argument = parse_value_expression(parser, PM_BINDING_POWER_DEFINED, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (!parsed_first_argument ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0u) | PM_PARSE_ACCEPTS_LABEL, PM_ERR_EXPECT_ARGUMENT, (uint16_t) (depth + 1)); + argument = parse_value_expression(parser, PM_BINDING_POWER_DEFINED, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (!parsed_first_argument ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0u) | PM_PARSE_ACCEPTS_LABEL), PM_ERR_EXPECT_ARGUMENT, (uint16_t) (depth + 1)); } bool contains_keywords = false; @@ -17792,7 +17792,7 @@ parse_case(pm_parser_t *parser, uint8_t flags, uint16_t depth) { } else if (!token_begins_expression_p(parser->current.type)) { predicate = NULL; } else { - predicate = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_CASE_EXPRESSION_AFTER_CASE, (uint16_t) (depth + 1)); + predicate = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_CASE_EXPRESSION_AFTER_CASE, (uint16_t) (depth + 1)); while (accept2(parser, PM_TOKEN_NEWLINE, PM_TOKEN_SEMICOLON)); } @@ -17911,11 +17911,11 @@ parse_case(pm_parser_t *parser, uint8_t flags, uint16_t depth) { * statements. */ if (accept1(parser, PM_TOKEN_KEYWORD_IF_MODIFIER)) { pm_token_t keyword = parser->previous; - pm_node_t *predicate = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_CONDITIONAL_IF_PREDICATE, (uint16_t) (depth + 1)); + pm_node_t *predicate = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_CONDITIONAL_IF_PREDICATE, (uint16_t) (depth + 1)); pattern = UP(pm_if_node_modifier_create(parser, pattern, &keyword, predicate)); } else if (accept1(parser, PM_TOKEN_KEYWORD_UNLESS_MODIFIER)) { pm_token_t keyword = parser->previous; - pm_node_t *predicate = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_CONDITIONAL_UNLESS_PREDICATE, (uint16_t) (depth + 1)); + pm_node_t *predicate = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_CONDITIONAL_UNLESS_PREDICATE, (uint16_t) (depth + 1)); pattern = UP(pm_unless_node_modifier_create(parser, pattern, &keyword, predicate)); } @@ -18004,7 +18004,7 @@ parse_class(pm_parser_t *parser, uint8_t flags, uint16_t depth) { if (accept1(parser, PM_TOKEN_LESS_LESS)) { pm_token_t operator = parser->previous; - pm_node_t *expression = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_EXPECT_EXPRESSION_AFTER_LESS_LESS, (uint16_t) (depth + 1)); + pm_node_t *expression = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_EXPECT_EXPRESSION_AFTER_LESS_LESS, (uint16_t) (depth + 1)); pm_parser_scope_push(parser, true); if (!match2(parser, PM_TOKEN_NEWLINE, PM_TOKEN_SEMICOLON)) { @@ -18053,7 +18053,7 @@ parse_class(pm_parser_t *parser, uint8_t flags, uint16_t depth) { parser->command_start = true; parser_lex(parser); - superclass = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_CLASS_SUPERCLASS, (uint16_t) (depth + 1)); + superclass = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_CLASS_SUPERCLASS, (uint16_t) (depth + 1)); } else { superclass = NULL; } @@ -18235,7 +18235,7 @@ parse_def(pm_parser_t *parser, pm_binding_power_t binding_power, uint8_t flags, parser_lex(parser); pm_token_t lparen = parser->previous; - pm_node_t *expression = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_DEF_RECEIVER, (uint16_t) (depth + 1)); + pm_node_t *expression = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_DEF_RECEIVER, (uint16_t) (depth + 1)); accept1(parser, PM_TOKEN_NEWLINE); expect1(parser, PM_TOKEN_PARENTHESIS_RIGHT, PM_ERR_EXPECT_RPAREN); @@ -19121,7 +19121,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u pm_static_literals_free(&hash_keys); parsed_bare_hash = true; } else { - element = parse_value_expression(parser, PM_BINDING_POWER_DEFINED, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_LABEL, PM_ERR_ARRAY_EXPRESSION, (uint16_t) (depth + 1)); + element = parse_value_expression(parser, PM_BINDING_POWER_DEFINED, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_LABEL), PM_ERR_ARRAY_EXPRESSION, (uint16_t) (depth + 1)); if (pm_symbol_node_label_p(parser, element) || accept1(parser, PM_TOKEN_EQUAL_GREATER)) { if (parsed_bare_hash) { @@ -19851,7 +19851,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u expect1(parser, PM_TOKEN_KEYWORD_IN, PM_ERR_FOR_IN); pm_token_t in_keyword = parser->previous; - pm_node_t *collection = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_FOR_COLLECTION, (uint16_t) (depth + 1)); + pm_node_t *collection = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_FOR_COLLECTION, (uint16_t) (depth + 1)); pm_do_loop_stack_pop(parser); pm_token_t do_keyword = { 0 }; @@ -19954,7 +19954,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u } } } else { - receiver = parse_expression(parser, PM_BINDING_POWER_NOT, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_NOT_EXPRESSION, (uint16_t) (depth + 1)); + receiver = parse_expression(parser, PM_BINDING_POWER_NOT, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_NOT_EXPRESSION, (uint16_t) (depth + 1)); } return UP(pm_call_node_not_create(parser, receiver, &message, &arguments)); @@ -20000,7 +20000,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u parser_lex(parser); pm_token_t keyword = parser->previous; - pm_node_t *predicate = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_CONDITIONAL_UNTIL_PREDICATE, (uint16_t) (depth + 1)); + pm_node_t *predicate = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_CONDITIONAL_UNTIL_PREDICATE, (uint16_t) (depth + 1)); pm_do_loop_stack_pop(parser); context_pop(parser); @@ -20033,7 +20033,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u parser_lex(parser); pm_token_t keyword = parser->previous; - pm_node_t *predicate = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_CONDITIONAL_WHILE_PREDICATE, (uint16_t) (depth + 1)); + pm_node_t *predicate = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_CONDITIONAL_WHILE_PREDICATE, (uint16_t) (depth + 1)); pm_do_loop_stack_pop(parser); context_pop(parser); @@ -20367,7 +20367,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u parser_lex(parser); pm_token_t operator = parser->previous; - pm_node_t *receiver = parse_expression(parser, pm_binding_powers[parser->previous.type].right, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (binding_power < PM_BINDING_POWER_MATCH ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0), PM_ERR_UNARY_RECEIVER, (uint16_t) (depth + 1)); + pm_node_t *receiver = parse_expression(parser, pm_binding_powers[parser->previous.type].right, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (binding_power < PM_BINDING_POWER_MATCH ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0)), PM_ERR_UNARY_RECEIVER, (uint16_t) (depth + 1)); pm_call_node_t *node = pm_call_node_unary_create(parser, &operator, receiver, "!"); pm_conditional_predicate(parser, receiver, PM_CONDITIONAL_PREDICATE_TYPE_NOT); @@ -20568,7 +20568,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u */ static pm_node_t * parse_assignment_value(pm_parser_t *parser, pm_binding_power_t previous_binding_power, pm_binding_power_t binding_power, uint8_t flags, pm_diagnostic_id_t diag_id, uint16_t depth) { - pm_node_t *value = parse_value_expression(parser, binding_power, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (previous_binding_power == PM_BINDING_POWER_ASSIGNMENT ? (flags & PM_PARSE_ACCEPTS_COMMAND_CALL) : (previous_binding_power < PM_BINDING_POWER_MATCH ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0)), diag_id, (uint16_t) (depth + 1)); + pm_node_t *value = parse_value_expression(parser, binding_power, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (previous_binding_power == PM_BINDING_POWER_ASSIGNMENT ? (flags & PM_PARSE_ACCEPTS_COMMAND_CALL) : (previous_binding_power < PM_BINDING_POWER_MATCH ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0))), diag_id, (uint16_t) (depth + 1)); // Assignments whose value is a command call (e.g., a = b c) can only // be followed by modifiers (if/unless/while/until/rescue) and not by @@ -20650,7 +20650,7 @@ parse_assignment_values(pm_parser_t *parser, pm_binding_power_t previous_binding bool permitted = true; if (previous_binding_power != PM_BINDING_POWER_STATEMENT && match1(parser, PM_TOKEN_USTAR)) permitted = false; - pm_node_t *value = parse_starred_expression(parser, binding_power, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (previous_binding_power == PM_BINDING_POWER_ASSIGNMENT ? (flags & PM_PARSE_ACCEPTS_COMMAND_CALL) : (previous_binding_power < PM_BINDING_POWER_MODIFIER ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0)), diag_id, (uint16_t) (depth + 1)); + pm_node_t *value = parse_starred_expression(parser, binding_power, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (previous_binding_power == PM_BINDING_POWER_ASSIGNMENT ? (flags & PM_PARSE_ACCEPTS_COMMAND_CALL) : (previous_binding_power < PM_BINDING_POWER_MODIFIER ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0))), diag_id, (uint16_t) (depth + 1)); if (!permitted) pm_parser_err_node(parser, value, PM_ERR_UNEXPECTED_MULTI_WRITE); parse_assignment_value_local(parser, value); @@ -20705,7 +20705,7 @@ parse_assignment_values(pm_parser_t *parser, pm_binding_power_t previous_binding } } - pm_node_t *right = parse_expression(parser, pm_binding_powers[PM_TOKEN_KEYWORD_RESCUE_MODIFIER].right, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (accepts_command_call_inner ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0), PM_ERR_RESCUE_MODIFIER_VALUE, (uint16_t) (depth + 1)); + pm_node_t *right = parse_expression(parser, pm_binding_powers[PM_TOKEN_KEYWORD_RESCUE_MODIFIER].right, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (accepts_command_call_inner ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0)), PM_ERR_RESCUE_MODIFIER_VALUE, (uint16_t) (depth + 1)); context_pop(parser); return UP(pm_rescue_modifier_node_create(parser, value, &rescue, right)); @@ -21417,14 +21417,14 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t case PM_TOKEN_KEYWORD_AND: { parser_lex(parser); - pm_node_t *right = parse_expression(parser, binding_power, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (parser->previous.type == PM_TOKEN_KEYWORD_AND ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0), PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR, (uint16_t) (depth + 1)); + pm_node_t *right = parse_expression(parser, binding_power, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (parser->previous.type == PM_TOKEN_KEYWORD_AND ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0)), PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR, (uint16_t) (depth + 1)); return UP(pm_and_node_create(parser, node, &token, right)); } case PM_TOKEN_KEYWORD_OR: case PM_TOKEN_PIPE_PIPE: { parser_lex(parser); - pm_node_t *right = parse_expression(parser, binding_power, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (parser->previous.type == PM_TOKEN_KEYWORD_OR ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0), PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR, (uint16_t) (depth + 1)); + pm_node_t *right = parse_expression(parser, binding_power, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (parser->previous.type == PM_TOKEN_KEYWORD_OR ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0)), PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR, (uint16_t) (depth + 1)); return UP(pm_or_node_create(parser, node, &token, right)); } case PM_TOKEN_EQUAL_TILDE: { @@ -21653,14 +21653,14 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t pm_token_t keyword = parser->current; parser_lex(parser); - pm_node_t *predicate = parse_value_expression(parser, binding_power, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_CONDITIONAL_IF_PREDICATE, (uint16_t) (depth + 1)); + pm_node_t *predicate = parse_value_expression(parser, binding_power, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_CONDITIONAL_IF_PREDICATE, (uint16_t) (depth + 1)); return UP(pm_if_node_modifier_create(parser, node, &keyword, predicate)); } case PM_TOKEN_KEYWORD_UNLESS_MODIFIER: { pm_token_t keyword = parser->current; parser_lex(parser); - pm_node_t *predicate = parse_value_expression(parser, binding_power, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_CONDITIONAL_UNLESS_PREDICATE, (uint16_t) (depth + 1)); + pm_node_t *predicate = parse_value_expression(parser, binding_power, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_CONDITIONAL_UNLESS_PREDICATE, (uint16_t) (depth + 1)); return UP(pm_unless_node_modifier_create(parser, node, &keyword, predicate)); } case PM_TOKEN_KEYWORD_UNTIL_MODIFIER: { @@ -21668,7 +21668,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t pm_statements_node_t *statements = pm_statements_node_create(parser); pm_statements_node_body_append(parser, statements, node, true); - pm_node_t *predicate = parse_value_expression(parser, binding_power, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_CONDITIONAL_UNTIL_PREDICATE, (uint16_t) (depth + 1)); + pm_node_t *predicate = parse_value_expression(parser, binding_power, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_CONDITIONAL_UNTIL_PREDICATE, (uint16_t) (depth + 1)); return UP(pm_until_node_modifier_create(parser, &token, predicate, statements, PM_NODE_TYPE_P(node, PM_BEGIN_NODE) ? PM_LOOP_FLAGS_BEGIN_MODIFIER : 0)); } case PM_TOKEN_KEYWORD_WHILE_MODIFIER: { @@ -21676,7 +21676,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t pm_statements_node_t *statements = pm_statements_node_create(parser); pm_statements_node_body_append(parser, statements, node, true); - pm_node_t *predicate = parse_value_expression(parser, binding_power, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_CONDITIONAL_WHILE_PREDICATE, (uint16_t) (depth + 1)); + pm_node_t *predicate = parse_value_expression(parser, binding_power, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_CONDITIONAL_WHILE_PREDICATE, (uint16_t) (depth + 1)); return UP(pm_while_node_modifier_create(parser, &token, predicate, statements, PM_NODE_TYPE_P(node, PM_BEGIN_NODE) ? PM_LOOP_FLAGS_BEGIN_MODIFIER : 0)); } case PM_TOKEN_QUESTION_MARK: { diff --git a/proc.c b/proc.c index f1fc9780607341..99fb880881b9d8 100644 --- a/proc.c +++ b/proc.c @@ -1515,20 +1515,14 @@ proc_eq(VALUE self, VALUE other) static VALUE iseq_location(const rb_iseq_t *iseq) { - VALUE loc[5]; - int i = 0; + VALUE loc[2]; if (!iseq) return Qnil; rb_iseq_check(iseq); - loc[i++] = rb_iseq_path(iseq); - const rb_code_location_t *cl = &ISEQ_BODY(iseq)->location.code_location; - loc[i++] = RB_INT2NUM(cl->beg_pos.lineno); - loc[i++] = RB_INT2NUM(cl->beg_pos.column); - loc[i++] = RB_INT2NUM(cl->end_pos.lineno); - loc[i++] = RB_INT2NUM(cl->end_pos.column); - RUBY_ASSERT_ALWAYS(i == numberof(loc)); + loc[0] = rb_iseq_path(iseq); + loc[1] = RB_INT2NUM(ISEQ_BODY(iseq)->location.first_lineno); - return rb_ary_new_from_values(i, loc); + return rb_ary_new4(2, loc); } VALUE @@ -1539,17 +1533,10 @@ rb_iseq_location(const rb_iseq_t *iseq) /* * call-seq: - * prc.source_location -> [String, Integer, Integer, Integer, Integer] + * prc.source_location -> [String, Integer] * - * Returns the location where the Proc was defined. - * The returned Array contains: - * (1) the Ruby source filename - * (2) the line number where the definition starts - * (3) the position where the definition starts, in number of bytes from the start of the line - * (4) the line number where the definition ends - * (5) the position where the definitions ends, in number of bytes from the start of the line - * - * This method will return +nil+ if the Proc was not defined in Ruby (i.e. native). + * Returns the Ruby source filename and line number containing this proc + * or +nil+ if this proc was not defined in Ruby (i.e. native). */ VALUE @@ -3200,17 +3187,10 @@ rb_method_entry_location(const rb_method_entry_t *me) /* * call-seq: - * meth.source_location -> [String, Integer, Integer, Integer, Integer] - * - * Returns the location where the method was defined. - * The returned Array contains: - * (1) the Ruby source filename - * (2) the line number where the definition starts - * (3) the position where the definition starts, in number of bytes from the start of the line - * (4) the line number where the definition ends - * (5) the position where the definitions ends, in number of bytes from the start of the line + * meth.source_location -> [String, Integer] * - * This method will return +nil+ if the method was not defined in Ruby (i.e. native). + * Returns the Ruby source filename and line number containing this method + * or nil if this method was not defined in Ruby (i.e. native). */ VALUE diff --git a/spec/ruby/core/method/source_location_spec.rb b/spec/ruby/core/method/source_location_spec.rb index 87413a2ab6780d..c5b296f6e2a7b0 100644 --- a/spec/ruby/core/method/source_location_spec.rb +++ b/spec/ruby/core/method/source_location_spec.rb @@ -11,23 +11,23 @@ end it "sets the first value to the path of the file in which the method was defined" do - file = @method.source_location[0] + file = @method.source_location.first file.should be_an_instance_of(String) file.should == File.realpath('fixtures/classes.rb', __dir__) end it "sets the last value to an Integer representing the line on which the method was defined" do - line = @method.source_location[1] + line = @method.source_location.last line.should be_an_instance_of(Integer) line.should == 5 end it "returns the last place the method was defined" do - MethodSpecs::SourceLocation.method(:redefined).source_location[1].should == 13 + MethodSpecs::SourceLocation.method(:redefined).source_location.last.should == 13 end it "returns the location of the original method even if it was aliased" do - MethodSpecs::SourceLocation.new.method(:aka).source_location[1].should == 17 + MethodSpecs::SourceLocation.new.method(:aka).source_location.last.should == 17 end it "works for methods defined with a block" do @@ -108,13 +108,7 @@ def f c = Class.new do eval('def self.m; end', nil, "foo", 100) end - location = c.method(:m).source_location - ruby_version_is(""..."4.1") do - location.should == ["foo", 100] - end - ruby_version_is("4.1") do - location.should == ["foo", 100, 0, 100, 15] - end + c.method(:m).source_location.should == ["foo", 100] end describe "for a Method generated by respond_to_missing?" do diff --git a/spec/ruby/core/proc/source_location_spec.rb b/spec/ruby/core/proc/source_location_spec.rb index fd33f21a26e8b3..a8b99287d5c380 100644 --- a/spec/ruby/core/proc/source_location_spec.rb +++ b/spec/ruby/core/proc/source_location_spec.rb @@ -17,64 +17,57 @@ end it "sets the first value to the path of the file in which the proc was defined" do - file = @proc.source_location[0] + file = @proc.source_location.first file.should be_an_instance_of(String) file.should == File.realpath('fixtures/source_location.rb', __dir__) - file = @proc_new.source_location[0] + file = @proc_new.source_location.first file.should be_an_instance_of(String) file.should == File.realpath('fixtures/source_location.rb', __dir__) - file = @lambda.source_location[0] + file = @lambda.source_location.first file.should be_an_instance_of(String) file.should == File.realpath('fixtures/source_location.rb', __dir__) - file = @method.source_location[0] + file = @method.source_location.first file.should be_an_instance_of(String) file.should == File.realpath('fixtures/source_location.rb', __dir__) end - it "sets the second value to an Integer representing the line on which the proc was defined" do - line = @proc.source_location[1] + it "sets the last value to an Integer representing the line on which the proc was defined" do + line = @proc.source_location.last line.should be_an_instance_of(Integer) line.should == 4 - line = @proc_new.source_location[1] + line = @proc_new.source_location.last line.should be_an_instance_of(Integer) line.should == 12 - line = @lambda.source_location[1] + line = @lambda.source_location.last line.should be_an_instance_of(Integer) line.should == 8 - line = @method.source_location[1] + line = @method.source_location.last line.should be_an_instance_of(Integer) line.should == 15 end it "works even if the proc was created on the same line" do - ruby_version_is(""..."4.1") do - proc { true }.source_location.should == [__FILE__, __LINE__] - Proc.new { true }.source_location.should == [__FILE__, __LINE__] - -> { true }.source_location.should == [__FILE__, __LINE__] - end - ruby_version_is("4.1") do - proc { true }.source_location.should == [__FILE__, __LINE__, 11, __LINE__, 19] - Proc.new { true }.source_location.should == [__FILE__, __LINE__, 15, __LINE__, 23] - -> { true }.source_location.should == [__FILE__, __LINE__, 6, __LINE__, 17] - end + proc { true }.source_location.should == [__FILE__, __LINE__] + Proc.new { true }.source_location.should == [__FILE__, __LINE__] + -> { true }.source_location.should == [__FILE__, __LINE__] end it "returns the first line of a multi-line proc (i.e. the line containing 'proc do')" do - ProcSpecs::SourceLocation.my_multiline_proc.source_location[1].should == 20 - ProcSpecs::SourceLocation.my_multiline_proc_new.source_location[1].should == 34 - ProcSpecs::SourceLocation.my_multiline_lambda.source_location[1].should == 27 + ProcSpecs::SourceLocation.my_multiline_proc.source_location.last.should == 20 + ProcSpecs::SourceLocation.my_multiline_proc_new.source_location.last.should == 34 + ProcSpecs::SourceLocation.my_multiline_lambda.source_location.last.should == 27 end it "returns the location of the proc's body; not necessarily the proc itself" do - ProcSpecs::SourceLocation.my_detached_proc.source_location[1].should == 41 - ProcSpecs::SourceLocation.my_detached_proc_new.source_location[1].should == 51 - ProcSpecs::SourceLocation.my_detached_lambda.source_location[1].should == 46 + ProcSpecs::SourceLocation.my_detached_proc.source_location.last.should == 41 + ProcSpecs::SourceLocation.my_detached_proc_new.source_location.last.should == 51 + ProcSpecs::SourceLocation.my_detached_lambda.source_location.last.should == 46 end it "returns the same value for a proc-ified method as the method reports" do @@ -93,12 +86,6 @@ it "works for eval with a given line" do proc = eval('-> {}', nil, "foo", 100) - location = proc.source_location - ruby_version_is(""..."4.1") do - location.should == ["foo", 100] - end - ruby_version_is("4.1") do - location.should == ["foo", 100, 0, 100, 5] - end + proc.source_location.should == ["foo", 100] end end diff --git a/spec/ruby/core/unboundmethod/source_location_spec.rb b/spec/ruby/core/unboundmethod/source_location_spec.rb index 9cc219801738d7..5c2f14362c40b4 100644 --- a/spec/ruby/core/unboundmethod/source_location_spec.rb +++ b/spec/ruby/core/unboundmethod/source_location_spec.rb @@ -7,23 +7,23 @@ end it "sets the first value to the path of the file in which the method was defined" do - file = @method.source_location[0] + file = @method.source_location.first file.should be_an_instance_of(String) file.should == File.realpath('fixtures/classes.rb', __dir__) end - it "sets the second value to an Integer representing the line on which the method was defined" do - line = @method.source_location[1] + it "sets the last value to an Integer representing the line on which the method was defined" do + line = @method.source_location.last line.should be_an_instance_of(Integer) line.should == 5 end it "returns the last place the method was defined" do - UnboundMethodSpecs::SourceLocation.method(:redefined).unbind.source_location[1].should == 13 + UnboundMethodSpecs::SourceLocation.method(:redefined).unbind.source_location.last.should == 13 end it "returns the location of the original method even if it was aliased" do - UnboundMethodSpecs::SourceLocation.instance_method(:aka).source_location[1].should == 17 + UnboundMethodSpecs::SourceLocation.instance_method(:aka).source_location.last.should == 17 end it "works for define_method methods" do @@ -54,12 +54,6 @@ c = Class.new do eval('def m; end', nil, "foo", 100) end - location = c.instance_method(:m).source_location - ruby_version_is(""..."4.1") do - location.should == ["foo", 100] - end - ruby_version_is("4.1") do - location.should == ["foo", 100, 0, 100, 10] - end + c.instance_method(:m).source_location.should == ["foo", 100] end end diff --git a/test/prism/ruby/find_fixtures.rb b/test/prism/ruby/find_fixtures.rb index df82cc700408b1..c1bef0d0e65ff0 100644 --- a/test/prism/ruby/find_fixtures.rb +++ b/test/prism/ruby/find_fixtures.rb @@ -45,7 +45,6 @@ module DefineMethod end module ForLoop - items = [1, 2, 3] for_proc = nil o = Object.new def o.each(&block) = block.call(block) diff --git a/test/prism/ruby/ripper_test.rb b/test/prism/ruby/ripper_test.rb index 8c80b9f886969f..7274454e1b44ed 100644 --- a/test/prism/ruby/ripper_test.rb +++ b/test/prism/ruby/ripper_test.rb @@ -106,6 +106,7 @@ class RipperTest < TestCase "variables.txt", "whitequark/dedenting_heredoc.txt", "whitequark/masgn_nested.txt", + "whitequark/newline_in_hash_argument.txt", "whitequark/numparam_ruby_bug_19025.txt", "whitequark/op_asgn_cmd.txt", "whitequark/parser_drops_truncated_parts_of_squiggly_heredoc.txt", @@ -135,7 +136,7 @@ def test_lex_ignored_missing_heredoc_end end end - UNSUPPORTED_EVENTS = %i[backtick comma heredoc_beg heredoc_end ignored_nl kw label_end lbrace lbracket lparen nl op rbrace rbracket rparen semicolon sp symbeg tstring_beg tstring_end words_sep ignored_sp] + UNSUPPORTED_EVENTS = %i[comma ignored_nl kw label_end lbrace lbracket lparen nl op rbrace rbracket rparen semicolon sp words_sep ignored_sp] SUPPORTED_EVENTS = Translation::Ripper::EVENTS - UNSUPPORTED_EVENTS module Events @@ -243,6 +244,14 @@ def string_like.to_str end end + def test_lex_coersion + string_like = Object.new + def string_like.to_str + "a" + end + assert_equal Ripper.lex(string_like), Translation::Ripper.lex(string_like) + end + # Check that the hardcoded values don't change without us noticing. def test_internals actual = Translation::Ripper.constants.select { |name| name.start_with?("EXPR_") }.sort diff --git a/test/ruby/test_lambda.rb b/test/ruby/test_lambda.rb index c1858a36ddecdd..ce0f3387600397 100644 --- a/test/ruby/test_lambda.rb +++ b/test/ruby/test_lambda.rb @@ -276,27 +276,27 @@ def test_break end def test_do_lambda_source_location - exp = [__LINE__ + 1, 10, __LINE__ + 5, 7] + exp_lineno = __LINE__ + 3 lmd = ->(x, y, z) do # end - file, *loc = lmd.source_location + file, lineno = lmd.source_location assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(exp, loc) + assert_equal(exp_lineno, lineno, "must be at the beginning of the block") end def test_brace_lambda_source_location - exp = [__LINE__ + 1, 10, __LINE__ + 5, 5] + exp_lineno = __LINE__ + 3 lmd = ->(x, y, z) { # } - file, *loc = lmd.source_location + file, lineno = lmd.source_location assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(exp, loc) + assert_equal(exp_lineno, lineno, "must be at the beginning of the block") end def test_not_orphan_return diff --git a/test/ruby/test_proc.rb b/test/ruby/test_proc.rb index 959ea87f25d667..f74342322f5b78 100644 --- a/test/ruby/test_proc.rb +++ b/test/ruby/test_proc.rb @@ -513,7 +513,7 @@ def test_binding_source_location file, lineno = method(:source_location_test).to_proc.binding.source_location assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(@@line_of_source_location_test[0], lineno, 'Bug #2427') + assert_equal(@@line_of_source_location_test, lineno, 'Bug #2427') end def test_binding_error_unless_ruby_frame @@ -1499,19 +1499,15 @@ def test_to_s assert_include(EnvUtil.labeled_class(name, Proc).new {}.to_s, name) end - @@line_of_source_location_test = [__LINE__ + 1, 2, __LINE__ + 3, 5] + @@line_of_source_location_test = __LINE__ + 1 def source_location_test a=1, b=2 end def test_source_location - file, *loc = method(:source_location_test).source_location + file, lineno = method(:source_location_test).source_location assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(@@line_of_source_location_test, loc, 'Bug #2427') - - file, *loc = self.class.instance_method(:source_location_test).source_location - assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(@@line_of_source_location_test, loc, 'Bug #2427') + assert_equal(@@line_of_source_location_test, lineno, 'Bug #2427') end @@line_of_attr_reader_source_location_test = __LINE__ + 3 @@ -1544,13 +1540,13 @@ def block_source_location_test(*args, &block) end def test_block_source_location - exp_loc = [__LINE__ + 3, 49, __LINE__ + 4, 49] - file, *loc = block_source_location_test(1, + exp_lineno = __LINE__ + 3 + file, lineno = block_source_location_test(1, 2, 3) do end assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(exp_loc, loc) + assert_equal(exp_lineno, lineno) end def test_splat_without_respond_to diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 20fc5bce16a886..0c7d76bdf67292 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -399,6 +399,23 @@ def array.itself = :not_itself RUBY end + def test_send_no_profiles_with_disabled_specialized_instruction + # Regression test: when specialized_instruction is disabled (as power_assert does), + # eval'd code uses `send` instead of `opt_send_without_block`, producing SendNoProfiles. + # The `times` call with a literal block is the SendNoProfiles send whose exit profiling + # triggers recompilation of `run`. After recompilation, `make`'s eval("proc { }") crashes + # in vm_make_env_each because the caller frame's EP[-1] (specval) has a stale value. + assert_runs ':ok', <<~RUBY + RubyVM::InstructionSequence.compile_option = { specialized_instruction: false } + eval <<~'INNERRUBY' + def make = eval("proc { }") + def run(n) = n.times { make } + INNERRUBY + run(6) + :ok + RUBY + end + private # Assert that every method call in `test_script` can be compiled by ZJIT diff --git a/tool/lib/_tmpdir.rb b/tool/lib/_tmpdir.rb index 89fd2b24cd6ae5..ac5b9be792ec63 100644 --- a/tool/lib/_tmpdir.rb +++ b/tool/lib/_tmpdir.rb @@ -85,16 +85,14 @@ def list_tree(parent, indent = "", &block) warn colorize.notice("Children under ")+colorize.fail(tmpdir)+":" Dir.chdir(tmpdir) do ls.list_tree(".") do |path, st| - begin - if st.directory? - Dir.rmdir(path) - else - File.unlink(path) - end - rescue Errno::EACCES - # On Windows, a killed process may still hold file locks briefly. - # Ignore and let FileUtils.rm_rf handle it below. + if st.directory? + Dir.rmdir(path) + else + File.unlink(path) end + rescue Errno::EACCES + # On Windows, a killed process may still hold file locks briefly. + # Ignore and let FileUtils.rm_rf handle it below. end end end diff --git a/tool/rbs_skip_tests b/tool/rbs_skip_tests index 4bcb5707a51d9d..39ac16cb8f1551 100644 --- a/tool/rbs_skip_tests +++ b/tool/rbs_skip_tests @@ -47,11 +47,6 @@ test_compile(RegexpSingletonTest) test_linear_time?(RegexpSingletonTest) test_new(RegexpSingletonTest) -## Failed tests caused by unreleased version of Ruby -test_source_location(MethodInstanceTest) -test_source_location(ProcInstanceTest) -test_source_location(UnboundMethodInstanceTest) - # Errno::ENOENT: No such file or directory - bundle test_collection_install__pathname_set(RBS::CliTest) test_collection_install__set_pathname__manifest(RBS::CliTest) diff --git a/vm.c b/vm.c index 916e379d671a1e..3825189f38ebac 100644 --- a/vm.c +++ b/vm.c @@ -550,7 +550,7 @@ zjit_compile(rb_execution_context_t *ec) // At call-threshold, compile the ISEQ with ZJIT. if (body->jit_entry_calls == rb_zjit_call_threshold) { - rb_zjit_compile_iseq(iseq, false); + rb_zjit_compile_iseq(iseq, ec, false); } } return body->jit_entry; @@ -610,7 +610,7 @@ jit_compile_exception(rb_execution_context_t *ec) // At call-threshold, compile the ISEQ with ZJIT. if (body->jit_exception_calls == rb_zjit_call_threshold) { - rb_zjit_compile_iseq(iseq, true); + rb_zjit_compile_iseq(iseq, ec, true); } } #endif diff --git a/zjit.c b/zjit.c index f9b029503aa8f4..fdebc922f0fc85 100644 --- a/zjit.c +++ b/zjit.c @@ -35,14 +35,14 @@ enum zjit_struct_offsets { void rb_zjit_profile_disable(const rb_iseq_t *iseq); void -rb_zjit_compile_iseq(const rb_iseq_t *iseq, bool jit_exception) +rb_zjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception) { RB_VM_LOCKING() { rb_vm_barrier(); // Compile a block version starting at the current instruction - uint8_t *rb_zjit_iseq_gen_entry_point(const rb_iseq_t *iseq, bool jit_exception); // defined in Rust - uintptr_t code_ptr = (uintptr_t)rb_zjit_iseq_gen_entry_point(iseq, jit_exception); + uint8_t *rb_zjit_iseq_gen_entry_point(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception); // defined in Rust + uintptr_t code_ptr = (uintptr_t)rb_zjit_iseq_gen_entry_point(iseq, ec, jit_exception); if (jit_exception) { iseq->body->jit_exception = (rb_jit_func_t)code_ptr; diff --git a/zjit.h b/zjit.h index f42b77cb356dac..d1d1b01df3b68e 100644 --- a/zjit.h +++ b/zjit.h @@ -13,7 +13,7 @@ extern void *rb_zjit_entry; extern uint64_t rb_zjit_call_threshold; extern uint64_t rb_zjit_profile_threshold; -void rb_zjit_compile_iseq(const rb_iseq_t *iseq, bool jit_exception); +void rb_zjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception); void rb_zjit_profile_insn(uint32_t insn, rb_execution_context_t *ec); void rb_zjit_profile_enable(const rb_iseq_t *iseq); void rb_zjit_bop_redefined(int redefined_flag, enum ruby_basic_operators bop); @@ -31,7 +31,7 @@ void rb_zjit_invalidate_no_singleton_class(VALUE klass); void rb_zjit_invalidate_root_box(void); #else #define rb_zjit_entry 0 -static inline void rb_zjit_compile_iseq(const rb_iseq_t *iseq, bool jit_exception) {} +static inline void rb_zjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception) {} static inline void rb_zjit_profile_insn(uint32_t insn, rb_execution_context_t *ec) {} static inline void rb_zjit_profile_enable(const rb_iseq_t *iseq) {} static inline void rb_zjit_bop_redefined(int redefined_flag, enum ruby_basic_operators bop) {} diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 2f2454f41e362a..9c4a456af91d2f 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -21,7 +21,7 @@ use crate::stats::{counter_ptr, with_time_stat, Counter, Counter::{compile_time_ use crate::{asm::CodeBlock, cruby::*, options::debug, virtualmem::CodePtr}; use crate::backend::lir::{self, Assembler, C_ARG_OPNDS, C_RET_OPND, CFP, EC, NATIVE_STACK_PTR, Opnd, SP, SideExit, SideExitRecompile, Target, asm_ccall, asm_comment}; use crate::hir::{iseq_to_hir, BlockId, Invariant, RangeType, SideExitReason::{self, *}, SpecialBackrefSymbol, SpecialObjectType}; -use crate::hir::{Const, FrameState, Function, Insn, InsnId, SendFallbackReason}; +use crate::hir::{BlockHandler, Const, FrameState, Function, Insn, InsnId, SendFallbackReason}; use crate::hir_type::{types, Type}; use crate::options::{get_option, PerfMap}; use crate::cast::IntoUsize; @@ -144,7 +144,13 @@ define_split_jumps! { /// If jit_exception is true, compile JIT code for handling exceptions. /// See jit_compile_exception() for details. #[unsafe(no_mangle)] -pub extern "C" fn rb_zjit_iseq_gen_entry_point(iseq: IseqPtr, jit_exception: bool) -> *const u8 { +pub extern "C" fn rb_zjit_iseq_gen_entry_point(iseq: IseqPtr, ec: EcPtr, jit_exception: bool) -> *const u8 { + // Don't compile when there is insufficient native stack space + if unsafe { rb_ec_stack_check(ec as _) } != 0 { + incr_counter!(skipped_native_stack_full); + return std::ptr::null(); + } + // Take a lock to avoid writing to ISEQ in parallel with Ractors. // with_vm_lock() does nothing if the program doesn't use Ractors. with_vm_lock(src_loc!(), || { @@ -609,10 +615,11 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::Param => unreachable!("block.insns should not have Insn::Param"), Insn::LoadArg { .. } => return Ok(()), // compiled in the LoadArg pre-pass above Insn::Snapshot { .. } => return Ok(()), // we don't need to do anything for this instruction at the moment - &Insn::Send { cd, blockiseq: None, state, reason, .. } => gen_send_without_block(jit, asm, cd, &function.frame_state(state), reason), - &Insn::Send { cd, blockiseq: Some(blockiseq), state, reason, .. } => gen_send(jit, asm, cd, blockiseq, &function.frame_state(state), reason), + &Insn::Send { cd, block: None, state, reason, .. } => gen_send_without_block(jit, asm, cd, &function.frame_state(state), reason), + &Insn::Send { cd, block: Some(BlockHandler::BlockIseq(blockiseq)), state, reason, .. } => gen_send(jit, asm, cd, blockiseq, &function.frame_state(state), reason), + &Insn::Send { cd, block: Some(BlockHandler::BlockArg), state, reason, .. } => gen_send(jit, asm, cd, std::ptr::null(), &function.frame_state(state), reason), &Insn::SendForward { cd, blockiseq, state, reason, .. } => gen_send_forward(jit, asm, cd, blockiseq, &function.frame_state(state), reason), - Insn::SendDirect { cme, iseq, recv, args, kw_bits, blockiseq, state, .. } => gen_send_iseq_direct(cb, jit, asm, *cme, *iseq, opnd!(recv), opnds!(args), *kw_bits, &function.frame_state(*state), *blockiseq), + Insn::SendDirect { cme, iseq, recv, args, kw_bits, block, state, .. } => gen_send_iseq_direct(cb, jit, asm, *cme, *iseq, opnd!(recv), opnds!(args), *kw_bits, &function.frame_state(*state), *block), &Insn::InvokeSuper { cd, blockiseq, state, reason, .. } => gen_invokesuper(jit, asm, cd, blockiseq, &function.frame_state(state), reason), &Insn::InvokeSuperForward { cd, blockiseq, state, reason, .. } => gen_invokesuperforward(jit, asm, cd, blockiseq, &function.frame_state(state), reason), &Insn::InvokeBlock { cd, state, reason, .. } => gen_invokeblock(jit, asm, cd, &function.frame_state(state), reason), @@ -674,10 +681,10 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio // There's no test case for this because no core cfuncs have this many parameters. But C extensions could have such methods. Insn::CCallWithFrame { cd, state, args, .. } if args.len() + 1 > C_ARG_OPNDS.len() => gen_send_without_block(jit, asm, *cd, &function.frame_state(*state), SendFallbackReason::CCallWithFrameTooManyArgs), - Insn::CCallWithFrame { cfunc, recv, name, args, cme, state, blockiseq, .. } => - gen_ccall_with_frame(jit, asm, *cfunc, *name, opnd!(recv), opnds!(args), *cme, *blockiseq, &function.frame_state(*state)), - Insn::CCallVariadic { cfunc, recv, name, args, cme, state, blockiseq, return_type: _, elidable: _ } => { - gen_ccall_variadic(jit, asm, *cfunc, *name, opnd!(recv), opnds!(args), *cme, *blockiseq, &function.frame_state(*state)) + Insn::CCallWithFrame { cfunc, recv, name, args, cme, state, block, .. } => + gen_ccall_with_frame(jit, asm, *cfunc, *name, opnd!(recv), opnds!(args), *cme, *block, &function.frame_state(*state)), + Insn::CCallVariadic { cfunc, recv, name, args, cme, state, block, return_type: _, elidable: _ } => { + gen_ccall_variadic(jit, asm, *cfunc, *name, opnd!(recv), opnds!(args), *cme, *block, &function.frame_state(*state)) } Insn::GetIvar { self_val, id, ic, state: _ } => gen_getivar(jit, asm, opnd!(self_val), *id, *ic), Insn::SetGlobal { id, val, state } => no_output!(gen_setglobal(jit, asm, *id, opnd!(val), &function.frame_state(*state))), @@ -1004,7 +1011,7 @@ fn gen_ccall_with_frame( recv: Opnd, args: Vec, cme: *const rb_callable_method_entry_t, - blockiseq: Option, + block: Option, state: &FrameState, ) -> lir::Opnd { gen_incr_counter(asm, Counter::non_variadic_cfunc_optimized_send_count); @@ -1020,7 +1027,7 @@ fn gen_ccall_with_frame( gen_spill_stack(jit, asm, state); gen_spill_locals(jit, asm, state); - let block_handler_specval = if let Some(block_iseq) = blockiseq { + let block_handler_specval = if let Some(BlockHandler::BlockIseq(block_iseq)) = block { // Change cfp->block_code in the current frame. See vm_caller_setup_arg_block(). // VM_CFP_TO_CAPTURED_BLOCK then turns &cfp->self into a block handler. // rb_captured_block->code.iseq aliases with cfp->block_code. @@ -1095,7 +1102,7 @@ fn gen_ccall_variadic( recv: Opnd, args: Vec, cme: *const rb_callable_method_entry_t, - blockiseq: Option, + block: Option, state: &FrameState, ) -> lir::Opnd { gen_incr_counter(asm, Counter::variadic_cfunc_optimized_send_count); @@ -1114,7 +1121,7 @@ fn gen_ccall_variadic( gen_spill_stack(jit, asm, state); gen_spill_locals(jit, asm, state); - let block_handler_specval = if let Some(blockiseq) = blockiseq { + let block_handler_specval = if let Some(BlockHandler::BlockIseq(blockiseq)) = block { gen_block_handler_specval(asm, blockiseq) } else { VM_BLOCK_HANDLER_NONE.into() @@ -1536,7 +1543,7 @@ fn gen_send_iseq_direct( args: Vec, kw_bits: u32, state: &FrameState, - blockiseq: Option, + block: Option, ) -> lir::Opnd { gen_incr_counter(asm, Counter::iseq_optimized_send_count); @@ -1556,7 +1563,7 @@ fn gen_send_iseq_direct( // The HIR specialization guards ensure we will only reach here for literal blocks, // not &block forwarding, &:foo, etc. Thise are rejected in `type_specialize` by // `unspecializable_call_type`. - let block_handler = blockiseq.map(|b| gen_block_handler_specval(asm, b)); + let block_handler = block.map(|bh| match bh { BlockHandler::BlockIseq(b) => gen_block_handler_specval(asm, b), BlockHandler::BlockArg => unreachable!("BlockArg in gen_send_iseq_direct") }); let callee_is_bmethod = VM_METHOD_TYPE_BMETHOD == unsafe { get_cme_def_type(cme) }; diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 591ca0aad7cf22..7107f7046afbb9 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -750,6 +750,15 @@ impl Display for SendFallbackReason { } } +/// How a block is passed to a send-like instruction. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum BlockHandler { + /// Literal block ISEQ (e.g. `foo { ... }`) + BlockIseq(IseqPtr), + /// Block arg passed via &proc (e.g. `foo(&block)`) + BlockArg, +} + /// An instruction in the SSA IR. The output of an instruction is referred to by the index of /// the instruction ([`InsnId`]). SSA form enables this, and [`UnionFind`] ([`Function::find`]) /// helps with editing. @@ -931,7 +940,7 @@ pub enum Insn { state: InsnId, return_type: Type, elidable: bool, - blockiseq: Option, + block: Option, }, /// Call a variadic C function with signature: func(int argc, VALUE *argv, VALUE recv) @@ -945,7 +954,7 @@ pub enum Insn { state: InsnId, return_type: Type, elidable: bool, - blockiseq: Option, + block: Option, }, /// Un-optimized fallback implementation (dynamic dispatch) for send-ish instructions @@ -953,7 +962,7 @@ pub enum Insn { Send { recv: InsnId, cd: *const rb_call_data, - blockiseq: Option, + block: Option, args: Vec, state: InsnId, reason: SendFallbackReason, @@ -1004,7 +1013,7 @@ pub enum Insn { iseq: IseqPtr, args: Vec, kw_bits: u32, - blockiseq: Option, + block: Option, state: InsnId, }, @@ -1865,21 +1874,25 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::Jump(target) => { write!(f, "Jump {target}") } Insn::IfTrue { val, target } => { write!(f, "IfTrue {val}, {target}") } Insn::IfFalse { val, target } => { write!(f, "IfFalse {val}, {target}") } - Insn::SendDirect { recv, cd, iseq, args, blockiseq, .. } => { - write!(f, "SendDirect {recv}, {:p}, :{} ({:?})", self.ptr_map.map_ptr(blockiseq), ruby_call_method_name(*cd), self.ptr_map.map_ptr(iseq))?; + Insn::SendDirect { recv, cd, iseq, args, block, .. } => { + let blockiseq = block.map(|bh| match bh { BlockHandler::BlockIseq(iseq) => iseq, BlockHandler::BlockArg => unreachable!() }); + write!(f, "SendDirect {recv}, {:p}, :{} ({:?})", self.ptr_map.map_ptr(&blockiseq), ruby_call_method_name(*cd), self.ptr_map.map_ptr(iseq))?; for arg in args { write!(f, ", {arg}")?; } Ok(()) } - Insn::Send { recv, cd, args, blockiseq, reason, .. } => { + Insn::Send { recv, cd, args, block, reason, .. } => { // For tests, we want to check HIR snippets textually. Addresses change // between runs, making tests fail. Instead, pick an arbitrary hex value to // use as a "pointer" so we can check the rest of the HIR. - if let Some(blockiseq) = *blockiseq { - write!(f, "Send {recv}, {:p}, :{}", self.ptr_map.map_ptr(blockiseq), ruby_call_method_name(*cd))?; - } else { - write!(f, "Send {recv}, :{}", ruby_call_method_name(*cd))?; + match *block { + Some(BlockHandler::BlockIseq(blockiseq)) => + write!(f, "Send {recv}, {:p}, :{}", self.ptr_map.map_ptr(blockiseq), ruby_call_method_name(*cd))?, + Some(BlockHandler::BlockArg) => + write!(f, "Send {recv}, &block, :{}", ruby_call_method_name(*cd))?, + None => + write!(f, "Send {recv}, :{}", ruby_call_method_name(*cd))?, } for arg in args { write!(f, ", {arg}")?; @@ -1991,13 +2004,17 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { } Ok(()) }, - Insn::CCallWithFrame { cfunc, recv, args, name, cme, blockiseq, .. } => { + Insn::CCallWithFrame { cfunc, recv, args, name, cme, block, .. } => { write!(f, "CCallWithFrame {recv}, :{}@{:p}", qualified_method_name(unsafe { (**cme).owner }, *name), self.ptr_map.map_ptr(cfunc))?; for arg in args { write!(f, ", {arg}")?; } - if let Some(blockiseq) = blockiseq { - write!(f, ", block={:p}", self.ptr_map.map_ptr(blockiseq))?; + match block { + Some(BlockHandler::BlockIseq(blockiseq)) => + write!(f, ", block={:p}", self.ptr_map.map_ptr(blockiseq))?, + Some(BlockHandler::BlockArg) => + write!(f, ", block=&block")?, + None => {} } Ok(()) }, @@ -2755,20 +2772,20 @@ impl Function { str: find!(str), state, }, - &SendDirect { recv, cd, cme, iseq, ref args, kw_bits, blockiseq, state } => SendDirect { + &SendDirect { recv, cd, cme, iseq, ref args, kw_bits, block, state } => SendDirect { recv: find!(recv), cd, cme, iseq, args: find_vec!(args), kw_bits, - blockiseq, + block, state, }, - &Send { recv, cd, blockiseq, ref args, state, reason } => Send { + &Send { recv, cd, block, ref args, state, reason } => Send { recv: find!(recv), cd, - blockiseq, + block, args: find_vec!(args), state, reason, @@ -2817,7 +2834,7 @@ impl Function { &ObjectAlloc { val, state } => ObjectAlloc { val: find!(val), state }, &ObjectAllocClass { class, state } => ObjectAllocClass { class, state: find!(state) }, &CCall { cfunc, recv, ref args, name, owner, return_type, elidable } => CCall { cfunc, recv: find!(recv), args: find_vec!(args), name, owner, return_type, elidable }, - &CCallWithFrame { cd, cfunc, recv, ref args, cme, name, state, return_type, elidable, blockiseq } => CCallWithFrame { + &CCallWithFrame { cd, cfunc, recv, ref args, cme, name, state, return_type, elidable, block } => CCallWithFrame { cd, cfunc, recv: find!(recv), @@ -2827,10 +2844,10 @@ impl Function { state: find!(state), return_type, elidable, - blockiseq, + block, }, - &CCallVariadic { cfunc, recv, ref args, cme, name, state, return_type, elidable, blockiseq } => CCallVariadic { - cfunc, recv: find!(recv), args: find_vec!(args), cme, name, state, return_type, elidable, blockiseq + &CCallVariadic { cfunc, recv, ref args, cme, name, state, return_type, elidable, block } => CCallVariadic { + cfunc, recv: find!(recv), args: find_vec!(args), cme, name, state, return_type, elidable, block }, &CheckMatch { target, pattern, flag, state } => CheckMatch { target: find!(target), pattern: find!(pattern), flag, state: find!(state) }, &Defined { op_type, obj, pushval, v, state } => Defined { op_type, obj, pushval, v: find!(v), state: find!(state) }, @@ -3566,12 +3583,12 @@ impl Function { assert!(self.blocks[block.0].insns.is_empty()); for insn_id in old_insns { match self.find(insn_id) { - Insn::Send { recv, blockiseq: None, args, state, cd, .. } if ruby_call_method_id(cd) == ID!(freeze) && args.is_empty() => + Insn::Send { recv, block: None, args, state, cd, .. } if ruby_call_method_id(cd) == ID!(freeze) && args.is_empty() => self.try_rewrite_freeze(block, insn_id, recv, state), - Insn::Send { recv, blockiseq: None, args, state, cd, .. } if ruby_call_method_id(cd) == ID!(minusat) && args.is_empty() => + Insn::Send { recv, block: None, args, state, cd, .. } if ruby_call_method_id(cd) == ID!(minusat) && args.is_empty() => self.try_rewrite_uminus(block, insn_id, recv, state), - Insn::Send { mut recv, cd, state, blockiseq, args, .. } => { - let has_block = blockiseq.is_some(); + Insn::Send { mut recv, cd, state, block: send_block, args, .. } => { + let has_block = send_block.is_some(); let frame_state = self.frame_state(state); let (klass, profiled_type) = match self.resolve_receiver_type(recv, self.type_of(recv), frame_state.insn_idx) { ReceiverTypeResolution::StaticallyKnown { class } => (class, None), @@ -3670,7 +3687,7 @@ impl Function { recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state }); } - let send_direct = self.push_insn(block, Insn::SendDirect { recv, cd, cme, iseq, args: processed_args, kw_bits, state: send_state, blockiseq }); + let send_direct = self.push_insn(block, Insn::SendDirect { recv, cd, cme, iseq, args: processed_args, kw_bits, state: send_state, block: send_block }); self.make_equal_to(insn_id, send_direct); } else if !has_block && def_type == VM_METHOD_TYPE_BMETHOD { let procv = unsafe { rb_get_def_bmethod_proc((*cme).def) }; @@ -3712,7 +3729,7 @@ impl Function { recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state }); } - let send_direct = self.push_insn(block, Insn::SendDirect { recv, cd, cme, iseq, args: processed_args, kw_bits, state: send_state, blockiseq: None }); + let send_direct = self.push_insn(block, Insn::SendDirect { recv, cd, cme, iseq, args: processed_args, kw_bits, state: send_state, block: None }); self.make_equal_to(insn_id, send_direct); } 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. @@ -3877,7 +3894,7 @@ impl Function { self.make_equal_to(insn_id, guard); } else { let recv = self.push_insn(block, Insn::GuardType { val, guard_type: Type::from_profiled_type(recv_type), state}); - let send_to_s = self.push_insn(block, Insn::Send { recv, cd, blockiseq: None, args: vec![], state, reason: ObjToStringNotString }); + let send_to_s = self.push_insn(block, Insn::Send { recv, cd, block: None, args: vec![], state, reason: ObjToStringNotString }); self.make_equal_to(insn_id, send_to_s); } } @@ -4075,7 +4092,7 @@ impl Function { args: processed_args, kw_bits, state: send_state, - blockiseq: None, + block: None, }); self.make_equal_to(insn_id, send_direct); @@ -4142,7 +4159,7 @@ impl Function { state, return_type: types::BasicObject, elidable: false, - blockiseq: None, + block: None, }) }; self.make_equal_to(insn_id, ccall); @@ -4192,7 +4209,7 @@ impl Function { state, return_type: types::BasicObject, elidable: false, - blockiseq: None, + block: None, }) }; self.make_equal_to(insn_id, ccall); @@ -4588,7 +4605,7 @@ impl Function { send: Insn, send_insn_id: InsnId, ) -> Result<(), ()> { - let Insn::Send { mut recv, cd, blockiseq, args, state, .. } = send else { + let Insn::Send { mut recv, cd, block: send_block, args, state, .. } = send else { return Err(()); }; @@ -4646,10 +4663,9 @@ impl Function { return Err(()); } - let blockiseq = match blockiseq { - None => unreachable!("went to reduce_send_without_block_to_ccall"), - Some(p) if p.is_null() => None, - Some(blockiseq) => Some(blockiseq), + let blockiseq = match send_block { + None | Some(BlockHandler::BlockArg) => unreachable!("went to reduce_send_without_block_to_ccall"), + Some(BlockHandler::BlockIseq(blockiseq)) => Some(blockiseq), }; let cfunc = unsafe { get_cme_def_body_cfunc(cme) }; @@ -4695,7 +4711,7 @@ impl Function { state, return_type: types::BasicObject, elidable: false, - blockiseq, + block: blockiseq.map(BlockHandler::BlockIseq), }); fun.make_equal_to(send_insn_id, ccall); Ok(()) @@ -4732,7 +4748,7 @@ impl Function { state, return_type: types::BasicObject, elidable: false, - blockiseq + block: blockiseq.map(BlockHandler::BlockIseq), }); fun.make_equal_to(send_insn_id, ccall); @@ -4886,7 +4902,7 @@ impl Function { state, return_type, elidable, - blockiseq: None, + block: None, }); fun.make_equal_to(send_insn_id, ccall); } @@ -4960,7 +4976,7 @@ impl Function { state, return_type, elidable, - blockiseq: None, + block: None, }); fun.make_equal_to(send_insn_id, ccall); @@ -4986,7 +5002,7 @@ impl Function { for insn_id in old_insns { let send = self.find(insn_id); match send { - send @ Insn::Send { recv, blockiseq: None, .. } => { + send @ Insn::Send { recv, block: None, .. } => { let recv_type = self.type_of(recv); if reduce_send_without_block_to_ccall(self, block, recv_type, send, insn_id).is_ok() { continue; @@ -5031,12 +5047,19 @@ impl Function { /// The remaining no-profile sends are turned into side exits that trigger recompilation with /// fresh profile data. fn convert_no_profile_sends(&mut self) { + // On the final version, recompilation is not possible, so converting sends to + // SideExits would just add overhead (the exit fires every time without benefit). + // Keep them as Send fallbacks so the interpreter handles them directly. + let payload = get_or_create_iseq_payload(self.iseq); + if payload.versions.len() + 1 >= crate::codegen::MAX_ISEQ_VERSIONS { + return; + } for block in self.rpo() { let old_insns = std::mem::take(&mut self.blocks[block.0].insns); assert!(self.blocks[block.0].insns.is_empty()); for insn_id in old_insns { match self.find(insn_id) { - Insn::Send { cd, state, reason: SendFallbackReason::SendWithoutBlockNoProfiles, .. } => { + Insn::Send { cd, state, reason: SendFallbackReason::SendWithoutBlockNoProfiles | SendFallbackReason::SendNoProfiles, .. } => { let argc = unsafe { vm_ci_argc((*cd).ci) } as i32; self.push_insn(block, Insn::SideExit { state, reason: SideExitReason::NoProfileSend, recompile: Some(argc) }); // SideExit is a terminator; don't add remaining instructions @@ -6758,15 +6781,17 @@ impl ProfileOracle { Self { payload, types: Default::default() } } - /// Map the interpreter-recorded types of the stack onto the HIR operands on our compile-time virtual stack - fn profile_stack(&mut self, state: &FrameState) { + /// Map the interpreter-recorded types of the stack onto the HIR operands on our compile-time virtual stack. + /// `stack_offset` is the number of extra stack entries above the profiled operands (e.g. 1 for + /// sends with ARGS_BLOCKARG, where the block arg sits on top of the regular args). + fn profile_stack(&mut self, state: &FrameState, stack_offset: usize) { let iseq_insn_idx = state.insn_idx; let Some(operand_types) = self.payload.profile.get_operand_types(iseq_insn_idx) else { return }; let entry = self.types.entry(iseq_insn_idx).or_default(); // operand_types is always going to be <= stack size (otherwise it would have an underflow // at run-time) so use that to drive iteration. for (idx, insn_type_distribution) in operand_types.iter().rev().enumerate() { - let insn = state.stack_topn(idx).expect("Unexpected stack underflow in profiling"); + let insn = state.stack_topn(idx + stack_offset).expect("Unexpected stack underflow in profiling"); entry.push((insn, TypeDistributionSummary::new(insn_type_distribution))) } } @@ -6969,7 +6994,17 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { } } else { - profiles.profile_stack(&exit_state); + // For sends with ARGS_BLOCKARG, the block arg sits on the stack above + // the profiled operands (receiver + regular args). Skip it so that the + // profile types map onto the correct HIR operands. + let stack_offset = if opcode == YARVINSN_send || opcode == YARVINSN_opt_send_without_block { + let cd: *const rb_call_data = get_arg(pc, 0).as_ptr(); + let flags = unsafe { vm_ci_flag(rb_get_call_data_ci(cd)) }; + usize::from(flags & VM_CALL_ARGS_BLOCKARG != 0) + } else { + 0 + }; + profiles.profile_stack(&exit_state, stack_offset); } // Flag a future getlocal/setlocal to add a patch point if this instruction is not leaf. @@ -7675,7 +7710,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { } let args = state.stack_pop_n(argc as usize)?; let recv = state.stack_pop()?; - let send = fun.push_insn(block, Insn::Send { recv, cd, blockiseq: None, args, state: exit_id, reason: Uncategorized(opcode) }); + let send = fun.push_insn(block, Insn::Send { recv, cd, block: None, args, state: exit_id, reason: Uncategorized(opcode) }); state.stack_push(send); } YARVINSN_opt_hash_freeze => { @@ -7833,7 +7868,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let recv = state.stack_pop().unwrap(); let refined_recv = fun.push_insn(block, Insn::RefineType { val: recv, new_type }); state.replace(recv, refined_recv); - let send = fun.push_insn(block, Insn::Send { recv: refined_recv, cd, blockiseq: None, args, state: snapshot, reason: Uncategorized(opcode) }); + let send = fun.push_insn(block, Insn::Send { recv: refined_recv, cd, block: None, args, state: snapshot, reason: Uncategorized(opcode) }); state.stack_push(send); fun.push_insn(block, Insn::Jump(BranchEdge { target: join_block, args: state.as_args(self_param) })); block @@ -7872,7 +7907,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let args = state.stack_pop_n(argc as usize)?; let recv = state.stack_pop()?; let reason = SendWithoutBlockPolymorphicFallback; - let send = fun.push_insn(block, Insn::Send { recv, cd, blockiseq: None, args, state: exit_id, reason }); + let send = fun.push_insn(block, Insn::Send { recv, cd, block: None, args, state: exit_id, reason }); state.stack_push(send); fun.push_insn(block, Insn::Jump(BranchEdge { target: join_block, args: state.as_args(self_param) })); break; // End the block @@ -7881,7 +7916,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let args = state.stack_pop_n(argc as usize)?; let recv = state.stack_pop()?; - let send = fun.push_insn(block, Insn::Send { recv, cd, blockiseq: None, args, state: exit_id, reason: Uncategorized(opcode) }); + let send = fun.push_insn(block, Insn::Send { recv, cd, block: None, args, state: exit_id, reason: Uncategorized(opcode) }); state.stack_push(send); } YARVINSN_send => { @@ -7904,10 +7939,17 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let args = state.stack_pop_n(argc as usize + usize::from(block_arg))?; let recv = state.stack_pop()?; - let send = fun.push_insn(block, Insn::Send { recv, cd, blockiseq: Some(blockiseq), args, state: exit_id, reason: Uncategorized(opcode) }); + let block_handler = if !blockiseq.is_null() { + Some(BlockHandler::BlockIseq(blockiseq)) + } else if block_arg { + Some(BlockHandler::BlockArg) + } else { + None + }; + let send = fun.push_insn(block, Insn::Send { recv, cd, block: block_handler, args, state: exit_id, reason: Uncategorized(opcode) }); state.stack_push(send); - if !blockiseq.is_null() { + if let Some(BlockHandler::BlockIseq(_)) = block_handler { // Reload locals that may have been modified by the blockiseq. // TODO: Avoid reloading locals that are not referenced by the blockiseq // or not used after this. Max thinks we could eventually DCE them. diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 2d3e43d0f1978b..3cf02934e7ad23 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -1485,9 +1485,10 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, length@0x1010, cme:0x1018) v24:ArrayExact = GuardType v10, ArrayExact - v25:BasicObject = CCallWithFrame v24, :Array#length@0x1040 + v25:CInt64 = ArrayLength v24 + v26:Fixnum = BoxFixnum v25 CheckInterrupts - Return v25 + Return v26 "); } @@ -4740,9 +4741,7 @@ mod hir_opt_tests { v18:CInt64 = LoadField v15, :_env_data_index_specval@0x1002 v19:CInt64 = GuardAnyBitSet v18, CUInt64(1) v20:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) - v22:BasicObject = Send v9, 0x1001, :tap, v20 # SendFallbackReason: Send: no profile data available - CheckInterrupts - Return v22 + SideExit NoProfileSend recompile "); } @@ -7916,7 +7915,7 @@ mod hir_opt_tests { v19:CInt64 = LoadField v16, :_env_data_index_specval@0x1002 v20:CInt64 = GuardAnyBitSet v19, CUInt64(1) v21:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) - v23:BasicObject = Send v14, 0x1001, :map, v21 # SendFallbackReason: Complex argument passing + v23:BasicObject = Send v14, &block, :map, v21 # SendFallbackReason: Complex argument passing CheckInterrupts Return v23 "); @@ -7949,7 +7948,7 @@ mod hir_opt_tests { v19:CInt64 = LoadField v16, :_env_data_index_specval@0x1002 v20:CInt64[0] = GuardBitEquals v19, CInt64(0) v21:NilClass = Const Value(nil) - v23:BasicObject = Send v14, 0x1001, :map, v21 # SendFallbackReason: Complex argument passing + v23:BasicObject = Send v14, &block, :map, v21 # SendFallbackReason: Complex argument passing CheckInterrupts Return v23 "); @@ -7983,7 +7982,7 @@ mod hir_opt_tests { v15:CInt64 = LoadField v12, :_env_data_index_specval@0x1001 v16:CInt64 = GuardAnyBitSet v15, CUInt64(1) v17:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) - v19:BasicObject = Send v10, 0x1000, :map, v17 # SendFallbackReason: Complex argument passing + v19:BasicObject = Send v10, &block, :map, v17 # SendFallbackReason: Complex argument passing CheckInterrupts Return v19 "); @@ -11427,12 +11426,50 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): v11:StaticSymbol[:the_block] = Const Value(VALUE(0x1000)) - v13:BasicObject = Send v6, 0x1008, :callee, v11 # SendFallbackReason: Send: no profile data available + v13:BasicObject = Send v6, &block, :callee, v11 # SendFallbackReason: Complex argument passing CheckInterrupts Return v13 "); } + #[test] + fn test_profile_stack_skips_block_arg() { + // Regression test: profile_stack must skip the &block arg on the stack when mapping + // profiled operand types. Without the fix, the receiver type would be mapped to the + // wrong stack slot, causing resolve_receiver_type to return NoProfile. + // With the fix, the receiver type is correctly resolved and the send gets past type + // resolution to hit the ARGS_BLOCKARG guard (ComplexArgPass) instead of NoProfile. + eval(" + def test(&block) = [].map(&block) + test { |x| x }; test { |x| x } + "); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :block@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :block@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + v14:ArrayExact = NewArray + v16:CPtr = GetEP 0 + v17:CInt64 = LoadField v16, :_env_data_index_flags@0x1001 + v18:CInt64 = GuardNoBitsSet v17, VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM=CUInt64(512) + v19:CInt64 = LoadField v16, :_env_data_index_specval@0x1002 + v20:CInt64 = GuardAnyBitSet v19, CUInt64(1) + v21:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) + v23:BasicObject = Send v14, &block, :map, v21 # SendFallbackReason: Complex argument passing + CheckInterrupts + Return v23 + "); + } + #[test] fn test_optimize_stringexact_eq_stringexact() { eval(r#" @@ -14925,7 +14962,9 @@ mod hir_opt_tests { #[test] fn test_recompile_no_profile_send() { - // Define a callee method and a test method that calls it + // Test the SideExit → recompile flow: a no-profile send becomes a SideExit, + // the exit profiles the send, triggers recompilation, and the new version + // optimizes it to SendDirect. eval(" def greet_recompile(x) = x.to_s def test_no_profile_recompile(flag) @@ -14942,8 +14981,12 @@ mod hir_opt_tests { // 2nd call compiles (greet has no profile data -> SideExit recompile) eval("test_no_profile_recompile(false); test_no_profile_recompile(false)"); - // The first compilation should have SideExit NoProfileSend recompile - // for the greet_recompile(42) callsite since it was never profiled. + // Now call with flag=true. This hits the SideExit, which profiles + // the send and invalidates the ISEQ for recompilation. + eval("test_no_profile_recompile(true)"); + + // After profiling via the side exit, rebuilding HIR should now + // have a SendDirect for greet_recompile instead of SideExit. assert_snapshot!(hir_string("test_no_profile_recompile"), @r" fn test_no_profile_recompile@:4: bb1(): @@ -14964,22 +15007,46 @@ mod hir_opt_tests { IfFalse v16, bb4(v9, v17) v19:Truthy = RefineType v10, Truthy v23:Fixnum[42] = Const Value(42) - SideExit NoProfileSend recompile + PatchPoint MethodRedefined(Object@0x1008, greet_recompile@0x1010, cme:0x1018) + v43:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v9, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] + v44:BasicObject = SendDirect v43, 0x1040, :greet_recompile (0x1050), v23 + CheckInterrupts + Return v44 bb4(v30:BasicObject, v31:Falsy): - v35:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v35:StringExact[VALUE(0x1058)] = Const Value(VALUE(0x1058)) v36:StringExact = StringCopy v35 CheckInterrupts Return v36 "); + } - // Now call with flag=true. This hits the SideExit, which profiles - // the send and invalidates the ISEQ for recompilation. - eval("test_no_profile_recompile(true)"); + #[test] + fn test_no_profile_send_on_final_version() { + // On the final ISEQ version (MAX_ISEQ_VERSIONS reached), no-profile sends should + // remain as Send fallbacks instead of being converted to SideExits, since recompilation + // is no longer possible and SideExits would fire every time without benefit. + // + // Use call_threshold=3 to ensure the method is auto-compiled before hir_string() builds + // the HIR. The auto-compile creates version 1, and hir_string() creates version 2 + // (= MAX_ISEQ_VERSIONS), so this is the final version. + set_call_threshold(3); + eval(" + def greet_final(x) = x.to_s + def test_final_version(flag) + if flag + greet_final(42) + else + 'hello' + end + end + "); + // Call enough times to trigger auto-compilation. flag=false so greet_final is never + // reached and has no profile data. + eval("3.times { test_final_version(false) }"); - // After profiling via the side exit, rebuilding HIR should now - // have a SendDirect for greet_recompile instead of SideExit. - assert_snapshot!(hir_string("test_no_profile_recompile"), @r" - fn test_no_profile_recompile@:4: + // On the final version, greet_final should be a Send fallback, not a SideExit. + assert_snapshot!(hir_string("test_final_version"), @r" + fn test_final_version@:4: bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf @@ -14998,13 +15065,11 @@ mod hir_opt_tests { IfFalse v16, bb4(v9, v17) v19:Truthy = RefineType v10, Truthy v23:Fixnum[42] = Const Value(42) - PatchPoint MethodRedefined(Object@0x1008, greet_recompile@0x1010, cme:0x1018) - v43:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v9, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] - v44:BasicObject = SendDirect v43, 0x1040, :greet_recompile (0x1050), v23 + v25:BasicObject = Send v9, :greet_final, v23 # SendFallbackReason: SendWithoutBlock: no profile data available CheckInterrupts - Return v44 + Return v25 bb4(v30:BasicObject, v31:Falsy): - v35:StringExact[VALUE(0x1058)] = Const Value(VALUE(0x1058)) + v35:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) v36:StringExact = StringCopy v35 CheckInterrupts Return v36 diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index a26f699c5be74f..606df902f20183 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -2095,7 +2095,7 @@ pub mod hir_build_tests { v7:BasicObject = LoadArg :a@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - v16:BasicObject = Send v9, 0x1008, :foo, v10 # SendFallbackReason: Uncategorized(send) + v16:BasicObject = Send v9, &block, :foo, v10 # SendFallbackReason: Uncategorized(send) CheckInterrupts Return v16 "); @@ -3525,7 +3525,7 @@ pub mod hir_build_tests { v38:CInt64[0] = GuardBitEquals v37, CInt64(0) v39:NilClass = Const Value(nil) v41:NilClass = GuardType v20, NilClass - v43:BasicObject = Send v17, 0x1004, :foo, v18, v29, v41, v39 # SendFallbackReason: Uncategorized(send) + v43:BasicObject = Send v17, &block, :foo, v18, v29, v41, v39 # SendFallbackReason: Uncategorized(send) CheckInterrupts Return v43 "); @@ -3562,7 +3562,7 @@ pub mod hir_build_tests { v23:CInt64 = GuardAnyBitSet v22, CUInt64(1) v24:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) v26:HashExact = GuardType v12, HashExact - v28:BasicObject = Send v11, 0x1002, :foo, v26, v24 # SendFallbackReason: Uncategorized(send) + v28:BasicObject = Send v11, &block, :foo, v26, v24 # SendFallbackReason: Uncategorized(send) CheckInterrupts Return v28 "); @@ -3599,7 +3599,7 @@ pub mod hir_build_tests { v23:CInt64 = GuardAnyBitSet v22, CUInt64(1) v24:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) v26:HashExact = GuardType v12, HashExact - v28:BasicObject = Send v11, 0x1002, :foo, v26, v24 # SendFallbackReason: Uncategorized(send) + v28:BasicObject = Send v11, &block, :foo, v26, v24 # SendFallbackReason: Uncategorized(send) CheckInterrupts Return v28 "); diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index bd36464bb73a32..b2e67afb166b9c 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -156,6 +156,7 @@ make_counters! { default { compiled_iseq_count, failed_iseq_count, + skipped_native_stack_full, compile_time_ns, profile_time_ns,