From c7550537f11dcf6450a9d3df3af3fa1f4fe05b15 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Thu, 30 Sep 2021 16:58:46 +0900 Subject: [PATCH] `RubyVM.keep_script_lines` `RubyVM.keep_script_lines` enables to keep script lines for each ISeq and AST. This feature is for debugger/REPL support. ```ruby RubyVM.keep_script_lines = true RubyVM::keep_script_lines = true eval("def foo = nil\ndef bar = nil") pp RubyVM::InstructionSequence.of(method(:foo)).script_lines ``` --- common.mk | 1 + compile.c | 2 +- internal/compile.h | 1 + iseq.c | 44 ++++++++++++++++++++++++++++++---- parse.y | 3 ++- test/ruby/test_rubyvm.rb | 52 ++++++++++++++++++++++++++++++++++++++++ vm.c | 39 ++++++++++++++++++++++++++++++ vm_core.h | 1 + 8 files changed, 137 insertions(+), 6 deletions(-) diff --git a/common.mk b/common.mk index bc0fd462ea..c703a6da48 100644 --- a/common.mk +++ b/common.mk @@ -7194,6 +7194,7 @@ iseq.$(OBJEXT): {$(VPATH)}node.h iseq.$(OBJEXT): {$(VPATH)}node_name.inc iseq.$(OBJEXT): {$(VPATH)}onigmo.h iseq.$(OBJEXT): {$(VPATH)}oniguruma.h +iseq.$(OBJEXT): {$(VPATH)}ractor.h iseq.$(OBJEXT): {$(VPATH)}ruby_assert.h iseq.$(OBJEXT): {$(VPATH)}ruby_atomic.h iseq.$(OBJEXT): {$(VPATH)}st.h diff --git a/compile.c b/compile.c index 71e078d20c..055d4e85f5 100644 --- a/compile.c +++ b/compile.c @@ -1328,7 +1328,7 @@ new_child_iseq(rb_iseq_t *iseq, const NODE *const node, ast.root = node; ast.compile_option = 0; - ast.script_lines = INT2FIX(-1); + ast.script_lines = iseq->body->variable.script_lines; debugs("[new_child_iseq]> ---------------------------------------\n"); int isolated_depth = ISEQ_COMPILE_DATA(iseq)->isolated_depth; diff --git a/internal/compile.h b/internal/compile.h index 932dce2744..d32c2233c9 100644 --- a/internal/compile.h +++ b/internal/compile.h @@ -25,6 +25,7 @@ st_index_t rb_iseq_cdhash_hash(VALUE a); /* iseq.c */ int rb_vm_insn_addr2insn(const void *); int rb_vm_insn_decode(const VALUE encoded); +extern bool ruby_vm_keep_script_lines; MJIT_SYMBOL_EXPORT_BEGIN /* iseq.c (export) */ diff --git a/iseq.c b/iseq.c index 68611798c7..0551e0c99a 100644 --- a/iseq.c +++ b/iseq.c @@ -595,7 +595,8 @@ new_arena(void) static VALUE prepare_iseq_build(rb_iseq_t *iseq, VALUE name, VALUE path, VALUE realpath, VALUE first_lineno, const rb_code_location_t *code_location, const int node_id, - const rb_iseq_t *parent, int isolated_depth, enum iseq_type type, const rb_compile_option_t *option) + const rb_iseq_t *parent, int isolated_depth, enum iseq_type type, + VALUE script_lines, const rb_compile_option_t *option) { VALUE coverage = Qfalse; VALUE err_info = Qnil; @@ -616,6 +617,8 @@ prepare_iseq_build(rb_iseq_t *iseq, ISEQ_ORIGINAL_ISEQ_CLEAR(iseq); body->variable.flip_count = 0; + RB_OBJ_WRITE(iseq, &body->variable.script_lines, script_lines); + ISEQ_COMPILE_DATA_ALLOC(iseq); RB_OBJ_WRITE(iseq, &ISEQ_COMPILE_DATA(iseq)->err_info, err_info); RB_OBJ_WRITE(iseq, &ISEQ_COMPILE_DATA(iseq)->catch_table_ary, Qnil); @@ -894,7 +897,17 @@ rb_iseq_new_with_opt(const rb_ast_body_t *ast, VALUE name, VALUE path, VALUE rea } if (ast && ast->compile_option) rb_iseq_make_compile_option(&new_opt, ast->compile_option); - prepare_iseq_build(iseq, name, path, realpath, first_lineno, node ? &node->nd_loc : NULL, node ? nd_node_id(node) : -1, parent, isolated_depth, type, &new_opt); + VALUE script_lines = Qnil; + + if (ast && !FIXNUM_P(ast->script_lines) && ast->script_lines) { + script_lines = ast->script_lines; + } + else if (parent) { + script_lines = parent->body->variable.script_lines; + } + + prepare_iseq_build(iseq, name, path, realpath, first_lineno, node ? &node->nd_loc : NULL, node ? nd_node_id(node) : -1, + parent, isolated_depth, type, script_lines, &new_opt); rb_iseq_compile_node(iseq, node); finish_iseq_build(iseq); @@ -913,7 +926,7 @@ rb_iseq_new_with_callback( rb_iseq_t *iseq = iseq_alloc(); if (!option) option = &COMPILE_OPTION_DEFAULT; - prepare_iseq_build(iseq, name, path, realpath, first_lineno, NULL, -1, parent, 0, type, option); + prepare_iseq_build(iseq, name, path, realpath, first_lineno, NULL, -1, parent, 0, type, Qnil, option); rb_iseq_compile_callback(iseq, ifunc); finish_iseq_build(iseq); @@ -1026,7 +1039,7 @@ iseq_load(VALUE data, const rb_iseq_t *parent, VALUE opt) make_compile_option(&option, opt); option.peephole_optimization = FALSE; /* because peephole optimization can modify original iseq */ prepare_iseq_build(iseq, name, path, realpath, first_lineno, &tmp_loc, NUM2INT(node_id), - parent, 0, (enum iseq_type)iseq_type, &option); + parent, 0, (enum iseq_type)iseq_type, Qnil, &option); rb_iseq_build_from_ary(iseq, misc, locals, params, exception, body); @@ -3680,6 +3693,26 @@ succ_index_lookup(const struct succ_index_table *sd, int x) } #endif + +/* + * call-seq: + * iseq.script_lines -> array or nil + * + * It returns recorded script lines if it is availalble. + * The script lines are not limited to the iseq range, but + * are entire lines of the source file. + * + * Note that this is an API for ruby internal use, debugging, + * and research. Do not use this for any other purpose. + * The compatibility is not guaranteed. + */ +static VALUE +iseqw_script_lines(VALUE self) +{ + const rb_iseq_t *iseq = iseqw_check(self); + return iseq->body->variable.script_lines; +} + /* * Document-class: RubyVM::InstructionSequence * @@ -3747,6 +3780,9 @@ Init_ISeq(void) rb_define_singleton_method(rb_cISeq, "disassemble", iseqw_s_disasm, 1); rb_define_singleton_method(rb_cISeq, "of", iseqw_s_of, 1); + // script lines + rb_define_method(rb_cISeq, "script_lines", iseqw_script_lines, 0); + rb_undef_method(CLASS_OF(rb_cISeq), "translate"); rb_undef_method(CLASS_OF(rb_cISeq), "load_iseq"); } diff --git a/parse.y b/parse.y index 9f44ff6235..2ec9d7770c 100644 --- a/parse.y +++ b/parse.y @@ -6306,7 +6306,8 @@ yycompile0(VALUE arg) cov = Qtrue; } } - if (p->keep_script_lines) { + + if (p->keep_script_lines || ruby_vm_keep_script_lines) { if (!p->debug_lines) { p->debug_lines = rb_ary_new(); } diff --git a/test/ruby/test_rubyvm.rb b/test/ruby/test_rubyvm.rb index 67d46e27ad..fecaacfa78 100644 --- a/test/ruby/test_rubyvm.rb +++ b/test/ruby/test_rubyvm.rb @@ -15,4 +15,56 @@ class TestRubyVM < Test::Unit::TestCase assert_raise(ArgumentError){ RubyVM.stat(:unknown) } assert_raise_with_message(ArgumentError, /\u{30eb 30d3 30fc}/) {RubyVM.stat(:"\u{30eb 30d3 30fc}")} end + + def parse_and_compile + script = <<~RUBY + a = 1 + def foo + b = 2 + end + 1.times{ + c = 3 + } + RUBY + + ast = RubyVM::AbstractSyntaxTree.parse(script) + iseq = RubyVM::InstructionSequence.compile(script) + + [ast, iseq] + end + + def test_keep_script_lines + prev_conf = RubyVM.keep_script_lines + + # keep + RubyVM.keep_script_lines = true + + ast, iseq = *parse_and_compile + + lines = ast.script_lines + assert_equal Array, lines.class + + lines = iseq.script_lines + assert_equal Array, lines.class + iseq.each_child{|child| + assert_equal lines, child.script_lines + } + + # don't keep + RubyVM.keep_script_lines = false + + ast, iseq = *parse_and_compile + + lines = ast.script_lines + assert_equal nil, lines + + lines = iseq.script_lines + assert_equal nil, lines + iseq.each_child{|child| + assert_equal lines, child.script_lines + } + + ensure + RubyVM.keep_script_lines = prev_conf + end end diff --git a/vm.c b/vm.c index c1099d699b..1f60906d1a 100644 --- a/vm.c +++ b/vm.c @@ -380,6 +380,7 @@ VALUE rb_block_param_proxy; VALUE ruby_vm_const_missing_count = 0; rb_vm_t *ruby_current_vm_ptr = NULL; rb_ractor_t *ruby_single_main_ractor; +bool ruby_vm_keep_script_lines; #ifdef RB_THREAD_LOCAL_SPECIFIER RB_THREAD_LOCAL_SPECIFIER rb_execution_context_t *ruby_current_ec; @@ -3338,6 +3339,41 @@ vm_mtbl2(VALUE self, VALUE obj, VALUE sym) return Qnil; } +/* + * call-seq: + * RubyVM.keep_script_lines -> true or false + * + * Return current +keep_script_lines+ status. Now it only returns + * +true+ of +false+, but it can return other objects in future. + * + * Note that this is an API for ruby internal use, debugging, + * and research. Do not use this for any other purpose. + * The compatibility is not guaranteed. + */ +static VALUE +vm_keep_script_lines(VALUE self) +{ + return RBOOL(ruby_vm_keep_script_lines); +} + +/* + * call-seq: + * RubyVM.keep_script_lines = true / false + * + * It set +keep_script_lines+ flag. If the flag is set, all + * loaded scripts are recorded in a interpreter process. + * + * Note that this is an API for ruby internal use, debugging, + * and research. Do not use this for any other purpose. + * The compatibility is not guaranteed. + */ +static VALUE +vm_keep_script_lines_set(VALUE self, VALUE flags) +{ + ruby_vm_keep_script_lines = RTEST(flags); + return flags; +} + void Init_VM(void) { @@ -3361,6 +3397,9 @@ Init_VM(void) rb_undef_alloc_func(rb_cRubyVM); rb_undef_method(CLASS_OF(rb_cRubyVM), "new"); rb_define_singleton_method(rb_cRubyVM, "stat", vm_stat, -1); + rb_define_singleton_method(rb_cRubyVM, "keep_script_lines", vm_keep_script_lines, 0); + rb_define_singleton_method(rb_cRubyVM, "keep_script_lines=", vm_keep_script_lines_set, 1); + #if USE_DEBUG_COUNTER rb_define_singleton_method(rb_cRubyVM, "reset_debug_counters", rb_debug_counter_reset, 0); rb_define_singleton_method(rb_cRubyVM, "show_debug_counters", rb_debug_counter_show, 0); diff --git a/vm_core.h b/vm_core.h index 11af8a365d..8519da8f9e 100644 --- a/vm_core.h +++ b/vm_core.h @@ -433,6 +433,7 @@ struct rb_iseq_constant_body { struct { rb_snum_t flip_count; + VALUE script_lines; VALUE coverage; VALUE pc2branchindex; VALUE *original_iseq;