diff --git a/.github/workflows/spec_guards.yml b/.github/workflows/spec_guards.yml index bfbe8296845fd5..edbd268d0f0b19 100644 --- a/.github/workflows/spec_guards.yml +++ b/.github/workflows/spec_guards.yml @@ -42,6 +42,7 @@ jobs: - ruby-3.2 - ruby-3.3 - ruby-3.4 + fail-fast: false steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 diff --git a/NEWS.md b/NEWS.md index 96da441a627813..7722f3fe00c42a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -191,7 +191,7 @@ The following default gems are updated. * date 3.5.0 * digest 3.2.1 * english 0.8.1 -* erb 5.1.3 +* erb 6.0.0 * etc 1.4.6 * fcntl 1.3.0 * fileutils 1.8.0 diff --git a/ext/io/wait/wait.c b/ext/io/wait/wait.c index f27d7c11e5db48..cf36346cf2f955 100644 --- a/ext/io/wait/wait.c +++ b/ext/io/wait/wait.c @@ -11,6 +11,8 @@ **********************************************************************/ +#include "ruby.h" /* abi_version */ + /* * IO wait methods are built in ruby now, just for backward compatibility. */ diff --git a/gems/bundled_gems b/gems/bundled_gems index e7b5091bcc7e2e..3245e9efbc6ae7 100644 --- a/gems/bundled_gems +++ b/gems/bundled_gems @@ -18,7 +18,7 @@ net-pop 0.1.2 https://github.com/ruby/net-pop net-smtp 0.5.1 https://github.com/ruby/net-smtp matrix 0.4.3 https://github.com/ruby/matrix prime 0.1.4 https://github.com/ruby/prime -rbs 3.9.5 https://github.com/ruby/rbs +rbs 3.9.5 https://github.com/ruby/rbs 22451ecbe262326176eb3915b64366712930685c # waiting for https://github.com/ruby/rbs/pull/2706 typeprof 0.31.0 https://github.com/ruby/typeprof debug 1.11.0 https://github.com/ruby/debug racc 1.8.1 https://github.com/ruby/racc diff --git a/lib/erb.rb b/lib/erb.rb index b1f4e9361b6384..445d4795b018bb 100644 --- a/lib/erb.rb +++ b/lib/erb.rb @@ -778,9 +778,6 @@ # [template processor]: https://en.wikipedia.org/wiki/Template_processor # class ERB - Revision = '$Date:: $' # :nodoc: #' - deprecate_constant :Revision - # :markup: markdown # # :call-seq: @@ -827,46 +824,12 @@ def self.version # # It's good practice to choose a variable name that begins with an underscore: `'_'`. # - # Backward Compatibility - # - # The calling sequence given above -- which is the one you should use -- - # is a simplified version of the complete formal calling sequence, - # which is: - # - # ``` - # ERB.new(template, - # safe_level=NOT_GIVEN, legacy_trim_mode=NOT_GIVEN, legacy_eoutvar=NOT_GIVEN, - # trim_mode: nil, eoutvar: '_erbout') - # ``` - # - # The second, third, and fourth positional arguments (those in the second line above) are deprecated; - # this method issues warnings if they are given. - # - # However, their values, if given, are handled thus: - # - # - `safe_level`: ignored. - # - `legacy_trim_mode`: overrides keyword argument `trim_mode`. - # - `legacy_eoutvar`: overrides keyword argument `eoutvar`. - # # [blank line control]: rdoc-ref:ERB@Suppressing+Unwanted+Blank+Lines # [combine trim modes]: rdoc-ref:ERB@Combining+Trim+Modes # [newline control]: rdoc-ref:ERB@Suppressing+Unwanted+Newlines # [shorthand format]: rdoc-ref:ERB@Shorthand+Format+for+Execution+Tags # - def initialize(str, safe_level=NOT_GIVEN, legacy_trim_mode=NOT_GIVEN, legacy_eoutvar=NOT_GIVEN, trim_mode: nil, eoutvar: '_erbout') - # Complex initializer for $SAFE deprecation at [Feature #14256]. Use keyword arguments to pass trim_mode or eoutvar. - if safe_level != NOT_GIVEN - warn 'Passing safe_level with the 2nd argument of ERB.new is deprecated. Do not use it, and specify other arguments as keyword arguments.', uplevel: 1 - end - if legacy_trim_mode != NOT_GIVEN - warn 'Passing trim_mode with the 3rd argument of ERB.new is deprecated. Use keyword argument like ERB.new(str, trim_mode: ...) instead.', uplevel: 1 - trim_mode = legacy_trim_mode - end - if legacy_eoutvar != NOT_GIVEN - warn 'Passing eoutvar with the 4th argument of ERB.new is deprecated. Use keyword argument like ERB.new(str, eoutvar: ...) instead.', uplevel: 1 - eoutvar = legacy_eoutvar - end - + def initialize(str, trim_mode: nil, eoutvar: '_erbout') compiler = make_compiler(trim_mode) set_eoutvar(compiler, eoutvar) @src, @encoding, @frozen_string = *compiler.compile(str) @@ -875,12 +838,6 @@ def initialize(str, safe_level=NOT_GIVEN, legacy_trim_mode=NOT_GIVEN, legacy_eou @_init = self.class.singleton_class end - # :markup: markdown - # - # Placeholder constant; used as default value for certain method arguments. - NOT_GIVEN = defined?(Ractor) ? Ractor.make_shareable(Object.new) : Object.new - private_constant :NOT_GIVEN - # :markup: markdown # # :call-seq: @@ -894,7 +851,6 @@ def initialize(str, safe_level=NOT_GIVEN, legacy_trim_mode=NOT_GIVEN, legacy_eou # # => # # ``` # - def make_compiler(trim_mode) ERB::Compiler.new(trim_mode) end @@ -1211,7 +1167,6 @@ def def_module(methodname='erb') # # ``` # - # def def_class(superklass=Object, methodname='result') cls = Class.new(superklass) def_method(cls, methodname, @filename || '(ERB)') diff --git a/lib/erb/version.rb b/lib/erb/version.rb index 138bf3988206fc..3794e8288b1bb6 100644 --- a/lib/erb/version.rb +++ b/lib/erb/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class ERB # The string \ERB version. - VERSION = '5.1.3' + VERSION = '6.0.0' end diff --git a/lib/rubygems/platform.rb b/lib/rubygems/platform.rb index e30c266fab1c39..f0eae47c54281b 100644 --- a/lib/rubygems/platform.rb +++ b/lib/rubygems/platform.rb @@ -146,6 +146,37 @@ def to_s to_a.compact.join(@cpu.nil? ? "" : "-") end + ## + # Deconstructs the platform into an array for pattern matching. + # Returns [cpu, os, version]. + # + # Gem::Platform.new("x86_64-linux").deconstruct #=> ["x86_64", "linux", nil] + # + # This enables array pattern matching: + # + # case Gem::Platform.new("arm64-darwin-21") + # in ["arm64", "darwin", version] + # # version => "21" + # end + alias_method :deconstruct, :to_a + + ## + # Deconstructs the platform into a hash for pattern matching. + # Returns a hash with keys +:cpu+, +:os+, and +:version+. + # + # Gem::Platform.new("x86_64-darwin-20").deconstruct_keys(nil) + # #=> { cpu: "x86_64", os: "darwin", version: "20" } + # + # This enables hash pattern matching: + # + # case Gem::Platform.new("x86_64-linux") + # in cpu: "x86_64", os: "linux" + # # Matches Linux on x86_64 + # end + def deconstruct_keys(keys) + { cpu: @cpu, os: @os, version: @version } + end + ## # Is +other+ equal to this platform? Two platforms are equal if they have # the same CPU, OS and version. diff --git a/spec/ruby/library/erb/new_spec.rb b/spec/ruby/library/erb/new_spec.rb index ec1be5c2342f04..f8192bff9925cd 100644 --- a/spec/ruby/library/erb/new_spec.rb +++ b/spec/ruby/library/erb/new_spec.rb @@ -139,17 +139,19 @@ ->{ ERB.new("<%= list %>").result }.should raise_error(NameError) end - describe "warning about arguments" do - it "warns when passed safe_level and later arguments" do - -> { - ERB.new(@eruby_str, nil, '%') - }.should complain(/warning: Passing safe_level with the 2nd argument of ERB.new is deprecated. Do not use it, and specify other arguments as keyword arguments./) - end - - it "does not warn when passed arguments as keyword argument" do - -> { - ERB.new(@eruby_str, trim_mode: '%') - }.should_not complain(/warning: Passing safe_level with the 2nd argument of ERB.new is deprecated. Do not use it, and specify other arguments as keyword arguments./) + version_is ERB.const_get(:VERSION, false), ""..."6.0.0" do + describe "warning about arguments" do + it "warns when passed safe_level and later arguments" do + -> { + ERB.new(@eruby_str, nil, '%') + }.should complain(/warning: Passing safe_level with the 2nd argument of ERB.new is deprecated. Do not use it, and specify other arguments as keyword arguments./) + end + + it "does not warn when passed arguments as keyword argument" do + -> { + ERB.new(@eruby_str, trim_mode: '%') + }.should_not complain(/warning: Passing safe_level with the 2nd argument of ERB.new is deprecated. Do not use it, and specify other arguments as keyword arguments./) + end end end end diff --git a/string.c b/string.c index 531b1c5594381b..c58573dc78d459 100644 --- a/string.c +++ b/string.c @@ -7097,10 +7097,12 @@ rb_str_to_f(VALUE str) /* * call-seq: - * to_s -> self or string + * to_s -> self or new_string * * Returns +self+ if +self+ is a +String+, * or +self+ converted to a +String+ if +self+ is a subclass of +String+. + * + * Related: see {Converting to New String}[rdoc-ref:String@Converting+to+New+String]. */ static VALUE @@ -8639,17 +8641,15 @@ rb_str_tr_bang(VALUE str, VALUE src, VALUE repl) * * Arguments +selector+ and +replacements+ must be valid character selectors * (see {Character Selectors}[rdoc-ref:character_selectors.rdoc]), - * and may use any of its valid forms, including negation, ranges, and escaping: + * and may use any of its valid forms, including negation, ranges, and escapes: * - * # Negation. - * 'hello'.tr('^aeiou', '-') # => "-e--o" - * # Ranges. - * 'ibm'.tr('b-z', 'a-z') # => "hal" - * # Escapes. + * 'hello'.tr('^aeiou', '-') # => "-e--o" # Negation. + * 'ibm'.tr('b-z', 'a-z') # => "hal" # Range. * 'hel^lo'.tr('\^aeiou', '-') # => "h-l-l-" # Escaped leading caret. * 'i-b-m'.tr('b\-z', 'a-z') # => "ibabm" # Escaped embedded hyphen. * 'foo\\bar'.tr('ab\\', 'XYZ') # => "fooZYXr" # Escaped backslash. * + * Related: see {Converting to New String}[rdoc-ref:String@Converting+to+New+String]. */ static VALUE diff --git a/test/erb/test_erb.rb b/test/erb/test_erb.rb index c0df690ccebe97..09496d31e25ca2 100644 --- a/test/erb/test_erb.rb +++ b/test/erb/test_erb.rb @@ -24,29 +24,6 @@ def test_with_filename assert_match(/\Atest filename:1\b/, e.backtrace[0]) end - # [deprecated] This will be removed later - def test_without_filename_with_safe_level - erb = EnvUtil.suppress_warning do - ERB.new("<% raise ::TestERB::MyError %>", 1) - end - e = assert_raise(MyError) { - erb.result - } - assert_match(/\A\(erb\):1\b/, e.backtrace[0]) - end - - # [deprecated] This will be removed later - def test_with_filename_and_safe_level - erb = EnvUtil.suppress_warning do - ERB.new("<% raise ::TestERB::MyError %>", 1) - end - erb.filename = "test filename" - e = assert_raise(MyError) { - erb.result - } - assert_match(/\Atest filename:1\b/, e.backtrace[0]) - end - def test_with_filename_lineno erb = ERB.new("<% raise ::TestERB::MyError %>") erb.filename = "test filename" @@ -117,25 +94,16 @@ def test_version end def test_core - # [deprecated] Fix initializer later - EnvUtil.suppress_warning do - _test_core(nil) - _test_core(0) - _test_core(1) - end - end - - def _test_core(safe) erb = @erb.new("hello") assert_equal("hello", erb.result) - erb = @erb.new("hello", safe, 0) + erb = @erb.new("hello", trim_mode: 0) assert_equal("hello", erb.result) - erb = @erb.new("hello", safe, 1) + erb = @erb.new("hello", trim_mode: 1) assert_equal("hello", erb.result) - erb = @erb.new("hello", safe, 2) + erb = @erb.new("hello", trim_mode: 2) assert_equal("hello", erb.result) src = <') + erb = @erb.new(src, trim_mode: '>') assert_equal(ans.chomp, erb.result) ans = <') + erb = @erb.new(src, trim_mode: '<>') assert_equal(ans, erb.result) ans = <') + erb = @erb.new(src, trim_mode: '%>') assert_equal(ans.chomp, erb.result) ans = <') + erb = @erb.new(src, trim_mode: '%<>') assert_equal(ans, erb.result) end @@ -254,12 +222,6 @@ def test_explicit_trim_line_with_carriage_return assert_equal("line\r\n" * 3, erb.result) end - def test_safe_level_warning - assert_warning(/#{__FILE__}:#{__LINE__ + 1}/) do - @erb.new("", 1) - end - end - def test_invalid_trim_mode pend if RUBY_ENGINE == 'truffleruby' @@ -688,27 +650,6 @@ def test_half_working_comment_backward_compatibility end end - # [deprecated] These interfaces will be removed later - def test_deprecated_interface_warnings - [nil, 0, 1, 2].each do |safe| - assert_warn(/2nd argument of ERB.new is deprecated/) do - ERB.new('', safe) - end - end - - [nil, '', '%', '%<>'].each do |trim| - assert_warn(/3rd argument of ERB.new is deprecated/) do - ERB.new('', nil, trim) - end - end - - [nil, '_erbout', '_hamlout'].each do |eoutvar| - assert_warn(/4th argument of ERB.new is deprecated/) do - ERB.new('', nil, nil, eoutvar) - end - end - end - def test_prohibited_marshal_dump erb = ERB.new("") assert_raise(TypeError) {Marshal.dump(erb)} diff --git a/test/rubygems/package/tar_test_case.rb b/test/rubygems/package/tar_test_case.rb index e3d812bf3fd23d..26135cf296727d 100644 --- a/test/rubygems/package/tar_test_case.rb +++ b/test/rubygems/package/tar_test_case.rb @@ -6,23 +6,7 @@ ## # A test case for Gem::Package::Tar* classes -class Gem::Package::TarTestCase < Gem::TestCase - def ASCIIZ(str, length) - str + "\0" * (length - str.length) - end - - def SP(s) - s + " " - end - - def SP_Z(s) - s + " \0" - end - - def Z(s) - s + "\0" - end - +module Gem::Package::TarTestMethods def assert_headers_equal(expected, actual) expected = expected.to_s unless String === expected actual = actual.to_s unless String === actual @@ -66,6 +50,26 @@ def assert_headers_equal(expected, actual) assert_equal expected[chksum_off, 8], actual[chksum_off, 8] end +end + +class Gem::Package::TarTestCase < Gem::TestCase + include Gem::Package::TarTestMethods + + def ASCIIZ(str, length) + str + "\0" * (length - str.length) + end + + def SP(s) + s + " " + end + + def SP_Z(s) + s + " \0" + end + + def Z(s) + s + "\0" + end def calc_checksum(header) sum = header.sum(0) diff --git a/test/rubygems/test_gem_package_tar_header_ractor.rb b/test/rubygems/test_gem_package_tar_header_ractor.rb index bcd8ab6ad9a603..98fac2802c31d1 100644 --- a/test/rubygems/test_gem_package_tar_header_ractor.rb +++ b/test/rubygems/test_gem_package_tar_header_ractor.rb @@ -8,51 +8,6 @@ end class TestGemPackageTarHeaderRactor < Gem::Package::TarTestCase - ASSERT_HEADERS_EQUAL = <<~RUBY - def assert_headers_equal(expected, actual) - expected = expected.to_s unless String === expected - actual = actual.to_s unless String === actual - - fields = %w[ - name 100 - mode 8 - uid 8 - gid 8 - size 12 - mtime 12 - checksum 8 - typeflag 1 - linkname 100 - magic 6 - version 2 - uname 32 - gname 32 - devmajor 8 - devminor 8 - prefix 155 - ] - - offset = 0 - - until fields.empty? do - name = fields.shift - length = fields.shift.to_i - - if name == "checksum" - chksum_off = offset - offset += length - next - end - - assert_equal expected[offset, length], actual[offset, length] - - offset += length - end - - assert_equal expected[chksum_off, 8], actual[chksum_off, 8] - end - RUBY - SETUP = <<~RUBY header = { name: "x", @@ -78,7 +33,9 @@ class Ractor; alias value take unless method_defined?(:value); end RUBY def test_decode_in_ractor - assert_ractor(ASSERT_HEADERS_EQUAL + SETUP + <<~RUBY, require: "rubygems/package") + assert_ractor(SETUP + <<~RUBY, require: "rubygems/package", require_relative: "package/tar_test_case") + include Gem::Package::TarTestMethods + new_header = Ractor.new(tar_header.to_s) do |str| Gem::Package::TarHeader.from StringIO.new str end.value @@ -88,7 +45,9 @@ def test_decode_in_ractor end def test_encode_in_ractor - assert_ractor(ASSERT_HEADERS_EQUAL + SETUP + <<~RUBY, require: "rubygems/package") + assert_ractor(SETUP + <<~RUBY, require: "rubygems/package", require_relative: "package/tar_test_case") + include Gem::Package::TarTestMethods + header_bytes = tar_header.to_s new_header_bytes = Ractor.new(header_bytes) do |str| diff --git a/test/rubygems/test_gem_platform.rb b/test/rubygems/test_gem_platform.rb index a3ae919809d370..0f1a715ab81ac2 100644 --- a/test/rubygems/test_gem_platform.rb +++ b/test/rubygems/test_gem_platform.rb @@ -683,4 +683,38 @@ def assert_local_match(name) def refute_local_match(name) refute_match Gem::Platform.local, name end + + def test_deconstruct + platform = Gem::Platform.new("x86_64-linux") + assert_equal ["x86_64", "linux", nil], platform.deconstruct + end + + def test_deconstruct_keys + platform = Gem::Platform.new("x86_64-darwin-20") + assert_equal({ cpu: "x86_64", os: "darwin", version: "20" }, platform.deconstruct_keys(nil)) + end + + def test_pattern_matching_array + platform = Gem::Platform.new("arm64-darwin-21") + result = + case platform + in ["arm64", "darwin", version] + version + else + "no match" + end + assert_equal "21", result + end + + def test_pattern_matching_hash + platform = Gem::Platform.new("x86_64-linux") + result = + case platform + in cpu: "x86_64", os: "linux" + "matched" + else + "no match" + end + assert_equal "matched", result + end end diff --git a/tool/test/test_commit_email.rb b/tool/test/test_commit_email.rb index 988a26cc2eab0c..674275279efd08 100644 --- a/tool/test/test_commit_email.rb +++ b/tool/test/test_commit_email.rb @@ -13,7 +13,11 @@ def setup git('init', '--initial-branch=master') git('config', 'user.name', 'Jóhän Grübél') git('config', 'user.email', 'johan@example.com') - env = { 'GIT_AUTHOR_DATE' => '2025-10-08T12:00:00Z', 'TZ' => 'UTC' } + env = { + 'GIT_AUTHOR_DATE' => '2025-10-08T12:00:00Z', + 'GIT_CONFIG_GLOBAL' => IO::NULL, + 'TZ' => 'UTC', + } git('commit', '--allow-empty', '-m', 'New repository initialized by cvs2svn.', env:) git('commit', '--allow-empty', '-m', 'Initial revision', env:) git('commit', '--allow-empty', '-m', 'version 1.0.0', env:) diff --git a/vm_core.h b/vm_core.h index 79d12f9ee44ad6..9f794d867841da 100644 --- a/vm_core.h +++ b/vm_core.h @@ -844,7 +844,7 @@ extern bool ruby_vm_during_cleanup; #define RUBY_VM_FIBER_MACHINE_STACK_SIZE_MIN ( 16 * 1024 * sizeof(VALUE)) /* 64 KB or 128 KB */ #endif -#if __has_feature(memory_sanitizer) || __has_feature(address_sanitizer) +#if __has_feature(memory_sanitizer) || __has_feature(address_sanitizer) || __has_feature(leak_sanitizer) /* It seems sanitizers consume A LOT of machine stacks */ #undef RUBY_VM_THREAD_MACHINE_STACK_SIZE #define RUBY_VM_THREAD_MACHINE_STACK_SIZE (1024 * 1024 * sizeof(VALUE)) diff --git a/zjit/src/json.rs b/zjit/src/json.rs new file mode 100644 index 00000000000000..1e63ba7b425902 --- /dev/null +++ b/zjit/src/json.rs @@ -0,0 +1,700 @@ +//! Single file JSON serializer for iongraph output of ZJIT HIR. + +use std::{ + fmt, + io::{self, Write}, +}; + +pub trait Jsonable { + fn to_json(&self) -> Json; +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Json { + Null, + Bool(bool), + Integer(isize), + UnsignedInteger(usize), + Floating(f64), + String(String), + Array(Vec), + Object(Vec<(String, Json)>), +} + +impl Json { + /// Convenience method for constructing a JSON array. + pub fn array(iter: I) -> Self + where + I: IntoIterator, + T: Into, + { + Json::Array(iter.into_iter().map(Into::into).collect()) + } + + pub fn empty_array() -> Self { + Json::Array(Vec::new()) + } + + pub fn object() -> JsonObjectBuilder { + JsonObjectBuilder::new() + } + + pub fn marshal(&self, writer: &mut W) -> JsonResult<()> { + match self { + Json::Null => writer.write_all(b"null"), + Json::Bool(b) => writer.write_all(if *b { b"true" } else { b"false" }), + Json::Integer(i) => write!(writer, "{}", i), + Json::UnsignedInteger(u) => write!(writer, "{}", u), + Json::Floating(f) => write!(writer, "{}", f), + Json::String(s) => return Self::write_str(writer, s), + Json::Array(jsons) => return Self::write_array(writer, jsons), + Json::Object(map) => return Self::write_object(writer, map), + }?; + Ok(()) + } + + pub fn write_str(writer: &mut W, s: &str) -> JsonResult<()> { + writer.write_all(b"\"")?; + + for ch in s.chars() { + match ch { + '"' => write!(writer, "\\\"")?, + '\\' => write!(writer, "\\\\")?, + // The following characters are control, but have a canonical representation. + // https://datatracker.ietf.org/doc/html/rfc8259#section-7 + '\n' => write!(writer, "\\n")?, + '\r' => write!(writer, "\\r")?, + '\t' => write!(writer, "\\t")?, + '\x08' => write!(writer, "\\b")?, + '\x0C' => write!(writer, "\\f")?, + ch if ch.is_control() => { + let code_point = ch as u32; + write!(writer, "\\u{:04X}", code_point)? + } + _ => write!(writer, "{}", ch)?, + }; + } + + writer.write_all(b"\"")?; + Ok(()) + } + + pub fn write_array(writer: &mut W, jsons: &[Json]) -> JsonResult<()> { + writer.write_all(b"[")?; + let mut prefix = ""; + for item in jsons { + write!(writer, "{}", prefix)?; + item.marshal(writer)?; + prefix = ", "; + } + writer.write_all(b"]")?; + Ok(()) + } + + pub fn write_object(writer: &mut W, pairs: &[(String, Json)]) -> JsonResult<()> { + writer.write_all(b"{")?; + let mut prefix = ""; + for (k, v) in pairs { + // Escape the keys, despite not being `Json::String` objects. + write!(writer, "{}", prefix)?; + Self::write_str(writer, k)?; + writer.write_all(b":")?; + v.marshal(writer)?; + prefix = ", "; + } + writer.write_all(b"}")?; + Ok(()) + } +} + +impl std::fmt::Display for Json { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let mut buf = Vec::new(); + self.marshal(&mut buf).map_err(|_| std::fmt::Error)?; + let s = String::from_utf8(buf).map_err(|_| std::fmt::Error)?; + write!(f, "{}", s) + } +} + +pub struct JsonObjectBuilder { + pairs: Vec<(String, Json)>, +} + +impl JsonObjectBuilder { + pub fn new() -> Self { + Self { pairs: Vec::new() } + } + + pub fn insert(mut self, key: K, value: V) -> Self + where + K: Into, + V: Into, + { + self.pairs.push((key.into(), value.into())); + self + } + + pub fn build(self) -> Json { + Json::Object(self.pairs) + } +} + +impl From<&str> for Json { + fn from(s: &str) -> Json { + Json::String(s.to_string()) + } +} + +impl From for Json { + fn from(s: String) -> Json { + Json::String(s) + } +} + +impl From for Json { + fn from(i: i32) -> Json { + Json::Integer(i as isize) + } +} + +impl From for Json { + fn from(i: i64) -> Json { + Json::Integer(i as isize) + } +} + +impl From for Json { + fn from(u: u32) -> Json { + Json::UnsignedInteger(u as usize) + } +} + +impl From for Json { + fn from(u: u64) -> Json { + Json::UnsignedInteger(u as usize) + } +} + +impl From for Json { + fn from(u: usize) -> Json { + Json::UnsignedInteger(u) + } +} + +impl From for Json { + fn from(b: bool) -> Json { + Json::Bool(b) + } +} + +impl TryFrom for Json { + type Error = JsonError; + fn try_from(f: f64) -> Result { + if f.is_finite() { + Ok(Json::Floating(f)) + } else { + Err(JsonError::FloatError(f)) + } + } +} + +impl> From> for Json { + fn from(v: Vec) -> Json { + Json::Array(v.into_iter().map(|item| item.into()).collect()) + } +} + +/// Convenience type for a result in JSON serialization. +pub type JsonResult = std::result::Result; + +#[derive(Debug)] +pub enum JsonError { + /// Wrapper for a standard `io::Error`. + IoError(io::Error), + /// On attempting to serialize an invalid `f32` or `f64`. + /// Stores invalid values as 64 bit float. + FloatError(f64), +} + +impl From for JsonError { + fn from(err: io::Error) -> Self { + JsonError::IoError(err) + } +} + +impl fmt::Display for JsonError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + JsonError::FloatError(v) => write!(f, "Cannot serialize float {}", v), + JsonError::IoError(e) => write!(f, "{}", e), + } + } +} + +impl std::error::Error for JsonError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + JsonError::IoError(e) => Some(e), + JsonError::FloatError(_) => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use insta::assert_snapshot; + + fn marshal_to_string(json: &Json) -> String { + let mut buf = Vec::new(); + json.marshal(&mut buf).unwrap(); + String::from_utf8(buf).unwrap() + } + + #[test] + fn test_null() { + let json = Json::Null; + assert_snapshot!(marshal_to_string(&json), @"null"); + } + + #[test] + fn test_bool() { + let json: Json = true.into(); + assert_snapshot!(marshal_to_string(&json), @"true"); + let json: Json = false.into(); + assert_snapshot!(marshal_to_string(&json), @"false"); + } + + #[test] + fn test_integer_positive() { + let json: Json = 42.into(); + assert_snapshot!(marshal_to_string(&json), @"42"); + } + + #[test] + fn test_integer_negative() { + let json: Json = (-123).into(); + assert_snapshot!(marshal_to_string(&json), @"-123"); + } + + #[test] + fn test_integer_zero() { + let json: Json = 0.into(); + assert_snapshot!(marshal_to_string(&json), @"0"); + } + + #[test] + fn test_floating() { + let json = 2.14159.try_into(); + assert!(json.is_ok()); + let json = json.unwrap(); + assert_snapshot!(marshal_to_string(&json), @"2.14159"); + } + + #[test] + fn test_floating_negative() { + let json = (-2.5).try_into(); + assert!(json.is_ok()); + let json = json.unwrap(); + assert_snapshot!(marshal_to_string(&json), @"-2.5"); + } + + #[test] + fn test_floating_error() { + let json: Result = f64::NAN.try_into(); + assert!(matches!(json, Err(JsonError::FloatError(_)))); + + let json: Result = f64::INFINITY.try_into(); + assert!(matches!(json, Err(JsonError::FloatError(_)))); + + let json: Result = f64::NEG_INFINITY.try_into(); + assert!(matches!(json, Err(JsonError::FloatError(_)))); + } + + #[test] + fn test_string_simple() { + let json: Json = "hello".into(); + assert_snapshot!(marshal_to_string(&json), @r#""hello""#); + } + + #[test] + fn test_string_empty() { + let json: Json = "".into(); + assert_snapshot!(marshal_to_string(&json), @r#""""#); + } + + #[test] + fn test_string_with_quotes() { + let json: Json = r#"hello "world""#.into(); + assert_snapshot!(marshal_to_string(&json), @r#""hello \"world\"""#); + } + + #[test] + fn test_string_with_backslash() { + let json: Json = r"path\to\file".into(); + assert_snapshot!(marshal_to_string(&json), @r#""path\\to\\file""#); + } + + #[test] + fn test_string_with_slash() { + let json: Json = "path/to/file".into(); + assert_snapshot!(marshal_to_string(&json), @r#""path/to/file""#); + } + + #[test] + fn test_string_with_newline() { + let json: Json = "line1\nline2".into(); + assert_snapshot!(marshal_to_string(&json), @r#""line1\nline2""#); + } + + #[test] + fn test_string_with_carriage_return() { + let json: Json = "line1\rline2".into(); + assert_snapshot!(marshal_to_string(&json), @r#""line1\rline2""#); + } + + #[test] + fn test_string_with_tab() { + let json: Json = "col1\tcol2".into(); + assert_snapshot!(marshal_to_string(&json), @r#""col1\tcol2""#); + } + + #[test] + fn test_string_with_backspace() { + let json: Json = "text\x08back".into(); + assert_snapshot!(marshal_to_string(&json), @r#""text\bback""#); + } + + #[test] + fn test_string_with_form_feed() { + let json: Json = "page\x0Cnew".into(); + assert_snapshot!(marshal_to_string(&json), @r#""page\fnew""#); + } + + #[test] + fn test_string_with_control_chars() { + let json: Json = "test\x01\x02\x03".into(); + assert_snapshot!(marshal_to_string(&json), @r#""test\u0001\u0002\u0003""#); + } + + #[test] + fn test_string_with_all_escapes() { + let json: Json = "\"\\/\n\r\t\x08\x0C".into(); + assert_snapshot!(marshal_to_string(&json), @r#""\"\\/\n\r\t\b\f""#); + } + + #[test] + fn test_array_empty() { + let json: Json = Vec::::new().into(); + assert_snapshot!(marshal_to_string(&json), @"[]"); + } + + #[test] + fn test_array_single_element() { + let json: Json = vec![42].into(); + assert_snapshot!(marshal_to_string(&json), @"[42]"); + } + + #[test] + fn test_array_multiple_elements() { + let json: Json = vec![1, 2, 3].into(); + assert_snapshot!(marshal_to_string(&json), @"[1, 2, 3]"); + } + + #[test] + fn test_array_mixed_types() { + let json = Json::Array(vec![ + Json::Null, + true.into(), + 42.into(), + 3.134.try_into().unwrap(), + "hello".into(), + ]); + assert_snapshot!(marshal_to_string(&json), @r#"[null, true, 42, 3.134, "hello"]"#); + } + + #[test] + fn test_array_nested() { + let json = Json::Array(vec![1.into(), vec![2, 3].into(), 4.into()]); + assert_snapshot!(marshal_to_string(&json), @"[1, [2, 3], 4]"); + } + + #[test] + fn test_object_empty() { + let json = Json::Object(vec![]); + assert_snapshot!(marshal_to_string(&json), @"{}"); + } + + #[test] + fn test_object_single_field() { + let json = Json::Object(vec![("key".to_string(), "value".into())]); + assert_snapshot!(marshal_to_string(&json), @r#"{"key":"value"}"#); + } + + #[test] + fn test_object_multiple_fields() { + let json = Json::Object(vec![ + ("name".to_string(), "Alice".into()), + ("age".to_string(), 30.into()), + ("active".to_string(), true.into()), + ]); + assert_snapshot!(marshal_to_string(&json), @r#"{"name":"Alice", "age":30, "active":true}"#); + } + + #[test] + fn test_object_with_escaped_key() { + let json = Json::Object(vec![("key\nwith\nnewlines".to_string(), 42.into())]); + assert_snapshot!(marshal_to_string(&json), @r#"{"key\nwith\nnewlines":42}"#); + } + + #[test] + fn test_object_nested() { + let inner = Json::Object(vec![("inner_key".to_string(), "inner_value".into())]); + let json = Json::Object(vec![("outer_key".to_string(), inner)]); + assert_snapshot!(marshal_to_string(&json), @r#"{"outer_key":{"inner_key":"inner_value"}}"#); + } + + #[test] + fn test_from_str() { + let json: Json = "test string".into(); + assert_snapshot!(marshal_to_string(&json), @r#""test string""#); + } + + #[test] + fn test_from_i32() { + let json: Json = 42i32.into(); + assert_snapshot!(marshal_to_string(&json), @"42"); + } + + #[test] + fn test_from_i64() { + let json: Json = 9223372036854775807i64.into(); + assert_snapshot!(marshal_to_string(&json), @"9223372036854775807"); + } + + #[test] + fn test_from_u32() { + let json: Json = 42u32.into(); + assert_snapshot!(marshal_to_string(&json), @"42"); + } + + #[test] + fn test_from_u64() { + let json: Json = 18446744073709551615u64.into(); + assert_snapshot!(marshal_to_string(&json), @"18446744073709551615"); + } + + #[test] + fn test_unsigned_integer_zero() { + let json: Json = 0u64.into(); + assert_snapshot!(marshal_to_string(&json), @"0"); + } + + #[test] + fn test_from_bool() { + let json_true: Json = true.into(); + let json_false: Json = false.into(); + assert_snapshot!(marshal_to_string(&json_true), @"true"); + assert_snapshot!(marshal_to_string(&json_false), @"false"); + } + + #[test] + fn test_from_vec() { + let json: Json = vec![1i32, 2i32, 3i32].into(); + assert_snapshot!(marshal_to_string(&json), @"[1, 2, 3]"); + } + + #[test] + fn test_from_vec_strings() { + let json: Json = vec!["a", "b", "c"].into(); + assert_snapshot!(marshal_to_string(&json), @r#"["a", "b", "c"]"#); + } + + #[test] + fn test_complex_nested_structure() { + let settings = Json::Object(vec![ + ("notifications".to_string(), true.into()), + ("theme".to_string(), "dark".into()), + ]); + + let json = Json::Object(vec![ + ("id".to_string(), 1.into()), + ("name".to_string(), "Alice".into()), + ("tags".to_string(), vec!["admin", "user"].into()), + ("settings".to_string(), settings), + ]); + assert_snapshot!(marshal_to_string(&json), @r#"{"id":1, "name":"Alice", "tags":["admin", "user"], "settings":{"notifications":true, "theme":"dark"}}"#); + } + + #[test] + fn test_deeply_nested_arrays() { + let json = Json::Array(vec![ + Json::Array(vec![vec![1, 2].into(), 3.into()]), + 4.into(), + ]); + assert_snapshot!(marshal_to_string(&json), @"[[[1, 2], 3], 4]"); + } + + #[test] + fn test_unicode_string() { + let json: Json = "兵马俑".into(); + assert_snapshot!(marshal_to_string(&json), @r#""兵马俑""#); + } + + #[test] + fn test_json_array_convenience() { + let json = Json::array(vec![1, 2, 3]); + assert_snapshot!(marshal_to_string(&json), @"[1, 2, 3]"); + } + + #[test] + fn test_json_array_from_iterator() { + let json = Json::array([1, 2, 3].iter().map(|&x| x * 2)); + assert_snapshot!(marshal_to_string(&json), @"[2, 4, 6]"); + } + + #[test] + fn test_json_empty_array() { + let json = Json::empty_array(); + assert_snapshot!(marshal_to_string(&json), @"[]"); + } + + #[test] + fn test_object_builder_empty() { + let json = Json::object().build(); + assert_snapshot!(marshal_to_string(&json), @"{}"); + } + + #[test] + fn test_object_builder_single_field() { + let json = Json::object().insert("key", "value").build(); + assert_snapshot!(marshal_to_string(&json), @r#"{"key":"value"}"#); + } + + #[test] + fn test_object_builder_multiple_fields() { + let json = Json::object() + .insert("name", "Alice") + .insert("age", 30) + .insert("active", true) + .build(); + assert_snapshot!(marshal_to_string(&json), @r#"{"name":"Alice", "age":30, "active":true}"#); + } + + #[test] + fn test_object_builder_with_nested_objects() { + let inner = Json::object().insert("inner_key", "inner_value").build(); + let json = Json::object().insert("outer_key", inner).build(); + assert_snapshot!(marshal_to_string(&json), @r#"{"outer_key":{"inner_key":"inner_value"}}"#); + } + + #[test] + fn test_object_builder_with_array() { + let json = Json::object().insert("items", vec![1, 2, 3]).build(); + assert_snapshot!(marshal_to_string(&json), @r#"{"items":[1, 2, 3]}"#); + } + + #[test] + fn test_display_trait() { + let json = Json::object() + .insert("name", "Bob") + .insert("count", 42) + .build(); + let display_output = format!("{}", json); + assert_snapshot!(display_output, @r#"{"name":"Bob", "count":42}"#); + } + + #[test] + fn test_display_trait_array() { + let json: Json = vec![1, 2, 3].into(); + let display_output = format!("{}", json); + assert_snapshot!(display_output, @"[1, 2, 3]"); + } + + #[test] + fn test_display_trait_string() { + let json: Json = "test".into(); + let display_output = format!("{}", json); + assert_snapshot!(display_output, @r#""test""#); + } + + #[test] + fn test_from_usize() { + let json: Json = 123usize.into(); + assert_snapshot!(marshal_to_string(&json), @"123"); + } + + #[test] + fn test_from_usize_large() { + let json: Json = usize::MAX.into(); + let expected = format!("{}", usize::MAX); + assert_eq!(marshal_to_string(&json), expected); + } + + #[test] + fn test_json_error_float_display() { + let err = JsonError::FloatError(f64::NAN); + let display_output = format!("{}", err); + assert!(display_output.contains("Cannot serialize float")); + assert!(display_output.contains("NaN")); + } + + #[test] + fn test_json_error_float_display_infinity() { + let err = JsonError::FloatError(f64::INFINITY); + let display_output = format!("{}", err); + assert_snapshot!(display_output, @"Cannot serialize float inf"); + } + + #[test] + fn test_json_error_io_display() { + let io_err = io::Error::new(io::ErrorKind::WriteZero, "write error"); + let err = JsonError::IoError(io_err); + let display_output = format!("{}", err); + assert_snapshot!(display_output, @"write error"); + } + + #[test] + fn test_io_error_during_marshal() { + struct FailingWriter; + impl Write for FailingWriter { + fn write(&mut self, _buf: &[u8]) -> io::Result { + Err(io::Error::other("simulated write failure")) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + let json: Json = "test".into(); + let mut writer = FailingWriter; + let result = json.marshal(&mut writer); + assert!(result.is_err()); + assert!(matches!(result, Err(JsonError::IoError(_)))); + } + + #[test] + fn test_clone_json() { + let json1: Json = vec![1, 2, 3].into(); + let json2 = json1.clone(); + assert_eq!(json1, json2); + } + + #[test] + fn test_debug_json() { + let json: Json = "test".into(); + let debug_output = format!("{:?}", json); + assert!(debug_output.contains("String")); + assert!(debug_output.contains("test")); + } + + #[test] + fn test_partial_eq_json() { + let json1: Json = 42.into(); + let json2: Json = 42.into(); + let json3: Json = 43.into(); + assert_eq!(json1, json2); + assert_ne!(json1, json3); + } +} diff --git a/zjit/src/lib.rs b/zjit/src/lib.rs index f8ff380148004e..88e7af02b75013 100644 --- a/zjit/src/lib.rs +++ b/zjit/src/lib.rs @@ -29,3 +29,4 @@ mod invariants; mod bitset; mod gc; mod payload; +mod json;