From 3643154e6d46c5bc6543d807ad1fc905e7993bc6 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Thu, 3 Sep 2026 15:31:36 -0400 Subject: [PATCH 1/4] [ruby/prism] Check heredoc state for escaped newlines When escaped newlines are encountered, they are added to the newline list. We need to check that we aren't backing up to read the rest of the line because of a heredoc, in which case the newline would have already have been added to the list. https://github.com/ruby/prism/commit/a3f8ba3d4a --- prism/prism.c | 10 +++++----- test/prism/fuzzer_test.rb | 4 ++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/prism/prism.c b/prism/prism.c index ecb329bef6a142..8754aecc9bdd47 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -9064,7 +9064,7 @@ escape_write_escape_encoded(pm_parser_t *parser, pm_buffer_t *buffer, pm_buffer_ } if (width == 1) { - if (*parser->current.end == '\n') pm_line_offset_list_append(&parser->metadata_arena, &parser->line_offsets, PM_TOKEN_END(parser, &parser->current) + 1); + if (parser->heredoc_end == NULL && *parser->current.end == '\n') pm_line_offset_list_append(&parser->metadata_arena, &parser->line_offsets, PM_TOKEN_END(parser, &parser->current) + 1); escape_write_byte(parser, buffer, regular_expression_buffer, flags, escape_byte(*parser->current.end++, flags)); } else if (width > 1) { // Valid multibyte character. Just ignore escape. @@ -9381,7 +9381,7 @@ escape_read(pm_parser_t *parser, pm_buffer_t *buffer, pm_buffer_t *regular_expre return; } - if (peeked == '\n') pm_line_offset_list_append(&parser->metadata_arena, &parser->line_offsets, PM_TOKEN_END(parser, &parser->current) + 1); + if (parser->heredoc_end == NULL && peeked == '\n') pm_line_offset_list_append(&parser->metadata_arena, &parser->line_offsets, PM_TOKEN_END(parser, &parser->current) + 1); parser->current.end++; escape_write_byte(parser, buffer, regular_expression_buffer, flags, escape_byte(peeked, flags | PM_ESCAPE_FLAG_CONTROL)); return; @@ -9440,7 +9440,7 @@ escape_read(pm_parser_t *parser, pm_buffer_t *buffer, pm_buffer_t *regular_expre return; } - if (peeked == '\n') pm_line_offset_list_append(&parser->metadata_arena, &parser->line_offsets, PM_TOKEN_END(parser, &parser->current) + 1); + if (parser->heredoc_end == NULL && peeked == '\n') pm_line_offset_list_append(&parser->metadata_arena, &parser->line_offsets, PM_TOKEN_END(parser, &parser->current) + 1); parser->current.end++; escape_write_byte(parser, buffer, regular_expression_buffer, flags, escape_byte(peeked, flags | PM_ESCAPE_FLAG_CONTROL)); return; @@ -9494,7 +9494,7 @@ escape_read(pm_parser_t *parser, pm_buffer_t *buffer, pm_buffer_t *regular_expre return; } - if (peeked == '\n') pm_line_offset_list_append(&parser->metadata_arena, &parser->line_offsets, PM_TOKEN_END(parser, &parser->current) + 1); + if (parser->heredoc_end == NULL && peeked == '\n') pm_line_offset_list_append(&parser->metadata_arena, &parser->line_offsets, PM_TOKEN_END(parser, &parser->current) + 1); parser->current.end++; escape_write_byte(parser, buffer, regular_expression_buffer, flags, escape_byte(peeked, flags | PM_ESCAPE_FLAG_META)); return; @@ -9502,7 +9502,7 @@ escape_read(pm_parser_t *parser, pm_buffer_t *buffer, pm_buffer_t *regular_expre } case '\r': { if (peek_offset(parser, 1) == '\n') { - pm_line_offset_list_append(&parser->metadata_arena, &parser->line_offsets, PM_TOKEN_END(parser, &parser->current) + 2); + if (parser->heredoc_end == NULL) pm_line_offset_list_append(&parser->metadata_arena, &parser->line_offsets, PM_TOKEN_END(parser, &parser->current) + 2); parser->current.end += 2; escape_write_byte_encoded(parser, buffer, flags, escape_byte('\n', flags)); return; diff --git a/test/prism/fuzzer_test.rb b/test/prism/fuzzer_test.rb index 4927478bdc2a1d..b6a18ab51eac2a 100644 --- a/test/prism/fuzzer_test.rb +++ b/test/prism/fuzzer_test.rb @@ -63,5 +63,9 @@ def self.snippet(name, source) a /{/, ''\\ RUBY + + snippet "escaped newline in char literal after heredoc opener", "< Date: Fri, 4 Sep 2026 09:41:04 -0400 Subject: [PATCH 2/4] ZJIT: Don't include builtin bits in user-defined classes (#18614) This makes Fixnum.intersect(some user class Foo) result in Empty instead of previously some nonsense value like Fixnum[Foo]. This allows folding a lot of calls to `IsA` to `false` that were not possible before. Drops total number of calls to `rb_obj_is_kind_of` from 3.5MM to 2.9MM on lobsters. ```diff --- /tmp/ccalls-before.txt +++ /tmp/ccalls-after.txt @@ -1,9 +1,9 @@ -Top-20 calls to C functions from JIT code (69.8% of total 87,587,232): - rb_vm_opt_send_without_block: 9,913,465 (11.3%) - rb_hash_aref: 9,901,809 (11.3%) - rb_ivar_get: 6,116,145 ( 7.0%) - rb_gc_writebarrier: 5,463,959 ( 6.2%) + Array#reverse_each: 236,567 ( 1.0%) +Top-20 calls to C functions from JIT code (69.6% of total 86,987,847): + rb_vm_opt_send_without_block: 9,913,448 (11.4%) + rb_hash_aref: 9,901,807 (11.4%) + rb_ivar_get: 6,116,147 ( 7.0%) + rb_gc_writebarrier: 5,463,952 ( 6.3%) rb_vm_invokeblock: 4,348,396 ( 5.0%) - rb_vm_send: 3,874,202 ( 4.4%) - Hash#fetch: 3,705,675 ( 4.2%) - rb_obj_is_kind_of: 3,507,142 ( 4.0%) - rb_hash_aset: 2,916,632 ( 3.3%) - rb_vm_getinstancevariable: 2,325,583 ( 2.7%) - Hash#key?: 1,411,874 ( 1.6%) + rb_vm_send: 3,874,201 ( 4.5%) + Hash#fetch: 3,705,677 ( 4.3%) + rb_hash_aset: 2,916,628 ( 3.4%) + rb_obj_is_kind_of: 2,897,725 ( 3.3%) + rb_vm_getinstancevariable: 2,325,637 ( 2.7%) ``` Co-authored-by: Aaron Patterson --- zjit/src/hir/opt_tests.rs | 87 +++++++++++++++++++++++++++++++++++++-- zjit/src/hir_type/mod.rs | 65 ++++++++++++++++++++++++++--- 2 files changed, 143 insertions(+), 9 deletions(-) diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 62ab8c6bd66096..3658f484b3828d 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -16535,9 +16535,56 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(String@0x1010) PatchPoint MethodRedefined(String@0x1010, is_a?@0x1011, cme:0x1018) v27:StringExact = GuardType v10, StringExact recompile - v28:BoolExact = IsA v27, v16 + v29:TrueClass = Const Value(true) CheckInterrupts - Return v28 + Return v29 + "); + } + + #[test] + fn test_specialize_is_a_class_polymorphic() { + set_call_threshold(4); + eval(r#" + def test(o) = o.is_a?(String) + test("asdf") + test(4) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :o@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :o@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint StableConstantNames(0x1008, String) + v16:ClassSubclass[String@0x1010] = Const Value(VALUE(0x1010)) + v19:CBool = HasType v10, Fixnum + CondBranch v19, bb5(), bb6() + bb5(): + PatchPoint MethodRedefined(Integer@0x1018, is_a?@0x1020, cme:0x1028) + v45:FalseClass = Const Value(false) + Jump bb4(v45) + bb6(): + v25:CBool = HasType v10, StringExact + CondBranch v25, bb7(), bb8() + bb7(): + PatchPoint NoSingletonClass(String@0x1010) + PatchPoint MethodRedefined(String@0x1010, is_a?@0x1020, cme:0x1028) + v46:TrueClass = Const Value(true) + Jump bb4(v46) + bb8(): + v31:BasicObject = Send v10, :is_a?, v16 # SendFallbackReason: Send: polymorphic call site + Jump bb4(v31) + bb4(v18:BasicObject): + CheckInterrupts + Return v18 "); } @@ -16664,9 +16711,9 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(String@0x1010) PatchPoint MethodRedefined(String@0x1010, kind_of?@0x1011, cme:0x1018) v27:StringExact = GuardType v10, StringExact recompile - v28:BoolExact = IsA v27, v16 + v29:TrueClass = Const Value(true) CheckInterrupts - Return v28 + Return v29 "); } @@ -16789,6 +16836,38 @@ mod hir_opt_tests { "); } + #[test] + fn test_fold_is_a_user_class_with_profiled_fixnum_to_false() { + eval(r#" + class C; end + def test(o) = o.is_a?(C) + test(5) + test(5) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :o@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :o@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint StableConstantNames(0x1008, C) + v16:ClassSubclass[C@0x1010] = Const Value(VALUE(0x1010)) + PatchPoint MethodRedefined(Integer@0x1018, is_a?@0x1020, cme:0x1028) + v26:Fixnum = GuardType v10, Fixnum recompile + v28:FalseClass = Const Value(false) + CheckInterrupts + Return v28 + "); + } + #[test] fn test_is_a_array_subclass_folds_to_true() { eval(r#" diff --git a/zjit/src/hir_type/mod.rs b/zjit/src/hir_type/mod.rs index 1a886070e30f4e..bc2ca1fec6eaf6 100644 --- a/zjit/src/hir_type/mod.rs +++ b/zjit/src/hir_type/mod.rs @@ -259,6 +259,9 @@ impl Type { Type::new(bits::Fixnum, Specialization::Object(VALUE::fixnum_from_usize(val as usize))) } + /// Find the type bits corresponding to exactly the given Ruby class. If we already have + /// pre-defined bit patterns for it (say, `NilClass` or `String`), then return those bits + /// (`NilClass`, `StringExact`). Otherwise return None. fn bits_from_exact_class(class: VALUE) -> Option { types::ExactBitsAndClass .iter() @@ -266,6 +269,19 @@ impl Type { .map(|&(bits, _)| bits) } + /// Find the type bits corresponding to the given Ruby class and all of its subclasses. If we + /// already have pre-defined bit patterns for it (say, `Array` or `Hash`), then return those + /// bits (`Array`, `Hash`). Otherwise return None. + fn bits_from_inexact_class(class: VALUE) -> Option { + types::InexactBitsAndClass + .iter() + .find(|&&(_, class_object)| unsafe { *class_object } == class) + .map(|&(bits, _)| bits) + } + + /// Find the type bits corresponding to the given Ruby class's subclasses, excluding the class + /// itself. If we already have pre-defined bit patterns for it (say, `Array` or `Hash`), then + /// return those bits (`ArraySubclass`, `HashSubclass`). Otherwise return None. fn bits_from_subclass(class: VALUE) -> Option { types::SubclassBitsAndClass .iter() @@ -352,6 +368,11 @@ impl Type { else { Self::from_class(val.class()).intersection(types::HeapBasicObject) } } + /// Try to represent the class using only bits, falling back to the nearest builtin subclass + /// and a TypeExact specialization. + /// + /// Useful for getting specific type information: if we know that we're allocating from a + /// specific class, we know the results will be exactly that class and not a subclass. pub fn from_class(class: VALUE) -> Type { if let Some(bits) = Self::bits_from_exact_class(class) { return Type::from_bits(bits); @@ -363,12 +384,20 @@ impl Type { get_class_name(class)) } + /// Try to represent the class or its subclasses using only bits, falling back to the nearest + /// builtin subclass and a Type specialization. + /// + /// Useful for querying subclassing. For example, if we want to query if some `t: Type` is a + /// subclass of `class`, we can use `t.is_subtype(Type::from_class_inexact(class))`. pub fn from_class_inexact(class: VALUE) -> Type { - let bits = types::InexactBitsAndClass - .iter() - .find(|&(_, class_object)| class.is_subclass_of(unsafe { **class_object }) == ClassRelationship::Subclass) - .unwrap_or_else(|| panic!("Class {} is not a subclass of BasicObject! Don't know what to do.", get_class_name(class))).0; - Type::new(bits, Specialization::Type(class)) + if let Some(bits) = Self::bits_from_inexact_class(class) { + return Type::from_bits(bits); + } + if let Some(bits) = Self::bits_from_subclass(class) { + return Type::new(bits, Specialization::Type(class)); + } + unreachable!("Class {} is not a subclass of BasicObject! Don't know what to do.", + get_class_name(class)) } /// Private. Only for creating type globals. @@ -917,6 +946,32 @@ mod tests { }); } + #[test] + fn from_class_inexact() { + crate::cruby::with_rubyvm(|| { + assert_bit_equal(Type::from_class_inexact(unsafe { rb_cArray }), types::Array); + assert_bit_equal(Type::from_class_inexact(unsafe { rb_cNilClass }), types::NilClass); + assert_bit_equal(Type::from_class_inexact(unsafe { rb_cString }), types::String); + let c_class = define_class("C", unsafe { rb_cObject }); + assert_bit_equal(Type::from_class_inexact(c_class), + Type::new(bits::ObjectSubclass, Specialization::Type(c_class))); + }); + } + + #[test] + fn intersection_of_builtin_and_user_class_inexact_is_empty() { + crate::cruby::with_rubyvm(|| { + let c_class = define_class("C", unsafe { rb_cObject }); + let c_inexact = Type::from_class_inexact(c_class); + // A Fixnum can never be an instance of C or any subclass of C; the + // bits are disjoint, so no specialization comparison is needed. + assert_bit_equal(Type::fixnum(123).intersection(c_inexact), types::Empty); + assert_bit_equal(c_inexact.intersection(Type::fixnum(123)), types::Empty); + assert_bit_equal(types::Fixnum.intersection(c_inexact), types::Empty); + assert_bit_equal(c_inexact.intersection(types::Fixnum), types::Empty); + }); + } + #[test] fn integer_has_ruby_class() { crate::cruby::with_rubyvm(|| { From 3a0ec78790eca7878228e919ae262a18178261be Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Wed, 2 Sep 2026 10:11:42 -0400 Subject: [PATCH 3/4] Don't create CCs for hook methods called from C MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calling these hook methods shouldn't create CCs because these are not hot methods. These CCs were only looked up in the gccct table (global call cache lookup table) for rb_funcall and friends. For one of our apps after bootup we reduced CCs significantly: | | A control | B (your commit) | B − A | |-----------------------|------------|-----------------|-----------------------| | callcaches (after GC) | 705,957 | 651,747 | −54,210 (−7.7%) | | cc tables | 46,361 | 43,051 | −3,310 | | total cc memory | 86,441,345 | 79,823,142 | −6,618,203 (−6.3 MB) | --- class.c | 3 ++- eval.c | 13 +++++++------ internal/vm.h | 1 + variable.c | 5 +++-- vm_method.c | 32 +++++++++++++++++++++++++++++++- 5 files changed, 44 insertions(+), 10 deletions(-) diff --git a/class.c b/class.c index f3ea869573a6ca..3fc6bf1fd006a5 100644 --- a/class.c +++ b/class.c @@ -28,6 +28,7 @@ #include "internal/object.h" #include "internal/string.h" #include "internal/variable.h" +#include "internal/vm.h" #include "ruby/st.h" #include "vm_core.h" #include "ruby/ractor.h" @@ -1404,7 +1405,7 @@ rb_class_inherited(VALUE super, VALUE klass) ID inherited; if (!super) super = rb_cObject; CONST_ID(inherited, "inherited"); - return rb_funcall(super, inherited, 1, klass); + return rb_funcallv_uncached(super, inherited, 1, &klass); } #ifdef rb_define_class diff --git a/eval.c b/eval.c index e23be408ada3c9..ddefe300f5da29 100644 --- a/eval.c +++ b/eval.c @@ -30,6 +30,7 @@ #include "internal/object.h" #include "internal/thread.h" #include "internal/variable.h" +#include "internal/vm.h" #include "ruby/fiber/scheduler.h" #include "iseq.h" #include "probes.h" @@ -1328,8 +1329,8 @@ rb_mod_include(int argc, VALUE *argv, VALUE module) } } while (argc--) { - rb_funcall(argv[argc], id_append_features, 1, module); - rb_funcall(argv[argc], id_included, 1, module); + rb_funcallv_uncached(argv[argc], id_append_features, 1, &module); + rb_funcallv_uncached(argv[argc], id_included, 1, &module); } return module; } @@ -1385,8 +1386,8 @@ rb_mod_prepend(int argc, VALUE *argv, VALUE module) } } while (argc--) { - rb_funcall(argv[argc], id_prepend_features, 1, module); - rb_funcall(argv[argc], id_prepended, 1, module); + rb_funcallv_uncached(argv[argc], id_prepend_features, 1, &module); + rb_funcallv_uncached(argv[argc], id_prepended, 1, &module); } return module; } @@ -1981,8 +1982,8 @@ rb_obj_extend(int argc, VALUE *argv, VALUE obj) } } while (argc--) { - rb_funcall(argv[argc], id_extend_object, 1, obj); - rb_funcall(argv[argc], id_extended, 1, obj); + rb_funcallv_uncached(argv[argc], id_extend_object, 1, &obj); + rb_funcallv_uncached(argv[argc], id_extended, 1, &obj); } return obj; } diff --git a/internal/vm.h b/internal/vm.h index 560c51d703435d..f6917c35cdb928 100644 --- a/internal/vm.h +++ b/internal/vm.h @@ -81,6 +81,7 @@ void rb_check_stack_overflow(void); VALUE rb_block_call2(VALUE obj, ID mid, int argc, const VALUE *argv, rb_block_call_func_t bl_proc, VALUE data2, long flags); struct vm_ifunc *rb_current_ifunc(void); VALUE rb_gccct_clear_table(void); +VALUE rb_funcallv_uncached(VALUE recv, ID mid, int argc, const VALUE *argv); VALUE rb_eval_cmd_call_kw(VALUE cmd, int argc, const VALUE *argv, int kw_splat); #if USE_YJIT || USE_ZJIT diff --git a/variable.c b/variable.c index 564bcda62fe576..b703819e2affdb 100644 --- a/variable.c +++ b/variable.c @@ -34,6 +34,7 @@ #include "internal/symbol.h" #include "internal/thread.h" #include "internal/variable.h" +#include "internal/vm.h" #include "ruby/encoding.h" #include "ruby/st.h" #include "ruby/util.h" @@ -3831,8 +3832,8 @@ static void const_added(VALUE klass, ID const_name) { if (GET_VM()->running) { - VALUE name = ID2SYM(const_name); - rb_funcallv(klass, idConst_added, 1, &name); + VALUE arg = ID2SYM(const_name); + rb_funcallv_uncached(klass, idConst_added, 1, &arg); } } diff --git a/vm_method.c b/vm_method.c index d01e4280cd4a7d..13ec548976e0cb 100644 --- a/vm_method.c +++ b/vm_method.c @@ -1704,6 +1704,8 @@ rb_check_overloaded_cme(const rb_callable_method_entry_t *cme, const struct rb_c return cme; } +static inline void stack_check(rb_execution_context_t *ec); + #define CALL_METHOD_HOOK(klass, hook, mid) do { \ const VALUE arg = ID2SYM(mid); \ VALUE recv_class = (klass); \ @@ -1712,7 +1714,7 @@ rb_check_overloaded_cme(const rb_callable_method_entry_t *cme, const struct rb_c recv_class = RCLASS_ATTACHED_OBJECT((klass)); \ hook_id = singleton_##hook; \ } \ - rb_funcallv(recv_class, hook_id, 1, &arg); \ + rb_funcallv_uncached(recv_class, hook_id, 1, &arg); \ } while (0) static void @@ -1932,6 +1934,34 @@ prepare_callable_method_entry(VALUE defined_class, ID id, const rb_method_entry_ } } +/* A hook like this fires from C with no call site to cache into except for the gccct table, + * which is often cleared anyway. It would leave a permanent CC behind (tied to the class) if it + * created one, so we try to avoid it. */ +VALUE +rb_funcallv_uncached(VALUE recv, ID mid, int argc, const VALUE *argv) +{ + VALUE defined_class; + const rb_method_entry_t *me = search_method(CLASS_OF(recv), mid, &defined_class); + + if (UNLIKELY(UNDEFINED_METHOD_ENTRY_P(me))) { + return rb_funcallv(recv, mid, argc, argv); + } + + const rb_callable_method_entry_t *cme; + + if (UNLIKELY(me->defined_class == 0)) { + // produce a transient CME that will get collected + cme = rb_method_entry_complement_defined_class(me, me->called_id, defined_class); + } + else { + cme = (const rb_callable_method_entry_t *)me; + } + + rb_execution_context_t *ec = GET_EC(); + stack_check(ec); + return rb_vm_call_kw(ec, recv, mid, argc, argv, cme, RB_NO_KEYWORDS); +} + static const rb_callable_method_entry_t * complemented_callable_method_entry(VALUE klass, ID id) { From 8cf4dc8ac79e8434a5d2db7d17138075120a7ea9 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Fri, 4 Sep 2026 13:22:42 -0400 Subject: [PATCH 4/4] ZJIT: Support dumping HIR to a file (#18604) This lets us analyze HIR without redirecting all stdout somewhere. --- doc/jit/zjit.md | 9 +++++++ zjit/src/hir.rs | 28 ++++++++++++++----- zjit/src/options.rs | 65 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 6 deletions(-) diff --git a/doc/jit/zjit.md b/doc/jit/zjit.md index e933afcc48055a..98a0295610795a 100644 --- a/doc/jit/zjit.md +++ b/doc/jit/zjit.md @@ -427,6 +427,15 @@ Note that this disables profiling. To inject interpreter profiles into ZJIT, con ./miniruby --zjit --zjit-dump-hir -e "30.times { 1 + 1 }" ``` +To write the dump to a file instead of stdout, pass an existing directory as the option value. Any value other than `all` or `debug` is treated as a directory name, and it composes with those format variants: + +```bash +./miniruby --zjit --zjit-dump-hir=/tmp --zjit-call-threshold=1 -e "1 + 1" +./miniruby --zjit --zjit-dump-hir=all --zjit-dump-hir=/tmp --zjit-call-threshold=1 -e "1 + 1" +``` + +The file `/tmp/hir-$PID` is truncated at startup and appended to as each method compiles. + ### Viewing HIR in Iongraph Using `--zjit-dump-hir-iongraph` will dump all compiled functions into a directory named `/tmp/zjit-iongraph-{PROCESS_PID}`. Each file will be named `func_{ZJIT_FUNC_NAME}.json`. In order to use them in the Iongraph viewer, you'll need to use `jq` to collate them to a single file. An example invocation of `jq` is shown below for reference. diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index f66bddd4eb4332..e8df6678717c1c 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2525,6 +2525,22 @@ impl<'a> FunctionPrinter<'a> { } } +/// Write a HIR dump to the file given by --zjit-dump-hir=some_directory, or to stdout if no path +/// was given. +fn print_hir_dump(label: &str, body: &dyn std::fmt::Display) { + match crate::options::get_option_ref!(dump_hir_file) { + Some(path) => { + use std::io::Write; + let result = std::fs::OpenOptions::new().create(true).append(true).open(path) + .and_then(|mut file| writeln!(file, "{label}:\n{body}")); + if let Err(e) = result { + eprintln!("ZJIT: Failed to write HIR dump to '{}': {}", path.display(), e); + } + } + None => println!("{label}:\n{body}"), + } +} + /// Union-Find (Disjoint-Set) is a data structure for managing disjoint sets that has an interface /// of two operations: /// @@ -7488,9 +7504,9 @@ impl Function { pub fn dump_hir(&self) { // Dump HIR after optimization match get_option!(dump_hir_opt) { - Some(DumpHIR::WithoutSnapshot) => println!("Optimized HIR:\n{}", FunctionPrinter::without_snapshot(self)), - Some(DumpHIR::All) => println!("Optimized HIR:\n{}", FunctionPrinter::with_snapshot(self)), - Some(DumpHIR::Debug) => println!("Optimized HIR:\n{:#?}", &self), + Some(DumpHIR::WithoutSnapshot) => print_hir_dump("Optimized HIR", &FunctionPrinter::without_snapshot(self)), + Some(DumpHIR::All) => print_hir_dump("Optimized HIR", &FunctionPrinter::with_snapshot(self)), + Some(DumpHIR::Debug) => print_hir_dump("Optimized HIR", &format_args!("{:#?}", self)), None => {}, } } @@ -10744,9 +10760,9 @@ fn add_iseq_to_hir( fun.infer_types(); match get_option!(dump_hir_init) { - Some(DumpHIR::WithoutSnapshot) => println!("Initial HIR:\n{}", FunctionPrinter::without_snapshot(fun)), - Some(DumpHIR::All) => println!("Initial HIR:\n{}", FunctionPrinter::with_snapshot(fun)), - Some(DumpHIR::Debug) => println!("Initial HIR:\n{:#?}", fun), + Some(DumpHIR::WithoutSnapshot) => print_hir_dump("Initial HIR", &FunctionPrinter::without_snapshot(fun)), + Some(DumpHIR::All) => print_hir_dump("Initial HIR", &FunctionPrinter::with_snapshot(fun)), + Some(DumpHIR::Debug) => print_hir_dump("Initial HIR", &format_args!("{:#?}", fun)), None => {}, } } diff --git a/zjit/src/options.rs b/zjit/src/options.rs index 27f2fa90762daf..ac1e20bfdd3a17 100644 --- a/zjit/src/options.rs +++ b/zjit/src/options.rs @@ -105,6 +105,9 @@ pub struct Options { /// Dump High-level IR after optimization, right before codegen. pub dump_hir_opt: Option, + /// Dump High-level IR to the given file instead of stdout + pub dump_hir_file: Option, + /// Dump High-level IR to the given file in Graphviz format after optimization pub dump_hir_graphviz: Option, @@ -203,6 +206,7 @@ impl Default for Options { disable_hir_opt: false, dump_hir_init: None, dump_hir_opt: None, + dump_hir_file: None, dump_hir_graphviz: None, dump_hir_iongraph: false, dump_lir: None, @@ -536,6 +540,27 @@ fn parse_option(str_ptr: *const std::os::raw::c_char) -> Option<()> { ("dump-hir" | "dump-hir-opt", "") => options.dump_hir_opt = Some(DumpHIR::WithoutSnapshot), ("dump-hir" | "dump-hir-opt", "all") => options.dump_hir_opt = Some(DumpHIR::All), ("dump-hir" | "dump-hir-opt", "debug") => options.dump_hir_opt = Some(DumpHIR::Debug), + // Any other value is a directory to dump HIR to instead of stdout. It composes with the + // format variants, e.g. `--zjit-dump-hir=all --zjit-dump-hir=/tmp/` dumps to /tmp/hir-PID. + ("dump-hir" | "dump-hir-opt", _) => { + let directory = std::fs::canonicalize(&opt_val) + .map_err(|e| eprintln!("Failed to canonicalize path '{opt_val}': {e}")).ok()?; + if !directory.is_dir() { + eprintln!("Path '{opt_val}' is not a directory"); + return None; + } + let file_name = directory.join(format!("hir-{}", std::process::id())); + // Truncate the file if it exists + std::fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&file_name) + .map_err(|e| eprintln!("Failed to open file '{}': {e}", file_name.display())) + .ok(); + options.dump_hir_file = Some(file_name); + options.dump_hir_opt.get_or_insert(DumpHIR::WithoutSnapshot); + } ("dump-hir-init", "") => options.dump_hir_init = Some(DumpHIR::WithoutSnapshot), ("dump-hir-init", "all") => options.dump_hir_init = Some(DumpHIR::All), @@ -752,6 +777,46 @@ pub extern "C" fn rb_zjit_get_stats_file_path_p(_ec: EcPtr, _self: VALUE) -> VAL mod tests { use super::*; + #[test] + fn parse_dump_hir_path() { + unsafe { OPTIONS = Some(Options::default()); } + + let path = std::path::PathBuf::from("/tmp"); + let option = CString::new(format!("dump-hir={}", path.display())).unwrap(); + + assert!(parse_option(option.as_ptr()).is_some()); + + let options = unsafe { OPTIONS.as_ref() }.unwrap(); + // parse_option canonicalizes the path, so canonicalize the expectation too + let expected = std::fs::canonicalize(&path).unwrap().join(format!("hir-{}", std::process::id())); + assert_eq!(options.dump_hir_file, Some(expected.clone())); + assert!(matches!(options.dump_hir_opt, Some(DumpHIR::WithoutSnapshot))); + assert!(expected.exists()); + + let _ = std::fs::remove_file(expected); + } + + #[test] + fn parse_dump_hir_path_keeps_format() { + unsafe { OPTIONS = Some(Options::default()); } + + let path = std::path::PathBuf::from("."); + let all = CString::new("dump-hir=all").unwrap(); + let file = CString::new(format!("dump-hir={}", path.display())).unwrap(); + + assert!(parse_option(all.as_ptr()).is_some()); + assert!(parse_option(file.as_ptr()).is_some()); + + let options = unsafe { OPTIONS.as_ref() }.unwrap(); + // parse_option canonicalizes the path, so canonicalize the expectation too + let expected = std::fs::canonicalize(&path).unwrap().join(format!("hir-{}", std::process::id())); + assert_eq!(options.dump_hir_file, Some(expected.clone())); + assert!(matches!(options.dump_hir_opt, Some(DumpHIR::All))); + assert!(expected.exists()); + + let _ = std::fs::remove_file(expected); + } + #[test] fn parse_dump_disasm_path() { unsafe { OPTIONS = Some(Options::default()); }