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/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/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/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", "<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) { 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/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(|| { 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()); }