diff --git a/gc/mmtk/src/heap/ruby_heap_trigger.rs b/gc/mmtk/src/heap/ruby_heap_trigger.rs index 63ee22b8bec13e..6188c7f0ced62a 100644 --- a/gc/mmtk/src/heap/ruby_heap_trigger.rs +++ b/gc/mmtk/src/heap/ruby_heap_trigger.rs @@ -103,3 +103,54 @@ impl RubyHeapTrigger { .expect("Attempt to use RUBY_HEAP_TRIGGER_CONFIG before it is initialized") } } + +#[cfg(test)] +mod tests { + use super::*; + + const MIN_HEAP_PAGES: usize = 256; + const MAX_HEAP_PAGES: usize = 4096; + + fn init_config() { + RUBY_HEAP_TRIGGER_CONFIG.get_or_init(|| RubyHeapTriggerConfig { + min_heap_pages: MIN_HEAP_PAGES, + max_heap_pages: MAX_HEAP_PAGES, + heap_pages_min_ratio: 0.2, + heap_pages_goal_ratio: 0.4, + heap_pages_max_ratio: 0.65, + }); + } + + fn trigger_with_target(target_heap_pages: usize) -> RubyHeapTrigger { + init_config(); + + RubyHeapTrigger { + target_heap_pages: AtomicUsize::new(target_heap_pages), + } + } + + #[test] + fn starts_at_the_min_heap_size() { + init_config(); + + let trigger = RubyHeapTrigger::default(); + + assert_eq!(trigger.get_current_heap_size_in_pages(), MIN_HEAP_PAGES); + } + + #[test] + fn reports_the_configured_max_heap_size() { + let trigger = trigger_with_target(MIN_HEAP_PAGES); + + assert_eq!(trigger.get_max_heap_size_in_pages(), MAX_HEAP_PAGES); + } + + #[test] + fn current_heap_size_tracks_the_target() { + let trigger = trigger_with_target(1024); + assert_eq!(trigger.get_current_heap_size_in_pages(), 1024); + + trigger.target_heap_pages.store(2048, Ordering::Relaxed); + assert_eq!(trigger.get_current_heap_size_in_pages(), 2048); + } +} diff --git a/gems/bundled_gems b/gems/bundled_gems index 3e4355a07d92e7..c10d746627ceb8 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 8.0.0 https://github.com/ruby/rdoc win32ole 1.9.3 https://github.com/ruby/win32ole -irb 1.18.0 https://github.com/ruby/irb +irb 1.18.0 https://github.com/ruby/irb ac42eaaa88e6399384c1a56cc948c4b2528a9cc2 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/lib/prism/lex_compat.rb b/lib/prism/lex_compat.rb index 4d92842bda7515..749f11173a42aa 100644 --- a/lib/prism/lex_compat.rb +++ b/lib/prism/lex_compat.rb @@ -78,6 +78,8 @@ def deconstruct_keys(keys) # :nodoc: BANG_EQUAL: :on_op, BANG_TILDE: :on_op, BRACE_LEFT: :on_lbrace, + BRACE_LEFT_ARGUMENT: :on_lbrace, + BRACE_LEFT_HASH: :on_lbrace, BRACE_RIGHT: :on_rbrace, BRACKET_LEFT: :on_lbracket, BRACKET_LEFT_ARRAY: :on_lbracket, @@ -191,6 +193,7 @@ def deconstruct_keys(keys) # :nodoc: NEWLINE: :on_nl, NUMBERED_REFERENCE: :on_backref, PARENTHESIS_LEFT: :on_lparen, + PARENTHESIS_LEFT_GROUPING: :on_lparen, PARENTHESIS_LEFT_PARENTHESES: :on_lparen, PARENTHESIS_RIGHT: :on_rparen, PERCENT: :on_op, @@ -233,6 +236,7 @@ def deconstruct_keys(keys) # :nodoc: USTAR: :on_op, USTAR_STAR: :on_op, WORDS_SEP: :on_words_sep, + XSTRING_BEGIN: :on_backtick, "__END__": :on___end__ }.freeze diff --git a/lib/prism/translation/parser/lexer.rb b/lib/prism/translation/parser/lexer.rb index 0b2f4b9da76cb1..c26f48bcfbfda7 100644 --- a/lib/prism/translation/parser/lexer.rb +++ b/lib/prism/translation/parser/lexer.rb @@ -28,11 +28,13 @@ class Lexer # :nodoc: AMPERSAND_DOT: :tANDDOT, AMPERSAND_EQUAL: :tOP_ASGN, BACK_REFERENCE: :tBACK_REF, - BACKTICK: :tXSTRING_BEG, + BACKTICK: :tBACK_REF2, BANG: :tBANG, BANG_EQUAL: :tNEQ, BANG_TILDE: :tNMATCH, BRACE_LEFT: :tLCURLY, + BRACE_LEFT_ARGUMENT: :tLBRACE_ARG, + BRACE_LEFT_HASH: :tLBRACE, BRACE_RIGHT: :tRCURLY, BRACKET_LEFT: :tLBRACK2, BRACKET_LEFT_ARRAY: :tLBRACK, @@ -141,6 +143,7 @@ class Lexer # :nodoc: NEWLINE: :tNL, NUMBERED_REFERENCE: :tNTH_REF, PARENTHESIS_LEFT: :tLPAREN2, + PARENTHESIS_LEFT_GROUPING: :tLPAREN, PARENTHESIS_LEFT_PARENTHESES: :tLPAREN_ARG, PARENTHESIS_RIGHT: :tRPAREN, PERCENT: :tPERCENT, @@ -180,32 +183,10 @@ class Lexer # :nodoc: UPLUS: :tUPLUS, USTAR: :tSTAR, USTAR_STAR: :tDSTAR, - WORDS_SEP: :tSPACE + WORDS_SEP: :tSPACE, + XSTRING_BEGIN: :tXSTRING_BEG } - # These constants represent flags in our lex state. We really, really - # don't want to be using them and we really, really don't want to be - # exposing them as part of our public API. Unfortunately, we don't have - # another way of matching the exact tokens that the parser gem expects - # without them. We should find another way to do this, but in the - # meantime we'll hide them from the documentation and mark them as - # private constants. - EXPR_BEG = 0x1 - EXPR_LABEL = 0x400 - - # The `PARENTHESIS_LEFT` token in Prism is classified as either - # `tLPAREN` or `tLPAREN2` in the Parser gem. The following token types - # are listed as those classified as `tLPAREN`. - LPAREN_CONVERSION_TOKEN_TYPES = Set.new([ - :kAND, :kBEGIN, :kBREAK, :kCASE, :kDO_COND, :kDO_LAMBDA, :kDO, :kELSE, - :kELSIF, :kENSURE, :kFOR, :kIF_MOD, :kIF, :kIN, :kNEXT, :kOR, - :kRESCUE_MOD, :kRESCUE, :kRETURN, :kTHEN, :kUNLESS_MOD, :kUNLESS, - :kUNTIL_MOD, :kUNTIL, :kWHEN, :kWHILE_MOD, :kWHILE, - :tAMPER, :tANDOP, :tBANG, :tCARET, :tCOMMA, :tDIVIDE, :tDOT2, :tDOT3, - :tEQL, :tLCURLY, :tLPAREN_ARG, :tLPAREN, :tLPAREN2, :tLSHFT, :tNL, - :tOP_ASGN, :tOROP, :tPIPE, :tSEMI, :tSTRING_DBEG, :tUMINUS, :tUPLUS - ]) - # Types of tokens that are allowed to continue a method call with comments in-between. # For these, the parser gem doesn't emit a newline token after the last comment. COMMENT_CONTINUATION_TYPES = Set.new([:COMMENT, :AMPERSAND_DOT, :DOT]) @@ -214,7 +195,7 @@ class Lexer # :nodoc: # Heredocs are complex and require us to keep track of a bit of info to refer to later HeredocData = Struct.new(:identifier, :common_whitespace, keyword_init: true) - private_constant :TYPES, :EXPR_BEG, :EXPR_LABEL, :LPAREN_CONVERSION_TOKEN_TYPES, :HeredocData + private_constant :TYPES, :HeredocData # The Parser::Source::Buffer that the tokens were lexed from. attr_reader :source_buffer @@ -253,7 +234,7 @@ def to_a comment_newline_location = nil while index < length - token, state = lexed[index] + token, _ = lexed[index] index += 1 next if TYPES_ALWAYS_SKIP.include?(token.type) @@ -324,10 +305,6 @@ def to_a value.chomp!(":") when :tLABEL_END value.chomp!(":") - when :tLCURLY - type = :tLBRACE if state == EXPR_BEG | EXPR_LABEL - when :tLPAREN2 - type = :tLPAREN if tokens.empty? || LPAREN_CONVERSION_TOKEN_TYPES.include?(tokens.dig(-1, 0)) when :tNTH_REF value = parse_integer(value.delete_prefix("$")) when :tOP_ASGN @@ -506,10 +483,6 @@ def to_a type = :tIDENTIFIER end when :tXSTRING_BEG - if (next_token = lexed[index]&.first) && !%i[STRING_CONTENT STRING_END EMBEXPR_BEGIN].include?(next_token.type) - # self.`() - type = :tBACK_REF2 - end quote_stack.push(value) when :tSYMBOLS_BEG, :tQSYMBOLS_BEG, :tWORDS_BEG, :tQWORDS_BEG if (next_token = lexed[index]&.first) && next_token.type == :WORDS_SEP diff --git a/prism/config.yml b/prism/config.yml index 21502ea8ca9e91..cc5eb7e099c228 100644 --- a/prism/config.yml +++ b/prism/config.yml @@ -377,7 +377,7 @@ tokens: - name: AMPERSAND_EQUAL comment: "&=" - name: BACKTICK - comment: "`" + comment: "` as a method name" - name: BACK_REFERENCE comment: "a back reference" - name: BANG @@ -388,6 +388,10 @@ tokens: comment: "!~" - name: BRACE_LEFT comment: "{" + - name: BRACE_LEFT_ARGUMENT + comment: "{ for a block following a parenthesized argument" + - name: BRACE_LEFT_HASH + comment: "{ for a hash literal" - name: BRACKET_LEFT comment: "[" - name: BRACKET_LEFT_ARRAY @@ -584,6 +588,8 @@ tokens: comment: "a numbered reference to a capture group in the previous regular expression match" - name: PARENTHESIS_LEFT comment: "(" + - name: PARENTHESIS_LEFT_GROUPING + comment: "( scanned at the beginning of an expression" - name: PARENTHESIS_LEFT_PARENTHESES comment: "( for a parentheses node" - name: PERCENT @@ -658,6 +664,8 @@ tokens: comment: "unary **" - name: WORDS_SEP comment: "a separator between words in a list" + - name: XSTRING_BEGIN + comment: "the beginning of an execution string" - name: __END__ comment: "marker for the point in the file at which the parser should stop" flags: diff --git a/prism/prism.c b/prism/prism.c index 87bb03738fdf95..bd16a3f2822db4 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -10493,9 +10493,15 @@ parser_lex(pm_parser_t *parser) { // ( case '(': { + /* A parenthesis scanned at the beginning of an expression + * groups the expression it wraps, while one scanned in + * argument position with a preceding space wraps a command + * argument. Everything else opens an argument list. */ pm_token_type_t type = PM_TOKEN_PARENTHESIS_LEFT; - if (space_seen && (lex_state_arg_p(parser) || parser->lex_state == (PM_LEX_STATE_END | PM_LEX_STATE_LABEL))) { + if (lex_state_beg_p(parser)) { + type = PM_TOKEN_PARENTHESIS_LEFT_GROUPING; + } else if (space_seen && (lex_state_arg_p(parser) || parser->lex_state == (PM_LEX_STATE_END | PM_LEX_STATE_LABEL))) { type = PM_TOKEN_PARENTHESIS_LEFT_PARENTHESES; } @@ -10554,24 +10560,28 @@ parser_lex(pm_parser_t *parser) { pm_token_type_t type = PM_TOKEN_BRACE_LEFT; if (parser->enclosure_nesting == parser->lambda_enclosure_nesting) { - // This { begins a lambda + /* This { begins a lambda */ parser->command_start = true; lex_state_set(parser, PM_LEX_STATE_BEG); type = PM_TOKEN_LAMBDA_BEGIN; } else if (lex_state_p(parser, PM_LEX_STATE_LABELED)) { - // This { begins a hash literal + /* This { begins a hash literal */ lex_state_set(parser, PM_LEX_STATE_BEG | PM_LEX_STATE_LABEL); + type = PM_TOKEN_BRACE_LEFT_HASH; } else if (lex_state_p(parser, PM_LEX_STATE_ARG_ANY | PM_LEX_STATE_END | PM_LEX_STATE_ENDFN)) { - // This { begins a block + /* This { begins a block */ parser->command_start = true; lex_state_set(parser, PM_LEX_STATE_BEG); } else if (lex_state_p(parser, PM_LEX_STATE_ENDARG)) { - // This { begins a block on a command + /* This { begins a block following a parenthesized + * command argument */ parser->command_start = true; lex_state_set(parser, PM_LEX_STATE_BEG); + type = PM_TOKEN_BRACE_LEFT_ARGUMENT; } else { - // This { begins a hash literal + /* This { begins a hash literal */ lex_state_set(parser, PM_LEX_STATE_BEG | PM_LEX_STATE_LABEL); + type = PM_TOKEN_BRACE_LEFT_HASH; } parser->enclosure_nesting++; @@ -10888,7 +10898,7 @@ parser_lex(pm_parser_t *parser) { } lex_mode_push_string(parser, true, false, '\0', '`'); - LEX(PM_TOKEN_BACKTICK); + LEX(PM_TOKEN_XSTRING_BEGIN); } // single-quoted string literal @@ -12751,6 +12761,14 @@ match4(const pm_parser_t *parser, pm_token_type_t type1, pm_token_type_t type2, return match1(parser, type1) || match1(parser, type2) || match1(parser, type3) || match1(parser, type4); } +/** + * Returns true if the current token is any of the five given types. + */ +static PRISM_INLINE bool +match5(const pm_parser_t *parser, pm_token_type_t type1, pm_token_type_t type2, pm_token_type_t type3, pm_token_type_t type4, pm_token_type_t type5) { + return match1(parser, type1) || match1(parser, type2) || match1(parser, type3) || match1(parser, type4) || match1(parser, type5); +} + /** * Returns true if the current token is any of the six given types. */ @@ -13571,7 +13589,7 @@ parse_targets(pm_parser_t *parser, pm_node_t *first_target, pm_binding_power_t b pm_node_t *splat = UP(pm_splat_node_create(parser, &star_operator, name)); pm_multi_target_node_targets_append(parser, result, splat); has_rest = true; - } else if (match1(parser, PM_TOKEN_PARENTHESIS_LEFT)) { + } else if (match1(parser, PM_TOKEN_PARENTHESIS_LEFT_GROUPING)) { context_push(parser, PM_CONTEXT_MULTI_TARGET); pm_node_t *target = parse_expression(parser, binding_power, PM_PARSE_ACCEPTS_DO_BLOCK, PM_ERR_EXPECT_EXPRESSION_AFTER_COMMA, (uint16_t) (depth + 1)); target = parse_target(parser, target, true, false); @@ -13782,7 +13800,7 @@ parse_assocs(pm_parser_t *parser, pm_static_literals_t *literals, pm_node_t *nod pm_token_t operator = parser->previous; pm_node_t *value = NULL; - if (match1(parser, PM_TOKEN_BRACE_LEFT)) { + if (match1(parser, PM_TOKEN_BRACE_LEFT_HASH)) { // If we're about to parse a nested hash that is being // pushed into this hash directly with **, then we want the // inner hash to share the static literals with the outer @@ -14335,7 +14353,7 @@ parse_arguments(pm_parser_t *parser, pm_arguments_t *arguments, bool accepts_for */ static pm_multi_target_node_t * parse_required_destructured_parameter(pm_parser_t *parser) { - expect1(parser, PM_TOKEN_PARENTHESIS_LEFT, PM_ERR_EXPECT_LPAREN_REQ_PARAMETER); + expect1(parser, PM_TOKEN_PARENTHESIS_LEFT_GROUPING, PM_ERR_EXPECT_LPAREN_REQ_PARAMETER); pm_multi_target_node_t *node = pm_multi_target_node_create(parser); pm_multi_target_node_opening_set(parser, node, &parser->previous); @@ -14354,7 +14372,7 @@ parse_required_destructured_parameter(pm_parser_t *parser) { break; } - if (match1(parser, PM_TOKEN_PARENTHESIS_LEFT)) { + if (match1(parser, PM_TOKEN_PARENTHESIS_LEFT_GROUPING)) { param = UP(parse_required_destructured_parameter(parser)); } else if (accept1(parser, PM_TOKEN_USTAR)) { pm_token_t star = parser->previous; @@ -14416,7 +14434,7 @@ static pm_parameters_order_t parameters_ordering[PM_TOKEN_MAXIMUM] = { [PM_TOKEN_AMPERSAND] = PM_PARAMETERS_ORDER_NOTHING_AFTER, [PM_TOKEN_UDOT_DOT_DOT] = PM_PARAMETERS_ORDER_NOTHING_AFTER, [PM_TOKEN_IDENTIFIER] = PM_PARAMETERS_ORDER_NAMED, - [PM_TOKEN_PARENTHESIS_LEFT] = PM_PARAMETERS_ORDER_NAMED, + [PM_TOKEN_PARENTHESIS_LEFT_GROUPING] = PM_PARAMETERS_ORDER_NAMED, [PM_TOKEN_EQUAL] = PM_PARAMETERS_ORDER_OPTIONAL, [PM_TOKEN_LABEL] = PM_PARAMETERS_ORDER_KEYWORDS, [PM_TOKEN_USTAR] = PM_PARAMETERS_ORDER_AFTER_OPTIONAL, @@ -14523,7 +14541,7 @@ parse_parameters( bool parsing = true; switch (parser->current.type) { - case PM_TOKEN_PARENTHESIS_LEFT: { + case PM_TOKEN_PARENTHESIS_LEFT_GROUPING: { update_parameter_state(parser, &parser->current, &order); pm_node_t *param = UP(parse_required_destructured_parameter(parser)); @@ -15400,7 +15418,7 @@ parse_block(pm_parser_t *parser, uint16_t depth) { * managed by the lexer. A `do`/`end` block is delimited by keywords, so we * push the frame here (covering the block parameters and body) and pop it * before consuming `end`, mirroring parse.y's `do_body` rule. */ - bool do_block = opening.type != PM_TOKEN_BRACE_LEFT; + bool do_block = opening.type != PM_TOKEN_BRACE_LEFT && opening.type != PM_TOKEN_BRACE_LEFT_ARGUMENT; if (do_block) pm_accepts_block_stack_push(parser, true); pm_parser_scope_push(parser, false); @@ -15425,7 +15443,7 @@ parse_block(pm_parser_t *parser, uint16_t depth) { accept1(parser, PM_TOKEN_NEWLINE); pm_node_t *statements = NULL; - if (opening.type == PM_TOKEN_BRACE_LEFT) { + if (!do_block) { if (!match1(parser, PM_TOKEN_BRACE_RIGHT)) { statements = UP(parse_statements(parser, PM_CONTEXT_BLOCK_BRACES, (uint16_t) (depth + 1))); } @@ -15522,7 +15540,7 @@ parse_arguments_list(pm_parser_t *parser, pm_arguments_t *arguments, bool full_a * it, so pop the delimiter frame, push the command-args frame, and then * restore the delimiter frame on top (the delimiter's closing token * will pop it back off during argument parsing). */ - bool lookahead_delimiter = match4(parser, PM_TOKEN_PARENTHESIS_LEFT, PM_TOKEN_PARENTHESIS_LEFT_PARENTHESES, PM_TOKEN_BRACKET_LEFT, PM_TOKEN_BRACKET_LEFT_ARRAY); + bool lookahead_delimiter = match5(parser, PM_TOKEN_PARENTHESIS_LEFT, PM_TOKEN_PARENTHESIS_LEFT_GROUPING, PM_TOKEN_PARENTHESIS_LEFT_PARENTHESES, PM_TOKEN_BRACKET_LEFT, PM_TOKEN_BRACKET_LEFT_ARRAY); if (lookahead_delimiter) pm_accepts_block_stack_pop(parser); pm_accepts_block_stack_push(parser, false); if (lookahead_delimiter) pm_accepts_block_stack_push(parser, true); @@ -15544,7 +15562,7 @@ parse_arguments_list(pm_parser_t *parser, pm_arguments_t *arguments, bool full_a * it, pop the command-args frame beneath it, and restore the block * frame so the block's `}` still pops it. This mirrors the `tLBRACE_ARG` * lookahead handling in parse.y's `command_args` rule. */ - bool lookahead_brace = match1(parser, PM_TOKEN_BRACE_LEFT); + bool lookahead_brace = match2(parser, PM_TOKEN_BRACE_LEFT, PM_TOKEN_BRACE_LEFT_ARGUMENT); if (lookahead_brace) pm_accepts_block_stack_pop(parser); pm_accepts_block_stack_pop(parser); if (lookahead_brace) pm_accepts_block_stack_push(parser, true); @@ -15556,7 +15574,7 @@ parse_arguments_list(pm_parser_t *parser, pm_arguments_t *arguments, bool full_a if (full_arguments) { pm_block_node_t *block = NULL; - if (accept1(parser, PM_TOKEN_BRACE_LEFT)) { + if (accept2(parser, PM_TOKEN_BRACE_LEFT, PM_TOKEN_BRACE_LEFT_ARGUMENT)) { found |= true; block = parse_block(parser, (uint16_t) (depth + 1)); pm_arguments_validate_block(parser, arguments, block); @@ -16018,7 +16036,7 @@ parse_conditional(pm_parser_t *parser, pm_context_t context, size_t opening_newl #define PM_CASE_PRIMITIVE PM_TOKEN_INTEGER: case PM_TOKEN_INTEGER_IMAGINARY: case PM_TOKEN_INTEGER_RATIONAL: \ case PM_TOKEN_INTEGER_RATIONAL_IMAGINARY: case PM_TOKEN_FLOAT: case PM_TOKEN_FLOAT_IMAGINARY: \ case PM_TOKEN_FLOAT_RATIONAL: case PM_TOKEN_FLOAT_RATIONAL_IMAGINARY: case PM_TOKEN_SYMBOL_BEGIN: \ - case PM_TOKEN_REGEXP_BEGIN: case PM_TOKEN_BACKTICK: case PM_TOKEN_PERCENT_LOWER_X: case PM_TOKEN_PERCENT_LOWER_I: \ + case PM_TOKEN_REGEXP_BEGIN: case PM_TOKEN_XSTRING_BEGIN: case PM_TOKEN_PERCENT_LOWER_X: case PM_TOKEN_PERCENT_LOWER_I: \ case PM_TOKEN_PERCENT_LOWER_W: case PM_TOKEN_PERCENT_UPPER_I: case PM_TOKEN_PERCENT_UPPER_W: \ case PM_TOKEN_STRING_BEGIN: case PM_TOKEN_KEYWORD_NIL: case PM_TOKEN_KEYWORD_SELF: case PM_TOKEN_KEYWORD_TRUE: \ case PM_TOKEN_KEYWORD_FALSE: case PM_TOKEN_KEYWORD___FILE__: case PM_TOKEN_KEYWORD___LINE__: \ @@ -17364,7 +17382,7 @@ parse_pattern_primitive(pm_parser_t *parser, pm_constant_id_list_t *captures, pm pm_array_pattern_node_requireds_append(parser->arena, node, inner); return UP(node); } - case PM_TOKEN_BRACE_LEFT: { + case PM_TOKEN_BRACE_LEFT_HASH: { bool previous_pattern_matching_newlines = parser->pattern_matching_newlines; parser->pattern_matching_newlines = false; @@ -17501,7 +17519,7 @@ parse_pattern_primitive(pm_parser_t *parser, pm_constant_id_list_t *captures, pm return UP(pm_pinned_variable_node_create(parser, &operator, variable)); } - case PM_TOKEN_PARENTHESIS_LEFT: { + case PM_TOKEN_PARENTHESIS_LEFT_GROUPING: { bool previous_pattern_matching_newlines = parser->pattern_matching_newlines; parser->pattern_matching_newlines = false; @@ -17585,7 +17603,7 @@ parse_pattern_primitives(pm_parser_t *parser, pm_constant_id_list_t *captures, p switch (parser->current.type) { case PM_TOKEN_IDENTIFIER: case PM_TOKEN_BRACKET_LEFT_ARRAY: - case PM_TOKEN_BRACE_LEFT: + case PM_TOKEN_BRACE_LEFT_HASH: case PM_TOKEN_CARET: case PM_TOKEN_CONSTANT: case PM_TOKEN_UCOLON_COLON: @@ -17604,7 +17622,7 @@ parse_pattern_primitives(pm_parser_t *parser, pm_constant_id_list_t *captures, p break; } - case PM_TOKEN_PARENTHESIS_LEFT: + case PM_TOKEN_PARENTHESIS_LEFT_GROUPING: case PM_TOKEN_PARENTHESIS_LEFT_PARENTHESES: { pm_token_t operator = parser->previous; pm_token_t opening = parser->current; @@ -19186,6 +19204,13 @@ parse_parentheses(pm_parser_t *parser, pm_binding_power_t binding_power, uint8_t /* If this is the end of the file or we match a right parenthesis, then we * have an empty parentheses node, and we can immediately return. */ if (match2(parser, PM_TOKEN_PARENTHESIS_RIGHT, PM_TOKEN_EOF)) { + /* A command argument group sets EXPR_ENDARG before its ')' is + * consumed, even when the group is empty, so that a following '{' is + * scanned as a block brace. */ + if (match1(parser, PM_TOKEN_PARENTHESIS_RIGHT) && opening.type == PM_TOKEN_PARENTHESIS_LEFT_PARENTHESES) { + lex_state_set(parser, PM_LEX_STATE_ENDARG); + } + expect1(parser, PM_TOKEN_PARENTHESIS_RIGHT, PM_ERR_EXPECT_RPAREN); pop_block_exits(parser, previous_block_exits); return UP(pm_parentheses_node_create(parser, &opening, NULL, &parser->previous, paren_flags)); @@ -19506,10 +19531,10 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u return UP(array); } - case PM_TOKEN_PARENTHESIS_LEFT: + case PM_TOKEN_PARENTHESIS_LEFT_GROUPING: case PM_TOKEN_PARENTHESIS_LEFT_PARENTHESES: return parse_parentheses(parser, binding_power, flags, depth); - case PM_TOKEN_BRACE_LEFT: { + case PM_TOKEN_BRACE_LEFT_HASH: { // If we were passed a current_hash_keys via the parser, then that // means we're already parsing a hash and we want to share the set // of hash keys with this inner hash we're about to parse for the @@ -20129,7 +20154,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u context_push(parser, PM_CONTEXT_DEFINED); bool newline = accept1(parser, PM_TOKEN_NEWLINE); - if (accept1(parser, PM_TOKEN_PARENTHESIS_LEFT)) { + if (accept2(parser, PM_TOKEN_PARENTHESIS_LEFT, PM_TOKEN_PARENTHESIS_LEFT_GROUPING)) { lparen = parser->previous; if (newline && accept1(parser, PM_TOKEN_PARENTHESIS_RIGHT)) { @@ -20298,7 +20323,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u accept1(parser, PM_TOKEN_NEWLINE); - if (accept1(parser, PM_TOKEN_PARENTHESIS_LEFT)) { + if (accept2(parser, PM_TOKEN_PARENTHESIS_LEFT, PM_TOKEN_PARENTHESIS_LEFT_GROUPING)) { pm_token_t lparen = parser->previous; if (accept1(parser, PM_TOKEN_PARENTHESIS_RIGHT)) { @@ -20619,7 +20644,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u pm_interpolated_regular_expression_node_closing_set(parser, interpolated, &closing); return UP(interpolated); } - case PM_TOKEN_BACKTICK: + case PM_TOKEN_XSTRING_BEGIN: case PM_TOKEN_PERCENT_LOWER_X: { parser_lex(parser); pm_token_t opening = parser->previous; diff --git a/prism/templates/src/tokens.c.erb b/prism/templates/src/tokens.c.erb index 472c82ea690939..fb71afe217f687 100644 --- a/prism/templates/src/tokens.c.erb +++ b/prism/templates/src/tokens.c.erb @@ -53,6 +53,10 @@ pm_token_str(pm_token_type_t token_type) { return "'!~'"; case PM_TOKEN_BRACE_LEFT: return "'{'"; + case PM_TOKEN_BRACE_LEFT_ARGUMENT: + return "'{'"; + case PM_TOKEN_BRACE_LEFT_HASH: + return "'{'"; case PM_TOKEN_BRACE_RIGHT: return "'}'"; case PM_TOKEN_BRACKET_LEFT: @@ -275,6 +279,8 @@ pm_token_str(pm_token_type_t token_type) { return "numbered reference"; case PM_TOKEN_PARENTHESIS_LEFT: return "'('"; + case PM_TOKEN_PARENTHESIS_LEFT_GROUPING: + return "'('"; case PM_TOKEN_PARENTHESIS_LEFT_PARENTHESES: return "'('"; case PM_TOKEN_PARENTHESIS_RIGHT: @@ -355,6 +361,8 @@ pm_token_str(pm_token_type_t token_type) { return "**"; case PM_TOKEN_WORDS_SEP: return "string separator"; + case PM_TOKEN_XSTRING_BEGIN: + return "backtick string literal"; case PM_TOKEN___END__: return "'__END__'"; case PM_TOKEN_MAXIMUM: diff --git a/spec/bundler/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb b/spec/bundler/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb index 91f02005586fc3..2a287af19587b6 100644 --- a/spec/bundler/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb +++ b/spec/bundler/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "bundler/fetcher" +require Spec::Path.rubygems_test_dir.join("pem_utilities") require Spec::Path.rubygems_test_dir.join("local_ssl_server_utilities") RSpec.describe "Bundler::Fetcher local SSL server", if: Gem::HAVE_OPENSSL do @@ -19,7 +20,7 @@ it "connects" do ssl_server = start_ssl_server allow(Bundler.settings).to receive(:[]).and_call_original - allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(File.join(certs_dir, "ca_cert.pem")) + allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(Gem::PemUtilities::CA_CERT_FILE) response = fetch_path("https://localhost:#{ssl_server.addr[1]}/yaml") expect(response.code).to eq("200") end @@ -29,8 +30,8 @@ verify_mode: OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT ) allow(Bundler.settings).to receive(:[]).and_call_original - allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(File.join(certs_dir, "ca_cert.pem")) - allow(Bundler.settings).to receive(:[]).with(:ssl_client_cert).and_return(File.join(certs_dir, "client.pem")) + allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(Gem::PemUtilities::CA_CERT_FILE) + allow(Bundler.settings).to receive(:[]).with(:ssl_client_cert).and_return(Gem::PemUtilities::CLIENT_FILE) response = fetch_path("https://localhost:#{ssl_server.addr[1]}/yaml") expect(response.code).to eq("200") end @@ -44,7 +45,7 @@ it "connects" do ssl_server = start_ssl_server(mode: :pqc) allow(Bundler.settings).to receive(:[]).and_call_original - allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(File.join(certs_dir, "mldsa65_ca_cert.pem")) + allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(Gem::PemUtilities::MLDSA65_CA_CERT_FILE) response = fetch_path("https://localhost:#{ssl_server.addr[1]}/yaml") expect(response.code).to eq("200") end @@ -55,8 +56,8 @@ verify_mode: OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT ) allow(Bundler.settings).to receive(:[]).and_call_original - allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(File.join(certs_dir, "mldsa65_ca_cert.pem")) - allow(Bundler.settings).to receive(:[]).with(:ssl_client_cert).and_return(File.join(certs_dir, "mldsa65_client.pem")) + allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(Gem::PemUtilities::MLDSA65_CA_CERT_FILE) + allow(Bundler.settings).to receive(:[]).with(:ssl_client_cert).and_return(Gem::PemUtilities::MLDSA65_CLIENT_FILE) response = fetch_path("https://localhost:#{ssl_server.addr[1]}/yaml") expect(response.code).to eq("200") end diff --git a/test/prism/errors/command_calls_25.txt b/test/prism/errors/command_calls_25.txt index cf04508f87d8a7..c8769c538ab75f 100644 --- a/test/prism/errors/command_calls_25.txt +++ b/test/prism/errors/command_calls_25.txt @@ -3,6 +3,7 @@ ^ expected a `do` keyword or a `{` to open the lambda block ^ unexpected ')', expecting end-of-input ^ unexpected ')', ignoring it - ^ unexpected end-of-input, assuming it is closing the parent top level context + ^ unexpected '{', ignoring it + ^ unexpected '}', ignoring it ^~ expected a lambda block beginning with `do` to end with `end` diff --git a/test/prism/ruby/parser_test.rb b/test/prism/ruby/parser_test.rb index 856ecedc1de39d..e44bc20d4dea0b 100644 --- a/test/prism/ruby/parser_test.rb +++ b/test/prism/ruby/parser_test.rb @@ -111,12 +111,8 @@ class ParserTest < TestCase skip_tokens = [ "dash_heredocs.txt", "embdoc_no_newline_at_end.txt", - "methods.txt", - "seattlerb/bug169.txt", "seattlerb/case_in.txt", "seattlerb/difficult4__leading_dots2.txt", - "seattlerb/difficult6__7.txt", - "seattlerb/difficult6__8.txt", "seattlerb/heredoc_unicode.txt", "seattlerb/parse_line_heredoc.txt", "seattlerb/pct_w_heredoc_interp_nested.txt", @@ -129,14 +125,10 @@ class ParserTest < TestCase "whitequark/beginless_irange_after_newline.txt", "whitequark/forward_arg_with_open_args.txt", "whitequark/kwarg_no_paren.txt", - "whitequark/lbrace_arg_after_command_args.txt", "whitequark/multiple_pattern_matches.txt", "whitequark/newline_in_hash_argument.txt", "whitequark/pattern_matching_hash.txt", - "whitequark/ruby_bug_14690.txt", - "whitequark/ruby_bug_9669.txt", - "whitequark/space_args_arg_block.txt", - "whitequark/space_args_block.txt" + "whitequark/ruby_bug_9669.txt" ] Fixture.each_for_version(except: skip_syntax_error, version: "3.3") do |fixture| diff --git a/test/rubygems/private_ec_key.pem b/test/rubygems/ec_private_key.pem similarity index 100% rename from test/rubygems/private_ec_key.pem rename to test/rubygems/ec_private_key.pem diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index 1a4f49ad64170e..174bd258168b0d 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -46,6 +46,7 @@ require "rubygems/vendor/uri/lib/uri" require "zlib" require_relative "mock_gem_ui" +require_relative "pem_utilities" # JRuby on Windows raises TypeError inside File.symlink (the wincode helper # trips on a nil path), so any test that exercises Gem::Installer's symlink @@ -1658,75 +1659,7 @@ def prefetch(reqs) # :nodoc: end end - ## - # Loads certificate named +cert_name+ from test/rubygems/. - - def self.load_cert(cert_name) - cert_file = cert_path cert_name - - cert = File.read cert_file - - OpenSSL::X509::Certificate.new cert - end - - ## - # Returns the path to the certificate named +cert_name+ from - # test/rubygems/. - - def self.cert_path(cert_name) - if begin - Time.at(2**32) - rescue StandardError - 32 - end == 32 - cert_file = "#{__dir__}/#{cert_name}_cert_32.pem" - - return cert_file if File.exist? cert_file - end - - "#{__dir__}/#{cert_name}_cert.pem" - end - - ## - # Loads a private key named +key_name+ with +passphrase+ in test/rubygems/ - - def self.load_key(key_name, passphrase = nil) - key_file = key_path key_name - - key = File.read key_file - - OpenSSL::PKey.read key, passphrase - end - - ## - # Returns the path to the key named +key_name+ from test/rubygems - - def self.key_path(key_name) - "#{__dir__}/#{key_name}_key.pem" - end - - # :stopdoc: - # only available in RubyGems tests - - PRIVATE_KEY_PASSPHRASE = "Foo bar" - - begin - PRIVATE_KEY = load_key "private" - PRIVATE_KEY_PATH = key_path "private" - - # ENCRYPTED_PRIVATE_KEY is PRIVATE_KEY encrypted with PRIVATE_KEY_PASSPHRASE - ENCRYPTED_PRIVATE_KEY = load_key "encrypted_private", PRIVATE_KEY_PASSPHRASE - ENCRYPTED_PRIVATE_KEY_PATH = key_path "encrypted_private" - - PUBLIC_KEY = PRIVATE_KEY.public_key - - PUBLIC_CERT = load_cert "public" - PUBLIC_CERT_PATH = cert_path "public" - rescue Errno::ENOENT - PRIVATE_KEY = nil - PUBLIC_KEY = nil - PUBLIC_CERT = nil - end if Gem::HAVE_OPENSSL + include Gem::PemUtilities end # https://github.com/seattlerb/minitest/blob/13c48a03d84a2a87855a4de0c959f96800100357/lib/minitest/mock.rb#L192 diff --git a/test/rubygems/local_ssl_server_utilities.rb b/test/rubygems/local_ssl_server_utilities.rb index a068efa9642d96..d0d73dbd1219bf 100644 --- a/test/rubygems/local_ssl_server_utilities.rb +++ b/test/rubygems/local_ssl_server_utilities.rb @@ -5,14 +5,10 @@ require "socket" require "openssl" +require_relative "pem_utilities" module Gem::LocalSSLServerUtilities - CERTS_DIR = __dir__ - - def certs_dir - CERTS_DIR - end - + include Gem::PemUtilities def initialize_ssl_server @ssl_server_thread = nil @ssl_server = nil @@ -40,13 +36,13 @@ def start_ssl_server(config = {}) case mode when :non_pqc - ctx.cert = cert("ssl_cert.pem") - ctx.key = key("ssl_key.pem") - ctx.ca_file = File.join(certs_dir, "ca_cert.pem") + ctx.cert = SSL_CERT + ctx.key = SSL_KEY + ctx.ca_file = CA_CERT_FILE when :pqc - ctx.cert = cert("mldsa65_ssl_cert.pem") - ctx.key = key("mldsa65_ssl_key.pem") - ctx.ca_file = File.join(certs_dir, "mldsa65_ca_cert.pem") + ctx.cert = MLDSA65_SSL_CERT + ctx.key = MLDSA65_SSL_KEY + ctx.ca_file = MLDSA65_CA_CERT_FILE ctx.groups = "X25519MLKEM768" end @@ -79,14 +75,6 @@ def handle_request(client) end end - def cert(filename) - OpenSSL::X509::Certificate.new(File.read(File.join(certs_dir, filename))) - end - - def key(filename) - OpenSSL::PKey.read(File.read(File.join(certs_dir, filename))) - end - def without_pqc_support(&block) # PQC algorithms ML-KEM and ML-DSA require OpenSSL >= 3.5. # https://openssl-library.org/post/2025-04-08-openssl-35-final-release/ @@ -123,8 +111,8 @@ def self.support_pqc_handshake? def self.probe_pqc_handshake server = TCPServer.new("127.0.0.1", 0) ctx = OpenSSL::SSL::SSLContext.new - ctx.cert = OpenSSL::X509::Certificate.new(File.read(File.join(CERTS_DIR, "mldsa65_ssl_cert.pem"))) - ctx.key = OpenSSL::PKey.read(File.read(File.join(CERTS_DIR, "mldsa65_ssl_key.pem"))) + ctx.cert = Gem::PemUtilities::MLDSA65_SSL_CERT + ctx.key = Gem::PemUtilities::MLDSA65_SSL_KEY ctx.groups = "X25519MLKEM768" ssl_server = OpenSSL::SSL::SSLServer.new(server, ctx) diff --git a/test/rubygems/pem_utilities.rb b/test/rubygems/pem_utilities.rb new file mode 100644 index 00000000000000..bec08d9a38dd4f --- /dev/null +++ b/test/rubygems/pem_utilities.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true + +# This file can be loaded by RubyGems test-unit files and Bundler rspec files. +# Don't add test-unit or rspec dependent logic in this file. + +require "rubygems/openssl" + +module Gem::PemUtilities + ## + # Loads certificate named +cert_name+ from test/rubygems/. + + def self.load_cert(cert_name) + cert = File.read(cert_file(cert_name)) + OpenSSL::X509::Certificate.new cert + end + + ## + # Returns the file path to the certificate named +cert_name+ from + # test/rubygems/. + + def self.cert_file(cert_name) + if begin + Time.at(2**32) + rescue StandardError + 32 + end == 32 + cert_file = "#{__dir__}/#{cert_name}_cert_32.pem" + + return cert_file if File.exist? cert_file + end + + "#{__dir__}/#{cert_name}_cert.pem" + end + + ## + # Loads a private key named +key_name+ with +passphrase+ in test/rubygems/ + + def self.load_key(key_name, passphrase = nil) + key = File.read(key_file(key_name)) + + # Rescue if unsupported key algorithm's file is read with old OpenSSL versions. + begin + OpenSSL::PKey.read key, passphrase + rescue OpenSSL::PKey::PKeyError + nil + end + end + + ## + # Returns the file path to the key named +key_name+ from test/rubygems + + def self.key_file(key_name) + "#{__dir__}/#{key_name}_key.pem" + end + + ## + # Returns the file path to the PEM file named +pem_name+ from test/rubygems + + def self.pem_file(pem_name) + "#{__dir__}/#{pem_name}.pem" + end + + # :stopdoc: + + PRIVATE_KEY_PASSPHRASE = "Foo bar" + + if Gem::HAVE_OPENSSL + # Only the key and certificate constants used in tests are managed here. Add + # constants here when adding or using new .pem files. The constant naming + # convention is _. + + # Keys and certificates mostly generated by create_certs.sh + # RSA CA + CA_CERT = load_cert "ca" + CA_CERT_FILE = cert_file "ca" + # RSA server + SSL_KEY = load_key "ssl" + SSL_KEY_FILE = key_file "ssl" + SSL_CERT = load_cert "ssl" + SSL_CERT_FILE = cert_file "ssl" + # RSA client key/cert pair + CLIENT_FILE = pem_file "client" + # RSA invalid client manually created without script + INVALID_CLIENT_FILE = pem_file "invalid_client" + # ML-DSA-65 CA + MLDSA65_CA_CERT_FILE = cert_file "mldsa65_ca" + # ML-DSA-65 server + MLDSA65_SSL_KEY = load_key "mldsa65_ssl" + MLDSA65_SSL_KEY_FILE = key_file "mldsa65_ssl" + MLDSA65_SSL_CERT = load_cert "mldsa65_ssl" + MLDSA65_SSL_CERT_FILE = cert_file "mldsa65_ssl" + # ML-DSA-65 client key/cert pair + MLDSA65_CLIENT_FILE = pem_file "mldsa65_client" + + # Keys and certificates generated by create_certs.rb + PRIVATE_KEY = load_key "private" + PRIVATE_KEY_FILE = key_file "private" + # ENCRYPTED_PRIVATE_KEY is PRIVATE_KEY encrypted with PRIVATE_KEY_PASSPHRASE + ENCRYPTED_PRIVATE_KEY = load_key "encrypted_private", PRIVATE_KEY_PASSPHRASE + ENCRYPTED_PRIVATE_KEY_FILE = key_file "encrypted_private" + PUBLIC_KEY = PRIVATE_KEY.public_key + PUBLIC_KEY_FILE = key_file "public" + PUBLIC_CERT = load_cert "public" + PUBLIC_CERT_FILE = cert_file "public" + ALTERNATE_KEY = load_key "alternate" + ALTERNATE_KEY_FILE = key_file "alternate" + ALTERNATE_CERT = load_cert "alternate" + ALTERNATE_CERT_FILE = cert_file "alternate" + CHILD_KEY = load_key "child" + CHILD_CERT = load_cert "child" + CHILD_CERT_FILE = cert_file "child" + GRANDCHILD_CERT = load_cert "grandchild" + INVALID_ISSUER_CERT = load_cert "invalid_issuer" + INVALID_SIGNER_CERT = load_cert "invalid_signer" + INVALIDCHILD_CERT = load_cert "invalidchild" + EXPIRED_CERT = load_cert "expired" + EXPIRED_CERT_FILE = cert_file "expired" + FUTURE_CERT = load_cert "future" + WRONG_KEY_CERT = load_cert "wrong_key" + + # Keys and certificates manually created without script + # RSA 3072 bits + RSA3072_PRIVATE_KEY_FILE = key_file "rsa3072_private" + RSA3072_PUBLIC_CERT = load_cert "rsa3072_public" + RSA3072_PUBLIC_CERT_FILE = cert_file "rsa3072_public" + # EC + EC_PRIVATE_KEY = load_key "ec_private", PRIVATE_KEY_PASSPHRASE + EC_PRIVATE_KEY_FILE = key_file "ec_private" + end +end diff --git a/test/rubygems/private3072_key.pem b/test/rubygems/rsa3072_private_key.pem similarity index 100% rename from test/rubygems/private3072_key.pem rename to test/rubygems/rsa3072_private_key.pem diff --git a/test/rubygems/public3072_cert.pem b/test/rubygems/rsa3072_public_cert.pem similarity index 100% rename from test/rubygems/public3072_cert.pem rename to test/rubygems/rsa3072_public_cert.pem diff --git a/test/rubygems/test_gem_commands_build_command.rb b/test/rubygems/test_gem_commands_build_command.rb index 9339f41f7cb875..cd88421c0754ff 100644 --- a/test/rubygems/test_gem_commands_build_command.rb +++ b/test/rubygems/test_gem_commands_build_command.rb @@ -5,12 +5,6 @@ require "rubygems/package" class TestGemCommandsBuildCommand < Gem::TestCase - CERT_FILE = cert_path "public3072" - SIGNING_KEY = key_path "private3072" - - EXPIRED_CERT_FILE = cert_path "expired" - PRIVATE_KEY_FILE = key_path "private" - def setup super @@ -591,8 +585,8 @@ def test_build_signed_gem trust_dir = Gem::Security.trust_dir spec = util_spec "some_gem" do |s| - s.signing_key = SIGNING_KEY - s.cert_chain = [CERT_FILE] + s.signing_key = RSA3072_PRIVATE_KEY_FILE + s.cert_chain = [RSA3072_PUBLIC_CERT_FILE] end gemspec_file = File.join(@tempdir, spec.spec_name) @@ -605,7 +599,7 @@ def test_build_signed_gem util_test_build_gem spec - trust_dir.trust_cert OpenSSL::X509::Certificate.new(File.read(CERT_FILE)) + trust_dir.trust_cert RSA3072_PUBLIC_CERT gem = Gem::Package.new(File.join(@tempdir, spec.file_name), Gem::Security::HighSecurity) diff --git a/test/rubygems/test_gem_commands_cert_command.rb b/test/rubygems/test_gem_commands_cert_command.rb index b9207cdcbcd39a..17fe3d789a6f8c 100644 --- a/test/rubygems/test_gem_commands_cert_command.rb +++ b/test/rubygems/test_gem_commands_cert_command.rb @@ -12,19 +12,6 @@ end class TestGemCommandsCertCommand < Gem::TestCase - ALTERNATE_CERT = load_cert "alternate" - EXPIRED_PUBLIC_CERT = load_cert "expired" - - ALTERNATE_KEY_FILE = key_path "alternate" - PRIVATE_KEY_FILE = key_path "private" - PRIVATE_EC_KEY_FILE = key_path "private_ec" - PUBLIC_KEY_FILE = key_path "public" - - ALTERNATE_CERT_FILE = cert_path "alternate" - CHILD_CERT_FILE = cert_path "child" - PUBLIC_CERT_FILE = cert_path "public" - EXPIRED_PUBLIC_CERT_FILE = cert_path "expired" - def setup super @@ -84,8 +71,6 @@ def test_execute_add end def test_execute_add_twice - self.class.cert_path "alternate" - @cmd.handle_options %W[ --add #{PUBLIC_CERT_FILE} --add #{ALTERNATE_CERT_FILE} @@ -289,7 +274,7 @@ def test_execute_build_key def test_execute_build_encrypted_key @cmd.handle_options %W[ --build nobody@example.com - --private-key #{ENCRYPTED_PRIVATE_KEY_PATH} + --private-key #{ENCRYPTED_PRIVATE_KEY_FILE} ] use_ui @ui do @@ -310,7 +295,7 @@ def test_execute_build_encrypted_key def test_execute_build_ec_key @cmd.handle_options %W[ --build nobody@example.com - --private-key #{PRIVATE_EC_KEY_FILE} + --private-key #{EC_PRIVATE_KEY_FILE} ] use_ui @ui do @@ -397,7 +382,7 @@ def test_execute_private_key def test_execute_encrypted_private_key use_ui @ui do - @cmd.send :handle_options, %W[--private-key #{ENCRYPTED_PRIVATE_KEY_PATH}] + @cmd.send :handle_options, %W[--private-key #{ENCRYPTED_PRIVATE_KEY_FILE}] end assert_equal "", @ui.output @@ -517,7 +502,7 @@ def test_execute_sign_encrypted_key assert_equal "/CN=alternate/DC=example", ALTERNATE_CERT.issuer.to_s @cmd.handle_options %W[ - --private-key #{ENCRYPTED_PRIVATE_KEY_PATH} + --private-key #{ENCRYPTED_PRIVATE_KEY_FILE} --certificate #{PUBLIC_CERT_FILE} --sign #{path} @@ -684,12 +669,12 @@ def test_execute_re_sign Dir.mkdir gem_path path = File.join @tempdir, "cert.pem" - Gem::Security.write_certificate EXPIRED_PUBLIC_CERT, path, 0o600 + Gem::Security.write_certificate EXPIRED_CERT, path, 0o600 - assert_equal "/CN=nobody/DC=example", EXPIRED_PUBLIC_CERT.issuer.to_s + assert_equal "/CN=nobody/DC=example", EXPIRED_CERT.issuer.to_s - tmp_expired_cert_file = File.join(@tempdir, File.basename(EXPIRED_PUBLIC_CERT_FILE)) - File.write(tmp_expired_cert_file, File.read(EXPIRED_PUBLIC_CERT_FILE)) + tmp_expired_cert_file = File.join(@tempdir, File.basename(EXPIRED_CERT_FILE)) + File.write(tmp_expired_cert_file, File.read(EXPIRED_CERT_FILE)) @cmd.handle_options %W[ --private-key #{PRIVATE_KEY_FILE} @@ -716,12 +701,12 @@ def test_execute_re_sign_with_cert_expiration_length_days Dir.mkdir gem_path path = File.join @tempdir, "cert.pem" - Gem::Security.write_certificate EXPIRED_PUBLIC_CERT, path, 0o600 + Gem::Security.write_certificate EXPIRED_CERT, path, 0o600 - assert_equal "/CN=nobody/DC=example", EXPIRED_PUBLIC_CERT.issuer.to_s + assert_equal "/CN=nobody/DC=example", EXPIRED_CERT.issuer.to_s - tmp_expired_cert_file = File.join(@tempdir, File.basename(EXPIRED_PUBLIC_CERT_FILE)) - File.write(tmp_expired_cert_file, File.read(EXPIRED_PUBLIC_CERT_FILE)) + tmp_expired_cert_file = File.join(@tempdir, File.basename(EXPIRED_CERT_FILE)) + File.write(tmp_expired_cert_file, File.read(EXPIRED_CERT_FILE)) @cmd.handle_options %W[ --private-key #{PRIVATE_KEY_FILE} @@ -851,7 +836,7 @@ def test_handle_options_sign def test_handle_options_sign_encrypted_key @cmd.handle_options %W[ --private-key #{ALTERNATE_KEY_FILE} - --private-key #{ENCRYPTED_PRIVATE_KEY_PATH} + --private-key #{ENCRYPTED_PRIVATE_KEY_FILE} --certificate #{ALTERNATE_CERT_FILE} --certificate #{PUBLIC_CERT_FILE} diff --git a/test/rubygems/test_gem_package.rb b/test/rubygems/test_gem_package.rb index 99763f4df19ec2..7b8ac4736d2f46 100644 --- a/test/rubygems/test_gem_package.rb +++ b/test/rubygems/test_gem_package.rb @@ -275,7 +275,7 @@ def test_build_auto_signed Gem::Security.write_private_key PRIVATE_KEY, private_key_path public_cert_path = File.join Gem.user_home, ".gem", "gem-public_cert.pem" - FileUtils.cp PUBLIC_CERT_PATH, public_cert_path + FileUtils.cp PUBLIC_CERT_FILE, public_cert_path spec = Gem::Specification.new "build", "1" spec.summary = "build" @@ -315,7 +315,7 @@ def test_build_auto_signed_encrypted_key FileUtils.mkdir_p File.join(Gem.user_home, ".gem") private_key_path = File.join Gem.user_home, ".gem", "gem-private_key.pem" - FileUtils.cp ENCRYPTED_PRIVATE_KEY_PATH, private_key_path + FileUtils.cp ENCRYPTED_PRIVATE_KEY_FILE, private_key_path public_cert_path = File.join Gem.user_home, ".gem", "gem-public_cert.pem" Gem::Security.write_certificate PUBLIC_CERT, public_cert_path diff --git a/test/rubygems/test_gem_remote_fetcher_local_ssl_server.rb b/test/rubygems/test_gem_remote_fetcher_local_ssl_server.rb index 95e6d3ac65a80a..813e8e2f026567 100644 --- a/test/rubygems/test_gem_remote_fetcher_local_ssl_server.rb +++ b/test/rubygems/test_gem_remote_fetcher_local_ssl_server.rb @@ -26,7 +26,7 @@ def teardown def test_ssl_connection ssl_server = start_ssl_server - temp_ca_cert = File.join(certs_dir, "ca_cert.pem") + temp_ca_cert = CA_CERT_FILE with_configured_fetcher(":ssl_ca_cert: #{temp_ca_cert}") do |fetcher| fetcher.fetch_path("https://localhost:#{ssl_server.addr[1]}/yaml") end @@ -36,7 +36,7 @@ def test_pqc_ssl_connection omit_unless_support_pqc ssl_server = start_ssl_server(mode: :pqc) - temp_ca_cert = File.join(certs_dir, "mldsa65_ca_cert.pem") + temp_ca_cert = MLDSA65_CA_CERT_FILE with_configured_fetcher(":ssl_ca_cert: #{temp_ca_cert}") do |fetcher| fetcher.fetch_path("https://localhost:#{ssl_server.addr[1]}/yaml") end @@ -47,8 +47,8 @@ def test_ssl_client_cert_auth_connection { verify_mode: OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT } ) - temp_ca_cert = File.join(certs_dir, "ca_cert.pem") - temp_client_cert = File.join(certs_dir, "client.pem") + temp_ca_cert = CA_CERT_FILE + temp_client_cert = CLIENT_FILE with_configured_fetcher( ":ssl_ca_cert: #{temp_ca_cert}\n" \ @@ -66,8 +66,8 @@ def test_pqc_ssl_client_cert_auth_connection verify_mode: OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT ) - temp_ca_cert = File.join(certs_dir, "mldsa65_ca_cert.pem") - temp_client_cert = File.join(certs_dir, "mldsa65_client.pem") + temp_ca_cert = MLDSA65_CA_CERT_FILE + temp_client_cert = MLDSA65_CLIENT_FILE with_configured_fetcher( ":ssl_ca_cert: #{temp_ca_cert}\n" \ @@ -82,8 +82,8 @@ def test_do_not_allow_invalid_client_cert_auth_connection { verify_mode: OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT } ) - temp_ca_cert = File.join(certs_dir, "ca_cert.pem") - temp_client_cert = File.join(certs_dir, "invalid_client.pem") + temp_ca_cert = CA_CERT_FILE + temp_client_cert = INVALID_CLIENT_FILE with_configured_fetcher( ":ssl_ca_cert: #{temp_ca_cert}\n" \ @@ -114,7 +114,7 @@ def test_ssl_connection_allow_verify_none def test_do_not_follow_insecure_redirect @server_uri = "http://example.com" ssl_server = start_ssl_server - temp_ca_cert = File.join(certs_dir, "ca_cert.pem") + temp_ca_cert = CA_CERT_FILE expected_error_message = "redirecting to non-https resource: #{@server_uri} (https://localhost:#{ssl_server.addr[1]}/insecure_redirect?to=#{@server_uri})" diff --git a/test/rubygems/test_gem_request.rb b/test/rubygems/test_gem_request.rb index cd0a416e79c02d..cee7e8943f232e 100644 --- a/test/rubygems/test_gem_request.rb +++ b/test/rubygems/test_gem_request.rb @@ -8,13 +8,6 @@ end class TestGemRequest < Gem::TestCase - CA_CERT_FILE = cert_path "ca" - CHILD_CERT = load_cert "child" - EXPIRED_CERT = load_cert "expired" - PUBLIC_CERT = load_cert "public" - PUBLIC_CERT_FILE = cert_path "public" - SSL_CERT = load_cert "ssl" - def make_request(uri, request_class, last_modified, proxy) Gem::Request.create_with_proxy uri, request_class, last_modified, proxy end diff --git a/test/rubygems/test_gem_security.rb b/test/rubygems/test_gem_security.rb index e97af3b959e95c..20f4b9fa8951cb 100644 --- a/test/rubygems/test_gem_security.rb +++ b/test/rubygems/test_gem_security.rb @@ -12,13 +12,6 @@ end class TestGemSecurity < Gem::TestCase - CHILD_KEY = load_key "child" - EC_KEY = load_key "private_ec", "Foo bar" - - ALTERNATE_CERT = load_cert "child" - CHILD_CERT = load_cert "child" - EXPIRED_CERT = load_cert "expired" - def test_class_create_cert name = PUBLIC_CERT.subject key = PRIVATE_KEY @@ -122,7 +115,7 @@ def test_class_get_public_key_rsa end def test_class_get_public_key_ec - pkey = Gem::Security.get_public_key(EC_KEY) + pkey = Gem::Security.get_public_key(EC_PRIVATE_KEY) assert_respond_to pkey, :public_to_pem end @@ -163,7 +156,7 @@ def test_class_re_sign_not_self_signed end assert_equal "#{child_alt_name.value} is not self-signed, contact " \ - "#{ALTERNATE_CERT.issuer} to obtain a valid certificate", + "#{CHILD_CERT.issuer} to obtain a valid certificate", e.message end diff --git a/test/rubygems/test_gem_security_policy.rb b/test/rubygems/test_gem_security_policy.rb index 2f4fb1ce287597..30116ae9ef428d 100644 --- a/test/rubygems/test_gem_security_policy.rb +++ b/test/rubygems/test_gem_security_policy.rb @@ -7,23 +7,6 @@ end class TestGemSecurityPolicy < Gem::TestCase - ALTERNATE_KEY = load_key "alternate" - INVALID_KEY = load_key "invalid" - CHILD_KEY = load_key "child" - GRANDCHILD_KEY = load_key "grandchild" - INVALIDCHILD_KEY = load_key "invalidchild" - - ALTERNATE_CERT = load_cert "alternate" - CA_CERT = load_cert "ca" - CHILD_CERT = load_cert "child" - EXPIRED_CERT = load_cert "expired" - FUTURE_CERT = load_cert "future" - GRANDCHILD_CERT = load_cert "grandchild" - INVALIDCHILD_CERT = load_cert "invalidchild" - INVALID_ISSUER_CERT = load_cert "invalid_issuer" - INVALID_SIGNER_CERT = load_cert "invalid_signer" - WRONG_KEY_CERT = load_cert "wrong_key" - def setup super diff --git a/test/rubygems/test_gem_security_signer.rb b/test/rubygems/test_gem_security_signer.rb index d0541b0a341f80..59a379322b6340 100644 --- a/test/rubygems/test_gem_security_signer.rb +++ b/test/rubygems/test_gem_security_signer.rb @@ -7,14 +7,6 @@ end class TestGemSecuritySigner < Gem::TestCase - ALTERNATE_KEY = load_key "alternate" - CHILD_KEY = load_key "child" - GRANDCHILD_KEY = load_key "grandchild" - - CHILD_CERT = load_cert "child" - GRANDCHILD_CERT = load_cert "grandchild" - EXPIRED_CERT = load_cert "expired" - def setup super @@ -70,7 +62,7 @@ def test_initialize_default end def test_initialize_key_path - key_file = PRIVATE_KEY_PATH + key_file = PRIVATE_KEY_FILE signer = Gem::Security::Signer.new key_file, nil @@ -78,7 +70,7 @@ def test_initialize_key_path end def test_initialize_encrypted_key_path - key_file = ENCRYPTED_PRIVATE_KEY_PATH + key_file = ENCRYPTED_PRIVATE_KEY_FILE signer = Gem::Security::Signer.new key_file, nil, PRIVATE_KEY_PASSPHRASE diff --git a/test/rubygems/test_gem_security_trust_dir.rb b/test/rubygems/test_gem_security_trust_dir.rb index bd3dfb86c231ca..57d7d1e4e8c056 100644 --- a/test/rubygems/test_gem_security_trust_dir.rb +++ b/test/rubygems/test_gem_security_trust_dir.rb @@ -7,8 +7,6 @@ end class TestGemSecurityTrustDir < Gem::TestCase - CHILD_CERT = load_cert "child" - def setup super diff --git a/zjit/src/backend/arm64/mod.rs b/zjit/src/backend/arm64/mod.rs index f74098f900c1b9..21812829763cda 100644 --- a/zjit/src/backend/arm64/mod.rs +++ b/zjit/src/backend/arm64/mod.rs @@ -1068,6 +1068,15 @@ impl Assembler { ldr_post(cb, opnd, A64Opnd::new_mem(64, C_SP_REG, C_SP_STEP)); } + /// Fill nops until a jump written at `last_patch_pos` can no longer reach + /// past the current write position. + fn emit_pad_after_patch_point(cb: &mut CodeBlock, last_patch_pos: Option) { + let Some(last_patch_pos) = last_patch_pos else { return }; + while cb.get_write_pos().saturating_sub(last_patch_pos) < cb.jmp_ptr_bytes() && !cb.has_dropped_bytes() { + nop(cb); + } + } + // List of GC offsets let mut gc_offsets: Vec = Vec::new(); @@ -1519,16 +1528,18 @@ impl Assembler { Insn::Jonz(opnd, target) => { emit_cmp_zero_jump(cb, opnd.into(), false, target.clone()); }, - Insn::PatchPoint(..) => unreachable!("PatchPoint should have been lowered to PadPatchPoint in arm64_scratch_split"), - Insn::PadPatchPoint => { - // If patch points are too close to each other or the end of the block, fill nop instructions - if let Some(last_patch_pos) = last_patch_pos { - while cb.get_write_pos().saturating_sub(last_patch_pos) < cb.jmp_ptr_bytes() && !cb.has_dropped_bytes() { - nop(cb); - } - } + Insn::PatchPoint(..) => unreachable!("PatchPoint should have been lowered to PatchPointPad in arm64_scratch_split"), + Insn::PatchPointPad => { + emit_pad_after_patch_point(cb, last_patch_pos); + // This position is itself where a jump gets written on invalidation, so it + // becomes what following code has to keep its distance from. last_patch_pos = Some(cb.get_write_pos()); }, + Insn::BoundaryPad => { + // A boundary is never patched, so it doesn't become a position to keep away + // from. The last patch point stays that, and the pad just gave it its room. + emit_pad_after_patch_point(cb, last_patch_pos); + }, Insn::IncrCounter { mem, value } => { // Get the status register allocated by arm64_scratch_split let Some(Insn::Cmp { @@ -1760,6 +1771,55 @@ mod tests { assert_eq!(20, cb.jmp_ptr_bytes()); } + #[test] + fn test_fallthrough_to_a_patchpoint() { + // At one point, this generated unnecessary nop padding + use crate::cruby::test_utils::{compile_to_iseq, with_rubyvm}; + use crate::hir::Invariant; + use crate::payload::IseqVersion; + + let version = IseqVersion::new(compile_to_iseq("nil")); + + // The PosMarker that split_patch_point() installs registers the patch point + // with ZJITState's invariant table while emitting, so the VM has to be booted. + let cb = with_rubyvm(|| { + crate::options::rb_zjit_prepare_options(); // Allow `get_option!` in Assembler + let mut asm = Assembler::new(); + let mut cb = CodeBlock::new_dummy(); + + let bb0 = asm.new_block(crate::hir::BlockId(0), true, 0); + let bb1 = asm.new_block(crate::hir::BlockId(1), false, 1); + + // The patch point's target only has to resolve to some address for the + // PosMarker that records it, so bb0 stands in for the side exit code. + let side_exit = asm.new_label("side_exit"); + + // bb0 falls through to bb1 + asm.set_current_block(bb0); + let label_bb0 = asm.new_label("bb0"); + asm.write_label(label_bb0); + asm.write_label(side_exit.clone()); + asm.mov(Opnd::Reg(TEMP_REGS[0]), Opnd::UImm(1)); + asm.push_insn(Insn::Jmp(Target::Block(Box::new(BranchEdge { target: bb1, args: vec![] })))); + + asm.set_current_block(bb1); + let label_bb1 = asm.new_label("bb1"); + asm.write_label(label_bb1); + asm.patch_point(side_exit.clone(), Invariant::SingleRactorMode, version); + asm.cret(Opnd::Reg(TEMP_REGS[0])); + + asm.compile_with_num_regs(&mut cb, 0); + cb + }); + + assert_disasm_snapshot!(cb.disasm(), @" + 0x0: mov x1, #1 + 0x4: mov x0, x1 + 0x8: ret + "); + assert_snapshot!(cb.hexdump(), @"210080d2e00301aac0035fd6"); + } + #[test] fn test_lir_string() { use crate::hir::SideExitReason; diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index c284bab6e9e096..ca2d5e53ddcbb6 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -910,10 +910,26 @@ pub enum Insn { /// Cold fields are boxed (see `PatchPointData`) to keep `Insn` small. PatchPoint(Box), - /// Make sure the last PatchPoint has enough space to insert a jump. - /// We insert this instruction at the end of each block so that the jump - /// will not overwrite the next block or a side exit. - PadPatchPoint, + /// Space reserved immediately before a PatchPoint, keeping the *preceding* + /// patch point's invalidation jump from running over this one's address. + /// The position right after this pad is itself where a jump gets written on + /// invalidation, so whatever follows has to keep `jmp_ptr_bytes()` of + /// distance from it in turn. + /// + /// Zero-width whenever there is already enough room, which is the common + /// case. + PatchPointPad, + + /// Space reserved at a boundary that a preceding PatchPoint's invalidation + /// jump must not cross: the start of a non-entry block, the start of a side + /// exit, or the end of the last block. Nothing is ever patched at a + /// boundary, so unlike [`Insn::PatchPointPad`] this only protects what comes + /// after it, and it does not become something later code has to keep away + /// from — the first patch point of a block needs no padding in front of it. + /// + /// Zero-width whenever there is already enough room, which is the common + /// case. + BoundaryPad, // Mark a position in the generated code PosMarker(PosMarkerFn), @@ -1001,10 +1017,11 @@ macro_rules! for_each_operand_impl { } Insn::BakeString(_) | + Insn::BoundaryPad | Insn::Breakpoint | Insn::Abort | Insn::Comment(_) | Insn::CPop { .. } | - Insn::PadPatchPoint | + Insn::PatchPointPad | Insn::PosMarker(_) | Insn::PosMarkerAtBlockEnd(_) => {}, @@ -1139,6 +1156,7 @@ impl Insn { Insn::Add { .. } => "Add", Insn::And { .. } => "And", Insn::BakeString(_) => "BakeString", + Insn::BoundaryPad => "BoundaryPad", Insn::Breakpoint => "Breakpoint", Insn::Abort => "Abort", Insn::Comment(_) => "Comment", @@ -1187,7 +1205,7 @@ impl Insn { Insn::Not { .. } => "Not", Insn::Or { .. } => "Or", Insn::PatchPoint(..) => "PatchPoint", - Insn::PadPatchPoint => "PadPatchPoint", + Insn::PatchPointPad => "PatchPointPad", Insn::PosMarker(_) => "PosMarker", Insn::PosMarkerAtBlockEnd(_) => "PosMarkerAtBlockEnd", Insn::RShift { .. } => "RShift", @@ -1879,17 +1897,15 @@ impl Assembler // Emit instructions with labels, expanding branch parameters let mut insns = Vec::with_capacity(ASSEMBLER_INSNS_CAPACITY); - let block_ids = self.block_order(); - let num_blocks = block_ids.len(); for (i, block_id) in block_ids.iter().enumerate() { let block = &self.basic_blocks[block_id.0]; // Entry blocks shouldn't ever be preceded by something that can // stomp on this block. if !block.is_entry { - push_insns_with_perf_symbol(&mut insns, "PadPatchPoint", |insns| { - insns.push(Insn::PadPatchPoint); + push_insns_with_perf_symbol(&mut insns, "BoundaryPad", |insns| { + insns.push(Insn::BoundaryPad); }); } @@ -1919,14 +1935,12 @@ impl Assembler if let Some(marker) = block_end_pos_marker { insns.push(Insn::PosMarker(marker)); } - - // Make sure we don't stomp on the next function - if block_id.0 == num_blocks - 1 { - push_insns_with_perf_symbol(&mut insns, "PadPatchPoint", |insns| { - insns.push(Insn::PadPatchPoint); - }); - } } + // Make sure we don't stomp on the next function + push_insns_with_perf_symbol(&mut insns, "BoundaryPad", |insns| { + insns.push(Insn::BoundaryPad); + }); + insns } @@ -2857,7 +2871,7 @@ impl Assembler // Side exit blocks are not part of the CFG at the moment, // so we need to manually ensure that patchpoints get padded // so that nobody stomps on us - asm.pad_patch_point(); + asm.boundary_pad(); asm_comment!(asm, "save cfp->pc"); asm.store(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_PC), *pc); @@ -3973,8 +3987,12 @@ impl Assembler { self.push_insn(Insn::PatchPoint(Box::new(PatchPointData { target, invariant, version }))); } - pub fn pad_patch_point(&mut self) { - self.push_insn(Insn::PadPatchPoint); + pub fn patch_point_pad(&mut self) { + self.push_insn(Insn::PatchPointPad); + } + + pub fn boundary_pad(&mut self) { + self.push_insn(Insn::BoundaryPad); } pub fn pos_marker(&mut self, marker_fn: impl Fn(CodePtr, &CodeBlock) + 'static) { diff --git a/zjit/src/backend/x86_64/mod.rs b/zjit/src/backend/x86_64/mod.rs index e83ad9f2e7ad73..35fff351148df9 100644 --- a/zjit/src/backend/x86_64/mod.rs +++ b/zjit/src/backend/x86_64/mod.rs @@ -744,6 +744,16 @@ impl Assembler { gc_offsets.push(ptr_offset); } + /// Fill nops until a jump written at `last_patch_pos` can no longer reach + /// past the current write position. + fn emit_pad_after_patch_point(cb: &mut CodeBlock, last_patch_pos: Option) { + let Some(last_patch_pos) = last_patch_pos else { return }; + let code_size = cb.get_write_pos().saturating_sub(last_patch_pos); + if code_size < cb.jmp_ptr_bytes() { + nop(cb, (cb.jmp_ptr_bytes() - code_size) as u32); + } + } + // List of GC offsets let mut gc_offsets: Vec = Vec::new(); @@ -1059,17 +1069,18 @@ impl Assembler { Insn::Joz(..) | Insn::Jonz(..) => unreachable!("Joz/Jonz should be unused for now"), - Insn::PatchPoint(..) => unreachable!("PatchPoint should have been lowered to PadPatchPoint in x86_scratch_split"), - Insn::PadPatchPoint => { - // If patch points are too close to each other or the end of the block, fill nop instructions - if let Some(last_patch_pos) = last_patch_pos { - let code_size = cb.get_write_pos().saturating_sub(last_patch_pos); - if code_size < cb.jmp_ptr_bytes() { - nop(cb, (cb.jmp_ptr_bytes() - code_size) as u32); - } - } + Insn::PatchPoint(..) => unreachable!("PatchPoint should have been lowered to PatchPointPad in x86_scratch_split"), + Insn::PatchPointPad => { + emit_pad_after_patch_point(cb, last_patch_pos); + // This position is itself where a jump gets written on invalidation, so it + // becomes what following code has to keep its distance from. last_patch_pos = Some(cb.get_write_pos()); }, + Insn::BoundaryPad => { + // A boundary is never patched, so it doesn't become a position to keep away + // from. The last patch point stays that, and the pad just gave it its room. + emit_pad_after_patch_point(cb, last_patch_pos); + }, // Atomically increment a counter at a given memory location Insn::IncrCounter { mem, value } => { @@ -1377,6 +1388,56 @@ mod tests { } } + #[test] + fn test_fallthrough_to_a_patchpoint() { + // At one point, this generated unnecessary nop padding + use crate::cruby::test_utils::{compile_to_iseq, with_rubyvm}; + use crate::hir::Invariant; + use crate::payload::IseqVersion; + + let version = IseqVersion::new(compile_to_iseq("nil")); + + // The PosMarker that split_patch_point() installs registers the patch point + // with ZJITState's invariant table while emitting, so the VM has to be booted. + let cb = with_rubyvm(|| { + crate::options::rb_zjit_prepare_options(); // Allow `get_option!` in Assembler + let mut asm = Assembler::new(); + let mut cb = CodeBlock::new_dummy(); + + let bb0 = asm.new_block(crate::hir::BlockId(0), true, 0); + let bb1 = asm.new_block(crate::hir::BlockId(1), false, 1); + + // The patch point's target only has to resolve to some address for the + // PosMarker that records it, so bb0 stands in for the side exit code. + let side_exit = asm.new_label("side_exit"); + + // bb0 falls through to bb1 + asm.set_current_block(bb0); + let label_bb0 = asm.new_label("bb0"); + asm.write_label(label_bb0); + asm.write_label(side_exit.clone()); + asm.mov(C_ARG_OPNDS[0], Opnd::UImm(1)); + asm.push_insn(Insn::Jmp(Target::Block(Box::new(BranchEdge { target: bb1, args: vec![] })))); + + asm.set_current_block(bb1); + let label_bb1 = asm.new_label("bb1"); + asm.write_label(label_bb1); + asm.patch_point(side_exit.clone(), Invariant::SingleRactorMode, version); + asm.cret(C_ARG_OPNDS[0]); + + asm.compile_with_num_regs(&mut cb, 0); + cb + }); + + assert_disasm_snapshot!(cb.disasm(), @" + 0x0: mov edi, 1 + 0x5: mov rax, rdi + 0x8: ret + 0x9: nop + "); + assert_snapshot!(cb.hexdump(), @"bf010000004889f8c390"); + } + #[test] fn test_lir_string() { use crate::hir::SideExitReason; diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index dace5d4b5dec5e..43670f5561b6e1 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -980,7 +980,7 @@ fn gen_patch_point(jit: &mut JITState, asm: &mut Assembler, function: &Function, asm.patch_point(Target::SideExit(Box::new(SideExitTarget { exit, reason: PatchPoint(invariant) })), invariant, jit.version); } -/// This is used by scratch_split to lower PatchPoint into PadPatchPoint and PosMarker. +/// This is used by scratch_split to lower PatchPoint into PatchPointPad and PosMarker. /// It's called at scratch_split so that we can use the Label after side-exit deduplication in compile_exits. pub fn split_patch_point(asm: &mut Assembler, target: &Target, invariant: Invariant, version: IseqVersionRef) { let Target::Label(exit_label) = *target else { @@ -988,7 +988,7 @@ pub fn split_patch_point(asm: &mut Assembler, target: &Target, invariant: Invari }; // Fill nop instructions if the last patch point is too close. - asm.pad_patch_point(); + asm.patch_point_pad(); // Remember the current address as a patch point asm.pos_marker(move |code_ptr, cb| { diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index b2c9bad57f93e5..58a1cfb94086a1 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -1090,6 +1090,46 @@ fn test_send_optional_return_default_with_argument() { assert_snapshot!(assert_compiles("entry"), @"1"); } +#[test] +fn test_send_keyword_to_positional_hash() { + eval(" + def test(arg) = arg + def entry = test(k: 1) + entry + "); + assert_snapshot!(assert_compiles("entry"), @"{k: 1}"); +} + +#[test] +fn test_send_multiple_keywords_to_positional_hash() { + eval(" + def test(arg) = arg + def entry = test(k: 1, v: 2) + entry + "); + assert_snapshot!(assert_compiles("entry"), @"{k: 1, v: 2}"); +} + +#[test] +fn test_send_positional_and_keyword_to_positional_hash() { + eval(" + def test(a, b) = [a, b] + def entry = test(1, k: 2) + entry + "); + assert_snapshot!(assert_compiles("entry"), @"[1, {k: 2}]"); +} + +#[test] +fn test_send_optional_and_keyword_to_positional_hash() { + eval(" + def test(a, b = 2) = [a, b] + def entry = test(k: 1) + entry + "); + assert_snapshot!(assert_compiles("entry"), @"[{k: 1}, 2]"); +} + #[test] fn test_send_rest_arguments_with_keyword_to_positional_hash() { eval(" @@ -1100,6 +1140,61 @@ fn test_send_rest_arguments_with_keyword_to_positional_hash() { assert_snapshot!(assert_compiles("entry"), @"[{k: 1}]"); } +#[test] +fn test_send_optional_and_rest_arguments_with_keyword_to_positional_hash() { + eval(" + def test(a, b = 2, *rest) = [a, b, rest] + def entry = test(1, k: 3) + entry + "); + assert_snapshot!(assert_compiles("entry"), @"[1, {k: 3}, []]"); +} + +#[test] +fn test_send_rest_and_post_arguments_with_keyword_to_positional_hash() { + eval(" + def test(a, *rest, b) = [a, rest, b] + def entry = test(1, 2, k: 3) + entry + "); + assert_snapshot!(assert_compiles("entry"), @"[1, [2], {k: 3}]"); +} + +#[test] +fn test_send_keyword_splat_to_positional_hash_fallback() { + eval(" + def test(arg) = arg + def entry = test(**{ k: 1 }) + entry + "); + assert_snapshot!(assert_compiles("entry"), @"{k: 1}"); +} + +#[test] +fn test_send_no_kwarg_to_positional_hash_fallback() { + eval(" + def test(arg, **nil) = arg + def entry + test(k: 1) + rescue ArgumentError + :argument_error + end + entry + "); + assert_snapshot!(assert_compiles("entry"), @":argument_error"); +} + +#[test] +fn test_send_ruby2_keywords_to_positional_hash_fallback() { + eval(" + def target(k:) = k + ruby2_keywords def forward(*args) = target(*args) + def entry = forward(k: 1) + entry + "); + assert_snapshot!(assert_compiles("entry"), @"1"); +} + #[test] fn test_send_rest_arguments_with_block_literal() { eval(" diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 9d6fb7448b2f00..ac5cda92de1fdb 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2642,15 +2642,21 @@ fn can_direct_send(function: &mut Function, block: BlockId, iseq: *const rb_iseq return false } - // SendDirect only models explicit keyword slots for now, so leave this - // conversion to VM dispatch. - if keywords_as_positional_hash { + // Plain keyword-to-positional-hash is safe to synthesize below. Keep VM + // dispatch for callee modes that need keyword-sensitive handling: **nil + // rejection and ruby2_keywords flag preservation. + if keywords_as_positional_hash + && (params.flags.accepts_no_kwarg() != 0 || params.flags.ruby2_keywords() != 0) + { function.count(block, complex_arg_pass_keyword_to_positional_hash); function.set_dynamic_send_reason(send_insn, ComplexArgPass); return false; } - let keyword_ok = c_int::try_from(caller_kw_count) + // After keyword-to-positional-hash, SendDirect receives no keyword slots; + // the caller keywords are represented by one extra positional Hash. + let effective_keyword_count = if keywords_as_positional_hash { 0 } else { caller_kw_count }; + let keyword_ok = c_int::try_from(effective_keyword_count) .as_ref() .map(|argc| (kw_req_num..=kw_total_num).contains(argc)) .unwrap_or(false); @@ -2670,7 +2676,9 @@ fn can_direct_send(function: &mut Function, block: BlockId, iseq: *const rb_iseq // With *rest, SendDirect receives one rest-array slot instead of each rest // element, so cap positional argc at required/post + filled opts + rest slot. let passed_opt_num = (caller_positional_i32 - min_positional).min(opt_num) as usize; - let send_positional_argc = if has_rest { min_positional as usize + passed_opt_num + 1 } else { caller_positional }; + // Without *rest, use the converted positional count so the synthesized + // keyword Hash is included in the SendDirect argument count. + let send_positional_argc = if has_rest { min_positional as usize + passed_opt_num + 1 } else { effective_positional }; let send_argc = send_positional_argc + kw_total_num as usize; let c_argc = 1 + send_argc + block_arg; // +1 for self @@ -3605,8 +3613,7 @@ impl Function { iseq: IseqPtr, state: InsnId, ) -> Result { - let kwarg = unsafe { rb_vm_ci_kwarg(ci) }; - let (processed_args, caller_argc, kw_bits) = self.setup_keyword_arguments(block, args, kwarg, iseq)?; + let (processed_args, caller_argc, kw_bits) = self.setup_keyword_arguments(block, args, ci, iseq, state)?; let (processed_args, jit_entry_idx) = self.setup_rest_parameter(block, processed_args, iseq, state)?; // If args were reordered or synthesized, create a new snapshot with the updated stack @@ -3639,17 +3646,49 @@ impl Function { &mut self, block: BlockId, args: &[InsnId], - kwarg: *const rb_callinfo_kwarg, + ci: *const rb_callinfo, iseq: IseqPtr, + state: InsnId, ) -> Result<(Vec, usize, u32), SendFallbackReason> { + let kwarg = unsafe { rb_vm_ci_kwarg(ci) }; let callee_keyword = unsafe { rb_get_iseq_body_param_keyword(iseq) }; if callee_keyword.is_null() { - if !kwarg.is_null() { - // Caller is passing kwargs but callee doesn't expect them. + if kwarg.is_null() { + // Neither caller nor callee have keywords - nothing to do + return Ok((args.to_vec(), args.len(), 0)); + } + + let params = unsafe { iseq.params() }; + let ci_flags = unsafe { rb_vm_ci_flag(ci) }; + if ci_flags & VM_CALL_KW_SPLAT != 0 { + // Caller **kw is one runtime Hash, not explicit keyword slots, so + // there is no static key/value list to repack here. return Err(SendDirectKeywordMismatch); } - // Neither caller nor callee have keywords - nothing to do - return Ok((args.to_vec(), args.len(), 0)); + + if params.flags.accepts_no_kwarg() != 0 || params.flags.ruby2_keywords() != 0 { + // These callee modes need VM keyword setup even without a keyword table: + // **nil rejects keywords, and ruby2_keywords requires RHASH_PASS_AS_KEYWORDS. + return Err(SendDirectKeywordMismatch); + } + + // Match vm_args.c's setup_parameters_complex via args_kw_argv_to_hash: + // explicit caller keywords passed to a method with no keyword table + // become one final positional Hash before regular parameter setup. + let caller_kw_count = unsafe { get_cikw_keyword_len(kwarg) } as usize; + let kw_args_start = args.len() - caller_kw_count; + let mut elements = Vec::with_capacity(caller_kw_count * 2); + for i in 0..caller_kw_count { + let keyword = unsafe { get_cikw_keywords_idx(kwarg, i as i32) }; + let key = self.push_insn(block, Insn::Const { val: Const::Value(keyword) }); + elements.push(key); + elements.push(args[kw_args_start + i]); + } + + let hash = self.push_insn(block, Insn::NewHash { elements, state }); + let mut processed_args = args[..kw_args_start].to_vec(); + processed_args.push(hash); + return Ok((processed_args, args.len(), 0)); } // kwarg may be null if caller passes no keywords but callee has optional keywords diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 81338e1bb24590..2cf29df1c7b3b1 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -4776,10 +4776,208 @@ mod hir_opt_tests { } #[test] - fn dont_specialize_call_to_rest_with_keyword_to_positional_hash() { + fn specialize_call_with_keyword_to_positional_hash() { + eval(" + def foo(arg) = arg.class + def test = foo(k: 1) + test + test + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v11:Fixnum[1] = Const Value(1) + v19:StaticSymbol[:k] = Const Value(VALUE(0x1000)) + v20:HashExact = NewHash v19: v11 + PatchPoint MethodRedefined(Object@0x1008, foo@0x1010, cme:0x1018) + v23:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] recompile + PushInlineFrame v23 (0x1040), v20 + PatchPoint NoSingletonClass(Hash@0x1068) + PatchPoint MethodRedefined(Hash@0x1068, class@0x1070, cme:0x1078) + v44:ClassSubclass[Hash@0x1068] = Const Value(VALUE(0x1068)) + CheckInterrupts + PopInlineFrame + Return v44 + "); + } + + #[test] + fn specialize_call_with_multiple_keywords_to_positional_hash() { + eval(" + def foo(arg) = arg + def test = foo(k: 1, v: 2) + test + test + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v11:Fixnum[1] = Const Value(1) + v13:Fixnum[2] = Const Value(2) + v21:StaticSymbol[:k] = Const Value(VALUE(0x1000)) + v22:StaticSymbol[:v] = Const Value(VALUE(0x1008)) + v23:HashExact = NewHash v21: v11, v22: v13 + PatchPoint MethodRedefined(Object@0x1010, foo@0x1018, cme:0x1020) + v26:ObjectSubclass[class_exact*:Object@VALUE(0x1010)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1010)] recompile + CheckInterrupts + Return v23 + "); + } + + #[test] + fn specialize_call_with_positional_and_keyword_to_positional_hash() { + eval(" + def foo(a, b) = [a, b] + def test = foo(1, k: 2) + test + test + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v11:Fixnum[1] = Const Value(1) + v13:Fixnum[2] = Const Value(2) + v21:StaticSymbol[:k] = Const Value(VALUE(0x1000)) + v22:HashExact = NewHash v21: v13 + PatchPoint MethodRedefined(Object@0x1008, foo@0x1010, cme:0x1018) + v25:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] recompile + PushInlineFrame v25 (0x1040), v11, v22 + v36:ArrayExact = NewArray v11, v22 + CheckInterrupts + PopInlineFrame + Return v36 + "); + } + + #[test] + fn specialize_call_with_optional_and_keyword_to_positional_hash() { + eval(" + def foo(a, b = 2) = [a, b] + def test = foo(k: 1) + test + test + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v11:Fixnum[1] = Const Value(1) + v19:StaticSymbol[:k] = Const Value(VALUE(0x1000)) + v20:HashExact = NewHash v19: v11 + PatchPoint MethodRedefined(Object@0x1008, foo@0x1010, cme:0x1018) + v23:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] recompile + PushInlineFrame v23 (0x1040), v20 + v31:Fixnum[2] = Const Value(2) + v42:ArrayExact = NewArray v20, v31 + CheckInterrupts + PopInlineFrame + Return v42 + "); + } + + #[test] + fn dont_specialize_keyword_splat_to_positional_hash() { enable_zjit_stats(); eval(" - def foo(*args) = args + def foo(arg) = arg + def test = foo(**{k: 1}) + test + test + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + IncrCounterPtr + Jump bb3(v4) + bb3(v7:BasicObject): + IncrCounter zjit_insn_count + IncrCounter zjit_insn_count + v14:HashExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + v15:HashExact = HashDup v14 + IncrCounter zjit_insn_count + IncrCounter complex_arg_pass_caller_kw_splat + v18:BasicObject = Send v7, :foo, v15 # SendFallbackReason: Complex argument passing + IncrCounter zjit_insn_count + CheckInterrupts + Return v18 + "); + } + + #[test] + fn dont_specialize_no_kwarg_to_positional_hash() { + enable_zjit_stats(); + eval(" + def foo(arg, **nil) = arg + def test = foo(k: 1) + begin; test; rescue ArgumentError; end + begin; test; rescue ArgumentError; end + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + IncrCounterPtr + Jump bb3(v4) + bb3(v7:BasicObject): + IncrCounter zjit_insn_count + IncrCounter zjit_insn_count + v14:Fixnum[1] = Const Value(1) + IncrCounter zjit_insn_count + IncrCounter complex_arg_pass_keyword_to_positional_hash + v17:BasicObject = Send v7, :foo, v14 # SendFallbackReason: Complex argument passing + IncrCounter zjit_insn_count + CheckInterrupts + Return v17 + "); + } + + #[test] + fn dont_specialize_ruby2_keywords_to_positional_hash() { + enable_zjit_stats(); + eval(" + ruby2_keywords def foo(*args) = args def test = foo(k: 1) test test @@ -4808,6 +5006,107 @@ mod hir_opt_tests { "); } + #[test] + fn specialize_call_to_rest_with_optional_and_keyword_to_positional_hash() { + eval(" + def foo(a, b = 2, *rest) = [a, b, rest] + def test = foo(1, k: 3) + test + test + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v11:Fixnum[1] = Const Value(1) + v13:Fixnum[3] = Const Value(3) + v21:StaticSymbol[:k] = Const Value(VALUE(0x1000)) + v22:HashExact = NewHash v21: v13 + v23:ArrayExact = NewArray + PatchPoint MethodRedefined(Object@0x1008, foo@0x1010, cme:0x1018) + v26:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] recompile + PushInlineFrame v26 (0x1040), v11, v22, v23 + v39:ArrayExact = NewArray v11, v22, v23 + CheckInterrupts + PopInlineFrame + Return v39 + "); + } + + #[test] + fn specialize_call_to_rest_with_keyword_to_positional_hash() { + eval(" + def foo(*args) = args + def test = foo(k: 1) + test + test + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v11:Fixnum[1] = Const Value(1) + v19:StaticSymbol[:k] = Const Value(VALUE(0x1000)) + v20:HashExact = NewHash v19: v11 + v21:ArrayExact = NewArray v20 + PatchPoint MethodRedefined(Object@0x1008, foo@0x1010, cme:0x1018) + v24:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] recompile + PushInlineFrame v24 (0x1040), v21 + CheckInterrupts + PopInlineFrame + Return v21 + "); + } + + #[test] + fn specialize_call_to_rest_and_post_with_keyword_to_positional_hash() { + eval(" + def foo(a, *rest, b) = [a, rest, b] + def test = foo(1, 2, k: 3) + test + test + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v11:Fixnum[1] = Const Value(1) + v13:Fixnum[2] = Const Value(2) + v15:Fixnum[3] = Const Value(3) + v23:StaticSymbol[:k] = Const Value(VALUE(0x1000)) + v24:HashExact = NewHash v23: v15 + v25:ArrayExact = NewArray v13 + PatchPoint MethodRedefined(Object@0x1008, foo@0x1010, cme:0x1018) + v28:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] recompile + PushInlineFrame v28 (0x1040), v11, v25, v24 + v41:ArrayExact = NewArray v11, v25, v24 + CheckInterrupts + PopInlineFrame + Return v41 + "); + } + #[test] fn dont_classify_keyword_to_positional_hash_argc_mismatch_as_complex_arg_pass() { eval(" @@ -4834,6 +5133,33 @@ mod hir_opt_tests { "); } + #[test] + fn dont_classify_keyword_to_positional_hash_too_many_args_as_complex_arg_pass() { + eval(" + def foo(a) = a + def test = foo(1, k: 2) + begin; test; rescue ArgumentError; end + begin; test; rescue ArgumentError; end + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v11:Fixnum[1] = Const Value(1) + v13:Fixnum[2] = Const Value(2) + v15:BasicObject = Send v6, :foo, v11, v13 # SendFallbackReason: Argument count does not match parameter count + CheckInterrupts + Return v15 + "); + } + #[test] fn test_send_call_to_iseq_with_optional_kw() { eval("