From 4009b713a64f469e593ffbc5bb4b37eff43f3be3 Mon Sep 17 00:00:00 2001 From: Andrii Furmanets Date: Thu, 2 Apr 2026 14:08:17 +0300 Subject: [PATCH 1/2] [Bug #21974] Fix RubyVM::AST inspection for ::Foo = 1 (#16642) * Fix AST CDECL children for top-level constants * Simplify cdecl AST regression test --- ast.c | 15 ++++++++++++++- test/ruby/test_ast.rb | 11 +++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/ast.c b/ast.c index 5357aa38a5ae09..b22330f6b26a9f 100644 --- a/ast.c +++ b/ast.c @@ -404,6 +404,19 @@ rest_arg(VALUE ast_value, const NODE *rest_arg) return NODE_NAMED_REST_P(rest_arg) ? NEW_CHILD(ast_value, rest_arg) : no_name_rest(); } +static ID +node_colon_name(const NODE *node) +{ + switch (nd_type(node)) { + case NODE_COLON2: + return RNODE_COLON2(node)->nd_mid; + case NODE_COLON3: + return RNODE_COLON3(node)->nd_mid; + default: + rb_bug("unexpected node: %s", ruby_node_name(nd_type(node))); + } +} + static VALUE node_children(VALUE ast_value, const NODE *node) { @@ -497,7 +510,7 @@ node_children(VALUE ast_value, const NODE *node) if (RNODE_CDECL(node)->nd_vid) { return rb_ary_new_from_args(2, ID2SYM(RNODE_CDECL(node)->nd_vid), NEW_CHILD(ast_value, RNODE_CDECL(node)->nd_value)); } - return rb_ary_new_from_args(3, NEW_CHILD(ast_value, RNODE_CDECL(node)->nd_else), ID2SYM(RNODE_COLON2(RNODE_CDECL(node)->nd_else)->nd_mid), NEW_CHILD(ast_value, RNODE_CDECL(node)->nd_value)); + return rb_ary_new_from_args(3, NEW_CHILD(ast_value, RNODE_CDECL(node)->nd_else), ID2SYM(node_colon_name(RNODE_CDECL(node)->nd_else)), NEW_CHILD(ast_value, RNODE_CDECL(node)->nd_value)); case NODE_OP_ASGN1: return rb_ary_new_from_args(4, NEW_CHILD(ast_value, RNODE_OP_ASGN1(node)->nd_recv), ID2SYM(RNODE_OP_ASGN1(node)->nd_mid), diff --git a/test/ruby/test_ast.rb b/test/ruby/test_ast.rb index 22ccbfb604d0e4..6d3999a32aaa16 100644 --- a/test/ruby/test_ast.rb +++ b/test/ruby/test_ast.rb @@ -215,6 +215,17 @@ def test_parse_file_raises_syntax_error end end + def test_cdecl_children_with_toplevel_constant_path + # [Bug #21974] + children = parse("::Foo = 1").children[2].children + + assert_equal(:COLON3, children[0].type) + assert_equal([:Foo], children[0].children) + assert_equal(:Foo, children[1]) + assert_equal(:INTEGER, children[2].type) + assert_equal([1], children[2].children) + end + def assert_parse(code, warning: '') node = assert_warning(warning) {RubyVM::AbstractSyntaxTree.parse(code)} assert_kind_of(RubyVM::AbstractSyntaxTree::Node, node, code) From e866d9534c03efa6befab2eb019cf2a10a08f412 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 2 Apr 2026 19:52:37 +0900 Subject: [PATCH 2/2] Prism: Close the open file once finished reading --- prism_compile.c | 46 ++++++++++++++++++++++++++++----- test/ruby/test_compile_prism.rb | 6 +++++ 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/prism_compile.c b/prism_compile.c index 4f22baaad5c97e..b693f2e05a806a 100644 --- a/prism_compile.c +++ b/prism_compile.c @@ -11458,6 +11458,37 @@ pm_parse_file_script_lines(const pm_scope_node_t *scope_node, const pm_parser_t return lines; } +struct load_from_fd_args { + VALUE path; + VALUE io; + int open_mode; + int fd; +}; + +static VALUE +close_file(VALUE args) +{ + struct load_from_fd_args *arg = (void *)args; + if (arg->fd != -1) { + close(arg->fd); + } + else if (!NIL_P(arg->io)) { + rb_io_close(arg->io); + } + return Qnil; +} + +static VALUE +load_content(VALUE args) +{ + struct load_from_fd_args *arg = (void *)args; + VALUE io = rb_io_fdopen(arg->fd, arg->open_mode, RSTRING_PTR(arg->path)); + arg->io = io; + arg->fd = -1; + rb_io_wait(io, RB_INT2NUM(RUBY_IO_READABLE), Qnil); + return rb_funcall(io, rb_intern("read"), 0); +} + /** * Attempt to load the file into memory. Return a Ruby error if the file cannot * be read. @@ -11478,13 +11509,14 @@ pm_load_file(pm_parse_result_t *result, VALUE filepath, bool load_error) // For non-regular files (pipes, character devices), we need to read // through Ruby IO to properly release the GVL while waiting for data. if (init_result == PM_SOURCE_INIT_ERROR_NON_REGULAR) { - const int open_mode = O_RDONLY | O_NONBLOCK; - int fd = open(RSTRING_PTR(filepath), open_mode); - if (fd == -1) goto error_generic; - - VALUE io = rb_io_fdopen(fd, open_mode, RSTRING_PTR(filepath)); - rb_io_wait(io, RB_INT2NUM(RUBY_IO_READABLE), Qnil); - VALUE contents = rb_funcall(io, rb_intern("read"), 0); + struct load_from_fd_args args = { + .path = filepath, + .open_mode = O_RDONLY | O_NONBLOCK, + .fd = rb_cloexec_open(RSTRING_PTR(filepath), args.open_mode, 0), + .io = Qnil, + }; + if (args.fd == -1) goto error_generic; + VALUE contents = rb_ensure(load_content, (VALUE)&args, close_file, (VALUE)&args); if (!RB_TYPE_P(contents, T_STRING)) goto error_generic; diff --git a/test/ruby/test_compile_prism.rb b/test/ruby/test_compile_prism.rb index 096f85a8deb708..c017111c0afd2d 100644 --- a/test/ruby/test_compile_prism.rb +++ b/test/ruby/test_compile_prism.rb @@ -2721,6 +2721,12 @@ def test_parse_file assert_raise TypeError do RubyVM::InstructionSequence.compile_file_prism(nil) end + + assert_nothing_raised(Errno::EMFILE, Errno::ENFILE) do + 10000.times do + RubyVM::InstructionSequence.compile_file_prism(File::NULL) + end + end end private